From e25af1030cfbd64bb9c668545e524e466566c239 Mon Sep 17 00:00:00 2001 From: acentelles Date: Thu, 16 Jul 2026 12:48:51 -0400 Subject: [PATCH 01/38] feat(field): add the Solinas backend to jolt-field Move the Solinas field stack from Akita revision 3b674abb66186037745af256e6a20d60bdcd7e89 into jolt-field behind a new `solinas` feature: 32/64/128-bit pseudo-Mersenne prime fields, degree 2/4/8 extension fields, packed NEON/AVX2/AVX-512 backends, unreduced accumulators, smooth-domain FFT helpers, and a `parallel` feature gating rayon. Jolt's existing capability traits remain canonical; Solinas-only operations live in narrow capability traits in solinas_traits.rs. FieldError carries field-layer input and size failures only. The temporary `akita` feature, its optional akita-config/akita-field git-pinned dependencies, and src/akita.rs are retained as a bootstrap edge so the current jolt-akita revision keeps building; they are removed in the final migration PR. --- Cargo.lock | 12 +- crates/jolt-field/Cargo.toml | 13 +- crates/jolt-field/src/ext/fp_ext2.rs | 521 ++++++++ crates/jolt-field/src/ext/fp_ext4.rs | 676 ++++++++++ crates/jolt-field/src/ext/fp_ext8.rs | 486 ++++++++ crates/jolt-field/src/ext/lift.rs | 480 +++++++ crates/jolt-field/src/ext/mod.rs | 45 + crates/jolt-field/src/ext/native_algebra.rs | 229 ++++ crates/jolt-field/src/ext/tests.rs | 574 +++++++++ crates/jolt-field/src/fft.rs | 1098 +++++++++++++++++ crates/jolt-field/src/field_error.rs | 16 + crates/jolt-field/src/lib.rs | 50 + crates/jolt-field/src/packed/avx2/fp128.rs | 231 ++++ crates/jolt-field/src/packed/avx2/fp32.rs | 689 +++++++++++ crates/jolt-field/src/packed/avx2/fp64.rs | 267 ++++ crates/jolt-field/src/packed/avx2/mod.rs | 65 + crates/jolt-field/src/packed/avx512/fp128.rs | 206 ++++ crates/jolt-field/src/packed/avx512/fp32.rs | 682 ++++++++++ crates/jolt-field/src/packed/avx512/fp64.rs | 246 ++++ crates/jolt-field/src/packed/avx512/mod.rs | 64 + crates/jolt-field/src/packed/ext/mod.rs | 480 +++++++ crates/jolt-field/src/packed/ext/tests.rs | 484 ++++++++ crates/jolt-field/src/packed/mod.rs | 418 +++++++ crates/jolt-field/src/packed/neon/fp128.rs | 314 +++++ crates/jolt-field/src/packed/neon/fp32.rs | 824 +++++++++++++ crates/jolt-field/src/packed/neon/fp64.rs | 224 ++++ crates/jolt-field/src/packed/neon/mod.rs | 43 + crates/jolt-field/src/packed/tests.rs | 344 ++++++ crates/jolt-field/src/parallel.rs | 104 ++ crates/jolt-field/src/prime/fp128/add_sub.rs | 409 ++++++ crates/jolt-field/src/prime/fp128/core.rs | 126 ++ crates/jolt-field/src/prime/fp128/mod.rs | 64 + crates/jolt-field/src/prime/fp128/mul.rs | 376 ++++++ crates/jolt-field/src/prime/fp128/primes.rs | 49 + crates/jolt-field/src/prime/fp128/reduce.rs | 195 +++ crates/jolt-field/src/prime/fp128/tests.rs | 194 +++ crates/jolt-field/src/prime/fp128/traits.rs | 186 +++ crates/jolt-field/src/prime/fp128/wide.rs | 302 +++++ crates/jolt-field/src/prime/fp32.rs | 631 ++++++++++ crates/jolt-field/src/prime/fp64.rs | 630 ++++++++++ crates/jolt-field/src/prime/mod.rs | 32 + crates/jolt-field/src/prime/native_algebra.rs | 88 ++ .../jolt-field/src/prime/native_capability.rs | 210 ++++ .../jolt-field/src/prime/pseudo_mersenne.rs | 174 +++ crates/jolt-field/src/prime/util.rs | 46 + crates/jolt-field/src/solinas_traits.rs | 64 + crates/jolt-field/src/unreduced/accum.rs | 543 ++++++++ crates/jolt-field/src/unreduced/mod.rs | 810 ++++++++++++ .../src/unreduced/native_algebra.rs | 94 ++ crates/jolt-field/src/unreduced/tests.rs | 262 ++++ 50 files changed, 15363 insertions(+), 7 deletions(-) create mode 100644 crates/jolt-field/src/ext/fp_ext2.rs create mode 100644 crates/jolt-field/src/ext/fp_ext4.rs create mode 100644 crates/jolt-field/src/ext/fp_ext8.rs create mode 100644 crates/jolt-field/src/ext/lift.rs create mode 100644 crates/jolt-field/src/ext/mod.rs create mode 100644 crates/jolt-field/src/ext/native_algebra.rs create mode 100644 crates/jolt-field/src/ext/tests.rs create mode 100644 crates/jolt-field/src/fft.rs create mode 100644 crates/jolt-field/src/field_error.rs create mode 100644 crates/jolt-field/src/packed/avx2/fp128.rs create mode 100644 crates/jolt-field/src/packed/avx2/fp32.rs create mode 100644 crates/jolt-field/src/packed/avx2/fp64.rs create mode 100644 crates/jolt-field/src/packed/avx2/mod.rs create mode 100644 crates/jolt-field/src/packed/avx512/fp128.rs create mode 100644 crates/jolt-field/src/packed/avx512/fp32.rs create mode 100644 crates/jolt-field/src/packed/avx512/fp64.rs create mode 100644 crates/jolt-field/src/packed/avx512/mod.rs create mode 100644 crates/jolt-field/src/packed/ext/mod.rs create mode 100644 crates/jolt-field/src/packed/ext/tests.rs create mode 100644 crates/jolt-field/src/packed/mod.rs create mode 100644 crates/jolt-field/src/packed/neon/fp128.rs create mode 100644 crates/jolt-field/src/packed/neon/fp32.rs create mode 100644 crates/jolt-field/src/packed/neon/fp64.rs create mode 100644 crates/jolt-field/src/packed/neon/mod.rs create mode 100644 crates/jolt-field/src/packed/tests.rs create mode 100644 crates/jolt-field/src/parallel.rs create mode 100644 crates/jolt-field/src/prime/fp128/add_sub.rs create mode 100644 crates/jolt-field/src/prime/fp128/core.rs create mode 100644 crates/jolt-field/src/prime/fp128/mod.rs create mode 100644 crates/jolt-field/src/prime/fp128/mul.rs create mode 100644 crates/jolt-field/src/prime/fp128/primes.rs create mode 100644 crates/jolt-field/src/prime/fp128/reduce.rs create mode 100644 crates/jolt-field/src/prime/fp128/tests.rs create mode 100644 crates/jolt-field/src/prime/fp128/traits.rs create mode 100644 crates/jolt-field/src/prime/fp128/wide.rs create mode 100644 crates/jolt-field/src/prime/fp32.rs create mode 100644 crates/jolt-field/src/prime/fp64.rs create mode 100644 crates/jolt-field/src/prime/mod.rs create mode 100644 crates/jolt-field/src/prime/native_algebra.rs create mode 100644 crates/jolt-field/src/prime/native_capability.rs create mode 100644 crates/jolt-field/src/prime/pseudo_mersenne.rs create mode 100644 crates/jolt-field/src/prime/util.rs create mode 100644 crates/jolt-field/src/solinas_traits.rs create mode 100644 crates/jolt-field/src/unreduced/accum.rs create mode 100644 crates/jolt-field/src/unreduced/mod.rs create mode 100644 crates/jolt-field/src/unreduced/native_algebra.rs create mode 100644 crates/jolt-field/src/unreduced/tests.rs diff --git a/Cargo.lock b/Cargo.lock index c740a34089..adedaf8f76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2410,7 +2410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2991,7 +2991,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3277,7 +3277,9 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_core 0.6.4", + "rayon", "serde", + "thiserror 2.0.18", ] [[package]] @@ -5747,7 +5749,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6535,7 +6537,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7129,7 +7131,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/crates/jolt-field/Cargo.toml b/crates/jolt-field/Cargo.toml index 8e6bfe20c7..cabbe5a875 100644 --- a/crates/jolt-field/Cargo.toml +++ b/crates/jolt-field/Cargo.toml @@ -3,15 +3,18 @@ name = "jolt-field" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" -description = "Field abstractions for the Jolt zkVM" +description = "Shared field abstractions and optimized BN254 and Solinas backends for Jolt" repository = "https://github.com/a16z/jolt" -keywords = ["SNARK", "cryptography", "finite-fields", "BN254"] +keywords = ["SNARK", "cryptography", "finite-fields", "BN254", "Solinas"] categories = ["cryptography"] [lints] workspace = true [dependencies] +# Temporary bootstrap edge for the staged Akita migration: keeps the legacy +# `akita` adapter buildable until the Akita cutover lands. Removed together +# with `src/akita.rs` in the final migration PR. akita-config = { workspace = true, optional = true } akita-field = { workspace = true, optional = true } ark-ff = { workspace = true, optional = true } @@ -22,11 +25,17 @@ serde = { workspace = true, features = ["derive"] } allocative = { workspace = true, optional = true } rand = { workspace = true } rand_core = { workspace = true } +rayon = { workspace = true, optional = true } +thiserror = { workspace = true } [features] +# Preserve Jolt's existing BN254 default during the coordinated cutover. default = ["bn254"] +# Temporary bootstrap feature; see the akita-* dependency note above. akita = ["dep:akita-config", "dep:akita-field"] bn254 = ["dep:ark-ff", "dep:ark-serialize", "dep:ark-bn254"] +solinas = [] +parallel = ["dep:rayon"] allocative = ["dep:allocative"] [dev-dependencies] diff --git a/crates/jolt-field/src/ext/fp_ext2.rs b/crates/jolt-field/src/ext/fp_ext2.rs new file mode 100644 index 0000000000..f7660e3241 --- /dev/null +++ b/crates/jolt-field/src/ext/fp_ext2.rs @@ -0,0 +1,521 @@ +use super::*; + +/// `FpExt2Config` with non-residue = -1. +/// +/// Valid when `p ≡ 3 (mod 4)`, i.e. -1 is a quadratic non-residue. +pub struct NegOneNr; + +impl FpExt2Config for NegOneNr { + const IS_NEG_ONE: bool = true; + + fn non_residue() -> F { + -F::one() + } +} + +/// `FpExt2Config` with non-residue = 2. +/// +/// Valid when `p ≡ 5 (mod 8)`, i.e. 2 is a quadratic non-residue. +/// All Akita pseudo-Mersenne primes (`2^k - c` with `c ≡ 3 mod 8`) +/// satisfy this. +pub struct TwoNr; + +impl FpExt2Config for TwoNr { + fn non_residue() -> F { + F::from_u64(2) + } + + #[inline] + fn mul_non_residue(x: A, _from_base: B) -> A + where + A: ExtensionCoeff, + B: FnOnce(F) -> A, + { + x + x + } +} + +/// Parameters for an `FpExt2` quadratic extension over base field `F`. +pub trait FpExt2Config { + /// Whether the non-residue is -1. + /// + /// When `true`, multiplication by the non-residue is a free negation and + /// the Karatsuba/squaring routines can avoid a base-field multiply. + const IS_NEG_ONE: bool = false; + + /// Non-residue `NR` such that `u^2 = NR`. + fn non_residue() -> F; + + /// Multiply a coefficient by the quadratic non-residue. + #[inline] + fn mul_non_residue(x: A, from_base: B) -> A + where + A: ExtensionCoeff, + B: FnOnce(F) -> A, + { + if Self::IS_NEG_ONE { + from_base(F::zero()) - x + } else { + from_base(Self::non_residue()) * x + } + } +} + +/// Quadratic extension element `c0 + c1 * u` with `u^2 = NR`. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[cfg_attr( + feature = "allocative", + allocative(bound = "F: FieldCore + allocative::Allocative, C: FpExt2Config") +)] +#[repr(transparent)] +pub struct FpExt2> { + /// Coefficients `[c0, c1]` in basis `[1, u]`. + pub coeffs: [F; 2], + _cfg: PhantomData C>, +} + +impl> FpExt2 { + /// Construct `c0 + c1 * u`. + #[inline] + pub fn new(c0: F, c1: F) -> Self { + Self { + coeffs: [c0, c1], + _cfg: PhantomData, + } + } + + /// Degree-0 coefficient. + #[inline] + pub fn c0(&self) -> F { + self.coeffs[0] + } + + /// Degree-1 coefficient. + #[inline] + pub fn c1(&self) -> F { + self.coeffs[1] + } + + /// Additive identity. + #[inline] + pub fn zero() -> Self { + Self::new(F::zero(), F::zero()) + } + + /// Multiplicative identity. + #[inline] + pub fn one() -> Self { + Self::new(F::one(), F::zero()) + } + + /// Check whether this element is zero. + #[inline] + pub fn is_zero(&self) -> bool { + self.coeffs[0].is_zero() && self.coeffs[1].is_zero() + } + + /// Construct from a `u64` embedded in the base field. + #[inline] + pub fn from_u64(val: u64) -> Self + where + F: FromPrimitiveInt, + { + Self::new(F::from_u64(val), F::zero()) + } + + /// Construct from an `i64` embedded in the base field. + #[inline] + pub fn from_i64(val: i64) -> Self + where + F: FromPrimitiveInt, + { + Self::new(F::from_i64(val), F::zero()) + } + + /// Multiply a base-field element by the non-residue. + /// + /// When `IS_NEG_ONE` is true this is just a negation (no multiply). + #[inline(always)] + fn mul_nr(x: F) -> F { + C::mul_non_residue(x, |base| base) + } + + /// Return the conjugate `c0 - c1 * u`. + #[inline] + pub fn conjugate(self) -> Self { + Self::new(self.coeffs[0], -self.coeffs[1]) + } + + /// Return the norm in the base field: `c0^2 - NR * c1^2`. + #[inline] + pub fn norm(self) -> F { + (self.coeffs[0] * self.coeffs[0]) - Self::mul_nr(self.coeffs[1] * self.coeffs[1]) + } +} + +impl> std::fmt::Debug for FpExt2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FpExt2") + .field("coeffs", &self.coeffs) + .finish() + } +} + +impl> Clone for FpExt2 { + fn clone(&self) -> Self { + *self + } +} + +impl> Copy for FpExt2 {} + +impl> Default for FpExt2 { + fn default() -> Self { + Self::new(F::zero(), F::zero()) + } +} + +impl> PartialEq for FpExt2 { + fn eq(&self, other: &Self) -> bool { + self.coeffs[0] == other.coeffs[0] && self.coeffs[1] == other.coeffs[1] + } +} + +impl> Eq for FpExt2 {} + +impl> Add for FpExt2 { + type Output = Self; + #[inline(always)] + fn add(self, rhs: Self) -> Self::Output { + Self::new( + self.coeffs[0] + rhs.coeffs[0], + self.coeffs[1] + rhs.coeffs[1], + ) + } +} +impl> Sub for FpExt2 { + type Output = Self; + #[inline(always)] + fn sub(self, rhs: Self) -> Self::Output { + Self::new( + self.coeffs[0] - rhs.coeffs[0], + self.coeffs[1] - rhs.coeffs[1], + ) + } +} +impl> Neg for FpExt2 { + type Output = Self; + #[inline(always)] + fn neg(self) -> Self::Output { + Self::new(-self.coeffs[0], -self.coeffs[1]) + } +} +impl> AddAssign for FpExt2 { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.coeffs[0] = self.coeffs[0] + rhs.coeffs[0]; + self.coeffs[1] = self.coeffs[1] + rhs.coeffs[1]; + } +} +impl> SubAssign for FpExt2 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.coeffs[0] = self.coeffs[0] - rhs.coeffs[0]; + self.coeffs[1] = self.coeffs[1] - rhs.coeffs[1]; + } +} +impl> Mul for FpExt2 { + type Output = Self; + #[inline(always)] + fn mul(self, rhs: Self) -> Self::Output { + let v0 = self.coeffs[0] * rhs.coeffs[0]; + let v1 = self.coeffs[1] * rhs.coeffs[1]; + let cross = (self.coeffs[0] + self.coeffs[1]) * (rhs.coeffs[0] + rhs.coeffs[1]); + Self::new(v0 + Self::mul_nr(v1), cross - v0 - v1) + } +} +impl> MulAssign for FpExt2 { + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl<'a, F: FieldCore, C: FpExt2Config> Add<&'a Self> for FpExt2 { + type Output = Self; + fn add(self, rhs: &'a Self) -> Self::Output { + self + *rhs + } +} +impl<'a, F: FieldCore, C: FpExt2Config> Sub<&'a Self> for FpExt2 { + type Output = Self; + fn sub(self, rhs: &'a Self) -> Self::Output { + self - *rhs + } +} +impl<'a, F: FieldCore, C: FpExt2Config> Mul<&'a Self> for FpExt2 { + type Output = Self; + fn mul(self, rhs: &'a Self) -> Self::Output { + self * *rhs + } +} + +impl> RingCore for FpExt2 { + /// Specialized squaring: 2 base-field multiplications instead of 3. + /// + /// `(c0 + c1·u)^2 = (c0^2 + NR·c1^2) + (2·c0·c1)·u` + #[inline(always)] + fn square(&self) -> Self { + let v0 = self.coeffs[0] * self.coeffs[0]; + let v1 = self.coeffs[1] * self.coeffs[1]; + Self::new( + v0 + Self::mul_nr(v1), + (self.coeffs[0] + self.coeffs[0]) * self.coeffs[1], + ) + } +} + +impl> Invertible for FpExt2 { + fn inverse(&self) -> Option { + if self.is_zero() { + return None; + } + let inv_n = self.norm().inverse()?; + Some(Self::new(self.coeffs[0] * inv_n, (-self.coeffs[1]) * inv_n)) + } +} + +impl> HalvingField for FpExt2 { + #[inline] + fn half(self) -> Self { + Self::new(self.coeffs[0].half(), self.coeffs[1].half()) + } +} + +impl> RandomSampling for FpExt2 { + fn random(rng: &mut R) -> Self { + Self::new(F::random(rng), F::random(rng)) + } +} + +impl> FromPrimitiveInt for FpExt2 { + fn from_u64(val: u64) -> Self { + Self::from_u64(val) + } + + fn from_i64(val: i64) -> Self { + Self::from_i64(val) + } + + fn from_u128(val: u128) -> Self { + Self::new(F::from_u128(val), F::zero()) + } + + fn from_i128(val: i128) -> Self { + Self::new(F::from_i128(val), F::zero()) + } +} + +impl> BalancedDigitLookup for FpExt2 {} + +/// Identity-stub `HasUnreducedOps` for `FpExt2` variants without a dedicated +/// delayed-reduction accumulator. `ProductAccum = Self`, so every multiply +/// reduces immediately. Same pattern as `FpExt4` and +/// `FpExt8<*>`. +macro_rules! impl_fp_ext2_unreduced_identity { + ($base:ident<$p:ident: $pty:ty>) => { + impl>> HasUnreducedOps for FpExt2<$base<$p>, C> { + type MulU64Accum = Self; + type ProductAccum = Self; + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Self { + self * Self::from_u64(small) + } + #[inline] + fn mul_to_product_accum(self, other: Self) -> Self { + self * other + } + #[inline] + fn reduce_mul_u64_accum(accum: Self) -> Self { + accum + } + #[inline] + fn reduce_product_accum(accum: Self) -> Self { + accum + } + } + + impl>> MulBaseUnreduced<$base<$p>> + for FpExt2<$base<$p>, C> + { + } + }; +} + +impl_fp_ext2_unreduced_identity!(Fp32); +impl_fp_ext2_unreduced_identity!(Fp128); + +macro_rules! impl_fp_ext2_default_optimized_fold { + ($base:ident<$p:ident: $pty:ty>) => { + impl>> HasOptimizedFold for FpExt2<$base<$p>, C> { + type FoldCtx = Self; + #[inline] + fn precompute_fold(r: Self) -> Self { + r + } + #[inline] + fn fold_one(r: &Self, even: Self, odd: Self) -> Self { + even + *r * (odd - even) + } + } + }; +} + +impl_fp_ext2_default_optimized_fold!(Fp32); +impl_fp_ext2_default_optimized_fold!(Fp128); + +/// Specialized EOR fold for `FpExt2, C>`. +/// +/// Mirrors `FpExt4`: precompute the "multiply by `r`" matrix +/// once per round, then fold each pair as `even + r·(odd − even)` using +/// base-field (`u64`) products with a single delayed reduction per output +/// coordinate. Only `Fp64` bases are specialized; other bases keep the generic +/// `FpExt2` fold via `impl_fp_ext2_default_optimized_fold`. +impl>> HasOptimizedFold for FpExt2, C> { + type FoldCtx = FoldMatrixFp64; + + /// Build the 2×2 "multiply by `r`" matrix in the `[1, u]` basis. + /// + /// For `r = r0 + r1·u` and `u² = NR`, multiplying `(a0, a1)` by `r` yields + /// `(r0·a0 + NR·r1·a1, r1·a0 + r0·a1)`, i.e. the matrix + /// `[[r0, NR·r1], [r1, r0]]`. `NR·r1` is materialized once via `mul_nr` + /// (a free negation for `IS_NEG_ONE`, a doubling for the `NR = 2` preset). + #[inline] + fn precompute_fold(r: Self) -> FoldMatrixFp64 { + let r0 = r.coeffs[0]; + let r1 = r.coeffs[1]; + let nr_r1 = Self::mul_nr(r1); + FoldMatrixFp64([ + [r0.to_limbs(), nr_r1.to_limbs()], + [r1.to_limbs(), r0.to_limbs()], + ]) + } + + /// Fold one pair: `even + r·(odd − even)`. + /// + /// Each output coordinate is the sum of two `u64×u64 → u128` base products, + /// reduced once by `Fp64::reduce_sum_of_two_products`. This is the + /// schoolbook product (4 base multiplies, 2 reductions) with delayed + /// reduction, versus the generic Karatsuba multiply (3 multiplies, 3 + /// reductions). The reduced coordinates are canonical, so the result is + /// byte-identical to the generic fold. + #[inline] + fn fold_one(ctx: &FoldMatrixFp64, even: Self, odd: Self) -> Self { + let m = &ctx.0; + let d0 = (odd.coeffs[0] - even.coeffs[0]).to_limbs() as u128; + let d1 = (odd.coeffs[1] - even.coeffs[1]).to_limbs() as u128; + let c0 = + Fp64::

::reduce_sum_of_two_products((m[0][0] as u128) * d0, (m[0][1] as u128) * d1); + let c1 = + Fp64::

::reduce_sum_of_two_products((m[1][0] as u128) * d0, (m[1][1] as u128) * d1); + Self::new(even.coeffs[0] + c0, even.coeffs[1] + c1) + } +} + +/// Split `value = lo128 + hi_carry * 2^128` into base-2^64 limbs +/// `[bits 0..64, bits 64..]` for a `Fp64ProductAccum` slot pair. +/// +/// The high limb may exceed 64 bits (it carries `hi_carry` in bits 64.., which +/// is small — at most 2 here), and the accumulator's `reduce` reconstructs +/// `lo + hi * 2^64` exactly, so the full (>128-bit) coefficient survives without +/// the wrap-mod-2^128 that a single-`u128` intermediate would incur. +#[inline(always)] +fn fp64_accum_limbs(lo128: u128, hi_carry: u128) -> [u128; 2] { + [lo128 as u64 as u128, (lo128 >> 64) | (hi_carry << 64)] +} + +/// Widening `FpExt2, C>` multiplication with delayed reduction. +/// +/// Each coefficient is a combination of base products that can exceed 128 bits +/// — `c0` reaches `p00 + p^2` (IS_NEG_ONE) or `p00 + 2*p11` (just under 2^130), +/// and `c1 = p01 + p10` reaches ~2^129. Forming them in a single `u128` would +/// drop the carry into bit 128 (wrap mod 2^128), which is *not* congruent mod +/// `p` and corrupts the delayed sum. We instead track the carry explicitly and +/// store base-2^64 limbs via [`fp64_accum_limbs`], so summing a batch and +/// reducing once is exact. For `IS_NEG_ONE` configs the `p^2` bias keeps `c0` +/// non-negative (and `p^2 == 0 (mod p)`, so it is invisible after reduction). +#[inline(always)] +pub(crate) fn fp_ext2_mul_to_accum_fp64>>( + a: [Fp64

; 2], + b: [Fp64

; 2], +) -> FpExt2Fp64ProductAccum { + let p00: u128 = a[0].mul_wide(b[0]); + let p11 = a[1].mul_wide(b[1]); + let p01 = a[0].mul_wide(b[1]); + let p10 = a[1].mul_wide(b[0]); + + let [c0_lo, c0_hi] = if C::IS_NEG_ONE { + // c0 = p00 + p^2 - p11, non-negative and < 2^129. + let modulus_sq = (P as u128) * (P as u128); + let (sum, carry_add) = p00.overflowing_add(modulus_sq); + let (diff, borrow) = sum.overflowing_sub(p11); + // c0 >= 0 guarantees carry_add >= borrow, so this stays in {0, 1}. + let hi_carry = (carry_add as u128) - (borrow as u128); + fp64_accum_limbs(diff, hi_carry) + } else { + // c0 = p00 + 2*p11, < 3*p^2 < 2^130 (carry in {0, 1, 2}). + let (sum1, carry1) = p00.overflowing_add(p11); + let (sum2, carry2) = sum1.overflowing_add(p11); + let hi_carry = (carry1 as u128) + (carry2 as u128); + fp64_accum_limbs(sum2, hi_carry) + }; + // c1 = p01 + p10, < 2*p^2 < 2^129 (carry in {0, 1}). + let (c1_sum, c1_carry) = p01.overflowing_add(p10); + let [c1_lo, c1_hi] = fp64_accum_limbs(c1_sum, c1_carry as u128); + + FpExt2Fp64ProductAccum([c0_lo, c0_hi, c1_lo, c1_hi]) +} + +impl>> HasUnreducedOps for FpExt2, C> { + type MulU64Accum = AccumPair< as HasUnreducedOps>::MulU64Accum>; + type ProductAccum = FpExt2Fp64ProductAccum; + + // `fp_ext2_mul_to_accum_fp64` keeps the full >128-bit coefficient via carry-aware + // base-2^64 limbs, so summing a batch and reducing once equals per-term `Mul`. + // Covered by the `Ext2` rounds in + // `sparse_tensor_factor_matches_dense_factor_rounds`. + const DELAYED_PRODUCT_SUM_IS_EXACT: bool = true; + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Self::MulU64Accum { + AccumPair( + self.coeffs[0].mul_u64_unreduced(small), + self.coeffs[1].mul_u64_unreduced(small), + ) + } + + #[inline] + fn mul_to_product_accum(self, other: Self) -> FpExt2Fp64ProductAccum { + fp_ext2_mul_to_accum_fp64::(self.coeffs, other.coeffs) + } + + #[inline] + fn reduce_mul_u64_accum(accum: Self::MulU64Accum) -> Self { + Self::new( + Fp64::

::reduce_mul_u64_accum(accum.0), + Fp64::

::reduce_mul_u64_accum(accum.1), + ) + } + + #[inline] + fn reduce_product_accum(accum: FpExt2Fp64ProductAccum) -> Self { + let [c0, c1] = accum.reduce::

(); + Self::new(c0, c1) + } +} + +impl>> MulBaseUnreduced> for FpExt2, C> {} + +/// Default quadratic extension used by the Solinas backend tests and helpers. +pub type Ext2 = FpExt2; diff --git a/crates/jolt-field/src/ext/fp_ext4.rs b/crates/jolt-field/src/ext/fp_ext4.rs new file mode 100644 index 0000000000..98754a27fd --- /dev/null +++ b/crates/jolt-field/src/ext/fp_ext4.rs @@ -0,0 +1,676 @@ +//! Akita's only degree-4 extension field (cyclotomic ring-subfield basis). +//! +//! Coefficients are stored in the `[1, e1, e2, e3]` basis used by trace reduction +//! and production fp32 presets. + +#![expect( + clippy::expl_impl_clone_on_copy, + reason = "manual Clone avoids adding irrelevant generic Clone bounds" +)] + +use super::*; + +/// Multiply ring-subfield quartic coefficient arrays in `[1, e1, e2, e3]` basis. +#[inline] +pub(crate) fn fp_ext4_mul_coeffs(a: [A; 4], b: [A; 4]) -> [A; 4] +where + F: FieldCore, + A: ExtensionCoeff, +{ + let [a0, a1, a2, a3] = a; + let [b0, b1, b2, b3] = b; + let tail0 = a1 * b1 + a2 * b2 + a3 * b3; + [ + a0 * b0 + tail0 + tail0, + a0 * b1 + a1 * b0 + a1 * b2 + a2 * b1 + a2 * b3 + a3 * b2, + a0 * b2 + a2 * b0 + a1 * b1 + a1 * b3 + a3 * b1 - a3 * b3, + a0 * b3 + a3 * b0 + a1 * b2 + a2 * b1 - a2 * b3 - a3 * b2, + ] +} + +/// Square ring-subfield quartic coefficient arrays in `[1, e1, e2, e3]` basis. +#[inline] +pub(crate) fn fp_ext4_square_coeffs(a: [A; 4]) -> [A; 4] +where + F: FieldCore, + A: ExtensionCoeff, +{ + let [a0, a1, a2, a3] = a; + let x0 = a0; + let x1 = a2; + let y0 = a1 - a3; + let y1 = a3; + + let x0x1 = x0 * x1; + let y0y1 = y0 * y1; + let x1_square = x1 * x1; + let y1_square = y1 * y1; + let aa = (x0 * x0 + x1_square + x1_square, x0x1 + x0x1); + let bb = (y0 * y0 + y1_square + y1_square, y0y1 + y0y1); + + let v0 = x0 * y0; + let v1 = x1 * y1; + let ab = (v0 + v1 + v1, (x0 + x1) * (y0 + y1) - v0 - v1); + let constant = (bb.0 + bb.0 + bb.1 + bb.1, bb.0 + bb.1 + bb.1); + let coeff_e1 = (ab.0 + ab.0, ab.1 + ab.1); + + [ + aa.0 + constant.0, + coeff_e1.0 + coeff_e1.1, + aa.1 + constant.1, + coeff_e1.1, + ] +} + +#[inline(always)] +fn fp32_product(a: Fp32

, b: Fp32

) -> u128 { + ((a.to_limbs() as u64) * (b.to_limbs() as u64)) as u128 +} + +#[inline(always)] +fn fp32_square_product(a: Fp32

) -> u128 { + fp32_product(a, a) +} + +#[inline(always)] +fn fp32_reduce_accum(x: u128) -> Fp32

{ + Fp32::

::from_canonical_u128_reduced(x) +} + +#[inline(always)] +fn fp32_modulus_square() -> u128 { + (P as u128) * (P as u128) +} + +#[inline(always)] +fn fp32_modulus_bits() -> u32 { + 32 - P.leading_zeros() +} + +/// Backend hook for scalar ring-subfield quartic multiplication. +/// +/// The default is the generic coefficient formula. Concrete base fields can +/// override this when their representation supports fusing product sums before +/// reduction. +pub trait FpExt4MulBackend: FieldCore { + /// Multiply two ring-subfield coefficient arrays in `[1, e1, e2, e3]` basis. + #[inline(always)] + fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { + fp_ext4_mul_coeffs::(a, b) + } + + /// Square one ring-subfield coefficient array in `[1, e1, e2, e3]` basis. + #[inline(always)] + fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { + fp_ext4_square_coeffs::(a) + } +} + +impl FpExt4MulBackend for Fp64

{} +impl FpExt4MulBackend for Fp128

{} + +impl FpExt4MulBackend for Fp32

{ + #[inline(always)] + fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { + let [a0, a1, a2, a3] = a; + let [b0, b1, b2, b3] = b; + let modulus_square = fp32_modulus_square::

(); + [ + fp32_reduce_accum( + fp32_product(a0, b0) + + 2 * (fp32_product(a1, b1) + fp32_product(a2, b2) + fp32_product(a3, b3)), + ), + fp32_reduce_accum( + fp32_product(a0, b1) + + fp32_product(a1, b0) + + fp32_product(a1, b2) + + fp32_product(a2, b1) + + fp32_product(a2, b3) + + fp32_product(a3, b2), + ), + fp32_reduce_accum( + fp32_product(a0, b2) + + fp32_product(a2, b0) + + fp32_product(a1, b1) + + fp32_product(a1, b3) + + fp32_product(a3, b1) + + modulus_square + - fp32_product(a3, b3), + ), + fp32_reduce_accum( + fp32_product(a0, b3) + + fp32_product(a3, b0) + + fp32_product(a1, b2) + + fp32_product(a2, b1) + + 2 * modulus_square + - fp32_product(a2, b3) + - fp32_product(a3, b2), + ), + ] + } + + #[inline(always)] + fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { + if fp32_modulus_bits::

() != 32 { + return Self::fp_ext4_mul(a, a); + } + + let [a0, a1, a2, a3] = a; + let modulus_square = fp32_modulus_square::

(); + let a0_square = fp32_square_product(a0); + let a1_square = fp32_square_product(a1); + let a2_square = fp32_square_product(a2); + let a3_square = fp32_square_product(a3); + let a0a1 = fp32_product(a0, a1); + let a0a2 = fp32_product(a0, a2); + let a0a3 = fp32_product(a0, a3); + let a1a2 = fp32_product(a1, a2); + let a1a3 = fp32_product(a1, a3); + let a2a3 = fp32_product(a2, a3); + + [ + fp32_reduce_accum(a0_square + 2 * (a1_square + a2_square + a3_square)), + fp32_reduce_accum(2 * (a0a1 + a1a2 + a2a3)), + fp32_reduce_accum(2 * a0a2 + a1_square + 2 * a1a3 + modulus_square - a3_square), + fp32_reduce_accum(2 * (a0a3 + a1a2 + modulus_square - a2a3)), + ] + } +} + +/// Widening `FpExt4>` multiplication that skips per-coefficient +/// Solinas reduction, returning `FpExt4Fp32ProductAccum` instead. +/// +/// The φ(X) ring reduction is already fused into the formulas — only the +/// base-field modular reduction is deferred. +#[inline(always)] +pub(crate) fn fp_ext4_mul_to_accum_fp32( + a: [Fp32

; 4], + b: [Fp32

; 4], +) -> FpExt4Fp32ProductAccum { + #[inline(always)] + fn product(a: Fp32

, b: Fp32

) -> u128 { + (a.to_limbs() as u128) * (b.to_limbs() as u128) + } + + let [a0, a1, a2, a3] = a; + let [b0, b1, b2, b3] = b; + let modulus_square = (P as u128) * (P as u128); + FpExt4Fp32ProductAccum([ + product(a0, b0) + 2 * (product(a1, b1) + product(a2, b2) + product(a3, b3)), + product(a0, b1) + + product(a1, b0) + + product(a1, b2) + + product(a2, b1) + + product(a2, b3) + + product(a3, b2), + product(a0, b2) + + product(a2, b0) + + product(a1, b1) + + product(a1, b3) + + product(a3, b1) + + modulus_square + - product(a3, b3), + product(a0, b3) + product(a3, b0) + product(a1, b2) + product(a2, b1) + 2 * modulus_square + - product(a2, b3) + - product(a3, b2), + ]) +} + +/// Quartic fixed-subfield element in the Akita cyclotomic basis. +/// +/// Coordinates are `[c0, c1, c2, c3]` in basis `[1, e1, e2, e3]`, where +/// `e_j = zeta^(jm) + zeta^(-jm)` for `m = D / 8` inside a compatible +/// cyclotomic ring. The scalar arithmetic is independent of the concrete ring +/// dimension `D`. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[cfg_attr( + feature = "allocative", + allocative(bound = "F: FieldCore + allocative::Allocative") +)] +#[repr(transparent)] +pub struct FpExt4 { + /// Coefficients in basis `[1, e1, e2, e3]`. + pub coeffs: [F; 4], +} + +impl FpExt4 { + /// Construct from ring-subfield basis coefficients `[c0, c1, c2, c3]`. + #[inline] + pub fn new(coeffs: [F; 4]) -> Self { + Self { coeffs } + } + + /// Additive identity. + #[inline] + pub fn zero() -> Self { + Self::new([F::zero(); 4]) + } + + /// Multiplicative identity. + #[inline] + pub fn one() -> Self { + Self::new([F::one(), F::zero(), F::zero(), F::zero()]) + } + + /// Check whether this element is zero. + #[inline] + pub fn is_zero(&self) -> bool { + self.coeffs.iter().all(|coeff| coeff.is_zero()) + } + + /// Construct from a `u64` embedded in the base field. + #[inline] + pub fn from_u64(val: u64) -> Self + where + F: FromPrimitiveInt, + { + Self::new([F::from_u64(val), F::zero(), F::zero(), F::zero()]) + } + + /// Construct from an `i64` embedded in the base field. + #[inline] + pub fn from_i64(val: i64) -> Self + where + F: FromPrimitiveInt, + { + Self::new([F::from_i64(val), F::zero(), F::zero(), F::zero()]) + } + + #[inline(always)] + fn fp_ext2_mul_by_e2_nr(lhs: (F, F), rhs: (F, F)) -> (F, F) { + let (a0, a1) = lhs; + let (b0, b1) = rhs; + let v0 = a0 * b0; + let v1 = a1 * b1; + let c1 = (a0 + a1) * (b0 + b1) - v0 - v1; + let c0 = v0 + v1 + v1; + (c0, c1) + } + + #[inline(always)] + fn fp_ext2_square_by_e2_nr(x: (F, F)) -> (F, F) { + let (a0, a1) = x; + let a0a1 = a0 * a1; + (a0.square() + a1.square() + a1.square(), a0a1 + a0a1) + } + + #[inline(always)] + fn fp_ext2_mul_by_e1_nr(x: (F, F)) -> (F, F) { + let (x0, x1) = x; + (x0 + x0 + x1 + x1, x0 + x1 + x1) + } + + #[inline(always)] + fn fp_ext2_inverse_by_e2_nr(x: (F, F)) -> Option<(F, F)> { + let (x0, x1) = x; + let inv_norm = (x0.square() - (x1.square() + x1.square())).inverse()?; + Some((x0 * inv_norm, -x1 * inv_norm)) + } +} + +impl std::fmt::Debug for FpExt4 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FpExt4") + .field("coeffs", &self.coeffs) + .finish() + } +} + +impl Clone for FpExt4 { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for FpExt4 {} + +impl Default for FpExt4 { + fn default() -> Self { + Self::zero() + } +} + +impl PartialEq for FpExt4 { + fn eq(&self, other: &Self) -> bool { + self.coeffs == other.coeffs + } +} + +impl Eq for FpExt4 {} + +impl Add for FpExt4 { + type Output = Self; + + #[inline(always)] + fn add(self, rhs: Self) -> Self::Output { + Self::new([ + self.coeffs[0] + rhs.coeffs[0], + self.coeffs[1] + rhs.coeffs[1], + self.coeffs[2] + rhs.coeffs[2], + self.coeffs[3] + rhs.coeffs[3], + ]) + } +} + +impl Sub for FpExt4 { + type Output = Self; + + #[inline(always)] + fn sub(self, rhs: Self) -> Self::Output { + Self::new([ + self.coeffs[0] - rhs.coeffs[0], + self.coeffs[1] - rhs.coeffs[1], + self.coeffs[2] - rhs.coeffs[2], + self.coeffs[3] - rhs.coeffs[3], + ]) + } +} + +impl Neg for FpExt4 { + type Output = Self; + + #[inline(always)] + fn neg(self) -> Self::Output { + Self::new([ + -self.coeffs[0], + -self.coeffs[1], + -self.coeffs[2], + -self.coeffs[3], + ]) + } +} + +impl AddAssign for FpExt4 { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.coeffs[0] = self.coeffs[0] + rhs.coeffs[0]; + self.coeffs[1] = self.coeffs[1] + rhs.coeffs[1]; + self.coeffs[2] = self.coeffs[2] + rhs.coeffs[2]; + self.coeffs[3] = self.coeffs[3] + rhs.coeffs[3]; + } +} + +impl SubAssign for FpExt4 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.coeffs[0] = self.coeffs[0] - rhs.coeffs[0]; + self.coeffs[1] = self.coeffs[1] - rhs.coeffs[1]; + self.coeffs[2] = self.coeffs[2] - rhs.coeffs[2]; + self.coeffs[3] = self.coeffs[3] - rhs.coeffs[3]; + } +} + +impl Mul for FpExt4 { + type Output = Self; + + #[inline(always)] + fn mul(self, rhs: Self) -> Self::Output { + Self::new(F::fp_ext4_mul(self.coeffs, rhs.coeffs)) + } +} + +impl MulAssign for FpExt4 { + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl<'a, F: FieldCore> Add<&'a Self> for FpExt4 { + type Output = Self; + + fn add(self, rhs: &'a Self) -> Self::Output { + self + *rhs + } +} + +impl<'a, F: FieldCore> Sub<&'a Self> for FpExt4 { + type Output = Self; + + fn sub(self, rhs: &'a Self) -> Self::Output { + self - *rhs + } +} + +impl<'a, F: FpExt4MulBackend> Mul<&'a Self> for FpExt4 { + type Output = Self; + + fn mul(self, rhs: &'a Self) -> Self::Output { + self * *rhs + } +} + +impl RingCore for FpExt4 { + #[inline(always)] + fn square(&self) -> Self { + Self::new(F::fp_ext4_square(self.coeffs)) + } +} + +impl Invertible for FpExt4 { + fn inverse(&self) -> Option { + if self.is_zero() { + return None; + } + + let [a0, a1, a2, a3] = self.coeffs; + let a = (a0, a2); + let b = (a1 - a3, a3); + + let aa = Self::fp_ext2_square_by_e2_nr(a); + let bb = Self::fp_ext2_square_by_e2_nr(b); + let norm = { + let nr_bb = Self::fp_ext2_mul_by_e1_nr(bb); + (aa.0 - nr_bb.0, aa.1 - nr_bb.1) + }; + let inv_norm = Self::fp_ext2_inverse_by_e2_nr(norm)?; + let constant = Self::fp_ext2_mul_by_e2_nr(a, inv_norm); + let e1_coeff = Self::fp_ext2_mul_by_e2_nr((-b.0, -b.1), inv_norm); + + Some(Self::new([ + constant.0, + e1_coeff.0 + e1_coeff.1, + constant.1, + e1_coeff.1, + ])) + } +} + +impl HalvingField for FpExt4 { + #[inline] + fn half(self) -> Self { + Self::new(std::array::from_fn(|i| self.coeffs[i].half())) + } +} + +impl RandomSampling for FpExt4 { + fn random(rng: &mut R) -> Self { + Self::new([ + F::random(rng), + F::random(rng), + F::random(rng), + F::random(rng), + ]) + } +} + +impl FromPrimitiveInt for FpExt4 { + fn from_u64(val: u64) -> Self { + Self::from_u64(val) + } + + fn from_i64(val: i64) -> Self { + Self::from_i64(val) + } + + fn from_u128(val: u128) -> Self { + Self::new([F::from_u128(val), F::zero(), F::zero(), F::zero()]) + } + + fn from_i128(val: i128) -> Self { + Self::new([F::from_i128(val), F::zero(), F::zero(), F::zero()]) + } +} + +impl BalancedDigitLookup for FpExt4 {} + +impl HasUnreducedOps for FpExt4> { + type MulU64Accum = Self; + type ProductAccum = FpExt4Fp32ProductAccum; + + // `fp_ext4_mul_to_accum_fp32` widens each Fp32 limb product + // (< 7·p² ≈ 2^65) into a u128 slot with no `mod 2^128` wrap, so summing a + // batch and reducing once matches per-limb reduce-then-add exactly. Covered + // by `fp_ext4_fp32_accum_summation`. + const DELAYED_PRODUCT_SUM_IS_EXACT: bool = true; + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Self::MulU64Accum { + let small = Fp32::

::from_u64(small); + Self::new(self.coeffs.map(|coeff| coeff * small)) + } + + #[inline] + fn mul_to_product_accum(self, other: Self) -> Self::ProductAccum { + fp_ext4_mul_to_accum_fp32(self.coeffs, other.coeffs) + } + + #[inline] + fn reduce_mul_u64_accum(accum: Self::MulU64Accum) -> Self { + accum + } + + #[inline] + fn reduce_product_accum(accum: Self::ProductAccum) -> Self { + Self::new(accum.reduce::

()) + } +} + +impl MulBaseUnreduced> for FpExt4> { + #[inline] + fn mul_base_to_product_accum(self, x: Fp32

) -> Self::ProductAccum { + // E × F has no cross terms: scale each base coordinate into its own + // u128 slot. Each product is `< p² < 2^62`, so a summed batch reduces + // exactly (see `DELAYED_PRODUCT_SUM_IS_EXACT`). + let x = x.to_limbs() as u128; + let [a0, a1, a2, a3] = self.coeffs; + FpExt4Fp32ProductAccum([ + (a0.to_limbs() as u128) * x, + (a1.to_limbs() as u128) * x, + (a2.to_limbs() as u128) * x, + (a3.to_limbs() as u128) * x, + ]) + } +} + +impl HasOptimizedFold for FpExt4> { + type FoldCtx = FoldMatrixFp32; + + #[inline] + fn precompute_fold(r: Self) -> FoldMatrixFp32 { + let [r0, r1, r2, r3] = r.coeffs; + let two = Fp32::

::from_u64(2); + FoldMatrixFp32([ + [ + r0.to_limbs(), + (two * r1).to_limbs(), + (two * r2).to_limbs(), + (two * r3).to_limbs(), + ], + [ + r1.to_limbs(), + (r0 + r2).to_limbs(), + (r1 + r3).to_limbs(), + r2.to_limbs(), + ], + [ + r2.to_limbs(), + (r1 + r3).to_limbs(), + r0.to_limbs(), + (r1 - r3).to_limbs(), + ], + [ + r3.to_limbs(), + r2.to_limbs(), + (r1 - r3).to_limbs(), + (r0 - r2).to_limbs(), + ], + ]) + } + + #[inline] + fn fold_one(ctx: &FoldMatrixFp32, even: Self, odd: Self) -> Self { + let m = &ctx.0; + let d: [u32; 4] = std::array::from_fn(|j| (odd.coeffs[j] - even.coeffs[j]).to_limbs()); + let folded: [Fp32

; 4] = if P < (1u32 << 31) { + // P < 2^31: each product < 2^62, sum of 4 < 2^64, fits in u64. + std::array::from_fn(|row| { + let acc: u64 = (m[row][0] as u64) * (d[0] as u64) + + (m[row][1] as u64) * (d[1] as u64) + + (m[row][2] as u64) * (d[2] as u64) + + (m[row][3] as u64) * (d[3] as u64); + Fp32::

::from_u64(acc) + even.coeffs[row] + }) + } else { + std::array::from_fn(|row| { + let acc: u128 = (m[row][0] as u128) * (d[0] as u128) + + (m[row][1] as u128) * (d[1] as u128) + + (m[row][2] as u128) * (d[2] as u128) + + (m[row][3] as u128) * (d[3] as u128); + Fp32::

::from_canonical_u128_reduced(acc) + even.coeffs[row] + }) + }; + FpExt4::new(folded) + } +} + +macro_rules! impl_fp_ext4_unreduced_identity { + ($base:ident<$p:ident: $pty:ty>) => { + impl HasUnreducedOps for FpExt4<$base<$p>> { + type MulU64Accum = Self; + type ProductAccum = Self; + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Self { + let small = $base::<$p>::from_u64(small); + Self::new(self.coeffs.map(|coeff| coeff * small)) + } + #[inline] + fn mul_to_product_accum(self, other: Self) -> Self { + self * other + } + #[inline] + fn reduce_mul_u64_accum(accum: Self) -> Self { + accum + } + #[inline] + fn reduce_product_accum(accum: Self) -> Self { + accum + } + } + + impl MulBaseUnreduced<$base<$p>> for FpExt4<$base<$p>> {} + }; +} + +impl_fp_ext4_unreduced_identity!(Fp64); +impl_fp_ext4_unreduced_identity!(Fp128); + +macro_rules! impl_fp_ext4_default_optimized_fold { + ($base:ident<$p:ident: $pty:ty>) => { + impl HasOptimizedFold for FpExt4<$base<$p>> { + type FoldCtx = Self; + #[inline] + fn precompute_fold(r: Self) -> Self { + r + } + #[inline] + fn fold_one(r: &Self, even: Self, odd: Self) -> Self { + even + *r * (odd - even) + } + } + }; +} + +impl_fp_ext4_default_optimized_fold!(Fp64); +impl_fp_ext4_default_optimized_fold!(Fp128); diff --git a/crates/jolt-field/src/ext/fp_ext8.rs b/crates/jolt-field/src/ext/fp_ext8.rs new file mode 100644 index 0000000000..659ec79689 --- /dev/null +++ b/crates/jolt-field/src/ext/fp_ext8.rs @@ -0,0 +1,486 @@ +//! Akita's only degree-8 extension field (cyclotomic ring-subfield basis). +//! +//! Coefficients are stored in the Chebyshev basis `[1, e1, ..., e7]`. + +#![expect( + clippy::expl_impl_clone_on_copy, + reason = "manual Clone avoids adding irrelevant generic Clone bounds" +)] + +use super::*; + +/// Chebyshev `φ` fold-back for a degree-8 accumulator, using caller-supplied +/// add/sub so the same routine serves scalar, `i64`, and SIMD lane types. +/// +/// `φ(k)` maps a product onto the `[1, e1, ..., e7]` basis: +/// `k = 0 → 2·constant`, `1 ≤ k ≤ 7 → +e_k`, `k = 8 → 0`, +/// `9 ≤ k ≤ 15 → −e_{16−k}`. +#[inline(always)] +fn fp_ext8_add_phi( + out: &mut [V; 8], + idx: usize, + value: V, + add: &impl Fn(V, V) -> V, + sub: &impl Fn(V, V) -> V, +) { + match idx { + 0 => out[0] = add(out[0], add(value, value)), + 1..=7 => out[idx] = add(out[idx], value), + 8 => {} + 9..=15 => out[16 - idx] = sub(out[16 - idx], value), + _ => unreachable!("fp_ext8 Chebyshev index out of range"), + } +} + +/// Karatsuba schedule for `FpExt8` multiplication in the Chebyshev +/// basis, generic over a lane type `V` and its add/sub/mul. +/// +/// One schedule serves every backend: the scalar field default and the NEON / +/// AVX2 / AVX-512 SIMD kernels. The schedule is purely an additive combination +/// of products, so callers that reduce per operation (field or intrinsic ops) +/// and callers that defer reduction to the end are both correct, provided the +/// accumulator does not overflow. +#[inline(always)] +pub(crate) fn fp_ext8_mul_schedule( + a: [V; 8], + b: [V; 8], + zero: V, + add: A, + sub: S, + mul: M, +) -> [V; 8] +where + V: Copy, + A: Fn(V, V) -> V, + S: Fn(V, V) -> V, + M: Fn(V, V) -> V, +{ + let diag: [V; 8] = std::array::from_fn(|i| mul(a[i], b[i])); + let mut out = [zero; 8]; + out[0] = diag[0]; + + for k in 1..8 { + let mixed = sub(sub(mul(add(a[0], a[k]), add(b[0], b[k])), diag[0]), diag[k]); + out[k] = add(out[k], mixed); + } + + for (i, &diag_i) in diag.iter().enumerate().skip(1) { + out[0] = add(out[0], add(diag_i, diag_i)); + fp_ext8_add_phi(&mut out, i + i, diag_i, &add, &sub); + } + + for i in 1..8 { + for j in (i + 1)..8 { + let mixed = sub(sub(mul(add(a[i], a[j]), add(b[i], b[j])), diag[i]), diag[j]); + fp_ext8_add_phi(&mut out, i + j, mixed, &add, &sub); + fp_ext8_add_phi(&mut out, j - i, mixed, &add, &sub); + } + } + + out +} + +/// Squaring schedule for `FpExt8`, generic over a lane type `V`. +/// +/// Uses `(a_i + a_j)² − a_i² − a_j² = 2·a_i·a_j` to compute `a_i·a_j` directly +/// and double, saving one add and two subs per cross-term versus the Karatsuba +/// form. Shares `fp_ext8_add_phi` with [`fp_ext8_mul_schedule`]. +#[inline(always)] +pub(crate) fn fp_ext8_square_schedule( + a: [V; 8], + zero: V, + add: A, + sub: S, + mul: M, +) -> [V; 8] +where + V: Copy, + A: Fn(V, V) -> V, + S: Fn(V, V) -> V, + M: Fn(V, V) -> V, +{ + let sq: [V; 8] = std::array::from_fn(|i| mul(a[i], a[i])); + let mut out = [zero; 8]; + out[0] = sq[0]; + + for k in 1..8 { + let cross = mul(a[0], a[k]); + out[k] = add(out[k], add(cross, cross)); + } + + for (i, &sq_i) in sq.iter().enumerate().skip(1) { + out[0] = add(out[0], add(sq_i, sq_i)); + fp_ext8_add_phi(&mut out, i + i, sq_i, &add, &sub); + } + + for i in 1..8 { + for j in (i + 1)..8 { + let cross = mul(a[i], a[j]); + let doubled = add(cross, cross); + fp_ext8_add_phi(&mut out, i + j, doubled, &add, &sub); + fp_ext8_add_phi(&mut out, j - i, doubled, &add, &sub); + } + } + + out +} + +#[inline(always)] +fn fp_ext8_mul_coeffs(a: [F; 8], b: [F; 8]) -> [F; 8] { + fp_ext8_mul_schedule(a, b, F::zero(), |x, y| x + y, |x, y| x - y, |x, y| x * y) +} + +/// Backend hook for scalar ring-subfield degree-8 multiplication. +pub trait FpExt8MulBackend: FieldCore { + /// Multiply coefficient arrays in `[1, e1, ..., e7]` basis. + #[inline(always)] + fn fp_ext8_mul(a: [Self; 8], b: [Self; 8]) -> [Self; 8] { + fp_ext8_mul_coeffs::(a, b) + } +} + +impl FpExt8MulBackend for Fp32

{} +impl FpExt8MulBackend for Fp64

{} +impl FpExt8MulBackend for Fp128

{} + +/// Degree-8 ring subfield element in canonical basis `[1, e1, ..., e7]`. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[cfg_attr( + feature = "allocative", + allocative(bound = "F: FieldCore + allocative::Allocative") +)] +#[repr(transparent)] +pub struct FpExt8 { + /// Coefficients in basis `[1, e1, ..., e7]`. + pub coeffs: [F; 8], +} + +impl FpExt8 { + /// Construct from canonical ring-subfield basis coefficients. + #[inline] + pub fn new(coeffs: [F; 8]) -> Self { + Self { coeffs } + } + + /// Additive identity. + #[inline] + pub fn zero() -> Self { + Self::new([F::zero(); 8]) + } + + /// Multiplicative identity. + #[inline] + pub fn one() -> Self { + Self::new(std::array::from_fn(|i| { + if i == 0 { + F::one() + } else { + F::zero() + } + })) + } + + /// Check whether this element is zero. + #[inline] + pub fn is_zero(&self) -> bool { + self.coeffs.iter().all(|coeff| coeff.is_zero()) + } + + /// Construct from a `u64` embedded in the base field. + #[inline] + pub fn from_u64(val: u64) -> Self + where + F: FromPrimitiveInt, + { + Self::new(std::array::from_fn(|i| { + if i == 0 { + F::from_u64(val) + } else { + F::zero() + } + })) + } + + /// Construct from an `i64` embedded in the base field. + #[inline] + pub fn from_i64(val: i64) -> Self + where + F: FromPrimitiveInt, + { + Self::new(std::array::from_fn(|i| { + if i == 0 { + F::from_i64(val) + } else { + F::zero() + } + })) + } +} + +impl std::fmt::Debug for FpExt8 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FpExt8") + .field("coeffs", &self.coeffs) + .finish() + } +} + +impl Clone for FpExt8 { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for FpExt8 {} + +impl Default for FpExt8 { + fn default() -> Self { + Self::zero() + } +} + +impl PartialEq for FpExt8 { + fn eq(&self, other: &Self) -> bool { + self.coeffs == other.coeffs + } +} + +impl Eq for FpExt8 {} + +impl Add for FpExt8 { + type Output = Self; + + #[inline(always)] + fn add(self, rhs: Self) -> Self::Output { + Self::new(std::array::from_fn(|i| self.coeffs[i] + rhs.coeffs[i])) + } +} + +impl Sub for FpExt8 { + type Output = Self; + + #[inline(always)] + fn sub(self, rhs: Self) -> Self::Output { + Self::new(std::array::from_fn(|i| self.coeffs[i] - rhs.coeffs[i])) + } +} + +impl Neg for FpExt8 { + type Output = Self; + + #[inline(always)] + fn neg(self) -> Self::Output { + Self::new(std::array::from_fn(|i| -self.coeffs[i])) + } +} + +impl AddAssign for FpExt8 { + #[inline] + fn add_assign(&mut self, rhs: Self) { + for i in 0..8 { + self.coeffs[i] += rhs.coeffs[i]; + } + } +} + +impl SubAssign for FpExt8 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + for i in 0..8 { + self.coeffs[i] -= rhs.coeffs[i]; + } + } +} + +impl Mul for FpExt8 { + type Output = Self; + + #[inline(always)] + fn mul(self, rhs: Self) -> Self::Output { + Self::new(F::fp_ext8_mul(self.coeffs, rhs.coeffs)) + } +} + +impl MulAssign for FpExt8 { + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl<'a, F: FieldCore> Add<&'a Self> for FpExt8 { + type Output = Self; + + fn add(self, rhs: &'a Self) -> Self::Output { + self + *rhs + } +} + +impl<'a, F: FieldCore> Sub<&'a Self> for FpExt8 { + type Output = Self; + + fn sub(self, rhs: &'a Self) -> Self::Output { + self - *rhs + } +} + +impl<'a, F: FpExt8MulBackend> Mul<&'a Self> for FpExt8 { + type Output = Self; + + fn mul(self, rhs: &'a Self) -> Self::Output { + self * *rhs + } +} + +impl RingCore for FpExt8 { + #[inline(always)] + fn square(&self) -> Self { + *self * *self + } +} + +impl Invertible for FpExt8 { + fn inverse(&self) -> Option { + if self.is_zero() { + return None; + } + + let mut aug = [[F::zero(); 9]; 8]; + for col in 0..8 { + let mut basis = [F::zero(); 8]; + basis[col] = F::one(); + let product = *self * Self::new(basis); + for (row, coeff) in product.coeffs.iter().copied().enumerate() { + aug[row][col] = coeff; + } + } + aug[0][8] = F::one(); + + for col in 0..8 { + let pivot = (col..8).find(|&row| !aug[row][col].is_zero())?; + if pivot != col { + aug.swap(col, pivot); + } + let inv = aug[col][col].inverse()?; + for entry in &mut aug[col][col..=8] { + *entry *= inv; + } + for row in 0..8 { + if row == col { + continue; + } + let factor = aug[row][col]; + if factor.is_zero() { + continue; + } + let pivot_row = aug[col]; + for (target, pivot) in aug[row][col..=8] + .iter_mut() + .zip(pivot_row[col..=8].iter().copied()) + { + *target -= factor * pivot; + } + } + } + + Some(Self::new(std::array::from_fn(|i| aug[i][8]))) + } +} + +impl HalvingField for FpExt8 { + #[inline] + fn half(self) -> Self { + Self::new(std::array::from_fn(|i| self.coeffs[i].half())) + } +} + +impl RandomSampling for FpExt8 { + fn random(rng: &mut R) -> Self { + Self::new(std::array::from_fn(|_| F::random(rng))) + } +} + +impl FromPrimitiveInt for FpExt8 { + fn from_u64(val: u64) -> Self { + Self::from_u64(val) + } + + fn from_i64(val: i64) -> Self { + Self::from_i64(val) + } + + fn from_u128(val: u128) -> Self { + Self::new(std::array::from_fn(|i| { + if i == 0 { + F::from_u128(val) + } else { + F::zero() + } + })) + } + + fn from_i128(val: i128) -> Self { + Self::new(std::array::from_fn(|i| { + if i == 0 { + F::from_i128(val) + } else { + F::zero() + } + })) + } +} + +impl BalancedDigitLookup for FpExt8 {} + +macro_rules! impl_fp_ext8_unreduced_identity { + ($base:ident<$p:ident: $pty:ty>) => { + impl HasUnreducedOps for FpExt8<$base<$p>> { + type MulU64Accum = Self; + type ProductAccum = Self; + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Self { + let small = $base::<$p>::from_u64(small); + Self::new(self.coeffs.map(|coeff| coeff * small)) + } + #[inline] + fn mul_to_product_accum(self, other: Self) -> Self { + self * other + } + #[inline] + fn reduce_mul_u64_accum(accum: Self) -> Self { + accum + } + #[inline] + fn reduce_product_accum(accum: Self) -> Self { + accum + } + } + + impl MulBaseUnreduced<$base<$p>> for FpExt8<$base<$p>> {} + }; +} + +impl_fp_ext8_unreduced_identity!(Fp32); +impl_fp_ext8_unreduced_identity!(Fp64); +impl_fp_ext8_unreduced_identity!(Fp128); + +macro_rules! impl_fp_ext8_default_optimized_fold { + ($base:ident<$p:ident: $pty:ty>) => { + impl HasOptimizedFold for FpExt8<$base<$p>> { + type FoldCtx = Self; + #[inline] + fn precompute_fold(r: Self) -> Self { + r + } + #[inline] + fn fold_one(r: &Self, even: Self, odd: Self) -> Self { + even + *r * (odd - even) + } + } + }; +} + +impl_fp_ext8_default_optimized_fold!(Fp32); +impl_fp_ext8_default_optimized_fold!(Fp64); +impl_fp_ext8_default_optimized_fold!(Fp128); diff --git a/crates/jolt-field/src/ext/lift.rs b/crates/jolt-field/src/ext/lift.rs new file mode 100644 index 0000000000..c1b76381cc --- /dev/null +++ b/crates/jolt-field/src/ext/lift.rs @@ -0,0 +1,480 @@ +//! Helpers for embedding base fields into extension fields. +//! +//! [`FpExt4`] and [`FpExt8`] use the cyclotomic ring-subfield basis aligned with +//! trace reduction and production fp32 presets. + +#![expect( + clippy::expect_used, + reason = "registered pseudo-Mersenne parameters are a field-type invariant" +)] + +use crate::ext::{FpExt2, FpExt2Config, FpExt4, FpExt4MulBackend, FpExt8, FpExt8MulBackend}; +use crate::unreduced::HasUnreducedOps; +use crate::{ + pseudo_mersenne_modulus, FieldCore, FieldError, FromPrimitiveInt, PseudoMersenneField, +}; + +/// Lift a base-field element into an extension field. +/// +/// This is intentionally small: for extension towers we embed into the constant term. +pub trait LiftBase: FieldCore { + /// Embed `x ∈ F` as a constant in `Self`. + fn lift_base(x: F) -> Self; +} + +/// Multiply an extension-field element by a base-field scalar. +/// +/// This avoids materializing the base scalar as an extension element and then +/// using a full extension multiply. For tower extensions this scales each +/// base-field coordinate directly. +pub trait MulBase: FieldCore { + /// Return `self * x`, where `x` is interpreted as a base-field scalar. + fn mul_base(self, x: F) -> Self; +} + +/// An algebraic extension of base field `F`. +/// +/// Provides the extension degree and a constructor from a slice of base-field +/// coefficients (in the canonical basis `{1, u, u^2, ...}`). +pub trait ExtField: FieldCore + LiftBase + MulBase + FromPrimitiveInt { + /// Extension degree: `[Self : F]`. + const EXT_DEGREE: usize; + + /// Construct from a coefficient slice `[c0, c1, ..., c_{d-1}]`. + /// + /// # Panics + /// Panics if `coeffs.len() != Self::EXT_DEGREE`. + fn from_base_slice(coeffs: &[F]) -> Self; + + /// Return base-field coefficients in the canonical basis. + fn to_base_vec(&self) -> Vec; +} + +/// Deferred-reduction extension-times-base multiply. +/// +/// `mul_base_to_product_accum` scales `self` by a base scalar `x` and writes the +/// result into [`HasUnreducedOps::ProductAccum`] without reducing, so a batch of +/// `E × F` products can be summed and reduced once. When +/// [`HasUnreducedOps::DELAYED_PRODUCT_SUM_IS_EXACT`] holds, the reduced sum equals +/// the per-term [`MulBase::mul_base`] sum within the accumulator's headroom. +/// +/// `E × F` has no cross terms, so the default body (lift `x` and reuse +/// [`HasUnreducedOps::mul_to_product_accum`]) is correct everywhere; extensions +/// whose product-accumulator layout admits cheaper coordinate scaling override it. +pub trait MulBaseUnreduced: ExtField + HasUnreducedOps { + /// Accumulate `self * x` (extension times base scalar) without reducing. + #[inline] + fn mul_base_to_product_accum(self, x: F) -> Self::ProductAccum { + self.mul_to_product_accum(Self::lift_base(x)) + } +} + +impl MulBaseUnreduced for F {} + +/// Frobenius operations for an extension field over `F`. +/// +/// The default implementations below are intentionally algebraic rather than +/// basis-specific: they raise to powers of the base-field modulus. Specialized +/// extension types can add cheaper implementations later, but this gives the +/// protocol a single auditable contract first. +pub trait FrobeniusExtField: ExtField { + /// Apply `x -> x^(q^power)`, where `q = |F|`. + fn frobenius_pow(self, power: usize) -> Self; + + /// Apply the inverse Frobenius power. Since `x -> x^q` has order + /// `[Self:F]` on `Self`, this is `frobenius_pow(EXT_DEGREE - power)`. + fn frobenius_inv_pow(self, power: usize) -> Self { + let degree = Self::EXT_DEGREE; + if degree == 0 { + return self; + } + self.frobenius_pow((degree - (power % degree)) % degree) + } +} + +#[inline] +fn field_pow_u128(mut base: E, mut exp: u128) -> E { + let mut acc = E::one(); + while exp > 0 { + if (exp & 1) == 1 { + acc *= base; + } + base *= base; + exp >>= 1; + } + acc +} + +#[inline] +fn base_modulus() -> u128 { + pseudo_mersenne_modulus(F::MODULUS_BITS, F::MODULUS_OFFSET) + .expect("pseudo-Mersenne modulus parameters must be valid") +} + +fn frobenius_pow_via_base_modulus(value: E, power: usize) -> E +where + F: PseudoMersenneField, + E: ExtField, +{ + let q = base_modulus::(); + let mut out = value; + for _ in 0..(power % E::EXT_DEGREE.max(1)) { + out = field_pow_u128(out, q); + } + out +} + +impl FrobeniusExtField for F +where + F: PseudoMersenneField, +{ + #[inline] + fn frobenius_pow(self, power: usize) -> Self { + let _ = power; + self + } +} + +impl FrobeniusExtField for FpExt2 +where + F: PseudoMersenneField, + C: FpExt2Config, +{ + #[inline] + fn frobenius_pow(self, power: usize) -> Self { + frobenius_pow_via_base_modulus::(self, power) + } +} + +impl FrobeniusExtField for FpExt4 +where + F: PseudoMersenneField + FpExt4MulBackend, +{ + #[inline] + fn frobenius_pow(self, power: usize) -> Self { + frobenius_pow_via_base_modulus::(self, power) + } +} + +impl FrobeniusExtField for FpExt8 +where + F: PseudoMersenneField + FpExt8MulBackend, +{ + #[inline] + fn frobenius_pow(self, power: usize) -> Self { + frobenius_pow_via_base_modulus::(self, power) + } +} + +/// Return the first `width` elements of the canonical extension basis. +/// +/// For [`FpExt4`] and [`FpExt8`] this is the fixed +/// ring-subfield basis `[1, e1, ...]`, so the chosen Moore-type theta family +/// is aligned with the coefficient packing basis used by `embed_subfield`. +/// +/// # Errors +/// +/// Returns an error if `width > E::EXT_DEGREE`. +pub fn canonical_frobenius_thetas(width: usize) -> Result, FieldError> +where + F: FieldCore, + E: ExtField, +{ + if width > E::EXT_DEGREE { + return Err(FieldError::InvalidInput(format!( + "Frobenius theta width {width} exceeds extension degree {}", + E::EXT_DEGREE + ))); + } + Ok((0..width) + .map(|idx| { + let mut coeffs = vec![F::zero(); E::EXT_DEGREE]; + coeffs[idx] = F::one(); + E::from_base_slice(&coeffs) + }) + .collect()) +} + +/// Solve `M_t(theta) z = r`, where +/// `M_t(theta)_{j,h} = theta_h^(q^-j)`. +/// +/// This intentionally uses dense elimination: supported Frobenius widths are +/// tiny (`<= [E:F]`) and explicit validation is more valuable here than a +/// clever specialized solver. +/// +/// # Errors +/// +/// Returns an error if the matrix is not square, the dimensions do not match, +/// or the Moore-type matrix is singular. +pub fn solve_frobenius_moore(thetas: &[E], rhs: &[E]) -> Result, FieldError> +where + F: PseudoMersenneField, + E: FrobeniusExtField, +{ + let n = thetas.len(); + if rhs.len() != n { + return Err(FieldError::InvalidSize { + expected: n, + actual: rhs.len(), + }); + } + let mut matrix = (0..n) + .map(|row| { + thetas + .iter() + .map(|&theta| theta.frobenius_inv_pow(row)) + .collect::>() + }) + .collect::>(); + let mut values = rhs.to_vec(); + + for col in 0..n { + let pivot = (col..n) + .find(|&row| !matrix[row][col].is_zero()) + .ok_or_else(|| { + FieldError::InvalidInput("singular Frobenius Moore-type matrix".to_string()) + })?; + if pivot != col { + matrix.swap(col, pivot); + values.swap(col, pivot); + } + let inv = matrix[col][col].inverse().ok_or_else(|| { + FieldError::InvalidInput("singular Frobenius Moore-type matrix".to_string()) + })?; + for entry in &mut matrix[col][col..] { + *entry *= inv; + } + values[col] *= inv; + + let pivot_tail = matrix[col][col..].to_vec(); + let pivot_value = values[col]; + for row in 0..n { + if row == col { + continue; + } + let factor = matrix[row][col]; + if factor.is_zero() { + continue; + } + for (entry, &pivot_entry) in matrix[row][col..].iter_mut().zip(pivot_tail.iter()) { + *entry -= factor * pivot_entry; + } + values[row] -= factor * pivot_value; + } + } + Ok(values) +} + +/// Validate that the canonical theta family gives a nonsingular Moore-type +/// matrix for `width`. +/// +/// # Errors +/// +/// Returns an error if theta construction fails or the Moore solve rejects. +pub fn validate_canonical_frobenius_thetas(width: usize) -> Result<(), FieldError> +where + F: PseudoMersenneField, + E: FrobeniusExtField, +{ + let thetas = canonical_frobenius_thetas::(width)?; + let rhs = (0..width) + .map(|idx| E::lift_base(F::from_u64((idx + 1) as u64))) + .collect::>(); + solve_frobenius_moore::(&thetas, &rhs).map(|_| ()) +} + +impl ExtField for F { + const EXT_DEGREE: usize = 1; + + #[inline] + fn from_base_slice(coeffs: &[F]) -> Self { + assert_eq!(coeffs.len(), 1); + coeffs[0] + } + + #[inline] + fn to_base_vec(&self) -> Vec { + vec![*self] + } +} + +impl ExtField for FpExt2 +where + F: FieldCore + FromPrimitiveInt, + C: FpExt2Config, +{ + const EXT_DEGREE: usize = 2; + + #[inline] + fn from_base_slice(coeffs: &[F]) -> Self { + assert_eq!(coeffs.len(), 2); + Self::new(coeffs[0], coeffs[1]) + } + + #[inline] + fn to_base_vec(&self) -> Vec { + vec![self.coeffs[0], self.coeffs[1]] + } +} + +impl ExtField for FpExt4 +where + F: FieldCore + FromPrimitiveInt + FpExt4MulBackend, +{ + const EXT_DEGREE: usize = 4; + + #[inline] + fn from_base_slice(coeffs: &[F]) -> Self { + assert_eq!(coeffs.len(), 4); + Self::new([coeffs[0], coeffs[1], coeffs[2], coeffs[3]]) + } + + #[inline] + fn to_base_vec(&self) -> Vec { + self.coeffs.to_vec() + } +} + +impl ExtField for FpExt8 +where + F: FieldCore + FromPrimitiveInt + FpExt8MulBackend, +{ + const EXT_DEGREE: usize = 8; + + #[inline] + fn from_base_slice(coeffs: &[F]) -> Self { + assert_eq!(coeffs.len(), 8); + Self::new([ + coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4], coeffs[5], coeffs[6], coeffs[7], + ]) + } + + #[inline] + fn to_base_vec(&self) -> Vec { + self.coeffs.to_vec() + } +} + +impl LiftBase for F { + #[inline] + fn lift_base(x: F) -> Self { + x + } +} + +impl MulBase for F { + #[inline] + fn mul_base(self, x: F) -> Self { + self * x + } +} + +impl LiftBase for FpExt2 +where + F: FieldCore, + C: FpExt2Config, +{ + #[inline] + fn lift_base(x: F) -> Self { + Self::new(x, F::zero()) + } +} + +impl MulBase for FpExt2 +where + F: FieldCore, + C: FpExt2Config, +{ + #[inline] + fn mul_base(self, x: F) -> Self { + Self::new(self.coeffs[0] * x, self.coeffs[1] * x) + } +} + +impl LiftBase for FpExt4 +where + F: FieldCore + FpExt4MulBackend, +{ + #[inline] + fn lift_base(x: F) -> Self { + Self::new([x, F::zero(), F::zero(), F::zero()]) + } +} + +impl MulBase for FpExt4 +where + F: FieldCore + FpExt4MulBackend, +{ + #[inline] + fn mul_base(self, x: F) -> Self { + Self::new(std::array::from_fn(|i| self.coeffs[i] * x)) + } +} + +impl LiftBase for FpExt8 +where + F: FieldCore + FpExt8MulBackend, +{ + #[inline] + fn lift_base(x: F) -> Self { + Self::new([ + x, + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + ]) + } +} + +impl MulBase for FpExt8 +where + F: FieldCore + FpExt8MulBackend, +{ + #[inline] + fn mul_base(self, x: F) -> Self { + Self::new(std::array::from_fn(|i| self.coeffs[i] * x)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Fp32, NegOneNr}; + + type F = Fp32<251>; + type E2 = FpExt2; + type E4 = FpExt4; + + #[test] + fn mul_base_matches_full_multiply_for_base_field() { + let x = F::from_u64(7); + let scalar = F::from_u64(11); + + assert_eq!(x.mul_base(scalar), x * scalar); + } + + #[test] + fn mul_base_matches_full_multiply_for_fp_ext2() { + let x = E2::new(F::from_u64(3), F::from_u64(5)); + let scalar = F::from_u64(11); + + assert_eq!(x.mul_base(scalar), x * E2::lift_base(scalar)); + } + + #[test] + fn mul_base_matches_full_multiply_for_fp_ext4() { + let x = E4::new([ + F::from_u64(3), + F::from_u64(5), + F::from_u64(7), + F::from_u64(13), + ]); + let scalar = F::from_u64(11); + + assert_eq!(x.mul_base(scalar), x * E4::lift_base(scalar)); + } +} diff --git a/crates/jolt-field/src/ext/mod.rs b/crates/jolt-field/src/ext/mod.rs new file mode 100644 index 0000000000..a17a87321b --- /dev/null +++ b/crates/jolt-field/src/ext/mod.rs @@ -0,0 +1,45 @@ +//! Quadratic, quartic, and octic extension fields. +//! +//! Akita supports one concrete degree-4 and degree-8 extension over each prime +//! base field (`FpExt4`, `FpExt8`): the cyclotomic ring-subfield basis used by +//! trace reduction and production fp32 presets. There is no alternate power- or +//! tower-basis quartic implementation. + +mod fp_ext2; +mod fp_ext4; +mod fp_ext8; +pub(crate) mod lift; +mod native_algebra; +#[cfg(test)] +mod tests; + +use super::prime::{Fp128, Fp32, Fp64}; +use super::unreduced::{ + AccumPair, FoldMatrixFp32, FoldMatrixFp64, FpExt2Fp64ProductAccum, FpExt4Fp32ProductAccum, + HasOptimizedFold, HasUnreducedOps, +}; +use crate::{ + BalancedDigitLookup, CanonicalField, FieldCore, FromPrimitiveInt, HalvingField, Invertible, + MulBaseUnreduced, RandomSampling, RingCore, +}; +use rand_core::RngCore; +use std::marker::PhantomData; +use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +pub use fp_ext2::{Ext2, FpExt2, FpExt2Config, NegOneNr, TwoNr}; +pub use fp_ext4::{FpExt4, FpExt4MulBackend}; +pub(crate) use fp_ext8::{fp_ext8_mul_schedule, fp_ext8_square_schedule}; +pub use fp_ext8::{FpExt8, FpExt8MulBackend}; + +/// Arithmetic shape shared by scalar and packed extension coefficients. +pub trait ExtensionCoeff: + Copy + Add + Sub + Mul +{ +} + +impl ExtensionCoeff for A +where + F: FieldCore, + A: Copy + Add + Sub + Mul, +{ +} diff --git a/crates/jolt-field/src/ext/native_algebra.rs b/crates/jolt-field/src/ext/native_algebra.rs new file mode 100644 index 0000000000..bf4e67fdc3 --- /dev/null +++ b/crates/jolt-field/src/ext/native_algebra.rs @@ -0,0 +1,229 @@ +//! Native `num_traits`/`std` supertrait impls and core-algebra markers for the +//! extension field types (`FpExt2`, `FpExt4`, `FpExt8`). +//! +//! These are the Jolt-free supertrait obligations of the native +//! [`AdditiveGroup`]/[`FieldCore`] hierarchy. The non-trivial `RingCore::square` +//! / `Invertible::inverse` impls stay co-located with each extension type. + +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::iter::{Product, Sum}; + +use num_traits::{One, Zero}; + +use super::{FpExt2, FpExt2Config, FpExt4, FpExt4MulBackend, FpExt8, FpExt8MulBackend}; +use crate::{AdditiveGroup, FieldCore}; + +// --- FpExt2 ----------------------------------------------------------------- + +impl> Zero for FpExt2 { + #[inline] + fn zero() -> Self { + Self::new(F::zero(), F::zero()) + } + + #[inline] + fn is_zero(&self) -> bool { + self.coeffs[0].is_zero() && self.coeffs[1].is_zero() + } +} + +impl> One for FpExt2 { + #[inline] + fn one() -> Self { + Self::new(F::one(), F::zero()) + } +} + +impl> fmt::Display for FpExt2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}, {})", self.coeffs[0], self.coeffs[1]) + } +} + +impl> Hash for FpExt2 { + fn hash(&self, state: &mut H) { + self.coeffs[0].hash(state); + self.coeffs[1].hash(state); + } +} + +impl> Sum for FpExt2 { + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), |acc, x| acc + x) + } +} + +impl<'a, F: FieldCore, C: FpExt2Config> Sum<&'a Self> for FpExt2 { + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), |acc, x| acc + *x) + } +} + +impl> Product for FpExt2 { + fn product>(iter: I) -> Self { + iter.fold(Self::one(), |acc, x| acc * x) + } +} + +impl<'a, F: FieldCore, C: FpExt2Config> Product<&'a Self> for FpExt2 { + fn product>(iter: I) -> Self { + iter.fold(Self::one(), |acc, x| acc * *x) + } +} + +impl> AdditiveGroup for FpExt2 {} +impl> FieldCore for FpExt2 {} + +// --- FpExt4 ----------------------------------------------------- + +impl Zero for FpExt4 { + #[inline] + fn zero() -> Self { + Self::new([F::zero(), F::zero(), F::zero(), F::zero()]) + } + + #[inline] + fn is_zero(&self) -> bool { + self.coeffs.iter().all(|coeff| coeff.is_zero()) + } +} + +impl One for FpExt4 { + #[inline] + fn one() -> Self { + Self::new([F::one(), F::zero(), F::zero(), F::zero()]) + } +} + +impl fmt::Display for FpExt4 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "({}, {}, {}, {})", + self.coeffs[0], self.coeffs[1], self.coeffs[2], self.coeffs[3] + ) + } +} + +impl Hash for FpExt4 { + fn hash(&self, state: &mut H) { + self.coeffs.hash(state); + } +} + +impl Sum for FpExt4 { + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), |acc, x| acc + x) + } +} + +impl<'a, F: FieldCore> Sum<&'a Self> for FpExt4 { + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), |acc, x| acc + *x) + } +} + +impl Product for FpExt4 { + fn product>(iter: I) -> Self { + iter.fold(Self::one(), |acc, x| acc * x) + } +} + +impl<'a, F: FieldCore + FpExt4MulBackend> Product<&'a Self> for FpExt4 { + fn product>(iter: I) -> Self { + iter.fold(Self::one(), |acc, x| acc * *x) + } +} + +impl AdditiveGroup for FpExt4 {} +impl FieldCore for FpExt4 {} + +// --- FpExt8 ----------------------------------------------------- + +impl Zero for FpExt8 { + #[inline] + fn zero() -> Self { + Self::new([ + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + ]) + } + + #[inline] + fn is_zero(&self) -> bool { + self.coeffs.iter().all(|coeff| coeff.is_zero()) + } +} + +impl One for FpExt8 { + #[inline] + fn one() -> Self { + Self::new([ + F::one(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + ]) + } +} + +impl fmt::Display for FpExt8 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "({}, {}, {}, {}, {}, {}, {}, {})", + self.coeffs[0], + self.coeffs[1], + self.coeffs[2], + self.coeffs[3], + self.coeffs[4], + self.coeffs[5], + self.coeffs[6], + self.coeffs[7] + ) + } +} + +impl Hash for FpExt8 { + fn hash(&self, state: &mut H) { + self.coeffs.hash(state); + } +} + +impl Sum for FpExt8 { + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), |acc, x| acc + x) + } +} + +impl<'a, F: FieldCore> Sum<&'a Self> for FpExt8 { + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), |acc, x| acc + *x) + } +} + +impl Product for FpExt8 { + fn product>(iter: I) -> Self { + iter.fold(Self::one(), |acc, x| acc * x) + } +} + +impl<'a, F: FieldCore + FpExt8MulBackend> Product<&'a Self> for FpExt8 { + fn product>(iter: I) -> Self { + iter.fold(Self::one(), |acc, x| acc * *x) + } +} + +impl AdditiveGroup for FpExt8 {} +impl FieldCore for FpExt8 {} diff --git a/crates/jolt-field/src/ext/tests.rs b/crates/jolt-field/src/ext/tests.rs new file mode 100644 index 0000000000..74a230b5a9 --- /dev/null +++ b/crates/jolt-field/src/ext/tests.rs @@ -0,0 +1,574 @@ +#![expect( + clippy::expect_used, + clippy::unreadable_literal, + clippy::unwrap_used, + reason = "tests assert field identities and retain copied field constants" +)] + +use super::*; +use crate::ext::lift::{ + canonical_frobenius_thetas, solve_frobenius_moore, validate_canonical_frobenius_thetas, + ExtField, FrobeniusExtField, +}; +use crate::Fp64; +use crate::{FromPrimitiveInt, Invertible}; +use rand::rngs::StdRng; +use rand::SeedableRng; + +type F = Fp64<4294967197>; +type E2 = Ext2; +type E4 = FpExt4; +type R4 = FpExt4; +type R8 = FpExt8; + +#[test] +fn fp_ext2_add_sub_identity() { + let a = E2::new(F::from_u64(3), F::from_u64(5)); + let b = E2::new(F::from_u64(7), F::from_u64(11)); + let c = a + b; + assert_eq!(c - b, a); + assert_eq!(c - a, b); +} + +#[test] +fn fp_ext2_mul_one() { + let a = E2::new(F::from_u64(42), F::from_u64(13)); + assert_eq!(a * E2::one(), a); + assert_eq!(E2::one() * a, a); +} + +#[test] +fn fp_ext2_mul_commutativity() { + let mut rng = StdRng::seed_from_u64(1234); + let a = E2::random(&mut rng); + let b = E2::random(&mut rng); + assert_eq!(a * b, b * a); +} + +#[test] +fn fp_ext2_karatsuba_matches_schoolbook() { + let mut rng = StdRng::seed_from_u64(5678); + for _ in 0..100 { + let a = E2::random(&mut rng); + let b = E2::random(&mut rng); + let nr = >::non_residue(); + let expected = E2::new( + (a.coeffs[0] * b.coeffs[0]) + (nr * (a.coeffs[1] * b.coeffs[1])), + (a.coeffs[0] * b.coeffs[1]) + (a.coeffs[1] * b.coeffs[0]), + ); + assert_eq!(a * b, expected); + } +} + +#[test] +fn fp_ext2_square_matches_mul() { + let mut rng = StdRng::seed_from_u64(9012); + for _ in 0..100 { + let a = E2::random(&mut rng); + assert_eq!(a.square(), a * a, "square mismatch for {a:?}"); + } +} + +#[test] +fn fp_ext2_inv() { + let mut rng = StdRng::seed_from_u64(3456); + for _ in 0..50 { + let a = E2::random(&mut rng); + if !a.is_zero() { + let inv = a.inverse().unwrap(); + assert_eq!(a * inv, E2::one()); + } + } +} + +#[test] +fn fp_ext4_mul_commutativity() { + let mut rng = StdRng::seed_from_u64(7890); + let a = E4::random(&mut rng); + let b = E4::random(&mut rng); + assert_eq!(a * b, b * a); +} + +#[test] +fn fp_ext4_square_matches_mul() { + let mut rng = StdRng::seed_from_u64(1111); + for _ in 0..50 { + let a = E4::random(&mut rng); + assert_eq!(a.square(), a * a); + } +} + +#[test] +fn fp_ext4_inv() { + let mut rng = StdRng::seed_from_u64(2222); + for _ in 0..50 { + let a = E4::random(&mut rng); + if !a.is_zero() { + let inv = a.inverse().unwrap(); + assert_eq!(a * inv, E4::one()); + } + } +} + +#[test] +fn fp_ext4_multiplication_table() { + let two = F::from_u64(2); + let e1 = R4::new([F::zero(), F::one(), F::zero(), F::zero()]); + let e2 = R4::new([F::zero(), F::zero(), F::one(), F::zero()]); + let e3 = R4::new([F::zero(), F::zero(), F::zero(), F::one()]); + let two_const = R4::new([two, F::zero(), F::zero(), F::zero()]); + + assert_eq!(e1 * e1, two_const + e2); + assert_eq!(e1 * e2, e1 + e3); + assert_eq!(e1 * e3, e2); + assert_eq!(e2 * e2, two_const); + assert_eq!(e2 * e3, e1 - e3); + assert_eq!(e3 * e3, two_const - e2); +} + +#[test] +fn fp_ext8_multiplication_table_spot_checks() { + let two = F::from_u64(2); + let e = |idx: usize| { + R8::new(std::array::from_fn(|i| { + if i == idx { + F::one() + } else { + F::zero() + } + })) + }; + let two_const = R8::new([ + two, + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + ]); + + assert_eq!(e(1) * e(1), two_const + e(2)); + assert_eq!(e(2) * e(2), two_const + e(4)); + assert_eq!(e(4) * e(4), two_const); + assert_eq!(e(7) * e(7), two_const - e(2)); + assert_eq!(e(5) * e(7), e(2) - e(4)); +} + +#[test] +fn fp_ext8_square_matches_mul() { + let mut rng = StdRng::seed_from_u64(7777); + for _ in 0..50 { + let a = R8::random(&mut rng); + assert_eq!(a.square(), a * a); + } +} + +#[test] +fn fp_ext8_inv() { + let mut rng = StdRng::seed_from_u64(8888); + for _ in 0..50 { + let a = R8::random(&mut rng); + if !a.is_zero() { + let inv = a.inverse().unwrap(); + assert_eq!(a * inv, R8::one()); + } + } +} + +#[test] +fn frobenius_fp_ext2_is_conjugation() { + let x = E2::new(F::from_u64(13), F::from_u64(21)); + assert_eq!(>::frobenius_pow(x, 0), x); + assert_eq!( + >::frobenius_pow(x, 1), + x.conjugate() + ); + assert_eq!(>::frobenius_pow(x, 2), x); + assert_eq!( + >::frobenius_inv_pow(x, 1), + x.conjugate() + ); +} + +#[test] +fn canonical_moore_thetas_solve_fp_ext2() { + validate_canonical_frobenius_thetas::(2).unwrap(); + let thetas = canonical_frobenius_thetas::(2).unwrap(); + let z = [ + E2::new(F::from_u64(3), F::from_u64(5)), + E2::new(F::from_u64(7), F::from_u64(11)), + ]; + let r = (0..2) + .map(|row| { + thetas + .iter() + .zip(z.iter()) + .fold(E2::zero(), |acc, (&theta, &z_h)| { + acc + >::frobenius_inv_pow(theta, row) * z_h + }) + }) + .collect::>(); + assert_eq!( + solve_frobenius_moore::(&thetas, &r).unwrap(), + z.to_vec() + ); +} + +#[test] +fn canonical_ring_subfield_thetas_are_the_packing_basis() { + let thetas = canonical_frobenius_thetas::(4).unwrap(); + assert_eq!( + thetas[0], + R4::new([F::one(), F::zero(), F::zero(), F::zero()]) + ); + assert_eq!( + thetas[1], + R4::new([F::zero(), F::one(), F::zero(), F::zero()]) + ); + assert_eq!( + thetas[2], + R4::new([F::zero(), F::zero(), F::one(), F::zero()]) + ); + assert_eq!( + thetas[3], + R4::new([F::zero(), F::zero(), F::zero(), F::one()]) + ); + validate_canonical_frobenius_thetas::(4).unwrap(); +} + +#[test] +fn canonical_fp_ext8_thetas_are_the_packing_basis() { + let thetas = canonical_frobenius_thetas::(8).unwrap(); + for (idx, theta) in thetas.iter().enumerate().take(8) { + assert_eq!( + *theta, + R8::new(std::array::from_fn(|i| { + if i == idx { + F::one() + } else { + F::zero() + } + })) + ); + } + validate_canonical_frobenius_thetas::(8).unwrap(); +} + +#[test] +fn duplicate_moore_theta_rejects() { + let theta = E2::one(); + let err = solve_frobenius_moore::(&[theta, theta], &[E2::one(), E2::one()]) + .expect_err("duplicate theta should be singular"); + assert!(format!("{err}").contains("singular")); +} + +#[test] +fn from_small_int_fp_ext2() { + let a = E2::from_u64(42); + assert_eq!(a, E2::new(F::from_u64(42), F::zero())); + + let b = E2::from_i64(-3); + assert_eq!(b, E2::new(F::from_i64(-3), F::zero())); + + let c = E2::from_u8(7); + assert_eq!(c, E2::from_u64(7)); + + let d = E2::from_u32(100_000); + assert_eq!(d, E2::from_u64(100_000)); +} + +#[test] +fn from_small_int_fp_ext4() { + let a = E4::from_u64(42); + assert_eq!( + a, + E4::new([F::from_u64(42), F::zero(), F::zero(), F::zero(),]) + ); + + let b = E4::from_i64(-7); + assert_eq!( + b, + E4::new([F::from_i64(-7), F::zero(), F::zero(), F::zero(),]) + ); +} + +#[test] +fn ext_field_degree() { + assert_eq!(>::EXT_DEGREE, 1); + assert_eq!(>::EXT_DEGREE, 2); + assert_eq!(>::EXT_DEGREE, 4); + assert_eq!(>::EXT_DEGREE, 4); + assert_eq!(>::EXT_DEGREE, 8); +} + +#[test] +fn ext_field_from_base_slice() { + let c0 = F::from_u64(3); + let c1 = F::from_u64(5); + let e2 = E2::from_base_slice(&[c0, c1]); + assert_eq!(e2, E2::new(c0, c1)); + + let c2 = F::from_u64(7); + let c3 = F::from_u64(11); + let e4 = E4::from_base_slice(&[c0, c1, c2, c3]); + assert_eq!(e4, E4::new([c0, c1, c2, c3])); + + let r4 = R4::from_base_slice(&[c0, c1, c2, c3]); + assert_eq!(r4, R4::new([c0, c1, c2, c3])); + + let c4 = F::from_u64(13); + let c5 = F::from_u64(17); + let c6 = F::from_u64(19); + let c7 = F::from_u64(23); + let r8 = R8::from_base_slice(&[c0, c1, c2, c3, c4, c5, c6, c7]); + assert_eq!(r8, R8::new([c0, c1, c2, c3, c4, c5, c6, c7])); +} + +#[test] +fn extension_fields_are_array_layouts() { + assert_eq!(core::mem::size_of::(), core::mem::size_of::<[F; 2]>()); + assert_eq!(core::mem::align_of::(), core::mem::align_of::<[F; 2]>()); + assert_eq!(core::mem::size_of::(), core::mem::size_of::<[F; 4]>()); + assert_eq!(core::mem::align_of::(), core::mem::align_of::<[F; 4]>()); +} + +#[test] +fn eq_impl() { + let a = E2::new(F::from_u64(1), F::from_u64(2)); + let b = E2::new(F::from_u64(1), F::from_u64(2)); + let c = E2::new(F::from_u64(1), F::from_u64(3)); + assert_eq!(a, b); + assert_ne!(a, c); +} + +#[test] +fn fp_ext4_fp32_product_accum_matches_direct_mul() { + use super::fp_ext4::fp_ext4_mul_to_accum_fp32; + use crate::unreduced::FpExt4Fp32ProductAccum; + use crate::Fp32; + use num_traits::Zero; + + type Fp = Fp32<251>; + type R4Fp32 = FpExt4; + + let mut rng = StdRng::seed_from_u64(0xACC0); + for _ in 0..200 { + let a = R4Fp32::random(&mut rng); + let b = R4Fp32::random(&mut rng); + let direct = a * b; + let accum = fp_ext4_mul_to_accum_fp32(a.coeffs, b.coeffs); + let reduced = R4Fp32::new(accum.reduce::<251>()); + assert_eq!(direct, reduced, "accum mismatch for a={a:?} b={b:?}"); + } + + let zero_accum = FpExt4Fp32ProductAccum::ZERO; + assert!(zero_accum.is_zero()); + let reduced_zero = R4Fp32::new(zero_accum.reduce::<251>()); + assert_eq!(reduced_zero, R4Fp32::zero()); +} + +#[test] +fn fp_ext4_fp32_accum_summation() { + use crate::Fp32; + use num_traits::Zero; + + type Fp = Fp32<251>; + type R4Fp32 = FpExt4; + + let mut rng = StdRng::seed_from_u64(0xACC1); + let n = 1024; + let pairs: Vec<(R4Fp32, R4Fp32)> = (0..n) + .map(|_| (R4Fp32::random(&mut rng), R4Fp32::random(&mut rng))) + .collect(); + + let direct_sum: R4Fp32 = pairs + .iter() + .map(|(a, b)| *a * *b) + .fold(R4Fp32::zero(), |s, p| s + p); + + let accum_sum = pairs.iter().fold( + ::ProductAccum::zero(), + |s, (a, b)| s + a.mul_to_product_accum(*b), + ); + let reduced = R4Fp32::reduce_product_accum(accum_sum); + + assert_eq!( + direct_sum, reduced, + "accumulated sum of {n} products mismatched" + ); +} + +#[test] +fn mul_base_to_product_accum_matches_mul_base_sum() { + use crate::{Fp32, MulBaseUnreduced}; + use num_traits::Zero; + + fn check(seed: u64) + where + Base: FieldCore + RandomSampling, + Ext: MulBaseUnreduced + Zero + RandomSampling, + { + let mut rng = StdRng::seed_from_u64(seed); + let n = 1024; + let pairs: Vec<(Ext, Base)> = (0..n) + .map(|_| (Ext::random(&mut rng), Base::random(&mut rng))) + .collect(); + + let direct: Ext = pairs + .iter() + .map(|(w, x)| w.mul_base(*x)) + .fold(Ext::zero(), |s, p| s + p); + + let accum = pairs.iter().fold( + ::ProductAccum::zero(), + |s, (w, x)| s + w.mul_base_to_product_accum(*x), + ); + + assert_eq!( + direct, + Ext::reduce_product_accum(accum), + "delayed base-scaling mismatch over {n} terms" + ); + } + + // fp_ext4/Fp32 takes the optimal coordinate-scaling override; fp_ext2/Fp64 + // takes the lifted default body. Both defer reduction. + check::, FpExt4>>(0xB001); + check::>(0xB002); +} + +// Regression guard for the `FpExt2` delayed-reduction accumulator. The earlier +// bug dropped the carry into bit 128 because each FpExt2 coefficient (c0 up to ~2^130, +// c1 up to ~2^129) was formed in a single `u128`. It only surfaces with near-`p` +// operands -- products around 2^128 -- which the small-modulus tests never reach, +// so these use the real 2^64-59 prime and cover both FpExt2 configs. +#[test] +fn fp_ext2_fp64_product_accum_matches_direct_mul_large_operands() { + use crate::Prime64Offset59; + + let mut rng = StdRng::seed_from_u64(0xF64A); + for _ in 0..256 { + // TwoNr (IS_NEG_ONE = false): c0 = p00 + 2*p11. + let a = Ext2::::random(&mut rng); + let b = Ext2::::random(&mut rng); + assert_eq!( + a * b, + Ext2::::reduce_product_accum(a.mul_to_product_accum(b)), + "TwoNr accum mismatch a={a:?} b={b:?}" + ); + + // NegOneNr (IS_NEG_ONE = true): c0 = p00 + p^2 - p11. + let c = FpExt2::::random(&mut rng); + let d = FpExt2::::random(&mut rng); + assert_eq!( + c * d, + FpExt2::::reduce_product_accum(c.mul_to_product_accum(d)), + "NegOneNr accum mismatch c={c:?} d={d:?}" + ); + } +} + +#[test] +fn fp_ext2_fp64_accum_summation_large_operands() { + use crate::Prime64Offset59; + use num_traits::Zero; + + type E = Ext2; + + let mut rng = StdRng::seed_from_u64(0xF64C); + let n = 1024; + let pairs: Vec<(E, E)> = (0..n) + .map(|_| (E::random(&mut rng), E::random(&mut rng))) + .collect(); + + let direct_sum: E = pairs + .iter() + .map(|(a, b)| *a * *b) + .fold(E::zero(), |s, p| s + p); + + let accum_sum = pairs + .iter() + .fold(::ProductAccum::zero(), |s, (a, b)| { + s + a.mul_to_product_accum(*b) + }); + + assert_eq!( + direct_sum, + E::reduce_product_accum(accum_sum), + "fp_ext2 accumulated sum of {n} products mismatched" + ); +} + +// The specialized `FpExt2` EOR fold must be byte-identical to the generic +// `even + r·(odd − even)`. Full-word `Prime64Offset59` exercises the +// carry-folding reduction path (products near 2^128, sum near 2^129); +// sub-word `Prime40Offset195` exercises the no-overflow path. Random operands +// reach carry=1 roughly half the time; the explicit max-coordinate cases pin +// the worst case. Covers both `FpExt2Config`s (TwoNr and NegOneNr). +#[test] +fn fp_ext2_fp64_optimized_fold_matches_generic() { + use crate::{Prime40Offset195, Prime64Offset59}; + + macro_rules! check_fold { + ($E:ty, $r:expr, $even:expr, $odd:expr) => {{ + let r: $E = $r; + let even: $E = $even; + let odd: $E = $odd; + let generic = even + r * (odd - even); + let ctx = <$E as HasOptimizedFold>::precompute_fold(r); + let optimized = <$E as HasOptimizedFold>::fold_one(&ctx, even, odd); + assert_eq!( + generic, + optimized, + "{} fold mismatch r={r:?} even={even:?} odd={odd:?}", + stringify!($E) + ); + }}; + } + + let mut rng = StdRng::seed_from_u64(0xF01D); + for _ in 0..512 { + check_fold!( + Ext2, + Ext2::random(&mut rng), + Ext2::random(&mut rng), + Ext2::random(&mut rng) + ); + check_fold!( + FpExt2, + FpExt2::random(&mut rng), + FpExt2::random(&mut rng), + FpExt2::random(&mut rng) + ); + check_fold!( + Ext2, + Ext2::random(&mut rng), + Ext2::random(&mut rng), + Ext2::random(&mut rng) + ); + check_fold!( + FpExt2, + FpExt2::random(&mut rng), + FpExt2::random(&mut rng), + FpExt2::random(&mut rng) + ); + } + + // Worst case for the full-word carry fold: all coordinates at p-1, so each + // base product is ≈ p² ≈ 2^128 and the per-coordinate sum is ≈ 2^129. + let max64 = Prime64Offset59::zero() - Prime64Offset59::one(); + check_fold!( + Ext2, + Ext2::new(max64, max64), + Ext2::zero(), + Ext2::new(max64, max64) + ); + check_fold!( + FpExt2, + FpExt2::new(max64, max64), + FpExt2::zero(), + FpExt2::new(max64, max64) + ); +} diff --git a/crates/jolt-field/src/fft.rs b/crates/jolt-field/src/fft.rs new file mode 100644 index 0000000000..1a87a54a84 --- /dev/null +++ b/crates/jolt-field/src/fft.rs @@ -0,0 +1,1098 @@ +//! Mixed-radix FFT over prime fields with smooth-order multiplicative subgroups. + +#![expect( + clippy::expect_used, + reason = "constructed FFT plans establish the indexed root and factor invariants" +)] +//! +//! # Setting +//! +//! The protocol primes [`crate::Prime128Offset2355`] +//! and [`crate::Prime128OffsetA7F7`] are pseudo-Mersenne, so +//! `p − 1` is not a power of two; each is instead chosen so it carries +//! a large **smooth factor** — a product of small primes: +//! +//! - `p = 2^128 − 2355`: smooth order `14_700 = 2² · 3 · 5² · 7²` +//! - `p = 2^128 − 2^32 + 22_537`: smooth order `17_496 = 2³ · 3⁷` +//! +//! FFT domain sizes are divisors of that smooth order; there is no +//! power-of-two NTT to fall back on. The primary use case is FFT-based +//! Reed-Solomon encoding inside the protocol. +//! +//! # Algorithm +//! +//! Iterative Cooley-Tukey decimation-in-time (DIT). For a domain size +//! `n = f_0 · f_1 · … · f_{s−1}` (each `f_i` a small prime, ≤ 7 in +//! practice), the size-`n` DFT factors recursively into size-`f_i` +//! DFTs combined with twiddle multiplications. The iterative form +//! permutes the input by mixed-radix digit reversal up front, then +//! sweeps `s` stages bottom-up, running radix-`f_i` butterflies in +//! place at each stage. +//! +//! # Optimizations +//! +//! All precomputed once when a `SmoothDomain` is built and reused +//! across transforms: +//! +//! - **Stage plan** (`factorize`, `digit_reversal_permutation`): the +//! per-stage radices and digit-reversal permutation are fixed at +//! construction. +//! - **Twiddle tables** (`StageData::twiddle_table`): the `ω^{jk}` +//! factor the DIT formula uses at every butterfly becomes a table +//! lookup plus a small power-up loop, replacing a `field_pow` call +//! per butterfly. +//! - **Ping-pong buffers** (`FftWorkspace`): the two length-`n` working +//! buffers are pre-allocated, so the transform itself is allocation-free. +//! - **Low-multiplication radix kernels** +//! (`FftWorkspace::butterfly_stages`): the size-`r` DFT inside each +//! butterfly is hand-tuned per radix, taking the multiplication +//! count from the naive `r²` down to `1, 2, 6, 18` for +//! `r ∈ {2, 3, 5, 7}` (radix 3 uses `1 + ω + ω² = 0`; radix 5 / 7 +//! use Karatsuba on the conjugate-pair-symmetrized inputs, with +//! the constants precomputed in `StageData::winograd`). +//! - **Smooth-subgroup-derived roots** +//! ([`primitive_nth_root`](crate::fft::primitive_nth_root)): +//! `ω_n` is one exponentiation of the field's compile-time +//! `SmoothFftField::SMOOTH_OMEGA` literal — no runtime base scan. +//! +//! # Coset evaluation and RS-extend +//! +//! Reed-Solomon extension interpolates a polynomial through the `k` +//! known evaluations (one inverse FFT) then evaluates it on +//! `blowup − 1` cosets of the base subgroup. Each coset evaluation is +//! a coset FFT — pre-twist `c_i ← c_i · s^i` then run a plain forward +//! FFT — see [`SmoothDomain::coset_forward`](crate::fft::SmoothDomain::coset_forward) +//! and [`SmoothDomain::rs_extend_batch`](crate::fft::SmoothDomain::rs_extend_batch). + +use crate::{FieldCore, FromPrimitiveInt, Invertible, SmoothFftField}; + +/// Compute `base^exp` by repeated squaring. +#[inline] +pub fn field_pow(base: F, mut exp: u64) -> F { + let mut result = F::one(); + let mut b = base; + while exp > 0 { + if exp & 1 == 1 { + result *= b; + } + b *= b; + exp >>= 1; + } + result +} + +/// Compute `base^exp` for u128 exponents. Test-only scanner helper. +#[cfg(test)] +pub(crate) fn field_pow_u128(base: F, mut exp: u128) -> F { + let mut result = F::one(); + let mut b = base; + while exp > 0 { + if exp & 1 == 1 { + result *= b; + } + b *= b; + exp >>= 1; + } + result +} + +/// Smallest prime factor of `n` (returns `n` itself if `n ≤ 1` or is prime). +fn smallest_prime_factor(n: usize) -> usize { + if n <= 1 { + return n; + } + for &p in &[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31] { + if n.is_multiple_of(p) { + return p; + } + } + let mut i = 37; + while i * i <= n { + if n.is_multiple_of(i) { + return i; + } + i += 2; + } + n +} + +/// Prime factorization of `n` (with multiplicity), in non-decreasing order. +/// +/// The mixed-radix decomposition uses each prime factor as the radix of +/// one stage, so e.g. `n = 14_700 = 2² · 3 · 5² · 7²` becomes seven +/// stages `[2, 2, 3, 5, 5, 7, 7]`. +fn factorize(mut n: usize) -> Vec { + let mut factors = Vec::new(); + while n > 1 { + let p = smallest_prime_factor(n); + factors.push(p); + n /= p; + } + factors +} + +/// Mixed-radix digit-reversal permutation, the analogue of bit-reversal +/// for power-of-two FFTs. +/// +/// For `n = f_0 · f_1 · … · f_{s−1}`, write index `k` in mixed-radix +/// form `k = d_0 + d_1 · f_0 + d_2 · f_0 · f_1 + …`; then `perm[k]` is +/// the index whose digits are the reverse sequence. Permuting the +/// input by this table aligns the recursion's base cases at +/// consecutive indices, which is what lets the bottom-up DIT sweep +/// work in place. +fn digit_reversal_permutation(n: usize, factors: &[usize]) -> Vec { + let s = factors.len(); + let mut perm = vec![0usize; n]; + for (k, perm_k) in perm.iter_mut().enumerate() { + let mut digits = vec![0usize; s]; + let mut tmp = k; + for (digit, &f) in digits.iter_mut().zip(factors.iter()) { + *digit = tmp % f; + tmp /= f; + } + let mut rev = 0usize; + for (&f, &d) in factors.iter().zip(digits.iter()) { + rev = rev * f + d; + } + *perm_k = rev; + } + perm +} + +/// Read-only data the inner butterfly loop consults at every stage. +/// +/// One per prime factor of `n`. Stage `i` combines `r` blocks of size +/// `block` (the product of all earlier stages' radices) into a single +/// block of size `block · r`. +struct StageData { + /// Radix of this stage — one of `{2, 3, 5, 7}` in practice. + r: usize, + /// Block size feeding this stage; output blocks are `block · r`. + block: usize, + /// `omega_r_pow[q] = ω_r^q` for `q ∈ 0..r`. Length fixed at 8 for + /// stack storage; entries past `r` stay `1` and are unused. + omega_r_pow: [F; 8], + /// `twiddle_table[j] = ω_{block · r}^j`, indexed by lane within a + /// group. The DIT butterfly's `ω^{jk}` factor decomposes as + /// `(twiddle_table[j])^k`, with `tw^k` materialized on the fly. + twiddle_table: Vec, + /// Precomputed Winograd constants for the low-mul radix-5 / 7 + /// kernels (empty for other radices). Layout: + /// + /// - `r == 5`: `[α/2, β/2, γ/2, δ/2, (α+β)/2, (γ+δ)/2]` where + /// `α = ω+ω⁴`, `β = ω²+ω³`, `γ = ω−ω⁴`, `δ = ω²−ω³`. + /// - `r == 7`: 9 `α_{jk}` then 9 `β_{jk}`, row-major over + /// `(j, k) ∈ {1, 2, 3}²`, with + /// `α_{jk} = (ω^{jk} + ω^{−jk})/2` and + /// `β_{jk} = (ω^{jk} − ω^{−jk})/2`. + /// + /// The `/2` is folded into the stored values so the kernel doesn't + /// halve at every butterfly. + winograd: Vec, +} + +/// Build the per-stage tables consumed by the iterative FFT. +/// +/// Walks `factors` in reverse so the resulting `Vec` is +/// already in bottom-up sweep order. For each stage: +/// +/// - `omega_new_block = omega^{n/(block · r)}` is the principal +/// `(block · r)`-th root of unity used to fill the twiddle table. +/// - `omega_r = omega_new_block^block` is the principal `r`-th root +/// used inside the size-`r` butterfly. +/// +/// Called twice per domain — once with `omega = ω_n` for the forward +/// transform and once with `omega = ω_n^{−1}` for the inverse. +fn precompute_stages( + omega: F, + n: usize, + factors: &[usize], +) -> Vec> { + let mut stages = Vec::with_capacity(factors.len()); + let mut block = 1usize; + + for &r in factors.iter().rev() { + debug_assert!(r <= 8, "radix {r} exceeds omega_r_pow capacity (max 8)"); + let new_block = block * r; + let omega_new_block = field_pow(omega, (n / new_block) as u64); + let omega_r = field_pow(omega_new_block, block as u64); + + let mut omega_r_pow = [F::one(); 8]; + for q in 1..r { + omega_r_pow[q] = omega_r_pow[q - 1] * omega_r; + } + + let mut twiddle_table = Vec::with_capacity(block); + let mut tw = F::one(); + for _ in 0..block { + twiddle_table.push(tw); + tw *= omega_new_block; + } + + let winograd = winograd_consts_for_radix::(r, &omega_r_pow); + + stages.push(StageData { + r, + block, + omega_r_pow, + twiddle_table, + winograd, + }); + + block = new_block; + } + stages +} + +/// Precompute the Winograd constants consumed by the radix-5 / 7 +/// kernels. Returns an empty vector for other radices. See the +/// doc-comment on `StageData::winograd` for the exact layout. +fn winograd_consts_for_radix( + r: usize, + omega_r_pow: &[F; 8], +) -> Vec { + match r { + 5 => { + let w1 = omega_r_pow[1]; + let w2 = omega_r_pow[2]; + let w3 = omega_r_pow[3]; + let w4 = omega_r_pow[4]; + let half = F::from_u64(2) + .inverse() + .expect("2 is invertible in a non-binary field"); + // α = ω+ω⁴, β = ω²+ω³, γ = ω−ω⁴, δ = ω²−ω³. + let alpha_half = (w1 + w4) * half; + let beta_half = (w2 + w3) * half; + let gamma_half = (w1 - w4) * half; + let delta_half = (w2 - w3) * half; + // (α+β)/2 = (Σ_{q=1..4} ω^q)/2 = (-1)/2 since 1+ω+…+ω⁴ = 0. + let ab_half = alpha_half + beta_half; + let gd_half = gamma_half + delta_half; + vec![ + alpha_half, beta_half, gamma_half, delta_half, ab_half, gd_half, + ] + } + 7 => { + // ω^{−q} mod 7 = ω^{7−q}; map negative exponents through + // `rem_euclid` so we can index the precomputed `omega_r_pow` + // table for both signs. + let w = omega_r_pow; + let pow = |q: isize| -> F { + let qq = q.rem_euclid(7) as usize; + w[qq] + }; + let half = F::from_u64(2) + .inverse() + .expect("2 is invertible in a non-binary field"); + let mut out = Vec::with_capacity(18); + // α_{jk} = (ω^{jk} + ω^{−jk})/2, row-major in (j, k). + for j in 1..=3 { + for k in 1..=3 { + let jk = (j * k) as isize; + out.push((pow(jk) + pow(-jk)) * half); + } + } + // β_{jk} = (ω^{jk} − ω^{−jk})/2, row-major in (j, k). + for j in 1..=3 { + for k in 1..=3 { + let jk = (j * k) as isize; + out.push((pow(jk) - pow(-jk)) * half); + } + } + out + } + _ => Vec::new(), + } +} + +/// Pre-allocated ping-pong buffers for an iterative mixed-radix FFT. +/// +/// `buf_a` is updated in place across all stages and holds the result +/// on return. `buf_b` is a scratch slot callers can pre-fill (see +/// `execute_from_b`); reused across the inverse and forward passes +/// inside `rs_extend_batch`. +struct FftWorkspace { + n: usize, + buf_a: Vec, + buf_b: Vec, +} + +impl FftWorkspace { + fn new(n: usize) -> Self { + Self { + n, + buf_a: vec![F::zero(); n], + buf_b: vec![F::zero(); n], + } + } + + /// Run an iterative mixed-radix Cooley-Tukey DIT FFT on `input`: + /// digit-reverse into `buf_a`, then sweep `stages` bottom-up + /// running radix-`r` butterflies in place. Returns a view into + /// `buf_a`. + fn execute(&mut self, input: &[F], stages: &[StageData], digit_rev: &[usize]) -> &[F] { + let n = self.n; + debug_assert_eq!(input.len(), n); + + for (i, &rev_i) in digit_rev.iter().enumerate() { + self.buf_a[rev_i] = input[i]; + } + + self.butterfly_stages(stages); + &self.buf_a[..n] + } + + /// Like [`Self::execute`], but reads the input from a `buf_b` the + /// caller has already populated. Used by `coset_forward` to avoid + /// an extra allocation for the twisted coefficient vector. + fn execute_from_b(&mut self, stages: &[StageData], digit_rev: &[usize]) -> &[F] { + let n = self.n; + + for (i, &rev_i) in digit_rev.iter().enumerate() { + self.buf_a[rev_i] = self.buf_b[i]; + } + + self.butterfly_stages(stages); + &self.buf_a[..n] + } + + /// Bottom-up FFT sweep. For each stage, the outer loop walks + /// independent groups of `block · r` consecutive entries; the + /// middle loop runs the `block` parallel butterflies inside one + /// group; each butterfly does a twiddle phase (scale lane `k` by + /// `twiddle_table[j]^k`) followed by a size-`r` DFT specialized + /// per radix. + fn butterfly_stages(&mut self, stages: &[StageData]) { + let n = self.n; + for stage in stages { + let r = stage.r; + let block = stage.block; + let new_block = block * r; + let omega_r_pow = &stage.omega_r_pow; + let twiddle_table = &stage.twiddle_table; + + for group_start in (0..n).step_by(new_block) { + for (j, tw_entry) in twiddle_table.iter().enumerate() { + let base = group_start + j; + + // Gather the `r` lanes of this butterfly into a + // stack array (cap of 8 is debug-asserted in + // `precompute_stages`). + let mut x = [F::zero(); 8]; + for (ki, xi) in x[..r].iter_mut().enumerate() { + *xi = self.buf_a[base + ki * block]; + } + + if j > 0 { + // Twiddle phase: scale lane k by tw^k. The + // unrolled per-radix sequences below share + // tw², tw³, … across lanes; the generic loop + // covers radices we don't have a tuned kernel + // for. Skipped entirely when j == 0 (tw = 1). + let tw = *tw_entry; + let tw2 = tw * tw; + match r { + 2 => { + x[1] *= tw; + } + 3 => { + x[1] *= tw; + x[2] *= tw2; + } + 5 => { + let tw3 = tw2 * tw; + let tw4 = tw2 * tw2; + x[1] *= tw; + x[2] *= tw2; + x[3] *= tw3; + x[4] *= tw4; + } + 7 => { + let tw3 = tw2 * tw; + let tw4 = tw2 * tw2; + let tw5 = tw4 * tw; + let tw6 = tw3 * tw3; + x[1] *= tw; + x[2] *= tw2; + x[3] *= tw3; + x[4] *= tw4; + x[5] *= tw5; + x[6] *= tw6; + } + _ => { + let mut tw_k = tw; + for xi in &mut x[1..r] { + *xi *= tw_k; + tw_k *= tw; + } + } + } + } + + // DFT phase: hand-tuned size-r kernel per radix. + match r { + 2 => { + self.buf_a[base] = x[0] + x[1]; + self.buf_a[base + block] = x[0] - x[1]; + } + 3 => { + // 2-mul DFT_3 from 1 + ω + ω² = 0: + // S = x₁ + x₂, T = ω·x₁ + ω²·x₂ + // y₀ = x₀ + S, y₁ = x₀ + T, y₂ = x₀ − S − T + let w1 = omega_r_pow[1]; + let w2 = omega_r_pow[2]; + let s = x[1] + x[2]; + let t = x[1] * w1 + x[2] * w2; + self.buf_a[base] = x[0] + s; + self.buf_a[base + block] = x[0] + t; + self.buf_a[base + 2 * block] = x[0] - s - t; + } + 5 => { + // 6-mul DFT_5 via Karatsuba on the + // (A, B) = x_j ± x_{5−j} pairs. Constants + // come from winograd_consts_for_radix(5): + // [α/2, β/2, γ/2, δ/2, (α+β)/2, (γ+δ)/2] + let cc = &stage.winograd; + debug_assert_eq!(cc.len(), 6); + let a_h = cc[0]; + let b_h = cc[1]; + let g_h = cc[2]; + let d_h = cc[3]; + let ab_h = cc[4]; + let gd_h = cc[5]; + + let a = x[1] + x[4]; + let b = x[2] + x[3]; + let c = x[1] - x[4]; + let d = x[2] - x[3]; + + // P-block (cosine, Karatsuba k₁+k₂+k₃): + // P₁ = A·α/2 + B·β/2, P₂ = A·β/2 + B·α/2 + let k1 = a * a_h; + let k2 = b * b_h; + let k3 = (a + b) * ab_h; + let p1 = k1 + k2; + let p2 = k3 - k1 - k2; + + // Q-block (sine, complex-mul Karatsuba): + // Q₁ = C·γ/2 + D·δ/2, Q₂ = C·δ/2 − D·γ/2 + let m1 = c * g_h; + let m2 = d * d_h; + let m3 = (c - d) * gd_h; + let q1 = m1 + m2; + let q2 = m3 - m1 + m2; + + self.buf_a[base] = x[0] + a + b; + self.buf_a[base + block] = x[0] + p1 + q1; + self.buf_a[base + 2 * block] = x[0] + p2 + q2; + self.buf_a[base + 3 * block] = x[0] + p2 - q2; + self.buf_a[base + 4 * block] = x[0] + p1 - q1; + } + 7 => { + // 18-mul DFT_7. Same conjugate-pair idea + // as DFT_5: pair x_j with x_{7−j} into + // A_j = x_j + x_{7−j} (symmetric) and + // B_j = x_j − x_{7−j} (antisymmetric), so + // + // x_j·ω^{jk} + x_{7−j}·ω^{−jk} + // = A_j · α_{jk} + B_j · β_{jk} + // + // with α_{jk}, β_{jk} (already including + // the /2) precomputed in `winograd`. + // Outputs y₄, y₅, y₆ recover by flipping + // the β sign. + let cc = &stage.winograd; + debug_assert_eq!(cc.len(), 18); + + let a1 = x[1] + x[6]; + let a2 = x[2] + x[5]; + let a3 = x[3] + x[4]; + let b1 = x[1] - x[6]; + let b2 = x[2] - x[5]; + let b3 = x[3] - x[4]; + + // α table at offset (j-1)*3 + (k-1). + let s1 = a1 * cc[0] + a2 * cc[3] + a3 * cc[6]; // k = 1 + let s2 = a1 * cc[1] + a2 * cc[4] + a3 * cc[7]; // k = 2 + let s3 = a1 * cc[2] + a2 * cc[5] + a3 * cc[8]; // k = 3 + + // β table at offset 9 + (j-1)*3 + (k-1). + let t1 = b1 * cc[9] + b2 * cc[12] + b3 * cc[15]; + let t2 = b1 * cc[10] + b2 * cc[13] + b3 * cc[16]; + let t3 = b1 * cc[11] + b2 * cc[14] + b3 * cc[17]; + + self.buf_a[base] = x[0] + a1 + a2 + a3; + self.buf_a[base + block] = x[0] + s1 + t1; + self.buf_a[base + 2 * block] = x[0] + s2 + t2; + self.buf_a[base + 3 * block] = x[0] + s3 + t3; + self.buf_a[base + 4 * block] = x[0] + s3 - t3; + self.buf_a[base + 5 * block] = x[0] + s2 - t2; + self.buf_a[base + 6 * block] = x[0] + s1 - t1; + } + _ => { + // Naive O(r²) fallback. + for (q, &wq) in omega_r_pow[..r].iter().enumerate() { + let mut val = x[0]; + let mut w = wq; + for &xp in &x[1..r] { + val += xp * w; + w *= wq; + } + self.buf_a[base + q * block] = val; + } + } + } + } + } + } + } +} + +/// Mixed-radix FFT domain backed by a smooth-order multiplicative subgroup. +/// +/// Holds the immutable state for a fixed-size FFT (roots of unity, +/// digit-reversal permutation, per-stage twiddle tables for both +/// directions). Build once with [`SmoothDomain::new`] and reuse across +/// transforms; `Sync`-safe since all fields are read-only after +/// construction. +pub struct SmoothDomain { + /// Number of points in the FFT domain. + pub n: usize, + /// Primitive `n`-th root of unity that generates the domain. + pub omega: F, + /// `n⁻¹`, applied to normalize the inverse transform. + n_inv: F, + /// Mixed-radix digit-reversal permutation, length `n`. + digit_rev: Vec, + /// Per-stage tables for the forward transform (twiddles in `ω`). + fwd_stages: Vec>, + /// Per-stage tables for the inverse transform (twiddles in `ω⁻¹`). + inv_stages: Vec>, +} + +impl SmoothDomain { + /// Build a domain of size `n` from a primitive `n`-th root of + /// unity. Precomputes the digit-reversal permutation and per-stage + /// tables for both forward and inverse transforms. + /// + /// # Panics + /// If `omega` is zero or `n` is not invertible in the field. + pub fn new(omega: F, n: usize) -> Self { + debug_assert_primitive_nth_root(omega, n); + let omega_inv = omega.inverse().expect("omega must be nonzero"); + let n_inv = F::from_u64(n as u64) + .inverse() + .expect("n must be invertible in field"); + let factors = factorize(n); + let digit_rev = digit_reversal_permutation(n, &factors); + let fwd_stages = precompute_stages(omega, n, &factors); + let inv_stages = precompute_stages(omega_inv, n, &factors); + Self { + n, + omega, + n_inv, + digit_rev, + fwd_stages, + inv_stages, + } + } + + /// Forward DFT: `Y[k] = Σ_{j=0}^{n-1} x[j] · ω^{jk}`. + /// + /// # Panics + /// If `input.len() != n`. + pub fn forward(&self, input: &[F]) -> Vec { + assert_eq!(input.len(), self.n); + let mut ws = FftWorkspace::new(self.n); + ws.execute(input, &self.fwd_stages, &self.digit_rev) + .to_vec() + } + + /// Inverse DFT: `x[j] = (1/n) · Σ_{k=0}^{n-1} Y[k] · ω^{-jk}`. + /// + /// # Panics + /// If `input.len() != n`. + pub fn inverse(&self, input: &[F]) -> Vec { + assert_eq!(input.len(), self.n); + let mut ws: FftWorkspace = FftWorkspace::new(self.n); + let mut result = ws + .execute(input, &self.inv_stages, &self.digit_rev) + .to_vec(); + for v in &mut result { + *v *= self.n_inv; + } + result + } + + /// Evaluate a polynomial at the shifted coset + /// `{shift · ω^i | i = 0, …, n−1}`. + /// + /// Reduces to a plain forward DFT on twisted coefficients via + /// + /// `P(shift · ω^i) = Σ_j (c_j · shift^j) · ω^{ij}`, + /// + /// so we pre-twist `c_j ← c_j · shift^j` into `buf_b` + /// (zero-padding any unused tail) and forward-FFT from there. + /// + /// # Panics + /// If `coeffs.len() > n`. + pub fn coset_forward(&self, coeffs: &[F], shift: F) -> Vec { + assert!(coeffs.len() <= self.n); + let mut ws: FftWorkspace = FftWorkspace::new(self.n); + let buf = &mut ws.buf_b[..self.n]; + let mut tw = F::one(); + for (i, &c) in coeffs.iter().enumerate() { + buf[i] = c * tw; + tw *= shift; + } + for v in &mut buf[coeffs.len()..] { + *v = F::zero(); + } + ws.execute_from_b(&self.fwd_stages, &self.digit_rev) + .to_vec() + } + + /// Reed-Solomon-extend `evals` from the base subgroup + /// `K = {ω_K^i}` (with `ω_K = ω_n^{blowup}`, `k = self.n`) to the + /// `blowup − 1` non-trivial cosets of `K` inside the larger + /// size-`(k · blowup)` subgroup. + /// + /// One inverse FFT recovers the polynomial through `evals`, then + /// `blowup − 1` coset forward FFTs (shifts `ω_n^j` for + /// `j = 1, …, blowup − 1`) evaluate it on each extension coset. + /// Returns `k · (blowup − 1)` values, coset-major; the original + /// evaluations on `K` are not re-emitted. All transforms share a + /// single workspace. + /// + /// # Panics + /// If `evals.len() != n`. + pub fn rs_extend_batch(&self, evals: &[F], omega_n: F, blowup: usize) -> Vec { + let k = self.n; + assert_eq!(evals.len(), k); + + let mut ws: FftWorkspace = FftWorkspace::new(self.n); + + let mut coeffs = ws + .execute(evals, &self.inv_stages, &self.digit_rev) + .to_vec(); + for v in &mut coeffs { + *v *= self.n_inv; + } + + let mut extension = Vec::with_capacity(k * (blowup - 1)); + for j in 1..blowup { + let shift = field_pow(omega_n, j as u64); + let buf = &mut ws.buf_b[..k]; + let mut tw = F::one(); + for (i, &c) in coeffs.iter().enumerate() { + buf[i] = c * tw; + tw *= shift; + } + let result = ws.execute_from_b(&self.fwd_stages, &self.digit_rev); + extension.extend_from_slice(result); + } + extension + } +} + +/// Primitive `n`-th root of unity in `F`, derived from +/// [`SmoothFftField::SMOOTH_OMEGA`] as +/// `omega_n = SMOOTH_OMEGA ^ (SMOOTH_SUBGROUP_ORDER / n)`. Requires +/// `n | SMOOTH_SUBGROUP_ORDER`. +/// +/// # Panics +/// If `n` does not divide [`SmoothFftField::SMOOTH_SUBGROUP_ORDER`], or +/// if `SMOOTH_OMEGA` is not in canonical form. +pub fn primitive_nth_root(n: usize) -> F { + assert!(n > 0, "n must be positive"); + assert_eq!( + F::SMOOTH_SUBGROUP_ORDER % n, + 0, + "n={n} must divide SMOOTH_SUBGROUP_ORDER={}", + F::SMOOTH_SUBGROUP_ORDER + ); + // Checked construction so a literal `≥ p` panics rather than + // being silently reduced. + let omega = F::from_canonical_u128_checked(F::SMOOTH_OMEGA) + .expect("SMOOTH_OMEGA must be < p (canonical form)"); + field_pow(omega, (F::SMOOTH_SUBGROUP_ORDER / n) as u64) +} + +/// Find a primitive `n`-th root of unity in `F` by scanning small +/// bases. +/// +/// Verifies primitivity against every distinct prime factor of `n`, so +/// it remains correct when a base lands in a strict subgroup (e.g. +/// `g = 2` is a quadratic residue modulo `Prime128OffsetA7F7`, so +/// `2^{(p−1)/n}` has order `n/2`). +/// +/// Used by per-prime tests as a drift guard on +/// [`SmoothFftField::SMOOTH_OMEGA`]; production code should call +/// [`primitive_nth_root`] instead. +/// +/// # Panics +/// If `n` does not divide `p − 1`, or if no base in `{2, 3, …, 47}` +/// yields a primitive `n`-th root. +#[cfg(test)] +#[expect( + clippy::panic, + reason = "test-only primitive-root scanner fails loudly" +)] +pub(crate) fn find_primitive_nth_root( + p_minus_1: u128, + n: usize, +) -> F { + assert_eq!( + p_minus_1 % (n as u128), + 0, + "n={n} must divide p-1={p_minus_1}" + ); + let exp = p_minus_1 / (n as u128); + let prime_factors = distinct_prime_factors(n); + + for &g in &[2u64, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47] { + let candidate = field_pow_u128(F::from_u64(g), exp); + if !is_primitive_nth_root(candidate, n, &prime_factors) { + continue; + } + return candidate; + } + panic!("no primitive {n}-th root of unity found in scanned bases"); +} + +/// Distinct prime factors of `n`, sorted ascending. +fn distinct_prime_factors(n: usize) -> Vec { + let mut factors = factorize(n); + factors.sort_unstable(); + factors.dedup(); + factors +} + +/// Test whether `omega` has exact multiplicative order `n`, given a slice +/// containing every distinct prime factor of `n`. +fn is_primitive_nth_root(omega: F, n: usize, distinct_factors: &[usize]) -> bool { + if field_pow(omega, n as u64) != F::one() { + return false; + } + distinct_factors + .iter() + .all(|&q| field_pow(omega, (n / q) as u64) != F::one()) +} + +/// Debug-only check that `omega` is a primitive `n`-th root of unity. +fn debug_assert_primitive_nth_root(omega: F, n: usize) { + if !cfg!(debug_assertions) { + return; + } + let factors = distinct_prime_factors(n); + assert!( + is_primitive_nth_root(omega, n, &factors), + "omega is not a primitive {n}-th root of unity (n's prime factors: {factors:?})" + ); +} + +/// Free-function wrapper around [`SmoothDomain::rs_extend_batch`]. +pub fn rs_extend_fft( + evals: &[F], + domain_k: &SmoothDomain, + omega_n: F, + blowup: usize, +) -> Vec { + domain_k.rs_extend_batch(evals, omega_n, blowup) +} + +#[cfg(test)] +mod test_support { + //! Prime-agnostic helpers shared by per-prime FFT parity tests. + //! + //! The two protocol primes (`Prime128Offset2355`, `Prime128OffsetA7F7`) + //! have different smooth-subgroup factorizations, but the parity + //! properties under test (FFT vs naive DFT, forward/inverse roundtrip, + //! RS-extend consistency) are identical. Factor them out here so the + //! per-prime modules carry only the size lattice that actually differs. + //! + //! All omegas come from [`super::primitive_nth_root`] (i.e. the + //! field's `SmoothFftField::SMOOTH_OMEGA`); the scanner + //! [`super::find_primitive_nth_root`] is only re-invoked by the + //! `smooth_omega_matches_search` per-prime tests as a drift guard + //! on the hardcoded literal. + use super::*; + use crate::FromPrimitiveInt; + use std::fmt::Debug; + use std::ops::{AddAssign, MulAssign}; + + pub(super) use super::find_primitive_nth_root; + + /// O(n^2) naive DFT, used as oracle for the iterative FFT under test. + fn naive_dft(input: &[F], omega: F) -> Vec { + let n = input.len(); + let mut out = vec![F::zero(); n]; + for (k, ok) in out.iter_mut().enumerate() { + for (j, &xj) in input.iter().enumerate() { + *ok += xj * field_pow(omega, (j * k) as u64); + } + } + out + } + + /// For each `n` in `sizes` that divides `F::SMOOTH_SUBGROUP_ORDER`, + /// assert the iterative FFT matches the naive DFT on a deterministic + /// input vector. Sizes that do not divide are silently skipped so + /// per-prime modules can share a single union of "interesting" sizes. + pub(super) fn assert_fft_matches_naive_dft(sizes: &[usize]) + where + F: SmoothFftField + FromPrimitiveInt + Invertible + Debug, + { + for &n in sizes { + if F::SMOOTH_SUBGROUP_ORDER % n != 0 { + continue; + } + let omega = primitive_nth_root::(n); + let input: Vec = (0..n).map(|i| F::from_u64((i + 1) as u64)).collect(); + let expected = naive_dft(&input, omega); + + let factors = factorize(n); + let digit_rev = digit_reversal_permutation(n, &factors); + let stages = precompute_stages(omega, n, &factors); + let mut ws: FftWorkspace = FftWorkspace::new(n); + let got = ws.execute(&input, &stages, &digit_rev).to_vec(); + assert_eq!(got, expected, "FFT mismatch for n={n}"); + } + } + + /// `forward(inverse(x)) == x` over a smooth domain of order `n`. + pub(super) fn assert_forward_inverse_roundtrip(n: usize) + where + F: SmoothFftField + FromPrimitiveInt + Invertible + Debug, + { + let omega = primitive_nth_root::(n); + let domain = SmoothDomain::new(omega, n); + let input: Vec = (0..n).map(|i| F::from_u64(i as u64 + 1)).collect(); + let transformed = domain.forward(&input); + let recovered = domain.inverse(&transformed); + assert_eq!(input, recovered); + } + + /// `rs_extend_fft` matches direct evaluation of the interpolating + /// polynomial on each of the `blowup - 1` extension cosets. + pub(super) fn assert_rs_extend_consistency(k: usize, blowup: usize) + where + F: SmoothFftField + FromPrimitiveInt + Invertible + Debug + AddAssign + MulAssign, + { + let n = k * blowup; + let omega_n = primitive_nth_root::(n); + let omega_k = field_pow(omega_n, blowup as u64); + let domain_k = SmoothDomain::new(omega_k, k); + + let evals: Vec = (0..k).map(|i| F::from_u64((i * 7 + 3) as u64)).collect(); + let coeffs = domain_k.inverse(&evals); + let extension = rs_extend_fft(&evals, &domain_k, omega_n, blowup); + assert_eq!(extension.len(), k * (blowup - 1)); + + for j in 1..blowup { + for i in 0..k { + let point = field_pow(omega_n, j as u64) * field_pow(omega_k, i as u64); + let mut expected = F::zero(); + let mut x_pow = F::one(); + for &c in &coeffs { + expected += c * x_pow; + x_pow *= point; + } + assert_eq!( + extension[(j - 1) * k + i], + expected, + "mismatch at coset {j}, position {i}" + ); + } + } + } +} + +#[cfg(test)] +mod prime_2355_tests { + //! `Prime128Offset2355` (`p = 2^128 - 2355`) has smooth multiplicative + //! subgroup of order `14_700 = 2^2 * 3 * 5^2 * 7^2`, drawing sizes from + //! the `{2, 3, 5, 7}` lattice. + use super::test_support::*; + use super::*; + use crate::Prime128Offset2355; + use crate::{CanonicalField, PseudoMersenneField}; + + type F = Prime128Offset2355; + + /// Drift guard: re-derive the primitive `SMOOTH_SUBGROUP_ORDER`-th + /// root of unity from a base scan and assert it equals the literal + /// declared in [`crate::prime::fp128`]. Also validates the + /// trait's structural invariant `SMOOTH_SUBGROUP_ORDER | (p − 1)`. + #[test] + fn smooth_omega_matches_search() { + let p_minus_1 = u128::MAX - F::MODULUS_OFFSET; + assert_eq!( + p_minus_1 % (F::SMOOTH_SUBGROUP_ORDER as u128), + 0, + "SMOOTH_SUBGROUP_ORDER must divide p − 1", + ); + let derived = find_primitive_nth_root::(p_minus_1, F::SMOOTH_SUBGROUP_ORDER); + let declared = + F::from_canonical_u128_checked(F::SMOOTH_OMEGA).expect("SMOOTH_OMEGA must be < p"); + assert_eq!( + derived, declared, + "SMOOTH_OMEGA literal has drifted from the scanner's primitive root" + ); + } + + #[test] + fn primitive_nth_root_has_correct_order_for_every_divisor() { + // Every `n | SMOOTH_SUBGROUP_ORDER` should yield a primitive + // n-th root via the trait derivation. + for &n in &[ + 2, 3, 4, 5, 6, 7, 10, 12, 14, 15, 20, 21, 25, 28, 30, 35, 42, 49, 50, 60, 70, 75, 84, + 98, 100, 105, 140, 147, 150, 175, 196, 210, 245, 294, 300, 350, 420, 490, 525, 588, + 700, 735, 980, 1050, 1225, 1470, 2100, 2450, 2940, 3675, 4900, 7350, 14700, + ] { + if F::SMOOTH_SUBGROUP_ORDER % n != 0 { + continue; + } + let omega = primitive_nth_root::(n); + let factors = distinct_prime_factors(n); + assert!( + is_primitive_nth_root(omega, n, &factors), + "primitive_nth_root failed primitivity check for n={n}" + ); + } + } + + #[test] + fn small_fft_matches_naive_dft() { + assert_fft_matches_naive_dft::(&[ + 2, 3, 4, 5, 6, 7, 10, 12, 14, 15, 20, 21, 25, 28, 42, 49, 50, + ]); + } + + #[test] + fn forward_inverse_roundtrip_300() { + assert_forward_inverse_roundtrip::(300); + } + + #[test] + fn forward_inverse_roundtrip_1470() { + assert_forward_inverse_roundtrip::(1470); + } + + #[test] + fn rs_extend_consistency() { + // k = 300 = 2^2 * 3 * 5^2, blowup = 7, so n = 2_100 | 14_700. + assert_rs_extend_consistency::(300, 7); + } +} + +#[cfg(test)] +mod prime_a7f7_tests { + //! `Prime128OffsetA7F7` (`p = 2^128 - 2^32 + 22537`) has smooth + //! multiplicative subgroup of order `2^3 * 3^7 = 17_496`, with a pure + //! radix-3 substructure of order `3^7 = 2_187`. Sizes are drawn from + //! the `{2, 3}` lattice instead of `{2, 3, 5, 7}`. + use super::test_support::*; + use super::*; + use crate::Prime128OffsetA7F7; + use crate::{CanonicalField, PseudoMersenneField}; + + type F = Prime128OffsetA7F7; + + /// Cross-implementation check: the radix-3 GPU NTT in + /// `gpu_bench/primeB_roots.hpp` bakes + /// `OMEGA_2187 = 2^((p_B − 1)/2187)` into a separate constant table. + /// The two implementations independently choose their generators (the + /// GPU uses `g = 2`, the Rust scanner uses the smallest base whose + /// `g^((p−1)/n)` reaches full order `n`), so the constants are not + /// expected to be *equal*; they are expected to be *primitive + /// 2187-th roots of unity in the same field*. We verify the GPU's + /// limb table is a valid primitive 2187-th root under the Rust + /// `Fp128` implementation, which is the meaningful invariant for + /// cross-impl correctness. + #[test] + fn gpu_omega_2187_is_primitive_in_rust_field() { + // Limbs from `gpu_bench/primeB_roots.hpp` OMEGA_2187, packed + // little-endian into a u128. + const GPU_OMEGA_2187: u128 = 0x44E6_6EEC_31E7_36A6_A030_9253_219B_CCCD; + let omega = F::from_canonical_u128_checked(GPU_OMEGA_2187) + .expect("GPU OMEGA_2187 must lie in [0, p_B)"); + let factors = distinct_prime_factors(2187); + assert!( + is_primitive_nth_root(omega, 2187, &factors), + "gpu_bench OMEGA_2187 is not a primitive 2187-th root under Rust Fp128", + ); + + // The GPU also bakes OMEGA_3 = OMEGA_2187^729 (a primitive cube + // root). Cross-check that limb table too. + const GPU_OMEGA_3: u128 = 0x66F1_B0EE_0E4A_40F7_0F69_0C7F_0F66_39DD; + let omega3 = + F::from_canonical_u128_checked(GPU_OMEGA_3).expect("GPU OMEGA_3 must lie in [0, p_B)"); + assert_eq!(field_pow(omega, 729), omega3, "OMEGA_3 != OMEGA_2187^729"); + assert!( + is_primitive_nth_root(omega3, 3, &distinct_prime_factors(3)), + "gpu_bench OMEGA_3 is not a primitive cube root", + ); + } + + /// Drift guard: see `prime_2355_tests::smooth_omega_matches_search`. + #[test] + fn smooth_omega_matches_search() { + let p_minus_1 = u128::MAX - F::MODULUS_OFFSET; + assert_eq!( + p_minus_1 % (F::SMOOTH_SUBGROUP_ORDER as u128), + 0, + "SMOOTH_SUBGROUP_ORDER must divide p − 1", + ); + let derived = find_primitive_nth_root::(p_minus_1, F::SMOOTH_SUBGROUP_ORDER); + let declared = + F::from_canonical_u128_checked(F::SMOOTH_OMEGA).expect("SMOOTH_OMEGA must be < p"); + assert_eq!( + derived, declared, + "SMOOTH_OMEGA literal has drifted from the scanner's primitive root" + ); + } + + #[test] + fn primitive_nth_root_has_correct_order_for_every_divisor() { + for &n in &[ + 2, 3, 6, 8, 9, 18, 24, 27, 54, 81, 162, 243, 486, 729, 1458, 2187, 4374, 8748, 17496, + ] { + if F::SMOOTH_SUBGROUP_ORDER % n != 0 { + continue; + } + let omega = primitive_nth_root::(n); + let factors = distinct_prime_factors(n); + assert!( + is_primitive_nth_root(omega, n, &factors), + "primitive_nth_root failed primitivity check for n={n}" + ); + } + } + + #[test] + fn small_fft_matches_naive_dft() { + assert_fft_matches_naive_dft::(&[2, 3, 6, 8, 9, 18, 24, 27, 54, 81, 162, 243, 486, 729]); + } + + #[test] + fn forward_inverse_roundtrip_243() { + assert_forward_inverse_roundtrip::(243); + } + + #[test] + fn forward_inverse_roundtrip_1458() { + assert_forward_inverse_roundtrip::(1458); + } + + #[test] + fn forward_inverse_roundtrip_2187() { + assert_forward_inverse_roundtrip::(2187); + } + + #[test] + fn rs_extend_consistency() { + // k = 243 (= 3^5), blowup = 9 (= 3^2), n = 3^7 = 2_187 | 17_496. + assert_rs_extend_consistency::(243, 9); + } +} diff --git a/crates/jolt-field/src/field_error.rs b/crates/jolt-field/src/field_error.rs new file mode 100644 index 0000000000..39e1b0243a --- /dev/null +++ b/crates/jolt-field/src/field_error.rs @@ -0,0 +1,16 @@ +/// Errors produced by backend-independent field helper algorithms. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum FieldError { + /// A caller supplied values with an invalid shape. + #[error("invalid field input: {0}")] + InvalidInput(String), + + /// A caller supplied a slice with an unexpected length. + #[error("invalid field input size: expected {expected}, got {actual}")] + InvalidSize { + /// Required number of elements. + expected: usize, + /// Supplied number of elements. + actual: usize, + }, +} diff --git a/crates/jolt-field/src/lib.rs b/crates/jolt-field/src/lib.rs index 5949f6f295..d2fb0d1985 100644 --- a/crates/jolt-field/src/lib.rs +++ b/crates/jolt-field/src/lib.rs @@ -25,6 +25,16 @@ //! - [`Fq`] — BN254 base field element //! - [`WideAccumulator`] — 9-limb deferred Montgomery reduction //! +//! # Solinas types (feature `solinas`) +//! +//! The Solinas backend provides optimized 32-, 64-, and 128-bit prime fields, +//! extension fields, packed NEON/AVX2/AVX-512 implementations, unreduced +//! accumulators, and smooth-domain FFT helpers. Akita adopts these types +//! directly in its cutover to `jolt-field`. Until that cutover lands, the +//! temporary `akita` feature retains the legacy adapter for the pre-cutover +//! `akita-field` types; it is a bootstrap edge, not the target architecture, +//! and is removed in the final migration PR. +//! //! # Multi-precision arithmetic //! //! - [`Limbs`] — fixed-width limb array for unreduced arithmetic @@ -39,6 +49,7 @@ mod canonical_bytes; mod canonical_u64; mod field; mod field_core; +mod field_error; mod fixed_byte_size; mod fixed_bytes; mod from_primitive_int; @@ -51,6 +62,8 @@ mod reducing_bytes; mod ring_core; mod signed_product_accumulator; mod small_scalar_accumulator; +#[cfg(feature = "solinas")] +mod solinas_traits; mod transcript_challenge; mod with_accumulator; @@ -61,6 +74,7 @@ pub use canonical_bytes::CanonicalBytes; pub use canonical_u64::CanonicalU64; pub use field::{Field, OptimizedMul}; pub use field_core::FieldCore; +pub use field_error::FieldError; pub use fixed_byte_size::FixedByteSize; pub use fixed_bytes::FixedBytes; pub use from_primitive_int::FromPrimitiveInt; @@ -68,6 +82,7 @@ pub use invertible::Invertible; pub use montgomery_constants::MontgomeryConstants; pub use mul_pow_2::MulPow2; pub use mul_primitive_int::MulPrimitiveInt; +pub use num_traits::{One, Zero}; pub use random_sampling::RandomSampling; pub use reducing_bytes::ReducingBytes; pub use ring_core::RingCore; @@ -77,6 +92,10 @@ pub use signed_product_accumulator::{ pub use small_scalar_accumulator::{ NaiveSignedScalarAccumulator, SignedScalarAccumulator, WithSmallScalarAccumulator, }; +#[cfg(feature = "solinas")] +pub use solinas_traits::{ + BalancedDigitLookup, CanonicalField, HalvingField, PseudoMersenneField, SmoothFftField, +}; pub use transcript_challenge::TranscriptChallenge; pub use with_accumulator::WithAccumulator; @@ -85,6 +104,37 @@ pub use limbs::Limbs; pub mod signed; +#[cfg(feature = "solinas")] +mod ext; +#[cfg(feature = "solinas")] +pub mod fft; +#[cfg(feature = "solinas")] +pub mod packed; +#[cfg(feature = "solinas")] +pub mod parallel; +#[cfg(feature = "solinas")] +mod prime; +#[cfg(feature = "solinas")] +pub mod unreduced; + +#[cfg(feature = "solinas")] +pub use ext::lift::{ + canonical_frobenius_thetas, solve_frobenius_moore, validate_canonical_frobenius_thetas, + ExtField, FrobeniusExtField, LiftBase, MulBase, MulBaseUnreduced, +}; +#[cfg(feature = "solinas")] +pub use ext::{ + Ext2, FpExt2, FpExt2Config, FpExt4, FpExt4MulBackend, FpExt8, FpExt8MulBackend, NegOneNr, TwoNr, +}; +#[cfg(feature = "solinas")] +pub use prime::{ + is_registered_prime_offset, pseudo_mersenne_modulus, registered_prime_offset_spec, Fp128, Fp32, + Fp64, Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, + Prime24Offset3, Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, + Prime48Offset59, Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, + PRIME_OFFSET_IMPLEMENTED_MAX_BITS, PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, +}; + #[cfg(feature = "bn254")] pub mod arkworks; #[cfg(feature = "bn254")] diff --git a/crates/jolt-field/src/packed/avx2/fp128.rs b/crates/jolt-field/src/packed/avx2/fp128.rs new file mode 100644 index 0000000000..df1c3e06bd --- /dev/null +++ b/crates/jolt-field/src/packed/avx2/fp128.rs @@ -0,0 +1,231 @@ +use super::*; + +/// Number of `Fp128` lanes in an AVX2 packed vector. +pub(crate) const FP128_WIDTH: usize = 4; + +/// AVX2 packed arithmetic for `Fp128

`, 4 lanes in SoA layout. +/// +/// Stores 4 elements as separate `lo` and `hi` `u64` arrays, enabling +/// vectorized add/sub via `__m256i`. Mul remains scalar per-lane. +#[derive(Clone, Copy)] +pub struct PackedFp128Avx2 { + lo: [u64; FP128_WIDTH], + hi: [u64; FP128_WIDTH], +} + +impl PackedFp128Avx2

{ + const P_LO: u64 = P as u64; + const P_HI: u64 = (P >> 64) as u64; +} + +impl Default for PackedFp128Avx2

{ + #[inline] + fn default() -> Self { + Self { + lo: [0; FP128_WIDTH], + hi: [0; FP128_WIDTH], + } + } +} + +impl fmt::Debug for PackedFp128Avx2

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let elems: Vec<_> = (0..FP128_WIDTH).map(|i| self.extract(i)).collect(); + f.debug_tuple("PackedFp128Avx2").field(&elems).finish() + } +} + +impl PartialEq for PackedFp128Avx2

{ + #[inline] + fn eq(&self, other: &Self) -> bool { + self.lo == other.lo && self.hi == other.hi + } +} + +impl Eq for PackedFp128Avx2

{} + +impl Add for PackedFp128Avx2

{ + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + unsafe { + let a_lo = _mm256_loadu_si256(self.lo.as_ptr().cast()); + let a_hi = _mm256_loadu_si256(self.hi.as_ptr().cast()); + let b_lo = _mm256_loadu_si256(rhs.lo.as_ptr().cast()); + let b_hi = _mm256_loadu_si256(rhs.hi.as_ptr().cast()); + let p_lo = _mm256_set1_epi64x(Self::P_LO as i64); + let p_hi = _mm256_set1_epi64x(Self::P_HI as i64); + let sign = _mm256_set1_epi64x(i64::MIN); + let one = _mm256_set1_epi64x(1); + + // 128-bit add with unsigned compare emulation (XOR sign bit) + let sum_lo = _mm256_add_epi64(a_lo, b_lo); + let carry_lo = + _mm256_cmpgt_epi64(_mm256_xor_si256(a_lo, sign), _mm256_xor_si256(sum_lo, sign)); + let carry_lo_bit = _mm256_and_si256(carry_lo, one); + + let hi_tmp = _mm256_add_epi64(a_hi, b_hi); + let ov1 = + _mm256_cmpgt_epi64(_mm256_xor_si256(a_hi, sign), _mm256_xor_si256(hi_tmp, sign)); + let sum_hi = _mm256_add_epi64(hi_tmp, carry_lo_bit); + let ov2 = _mm256_cmpgt_epi64( + _mm256_xor_si256(hi_tmp, sign), + _mm256_xor_si256(sum_hi, sign), + ); + let carry_128 = _mm256_or_si256(ov1, ov2); + + // 128-bit subtract P + let red_lo = _mm256_sub_epi64(sum_lo, p_lo); + let borrow_lo = + _mm256_cmpgt_epi64(_mm256_xor_si256(p_lo, sign), _mm256_xor_si256(sum_lo, sign)); + let borrow_lo_bit = _mm256_and_si256(borrow_lo, one); + + let red_hi_tmp = _mm256_sub_epi64(sum_hi, p_hi); + let bw1 = + _mm256_cmpgt_epi64(_mm256_xor_si256(p_hi, sign), _mm256_xor_si256(sum_hi, sign)); + let red_hi = _mm256_sub_epi64(red_hi_tmp, borrow_lo_bit); + let bw2 = _mm256_cmpgt_epi64( + _mm256_xor_si256(borrow_lo_bit, sign), + _mm256_xor_si256(red_hi_tmp, sign), + ); + let borrow = _mm256_or_si256(bw1, bw2); + + // use_reduced = carry_128 | !borrow + let not_borrow = _mm256_xor_si256(borrow, _mm256_set1_epi64x(-1)); + let use_reduced = _mm256_or_si256(carry_128, not_borrow); + let out_lo = _mm256_blendv_epi8(sum_lo, red_lo, use_reduced); + let out_hi = _mm256_blendv_epi8(sum_hi, red_hi, use_reduced); + + let mut result = Self::default(); + _mm256_storeu_si256(result.lo.as_mut_ptr().cast(), out_lo); + _mm256_storeu_si256(result.hi.as_mut_ptr().cast(), out_hi); + result + } + } +} + +impl Sub for PackedFp128Avx2

{ + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + unsafe { + let a_lo = _mm256_loadu_si256(self.lo.as_ptr().cast()); + let a_hi = _mm256_loadu_si256(self.hi.as_ptr().cast()); + let b_lo = _mm256_loadu_si256(rhs.lo.as_ptr().cast()); + let b_hi = _mm256_loadu_si256(rhs.hi.as_ptr().cast()); + let p_lo = _mm256_set1_epi64x(Self::P_LO as i64); + let p_hi = _mm256_set1_epi64x(Self::P_HI as i64); + let sign = _mm256_set1_epi64x(i64::MIN); + let one = _mm256_set1_epi64x(1); + + // 128-bit sub + let diff_lo = _mm256_sub_epi64(a_lo, b_lo); + let borrow_lo = + _mm256_cmpgt_epi64(_mm256_xor_si256(b_lo, sign), _mm256_xor_si256(a_lo, sign)); + let borrow_lo_bit = _mm256_and_si256(borrow_lo, one); + + let hi_tmp = _mm256_sub_epi64(a_hi, b_hi); + let bw1 = + _mm256_cmpgt_epi64(_mm256_xor_si256(b_hi, sign), _mm256_xor_si256(a_hi, sign)); + let diff_hi = _mm256_sub_epi64(hi_tmp, borrow_lo_bit); + let bw2 = _mm256_cmpgt_epi64( + _mm256_xor_si256(borrow_lo_bit, sign), + _mm256_xor_si256(hi_tmp, sign), + ); + let borrow_128 = _mm256_or_si256(bw1, bw2); + + // Correction: add P back where underflow occurred + let corr_lo = _mm256_add_epi64(diff_lo, p_lo); + let carry_lo = _mm256_cmpgt_epi64( + _mm256_xor_si256(diff_lo, sign), + _mm256_xor_si256(corr_lo, sign), + ); + let carry_lo_bit = _mm256_and_si256(carry_lo, one); + let corr_hi = _mm256_add_epi64(diff_hi, p_hi); + let corr_hi = _mm256_add_epi64(corr_hi, carry_lo_bit); + + let out_lo = _mm256_blendv_epi8(diff_lo, corr_lo, borrow_128); + let out_hi = _mm256_blendv_epi8(diff_hi, corr_hi, borrow_128); + + let mut result = Self::default(); + _mm256_storeu_si256(result.lo.as_mut_ptr().cast(), out_lo); + _mm256_storeu_si256(result.hi.as_mut_ptr().cast(), out_hi); + result + } + } +} + +impl Mul for PackedFp128Avx2

{ + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self { + let mut out = Self::default(); + for i in 0..FP128_WIDTH { + let a = Fp128::

([self.lo[i], self.hi[i]]); + let b = Fp128::

([rhs.lo[i], rhs.hi[i]]); + let r = a * b; + out.lo[i] = r.0[0]; + out.hi[i] = r.0[1]; + } + out + } +} + +impl PackedValue for PackedFp128Avx2

{ + type Value = Fp128

; + const WIDTH: usize = FP128_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Value, + { + let mut lo = [0u64; FP128_WIDTH]; + let mut hi = [0u64; FP128_WIDTH]; + for i in 0..FP128_WIDTH { + let v = f(i); + lo[i] = v.0[0]; + hi[i] = v.0[1]; + } + Self { lo, hi } + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Value { + debug_assert!(lane < FP128_WIDTH); + Fp128([self.lo[lane], self.hi[lane]]) + } +} + +impl AddAssign for PackedFp128Avx2

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for PackedFp128Avx2

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for PackedFp128Avx2

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl PackedField for PackedFp128Avx2

{ + type Scalar = Fp128

; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self { + lo: [value.0[0]; FP128_WIDTH], + hi: [value.0[1]; FP128_WIDTH], + } + } +} diff --git a/crates/jolt-field/src/packed/avx2/fp32.rs b/crates/jolt-field/src/packed/avx2/fp32.rs new file mode 100644 index 0000000000..2741f454e7 --- /dev/null +++ b/crates/jolt-field/src/packed/avx2/fp32.rs @@ -0,0 +1,689 @@ +use super::*; + +/// Number of `Fp32` lanes in an AVX2 packed vector. +pub(crate) const FP32_WIDTH: usize = 8; + +/// AVX2 packed arithmetic for `Fp32

`, processing 8 lanes. +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct PackedFp32Avx2(pub [Fp32

; FP32_WIDTH]); + +impl PackedFp32Avx2

{ + const BITS: u32 = 32 - P.leading_zeros(); + + const C: u32 = { + let c = if Self::BITS == 32 { + 0u32.wrapping_sub(P) + } else { + (1u32 << Self::BITS) - P + }; + assert!(P != 0, "modulus must be nonzero"); + assert!(P & 1 == 1, "modulus must be odd"); + assert!( + (c as u64) * (c as u64 + 1) < P as u64, + "C(C+1) < P required for fused canonicalize" + ); + c + }; + + const MASK_U64: u64 = if Self::BITS == 32 { + u32::MAX as u64 + } else { + (1u64 << Self::BITS) - 1 + }; + + /// Whether two Solinas folds suffice to bring the sum of four + /// `(P-1)^2` products into `[0, 2*P)` for the final canonicalize step. + /// Mirrors `PackedFp32Neon::TWO_FOLD_FOUR_PRODUCT_OK`. When `false`, + /// `solinas_reduce` must do a third fold before handing off to + /// `pack_and_canonicalize`. + const TWO_FOLD_FOUR_PRODUCT_OK: bool = { + let c = Self::C as u64; + 4 * c * c + 3 * c <= (1u64 << Self::BITS) + }; + + #[inline(always)] + fn to_vec(self) -> __m256i { + unsafe { transmute(self) } + } + + #[inline(always)] + unsafe fn from_vec(v: __m256i) -> Self { + unsafe { transmute(v) } + } + + /// Multiply each `u64` lane by `C`. Building block of Solinas reduction; + /// the `C == 1` fast path skips the multiply entirely for Mersenne-like + /// primes. Mirrors `PackedFp32Neon::mul_c_u64`. + /// + /// AVX2 has no native 64×64-bit multiply, so we split `x` into two 32-bit + /// halves, multiply each by `C` with `_mm256_mul_epu32` (32×32→64), then + /// recombine: `x*C = x_lo*C + ((x_hi*C) << 32)` (mod 2^64). The previous + /// implementation used a single `_mm256_mul_epu32(x, c_vec)` which only + /// reads the *low 32 bits* of `x` and silently dropped bit 32+ — fine for + /// `BITS == 32` (where the caller's `prod >> 32` always fits in 32 bits) + /// but wrong for `BITS == 31` and `C != 1` where `prod >> 31` can occupy + /// 33 bits. + #[inline(always)] + unsafe fn mul_c_u64(x: __m256i) -> __m256i { + if Self::C == 1 { + return x; + } + let c_vec = _mm256_set1_epi64x(Self::C as i64); + let lo_part = _mm256_mul_epu32(x, c_vec); + let hi_part = _mm256_mul_epu32(_mm256_srli_epi64::<32>(x), c_vec); + _mm256_add_epi64(lo_part, _mm256_slli_epi64::<32>(hi_part)) + } + + /// One Solinas fold of a single 64-bit product lane (BITS == 32 only): + /// `(x & (2^32-1)) + C*(x >> 32)`. For a single product `x < 2^64` the + /// high word `x >> 32 < 2^32`, so the result is `< 2^40`. Lets the + /// `BITS == 32` dot-product sum up to four folded terms (each `< 2^40`) + /// below `2^42` without `u64` overflow, removing the per-product carry + /// tracking. Mirrors `PackedFp32Avx512::fold_product_once`. + #[inline(always)] + unsafe fn fold_product_once(x: __m256i) -> __m256i { + let lo = _mm256_and_si256(x, _mm256_set1_epi64x(Self::MASK_U64 as i64)); + let hi = _mm256_srli_epi64::<32>(x); + _mm256_add_epi64(lo, Self::mul_c_u64(hi)) + } + + /// Plonky3-style Mersenne31 multiply (P = 2^31 - 1). Specialized fold + /// using `_mm256_srli_epi64::<31>` shifts. Used by the `Mul` impl when + /// `Self::BITS == 31 && Self::C == 1`. + #[inline(always)] + unsafe fn mul_mersenne31_vec(a: __m256i, b: __m256i) -> __m256i { + unsafe { + let lhs_odd_dbl = _mm256_srli_epi64::<31>(a); + let rhs_odd = movehdup_epi32(b); + + let prod_odd_dbl = _mm256_mul_epu32(rhs_odd, lhs_odd_dbl); + let prod_evn = _mm256_mul_epu32(b, a); + + let prod_odd_lo_dirty = _mm256_slli_epi64::<31>(prod_odd_dbl); + let prod_evn_hi = _mm256_srli_epi64::<31>(prod_evn); + + let prod_lo_dirty = _mm256_blend_epi32::<0b1010_1010>(prod_evn, prod_odd_lo_dirty); + let prod_hi = _mm256_blend_epi32::<0b1010_1010>(prod_evn_hi, prod_odd_dbl); + + let p = _mm256_set1_epi32(P as i32); + let prod_lo = _mm256_and_si256(prod_lo_dirty, p); + let folded = _mm256_add_epi32(prod_lo, prod_hi); + _mm256_min_epu32(folded, _mm256_sub_epi32(folded, p)) + } + } + + /// Vector form of field add: 8-lane add + canonicalize to `[0, P)`. + /// Mirrors `PackedFp32Neon::add_vec`. + #[inline(always)] + unsafe fn add_vec(a: __m256i, b: __m256i) -> __m256i { + let p = _mm256_set1_epi32(P as i32); + if Self::BITS <= 31 { + let t = _mm256_add_epi32(a, b); + let u = _mm256_sub_epi32(t, p); + _mm256_min_epu32(t, u) + } else { + // BITS == 32: a + b may overflow u32. Detect via unsigned compare + // (sign-bit-XOR trick), correct by adding C (since 2^32 ≡ C mod P), + // then conditional subtract P. + let c = _mm256_set1_epi32(Self::C as i32); + let t = _mm256_add_epi32(a, b); + let sign32 = _mm256_set1_epi32(i32::MIN); + let overflow = + _mm256_cmpgt_epi32(_mm256_xor_si256(a, sign32), _mm256_xor_si256(t, sign32)); + let t2 = _mm256_add_epi32(t, _mm256_and_si256(overflow, c)); + let r = _mm256_sub_epi32(t2, p); + _mm256_min_epu32(t2, r) + } + } + + /// Vector form of field sub: 8-lane sub + canonicalize to `[0, P)`. + /// Mirrors `PackedFp32Neon::sub_vec`. + #[inline(always)] + unsafe fn sub_vec(a: __m256i, b: __m256i) -> __m256i { + let p = _mm256_set1_epi32(P as i32); + if Self::BITS <= 31 { + let t = _mm256_sub_epi32(a, b); + let u = _mm256_add_epi32(t, p); + _mm256_min_epu32(t, u) + } else { + // BITS == 32: t = a - b may underflow. If a < b, t wraps to + // t + 2^32; we want t + P = t + 2^32 - C, i.e. subtract C. + let t = _mm256_sub_epi32(a, b); + let sign32 = _mm256_set1_epi32(i32::MIN); + let underflow = + _mm256_cmpgt_epi32(_mm256_xor_si256(b, sign32), _mm256_xor_si256(a, sign32)); + let c = _mm256_set1_epi32(Self::C as i32); + _mm256_sub_epi32(t, _mm256_and_si256(underflow, c)) + } + } + + /// Vector form of field mul: 8-lane Solinas multiply + canonicalize. + /// Mirrors `PackedFp32Neon::mul_vec`. + #[inline(always)] + unsafe fn mul_vec(a: __m256i, b: __m256i) -> __m256i { + let prod_evn = _mm256_mul_epu32(a, b); + let a_odd = movehdup_epi32(a); + let b_odd = movehdup_epi32(b); + let prod_odd = _mm256_mul_epu32(a_odd, b_odd); + Self::solinas_reduce(prod_evn, prod_odd) + } + + /// 4-way fused multiply-accumulate with a single end-reduction. + /// Computes `sum_i a[i] * b[i]` lane-wise and canonicalizes. The key + /// fused operation for `FpExt4` and power-basis FpExt4 multiply. + /// Mirrors `PackedFp32Neon::dot_product_4_vec`. For `BITS <= 31`, four + /// `(2^31 - 1)^2` products sum below `2^64`, so the raw products + /// accumulate without overflow. For `BITS == 32`, each product is + /// pre-folded once (`< 2^40`) so four folds sum below `2^42`, again + /// overflow-free. Both branches end in a single carry-free + /// `solinas_reduce`; the `if` is a const condition resolved at compile + /// time. + #[inline(always)] + unsafe fn dot_product_4_vec(a: [__m256i; 4], b: [__m256i; 4]) -> __m256i { + let mut sum_evn = _mm256_mul_epu32(a[0], b[0]); + let mut sum_odd = _mm256_mul_epu32(movehdup_epi32(a[0]), movehdup_epi32(b[0])); + + if Self::BITS <= 31 { + for i in 1..4 { + let prod_evn = _mm256_mul_epu32(a[i], b[i]); + let prod_odd = _mm256_mul_epu32(movehdup_epi32(a[i]), movehdup_epi32(b[i])); + sum_evn = _mm256_add_epi64(sum_evn, prod_evn); + sum_odd = _mm256_add_epi64(sum_odd, prod_odd); + } + return Self::solinas_reduce(sum_evn, sum_odd); + } + + // BITS == 32: four 32-bit products overflow a `u64` sum, so pre-fold each + // product once (`< 2^40`) and accumulate the folds (`< 4*2^40 < 2^42`), + // which is carry-free, then a single carry-free `solinas_reduce`. + let mut sum_evn = Self::fold_product_once(sum_evn); + let mut sum_odd = Self::fold_product_once(sum_odd); + for i in 1..4 { + let prod_evn = Self::fold_product_once(_mm256_mul_epu32(a[i], b[i])); + let prod_odd = Self::fold_product_once(_mm256_mul_epu32( + movehdup_epi32(a[i]), + movehdup_epi32(b[i]), + )); + sum_evn = _mm256_add_epi64(sum_evn, prod_evn); + sum_odd = _mm256_add_epi64(sum_odd, prod_odd); + } + Self::solinas_reduce(sum_evn, sum_odd) + } + + /// 3-way fused multiply-accumulate with a single end-reduction. + #[inline(always)] + unsafe fn dot_product_3_vec(a: [__m256i; 3], b: [__m256i; 3]) -> __m256i { + let mut sum_evn = _mm256_mul_epu32(a[0], b[0]); + let mut sum_odd = _mm256_mul_epu32(movehdup_epi32(a[0]), movehdup_epi32(b[0])); + + if Self::BITS <= 31 { + for i in 1..3 { + let prod_evn = _mm256_mul_epu32(a[i], b[i]); + let prod_odd = _mm256_mul_epu32(movehdup_epi32(a[i]), movehdup_epi32(b[i])); + sum_evn = _mm256_add_epi64(sum_evn, prod_evn); + sum_odd = _mm256_add_epi64(sum_odd, prod_odd); + } + return Self::solinas_reduce(sum_evn, sum_odd); + } + + // BITS == 32: pre-fold (see `dot_product_4_vec`). + let mut sum_evn = Self::fold_product_once(sum_evn); + let mut sum_odd = Self::fold_product_once(sum_odd); + for i in 1..3 { + let prod_evn = Self::fold_product_once(_mm256_mul_epu32(a[i], b[i])); + let prod_odd = Self::fold_product_once(_mm256_mul_epu32( + movehdup_epi32(a[i]), + movehdup_epi32(b[i]), + )); + sum_evn = _mm256_add_epi64(sum_evn, prod_evn); + sum_odd = _mm256_add_epi64(sum_odd, prod_odd); + } + Self::solinas_reduce(sum_evn, sum_odd) + } + + /// Multiply by an `FpExt2` non-residue (used by `fp_ext2_mul`). Recognizes the + /// `nr == -1` and `nr == 2` fast paths to avoid full multiplies. + /// Mirrors `PackedFp32Neon::mul_nr_vec`. + #[inline(always)] + unsafe fn mul_nr_vec(x: __m256i) -> __m256i + where + C: FpExt2Config>, + { + if C::IS_NEG_ONE { + Self::sub_vec(_mm256_setzero_si256(), x) + } else if C::non_residue().0 == 2 { + Self::add_vec(x, x) + } else { + C::mul_non_residue(Self::from_vec(x), Self::broadcast).to_vec() + } + } + + /// Two-or-three-fold Solinas reduction of 4+4 `u64` products → 8 `u32` + /// lanes. Inputs are the even-lane and odd-lane product vectors from + /// `_mm256_mul_epu32`. Mirrors `PackedFp32Neon::solinas_reduce`. + /// + /// The `Self::BITS == 31` branches use immediate-shift + /// `_mm256_srli_epi64::<31>` instead of the generic variable-shift + /// `_mm256_srl_epi64(.., shift)`, mirroring the same specialisation + /// the base-field `Mul` impl uses on Mersenne31, so extension-field + /// operations on Mersenne31 get the same per-shift win. + /// + /// Two folds always suffice when `Self::TWO_FOLD_FOUR_PRODUCT_OK`. When + /// it doesn't (large `C` such that `4*C^2 + 3*C > 2^BITS`), we run a + /// third fold so `pack_and_canonicalize`'s single subtract-and-min step + /// is enough to land in `[0, P)`. + #[inline(always)] + unsafe fn solinas_reduce(prod_evn: __m256i, prod_odd: __m256i) -> __m256i { + let mask = _mm256_set1_epi64x(Self::MASK_U64 as i64); + let shift = _mm_set_epi64x(0, Self::BITS as i64); + + // Fold 1 + let evn_lo = _mm256_and_si256(prod_evn, mask); + let evn_hi = if Self::BITS == 31 { + _mm256_srli_epi64::<31>(prod_evn) + } else { + _mm256_srl_epi64(prod_evn, shift) + }; + let evn_f1 = _mm256_add_epi64(evn_lo, Self::mul_c_u64(evn_hi)); + + let odd_lo = _mm256_and_si256(prod_odd, mask); + let odd_hi = if Self::BITS == 31 { + _mm256_srli_epi64::<31>(prod_odd) + } else { + _mm256_srl_epi64(prod_odd, shift) + }; + let odd_f1 = _mm256_add_epi64(odd_lo, Self::mul_c_u64(odd_hi)); + + // Fold 2 + let evn_f1_lo = _mm256_and_si256(evn_f1, mask); + let evn_f1_hi = if Self::BITS == 31 { + _mm256_srli_epi64::<31>(evn_f1) + } else { + _mm256_srl_epi64(evn_f1, shift) + }; + let evn_f2 = _mm256_add_epi64(evn_f1_lo, Self::mul_c_u64(evn_f1_hi)); + + let odd_f1_lo = _mm256_and_si256(odd_f1, mask); + let odd_f1_hi = if Self::BITS == 31 { + _mm256_srli_epi64::<31>(odd_f1) + } else { + _mm256_srl_epi64(odd_f1, shift) + }; + let odd_f2 = _mm256_add_epi64(odd_f1_lo, Self::mul_c_u64(odd_f1_hi)); + + // Optional third fold for large-C primes (e.g. Generic31Offset32787) + // where two folds leave residue > 2*P. + let (evn_final, odd_final) = if Self::TWO_FOLD_FOUR_PRODUCT_OK { + (evn_f2, odd_f2) + } else { + let evn_f2_lo = _mm256_and_si256(evn_f2, mask); + let evn_f2_hi = if Self::BITS == 31 { + _mm256_srli_epi64::<31>(evn_f2) + } else { + _mm256_srl_epi64(evn_f2, shift) + }; + let odd_f2_lo = _mm256_and_si256(odd_f2, mask); + let odd_f2_hi = if Self::BITS == 31 { + _mm256_srli_epi64::<31>(odd_f2) + } else { + _mm256_srl_epi64(odd_f2, shift) + }; + ( + _mm256_add_epi64(evn_f2_lo, Self::mul_c_u64(evn_f2_hi)), + _mm256_add_epi64(odd_f2_lo, Self::mul_c_u64(odd_f2_hi)), + ) + }; + + Self::pack_and_canonicalize(evn_final, odd_final) + } + + /// Combine 4+4 `u64` lanes (in range `[0, 2P)`) into 8 `u32` lanes + /// canonicalized to `[0, P)`. For `BITS < 32` the values fit in `u32`, + /// so we can pack first and subtract `P` at `u32` width. For `BITS == 32` + /// the worst case can exceed `u32::MAX`, so we conditionally subtract `P` + /// at `u64` width first, then pack. Mirrors the post-fold tail of + /// `PackedFp32Neon::solinas_reduce`. + #[inline(always)] + unsafe fn pack_and_canonicalize(evn_f2: __m256i, odd_f2: __m256i) -> __m256i { + if Self::BITS < 32 { + let odd_shifted = _mm256_slli_epi64::<32>(odd_f2); + let combined = _mm256_blend_epi32::<0b10101010>(evn_f2, odd_shifted); + let p = _mm256_set1_epi32(P as i32); + let reduced = _mm256_sub_epi32(combined, p); + _mm256_min_epu32(combined, reduced) + } else { + let p_u64 = _mm256_set1_epi64x(P as i64); + let sign = _mm256_set1_epi64x(i64::MIN); + let p_s = _mm256_xor_si256(p_u64, sign); + + let red_evn = _mm256_sub_epi64(evn_f2, p_u64); + let evn_s = _mm256_xor_si256(evn_f2, sign); + let keep_evn = _mm256_cmpgt_epi64(p_s, evn_s); + let out_evn = _mm256_blendv_epi8(red_evn, evn_f2, keep_evn); + + let red_odd = _mm256_sub_epi64(odd_f2, p_u64); + let odd_s = _mm256_xor_si256(odd_f2, sign); + let keep_odd = _mm256_cmpgt_epi64(p_s, odd_s); + let out_odd = _mm256_blendv_epi8(red_odd, odd_f2, keep_odd); + + let odd_shifted = _mm256_slli_epi64::<32>(out_odd); + _mm256_blend_epi32::<0b10101010>(out_evn, odd_shifted) + } + } +} + +impl Default for PackedFp32Avx2

{ + #[inline] + fn default() -> Self { + Self([Fp32(0); FP32_WIDTH]) + } +} + +impl fmt::Debug for PackedFp32Avx2

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("PackedFp32Avx2").field(&self.0).finish() + } +} + +impl PartialEq for PackedFp32Avx2

{ + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for PackedFp32Avx2

{} + +impl Add for PackedFp32Avx2

{ + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + unsafe { Self::from_vec(Self::add_vec(self.to_vec(), rhs.to_vec())) } + } +} + +impl Sub for PackedFp32Avx2

{ + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + unsafe { Self::from_vec(Self::sub_vec(self.to_vec(), rhs.to_vec())) } + } +} + +impl Mul for PackedFp32Avx2

{ + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self { + unsafe { + let a = self.to_vec(); + let b = rhs.to_vec(); + + if Self::BITS == 31 && Self::C == 1 { + return Self::from_vec(Self::mul_mersenne31_vec(a, b)); + } + + let prod_evn = _mm256_mul_epu32(a, b); + let a_odd = movehdup_epi32(a); + let b_odd = movehdup_epi32(b); + let prod_odd = _mm256_mul_epu32(a_odd, b_odd); + + let mask = _mm256_set1_epi64x(Self::MASK_U64 as i64); + let shift = _mm_set_epi64x(0, Self::BITS as i64); + + // Fold 1 + let evn_lo = _mm256_and_si256(prod_evn, mask); + let evn_hi = if Self::BITS == 31 { + _mm256_srli_epi64::<31>(prod_evn) + } else { + _mm256_srl_epi64(prod_evn, shift) + }; + let evn_f1 = _mm256_add_epi64(evn_lo, Self::mul_c_u64(evn_hi)); + + let odd_lo = _mm256_and_si256(prod_odd, mask); + let odd_hi = if Self::BITS == 31 { + _mm256_srli_epi64::<31>(prod_odd) + } else { + _mm256_srl_epi64(prod_odd, shift) + }; + let odd_f1 = _mm256_add_epi64(odd_lo, Self::mul_c_u64(odd_hi)); + + // Fold 2 + let evn_f1_lo = _mm256_and_si256(evn_f1, mask); + let evn_f1_hi = if Self::BITS == 31 { + _mm256_srli_epi64::<31>(evn_f1) + } else { + _mm256_srl_epi64(evn_f1, shift) + }; + let evn_f2 = _mm256_add_epi64(evn_f1_lo, Self::mul_c_u64(evn_f1_hi)); + + let odd_f1_lo = _mm256_and_si256(odd_f1, mask); + let odd_f1_hi = if Self::BITS == 31 { + _mm256_srli_epi64::<31>(odd_f1) + } else { + _mm256_srl_epi64(odd_f1, shift) + }; + let odd_f2 = _mm256_add_epi64(odd_f1_lo, Self::mul_c_u64(odd_f1_hi)); + + // Recombine + canonicalize. For `BITS == 32` the two-fold residue + // can land in `[2^32, 2*P)` (up to `2^32 + C^2`), so the subtract + // must happen on the full 64-bit lanes before packing; a 32-bit + // recombine would drop bit 32. `pack_and_canonicalize` does the + // 64-bit subtract for `BITS == 32` and is identical to the inline + // 32-bit recombine for `BITS < 32`. + Self::from_vec(Self::pack_and_canonicalize(evn_f2, odd_f2)) + } + } +} + +impl PackedValue for PackedFp32Avx2

{ + type Value = Fp32

; + const WIDTH: usize = FP32_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Value, + { + Self([f(0), f(1), f(2), f(3), f(4), f(5), f(6), f(7)]) + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Value { + debug_assert!(lane < FP32_WIDTH); + self.0[lane] + } +} + +impl AddAssign for PackedFp32Avx2

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for PackedFp32Avx2

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for PackedFp32Avx2

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl PackedField for PackedFp32Avx2

{ + type Scalar = Fp32

; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self([value; FP32_WIDTH]) + } + + #[inline(always)] + fn fp_ext2_mul(a0: Self, a1: Self, b0: Self, b1: Self) -> (Self, Self) + where + C: FpExt2Config, + { + unsafe { + let a0 = a0.to_vec(); + let a1 = a1.to_vec(); + let b0 = b0.to_vec(); + let b1 = b1.to_vec(); + + let v0 = Self::mul_vec(a0, b0); + let v1 = Self::mul_vec(a1, b1); + let cross = Self::mul_vec(Self::add_vec(a0, a1), Self::add_vec(b0, b1)); + + ( + Self::from_vec(Self::add_vec(v0, Self::mul_nr_vec::(v1))), + Self::from_vec(Self::sub_vec(Self::sub_vec(cross, v0), v1)), + ) + } + } + + #[inline(always)] + fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { + unsafe { + let [a0, a1, a2, a3] = a.map(Self::to_vec); + let [b0, b1, b2, b3] = b.map(Self::to_vec); + let two_b1 = Self::add_vec(b1, b1); + let two_b2 = Self::add_vec(b2, b2); + let two_b3 = Self::add_vec(b3, b3); + let b0_plus_b2 = Self::add_vec(b0, b2); + let b1_plus_b3 = Self::add_vec(b1, b3); + let b1_minus_b3 = Self::sub_vec(b1, b3); + let b0_minus_b2 = Self::sub_vec(b0, b2); + [ + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [b0, two_b1, two_b2, two_b3], + )), + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [b1, b0_plus_b2, b1_plus_b3, b2], + )), + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [b2, b1_plus_b3, b0, b1_minus_b3], + )), + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [b3, b2, b1_minus_b3, b0_minus_b2], + )), + ] + } + } + + #[inline(always)] + fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { + unsafe { + let [a0, a1, a2, a3] = a.map(Self::to_vec); + let zero = _mm256_setzero_si256(); + let two_a1 = Self::add_vec(a1, a1); + let two_a2 = Self::add_vec(a2, a2); + let two_a3 = Self::add_vec(a3, a3); + let neg_a3 = Self::sub_vec(zero, a3); + let neg_two_a3 = Self::sub_vec(zero, two_a3); + [ + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [a0, two_a1, two_a2, two_a3], + )), + Self::from_vec(Self::dot_product_3_vec( + [a0, a1, a2], + [two_a1, two_a2, two_a3], + )), + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a1, a3], + [two_a2, a1, two_a3, neg_a3], + )), + Self::from_vec(Self::dot_product_3_vec( + [a0, a1, a2], + [two_a3, two_a2, neg_two_a3], + )), + ] + } + } + + #[inline(always)] + fn fp_ext4_inverse(a: [Self; 4]) -> Option<[Self; 4]> + where + Self::Scalar: Invertible, + { + unsafe { + let [a0, a1, a2, a3] = a.map(Self::to_vec); + let zero = _mm256_setzero_si256(); + let x0 = a0; + let x1 = a2; + let y0 = Self::sub_vec(a1, a3); + let y1 = a3; + + let x1_square = Self::mul_vec(x1, x1); + let y1_square = Self::mul_vec(y1, y1); + let aa0 = Self::add_vec(Self::mul_vec(x0, x0), Self::add_vec(x1_square, x1_square)); + let aa1 = { + let x0x1 = Self::mul_vec(x0, x1); + Self::add_vec(x0x1, x0x1) + }; + let bb0 = Self::add_vec(Self::mul_vec(y0, y0), Self::add_vec(y1_square, y1_square)); + let bb1 = { + let y0y1 = Self::mul_vec(y0, y1); + Self::add_vec(y0y1, y0y1) + }; + let nr_bb0 = Self::add_vec(Self::add_vec(bb0, bb0), Self::add_vec(bb1, bb1)); + let nr_bb1 = Self::add_vec(bb0, Self::add_vec(bb1, bb1)); + let norm0 = Self::sub_vec(aa0, nr_bb0); + let norm1 = Self::sub_vec(aa1, nr_bb1); + + let inv_norm_base = { + let norm1_square = Self::mul_vec(norm1, norm1); + let norm_base = Self::sub_vec( + Self::mul_vec(norm0, norm0), + Self::add_vec(norm1_square, norm1_square), + ); + Self::from_vec(norm_base).inverse()?.to_vec() + }; + let inv_norm0 = Self::mul_vec(norm0, inv_norm_base); + let inv_norm1 = Self::mul_vec(Self::sub_vec(zero, norm1), inv_norm_base); + + let v0 = Self::mul_vec(x0, inv_norm0); + let v1 = Self::mul_vec(x1, inv_norm1); + let constant0 = Self::add_vec(v0, Self::add_vec(v1, v1)); + let constant1 = Self::sub_vec( + Self::sub_vec( + Self::mul_vec(Self::add_vec(x0, x1), Self::add_vec(inv_norm0, inv_norm1)), + v0, + ), + v1, + ); + + let neg_y0 = Self::sub_vec(zero, y0); + let neg_y1 = Self::sub_vec(zero, y1); + let w0 = Self::mul_vec(neg_y0, inv_norm0); + let w1 = Self::mul_vec(neg_y1, inv_norm1); + let e1_coeff0 = Self::add_vec(w0, Self::add_vec(w1, w1)); + let e1_coeff1 = Self::sub_vec( + Self::sub_vec( + Self::mul_vec( + Self::add_vec(neg_y0, neg_y1), + Self::add_vec(inv_norm0, inv_norm1), + ), + w0, + ), + w1, + ); + + Some([ + Self::from_vec(constant0), + Self::from_vec(Self::add_vec(e1_coeff0, e1_coeff1)), + Self::from_vec(constant1), + Self::from_vec(e1_coeff1), + ]) + } + } +} diff --git a/crates/jolt-field/src/packed/avx2/fp64.rs b/crates/jolt-field/src/packed/avx2/fp64.rs new file mode 100644 index 0000000000..86350ef42d --- /dev/null +++ b/crates/jolt-field/src/packed/avx2/fp64.rs @@ -0,0 +1,267 @@ +use super::*; + +/// Number of `Fp64` lanes in an AVX2 packed vector. +pub(crate) const FP64_WIDTH: usize = 4; + +/// AVX2 packed arithmetic for `Fp64

`, processing 4 lanes. +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct PackedFp64Avx2(pub [Fp64

; FP64_WIDTH]); + +impl PackedFp64Avx2

{ + const BITS: u32 = 64 - P.leading_zeros(); + + const C_LO: u64 = { + let c = if Self::BITS == 64 { + 0u64.wrapping_sub(P) + } else { + (1u64 << Self::BITS) - P + }; + assert!(P != 0, "modulus must be nonzero"); + assert!(P & 1 == 1, "modulus must be odd"); + c + }; + + const MASK64: u64 = if Self::BITS < 64 { + (1u64 << Self::BITS) - 1 + } else { + u64::MAX + }; + + #[inline(always)] + fn to_vec(self) -> __m256i { + unsafe { transmute(self) } + } + + #[inline(always)] + unsafe fn from_vec(v: __m256i) -> Self { + unsafe { transmute(v) } + } + + #[inline] + unsafe fn reduce128_vec(hi: __m256i, lo: __m256i) -> __m256i { + if Self::BITS < 64 { + Self::reduce128_small_k(hi, lo) + } else { + Self::reduce128_full_k(hi, lo) + } + } + + /// Reduction for BITS < 64. All intermediates fit in u64 — no overflow. + #[inline] + unsafe fn reduce128_small_k(hi: __m256i, lo: __m256i) -> __m256i { + let mask_k = _mm256_set1_epi64x(Self::MASK64 as i64); + let c_vec = _mm256_set1_epi64x(Self::C_LO as i64); + let p_vec = _mm256_set1_epi64x(P as i64); + let shift_k = _mm_set_epi64x(0, Self::BITS as i64); + let shift_64mk = _mm_set_epi64x(0, (64 - Self::BITS) as i64); + + let lo_k = _mm256_and_si256(lo, mask_k); + let lo_upper = _mm256_srl_epi64(lo, shift_k); + let hi_shifted = _mm256_sll_epi64(hi, shift_64mk); + let hi_k = _mm256_or_si256(lo_upper, hi_shifted); + + let c_hi_lo = _mm256_mul_epu32(c_vec, hi_k); + let hi_k_top = _mm256_srli_epi64::<32>(hi_k); + let c_hi_top = _mm256_mul_epu32(c_vec, hi_k_top); + let c_hi_top_shifted = _mm256_slli_epi64::<32>(c_hi_top); + let c_hi_full = _mm256_add_epi64(c_hi_lo, c_hi_top_shifted); + + let fold1 = _mm256_add_epi64(lo_k, c_hi_full); + + let fold1_lo_k = _mm256_and_si256(fold1, mask_k); + let fold1_hi = _mm256_srl_epi64(fold1, shift_k); + let c_fold1_hi = _mm256_mul_epu32(c_vec, fold1_hi); + let fold2 = _mm256_add_epi64(fold1_lo_k, c_fold1_hi); + + let reduced = _mm256_sub_epi64(fold2, p_vec); + let sign = _mm256_set1_epi64x(i64::MIN); + let fold2_s = _mm256_xor_si256(fold2, sign); + let reduced_s = _mm256_xor_si256(reduced, sign); + let fold2_lt = _mm256_cmpgt_epi64(reduced_s, fold2_s); + _mm256_blendv_epi8(reduced, fold2, fold2_lt) + } + + /// Reduction for BITS == 64. Uses XOR-with-SIGN_BIT trick for unsigned + /// overflow detection. + #[inline] + unsafe fn reduce128_full_k(hi: __m256i, lo: __m256i) -> __m256i { + let c_vec = _mm256_set1_epi64x(Self::C_LO as i64); + let p_vec = _mm256_set1_epi64x(P as i64); + let sign = _mm256_set1_epi64x(i64::MIN); + let c_hi_lo = _mm256_mul_epu32(c_vec, hi); + let hi_hi = _mm256_srli_epi64::<32>(hi); + let c_hi_hi = _mm256_mul_epu32(c_vec, hi_hi); + + let c_hi_hi_lo32 = _mm256_slli_epi64::<32>(c_hi_hi); + let c_hi_carry = _mm256_srli_epi64::<32>(c_hi_hi); + + let sum_lo = _mm256_add_epi64(c_hi_lo, c_hi_hi_lo32); + let c_hi_lo_s = _mm256_xor_si256(c_hi_lo, sign); + let sum_lo_s = _mm256_xor_si256(sum_lo, sign); + let carry0 = _mm256_cmpgt_epi64(c_hi_lo_s, sum_lo_s); + let overflow = _mm256_sub_epi64(c_hi_carry, carry0); + + let s = _mm256_add_epi64(lo, sum_lo); + let lo_s = _mm256_xor_si256(lo, sign); + let s_s = _mm256_xor_si256(s, sign); + let carry1 = _mm256_cmpgt_epi64(lo_s, s_s); + let total_overflow = _mm256_sub_epi64(overflow, carry1); + + let final_corr = _mm256_mul_epu32(c_vec, total_overflow); + let result = _mm256_add_epi64(s, final_corr); + let s2_s = _mm256_xor_si256(s, sign); + let result_s = _mm256_xor_si256(result, sign); + let carry_f = _mm256_cmpgt_epi64(s2_s, result_s); + let corr_f = _mm256_and_si256(carry_f, c_vec); + let result = _mm256_add_epi64(result, corr_f); + + let result_s2 = _mm256_xor_si256(result, sign); + let p_s = _mm256_xor_si256(p_vec, sign); + let lt_p = _mm256_cmpgt_epi64(p_s, result_s2); + let sub_amt = _mm256_andnot_si256(lt_p, p_vec); + _mm256_sub_epi64(result, sub_amt) + } +} + +impl Default for PackedFp64Avx2

{ + #[inline] + fn default() -> Self { + Self([Fp64(0); FP64_WIDTH]) + } +} + +impl fmt::Debug for PackedFp64Avx2

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("PackedFp64Avx2").field(&self.0).finish() + } +} + +impl PartialEq for PackedFp64Avx2

{ + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for PackedFp64Avx2

{} + +impl Add for PackedFp64Avx2

{ + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + unsafe { + let a = self.to_vec(); + let b = rhs.to_vec(); + let p = _mm256_set1_epi64x(P as i64); + + let result = if Self::BITS <= 62 { + // a + b < 2P < 2^63: no overflow. + let s = _mm256_add_epi64(a, b); + let r = _mm256_sub_epi64(s, p); + // s < P? Use signed compare after shift trick. + let sign = _mm256_set1_epi64x(i64::MIN); + let s_s = _mm256_xor_si256(s, sign); + let p_s = _mm256_xor_si256(p, sign); + let borrow = _mm256_cmpgt_epi64(p_s, s_s); + _mm256_blendv_epi8(r, s, borrow) + } else { + // a + b can overflow u64. + let s = _mm256_add_epi64(a, b); + let sign = _mm256_set1_epi64x(i64::MIN); + let a_s = _mm256_xor_si256(a, sign); + let s_s = _mm256_xor_si256(s, sign); + let overflow = _mm256_cmpgt_epi64(a_s, s_s); + let c = _mm256_set1_epi64x(Self::C_LO as i64); + let s_plus_c = _mm256_add_epi64(s, c); + let s_minus_p = _mm256_sub_epi64(s, p); + let p_s = _mm256_xor_si256(p, sign); + let lt_p = _mm256_cmpgt_epi64(p_s, s_s); + let no_of = _mm256_blendv_epi8(s_minus_p, s, lt_p); + _mm256_blendv_epi8(no_of, s_plus_c, overflow) + }; + + Self::from_vec(result) + } + } +} + +impl Sub for PackedFp64Avx2

{ + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + unsafe { + let a = self.to_vec(); + let b = rhs.to_vec(); + let p = _mm256_set1_epi64x(P as i64); + let d = _mm256_sub_epi64(a, b); + + let sign = _mm256_set1_epi64x(i64::MIN); + let a_s = _mm256_xor_si256(a, sign); + let b_s = _mm256_xor_si256(b, sign); + let underflow = _mm256_cmpgt_epi64(b_s, a_s); + let corrected = _mm256_add_epi64(d, p); + Self::from_vec(_mm256_blendv_epi8(d, corrected, underflow)) + } + } +} + +impl Mul for PackedFp64Avx2

{ + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self { + unsafe { + let (hi, lo) = mul64_64_256(self.to_vec(), rhs.to_vec()); + Self::from_vec(Self::reduce128_vec(hi, lo)) + } + } +} + +impl PackedValue for PackedFp64Avx2

{ + type Value = Fp64

; + const WIDTH: usize = FP64_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Value, + { + Self([f(0), f(1), f(2), f(3)]) + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Value { + debug_assert!(lane < FP64_WIDTH); + self.0[lane] + } +} + +impl AddAssign for PackedFp64Avx2

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for PackedFp64Avx2

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for PackedFp64Avx2

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl PackedField for PackedFp64Avx2

{ + type Scalar = Fp64

; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self([value; FP64_WIDTH]) + } +} diff --git a/crates/jolt-field/src/packed/avx2/mod.rs b/crates/jolt-field/src/packed/avx2/mod.rs new file mode 100644 index 0000000000..f6217f35e0 --- /dev/null +++ b/crates/jolt-field/src/packed/avx2/mod.rs @@ -0,0 +1,65 @@ +//! AVX2 packed backends for Fp32, Fp64, Fp128. +//! +//! Techniques adapted from plonky2 (Goldilocks) and plonky3 (Mersenne-31). + +#![expect( + clippy::undocumented_unsafe_blocks, + reason = "ported AVX2 kernels retain their audited intrinsic-level invariants" +)] + +use super::{PackedField, PackedValue}; +use crate::ext::FpExt2Config; +use crate::Invertible; +use crate::{Fp128, Fp32, Fp64}; +use core::arch::x86_64::*; +use core::fmt; +use core::mem::transmute; +use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; + +/// Duplicate high 32 bits of each 64-bit lane into the low 32 bits. +/// Uses the float `movehdup` instruction which runs on port 5 (doesn't compete +/// with multiply on ports 0/1). +#[inline(always)] +unsafe fn movehdup_epi32(x: __m256i) -> __m256i { + _mm256_castps_si256(_mm256_movehdup_ps(_mm256_castsi256_ps(x))) +} + +#[inline(always)] +unsafe fn moveldup_epi32(x: __m256i) -> __m256i { + _mm256_castps_si256(_mm256_moveldup_ps(_mm256_castsi256_ps(x))) +} + +/// 64×64→128 schoolbook multiply using 32×32→64 partial products. +/// Returns (hi, lo) representing the 128-bit product. +#[inline] +unsafe fn mul64_64_256(x: __m256i, y: __m256i) -> (__m256i, __m256i) { + let x_hi = movehdup_epi32(x); + let y_hi = movehdup_epi32(y); + + let mul_ll = _mm256_mul_epu32(x, y); + let mul_lh = _mm256_mul_epu32(x, y_hi); + let mul_hl = _mm256_mul_epu32(x_hi, y); + let mul_hh = _mm256_mul_epu32(x_hi, y_hi); + + let mul_ll_hi = _mm256_srli_epi64::<32>(mul_ll); + let t0 = _mm256_add_epi64(mul_hl, mul_ll_hi); + let mask32 = _mm256_set1_epi64x(0xFFFF_FFFF_i64); + let t0_lo = _mm256_and_si256(t0, mask32); + let t0_hi = _mm256_srli_epi64::<32>(t0); + let t1 = _mm256_add_epi64(mul_lh, t0_lo); + let t2 = _mm256_add_epi64(mul_hh, t0_hi); + let t1_hi = _mm256_srli_epi64::<32>(t1); + let res_hi = _mm256_add_epi64(t2, t1_hi); + + let t1_lo = moveldup_epi32(t1); + let res_lo = _mm256_blend_epi32::<0b10101010>(mul_ll, t1_lo); + + (res_hi, res_lo) +} + +mod fp128; +mod fp32; +mod fp64; +pub(crate) use fp128::*; +pub(crate) use fp32::*; +pub(crate) use fp64::*; diff --git a/crates/jolt-field/src/packed/avx512/fp128.rs b/crates/jolt-field/src/packed/avx512/fp128.rs new file mode 100644 index 0000000000..8a1dd3791d --- /dev/null +++ b/crates/jolt-field/src/packed/avx512/fp128.rs @@ -0,0 +1,206 @@ +use super::*; + +/// Number of `Fp128` lanes in an AVX-512 packed vector. +pub(crate) const FP128_WIDTH: usize = 8; + +/// AVX-512 packed arithmetic for `Fp128

`, 8 lanes in SoA layout. +/// +/// Stores 8 elements as separate `lo` and `hi` `u64` arrays, enabling +/// vectorized add/sub via `__m512i`. Mul remains scalar per-lane. +#[derive(Clone, Copy)] +pub struct PackedFp128Avx512 { + lo: [u64; FP128_WIDTH], + hi: [u64; FP128_WIDTH], +} + +impl PackedFp128Avx512

{ + const P_LO: u64 = P as u64; + const P_HI: u64 = (P >> 64) as u64; +} + +impl Default for PackedFp128Avx512

{ + #[inline] + fn default() -> Self { + Self { + lo: [0; FP128_WIDTH], + hi: [0; FP128_WIDTH], + } + } +} + +impl fmt::Debug for PackedFp128Avx512

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let elems: Vec<_> = (0..FP128_WIDTH).map(|i| self.extract(i)).collect(); + f.debug_tuple("PackedFp128Avx512").field(&elems).finish() + } +} + +impl PartialEq for PackedFp128Avx512

{ + #[inline] + fn eq(&self, other: &Self) -> bool { + self.lo == other.lo && self.hi == other.hi + } +} + +impl Eq for PackedFp128Avx512

{} + +impl Add for PackedFp128Avx512

{ + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + unsafe { + let a_lo = _mm512_loadu_si512(self.lo.as_ptr().cast()); + let a_hi = _mm512_loadu_si512(self.hi.as_ptr().cast()); + let b_lo = _mm512_loadu_si512(rhs.lo.as_ptr().cast()); + let b_hi = _mm512_loadu_si512(rhs.hi.as_ptr().cast()); + let p_lo = _mm512_set1_epi64(Self::P_LO as i64); + let p_hi = _mm512_set1_epi64(Self::P_HI as i64); + let one = _mm512_set1_epi64(1); + + // 128-bit add: (sum_hi, sum_lo) = (a_hi, a_lo) + (b_hi, b_lo) + let sum_lo = _mm512_add_epi64(a_lo, b_lo); + let carry_lo = _mm512_cmplt_epu64_mask(sum_lo, a_lo); + let hi_tmp = _mm512_add_epi64(a_hi, b_hi); + let ov1 = _mm512_cmplt_epu64_mask(hi_tmp, a_hi); + let sum_hi = _mm512_mask_add_epi64(hi_tmp, carry_lo, hi_tmp, one); + let ov2 = _mm512_cmplt_epu64_mask(sum_hi, hi_tmp); + let carry_128 = ov1 | ov2; + + // 128-bit subtract P: (red_hi, red_lo) = (sum_hi, sum_lo) - P + let red_lo = _mm512_sub_epi64(sum_lo, p_lo); + let borrow_lo = _mm512_cmplt_epu64_mask(sum_lo, p_lo); + let red_hi_tmp = _mm512_sub_epi64(sum_hi, p_hi); + let bw1 = _mm512_cmplt_epu64_mask(sum_hi, p_hi); + let red_hi = _mm512_mask_sub_epi64(red_hi_tmp, borrow_lo, red_hi_tmp, one); + let bw2 = _mm512_cmplt_epu64_mask(red_hi_tmp, _mm512_maskz_mov_epi64(borrow_lo, one)); + let borrow = bw1 | bw2; + + // Use reduced if: overflow happened OR subtraction didn't borrow + let use_reduced = carry_128 | !borrow; + let out_lo = _mm512_mask_blend_epi64(use_reduced, sum_lo, red_lo); + let out_hi = _mm512_mask_blend_epi64(use_reduced, sum_hi, red_hi); + + let mut result = Self::default(); + _mm512_storeu_si512(result.lo.as_mut_ptr().cast(), out_lo); + _mm512_storeu_si512(result.hi.as_mut_ptr().cast(), out_hi); + result + } + } +} + +impl Sub for PackedFp128Avx512

{ + type Output = Self; + // `bw1 | bw2` below is correct 128-bit borrow wiring (mask OR), not an + // arithmetic bug; suppress the lint locally rather than module-wide. + #[expect(clippy::suspicious_arithmetic_impl)] + #[inline] + fn sub(self, rhs: Self) -> Self { + unsafe { + let a_lo = _mm512_loadu_si512(self.lo.as_ptr().cast()); + let a_hi = _mm512_loadu_si512(self.hi.as_ptr().cast()); + let b_lo = _mm512_loadu_si512(rhs.lo.as_ptr().cast()); + let b_hi = _mm512_loadu_si512(rhs.hi.as_ptr().cast()); + let p_lo = _mm512_set1_epi64(Self::P_LO as i64); + let p_hi = _mm512_set1_epi64(Self::P_HI as i64); + let one = _mm512_set1_epi64(1); + + // 128-bit sub: (diff_hi, diff_lo) = (a_hi, a_lo) - (b_hi, b_lo) + let diff_lo = _mm512_sub_epi64(a_lo, b_lo); + let borrow_lo = _mm512_cmplt_epu64_mask(a_lo, b_lo); + let hi_tmp = _mm512_sub_epi64(a_hi, b_hi); + let bw1 = _mm512_cmplt_epu64_mask(a_hi, b_hi); + let diff_hi = _mm512_mask_sub_epi64(hi_tmp, borrow_lo, hi_tmp, one); + let bw2 = _mm512_cmplt_epu64_mask(hi_tmp, _mm512_maskz_mov_epi64(borrow_lo, one)); + let borrow_128 = bw1 | bw2; + + // Correction: add P back where underflow occurred + let corr_lo = _mm512_add_epi64(diff_lo, p_lo); + let carry_lo = _mm512_cmplt_epu64_mask(corr_lo, diff_lo); + let corr_hi = _mm512_add_epi64(diff_hi, p_hi); + let corr_hi = _mm512_mask_add_epi64(corr_hi, carry_lo, corr_hi, one); + + let out_lo = _mm512_mask_blend_epi64(borrow_128, diff_lo, corr_lo); + let out_hi = _mm512_mask_blend_epi64(borrow_128, diff_hi, corr_hi); + + let mut result = Self::default(); + _mm512_storeu_si512(result.lo.as_mut_ptr().cast(), out_lo); + _mm512_storeu_si512(result.hi.as_mut_ptr().cast(), out_hi); + result + } + } +} + +impl Mul for PackedFp128Avx512

{ + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self { + let mut out = Self::default(); + for i in 0..FP128_WIDTH { + let a = Fp128::

([self.lo[i], self.hi[i]]); + let b = Fp128::

([rhs.lo[i], rhs.hi[i]]); + let r = a * b; + out.lo[i] = r.0[0]; + out.hi[i] = r.0[1]; + } + out + } +} + +impl PackedValue for PackedFp128Avx512

{ + type Value = Fp128

; + const WIDTH: usize = FP128_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Value, + { + let mut lo = [0u64; FP128_WIDTH]; + let mut hi = [0u64; FP128_WIDTH]; + for i in 0..FP128_WIDTH { + let v = f(i); + lo[i] = v.0[0]; + hi[i] = v.0[1]; + } + Self { lo, hi } + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Value { + debug_assert!(lane < FP128_WIDTH); + Fp128([self.lo[lane], self.hi[lane]]) + } +} + +impl AddAssign for PackedFp128Avx512

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for PackedFp128Avx512

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for PackedFp128Avx512

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl PackedField for PackedFp128Avx512

{ + type Scalar = Fp128

; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self { + lo: [value.0[0]; FP128_WIDTH], + hi: [value.0[1]; FP128_WIDTH], + } + } +} diff --git a/crates/jolt-field/src/packed/avx512/fp32.rs b/crates/jolt-field/src/packed/avx512/fp32.rs new file mode 100644 index 0000000000..96175d9753 --- /dev/null +++ b/crates/jolt-field/src/packed/avx512/fp32.rs @@ -0,0 +1,682 @@ +use super::*; + +/// Number of `Fp32` lanes in an AVX-512 packed vector. +pub(crate) const FP32_WIDTH: usize = 16; + +/// AVX-512 packed arithmetic for `Fp32

`, processing 16 lanes. +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct PackedFp32Avx512(pub [Fp32

; FP32_WIDTH]); + +impl PackedFp32Avx512

{ + const BITS: u32 = 32 - P.leading_zeros(); + + const C: u32 = { + let c = if Self::BITS == 32 { + 0u32.wrapping_sub(P) + } else { + (1u32 << Self::BITS) - P + }; + assert!(P != 0, "modulus must be nonzero"); + assert!(P & 1 == 1, "modulus must be odd"); + assert!( + (c as u64) * (c as u64 + 1) < P as u64, + "C(C+1) < P required for fused canonicalize" + ); + c + }; + + const MASK_U64: u64 = if Self::BITS == 32 { + u32::MAX as u64 + } else { + (1u64 << Self::BITS) - 1 + }; + + /// Whether two Solinas folds suffice to bring the sum of four + /// `(P-1)^2` products into `[0, 2*P)` for the final canonicalize step. + /// Mirrors `PackedFp32Neon::TWO_FOLD_FOUR_PRODUCT_OK`. When `false`, + /// `solinas_reduce` must do a third fold before handing off to + /// `pack_and_canonicalize`. + const TWO_FOLD_FOUR_PRODUCT_OK: bool = { + let c = Self::C as u64; + 4 * c * c + 3 * c <= (1u64 << Self::BITS) + }; + + #[inline(always)] + fn to_vec(self) -> __m512i { + unsafe { transmute(self) } + } + + #[inline(always)] + unsafe fn from_vec(v: __m512i) -> Self { + unsafe { transmute(v) } + } + + /// Multiply each `u64` lane by `C`. Building block of Solinas reduction; + /// the `C == 1` fast path skips the multiply entirely for Mersenne-like + /// primes. + /// + /// Uses `_mm512_mullo_epi64` (AVX-512DQ, single `vpmullq`) for full + /// 64-bit width. The previous implementation used `_mm512_mul_epu32` + /// which only reads the *low 32 bits* of each lane and silently dropped + /// bit 32+ of the input — fine for `BITS == 32` (where the caller's + /// `prod >> 32` always fits in 32 bits) but wrong for `BITS == 31` and + /// `C != 1` where `prod >> 31` can occupy 33 bits. + #[inline(always)] + unsafe fn mul_c_u64(x: __m512i) -> __m512i { + if Self::C == 1 { + x + } else { + let c_vec = _mm512_set1_epi64(Self::C as i64); + _mm512_mullo_epi64(x, c_vec) + } + } + + /// One Solinas fold of a single 64-bit product lane (BITS == 32 only): + /// `(x & (2^32-1)) + C*(x >> 32)`. For a single product `x < 2^64` the + /// high word `x >> 32 < 2^32`, so the result is `< 2^32 + C*2^32 < 2^40`. + /// Used by the `BITS == 32` dot-product path to pre-fold each product so + /// that up to four folded terms (each `< 2^40`) sum below `2^42` without + /// overflowing a `u64`, removing the per-product carry tracking. + #[inline(always)] + unsafe fn fold_product_once(x: __m512i) -> __m512i { + let lo = _mm512_and_si512(x, _mm512_set1_epi64(Self::MASK_U64 as i64)); + let hi = _mm512_srli_epi64::<32>(x); + _mm512_add_epi64(lo, Self::mul_c_u64(hi)) + } + + /// Plonky3-style Mersenne31 multiply (P = 2^31 - 1). Specialized using + /// `_mm512_srli_epi64::<31>` shifts and 16-lane mask blends. Used by + /// the `Mul` impl when `Self::BITS == 31 && Self::C == 1`. + #[inline(always)] + unsafe fn mul_mersenne31_vec(a: __m512i, b: __m512i) -> __m512i { + unsafe { + const EVENS: __mmask16 = 0b0101_0101_0101_0101; + const ODDS: __mmask16 = 0b1010_1010_1010_1010; + + let lhs_evn_dbl = _mm512_add_epi32(a, a); + let rhs_odd = movehdup_epi32_512(b); + let lhs_odd_dbl = _mm512_srli_epi64::<31>(a); + + let prod_odd_dbl = _mm512_mul_epu32(lhs_odd_dbl, rhs_odd); + let prod_evn_dbl = _mm512_mul_epu32(lhs_evn_dbl, b); + + let prod_lo_dbl = + _mm512_mask_blend_epi32(ODDS, prod_evn_dbl, moveldup_epi32_512(prod_odd_dbl)); + let prod_hi = + _mm512_mask_blend_epi32(EVENS, prod_odd_dbl, movehdup_epi32_512(prod_evn_dbl)); + let prod_lo = _mm512_srli_epi32::<1>(prod_lo_dbl); + + let p = _mm512_set1_epi32(P as i32); + let folded = _mm512_add_epi32(prod_lo, prod_hi); + _mm512_min_epu32(folded, _mm512_sub_epi32(folded, p)) + } + } + + /// Vector form of field add: 16-lane add + canonicalize to `[0, P)`. + /// Mirrors `PackedFp32Avx2::add_vec` with native AVX-512 mask compares. + #[inline(always)] + unsafe fn add_vec(a: __m512i, b: __m512i) -> __m512i { + let p = _mm512_set1_epi32(P as i32); + if Self::BITS <= 31 { + let t = _mm512_add_epi32(a, b); + let u = _mm512_sub_epi32(t, p); + _mm512_min_epu32(t, u) + } else { + let c = _mm512_set1_epi32(Self::C as i32); + let t = _mm512_add_epi32(a, b); + let overflow = _mm512_cmplt_epu32_mask(t, a); + let t2 = _mm512_mask_add_epi32(t, overflow, t, c); + let geq_p = _mm512_cmpge_epu32_mask(t2, p); + _mm512_mask_sub_epi32(t2, geq_p, t2, p) + } + } + + /// Vector form of field sub: 16-lane sub + canonicalize to `[0, P)`. + /// Mirrors `PackedFp32Avx2::sub_vec` with native AVX-512 mask compares. + #[inline(always)] + unsafe fn sub_vec(a: __m512i, b: __m512i) -> __m512i { + let p = _mm512_set1_epi32(P as i32); + if Self::BITS <= 31 { + let t = _mm512_sub_epi32(a, b); + let u = _mm512_add_epi32(t, p); + _mm512_min_epu32(t, u) + } else { + let t = _mm512_sub_epi32(a, b); + let underflow = _mm512_cmplt_epu32_mask(a, b); + _mm512_mask_add_epi32(t, underflow, t, p) + } + } + + /// Vector form of field mul: 16-lane Solinas multiply + canonicalize. + #[inline(always)] + unsafe fn mul_vec(a: __m512i, b: __m512i) -> __m512i { + let prod_evn = _mm512_mul_epu32(a, b); + let a_odd = movehdup_epi32_512(a); + let b_odd = movehdup_epi32_512(b); + let prod_odd = _mm512_mul_epu32(a_odd, b_odd); + Self::solinas_reduce(prod_evn, prod_odd) + } + + /// 4-way fused multiply-accumulate with a single end-reduction. + /// Mirrors `PackedFp32Avx2::dot_product_4_vec` at 16 lanes. For + /// `BITS <= 31`, four `(2^31 - 1)^2` products sum below `2^64`, so the + /// raw products accumulate without overflow. For `BITS == 32`, each + /// product is pre-folded once (`< 2^40`) so four folds sum below `2^42`, + /// again overflow-free. Both branches end in a single carry-free + /// `solinas_reduce`; the `if` is a const condition resolved at compile + /// time. + #[inline(always)] + unsafe fn dot_product_4_vec(a: [__m512i; 4], b: [__m512i; 4]) -> __m512i { + let mut sum_evn = _mm512_mul_epu32(a[0], b[0]); + let mut sum_odd = _mm512_mul_epu32(movehdup_epi32_512(a[0]), movehdup_epi32_512(b[0])); + + if Self::BITS <= 31 { + for i in 1..4 { + let prod_evn = _mm512_mul_epu32(a[i], b[i]); + let prod_odd = _mm512_mul_epu32(movehdup_epi32_512(a[i]), movehdup_epi32_512(b[i])); + sum_evn = _mm512_add_epi64(sum_evn, prod_evn); + sum_odd = _mm512_add_epi64(sum_odd, prod_odd); + } + return Self::solinas_reduce(sum_evn, sum_odd); + } + + // BITS == 32: four 32-bit products overflow a `u64` sum, so pre-fold each + // product once (`< 2^40`) and accumulate the folds (`< 4*2^40 < 2^42`), + // which is carry-free, then a single carry-free `solinas_reduce`. + let mut sum_evn = Self::fold_product_once(sum_evn); + let mut sum_odd = Self::fold_product_once(sum_odd); + for i in 1..4 { + let prod_evn = Self::fold_product_once(_mm512_mul_epu32(a[i], b[i])); + let prod_odd = Self::fold_product_once(_mm512_mul_epu32( + movehdup_epi32_512(a[i]), + movehdup_epi32_512(b[i]), + )); + sum_evn = _mm512_add_epi64(sum_evn, prod_evn); + sum_odd = _mm512_add_epi64(sum_odd, prod_odd); + } + Self::solinas_reduce(sum_evn, sum_odd) + } + + /// 3-way fused multiply-accumulate with a single end-reduction. + #[inline(always)] + unsafe fn dot_product_3_vec(a: [__m512i; 3], b: [__m512i; 3]) -> __m512i { + let mut sum_evn = _mm512_mul_epu32(a[0], b[0]); + let mut sum_odd = _mm512_mul_epu32(movehdup_epi32_512(a[0]), movehdup_epi32_512(b[0])); + + if Self::BITS <= 31 { + for i in 1..3 { + let prod_evn = _mm512_mul_epu32(a[i], b[i]); + let prod_odd = _mm512_mul_epu32(movehdup_epi32_512(a[i]), movehdup_epi32_512(b[i])); + sum_evn = _mm512_add_epi64(sum_evn, prod_evn); + sum_odd = _mm512_add_epi64(sum_odd, prod_odd); + } + return Self::solinas_reduce(sum_evn, sum_odd); + } + + // BITS == 32: pre-fold (see `dot_product_4_vec`). + let mut sum_evn = Self::fold_product_once(sum_evn); + let mut sum_odd = Self::fold_product_once(sum_odd); + for i in 1..3 { + let prod_evn = Self::fold_product_once(_mm512_mul_epu32(a[i], b[i])); + let prod_odd = Self::fold_product_once(_mm512_mul_epu32( + movehdup_epi32_512(a[i]), + movehdup_epi32_512(b[i]), + )); + sum_evn = _mm512_add_epi64(sum_evn, prod_evn); + sum_odd = _mm512_add_epi64(sum_odd, prod_odd); + } + Self::solinas_reduce(sum_evn, sum_odd) + } + + /// Multiply by an `FpExt2` non-residue. Recognizes `nr == -1` and `nr == 2` + /// fast paths. + #[inline(always)] + unsafe fn mul_nr_vec(x: __m512i) -> __m512i + where + C: FpExt2Config>, + { + if C::IS_NEG_ONE { + Self::sub_vec(_mm512_setzero_si512(), x) + } else if C::non_residue().0 == 2 { + Self::add_vec(x, x) + } else { + C::mul_non_residue(Self::from_vec(x), Self::broadcast).to_vec() + } + } + + /// Two-or-three-fold Solinas reduction of 8+8 `u64` products → 16 `u32` + /// lanes. + /// + /// The `Self::BITS == 31` branches use immediate-shift + /// `_mm512_srli_epi64::<31>` instead of the generic variable-shift + /// `_mm512_srl_epi64(.., shift)`, mirroring the same specialisation + /// the base-field `Mul` impl uses on Mersenne31, so extension-field + /// operations on Mersenne31 get the same per-shift win. + /// + /// Two folds always suffice when `Self::TWO_FOLD_FOUR_PRODUCT_OK`. When + /// it doesn't (large `C` such that `4*C^2 + 3*C > 2^BITS`), we run a + /// third fold so `pack_and_canonicalize`'s single subtract-and-min step + /// is enough to land in `[0, P)`. Mirrors `PackedFp32Neon::solinas_reduce`. + #[inline(always)] + unsafe fn solinas_reduce(prod_evn: __m512i, prod_odd: __m512i) -> __m512i { + let mask = _mm512_set1_epi64(Self::MASK_U64 as i64); + let shift = _mm_set_epi64x(0, Self::BITS as i64); + + // Fold 1 + let evn_lo = _mm512_and_si512(prod_evn, mask); + let evn_hi = if Self::BITS == 31 { + _mm512_srli_epi64::<31>(prod_evn) + } else { + _mm512_srl_epi64(prod_evn, shift) + }; + let evn_f1 = _mm512_add_epi64(evn_lo, Self::mul_c_u64(evn_hi)); + + let odd_lo = _mm512_and_si512(prod_odd, mask); + let odd_hi = if Self::BITS == 31 { + _mm512_srli_epi64::<31>(prod_odd) + } else { + _mm512_srl_epi64(prod_odd, shift) + }; + let odd_f1 = _mm512_add_epi64(odd_lo, Self::mul_c_u64(odd_hi)); + + // Fold 2 + let evn_f1_lo = _mm512_and_si512(evn_f1, mask); + let evn_f1_hi = if Self::BITS == 31 { + _mm512_srli_epi64::<31>(evn_f1) + } else { + _mm512_srl_epi64(evn_f1, shift) + }; + let evn_f2 = _mm512_add_epi64(evn_f1_lo, Self::mul_c_u64(evn_f1_hi)); + + let odd_f1_lo = _mm512_and_si512(odd_f1, mask); + let odd_f1_hi = if Self::BITS == 31 { + _mm512_srli_epi64::<31>(odd_f1) + } else { + _mm512_srl_epi64(odd_f1, shift) + }; + let odd_f2 = _mm512_add_epi64(odd_f1_lo, Self::mul_c_u64(odd_f1_hi)); + + // Optional third fold for large-C primes (e.g. Generic31Offset32787) + // where two folds leave residue > 2*P. + let (evn_final, odd_final) = if Self::TWO_FOLD_FOUR_PRODUCT_OK { + (evn_f2, odd_f2) + } else { + let evn_f2_lo = _mm512_and_si512(evn_f2, mask); + let evn_f2_hi = if Self::BITS == 31 { + _mm512_srli_epi64::<31>(evn_f2) + } else { + _mm512_srl_epi64(evn_f2, shift) + }; + let odd_f2_lo = _mm512_and_si512(odd_f2, mask); + let odd_f2_hi = if Self::BITS == 31 { + _mm512_srli_epi64::<31>(odd_f2) + } else { + _mm512_srl_epi64(odd_f2, shift) + }; + ( + _mm512_add_epi64(evn_f2_lo, Self::mul_c_u64(evn_f2_hi)), + _mm512_add_epi64(odd_f2_lo, Self::mul_c_u64(odd_f2_hi)), + ) + }; + + Self::pack_and_canonicalize(evn_final, odd_final) + } + + /// Combine 8+8 `u64` lanes into 16 `u32` lanes canonicalized to `[0, P)`. + /// AVX-512 uses native unsigned compare masks and `_mm512_min_epu64`, + /// simplifying both branches vs the AVX2 implementation. + #[inline(always)] + unsafe fn pack_and_canonicalize(evn_f2: __m512i, odd_f2: __m512i) -> __m512i { + if Self::BITS < 32 { + let odd_shifted = _mm512_slli_epi64::<32>(odd_f2); + let combined = _mm512_mask_blend_epi32(0b1010_1010_1010_1010, evn_f2, odd_shifted); + let p = _mm512_set1_epi32(P as i32); + let reduced = _mm512_sub_epi32(combined, p); + _mm512_min_epu32(combined, reduced) + } else { + let p_u64 = _mm512_set1_epi64(P as i64); + + let red_evn = _mm512_sub_epi64(evn_f2, p_u64); + let out_evn = _mm512_min_epu64(evn_f2, red_evn); + + let red_odd = _mm512_sub_epi64(odd_f2, p_u64); + let out_odd = _mm512_min_epu64(odd_f2, red_odd); + + let odd_shifted = _mm512_slli_epi64::<32>(out_odd); + _mm512_mask_blend_epi32(0b1010_1010_1010_1010, out_evn, odd_shifted) + } + } +} + +impl Default for PackedFp32Avx512

{ + #[inline] + fn default() -> Self { + Self([Fp32(0); FP32_WIDTH]) + } +} + +impl fmt::Debug for PackedFp32Avx512

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("PackedFp32Avx512").field(&self.0).finish() + } +} + +impl PartialEq for PackedFp32Avx512

{ + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for PackedFp32Avx512

{} + +impl Add for PackedFp32Avx512

{ + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + unsafe { Self::from_vec(Self::add_vec(self.to_vec(), rhs.to_vec())) } + } +} + +impl Sub for PackedFp32Avx512

{ + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + unsafe { Self::from_vec(Self::sub_vec(self.to_vec(), rhs.to_vec())) } + } +} + +impl Mul for PackedFp32Avx512

{ + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self { + unsafe { + let a = self.to_vec(); + let b = rhs.to_vec(); + + if Self::BITS == 31 && Self::C == 1 { + return Self::from_vec(Self::mul_mersenne31_vec(a, b)); + } + + let prod_evn = _mm512_mul_epu32(a, b); + let a_odd = movehdup_epi32_512(a); + let b_odd = movehdup_epi32_512(b); + let prod_odd = _mm512_mul_epu32(a_odd, b_odd); + + let mask = _mm512_set1_epi64(Self::MASK_U64 as i64); + let shift = _mm_set_epi64x(0, Self::BITS as i64); + + // Fold 1 + let evn_lo = _mm512_and_si512(prod_evn, mask); + let evn_hi = if Self::BITS == 31 { + _mm512_srli_epi64::<31>(prod_evn) + } else { + _mm512_srl_epi64(prod_evn, shift) + }; + let evn_f1 = _mm512_add_epi64(evn_lo, Self::mul_c_u64(evn_hi)); + + let odd_lo = _mm512_and_si512(prod_odd, mask); + let odd_hi = if Self::BITS == 31 { + _mm512_srli_epi64::<31>(prod_odd) + } else { + _mm512_srl_epi64(prod_odd, shift) + }; + let odd_f1 = _mm512_add_epi64(odd_lo, Self::mul_c_u64(odd_hi)); + + // Fold 2 + let evn_f1_lo = _mm512_and_si512(evn_f1, mask); + let evn_f1_hi = if Self::BITS == 31 { + _mm512_srli_epi64::<31>(evn_f1) + } else { + _mm512_srl_epi64(evn_f1, shift) + }; + let evn_f2 = _mm512_add_epi64(evn_f1_lo, Self::mul_c_u64(evn_f1_hi)); + + let odd_f1_lo = _mm512_and_si512(odd_f1, mask); + let odd_f1_hi = if Self::BITS == 31 { + _mm512_srli_epi64::<31>(odd_f1) + } else { + _mm512_srl_epi64(odd_f1, shift) + }; + let odd_f2 = _mm512_add_epi64(odd_f1_lo, Self::mul_c_u64(odd_f1_hi)); + + // Recombine + canonicalize. For `BITS == 32` the two-fold residue + // can land in `[2^32, 2*P)` (up to `2^32 + C^2`), so the subtract + // must happen on the full 64-bit lanes before packing; a 32-bit + // recombine would drop bit 32. `pack_and_canonicalize` does the + // 64-bit subtract for `BITS == 32` and is identical to the inline + // 32-bit recombine for `BITS < 32`. + Self::from_vec(Self::pack_and_canonicalize(evn_f2, odd_f2)) + } + } +} + +impl PackedValue for PackedFp32Avx512

{ + type Value = Fp32

; + const WIDTH: usize = FP32_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Value, + { + Self([ + f(0), + f(1), + f(2), + f(3), + f(4), + f(5), + f(6), + f(7), + f(8), + f(9), + f(10), + f(11), + f(12), + f(13), + f(14), + f(15), + ]) + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Value { + debug_assert!(lane < FP32_WIDTH); + self.0[lane] + } +} + +impl AddAssign for PackedFp32Avx512

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for PackedFp32Avx512

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for PackedFp32Avx512

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl PackedField for PackedFp32Avx512

{ + type Scalar = Fp32

; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self([value; FP32_WIDTH]) + } + + #[inline(always)] + fn fp_ext2_mul(a0: Self, a1: Self, b0: Self, b1: Self) -> (Self, Self) + where + C: FpExt2Config, + { + unsafe { + let a0 = a0.to_vec(); + let a1 = a1.to_vec(); + let b0 = b0.to_vec(); + let b1 = b1.to_vec(); + + let v0 = Self::mul_vec(a0, b0); + let v1 = Self::mul_vec(a1, b1); + let cross = Self::mul_vec(Self::add_vec(a0, a1), Self::add_vec(b0, b1)); + + ( + Self::from_vec(Self::add_vec(v0, Self::mul_nr_vec::(v1))), + Self::from_vec(Self::sub_vec(Self::sub_vec(cross, v0), v1)), + ) + } + } + + #[inline(always)] + fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { + unsafe { + let [a0, a1, a2, a3] = a.map(Self::to_vec); + let [b0, b1, b2, b3] = b.map(Self::to_vec); + let two_b1 = Self::add_vec(b1, b1); + let two_b2 = Self::add_vec(b2, b2); + let two_b3 = Self::add_vec(b3, b3); + let b0_plus_b2 = Self::add_vec(b0, b2); + let b1_plus_b3 = Self::add_vec(b1, b3); + let b1_minus_b3 = Self::sub_vec(b1, b3); + let b0_minus_b2 = Self::sub_vec(b0, b2); + [ + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [b0, two_b1, two_b2, two_b3], + )), + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [b1, b0_plus_b2, b1_plus_b3, b2], + )), + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [b2, b1_plus_b3, b0, b1_minus_b3], + )), + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [b3, b2, b1_minus_b3, b0_minus_b2], + )), + ] + } + } + + #[inline(always)] + fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { + unsafe { + let [a0, a1, a2, a3] = a.map(Self::to_vec); + let zero = _mm512_setzero_si512(); + let two_a1 = Self::add_vec(a1, a1); + let two_a2 = Self::add_vec(a2, a2); + let two_a3 = Self::add_vec(a3, a3); + let neg_a3 = Self::sub_vec(zero, a3); + let neg_two_a3 = Self::sub_vec(zero, two_a3); + [ + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [a0, two_a1, two_a2, two_a3], + )), + Self::from_vec(Self::dot_product_3_vec( + [a0, a1, a2], + [two_a1, two_a2, two_a3], + )), + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a1, a3], + [two_a2, a1, two_a3, neg_a3], + )), + Self::from_vec(Self::dot_product_3_vec( + [a0, a1, a2], + [two_a3, two_a2, neg_two_a3], + )), + ] + } + } + + #[inline(always)] + fn fp_ext4_inverse(a: [Self; 4]) -> Option<[Self; 4]> + where + Self::Scalar: Invertible, + { + unsafe { + let [a0, a1, a2, a3] = a.map(Self::to_vec); + let zero = _mm512_setzero_si512(); + let x0 = a0; + let x1 = a2; + let y0 = Self::sub_vec(a1, a3); + let y1 = a3; + + let x1_square = Self::mul_vec(x1, x1); + let y1_square = Self::mul_vec(y1, y1); + let aa0 = Self::add_vec(Self::mul_vec(x0, x0), Self::add_vec(x1_square, x1_square)); + let aa1 = { + let x0x1 = Self::mul_vec(x0, x1); + Self::add_vec(x0x1, x0x1) + }; + let bb0 = Self::add_vec(Self::mul_vec(y0, y0), Self::add_vec(y1_square, y1_square)); + let bb1 = { + let y0y1 = Self::mul_vec(y0, y1); + Self::add_vec(y0y1, y0y1) + }; + let nr_bb0 = Self::add_vec(Self::add_vec(bb0, bb0), Self::add_vec(bb1, bb1)); + let nr_bb1 = Self::add_vec(bb0, Self::add_vec(bb1, bb1)); + let norm0 = Self::sub_vec(aa0, nr_bb0); + let norm1 = Self::sub_vec(aa1, nr_bb1); + + let inv_norm_base = { + let norm1_square = Self::mul_vec(norm1, norm1); + let norm_base = Self::sub_vec( + Self::mul_vec(norm0, norm0), + Self::add_vec(norm1_square, norm1_square), + ); + Self::from_vec(norm_base).inverse()?.to_vec() + }; + let inv_norm0 = Self::mul_vec(norm0, inv_norm_base); + let inv_norm1 = Self::mul_vec(Self::sub_vec(zero, norm1), inv_norm_base); + + let v0 = Self::mul_vec(x0, inv_norm0); + let v1 = Self::mul_vec(x1, inv_norm1); + let constant0 = Self::add_vec(v0, Self::add_vec(v1, v1)); + let constant1 = Self::sub_vec( + Self::sub_vec( + Self::mul_vec(Self::add_vec(x0, x1), Self::add_vec(inv_norm0, inv_norm1)), + v0, + ), + v1, + ); + + let neg_y0 = Self::sub_vec(zero, y0); + let neg_y1 = Self::sub_vec(zero, y1); + let w0 = Self::mul_vec(neg_y0, inv_norm0); + let w1 = Self::mul_vec(neg_y1, inv_norm1); + let e1_coeff0 = Self::add_vec(w0, Self::add_vec(w1, w1)); + let e1_coeff1 = Self::sub_vec( + Self::sub_vec( + Self::mul_vec( + Self::add_vec(neg_y0, neg_y1), + Self::add_vec(inv_norm0, inv_norm1), + ), + w0, + ), + w1, + ); + + Some([ + Self::from_vec(constant0), + Self::from_vec(Self::add_vec(e1_coeff0, e1_coeff1)), + Self::from_vec(constant1), + Self::from_vec(e1_coeff1), + ]) + } + } +} diff --git a/crates/jolt-field/src/packed/avx512/fp64.rs b/crates/jolt-field/src/packed/avx512/fp64.rs new file mode 100644 index 0000000000..bb304497c0 --- /dev/null +++ b/crates/jolt-field/src/packed/avx512/fp64.rs @@ -0,0 +1,246 @@ +use super::*; + +/// Number of `Fp64` lanes in an AVX-512 packed vector. +pub(crate) const FP64_WIDTH: usize = 8; + +/// AVX-512 packed arithmetic for `Fp64

`, processing 8 lanes. +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct PackedFp64Avx512(pub [Fp64

; FP64_WIDTH]); + +impl PackedFp64Avx512

{ + const BITS: u32 = 64 - P.leading_zeros(); + + const C_LO: u64 = { + let c = if Self::BITS == 64 { + 0u64.wrapping_sub(P) + } else { + (1u64 << Self::BITS) - P + }; + assert!(P != 0, "modulus must be nonzero"); + assert!(P & 1 == 1, "modulus must be odd"); + c + }; + + const MASK64: u64 = if Self::BITS < 64 { + (1u64 << Self::BITS) - 1 + } else { + u64::MAX + }; + + #[inline(always)] + fn to_vec(self) -> __m512i { + unsafe { transmute(self) } + } + + #[inline(always)] + unsafe fn from_vec(v: __m512i) -> Self { + unsafe { transmute(v) } + } + + /// Vectorized 128-bit Solinas reduction for p = 2^BITS - C. + /// Given (hi, lo) = 128-bit product, computes result ≡ (hi*2^64 + lo) mod p. + #[inline] + unsafe fn reduce128_vec(hi: __m512i, lo: __m512i) -> __m512i { + if Self::BITS < 64 { + Self::reduce128_small_k(hi, lo) + } else { + Self::reduce128_full_k(hi, lo) + } + } + + /// Reduction for BITS < 64 (e.g. 40-bit prime). No overflow issues: all + /// intermediates fit in u64. + #[inline] + unsafe fn reduce128_small_k(hi: __m512i, lo: __m512i) -> __m512i { + let mask_k = _mm512_set1_epi64(Self::MASK64 as i64); + let c_vec = _mm512_set1_epi64(Self::C_LO as i64); + let p_vec = _mm512_set1_epi64(P as i64); + let shift_k = _mm_set_epi64x(0, Self::BITS as i64); + let shift_64mk = _mm_set_epi64x(0, (64 - Self::BITS) as i64); + + let lo_k = _mm512_and_si512(lo, mask_k); + let lo_upper = _mm512_srl_epi64(lo, shift_k); + let hi_shifted = _mm512_sll_epi64(hi, shift_64mk); + let hi_k = _mm512_or_si512(lo_upper, hi_shifted); + + // c * hi_k: hi_k may exceed 32 bits, split into lo32 and top + let c_hi_lo = _mm512_mul_epu32(c_vec, hi_k); + let hi_k_top = _mm512_srli_epi64::<32>(hi_k); + let c_hi_top = _mm512_mul_epu32(c_vec, hi_k_top); + let c_hi_top_shifted = _mm512_slli_epi64::<32>(c_hi_top); + let c_hi_full = _mm512_add_epi64(c_hi_lo, c_hi_top_shifted); + + let fold1 = _mm512_add_epi64(lo_k, c_hi_full); + + let fold1_lo_k = _mm512_and_si512(fold1, mask_k); + let fold1_hi = _mm512_srl_epi64(fold1, shift_k); + let c_fold1_hi = _mm512_mul_epu32(c_vec, fold1_hi); + let fold2 = _mm512_add_epi64(fold1_lo_k, c_fold1_hi); + + let reduced = _mm512_sub_epi64(fold2, p_vec); + _mm512_min_epu64(fold2, reduced) + } + + /// Reduction for BITS == 64 (e.g. p = 2^64 - 87). Tracks overflow from + /// c*hi exceeding 64 bits, using native unsigned comparisons. + #[inline] + unsafe fn reduce128_full_k(hi: __m512i, lo: __m512i) -> __m512i { + let c_vec = _mm512_set1_epi64(Self::C_LO as i64); + let p_vec = _mm512_set1_epi64(P as i64); + let one = _mm512_set1_epi64(1); + + // c * hi_lo32 + let c_hi_lo = _mm512_mul_epu32(c_vec, hi); + // c * hi_hi32 + let hi_hi = _mm512_srli_epi64::<32>(hi); + let c_hi_hi = _mm512_mul_epu32(c_vec, hi_hi); + + let c_hi_hi_lo32 = _mm512_slli_epi64::<32>(c_hi_hi); + let c_hi_carry = _mm512_srli_epi64::<32>(c_hi_hi); + + // Lower 64 bits of c * hi + let sum_lo = _mm512_add_epi64(c_hi_lo, c_hi_hi_lo32); + let carry0 = _mm512_cmplt_epu64_mask(sum_lo, c_hi_lo); + let overflow = _mm512_mask_add_epi64(c_hi_carry, carry0, c_hi_carry, one); + + // lo + sum_lo + let s = _mm512_add_epi64(lo, sum_lo); + let carry1 = _mm512_cmplt_epu64_mask(s, lo); + let total_overflow = _mm512_mask_add_epi64(overflow, carry1, overflow, one); + + // Fold overflow: total_overflow * c (at most ~2^15) + let final_corr = _mm512_mul_epu32(c_vec, total_overflow); + let result = _mm512_add_epi64(s, final_corr); + let carry_f = _mm512_cmplt_epu64_mask(result, s); + let result = _mm512_mask_add_epi64(result, carry_f, result, c_vec); + + let ge_mask = _mm512_cmpge_epu64_mask(result, p_vec); + _mm512_mask_sub_epi64(result, ge_mask, result, p_vec) + } +} + +impl Default for PackedFp64Avx512

{ + #[inline] + fn default() -> Self { + Self([Fp64(0); FP64_WIDTH]) + } +} + +impl fmt::Debug for PackedFp64Avx512

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("PackedFp64Avx512").field(&self.0).finish() + } +} + +impl PartialEq for PackedFp64Avx512

{ + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for PackedFp64Avx512

{} + +impl Add for PackedFp64Avx512

{ + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + unsafe { + let a = self.to_vec(); + let b = rhs.to_vec(); + let p = _mm512_set1_epi64(P as i64); + + let result = if Self::BITS <= 62 { + let s = _mm512_add_epi64(a, b); + let geq_p = _mm512_cmpge_epu64_mask(s, p); + _mm512_mask_sub_epi64(s, geq_p, s, p) + } else { + let s = _mm512_add_epi64(a, b); + let overflow = _mm512_cmplt_epu64_mask(s, a); + let c = _mm512_set1_epi64(Self::C_LO as i64); + let geq_p = _mm512_cmpge_epu64_mask(s, p); + let no_of = _mm512_mask_sub_epi64(s, geq_p, s, p); + let s_plus_c = _mm512_add_epi64(s, c); + _mm512_mask_blend_epi64(overflow, no_of, s_plus_c) + }; + + Self::from_vec(result) + } + } +} + +impl Sub for PackedFp64Avx512

{ + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + unsafe { + let a = self.to_vec(); + let b = rhs.to_vec(); + let p = _mm512_set1_epi64(P as i64); + let d = _mm512_sub_epi64(a, b); + let underflow = _mm512_cmplt_epu64_mask(a, b); + Self::from_vec(_mm512_mask_add_epi64(d, underflow, d, p)) + } + } +} + +impl Mul for PackedFp64Avx512

{ + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self { + unsafe { + let (hi, lo) = mul64_64_512(self.to_vec(), rhs.to_vec()); + Self::from_vec(Self::reduce128_vec(hi, lo)) + } + } +} + +impl PackedValue for PackedFp64Avx512

{ + type Value = Fp64

; + const WIDTH: usize = FP64_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Value, + { + Self([f(0), f(1), f(2), f(3), f(4), f(5), f(6), f(7)]) + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Value { + debug_assert!(lane < FP64_WIDTH); + self.0[lane] + } +} + +impl AddAssign for PackedFp64Avx512

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for PackedFp64Avx512

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for PackedFp64Avx512

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl PackedField for PackedFp64Avx512

{ + type Scalar = Fp64

; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self([value; FP64_WIDTH]) + } +} diff --git a/crates/jolt-field/src/packed/avx512/mod.rs b/crates/jolt-field/src/packed/avx512/mod.rs new file mode 100644 index 0000000000..e84f37945b --- /dev/null +++ b/crates/jolt-field/src/packed/avx512/mod.rs @@ -0,0 +1,64 @@ +//! AVX-512 packed backends for Fp32, Fp64, Fp128. +//! +//! Requires AVX-512F + AVX-512DQ. Uses native unsigned comparisons and mask +//! registers for branchless conditionals. + +#![expect( + clippy::undocumented_unsafe_blocks, + reason = "ported AVX-512 kernels retain their audited intrinsic-level invariants" +)] + +use super::{PackedField, PackedValue}; +use crate::ext::FpExt2Config; +use crate::Invertible; +use crate::{Fp128, Fp32, Fp64}; +use core::arch::x86_64::*; +use core::fmt; +use core::mem::transmute; +use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; + +#[inline(always)] +unsafe fn movehdup_epi32_512(x: __m512i) -> __m512i { + _mm512_castps_si512(_mm512_movehdup_ps(_mm512_castsi512_ps(x))) +} + +#[inline(always)] +unsafe fn moveldup_epi32_512(x: __m512i) -> __m512i { + _mm512_castps_si512(_mm512_moveldup_ps(_mm512_castsi512_ps(x))) +} + +/// 64×64→128 schoolbook multiply using 32×32→64 partial products. +/// Returns (hi, lo) representing the 128-bit product. +/// Adapted from plonky3's Goldilocks AVX-512 backend. +#[inline] +unsafe fn mul64_64_512(x: __m512i, y: __m512i) -> (__m512i, __m512i) { + let x_hi = movehdup_epi32_512(x); + let y_hi = movehdup_epi32_512(y); + + let mul_ll = _mm512_mul_epu32(x, y); + let mul_lh = _mm512_mul_epu32(x, y_hi); + let mul_hl = _mm512_mul_epu32(x_hi, y); + let mul_hh = _mm512_mul_epu32(x_hi, y_hi); + + let mul_ll_hi = _mm512_srli_epi64::<32>(mul_ll); + let t0 = _mm512_add_epi64(mul_hl, mul_ll_hi); + let mask32 = _mm512_set1_epi64(0xFFFF_FFFF_i64); + let t0_lo = _mm512_and_si512(t0, mask32); + let t0_hi = _mm512_srli_epi64::<32>(t0); + let t1 = _mm512_add_epi64(mul_lh, t0_lo); + let t2 = _mm512_add_epi64(mul_hh, t0_hi); + let t1_hi = _mm512_srli_epi64::<32>(t1); + let res_hi = _mm512_add_epi64(t2, t1_hi); + + let t1_lo = moveldup_epi32_512(t1); + let res_lo = _mm512_mask_blend_epi32(0b0101_0101_0101_0101, t1_lo, mul_ll); + + (res_hi, res_lo) +} + +mod fp128; +mod fp32; +mod fp64; +pub(crate) use fp128::*; +pub(crate) use fp32::*; +pub(crate) use fp64::*; diff --git a/crates/jolt-field/src/packed/ext/mod.rs b/crates/jolt-field/src/packed/ext/mod.rs new file mode 100644 index 0000000000..13f0bfb8d3 --- /dev/null +++ b/crates/jolt-field/src/packed/ext/mod.rs @@ -0,0 +1,480 @@ +//! Packed extension field types using transpose-based packing. +//! +//! A `PackedFpExt2` stores `[PF; 2]` where `PF` is the packed base field. +//! Each `PF` lane contains the corresponding coefficient of an `FpExt2` element. +//! This enables WIDTH-fold parallel arithmetic over `FpExt2` using existing SIMD +//! base-field operations. + +#![expect( + clippy::expl_impl_clone_on_copy, + reason = "manual Clone avoids adding irrelevant generic Clone bounds" +)] + +use crate::ext::{FpExt2, FpExt2Config, FpExt4, FpExt4MulBackend, FpExt8, FpExt8MulBackend}; +use crate::packed::{HasPacking, PackedField, PackedValue}; +use crate::{FieldCore, Invertible}; +use core::ops::{Add, Mul, Sub}; + +/// Packed `FpExt2` elements stored in transpose layout: `[PF; 2]`. +/// +/// If `PF` has width `W`, this represents `W` parallel `FpExt2` values. +pub struct PackedFpExt2, PF: PackedField> { + /// Degree-0 coefficient (packed across SIMD lanes). + pub c0: PF, + /// Degree-1 coefficient (packed across SIMD lanes). + pub c1: PF, + _marker: std::marker::PhantomData (F, C)>, +} + +impl, PF: PackedField> Clone + for PackedFpExt2 +{ + fn clone(&self) -> Self { + *self + } +} + +impl, PF: PackedField> Copy + for PackedFpExt2 +{ +} + +impl, PF: PackedField> std::fmt::Debug + for PackedFpExt2 +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PackedFpExt2").finish_non_exhaustive() + } +} + +impl, PF: PackedField> PackedFpExt2 { + /// Create a `PackedFpExt2` from its two packed coefficients. + #[inline] + pub fn new(c0: PF, c1: PF) -> Self { + Self { + c0, + c1, + _marker: std::marker::PhantomData, + } + } +} + +impl PackedValue for PackedFpExt2 +where + F: FieldCore + 'static, + C: FpExt2Config + 'static, + PF: PackedField, +{ + type Value = FpExt2; + const WIDTH: usize = PF::WIDTH; + + fn from_fn(mut f: G) -> Self + where + G: FnMut(usize) -> Self::Value, + { + let mut c0s = Vec::with_capacity(PF::WIDTH); + let mut c1s = Vec::with_capacity(PF::WIDTH); + for i in 0..PF::WIDTH { + let val = f(i); + c0s.push(val.coeffs[0]); + c1s.push(val.coeffs[1]); + } + Self::new(PF::from_fn(|i| c0s[i]), PF::from_fn(|i| c1s[i])) + } + + fn extract(&self, lane: usize) -> Self::Value { + FpExt2::new(self.c0.extract(lane), self.c1.extract(lane)) + } +} + +impl Add for PackedFpExt2 +where + F: FieldCore, + C: FpExt2Config, + PF: PackedField, +{ + type Output = Self; + #[inline(always)] + fn add(self, rhs: Self) -> Self { + Self::new(self.c0 + rhs.c0, self.c1 + rhs.c1) + } +} + +impl Sub for PackedFpExt2 +where + F: FieldCore, + C: FpExt2Config, + PF: PackedField, +{ + type Output = Self; + #[inline(always)] + fn sub(self, rhs: Self) -> Self { + Self::new(self.c0 - rhs.c0, self.c1 - rhs.c1) + } +} + +impl Mul for PackedFpExt2 +where + F: FieldCore, + C: FpExt2Config, + PF: PackedField, +{ + type Output = Self; + #[inline(always)] + fn mul(self, rhs: Self) -> Self { + let (c0, c1) = PF::fp_ext2_mul::(self.c0, self.c1, rhs.c0, rhs.c1); + Self::new(c0, c1) + } +} + +impl PackedField for PackedFpExt2 +where + F: FieldCore + 'static, + C: FpExt2Config + 'static, + PF: PackedField, +{ + type Scalar = FpExt2; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self::new( + PF::broadcast(value.coeffs[0]), + PF::broadcast(value.coeffs[1]), + ) + } + + #[inline(always)] + fn inverse(self) -> Option + where + Self::Scalar: Invertible, + { + let norm = self.c0 * self.c0 - C::mul_non_residue(self.c1 * self.c1, PF::broadcast); + let inv_norm = norm.inverse()?; + let zero = PF::broadcast(F::zero()); + Some(Self::new(self.c0 * inv_norm, (zero - self.c1) * inv_norm)) + } +} + +impl HasPacking for FpExt2 +where + F: FieldCore + HasPacking + 'static, + C: FpExt2Config + 'static, +{ + type Packing = PackedFpExt2; +} + +/// Packed `FpExt4` elements stored as `[PF; 4]`. +pub struct PackedFpExt4> { + /// Packed coefficients in `[1, e1, e2, e3]` order. + pub coeffs: [PF; 4], + _marker: std::marker::PhantomData F>, +} + +impl Clone for PackedFpExt4 +where + F: FieldCore, + PF: PackedField, +{ + fn clone(&self) -> Self { + *self + } +} + +impl Copy for PackedFpExt4 +where + F: FieldCore, + PF: PackedField, +{ +} + +impl std::fmt::Debug for PackedFpExt4 +where + F: FieldCore, + PF: PackedField, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PackedFpExt4").finish_non_exhaustive() + } +} + +impl PackedFpExt4 +where + F: FieldCore, + PF: PackedField, +{ + /// Create a packed value from packed ring-subfield coefficients. + #[inline] + pub fn new(coeffs: [PF; 4]) -> Self { + Self { + coeffs, + _marker: std::marker::PhantomData, + } + } + + /// Square using the packed ring-subfield backend hook. + #[inline(always)] + pub fn square(self) -> Self { + Self::new(PF::fp_ext4_square(self.coeffs)) + } +} + +impl PackedValue for PackedFpExt4 +where + F: FieldCore + 'static, + PF: PackedField, +{ + type Value = FpExt4; + const WIDTH: usize = PF::WIDTH; + + fn from_fn(mut f: G) -> Self + where + G: FnMut(usize) -> Self::Value, + { + let mut coeffs: [Vec; 4] = std::array::from_fn(|_| Vec::with_capacity(PF::WIDTH)); + for i in 0..PF::WIDTH { + let val = f(i); + for (j, coeff) in val.coeffs.into_iter().enumerate() { + coeffs[j].push(coeff); + } + } + Self::new(std::array::from_fn(|j| PF::from_fn(|i| coeffs[j][i]))) + } + + fn extract(&self, lane: usize) -> Self::Value { + FpExt4::new(std::array::from_fn(|j| self.coeffs[j].extract(lane))) + } +} + +impl Add for PackedFpExt4 +where + F: FieldCore, + PF: PackedField, +{ + type Output = Self; + #[inline(always)] + fn add(self, rhs: Self) -> Self { + let [a0, a1, a2, a3] = self.coeffs; + let [b0, b1, b2, b3] = rhs.coeffs; + Self::new([a0 + b0, a1 + b1, a2 + b2, a3 + b3]) + } +} + +impl Sub for PackedFpExt4 +where + F: FieldCore, + PF: PackedField, +{ + type Output = Self; + #[inline(always)] + fn sub(self, rhs: Self) -> Self { + let [a0, a1, a2, a3] = self.coeffs; + let [b0, b1, b2, b3] = rhs.coeffs; + Self::new([a0 - b0, a1 - b1, a2 - b2, a3 - b3]) + } +} + +impl Mul for PackedFpExt4 +where + F: FieldCore, + PF: PackedField, +{ + type Output = Self; + #[inline(always)] + fn mul(self, rhs: Self) -> Self { + Self::new(PF::fp_ext4_mul(self.coeffs, rhs.coeffs)) + } +} + +impl PackedField for PackedFpExt4 +where + F: FieldCore + FpExt4MulBackend + 'static, + PF: PackedField, +{ + type Scalar = FpExt4; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self::new(std::array::from_fn(|i| PF::broadcast(value.coeffs[i]))) + } + + #[inline(always)] + fn square(self) -> Self { + Self::new(PF::fp_ext4_square(self.coeffs)) + } + + #[inline(always)] + fn inverse(self) -> Option + where + Self::Scalar: Invertible, + { + Some(Self::new(PF::fp_ext4_inverse(self.coeffs)?)) + } +} + +impl HasPacking for FpExt4 +where + F: FieldCore + HasPacking + FpExt4MulBackend + 'static, +{ + type Packing = PackedFpExt4; +} + +/// Packed `FpExt8` elements stored in transpose layout: `[PF; 8]`. +/// +/// Each `PF` lane contains one coefficient of a degree-8 Chebyshev-basis element. +pub struct PackedFpExt8> { + /// Packed coefficients in `[1, e1, ..., e7]` order. + pub coeffs: [PF; 8], + _marker: std::marker::PhantomData F>, +} + +impl Clone for PackedFpExt8 +where + F: FieldCore, + PF: PackedField, +{ + fn clone(&self) -> Self { + *self + } +} + +impl Copy for PackedFpExt8 +where + F: FieldCore, + PF: PackedField, +{ +} + +impl std::fmt::Debug for PackedFpExt8 +where + F: FieldCore, + PF: PackedField, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PackedFpExt8").finish_non_exhaustive() + } +} + +impl PackedFpExt8 +where + F: FieldCore, + PF: PackedField, +{ + /// Create a packed value from packed ring-subfield coefficients. + #[inline] + pub fn new(coeffs: [PF; 8]) -> Self { + Self { + coeffs, + _marker: std::marker::PhantomData, + } + } +} + +impl PackedValue for PackedFpExt8 +where + F: FieldCore + 'static, + PF: PackedField, +{ + type Value = FpExt8; + const WIDTH: usize = PF::WIDTH; + + fn from_fn(mut f: G) -> Self + where + G: FnMut(usize) -> Self::Value, + { + let mut coeffs: [Vec; 8] = std::array::from_fn(|_| Vec::with_capacity(PF::WIDTH)); + for i in 0..PF::WIDTH { + let val = f(i); + for (j, coeff) in val.coeffs.into_iter().enumerate() { + coeffs[j].push(coeff); + } + } + Self::new(std::array::from_fn(|j| PF::from_fn(|i| coeffs[j][i]))) + } + + fn extract(&self, lane: usize) -> Self::Value { + FpExt8::new(std::array::from_fn(|j| self.coeffs[j].extract(lane))) + } +} + +impl Add for PackedFpExt8 +where + F: FieldCore, + PF: PackedField, +{ + type Output = Self; + #[inline(always)] + fn add(self, rhs: Self) -> Self { + Self::new(std::array::from_fn(|i| self.coeffs[i] + rhs.coeffs[i])) + } +} + +impl Sub for PackedFpExt8 +where + F: FieldCore, + PF: PackedField, +{ + type Output = Self; + #[inline(always)] + fn sub(self, rhs: Self) -> Self { + Self::new(std::array::from_fn(|i| self.coeffs[i] - rhs.coeffs[i])) + } +} + +impl Mul for PackedFpExt8 +where + F: FieldCore, + PF: PackedField, +{ + type Output = Self; + #[inline(always)] + fn mul(self, rhs: Self) -> Self { + Self::new(PF::fp_ext8_mul(self.coeffs, rhs.coeffs)) + } +} + +impl PackedField for PackedFpExt8 +where + F: FieldCore + FpExt8MulBackend + 'static, + PF: PackedField, +{ + type Scalar = FpExt8; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self::new(std::array::from_fn(|i| PF::broadcast(value.coeffs[i]))) + } + + #[inline(always)] + fn square(self) -> Self { + Self::new(PF::fp_ext8_square(self.coeffs)) + } + + #[inline(always)] + fn inverse(self) -> Option + where + Self::Scalar: Invertible, + { + // FpExt8 inversion uses Gaussian elimination — delegate lane by lane. + let mut coeffs: [Vec; 8] = std::array::from_fn(|_| Vec::with_capacity(PF::WIDTH)); + for lane in 0..PF::WIDTH { + let scalar = self.extract(lane); + let inv = scalar.inverse()?; + for (j, c) in inv.coeffs.into_iter().enumerate() { + coeffs[j].push(c); + } + } + Some(Self::new(std::array::from_fn(|j| { + PF::from_fn(|i| coeffs[j][i]) + }))) + } +} + +impl HasPacking for FpExt8 +where + F: FieldCore + HasPacking + FpExt8MulBackend + 'static, +{ + type Packing = PackedFpExt8; +} + +#[cfg(test)] +mod tests; diff --git a/crates/jolt-field/src/packed/ext/tests.rs b/crates/jolt-field/src/packed/ext/tests.rs new file mode 100644 index 0000000000..e3078541d4 --- /dev/null +++ b/crates/jolt-field/src/packed/ext/tests.rs @@ -0,0 +1,484 @@ +#![expect( + clippy::unreadable_literal, + clippy::unwrap_used, + reason = "tests assert field identities and retain copied field constants" +)] + +use super::*; +use crate::ext::{Ext2, FpExt2, FpExt4, TwoNr}; +use crate::Fp32; +use crate::Fp64; +use crate::Prime31Offset19; +use crate::Prime32Offset99; +use crate::Prime64Offset59; +use crate::RandomSampling; +use crate::RingCore; +use rand::rngs::StdRng; +use rand::SeedableRng; + +type F = Fp64<4294967197>; +type E2 = Ext2; +type R4 = FpExt4; +type PE2 = PackedFpExt2::Packing>; +type PR4 = PackedFpExt4::Packing>; +type Mersenne31 = Fp32<{ (1u32 << 31) - 1 }>; +type Generic30Offset16397 = Fp32<{ (1u32 << 30) - 16_397 }>; +type Generic31Offset61 = Fp32<{ (1u32 << 31) - 61 }>; +type Generic31Offset32787 = Fp32<{ (1u32 << 31) - 32_787 }>; +type PR4Prime31 = PackedFpExt4::Packing>; +type PR4Mersenne31 = PackedFpExt4::Packing>; +type PR4Generic30Offset16397 = + PackedFpExt4::Packing>; +type PR4Generic31Offset61 = + PackedFpExt4::Packing>; +type PR4Generic31Offset32787 = + PackedFpExt4::Packing>; +type R4Prime32 = FpExt4; +type PR4Prime32 = PackedFpExt4::Packing>; +type E2Full = FpExt2; +type PE2Full = PackedFpExt2::Packing>; + +fn fp32_ext_edge_values() -> [Fp32

; 4] { + [ + Fp32::

::from_canonical_u32(P - 1), + Fp32::

::from_canonical_u32(P - 2), + Fp32::

::from_canonical_u32((P - 1) / 2), + Fp32::

::one(), + ] +} + +fn check_packed_fp_ext4_edge() +where + PR4: PackedField>> + PackedValue>>, +{ + let values = fp32_ext_edge_values::

(); + let elem = |offset: usize| { + FpExt4::>::new(std::array::from_fn(|j| values[(offset + j) % values.len()])) + }; + let a = PR4::from_fn(elem); + let b = PR4::from_fn(|i| elem(i + 1)); + let product = a * b; + let square = a.square(); + + for lane in 0..PR4::WIDTH { + let lhs = elem(lane); + let rhs = elem(lane + 1); + assert_eq!( + product.extract(lane), + lhs * rhs, + "packed FpExt4 edge mul mismatch at lane {lane}" + ); + assert_eq!( + square.extract(lane), + lhs.square(), + "packed FpExt4 edge square mismatch at lane {lane}" + ); + } +} + +#[test] +fn packed_fp_ext2_add() { + let mut rng = StdRng::seed_from_u64(100); + let width = ::WIDTH; + let a_elems: Vec = (0..width).map(|_| E2::random(&mut rng)).collect(); + let b_elems: Vec = (0..width).map(|_| E2::random(&mut rng)).collect(); + + let pa = PE2::from_fn(|i| a_elems[i]); + let pb = PE2::from_fn(|i| b_elems[i]); + let pc = pa + pb; + + for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { + assert_eq!(pc.extract(i), *a + *b); + } +} + +#[test] +fn packed_fp_ext2_mul() { + let mut rng = StdRng::seed_from_u64(200); + let width = ::WIDTH; + let a_elems: Vec = (0..width).map(|_| E2::random(&mut rng)).collect(); + let b_elems: Vec = (0..width).map(|_| E2::random(&mut rng)).collect(); + + let pa = PE2::from_fn(|i| a_elems[i]); + let pb = PE2::from_fn(|i| b_elems[i]); + let pc = pa * pb; + + for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { + assert_eq!( + pc.extract(i), + *a * *b, + "packed FpExt2 mul mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext2_mul_full_word_fp64() { + let mut rng = StdRng::seed_from_u64(201); + let width = ::WIDTH; + let a_elems: Vec = (0..width).map(|_| E2Full::random(&mut rng)).collect(); + let b_elems: Vec = (0..width).map(|_| E2Full::random(&mut rng)).collect(); + + let pa = PE2Full::from_fn(|i| a_elems[i]); + let pb = PE2Full::from_fn(|i| b_elems[i]); + let pc = pa * pb; + + for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { + assert_eq!( + pc.extract(i), + *a * *b, + "full-word packed FpExt2 mul mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext2_broadcast() { + let val = E2::new(F::from_u64(7), F::from_u64(11)); + let packed = PE2::broadcast(val); + let width = ::WIDTH; + for i in 0..width { + assert_eq!(packed.extract(i), val); + } +} + +#[test] +fn packed_fp_ext4_add() { + let mut rng = StdRng::seed_from_u64(360); + let width = ::WIDTH; + let a_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); + let b_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); + + let pa = PR4::from_fn(|i| a_elems[i]); + let pb = PR4::from_fn(|i| b_elems[i]); + let pc = pa + pb; + + for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { + assert_eq!( + pc.extract(i), + *a + *b, + "packed FpExt4 add mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext4_sub() { + let mut rng = StdRng::seed_from_u64(361); + let width = ::WIDTH; + let a_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); + let b_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); + + let pa = PR4::from_fn(|i| a_elems[i]); + let pb = PR4::from_fn(|i| b_elems[i]); + let pc = pa - pb; + + for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { + assert_eq!( + pc.extract(i), + *a - *b, + "packed FpExt4 sub mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext4_mul() { + let mut rng = StdRng::seed_from_u64(362); + let width = ::WIDTH; + let a_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); + let b_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); + + let pa = PR4::from_fn(|i| a_elems[i]); + let pb = PR4::from_fn(|i| b_elems[i]); + let pc = pa * pb; + + for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { + assert_eq!( + pc.extract(i), + *a * *b, + "packed FpExt4 mul mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext4_mul_prime32() { + let mut rng = StdRng::seed_from_u64(365); + let width = ::WIDTH; + let a_elems: Vec = (0..width).map(|_| R4Prime32::random(&mut rng)).collect(); + let b_elems: Vec = (0..width).map(|_| R4Prime32::random(&mut rng)).collect(); + + let pa = PR4Prime32::from_fn(|i| a_elems[i]); + let pb = PR4Prime32::from_fn(|i| b_elems[i]); + let pc = pa * pb; + + for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { + assert_eq!( + pc.extract(i), + *a * *b, + "Prime32 packed FpExt4 mul mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext4_prime31_edge_lanes() { + check_packed_fp_ext4_edge::< + { crate::prime::pseudo_mersenne::PRIME31_OFFSET19_MODULUS }, + PR4Prime31, + >(); +} + +#[test] +fn packed_fp_ext4_mersenne31_edge_lanes() { + check_packed_fp_ext4_edge::<{ (1u32 << 31) - 1 }, PR4Mersenne31>(); +} + +#[test] +fn packed_fp_ext4_prime32_edge_lanes() { + check_packed_fp_ext4_edge::< + { crate::prime::pseudo_mersenne::PRIME32_OFFSET99_MODULUS }, + PR4Prime32, + >(); +} + +#[test] +fn packed_fp_ext4_generic31_edge_lanes() { + check_packed_fp_ext4_edge::<{ (1u32 << 31) - 61 }, PR4Generic31Offset61>(); +} + +#[test] +fn packed_fp_ext4_large_generic30_edge_lanes() { + check_packed_fp_ext4_edge::<{ (1u32 << 30) - 16_397 }, PR4Generic30Offset16397>(); +} + +#[test] +fn packed_fp_ext4_large_generic31_edge_lanes() { + check_packed_fp_ext4_edge::<{ (1u32 << 31) - 32_787 }, PR4Generic31Offset32787>(); +} + +#[test] +fn packed_fp_ext4_square() { + let mut rng = StdRng::seed_from_u64(363); + let width = ::WIDTH; + let elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); + + let packed = PR4::from_fn(|i| elems[i]); + let squared = packed.square(); + + for (i, elem) in elems.iter().enumerate() { + assert_eq!( + squared.extract(i), + elem.square(), + "packed FpExt4 square mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext4_square_prime32() { + let mut rng = StdRng::seed_from_u64(366); + let width = ::WIDTH; + let elems: Vec = (0..width).map(|_| R4Prime32::random(&mut rng)).collect(); + + let packed = PR4Prime32::from_fn(|i| elems[i]); + let squared = packed.square(); + + for (i, elem) in elems.iter().enumerate() { + assert_eq!( + squared.extract(i), + elem.square(), + "Prime32 packed FpExt4 square mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext4_square_mersenne31() { + let mut rng = StdRng::seed_from_u64(367); + type R4M31 = FpExt4; + let width = ::WIDTH; + let elems: Vec = (0..width).map(|_| R4M31::random(&mut rng)).collect(); + + let packed = PR4Mersenne31::from_fn(|i| elems[i]); + let squared = packed.square(); + + for (i, elem) in elems.iter().enumerate() { + assert_eq!( + squared.extract(i), + elem.square(), + "Mersenne31 packed FpExt4 square mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext4_inverse() { + let mut rng = StdRng::seed_from_u64(367); + let width = ::WIDTH; + let elems: Vec = (0..width) + .map(|_| { + let x = R4::random(&mut rng); + if x.is_zero() { + R4::one() + } else { + x + } + }) + .collect(); + + let packed = PR4::from_fn(|i| elems[i]); + let inverted = packed.inverse().unwrap(); + + for (i, elem) in elems.iter().enumerate() { + assert_eq!( + inverted.extract(i), + elem.inverse().unwrap(), + "packed FpExt4 inverse mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext4_broadcast() { + let val = R4::new([ + F::from_u64(7), + F::from_u64(11), + F::from_u64(13), + F::from_u64(17), + ]); + let packed = PR4::broadcast(val); + let width = ::WIDTH; + for i in 0..width { + assert_eq!(packed.extract(i), val); + } +} + +#[test] +fn packed_fp_ext4_pack_unpack() { + let mut rng = StdRng::seed_from_u64(364); + let width = ::WIDTH; + let elems: Vec = (0..width * 3).map(|_| R4::random(&mut rng)).collect(); + + let packed = PR4::pack_slice(&elems); + let unpacked = PR4::unpack_slice(&packed); + + assert_eq!(elems, unpacked); +} + +#[test] +fn pack_unpack_roundtrip_fp_ext2() { + let mut rng = StdRng::seed_from_u64(400); + let width = ::WIDTH; + let elems: Vec = (0..width * 3).map(|_| E2::random(&mut rng)).collect(); + + let packed = PE2::pack_slice(&elems); + let unpacked = PE2::unpack_slice(&packed); + + assert_eq!(elems, unpacked); +} + +type R8Fp64 = FpExt8; +type PR8Fp64 = PackedFpExt8::Packing>; +type R8Prime31 = FpExt8; +type PR8Prime31 = PackedFpExt8::Packing>; +type R8Prime32 = FpExt8; +type PR8Prime32 = PackedFpExt8::Packing>; + +#[test] +fn packed_fp_ext8_mul_fp64() { + let mut rng = StdRng::seed_from_u64(500); + let width = ::WIDTH; + let a_elems: Vec = (0..width).map(|_| R8Fp64::random(&mut rng)).collect(); + let b_elems: Vec = (0..width).map(|_| R8Fp64::random(&mut rng)).collect(); + + let pa = PR8Fp64::from_fn(|i| a_elems[i]); + let pb = PR8Fp64::from_fn(|i| b_elems[i]); + let pc = pa * pb; + + for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { + assert_eq!( + pc.extract(i), + *a * *b, + "packed FpExt8 mul mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext8_mul_prime31() { + let mut rng = StdRng::seed_from_u64(501); + let width = ::WIDTH; + let a_elems: Vec = (0..width).map(|_| R8Prime31::random(&mut rng)).collect(); + let b_elems: Vec = (0..width).map(|_| R8Prime31::random(&mut rng)).collect(); + + let pa = PR8Prime31::from_fn(|i| a_elems[i]); + let pb = PR8Prime31::from_fn(|i| b_elems[i]); + let pc = pa * pb; + + for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { + assert_eq!( + pc.extract(i), + *a * *b, + "packed FpExt8 mul mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext8_mul_prime32() { + let mut rng = StdRng::seed_from_u64(502); + let width = ::WIDTH; + let a_elems: Vec = (0..width).map(|_| R8Prime32::random(&mut rng)).collect(); + let b_elems: Vec = (0..width).map(|_| R8Prime32::random(&mut rng)).collect(); + + let pa = PR8Prime32::from_fn(|i| a_elems[i]); + let pb = PR8Prime32::from_fn(|i| b_elems[i]); + let pc = pa * pb; + + for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { + assert_eq!( + pc.extract(i), + *a * *b, + "packed FpExt8 mul mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext8_square() { + let mut rng = StdRng::seed_from_u64(504); + let width = ::WIDTH; + let a_elems: Vec = (0..width).map(|_| R8Prime31::random(&mut rng)).collect(); + + let pa = PR8Prime31::from_fn(|i| a_elems[i]); + let sq = pa.square(); + + for (i, a) in a_elems.iter().enumerate() { + assert_eq!( + sq.extract(i), + a.square(), + "packed FpExt8 square mismatch at lane {i}" + ); + } +} + +#[test] +fn packed_fp_ext8_broadcast() { + let val = R8Fp64::new([ + F::from_u64(1), + F::from_u64(2), + F::from_u64(3), + F::from_u64(4), + F::from_u64(5), + F::from_u64(6), + F::from_u64(7), + F::from_u64(8), + ]); + let packed = PR8Fp64::broadcast(val); + let width = ::WIDTH; + for i in 0..width { + assert_eq!(packed.extract(i), val); + } +} diff --git a/crates/jolt-field/src/packed/mod.rs b/crates/jolt-field/src/packed/mod.rs new file mode 100644 index 0000000000..748d995b92 --- /dev/null +++ b/crates/jolt-field/src/packed/mod.rs @@ -0,0 +1,418 @@ +//! Packed field abstractions and architecture-specific SIMD backends. + +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx2", + not(all(target_feature = "avx512f", target_feature = "avx512dq")) +))] +pub(crate) mod avx2; +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx512f", + target_feature = "avx512dq" +))] +pub(crate) mod avx512; +pub(crate) mod ext; +#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] +pub(crate) mod neon; + +pub use ext::{PackedFpExt2, PackedFpExt4, PackedFpExt8}; + +use crate::ext::{fp_ext8_mul_schedule, fp_ext8_square_schedule, FpExt2Config}; +use crate::{FieldCore, Fp128, Fp32, Fp64, Invertible}; +use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; +use num_traits::Zero; + +/// Array-like packed values over a scalar type. +pub trait PackedValue: 'static + Copy + Send + Sync { + /// Scalar value type carried by each lane. + type Value: 'static + Copy + Send + Sync; + + /// Number of scalar lanes. + const WIDTH: usize; + + /// Build from a lane generator. + fn from_fn(f: F) -> Self + where + F: FnMut(usize) -> Self::Value; + + /// Extract one lane. + fn extract(&self, lane: usize) -> Self::Value; + + /// Pack a scalar slice into packed values. + /// + /// # Panics + /// + /// Panics if the length is not divisible by `WIDTH`. + #[inline] + fn pack_slice(buf: &[Self::Value]) -> Vec { + assert!( + buf.len() % Self::WIDTH == 0, + "slice length {} must be divisible by WIDTH {}", + buf.len(), + Self::WIDTH + ); + buf.chunks_exact(Self::WIDTH) + .map(|chunk| Self::from_fn(|i| chunk[i])) + .collect() + } + + /// Packed prefix + scalar suffix split. + #[inline] + fn pack_slice_with_suffix(buf: &[Self::Value]) -> (Vec, &[Self::Value]) { + let split = buf.len() - (buf.len() % Self::WIDTH); + let (packed, suffix) = buf.split_at(split); + (Self::pack_slice(packed), suffix) + } + + /// Unpack packed values into a flat scalar vector. + #[inline] + fn unpack_slice(buf: &[Self]) -> Vec { + let mut out = Vec::with_capacity(buf.len() * Self::WIDTH); + for packed in buf { + for lane in 0..Self::WIDTH { + out.push(packed.extract(lane)); + } + } + out + } +} + +/// Packed arithmetic over a scalar field. +pub trait PackedField: + PackedValue + Add + Sub + Mul +{ + /// Scalar field type. + type Scalar: FieldCore; + + /// Broadcast one scalar across all lanes. + fn broadcast(value: Self::Scalar) -> Self; + + /// Square one packed value. + #[inline(always)] + fn square(self) -> Self { + self * self + } + + /// Invert one packed value lane-wise. + #[inline] + fn inverse(self) -> Option + where + Self::Scalar: Invertible, + { + let mut inverses = Vec::with_capacity(Self::WIDTH); + for lane in 0..Self::WIDTH { + inverses.push(self.extract(lane).inverse()?); + } + Some(Self::from_fn(|i| inverses[i])) + } + + /// Backend hook for multiplying two packed `FpExt2` values in coefficient form. + #[inline(always)] + fn fp_ext2_mul(a0: Self, a1: Self, b0: Self, b1: Self) -> (Self, Self) + where + C: FpExt2Config, + { + let v0 = a0 * b0; + let v1 = a1 * b1; + let cross = (a0 + a1) * (b0 + b1); + ( + v0 + C::mul_non_residue(v1, Self::broadcast), + cross - v0 - v1, + ) + } + + /// Backend hook for multiplying packed ring-subfield quartics. + #[inline(always)] + fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { + let [a0, a1, a2, a3] = a; + let [b0, b1, b2, b3] = b; + let tail0 = a1 * b1 + a2 * b2 + a3 * b3; + [ + a0 * b0 + tail0 + tail0, + a0 * b1 + a1 * b0 + a1 * b2 + a2 * b1 + a2 * b3 + a3 * b2, + a0 * b2 + a2 * b0 + a1 * b1 + a1 * b3 + a3 * b1 - a3 * b3, + a0 * b3 + a3 * b0 + a1 * b2 + a2 * b1 - a2 * b3 - a3 * b2, + ] + } + + /// Backend hook for squaring packed ring-subfield quartics. + #[inline(always)] + fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { + let [a0, a1, a2, a3] = a; + let x0 = a0; + let x1 = a2; + let y0 = a1 - a3; + let y1 = a3; + + let x0x1 = x0 * x1; + let y0y1 = y0 * y1; + let x1_square = x1 * x1; + let y1_square = y1 * y1; + let aa = (x0 * x0 + x1_square + x1_square, x0x1 + x0x1); + let bb = (y0 * y0 + y1_square + y1_square, y0y1 + y0y1); + + let v0 = x0 * y0; + let v1 = x1 * y1; + let ab = (v0 + v1 + v1, (x0 + x1) * (y0 + y1) - v0 - v1); + let constant = (bb.0 + bb.0 + bb.1 + bb.1, bb.0 + bb.1 + bb.1); + let coeff_e1 = (ab.0 + ab.0, ab.1 + ab.1); + + [ + aa.0 + constant.0, + coeff_e1.0 + coeff_e1.1, + aa.1 + constant.1, + coeff_e1.1, + ] + } + + /// Backend hook for inverting packed ring-subfield quartics. + #[inline(always)] + fn fp_ext4_inverse(a: [Self; 4]) -> Option<[Self; 4]> + where + Self::Scalar: Invertible, + { + let zero = Self::broadcast(Self::Scalar::zero()); + let [a0, a1, a2, a3] = a; + let x0 = a0; + let x1 = a2; + let y0 = a1 - a3; + let y1 = a3; + + let x0x1 = x0 * x1; + let y0y1 = y0 * y1; + let x1_square = x1 * x1; + let y1_square = y1 * y1; + let aa = (x0 * x0 + x1_square + x1_square, x0x1 + x0x1); + let bb = (y0 * y0 + y1_square + y1_square, y0y1 + y0y1); + let nr_bb = (bb.0 + bb.0 + bb.1 + bb.1, bb.0 + bb.1 + bb.1); + let norm = (aa.0 - nr_bb.0, aa.1 - nr_bb.1); + let inv_norm_base = (norm.0 * norm.0 - (norm.1 * norm.1 + norm.1 * norm.1)).inverse()?; + let inv_norm = (norm.0 * inv_norm_base, (zero - norm.1) * inv_norm_base); + + let v0 = x0 * inv_norm.0; + let v1 = x1 * inv_norm.1; + let constant = ( + v0 + v1 + v1, + (x0 + x1) * (inv_norm.0 + inv_norm.1) - v0 - v1, + ); + let neg_y0 = zero - y0; + let neg_y1 = zero - y1; + let w0 = neg_y0 * inv_norm.0; + let w1 = neg_y1 * inv_norm.1; + let e1_coeff = ( + w0 + w1 + w1, + (neg_y0 + neg_y1) * (inv_norm.0 + inv_norm.1) - w0 - w1, + ); + + Some([constant.0, e1_coeff.0 + e1_coeff.1, constant.1, e1_coeff.1]) + } + + /// Backend hook for multiplying packed ring-subfield degree-8 elements. + #[inline(always)] + fn fp_ext8_mul(a: [Self; 8], b: [Self; 8]) -> [Self; 8] { + fp_ext8_mul_schedule( + a, + b, + Self::broadcast(Self::Scalar::zero()), + |x, y| x + y, + |x, y| x - y, + |x, y| x * y, + ) + } + + /// Backend hook for squaring packed ring-subfield degree-8 elements. + #[inline(always)] + fn fp_ext8_square(a: [Self; 8]) -> [Self; 8] { + fp_ext8_square_schedule( + a, + Self::broadcast(Self::Scalar::zero()), + |x, y| x + y, + |x, y| x - y, + |x, y| x * y, + ) + } +} + +/// Scalar fallback packed type with one lane. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[repr(transparent)] +pub struct NoPacking(pub [T; 1]); + +impl PackedValue for NoPacking +where + T: 'static + Copy + Send + Sync, +{ + type Value = T; + const WIDTH: usize = 1; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Value, + { + Self([f(0)]) + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Value { + debug_assert_eq!(lane, 0); + self.0[0] + } +} + +impl Add for NoPacking { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self([self.0[0] + rhs.0[0]]) + } +} + +impl Sub for NoPacking { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self([self.0[0] - rhs.0[0]]) + } +} + +impl Mul for NoPacking { + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self { + Self([self.0[0] * rhs.0[0]]) + } +} + +impl AddAssign for NoPacking { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for NoPacking { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for NoPacking { + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl PackedField for NoPacking { + type Scalar = T; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self([value]) + } +} + +/// Scalar field -> packed field association. +pub trait HasPacking: FieldCore { + /// Packed representation for this scalar field. + type Packing: PackedField; +} + +/// Selected packed backend for `Fp128`. +#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] +pub type Fp128Packing = neon::PackedFp128Neon

; + +/// Selected packed backend for `Fp128`. +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx512f", + target_feature = "avx512dq" +))] +pub type Fp128Packing = avx512::PackedFp128Avx512

; + +/// Selected packed backend for `Fp128`. +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx2", + not(all(target_feature = "avx512f", target_feature = "avx512dq")) +))] +pub type Fp128Packing = avx2::PackedFp128Avx2

; + +/// Selected packed backend for `Fp128`. +#[cfg(not(any( + all(target_arch = "aarch64", target_feature = "neon"), + all(target_arch = "x86_64", target_feature = "avx2") +)))] +pub type Fp128Packing = NoPacking>; + +impl HasPacking for Fp128

{ + type Packing = Fp128Packing

; +} + +/// Selected packed backend for `Fp32`. +#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] +pub type Fp32Packing = neon::PackedFp32Neon

; + +/// Selected packed backend for `Fp32`. +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx512f", + target_feature = "avx512dq" +))] +pub type Fp32Packing = avx512::PackedFp32Avx512

; + +/// Selected packed backend for `Fp32`. +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx2", + not(all(target_feature = "avx512f", target_feature = "avx512dq")) +))] +pub type Fp32Packing = avx2::PackedFp32Avx2

; + +/// Selected packed backend for `Fp32`. +#[cfg(not(any( + all(target_arch = "aarch64", target_feature = "neon"), + all(target_arch = "x86_64", target_feature = "avx2") +)))] +pub type Fp32Packing = NoPacking>; + +impl HasPacking for Fp32

{ + type Packing = Fp32Packing

; +} + +/// Selected packed backend for `Fp64`. +#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] +pub type Fp64Packing = neon::PackedFp64Neon

; + +/// Selected packed backend for `Fp64`. +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx512f", + target_feature = "avx512dq" +))] +pub type Fp64Packing = avx512::PackedFp64Avx512

; + +/// Selected packed backend for `Fp64`. +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx2", + not(all(target_feature = "avx512f", target_feature = "avx512dq")) +))] +pub type Fp64Packing = avx2::PackedFp64Avx2

; + +/// Selected packed backend for `Fp64`. +#[cfg(not(any( + all(target_arch = "aarch64", target_feature = "neon"), + all(target_arch = "x86_64", target_feature = "avx2") +)))] +pub type Fp64Packing = NoPacking>; + +impl HasPacking for Fp64

{ + type Packing = Fp64Packing

; +} + +#[cfg(test)] +mod tests; diff --git a/crates/jolt-field/src/packed/neon/fp128.rs b/crates/jolt-field/src/packed/neon/fp128.rs new file mode 100644 index 0000000000..a7f1ba0b99 --- /dev/null +++ b/crates/jolt-field/src/packed/neon/fp128.rs @@ -0,0 +1,314 @@ +use super::*; + +/// Number of packed `Fp128` lanes in this backend. +pub(crate) const FP128_WIDTH: usize = 2; + +/// True SoA layout for two packed `Fp128` lanes. +/// +/// `lo = [lane0.lo, lane1.lo]` +/// `hi = [lane0.hi, lane1.hi]` +#[derive(Clone, Copy)] +pub struct PackedFp128Neon { + lo: [u64; 2], + hi: [u64; 2], +} +#[inline(always)] +const fn modulus_lo() -> u64 { + P as u64 +} + +#[inline(always)] +const fn modulus_hi() -> u64 { + (P >> 64) as u64 +} + +use crate::prime::util::{is_pow2_u64, log2_pow2_u64}; +impl PackedFp128Neon

{ + const C: u128 = { + let c = 0u128.wrapping_sub(P); + assert!(P != 0, "modulus must be nonzero"); + assert!(P & 1 == 1, "modulus must be odd"); + assert!(c < (1u128 << 64), "P must be 2^128 - c with c < 2^64"); + assert!( + c * (c + 1) < P, + "C(C+1) < P required for fused canonicalize" + ); + c + }; + const C_LO: u64 = Self::C as u64; + const C_SHIFT_KIND: i8 = { + let c = Self::C_LO; + if c > 1 && is_pow2_u64(c - 1) { + 1 + } else if c == u64::MAX || is_pow2_u64(c + 1) { + -1 + } else { + 0 + } + }; + const C_SHIFT: u32 = { + let c = Self::C_LO; + if Self::C_SHIFT_KIND == 1 { + log2_pow2_u64(c - 1) + } else if Self::C_SHIFT_KIND == -1 { + if c == u64::MAX { + 64 + } else { + log2_pow2_u64(c + 1) + } + } else { + 0 + } + }; + + #[inline(always)] + fn mul_wide_u64(a: u64, b: u64) -> (u64, u64) { + let prod = (a as u128) * (b as u128); + (prod as u64, (prod >> 64) as u64) + } + + #[inline(always)] + fn mul_c_wide(x: u64) -> (u64, u64) { + if Self::C_SHIFT_KIND == 1 { + let v = ((x as u128) << Self::C_SHIFT) + x as u128; + (v as u64, (v >> 64) as u64) + } else if Self::C_SHIFT_KIND == -1 { + let v = ((x as u128) << Self::C_SHIFT) - x as u128; + (v as u64, (v >> 64) as u64) + } else { + Self::mul_wide_u64(Self::C_LO, x) + } + } + + #[inline(always)] + fn fold2_canonicalize(t0: u64, t1: u64, t2: u64) -> (u64, u64) { + let (ct2_lo, ct2_hi) = Self::mul_c_wide(t2); + + let (s0, carry0) = t0.overflowing_add(ct2_lo); + let (s1a, carry1a) = t1.overflowing_add(ct2_hi); + let (s1, carry1b) = s1a.overflowing_add(carry0 as u64); + let overflow = carry1a | carry1b; + + let (r0, carry2) = s0.overflowing_add(Self::C_LO); + let (r1, carry3) = s1.overflowing_add(carry2 as u64); + + if overflow | carry3 { + (r0, r1) + } else { + (s0, s1) + } + } + + #[inline(always)] + fn mul_raw_lane(a0: u64, a1: u64, b0: u64, b1: u64) -> (u64, u64) { + let (p00_lo, p00_hi) = Self::mul_wide_u64(a0, b0); + let (p01_lo, p01_hi) = Self::mul_wide_u64(a0, b1); + let (p10_lo, p10_hi) = Self::mul_wide_u64(a1, b0); + let (p11_lo, p11_hi) = Self::mul_wide_u64(a1, b1); + + let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; + let r0 = p00_lo; + let r1 = row1 as u64; + let carry1 = (row1 >> 64) as u64; + + let row2 = p01_hi as u128 + p10_hi as u128 + p11_lo as u128 + carry1 as u128; + let r2 = row2 as u64; + let carry2 = (row2 >> 64) as u64; + + let row3 = p11_hi as u128 + carry2 as u128; + let r3 = row3 as u64; + debug_assert_eq!(row3 >> 64, 0); + + let (cr2_lo, cr2_hi) = Self::mul_c_wide(r2); + let (cr3_lo, cr3_hi) = Self::mul_c_wide(r3); + + let t0_sum = r0 as u128 + cr2_lo as u128; + let t0 = t0_sum as u64; + let carryf = (t0_sum >> 64) as u64; + + let t1_sum = r1 as u128 + cr2_hi as u128 + cr3_lo as u128 + carryf as u128; + let t1 = t1_sum as u64; + + let t2_sum = cr3_hi as u128 + (t1_sum >> 64); + let t2 = t2_sum as u64; + debug_assert_eq!(t2_sum >> 64, 0); + + Self::fold2_canonicalize(t0, t1, t2) + } +} + +impl Default for PackedFp128Neon

{ + #[inline] + fn default() -> Self { + Self::broadcast(Fp128::zero()) + } +} + +impl fmt::Debug for PackedFp128Neon

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("PackedFp128Neon") + .field(&[self.extract(0), self.extract(1)]) + .finish() + } +} + +impl PartialEq for PackedFp128Neon

{ + #[inline] + fn eq(&self, other: &Self) -> bool { + self.extract(0) == other.extract(0) && self.extract(1) == other.extract(1) + } +} + +impl Eq for PackedFp128Neon

{} + +impl PackedValue for PackedFp128Neon

{ + type Value = Fp128

; + const WIDTH: usize = FP128_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Value, + { + let x0 = f(0); + let x1 = f(1); + Self { + lo: [x0.0[0], x1.0[0]], + hi: [x0.0[1], x1.0[1]], + } + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Value { + debug_assert!(lane < FP128_WIDTH); + Fp128([self.lo[lane], self.hi[lane]]) + } +} + +impl Add for PackedFp128Neon

{ + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + let lo_a = to_vec(self.lo); + let hi_a = to_vec(self.hi); + let lo_b = to_vec(rhs.lo); + let hi_b = to_vec(rhs.hi); + + let (out_lo, out_hi) = unsafe { + let c_vec = vdupq_n_u64(Self::C_LO); + + // s = a + b (128-bit, two lanes). + // Carry propagation uses raw comparison masks with sub: subtracting + // a lane of all-1s is equivalent to adding 1 in wrapping arithmetic. + let sum_lo = vaddq_u64(lo_a, lo_b); + let carry_lo = vcltq_u64(sum_lo, lo_a); + + let hi_tmp = vaddq_u64(hi_a, hi_b); + let carry_hi1 = vcltq_u64(hi_tmp, hi_a); + let sum_hi = vsubq_u64(hi_tmp, carry_lo); + let carry_hi2 = vcltq_u64(sum_hi, hi_tmp); + let overflow = vorrq_u64(carry_hi1, carry_hi2); + + // t = s + C. Since p = 2^128 - C, this is s - p (mod 2^128). + // If s + C >= 2^128 then s >= p, so the reduced value t is correct. + let t_lo = vaddq_u64(sum_lo, c_vec); + let carry_c = vcltq_u64(t_lo, sum_lo); + let t_hi = vsubq_u64(sum_hi, carry_c); + let carry_t = vcltq_u64(t_hi, sum_hi); + + let use_reduced = vorrq_u64(overflow, carry_t); + let out_lo = vbslq_u64(use_reduced, t_lo, sum_lo); + let out_hi = vbslq_u64(use_reduced, t_hi, sum_hi); + (out_lo, out_hi) + }; + + Self { + lo: from_vec(out_lo), + hi: from_vec(out_hi), + } + } +} + +impl Sub for PackedFp128Neon

{ + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + let lo_a = to_vec(self.lo); + let hi_a = to_vec(self.hi); + let lo_b = to_vec(rhs.lo); + let hi_b = to_vec(rhs.hi); + + let (out_lo, out_hi) = unsafe { + let p_lo = vdupq_n_u64(modulus_lo::

()); + let p_hi = vdupq_n_u64(modulus_hi::

()); + + let diff_lo = vsubq_u64(lo_a, lo_b); + let borrow_lo = mask_to_bit(vcltq_u64(lo_a, lo_b)); + + let diff_hi_tmp = vsubq_u64(hi_a, hi_b); + let borrow_hi1 = vcltq_u64(hi_a, hi_b); + let diff_hi = vsubq_u64(diff_hi_tmp, borrow_lo); + let borrow_hi2 = vcltq_u64(diff_hi_tmp, borrow_lo); + let borrow_128 = vorrq_u64(borrow_hi1, borrow_hi2); + + let corr_lo = vaddq_u64(diff_lo, p_lo); + let carry_lo = mask_to_bit(vcltq_u64(corr_lo, diff_lo)); + + let corr_hi_tmp = vaddq_u64(diff_hi, p_hi); + let corr_hi = vaddq_u64(corr_hi_tmp, carry_lo); + + let out_lo = vbslq_u64(borrow_128, corr_lo, diff_lo); + let out_hi = vbslq_u64(borrow_128, corr_hi, diff_hi); + (out_lo, out_hi) + }; + + Self { + lo: from_vec(out_lo), + hi: from_vec(out_hi), + } + } +} + +impl Mul for PackedFp128Neon

{ + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self { + let (o0_lo, o0_hi) = Self::mul_raw_lane(self.lo[0], self.hi[0], rhs.lo[0], rhs.hi[0]); + let (o1_lo, o1_hi) = Self::mul_raw_lane(self.lo[1], self.hi[1], rhs.lo[1], rhs.hi[1]); + + Self { + lo: [o0_lo, o1_lo], + hi: [o0_hi, o1_hi], + } + } +} + +impl AddAssign for PackedFp128Neon

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for PackedFp128Neon

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for PackedFp128Neon

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl PackedField for PackedFp128Neon

{ + type Scalar = Fp128

; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self::from_fn(|_| value) + } +} diff --git a/crates/jolt-field/src/packed/neon/fp32.rs b/crates/jolt-field/src/packed/neon/fp32.rs new file mode 100644 index 0000000000..454e39731f --- /dev/null +++ b/crates/jolt-field/src/packed/neon/fp32.rs @@ -0,0 +1,824 @@ +use super::*; + +/// Number of packed `Fp32` lanes. +pub(crate) const FP32_WIDTH: usize = 4; + +/// NEON packed `Fp32` backend: 4 lanes in `uint32x4_t`. +#[derive(Clone, Copy)] +pub struct PackedFp32Neon { + vals: [u32; 4], +} + +#[inline(always)] +fn to_vec32(x: [u32; 4]) -> uint32x4_t { + unsafe { transmute::<[u32; 4], uint32x4_t>(x) } +} + +#[inline(always)] +fn from_vec32(v: uint32x4_t) -> [u32; 4] { + unsafe { transmute::(v) } +} + +impl PackedFp32Neon

{ + const BITS: u32 = 32 - P.leading_zeros(); + + const C: u32 = { + let c = if Self::BITS == 32 { + 0u32.wrapping_sub(P) + } else { + (1u32 << Self::BITS) - P + }; + assert!(P != 0, "modulus must be nonzero"); + assert!(P & 1 == 1, "modulus must be odd"); + assert!( + (c as u64) * (c as u64 + 1) < P as u64, + "C(C+1) < P required for fused canonicalize" + ); + c + }; + + const MASK_U64: u64 = if Self::BITS == 32 { + u32::MAX as u64 + } else { + (1u64 << Self::BITS) - 1 + }; + + const TWO_FOLD_FOUR_PRODUCT_OK: bool = { + let c = Self::C as u64; + 4 * c * c + 3 * c <= (1u64 << Self::BITS) + }; + + #[inline(always)] + fn to_vec(self) -> uint32x4_t { + to_vec32(self.vals) + } + + #[inline(always)] + fn from_vec(v: uint32x4_t) -> Self { + Self { + vals: from_vec32(v), + } + } + + #[inline(always)] + fn add_vec(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t { + unsafe { + let p = vdupq_n_u32(P); + if Self::BITS <= 31 { + let t = vaddq_u32(a, b); + vminq_u32(t, vsubq_u32(t, p)) + } else { + let c = vdupq_n_u32(Self::C); + let t = vaddq_u32(a, b); + let overflow = vcltq_u32(t, a); + let folded = vaddq_u32(t, vandq_u32(overflow, c)); + vminq_u32(folded, vsubq_u32(folded, p)) + } + } + } + + #[inline(always)] + fn sub_vec(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t { + unsafe { + let p = vdupq_n_u32(P); + if Self::BITS <= 31 { + let t = vsubq_u32(a, b); + vminq_u32(t, vaddq_u32(t, p)) + } else { + let t = vsubq_u32(a, b); + let underflow = vcltq_u32(a, b); + vsubq_u32(t, vandq_u32(underflow, vdupq_n_u32(Self::C))) + } + } + } + + #[inline(always)] + fn mul_vec(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t { + unsafe { + if Self::BITS == 31 { + return if Self::C == 1 { + Self::mul_mersenne31_vec(a, b) + } else { + Self::mul_pmersenne31_vec(a, b) + }; + } + let prod_lo = vmull_u32(vget_low_u32(a), vget_low_u32(b)); + let prod_hi = vmull_high_u32(a, b); + Self::solinas_reduce(prod_lo, prod_hi) + } + } + + #[inline(always)] + unsafe fn mul_mersenne31_vec(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t { + unsafe { + let p = vdupq_n_u32(P); + let prod_hi31 = vreinterpretq_u32_s32(vqdmulhq_s32( + vreinterpretq_s32_u32(a), + vreinterpretq_s32_u32(b), + )); + let prod_lo32 = vmulq_u32(a, b); + let folded = vmlsq_u32(prod_lo32, prod_hi31, p); + vminq_u32(folded, vsubq_u32(folded, p)) + } + } + + /// Packed multiply for 31-bit pseudo-Mersenne primes `P = 2^31 - C` + /// (`BITS == 31`, `C > 1`), reducing entirely in 32-bit lanes. + /// + /// This generalises the `C == 1` Mersenne kernel + /// ([`Self::mul_mersenne31_vec`]) to any small `C` admitted by the + /// `Fp32

` invariant `C(C+1) < P`, replacing the 64-bit-widening + /// [`Self::solinas_reduce`] path. It keeps all four lanes in `uint32x4_t` + /// and uses two `vqdmulhq_s32` high-multiplies (the same instruction the + /// Mersenne path uses) to extract Solinas fold high words without ever + /// forming a 64-bit intermediate. + /// + /// # Correctness (exact, no estimation) + /// + /// Precondition: lanes `a, b ∈ [0, P)` (the `Add`/`Sub`/`Mul` impls all + /// return canonical lanes, so every `mul_vec` input is canonical). Write + /// `z = a*b`, so `0 ≤ z ≤ (P-1)^2 < 2^62`. All steps are exact integer + /// identities; the only inequality used is the compile-time invariant + /// `C(C+1) < P`, which gives `C^2 < P < 2^31` and `C(C+2) < 2^31`. + /// + /// 1. `h = sqdmulh(a,b) = floor(2z / 2^32) = floor(z / 2^31)`, exact + /// because `2z < 2^63` (no saturation), and `h ∈ [0, 2^31)`. + /// 2. `z_lo31 = (z mod 2^32) & (2^31-1) = z mod 2^31`, so + /// `z = h·2^31 + z_lo31` exactly. + /// 3. Since `2^31 = P + C ≡ C (mod P)`, `z ≡ C·h + z_lo31 =: t (mod P)`. + /// 4. Fold `t`: `hh = sqdmulh(h, C) = floor(C·h / 2^31) ∈ [0, C)` (exact, + /// `2·h·C < 2^63`), and `ch_lo31 = (C·h mod 2^32) & (2^31-1) + /// = C·h mod 2^31`, so `C·h = hh·2^31 + ch_lo31`. + /// 5. `s = ch_lo31 + z_lo31 < 2^32` (sum of two sub-`2^31` values, no u32 + /// overflow). With `hp = hh + (s >> 31)` and `lo31p = s & (2^31-1)`, + /// `t = hh·2^31 + s = hp·2^31 + lo31p`, and `hp ≤ (C-1) + 1 = C`. + /// 6. Fold again: `t ≡ C·hp + lo31p =: t' (mod P)`. Since `hp ≤ C`, + /// `C·hp ≤ C^2 < 2^31` (so `vmulq_u32(hp, C)` is exact, no wrap), and + /// `t' = C·hp + lo31p < C^2 + 2^31 < 2^32` (no u32 overflow). + /// 7. `t' < C^2 + 2^31 ≤ 2P` because `C(C+2) < 2^31 ⇔ C^2 + 2^31 < 2P`. + /// Thus `t' ≡ z (mod P)` and `t' ∈ [0, 2P)`, i.e. `t' ∈ {r, r+P}` for + /// `r = z mod P`. The final `vminq_u32(t', t' - P)` (wrapping sub) + /// returns the canonical `r ∈ [0, P)`. + #[inline(always)] + unsafe fn mul_pmersenne31_vec(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t { + unsafe { + let mask31 = vdupq_n_u32((1u32 << 31) - 1); + let cvec = vdupq_n_u32(Self::C); + let p = vdupq_n_u32(P); + + // Step 1-2: high/low split of z = a*b. + let h = vreinterpretq_u32_s32(vqdmulhq_s32( + vreinterpretq_s32_u32(a), + vreinterpretq_s32_u32(b), + )); + let z_lo31 = vandq_u32(vmulq_u32(a, b), mask31); + + // Step 3-5: first Solinas fold t = C*h + z_lo31 = hp*2^31 + lo31p. + let hh = vreinterpretq_u32_s32(vqdmulhq_s32( + vreinterpretq_s32_u32(h), + vreinterpretq_s32_u32(cvec), + )); + let ch_lo31 = vandq_u32(vmulq_u32(h, cvec), mask31); + let s = vaddq_u32(ch_lo31, z_lo31); + let hp = vaddq_u32(hh, vshrq_n_u32::<31>(s)); + let lo31p = vandq_u32(s, mask31); + + // Step 6-7: second fold t' = C*hp + lo31p in [0, 2P), canonicalize. + let tprime = vaddq_u32(vmulq_u32(hp, cvec), lo31p); + vminq_u32(tprime, vsubq_u32(tprime, p)) + } + } + + #[inline(always)] + fn add_u64_with_carry( + sum: uint64x2_t, + rhs: uint64x2_t, + carry: uint64x2_t, + ) -> (uint64x2_t, uint64x2_t) { + unsafe { + let next = vaddq_u64(sum, rhs); + let overflow = vcltq_u64(next, sum); + (next, vaddq_u64(carry, mask_to_bit(overflow))) + } + } + + #[inline(always)] + fn carry_correction(carry: uint64x2_t) -> uint64x2_t { + unsafe { vmull_u32(vmovn_u64(carry), vdup_n_u32(Fp32::

::SHIFT64_MOD_P)) } + } + + #[inline(always)] + fn dot_product_4_vec(a: [uint32x4_t; 4], b: [uint32x4_t; 4]) -> uint32x4_t { + unsafe { + let mut sum_lo = vmull_u32(vget_low_u32(a[0]), vget_low_u32(b[0])); + let mut sum_hi = vmull_high_u32(a[0], b[0]); + + if Self::BITS <= 31 { + sum_lo = vaddq_u64(sum_lo, vmull_u32(vget_low_u32(a[1]), vget_low_u32(b[1]))); + sum_hi = vaddq_u64(sum_hi, vmull_high_u32(a[1], b[1])); + sum_lo = vaddq_u64(sum_lo, vmull_u32(vget_low_u32(a[2]), vget_low_u32(b[2]))); + sum_hi = vaddq_u64(sum_hi, vmull_high_u32(a[2], b[2])); + sum_lo = vaddq_u64(sum_lo, vmull_u32(vget_low_u32(a[3]), vget_low_u32(b[3]))); + sum_hi = vaddq_u64(sum_hi, vmull_high_u32(a[3], b[3])); + + return Self::solinas_reduce(sum_lo, sum_hi); + } + + let mut carry_lo = vdupq_n_u64(0); + let mut carry_hi = vdupq_n_u64(0); + + let prod_lo_1 = vmull_u32(vget_low_u32(a[1]), vget_low_u32(b[1])); + let prod_hi_1 = vmull_high_u32(a[1], b[1]); + (sum_lo, carry_lo) = Self::add_u64_with_carry(sum_lo, prod_lo_1, carry_lo); + (sum_hi, carry_hi) = Self::add_u64_with_carry(sum_hi, prod_hi_1, carry_hi); + + let prod_lo_2 = vmull_u32(vget_low_u32(a[2]), vget_low_u32(b[2])); + let prod_hi_2 = vmull_high_u32(a[2], b[2]); + (sum_lo, carry_lo) = Self::add_u64_with_carry(sum_lo, prod_lo_2, carry_lo); + (sum_hi, carry_hi) = Self::add_u64_with_carry(sum_hi, prod_hi_2, carry_hi); + + let prod_lo_3 = vmull_u32(vget_low_u32(a[3]), vget_low_u32(b[3])); + let prod_hi_3 = vmull_high_u32(a[3], b[3]); + (sum_lo, carry_lo) = Self::add_u64_with_carry(sum_lo, prod_lo_3, carry_lo); + (sum_hi, carry_hi) = Self::add_u64_with_carry(sum_hi, prod_hi_3, carry_hi); + + Self::solinas_reduce_with_carry(sum_lo, sum_hi, carry_lo, carry_hi) + } + } + + #[inline(always)] + fn dot_product_3_vec(a: [uint32x4_t; 3], b: [uint32x4_t; 3]) -> uint32x4_t { + unsafe { + let mut sum_lo = vmull_u32(vget_low_u32(a[0]), vget_low_u32(b[0])); + let mut sum_hi = vmull_high_u32(a[0], b[0]); + + if Self::BITS <= 31 { + sum_lo = vaddq_u64(sum_lo, vmull_u32(vget_low_u32(a[1]), vget_low_u32(b[1]))); + sum_hi = vaddq_u64(sum_hi, vmull_high_u32(a[1], b[1])); + sum_lo = vaddq_u64(sum_lo, vmull_u32(vget_low_u32(a[2]), vget_low_u32(b[2]))); + sum_hi = vaddq_u64(sum_hi, vmull_high_u32(a[2], b[2])); + + return Self::solinas_reduce(sum_lo, sum_hi); + } + + let mut carry_lo = vdupq_n_u64(0); + let mut carry_hi = vdupq_n_u64(0); + + let prod_lo_1 = vmull_u32(vget_low_u32(a[1]), vget_low_u32(b[1])); + let prod_hi_1 = vmull_high_u32(a[1], b[1]); + (sum_lo, carry_lo) = Self::add_u64_with_carry(sum_lo, prod_lo_1, carry_lo); + (sum_hi, carry_hi) = Self::add_u64_with_carry(sum_hi, prod_hi_1, carry_hi); + + let prod_lo_2 = vmull_u32(vget_low_u32(a[2]), vget_low_u32(b[2])); + let prod_hi_2 = vmull_high_u32(a[2], b[2]); + (sum_lo, carry_lo) = Self::add_u64_with_carry(sum_lo, prod_lo_2, carry_lo); + (sum_hi, carry_hi) = Self::add_u64_with_carry(sum_hi, prod_hi_2, carry_hi); + + Self::solinas_reduce_with_carry(sum_lo, sum_hi, carry_lo, carry_hi) + } + } + + #[inline(always)] + fn mul_nr_vec(x: uint32x4_t) -> uint32x4_t + where + C: FpExt2Config>, + { + if C::IS_NEG_ONE { + Self::sub_vec(unsafe { vdupq_n_u32(0) }, x) + } else if C::non_residue().0 == 2 { + Self::add_vec(x, x) + } else { + C::mul_non_residue(Self::from_vec(x), Self::broadcast).to_vec() + } + } + + #[inline(always)] + fn mul_c_u64(hi: uint64x2_t, c: uint32x2_t) -> uint64x2_t { + unsafe { + if Self::C == 1 { + return hi; + } + if Self::C == 3 { + return vaddq_u64(vshlq_n_u64::<1>(hi), hi); + } + if Self::C == 19 { + return vaddq_u64(vaddq_u64(vshlq_n_u64::<4>(hi), vshlq_n_u64::<1>(hi)), hi); + } + if Self::C == 35 { + return vaddq_u64(vaddq_u64(vshlq_n_u64::<5>(hi), vshlq_n_u64::<1>(hi)), hi); + } + if Self::C == 99 { + return vaddq_u64( + vaddq_u64(vshlq_n_u64::<6>(hi), vshlq_n_u64::<5>(hi)), + vaddq_u64(vshlq_n_u64::<1>(hi), hi), + ); + } + let lo = vmull_u32(vmovn_u64(hi), c); + let hi = vmull_u32(vmovn_u64(vshrq_n_u64::<32>(hi)), c); + vaddq_u64(lo, vshlq_n_u64::<32>(hi)) + } + } + + #[inline(always)] + fn solinas_reduce(prod_lo: uint64x2_t, prod_hi: uint64x2_t) -> uint32x4_t { + unsafe { + if Self::BITS == 31 { + return Self::solinas_reduce_bits31(prod_lo, prod_hi); + } + + let mask = vdupq_n_u64(Self::MASK_U64); + let neg_bits = vdupq_n_s64(-(Self::BITS as i64)); + let c = vdup_n_u32(Self::C); + + let f1_lo = vaddq_u64( + vandq_u64(prod_lo, mask), + Self::mul_c_u64(vshlq_u64(prod_lo, neg_bits), c), + ); + let f1_hi = vaddq_u64( + vandq_u64(prod_hi, mask), + Self::mul_c_u64(vshlq_u64(prod_hi, neg_bits), c), + ); + + let f2_lo = vaddq_u64( + vandq_u64(f1_lo, mask), + Self::mul_c_u64(vshlq_u64(f1_lo, neg_bits), c), + ); + let f2_hi = vaddq_u64( + vandq_u64(f1_hi, mask), + Self::mul_c_u64(vshlq_u64(f1_hi, neg_bits), c), + ); + + if Self::BITS < 32 { + let (reduced_lo, reduced_hi) = if Self::TWO_FOLD_FOUR_PRODUCT_OK { + (f2_lo, f2_hi) + } else { + ( + vaddq_u64( + vandq_u64(f2_lo, mask), + Self::mul_c_u64(vshlq_u64(f2_lo, neg_bits), c), + ), + vaddq_u64( + vandq_u64(f2_hi, mask), + Self::mul_c_u64(vshlq_u64(f2_hi, neg_bits), c), + ), + ) + }; + + let result = vcombine_u32(vmovn_u64(reduced_lo), vmovn_u64(reduced_hi)); + let p = vdupq_n_u32(P); + vminq_u32(result, vsubq_u32(result, p)) + } else { + let p_u64 = vdupq_n_u64(P as u64); + + let red_lo = vsubq_u64(f2_lo, p_u64); + let keep_lo = vcltq_u64(f2_lo, p_u64); + let out_lo = vbslq_u64(keep_lo, f2_lo, red_lo); + + let red_hi = vsubq_u64(f2_hi, p_u64); + let keep_hi = vcltq_u64(f2_hi, p_u64); + let out_hi = vbslq_u64(keep_hi, f2_hi, red_hi); + + vcombine_u32(vmovn_u64(out_lo), vmovn_u64(out_hi)) + } + } + } + + #[inline(always)] + fn solinas_reduce_bits31(prod_lo: uint64x2_t, prod_hi: uint64x2_t) -> uint32x4_t { + unsafe { + let mask = vdupq_n_u64((1u64 << 31) - 1); + let c = vdup_n_u32(Self::C); + + let f1_lo = vaddq_u64( + vandq_u64(prod_lo, mask), + Self::mul_c_u64(vshrq_n_u64::<31>(prod_lo), c), + ); + let f1_hi = vaddq_u64( + vandq_u64(prod_hi, mask), + Self::mul_c_u64(vshrq_n_u64::<31>(prod_hi), c), + ); + + let f2_lo = vaddq_u64( + vandq_u64(f1_lo, mask), + Self::mul_c_u64(vshrq_n_u64::<31>(f1_lo), c), + ); + let f2_hi = vaddq_u64( + vandq_u64(f1_hi, mask), + Self::mul_c_u64(vshrq_n_u64::<31>(f1_hi), c), + ); + + let (reduced_lo, reduced_hi) = if Self::TWO_FOLD_FOUR_PRODUCT_OK { + (f2_lo, f2_hi) + } else { + ( + vaddq_u64( + vandq_u64(f2_lo, mask), + Self::mul_c_u64(vshrq_n_u64::<31>(f2_lo), c), + ), + vaddq_u64( + vandq_u64(f2_hi, mask), + Self::mul_c_u64(vshrq_n_u64::<31>(f2_hi), c), + ), + ) + }; + + let result = vcombine_u32(vmovn_u64(reduced_lo), vmovn_u64(reduced_hi)); + let p = vdupq_n_u32(P); + vminq_u32(result, vsubq_u32(result, p)) + } + } + + #[inline(always)] + fn solinas_reduce_with_carry( + prod_lo: uint64x2_t, + prod_hi: uint64x2_t, + carry_lo: uint64x2_t, + carry_hi: uint64x2_t, + ) -> uint32x4_t { + unsafe { + if Self::BITS == 31 { + return Self::solinas_reduce_with_carry_bits31( + prod_lo, prod_hi, carry_lo, carry_hi, + ); + } + + let mask = vdupq_n_u64(Self::MASK_U64); + let neg_bits = vdupq_n_s64(-(Self::BITS as i64)); + let c = vdup_n_u32(Self::C); + + let f1_lo = vaddq_u64( + vaddq_u64( + vandq_u64(prod_lo, mask), + Self::mul_c_u64(vshlq_u64(prod_lo, neg_bits), c), + ), + Self::carry_correction(carry_lo), + ); + let f1_hi = vaddq_u64( + vaddq_u64( + vandq_u64(prod_hi, mask), + Self::mul_c_u64(vshlq_u64(prod_hi, neg_bits), c), + ), + Self::carry_correction(carry_hi), + ); + + let f2_lo = vaddq_u64( + vandq_u64(f1_lo, mask), + Self::mul_c_u64(vshlq_u64(f1_lo, neg_bits), c), + ); + let f2_hi = vaddq_u64( + vandq_u64(f1_hi, mask), + Self::mul_c_u64(vshlq_u64(f1_hi, neg_bits), c), + ); + + if Self::BITS < 32 { + let (reduced_lo, reduced_hi) = if Self::TWO_FOLD_FOUR_PRODUCT_OK { + (f2_lo, f2_hi) + } else { + ( + vaddq_u64( + vandq_u64(f2_lo, mask), + Self::mul_c_u64(vshlq_u64(f2_lo, neg_bits), c), + ), + vaddq_u64( + vandq_u64(f2_hi, mask), + Self::mul_c_u64(vshlq_u64(f2_hi, neg_bits), c), + ), + ) + }; + + let result = vcombine_u32(vmovn_u64(reduced_lo), vmovn_u64(reduced_hi)); + let p = vdupq_n_u32(P); + vminq_u32(result, vsubq_u32(result, p)) + } else { + let p_u64 = vdupq_n_u64(P as u64); + + let red_lo = vsubq_u64(f2_lo, p_u64); + let keep_lo = vcltq_u64(f2_lo, p_u64); + let out_lo = vbslq_u64(keep_lo, f2_lo, red_lo); + + let red_hi = vsubq_u64(f2_hi, p_u64); + let keep_hi = vcltq_u64(f2_hi, p_u64); + let out_hi = vbslq_u64(keep_hi, f2_hi, red_hi); + + vcombine_u32(vmovn_u64(out_lo), vmovn_u64(out_hi)) + } + } + } + + /// `solinas_reduce_with_carry` specialised for `BITS == 31` (Mersenne31 + /// and any pseudo-Mersenne `Fp32

` with `P = 2^31 - C`). Sibling of + /// `solinas_reduce_bits31`: a separate function that swaps the + /// variable-amount `vshlq_u64(.., neg_bits)` for the immediate-shift + /// `vshrq_n_u64::<31>`, reducing shift-count register pressure and + /// dispatch port pressure. Since `BITS == 31` implies `BITS < 32`, the + /// `else` branch of the canonicalisation can be dropped. + #[inline(always)] + fn solinas_reduce_with_carry_bits31( + prod_lo: uint64x2_t, + prod_hi: uint64x2_t, + carry_lo: uint64x2_t, + carry_hi: uint64x2_t, + ) -> uint32x4_t { + unsafe { + let mask = vdupq_n_u64((1u64 << 31) - 1); + let c = vdup_n_u32(Self::C); + + // Fold 1 with carry correction + let f1_lo = vaddq_u64( + vaddq_u64( + vandq_u64(prod_lo, mask), + Self::mul_c_u64(vshrq_n_u64::<31>(prod_lo), c), + ), + Self::carry_correction(carry_lo), + ); + let f1_hi = vaddq_u64( + vaddq_u64( + vandq_u64(prod_hi, mask), + Self::mul_c_u64(vshrq_n_u64::<31>(prod_hi), c), + ), + Self::carry_correction(carry_hi), + ); + + // Fold 2 + let f2_lo = vaddq_u64( + vandq_u64(f1_lo, mask), + Self::mul_c_u64(vshrq_n_u64::<31>(f1_lo), c), + ); + let f2_hi = vaddq_u64( + vandq_u64(f1_hi, mask), + Self::mul_c_u64(vshrq_n_u64::<31>(f1_hi), c), + ); + + let (reduced_lo, reduced_hi) = if Self::TWO_FOLD_FOUR_PRODUCT_OK { + (f2_lo, f2_hi) + } else { + ( + vaddq_u64( + vandq_u64(f2_lo, mask), + Self::mul_c_u64(vshrq_n_u64::<31>(f2_lo), c), + ), + vaddq_u64( + vandq_u64(f2_hi, mask), + Self::mul_c_u64(vshrq_n_u64::<31>(f2_hi), c), + ), + ) + }; + + let result = vcombine_u32(vmovn_u64(reduced_lo), vmovn_u64(reduced_hi)); + let p = vdupq_n_u32(P); + vminq_u32(result, vsubq_u32(result, p)) + } + } +} + +impl Default for PackedFp32Neon

{ + #[inline] + fn default() -> Self { + Self { vals: [0; 4] } + } +} + +impl fmt::Debug for PackedFp32Neon

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("PackedFp32Neon").field(&self.vals).finish() + } +} + +impl PartialEq for PackedFp32Neon

{ + #[inline] + fn eq(&self, other: &Self) -> bool { + self.vals == other.vals + } +} + +impl Eq for PackedFp32Neon

{} + +impl Add for PackedFp32Neon

{ + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self::from_vec(Self::add_vec(self.to_vec(), rhs.to_vec())) + } +} + +impl Sub for PackedFp32Neon

{ + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self::from_vec(Self::sub_vec(self.to_vec(), rhs.to_vec())) + } +} + +impl Mul for PackedFp32Neon

{ + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self { + Self::from_vec(Self::mul_vec(self.to_vec(), rhs.to_vec())) + } +} + +impl PackedValue for PackedFp32Neon

{ + type Value = Fp32

; + const WIDTH: usize = FP32_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Value, + { + Self { + vals: [f(0).0, f(1).0, f(2).0, f(3).0], + } + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Value { + debug_assert!(lane < FP32_WIDTH); + Fp32(self.vals[lane]) + } +} + +impl AddAssign for PackedFp32Neon

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for PackedFp32Neon

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for PackedFp32Neon

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl PackedField for PackedFp32Neon

{ + type Scalar = Fp32

; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self { vals: [value.0; 4] } + } + + #[inline(always)] + fn fp_ext2_mul(a0: Self, a1: Self, b0: Self, b1: Self) -> (Self, Self) + where + C: FpExt2Config, + { + let a0 = a0.to_vec(); + let a1 = a1.to_vec(); + let b0 = b0.to_vec(); + let b1 = b1.to_vec(); + + let v0 = Self::mul_vec(a0, b0); + let v1 = Self::mul_vec(a1, b1); + let cross = Self::mul_vec(Self::add_vec(a0, a1), Self::add_vec(b0, b1)); + + ( + Self::from_vec(Self::add_vec(v0, Self::mul_nr_vec::(v1))), + Self::from_vec(Self::sub_vec(Self::sub_vec(cross, v0), v1)), + ) + } + + #[inline(always)] + fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { + let [a0, a1, a2, a3] = a.map(Self::to_vec); + let [b0, b1, b2, b3] = b.map(Self::to_vec); + let two_b1 = Self::add_vec(b1, b1); + let two_b2 = Self::add_vec(b2, b2); + let two_b3 = Self::add_vec(b3, b3); + let b0_plus_b2 = Self::add_vec(b0, b2); + let b1_plus_b3 = Self::add_vec(b1, b3); + let b1_minus_b3 = Self::sub_vec(b1, b3); + let b0_minus_b2 = Self::sub_vec(b0, b2); + [ + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [b0, two_b1, two_b2, two_b3], + )), + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [b1, b0_plus_b2, b1_plus_b3, b2], + )), + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [b2, b1_plus_b3, b0, b1_minus_b3], + )), + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [b3, b2, b1_minus_b3, b0_minus_b2], + )), + ] + } + + #[inline(always)] + fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { + let [a0, a1, a2, a3] = a.map(Self::to_vec); + let zero = unsafe { vdupq_n_u32(0) }; + let two_a1 = Self::add_vec(a1, a1); + let two_a2 = Self::add_vec(a2, a2); + let two_a3 = Self::add_vec(a3, a3); + let neg_a3 = Self::sub_vec(zero, a3); + let neg_two_a3 = Self::sub_vec(zero, two_a3); + [ + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a2, a3], + [a0, two_a1, two_a2, two_a3], + )), + Self::from_vec(Self::dot_product_3_vec( + [a0, a1, a2], + [two_a1, two_a2, two_a3], + )), + Self::from_vec(Self::dot_product_4_vec( + [a0, a1, a1, a3], + [two_a2, a1, two_a3, neg_a3], + )), + Self::from_vec(Self::dot_product_3_vec( + [a0, a1, a2], + [two_a3, two_a2, neg_two_a3], + )), + ] + } + + #[inline(always)] + fn fp_ext4_inverse(a: [Self; 4]) -> Option<[Self; 4]> + where + Self::Scalar: Invertible, + { + let [a0, a1, a2, a3] = a.map(Self::to_vec); + let zero = unsafe { vdupq_n_u32(0) }; + let x0 = a0; + let x1 = a2; + let y0 = Self::sub_vec(a1, a3); + let y1 = a3; + + let x1_square = Self::mul_vec(x1, x1); + let y1_square = Self::mul_vec(y1, y1); + let aa0 = Self::add_vec(Self::mul_vec(x0, x0), Self::add_vec(x1_square, x1_square)); + let aa1 = { + let x0x1 = Self::mul_vec(x0, x1); + Self::add_vec(x0x1, x0x1) + }; + let bb0 = Self::add_vec(Self::mul_vec(y0, y0), Self::add_vec(y1_square, y1_square)); + let bb1 = { + let y0y1 = Self::mul_vec(y0, y1); + Self::add_vec(y0y1, y0y1) + }; + let nr_bb0 = Self::add_vec(Self::add_vec(bb0, bb0), Self::add_vec(bb1, bb1)); + let nr_bb1 = Self::add_vec(bb0, Self::add_vec(bb1, bb1)); + let norm0 = Self::sub_vec(aa0, nr_bb0); + let norm1 = Self::sub_vec(aa1, nr_bb1); + + let inv_norm_base = { + let norm1_square = Self::mul_vec(norm1, norm1); + let norm_base = Self::sub_vec( + Self::mul_vec(norm0, norm0), + Self::add_vec(norm1_square, norm1_square), + ); + Self::from_vec(norm_base).inverse()?.to_vec() + }; + let inv_norm0 = Self::mul_vec(norm0, inv_norm_base); + let inv_norm1 = Self::mul_vec(Self::sub_vec(zero, norm1), inv_norm_base); + + let v0 = Self::mul_vec(x0, inv_norm0); + let v1 = Self::mul_vec(x1, inv_norm1); + let constant0 = Self::add_vec(v0, Self::add_vec(v1, v1)); + let constant1 = Self::sub_vec( + Self::sub_vec( + Self::mul_vec(Self::add_vec(x0, x1), Self::add_vec(inv_norm0, inv_norm1)), + v0, + ), + v1, + ); + + let neg_y0 = Self::sub_vec(zero, y0); + let neg_y1 = Self::sub_vec(zero, y1); + let w0 = Self::mul_vec(neg_y0, inv_norm0); + let w1 = Self::mul_vec(neg_y1, inv_norm1); + let e1_coeff0 = Self::add_vec(w0, Self::add_vec(w1, w1)); + let e1_coeff1 = Self::sub_vec( + Self::sub_vec( + Self::mul_vec( + Self::add_vec(neg_y0, neg_y1), + Self::add_vec(inv_norm0, inv_norm1), + ), + w0, + ), + w1, + ); + + Some([ + Self::from_vec(constant0), + Self::from_vec(Self::add_vec(e1_coeff0, e1_coeff1)), + Self::from_vec(constant1), + Self::from_vec(e1_coeff1), + ]) + } +} diff --git a/crates/jolt-field/src/packed/neon/fp64.rs b/crates/jolt-field/src/packed/neon/fp64.rs new file mode 100644 index 0000000000..9572bb8649 --- /dev/null +++ b/crates/jolt-field/src/packed/neon/fp64.rs @@ -0,0 +1,224 @@ +use super::*; + +/// Number of packed `Fp64` lanes. +pub(crate) const FP64_WIDTH: usize = 2; + +/// NEON packed `Fp64` backend: 2 lanes in `uint64x2_t`. +#[derive(Clone, Copy)] +pub struct PackedFp64Neon { + vals: [u64; 2], +} + +impl PackedFp64Neon

{ + const BITS: u32 = 64 - P.leading_zeros(); + + const C_LO: u64 = { + let c = if Self::BITS == 64 { + 0u64.wrapping_sub(P) + } else { + (1u64 << Self::BITS) - P + }; + assert!(P != 0, "modulus must be nonzero"); + assert!(P & 1 == 1, "modulus must be odd"); + c + }; + + const MASK64: u64 = if Self::BITS < 64 { + (1u64 << Self::BITS) - 1 + } else { + u64::MAX + }; + + const MASK_U128: u128 = if Self::BITS == 64 { + u64::MAX as u128 + } else { + (1u128 << Self::BITS) - 1 + }; + + const FOLD_IN_U64: bool = + Self::BITS < 64 && (Self::C_LO as u128) < (1u128 << (64 - Self::BITS)); + + #[inline(always)] + fn mul_c_narrow(x: u64) -> u64 { + Self::C_LO.wrapping_mul(x) + } + + #[inline(always)] + fn reduce_product(x: u128) -> u64 { + if Self::FOLD_IN_U64 { + let lo = x as u64; + let hi = (x >> 64) as u64; + let high = (lo >> Self::BITS) | (hi << (64 - Self::BITS)); + let f1 = (lo & Self::MASK64).wrapping_add(Self::mul_c_narrow(high)); + let f2 = (f1 & Self::MASK64).wrapping_add(Self::mul_c_narrow(f1 >> Self::BITS)); + let reduced = f2.wrapping_sub(P); + let borrow = reduced >> 63; + reduced.wrapping_add(borrow.wrapping_neg() & P) + } else { + let f1 = + (x & Self::MASK_U128) + (Self::C_LO as u128) * ((x >> Self::BITS) as u64 as u128); + let f2 = + (f1 & Self::MASK_U128) + (Self::C_LO as u128) * ((f1 >> Self::BITS) as u64 as u128); + let reduced = f2.wrapping_sub(P as u128); + let borrow = reduced >> 127; + reduced.wrapping_add(borrow.wrapping_neg() & (P as u128)) as u64 + } + } +} + +impl Default for PackedFp64Neon

{ + #[inline] + fn default() -> Self { + Self { vals: [0; 2] } + } +} + +impl fmt::Debug for PackedFp64Neon

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("PackedFp64Neon").field(&self.vals).finish() + } +} + +impl PartialEq for PackedFp64Neon

{ + #[inline] + fn eq(&self, other: &Self) -> bool { + self.vals == other.vals + } +} + +impl Eq for PackedFp64Neon

{} + +impl Add for PackedFp64Neon

{ + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + let a = to_vec(self.vals); + let b = to_vec(rhs.vals); + let result = unsafe { + let p = vdupq_n_u64(P); + if Self::BITS == 64 { + let s = vaddq_u64(a, b); + let overflow = vcltq_u64(s, a); + let folded = vaddq_u64(s, vandq_u64(overflow, vdupq_n_u64(Self::C_LO))); + let reduced = vsubq_u64(folded, p); + let borrow = vcltq_u64(folded, p); + vbslq_u64(borrow, folded, reduced) + } else if Self::BITS <= 62 { + let s = vaddq_u64(a, b); + let r = vsubq_u64(s, p); + let borrow = vcltq_u64(s, p); + vbslq_u64(borrow, s, r) + } else { + let s = vaddq_u64(a, b); + let overflow = vcltq_u64(s, a); + let c = vdupq_n_u64(Self::C_LO); + let s_plus_c = vaddq_u64(s, c); + let s_minus_p = vsubq_u64(s, p); + let borrow = vcltq_u64(s, p); + let no_of = vbslq_u64(borrow, s, s_minus_p); + vbslq_u64(overflow, s_plus_c, no_of) + } + }; + Self { + vals: from_vec(result), + } + } +} + +impl Sub for PackedFp64Neon

{ + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + let a = to_vec(self.vals); + let b = to_vec(rhs.vals); + let result = unsafe { + let d = vsubq_u64(a, b); + let underflow = vcltq_u64(a, b); + if Self::BITS == 64 { + vsubq_u64(d, vandq_u64(underflow, vdupq_n_u64(Self::C_LO))) + } else { + vbslq_u64(underflow, vaddq_u64(d, vdupq_n_u64(P)), d) + } + }; + Self { + vals: from_vec(result), + } + } +} + +impl Mul for PackedFp64Neon

{ + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self { + let x0 = (self.vals[0] as u128) * (rhs.vals[0] as u128); + let x1 = (self.vals[1] as u128) * (rhs.vals[1] as u128); + let r0 = Self::reduce_product(x0); + let r1 = Self::reduce_product(x1); + Self { vals: [r0, r1] } + } +} + +impl PackedValue for PackedFp64Neon

{ + type Value = Fp64

; + const WIDTH: usize = FP64_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Value, + { + Self { + vals: [f(0).0, f(1).0], + } + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Value { + debug_assert!(lane < FP64_WIDTH); + Fp64(self.vals[lane]) + } +} + +impl AddAssign for PackedFp64Neon

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for PackedFp64Neon

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for PackedFp64Neon

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl PackedField for PackedFp64Neon

{ + type Scalar = Fp64

; + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self { vals: [value.0; 2] } + } + + #[inline(always)] + fn fp_ext2_mul(a0: Self, a1: Self, b0: Self, b1: Self) -> (Self, Self) + where + C: FpExt2Config, + { + let v0 = a0 * b0; + let v1 = a1 * b1; + let cross = (a0 + a1) * (b0 + b1); + ( + v0 + C::mul_non_residue(v1, Self::broadcast), + cross - v0 - v1, + ) + } +} diff --git a/crates/jolt-field/src/packed/neon/mod.rs b/crates/jolt-field/src/packed/neon/mod.rs new file mode 100644 index 0000000000..c164a7046f --- /dev/null +++ b/crates/jolt-field/src/packed/neon/mod.rs @@ -0,0 +1,43 @@ +//! AArch64 NEON packed backends for Fp32, Fp64, Fp128. + +#![expect( + clippy::undocumented_unsafe_blocks, + reason = "ported NEON kernels retain their audited intrinsic-level invariants" +)] + +use super::{PackedField, PackedValue}; +use crate::ext::FpExt2Config; +use crate::Invertible; +use crate::{Fp128, Fp32, Fp64}; +use core::arch::aarch64::{ + uint32x2_t, uint32x4_t, uint64x2_t, vaddq_u32, vaddq_u64, vandq_u32, vandq_u64, vbslq_u64, + vcltq_u32, vcltq_u64, vcombine_u32, vdup_n_u32, vdupq_n_s64, vdupq_n_u32, vdupq_n_u64, + vget_low_u32, vminq_u32, vmlsq_u32, vmovn_u64, vmull_high_u32, vmull_u32, vmulq_u32, vorrq_u64, + vqdmulhq_s32, vreinterpretq_s32_u32, vreinterpretq_u32_s32, vshlq_n_u64, vshlq_u64, + vshrq_n_u32, vshrq_n_u64, vsubq_u32, vsubq_u64, +}; +use core::fmt; +use core::mem::transmute; +use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; + +#[inline(always)] +fn to_vec(x: [u64; 2]) -> uint64x2_t { + unsafe { transmute::<[u64; 2], uint64x2_t>(x) } +} + +#[inline(always)] +fn from_vec(v: uint64x2_t) -> [u64; 2] { + unsafe { transmute::(v) } +} + +#[inline(always)] +fn mask_to_bit(mask: uint64x2_t) -> uint64x2_t { + unsafe { vandq_u64(mask, vdupq_n_u64(1)) } +} + +mod fp128; +mod fp32; +mod fp64; +pub(crate) use fp128::*; +pub(crate) use fp32::*; +pub(crate) use fp64::*; diff --git a/crates/jolt-field/src/packed/tests.rs b/crates/jolt-field/src/packed/tests.rs new file mode 100644 index 0000000000..885e832166 --- /dev/null +++ b/crates/jolt-field/src/packed/tests.rs @@ -0,0 +1,344 @@ +#![expect( + clippy::unreadable_literal, + reason = "packed regression vectors retain their generated decimal form" +)] + +use super::{HasPacking, PackedField, PackedValue}; +use crate::{ + CanonicalField, FieldCore, Fp32, Prime128Offset275, Prime24Offset3, Prime31Offset19, + Prime32Offset99, Prime40Offset195, Prime64Offset59, RandomSampling, +}; +use rand::{rngs::StdRng, RngCore, SeedableRng}; + +fn rand_u128(rng: &mut R) -> u128 { + let lo = rng.next_u64() as u128; + let hi = rng.next_u64() as u128; + lo | (hi << 64) +} + +fn check_packed_add_sub_mul(seed: u64) +where + F: FieldCore + RandomSampling + PartialEq + std::fmt::Debug, + PF: PackedField + PackedValue, +{ + let mut rng = StdRng::seed_from_u64(seed); + let len = PF::WIDTH * 17 + 3; + let lhs: Vec = (0..len).map(|_| RandomSampling::random(&mut rng)).collect(); + let rhs: Vec = (0..len).map(|_| RandomSampling::random(&mut rng)).collect(); + + let (lhs_p, lhs_s) = PF::pack_slice_with_suffix(&lhs); + let (rhs_p, rhs_s) = PF::pack_slice_with_suffix(&rhs); + + let add_p: Vec = lhs_p + .iter() + .zip(rhs_p.iter()) + .map(|(&a, &b)| a + b) + .collect(); + let sub_p: Vec = lhs_p + .iter() + .zip(rhs_p.iter()) + .map(|(&a, &b)| a - b) + .collect(); + let mul_p: Vec = lhs_p + .iter() + .zip(rhs_p.iter()) + .map(|(&a, &b)| a * b) + .collect(); + + let mut add_out = PF::unpack_slice(&add_p); + let mut sub_out = PF::unpack_slice(&sub_p); + let mut mul_out = PF::unpack_slice(&mul_p); + + for (&a, &b) in lhs_s.iter().zip(rhs_s.iter()) { + add_out.push(a + b); + sub_out.push(a - b); + mul_out.push(a * b); + } + + for i in 0..len { + assert_eq!( + add_out[i], + lhs[i] + rhs[i], + "packed add mismatch at lane {i}" + ); + assert_eq!( + sub_out[i], + lhs[i] - rhs[i], + "packed sub mismatch at lane {i}" + ); + assert_eq!( + mul_out[i], + lhs[i] * rhs[i], + "packed mul mismatch at lane {i}" + ); + } +} + +fn check_broadcast_roundtrip(val: F) +where + F: FieldCore + PartialEq + std::fmt::Debug, + PF: PackedField + PackedValue, +{ + let p = PF::broadcast(val); + for lane in 0..PF::WIDTH { + assert_eq!(p.extract(lane), val); + } +} + +fn check_packed_fp32_edge_lanes() +where + PF: PackedField> + PackedValue>, +{ + let p_minus_one = Fp32::

::from_canonical_u32(P - 1); + let p_minus_two = Fp32::

::from_canonical_u32(P - 2); + let values = [ + Fp32::

::zero(), + Fp32::

::one(), + p_minus_two, + p_minus_one, + ]; + let a = PF::from_fn(|i| values[i % values.len()]); + let b = PF::from_fn(|i| values[(i + 1) % values.len()]); + + let add = a + b; + let sub = a - b; + let mul = a * b; + + for lane in 0..PF::WIDTH { + let lhs = values[lane % values.len()]; + let rhs = values[(lane + 1) % values.len()]; + assert_eq!(add.extract(lane), lhs + rhs, "packed add edge lane {lane}"); + assert_eq!(sub.extract(lane), lhs - rhs, "packed sub edge lane {lane}"); + assert_eq!(mul.extract(lane), lhs * rhs, "packed mul edge lane {lane}"); + } +} + +#[test] +fn packed_fp128_add_sub_mul_match_scalar() { + type F = Prime128Offset275; + type PF = ::Packing; + + let mut rng = StdRng::seed_from_u64(0x55aa_4422_1177_0033); + let len = PF::WIDTH * 17 + 3; + let lhs: Vec = (0..len) + .map(|_| F::from_canonical_u128_reduced(rand_u128(&mut rng))) + .collect(); + let rhs: Vec = (0..len) + .map(|_| F::from_canonical_u128_reduced(rand_u128(&mut rng))) + .collect(); + + let (lhs_p, lhs_s) = PF::pack_slice_with_suffix(&lhs); + let (rhs_p, rhs_s) = PF::pack_slice_with_suffix(&rhs); + + let add_p: Vec = lhs_p + .iter() + .zip(rhs_p.iter()) + .map(|(&a, &b)| a + b) + .collect(); + let sub_p: Vec = lhs_p + .iter() + .zip(rhs_p.iter()) + .map(|(&a, &b)| a - b) + .collect(); + let mul_p: Vec = lhs_p + .iter() + .zip(rhs_p.iter()) + .map(|(&a, &b)| a * b) + .collect(); + + let mut add_out = PF::unpack_slice(&add_p); + let mut sub_out = PF::unpack_slice(&sub_p); + let mut mul_out = PF::unpack_slice(&mul_p); + + for (&a, &b) in lhs_s.iter().zip(rhs_s.iter()) { + add_out.push(a + b); + sub_out.push(a - b); + mul_out.push(a * b); + } + + for i in 0..len { + assert_eq!( + add_out[i], + lhs[i] + rhs[i], + "packed add mismatch at lane {i}" + ); + assert_eq!( + sub_out[i], + lhs[i] - rhs[i], + "packed sub mismatch at lane {i}" + ); + assert_eq!( + mul_out[i], + lhs[i] * rhs[i], + "packed mul mismatch at lane {i}" + ); + } +} + +#[test] +fn fp128_broadcast_and_extract_roundtrip() { + type F = Prime128Offset275; + type PF = ::Packing; + check_broadcast_roundtrip::(F::from_u64(42)); +} + +#[test] +fn packed_fp32_24b_add_sub_mul() { + type F = Prime24Offset3; + type PF = ::Packing; + check_packed_add_sub_mul::(0xaa24_bb24_cc24_dd24); +} + +#[test] +fn packed_fp32_31b_add_sub_mul() { + type F = Prime31Offset19; + type PF = ::Packing; + check_packed_add_sub_mul::(0xaa31_bb31_cc31_dd31); +} + +#[test] +fn packed_fp32_31b_edge_lanes() { + type F = Prime31Offset19; + type PF = ::Packing; + check_packed_fp32_edge_lanes::<{ crate::prime::pseudo_mersenne::PRIME31_OFFSET19_MODULUS }, PF>( + ); +} + +#[test] +fn packed_mersenne31_edge_lanes() { + type F = Fp32<{ (1u32 << 31) - 1 }>; + type PF = ::Packing; + check_packed_fp32_edge_lanes::<{ (1u32 << 31) - 1 }, PF>(); +} + +/// Stress the 31-bit pseudo-Mersenne (`C > 1`) packed multiply against the +/// scalar reference across boundary values and a large random sweep. This +/// confirms (does not justify) the exact correctness proof on +/// `mul_pmersenne31_vec`: the tightest cases are `z = (P-1)^2` and inputs +/// that drive the second fold's `t'` toward `2P`. +#[test] +fn packed_fp32_31b_mul_matches_scalar_stress() { + type F = Prime31Offset19; + type PF = ::Packing; + const P: u32 = crate::prime::pseudo_mersenne::PRIME31_OFFSET19_MODULUS; + + let boundary = [ + 0u32, + 1, + 2, + 3, + 19, + 1 << 15, + 1 << 30, + (1 << 30) + 1, + (P - 1) / 2, + P - 3, + P - 2, + P - 1, + ]; + + let mut inputs: Vec = boundary.iter().map(|&v| F::from_canonical_u32(v)).collect(); + let mut rng = StdRng::seed_from_u64(0x31be_19ca_fe00_1357); + for _ in 0..(1 << 16) { + inputs.push(F::from_canonical_u32(rng.next_u32() % P)); + } + + let lhs: Vec = inputs.clone(); + let rhs: Vec = { + let mut r = inputs.clone(); + r.rotate_left(1); + r + }; + + let (lhs_p, lhs_s) = PF::pack_slice_with_suffix(&lhs); + let (rhs_p, rhs_s) = PF::pack_slice_with_suffix(&rhs); + let mul_p: Vec = lhs_p + .iter() + .zip(rhs_p.iter()) + .map(|(&a, &b)| a * b) + .collect(); + let mut mul_out = PF::unpack_slice(&mul_p); + for (&a, &b) in lhs_s.iter().zip(rhs_s.iter()) { + mul_out.push(a * b); + } + for i in 0..lhs.len() { + assert_eq!(mul_out[i], lhs[i] * rhs[i], "packed mul mismatch at {i}"); + } + + // Full boundary x boundary cross product (every tight combination). + for &x in &boundary { + for &y in &boundary { + let a = PF::broadcast(F::from_canonical_u32(x)); + let b = PF::broadcast(F::from_canonical_u32(y)); + let got = (a * b).extract(0); + let want = F::from_canonical_u32(x) * F::from_canonical_u32(y); + assert_eq!(got, want, "boundary mul {x}*{y}"); + } + } +} + +#[test] +fn packed_fp32_32b_add_sub_mul() { + type F = Prime32Offset99; + type PF = ::Packing; + check_packed_add_sub_mul::(0xaa32_bb32_cc32_dd32); +} + +/// Regression guard for the 32-bit (`BITS == 32`) packed base multiply. +/// +/// For these primes the two-fold Solinas residue can land in `[2^32, 2*P)` +/// (up to `2^32 + C^2`). The packed `Mul` recombine must subtract `P` on the +/// full 64-bit lanes before packing; a 32-bit recombine drops bit 32 and +/// returns a result that is `C` too small. The probability of hitting this +/// window with uniform random inputs is `~C/2^32 ≈ 2e-6`, so the random +/// parity sweep misses it; these vectors hit it deterministically. They were +/// found by exhaustively comparing the truncating recombine to the true +/// modular product (all land in the overflow window on `Prime32Offset99`). +#[test] +fn packed_fp32_32b_mul_two_fold_overflow_window() { + type F = Prime32Offset99; + type PF = ::Packing; + const VECTORS: [(u32, u32); 7] = [ + (3136721438, 3536064673), + (2498152412, 1827148629), + (2062525777, 3207684599), + (4027016701, 3739597742), + (2476582663, 3902052967), + (4161561975, 3109742861), + (1924659530, 1057556213), + ]; + for (x, y) in VECTORS { + let a = F::from_canonical_u32(x); + let b = F::from_canonical_u32(y); + let got = (PF::broadcast(a) * PF::broadcast(b)).extract(0); + assert_eq!(got, a * b, "packed 32b mul mismatch for {x} * {y}"); + } +} + +#[test] +fn fp32_broadcast_and_extract_roundtrip() { + type F = Prime24Offset3; + type PF = ::Packing; + check_broadcast_roundtrip::(F::from_u64(42)); +} + +#[test] +fn packed_fp64_40b_add_sub_mul() { + type F = Prime40Offset195; + type PF = ::Packing; + check_packed_add_sub_mul::(0xaa40_bb40_cc40_dd40); +} + +#[test] +fn packed_fp64_64b_add_sub_mul() { + type F = Prime64Offset59; + type PF = ::Packing; + check_packed_add_sub_mul::(0xaa64_bb64_cc64_dd64); +} + +#[test] +fn fp64_broadcast_and_extract_roundtrip() { + type F = Prime40Offset195; + type PF = ::Packing; + check_broadcast_roundtrip::(F::from_u64(42)); +} diff --git a/crates/jolt-field/src/parallel.rs b/crates/jolt-field/src/parallel.rs new file mode 100644 index 0000000000..c334773ce0 --- /dev/null +++ b/crates/jolt-field/src/parallel.rs @@ -0,0 +1,104 @@ +//! Conditional parallelism utilities. +//! +//! When the `parallel` feature is enabled, the `cfg_iter!` family of macros +//! expand to rayon's parallel iterators. Otherwise they fall back to standard +//! sequential iterators. + +#[cfg(feature = "parallel")] +pub use rayon::prelude::*; + +/// Returns `.par_iter()` when `parallel` is enabled, `.iter()` otherwise. +#[macro_export] +macro_rules! cfg_iter { + ($e:expr) => {{ + #[cfg(feature = "parallel")] + let it = $e.par_iter(); + #[cfg(not(feature = "parallel"))] + let it = $e.iter(); + it + }}; +} + +/// Returns `.par_iter_mut()` when `parallel` is enabled, `.iter_mut()` otherwise. +#[macro_export] +macro_rules! cfg_iter_mut { + ($e:expr) => {{ + #[cfg(feature = "parallel")] + let it = $e.par_iter_mut(); + #[cfg(not(feature = "parallel"))] + let it = $e.iter_mut(); + it + }}; +} + +/// Returns `.into_par_iter()` when `parallel` is enabled, `.into_iter()` otherwise. +#[macro_export] +macro_rules! cfg_into_iter { + ($e:expr) => {{ + #[cfg(feature = "parallel")] + let it = $e.into_par_iter(); + #[cfg(not(feature = "parallel"))] + let it = $e.into_iter(); + it + }}; +} + +/// Returns `.par_chunks(n)` when `parallel` is enabled, `.chunks(n)` otherwise. +#[macro_export] +macro_rules! cfg_chunks { + ($e:expr, $n:expr) => {{ + #[cfg(feature = "parallel")] + let it = $e.par_chunks($n); + #[cfg(not(feature = "parallel"))] + let it = $e.chunks($n); + it + }}; +} + +/// Returns `.par_chunks_mut(n)` when `parallel` is enabled, `.chunks_mut(n)` otherwise. +#[macro_export] +macro_rules! cfg_chunks_mut { + ($e:expr, $n:expr) => {{ + #[cfg(feature = "parallel")] + let it = $e.par_chunks_mut($n); + #[cfg(not(feature = "parallel"))] + let it = $e.chunks_mut($n); + it + }}; +} + +/// Runs two closures potentially in parallel via `rayon::join`. +/// +/// Without `parallel`: runs them sequentially and returns the pair. +#[macro_export] +macro_rules! cfg_join { + ($f_a:expr, $f_b:expr) => {{ + #[cfg(feature = "parallel")] + let result = rayon::join($f_a, $f_b); + #[cfg(not(feature = "parallel"))] + let result = ($f_a(), $f_b()); + result + }}; +} + +/// Parallel fold-reduce over a range. +/// +/// With `parallel`: `range.into_par_iter().fold(identity, fold_op).reduce(identity, reduce_op)`. +/// Without: `range.into_iter().fold(identity(), fold_op)`. +#[macro_export] +macro_rules! cfg_fold_reduce { + ($range:expr, $identity:expr, $fold_op:expr, $reduce_op:expr) => {{ + #[cfg(feature = "parallel")] + let result = $range + .into_par_iter() + .fold($identity, $fold_op) + .reduce($identity, $reduce_op); + #[cfg(not(feature = "parallel"))] + let result = $range.into_iter().fold(($identity)(), $fold_op); + result + }}; +} + +pub use crate::{ + cfg_chunks, cfg_chunks_mut, cfg_fold_reduce, cfg_into_iter, cfg_iter, cfg_iter_mut, cfg_join, +}; diff --git a/crates/jolt-field/src/prime/fp128/add_sub.rs b/crates/jolt-field/src/prime/fp128/add_sub.rs new file mode 100644 index 0000000000..ac026b290d --- /dev/null +++ b/crates/jolt-field/src/prime/fp128/add_sub.rs @@ -0,0 +1,409 @@ +#![cfg_attr( + any(target_arch = "aarch64", target_arch = "x86_64"), + expect( + clippy::undocumented_unsafe_blocks, + reason = "ported inline-assembly kernels retain their audited flag-flow invariants" + ) +)] + +use super::*; + +impl Fp128

{ + #[inline(always)] + pub(super) fn add_raw(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + #[cfg(target_arch = "aarch64")] + { + // On AArch64 we can keep the reduction predicate in flags via `ccmp`, + // which is materially better than the generic `u128` lowering. + Self::add_raw_aarch64_dispatch(a, b) + } + + #[cfg(target_arch = "x86_64")] + { + // On x86-64, `sbb reg, reg` turns carry1 into a 0/-1 mask without + // leaving flags. After computing `s + C`, one more `adc mask, mask` + // makes ZF encode "need reduction", so the final select stays on + // the flag path via `cmovne`. + Self::add_raw_x86_64_dispatch(a, b) + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + { + Self::add_raw_portable(a, b) + } + } + + #[cfg_attr( + any(target_arch = "aarch64", target_arch = "x86_64"), + expect( + dead_code, + reason = "target-specific helper is intentionally unused on some architectures" + ) + )] + #[inline(always)] + fn add_raw_portable(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + // Compute s = a + b as two limbs. + let (s0, carry0) = a[0].overflowing_add(b[0]); + let (s1a, carry1a) = a[1].overflowing_add(b[1]); + let (s1, carry1b) = s1a.overflowing_add(carry0 as u64); + let overflow = carry1a | carry1b; + + // Since p = 2^128 - C and C < 2^64, reducing s modulo p is just + // adding C into the low limb and propagating that carry. + let (r0, carry2) = s0.overflowing_add(Self::C_LO); + let (r1, carry3) = s1.overflowing_add(carry2 as u64); + + pack( + if overflow | carry3 { r0 } else { s0 }, + if overflow | carry3 { r1 } else { s1 }, + ) + } + + #[cfg(target_arch = "aarch64")] + #[inline(always)] + fn add_raw_aarch64_dispatch(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + // The immediate form is best when C < 4096 (the AArch64 add-immediate + // encoding limit). Stable Rust does not let us feed `Self::C_LO` + // directly into an `asm!(..., const ...)` operand, so the known + // built-in offsets are spelled out here and everything else uses the + // register form. + match Self::C_LO { + 275 => Self::add_raw_aarch64_imm::<275>(a, b), + 159 => Self::add_raw_aarch64_imm::<159>(a, b), + 2355 => Self::add_raw_aarch64_imm::<2355>(a, b), + _ => Self::add_raw_aarch64_reg(a, b, Self::C_LO), + } + } + + #[cfg(target_arch = "aarch64")] + #[inline(always)] + fn add_raw_aarch64_imm(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + let out_lo: u64; + let out_hi: u64; + unsafe { + // carry1 is the overflow bit from a + b. + // carry2 is the overflow bit from s + C, equivalently s >= p. + // `ccmp` folds `carry1 | carry2` back into flags so the final + // select stays branchless and never round-trips through GPR logic. + asm!( + "adds {s_lo}, {a_lo}, {b_lo}", + "adcs {s_hi}, {a_hi}, {b_hi}", + "cset {carry1:w}, hs", + "adds {t_lo}, {s_lo}, #{c}", + "adcs {t_hi}, {s_hi}, xzr", + "ccmp {carry1:w}, #0, #0, lo", + "csel {out_lo}, {t_lo}, {s_lo}, ne", + "csel {out_hi}, {t_hi}, {s_hi}, ne", + c = const C, + a_lo = in(reg) a[0], + a_hi = in(reg) a[1], + b_lo = in(reg) b[0], + b_hi = in(reg) b[1], + s_lo = out(reg) _, + s_hi = out(reg) _, + t_lo = out(reg) _, + t_hi = out(reg) _, + carry1 = out(reg) _, + out_lo = lateout(reg) out_lo, + out_hi = lateout(reg) out_hi, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } + + #[cfg(target_arch = "aarch64")] + #[inline(always)] + fn add_raw_aarch64_reg(a: [u64; 2], b: [u64; 2], c: u64) -> [u64; 2] { + let out_lo: u64; + let out_hi: u64; + unsafe { + // Same flag flow as the immediate path above, but with C supplied in + // a register for offsets that are not encodable as add immediates. + asm!( + "adds {s_lo}, {a_lo}, {b_lo}", + "adcs {s_hi}, {a_hi}, {b_hi}", + "cset {carry1:w}, hs", + "adds {t_lo}, {s_lo}, {c}", + "adcs {t_hi}, {s_hi}, xzr", + "ccmp {carry1:w}, #0, #0, lo", + "csel {out_lo}, {t_lo}, {s_lo}, ne", + "csel {out_hi}, {t_hi}, {s_hi}, ne", + c = in(reg) c, + a_lo = in(reg) a[0], + a_hi = in(reg) a[1], + b_lo = in(reg) b[0], + b_hi = in(reg) b[1], + s_lo = out(reg) _, + s_hi = out(reg) _, + t_lo = out(reg) _, + t_hi = out(reg) _, + carry1 = out(reg) _, + out_lo = lateout(reg) out_lo, + out_hi = lateout(reg) out_hi, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } + + #[cfg(target_arch = "x86_64")] + #[inline(always)] + fn add_raw_x86_64_dispatch(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + // As on AArch64, stable Rust does not let us feed `Self::C_LO` + // directly into a const asm operand. The built-in offsets get the + // immediate form and everything else uses the register form. + match Self::C_LO { + 275 => Self::add_raw_x86_64_imm::<275>(a, b), + 159 => Self::add_raw_x86_64_imm::<159>(a, b), + 2355 => Self::add_raw_x86_64_imm::<2355>(a, b), + // For C >= 2^31 the i32 immediate form is unusable: `add r64, + // imm32` sign-extends the immediate, which would silently + // corrupt the high limb. Such offsets fall through to the + // register form below (`Prime128OffsetA7F7` lands here). + _ => Self::add_raw_x86_64_reg(a, b, Self::C_LO), + } + } + + #[cfg(target_arch = "x86_64")] + #[inline(always)] + fn add_raw_x86_64_imm(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + let mut out_lo = a[0]; + let mut out_hi = a[1]; + let _mask: u64; + let _t_lo: u64; + let _t_hi: u64; + unsafe { + // After `s = a + b`, `sbb mask, mask` materializes carry1 as 0/-1. + // After `t = s + C`, `adc mask, mask` leaves ZF=1 iff neither + // carry1 nor carry2 was set. `cmovne` then picks `t` exactly when + // reduction is needed. + asm!( + "add {out_lo}, {b_lo}", + "adc {out_hi}, {b_hi}", + "sbb {mask}, {mask}", + "mov {t_lo}, {out_lo}", + "mov {t_hi}, {out_hi}", + "add {t_lo}, {c}", + "adc {t_hi}, 0", + "adc {mask}, {mask}", + "cmovne {out_lo}, {t_lo}", + "cmovne {out_hi}, {t_hi}", + out_lo = inout(reg) out_lo, + out_hi = inout(reg) out_hi, + b_lo = in(reg) b[0], + b_hi = in(reg) b[1], + mask = out(reg) _mask, + t_lo = out(reg) _t_lo, + t_hi = out(reg) _t_hi, + c = const C, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } + + #[cfg(target_arch = "x86_64")] + #[inline(always)] + fn add_raw_x86_64_reg(a: [u64; 2], b: [u64; 2], c: u64) -> [u64; 2] { + let mut out_lo = a[0]; + let mut out_hi = a[1]; + let _mask: u64; + let _t_lo: u64; + let _t_hi: u64; + unsafe { + asm!( + "add {out_lo}, {b_lo}", + "adc {out_hi}, {b_hi}", + "sbb {mask}, {mask}", + "mov {t_lo}, {out_lo}", + "mov {t_hi}, {out_hi}", + "add {t_lo}, {c}", + "adc {t_hi}, 0", + "adc {mask}, {mask}", + "cmovne {out_lo}, {t_lo}", + "cmovne {out_hi}, {t_hi}", + out_lo = inout(reg) out_lo, + out_hi = inout(reg) out_hi, + b_lo = in(reg) b[0], + b_hi = in(reg) b[1], + c = in(reg) c, + mask = out(reg) _mask, + t_lo = out(reg) _t_lo, + t_hi = out(reg) _t_hi, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } + + #[inline(always)] + pub(super) fn sub_raw(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + #[cfg(target_arch = "aarch64")] + { + // The const path still uses `sub_raw_portable`, but at runtime on + // AArch64 we can keep subtraction in limbs and reduce with `-C` + // instead of materializing `P = 2^128 - C`. + Self::sub_raw_aarch64_dispatch(a, b) + } + + #[cfg(target_arch = "x86_64")] + { + // On x86-64, `sbb reg, reg` turns the final borrow into a 0/-1 mask. + // Masking that with C lets us keep the same "select 0 or C, then do + // one final subtract" structure that worked well on AArch64. + Self::sub_raw_x86_64_dispatch(a, b) + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + { + Self::sub_raw_portable(a, b) + } + } + + #[inline(always)] + pub(super) const fn sub_raw_portable(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + let (diff, borrow) = to_u128(a).overflowing_sub(to_u128(b)); + from_u128(if borrow { diff.wrapping_add(P) } else { diff }) + } + + #[cfg(target_arch = "aarch64")] + #[inline(always)] + fn sub_raw_aarch64_dispatch(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + // As in add_raw, stable Rust cannot feed `Self::C_LO` directly into a + // `const` asm operand, so the built-in offsets get immediate forms and + // everything else falls back to the register form. + match Self::C_LO { + 275 => Self::sub_raw_aarch64_imm::<275>(a, b), + 159 => Self::sub_raw_aarch64_imm::<159>(a, b), + 2355 => Self::sub_raw_aarch64_imm::<2355>(a, b), + _ => Self::sub_raw_aarch64_reg(a, b, Self::C_LO), + } + } + + #[cfg(target_arch = "aarch64")] + #[inline(always)] + fn sub_raw_aarch64_imm(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + let out_lo: u64; + let out_hi: u64; + unsafe { + // If `a - b` borrows, then modulo `p = 2^128 - C` we need + // `diff + p = diff - C (mod 2^128)`. Instead of round-tripping the + // borrow bit through a GPR with `cset`/`cmp`, select the subtrahend + // (`0` or `C`) directly from flags and do one final subtract. + asm!( + "mov {c_tmp}, #{c}", + "subs {out_lo}, {a_lo}, {b_lo}", + "sbcs {out_hi}, {a_hi}, {b_hi}", + "csel {c_tmp}, xzr, {c_tmp}, hs", + "subs {out_lo}, {out_lo}, {c_tmp}", + "sbc {out_hi}, {out_hi}, xzr", + c = const C, + a_lo = in(reg) a[0], + a_hi = in(reg) a[1], + b_lo = in(reg) b[0], + b_hi = in(reg) b[1], + c_tmp = out(reg) _, + out_lo = out(reg) out_lo, + out_hi = out(reg) out_hi, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } + + #[cfg(target_arch = "aarch64")] + #[inline(always)] + fn sub_raw_aarch64_reg(a: [u64; 2], b: [u64; 2], c: u64) -> [u64; 2] { + let out_lo: u64; + let out_hi: u64; + unsafe { + asm!( + "subs {out_lo}, {a_lo}, {b_lo}", + "sbcs {out_hi}, {a_hi}, {b_hi}", + "csel {c_tmp}, xzr, {c}, hs", + "subs {out_lo}, {out_lo}, {c_tmp}", + "sbc {out_hi}, {out_hi}, xzr", + c = in(reg) c, + a_lo = in(reg) a[0], + a_hi = in(reg) a[1], + b_lo = in(reg) b[0], + b_hi = in(reg) b[1], + c_tmp = out(reg) _, + out_lo = out(reg) out_lo, + out_hi = out(reg) out_hi, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } + + #[cfg(target_arch = "x86_64")] + #[inline(always)] + fn sub_raw_x86_64_dispatch(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + // The immediate form keeps C out of the input register set for the + // built-in offsets. Stable Rust does not let us pass `Self::C_LO` + // directly as a const asm operand, so the known built-ins are spelled + // out here and everything else uses the register form. + match Self::C_LO { + 275 => Self::sub_raw_x86_64_imm::<275>(a, b), + 159 => Self::sub_raw_x86_64_imm::<159>(a, b), + 2355 => Self::sub_raw_x86_64_imm::<2355>(a, b), + // See the matching note in `add_raw_x86_64_dispatch`: offsets + // with C >= 2^31 cannot use the i32 immediate form because the + // sign-extended `and r64, imm32` would corrupt the mask, so + // they fall through to the register path here. + _ => Self::sub_raw_x86_64_reg(a, b, Self::C_LO), + } + } + + #[cfg(target_arch = "x86_64")] + #[inline(always)] + fn sub_raw_x86_64_imm(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + let mut out_lo = a[0]; + let mut out_hi = a[1]; + unsafe { + asm!( + "sub {out_lo}, {b_lo}", + "sbb {out_hi}, {b_hi}", + "sbb {mask}, {mask}", + "and {mask}, {c}", + "sub {out_lo}, {mask}", + "sbb {out_hi}, 0", + out_lo = inout(reg) out_lo, + out_hi = inout(reg) out_hi, + b_lo = in(reg) b[0], + b_hi = in(reg) b[1], + mask = out(reg) _, + c = const C, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } + + #[cfg(target_arch = "x86_64")] + #[inline(always)] + fn sub_raw_x86_64_reg(a: [u64; 2], b: [u64; 2], c: u64) -> [u64; 2] { + let mut out_lo = a[0]; + let mut out_hi = a[1]; + unsafe { + asm!( + "sub {out_lo}, {b_lo}", + "sbb {out_hi}, {b_hi}", + "sbb {mask}, {mask}", + "and {mask}, {c}", + "sub {out_lo}, {mask}", + "sbb {out_hi}, 0", + out_lo = inout(reg) out_lo, + out_hi = inout(reg) out_hi, + b_lo = in(reg) b[0], + b_hi = in(reg) b[1], + c = in(reg) c, + mask = out(reg) _, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } +} diff --git a/crates/jolt-field/src/prime/fp128/core.rs b/crates/jolt-field/src/prime/fp128/core.rs new file mode 100644 index 0000000000..00d69b79a2 --- /dev/null +++ b/crates/jolt-field/src/prime/fp128/core.rs @@ -0,0 +1,126 @@ +use super::*; + +/// 128-bit prime field element for primes of the form `p = 2^128 - c`. +/// +/// Stored as `[u64; 2]` (lo, hi) for 8-byte alignment and direct limb access. +/// +/// The offset `c = 2^128 - p` and all derived constants are computed at +/// compile time from the const-generic `P`. Instantiating `Fp128` with a +/// modulus that is not of this form is a compile-time error. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, Default)] +pub struct Fp128(pub(crate) [u64; 2]); + +impl PartialEq for Fp128

{ + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for Fp128

{} + +impl Fp128

{ + /// Offset `c = 2^128 − p`. Validated at compile time. + pub const C: u128 = { + let c = 0u128.wrapping_sub(P); + assert!(P != 0, "modulus must be nonzero"); + assert!(P & 1 == 1, "modulus must be odd"); + assert!( + c < (1u128 << 32), + "C must be < 2^32 (asm fold-2 uses single mul)" + ); + assert!( + c * (c + 1) < P, + "C(C+1) < P required for fused canonicalize" + ); + c + }; + /// Low 64 bits of `C` (always equals `C` since `C < 2^32`). + pub const C_LO: u64 = Self::C as u64; + + /// Create from a canonical representative in `[0, p)`. + #[inline] + pub fn from_canonical_u128(x: u128) -> Self { + debug_assert!(x < P); + Self(from_u128(x)) + } + + /// Additive identity. + #[inline] + pub fn zero() -> Self { + Self(pack(0, 0)) + } + + /// Multiplicative identity. + #[inline] + pub fn one() -> Self { + Self(pack(1, 0)) + } + + /// Check whether this element is zero. + #[inline] + pub fn is_zero(&self) -> bool { + self.0 == [0, 0] + } + + /// Multiplicative inverse, or `None` for zero. + #[inline] + pub fn inverse(&self) -> Option { + ::inverse(self) + } + + /// Construct from a `u64` reduced modulo the field modulus. + #[inline] + pub fn from_u64(val: u64) -> Self { + Self(from_u128(val as u128)) + } + + /// Construct from an `i64` reduced modulo the field modulus. + #[inline] + pub fn from_i64(val: i64) -> Self { + Self::from_i64_const(val) + } + + /// Construct from an `i8` reduced modulo the field modulus. + #[inline] + pub fn from_i8(val: i8) -> Self { + Self::from_i64(val as i64) + } + + /// Return the canonical representative in `[0, p)`. + #[inline] + pub fn to_canonical_u128(self) -> u128 { + to_u128(self.0) + } + + /// Const-evaluable `from_i64`. Embeds a small signed integer into `Fp`. + pub const fn from_i64_const(val: i64) -> Self { + if val >= 0 { + Self(from_u128(val as u128)) + } else { + Self(Self::sub_raw_portable( + pack(0, 0), + from_u128(val.unsigned_abs() as u128), + )) + } + } + + /// Const-evaluable lookup table for balanced digits in `[-b/2, b/2)` + /// where `b = 2^log_basis`. Requires `log_basis <= 6`. + /// + /// # Panics + /// + /// Panics if `log_basis` is outside `1..=6`. + pub const fn digit_lut(log_basis: u32) -> [Self; 64] { + assert!(log_basis > 0 && log_basis <= 6); + let b = 1u32 << log_basis; + let half_b = (b / 2) as i64; + let mut lut = [Self(pack(0, 0)); 64]; + let mut i = 0u32; + while i < b { + lut[i as usize] = Self::from_i64_const(i as i64 - half_b); + i += 1; + } + lut + } +} diff --git a/crates/jolt-field/src/prime/fp128/mod.rs b/crates/jolt-field/src/prime/fp128/mod.rs new file mode 100644 index 0000000000..7c136221b8 --- /dev/null +++ b/crates/jolt-field/src/prime/fp128/mod.rs @@ -0,0 +1,64 @@ +//! 128-bit prime field for primes of the form `p = 2^128 − c` with `c < 2^32`. +//! +//! Uses Solinas-style two-fold reduction: no Montgomery form, ~23 cycles/mul +//! on both AArch64 and x86-64. The offset `c` is computed at compile time +//! from the const-generic modulus `P`. +//! +//! ## Built-in primes +//! +//! Two built-in protocol primes are exposed: +//! +//! - `Prime128OffsetA7F7` (`p = 2^128 − 2^32 + 22537`, `C = 0xFFFFA7F7`), +//! whose multiplicative group has a smooth subgroup of order +//! `2^3 · 3^7 = 17 496` (with a clean radix-3 substructure of order +//! `3^7 = 2187`). This is the default protocol prime. +//! - `Prime128Offset2355` (`p = 2^128 − 2355`), with smooth subgroup +//! `2² · 3 · 5² · 7² = 14 700`, supported as a peer prime. +//! +//! A secondary split-NTT-only prime `Prime128Offset159` +//! (`p = 2^128 − 159`, `p ≡ 33 mod 64`) is kept for the algebra benchmark/test +//! path that only needs 32-way roots of unity. + +mod add_sub; +mod core; +mod mul; +mod primes; +mod reduce; +#[cfg(test)] +mod tests; +mod traits; +mod wide; + +#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] +use ::core::arch::asm; +use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +use crate::{FromPrimitiveInt, Invertible, RandomSampling}; +use rand_core::RngCore; + +use crate::{ + BalancedDigitLookup, CanonicalField, HalvingField, PseudoMersenneField, SmoothFftField, +}; + +use super::util::{is_pow2_u64, log2_pow2_u64, mul64_wide}; + +pub use self::core::Fp128; +pub use primes::{Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7}; + +/// Pack two u64 limbs into `[lo, hi]`. +#[inline(always)] +pub(super) const fn pack(lo: u64, hi: u64) -> [u64; 2] { + [lo, hi] +} + +/// Convert `u128` → `[u64; 2]`. +#[inline(always)] +pub(super) const fn from_u128(x: u128) -> [u64; 2] { + [x as u64, (x >> 64) as u64] +} + +/// Convert `[u64; 2]` → `u128`. +#[inline(always)] +pub(super) const fn to_u128(x: [u64; 2]) -> u128 { + x[0] as u128 | (x[1] as u128) << 64 +} diff --git a/crates/jolt-field/src/prime/fp128/mul.rs b/crates/jolt-field/src/prime/fp128/mul.rs new file mode 100644 index 0000000000..88bf5da2e5 --- /dev/null +++ b/crates/jolt-field/src/prime/fp128/mul.rs @@ -0,0 +1,376 @@ +#![cfg_attr( + target_arch = "aarch64", + expect( + clippy::undocumented_unsafe_blocks, + reason = "ported inline-assembly kernels retain their audited carry-flow invariants" + ) +)] + +use super::*; + +impl Fp128

{ + #[inline(always)] + pub(super) fn mul_raw(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + #[cfg(target_arch = "aarch64")] + { + Self::mul_raw_aarch64(a, b) + } + + #[cfg(not(target_arch = "aarch64"))] + { + Self::mul_raw_portable(a, b) + } + } + + #[cfg_attr( + target_arch = "aarch64", + expect( + dead_code, + reason = "target-specific helper is intentionally unused on some architectures" + ) + )] + #[inline(always)] + fn mul_raw_portable(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + let [r0, r1, r2, r3] = Self(a).mul_wide(Self(b)); + Self::reduce_4(r0, r1, r2, r3) + } + + #[inline(always)] + fn mul_add_raw(a: [u64; 2], b: [u64; 2], addend: [u64; 2]) -> [u64; 2] { + #[cfg(target_arch = "aarch64")] + { + Self::mul_add_raw_aarch64(a, b, addend) + } + + #[cfg(not(target_arch = "aarch64"))] + { + Self::mul_add_raw_portable(a, b, addend) + } + } + + #[cfg_attr( + target_arch = "aarch64", + expect( + dead_code, + reason = "target-specific helper is intentionally unused on some architectures" + ) + )] + #[inline(always)] + fn mul_add_raw_portable(a: [u64; 2], b: [u64; 2], addend: [u64; 2]) -> [u64; 2] { + let prod = Self(a).mul_wide(Self(b)); + let [s0, s1, s2, s3] = Self::add_128_into_256(prod, addend); + Self::reduce_4(s0, s1, s2, s3) + } + + #[cfg(target_arch = "aarch64")] + #[inline(always)] + fn mul_add_raw_aarch64(a: [u64; 2], b: [u64; 2], addend: [u64; 2]) -> [u64; 2] { + let out_lo: u64; + let out_hi: u64; + unsafe { + asm!( + // Schoolbook 2×2 → 256-bit product [r0,r1,r2,r3] + "mul {p00l}, {a0}, {b0}", + "umulh {p00h}, {a0}, {b0}", + "mul {p01l}, {a0}, {b1}", + "umulh {p01h}, {a0}, {b1}", + "mul {p10l}, {a1}, {b0}", + "umulh {p10h}, {a1}, {b0}", + "mul {p11l}, {a1}, {b1}", + "umulh {p11h}, {a1}, {b1}", + + // Carry accumulation into [r0=p00l, r1=p00h, r2=p01h, r3=p11h] + "adds {p00h}, {p00h}, {p01l}", + "cset {p01l:w}, hs", + "adds {p01h}, {p01h}, {p10h}", + "cset {p10h:w}, hs", + "adds {p01h}, {p01h}, {p11l}", + "cinc {p10h}, {p10h}, hs", + "adds {p00h}, {p00h}, {p10l}", + "adcs {p01h}, {p01h}, {p01l}", + "adc {p11h}, {p11h}, {p10h}", + + // Fuse the addend into the low 128 bits before the Solinas fold. + "adds {p00l}, {p00l}, {add_lo}", + "adcs {p00h}, {p00h}, {add_hi}", + "adcs {p01h}, {p01h}, xzr", + "adc {p11h}, {p11h}, xzr", + + // Fold-1: [t0,t1,t2] = [r0,r1] + C·[r2,r3] + "mul {p01l}, {p01h}, {c}", + "umulh {p10l}, {p01h}, {c}", + "mul {p10h}, {p11h}, {c}", + "umulh {p11l}, {p11h}, {c}", + + "adds {p00l}, {p00l}, {p01l}", + "adcs {p00h}, {p00h}, {p10l}", + "cset {p01h:w}, hs", + "adds {p00h}, {p00h}, {p10h}", + "adc {p11h}, {p11l}, {p01h}", + + // Fold-2 + canonicalize via ccmp + "mul {p01l}, {p11h}, {c}", + "adds {p00l}, {p00l}, {p01l}", + "adcs {p00h}, {p00h}, xzr", + "cset {p01l:w}, hs", + "adds {p10l}, {p00l}, {c}", + "adcs {p10h}, {p00h}, xzr", + "ccmp {p01l:w}, #0, #0, lo", + "csel {out_lo}, {p10l}, {p00l}, ne", + "csel {out_hi}, {p10h}, {p00h}, ne", + + a0 = in(reg) a[0], + a1 = in(reg) a[1], + b0 = in(reg) b[0], + b1 = in(reg) b[1], + add_lo = in(reg) addend[0], + add_hi = in(reg) addend[1], + c = in(reg) Self::C_LO, + p00l = out(reg) _, + p00h = out(reg) _, + p01l = out(reg) _, + p01h = out(reg) _, + p10l = out(reg) _, + p10h = out(reg) _, + p11l = out(reg) _, + p11h = out(reg) _, + out_lo = lateout(reg) out_lo, + out_hi = lateout(reg) out_hi, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } + + /// 35-instruction AArch64 inline-asm multiply with Solinas reduction. + /// + /// Saves 6 instructions vs LLVM's codegen by: + /// - Fold-1 carry chain: direct adds/adcs/adc (5 vs 8 instructions), + /// avoiding intermediate cset/cinc shuttling of carries. + /// - Fold-2 + canonicalize: `ccmp` folds the overflow predicate with + /// the ≥p check (8 vs 10 instructions). + /// + /// Benchmarked at 1.29x throughput improvement on Apple M4. + #[cfg(target_arch = "aarch64")] + #[inline(always)] + fn mul_raw_aarch64(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + let out_lo: u64; + let out_hi: u64; + unsafe { + asm!( + // Schoolbook 2×2 → 256-bit product [r0,r1,r2,r3] + "mul {p00l}, {a0}, {b0}", + "umulh {p00h}, {a0}, {b0}", + "mul {p01l}, {a0}, {b1}", + "umulh {p01h}, {a0}, {b1}", + "mul {p10l}, {a1}, {b0}", + "umulh {p10h}, {a1}, {b0}", + "mul {p11l}, {a1}, {b1}", + "umulh {p11h}, {a1}, {b1}", + + // Carry accumulation into [r0=p00l, r1=p00h, r2=p01h, r3=p11h] + "adds {p00h}, {p00h}, {p01l}", + "cset {p01l:w}, hs", + "adds {p01h}, {p01h}, {p10h}", + "cset {p10h:w}, hs", + "adds {p01h}, {p01h}, {p11l}", + "cinc {p10h}, {p10h}, hs", + "adds {p00h}, {p00h}, {p10l}", + "adcs {p01h}, {p01h}, {p01l}", + "adc {p11h}, {p11h}, {p10h}", + + // Fold-1: [t0,t1,t2] = [r0,r1] + C·[r2,r3] + "mul {p01l}, {p01h}, {c}", + "umulh {p10l}, {p01h}, {c}", + "mul {p10h}, {p11h}, {c}", + "umulh {p11l}, {p11h}, {c}", + + "adds {p00l}, {p00l}, {p01l}", + "adcs {p00h}, {p00h}, {p10l}", + "cset {p01h:w}, hs", + "adds {p00h}, {p00h}, {p10h}", + "adc {p11h}, {p11l}, {p01h}", + + // Fold-2 + canonicalize via ccmp (C < 2^32 ⇒ C·t2 fits in 64 bits) + "mul {p01l}, {p11h}, {c}", + "adds {p00l}, {p00l}, {p01l}", + "adcs {p00h}, {p00h}, xzr", + "cset {p01l:w}, hs", + "adds {p10l}, {p00l}, {c}", + "adcs {p10h}, {p00h}, xzr", + "ccmp {p01l:w}, #0, #0, lo", + "csel {out_lo}, {p10l}, {p00l}, ne", + "csel {out_hi}, {p10h}, {p00h}, ne", + + a0 = in(reg) a[0], + a1 = in(reg) a[1], + b0 = in(reg) b[0], + b1 = in(reg) b[1], + c = in(reg) Self::C_LO, + p00l = out(reg) _, + p00h = out(reg) _, + p01l = out(reg) _, + p01h = out(reg) _, + p10l = out(reg) _, + p10h = out(reg) _, + p11l = out(reg) _, + p11h = out(reg) _, + out_lo = lateout(reg) out_lo, + out_hi = lateout(reg) out_hi, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } + + #[inline(always)] + fn sqr_wide(self) -> [u64; 4] { + let (a0, a1) = (self.0[0], self.0[1]); + let (p00_lo, p00_hi) = mul64_wide(a0, a0); + let (p01_lo, p01_hi) = mul64_wide(a0, a1); + let (p11_lo, p11_hi) = mul64_wide(a1, a1); + + let row1 = p00_hi as u128 + (p01_lo as u128) * 2; + let r0 = p00_lo; + let r1 = row1 as u64; + let carry1 = (row1 >> 64) as u64; + + let row2 = (p01_hi as u128) * 2 + p11_lo as u128 + carry1 as u128; + let r2 = row2 as u64; + let carry2 = (row2 >> 64) as u64; + + let row3 = p11_hi as u128 + carry2 as u128; + let r3 = row3 as u64; + debug_assert_eq!(row3 >> 64, 0); + + [r0, r1, r2, r3] + } + + #[inline(always)] + fn sqr_raw(a: [u64; 2]) -> [u64; 2] { + #[cfg(target_arch = "aarch64")] + { + Self::sqr_raw_aarch64(a) + } + + #[cfg(not(target_arch = "aarch64"))] + { + Self::sqr_raw_portable(a) + } + } + + #[cfg_attr( + target_arch = "aarch64", + expect( + dead_code, + reason = "target-specific helper is intentionally unused on some architectures" + ) + )] + #[inline(always)] + fn sqr_raw_portable(a: [u64; 2]) -> [u64; 2] { + let [r0, r1, r2, r3] = Self(a).sqr_wide(); + Self::reduce_4(r0, r1, r2, r3) + } + + /// 31-instruction AArch64 inline-asm squaring with Solinas reduction. + /// + /// Uses 3 widening multiplies (vs 4 for general mul) and doubles the + /// cross term via shifted-register operands. Same fold-1 + ccmp + /// canonicalize as `mul_raw_aarch64`. + #[cfg(target_arch = "aarch64")] + #[inline(always)] + fn sqr_raw_aarch64(a: [u64; 2]) -> [u64; 2] { + let out_lo: u64; + let out_hi: u64; + unsafe { + asm!( + // Squaring schoolbook: 3 widening muls + "mul {p00l}, {a0}, {a0}", + "umulh {p00h}, {a0}, {a0}", + "mul {p01l}, {a0}, {a1}", + "umulh {p01h}, {a0}, {a1}", + "mul {p11l}, {a1}, {a1}", + "umulh {p11h}, {a1}, {a1}", + + // Carry accumulation with doubled cross term + // row1 = p00h + 2*p01l, row2 = 2*p01h + p11l, r3 = p11h + carries + "lsr {t0}, {p01l}, #63", + "lsr {t1}, {p01h}, #63", + "adds {p01h}, {p11l}, {p01h}, lsl #1", + "cinc {t1}, {t1}, hs", + "adds {p00h}, {p00h}, {p01l}, lsl #1", + "adcs {p01h}, {p01h}, {t0}", + "adc {p11h}, {p11h}, {t1}", + + // At this point: r0=p00l, r1=p00h, r2=p01h, r3=p11h + + // Fold-1: [t0,t1,t2] = [r0,r1] + C·[r2,r3] + "mul {t0}, {p01h}, {c}", + "umulh {t1}, {p01h}, {c}", + "mul {p01l}, {p11h}, {c}", + "umulh {p11l}, {p11h}, {c}", + + "adds {p00l}, {p00l}, {t0}", + "adcs {p00h}, {p00h}, {t1}", + "cset {t0:w}, hs", + "adds {p00h}, {p00h}, {p01l}", + "adc {p11h}, {p11l}, {t0}", + + // Fold-2 + canonicalize via ccmp (C < 2^32 ⇒ C·t2 fits in 64 bits) + "mul {t0}, {p11h}, {c}", + "adds {p00l}, {p00l}, {t0}", + "adcs {p00h}, {p00h}, xzr", + "cset {t0:w}, hs", + "adds {t1}, {p00l}, {c}", + "adcs {p01l}, {p00h}, xzr", + "ccmp {t0:w}, #0, #0, lo", + "csel {out_lo}, {t1}, {p00l}, ne", + "csel {out_hi}, {p01l}, {p00h}, ne", + + a0 = in(reg) a[0], + a1 = in(reg) a[1], + c = in(reg) Self::C_LO, + p00l = out(reg) _, + p00h = out(reg) _, + p01l = out(reg) _, + p01h = out(reg) _, + p11l = out(reg) _, + p11h = out(reg) _, + t0 = out(reg) _, + t1 = out(reg) _, + out_lo = lateout(reg) out_lo, + out_hi = lateout(reg) out_hi, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } + + /// Squaring, equivalent to `self * self`. + #[inline(always)] + pub fn square(self) -> Self { + Self(Self::sqr_raw(self.0)) + } + + /// Fused multiply-add, equivalent to `self * rhs + addend`. + /// + /// This widens the product, adds the canonical addend before reduction, + /// and performs a single final Solinas reduction. + #[inline(always)] + pub fn mul_add(self, rhs: Self, addend: Self) -> Self { + Self(Self::mul_add_raw(self.0, rhs.0, addend.0)) + } + + pub(super) fn pow_u128(self, mut exp: u128) -> Self { + let mut base = self; + let mut acc = Self::one(); + while exp > 0 { + if (exp & 1) == 1 { + acc *= base; + } + base = Self(Self::sqr_raw(base.0)); + exp >>= 1; + } + acc + } +} diff --git a/crates/jolt-field/src/prime/fp128/primes.rs b/crates/jolt-field/src/prime/fp128/primes.rs new file mode 100644 index 0000000000..240cf1e3c2 --- /dev/null +++ b/crates/jolt-field/src/prime/fp128/primes.rs @@ -0,0 +1,49 @@ +use super::*; + +/// `p = 2^128 − 275` (C = 275). +pub type Prime128Offset275 = Fp128<0xfffffffffffffffffffffffffffffeed>; +/// `p = 2^128 − 159` (C = 159). Split-NTT-only helper prime. +pub type Prime128Offset159 = Fp128<0xffffffffffffffffffffffffffffff61>; +/// `p = 2^128 − 2355` (C = 2355, p ≡ 5 mod 8). +/// +/// Smooth multiplicative subgroup of order 14700 = 2² × 3 × 5² × 7², +/// supporting mixed-radix FFT up to size 14700 (e.g. 1470 = 2·3·5·7² +/// for RS encoding with 256+1024 ≥ 1280 evaluations). +/// +/// Factorization: `p − 1 = 2² · 3 · 5² · 7² · 701 · 2955365183 · 11173595356596918495491`. +pub type Prime128Offset2355 = Fp128<0xfffffffffffffffffffffffffffff6cd>; + +impl SmoothFftField for Prime128Offset2355 { + const SMOOTH_SUBGROUP_ORDER: usize = 14_700; + /// `2 ^ ((p − 1) / 14_700)` where `g = 2` is a primitive root of `p`. + /// Verified by `prime_2355_tests::smooth_omega_matches_search` in + /// `src/fft.rs`. + const SMOOTH_OMEGA: u128 = 0x2ecd_18d0_8238_2c0c_818c_c05f_446a_8075; +} + +/// `p = 2^128 − 2^32 + 22537` (C = 2^32 − 22537 = 0xFFFFA7F7). +/// +/// Solinas-form prime sharing the same CPU reduction cost as +/// `Prime128Offset2355` on x86_64 / AArch64 (both go through the generic +/// 32-bit-C `mul_c_wide` path; neither C is of the form `2^a ± 1`). The +/// multiplicative group contains a smooth subgroup of order +/// `2^3 · 3^7 = 17 496` with a pure radix-3 subgroup of order +/// `3^7 = 2187`, enabling a low-mul mixed-radix FFT. +/// +/// Factorization of `p − 1` includes `2^3 · 3^7 · 19 · 41 · 459 647 · …`. +/// +/// Subgroup sizes available for FFT-based RS encoding include +/// `1458 = 2 · 3^6`, `2187 = 3^7`, `4374 = 2 · 3^7`, `8748 = 2^2 · 3^7`, +/// and the full `17 496 = 2^3 · 3^7`. +pub type Prime128OffsetA7F7 = Fp128<0xffffffffffffffffffffffff00005809>; + +impl SmoothFftField for Prime128OffsetA7F7 { + const SMOOTH_SUBGROUP_ORDER: usize = 17_496; + /// `g ^ ((p − 1) / 17_496)` where `g` is the smallest primitive root + /// found by `find_primitive_nth_root` (note: `g = 2` is a quadratic + /// residue mod `p` and therefore *not* a primitive root, so the + /// scanner falls through to the next candidate). Verified by + /// `prime_a7f7_tests::smooth_omega_matches_search` in + /// `src/fft.rs`. + const SMOOTH_OMEGA: u128 = 0x4e9f_650b_7003_d201_9945_e1da_c47c_8b18; +} diff --git a/crates/jolt-field/src/prime/fp128/reduce.rs b/crates/jolt-field/src/prime/fp128/reduce.rs new file mode 100644 index 0000000000..242edf9d84 --- /dev/null +++ b/crates/jolt-field/src/prime/fp128/reduce.rs @@ -0,0 +1,195 @@ +use super::*; + +impl Fp128

{ + /// +1 means `C = 2^a + 1`, -1 means `C = 2^a - 1`, 0 means generic. + const C_SHIFT_KIND: i8 = { + let c = Self::C_LO; + if c > 1 && is_pow2_u64(c - 1) { + 1 + } else if c == u64::MAX || is_pow2_u64(c + 1) { + -1 + } else { + 0 + } + }; + const C_SHIFT: u32 = { + let c = Self::C_LO; + if Self::C_SHIFT_KIND == 1 { + log2_pow2_u64(c - 1) + } else if Self::C_SHIFT_KIND == -1 { + if c == u64::MAX { + 64 + } else { + log2_pow2_u64(c + 1) + } + } else { + 0 + } + }; + + /// Multiply by `C = 2^128 - P`. For `C = 2^a ± 1`, this is shift/add or + /// shift/sub only; otherwise it falls back to generic widening multiply. + #[inline(always)] + fn mul_c_wide(x: u64) -> (u64, u64) { + if Self::C_SHIFT_KIND == 1 { + let v = ((x as u128) << Self::C_SHIFT) + x as u128; + (v as u64, (v >> 64) as u64) + } else if Self::C_SHIFT_KIND == -1 { + let v = ((x as u128) << Self::C_SHIFT) - x as u128; + (v as u64, (v >> 64) as u64) + } else { + mul64_wide(Self::C_LO, x) + } + } + + /// Fold 2 + canonicalize: reduce `[t0, t1] + t2·2^128` into `[0, p)`. + /// + /// Correctness argument for the fused overflow+canonicalize: + /// + /// Let `v = base + C·t2` (mathematical, not mod 2^128). + /// From the fold-1 mac chain, `t2 ≤ C`, so `C·t2 ≤ C²`. + /// + /// - **No overflow** (`v < 2^128`): `s = v`, and the standard + /// canonicalize applies — `s + C` carries iff `s ≥ P`. + /// - **Overflow** (`v ≥ 2^128`): `s = v − 2^128`, so `s < C·t2 ≤ C²`. + /// The correct reduced value is `s + C` (since `2^128 ≡ C mod P`). + /// Because `s + C < C² + C = C(C+1)` and `C(C+1) < P` for all + /// `C < 2^64`, the value `s + C` is already in `[0, P)` — no + /// further canonicalization is needed, and `s + C < 2^128` so the + /// add does NOT carry. + /// + /// Therefore `if (overflow | carry) { s + C } else { s }` is correct + /// in both cases, fusing the overflow correction with canonicalization. + #[inline(always)] + fn fold2_canonicalize(t0: u64, t1: u64, t2: u64) -> [u64; 2] { + let (ct2_lo, ct2_hi) = Self::mul_c_wide(t2); + + let (s0, carry0) = t0.overflowing_add(ct2_lo); + let (s1a, carry1a) = t1.overflowing_add(ct2_hi); + let (s1, carry1b) = s1a.overflowing_add(carry0 as u64); + let overflow = carry1a | carry1b; + + let (r0, carry2) = s0.overflowing_add(Self::C_LO); + let (r1, carry3) = s1.overflowing_add(carry2 as u64); + + pack( + if overflow | carry3 { r0 } else { s0 }, + if overflow | carry3 { r1 } else { s1 }, + ) + } + + /// Solinas fold for exactly 4 limbs: `[r0,r1] + C·[r2,r3]` → 3 limbs, + /// then `fold2_canonicalize`. + #[inline(always)] + pub(super) fn reduce_4(r0: u64, r1: u64, r2: u64, r3: u64) -> [u64; 2] { + let (cr2_lo, cr2_hi) = Self::mul_c_wide(r2); + let (cr3_lo, cr3_hi) = Self::mul_c_wide(r3); + + let t0_sum = r0 as u128 + cr2_lo as u128; + let t0 = t0_sum as u64; + let carryf = (t0_sum >> 64) as u64; + + let t1_sum = r1 as u128 + cr2_hi as u128 + cr3_lo as u128 + carryf as u128; + let t1 = t1_sum as u64; + + let t2_sum = cr3_hi as u128 + (t1_sum >> 64); + let t2 = t2_sum as u64; + debug_assert_eq!(t2_sum >> 64, 0); + + Self::fold2_canonicalize(t0, t1, t2) + } + + /// Add a canonical 128-bit value into a 256-bit little-endian limb array. + /// + /// Since both multiplicands and addends are canonical field elements, + /// `a * b + c < 2^256`, so the top carry is guaranteed to be zero. + #[inline(always)] + pub(super) fn add_128_into_256(prod: [u64; 4], addend: [u64; 2]) -> [u64; 4] { + let (s0, carry0) = prod[0].overflowing_add(addend[0]); + let (s1a, carry1a) = prod[1].overflowing_add(addend[1]); + let (s1, carry1b) = s1a.overflowing_add(carry0 as u64); + let carry1 = carry1a | carry1b; + let (s2, carry2) = prod[2].overflowing_add(carry1 as u64); + let (s3, carry3) = prod[3].overflowing_add(carry2 as u64); + debug_assert!(!carry3); + [s0, s1, s2, s3] + } + + /// Reduce an arbitrary-width little-endian limb array to a canonical + /// field element via iterated Solinas folding. + /// + /// Each fold splits at the 128-bit boundary and replaces + /// `hi · 2^128` with `hi · C`, reducing width by one limb per + /// iteration. Supports 0–10 input limbs (up to 640 bits). + /// + /// # Panics + /// + /// Panics if `limbs.len() > 10`. + #[inline(always)] + pub fn solinas_reduce(limbs: &[u64]) -> Self { + match limbs.len() { + 0 => Self::zero(), + 1 => Self(pack(limbs[0], 0)), + 2 => Self::from_canonical_u128_reduced(to_u128([limbs[0], limbs[1]])), + 3 => Self(Self::fold2_canonicalize(limbs[0], limbs[1], limbs[2])), + 4 => Self(Self::reduce_4(limbs[0], limbs[1], limbs[2], limbs[3])), + 5 => { + let (l0, l1, l2, l3, l4) = (limbs[0], limbs[1], limbs[2], limbs[3], limbs[4]); + let (c2_lo, c2_hi) = Self::mul_c_wide(l2); + let (c3_lo, c3_hi) = Self::mul_c_wide(l3); + let (c4_lo, c4_hi) = Self::mul_c_wide(l4); + + let s0 = l0 as u128 + c2_lo as u128; + let s1 = l1 as u128 + c2_hi as u128 + c3_lo as u128 + (s0 >> 64); + let s2 = c3_hi as u128 + c4_lo as u128 + (s1 >> 64); + let s3 = c4_hi as u128 + (s2 >> 64); + debug_assert_eq!(s3 >> 64, 0); + + Self(Self::reduce_4(s0 as u64, s1 as u64, s2 as u64, s3 as u64)) + } + n => { + assert!(n <= 10, "solinas_reduce supports at most 10 limbs"); + let mut buf = [0u64; 11]; + buf[..n].copy_from_slice(limbs); + let mut len = n; + let c = Self::C_LO; + + while len > 5 { + let high_len = len - 2; + let mut next = [0u64; 11]; + + let mut carry: u64 = 0; + for i in 0..high_len { + let wide = c as u128 * buf[i + 2] as u128 + carry as u128; + next[i] = wide as u64; + carry = (wide >> 64) as u64; + } + next[high_len] = carry; + + let s0 = next[0] as u128 + buf[0] as u128; + next[0] = s0 as u64; + let s1 = next[1] as u128 + buf[1] as u128 + (s0 >> 64); + next[1] = s1 as u64; + let mut c_out = (s1 >> 64) as u64; + for limb in &mut next[2..=high_len] { + if c_out == 0 { + break; + } + let s = *limb as u128 + c_out as u128; + *limb = s as u64; + c_out = (s >> 64) as u64; + } + debug_assert_eq!(c_out, 0); + + buf = next; + len -= 1; + while len > 5 && buf[len - 1] == 0 { + len -= 1; + } + } + + Self::solinas_reduce(&buf[..len]) + } + } + } +} diff --git a/crates/jolt-field/src/prime/fp128/tests.rs b/crates/jolt-field/src/prime/fp128/tests.rs new file mode 100644 index 0000000000..9626b09a86 --- /dev/null +++ b/crates/jolt-field/src/prime/fp128/tests.rs @@ -0,0 +1,194 @@ +use super::*; +use crate::{PseudoMersenneField, RandomSampling}; +use rand::rngs::StdRng; +use rand::SeedableRng; +use rand_core::RngCore; + +type F = Prime128Offset275; + +#[test] +fn to_limbs_roundtrip() { + let mut rng = StdRng::seed_from_u64(0xdead_beef_cafe_1234); + for _ in 0..1000 { + let a: F = RandomSampling::random(&mut rng); + assert_eq!(Fp128(a.to_limbs()), a); + } +} + +#[test] +fn mul_wide_u64_matches_full_mul() { + let mut rng = StdRng::seed_from_u64(0x1122_3344_5566_7788); + for _ in 0..1000 { + let a: F = RandomSampling::random(&mut rng); + let b = rng.next_u64(); + let expected = a * F::from_u64(b); + let reduced = F::solinas_reduce(&a.mul_wide_u64(b)); + assert_eq!(reduced, expected); + } +} + +#[test] +fn mul_wide_matches_full_mul() { + let mut rng = StdRng::seed_from_u64(0xaabb_ccdd_eeff_0011); + for _ in 0..1000 { + let a: F = RandomSampling::random(&mut rng); + let b: F = RandomSampling::random(&mut rng); + let expected = a * b; + let reduced = F::solinas_reduce(&a.mul_wide(b)); + assert_eq!(reduced, expected); + } +} + +#[test] +fn mul_add_matches_mul_then_add() { + let mut rng = StdRng::seed_from_u64(0x3141_5926_5358_9793); + for _ in 0..1000 { + let a: F = RandomSampling::random(&mut rng); + let b: F = RandomSampling::random(&mut rng); + let c: F = RandomSampling::random(&mut rng); + assert_eq!(a.mul_add(b, c), a * b + c); + } + + let near = -F::one(); + assert_eq!(near.mul_add(near, near), near * near + near); +} + +#[test] +fn mul_wide_u128_matches_full_mul() { + let mut rng = StdRng::seed_from_u64(0x9988_7766_5544_3322); + for _ in 0..1000 { + let a: F = RandomSampling::random(&mut rng); + let b = rng.next_u64() as u128 | ((rng.next_u64() as u128) << 64); + let expected = a * F::from_canonical_u128_reduced(b); + let reduced = F::solinas_reduce(&a.mul_wide_u128(b)); + assert_eq!(reduced, expected); + } +} + +#[test] +fn mul_wide_limbs_roundtrips_through_reduction() { + let mut rng = StdRng::seed_from_u64(0x1bad_f00d_0ddc_afe1); + for _ in 0..1000 { + let a: F = RandomSampling::random(&mut rng); + let b3 = [rng.next_u64(), rng.next_u64(), rng.next_u64()]; + let b4 = [ + rng.next_u64(), + rng.next_u64(), + rng.next_u64(), + rng.next_u64(), + ]; + + let got3_full = a.mul_wide_limbs::<3, 5>(b3); + let got3_trunc = a.mul_wide_limbs::<3, 4>(b3); + assert_eq!( + got3_trunc, + [got3_full[0], got3_full[1], got3_full[2], got3_full[3]] + ); + let exp3 = a * F::solinas_reduce(&b3); + assert_eq!(F::solinas_reduce(&got3_full), exp3); + + let got4_full = a.mul_wide_limbs::<4, 6>(b4); + let got4_trunc = a.mul_wide_limbs::<4, 4>(b4); + assert_eq!( + got4_trunc, + [got4_full[0], got4_full[1], got4_full[2], got4_full[3]] + ); + let exp4 = a * F::solinas_reduce(&b4); + assert_eq!(F::solinas_reduce(&got4_full), exp4); + } +} + +#[test] +fn solinas_reduce_small_inputs() { + assert_eq!(F::solinas_reduce(&[]), F::zero()); + assert_eq!(F::solinas_reduce(&[42]), F::from_u64(42)); + let one_shifted = F::from_canonical_u128_reduced(1u128 << 64); + assert_eq!(F::solinas_reduce(&[0, 1]), one_shifted); +} + +#[test] +fn solinas_reduce_4_limbs_max() { + // 2^256 - 1 ≡ C² - 1 (mod P), since 2^128 ≡ C + let c = F::from_canonical_u128_reduced(::MODULUS_OFFSET); + let expected = c * c - F::one(); + assert_eq!(F::solinas_reduce(&[u64::MAX; 4]), expected); +} + +#[test] +fn solinas_reduce_9_limbs() { + // 1 + 2^512 = 1 + (2^128)^4 ≡ 1 + C^4 + let c = F::from_canonical_u128_reduced(::MODULUS_OFFSET); + let expected = F::one() + c * c * c * c; + assert_eq!(F::solinas_reduce(&[1, 0, 0, 0, 0, 0, 0, 0, 1]), expected); +} + +#[test] +fn solinas_reduce_accumulated_products() { + let mut rng = StdRng::seed_from_u64(0xfeed_face_0bad_c0de); + let mut acc = [0u64; 5]; + let mut expected = F::zero(); + + for _ in 0..200 { + let a: F = RandomSampling::random(&mut rng); + let b = rng.next_u64(); + let wide = a.mul_wide_u64(b); + + let mut carry: u64 = 0; + for j in 0..5 { + let addend = if j < 3 { wide[j] } else { 0 }; + let sum = acc[j] as u128 + addend as u128 + carry as u128; + acc[j] = sum as u64; + carry = (sum >> 64) as u64; + } + assert_eq!(carry, 0); + expected += a * F::from_u64(b); + } + + assert_eq!(F::solinas_reduce(&acc), expected); +} + +#[test] +fn solinas_reduce_cross_prime() { + type G = Prime128Offset275; + let c = G::from_canonical_u128_reduced(::MODULUS_OFFSET); + let expected = c * c - G::one(); + assert_eq!(G::solinas_reduce(&[u64::MAX; 4]), expected); +} + +#[test] +fn from_i64_handles_min_without_overflow() { + let x = F::from_i64(i64::MIN); + let y = F::from_u64(i64::MIN.unsigned_abs()); + assert_eq!(x + y, F::zero()); +} + +#[test] +fn prime128_offset_a7f7_constants() { + // p = 2^128 − 2^32 + 22537, so C = 2^32 − 22537 = 0xFFFFA7F7. + assert_eq!( + ::MODULUS_OFFSET, + 0xFFFFA7F7, + ); + assert_eq!(Prime128OffsetA7F7::C, 0xFFFFA7F7); + assert_eq!(Prime128OffsetA7F7::C_LO, 0xFFFFA7F7); + // Round-trip through the field arithmetic: p ≡ 0 (mod p), so + // Fp(2^128 − C) + Fp(C) = 0. + let neg_c = -Prime128OffsetA7F7::from_canonical_u128_reduced(0xFFFFA7F7); + assert_eq!( + neg_c + Prime128OffsetA7F7::from_canonical_u128_reduced(0xFFFFA7F7), + Prime128OffsetA7F7::zero() + ); +} + +#[test] +fn prime128_offset_a7f7_mul_wide_matches_full_mul() { + type G = Prime128OffsetA7F7; + let mut rng = StdRng::seed_from_u64(0xa7f7_a7f7_a7f7_a7f7); + for _ in 0..1000 { + let a: G = RandomSampling::random(&mut rng); + let b: G = RandomSampling::random(&mut rng); + let expected = a * b; + let reduced = G::solinas_reduce(&a.mul_wide(b)); + assert_eq!(reduced, expected); + } +} diff --git a/crates/jolt-field/src/prime/fp128/traits.rs b/crates/jolt-field/src/prime/fp128/traits.rs new file mode 100644 index 0000000000..819b4a333e --- /dev/null +++ b/crates/jolt-field/src/prime/fp128/traits.rs @@ -0,0 +1,186 @@ +use super::*; + +impl Add for Fp128

{ + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self::Output { + Self(Self::add_raw(self.0, rhs.0)) + } +} + +impl Sub for Fp128

{ + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + Self(Self::sub_raw(self.0, rhs.0)) + } +} + +impl Mul for Fp128

{ + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Self(Self::mul_raw(self.0, rhs.0)) + } +} + +impl Neg for Fp128

{ + type Output = Self; + #[inline] + fn neg(self) -> Self::Output { + Self(Self::sub_raw(pack(0, 0), self.0)) + } +} + +impl AddAssign for Fp128

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for Fp128

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for Fp128

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl<'a, const P: u128> Add<&'a Self> for Fp128

{ + type Output = Self; + #[inline] + fn add(self, rhs: &'a Self) -> Self::Output { + self + *rhs + } +} + +impl<'a, const P: u128> Sub<&'a Self> for Fp128

{ + type Output = Self; + #[inline] + fn sub(self, rhs: &'a Self) -> Self::Output { + self - *rhs + } +} + +impl<'a, const P: u128> Mul<&'a Self> for Fp128

{ + type Output = Self; + #[inline] + fn mul(self, rhs: &'a Self) -> Self::Output { + self * *rhs + } +} + +impl Invertible for Fp128

{ + #[inline(always)] + fn inverse(&self) -> Option { + let inv = self.inv_or_zero(); + if self.is_zero() { + None + } else { + Some(inv) + } + } + + #[inline(always)] + fn inv_or_zero(self) -> Self { + let candidate = self.pow_u128(P.wrapping_sub(2)); + let v = to_u128(self.0); + let nz = ((v | v.wrapping_neg()) >> 127) & 1; + let mask = 0u128.wrapping_sub(nz); + let masked = to_u128(candidate.0) & mask; + Self(from_u128(masked)) + } +} + +impl HalvingField for Fp128

{ + #[inline] + fn half(self) -> Self { + let x = to_u128(self.0); + let half = (x >> 1) + (x & 1) * ((P >> 1) + 1); + Self(from_u128(half)) + } +} + +impl RandomSampling for Fp128

{ + #[inline(always)] + fn random(rng: &mut R) -> Self { + loop { + let lo = rng.next_u64(); + let hi = rng.next_u64(); + let x = lo as u128 | (hi as u128) << 64; + if x < P { + return Self(pack(lo, hi)); + } + } + } +} + +impl FromPrimitiveInt for Fp128

{ + #[inline(always)] + fn from_u64(val: u64) -> Self { + // For Fp128 pseudo-Mersenne primes, p = 2^128 - c with c < 2^64. + // Therefore any u64 is always canonical (< p), so this can be a + // direct limb construction with no reduction path. + Self::from_u64(val) + } + + #[inline(always)] + fn from_i64(val: i64) -> Self { + Self::from_i64(val) + } + + #[inline(always)] + fn from_u128(val: u128) -> Self { + Self::from_canonical_u128_reduced(val) + } + + #[inline(always)] + fn from_i128(val: i128) -> Self { + if val >= 0 { + Self::from_u128(val as u128) + } else { + -Self::from_u128(val.unsigned_abs()) + } + } +} + +impl BalancedDigitLookup for Fp128

{ + fn digit_lut(log_basis: u32) -> [Self; 64] { + Self::digit_lut(log_basis) + } +} + +impl CanonicalField for Fp128

{ + fn to_canonical_u128(self) -> u128 { + to_u128(self.0) + } + + fn modulus_bits() -> u32 { + u128::BITS - P.leading_zeros() + } + + fn from_canonical_u128_checked(val: u128) -> Option { + if val < P { + Some(Self(from_u128(val))) + } else { + None + } + } + + fn from_canonical_u128_reduced(val: u128) -> Self { + let (sub, borrow) = val.overflowing_sub(P); + Self(from_u128(if borrow { val } else { sub })) + } +} + +impl PseudoMersenneField for Fp128

{ + const MODULUS_BITS: u32 = 128; + const MODULUS_OFFSET: u128 = Self::C; +} diff --git a/crates/jolt-field/src/prime/fp128/wide.rs b/crates/jolt-field/src/prime/fp128/wide.rs new file mode 100644 index 0000000000..a797f4d93b --- /dev/null +++ b/crates/jolt-field/src/prime/fp128/wide.rs @@ -0,0 +1,302 @@ +use super::*; + +impl Fp128

{ + /// Extract the canonical `[lo, hi]` limb representation. + #[inline(always)] + pub fn to_limbs(self) -> [u64; 2] { + self.0 + } + + /// 128×64 → 192-bit widening multiply, **no reduction**. + /// + /// Returns `[lo, mid, hi]` representing `self · other` as a 192-bit + /// integer. Cost: 2 widening `mul64`. + #[inline(always)] + pub fn mul_wide_u64(self, other: u64) -> [u64; 3] { + let (a0, a1) = (self.0[0], self.0[1]); + let (p0_lo, p0_hi) = mul64_wide(a0, other); + let (p1_lo, p1_hi) = mul64_wide(a1, other); + let mid = p0_hi as u128 + p1_lo as u128; + let hi = p1_hi + (mid >> 64) as u64; + [p0_lo, mid as u64, hi] + } + + /// 128×128 → 256-bit widening multiply, **no reduction**. + /// + /// Returns `[r0, r1, r2, r3]` representing `self · other` as a 256-bit + /// integer. This is the schoolbook 2×2 portion of the Solinas multiply, + /// without the reduction fold. Cost: 4 widening `mul64`. + #[inline(always)] + pub fn mul_wide(self, other: Self) -> [u64; 4] { + let (a0, a1) = (self.0[0], self.0[1]); + let (b0, b1) = (other.0[0], other.0[1]); + let (p00_lo, p00_hi) = mul64_wide(a0, b0); + let (p01_lo, p01_hi) = mul64_wide(a0, b1); + let (p10_lo, p10_hi) = mul64_wide(a1, b0); + let (p11_lo, p11_hi) = mul64_wide(a1, b1); + + let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; + let r0 = p00_lo; + let r1 = row1 as u64; + let carry1 = (row1 >> 64) as u64; + + let row2 = p01_hi as u128 + p10_hi as u128 + p11_lo as u128 + carry1 as u128; + let r2 = row2 as u64; + let carry2 = (row2 >> 64) as u64; + + let row3 = p11_hi as u128 + carry2 as u128; + let r3 = row3 as u64; + debug_assert_eq!(row3 >> 64, 0); + + [r0, r1, r2, r3] + } + + /// 128×128 → 256-bit widening multiply with a raw `u128` operand, + /// **no reduction**. + #[inline(always)] + pub fn mul_wide_u128(self, other: u128) -> [u64; 4] { + self.mul_wide(Self(from_u128(other))) + } + + /// 128×(64*M) → (64*OUT) widening multiply, **no reduction**. + /// + /// Multiplies a canonical Fp128 value (`[u64; 2]`) by an arbitrary + /// little-endian limb array and returns the little-endian product + /// truncated/extended to `OUT` limbs. + #[inline(always)] + pub fn mul_wide_limbs(self, other: [u64; M]) -> [u64; OUT] { + let (a0, a1) = (self.0[0], self.0[1]); + + // Hot-path specializations used by Jolt (M in {3,4}, OUT in {4,5}). + // These avoid loop/control-flow overhead in tight sumcheck FMAs. + if M == 3 && OUT == 5 { + let b0 = other[0]; + let b1 = other[1]; + let b2 = other[2]; + + let (p00_lo, p00_hi) = mul64_wide(a0, b0); + let (p01_lo, p01_hi) = mul64_wide(a0, b1); + let (p02_lo, p02_hi) = mul64_wide(a0, b2); + let (p10_lo, p10_hi) = mul64_wide(a1, b0); + let (p11_lo, p11_hi) = mul64_wide(a1, b1); + let (p12_lo, p12_hi) = mul64_wide(a1, b2); + + let r0 = p00_lo; + + let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; + let r1 = row1 as u64; + let carry1 = row1 >> 64; + + let row2 = p01_hi as u128 + p02_lo as u128 + p10_hi as u128 + p11_lo as u128 + carry1; + let r2 = row2 as u64; + let carry2 = row2 >> 64; + + let row3 = p02_hi as u128 + p11_hi as u128 + p12_lo as u128 + carry2; + let r3 = row3 as u64; + let carry3 = row3 >> 64; + + let row4 = p12_hi as u128 + carry3; + let r4 = row4 as u64; + debug_assert_eq!(row4 >> 64, 0); + + let mut out = [0u64; OUT]; + out[0] = r0; + out[1] = r1; + out[2] = r2; + out[3] = r3; + out[4] = r4; + return out; + } + if M == 3 && OUT == 4 { + let b0 = other[0]; + let b1 = other[1]; + let b2 = other[2]; + + let (p00_lo, p00_hi) = mul64_wide(a0, b0); + let (p01_lo, p01_hi) = mul64_wide(a0, b1); + let (p02_lo, p02_hi) = mul64_wide(a0, b2); + let (p10_lo, p10_hi) = mul64_wide(a1, b0); + let (p11_lo, p11_hi) = mul64_wide(a1, b1); + let p12_lo = a1.wrapping_mul(b2); + + let r0 = p00_lo; + + let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; + let r1 = row1 as u64; + let carry1 = row1 >> 64; + + let row2 = p01_hi as u128 + p02_lo as u128 + p10_hi as u128 + p11_lo as u128 + carry1; + let r2 = row2 as u64; + let carry2 = row2 >> 64; + + let row3 = p02_hi as u128 + p11_hi as u128 + p12_lo as u128 + carry2; + let r3 = row3 as u64; + + let mut out = [0u64; OUT]; + out[0] = r0; + out[1] = r1; + out[2] = r2; + out[3] = r3; + return out; + } + if M == 4 && OUT == 6 { + let b0 = other[0]; + let b1 = other[1]; + let b2 = other[2]; + let b3 = other[3]; + + let (p00_lo, p00_hi) = mul64_wide(a0, b0); + let (p01_lo, p01_hi) = mul64_wide(a0, b1); + let (p02_lo, p02_hi) = mul64_wide(a0, b2); + let (p03_lo, p03_hi) = mul64_wide(a0, b3); + let (p10_lo, p10_hi) = mul64_wide(a1, b0); + let (p11_lo, p11_hi) = mul64_wide(a1, b1); + let (p12_lo, p12_hi) = mul64_wide(a1, b2); + let (p13_lo, p13_hi) = mul64_wide(a1, b3); + + let r0 = p00_lo; + + let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; + let r1 = row1 as u64; + let carry1 = row1 >> 64; + + let row2 = p01_hi as u128 + p02_lo as u128 + p10_hi as u128 + p11_lo as u128 + carry1; + let r2 = row2 as u64; + let carry2 = row2 >> 64; + + let row3 = p02_hi as u128 + p03_lo as u128 + p11_hi as u128 + p12_lo as u128 + carry2; + let r3 = row3 as u64; + let carry3 = row3 >> 64; + + let row4 = p03_hi as u128 + p12_hi as u128 + p13_lo as u128 + carry3; + let r4 = row4 as u64; + let carry4 = row4 >> 64; + + let row5 = p13_hi as u128 + carry4; + let r5 = row5 as u64; + debug_assert_eq!(row5 >> 64, 0); + + let mut out = [0u64; OUT]; + out[0] = r0; + out[1] = r1; + out[2] = r2; + out[3] = r3; + out[4] = r4; + out[5] = r5; + return out; + } + if M == 4 && OUT == 5 { + let b0 = other[0]; + let b1 = other[1]; + let b2 = other[2]; + let b3 = other[3]; + + let (p00_lo, p00_hi) = mul64_wide(a0, b0); + let (p01_lo, p01_hi) = mul64_wide(a0, b1); + let (p02_lo, p02_hi) = mul64_wide(a0, b2); + let (p03_lo, p03_hi) = mul64_wide(a0, b3); + let (p10_lo, p10_hi) = mul64_wide(a1, b0); + let (p11_lo, p11_hi) = mul64_wide(a1, b1); + let (p12_lo, p12_hi) = mul64_wide(a1, b2); + let p13_lo = a1.wrapping_mul(b3); + + let r0 = p00_lo; + + let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; + let r1 = row1 as u64; + let carry1 = row1 >> 64; + + let row2 = p01_hi as u128 + p02_lo as u128 + p10_hi as u128 + p11_lo as u128 + carry1; + let r2 = row2 as u64; + let carry2 = row2 >> 64; + + let row3 = p02_hi as u128 + p03_lo as u128 + p11_hi as u128 + p12_lo as u128 + carry2; + let r3 = row3 as u64; + let carry3 = row3 >> 64; + + let row4 = p03_hi as u128 + p12_hi as u128 + p13_lo as u128 + carry3; + let r4 = row4 as u64; + + let mut out = [0u64; OUT]; + out[0] = r0; + out[1] = r1; + out[2] = r2; + out[3] = r3; + out[4] = r4; + return out; + } + if M == 4 && OUT == 4 { + let b0 = other[0]; + let b1 = other[1]; + let b2 = other[2]; + let b3 = other[3]; + + let (p00_lo, p00_hi) = mul64_wide(a0, b0); + let (p01_lo, p01_hi) = mul64_wide(a0, b1); + let (p02_lo, p02_hi) = mul64_wide(a0, b2); + let p03_lo = a0.wrapping_mul(b3); + let (p10_lo, p10_hi) = mul64_wide(a1, b0); + let (p11_lo, p11_hi) = mul64_wide(a1, b1); + let p12_lo = a1.wrapping_mul(b2); + + let r0 = p00_lo; + + let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; + let r1 = row1 as u64; + let carry1 = row1 >> 64; + + let row2 = p01_hi as u128 + p02_lo as u128 + p10_hi as u128 + p11_lo as u128 + carry1; + let r2 = row2 as u64; + let carry2 = row2 >> 64; + + let row3 = p02_hi as u128 + p03_lo as u128 + p11_hi as u128 + p12_lo as u128 + carry2; + let r3 = row3 as u64; + + let mut out = [0u64; OUT]; + out[0] = r0; + out[1] = r1; + out[2] = r2; + out[3] = r3; + return out; + } + + let mut out = [0u64; OUT]; + + for (i, &b) in other.iter().enumerate() { + if i >= OUT { + break; + } + + let (p0_lo, p0_hi) = mul64_wide(a0, b); + let (p1_lo, p1_hi) = mul64_wide(a1, b); + + let s0 = out[i] as u128 + p0_lo as u128; + out[i] = s0 as u64; + let mut carry = s0 >> 64; + + if i + 1 >= OUT { + continue; + } + let s1 = out[i + 1] as u128 + p0_hi as u128 + p1_lo as u128 + carry; + out[i + 1] = s1 as u64; + carry = s1 >> 64; + + if i + 2 >= OUT { + continue; + } + let s2 = out[i + 2] as u128 + p1_hi as u128 + carry; + out[i + 2] = s2 as u64; + + let mut carry_hi = s2 >> 64; + let mut j = i + 3; + while carry_hi != 0 && j < OUT { + let sj = out[j] as u128 + carry_hi; + out[j] = sj as u64; + carry_hi = sj >> 64; + j += 1; + } + } + + out + } +} diff --git a/crates/jolt-field/src/prime/fp32.rs b/crates/jolt-field/src/prime/fp32.rs new file mode 100644 index 0000000000..ca486f5573 --- /dev/null +++ b/crates/jolt-field/src/prime/fp32.rs @@ -0,0 +1,631 @@ +//! Prime field for primes of the form `p = 2^k − c` with `c` small, backed +//! by `u32` storage. +//! +//! Uses Solinas-style two-fold reduction: the offset `c` and fold point `k` +//! are computed at compile time from the const-generic modulus `P`. + +use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +use crate::{FromPrimitiveInt, Invertible, RandomSampling}; +use rand_core::RngCore; + +use crate::{BalancedDigitLookup, CanonicalField, HalvingField, PseudoMersenneField}; + +/// Prime field element for primes `p = 2^k − c` stored as `u32`. +/// +/// The fold point `k` and offset `c = 2^k − p` are computed at compile time +/// from the const-generic `P`. Instantiating with a modulus that does not +/// satisfy the prime Solinas conditions is a compile-time error. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Fp32(pub(crate) u32); + +impl Fp32

{ + /// Fold point: smallest `k` such that `P ≤ 2^k`. + const BITS: u32 = 32 - P.leading_zeros(); + + /// Offset `c = 2^k − P`. + pub const C: u32 = { + let c = if Self::BITS == 32 { + 0u32.wrapping_sub(P) + } else { + (1u32 << Self::BITS) - P + }; + assert!(P != 0, "modulus must be nonzero"); + assert!(P & 1 == 1, "modulus must be odd"); + assert!(Self::is_prime_modulus(P), "modulus must be prime"); + assert!( + (c as u64) * (c as u64 + 1) < P as u64, + "C(C+1) < P required for fused canonicalize" + ); + c + }; + + const fn is_prime_modulus(n: u32) -> bool { + if n < 2 { + return false; + } + if n.is_multiple_of(2) { + return n == 2; + } + let mut d = 3u32; + while (d as u64) * (d as u64) <= n as u64 { + if n.is_multiple_of(d) { + return false; + } + d += 2; + } + true + } + + /// Mask for extracting the low `BITS` bits from a u64. + const MASK: u64 = if Self::BITS == 32 { + u32::MAX as u64 + } else { + (1u64 << Self::BITS) - 1 + }; + + pub(crate) const SHIFT64_MOD_P: u32 = { + let c = Self::C as u128; + let bits = Self::BITS; + let mask = if bits == 32 { + u32::MAX as u128 + } else { + (1u128 << bits) - 1 + }; + let mut v = 1u128 << 64; + while v >> bits != 0 { + v = (v & mask) + c * (v >> bits); + } + let reduced = (v as u64).wrapping_sub(P as u64); + let borrow = reduced >> 63; + reduced.wrapping_add(borrow.wrapping_neg() & (P as u64)) as u32 + }; + + #[inline(always)] + fn canonicalize_folded(v: u64) -> u32 { + if Self::BITS <= 31 { + let x = v as u32; + x.min(x.wrapping_sub(P)) + } else { + let reduced = v.wrapping_sub(P as u64); + let borrow = reduced >> 63; + reduced.wrapping_add(borrow.wrapping_neg() & (P as u64)) as u32 + } + } + + /// Create from a canonical representative in `[0, P)`. + #[inline] + pub fn from_canonical_u32(x: u32) -> Self { + debug_assert!(x < P); + Self(x) + } + + /// Additive identity. + #[inline] + pub fn zero() -> Self { + Self(0) + } + + /// Multiplicative identity. + #[inline] + pub fn one() -> Self { + Self(u32::from(P > 1)) + } + + /// Check whether this element is zero. + #[inline] + pub fn is_zero(&self) -> bool { + self.0 == 0 + } + + /// Multiplicative inverse, or `None` for zero. + #[inline] + pub fn inverse(&self) -> Option { + ::inverse(self) + } + + /// Construct from a `u64` reduced modulo the field modulus. + #[inline] + pub fn from_u64(val: u64) -> Self { + Self(Self::reduce_u64(val)) + } + + /// Construct from an `i64` reduced modulo the field modulus. + #[inline] + pub fn from_i64(val: i64) -> Self { + if val >= 0 { + Self::from_u64(val as u64) + } else { + -Self::from_u64(val.unsigned_abs()) + } + } + + /// Construct from an `i8` reduced modulo the field modulus. + #[inline] + pub fn from_i8(val: i8) -> Self { + Self::from_i64(val as i64) + } + + /// Return the canonical representative in `[0, P)`. + #[inline] + pub fn to_canonical_u32(self) -> u32 { + self.0 + } + + /// Solinas reduction: fold a u64 at bit `BITS` until the value fits, + /// then conditionally subtract `P`. + /// + /// For multiplication products (< 2^{2·BITS}) exactly 2 folds suffice; + /// for arbitrary u64 inputs (e.g. `from_u64`) the loop runs at most + /// `ceil(64 / BITS)` iterations. + #[inline(always)] + fn reduce_u64(x: u64) -> u32 { + let c = Self::C as u64; + let mut v = x; + while v >> Self::BITS != 0 { + v = (v & Self::MASK) + c * (v >> Self::BITS); + } + Self::canonicalize_folded(v) + } + + /// Reduce a `u128` to canonical form (for `from_canonical_u128_reduced`). + #[inline(always)] + fn reduce_u128(x: u128) -> u32 { + let c = Self::C as u128; + let bits = Self::BITS; + let mask = if bits == 32 { + u32::MAX as u128 + } else { + (1u128 << bits) - 1 + }; + let mut v = x; + while v >> bits != 0 { + v = (v & mask) + c * (v >> bits); + } + Self::canonicalize_folded(v as u64) + } + + /// Two-fold Solinas reduction for multiplication products. + /// + /// Input must be < 2^{2·BITS} (guaranteed for `a*b` where `a,b < P`). + /// Exactly 2 folds + conditional subtract, no loop. + #[inline(always)] + fn reduce_product(x: u64) -> u32 { + let c = Self::C as u64; + let f1 = (x & Self::MASK) + c * (x >> Self::BITS); + let f2 = (f1 & Self::MASK) + c * (f1 >> Self::BITS); + Self::canonicalize_folded(f2) + } + + #[inline(always)] + fn add_raw(a: u32, b: u32) -> u32 { + if Self::BITS <= 31 { + let sum = a.wrapping_add(b); + sum.min(sum.wrapping_sub(P)) + } else { + let s = (a as u64) + (b as u64); + let reduced = s.wrapping_sub(P as u64); + let borrow = reduced >> 63; + reduced.wrapping_add(borrow.wrapping_neg() & (P as u64)) as u32 + } + } + + #[inline(always)] + fn sub_raw(a: u32, b: u32) -> u32 { + if Self::BITS <= 31 { + let diff = a.wrapping_sub(b); + diff.min(diff.wrapping_add(P)) + } else { + let diff = (a as u64).wrapping_sub(b as u64); + let borrow = diff >> 63; + diff.wrapping_add(borrow.wrapping_neg() & (P as u64)) as u32 + } + } + + #[inline(always)] + fn mul_raw(a: u32, b: u32) -> u32 { + Self::reduce_product((a as u64) * (b as u64)) + } + + #[inline(always)] + fn sqr_raw(a: u32) -> u32 { + Self::mul_raw(a, a) + } + + /// Squaring, equivalent to `self * self`. + #[inline(always)] + pub fn square(self) -> Self { + Self(Self::sqr_raw(self.0)) + } + + fn pow(self, mut exp: u64) -> Self { + let mut base = self; + let mut acc = Self::one(); + while exp > 0 { + if (exp & 1) == 1 { + acc *= base; + } + base = base.square(); + exp >>= 1; + } + acc + } + + /// Extract the canonical value. + #[inline(always)] + pub fn to_limbs(self) -> u32 { + self.0 + } + + /// 32×32 → 64-bit widening multiply, **no reduction**. + #[inline(always)] + pub fn mul_wide(self, other: Self) -> u64 { + (self.0 as u64) * (other.0 as u64) + } + + /// 32×32 → 64-bit widening multiply with a raw `u32` operand, + /// **no reduction**. + #[inline(always)] + pub fn mul_wide_u32(self, other: u32) -> u64 { + (self.0 as u64) * (other as u64) + } + + /// Reduce a u64 value via Solinas folding to a canonical field element. + #[inline(always)] + pub fn solinas_reduce(x: u64) -> Self { + Self(Self::reduce_u64(x)) + } +} + +impl Add for Fp32

{ + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self::Output { + Self(Self::add_raw(self.0, rhs.0)) + } +} + +impl Sub for Fp32

{ + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + Self(Self::sub_raw(self.0, rhs.0)) + } +} + +impl Mul for Fp32

{ + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Self(Self::mul_raw(self.0, rhs.0)) + } +} + +impl Neg for Fp32

{ + type Output = Self; + #[inline] + fn neg(self) -> Self::Output { + Self(Self::sub_raw(0, self.0)) + } +} + +impl AddAssign for Fp32

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for Fp32

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for Fp32

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl<'a, const P: u32> Add<&'a Self> for Fp32

{ + type Output = Self; + #[inline] + fn add(self, rhs: &'a Self) -> Self::Output { + self + *rhs + } +} + +impl<'a, const P: u32> Sub<&'a Self> for Fp32

{ + type Output = Self; + #[inline] + fn sub(self, rhs: &'a Self) -> Self::Output { + self - *rhs + } +} + +impl<'a, const P: u32> Mul<&'a Self> for Fp32

{ + type Output = Self; + #[inline] + fn mul(self, rhs: &'a Self) -> Self::Output { + self * *rhs + } +} + +impl Invertible for Fp32

{ + #[inline(always)] + fn inverse(&self) -> Option { + let inv = self.inv_or_zero(); + if self.is_zero() { + None + } else { + Some(inv) + } + } + + #[inline(always)] + fn inv_or_zero(self) -> Self { + let candidate = self.pow((P as u64).wrapping_sub(2)); + let nz = ((self.0 | self.0.wrapping_neg()) >> 31) & 1; + let mask = 0u32.wrapping_sub(nz); + Self(candidate.0 & mask) + } +} + +impl HalvingField for Fp32

{ + #[inline] + fn half(self) -> Self { + if Self::BITS == 31 && Self::C == 1 { + Self((self.0 >> 1) | ((self.0 & 1) << 30)) + } else { + let half_p_plus_one = (P >> 1) + 1; + let correction = 0u32.wrapping_sub(self.0 & 1) & half_p_plus_one; + Self((self.0 >> 1) + correction) + } + } +} + +impl RandomSampling for Fp32

{ + #[inline(always)] + fn random(rng: &mut R) -> Self { + Self(Self::reduce_u64(rng.next_u64())) + } +} + +impl FromPrimitiveInt for Fp32

{ + #[inline(always)] + fn from_u64(val: u64) -> Self { + Self::from_u64(val) + } + + #[inline(always)] + fn from_i64(val: i64) -> Self { + Self::from_i64(val) + } + + #[inline(always)] + fn from_u128(val: u128) -> Self { + Self(Self::reduce_u128(val)) + } + + #[inline(always)] + fn from_i128(val: i128) -> Self { + if val >= 0 { + Self::from_u128(val as u128) + } else { + -Self::from_u128(val.unsigned_abs()) + } + } +} + +impl BalancedDigitLookup for Fp32

{} + +impl CanonicalField for Fp32

{ + fn to_canonical_u128(self) -> u128 { + self.0 as u128 + } + + fn modulus_bits() -> u32 { + Self::BITS + } + + fn from_canonical_u128_checked(val: u128) -> Option { + if val < P as u128 { + Some(Self(val as u32)) + } else { + None + } + } + + fn from_canonical_u128_reduced(val: u128) -> Self { + Self(Self::reduce_u128(val)) + } +} + +impl PseudoMersenneField for Fp32

{ + const MODULUS_BITS: u32 = Self::BITS; + const MODULUS_OFFSET: u128 = Self::C as u128; +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::rngs::StdRng; + use rand::SeedableRng; + + type F = Fp32<251>; // 2^8 - 5 + + #[test] + fn solinas_constants() { + assert_eq!(F::BITS, 8); + assert_eq!(F::C, 5); + assert_eq!(F::MASK, 255); + + type G = Fp32<{ (1u32 << 24) - 3 }>; // 2^24 - 3 + assert_eq!(G::BITS, 24); + assert_eq!(G::C, 3); + } + + #[test] + fn basic_arithmetic() { + let a = F::from_u64(100); + let b = F::from_u64(200); + assert_eq!((a + b).to_canonical_u32(), (100 + 200) % 251); + assert_eq!((a * b).to_canonical_u32(), (100 * 200) % 251); + assert_eq!((b - a).to_canonical_u32(), 100); + assert_eq!((-a).to_canonical_u32(), 251 - 100); + } + + #[test] + fn prime31_fast_path_edges() { + const P31: u32 = (1u32 << 31) - 19; + type G = Fp32; + + assert_eq!(G::BITS, 31); + assert_eq!(G::C, 19); + + let zero = G::zero(); + let one = G::one(); + let p_minus_one = G::from_canonical_u32(P31 - 1); + let p_minus_two = G::from_canonical_u32(P31 - 2); + + assert_eq!((p_minus_one + one).to_canonical_u32(), 0); + assert_eq!((p_minus_one + p_minus_one).to_canonical_u32(), P31 - 2); + assert_eq!((zero - one).to_canonical_u32(), P31 - 1); + assert_eq!((one - p_minus_one).to_canonical_u32(), 2); + assert_eq!((-zero).to_canonical_u32(), 0); + assert_eq!((-one).to_canonical_u32(), P31 - 1); + assert_eq!((p_minus_one * p_minus_one).to_canonical_u32(), 1); + assert_eq!((p_minus_two * p_minus_two).to_canonical_u32(), 4); + + for x in [zero, one, p_minus_two, p_minus_one] { + assert_eq!(x.half() + x.half(), x); + } + + type M = Fp32<{ (1u32 << 31) - 1 }>; + for x in [ + M::zero(), + M::one(), + M::from_canonical_u32((1u32 << 31) - 3), + M::from_canonical_u32((1u32 << 31) - 2), + ] { + assert_eq!(x.half() + x.half(), x); + } + } + + #[test] + fn prime31_random_arithmetic_matches_u64_modulus() { + const P31: u32 = (1u32 << 31) - 19; + type G = Fp32; + + let mut rng = StdRng::seed_from_u64(0x31_31_31_31); + for _ in 0..1000 { + let a_raw = rng.next_u32() & ((1u32 << 31) - 1); + let b_raw = rng.next_u32() & ((1u32 << 31) - 1); + let a = G::from_u64(a_raw as u64); + let b = G::from_u64(b_raw as u64); + let p = P31 as u64; + let a_can = (a_raw as u64) % p; + let b_can = (b_raw as u64) % p; + + assert_eq!((a + b).to_canonical_u32() as u64, (a_can + b_can) % p); + assert_eq!((a - b).to_canonical_u32() as u64, (a_can + p - b_can) % p); + assert_eq!((a * b).to_canonical_u32() as u64, (a_can * b_can) % p); + } + + assert_eq!( + G::from_u64(u64::MAX).to_canonical_u32() as u64, + u64::MAX % (P31 as u64) + ); + } + + #[test] + fn fp31_u128_reduction_matches_modulus() { + fn check(inputs: &[u128]) { + for &input in inputs { + assert_eq!( + Fp32::

::from_canonical_u128_reduced(input).to_canonical_u32() as u128, + input % (P as u128), + "u128 reduction mismatch for P={P}, input={input}" + ); + } + } + + const PRIME31: u32 = (1u32 << 31) - 19; + const MERSENNE31: u32 = (1u32 << 31) - 1; + const GENERIC30: u32 = (1u32 << 30) - 16_397; + const GENERIC31: u32 = (1u32 << 31) - 32_787; + let inputs = [ + 0, + 1, + PRIME31 as u128 - 1, + PRIME31 as u128, + PRIME31 as u128 + 1, + (PRIME31 as u128) * (PRIME31 as u128) - 1, + 1u128 << 63, + (1u128 << 96) + 123_456_789, + u128::MAX, + ]; + + check::(&inputs); + check::(&inputs); + check::(&inputs); + check::(&inputs); + } + + #[test] + fn mul_wide_matches_full_mul() { + let mut rng = StdRng::seed_from_u64(0x1234_5678); + for _ in 0..1000 { + let a: F = RandomSampling::random(&mut rng); + let b: F = RandomSampling::random(&mut rng); + let expected = a * b; + let reduced = F::solinas_reduce(a.mul_wide(b)); + assert_eq!(reduced, expected); + } + } + + #[test] + fn mul_wide_u32_matches() { + let mut rng = StdRng::seed_from_u64(0xabcd_ef01); + for _ in 0..1000 { + let a: F = RandomSampling::random(&mut rng); + let b = rng.next_u32() % 251; + let expected = a * F::from_canonical_u32(b); + let reduced = F::solinas_reduce(a.mul_wide_u32(b)); + assert_eq!(reduced, expected); + } + } + + #[test] + fn reduce_large_values() { + assert_eq!( + F::from_u64(u64::MAX).to_canonical_u32(), + (u64::MAX % 251) as u32 + ); + assert_eq!(F::from_u64(0).to_canonical_u32(), 0); + assert_eq!(F::from_u64(251).to_canonical_u32(), 0); + assert_eq!(F::from_u64(252).to_canonical_u32(), 1); + } + + #[test] + fn pseudo_mersenne_trait() { + assert_eq!(::MODULUS_BITS, 8); + assert_eq!(::MODULUS_OFFSET, 5); + } + + #[test] + fn cross_prime_32bit() { + type G = Fp32<{ u32::MAX - 98 }>; // 2^32 - 99 + assert_eq!(G::BITS, 32); + assert_eq!(G::C, 99); + + let a = G::from_u64(1_000_000); + let b = G::from_u64(2_000_000); + let product = (1_000_000u64 * 2_000_000u64) % ((1u64 << 32) - 99); + assert_eq!((a * b).to_canonical_u32(), product as u32); + } +} diff --git a/crates/jolt-field/src/prime/fp64.rs b/crates/jolt-field/src/prime/fp64.rs new file mode 100644 index 0000000000..25a6d21d1e --- /dev/null +++ b/crates/jolt-field/src/prime/fp64.rs @@ -0,0 +1,630 @@ +//! Prime field for primes of the form `p = 2^k − c` with `c` small, backed +//! by `u64` storage. +//! +//! Uses Solinas-style two-fold reduction. For `c = 2^a ± 1` the fold +//! multiply is replaced by shift+add/sub, saving a u128 widening multiply. + +use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +use crate::{FromPrimitiveInt, Invertible, RandomSampling}; +use rand_core::RngCore; + +use crate::{BalancedDigitLookup, CanonicalField, HalvingField, PseudoMersenneField}; + +use super::util::{is_pow2_u64, log2_pow2_u64, mul64_wide}; + +/// Prime field element for primes `p = 2^k − c` stored as `u64`. +/// +/// The fold point `k` and offset `c = 2^k − p` are computed at compile time +/// from the const-generic `P`. For `c = 2^a ± 1`, the fold multiply is +/// replaced by shift+add/sub. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Fp64(pub(crate) u64); + +impl Fp64

{ + /// Fold point: smallest `k` such that `P ≤ 2^k`. + const BITS: u32 = 64 - P.leading_zeros(); + + /// Offset `c = 2^k − P`. + pub const C: u64 = { + let c = if Self::BITS == 64 { + 0u64.wrapping_sub(P) + } else { + (1u64 << Self::BITS) - P + }; + assert!(P != 0, "modulus must be nonzero"); + assert!(P & 1 == 1, "modulus must be odd"); + assert!( + (c as u128) * (c as u128 + 1) < P as u128, + "C(C+1) < P required for fused canonicalize" + ); + c + }; + + /// +1 means `C = 2^a + 1`, -1 means `C = 2^a - 1`, 0 means generic. + const C_SHIFT_KIND: i8 = { + let c = Self::C; + if c > 1 && is_pow2_u64(c - 1) { + 1 + } else if c == u64::MAX || is_pow2_u64(c + 1) { + -1 + } else { + 0 + } + }; + + const C_SHIFT: u32 = { + let c = Self::C; + if Self::C_SHIFT_KIND == 1 { + log2_pow2_u64(c - 1) + } else if Self::C_SHIFT_KIND == -1 { + if c == u64::MAX { + 64 + } else { + log2_pow2_u64(c + 1) + } + } else { + 0 + } + }; + + /// Mask for extracting the low `BITS` bits from a u128. + const MASK: u128 = if Self::BITS == 64 { + u64::MAX as u128 + } else { + (1u128 << Self::BITS) - 1 + }; + + /// u64-width mask (only valid when BITS < 64). + const MASK64: u64 = if Self::BITS < 64 { + (1u64 << Self::BITS) - 1 + } else { + u64::MAX + }; + + /// Whether Solinas folding of a multiplication product can stay + /// entirely in u64. True when BITS < 64 and C·2^BITS < 2^64. + const FOLD_IN_U64: bool = Self::BITS < 64 && (Self::C as u128) < (1u128 << (64 - Self::BITS)); + + /// u64 multiply by C, split into u32-wide halves so LLVM emits + /// `umull` (32×32→64) instead of promoting to u128. + /// Only valid when C fits in u32 (always true: C < sqrt(P) < 2^32). + #[inline(always)] + fn mul_c_narrow(x: u64) -> u64 { + #[cfg(target_arch = "x86_64")] + { + // x86_64 has fast scalar 64-bit multiply; use one multiply instead + // of two widened 32-bit multiplies in the fold hot path. + Self::C.wrapping_mul(x) + } + #[cfg(not(target_arch = "x86_64"))] + { + let c = Self::C as u32; + let x_lo = x as u32; + let x_hi = (x >> 32) as u32; + (c as u64 * x_lo as u64).wrapping_add((c as u64 * x_hi as u64) << 32) + } + } + + /// Multiply `x` by `C`. For `C = 2^a ± 1` uses shift+add/sub. + #[inline(always)] + fn mul_c(x: u64) -> u128 { + if Self::C_SHIFT_KIND == 1 { + ((x as u128) << Self::C_SHIFT) + x as u128 + } else if Self::C_SHIFT_KIND == -1 { + ((x as u128) << Self::C_SHIFT) - x as u128 + } else { + (Self::C as u128) * (x as u128) + } + } + + /// Create from a canonical representative in `[0, P)`. + #[inline] + pub fn from_canonical_u64(x: u64) -> Self { + debug_assert!(x < P); + Self(x) + } + + /// Additive identity. + #[inline] + pub fn zero() -> Self { + Self(0) + } + + /// Multiplicative identity. + #[inline] + pub fn one() -> Self { + Self(u64::from(P > 1)) + } + + /// Check whether this element is zero. + #[inline] + pub fn is_zero(&self) -> bool { + self.0 == 0 + } + + /// Multiplicative inverse, or `None` for zero. + #[inline] + pub fn inverse(&self) -> Option { + ::inverse(self) + } + + /// Construct from a `u64` reduced modulo the field modulus. + #[inline] + pub fn from_u64(val: u64) -> Self { + Self(Self::reduce_u128(val as u128)) + } + + /// Construct from an `i64` reduced modulo the field modulus. + #[inline] + pub fn from_i64(val: i64) -> Self { + if val >= 0 { + Self::from_u64(val as u64) + } else { + -Self::from_u64(val.unsigned_abs()) + } + } + + /// Construct from an `i8` reduced modulo the field modulus. + #[inline] + pub fn from_i8(val: i8) -> Self { + Self::from_i64(val as i64) + } + + /// Return the canonical representative in `[0, P)`. + #[inline] + pub fn to_canonical_u64(self) -> u64 { + self.0 + } + + /// Solinas reduction: fold a u128 at bit `BITS` until the value fits, + /// then conditionally subtract `P`. + /// + /// For multiplication products (< 2^{2·BITS}) exactly 2 folds suffice; + /// for arbitrary u128 inputs the loop runs at most `ceil(128 / BITS)` + /// iterations. + #[inline(always)] + fn reduce_u128(x: u128) -> u64 { + let mut v = x; + while v >> Self::BITS != 0 { + v = (v & Self::MASK) + Self::mul_c((v >> Self::BITS) as u64); + } + let reduced = v.wrapping_sub(P as u128); + let borrow = reduced >> 127; + reduced.wrapping_add(borrow.wrapping_neg() & (P as u128)) as u64 + } + + /// Two-fold Solinas reduction for multiplication products. + /// + /// Input must be < 2^{2·BITS} (guaranteed for `a*b` where `a,b < P`). + /// Exactly 2 folds + conditional subtract, no loop. + /// + /// When `FOLD_IN_U64` is true the entire reduction stays in u64, + /// avoiding expensive u128 mask/shift on sub-word primes. + #[inline(always)] + fn reduce_product(x: u128) -> u64 { + if Self::FOLD_IN_U64 { + let lo = x as u64; + let hi = (x >> 64) as u64; + let high = (lo >> Self::BITS) | (hi << (64 - Self::BITS)); + let f1 = (lo & Self::MASK64) + Self::mul_c_narrow(high); + let f2 = (f1 & Self::MASK64) + Self::mul_c_narrow(f1 >> Self::BITS); + let reduced = f2.wrapping_sub(P); + let borrow = reduced >> 63; + reduced.wrapping_add(borrow.wrapping_neg() & P) + } else { + let f1 = (x & Self::MASK) + Self::mul_c((x >> Self::BITS) as u64); + let f2 = (f1 & Self::MASK) + Self::mul_c((f1 >> Self::BITS) as u64); + let reduced = f2.wrapping_sub(P as u128); + let borrow = reduced >> 127; + reduced.wrapping_add(borrow.wrapping_neg() & (P as u128)) as u64 + } + } + + /// BMI2 fast path: avoid re-materializing `u128` product in the common + /// sub-word configuration where reduction stays in `u64`. + #[cfg(all(target_arch = "x86_64", target_feature = "bmi2"))] + #[inline(always)] + fn reduce_product_wide(lo: u64, hi: u64) -> u64 { + if Self::FOLD_IN_U64 { + let high = (lo >> Self::BITS) | (hi << (64 - Self::BITS)); + let f1 = (lo & Self::MASK64) + Self::mul_c_narrow(high); + let f2 = (f1 & Self::MASK64) + Self::mul_c_narrow(f1 >> Self::BITS); + let reduced = f2.wrapping_sub(P); + let borrow = reduced >> 63; + reduced.wrapping_add(borrow.wrapping_neg() & P) + } else { + Self::reduce_product(lo as u128 | ((hi as u128) << 64)) + } + } + + #[inline(always)] + fn add_raw(a: u64, b: u64) -> u64 { + if Self::BITS == 64 { + let (s, overflow) = a.overflowing_add(b); + let folded = s.wrapping_add((overflow as u64).wrapping_neg() & Self::C); + let reduced = folded.wrapping_sub(P); + let borrow = (folded < P) as u64; + reduced.wrapping_add(borrow.wrapping_neg() & P) + } else if Self::BITS <= 62 { + let s = a + b; + let reduced = s.wrapping_sub(P); + let borrow = reduced >> 63; + reduced.wrapping_add(borrow.wrapping_neg() & P) + } else { + let s = (a as u128) + (b as u128); + let reduced = s.wrapping_sub(P as u128); + let borrow = reduced >> 127; + reduced.wrapping_add(borrow.wrapping_neg() & (P as u128)) as u64 + } + } + + #[inline(always)] + fn sub_raw(a: u64, b: u64) -> u64 { + if Self::BITS == 64 { + let (diff, underflow) = a.overflowing_sub(b); + diff.wrapping_sub((underflow as u64).wrapping_neg() & Self::C) + } else if Self::BITS <= 62 { + let diff = a.wrapping_sub(b); + let borrow = diff >> 63; + diff.wrapping_add(borrow.wrapping_neg() & P) + } else { + let diff = (a as u128).wrapping_sub(b as u128); + let borrow = diff >> 127; + diff.wrapping_add(borrow.wrapping_neg() & (P as u128)) as u64 + } + } + + #[inline(always)] + fn mul_raw(a: u64, b: u64) -> u64 { + #[cfg(all(target_arch = "x86_64", target_feature = "bmi2"))] + { + let (lo, hi) = mul64_wide(a, b); + Self::reduce_product_wide(lo, hi) + } + #[cfg(not(all(target_arch = "x86_64", target_feature = "bmi2")))] + { + Self::reduce_product((a as u128) * (b as u128)) + } + } + + #[inline(always)] + fn sqr_raw(a: u64) -> u64 { + Self::mul_raw(a, a) + } + + /// Squaring, equivalent to `self * self`. + #[inline(always)] + pub fn square(self) -> Self { + Self(Self::sqr_raw(self.0)) + } + + fn pow(self, mut exp: u64) -> Self { + let mut base = self; + let mut acc = Self::one(); + while exp > 0 { + if (exp & 1) == 1 { + acc *= base; + } + base = base.square(); + exp >>= 1; + } + acc + } + + /// Extract the canonical value. + #[inline(always)] + pub fn to_limbs(self) -> u64 { + self.0 + } + + /// 64×64 → 128-bit widening multiply, **no reduction**. + #[inline(always)] + pub fn mul_wide(self, other: Self) -> u128 { + let (lo, hi) = mul64_wide(self.0, other.0); + lo as u128 | ((hi as u128) << 64) + } + + /// 64×64 → 128-bit widening multiply with a raw `u64` operand, + /// **no reduction**. + #[inline(always)] + pub fn mul_wide_u64(self, other: u64) -> u128 { + let (lo, hi) = mul64_wide(self.0, other); + lo as u128 | ((hi as u128) << 64) + } + + /// Reduce a u128 value via Solinas folding to a canonical field element. + #[inline(always)] + pub fn solinas_reduce(x: u128) -> Self { + Self(Self::reduce_u128(x)) + } + + /// Reduce the integer sum `w0 + w1` of two products of canonical residues + /// (each `a·b` with `a, b < P`, so each `< 2^{2·BITS}`) to a canonical + /// field element. + /// + /// Used by the specialized `FpExt2` EOR fold, which forms each output + /// coordinate as a sum of two base-field products before a single + /// reduction. + /// + /// - Sub-word primes (`BITS < 64`): each product is `< 2^{2·BITS} ≤ 2^126`, + /// so the sum is `< 2^127` and never overflows `u128`; reduce directly. + /// - Full-word primes (`BITS == 64`, `P = 2^64 − C` with `C < 2^32`): the + /// sum can reach `< 2^129`. Split it into 64-bit limbs and fold the high + /// parts with `2^64 ≡ C` and `2^128 ≡ C² (mod P)`. The folded value is + /// `< 2^97`, which `solinas_reduce` finishes. The result is congruent to + /// `w0 + w1 (mod P)`, hence byte-identical to the canonical reduction. + #[inline(always)] + pub(crate) fn reduce_sum_of_two_products(w0: u128, w1: u128) -> Self { + if Self::BITS < 64 { + Self::solinas_reduce(w0.wrapping_add(w1)) + } else { + let (s, carry) = w0.overflowing_add(w1); + let cc = Self::C as u128; + let folded = + (s as u64 as u128) + ((s >> 64) as u64 as u128) * cc + (carry as u128) * cc * cc; + Self::solinas_reduce(folded) + } + } +} + +impl Add for Fp64

{ + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self::Output { + Self(Self::add_raw(self.0, rhs.0)) + } +} + +impl Sub for Fp64

{ + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + Self(Self::sub_raw(self.0, rhs.0)) + } +} + +impl Mul for Fp64

{ + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Self(Self::mul_raw(self.0, rhs.0)) + } +} + +impl Neg for Fp64

{ + type Output = Self; + #[inline] + fn neg(self) -> Self::Output { + Self(Self::sub_raw(0, self.0)) + } +} + +impl AddAssign for Fp64

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for Fp64

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for Fp64

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl<'a, const P: u64> Add<&'a Self> for Fp64

{ + type Output = Self; + #[inline] + fn add(self, rhs: &'a Self) -> Self::Output { + self + *rhs + } +} + +impl<'a, const P: u64> Sub<&'a Self> for Fp64

{ + type Output = Self; + #[inline] + fn sub(self, rhs: &'a Self) -> Self::Output { + self - *rhs + } +} + +impl<'a, const P: u64> Mul<&'a Self> for Fp64

{ + type Output = Self; + #[inline] + fn mul(self, rhs: &'a Self) -> Self::Output { + self * *rhs + } +} + +impl Invertible for Fp64

{ + #[inline(always)] + fn inverse(&self) -> Option { + let inv = self.inv_or_zero(); + if self.is_zero() { + None + } else { + Some(inv) + } + } + + #[inline(always)] + fn inv_or_zero(self) -> Self { + let candidate = self.pow(P.wrapping_sub(2)); + let nz = ((self.0 | self.0.wrapping_neg()) >> 63) & 1; + let mask = 0u64.wrapping_sub(nz); + Self(candidate.0 & mask) + } +} + +impl HalvingField for Fp64

{ + #[inline] + fn half(self) -> Self { + let x = self.0 as u128; + Self(((x + (x & 1) * P as u128) >> 1) as u64) + } +} + +impl RandomSampling for Fp64

{ + #[inline(always)] + fn random(rng: &mut R) -> Self { + let lo = rng.next_u64() as u128; + let hi = rng.next_u64() as u128; + Self(Self::reduce_u128(lo | (hi << 64))) + } +} + +impl FromPrimitiveInt for Fp64

{ + #[inline(always)] + fn from_u64(val: u64) -> Self { + Self::from_u64(val) + } + + #[inline(always)] + fn from_i64(val: i64) -> Self { + Self::from_i64(val) + } + + #[inline(always)] + fn from_u128(val: u128) -> Self { + Self(Self::reduce_u128(val)) + } + + #[inline(always)] + fn from_i128(val: i128) -> Self { + if val >= 0 { + Self::from_u128(val as u128) + } else { + -Self::from_u128(val.unsigned_abs()) + } + } +} + +impl BalancedDigitLookup for Fp64

{} + +impl CanonicalField for Fp64

{ + fn to_canonical_u128(self) -> u128 { + self.0 as u128 + } + + fn modulus_bits() -> u32 { + Self::BITS + } + + fn from_canonical_u128_checked(val: u128) -> Option { + if val < P as u128 { + Some(Self(val as u64)) + } else { + None + } + } + + fn from_canonical_u128_reduced(val: u128) -> Self { + Self(Self::reduce_u128(val)) + } +} + +impl PseudoMersenneField for Fp64

{ + const MODULUS_BITS: u32 = Self::BITS; + const MODULUS_OFFSET: u128 = Self::C as u128; +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::rngs::StdRng; + use rand::SeedableRng; + + type F40 = Fp64<{ (1u64 << 40) - 195 }>; // 2^40 - 195 + type F64 = Fp64<{ u64::MAX - 58 }>; // 2^64 - 59 + + #[test] + fn solinas_constants() { + assert_eq!(F40::BITS, 40); + assert_eq!(F40::C, 195); + + assert_eq!(F64::BITS, 64); + assert_eq!(F64::C, 59); + } + + #[test] + fn basic_arithmetic_sub_word() { + let a = F40::from_u64(1_000_000); + let b = F40::from_u64(2_000_000); + let p = (1u64 << 40) - 195; + assert_eq!((a + b).to_canonical_u64(), 3_000_000); + assert_eq!( + (a * b).to_canonical_u64(), + (1_000_000u128 * 2_000_000u128 % p as u128) as u64 + ); + } + + #[test] + fn basic_arithmetic_full_word() { + let a = F64::from_u64(1_000_000_000); + let b = F64::from_u64(2_000_000_000); + let p = u64::MAX - 58; + assert_eq!( + (a * b).to_canonical_u64(), + (1_000_000_000u128 * 2_000_000_000u128 % p as u128) as u64 + ); + } + + #[test] + fn mul_wide_matches_full_mul() { + let mut rng = StdRng::seed_from_u64(0xdead_beef); + for _ in 0..1000 { + let a: F40 = RandomSampling::random(&mut rng); + let b: F40 = RandomSampling::random(&mut rng); + let expected = a * b; + let reduced = F40::solinas_reduce(a.mul_wide(b)); + assert_eq!(reduced, expected); + } + } + + #[test] + fn mul_wide_u64_matches() { + let mut rng = StdRng::seed_from_u64(0xcafe_d00d); + for _ in 0..1000 { + let a: F40 = RandomSampling::random(&mut rng); + let b = rng.next_u64() % ((1u64 << 40) - 195); + let expected = a * F40::from_canonical_u64(b); + let reduced = F40::solinas_reduce(a.mul_wide_u64(b)); + assert_eq!(reduced, expected); + } + } + + #[test] + fn pseudo_mersenne_trait() { + assert_eq!(::MODULUS_BITS, 40); + assert_eq!(::MODULUS_OFFSET, 195); + assert_eq!(::MODULUS_BITS, 64); + assert_eq!(::MODULUS_OFFSET, 59); + } + + #[test] + fn shift_optimization_detected() { + type G = Fp64<{ (1u64 << 56) - 27 }>; // C = 27, not 2^a±1 + assert_eq!(G::C_SHIFT_KIND, 0); + + type H = Fp64<{ u64::MAX - 58 }>; // C = 59, not 2^a±1 + assert_eq!(H::C_SHIFT_KIND, 0); + } + + #[test] + fn reduce_u128_large() { + assert_eq!(F64::from_canonical_u128_reduced(u128::MAX), { + let p = u64::MAX as u128 - 58; + F64::from_canonical_u64((u128::MAX % p) as u64) + }); + } +} diff --git a/crates/jolt-field/src/prime/mod.rs b/crates/jolt-field/src/prime/mod.rs new file mode 100644 index 0000000000..6c948e3ceb --- /dev/null +++ b/crates/jolt-field/src/prime/mod.rs @@ -0,0 +1,32 @@ +//! Pseudo-Mersenne prime fields (`p = 2^k − c`) and their named instances. +//! +//! The leaf layer of the field DAG: the `u32`/`u64`/`u128`-backed +//! `Fp{32,64,128}` field types, the `2^k − offset` registry +//! (`pseudo_mersenne`), and the shared low-level arithmetic helpers (`util`). +//! The extension towers (`ext`), packing (`packed`), and wide accumulators +//! (`unreduced`) all build on top of this module. + +#![expect( + clippy::unreadable_literal, + reason = "ported modulus and regression constants retain their audited spelling" +)] + +pub(crate) mod fp128; +pub(crate) mod fp32; +pub(crate) mod fp64; +mod native_algebra; +mod native_capability; +pub(crate) mod pseudo_mersenne; +pub(crate) mod util; + +pub use fp128::{ + Fp128, Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, +}; +pub use fp32::Fp32; +pub use fp64::Fp64; +pub use pseudo_mersenne::{ + is_registered_prime_offset, pseudo_mersenne_modulus, registered_prime_offset_spec, + Prime24Offset3, Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, + Prime48Offset59, Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, + PRIME_OFFSET_IMPLEMENTED_MAX_BITS, PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, +}; diff --git a/crates/jolt-field/src/prime/native_algebra.rs b/crates/jolt-field/src/prime/native_algebra.rs new file mode 100644 index 0000000000..dbfab83675 --- /dev/null +++ b/crates/jolt-field/src/prime/native_algebra.rs @@ -0,0 +1,88 @@ +//! Native `num_traits`/`std` supertrait impls and core-algebra markers for the +//! concrete prime fields (`Fp32`/`Fp64`/`Fp128`). +//! +//! These are the Jolt-free supertrait obligations of the native +//! [`AdditiveGroup`]/[`RingCore`]/[`FieldCore`] hierarchy: +//! `Zero`/`One`/`Display`/`Hash`/`Sum`/`Product` plus the empty algebra markers. +//! The non-trivial `RingCore::square` / `Invertible::inverse` impls stay +//! co-located with each prime type. + +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::iter::{Product, Sum}; + +use num_traits::{One, Zero}; + +use super::{Fp128, Fp32, Fp64}; +use crate::{AdditiveGroup, CanonicalField, FieldCore, RingCore}; + +macro_rules! impl_prime_native_algebra { + ($ty:ident<$p:ident: $p_ty:ty>, $canon:ident) => { + impl Zero for $ty<$p> { + #[inline] + fn zero() -> Self { + Self::default() + } + + #[inline] + fn is_zero(&self) -> bool { + self.to_canonical_u128() == 0 + } + } + + impl One for $ty<$p> { + #[inline] + fn one() -> Self { + if $p > 1 { + Self::$canon(1) + } else { + Self::zero() + } + } + } + + impl fmt::Display for $ty<$p> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_canonical_u128()) + } + } + + impl Hash for $ty<$p> { + fn hash(&self, state: &mut H) { + self.to_canonical_u128().hash(state); + } + } + + impl Sum for $ty<$p> { + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), |acc, x| acc + x) + } + } + + impl<'a, const $p: $p_ty> Sum<&'a Self> for $ty<$p> { + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), |acc, x| acc + *x) + } + } + + impl Product for $ty<$p> { + fn product>(iter: I) -> Self { + iter.fold(Self::one(), |acc, x| acc * x) + } + } + + impl<'a, const $p: $p_ty> Product<&'a Self> for $ty<$p> { + fn product>(iter: I) -> Self { + iter.fold(Self::one(), |acc, x| acc * *x) + } + } + + impl AdditiveGroup for $ty<$p> {} + impl RingCore for $ty<$p> {} + impl FieldCore for $ty<$p> {} + }; +} + +impl_prime_native_algebra!(Fp32, from_canonical_u32); +impl_prime_native_algebra!(Fp64, from_canonical_u64); +impl_prime_native_algebra!(Fp128, from_canonical_u128); diff --git a/crates/jolt-field/src/prime/native_capability.rs b/crates/jolt-field/src/prime/native_capability.rs new file mode 100644 index 0000000000..727ef866bd --- /dev/null +++ b/crates/jolt-field/src/prime/native_capability.rs @@ -0,0 +1,210 @@ +//! Native capability-trait impls for the prime fields: primitive-int +//! multiplication markers, the canonical byte/transcript surface, bit-length +//! introspection, and the `WithAccumulator` association (native `NaiveAccumulator`). +//! +//! `FromPrimitiveInt`/`RandomSampling` carry per-type logic and stay in the prime +//! modules; this module owns the shared derived-capability implementations used +//! directly by both Jolt and Akita. + +use std::mem::size_of; + +use super::{Fp128, Fp32, Fp64}; +use crate::{ + CanonicalBitLength, CanonicalBytes, CanonicalField, CanonicalU64, Field, FieldCore, + FixedByteSize, FixedBytes, FromPrimitiveInt, MulPow2, MulPrimitiveInt, NaiveAccumulator, + NaiveSignedProductAccumulator, NaiveSignedScalarAccumulator, ReducingBytes, + TranscriptChallenge, WithAccumulator, WithSignedProductAccumulator, WithSmallScalarAccumulator, +}; + +macro_rules! impl_prime_native_capability { + ($ty:ident<$p:ident: $p_ty:ty>, $bytes:expr, $fixed_bytes:literal) => { + impl MulPow2 for $ty<$p> {} + impl MulPrimitiveInt for $ty<$p> {} + + impl FixedByteSize for $ty<$p> { + const NUM_BYTES: usize = $bytes; + } + + impl CanonicalBytes for $ty<$p> { + #[inline(always)] + fn to_bytes_le(&self, out: &mut [u8]) { + assert_eq!(out.len(), ::NUM_BYTES); + out.copy_from_slice( + &self.to_canonical_u128().to_le_bytes()[..::NUM_BYTES], + ); + } + } + + impl ReducingBytes for $ty<$p> { + #[inline(always)] + fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { + if bytes.len() <= size_of::() { + let mut padded = [0u8; size_of::()]; + padded[..bytes.len()].copy_from_slice(bytes); + return ::from_u128(u128::from_le_bytes(padded)); + } + + reduce_le_bytes_mod_order(bytes) + } + } + + impl TranscriptChallenge for $ty<$p> { + #[inline(always)] + fn from_challenge_bytes(bytes: &[u8]) -> Self { + ::from_le_bytes_mod_order(bytes) + } + } + + impl FixedBytes<$fixed_bytes> for $ty<$p> {} + + impl CanonicalBitLength for $ty<$p> { + #[inline] + fn num_bits(&self) -> u32 { + let value = self.to_canonical_u128(); + u128::BITS - value.leading_zeros() + } + } + + impl CanonicalU64 for $ty<$p> { + #[inline] + fn to_canonical_u64_checked(&self) -> Option { + self.to_canonical_u128().try_into().ok() + } + } + + impl WithAccumulator for $ty<$p> { + type Accumulator = NaiveAccumulator; + } + + impl WithSmallScalarAccumulator for $ty<$p> { + type SmallScalarAccumulator = NaiveSignedScalarAccumulator; + } + + impl WithSignedProductAccumulator for $ty<$p> { + type SignedProductAccumulator = NaiveSignedProductAccumulator; + } + + impl Field for $ty<$p> {} + }; +} + +/// Horner reduction of arbitrary-length little-endian bytes modulo the field +/// order (the >16-byte path of `ReducingBytes::from_le_bytes_mod_order`). +#[inline(always)] +fn reduce_le_bytes_mod_order(bytes: &[u8]) -> F { + let base = F::from_u64(256); + bytes.iter().rev().fold(F::zero(), |acc, &byte| { + acc * base + F::from_u64(byte as u64) + }) +} + +impl_prime_native_capability!(Fp32, 4, 4); +impl_prime_native_capability!(Fp64, 8, 8); +impl_prime_native_capability!(Fp128, 16, 16); + +#[cfg(test)] +mod tests { + //! Native byte / transcript / accumulator capability tests. + //! + //! These exercise the Solinas backend directly, so they run under + //! `--no-default-features --features solinas` as well as combined builds. + use super::*; + use crate::Prime128Offset275; + use crate::{AdditiveAccumulator, RingAccumulator}; + + /// Asserts the full canonical byte round-trip on the native traits. + fn assert_native_byte_roundtrip(value: F, expected: [u8; N]) + where + F: CanonicalField + + CanonicalBytes + + ReducingBytes + + TranscriptChallenge + + FixedByteSize + + FixedBytes + + CanonicalBitLength + + CanonicalU64 + + std::fmt::Debug + + Eq, + { + assert_eq!(::NUM_BYTES, N); + + // to_bytes_le (the audited method) into a correctly sized buffer, plus the + // array/vec convenience wrappers — all three must agree. + let mut buf = [0u8; N]; + value.to_bytes_le(&mut buf); + assert_eq!(buf, expected); + assert_eq!(value.to_bytes_array(), expected); + assert_eq!(value.to_bytes_le_vec(), expected.to_vec()); + + // Reducing / fixed / challenge constructors all invert the encoding. + assert_eq!(F::from_bytes_array(&buf), value); + assert_eq!(F::from_le_bytes_mod_order(&buf), value); + assert_eq!(F::from_challenge_bytes(&buf), value); + + assert_eq!( + value.num_bits(), + u128::BITS - value.to_canonical_u128().leading_zeros() + ); + } + + #[test] + fn prime_fields_native_byte_capabilities() { + type F32 = Fp32<251>; + type F64 = Fp64<4294967197>; + type F128 = Prime128Offset275; + + assert_native_byte_roundtrip::(F32::from_u64(42), 42u32.to_le_bytes()); + assert_native_byte_roundtrip::(F64::from_u64(42), 42u64.to_le_bytes()); + assert_native_byte_roundtrip::( + F128::from_canonical_u128(0x0102_0304_0506_0708), + 0x0102_0304_0506_0708u128.to_le_bytes(), + ); + + // Reducing constructor on a short slice: 255 mod 251 == 4. + assert_eq!(F32::from_le_bytes_mod_order(&[255, 0]), F32::from_u64(4)); + assert_eq!(F32::from_challenge_bytes(&[255, 0]), F32::from_u64(4)); + + // Over-long slice (> 16 bytes) takes the Horner-reduction path; trailing + // zero limbs must not change the value (and must not panic). + let value = F128::from_canonical_u128(0x00DE_AD00_BEEF); + let mut over_long = value.to_bytes_le_vec(); + over_long.extend_from_slice(&[0u8; 8]); + assert!(over_long.len() > 16); + assert_eq!(F128::from_le_bytes_mod_order(&over_long), value); + + // Bit-length + checked-u64 extraction. + assert_eq!(F32::zero().num_bits(), 0); + assert_eq!(F64::from_u64(7).to_canonical_u64_checked(), Some(7)); + assert_eq!( + F128::from_canonical_u128(1u128 << 65).to_canonical_u64_checked(), + None + ); + } + + #[test] + fn prime_fields_native_mul_capabilities() { + type F32 = Fp32<251>; + type F64 = Fp64<4294967197>; + + // mul_pow_2: 3 * 2^4 == 48. + assert_eq!(F32::from_u64(3).mul_pow_2(4), F32::from_u64(48)); + assert_eq!(F64::from_u64(5).mul_pow_2(0), F64::from_u64(5)); + // Large shift still agrees with repeated doubling. + let doubled = (0..40).fold(F64::from_u64(1), |acc, _| acc + acc); + assert_eq!(F64::from_u64(1).mul_pow_2(40), doubled); + + // mul_u64 / mul_i64. + assert_eq!(F64::from_u64(9).mul_u64(7), F64::from_u64(63)); + assert_eq!(F64::from_u64(9).mul_i64(-1), -F64::from_u64(9)); + } + + #[test] + fn prime_fields_native_accumulator() { + type F64 = Fp64<4294967197>; + + let mut acc = ::Accumulator::default(); + acc.fmadd(F64::from_u64(9), F64::from_u64(7)); + acc.add(F64::from_u64(2)); + assert_eq!(acc.reduce(), F64::from_u64(65)); + } +} diff --git a/crates/jolt-field/src/prime/pseudo_mersenne.rs b/crates/jolt-field/src/prime/pseudo_mersenne.rs new file mode 100644 index 0000000000..3795c432bd --- /dev/null +++ b/crates/jolt-field/src/prime/pseudo_mersenne.rs @@ -0,0 +1,174 @@ +//! `2^k - offset` pseudo-Mersenne registry and field aliases. +//! +//! Concrete aliases include both coordinates of `q = 2^k - offset` so adding +//! another prime at the same bit width does not create an implicit canonical +//! choice. + +use super::{Fp32, Fp64}; + +/// Maximum supported offset in this `2^k - offset` specialization. +pub const PRIME_OFFSET_MAX: u128 = 1u128 << 16; + +/// Current active bit-size bound for concrete field aliases in this phase. +pub const PRIME_OFFSET_IMPLEMENTED_MAX_BITS: u32 = 128; + +/// Metadata describing a `2^k - offset` pseudo-Mersenne modulus. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PrimeOffsetSpec { + /// `k` in `2^k - offset`. + pub bits: u32, + /// `offset` in `2^k - offset`. + pub offset: u16, + /// Modulus value. + pub modulus: u128, +} + +/// Compute `2^k - offset` for `k <= 128`. +pub const fn pseudo_mersenne_modulus(bits: u32, offset: u128) -> Option { + if bits == 0 || bits > 128 || offset == 0 { + return None; + } + if bits == 128 { + Some(u128::MAX - (offset - 1)) + } else { + Some((1u128 << bits) - offset) + } +} + +/// Return the registered prime spec for exactly `(bits, offset)`. +pub const fn registered_prime_offset_spec(bits: u32, offset: u128) -> Option { + let mut i = 0; + while i < PRIME_OFFSET_SPECS.len() { + let spec = PRIME_OFFSET_SPECS[i]; + if spec.bits == bits && (spec.offset as u128) == offset { + return Some(spec); + } + i += 1; + } + None +} + +/// Check whether `(k, offset)` is an explicitly registered `2^k - offset` prime. +pub const fn is_registered_prime_offset(bits: u32, offset: u128) -> bool { + if bits > PRIME_OFFSET_IMPLEMENTED_MAX_BITS || offset > PRIME_OFFSET_MAX { + return false; + } + registered_prime_offset_spec(bits, offset).is_some() +} + +/// `offset` for `k = 24`. +pub(crate) const PRIME24_OFFSET3_OFFSET: u16 = 3; +/// `offset` for `k = 30`. +pub(crate) const PRIME30_OFFSET35_OFFSET: u16 = 35; +/// `offset` for `k = 31`. +pub(crate) const PRIME31_OFFSET19_OFFSET: u16 = 19; +/// `offset` for `k = 32`. +pub(crate) const PRIME32_OFFSET99_OFFSET: u16 = 99; +/// `offset` for `k = 40`. +pub(crate) const PRIME40_OFFSET195_OFFSET: u16 = 195; +/// `offset` for `k = 48`. +pub(crate) const PRIME48_OFFSET59_OFFSET: u16 = 59; +/// `offset` for `k = 56`. +pub(crate) const PRIME56_OFFSET27_OFFSET: u16 = 27; +/// `offset` for `k = 64`. +pub(crate) const PRIME64_OFFSET59_OFFSET: u16 = 59; +/// `offset` for `k = 128`. +pub(crate) const PRIME128_OFFSET275_OFFSET: u16 = 275; + +/// `2^24 - 3`. +pub(crate) const PRIME24_OFFSET3_MODULUS: u32 = + ((1u128 << 24) - (PRIME24_OFFSET3_OFFSET as u128)) as u32; +/// `2^30 - 35`. +pub(crate) const PRIME30_OFFSET35_MODULUS: u32 = + ((1u128 << 30) - (PRIME30_OFFSET35_OFFSET as u128)) as u32; +/// `2^31 - 19`. +pub(crate) const PRIME31_OFFSET19_MODULUS: u32 = + ((1u128 << 31) - (PRIME31_OFFSET19_OFFSET as u128)) as u32; +/// `2^32 - 99`. +pub(crate) const PRIME32_OFFSET99_MODULUS: u32 = + ((1u128 << 32) - (PRIME32_OFFSET99_OFFSET as u128)) as u32; +/// `2^40 - 195`. +pub(crate) const PRIME40_OFFSET195_MODULUS: u64 = + ((1u128 << 40) - (PRIME40_OFFSET195_OFFSET as u128)) as u64; +/// `2^48 - 59`. +pub(crate) const PRIME48_OFFSET59_MODULUS: u64 = + ((1u128 << 48) - (PRIME48_OFFSET59_OFFSET as u128)) as u64; +/// `2^56 - 27`. +pub(crate) const PRIME56_OFFSET27_MODULUS: u64 = + ((1u128 << 56) - (PRIME56_OFFSET27_OFFSET as u128)) as u64; +/// `2^64 - 59`. +pub(crate) const PRIME64_OFFSET59_MODULUS: u64 = u64::MAX - ((PRIME64_OFFSET59_OFFSET as u64) - 1); +/// `2^128 - 275`. +pub(crate) const PRIME128_OFFSET275_MODULUS: u128 = + u128::MAX - (PRIME128_OFFSET275_OFFSET as u128 - 1); + +/// Prime field for `2^24 - 3`. +pub type Prime24Offset3 = Fp32; +/// Prime field for `2^30 - 35`. +pub type Prime30Offset35 = Fp32; +/// Prime field for `2^31 - 19`. +pub type Prime31Offset19 = Fp32; +/// Prime field for `2^32 - 99`. +pub type Prime32Offset99 = Fp32; +/// Prime field for `2^40 - 195`. +pub type Prime40Offset195 = Fp64; +/// Prime field for `2^48 - 59`. +pub type Prime48Offset59 = Fp64; +/// Prime field for `2^56 - 27`. +pub type Prime56Offset27 = Fp64; +/// Prime field for `2^64 - 59`. +pub type Prime64Offset59 = Fp64; + +/// `2^k - offset` profiles currently enabled in-code. +/// +/// Every enabled entry satisfies the current in-code `2^k - offset` policy. +pub const PRIME_OFFSET_SPECS: [PrimeOffsetSpec; 9] = [ + PrimeOffsetSpec { + bits: 24, + offset: PRIME24_OFFSET3_OFFSET, + modulus: PRIME24_OFFSET3_MODULUS as u128, + }, + PrimeOffsetSpec { + bits: 30, + offset: PRIME30_OFFSET35_OFFSET, + modulus: PRIME30_OFFSET35_MODULUS as u128, + }, + PrimeOffsetSpec { + bits: 31, + offset: PRIME31_OFFSET19_OFFSET, + modulus: PRIME31_OFFSET19_MODULUS as u128, + }, + PrimeOffsetSpec { + bits: 32, + offset: PRIME32_OFFSET99_OFFSET, + modulus: PRIME32_OFFSET99_MODULUS as u128, + }, + PrimeOffsetSpec { + bits: 40, + offset: PRIME40_OFFSET195_OFFSET, + modulus: PRIME40_OFFSET195_MODULUS as u128, + }, + PrimeOffsetSpec { + bits: 48, + offset: PRIME48_OFFSET59_OFFSET, + modulus: PRIME48_OFFSET59_MODULUS as u128, + }, + PrimeOffsetSpec { + bits: 56, + offset: PRIME56_OFFSET27_OFFSET, + modulus: PRIME56_OFFSET27_MODULUS as u128, + }, + PrimeOffsetSpec { + bits: 64, + offset: PRIME64_OFFSET59_OFFSET, + modulus: PRIME64_OFFSET59_MODULUS as u128, + }, + PrimeOffsetSpec { + bits: 128, + offset: PRIME128_OFFSET275_OFFSET, + modulus: PRIME128_OFFSET275_MODULUS, + }, +]; + +// All PseudoMersenneField impls for Fp32/Fp64/Fp128 are blanket impls in +// their respective modules (fp32.rs, fp64.rs, fp128.rs). diff --git a/crates/jolt-field/src/prime/util.rs b/crates/jolt-field/src/prime/util.rs new file mode 100644 index 0000000000..5252ace6de --- /dev/null +++ b/crates/jolt-field/src/prime/util.rs @@ -0,0 +1,46 @@ +//! Shared helpers for field arithmetic backends. + +#![cfg_attr( + all(target_arch = "x86_64", target_feature = "bmi2"), + expect( + clippy::undocumented_unsafe_blocks, + reason = "the BMI2 intrinsic is gated by its required target feature" + ) +)] + +#[inline(always)] +pub(crate) const fn is_pow2_u64(x: u64) -> bool { + x.is_power_of_two() +} + +#[inline(always)] +pub(crate) const fn log2_pow2_u64(mut x: u64) -> u32 { + let mut k = 0u32; + while x > 1 { + x >>= 1; + k += 1; + } + k +} + +/// `a * b` widening to 128 bits; returns `(lo64, hi64)`. +#[inline(always)] +pub(crate) fn mul64_wide(a: u64, b: u64) -> (u64, u64) { + #[cfg(all(target_arch = "x86_64", target_feature = "bmi2"))] + { + unsafe { mul64_wide_bmi2(a, b) } + } + #[cfg(not(all(target_arch = "x86_64", target_feature = "bmi2")))] + { + let prod = (a as u128) * (b as u128); + (prod as u64, (prod >> 64) as u64) + } +} + +#[cfg(all(target_arch = "x86_64", target_feature = "bmi2"))] +#[inline(always)] +unsafe fn mul64_wide_bmi2(a: u64, b: u64) -> (u64, u64) { + let mut hi = 0; + let lo = unsafe { std::arch::x86_64::_mulx_u64(a, b, &mut hi) }; + (lo, hi) +} diff --git a/crates/jolt-field/src/solinas_traits.rs b/crates/jolt-field/src/solinas_traits.rs new file mode 100644 index 0000000000..4cd73ed2d5 --- /dev/null +++ b/crates/jolt-field/src/solinas_traits.rs @@ -0,0 +1,64 @@ +use crate::{FieldCore, FromPrimitiveInt}; +use num_traits::Zero; + +/// Canonical integer representation for a prime-field element. +pub trait CanonicalField: FieldCore + FromPrimitiveInt { + /// Returns the unique representative in `[0, p)`. + fn to_canonical_u128(self) -> u128; + + /// Returns the bit width of the field modulus. + fn modulus_bits() -> u32; + + /// Constructs an element when `val` is a canonical representative. + fn from_canonical_u128_checked(val: u128) -> Option; + + /// Constructs an element by reducing `val` modulo the field modulus. + fn from_canonical_u128_reduced(val: u128) -> Self; +} + +/// Field types with a cheap division-by-two operation. +pub trait HalvingField: FieldCore { + /// Divides this element by two. + fn half(self) -> Self; + + /// Returns the multiplicative inverse of two. + #[inline] + fn two_inv() -> Self { + Self::one().half() + } +} + +/// Balanced signed-digit lookup support for small power-of-two bases. +pub trait BalancedDigitLookup: FromPrimitiveInt + Zero + Copy { + /// Builds the balanced digit table for `1 <= log_basis <= 6`. + fn digit_lut(log_basis: u32) -> [Self; 64] { + debug_assert!(log_basis > 0 && log_basis <= 6); + let basis = 1usize << log_basis; + let half_basis = (basis >> 1) as i64; + std::array::from_fn(|i| { + if i < basis { + Self::from_i64(i as i64 - half_basis) + } else { + Self::zero() + } + }) + } +} + +/// Metadata for a pseudo-Mersenne modulus `2^k - c`. +pub trait PseudoMersenneField: CanonicalField { + /// Exponent `k` in `2^k - c`. + const MODULUS_BITS: u32; + + /// Offset `c` in `2^k - c`. + const MODULUS_OFFSET: u128; +} + +/// Field with a precomputed primitive root of a supported smooth subgroup. +pub trait SmoothFftField: CanonicalField + PseudoMersenneField { + /// Order of the supported smooth multiplicative subgroup. + const SMOOTH_SUBGROUP_ORDER: usize; + + /// Canonical representation of its primitive root. + const SMOOTH_OMEGA: u128; +} diff --git a/crates/jolt-field/src/unreduced/accum.rs b/crates/jolt-field/src/unreduced/accum.rs new file mode 100644 index 0000000000..5d86f9e941 --- /dev/null +++ b/crates/jolt-field/src/unreduced/accum.rs @@ -0,0 +1,543 @@ +//! Delayed-reduction product accumulators. +//! +//! Each accumulator widens field products into `u128` limbs so a batch of +//! products can be summed without intermediate modular reduction, then +//! reduced once via the owning field's `HasUnreducedOps` impl. + +use super::*; + +/// Accumulator for `Fp32 × u64` and `Fp32 × Fp32` products. +/// +/// Products are split into two 64-bit limbs stored as u128 slots. The second +/// limb is zero for `Fp32 × Fp32` products. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Fp32ProductAccum(pub [u128; 2]); + +impl Fp32ProductAccum { + /// Additive identity accumulator. + pub const ZERO: Self = Self([0; 2]); + + /// Reduce accumulated products to a canonical `Fp32

`. + #[inline] + pub fn reduce(self) -> Fp32

{ + let [s0, s1] = self.0; + let a = Fp32::

::from_canonical_u128_reduced(s0); + let b = Fp32::

::from_canonical_u128_reduced(s1); + let shift = Fp32::

::from_canonical_u32(Fp32::

::SHIFT64_MOD_P); + a + b * shift + } +} + +impl From> for Fp32ProductAccum { + #[inline] + fn from(x: Fp32

) -> Self { + Self([x.to_limbs() as u128, 0]) + } +} + +impl Add for Fp32ProductAccum { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self([ + self.0[0].wrapping_add(rhs.0[0]), + self.0[1].wrapping_add(rhs.0[1]), + ]) + } +} +impl AddAssign for Fp32ProductAccum { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.0[0] = self.0[0].wrapping_add(rhs.0[0]); + self.0[1] = self.0[1].wrapping_add(rhs.0[1]); + } +} +impl Sub for Fp32ProductAccum { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self([ + self.0[0].wrapping_sub(rhs.0[0]), + self.0[1].wrapping_sub(rhs.0[1]), + ]) + } +} +impl SubAssign for Fp32ProductAccum { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.0[0] = self.0[0].wrapping_sub(rhs.0[0]); + self.0[1] = self.0[1].wrapping_sub(rhs.0[1]); + } +} +impl Neg for Fp32ProductAccum { + type Output = Self; + #[inline] + fn neg(self) -> Self { + Self([self.0[0].wrapping_neg(), self.0[1].wrapping_neg()]) + } +} + +/// Accumulator for `FpExt4` products with delayed reduction. +/// +/// Each slot holds the unreduced u128 sum for one of the 4 ring-subfield +/// coefficients. The fused polynomial-multiply + φ(X)-reduction is already +/// applied in the formulas — only the per-coefficient Solinas reduction +/// (`from_canonical_u128_reduced`) is deferred. +/// +/// Headroom: each single product contributes at most 7 × P² ≈ 2^65 per +/// slot (slot 0 is the worst case). The u128 capacity of 2^128 allows up +/// to 2^63 accumulations before overflow. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FpExt4Fp32ProductAccum(pub [u128; 4]); + +impl FpExt4Fp32ProductAccum { + /// Additive identity accumulator. + pub const ZERO: Self = Self([0; 4]); + + /// Reduce accumulated unreduced coefficients to a canonical + /// `FpExt4>`. + #[inline] + pub fn reduce(self) -> [Fp32

; 4] { + [ + Fp32::

::from_canonical_u128_reduced(self.0[0]), + Fp32::

::from_canonical_u128_reduced(self.0[1]), + Fp32::

::from_canonical_u128_reduced(self.0[2]), + Fp32::

::from_canonical_u128_reduced(self.0[3]), + ] + } +} + +impl Add for FpExt4Fp32ProductAccum { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self([ + self.0[0].wrapping_add(rhs.0[0]), + self.0[1].wrapping_add(rhs.0[1]), + self.0[2].wrapping_add(rhs.0[2]), + self.0[3].wrapping_add(rhs.0[3]), + ]) + } +} +impl AddAssign for FpExt4Fp32ProductAccum { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.0[0] = self.0[0].wrapping_add(rhs.0[0]); + self.0[1] = self.0[1].wrapping_add(rhs.0[1]); + self.0[2] = self.0[2].wrapping_add(rhs.0[2]); + self.0[3] = self.0[3].wrapping_add(rhs.0[3]); + } +} +impl Sub for FpExt4Fp32ProductAccum { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self([ + self.0[0].wrapping_sub(rhs.0[0]), + self.0[1].wrapping_sub(rhs.0[1]), + self.0[2].wrapping_sub(rhs.0[2]), + self.0[3].wrapping_sub(rhs.0[3]), + ]) + } +} +impl SubAssign for FpExt4Fp32ProductAccum { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.0[0] = self.0[0].wrapping_sub(rhs.0[0]); + self.0[1] = self.0[1].wrapping_sub(rhs.0[1]); + self.0[2] = self.0[2].wrapping_sub(rhs.0[2]); + self.0[3] = self.0[3].wrapping_sub(rhs.0[3]); + } +} +impl Neg for FpExt4Fp32ProductAccum { + type Output = Self; + #[inline] + fn neg(self) -> Self { + Self([ + self.0[0].wrapping_neg(), + self.0[1].wrapping_neg(), + self.0[2].wrapping_neg(), + self.0[3].wrapping_neg(), + ]) + } +} + +/// Accumulator for `Fp64 × u64` products (also used for `Fp64 × Fp64`). +/// +/// Each product is ≤ 128 bits, split into two u64 halves stored as u128 slots. +/// Headroom: 2^64 additions per slot before overflow. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Fp64ProductAccum(pub [u128; 2]); + +impl Fp64ProductAccum { + /// Additive identity accumulator. + pub const ZERO: Self = Self([0; 2]); + + /// Reduce accumulated products to a canonical `Fp64

`. + #[inline] + pub fn reduce(self) -> Fp64

{ + let [s0, s1] = self.0; + // s0 = Σ lo_i, s1 = Σ hi_i; value = s0 + s1 * 2^64 + let a = Fp64::

::solinas_reduce(s0); + let b = Fp64::

::solinas_reduce(s1); + let shift = Fp64::

::solinas_reduce(1u128 << 64); + let b_shifted = Fp64::

::solinas_reduce(b.mul_wide_u64(shift.to_limbs())); + a + b_shifted + } +} + +impl From> for Fp64ProductAccum { + #[inline] + fn from(x: Fp64

) -> Self { + Self([x.to_limbs() as u128, 0]) + } +} + +impl Add for Fp64ProductAccum { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self([ + self.0[0].wrapping_add(rhs.0[0]), + self.0[1].wrapping_add(rhs.0[1]), + ]) + } +} +impl AddAssign for Fp64ProductAccum { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.0[0] = self.0[0].wrapping_add(rhs.0[0]); + self.0[1] = self.0[1].wrapping_add(rhs.0[1]); + } +} +impl Sub for Fp64ProductAccum { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self([ + self.0[0].wrapping_sub(rhs.0[0]), + self.0[1].wrapping_sub(rhs.0[1]), + ]) + } +} +impl SubAssign for Fp64ProductAccum { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.0[0] = self.0[0].wrapping_sub(rhs.0[0]); + self.0[1] = self.0[1].wrapping_sub(rhs.0[1]); + } +} +impl Neg for Fp64ProductAccum { + type Output = Self; + #[inline] + fn neg(self) -> Self { + Self([self.0[0].wrapping_neg(), self.0[1].wrapping_neg()]) + } +} + +/// Accumulator for `FpExt2` products with delayed reduction. +/// +/// Each coefficient is stored as an `Fp64ProductAccum` (lo64/hi64 limb-split). +/// This avoids carry-chain arithmetic -- addition is `wrapping_add` per slot. +/// Reduction delegates to `Fp64ProductAccum::reduce` per coefficient. +/// +/// Headroom: each `Fp64ProductAccum` slot holds u64 halves in u128, +/// so 2^64 accumulations before overflow. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FpExt2Fp64ProductAccum(pub [u128; 4]); + +impl FpExt2Fp64ProductAccum { + /// Additive identity accumulator. + pub const ZERO: Self = Self([0; 4]); + + /// Reduce accumulated products to a canonical `[Fp64

; 2]`. + #[inline] + pub fn reduce(self) -> [Fp64

; 2] { + [ + Fp64ProductAccum([self.0[0], self.0[1]]).reduce::

(), + Fp64ProductAccum([self.0[2], self.0[3]]).reduce::

(), + ] + } +} + +impl Add for FpExt2Fp64ProductAccum { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self([ + self.0[0].wrapping_add(rhs.0[0]), + self.0[1].wrapping_add(rhs.0[1]), + self.0[2].wrapping_add(rhs.0[2]), + self.0[3].wrapping_add(rhs.0[3]), + ]) + } +} +impl AddAssign for FpExt2Fp64ProductAccum { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.0[0] = self.0[0].wrapping_add(rhs.0[0]); + self.0[1] = self.0[1].wrapping_add(rhs.0[1]); + self.0[2] = self.0[2].wrapping_add(rhs.0[2]); + self.0[3] = self.0[3].wrapping_add(rhs.0[3]); + } +} +impl Sub for FpExt2Fp64ProductAccum { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self([ + self.0[0].wrapping_sub(rhs.0[0]), + self.0[1].wrapping_sub(rhs.0[1]), + self.0[2].wrapping_sub(rhs.0[2]), + self.0[3].wrapping_sub(rhs.0[3]), + ]) + } +} +impl SubAssign for FpExt2Fp64ProductAccum { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.0[0] = self.0[0].wrapping_sub(rhs.0[0]); + self.0[1] = self.0[1].wrapping_sub(rhs.0[1]); + self.0[2] = self.0[2].wrapping_sub(rhs.0[2]); + self.0[3] = self.0[3].wrapping_sub(rhs.0[3]); + } +} +impl Neg for FpExt2Fp64ProductAccum { + type Output = Self; + #[inline] + fn neg(self) -> Self { + Self([ + self.0[0].wrapping_neg(), + self.0[1].wrapping_neg(), + self.0[2].wrapping_neg(), + self.0[3].wrapping_neg(), + ]) + } +} + +/// Accumulator for `Fp128 × u64` products. +/// +/// Each `mul_wide_u64` produces 3 u64 limbs; stored as `[u128; 3]`. +/// Headroom: 2^64 additions per slot. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Fp128MulU64Accum(pub [u128; 3]); + +impl Fp128MulU64Accum { + /// Additive identity accumulator. + pub const ZERO: Self = Self([0; 3]); + + /// Reduce to canonical `Fp128

`. + #[inline] + pub fn reduce(self) -> Fp128

{ + let [s0, s1, s2] = self.0; + let c0 = s0 >> 64; + let r0 = s0 as u64; + let t1 = s1 + c0; + let r1 = t1 as u64; + let c1 = t1 >> 64; + let t2 = s2 + c1; + let r2 = t2 as u64; + let r3 = (t2 >> 64) as u64; + Fp128::

::solinas_reduce(&[r0, r1, r2, r3]) + } +} + +impl From> for Fp128MulU64Accum { + #[inline] + fn from(x: Fp128

) -> Self { + let [lo, hi] = x.to_limbs(); + Self([lo as u128, hi as u128, 0]) + } +} + +impl Add for Fp128MulU64Accum { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self([ + self.0[0] + rhs.0[0], + self.0[1] + rhs.0[1], + self.0[2] + rhs.0[2], + ]) + } +} +impl AddAssign for Fp128MulU64Accum { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.0[0] += rhs.0[0]; + self.0[1] += rhs.0[1]; + self.0[2] += rhs.0[2]; + } +} +impl Sub for Fp128MulU64Accum { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self([ + self.0[0].wrapping_sub(rhs.0[0]), + self.0[1].wrapping_sub(rhs.0[1]), + self.0[2].wrapping_sub(rhs.0[2]), + ]) + } +} +impl SubAssign for Fp128MulU64Accum { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.0[0] = self.0[0].wrapping_sub(rhs.0[0]); + self.0[1] = self.0[1].wrapping_sub(rhs.0[1]); + self.0[2] = self.0[2].wrapping_sub(rhs.0[2]); + } +} +impl Neg for Fp128MulU64Accum { + type Output = Self; + #[inline] + fn neg(self) -> Self { + Self([ + self.0[0].wrapping_neg(), + self.0[1].wrapping_neg(), + self.0[2].wrapping_neg(), + ]) + } +} + +/// Accumulator for `Fp128 × Fp128` products. +/// +/// Each `mul_wide` produces 4 u64 limbs; stored as `[u128; 4]`. +/// Headroom: 2^64 additions per slot. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Fp128ProductAccum(pub [u128; 4]); + +impl Fp128ProductAccum { + /// Additive identity accumulator. + pub const ZERO: Self = Self([0; 4]); + + /// Reduce to canonical `Fp128

`. + #[inline] + pub fn reduce(self) -> Fp128

{ + let [s0, s1, s2, s3] = self.0; + let c0 = s0 >> 64; + let r0 = s0 as u64; + let t1 = s1 + c0; + let r1 = t1 as u64; + let c1 = t1 >> 64; + let t2 = s2 + c1; + let r2 = t2 as u64; + let c2 = t2 >> 64; + let t3 = s3 + c2; + let r3 = t3 as u64; + let r4 = (t3 >> 64) as u64; + Fp128::

::solinas_reduce(&[r0, r1, r2, r3, r4]) + } +} + +impl From> for Fp128ProductAccum { + #[inline] + fn from(x: Fp128

) -> Self { + let [lo, hi] = x.to_limbs(); + Self([lo as u128, hi as u128, 0, 0]) + } +} + +impl Add for Fp128ProductAccum { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self([ + self.0[0].wrapping_add(rhs.0[0]), + self.0[1].wrapping_add(rhs.0[1]), + self.0[2].wrapping_add(rhs.0[2]), + self.0[3].wrapping_add(rhs.0[3]), + ]) + } +} +impl AddAssign for Fp128ProductAccum { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.0[0] = self.0[0].wrapping_add(rhs.0[0]); + self.0[1] = self.0[1].wrapping_add(rhs.0[1]); + self.0[2] = self.0[2].wrapping_add(rhs.0[2]); + self.0[3] = self.0[3].wrapping_add(rhs.0[3]); + } +} +impl Sub for Fp128ProductAccum { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self([ + self.0[0].wrapping_sub(rhs.0[0]), + self.0[1].wrapping_sub(rhs.0[1]), + self.0[2].wrapping_sub(rhs.0[2]), + self.0[3].wrapping_sub(rhs.0[3]), + ]) + } +} +impl SubAssign for Fp128ProductAccum { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.0[0] = self.0[0].wrapping_sub(rhs.0[0]); + self.0[1] = self.0[1].wrapping_sub(rhs.0[1]); + self.0[2] = self.0[2].wrapping_sub(rhs.0[2]); + self.0[3] = self.0[3].wrapping_sub(rhs.0[3]); + } +} +impl Neg for Fp128ProductAccum { + type Output = Self; + #[inline] + fn neg(self) -> Self { + Self([ + self.0[0].wrapping_neg(), + self.0[1].wrapping_neg(), + self.0[2].wrapping_neg(), + self.0[3].wrapping_neg(), + ]) + } +} + +/// Pair accumulator for extension fields. +/// +/// Wraps two base-field accumulators `(c0, c1)` component-wise. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AccumPair(pub A, pub A); + +impl Add for AccumPair { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self(self.0 + rhs.0, self.1 + rhs.1) + } +} +impl AddAssign for AccumPair { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + self.1 += rhs.1; + } +} +impl Sub for AccumPair { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self(self.0 - rhs.0, self.1 - rhs.1) + } +} +impl SubAssign for AccumPair { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.0 -= rhs.0; + self.1 -= rhs.1; + } +} +impl Neg for AccumPair { + type Output = Self; + #[inline] + fn neg(self) -> Self { + Self(-self.0, -self.1) + } +} diff --git a/crates/jolt-field/src/unreduced/mod.rs b/crates/jolt-field/src/unreduced/mod.rs new file mode 100644 index 0000000000..649258c816 --- /dev/null +++ b/crates/jolt-field/src/unreduced/mod.rs @@ -0,0 +1,810 @@ +//! Wide unreduced field accumulators for carry-free signed addition. +//! +//! Each type splits a canonical field element into 16-bit limbs stored in +//! `i32` slots. Addition and negation are element-wise i32 ops — no carry +//! propagation, no modular reduction. Reduction back to canonical form +//! happens once after accumulation via +//! [`reduce`](crate::unreduced::Fp128x8i32::reduce). +//! +//! The i32 overflow budget is `i32::MAX / u16::MAX ≈ 32,769` signed +//! additions before any limb can overflow. + +#![cfg_attr( + target_arch = "aarch64", + expect( + clippy::undocumented_unsafe_blocks, + reason = "ported NEON accumulator operations retain their audited lane invariants" + ) +)] + +use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; + +use crate::{AdditiveGroup, CanonicalField, FieldCore}; + +use super::prime::{Fp128, Fp32, Fp64}; + +mod accum; +mod native_algebra; +pub use accum::*; + +/// Wide unreduced accumulator for `Fp32`: 2 × i32 limbs (16-bit data each). +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct Fp32x2i32(pub [i32; 2]); + +impl Fp32x2i32 { + /// Additive identity accumulator. + pub const ZERO: Self = Self([0; 2]); + + /// Returns the zero accumulator. + #[inline] + pub fn zero() -> Self { + Self::ZERO + } +} + +impl From> for Fp32x2i32 { + #[inline] + fn from(x: Fp32

) -> Self { + let v = x.0; + Self([(v & 0xFFFF) as i32, (v >> 16) as i32]) + } +} + +impl Fp32x2i32 { + /// Multiply every limb by a small signed scalar. + /// + /// Safe when `|small| * max_limb_magnitude` fits in i32. After `From`, + /// limbs are in `[0, 0xFFFF]`, so `|small| ≤ 32_767` is safe for a single + /// product. For accumulation of `k` scaled values, require + /// `k * |small| * 0xFFFF < i32::MAX`, i.e. roughly `k * |small| < 32_768`. + #[inline] + pub fn scale_i32(self, small: i32) -> Self { + Self([self.0[0] * small, self.0[1] * small]) + } + + /// Reduce back to canonical `Fp32

`. + /// + /// Carry-propagates the i32 limbs into a signed value, normalizes to + /// `[0, p)`, and returns the canonical field element. + #[inline] + pub fn reduce(self) -> Fp32

{ + let [l0, l1] = self.0; + // Carry-propagate: value = l0 + l1 * 2^16 + let wide = l0 as i64 + (l1 as i64) * (1i64 << 16); + // Normalize to [0, p) + let p = P as i64; + let normalized = ((wide % p) + p) % p; + Fp32::from_canonical_u32(normalized as u32) + } +} + +impl Add for Fp32x2i32 { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self([self.0[0] + rhs.0[0], self.0[1] + rhs.0[1]]) + } +} + +impl AddAssign for Fp32x2i32 { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.0[0] += rhs.0[0]; + self.0[1] += rhs.0[1]; + } +} + +impl Sub for Fp32x2i32 { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self([self.0[0] - rhs.0[0], self.0[1] - rhs.0[1]]) + } +} + +impl SubAssign for Fp32x2i32 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.0[0] -= rhs.0[0]; + self.0[1] -= rhs.0[1]; + } +} + +impl Neg for Fp32x2i32 { + type Output = Self; + #[inline] + fn neg(self) -> Self { + Self([-self.0[0], -self.0[1]]) + } +} + +/// Wide unreduced accumulator for `Fp64`: 4 × i32 limbs (16-bit data each). +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct Fp64x4i32(pub [i32; 4]); + +impl Fp64x4i32 { + /// Additive identity accumulator. + pub const ZERO: Self = Self([0; 4]); + + /// Returns the zero accumulator. + #[inline] + pub fn zero() -> Self { + Self::ZERO + } +} + +impl From> for Fp64x4i32 { + #[inline] + fn from(x: Fp64

) -> Self { + let v = x.0; + Self([ + (v & 0xFFFF) as i32, + ((v >> 16) & 0xFFFF) as i32, + ((v >> 32) & 0xFFFF) as i32, + ((v >> 48) & 0xFFFF) as i32, + ]) + } +} + +impl Fp64x4i32 { + /// Multiply every limb by a small signed scalar. See [`Fp32x2i32::scale_i32`]. + #[inline] + pub fn scale_i32(self, small: i32) -> Self { + Self([ + self.0[0] * small, + self.0[1] * small, + self.0[2] * small, + self.0[3] * small, + ]) + } + + /// Reduce back to canonical `Fp64

`. + #[inline] + pub fn reduce(self) -> Fp64

{ + let [l0, l1, l2, l3] = self.0; + // Carry-propagate: value = l0 + l1*2^16 + l2*2^32 + l3*2^48 + let wide = l0 as i128 + + (l1 as i128) * (1i128 << 16) + + (l2 as i128) * (1i128 << 32) + + (l3 as i128) * (1i128 << 48); + let p = P as i128; + let normalized = ((wide % p) + p) % p; + Fp64::

::from_canonical_u64(normalized as u64) + } +} + +#[cfg(target_arch = "aarch64")] +impl Add for Fp64x4i32 { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + unsafe { + use std::arch::aarch64::*; + let a = vld1q_s32(self.0.as_ptr()); + let b = vld1q_s32(rhs.0.as_ptr()); + let mut out = [0i32; 4]; + vst1q_s32(out.as_mut_ptr(), vaddq_s32(a, b)); + Self(out) + } + } +} + +#[cfg(target_arch = "aarch64")] +impl AddAssign for Fp64x4i32 { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +#[cfg(target_arch = "aarch64")] +impl Sub for Fp64x4i32 { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + unsafe { + use std::arch::aarch64::*; + let a = vld1q_s32(self.0.as_ptr()); + let b = vld1q_s32(rhs.0.as_ptr()); + let mut out = [0i32; 4]; + vst1q_s32(out.as_mut_ptr(), vsubq_s32(a, b)); + Self(out) + } + } +} + +#[cfg(target_arch = "aarch64")] +impl SubAssign for Fp64x4i32 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +#[cfg(target_arch = "aarch64")] +impl Neg for Fp64x4i32 { + type Output = Self; + #[inline] + fn neg(self) -> Self { + unsafe { + use std::arch::aarch64::*; + let a = vld1q_s32(self.0.as_ptr()); + let mut out = [0i32; 4]; + vst1q_s32(out.as_mut_ptr(), vnegq_s32(a)); + Self(out) + } + } +} + +#[cfg(not(target_arch = "aarch64"))] +impl Add for Fp64x4i32 { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self([ + self.0[0] + rhs.0[0], + self.0[1] + rhs.0[1], + self.0[2] + rhs.0[2], + self.0[3] + rhs.0[3], + ]) + } +} + +#[cfg(not(target_arch = "aarch64"))] +impl AddAssign for Fp64x4i32 { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.0[0] += rhs.0[0]; + self.0[1] += rhs.0[1]; + self.0[2] += rhs.0[2]; + self.0[3] += rhs.0[3]; + } +} + +#[cfg(not(target_arch = "aarch64"))] +impl Sub for Fp64x4i32 { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self([ + self.0[0] - rhs.0[0], + self.0[1] - rhs.0[1], + self.0[2] - rhs.0[2], + self.0[3] - rhs.0[3], + ]) + } +} + +#[cfg(not(target_arch = "aarch64"))] +impl SubAssign for Fp64x4i32 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.0[0] -= rhs.0[0]; + self.0[1] -= rhs.0[1]; + self.0[2] -= rhs.0[2]; + self.0[3] -= rhs.0[3]; + } +} + +#[cfg(not(target_arch = "aarch64"))] +impl Neg for Fp64x4i32 { + type Output = Self; + #[inline] + fn neg(self) -> Self { + Self([-self.0[0], -self.0[1], -self.0[2], -self.0[3]]) + } +} + +/// Wide unreduced accumulator for `Fp128`: 8 × i32 limbs (16-bit data each). +/// +/// On AVX2, one element fits a single 256-bit YMM register. On NEON, it +/// spans two 128-bit Q registers. All arithmetic is carry-free element-wise +/// i32 operations. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct Fp128x8i32(pub [i32; 8]); + +impl Fp128x8i32 { + /// Additive identity accumulator. + pub const ZERO: Self = Self([0; 8]); + + /// Returns the zero accumulator. + #[inline] + pub fn zero() -> Self { + Self::ZERO + } +} + +impl From> for Fp128x8i32 { + #[inline] + fn from(x: Fp128

) -> Self { + let lo = x.0[0]; + let hi = x.0[1]; + Self([ + (lo & 0xFFFF) as i32, + ((lo >> 16) & 0xFFFF) as i32, + ((lo >> 32) & 0xFFFF) as i32, + ((lo >> 48) & 0xFFFF) as i32, + (hi & 0xFFFF) as i32, + ((hi >> 16) & 0xFFFF) as i32, + ((hi >> 32) & 0xFFFF) as i32, + ((hi >> 48) & 0xFFFF) as i32, + ]) + } +} + +impl Fp128x8i32 { + /// Multiply every limb by a small signed scalar. See [`Fp32x2i32::scale_i32`]. + #[inline] + pub fn scale_i32(self, small: i32) -> Self { + Self([ + self.0[0] * small, + self.0[1] * small, + self.0[2] * small, + self.0[3] * small, + self.0[4] * small, + self.0[5] * small, + self.0[6] * small, + self.0[7] * small, + ]) + } + + /// Reduce back to canonical `Fp128

`. + /// + /// Carry-propagates the 8 × i32 limbs into unsigned u64 limbs, then + /// applies Solinas reduction. + #[inline] + pub fn reduce(self) -> Fp128

{ + let limbs = self.0; + + // Carry-propagate from low to high, accumulating into i64 slots. + // Each i32 limb can be in [-32769*65535, 32769*65535] ≈ ±2^31. + // After propagation, each 16-bit "digit" is in [0, 65535] and we + // may have a signed residual in the top that overflows 128 bits. + let mut carry: i64 = 0; + let mut digits = [0u16; 8]; + for i in 0..8 { + let v = limbs[i] as i64 + carry; + // Arithmetic right-shift to propagate sign correctly + digits[i] = (v & 0xFFFF) as u16; + carry = v >> 16; + } + + // Reassemble into u64 limbs + let lo = digits[0] as u64 + | (digits[1] as u64) << 16 + | (digits[2] as u64) << 32 + | (digits[3] as u64) << 48; + let hi = digits[4] as u64 + | (digits[5] as u64) << 16 + | (digits[6] as u64) << 32 + | (digits[7] as u64) << 48; + + // p = 2^128 - c, so 2^128 ≡ c (mod p). + // value = lo + hi*2^64 + carry*2^128 ≡ lo + hi*2^64 + carry*c (mod p). + let c = Fp128::

::C_LO; + match carry.cmp(&0) { + std::cmp::Ordering::Equal => { + Fp128::

::from_canonical_u128_reduced(lo as u128 | (hi as u128) << 64) + } + std::cmp::Ordering::Greater => Fp128::

::solinas_reduce(&[lo, hi, carry as u64]), + std::cmp::Ordering::Less => { + // carry < 0: value = base - |carry|*c. + let neg_carry = (-carry) as u64; + let sub = neg_carry as u128 * c as u128; + let base = lo as u128 | (hi as u128) << 64; + if base >= sub { + Fp128::

::from_canonical_u128_reduced(base - sub) + } else { + let diff = sub - base; + Fp128::

::from_canonical_u128_reduced(P - diff) + } + } + } + } +} + +#[cfg(target_arch = "aarch64")] +impl Add for Fp128x8i32 { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + unsafe { + use std::arch::aarch64::*; + let a0 = vld1q_s32(self.0.as_ptr()); + let a1 = vld1q_s32(self.0.as_ptr().add(4)); + let b0 = vld1q_s32(rhs.0.as_ptr()); + let b1 = vld1q_s32(rhs.0.as_ptr().add(4)); + let mut out = [0i32; 8]; + vst1q_s32(out.as_mut_ptr(), vaddq_s32(a0, b0)); + vst1q_s32(out.as_mut_ptr().add(4), vaddq_s32(a1, b1)); + Self(out) + } + } +} + +#[cfg(target_arch = "aarch64")] +impl AddAssign for Fp128x8i32 { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +#[cfg(target_arch = "aarch64")] +impl Sub for Fp128x8i32 { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + unsafe { + use std::arch::aarch64::*; + let a0 = vld1q_s32(self.0.as_ptr()); + let a1 = vld1q_s32(self.0.as_ptr().add(4)); + let b0 = vld1q_s32(rhs.0.as_ptr()); + let b1 = vld1q_s32(rhs.0.as_ptr().add(4)); + let mut out = [0i32; 8]; + vst1q_s32(out.as_mut_ptr(), vsubq_s32(a0, b0)); + vst1q_s32(out.as_mut_ptr().add(4), vsubq_s32(a1, b1)); + Self(out) + } + } +} + +#[cfg(target_arch = "aarch64")] +impl SubAssign for Fp128x8i32 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +#[cfg(target_arch = "aarch64")] +impl Neg for Fp128x8i32 { + type Output = Self; + #[inline] + fn neg(self) -> Self { + unsafe { + use std::arch::aarch64::*; + let a0 = vld1q_s32(self.0.as_ptr()); + let a1 = vld1q_s32(self.0.as_ptr().add(4)); + let mut out = [0i32; 8]; + vst1q_s32(out.as_mut_ptr(), vnegq_s32(a0)); + vst1q_s32(out.as_mut_ptr().add(4), vnegq_s32(a1)); + Self(out) + } + } +} + +#[cfg(not(target_arch = "aarch64"))] +impl Add for Fp128x8i32 { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self([ + self.0[0] + rhs.0[0], + self.0[1] + rhs.0[1], + self.0[2] + rhs.0[2], + self.0[3] + rhs.0[3], + self.0[4] + rhs.0[4], + self.0[5] + rhs.0[5], + self.0[6] + rhs.0[6], + self.0[7] + rhs.0[7], + ]) + } +} + +#[cfg(not(target_arch = "aarch64"))] +impl AddAssign for Fp128x8i32 { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.0[0] += rhs.0[0]; + self.0[1] += rhs.0[1]; + self.0[2] += rhs.0[2]; + self.0[3] += rhs.0[3]; + self.0[4] += rhs.0[4]; + self.0[5] += rhs.0[5]; + self.0[6] += rhs.0[6]; + self.0[7] += rhs.0[7]; + } +} + +#[cfg(not(target_arch = "aarch64"))] +impl Sub for Fp128x8i32 { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self([ + self.0[0] - rhs.0[0], + self.0[1] - rhs.0[1], + self.0[2] - rhs.0[2], + self.0[3] - rhs.0[3], + self.0[4] - rhs.0[4], + self.0[5] - rhs.0[5], + self.0[6] - rhs.0[6], + self.0[7] - rhs.0[7], + ]) + } +} + +#[cfg(not(target_arch = "aarch64"))] +impl SubAssign for Fp128x8i32 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.0[0] -= rhs.0[0]; + self.0[1] -= rhs.0[1]; + self.0[2] -= rhs.0[2]; + self.0[3] -= rhs.0[3]; + self.0[4] -= rhs.0[4]; + self.0[5] -= rhs.0[5]; + self.0[6] -= rhs.0[6]; + self.0[7] -= rhs.0[7]; + } +} + +#[cfg(not(target_arch = "aarch64"))] +impl Neg for Fp128x8i32 { + type Output = Self; + #[inline] + fn neg(self) -> Self { + Self([ + -self.0[0], -self.0[1], -self.0[2], -self.0[3], -self.0[4], -self.0[5], -self.0[6], + -self.0[7], + ]) + } +} + +/// Reduce a wide unreduced accumulator back to a canonical field element. +pub trait ReduceTo { + /// Carry-propagate and reduce to a canonical field element. + fn reduce(self) -> F; +} + +impl ReduceTo> for Fp32x2i32 { + #[inline] + fn reduce(self) -> Fp32

{ + Fp32x2i32::reduce::

(self) + } +} + +impl ReduceTo> for Fp64x4i32 { + #[inline] + fn reduce(self) -> Fp64

{ + Fp64x4i32::reduce::

(self) + } +} + +impl ReduceTo> for Fp128x8i32 { + #[inline] + fn reduce(self) -> Fp128

{ + Fp128x8i32::reduce::

(self) + } +} + +/// Precomputed fold context for `FpExt4>`. +/// +/// Stores a 4×4 multiplication matrix derived from the challenge `r`, +/// enabling fold via 4 scalar multiply-accumulates per coefficient +/// instead of the general 22-product ring multiplication. +#[derive(Debug, Clone, Copy)] +pub struct FoldMatrixFp32(pub(crate) [[u32; 4]; 4]); + +/// Precomputed fold context for `FpExt2, C>`. +/// +/// Stores the 2×2 "multiply by the challenge `r`" matrix in the `[1, u]` +/// basis (`u² = NR`) as canonical `u64` limbs. Folding then uses two +/// base-field products per output coordinate with a single delayed +/// reduction, instead of the generic per-element Karatsuba multiply that +/// reduces three times. +#[derive(Debug, Clone, Copy)] +pub struct FoldMatrixFp64(pub(crate) [[u64; 2]; 2]); + +/// Per-element fold optimization trait. +/// +/// Allows field types to precompute a fold context from challenge `r` +/// (e.g. a multiplication matrix) and apply it per-element. The loop +/// structure and parallelism live in the caller (`fold_evals_in_place`). +pub trait HasOptimizedFold: FieldCore { + /// Precomputed context for folding by a fixed challenge `r`. + type FoldCtx: Copy + Send + Sync; + + /// Build the fold context from challenge `r`. + fn precompute_fold(r: Self) -> Self::FoldCtx; + + /// Fold one element pair: `even + r*(odd - even)`. + fn fold_one(ctx: &Self::FoldCtx, even: Self, odd: Self) -> Self; +} + +/// Multi-level unreduced multiplication hierarchy. +/// +/// Provides `field × u64` and `field × field` widening multiplies that return +/// accumulator types supporting carry-free addition. Reduction back to a +/// canonical field element happens once after accumulation. +pub trait HasUnreducedOps: FieldCore { + /// Accumulator for `self × u64` products (narrower than full product). + type MulU64Accum: AdditiveGroup; + /// Accumulator for `self × self` products. + type ProductAccum: AdditiveGroup; + + /// Whether delayed reduction over `ProductAccum` is exact relative to + /// per-term `Mul` for the small product batches used by inner products. + /// + /// When `true`, `reduce_product_accum(sum_i mul_to_product_accum(a_i, b_i))` + /// equals `sum_i a_i * b_i` for batch sizes within the accumulator's + /// non-wrapping headroom. The conservative default is `false`; a field opts + /// in only once its accumulator is proven exact (see `FpExt4` + /// and `FpExt2`). Fields that leave it `false` keep the per-term reduce + /// path, so callers that must stay byte-identical to `Mul` are unaffected. + const DELAYED_PRODUCT_SUM_IS_EXACT: bool = false; + + /// Widening `self × small` with no reduction. + fn mul_u64_unreduced(self, small: u64) -> Self::MulU64Accum; + /// Widening `self × other` with no reduction. + fn mul_to_product_accum(self, other: Self) -> Self::ProductAccum; + + /// Reduce a narrow-mul accumulator to a canonical field element. + fn reduce_mul_u64_accum(accum: Self::MulU64Accum) -> Self; + /// Reduce a full-product accumulator to a canonical field element. + fn reduce_product_accum(accum: Self::ProductAccum) -> Self; +} + +macro_rules! impl_default_optimized_fold { + ($base:ident<$p:ident: $pty:ty>) => { + impl HasOptimizedFold for $base<$p> { + type FoldCtx = Self; + #[inline] + fn precompute_fold(r: Self) -> Self { + r + } + #[inline] + fn fold_one(r: &Self, even: Self, odd: Self) -> Self { + even + *r * (odd - even) + } + } + }; +} + +impl_default_optimized_fold!(Fp64); +impl_default_optimized_fold!(Fp32); +impl_default_optimized_fold!(Fp128); + +impl HasUnreducedOps for Fp64

{ + type MulU64Accum = Fp64ProductAccum; + type ProductAccum = Fp64ProductAccum; + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Fp64ProductAccum { + let wide = self.mul_wide_u64(small); + Fp64ProductAccum([wide & u64::MAX as u128, wide >> 64]) + } + + #[inline] + fn mul_to_product_accum(self, other: Self) -> Fp64ProductAccum { + let wide = self.mul_wide(other); + Fp64ProductAccum([wide & u64::MAX as u128, wide >> 64]) + } + + #[inline] + fn reduce_mul_u64_accum(accum: Fp64ProductAccum) -> Self { + accum.reduce::

() + } + + #[inline] + fn reduce_product_accum(accum: Fp64ProductAccum) -> Self { + accum.reduce::

() + } +} + +impl HasUnreducedOps for Fp32

{ + type MulU64Accum = Fp32ProductAccum; + type ProductAccum = Fp32ProductAccum; + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Fp32ProductAccum { + let wide = (self.to_limbs() as u128) * (small as u128); + Fp32ProductAccum([wide & u64::MAX as u128, wide >> 64]) + } + + #[inline] + fn mul_to_product_accum(self, other: Self) -> Fp32ProductAccum { + Fp32ProductAccum([self.mul_wide(other) as u128, 0]) + } + + #[inline] + fn reduce_mul_u64_accum(accum: Fp32ProductAccum) -> Self { + accum.reduce::

() + } + + #[inline] + fn reduce_product_accum(accum: Fp32ProductAccum) -> Self { + accum.reduce::

() + } +} + +impl HasUnreducedOps for Fp128

{ + type MulU64Accum = Fp128MulU64Accum; + type ProductAccum = Fp128ProductAccum; + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Fp128MulU64Accum { + let [lo, mid, hi] = self.mul_wide_u64(small); + Fp128MulU64Accum([lo as u128, mid as u128, hi as u128]) + } + + #[inline] + fn mul_to_product_accum(self, other: Self) -> Fp128ProductAccum { + let [r0, r1, r2, r3] = self.mul_wide(other); + Fp128ProductAccum([r0 as u128, r1 as u128, r2 as u128, r3 as u128]) + } + + #[inline] + fn reduce_mul_u64_accum(accum: Fp128MulU64Accum) -> Self { + accum.reduce::

() + } + + #[inline] + fn reduce_product_accum(accum: Fp128ProductAccum) -> Self { + accum.reduce::

() + } +} + +/// Element-wise scaling of a wide accumulator by a small signed integer. +pub trait ScaleI32 { + /// Scale each element by `small`. + fn scale_i32(self, small: i32) -> Self; +} + +impl ScaleI32 for Fp32x2i32 { + #[inline] + fn scale_i32(self, small: i32) -> Self { + self.scale_i32(small) + } +} + +impl ScaleI32 for Fp64x4i32 { + #[inline] + fn scale_i32(self, small: i32) -> Self { + self.scale_i32(small) + } +} + +impl ScaleI32 for Fp128x8i32 { + #[inline] + fn scale_i32(self, small: i32) -> Self { + self.scale_i32(small) + } +} + +/// Associates a field type with its wide unreduced accumulator. +pub trait HasWide: FieldCore { + /// The wide accumulator type. + type Wide: AdditiveGroup + From + ReduceTo + ScaleI32; + + /// Convert `self` to wide form and scale every limb by `small`. + /// + /// Equivalent to `Self::Wide::from(self).scale_i32(small)` but avoids + /// the trait-method ambiguity at call sites. + #[inline] + fn mul_small_to_wide(self, small: i32) -> Self::Wide { + Self::Wide::from(self).scale_i32(small) + } +} + +impl HasWide for Fp32

{ + type Wide = Fp32x2i32; +} + +impl HasWide for Fp64

{ + type Wide = Fp64x4i32; +} + +impl HasWide for Fp128

{ + type Wide = Fp128x8i32; +} + +#[cfg(test)] +mod tests; diff --git a/crates/jolt-field/src/unreduced/native_algebra.rs b/crates/jolt-field/src/unreduced/native_algebra.rs new file mode 100644 index 0000000000..616c4988f9 --- /dev/null +++ b/crates/jolt-field/src/unreduced/native_algebra.rs @@ -0,0 +1,94 @@ +//! Native `num_traits`/`std` supertrait impls for the wide unreduced +//! accumulator types and the generic [`AccumPair`]. +//! +//! These are the Jolt-free supertrait obligations of the native +//! [`AdditiveGroup`] hierarchy: `Zero` plus the `Add`/`Sub` by-reference +//! forwarders that `AdditiveGroup` requires. + +use std::ops::{Add, Sub}; + +use num_traits::Zero; + +use super::{ + AccumPair, Fp128MulU64Accum, Fp128ProductAccum, Fp128x8i32, Fp32ProductAccum, Fp32x2i32, + Fp64ProductAccum, Fp64x4i32, FpExt2Fp64ProductAccum, FpExt4Fp32ProductAccum, +}; +use crate::AdditiveGroup; + +macro_rules! impl_wide_native_additive { + ($ty:ty, $zero:expr) => { + impl Zero for $ty { + #[inline] + fn zero() -> Self { + $zero + } + + #[inline] + fn is_zero(&self) -> bool { + *self == Self::zero() + } + } + + impl<'a> Add<&'a Self> for $ty { + type Output = Self; + + #[inline] + fn add(self, rhs: &'a Self) -> Self::Output { + self + *rhs + } + } + + impl<'a> Sub<&'a Self> for $ty { + type Output = Self; + + #[inline] + fn sub(self, rhs: &'a Self) -> Self::Output { + self - *rhs + } + } + + impl AdditiveGroup for $ty {} + }; +} + +impl_wide_native_additive!(Fp32x2i32, Fp32x2i32([0; 2])); +impl_wide_native_additive!(Fp64x4i32, Fp64x4i32([0; 4])); +impl_wide_native_additive!(Fp128x8i32, Fp128x8i32([0; 8])); +impl_wide_native_additive!(Fp32ProductAccum, Fp32ProductAccum([0; 2])); +impl_wide_native_additive!(Fp64ProductAccum, Fp64ProductAccum([0; 2])); +impl_wide_native_additive!(Fp128MulU64Accum, Fp128MulU64Accum([0; 3])); +impl_wide_native_additive!(Fp128ProductAccum, Fp128ProductAccum([0; 4])); +impl_wide_native_additive!(FpExt4Fp32ProductAccum, FpExt4Fp32ProductAccum([0; 4])); +impl_wide_native_additive!(FpExt2Fp64ProductAccum, FpExt2Fp64ProductAccum([0; 4])); + +impl Zero for AccumPair { + #[inline] + fn zero() -> Self { + Self(A::zero(), A::zero()) + } + + #[inline] + fn is_zero(&self) -> bool { + self.0.is_zero() && self.1.is_zero() + } +} + +impl<'a, A: AdditiveGroup> Add<&'a Self> for AccumPair { + type Output = Self; + + #[inline] + fn add(self, rhs: &'a Self) -> Self::Output { + self + *rhs + } +} + +impl<'a, A: AdditiveGroup> Sub<&'a Self> for AccumPair { + type Output = Self; + + #[inline] + fn sub(self, rhs: &'a Self) -> Self::Output { + self - *rhs + } +} + +impl AdditiveGroup for AccumPair {} diff --git a/crates/jolt-field/src/unreduced/tests.rs b/crates/jolt-field/src/unreduced/tests.rs new file mode 100644 index 0000000000..730c60c898 --- /dev/null +++ b/crates/jolt-field/src/unreduced/tests.rs @@ -0,0 +1,262 @@ +#![expect( + clippy::unreadable_literal, + reason = "regression tests retain copied modulus constants" +)] + +use super::*; +use crate::RandomSampling; +use crate::{Prime128Offset275, Prime24Offset3, Prime40Offset195}; +use rand::rngs::StdRng; +use rand::SeedableRng; +use rand_core::RngCore; + +type F128 = Prime128Offset275; +type F32 = Prime24Offset3; +type F64 = Prime40Offset195; + +const P128: u128 = 0xfffffffffffffffffffffffffffffeed; +const P32: u32 = (1 << 24) - 3; +const P64: u64 = (1 << 40) - 195; + +#[test] +fn fp128_roundtrip() { + let mut rng = StdRng::seed_from_u64(0xdead_1234); + for _ in 0..1000 { + let a: F128 = RandomSampling::random(&mut rng); + let wide = Fp128x8i32::from(a); + let back = wide.reduce::(); + assert_eq!(a, back, "roundtrip failed for {a:?}"); + } +} + +#[test] +fn fp128_accumulate_matches_scalar() { + let mut rng = StdRng::seed_from_u64(0xbeef_cafe_4321); + let n = 1000; + let vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + + let scalar_sum = vals.iter().fold(F128::zero(), |acc, &x| acc + x); + + let wide_sum = vals + .iter() + .fold(Fp128x8i32::zero(), |acc, &x| acc + Fp128x8i32::from(x)); + let reduced = wide_sum.reduce::(); + + assert_eq!(scalar_sum, reduced); +} + +#[test] +fn fp128_add_sub_neg_match_scalar() { + let mut rng = StdRng::seed_from_u64(0x1122_3344_5566); + for _ in 0..500 { + let a: F128 = RandomSampling::random(&mut rng); + let b: F128 = RandomSampling::random(&mut rng); + + let wa = Fp128x8i32::from(a); + let wb = Fp128x8i32::from(b); + + assert_eq!((wa + wb).reduce::(), a + b); + assert_eq!((wa - wb).reduce::(), a - b); + assert_eq!((-wa).reduce::(), -a); + } +} + +#[test] +fn fp128_mixed_add_sub_stress() { + let mut rng = StdRng::seed_from_u64(0xaaaa_bbbb_cccc); + let n = 500; + let vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + + let mut scalar = F128::zero(); + let mut wide = Fp128x8i32::zero(); + for (i, &v) in vals.iter().enumerate() { + let wv = Fp128x8i32::from(v); + if i % 3 == 0 { + scalar -= v; + wide -= wv; + } else { + scalar += v; + wide += wv; + } + } + assert_eq!(wide.reduce::(), scalar); +} + +#[test] +fn fp32_roundtrip() { + let mut rng = StdRng::seed_from_u64(0x3232_3232); + for _ in 0..1000 { + let a: F32 = RandomSampling::random(&mut rng); + let wide = Fp32x2i32::from(a); + let back = wide.reduce::(); + assert_eq!(a, back); + } +} + +#[test] +fn fp32_accumulate_matches_scalar() { + let mut rng = StdRng::seed_from_u64(0x3232_abcd); + let n = 1000; + let vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + + let scalar_sum = vals.iter().fold(F32::zero(), |acc, &x| acc + x); + let wide_sum = vals + .iter() + .fold(Fp32x2i32::zero(), |acc, &x| acc + Fp32x2i32::from(x)); + assert_eq!(wide_sum.reduce::(), scalar_sum); +} + +#[test] +fn fp64_roundtrip() { + let mut rng = StdRng::seed_from_u64(0x6464_6464); + for _ in 0..1000 { + let a: F64 = RandomSampling::random(&mut rng); + let wide = Fp64x4i32::from(a); + let back = wide.reduce::(); + assert_eq!(a, back); + } +} + +#[test] +fn fp64_accumulate_matches_scalar() { + let mut rng = StdRng::seed_from_u64(0x6464_beef); + let n = 1000; + let vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + + let scalar_sum = vals.iter().fold(F64::zero(), |acc, &x| acc + x); + let wide_sum = vals + .iter() + .fold(Fp64x4i32::zero(), |acc, &x| acc + Fp64x4i32::from(x)); + assert_eq!(wide_sum.reduce::(), scalar_sum); +} + +#[test] +fn fp64_product_accum_matches_scalar() { + let mut rng = StdRng::seed_from_u64(0x6464_4444); + let n = 500; + let a_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let b_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + + let scalar_sum: F64 = a_vals + .iter() + .zip(b_vals.iter()) + .fold(F64::zero(), |acc, (&a, &b)| acc + a * b); + + let accum_sum = a_vals + .iter() + .zip(b_vals.iter()) + .fold(Fp64ProductAccum::ZERO, |acc, (&a, &b)| { + acc + a.mul_to_product_accum(b) + }); + assert_eq!(F64::reduce_product_accum(accum_sum), scalar_sum); +} + +#[test] +fn fp64_ext2_product_accum_matches_scalar() { + use crate::Ext2; + + type E = Ext2; + + let mut rng = StdRng::seed_from_u64(0x6464_4445); + let n = 500; + let a_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let b_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + + let scalar_sum: E = a_vals + .iter() + .zip(b_vals.iter()) + .fold(E::zero(), |acc, (&a, &b)| acc + a * b); + + let accum_sum = a_vals.iter().zip(b_vals.iter()).fold( + <::ProductAccum as num_traits::Zero>::zero(), + |acc, (&a, &b)| acc + a.mul_to_product_accum(b), + ); + assert_eq!(E::reduce_product_accum(accum_sum), scalar_sum); +} + +#[test] +fn fp64_mul_u64_accum_matches_scalar() { + let mut rng = StdRng::seed_from_u64(0x6464_5555); + let n = 500; + let a_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let b_vals: Vec = (0..n).map(|_| rng.next_u64() >> 32).collect(); + + let scalar_sum: F64 = a_vals + .iter() + .zip(b_vals.iter()) + .fold(F64::zero(), |acc, (&a, &b)| acc + a * F64::from_u64(b)); + + let accum_sum = a_vals + .iter() + .zip(b_vals.iter()) + .fold(Fp64ProductAccum::ZERO, |acc, (&a, &b)| { + acc + a.mul_u64_unreduced(b) + }); + assert_eq!(F64::reduce_mul_u64_accum(accum_sum), scalar_sum); +} + +#[test] +fn fp128_product_accum_matches_scalar() { + let mut rng = StdRng::seed_from_u64(0x0128_6666); + let n = 500; + let a_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let b_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + + let scalar_sum: F128 = a_vals + .iter() + .zip(b_vals.iter()) + .fold(F128::zero(), |acc, (&a, &b)| acc + a * b); + + let accum_sum = a_vals + .iter() + .zip(b_vals.iter()) + .fold(Fp128ProductAccum::ZERO, |acc, (&a, &b)| { + acc + a.mul_to_product_accum(b) + }); + assert_eq!(F128::reduce_product_accum(accum_sum), scalar_sum); +} + +#[test] +fn fp128_mul_u64_accum_matches_scalar() { + let mut rng = StdRng::seed_from_u64(0x0128_7777); + let n = 500; + let a_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let b_vals: Vec = (0..n).map(|_| rng.next_u64()).collect(); + + let scalar_sum: F128 = a_vals + .iter() + .zip(b_vals.iter()) + .fold(F128::zero(), |acc, (&a, &b)| acc + a * F128::from_u64(b)); + + let accum_sum = a_vals + .iter() + .zip(b_vals.iter()) + .fold(Fp128MulU64Accum::ZERO, |acc, (&a, &b)| { + acc + a.mul_u64_unreduced(b) + }); + assert_eq!(F128::reduce_mul_u64_accum(accum_sum), scalar_sum); +} + +#[test] +fn fp128_product_accum_sub_neg() { + let mut rng = StdRng::seed_from_u64(0x0128_8888); + let n = 500; + let a_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let b_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + + let mut scalar_sum = F128::zero(); + let mut accum_pos = Fp128ProductAccum::ZERO; + let mut accum_neg = Fp128ProductAccum::ZERO; + for (i, (&a, &b)) in a_vals.iter().zip(b_vals.iter()).enumerate() { + let prod = a.mul_to_product_accum(b); + if i % 2 == 0 { + scalar_sum += a * b; + accum_pos += prod; + } else { + scalar_sum -= a * b; + accum_neg += prod; + } + } + let result = F128::reduce_product_accum(accum_pos) - F128::reduce_product_accum(accum_neg); + assert_eq!(result, scalar_sum); +} From 86816df03a9bc9327dbad5083f2aba825c714602 Mon Sep 17 00:00:00 2001 From: acentelles Date: Thu, 16 Jul 2026 12:50:01 -0400 Subject: [PATCH 02/38] test(field): add Solinas benchmark and fuzz target Move the Criterion field-arithmetic benchmark from Akita as benches/solinas_field_arith (with plonky3 baselines as dev-only comparisons) and add a Solinas arithmetic fuzz target to the field fuzz workspace. --- Cargo.lock | 41 + crates/jolt-field/Cargo.toml | 9 + .../jolt-field/benches/solinas_field_arith.rs | 34 + .../benches/solinas_field_arith/arithmetic.rs | 696 +++++++++++++ .../benches/solinas_field_arith/base.rs | 64 ++ .../benches/solinas_field_arith/cases.rs | 25 + .../benches/solinas_field_arith/comparison.rs | 63 ++ .../benches/solinas_field_arith/data.rs | 17 + .../benches/solinas_field_arith/ext2.rs | 47 + .../benches/solinas_field_arith/ext4.rs | 53 + .../benches/solinas_field_arith/kernel.rs | 147 +++ .../benches/solinas_field_arith/mod.rs | 21 + .../benches/solinas_field_arith/parallel.rs | 235 +++++ .../benches/solinas_field_arith/params.rs | 55 + .../benches/solinas_field_arith/plonky3.rs | 957 ++++++++++++++++++ .../benches/solinas_field_arith/wide.rs | 121 +++ crates/jolt-field/fuzz/Cargo.lock | 21 + crates/jolt-field/fuzz/Cargo.toml | 7 +- .../fuzz/fuzz_targets/solinas_field_arith.rs | 40 + 19 files changed, 2652 insertions(+), 1 deletion(-) create mode 100644 crates/jolt-field/benches/solinas_field_arith.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/arithmetic.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/base.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/cases.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/comparison.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/data.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/ext2.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/ext4.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/kernel.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/mod.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/parallel.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/params.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/plonky3.rs create mode 100644 crates/jolt-field/benches/solinas_field_arith/wide.rs create mode 100644 crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs diff --git a/Cargo.lock b/Cargo.lock index adedaf8f76..8375aca643 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3274,6 +3274,10 @@ dependencies = [ "ark-std 0.5.0", "criterion", "num-traits", + "p3-baby-bear", + "p3-field", + "p3-koala-bear", + "p3-mersenne-31", "rand 0.8.5", "rand_chacha 0.3.1", "rand_core 0.6.4", @@ -4451,6 +4455,22 @@ dependencies = [ "jolt-sdk", ] +[[package]] +name = "p3-baby-bear" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bc665d4650710aedd2424a59d88c19cb94d85375defc87bf6264d4be25ae6fa" +dependencies = [ + "p3-challenger", + "p3-field", + "p3-mds", + "p3-monty-31", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "rand 0.10.1", +] + [[package]] name = "p3-challenger" version = "0.5.3" @@ -4546,6 +4566,27 @@ dependencies = [ "rand 0.10.1", ] +[[package]] +name = "p3-mersenne-31" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "012351fb727ba404175ea1cc159fb6234fa370ce9399317ba04d38ca43e55bfa" +dependencies = [ + "itertools 0.14.0", + "num-bigint", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-mds", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.1", + "serde", +] + [[package]] name = "p3-monty-31" version = "0.5.3" diff --git a/crates/jolt-field/Cargo.toml b/crates/jolt-field/Cargo.toml index cabbe5a875..024b8d934a 100644 --- a/crates/jolt-field/Cargo.toml +++ b/crates/jolt-field/Cargo.toml @@ -42,8 +42,17 @@ allocative = ["dep:allocative"] ark-std = { workspace = true } rand_chacha = { workspace = true } criterion = { workspace = true } +p3-field = "=0.5.3" +p3-mersenne-31 = "=0.5.3" +p3-baby-bear = "=0.5.3" +p3-koala-bear = "=0.5.3" [[bench]] name = "field_arith" harness = false required-features = ["bn254"] + +[[bench]] +name = "solinas_field_arith" +harness = false +required-features = ["bn254", "solinas", "parallel"] diff --git a/crates/jolt-field/benches/solinas_field_arith.rs b/crates/jolt-field/benches/solinas_field_arith.rs new file mode 100644 index 0000000000..d240dc8d1f --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith.rs @@ -0,0 +1,34 @@ +#![expect( + deprecated, + unused_results, + clippy::expect_used, + clippy::explicit_iter_loop, + clippy::map_unwrap_or, + clippy::semicolon_if_nothing_returned, + reason = "ported benchmark preserves the established measurement harness" +)] + +#[path = "solinas_field_arith/mod.rs"] +mod field_arith_suite; + +use criterion::{criterion_group, criterion_main}; +use field_arith_suite::{ + bench_base_field_matrix, bench_comparisons, bench_ext2_matrix, bench_ext4_matrix, + bench_kernel_patterns, bench_p3_base_matrix, bench_p3_ext4_matrix, bench_p3_ext5_matrix, + bench_parallel_throughput, bench_wide_ops, +}; + +criterion_group!( + field_arith, + bench_base_field_matrix, + bench_ext2_matrix, + bench_ext4_matrix, + bench_p3_base_matrix, + bench_p3_ext4_matrix, + bench_p3_ext5_matrix, + bench_wide_ops, + bench_kernel_patterns, + bench_comparisons, + bench_parallel_throughput +); +criterion_main!(field_arith); diff --git a/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs b/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs new file mode 100644 index 0000000000..b37fd5f4c6 --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs @@ -0,0 +1,696 @@ +use std::ops::{AddAssign, MulAssign, SubAssign}; +use std::time::Instant; + +use criterion::{black_box, Criterion, Throughput}; +use jolt_field::packed::PackedField; +use jolt_field::{FieldCore, Invertible, RandomSampling, RingCore}; +use rand::{rngs::StdRng, SeedableRng}; + +use super::data::duration_per_logical_op; +use super::params::ArithmeticBenchParams; + +pub(crate) fn bench_arithmetic_case( + c: &mut Criterion, + family: &str, + label: &str, + seed: u64, + params: ArithmeticBenchParams, +) where + F: FieldCore + + RandomSampling + + RingCore + + Invertible + + AddAssign + + SubAssign + + MulAssign + + 'static, + PF: PackedField + Copy + 'static, +{ + let mut rng = StdRng::seed_from_u64(seed); + let scalar_latency_inputs: Vec = (0..params.latency_iters) + .map(|_| F::random(&mut rng)) + .collect(); + let packed_latency_inputs: Vec = (0..params.latency_iters) + .map(|_| PF::from_fn(|_| F::random(&mut rng))) + .collect(); + let scalar_stream_lanes: Vec<(F, F)> = (0..params.streams) + .map(|_| (F::random(&mut rng), F::random(&mut rng))) + .collect(); + let packed_stream_lanes: Vec<(PF, PF)> = (0..params.streams) + .map(|_| { + ( + PF::from_fn(|_| F::random(&mut rng)), + PF::from_fn(|_| F::random(&mut rng)), + ) + }) + .collect(); + + let mut latency_group = c.benchmark_group(format!( + "field_arith/{family}/latency_chain/{label}_w{}", + PF::WIDTH + )); + + bench_scalar_latency::( + &mut latency_group, + "add", + params.latency_iters, + &scalar_latency_inputs, + |mut acc, x| { + acc += x; + acc + }, + F::zero(), + ); + bench_scalar_latency::( + &mut latency_group, + "sub", + params.latency_iters, + &scalar_latency_inputs, + |mut acc, x| { + acc -= x; + acc + }, + F::zero(), + ); + bench_scalar_unary_latency::( + &mut latency_group, + "neg", + params.latency_iters, + &scalar_latency_inputs, + |acc| -acc, + ); + bench_scalar_unary_latency::( + &mut latency_group, + "double", + params.latency_iters, + &scalar_latency_inputs, + |acc| acc + acc, + ); + bench_scalar_latency::( + &mut latency_group, + "add_neg", + params.latency_iters, + &scalar_latency_inputs, + |acc, x| -(acc + x), + F::zero(), + ); + bench_scalar_latency::( + &mut latency_group, + "double_add", + params.latency_iters, + &scalar_latency_inputs, + |acc, x| acc + acc + x, + F::zero(), + ); + bench_scalar_latency::( + &mut latency_group, + "mul", + params.latency_iters, + &scalar_latency_inputs, + |mut acc, x| { + acc *= x; + acc + }, + F::one(), + ); + bench_scalar_latency::( + &mut latency_group, + "mul_add", + params.latency_iters, + &scalar_latency_inputs, + |acc, x| acc * x + acc, + F::one(), + ); + + latency_group.throughput(Throughput::Elements(1)); + latency_group.bench_function( + format!("scalar_square_chain/{}_ns_per_op", params.latency_iters), + |b| { + b.iter_custom(|iters| { + let mut acc = black_box(scalar_latency_inputs[0]); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.latency_iters { + acc = acc.square(); + } + } + black_box(acc); + duration_per_logical_op(start.elapsed(), params.latency_iters as u64) + }) + }, + ); + + latency_group.throughput(Throughput::Elements(1)); + latency_group.bench_function( + format!("scalar_mul_self_chain/{}_ns_per_op", params.latency_iters), + |b| { + b.iter_custom(|iters| { + let mut acc = black_box(scalar_latency_inputs[0]); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.latency_iters { + acc = acc * acc; + } + } + black_box(acc); + duration_per_logical_op(start.elapsed(), params.latency_iters as u64) + }) + }, + ); + + latency_group.throughput(Throughput::Elements(1)); + latency_group.bench_function( + format!( + "scalar_inverse_chain/{}_ns_per_op", + params.inverse_latency_iters + ), + |b| { + b.iter_custom(|iters| { + let inputs = black_box(&scalar_latency_inputs[..params.inverse_latency_iters]); + let mut acc = F::one(); + let start = Instant::now(); + for _ in 0..iters { + for x in inputs { + acc = (acc + *x).inverse().unwrap_or_else(F::one); + } + } + black_box(acc); + duration_per_logical_op(start.elapsed(), params.inverse_latency_iters as u64) + }) + }, + ); + + bench_packed_latency::( + &mut latency_group, + "add", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc + x, + PF::broadcast(F::zero()), + ); + bench_packed_latency::( + &mut latency_group, + "sub", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc - x, + PF::broadcast(F::zero()), + ); + let packed_zero = PF::broadcast(F::zero()); + bench_packed_unary_latency::( + &mut latency_group, + "neg", + params.latency_iters, + &packed_latency_inputs, + |acc| packed_zero - acc, + ); + bench_packed_unary_latency::( + &mut latency_group, + "double", + params.latency_iters, + &packed_latency_inputs, + |acc| acc + acc, + ); + bench_packed_latency::( + &mut latency_group, + "add_neg", + params.latency_iters, + &packed_latency_inputs, + |acc, x| packed_zero - (acc + x), + packed_zero, + ); + bench_packed_latency::( + &mut latency_group, + "double_add", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc + acc + x, + PF::broadcast(F::zero()), + ); + bench_packed_latency::( + &mut latency_group, + "mul", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc * x, + PF::broadcast(F::one()), + ); + bench_packed_latency::( + &mut latency_group, + "mul_add", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc * x + acc, + PF::broadcast(F::one()), + ); + bench_packed_unary_latency::( + &mut latency_group, + "square", + params.latency_iters, + &packed_latency_inputs, + |acc| acc.square(), + ); + bench_packed_unary_latency::( + &mut latency_group, + "mul_self", + params.latency_iters, + &packed_latency_inputs, + |acc| acc * acc, + ); + latency_group.throughput(Throughput::Elements(1)); + latency_group.bench_function( + format!( + "packed_inverse_chain/{}x{}_ns_lane", + params.inverse_latency_iters, + PF::WIDTH + ), + |b| { + b.iter_custom(|iters| { + let inputs = black_box(&packed_latency_inputs[..params.inverse_latency_iters]); + let mut acc = PF::broadcast(F::one()); + let start = Instant::now(); + for _ in 0..iters { + for x in inputs { + acc = (acc + *x) + .inverse() + .unwrap_or_else(|| PF::broadcast(F::one())); + } + } + black_box(acc.extract(0)); + duration_per_logical_op( + start.elapsed(), + (params.inverse_latency_iters * PF::WIDTH) as u64, + ) + }) + }, + ); + + latency_group.finish(); + + let mut throughput_group = c.benchmark_group(format!( + "field_arith/{family}/throughput_stream/{label}_w{}", + PF::WIDTH + )); + + bench_scalar_throughput::( + &mut throughput_group, + "add", + params, + &scalar_stream_lanes, + |mut acc, x| { + acc += x; + acc + }, + |a, b| a + b, + ); + bench_scalar_throughput::( + &mut throughput_group, + "sub", + params, + &scalar_stream_lanes, + |mut acc, x| { + acc -= x; + acc + }, + |a, b| a - b, + ); + bench_scalar_throughput::( + &mut throughput_group, + "mul", + params, + &scalar_stream_lanes, + |mut acc, x| { + acc *= x; + acc + }, + |a, b| a * b, + ); + + throughput_group.throughput(Throughput::Elements(1)); + throughput_group.bench_function( + format!( + "scalar_square_stream/{}x{}_ns_per_op", + params.streams, params.throughput_iters + ), + |b| { + b.iter_custom(|iters| { + let lanes = black_box(&scalar_stream_lanes); + let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.throughput_iters { + for acc_i in acc.iter_mut() { + *acc_i = acc_i.square(); + } + } + } + black_box(acc[0]); + duration_per_logical_op( + start.elapsed(), + (params.streams * params.throughput_iters) as u64, + ) + }) + }, + ); + + throughput_group.throughput(Throughput::Elements(1)); + throughput_group.bench_function( + format!( + "scalar_inverse_stream/{}x{}_ns_per_op", + params.streams, params.inverse_throughput_iters + ), + |b| { + b.iter_custom(|iters| { + let lanes = black_box(&scalar_stream_lanes); + let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.inverse_throughput_iters { + for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { + *acc_i = (*acc_i + lane.0).inverse().unwrap_or_else(F::one); + } + } + } + black_box(acc[0]); + duration_per_logical_op( + start.elapsed(), + (params.streams * params.inverse_throughput_iters) as u64, + ) + }) + }, + ); + + bench_packed_throughput::( + &mut throughput_group, + "add", + params, + &packed_stream_lanes, + |acc, x| acc + x, + |a, b| a + b, + ); + bench_packed_throughput::( + &mut throughput_group, + "sub", + params, + &packed_stream_lanes, + |acc, x| acc - x, + |a, b| a - b, + ); + bench_packed_throughput::( + &mut throughput_group, + "mul", + params, + &packed_stream_lanes, + |acc, x| acc * x, + |a, b| a * b, + ); + + throughput_group.throughput(Throughput::Elements(1)); + throughput_group.bench_function( + format!( + "packed_square_stream/{}x{}x{}_ns_lane", + params.streams, + PF::WIDTH, + params.throughput_iters + ), + |b| { + b.iter_custom(|iters| { + let lanes = black_box(&packed_stream_lanes); + let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.throughput_iters { + for acc_i in acc.iter_mut() { + *acc_i = acc_i.square(); + } + } + } + black_box(acc[0].extract(0)); + duration_per_logical_op( + start.elapsed(), + (params.streams * PF::WIDTH * params.throughput_iters) as u64, + ) + }) + }, + ); + + throughput_group.throughput(Throughput::Elements(1)); + throughput_group.bench_function( + format!( + "packed_mul_self_stream/{}x{}x{}_ns_lane", + params.streams, + PF::WIDTH, + params.throughput_iters + ), + |b| { + b.iter_custom(|iters| { + let lanes = black_box(&packed_stream_lanes); + let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.throughput_iters { + for acc_i in acc.iter_mut() { + let x = *acc_i; + *acc_i = x * x; + } + } + } + black_box(acc[0].extract(0)); + duration_per_logical_op( + start.elapsed(), + (params.streams * PF::WIDTH * params.throughput_iters) as u64, + ) + }) + }, + ); + + throughput_group.throughput(Throughput::Elements(1)); + throughput_group.bench_function( + format!( + "packed_inverse_stream/{}x{}x{}_ns_lane", + params.streams, + PF::WIDTH, + params.inverse_throughput_iters + ), + |b| { + b.iter_custom(|iters| { + let lanes = black_box(&packed_stream_lanes); + let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.inverse_throughput_iters { + for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { + *acc_i = (*acc_i + lane.0) + .inverse() + .unwrap_or_else(|| PF::broadcast(F::one())); + } + } + } + black_box(acc[0].extract(0)); + duration_per_logical_op( + start.elapsed(), + (params.streams * PF::WIDTH * params.inverse_throughput_iters) as u64, + ) + }) + }, + ); + + throughput_group.finish(); +} + +fn bench_scalar_latency( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + op: &str, + latency_iters: usize, + inputs: &[F], + step: impl Fn(F, F) -> F, + init: F, +) where + F: FieldCore, +{ + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!("scalar_{op}_chain/{latency_iters}_ns_per_op"), + |b| { + b.iter_custom(|iters| { + let inputs = black_box(inputs); + let mut acc = init; + let start = Instant::now(); + for _ in 0..iters { + for x in inputs { + acc = step(acc, *x); + } + } + black_box(acc); + duration_per_logical_op(start.elapsed(), latency_iters as u64) + }) + }, + ); +} + +fn bench_scalar_unary_latency( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + op: &str, + latency_iters: usize, + inputs: &[F], + step: impl Fn(F) -> F, +) where + F: FieldCore, +{ + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!("scalar_{op}_chain/{latency_iters}_ns_per_op"), + |b| { + b.iter_custom(|iters| { + let mut acc = black_box(inputs[0]); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..latency_iters { + acc = step(acc); + } + } + black_box(acc); + duration_per_logical_op(start.elapsed(), latency_iters as u64) + }) + }, + ); +} + +fn bench_packed_latency( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + op: &str, + latency_iters: usize, + inputs: &[PF], + step: impl Fn(PF, PF) -> PF, + init: PF, +) where + F: FieldCore, + PF: PackedField + Copy, +{ + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!("packed_{op}_chain/{latency_iters}x{}_ns_lane", PF::WIDTH), + |b| { + b.iter_custom(|iters| { + let inputs = black_box(inputs); + let mut acc = init; + let start = Instant::now(); + for _ in 0..iters { + for x in inputs { + acc = step(acc, *x); + } + } + black_box(acc.extract(0)); + duration_per_logical_op(start.elapsed(), (latency_iters * PF::WIDTH) as u64) + }) + }, + ); +} + +fn bench_packed_unary_latency( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + op: &str, + latency_iters: usize, + inputs: &[PF], + step: impl Fn(PF) -> PF, +) where + F: FieldCore, + PF: PackedField + Copy, +{ + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!("packed_{op}_chain/{latency_iters}x{}_ns_lane", PF::WIDTH), + |b| { + b.iter_custom(|iters| { + let mut acc = black_box(inputs[0]); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..latency_iters { + acc = step(acc); + } + } + black_box(acc.extract(0)); + duration_per_logical_op(start.elapsed(), (latency_iters * PF::WIDTH) as u64) + }) + }, + ); +} + +fn bench_scalar_throughput( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + op: &str, + params: ArithmeticBenchParams, + lanes: &[(F, F)], + step: impl Fn(F, F) -> F, + init: impl Fn(F, F) -> F, +) where + F: FieldCore, +{ + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!( + "scalar_{op}_stream/{}x{}_ns_per_op", + params.streams, params.throughput_iters + ), + |b| { + b.iter_custom(|iters| { + let lanes = black_box(lanes); + let mut acc: Vec = lanes.iter().map(|(a, b)| init(*a, *b)).collect(); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.throughput_iters { + for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { + *acc_i = step(*acc_i, lane.0); + } + } + } + black_box(acc[0]); + duration_per_logical_op( + start.elapsed(), + (params.streams * params.throughput_iters) as u64, + ) + }) + }, + ); +} + +fn bench_packed_throughput( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + op: &str, + params: ArithmeticBenchParams, + lanes: &[(PF, PF)], + step: impl Fn(PF, PF) -> PF, + init: impl Fn(PF, PF) -> PF, +) where + F: FieldCore, + PF: PackedField + Copy, +{ + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!( + "packed_{op}_stream/{}x{}x{}_ns_lane", + params.streams, + PF::WIDTH, + params.throughput_iters + ), + |b| { + b.iter_custom(|iters| { + let lanes = black_box(lanes); + let mut acc: Vec = lanes.iter().map(|(a, b)| init(*a, *b)).collect(); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.throughput_iters { + for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { + *acc_i = step(*acc_i, lane.0); + } + } + } + black_box(acc[0].extract(0)); + duration_per_logical_op( + start.elapsed(), + (params.streams * PF::WIDTH * params.throughput_iters) as u64, + ) + }) + }, + ); +} diff --git a/crates/jolt-field/benches/solinas_field_arith/base.rs b/crates/jolt-field/benches/solinas_field_arith/base.rs new file mode 100644 index 0000000000..67858732d8 --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/base.rs @@ -0,0 +1,64 @@ +use criterion::Criterion; +use jolt_field::{ + Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime48Offset59, Prime56Offset27, + Prime64Offset59, +}; + +use super::arithmetic::bench_arithmetic_case; +use super::cases::*; +use super::params::ArithmeticBenchParams; + +pub(crate) fn bench_base_field_matrix(c: &mut Criterion) { + let params = ArithmeticBenchParams::from_env("AKITA_BENCH_BASE_ARITH", 2048, 256); + + bench_arithmetic_case::( + c, + "base", + PRIME31_OFFSET19, + 0xba5e_0031, + params, + ); + bench_arithmetic_case::( + c, + "base", + MERSENNE31, + 0xba5e_3131, + params, + ); + bench_arithmetic_case::( + c, + "base", + PRIME32_OFFSET99, + 0xba5e_0032, + params, + ); + bench_arithmetic_case::( + c, + "base", + PRIME40_OFFSET195, + 0xba5e_0040, + params, + ); + bench_arithmetic_case::( + c, + "base", + PRIME48_OFFSET59, + 0xba5e_0048, + params, + ); + bench_arithmetic_case::( + c, + "base", + PRIME56_OFFSET27, + 0xba5e_0056, + params, + ); + bench_arithmetic_case::( + c, + "base", + PRIME64_OFFSET59, + 0xba5e_0064, + params, + ); + bench_arithmetic_case::(c, "base", PRIME128_OFFSET275, 0xba5e_0128, params); +} diff --git a/crates/jolt-field/benches/solinas_field_arith/cases.rs b/crates/jolt-field/benches/solinas_field_arith/cases.rs new file mode 100644 index 0000000000..aafb95bf93 --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/cases.rs @@ -0,0 +1,25 @@ +use jolt_field::packed::{Fp32Packing, HasPacking}; +use jolt_field::{ + Fp32, Prime128Offset275, Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime48Offset59, + Prime56Offset27, Prime64Offset59, +}; + +pub(crate) type Mersenne31 = Fp32<{ (1u32 << 31) - 1 }>; +pub(crate) type PackedMersenne31 = Fp32Packing<{ (1u32 << 31) - 1 }>; +pub(crate) type P31O19 = ::Packing; +pub(crate) type P32O99 = ::Packing; +pub(crate) type P40O195 = ::Packing; +pub(crate) type P48O59 = ::Packing; +pub(crate) type P56O27 = ::Packing; +pub(crate) type P64O59 = ::Packing; +pub(crate) type P128O275 = ::Packing; +pub(crate) type F128 = Prime128Offset275; + +pub(crate) const PRIME31_OFFSET19: &str = "prime31_offset19"; +pub(crate) const MERSENNE31: &str = "mersenne31"; +pub(crate) const PRIME32_OFFSET99: &str = "prime32_offset99"; +pub(crate) const PRIME40_OFFSET195: &str = "prime40_offset195"; +pub(crate) const PRIME48_OFFSET59: &str = "prime48_offset59"; +pub(crate) const PRIME56_OFFSET27: &str = "prime56_offset27"; +pub(crate) const PRIME64_OFFSET59: &str = "prime64_offset59"; +pub(crate) const PRIME128_OFFSET275: &str = "prime128_offset275"; diff --git a/crates/jolt-field/benches/solinas_field_arith/comparison.rs b/crates/jolt-field/benches/solinas_field_arith/comparison.rs new file mode 100644 index 0000000000..7551726504 --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/comparison.rs @@ -0,0 +1,63 @@ +use ark_bn254::Fr as BN254Fr; +use ark_ff::{AdditiveGroup, Field, UniformRand}; +use criterion::{black_box, Criterion}; +use rand::{rngs::StdRng, SeedableRng}; + +pub(crate) fn bench_comparisons(c: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(0x5eed); + let inputs: Vec = (0..2048).map(|_| BN254Fr::rand(&mut rng)).collect(); + + let mut group = c.benchmark_group("field_arith/comparison/bn254"); + + group.bench_function("mul_add_chain_2048", |b| { + b.iter(|| { + let mut acc = BN254Fr::ONE; + for x in inputs.iter() { + acc = acc * x + acc; + } + black_box(acc) + }) + }); + + group.bench_function("mul_chain_2048", |b| { + b.iter(|| { + let mut acc = BN254Fr::ONE; + for x in inputs.iter() { + acc *= x; + } + black_box(acc) + }) + }); + + group.bench_function("mul_parallel_1024", |b| { + b.iter(|| { + let mut sum = BN254Fr::ZERO; + for pair in inputs.chunks_exact(2) { + sum += pair[0] * pair[1]; + } + black_box(sum) + }) + }); + + group.bench_function("sqr_chain_2048", |b| { + b.iter(|| { + let mut acc = inputs[0]; + for _ in 0..2048 { + acc.square_in_place(); + } + black_box(acc) + }) + }); + + group.bench_function("inv_256", |b| { + b.iter(|| { + let mut acc = BN254Fr::ONE; + for x in inputs[..256].iter() { + acc *= x.inverse().unwrap_or(BN254Fr::ZERO); + } + black_box(acc) + }) + }); + + group.finish(); +} diff --git a/crates/jolt-field/benches/solinas_field_arith/data.rs b/crates/jolt-field/benches/solinas_field_arith/data.rs new file mode 100644 index 0000000000..bede964e4f --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/data.rs @@ -0,0 +1,17 @@ +use std::time::Duration; + +use rand::RngCore; + +pub(crate) fn rand_u128(rng: &mut R) -> u128 { + let lo = rng.next_u64() as u128; + let hi = rng.next_u64() as u128; + lo | (hi << 64) +} + +/// Per-logical-op time returned from `iter_custom`. +/// +/// Criterion divides the returned duration by its batch `iters` again; only count logical ops +/// inside one batch (e.g. `latency_iters` or `latency_iters * WIDTH`). +pub(crate) fn duration_per_logical_op(elapsed: Duration, logical_ops_per_batch: u64) -> Duration { + Duration::from_secs_f64(elapsed.as_secs_f64() / logical_ops_per_batch.max(1) as f64) +} diff --git a/crates/jolt-field/benches/solinas_field_arith/ext2.rs b/crates/jolt-field/benches/solinas_field_arith/ext2.rs new file mode 100644 index 0000000000..a469712c5d --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/ext2.rs @@ -0,0 +1,47 @@ +use criterion::Criterion; +use jolt_field::packed::{HasPacking, PackedFpExt2}; +use jolt_field::{FpExt2, Prime31Offset19, Prime32Offset99, Prime64Offset59, TwoNr}; + +use super::arithmetic::bench_arithmetic_case; +use super::params::ArithmeticBenchParams; + +pub(crate) fn bench_ext2_matrix(c: &mut Criterion) { + type F31 = Prime31Offset19; + type PF31 = ::Packing; + type F31FpExt2 = FpExt2; + type PF31FpExt2 = PackedFpExt2; + + type F32 = Prime32Offset99; + type PF32 = ::Packing; + type F32FpExt2 = FpExt2; + type PF32FpExt2 = PackedFpExt2; + + type F64 = Prime64Offset59; + type PF64 = ::Packing; + type F64FpExt2 = FpExt2; + type PF64FpExt2 = PackedFpExt2; + + let params = ArithmeticBenchParams::from_env("AKITA_BENCH_EXT2_ARITH", 512, 128); + + bench_arithmetic_case::( + c, + "ext2", + "prime31_offset19_fp_ext2", + 0xe200_0031, + params, + ); + bench_arithmetic_case::( + c, + "ext2", + "prime32_offset99_fp_ext2", + 0xe200_0032, + params, + ); + bench_arithmetic_case::( + c, + "ext2", + "prime64_offset59_fp_ext2", + 0xe200_0064, + params, + ); +} diff --git a/crates/jolt-field/benches/solinas_field_arith/ext4.rs b/crates/jolt-field/benches/solinas_field_arith/ext4.rs new file mode 100644 index 0000000000..e1d5dffd6e --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/ext4.rs @@ -0,0 +1,53 @@ +//! Degree-4 extension microbenches. +//! +//! Criterion directory names are capped at 64 characters (`MAX_DIRECTORY_NAME_LEN`). +//! Use the short `label` strings below (≤ 12 chars before `_w{width}`) so groups are not +//! truncated. + +use criterion::Criterion; +use jolt_field::packed::HasPacking; +use jolt_field::{FpExt4, Prime31Offset19, Prime32Offset99}; + +use super::arithmetic::bench_arithmetic_case; +use super::cases::Mersenne31; +use super::params::ArithmeticBenchParams; + +pub(crate) fn bench_ext4_matrix(c: &mut Criterion) { + type F31Mersenne = Mersenne31; + type F31MersenneFpExt4 = FpExt4; + type PF31MersenneFpExt4 = ::Packing; + + type F31 = Prime31Offset19; + type F31FpExt4 = FpExt4; + type PF31FpExt4 = ::Packing; + + type F32 = Prime32Offset99; + type F32FpExt4 = FpExt4; + type PF32FpExt4 = ::Packing; + + let params = ArithmeticBenchParams::from_env("AKITA_BENCH_EXT4_ARITH", 512, 128); + + bench_arithmetic_case::( + c, + "ext4", + "m31_fp_ext4", + 0xe400_3031_00a1, + params, + ); + + bench_arithmetic_case::( + c, + "ext4", + "p31o19_fp_ext4", + 0xe400_3031, + params, + ); + + bench_arithmetic_case::( + c, + "ext4", + "p32o99_fp_ext4", + 0xe400_3032, + params, + ); +} diff --git a/crates/jolt-field/benches/solinas_field_arith/kernel.rs b/crates/jolt-field/benches/solinas_field_arith/kernel.rs new file mode 100644 index 0000000000..9fe22695c0 --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/kernel.rs @@ -0,0 +1,147 @@ +use criterion::{black_box, Criterion, Throughput}; +use jolt_field::packed::PackedField; +use jolt_field::{CanonicalField, FieldCore, Prime128Offset275, RandomSampling}; +use rand::{rngs::StdRng, RngCore, SeedableRng}; + +use super::cases::*; +use super::data::rand_u128; + +pub(crate) fn bench_kernel_patterns(c: &mut Criterion) { + bench_packed_sumcheck_mix(c); + bench_fp128_accumulator_pattern(c); +} + +fn bench_packed_sumcheck_mix(c: &mut Criterion) { + let n = 4096u64; + let mut rng = StdRng::seed_from_u64(0x5151_cafe); + + let mut group = c.benchmark_group("field_arith/kernel/packed_macc"); + group.throughput(Throughput::Elements(n)); + + use jolt_field::{Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime64Offset59}; + + sumcheck_bench::(&mut group, PRIME31_OFFSET19, &mut rng, n); + sumcheck_bench::(&mut group, MERSENNE31, &mut rng, n); + sumcheck_bench::(&mut group, PRIME32_OFFSET99, &mut rng, n); + sumcheck_bench::(&mut group, PRIME40_OFFSET195, &mut rng, n); + sumcheck_bench::(&mut group, PRIME64_OFFSET59, &mut rng, n); + sumcheck_bench::(&mut group, PRIME128_OFFSET275, &mut rng, n); + + group.finish(); +} + +fn sumcheck_bench( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + label: &str, + rng: &mut StdRng, + n: u64, +) where + F: FieldCore + RandomSampling + 'static, + PF: PackedField + Copy + 'static, +{ + let eq: Vec = (0..n).map(|_| F::random(rng)).collect(); + let poly: Vec = (0..n).map(|_| F::random(rng)).collect(); + let eq_p = PF::pack_slice(&eq); + let poly_p = PF::pack_slice(&poly); + + group.bench_function(format!("{label}_packed_macc"), |b| { + b.iter(|| { + let e = black_box(&eq_p); + let p_v = black_box(&poly_p); + let mut acc = PF::broadcast(F::zero()); + for i in 0..e.len() { + acc = acc + e[i] * p_v[i]; + } + black_box(acc) + }) + }); +} + +fn bench_fp128_accumulator_pattern(c: &mut Criterion) { + type F = Prime128Offset275; + + let mut rng = StdRng::seed_from_u64(0xacc0_1a70_0002); + let inputs_a: Vec = (0..256) + .map(|_| F::from_canonical_u128_reduced(rand_u128(&mut rng))) + .collect(); + let inputs_b_u64: Vec = (0..256).map(|_| rng.next_u64()).collect(); + let inputs_b_f: Vec = (0..256) + .map(|_| F::from_canonical_u128_reduced(rand_u128(&mut rng))) + .collect(); + + let mut group = c.benchmark_group("field_arith/kernel/fp128_accumulator"); + + for &n in &[16, 64, 256] { + group.bench_function(format!("eager_mul_u64_{n}"), |bench| { + bench.iter(|| { + let a_s = black_box(&inputs_a[..n]); + let b_s = black_box(&inputs_b_u64[..n]); + let mut acc = F::zero(); + for i in 0..n { + acc += a_s[i] * F::from_u64(b_s[i]); + } + black_box(acc) + }) + }); + + group.bench_function(format!("widening_accum_u64_{n}"), |bench| { + bench.iter(|| { + let a_s = black_box(&inputs_a[..n]); + let b_s = black_box(&inputs_b_u64[..n]); + let mut acc = [0u64; 5]; + for i in 0..n { + let wide = a_s[i].mul_wide_u64(b_s[i]); + let mut carry: u64 = 0; + for j in 0..3 { + let sum = acc[j] as u128 + wide[j] as u128 + carry as u128; + acc[j] = sum as u64; + carry = (sum >> 64) as u64; + } + for item in &mut acc[3..5] { + let sum = *item as u128 + carry as u128; + *item = sum as u64; + carry = (sum >> 64) as u64; + } + } + black_box(F::solinas_reduce(&acc)) + }) + }); + + group.bench_function(format!("eager_mul_full_{n}"), |bench| { + bench.iter(|| { + let a_s = black_box(&inputs_a[..n]); + let b_s = black_box(&inputs_b_f[..n]); + let mut acc = F::zero(); + for i in 0..n { + acc += a_s[i] * b_s[i]; + } + black_box(acc) + }) + }); + + group.bench_function(format!("widening_accum_full_{n}"), |bench| { + bench.iter(|| { + let a_s = black_box(&inputs_a[..n]); + let b_s = black_box(&inputs_b_f[..n]); + let mut acc = [0u64; 6]; + for i in 0..n { + let wide = a_s[i].mul_wide(b_s[i]); + let mut carry: u64 = 0; + for j in 0..4 { + let sum = acc[j] as u128 + wide[j] as u128 + carry as u128; + acc[j] = sum as u64; + carry = (sum >> 64) as u64; + } + for item in &mut acc[4..6] { + let sum = *item as u128 + carry as u128; + *item = sum as u64; + carry = (sum >> 64) as u64; + } + } + black_box(F::solinas_reduce(&acc)) + }) + }); + } + + group.finish(); +} diff --git a/crates/jolt-field/benches/solinas_field_arith/mod.rs b/crates/jolt-field/benches/solinas_field_arith/mod.rs new file mode 100644 index 0000000000..0a36950d56 --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/mod.rs @@ -0,0 +1,21 @@ +pub(crate) mod arithmetic; +pub(crate) mod base; +pub(crate) mod cases; +pub(crate) mod comparison; +pub(crate) mod data; +pub(crate) mod ext2; +pub(crate) mod ext4; +pub(crate) mod kernel; +pub(crate) mod parallel; +pub(crate) mod params; +pub(crate) mod plonky3; +pub(crate) mod wide; + +pub(crate) use base::bench_base_field_matrix; +pub(crate) use comparison::bench_comparisons; +pub(crate) use ext2::bench_ext2_matrix; +pub(crate) use ext4::bench_ext4_matrix; +pub(crate) use kernel::bench_kernel_patterns; +pub(crate) use parallel::bench_parallel_throughput; +pub(crate) use plonky3::{bench_p3_base_matrix, bench_p3_ext4_matrix, bench_p3_ext5_matrix}; +pub(crate) use wide::bench_wide_ops; diff --git a/crates/jolt-field/benches/solinas_field_arith/parallel.rs b/crates/jolt-field/benches/solinas_field_arith/parallel.rs new file mode 100644 index 0000000000..525f2541fa --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/parallel.rs @@ -0,0 +1,235 @@ +#[cfg(feature = "parallel")] +use std::env; +#[cfg(feature = "parallel")] +use std::thread; + +#[cfg(feature = "parallel")] +use criterion::{black_box, Criterion, Throughput}; +#[cfg(feature = "parallel")] +use jolt_field::packed::{PackedField, PackedValue}; +#[cfg(feature = "parallel")] +use jolt_field::{ + CanonicalField, Prime128Offset275, Prime31Offset19, Prime64Offset59, RandomSampling, +}; +#[cfg(feature = "parallel")] +use rand::{rngs::StdRng, SeedableRng}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +#[cfg(feature = "parallel")] +use rayon::ThreadPoolBuilder; + +#[cfg(feature = "parallel")] +use super::cases::*; +#[cfg(feature = "parallel")] +use super::data::rand_u128; +#[cfg(feature = "parallel")] +use super::params::env_usize; + +#[cfg(feature = "parallel")] +pub(crate) fn bench_parallel_throughput(c: &mut Criterion) { + let profile = env::var("AKITA_BENCH_PAR_PROFILE").unwrap_or_else(|_| "dev".to_string()); + let default_n = match profile.as_str() { + "scale" | "large" => 1 << 20, + "xlarge" => 1 << 22, + _ => 1 << 15, + }; + let n = env_usize("AKITA_BENCH_PAR_N", default_n); + let default_chunk = match profile.as_str() { + "scale" | "large" => 1 << 14, + "xlarge" => 1 << 15, + _ => 1 << 12, + }; + let chunk = env_usize("AKITA_BENCH_PAR_CHUNK", default_chunk); + let threads = env_usize( + "AKITA_BENCH_PAR_THREADS", + thread::available_parallelism() + .map(|v| v.get()) + .unwrap_or(1), + ); + + assert!(threads > 0, "AKITA_BENCH_PAR_THREADS must be > 0"); + assert!(n > 0, "AKITA_BENCH_PAR_N must be > 0"); + assert!(chunk > 0, "AKITA_BENCH_PAR_CHUNK must be > 0"); + + let pool = ThreadPoolBuilder::new() + .num_threads(threads) + .build() + .expect("build benchmark rayon pool"); + + let mut rng = StdRng::seed_from_u64(0x7061_7261_0001); + let lhs31: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let rhs31: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let lhs64: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let rhs64: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let lhs128: Vec = (0..n) + .map(|_| Prime128Offset275::from_canonical_u128_reduced(rand_u128(&mut rng))) + .collect(); + let rhs128: Vec = (0..n) + .map(|_| Prime128Offset275::from_canonical_u128_reduced(rand_u128(&mut rng))) + .collect(); + + let lhs31_p = P31O19::pack_slice(&lhs31); + let rhs31_p = P31O19::pack_slice(&rhs31); + let lhs64_p = P64O59::pack_slice(&lhs64); + let rhs64_p = P64O59::pack_slice(&rhs64); + let lhs128_p = P128O275::pack_slice(&lhs128); + let rhs128_p = P128O275::pack_slice(&rhs128); + + let mut out31 = vec![Prime31Offset19::zero(); n]; + let mut out64 = vec![Prime64Offset59::zero(); n]; + let mut out128 = vec![F128::zero(); n]; + let mut out31_p = vec![P31O19::broadcast(Prime31Offset19::zero()); lhs31_p.len()]; + let mut out64_p = vec![P64O59::broadcast(Prime64Offset59::zero()); lhs64_p.len()]; + let mut out128_p = vec![P128O275::broadcast(F128::zero()); lhs128_p.len()]; + + let mut group = c.benchmark_group(format!( + "field_arith/parallel/{profile}/n{n}/chunk{chunk}/threads{threads}" + )); + group.throughput(Throughput::Elements(n as u64)); + + bench_scalar_parallel( + &mut group, + &pool, + PRIME31_OFFSET19, + &lhs31, + &rhs31, + &mut out31, + chunk, + ); + bench_scalar_parallel( + &mut group, + &pool, + PRIME64_OFFSET59, + &lhs64, + &rhs64, + &mut out64, + chunk, + ); + bench_scalar_parallel( + &mut group, + &pool, + PRIME128_OFFSET275, + &lhs128, + &rhs128, + &mut out128, + chunk, + ); + bench_packed_parallel( + &mut group, + &pool, + PRIME31_OFFSET19, + &lhs31_p, + &rhs31_p, + &mut out31_p, + (chunk / P31O19::WIDTH).max(1), + ); + bench_packed_parallel( + &mut group, + &pool, + PRIME64_OFFSET59, + &lhs64_p, + &rhs64_p, + &mut out64_p, + (chunk / P64O59::WIDTH).max(1), + ); + bench_packed_parallel( + &mut group, + &pool, + PRIME128_OFFSET275, + &lhs128_p, + &rhs128_p, + &mut out128_p, + (chunk / P128O275::WIDTH).max(1), + ); + + group.finish(); +} + +#[cfg(feature = "parallel")] +fn bench_scalar_parallel( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + pool: &rayon::ThreadPool, + label: &str, + lhs: &[F], + rhs: &[F], + out: &mut [F], + chunk: usize, +) where + F: jolt_field::FieldCore + Send + Sync, +{ + group.bench_function(format!("{label}_mul_seq"), |b| { + b.iter(|| { + let a = black_box(lhs); + let b_v = black_box(rhs); + for i in 0..out.len() { + out[i] = a[i] * b_v[i]; + } + black_box(out[0]) + }) + }); + + group.bench_function(format!("{label}_mul_par_chunked"), |b| { + b.iter(|| { + let a = black_box(lhs); + let b_v = black_box(rhs); + pool.install(|| { + out.par_chunks_mut(chunk) + .enumerate() + .for_each(|(chunk_idx, out_chunk)| { + let start = chunk_idx * chunk; + for (j, dst) in out_chunk.iter_mut().enumerate() { + let idx = start + j; + *dst = a[idx] * b_v[idx]; + } + }); + }); + black_box(out[0]) + }) + }); +} + +#[cfg(feature = "parallel")] +fn bench_packed_parallel( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + pool: &rayon::ThreadPool, + label: &str, + lhs: &[PF], + rhs: &[PF], + out: &mut [PF], + chunk: usize, +) where + PF: PackedField + Copy + Send + Sync, +{ + group.bench_function(format!("{label}_packed_mul_seq"), |b| { + b.iter(|| { + let a = black_box(lhs); + let b_v = black_box(rhs); + for i in 0..out.len() { + out[i] = a[i] * b_v[i]; + } + black_box(out[0].extract(0)) + }) + }); + + group.bench_function(format!("{label}_packed_mul_par_chunked"), |b| { + b.iter(|| { + let a = black_box(lhs); + let b_v = black_box(rhs); + pool.install(|| { + out.par_chunks_mut(chunk) + .enumerate() + .for_each(|(chunk_idx, out_chunk)| { + let start = chunk_idx * chunk; + for (j, dst) in out_chunk.iter_mut().enumerate() { + let idx = start + j; + *dst = a[idx] * b_v[idx]; + } + }); + }); + black_box(out[0].extract(0)) + }) + }); +} + +#[cfg(not(feature = "parallel"))] +pub(crate) fn bench_parallel_throughput(_: &mut criterion::Criterion) {} diff --git a/crates/jolt-field/benches/solinas_field_arith/params.rs b/crates/jolt-field/benches/solinas_field_arith/params.rs new file mode 100644 index 0000000000..9b73f733c1 --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/params.rs @@ -0,0 +1,55 @@ +use std::env; + +#[derive(Clone, Copy)] +pub(crate) struct ArithmeticBenchParams { + pub(crate) latency_iters: usize, + pub(crate) inverse_latency_iters: usize, + pub(crate) throughput_iters: usize, + pub(crate) inverse_throughput_iters: usize, + pub(crate) streams: usize, +} + +impl ArithmeticBenchParams { + pub(crate) fn from_env( + prefix: &str, + latency_default: usize, + throughput_default: usize, + ) -> Self { + let latency_iters = env_usize(&format!("{prefix}_LATENCY_ITERS"), latency_default); + let inverse_latency_iters = + env_usize(&format!("{prefix}_INVERSE_LATENCY_ITERS"), 128).min(latency_iters); + let throughput_iters = env_usize(&format!("{prefix}_THROUGHPUT_ITERS"), throughput_default); + let inverse_throughput_iters = env_usize(&format!("{prefix}_INVERSE_THROUGHPUT_ITERS"), 32); + let streams = env_usize(&format!("{prefix}_STREAMS"), 8); + + assert!(latency_iters > 0, "{prefix}_LATENCY_ITERS must be > 0"); + assert!( + inverse_latency_iters > 0, + "{prefix}_INVERSE_LATENCY_ITERS must be > 0" + ); + assert!( + throughput_iters > 0, + "{prefix}_THROUGHPUT_ITERS must be > 0" + ); + assert!( + inverse_throughput_iters > 0, + "{prefix}_INVERSE_THROUGHPUT_ITERS must be > 0" + ); + assert!(streams > 0, "{prefix}_STREAMS must be > 0"); + + Self { + latency_iters, + inverse_latency_iters, + throughput_iters, + inverse_throughput_iters, + streams, + } + } +} + +pub(crate) fn env_usize(name: &str, default: usize) -> usize { + env::var(name) + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(default) +} diff --git a/crates/jolt-field/benches/solinas_field_arith/plonky3.rs b/crates/jolt-field/benches/solinas_field_arith/plonky3.rs new file mode 100644 index 0000000000..73ada8a18b --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/plonky3.rs @@ -0,0 +1,957 @@ +use std::time::Instant; + +use criterion::{black_box, Criterion, Throughput}; +use p3_baby_bear::BabyBear; +use p3_field::extension::{BinomialExtensionField, QuinticTrinomialExtensionField}; +use p3_field::{ + BasedVectorSpace, ExtensionField, Field, PackedField, PackedFieldExtension, PackedValue, + PrimeCharacteristicRing, +}; +use p3_koala_bear::KoalaBear; +use p3_mersenne_31::Mersenne31; +use rand::{rngs::StdRng, RngCore, SeedableRng}; + +use super::data::duration_per_logical_op; +use super::params::ArithmeticBenchParams; + +fn sample_base(rng: &mut StdRng) -> F { + F::from_u64(rng.next_u64()) +} + +fn sample_ext + BasedVectorSpace>( + rng: &mut StdRng, +) -> EF { + EF::from_basis_coefficients_fn(|_| sample_base::(rng)) +} + +pub(crate) fn bench_p3_base_case( + c: &mut Criterion, + family: &str, + label: &str, + seed: u64, + params: ArithmeticBenchParams, +) where + F: Field + Copy, + F::Packing: PackedField + Copy, +{ + let mut rng = StdRng::seed_from_u64(seed); + let scalar_latency_inputs: Vec = (0..params.latency_iters) + .map(|_| sample_base(&mut rng)) + .collect(); + let packed_latency_inputs: Vec = (0..params.latency_iters) + .map(|_| F::Packing::from_fn(|_| sample_base(&mut rng))) + .collect(); + let scalar_stream_lanes: Vec<(F, F)> = (0..params.streams) + .map(|_| (sample_base(&mut rng), sample_base(&mut rng))) + .collect(); + let packed_stream_lanes: Vec<(F::Packing, F::Packing)> = (0..params.streams) + .map(|_| { + ( + F::Packing::from_fn(|_| sample_base(&mut rng)), + F::Packing::from_fn(|_| sample_base(&mut rng)), + ) + }) + .collect(); + + let width = ::WIDTH; + + let mut latency_group = c.benchmark_group(format!( + "field_arith/{family}/latency_chain/{label}_w{width}" + )); + + p3_bench_scalar_suite_latency(&mut latency_group, params, &scalar_latency_inputs); + + let packed_zero = F::Packing::broadcast(F::ZERO); + let packed_one = F::Packing::broadcast(F::ONE); + + p3_bench_packed_latency( + &mut latency_group, + width, + "add", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc + x, + packed_zero, + ); + p3_bench_packed_latency( + &mut latency_group, + width, + "sub", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc - x, + packed_zero, + ); + p3_bench_packed_unary_latency( + &mut latency_group, + width, + "neg", + params.latency_iters, + &packed_latency_inputs, + |acc| packed_zero - acc, + ); + p3_bench_packed_unary_latency( + &mut latency_group, + width, + "double", + params.latency_iters, + &packed_latency_inputs, + |acc| acc + acc, + ); + p3_bench_packed_latency( + &mut latency_group, + width, + "add_neg", + params.latency_iters, + &packed_latency_inputs, + |acc, x| packed_zero - (acc + x), + packed_zero, + ); + p3_bench_packed_latency( + &mut latency_group, + width, + "double_add", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc + acc + x, + packed_zero, + ); + p3_bench_packed_latency( + &mut latency_group, + width, + "mul", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc * x, + packed_one, + ); + p3_bench_packed_latency( + &mut latency_group, + width, + "mul_add", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc * x + acc, + packed_one, + ); + p3_bench_packed_unary_latency( + &mut latency_group, + width, + "square", + params.latency_iters, + &packed_latency_inputs, + |acc| acc.square(), + ); + p3_bench_packed_unary_latency( + &mut latency_group, + width, + "mul_self", + params.latency_iters, + &packed_latency_inputs, + |acc| acc * acc, + ); + + latency_group.throughput(Throughput::Elements(1)); + latency_group.bench_function( + format!( + "packed_inverse_chain/{}x{width}_ns_lane", + params.inverse_latency_iters + ), + |b| { + b.iter_custom(|iters| { + let inputs = black_box(&packed_latency_inputs[..params.inverse_latency_iters]); + let mut acc = packed_one; + let start = Instant::now(); + for _ in 0..iters { + for x in inputs { + acc = F::Packing::from_fn(|lane| { + (PackedValue::extract(&acc, lane) + PackedValue::extract(x, lane)) + .inverse() + }); + } + } + black_box(PackedValue::extract(&acc, 0)); + duration_per_logical_op( + start.elapsed(), + (params.inverse_latency_iters * width) as u64, + ) + }) + }, + ); + + latency_group.finish(); + + let mut throughput_group = c.benchmark_group(format!( + "field_arith/{family}/throughput_stream/{label}_w{width}" + )); + + p3_bench_scalar_suite_throughput(&mut throughput_group, params, &scalar_stream_lanes); + + p3_bench_packed_throughput( + &mut throughput_group, + width, + "add", + params, + &packed_stream_lanes, + |acc, x| acc + x, + |a, b| a + b, + ); + p3_bench_packed_throughput( + &mut throughput_group, + width, + "sub", + params, + &packed_stream_lanes, + |acc, x| acc - x, + |a, b| a - b, + ); + p3_bench_packed_throughput( + &mut throughput_group, + width, + "mul", + params, + &packed_stream_lanes, + |acc, x| acc * x, + |a, b| a * b, + ); + p3_bench_packed_throughput( + &mut throughput_group, + width, + "square", + params, + &packed_stream_lanes, + |acc, _| acc.square(), + |a, _| a.square(), + ); + + throughput_group.throughput(Throughput::Elements(1)); + throughput_group.bench_function( + format!( + "packed_inverse_stream/{}x{width}x{}_ns_lane", + params.streams, params.inverse_throughput_iters + ), + |b| { + b.iter_custom(|iters| { + let lanes = black_box(&packed_stream_lanes); + let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.inverse_throughput_iters { + for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { + let next = F::Packing::from_fn(|i| { + (PackedValue::extract(acc_i, i) + PackedValue::extract(&lane.0, i)) + .inverse() + }); + *acc_i = next; + } + } + } + black_box(PackedValue::extract(&acc[0], 0)); + duration_per_logical_op( + start.elapsed(), + (params.streams * width * params.inverse_throughput_iters) as u64, + ) + }) + }, + ); + + throughput_group.finish(); +} + +pub(crate) fn bench_p3_ext_case( + c: &mut Criterion, + family: &str, + label: &str, + seed: u64, + params: ArithmeticBenchParams, +) where + Base: Field + Copy, + Base::Packing: PackedField + Copy, + EF: ExtensionField + BasedVectorSpace + Copy, + EF::ExtensionPacking: PackedFieldExtension + Copy, +{ + let width = ::WIDTH; + + let mut rng = StdRng::seed_from_u64(seed); + let scalar_latency_inputs: Vec = (0..params.latency_iters) + .map(|_| sample_ext::(&mut rng)) + .collect(); + let packed_latency_inputs: Vec = (0..params.latency_iters) + .map(|_| { + let ext_vals: Vec = (0..width) + .map(|_| sample_ext::(&mut rng)) + .collect(); + EF::ExtensionPacking::from_ext_slice(&ext_vals) + }) + .collect(); + let scalar_stream_lanes: Vec<(EF, EF)> = (0..params.streams) + .map(|_| (sample_ext(&mut rng), sample_ext(&mut rng))) + .collect(); + let packed_stream_lanes: Vec<(EF::ExtensionPacking, EF::ExtensionPacking)> = (0..params + .streams) + .map(|_| { + let a: Vec = (0..width) + .map(|_| sample_ext::(&mut rng)) + .collect(); + let b: Vec = (0..width) + .map(|_| sample_ext::(&mut rng)) + .collect(); + ( + EF::ExtensionPacking::from_ext_slice(&a), + EF::ExtensionPacking::from_ext_slice(&b), + ) + }) + .collect(); + + let mut latency_group = c.benchmark_group(format!( + "field_arith/{family}/latency_chain/{label}_w{width}" + )); + + p3_bench_scalar_suite_latency(&mut latency_group, params, &scalar_latency_inputs); + + let packed_zero = broadcast_ext::(EF::ZERO, width); + let packed_one = broadcast_ext::(EF::ONE, width); + + p3_bench_packed_ext_latency( + &mut latency_group, + width, + "add", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc + x, + packed_zero, + ); + p3_bench_packed_ext_latency( + &mut latency_group, + width, + "sub", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc - x, + packed_zero, + ); + p3_bench_packed_ext_unary_latency( + &mut latency_group, + width, + "neg", + params.latency_iters, + &packed_latency_inputs, + |acc| packed_zero - acc, + ); + p3_bench_packed_ext_unary_latency( + &mut latency_group, + width, + "double", + params.latency_iters, + &packed_latency_inputs, + |acc| acc + acc, + ); + p3_bench_packed_ext_latency( + &mut latency_group, + width, + "add_neg", + params.latency_iters, + &packed_latency_inputs, + |acc, x| packed_zero - (acc + x), + packed_zero, + ); + p3_bench_packed_ext_latency( + &mut latency_group, + width, + "double_add", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc + acc + x, + packed_zero, + ); + p3_bench_packed_ext_latency( + &mut latency_group, + width, + "mul", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc * x, + packed_one, + ); + p3_bench_packed_ext_latency( + &mut latency_group, + width, + "mul_add", + params.latency_iters, + &packed_latency_inputs, + |acc, x| acc * x + acc, + packed_one, + ); + p3_bench_packed_ext_unary_latency( + &mut latency_group, + width, + "square", + params.latency_iters, + &packed_latency_inputs, + |acc| acc.square(), + ); + p3_bench_packed_ext_unary_latency( + &mut latency_group, + width, + "mul_self", + params.latency_iters, + &packed_latency_inputs, + |acc| acc * acc, + ); + + latency_group.finish(); + + let mut throughput_group = c.benchmark_group(format!( + "field_arith/{family}/throughput_stream/{label}_w{width}" + )); + + p3_bench_scalar_suite_throughput(&mut throughput_group, params, &scalar_stream_lanes); + + p3_bench_packed_ext_throughput( + &mut throughput_group, + width, + "add", + params, + &packed_stream_lanes, + |acc, x| acc + x, + |a, b| a + b, + ); + p3_bench_packed_ext_throughput( + &mut throughput_group, + width, + "sub", + params, + &packed_stream_lanes, + |acc, x| acc - x, + |a, b| a - b, + ); + p3_bench_packed_ext_throughput( + &mut throughput_group, + width, + "mul", + params, + &packed_stream_lanes, + |acc, x| acc * x, + |a, b| a * b, + ); + p3_bench_packed_ext_throughput( + &mut throughput_group, + width, + "square", + params, + &packed_stream_lanes, + |acc, _| acc.square(), + |a, _| a.square(), + ); + + throughput_group.finish(); +} + +fn broadcast_ext + BasedVectorSpace>( + value: EF, + width: usize, +) -> EF::ExtensionPacking +where + EF::ExtensionPacking: PackedFieldExtension, +{ + EF::ExtensionPacking::from_ext_slice(&(0..width).map(|_| value).collect::>()) +} + +pub(crate) fn bench_p3_base_matrix(c: &mut Criterion) { + let params = ArithmeticBenchParams::from_env("AKITA_BENCH_BASE_ARITH", 2048, 256); + + bench_p3_base_case::(c, "base", "p3_mersenne31", 0xba5e_3131_0003, params); + bench_p3_base_case::(c, "base", "p3_baby_bear", 0xba5e_babe_0003, params); + bench_p3_base_case::(c, "base", "p3_koala_bear", 0xba5e_c0a1_a003, params); +} + +pub(crate) fn bench_p3_ext4_matrix(c: &mut Criterion) { + let params = ArithmeticBenchParams::from_env("AKITA_BENCH_EXT4_ARITH", 512, 128); + + bench_p3_ext_case::>( + c, + "ext4", + "p3_baby_bear_ext4", + 0xe400_babe_0004, + params, + ); + bench_p3_ext_case::>( + c, + "ext4", + "p3_koala_bear_ext4", + 0xe400_c0a1_a004, + params, + ); +} + +pub(crate) fn bench_p3_ext5_matrix(c: &mut Criterion) { + let params = ArithmeticBenchParams::from_env("AKITA_BENCH_EXT5_ARITH", 512, 128); + + bench_p3_ext_case::>( + c, + "ext5", + "p3_baby_bear_ext5", + 0xe500_babe_0005, + params, + ); + bench_p3_ext_case::>( + c, + "ext5", + "p3_koala_bear_ext5", + 0xe500_c0a1_a005, + params, + ); +} + +/// Full scalar latency-chain op set, shared by the base and extension matrices +/// (both operate on a `Field`, only the concrete type differs). +fn p3_bench_scalar_suite_latency( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + params: ArithmeticBenchParams, + inputs: &[S], +) { + p3_bench_scalar_latency( + group, + "add", + params.latency_iters, + inputs, + |acc, x| acc + x, + S::ZERO, + ); + p3_bench_scalar_latency( + group, + "sub", + params.latency_iters, + inputs, + |acc, x| acc - x, + S::ZERO, + ); + p3_bench_scalar_unary_latency(group, "neg", params.latency_iters, inputs, |acc| -acc); + p3_bench_scalar_unary_latency(group, "double", params.latency_iters, inputs, |acc| { + acc.double() + }); + p3_bench_scalar_latency( + group, + "add_neg", + params.latency_iters, + inputs, + |acc, x| -(acc + x), + S::ZERO, + ); + p3_bench_scalar_latency( + group, + "double_add", + params.latency_iters, + inputs, + |acc, x| acc + acc + x, + S::ZERO, + ); + p3_bench_scalar_latency( + group, + "mul", + params.latency_iters, + inputs, + |acc, x| acc * x, + S::ONE, + ); + p3_bench_scalar_latency( + group, + "mul_add", + params.latency_iters, + inputs, + |acc, x| acc * x + acc, + S::ONE, + ); + + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!("scalar_square_chain/{}_ns_per_op", params.latency_iters), + |b| { + b.iter_custom(|iters| { + let mut acc = black_box(inputs[0]); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.latency_iters { + acc = acc.square(); + } + } + black_box(acc); + duration_per_logical_op(start.elapsed(), params.latency_iters as u64) + }) + }, + ); + + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!("scalar_mul_self_chain/{}_ns_per_op", params.latency_iters), + |b| { + b.iter_custom(|iters| { + let mut acc = black_box(inputs[0]); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.latency_iters { + acc = acc * acc; + } + } + black_box(acc); + duration_per_logical_op(start.elapsed(), params.latency_iters as u64) + }) + }, + ); + + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!( + "scalar_inverse_chain/{}_ns_per_op", + params.inverse_latency_iters + ), + |b| { + b.iter_custom(|iters| { + let inputs = black_box(&inputs[..params.inverse_latency_iters]); + let mut acc = S::ONE; + let start = Instant::now(); + for _ in 0..iters { + for x in inputs { + acc = (acc + *x).inverse(); + } + } + black_box(acc); + duration_per_logical_op(start.elapsed(), params.inverse_latency_iters as u64) + }) + }, + ); +} + +/// Full scalar throughput-stream op set, shared by the base and extension matrices. +fn p3_bench_scalar_suite_throughput( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + params: ArithmeticBenchParams, + lanes: &[(S, S)], +) { + p3_bench_scalar_throughput(group, "add", params, lanes, |acc, x| acc + x, |a, b| a + b); + p3_bench_scalar_throughput(group, "sub", params, lanes, |acc, x| acc - x, |a, b| a - b); + p3_bench_scalar_throughput(group, "mul", params, lanes, |acc, x| acc * x, |a, b| a * b); + p3_bench_scalar_throughput( + group, + "square", + params, + lanes, + |acc, _| acc.square(), + |a, _| a.square(), + ); + + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!( + "scalar_inverse_stream/{}x{}_ns_per_op", + params.streams, params.inverse_throughput_iters + ), + |b| { + b.iter_custom(|iters| { + let lanes = black_box(lanes); + let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.inverse_throughput_iters { + for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { + *acc_i = (*acc_i + lane.0).inverse(); + } + } + } + black_box(acc[0]); + duration_per_logical_op( + start.elapsed(), + (params.streams * params.inverse_throughput_iters) as u64, + ) + }) + }, + ); +} + +fn p3_bench_scalar_latency( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + op: &str, + latency_iters: usize, + inputs: &[F], + step: impl Fn(F, F) -> F, + init: F, +) { + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!("scalar_{op}_chain/{latency_iters}_ns_per_op"), + |b| { + b.iter_custom(|iters| { + let inputs = black_box(inputs); + let mut acc = init; + let start = Instant::now(); + for _ in 0..iters { + for x in inputs { + acc = step(acc, *x); + } + } + black_box(acc); + duration_per_logical_op(start.elapsed(), latency_iters as u64) + }) + }, + ); +} + +fn p3_bench_scalar_unary_latency( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + op: &str, + latency_iters: usize, + inputs: &[F], + step: impl Fn(F) -> F, +) { + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!("scalar_{op}_chain/{latency_iters}_ns_per_op"), + |b| { + b.iter_custom(|iters| { + let mut acc = black_box(inputs[0]); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..latency_iters { + acc = step(acc); + } + } + black_box(acc); + duration_per_logical_op(start.elapsed(), latency_iters as u64) + }) + }, + ); +} + +fn p3_bench_packed_latency( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + width: usize, + op: &str, + latency_iters: usize, + inputs: &[PF], + step: impl Fn(PF, PF) -> PF, + init: PF, +) { + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!("packed_{op}_chain/{latency_iters}x{width}_ns_lane"), + |b| { + b.iter_custom(|iters| { + let inputs = black_box(inputs); + let mut acc = init; + let start = Instant::now(); + for _ in 0..iters { + for x in inputs { + acc = step(acc, *x); + } + } + black_box(::extract(&acc, 0)); + duration_per_logical_op(start.elapsed(), (latency_iters * width) as u64) + }) + }, + ); +} + +fn p3_bench_packed_unary_latency( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + width: usize, + op: &str, + latency_iters: usize, + inputs: &[PF], + step: impl Fn(PF) -> PF, +) { + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!("packed_{op}_chain/{latency_iters}x{width}_ns_lane"), + |b| { + b.iter_custom(|iters| { + let mut acc = black_box(inputs[0]); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..latency_iters { + acc = step(acc); + } + } + black_box(::extract(&acc, 0)); + duration_per_logical_op(start.elapsed(), (latency_iters * width) as u64) + }) + }, + ); +} + +fn p3_bench_scalar_throughput( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + op: &str, + params: ArithmeticBenchParams, + lanes: &[(F, F)], + step: impl Fn(F, F) -> F, + init: impl Fn(F, F) -> F, +) { + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!( + "scalar_{op}_stream/{}x{}_ns_per_op", + params.streams, params.throughput_iters + ), + |b| { + b.iter_custom(|iters| { + let lanes = black_box(lanes); + let mut acc: Vec = lanes.iter().map(|(a, b)| init(*a, *b)).collect(); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.throughput_iters { + for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { + *acc_i = step(*acc_i, lane.0); + } + } + } + black_box(acc[0]); + duration_per_logical_op( + start.elapsed(), + (params.streams * params.throughput_iters) as u64, + ) + }) + }, + ); +} + +fn p3_bench_packed_throughput( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + width: usize, + op: &str, + params: ArithmeticBenchParams, + lanes: &[(PF, PF)], + step: impl Fn(PF, PF) -> PF, + init: impl Fn(PF, PF) -> PF, +) { + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!( + "packed_{op}_stream/{}x{width}x{}_ns_lane", + params.streams, params.throughput_iters + ), + |b| { + b.iter_custom(|iters| { + let lanes = black_box(lanes); + let mut acc: Vec = lanes.iter().map(|(a, b)| init(*a, *b)).collect(); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.throughput_iters { + for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { + *acc_i = step(*acc_i, lane.0); + } + } + } + black_box(::extract(&acc[0], 0)); + duration_per_logical_op( + start.elapsed(), + (params.streams * width * params.throughput_iters) as u64, + ) + }) + }, + ); +} + +fn p3_bench_packed_ext_latency( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + width: usize, + op: &str, + latency_iters: usize, + inputs: &[EP], + step: impl Fn(EP, EP) -> EP, + init: EP, +) where + Base: Field, + EF: ExtensionField, + EP: PackedFieldExtension + Copy, +{ + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!("packed_{op}_chain/{latency_iters}x{width}_ns_lane"), + |b| { + b.iter_custom(|iters| { + let inputs = black_box(inputs); + let mut acc = init; + let start = Instant::now(); + for _ in 0..iters { + for x in inputs { + acc = step(acc, *x); + } + } + black_box(PackedFieldExtension::extract(&acc, 0)); + duration_per_logical_op(start.elapsed(), (latency_iters * width) as u64) + }) + }, + ); +} + +fn p3_bench_packed_ext_unary_latency( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + width: usize, + op: &str, + latency_iters: usize, + inputs: &[EP], + step: impl Fn(EP) -> EP, +) where + Base: Field, + EF: ExtensionField, + EP: PackedFieldExtension + Copy, +{ + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!("packed_{op}_chain/{latency_iters}x{width}_ns_lane"), + |b| { + b.iter_custom(|iters| { + let mut acc = black_box(inputs[0]); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..latency_iters { + acc = step(acc); + } + } + black_box(PackedFieldExtension::extract(&acc, 0)); + duration_per_logical_op(start.elapsed(), (latency_iters * width) as u64) + }) + }, + ); +} + +fn p3_bench_packed_ext_throughput( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + width: usize, + op: &str, + params: ArithmeticBenchParams, + lanes: &[(EP, EP)], + step: impl Fn(EP, EP) -> EP, + init: impl Fn(EP, EP) -> EP, +) where + Base: Field, + EF: ExtensionField, + EP: PackedFieldExtension + Copy, +{ + group.throughput(Throughput::Elements(1)); + group.bench_function( + format!( + "packed_{op}_stream/{}x{width}x{}_ns_lane", + params.streams, params.throughput_iters + ), + |b| { + b.iter_custom(|iters| { + let lanes = black_box(lanes); + let mut acc: Vec = lanes.iter().map(|(a, b)| init(*a, *b)).collect(); + let start = Instant::now(); + for _ in 0..iters { + for _ in 0..params.throughput_iters { + for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { + *acc_i = step(*acc_i, lane.0); + } + } + } + black_box(acc[0].extract(0)); + duration_per_logical_op( + start.elapsed(), + (params.streams * width * params.throughput_iters) as u64, + ) + }) + }, + ); +} diff --git a/crates/jolt-field/benches/solinas_field_arith/wide.rs b/crates/jolt-field/benches/solinas_field_arith/wide.rs new file mode 100644 index 0000000000..13805a3cdd --- /dev/null +++ b/crates/jolt-field/benches/solinas_field_arith/wide.rs @@ -0,0 +1,121 @@ +use criterion::{black_box, Criterion}; +use jolt_field::{CanonicalField, Prime128Offset275}; +use rand::{rngs::StdRng, RngCore, SeedableRng}; + +use super::data::rand_u128; + +pub(crate) fn bench_wide_ops(c: &mut Criterion) { + type F = Prime128Offset275; + + let mut rng = StdRng::seed_from_u64(0x01de_be0c_0001); + let a = F::from_canonical_u128_reduced(rand_u128(&mut rng)); + let b = F::from_canonical_u128_reduced(rand_u128(&mut rng)); + let b_u64 = rng.next_u64(); + + let mut group = c.benchmark_group("field_arith/wide/prime128_offset275"); + + group.bench_function("mul_wide_u64_only", |bench| { + bench.iter(|| black_box(black_box(a).mul_wide_u64(black_box(b_u64)))) + }); + + group.bench_function("mul_wide_only", |bench| { + bench.iter(|| black_box(black_box(a).mul_wide(black_box(b)))) + }); + + let limbs3 = [rng.next_u64(), rng.next_u64(), rng.next_u64()]; + let limbs4 = [ + rng.next_u64(), + rng.next_u64(), + rng.next_u64(), + rng.next_u64(), + ]; + + group.bench_function("mul_wide_limbs_3_to_5_only", |bench| { + bench.iter(|| black_box(black_box(a).mul_wide_limbs::<3, 5>(black_box(limbs3)))) + }); + group.bench_function("mul_wide_limbs_3_to_4_only", |bench| { + bench.iter(|| black_box(black_box(a).mul_wide_limbs::<3, 4>(black_box(limbs3)))) + }); + group.bench_function("mul_wide_limbs_4_to_5_only", |bench| { + bench.iter(|| black_box(black_box(a).mul_wide_limbs::<4, 5>(black_box(limbs4)))) + }); + group.bench_function("mul_wide_limbs_4_to_4_only", |bench| { + bench.iter(|| black_box(black_box(a).mul_wide_limbs::<4, 4>(black_box(limbs4)))) + }); + + group.bench_function("full_mul_u64_reduce", |bench| { + bench.iter(|| black_box(black_box(a) * F::from_u64(black_box(b_u64)))) + }); + + group.bench_function("full_mul_reduce", |bench| { + bench.iter(|| black_box(black_box(a) * black_box(b))) + }); + + let wide3 = a.mul_wide_u64(b_u64); + let wide4 = a.mul_wide(b); + let wide5 = { + let mut l = [0u64; 5]; + l[..3].copy_from_slice(&wide3); + l[4] = rng.next_u64() & 0xFF; + l + }; + + group.bench_function("solinas_reduce_3_limbs", |bench| { + bench.iter(|| black_box(F::solinas_reduce(black_box(&wide3)))) + }); + + group.bench_function("solinas_reduce_4_limbs", |bench| { + bench.iter(|| black_box(F::solinas_reduce(black_box(&wide4)))) + }); + + group.bench_function("solinas_reduce_5_limbs", |bench| { + bench.iter(|| black_box(F::solinas_reduce(black_box(&wide5)))) + }); + + group.bench_function("mul_wide_u64_roundtrip", |bench| { + bench.iter(|| { + let x = black_box(a); + let y = black_box(b_u64); + black_box(F::solinas_reduce(&x.mul_wide_u64(y))) + }) + }); + + group.bench_function("mul_wide_roundtrip", |bench| { + bench.iter(|| { + let x = black_box(a); + let y = black_box(b); + black_box(F::solinas_reduce(&x.mul_wide(y))) + }) + }); + + group.bench_function("mul_wide_limbs_3_to_5_roundtrip", |bench| { + bench.iter(|| { + let x = black_box(a); + let m = black_box(limbs3); + black_box(F::solinas_reduce(&x.mul_wide_limbs::<3, 5>(m))) + }) + }); + group.bench_function("mul_wide_limbs_3_to_4_roundtrip", |bench| { + bench.iter(|| { + let x = black_box(a); + let m = black_box(limbs3); + black_box(F::solinas_reduce(&x.mul_wide_limbs::<3, 4>(m))) + }) + }); + group.bench_function("mul_wide_limbs_4_to_5_roundtrip", |bench| { + bench.iter(|| { + let x = black_box(a); + let m = black_box(limbs4); + black_box(F::solinas_reduce(&x.mul_wide_limbs::<4, 5>(m))) + }) + }); + group.bench_function("mul_wide_limbs_4_to_4_roundtrip", |bench| { + bench.iter(|| { + let x = black_box(a); + let m = black_box(limbs4); + black_box(F::solinas_reduce(&x.mul_wide_limbs::<4, 4>(m))) + }) + }); + + group.finish(); +} diff --git a/crates/jolt-field/fuzz/Cargo.lock b/crates/jolt-field/fuzz/Cargo.lock index 1521538a07..38820de380 100644 --- a/crates/jolt-field/fuzz/Cargo.lock +++ b/crates/jolt-field/fuzz/Cargo.lock @@ -303,6 +303,7 @@ dependencies = [ "rand", "rand_core", "serde", + "thiserror", ] [[package]] @@ -476,6 +477,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.19.0" diff --git a/crates/jolt-field/fuzz/Cargo.toml b/crates/jolt-field/fuzz/Cargo.toml index 180097ec90..97af7c75a8 100644 --- a/crates/jolt-field/fuzz/Cargo.toml +++ b/crates/jolt-field/fuzz/Cargo.toml @@ -11,7 +11,7 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" -jolt-field = { path = ".." } +jolt-field = { path = "..", default-features = false, features = ["bn254", "solinas"] } num-traits = "0.2" [[bin]] @@ -33,3 +33,8 @@ doc = false name = "wide_accumulator_merge" path = "fuzz_targets/wide_accumulator_merge.rs" doc = false + +[[bin]] +name = "solinas_field_arith" +path = "fuzz_targets/solinas_field_arith.rs" +doc = false diff --git a/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs b/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs new file mode 100644 index 0000000000..99a70fbc28 --- /dev/null +++ b/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs @@ -0,0 +1,40 @@ +#![no_main] + +use jolt_field::{ + FpExt4, FromPrimitiveInt, Invertible, Prime128Offset275, Prime31Offset19, ReducingBytes, +}; +use libfuzzer_sys::fuzz_target; +use num_traits::Zero; + +fuzz_target!(|data: &[u8]| { + if data.len() < 64 { + return; + } + + let a31 = Prime31Offset19::from_le_bytes_mod_order(&data[..16]); + let b31 = Prime31Offset19::from_le_bytes_mod_order(&data[16..32]); + assert_eq!((a31 + b31) - b31, a31); + assert_eq!((a31 - b31) + b31, a31); + if !a31.is_zero() { + assert_eq!(a31 * a31.inverse().unwrap(), Prime31Offset19::from_u64(1)); + } + + let a128 = Prime128Offset275::from_le_bytes_mod_order(&data[..32]); + let b128 = Prime128Offset275::from_le_bytes_mod_order(&data[32..64]); + assert_eq!((a128 + b128) - b128, a128); + assert_eq!((a128 - b128) + b128, a128); + if !a128.is_zero() { + assert_eq!( + a128 * a128.inverse().unwrap(), + Prime128Offset275::from_u64(1) + ); + } + + let extension = FpExt4::new([a31, b31, a31 + b31, a31 - b31]); + if !extension.is_zero() { + assert_eq!( + extension * extension.inverse().unwrap(), + FpExt4::::from_u64(1) + ); + } +}); From 09b2f7b6ddd9427c756b781c39530a6c005e332d Mon Sep 17 00:00:00 2001 From: acentelles Date: Thu, 16 Jul 2026 12:50:28 -0400 Subject: [PATCH 03/38] ci: add shared field feature matrix and package identity check New field-stack job checks jolt-field with no backend, BN254, Solinas, and combined backends, runs the Solinas test suite, and enforces the shared field package identity: exactly one jolt-field in the workspace graph, with the pre-cutover akita-field allowed only from the immutable bootstrap Git pin. The final migration PR tightens the check to reject every akita-field identity. --- .github/workflows/rust.yml | 19 ++++++++++ scripts/check-shared-field-identity.sh | 52 ++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100755 scripts/check-shared-field-identity.sh diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5ccaac20b1..45e13a66ab 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -40,6 +40,25 @@ jobs: - name: taplo fmt --check run: taplo fmt --check + field-stack: + name: Shared field feature matrix + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Install nextest + uses: taiki-e/install-action@nextest + - name: Require one jolt-field identity (bootstrap akita-field pin allowed) + run: scripts/check-shared-field-identity.sh + - name: Check no backend + run: cargo check -p jolt-field --no-default-features + - name: Check BN254 backend + run: cargo check -p jolt-field --no-default-features --features bn254 + - name: Test Solinas backend + run: cargo nextest run -p jolt-field --no-default-features --features solinas --cargo-quiet + - name: Check combined backends + run: cargo check -p jolt-field --no-default-features --features bn254,solinas + clippy: runs-on: ubuntu-latest steps: diff --git a/scripts/check-shared-field-identity.sh b/scripts/check-shared-field-identity.sh new file mode 100755 index 0000000000..cae2e6e5c6 --- /dev/null +++ b/scripts/check-shared-field-identity.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Intermediate-state check for the staged Akita field migration. +# +# After the Solinas stack lands in `jolt-field` (this state), the workspace +# must resolve exactly one `jolt-field` package identity. The pre-cutover +# `akita-field` package is still reachable through the temporary bootstrap +# `akita` feature, but only from Jolt's immutable Akita Git pin, never from a +# local path. The final migration PR replaces this check with one that rejects +# every `akita-field` identity. + +tree="$(cargo tree --workspace --edges normal,build --prefix none)" + +jolt_identities="$( + grep '^jolt-field v' <<<"$tree" \ + | sed 's/ (\*)$//' \ + | sort -u +)" +jolt_count="$(grep -c '^jolt-field v' <<<"$jolt_identities" || true)" + +if [[ "$jolt_count" -ne 1 ]]; then + echo "error: expected exactly one jolt-field package identity, found $jolt_count" >&2 + printf '%s\n' "$jolt_identities" >&2 + exit 1 +fi + +akita_identities="$( + { grep '^akita-field v' <<<"$tree" || true; } \ + | sed 's/ (\*)$//' \ + | sort -u +)" + +if [[ -n "$akita_identities" ]]; then + akita_count="$(grep -c '^akita-field v' <<<"$akita_identities" || true)" + + if [[ "$akita_count" -ne 1 ]]; then + echo "error: expected at most one bootstrap akita-field identity, found $akita_count" >&2 + printf '%s\n' "$akita_identities" >&2 + exit 1 + fi + + if ! grep -q 'https://github.com/LayerZero-Labs/akita' <<<"$akita_identities"; then + echo "error: bootstrap akita-field must resolve from the pinned Akita Git source" >&2 + printf '%s\n' "$akita_identities" >&2 + exit 1 + fi + + printf 'bootstrap akita-field identity: %s\n' "$akita_identities" +fi + +printf 'shared field identity: %s\n' "$jolt_identities" From 763e3f0c93f6243cf18546684c5dbf5638e9f9e5 Mon Sep 17 00:00:00 2001 From: acentelles Date: Tue, 21 Jul 2026 19:01:12 -0400 Subject: [PATCH 04/38] docs(specs): add consolidate-field-traits spec Plan to shrink jolt-field from 46 public traits to at most 22: delete dead surface, merge single-purpose capability traits, adopt the arkworks/bn254.rs per-type impl style, and move wire serialization of the Solinas types to serde + bincode. Supersedes the granularity of unify-field-hierarchy.md while preserving its layering invariants. --- specs/consolidate-field-traits.md | 164 ++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 specs/consolidate-field-traits.md diff --git a/specs/consolidate-field-traits.md b/specs/consolidate-field-traits.md new file mode 100644 index 0000000000..adbe774cae --- /dev/null +++ b/specs/consolidate-field-traits.md @@ -0,0 +1,164 @@ +# Spec: Consolidate the jolt-field Trait Surface + +| Field | Value | +|-------------|--------------------------------| +| Author(s) | @Acentelles | +| Created | 2026-07-21 | +| Status | proposed | +| PR | feat/solinas-field-stack (branch, pre-PR) | + +## Summary + +The Solinas field stack imported by `feat/solinas-field-stack` brings `crates/jolt-field` to 46 public traits across 82 files, with 15 one-trait-per-file micro-files at the crate root (several under 10 lines). For comparison, arkworks' `ff` + `serialize` crates expose 25 public traits while covering serialization, extension towers up to degree 12, FFT domains, and hash-to-field. A usage audit of this workspace and of akita (the stack's main consumer, pinned to this branch) shows that a large fraction of the surface has no generic consumer anywhere: both signed-accumulator trait families, `ExtensionCoeff`, `ScaleI32`, `BalancedDigitLookup`, `MontgomeryConstants`, and the entire `fft.rs` module (1,100 lines). Three parallel deferred-reduction accumulator designs coexist. This spec consolidates the crate to roughly 22 traits and 4 root modules, adopts the explicit per-type implementation style of `arkworks/bn254.rs`, and switches wire serialization for the Solinas types to serde + bincode. The goal is to minimize the audit surface without losing the layering that lets non-BN254 fields and rings implement only what they have. + +This spec supersedes parts of [`unify-field-hierarchy.md`](./unify-field-hierarchy.md) (PR #1484). That spec's core insight is preserved: a slim algebraic ladder (`AdditiveGroup` to `RingCore` to `FieldCore`) with capabilities that are not algebraic descendants of the field marker. What is superseded is the granularity: one trait per capability method proved too fine once the extension-field, packed, and unreduced layers landed on top of it. + +## Intent + +### Goal + +Reduce `crates/jolt-field` from 46 public traits to at most 22 by deleting dead traits and merging single-purpose capability traits into cohesive ones, while keeping every generic algorithm in this workspace and in akita expressible. + +Key structural decisions: + +- The algebraic ladder stays: `AdditiveGroup` to `RingCore` to `FieldCore`, with `Field` as the Jolt compatibility umbrella. +- One canonical-representation trait (`CanonicalRepr`) replaces the seven byte/introspection/challenge traits. +- One `Accumulator` trait replaces the six accumulator traits that have consumers; the two families with zero consumers are deleted. +- The extension-field surface is `ExtField` (absorbing `LiftBase`, `MulBase`, `FrobeniusExtField`), `MulBaseUnreduced`, `FpExt2Config`, and a single merged `ExtMulBackend`. +- Wire serialization of Solinas types is serde + bincode, following the existing serde implementations on `arkworks/bn254.rs`; the hand-rolled akita-style byte encoding is not adopted. Canonical bytes survive only as the Fiat-Shamir transcript surface. +- Concrete types follow the `arkworks/bn254.rs` pattern: everything a type implements is visible in that type's file, either as an explicit `impl` block or as a one-line invocation of a shared macro. The three `native_algebra.rs` side-files are eliminated. + +### Invariants + +This is a refactor of trait boundaries, not of arithmetic. The `jolt-eval` invariants relevant to field arithmetic (`SplitEqBindLowHigh`, `SplitEqBindHighLow`, `FieldMulScalar`, `Soundness`) must continue to pass. No new `jolt-eval` invariants are required. + +1. **BN254 behavior is byte-identical.** Same Fiat-Shamir stream, same proof bytes for the `muldiv` e2e test in standard mode, same verifier outcomes. `Fr`'s existing serde and byte encodings are untouched. +2. **Every operation currently callable through a jolt-field trait remains available** under the merged trait, same semantics, same `#[inline]` discipline on both trait method and impl (carried over from #1484, invariant 7). +3. **The slim hierarchy remains implementable by a non-BN254 field with no arkworks dependency.** The `mersenne61_compat` test (in `jolt-sumcheck`) keeps compiling and passing against `jolt-field --no-default-features`, with its bound list updated to the merged traits. +4. **Rings that are not fields keep a home.** `RingCore` (no inversion) and `AdditiveGroup` (implemented by wide accumulator types that have no multiplication) survive as distinct layers; nothing forces a cyclotomic ring or an accumulator to claim field capabilities. +5. **Transcript bytes never go through bincode.** Fiat-Shamir absorption and challenge derivation use the explicit canonical little-endian encoding on `CanonicalRepr`; bincode is only the proof/wire format. +6. **No codegen regressions.** All dispatch remains static; merged traits change name resolution, not monomorphization. + + +### Non-Goals + +1. **Akita compatibility in lockstep.** Akita pins a git rev of this fork and will adapt its trait bounds when it next bumps the pin. No companion PR, no intermediate compatibility shims. +2. Changing BN254 proof wire format or any transcript byte stream. +3. Unifying jolt-prover-legacy's own `JoltField` deferred-reduction machinery (`mul_to_product_accum`, `Folded256ProductAccum`) with this crate's. Same idea, third copy, separate migration. +4. Macro-generating the packed SIMD intrinsic bodies. NEON and AVX arithmetic are genuinely different; only their selection boilerplate is consolidated. +5. Removing the temporary `akita` bootstrap feature and `src/akita.rs`; that happens in the final migration PR as already planned. +6. Renaming `Field`; it stays the umbrella name (per the #1484 naming decision). + +## Evaluation + +### Acceptance Criteria + +- [ ] `grep -rc '^pub trait' crates/jolt-field/src` totals at most 22. +- [ ] The crate root has at most 4 trait-defining modules (`algebra.rs`, `canonical.rs`, `accumulator.rs`, `field.rs`) plus the feature-gated backend modules; the 15 micro-files are gone. +- [ ] Zero references remain to: `SignedScalarAccumulator`, `WithSmallScalarAccumulator`, `SignedProductAccumulator`, `WithSignedProductAccumulator`, `ExtensionCoeff`, `BalancedDigitLookup` (trait), `ScaleI32`, `PackedValue`, `LiftBase`, `MulBase`, `FrobeniusExtField`, `FpExt4MulBackend`, `FpExt8MulBackend`, `AdditiveAccumulator`, `Invertible`, `RandomSampling`, `MulPow2`, `MulPrimitiveInt`, `CanonicalBytes`, `ReducingBytes`, `FixedByteSize`, `FixedBytes`, `CanonicalU64`, `CanonicalBitLength`, `TranscriptChallenge`, `SmoothFftField`. +- [ ] `fft.rs` is removed from `jolt-field` (it has zero consumers in this workspace). +- [ ] The three `native_algebra.rs` files are deleted; one shared macro provides the supertrait glue, invoked from each concrete type's own file. +- [ ] `Fp32`, `Fp64`, `Fp128`, `FpExt2`, `FpExt4`, `FpExt8` implement `serde::Serialize`/`Deserialize` with canonical encoding; a bincode round-trip test covers each. +- [ ] The degree-4 extension mul/square schedule exists in exactly one place (shared between scalar and packed backends, as degree-8 already is). +- [ ] `muldiv` e2e passes in both modes; standard-mode proof bytes match main (size and content). +- [ ] Serialized size tests: each Solinas field element bincode-encodes to exactly `NUM_BYTES` bytes; a `Vec` of $n$ elements encodes to $n \cdot \texttt{NUM\_BYTES}$ plus a single length prefix. +- [ ] `mersenne61_compat` passes with updated bounds and still no arkworks dependency. + +### Testing Strategy + +- Full `cargo nextest run --cargo-quiet`; `muldiv` e2e in `--features host` and `--features host,zk`. +- `cargo clippy --all --features host -q --all-targets -- -D warnings` and again with `host,zk`. +- `jolt-field` standalone under feature combos: `bn254`, `solinas`, `bn254,solinas,parallel`, `--no-default-features`. +- The `solinas_field_arith` fuzz target and both criterion benches must still build. +- Existing in-crate unit tests (prime, ext, packed, unreduced) are updated for renamed bounds but not weakened; deleted traits take their dead tests with them. +- New: bincode round-trip tests for all Solinas types; a compile test that the merged `CanonicalRepr` default challenge derivation matches the previous `TranscriptChallenge` behavior on `Fr` (identical bytes in, identical element out). + +### Performance + +No regression expected: every merge is a compile-time rename and static dispatch is unchanged. Verify, do not assume: + +- Run `solinas_field_arith` and `field_arith` criterion benches before and after; results within noise. +- Proof size: standard-mode `muldiv` proof bytes are compared against main (identical, so identical size); Solinas-side serialized sizes are asserted by the per-element `NUM_BYTES` tests in Acceptance Criteria. +- The existing `jolt-eval` objectives for prover time must not move; no new objectives needed. +- All `#[inline]`/`#[inline(always)]` annotations are preserved through the moves. + +## Design + +### Architecture + +Trait disposition, all 46 accounted for: + +**Deleted, no consumer in this workspace or akita (11):** + +| Trait | Notes | +|---|---| +| `SignedScalarAccumulator`, `WithSmallScalarAccumulator` | plus `NaiveSignedScalarAccumulator`, `FrSmallScalarAccumulator`, `arkworks/small_scalar_accumulator.rs` | +| `SignedProductAccumulator`, `WithSignedProductAccumulator` | plus `NaiveSignedProductAccumulator`, `FrSignedProductAccumulator`, `arkworks/signed_product_accumulator.rs` | +| `ExtensionCoeff` | single blanket impl; inline the bound at its one use in `fp_ext2` | +| `BalancedDigitLookup` | becomes a free function `balanced_digit_lut(log_basis)` | +| `MontgomeryConstants` | one impl (`Fr`), zero consumers; OPEN: confirm no out-of-tree GPU/Metal consumer before deleting, else move private into `arkworks/` | +| `SmoothFftField` | leaves the crate together with `fft.rs` | +| `ScaleI32`, `PackedValue`, `AdditiveAccumulator`, absorbed below | listed here for completeness of the count | + +**Merged (22 traits fold into 7 survivors):** + +| Survivor | Absorbs | Rationale | +|---|---|---| +| `FieldCore` | `Invertible`, `RandomSampling` | every `FieldCore` type implements both; rings stay unaffected at `RingCore` | +| `FromPrimitiveInt` (gains `RingCore` supertrait) | `MulPow2`, `MulPrimitiveInt` | the absorbed traits are pure default-method helpers over exactly this bound | +| `CanonicalRepr` (new, one file) | `CanonicalBytes`, `ReducingBytes`, `FixedByteSize`, `FixedBytes`, `CanonicalU64`, `CanonicalBitLength`, `TranscriptChallenge` | one trait: `NUM_BYTES`, `to_bytes_le`, `from_le_bytes_mod_order`, `to_canonical_u64_checked`, `num_bits`, `from_challenge_bytes` (defaulted to reducing decode) | +| `Accumulator` | `AdditiveAccumulator` + `RingAccumulator` | the two are only ever implemented and consumed together (`WideAccumulator`, `NaiveAccumulator`) | +| `ExtField` | `LiftBase`, `MulBase`, `FrobeniusExtField` | identical implementor sets (blanket `F` + `FpExt2/4/8`); Frobenius requires a pseudo-Mersenne base, which all current bases are | +| `ExtMulBackend` | `FpExt4MulBackend` + `FpExt8MulBackend` | same three implementors, same role (per-width fused schedules) | +| `ReduceTo` | `ScaleI32` | same three wide-limb implementors | +| `PackedField` | `PackedValue` | identical 13 implementors; nothing bounds on `PackedValue` alone | + +**Kept as-is (14):** `AdditiveGroup`, `RingCore`, `Field` (umbrella), `WithAccumulator` (the additive-layer accumulator association that lets rings use `NaiveAccumulator` without field capabilities, per #1484), `OptimizedMul` (consumed by jolt-prover-legacy; candidate to relocate there in a later sweep), `CanonicalField`, `HalvingField` (implemented by extension fields, so it cannot fold into `CanonicalField`), `PseudoMersenneField`, `MulBaseUnreduced`, `FpExt2Config`, `HasUnreducedOps`, `HasOptimizedFold`, `HasWide`, `HasPacking`. + +Final count: 7 survivors + 14 kept = at most 22 public traits (21 if `MontgomeryConstants`' deletion is confirmed and `OptimizedMul` relocates). + +**Serialization split.** Two distinct concerns, two mechanisms: + +- Proof/wire format: serde + bincode. Solinas types serialize their canonical form (never an internal representation), exactly as `arkworks/bn254.rs` already does for `Fr`. Extension fields serialize as arrays of base-field elements. Nothing replicates akita-serialization's `FpExt2Config`-bound custom encode/decode. +- Fiat-Shamir: `CanonicalRepr`'s explicit little-endian canonical encoding. A transcript needs a specified encoding, not whatever an encoder version emits, so this deliberately does not route through bincode. + +**File layout.** Crate root: `algebra.rs` (`AdditiveGroup`, `RingCore`, `FieldCore`, `FromPrimitiveInt`, `OptimizedMul`), `canonical.rs` (`CanonicalRepr`), `accumulator.rs` (`Accumulator`, `WithAccumulator`, `NaiveAccumulator`), `field.rs` (`Field` umbrella). Solinas traits (`CanonicalField`, `HalvingField`, `PseudoMersenneField`) move into `prime/mod.rs`. Each concrete type's file shows its full trait surface bn254.rs-style; shared macros are limited to: + +1. one `impl_native_algebra!` macro (replacing the three `native_algebra.rs` files and the 230 hand-written lines in `ext/native_algebra.rs`), invoked inside each type's file; +2. one operator-matrix macro in the style of `bn254.rs`'s `delegate_binop!` and legacy's `impl_field_ops_inline!`, deduplicating the hand-written `Add`/`Sub`/`Mul`/`Neg`/`*Assign`/by-reference blocks in `fp32.rs`, `fp64.rs`, `fp128/`, and the ext types (reduction bodies stay hand-written per type; `fp64`'s `C_SHIFT` specialization is math, not boilerplate); +3. the existing capability macro (`native_capability.rs` contents), with invocations moved into the per-type files. + +The three near-identical cfg-cascade blocks selecting `Fp{32,64,128}Packing` in `packed/mod.rs:324-415` collapse into one macro. Packed intrinsic bodies are untouched. + +### Alternatives Considered + +1. **Keep the fine-grained hierarchy of #1484.** Rejected: at 46 traits the decomposition costs more audit surface than it buys in bound minimalism, and several capabilities it kept orthogonal (`TranscriptChallenge`, `FixedBytes`) have exactly the six field types as implementors, making the orthogonality theoretical. Where #1484's rationale is still live (rings without inversion, additive-only accumulator types, no-arkworks implementability), the layer survives. +2. **One monolithic `JoltField` like jolt-prover-legacy.** Rejected: akita's prover is written against the intermediate layers (`CanonicalField` ~578 bound sites, `ExtField` ~358, `HasWide` ~118), and cyclotomic rings need `RingCore` without field claims. A monolith re-creates the problem #1484 fixed. +3. **Macro-generate whole field types (plonky3-style).** Rejected in favor of the bn254.rs pattern: explicit impls are the audit-friendly choice; macros are confined to operator matrices and supertrait glue where the expansion is mechanical and identical across types. +4. **Merging `TranscriptChallenge` into `CanonicalRepr`** was debated against #1484's warning that binary fields and Plonky-style challengers decode challenges differently. Merged anyway with a defaulted method: if a challenge type that is not a canonical field ever appears, re-split the trait with that first consumer. Recorded here so the re-split is a known escape hatch, not a regression. + +## Documentation + +Internal refactor of a crate the book does not document in detail. After landing, run `/update-docs` against the base commit to catch any book references to renamed traits; expected changes are nil or a few identifier updates in the architecture chapter. + +## Execution + +Five phases, each independently green (clippy both modes, nextest, `muldiv` both modes): + +1. **Deletions.** Signed-accumulator families, `ExtensionCoeff`, `BalancedDigitLookup` to free fn, `fft.rs` + `SmoothFftField` out, `MontgomeryConstants` (after the GPU check). Drop the corresponding supertraits from `Field`. +2. **Accumulators.** Merge `AdditiveAccumulator` + `RingAccumulator` into `Accumulator`; keep `WithAccumulator` as the association point; implementors unchanged. +3. **Root merges + serde.** `FieldCore` and `FromPrimitiveInt` absorptions; `CanonicalRepr`; serde impls + bincode round-trip tests for all Solinas types; root file consolidation to 4 modules; update `mersenne61_compat` bounds. +4. **Ext cluster.** `ExtField` absorbs lift/mul_base/frobenius; `ExtMulBackend`; dedupe the degree-4 schedule shared with `packed/`. +5. **Layout + macros.** `impl_native_algebra!`, the operator-matrix macro, per-type invocation placement, packed cfg-cascade collapse. Benchmark before/after this phase specifically. + +Roughly half the touched traits predate this branch on main (the #1484 layer), so the diff extends beyond the branch's own delta; this is intentional and is what the consolidation is for. + +Open question blocking only its own line item: does `MontgomeryConstants` have an out-of-tree GPU/Metal consumer? (Its doc comment says it exists for GPU backends.) + +## References + +- [`unify-field-hierarchy.md`](./unify-field-hierarchy.md), PR #1484: predecessor spec; this spec supersedes its granularity while preserving its layering and invariants 1, 2, 3, 7. +- `crates/jolt-field/src/arkworks/bn254.rs`: the implementation pattern to follow. +- arkworks `ff` + `serialize` (25 public traits): the external calibration point for trait-count parity. +- akita (`LayerZero-Labs/akita`, formerly hachi): consumer whose generic bound sites define which traits are load-bearing; adapts on its next pin bump. +- `scripts/check-shared-field-identity.sh`: unchanged by this spec. From fec8255084f0ba1813b35a805a4a7cee6325924f Mon Sep 17 00:00:00 2001 From: acentelles Date: Tue, 21 Jul 2026 19:01:12 -0400 Subject: [PATCH 05/38] refactor(field): delete dead trait surface (spec phase 1) Remove traits with no generic consumer in this workspace or akita: - SignedScalarAccumulator/WithSmallScalarAccumulator and SignedProductAccumulator/WithSignedProductAccumulator families, including the Fr and Naive implementations and the two bn254_ops kernels only they used; the Field umbrella no longer requires them - ExtensionCoeff (single blanket impl; bounds inlined at use sites) - BalancedDigitLookup (replaced by the free fn balanced_digit_lut) - SmoothFftField and fft.rs (no consumer outside their own tests) MontgomeryConstants is retained pending confirmation that no out-of-tree GPU backend consumes it. --- crates/jolt-field/src/akita.rs | 13 +- crates/jolt-field/src/arkworks/bn254.rs | 9 - crates/jolt-field/src/arkworks/bn254_fq.rs | 13 +- crates/jolt-field/src/arkworks/bn254_ops.rs | 14 - crates/jolt-field/src/arkworks/mod.rs | 2 - .../arkworks/signed_product_accumulator.rs | 121 -- .../src/arkworks/small_scalar_accumulator.rs | 120 -- crates/jolt-field/src/ext/fp_ext2.rs | 6 +- crates/jolt-field/src/ext/fp_ext4.rs | 16 +- crates/jolt-field/src/ext/fp_ext8.rs | 2 - crates/jolt-field/src/ext/mod.rs | 14 +- crates/jolt-field/src/fft.rs | 1098 ----------------- crates/jolt-field/src/field.rs | 4 +- crates/jolt-field/src/lib.rs | 22 +- crates/jolt-field/src/prime/fp128/core.rs | 19 - crates/jolt-field/src/prime/fp128/mod.rs | 2 +- crates/jolt-field/src/prime/fp128/primes.rs | 19 - crates/jolt-field/src/prime/fp128/traits.rs | 6 - crates/jolt-field/src/prime/fp32.rs | 4 +- crates/jolt-field/src/prime/fp64.rs | 4 +- .../jolt-field/src/prime/native_capability.rs | 11 +- .../src/signed_product_accumulator.rs | 54 - .../src/small_scalar_accumulator.rs | 60 - crates/jolt-field/src/solinas_traits.rs | 36 +- 24 files changed, 33 insertions(+), 1636 deletions(-) delete mode 100644 crates/jolt-field/src/arkworks/signed_product_accumulator.rs delete mode 100644 crates/jolt-field/src/arkworks/small_scalar_accumulator.rs delete mode 100644 crates/jolt-field/src/fft.rs delete mode 100644 crates/jolt-field/src/signed_product_accumulator.rs delete mode 100644 crates/jolt-field/src/small_scalar_accumulator.rs diff --git a/crates/jolt-field/src/akita.rs b/crates/jolt-field/src/akita.rs index 90d31fc216..a6f679834f 100644 --- a/crates/jolt-field/src/akita.rs +++ b/crates/jolt-field/src/akita.rs @@ -4,9 +4,8 @@ use rand_core::RngCore; use crate::{ AdditiveGroup, CanonicalBitLength, CanonicalBytes, CanonicalU64, Field, FieldCore, FixedByteSize, FixedBytes, FromPrimitiveInt, Invertible, MulPow2, MulPrimitiveInt, - NaiveAccumulator, NaiveSignedProductAccumulator, NaiveSignedScalarAccumulator, RandomSampling, - ReducingBytes, RingCore, TranscriptChallenge, WithAccumulator, WithSignedProductAccumulator, - WithSmallScalarAccumulator, + NaiveAccumulator, RandomSampling, ReducingBytes, RingCore, TranscriptChallenge, + WithAccumulator, }; impl AdditiveGroup for AkitaField {} @@ -100,12 +99,4 @@ impl WithAccumulator for AkitaField { type Accumulator = NaiveAccumulator; } -impl WithSmallScalarAccumulator for AkitaField { - type SmallScalarAccumulator = NaiveSignedScalarAccumulator; -} - -impl WithSignedProductAccumulator for AkitaField { - type SignedProductAccumulator = NaiveSignedProductAccumulator; -} - impl Field for AkitaField {} diff --git a/crates/jolt-field/src/arkworks/bn254.rs b/crates/jolt-field/src/arkworks/bn254.rs index bbbe361359..d31096ca7f 100644 --- a/crates/jolt-field/src/arkworks/bn254.rs +++ b/crates/jolt-field/src/arkworks/bn254.rs @@ -6,7 +6,6 @@ use crate::{ AdditiveGroup, CanonicalBitLength, CanonicalBytes, CanonicalU64, Field, FieldCore, FixedByteSize, FixedBytes, FromPrimitiveInt, Invertible, Limbs, MulPrimitiveInt, RandomSampling, ReducingBytes, RingCore, TranscriptChallenge, WithAccumulator, - WithSignedProductAccumulator, WithSmallScalarAccumulator, }; use ark_ff::{prelude::*, PrimeField, UniformRand}; use rand_core::RngCore; @@ -448,14 +447,6 @@ impl WithAccumulator for Fr { type Accumulator = super::wide_accumulator::WideAccumulator; } -impl WithSmallScalarAccumulator for Fr { - type SmallScalarAccumulator = super::small_scalar_accumulator::FrSmallScalarAccumulator; -} - -impl WithSignedProductAccumulator for Fr { - type SignedProductAccumulator = super::signed_product_accumulator::FrSignedProductAccumulator; -} - impl crate::MulPow2 for Fr {} impl MulPrimitiveInt for Fr { diff --git a/crates/jolt-field/src/arkworks/bn254_fq.rs b/crates/jolt-field/src/arkworks/bn254_fq.rs index 73d620f695..f40eb41ed2 100644 --- a/crates/jolt-field/src/arkworks/bn254_fq.rs +++ b/crates/jolt-field/src/arkworks/bn254_fq.rs @@ -6,9 +6,8 @@ use crate::{ AdditiveGroup, CanonicalBitLength, CanonicalBytes, CanonicalU64, Field, FieldCore, FixedByteSize, FixedBytes, FromPrimitiveInt, Invertible, Limbs, MulPrimitiveInt, - NaiveAccumulator, NaiveSignedProductAccumulator, NaiveSignedScalarAccumulator, RandomSampling, - ReducingBytes, RingCore, TranscriptChallenge, WithAccumulator, WithSignedProductAccumulator, - WithSmallScalarAccumulator, + NaiveAccumulator, RandomSampling, ReducingBytes, RingCore, TranscriptChallenge, + WithAccumulator, }; use ark_ff::{prelude::*, PrimeField, UniformRand}; use rand_core::RngCore; @@ -425,14 +424,6 @@ impl WithAccumulator for Fq { type Accumulator = NaiveAccumulator; } -impl WithSmallScalarAccumulator for Fq { - type SmallScalarAccumulator = NaiveSignedScalarAccumulator; -} - -impl WithSignedProductAccumulator for Fq { - type SignedProductAccumulator = NaiveSignedProductAccumulator; -} - impl crate::MulPow2 for Fq {} impl MulPrimitiveInt for Fq {} diff --git a/crates/jolt-field/src/arkworks/bn254_ops.rs b/crates/jolt-field/src/arkworks/bn254_ops.rs index 7fb684a10d..aabb3cc378 100644 --- a/crates/jolt-field/src/arkworks/bn254_ops.rs +++ b/crates/jolt-field/src/arkworks/bn254_ops.rs @@ -2,7 +2,6 @@ //! //! Low-level field arithmetic (Montgomery/Barrett reduction, scalar multiplication, //! precomputed lookup tables). -use crate::Limbs; use ark_bn254::FrConfig; use ark_ff::{BigInt, Fp, MontConfig}; use num_traits::Zero; @@ -332,14 +331,6 @@ fn bigint4_mul_u64(a: &BigInt, b: u64) -> BigInt<5> { res } -#[inline(always)] -pub(crate) fn mul_u64_unreduced(a: Fr, b: u64) -> Limbs<5> { - if b == 0 || Zero::is_zero(&a) { - return Limbs::zero(); - } - bigint4_mul_u64(&a.0, b).into() -} - /// Multiply BigInt<4> by u128, producing BigInt<6>. #[inline(always)] fn bigint4_mul_u128(a: &BigInt, b: u128) -> BigInt<6> { @@ -375,11 +366,6 @@ fn from_unchecked_nplus1(element: BigInt<5>) -> Fr { Fp::new_unchecked(r) } -#[inline(always)] -pub(crate) fn reduce_nplus1(element: Limbs<5>) -> Fr { - from_unchecked_nplus1(element.into()) -} - /// Barrett reduce BigInt<6> → Fr via two rounds #[inline(always)] fn from_unchecked_nplus2(element: BigInt<6>) -> Fr { diff --git a/crates/jolt-field/src/arkworks/mod.rs b/crates/jolt-field/src/arkworks/mod.rs index 7ff1700012..3014d7ce24 100644 --- a/crates/jolt-field/src/arkworks/mod.rs +++ b/crates/jolt-field/src/arkworks/mod.rs @@ -10,8 +10,6 @@ pub mod bn254; pub mod bn254_fq; pub(crate) mod bn254_ops; pub mod montgomery_impl; -pub mod signed_product_accumulator; -pub mod small_scalar_accumulator; pub mod wide_accumulator; impl From> for BigInt { diff --git a/crates/jolt-field/src/arkworks/signed_product_accumulator.rs b/crates/jolt-field/src/arkworks/signed_product_accumulator.rs deleted file mode 100644 index 7e287ea029..0000000000 --- a/crates/jolt-field/src/arkworks/signed_product_accumulator.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::{signed::S256, Limbs, SignedProductAccumulator}; -use ark_ff::{BigInt, MontConfig}; -use num_traits::Zero; - -use super::{bn254::Fr, bn254_ops}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct FrSignedProductAccumulator { - pos: [u128; 8], - neg: [u128; 8], -} - -impl Default for FrSignedProductAccumulator { - #[inline] - fn default() -> Self { - Self { - pos: [0; 8], - neg: [0; 8], - } - } -} - -impl FrSignedProductAccumulator { - #[inline(always)] - fn fmadd_magnitude(slots: &mut [u128; 8], value: Fr, magnitude: Limbs<4>) { - let value = value.inner_limbs(); - for i in 0..4 { - for j in 0..4 { - let product = (value.0[i] as u128) * (magnitude.0[j] as u128); - slots[i + j] += (product as u64) as u128; - slots[i + j + 1] += ((product >> 64) as u64) as u128; - } - } - } - - #[inline] - fn normalize(slots: [u128; 8]) -> Limbs<9> { - let mut out = [0u64; 9]; - let mut carry = 0u128; - for (index, slot) in slots.into_iter().enumerate() { - let (sum, overflow) = slot.overflowing_add(carry); - out[index] = sum as u64; - carry = (sum >> 64) + ((overflow as u128) << 64); - } - out[8] = carry as u64; - Limbs(out) - } -} - -impl SignedProductAccumulator for FrSignedProductAccumulator { - type Element = Fr; - - #[inline(always)] - fn fmadd_s256(&mut self, value: Fr, scalar: &S256) { - if scalar.is_zero() { - return; - } - if scalar.is_positive { - Self::fmadd_magnitude(&mut self.pos, value, scalar.magnitude); - } else { - Self::fmadd_magnitude(&mut self.neg, value, scalar.magnitude); - } - } - - #[inline] - fn reduce(self) -> Fr { - let pos = Self::normalize(self.pos); - let neg = Self::normalize(self.neg); - let montgomery_r_value = - Fr::from_bigint_unchecked(Limbs(>::R2.0)); - let reduced = if pos >= neg { - Fr::from_inner(bn254_ops::from_montgomery_reduce(BigInt::from( - pos.sub_trunc::<9, 9>(&neg), - ))) - } else { - -Fr::from_inner(bn254_ops::from_montgomery_reduce(BigInt::from( - neg.sub_trunc::<9, 9>(&pos), - ))) - }; - reduced * montgomery_r_value - } -} - -#[cfg(test)] -mod tests { - use crate::FromPrimitiveInt; - - use super::*; - - fn s256_to_fr(value: &S256) -> Fr { - let mut bytes = [0u8; 32]; - for (index, limb) in value.magnitude_limbs().iter().copied().enumerate() { - bytes[index * 8..(index + 1) * 8].copy_from_slice(&limb.to_le_bytes()); - } - let magnitude = Fr::from_le_bytes_mod_order(&bytes); - if value.is_positive { - magnitude - } else { - -magnitude - } - } - - #[test] - fn signed_product_accumulator_reduces_mixed_terms() { - let terms = [ - (Fr::from_u64(3), S256::from_i128(17)), - (Fr::from_u64(11), S256::from_i128(-9)), - (Fr::from_u64(42), S256::new([7, 5, 3, 1], true)), - (Fr::from_u64(6), S256::new([u64::MAX, 19, 0, 0], false)), - ]; - - let mut acc = FrSignedProductAccumulator::default(); - let mut expected = Fr::from_u64(0); - for (field, scalar) in terms { - acc.fmadd_s256(field, &scalar); - expected += field * s256_to_fr(&scalar); - } - - assert_eq!(acc.reduce(), expected); - } -} diff --git a/crates/jolt-field/src/arkworks/small_scalar_accumulator.rs b/crates/jolt-field/src/arkworks/small_scalar_accumulator.rs deleted file mode 100644 index 3523395a4f..0000000000 --- a/crates/jolt-field/src/arkworks/small_scalar_accumulator.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::{Limbs, SignedScalarAccumulator}; - -use super::{bn254::Fr, bn254_ops}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct FrSmallScalarAccumulator { - pos: Limbs<5>, - neg: Limbs<5>, -} - -impl Default for FrSmallScalarAccumulator { - #[inline(always)] - fn default() -> Self { - Self { - pos: Limbs::zero(), - neg: Limbs::zero(), - } - } -} - -impl FrSmallScalarAccumulator { - #[inline(always)] - fn add_to_pos(&mut self, value: Fr) { - self.pos.add_assign_trunc::<4>(&value.inner_limbs()); - } - - #[inline(always)] - fn add_to_neg(&mut self, value: Fr) { - self.neg.add_assign_trunc::<4>(&value.inner_limbs()); - } - - #[inline(always)] - fn fmadd_magnitude_to_pos(&mut self, value: Fr, scalar: u64) { - if scalar == 0 { - return; - } - if scalar == 1 { - self.add_to_pos(value); - return; - } - self.pos - .add_assign_trunc::<5>(&bn254_ops::mul_u64_unreduced(value.0, scalar)); - } - - #[inline(always)] - fn fmadd_magnitude_to_neg(&mut self, value: Fr, scalar: u64) { - if scalar == 0 { - return; - } - if scalar == 1 { - self.add_to_neg(value); - return; - } - self.neg - .add_assign_trunc::<5>(&bn254_ops::mul_u64_unreduced(value.0, scalar)); - } -} - -impl SignedScalarAccumulator for FrSmallScalarAccumulator { - type Element = Fr; - - #[inline(always)] - fn add(&mut self, value: Fr) { - self.add_to_pos(value); - } - - #[inline(always)] - fn fmadd_u64(&mut self, value: Fr, scalar: u64) { - self.fmadd_magnitude_to_pos(value, scalar); - } - - #[inline(always)] - fn fmadd_i64(&mut self, value: Fr, scalar: i64) { - let magnitude = scalar.unsigned_abs(); - if scalar >= 0 { - self.fmadd_magnitude_to_pos(value, magnitude); - } else { - self.fmadd_magnitude_to_neg(value, magnitude); - } - } - - #[inline(always)] - fn reduce(self) -> Fr { - if self.pos >= self.neg { - Fr::from_inner(bn254_ops::reduce_nplus1( - self.pos.sub_trunc::<5, 5>(&self.neg), - )) - } else { - -Fr::from_inner(bn254_ops::reduce_nplus1( - self.neg.sub_trunc::<5, 5>(&self.pos), - )) - } - } -} - -#[cfg(test)] -mod tests { - use crate::FromPrimitiveInt; - - use super::*; - - #[test] - fn signed_small_scalar_accumulator_reduces_mixed_terms() { - let mut acc = FrSmallScalarAccumulator::default(); - acc.fmadd_u64(Fr::from_u64(3), 16); - acc.fmadd_i64(Fr::from_u64(5), -7); - acc.add(Fr::from_u64(11)); - - assert_eq!(acc.reduce(), Fr::from_u64(24)); - } - - #[test] - fn signed_small_scalar_accumulator_handles_negative_result() { - let mut acc = FrSmallScalarAccumulator::default(); - acc.fmadd_i64(Fr::from_u64(9), -13); - acc.fmadd_u64(Fr::from_u64(2), 7); - - assert_eq!(acc.reduce(), -Fr::from_u64(103)); - } -} diff --git a/crates/jolt-field/src/ext/fp_ext2.rs b/crates/jolt-field/src/ext/fp_ext2.rs index f7660e3241..f0e11cd186 100644 --- a/crates/jolt-field/src/ext/fp_ext2.rs +++ b/crates/jolt-field/src/ext/fp_ext2.rs @@ -28,7 +28,7 @@ impl FpExt2Config for TwoNr { #[inline] fn mul_non_residue(x: A, _from_base: B) -> A where - A: ExtensionCoeff, + A: Copy + Add + Sub + Mul, B: FnOnce(F) -> A, { x + x @@ -50,7 +50,7 @@ pub trait FpExt2Config { #[inline] fn mul_non_residue(x: A, from_base: B) -> A where - A: ExtensionCoeff, + A: Copy + Add + Sub + Mul, B: FnOnce(F) -> A, { if Self::IS_NEG_ONE { @@ -316,8 +316,6 @@ impl> FromPrimitiveInt for F } } -impl> BalancedDigitLookup for FpExt2 {} - /// Identity-stub `HasUnreducedOps` for `FpExt2` variants without a dedicated /// delayed-reduction accumulator. `ProductAccum = Self`, so every multiply /// reduces immediately. Same pattern as `FpExt4` and diff --git a/crates/jolt-field/src/ext/fp_ext4.rs b/crates/jolt-field/src/ext/fp_ext4.rs index 98754a27fd..03c04c82e8 100644 --- a/crates/jolt-field/src/ext/fp_ext4.rs +++ b/crates/jolt-field/src/ext/fp_ext4.rs @@ -12,10 +12,9 @@ use super::*; /// Multiply ring-subfield quartic coefficient arrays in `[1, e1, e2, e3]` basis. #[inline] -pub(crate) fn fp_ext4_mul_coeffs(a: [A; 4], b: [A; 4]) -> [A; 4] +pub(crate) fn fp_ext4_mul_coeffs(a: [A; 4], b: [A; 4]) -> [A; 4] where - F: FieldCore, - A: ExtensionCoeff, + A: Copy + Add + Sub + Mul, { let [a0, a1, a2, a3] = a; let [b0, b1, b2, b3] = b; @@ -30,10 +29,9 @@ where /// Square ring-subfield quartic coefficient arrays in `[1, e1, e2, e3]` basis. #[inline] -pub(crate) fn fp_ext4_square_coeffs(a: [A; 4]) -> [A; 4] +pub(crate) fn fp_ext4_square_coeffs(a: [A; 4]) -> [A; 4] where - F: FieldCore, - A: ExtensionCoeff, + A: Copy + Add + Sub + Mul, { let [a0, a1, a2, a3] = a; let x0 = a0; @@ -96,13 +94,13 @@ pub trait FpExt4MulBackend: FieldCore { /// Multiply two ring-subfield coefficient arrays in `[1, e1, e2, e3]` basis. #[inline(always)] fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { - fp_ext4_mul_coeffs::(a, b) + fp_ext4_mul_coeffs::(a, b) } /// Square one ring-subfield coefficient array in `[1, e1, e2, e3]` basis. #[inline(always)] fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { - fp_ext4_square_coeffs::(a) + fp_ext4_square_coeffs::(a) } } @@ -512,8 +510,6 @@ impl FromPrimitiveInt for FpExt4 { } } -impl BalancedDigitLookup for FpExt4 {} - impl HasUnreducedOps for FpExt4> { type MulU64Accum = Self; type ProductAccum = FpExt4Fp32ProductAccum; diff --git a/crates/jolt-field/src/ext/fp_ext8.rs b/crates/jolt-field/src/ext/fp_ext8.rs index 659ec79689..82cf577e19 100644 --- a/crates/jolt-field/src/ext/fp_ext8.rs +++ b/crates/jolt-field/src/ext/fp_ext8.rs @@ -430,8 +430,6 @@ impl FromPrimitiveInt for FpExt8 { } } -impl BalancedDigitLookup for FpExt8 {} - macro_rules! impl_fp_ext8_unreduced_identity { ($base:ident<$p:ident: $pty:ty>) => { impl HasUnreducedOps for FpExt8<$base<$p>> { diff --git a/crates/jolt-field/src/ext/mod.rs b/crates/jolt-field/src/ext/mod.rs index a17a87321b..303a75d400 100644 --- a/crates/jolt-field/src/ext/mod.rs +++ b/crates/jolt-field/src/ext/mod.rs @@ -19,7 +19,7 @@ use super::unreduced::{ HasOptimizedFold, HasUnreducedOps, }; use crate::{ - BalancedDigitLookup, CanonicalField, FieldCore, FromPrimitiveInt, HalvingField, Invertible, + CanonicalField, FieldCore, FromPrimitiveInt, HalvingField, Invertible, MulBaseUnreduced, RandomSampling, RingCore, }; use rand_core::RngCore; @@ -31,15 +31,3 @@ pub use fp_ext4::{FpExt4, FpExt4MulBackend}; pub(crate) use fp_ext8::{fp_ext8_mul_schedule, fp_ext8_square_schedule}; pub use fp_ext8::{FpExt8, FpExt8MulBackend}; -/// Arithmetic shape shared by scalar and packed extension coefficients. -pub trait ExtensionCoeff: - Copy + Add + Sub + Mul -{ -} - -impl ExtensionCoeff for A -where - F: FieldCore, - A: Copy + Add + Sub + Mul, -{ -} diff --git a/crates/jolt-field/src/fft.rs b/crates/jolt-field/src/fft.rs deleted file mode 100644 index 1a87a54a84..0000000000 --- a/crates/jolt-field/src/fft.rs +++ /dev/null @@ -1,1098 +0,0 @@ -//! Mixed-radix FFT over prime fields with smooth-order multiplicative subgroups. - -#![expect( - clippy::expect_used, - reason = "constructed FFT plans establish the indexed root and factor invariants" -)] -//! -//! # Setting -//! -//! The protocol primes [`crate::Prime128Offset2355`] -//! and [`crate::Prime128OffsetA7F7`] are pseudo-Mersenne, so -//! `p − 1` is not a power of two; each is instead chosen so it carries -//! a large **smooth factor** — a product of small primes: -//! -//! - `p = 2^128 − 2355`: smooth order `14_700 = 2² · 3 · 5² · 7²` -//! - `p = 2^128 − 2^32 + 22_537`: smooth order `17_496 = 2³ · 3⁷` -//! -//! FFT domain sizes are divisors of that smooth order; there is no -//! power-of-two NTT to fall back on. The primary use case is FFT-based -//! Reed-Solomon encoding inside the protocol. -//! -//! # Algorithm -//! -//! Iterative Cooley-Tukey decimation-in-time (DIT). For a domain size -//! `n = f_0 · f_1 · … · f_{s−1}` (each `f_i` a small prime, ≤ 7 in -//! practice), the size-`n` DFT factors recursively into size-`f_i` -//! DFTs combined with twiddle multiplications. The iterative form -//! permutes the input by mixed-radix digit reversal up front, then -//! sweeps `s` stages bottom-up, running radix-`f_i` butterflies in -//! place at each stage. -//! -//! # Optimizations -//! -//! All precomputed once when a `SmoothDomain` is built and reused -//! across transforms: -//! -//! - **Stage plan** (`factorize`, `digit_reversal_permutation`): the -//! per-stage radices and digit-reversal permutation are fixed at -//! construction. -//! - **Twiddle tables** (`StageData::twiddle_table`): the `ω^{jk}` -//! factor the DIT formula uses at every butterfly becomes a table -//! lookup plus a small power-up loop, replacing a `field_pow` call -//! per butterfly. -//! - **Ping-pong buffers** (`FftWorkspace`): the two length-`n` working -//! buffers are pre-allocated, so the transform itself is allocation-free. -//! - **Low-multiplication radix kernels** -//! (`FftWorkspace::butterfly_stages`): the size-`r` DFT inside each -//! butterfly is hand-tuned per radix, taking the multiplication -//! count from the naive `r²` down to `1, 2, 6, 18` for -//! `r ∈ {2, 3, 5, 7}` (radix 3 uses `1 + ω + ω² = 0`; radix 5 / 7 -//! use Karatsuba on the conjugate-pair-symmetrized inputs, with -//! the constants precomputed in `StageData::winograd`). -//! - **Smooth-subgroup-derived roots** -//! ([`primitive_nth_root`](crate::fft::primitive_nth_root)): -//! `ω_n` is one exponentiation of the field's compile-time -//! `SmoothFftField::SMOOTH_OMEGA` literal — no runtime base scan. -//! -//! # Coset evaluation and RS-extend -//! -//! Reed-Solomon extension interpolates a polynomial through the `k` -//! known evaluations (one inverse FFT) then evaluates it on -//! `blowup − 1` cosets of the base subgroup. Each coset evaluation is -//! a coset FFT — pre-twist `c_i ← c_i · s^i` then run a plain forward -//! FFT — see [`SmoothDomain::coset_forward`](crate::fft::SmoothDomain::coset_forward) -//! and [`SmoothDomain::rs_extend_batch`](crate::fft::SmoothDomain::rs_extend_batch). - -use crate::{FieldCore, FromPrimitiveInt, Invertible, SmoothFftField}; - -/// Compute `base^exp` by repeated squaring. -#[inline] -pub fn field_pow(base: F, mut exp: u64) -> F { - let mut result = F::one(); - let mut b = base; - while exp > 0 { - if exp & 1 == 1 { - result *= b; - } - b *= b; - exp >>= 1; - } - result -} - -/// Compute `base^exp` for u128 exponents. Test-only scanner helper. -#[cfg(test)] -pub(crate) fn field_pow_u128(base: F, mut exp: u128) -> F { - let mut result = F::one(); - let mut b = base; - while exp > 0 { - if exp & 1 == 1 { - result *= b; - } - b *= b; - exp >>= 1; - } - result -} - -/// Smallest prime factor of `n` (returns `n` itself if `n ≤ 1` or is prime). -fn smallest_prime_factor(n: usize) -> usize { - if n <= 1 { - return n; - } - for &p in &[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31] { - if n.is_multiple_of(p) { - return p; - } - } - let mut i = 37; - while i * i <= n { - if n.is_multiple_of(i) { - return i; - } - i += 2; - } - n -} - -/// Prime factorization of `n` (with multiplicity), in non-decreasing order. -/// -/// The mixed-radix decomposition uses each prime factor as the radix of -/// one stage, so e.g. `n = 14_700 = 2² · 3 · 5² · 7²` becomes seven -/// stages `[2, 2, 3, 5, 5, 7, 7]`. -fn factorize(mut n: usize) -> Vec { - let mut factors = Vec::new(); - while n > 1 { - let p = smallest_prime_factor(n); - factors.push(p); - n /= p; - } - factors -} - -/// Mixed-radix digit-reversal permutation, the analogue of bit-reversal -/// for power-of-two FFTs. -/// -/// For `n = f_0 · f_1 · … · f_{s−1}`, write index `k` in mixed-radix -/// form `k = d_0 + d_1 · f_0 + d_2 · f_0 · f_1 + …`; then `perm[k]` is -/// the index whose digits are the reverse sequence. Permuting the -/// input by this table aligns the recursion's base cases at -/// consecutive indices, which is what lets the bottom-up DIT sweep -/// work in place. -fn digit_reversal_permutation(n: usize, factors: &[usize]) -> Vec { - let s = factors.len(); - let mut perm = vec![0usize; n]; - for (k, perm_k) in perm.iter_mut().enumerate() { - let mut digits = vec![0usize; s]; - let mut tmp = k; - for (digit, &f) in digits.iter_mut().zip(factors.iter()) { - *digit = tmp % f; - tmp /= f; - } - let mut rev = 0usize; - for (&f, &d) in factors.iter().zip(digits.iter()) { - rev = rev * f + d; - } - *perm_k = rev; - } - perm -} - -/// Read-only data the inner butterfly loop consults at every stage. -/// -/// One per prime factor of `n`. Stage `i` combines `r` blocks of size -/// `block` (the product of all earlier stages' radices) into a single -/// block of size `block · r`. -struct StageData { - /// Radix of this stage — one of `{2, 3, 5, 7}` in practice. - r: usize, - /// Block size feeding this stage; output blocks are `block · r`. - block: usize, - /// `omega_r_pow[q] = ω_r^q` for `q ∈ 0..r`. Length fixed at 8 for - /// stack storage; entries past `r` stay `1` and are unused. - omega_r_pow: [F; 8], - /// `twiddle_table[j] = ω_{block · r}^j`, indexed by lane within a - /// group. The DIT butterfly's `ω^{jk}` factor decomposes as - /// `(twiddle_table[j])^k`, with `tw^k` materialized on the fly. - twiddle_table: Vec, - /// Precomputed Winograd constants for the low-mul radix-5 / 7 - /// kernels (empty for other radices). Layout: - /// - /// - `r == 5`: `[α/2, β/2, γ/2, δ/2, (α+β)/2, (γ+δ)/2]` where - /// `α = ω+ω⁴`, `β = ω²+ω³`, `γ = ω−ω⁴`, `δ = ω²−ω³`. - /// - `r == 7`: 9 `α_{jk}` then 9 `β_{jk}`, row-major over - /// `(j, k) ∈ {1, 2, 3}²`, with - /// `α_{jk} = (ω^{jk} + ω^{−jk})/2` and - /// `β_{jk} = (ω^{jk} − ω^{−jk})/2`. - /// - /// The `/2` is folded into the stored values so the kernel doesn't - /// halve at every butterfly. - winograd: Vec, -} - -/// Build the per-stage tables consumed by the iterative FFT. -/// -/// Walks `factors` in reverse so the resulting `Vec` is -/// already in bottom-up sweep order. For each stage: -/// -/// - `omega_new_block = omega^{n/(block · r)}` is the principal -/// `(block · r)`-th root of unity used to fill the twiddle table. -/// - `omega_r = omega_new_block^block` is the principal `r`-th root -/// used inside the size-`r` butterfly. -/// -/// Called twice per domain — once with `omega = ω_n` for the forward -/// transform and once with `omega = ω_n^{−1}` for the inverse. -fn precompute_stages( - omega: F, - n: usize, - factors: &[usize], -) -> Vec> { - let mut stages = Vec::with_capacity(factors.len()); - let mut block = 1usize; - - for &r in factors.iter().rev() { - debug_assert!(r <= 8, "radix {r} exceeds omega_r_pow capacity (max 8)"); - let new_block = block * r; - let omega_new_block = field_pow(omega, (n / new_block) as u64); - let omega_r = field_pow(omega_new_block, block as u64); - - let mut omega_r_pow = [F::one(); 8]; - for q in 1..r { - omega_r_pow[q] = omega_r_pow[q - 1] * omega_r; - } - - let mut twiddle_table = Vec::with_capacity(block); - let mut tw = F::one(); - for _ in 0..block { - twiddle_table.push(tw); - tw *= omega_new_block; - } - - let winograd = winograd_consts_for_radix::(r, &omega_r_pow); - - stages.push(StageData { - r, - block, - omega_r_pow, - twiddle_table, - winograd, - }); - - block = new_block; - } - stages -} - -/// Precompute the Winograd constants consumed by the radix-5 / 7 -/// kernels. Returns an empty vector for other radices. See the -/// doc-comment on `StageData::winograd` for the exact layout. -fn winograd_consts_for_radix( - r: usize, - omega_r_pow: &[F; 8], -) -> Vec { - match r { - 5 => { - let w1 = omega_r_pow[1]; - let w2 = omega_r_pow[2]; - let w3 = omega_r_pow[3]; - let w4 = omega_r_pow[4]; - let half = F::from_u64(2) - .inverse() - .expect("2 is invertible in a non-binary field"); - // α = ω+ω⁴, β = ω²+ω³, γ = ω−ω⁴, δ = ω²−ω³. - let alpha_half = (w1 + w4) * half; - let beta_half = (w2 + w3) * half; - let gamma_half = (w1 - w4) * half; - let delta_half = (w2 - w3) * half; - // (α+β)/2 = (Σ_{q=1..4} ω^q)/2 = (-1)/2 since 1+ω+…+ω⁴ = 0. - let ab_half = alpha_half + beta_half; - let gd_half = gamma_half + delta_half; - vec![ - alpha_half, beta_half, gamma_half, delta_half, ab_half, gd_half, - ] - } - 7 => { - // ω^{−q} mod 7 = ω^{7−q}; map negative exponents through - // `rem_euclid` so we can index the precomputed `omega_r_pow` - // table for both signs. - let w = omega_r_pow; - let pow = |q: isize| -> F { - let qq = q.rem_euclid(7) as usize; - w[qq] - }; - let half = F::from_u64(2) - .inverse() - .expect("2 is invertible in a non-binary field"); - let mut out = Vec::with_capacity(18); - // α_{jk} = (ω^{jk} + ω^{−jk})/2, row-major in (j, k). - for j in 1..=3 { - for k in 1..=3 { - let jk = (j * k) as isize; - out.push((pow(jk) + pow(-jk)) * half); - } - } - // β_{jk} = (ω^{jk} − ω^{−jk})/2, row-major in (j, k). - for j in 1..=3 { - for k in 1..=3 { - let jk = (j * k) as isize; - out.push((pow(jk) - pow(-jk)) * half); - } - } - out - } - _ => Vec::new(), - } -} - -/// Pre-allocated ping-pong buffers for an iterative mixed-radix FFT. -/// -/// `buf_a` is updated in place across all stages and holds the result -/// on return. `buf_b` is a scratch slot callers can pre-fill (see -/// `execute_from_b`); reused across the inverse and forward passes -/// inside `rs_extend_batch`. -struct FftWorkspace { - n: usize, - buf_a: Vec, - buf_b: Vec, -} - -impl FftWorkspace { - fn new(n: usize) -> Self { - Self { - n, - buf_a: vec![F::zero(); n], - buf_b: vec![F::zero(); n], - } - } - - /// Run an iterative mixed-radix Cooley-Tukey DIT FFT on `input`: - /// digit-reverse into `buf_a`, then sweep `stages` bottom-up - /// running radix-`r` butterflies in place. Returns a view into - /// `buf_a`. - fn execute(&mut self, input: &[F], stages: &[StageData], digit_rev: &[usize]) -> &[F] { - let n = self.n; - debug_assert_eq!(input.len(), n); - - for (i, &rev_i) in digit_rev.iter().enumerate() { - self.buf_a[rev_i] = input[i]; - } - - self.butterfly_stages(stages); - &self.buf_a[..n] - } - - /// Like [`Self::execute`], but reads the input from a `buf_b` the - /// caller has already populated. Used by `coset_forward` to avoid - /// an extra allocation for the twisted coefficient vector. - fn execute_from_b(&mut self, stages: &[StageData], digit_rev: &[usize]) -> &[F] { - let n = self.n; - - for (i, &rev_i) in digit_rev.iter().enumerate() { - self.buf_a[rev_i] = self.buf_b[i]; - } - - self.butterfly_stages(stages); - &self.buf_a[..n] - } - - /// Bottom-up FFT sweep. For each stage, the outer loop walks - /// independent groups of `block · r` consecutive entries; the - /// middle loop runs the `block` parallel butterflies inside one - /// group; each butterfly does a twiddle phase (scale lane `k` by - /// `twiddle_table[j]^k`) followed by a size-`r` DFT specialized - /// per radix. - fn butterfly_stages(&mut self, stages: &[StageData]) { - let n = self.n; - for stage in stages { - let r = stage.r; - let block = stage.block; - let new_block = block * r; - let omega_r_pow = &stage.omega_r_pow; - let twiddle_table = &stage.twiddle_table; - - for group_start in (0..n).step_by(new_block) { - for (j, tw_entry) in twiddle_table.iter().enumerate() { - let base = group_start + j; - - // Gather the `r` lanes of this butterfly into a - // stack array (cap of 8 is debug-asserted in - // `precompute_stages`). - let mut x = [F::zero(); 8]; - for (ki, xi) in x[..r].iter_mut().enumerate() { - *xi = self.buf_a[base + ki * block]; - } - - if j > 0 { - // Twiddle phase: scale lane k by tw^k. The - // unrolled per-radix sequences below share - // tw², tw³, … across lanes; the generic loop - // covers radices we don't have a tuned kernel - // for. Skipped entirely when j == 0 (tw = 1). - let tw = *tw_entry; - let tw2 = tw * tw; - match r { - 2 => { - x[1] *= tw; - } - 3 => { - x[1] *= tw; - x[2] *= tw2; - } - 5 => { - let tw3 = tw2 * tw; - let tw4 = tw2 * tw2; - x[1] *= tw; - x[2] *= tw2; - x[3] *= tw3; - x[4] *= tw4; - } - 7 => { - let tw3 = tw2 * tw; - let tw4 = tw2 * tw2; - let tw5 = tw4 * tw; - let tw6 = tw3 * tw3; - x[1] *= tw; - x[2] *= tw2; - x[3] *= tw3; - x[4] *= tw4; - x[5] *= tw5; - x[6] *= tw6; - } - _ => { - let mut tw_k = tw; - for xi in &mut x[1..r] { - *xi *= tw_k; - tw_k *= tw; - } - } - } - } - - // DFT phase: hand-tuned size-r kernel per radix. - match r { - 2 => { - self.buf_a[base] = x[0] + x[1]; - self.buf_a[base + block] = x[0] - x[1]; - } - 3 => { - // 2-mul DFT_3 from 1 + ω + ω² = 0: - // S = x₁ + x₂, T = ω·x₁ + ω²·x₂ - // y₀ = x₀ + S, y₁ = x₀ + T, y₂ = x₀ − S − T - let w1 = omega_r_pow[1]; - let w2 = omega_r_pow[2]; - let s = x[1] + x[2]; - let t = x[1] * w1 + x[2] * w2; - self.buf_a[base] = x[0] + s; - self.buf_a[base + block] = x[0] + t; - self.buf_a[base + 2 * block] = x[0] - s - t; - } - 5 => { - // 6-mul DFT_5 via Karatsuba on the - // (A, B) = x_j ± x_{5−j} pairs. Constants - // come from winograd_consts_for_radix(5): - // [α/2, β/2, γ/2, δ/2, (α+β)/2, (γ+δ)/2] - let cc = &stage.winograd; - debug_assert_eq!(cc.len(), 6); - let a_h = cc[0]; - let b_h = cc[1]; - let g_h = cc[2]; - let d_h = cc[3]; - let ab_h = cc[4]; - let gd_h = cc[5]; - - let a = x[1] + x[4]; - let b = x[2] + x[3]; - let c = x[1] - x[4]; - let d = x[2] - x[3]; - - // P-block (cosine, Karatsuba k₁+k₂+k₃): - // P₁ = A·α/2 + B·β/2, P₂ = A·β/2 + B·α/2 - let k1 = a * a_h; - let k2 = b * b_h; - let k3 = (a + b) * ab_h; - let p1 = k1 + k2; - let p2 = k3 - k1 - k2; - - // Q-block (sine, complex-mul Karatsuba): - // Q₁ = C·γ/2 + D·δ/2, Q₂ = C·δ/2 − D·γ/2 - let m1 = c * g_h; - let m2 = d * d_h; - let m3 = (c - d) * gd_h; - let q1 = m1 + m2; - let q2 = m3 - m1 + m2; - - self.buf_a[base] = x[0] + a + b; - self.buf_a[base + block] = x[0] + p1 + q1; - self.buf_a[base + 2 * block] = x[0] + p2 + q2; - self.buf_a[base + 3 * block] = x[0] + p2 - q2; - self.buf_a[base + 4 * block] = x[0] + p1 - q1; - } - 7 => { - // 18-mul DFT_7. Same conjugate-pair idea - // as DFT_5: pair x_j with x_{7−j} into - // A_j = x_j + x_{7−j} (symmetric) and - // B_j = x_j − x_{7−j} (antisymmetric), so - // - // x_j·ω^{jk} + x_{7−j}·ω^{−jk} - // = A_j · α_{jk} + B_j · β_{jk} - // - // with α_{jk}, β_{jk} (already including - // the /2) precomputed in `winograd`. - // Outputs y₄, y₅, y₆ recover by flipping - // the β sign. - let cc = &stage.winograd; - debug_assert_eq!(cc.len(), 18); - - let a1 = x[1] + x[6]; - let a2 = x[2] + x[5]; - let a3 = x[3] + x[4]; - let b1 = x[1] - x[6]; - let b2 = x[2] - x[5]; - let b3 = x[3] - x[4]; - - // α table at offset (j-1)*3 + (k-1). - let s1 = a1 * cc[0] + a2 * cc[3] + a3 * cc[6]; // k = 1 - let s2 = a1 * cc[1] + a2 * cc[4] + a3 * cc[7]; // k = 2 - let s3 = a1 * cc[2] + a2 * cc[5] + a3 * cc[8]; // k = 3 - - // β table at offset 9 + (j-1)*3 + (k-1). - let t1 = b1 * cc[9] + b2 * cc[12] + b3 * cc[15]; - let t2 = b1 * cc[10] + b2 * cc[13] + b3 * cc[16]; - let t3 = b1 * cc[11] + b2 * cc[14] + b3 * cc[17]; - - self.buf_a[base] = x[0] + a1 + a2 + a3; - self.buf_a[base + block] = x[0] + s1 + t1; - self.buf_a[base + 2 * block] = x[0] + s2 + t2; - self.buf_a[base + 3 * block] = x[0] + s3 + t3; - self.buf_a[base + 4 * block] = x[0] + s3 - t3; - self.buf_a[base + 5 * block] = x[0] + s2 - t2; - self.buf_a[base + 6 * block] = x[0] + s1 - t1; - } - _ => { - // Naive O(r²) fallback. - for (q, &wq) in omega_r_pow[..r].iter().enumerate() { - let mut val = x[0]; - let mut w = wq; - for &xp in &x[1..r] { - val += xp * w; - w *= wq; - } - self.buf_a[base + q * block] = val; - } - } - } - } - } - } - } -} - -/// Mixed-radix FFT domain backed by a smooth-order multiplicative subgroup. -/// -/// Holds the immutable state for a fixed-size FFT (roots of unity, -/// digit-reversal permutation, per-stage twiddle tables for both -/// directions). Build once with [`SmoothDomain::new`] and reuse across -/// transforms; `Sync`-safe since all fields are read-only after -/// construction. -pub struct SmoothDomain { - /// Number of points in the FFT domain. - pub n: usize, - /// Primitive `n`-th root of unity that generates the domain. - pub omega: F, - /// `n⁻¹`, applied to normalize the inverse transform. - n_inv: F, - /// Mixed-radix digit-reversal permutation, length `n`. - digit_rev: Vec, - /// Per-stage tables for the forward transform (twiddles in `ω`). - fwd_stages: Vec>, - /// Per-stage tables for the inverse transform (twiddles in `ω⁻¹`). - inv_stages: Vec>, -} - -impl SmoothDomain { - /// Build a domain of size `n` from a primitive `n`-th root of - /// unity. Precomputes the digit-reversal permutation and per-stage - /// tables for both forward and inverse transforms. - /// - /// # Panics - /// If `omega` is zero or `n` is not invertible in the field. - pub fn new(omega: F, n: usize) -> Self { - debug_assert_primitive_nth_root(omega, n); - let omega_inv = omega.inverse().expect("omega must be nonzero"); - let n_inv = F::from_u64(n as u64) - .inverse() - .expect("n must be invertible in field"); - let factors = factorize(n); - let digit_rev = digit_reversal_permutation(n, &factors); - let fwd_stages = precompute_stages(omega, n, &factors); - let inv_stages = precompute_stages(omega_inv, n, &factors); - Self { - n, - omega, - n_inv, - digit_rev, - fwd_stages, - inv_stages, - } - } - - /// Forward DFT: `Y[k] = Σ_{j=0}^{n-1} x[j] · ω^{jk}`. - /// - /// # Panics - /// If `input.len() != n`. - pub fn forward(&self, input: &[F]) -> Vec { - assert_eq!(input.len(), self.n); - let mut ws = FftWorkspace::new(self.n); - ws.execute(input, &self.fwd_stages, &self.digit_rev) - .to_vec() - } - - /// Inverse DFT: `x[j] = (1/n) · Σ_{k=0}^{n-1} Y[k] · ω^{-jk}`. - /// - /// # Panics - /// If `input.len() != n`. - pub fn inverse(&self, input: &[F]) -> Vec { - assert_eq!(input.len(), self.n); - let mut ws: FftWorkspace = FftWorkspace::new(self.n); - let mut result = ws - .execute(input, &self.inv_stages, &self.digit_rev) - .to_vec(); - for v in &mut result { - *v *= self.n_inv; - } - result - } - - /// Evaluate a polynomial at the shifted coset - /// `{shift · ω^i | i = 0, …, n−1}`. - /// - /// Reduces to a plain forward DFT on twisted coefficients via - /// - /// `P(shift · ω^i) = Σ_j (c_j · shift^j) · ω^{ij}`, - /// - /// so we pre-twist `c_j ← c_j · shift^j` into `buf_b` - /// (zero-padding any unused tail) and forward-FFT from there. - /// - /// # Panics - /// If `coeffs.len() > n`. - pub fn coset_forward(&self, coeffs: &[F], shift: F) -> Vec { - assert!(coeffs.len() <= self.n); - let mut ws: FftWorkspace = FftWorkspace::new(self.n); - let buf = &mut ws.buf_b[..self.n]; - let mut tw = F::one(); - for (i, &c) in coeffs.iter().enumerate() { - buf[i] = c * tw; - tw *= shift; - } - for v in &mut buf[coeffs.len()..] { - *v = F::zero(); - } - ws.execute_from_b(&self.fwd_stages, &self.digit_rev) - .to_vec() - } - - /// Reed-Solomon-extend `evals` from the base subgroup - /// `K = {ω_K^i}` (with `ω_K = ω_n^{blowup}`, `k = self.n`) to the - /// `blowup − 1` non-trivial cosets of `K` inside the larger - /// size-`(k · blowup)` subgroup. - /// - /// One inverse FFT recovers the polynomial through `evals`, then - /// `blowup − 1` coset forward FFTs (shifts `ω_n^j` for - /// `j = 1, …, blowup − 1`) evaluate it on each extension coset. - /// Returns `k · (blowup − 1)` values, coset-major; the original - /// evaluations on `K` are not re-emitted. All transforms share a - /// single workspace. - /// - /// # Panics - /// If `evals.len() != n`. - pub fn rs_extend_batch(&self, evals: &[F], omega_n: F, blowup: usize) -> Vec { - let k = self.n; - assert_eq!(evals.len(), k); - - let mut ws: FftWorkspace = FftWorkspace::new(self.n); - - let mut coeffs = ws - .execute(evals, &self.inv_stages, &self.digit_rev) - .to_vec(); - for v in &mut coeffs { - *v *= self.n_inv; - } - - let mut extension = Vec::with_capacity(k * (blowup - 1)); - for j in 1..blowup { - let shift = field_pow(omega_n, j as u64); - let buf = &mut ws.buf_b[..k]; - let mut tw = F::one(); - for (i, &c) in coeffs.iter().enumerate() { - buf[i] = c * tw; - tw *= shift; - } - let result = ws.execute_from_b(&self.fwd_stages, &self.digit_rev); - extension.extend_from_slice(result); - } - extension - } -} - -/// Primitive `n`-th root of unity in `F`, derived from -/// [`SmoothFftField::SMOOTH_OMEGA`] as -/// `omega_n = SMOOTH_OMEGA ^ (SMOOTH_SUBGROUP_ORDER / n)`. Requires -/// `n | SMOOTH_SUBGROUP_ORDER`. -/// -/// # Panics -/// If `n` does not divide [`SmoothFftField::SMOOTH_SUBGROUP_ORDER`], or -/// if `SMOOTH_OMEGA` is not in canonical form. -pub fn primitive_nth_root(n: usize) -> F { - assert!(n > 0, "n must be positive"); - assert_eq!( - F::SMOOTH_SUBGROUP_ORDER % n, - 0, - "n={n} must divide SMOOTH_SUBGROUP_ORDER={}", - F::SMOOTH_SUBGROUP_ORDER - ); - // Checked construction so a literal `≥ p` panics rather than - // being silently reduced. - let omega = F::from_canonical_u128_checked(F::SMOOTH_OMEGA) - .expect("SMOOTH_OMEGA must be < p (canonical form)"); - field_pow(omega, (F::SMOOTH_SUBGROUP_ORDER / n) as u64) -} - -/// Find a primitive `n`-th root of unity in `F` by scanning small -/// bases. -/// -/// Verifies primitivity against every distinct prime factor of `n`, so -/// it remains correct when a base lands in a strict subgroup (e.g. -/// `g = 2` is a quadratic residue modulo `Prime128OffsetA7F7`, so -/// `2^{(p−1)/n}` has order `n/2`). -/// -/// Used by per-prime tests as a drift guard on -/// [`SmoothFftField::SMOOTH_OMEGA`]; production code should call -/// [`primitive_nth_root`] instead. -/// -/// # Panics -/// If `n` does not divide `p − 1`, or if no base in `{2, 3, …, 47}` -/// yields a primitive `n`-th root. -#[cfg(test)] -#[expect( - clippy::panic, - reason = "test-only primitive-root scanner fails loudly" -)] -pub(crate) fn find_primitive_nth_root( - p_minus_1: u128, - n: usize, -) -> F { - assert_eq!( - p_minus_1 % (n as u128), - 0, - "n={n} must divide p-1={p_minus_1}" - ); - let exp = p_minus_1 / (n as u128); - let prime_factors = distinct_prime_factors(n); - - for &g in &[2u64, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47] { - let candidate = field_pow_u128(F::from_u64(g), exp); - if !is_primitive_nth_root(candidate, n, &prime_factors) { - continue; - } - return candidate; - } - panic!("no primitive {n}-th root of unity found in scanned bases"); -} - -/// Distinct prime factors of `n`, sorted ascending. -fn distinct_prime_factors(n: usize) -> Vec { - let mut factors = factorize(n); - factors.sort_unstable(); - factors.dedup(); - factors -} - -/// Test whether `omega` has exact multiplicative order `n`, given a slice -/// containing every distinct prime factor of `n`. -fn is_primitive_nth_root(omega: F, n: usize, distinct_factors: &[usize]) -> bool { - if field_pow(omega, n as u64) != F::one() { - return false; - } - distinct_factors - .iter() - .all(|&q| field_pow(omega, (n / q) as u64) != F::one()) -} - -/// Debug-only check that `omega` is a primitive `n`-th root of unity. -fn debug_assert_primitive_nth_root(omega: F, n: usize) { - if !cfg!(debug_assertions) { - return; - } - let factors = distinct_prime_factors(n); - assert!( - is_primitive_nth_root(omega, n, &factors), - "omega is not a primitive {n}-th root of unity (n's prime factors: {factors:?})" - ); -} - -/// Free-function wrapper around [`SmoothDomain::rs_extend_batch`]. -pub fn rs_extend_fft( - evals: &[F], - domain_k: &SmoothDomain, - omega_n: F, - blowup: usize, -) -> Vec { - domain_k.rs_extend_batch(evals, omega_n, blowup) -} - -#[cfg(test)] -mod test_support { - //! Prime-agnostic helpers shared by per-prime FFT parity tests. - //! - //! The two protocol primes (`Prime128Offset2355`, `Prime128OffsetA7F7`) - //! have different smooth-subgroup factorizations, but the parity - //! properties under test (FFT vs naive DFT, forward/inverse roundtrip, - //! RS-extend consistency) are identical. Factor them out here so the - //! per-prime modules carry only the size lattice that actually differs. - //! - //! All omegas come from [`super::primitive_nth_root`] (i.e. the - //! field's `SmoothFftField::SMOOTH_OMEGA`); the scanner - //! [`super::find_primitive_nth_root`] is only re-invoked by the - //! `smooth_omega_matches_search` per-prime tests as a drift guard - //! on the hardcoded literal. - use super::*; - use crate::FromPrimitiveInt; - use std::fmt::Debug; - use std::ops::{AddAssign, MulAssign}; - - pub(super) use super::find_primitive_nth_root; - - /// O(n^2) naive DFT, used as oracle for the iterative FFT under test. - fn naive_dft(input: &[F], omega: F) -> Vec { - let n = input.len(); - let mut out = vec![F::zero(); n]; - for (k, ok) in out.iter_mut().enumerate() { - for (j, &xj) in input.iter().enumerate() { - *ok += xj * field_pow(omega, (j * k) as u64); - } - } - out - } - - /// For each `n` in `sizes` that divides `F::SMOOTH_SUBGROUP_ORDER`, - /// assert the iterative FFT matches the naive DFT on a deterministic - /// input vector. Sizes that do not divide are silently skipped so - /// per-prime modules can share a single union of "interesting" sizes. - pub(super) fn assert_fft_matches_naive_dft(sizes: &[usize]) - where - F: SmoothFftField + FromPrimitiveInt + Invertible + Debug, - { - for &n in sizes { - if F::SMOOTH_SUBGROUP_ORDER % n != 0 { - continue; - } - let omega = primitive_nth_root::(n); - let input: Vec = (0..n).map(|i| F::from_u64((i + 1) as u64)).collect(); - let expected = naive_dft(&input, omega); - - let factors = factorize(n); - let digit_rev = digit_reversal_permutation(n, &factors); - let stages = precompute_stages(omega, n, &factors); - let mut ws: FftWorkspace = FftWorkspace::new(n); - let got = ws.execute(&input, &stages, &digit_rev).to_vec(); - assert_eq!(got, expected, "FFT mismatch for n={n}"); - } - } - - /// `forward(inverse(x)) == x` over a smooth domain of order `n`. - pub(super) fn assert_forward_inverse_roundtrip(n: usize) - where - F: SmoothFftField + FromPrimitiveInt + Invertible + Debug, - { - let omega = primitive_nth_root::(n); - let domain = SmoothDomain::new(omega, n); - let input: Vec = (0..n).map(|i| F::from_u64(i as u64 + 1)).collect(); - let transformed = domain.forward(&input); - let recovered = domain.inverse(&transformed); - assert_eq!(input, recovered); - } - - /// `rs_extend_fft` matches direct evaluation of the interpolating - /// polynomial on each of the `blowup - 1` extension cosets. - pub(super) fn assert_rs_extend_consistency(k: usize, blowup: usize) - where - F: SmoothFftField + FromPrimitiveInt + Invertible + Debug + AddAssign + MulAssign, - { - let n = k * blowup; - let omega_n = primitive_nth_root::(n); - let omega_k = field_pow(omega_n, blowup as u64); - let domain_k = SmoothDomain::new(omega_k, k); - - let evals: Vec = (0..k).map(|i| F::from_u64((i * 7 + 3) as u64)).collect(); - let coeffs = domain_k.inverse(&evals); - let extension = rs_extend_fft(&evals, &domain_k, omega_n, blowup); - assert_eq!(extension.len(), k * (blowup - 1)); - - for j in 1..blowup { - for i in 0..k { - let point = field_pow(omega_n, j as u64) * field_pow(omega_k, i as u64); - let mut expected = F::zero(); - let mut x_pow = F::one(); - for &c in &coeffs { - expected += c * x_pow; - x_pow *= point; - } - assert_eq!( - extension[(j - 1) * k + i], - expected, - "mismatch at coset {j}, position {i}" - ); - } - } - } -} - -#[cfg(test)] -mod prime_2355_tests { - //! `Prime128Offset2355` (`p = 2^128 - 2355`) has smooth multiplicative - //! subgroup of order `14_700 = 2^2 * 3 * 5^2 * 7^2`, drawing sizes from - //! the `{2, 3, 5, 7}` lattice. - use super::test_support::*; - use super::*; - use crate::Prime128Offset2355; - use crate::{CanonicalField, PseudoMersenneField}; - - type F = Prime128Offset2355; - - /// Drift guard: re-derive the primitive `SMOOTH_SUBGROUP_ORDER`-th - /// root of unity from a base scan and assert it equals the literal - /// declared in [`crate::prime::fp128`]. Also validates the - /// trait's structural invariant `SMOOTH_SUBGROUP_ORDER | (p − 1)`. - #[test] - fn smooth_omega_matches_search() { - let p_minus_1 = u128::MAX - F::MODULUS_OFFSET; - assert_eq!( - p_minus_1 % (F::SMOOTH_SUBGROUP_ORDER as u128), - 0, - "SMOOTH_SUBGROUP_ORDER must divide p − 1", - ); - let derived = find_primitive_nth_root::(p_minus_1, F::SMOOTH_SUBGROUP_ORDER); - let declared = - F::from_canonical_u128_checked(F::SMOOTH_OMEGA).expect("SMOOTH_OMEGA must be < p"); - assert_eq!( - derived, declared, - "SMOOTH_OMEGA literal has drifted from the scanner's primitive root" - ); - } - - #[test] - fn primitive_nth_root_has_correct_order_for_every_divisor() { - // Every `n | SMOOTH_SUBGROUP_ORDER` should yield a primitive - // n-th root via the trait derivation. - for &n in &[ - 2, 3, 4, 5, 6, 7, 10, 12, 14, 15, 20, 21, 25, 28, 30, 35, 42, 49, 50, 60, 70, 75, 84, - 98, 100, 105, 140, 147, 150, 175, 196, 210, 245, 294, 300, 350, 420, 490, 525, 588, - 700, 735, 980, 1050, 1225, 1470, 2100, 2450, 2940, 3675, 4900, 7350, 14700, - ] { - if F::SMOOTH_SUBGROUP_ORDER % n != 0 { - continue; - } - let omega = primitive_nth_root::(n); - let factors = distinct_prime_factors(n); - assert!( - is_primitive_nth_root(omega, n, &factors), - "primitive_nth_root failed primitivity check for n={n}" - ); - } - } - - #[test] - fn small_fft_matches_naive_dft() { - assert_fft_matches_naive_dft::(&[ - 2, 3, 4, 5, 6, 7, 10, 12, 14, 15, 20, 21, 25, 28, 42, 49, 50, - ]); - } - - #[test] - fn forward_inverse_roundtrip_300() { - assert_forward_inverse_roundtrip::(300); - } - - #[test] - fn forward_inverse_roundtrip_1470() { - assert_forward_inverse_roundtrip::(1470); - } - - #[test] - fn rs_extend_consistency() { - // k = 300 = 2^2 * 3 * 5^2, blowup = 7, so n = 2_100 | 14_700. - assert_rs_extend_consistency::(300, 7); - } -} - -#[cfg(test)] -mod prime_a7f7_tests { - //! `Prime128OffsetA7F7` (`p = 2^128 - 2^32 + 22537`) has smooth - //! multiplicative subgroup of order `2^3 * 3^7 = 17_496`, with a pure - //! radix-3 substructure of order `3^7 = 2_187`. Sizes are drawn from - //! the `{2, 3}` lattice instead of `{2, 3, 5, 7}`. - use super::test_support::*; - use super::*; - use crate::Prime128OffsetA7F7; - use crate::{CanonicalField, PseudoMersenneField}; - - type F = Prime128OffsetA7F7; - - /// Cross-implementation check: the radix-3 GPU NTT in - /// `gpu_bench/primeB_roots.hpp` bakes - /// `OMEGA_2187 = 2^((p_B − 1)/2187)` into a separate constant table. - /// The two implementations independently choose their generators (the - /// GPU uses `g = 2`, the Rust scanner uses the smallest base whose - /// `g^((p−1)/n)` reaches full order `n`), so the constants are not - /// expected to be *equal*; they are expected to be *primitive - /// 2187-th roots of unity in the same field*. We verify the GPU's - /// limb table is a valid primitive 2187-th root under the Rust - /// `Fp128` implementation, which is the meaningful invariant for - /// cross-impl correctness. - #[test] - fn gpu_omega_2187_is_primitive_in_rust_field() { - // Limbs from `gpu_bench/primeB_roots.hpp` OMEGA_2187, packed - // little-endian into a u128. - const GPU_OMEGA_2187: u128 = 0x44E6_6EEC_31E7_36A6_A030_9253_219B_CCCD; - let omega = F::from_canonical_u128_checked(GPU_OMEGA_2187) - .expect("GPU OMEGA_2187 must lie in [0, p_B)"); - let factors = distinct_prime_factors(2187); - assert!( - is_primitive_nth_root(omega, 2187, &factors), - "gpu_bench OMEGA_2187 is not a primitive 2187-th root under Rust Fp128", - ); - - // The GPU also bakes OMEGA_3 = OMEGA_2187^729 (a primitive cube - // root). Cross-check that limb table too. - const GPU_OMEGA_3: u128 = 0x66F1_B0EE_0E4A_40F7_0F69_0C7F_0F66_39DD; - let omega3 = - F::from_canonical_u128_checked(GPU_OMEGA_3).expect("GPU OMEGA_3 must lie in [0, p_B)"); - assert_eq!(field_pow(omega, 729), omega3, "OMEGA_3 != OMEGA_2187^729"); - assert!( - is_primitive_nth_root(omega3, 3, &distinct_prime_factors(3)), - "gpu_bench OMEGA_3 is not a primitive cube root", - ); - } - - /// Drift guard: see `prime_2355_tests::smooth_omega_matches_search`. - #[test] - fn smooth_omega_matches_search() { - let p_minus_1 = u128::MAX - F::MODULUS_OFFSET; - assert_eq!( - p_minus_1 % (F::SMOOTH_SUBGROUP_ORDER as u128), - 0, - "SMOOTH_SUBGROUP_ORDER must divide p − 1", - ); - let derived = find_primitive_nth_root::(p_minus_1, F::SMOOTH_SUBGROUP_ORDER); - let declared = - F::from_canonical_u128_checked(F::SMOOTH_OMEGA).expect("SMOOTH_OMEGA must be < p"); - assert_eq!( - derived, declared, - "SMOOTH_OMEGA literal has drifted from the scanner's primitive root" - ); - } - - #[test] - fn primitive_nth_root_has_correct_order_for_every_divisor() { - for &n in &[ - 2, 3, 6, 8, 9, 18, 24, 27, 54, 81, 162, 243, 486, 729, 1458, 2187, 4374, 8748, 17496, - ] { - if F::SMOOTH_SUBGROUP_ORDER % n != 0 { - continue; - } - let omega = primitive_nth_root::(n); - let factors = distinct_prime_factors(n); - assert!( - is_primitive_nth_root(omega, n, &factors), - "primitive_nth_root failed primitivity check for n={n}" - ); - } - } - - #[test] - fn small_fft_matches_naive_dft() { - assert_fft_matches_naive_dft::(&[2, 3, 6, 8, 9, 18, 24, 27, 54, 81, 162, 243, 486, 729]); - } - - #[test] - fn forward_inverse_roundtrip_243() { - assert_forward_inverse_roundtrip::(243); - } - - #[test] - fn forward_inverse_roundtrip_1458() { - assert_forward_inverse_roundtrip::(1458); - } - - #[test] - fn forward_inverse_roundtrip_2187() { - assert_forward_inverse_roundtrip::(2187); - } - - #[test] - fn rs_extend_consistency() { - // k = 243 (= 3^5), blowup = 9 (= 3^2), n = 3^7 = 2_187 | 17_496. - assert_rs_extend_consistency::(243, 9); - } -} diff --git a/crates/jolt-field/src/field.rs b/crates/jolt-field/src/field.rs index 362f4fcf52..6d0a74e92c 100644 --- a/crates/jolt-field/src/field.rs +++ b/crates/jolt-field/src/field.rs @@ -5,7 +5,7 @@ use std::ops::Mul; use crate::{ CanonicalBitLength, CanonicalBytes, CanonicalU64, FieldCore, FixedByteSize, FromPrimitiveInt, MulPow2, MulPrimitiveInt, RandomSampling, ReducingBytes, RingCore, TranscriptChallenge, - WithAccumulator, WithSignedProductAccumulator, WithSmallScalarAccumulator, + WithAccumulator, }; /// Prime field element abstraction used throughout Jolt. @@ -37,8 +37,6 @@ pub trait Field: + CanonicalU64 + RandomSampling + WithAccumulator - + WithSmallScalarAccumulator - + WithSignedProductAccumulator + MulPow2 + MulPrimitiveInt { diff --git a/crates/jolt-field/src/lib.rs b/crates/jolt-field/src/lib.rs index d2fb0d1985..355ca9b55c 100644 --- a/crates/jolt-field/src/lib.rs +++ b/crates/jolt-field/src/lib.rs @@ -28,8 +28,8 @@ //! # Solinas types (feature `solinas`) //! //! The Solinas backend provides optimized 32-, 64-, and 128-bit prime fields, -//! extension fields, packed NEON/AVX2/AVX-512 implementations, unreduced -//! accumulators, and smooth-domain FFT helpers. Akita adopts these types +//! extension fields, packed NEON/AVX2/AVX-512 implementations, and unreduced +//! accumulators. Akita adopts these types //! directly in its cutover to `jolt-field`. Until that cutover lands, the //! temporary `akita` feature retains the legacy adapter for the pre-cutover //! `akita-field` types; it is a bootstrap edge, not the target architecture, @@ -60,8 +60,6 @@ mod mul_primitive_int; mod random_sampling; mod reducing_bytes; mod ring_core; -mod signed_product_accumulator; -mod small_scalar_accumulator; #[cfg(feature = "solinas")] mod solinas_traits; mod transcript_challenge; @@ -86,16 +84,8 @@ pub use num_traits::{One, Zero}; pub use random_sampling::RandomSampling; pub use reducing_bytes::ReducingBytes; pub use ring_core::RingCore; -pub use signed_product_accumulator::{ - NaiveSignedProductAccumulator, SignedProductAccumulator, WithSignedProductAccumulator, -}; -pub use small_scalar_accumulator::{ - NaiveSignedScalarAccumulator, SignedScalarAccumulator, WithSmallScalarAccumulator, -}; #[cfg(feature = "solinas")] -pub use solinas_traits::{ - BalancedDigitLookup, CanonicalField, HalvingField, PseudoMersenneField, SmoothFftField, -}; +pub use solinas_traits::{balanced_digit_lut, CanonicalField, HalvingField, PseudoMersenneField}; pub use transcript_challenge::TranscriptChallenge; pub use with_accumulator::WithAccumulator; @@ -107,8 +97,6 @@ pub mod signed; #[cfg(feature = "solinas")] mod ext; #[cfg(feature = "solinas")] -pub mod fft; -#[cfg(feature = "solinas")] pub mod packed; #[cfg(feature = "solinas")] pub mod parallel; @@ -142,8 +130,4 @@ pub use arkworks::bn254::Fr; #[cfg(feature = "bn254")] pub use arkworks::bn254_fq::Fq; #[cfg(feature = "bn254")] -pub use arkworks::signed_product_accumulator::FrSignedProductAccumulator; -#[cfg(feature = "bn254")] -pub use arkworks::small_scalar_accumulator::FrSmallScalarAccumulator; -#[cfg(feature = "bn254")] pub use arkworks::wide_accumulator::WideAccumulator; diff --git a/crates/jolt-field/src/prime/fp128/core.rs b/crates/jolt-field/src/prime/fp128/core.rs index 00d69b79a2..340f60a1df 100644 --- a/crates/jolt-field/src/prime/fp128/core.rs +++ b/crates/jolt-field/src/prime/fp128/core.rs @@ -104,23 +104,4 @@ impl Fp128

{ )) } } - - /// Const-evaluable lookup table for balanced digits in `[-b/2, b/2)` - /// where `b = 2^log_basis`. Requires `log_basis <= 6`. - /// - /// # Panics - /// - /// Panics if `log_basis` is outside `1..=6`. - pub const fn digit_lut(log_basis: u32) -> [Self; 64] { - assert!(log_basis > 0 && log_basis <= 6); - let b = 1u32 << log_basis; - let half_b = (b / 2) as i64; - let mut lut = [Self(pack(0, 0)); 64]; - let mut i = 0u32; - while i < b { - lut[i as usize] = Self::from_i64_const(i as i64 - half_b); - i += 1; - } - lut - } } diff --git a/crates/jolt-field/src/prime/fp128/mod.rs b/crates/jolt-field/src/prime/fp128/mod.rs index 7c136221b8..12ba5ec978 100644 --- a/crates/jolt-field/src/prime/fp128/mod.rs +++ b/crates/jolt-field/src/prime/fp128/mod.rs @@ -37,7 +37,7 @@ use crate::{FromPrimitiveInt, Invertible, RandomSampling}; use rand_core::RngCore; use crate::{ - BalancedDigitLookup, CanonicalField, HalvingField, PseudoMersenneField, SmoothFftField, + CanonicalField, HalvingField, PseudoMersenneField, }; use super::util::{is_pow2_u64, log2_pow2_u64, mul64_wide}; diff --git a/crates/jolt-field/src/prime/fp128/primes.rs b/crates/jolt-field/src/prime/fp128/primes.rs index 240cf1e3c2..9f49d8084c 100644 --- a/crates/jolt-field/src/prime/fp128/primes.rs +++ b/crates/jolt-field/src/prime/fp128/primes.rs @@ -13,14 +13,6 @@ pub type Prime128Offset159 = Fp128<0xffffffffffffffffffffffffffffff61>; /// Factorization: `p − 1 = 2² · 3 · 5² · 7² · 701 · 2955365183 · 11173595356596918495491`. pub type Prime128Offset2355 = Fp128<0xfffffffffffffffffffffffffffff6cd>; -impl SmoothFftField for Prime128Offset2355 { - const SMOOTH_SUBGROUP_ORDER: usize = 14_700; - /// `2 ^ ((p − 1) / 14_700)` where `g = 2` is a primitive root of `p`. - /// Verified by `prime_2355_tests::smooth_omega_matches_search` in - /// `src/fft.rs`. - const SMOOTH_OMEGA: u128 = 0x2ecd_18d0_8238_2c0c_818c_c05f_446a_8075; -} - /// `p = 2^128 − 2^32 + 22537` (C = 2^32 − 22537 = 0xFFFFA7F7). /// /// Solinas-form prime sharing the same CPU reduction cost as @@ -36,14 +28,3 @@ impl SmoothFftField for Prime128Offset2355 { /// `1458 = 2 · 3^6`, `2187 = 3^7`, `4374 = 2 · 3^7`, `8748 = 2^2 · 3^7`, /// and the full `17 496 = 2^3 · 3^7`. pub type Prime128OffsetA7F7 = Fp128<0xffffffffffffffffffffffff00005809>; - -impl SmoothFftField for Prime128OffsetA7F7 { - const SMOOTH_SUBGROUP_ORDER: usize = 17_496; - /// `g ^ ((p − 1) / 17_496)` where `g` is the smallest primitive root - /// found by `find_primitive_nth_root` (note: `g = 2` is a quadratic - /// residue mod `p` and therefore *not* a primitive root, so the - /// scanner falls through to the next candidate). Verified by - /// `prime_a7f7_tests::smooth_omega_matches_search` in - /// `src/fft.rs`. - const SMOOTH_OMEGA: u128 = 0x4e9f_650b_7003_d201_9945_e1da_c47c_8b18; -} diff --git a/crates/jolt-field/src/prime/fp128/traits.rs b/crates/jolt-field/src/prime/fp128/traits.rs index 819b4a333e..b2b6cec90a 100644 --- a/crates/jolt-field/src/prime/fp128/traits.rs +++ b/crates/jolt-field/src/prime/fp128/traits.rs @@ -151,12 +151,6 @@ impl FromPrimitiveInt for Fp128

{ } } -impl BalancedDigitLookup for Fp128

{ - fn digit_lut(log_basis: u32) -> [Self; 64] { - Self::digit_lut(log_basis) - } -} - impl CanonicalField for Fp128

{ fn to_canonical_u128(self) -> u128 { to_u128(self.0) diff --git a/crates/jolt-field/src/prime/fp32.rs b/crates/jolt-field/src/prime/fp32.rs index ca486f5573..0e09fb5296 100644 --- a/crates/jolt-field/src/prime/fp32.rs +++ b/crates/jolt-field/src/prime/fp32.rs @@ -9,7 +9,7 @@ use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use crate::{FromPrimitiveInt, Invertible, RandomSampling}; use rand_core::RngCore; -use crate::{BalancedDigitLookup, CanonicalField, HalvingField, PseudoMersenneField}; +use crate::{CanonicalField, HalvingField, PseudoMersenneField}; /// Prime field element for primes `p = 2^k − c` stored as `u32`. /// @@ -421,8 +421,6 @@ impl FromPrimitiveInt for Fp32

{ } } -impl BalancedDigitLookup for Fp32

{} - impl CanonicalField for Fp32

{ fn to_canonical_u128(self) -> u128 { self.0 as u128 diff --git a/crates/jolt-field/src/prime/fp64.rs b/crates/jolt-field/src/prime/fp64.rs index 25a6d21d1e..e99a628791 100644 --- a/crates/jolt-field/src/prime/fp64.rs +++ b/crates/jolt-field/src/prime/fp64.rs @@ -9,7 +9,7 @@ use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use crate::{FromPrimitiveInt, Invertible, RandomSampling}; use rand_core::RngCore; -use crate::{BalancedDigitLookup, CanonicalField, HalvingField, PseudoMersenneField}; +use crate::{CanonicalField, HalvingField, PseudoMersenneField}; use super::util::{is_pow2_u64, log2_pow2_u64, mul64_wide}; @@ -509,8 +509,6 @@ impl FromPrimitiveInt for Fp64

{ } } -impl BalancedDigitLookup for Fp64

{} - impl CanonicalField for Fp64

{ fn to_canonical_u128(self) -> u128 { self.0 as u128 diff --git a/crates/jolt-field/src/prime/native_capability.rs b/crates/jolt-field/src/prime/native_capability.rs index 727ef866bd..efcabeda5f 100644 --- a/crates/jolt-field/src/prime/native_capability.rs +++ b/crates/jolt-field/src/prime/native_capability.rs @@ -12,8 +12,7 @@ use super::{Fp128, Fp32, Fp64}; use crate::{ CanonicalBitLength, CanonicalBytes, CanonicalField, CanonicalU64, Field, FieldCore, FixedByteSize, FixedBytes, FromPrimitiveInt, MulPow2, MulPrimitiveInt, NaiveAccumulator, - NaiveSignedProductAccumulator, NaiveSignedScalarAccumulator, ReducingBytes, - TranscriptChallenge, WithAccumulator, WithSignedProductAccumulator, WithSmallScalarAccumulator, + ReducingBytes, TranscriptChallenge, WithAccumulator, }; macro_rules! impl_prime_native_capability { @@ -76,14 +75,6 @@ macro_rules! impl_prime_native_capability { type Accumulator = NaiveAccumulator; } - impl WithSmallScalarAccumulator for $ty<$p> { - type SmallScalarAccumulator = NaiveSignedScalarAccumulator; - } - - impl WithSignedProductAccumulator for $ty<$p> { - type SignedProductAccumulator = NaiveSignedProductAccumulator; - } - impl Field for $ty<$p> {} }; } diff --git a/crates/jolt-field/src/signed_product_accumulator.rs b/crates/jolt-field/src/signed_product_accumulator.rs deleted file mode 100644 index 25447e6081..0000000000 --- a/crates/jolt-field/src/signed_product_accumulator.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::{signed::S256, AdditiveGroup, ReducingBytes, RingCore}; -use num_traits::Zero; - -pub trait SignedProductAccumulator: Default + Copy + Send + Sync { - type Element: AdditiveGroup + RingCore + ReducingBytes; - - fn fmadd_s256(&mut self, value: Self::Element, scalar: &S256); - - fn reduce(self) -> Self::Element; -} - -pub trait WithSignedProductAccumulator: AdditiveGroup { - type SignedProductAccumulator: SignedProductAccumulator; -} - -#[derive(Clone, Copy)] -pub struct NaiveSignedProductAccumulator(R); - -impl Default for NaiveSignedProductAccumulator { - #[inline] - fn default() -> Self { - Self(R::zero()) - } -} - -impl SignedProductAccumulator for NaiveSignedProductAccumulator -where - R: AdditiveGroup + RingCore + ReducingBytes, -{ - type Element = R; - - #[inline] - fn fmadd_s256(&mut self, value: R, scalar: &S256) { - if scalar.is_zero() { - return; - } - let mut bytes = [0u8; 32]; - for (index, limb) in scalar.magnitude_limbs().iter().copied().enumerate() { - bytes[index * 8..(index + 1) * 8].copy_from_slice(&limb.to_le_bytes()); - } - let magnitude = R::from_le_bytes_mod_order(&bytes); - let term = if scalar.is_positive { - value * magnitude - } else { - -(value * magnitude) - }; - self.0 += term; - } - - #[inline] - fn reduce(self) -> R { - self.0 - } -} diff --git a/crates/jolt-field/src/small_scalar_accumulator.rs b/crates/jolt-field/src/small_scalar_accumulator.rs deleted file mode 100644 index 9a6c3cc173..0000000000 --- a/crates/jolt-field/src/small_scalar_accumulator.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::{AdditiveGroup, MulPrimitiveInt}; - -pub trait SignedScalarAccumulator: Default + Copy + Send + Sync { - type Element: AdditiveGroup + MulPrimitiveInt; - - fn add(&mut self, value: Self::Element); - - fn fmadd_u64(&mut self, value: Self::Element, scalar: u64); - - fn fmadd_i64(&mut self, value: Self::Element, scalar: i64) { - let magnitude = scalar.unsigned_abs(); - if scalar >= 0 { - self.fmadd_u64(value, magnitude); - } else { - self.add(-value.mul_u64(magnitude)); - } - } - - fn reduce(self) -> Self::Element; -} - -pub trait WithSmallScalarAccumulator: AdditiveGroup { - type SmallScalarAccumulator: SignedScalarAccumulator; -} - -#[derive(Clone, Copy)] -pub struct NaiveSignedScalarAccumulator(R); - -impl Default for NaiveSignedScalarAccumulator { - #[inline] - fn default() -> Self { - Self(R::zero()) - } -} - -impl SignedScalarAccumulator - for NaiveSignedScalarAccumulator -{ - type Element = R; - - #[inline] - fn add(&mut self, value: R) { - self.0 += value; - } - - #[inline] - fn fmadd_u64(&mut self, value: R, scalar: u64) { - self.0 += value.mul_u64(scalar); - } - - #[inline] - fn fmadd_i64(&mut self, value: R, scalar: i64) { - self.0 += value.mul_i64(scalar); - } - - #[inline] - fn reduce(self) -> R { - self.0 - } -} diff --git a/crates/jolt-field/src/solinas_traits.rs b/crates/jolt-field/src/solinas_traits.rs index 4cd73ed2d5..e147273d13 100644 --- a/crates/jolt-field/src/solinas_traits.rs +++ b/crates/jolt-field/src/solinas_traits.rs @@ -28,21 +28,18 @@ pub trait HalvingField: FieldCore { } } -/// Balanced signed-digit lookup support for small power-of-two bases. -pub trait BalancedDigitLookup: FromPrimitiveInt + Zero + Copy { - /// Builds the balanced digit table for `1 <= log_basis <= 6`. - fn digit_lut(log_basis: u32) -> [Self; 64] { - debug_assert!(log_basis > 0 && log_basis <= 6); - let basis = 1usize << log_basis; - let half_basis = (basis >> 1) as i64; - std::array::from_fn(|i| { - if i < basis { - Self::from_i64(i as i64 - half_basis) - } else { - Self::zero() - } - }) - } +/// Builds the balanced signed-digit table for `1 <= log_basis <= 6`. +pub fn balanced_digit_lut(log_basis: u32) -> [F; 64] { + debug_assert!(log_basis > 0 && log_basis <= 6); + let basis = 1usize << log_basis; + let half_basis = (basis >> 1) as i64; + std::array::from_fn(|i| { + if i < basis { + F::from_i64(i as i64 - half_basis) + } else { + F::zero() + } + }) } /// Metadata for a pseudo-Mersenne modulus `2^k - c`. @@ -53,12 +50,3 @@ pub trait PseudoMersenneField: CanonicalField { /// Offset `c` in `2^k - c`. const MODULUS_OFFSET: u128; } - -/// Field with a precomputed primitive root of a supported smooth subgroup. -pub trait SmoothFftField: CanonicalField + PseudoMersenneField { - /// Order of the supported smooth multiplicative subgroup. - const SMOOTH_SUBGROUP_ORDER: usize; - - /// Canonical representation of its primitive root. - const SMOOTH_OMEGA: u128; -} From 029881fd421095de03e0b41a447b14a146d4946c Mon Sep 17 00:00:00 2001 From: acentelles Date: Tue, 21 Jul 2026 19:42:35 -0400 Subject: [PATCH 06/38] refactor(field): merge accumulator traits into one Accumulator (spec phase 2) AdditiveAccumulator and RingAccumulator were only ever implemented and consumed together (WideAccumulator, NaiveAccumulator); merge them into a single Accumulator trait carrying add/merge/reduce plus the fmadd family. WithAccumulator moves into accumulator.rs and now supertraits RingCore + FromPrimitiveInt, which its associated type already implied. Because WithAccumulator now guarantees Accumulator by declaration, the 18 'Accumulator: RingAccumulator' where-clauses across jolt-crypto, jolt-blindfold, and jolt-verifier are redundant and removed. --- crates/jolt-blindfold/src/prove.rs | 8 +-- crates/jolt-blindfold/src/verify.rs | 7 +-- crates/jolt-crypto/src/commitment.rs | 11 +--- .../fuzz_targets/wide_accumulator_fmadd.rs | 2 +- .../fuzz_targets/wide_accumulator_merge.rs | 2 +- crates/jolt-field/src/accumulator.rs | 52 +++++++++---------- .../src/arkworks/wide_accumulator.rs | 10 ++-- crates/jolt-field/src/lib.rs | 6 +-- .../jolt-field/src/prime/native_capability.rs | 2 +- crates/jolt-field/src/with_accumulator.rs | 7 --- crates/jolt-field/tests/coverage.rs | 4 +- crates/jolt-verifier/src/verifier.rs | 3 +- 12 files changed, 40 insertions(+), 74 deletions(-) delete mode 100644 crates/jolt-field/src/with_accumulator.rs diff --git a/crates/jolt-blindfold/src/prove.rs b/crates/jolt-blindfold/src/prove.rs index 991b118701..e2715b7139 100644 --- a/crates/jolt-blindfold/src/prove.rs +++ b/crates/jolt-blindfold/src/prove.rs @@ -1,5 +1,5 @@ use jolt_crypto::{HomomorphicCommitment, VectorCommitment, VectorCommitmentOpening}; -use jolt_field::{Field, RingAccumulator, WithAccumulator}; +use jolt_field::Field; use jolt_poly::{BindingOrder, EqPolynomial, Polynomial, UnivariatePoly}; use jolt_r1cs::{ConstraintMatrices, ConstraintMatrixEvalError, SparseRow}; use jolt_sumcheck::{CompressedSumcheckProof, SUMCHECK_ROUND_TRANSCRIPT_LABEL}; @@ -127,8 +127,6 @@ where entry_point: &[F], name: &'static str, ) -> Result<(VectorCommitmentOpening, F), ProverError> - where - ::Accumulator: RingAccumulator, { open_committed_rows::(setup, rows, blindings, row_point, entry_point, name) } @@ -166,7 +164,6 @@ where VC::Output: HomomorphicCommitment + AppendToTranscript, T: Transcript, R: RngCore, - ::Accumulator: RingAccumulator, { let mut row_committer = DirectBlindFoldRowCommitter; prove_with_row_committer::( @@ -194,7 +191,6 @@ where T: Transcript, R: RngCore, C: BlindFoldRowCommitter, - ::Accumulator: RingAccumulator, { validate_witness::(setup, protocol, witness)?; @@ -696,7 +692,6 @@ fn open_committed_rows( where F: Field, VC: VectorCommitment, - ::Accumulator: RingAccumulator, { let row_count = basis_len_from_point_len("row point", row_point.len())?; ensure_len(name, row_count, rows.len())?; @@ -1038,7 +1033,6 @@ where F: Field, VC: VectorCommitment, C: BlindFoldRowCommitter, - ::Accumulator: RingAccumulator, { let row_vars = log2_power_of_two("witness row count", witness_rows.len())?; let entry_vars = log2_power_of_two("witness row length", witness_rows[0].len())?; diff --git a/crates/jolt-blindfold/src/verify.rs b/crates/jolt-blindfold/src/verify.rs index d6a81baf51..45c5a6f889 100644 --- a/crates/jolt-blindfold/src/verify.rs +++ b/crates/jolt-blindfold/src/verify.rs @@ -1,5 +1,5 @@ use jolt_crypto::{HomomorphicCommitment, VectorCommitment, VectorCommitmentOpening}; -use jolt_field::{Field, FieldCore, RingAccumulator, WithAccumulator}; +use jolt_field::{Field, FieldCore}; use jolt_poly::EqPolynomial; use jolt_r1cs::{ConstraintMatrices, MatrixColumnContributions}; use jolt_sumcheck::{BooleanHypercube, SumcheckClaim, SUMCHECK_ROUND_TRANSCRIPT_LABEL}; @@ -18,7 +18,6 @@ impl BlindFoldProtocol where F: Field + AppendToTranscript, Com: Copy + HomomorphicCommitment + AppendToTranscript, - ::Accumulator: RingAccumulator, { pub fn verify( &self, @@ -126,7 +125,6 @@ impl BlindFoldProtocol where F: Field + AppendToTranscript, Com: Copy + HomomorphicCommitment + AppendToTranscript, - ::Accumulator: RingAccumulator, { fn verify_outer_folded_r1cs( &self, @@ -247,7 +245,6 @@ impl BlindFoldProtocol where F: Field + AppendToTranscript, Com: Copy + HomomorphicCommitment + AppendToTranscript, - ::Accumulator: RingAccumulator, { fn verify_folded_eval_witness_bindings( &self, @@ -365,7 +362,6 @@ impl WitnessCoordinate { F: Field, VC: VectorCommitment, VC::Output: Copy + HomomorphicCommitment, - ::Accumulator: RingAccumulator, { let witness_row_count = folded.witness_row_commitments.len(); if witness_row_count == 0 || !witness_row_count.is_power_of_two() { @@ -400,7 +396,6 @@ impl BlindFoldProtocol where F: Field + AppendToTranscript, Com: Copy + HomomorphicCommitment + AppendToTranscript, - ::Accumulator: RingAccumulator, { fn verify_inner_folded_r1cs( &self, diff --git a/crates/jolt-crypto/src/commitment.rs b/crates/jolt-crypto/src/commitment.rs index 2727b5fd67..4b09d9e865 100644 --- a/crates/jolt-crypto/src/commitment.rs +++ b/crates/jolt-crypto/src/commitment.rs @@ -3,7 +3,7 @@ use std::{ fmt::{self, Debug}, }; -use jolt_field::{AdditiveAccumulator, Field, RingAccumulator, WithAccumulator}; +use jolt_field::{Accumulator, Field, WithAccumulator}; use jolt_poly::EqPolynomial; use jolt_transcript::AppendToTranscript; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -70,8 +70,6 @@ pub trait VectorCommitment: row_point: &[Self::Field], entry_point: &[Self::Field], ) -> Result<(VectorCommitmentOpening, Self::Field), VectorOpeningError> - where - ::Accumulator: RingAccumulator, { let row_count = point_len_to_basis_len(row_point.len())?; validate_row_len(row_len, entry_point.len())?; @@ -117,7 +115,6 @@ pub trait VectorCommitment: ) -> Result where Self::Output: HomomorphicCommitment, - ::Accumulator: RingAccumulator, { let row_count = point_len_to_basis_len(row_point.len())?; if row_commitments.len() != row_count { @@ -280,8 +277,6 @@ fn combine_rows( row_weights: &[F], max_len: usize, ) -> Vec -where - ::Accumulator: RingAccumulator, { let mut combined_vector = vec![F::zero(); row_len]; @@ -322,8 +317,6 @@ fn combine_rows( row_weights: &[F], _max_len: usize, ) -> Vec -where - ::Accumulator: RingAccumulator, { let mut combined_vector = vec![F::zero(); row_len]; @@ -341,8 +334,6 @@ where } fn inner_product(lhs: &[F], rhs: &[F]) -> F -where - ::Accumulator: RingAccumulator, { #[cfg(feature = "parallel")] { diff --git a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs index 84c149a022..04f7be779f 100644 --- a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs +++ b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs @@ -1,5 +1,5 @@ #![no_main] -use jolt_field::{AdditiveAccumulator, Fr, ReducingBytes, RingAccumulator, WideAccumulator}; +use jolt_field::{Accumulator, Fr, ReducingBytes, WideAccumulator}; use libfuzzer_sys::fuzz_target; use num_traits::Zero; diff --git a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs index d82049deea..faef8c486d 100644 --- a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs +++ b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs @@ -1,5 +1,5 @@ #![no_main] -use jolt_field::{AdditiveAccumulator, Fr, ReducingBytes, RingAccumulator, WideAccumulator}; +use jolt_field::{Accumulator, Fr, ReducingBytes, WideAccumulator}; use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { diff --git a/crates/jolt-field/src/accumulator.rs b/crates/jolt-field/src/accumulator.rs index 74143bdb55..59ca235d9a 100644 --- a/crates/jolt-field/src/accumulator.rs +++ b/crates/jolt-field/src/accumulator.rs @@ -1,7 +1,7 @@ //! Deferred-reduction accumulators. //! //! In sumcheck inner loops, many products are summed before the final result -//! is needed. [`RingAccumulator`] lets implementations defer modular reduction +//! is needed. [`Accumulator`] lets implementations defer modular reduction //! by accumulating in wider integer types, reducing once at the end. This //! amortizes the expensive reduction across hundreds of multiply-add steps. //! @@ -9,31 +9,16 @@ //! - `WideAccumulator` (BN254, in `arkworks/`) — 9-limb wide integer accumulator //! that defers Montgomery reduction. -use crate::{AdditiveGroup, FromPrimitiveInt, RingCore}; +use crate::{FromPrimitiveInt, RingCore}; use num_traits::One; -/// Accumulates additive values with potentially deferred reduction. -pub trait AdditiveAccumulator: Default + Copy + Send + Sync { - /// The element type this accumulator reduces to. - type Element: AdditiveGroup; - - /// Adds one element into the accumulator. - fn add(&mut self, value: Self::Element); - - /// Merge another accumulator's partial sum into this one. - fn merge(&mut self, other: Self); - - /// Finalize: reduce the accumulated value to an element. - fn reduce(self) -> Self::Element; -} - -/// Accumulates products with potentially deferred modular reduction. +/// Accumulates sums and products with potentially deferred modular reduction. /// /// The hot loop pattern `acc += a * b` repeated hundreds of times per output /// slot dominates the CPU prover. Standard field arithmetic reduces mod p /// after every multiply and every add. Implementations for specific fields /// (e.g., BN254 Fr) can instead accumulate unreduced wide products and -/// reduce once at the end via [`AdditiveAccumulator::reduce`]. +/// reduce once at the end via [`reduce`](Self::reduce). /// /// # Invariants /// @@ -42,10 +27,19 @@ pub trait AdditiveAccumulator: Default + Copy + Send + Sync { /// partial result (used for parallel reduction). /// - [`reduce`](Self::reduce) must return the field element equal to the /// accumulated sum of products. -pub trait RingAccumulator: AdditiveAccumulator -where - Self::Element: RingCore + FromPrimitiveInt, -{ +pub trait Accumulator: Default + Copy + Send + Sync { + /// The element type this accumulator reduces to. + type Element: RingCore + FromPrimitiveInt; + + /// Adds one element into the accumulator. + fn add(&mut self, value: Self::Element); + + /// Merge another accumulator's partial sum into this one. + fn merge(&mut self, other: Self); + + /// Finalize: reduce the accumulated value to an element. + fn reduce(self) -> Self::Element; + /// Fused multiply-add: `self += a * b` without intermediate reduction. fn fmadd(&mut self, a: Self::Element, b: Self::Element); @@ -79,9 +73,15 @@ where } } +/// Associates a redundant accumulator representation with an element type. +pub trait WithAccumulator: RingCore + FromPrimitiveInt { + /// Accumulator type. + type Accumulator: Accumulator; +} + /// Naive accumulator using standard field arithmetic. /// -/// Every [`fmadd`](RingAccumulator::fmadd) performs a full modular multiply +/// Every [`fmadd`](Accumulator::fmadd) performs a full modular multiply /// and add. Used as a fallback for fields without wide-integer optimization. #[derive(Clone, Copy)] pub struct NaiveAccumulator(R); @@ -93,7 +93,7 @@ impl Default for NaiveAccumulator { } } -impl AdditiveAccumulator for NaiveAccumulator { +impl Accumulator for NaiveAccumulator { type Element = R; #[inline] @@ -110,9 +110,7 @@ impl AdditiveAccumulator for NaiveAccumulator fn reduce(self) -> R { self.0 } -} -impl RingAccumulator for NaiveAccumulator { #[inline] fn fmadd(&mut self, a: R, b: R) { self.0 += a * b; diff --git a/crates/jolt-field/src/arkworks/wide_accumulator.rs b/crates/jolt-field/src/arkworks/wide_accumulator.rs index f5c9fba8ad..1ccede1f02 100644 --- a/crates/jolt-field/src/arkworks/wide_accumulator.rs +++ b/crates/jolt-field/src/arkworks/wide_accumulator.rs @@ -9,7 +9,7 @@ //! accumulated into eight positional `u128` slots. Carry headroom in each //! slot lets the hot loop avoid carry propagation until reduction. -use crate::accumulator::{AdditiveAccumulator, RingAccumulator}; +use crate::accumulator::Accumulator; use crate::arkworks::bn254::Fr; use ark_ff::BigInt; @@ -19,7 +19,7 @@ use super::bn254_ops; /// /// Stores the running sum of Montgomery-form products in positional `u128` /// slots. Converting to a field element requires one carry propagation pass -/// and one Montgomery reduction via [`AdditiveAccumulator::reduce`]. +/// and one Montgomery reduction via [`Accumulator::reduce`]. #[derive(Clone, Copy)] pub struct WideAccumulator { slots: [u128; 8], @@ -32,7 +32,7 @@ impl Default for WideAccumulator { } } -impl AdditiveAccumulator for WideAccumulator { +impl Accumulator for WideAccumulator { type Element = Fr; #[inline(always)] @@ -52,9 +52,7 @@ impl AdditiveAccumulator for WideAccumulator { // Montgomery reduction divides product terms by R. Fr::from_inner(bn254_ops::from_montgomery_reduce(self.normalize())) } -} -impl RingAccumulator for WideAccumulator { #[inline(always)] fn fmadd(&mut self, a: Fr, b: Fr) { let a = a.inner_limbs(); @@ -87,7 +85,7 @@ impl WideAccumulator { #[cfg(test)] mod tests { use super::*; - use crate::{AdditiveAccumulator, FromPrimitiveInt}; + use crate::{Accumulator, FromPrimitiveInt}; #[test] fn single_fmadd() { diff --git a/crates/jolt-field/src/lib.rs b/crates/jolt-field/src/lib.rs index 355ca9b55c..f16850eeab 100644 --- a/crates/jolt-field/src/lib.rs +++ b/crates/jolt-field/src/lib.rs @@ -15,7 +15,7 @@ //! # Core traits //! //! - [`Field`] — Jolt compatibility umbrella -//! - [`RingAccumulator`] — deferred-reduction fused multiply-add +//! - [`Accumulator`] — deferred-reduction fused multiply-add //! - [`OptimizedMul`] — fast-path short-circuits for zero/one //! - [`MontgomeryConstants`] — Montgomery form constants for GPU backends //! @@ -63,9 +63,8 @@ mod ring_core; #[cfg(feature = "solinas")] mod solinas_traits; mod transcript_challenge; -mod with_accumulator; -pub use accumulator::{AdditiveAccumulator, NaiveAccumulator, RingAccumulator}; +pub use accumulator::{Accumulator, NaiveAccumulator, WithAccumulator}; pub use additive_group::AdditiveGroup; pub use canonical_bit_length::CanonicalBitLength; pub use canonical_bytes::CanonicalBytes; @@ -87,7 +86,6 @@ pub use ring_core::RingCore; #[cfg(feature = "solinas")] pub use solinas_traits::{balanced_digit_lut, CanonicalField, HalvingField, PseudoMersenneField}; pub use transcript_challenge::TranscriptChallenge; -pub use with_accumulator::WithAccumulator; pub mod limbs; pub use limbs::Limbs; diff --git a/crates/jolt-field/src/prime/native_capability.rs b/crates/jolt-field/src/prime/native_capability.rs index efcabeda5f..ed20da8180 100644 --- a/crates/jolt-field/src/prime/native_capability.rs +++ b/crates/jolt-field/src/prime/native_capability.rs @@ -101,7 +101,7 @@ mod tests { //! `--no-default-features --features solinas` as well as combined builds. use super::*; use crate::Prime128Offset275; - use crate::{AdditiveAccumulator, RingAccumulator}; + use crate::Accumulator; /// Asserts the full canonical byte round-trip on the native traits. fn assert_native_byte_roundtrip(value: F, expected: [u8; N]) diff --git a/crates/jolt-field/src/with_accumulator.rs b/crates/jolt-field/src/with_accumulator.rs deleted file mode 100644 index 65c50cc90d..0000000000 --- a/crates/jolt-field/src/with_accumulator.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::{AdditiveAccumulator, AdditiveGroup}; - -/// Associates an additive redundant accumulator with an element type. -pub trait WithAccumulator: AdditiveGroup { - /// Accumulator type. - type Accumulator: AdditiveAccumulator; -} diff --git a/crates/jolt-field/tests/coverage.rs b/crates/jolt-field/tests/coverage.rs index e083e2696b..81cc5b6c59 100644 --- a/crates/jolt-field/tests/coverage.rs +++ b/crates/jolt-field/tests/coverage.rs @@ -8,8 +8,8 @@ use ark_std::test_rng; use jolt_field::signed::*; use jolt_field::{ - AdditiveAccumulator, FixedBytes, Fr, FromPrimitiveInt, Limbs, MulPow2, NaiveAccumulator, - OptimizedMul, RandomSampling, RingAccumulator, + Accumulator, FixedBytes, Fr, FromPrimitiveInt, Limbs, MulPow2, NaiveAccumulator, + OptimizedMul, RandomSampling, }; use num_traits::{One, Zero}; diff --git a/crates/jolt-verifier/src/verifier.rs b/crates/jolt-verifier/src/verifier.rs index 608dfb6de1..04f5663219 100644 --- a/crates/jolt-verifier/src/verifier.rs +++ b/crates/jolt-verifier/src/verifier.rs @@ -5,7 +5,7 @@ use jolt_claims::protocols::jolt::{ JoltOneHotConfig, JoltReadWriteConfig, JoltRelationId, TracePolynomialOrder, }; use jolt_crypto::{HomomorphicCommitment, VectorCommitment}; -use jolt_field::{Field, RingAccumulator, WithAccumulator}; +use jolt_field::Field; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, ZkOpeningScheme}; use jolt_program::preprocess::{compute_max_ram_k, compute_min_ram_k}; use jolt_sumcheck::SumcheckProof; @@ -50,7 +50,6 @@ where VC: VectorCommitment, VC::Output: Copy + HomomorphicCommitment + AppendToTranscript, T: Transcript, - ::Accumulator: RingAccumulator, { let PreStage1VerifierState { checked, From 26b26e8b60a93c6a00d9a779ea47868f5e116f70 Mon Sep 17 00:00:00 2001 From: acentelles Date: Tue, 21 Jul 2026 22:17:51 -0400 Subject: [PATCH 07/38] refactor(field): consolidate root traits, adopt serde+bincode wire format (spec phase 3) Merge the fine-grained capability traits into cohesive ones: - FieldCore absorbs Invertible (inverse, inv_or_zero) and RandomSampling - FromPrimitiveInt gains a RingCore supertrait and absorbs the MulPow2 and MulPrimitiveInt default-method helpers - new CanonicalRepr (the Fiat-Shamir transcript surface) replaces CanonicalBytes, ReducingBytes, FixedByteSize, FixedBytes, CanonicalU64, CanonicalBitLength, and TranscriptChallenge; per-type challenge derivations (Fr's masked 125-bit path) move over verbatim - the crate root shrinks from 15 one-trait micro-files to algebra.rs, canonical.rs, accumulator.rs, and field.rs Wire serialization of the Solinas types is now serde + bincode, mirroring Fr's existing canonical [u8; N] pattern: prime fields encode as exactly NUM_BYTES bytes with checked canonical deserialization, extension fields as [F; K]. Fiat-Shamir bytes stay on CanonicalRepr's explicit encoding, never bincode. New serde_roundtrip tests pin per-element sizes, single length prefixes on vectors, and rejection of non-canonical encodings, so proof size cannot grow. The mersenne61_compat and Gf2 compatibility tests keep passing with merged bounds and no arkworks dependency. --- Cargo.lock | 1 + crates/jolt-akita/src/adapters.rs | 2 +- crates/jolt-akita/src/scheme.rs | 2 +- crates/jolt-blindfold/src/prove.rs | 3 +- crates/jolt-blindfold/tests/support/mod.rs | 6 +- .../geometry/claim_reductions/bytecode.rs | 2 +- .../geometry/claim_reductions/precommitted.rs | 2 +- .../src/protocols/jolt/geometry/dimensions.rs | 2 +- crates/jolt-crypto/benches/crypto.rs | 2 +- .../fuzz/fuzz_targets/group_arith.rs | 6 +- .../fuzz/fuzz_targets/pedersen_commit.rs | 6 +- crates/jolt-crypto/src/commitment.rs | 12 +- crates/jolt-crypto/tests/coverage.rs | 2 +- crates/jolt-crypto/tests/group_laws.rs | 2 +- crates/jolt-crypto/tests/pairing.rs | 2 +- crates/jolt-crypto/tests/pedersen.rs | 2 +- crates/jolt-dory/benches/dory.rs | 16 +- .../fuzz/fuzz_targets/verify_tampered.rs | 2 +- crates/jolt-dory/src/scheme.rs | 6 +- crates/jolt-dory/src/streaming.rs | 14 +- crates/jolt-dory/src/types.rs | 8 +- crates/jolt-dory/tests/commit_open_verify.rs | 36 +-- crates/jolt-field/Cargo.toml | 1 + crates/jolt-field/benches/field_arith.rs | 22 +- .../benches/solinas_field_arith/arithmetic.rs | 11 +- .../benches/solinas_field_arith/kernel.rs | 4 +- .../benches/solinas_field_arith/parallel.rs | 12 +- .../fuzz/fuzz_targets/field_arith.rs | 6 +- .../fuzz/fuzz_targets/from_bytes.rs | 10 +- .../fuzz/fuzz_targets/solinas_field_arith.rs | 2 +- .../fuzz_targets/wide_accumulator_fmadd.rs | 6 +- .../fuzz_targets/wide_accumulator_merge.rs | 6 +- crates/jolt-field/src/additive_group.rs | 20 -- crates/jolt-field/src/akita.rs | 49 +--- crates/jolt-field/src/algebra.rs | 217 ++++++++++++++++++ crates/jolt-field/src/arkworks/bn254.rs | 91 +++----- crates/jolt-field/src/arkworks/bn254_fq.rs | 48 ++-- crates/jolt-field/src/canonical.rs | 59 +++++ crates/jolt-field/src/canonical_bit_length.rs | 7 - crates/jolt-field/src/canonical_bytes.rs | 13 -- crates/jolt-field/src/canonical_u64.rs | 5 - crates/jolt-field/src/ext/fp_ext2.rs | 29 ++- crates/jolt-field/src/ext/fp_ext4.rs | 31 +-- crates/jolt-field/src/ext/fp_ext8.rs | 26 ++- crates/jolt-field/src/ext/mod.rs | 4 +- crates/jolt-field/src/ext/native_algebra.rs | 5 +- crates/jolt-field/src/ext/tests.rs | 6 +- crates/jolt-field/src/field.rs | 83 +------ crates/jolt-field/src/field_core.rs | 4 - crates/jolt-field/src/fixed_byte_size.rs | 5 - crates/jolt-field/src/fixed_bytes.rs | 19 -- crates/jolt-field/src/from_primitive_int.rs | 46 ---- crates/jolt-field/src/invertible.rs | 13 -- crates/jolt-field/src/lib.rs | 45 +--- crates/jolt-field/src/mul_pow_2.rs | 17 -- crates/jolt-field/src/mul_primitive_int.rs | 28 --- crates/jolt-field/src/packed/avx2/mod.rs | 2 +- crates/jolt-field/src/packed/avx512/mod.rs | 2 +- crates/jolt-field/src/packed/ext/mod.rs | 8 +- crates/jolt-field/src/packed/ext/tests.rs | 2 +- crates/jolt-field/src/packed/mod.rs | 6 +- crates/jolt-field/src/packed/neon/fp32.rs | 2 +- crates/jolt-field/src/packed/neon/mod.rs | 2 +- crates/jolt-field/src/packed/tests.rs | 8 +- crates/jolt-field/src/prime/fp128/core.rs | 2 +- crates/jolt-field/src/prime/fp128/mod.rs | 6 +- crates/jolt-field/src/prime/fp128/tests.rs | 26 +-- crates/jolt-field/src/prime/fp128/traits.rs | 37 ++- crates/jolt-field/src/prime/fp32.rs | 39 ++-- crates/jolt-field/src/prime/fp64.rs | 43 ++-- crates/jolt-field/src/prime/native_algebra.rs | 5 +- .../jolt-field/src/prime/native_capability.rs | 78 ++----- crates/jolt-field/src/random_sampling.rs | 7 - crates/jolt-field/src/reducing_bytes.rs | 5 - crates/jolt-field/src/ring_core.rs | 52 ----- crates/jolt-field/src/transcript_challenge.rs | 12 - crates/jolt-field/src/unreduced/tests.rs | 40 ++-- .../tests/binary_field_core_compat.rs | 28 ++- crates/jolt-field/tests/coverage.rs | 24 +- crates/jolt-field/tests/field_operations.rs | 46 ++-- crates/jolt-field/tests/serde_roundtrip.rs | 114 +++++++++ crates/jolt-hyperkzg/benches/hyperkzg.rs | 2 +- crates/jolt-hyperkzg/src/scheme.rs | 2 +- .../jolt-hyperkzg/tests/commit_open_verify.rs | 2 +- crates/jolt-openings/tests/packing.rs | 2 +- crates/jolt-openings/tests/support/common.rs | 2 +- crates/jolt-openings/tests/support/packed.rs | 2 +- crates/jolt-poly/benches/poly_ops.rs | 2 +- .../fuzz/fuzz_targets/dense_poly_ops.rs | 6 +- crates/jolt-poly/src/dense.rs | 2 +- crates/jolt-poly/src/eq.rs | 2 +- crates/jolt-poly/src/eq_plus_one.rs | 2 +- crates/jolt-poly/src/lt.rs | 2 +- crates/jolt-poly/src/multilinear.rs | 2 +- crates/jolt-poly/src/one_hot.rs | 2 +- crates/jolt-poly/src/split_eq.rs | 2 +- crates/jolt-poly/tests/integration.rs | 2 +- .../src/constraints/field_constraints.rs | 2 +- crates/jolt-r1cs/src/key.rs | 2 +- .../fuzz/fuzz_targets/sumcheck_verifier.rs | 4 +- .../fuzz/fuzz_targets/valid_prefix_proof.rs | 4 +- crates/jolt-sumcheck/src/scalar.rs | 20 +- .../jolt-sumcheck/tests/mersenne61_compat.rs | 46 +--- crates/jolt-transcript/src/digest.rs | 10 +- crates/jolt-transcript/src/legacy.rs | 12 +- crates/jolt-verifier-derive/src/lib.rs | 2 +- .../tests/statistical_independence/zk.rs | 4 +- .../src/protocols/jolt_vm/field_inline/mod.rs | 4 +- jolt-eval/src/invariant/field_mul_scalar.rs | 2 +- .../src/invariant/transcript_symmetry.rs | 6 +- .../src/objective/performance/field_mul.rs | 2 +- tracer/src/instruction/field_inline.rs | 4 +- 112 files changed, 903 insertions(+), 939 deletions(-) delete mode 100644 crates/jolt-field/src/additive_group.rs create mode 100644 crates/jolt-field/src/algebra.rs create mode 100644 crates/jolt-field/src/canonical.rs delete mode 100644 crates/jolt-field/src/canonical_bit_length.rs delete mode 100644 crates/jolt-field/src/canonical_bytes.rs delete mode 100644 crates/jolt-field/src/canonical_u64.rs delete mode 100644 crates/jolt-field/src/field_core.rs delete mode 100644 crates/jolt-field/src/fixed_byte_size.rs delete mode 100644 crates/jolt-field/src/fixed_bytes.rs delete mode 100644 crates/jolt-field/src/from_primitive_int.rs delete mode 100644 crates/jolt-field/src/invertible.rs delete mode 100644 crates/jolt-field/src/mul_pow_2.rs delete mode 100644 crates/jolt-field/src/mul_primitive_int.rs delete mode 100644 crates/jolt-field/src/random_sampling.rs delete mode 100644 crates/jolt-field/src/reducing_bytes.rs delete mode 100644 crates/jolt-field/src/ring_core.rs delete mode 100644 crates/jolt-field/src/transcript_challenge.rs create mode 100644 crates/jolt-field/tests/serde_roundtrip.rs diff --git a/Cargo.lock b/Cargo.lock index 8375aca643..ef7a4204b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3272,6 +3272,7 @@ dependencies = [ "ark-ff 0.5.0 (git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout)", "ark-serialize 0.5.0 (git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout)", "ark-std 0.5.0", + "bincode 2.0.1", "criterion", "num-traits", "p3-baby-bear", diff --git a/crates/jolt-akita/src/adapters.rs b/crates/jolt-akita/src/adapters.rs index ef38a4d0a0..d9d8367eeb 100644 --- a/crates/jolt-akita/src/adapters.rs +++ b/crates/jolt-akita/src/adapters.rs @@ -9,7 +9,7 @@ use akita_types::{ AkitaCommitmentHint as AkitaBackendCommitmentHint, AkitaVerifierSetup as AkitaBackendVerifierSetup, RingCommitment as AkitaBackendRingCommitment, }; -use jolt_field::CanonicalBytes; +use jolt_field::CanonicalRepr; use jolt_openings::{OpeningsError, VerifierOpeningClaim}; use jolt_poly::{MultilinearPoly, OneHotIndexOrder, Polynomial}; use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript, U64Word}; diff --git a/crates/jolt-akita/src/scheme.rs b/crates/jolt-akita/src/scheme.rs index 6e59f9f6fa..f5fde29ec0 100644 --- a/crates/jolt-akita/src/scheme.rs +++ b/crates/jolt-akita/src/scheme.rs @@ -1,6 +1,6 @@ use akita_pcs::{CommitmentProver, ComputeBackendSetup, CpuBackend, RootPolyShape}; use jolt_crypto::Commitment; -use jolt_field::CanonicalBytes; +use jolt_field::CanonicalRepr; use jolt_openings::{ BatchOpeningScheme, CommitmentScheme, EvaluationClaim, OpeningsError, VerifierOpeningClaim, ZkBatchOpeningScheme, ZkOpeningScheme, diff --git a/crates/jolt-blindfold/src/prove.rs b/crates/jolt-blindfold/src/prove.rs index e2715b7139..58c1f90065 100644 --- a/crates/jolt-blindfold/src/prove.rs +++ b/crates/jolt-blindfold/src/prove.rs @@ -126,8 +126,7 @@ where row_point: &[F], entry_point: &[F], name: &'static str, - ) -> Result<(VectorCommitmentOpening, F), ProverError> - { + ) -> Result<(VectorCommitmentOpening, F), ProverError> { open_committed_rows::(setup, rows, blindings, row_point, entry_point, name) } } diff --git a/crates/jolt-blindfold/tests/support/mod.rs b/crates/jolt-blindfold/tests/support/mod.rs index 568a2cef5a..c955e11753 100644 --- a/crates/jolt-blindfold/tests/support/mod.rs +++ b/crates/jolt-blindfold/tests/support/mod.rs @@ -12,7 +12,7 @@ use jolt_claims::{challenge, constant, derived, opening, Expr}; use jolt_crypto::{ Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment, VectorCommitmentOpening, }; -use jolt_field::{FixedBytes, Fr, FromPrimitiveInt, Invertible}; +use jolt_field::{CanonicalRepr, FieldCore, Fr, FromPrimitiveInt}; use jolt_poly::{CompressedPoly, EqPolynomial}; use jolt_r1cs::{ClaimSourceTable, ConstraintMatrices, R1csBuilder}; use jolt_sumcheck::{ @@ -108,7 +108,7 @@ pub fn f(value: u64) -> F { pub fn rng_field(rng: &mut impl RngCore) -> F { let mut bytes = [0u8; 32]; rng.fill_bytes(&mut bytes); - F::from_bytes_array(&bytes) + ::from_le_bytes_mod_order(&bytes) } pub fn inverse(value: F) -> F { @@ -145,7 +145,7 @@ impl StatisticalProjection { } pub fn field_low_u64(value: F) -> u64 { - let bytes = value.to_bytes_array(); + let bytes = value.to_bytes_le_vec(); u64::from_le_bytes([ bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], ]) diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/bytecode.rs b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/bytecode.rs index 1884b32fc3..e1b4462414 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/bytecode.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/bytecode.rs @@ -541,7 +541,7 @@ mod tests { use super::super::super::bytecode::{read_raf_public_values, BytecodeReadRafEvaluationInputs}; use super::*; use crate::protocols::jolt::JoltPolynomialId; - use jolt_field::{Fr, FromPrimitiveInt, Invertible}; + use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use jolt_lookup_tables::InstructionLookupTable; use jolt_riscv::{ instructions::Noop, Flags, InterleavedBitsMarker, JoltInstruction, JoltInstructionKind, diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/precommitted.rs b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/precommitted.rs index 9ebb7534b8..2206e2048d 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/precommitted.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/precommitted.rs @@ -494,7 +494,7 @@ mod tests { use super::*; use crate::protocols::jolt::geometry::dimensions::TracePolynomialOrder; - use jolt_field::{Fr, FromPrimitiveInt, Invertible}; + use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; #[test] fn cycle_skip_scale_counts_inactive_cycle_rounds() { diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs b/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs index 1b53af7039..2989c8c575 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs @@ -490,7 +490,7 @@ mod tests { PrecommittedClaimReduction, PrecommittedReductionLayout, }; use super::*; - use jolt_field::{Fr, FromPrimitiveInt, Invertible}; + use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use jolt_poly::EqPolynomial; fn dimensions() -> JoltOneHotDimensions { diff --git a/crates/jolt-crypto/benches/crypto.rs b/crates/jolt-crypto/benches/crypto.rs index d41511ccb3..23ae34fd64 100644 --- a/crates/jolt-crypto/benches/crypto.rs +++ b/crates/jolt-crypto/benches/crypto.rs @@ -5,7 +5,7 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use jolt_crypto::{ Bn254, Bn254G1, Bn254G2, JoltGroup, PairingGroup, Pedersen, PedersenSetup, VectorCommitment, }; -use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; +use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-crypto/fuzz/fuzz_targets/group_arith.rs b/crates/jolt-crypto/fuzz/fuzz_targets/group_arith.rs index cfe9b9062a..5a4fa15080 100644 --- a/crates/jolt-crypto/fuzz/fuzz_targets/group_arith.rs +++ b/crates/jolt-crypto/fuzz/fuzz_targets/group_arith.rs @@ -1,14 +1,14 @@ #![no_main] use jolt_crypto::{Bn254, Bn254G1, JoltGroup}; -use jolt_field::{Fr, ReducingBytes}; +use jolt_field::{Fr, CanonicalRepr}; use libfuzzer_sys::fuzz_target; fn parse_input(data: &[u8]) -> Option<(Fr, Fr, Bn254G1)> { if data.len() < 64 { return None; } - let s1 = ::from_le_bytes_mod_order(&data[..32]); - let s2 = ::from_le_bytes_mod_order(&data[32..64]); + let s1 = ::from_le_bytes_mod_order(&data[..32]); + let s2 = ::from_le_bytes_mod_order(&data[32..64]); let g = Bn254::g1_generator(); let p = g.scalar_mul(&s1); Some((s1, s2, p)) diff --git a/crates/jolt-crypto/fuzz/fuzz_targets/pedersen_commit.rs b/crates/jolt-crypto/fuzz/fuzz_targets/pedersen_commit.rs index aaa1d14a62..3e7aa53797 100644 --- a/crates/jolt-crypto/fuzz/fuzz_targets/pedersen_commit.rs +++ b/crates/jolt-crypto/fuzz/fuzz_targets/pedersen_commit.rs @@ -1,6 +1,6 @@ #![no_main] use jolt_crypto::{Bn254, Bn254G1, VectorCommitment, JoltGroup, Pedersen, PedersenSetup}; -use jolt_field::{Fr, FromPrimitiveInt, ReducingBytes}; +use jolt_field::{Fr, FromPrimitiveInt, CanonicalRepr}; use libfuzzer_sys::fuzz_target; /// Fixed small setup (4 generators) — deterministic so we don't waste fuzzer @@ -23,9 +23,9 @@ fuzz_target!(|data: &[u8]| { let setup = fixed_setup(); let values: Vec = (0..4) - .map(|i| ::from_le_bytes_mod_order(&data[i * 32..(i + 1) * 32])) + .map(|i| ::from_le_bytes_mod_order(&data[i * 32..(i + 1) * 32])) .collect(); - let blinding = ::from_le_bytes_mod_order(&data[128..160]); + let blinding = ::from_le_bytes_mod_order(&data[128..160]); // Commit-verify round-trip let c = Pedersen::::commit(&setup, &values, &blinding); diff --git a/crates/jolt-crypto/src/commitment.rs b/crates/jolt-crypto/src/commitment.rs index 4b09d9e865..55b5b7c0d6 100644 --- a/crates/jolt-crypto/src/commitment.rs +++ b/crates/jolt-crypto/src/commitment.rs @@ -69,8 +69,7 @@ pub trait VectorCommitment: row_len: usize, row_point: &[Self::Field], entry_point: &[Self::Field], - ) -> Result<(VectorCommitmentOpening, Self::Field), VectorOpeningError> - { + ) -> Result<(VectorCommitmentOpening, Self::Field), VectorOpeningError> { let row_count = point_len_to_basis_len(row_point.len())?; validate_row_len(row_len, entry_point.len())?; let max_len = row_count @@ -276,8 +275,7 @@ fn combine_rows( row_len: usize, row_weights: &[F], max_len: usize, -) -> Vec -{ +) -> Vec { let mut combined_vector = vec![F::zero(); row_len]; if max_len >= PAR_THRESHOLD { @@ -316,8 +314,7 @@ fn combine_rows( row_len: usize, row_weights: &[F], _max_len: usize, -) -> Vec -{ +) -> Vec { let mut combined_vector = vec![F::zero(); row_len]; for (entry_index, combined_entry) in combined_vector.iter_mut().enumerate() { @@ -333,8 +330,7 @@ fn combine_rows( combined_vector } -fn inner_product(lhs: &[F], rhs: &[F]) -> F -{ +fn inner_product(lhs: &[F], rhs: &[F]) -> F { #[cfg(feature = "parallel")] { if lhs.len() >= PAR_THRESHOLD { diff --git a/crates/jolt-crypto/tests/coverage.rs b/crates/jolt-crypto/tests/coverage.rs index 6d59088099..c3f1239b83 100644 --- a/crates/jolt-crypto/tests/coverage.rs +++ b/crates/jolt-crypto/tests/coverage.rs @@ -7,7 +7,7 @@ use jolt_crypto::ec::bn254::glv; use jolt_crypto::{ Bn254, Bn254G1, Bn254G2, Bn254GT, HomomorphicCommitment, JoltGroup, PairingGroup, }; -use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; +use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-crypto/tests/group_laws.rs b/crates/jolt-crypto/tests/group_laws.rs index 4477227487..ef07767f79 100644 --- a/crates/jolt-crypto/tests/group_laws.rs +++ b/crates/jolt-crypto/tests/group_laws.rs @@ -1,7 +1,7 @@ //! Algebraic group law tests for BN254 G1 and G2. use jolt_crypto::{Bn254, Bn254G1, Bn254G2, JoltGroup}; -use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; +use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-crypto/tests/pairing.rs b/crates/jolt-crypto/tests/pairing.rs index a50c75dd4d..a5a5d8be8b 100644 --- a/crates/jolt-crypto/tests/pairing.rs +++ b/crates/jolt-crypto/tests/pairing.rs @@ -1,7 +1,7 @@ //! Pairing bilinearity and consistency tests for BN254. use jolt_crypto::{Bn254, Bn254G2, Bn254GT, JoltGroup, PairingGroup}; -use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; +use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-crypto/tests/pedersen.rs b/crates/jolt-crypto/tests/pedersen.rs index 03828379c2..47509232b0 100644 --- a/crates/jolt-crypto/tests/pedersen.rs +++ b/crates/jolt-crypto/tests/pedersen.rs @@ -6,7 +6,7 @@ use jolt_crypto::{ Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment, VectorCommitmentOpening, VectorOpeningError, }; -use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; +use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use jolt_poly::EqPolynomial; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-dory/benches/dory.rs b/crates/jolt-dory/benches/dory.rs index dc40253ce1..2fd3d13c70 100644 --- a/crates/jolt-dory/benches/dory.rs +++ b/crates/jolt-dory/benches/dory.rs @@ -7,7 +7,7 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use jolt_dory::{DoryScheme, DoryVerifierSetup}; -use jolt_field::{Fr, RandomSampling}; +use jolt_field::{FieldCore, Fr}; use jolt_openings::{CommitmentScheme, StreamingCommitment, ZkOpeningScheme}; use jolt_poly::{OneHotPolynomial, Polynomial}; use jolt_transcript::Transcript; @@ -63,7 +63,7 @@ fn bench_open(c: &mut Criterion) { let mut rng = ChaCha20Rng::seed_from_u64(0); let poly = Polynomial::::random(nv, &mut rng); let point: Vec = (0..nv) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); (poly, point, eval) @@ -94,7 +94,7 @@ fn bench_verify(c: &mut Criterion) { let mut rng = ChaCha20Rng::seed_from_u64(0); let poly = Polynomial::::random(nv, &mut rng); let point: Vec = (0..nv) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, _) = @@ -166,8 +166,8 @@ fn bench_combine(c: &mut Criterion) { let poly_b = Polynomial::::random(num_vars, &mut rng); let (commit_a, _) = DoryScheme::commit(poly_a.evaluations(), &setup).unwrap(); let (commit_b, _) = DoryScheme::commit(poly_b.evaluations(), &setup).unwrap(); - let s_a = ::random(&mut rng); - let s_b = ::random(&mut rng); + let s_a = ::random(&mut rng); + let s_b = ::random(&mut rng); group.bench_with_input(BenchmarkId::from_parameter(num_vars), &num_vars, |b, _| { b.iter(|| { @@ -192,7 +192,7 @@ fn bench_combine_hints(c: &mut Criterion) { .map(|_| { let poly = Polynomial::::random(num_vars, &mut rng); let (_, hint) = DoryScheme::commit(poly.evaluations(), &setup).unwrap(); - (hint, ::random(&mut rng)) + (hint, ::random(&mut rng)) }) .collect(); let hints: Vec<_> = hints_and_scalars.iter().map(|(h, _)| h.clone()).collect(); @@ -230,7 +230,7 @@ fn bench_open_zk(c: &mut Criterion) { let mut rng = ChaCha20Rng::seed_from_u64(0); let poly = Polynomial::::random(nv, &mut rng); let point: Vec = (0..nv) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (_, hint) = @@ -265,7 +265,7 @@ fn bench_verify_zk(c: &mut Criterion) { let mut rng = ChaCha20Rng::seed_from_u64(0); let poly = Polynomial::::random(nv, &mut rng); let point: Vec = (0..nv) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = diff --git a/crates/jolt-dory/fuzz/fuzz_targets/verify_tampered.rs b/crates/jolt-dory/fuzz/fuzz_targets/verify_tampered.rs index 05c183dba2..bb39462066 100644 --- a/crates/jolt-dory/fuzz/fuzz_targets/verify_tampered.rs +++ b/crates/jolt-dory/fuzz/fuzz_targets/verify_tampered.rs @@ -3,7 +3,7 @@ use std::sync::OnceLock; use jolt_dory::{DoryCommitment, DoryProof, DoryScheme, DoryVerifierSetup}; -use jolt_field::{Fr, RandomSampling}; +use jolt_field::{Fr, FieldCore}; use jolt_openings::CommitmentScheme; use jolt_poly::Polynomial; use jolt_transcript::Blake2bTranscript; diff --git a/crates/jolt-dory/src/scheme.rs b/crates/jolt-dory/src/scheme.rs index 1e1d9a9fc0..12d6a1cede 100644 --- a/crates/jolt-dory/src/scheme.rs +++ b/crates/jolt-dory/src/scheme.rs @@ -528,7 +528,7 @@ mod tests { use super::*; use jolt_crypto::{Pedersen, VectorCommitment}; - use jolt_field::{FromPrimitiveInt, RandomSampling}; + use jolt_field::{FieldCore, FromPrimitiveInt}; use jolt_poly::Polynomial; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; @@ -543,7 +543,7 @@ mod tests { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); @@ -617,7 +617,7 @@ mod tests { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); diff --git a/crates/jolt-dory/src/streaming.rs b/crates/jolt-dory/src/streaming.rs index 64c17d00ee..70d9d89cc9 100644 --- a/crates/jolt-dory/src/streaming.rs +++ b/crates/jolt-dory/src/streaming.rs @@ -352,8 +352,8 @@ fn scalar_affine_bases<'a>( mod tests { #![expect(clippy::unwrap_used, reason = "tests unwrap successful PCS operations")] + use jolt_field::FieldCore; use jolt_field::FromPrimitiveInt; - use jolt_field::RandomSampling; use jolt_openings::{ CommitmentScheme, StreamingCommitment, ZkOpeningScheme, ZkStreamingCommitment, }; @@ -376,7 +376,7 @@ mod tests { let prover_setup = DoryScheme::setup_prover(num_vars); let evals: Vec = (0..num_rows * num_cols) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let poly = jolt_poly::Polynomial::new(evals.clone()); @@ -394,7 +394,7 @@ mod tests { ); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"stream-open"); @@ -451,7 +451,7 @@ mod tests { ); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"u64-stream-open"); @@ -516,7 +516,7 @@ mod tests { let mut rng = ChaCha20Rng::seed_from_u64(313); let point = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect::>(); let eval = Fr::from_u64(0); let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"zero-zk-open"); @@ -623,7 +623,7 @@ mod tests { let mut rng = ChaCha20Rng::seed_from_u64(317); let point = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect::>(); let eval = poly.evaluate(&point); let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"one-hot-zk-open"); @@ -686,7 +686,7 @@ mod tests { ); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"i128-stream-open"); diff --git a/crates/jolt-dory/src/types.rs b/crates/jolt-dory/src/types.rs index 8d38591712..50f2027556 100644 --- a/crates/jolt-dory/src/types.rs +++ b/crates/jolt-dory/src/types.rs @@ -174,7 +174,7 @@ fn validate_proof_round_count(buf: &[u8]) -> Result<(), String> { )] mod tests { use super::*; - use jolt_field::RandomSampling; + use jolt_field::FieldCore; use jolt_openings::CommitmentScheme; use jolt_poly::Polynomial; use jolt_transcript::Transcript; @@ -213,7 +213,7 @@ mod tests { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = @@ -254,7 +254,7 @@ mod tests { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); @@ -290,7 +290,7 @@ mod tests { let prover_setup = crate::DoryScheme::setup_prover(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); diff --git a/crates/jolt-dory/tests/commit_open_verify.rs b/crates/jolt-dory/tests/commit_open_verify.rs index d8c728b26f..1d28dade63 100644 --- a/crates/jolt-dory/tests/commit_open_verify.rs +++ b/crates/jolt-dory/tests/commit_open_verify.rs @@ -11,7 +11,7 @@ use dory::backends::arkworks::ArkG1; use jolt_dory::DoryScheme; -use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; +use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use jolt_openings::{ AdditivelyHomomorphic, CommitmentScheme, StreamingCommitment, ZkOpeningScheme, }; @@ -26,7 +26,7 @@ fn round_trip>(num_vars: usize, seed: u64, label: let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); @@ -130,7 +130,7 @@ fn streaming_zk_commitment_is_blinded_and_verifies() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); @@ -170,7 +170,7 @@ fn wrong_eval_rejected() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); @@ -200,7 +200,7 @@ fn wrong_point_rejected() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); @@ -235,8 +235,8 @@ fn combine_linear_combination() { let (commit_a, _) = DoryScheme::commit(poly_a.evaluations(), &prover_setup).unwrap(); let (commit_b, _) = DoryScheme::commit(poly_b.evaluations(), &prover_setup).unwrap(); - let c1 = ::random(&mut rng); - let c2 = ::random(&mut rng); + let c1 = ::random(&mut rng); + let c2 = ::random(&mut rng); let combined = DoryScheme::combine(&[commit_a, commit_b], &[c1, c2]); @@ -294,7 +294,7 @@ fn wrong_commitment_rejected() { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); @@ -329,7 +329,7 @@ fn wrong_transcript_domain_rejected() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); @@ -356,7 +356,7 @@ fn zk_round_trip>(num_vars: usize, seed: u64, labe let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = @@ -396,7 +396,7 @@ fn transparent_verify_rejects_zk_opening_proof() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = @@ -424,7 +424,7 @@ fn zk_wrong_commitment_rejected() { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = @@ -455,7 +455,7 @@ fn transparent_commitment_rejected_for_zk_blinded_proof() { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (transparent_commitment, _) = @@ -496,8 +496,8 @@ fn zk_combined_commitment_and_hint_verify() { let (commit_b, hint_b) = ::commit_zk(poly_b.evaluations(), &prover_setup).unwrap(); - let c1 = ::random(&mut rng); - let c2 = ::random(&mut rng); + let c1 = ::random(&mut rng); + let c2 = ::random(&mut rng); let combined_commitment = DoryScheme::combine(&[commit_a, commit_b], &[c1, c2]); let combined_hint = DoryScheme::combine_hints(vec![hint_a, hint_b], &[c1, c2]); @@ -509,7 +509,7 @@ fn zk_combined_commitment_and_hint_verify() { .collect(); let weighted_poly = Polynomial::new(weighted_evals); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = weighted_poly.evaluate(&point); @@ -545,7 +545,7 @@ fn wrong_eval_commitment_rejected_zk() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = @@ -574,7 +574,7 @@ fn zk_wrong_transcript_domain_rejected() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = diff --git a/crates/jolt-field/Cargo.toml b/crates/jolt-field/Cargo.toml index 024b8d934a..0886ce57ff 100644 --- a/crates/jolt-field/Cargo.toml +++ b/crates/jolt-field/Cargo.toml @@ -39,6 +39,7 @@ parallel = ["dep:rayon"] allocative = ["dep:allocative"] [dev-dependencies] +bincode = { workspace = true } ark-std = { workspace = true } rand_chacha = { workspace = true } criterion = { workspace = true } diff --git a/crates/jolt-field/benches/field_arith.rs b/crates/jolt-field/benches/field_arith.rs index cfe784f76c..ef86a0b1f8 100644 --- a/crates/jolt-field/benches/field_arith.rs +++ b/crates/jolt-field/benches/field_arith.rs @@ -3,14 +3,14 @@ use std::hint::black_box; use criterion::{criterion_group, criterion_main, Criterion}; -use jolt_field::{FixedBytes, Fr, MulPrimitiveInt, RandomSampling}; +use jolt_field::{CanonicalRepr, FieldCore, Fr, FromPrimitiveInt}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; fn bench_field_mul(c: &mut Criterion) { let mut rng = ChaCha20Rng::seed_from_u64(0); - let a: Fr = ::random(&mut rng); - let b: Fr = ::random(&mut rng); + let a: Fr = ::random(&mut rng); + let b: Fr = ::random(&mut rng); c.bench_function("Fr * Fr", |bench| { bench.iter(|| black_box(a) * black_box(b)); @@ -19,35 +19,35 @@ fn bench_field_mul(c: &mut Criterion) { fn bench_mul_u64(c: &mut Criterion) { let mut rng = ChaCha20Rng::seed_from_u64(1); - let a: Fr = ::random(&mut rng); + let a: Fr = ::random(&mut rng); let n = 0xDEAD_BEEF_CAFE_BABEu64; c.bench_function("Fr::mul_u64", |bench| { - bench.iter(|| ::mul_u64(black_box(&a), black_box(n))); + bench.iter(|| ::mul_u64(black_box(&a), black_box(n))); }); } fn bench_mul_u128(c: &mut Criterion) { let mut rng = ChaCha20Rng::seed_from_u64(2); - let a: Fr = ::random(&mut rng); + let a: Fr = ::random(&mut rng); let n = 0xDEAD_BEEF_CAFE_BABE_1234_5678_9ABC_DEF0u128; c.bench_function("Fr::mul_u128", |bench| { - bench.iter(|| ::mul_u128(black_box(&a), black_box(n))); + bench.iter(|| ::mul_u128(black_box(&a), black_box(n))); }); } fn bench_to_from_bytes(c: &mut Criterion) { let mut rng = ChaCha20Rng::seed_from_u64(4); - let a: Fr = ::random(&mut rng); - let bytes = a.to_bytes_array(); + let a: Fr = ::random(&mut rng); + let bytes = a.to_bytes_le_vec(); c.bench_function("Fr::to_bytes", |bench| { - bench.iter(|| black_box(a).to_bytes_array()); + bench.iter(|| black_box(a).to_bytes_le_vec()); }); c.bench_function("Fr::from_bytes", |bench| { - bench.iter(|| Fr::from_bytes_array(black_box(&bytes))); + bench.iter(|| ::from_le_bytes_mod_order(black_box(&bytes))); }); } diff --git a/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs b/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs index b37fd5f4c6..18f1c41d2d 100644 --- a/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs +++ b/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs @@ -3,7 +3,7 @@ use std::time::Instant; use criterion::{black_box, Criterion, Throughput}; use jolt_field::packed::PackedField; -use jolt_field::{FieldCore, Invertible, RandomSampling, RingCore}; +use jolt_field::{FieldCore, RingCore}; use rand::{rngs::StdRng, SeedableRng}; use super::data::duration_per_logical_op; @@ -16,14 +16,7 @@ pub(crate) fn bench_arithmetic_case( seed: u64, params: ArithmeticBenchParams, ) where - F: FieldCore - + RandomSampling - + RingCore - + Invertible - + AddAssign - + SubAssign - + MulAssign - + 'static, + F: FieldCore + FieldCore + RingCore + FieldCore + AddAssign + SubAssign + MulAssign + 'static, PF: PackedField + Copy + 'static, { let mut rng = StdRng::seed_from_u64(seed); diff --git a/crates/jolt-field/benches/solinas_field_arith/kernel.rs b/crates/jolt-field/benches/solinas_field_arith/kernel.rs index 9fe22695c0..16cf23caef 100644 --- a/crates/jolt-field/benches/solinas_field_arith/kernel.rs +++ b/crates/jolt-field/benches/solinas_field_arith/kernel.rs @@ -1,6 +1,6 @@ use criterion::{black_box, Criterion, Throughput}; use jolt_field::packed::PackedField; -use jolt_field::{CanonicalField, FieldCore, Prime128Offset275, RandomSampling}; +use jolt_field::{CanonicalField, FieldCore, Prime128Offset275}; use rand::{rngs::StdRng, RngCore, SeedableRng}; use super::cases::*; @@ -36,7 +36,7 @@ fn sumcheck_bench( rng: &mut StdRng, n: u64, ) where - F: FieldCore + RandomSampling + 'static, + F: FieldCore + FieldCore + 'static, PF: PackedField + Copy + 'static, { let eq: Vec = (0..n).map(|_| F::random(rng)).collect(); diff --git a/crates/jolt-field/benches/solinas_field_arith/parallel.rs b/crates/jolt-field/benches/solinas_field_arith/parallel.rs index 525f2541fa..f900d0f2cc 100644 --- a/crates/jolt-field/benches/solinas_field_arith/parallel.rs +++ b/crates/jolt-field/benches/solinas_field_arith/parallel.rs @@ -8,9 +8,7 @@ use criterion::{black_box, Criterion, Throughput}; #[cfg(feature = "parallel")] use jolt_field::packed::{PackedField, PackedValue}; #[cfg(feature = "parallel")] -use jolt_field::{ - CanonicalField, Prime128Offset275, Prime31Offset19, Prime64Offset59, RandomSampling, -}; +use jolt_field::{CanonicalField, FieldCore, Prime128Offset275, Prime31Offset19, Prime64Offset59}; #[cfg(feature = "parallel")] use rand::{rngs::StdRng, SeedableRng}; #[cfg(feature = "parallel")] @@ -57,10 +55,10 @@ pub(crate) fn bench_parallel_throughput(c: &mut Criterion) { .expect("build benchmark rayon pool"); let mut rng = StdRng::seed_from_u64(0x7061_7261_0001); - let lhs31: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); - let rhs31: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); - let lhs64: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); - let rhs64: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let lhs31: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); + let rhs31: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); + let lhs64: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); + let rhs64: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); let lhs128: Vec = (0..n) .map(|_| Prime128Offset275::from_canonical_u128_reduced(rand_u128(&mut rng))) .collect(); diff --git a/crates/jolt-field/fuzz/fuzz_targets/field_arith.rs b/crates/jolt-field/fuzz/fuzz_targets/field_arith.rs index 4e71b313d7..f7635c1dde 100644 --- a/crates/jolt-field/fuzz/fuzz_targets/field_arith.rs +++ b/crates/jolt-field/fuzz/fuzz_targets/field_arith.rs @@ -1,5 +1,5 @@ #![no_main] -use jolt_field::{Fr, FromPrimitiveInt, Invertible, ReducingBytes}; +use jolt_field::{Fr, FromPrimitiveInt, FieldCore, CanonicalRepr}; use libfuzzer_sys::fuzz_target; use num_traits::Zero; @@ -7,8 +7,8 @@ fuzz_target!(|data: &[u8]| { if data.len() < 64 { return; } - let a = ::from_le_bytes_mod_order(&data[..32]); - let b = ::from_le_bytes_mod_order(&data[32..64]); + let a = ::from_le_bytes_mod_order(&data[..32]); + let b = ::from_le_bytes_mod_order(&data[32..64]); // Arithmetic operations must not panic let sum = a + b; diff --git a/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs b/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs index d8fcbf633d..43803908ad 100644 --- a/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs +++ b/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs @@ -1,14 +1,14 @@ #![no_main] -use jolt_field::{FixedBytes, Fr, ReducingBytes}; +use jolt_field::{CanonicalRepr, Fr, CanonicalRepr}; use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { // from_bytes should never panic on arbitrary input - let a = ::from_le_bytes_mod_order(data); + let a = ::from_le_bytes_mod_order(data); // Round-trip: from_bytes → to_bytes → from_bytes must be stable - let bytes = a.to_bytes_array(); - let b = ::from_le_bytes_mod_order(&bytes); - let bytes2 = b.to_bytes_array(); + let bytes = a.to_bytes_le_vec(); + let b = ::from_le_bytes_mod_order(&bytes); + let bytes2 = b.to_bytes_le_vec(); assert_eq!(bytes, bytes2, "from_bytes round-trip is not stable"); }); diff --git a/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs b/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs index 99a70fbc28..7a581819b3 100644 --- a/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs +++ b/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs @@ -1,7 +1,7 @@ #![no_main] use jolt_field::{ - FpExt4, FromPrimitiveInt, Invertible, Prime128Offset275, Prime31Offset19, ReducingBytes, + FpExt4, FromPrimitiveInt, FieldCore, Prime128Offset275, Prime31Offset19, CanonicalRepr, }; use libfuzzer_sys::fuzz_target; use num_traits::Zero; diff --git a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs index 04f7be779f..904710862c 100644 --- a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs +++ b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs @@ -1,5 +1,5 @@ #![no_main] -use jolt_field::{Accumulator, Fr, ReducingBytes, WideAccumulator}; +use jolt_field::{Accumulator, Fr, CanonicalRepr, WideAccumulator}; use libfuzzer_sys::fuzz_target; use num_traits::Zero; @@ -16,9 +16,9 @@ fuzz_target!(|data: &[u8]| { let pairs = data.len() / 64; for i in 0..pairs { let offset = i * 64; - let a = ::from_le_bytes_mod_order(&data[offset..offset + 32]); + let a = ::from_le_bytes_mod_order(&data[offset..offset + 32]); let b = - ::from_le_bytes_mod_order(&data[offset + 32..offset + 64]); + ::from_le_bytes_mod_order(&data[offset + 32..offset + 64]); acc.fmadd(a, b); naive_sum += a * b; diff --git a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs index faef8c486d..528f8b191a 100644 --- a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs +++ b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs @@ -1,5 +1,5 @@ #![no_main] -use jolt_field::{Accumulator, Fr, ReducingBytes, WideAccumulator}; +use jolt_field::{Accumulator, Fr, CanonicalRepr, WideAccumulator}; use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { @@ -17,9 +17,9 @@ fuzz_target!(|data: &[u8]| { for i in 0..pairs { let offset = i * 64; - let a = ::from_le_bytes_mod_order(&data[offset..offset + 32]); + let a = ::from_le_bytes_mod_order(&data[offset..offset + 32]); let b = - ::from_le_bytes_mod_order(&data[offset + 32..offset + 64]); + ::from_le_bytes_mod_order(&data[offset + 32..offset + 64]); if i < split { acc1.fmadd(a, b); diff --git a/crates/jolt-field/src/additive_group.rs b/crates/jolt-field/src/additive_group.rs deleted file mode 100644 index cd7146d885..0000000000 --- a/crates/jolt-field/src/additive_group.rs +++ /dev/null @@ -1,20 +0,0 @@ -use num_traits::Zero; -use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; - -/// Minimal additive group operations shared by fields, rings, and accumulators. -pub trait AdditiveGroup: - Sized - + Clone - + Copy - + Send - + Sync - + Zero - + Add - + for<'a> Add<&'a Self, Output = Self> - + AddAssign - + Sub - + for<'a> Sub<&'a Self, Output = Self> - + SubAssign - + Neg -{ -} diff --git a/crates/jolt-field/src/akita.rs b/crates/jolt-field/src/akita.rs index a6f679834f..f76e67c42f 100644 --- a/crates/jolt-field/src/akita.rs +++ b/crates/jolt-field/src/akita.rs @@ -2,9 +2,7 @@ use akita_config::proof_optimized::fp128::Field as AkitaField; use rand_core::RngCore; use crate::{ - AdditiveGroup, CanonicalBitLength, CanonicalBytes, CanonicalU64, Field, FieldCore, - FixedByteSize, FixedBytes, FromPrimitiveInt, Invertible, MulPow2, MulPrimitiveInt, - NaiveAccumulator, RandomSampling, ReducingBytes, RingCore, TranscriptChallenge, + AdditiveGroup, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, NaiveAccumulator, RingCore, WithAccumulator, }; @@ -12,14 +10,17 @@ impl AdditiveGroup for AkitaField {} impl RingCore for AkitaField {} -impl Invertible for AkitaField { +impl FieldCore for AkitaField { #[inline] fn inverse(&self) -> Option { ::inverse(self) } -} -impl FieldCore for AkitaField {} + #[inline] + fn random(rng: &mut R) -> Self { + ::random(rng) + } +} impl FromPrimitiveInt for AkitaField { #[inline] @@ -43,58 +44,30 @@ impl FromPrimitiveInt for AkitaField { } } -impl RandomSampling for AkitaField { - #[inline] - fn random(rng: &mut R) -> Self { - ::random(rng) - } -} - -impl MulPow2 for AkitaField {} - -impl MulPrimitiveInt for AkitaField {} - -impl FixedByteSize for AkitaField { +impl CanonicalRepr for AkitaField { const NUM_BYTES: usize = ::NUM_BYTES; -} -impl CanonicalBytes for AkitaField { #[inline(always)] fn to_bytes_le(&self, out: &mut [u8]) { ::to_bytes_le(self, out); } -} -impl ReducingBytes for AkitaField { #[inline(always)] fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { ::from_le_bytes_mod_order(bytes) } -} -impl TranscriptChallenge for AkitaField { - #[inline(always)] - fn from_challenge_bytes(bytes: &[u8]) -> Self { - ::from_le_bytes_mod_order(bytes) + #[inline] + fn to_canonical_u64_checked(&self) -> Option { + ::to_canonical_u64_checked(self) } -} -impl FixedBytes<16> for AkitaField {} - -impl CanonicalBitLength for AkitaField { #[inline] fn num_bits(&self) -> u32 { ::num_bits(self) } } -impl CanonicalU64 for AkitaField { - #[inline] - fn to_canonical_u64_checked(&self) -> Option { - ::to_canonical_u64_checked(self) - } -} - impl WithAccumulator for AkitaField { type Accumulator = NaiveAccumulator; } diff --git a/crates/jolt-field/src/algebra.rs b/crates/jolt-field/src/algebra.rs new file mode 100644 index 0000000000..e096e96465 --- /dev/null +++ b/crates/jolt-field/src/algebra.rs @@ -0,0 +1,217 @@ +//! Core algebraic ladder: additive groups, rings, and fields, plus +//! primitive-integer embedding. + +use num_traits::{One, Zero}; +use rand_core::RngCore; +use std::fmt::{Debug, Display}; +use std::hash::Hash; +use std::iter::{Product, Sum}; +use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +/// Minimal additive group operations shared by fields, rings, and accumulators. +pub trait AdditiveGroup: + Sized + + Clone + + Copy + + Send + + Sync + + Zero + + Add + + for<'a> Add<&'a Self, Output = Self> + + AddAssign + + Sub + + for<'a> Sub<&'a Self, Output = Self> + + SubAssign + + Neg +{ +} + +/// Core ring arithmetic: additive group plus multiplication and one. +pub trait RingCore: + AdditiveGroup + + One + + PartialEq + + Eq + + Default + + Debug + + Display + + Hash + + Mul + + for<'a> Mul<&'a Self, Output = Self> + + MulAssign + + Sum + + for<'a> Sum<&'a Self> + + Product + + for<'a> Product<&'a Self> +{ + /// Returns `self * self`. + #[inline] + fn square(&self) -> Self { + *self * *self + } + + #[inline] + fn pow2(exponent: usize) -> Self { + let mut result = Self::one(); + let mut base = Self::one() + Self::one(); + let mut remaining = exponent; + + while remaining > 0 { + if remaining % 2 == 1 { + result *= base; + } + remaining /= 2; + if remaining > 0 { + base = base.square(); + } + } + + result + } +} + +/// Algebraic field: ring arithmetic plus explicit inversion and sampling. +pub trait FieldCore: RingCore { + /// Multiplicative inverse, or `None` for the zero element. + fn inverse(&self) -> Option; + + /// Multiplicative inverse with zero mapped to zero. + #[inline] + fn inv_or_zero(self) -> Self { + self.inverse().unwrap_or_else(Self::zero) + } + + /// Samples a random element (RNG-backed, for tests and witnesses). + fn random(rng: &mut R) -> Self; +} + +/// Embed primitive integer values and multiply by primitive integer scalars. +pub trait FromPrimitiveInt: RingCore { + #[inline] + fn from_bool(v: bool) -> Self { + if v { + Self::from_u64(1) + } else { + Self::from_u64(0) + } + } + + #[inline] + fn from_u8(v: u8) -> Self { + Self::from_u64(v as u64) + } + + #[inline] + fn from_i8(v: i8) -> Self { + Self::from_i64(v as i64) + } + + #[inline] + fn from_u16(v: u16) -> Self { + Self::from_u64(v as u64) + } + + #[inline] + fn from_i16(v: i16) -> Self { + Self::from_i64(v as i64) + } + + #[inline] + fn from_u32(v: u32) -> Self { + Self::from_u64(v as u64) + } + + #[inline] + fn from_i32(v: i32) -> Self { + Self::from_i64(v as i64) + } + + fn from_u64(v: u64) -> Self; + fn from_i64(v: i64) -> Self; + fn from_u128(v: u128) -> Self; + fn from_i128(v: i128) -> Self; + + /// Multiplies by a `u64`. + #[inline(always)] + fn mul_u64(&self, n: u64) -> Self { + *self * Self::from_u64(n) + } + + /// Multiplies by an `i64`. + #[inline(always)] + fn mul_i64(&self, n: i64) -> Self { + *self * Self::from_i64(n) + } + + /// Multiplies by a `u128`. + #[inline(always)] + fn mul_u128(&self, n: u128) -> Self { + *self * Self::from_u128(n) + } + + /// Multiplies by an `i128`. + #[inline(always)] + fn mul_i128(&self, n: i128) -> Self { + *self * Self::from_i128(n) + } + + /// Multiplies this ring element by the integer `2^pow`. + #[inline] + fn mul_pow_2(&self, pow: usize) -> Self { + assert!(pow <= 255, "pow > 255"); + let mut res = *self; + let mut p = pow; + while p >= 64 { + res *= Self::from_u64(1 << 63); + p -= 63; + } + res * Self::from_u64(1 << p) + } +} + +/// Multiplication with fast-path short-circuits for zero and one. +/// +/// In sumcheck hot loops many evaluations multiply by 0 or 1. +/// These methods avoid the full Montgomery multiplication in those cases. +pub trait OptimizedMul: Sized + Mul { + /// Returns `zero()` immediately if either operand is zero. + fn mul_0_optimized(self, other: Rhs) -> Self::Output; + /// Returns the other operand immediately if either is one. + fn mul_1_optimized(self, other: Rhs) -> Self::Output; + /// Combined: short-circuits on both zero and one. + fn mul_01_optimized(self, other: Rhs) -> Self::Output; +} + +impl OptimizedMul for F +where + F: RingCore, +{ + #[inline(always)] + fn mul_0_optimized(self, other: F) -> F { + if self.is_zero() || other.is_zero() { + Self::zero() + } else { + self * other + } + } + + #[inline(always)] + fn mul_1_optimized(self, other: F) -> F { + if self.is_one() { + other + } else if other.is_one() { + self + } else { + self * other + } + } + + #[inline(always)] + fn mul_01_optimized(self, other: F) -> F { + if self.is_zero() || other.is_zero() { + Self::zero() + } else { + self.mul_1_optimized(other) + } + } +} diff --git a/crates/jolt-field/src/arkworks/bn254.rs b/crates/jolt-field/src/arkworks/bn254.rs index d31096ca7f..974f116d3d 100644 --- a/crates/jolt-field/src/arkworks/bn254.rs +++ b/crates/jolt-field/src/arkworks/bn254.rs @@ -3,9 +3,8 @@ //! [`Fr`] is `#[repr(transparent)]` over the inner arkworks scalar field element, //! so it has identical layout and can be transmuted where needed. use crate::{ - AdditiveGroup, CanonicalBitLength, CanonicalBytes, CanonicalU64, Field, FieldCore, - FixedByteSize, FixedBytes, FromPrimitiveInt, Invertible, Limbs, MulPrimitiveInt, - RandomSampling, ReducingBytes, RingCore, TranscriptChallenge, WithAccumulator, + AdditiveGroup, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, Limbs, RingCore, + WithAccumulator, }; use ark_ff::{prelude::*, PrimeField, UniformRand}; use rand_core::RngCore; @@ -323,39 +322,54 @@ impl RingCore for Fr { } } -impl Invertible for Fr { +impl FieldCore for Fr { #[inline] fn inverse(&self) -> Option { ::inverse(&self.0).map(Fr) } -} -impl FieldCore for Fr {} + #[inline] + fn random(rng: &mut R) -> Self { + Fr(::rand(rng)) + } +} -impl FixedByteSize for Fr { +impl CanonicalRepr for Fr { const NUM_BYTES: usize = 32; -} -impl CanonicalBytes for Fr { #[expect(clippy::expect_used)] #[inline] fn to_bytes_le(&self, out: &mut [u8]) { - assert_eq!(out.len(), ::NUM_BYTES); + assert_eq!(out.len(), ::NUM_BYTES); use ark_serialize::CanonicalSerialize; self.0 .serialize_compressed(out) .expect("BN254 Fr always serializes to 32 bytes"); } -} -impl ReducingBytes for Fr { #[inline] fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { Fr::from_le_bytes_mod_order(bytes) } -} -impl TranscriptChallenge for Fr { + #[inline] + fn to_canonical_u64_checked(&self) -> Option { + let bigint = ::into_bigint(self.0); + let limbs: &[u64] = bigint.as_ref(); + let result = limbs[0]; + + if ::from_u64(result) != *self { + None + } else { + Some(result) + } + } + + #[inline] + fn num_bits(&self) -> u32 { + ::into_bigint(self.0).num_bits() + } + #[inline] fn from_challenge_bytes(bytes: &[u8]) -> Self { let mut buf = [0u8; 16]; @@ -382,37 +396,6 @@ impl TranscriptChallenge for Fr { } } -impl FixedBytes<32> for Fr {} - -impl CanonicalU64 for Fr { - #[inline] - fn to_canonical_u64_checked(&self) -> Option { - let bigint = ::into_bigint(self.0); - let limbs: &[u64] = bigint.as_ref(); - let result = limbs[0]; - - if ::from_u64(result) != *self { - None - } else { - Some(result) - } - } -} - -impl CanonicalBitLength for Fr { - #[inline] - fn num_bits(&self) -> u32 { - ::into_bigint(self.0).num_bits() - } -} - -impl RandomSampling for Fr { - #[inline] - fn random(rng: &mut R) -> Self { - Fr(::rand(rng)) - } -} - impl FromPrimitiveInt for Fr { #[inline] fn from_u64(n: u64) -> Self { @@ -441,15 +424,7 @@ impl FromPrimitiveInt for Fr { fn from_u128(val: u128) -> Self { Fr(bn254_ops::from_u128(val)) } -} - -impl WithAccumulator for Fr { - type Accumulator = super::wide_accumulator::WideAccumulator; -} - -impl crate::MulPow2 for Fr {} -impl MulPrimitiveInt for Fr { #[inline] fn mul_u64(&self, n: u64) -> Self { Fr(bn254_ops::mul_u64(self.0, n)) @@ -471,13 +446,17 @@ impl MulPrimitiveInt for Fr { } } +impl WithAccumulator for Fr { + type Accumulator = super::wide_accumulator::WideAccumulator; +} + impl Field for Fr {} #[cfg(test)] #[expect(clippy::unwrap_used)] mod tests { use super::*; - use crate::{CanonicalU64, FixedBytes}; + use crate::CanonicalRepr; #[test] fn field_arithmetic_basic() { @@ -502,8 +481,8 @@ mod tests { #[test] fn serialization_roundtrip() { let val = Fr::from_u64(123_456_789); - let bytes = val.to_bytes_array(); - let recovered = Fr::from_bytes_array(&bytes); + let bytes = val.to_bytes_le_vec(); + let recovered = ::from_le_bytes_mod_order(&bytes); assert_eq!(val, recovered); } diff --git a/crates/jolt-field/src/arkworks/bn254_fq.rs b/crates/jolt-field/src/arkworks/bn254_fq.rs index f40eb41ed2..992c38e55c 100644 --- a/crates/jolt-field/src/arkworks/bn254_fq.rs +++ b/crates/jolt-field/src/arkworks/bn254_fq.rs @@ -4,10 +4,8 @@ //! scalar field of Grumpkin. use crate::{ - AdditiveGroup, CanonicalBitLength, CanonicalBytes, CanonicalU64, Field, FieldCore, - FixedByteSize, FixedBytes, FromPrimitiveInt, Invertible, Limbs, MulPrimitiveInt, - NaiveAccumulator, RandomSampling, ReducingBytes, RingCore, TranscriptChallenge, - WithAccumulator, + AdditiveGroup, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, Limbs, NaiveAccumulator, + RingCore, WithAccumulator, }; use ark_ff::{prelude::*, PrimeField, UniformRand}; use rand_core::RngCore; @@ -304,39 +302,36 @@ impl RingCore for Fq { } } -impl Invertible for Fq { +impl FieldCore for Fq { #[inline] fn inverse(&self) -> Option { ::inverse(&self.0).map(Fq) } -} -impl FieldCore for Fq {} + #[inline] + fn random(rng: &mut R) -> Self { + Fq(::rand(rng)) + } +} -impl FixedByteSize for Fq { +impl CanonicalRepr for Fq { const NUM_BYTES: usize = 32; -} -impl CanonicalBytes for Fq { #[expect(clippy::expect_used)] #[inline] fn to_bytes_le(&self, out: &mut [u8]) { - assert_eq!(out.len(), ::NUM_BYTES); + assert_eq!(out.len(), ::NUM_BYTES); use ark_serialize::CanonicalSerialize; self.0 .serialize_compressed(out) .expect("BN254 Fq always serializes to 32 bytes"); } -} -impl ReducingBytes for Fq { #[inline] fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { Fq::from_le_bytes_mod_order(bytes) } -} -impl TranscriptChallenge for Fq { #[inline] fn from_challenge_bytes(bytes: &[u8]) -> Self { let mut buf = [0u8; 16]; @@ -357,11 +352,7 @@ impl TranscriptChallenge for Fq { buf.reverse(); Fq::from_le_bytes_mod_order(&buf) } -} -impl FixedBytes<32> for Fq {} - -impl CanonicalU64 for Fq { #[inline] fn to_canonical_u64_checked(&self) -> Option { let bigint = ::into_bigint(self.0); @@ -374,22 +365,13 @@ impl CanonicalU64 for Fq { Some(result) } } -} -impl CanonicalBitLength for Fq { #[inline] fn num_bits(&self) -> u32 { ::into_bigint(self.0).num_bits() } } -impl RandomSampling for Fq { - #[inline] - fn random(rng: &mut R) -> Self { - Fq(::rand(rng)) - } -} - impl FromPrimitiveInt for Fq { #[inline] fn from_u64(n: u64) -> Self { @@ -424,17 +406,13 @@ impl WithAccumulator for Fq { type Accumulator = NaiveAccumulator; } -impl crate::MulPow2 for Fq {} - -impl MulPrimitiveInt for Fq {} - impl Field for Fq {} #[cfg(test)] #[expect(clippy::unwrap_used)] mod tests { use super::*; - use crate::{CanonicalU64, FixedBytes}; + use crate::CanonicalRepr; #[test] fn field_arithmetic_basic() { @@ -448,8 +426,8 @@ mod tests { #[test] fn serialization_roundtrip() { let val = Fq::from_u64(123_456_789); - let bytes = val.to_bytes_array(); - let recovered = Fq::from_bytes_array(&bytes); + let bytes = val.to_bytes_le_vec(); + let recovered = ::from_le_bytes_mod_order(&bytes); assert_eq!(val, recovered); } diff --git a/crates/jolt-field/src/canonical.rs b/crates/jolt-field/src/canonical.rs new file mode 100644 index 0000000000..f8e4387626 --- /dev/null +++ b/crates/jolt-field/src/canonical.rs @@ -0,0 +1,59 @@ +//! Canonical byte representation: the Fiat-Shamir transcript surface. + +use std::fmt::Debug; +use std::hash::Hash; + +/// Canonical little-endian representation of a field element. +/// +/// This trait is the transcript surface: Fiat-Shamir absorption and challenge +/// derivation use these explicit canonical encodings so the hashed byte +/// stream is specified independently of any serialization library. Proof and +/// wire serialization go through serde + bincode instead; the two must not be +/// conflated. +/// +/// # Invariants +/// +/// - The encoding is injective on canonical representatives: equal elements +/// produce equal bytes, distinct elements produce distinct bytes. +/// - [`to_bytes_le`](Self::to_bytes_le) always writes exactly +/// [`NUM_BYTES`](Self::NUM_BYTES) bytes of the unique representative. +pub trait CanonicalRepr: + Sized + Copy + Default + PartialEq + Eq + Debug + Hash + Sync + Send + 'static +{ + /// Byte length of the fixed-size canonical encoding. + const NUM_BYTES: usize; + + /// Writes the canonical little-endian encoding into `out`. + fn to_bytes_le(&self, out: &mut [u8]); + + /// Returns the canonical little-endian encoding as a vector. + #[inline] + fn to_bytes_le_vec(&self) -> Vec { + let mut out = vec![0u8; Self::NUM_BYTES]; + self.to_bytes_le(&mut out); + out + } + + /// Deserializes little-endian bytes by reducing into this type. + fn from_le_bytes_mod_order(bytes: &[u8]) -> Self; + + /// Returns the canonical representative as `u64` if it fits. + fn to_canonical_u64_checked(&self) -> Option; + + /// Number of significant bits in this element's canonical representative. + /// + /// Zero is considered to have zero significant bits. + fn num_bits(&self) -> u32; + + /// Constructs a Fiat-Shamir challenge from squeezed transcript bytes. + #[inline] + fn from_challenge_bytes(bytes: &[u8]) -> Self { + Self::from_le_bytes_mod_order(bytes) + } + + /// Constructs a non-optimized scalar challenge from transcript bytes. + #[inline] + fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { + Self::from_challenge_bytes(bytes) + } +} diff --git a/crates/jolt-field/src/canonical_bit_length.rs b/crates/jolt-field/src/canonical_bit_length.rs deleted file mode 100644 index 89664ed862..0000000000 --- a/crates/jolt-field/src/canonical_bit_length.rs +++ /dev/null @@ -1,7 +0,0 @@ -/// Significant-bit introspection for canonical representatives. -pub trait CanonicalBitLength { - /// Number of significant bits in this element's canonical representative. - /// - /// Zero is considered to have zero significant bits. - fn num_bits(&self) -> u32; -} diff --git a/crates/jolt-field/src/canonical_bytes.rs b/crates/jolt-field/src/canonical_bytes.rs deleted file mode 100644 index 6b0363f65c..0000000000 --- a/crates/jolt-field/src/canonical_bytes.rs +++ /dev/null @@ -1,13 +0,0 @@ -/// Canonical little-endian byte encoding. -pub trait CanonicalBytes: Sized + crate::FixedByteSize { - /// Writes the canonical little-endian encoding into `out`. - fn to_bytes_le(&self, out: &mut [u8]); - - /// Returns the canonical little-endian encoding as a vector. - #[inline] - fn to_bytes_le_vec(&self) -> Vec { - let mut out = vec![0u8; Self::NUM_BYTES]; - self.to_bytes_le(&mut out); - out - } -} diff --git a/crates/jolt-field/src/canonical_u64.rs b/crates/jolt-field/src/canonical_u64.rs deleted file mode 100644 index 9d2b8f1cab..0000000000 --- a/crates/jolt-field/src/canonical_u64.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// Checked extraction of canonical representatives that fit in `u64`. -pub trait CanonicalU64 { - /// Returns the canonical representative as `u64` if it fits. - fn to_canonical_u64_checked(&self) -> Option; -} diff --git a/crates/jolt-field/src/ext/fp_ext2.rs b/crates/jolt-field/src/ext/fp_ext2.rs index f0e11cd186..3c2fa2c69b 100644 --- a/crates/jolt-field/src/ext/fp_ext2.rs +++ b/crates/jolt-field/src/ext/fp_ext2.rs @@ -275,7 +275,7 @@ impl> RingCore for FpExt2 { } } -impl> Invertible for FpExt2 { +impl> FieldCore for FpExt2 { fn inverse(&self) -> Option { if self.is_zero() { return None; @@ -283,6 +283,10 @@ impl> Invertible for FpExt2 { let inv_n = self.norm().inverse()?; Some(Self::new(self.coeffs[0] * inv_n, (-self.coeffs[1]) * inv_n)) } + + fn random(rng: &mut R) -> Self { + Self::new(F::random(rng), F::random(rng)) + } } impl> HalvingField for FpExt2 { @@ -292,12 +296,6 @@ impl> HalvingField for FpExt2 { } } -impl> RandomSampling for FpExt2 { - fn random(rng: &mut R) -> Self { - Self::new(F::random(rng), F::random(rng)) - } -} - impl> FromPrimitiveInt for FpExt2 { fn from_u64(val: u64) -> Self { Self::from_u64(val) @@ -517,3 +515,20 @@ impl>> MulBaseUnreduced> for FpExt /// Default quadratic extension used by the Solinas backend tests and helpers. pub type Ext2 = FpExt2; + +impl> serde::Serialize for FpExt2 { + fn serialize(&self, serializer: S) -> Result { + self.coeffs.serialize(serializer) + } +} + +impl<'de, F, C> serde::Deserialize<'de> for FpExt2 +where + F: FieldCore + serde::Deserialize<'de>, + C: FpExt2Config, +{ + fn deserialize>(deserializer: D) -> Result { + let [c0, c1] = <[F; 2]>::deserialize(deserializer)?; + Ok(Self::new(c0, c1)) + } +} diff --git a/crates/jolt-field/src/ext/fp_ext4.rs b/crates/jolt-field/src/ext/fp_ext4.rs index 03c04c82e8..4880a86dde 100644 --- a/crates/jolt-field/src/ext/fp_ext4.rs +++ b/crates/jolt-field/src/ext/fp_ext4.rs @@ -445,7 +445,11 @@ impl RingCore for FpExt4 { } } -impl Invertible for FpExt4 { +impl FieldCore for FpExt4 { + fn random(rng: &mut R) -> Self { + Self::new(std::array::from_fn(|_| F::random(rng))) + } + fn inverse(&self) -> Option { if self.is_zero() { return None; @@ -481,18 +485,7 @@ impl HalvingField for FpExt4 { } } -impl RandomSampling for FpExt4 { - fn random(rng: &mut R) -> Self { - Self::new([ - F::random(rng), - F::random(rng), - F::random(rng), - F::random(rng), - ]) - } -} - -impl FromPrimitiveInt for FpExt4 { +impl FromPrimitiveInt for FpExt4 { fn from_u64(val: u64) -> Self { Self::from_u64(val) } @@ -670,3 +663,15 @@ macro_rules! impl_fp_ext4_default_optimized_fold { impl_fp_ext4_default_optimized_fold!(Fp64); impl_fp_ext4_default_optimized_fold!(Fp128); + +impl serde::Serialize for FpExt4 { + fn serialize(&self, serializer: S) -> Result { + self.coeffs.serialize(serializer) + } +} + +impl<'de, F: FieldCore + serde::Deserialize<'de>> serde::Deserialize<'de> for FpExt4 { + fn deserialize>(deserializer: D) -> Result { + Ok(Self::new(<[F; 4]>::deserialize(deserializer)?)) + } +} diff --git a/crates/jolt-field/src/ext/fp_ext8.rs b/crates/jolt-field/src/ext/fp_ext8.rs index 82cf577e19..5628ad771a 100644 --- a/crates/jolt-field/src/ext/fp_ext8.rs +++ b/crates/jolt-field/src/ext/fp_ext8.rs @@ -339,7 +339,11 @@ impl RingCore for FpExt8 { } } -impl Invertible for FpExt8 { +impl FieldCore for FpExt8 { + fn random(rng: &mut R) -> Self { + Self::new(std::array::from_fn(|_| F::random(rng))) + } + fn inverse(&self) -> Option { if self.is_zero() { return None; @@ -394,13 +398,7 @@ impl HalvingField for FpExt8 { } } -impl RandomSampling for FpExt8 { - fn random(rng: &mut R) -> Self { - Self::new(std::array::from_fn(|_| F::random(rng))) - } -} - -impl FromPrimitiveInt for FpExt8 { +impl FromPrimitiveInt for FpExt8 { fn from_u64(val: u64) -> Self { Self::from_u64(val) } @@ -482,3 +480,15 @@ macro_rules! impl_fp_ext8_default_optimized_fold { impl_fp_ext8_default_optimized_fold!(Fp32); impl_fp_ext8_default_optimized_fold!(Fp64); impl_fp_ext8_default_optimized_fold!(Fp128); + +impl serde::Serialize for FpExt8 { + fn serialize(&self, serializer: S) -> Result { + self.coeffs.serialize(serializer) + } +} + +impl<'de, F: FieldCore + serde::Deserialize<'de>> serde::Deserialize<'de> for FpExt8 { + fn deserialize>(deserializer: D) -> Result { + Ok(Self::new(<[F; 8]>::deserialize(deserializer)?)) + } +} diff --git a/crates/jolt-field/src/ext/mod.rs b/crates/jolt-field/src/ext/mod.rs index 303a75d400..5c5f5bdca3 100644 --- a/crates/jolt-field/src/ext/mod.rs +++ b/crates/jolt-field/src/ext/mod.rs @@ -19,8 +19,7 @@ use super::unreduced::{ HasOptimizedFold, HasUnreducedOps, }; use crate::{ - CanonicalField, FieldCore, FromPrimitiveInt, HalvingField, Invertible, - MulBaseUnreduced, RandomSampling, RingCore, + CanonicalField, FieldCore, FromPrimitiveInt, HalvingField, MulBaseUnreduced, RingCore, }; use rand_core::RngCore; use std::marker::PhantomData; @@ -30,4 +29,3 @@ pub use fp_ext2::{Ext2, FpExt2, FpExt2Config, NegOneNr, TwoNr}; pub use fp_ext4::{FpExt4, FpExt4MulBackend}; pub(crate) use fp_ext8::{fp_ext8_mul_schedule, fp_ext8_square_schedule}; pub use fp_ext8::{FpExt8, FpExt8MulBackend}; - diff --git a/crates/jolt-field/src/ext/native_algebra.rs b/crates/jolt-field/src/ext/native_algebra.rs index bf4e67fdc3..6665f105af 100644 --- a/crates/jolt-field/src/ext/native_algebra.rs +++ b/crates/jolt-field/src/ext/native_algebra.rs @@ -3,7 +3,7 @@ //! //! These are the Jolt-free supertrait obligations of the native //! [`AdditiveGroup`]/[`FieldCore`] hierarchy. The non-trivial `RingCore::square` -//! / `Invertible::inverse` impls stay co-located with each extension type. +//! / `FieldCore::inverse` impls stay co-located with each extension type. use std::fmt; use std::hash::{Hash, Hasher}; @@ -73,7 +73,6 @@ impl<'a, F: FieldCore, C: FpExt2Config> Product<&'a Self> for FpExt2 { } impl> AdditiveGroup for FpExt2 {} -impl> FieldCore for FpExt2 {} // --- FpExt4 ----------------------------------------------------- @@ -137,7 +136,6 @@ impl<'a, F: FieldCore + FpExt4MulBackend> Product<&'a Self> for FpExt4 { } impl AdditiveGroup for FpExt4 {} -impl FieldCore for FpExt4 {} // --- FpExt8 ----------------------------------------------------- @@ -226,4 +224,3 @@ impl<'a, F: FieldCore + FpExt8MulBackend> Product<&'a Self> for FpExt8 { } impl AdditiveGroup for FpExt8 {} -impl FieldCore for FpExt8 {} diff --git a/crates/jolt-field/src/ext/tests.rs b/crates/jolt-field/src/ext/tests.rs index 74a230b5a9..79be9b6172 100644 --- a/crates/jolt-field/src/ext/tests.rs +++ b/crates/jolt-field/src/ext/tests.rs @@ -11,7 +11,7 @@ use crate::ext::lift::{ ExtField, FrobeniusExtField, }; use crate::Fp64; -use crate::{FromPrimitiveInt, Invertible}; +use crate::{FieldCore, FromPrimitiveInt}; use rand::rngs::StdRng; use rand::SeedableRng; @@ -407,8 +407,8 @@ fn mul_base_to_product_accum_matches_mul_base_sum() { fn check(seed: u64) where - Base: FieldCore + RandomSampling, - Ext: MulBaseUnreduced + Zero + RandomSampling, + Base: FieldCore, + Ext: MulBaseUnreduced + Zero, { let mut rng = StdRng::seed_from_u64(seed); let n = 1024; diff --git a/crates/jolt-field/src/field.rs b/crates/jolt-field/src/field.rs index 6d0a74e92c..e77db71999 100644 --- a/crates/jolt-field/src/field.rs +++ b/crates/jolt-field/src/field.rs @@ -1,12 +1,4 @@ -use std::fmt::{Debug, Display}; -use std::hash::Hash; -use std::ops::Mul; - -use crate::{ - CanonicalBitLength, CanonicalBytes, CanonicalU64, FieldCore, FixedByteSize, FromPrimitiveInt, - MulPow2, MulPrimitiveInt, RandomSampling, ReducingBytes, RingCore, TranscriptChallenge, - WithAccumulator, -}; +use crate::{CanonicalRepr, FieldCore, FromPrimitiveInt, WithAccumulator}; /// Prime field element abstraction used throughout Jolt. /// @@ -16,75 +8,4 @@ use crate::{ /// All arithmetic is modular over the field's prime order. Elements are `Copy`, /// thread-safe, and cheaply serializable. Negative integers are mapped via /// their canonical representative modulo `p`. -pub trait Field: - 'static - + Sized - + Copy - + Sync - + Send - + Default - + Eq - + Hash - + Display - + Debug - + FieldCore - + FromPrimitiveInt - + CanonicalBytes - + ReducingBytes - + TranscriptChallenge - + FixedByteSize - + CanonicalBitLength - + CanonicalU64 - + RandomSampling - + WithAccumulator - + MulPow2 - + MulPrimitiveInt -{ -} - -/// Multiplication with fast-path short-circuits for zero and one. -/// -/// In sumcheck hot loops many evaluations multiply by 0 or 1. -/// These methods avoid the full Montgomery multiplication in those cases. -pub trait OptimizedMul: Sized + Mul { - /// Returns `zero()` immediately if either operand is zero. - fn mul_0_optimized(self, other: Rhs) -> Self::Output; - /// Returns the other operand immediately if either is one. - fn mul_1_optimized(self, other: Rhs) -> Self::Output; - /// Combined: short-circuits on both zero and one. - fn mul_01_optimized(self, other: Rhs) -> Self::Output; -} - -impl OptimizedMul for F -where - F: RingCore, -{ - #[inline(always)] - fn mul_0_optimized(self, other: F) -> F { - if self.is_zero() || other.is_zero() { - Self::zero() - } else { - self * other - } - } - - #[inline(always)] - fn mul_1_optimized(self, other: F) -> F { - if self.is_one() { - other - } else if other.is_one() { - self - } else { - self * other - } - } - - #[inline(always)] - fn mul_01_optimized(self, other: F) -> F { - if self.is_zero() || other.is_zero() { - Self::zero() - } else { - self.mul_1_optimized(other) - } - } -} +pub trait Field: FieldCore + FromPrimitiveInt + CanonicalRepr + WithAccumulator {} diff --git a/crates/jolt-field/src/field_core.rs b/crates/jolt-field/src/field_core.rs deleted file mode 100644 index d9a6f5eee7..0000000000 --- a/crates/jolt-field/src/field_core.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::{Invertible, RingCore}; - -/// Algebraic field marker: ring arithmetic plus explicit inversion. -pub trait FieldCore: RingCore + Invertible {} diff --git a/crates/jolt-field/src/fixed_byte_size.rs b/crates/jolt-field/src/fixed_byte_size.rs deleted file mode 100644 index 09e1ac47a4..0000000000 --- a/crates/jolt-field/src/fixed_byte_size.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// Fixed byte-size metadata for canonical encodings. -pub trait FixedByteSize { - /// Byte length of the fixed-size encoding. - const NUM_BYTES: usize; -} diff --git a/crates/jolt-field/src/fixed_bytes.rs b/crates/jolt-field/src/fixed_bytes.rs deleted file mode 100644 index 6f465be63b..0000000000 --- a/crates/jolt-field/src/fixed_bytes.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::{CanonicalBytes, FixedByteSize, ReducingBytes}; - -/// Fixed-array convenience API for canonical field/value encodings. -pub trait FixedBytes: CanonicalBytes + ReducingBytes + FixedByteSize { - /// Returns the canonical fixed-size byte encoding. - #[inline] - fn to_bytes_array(&self) -> [u8; N] { - debug_assert_eq!(Self::NUM_BYTES, N); - let mut out = [0u8; N]; - self.to_bytes_le(&mut out); - out - } - - /// Reducing constructor from a fixed-size byte array. - #[inline] - fn from_bytes_array(bytes: &[u8; N]) -> Self { - Self::from_le_bytes_mod_order(bytes) - } -} diff --git a/crates/jolt-field/src/from_primitive_int.rs b/crates/jolt-field/src/from_primitive_int.rs deleted file mode 100644 index 31cf1aef12..0000000000 --- a/crates/jolt-field/src/from_primitive_int.rs +++ /dev/null @@ -1,46 +0,0 @@ -/// Embed primitive integer values into a scalar object. -pub trait FromPrimitiveInt: Sized { - #[inline] - fn from_bool(v: bool) -> Self { - if v { - Self::from_u64(1) - } else { - Self::from_u64(0) - } - } - - #[inline] - fn from_u8(v: u8) -> Self { - Self::from_u64(v as u64) - } - - #[inline] - fn from_i8(v: i8) -> Self { - Self::from_i64(v as i64) - } - - #[inline] - fn from_u16(v: u16) -> Self { - Self::from_u64(v as u64) - } - - #[inline] - fn from_i16(v: i16) -> Self { - Self::from_i64(v as i64) - } - - #[inline] - fn from_u32(v: u32) -> Self { - Self::from_u64(v as u64) - } - - #[inline] - fn from_i32(v: i32) -> Self { - Self::from_i64(v as i64) - } - - fn from_u64(v: u64) -> Self; - fn from_i64(v: i64) -> Self; - fn from_u128(v: u128) -> Self; - fn from_i128(v: i128) -> Self; -} diff --git a/crates/jolt-field/src/invertible.rs b/crates/jolt-field/src/invertible.rs deleted file mode 100644 index 5e15491d5a..0000000000 --- a/crates/jolt-field/src/invertible.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::RingCore; - -/// Ring-level inversion capability with explicit zero handling. -pub trait Invertible: RingCore { - /// Multiplicative inverse, or `None` for the zero element. - fn inverse(&self) -> Option; - - /// Multiplicative inverse with zero mapped to zero. - #[inline] - fn inv_or_zero(self) -> Self { - self.inverse().unwrap_or_else(Self::zero) - } -} diff --git a/crates/jolt-field/src/lib.rs b/crates/jolt-field/src/lib.rs index f16850eeab..b8ae22dda0 100644 --- a/crates/jolt-field/src/lib.rs +++ b/crates/jolt-field/src/lib.rs @@ -5,12 +5,13 @@ //! //! ```text //! AdditiveGroup -> RingCore -> FieldCore -//! \-> Invertible //! ``` //! -//! Serialization, sampling, transcript challenges, primitive-integer embedding, -//! and accumulator support are separate capabilities so non-BN254 fields can -//! opt into only the surface they actually provide. +//! [`CanonicalRepr`] (the Fiat-Shamir transcript surface), primitive-integer +//! embedding, and accumulator support are separate capabilities so non-BN254 +//! fields and rings opt into only the surface they actually provide. Proof +//! and wire serialization use serde + bincode, never the canonical transcript +//! encoding. //! //! # Core traits //! @@ -41,51 +42,25 @@ //! - [`signed`] module — `S64`, `S128`, `S192`, `S256` and half-limb variants mod accumulator; -mod additive_group; #[cfg(feature = "akita")] mod akita; -mod canonical_bit_length; -mod canonical_bytes; -mod canonical_u64; +mod algebra; +mod canonical; mod field; -mod field_core; mod field_error; -mod fixed_byte_size; -mod fixed_bytes; -mod from_primitive_int; -mod invertible; mod montgomery_constants; -mod mul_pow_2; -mod mul_primitive_int; -mod random_sampling; -mod reducing_bytes; -mod ring_core; #[cfg(feature = "solinas")] mod solinas_traits; -mod transcript_challenge; pub use accumulator::{Accumulator, NaiveAccumulator, WithAccumulator}; -pub use additive_group::AdditiveGroup; -pub use canonical_bit_length::CanonicalBitLength; -pub use canonical_bytes::CanonicalBytes; -pub use canonical_u64::CanonicalU64; -pub use field::{Field, OptimizedMul}; -pub use field_core::FieldCore; +pub use algebra::{AdditiveGroup, FieldCore, FromPrimitiveInt, OptimizedMul, RingCore}; +pub use canonical::CanonicalRepr; +pub use field::Field; pub use field_error::FieldError; -pub use fixed_byte_size::FixedByteSize; -pub use fixed_bytes::FixedBytes; -pub use from_primitive_int::FromPrimitiveInt; -pub use invertible::Invertible; pub use montgomery_constants::MontgomeryConstants; -pub use mul_pow_2::MulPow2; -pub use mul_primitive_int::MulPrimitiveInt; pub use num_traits::{One, Zero}; -pub use random_sampling::RandomSampling; -pub use reducing_bytes::ReducingBytes; -pub use ring_core::RingCore; #[cfg(feature = "solinas")] pub use solinas_traits::{balanced_digit_lut, CanonicalField, HalvingField, PseudoMersenneField}; -pub use transcript_challenge::TranscriptChallenge; pub mod limbs; pub use limbs::Limbs; diff --git a/crates/jolt-field/src/mul_pow_2.rs b/crates/jolt-field/src/mul_pow_2.rs deleted file mode 100644 index dbc30ee975..0000000000 --- a/crates/jolt-field/src/mul_pow_2.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::{FromPrimitiveInt, RingCore}; - -/// Multiplication by powers of two. -pub trait MulPow2: RingCore + FromPrimitiveInt { - /// Multiplies this ring element by the integer `2^pow`. - #[inline] - fn mul_pow_2(&self, pow: usize) -> Self { - assert!(pow <= 255, "pow > 255"); - let mut res = *self; - let mut p = pow; - while p >= 64 { - res *= Self::from_u64(1 << 63); - p -= 63; - } - res * Self::from_u64(1 << p) - } -} diff --git a/crates/jolt-field/src/mul_primitive_int.rs b/crates/jolt-field/src/mul_primitive_int.rs deleted file mode 100644 index 4980596de0..0000000000 --- a/crates/jolt-field/src/mul_primitive_int.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::{FromPrimitiveInt, RingCore}; - -/// Multiplication by primitive integer scalars. -pub trait MulPrimitiveInt: RingCore + FromPrimitiveInt { - /// Multiplies by a `u64`. - #[inline(always)] - fn mul_u64(&self, n: u64) -> Self { - *self * Self::from_u64(n) - } - - /// Multiplies by an `i64`. - #[inline(always)] - fn mul_i64(&self, n: i64) -> Self { - *self * Self::from_i64(n) - } - - /// Multiplies by a `u128`. - #[inline(always)] - fn mul_u128(&self, n: u128) -> Self { - *self * Self::from_u128(n) - } - - /// Multiplies by an `i128`. - #[inline(always)] - fn mul_i128(&self, n: i128) -> Self { - *self * Self::from_i128(n) - } -} diff --git a/crates/jolt-field/src/packed/avx2/mod.rs b/crates/jolt-field/src/packed/avx2/mod.rs index f6217f35e0..4fdb081afb 100644 --- a/crates/jolt-field/src/packed/avx2/mod.rs +++ b/crates/jolt-field/src/packed/avx2/mod.rs @@ -9,7 +9,7 @@ use super::{PackedField, PackedValue}; use crate::ext::FpExt2Config; -use crate::Invertible; +use crate::FieldCore; use crate::{Fp128, Fp32, Fp64}; use core::arch::x86_64::*; use core::fmt; diff --git a/crates/jolt-field/src/packed/avx512/mod.rs b/crates/jolt-field/src/packed/avx512/mod.rs index e84f37945b..f8b05f5e60 100644 --- a/crates/jolt-field/src/packed/avx512/mod.rs +++ b/crates/jolt-field/src/packed/avx512/mod.rs @@ -10,7 +10,7 @@ use super::{PackedField, PackedValue}; use crate::ext::FpExt2Config; -use crate::Invertible; +use crate::FieldCore; use crate::{Fp128, Fp32, Fp64}; use core::arch::x86_64::*; use core::fmt; diff --git a/crates/jolt-field/src/packed/ext/mod.rs b/crates/jolt-field/src/packed/ext/mod.rs index 13f0bfb8d3..7350116ce7 100644 --- a/crates/jolt-field/src/packed/ext/mod.rs +++ b/crates/jolt-field/src/packed/ext/mod.rs @@ -12,7 +12,7 @@ use crate::ext::{FpExt2, FpExt2Config, FpExt4, FpExt4MulBackend, FpExt8, FpExt8MulBackend}; use crate::packed::{HasPacking, PackedField, PackedValue}; -use crate::{FieldCore, Invertible}; +use crate::FieldCore; use core::ops::{Add, Mul, Sub}; /// Packed `FpExt2` elements stored in transpose layout: `[PF; 2]`. @@ -146,7 +146,7 @@ where #[inline(always)] fn inverse(self) -> Option where - Self::Scalar: Invertible, + Self::Scalar: FieldCore, { let norm = self.c0 * self.c0 - C::mul_non_residue(self.c1 * self.c1, PF::broadcast); let inv_norm = norm.inverse()?; @@ -305,7 +305,7 @@ where #[inline(always)] fn inverse(self) -> Option where - Self::Scalar: Invertible, + Self::Scalar: FieldCore, { Some(Self::new(PF::fp_ext4_inverse(self.coeffs)?)) } @@ -452,7 +452,7 @@ where #[inline(always)] fn inverse(self) -> Option where - Self::Scalar: Invertible, + Self::Scalar: FieldCore, { // FpExt8 inversion uses Gaussian elimination — delegate lane by lane. let mut coeffs: [Vec; 8] = std::array::from_fn(|_| Vec::with_capacity(PF::WIDTH)); diff --git a/crates/jolt-field/src/packed/ext/tests.rs b/crates/jolt-field/src/packed/ext/tests.rs index e3078541d4..52a7ecb236 100644 --- a/crates/jolt-field/src/packed/ext/tests.rs +++ b/crates/jolt-field/src/packed/ext/tests.rs @@ -6,12 +6,12 @@ use super::*; use crate::ext::{Ext2, FpExt2, FpExt4, TwoNr}; +use crate::FieldCore; use crate::Fp32; use crate::Fp64; use crate::Prime31Offset19; use crate::Prime32Offset99; use crate::Prime64Offset59; -use crate::RandomSampling; use crate::RingCore; use rand::rngs::StdRng; use rand::SeedableRng; diff --git a/crates/jolt-field/src/packed/mod.rs b/crates/jolt-field/src/packed/mod.rs index 748d995b92..9abb2e32b9 100644 --- a/crates/jolt-field/src/packed/mod.rs +++ b/crates/jolt-field/src/packed/mod.rs @@ -19,7 +19,7 @@ pub(crate) mod neon; pub use ext::{PackedFpExt2, PackedFpExt4, PackedFpExt8}; use crate::ext::{fp_ext8_mul_schedule, fp_ext8_square_schedule, FpExt2Config}; -use crate::{FieldCore, Fp128, Fp32, Fp64, Invertible}; +use crate::{FieldCore, Fp128, Fp32, Fp64}; use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; use num_traits::Zero; @@ -98,7 +98,7 @@ pub trait PackedField: #[inline] fn inverse(self) -> Option where - Self::Scalar: Invertible, + Self::Scalar: FieldCore, { let mut inverses = Vec::with_capacity(Self::WIDTH); for lane in 0..Self::WIDTH { @@ -170,7 +170,7 @@ pub trait PackedField: #[inline(always)] fn fp_ext4_inverse(a: [Self; 4]) -> Option<[Self; 4]> where - Self::Scalar: Invertible, + Self::Scalar: FieldCore, { let zero = Self::broadcast(Self::Scalar::zero()); let [a0, a1, a2, a3] = a; diff --git a/crates/jolt-field/src/packed/neon/fp32.rs b/crates/jolt-field/src/packed/neon/fp32.rs index 454e39731f..38da7bcb01 100644 --- a/crates/jolt-field/src/packed/neon/fp32.rs +++ b/crates/jolt-field/src/packed/neon/fp32.rs @@ -750,7 +750,7 @@ impl PackedField for PackedFp32Neon

{ #[inline(always)] fn fp_ext4_inverse(a: [Self; 4]) -> Option<[Self; 4]> where - Self::Scalar: Invertible, + Self::Scalar: FieldCore, { let [a0, a1, a2, a3] = a.map(Self::to_vec); let zero = unsafe { vdupq_n_u32(0) }; diff --git a/crates/jolt-field/src/packed/neon/mod.rs b/crates/jolt-field/src/packed/neon/mod.rs index c164a7046f..68ee9812bf 100644 --- a/crates/jolt-field/src/packed/neon/mod.rs +++ b/crates/jolt-field/src/packed/neon/mod.rs @@ -7,7 +7,7 @@ use super::{PackedField, PackedValue}; use crate::ext::FpExt2Config; -use crate::Invertible; +use crate::FieldCore; use crate::{Fp128, Fp32, Fp64}; use core::arch::aarch64::{ uint32x2_t, uint32x4_t, uint64x2_t, vaddq_u32, vaddq_u64, vandq_u32, vandq_u64, vbslq_u64, diff --git a/crates/jolt-field/src/packed/tests.rs b/crates/jolt-field/src/packed/tests.rs index 885e832166..b4f5f32214 100644 --- a/crates/jolt-field/src/packed/tests.rs +++ b/crates/jolt-field/src/packed/tests.rs @@ -6,7 +6,7 @@ use super::{HasPacking, PackedField, PackedValue}; use crate::{ CanonicalField, FieldCore, Fp32, Prime128Offset275, Prime24Offset3, Prime31Offset19, - Prime32Offset99, Prime40Offset195, Prime64Offset59, RandomSampling, + Prime32Offset99, Prime40Offset195, Prime64Offset59, }; use rand::{rngs::StdRng, RngCore, SeedableRng}; @@ -18,13 +18,13 @@ fn rand_u128(rng: &mut R) -> u128 { fn check_packed_add_sub_mul(seed: u64) where - F: FieldCore + RandomSampling + PartialEq + std::fmt::Debug, + F: FieldCore + PartialEq + std::fmt::Debug, PF: PackedField + PackedValue, { let mut rng = StdRng::seed_from_u64(seed); let len = PF::WIDTH * 17 + 3; - let lhs: Vec = (0..len).map(|_| RandomSampling::random(&mut rng)).collect(); - let rhs: Vec = (0..len).map(|_| RandomSampling::random(&mut rng)).collect(); + let lhs: Vec = (0..len).map(|_| FieldCore::random(&mut rng)).collect(); + let rhs: Vec = (0..len).map(|_| FieldCore::random(&mut rng)).collect(); let (lhs_p, lhs_s) = PF::pack_slice_with_suffix(&lhs); let (rhs_p, rhs_s) = PF::pack_slice_with_suffix(&rhs); diff --git a/crates/jolt-field/src/prime/fp128/core.rs b/crates/jolt-field/src/prime/fp128/core.rs index 340f60a1df..0913076522 100644 --- a/crates/jolt-field/src/prime/fp128/core.rs +++ b/crates/jolt-field/src/prime/fp128/core.rs @@ -66,7 +66,7 @@ impl Fp128

{ /// Multiplicative inverse, or `None` for zero. #[inline] pub fn inverse(&self) -> Option { - ::inverse(self) + ::inverse(self) } /// Construct from a `u64` reduced modulo the field modulus. diff --git a/crates/jolt-field/src/prime/fp128/mod.rs b/crates/jolt-field/src/prime/fp128/mod.rs index 12ba5ec978..6a4f51b413 100644 --- a/crates/jolt-field/src/prime/fp128/mod.rs +++ b/crates/jolt-field/src/prime/fp128/mod.rs @@ -33,12 +33,10 @@ mod wide; use ::core::arch::asm; use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use crate::{FromPrimitiveInt, Invertible, RandomSampling}; +use crate::{FieldCore, FromPrimitiveInt}; use rand_core::RngCore; -use crate::{ - CanonicalField, HalvingField, PseudoMersenneField, -}; +use crate::{CanonicalField, HalvingField, PseudoMersenneField}; use super::util::{is_pow2_u64, log2_pow2_u64, mul64_wide}; diff --git a/crates/jolt-field/src/prime/fp128/tests.rs b/crates/jolt-field/src/prime/fp128/tests.rs index 9626b09a86..8ddeb5dce0 100644 --- a/crates/jolt-field/src/prime/fp128/tests.rs +++ b/crates/jolt-field/src/prime/fp128/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::{PseudoMersenneField, RandomSampling}; +use crate::{FieldCore, PseudoMersenneField}; use rand::rngs::StdRng; use rand::SeedableRng; use rand_core::RngCore; @@ -10,7 +10,7 @@ type F = Prime128Offset275; fn to_limbs_roundtrip() { let mut rng = StdRng::seed_from_u64(0xdead_beef_cafe_1234); for _ in 0..1000 { - let a: F = RandomSampling::random(&mut rng); + let a: F = FieldCore::random(&mut rng); assert_eq!(Fp128(a.to_limbs()), a); } } @@ -19,7 +19,7 @@ fn to_limbs_roundtrip() { fn mul_wide_u64_matches_full_mul() { let mut rng = StdRng::seed_from_u64(0x1122_3344_5566_7788); for _ in 0..1000 { - let a: F = RandomSampling::random(&mut rng); + let a: F = FieldCore::random(&mut rng); let b = rng.next_u64(); let expected = a * F::from_u64(b); let reduced = F::solinas_reduce(&a.mul_wide_u64(b)); @@ -31,8 +31,8 @@ fn mul_wide_u64_matches_full_mul() { fn mul_wide_matches_full_mul() { let mut rng = StdRng::seed_from_u64(0xaabb_ccdd_eeff_0011); for _ in 0..1000 { - let a: F = RandomSampling::random(&mut rng); - let b: F = RandomSampling::random(&mut rng); + let a: F = FieldCore::random(&mut rng); + let b: F = FieldCore::random(&mut rng); let expected = a * b; let reduced = F::solinas_reduce(&a.mul_wide(b)); assert_eq!(reduced, expected); @@ -43,9 +43,9 @@ fn mul_wide_matches_full_mul() { fn mul_add_matches_mul_then_add() { let mut rng = StdRng::seed_from_u64(0x3141_5926_5358_9793); for _ in 0..1000 { - let a: F = RandomSampling::random(&mut rng); - let b: F = RandomSampling::random(&mut rng); - let c: F = RandomSampling::random(&mut rng); + let a: F = FieldCore::random(&mut rng); + let b: F = FieldCore::random(&mut rng); + let c: F = FieldCore::random(&mut rng); assert_eq!(a.mul_add(b, c), a * b + c); } @@ -57,7 +57,7 @@ fn mul_add_matches_mul_then_add() { fn mul_wide_u128_matches_full_mul() { let mut rng = StdRng::seed_from_u64(0x9988_7766_5544_3322); for _ in 0..1000 { - let a: F = RandomSampling::random(&mut rng); + let a: F = FieldCore::random(&mut rng); let b = rng.next_u64() as u128 | ((rng.next_u64() as u128) << 64); let expected = a * F::from_canonical_u128_reduced(b); let reduced = F::solinas_reduce(&a.mul_wide_u128(b)); @@ -69,7 +69,7 @@ fn mul_wide_u128_matches_full_mul() { fn mul_wide_limbs_roundtrips_through_reduction() { let mut rng = StdRng::seed_from_u64(0x1bad_f00d_0ddc_afe1); for _ in 0..1000 { - let a: F = RandomSampling::random(&mut rng); + let a: F = FieldCore::random(&mut rng); let b3 = [rng.next_u64(), rng.next_u64(), rng.next_u64()]; let b4 = [ rng.next_u64(), @@ -129,7 +129,7 @@ fn solinas_reduce_accumulated_products() { let mut expected = F::zero(); for _ in 0..200 { - let a: F = RandomSampling::random(&mut rng); + let a: F = FieldCore::random(&mut rng); let b = rng.next_u64(); let wide = a.mul_wide_u64(b); @@ -185,8 +185,8 @@ fn prime128_offset_a7f7_mul_wide_matches_full_mul() { type G = Prime128OffsetA7F7; let mut rng = StdRng::seed_from_u64(0xa7f7_a7f7_a7f7_a7f7); for _ in 0..1000 { - let a: G = RandomSampling::random(&mut rng); - let b: G = RandomSampling::random(&mut rng); + let a: G = FieldCore::random(&mut rng); + let b: G = FieldCore::random(&mut rng); let expected = a * b; let reduced = G::solinas_reduce(&a.mul_wide(b)); assert_eq!(reduced, expected); diff --git a/crates/jolt-field/src/prime/fp128/traits.rs b/crates/jolt-field/src/prime/fp128/traits.rs index b2b6cec90a..70e685f431 100644 --- a/crates/jolt-field/src/prime/fp128/traits.rs +++ b/crates/jolt-field/src/prime/fp128/traits.rs @@ -77,7 +77,7 @@ impl<'a, const P: u128> Mul<&'a Self> for Fp128

{ } } -impl Invertible for Fp128

{ +impl FieldCore for Fp128

{ #[inline(always)] fn inverse(&self) -> Option { let inv = self.inv_or_zero(); @@ -97,18 +97,7 @@ impl Invertible for Fp128

{ let masked = to_u128(candidate.0) & mask; Self(from_u128(masked)) } -} -impl HalvingField for Fp128

{ - #[inline] - fn half(self) -> Self { - let x = to_u128(self.0); - let half = (x >> 1) + (x & 1) * ((P >> 1) + 1); - Self(from_u128(half)) - } -} - -impl RandomSampling for Fp128

{ #[inline(always)] fn random(rng: &mut R) -> Self { loop { @@ -122,6 +111,15 @@ impl RandomSampling for Fp128

{ } } +impl HalvingField for Fp128

{ + #[inline] + fn half(self) -> Self { + let x = to_u128(self.0); + let half = (x >> 1) + (x & 1) * ((P >> 1) + 1); + Self(from_u128(half)) + } +} + impl FromPrimitiveInt for Fp128

{ #[inline(always)] fn from_u64(val: u64) -> Self { @@ -178,3 +176,18 @@ impl PseudoMersenneField for Fp128

{ const MODULUS_BITS: u32 = 128; const MODULUS_OFFSET: u128 = Self::C; } + +impl serde::Serialize for Fp128

{ + fn serialize(&self, serializer: S) -> Result { + let buf = self.to_canonical_u128().to_le_bytes(); + <[u8; 16]>::serialize(&buf, serializer) + } +} + +impl<'de, const P: u128> serde::Deserialize<'de> for Fp128

{ + fn deserialize>(deserializer: D) -> Result { + let buf = <[u8; 16]>::deserialize(deserializer)?; + Self::from_canonical_u128_checked(u128::from_le_bytes(buf)) + .ok_or_else(|| serde::de::Error::custom("non-canonical Fp128 encoding")) + } +} diff --git a/crates/jolt-field/src/prime/fp32.rs b/crates/jolt-field/src/prime/fp32.rs index 0e09fb5296..8cf30ca55e 100644 --- a/crates/jolt-field/src/prime/fp32.rs +++ b/crates/jolt-field/src/prime/fp32.rs @@ -6,7 +6,7 @@ use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use crate::{FromPrimitiveInt, Invertible, RandomSampling}; +use crate::{FieldCore, FromPrimitiveInt}; use rand_core::RngCore; use crate::{CanonicalField, HalvingField, PseudoMersenneField}; @@ -122,7 +122,7 @@ impl Fp32

{ /// Multiplicative inverse, or `None` for zero. #[inline] pub fn inverse(&self) -> Option { - ::inverse(self) + ::inverse(self) } /// Construct from a `u64` reduced modulo the field modulus. @@ -355,7 +355,7 @@ impl<'a, const P: u32> Mul<&'a Self> for Fp32

{ } } -impl Invertible for Fp32

{ +impl FieldCore for Fp32

{ #[inline(always)] fn inverse(&self) -> Option { let inv = self.inv_or_zero(); @@ -373,6 +373,11 @@ impl Invertible for Fp32

{ let mask = 0u32.wrapping_sub(nz); Self(candidate.0 & mask) } + + #[inline(always)] + fn random(rng: &mut R) -> Self { + Self(Self::reduce_u64(rng.next_u64())) + } } impl HalvingField for Fp32

{ @@ -388,13 +393,6 @@ impl HalvingField for Fp32

{ } } -impl RandomSampling for Fp32

{ - #[inline(always)] - fn random(rng: &mut R) -> Self { - Self(Self::reduce_u64(rng.next_u64())) - } -} - impl FromPrimitiveInt for Fp32

{ #[inline(always)] fn from_u64(val: u64) -> Self { @@ -448,6 +446,21 @@ impl PseudoMersenneField for Fp32

{ const MODULUS_OFFSET: u128 = Self::C as u128; } +impl serde::Serialize for Fp32

{ + fn serialize(&self, serializer: S) -> Result { + let buf = (self.to_canonical_u128() as u32).to_le_bytes(); + <[u8; 4]>::serialize(&buf, serializer) + } +} + +impl<'de, const P: u32> serde::Deserialize<'de> for Fp32

{ + fn deserialize>(deserializer: D) -> Result { + let buf = <[u8; 4]>::deserialize(deserializer)?; + Self::from_canonical_u128_checked(u32::from_le_bytes(buf) as u128) + .ok_or_else(|| serde::de::Error::custom("non-canonical Fp32 encoding")) + } +} + #[cfg(test)] mod tests { use super::*; @@ -578,8 +591,8 @@ mod tests { fn mul_wide_matches_full_mul() { let mut rng = StdRng::seed_from_u64(0x1234_5678); for _ in 0..1000 { - let a: F = RandomSampling::random(&mut rng); - let b: F = RandomSampling::random(&mut rng); + let a: F = FieldCore::random(&mut rng); + let b: F = FieldCore::random(&mut rng); let expected = a * b; let reduced = F::solinas_reduce(a.mul_wide(b)); assert_eq!(reduced, expected); @@ -590,7 +603,7 @@ mod tests { fn mul_wide_u32_matches() { let mut rng = StdRng::seed_from_u64(0xabcd_ef01); for _ in 0..1000 { - let a: F = RandomSampling::random(&mut rng); + let a: F = FieldCore::random(&mut rng); let b = rng.next_u32() % 251; let expected = a * F::from_canonical_u32(b); let reduced = F::solinas_reduce(a.mul_wide_u32(b)); diff --git a/crates/jolt-field/src/prime/fp64.rs b/crates/jolt-field/src/prime/fp64.rs index e99a628791..33d97b41c0 100644 --- a/crates/jolt-field/src/prime/fp64.rs +++ b/crates/jolt-field/src/prime/fp64.rs @@ -6,7 +6,7 @@ use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use crate::{FromPrimitiveInt, Invertible, RandomSampling}; +use crate::{FieldCore, FromPrimitiveInt}; use rand_core::RngCore; use crate::{CanonicalField, HalvingField, PseudoMersenneField}; @@ -147,7 +147,7 @@ impl Fp64

{ /// Multiplicative inverse, or `None` for zero. #[inline] pub fn inverse(&self) -> Option { - ::inverse(self) + ::inverse(self) } /// Construct from a `u64` reduced modulo the field modulus. @@ -446,7 +446,7 @@ impl<'a, const P: u64> Mul<&'a Self> for Fp64

{ } } -impl Invertible for Fp64

{ +impl FieldCore for Fp64

{ #[inline(always)] fn inverse(&self) -> Option { let inv = self.inv_or_zero(); @@ -464,6 +464,13 @@ impl Invertible for Fp64

{ let mask = 0u64.wrapping_sub(nz); Self(candidate.0 & mask) } + + #[inline(always)] + fn random(rng: &mut R) -> Self { + let lo = rng.next_u64() as u128; + let hi = rng.next_u64() as u128; + Self(Self::reduce_u128(lo | (hi << 64))) + } } impl HalvingField for Fp64

{ @@ -474,15 +481,6 @@ impl HalvingField for Fp64

{ } } -impl RandomSampling for Fp64

{ - #[inline(always)] - fn random(rng: &mut R) -> Self { - let lo = rng.next_u64() as u128; - let hi = rng.next_u64() as u128; - Self(Self::reduce_u128(lo | (hi << 64))) - } -} - impl FromPrimitiveInt for Fp64

{ #[inline(always)] fn from_u64(val: u64) -> Self { @@ -536,6 +534,21 @@ impl PseudoMersenneField for Fp64

{ const MODULUS_OFFSET: u128 = Self::C as u128; } +impl serde::Serialize for Fp64

{ + fn serialize(&self, serializer: S) -> Result { + let buf = (self.to_canonical_u128() as u64).to_le_bytes(); + <[u8; 8]>::serialize(&buf, serializer) + } +} + +impl<'de, const P: u64> serde::Deserialize<'de> for Fp64

{ + fn deserialize>(deserializer: D) -> Result { + let buf = <[u8; 8]>::deserialize(deserializer)?; + Self::from_canonical_u128_checked(u64::from_le_bytes(buf) as u128) + .ok_or_else(|| serde::de::Error::custom("non-canonical Fp64 encoding")) + } +} + #[cfg(test)] mod tests { use super::*; @@ -581,8 +594,8 @@ mod tests { fn mul_wide_matches_full_mul() { let mut rng = StdRng::seed_from_u64(0xdead_beef); for _ in 0..1000 { - let a: F40 = RandomSampling::random(&mut rng); - let b: F40 = RandomSampling::random(&mut rng); + let a: F40 = FieldCore::random(&mut rng); + let b: F40 = FieldCore::random(&mut rng); let expected = a * b; let reduced = F40::solinas_reduce(a.mul_wide(b)); assert_eq!(reduced, expected); @@ -593,7 +606,7 @@ mod tests { fn mul_wide_u64_matches() { let mut rng = StdRng::seed_from_u64(0xcafe_d00d); for _ in 0..1000 { - let a: F40 = RandomSampling::random(&mut rng); + let a: F40 = FieldCore::random(&mut rng); let b = rng.next_u64() % ((1u64 << 40) - 195); let expected = a * F40::from_canonical_u64(b); let reduced = F40::solinas_reduce(a.mul_wide_u64(b)); diff --git a/crates/jolt-field/src/prime/native_algebra.rs b/crates/jolt-field/src/prime/native_algebra.rs index dbfab83675..08ba365996 100644 --- a/crates/jolt-field/src/prime/native_algebra.rs +++ b/crates/jolt-field/src/prime/native_algebra.rs @@ -4,7 +4,7 @@ //! These are the Jolt-free supertrait obligations of the native //! [`AdditiveGroup`]/[`RingCore`]/[`FieldCore`] hierarchy: //! `Zero`/`One`/`Display`/`Hash`/`Sum`/`Product` plus the empty algebra markers. -//! The non-trivial `RingCore::square` / `Invertible::inverse` impls stay +//! The non-trivial `FieldCore::inverse`/`FieldCore::random` impls stay //! co-located with each prime type. use std::fmt; @@ -14,7 +14,7 @@ use std::iter::{Product, Sum}; use num_traits::{One, Zero}; use super::{Fp128, Fp32, Fp64}; -use crate::{AdditiveGroup, CanonicalField, FieldCore, RingCore}; +use crate::{AdditiveGroup, CanonicalField, RingCore}; macro_rules! impl_prime_native_algebra { ($ty:ident<$p:ident: $p_ty:ty>, $canon:ident) => { @@ -79,7 +79,6 @@ macro_rules! impl_prime_native_algebra { impl AdditiveGroup for $ty<$p> {} impl RingCore for $ty<$p> {} - impl FieldCore for $ty<$p> {} }; } diff --git a/crates/jolt-field/src/prime/native_capability.rs b/crates/jolt-field/src/prime/native_capability.rs index ed20da8180..7fad6a2b08 100644 --- a/crates/jolt-field/src/prime/native_capability.rs +++ b/crates/jolt-field/src/prime/native_capability.rs @@ -1,8 +1,8 @@ -//! Native capability-trait impls for the prime fields: primitive-int -//! multiplication markers, the canonical byte/transcript surface, bit-length -//! introspection, and the `WithAccumulator` association (native `NaiveAccumulator`). +//! Native capability-trait impls for the prime fields: the canonical +//! byte/transcript surface and the `WithAccumulator` association (native +//! `NaiveAccumulator`). //! -//! `FromPrimitiveInt`/`RandomSampling` carry per-type logic and stay in the prime +//! `FromPrimitiveInt`/`FieldCore` carry per-type logic and stay in the prime //! modules; this module owns the shared derived-capability implementations used //! directly by both Jolt and Akita. @@ -10,31 +10,23 @@ use std::mem::size_of; use super::{Fp128, Fp32, Fp64}; use crate::{ - CanonicalBitLength, CanonicalBytes, CanonicalField, CanonicalU64, Field, FieldCore, - FixedByteSize, FixedBytes, FromPrimitiveInt, MulPow2, MulPrimitiveInt, NaiveAccumulator, - ReducingBytes, TranscriptChallenge, WithAccumulator, + CanonicalField, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, NaiveAccumulator, + WithAccumulator, }; macro_rules! impl_prime_native_capability { - ($ty:ident<$p:ident: $p_ty:ty>, $bytes:expr, $fixed_bytes:literal) => { - impl MulPow2 for $ty<$p> {} - impl MulPrimitiveInt for $ty<$p> {} - - impl FixedByteSize for $ty<$p> { + ($ty:ident<$p:ident: $p_ty:ty>, $bytes:expr) => { + impl CanonicalRepr for $ty<$p> { const NUM_BYTES: usize = $bytes; - } - impl CanonicalBytes for $ty<$p> { #[inline(always)] fn to_bytes_le(&self, out: &mut [u8]) { - assert_eq!(out.len(), ::NUM_BYTES); + assert_eq!(out.len(), ::NUM_BYTES); out.copy_from_slice( - &self.to_canonical_u128().to_le_bytes()[..::NUM_BYTES], + &self.to_canonical_u128().to_le_bytes()[..::NUM_BYTES], ); } - } - impl ReducingBytes for $ty<$p> { #[inline(always)] fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { if bytes.len() <= size_of::() { @@ -45,18 +37,12 @@ macro_rules! impl_prime_native_capability { reduce_le_bytes_mod_order(bytes) } - } - impl TranscriptChallenge for $ty<$p> { - #[inline(always)] - fn from_challenge_bytes(bytes: &[u8]) -> Self { - ::from_le_bytes_mod_order(bytes) + #[inline] + fn to_canonical_u64_checked(&self) -> Option { + self.to_canonical_u128().try_into().ok() } - } - impl FixedBytes<$fixed_bytes> for $ty<$p> {} - - impl CanonicalBitLength for $ty<$p> { #[inline] fn num_bits(&self) -> u32 { let value = self.to_canonical_u128(); @@ -64,13 +50,6 @@ macro_rules! impl_prime_native_capability { } } - impl CanonicalU64 for $ty<$p> { - #[inline] - fn to_canonical_u64_checked(&self) -> Option { - self.to_canonical_u128().try_into().ok() - } - } - impl WithAccumulator for $ty<$p> { type Accumulator = NaiveAccumulator; } @@ -80,7 +59,7 @@ macro_rules! impl_prime_native_capability { } /// Horner reduction of arbitrary-length little-endian bytes modulo the field -/// order (the >16-byte path of `ReducingBytes::from_le_bytes_mod_order`). +/// order (the >16-byte path of `CanonicalRepr::from_le_bytes_mod_order`). #[inline(always)] fn reduce_le_bytes_mod_order(bytes: &[u8]) -> F { let base = F::from_u64(256); @@ -89,9 +68,9 @@ fn reduce_le_bytes_mod_order(bytes: &[u8]) -> F }) } -impl_prime_native_capability!(Fp32, 4, 4); -impl_prime_native_capability!(Fp64, 8, 8); -impl_prime_native_capability!(Fp128, 16, 16); +impl_prime_native_capability!(Fp32, 4); +impl_prime_native_capability!(Fp64, 8); +impl_prime_native_capability!(Fp128, 16); #[cfg(test)] mod tests { @@ -100,35 +79,24 @@ mod tests { //! These exercise the Solinas backend directly, so they run under //! `--no-default-features --features solinas` as well as combined builds. use super::*; - use crate::Prime128Offset275; use crate::Accumulator; + use crate::Prime128Offset275; /// Asserts the full canonical byte round-trip on the native traits. fn assert_native_byte_roundtrip(value: F, expected: [u8; N]) where - F: CanonicalField - + CanonicalBytes - + ReducingBytes - + TranscriptChallenge - + FixedByteSize - + FixedBytes - + CanonicalBitLength - + CanonicalU64 - + std::fmt::Debug - + Eq, + F: CanonicalField + CanonicalRepr + std::fmt::Debug + Eq, { - assert_eq!(::NUM_BYTES, N); + assert_eq!(::NUM_BYTES, N); - // to_bytes_le (the audited method) into a correctly sized buffer, plus the - // array/vec convenience wrappers — all three must agree. + // to_bytes_le (the audited method) into a correctly sized buffer, plus + // the vec convenience wrapper — both must agree. let mut buf = [0u8; N]; value.to_bytes_le(&mut buf); assert_eq!(buf, expected); - assert_eq!(value.to_bytes_array(), expected); assert_eq!(value.to_bytes_le_vec(), expected.to_vec()); - // Reducing / fixed / challenge constructors all invert the encoding. - assert_eq!(F::from_bytes_array(&buf), value); + // Reducing / challenge constructors all invert the encoding. assert_eq!(F::from_le_bytes_mod_order(&buf), value); assert_eq!(F::from_challenge_bytes(&buf), value); diff --git a/crates/jolt-field/src/random_sampling.rs b/crates/jolt-field/src/random_sampling.rs deleted file mode 100644 index ea56da35fb..0000000000 --- a/crates/jolt-field/src/random_sampling.rs +++ /dev/null @@ -1,7 +0,0 @@ -use rand_core::RngCore; - -/// RNG-backed sampling for tests and witnesses. -pub trait RandomSampling { - /// Samples a random element. - fn random(rng: &mut R) -> Self; -} diff --git a/crates/jolt-field/src/reducing_bytes.rs b/crates/jolt-field/src/reducing_bytes.rs deleted file mode 100644 index 89d4302105..0000000000 --- a/crates/jolt-field/src/reducing_bytes.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// Reducing little-endian byte constructor. -pub trait ReducingBytes: Sized { - /// Deserializes little-endian bytes by reducing into this type. - fn from_le_bytes_mod_order(bytes: &[u8]) -> Self; -} diff --git a/crates/jolt-field/src/ring_core.rs b/crates/jolt-field/src/ring_core.rs deleted file mode 100644 index 5a540eb8de..0000000000 --- a/crates/jolt-field/src/ring_core.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::AdditiveGroup; -use num_traits::One; -use std::{ - fmt::{Debug, Display}, - hash::Hash, - iter::{Product, Sum}, - ops::{Mul, MulAssign}, -}; - -/// Core ring arithmetic: additive group plus multiplication and one. -pub trait RingCore: - AdditiveGroup - + One - + PartialEq - + Eq - + Default - + Debug - + Display - + Hash - + Mul - + for<'a> Mul<&'a Self, Output = Self> - + MulAssign - + Sum - + for<'a> Sum<&'a Self> - + Product - + for<'a> Product<&'a Self> -{ - /// Returns `self * self`. - #[inline] - fn square(&self) -> Self { - *self * *self - } - - #[inline] - fn pow2(exponent: usize) -> Self { - let mut result = Self::one(); - let mut base = Self::one() + Self::one(); - let mut remaining = exponent; - - while remaining > 0 { - if remaining % 2 == 1 { - result *= base; - } - remaining /= 2; - if remaining > 0 { - base = base.square(); - } - } - - result - } -} diff --git a/crates/jolt-field/src/transcript_challenge.rs b/crates/jolt-field/src/transcript_challenge.rs deleted file mode 100644 index 4fcdc15e8a..0000000000 --- a/crates/jolt-field/src/transcript_challenge.rs +++ /dev/null @@ -1,12 +0,0 @@ -/// Fiat-Shamir challenge decoding from squeezed transcript bytes. -pub trait TranscriptChallenge: - Sized + Copy + Default + PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Sync + Send + 'static -{ - /// Constructs a challenge from transcript bytes. - fn from_challenge_bytes(bytes: &[u8]) -> Self; - - /// Constructs a non-optimized scalar challenge from transcript bytes. - fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { - Self::from_challenge_bytes(bytes) - } -} diff --git a/crates/jolt-field/src/unreduced/tests.rs b/crates/jolt-field/src/unreduced/tests.rs index 730c60c898..ff918edd8e 100644 --- a/crates/jolt-field/src/unreduced/tests.rs +++ b/crates/jolt-field/src/unreduced/tests.rs @@ -4,7 +4,7 @@ )] use super::*; -use crate::RandomSampling; +use crate::FieldCore; use crate::{Prime128Offset275, Prime24Offset3, Prime40Offset195}; use rand::rngs::StdRng; use rand::SeedableRng; @@ -22,7 +22,7 @@ const P64: u64 = (1 << 40) - 195; fn fp128_roundtrip() { let mut rng = StdRng::seed_from_u64(0xdead_1234); for _ in 0..1000 { - let a: F128 = RandomSampling::random(&mut rng); + let a: F128 = FieldCore::random(&mut rng); let wide = Fp128x8i32::from(a); let back = wide.reduce::(); assert_eq!(a, back, "roundtrip failed for {a:?}"); @@ -33,7 +33,7 @@ fn fp128_roundtrip() { fn fp128_accumulate_matches_scalar() { let mut rng = StdRng::seed_from_u64(0xbeef_cafe_4321); let n = 1000; - let vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); let scalar_sum = vals.iter().fold(F128::zero(), |acc, &x| acc + x); @@ -49,8 +49,8 @@ fn fp128_accumulate_matches_scalar() { fn fp128_add_sub_neg_match_scalar() { let mut rng = StdRng::seed_from_u64(0x1122_3344_5566); for _ in 0..500 { - let a: F128 = RandomSampling::random(&mut rng); - let b: F128 = RandomSampling::random(&mut rng); + let a: F128 = FieldCore::random(&mut rng); + let b: F128 = FieldCore::random(&mut rng); let wa = Fp128x8i32::from(a); let wb = Fp128x8i32::from(b); @@ -65,7 +65,7 @@ fn fp128_add_sub_neg_match_scalar() { fn fp128_mixed_add_sub_stress() { let mut rng = StdRng::seed_from_u64(0xaaaa_bbbb_cccc); let n = 500; - let vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); let mut scalar = F128::zero(); let mut wide = Fp128x8i32::zero(); @@ -86,7 +86,7 @@ fn fp128_mixed_add_sub_stress() { fn fp32_roundtrip() { let mut rng = StdRng::seed_from_u64(0x3232_3232); for _ in 0..1000 { - let a: F32 = RandomSampling::random(&mut rng); + let a: F32 = FieldCore::random(&mut rng); let wide = Fp32x2i32::from(a); let back = wide.reduce::(); assert_eq!(a, back); @@ -97,7 +97,7 @@ fn fp32_roundtrip() { fn fp32_accumulate_matches_scalar() { let mut rng = StdRng::seed_from_u64(0x3232_abcd); let n = 1000; - let vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); let scalar_sum = vals.iter().fold(F32::zero(), |acc, &x| acc + x); let wide_sum = vals @@ -110,7 +110,7 @@ fn fp32_accumulate_matches_scalar() { fn fp64_roundtrip() { let mut rng = StdRng::seed_from_u64(0x6464_6464); for _ in 0..1000 { - let a: F64 = RandomSampling::random(&mut rng); + let a: F64 = FieldCore::random(&mut rng); let wide = Fp64x4i32::from(a); let back = wide.reduce::(); assert_eq!(a, back); @@ -121,7 +121,7 @@ fn fp64_roundtrip() { fn fp64_accumulate_matches_scalar() { let mut rng = StdRng::seed_from_u64(0x6464_beef); let n = 1000; - let vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); let scalar_sum = vals.iter().fold(F64::zero(), |acc, &x| acc + x); let wide_sum = vals @@ -134,8 +134,8 @@ fn fp64_accumulate_matches_scalar() { fn fp64_product_accum_matches_scalar() { let mut rng = StdRng::seed_from_u64(0x6464_4444); let n = 500; - let a_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); - let b_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let a_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); + let b_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); let scalar_sum: F64 = a_vals .iter() @@ -159,8 +159,8 @@ fn fp64_ext2_product_accum_matches_scalar() { let mut rng = StdRng::seed_from_u64(0x6464_4445); let n = 500; - let a_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); - let b_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let a_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); + let b_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); let scalar_sum: E = a_vals .iter() @@ -178,7 +178,7 @@ fn fp64_ext2_product_accum_matches_scalar() { fn fp64_mul_u64_accum_matches_scalar() { let mut rng = StdRng::seed_from_u64(0x6464_5555); let n = 500; - let a_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let a_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); let b_vals: Vec = (0..n).map(|_| rng.next_u64() >> 32).collect(); let scalar_sum: F64 = a_vals @@ -199,8 +199,8 @@ fn fp64_mul_u64_accum_matches_scalar() { fn fp128_product_accum_matches_scalar() { let mut rng = StdRng::seed_from_u64(0x0128_6666); let n = 500; - let a_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); - let b_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let a_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); + let b_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); let scalar_sum: F128 = a_vals .iter() @@ -220,7 +220,7 @@ fn fp128_product_accum_matches_scalar() { fn fp128_mul_u64_accum_matches_scalar() { let mut rng = StdRng::seed_from_u64(0x0128_7777); let n = 500; - let a_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let a_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); let b_vals: Vec = (0..n).map(|_| rng.next_u64()).collect(); let scalar_sum: F128 = a_vals @@ -241,8 +241,8 @@ fn fp128_mul_u64_accum_matches_scalar() { fn fp128_product_accum_sub_neg() { let mut rng = StdRng::seed_from_u64(0x0128_8888); let n = 500; - let a_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); - let b_vals: Vec = (0..n).map(|_| RandomSampling::random(&mut rng)).collect(); + let a_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); + let b_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); let mut scalar_sum = F128::zero(); let mut accum_pos = Fp128ProductAccum::ZERO; diff --git a/crates/jolt-field/tests/binary_field_core_compat.rs b/crates/jolt-field/tests/binary_field_core_compat.rs index a6a9787152..414f8f8b23 100644 --- a/crates/jolt-field/tests/binary_field_core_compat.rs +++ b/crates/jolt-field/tests/binary_field_core_compat.rs @@ -12,9 +12,7 @@ use std::{ ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}, }; -use jolt_field::{ - AdditiveGroup, CanonicalBytes, FieldCore, FixedByteSize, Invertible, ReducingBytes, RingCore, -}; +use jolt_field::{AdditiveGroup, CanonicalRepr, FieldCore, RingCore}; use num_traits::{One, Zero}; #[derive(Clone, Copy, Default, PartialEq, Eq, Hash)] @@ -156,7 +154,7 @@ impl<'a> Product<&'a Gf2> for Gf2 { impl AdditiveGroup for Gf2 {} impl RingCore for Gf2 {} -impl Invertible for Gf2 { +impl FieldCore for Gf2 { fn inverse(&self) -> Option { if self.is_zero() { None @@ -164,28 +162,34 @@ impl Invertible for Gf2 { Some(Self::one()) } } + + fn random(rng: &mut R) -> Self { + Self(rng.next_u32() & 1 == 1) + } } -impl FieldCore for Gf2 {} +impl CanonicalRepr for Gf2 { + const NUM_BYTES: usize = 1; -impl CanonicalBytes for Gf2 { fn to_bytes_le(&self, out: &mut [u8]) { assert_eq!(out.len(), 1); out[0] = self.0 as u8; } -} -impl ReducingBytes for Gf2 { fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { Self(bytes.iter().fold(0u8, |acc, b| acc ^ (b & 1)) == 1) } -} -impl FixedByteSize for Gf2 { - const NUM_BYTES: usize = 1; + fn to_canonical_u64_checked(&self) -> Option { + Some(self.0 as u64) + } + + fn num_bits(&self) -> u32 { + self.0 as u32 + } } -fn accepts_field_core(x: F) -> F { +fn accepts_field_core(x: F) -> F { x.square() } diff --git a/crates/jolt-field/tests/coverage.rs b/crates/jolt-field/tests/coverage.rs index 81cc5b6c59..89c34ec653 100644 --- a/crates/jolt-field/tests/coverage.rs +++ b/crates/jolt-field/tests/coverage.rs @@ -8,8 +8,8 @@ use ark_std::test_rng; use jolt_field::signed::*; use jolt_field::{ - Accumulator, FixedBytes, Fr, FromPrimitiveInt, Limbs, MulPow2, NaiveAccumulator, - OptimizedMul, RandomSampling, + Accumulator, CanonicalRepr, FieldCore, Fr, FromPrimitiveInt, Limbs, NaiveAccumulator, + OptimizedMul, }; use num_traits::{One, Zero}; @@ -101,8 +101,8 @@ fn wide_accumulator_many_fmadds() { let mut expected = Fr::zero(); let mut rng = test_rng(); for _ in 0..500 { - let a: Fr = ::random(&mut rng); - let b: Fr = ::random(&mut rng); + let a: Fr = ::random(&mut rng); + let b: Fr = ::random(&mut rng); acc.fmadd(a, b); expected += a * b; } @@ -112,8 +112,8 @@ fn wide_accumulator_many_fmadds() { #[test] fn optimized_mul_blanket_impl() { let mut rng = test_rng(); - let a: Fr = ::random(&mut rng); - let b: Fr = ::random(&mut rng); + let a: Fr = ::random(&mut rng); + let b: Fr = ::random(&mut rng); // mul_0_optimized: both nonzero assert_eq!(a.mul_0_optimized(b), a * b); @@ -174,14 +174,14 @@ fn field_from_small_types_boundary() { fn field_mul_pow_2_boundary() { let f = ::from_u64(1); // pow=0 -> f * 1 = f - assert_eq!(::mul_pow_2(&f, 0), f); + assert_eq!(::mul_pow_2(&f, 0), f); // pow=1 -> f * 2 assert_eq!( - ::mul_pow_2(&f, 1), + ::mul_pow_2(&f, 1), ::from_u64(2) ); // pow=64 -> goes through while loop at least once - let result = ::mul_pow_2(&f, 64); + let result = ::mul_pow_2(&f, 64); let mut expected = f; for _ in 0..64 { expected = expected + expected; @@ -193,7 +193,7 @@ fn field_mul_pow_2_boundary() { #[should_panic(expected = "pow > 255")] fn field_mul_pow_2_overflow() { let f = ::from_u64(1); - let _ = ::mul_pow_2(&f, 256); + let _ = ::mul_pow_2(&f, 256); } #[test] @@ -957,8 +957,8 @@ fn fr_neg() { fn fr_inner_roundtrip() { // Test Fr(ark_bn254::Fr) -> ark_bn254::Fr conversion (inner type) let a = Fr::from_u64(12345); - let bytes = a.to_bytes_array(); - let b = Fr::from_bytes_array(&bytes); + let bytes = a.to_bytes_le_vec(); + let b = ::from_le_bytes_mod_order(&bytes); assert_eq!(a, b); } diff --git a/crates/jolt-field/tests/field_operations.rs b/crates/jolt-field/tests/field_operations.rs index 2f56bd304c..5fb166621e 100644 --- a/crates/jolt-field/tests/field_operations.rs +++ b/crates/jolt-field/tests/field_operations.rs @@ -2,9 +2,7 @@ use ark_std::rand::Rng; use ark_std::{test_rng, One, Zero}; -use jolt_field::{ - CanonicalU64, Fr, FromPrimitiveInt, MulPow2, MulPrimitiveInt, RandomSampling, ReducingBytes, -}; +use jolt_field::{CanonicalRepr, FieldCore, Fr, FromPrimitiveInt}; use rand_chacha::rand_core::RngCore; #[test] @@ -21,7 +19,7 @@ fn implicit_montgomery_conversion() { for _ in 0..256 { let x = rng.next_u64(); - let y: Fr = ::random(&mut rng); + let y: Fr = ::random(&mut rng); assert_eq!( y * ::from_u64(x), y * ::from_u64(x) @@ -90,7 +88,7 @@ fn bytes_conversion() { for &len in &[1, 8, 16, 32, 48, 64] { let mut bytes = vec![0u8; len]; rng.fill_bytes(&mut bytes); - let _field_elem = ::from_le_bytes_mod_order(&bytes); + let _field_elem = ::from_le_bytes_mod_order(&bytes); } } @@ -132,11 +130,11 @@ fn mul_u64_method() { let mut rng = test_rng(); for _ in 0..100 { - let field_elem: Fr = ::random(&mut rng); + let field_elem: Fr = ::random(&mut rng); let n = rng.next_u64(); // Use UFCS to call trait method (arkworks has inherent mul_u64 with different signature) - let result = ::mul_u64(&field_elem, n); + let result = ::mul_u64(&field_elem, n); let expected = field_elem * ::from_u64(n); assert_eq!(result, expected); } @@ -147,10 +145,10 @@ fn mul_i64_method() { let mut rng = test_rng(); for _ in 0..100 { - let field_elem: Fr = ::random(&mut rng); + let field_elem: Fr = ::random(&mut rng); let n = rng.gen::(); - let result = ::mul_i64(&field_elem, n); + let result = ::mul_i64(&field_elem, n); let expected = field_elem * ::from_i64(n); assert_eq!(result, expected); } @@ -161,10 +159,10 @@ fn mul_u128_method() { let mut rng = test_rng(); for _ in 0..100 { - let field_elem: Fr = ::random(&mut rng); + let field_elem: Fr = ::random(&mut rng); let n = rng.gen::(); - let result = ::mul_u128(&field_elem, n); + let result = ::mul_u128(&field_elem, n); let expected = field_elem * ::from_u128(n); assert_eq!(result, expected); } @@ -175,10 +173,10 @@ fn mul_i128_method() { let mut rng = test_rng(); for _ in 0..100 { - let field_elem: Fr = ::random(&mut rng); + let field_elem: Fr = ::random(&mut rng); let n = rng.gen::(); - let result = ::mul_i128(&field_elem, n); + let result = ::mul_i128(&field_elem, n); let expected = field_elem * ::from_i128(n); assert_eq!(result, expected); } @@ -189,10 +187,10 @@ fn mul_pow_2_method() { let mut rng = test_rng(); for _ in 0..10 { - let field_elem: Fr = ::random(&mut rng); + let field_elem: Fr = ::random(&mut rng); for pow in [0, 1, 2, 7, 16, 32, 63, 64, 127, 128, 255] { - let result = ::mul_pow_2(&field_elem, pow); + let result = ::mul_pow_2(&field_elem, pow); let mut expected = field_elem; for _ in 0..pow { expected = expected + expected; @@ -207,7 +205,7 @@ fn mul_by_small_values() { let mut rng = test_rng(); for _ in 0..100 { - let field_elem: Fr = ::random(&mut rng); + let field_elem: Fr = ::random(&mut rng); let small_val = rng.gen_range(0u64..1000); let result1 = field_elem * ::from_u64(small_val); @@ -224,7 +222,7 @@ fn mul_by_small_values() { #[test] fn special_values() { let mut rng = test_rng(); - let field_elem: Fr = ::random(&mut rng); + let field_elem: Fr = ::random(&mut rng); assert_eq!( field_elem * ::from_u64(0), @@ -236,10 +234,16 @@ fn special_values() { ); assert!((Fr::zero() * ::from_u64(rng.next_u64())).is_zero()); - assert_eq!(::mul_u64(&field_elem, 0), Fr::zero()); - assert_eq!(::mul_u64(&field_elem, 1), field_elem); assert_eq!( - ::mul_u64(&Fr::zero(), 42), + ::mul_u64(&field_elem, 0), + Fr::zero() + ); + assert_eq!( + ::mul_u64(&field_elem, 1), + field_elem + ); + assert_eq!( + ::mul_u64(&Fr::zero(), 42), Fr::zero() ); } @@ -252,6 +256,6 @@ fn to_u64_conversion() { } let mut rng = test_rng(); - let large_field: Fr = ::random(&mut rng); + let large_field: Fr = ::random(&mut rng); let _ = large_field.to_canonical_u64_checked(); } diff --git a/crates/jolt-field/tests/serde_roundtrip.rs b/crates/jolt-field/tests/serde_roundtrip.rs new file mode 100644 index 0000000000..4566d4b4f9 --- /dev/null +++ b/crates/jolt-field/tests/serde_roundtrip.rs @@ -0,0 +1,114 @@ +//! Wire-format guarantees for the Solinas types: bincode round-trips, exact +//! per-element sizes (`NUM_BYTES`, no per-element overhead), and rejection of +//! non-canonical encodings. +#![cfg(feature = "solinas")] +#![expect(clippy::unwrap_used)] + +use jolt_field::{ + CanonicalRepr, Ext2, FieldCore, FpExt4, FpExt8, Prime128Offset275, Prime32Offset99, + Prime64Offset59, +}; +use rand::rngs::StdRng; +use rand::SeedableRng; +use serde::de::DeserializeOwned; +use serde::Serialize; + +type F32 = Prime32Offset99; +type F64 = Prime64Offset59; +type F128 = Prime128Offset275; + +fn assert_roundtrip_with_size(value: &T, expected_len: usize) +where + T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug, +{ + let bytes = bincode::serde::encode_to_vec(value, bincode::config::standard()).unwrap(); + assert_eq!( + bytes.len(), + expected_len, + "serialized size must be exactly the canonical byte length" + ); + let (decoded, consumed): (T, usize) = + bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap(); + assert_eq!(consumed, bytes.len()); + assert_eq!(&decoded, value); +} + +#[test] +fn prime_field_elements_encode_to_num_bytes() { + let mut rng = StdRng::seed_from_u64(7); + for _ in 0..32 { + assert_roundtrip_with_size(&F32::random(&mut rng), ::NUM_BYTES); + assert_roundtrip_with_size(&F64::random(&mut rng), ::NUM_BYTES); + assert_roundtrip_with_size(&F128::random(&mut rng), ::NUM_BYTES); + } +} + +#[test] +fn extension_field_elements_encode_to_num_coeffs_times_num_bytes() { + let mut rng = StdRng::seed_from_u64(8); + for _ in 0..16 { + assert_roundtrip_with_size(&Ext2::::random(&mut rng), 2 * 8); + assert_roundtrip_with_size(&FpExt4::::random(&mut rng), 4 * 4); + assert_roundtrip_with_size(&FpExt8::::random(&mut rng), 8 * 4); + } +} + +#[test] +fn vectors_add_only_a_single_length_prefix() { + let mut rng = StdRng::seed_from_u64(9); + for n in [0usize, 1, 17, 200] { + let v: Vec = (0..n).map(|_| F64::random(&mut rng)).collect(); + let bytes = bincode::serde::encode_to_vec(&v, bincode::config::standard()).unwrap(); + // bincode's standard config uses a varint length prefix: 1 byte for + // lengths below 251. + let prefix = if n < 251 { 1 } else { 3 }; + assert_eq!(bytes.len(), prefix + n * ::NUM_BYTES); + let (decoded, _): (Vec, usize) = + bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap(); + assert_eq!(decoded, v); + } +} + +#[test] +fn non_canonical_encodings_are_rejected() { + // The modulus itself is not a canonical representative. + let p32: u32 = u32::MAX - 98; + let bytes = + bincode::serde::encode_to_vec(p32.to_le_bytes(), bincode::config::standard()).unwrap(); + assert!( + bincode::serde::decode_from_slice::(&bytes, bincode::config::standard()).is_err() + ); + + let p64: u64 = u64::MAX - 58; + let bytes = + bincode::serde::encode_to_vec(p64.to_le_bytes(), bincode::config::standard()).unwrap(); + assert!( + bincode::serde::decode_from_slice::(&bytes, bincode::config::standard()).is_err() + ); + + let p128: u128 = u128::MAX - 274; + let bytes = + bincode::serde::encode_to_vec(p128.to_le_bytes(), bincode::config::standard()).unwrap(); + assert!( + bincode::serde::decode_from_slice::(&bytes, bincode::config::standard()).is_err() + ); +} + +#[test] +fn canonical_transcript_bytes_and_serde_bytes_agree_for_prime_fields() { + // For the prime fields both encodings are the canonical little-endian + // representative; pin that so neither drifts. + let mut rng = StdRng::seed_from_u64(10); + for _ in 0..16 { + let x = F128::random(&mut rng); + let wire = bincode::serde::encode_to_vec(x, bincode::config::standard()).unwrap(); + assert_eq!(wire, x.to_bytes_le_vec()); + } +} + +#[test] +fn from_u64_sanity() { + let x = F32::from_u64(42); + let bytes = bincode::serde::encode_to_vec(x, bincode::config::standard()).unwrap(); + assert_eq!(bytes, 42u32.to_le_bytes()); +} diff --git a/crates/jolt-hyperkzg/benches/hyperkzg.rs b/crates/jolt-hyperkzg/benches/hyperkzg.rs index c4d9cb8caa..ddc041a06d 100644 --- a/crates/jolt-hyperkzg/benches/hyperkzg.rs +++ b/crates/jolt-hyperkzg/benches/hyperkzg.rs @@ -6,7 +6,7 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use jolt_crypto::Bn254; -use jolt_field::{Fr, RandomSampling}; +use jolt_field::{FieldCore, Fr}; use jolt_hyperkzg::{HyperKZGProverSetup, HyperKZGScheme, HyperKZGVerifierSetup}; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme}; use jolt_poly::Polynomial; diff --git a/crates/jolt-hyperkzg/src/scheme.rs b/crates/jolt-hyperkzg/src/scheme.rs index a742d3143d..5568362246 100644 --- a/crates/jolt-hyperkzg/src/scheme.rs +++ b/crates/jolt-hyperkzg/src/scheme.rs @@ -11,7 +11,7 @@ use std::marker::PhantomData; use jolt_crypto::{Commitment, DeriveSetup, JoltGroup, PairingGroup, PedersenSetup}; -use jolt_field::{FromPrimitiveInt, RandomSampling}; +use jolt_field::{FieldCore, FromPrimitiveInt}; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, OpeningsError}; use jolt_poly::MultilinearPoly; use jolt_transcript::{AppendToTranscript, Transcript}; diff --git a/crates/jolt-hyperkzg/tests/commit_open_verify.rs b/crates/jolt-hyperkzg/tests/commit_open_verify.rs index f440da7362..24d40d55e4 100644 --- a/crates/jolt-hyperkzg/tests/commit_open_verify.rs +++ b/crates/jolt-hyperkzg/tests/commit_open_verify.rs @@ -7,7 +7,7 @@ )] use jolt_crypto::Bn254; -use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; +use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use jolt_hyperkzg::{HyperKZGProverSetup, HyperKZGScheme, HyperKZGVerifierSetup}; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme}; use jolt_poly::Polynomial; diff --git a/crates/jolt-openings/tests/packing.rs b/crates/jolt-openings/tests/packing.rs index 90d6f7998d..d5c977412e 100644 --- a/crates/jolt-openings/tests/packing.rs +++ b/crates/jolt-openings/tests/packing.rs @@ -1,6 +1,6 @@ #![expect(clippy::expect_used, reason = "tests may panic on assertion failures")] -use jolt_field::{Fr, RandomSampling}; +use jolt_field::{FieldCore, Fr}; use jolt_openings::{OpeningsError, PrefixPacking}; use jolt_poly::{boolean_point_msb, eq_index_msb, Polynomial}; use rand_chacha::ChaCha20Rng; diff --git a/crates/jolt-openings/tests/support/common.rs b/crates/jolt-openings/tests/support/common.rs index 1d47701381..4f8af2a95a 100644 --- a/crates/jolt-openings/tests/support/common.rs +++ b/crates/jolt-openings/tests/support/common.rs @@ -1,5 +1,5 @@ use jolt_crypto::Bn254; -use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; +use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use jolt_hyperkzg::{HyperKZGProverSetup, HyperKZGScheme, HyperKZGVerifierSetup}; use jolt_openings::{CommitmentScheme, EvaluationClaim, VerifierOpeningClaim}; use jolt_poly::{MultilinearPoly, Point, Polynomial, HIGH_TO_LOW}; diff --git a/crates/jolt-openings/tests/support/packed.rs b/crates/jolt-openings/tests/support/packed.rs index 922336fcab..ddfd5a8647 100644 --- a/crates/jolt-openings/tests/support/packed.rs +++ b/crates/jolt-openings/tests/support/packed.rs @@ -1,4 +1,4 @@ -use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; +use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use jolt_openings::{EvaluationClaim, OpeningsError, PrefixPacking}; use jolt_poly::Polynomial; use rand_chacha::ChaCha20Rng; diff --git a/crates/jolt-poly/benches/poly_ops.rs b/crates/jolt-poly/benches/poly_ops.rs index 51ace32b25..f9b089e2ab 100644 --- a/crates/jolt-poly/benches/poly_ops.rs +++ b/crates/jolt-poly/benches/poly_ops.rs @@ -1,7 +1,7 @@ #![expect(unused_results)] use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use jolt_field::{Fr, RandomSampling}; +use jolt_field::{FieldCore, Fr}; use jolt_poly::{EqPolynomial, Polynomial}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/fuzz/fuzz_targets/dense_poly_ops.rs b/crates/jolt-poly/fuzz/fuzz_targets/dense_poly_ops.rs index 3ffb0d46d3..a6372ea6a0 100644 --- a/crates/jolt-poly/fuzz/fuzz_targets/dense_poly_ops.rs +++ b/crates/jolt-poly/fuzz/fuzz_targets/dense_poly_ops.rs @@ -1,5 +1,5 @@ #![no_main] -use jolt_field::{Fr, ReducingBytes}; +use jolt_field::{Fr, CanonicalRepr}; use jolt_poly::Polynomial; use libfuzzer_sys::fuzz_target; @@ -20,7 +20,7 @@ fuzz_target!(|data: &[u8]| { // Build evaluation vector from fuzzer data let evals: Vec = (0..n) - .map(|i| ::from_le_bytes_mod_order(&data[i * 32..(i + 1) * 32])) + .map(|i| ::from_le_bytes_mod_order(&data[i * 32..(i + 1) * 32])) .collect(); let poly = Polynomial::new(evals); @@ -28,7 +28,7 @@ fuzz_target!(|data: &[u8]| { let point_start = n * 32; let point: Vec = (0..num_vars) .map(|i| { - ::from_le_bytes_mod_order( + ::from_le_bytes_mod_order( &data[point_start + i * 32..point_start + (i + 1) * 32], ) }) diff --git a/crates/jolt-poly/src/dense.rs b/crates/jolt-poly/src/dense.rs index 93db8eecfe..6d474ce9ef 100644 --- a/crates/jolt-poly/src/dense.rs +++ b/crates/jolt-poly/src/dense.rs @@ -580,7 +580,7 @@ impl Neg for Polynomial { mod tests { use super::*; use jolt_field::Fr; - use jolt_field::{FromPrimitiveInt, RandomSampling}; + use jolt_field::{FieldCore, FromPrimitiveInt}; use num_traits::{One, Zero}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/src/eq.rs b/crates/jolt-poly/src/eq.rs index 206bf82908..cf47672ac4 100644 --- a/crates/jolt-poly/src/eq.rs +++ b/crates/jolt-poly/src/eq.rs @@ -476,7 +476,7 @@ impl crate::MultilinearEvaluation for EqPolynomial { mod tests { use super::*; use jolt_field::Fr; - use jolt_field::{FromPrimitiveInt, RandomSampling}; + use jolt_field::{FieldCore, FromPrimitiveInt}; use num_traits::{One, Zero}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/src/eq_plus_one.rs b/crates/jolt-poly/src/eq_plus_one.rs index e05cb69ded..99eb8b06f2 100644 --- a/crates/jolt-poly/src/eq_plus_one.rs +++ b/crates/jolt-poly/src/eq_plus_one.rs @@ -170,7 +170,7 @@ impl EqPlusOnePrefixSuffix { #[cfg(test)] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; + use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use num_traits::{One, Zero}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/src/lt.rs b/crates/jolt-poly/src/lt.rs index 1b50547463..fe92c61c28 100644 --- a/crates/jolt-poly/src/lt.rs +++ b/crates/jolt-poly/src/lt.rs @@ -169,7 +169,7 @@ fn bind_in_place(v: &mut Vec, challenge: F) { #[cfg(test)] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; + use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use num_traits::{One, Zero}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/src/multilinear.rs b/crates/jolt-poly/src/multilinear.rs index 4856e87072..cf9e3ec9ce 100644 --- a/crates/jolt-poly/src/multilinear.rs +++ b/crates/jolt-poly/src/multilinear.rs @@ -465,7 +465,7 @@ impl> MultilinearPoly for RlcSource { #[cfg(test)] mod tests { use super::*; - use jolt_field::{Fr, RandomSampling}; + use jolt_field::{FieldCore, Fr}; use num_traits::Zero; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/src/one_hot.rs b/crates/jolt-poly/src/one_hot.rs index 7d450ff4f6..c6c3aaef89 100644 --- a/crates/jolt-poly/src/one_hot.rs +++ b/crates/jolt-poly/src/one_hot.rs @@ -204,7 +204,7 @@ impl MultilinearPoly for OneHotPolynomial { mod tests { use super::*; use crate::Polynomial; - use jolt_field::{Fr, RandomSampling}; + use jolt_field::{FieldCore, Fr}; use num_traits::Zero; use rand_chacha::ChaCha20Rng; use rand_core::{RngCore, SeedableRng}; diff --git a/crates/jolt-poly/src/split_eq.rs b/crates/jolt-poly/src/split_eq.rs index 7f5617dbc6..c1300debc0 100644 --- a/crates/jolt-poly/src/split_eq.rs +++ b/crates/jolt-poly/src/split_eq.rs @@ -495,7 +495,7 @@ impl GruenSplitEqPolynomial { #[cfg(test)] mod tests { - use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; + use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/tests/integration.rs b/crates/jolt-poly/tests/integration.rs index f9c5981c1e..5ab0f61798 100644 --- a/crates/jolt-poly/tests/integration.rs +++ b/crates/jolt-poly/tests/integration.rs @@ -5,7 +5,7 @@ //! (Polynomial, EqPolynomial, UnivariatePoly, IdentityPolynomial, RlcSource) //! that are used throughout the proving system. -use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; +use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use jolt_poly::{ EqPolynomial, IdentityPolynomial, MultilinearEvaluation, MultilinearPoly, Polynomial, RlcSource, UnivariatePoly, diff --git a/crates/jolt-r1cs/src/constraints/field_constraints.rs b/crates/jolt-r1cs/src/constraints/field_constraints.rs index 8cc581b36d..e378a922fc 100644 --- a/crates/jolt-r1cs/src/constraints/field_constraints.rs +++ b/crates/jolt-r1cs/src/constraints/field_constraints.rs @@ -176,7 +176,7 @@ pub fn field_inline_trace_constraints() -> crate::ConstraintMatrices Vec { diff --git a/crates/jolt-r1cs/src/key.rs b/crates/jolt-r1cs/src/key.rs index 284db04e13..b097575afb 100644 --- a/crates/jolt-r1cs/src/key.rs +++ b/crates/jolt-r1cs/src/key.rs @@ -280,7 +280,7 @@ impl R1csKey { mod tests { use super::*; use crate::constraint::ConstraintMatrices; - use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; + use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; use num_traits::{One, Zero}; /// x * x = y, y * x = z — 2 constraints, 4 vars [1, x, y, z] diff --git a/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs b/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs index 1b0473ce96..d735da97d0 100644 --- a/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs +++ b/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs @@ -8,7 +8,7 @@ #![no_main] -use jolt_field::{Fr, ReducingBytes}; +use jolt_field::{Fr, CanonicalRepr}; use jolt_poly::UnivariatePoly; use jolt_sumcheck::{BooleanHypercube, SumcheckClaim, SumcheckVerifier}; use jolt_transcript::{Blake2bTranscript, Transcript}; @@ -72,5 +72,5 @@ fuzz_target!(|data: &[u8]| { #[inline] fn read_scalar(bytes: &[u8]) -> Fr { debug_assert_eq!(bytes.len(), SCALAR_BYTES); - ::from_le_bytes_mod_order(bytes) + ::from_le_bytes_mod_order(bytes) } diff --git a/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs b/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs index 9532b07085..98708458fe 100644 --- a/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs +++ b/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs @@ -22,7 +22,7 @@ #![no_main] -use jolt_field::{Fr, ReducingBytes}; +use jolt_field::{Fr, CanonicalRepr}; use jolt_poly::UnivariatePoly; use jolt_sumcheck::{BooleanHypercube, SumcheckClaim, SumcheckVerifier}; use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; @@ -138,5 +138,5 @@ fuzz_target!(|data: &[u8]| { #[inline] fn read_scalar(bytes: &[u8]) -> Fr { debug_assert_eq!(bytes.len(), SCALAR_BYTES); - ::from_le_bytes_mod_order(bytes) + ::from_le_bytes_mod_order(bytes) } diff --git a/crates/jolt-sumcheck/src/scalar.rs b/crates/jolt-sumcheck/src/scalar.rs index 761a833ffe..0a2848f38c 100644 --- a/crates/jolt-sumcheck/src/scalar.rs +++ b/crates/jolt-sumcheck/src/scalar.rs @@ -3,18 +3,16 @@ use std::{ hash::Hash, }; -use jolt_field::{ - CanonicalBytes, FieldCore, FixedByteSize, FromPrimitiveInt, MulPow2, TranscriptChallenge, -}; +use jolt_field::{CanonicalRepr, FieldCore, FromPrimitiveInt}; /// Scalar capabilities used by the verifier-side sumcheck crate. pub trait SumcheckScalar: FieldCore + FromPrimitiveInt - + MulPow2 - + CanonicalBytes - + FixedByteSize - + TranscriptChallenge + + FromPrimitiveInt + + CanonicalRepr + + CanonicalRepr + + CanonicalRepr + Copy + Default + Eq @@ -30,10 +28,10 @@ pub trait SumcheckScalar: impl SumcheckScalar for F where F: FieldCore + FromPrimitiveInt - + MulPow2 - + CanonicalBytes - + FixedByteSize - + TranscriptChallenge + + FromPrimitiveInt + + CanonicalRepr + + CanonicalRepr + + CanonicalRepr + Copy + Default + Eq diff --git a/crates/jolt-sumcheck/tests/mersenne61_compat.rs b/crates/jolt-sumcheck/tests/mersenne61_compat.rs index 5926c27574..849d776824 100644 --- a/crates/jolt-sumcheck/tests/mersenne61_compat.rs +++ b/crates/jolt-sumcheck/tests/mersenne61_compat.rs @@ -15,9 +15,8 @@ use std::{ }; use jolt_field::{ - AdditiveGroup, CanonicalBitLength, CanonicalBytes, CanonicalU64, FieldCore, FixedByteSize, - FixedBytes, FromPrimitiveInt, Invertible, MulPow2, MulPrimitiveInt, NaiveAccumulator, - RandomSampling, ReducingBytes, RingCore, TranscriptChallenge, WithAccumulator, + AdditiveGroup, CanonicalRepr, FieldCore, FromPrimitiveInt, NaiveAccumulator, RingCore, + WithAccumulator, }; use jolt_sumcheck::{ BooleanHypercube, ClearRound, EvaluationClaim, RoundMessage, SumcheckClaim, SumcheckVerifier, @@ -200,7 +199,7 @@ impl<'a> Product<&'a Mersenne61> for Mersenne61 { impl AdditiveGroup for Mersenne61 {} impl RingCore for Mersenne61 {} -impl Invertible for Mersenne61 { +impl FieldCore for Mersenne61 { fn inverse(&self) -> Option { if self.is_zero() { None @@ -208,9 +207,11 @@ impl Invertible for Mersenne61 { Some(self.pow(MODULUS - 2)) } } -} -impl FieldCore for Mersenne61 {} + fn random(rng: &mut R) -> Self { + Self::from_u64(rng.next_u64()) + } +} impl FromPrimitiveInt for Mersenne61 { fn from_u64(v: u64) -> Self { @@ -238,59 +239,34 @@ impl FromPrimitiveInt for Mersenne61 { } } -impl RandomSampling for Mersenne61 { - fn random(rng: &mut R) -> Self { - Self::from_u64(rng.next_u64()) - } -} +impl CanonicalRepr for Mersenne61 { + const NUM_BYTES: usize = 8; -impl CanonicalBytes for Mersenne61 { fn to_bytes_le(&self, out: &mut [u8]) { assert_eq!(out.len(), 8); out.copy_from_slice(&self.0.to_le_bytes()); } -} -impl ReducingBytes for Mersenne61 { fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { let mut buf = [0u8; 16]; let len = bytes.len().min(16); buf[..len].copy_from_slice(&bytes[..len]); Self::from_u128(u128::from_le_bytes(buf)) } -} -impl TranscriptChallenge for Mersenne61 { - fn from_challenge_bytes(bytes: &[u8]) -> Self { - Self::from_le_bytes_mod_order(bytes) + fn to_canonical_u64_checked(&self) -> Option { + Some(self.0) } -} - -impl FixedByteSize for Mersenne61 { - const NUM_BYTES: usize = 8; -} - -impl FixedBytes<8> for Mersenne61 {} -impl CanonicalBitLength for Mersenne61 { fn num_bits(&self) -> u32 { u64::BITS - self.0.leading_zeros() } } -impl CanonicalU64 for Mersenne61 { - fn to_canonical_u64_checked(&self) -> Option { - Some(self.0) - } -} - impl WithAccumulator for Mersenne61 { type Accumulator = NaiveAccumulator; } -impl MulPow2 for Mersenne61 {} -impl MulPrimitiveInt for Mersenne61 {} - #[derive(Clone, Debug)] struct LinearRound { coeffs: [Mersenne61; 2], diff --git a/crates/jolt-transcript/src/digest.rs b/crates/jolt-transcript/src/digest.rs index 2a47fef0bc..c7c9246629 100644 --- a/crates/jolt-transcript/src/digest.rs +++ b/crates/jolt-transcript/src/digest.rs @@ -33,7 +33,7 @@ pub struct DigestTranscript + 'static, F> { impl Clone for DigestTranscript where D: Digest, - F: jolt_field::TranscriptChallenge, + F: jolt_field::CanonicalRepr, { fn clone(&self) -> Self { Self { @@ -54,7 +54,7 @@ where impl Default for DigestTranscript where D: Digest, - F: jolt_field::TranscriptChallenge, + F: jolt_field::CanonicalRepr, { fn default() -> Self { Self::new(b"") @@ -64,7 +64,7 @@ where impl std::fmt::Debug for DigestTranscript where D: Digest, - F: jolt_field::TranscriptChallenge, + F: jolt_field::CanonicalRepr, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DigestTranscript") @@ -77,7 +77,7 @@ where impl DigestTranscript where D: Digest, - F: jolt_field::TranscriptChallenge, + F: jolt_field::CanonicalRepr, { #[inline] fn hasher(&self) -> D { @@ -131,7 +131,7 @@ where impl Transcript for DigestTranscript where D: Digest, - F: jolt_field::TranscriptChallenge, + F: jolt_field::CanonicalRepr, { type Challenge = F; diff --git a/crates/jolt-transcript/src/legacy.rs b/crates/jolt-transcript/src/legacy.rs index 2a3ef2dd1b..5d9defa9e2 100644 --- a/crates/jolt-transcript/src/legacy.rs +++ b/crates/jolt-transcript/src/legacy.rs @@ -7,7 +7,7 @@ use std::marker::PhantomData; -use jolt_field::{CanonicalBytes, Field, FromPrimitiveInt, TranscriptChallenge}; +use jolt_field::{CanonicalRepr, Field, FromPrimitiveInt}; use spongefish::{DuplexSpongeInterface, Encoding}; use crate::codec::BytesMsg; @@ -30,7 +30,7 @@ pub const MAX_LABEL_LEN: usize = 32; /// barriers. pub trait Transcript: Default + Sync + Send + 'static { /// The challenge type produced by this transcript. - type Challenge: TranscriptChallenge; + type Challenge: CanonicalRepr; /// Creates a new transcript with the given domain separation label. /// @@ -120,7 +120,7 @@ pub trait AppendToTranscript { /// Big-endian field element absorption (matches jolt-prover-legacy's EVM-compatible /// byte order). -impl AppendToTranscript for F { +impl AppendToTranscript for F { fn append_to_transcript(&self, transcript: &mut T) { let mut buf = vec![0u8; F::NUM_BYTES]; self.to_bytes_le(&mut buf); @@ -187,7 +187,7 @@ impl AppendToTranscript for U64Word { pub struct SpongeTranscript where H: DuplexSpongeInterface + Clone + Default + Send + Sync + 'static, - F: TranscriptChallenge, + F: CanonicalRepr, { sponge: H, _field: PhantomData, @@ -196,7 +196,7 @@ where impl Default for SpongeTranscript where H: DuplexSpongeInterface + Clone + Default + Send + Sync + 'static, - F: TranscriptChallenge, + F: CanonicalRepr, { fn default() -> Self { Self::new(b"") @@ -222,7 +222,7 @@ fn peek_state + Clone>(sponge: &H) -> [u8; 32] impl Transcript for SpongeTranscript where H: DuplexSpongeInterface + Clone + Default + Send + Sync + 'static, - F: TranscriptChallenge, + F: CanonicalRepr, { type Challenge = F; diff --git a/crates/jolt-verifier-derive/src/lib.rs b/crates/jolt-verifier-derive/src/lib.rs index f3283abe80..c33e1b514f 100644 --- a/crates/jolt-verifier-derive/src/lib.rs +++ b/crates/jolt-verifier-derive/src/lib.rs @@ -431,7 +431,7 @@ fn expand(input: DeriveInput) -> syn::Result { __T: ::jolt_transcript::Transcript, { use #relations::ConcreteSumcheck as _; - use ::jolt_field::MulPow2 as _; + use ::jolt_field::FromPrimitiveInt as _; #(#sum_bindings)* diff --git a/crates/jolt-verifier/tests/statistical_independence/zk.rs b/crates/jolt-verifier/tests/statistical_independence/zk.rs index 7c43bd8786..4d3a8fc2df 100644 --- a/crates/jolt-verifier/tests/statistical_independence/zk.rs +++ b/crates/jolt-verifier/tests/statistical_independence/zk.rs @@ -14,7 +14,7 @@ use std::collections::BTreeMap; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] use ark_serialize::CanonicalSerialize; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_field::{FixedBytes, Fr}; +use jolt_field::{CanonicalRepr, Fr}; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] use jolt_sumcheck::SumcheckProof; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] @@ -601,7 +601,7 @@ fn selected_positions(len: usize) -> Vec { #[cfg(all(feature = "prover-fixtures", feature = "zk"))] fn field_low_u64(value: Fr) -> u64 { - let bytes = value.to_bytes_array(); + let bytes = value.to_bytes_le_vec(); u64::from_le_bytes([ bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], ]) diff --git a/crates/jolt-witness/src/protocols/jolt_vm/field_inline/mod.rs b/crates/jolt-witness/src/protocols/jolt_vm/field_inline/mod.rs index 5d0d8663a2..86aa776002 100644 --- a/crates/jolt-witness/src/protocols/jolt_vm/field_inline/mod.rs +++ b/crates/jolt-witness/src/protocols/jolt_vm/field_inline/mod.rs @@ -4,7 +4,7 @@ use jolt_claims::protocols::field_inline::{ FieldInlineCommittedPolynomial, FieldInlineDerivedId, FieldInlineOpFlag, FieldInlineOpeningId, FieldInlineVirtualPolynomial, FIELD_REGISTERS_LOG_K, }; -use jolt_field::{Field, ReducingBytes}; +use jolt_field::{CanonicalRepr, Field}; use jolt_program::{ execution::{JoltProgram, TraceOutput, TraceRow, TraceSource}, field_inline::{ @@ -736,7 +736,7 @@ fn decode_value(value: FieldEncodedValue) -> F { bytes.copy_from_slice(&value.bytes_le[..8]); return F::from_u64(u64::from_le_bytes(bytes)); } - ::from_le_bytes_mod_order(&value.bytes_le) + ::from_le_bytes_mod_order(&value.bytes_le) } fn is_register_domain_virtual(id: FieldInlineVirtualPolynomial) -> bool { diff --git a/jolt-eval/src/invariant/field_mul_scalar.rs b/jolt-eval/src/invariant/field_mul_scalar.rs index ed461f1828..35d725b09e 100644 --- a/jolt-eval/src/invariant/field_mul_scalar.rs +++ b/jolt-eval/src/invariant/field_mul_scalar.rs @@ -1,6 +1,6 @@ use arbitrary::{Arbitrary, Unstructured}; use jolt_field::arkworks::bn254::Fr; -use jolt_field::{FromPrimitiveInt, MulPrimitiveInt}; +use jolt_field::FromPrimitiveInt; use crate::invariant::{CheckError, Invariant, InvariantViolation}; diff --git a/jolt-eval/src/invariant/transcript_symmetry.rs b/jolt-eval/src/invariant/transcript_symmetry.rs index bf06838ab6..96287e7f95 100644 --- a/jolt-eval/src/invariant/transcript_symmetry.rs +++ b/jolt-eval/src/invariant/transcript_symmetry.rs @@ -4,7 +4,7 @@ //! verifier challenges. use arbitrary::{Arbitrary, Unstructured}; -use jolt_field::{FixedBytes, Fr as JFr}; +use jolt_field::{CanonicalRepr, Fr as JFr}; use spongefish::instantiations::{Blake2b512, Keccak}; use jolt_transcript::{prover_transcript, verifier_transcript, BytesMsg, PoseidonSponge}; @@ -125,7 +125,9 @@ where } fn scalar_bytes(value: JFr) -> [u8; 32] { - value.to_bytes_array() + let mut out = [0u8; 32]; + value.to_bytes_le(&mut out); + out } fn violation(what: &str, op_idx: usize, err: spongefish::VerificationError) -> CheckError { diff --git a/jolt-eval/src/objective/performance/field_mul.rs b/jolt-eval/src/objective/performance/field_mul.rs index 44ff3634b9..99ff669b51 100644 --- a/jolt-eval/src/objective/performance/field_mul.rs +++ b/jolt-eval/src/objective/performance/field_mul.rs @@ -1,5 +1,5 @@ use jolt_field::arkworks::bn254::Fr; -use jolt_field::{MulPrimitiveInt, RandomSampling}; +use jolt_field::{FieldCore, FromPrimitiveInt}; use crate::objective::{Objective, OptimizationObjective, PerformanceObjective}; diff --git a/tracer/src/instruction/field_inline.rs b/tracer/src/instruction/field_inline.rs index 008d75a087..a0cf68c0c9 100644 --- a/tracer/src/instruction/field_inline.rs +++ b/tracer/src/instruction/field_inline.rs @@ -3,7 +3,7 @@ reason = "Tracer concrete instruction names mirror generated Jolt instruction constants" )] -use jolt_field::{CanonicalBytes, CanonicalU64, Fr, Invertible, ReducingBytes}; +use jolt_field::{CanonicalRepr, FieldCore, Fr}; use jolt_program::field_inline::{ FieldEncodedValue, FieldInlineBridge, FieldInlineTraceData, FieldRegisterRead, FieldRegisterWrite, @@ -329,7 +329,7 @@ fn execute_load_imm( } fn decode_field(value: FieldEncodedValue) -> Fr { - ::from_le_bytes_mod_order(&value.bytes_le) + ::from_le_bytes_mod_order(&value.bytes_le) } fn encode_field(value: Fr) -> FieldEncodedValue { From f9897fd6425087645f0619b90af6aebc22bb0815 Mon Sep 17 00:00:00 2001 From: acentelles Date: Tue, 21 Jul 2026 23:40:38 -0400 Subject: [PATCH 08/38] refactor(field): consolidate the extension-field trait cluster (spec phase 4) - ExtField absorbs LiftBase (lift_base), MulBase (mul_base), and FrobeniusExtField (frobenius_pow / frobenius_inv_pow); one trait now carries the full extension contract. The FpExt2/4/8 impls require a pseudo-Mersenne base, which every current base is. - FpExt4MulBackend and FpExt8MulBackend merge into a single ExtMulBackend with the same three implementors; Fp32 keeps its fused delayed-reduction quartic overrides. - The degree-4 mul/square schedule now exists once: the packed PackedField defaults call the shared fp_ext4_mul_coeffs / fp_ext4_square_coeffs (previously copy-pasted verbatim), matching how degree-8 already shares its schedule. --- crates/jolt-field/src/ext/fp_ext4.rs | 30 ++- crates/jolt-field/src/ext/fp_ext8.rs | 29 +-- crates/jolt-field/src/ext/lift.rs | 268 ++++++++------------ crates/jolt-field/src/ext/mod.rs | 7 +- crates/jolt-field/src/ext/native_algebra.rs | 18 +- crates/jolt-field/src/ext/tests.rs | 18 +- crates/jolt-field/src/lib.rs | 6 +- crates/jolt-field/src/packed/ext/mod.rs | 10 +- crates/jolt-field/src/packed/mod.rs | 41 +-- 9 files changed, 160 insertions(+), 267 deletions(-) diff --git a/crates/jolt-field/src/ext/fp_ext4.rs b/crates/jolt-field/src/ext/fp_ext4.rs index 4880a86dde..7a6ce71858 100644 --- a/crates/jolt-field/src/ext/fp_ext4.rs +++ b/crates/jolt-field/src/ext/fp_ext4.rs @@ -85,12 +85,12 @@ fn fp32_modulus_bits() -> u32 { 32 - P.leading_zeros() } -/// Backend hook for scalar ring-subfield quartic multiplication. +/// Backend hook for scalar ring-subfield extension multiplication (degree 4 and 8). /// /// The default is the generic coefficient formula. Concrete base fields can /// override this when their representation supports fusing product sums before /// reduction. -pub trait FpExt4MulBackend: FieldCore { +pub trait ExtMulBackend: FieldCore { /// Multiply two ring-subfield coefficient arrays in `[1, e1, e2, e3]` basis. #[inline(always)] fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { @@ -102,12 +102,18 @@ pub trait FpExt4MulBackend: FieldCore { fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { fp_ext4_square_coeffs::(a) } + + /// Multiply coefficient arrays in `[1, e1, ..., e7]` basis. + #[inline(always)] + fn fp_ext8_mul(a: [Self; 8], b: [Self; 8]) -> [Self; 8] { + fp_ext8_mul_coeffs::(a, b) + } } -impl FpExt4MulBackend for Fp64

{} -impl FpExt4MulBackend for Fp128

{} +impl ExtMulBackend for Fp64

{} +impl ExtMulBackend for Fp128

{} -impl FpExt4MulBackend for Fp32

{ +impl ExtMulBackend for Fp32

{ #[inline(always)] fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { let [a0, a1, a2, a3] = a; @@ -398,7 +404,7 @@ impl SubAssign for FpExt4 { } } -impl Mul for FpExt4 { +impl Mul for FpExt4 { type Output = Self; #[inline(always)] @@ -407,7 +413,7 @@ impl Mul for FpExt4 { } } -impl MulAssign for FpExt4 { +impl MulAssign for FpExt4 { #[inline] fn mul_assign(&mut self, rhs: Self) { *self = *self * rhs; @@ -430,7 +436,7 @@ impl<'a, F: FieldCore> Sub<&'a Self> for FpExt4 { } } -impl<'a, F: FpExt4MulBackend> Mul<&'a Self> for FpExt4 { +impl<'a, F: ExtMulBackend> Mul<&'a Self> for FpExt4 { type Output = Self; fn mul(self, rhs: &'a Self) -> Self::Output { @@ -438,14 +444,14 @@ impl<'a, F: FpExt4MulBackend> Mul<&'a Self> for FpExt4 { } } -impl RingCore for FpExt4 { +impl RingCore for FpExt4 { #[inline(always)] fn square(&self) -> Self { Self::new(F::fp_ext4_square(self.coeffs)) } } -impl FieldCore for FpExt4 { +impl FieldCore for FpExt4 { fn random(rng: &mut R) -> Self { Self::new(std::array::from_fn(|_| F::random(rng))) } @@ -478,14 +484,14 @@ impl FieldCore for FpExt4 { } } -impl HalvingField for FpExt4 { +impl HalvingField for FpExt4 { #[inline] fn half(self) -> Self { Self::new(std::array::from_fn(|i| self.coeffs[i].half())) } } -impl FromPrimitiveInt for FpExt4 { +impl FromPrimitiveInt for FpExt4 { fn from_u64(val: u64) -> Self { Self::from_u64(val) } diff --git a/crates/jolt-field/src/ext/fp_ext8.rs b/crates/jolt-field/src/ext/fp_ext8.rs index 5628ad771a..44b0bd9dd6 100644 --- a/crates/jolt-field/src/ext/fp_ext8.rs +++ b/crates/jolt-field/src/ext/fp_ext8.rs @@ -126,23 +126,10 @@ where } #[inline(always)] -fn fp_ext8_mul_coeffs(a: [F; 8], b: [F; 8]) -> [F; 8] { +pub(crate) fn fp_ext8_mul_coeffs(a: [F; 8], b: [F; 8]) -> [F; 8] { fp_ext8_mul_schedule(a, b, F::zero(), |x, y| x + y, |x, y| x - y, |x, y| x * y) } -/// Backend hook for scalar ring-subfield degree-8 multiplication. -pub trait FpExt8MulBackend: FieldCore { - /// Multiply coefficient arrays in `[1, e1, ..., e7]` basis. - #[inline(always)] - fn fp_ext8_mul(a: [Self; 8], b: [Self; 8]) -> [Self; 8] { - fp_ext8_mul_coeffs::(a, b) - } -} - -impl FpExt8MulBackend for Fp32

{} -impl FpExt8MulBackend for Fp64

{} -impl FpExt8MulBackend for Fp128

{} - /// Degree-8 ring subfield element in canonical basis `[1, e1, ..., e7]`. #[cfg_attr(feature = "allocative", derive(allocative::Allocative))] #[cfg_attr( @@ -292,7 +279,7 @@ impl SubAssign for FpExt8 { } } -impl Mul for FpExt8 { +impl Mul for FpExt8 { type Output = Self; #[inline(always)] @@ -301,7 +288,7 @@ impl Mul for FpExt8 { } } -impl MulAssign for FpExt8 { +impl MulAssign for FpExt8 { #[inline] fn mul_assign(&mut self, rhs: Self) { *self = *self * rhs; @@ -324,7 +311,7 @@ impl<'a, F: FieldCore> Sub<&'a Self> for FpExt8 { } } -impl<'a, F: FpExt8MulBackend> Mul<&'a Self> for FpExt8 { +impl<'a, F: ExtMulBackend> Mul<&'a Self> for FpExt8 { type Output = Self; fn mul(self, rhs: &'a Self) -> Self::Output { @@ -332,14 +319,14 @@ impl<'a, F: FpExt8MulBackend> Mul<&'a Self> for FpExt8 { } } -impl RingCore for FpExt8 { +impl RingCore for FpExt8 { #[inline(always)] fn square(&self) -> Self { *self * *self } } -impl FieldCore for FpExt8 { +impl FieldCore for FpExt8 { fn random(rng: &mut R) -> Self { Self::new(std::array::from_fn(|_| F::random(rng))) } @@ -391,14 +378,14 @@ impl FieldCore for FpExt8 { } } -impl HalvingField for FpExt8 { +impl HalvingField for FpExt8 { #[inline] fn half(self) -> Self { Self::new(std::array::from_fn(|i| self.coeffs[i].half())) } } -impl FromPrimitiveInt for FpExt8 { +impl FromPrimitiveInt for FpExt8 { fn from_u64(val: u64) -> Self { Self::from_u64(val) } diff --git a/crates/jolt-field/src/ext/lift.rs b/crates/jolt-field/src/ext/lift.rs index c1b76381cc..19cc3f50cf 100644 --- a/crates/jolt-field/src/ext/lift.rs +++ b/crates/jolt-field/src/ext/lift.rs @@ -1,4 +1,4 @@ -//! Helpers for embedding base fields into extension fields. +//! The extension-field abstraction over a base field. //! //! [`FpExt4`] and [`FpExt8`] use the cyclotomic ring-subfield basis aligned with //! trace reduction and production fp32 presets. @@ -8,37 +8,33 @@ reason = "registered pseudo-Mersenne parameters are a field-type invariant" )] -use crate::ext::{FpExt2, FpExt2Config, FpExt4, FpExt4MulBackend, FpExt8, FpExt8MulBackend}; +use crate::ext::{ExtMulBackend, FpExt2, FpExt2Config, FpExt4, FpExt8}; use crate::unreduced::HasUnreducedOps; use crate::{ pseudo_mersenne_modulus, FieldCore, FieldError, FromPrimitiveInt, PseudoMersenneField, }; -/// Lift a base-field element into an extension field. +/// An algebraic extension of base field `F`. /// -/// This is intentionally small: for extension towers we embed into the constant term. -pub trait LiftBase: FieldCore { +/// Provides the extension degree, embedding of and multiplication by base +/// elements, coefficient access in the canonical basis `{1, u, u^2, ...}`, +/// and Frobenius powers. +pub trait ExtField: FieldCore + FromPrimitiveInt { + /// Extension degree: `[Self : F]`. + const EXT_DEGREE: usize; + /// Embed `x ∈ F` as a constant in `Self`. + /// + /// This is intentionally small: for extension towers we embed into the + /// constant term. fn lift_base(x: F) -> Self; -} -/// Multiply an extension-field element by a base-field scalar. -/// -/// This avoids materializing the base scalar as an extension element and then -/// using a full extension multiply. For tower extensions this scales each -/// base-field coordinate directly. -pub trait MulBase: FieldCore { /// Return `self * x`, where `x` is interpreted as a base-field scalar. + /// + /// This avoids materializing the base scalar as an extension element and + /// then using a full extension multiply. For tower extensions this scales + /// each base-field coordinate directly. fn mul_base(self, x: F) -> Self; -} - -/// An algebraic extension of base field `F`. -/// -/// Provides the extension degree and a constructor from a slice of base-field -/// coefficients (in the canonical basis `{1, u, u^2, ...}`). -pub trait ExtField: FieldCore + LiftBase + MulBase + FromPrimitiveInt { - /// Extension degree: `[Self : F]`. - const EXT_DEGREE: usize; /// Construct from a coefficient slice `[c0, c1, ..., c_{d-1}]`. /// @@ -48,6 +44,24 @@ pub trait ExtField: FieldCore + LiftBase + MulBase + FromPri /// Return base-field coefficients in the canonical basis. fn to_base_vec(&self) -> Vec; + + /// Apply `x -> x^(q^power)`, where `q = |F|`. + /// + /// The provided implementations are intentionally algebraic rather than + /// basis-specific: they raise to powers of the base-field modulus. + /// Specialized extension types can add cheaper implementations later, but + /// this gives the protocol a single auditable contract first. + fn frobenius_pow(self, power: usize) -> Self; + + /// Apply the inverse Frobenius power. Since `x -> x^q` has order + /// `[Self:F]` on `Self`, this is `frobenius_pow(EXT_DEGREE - power)`. + fn frobenius_inv_pow(self, power: usize) -> Self { + let degree = Self::EXT_DEGREE; + if degree == 0 { + return self; + } + self.frobenius_pow((degree - (power % degree)) % degree) + } } /// Deferred-reduction extension-times-base multiply. @@ -56,7 +70,7 @@ pub trait ExtField: FieldCore + LiftBase + MulBase + FromPri /// result into [`HasUnreducedOps::ProductAccum`] without reducing, so a batch of /// `E × F` products can be summed and reduced once. When /// [`HasUnreducedOps::DELAYED_PRODUCT_SUM_IS_EXACT`] holds, the reduced sum equals -/// the per-term [`MulBase::mul_base`] sum within the accumulator's headroom. +/// the per-term [`ExtField::mul_base`] sum within the accumulator's headroom. /// /// `E × F` has no cross terms, so the default body (lift `x` and reuse /// [`HasUnreducedOps::mul_to_product_accum`]) is correct everywhere; extensions @@ -69,28 +83,7 @@ pub trait MulBaseUnreduced: ExtField + HasUnreducedOps { } } -impl MulBaseUnreduced for F {} - -/// Frobenius operations for an extension field over `F`. -/// -/// The default implementations below are intentionally algebraic rather than -/// basis-specific: they raise to powers of the base-field modulus. Specialized -/// extension types can add cheaper implementations later, but this gives the -/// protocol a single auditable contract first. -pub trait FrobeniusExtField: ExtField { - /// Apply `x -> x^(q^power)`, where `q = |F|`. - fn frobenius_pow(self, power: usize) -> Self; - - /// Apply the inverse Frobenius power. Since `x -> x^q` has order - /// `[Self:F]` on `Self`, this is `frobenius_pow(EXT_DEGREE - power)`. - fn frobenius_inv_pow(self, power: usize) -> Self { - let degree = Self::EXT_DEGREE; - if degree == 0 { - return self; - } - self.frobenius_pow((degree - (power % degree)) % degree) - } -} +impl MulBaseUnreduced for F {} #[inline] fn field_pow_u128(mut base: E, mut exp: u128) -> E { @@ -124,48 +117,6 @@ where out } -impl FrobeniusExtField for F -where - F: PseudoMersenneField, -{ - #[inline] - fn frobenius_pow(self, power: usize) -> Self { - let _ = power; - self - } -} - -impl FrobeniusExtField for FpExt2 -where - F: PseudoMersenneField, - C: FpExt2Config, -{ - #[inline] - fn frobenius_pow(self, power: usize) -> Self { - frobenius_pow_via_base_modulus::(self, power) - } -} - -impl FrobeniusExtField for FpExt4 -where - F: PseudoMersenneField + FpExt4MulBackend, -{ - #[inline] - fn frobenius_pow(self, power: usize) -> Self { - frobenius_pow_via_base_modulus::(self, power) - } -} - -impl FrobeniusExtField for FpExt8 -where - F: PseudoMersenneField + FpExt8MulBackend, -{ - #[inline] - fn frobenius_pow(self, power: usize) -> Self { - frobenius_pow_via_base_modulus::(self, power) - } -} - /// Return the first `width` elements of the canonical extension basis. /// /// For [`FpExt4`] and [`FpExt8`] this is the fixed @@ -209,7 +160,7 @@ where pub fn solve_frobenius_moore(thetas: &[E], rhs: &[E]) -> Result, FieldError> where F: PseudoMersenneField, - E: FrobeniusExtField, + E: ExtField, { let n = thetas.len(); if rhs.len() != n { @@ -274,7 +225,7 @@ where pub fn validate_canonical_frobenius_thetas(width: usize) -> Result<(), FieldError> where F: PseudoMersenneField, - E: FrobeniusExtField, + E: ExtField, { let thetas = canonical_frobenius_thetas::(width)?; let rhs = (0..width) @@ -283,9 +234,19 @@ where solve_frobenius_moore::(&thetas, &rhs).map(|_| ()) } -impl ExtField for F { +impl ExtField for F { const EXT_DEGREE: usize = 1; + #[inline] + fn lift_base(x: F) -> Self { + x + } + + #[inline] + fn mul_base(self, x: F) -> Self { + self * x + } + #[inline] fn from_base_slice(coeffs: &[F]) -> Self { assert_eq!(coeffs.len(), 1); @@ -296,125 +257,87 @@ impl ExtField for F { fn to_base_vec(&self) -> Vec { vec![*self] } + + #[inline] + fn frobenius_pow(self, power: usize) -> Self { + let _ = power; + self + } } impl ExtField for FpExt2 where - F: FieldCore + FromPrimitiveInt, + F: PseudoMersenneField, C: FpExt2Config, { const EXT_DEGREE: usize = 2; #[inline] - fn from_base_slice(coeffs: &[F]) -> Self { - assert_eq!(coeffs.len(), 2); - Self::new(coeffs[0], coeffs[1]) + fn lift_base(x: F) -> Self { + Self::new(x, F::zero()) } #[inline] - fn to_base_vec(&self) -> Vec { - vec![self.coeffs[0], self.coeffs[1]] + fn mul_base(self, x: F) -> Self { + Self::new(self.coeffs[0] * x, self.coeffs[1] * x) } -} - -impl ExtField for FpExt4 -where - F: FieldCore + FromPrimitiveInt + FpExt4MulBackend, -{ - const EXT_DEGREE: usize = 4; #[inline] fn from_base_slice(coeffs: &[F]) -> Self { - assert_eq!(coeffs.len(), 4); - Self::new([coeffs[0], coeffs[1], coeffs[2], coeffs[3]]) + assert_eq!(coeffs.len(), 2); + Self::new(coeffs[0], coeffs[1]) } #[inline] fn to_base_vec(&self) -> Vec { - self.coeffs.to_vec() - } -} - -impl ExtField for FpExt8 -where - F: FieldCore + FromPrimitiveInt + FpExt8MulBackend, -{ - const EXT_DEGREE: usize = 8; - - #[inline] - fn from_base_slice(coeffs: &[F]) -> Self { - assert_eq!(coeffs.len(), 8); - Self::new([ - coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4], coeffs[5], coeffs[6], coeffs[7], - ]) + vec![self.coeffs[0], self.coeffs[1]] } #[inline] - fn to_base_vec(&self) -> Vec { - self.coeffs.to_vec() + fn frobenius_pow(self, power: usize) -> Self { + frobenius_pow_via_base_modulus::(self, power) } } -impl LiftBase for F { +impl ExtField for FpExt4 +where + F: PseudoMersenneField + ExtMulBackend, +{ + const EXT_DEGREE: usize = 4; + #[inline] fn lift_base(x: F) -> Self { - x + Self::new([x, F::zero(), F::zero(), F::zero()]) } -} -impl MulBase for F { #[inline] fn mul_base(self, x: F) -> Self { - self * x + Self::new(std::array::from_fn(|i| self.coeffs[i] * x)) } -} -impl LiftBase for FpExt2 -where - F: FieldCore, - C: FpExt2Config, -{ #[inline] - fn lift_base(x: F) -> Self { - Self::new(x, F::zero()) + fn from_base_slice(coeffs: &[F]) -> Self { + assert_eq!(coeffs.len(), 4); + Self::new([coeffs[0], coeffs[1], coeffs[2], coeffs[3]]) } -} -impl MulBase for FpExt2 -where - F: FieldCore, - C: FpExt2Config, -{ #[inline] - fn mul_base(self, x: F) -> Self { - Self::new(self.coeffs[0] * x, self.coeffs[1] * x) + fn to_base_vec(&self) -> Vec { + self.coeffs.to_vec() } -} -impl LiftBase for FpExt4 -where - F: FieldCore + FpExt4MulBackend, -{ #[inline] - fn lift_base(x: F) -> Self { - Self::new([x, F::zero(), F::zero(), F::zero()]) + fn frobenius_pow(self, power: usize) -> Self { + frobenius_pow_via_base_modulus::(self, power) } } -impl MulBase for FpExt4 +impl ExtField for FpExt8 where - F: FieldCore + FpExt4MulBackend, + F: PseudoMersenneField + ExtMulBackend, { - #[inline] - fn mul_base(self, x: F) -> Self { - Self::new(std::array::from_fn(|i| self.coeffs[i] * x)) - } -} + const EXT_DEGREE: usize = 8; -impl LiftBase for FpExt8 -where - F: FieldCore + FpExt8MulBackend, -{ #[inline] fn lift_base(x: F) -> Self { Self::new([ @@ -428,16 +351,29 @@ where F::zero(), ]) } -} -impl MulBase for FpExt8 -where - F: FieldCore + FpExt8MulBackend, -{ #[inline] fn mul_base(self, x: F) -> Self { Self::new(std::array::from_fn(|i| self.coeffs[i] * x)) } + + #[inline] + fn from_base_slice(coeffs: &[F]) -> Self { + assert_eq!(coeffs.len(), 8); + Self::new([ + coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4], coeffs[5], coeffs[6], coeffs[7], + ]) + } + + #[inline] + fn to_base_vec(&self) -> Vec { + self.coeffs.to_vec() + } + + #[inline] + fn frobenius_pow(self, power: usize) -> Self { + frobenius_pow_via_base_modulus::(self, power) + } } #[cfg(test)] diff --git a/crates/jolt-field/src/ext/mod.rs b/crates/jolt-field/src/ext/mod.rs index 5c5f5bdca3..3ecd29b075 100644 --- a/crates/jolt-field/src/ext/mod.rs +++ b/crates/jolt-field/src/ext/mod.rs @@ -26,6 +26,7 @@ use std::marker::PhantomData; use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; pub use fp_ext2::{Ext2, FpExt2, FpExt2Config, NegOneNr, TwoNr}; -pub use fp_ext4::{FpExt4, FpExt4MulBackend}; -pub(crate) use fp_ext8::{fp_ext8_mul_schedule, fp_ext8_square_schedule}; -pub use fp_ext8::{FpExt8, FpExt8MulBackend}; +pub(crate) use fp_ext4::{fp_ext4_mul_coeffs, fp_ext4_square_coeffs}; +pub use fp_ext4::{ExtMulBackend, FpExt4}; +pub use fp_ext8::FpExt8; +pub(crate) use fp_ext8::{fp_ext8_mul_coeffs, fp_ext8_mul_schedule, fp_ext8_square_schedule}; diff --git a/crates/jolt-field/src/ext/native_algebra.rs b/crates/jolt-field/src/ext/native_algebra.rs index 6665f105af..1b2173947c 100644 --- a/crates/jolt-field/src/ext/native_algebra.rs +++ b/crates/jolt-field/src/ext/native_algebra.rs @@ -11,7 +11,7 @@ use std::iter::{Product, Sum}; use num_traits::{One, Zero}; -use super::{FpExt2, FpExt2Config, FpExt4, FpExt4MulBackend, FpExt8, FpExt8MulBackend}; +use super::{ExtMulBackend, FpExt2, FpExt2Config, FpExt4, FpExt8}; use crate::{AdditiveGroup, FieldCore}; // --- FpExt2 ----------------------------------------------------------------- @@ -88,7 +88,7 @@ impl Zero for FpExt4 { } } -impl One for FpExt4 { +impl One for FpExt4 { #[inline] fn one() -> Self { Self::new([F::one(), F::zero(), F::zero(), F::zero()]) @@ -123,19 +123,19 @@ impl<'a, F: FieldCore> Sum<&'a Self> for FpExt4 { } } -impl Product for FpExt4 { +impl Product for FpExt4 { fn product>(iter: I) -> Self { iter.fold(Self::one(), |acc, x| acc * x) } } -impl<'a, F: FieldCore + FpExt4MulBackend> Product<&'a Self> for FpExt4 { +impl<'a, F: FieldCore + ExtMulBackend> Product<&'a Self> for FpExt4 { fn product>(iter: I) -> Self { iter.fold(Self::one(), |acc, x| acc * *x) } } -impl AdditiveGroup for FpExt4 {} +impl AdditiveGroup for FpExt4 {} // --- FpExt8 ----------------------------------------------------- @@ -160,7 +160,7 @@ impl Zero for FpExt8 { } } -impl One for FpExt8 { +impl One for FpExt8 { #[inline] fn one() -> Self { Self::new([ @@ -211,16 +211,16 @@ impl<'a, F: FieldCore> Sum<&'a Self> for FpExt8 { } } -impl Product for FpExt8 { +impl Product for FpExt8 { fn product>(iter: I) -> Self { iter.fold(Self::one(), |acc, x| acc * x) } } -impl<'a, F: FieldCore + FpExt8MulBackend> Product<&'a Self> for FpExt8 { +impl<'a, F: FieldCore + ExtMulBackend> Product<&'a Self> for FpExt8 { fn product>(iter: I) -> Self { iter.fold(Self::one(), |acc, x| acc * *x) } } -impl AdditiveGroup for FpExt8 {} +impl AdditiveGroup for FpExt8 {} diff --git a/crates/jolt-field/src/ext/tests.rs b/crates/jolt-field/src/ext/tests.rs index 79be9b6172..51ccd8429d 100644 --- a/crates/jolt-field/src/ext/tests.rs +++ b/crates/jolt-field/src/ext/tests.rs @@ -8,7 +8,7 @@ use super::*; use crate::ext::lift::{ canonical_frobenius_thetas, solve_frobenius_moore, validate_canonical_frobenius_thetas, - ExtField, FrobeniusExtField, + ExtField, }; use crate::Fp64; use crate::{FieldCore, FromPrimitiveInt}; @@ -180,16 +180,10 @@ fn fp_ext8_inv() { #[test] fn frobenius_fp_ext2_is_conjugation() { let x = E2::new(F::from_u64(13), F::from_u64(21)); - assert_eq!(>::frobenius_pow(x, 0), x); - assert_eq!( - >::frobenius_pow(x, 1), - x.conjugate() - ); - assert_eq!(>::frobenius_pow(x, 2), x); - assert_eq!( - >::frobenius_inv_pow(x, 1), - x.conjugate() - ); + assert_eq!(>::frobenius_pow(x, 0), x); + assert_eq!(>::frobenius_pow(x, 1), x.conjugate()); + assert_eq!(>::frobenius_pow(x, 2), x); + assert_eq!(>::frobenius_inv_pow(x, 1), x.conjugate()); } #[test] @@ -206,7 +200,7 @@ fn canonical_moore_thetas_solve_fp_ext2() { .iter() .zip(z.iter()) .fold(E2::zero(), |acc, (&theta, &z_h)| { - acc + >::frobenius_inv_pow(theta, row) * z_h + acc + >::frobenius_inv_pow(theta, row) * z_h }) }) .collect::>(); diff --git a/crates/jolt-field/src/lib.rs b/crates/jolt-field/src/lib.rs index b8ae22dda0..f571e294c4 100644 --- a/crates/jolt-field/src/lib.rs +++ b/crates/jolt-field/src/lib.rs @@ -81,12 +81,10 @@ pub mod unreduced; #[cfg(feature = "solinas")] pub use ext::lift::{ canonical_frobenius_thetas, solve_frobenius_moore, validate_canonical_frobenius_thetas, - ExtField, FrobeniusExtField, LiftBase, MulBase, MulBaseUnreduced, + ExtField, MulBaseUnreduced, }; #[cfg(feature = "solinas")] -pub use ext::{ - Ext2, FpExt2, FpExt2Config, FpExt4, FpExt4MulBackend, FpExt8, FpExt8MulBackend, NegOneNr, TwoNr, -}; +pub use ext::{Ext2, ExtMulBackend, FpExt2, FpExt2Config, FpExt4, FpExt8, NegOneNr, TwoNr}; #[cfg(feature = "solinas")] pub use prime::{ is_registered_prime_offset, pseudo_mersenne_modulus, registered_prime_offset_spec, Fp128, Fp32, diff --git a/crates/jolt-field/src/packed/ext/mod.rs b/crates/jolt-field/src/packed/ext/mod.rs index 7350116ce7..1e17fc0779 100644 --- a/crates/jolt-field/src/packed/ext/mod.rs +++ b/crates/jolt-field/src/packed/ext/mod.rs @@ -10,7 +10,7 @@ reason = "manual Clone avoids adding irrelevant generic Clone bounds" )] -use crate::ext::{FpExt2, FpExt2Config, FpExt4, FpExt4MulBackend, FpExt8, FpExt8MulBackend}; +use crate::ext::{ExtMulBackend, FpExt2, FpExt2Config, FpExt4, FpExt8}; use crate::packed::{HasPacking, PackedField, PackedValue}; use crate::FieldCore; use core::ops::{Add, Mul, Sub}; @@ -287,7 +287,7 @@ where impl PackedField for PackedFpExt4 where - F: FieldCore + FpExt4MulBackend + 'static, + F: FieldCore + ExtMulBackend + 'static, PF: PackedField, { type Scalar = FpExt4; @@ -313,7 +313,7 @@ where impl HasPacking for FpExt4 where - F: FieldCore + HasPacking + FpExt4MulBackend + 'static, + F: FieldCore + HasPacking + ExtMulBackend + 'static, { type Packing = PackedFpExt4; } @@ -434,7 +434,7 @@ where impl PackedField for PackedFpExt8 where - F: FieldCore + FpExt8MulBackend + 'static, + F: FieldCore + ExtMulBackend + 'static, PF: PackedField, { type Scalar = FpExt8; @@ -471,7 +471,7 @@ where impl HasPacking for FpExt8 where - F: FieldCore + HasPacking + FpExt8MulBackend + 'static, + F: FieldCore + HasPacking + ExtMulBackend + 'static, { type Packing = PackedFpExt8; } diff --git a/crates/jolt-field/src/packed/mod.rs b/crates/jolt-field/src/packed/mod.rs index 9abb2e32b9..e7874f0091 100644 --- a/crates/jolt-field/src/packed/mod.rs +++ b/crates/jolt-field/src/packed/mod.rs @@ -18,7 +18,10 @@ pub(crate) mod neon; pub use ext::{PackedFpExt2, PackedFpExt4, PackedFpExt8}; -use crate::ext::{fp_ext8_mul_schedule, fp_ext8_square_schedule, FpExt2Config}; +use crate::ext::{ + fp_ext4_mul_coeffs, fp_ext4_square_coeffs, fp_ext8_mul_schedule, fp_ext8_square_schedule, + FpExt2Config, +}; use crate::{FieldCore, Fp128, Fp32, Fp64}; use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; use num_traits::Zero; @@ -125,45 +128,13 @@ pub trait PackedField: /// Backend hook for multiplying packed ring-subfield quartics. #[inline(always)] fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { - let [a0, a1, a2, a3] = a; - let [b0, b1, b2, b3] = b; - let tail0 = a1 * b1 + a2 * b2 + a3 * b3; - [ - a0 * b0 + tail0 + tail0, - a0 * b1 + a1 * b0 + a1 * b2 + a2 * b1 + a2 * b3 + a3 * b2, - a0 * b2 + a2 * b0 + a1 * b1 + a1 * b3 + a3 * b1 - a3 * b3, - a0 * b3 + a3 * b0 + a1 * b2 + a2 * b1 - a2 * b3 - a3 * b2, - ] + fp_ext4_mul_coeffs::(a, b) } /// Backend hook for squaring packed ring-subfield quartics. #[inline(always)] fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { - let [a0, a1, a2, a3] = a; - let x0 = a0; - let x1 = a2; - let y0 = a1 - a3; - let y1 = a3; - - let x0x1 = x0 * x1; - let y0y1 = y0 * y1; - let x1_square = x1 * x1; - let y1_square = y1 * y1; - let aa = (x0 * x0 + x1_square + x1_square, x0x1 + x0x1); - let bb = (y0 * y0 + y1_square + y1_square, y0y1 + y0y1); - - let v0 = x0 * y0; - let v1 = x1 * y1; - let ab = (v0 + v1 + v1, (x0 + x1) * (y0 + y1) - v0 - v1); - let constant = (bb.0 + bb.0 + bb.1 + bb.1, bb.0 + bb.1 + bb.1); - let coeff_e1 = (ab.0 + ab.0, ab.1 + ab.1); - - [ - aa.0 + constant.0, - coeff_e1.0 + coeff_e1.1, - aa.1 + constant.1, - coeff_e1.1, - ] + fp_ext4_square_coeffs::(a) } /// Backend hook for inverting packed ring-subfield quartics. From f88ee8fbb5a8d8790aac755d24fb7e51309457da Mon Sep 17 00:00:00 2001 From: acentelles Date: Wed, 22 Jul 2026 01:30:52 -0400 Subject: [PATCH 09/38] refactor(field): bn254.rs-style layout and shared macros (spec phase 5) Every concrete type's trait surface now lives in its own file, with three shared macros carrying the mechanical expansions: - impl_native_ring_algebra! / impl_native_additive! (new native_algebra.rs) replace the three per-module native_algebra.rs side-files, including the 226 hand-written lines in ext/; invocations sit in each type's file - impl_prime_ops! collapses the 30 hand-duplicated operator impl blocks across Fp32/Fp64/Fp128 into 3 invocations; the *_raw reduction kernels stay hand-written per type - select_packing! collapses the three near-identical cfg cascades that choose the packed backend per prime width - impl_prime_native_capability! invocations move into the per-type files Also finishes the disposition table: ScaleI32 merges into ReduceTo, PackedValue merges into PackedField, and jolt-field's OptimizedMul is deleted outright (jolt-prover-legacy has its own identical trait; the jolt-field copy had no consumer). jolt-field is now at 22 public traits. Criterion solinas_field_arith before/after shows no regression on any of the 20 benchmarks. --- .../benches/solinas_field_arith/parallel.rs | 2 +- crates/jolt-field/src/algebra.rs | 47 ---- crates/jolt-field/src/ext/fp_ext2.rs | 15 ++ crates/jolt-field/src/ext/fp_ext4.rs | 16 ++ crates/jolt-field/src/ext/fp_ext8.rs | 32 +++ crates/jolt-field/src/ext/mod.rs | 1 - crates/jolt-field/src/ext/native_algebra.rs | 226 ------------------ crates/jolt-field/src/lib.rs | 5 +- crates/jolt-field/src/native_algebra.rs | 212 ++++++++++++++++ crates/jolt-field/src/packed/avx2/fp128.rs | 49 ++-- crates/jolt-field/src/packed/avx2/fp32.rs | 37 ++- crates/jolt-field/src/packed/avx2/fp64.rs | 35 ++- crates/jolt-field/src/packed/avx2/mod.rs | 2 +- crates/jolt-field/src/packed/avx512/fp128.rs | 49 ++-- crates/jolt-field/src/packed/avx512/fp32.rs | 53 ++-- crates/jolt-field/src/packed/avx512/fp64.rs | 35 ++- crates/jolt-field/src/packed/avx512/mod.rs | 2 +- crates/jolt-field/src/packed/ext/mod.rs | 141 +++++------ crates/jolt-field/src/packed/ext/tests.rs | 42 ++-- crates/jolt-field/src/packed/mod.rs | 199 ++++++--------- crates/jolt-field/src/packed/neon/fp128.rs | 45 ++-- crates/jolt-field/src/packed/neon/fp32.rs | 39 ++- crates/jolt-field/src/packed/neon/fp64.rs | 39 ++- crates/jolt-field/src/packed/neon/mod.rs | 2 +- crates/jolt-field/src/packed/tests.rs | 8 +- crates/jolt-field/src/prime/fp128/mod.rs | 1 - crates/jolt-field/src/prime/fp128/traits.rs | 93 ++----- crates/jolt-field/src/prime/fp32.rs | 97 ++------ crates/jolt-field/src/prime/fp64.rs | 97 ++------ crates/jolt-field/src/prime/mod.rs | 3 +- crates/jolt-field/src/prime/native_algebra.rs | 87 ------- .../jolt-field/src/prime/native_capability.rs | 42 ++-- crates/jolt-field/src/unreduced/accum.rs | 13 + crates/jolt-field/src/unreduced/mod.rs | 53 ++-- .../src/unreduced/native_algebra.rs | 94 -------- crates/jolt-field/tests/coverage.rs | 39 +-- 36 files changed, 725 insertions(+), 1227 deletions(-) delete mode 100644 crates/jolt-field/src/ext/native_algebra.rs create mode 100644 crates/jolt-field/src/native_algebra.rs delete mode 100644 crates/jolt-field/src/prime/native_algebra.rs delete mode 100644 crates/jolt-field/src/unreduced/native_algebra.rs diff --git a/crates/jolt-field/benches/solinas_field_arith/parallel.rs b/crates/jolt-field/benches/solinas_field_arith/parallel.rs index f900d0f2cc..fe023b0ac2 100644 --- a/crates/jolt-field/benches/solinas_field_arith/parallel.rs +++ b/crates/jolt-field/benches/solinas_field_arith/parallel.rs @@ -6,7 +6,7 @@ use std::thread; #[cfg(feature = "parallel")] use criterion::{black_box, Criterion, Throughput}; #[cfg(feature = "parallel")] -use jolt_field::packed::{PackedField, PackedValue}; +use jolt_field::packed::PackedField; #[cfg(feature = "parallel")] use jolt_field::{CanonicalField, FieldCore, Prime128Offset275, Prime31Offset19, Prime64Offset59}; #[cfg(feature = "parallel")] diff --git a/crates/jolt-field/src/algebra.rs b/crates/jolt-field/src/algebra.rs index e096e96465..b08b02863b 100644 --- a/crates/jolt-field/src/algebra.rs +++ b/crates/jolt-field/src/algebra.rs @@ -168,50 +168,3 @@ pub trait FromPrimitiveInt: RingCore { res * Self::from_u64(1 << p) } } - -/// Multiplication with fast-path short-circuits for zero and one. -/// -/// In sumcheck hot loops many evaluations multiply by 0 or 1. -/// These methods avoid the full Montgomery multiplication in those cases. -pub trait OptimizedMul: Sized + Mul { - /// Returns `zero()` immediately if either operand is zero. - fn mul_0_optimized(self, other: Rhs) -> Self::Output; - /// Returns the other operand immediately if either is one. - fn mul_1_optimized(self, other: Rhs) -> Self::Output; - /// Combined: short-circuits on both zero and one. - fn mul_01_optimized(self, other: Rhs) -> Self::Output; -} - -impl OptimizedMul for F -where - F: RingCore, -{ - #[inline(always)] - fn mul_0_optimized(self, other: F) -> F { - if self.is_zero() || other.is_zero() { - Self::zero() - } else { - self * other - } - } - - #[inline(always)] - fn mul_1_optimized(self, other: F) -> F { - if self.is_one() { - other - } else if other.is_one() { - self - } else { - self * other - } - } - - #[inline(always)] - fn mul_01_optimized(self, other: F) -> F { - if self.is_zero() || other.is_zero() { - Self::zero() - } else { - self.mul_1_optimized(other) - } - } -} diff --git a/crates/jolt-field/src/ext/fp_ext2.rs b/crates/jolt-field/src/ext/fp_ext2.rs index 3c2fa2c69b..5c5eb346ad 100644 --- a/crates/jolt-field/src/ext/fp_ext2.rs +++ b/crates/jolt-field/src/ext/fp_ext2.rs @@ -532,3 +532,18 @@ where Ok(Self::new(c0, c1)) } } + +use crate::native_algebra::impl_native_ring_algebra; + +impl_native_ring_algebra!( + impl[F: FieldCore, C: FpExt2Config] FpExt2 { + zero: Self::new(F::zero(), F::zero()), + is_zero(x): ::num_traits::Zero::is_zero(&x.coeffs[0]) && ::num_traits::Zero::is_zero(&x.coeffs[1]), + one: Self::new(F::one(), F::zero()), + display(x, f): write!(f, "({}, {})", x.coeffs[0], x.coeffs[1]), + hash(x, state): { + ::std::hash::Hash::hash(&x.coeffs[0], state); + ::std::hash::Hash::hash(&x.coeffs[1], state); + }, + } +); diff --git a/crates/jolt-field/src/ext/fp_ext4.rs b/crates/jolt-field/src/ext/fp_ext4.rs index 7a6ce71858..e59a98b166 100644 --- a/crates/jolt-field/src/ext/fp_ext4.rs +++ b/crates/jolt-field/src/ext/fp_ext4.rs @@ -681,3 +681,19 @@ impl<'de, F: FieldCore + serde::Deserialize<'de>> serde::Deserialize<'de> for Fp Ok(Self::new(<[F; 4]>::deserialize(deserializer)?)) } } + +use crate::native_algebra::impl_native_ring_algebra; + +impl_native_ring_algebra!( + impl[F: FieldCore + ExtMulBackend] FpExt4 { + zero: Self::new([F::zero(); 4]), + is_zero(x): x.coeffs.iter().all(|c| ::num_traits::Zero::is_zero(c)), + one: Self::new([F::one(), F::zero(), F::zero(), F::zero()]), + display(x, f): write!( + f, + "({}, {}, {}, {})", + x.coeffs[0], x.coeffs[1], x.coeffs[2], x.coeffs[3] + ), + hash(x, state): ::std::hash::Hash::hash(&x.coeffs, state), + } +); diff --git a/crates/jolt-field/src/ext/fp_ext8.rs b/crates/jolt-field/src/ext/fp_ext8.rs index 44b0bd9dd6..912cd05876 100644 --- a/crates/jolt-field/src/ext/fp_ext8.rs +++ b/crates/jolt-field/src/ext/fp_ext8.rs @@ -479,3 +479,35 @@ impl<'de, F: FieldCore + serde::Deserialize<'de>> serde::Deserialize<'de> for Fp Ok(Self::new(<[F; 8]>::deserialize(deserializer)?)) } } + +use crate::native_algebra::impl_native_ring_algebra; + +impl_native_ring_algebra!( + impl[F: FieldCore + ExtMulBackend] FpExt8 { + zero: Self::new([F::zero(); 8]), + is_zero(x): x.coeffs.iter().all(|c| ::num_traits::Zero::is_zero(c)), + one: Self::new([ + F::one(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + F::zero(), + ]), + display(x, f): write!( + f, + "({}, {}, {}, {}, {}, {}, {}, {})", + x.coeffs[0], + x.coeffs[1], + x.coeffs[2], + x.coeffs[3], + x.coeffs[4], + x.coeffs[5], + x.coeffs[6], + x.coeffs[7] + ), + hash(x, state): ::std::hash::Hash::hash(&x.coeffs, state), + } +); diff --git a/crates/jolt-field/src/ext/mod.rs b/crates/jolt-field/src/ext/mod.rs index 3ecd29b075..89695e96e9 100644 --- a/crates/jolt-field/src/ext/mod.rs +++ b/crates/jolt-field/src/ext/mod.rs @@ -9,7 +9,6 @@ mod fp_ext2; mod fp_ext4; mod fp_ext8; pub(crate) mod lift; -mod native_algebra; #[cfg(test)] mod tests; diff --git a/crates/jolt-field/src/ext/native_algebra.rs b/crates/jolt-field/src/ext/native_algebra.rs deleted file mode 100644 index 1b2173947c..0000000000 --- a/crates/jolt-field/src/ext/native_algebra.rs +++ /dev/null @@ -1,226 +0,0 @@ -//! Native `num_traits`/`std` supertrait impls and core-algebra markers for the -//! extension field types (`FpExt2`, `FpExt4`, `FpExt8`). -//! -//! These are the Jolt-free supertrait obligations of the native -//! [`AdditiveGroup`]/[`FieldCore`] hierarchy. The non-trivial `RingCore::square` -//! / `FieldCore::inverse` impls stay co-located with each extension type. - -use std::fmt; -use std::hash::{Hash, Hasher}; -use std::iter::{Product, Sum}; - -use num_traits::{One, Zero}; - -use super::{ExtMulBackend, FpExt2, FpExt2Config, FpExt4, FpExt8}; -use crate::{AdditiveGroup, FieldCore}; - -// --- FpExt2 ----------------------------------------------------------------- - -impl> Zero for FpExt2 { - #[inline] - fn zero() -> Self { - Self::new(F::zero(), F::zero()) - } - - #[inline] - fn is_zero(&self) -> bool { - self.coeffs[0].is_zero() && self.coeffs[1].is_zero() - } -} - -impl> One for FpExt2 { - #[inline] - fn one() -> Self { - Self::new(F::one(), F::zero()) - } -} - -impl> fmt::Display for FpExt2 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "({}, {})", self.coeffs[0], self.coeffs[1]) - } -} - -impl> Hash for FpExt2 { - fn hash(&self, state: &mut H) { - self.coeffs[0].hash(state); - self.coeffs[1].hash(state); - } -} - -impl> Sum for FpExt2 { - fn sum>(iter: I) -> Self { - iter.fold(Self::zero(), |acc, x| acc + x) - } -} - -impl<'a, F: FieldCore, C: FpExt2Config> Sum<&'a Self> for FpExt2 { - fn sum>(iter: I) -> Self { - iter.fold(Self::zero(), |acc, x| acc + *x) - } -} - -impl> Product for FpExt2 { - fn product>(iter: I) -> Self { - iter.fold(Self::one(), |acc, x| acc * x) - } -} - -impl<'a, F: FieldCore, C: FpExt2Config> Product<&'a Self> for FpExt2 { - fn product>(iter: I) -> Self { - iter.fold(Self::one(), |acc, x| acc * *x) - } -} - -impl> AdditiveGroup for FpExt2 {} - -// --- FpExt4 ----------------------------------------------------- - -impl Zero for FpExt4 { - #[inline] - fn zero() -> Self { - Self::new([F::zero(), F::zero(), F::zero(), F::zero()]) - } - - #[inline] - fn is_zero(&self) -> bool { - self.coeffs.iter().all(|coeff| coeff.is_zero()) - } -} - -impl One for FpExt4 { - #[inline] - fn one() -> Self { - Self::new([F::one(), F::zero(), F::zero(), F::zero()]) - } -} - -impl fmt::Display for FpExt4 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "({}, {}, {}, {})", - self.coeffs[0], self.coeffs[1], self.coeffs[2], self.coeffs[3] - ) - } -} - -impl Hash for FpExt4 { - fn hash(&self, state: &mut H) { - self.coeffs.hash(state); - } -} - -impl Sum for FpExt4 { - fn sum>(iter: I) -> Self { - iter.fold(Self::zero(), |acc, x| acc + x) - } -} - -impl<'a, F: FieldCore> Sum<&'a Self> for FpExt4 { - fn sum>(iter: I) -> Self { - iter.fold(Self::zero(), |acc, x| acc + *x) - } -} - -impl Product for FpExt4 { - fn product>(iter: I) -> Self { - iter.fold(Self::one(), |acc, x| acc * x) - } -} - -impl<'a, F: FieldCore + ExtMulBackend> Product<&'a Self> for FpExt4 { - fn product>(iter: I) -> Self { - iter.fold(Self::one(), |acc, x| acc * *x) - } -} - -impl AdditiveGroup for FpExt4 {} - -// --- FpExt8 ----------------------------------------------------- - -impl Zero for FpExt8 { - #[inline] - fn zero() -> Self { - Self::new([ - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - ]) - } - - #[inline] - fn is_zero(&self) -> bool { - self.coeffs.iter().all(|coeff| coeff.is_zero()) - } -} - -impl One for FpExt8 { - #[inline] - fn one() -> Self { - Self::new([ - F::one(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - ]) - } -} - -impl fmt::Display for FpExt8 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "({}, {}, {}, {}, {}, {}, {}, {})", - self.coeffs[0], - self.coeffs[1], - self.coeffs[2], - self.coeffs[3], - self.coeffs[4], - self.coeffs[5], - self.coeffs[6], - self.coeffs[7] - ) - } -} - -impl Hash for FpExt8 { - fn hash(&self, state: &mut H) { - self.coeffs.hash(state); - } -} - -impl Sum for FpExt8 { - fn sum>(iter: I) -> Self { - iter.fold(Self::zero(), |acc, x| acc + x) - } -} - -impl<'a, F: FieldCore> Sum<&'a Self> for FpExt8 { - fn sum>(iter: I) -> Self { - iter.fold(Self::zero(), |acc, x| acc + *x) - } -} - -impl Product for FpExt8 { - fn product>(iter: I) -> Self { - iter.fold(Self::one(), |acc, x| acc * x) - } -} - -impl<'a, F: FieldCore + ExtMulBackend> Product<&'a Self> for FpExt8 { - fn product>(iter: I) -> Self { - iter.fold(Self::one(), |acc, x| acc * *x) - } -} - -impl AdditiveGroup for FpExt8 {} diff --git a/crates/jolt-field/src/lib.rs b/crates/jolt-field/src/lib.rs index f571e294c4..e37071c968 100644 --- a/crates/jolt-field/src/lib.rs +++ b/crates/jolt-field/src/lib.rs @@ -17,7 +17,6 @@ //! //! - [`Field`] — Jolt compatibility umbrella //! - [`Accumulator`] — deferred-reduction fused multiply-add -//! - [`OptimizedMul`] — fast-path short-circuits for zero/one //! - [`MontgomeryConstants`] — Montgomery form constants for GPU backends //! //! # BN254 types (feature `bn254`) @@ -50,10 +49,12 @@ mod field; mod field_error; mod montgomery_constants; #[cfg(feature = "solinas")] +mod native_algebra; +#[cfg(feature = "solinas")] mod solinas_traits; pub use accumulator::{Accumulator, NaiveAccumulator, WithAccumulator}; -pub use algebra::{AdditiveGroup, FieldCore, FromPrimitiveInt, OptimizedMul, RingCore}; +pub use algebra::{AdditiveGroup, FieldCore, FromPrimitiveInt, RingCore}; pub use canonical::CanonicalRepr; pub use field::Field; pub use field_error::FieldError; diff --git a/crates/jolt-field/src/native_algebra.rs b/crates/jolt-field/src/native_algebra.rs new file mode 100644 index 0000000000..74caf520ae --- /dev/null +++ b/crates/jolt-field/src/native_algebra.rs @@ -0,0 +1,212 @@ +//! Shared macros for the mechanical supertrait obligations of the algebra +//! hierarchy. +//! +//! Each macro is invoked from the concrete type's own file, so a type's full +//! trait surface stays visible where the type is defined; only the expansion +//! is shared. + +/// Implements `Zero`, `One`, `Display`, `Hash`, owned and by-reference +/// `Sum`/`Product`, and the `AdditiveGroup` marker for a ring-like type from +/// per-type leaf expressions. +macro_rules! impl_native_ring_algebra { + ( + impl[$($g:tt)*] $ty:ty { + zero: $zero:expr, + is_zero($isz:ident): $is_zero:expr, + one: $one:expr, + display($dv:ident, $f:ident): $display:expr, + hash($hv:ident, $st:ident): $hash:expr $(,)? + } + ) => { + impl<$($g)*> ::num_traits::Zero for $ty { + #[inline] + fn zero() -> Self { + $zero + } + + #[inline] + fn is_zero(&self) -> bool { + let $isz = self; + $is_zero + } + } + + impl<$($g)*> ::num_traits::One for $ty { + #[inline] + fn one() -> Self { + $one + } + } + + impl<$($g)*> ::std::fmt::Display for $ty { + fn fmt(&self, $f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + let $dv = self; + $display + } + } + + impl<$($g)*> ::std::hash::Hash for $ty { + fn hash(&self, $st: &mut JfHasher) { + let $hv = self; + $hash + } + } + + impl<$($g)*> ::std::iter::Sum for $ty { + fn sum>(iter: I) -> Self { + iter.fold(::zero(), |acc, x| acc + x) + } + } + + impl<'jf_ref, $($g)*> ::std::iter::Sum<&'jf_ref Self> for $ty { + fn sum>(iter: I) -> Self { + iter.fold(::zero(), |acc, x| acc + *x) + } + } + + impl<$($g)*> ::std::iter::Product for $ty { + fn product>(iter: I) -> Self { + iter.fold(::one(), |acc, x| acc * x) + } + } + + impl<'jf_ref, $($g)*> ::std::iter::Product<&'jf_ref Self> for $ty { + fn product>(iter: I) -> Self { + iter.fold(::one(), |acc, x| acc * *x) + } + } + + impl<$($g)*> $crate::AdditiveGroup for $ty {} + }; +} + +/// Implements `Zero`, the by-reference `Add`/`Sub` forwarders, and the +/// `AdditiveGroup` marker for wide accumulator types (no multiplication, no +/// multiplicative identity). +macro_rules! impl_native_additive { + ( + impl[$($g:tt)*] $ty:ty { + zero: $zero:expr, + is_zero($isz:ident): $is_zero:expr $(,)? + } + ) => { + impl<$($g)*> ::num_traits::Zero for $ty { + #[inline] + fn zero() -> Self { + $zero + } + + #[inline] + fn is_zero(&self) -> bool { + let $isz = self; + $is_zero + } + } + + impl<'jf_ref, $($g)*> ::std::ops::Add<&'jf_ref Self> for $ty { + type Output = Self; + + #[inline] + fn add(self, rhs: &'jf_ref Self) -> Self::Output { + self + *rhs + } + } + + impl<'jf_ref, $($g)*> ::std::ops::Sub<&'jf_ref Self> for $ty { + type Output = Self; + + #[inline] + fn sub(self, rhs: &'jf_ref Self) -> Self::Output { + self - *rhs + } + } + + impl<$($g)*> $crate::AdditiveGroup for $ty {} + }; +} + +/// Implements the value, assignment, and by-reference operator matrix for a +/// const-generic Solinas prime type by delegating to its `add_raw`/`sub_raw`/ +/// `mul_raw` kernels. The reduction logic itself stays hand-written per type. +macro_rules! impl_prime_ops { + ($ty:ident<$p:ident: $p_ty:ty>, zero_raw: $zero_raw:expr) => { + impl ::std::ops::Add for $ty<$p> { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self::Output { + Self(Self::add_raw(self.0, rhs.0)) + } + } + + impl ::std::ops::Sub for $ty<$p> { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + Self(Self::sub_raw(self.0, rhs.0)) + } + } + + impl ::std::ops::Mul for $ty<$p> { + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Self(Self::mul_raw(self.0, rhs.0)) + } + } + + impl ::std::ops::Neg for $ty<$p> { + type Output = Self; + #[inline] + fn neg(self) -> Self::Output { + Self(Self::sub_raw($zero_raw, self.0)) + } + } + + impl ::std::ops::AddAssign for $ty<$p> { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } + } + + impl ::std::ops::SubAssign for $ty<$p> { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } + } + + impl ::std::ops::MulAssign for $ty<$p> { + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } + } + + impl<'jf_ref, const $p: $p_ty> ::std::ops::Add<&'jf_ref Self> for $ty<$p> { + type Output = Self; + #[inline] + fn add(self, rhs: &'jf_ref Self) -> Self::Output { + self + *rhs + } + } + + impl<'jf_ref, const $p: $p_ty> ::std::ops::Sub<&'jf_ref Self> for $ty<$p> { + type Output = Self; + #[inline] + fn sub(self, rhs: &'jf_ref Self) -> Self::Output { + self - *rhs + } + } + + impl<'jf_ref, const $p: $p_ty> ::std::ops::Mul<&'jf_ref Self> for $ty<$p> { + type Output = Self; + #[inline] + fn mul(self, rhs: &'jf_ref Self) -> Self::Output { + self * *rhs + } + } + }; +} + +pub(crate) use {impl_native_additive, impl_native_ring_algebra, impl_prime_ops}; diff --git a/crates/jolt-field/src/packed/avx2/fp128.rs b/crates/jolt-field/src/packed/avx2/fp128.rs index df1c3e06bd..a464c71db6 100644 --- a/crates/jolt-field/src/packed/avx2/fp128.rs +++ b/crates/jolt-field/src/packed/avx2/fp128.rs @@ -171,32 +171,6 @@ impl Mul for PackedFp128Avx2

{ } } -impl PackedValue for PackedFp128Avx2

{ - type Value = Fp128

; - const WIDTH: usize = FP128_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Value, - { - let mut lo = [0u64; FP128_WIDTH]; - let mut hi = [0u64; FP128_WIDTH]; - for i in 0..FP128_WIDTH { - let v = f(i); - lo[i] = v.0[0]; - hi[i] = v.0[1]; - } - Self { lo, hi } - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Value { - debug_assert!(lane < FP128_WIDTH); - Fp128([self.lo[lane], self.hi[lane]]) - } -} - impl AddAssign for PackedFp128Avx2

{ #[inline] fn add_assign(&mut self, rhs: Self) { @@ -219,6 +193,29 @@ impl MulAssign for PackedFp128Avx2

{ } impl PackedField for PackedFp128Avx2

{ + const WIDTH: usize = FP128_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Scalar, + { + let mut lo = [0u64; FP128_WIDTH]; + let mut hi = [0u64; FP128_WIDTH]; + for i in 0..FP128_WIDTH { + let v = f(i); + lo[i] = v.0[0]; + hi[i] = v.0[1]; + } + Self { lo, hi } + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Scalar { + debug_assert!(lane < FP128_WIDTH); + Fp128([self.lo[lane], self.hi[lane]]) + } + type Scalar = Fp128

; #[inline] diff --git a/crates/jolt-field/src/packed/avx2/fp32.rs b/crates/jolt-field/src/packed/avx2/fp32.rs index 2741f454e7..ac3ba9c47d 100644 --- a/crates/jolt-field/src/packed/avx2/fp32.rs +++ b/crates/jolt-field/src/packed/avx2/fp32.rs @@ -476,25 +476,6 @@ impl Mul for PackedFp32Avx2

{ } } -impl PackedValue for PackedFp32Avx2

{ - type Value = Fp32

; - const WIDTH: usize = FP32_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Value, - { - Self([f(0), f(1), f(2), f(3), f(4), f(5), f(6), f(7)]) - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Value { - debug_assert!(lane < FP32_WIDTH); - self.0[lane] - } -} - impl AddAssign for PackedFp32Avx2

{ #[inline] fn add_assign(&mut self, rhs: Self) { @@ -517,6 +498,22 @@ impl MulAssign for PackedFp32Avx2

{ } impl PackedField for PackedFp32Avx2

{ + const WIDTH: usize = FP32_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Scalar, + { + Self([f(0), f(1), f(2), f(3), f(4), f(5), f(6), f(7)]) + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Scalar { + debug_assert!(lane < FP32_WIDTH); + self.0[lane] + } + type Scalar = Fp32

; #[inline] @@ -613,7 +610,7 @@ impl PackedField for PackedFp32Avx2

{ #[inline(always)] fn fp_ext4_inverse(a: [Self; 4]) -> Option<[Self; 4]> where - Self::Scalar: Invertible, + Self::Scalar: FieldCore, { unsafe { let [a0, a1, a2, a3] = a.map(Self::to_vec); diff --git a/crates/jolt-field/src/packed/avx2/fp64.rs b/crates/jolt-field/src/packed/avx2/fp64.rs index 86350ef42d..a4074154b6 100644 --- a/crates/jolt-field/src/packed/avx2/fp64.rs +++ b/crates/jolt-field/src/packed/avx2/fp64.rs @@ -217,25 +217,6 @@ impl Mul for PackedFp64Avx2

{ } } -impl PackedValue for PackedFp64Avx2

{ - type Value = Fp64

; - const WIDTH: usize = FP64_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Value, - { - Self([f(0), f(1), f(2), f(3)]) - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Value { - debug_assert!(lane < FP64_WIDTH); - self.0[lane] - } -} - impl AddAssign for PackedFp64Avx2

{ #[inline] fn add_assign(&mut self, rhs: Self) { @@ -258,6 +239,22 @@ impl MulAssign for PackedFp64Avx2

{ } impl PackedField for PackedFp64Avx2

{ + const WIDTH: usize = FP64_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Scalar, + { + Self([f(0), f(1), f(2), f(3)]) + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Scalar { + debug_assert!(lane < FP64_WIDTH); + self.0[lane] + } + type Scalar = Fp64

; #[inline] diff --git a/crates/jolt-field/src/packed/avx2/mod.rs b/crates/jolt-field/src/packed/avx2/mod.rs index 4fdb081afb..ce89f4ac8d 100644 --- a/crates/jolt-field/src/packed/avx2/mod.rs +++ b/crates/jolt-field/src/packed/avx2/mod.rs @@ -7,7 +7,7 @@ reason = "ported AVX2 kernels retain their audited intrinsic-level invariants" )] -use super::{PackedField, PackedValue}; +use super::{PackedField}; use crate::ext::FpExt2Config; use crate::FieldCore; use crate::{Fp128, Fp32, Fp64}; diff --git a/crates/jolt-field/src/packed/avx512/fp128.rs b/crates/jolt-field/src/packed/avx512/fp128.rs index 8a1dd3791d..f0bbcc49ed 100644 --- a/crates/jolt-field/src/packed/avx512/fp128.rs +++ b/crates/jolt-field/src/packed/avx512/fp128.rs @@ -146,32 +146,6 @@ impl Mul for PackedFp128Avx512

{ } } -impl PackedValue for PackedFp128Avx512

{ - type Value = Fp128

; - const WIDTH: usize = FP128_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Value, - { - let mut lo = [0u64; FP128_WIDTH]; - let mut hi = [0u64; FP128_WIDTH]; - for i in 0..FP128_WIDTH { - let v = f(i); - lo[i] = v.0[0]; - hi[i] = v.0[1]; - } - Self { lo, hi } - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Value { - debug_assert!(lane < FP128_WIDTH); - Fp128([self.lo[lane], self.hi[lane]]) - } -} - impl AddAssign for PackedFp128Avx512

{ #[inline] fn add_assign(&mut self, rhs: Self) { @@ -194,6 +168,29 @@ impl MulAssign for PackedFp128Avx512

{ } impl PackedField for PackedFp128Avx512

{ + const WIDTH: usize = FP128_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Scalar, + { + let mut lo = [0u64; FP128_WIDTH]; + let mut hi = [0u64; FP128_WIDTH]; + for i in 0..FP128_WIDTH { + let v = f(i); + lo[i] = v.0[0]; + hi[i] = v.0[1]; + } + Self { lo, hi } + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Scalar { + debug_assert!(lane < FP128_WIDTH); + Fp128([self.lo[lane], self.hi[lane]]) + } + type Scalar = Fp128

; #[inline] diff --git a/crates/jolt-field/src/packed/avx512/fp32.rs b/crates/jolt-field/src/packed/avx512/fp32.rs index 96175d9753..a53895e22a 100644 --- a/crates/jolt-field/src/packed/avx512/fp32.rs +++ b/crates/jolt-field/src/packed/avx512/fp32.rs @@ -452,14 +452,34 @@ impl Mul for PackedFp32Avx512

{ } } -impl PackedValue for PackedFp32Avx512

{ - type Value = Fp32

; +impl AddAssign for PackedFp32Avx512

{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for PackedFp32Avx512

{ + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign for PackedFp32Avx512

{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl PackedField for PackedFp32Avx512

{ const WIDTH: usize = FP32_WIDTH; #[inline] fn from_fn(mut f: F) -> Self where - F: FnMut(usize) -> Self::Value, + F: FnMut(usize) -> Self::Scalar, { Self([ f(0), @@ -482,34 +502,11 @@ impl PackedValue for PackedFp32Avx512

{ } #[inline] - fn extract(&self, lane: usize) -> Self::Value { + fn extract(&self, lane: usize) -> Self::Scalar { debug_assert!(lane < FP32_WIDTH); self.0[lane] } -} -impl AddAssign for PackedFp32Avx512

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for PackedFp32Avx512

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for PackedFp32Avx512

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl PackedField for PackedFp32Avx512

{ type Scalar = Fp32

; #[inline] @@ -606,7 +603,7 @@ impl PackedField for PackedFp32Avx512

{ #[inline(always)] fn fp_ext4_inverse(a: [Self; 4]) -> Option<[Self; 4]> where - Self::Scalar: Invertible, + Self::Scalar: FieldCore, { unsafe { let [a0, a1, a2, a3] = a.map(Self::to_vec); diff --git a/crates/jolt-field/src/packed/avx512/fp64.rs b/crates/jolt-field/src/packed/avx512/fp64.rs index bb304497c0..6d60e6ca9d 100644 --- a/crates/jolt-field/src/packed/avx512/fp64.rs +++ b/crates/jolt-field/src/packed/avx512/fp64.rs @@ -196,25 +196,6 @@ impl Mul for PackedFp64Avx512

{ } } -impl PackedValue for PackedFp64Avx512

{ - type Value = Fp64

; - const WIDTH: usize = FP64_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Value, - { - Self([f(0), f(1), f(2), f(3), f(4), f(5), f(6), f(7)]) - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Value { - debug_assert!(lane < FP64_WIDTH); - self.0[lane] - } -} - impl AddAssign for PackedFp64Avx512

{ #[inline] fn add_assign(&mut self, rhs: Self) { @@ -237,6 +218,22 @@ impl MulAssign for PackedFp64Avx512

{ } impl PackedField for PackedFp64Avx512

{ + const WIDTH: usize = FP64_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Scalar, + { + Self([f(0), f(1), f(2), f(3), f(4), f(5), f(6), f(7)]) + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Scalar { + debug_assert!(lane < FP64_WIDTH); + self.0[lane] + } + type Scalar = Fp64

; #[inline] diff --git a/crates/jolt-field/src/packed/avx512/mod.rs b/crates/jolt-field/src/packed/avx512/mod.rs index f8b05f5e60..801560074c 100644 --- a/crates/jolt-field/src/packed/avx512/mod.rs +++ b/crates/jolt-field/src/packed/avx512/mod.rs @@ -8,7 +8,7 @@ reason = "ported AVX-512 kernels retain their audited intrinsic-level invariants" )] -use super::{PackedField, PackedValue}; +use super::{PackedField}; use crate::ext::FpExt2Config; use crate::FieldCore; use crate::{Fp128, Fp32, Fp64}; diff --git a/crates/jolt-field/src/packed/ext/mod.rs b/crates/jolt-field/src/packed/ext/mod.rs index 1e17fc0779..b7e73402b4 100644 --- a/crates/jolt-field/src/packed/ext/mod.rs +++ b/crates/jolt-field/src/packed/ext/mod.rs @@ -11,7 +11,7 @@ )] use crate::ext::{ExtMulBackend, FpExt2, FpExt2Config, FpExt4, FpExt8}; -use crate::packed::{HasPacking, PackedField, PackedValue}; +use crate::packed::{HasPacking, PackedField}; use crate::FieldCore; use core::ops::{Add, Mul, Sub}; @@ -59,34 +59,6 @@ impl, PF: PackedField> PackedFpExt2 } } -impl PackedValue for PackedFpExt2 -where - F: FieldCore + 'static, - C: FpExt2Config + 'static, - PF: PackedField, -{ - type Value = FpExt2; - const WIDTH: usize = PF::WIDTH; - - fn from_fn(mut f: G) -> Self - where - G: FnMut(usize) -> Self::Value, - { - let mut c0s = Vec::with_capacity(PF::WIDTH); - let mut c1s = Vec::with_capacity(PF::WIDTH); - for i in 0..PF::WIDTH { - let val = f(i); - c0s.push(val.coeffs[0]); - c1s.push(val.coeffs[1]); - } - Self::new(PF::from_fn(|i| c0s[i]), PF::from_fn(|i| c1s[i])) - } - - fn extract(&self, lane: usize) -> Self::Value { - FpExt2::new(self.c0.extract(lane), self.c1.extract(lane)) - } -} - impl Add for PackedFpExt2 where F: FieldCore, @@ -133,6 +105,25 @@ where C: FpExt2Config + 'static, PF: PackedField, { + const WIDTH: usize = PF::WIDTH; + + fn from_fn(mut f: G) -> Self + where + G: FnMut(usize) -> Self::Scalar, + { + let mut c0s = Vec::with_capacity(PF::WIDTH); + let mut c1s = Vec::with_capacity(PF::WIDTH); + for i in 0..PF::WIDTH { + let val = f(i); + c0s.push(val.coeffs[0]); + c1s.push(val.coeffs[1]); + } + Self::new(PF::from_fn(|i| c0s[i]), PF::from_fn(|i| c1s[i])) + } + + fn extract(&self, lane: usize) -> Self::Scalar { + FpExt2::new(self.c0.extract(lane), self.c1.extract(lane)) + } type Scalar = FpExt2; #[inline] @@ -218,33 +209,6 @@ where } } -impl PackedValue for PackedFpExt4 -where - F: FieldCore + 'static, - PF: PackedField, -{ - type Value = FpExt4; - const WIDTH: usize = PF::WIDTH; - - fn from_fn(mut f: G) -> Self - where - G: FnMut(usize) -> Self::Value, - { - let mut coeffs: [Vec; 4] = std::array::from_fn(|_| Vec::with_capacity(PF::WIDTH)); - for i in 0..PF::WIDTH { - let val = f(i); - for (j, coeff) in val.coeffs.into_iter().enumerate() { - coeffs[j].push(coeff); - } - } - Self::new(std::array::from_fn(|j| PF::from_fn(|i| coeffs[j][i]))) - } - - fn extract(&self, lane: usize) -> Self::Value { - FpExt4::new(std::array::from_fn(|j| self.coeffs[j].extract(lane))) - } -} - impl Add for PackedFpExt4 where F: FieldCore, @@ -290,6 +254,25 @@ where F: FieldCore + ExtMulBackend + 'static, PF: PackedField, { + const WIDTH: usize = PF::WIDTH; + + fn from_fn(mut f: G) -> Self + where + G: FnMut(usize) -> Self::Scalar, + { + let mut coeffs: [Vec; 4] = std::array::from_fn(|_| Vec::with_capacity(PF::WIDTH)); + for i in 0..PF::WIDTH { + let val = f(i); + for (j, coeff) in val.coeffs.into_iter().enumerate() { + coeffs[j].push(coeff); + } + } + Self::new(std::array::from_fn(|j| PF::from_fn(|i| coeffs[j][i]))) + } + + fn extract(&self, lane: usize) -> Self::Scalar { + FpExt4::new(std::array::from_fn(|j| self.coeffs[j].extract(lane))) + } type Scalar = FpExt4; #[inline] @@ -369,33 +352,6 @@ where } } -impl PackedValue for PackedFpExt8 -where - F: FieldCore + 'static, - PF: PackedField, -{ - type Value = FpExt8; - const WIDTH: usize = PF::WIDTH; - - fn from_fn(mut f: G) -> Self - where - G: FnMut(usize) -> Self::Value, - { - let mut coeffs: [Vec; 8] = std::array::from_fn(|_| Vec::with_capacity(PF::WIDTH)); - for i in 0..PF::WIDTH { - let val = f(i); - for (j, coeff) in val.coeffs.into_iter().enumerate() { - coeffs[j].push(coeff); - } - } - Self::new(std::array::from_fn(|j| PF::from_fn(|i| coeffs[j][i]))) - } - - fn extract(&self, lane: usize) -> Self::Value { - FpExt8::new(std::array::from_fn(|j| self.coeffs[j].extract(lane))) - } -} - impl Add for PackedFpExt8 where F: FieldCore, @@ -437,6 +393,25 @@ where F: FieldCore + ExtMulBackend + 'static, PF: PackedField, { + const WIDTH: usize = PF::WIDTH; + + fn from_fn(mut f: G) -> Self + where + G: FnMut(usize) -> Self::Scalar, + { + let mut coeffs: [Vec; 8] = std::array::from_fn(|_| Vec::with_capacity(PF::WIDTH)); + for i in 0..PF::WIDTH { + let val = f(i); + for (j, coeff) in val.coeffs.into_iter().enumerate() { + coeffs[j].push(coeff); + } + } + Self::new(std::array::from_fn(|j| PF::from_fn(|i| coeffs[j][i]))) + } + + fn extract(&self, lane: usize) -> Self::Scalar { + FpExt8::new(std::array::from_fn(|j| self.coeffs[j].extract(lane))) + } type Scalar = FpExt8; #[inline] diff --git a/crates/jolt-field/src/packed/ext/tests.rs b/crates/jolt-field/src/packed/ext/tests.rs index 52a7ecb236..b66d0a5dae 100644 --- a/crates/jolt-field/src/packed/ext/tests.rs +++ b/crates/jolt-field/src/packed/ext/tests.rs @@ -49,7 +49,7 @@ fn fp32_ext_edge_values() -> [Fp32

; 4] { fn check_packed_fp_ext4_edge() where - PR4: PackedField>> + PackedValue>>, + PR4: PackedField>>, { let values = fp32_ext_edge_values::

(); let elem = |offset: usize| { @@ -79,7 +79,7 @@ where #[test] fn packed_fp_ext2_add() { let mut rng = StdRng::seed_from_u64(100); - let width = ::WIDTH; + let width = ::WIDTH; let a_elems: Vec = (0..width).map(|_| E2::random(&mut rng)).collect(); let b_elems: Vec = (0..width).map(|_| E2::random(&mut rng)).collect(); @@ -95,7 +95,7 @@ fn packed_fp_ext2_add() { #[test] fn packed_fp_ext2_mul() { let mut rng = StdRng::seed_from_u64(200); - let width = ::WIDTH; + let width = ::WIDTH; let a_elems: Vec = (0..width).map(|_| E2::random(&mut rng)).collect(); let b_elems: Vec = (0..width).map(|_| E2::random(&mut rng)).collect(); @@ -115,7 +115,7 @@ fn packed_fp_ext2_mul() { #[test] fn packed_fp_ext2_mul_full_word_fp64() { let mut rng = StdRng::seed_from_u64(201); - let width = ::WIDTH; + let width = ::WIDTH; let a_elems: Vec = (0..width).map(|_| E2Full::random(&mut rng)).collect(); let b_elems: Vec = (0..width).map(|_| E2Full::random(&mut rng)).collect(); @@ -136,7 +136,7 @@ fn packed_fp_ext2_mul_full_word_fp64() { fn packed_fp_ext2_broadcast() { let val = E2::new(F::from_u64(7), F::from_u64(11)); let packed = PE2::broadcast(val); - let width = ::WIDTH; + let width = ::WIDTH; for i in 0..width { assert_eq!(packed.extract(i), val); } @@ -145,7 +145,7 @@ fn packed_fp_ext2_broadcast() { #[test] fn packed_fp_ext4_add() { let mut rng = StdRng::seed_from_u64(360); - let width = ::WIDTH; + let width = ::WIDTH; let a_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); let b_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); @@ -165,7 +165,7 @@ fn packed_fp_ext4_add() { #[test] fn packed_fp_ext4_sub() { let mut rng = StdRng::seed_from_u64(361); - let width = ::WIDTH; + let width = ::WIDTH; let a_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); let b_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); @@ -185,7 +185,7 @@ fn packed_fp_ext4_sub() { #[test] fn packed_fp_ext4_mul() { let mut rng = StdRng::seed_from_u64(362); - let width = ::WIDTH; + let width = ::WIDTH; let a_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); let b_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); @@ -205,7 +205,7 @@ fn packed_fp_ext4_mul() { #[test] fn packed_fp_ext4_mul_prime32() { let mut rng = StdRng::seed_from_u64(365); - let width = ::WIDTH; + let width = ::WIDTH; let a_elems: Vec = (0..width).map(|_| R4Prime32::random(&mut rng)).collect(); let b_elems: Vec = (0..width).map(|_| R4Prime32::random(&mut rng)).collect(); @@ -261,7 +261,7 @@ fn packed_fp_ext4_large_generic31_edge_lanes() { #[test] fn packed_fp_ext4_square() { let mut rng = StdRng::seed_from_u64(363); - let width = ::WIDTH; + let width = ::WIDTH; let elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); let packed = PR4::from_fn(|i| elems[i]); @@ -279,7 +279,7 @@ fn packed_fp_ext4_square() { #[test] fn packed_fp_ext4_square_prime32() { let mut rng = StdRng::seed_from_u64(366); - let width = ::WIDTH; + let width = ::WIDTH; let elems: Vec = (0..width).map(|_| R4Prime32::random(&mut rng)).collect(); let packed = PR4Prime32::from_fn(|i| elems[i]); @@ -298,7 +298,7 @@ fn packed_fp_ext4_square_prime32() { fn packed_fp_ext4_square_mersenne31() { let mut rng = StdRng::seed_from_u64(367); type R4M31 = FpExt4; - let width = ::WIDTH; + let width = ::WIDTH; let elems: Vec = (0..width).map(|_| R4M31::random(&mut rng)).collect(); let packed = PR4Mersenne31::from_fn(|i| elems[i]); @@ -316,7 +316,7 @@ fn packed_fp_ext4_square_mersenne31() { #[test] fn packed_fp_ext4_inverse() { let mut rng = StdRng::seed_from_u64(367); - let width = ::WIDTH; + let width = ::WIDTH; let elems: Vec = (0..width) .map(|_| { let x = R4::random(&mut rng); @@ -349,7 +349,7 @@ fn packed_fp_ext4_broadcast() { F::from_u64(17), ]); let packed = PR4::broadcast(val); - let width = ::WIDTH; + let width = ::WIDTH; for i in 0..width { assert_eq!(packed.extract(i), val); } @@ -358,7 +358,7 @@ fn packed_fp_ext4_broadcast() { #[test] fn packed_fp_ext4_pack_unpack() { let mut rng = StdRng::seed_from_u64(364); - let width = ::WIDTH; + let width = ::WIDTH; let elems: Vec = (0..width * 3).map(|_| R4::random(&mut rng)).collect(); let packed = PR4::pack_slice(&elems); @@ -370,7 +370,7 @@ fn packed_fp_ext4_pack_unpack() { #[test] fn pack_unpack_roundtrip_fp_ext2() { let mut rng = StdRng::seed_from_u64(400); - let width = ::WIDTH; + let width = ::WIDTH; let elems: Vec = (0..width * 3).map(|_| E2::random(&mut rng)).collect(); let packed = PE2::pack_slice(&elems); @@ -389,7 +389,7 @@ type PR8Prime32 = PackedFpExt8: #[test] fn packed_fp_ext8_mul_fp64() { let mut rng = StdRng::seed_from_u64(500); - let width = ::WIDTH; + let width = ::WIDTH; let a_elems: Vec = (0..width).map(|_| R8Fp64::random(&mut rng)).collect(); let b_elems: Vec = (0..width).map(|_| R8Fp64::random(&mut rng)).collect(); @@ -409,7 +409,7 @@ fn packed_fp_ext8_mul_fp64() { #[test] fn packed_fp_ext8_mul_prime31() { let mut rng = StdRng::seed_from_u64(501); - let width = ::WIDTH; + let width = ::WIDTH; let a_elems: Vec = (0..width).map(|_| R8Prime31::random(&mut rng)).collect(); let b_elems: Vec = (0..width).map(|_| R8Prime31::random(&mut rng)).collect(); @@ -429,7 +429,7 @@ fn packed_fp_ext8_mul_prime31() { #[test] fn packed_fp_ext8_mul_prime32() { let mut rng = StdRng::seed_from_u64(502); - let width = ::WIDTH; + let width = ::WIDTH; let a_elems: Vec = (0..width).map(|_| R8Prime32::random(&mut rng)).collect(); let b_elems: Vec = (0..width).map(|_| R8Prime32::random(&mut rng)).collect(); @@ -449,7 +449,7 @@ fn packed_fp_ext8_mul_prime32() { #[test] fn packed_fp_ext8_square() { let mut rng = StdRng::seed_from_u64(504); - let width = ::WIDTH; + let width = ::WIDTH; let a_elems: Vec = (0..width).map(|_| R8Prime31::random(&mut rng)).collect(); let pa = PR8Prime31::from_fn(|i| a_elems[i]); @@ -477,7 +477,7 @@ fn packed_fp_ext8_broadcast() { F::from_u64(8), ]); let packed = PR8Fp64::broadcast(val); - let width = ::WIDTH; + let width = ::WIDTH; for i in 0..width { assert_eq!(packed.extract(i), val); } diff --git a/crates/jolt-field/src/packed/mod.rs b/crates/jolt-field/src/packed/mod.rs index e7874f0091..675d1d4fd8 100644 --- a/crates/jolt-field/src/packed/mod.rs +++ b/crates/jolt-field/src/packed/mod.rs @@ -26,10 +26,12 @@ use crate::{FieldCore, Fp128, Fp32, Fp64}; use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; use num_traits::Zero; -/// Array-like packed values over a scalar type. -pub trait PackedValue: 'static + Copy + Send + Sync { - /// Scalar value type carried by each lane. - type Value: 'static + Copy + Send + Sync; +/// Packed arithmetic over a scalar field. +pub trait PackedField: + 'static + Copy + Send + Sync + Add + Sub + Mul +{ + /// Scalar field type. + type Scalar: FieldCore; /// Number of scalar lanes. const WIDTH: usize; @@ -37,10 +39,10 @@ pub trait PackedValue: 'static + Copy + Send + Sync { /// Build from a lane generator. fn from_fn(f: F) -> Self where - F: FnMut(usize) -> Self::Value; + F: FnMut(usize) -> Self::Scalar; /// Extract one lane. - fn extract(&self, lane: usize) -> Self::Value; + fn extract(&self, lane: usize) -> Self::Scalar; /// Pack a scalar slice into packed values. /// @@ -48,7 +50,7 @@ pub trait PackedValue: 'static + Copy + Send + Sync { /// /// Panics if the length is not divisible by `WIDTH`. #[inline] - fn pack_slice(buf: &[Self::Value]) -> Vec { + fn pack_slice(buf: &[Self::Scalar]) -> Vec { assert!( buf.len() % Self::WIDTH == 0, "slice length {} must be divisible by WIDTH {}", @@ -62,7 +64,7 @@ pub trait PackedValue: 'static + Copy + Send + Sync { /// Packed prefix + scalar suffix split. #[inline] - fn pack_slice_with_suffix(buf: &[Self::Value]) -> (Vec, &[Self::Value]) { + fn pack_slice_with_suffix(buf: &[Self::Scalar]) -> (Vec, &[Self::Scalar]) { let split = buf.len() - (buf.len() % Self::WIDTH); let (packed, suffix) = buf.split_at(split); (Self::pack_slice(packed), suffix) @@ -70,7 +72,7 @@ pub trait PackedValue: 'static + Copy + Send + Sync { /// Unpack packed values into a flat scalar vector. #[inline] - fn unpack_slice(buf: &[Self]) -> Vec { + fn unpack_slice(buf: &[Self]) -> Vec { let mut out = Vec::with_capacity(buf.len() * Self::WIDTH); for packed in buf { for lane in 0..Self::WIDTH { @@ -79,14 +81,6 @@ pub trait PackedValue: 'static + Copy + Send + Sync { } out } -} - -/// Packed arithmetic over a scalar field. -pub trait PackedField: - PackedValue + Add + Sub + Mul -{ - /// Scalar field type. - type Scalar: FieldCore; /// Broadcast one scalar across all lanes. fn broadcast(value: Self::Scalar) -> Self; @@ -210,28 +204,6 @@ pub trait PackedField: #[repr(transparent)] pub struct NoPacking(pub [T; 1]); -impl PackedValue for NoPacking -where - T: 'static + Copy + Send + Sync, -{ - type Value = T; - const WIDTH: usize = 1; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Value, - { - Self([f(0)]) - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Value { - debug_assert_eq!(lane, 0); - self.0[0] - } -} - impl Add for NoPacking { type Output = Self; #[inline] @@ -278,6 +250,21 @@ impl MulAssign for NoPacking { } impl PackedField for NoPacking { + const WIDTH: usize = 1; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Scalar, + { + Self([f(0)]) + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Scalar { + debug_assert_eq!(lane, 0); + self.0[0] + } type Scalar = T; #[inline] @@ -292,98 +279,52 @@ pub trait HasPacking: FieldCore { type Packing: PackedField; } -/// Selected packed backend for `Fp128`. -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -pub type Fp128Packing = neon::PackedFp128Neon

; - -/// Selected packed backend for `Fp128`. -#[cfg(all( - target_arch = "x86_64", - target_feature = "avx512f", - target_feature = "avx512dq" -))] -pub type Fp128Packing = avx512::PackedFp128Avx512

; - -/// Selected packed backend for `Fp128`. -#[cfg(all( - target_arch = "x86_64", - target_feature = "avx2", - not(all(target_feature = "avx512f", target_feature = "avx512dq")) -))] -pub type Fp128Packing = avx2::PackedFp128Avx2

; - -/// Selected packed backend for `Fp128`. -#[cfg(not(any( - all(target_arch = "aarch64", target_feature = "neon"), - all(target_arch = "x86_64", target_feature = "avx2") -)))] -pub type Fp128Packing = NoPacking>; - -impl HasPacking for Fp128

{ - type Packing = Fp128Packing

; -} - -/// Selected packed backend for `Fp32`. -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -pub type Fp32Packing = neon::PackedFp32Neon

; - -/// Selected packed backend for `Fp32`. -#[cfg(all( - target_arch = "x86_64", - target_feature = "avx512f", - target_feature = "avx512dq" -))] -pub type Fp32Packing = avx512::PackedFp32Avx512

; - -/// Selected packed backend for `Fp32`. -#[cfg(all( - target_arch = "x86_64", - target_feature = "avx2", - not(all(target_feature = "avx512f", target_feature = "avx512dq")) -))] -pub type Fp32Packing = avx2::PackedFp32Avx2

; - -/// Selected packed backend for `Fp32`. -#[cfg(not(any( - all(target_arch = "aarch64", target_feature = "neon"), - all(target_arch = "x86_64", target_feature = "avx2") -)))] -pub type Fp32Packing = NoPacking>; - -impl HasPacking for Fp32

{ - type Packing = Fp32Packing

; +/// Selects the packed backend for a Solinas prime at compile time: +/// NEON on aarch64, AVX-512 then AVX2 on x86_64, scalar `NoPacking` otherwise. +macro_rules! select_packing { + ($alias:ident<$p:ident: $p_ty:ty>, $scalar:ident, $neon:ident, $avx512:ident, $avx2:ident) => { + /// Selected packed backend for this prime width. + #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] + pub type $alias = neon::$neon<$p>; + + /// Selected packed backend for this prime width. + #[cfg(all( + target_arch = "x86_64", + target_feature = "avx512f", + target_feature = "avx512dq" + ))] + pub type $alias = avx512::$avx512<$p>; + + /// Selected packed backend for this prime width. + #[cfg(all( + target_arch = "x86_64", + target_feature = "avx2", + not(all(target_feature = "avx512f", target_feature = "avx512dq")) + ))] + pub type $alias = avx2::$avx2<$p>; + + /// Selected packed backend for this prime width. + #[cfg(not(any( + all(target_arch = "aarch64", target_feature = "neon"), + all(target_arch = "x86_64", target_feature = "avx2") + )))] + pub type $alias = NoPacking<$scalar<$p>>; + + impl HasPacking for $scalar<$p> { + type Packing = $alias<$p>; + } + }; } -/// Selected packed backend for `Fp64`. -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -pub type Fp64Packing = neon::PackedFp64Neon

; - -/// Selected packed backend for `Fp64`. -#[cfg(all( - target_arch = "x86_64", - target_feature = "avx512f", - target_feature = "avx512dq" -))] -pub type Fp64Packing = avx512::PackedFp64Avx512

; - -/// Selected packed backend for `Fp64`. -#[cfg(all( - target_arch = "x86_64", - target_feature = "avx2", - not(all(target_feature = "avx512f", target_feature = "avx512dq")) -))] -pub type Fp64Packing = avx2::PackedFp64Avx2

; - -/// Selected packed backend for `Fp64`. -#[cfg(not(any( - all(target_arch = "aarch64", target_feature = "neon"), - all(target_arch = "x86_64", target_feature = "avx2") -)))] -pub type Fp64Packing = NoPacking>; - -impl HasPacking for Fp64

{ - type Packing = Fp64Packing

; -} +select_packing!(Fp32Packing, Fp32, PackedFp32Neon, PackedFp32Avx512, PackedFp32Avx2); +select_packing!(Fp64Packing, Fp64, PackedFp64Neon, PackedFp64Avx512, PackedFp64Avx2); +select_packing!( + Fp128Packing, + Fp128, + PackedFp128Neon, + PackedFp128Avx512, + PackedFp128Avx2 +); #[cfg(test)] mod tests; diff --git a/crates/jolt-field/src/packed/neon/fp128.rs b/crates/jolt-field/src/packed/neon/fp128.rs index a7f1ba0b99..7bc75bce0e 100644 --- a/crates/jolt-field/src/packed/neon/fp128.rs +++ b/crates/jolt-field/src/packed/neon/fp128.rs @@ -161,30 +161,6 @@ impl PartialEq for PackedFp128Neon

{ impl Eq for PackedFp128Neon

{} -impl PackedValue for PackedFp128Neon

{ - type Value = Fp128

; - const WIDTH: usize = FP128_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Value, - { - let x0 = f(0); - let x1 = f(1); - Self { - lo: [x0.0[0], x1.0[0]], - hi: [x0.0[1], x1.0[1]], - } - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Value { - debug_assert!(lane < FP128_WIDTH); - Fp128([self.lo[lane], self.hi[lane]]) - } -} - impl Add for PackedFp128Neon

{ type Output = Self; #[inline] @@ -305,6 +281,27 @@ impl MulAssign for PackedFp128Neon

{ } impl PackedField for PackedFp128Neon

{ + const WIDTH: usize = FP128_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Scalar, + { + let x0 = f(0); + let x1 = f(1); + Self { + lo: [x0.0[0], x1.0[0]], + hi: [x0.0[1], x1.0[1]], + } + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Scalar { + debug_assert!(lane < FP128_WIDTH); + Fp128([self.lo[lane], self.hi[lane]]) + } + type Scalar = Fp128

; #[inline] diff --git a/crates/jolt-field/src/packed/neon/fp32.rs b/crates/jolt-field/src/packed/neon/fp32.rs index 38da7bcb01..abae91537d 100644 --- a/crates/jolt-field/src/packed/neon/fp32.rs +++ b/crates/jolt-field/src/packed/neon/fp32.rs @@ -617,27 +617,6 @@ impl Mul for PackedFp32Neon

{ } } -impl PackedValue for PackedFp32Neon

{ - type Value = Fp32

; - const WIDTH: usize = FP32_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Value, - { - Self { - vals: [f(0).0, f(1).0, f(2).0, f(3).0], - } - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Value { - debug_assert!(lane < FP32_WIDTH); - Fp32(self.vals[lane]) - } -} - impl AddAssign for PackedFp32Neon

{ #[inline] fn add_assign(&mut self, rhs: Self) { @@ -660,6 +639,24 @@ impl MulAssign for PackedFp32Neon

{ } impl PackedField for PackedFp32Neon

{ + const WIDTH: usize = FP32_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Scalar, + { + Self { + vals: [f(0).0, f(1).0, f(2).0, f(3).0], + } + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Scalar { + debug_assert!(lane < FP32_WIDTH); + Fp32(self.vals[lane]) + } + type Scalar = Fp32

; #[inline] diff --git a/crates/jolt-field/src/packed/neon/fp64.rs b/crates/jolt-field/src/packed/neon/fp64.rs index 9572bb8649..0b18f23f0e 100644 --- a/crates/jolt-field/src/packed/neon/fp64.rs +++ b/crates/jolt-field/src/packed/neon/fp64.rs @@ -158,27 +158,6 @@ impl Mul for PackedFp64Neon

{ } } -impl PackedValue for PackedFp64Neon

{ - type Value = Fp64

; - const WIDTH: usize = FP64_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Value, - { - Self { - vals: [f(0).0, f(1).0], - } - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Value { - debug_assert!(lane < FP64_WIDTH); - Fp64(self.vals[lane]) - } -} - impl AddAssign for PackedFp64Neon

{ #[inline] fn add_assign(&mut self, rhs: Self) { @@ -201,6 +180,24 @@ impl MulAssign for PackedFp64Neon

{ } impl PackedField for PackedFp64Neon

{ + const WIDTH: usize = FP64_WIDTH; + + #[inline] + fn from_fn(mut f: F) -> Self + where + F: FnMut(usize) -> Self::Scalar, + { + Self { + vals: [f(0).0, f(1).0], + } + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Scalar { + debug_assert!(lane < FP64_WIDTH); + Fp64(self.vals[lane]) + } + type Scalar = Fp64

; #[inline] diff --git a/crates/jolt-field/src/packed/neon/mod.rs b/crates/jolt-field/src/packed/neon/mod.rs index 68ee9812bf..28a8d2b2d6 100644 --- a/crates/jolt-field/src/packed/neon/mod.rs +++ b/crates/jolt-field/src/packed/neon/mod.rs @@ -5,7 +5,7 @@ reason = "ported NEON kernels retain their audited intrinsic-level invariants" )] -use super::{PackedField, PackedValue}; +use super::PackedField; use crate::ext::FpExt2Config; use crate::FieldCore; use crate::{Fp128, Fp32, Fp64}; diff --git a/crates/jolt-field/src/packed/tests.rs b/crates/jolt-field/src/packed/tests.rs index b4f5f32214..595bc13f53 100644 --- a/crates/jolt-field/src/packed/tests.rs +++ b/crates/jolt-field/src/packed/tests.rs @@ -3,7 +3,7 @@ reason = "packed regression vectors retain their generated decimal form" )] -use super::{HasPacking, PackedField, PackedValue}; +use super::{HasPacking, PackedField}; use crate::{ CanonicalField, FieldCore, Fp32, Prime128Offset275, Prime24Offset3, Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime64Offset59, @@ -19,7 +19,7 @@ fn rand_u128(rng: &mut R) -> u128 { fn check_packed_add_sub_mul(seed: u64) where F: FieldCore + PartialEq + std::fmt::Debug, - PF: PackedField + PackedValue, + PF: PackedField, { let mut rng = StdRng::seed_from_u64(seed); let len = PF::WIDTH * 17 + 3; @@ -77,7 +77,7 @@ where fn check_broadcast_roundtrip(val: F) where F: FieldCore + PartialEq + std::fmt::Debug, - PF: PackedField + PackedValue, + PF: PackedField, { let p = PF::broadcast(val); for lane in 0..PF::WIDTH { @@ -87,7 +87,7 @@ where fn check_packed_fp32_edge_lanes() where - PF: PackedField> + PackedValue>, + PF: PackedField>, { let p_minus_one = Fp32::

::from_canonical_u32(P - 1); let p_minus_two = Fp32::

::from_canonical_u32(P - 2); diff --git a/crates/jolt-field/src/prime/fp128/mod.rs b/crates/jolt-field/src/prime/fp128/mod.rs index 6a4f51b413..9c22f72a67 100644 --- a/crates/jolt-field/src/prime/fp128/mod.rs +++ b/crates/jolt-field/src/prime/fp128/mod.rs @@ -31,7 +31,6 @@ mod wide; #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] use ::core::arch::asm; -use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use crate::{FieldCore, FromPrimitiveInt}; use rand_core::RngCore; diff --git a/crates/jolt-field/src/prime/fp128/traits.rs b/crates/jolt-field/src/prime/fp128/traits.rs index 70e685f431..054eb43f8e 100644 --- a/crates/jolt-field/src/prime/fp128/traits.rs +++ b/crates/jolt-field/src/prime/fp128/traits.rs @@ -1,81 +1,10 @@ use super::*; -impl Add for Fp128

{ - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self::Output { - Self(Self::add_raw(self.0, rhs.0)) - } -} - -impl Sub for Fp128

{ - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self::Output { - Self(Self::sub_raw(self.0, rhs.0)) - } -} - -impl Mul for Fp128

{ - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self::Output { - Self(Self::mul_raw(self.0, rhs.0)) - } -} - -impl Neg for Fp128

{ - type Output = Self; - #[inline] - fn neg(self) -> Self::Output { - Self(Self::sub_raw(pack(0, 0), self.0)) - } -} - -impl AddAssign for Fp128

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for Fp128

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} +use crate::native_algebra::{impl_native_ring_algebra, impl_prime_ops}; +use crate::prime::native_capability::impl_prime_native_capability; +use crate::RingCore; -impl MulAssign for Fp128

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl<'a, const P: u128> Add<&'a Self> for Fp128

{ - type Output = Self; - #[inline] - fn add(self, rhs: &'a Self) -> Self::Output { - self + *rhs - } -} - -impl<'a, const P: u128> Sub<&'a Self> for Fp128

{ - type Output = Self; - #[inline] - fn sub(self, rhs: &'a Self) -> Self::Output { - self - *rhs - } -} - -impl<'a, const P: u128> Mul<&'a Self> for Fp128

{ - type Output = Self; - #[inline] - fn mul(self, rhs: &'a Self) -> Self::Output { - self * *rhs - } -} +impl_prime_ops!(Fp128, zero_raw: pack(0, 0)); impl FieldCore for Fp128

{ #[inline(always)] @@ -177,6 +106,20 @@ impl PseudoMersenneField for Fp128

{ const MODULUS_OFFSET: u128 = Self::C; } +impl_native_ring_algebra!( + impl[const P: u128] Fp128

{ + zero: Self::default(), + is_zero(x): x.to_canonical_u128() == 0, + one: if P > 1 { Self::from_canonical_u128(1) } else { Self::default() }, + display(x, f): write!(f, "{}", x.to_canonical_u128()), + hash(x, state): ::std::hash::Hash::hash(&x.to_canonical_u128(), state), + } +); + +impl RingCore for Fp128

{} + +impl_prime_native_capability!(Fp128, 16); + impl serde::Serialize for Fp128

{ fn serialize(&self, serializer: S) -> Result { let buf = self.to_canonical_u128().to_le_bytes(); diff --git a/crates/jolt-field/src/prime/fp32.rs b/crates/jolt-field/src/prime/fp32.rs index 8cf30ca55e..73e4f6c426 100644 --- a/crates/jolt-field/src/prime/fp32.rs +++ b/crates/jolt-field/src/prime/fp32.rs @@ -4,9 +4,9 @@ //! Uses Solinas-style two-fold reduction: the offset `c` and fold point `k` //! are computed at compile time from the const-generic modulus `P`. -use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; - -use crate::{FieldCore, FromPrimitiveInt}; +use crate::native_algebra::{impl_native_ring_algebra, impl_prime_ops}; +use crate::prime::native_capability::impl_prime_native_capability; +use crate::{FieldCore, FromPrimitiveInt, RingCore}; use rand_core::RngCore; use crate::{CanonicalField, HalvingField, PseudoMersenneField}; @@ -278,82 +278,7 @@ impl Fp32

{ } } -impl Add for Fp32

{ - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self::Output { - Self(Self::add_raw(self.0, rhs.0)) - } -} - -impl Sub for Fp32

{ - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self::Output { - Self(Self::sub_raw(self.0, rhs.0)) - } -} - -impl Mul for Fp32

{ - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self::Output { - Self(Self::mul_raw(self.0, rhs.0)) - } -} - -impl Neg for Fp32

{ - type Output = Self; - #[inline] - fn neg(self) -> Self::Output { - Self(Self::sub_raw(0, self.0)) - } -} - -impl AddAssign for Fp32

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for Fp32

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for Fp32

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl<'a, const P: u32> Add<&'a Self> for Fp32

{ - type Output = Self; - #[inline] - fn add(self, rhs: &'a Self) -> Self::Output { - self + *rhs - } -} - -impl<'a, const P: u32> Sub<&'a Self> for Fp32

{ - type Output = Self; - #[inline] - fn sub(self, rhs: &'a Self) -> Self::Output { - self - *rhs - } -} - -impl<'a, const P: u32> Mul<&'a Self> for Fp32

{ - type Output = Self; - #[inline] - fn mul(self, rhs: &'a Self) -> Self::Output { - self * *rhs - } -} +impl_prime_ops!(Fp32, zero_raw: 0); impl FieldCore for Fp32

{ #[inline(always)] @@ -446,6 +371,20 @@ impl PseudoMersenneField for Fp32

{ const MODULUS_OFFSET: u128 = Self::C as u128; } +impl_native_ring_algebra!( + impl[const P: u32] Fp32

{ + zero: Self::default(), + is_zero(x): x.to_canonical_u128() == 0, + one: if P > 1 { Self::from_canonical_u32(1) } else { Self::default() }, + display(x, f): write!(f, "{}", x.to_canonical_u128()), + hash(x, state): ::std::hash::Hash::hash(&x.to_canonical_u128(), state), + } +); + +impl RingCore for Fp32

{} + +impl_prime_native_capability!(Fp32, 4); + impl serde::Serialize for Fp32

{ fn serialize(&self, serializer: S) -> Result { let buf = (self.to_canonical_u128() as u32).to_le_bytes(); diff --git a/crates/jolt-field/src/prime/fp64.rs b/crates/jolt-field/src/prime/fp64.rs index 33d97b41c0..f31796458b 100644 --- a/crates/jolt-field/src/prime/fp64.rs +++ b/crates/jolt-field/src/prime/fp64.rs @@ -4,9 +4,9 @@ //! Uses Solinas-style two-fold reduction. For `c = 2^a ± 1` the fold //! multiply is replaced by shift+add/sub, saving a u128 widening multiply. -use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; - -use crate::{FieldCore, FromPrimitiveInt}; +use crate::native_algebra::{impl_native_ring_algebra, impl_prime_ops}; +use crate::prime::native_capability::impl_prime_native_capability; +use crate::{FieldCore, FromPrimitiveInt, RingCore}; use rand_core::RngCore; use crate::{CanonicalField, HalvingField, PseudoMersenneField}; @@ -369,82 +369,7 @@ impl Fp64

{ } } -impl Add for Fp64

{ - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self::Output { - Self(Self::add_raw(self.0, rhs.0)) - } -} - -impl Sub for Fp64

{ - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self::Output { - Self(Self::sub_raw(self.0, rhs.0)) - } -} - -impl Mul for Fp64

{ - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self::Output { - Self(Self::mul_raw(self.0, rhs.0)) - } -} - -impl Neg for Fp64

{ - type Output = Self; - #[inline] - fn neg(self) -> Self::Output { - Self(Self::sub_raw(0, self.0)) - } -} - -impl AddAssign for Fp64

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for Fp64

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for Fp64

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl<'a, const P: u64> Add<&'a Self> for Fp64

{ - type Output = Self; - #[inline] - fn add(self, rhs: &'a Self) -> Self::Output { - self + *rhs - } -} - -impl<'a, const P: u64> Sub<&'a Self> for Fp64

{ - type Output = Self; - #[inline] - fn sub(self, rhs: &'a Self) -> Self::Output { - self - *rhs - } -} - -impl<'a, const P: u64> Mul<&'a Self> for Fp64

{ - type Output = Self; - #[inline] - fn mul(self, rhs: &'a Self) -> Self::Output { - self * *rhs - } -} +impl_prime_ops!(Fp64, zero_raw: 0); impl FieldCore for Fp64

{ #[inline(always)] @@ -534,6 +459,20 @@ impl PseudoMersenneField for Fp64

{ const MODULUS_OFFSET: u128 = Self::C as u128; } +impl_native_ring_algebra!( + impl[const P: u64] Fp64

{ + zero: Self::default(), + is_zero(x): x.to_canonical_u128() == 0, + one: if P > 1 { Self::from_canonical_u64(1) } else { Self::default() }, + display(x, f): write!(f, "{}", x.to_canonical_u128()), + hash(x, state): ::std::hash::Hash::hash(&x.to_canonical_u128(), state), + } +); + +impl RingCore for Fp64

{} + +impl_prime_native_capability!(Fp64, 8); + impl serde::Serialize for Fp64

{ fn serialize(&self, serializer: S) -> Result { let buf = (self.to_canonical_u128() as u64).to_le_bytes(); diff --git a/crates/jolt-field/src/prime/mod.rs b/crates/jolt-field/src/prime/mod.rs index 6c948e3ceb..73a7b8e60c 100644 --- a/crates/jolt-field/src/prime/mod.rs +++ b/crates/jolt-field/src/prime/mod.rs @@ -14,8 +14,7 @@ pub(crate) mod fp128; pub(crate) mod fp32; pub(crate) mod fp64; -mod native_algebra; -mod native_capability; +pub(crate) mod native_capability; pub(crate) mod pseudo_mersenne; pub(crate) mod util; diff --git a/crates/jolt-field/src/prime/native_algebra.rs b/crates/jolt-field/src/prime/native_algebra.rs deleted file mode 100644 index 08ba365996..0000000000 --- a/crates/jolt-field/src/prime/native_algebra.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! Native `num_traits`/`std` supertrait impls and core-algebra markers for the -//! concrete prime fields (`Fp32`/`Fp64`/`Fp128`). -//! -//! These are the Jolt-free supertrait obligations of the native -//! [`AdditiveGroup`]/[`RingCore`]/[`FieldCore`] hierarchy: -//! `Zero`/`One`/`Display`/`Hash`/`Sum`/`Product` plus the empty algebra markers. -//! The non-trivial `FieldCore::inverse`/`FieldCore::random` impls stay -//! co-located with each prime type. - -use std::fmt; -use std::hash::{Hash, Hasher}; -use std::iter::{Product, Sum}; - -use num_traits::{One, Zero}; - -use super::{Fp128, Fp32, Fp64}; -use crate::{AdditiveGroup, CanonicalField, RingCore}; - -macro_rules! impl_prime_native_algebra { - ($ty:ident<$p:ident: $p_ty:ty>, $canon:ident) => { - impl Zero for $ty<$p> { - #[inline] - fn zero() -> Self { - Self::default() - } - - #[inline] - fn is_zero(&self) -> bool { - self.to_canonical_u128() == 0 - } - } - - impl One for $ty<$p> { - #[inline] - fn one() -> Self { - if $p > 1 { - Self::$canon(1) - } else { - Self::zero() - } - } - } - - impl fmt::Display for $ty<$p> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_canonical_u128()) - } - } - - impl Hash for $ty<$p> { - fn hash(&self, state: &mut H) { - self.to_canonical_u128().hash(state); - } - } - - impl Sum for $ty<$p> { - fn sum>(iter: I) -> Self { - iter.fold(Self::zero(), |acc, x| acc + x) - } - } - - impl<'a, const $p: $p_ty> Sum<&'a Self> for $ty<$p> { - fn sum>(iter: I) -> Self { - iter.fold(Self::zero(), |acc, x| acc + *x) - } - } - - impl Product for $ty<$p> { - fn product>(iter: I) -> Self { - iter.fold(Self::one(), |acc, x| acc * x) - } - } - - impl<'a, const $p: $p_ty> Product<&'a Self> for $ty<$p> { - fn product>(iter: I) -> Self { - iter.fold(Self::one(), |acc, x| acc * *x) - } - } - - impl AdditiveGroup for $ty<$p> {} - impl RingCore for $ty<$p> {} - }; -} - -impl_prime_native_algebra!(Fp32, from_canonical_u32); -impl_prime_native_algebra!(Fp64, from_canonical_u64); -impl_prime_native_algebra!(Fp128, from_canonical_u128); diff --git a/crates/jolt-field/src/prime/native_capability.rs b/crates/jolt-field/src/prime/native_capability.rs index 7fad6a2b08..b2489dea1d 100644 --- a/crates/jolt-field/src/prime/native_capability.rs +++ b/crates/jolt-field/src/prime/native_capability.rs @@ -6,36 +6,33 @@ //! modules; this module owns the shared derived-capability implementations used //! directly by both Jolt and Akita. -use std::mem::size_of; - -use super::{Fp128, Fp32, Fp64}; -use crate::{ - CanonicalField, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, NaiveAccumulator, - WithAccumulator, -}; +use crate::{FieldCore, FromPrimitiveInt}; macro_rules! impl_prime_native_capability { ($ty:ident<$p:ident: $p_ty:ty>, $bytes:expr) => { - impl CanonicalRepr for $ty<$p> { + impl $crate::CanonicalRepr for $ty<$p> { const NUM_BYTES: usize = $bytes; #[inline(always)] fn to_bytes_le(&self, out: &mut [u8]) { - assert_eq!(out.len(), ::NUM_BYTES); + assert_eq!(out.len(), ::NUM_BYTES); out.copy_from_slice( - &self.to_canonical_u128().to_le_bytes()[..::NUM_BYTES], + &self.to_canonical_u128().to_le_bytes() + [..::NUM_BYTES], ); } #[inline(always)] fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { - if bytes.len() <= size_of::() { - let mut padded = [0u8; size_of::()]; + if bytes.len() <= ::std::mem::size_of::() { + let mut padded = [0u8; ::std::mem::size_of::()]; padded[..bytes.len()].copy_from_slice(bytes); - return ::from_u128(u128::from_le_bytes(padded)); + return ::from_u128(u128::from_le_bytes( + padded, + )); } - reduce_le_bytes_mod_order(bytes) + $crate::prime::native_capability::reduce_le_bytes_mod_order(bytes) } #[inline] @@ -50,27 +47,25 @@ macro_rules! impl_prime_native_capability { } } - impl WithAccumulator for $ty<$p> { - type Accumulator = NaiveAccumulator; + impl $crate::WithAccumulator for $ty<$p> { + type Accumulator = $crate::NaiveAccumulator; } - impl Field for $ty<$p> {} + impl $crate::Field for $ty<$p> {} }; } /// Horner reduction of arbitrary-length little-endian bytes modulo the field /// order (the >16-byte path of `CanonicalRepr::from_le_bytes_mod_order`). #[inline(always)] -fn reduce_le_bytes_mod_order(bytes: &[u8]) -> F { +pub(crate) fn reduce_le_bytes_mod_order(bytes: &[u8]) -> F { let base = F::from_u64(256); bytes.iter().rev().fold(F::zero(), |acc, &byte| { acc * base + F::from_u64(byte as u64) }) } -impl_prime_native_capability!(Fp32, 4); -impl_prime_native_capability!(Fp64, 8); -impl_prime_native_capability!(Fp128, 16); +pub(crate) use impl_prime_native_capability; #[cfg(test)] mod tests { @@ -79,8 +74,9 @@ mod tests { //! These exercise the Solinas backend directly, so they run under //! `--no-default-features --features solinas` as well as combined builds. use super::*; - use crate::Accumulator; - use crate::Prime128Offset275; + use crate::{ + Accumulator, CanonicalField, CanonicalRepr, Fp32, Fp64, Prime128Offset275, WithAccumulator, + }; /// Asserts the full canonical byte round-trip on the native traits. fn assert_native_byte_roundtrip(value: F, expected: [u8; N]) diff --git a/crates/jolt-field/src/unreduced/accum.rs b/crates/jolt-field/src/unreduced/accum.rs index 5d86f9e941..5ba68ff90e 100644 --- a/crates/jolt-field/src/unreduced/accum.rs +++ b/crates/jolt-field/src/unreduced/accum.rs @@ -541,3 +541,16 @@ impl Neg for AccumPair { Self(-self.0, -self.1) } } + +use crate::native_algebra::impl_native_additive; + +impl_native_additive!(impl[] Fp32ProductAccum { zero: Fp32ProductAccum([0; 2]), is_zero(x): *x == Self::zero() }); +impl_native_additive!(impl[] Fp64ProductAccum { zero: Fp64ProductAccum([0; 2]), is_zero(x): *x == Self::zero() }); +impl_native_additive!(impl[] Fp128MulU64Accum { zero: Fp128MulU64Accum([0; 3]), is_zero(x): *x == Self::zero() }); +impl_native_additive!(impl[] Fp128ProductAccum { zero: Fp128ProductAccum([0; 4]), is_zero(x): *x == Self::zero() }); +impl_native_additive!(impl[] FpExt4Fp32ProductAccum { zero: FpExt4Fp32ProductAccum([0; 4]), is_zero(x): *x == Self::zero() }); +impl_native_additive!(impl[] FpExt2Fp64ProductAccum { zero: FpExt2Fp64ProductAccum([0; 4]), is_zero(x): *x == Self::zero() }); +impl_native_additive!(impl[A: crate::AdditiveGroup] AccumPair { + zero: Self(A::zero(), A::zero()), + is_zero(x): ::num_traits::Zero::is_zero(&x.0) && ::num_traits::Zero::is_zero(&x.1), +}); diff --git a/crates/jolt-field/src/unreduced/mod.rs b/crates/jolt-field/src/unreduced/mod.rs index 649258c816..59b8ee5a15 100644 --- a/crates/jolt-field/src/unreduced/mod.rs +++ b/crates/jolt-field/src/unreduced/mod.rs @@ -24,7 +24,6 @@ use crate::{AdditiveGroup, CanonicalField, FieldCore}; use super::prime::{Fp128, Fp32, Fp64}; mod accum; -mod native_algebra; pub use accum::*; /// Wide unreduced accumulator for `Fp32`: 2 × i32 limbs (16-bit data each). @@ -562,6 +561,9 @@ impl Neg for Fp128x8i32 { pub trait ReduceTo { /// Carry-propagate and reduce to a canonical field element. fn reduce(self) -> F; + + /// Scale each element by `small`. + fn scale_i32(self, small: i32) -> Self; } impl ReduceTo> for Fp32x2i32 { @@ -569,6 +571,11 @@ impl ReduceTo> for Fp32x2i32 { fn reduce(self) -> Fp32

{ Fp32x2i32::reduce::

(self) } + + #[inline] + fn scale_i32(self, small: i32) -> Self { + self.scale_i32(small) + } } impl ReduceTo> for Fp64x4i32 { @@ -576,6 +583,11 @@ impl ReduceTo> for Fp64x4i32 { fn reduce(self) -> Fp64

{ Fp64x4i32::reduce::

(self) } + + #[inline] + fn scale_i32(self, small: i32) -> Self { + self.scale_i32(small) + } } impl ReduceTo> for Fp128x8i32 { @@ -583,6 +595,11 @@ impl ReduceTo> for Fp128x8i32 { fn reduce(self) -> Fp128

{ Fp128x8i32::reduce::

(self) } + + #[inline] + fn scale_i32(self, small: i32) -> Self { + self.scale_i32(small) + } } /// Precomputed fold context for `FpExt4>`. @@ -753,36 +770,10 @@ impl HasUnreducedOps for Fp128

{ } /// Element-wise scaling of a wide accumulator by a small signed integer. -pub trait ScaleI32 { - /// Scale each element by `small`. - fn scale_i32(self, small: i32) -> Self; -} - -impl ScaleI32 for Fp32x2i32 { - #[inline] - fn scale_i32(self, small: i32) -> Self { - self.scale_i32(small) - } -} - -impl ScaleI32 for Fp64x4i32 { - #[inline] - fn scale_i32(self, small: i32) -> Self { - self.scale_i32(small) - } -} - -impl ScaleI32 for Fp128x8i32 { - #[inline] - fn scale_i32(self, small: i32) -> Self { - self.scale_i32(small) - } -} - /// Associates a field type with its wide unreduced accumulator. pub trait HasWide: FieldCore { /// The wide accumulator type. - type Wide: AdditiveGroup + From + ReduceTo + ScaleI32; + type Wide: AdditiveGroup + From + ReduceTo; /// Convert `self` to wide form and scale every limb by `small`. /// @@ -808,3 +799,9 @@ impl HasWide for Fp128

{ #[cfg(test)] mod tests; + +use crate::native_algebra::impl_native_additive; + +impl_native_additive!(impl[] Fp32x2i32 { zero: Fp32x2i32([0; 2]), is_zero(x): *x == Self::zero() }); +impl_native_additive!(impl[] Fp64x4i32 { zero: Fp64x4i32([0; 4]), is_zero(x): *x == Self::zero() }); +impl_native_additive!(impl[] Fp128x8i32 { zero: Fp128x8i32([0; 8]), is_zero(x): *x == Self::zero() }); diff --git a/crates/jolt-field/src/unreduced/native_algebra.rs b/crates/jolt-field/src/unreduced/native_algebra.rs deleted file mode 100644 index 616c4988f9..0000000000 --- a/crates/jolt-field/src/unreduced/native_algebra.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Native `num_traits`/`std` supertrait impls for the wide unreduced -//! accumulator types and the generic [`AccumPair`]. -//! -//! These are the Jolt-free supertrait obligations of the native -//! [`AdditiveGroup`] hierarchy: `Zero` plus the `Add`/`Sub` by-reference -//! forwarders that `AdditiveGroup` requires. - -use std::ops::{Add, Sub}; - -use num_traits::Zero; - -use super::{ - AccumPair, Fp128MulU64Accum, Fp128ProductAccum, Fp128x8i32, Fp32ProductAccum, Fp32x2i32, - Fp64ProductAccum, Fp64x4i32, FpExt2Fp64ProductAccum, FpExt4Fp32ProductAccum, -}; -use crate::AdditiveGroup; - -macro_rules! impl_wide_native_additive { - ($ty:ty, $zero:expr) => { - impl Zero for $ty { - #[inline] - fn zero() -> Self { - $zero - } - - #[inline] - fn is_zero(&self) -> bool { - *self == Self::zero() - } - } - - impl<'a> Add<&'a Self> for $ty { - type Output = Self; - - #[inline] - fn add(self, rhs: &'a Self) -> Self::Output { - self + *rhs - } - } - - impl<'a> Sub<&'a Self> for $ty { - type Output = Self; - - #[inline] - fn sub(self, rhs: &'a Self) -> Self::Output { - self - *rhs - } - } - - impl AdditiveGroup for $ty {} - }; -} - -impl_wide_native_additive!(Fp32x2i32, Fp32x2i32([0; 2])); -impl_wide_native_additive!(Fp64x4i32, Fp64x4i32([0; 4])); -impl_wide_native_additive!(Fp128x8i32, Fp128x8i32([0; 8])); -impl_wide_native_additive!(Fp32ProductAccum, Fp32ProductAccum([0; 2])); -impl_wide_native_additive!(Fp64ProductAccum, Fp64ProductAccum([0; 2])); -impl_wide_native_additive!(Fp128MulU64Accum, Fp128MulU64Accum([0; 3])); -impl_wide_native_additive!(Fp128ProductAccum, Fp128ProductAccum([0; 4])); -impl_wide_native_additive!(FpExt4Fp32ProductAccum, FpExt4Fp32ProductAccum([0; 4])); -impl_wide_native_additive!(FpExt2Fp64ProductAccum, FpExt2Fp64ProductAccum([0; 4])); - -impl Zero for AccumPair { - #[inline] - fn zero() -> Self { - Self(A::zero(), A::zero()) - } - - #[inline] - fn is_zero(&self) -> bool { - self.0.is_zero() && self.1.is_zero() - } -} - -impl<'a, A: AdditiveGroup> Add<&'a Self> for AccumPair { - type Output = Self; - - #[inline] - fn add(self, rhs: &'a Self) -> Self::Output { - self + *rhs - } -} - -impl<'a, A: AdditiveGroup> Sub<&'a Self> for AccumPair { - type Output = Self; - - #[inline] - fn sub(self, rhs: &'a Self) -> Self::Output { - self - *rhs - } -} - -impl AdditiveGroup for AccumPair {} diff --git a/crates/jolt-field/tests/coverage.rs b/crates/jolt-field/tests/coverage.rs index 89c34ec653..16346fc393 100644 --- a/crates/jolt-field/tests/coverage.rs +++ b/crates/jolt-field/tests/coverage.rs @@ -1,7 +1,7 @@ #![cfg(feature = "bn254")] //! Targeted tests to improve code coverage across the jolt-field crate. //! -//! Covers: NaiveAccumulator, WideAccumulator, OptimizedMul blanket impl, +//! Covers: NaiveAccumulator, WideAccumulator, //! Field default methods, SignedBigInt uncovered paths, //! SignedBigIntHi32 uncovered paths, and macro-generated operator variants. @@ -9,7 +9,6 @@ use ark_std::test_rng; use jolt_field::signed::*; use jolt_field::{ Accumulator, CanonicalRepr, FieldCore, Fr, FromPrimitiveInt, Limbs, NaiveAccumulator, - OptimizedMul, }; use num_traits::{One, Zero}; @@ -109,42 +108,6 @@ fn wide_accumulator_many_fmadds() { assert_eq!(acc.reduce(), expected); } -#[test] -fn optimized_mul_blanket_impl() { - let mut rng = test_rng(); - let a: Fr = ::random(&mut rng); - let b: Fr = ::random(&mut rng); - - // mul_0_optimized: both nonzero - assert_eq!(a.mul_0_optimized(b), a * b); - - // mul_0_optimized: first is zero - assert!(Fr::zero().mul_0_optimized(b).is_zero()); - - // mul_0_optimized: second is zero - assert!(a.mul_0_optimized(Fr::zero()).is_zero()); - - // mul_1_optimized: first is one - assert_eq!(Fr::one().mul_1_optimized(b), b); - - // mul_1_optimized: second is one - assert_eq!(a.mul_1_optimized(Fr::one()), a); - - // mul_1_optimized: neither is one - assert_eq!(a.mul_1_optimized(b), a * b); - - // mul_01_optimized: zero path - assert!(Fr::zero().mul_01_optimized(b).is_zero()); - assert!(a.mul_01_optimized(Fr::zero()).is_zero()); - - // mul_01_optimized: one path - assert_eq!(Fr::one().mul_01_optimized(b), b); - assert_eq!(a.mul_01_optimized(Fr::one()), a); - - // mul_01_optimized: general path - assert_eq!(a.mul_01_optimized(b), a * b); -} - #[test] fn field_from_bool_edge() { assert_eq!(::from_bool(true), Fr::one()); From 65f70be72b8fa90e6509da3d05119ca9b0c662c0 Mon Sep 17 00:00:00 2001 From: acentelles Date: Wed, 22 Jul 2026 01:35:54 -0400 Subject: [PATCH 10/38] docs(specs): tick verified acceptance criteria in consolidate-field-traits --- specs/consolidate-field-traits.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/specs/consolidate-field-traits.md b/specs/consolidate-field-traits.md index adbe774cae..29f328735c 100644 --- a/specs/consolidate-field-traits.md +++ b/specs/consolidate-field-traits.md @@ -53,16 +53,16 @@ This is a refactor of trait boundaries, not of arithmetic. The `jolt-eval` invar ### Acceptance Criteria -- [ ] `grep -rc '^pub trait' crates/jolt-field/src` totals at most 22. -- [ ] The crate root has at most 4 trait-defining modules (`algebra.rs`, `canonical.rs`, `accumulator.rs`, `field.rs`) plus the feature-gated backend modules; the 15 micro-files are gone. -- [ ] Zero references remain to: `SignedScalarAccumulator`, `WithSmallScalarAccumulator`, `SignedProductAccumulator`, `WithSignedProductAccumulator`, `ExtensionCoeff`, `BalancedDigitLookup` (trait), `ScaleI32`, `PackedValue`, `LiftBase`, `MulBase`, `FrobeniusExtField`, `FpExt4MulBackend`, `FpExt8MulBackend`, `AdditiveAccumulator`, `Invertible`, `RandomSampling`, `MulPow2`, `MulPrimitiveInt`, `CanonicalBytes`, `ReducingBytes`, `FixedByteSize`, `FixedBytes`, `CanonicalU64`, `CanonicalBitLength`, `TranscriptChallenge`, `SmoothFftField`. -- [ ] `fft.rs` is removed from `jolt-field` (it has zero consumers in this workspace). -- [ ] The three `native_algebra.rs` files are deleted; one shared macro provides the supertrait glue, invoked from each concrete type's own file. -- [ ] `Fp32`, `Fp64`, `Fp128`, `FpExt2`, `FpExt4`, `FpExt8` implement `serde::Serialize`/`Deserialize` with canonical encoding; a bincode round-trip test covers each. -- [ ] The degree-4 extension mul/square schedule exists in exactly one place (shared between scalar and packed backends, as degree-8 already is). -- [ ] `muldiv` e2e passes in both modes; standard-mode proof bytes match main (size and content). -- [ ] Serialized size tests: each Solinas field element bincode-encodes to exactly `NUM_BYTES` bytes; a `Vec` of $n$ elements encodes to $n \cdot \texttt{NUM\_BYTES}$ plus a single length prefix. -- [ ] `mersenne61_compat` passes with updated bounds and still no arkworks dependency. +- [x] `grep -rc '^pub trait' crates/jolt-field/src` totals at most 22. (22 as of phase 5; 21 once `MontgomeryConstants` is resolved.) +- [x] The crate root has at most 4 trait-defining modules (`algebra.rs`, `canonical.rs`, `accumulator.rs`, `field.rs`) plus the feature-gated backend modules; the 15 micro-files are gone. +- [x] Zero references remain to: `SignedScalarAccumulator`, `WithSmallScalarAccumulator`, `SignedProductAccumulator`, `WithSignedProductAccumulator`, `ExtensionCoeff`, `BalancedDigitLookup` (trait), `ScaleI32`, `PackedValue`, `LiftBase`, `MulBase`, `FrobeniusExtField`, `FpExt4MulBackend`, `FpExt8MulBackend`, `AdditiveAccumulator`, `Invertible`, `RandomSampling`, `MulPow2`, `MulPrimitiveInt`, `CanonicalBytes`, `ReducingBytes`, `FixedByteSize`, `FixedBytes`, `CanonicalU64`, `CanonicalBitLength`, `TranscriptChallenge`, `SmoothFftField`. +- [x] `fft.rs` is removed from `jolt-field` (it has zero consumers in this workspace). +- [x] The three `native_algebra.rs` files are deleted; one shared macro provides the supertrait glue, invoked from each concrete type's own file. +- [x] `Fp32`, `Fp64`, `Fp128`, `FpExt2`, `FpExt4`, `FpExt8` implement `serde::Serialize`/`Deserialize` with canonical encoding; a bincode round-trip test covers each. +- [x] The degree-4 extension mul/square schedule exists in exactly one place (shared between scalar and packed backends, as degree-8 already is). +- [x] `muldiv` e2e passes in both modes. (Byte-for-byte proof comparison against a main-built proof not yet run; the Fiat-Shamir and `Fr` serialization paths were moved verbatim.) +- [x] Serialized size tests: each Solinas field element bincode-encodes to exactly `NUM_BYTES` bytes; a `Vec` of $n$ elements encodes to $n \cdot \texttt{NUM\_BYTES}$ plus a single length prefix. +- [x] `mersenne61_compat` passes with updated bounds and still no arkworks dependency. ### Testing Strategy From 4f2f9ddca32f540e936c2bdcab494d795cd0de8a Mon Sep 17 00:00:00 2001 From: acentelles Date: Wed, 22 Jul 2026 12:49:42 -0400 Subject: [PATCH 11/38] chore(field): remove committed fuzz build artifacts The fuzz crate's target/ directory (630 generated files) was accidentally committed in d8b28309f. Remove it from the index and ignore it going forward. Co-Authored-By: Claude Fable 5 --- crates/jolt-field/fuzz/.gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 crates/jolt-field/fuzz/.gitignore diff --git a/crates/jolt-field/fuzz/.gitignore b/crates/jolt-field/fuzz/.gitignore new file mode 100644 index 0000000000..fe68c971b7 --- /dev/null +++ b/crates/jolt-field/fuzz/.gitignore @@ -0,0 +1,4 @@ +target/ +corpus/ +artifacts/ +coverage/ From 9aff1cdd20d29bf6ffaec420942e14cdaaf26330 Mon Sep 17 00:00:00 2001 From: acentelles Date: Wed, 22 Jul 2026 15:55:52 -0400 Subject: [PATCH 12/38] fix(field): audit remediation for the trait consolidation - repair the from_bytes fuzz target's duplicate import (the fuzz crate is its own workspace, so workspace clippy never compiles it) - dedupe mechanical-rename residue bounds in the solinas benches and jolt-sumcheck's SumcheckScalar - move the Solinas prime traits (CanonicalField, HalvingField, PseudoMersenneField, balanced_digit_lut) from the crate root into prime/traits.rs per the spec's layout - amend the spec: OptimizedMul was deleted as a duplicate of jolt-prover-legacy's own trait rather than kept; record the actual Solinas-trait location and the montgomery_constants.rs remainder --- .../benches/solinas_field_arith/arithmetic.rs | 4 ++-- .../benches/solinas_field_arith/kernel.rs | 2 +- crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs | 2 +- crates/jolt-field/src/lib.rs | 13 +++++-------- crates/jolt-field/src/packed/avx2/mod.rs | 2 +- crates/jolt-field/src/packed/avx512/mod.rs | 2 +- crates/jolt-field/src/prime/mod.rs | 3 +++ .../src/{solinas_traits.rs => prime/traits.rs} | 0 crates/jolt-sumcheck/src/scalar.rs | 3 --- specs/consolidate-field-traits.md | 6 +++--- 10 files changed, 17 insertions(+), 20 deletions(-) rename crates/jolt-field/src/{solinas_traits.rs => prime/traits.rs} (100%) diff --git a/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs b/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs index 18f1c41d2d..46f6af6591 100644 --- a/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs +++ b/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs @@ -3,7 +3,7 @@ use std::time::Instant; use criterion::{black_box, Criterion, Throughput}; use jolt_field::packed::PackedField; -use jolt_field::{FieldCore, RingCore}; +use jolt_field::FieldCore; use rand::{rngs::StdRng, SeedableRng}; use super::data::duration_per_logical_op; @@ -16,7 +16,7 @@ pub(crate) fn bench_arithmetic_case( seed: u64, params: ArithmeticBenchParams, ) where - F: FieldCore + FieldCore + RingCore + FieldCore + AddAssign + SubAssign + MulAssign + 'static, + F: FieldCore + AddAssign + SubAssign + MulAssign + 'static, PF: PackedField + Copy + 'static, { let mut rng = StdRng::seed_from_u64(seed); diff --git a/crates/jolt-field/benches/solinas_field_arith/kernel.rs b/crates/jolt-field/benches/solinas_field_arith/kernel.rs index 16cf23caef..e5e52f896f 100644 --- a/crates/jolt-field/benches/solinas_field_arith/kernel.rs +++ b/crates/jolt-field/benches/solinas_field_arith/kernel.rs @@ -36,7 +36,7 @@ fn sumcheck_bench( rng: &mut StdRng, n: u64, ) where - F: FieldCore + FieldCore + 'static, + F: FieldCore + 'static, PF: PackedField + Copy + 'static, { let eq: Vec = (0..n).map(|_| F::random(rng)).collect(); diff --git a/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs b/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs index 43803908ad..1d77da3b72 100644 --- a/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs +++ b/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs @@ -1,5 +1,5 @@ #![no_main] -use jolt_field::{CanonicalRepr, Fr, CanonicalRepr}; +use jolt_field::{CanonicalRepr, Fr}; use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { diff --git a/crates/jolt-field/src/lib.rs b/crates/jolt-field/src/lib.rs index e37071c968..a3f3df1e6c 100644 --- a/crates/jolt-field/src/lib.rs +++ b/crates/jolt-field/src/lib.rs @@ -50,8 +50,6 @@ mod field_error; mod montgomery_constants; #[cfg(feature = "solinas")] mod native_algebra; -#[cfg(feature = "solinas")] -mod solinas_traits; pub use accumulator::{Accumulator, NaiveAccumulator, WithAccumulator}; pub use algebra::{AdditiveGroup, FieldCore, FromPrimitiveInt, RingCore}; @@ -60,8 +58,6 @@ pub use field::Field; pub use field_error::FieldError; pub use montgomery_constants::MontgomeryConstants; pub use num_traits::{One, Zero}; -#[cfg(feature = "solinas")] -pub use solinas_traits::{balanced_digit_lut, CanonicalField, HalvingField, PseudoMersenneField}; pub mod limbs; pub use limbs::Limbs; @@ -88,10 +84,11 @@ pub use ext::lift::{ pub use ext::{Ext2, ExtMulBackend, FpExt2, FpExt2Config, FpExt4, FpExt8, NegOneNr, TwoNr}; #[cfg(feature = "solinas")] pub use prime::{ - is_registered_prime_offset, pseudo_mersenne_modulus, registered_prime_offset_spec, Fp128, Fp32, - Fp64, Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, - Prime24Offset3, Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, - Prime48Offset59, Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, + balanced_digit_lut, is_registered_prime_offset, pseudo_mersenne_modulus, + registered_prime_offset_spec, CanonicalField, Fp128, Fp32, Fp64, HalvingField, + Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, Prime24Offset3, + Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime48Offset59, + Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, PseudoMersenneField, PRIME_OFFSET_IMPLEMENTED_MAX_BITS, PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, }; diff --git a/crates/jolt-field/src/packed/avx2/mod.rs b/crates/jolt-field/src/packed/avx2/mod.rs index ce89f4ac8d..2e1e427aed 100644 --- a/crates/jolt-field/src/packed/avx2/mod.rs +++ b/crates/jolt-field/src/packed/avx2/mod.rs @@ -7,7 +7,7 @@ reason = "ported AVX2 kernels retain their audited intrinsic-level invariants" )] -use super::{PackedField}; +use super::PackedField; use crate::ext::FpExt2Config; use crate::FieldCore; use crate::{Fp128, Fp32, Fp64}; diff --git a/crates/jolt-field/src/packed/avx512/mod.rs b/crates/jolt-field/src/packed/avx512/mod.rs index 801560074c..67bc5d74a9 100644 --- a/crates/jolt-field/src/packed/avx512/mod.rs +++ b/crates/jolt-field/src/packed/avx512/mod.rs @@ -8,7 +8,7 @@ reason = "ported AVX-512 kernels retain their audited intrinsic-level invariants" )] -use super::{PackedField}; +use super::PackedField; use crate::ext::FpExt2Config; use crate::FieldCore; use crate::{Fp128, Fp32, Fp64}; diff --git a/crates/jolt-field/src/prime/mod.rs b/crates/jolt-field/src/prime/mod.rs index 73a7b8e60c..e4d57bf643 100644 --- a/crates/jolt-field/src/prime/mod.rs +++ b/crates/jolt-field/src/prime/mod.rs @@ -18,6 +18,9 @@ pub(crate) mod native_capability; pub(crate) mod pseudo_mersenne; pub(crate) mod util; +mod traits; +pub use traits::{balanced_digit_lut, CanonicalField, HalvingField, PseudoMersenneField}; + pub use fp128::{ Fp128, Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, }; diff --git a/crates/jolt-field/src/solinas_traits.rs b/crates/jolt-field/src/prime/traits.rs similarity index 100% rename from crates/jolt-field/src/solinas_traits.rs rename to crates/jolt-field/src/prime/traits.rs diff --git a/crates/jolt-sumcheck/src/scalar.rs b/crates/jolt-sumcheck/src/scalar.rs index 0a2848f38c..fb97127f94 100644 --- a/crates/jolt-sumcheck/src/scalar.rs +++ b/crates/jolt-sumcheck/src/scalar.rs @@ -9,9 +9,6 @@ use jolt_field::{CanonicalRepr, FieldCore, FromPrimitiveInt}; pub trait SumcheckScalar: FieldCore + FromPrimitiveInt - + FromPrimitiveInt - + CanonicalRepr - + CanonicalRepr + CanonicalRepr + Copy + Default diff --git a/specs/consolidate-field-traits.md b/specs/consolidate-field-traits.md index 29f328735c..a8e20504fd 100644 --- a/specs/consolidate-field-traits.md +++ b/specs/consolidate-field-traits.md @@ -113,16 +113,16 @@ Trait disposition, all 46 accounted for: | `ReduceTo` | `ScaleI32` | same three wide-limb implementors | | `PackedField` | `PackedValue` | identical 13 implementors; nothing bounds on `PackedValue` alone | -**Kept as-is (14):** `AdditiveGroup`, `RingCore`, `Field` (umbrella), `WithAccumulator` (the additive-layer accumulator association that lets rings use `NaiveAccumulator` without field capabilities, per #1484), `OptimizedMul` (consumed by jolt-prover-legacy; candidate to relocate there in a later sweep), `CanonicalField`, `HalvingField` (implemented by extension fields, so it cannot fold into `CanonicalField`), `PseudoMersenneField`, `MulBaseUnreduced`, `FpExt2Config`, `HasUnreducedOps`, `HasOptimizedFold`, `HasWide`, `HasPacking`. +**Kept as-is (14):** `AdditiveGroup`, `RingCore`, `Field` (umbrella), `WithAccumulator` (the additive-layer accumulator association that lets rings use `NaiveAccumulator` without field capabilities, per #1484), `CanonicalField`, `HalvingField` (implemented by extension fields, so it cannot fold into `CanonicalField`), `PseudoMersenneField`, `MulBaseUnreduced`, `FpExt2Config`, `HasUnreducedOps`, `HasOptimizedFold`, `HasWide`, `HasPacking`. -Final count: 7 survivors + 14 kept = at most 22 public traits (21 if `MontgomeryConstants`' deletion is confirmed and `OptimizedMul` relocates). +Final count: 22 public traits (21 if `MontgomeryConstants`' deletion is confirmed). Amendment during implementation: `OptimizedMul` was deleted outright rather than kept; jolt-prover-legacy defines its own identical trait and never consumed jolt-field's copy. **Serialization split.** Two distinct concerns, two mechanisms: - Proof/wire format: serde + bincode. Solinas types serialize their canonical form (never an internal representation), exactly as `arkworks/bn254.rs` already does for `Fr`. Extension fields serialize as arrays of base-field elements. Nothing replicates akita-serialization's `FpExt2Config`-bound custom encode/decode. - Fiat-Shamir: `CanonicalRepr`'s explicit little-endian canonical encoding. A transcript needs a specified encoding, not whatever an encoder version emits, so this deliberately does not route through bincode. -**File layout.** Crate root: `algebra.rs` (`AdditiveGroup`, `RingCore`, `FieldCore`, `FromPrimitiveInt`, `OptimizedMul`), `canonical.rs` (`CanonicalRepr`), `accumulator.rs` (`Accumulator`, `WithAccumulator`, `NaiveAccumulator`), `field.rs` (`Field` umbrella). Solinas traits (`CanonicalField`, `HalvingField`, `PseudoMersenneField`) move into `prime/mod.rs`. Each concrete type's file shows its full trait surface bn254.rs-style; shared macros are limited to: +**File layout.** Crate root: `algebra.rs` (`AdditiveGroup`, `RingCore`, `FieldCore`, `FromPrimitiveInt`, `OptimizedMul`), `canonical.rs` (`CanonicalRepr`), `accumulator.rs` (`Accumulator`, `WithAccumulator`, `NaiveAccumulator`), `field.rs` (`Field` umbrella). Solinas traits (`CanonicalField`, `HalvingField`, `PseudoMersenneField`, and the `balanced_digit_lut` helper) move into `prime/traits.rs`, re-exported through `prime`. `montgomery_constants.rs` remains a separate root file until the `MontgomeryConstants` open question resolves. Each concrete type's file shows its full trait surface bn254.rs-style; shared macros are limited to: 1. one `impl_native_algebra!` macro (replacing the three `native_algebra.rs` files and the 230 hand-written lines in `ext/native_algebra.rs`), invoked inside each type's file; 2. one operator-matrix macro in the style of `bn254.rs`'s `delegate_binop!` and legacy's `impl_field_ops_inline!`, deduplicating the hand-written `Add`/`Sub`/`Mul`/`Neg`/`*Assign`/by-reference blocks in `fp32.rs`, `fp64.rs`, `fp128/`, and the ext types (reduction bodies stay hand-written per type; `fp64`'s `C_SHIFT` specialization is math, not boilerplate); From fe1d5d41fa78b323f04c82ab5900303eda1a8627 Mon Sep 17 00:00:00 2001 From: acentelles Date: Wed, 22 Jul 2026 16:42:52 -0400 Subject: [PATCH 13/38] docs(specs): record byte-for-byte proof comparison against main Standard-mode muldiv proofs from this branch and main are identical in all 63,371 bytes when both builds share a Dory URS, and the main-built verifier accepts the branch-built proof. cfg(test) builds randomize the URS per process (DoryGlobals::configure_test_cache_root), so the comparison requires pinning a shared URS cache. Co-Authored-By: Claude Fable 5 --- specs/consolidate-field-traits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/consolidate-field-traits.md b/specs/consolidate-field-traits.md index a8e20504fd..aac512478b 100644 --- a/specs/consolidate-field-traits.md +++ b/specs/consolidate-field-traits.md @@ -60,7 +60,7 @@ This is a refactor of trait boundaries, not of arithmetic. The `jolt-eval` invar - [x] The three `native_algebra.rs` files are deleted; one shared macro provides the supertrait glue, invoked from each concrete type's own file. - [x] `Fp32`, `Fp64`, `Fp128`, `FpExt2`, `FpExt4`, `FpExt8` implement `serde::Serialize`/`Deserialize` with canonical encoding; a bincode round-trip test covers each. - [x] The degree-4 extension mul/square schedule exists in exactly one place (shared between scalar and packed backends, as degree-8 already is). -- [x] `muldiv` e2e passes in both modes. (Byte-for-byte proof comparison against a main-built proof not yet run; the Fiat-Shamir and `Fr` serialization paths were moved verbatim.) +- [x] `muldiv` e2e passes in both modes. (Byte-for-byte proof comparison against a main-built proof verified 2026-07-22: standard-mode `muldiv` proofs are identical in all 63,371 bytes, proving is deterministic run-to-run, and the main-built verifier accepts the branch-built proof. Caveat for reproduction: `cfg(test)` builds randomize the Dory URS per process via `DoryGlobals::configure_test_cache_root`, so cross-process comparison requires pinning both builds to a shared URS cache.) - [x] Serialized size tests: each Solinas field element bincode-encodes to exactly `NUM_BYTES` bytes; a `Vec` of $n$ elements encodes to $n \cdot \texttt{NUM\_BYTES}$ plus a single length prefix. - [x] `mersenne61_compat` passes with updated bounds and still no arkworks dependency. From 79944f26c0ebdb103d527e658594897f056af575 Mon Sep 17 00:00:00 2001 From: acentelles Date: Wed, 29 Jul 2026 13:37:13 -0400 Subject: [PATCH 14/38] fix(field): repair Fp64::reduce_u128 truncation and S160 mul overflow Two arithmetic bugs found by the jolt-field-two differential harness (see PR #1684 review), both predating this PR's refactor: - Fp64::reduce_u128 cast the fold's high part to u64. For sub-word primes (40/48/56-bit) inputs >= 2^(64+BITS) reduced incorrectly, which includes essentially all 16-byte Fiat-Shamir challenge derivations. The fold multiply now stays in u128 (bounded by 2^127 since the registry enforces C(C+1) < P). Fp32's fold was already correct. - S160::mul_magnitudes fused two (three) full 64x64 products into one u128 cross-term sum, overflowing for large second limbs: debug panic, release wraparound. Word 1 now uses overflow-tracked accumulation (the true carry into word 2 reaches 2^65 and must include the overflow bit at weight 2^64); word 2 uses wrapping adds since only its low 32 bits survive in the 160-bit result. The N=1 arm was safe (32-bit high limb); the general N>=3 path already accumulated safely. The identical S160 kernel exists in the arkworks fork and needs the same fix there (reported separately). Regression tests: u128-mod oracle over all registered sub-word primes incl. the challenge-bytes path, and a 32-bit-digit schoolbook oracle for S160 with boundary limbs. Both tests fail against the unfixed kernels. --- crates/jolt-field/src/prime/fp64.rs | 65 +++++++++++++- .../src/signed/signed_bigint_hi32.rs | 87 +++++++++++++++++-- 2 files changed, 146 insertions(+), 6 deletions(-) diff --git a/crates/jolt-field/src/prime/fp64.rs b/crates/jolt-field/src/prime/fp64.rs index f31796458b..1c57d5ef1b 100644 --- a/crates/jolt-field/src/prime/fp64.rs +++ b/crates/jolt-field/src/prime/fp64.rs @@ -188,7 +188,11 @@ impl Fp64

{ fn reduce_u128(x: u128) -> u64 { let mut v = x; while v >> Self::BITS != 0 { - v = (v & Self::MASK) + Self::mul_c((v >> Self::BITS) as u64); + // The fold's high part `v >> BITS` can exceed 64 bits for + // sub-word primes (BITS < 64), so the multiply by `C` must stay + // in u128. It cannot overflow: `v >> BITS < 2^(128 - BITS)` and + // `C < 2^(BITS - 1)`, so the product is below `2^127`. + v = (v & Self::MASK) + (v >> Self::BITS) * (Self::C as u128); } let reduced = v.wrapping_sub(P as u128); let borrow = reduced >> 127; @@ -570,6 +574,65 @@ mod tests { assert_eq!(H::C_SHIFT_KIND, 0); } + #[test] + fn reduce_u128_subword_primes() { + // Regression: the fold's high part `v >> BITS` exceeds 64 bits for + // sub-word primes once the input reaches 2^(64 + BITS); the pre-fix + // kernel truncated it with `as u64`. 16-byte challenge inputs are + // essentially always in that domain. + fn check() { + let bits = 64 - P.leading_zeros(); + // For the full-word prime (BITS = 64) the truncation threshold + // 2^(64 + BITS) is out of u128 range; clamp the shift and rely on + // the u128::MAX cases. + let shift = (64 + bits).min(127); + let cases: [u128; 6] = [ + u128::MAX, + 1u128 << shift, + (1u128 << shift) + 12_345, + (1u128 << shift) - 1, + u128::MAX - P as u128, + (P as u128) << 63, + ]; + for x in cases { + let expected = (x % P as u128) as u64; + assert_eq!( + Fp64::

::from_canonical_u128_reduced(x).to_canonical_u64(), + expected, + "P = {P}, x = {x}" + ); + } + } + check::<{ (1u64 << 40) - 195 }>(); + check::<{ (1u64 << 48) - 59 }>(); + check::<{ (1u64 << 56) - 27 }>(); + check::<{ u64::MAX - 58 }>(); + } + + #[test] + fn challenge_bytes_subword_primes() { + // 16-byte Fiat-Shamir challenge derivation must agree with plain + // u128 modular reduction for every registered Fp64 prime. + let bytes: [u8; 16] = [ + 0xEF, 0xBE, 0xAD, 0xDE, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, + 0xBA, 0x98, + ]; + let x = u128::from_le_bytes(bytes); + fn check(x: u128, bytes: &[u8]) { + use crate::CanonicalRepr; + let expected = (x % P as u128) as u64; + assert_eq!( + Fp64::

::from_challenge_bytes(bytes).to_canonical_u64(), + expected, + "P = {P}" + ); + } + check::<{ (1u64 << 40) - 195 }>(x, &bytes); + check::<{ (1u64 << 48) - 59 }>(x, &bytes); + check::<{ (1u64 << 56) - 27 }>(x, &bytes); + check::<{ u64::MAX - 58 }>(x, &bytes); + } + #[test] fn reduce_u128_large() { assert_eq!(F64::from_canonical_u128_reduced(u128::MAX), { diff --git a/crates/jolt-field/src/signed/signed_bigint_hi32.rs b/crates/jolt-field/src/signed/signed_bigint_hi32.rs index ca2f39ea71..d3e3d0c42b 100644 --- a/crates/jolt-field/src/signed/signed_bigint_hi32.rs +++ b/crates/jolt-field/src/signed/signed_bigint_hi32.rs @@ -189,14 +189,24 @@ impl SignedBigIntHi32 { let r0 = t0 as u64; let carry0 = t0 >> 64; - let sum1 = carry0 + (a0 as u128) * (b1 as u128) + (a1 as u128) * (b0 as u128); + // Word 1 sums two full 64x64 products plus the carry, which can + // exceed u128 (2 * (2^64 - 1)^2 > 2^128); one product plus the + // carry always fits, so add the second with overflow tracking. + let p01 = (a0 as u128) * (b1 as u128); + let p10 = (a1 as u128) * (b0 as u128); + let (sum1, overflow1) = (p01 + carry0).overflowing_add(p10); let r1 = sum1 as u64; - let carry1 = sum1 >> 64; + // True carry into word 2 (up to 2^65): high half plus the + // overflowed 2^128 bit, which contributes 2^64 to the carry. + let carry1 = (sum1 >> 64) + ((overflow1 as u128) << 64); + // Only the low 32 bits of word 2 survive in the 160-bit result, + // and wrapping addition preserves low bits exactly, so word 2 + // needs no overflow tracking. let sum2 = carry1 - + (a0 as u128) * (b2 as u128) - + (a1 as u128) * (b1 as u128) - + (a2 as u128) * (b0 as u128); + .wrapping_add((a0 as u128) * (b2 as u128)) + .wrapping_add((a1 as u128) * (b1 as u128)) + .wrapping_add((a2 as u128) * (b0 as u128)); let r2 = sum2 as u64; let hi = (r2 & 0xFFFF_FFFF) as u32; @@ -538,6 +548,73 @@ impl Default for S160 { #[cfg(test)] mod tests { + + fn oracle_mul_160(a: &S160, b: &S160) -> ([u64; 2], u32) { + // 32-bit digit schoolbook, truncated to 160 bits: the independent + // reference for the unrolled N == 2 kernel. + let digits = |v: &S160| -> [u32; 5] { + [ + v.magnitude_lo[0] as u32, + (v.magnitude_lo[0] >> 32) as u32, + v.magnitude_lo[1] as u32, + (v.magnitude_lo[1] >> 32) as u32, + v.magnitude_hi, + ] + }; + let (a, b) = (digits(a), digits(b)); + let mut cols = [0u128; 10]; + for i in 0..5 { + for j in 0..5 { + cols[i + j] += (a[i] as u128) * (b[j] as u128); + } + } + let mut out = [0u32; 10]; + let mut carry = 0u128; + for k in 0..10 { + let v = cols[k] + carry; + out[k] = (v & 0xFFFF_FFFF) as u32; + carry = v >> 32; + } + ( + [ + out[0] as u64 | (out[1] as u64) << 32, + out[2] as u64 | (out[3] as u64) << 32, + ], + out[4], + ) + } + + #[test] + fn s160_mul_magnitudes_large_second_limbs() { + // Regression: the pre-fix kernel fused two (three) full 64x64 + // products into one u128 sum, which overflows once both operands + // have large second limbs (debug panic, release wraparound). + let make = |lo: [u64; 2], hi: u32| S160 { + magnitude_lo: lo, + magnitude_hi: hi, + is_positive: true, + }; + let cases = [ + (make([u64::MAX, u64::MAX], 0), make([u64::MAX, u64::MAX], 0)), + ( + make([u64::MAX, u64::MAX], u32::MAX), + make([u64::MAX, u64::MAX], u32::MAX), + ), + (make([0, u64::MAX], 0), make([0, u64::MAX], 0)), + (make([1, u64::MAX], 0), make([u64::MAX, 1], 0)), + ( + make([0xDEAD_BEEF_0123_4567, 0x89AB_CDEF_FEDC_BA98], 0x0F0F_0F0F), + make([0x1111_2222_3333_4444, 0x5555_6666_7777_8888], 0xFFFF_0000), + ), + (make([42, 0], 0), make([37, 0], 0)), + ]; + for (a, b) in cases { + let got = a.mul_magnitudes(&b); + let want = oracle_mul_160(&a, &b); + assert_eq!(got, want, "a = {a:?}, b = {b:?}"); + } + } + use super::*; #[test] From a80f0e5fc76aeaf5d2a1c01589c5ea44db328079 Mon Sep 17 00:00:00 2001 From: acentelles Date: Wed, 29 Jul 2026 16:09:58 -0400 Subject: [PATCH 15/38] docs(field): correct the S160 overflow failure-mode description Adversarial audit of the fix showed the old fused kernel's release output was correct (wrapped carries cancel modulo 2^64; only the low 32 bits of word 2 survive), so the pre-fix defect was a debug-only panic, not release wraparound. Tighten the carry bound to its exact maximum while here. --- crates/jolt-field/src/signed/signed_bigint_hi32.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/jolt-field/src/signed/signed_bigint_hi32.rs b/crates/jolt-field/src/signed/signed_bigint_hi32.rs index d3e3d0c42b..06ee1ba125 100644 --- a/crates/jolt-field/src/signed/signed_bigint_hi32.rs +++ b/crates/jolt-field/src/signed/signed_bigint_hi32.rs @@ -192,11 +192,14 @@ impl SignedBigIntHi32 { // Word 1 sums two full 64x64 products plus the carry, which can // exceed u128 (2 * (2^64 - 1)^2 > 2^128); one product plus the // carry always fits, so add the second with overflow tracking. + // (The old fused sum's failure mode was a debug-only panic: its + // wrapped carries cancel modulo 2^64, and only the low 32 bits + // of word 2 survive, so release output happened to be correct.) let p01 = (a0 as u128) * (b1 as u128); let p10 = (a1 as u128) * (b0 as u128); let (sum1, overflow1) = (p01 + carry0).overflowing_add(p10); let r1 = sum1 as u64; - // True carry into word 2 (up to 2^65): high half plus the + // True carry into word 2 (at most 2^65 - 3): high half plus the // overflowed 2^128 bit, which contributes 2^64 to the carry. let carry1 = (sum1 >> 64) + ((overflow1 as u128) << 64); From ff5bf9cd8821a101a77252fde9779465ebbf01b7 Mon Sep 17 00:00:00 2001 From: acentelles Date: Thu, 30 Jul 2026 14:28:56 -0400 Subject: [PATCH 16/38] refactor(field): split CanonicalBytes out of CanonicalRepr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transcript absorption only needs a canonical byte encoding (NUM_BYTES + to_bytes_le), but the consolidated CanonicalRepr forced every absorbable type to also claim the field-decode surface (reducing constructor, canonical-integer views, challenge derivation). #1675's NoCommitment placeholder — a commitment, not a field element — exposed the over-coupling: it satisfied the old two-method CanonicalBytes and had nothing honest to say for the rest. Split the absorption surface back out: CanonicalBytes carries the encoding and its injectivity invariants; CanonicalRepr: CanonicalBytes keeps decode and challenges; jolt-transcript's AppendToTranscript blanket narrows to CanonicalBytes; NoCommitment implements only what it is. Byte-only consumers (stage8 immediates, jolt-akita scheme/adapters, jolt-eval transcript symmetry, blindfold test support) narrow accordingly; decode users are unchanged. The consolidation spec's trait count is amended to 23. --- crates/jolt-akita/src/adapters.rs | 2 +- crates/jolt-akita/src/scheme.rs | 2 +- crates/jolt-blindfold/tests/support/mod.rs | 2 +- crates/jolt-field/benches/field_arith.rs | 2 +- .../fuzz/fuzz_targets/from_bytes.rs | 2 +- crates/jolt-field/src/akita.rs | 8 +++-- crates/jolt-field/src/arkworks/bn254.rs | 10 +++--- crates/jolt-field/src/arkworks/bn254_fq.rs | 10 +++--- crates/jolt-field/src/canonical.rs | 33 ++++++++++++------- crates/jolt-field/src/lib.rs | 2 +- .../jolt-field/src/prime/native_capability.rs | 13 +++++--- .../tests/binary_field_core_compat.rs | 6 ++-- crates/jolt-field/tests/coverage.rs | 3 +- crates/jolt-field/tests/serde_roundtrip.rs | 12 +++---- crates/jolt-prover-legacy/src/zkvm/packed.rs | 9 +++-- .../jolt-sumcheck/tests/mersenne61_compat.rs | 8 +++-- crates/jolt-transcript/src/legacy.rs | 4 +-- .../jolt-verifier/src/stages/stage8/packed.rs | 8 ++--- .../src/stages/stage8/reconstruction.rs | 4 +-- .../tests/statistical_independence/zk.rs | 2 +- .../src/invariant/transcript_symmetry.rs | 2 +- specs/consolidate-field-traits.md | 4 +-- tracer/src/instruction/field_inline.rs | 2 +- 23 files changed, 87 insertions(+), 63 deletions(-) diff --git a/crates/jolt-akita/src/adapters.rs b/crates/jolt-akita/src/adapters.rs index e53d5f241a..616e3e103d 100644 --- a/crates/jolt-akita/src/adapters.rs +++ b/crates/jolt-akita/src/adapters.rs @@ -9,7 +9,7 @@ use akita_types::{ AkitaCommitmentHint as AkitaBackendCommitmentHint, AkitaVerifierSetup as AkitaBackendVerifierSetup, Commitment as AkitaBackendRingCommitment, }; -use jolt_field::CanonicalRepr; +use jolt_field::CanonicalBytes; use jolt_openings::{OpeningsError, VerifierOpeningClaim}; use jolt_poly::{MultilinearPoly, OneHotIndexOrder, OneHotPolynomial, Polynomial}; use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript, U64Word}; diff --git a/crates/jolt-akita/src/scheme.rs b/crates/jolt-akita/src/scheme.rs index 08f7ec642d..15555a38ca 100644 --- a/crates/jolt-akita/src/scheme.rs +++ b/crates/jolt-akita/src/scheme.rs @@ -1,6 +1,6 @@ use akita_pcs::{ComputeBackendSetup, CpuBackend}; use jolt_crypto::Commitment; -use jolt_field::CanonicalRepr; +use jolt_field::CanonicalBytes; use jolt_openings::{ BatchOpeningScheme, CommitmentScheme, EvaluationClaim, OpeningsError, VerifierOpeningClaim, ZkBatchOpeningScheme, ZkOpeningScheme, diff --git a/crates/jolt-blindfold/tests/support/mod.rs b/crates/jolt-blindfold/tests/support/mod.rs index c955e11753..c314f3f190 100644 --- a/crates/jolt-blindfold/tests/support/mod.rs +++ b/crates/jolt-blindfold/tests/support/mod.rs @@ -12,7 +12,7 @@ use jolt_claims::{challenge, constant, derived, opening, Expr}; use jolt_crypto::{ Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment, VectorCommitmentOpening, }; -use jolt_field::{CanonicalRepr, FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{CanonicalBytes, FieldCore, Fr, FromPrimitiveInt}; use jolt_poly::{CompressedPoly, EqPolynomial}; use jolt_r1cs::{ClaimSourceTable, ConstraintMatrices, R1csBuilder}; use jolt_sumcheck::{ diff --git a/crates/jolt-field/benches/field_arith.rs b/crates/jolt-field/benches/field_arith.rs index ef86a0b1f8..90e9152790 100644 --- a/crates/jolt-field/benches/field_arith.rs +++ b/crates/jolt-field/benches/field_arith.rs @@ -3,7 +3,7 @@ use std::hint::black_box; use criterion::{criterion_group, criterion_main, Criterion}; -use jolt_field::{CanonicalRepr, FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{CanonicalBytes, CanonicalRepr, FieldCore, Fr, FromPrimitiveInt}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs b/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs index 1d77da3b72..436af75a5f 100644 --- a/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs +++ b/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs @@ -1,5 +1,5 @@ #![no_main] -use jolt_field::{CanonicalRepr, Fr}; +use jolt_field::{CanonicalBytes, CanonicalRepr, Fr}; use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { diff --git a/crates/jolt-field/src/akita.rs b/crates/jolt-field/src/akita.rs index 927daebc49..a572e9fc5f 100644 --- a/crates/jolt-field/src/akita.rs +++ b/crates/jolt-field/src/akita.rs @@ -2,8 +2,8 @@ use akita_config::proof_optimized::fp128::Field as AkitaField; use rand_core::RngCore; use crate::{ - AdditiveGroup, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, NaiveAccumulator, RingCore, - WithAccumulator, + AdditiveGroup, CanonicalBytes, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, + NaiveAccumulator, RingCore, WithAccumulator, }; impl AdditiveGroup for AkitaField {} @@ -44,14 +44,16 @@ impl FromPrimitiveInt for AkitaField { } } -impl CanonicalRepr for AkitaField { +impl CanonicalBytes for AkitaField { const NUM_BYTES: usize = ::NUM_BYTES; #[inline(always)] fn to_bytes_le(&self, out: &mut [u8]) { ::to_bytes_le(self, out); } +} +impl CanonicalRepr for AkitaField { #[inline(always)] fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { ::from_le_bytes_mod_order(bytes) diff --git a/crates/jolt-field/src/arkworks/bn254.rs b/crates/jolt-field/src/arkworks/bn254.rs index 974f116d3d..cf2fd296d2 100644 --- a/crates/jolt-field/src/arkworks/bn254.rs +++ b/crates/jolt-field/src/arkworks/bn254.rs @@ -3,8 +3,8 @@ //! [`Fr`] is `#[repr(transparent)]` over the inner arkworks scalar field element, //! so it has identical layout and can be transmuted where needed. use crate::{ - AdditiveGroup, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, Limbs, RingCore, - WithAccumulator, + AdditiveGroup, CanonicalBytes, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, Limbs, + RingCore, WithAccumulator, }; use ark_ff::{prelude::*, PrimeField, UniformRand}; use rand_core::RngCore; @@ -334,19 +334,21 @@ impl FieldCore for Fr { } } -impl CanonicalRepr for Fr { +impl CanonicalBytes for Fr { const NUM_BYTES: usize = 32; #[expect(clippy::expect_used)] #[inline] fn to_bytes_le(&self, out: &mut [u8]) { - assert_eq!(out.len(), ::NUM_BYTES); + assert_eq!(out.len(), ::NUM_BYTES); use ark_serialize::CanonicalSerialize; self.0 .serialize_compressed(out) .expect("BN254 Fr always serializes to 32 bytes"); } +} +impl CanonicalRepr for Fr { #[inline] fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { Fr::from_le_bytes_mod_order(bytes) diff --git a/crates/jolt-field/src/arkworks/bn254_fq.rs b/crates/jolt-field/src/arkworks/bn254_fq.rs index 992c38e55c..41c67363d9 100644 --- a/crates/jolt-field/src/arkworks/bn254_fq.rs +++ b/crates/jolt-field/src/arkworks/bn254_fq.rs @@ -4,8 +4,8 @@ //! scalar field of Grumpkin. use crate::{ - AdditiveGroup, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, Limbs, NaiveAccumulator, - RingCore, WithAccumulator, + AdditiveGroup, CanonicalBytes, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, Limbs, + NaiveAccumulator, RingCore, WithAccumulator, }; use ark_ff::{prelude::*, PrimeField, UniformRand}; use rand_core::RngCore; @@ -314,19 +314,21 @@ impl FieldCore for Fq { } } -impl CanonicalRepr for Fq { +impl CanonicalBytes for Fq { const NUM_BYTES: usize = 32; #[expect(clippy::expect_used)] #[inline] fn to_bytes_le(&self, out: &mut [u8]) { - assert_eq!(out.len(), ::NUM_BYTES); + assert_eq!(out.len(), ::NUM_BYTES); use ark_serialize::CanonicalSerialize; self.0 .serialize_compressed(out) .expect("BN254 Fq always serializes to 32 bytes"); } +} +impl CanonicalRepr for Fq { #[inline] fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { Fq::from_le_bytes_mod_order(bytes) diff --git a/crates/jolt-field/src/canonical.rs b/crates/jolt-field/src/canonical.rs index 3770fc735e..7d51847577 100644 --- a/crates/jolt-field/src/canonical.rs +++ b/crates/jolt-field/src/canonical.rs @@ -3,23 +3,27 @@ use std::fmt::Debug; use std::hash::Hash; -/// Canonical little-endian representation of a field element. +/// Fixed-size canonical little-endian byte encoding: the transcript +/// absorption surface. /// -/// This trait is the transcript surface: Fiat-Shamir absorption and challenge -/// derivation use these explicit canonical encodings so the hashed byte -/// stream is specified independently of any serialization library. Proof and -/// wire serialization go through serde + bincode instead; the two must not be -/// conflated. +/// Fiat-Shamir absorption uses this explicit canonical encoding so the +/// hashed byte stream is specified independently of any serialization +/// library. Proof and wire serialization go through serde + bincode +/// instead; the two must not be conflated. +/// +/// This is deliberately the *narrow* claim, "this value has one canonical +/// byte encoding", implementable by non-field types (e.g. zero-sized +/// commitment placeholders) that must be transcript-absorbable without +/// pretending to be decodable field elements. Field types get the full +/// decode surface via [`CanonicalRepr`]. /// /// # Invariants /// -/// - The encoding is injective on canonical representatives: equal elements -/// produce equal bytes, distinct elements produce distinct bytes. +/// - The encoding is injective on canonical representatives: equal values +/// produce equal bytes, distinct values produce distinct bytes. /// - [`to_bytes_le`](Self::to_bytes_le) always writes exactly /// [`NUM_BYTES`](Self::NUM_BYTES) bytes of the unique representative. -pub trait CanonicalRepr: - Sized + Copy + Default + PartialEq + Eq + Debug + Hash + Sync + Send + 'static -{ +pub trait CanonicalBytes { /// Byte length of the fixed-size canonical encoding. const NUM_BYTES: usize; @@ -33,7 +37,14 @@ pub trait CanonicalRepr: self.to_bytes_le(&mut out); out } +} +/// Canonical decode-and-introspect surface of a field element: reducing +/// byte/challenge constructors and canonical-integer views, on top of the +/// [`CanonicalBytes`] encoding. +pub trait CanonicalRepr: + CanonicalBytes + Sized + Copy + Default + PartialEq + Eq + Debug + Hash + Sync + Send + 'static +{ /// Deserializes little-endian bytes by reducing into this type. fn from_le_bytes_mod_order(bytes: &[u8]) -> Self; diff --git a/crates/jolt-field/src/lib.rs b/crates/jolt-field/src/lib.rs index a3f3df1e6c..cfaa981129 100644 --- a/crates/jolt-field/src/lib.rs +++ b/crates/jolt-field/src/lib.rs @@ -53,7 +53,7 @@ mod native_algebra; pub use accumulator::{Accumulator, NaiveAccumulator, WithAccumulator}; pub use algebra::{AdditiveGroup, FieldCore, FromPrimitiveInt, RingCore}; -pub use canonical::CanonicalRepr; +pub use canonical::{CanonicalBytes, CanonicalRepr}; pub use field::Field; pub use field_error::FieldError; pub use montgomery_constants::MontgomeryConstants; diff --git a/crates/jolt-field/src/prime/native_capability.rs b/crates/jolt-field/src/prime/native_capability.rs index b2489dea1d..eea6276f0f 100644 --- a/crates/jolt-field/src/prime/native_capability.rs +++ b/crates/jolt-field/src/prime/native_capability.rs @@ -10,18 +10,20 @@ use crate::{FieldCore, FromPrimitiveInt}; macro_rules! impl_prime_native_capability { ($ty:ident<$p:ident: $p_ty:ty>, $bytes:expr) => { - impl $crate::CanonicalRepr for $ty<$p> { + impl $crate::CanonicalBytes for $ty<$p> { const NUM_BYTES: usize = $bytes; #[inline(always)] fn to_bytes_le(&self, out: &mut [u8]) { - assert_eq!(out.len(), ::NUM_BYTES); + assert_eq!(out.len(), ::NUM_BYTES); out.copy_from_slice( &self.to_canonical_u128().to_le_bytes() - [..::NUM_BYTES], + [..::NUM_BYTES], ); } + } + impl $crate::CanonicalRepr for $ty<$p> { #[inline(always)] fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { if bytes.len() <= ::std::mem::size_of::() { @@ -75,7 +77,8 @@ mod tests { //! `--no-default-features --features solinas` as well as combined builds. use super::*; use crate::{ - Accumulator, CanonicalField, CanonicalRepr, Fp32, Fp64, Prime128Offset275, WithAccumulator, + Accumulator, CanonicalBytes, CanonicalField, CanonicalRepr, Fp32, Fp64, Prime128Offset275, + WithAccumulator, }; /// Asserts the full canonical byte round-trip on the native traits. @@ -83,7 +86,7 @@ mod tests { where F: CanonicalField + CanonicalRepr + std::fmt::Debug + Eq, { - assert_eq!(::NUM_BYTES, N); + assert_eq!(::NUM_BYTES, N); // to_bytes_le (the audited method) into a correctly sized buffer, plus // the vec convenience wrapper — both must agree. diff --git a/crates/jolt-field/tests/binary_field_core_compat.rs b/crates/jolt-field/tests/binary_field_core_compat.rs index 414f8f8b23..d12cc58331 100644 --- a/crates/jolt-field/tests/binary_field_core_compat.rs +++ b/crates/jolt-field/tests/binary_field_core_compat.rs @@ -12,7 +12,7 @@ use std::{ ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}, }; -use jolt_field::{AdditiveGroup, CanonicalRepr, FieldCore, RingCore}; +use jolt_field::{AdditiveGroup, CanonicalBytes, CanonicalRepr, FieldCore, RingCore}; use num_traits::{One, Zero}; #[derive(Clone, Copy, Default, PartialEq, Eq, Hash)] @@ -168,14 +168,16 @@ impl FieldCore for Gf2 { } } -impl CanonicalRepr for Gf2 { +impl CanonicalBytes for Gf2 { const NUM_BYTES: usize = 1; fn to_bytes_le(&self, out: &mut [u8]) { assert_eq!(out.len(), 1); out[0] = self.0 as u8; } +} +impl CanonicalRepr for Gf2 { fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { Self(bytes.iter().fold(0u8, |acc, b| acc ^ (b & 1)) == 1) } diff --git a/crates/jolt-field/tests/coverage.rs b/crates/jolt-field/tests/coverage.rs index 16346fc393..15fd5652d2 100644 --- a/crates/jolt-field/tests/coverage.rs +++ b/crates/jolt-field/tests/coverage.rs @@ -8,7 +8,8 @@ use ark_std::test_rng; use jolt_field::signed::*; use jolt_field::{ - Accumulator, CanonicalRepr, FieldCore, Fr, FromPrimitiveInt, Limbs, NaiveAccumulator, + Accumulator, CanonicalBytes, CanonicalRepr, FieldCore, Fr, FromPrimitiveInt, Limbs, + NaiveAccumulator, }; use num_traits::{One, Zero}; diff --git a/crates/jolt-field/tests/serde_roundtrip.rs b/crates/jolt-field/tests/serde_roundtrip.rs index 4566d4b4f9..ca551fcd1f 100644 --- a/crates/jolt-field/tests/serde_roundtrip.rs +++ b/crates/jolt-field/tests/serde_roundtrip.rs @@ -5,8 +5,8 @@ #![expect(clippy::unwrap_used)] use jolt_field::{ - CanonicalRepr, Ext2, FieldCore, FpExt4, FpExt8, Prime128Offset275, Prime32Offset99, - Prime64Offset59, + CanonicalBytes, CanonicalRepr, Ext2, FieldCore, FpExt4, FpExt8, Prime128Offset275, + Prime32Offset99, Prime64Offset59, }; use rand::rngs::StdRng; use rand::SeedableRng; @@ -37,9 +37,9 @@ where fn prime_field_elements_encode_to_num_bytes() { let mut rng = StdRng::seed_from_u64(7); for _ in 0..32 { - assert_roundtrip_with_size(&F32::random(&mut rng), ::NUM_BYTES); - assert_roundtrip_with_size(&F64::random(&mut rng), ::NUM_BYTES); - assert_roundtrip_with_size(&F128::random(&mut rng), ::NUM_BYTES); + assert_roundtrip_with_size(&F32::random(&mut rng), ::NUM_BYTES); + assert_roundtrip_with_size(&F64::random(&mut rng), ::NUM_BYTES); + assert_roundtrip_with_size(&F128::random(&mut rng), ::NUM_BYTES); } } @@ -62,7 +62,7 @@ fn vectors_add_only_a_single_length_prefix() { // bincode's standard config uses a varint length prefix: 1 byte for // lengths below 251. let prefix = if n < 251 { 1 } else { 3 }; - assert_eq!(bytes.len(), prefix + n * ::NUM_BYTES); + assert_eq!(bytes.len(), prefix + n * ::NUM_BYTES); let (decoded, _): (Vec, usize) = bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap(); assert_eq!(decoded, v); diff --git a/crates/jolt-prover-legacy/src/zkvm/packed.rs b/crates/jolt-prover-legacy/src/zkvm/packed.rs index 12b6a0a8fb..99fdf67ab7 100644 --- a/crates/jolt-prover-legacy/src/zkvm/packed.rs +++ b/crates/jolt-prover-legacy/src/zkvm/packed.rs @@ -2299,7 +2299,7 @@ mod committed_tests { } use jolt_crypto::{Commitment, HomomorphicCommitment, VectorCommitment}; -use jolt_field::{CanonicalBytes, Field, FixedByteSize}; +use jolt_field::{CanonicalBytes, Field}; use serde::{Deserialize, Serialize}; use std::fmt::{self, Debug}; @@ -2336,12 +2336,11 @@ pub struct NoCommitment; // `AppendToTranscript` comes from jolt-transcript's blanket impl over // `CanonicalBytes`: an empty canonical encoding, so absorbing a -// `NoCommitment` is a no-op. -impl FixedByteSize for NoCommitment { +// `NoCommitment` is a no-op. Deliberately NOT `CanonicalRepr`: a commitment +// placeholder is not a decodable field element. +impl CanonicalBytes for NoCommitment { const NUM_BYTES: usize = 0; -} -impl CanonicalBytes for NoCommitment { fn to_bytes_le(&self, _out: &mut [u8]) {} } diff --git a/crates/jolt-sumcheck/tests/mersenne61_compat.rs b/crates/jolt-sumcheck/tests/mersenne61_compat.rs index 849d776824..2d91a38429 100644 --- a/crates/jolt-sumcheck/tests/mersenne61_compat.rs +++ b/crates/jolt-sumcheck/tests/mersenne61_compat.rs @@ -15,8 +15,8 @@ use std::{ }; use jolt_field::{ - AdditiveGroup, CanonicalRepr, FieldCore, FromPrimitiveInt, NaiveAccumulator, RingCore, - WithAccumulator, + AdditiveGroup, CanonicalBytes, CanonicalRepr, FieldCore, FromPrimitiveInt, NaiveAccumulator, + RingCore, WithAccumulator, }; use jolt_sumcheck::{ BooleanHypercube, ClearRound, EvaluationClaim, RoundMessage, SumcheckClaim, SumcheckVerifier, @@ -239,14 +239,16 @@ impl FromPrimitiveInt for Mersenne61 { } } -impl CanonicalRepr for Mersenne61 { +impl CanonicalBytes for Mersenne61 { const NUM_BYTES: usize = 8; fn to_bytes_le(&self, out: &mut [u8]) { assert_eq!(out.len(), 8); out.copy_from_slice(&self.0.to_le_bytes()); } +} +impl CanonicalRepr for Mersenne61 { fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { let mut buf = [0u8; 16]; let len = bytes.len().min(16); diff --git a/crates/jolt-transcript/src/legacy.rs b/crates/jolt-transcript/src/legacy.rs index fae7dc15d0..803f25c49d 100644 --- a/crates/jolt-transcript/src/legacy.rs +++ b/crates/jolt-transcript/src/legacy.rs @@ -7,7 +7,7 @@ use std::marker::PhantomData; -use jolt_field::{CanonicalRepr, Field, FromPrimitiveInt}; +use jolt_field::{CanonicalBytes, CanonicalRepr, Field, FromPrimitiveInt}; use spongefish::{DuplexSpongeInterface, Encoding}; use crate::codec::BytesMsg; @@ -120,7 +120,7 @@ pub trait AppendToTranscript { /// Big-endian field element absorption (matches jolt-prover-legacy's EVM-compatible /// byte order). -impl AppendToTranscript for F { +impl AppendToTranscript for F { fn append_to_transcript(&self, transcript: &mut T) { let mut buf = vec![0u8; F::NUM_BYTES]; self.to_bytes_le(&mut buf); diff --git a/crates/jolt-verifier/src/stages/stage8/packed.rs b/crates/jolt-verifier/src/stages/stage8/packed.rs index 307011d8a4..e45eb56356 100644 --- a/crates/jolt-verifier/src/stages/stage8/packed.rs +++ b/crates/jolt-verifier/src/stages/stage8/packed.rs @@ -18,7 +18,7 @@ use jolt_claims::protocols::jolt::lattice::strategy::{ use jolt_claims::protocols::jolt::{ JoltAdviceKind, JoltCommittedPolynomial, JoltOneHotConfig, JoltOpeningId, JoltPolynomialId, }; -use jolt_field::{CanonicalRepr, Field}; +use jolt_field::{CanonicalBytes, Field}; use jolt_openings::{ verify_packed_openings, CommitmentScheme, EvaluationClaim, PackedObjectGroup, PackedVerifierObject, PrefixPackedStatement, PrefixPacking, @@ -292,7 +292,7 @@ where precommitted_packing(&PrecommittedPackingShape { bytecode_chunks: committed.bytecode_chunk_count(), log_bytecode_rows, - imm_byte_width: ::NUM_BYTES, + imm_byte_width: ::NUM_BYTES, program_image_log_words, }) .map_err(batch_failed)?, @@ -730,7 +730,7 @@ mod tests { imm_bytes: vec![ point( jolt_claims::protocols::jolt::lattice::geometry::byte_num_vars( - ::NUM_BYTES, + ::NUM_BYTES, LOG_BYTECODE_ROWS, ) .unwrap() @@ -776,7 +776,7 @@ mod tests { precommitted_packing(&PrecommittedPackingShape { bytecode_chunks: BYTECODE_CHUNKS, log_bytecode_rows: LOG_BYTECODE_ROWS, - imm_byte_width: ::NUM_BYTES, + imm_byte_width: ::NUM_BYTES, program_image_log_words: Some(LOG_IMAGE_WORDS), }) .unwrap(), diff --git a/crates/jolt-verifier/src/stages/stage8/reconstruction.rs b/crates/jolt-verifier/src/stages/stage8/reconstruction.rs index 79cebb42c2..575a7a7d70 100644 --- a/crates/jolt-verifier/src/stages/stage8/reconstruction.rs +++ b/crates/jolt-verifier/src/stages/stage8/reconstruction.rs @@ -38,7 +38,7 @@ use jolt_claims::protocols::jolt::{ UntrustedAdviceReconstructionPublic, }; use jolt_claims::{NoChallenges, SymbolicSumcheck}; -use jolt_field::{CanonicalRepr, Field}; +use jolt_field::{CanonicalBytes, Field}; use jolt_poly::math::Math; use jolt_poly::{eq_index_msb, try_eq_mle}; use jolt_sumcheck::SumcheckProof; @@ -676,7 +676,7 @@ where let (r_lane, r_row) = shared_point.split_at(lane_vars); let dimensions = BytecodeReconstructionDimensions { chunks: chunk_values.len(), - imm_byte_width: ::NUM_BYTES, + imm_byte_width: ::NUM_BYTES, }; let instance = BytecodeChunkReconstructionInstance { symbolic: BytecodeSymbolic::new(dimensions), diff --git a/crates/jolt-verifier/tests/statistical_independence/zk.rs b/crates/jolt-verifier/tests/statistical_independence/zk.rs index 642cdabb1b..7a846ca833 100644 --- a/crates/jolt-verifier/tests/statistical_independence/zk.rs +++ b/crates/jolt-verifier/tests/statistical_independence/zk.rs @@ -14,7 +14,7 @@ use std::collections::BTreeMap; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] use ark_serialize::CanonicalSerialize; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_field::{CanonicalRepr, Fr}; +use jolt_field::{CanonicalBytes, CanonicalRepr, Fr}; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] use jolt_sumcheck::SumcheckProof; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] diff --git a/jolt-eval/src/invariant/transcript_symmetry.rs b/jolt-eval/src/invariant/transcript_symmetry.rs index 96287e7f95..b9c7f588e8 100644 --- a/jolt-eval/src/invariant/transcript_symmetry.rs +++ b/jolt-eval/src/invariant/transcript_symmetry.rs @@ -4,7 +4,7 @@ //! verifier challenges. use arbitrary::{Arbitrary, Unstructured}; -use jolt_field::{CanonicalRepr, Fr as JFr}; +use jolt_field::{CanonicalBytes, Fr as JFr}; use spongefish::instantiations::{Blake2b512, Keccak}; use jolt_transcript::{prover_transcript, verifier_transcript, BytesMsg, PoseidonSponge}; diff --git a/specs/consolidate-field-traits.md b/specs/consolidate-field-traits.md index aac512478b..184c123f01 100644 --- a/specs/consolidate-field-traits.md +++ b/specs/consolidate-field-traits.md @@ -53,7 +53,7 @@ This is a refactor of trait boundaries, not of arithmetic. The `jolt-eval` invar ### Acceptance Criteria -- [x] `grep -rc '^pub trait' crates/jolt-field/src` totals at most 22. (22 as of phase 5; 21 once `MontgomeryConstants` is resolved.) +- [x] `grep -rc '^pub trait' crates/jolt-field/src` totals at most 22. (22 as of phase 5; amended to 23 when `CanonicalBytes` was split back out of `CanonicalRepr` so transcript absorption does not require the field-decode contract — see the `NoCommitment` case from #1675; 22 once `MontgomeryConstants` is resolved.) - [x] The crate root has at most 4 trait-defining modules (`algebra.rs`, `canonical.rs`, `accumulator.rs`, `field.rs`) plus the feature-gated backend modules; the 15 micro-files are gone. - [x] Zero references remain to: `SignedScalarAccumulator`, `WithSmallScalarAccumulator`, `SignedProductAccumulator`, `WithSignedProductAccumulator`, `ExtensionCoeff`, `BalancedDigitLookup` (trait), `ScaleI32`, `PackedValue`, `LiftBase`, `MulBase`, `FrobeniusExtField`, `FpExt4MulBackend`, `FpExt8MulBackend`, `AdditiveAccumulator`, `Invertible`, `RandomSampling`, `MulPow2`, `MulPrimitiveInt`, `CanonicalBytes`, `ReducingBytes`, `FixedByteSize`, `FixedBytes`, `CanonicalU64`, `CanonicalBitLength`, `TranscriptChallenge`, `SmoothFftField`. - [x] `fft.rs` is removed from `jolt-field` (it has zero consumers in this workspace). @@ -106,7 +106,7 @@ Trait disposition, all 46 accounted for: |---|---|---| | `FieldCore` | `Invertible`, `RandomSampling` | every `FieldCore` type implements both; rings stay unaffected at `RingCore` | | `FromPrimitiveInt` (gains `RingCore` supertrait) | `MulPow2`, `MulPrimitiveInt` | the absorbed traits are pure default-method helpers over exactly this bound | -| `CanonicalRepr` (new, one file) | `CanonicalBytes`, `ReducingBytes`, `FixedByteSize`, `FixedBytes`, `CanonicalU64`, `CanonicalBitLength`, `TranscriptChallenge` | one trait: `NUM_BYTES`, `to_bytes_le`, `from_le_bytes_mod_order`, `to_canonical_u64_checked`, `num_bits`, `from_challenge_bytes` (defaulted to reducing decode) | +| `CanonicalRepr` (new, one file; `CanonicalBytes` later re-split out as its supertrait, carrying `NUM_BYTES` + `to_bytes_le`, so non-field transcript-absorbable types keep a narrow claim) | `ReducingBytes`, `FixedByteSize`, `FixedBytes`, `CanonicalU64`, `CanonicalBitLength`, `TranscriptChallenge` | one trait: `NUM_BYTES`, `to_bytes_le`, `from_le_bytes_mod_order`, `to_canonical_u64_checked`, `num_bits`, `from_challenge_bytes` (defaulted to reducing decode) | | `Accumulator` | `AdditiveAccumulator` + `RingAccumulator` | the two are only ever implemented and consumed together (`WideAccumulator`, `NaiveAccumulator`) | | `ExtField` | `LiftBase`, `MulBase`, `FrobeniusExtField` | identical implementor sets (blanket `F` + `FpExt2/4/8`); Frobenius requires a pseudo-Mersenne base, which all current bases are | | `ExtMulBackend` | `FpExt4MulBackend` + `FpExt8MulBackend` | same three implementors, same role (per-width fused schedules) | diff --git a/tracer/src/instruction/field_inline.rs b/tracer/src/instruction/field_inline.rs index a0cf68c0c9..bad3e3895c 100644 --- a/tracer/src/instruction/field_inline.rs +++ b/tracer/src/instruction/field_inline.rs @@ -3,7 +3,7 @@ reason = "Tracer concrete instruction names mirror generated Jolt instruction constants" )] -use jolt_field::{CanonicalRepr, FieldCore, Fr}; +use jolt_field::{CanonicalBytes, CanonicalRepr, FieldCore, Fr}; use jolt_program::field_inline::{ FieldEncodedValue, FieldInlineBridge, FieldInlineTraceData, FieldRegisterRead, FieldRegisterWrite, From 5635b9824b8474691f4c66890e99d4ec7c4dc123 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Tue, 28 Jul 2026 15:27:59 -0400 Subject: [PATCH 17/38] feat(jolt-field-two): first-principles rebuild of jolt-field, checkpoints 1-4 Reference implementation of a minimal-LOC jolt-field rebuild, far enough along to show the shape. SPEC.md in the crate is the working spec: counting rules, trait system (15 contracts vs 22), backend architecture, per-file budgets, build order, and acceptance criteria for the remaining checkpoints (fp128, extensions, unreduced, packed, parallel). Structure: an unconditional contract layer at the crate root (algebra.rs spine + stamping macros + Limbs/signed) and two feature-gated backends that implement it as peers -- bn254/ (arkworks adapter + Barrett/Montgomery kernel + WideAccumulator) and solinas/ (one define_solinas_prime! fold algebra stamped at u32/u64, plus the offset registry). 2,050 counted LOC so far against a 2,310 budget; the equivalent baseline surface is ~4,200. Everything is differential-tested against jolt-field as the oracle (ops, serde bytes, transcript bytes, challenge derivation) plus independent u128/i128 oracles. That testing found two baseline bugs worth fixing in jolt-field regardless of this crate's fate: - S160's unrolled mul kernel overflows u128 in its cross-term sum for large second limbs (panics in debug, wraps in release). - Fp64::reduce_u128 truncates the fold's high part to u64, so sub-word u64 primes (40/48/56-bit) reduce inputs >= 2^(64+BITS) incorrectly -- including 16-byte challenge derivation, which is essentially always in the broken domain. This crate implements the correct reduction and gates its baseline-parity tests to the baseline's correct domain. Signed-bigint API is the consumer-audited subset (workspace + akita grep); unconsumed constructors and truncating combinators were dropped, and dead specializations (C_SHIFT, Mersenne31 half) were cut with evidence recorded in SPEC.md. --- Cargo.lock | 19 + Cargo.toml | 1 + crates/jolt-field-two/Cargo.toml | 37 ++ crates/jolt-field-two/SPEC.md | 218 +++++++++ crates/jolt-field-two/src/algebra.rs | 395 +++++++++++++++ crates/jolt-field-two/src/bn254/mod.rs | 342 +++++++++++++ crates/jolt-field-two/src/bn254/mont.rs | 436 +++++++++++++++++ crates/jolt-field-two/src/lib.rs | 54 +++ crates/jolt-field-two/src/limbs.rs | 291 ++++++++++++ crates/jolt-field-two/src/ops.rs | 187 ++++++++ crates/jolt-field-two/src/signed.rs | 448 ++++++++++++++++++ crates/jolt-field-two/src/solinas/mod.rs | 135 ++++++ crates/jolt-field-two/src/solinas/word.rs | 443 +++++++++++++++++ .../tests/bn254_differential.rs | 260 ++++++++++ .../tests/limbs_signed_differential.rs | 267 +++++++++++ .../tests/solinas_words_differential.rs | 326 +++++++++++++ crates/jolt-field-two/tests/spine.rs | 311 ++++++++++++ 17 files changed, 4170 insertions(+) create mode 100644 crates/jolt-field-two/Cargo.toml create mode 100644 crates/jolt-field-two/SPEC.md create mode 100644 crates/jolt-field-two/src/algebra.rs create mode 100644 crates/jolt-field-two/src/bn254/mod.rs create mode 100644 crates/jolt-field-two/src/bn254/mont.rs create mode 100644 crates/jolt-field-two/src/lib.rs create mode 100644 crates/jolt-field-two/src/limbs.rs create mode 100644 crates/jolt-field-two/src/ops.rs create mode 100644 crates/jolt-field-two/src/signed.rs create mode 100644 crates/jolt-field-two/src/solinas/mod.rs create mode 100644 crates/jolt-field-two/src/solinas/word.rs create mode 100644 crates/jolt-field-two/tests/bn254_differential.rs create mode 100644 crates/jolt-field-two/tests/limbs_signed_differential.rs create mode 100644 crates/jolt-field-two/tests/solinas_words_differential.rs create mode 100644 crates/jolt-field-two/tests/spine.rs diff --git a/Cargo.lock b/Cargo.lock index 11061b5444..c6ca388f33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3311,6 +3311,25 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "jolt-field-two" +version = "0.1.0" +dependencies = [ + "allocative", + "ark-bn254 0.5.0 (git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout)", + "ark-ff 0.5.0 (git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout)", + "ark-serialize 0.5.0 (git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout)", + "bincode 2.0.1", + "jolt-field", + "num-traits", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "rayon", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "jolt-hyperkzg" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 24d22bcd9c..5147c833bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ members = [ "crates/jolt-kernels", "crates/jolt-kernels-derive", "crates/jolt-prover", + "crates/jolt-field-two", "crates/jolt-prover-legacy", "tracer", "common", diff --git a/crates/jolt-field-two/Cargo.toml b/crates/jolt-field-two/Cargo.toml new file mode 100644 index 0000000000..58d8a85441 --- /dev/null +++ b/crates/jolt-field-two/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "jolt-field-two" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +description = "Minimal-LOC rebuild of jolt-field: shared field abstractions with BN254 and Solinas backends" +repository = "https://github.com/a16z/jolt" +keywords = ["SNARK", "cryptography", "finite-fields", "BN254", "Solinas"] +categories = ["cryptography"] + +[lints] +workspace = true + +[dependencies] +ark-ff = { workspace = true, optional = true } +ark-serialize = { workspace = true, optional = true } +ark-bn254 = { workspace = true, features = ["curve"], optional = true } +num-traits = { workspace = true } +serde = { workspace = true, features = ["derive"] } +allocative = { workspace = true, optional = true } +rand_core = { workspace = true } +rayon = { workspace = true, optional = true } +thiserror = { workspace = true } + +[features] +default = ["bn254"] +bn254 = ["dep:ark-ff", "dep:ark-serialize", "dep:ark-bn254"] +solinas = [] +parallel = ["dep:rayon"] +allocative = ["dep:allocative"] + +[dev-dependencies] +bincode = { workspace = true } +# Differential-testing oracle: the crate this one is rebuilding. +jolt-field = { path = "../jolt-field", features = ["solinas"] } +rand = { workspace = true } +rand_chacha = { workspace = true } diff --git a/crates/jolt-field-two/SPEC.md b/crates/jolt-field-two/SPEC.md new file mode 100644 index 0000000000..c73f42e040 --- /dev/null +++ b/crates/jolt-field-two/SPEC.md @@ -0,0 +1,218 @@ +# Spec: jolt-field-two — minimal-LOC rebuild of jolt-field + +| Field | Value | +|---------|----------------------------------------------------| +| Status | approved — building (checkpoint 1) | +| Baseline| `jolt-field` @ PR #1684 head (`fe1d5d41f`) | +| Goal | functional parity at ≤ 6,300 counted LOC (baseline: 11,410) | + +## Goal + +Rebuild `crates/jolt-field` from first principles minimizing source LOC while +preserving functionality: both backends (BN254 arkworks + full Solinas stack), +wire/transcript **byte** compatibility, and static dispatch. Trait names and +boundaries are redesigned from scratch — old-name compatibility is explicitly +NOT a goal (approved); consumers rebind at replacement time. The crate lives +at `crates/jolt-field-two` until ready to replace `jolt-field`. + +## Counting rules + +**Counted:** non-blank, non-comment lines under `src/`, excluding `#[cfg(test)]` +regions. Doc comments and regular comments are **free** — the budget must never +create pressure to strip documentation. Inline test modules must be the final +item of their file (the counter cuts at the first `#[cfg(test)]`). + +Measurement (run from the crate root): + +```bash +for f in $(find src -name '*.rs'); do + awk '/^[[:space:]]*#\[cfg\(test\)\]/{exit} !/^[[:space:]]*(\/\/|$)/{c++} END{print c+0}' "$f" +done | paste -sd+ - | bc +``` + +Tests, benches, and fuzz targets are uncounted and unlimited. + +## Trait system (first principles) + +**15 public traits** (baseline: 22; pre-consolidation: 46). Two backends, one +spine; every merge below is justified by "same implementor set or a strict +capability subset with defaulted members". + +### Spine (unconditional, 7) + +| Trait | Replaces | Contents | +|---|---|---| +| `AdditiveGroup` | same | `Zero` + add/sub/neg (owned + by-ref) + `Copy/Send/Sync` | +| `Ring` | `RingCore` + `FromPrimitiveInt` | ring ops, `square`/`pow2`, **integer embedding** (`from_u64/i64/u128/i128` required; small widths + `mul_*`/`mul_pow_2` defaulted). Rationale: every unital ring embeds ℤ; keeping embedding separate bought nothing and doubled bounds everywhere | +| `Field` | `FieldCore` + `HalvingField` | `inverse`, `inv_or_zero`, `random`, and defaulted `half`/`two_inv` (fast impls override). Rationale: char ≠ 2 always, so halving is field-generic with a default | +| `CanonicalEncoding` | `CanonicalRepr` + `CanonicalField` | one canonicity surface: `NUM_BYTES`, `MODULUS_BITS` (bit length of \|F\|), `to_bytes_le`, `from_bytes_le_reduced`, `from_bytes_le_checked`, `to_u128_checked`, `from_u128_checked/_reduced`, `num_bits`, defaulted challenge derivation. Transcript bytes are specified here; wire serde reuses `from_bytes_le_checked` so canonical-rejection is uniform | +| `Accumulator` | same | `add`/`merge`/`reduce`/`fmadd` + defaulted small-scalar fmadds | +| `WithAccumulator` | same (bound: `Ring`) | associated `Accumulator` | +| `JoltField` | `Field` umbrella | **blanket-implemented** marker: `Field + CanonicalEncoding + WithAccumulator + Serialize + DeserializeOwned`. Blanket impl means it can never be forgotten; serde in the umbrella because every proof-system field needs a wire format | + +### Solinas (feature-gated, 8) + +| Trait | Replaces | Contents | +|---|---|---| +| `PseudoMersenne` | `PseudoMersenneField` + `ExtMulBackend` | `const OFFSET: u128` (bits live on `CanonicalEncoding`) + the degree-4/8 ext-mul kernel hooks with generic coefficient-formula defaults (only `Fp32` overrides, fusing i64 accumulation) | +| `ExtField` | same | degree, `lift_base`, `mul_base`, coeff access, Frobenius | +| `Ext2Config` | `FpExt2Config` | quadratic non-residue config (ZST pattern), `IS_NEG_ONE` fast path | +| `MulBaseUnreduced` | same | tiny overridable ext×base deferred multiply | +| `Unreduced` | `HasUnreducedOps` + `HasWide` + `ReduceTo` | **one deferred-reduction companion surface**: `type Product`, `type SmallProduct`, `type Wide` (i32-lane), `SUM_IS_EXACT`, widening muls + `reduce_*` for each, `scale_wide`. Rationale: these were three fragments of one concept — "the unreduced value algebra around a field"; routing reduction through the field type kills `ReduceTo`'s ambiguity workarounds | +| `Fold` | `HasOptimizedFold` | `precompute(r) -> Ctx`, `fold_one(ctx, even, odd)` — documented honestly as the multilinear bind `even + r·(odd − even)`, a protocol-support hook that lives here because implementations exploit field representation | +| `Packed` | `PackedField` | lanes: `Scalar`, `WIDTH`, `from_fn`/`extract`/`broadcast` + defaulted slice helpers + packed ext2 kernel hook | +| `WithPacking` | `HasPacking` | associated `Packing` | + +**Deleted outright:** `MontgomeryConstants` (approved), the `akita` bootstrap +(approved), `CanonicalField`, `HalvingField`, `ExtMulBackend`, `HasWide`, +`HasUnreducedOps`, `ReduceTo`, `FromPrimitiveInt` (all merged as above). + +**Exported stamping macros** (`ops.rs`): `impl_ring_ops!` (full operator +matrix + `Zero`/`One`/`Sum`/`Product` from raw add/sub/mul/neg), +`impl_serde_bytes!` (canonical-checked serde over `CanonicalEncoding`, byte-format +identical to baseline). Exported so third-party field implementors pay the +same near-zero boilerplate we do — the `mersenne61`-style compat test consumes +them as a third party would. + +## Scope + +**Parity (functionality, not names):** everything jolt-field @ baseline does — +BN254 `Fr`/`Fq`/`WideAccumulator`; Solinas `Fp32`/`Fp64`/`Fp128` + 12 +registered prime offsets; `FpExt2/4/8` + Frobenius/Moore machinery; packed +NEON/AVX2/AVX-512 × {32,64,128} + packed ext + `NoPacking`; lane accumulators +and fold matrices; `S64`–`S256` + hi32 variants; `Limbs`; rayon helpers; +`allocative` derives. Features: `default = ["bn254"]`, `solinas`, `parallel`, +`allocative`. + +**Byte compatibility (hard invariants):** +- BN254 serde bytes and Fiat-Shamir transcript bytes identical to jolt-field. +- Solinas serde+bincode wire bytes and `CanonicalEncoding` transcript bytes identical + to jolt-field (replacement must not change proof bytes). + +**Dropped (approved):** `akita` bootstrap feature/module, `MontgomeryConstants`. + +## Design pillars + +1. **Const-generic scalar core**: `Fp64` etc., fold constants + derived at monomorphization, `C(C+1) < P` const-asserted — in exactly one + place per layer. +2. **`macro_rules!` only — no proc macros.** We control every type; nothing + needs to parse Rust. No `jolt-field-derive` (decision recorded; revisit + only if the operator matrices become unmanageable). +3. **One fold-algebra source of truth per axis of variation:** widths — one + `define_solinas_prime!` body stamps `Fp32`/`Fp64` (fp128 is genuinely + two-limb, hand-written); ISAs — one packed engine macro stamped per + (width × ISA) over per-ISA primitive vocabularies (`simd.rs`); kernels that + differ *algorithmically* (AVX2's missing 64-bit widening mul) live in the + primitive layer. +4. **Differential testing against jolt-field as the oracle** (dev-dependency): + random-op equivalence per type, byte-equality for serde and transcript + encodings, packed-vs-scalar, num-bigint as independent third oracle. +5. **Workspace member from day one** (approved): inherits lints, dep versions, + and the arkworks `[patch.crates-io]` fork. Every checkpoint leaves the + workspace green. + +## Backend architecture + +The crate is two layers with a one-way dependency: + +1. **Contract layer** (crate root, unconditional): every trait the crate + defines, the stamping macros, and the backend-neutral value types + (`Limbs`, `signed`). Contract files contain **trait definitions only — no + backend impls, no arithmetic**. The full capability surface of the crate + is readable from the root regardless of which features are enabled. +2. **Backend layer** (feature-gated modules): implementations of the + contracts. `bn254/` adapts an external implementation (arkworks) plus + first-party Barrett/Montgomery kernels; `solinas/` is a fully first-party + implementation. A backend module may be deleted (or a new one added) + without touching the contract layer. + +Rules: contract files never reference backend modules; backends never +reference each other; a backend's public surface is exactly its `impl + for ` items plus its concrete types — enumerable by +grepping the module. Adding a backend = implement the spine (JoltField, +serde, and the conformance-law tests come free via the blanket impl and +exported macros — the Mersenne-61 spine test demonstrates the full recipe); +optionally implement the capability contracts (`PseudoMersenne`, extensions, +`Unreduced`, `Fold`, `Packed`) to light up the generic machinery that bounds +on them. + +## File structure and budgets + +| File | Budget | Contents | +|---|---|---| +| **Contract layer (root, unconditional)** | | | +| `src/lib.rs` | 70 | crate docs, feature gates, re-exports, `FieldError` | +| `src/algebra.rs` | 260 | spine: 7 traits + `NaiveAccumulator` + `PseudoMersenne` | +| `src/extension.rs` | 60 | contracts: `ExtField`, `Ext2Config`, `MulBaseUnreduced` | +| `src/unreduced.rs` | 70 | contracts: `Unreduced`, `Fold` | +| `src/packed.rs` | 90 | contracts: `Packed`, `WithPacking` + generic `NoPacking` | +| `src/ops.rs` | 180 | `impl_ring_ops!`, `impl_serde_bytes!` (backend-neutral) | +| `src/limbs.rs` | 220 | `Limbs` | +| `src/signed.rs` | 420 | signed bigint families (consumer-audited surface) | +| **bn254 backend** | | | +| `src/bn254/mod.rs` | 400 | `Fr`, `Fq` via one wrapping macro; serde; transcript bytes | +| `src/bn254/mont.rs` | 300 | Barrett/Montgomery kernel + `WideAccumulator` | +| **solinas backend** | | | +| `src/solinas/mod.rs` | 90 | offset registry, aliases, shared helpers | +| `src/solinas/word.rs` | 380 | `define_solinas_prime!` → `Fp32`, `Fp64` | +| `src/solinas/fp128.rs` | 700 | two-limb add/sub/mul/reduce/wide | +| `src/solinas/ext.rs` | 890 | FpExt2/4/8 impls, schedules, Frobenius + Moore | +| `src/solinas/unreduced.rs` | 530 | lane accumulators, fold matrices, contract impls | +| `src/solinas/parallel.rs` | 80 | rayon helpers | +| `src/solinas/packed/mod.rs` | 30 | backend selection | +| `src/solinas/packed/simd.rs` | 350 | per-ISA primitive vocabulary (neon/avx2/avx512) | +| `src/solinas/packed/engine.rs` | 550 | shared packed algebra, stamped per width × ISA | +| `src/solinas/packed/fp128.rs` | 350 | 128-bit-lane engine | +| `src/solinas/packed/ext.rs` | 230 | packed FpExt2/4/8 | +| **Total** | **6,240** | vs 11,410 baseline (−45%) | + +Component budgets are unchanged — the contract/impl split carves each +component's contracts out of its old single-file budget (extensions 950 = +60 + 890, unreduced 600 = 70 + 530, packed selection 120 = 90 + 30). + +Baseline per component (same metric): packed 3,720 → 1,600 · prime 2,140 → +1,170 · ext 1,661 → 950 · arkworks 1,170 → 700 · unreduced 1,073 → 600 · +signed 1,004 → 420 · limbs 246 → 220 · spine+glue ~400 → 430 · parallel 78 → 80. + +**Budget discipline:** if a component lands ≤10% over after honest +compression, we discuss trade rather than golf. Riskiest: `packed/` and `ext.rs`. + +## Build order (one review checkpoint each) + +1. **Scaffold + spine** — crate, workspace membership, `algebra.rs`, `ops.rs`, + `lib.rs`. Accept: compiles no-default-features; a Mersenne-61 toy field in + `tests/` implements the full spine through the exported macros (third-party + implementability, no arkworks); law suite green. +2. **BN254** — early end-to-end validation of the spine with the field Jolt + actually runs. Accept: differential vs jolt-field `Fr`/`Fq` (ops, serde + bytes, transcript bytes), `WideAccumulator` exactness. +3. **Limbs + signed** — accept: differential + boundary tests. +4. **Solinas words** — `word.rs` + registry. Accept: differential vs + jolt-field `Fp32`/`Fp64` across all registered ≤64-bit offsets; num-bigint + oracle; serde byte-equality. +5. **Fp128** — same acceptance for 128-bit offsets. +6. **Extensions** — accept: differential + schoolbook oracle + Frobenius/Moore + parity; bench the generic-default deg-4 kernel vs jolt-field's `Fp32` + override before deciding to keep the override. +7. **Unreduced** — accept: accumulator-exactness vs direct mul, fold parity. +8. **Packed** — accept: packed-vs-scalar on native ISA; `cargo check` with + `-Ctarget-feature` for the other ISAs; NEON run on this machine (aarch64). +9. **Parallel + assembly** — final LOC audit, feature-matrix build, crate docs. + +## Non-goals + +- Replacing `jolt-field` in consumers (separate PR once accepted). +- Porting the Criterion bench suite (thin comparison bench later; uncounted). +- CI wiring (follows at replacement; will include a target-feature lane so + SIMD is not CI-dark — fixing a baseline gap). + +## Resolved questions + +1. Counting rules — **approved**. +2. Drop `akita` bootstrap + `MontgomeryConstants` — **approved** (bn254 + + solinas are the two supported stacks). +3. Trait redesign — **approved and widened**: full first-principles redesign, + breaking old names is fine (see Trait system). +4. Workspace membership from day one — **approved**. +5. Budgets/order — **approved**. diff --git a/crates/jolt-field-two/src/algebra.rs b/crates/jolt-field-two/src/algebra.rs new file mode 100644 index 0000000000..c8fd1969f1 --- /dev/null +++ b/crates/jolt-field-two/src/algebra.rs @@ -0,0 +1,395 @@ +//! The trait spine: the algebraic ladder, the canonical (transcript) +//! representation, and deferred-reduction accumulators. +//! +//! ```text +//! AdditiveGroup -> Ring -> Field +//! ``` +//! +//! [`CanonicalEncoding`] and [`WithAccumulator`] are orthogonal capabilities; +//! [`JoltField`] is the blanket-implemented bundle of everything Jolt's +//! protocol stack requires of a scalar field. + +use num_traits::{One, Zero}; +use rand_core::RngCore; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt::{Debug, Display}; +use std::hash::Hash; +use std::iter::{Product, Sum}; +use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +/// Minimal additive group shared by fields, rings, and wide accumulators. +pub trait AdditiveGroup: + Sized + + Clone + + Copy + + Send + + Sync + + Zero + + Add + + for<'a> Add<&'a Self, Output = Self> + + AddAssign + + Sub + + for<'a> Sub<&'a Self, Output = Self> + + SubAssign + + Neg +{ +} + +/// Unital ring: additive group plus multiplication, one, and the integer +/// embedding. +/// +/// The embedding lives here rather than on a separate trait because every +/// unital ring embeds the integers; only the four widest conversions are +/// required, everything else is defaulted on top of them. +pub trait Ring: + AdditiveGroup + + One + + PartialEq + + Eq + + Default + + Debug + + Display + + Hash + + Mul + + for<'a> Mul<&'a Self, Output = Self> + + MulAssign + + Sum + + for<'a> Sum<&'a Self> + + Product + + for<'a> Product<&'a Self> +{ + fn from_u64(v: u64) -> Self; + fn from_i64(v: i64) -> Self; + fn from_u128(v: u128) -> Self; + fn from_i128(v: i128) -> Self; + + #[inline] + fn from_bool(v: bool) -> Self { + Self::from_u64(v as u64) + } + + #[inline] + fn from_u8(v: u8) -> Self { + Self::from_u64(v as u64) + } + + #[inline] + fn from_i8(v: i8) -> Self { + Self::from_i64(v as i64) + } + + #[inline] + fn from_u16(v: u16) -> Self { + Self::from_u64(v as u64) + } + + #[inline] + fn from_i16(v: i16) -> Self { + Self::from_i64(v as i64) + } + + #[inline] + fn from_u32(v: u32) -> Self { + Self::from_u64(v as u64) + } + + #[inline] + fn from_i32(v: i32) -> Self { + Self::from_i64(v as i64) + } + + /// Returns `self * self`. + #[inline] + fn square(&self) -> Self { + *self * *self + } + + /// Returns the ring element `2^exponent`. + #[inline] + fn pow2(exponent: usize) -> Self { + let mut result = Self::one(); + let mut base = Self::one() + Self::one(); + let mut remaining = exponent; + while remaining > 0 { + if remaining % 2 == 1 { + result *= base; + } + remaining /= 2; + if remaining > 0 { + base = base.square(); + } + } + result + } + + /// Multiplies by a `u64`. + #[inline(always)] + fn mul_u64(&self, n: u64) -> Self { + *self * Self::from_u64(n) + } + + /// Multiplies by an `i64`. + #[inline(always)] + fn mul_i64(&self, n: i64) -> Self { + *self * Self::from_i64(n) + } + + /// Multiplies by a `u128`. + #[inline(always)] + fn mul_u128(&self, n: u128) -> Self { + *self * Self::from_u128(n) + } + + /// Multiplies by an `i128`. + #[inline(always)] + fn mul_i128(&self, n: i128) -> Self { + *self * Self::from_i128(n) + } + + /// Multiplies this ring element by the integer `2^pow`. + #[inline] + fn mul_pow_2(&self, pow: usize) -> Self { + assert!(pow <= 255, "pow > 255"); + let mut res = *self; + let mut p = pow; + while p >= 64 { + res *= Self::from_u64(1 << 63); + p -= 63; + } + res * Self::from_u64(1 << p) + } +} + +/// Algebraic field: ring arithmetic plus inversion, sampling, and halving. +pub trait Field: Ring { + /// Multiplicative inverse, or `None` for the zero element. + fn inverse(&self) -> Option; + + /// Multiplicative inverse with zero mapped to zero. + #[inline] + fn inv_or_zero(self) -> Self { + self.inverse().unwrap_or_else(Self::zero) + } + + /// Samples a random element (RNG-backed, for tests and witnesses). + fn random(rng: &mut R) -> Self; + + /// The multiplicative inverse of two. + /// + /// Defaulted via [`inverse`](Self::inverse); fields with a cheap shift + /// implementation override [`half`](Self::half) and this together. + #[inline] + #[expect(clippy::expect_used, reason = "characteristic two is unsupported")] + fn two_inv() -> Self { + Self::from_u64(2) + .inverse() + .expect("field has characteristic two") + } + + /// Divides this element by two. + #[inline] + fn half(self) -> Self { + self * Self::two_inv() + } +} + +/// Metadata contract for a pseudo-Mersenne field `p = 2^k − c`. +/// +/// The exponent `k` is [`CanonicalEncoding::MODULUS_BITS`]; implementing +/// this contract lights up the generic machinery bounded on it (extension +/// towers, packed backends). +pub trait PseudoMersenne: Field + CanonicalEncoding { + /// Offset `c` in `2^k − c`. + const OFFSET: u128; +} + +/// Canonical little-endian representation: the Fiat-Shamir transcript surface +/// and the single source of canonicity for wire serialization. +/// +/// Transcript absorption and challenge derivation use these explicit +/// encodings so the hashed byte stream is specified independently of any +/// serialization library. Proof/wire serialization goes through serde + +/// bincode, reusing [`from_bytes_le_checked`](Self::from_bytes_le_checked) +/// so non-canonical encodings are rejected uniformly. +/// +/// # Invariants +/// +/// - The encoding is injective on canonical representatives: equal elements +/// produce equal bytes, distinct elements produce distinct bytes. +/// - [`to_bytes_le`](Self::to_bytes_le) always writes exactly +/// [`NUM_BYTES`](Self::NUM_BYTES) bytes of the unique representative. +pub trait CanonicalEncoding: + Sized + Copy + Default + PartialEq + Eq + Debug + Hash + Send + Sync + 'static +{ + /// Byte length of the fixed-size canonical encoding. + const NUM_BYTES: usize; + + /// Bit length of the field order `|F|` (for prime fields, the modulus). + const MODULUS_BITS: u32; + + /// Writes the canonical little-endian encoding into `out`. + fn to_bytes_le(&self, out: &mut [u8]); + + /// Returns the canonical little-endian encoding as a vector. + #[inline] + fn to_bytes_le_vec(&self) -> Vec { + let mut out = vec![0u8; Self::NUM_BYTES]; + self.to_bytes_le(&mut out); + out + } + + /// Decodes little-endian bytes of any length by reducing into the field. + fn from_bytes_le_reduced(bytes: &[u8]) -> Self; + + /// Decodes exactly [`NUM_BYTES`](Self::NUM_BYTES) canonical bytes; + /// `None` on wrong length or a non-canonical value. + fn from_bytes_le_checked(bytes: &[u8]) -> Option; + + /// Returns the canonical representative if it fits in a `u128`. + /// + /// For extension fields: the constant coefficient, when all higher + /// coefficients are zero. + fn to_u128_checked(&self) -> Option; + + /// Returns the canonical representative if it fits in a `u64`. + #[inline] + fn to_u64_checked(&self) -> Option { + self.to_u128_checked().and_then(|v| u64::try_from(v).ok()) + } + + /// Constructs an element when `v` is a canonical representative. + fn from_u128_checked(v: u128) -> Option; + + /// Constructs an element by reducing `v` modulo the field order. + fn from_u128_reduced(v: u128) -> Self; + + /// Number of significant bits in this element's canonical representative. + /// + /// Zero is considered to have zero significant bits. + fn num_bits(&self) -> u32; + + /// Constructs a Fiat-Shamir challenge from squeezed transcript bytes. + #[inline] + fn from_challenge_bytes(bytes: &[u8]) -> Self { + Self::from_bytes_le_reduced(bytes) + } + + /// Constructs a non-optimized scalar challenge from transcript bytes. + #[inline] + fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { + Self::from_challenge_bytes(bytes) + } +} + +/// Accumulates sums and products with potentially deferred modular reduction. +/// +/// The hot-loop pattern `acc += a * b` repeated hundreds of times per output +/// slot dominates the CPU prover. Implementations for specific fields can +/// accumulate unreduced wide products and reduce once at the end. +/// +/// # Invariants +/// +/// - [`fmadd`](Self::fmadd) must be equivalent to `acc += a * b` in the field. +/// - [`merge`](Self::merge) must be equivalent to adding another +/// accumulator's partial result (used for parallel reduction). +/// - [`reduce`](Self::reduce) must return the element equal to the +/// accumulated sum of products. +pub trait Accumulator: Default + Copy + Send + Sync { + /// The element type this accumulator reduces to. + type Element: Ring; + + /// Adds one element into the accumulator. + fn add(&mut self, value: Self::Element); + + /// Merges another accumulator's partial sum into this one. + fn merge(&mut self, other: Self); + + /// Finalizes: reduces the accumulated value to an element. + fn reduce(self) -> Self::Element; + + /// Fused multiply-add: `self += a * b` without intermediate reduction. + fn fmadd(&mut self, a: Self::Element, b: Self::Element); + + /// Fused multiply-add with a `u8` scalar: `self += a * F::from(b)`. + #[inline] + fn fmadd_u8(&mut self, a: Self::Element, b: u8) { + self.fmadd(a, Self::Element::from_u8(b)); + } + + /// Fused multiply-add with a `u64` scalar: `self += a * F::from(b)`. + #[inline] + fn fmadd_u64(&mut self, a: Self::Element, b: u64) { + self.fmadd(a, Self::Element::from_u64(b)); + } + + /// Fused multiply-add with an `i64` scalar: `self += a * F::from(b)`. + #[inline] + fn fmadd_i64(&mut self, a: Self::Element, b: i64) { + self.fmadd(a, Self::Element::from_i64(b)); + } + + /// Fused multiply-add with a `bool` scalar: `self += a` when `b` is true. + #[inline] + fn fmadd_bool(&mut self, a: Self::Element, b: bool) { + if b { + self.add(a); + } + } +} + +/// Associates a deferred-reduction accumulator with an element type. +pub trait WithAccumulator: Ring { + /// Accumulator type. + type Accumulator: Accumulator; +} + +/// Fallback accumulator using standard ring arithmetic: every +/// [`fmadd`](Accumulator::fmadd) performs a full multiply and add. +#[derive(Clone, Copy)] +pub struct NaiveAccumulator(R); + +impl Default for NaiveAccumulator { + #[inline] + fn default() -> Self { + Self(R::zero()) + } +} + +impl Accumulator for NaiveAccumulator { + type Element = R; + + #[inline] + fn add(&mut self, value: R) { + self.0 += value; + } + + #[inline] + fn merge(&mut self, other: Self) { + self.0 += other.0; + } + + #[inline] + fn reduce(self) -> R { + self.0 + } + + #[inline] + fn fmadd(&mut self, a: R, b: R) { + self.0 += a * b; + } +} + +/// Everything Jolt's protocol stack requires of a scalar field: field +/// algebra, a canonical transcript encoding, an accumulator, and a serde +/// wire format. +/// +/// Blanket-implemented — implement the component traits and this follows. +pub trait JoltField: + Field + CanonicalEncoding + WithAccumulator + Serialize + DeserializeOwned +{ +} + +impl JoltField + for T +{ +} diff --git a/crates/jolt-field-two/src/bn254/mod.rs b/crates/jolt-field-two/src/bn254/mod.rs new file mode 100644 index 0000000000..4b2b11cecd --- /dev/null +++ b/crates/jolt-field-two/src/bn254/mod.rs @@ -0,0 +1,342 @@ +//! BN254 backend: `#[repr(transparent)]` newtypes over arkworks decoupling +//! the public API from the arkworks types. +//! +//! Byte formats are frozen: serde and transcript encodings are the 32-byte +//! little-endian canonical form (identical to jolt-field), and challenge +//! derivation reproduces the legacy 125-bit shifted / big-endian-scalar +//! conventions exactly. + +mod mont; + +pub use mont::WideAccumulator; + +use crate::{CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; +use ark_ff::{BigInteger, PrimeField, UniformRand}; +use rand_core::RngCore; + +/// Stamps a BN254 field wrapper: operators, conversions, serde (canonical +/// 32-byte LE), ark-serialize interop, and the canonical-encoding surface. +macro_rules! wrap_bn254 { + ($(#[$doc:meta])* $ty:ident, $inner:ty, $accum:ty, challenge($low:ident, $high:ident): $challenge:expr) => { + $(#[$doc])* + #[derive(Clone, Copy, Default, PartialEq, Eq, Hash)] + #[repr(transparent)] + pub struct $ty(pub(crate) $inner); + + impl $ty { + /// Access the internal Montgomery-form limbs. + #[inline(always)] + pub fn inner_limbs(self) -> [u64; 4] { + (self.0).0 .0 + } + } + + impl From<$inner> for $ty { + #[inline(always)] + fn from(inner: $inner) -> Self { + $ty(inner) + } + } + + impl From<$ty> for $inner { + #[inline(always)] + fn from(wrapper: $ty) -> Self { + wrapper.0 + } + } + + impl std::fmt::Debug for $ty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&self.0, f) + } + } + + impl std::fmt::Display for $ty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } + } + + $crate::impl_ring_ops!(impl[] $ty { + add(a, b): $ty(a.0 + b.0), + sub(a, b): $ty(a.0 - b.0), + mul(a, b): $ty(a.0 * b.0), + neg(a): $ty(-a.0), + zero: $ty(<$inner as ::num_traits::Zero>::zero()), + one: $ty(<$inner as ::num_traits::One>::one()), + }); + + impl std::ops::Div for $ty { + type Output = Self; + #[inline] + fn div(self, rhs: Self) -> Self { + $ty(self.0 / rhs.0) + } + } + + impl<'a> std::ops::Div<&'a $ty> for $ty { + type Output = Self; + #[inline] + fn div(self, rhs: &'a $ty) -> Self { + $ty(self.0 / rhs.0) + } + } + + impl Field for $ty { + #[inline] + fn inverse(&self) -> Option { + <$inner as ark_ff::Field>::inverse(&self.0).map($ty) + } + + #[inline] + fn random(rng: &mut R) -> Self { + $ty(<$inner as UniformRand>::rand(rng)) + } + } + + impl CanonicalEncoding for $ty { + const NUM_BYTES: usize = 32; + const MODULUS_BITS: u32 = 254; + + #[inline] + fn to_bytes_le(&self, out: &mut [u8]) { + assert_eq!(out.len(), ::NUM_BYTES); + use ark_serialize::CanonicalSerialize; + self.0 + .serialize_compressed(out) + .expect("BN254 element serializes to 32 bytes"); + } + + #[inline] + fn from_bytes_le_reduced(bytes: &[u8]) -> Self { + $ty(<$inner>::from_le_bytes_mod_order(bytes)) + } + + #[inline] + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + use ark_serialize::CanonicalDeserialize; + if bytes.len() != ::NUM_BYTES { + return None; + } + <$inner>::deserialize_compressed(bytes).ok().map($ty) + } + + #[inline] + fn to_u128_checked(&self) -> Option { + let bigint = self.0.into_bigint(); + let limbs: &[u64] = bigint.as_ref(); + (limbs[2] == 0 && limbs[3] == 0) + .then(|| ((limbs[1] as u128) << 64) | limbs[0] as u128) + } + + #[inline] + fn from_u128_checked(v: u128) -> Option { + Some(<$ty as Ring>::from_u128(v)) + } + + #[inline] + fn from_u128_reduced(v: u128) -> Self { + <$ty as Ring>::from_u128(v) + } + + #[inline] + fn num_bits(&self) -> u32 { + self.0.into_bigint().num_bits() + } + + /// Legacy convention: a 125-bit masked challenge placed in the two + /// HIGH limbs of a 4-limb integer. The limb interpretation is + /// per-type (`$challenge`) and byte-frozen — Fr routes through the + /// fork's raw `from_bigint_unchecked`, Fq through checked + /// `from_bigint`; the two do NOT produce the same field value. + #[inline] + fn from_challenge_bytes(bytes: &[u8]) -> Self { + let mut buf = [0u8; 16]; + let len = bytes.len().min(buf.len()); + buf[..len].copy_from_slice(&bytes[..len]); + let value = u128::from_le_bytes(buf); + let $low = value as u64; + // Top 3 bits of the high limb are zeroed so the value < p. + let $high = ((value >> 64) as u64) & (u64::MAX >> 3); + let Some(inner) = $challenge else { + unreachable!("masked 125-bit shifted challenge fits in BN254") + }; + $ty(inner) + } + + /// Legacy convention: digest bytes are interpreted as a big-endian + /// integer before reduction. + #[inline] + fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { + let mut buf = bytes.to_vec(); + buf.reverse(); + Self::from_bytes_le_reduced(&buf) + } + } + + $crate::impl_serde_bytes!(impl[] $ty, 32); + + impl ark_serialize::CanonicalSerialize for $ty { + fn serialize_with_mode( + &self, + writer: W, + compress: ark_serialize::Compress, + ) -> Result<(), ark_serialize::SerializationError> { + self.0.serialize_with_mode(writer, compress) + } + + fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { + self.0.serialized_size(compress) + } + } + + impl ark_serialize::Valid for $ty { + fn check(&self) -> Result<(), ark_serialize::SerializationError> { + self.0.check() + } + } + + impl ark_serialize::CanonicalDeserialize for $ty { + fn deserialize_with_mode( + reader: R, + compress: ark_serialize::Compress, + validate: ark_serialize::Validate, + ) -> Result { + <$inner>::deserialize_with_mode(reader, compress, validate).map($ty) + } + } + + impl UniformRand for $ty { + fn rand(rng: &mut R) -> Self { + $ty(<$inner as UniformRand>::rand(rng)) + } + } + + #[cfg(feature = "allocative")] + impl allocative::Allocative for $ty { + fn visit<'a, 'b: 'a>(&self, visitor: &'a mut allocative::Visitor<'b>) { + visitor.visit_simple_sized::(); + } + } + + impl WithAccumulator for $ty { + type Accumulator = $accum; + } + }; +} + +wrap_bn254!( + /// BN254 scalar field element (`#[repr(transparent)]` over `ark_bn254::Fr`). + Fr, + ark_bn254::Fr, + WideAccumulator, + challenge(low, high): ark_bn254::Fr::from_bigint_unchecked(ark_ff::BigInt::new([0, 0, low, high])) +); + +wrap_bn254!( + /// BN254 base field element (`#[repr(transparent)]` over `ark_bn254::Fq`). + Fq, + ark_bn254::Fq, + NaiveAccumulator, + challenge(low, high): ark_bn254::Fq::from_bigint(ark_ff::BigInt::new([0, 0, low, high])) +); + +impl Ring for Fr { + #[inline] + fn from_u64(v: u64) -> Self { + Fr(mont::from_u64(v)) + } + + #[inline] + fn from_i64(v: i64) -> Self { + if v < 0 { + -Fr(mont::from_u64(v.unsigned_abs())) + } else { + Fr(mont::from_u64(v as u64)) + } + } + + #[inline] + fn from_u128(v: u128) -> Self { + Fr(mont::from_u128(v)) + } + + #[inline] + fn from_i128(v: i128) -> Self { + if v < 0 { + -Fr(mont::from_u128(v.unsigned_abs())) + } else { + Fr(mont::from_u128(v as u128)) + } + } + + #[inline] + fn square(&self) -> Self { + Fr(ark_ff::Field::square(&self.0)) + } + + #[inline] + fn mul_u64(&self, n: u64) -> Self { + Fr(mont::mul_u64(self.0, n)) + } + + #[inline(always)] + fn mul_i64(&self, n: i64) -> Self { + let res = self.mul_u64(n.unsigned_abs()); + if n < 0 { + -res + } else { + res + } + } + + #[inline(always)] + fn mul_u128(&self, n: u128) -> Self { + Fr(mont::mul_u128(self.0, n)) + } + + #[inline] + fn mul_i128(&self, n: i128) -> Self { + let res = self.mul_u128(n.unsigned_abs()); + if n < 0 { + -res + } else { + res + } + } +} + +impl Ring for Fq { + #[inline] + fn from_u64(v: u64) -> Self { + Fq(ark_bn254::Fq::from(v)) + } + + #[inline] + fn from_i64(v: i64) -> Self { + if v < 0 { + -Self::from_u64(v.unsigned_abs()) + } else { + Self::from_u64(v as u64) + } + } + + #[inline] + fn from_u128(v: u128) -> Self { + Fq(ark_bn254::Fq::from(v)) + } + + #[inline] + fn from_i128(v: i128) -> Self { + if v < 0 { + -Self::from_u128(v.unsigned_abs()) + } else { + Self::from_u128(v as u128) + } + } + + #[inline] + fn square(&self) -> Self { + Fq(ark_ff::Field::square(&self.0)) + } +} diff --git a/crates/jolt-field-two/src/bn254/mont.rs b/crates/jolt-field-two/src/bn254/mont.rs new file mode 100644 index 0000000000..5a73f222d8 --- /dev/null +++ b/crates/jolt-field-two/src/bn254/mont.rs @@ -0,0 +1,436 @@ +//! BN254 Fr Montgomery/Barrett arithmetic kernel and the wide accumulator. +//! +//! Ported from jolt-field's `arkworks/bn254_ops.rs` + `wide_accumulator.rs` +//! with identical algorithms: Barrett folding for scalar multiplication, +//! a compile-time Montgomery table for small-integer conversion, and the +//! folded 4×4 product accumulator with deferred Montgomery reduction. + +use crate::Accumulator; +use ark_bn254::FrConfig; +use ark_ff::{BigInt, Fp, MontConfig}; +use num_traits::Zero; + +use super::Fr; + +type InnerFr = ark_bn254::Fr; + +const N: usize = 4; +const MODULUS: [u64; N] = >::MODULUS.0; +const INV: u64 = >::INV; +const R: BigInt = >::R; + +const MODULUS_HAS_SPARE_BIT: bool = MODULUS[N - 1] >> 63 == 0; +const MODULUS_NUM_SPARE_BITS: u32 = MODULUS[N - 1].leading_zeros(); + +/// a + b * c + carry → (result, new carry) +#[inline(always)] +fn mac_with_carry(a: u64, b: u64, c: u64, carry: &mut u64) -> u64 { + let tmp = (a as u128) + (b as u128) * (c as u128) + (*carry as u128); + *carry = (tmp >> 64) as u64; + tmp as u64 +} + +/// *a += b + carry → new carry +#[inline(always)] +fn adc(a: &mut u64, b: u64, carry: u64) -> u64 { + let tmp = (*a as u128) + (b as u128) + (carry as u128); + *a = tmp as u64; + (tmp >> 64) as u64 +} + +/// *a -= b + borrow → new borrow (1 if underflow) +#[inline(always)] +fn sbb(a: &mut u64, b: u64, borrow: u64) -> u64 { + let tmp = (1u128 << 64) + (*a as u128) - (b as u128) - (borrow as u128); + *a = tmp as u64; + u64::from(tmp >> 64 == 0) +} + +/// `k * p` for small `k`, as (low N limbs, carry limb). +const fn modulus_times(k: u64) -> ([u64; N], u64) { + let mut lo = [0u64; N]; + let mut carry = 0u64; + let mut i = 0; + while i < N { + let v = (MODULUS[i] as u128) * (k as u128) + carry as u128; + lo[i] = v as u64; + carry = (v >> 64) as u64; + i += 1; + } + (lo, carry) +} + +const MODULUS_TIMES_2: ([u64; N], u64) = modulus_times(2); +const MODULUS_TIMES_3: ([u64; N], u64) = modulus_times(3); + +/// Barrett mu = floor(2^(N*64 + 64 - spare_bits - 1) / MODULUS), computed via +/// normalized Knuth long division. The quotient fits in a single u64. +const BARRETT_MU: u64 = { + let shift = MODULUS_NUM_SPARE_BITS; + let p_hi = if shift > 0 { + (MODULUS[3] << shift) | (MODULUS[2] >> (64 - shift)) + } else { + MODULUS[3] + }; + let p_lo = if shift > 0 { + (MODULUS[2] << shift) | (MODULUS[1] >> (64 - shift)) + } else { + MODULUS[2] + }; + // Normalized dividend top limbs are [1 << 63, 0]. + let dividend_top = (1u128 << 63) << 64; + let mut q = dividend_top / (p_hi as u128); + let mut r = dividend_top - q * (p_hi as u128); + while r < (1u128 << 64) && q * (p_lo as u128) > (r << 64) { + q -= 1; + r += p_hi as u128; + } + q as u64 +}; + +/// `PRECOMP_TABLE[i]` = Montgomery form of `i`, for fast small-int conversion. +const PRECOMP_TABLE_SIZE: usize = 1 << 14; +static PRECOMP_TABLE: [InnerFr; PRECOMP_TABLE_SIZE] = { + let mut table = [Fp::new_unchecked(BigInt([0u64; N])); PRECOMP_TABLE_SIZE]; + let mut i = 1usize; + while i < PRECOMP_TABLE_SIZE { + let mut limbs = [0u64; N]; + limbs[0] = i as u64; + table[i] = Fp::new(BigInt::new(limbs)); + i += 1; + } + table +}; + +/// Compare two 4-limb numbers. +#[inline(always)] +fn compare_4(a: [u64; N], b: [u64; N]) -> core::cmp::Ordering { + let mut i = N; + while i > 0 { + i -= 1; + if a[i] != b[i] { + return if a[i] > b[i] { + core::cmp::Ordering::Greater + } else { + core::cmp::Ordering::Less + }; + } + } + core::cmp::Ordering::Equal +} + +/// a - b for 4-limb numbers. Caller guarantees a >= b. +#[inline(always)] +fn sub_4(a: [u64; N], b: [u64; N]) -> [u64; N] { + let mut result = a; + let mut borrow = 0u64; + borrow = sbb(&mut result[0], b[0], borrow); + borrow = sbb(&mut result[1], b[1], borrow); + borrow = sbb(&mut result[2], b[2], borrow); + let _ = sbb(&mut result[3], b[3], borrow); + result +} + +/// Reduce a 5-limb Barrett intermediate known to be < 4p down to < p. +/// +/// BN254 has two spare bits, so 2p and 3p fit in N limbs and the top +/// intermediate limb is always zero here. +#[inline(always)] +fn barrett_cond_subtract(r_tmp: BigInt<5>) -> BigInt { + let r_n: [u64; N] = [r_tmp.0[0], r_tmp.0[1], r_tmp.0[2], r_tmp.0[3]]; + if compare_4(r_n, MODULUS_TIMES_2.0) != core::cmp::Ordering::Less { + if compare_4(r_n, MODULUS_TIMES_3.0) != core::cmp::Ordering::Less { + BigInt(sub_4(r_n, MODULUS_TIMES_3.0)) + } else { + BigInt(sub_4(r_n, MODULUS_TIMES_2.0)) + } + } else if compare_4(r_n, MODULUS) != core::cmp::Ordering::Less { + BigInt(sub_4(r_n, MODULUS)) + } else { + BigInt(r_n) + } +} + +/// Barrett reduction kernel: reduce 5 limbs → 4 limbs (mod p). +#[inline(always)] +fn barrett_reduce_5_to_4(c: BigInt<5>) -> BigInt { + let tilde_c: u64 = if MODULUS_HAS_SPARE_BIT { + (c.0[N] << MODULUS_NUM_SPARE_BITS) + (c.0[N - 1] >> (64 - MODULUS_NUM_SPARE_BITS)) + } else { + c.0[N] + }; + let m: u64 = ((tilde_c as u128 * BARRETT_MU as u128) >> 64) as u64; + + // r_tmp = c - m * 2p + let (m2p_lo, m2p_hi) = MODULUS_TIMES_2; + let mut m2p = BigInt([m2p_lo[0], m2p_lo[1], m2p_lo[2], m2p_lo[3], m2p_hi]); + let mut carry = 0u64; + for limb in &mut m2p.0 { + let prod = (*limb as u128) * (m as u128) + (carry as u128); + *limb = prod as u64; + carry = (prod >> 64) as u64; + } + let mut r_tmp = c.0; + let mut borrow = 0u64; + for (r, &sub) in r_tmp.iter_mut().zip(m2p.0.iter()) { + borrow = sbb(r, sub, borrow); + } + debug_assert!(borrow == 0, "borrow in Barrett c - m*2p"); + + barrett_cond_subtract(BigInt(r_tmp)) +} + +/// N Montgomery reduction steps on a buffer of L >= 2N limbs; returns the +/// final carry. +#[inline(always)] +fn montgomery_reduce_in_place(limbs: &mut [u64; L]) -> u64 { + let mut carry2 = 0u64; + for i in 0..N { + let tmp = limbs[i].wrapping_mul(INV); + let mut carry = 0u64; + let _ = mac_with_carry(limbs[i], tmp, MODULUS[0], &mut carry); + for j in 1..N { + limbs[i + j] = mac_with_carry(limbs[i + j], tmp, MODULUS[j], &mut carry); + } + carry2 = adc(&mut limbs[i + N], carry, carry2); + } + carry2 +} + +/// Montgomery reduce an L-limb integer (L >= 2N) to a field element. +/// +/// For L > 2N the tail is first folded down via Barrett, then the standard +/// N-step Montgomery REDC runs. +#[inline(always)] +pub(crate) fn from_montgomery_reduce(unreduced: BigInt) -> InnerFr { + debug_assert!(L >= 2 * N, "montgomery_reduce requires L >= 2N"); + let mut buf = unreduced.0; + + if L > 2 * N { + let mut acc = [0u64; N]; + let mut i = L; + while i > N { + i -= 1; + let c5 = BigInt([buf[i], acc[0], acc[1], acc[2], acc[3]]); + acc = barrett_reduce_5_to_4(c5).0; + } + buf[N..2 * N].copy_from_slice(&acc); + for slot in &mut buf[2 * N..L] { + *slot = 0; + } + } + + let carry = montgomery_reduce_in_place(&mut buf); + + let mut result_limbs = [0u64; N]; + result_limbs.copy_from_slice(&buf[N..2 * N]); + let mut result = Fp::new_unchecked(BigInt::(result_limbs)); + + let needs_sub = if MODULUS_HAS_SPARE_BIT { + compare_4(result.0 .0, MODULUS) != core::cmp::Ordering::Less + } else { + carry != 0 || compare_4(result.0 .0, MODULUS) != core::cmp::Ordering::Less + }; + if needs_sub { + result.0 = BigInt(sub_4(result.0 .0, MODULUS)); + } + result +} + +/// Multiply BigInt<4> by u64, producing BigInt<5>. +#[inline(always)] +fn bigint4_mul_u64(a: &BigInt, b: u64) -> BigInt<5> { + let mut res = BigInt::<5>([0u64; 5]); + let mut carry = 0u64; + for i in 0..N { + res.0[i] = mac_with_carry(0, a.0[i], b, &mut carry); + } + res.0[N] = carry; + res +} + +/// Multiply BigInt<4> by u128, producing BigInt<6>. +#[inline(always)] +fn bigint4_mul_u128(a: &BigInt, b: u128) -> BigInt<6> { + let (b_lo, b_hi) = (b as u64, (b >> 64) as u64); + let mut res = BigInt::<6>([0u64; 6]); + let mut carry = 0u64; + for i in 0..N { + res.0[i] = mac_with_carry(res.0[i], a.0[i], b_lo, &mut carry); + } + res.0[N] = carry; + let mut carry2 = 0u64; + for i in 0..N { + res.0[i + 1] = mac_with_carry(res.0[i + 1], a.0[i], b_hi, &mut carry2); + } + res.0[N + 1] = carry2; + res +} + +/// Barrett reduce BigInt<6> → Fr via two rounds. +#[inline(always)] +fn from_unchecked_nplus2(element: BigInt<6>) -> InnerFr { + let c1 = BigInt::<5>([ + element.0[1], + element.0[2], + element.0[3], + element.0[4], + element.0[5], + ]); + let r1 = barrett_reduce_5_to_4(c1); + let c2 = BigInt([element.0[0], r1.0[0], r1.0[1], r1.0[2], r1.0[3]]); + Fp::new_unchecked(barrett_reduce_5_to_4(c2)) +} + +/// Multiply a field element by u64 via one Barrett round. +#[inline(always)] +pub(crate) fn mul_u64(a: InnerFr, b: u64) -> InnerFr { + if b == 0 || Zero::is_zero(&a) { + return InnerFr::zero(); + } + if b == 1 { + return a; + } + Fp::new_unchecked(barrett_reduce_5_to_4(bigint4_mul_u64(&a.0, b))) +} + +/// Multiply a field element by u128 via up to two Barrett rounds. +#[inline(always)] +pub(crate) fn mul_u128(a: InnerFr, b: u128) -> InnerFr { + if b >> 64 == 0 { + mul_u64(a, b as u64) + } else { + from_unchecked_nplus2(bigint4_mul_u128(&a.0, b)) + } +} + +/// Convert u64 → Fr: table lookup for small values, `mul_u64(R, n)` otherwise. +#[inline(always)] +pub(crate) fn from_u64(n: u64) -> InnerFr { + if n < PRECOMP_TABLE_SIZE as u64 { + PRECOMP_TABLE[n as usize] + } else { + mul_u64(Fp::new_unchecked(R), n) + } +} + +/// Convert u128 → Fr: table lookup for small values, `mul_u128(R, n)` otherwise. +#[inline(always)] +pub(crate) fn from_u128(n: u128) -> InnerFr { + if n < PRECOMP_TABLE_SIZE as u128 { + PRECOMP_TABLE[n as usize] + } else { + mul_u128(Fp::new_unchecked(R), n) + } +} + +/// Folded 4×4 product accumulator for BN254 Fr deferred reduction. +/// +/// Stores the running sum of Montgomery-form products in positional `u128` +/// slots; each fmadd defers all carry propagation, and +/// [`Accumulator::reduce`] performs one carry pass plus one Montgomery +/// reduction. The `u128` slots give ~2^63 fmadds of headroom. +#[derive(Clone, Copy)] +pub struct WideAccumulator { + slots: [u128; 8], +} + +impl Default for WideAccumulator { + #[inline] + fn default() -> Self { + Self { slots: [0; 8] } + } +} + +impl WideAccumulator { + /// Carry-propagate the positional slots into a 9-limb integer. + #[inline] + fn normalize(self) -> BigInt<9> { + let mut out = [0u64; 9]; + let mut carry = 0u128; + for (index, slot) in self.slots.into_iter().enumerate() { + let (sum, overflow) = slot.overflowing_add(carry); + out[index] = sum as u64; + carry = (sum >> 64) + ((overflow as u128) << 64); + } + out[8] = carry as u64; + BigInt::new(out) + } +} + +impl Accumulator for WideAccumulator { + type Element = Fr; + + /// WARNING: elements are accumulated as `value * one` so every slot term + /// is a product of two Montgomery forms, matching what `reduce` divides + /// out. Do not add raw limbs directly. + #[inline(always)] + fn add(&mut self, value: Fr) { + self.fmadd(value, ::one()); + } + + #[inline(always)] + fn merge(&mut self, other: Self) { + for (lhs, rhs) in self.slots.iter_mut().zip(other.slots) { + *lhs += rhs; + } + } + + fn reduce(self) -> Fr { + Fr(from_montgomery_reduce(self.normalize())) + } + + #[inline(always)] + fn fmadd(&mut self, a: Fr, b: Fr) { + let (a, b) = (a.inner_limbs(), b.inner_limbs()); + for (i, &ai) in a.iter().enumerate() { + for (j, &bj) in b.iter().enumerate() { + let product = (ai as u128) * (bj as u128); + self.slots[i + j] += (product as u64) as u128; + self.slots[i + j + 1] += ((product >> 64) as u64) as u128; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ark_ff::UniformRand; + use rand::{Rng, SeedableRng}; + + #[test] + fn kernel_matches_arkworks() { + let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(7); + for _ in 0..500 { + let a = InnerFr::rand(&mut rng); + let b: u64 = rng.gen(); + let c: u128 = rng.gen(); + assert_eq!(mul_u64(a, b), a * InnerFr::from(b)); + assert_eq!(mul_u128(a, c), a * InnerFr::from(c)); + assert_eq!(from_u64(b), InnerFr::from(b)); + assert_eq!(from_u128(c), InnerFr::from(c)); + } + let boundary = PRECOMP_TABLE_SIZE as u64; + assert_eq!(from_u64(boundary - 1), InnerFr::from(boundary - 1)); + assert_eq!(from_u64(boundary), InnerFr::from(boundary)); + } + + #[test] + fn montgomery_reduce_roundtrip() { + let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(8); + for _ in 0..200 { + let a = InnerFr::rand(&mut rng); + let b = InnerFr::rand(&mut rng); + let mut prod = BigInt::<8>([0u64; 8]); + for (i, &ai) in a.0 .0.iter().enumerate() { + let mut carry = 0u64; + for (j, &bj) in b.0 .0.iter().enumerate() { + prod.0[i + j] = mac_with_carry(prod.0[i + j], ai, bj, &mut carry); + } + prod.0[i + 4] = carry; + } + assert_eq!(from_montgomery_reduce::<8>(prod), a * b); + } + } +} diff --git a/crates/jolt-field-two/src/lib.rs b/crates/jolt-field-two/src/lib.rs new file mode 100644 index 0000000000..43cd3be4dc --- /dev/null +++ b/crates/jolt-field-two/src/lib.rs @@ -0,0 +1,54 @@ +//! Field and ring abstractions for the Jolt zkVM. +//! +//! A slim algebraic ladder — [`AdditiveGroup`] → [`Ring`] → [`Field`] — with +//! orthogonal capabilities: [`CanonicalEncoding`] (the Fiat-Shamir transcript +//! surface) and [`WithAccumulator`] (deferred-reduction fused multiply-add). +//! [`JoltField`] is the blanket-implemented bundle of everything Jolt's +//! protocol stack requires of a scalar field. +//! +//! Proof/wire serialization is serde + bincode over canonical bytes (see +//! [`impl_serde_bytes!`]); transcript bytes use [`CanonicalEncoding`]'s explicit +//! little-endian encoding and never go through a serialization library. +//! +//! # Backends +//! +//! - `bn254` (default): BN254 `Fr`/`Fq` via arkworks, plus a 9-limb wide +//! accumulator with deferred Montgomery reduction. +//! - `solinas`: 32/64/128-bit pseudo-Mersenne prime fields, extension +//! towers, packed NEON/AVX2/AVX-512 backends, unreduced accumulators. + +mod algebra; +#[cfg(feature = "bn254")] +mod bn254; +mod limbs; +mod ops; +pub mod signed; +#[cfg(feature = "solinas")] +pub mod solinas; + +pub use algebra::{ + Accumulator, AdditiveGroup, CanonicalEncoding, Field, JoltField, NaiveAccumulator, + PseudoMersenne, Ring, WithAccumulator, +}; +#[cfg(feature = "bn254")] +pub use bn254::{Fq, Fr, WideAccumulator}; +pub use limbs::Limbs; +pub use num_traits::{One, Zero}; +#[cfg(feature = "solinas")] +pub use solinas::{ + balanced_digit_lut, is_registered_prime_offset, pseudo_mersenne_modulus, + registered_prime_offset_spec, Fp32, Fp64, Prime24Offset3, Prime30Offset35, Prime31Offset19, + Prime32Offset99, Prime40Offset195, Prime48Offset59, Prime56Offset27, Prime64Offset59, + PrimeOffsetSpec, PRIME_OFFSET_IMPLEMENTED_MAX_BITS, PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, +}; + +/// Backend-independent input and shape failures. +#[derive(Debug, thiserror::Error)] +pub enum FieldError { + /// Invalid input parameter or value. + #[error("invalid input: {0}")] + InvalidInput(String), + /// Length mismatch between an expected and provided shape. + #[error("invalid size: expected {expected}, actual {actual}")] + InvalidSize { expected: usize, actual: usize }, +} diff --git a/crates/jolt-field-two/src/limbs.rs b/crates/jolt-field-two/src/limbs.rs new file mode 100644 index 0000000000..81d030a515 --- /dev/null +++ b/crates/jolt-field-two/src/limbs.rs @@ -0,0 +1,291 @@ +//! Fixed-width limb array for multi-precision arithmetic. +//! +//! [`Limbs`] is a `#[repr(transparent)]` newtype over `[u64; N]`. +//! All truncated arithmetic lives here as inherent methods. + +use core::cmp::Ordering; + +/// Fixed-width array of `N` 64-bit limbs in little-endian order. +/// +/// Used as the magnitude type for [`SignedBigInt`](crate::signed::SignedBigInt) +/// and as the output of truncated multiplication in unreduced arithmetic. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct Limbs(pub [u64; N]); + +impl Default for Limbs { + #[inline] + fn default() -> Self { + Self::zero() + } +} + +impl Limbs { + #[inline] + pub const fn new(limbs: [u64; N]) -> Self { + Self(limbs) + } + + #[inline] + pub const fn zero() -> Self { + Self([0u64; N]) + } + + #[inline] + pub fn is_zero(&self) -> bool { + self.0.iter().all(|&l| l == 0) + } + + /// Number of significant bits in the value. + #[inline] + pub fn num_bits(&self) -> u32 { + let mut i = N; + while i > 0 { + i -= 1; + if self.0[i] != 0 { + return (i as u32) * 64 + (64 - self.0[i].leading_zeros()); + } + } + 0 + } + + /// Constructs from a single `u64`, placed in the lowest limb. + #[inline] + pub fn from_u64(val: u64) -> Self { + let mut limbs = [0u64; N]; + if N > 0 { + limbs[0] = val; + } + Self(limbs) + } + + /// In-place addition with carry propagation. + /// Returns `true` if the final carry overflowed. + #[inline] + pub fn add_with_carry(&mut self, other: &Self) -> bool { + let mut carry = 0u64; + for i in 0..N { + let sum = (self.0[i] as u128) + (other.0[i] as u128) + (carry as u128); + self.0[i] = sum as u64; + carry = (sum >> 64) as u64; + } + carry != 0 + } + + /// In-place subtraction with borrow propagation. + /// Returns `true` if the final borrow underflowed. + #[inline] + pub fn sub_with_borrow(&mut self, other: &Self) -> bool { + let mut borrow = false; + for i in 0..N { + let (d1, b1) = self.0[i].overflowing_sub(other.0[i]); + let (d2, b2) = d1.overflowing_sub(u64::from(borrow)); + self.0[i] = d2; + borrow = b1 || b2; + } + borrow + } + + /// Truncated multiplication: `self * other`, keeping the low `P` limbs. + #[inline(always)] + pub fn mul_trunc(&self, other: &Limbs) -> Limbs

{ + let mut res = Limbs::

::zero(); + fm_limbs_into::(&self.0, &other.0, &mut res.0); + res + } + + /// Truncated addition: `self + other`, keeping the low `P` limbs. + #[inline] + pub fn add_trunc(&self, other: &Limbs) -> Limbs

{ + let mut acc = Limbs::

::zero(); + let copy_len = if P < N { P } else { N }; + acc.0[..copy_len].copy_from_slice(&self.0[..copy_len]); + acc.add_assign_trunc::(other); + acc + } + + /// Truncated subtraction: `self - other`, keeping the low `P` limbs. + #[inline] + pub fn sub_trunc(&self, other: &Limbs) -> Limbs

{ + let mut acc = Limbs::

::zero(); + let copy_len = if P < N { P } else { N }; + acc.0[..copy_len].copy_from_slice(&self.0[..copy_len]); + acc.sub_assign_trunc::(other); + acc + } + + /// In-place truncated addition: `self += other`, keeping `N` limbs. + #[inline] + pub fn add_assign_trunc(&mut self, other: &Limbs) { + debug_assert!(M <= N, "add_assign_trunc: right operand wider than self"); + let mut carry = 0u64; + for i in 0..N { + let rhs = if i < M { other.0[i] } else { 0 }; + let sum = (self.0[i] as u128) + (rhs as u128) + (carry as u128); + self.0[i] = sum as u64; + carry = (sum >> 64) as u64; + } + } + + /// In-place truncated subtraction: `self -= other`, keeping `N` limbs. + #[inline] + pub fn sub_assign_trunc(&mut self, other: &Limbs) { + debug_assert!(M <= N, "sub_assign_trunc: right operand wider than self"); + let mut borrow = 0u64; + for i in 0..N { + let rhs = if i < M { other.0[i] } else { 0 }; + let diff = (self.0[i] as u128) + .wrapping_sub(rhs as u128) + .wrapping_sub(borrow as u128); + self.0[i] = diff as u64; + borrow = u64::from(diff > u64::MAX as u128); + } + } + + /// Fused multiply-add: `self += a * b`, keeping `N` limbs, with full + /// carry propagation through all higher limbs. + /// + /// Required when accumulating many products to avoid silent overflow at + /// each row's spill position. + #[inline] + pub fn fmadd(&mut self, a: &Limbs, b: &Limbs) { + let i_limit = if A < N { A } else { N }; + for i in 0..i_limit { + let mut carry = 0u64; + let j_limit = if B < (N - i) { B } else { N - i }; + for j in 0..j_limit { + let idx = i + j; + let prod = + (a.0[i] as u128) * (b.0[j] as u128) + (self.0[idx] as u128) + (carry as u128); + self.0[idx] = prod as u64; + carry = (prod >> 64) as u64; + } + let mut k = i + j_limit; + while carry != 0 && k < N { + let sum = (self.0[k] as u128) + (carry as u128); + self.0[k] = sum as u64; + carry = (sum >> 64) as u64; + k += 1; + } + } + } + + /// Multiply and keep only the low `N` limbs (same width as self). + #[inline(always)] + pub fn mul_low(&self, other: &Self) -> Self { + self.mul_trunc::(other) + } + + /// Zero-extend a narrower `Limbs` into `Limbs`. + #[inline] + pub fn zero_extend_from(smaller: &Limbs) -> Limbs { + debug_assert!(M <= N, "cannot zero-extend from a wider source"); + let mut limbs = [0u64; N]; + let copy_len = if M < N { M } else { N }; + limbs[..copy_len].copy_from_slice(&smaller.0[..copy_len]); + Limbs(limbs) + } +} + +impl From for Limbs { + #[inline] + fn from(val: u64) -> Self { + Self::from_u64(val) + } +} + +impl AsRef<[u64]> for Limbs { + #[inline] + fn as_ref(&self) -> &[u64] { + &self.0 + } +} + +impl PartialOrd for Limbs { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Limbs { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + let mut i = N; + while i > 0 { + i -= 1; + match self.0[i].cmp(&other.0[i]) { + Ordering::Equal => {} + ord => return ord, + } + } + Ordering::Equal + } +} + +impl core::fmt::Debug for Limbs { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Limbs([")?; + for (i, limb) in self.0.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{limb:#018x}")?; + } + write!(f, "])") + } +} + +impl core::fmt::Display for Limbs { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut started = false; + for &limb in self.0.iter().rev() { + if started { + write!(f, "{limb:016x}")?; + } else if limb != 0 { + write!(f, "{limb:x}")?; + started = true; + } + } + if !started { + write!(f, "0")?; + } + Ok(()) + } +} + +#[cfg(feature = "allocative")] +impl allocative::Allocative for Limbs { + fn visit<'a, 'b: 'a>(&self, visitor: &'a mut allocative::Visitor<'b>) { + visitor.visit_simple_sized::(); + } +} + +/// Core schoolbook multiplication accumulator: `acc += a * b`, keeping only +/// the low `P` limbs. +#[inline(always)] +fn fm_limbs_into( + a: &[u64; N], + b: &[u64; M], + acc: &mut [u64; P], +) { + for (j, &mul_limb) in b.iter().enumerate() { + if mul_limb == 0 { + continue; + } + let mut carry = 0u64; + for (i, &a_limb) in a.iter().enumerate() { + let idx = j + i; + if idx < P { + let prod = + (a_limb as u128) * (mul_limb as u128) + (acc[idx] as u128) + (carry as u128); + acc[idx] = prod as u64; + carry = (prod >> 64) as u64; + } + } + let next = j + N; + if next < P { + acc[next] = acc[next].wrapping_add(carry); + } + } +} diff --git a/crates/jolt-field-two/src/ops.rs b/crates/jolt-field-two/src/ops.rs new file mode 100644 index 0000000000..907e9c907b --- /dev/null +++ b/crates/jolt-field-two/src/ops.rs @@ -0,0 +1,187 @@ +//! Exported stamping macros. +//! +//! Concrete types provide raw add/sub/mul/neg bodies and the two identities; +//! these macros emit the full operator matrix, iterator sums/products, and +//! canonical-bytes serde. They are exported so third-party field +//! implementors pay the same near-zero boilerplate as the in-crate backends. +//! +//! Generic parameters are passed as a raw token list: `impl[const P: u64]`, +//! `impl[F: Field, C: Config]`, or `impl[]` for concrete types. + +/// Implements the additive operator matrix for a group type: `Add`/`Sub` +/// (owned and by-ref), `AddAssign`/`SubAssign`, `Neg`, `Zero`, and the +/// [`AdditiveGroup`](crate::AdditiveGroup) marker. +/// +/// `is_zero` compares against the zero expression, which is correct for +/// types whose stored representation is canonical. +#[macro_export] +macro_rules! impl_group_ops { + (impl[$($g:tt)*] $ty:ty { + add($aa:ident, $ab:ident): $add:expr, + sub($sa:ident, $sb:ident): $sub:expr, + neg($na:ident): $neg:expr, + zero: $zero:expr $(,)? + }) => { + impl<$($g)*> ::core::ops::Add for $ty { + type Output = Self; + #[inline(always)] + fn add(self, rhs: Self) -> Self { + let ($aa, $ab) = (self, rhs); + $add + } + } + impl<'a, $($g)*> ::core::ops::Add<&'a $ty> for $ty { + type Output = Self; + #[inline(always)] + fn add(self, rhs: &'a $ty) -> Self { + self + *rhs + } + } + impl<$($g)*> ::core::ops::AddAssign for $ty { + #[inline(always)] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } + } + impl<$($g)*> ::core::ops::Sub for $ty { + type Output = Self; + #[inline(always)] + fn sub(self, rhs: Self) -> Self { + let ($sa, $sb) = (self, rhs); + $sub + } + } + impl<'a, $($g)*> ::core::ops::Sub<&'a $ty> for $ty { + type Output = Self; + #[inline(always)] + fn sub(self, rhs: &'a $ty) -> Self { + self - *rhs + } + } + impl<$($g)*> ::core::ops::SubAssign for $ty { + #[inline(always)] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } + } + impl<$($g)*> ::core::ops::Neg for $ty { + type Output = Self; + #[inline(always)] + fn neg(self) -> Self { + let $na = self; + $neg + } + } + impl<$($g)*> ::num_traits::Zero for $ty { + #[inline(always)] + fn zero() -> Self { + $zero + } + #[inline(always)] + fn is_zero(&self) -> bool { + *self == $zero + } + } + impl<$($g)*> $crate::AdditiveGroup for $ty {} + }; +} + +/// Implements the full ring operator matrix: everything in +/// [`impl_group_ops!`] plus `Mul` (owned and by-ref), `MulAssign`, `One`, +/// and iterator `Sum`/`Product` (owned and by-ref). +#[macro_export] +macro_rules! impl_ring_ops { + (impl[$($g:tt)*] $ty:ty { + add($aa:ident, $ab:ident): $add:expr, + sub($sa:ident, $sb:ident): $sub:expr, + mul($ma:ident, $mb:ident): $mul:expr, + neg($na:ident): $neg:expr, + zero: $zero:expr, + one: $one:expr $(,)? + }) => { + $crate::impl_group_ops!(impl[$($g)*] $ty { + add($aa, $ab): $add, + sub($sa, $sb): $sub, + neg($na): $neg, + zero: $zero, + }); + impl<$($g)*> ::core::ops::Mul for $ty { + type Output = Self; + #[inline(always)] + fn mul(self, rhs: Self) -> Self { + let ($ma, $mb) = (self, rhs); + $mul + } + } + impl<'a, $($g)*> ::core::ops::Mul<&'a $ty> for $ty { + type Output = Self; + #[inline(always)] + fn mul(self, rhs: &'a $ty) -> Self { + self * *rhs + } + } + impl<$($g)*> ::core::ops::MulAssign for $ty { + #[inline(always)] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } + } + impl<$($g)*> ::num_traits::One for $ty { + #[inline(always)] + fn one() -> Self { + $one + } + } + impl<$($g)*> ::core::iter::Sum for $ty { + #[inline] + fn sum>(iter: I) -> Self { + iter.fold($zero, |acc, x| acc + x) + } + } + impl<'a, $($g)*> ::core::iter::Sum<&'a $ty> for $ty { + #[inline] + fn sum>(iter: I) -> Self { + iter.fold($zero, |acc, x| acc + *x) + } + } + impl<$($g)*> ::core::iter::Product for $ty { + #[inline] + fn product>(iter: I) -> Self { + iter.fold($one, |acc, x| acc * x) + } + } + impl<'a, $($g)*> ::core::iter::Product<&'a $ty> for $ty { + #[inline] + fn product>(iter: I) -> Self { + iter.fold($one, |acc, x| acc * *x) + } + } + }; +} + +/// Implements canonical-bytes serde for a [`CanonicalEncoding`](crate::CanonicalEncoding) +/// type: serializes the exact `NUM_BYTES` little-endian canonical encoding +/// as a fixed-size byte array (no length prefix under bincode) and rejects +/// non-canonical or wrong-length encodings on deserialize. +/// +/// `$n` must equal the type's `NUM_BYTES` (debug-asserted). +#[macro_export] +macro_rules! impl_serde_bytes { + (impl[$($g:tt)*] $ty:ty, $n:expr) => { + impl<$($g)*> ::serde::Serialize for $ty { + fn serialize(&self, serializer: S) -> Result { + debug_assert_eq!($n, <$ty as $crate::CanonicalEncoding>::NUM_BYTES); + let mut buf = [0u8; $n]; + $crate::CanonicalEncoding::to_bytes_le(self, &mut buf); + <[u8; $n]>::serialize(&buf, serializer) + } + } + impl<'de, $($g)*> ::serde::Deserialize<'de> for $ty { + fn deserialize>(deserializer: D) -> Result { + let buf = <[u8; $n]>::deserialize(deserializer)?; + <$ty as $crate::CanonicalEncoding>::from_bytes_le_checked(&buf) + .ok_or_else(|| ::serde::de::Error::custom("non-canonical field element encoding")) + } + } + }; +} diff --git a/crates/jolt-field-two/src/signed.rs b/crates/jolt-field-two/src/signed.rs new file mode 100644 index 0000000000..06abe43993 --- /dev/null +++ b/crates/jolt-field-two/src/signed.rs @@ -0,0 +1,448 @@ +//! Sign-magnitude big integers. +//! +//! Two families share one sign state machine (stamped by +//! `impl_signed_family!` over normalized magnitude ops): +//! +//! - [`SignedBigInt`]: magnitude `Limbs` (width `N * 64` bits); +//! aliases [`S64`], [`S128`], [`S192`], [`S256`]. +//! - [`SignedBigIntHi32`]: magnitude `[u64; N]` + `u32` tail (width +//! `N * 64 + 32` bits) — 4 bytes smaller than `N + 1` full limbs, which +//! matters when millions are stored in witness polynomials; aliases +//! [`S96`], [`S160`], [`S224`]. +//! +//! The surface is the consumer-used subset of jolt-field's signed API +//! (workspace + Akita audited); unconsumed constructors and truncating +//! combinators were dropped. +//! +//! Zero is not canonicalized: a zero magnitude may carry either sign. +//! Equality and ordering treat `+0` and `-0` as equal. + +use crate::Limbs; +use core::cmp::Ordering; +use num_traits::Zero; + +/// A signed big integer using `Limbs` for magnitude and a sign bit. +#[derive(Clone, Copy, Debug)] +pub struct SignedBigInt { + pub magnitude: Limbs, + pub is_positive: bool, +} + +pub type S64 = SignedBigInt<1>; +pub type S128 = SignedBigInt<2>; +pub type S192 = SignedBigInt<3>; +pub type S256 = SignedBigInt<4>; + +/// Compact signed big integer with a `u32` top limb. +#[derive(Clone, Copy, Debug)] +pub struct SignedBigIntHi32 { + magnitude_lo: [u64; N], + magnitude_hi: u32, + is_positive: bool, +} + +pub type S96 = SignedBigIntHi32<1>; +pub type S160 = SignedBigIntHi32<2>; +pub type S224 = SignedBigIntHi32<3>; + +/// Stamps one binary operator (owned rhs) plus its assign form, delegating +/// to an in-place method. +macro_rules! signed_binop { + ($T:ident, $Op:ident, $method:ident, $OpAssign:ident, $assign_method:ident, $apply:ident) => { + impl core::ops::$Op for $T { + type Output = Self; + #[inline] + fn $method(mut self, rhs: Self) -> Self { + self.$apply(&rhs); + self + } + } + impl core::ops::$OpAssign for $T { + #[inline] + fn $assign_method(&mut self, rhs: Self) { + self.$apply(&rhs); + } + } + }; +} + +/// Stamps the shared sign-magnitude state machine over a family providing +/// normalized magnitude ops (`mag_is_zero`, `mag_cmp`, `mag_add`, `mag_sub`, +/// `mag_mul`), a `zero()` constructor, and an `is_positive` field: the +/// operator matrix, `Neg`, sign-aware `Eq`/`Ord`, `Zero`, `Default`, and +/// `allocative` support. +macro_rules! impl_signed_family { + ($T:ident) => { + impl $T { + #[inline(always)] + fn add_assign_in_place(&mut self, rhs: &Self) { + if self.is_positive == rhs.is_positive { + self.mag_add(rhs); + } else if self.mag_cmp(rhs) != Ordering::Less { + self.mag_sub(rhs); + } else { + let old = core::mem::replace(self, *rhs); + self.mag_sub(&old); + } + } + + #[inline(always)] + fn sub_assign_in_place(&mut self, rhs: &Self) { + self.add_assign_in_place(&rhs.negate()); + } + + #[inline(always)] + fn mul_assign_in_place(&mut self, rhs: &Self) { + self.is_positive = self.is_positive == rhs.is_positive; + self.mag_mul(rhs); + } + + /// Flips this value's sign. + #[inline] + pub fn negate(mut self) -> Self { + self.is_positive = !self.is_positive; + self + } + + /// Returns the sign (`true` = non-negative). + #[inline] + pub const fn sign(&self) -> bool { + self.is_positive + } + } + + signed_binop!($T, Add, add, AddAssign, add_assign, add_assign_in_place); + signed_binop!($T, Sub, sub, SubAssign, sub_assign, sub_assign_in_place); + signed_binop!($T, Mul, mul, MulAssign, mul_assign, mul_assign_in_place); + + impl core::ops::Neg for $T { + type Output = Self; + #[inline] + fn neg(self) -> Self { + self.negate() + } + } + + impl PartialEq for $T { + #[inline] + fn eq(&self, other: &Self) -> bool { + (self.mag_is_zero() && other.mag_is_zero()) + || (self.is_positive == other.is_positive + && self.mag_cmp(other) == Ordering::Equal) + } + } + + impl Eq for $T {} + + impl PartialOrd for $T { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl Ord for $T { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + if self.mag_is_zero() && other.mag_is_zero() { + return Ordering::Equal; + } + match (self.is_positive, other.is_positive) { + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + (positive, _) => { + let ord = self.mag_cmp(other); + if positive { + ord + } else { + ord.reverse() + } + } + } + } + } + + impl Zero for $T { + #[inline] + fn zero() -> Self { + Self::zero() + } + #[inline] + fn is_zero(&self) -> bool { + self.mag_is_zero() + } + } + + impl Default for $T { + #[inline] + fn default() -> Self { + Self::zero() + } + } + + #[cfg(feature = "allocative")] + impl allocative::Allocative for $T { + fn visit<'a, 'b: 'a>(&self, visitor: &'a mut allocative::Visitor<'b>) { + visitor.visit_simple_sized::(); + } + } + }; +} + +impl_signed_family!(SignedBigInt); +impl_signed_family!(SignedBigIntHi32); + +impl SignedBigInt { + #[inline(always)] + fn mag_is_zero(&self) -> bool { + self.magnitude.is_zero() + } + + #[inline(always)] + fn mag_cmp(&self, rhs: &Self) -> Ordering { + self.magnitude.cmp(&rhs.magnitude) + } + + #[inline(always)] + fn mag_add(&mut self, rhs: &Self) { + let _ = self.magnitude.add_with_carry(&rhs.magnitude); + } + + #[inline(always)] + fn mag_sub(&mut self, rhs: &Self) { + let _ = self.magnitude.sub_with_borrow(&rhs.magnitude); + } + + #[inline(always)] + fn mag_mul(&mut self, rhs: &Self) { + self.magnitude = self.magnitude.mul_low(&rhs.magnitude); + } + + #[inline] + pub fn new(limbs: [u64; N], is_positive: bool) -> Self { + Self::from_limbs(Limbs::new(limbs), is_positive) + } + + #[inline] + pub fn from_limbs(magnitude: Limbs, is_positive: bool) -> Self { + Self { + magnitude, + is_positive, + } + } + + #[inline] + pub fn zero() -> Self { + Self::from_limbs(Limbs::zero(), true) + } + + #[inline] + pub fn magnitude_limbs(&self) -> [u64; N] { + self.magnitude.0 + } + + /// Multiplies and truncates the result to `P` limbs. + #[inline] + pub fn mul_trunc( + &self, + rhs: &SignedBigInt, + ) -> SignedBigInt

{ + SignedBigInt::from_limbs( + self.magnitude.mul_trunc::(&rhs.magnitude), + self.is_positive == rhs.is_positive, + ) + } + + #[inline] + pub fn from_u64(value: u64) -> Self { + Self::from_u64_with_sign(value, true) + } + + #[inline] + pub fn from_u64_with_sign(value: u64, is_positive: bool) -> Self { + Self::from_limbs(Limbs::from_u64(value), is_positive) + } + + #[inline] + pub fn from_i64(value: i64) -> Self { + Self::from_u64_with_sign(value.unsigned_abs(), value >= 0) + } + + #[inline] + fn from_u128_with_sign(value: u128, is_positive: bool) -> Self { + debug_assert!(N >= 2, "u128 conversion requires at least 2 limbs"); + let mut limbs = [0u64; N]; + limbs[0] = value as u64; + limbs[1] = (value >> 64) as u64; + Self::new(limbs, is_positive) + } + + #[inline] + pub fn from_u128(value: u128) -> Self { + Self::from_u128_with_sign(value, true) + } + + #[inline] + pub fn from_i128(value: i128) -> Self { + Self::from_u128_with_sign(value.unsigned_abs(), value >= 0) + } +} + +impl S64 { + #[inline] + pub fn to_i128(&self) -> i128 { + let magnitude = self.magnitude.0[0] as i128; + if self.is_positive { + magnitude + } else { + -magnitude + } + } + + #[inline] + pub fn magnitude_as_u64(&self) -> u64 { + self.magnitude.0[0] + } +} + +impl S128 { + /// Returns the value if it fits in `i128` (`i128::MIN` included). + #[inline] + pub fn to_i128(&self) -> Option { + let mag = self.magnitude_as_u128(); + if self.is_positive { + (mag >> 127 == 0).then_some(mag as i128) + } else if mag >> 127 == 0 { + Some(-(mag as i128)) + } else { + (mag == 1 << 127).then_some(i128::MIN) + } + } + + #[inline] + pub fn magnitude_as_u128(&self) -> u128 { + (self.magnitude.0[1] as u128) << 64 | (self.magnitude.0[0] as u128) + } +} + +impl SignedBigIntHi32 { + #[inline(always)] + fn mag_is_zero(&self) -> bool { + self.magnitude_hi == 0 && self.magnitude_lo.iter().all(|&l| l == 0) + } + + #[inline(always)] + fn mag_cmp(&self, rhs: &Self) -> Ordering { + self.magnitude_hi.cmp(&rhs.magnitude_hi).then_with(|| { + self.magnitude_lo + .iter() + .rev() + .cmp(rhs.magnitude_lo.iter().rev()) + }) + } + + #[inline(always)] + fn mag_add(&mut self, rhs: &Self) { + let mut carry: u128 = 0; + for i in 0..N { + let sum = (self.magnitude_lo[i] as u128) + (rhs.magnitude_lo[i] as u128) + carry; + self.magnitude_lo[i] = sum as u64; + carry = sum >> 64; + } + // The u32 tail wraps at width, matching full-limb truncation semantics. + self.magnitude_hi = + ((self.magnitude_hi as u128) + (rhs.magnitude_hi as u128) + carry) as u32; + } + + #[inline(always)] + fn mag_sub(&mut self, rhs: &Self) { + let mut borrow = false; + for i in 0..N { + let (d1, b1) = self.magnitude_lo[i].overflowing_sub(rhs.magnitude_lo[i]); + let (d2, b2) = d1.overflowing_sub(u64::from(borrow)); + self.magnitude_lo[i] = d2; + borrow = b1 || b2; + } + self.magnitude_hi = self + .magnitude_hi + .wrapping_sub(rhs.magnitude_hi) + .wrapping_sub(u32::from(borrow)); + } + + /// General `(N+1)`-limb schoolbook multiply truncated to the type width. + /// + /// Carries are extracted per partial product, so intermediate sums never + /// overflow `u128` on any input (the baseline's unrolled kernels wrap for + /// large second limbs). Constant bounds let LLVM fully unroll this. + #[inline(always)] + fn mag_mul(&mut self, rhs: &Self) { + debug_assert!(2 * (N + 1) <= 16, "N too large for the product buffer"); + let limb = |lo: &[u64; N], hi: u32, i: usize| { + if i < N { + lo[i] as u128 + } else { + hi as u128 + } + }; + let mut prod = [0u64; 16]; + for i in 0..=N { + let a = limb(&self.magnitude_lo, self.magnitude_hi, i); + let mut carry: u128 = 0; + for j in 0..=N { + let p = a * limb(&rhs.magnitude_lo, rhs.magnitude_hi, j) + + (prod[i + j] as u128) + + carry; + prod[i + j] = p as u64; + carry = p >> 64; + } + } + self.magnitude_lo.copy_from_slice(&prod[..N]); + self.magnitude_hi = prod[N] as u32; + } + + #[inline] + pub const fn new(magnitude_lo: [u64; N], magnitude_hi: u32, is_positive: bool) -> Self { + Self { + magnitude_lo, + magnitude_hi, + is_positive, + } + } + + #[inline] + pub const fn zero() -> Self { + Self::new([0; N], 0, true) + } + + #[inline] + pub const fn magnitude_lo(&self) -> &[u64; N] { + &self.magnitude_lo + } + + #[inline] + pub const fn magnitude_hi(&self) -> u32 { + self.magnitude_hi + } + + #[inline] + pub const fn is_positive(&self) -> bool { + self.is_positive + } + + /// Converts into a full-limb `SignedBigInt`; asserts `NPLUS1 == N + 1`. + #[inline] + pub fn to_signed_bigint_nplus1(&self) -> SignedBigInt { + assert!(NPLUS1 == N + 1, "NPLUS1 must be N + 1"); + let mut limbs = [0u64; NPLUS1]; + limbs[..N].copy_from_slice(&self.magnitude_lo); + limbs[N] = self.magnitude_hi as u64; + SignedBigInt::from_limbs(Limbs::new(limbs), self.is_positive) + } +} + +impl From for S160 { + #[inline] + fn from(val: u128) -> Self { + Self::new([val as u64, (val >> 64) as u64], 0, true) + } +} diff --git a/crates/jolt-field-two/src/solinas/mod.rs b/crates/jolt-field-two/src/solinas/mod.rs new file mode 100644 index 0000000000..6d76175328 --- /dev/null +++ b/crates/jolt-field-two/src/solinas/mod.rs @@ -0,0 +1,135 @@ +//! Solinas backend: pseudo-Mersenne prime fields `p = 2^k − c`. +//! +//! `word.rs` stamps the `u32`- and `u64`-backed field types from one fold +//! algebra; this module holds the family trait, the `2^k − offset` registry, +//! and shared helpers. + +mod word; + +pub use word::{Fp32, Fp64}; + +use crate::Ring; + +/// Maximum supported offset in the `2^k − offset` specialization. +pub const PRIME_OFFSET_MAX: u128 = 1 << 16; + +/// Current active bit-size bound for concrete field aliases. +pub const PRIME_OFFSET_IMPLEMENTED_MAX_BITS: u32 = 128; + +/// Metadata describing a registered `2^k − offset` pseudo-Mersenne modulus. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PrimeOffsetSpec { + /// `k` in `2^k − offset`. + pub bits: u32, + /// `offset` in `2^k − offset`. + pub offset: u16, + /// Modulus value. + pub modulus: u128, +} + +/// Compute `2^k − offset` for `k <= 128`. +pub const fn pseudo_mersenne_modulus(bits: u32, offset: u128) -> Option { + if bits == 0 || bits > 128 || offset == 0 { + return None; + } + if bits == 128 { + Some(u128::MAX - (offset - 1)) + } else { + Some((1u128 << bits) - offset) + } +} + +/// `2^k − offset` as the storage word for a registered alias; fails at +/// compile time on invalid parameters. +#[expect( + clippy::panic, + reason = "CTFE-only: all call sites are const registry entries" +)] +const fn pm(bits: u32, offset: u16) -> u128 { + match pseudo_mersenne_modulus(bits, offset as u128) { + Some(m) => m, + None => panic!("invalid pseudo-Mersenne parameters"), + } +} + +const fn spec(bits: u32, offset: u16) -> PrimeOffsetSpec { + PrimeOffsetSpec { + bits, + offset, + modulus: pm(bits, offset), + } +} + +/// `2^k − offset` profiles currently enabled in-code. +pub const PRIME_OFFSET_SPECS: [PrimeOffsetSpec; 9] = [ + spec(24, 3), + spec(30, 35), + spec(31, 19), + spec(32, 99), + spec(40, 195), + spec(48, 59), + spec(56, 27), + spec(64, 59), + spec(128, 275), +]; + +/// Return the registered prime spec for exactly `(bits, offset)`. +pub const fn registered_prime_offset_spec(bits: u32, offset: u128) -> Option { + let mut i = 0; + while i < PRIME_OFFSET_SPECS.len() { + if PRIME_OFFSET_SPECS[i].bits == bits && (PRIME_OFFSET_SPECS[i].offset as u128) == offset { + return Some(PRIME_OFFSET_SPECS[i]); + } + i += 1; + } + None +} + +/// Check whether `(k, offset)` is an explicitly registered `2^k − offset` prime. +pub const fn is_registered_prime_offset(bits: u32, offset: u128) -> bool { + offset <= PRIME_OFFSET_MAX + && bits <= PRIME_OFFSET_IMPLEMENTED_MAX_BITS + && registered_prime_offset_spec(bits, offset).is_some() +} + +/// Prime field for `2^24 - 3`. +pub type Prime24Offset3 = Fp32<{ pm(24, 3) as u32 }>; +/// Prime field for `2^30 - 35`. +pub type Prime30Offset35 = Fp32<{ pm(30, 35) as u32 }>; +/// Prime field for `2^31 - 19`. +pub type Prime31Offset19 = Fp32<{ pm(31, 19) as u32 }>; +/// Prime field for `2^32 - 99`. +pub type Prime32Offset99 = Fp32<{ pm(32, 99) as u32 }>; +/// Prime field for `2^40 - 195`. +pub type Prime40Offset195 = Fp64<{ pm(40, 195) as u64 }>; +/// Prime field for `2^48 - 59`. +pub type Prime48Offset59 = Fp64<{ pm(48, 59) as u64 }>; +/// Prime field for `2^56 - 27`. +pub type Prime56Offset27 = Fp64<{ pm(56, 27) as u64 }>; +/// Prime field for `2^64 - 59`. +pub type Prime64Offset59 = Fp64<{ pm(64, 59) as u64 }>; + +/// Builds the balanced signed-digit table for `1 <= log_basis <= 6`. +pub fn balanced_digit_lut(log_basis: u32) -> [F; 64] { + debug_assert!(log_basis > 0 && log_basis <= 6); + let basis = 1usize << log_basis; + let half_basis = (basis >> 1) as i64; + std::array::from_fn(|i| { + if i < basis { + F::from_i64(i as i64 - half_basis) + } else { + F::zero() + } + }) +} + +/// Horner reduction of arbitrary-length little-endian bytes modulo the field +/// order (the >16-byte path of +/// [`from_bytes_le_reduced`](crate::CanonicalEncoding::from_bytes_le_reduced)). +#[inline(always)] +pub(crate) fn reduce_le_bytes_mod_order(bytes: &[u8]) -> F { + let base = F::from_u64(256); + bytes.iter().rev().fold(F::zero(), |acc, &byte| { + acc * base + F::from_u64(byte as u64) + }) +} diff --git a/crates/jolt-field-two/src/solinas/word.rs b/crates/jolt-field-two/src/solinas/word.rs new file mode 100644 index 0000000000..762619f1be --- /dev/null +++ b/crates/jolt-field-two/src/solinas/word.rs @@ -0,0 +1,443 @@ +//! Single-word pseudo-Mersenne prime fields: one Solinas fold algebra +//! stamped at `u32` ([`Fp32`]) and `u64` ([`Fp64`]) storage. +//! +//! The fold point `k` and offset `c = 2^k − p` are computed at compile time +//! from the const-generic modulus; the `C(C+1) < P` precondition for the +//! fused two-fold-plus-canonicalize reduction is const-asserted in exactly +//! one place. Per-width differences enter only through the `mul`/`random` +//! macro arguments (the `u64` width has a fold-entirely-in-`u64` product +//! path for sub-word primes, with a BMI2 variant on x86-64). + +use crate::PseudoMersenne; +use crate::{CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; +use rand_core::RngCore; + +/// Trial-division primality check, cheap enough for CTFE at u32 scale. +/// (64-bit moduli skip the check: 2^32 const-eval iterations is not viable.) +const fn is_small_prime(n: u64) -> bool { + if n < 2 { + return false; + } + if n.is_multiple_of(2) { + return n == 2; + } + let mut d = 3u64; + while d * d <= n { + if n.is_multiple_of(d) { + return false; + } + d += 2; + } + true +} + +macro_rules! define_solinas_prime { + ( + $(#[$doc:meta])* $name:ident, + word: $word:ty, + from_canonical: $from_canon:ident, + to_canonical: $to_canon:ident, + double: $double:ty, + mul_wide_raw: $mul_wide_raw:ident($raw:ty), + mul($ma:ident, $mb:ident): $mul:expr, + random($rng:ident): $random:expr $(,)? + ) => { + $(#[$doc])* + #[cfg_attr(feature = "allocative", derive(allocative::Allocative))] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] + #[repr(transparent)] + pub struct $name(pub(crate) $word); + + impl $name

{ + /// Fold point: smallest `k` such that `P <= 2^k`. + pub(crate) const BITS: u32 = <$word>::BITS - P.leading_zeros(); + + /// Offset `c = 2^k − P`. Instantiating with a modulus that + /// violates the Solinas preconditions is a compile-time error. + pub const C: $word = { + let c = if Self::BITS == <$word>::BITS { + (0 as $word).wrapping_sub(P) + } else { + ((1 as $word) << Self::BITS) - P + }; + assert!(P != 0, "modulus must be nonzero"); + assert!(P & 1 == 1, "modulus must be odd"); + assert!( + <$word>::BITS > 32 || is_small_prime(P as u64), + "modulus must be prime" + ); + assert!( + (c as u128) * (c as u128 + 1) < P as u128, + "C(C+1) < P required for fused canonicalize" + ); + c + }; + + /// Mask for the low `BITS` bits of a double word. + const MASK: $double = if Self::BITS == <$word>::BITS { + <$word>::MAX as $double + } else { + ((1 as $double) << Self::BITS) - 1 + }; + + const MASK128: u128 = Self::MASK as u128; + + /// Conditional subtract of a folded value down to `[0, P)`. + #[inline(always)] + fn canonicalize_folded(v: $double) -> $word { + if Self::BITS < <$word>::BITS { + let x = v as $word; + x.min(x.wrapping_sub(P)) + } else { + let reduced = v.wrapping_sub(P as $double); + let borrow = reduced >> (<$double>::BITS - 1); + reduced.wrapping_add(borrow.wrapping_neg() & (P as $double)) as $word + } + } + + /// Loop-fold Solinas reduction of an arbitrary double word. + #[inline(always)] + fn reduce_double(x: $double) -> $word { + let mut v = x; + while v >> Self::BITS != 0 { + v = (v & Self::MASK) + (Self::C as $double) * (v >> Self::BITS); + } + Self::canonicalize_folded(v) + } + + /// Loop-fold Solinas reduction of an arbitrary `u128`. + #[inline(always)] + fn reduce_u128(x: u128) -> $word { + let mut v = x; + while v >> Self::BITS != 0 { + v = (v & Self::MASK128) + (Self::C as u128) * (v >> Self::BITS); + } + Self::canonicalize_folded(v as $double) + } + + /// Two-fold Solinas reduction for products `< 2^{2·BITS}`. + #[inline(always)] + fn reduce_product(x: $double) -> $word { + let c = Self::C as $double; + let f1 = (x & Self::MASK) + c * (x >> Self::BITS); + let f2 = (f1 & Self::MASK) + c * (f1 >> Self::BITS); + Self::canonicalize_folded(f2) + } + + #[inline(always)] + fn add_raw(a: $word, b: $word) -> $word { + if Self::BITS < <$word>::BITS { + let s = a.wrapping_add(b); + s.min(s.wrapping_sub(P)) + } else { + // Full-word: fold the carry with 2^k ≡ C, then subtract. + let (s, overflow) = a.overflowing_add(b); + let folded = s.wrapping_add((overflow as $word).wrapping_neg() & Self::C); + folded.min(folded.wrapping_sub(P)) + } + } + + #[inline(always)] + fn sub_raw(a: $word, b: $word) -> $word { + if Self::BITS < <$word>::BITS { + let d = a.wrapping_sub(b); + d.min(d.wrapping_add(P)) + } else { + let (d, underflow) = a.overflowing_sub(b); + d.wrapping_sub((underflow as $word).wrapping_neg() & Self::C) + } + } + + #[inline(always)] + fn mul_raw(a: $word, b: $word) -> $word { + let ($ma, $mb) = (a, b); + $mul + } + + fn pow(self, mut exp: u64) -> Self { + let mut base = self; + let mut acc = ::one(); + while exp > 0 { + if (exp & 1) == 1 { + acc *= base; + } + base = Self(Self::mul_raw(base.0, base.0)); + exp >>= 1; + } + acc + } + + /// Create from a canonical representative in `[0, P)`. + #[inline] + pub fn $from_canon(x: $word) -> Self { + debug_assert!(x < P); + Self(x) + } + + /// Return the canonical representative in `[0, P)`. + #[inline] + pub fn $to_canon(self) -> $word { + self.0 + } + + /// Extract the canonical value. + #[inline(always)] + pub fn to_limbs(self) -> $word { + self.0 + } + + /// Widening multiply to a double word, **no reduction**. + #[inline(always)] + pub fn mul_wide(self, other: Self) -> $double { + (self.0 as $double) * (other.0 as $double) + } + + /// Widening multiply by a raw word operand, **no reduction**. + #[inline(always)] + pub fn $mul_wide_raw(self, other: $raw) -> $double { + (self.0 as $double) * (other as $double) + } + + /// Reduce a double word via Solinas folding to a canonical element. + #[inline(always)] + pub fn solinas_reduce(x: $double) -> Self { + Self(Self::reduce_double(x)) + } + } + + $crate::impl_ring_ops!(impl[const P: $word] $name

{ + add(a, b): $name(Self::add_raw(a.0, b.0)), + sub(a, b): $name(Self::sub_raw(a.0, b.0)), + mul(a, b): $name(Self::mul_raw(a.0, b.0)), + neg(a): $name(Self::sub_raw(0, a.0)), + zero: $name(0), + one: $name((P > 1) as $word), + }); + + impl std::fmt::Display for $name

{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } + } + + impl Ring for $name

{ + #[inline(always)] + fn from_u64(v: u64) -> Self { + Self(Self::reduce_double(v as $double)) + } + + #[inline(always)] + fn from_i64(v: i64) -> Self { + if v >= 0 { + Self::from_u64(v as u64) + } else { + -Self::from_u64(v.unsigned_abs()) + } + } + + #[inline(always)] + fn from_u128(v: u128) -> Self { + Self(Self::reduce_u128(v)) + } + + #[inline(always)] + fn from_i128(v: i128) -> Self { + if v >= 0 { + Self::from_u128(v as u128) + } else { + -Self::from_u128(v.unsigned_abs()) + } + } + + #[inline(always)] + fn square(&self) -> Self { + Self(Self::mul_raw(self.0, self.0)) + } + } + + impl Field for $name

{ + #[inline(always)] + fn inverse(&self) -> Option { + let inv = self.inv_or_zero(); + if num_traits::Zero::is_zero(self) { + None + } else { + Some(inv) + } + } + + /// Fermat inversion with branchless zero-masking. + #[inline(always)] + fn inv_or_zero(self) -> Self { + let candidate = self.pow((P as u64).wrapping_sub(2)); + let nz = ((self.0 | self.0.wrapping_neg()) >> (<$word>::BITS - 1)) & 1; + let mask = (0 as $word).wrapping_sub(nz); + Self(candidate.0 & mask) + } + + #[inline(always)] + fn random($rng: &mut R) -> Self { + $random + } + + #[inline] + fn half(self) -> Self { + let x = self.0 as $double; + Self(((x + (x & 1) * P as $double) >> 1) as $word) + } + + #[inline] + fn two_inv() -> Self { + ::one().half() + } + } + + impl CanonicalEncoding for $name

{ + const NUM_BYTES: usize = (<$word>::BITS / 8) as usize; + const MODULUS_BITS: u32 = Self::BITS; + + #[inline(always)] + fn to_bytes_le(&self, out: &mut [u8]) { + assert_eq!(out.len(), Self::NUM_BYTES); + out.copy_from_slice(&self.0.to_le_bytes()); + } + + #[inline(always)] + fn from_bytes_le_reduced(bytes: &[u8]) -> Self { + if bytes.len() <= 16 { + let mut padded = [0u8; 16]; + padded[..bytes.len()].copy_from_slice(bytes); + return Self::from_u128(u128::from_le_bytes(padded)); + } + $crate::solinas::reduce_le_bytes_mod_order(bytes) + } + + #[inline] + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + let arr: [u8; (<$word>::BITS / 8) as usize] = bytes.try_into().ok()?; + Self::from_u128_checked(<$word>::from_le_bytes(arr) as u128) + } + + #[inline] + fn to_u128_checked(&self) -> Option { + Some(self.0 as u128) + } + + #[inline] + fn from_u128_checked(v: u128) -> Option { + (v < P as u128).then(|| Self(v as $word)) + } + + #[inline] + fn from_u128_reduced(v: u128) -> Self { + Self(Self::reduce_u128(v)) + } + + #[inline] + fn num_bits(&self) -> u32 { + <$word>::BITS - self.0.leading_zeros() + } + } + + $crate::impl_serde_bytes!(impl[const P: $word] $name

, (<$word>::BITS / 8) as usize); + + impl WithAccumulator for $name

{ + type Accumulator = NaiveAccumulator; + } + + impl PseudoMersenne for $name

{ + const OFFSET: u128 = Self::C as u128; + } + }; +} + +define_solinas_prime!( + /// Prime field element for primes `p = 2^k − c` stored as `u32`. + Fp32, + word: u32, + from_canonical: from_canonical_u32, + to_canonical: to_canonical_u32, + double: u64, + mul_wide_raw: mul_wide_u32(u32), + mul(a, b): Self::reduce_product((a as u64) * (b as u64)), + random(rng): Self(Self::reduce_double(rng.next_u64())), +); + +define_solinas_prime!( + /// Prime field element for primes `p = 2^k − c` stored as `u64`. + Fp64, + word: u64, + from_canonical: from_canonical_u64, + to_canonical: to_canonical_u64, + double: u128, + mul_wide_raw: mul_wide_u64(u64), + mul(a, b): { + if fp64_folds_in_word(P) { + fp64_mul_fast::

(a, b) + } else { + Self::reduce_product((a as u128) * (b as u128)) + } + }, + random(rng): { + let lo = rng.next_u64() as u128; + let hi = rng.next_u64() as u128; + Self(Self::reduce_u128(lo | (hi << 64))) + }, +); + +/// Whether the two-fold product reduction stays entirely in `u64` for the +/// modulus `p`: sub-word prime with `C · 2^BITS < 2^64`. +#[inline(always)] +const fn fp64_folds_in_word(p: u64) -> bool { + let bits = 64 - p.leading_zeros(); + bits < 64 && (((1u64 << bits) - p) as u128) < (1u128 << (64 - bits)) +} + +/// `a * b` widening to 128 bits; returns `(lo, hi)`. +#[inline(always)] +fn mul64_wide(a: u64, b: u64) -> (u64, u64) { + #[cfg(all(target_arch = "x86_64", target_feature = "bmi2"))] + { + let mut hi = 0; + // SAFETY: the BMI2 intrinsic is gated by its required target feature. + let lo = unsafe { std::arch::x86_64::_mulx_u64(a, b, &mut hi) }; + (lo, hi) + } + #[cfg(not(all(target_arch = "x86_64", target_feature = "bmi2")))] + { + let prod = (a as u128) * (b as u128); + (prod as u64, (prod >> 64) as u64) + } +} + +/// `c * x` split into u32-wide halves so LLVM emits `umull` on aarch64 +/// instead of promoting to `u128` (valid because `C < sqrt(P) < 2^32`); +/// x86-64 keeps the single fast 64-bit multiply. +#[inline(always)] +fn mul_c_narrow(c: u64, x: u64) -> u64 { + #[cfg(target_arch = "x86_64")] + { + c.wrapping_mul(x) + } + #[cfg(not(target_arch = "x86_64"))] + { + let (c, x_lo, x_hi) = (c as u32 as u64, x as u32 as u64, x >> 32); + (c * x_lo).wrapping_add((c * x_hi) << 32) + } +} + +/// Two-fold product reduction entirely in `u64` (requires +/// [`fp64_folds_in_word`]): avoids u128 mask/shift on sub-word primes. +#[inline(always)] +fn fp64_mul_fast(a: u64, b: u64) -> u64 { + let bits = 64 - P.leading_zeros(); + let c = (1u64 << bits).wrapping_sub(P); + let mask = (1u64 << bits) - 1; + let (lo, hi) = mul64_wide(a, b); + let high = (lo >> bits) | (hi << (64 - bits)); + let f1 = (lo & mask) + mul_c_narrow(c, high); + let f2 = (f1 & mask) + mul_c_narrow(c, f1 >> bits); + let reduced = f2.wrapping_sub(P); + let borrow = reduced >> 63; + reduced.wrapping_add(borrow.wrapping_neg() & P) +} diff --git a/crates/jolt-field-two/tests/bn254_differential.rs b/crates/jolt-field-two/tests/bn254_differential.rs new file mode 100644 index 0000000000..d213aff287 --- /dev/null +++ b/crates/jolt-field-two/tests/bn254_differential.rs @@ -0,0 +1,260 @@ +//! Differential tests: jolt-field-two's BN254 backend against jolt-field. +//! +//! jolt-field (the crate being rebuilt) is the oracle: every operation, +//! serde byte stream, and transcript byte stream must match exactly. + +#![cfg(feature = "bn254")] +#![expect(clippy::unwrap_used, reason = "test code")] + +use jolt_field as base; +use jolt_field_two as two; + +use base::{Accumulator as _, CanonicalRepr, FieldCore, FromPrimitiveInt, RingCore}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use two::{Accumulator as _, CanonicalEncoding, Field as _, Ring}; + +fn rng() -> ChaCha20Rng { + ChaCha20Rng::seed_from_u64(0xb254_b254) +} + +/// Sample a matched (baseline, rebuilt) element pair from the same bytes. +fn sample_pair(rng: &mut ChaCha20Rng) -> (base::Fr, two::Fr) { + let bytes: [u8; 32] = rng.gen(); + let b = ::from_le_bytes_mod_order(&bytes); + let t = ::from_bytes_le_reduced(&bytes); + assert_matches(t, b); + (b, t) +} + +/// Byte-level equality between the two crates' elements. +fn assert_matches(ours: two::Fr, theirs: base::Fr) { + assert_eq!(ours.to_bytes_le_vec(), theirs.to_bytes_le_vec()); +} + +fn assert_matches_fq(ours: two::Fq, theirs: base::Fq) { + assert_eq!(ours.to_bytes_le_vec(), theirs.to_bytes_le_vec()); +} + +#[test] +fn arithmetic_matches() { + let mut rng = rng(); + for _ in 0..500 { + let (b1, t1) = sample_pair(&mut rng); + let (b2, t2) = sample_pair(&mut rng); + assert_matches(t1 + t2, b1 + b2); + assert_matches(t1 - t2, b1 - b2); + assert_matches(t1 * t2, b1 * b2); + assert_matches(-t1, -b1); + assert_matches(Ring::square(&t1), RingCore::square(&b1)); + let (ti, bi) = (t1.inverse(), b1.inverse()); + assert_eq!(ti.is_some(), bi.is_some(), "inverse disagreement"); + if let (Some(ti), Some(bi)) = (ti, bi) { + assert_matches(ti, bi); + } + if t2 != two::Zero::zero() { + assert_matches(t1 / t2, b1 / b2); + } + } +} + +#[test] +fn integer_conversions_match() { + let mut rng = rng(); + let check = |v_u64: u64, v_i64: i64, v_u128: u128, v_i128: i128| { + assert_matches(two::Fr::from_u64(v_u64), FromPrimitiveInt::from_u64(v_u64)); + assert_matches(two::Fr::from_i64(v_i64), FromPrimitiveInt::from_i64(v_i64)); + assert_matches( + two::Fr::from_u128(v_u128), + FromPrimitiveInt::from_u128(v_u128), + ); + assert_matches( + two::Fr::from_i128(v_i128), + FromPrimitiveInt::from_i128(v_i128), + ); + }; + // Boundary values, including both sides of the Montgomery precomp table. + for v in [0u64, 1, 2, 16383, 16384, 16385, u64::MAX] { + check(v, v as i64, v as u128, v as i128); + } + check(0, -1, u128::MAX, i128::MIN); + check(0, i64::MIN, 1 << 127, -(1 << 100)); + for _ in 0..300 { + check(rng.gen(), rng.gen(), rng.gen(), rng.gen()); + } + assert_matches(two::Fr::from_bool(true), FromPrimitiveInt::from_bool(true)); +} + +#[test] +fn scalar_mul_fast_paths_match() { + let mut rng = rng(); + for _ in 0..300 { + let (b, t) = sample_pair(&mut rng); + let s64: u64 = rng.gen(); + let s128: u128 = rng.gen(); + let si64: i64 = rng.gen(); + let si128: i128 = rng.gen(); + assert_matches(t.mul_u64(s64), b.mul_u64(s64)); + assert_matches(t.mul_i64(si64), b.mul_i64(si64)); + assert_matches(t.mul_u128(s128), b.mul_u128(s128)); + assert_matches(t.mul_i128(si128), b.mul_i128(si128)); + // Low-limb-only u128 exercises the single-round Barrett path. + assert_matches(t.mul_u128(s64 as u128), b.mul_u128(s64 as u128)); + for edge in [0u64, 1, 2] { + assert_matches(t.mul_u64(edge), b.mul_u64(edge)); + } + } +} + +#[test] +fn serde_bytes_match() { + let mut rng = rng(); + let cfg = bincode::config::standard(); + for _ in 0..100 { + let (b, t) = sample_pair(&mut rng); + let b_bytes = bincode::serde::encode_to_vec(b, cfg).unwrap(); + let t_bytes = bincode::serde::encode_to_vec(t, cfg).unwrap(); + assert_eq!(b_bytes, t_bytes, "wire bytes diverge"); + // Cross-decode: each crate accepts the other's encoding. + let (b_back, _): (base::Fr, usize) = + bincode::serde::decode_from_slice(&t_bytes, cfg).unwrap(); + let (t_back, _): (two::Fr, usize) = + bincode::serde::decode_from_slice(&b_bytes, cfg).unwrap(); + assert_matches(t_back, b_back); + } + // Non-canonical wire bytes rejected by both. + let bad = bincode::serde::encode_to_vec([0xffu8; 32], cfg).unwrap(); + assert!(bincode::serde::decode_from_slice::(&bad, cfg).is_err()); + assert!(bincode::serde::decode_from_slice::(&bad, cfg).is_err()); +} + +#[test] +fn transcript_surface_matches() { + let mut rng = rng(); + for _ in 0..200 { + let (b, t) = sample_pair(&mut rng); + assert_eq!(CanonicalEncoding::num_bits(&t), CanonicalRepr::num_bits(&b)); + assert_eq!(t.to_u64_checked(), b.to_canonical_u64_checked()); + + let challenge: [u8; 16] = rng.gen(); + assert_matches( + ::from_challenge_bytes(&challenge), + ::from_challenge_bytes(&challenge), + ); + let digest: [u8; 32] = rng.gen(); + assert_matches( + ::from_scalar_challenge_bytes(&digest), + ::from_scalar_challenge_bytes(&digest), + ); + let wide: [u8; 48] = std::array::from_fn(|_| rng.gen()); + assert_matches( + ::from_bytes_le_reduced(&wide), + ::from_le_bytes_mod_order(&wide), + ); + } + // Small-value integer views agree with construction. + for v in [0u64, 1, 999, u64::MAX] { + assert_eq!(two::Fr::from_u64(v).to_u64_checked(), Some(v)); + assert_eq!(two::Fr::from_u64(v).to_u128_checked(), Some(v as u128)); + } + assert_eq!( + two::Fr::from_u128(u128::MAX).to_u128_checked(), + Some(u128::MAX) + ); + assert_eq!(two::Fr::from_u128(u128::MAX).to_u64_checked(), None); +} + +#[test] +fn wide_accumulator_matches() { + let mut rng = rng(); + let mut base_acc = ::Accumulator::default(); + let mut two_acc = ::Accumulator::default(); + for _ in 0..1000 { + let (b1, t1) = sample_pair(&mut rng); + let (b2, t2) = sample_pair(&mut rng); + base_acc.fmadd(b1, b2); + two_acc.fmadd(t1, t2); + } + assert_matches(two_acc.reduce(), base_acc.reduce()); + + // add / small-scalar fmadds / merge parity. + let (b, t) = sample_pair(&mut rng); + let mut base_acc = ::Accumulator::default(); + let mut two_acc = ::Accumulator::default(); + base_acc.add(b); + two_acc.add(t); + base_acc.fmadd_u8(b, 200); + two_acc.fmadd_u8(t, 200); + base_acc.fmadd_u64(b, u64::MAX); + two_acc.fmadd_u64(t, u64::MAX); + base_acc.fmadd_i64(b, -12345); + two_acc.fmadd_i64(t, -12345); + base_acc.fmadd_bool(b, true); + two_acc.fmadd_bool(t, true); + + let mut base_other = ::Accumulator::default(); + let mut two_other = ::Accumulator::default(); + base_other.fmadd(b, b); + two_other.fmadd(t, t); + base_acc.merge(base_other); + two_acc.merge(two_other); + assert_matches(two_acc.reduce(), base_acc.reduce()); + + // Empty accumulators reduce to zero. + let empty = ::Accumulator::default(); + assert_eq!(empty.reduce(), two::Fr::from_u64(0)); +} + +#[test] +fn fq_matches() { + let mut rng = rng(); + for _ in 0..300 { + let bytes: [u8; 32] = rng.gen(); + let b1 = ::from_le_bytes_mod_order(&bytes); + let t1 = ::from_bytes_le_reduced(&bytes); + assert_matches_fq(t1, b1); + let bytes2: [u8; 32] = rng.gen(); + let b2 = ::from_le_bytes_mod_order(&bytes2); + let t2 = ::from_bytes_le_reduced(&bytes2); + + assert_matches_fq(t1 + t2, b1 + b2); + assert_matches_fq(t1 * t2, b1 * b2); + assert_matches_fq(-t1, -b1); + if let (Some(ti), Some(bi)) = (t1.inverse(), b1.inverse()) { + assert_matches_fq(ti, bi); + } + + let v: u64 = rng.gen(); + assert_matches_fq(two::Fq::from_u64(v), FromPrimitiveInt::from_u64(v)); + + let challenge: [u8; 16] = rng.gen(); + assert_matches_fq( + ::from_challenge_bytes(&challenge), + ::from_challenge_bytes(&challenge), + ); + + let cfg = bincode::config::standard(); + assert_eq!( + bincode::serde::encode_to_vec(t1, cfg).unwrap(), + bincode::serde::encode_to_vec(b1, cfg).unwrap(), + ); + } +} + +fn inner_product(xs: &[F], ys: &[F]) -> F { + let mut acc = F::Accumulator::default(); + for (&x, &y) in xs.iter().zip(ys) { + acc.fmadd(x, y); + } + acc.reduce() +} + +#[test] +fn jolt_field_blanket_covers_bn254() { + let xs = [two::Fr::from_u64(2), two::Fr::from_u64(3)]; + let ys = [two::Fr::from_u64(5), two::Fr::from_u64(7)]; + assert_eq!(inner_product(&xs, &ys), two::Fr::from_u64(31)); + let xq = [two::Fq::from_u64(2)]; + let yq = [two::Fq::from_u64(5)]; + assert_eq!(inner_product(&xq, &yq), two::Fq::from_u64(10)); +} diff --git a/crates/jolt-field-two/tests/limbs_signed_differential.rs b/crates/jolt-field-two/tests/limbs_signed_differential.rs new file mode 100644 index 0000000000..59552c14fb --- /dev/null +++ b/crates/jolt-field-two/tests/limbs_signed_differential.rs @@ -0,0 +1,267 @@ +//! Differential tests for `Limbs` and the signed bigint families against +//! jolt-field, plus u128/i128 oracles for the widths that fit. + +use jolt_field as base; +use jolt_field_two as two; + +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; + +fn rng() -> ChaCha20Rng { + ChaCha20Rng::seed_from_u64(0x11b5_519d) +} + +#[test] +fn limbs_arithmetic_matches() { + let mut rng = rng(); + for _ in 0..500 { + let a: [u64; 4] = rng.gen(); + let b: [u64; 4] = rng.gen(); + let (ba, bb) = (base::Limbs::new(a), base::Limbs::new(b)); + let (ta, tb) = (two::Limbs::new(a), two::Limbs::new(b)); + + assert_eq!(ta.mul_trunc::<4, 6>(&tb).0, ba.mul_trunc::<4, 6>(&bb).0); + assert_eq!(ta.mul_trunc::<4, 2>(&tb).0, ba.mul_trunc::<4, 2>(&bb).0); + assert_eq!(ta.mul_low(&tb).0, ba.mul_low(&bb).0); + assert_eq!(ta.add_trunc::<4, 4>(&tb).0, ba.add_trunc::<4, 4>(&bb).0); + assert_eq!(ta.sub_trunc::<4, 4>(&tb).0, ba.sub_trunc::<4, 4>(&bb).0); + assert_eq!(ta.cmp(&tb), ba.cmp(&bb)); + assert_eq!(ta.num_bits(), ba.num_bits()); + assert_eq!(format!("{ta}"), format!("{ba}")); + assert_eq!(format!("{ta:?}"), format!("{ba:?}")); + + let (mut ca, mut cb) = (ta, ba); + assert_eq!(ca.add_with_carry(&tb), cb.add_with_carry(&bb)); + assert_eq!(ca.0, cb.0); + assert_eq!(ca.sub_with_borrow(&tb), cb.sub_with_borrow(&bb)); + assert_eq!(ca.0, cb.0); + } +} + +#[test] +fn limbs_fmadd_matches() { + let mut rng = rng(); + let mut base_acc = base::Limbs::<5>::zero(); + let mut two_acc = two::Limbs::<5>::zero(); + for _ in 0..5000 { + let a: [u64; 2] = rng.gen(); + let b: [u64; 2] = rng.gen(); + base_acc.fmadd::<2, 2>(&base::Limbs::new(a), &base::Limbs::new(b)); + two_acc.fmadd::<2, 2>(&two::Limbs::new(a), &two::Limbs::new(b)); + } + assert_eq!(two_acc.0, base_acc.0); +} + +#[test] +fn limbs_u128_oracle() { + let mut rng = rng(); + for _ in 0..500 { + let a: u64 = rng.gen(); + let b: u64 = rng.gen(); + let product = two::Limbs::<1>::new([a]).mul_trunc::<1, 2>(&two::Limbs::new([b])); + let expected = (a as u128) * (b as u128); + assert_eq!(product.0, [expected as u64, (expected >> 64) as u64]); + } +} + +fn to_base_signed( + t: &two::signed::SignedBigInt, +) -> base::signed::SignedBigInt { + base::signed::SignedBigInt::new(t.magnitude.0, t.is_positive) +} + +fn assert_signed_matches( + ours: two::signed::SignedBigInt, + theirs: base::signed::SignedBigInt, +) { + assert_eq!(ours.magnitude.0, theirs.magnitude.0, "magnitude diverges"); + if !ours.magnitude.is_zero() { + assert_eq!(ours.is_positive, theirs.is_positive, "sign diverges"); + } +} + +#[test] +fn signed_bigint_ops_match() { + let mut rng = rng(); + for _ in 0..500 { + let (la, sa): ([u64; 2], bool) = (rng.gen(), rng.gen()); + let (lb, sb): ([u64; 2], bool) = (rng.gen(), rng.gen()); + let ta = two::signed::SignedBigInt::new(la, sa); + let tb = two::signed::SignedBigInt::new(lb, sb); + let (ba, bb) = (to_base_signed(&ta), to_base_signed(&tb)); + + assert_signed_matches(ta + tb, ba + bb); + assert_signed_matches(ta - tb, ba - bb); + assert_signed_matches(ta * tb, ba * bb); + assert_signed_matches(-ta, -ba); + assert_eq!(ta.cmp(&tb), ba.cmp(&bb)); + assert_eq!(ta == tb, ba == bb); + assert_signed_matches(ta.mul_trunc::<2, 3>(&tb), ba.mul_trunc::<2, 3>(&bb)); + assert_eq!(ta.magnitude_limbs(), ba.magnitude_limbs()); + + let mut x = ta; + x += tb; + x *= ta; + x -= tb; + let mut bx = ba; + bx += bb; + bx *= ba; + bx -= bb; + assert_signed_matches(x, bx); + } +} + +#[test] +fn signed_bigint_i128_oracle() { + let mut rng = rng(); + for _ in 0..500 { + let a: i64 = rng.gen(); + let b: i64 = rng.gen(); + let sa = two::signed::S128::from_i128(a as i128); + let sb = two::signed::S128::from_i128(b as i128); + assert_eq!((sa + sb).to_i128(), Some(a as i128 + b as i128)); + assert_eq!((sa - sb).to_i128(), Some(a as i128 - b as i128)); + assert_eq!((sa * sb).to_i128(), Some(a as i128 * b as i128)); + assert_eq!(sa.cmp(&sb), (a as i128).cmp(&(b as i128))); + assert_eq!( + two::signed::S64::from_i64(b).to_i128(), + b as i128, + "S64 round-trip" + ); + } + // i128 extremes and the ±0 convention. + assert_eq!( + two::signed::S128::from_i128(i128::MIN).to_i128(), + Some(i128::MIN) + ); + assert_eq!( + two::signed::S128::new([0, 1 << 63], true).to_i128(), + None, + "positive 2^127 does not fit" + ); + assert_eq!( + two::signed::S128::from_u128(u128::MAX).magnitude_as_u128(), + u128::MAX + ); + assert_eq!(two::signed::S64::from_u64(7).magnitude_as_u64(), 7); + assert_eq!(two::signed::S64::from_u64_with_sign(7, false).to_i128(), -7); + let pos_zero = two::signed::S64::new([0], true); + let neg_zero = two::signed::S64::new([0], false); + assert_eq!(pos_zero, neg_zero); + assert_eq!(pos_zero.cmp(&neg_zero), std::cmp::Ordering::Equal); +} + +fn to_base_hi32( + t: &two::signed::SignedBigIntHi32, +) -> base::signed::SignedBigIntHi32 { + base::signed::SignedBigIntHi32::new(*t.magnitude_lo(), t.magnitude_hi(), t.is_positive()) +} + +fn assert_hi32_matches( + ours: two::signed::SignedBigIntHi32, + theirs: base::signed::SignedBigIntHi32, +) { + assert_eq!(ours.magnitude_lo(), theirs.magnitude_lo(), "lo diverges"); + assert_eq!(ours.magnitude_hi(), theirs.magnitude_hi(), "hi diverges"); + let zero = ours.magnitude_hi() == 0 && ours.magnitude_lo().iter().all(|&l| l == 0); + if !zero { + assert_eq!(ours.is_positive(), theirs.is_positive(), "sign diverges"); + } +} + +#[test] +fn hi32_ops_match() { + let mut rng = rng(); + // S96 (N=1), S160 (N=2), S224 (N=3): exercises the general schoolbook + // multiply against the baseline's hand-unrolled N=1/N=2 kernels. + for _ in 0..500 { + let a96 = two::signed::S96::new([rng.gen()], rng.gen(), rng.gen()); + let b96 = two::signed::S96::new([rng.gen()], rng.gen(), rng.gen()); + assert_hi32_matches(a96 + b96, to_base_hi32(&a96) + to_base_hi32(&b96)); + assert_hi32_matches(a96 - b96, to_base_hi32(&a96) - to_base_hi32(&b96)); + assert_hi32_matches(a96 * b96, to_base_hi32(&a96) * to_base_hi32(&b96)); + + let a160 = two::signed::S160::new(rng.gen(), rng.gen(), rng.gen()); + let b160 = two::signed::S160::new(rng.gen(), rng.gen(), rng.gen()); + assert_hi32_matches(a160 + b160, to_base_hi32(&a160) + to_base_hi32(&b160)); + assert_hi32_matches(a160 - b160, to_base_hi32(&a160) - to_base_hi32(&b160)); + assert_eq!( + a160.cmp(&b160), + to_base_hi32(&a160).cmp(&to_base_hi32(&b160)) + ); + // Baseline's unrolled S160 mul kernel overflows u128 in its cross-term + // sum when the second limbs are large (panics in debug, wraps in + // release), so the vs-baseline mul comparison is restricted to the + // domain where the baseline is correct. Full-range correctness of our + // kernel is covered by `hi32_mul_full_range_oracle`. + let mask = (1u64 << 62) - 1; + let a160m = two::signed::S160::new( + [a160.magnitude_lo()[0], a160.magnitude_lo()[1] & mask], + a160.magnitude_hi(), + a160.is_positive(), + ); + let b160m = two::signed::S160::new( + [b160.magnitude_lo()[0], b160.magnitude_lo()[1] & mask], + b160.magnitude_hi(), + b160.is_positive(), + ); + assert_hi32_matches(a160m * b160m, to_base_hi32(&a160m) * to_base_hi32(&b160m)); + + let a224 = two::signed::S224::new(rng.gen(), rng.gen(), rng.gen()); + let b224 = two::signed::S224::new(rng.gen(), rng.gen(), rng.gen()); + assert_hi32_matches(a224 + b224, to_base_hi32(&a224) + to_base_hi32(&b224)); + assert_hi32_matches(a224 * b224, to_base_hi32(&a224) * to_base_hi32(&b224)); + + // Neg and assign forms. + let mut x = a160; + x += b160; + x -= a160; + let mut bx = to_base_hi32(&a160); + bx += to_base_hi32(&b160); + bx -= to_base_hi32(&a160); + assert_hi32_matches(x, bx); + assert_hi32_matches(-a96, -to_base_hi32(&a96)); + } +} + +#[test] +fn hi32_mul_full_range_oracle() { + // Full-limb SignedBigInt<3> multiplication (overflow-safe mac chains) as + // the reference for S160 multiply across the ENTIRE input domain, + // including where the baseline hi32 kernel wraps. + let mut rng = rng(); + for _ in 0..1000 { + let a = two::signed::S160::new(rng.gen(), rng.gen(), rng.gen()); + let b = two::signed::S160::new(rng.gen(), rng.gen(), rng.gen()); + let product = a * b; + let wide = a + .to_signed_bigint_nplus1::<3>() + .mul_trunc::<3, 3>(&b.to_signed_bigint_nplus1::<3>()); + assert_eq!(&product.magnitude_lo()[..], &wide.magnitude.0[..2]); + assert_eq!( + product.magnitude_hi() as u64, + wide.magnitude.0[2] & 0xFFFF_FFFF + ); + } +} + +#[test] +fn hi32_conversions_match() { + let mut rng = rng(); + for _ in 0..300 { + let v = two::signed::S160::new(rng.gen(), rng.gen(), rng.gen()); + let bv = to_base_hi32(&v); + let sb = v.to_signed_bigint_nplus1::<3>(); + let bsb = bv.to_signed_bigint_nplus1::<3>(); + assert_signed_matches(sb, bsb); + + let u: u128 = rng.gen(); + assert_hi32_matches(two::signed::S160::from(u), base::signed::S160::from(u)); + } + // Addition carries into the u32 tail through the public ops. + let big = two::signed::S160::from(u128::MAX); + let sum = big + big; + let base_sum = base::signed::S160::from(u128::MAX) + base::signed::S160::from(u128::MAX); + assert_hi32_matches(sum, base_sum); + assert_eq!(sum.magnitude_hi(), 1); +} diff --git a/crates/jolt-field-two/tests/solinas_words_differential.rs b/crates/jolt-field-two/tests/solinas_words_differential.rs new file mode 100644 index 0000000000..e9426268f4 --- /dev/null +++ b/crates/jolt-field-two/tests/solinas_words_differential.rs @@ -0,0 +1,326 @@ +//! Differential tests for the Solinas word fields (`Fp32`/`Fp64`) against +//! jolt-field across every registered ≤64-bit prime offset, with u128 +//! modular arithmetic as the independent oracle. + +#![cfg(feature = "solinas")] +#![expect(clippy::unwrap_used, reason = "test code")] + +use jolt_field as base; +use jolt_field_two as two; + +use base::{ + CanonicalField, CanonicalRepr, FromPrimitiveInt, HalvingField, PseudoMersenneField, RingCore, +}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use two::{Accumulator as _, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, Ring}; + +fn rng() -> ChaCha20Rng { + ChaCha20Rng::seed_from_u64(0x5011_a5a5) +} + +/// Full differential + oracle sweep for one (rebuilt, baseline, modulus) triple. +macro_rules! check_prime { + ($two:ty, $base:ty, $p:expr, $rng:expr) => {{ + let p: u128 = $p; + let bits = <$two as CanonicalEncoding>::MODULUS_BITS; + // Baseline Fp64::reduce_u128 truncates the fold's high part to u64 + // (`(v >> BITS) as u64`), so for sub-word u64 primes it reduces + // inputs >= 2^(64+BITS) incorrectly. Ours reduces correctly on the + // full domain (asserted against the oracle); baseline reduction + // parity is only asserted on its correct domain. + let base_reduces_correctly = |raw: u128| bits + 64 >= 128 || raw < (1u128 << (bits + 64)); + let sample = |rng: &mut ChaCha20Rng| -> ($two, $base, u128) { + let raw: u128 = rng.gen(); + let v = raw % p; + let t = <$two as CanonicalEncoding>::from_u128_reduced(raw); + assert_eq!(t.to_u128_checked(), Some(v), "reduction vs oracle"); + if base_reduces_correctly(raw) { + let b = <$base as CanonicalField>::from_canonical_u128_reduced(raw); + assert_eq!(b.to_canonical_u128(), v, "baseline reduction vs oracle"); + } + let b = <$base as CanonicalField>::from_canonical_u128_checked(v).unwrap(); + (t, b, v) + }; + + // Metadata parity. + assert_eq!( + <$two as CanonicalEncoding>::MODULUS_BITS, + <$base as CanonicalField>::modulus_bits() + ); + assert_eq!( + <$two as PseudoMersenne>::OFFSET, + <$base as PseudoMersenneField>::MODULUS_OFFSET + ); + assert_eq!( + <$two as CanonicalEncoding>::NUM_BYTES, + <$base as CanonicalRepr>::NUM_BYTES + ); + + let cfg = bincode::config::standard(); + for _ in 0..200 { + let (ta, ba, va) = sample($rng); + let (tb, bb, vb) = sample($rng); + + // Arithmetic vs baseline and vs the u128 oracle. + let cases: [($two, $base, u128); 4] = [ + (ta + tb, ba + bb, (va + vb) % p), + (ta - tb, ba - bb, (va + p - vb) % p), + (ta * tb, ba * bb, (va * vb) % p), + (-ta, -ba, (p - va) % p), + ]; + for (t, b, v) in cases { + assert_eq!(t.to_u128_checked(), Some(v)); + assert_eq!(b.to_canonical_u128(), v); + } + assert_eq!( + Ring::square(&ta).to_u128_checked().unwrap(), + RingCore::square(&ba).to_canonical_u128() + ); + assert_eq!( + ta.half().to_u128_checked().unwrap(), + ba.half().to_canonical_u128() + ); + match (ta.inverse(), ba.inverse()) { + (Some(ti), Some(bi)) => { + assert_eq!(ti.to_u128_checked().unwrap(), bi.to_canonical_u128()); + assert_eq!((ti * ta).to_u128_checked(), Some(1)); + } + (ti, bi) => assert_eq!(ti.is_none(), bi.is_none()), + } + + // Widening multiply + explicit reduction round-trip. + assert_eq!( + <$two>::solinas_reduce(ta.mul_wide(tb)).to_u128_checked(), + Some((va * vb) % p) + ); + + // Integer conversions. + let x64: u64 = $rng.gen(); + let xi: i64 = $rng.gen(); + assert_eq!( + <$two as Ring>::from_u64(x64).to_u128_checked().unwrap(), + <$base as FromPrimitiveInt>::from_u64(x64).to_canonical_u128() + ); + assert_eq!( + <$two as Ring>::from_i64(xi).to_u128_checked().unwrap(), + <$base as FromPrimitiveInt>::from_i64(xi).to_canonical_u128() + ); + assert_eq!( + ta.mul_u64(x64).to_u128_checked().unwrap(), + ba.mul_u64(x64).to_canonical_u128() + ); + + // Transcript surface: bytes, reducing decodes, challenges. + assert_eq!(ta.to_bytes_le_vec(), ba.to_bytes_le_vec()); + assert_eq!( + CanonicalEncoding::num_bits(&ta), + CanonicalRepr::num_bits(&ba) + ); + assert_eq!(ta.to_u64_checked(), ba.to_canonical_u64_checked()); + let challenge: [u8; 32] = $rng.gen(); + // 16-byte decodes hit the baseline truncation bug on sub-word + // u64 primes; ours is asserted against the oracle there instead. + for len in [8usize, 16, 32] { + let ours = <$two as CanonicalEncoding>::from_bytes_le_reduced(&challenge[..len]); + assert_eq!( + <$two as CanonicalEncoding>::from_challenge_bytes(&challenge[..len]), + ours, + "challenge derivation defaults to the reducing decode" + ); + if len <= 16 { + let mut padded = [0u8; 16]; + padded[..len].copy_from_slice(&challenge[..len]); + let raw = u128::from_le_bytes(padded); + assert_eq!(ours.to_u128_checked(), Some(raw % p), "decode vs oracle"); + if !base_reduces_correctly(raw) { + continue; + } + } + assert_eq!( + ours.to_u128_checked().unwrap(), + <$base as CanonicalRepr>::from_le_bytes_mod_order(&challenge[..len]) + .to_canonical_u128() + ); + } + + // Wire bytes: equality, cross-decode, canonical rejection. + let t_bytes = bincode::serde::encode_to_vec(ta, cfg).unwrap(); + let b_bytes = bincode::serde::encode_to_vec(ba, cfg).unwrap(); + assert_eq!(t_bytes, b_bytes, "wire bytes diverge"); + let (t_back, _): ($two, usize) = + bincode::serde::decode_from_slice(&b_bytes, cfg).unwrap(); + assert_eq!(t_back.to_u128_checked(), Some(va)); + } + + // Boundary values through every arithmetic path. + let boundaries: Vec = vec![0, 1, 2, p - 2, p - 1, p / 2, p / 2 + 1]; + for &x in &boundaries { + for &y in &boundaries { + let tx = <$two as CanonicalEncoding>::from_u128_checked(x).unwrap(); + let ty = <$two as CanonicalEncoding>::from_u128_checked(y).unwrap(); + assert_eq!(tx.to_u128_checked(), Some(x)); + assert_eq!((tx + ty).to_u128_checked(), Some((x + y) % p)); + assert_eq!((tx - ty).to_u128_checked(), Some((x + p - y) % p)); + assert_eq!((tx * ty).to_u128_checked(), Some((x * y) % p)); + } + } + assert_eq!(<$two as CanonicalEncoding>::from_u128_checked(p), None); + assert_eq!( + <$two as CanonicalEncoding>::from_u128_reduced(u128::MAX).to_u128_checked(), + Some(u128::MAX % p) + ); + assert_eq!( + <$two as Ring>::from_i128(i128::MIN).to_u128_checked(), + Some((p - ((1u128 << 127) % p)) % p), + "i128::MIN vs oracle" + ); + + // Non-canonical wire encodings rejected (encode p itself). + let n = <$two as CanonicalEncoding>::NUM_BYTES; + let p_bytes = &p.to_le_bytes()[..n]; + assert_eq!( + <$two as CanonicalEncoding>::from_bytes_le_checked(p_bytes), + None + ); + assert_eq!( + <$two as CanonicalEncoding>::from_bytes_le_checked(&p.to_le_bytes()[..n - 1]), + None, + "wrong length rejected" + ); + }}; +} + +#[test] +fn fp32_offsets_match() { + let mut rng = rng(); + check_prime!( + two::Prime24Offset3, + base::Prime24Offset3, + (1 << 24) - 3, + &mut rng + ); + check_prime!( + two::Prime30Offset35, + base::Prime30Offset35, + (1 << 30) - 35, + &mut rng + ); + check_prime!( + two::Prime31Offset19, + base::Prime31Offset19, + (1 << 31) - 19, + &mut rng + ); + check_prime!( + two::Prime32Offset99, + base::Prime32Offset99, + (1 << 32) - 99, + &mut rng + ); + // Ad hoc small prime and Mersenne31 (unregistered but instantiable). + check_prime!(two::Fp32<251>, base::Fp32<251>, 251, &mut rng); + check_prime!( + two::Fp32<{ (1 << 31) - 1 }>, + base::Fp32<{ (1 << 31) - 1 }>, + (1 << 31) - 1, + &mut rng + ); +} + +#[test] +fn fp64_offsets_match() { + let mut rng = rng(); + check_prime!( + two::Prime40Offset195, + base::Prime40Offset195, + (1 << 40) - 195, + &mut rng + ); + check_prime!( + two::Prime48Offset59, + base::Prime48Offset59, + (1 << 48) - 59, + &mut rng + ); + check_prime!( + two::Prime56Offset27, + base::Prime56Offset27, + (1 << 56) - 27, + &mut rng + ); + check_prime!( + two::Prime64Offset59, + base::Prime64Offset59, + (1 << 64) - 59, + &mut rng + ); + // Mersenne61: sub-word u64 prime exercising C = 1. + check_prime!( + two::Fp64<{ (1 << 61) - 1 }>, + base::Fp64<{ (1 << 61) - 1 }>, + (1 << 61) - 1, + &mut rng + ); +} + +#[test] +fn registry_matches() { + assert_eq!( + two::PRIME_OFFSET_SPECS.len(), + base::PRIME_OFFSET_SPECS.len() + ); + for (t, b) in two::PRIME_OFFSET_SPECS + .iter() + .zip(base::PRIME_OFFSET_SPECS.iter()) + { + assert_eq!((t.bits, t.offset, t.modulus), (b.bits, b.offset, b.modulus)); + assert!(two::is_registered_prime_offset(t.bits, t.offset as u128)); + assert_eq!( + two::pseudo_mersenne_modulus(t.bits, t.offset as u128), + Some(t.modulus) + ); + assert_eq!( + two::registered_prime_offset_spec(t.bits, t.offset as u128).map(|s| s.modulus), + Some(t.modulus) + ); + } + assert_eq!(two::PRIME_OFFSET_MAX, base::PRIME_OFFSET_MAX); + assert_eq!( + two::PRIME_OFFSET_IMPLEMENTED_MAX_BITS, + base::PRIME_OFFSET_IMPLEMENTED_MAX_BITS + ); + assert!(!two::is_registered_prime_offset(61, 1)); + assert_eq!(two::pseudo_mersenne_modulus(0, 3), None); + assert_eq!( + two::pseudo_mersenne_modulus(128, 275), + Some(u128::MAX - 274) + ); +} + +#[test] +fn balanced_digit_lut_matches() { + for log_basis in 1..=6 { + let t: [two::Prime64Offset59; 64] = two::balanced_digit_lut(log_basis); + let b: [base::Prime64Offset59; 64] = base::balanced_digit_lut(log_basis); + for (x, y) in t.iter().zip(b.iter()) { + assert_eq!(x.to_u128_checked().unwrap(), y.to_canonical_u128()); + } + } +} + +fn inner_product(xs: &[F], ys: &[F]) -> F { + let mut acc = F::Accumulator::default(); + for (&x, &y) in xs.iter().zip(ys) { + acc.fmadd(x, y); + } + acc.reduce() +} + +#[test] +fn jolt_field_blanket_covers_solinas() { + type F = two::Prime64Offset59; + let xs = [::from_u64(2), ::from_u64(3)]; + let ys = [::from_u64(5), ::from_u64(7)]; + assert_eq!(inner_product(&xs, &ys), ::from_u64(31)); +} diff --git a/crates/jolt-field-two/tests/spine.rs b/crates/jolt-field-two/tests/spine.rs new file mode 100644 index 0000000000..bc8064a1fe --- /dev/null +++ b/crates/jolt-field-two/tests/spine.rs @@ -0,0 +1,311 @@ +//! Spine conformance: a third-party Mersenne-61 field implemented with no +//! arkworks dependency, driven through the exported stamping macros. +//! +//! This is the implementability proof for the trait spine: everything a +//! non-BN254, non-Solinas field must provide, and nothing more. + +#![expect(clippy::unwrap_used, reason = "test code")] + +use jolt_field_two::{ + impl_ring_ops, impl_serde_bytes, Accumulator, CanonicalEncoding, Field, JoltField, + NaiveAccumulator, One, Ring, WithAccumulator, Zero, +}; +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; + +const P: u64 = (1 << 61) - 1; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +struct M61(u64); + +fn reduce128(v: u128) -> u64 { + (v % P as u128) as u64 +} + +impl_ring_ops!(impl[] M61 { + add(a, b): M61((a.0 + b.0) % P), + sub(a, b): M61((a.0 + P - b.0) % P), + mul(a, b): M61(reduce128(a.0 as u128 * b.0 as u128)), + neg(a): M61((P - a.0) % P), + zero: M61(0), + one: M61(1), +}); + +impl std::fmt::Display for M61 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Ring for M61 { + fn from_u64(v: u64) -> Self { + M61(v % P) + } + fn from_i64(v: i64) -> Self { + if v >= 0 { + Self::from_u64(v as u64) + } else { + -Self::from_u64(v.unsigned_abs()) + } + } + fn from_u128(v: u128) -> Self { + M61(reduce128(v)) + } + fn from_i128(v: i128) -> Self { + if v >= 0 { + Self::from_u128(v as u128) + } else { + -Self::from_u128(v.unsigned_abs()) + } + } +} + +impl Field for M61 { + fn inverse(&self) -> Option { + if self.0 == 0 { + return None; + } + let (mut acc, mut base, mut e) = (M61(1), *self, P - 2); + while e > 0 { + if e & 1 == 1 { + acc *= base; + } + base *= base; + e >>= 1; + } + Some(acc) + } + fn random(rng: &mut R) -> Self { + Self::from_u128(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128) + } +} + +impl CanonicalEncoding for M61 { + const NUM_BYTES: usize = 8; + const MODULUS_BITS: u32 = 61; + fn to_bytes_le(&self, out: &mut [u8]) { + out.copy_from_slice(&self.0.to_le_bytes()); + } + fn from_bytes_le_reduced(bytes: &[u8]) -> Self { + let base = M61::from_u64(256); + bytes + .iter() + .rev() + .fold(M61(0), |acc, &b| acc * base + M61::from_u64(b as u64)) + } + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + let arr: [u8; 8] = bytes.try_into().ok()?; + Self::from_u128_checked(u64::from_le_bytes(arr) as u128) + } + fn to_u128_checked(&self) -> Option { + Some(self.0 as u128) + } + fn from_u128_checked(v: u128) -> Option { + (v < P as u128).then_some(M61(v as u64)) + } + fn from_u128_reduced(v: u128) -> Self { + M61(reduce128(v)) + } + fn num_bits(&self) -> u32 { + 64 - self.0.leading_zeros() + } +} + +impl_serde_bytes!(impl[] M61, 8); + +impl WithAccumulator for M61 { + type Accumulator = NaiveAccumulator; +} + +fn rng() -> ChaCha20Rng { + ChaCha20Rng::seed_from_u64(0x6a6f_6c74) +} + +fn random_triple(rng: &mut ChaCha20Rng) -> (M61, M61, M61) { + (M61::random(rng), M61::random(rng), M61::random(rng)) +} + +#[test] +#[expect(clippy::op_ref, reason = "exercising the by-ref operator impls")] +fn ring_laws() { + let mut rng = rng(); + for _ in 0..1000 { + let (a, b, c) = random_triple(&mut rng); + assert_eq!((a + b) + c, a + (b + c)); + assert_eq!(a + b, b + a); + assert_eq!((a * b) * c, a * (b * c)); + assert_eq!(a * b, b * a); + assert_eq!(a * (b + c), a * b + a * c); + assert_eq!(a - a, M61::zero()); + assert_eq!(a + (-a), M61::zero()); + assert_eq!(a * M61::one(), a); + assert_eq!(a.square(), a * a); + assert_eq!(a + &b, a + b); + assert_eq!(a - &b, a - b); + assert_eq!(a * &b, a * b); + } +} + +#[test] +fn assign_sum_product_forms() { + let mut rng = rng(); + let (a, b, c) = random_triple(&mut rng); + let mut x = a; + x += b; + x -= c; + x *= b; + assert_eq!(x, (a + b - c) * b); + let xs = [a, b, c]; + assert_eq!(xs.iter().sum::(), a + b + c); + assert_eq!(xs.into_iter().sum::(), a + b + c); + assert_eq!(xs.iter().product::(), a * b * c); + assert_eq!(xs.into_iter().product::(), a * b * c); +} + +#[test] +fn field_laws() { + let mut rng = rng(); + for _ in 0..200 { + let a = M61::random(&mut rng); + if a.is_zero() { + continue; + } + assert_eq!(a * a.inverse().unwrap(), M61::one()); + assert_eq!(a.inv_or_zero(), a.inverse().unwrap()); + assert_eq!(a.half() + a.half(), a); + } + assert!(M61::zero().inverse().is_none()); + assert_eq!(M61::zero().inv_or_zero(), M61::zero()); + assert_eq!(M61::two_inv() * M61::from_u64(2), M61::one()); +} + +#[test] +fn integer_embedding() { + assert_eq!(M61::from_bool(true), M61::one()); + assert_eq!(M61::from_i64(-1), -M61::one()); + assert_eq!(M61::from_i128(-1), -M61::one()); + assert_eq!(M61::from_u128(u128::MAX), M61(reduce128(u128::MAX))); + assert_eq!(M61::from_u8(255), M61(255)); + assert_eq!(M61::from_i32(-7), -M61(7)); + assert_eq!(M61::pow2(5), M61(32)); + assert_eq!(M61::pow2(0), M61::one()); + let a = M61(12345); + assert_eq!(a.mul_pow_2(61), a * M61::pow2(61)); + assert_eq!(a.mul_pow_2(200), a * M61::pow2(200)); + assert_eq!(a.mul_u64(7), a * M61(7)); + assert_eq!(a.mul_i64(-7), -(a * M61(7))); + assert_eq!(a.mul_u128(1 << 90), a * M61::from_u128(1 << 90)); + assert_eq!(a.mul_i128(-(1 << 90)), -(a * M61::from_u128(1 << 90))); +} + +#[test] +fn canonical_surface() { + let mut rng = rng(); + for _ in 0..200 { + let a = M61::random(&mut rng); + let bytes = a.to_bytes_le_vec(); + assert_eq!(bytes.len(), M61::NUM_BYTES); + assert_eq!(M61::from_bytes_le_checked(&bytes), Some(a)); + assert_eq!(M61::from_bytes_le_reduced(&bytes), a); + assert_eq!( + M61::from_u128_checked(a.to_u128_checked().unwrap()), + Some(a) + ); + } + // Non-canonical and wrong-length encodings are rejected. + assert_eq!(M61::from_bytes_le_checked(&P.to_le_bytes()), None); + assert_eq!(M61::from_bytes_le_checked(&u64::MAX.to_le_bytes()), None); + assert_eq!(M61::from_bytes_le_checked(&[0u8; 7]), None); + // Reducing decode agrees with integer reduction on oversized input. + let wide = [0xabu8; 16]; + assert_eq!( + M61::from_bytes_le_reduced(&wide), + M61::from_u128_reduced(u128::from_le_bytes(wide)) + ); + // Challenge derivation defaults to the reducing decode. + assert_eq!( + M61::from_challenge_bytes(&wide), + M61::from_bytes_le_reduced(&wide) + ); + assert_eq!( + M61::from_scalar_challenge_bytes(&wide), + M61::from_challenge_bytes(&wide) + ); + assert_eq!(M61::zero().num_bits(), 0); + assert_eq!(M61(0b1011).num_bits(), 4); + assert_eq!(M61(u64::MAX % P).to_u64_checked(), Some(u64::MAX % P)); +} + +#[test] +fn serde_bytes_format() { + let mut rng = rng(); + let cfg = bincode::config::standard(); + for _ in 0..50 { + let a = M61::random(&mut rng); + let bytes = bincode::serde::encode_to_vec(a, cfg).unwrap(); + assert_eq!(bytes, a.to_bytes_le_vec(), "fixed array, no length prefix"); + let (back, read): (M61, usize) = bincode::serde::decode_from_slice(&bytes, cfg).unwrap(); + assert_eq!((back, read), (a, bytes.len())); + } + // A vector pays exactly one length prefix. + let v = vec![M61(1), M61(2), M61(3)]; + let bytes = bincode::serde::encode_to_vec(&v, cfg).unwrap(); + assert_eq!(bytes.len(), 1 + 3 * M61::NUM_BYTES); + // Non-canonical wire bytes are rejected. + let bad = bincode::serde::encode_to_vec(P.to_le_bytes(), cfg).unwrap(); + assert!(bincode::serde::decode_from_slice::(&bad, cfg).is_err()); +} + +#[test] +fn accumulator_equivalence() { + let mut rng = rng(); + let pairs: Vec<(M61, M61)> = (0..100) + .map(|_| (M61::random(&mut rng), M61::random(&mut rng))) + .collect(); + let direct: M61 = pairs.iter().map(|&(a, b)| a * b).sum(); + let mut acc = ::Accumulator::default(); + for &(a, b) in &pairs { + acc.fmadd(a, b); + } + assert_eq!(acc.reduce(), direct); + + let (mut left, mut right) = ( + ::Accumulator::default(), + ::Accumulator::default(), + ); + for &(a, b) in &pairs[..50] { + left.fmadd(a, b); + } + for &(a, b) in &pairs[50..] { + right.fmadd(a, b); + } + left.merge(right); + assert_eq!(left.reduce(), direct); + + let a = M61(9999); + let mut acc = NaiveAccumulator::::default(); + acc.add(a); + acc.fmadd_u8(a, 3); + acc.fmadd_u64(a, 1 << 40); + acc.fmadd_i64(a, -5); + acc.fmadd_bool(a, true); + acc.fmadd_bool(a, false); + let expected = a + a * M61(3) + a * M61(1 << 40) - a * M61(5) + a; + assert_eq!(acc.reduce(), expected); +} + +fn inner_product(xs: &[F], ys: &[F]) -> F { + let mut acc = F::Accumulator::default(); + for (&x, &y) in xs.iter().zip(ys) { + acc.fmadd(x, y); + } + acc.reduce() +} + +#[test] +fn jolt_field_blanket() { + // M61 satisfies the JoltField bundle without any explicit impl. + let xs = [M61(2), M61(3)]; + let ys = [M61(5), M61(7)]; + assert_eq!(inner_product(&xs, &ys), M61(31)); +} From 1502e01819a9791fb2a60631dadc138acee4166d Mon Sep 17 00:00:00 2001 From: acentelles Date: Wed, 29 Jul 2026 14:56:45 -0400 Subject: [PATCH 18/38] test(jolt-field-two): un-gate baseline-parity tests after upstream fixes The Fp64::reduce_u128 truncation and S160 cross-term overflow are fixed on the PR #1684 branch (79944f2), so the differential tests no longer need to restrict baseline comparisons to the previously-correct domains: full-u128 reduction parity, 16-byte challenge-decode parity on sub-word primes, and full-range S160 multiply parity now run unconditionally. --- .../tests/limbs_signed_differential.rs | 21 ++++--------------- .../tests/solinas_words_differential.rs | 21 ++++++------------- 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/crates/jolt-field-two/tests/limbs_signed_differential.rs b/crates/jolt-field-two/tests/limbs_signed_differential.rs index 59552c14fb..d1f504677f 100644 --- a/crates/jolt-field-two/tests/limbs_signed_differential.rs +++ b/crates/jolt-field-two/tests/limbs_signed_differential.rs @@ -189,23 +189,10 @@ fn hi32_ops_match() { a160.cmp(&b160), to_base_hi32(&a160).cmp(&to_base_hi32(&b160)) ); - // Baseline's unrolled S160 mul kernel overflows u128 in its cross-term - // sum when the second limbs are large (panics in debug, wraps in - // release), so the vs-baseline mul comparison is restricted to the - // domain where the baseline is correct. Full-range correctness of our - // kernel is covered by `hi32_mul_full_range_oracle`. - let mask = (1u64 << 62) - 1; - let a160m = two::signed::S160::new( - [a160.magnitude_lo()[0], a160.magnitude_lo()[1] & mask], - a160.magnitude_hi(), - a160.is_positive(), - ); - let b160m = two::signed::S160::new( - [b160.magnitude_lo()[0], b160.magnitude_lo()[1] & mask], - b160.magnitude_hi(), - b160.is_positive(), - ); - assert_hi32_matches(a160m * b160m, to_base_hi32(&a160m) * to_base_hi32(&b160m)); + // Baseline's unrolled S160 mul kernel originally overflowed u128 in + // its cross-term sum for large second limbs; fixed on the PR #1684 + // branch, so the vs-baseline comparison runs on the full range. + assert_hi32_matches(a160 * b160, to_base_hi32(&a160) * to_base_hi32(&b160)); let a224 = two::signed::S224::new(rng.gen(), rng.gen(), rng.gen()); let b224 = two::signed::S224::new(rng.gen(), rng.gen(), rng.gen()); diff --git a/crates/jolt-field-two/tests/solinas_words_differential.rs b/crates/jolt-field-two/tests/solinas_words_differential.rs index e9426268f4..e29cd1dc16 100644 --- a/crates/jolt-field-two/tests/solinas_words_differential.rs +++ b/crates/jolt-field-two/tests/solinas_words_differential.rs @@ -24,21 +24,17 @@ macro_rules! check_prime { ($two:ty, $base:ty, $p:expr, $rng:expr) => {{ let p: u128 = $p; let bits = <$two as CanonicalEncoding>::MODULUS_BITS; - // Baseline Fp64::reduce_u128 truncates the fold's high part to u64 - // (`(v >> BITS) as u64`), so for sub-word u64 primes it reduces - // inputs >= 2^(64+BITS) incorrectly. Ours reduces correctly on the - // full domain (asserted against the oracle); baseline reduction - // parity is only asserted on its correct domain. - let base_reduces_correctly = |raw: u128| bits + 64 >= 128 || raw < (1u128 << (bits + 64)); + // Baseline Fp64::reduce_u128 originally truncated the fold's high + // part for sub-word primes; fixed on the PR #1684 branch, so parity + // is asserted on the full u128 domain. + let _ = bits; let sample = |rng: &mut ChaCha20Rng| -> ($two, $base, u128) { let raw: u128 = rng.gen(); let v = raw % p; let t = <$two as CanonicalEncoding>::from_u128_reduced(raw); assert_eq!(t.to_u128_checked(), Some(v), "reduction vs oracle"); - if base_reduces_correctly(raw) { - let b = <$base as CanonicalField>::from_canonical_u128_reduced(raw); - assert_eq!(b.to_canonical_u128(), v, "baseline reduction vs oracle"); - } + let b = <$base as CanonicalField>::from_canonical_u128_reduced(raw); + assert_eq!(b.to_canonical_u128(), v, "baseline reduction vs oracle"); let b = <$base as CanonicalField>::from_canonical_u128_checked(v).unwrap(); (t, b, v) }; @@ -119,8 +115,6 @@ macro_rules! check_prime { ); assert_eq!(ta.to_u64_checked(), ba.to_canonical_u64_checked()); let challenge: [u8; 32] = $rng.gen(); - // 16-byte decodes hit the baseline truncation bug on sub-word - // u64 primes; ours is asserted against the oracle there instead. for len in [8usize, 16, 32] { let ours = <$two as CanonicalEncoding>::from_bytes_le_reduced(&challenge[..len]); assert_eq!( @@ -133,9 +127,6 @@ macro_rules! check_prime { padded[..len].copy_from_slice(&challenge[..len]); let raw = u128::from_le_bytes(padded); assert_eq!(ours.to_u128_checked(), Some(raw % p), "decode vs oracle"); - if !base_reduces_correctly(raw) { - continue; - } } assert_eq!( ours.to_u128_checked().unwrap(), From 0961df44f6f5877d1b572e35b802bbfffb2058db Mon Sep 17 00:00:00 2001 From: acentelles Date: Wed, 29 Jul 2026 16:26:05 -0400 Subject: [PATCH 19/38] test(jolt-field-two): close entry-audit gaps - differential asserts for from_scalar_challenge_bytes on the Solinas words (all lengths) and on Fq (the Fr side already had one) - drop a dead binding left by the un-gating commit - SPEC amendments: impl_group_ops! is a third exported stamping macro; the registry has 9 offsets at this baseline, not 12; note that PseudoMersenne is defined unconditionally in algebra.rs per the file table --- crates/jolt-field-two/SPEC.md | 7 ++++--- crates/jolt-field-two/tests/bn254_differential.rs | 4 ++++ .../tests/solinas_words_differential.rs | 11 +++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/jolt-field-two/SPEC.md b/crates/jolt-field-two/SPEC.md index c73f42e040..6236aa3f19 100644 --- a/crates/jolt-field-two/SPEC.md +++ b/crates/jolt-field-two/SPEC.md @@ -54,7 +54,7 @@ capability subset with defaulted members". | Trait | Replaces | Contents | |---|---|---| -| `PseudoMersenne` | `PseudoMersenneField` + `ExtMulBackend` | `const OFFSET: u128` (bits live on `CanonicalEncoding`) + the degree-4/8 ext-mul kernel hooks with generic coefficient-formula defaults (only `Fp32` overrides, fusing i64 accumulation) | +| `PseudoMersenne` (defined unconditionally in `algebra.rs`, per the file table) | `PseudoMersenneField` + `ExtMulBackend` | `const OFFSET: u128` (bits live on `CanonicalEncoding`) + the degree-4/8 ext-mul kernel hooks with generic coefficient-formula defaults (only `Fp32` overrides, fusing i64 accumulation) | | `ExtField` | same | degree, `lift_base`, `mul_base`, coeff access, Frobenius | | `Ext2Config` | `FpExt2Config` | quadratic non-residue config (ZST pattern), `IS_NEG_ONE` fast path | | `MulBaseUnreduced` | same | tiny overridable ext×base deferred multiply | @@ -69,6 +69,7 @@ capability subset with defaulted members". **Exported stamping macros** (`ops.rs`): `impl_ring_ops!` (full operator matrix + `Zero`/`One`/`Sum`/`Product` from raw add/sub/mul/neg), +`impl_group_ops!` (the additive-only subset, for accumulator types), `impl_serde_bytes!` (canonical-checked serde over `CanonicalEncoding`, byte-format identical to baseline). Exported so third-party field implementors pay the same near-zero boilerplate we do — the `mersenne61`-style compat test consumes @@ -77,8 +78,8 @@ them as a third party would. ## Scope **Parity (functionality, not names):** everything jolt-field @ baseline does — -BN254 `Fr`/`Fq`/`WideAccumulator`; Solinas `Fp32`/`Fp64`/`Fp128` + 12 -registered prime offsets; `FpExt2/4/8` + Frobenius/Moore machinery; packed +BN254 `Fr`/`Fq`/`WideAccumulator`; Solinas `Fp32`/`Fp64`/`Fp128` + the 9 +registered prime offsets (count tracks the baseline registry); `FpExt2/4/8` + Frobenius/Moore machinery; packed NEON/AVX2/AVX-512 × {32,64,128} + packed ext + `NoPacking`; lane accumulators and fold matrices; `S64`–`S256` + hi32 variants; `Limbs`; rayon helpers; `allocative` derives. Features: `default = ["bn254"]`, `solinas`, `parallel`, diff --git a/crates/jolt-field-two/tests/bn254_differential.rs b/crates/jolt-field-two/tests/bn254_differential.rs index d213aff287..633a159270 100644 --- a/crates/jolt-field-two/tests/bn254_differential.rs +++ b/crates/jolt-field-two/tests/bn254_differential.rs @@ -232,6 +232,10 @@ fn fq_matches() { ::from_challenge_bytes(&challenge), ::from_challenge_bytes(&challenge), ); + assert_matches_fq( + ::from_scalar_challenge_bytes(&challenge), + ::from_scalar_challenge_bytes(&challenge), + ); let cfg = bincode::config::standard(); assert_eq!( diff --git a/crates/jolt-field-two/tests/solinas_words_differential.rs b/crates/jolt-field-two/tests/solinas_words_differential.rs index e29cd1dc16..5bbe1afc08 100644 --- a/crates/jolt-field-two/tests/solinas_words_differential.rs +++ b/crates/jolt-field-two/tests/solinas_words_differential.rs @@ -23,11 +23,9 @@ fn rng() -> ChaCha20Rng { macro_rules! check_prime { ($two:ty, $base:ty, $p:expr, $rng:expr) => {{ let p: u128 = $p; - let bits = <$two as CanonicalEncoding>::MODULUS_BITS; // Baseline Fp64::reduce_u128 originally truncated the fold's high // part for sub-word primes; fixed on the PR #1684 branch, so parity // is asserted on the full u128 domain. - let _ = bits; let sample = |rng: &mut ChaCha20Rng| -> ($two, $base, u128) { let raw: u128 = rng.gen(); let v = raw % p; @@ -122,6 +120,15 @@ macro_rules! check_prime { ours, "challenge derivation defaults to the reducing decode" ); + assert_eq!( + <$two as CanonicalEncoding>::from_scalar_challenge_bytes(&challenge[..len]) + .to_u128_checked(), + Some( + <$base as CanonicalRepr>::from_scalar_challenge_bytes(&challenge[..len]) + .to_canonical_u128() + ), + "scalar challenge derivation diverges from baseline" + ); if len <= 16 { let mut padded = [0u8; 16]; padded[..len].copy_from_slice(&challenge[..len]); From 49f16214eb8d566919911895b6ab86ab05f53d8a Mon Sep 17 00:00:00 2001 From: acentelles Date: Thu, 30 Jul 2026 15:42:55 -0400 Subject: [PATCH 20/38] test(jolt-field-two): reconcile differentials with the CanonicalBytes split The baseline's byte surface (NUM_BYTES, to_bytes_le) moved from CanonicalRepr to its new CanonicalBytes supertrait; import both where the oracles compare encodings. --- crates/jolt-field-two/tests/bn254_differential.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/jolt-field-two/tests/bn254_differential.rs b/crates/jolt-field-two/tests/bn254_differential.rs index 633a159270..62d9bf869e 100644 --- a/crates/jolt-field-two/tests/bn254_differential.rs +++ b/crates/jolt-field-two/tests/bn254_differential.rs @@ -9,7 +9,9 @@ use jolt_field as base; use jolt_field_two as two; -use base::{Accumulator as _, CanonicalRepr, FieldCore, FromPrimitiveInt, RingCore}; +use base::{ + Accumulator as _, CanonicalBytes, CanonicalRepr, FieldCore, FromPrimitiveInt, RingCore, +}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; use two::{Accumulator as _, CanonicalEncoding, Field as _, Ring}; From e1cbc17a3b31bbb3593d0242edbd0bd74a033c08 Mon Sep 17 00:00:00 2001 From: acentelles Date: Thu, 30 Jul 2026 17:38:44 -0400 Subject: [PATCH 21/38] feat(jolt-field-two): fp128 two-limb Solinas field (checkpoint 5) One hand-written module (528 counted LOC vs the 700 budget) porting the baseline prime/fp128/{core,add_sub,mul,reduce,wide,primes,traits}: [u64; 2] lo/hi representation, branchless carry-chain add/sub, schoolbook 2x2 mul/sqr with the AArch64 inline-asm kernels kept verbatim (1.29x on M4 per the baseline's own measurement; an AArch64-only unit test cross-checks asm vs portable on all four offsets), C = 2^a +/- 1 shift specialization, <=10-limb Solinas fold, Fermat inversion, rejection-sampling random, and the four registered 128-bit primes re-exported under their baseline names. Every reduction step carries a re-derived bound comment. One sharpening over the baseline's argument: fold2_canonicalize is exact and canonical for any u64 third limb given only C < 2^32 (overflow leaves the partial sum below 2^96, so the single +C correction cannot re-overflow); the C(C+1) < P assert is kept and documented as implied for the fused paths. Dropped with evidence recorded in SPEC.md: the add/sub asm kernels (~470 lines, no in-tree benchmark, same algorithm as the branchless portable path), mul_add and its asm, mul_wide_limbs, from_i64_const (akita-only consumers, drop already approved). Differential suite: per-offset parity vs the baseline Fp128 (ops owned+by-ref, algebra identities, bincode wire bytes, canonical and wrong-length rejection, byte/num_bits/u64 views, challenges at 8/16/32), an independent 4x64-limb schoolbook + long-division oracle, boundary sets, and all-max limbs at every fold length 0-10. Also fixes tests/solinas_words_differential.rs, which did not compile under --all-features since the CanonicalBytes split (the reconcile pass ran without the solinas feature - the recurring feature-gate lesson). 39/39 tests, clippy all-features clean, no-default-features and solinas-only builds clean. --- Cargo.lock | 2 +- crates/jolt-field-two/SPEC.md | 28 + crates/jolt-field-two/src/lib.rs | 3 +- crates/jolt-field-two/src/solinas/fp128.rs | 821 ++++++++++++++++++ crates/jolt-field-two/src/solinas/mod.rs | 23 +- crates/jolt-field-two/src/solinas/word.rs | 5 +- .../tests/solinas_fp128_differential.rs | 456 ++++++++++ .../tests/solinas_words_differential.rs | 5 +- 8 files changed, 1332 insertions(+), 11 deletions(-) create mode 100644 crates/jolt-field-two/src/solinas/fp128.rs create mode 100644 crates/jolt-field-two/tests/solinas_fp128_differential.rs diff --git a/Cargo.lock b/Cargo.lock index c6ca388f33..de4d9314ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3322,7 +3322,7 @@ dependencies = [ "bincode 2.0.1", "jolt-field", "num-traits", - "rand 0.8.5", + "rand 0.8.7", "rand_chacha 0.3.1", "rand_core 0.6.4", "rayon", diff --git a/crates/jolt-field-two/SPEC.md b/crates/jolt-field-two/SPEC.md index 6236aa3f19..a71399b55f 100644 --- a/crates/jolt-field-two/SPEC.md +++ b/crates/jolt-field-two/SPEC.md @@ -92,6 +92,34 @@ and fold matrices; `S64`–`S256` + hi32 variants; `Limbs`; rayon helpers; **Dropped (approved):** `akita` bootstrap feature/module, `MontgomeryConstants`. +**Dropped-specialization evidence (checkpoint 5, fp128):** + +- **Kept:** the baseline's AArch64 inline-asm `mul`/`sqr` kernels — the only + per-arch specializations with recorded evidence (1.29x throughput on Apple + M4, per the baseline's own doc comment) and the prover hot path. An + AArch64-only unit test in `fp128.rs` cross-checks them against the portable + fold on random + boundary inputs for all four registered offsets. +- **Dropped:** AArch64 and x86-64 inline-asm add/sub kernels + (`add_raw_{aarch64,x86_64}_{imm,reg}` + dispatchers, ~470 source lines). + Same carry-chain algorithm as the portable path with hand-scheduled flag + flow (`ccmp`/`sbb`-mask selects); no benchmark recorded in-tree, only + qualitative comments. The portable path is branchless and compiles to a + near-identical adds/adcs/csel (resp. add/adc/cmov) sequence. The x86-64 + imm-vs-reg dispatch subtlety (sign-extended imm32 unusable for C ≥ 2^31, + i.e. `Prime128OffsetA7F7`) dies with it. Baseline x86-64 `mul` was already + portable — nothing dropped there. +- **Dropped:** the AArch64 `mul_add` asm kernel together with the + `mul_add`/`add_128_into_256` fused multiply-add surface: no consumer + anywhere in the parity scope (only baseline fp128's own tests call it). +- **Dropped:** `mul_wide_limbs` (generic loop + M/OUT-unrolled + hot-path specializations, ~270 lines): its only workspace consumer is + `jolt-prover-legacy`'s akita field glue, and the akita bootstrap is + dropped (approved above). `mul_wide`/`mul_wide_u64`/`mul_wide_u128` and + the ≤10-limb `solinas_reduce` — the surfaces the in-scope unreduced + accumulators use — are ported. +- **Dropped:** `from_i64_const` (const-evaluable embedding): akita-only + (its `MONTGOMERY_R` constants). + ## Design pillars 1. **Const-generic scalar core**: `Fp64` etc., fold constants diff --git a/crates/jolt-field-two/src/lib.rs b/crates/jolt-field-two/src/lib.rs index 43cd3be4dc..c32c0e869b 100644 --- a/crates/jolt-field-two/src/lib.rs +++ b/crates/jolt-field-two/src/lib.rs @@ -37,7 +37,8 @@ pub use num_traits::{One, Zero}; #[cfg(feature = "solinas")] pub use solinas::{ balanced_digit_lut, is_registered_prime_offset, pseudo_mersenne_modulus, - registered_prime_offset_spec, Fp32, Fp64, Prime24Offset3, Prime30Offset35, Prime31Offset19, + registered_prime_offset_spec, Fp128, Fp32, Fp64, Prime128Offset159, Prime128Offset2355, + Prime128Offset275, Prime128OffsetA7F7, Prime24Offset3, Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime48Offset59, Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, PRIME_OFFSET_IMPLEMENTED_MAX_BITS, PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, }; diff --git a/crates/jolt-field-two/src/solinas/fp128.rs b/crates/jolt-field-two/src/solinas/fp128.rs new file mode 100644 index 0000000000..684f129342 --- /dev/null +++ b/crates/jolt-field-two/src/solinas/fp128.rs @@ -0,0 +1,821 @@ +//! Two-limb pseudo-Mersenne prime field: `p = 2^128 − C` with `C < 2^32`, +//! stored as `[u64; 2]` little-endian limbs ([`Fp128`]). +//! +//! Solinas-style two-fold reduction, no Montgomery form: a 256-bit product +//! is folded twice through `2^128 ≡ C (mod p)` and canonicalized with one +//! conditional add. `C < 2^32` keeps every fold term inside two limbs and is +//! const-asserted in exactly one place ([`Fp128::C`]), together with +//! `C(C+1) < P` (implied by `C < 2^32`, kept as belt-and-suspenders). +//! +//! Unlike the word-sized fields, the modulus is NOT primality-checked at +//! compile time (2^128 trial division is not CTFE-viable); instantiating a +//! composite odd modulus yields a ring whose `inverse` is meaningless. +//! +//! On AArch64 the multiply and squaring kernels use inline assembly +//! (benchmarked at 1.29x throughput vs the portable path on Apple M4); every +//! other path, and every path on other architectures, is portable Rust. + +use super::word::mul64_wide; +use crate::PseudoMersenne; +use crate::{CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; +use rand_core::RngCore; +#[cfg(target_arch = "aarch64")] +use std::arch::asm; + +/// Pack two `u64` limbs into little-endian `[lo, hi]`. +#[inline(always)] +const fn pack(lo: u64, hi: u64) -> [u64; 2] { + [lo, hi] +} + +/// Split a `u128` into little-endian `[u64; 2]` limbs. +#[inline(always)] +const fn split(x: u128) -> [u64; 2] { + [x as u64, (x >> 64) as u64] +} + +/// Join little-endian `[u64; 2]` limbs into a `u128`. +#[inline(always)] +const fn join(x: [u64; 2]) -> u128 { + x[0] as u128 | (x[1] as u128) << 64 +} + +/// 128-bit prime field element for primes of the form `p = 2^128 − c`, +/// stored as `[u64; 2]` little-endian limbs holding the canonical +/// representative in `[0, p)`. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[repr(transparent)] +pub struct Fp128(pub(crate) [u64; 2]); + +impl Fp128

{ + /// Offset `c = 2^128 − P`. Instantiating with a modulus that violates + /// the Solinas preconditions is a compile-time error. + pub const C: u128 = { + let c = 0u128.wrapping_sub(P); + assert!(P != 0, "modulus must be nonzero"); + assert!(P & 1 == 1, "modulus must be odd"); + assert!(c < (1 << 32), "C must be < 2^32 (two-limb fold terms)"); + assert!( + c * (c + 1) < P, + "C(C+1) < P required for fused canonicalize" + ); + c + }; + + /// Low 64 bits of `C` (always equals `C` since `C < 2^32`). + pub const C_LO: u64 = Self::C as u64; + + /// `+1` means `C = 2^a + 1`, `-1` means `C = 2^a − 1`, `0` means generic. + /// (`C < 2^32` is const-asserted, so `C + 1` cannot overflow and the + /// shift is at most 32.) + const C_SHIFT_KIND: i8 = { + let c = Self::C_LO; + if c > 1 && (c - 1).is_power_of_two() { + 1 + } else if (c + 1).is_power_of_two() { + -1 + } else { + 0 + } + }; + const C_SHIFT: u32 = if Self::C_SHIFT_KIND == 1 { + (Self::C_LO - 1).trailing_zeros() + } else if Self::C_SHIFT_KIND == -1 { + (Self::C_LO + 1).trailing_zeros() + } else { + 0 + }; + + /// Widening multiply by `C`: returns `C·x` as `(lo, hi)`. + /// + /// For `C = 2^a ± 1` this is shift/add or shift/sub only; otherwise a + /// generic widening multiply. Bound: `C·x < 2^32 · 2^64 = 2^96`, so + /// `hi ≤ C − 1 < 2^32`. + #[inline(always)] + fn mul_c_wide(x: u64) -> (u64, u64) { + if Self::C_SHIFT_KIND == 1 { + let v = ((x as u128) << Self::C_SHIFT) + x as u128; + (v as u64, (v >> 64) as u64) + } else if Self::C_SHIFT_KIND == -1 { + let v = ((x as u128) << Self::C_SHIFT) - x as u128; + (v as u64, (v >> 64) as u64) + } else { + mul64_wide(Self::C_LO, x) + } + } + + /// Fold 2 + canonicalize: reduce `[t0, t1] + t2·2^128` into `[0, p)`. + /// + /// Valid for ANY `u64` `t2` given `C < 2^32`. Let `v = t + C·t2` + /// (mathematical, not mod 2^128) with `t = [t0, t1] < 2^128`: + /// + /// - `v < 2^128 + C·(2^64 − 1) < 2^128 + 2^96 < 2^129`, so the two-limb + /// add wraps at most once (`overflow` is a single bit). + /// - **No overflow** (`v < 2^128`): `s = v` and the standard + /// canonicalize applies — `s + C` carries (`carry3`) iff `s ≥ p`, + /// in which case the wrapped `s + C` equals `s − p < C < p` (since + /// `s < 2^128 = p + C`). + /// - **Overflow** (`v ≥ 2^128`): `s = v − 2^128 < C·t2`, and the correct + /// residue is `s + C` (because `2^128 ≡ C mod p`). Since + /// `s + C < C·(t2 + 1) ≤ C·2^64 < 2^96 < p`, that value is already + /// canonical and the add does not carry. + /// + /// Hence `if overflow | carry3 { s + C } else { s }` is correct in both + /// cases, fusing the overflow correction with canonicalization. + #[inline(always)] + fn fold2_canonicalize(t0: u64, t1: u64, t2: u64) -> [u64; 2] { + let (ct2_lo, ct2_hi) = Self::mul_c_wide(t2); + + let (s0, carry0) = t0.overflowing_add(ct2_lo); + let (s1a, carry1a) = t1.overflowing_add(ct2_hi); + let (s1, carry1b) = s1a.overflowing_add(carry0 as u64); + let overflow = carry1a | carry1b; + + let (r0, carry2) = s0.overflowing_add(Self::C_LO); + let (r1, carry3) = s1.overflowing_add(carry2 as u64); + + pack( + if overflow | carry3 { r0 } else { s0 }, + if overflow | carry3 { r1 } else { s1 }, + ) + } + + /// Fold 1 for exactly 4 limbs: `[r0,r1] + C·[r2,r3]` → 3 limbs `[t0,t1,t2]`, + /// then [`fold2_canonicalize`](Self::fold2_canonicalize). + /// + /// Bounds (any 256-bit input): `t = lo128 + C·hi128 ≤ (C+1)(2^128 − 1) + /// < (C+1)·2^128`, so `t2 = t >> 128 ≤ C`. Per limb: `t0_sum ≤ + /// 2(2^64 − 1)` (carry ≤ 1); `t1_sum ≤ 3(2^64 − 1) + 1 < 2^66` (its high + /// part ≤ 2, kept via the full `u128` shift — never truncated); + /// `t2_sum ≤ (C − 1) + 2 < 2^64` (no fourth limb, debug-asserted). + #[inline(always)] + fn reduce_4(r0: u64, r1: u64, r2: u64, r3: u64) -> [u64; 2] { + let (cr2_lo, cr2_hi) = Self::mul_c_wide(r2); + let (cr3_lo, cr3_hi) = Self::mul_c_wide(r3); + + let t0_sum = r0 as u128 + cr2_lo as u128; + let t0 = t0_sum as u64; + let carryf = (t0_sum >> 64) as u64; + + let t1_sum = r1 as u128 + cr2_hi as u128 + cr3_lo as u128 + carryf as u128; + let t1 = t1_sum as u64; + + let t2_sum = cr3_hi as u128 + (t1_sum >> 64); + let t2 = t2_sum as u64; + debug_assert_eq!(t2_sum >> 64, 0); + + Self::fold2_canonicalize(t0, t1, t2) + } + + /// Carry-chain add with fused reduction. + /// + /// For `a, b < p`: if the two-limb add wraps (`overflow`), the real sum + /// is `s + 2^128 ≡ s + C`, and `s = a + b − 2^128 < 2p − 2^128 = p − C`, + /// so `s + C < p` is already canonical (and `carry3 = 0`). Without wrap, + /// `s + C` carries iff `s ≥ p`, and then the wrapped value is + /// `s − p ≤ p − 2`. Both cases select `r` on `overflow | carry3`. + #[inline(always)] + fn add_raw(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + let (s0, carry0) = a[0].overflowing_add(b[0]); + let (s1a, carry1a) = a[1].overflowing_add(b[1]); + let (s1, carry1b) = s1a.overflowing_add(carry0 as u64); + let overflow = carry1a | carry1b; + + let (r0, carry2) = s0.overflowing_add(Self::C_LO); + let (r1, carry3) = s1.overflowing_add(carry2 as u64); + + pack( + if overflow | carry3 { r0 } else { s0 }, + if overflow | carry3 { r1 } else { s1 }, + ) + } + + /// Subtract with borrow-conditional modulus add-back (`a − b + p` when + /// `a < b`; the wrapped difference plus `p` cannot wrap again since + /// `a − b + 2^128 + p − 2^128 = a − b + p < p`). + #[inline(always)] + fn sub_raw(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + let (diff, borrow) = join(a).overflowing_sub(join(b)); + split(if borrow { diff.wrapping_add(P) } else { diff }) + } + + #[inline(always)] + fn mul_raw(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + #[cfg(target_arch = "aarch64")] + { + Self::mul_raw_aarch64(a, b) + } + #[cfg(not(target_arch = "aarch64"))] + { + Self::mul_raw_portable(a, b) + } + } + + /// Portable multiply: schoolbook 2×2 widening product, then the two + /// Solinas folds. On AArch64 this is compiled only under `cfg(test)` + /// as the differential oracle for the assembly kernel. + #[cfg(any(not(target_arch = "aarch64"), test))] + #[inline(always)] + fn mul_raw_portable(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + let [r0, r1, r2, r3] = Self(a).mul_wide(Self(b)); + Self::reduce_4(r0, r1, r2, r3) + } + + /// 35-instruction AArch64 inline-asm multiply with Solinas reduction. + /// + /// Saves 6 instructions vs LLVM's codegen of the portable path by: + /// - Fold-1 carry chain: direct adds/adcs/adc (5 vs 8 instructions), + /// avoiding intermediate cset/cinc shuttling of carries. + /// - Fold-2 + canonicalize: `ccmp` folds the overflow predicate with + /// the ≥p check (8 vs 10 instructions). + /// + /// Benchmarked at 1.29x throughput improvement on Apple M4. + #[cfg(target_arch = "aarch64")] + #[inline(always)] + fn mul_raw_aarch64(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { + let out_lo: u64; + let out_hi: u64; + // SAFETY: register-only inline asm (pure, nomem, nostack) over plain + // integer operands; the carry/flag flow implements exactly the + // portable `mul_wide` + `reduce_4` algorithm, and `C < 2^32` (const- + // asserted) guarantees the fold-2 `mul {p11h}, {c}` cannot overflow. + unsafe { + asm!( + // Schoolbook 2×2 → 256-bit product [r0,r1,r2,r3] + "mul {p00l}, {a0}, {b0}", + "umulh {p00h}, {a0}, {b0}", + "mul {p01l}, {a0}, {b1}", + "umulh {p01h}, {a0}, {b1}", + "mul {p10l}, {a1}, {b0}", + "umulh {p10h}, {a1}, {b0}", + "mul {p11l}, {a1}, {b1}", + "umulh {p11h}, {a1}, {b1}", + + // Carry accumulation into [r0=p00l, r1=p00h, r2=p01h, r3=p11h] + "adds {p00h}, {p00h}, {p01l}", + "cset {p01l:w}, hs", + "adds {p01h}, {p01h}, {p10h}", + "cset {p10h:w}, hs", + "adds {p01h}, {p01h}, {p11l}", + "cinc {p10h}, {p10h}, hs", + "adds {p00h}, {p00h}, {p10l}", + "adcs {p01h}, {p01h}, {p01l}", + "adc {p11h}, {p11h}, {p10h}", + + // Fold-1: [t0,t1,t2] = [r0,r1] + C·[r2,r3] + "mul {p01l}, {p01h}, {c}", + "umulh {p10l}, {p01h}, {c}", + "mul {p10h}, {p11h}, {c}", + "umulh {p11l}, {p11h}, {c}", + + "adds {p00l}, {p00l}, {p01l}", + "adcs {p00h}, {p00h}, {p10l}", + "cset {p01h:w}, hs", + "adds {p00h}, {p00h}, {p10h}", + "adc {p11h}, {p11l}, {p01h}", + + // Fold-2 + canonicalize via ccmp (C < 2^32 ⇒ C·t2 fits in 64 bits) + "mul {p01l}, {p11h}, {c}", + "adds {p00l}, {p00l}, {p01l}", + "adcs {p00h}, {p00h}, xzr", + "cset {p01l:w}, hs", + "adds {p10l}, {p00l}, {c}", + "adcs {p10h}, {p00h}, xzr", + "ccmp {p01l:w}, #0, #0, lo", + "csel {out_lo}, {p10l}, {p00l}, ne", + "csel {out_hi}, {p10h}, {p00h}, ne", + + a0 = in(reg) a[0], + a1 = in(reg) a[1], + b0 = in(reg) b[0], + b1 = in(reg) b[1], + c = in(reg) Self::C_LO, + p00l = out(reg) _, + p00h = out(reg) _, + p01l = out(reg) _, + p01h = out(reg) _, + p10l = out(reg) _, + p10h = out(reg) _, + p11l = out(reg) _, + p11h = out(reg) _, + out_lo = lateout(reg) out_lo, + out_hi = lateout(reg) out_hi, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } + + #[inline(always)] + fn sqr_raw(a: [u64; 2]) -> [u64; 2] { + #[cfg(target_arch = "aarch64")] + { + Self::sqr_raw_aarch64(a) + } + #[cfg(not(target_arch = "aarch64"))] + { + Self::sqr_raw_portable(a) + } + } + + /// Portable squaring (see [`mul_raw_portable`](Self::mul_raw_portable) + /// for the AArch64 `cfg(test)` role). + #[cfg(any(not(target_arch = "aarch64"), test))] + #[inline(always)] + fn sqr_raw_portable(a: [u64; 2]) -> [u64; 2] { + let [r0, r1, r2, r3] = Self(a).sqr_wide(); + Self::reduce_4(r0, r1, r2, r3) + } + + /// Squaring schoolbook with the cross term doubled: 3 widening muls. + /// + /// Row bounds: `row1 = p00_hi + 2·p01_lo ≤ 3(2^64 − 1) < 2^66` (carry ≤ + /// 2), `row2 = 2·p01_hi + p11_lo + carry1 < 2^66` (carry ≤ 2), and the + /// top limb is exact because `a² < 2^256` (debug-asserted). + #[cfg(any(not(target_arch = "aarch64"), test))] + #[inline(always)] + fn sqr_wide(self) -> [u64; 4] { + let (a0, a1) = (self.0[0], self.0[1]); + let (p00_lo, p00_hi) = mul64_wide(a0, a0); + let (p01_lo, p01_hi) = mul64_wide(a0, a1); + let (p11_lo, p11_hi) = mul64_wide(a1, a1); + + let row1 = p00_hi as u128 + (p01_lo as u128) * 2; + let r0 = p00_lo; + let r1 = row1 as u64; + let carry1 = (row1 >> 64) as u64; + + let row2 = (p01_hi as u128) * 2 + p11_lo as u128 + carry1 as u128; + let r2 = row2 as u64; + let carry2 = (row2 >> 64) as u64; + + let row3 = p11_hi as u128 + carry2 as u128; + let r3 = row3 as u64; + debug_assert_eq!(row3 >> 64, 0); + + [r0, r1, r2, r3] + } + + /// 31-instruction AArch64 inline-asm squaring with Solinas reduction: + /// 3 widening multiplies (vs 4 for general mul), the cross term doubled + /// via shifted-register operands, then the same fold-1 + ccmp + /// canonicalize as [`mul_raw_aarch64`](Self::mul_raw_aarch64). + #[cfg(target_arch = "aarch64")] + #[inline(always)] + fn sqr_raw_aarch64(a: [u64; 2]) -> [u64; 2] { + let out_lo: u64; + let out_hi: u64; + // SAFETY: register-only inline asm (pure, nomem, nostack) over plain + // integer operands; implements exactly `sqr_wide` + `reduce_4`, with + // the same `C < 2^32` fold-2 invariant as `mul_raw_aarch64`. + unsafe { + asm!( + // Squaring schoolbook: 3 widening muls + "mul {p00l}, {a0}, {a0}", + "umulh {p00h}, {a0}, {a0}", + "mul {p01l}, {a0}, {a1}", + "umulh {p01h}, {a0}, {a1}", + "mul {p11l}, {a1}, {a1}", + "umulh {p11h}, {a1}, {a1}", + + // Carry accumulation with doubled cross term + // row1 = p00h + 2*p01l, row2 = 2*p01h + p11l, r3 = p11h + carries + "lsr {t0}, {p01l}, #63", + "lsr {t1}, {p01h}, #63", + "adds {p01h}, {p11l}, {p01h}, lsl #1", + "cinc {t1}, {t1}, hs", + "adds {p00h}, {p00h}, {p01l}, lsl #1", + "adcs {p01h}, {p01h}, {t0}", + "adc {p11h}, {p11h}, {t1}", + + // At this point: r0=p00l, r1=p00h, r2=p01h, r3=p11h + + // Fold-1: [t0,t1,t2] = [r0,r1] + C·[r2,r3] + "mul {t0}, {p01h}, {c}", + "umulh {t1}, {p01h}, {c}", + "mul {p01l}, {p11h}, {c}", + "umulh {p11l}, {p11h}, {c}", + + "adds {p00l}, {p00l}, {t0}", + "adcs {p00h}, {p00h}, {t1}", + "cset {t0:w}, hs", + "adds {p00h}, {p00h}, {p01l}", + "adc {p11h}, {p11l}, {t0}", + + // Fold-2 + canonicalize via ccmp (C < 2^32 ⇒ C·t2 fits in 64 bits) + "mul {t0}, {p11h}, {c}", + "adds {p00l}, {p00l}, {t0}", + "adcs {p00h}, {p00h}, xzr", + "cset {t0:w}, hs", + "adds {t1}, {p00l}, {c}", + "adcs {p01l}, {p00h}, xzr", + "ccmp {t0:w}, #0, #0, lo", + "csel {out_lo}, {t1}, {p00l}, ne", + "csel {out_hi}, {p01l}, {p00h}, ne", + + a0 = in(reg) a[0], + a1 = in(reg) a[1], + c = in(reg) Self::C_LO, + p00l = out(reg) _, + p00h = out(reg) _, + p01l = out(reg) _, + p01h = out(reg) _, + p11l = out(reg) _, + p11h = out(reg) _, + t0 = out(reg) _, + t1 = out(reg) _, + out_lo = lateout(reg) out_lo, + out_hi = lateout(reg) out_hi, + options(pure, nomem, nostack), + ); + } + pack(out_lo, out_hi) + } + + fn pow_u128(self, mut exp: u128) -> Self { + let mut base = self; + let mut acc = ::one(); + while exp > 0 { + if (exp & 1) == 1 { + acc *= base; + } + base = Self(Self::sqr_raw(base.0)); + exp >>= 1; + } + acc + } + + /// Create from a canonical representative in `[0, P)`. + #[inline] + pub fn from_canonical_u128(x: u128) -> Self { + debug_assert!(x < P); + Self(split(x)) + } + + /// Return the canonical representative in `[0, P)`. + #[inline] + pub fn to_canonical_u128(self) -> u128 { + join(self.0) + } + + /// Extract the canonical `[lo, hi]` limb representation. + #[inline(always)] + pub fn to_limbs(self) -> [u64; 2] { + self.0 + } + + /// 128×128 → 256-bit widening multiply, **no reduction**. + /// + /// Returns `[r0, r1, r2, r3]`: the schoolbook 2×2 portion of the Solinas + /// multiply without the reduction fold. Cost: 4 widening `mul64`. Row + /// bounds: each row sums at most three limb halves plus a carry ≤ 2, so + /// every row fits `u128` with its high part ≤ 2 (kept via the full + /// `u128` shift); the top limb is exact because `a·b < 2^256` + /// (debug-asserted). + #[inline(always)] + pub fn mul_wide(self, other: Self) -> [u64; 4] { + let (a0, a1) = (self.0[0], self.0[1]); + let (b0, b1) = (other.0[0], other.0[1]); + let (p00_lo, p00_hi) = mul64_wide(a0, b0); + let (p01_lo, p01_hi) = mul64_wide(a0, b1); + let (p10_lo, p10_hi) = mul64_wide(a1, b0); + let (p11_lo, p11_hi) = mul64_wide(a1, b1); + + let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; + let r0 = p00_lo; + let r1 = row1 as u64; + let carry1 = (row1 >> 64) as u64; + + let row2 = p01_hi as u128 + p10_hi as u128 + p11_lo as u128 + carry1 as u128; + let r2 = row2 as u64; + let carry2 = (row2 >> 64) as u64; + + let row3 = p11_hi as u128 + carry2 as u128; + let r3 = row3 as u64; + debug_assert_eq!(row3 >> 64, 0); + + [r0, r1, r2, r3] + } + + /// 128×64 → 192-bit widening multiply, **no reduction**. + /// + /// Returns `[lo, mid, hi]`. Cost: 2 widening `mul64`. Bounds: + /// `mid ≤ 2(2^64 − 1)` (carry ≤ 1) and `hi ≤ (2^64 − 2) + 1` cannot + /// overflow because `a·b < 2^192`. + #[inline(always)] + pub fn mul_wide_u64(self, other: u64) -> [u64; 3] { + let (a0, a1) = (self.0[0], self.0[1]); + let (p0_lo, p0_hi) = mul64_wide(a0, other); + let (p1_lo, p1_hi) = mul64_wide(a1, other); + let mid = p0_hi as u128 + p1_lo as u128; + let hi = p1_hi + (mid >> 64) as u64; + [p0_lo, mid as u64, hi] + } + + /// 128×128 → 256-bit widening multiply with a raw `u128` operand, + /// **no reduction**. + #[inline(always)] + pub fn mul_wide_u128(self, other: u128) -> [u64; 4] { + self.mul_wide(Self(split(other))) + } + + /// Reduce an arbitrary-width little-endian limb array to a canonical + /// field element via iterated Solinas folding. + /// + /// Each fold splits at the 128-bit boundary and replaces `hi · 2^128` + /// with `hi · C`, shrinking the value by one limb per iteration. + /// Supports 0–10 input limbs (up to 640 bits). + /// + /// # Panics + /// + /// Panics if `limbs.len() > 10`. + #[inline(always)] + pub fn solinas_reduce(limbs: &[u64]) -> Self { + match limbs.len() { + 0 => Self(pack(0, 0)), + // A single limb is always canonical: 2^64 < p. + 1 => Self(pack(limbs[0], 0)), + // Any u128 < 2^128 = p + C needs at most one subtraction of p. + 2 => Self::from_u128_reduced(join([limbs[0], limbs[1]])), + // fold2_canonicalize accepts any u64 third limb (see its bounds). + 3 => Self(Self::fold2_canonicalize(limbs[0], limbs[1], limbs[2])), + 4 => Self(Self::reduce_4(limbs[0], limbs[1], limbs[2], limbs[3])), + 5 => { + // One fold 320 → 256 bits, then reduce_4. Limb bounds: + // s0 ≤ 2(2^64−1) (carry ≤ 1); s1 ≤ 2(2^64−1) + (C−1) + 1 + // (carry ≤ 2); s2 ≤ (C−1) + (2^64−1) + 2 (carry ≤ 1); + // s3 = c4_hi + carry ≤ C < 2^64, so no fifth limb + // (debug-asserted). All carries use full u128 shifts. + let (l0, l1, l2, l3, l4) = (limbs[0], limbs[1], limbs[2], limbs[3], limbs[4]); + let (c2_lo, c2_hi) = Self::mul_c_wide(l2); + let (c3_lo, c3_hi) = Self::mul_c_wide(l3); + let (c4_lo, c4_hi) = Self::mul_c_wide(l4); + + let s0 = l0 as u128 + c2_lo as u128; + let s1 = l1 as u128 + c2_hi as u128 + c3_lo as u128 + (s0 >> 64); + let s2 = c3_hi as u128 + c4_lo as u128 + (s1 >> 64); + let s3 = c4_hi as u128 + (s2 >> 64); + debug_assert_eq!(s3 >> 64, 0); + + Self(Self::reduce_4(s0 as u64, s1 as u64, s2 as u64, s3 as u64)) + } + n => { + assert!(n <= 10, "solinas_reduce supports at most 10 limbs"); + let mut buf = [0u64; 11]; + buf[..n].copy_from_slice(limbs); + let mut len = n; + let c = Self::C_LO; + + // Each pass computes C·buf[2..len] + buf[0..2]. With + // H < 2^{64(len−2)} and L < 2^128, the result is + // < 2^{64(len−2)+32} + 2^128 < 2^{64(len−1)} for len ≥ 4, + // so it fits len−1 limbs and the carry chain dies out + // (debug-asserted below). + while len > 5 { + let high_len = len - 2; + let mut next = [0u64; 11]; + + let mut carry: u64 = 0; + for i in 0..high_len { + let wide = c as u128 * buf[i + 2] as u128 + carry as u128; + next[i] = wide as u64; + carry = (wide >> 64) as u64; + } + // carry ≤ C − 1: the top partial product's high half. + next[high_len] = carry; + + let s0 = next[0] as u128 + buf[0] as u128; + next[0] = s0 as u64; + let s1 = next[1] as u128 + buf[1] as u128 + (s0 >> 64); + next[1] = s1 as u64; + let mut c_out = (s1 >> 64) as u64; + for limb in &mut next[2..=high_len] { + if c_out == 0 { + break; + } + let s = *limb as u128 + c_out as u128; + *limb = s as u64; + c_out = (s >> 64) as u64; + } + debug_assert_eq!(c_out, 0); + + buf = next; + len -= 1; + while len > 5 && buf[len - 1] == 0 { + len -= 1; + } + } + + Self::solinas_reduce(&buf[..len]) + } + } + } +} + +crate::impl_ring_ops!(impl[const P: u128] Fp128

{ + add(a, b): Fp128(Self::add_raw(a.0, b.0)), + sub(a, b): Fp128(Self::sub_raw(a.0, b.0)), + mul(a, b): Fp128(Self::mul_raw(a.0, b.0)), + neg(a): Fp128(Self::sub_raw(pack(0, 0), a.0)), + zero: Fp128(pack(0, 0)), + // P > 1 is implied by the C asserts (odd and C(C+1) < P). + one: Fp128(pack(1, 0)), +}); + +impl std::fmt::Display for Fp128

{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_canonical_u128()) + } +} + +impl Ring for Fp128

{ + /// Any u64 is canonical: `p = 2^128 − C > 2^64`, so no reduction path. + #[inline(always)] + fn from_u64(v: u64) -> Self { + Self(pack(v, 0)) + } + + #[inline(always)] + fn from_i64(v: i64) -> Self { + if v >= 0 { + Self::from_u64(v as u64) + } else { + -Self::from_u64(v.unsigned_abs()) + } + } + + #[inline(always)] + fn from_u128(v: u128) -> Self { + Self::from_u128_reduced(v) + } + + #[inline(always)] + fn from_i128(v: i128) -> Self { + if v >= 0 { + Self::from_u128(v as u128) + } else { + -Self::from_u128(v.unsigned_abs()) + } + } + + #[inline(always)] + fn square(&self) -> Self { + Self(Self::sqr_raw(self.0)) + } +} + +impl Field for Fp128

{ + #[inline(always)] + fn inverse(&self) -> Option { + let inv = self.inv_or_zero(); + if num_traits::Zero::is_zero(self) { + None + } else { + Some(inv) + } + } + + /// Fermat inversion with branchless zero-masking. + #[inline(always)] + fn inv_or_zero(self) -> Self { + let candidate = self.pow_u128(P.wrapping_sub(2)); + let v = join(self.0); + let nz = ((v | v.wrapping_neg()) >> 127) & 1; + let mask = 0u128.wrapping_sub(nz); + Self(split(join(candidate.0) & mask)) + } + + /// Rejection sampling: draws `(lo, hi)` until the value is canonical. + /// The rejection probability is `C / 2^128 < 2^-96` per draw. + #[inline(always)] + fn random(rng: &mut R) -> Self { + loop { + let lo = rng.next_u64(); + let hi = rng.next_u64(); + if join(pack(lo, hi)) < P { + return Self(pack(lo, hi)); + } + } + } + + /// Halving via shift: `(x + (x odd)·p) / 2`, computed as + /// `(x >> 1) + (x & 1)·(p + 1)/2`, which stays below `p` (no overflow): + /// for odd `x ≤ p − 2`, the sum is at most `(p − 3)/2 + (p + 1)/2 = p − 1`. + #[inline] + fn half(self) -> Self { + let x = join(self.0); + Self(split((x >> 1) + (x & 1) * ((P >> 1) + 1))) + } + + #[inline] + fn two_inv() -> Self { + ::one().half() + } +} + +impl CanonicalEncoding for Fp128

{ + const NUM_BYTES: usize = 16; + // C < 2^32 implies p > 2^127, so the modulus is exactly 128 bits. + const MODULUS_BITS: u32 = 128; + + #[inline(always)] + fn to_bytes_le(&self, out: &mut [u8]) { + assert_eq!(out.len(), Self::NUM_BYTES); + out.copy_from_slice(&join(self.0).to_le_bytes()); + } + + #[inline(always)] + fn from_bytes_le_reduced(bytes: &[u8]) -> Self { + if bytes.len() <= 16 { + let mut padded = [0u8; 16]; + padded[..bytes.len()].copy_from_slice(bytes); + return Self::from_u128(u128::from_le_bytes(padded)); + } + crate::solinas::reduce_le_bytes_mod_order(bytes) + } + + #[inline] + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + let arr: [u8; 16] = bytes.try_into().ok()?; + Self::from_u128_checked(u128::from_le_bytes(arr)) + } + + #[inline] + fn to_u128_checked(&self) -> Option { + Some(join(self.0)) + } + + #[inline] + fn from_u128_checked(v: u128) -> Option { + (v < P).then(|| Self(split(v))) + } + + /// Any u128 is below `2^128 = p + C < 2p`, so a single conditional + /// subtraction canonicalizes (and `v − p < C < p`). + #[inline] + fn from_u128_reduced(v: u128) -> Self { + let (sub, borrow) = v.overflowing_sub(P); + Self(split(if borrow { v } else { sub })) + } + + #[inline] + fn num_bits(&self) -> u32 { + u128::BITS - join(self.0).leading_zeros() + } +} + +crate::impl_serde_bytes!(impl[const P: u128] Fp128

, 16); + +impl WithAccumulator for Fp128

{ + type Accumulator = NaiveAccumulator; +} + +impl PseudoMersenne for Fp128

{ + const OFFSET: u128 = Self::C; +} + +// AArch64-only: the inline-asm kernels against the portable fold, so a +// machine running the asm still exercises (and cross-checks) both paths. +#[cfg(test)] +#[cfg(target_arch = "aarch64")] +mod tests { + use super::*; + use rand::{Rng, SeedableRng}; + use rand_chacha::ChaCha20Rng; + + fn cases(p: u128) -> Vec<[u64; 2]> { + let mut rng = ChaCha20Rng::seed_from_u64(0xf128_a5a5); + let mut v: Vec<[u64; 2]> = [0u128, 1, 2, p / 2, p / 2 + 1, p - 2, p - 1] + .iter() + .map(|&x| split(x)) + .collect(); + v.extend((0..500).map(|_| split(rng.gen::() % p))); + v + } + + fn check() { + for a in cases(P) { + assert_eq!( + Fp128::

::sqr_raw_aarch64(a), + Fp128::

::sqr_raw_portable(a), + "sqr asm vs portable, a={a:?}" + ); + for b in cases(P) { + assert_eq!( + Fp128::

::mul_raw_aarch64(a, b), + Fp128::

::mul_raw_portable(a, b), + "mul asm vs portable, a={a:?} b={b:?}" + ); + } + } + } + + #[test] + fn asm_matches_portable() { + check::<{ u128::MAX - 274 }>(); // C = 275 + check::<{ u128::MAX - 158 }>(); // C = 159 + check::<{ u128::MAX - 2354 }>(); // C = 2355 + check::<{ u128::MAX - 0xFFFF_A7F6 }>(); // C = 0xFFFF_A7F7 + } +} diff --git a/crates/jolt-field-two/src/solinas/mod.rs b/crates/jolt-field-two/src/solinas/mod.rs index 6d76175328..4517ebbe58 100644 --- a/crates/jolt-field-two/src/solinas/mod.rs +++ b/crates/jolt-field-two/src/solinas/mod.rs @@ -1,11 +1,13 @@ //! Solinas backend: pseudo-Mersenne prime fields `p = 2^k − c`. //! //! `word.rs` stamps the `u32`- and `u64`-backed field types from one fold -//! algebra; this module holds the family trait, the `2^k − offset` registry, -//! and shared helpers. +//! algebra; `fp128.rs` is the hand-written two-limb field; this module holds +//! the family trait, the `2^k − offset` registry, and shared helpers. +mod fp128; mod word; +pub use fp128::Fp128; pub use word::{Fp32, Fp64}; use crate::Ring; @@ -45,8 +47,8 @@ pub const fn pseudo_mersenne_modulus(bits: u32, offset: u128) -> Option { clippy::panic, reason = "CTFE-only: all call sites are const registry entries" )] -const fn pm(bits: u32, offset: u16) -> u128 { - match pseudo_mersenne_modulus(bits, offset as u128) { +const fn pm(bits: u32, offset: u128) -> u128 { + match pseudo_mersenne_modulus(bits, offset) { Some(m) => m, None => panic!("invalid pseudo-Mersenne parameters"), } @@ -56,7 +58,7 @@ const fn spec(bits: u32, offset: u16) -> PrimeOffsetSpec { PrimeOffsetSpec { bits, offset, - modulus: pm(bits, offset), + modulus: pm(bits, offset as u128), } } @@ -108,6 +110,17 @@ pub type Prime48Offset59 = Fp64<{ pm(48, 59) as u64 }>; pub type Prime56Offset27 = Fp64<{ pm(56, 27) as u64 }>; /// Prime field for `2^64 - 59`. pub type Prime64Offset59 = Fp64<{ pm(64, 59) as u64 }>; +/// Prime field for `2^128 − 275`. +pub type Prime128Offset275 = Fp128<{ pm(128, 275) }>; +/// Prime field for `2^128 − 159`. Split-NTT-only helper prime. +pub type Prime128Offset159 = Fp128<{ pm(128, 159) }>; +/// Prime field for `2^128 − 2355` (`p ≡ 5 mod 8`): smooth multiplicative +/// subgroup of order `14700 = 2² · 3 · 5² · 7²` for mixed-radix FFT. +pub type Prime128Offset2355 = Fp128<{ pm(128, 2355) }>; +/// Prime field for `2^128 − 2^32 + 22537` (`C = 0xFFFF_A7F7`): smooth +/// multiplicative subgroup of order `2^3 · 3^7 = 17496` (pure radix-3 +/// subgroup `3^7 = 2187`). The default protocol prime. +pub type Prime128OffsetA7F7 = Fp128<{ pm(128, 0xFFFF_A7F7) }>; /// Builds the balanced signed-digit table for `1 <= log_basis <= 6`. pub fn balanced_digit_lut(log_basis: u32) -> [F; 64] { diff --git a/crates/jolt-field-two/src/solinas/word.rs b/crates/jolt-field-two/src/solinas/word.rs index 762619f1be..e8e8c169b6 100644 --- a/crates/jolt-field-two/src/solinas/word.rs +++ b/crates/jolt-field-two/src/solinas/word.rs @@ -393,9 +393,10 @@ const fn fp64_folds_in_word(p: u64) -> bool { bits < 64 && (((1u64 << bits) - p) as u128) < (1u128 << (64 - bits)) } -/// `a * b` widening to 128 bits; returns `(lo, hi)`. +/// `a * b` widening to 128 bits; returns `(lo, hi)`. Shared with the +/// two-limb field (`fp128.rs`). #[inline(always)] -fn mul64_wide(a: u64, b: u64) -> (u64, u64) { +pub(super) fn mul64_wide(a: u64, b: u64) -> (u64, u64) { #[cfg(all(target_arch = "x86_64", target_feature = "bmi2"))] { let mut hi = 0; diff --git a/crates/jolt-field-two/tests/solinas_fp128_differential.rs b/crates/jolt-field-two/tests/solinas_fp128_differential.rs new file mode 100644 index 0000000000..565b9ada74 --- /dev/null +++ b/crates/jolt-field-two/tests/solinas_fp128_differential.rs @@ -0,0 +1,456 @@ +//! Differential tests for the two-limb Solinas field (`Fp128`) against +//! jolt-field across every 128-bit prime offset, with a 4×64-limb schoolbook +//! multiply + binary long division as the independent oracle (`u128` cannot +//! hold the 256-bit intermediates, and num-bigint is not a dev-dependency). + +#![cfg(feature = "solinas")] +#![expect(clippy::unwrap_used, reason = "test code")] + +use jolt_field as base; +use jolt_field_two as two; + +use base::{ + CanonicalBytes, CanonicalField, CanonicalRepr, FieldCore, FromPrimitiveInt, HalvingField, + PseudoMersenneField, RingCore, +}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use two::{Accumulator as _, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, Ring}; + +fn rng() -> ChaCha20Rng { + ChaCha20Rng::seed_from_u64(0xf128_a5a5) +} + +/// 128×128 → 256-bit schoolbook multiply over 64-bit halves; independent of +/// the crate's `mul_wide` (different limb/carry structure, no shared code). +fn oracle_mul_256(a: u128, b: u128) -> [u64; 4] { + let (a0, a1) = (a as u64 as u128, a >> 64); + let (b0, b1) = (b as u64 as u128, b >> 64); + let (p00, p01, p10, p11) = (a0 * b0, a0 * b1, a1 * b0, a1 * b1); + const LO: u128 = u64::MAX as u128; + let mid = (p00 >> 64) + (p01 & LO) + (p10 & LO); + let hi = (p01 >> 64) + (p10 >> 64) + (p11 & LO) + (mid >> 64); + let top = (p11 >> 64) + (hi >> 64); + [p00 as u64, mid as u64, hi as u64, top as u64] +} + +/// Little-endian limbs mod `p` by binary long division (msb first, one +/// conditional subtract per bit) — no Solinas folding anywhere. +fn oracle_mod(limbs: &[u64], p: u128) -> u128 { + let mut r: u128 = 0; + for &limb in limbs.iter().rev() { + for i in (0..64).rev() { + let top = r >> 127; + let mut v = (r << 1) | ((limb >> i) & 1) as u128; + if top == 1 { + // Real value is 2^128 + v < 2p; subtracting p once leaves + // v + (2^128 − p) < p, computed in wrapping arithmetic. + v = v.wrapping_add(0u128.wrapping_sub(p)); + } else if v >= p { + v -= p; + } + r = v; + } + } + r +} + +fn oracle_mul(a: u128, b: u128, p: u128) -> u128 { + oracle_mod(&oracle_mul_256(a, b), p) +} + +/// `a + b (mod p)` for `a, b < p`, via the limb oracle (sum may exceed u128). +fn oracle_add(a: u128, b: u128, p: u128) -> u128 { + let (s, overflow) = a.overflowing_add(b); + oracle_mod(&[s as u64, (s >> 64) as u64, overflow as u64], p) +} + +fn oracle_sub(a: u128, b: u128, p: u128) -> u128 { + oracle_add(a, p - b, p) +} + +/// Full differential + oracle sweep for one (rebuilt, baseline, modulus) +/// triple. `inverses: false` skips inverse checks for moduli of unverified +/// primality (used by the `C = 2^a ± 1` shift-path coverage). +macro_rules! check_prime128 { + ($two:ty, $base:ty, $p:expr, $rng:expr) => { + check_prime128!($two, $base, $p, $rng, inverses: true) + }; + ($two:ty, $base:ty, $p:expr, $rng:expr, inverses: $inverses:expr) => {{ + let p: u128 = $p; + let c: u128 = 0u128.wrapping_sub(p); + let sample = |rng: &mut ChaCha20Rng| -> ($two, $base, u128) { + let raw: u128 = rng.gen(); + let v = oracle_mod(&[raw as u64, (raw >> 64) as u64], p); + let t = <$two as CanonicalEncoding>::from_u128_reduced(raw); + assert_eq!(t.to_u128_checked(), Some(v), "reduction vs oracle"); + let b = <$base as CanonicalField>::from_canonical_u128_reduced(raw); + assert_eq!(b.to_canonical_u128(), v, "baseline reduction vs oracle"); + let b = <$base as CanonicalField>::from_canonical_u128_checked(v).unwrap(); + (t, b, v) + }; + + // Metadata parity. + assert_eq!( + <$two as CanonicalEncoding>::MODULUS_BITS, + <$base as CanonicalField>::modulus_bits() + ); + assert_eq!(<$two as PseudoMersenne>::OFFSET, c); + assert_eq!( + <$two as PseudoMersenne>::OFFSET, + <$base as PseudoMersenneField>::MODULUS_OFFSET + ); + assert_eq!( + <$two as CanonicalEncoding>::NUM_BYTES, + <$base as CanonicalBytes>::NUM_BYTES + ); + assert_eq!(<$two as CanonicalEncoding>::NUM_BYTES, 16); + + let cfg = bincode::config::standard(); + for _ in 0..200 { + let (ta, ba, va) = sample($rng); + let (tb, bb, vb) = sample($rng); + + // Arithmetic vs baseline and vs the limb oracle. + let cases: [($two, $base, u128); 4] = [ + (ta + tb, ba + bb, oracle_add(va, vb, p)), + (ta - tb, ba - bb, oracle_sub(va, vb, p)), + (ta * tb, ba * bb, oracle_mul(va, vb, p)), + (-ta, -ba, oracle_sub(0, va, p)), + ]; + for (t, b, v) in cases { + assert_eq!(t.to_u128_checked(), Some(v)); + assert_eq!(b.to_canonical_u128(), v); + } + + // By-ref and assigning operator forms agree with the owned ones. + assert_eq!(ta + &tb, ta + tb); + assert_eq!(ta - &tb, ta - tb); + assert_eq!(ta * &tb, ta * tb); + let (mut x, mut y, mut z) = (ta, ta, ta); + x += tb; + y -= tb; + z *= tb; + assert_eq!((x, y, z), (ta + tb, ta - tb, ta * tb)); + + assert_eq!( + Ring::square(&ta).to_u128_checked(), + Some(oracle_mul(va, va, p)) + ); + assert_eq!( + Ring::square(&ta).to_u128_checked().unwrap(), + RingCore::square(&ba).to_canonical_u128() + ); + assert_eq!( + ta.half().to_u128_checked().unwrap(), + ba.half().to_canonical_u128() + ); + assert_eq!((ta.half() + ta.half()).to_u128_checked(), Some(va)); + if $inverses { + match (ta.inverse(), ba.inverse()) { + (Some(ti), Some(bi)) => { + assert_eq!(ti.to_u128_checked().unwrap(), bi.to_canonical_u128()); + assert_eq!((ti * ta).to_u128_checked(), Some(1)); + } + (ti, bi) => assert_eq!(ti.is_none(), bi.is_none()), + } + } + + // Wide multiplies: limb parity with the baseline, then round-trip + // through solinas_reduce against the independent oracle. + assert_eq!(ta.to_limbs(), ba.to_limbs()); + assert_eq!(ta.mul_wide(tb), ba.mul_wide(bb)); + assert_eq!(ta.mul_wide(tb), oracle_mul_256(va, vb)); + assert_eq!( + <$two>::solinas_reduce(&ta.mul_wide(tb)).to_u128_checked(), + Some(oracle_mul(va, vb, p)) + ); + let x64: u64 = $rng.gen(); + assert_eq!(ta.mul_wide_u64(x64), ba.mul_wide_u64(x64)); + assert_eq!( + <$two>::solinas_reduce(&ta.mul_wide_u64(x64)).to_u128_checked(), + Some(oracle_mul(va, x64 as u128, p)) + ); + let x128: u128 = $rng.gen::() % p; + assert_eq!(ta.mul_wide_u128(x128), ba.mul_wide_u128(x128)); + assert_eq!( + <$two>::solinas_reduce(&ta.mul_wide_u128(x128)).to_u128_checked(), + Some(oracle_mul(va, x128, p)) + ); + + // Integer conversions. + let xi: i64 = $rng.gen(); + assert_eq!( + <$two as Ring>::from_u64(x64).to_u128_checked().unwrap(), + <$base as FromPrimitiveInt>::from_u64(x64).to_canonical_u128() + ); + assert_eq!( + <$two as Ring>::from_i64(xi).to_u128_checked().unwrap(), + <$base as FromPrimitiveInt>::from_i64(xi).to_canonical_u128() + ); + assert_eq!( + ta.mul_u64(x64).to_u128_checked().unwrap(), + ba.mul_u64(x64).to_canonical_u128() + ); + + // Transcript surface: bytes, reducing decodes, challenges. + assert_eq!(ta.to_bytes_le_vec(), ba.to_bytes_le_vec()); + assert_eq!(ta.to_bytes_le_vec(), va.to_le_bytes().to_vec()); + assert_eq!( + CanonicalEncoding::num_bits(&ta), + CanonicalRepr::num_bits(&ba) + ); + assert_eq!(ta.to_u64_checked(), ba.to_canonical_u64_checked()); + let challenge: [u8; 32] = $rng.gen(); + for len in [8usize, 16, 32] { + let ours = <$two as CanonicalEncoding>::from_bytes_le_reduced(&challenge[..len]); + assert_eq!( + <$two as CanonicalEncoding>::from_challenge_bytes(&challenge[..len]), + ours, + "challenge derivation defaults to the reducing decode" + ); + assert_eq!( + <$two as CanonicalEncoding>::from_scalar_challenge_bytes(&challenge[..len]) + .to_u128_checked(), + Some( + <$base as CanonicalRepr>::from_scalar_challenge_bytes(&challenge[..len]) + .to_canonical_u128() + ), + "scalar challenge derivation diverges from baseline" + ); + let mut padded = [0u8; 32]; + padded[..len].copy_from_slice(&challenge[..len]); + let limbs: [u64; 4] = + std::array::from_fn(|i| u64::from_le_bytes(padded[8 * i..8 * i + 8].try_into().unwrap())); + assert_eq!( + ours.to_u128_checked(), + Some(oracle_mod(&limbs, p)), + "decode vs oracle" + ); + assert_eq!( + ours.to_u128_checked().unwrap(), + <$base as CanonicalRepr>::from_le_bytes_mod_order(&challenge[..len]) + .to_canonical_u128() + ); + } + + // Wire bytes: equality, cross-decode, canonical rejection. + let t_bytes = bincode::serde::encode_to_vec(ta, cfg).unwrap(); + let b_bytes = bincode::serde::encode_to_vec(ba, cfg).unwrap(); + assert_eq!(t_bytes, b_bytes, "wire bytes diverge"); + let (t_back, _): ($two, usize) = + bincode::serde::decode_from_slice(&b_bytes, cfg).unwrap(); + assert_eq!(t_back.to_u128_checked(), Some(va)); + } + + // Boundary values through every arithmetic path (mul via the oracle: + // u128 cannot hold the products). + let boundaries: Vec = vec![0, 1, 2, p - 2, p - 1, p / 2, p / 2 + 1]; + for &x in &boundaries { + for &y in &boundaries { + let tx = <$two as CanonicalEncoding>::from_u128_checked(x).unwrap(); + let ty = <$two as CanonicalEncoding>::from_u128_checked(y).unwrap(); + assert_eq!(tx.to_u128_checked(), Some(x)); + assert_eq!((tx + ty).to_u128_checked(), Some(oracle_add(x, y, p))); + assert_eq!((tx - ty).to_u128_checked(), Some(oracle_sub(x, y, p))); + assert_eq!((tx * ty).to_u128_checked(), Some(oracle_mul(x, y, p))); + assert_eq!(Ring::square(&tx).to_u128_checked(), Some(oracle_mul(x, x, p))); + } + } + assert_eq!(<$two as CanonicalEncoding>::from_u128_checked(p), None); + // Reducing constructor adjacent to the canonical threshold. + for raw in [p - 1, p, p + 1, u128::MAX] { + assert_eq!( + <$two as CanonicalEncoding>::from_u128_reduced(raw).to_u128_checked(), + Some(oracle_mod(&[raw as u64, (raw >> 64) as u64], p)) + ); + } + assert_eq!( + <$two as Ring>::from_i128(i128::MIN).to_u128_checked(), + Some(p - (1u128 << 127)), + "i128::MIN vs oracle (2^127 < p, so its negation is p − 2^127)" + ); + + // solinas_reduce across every length dispatch and fold threshold: + // all-ones limbs maximize every fold intermediate (t2 hits its bound), + // and single-high-limb patterns pin each C-power identity. + let mut limb_cases: Vec> = vec![ + vec![], + vec![42], + vec![u64::MAX], + vec![u64::MAX, u64::MAX], + vec![0, 0, 1], + vec![u64::MAX, u64::MAX, u64::MAX], + vec![0, 0, 0, 1], + vec![1, 0, 0, 0, 0, 0, 0, 0, 1], + ]; + for len in 4..=10 { + limb_cases.push(vec![u64::MAX; len]); + } + for _ in 0..50 { + let len = $rng.gen_range(0..=10); + limb_cases.push((0..len).map(|_| $rng.gen()).collect()); + } + for limbs in &limb_cases { + let got = <$two>::solinas_reduce(limbs).to_u128_checked().unwrap(); + assert_eq!(got, oracle_mod(limbs, p), "solinas_reduce vs oracle: {limbs:?}"); + assert_eq!( + got, + <$base>::solinas_reduce(limbs).to_canonical_u128(), + "solinas_reduce vs baseline: {limbs:?}" + ); + } + + // Zero/One and iterator Sum/Product (owned and by-ref). + use num_traits::{One, Zero}; + assert!(<$two>::zero().is_zero()); + assert_eq!(<$two>::zero().to_u128_checked(), Some(0)); + assert_eq!(<$two>::one().to_u128_checked(), Some(1)); + assert!(!(<$two>::one().is_zero())); + assert_eq!(-<$two>::zero(), <$two>::zero()); + let xs: Vec<$two> = (0..9).map(|_| sample($rng).0).collect(); + let expected_sum = xs.iter().fold(<$two>::zero(), |a, &x| a + x); + let expected_prod = xs.iter().fold(<$two>::one(), |a, &x| a * x); + assert_eq!(xs.iter().copied().sum::<$two>(), expected_sum); + assert_eq!(xs.iter().sum::<$two>(), expected_sum); + assert_eq!(xs.iter().copied().product::<$two>(), expected_prod); + assert_eq!(xs.iter().product::<$two>(), expected_prod); + + // Non-canonical wire encodings rejected (encode p itself), both at + // the CanonicalEncoding surface and through serde. + let p_bytes = p.to_le_bytes(); + assert_eq!( + <$two as CanonicalEncoding>::from_bytes_le_checked(&p_bytes), + None + ); + assert_eq!( + <$two as CanonicalEncoding>::from_bytes_le_checked(&p_bytes[..15]), + None, + "wrong length rejected" + ); + assert_eq!( + <$two as CanonicalEncoding>::from_bytes_le_checked(&[0u8; 17]), + None, + "over-length rejected" + ); + assert!( + bincode::serde::decode_from_slice::<$two, _>(&p_bytes, cfg).is_err(), + "serde must reject non-canonical bytes" + ); + }}; +} + +#[test] +fn fp128_offset275_matches() { + let mut rng = rng(); + check_prime128!( + two::Prime128Offset275, + base::Prime128Offset275, + u128::MAX - 274, + &mut rng + ); +} + +#[test] +fn fp128_offset159_matches() { + let mut rng = rng(); + check_prime128!( + two::Prime128Offset159, + base::Prime128Offset159, + u128::MAX - 158, + &mut rng + ); +} + +#[test] +fn fp128_offset2355_matches() { + let mut rng = rng(); + check_prime128!( + two::Prime128Offset2355, + base::Prime128Offset2355, + u128::MAX - 2354, + &mut rng + ); +} + +#[test] +fn fp128_offset_a7f7_matches() { + let mut rng = rng(); + check_prime128!( + two::Prime128OffsetA7F7, + base::Prime128OffsetA7F7, + u128::MAX - 0xFFFF_A7F6, + &mut rng + ); + assert_eq!( + two::pseudo_mersenne_modulus(128, 0xFFFF_A7F7), + Some(u128::MAX - 0xFFFF_A7F6) + ); + // Registered coverage stops at PRIME_OFFSET_MAX; A7F7 is above it, like + // the baseline registry. + assert!(!two::is_registered_prime_offset(128, 0xFFFF_A7F7)); + assert!(two::is_registered_prime_offset(128, 275)); +} + +/// `C = 2^a ± 1` moduli exercise the shift/add and shift/sub branches of +/// `mul_c_wide` that no registered prime reaches. Primality of these moduli +/// is unverified, so inverse checks are skipped (everything else is +/// ring-level and needs only an odd modulus). +#[test] +fn fp128_shift_kind_c_paths_match() { + let mut rng = rng(); + // C = 5 = 2^2 + 1 (shift-kind +1). + check_prime128!( + two::Fp128<{ u128::MAX - 4 }>, + base::Fp128<{ u128::MAX - 4 }>, + u128::MAX - 4, + &mut rng, + inverses: false + ); + // C = 7 = 2^3 − 1 (shift-kind −1). + check_prime128!( + two::Fp128<{ u128::MAX - 6 }>, + base::Fp128<{ u128::MAX - 6 }>, + u128::MAX - 6, + &mut rng, + inverses: false + ); +} + +/// Identical rejection sampling: same seed, same element stream. +#[test] +fn fp128_random_matches_baseline() { + fn check() { + let (mut r1, mut r2) = (rng(), rng()); + for _ in 0..100 { + let t: two::Fp128

= two::Field::random(&mut r1); + let b: base::Fp128

= FieldCore::random(&mut r2); + assert_eq!(t.to_u128_checked().unwrap(), b.to_canonical_u128()); + } + } + check::<{ u128::MAX - 274 }>(); + check::<{ u128::MAX - 158 }>(); + check::<{ u128::MAX - 2354 }>(); + check::<{ u128::MAX - 0xFFFF_A7F6 }>(); +} + +fn inner_product(xs: &[F], ys: &[F]) -> F { + let mut acc = F::Accumulator::default(); + for (&x, &y) in xs.iter().zip(ys) { + acc.fmadd(x, y); + } + acc.reduce() +} + +#[test] +fn jolt_field_blanket_covers_fp128() { + fn check() { + let xs = [F::from_u64(2), F::from_u64(3)]; + let ys = [F::from_u64(5), F::from_u64(7)]; + assert_eq!(inner_product(&xs, &ys), F::from_u64(31)); + } + check::(); + check::(); + check::(); + check::(); +} diff --git a/crates/jolt-field-two/tests/solinas_words_differential.rs b/crates/jolt-field-two/tests/solinas_words_differential.rs index 5bbe1afc08..574046329b 100644 --- a/crates/jolt-field-two/tests/solinas_words_differential.rs +++ b/crates/jolt-field-two/tests/solinas_words_differential.rs @@ -9,7 +9,8 @@ use jolt_field as base; use jolt_field_two as two; use base::{ - CanonicalField, CanonicalRepr, FromPrimitiveInt, HalvingField, PseudoMersenneField, RingCore, + CanonicalBytes, CanonicalField, CanonicalRepr, FromPrimitiveInt, HalvingField, + PseudoMersenneField, RingCore, }; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -48,7 +49,7 @@ macro_rules! check_prime { ); assert_eq!( <$two as CanonicalEncoding>::NUM_BYTES, - <$base as CanonicalRepr>::NUM_BYTES + <$base as CanonicalBytes>::NUM_BYTES ); let cfg = bincode::config::standard(); From ae313afa227b10a5325cd4395629f188a3d4801d Mon Sep 17 00:00:00 2001 From: acentelles Date: Thu, 30 Jul 2026 18:44:37 -0400 Subject: [PATCH 22/38] feat(jolt-field-two): cyclotomic extensions FpExt2/4/8 (checkpoint 6) Contracts in src/extension.rs (54/60 counted): ExtField (degree, lift, mul_base, coefficient access, Frobenius) and Ext2Config with the NegOneNr/TwoNr ZSTs. MulBaseUnreduced is deferred to checkpoint 7 with its reason recorded: the contract is stated in terms of Unreduced::Product, which does not exist yet. Lane-generic deg-4/8 coefficient schedules live in a new unconditional src/schedules.rs (133/140): the PseudoMersenne kernel-hook defaults in algebra.rs and the packed lanes (checkpoint 8) share the same formulas, so they sit in the root layer as backend-neutral algebra; the old ext.rs budget is re-carved 890 -> 750 + 140 with the component total unchanged. PseudoMersenne gains the four hooks (ext4/8 mul + square); an ext8_square hook is new relative to the baseline (which squared degree 8 via full mul) - value-identical, fewer ops, flagged in SPEC. Headline: the baseline's fused-accumulation Fp32 deg-4 override LOST the SPEC's checkpoint-6 bench gate - generic default 12.3 ns/op vs fused 31.1 ns/op for mul (2.5x), 15.3 vs 28.3 for square, on aarch64/M4 with the ported override reproducing the baseline's own timing (31.0) as the sanity anchor. Immediate word-sized reductions pipeline better than u128 accumulation chains here. The override is dropped from src with evidence in SPEC.md; the port (with re-derived column-sum bounds) survives in benches/ext4_kernels.rs for x86-64 re-evaluation, where the verdict could flip. Impls in src/solinas/ext.rs (617/750): Karatsuba FpExt2 with norm inverse, FpExt4 subfield-tower inverse, FpExt8 Gaussian-elimination inverse, Frobenius via base-modulus powers, Moore thetas/solve/ validate, serde as [F; K] coefficient arrays byte-identical to the baseline. Differential suite (25 new tests, 64/64 all-features total): five base fields x {FpExt2 both configs, FpExt4, FpExt8, Moore} against the baseline plus an independent schoolbook oracle (binary-long-division modular arithmetic, phi-rule Chebyshev multiply), boundary coefficient patterns, ring-identity spot checks, fixed seeds. Baseline oddities recorded: the fused-square bits!=32 guard has no recorded rationale; NegOneNr is a field on no registered prime (all are 1 mod 4); baseline ext types implement no canonical-bytes trait, so ext types here are deliberately not JoltField (parity). --- crates/jolt-field-two/Cargo.toml | 5 + crates/jolt-field-two/SPEC.md | 38 +- crates/jolt-field-two/benches/ext4_kernels.rs | 177 ++++ crates/jolt-field-two/src/algebra.rs | 28 + crates/jolt-field-two/src/extension.rs | 113 +++ crates/jolt-field-two/src/lib.rs | 15 +- crates/jolt-field-two/src/schedules.rs | 186 +++++ crates/jolt-field-two/src/solinas/ext.rs | 774 ++++++++++++++++++ crates/jolt-field-two/src/solinas/mod.rs | 5 + crates/jolt-field-two/src/solinas/word.rs | 4 + .../tests/solinas_ext_differential.rs | 692 ++++++++++++++++ 11 files changed, 2027 insertions(+), 10 deletions(-) create mode 100644 crates/jolt-field-two/benches/ext4_kernels.rs create mode 100644 crates/jolt-field-two/src/extension.rs create mode 100644 crates/jolt-field-two/src/schedules.rs create mode 100644 crates/jolt-field-two/src/solinas/ext.rs create mode 100644 crates/jolt-field-two/tests/solinas_ext_differential.rs diff --git a/crates/jolt-field-two/Cargo.toml b/crates/jolt-field-two/Cargo.toml index 58d8a85441..2621969c3f 100644 --- a/crates/jolt-field-two/Cargo.toml +++ b/crates/jolt-field-two/Cargo.toml @@ -35,3 +35,8 @@ bincode = { workspace = true } jolt-field = { path = "../jolt-field", features = ["solinas"] } rand = { workspace = true } rand_chacha = { workspace = true } + +[[bench]] +name = "ext4_kernels" +harness = false +required-features = ["solinas"] diff --git a/crates/jolt-field-two/SPEC.md b/crates/jolt-field-two/SPEC.md index a71399b55f..8f6c0989dd 100644 --- a/crates/jolt-field-two/SPEC.md +++ b/crates/jolt-field-two/SPEC.md @@ -54,10 +54,10 @@ capability subset with defaulted members". | Trait | Replaces | Contents | |---|---|---| -| `PseudoMersenne` (defined unconditionally in `algebra.rs`, per the file table) | `PseudoMersenneField` + `ExtMulBackend` | `const OFFSET: u128` (bits live on `CanonicalEncoding`) + the degree-4/8 ext-mul kernel hooks with generic coefficient-formula defaults (only `Fp32` overrides, fusing i64 accumulation) | +| `PseudoMersenne` (defined unconditionally in `algebra.rs`, per the file table) | `PseudoMersenneField` + `ExtMulBackend` | `const OFFSET: u128` (bits live on `CanonicalEncoding`) + the degree-4/8 ext-mul/square kernel hooks (`ext4_mul`, `ext4_square`, `ext8_mul`, `ext8_square`) with generic coefficient-formula defaults (`schedules.rs`). No base field overrides them: the baseline's fused-accumulation `Fp32` override lost the checkpoint-6 bench gate (see dropped-specialization evidence) | | `ExtField` | same | degree, `lift_base`, `mul_base`, coeff access, Frobenius | | `Ext2Config` | `FpExt2Config` | quadratic non-residue config (ZST pattern), `IS_NEG_ONE` fast path | -| `MulBaseUnreduced` | same | tiny overridable ext×base deferred multiply | +| `MulBaseUnreduced` | same | tiny overridable ext×base deferred multiply — **deferred to checkpoint 7**: its contract is stated in terms of `Unreduced::Product`, which does not exist until the unreduced checkpoint | | `Unreduced` | `HasUnreducedOps` + `HasWide` + `ReduceTo` | **one deferred-reduction companion surface**: `type Product`, `type SmallProduct`, `type Wide` (i32-lane), `SUM_IS_EXACT`, widening muls + `reduce_*` for each, `scale_wide`. Rationale: these were three fragments of one concept — "the unreduced value algebra around a field"; routing reduction through the field type kills `ReduceTo`'s ambiguity workarounds | | `Fold` | `HasOptimizedFold` | `precompute(r) -> Ctx`, `fold_one(ctx, even, odd)` — documented honestly as the multilinear bind `even + r·(odd − even)`, a protocol-support hook that lives here because implementations exploit field representation | | `Packed` | `PackedField` | lanes: `Scalar`, `WIDTH`, `from_fn`/`extract`/`broadcast` + defaulted slice helpers + packed ext2 kernel hook | @@ -120,6 +120,33 @@ and fold matrices; `S64`–`S256` + hi32 variants; `Limbs`; rayon helpers; - **Dropped:** `from_i64_const` (const-evaluable embedding): akita-only (its `MONTGOMERY_R` constants). +**Dropped-specialization evidence (checkpoint 6, extensions):** + +- **Dropped:** the baseline's fused `Fp32` deg-4 ext-mul/square override + (u128 column accumulation with `P²` biases, one reduction per output + coefficient). Bench gate (`benches/ext4_kernels.rs`, aarch64 / Apple M4, + release codegen, 4096 batched ops × 100 reps, best of 7, over + `Prime32Offset99`): generic-schedule mul **12.3 ns/op** vs fused port + **31.1 ns/op** (fused 2.5x slower; the baseline crate's own fused + override measured 31.0 ns — the port reproduces it exactly); square + **15.3 ns** vs **28.3 ns** (1.85x slower; baseline 28.6 ns). + Keep-threshold was a >10% fused win, so all + four `PseudoMersenne` kernel hooks retain their generic defaults and no + base field overrides them. Caveat: measured on aarch64 only (immediate + word-sized reductions pipeline better than u128 accumulation chains + there); the fused port stays in the bench harness — rerun it on x86-64 + before reintroducing an override. +- **Deferred:** the `MulBaseUnreduced` contract to checkpoint 7 — its + baseline definition (`mul_base_to_product_accum`) returns + `Unreduced::Product`, which does not exist until the unreduced surface + lands; inventing a placeholder shape now would just be churn. +- **Added (not in baseline):** an `ext8_square` hook on `PseudoMersenne` + defaulting to the deg-8 squaring schedule. The baseline computed + `FpExt8::square` as a full multiply and used its square schedule only in + the packed kernels; routing scalar squaring through the same schedule is + value-identical (pure ring ops), saves base ops, and gives the schedule + its scalar consumer. + ## Design pillars 1. **Const-generic scalar core**: `Fp64` etc., fold constants @@ -173,10 +200,11 @@ on them. | **Contract layer (root, unconditional)** | | | | `src/lib.rs` | 70 | crate docs, feature gates, re-exports, `FieldError` | | `src/algebra.rs` | 260 | spine: 7 traits + `NaiveAccumulator` + `PseudoMersenne` | -| `src/extension.rs` | 60 | contracts: `ExtField`, `Ext2Config`, `MulBaseUnreduced` | +| `src/extension.rs` | 60 | contracts: `ExtField`, `Ext2Config` + NR config ZSTs (`MulBaseUnreduced` lands with checkpoint 7) | | `src/unreduced.rs` | 70 | contracts: `Unreduced`, `Fold` | | `src/packed.rs` | 90 | contracts: `Packed`, `WithPacking` + generic `NoPacking` | | `src/ops.rs` | 180 | `impl_ring_ops!`, `impl_serde_bytes!` (backend-neutral) | +| `src/schedules.rs` | 140 | lane-generic deg-4/8 ext coefficient schedules — unconditional because the `PseudoMersenne` hook defaults (algebra.rs) and the packed lanes (checkpoint 8) share them; carved out of the old `ext.rs` budget (890 → 750, component total unchanged) | | `src/limbs.rs` | 220 | `Limbs` | | `src/signed.rs` | 420 | signed bigint families (consumer-audited surface) | | **bn254 backend** | | | @@ -186,7 +214,7 @@ on them. | `src/solinas/mod.rs` | 90 | offset registry, aliases, shared helpers | | `src/solinas/word.rs` | 380 | `define_solinas_prime!` → `Fp32`, `Fp64` | | `src/solinas/fp128.rs` | 700 | two-limb add/sub/mul/reduce/wide | -| `src/solinas/ext.rs` | 890 | FpExt2/4/8 impls, schedules, Frobenius + Moore | +| `src/solinas/ext.rs` | 750 | FpExt2/4/8 impls, ExtField impls, Frobenius + Moore | | `src/solinas/unreduced.rs` | 530 | lane accumulators, fold matrices, contract impls | | `src/solinas/parallel.rs` | 80 | rayon helpers | | `src/solinas/packed/mod.rs` | 30 | backend selection | @@ -198,7 +226,7 @@ on them. Component budgets are unchanged — the contract/impl split carves each component's contracts out of its old single-file budget (extensions 950 = -60 + 890, unreduced 600 = 70 + 530, packed selection 120 = 90 + 30). +60 + 140 + 750, unreduced 600 = 70 + 530, packed selection 120 = 90 + 30). Baseline per component (same metric): packed 3,720 → 1,600 · prime 2,140 → 1,170 · ext 1,661 → 950 · arkworks 1,170 → 700 · unreduced 1,073 → 600 · diff --git a/crates/jolt-field-two/benches/ext4_kernels.rs b/crates/jolt-field-two/benches/ext4_kernels.rs new file mode 100644 index 0000000000..05dc5009ae --- /dev/null +++ b/crates/jolt-field-two/benches/ext4_kernels.rs @@ -0,0 +1,177 @@ +//! Bench gate for the `Fp32` degree-4 ext-mul kernels (checkpoint 6 +//! acceptance): the generic coefficient-formula schedule (what the crate +//! ships as the `PseudoMersenne` hook default) vs a local port of the +//! baseline's fused u128-accumulation `Fp32` override, on batched degree-4 +//! muls and squares over `Prime32Offset99`. The baseline `FpExt4` +//! (which ships the fused override) is included for context. +//! +//! Outcome recorded in SPEC.md: the fused port LOST on aarch64/Apple M4 +//! (generic ≈ 2.5x faster on mul, ≈ 1.85x on square; the port reproduces +//! the baseline override's timing exactly), so the override was dropped +//! and the crate keeps the generic defaults. This harness stays as the +//! reproducible evidence; rerun it before reintroducing an override. +//! +//! Run: `cargo bench -p jolt-field-two --features solinas --bench ext4_kernels` + +#![expect( + clippy::unwrap_used, + clippy::print_stdout, + reason = "bench harness: canonical conversions of canonical values; stdout is the report" +)] + +use jolt_field as base; +use jolt_field_two as two; + +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; +use std::hint::black_box; +use std::time::Instant; + +use two::{CanonicalEncoding, Field, Ring}; + +type Fp = two::Prime32Offset99; +type E4 = two::FpExt4; +type BaseFp = base::Prime32Offset99; +type BaseE4 = base::FpExt4; + +const N: usize = 1 << 12; +const REPS: usize = 100; +const TRIALS: usize = 7; + +/// Widening product of canonical `Fp32` values, exact in `u64` +/// (`a·b < P² < 2^64`), widened to `u128` for column accumulation. +#[inline(always)] +fn product(a: Fp, b: Fp) -> u128 { + ((a.to_limbs() as u64) * (b.to_limbs() as u64)) as u128 +} + +const P: u32 = 4_294_967_197; // 2^32 − 99 + +/// Port of the baseline's fused `Fp32` degree-4 multiply: accumulate the +/// raw products of each output coefficient in a `u128`, reduce once. +/// +/// Bounds (every term `< P² < 2^64`, sums evaluated left to right): +/// `c0 ≤ 7·P² < 2^67`; `c1 ≤ 6·P²`; `c2` has a `P²` bias ≥ the single +/// subtrahend `p33`; `c3` has a `2·P²` bias ≥ `p23 + p32`. No `u128` wrap, +/// biases are multiples of `P`, so results equal the generic schedule's. +#[inline(always)] +fn fused_mul(a: [Fp; 4], b: [Fp; 4]) -> [Fp; 4] { + let [a0, a1, a2, a3] = a; + let [b0, b1, b2, b3] = b; + let msq = (P as u128) * (P as u128); + [ + Fp::from_u128_reduced( + product(a0, b0) + 2 * (product(a1, b1) + product(a2, b2) + product(a3, b3)), + ), + Fp::from_u128_reduced( + product(a0, b1) + + product(a1, b0) + + product(a1, b2) + + product(a2, b1) + + product(a2, b3) + + product(a3, b2), + ), + Fp::from_u128_reduced( + product(a0, b2) + + product(a2, b0) + + product(a1, b1) + + product(a1, b3) + + product(a3, b1) + + msq + - product(a3, b3), + ), + Fp::from_u128_reduced( + product(a0, b3) + product(a3, b0) + product(a1, b2) + product(a2, b1) + 2 * msq + - product(a2, b3) + - product(a3, b2), + ), + ] +} + +/// Port of the baseline's fused `Fp32` degree-4 squaring (10 products); +/// same bound structure as [`fused_mul`], every column `< 8·P² < 2^67`. +#[inline(always)] +fn fused_square(a: [Fp; 4]) -> [Fp; 4] { + let [a0, a1, a2, a3] = a; + let msq = (P as u128) * (P as u128); + let a0_square = product(a0, a0); + let a1_square = product(a1, a1); + let a2_square = product(a2, a2); + let a3_square = product(a3, a3); + let a0a1 = product(a0, a1); + let a0a2 = product(a0, a2); + let a0a3 = product(a0, a3); + let a1a2 = product(a1, a2); + let a1a3 = product(a1, a3); + let a2a3 = product(a2, a3); + [ + Fp::from_u128_reduced(a0_square + 2 * (a1_square + a2_square + a3_square)), + Fp::from_u128_reduced(2 * (a0a1 + a1a2 + a2a3)), + Fp::from_u128_reduced(2 * a0a2 + a1_square + 2 * a1a3 + msq - a3_square), + Fp::from_u128_reduced(2 * (a0a3 + a1a2 + msq - a2a3)), + ] +} + +fn measure(inputs: &[T], mut op: impl FnMut(T) -> R) -> f64 { + let mut best = f64::INFINITY; + for _ in 0..TRIALS { + let start = Instant::now(); + for _ in 0..REPS { + for &x in inputs { + let _ = black_box(op(x)); + } + } + let ns = start.elapsed().as_nanos() as f64 / (REPS * inputs.len()) as f64; + best = best.min(ns); + } + best +} + +fn main() { + let mut rng = ChaCha20Rng::seed_from_u64(0xE4B_E4B); + let pairs: Vec<(E4, E4)> = (0..N) + .map(|_| (E4::random(&mut rng), E4::random(&mut rng))) + .collect(); + let base_pairs: Vec<(BaseE4, BaseE4)> = pairs + .iter() + .map(|(a, b)| { + let conv = |x: &E4| { + BaseE4::new(x.coeffs.map(|c| { + base::CanonicalField::from_canonical_u128_checked(c.to_u128_checked().unwrap()) + .unwrap() + })) + }; + (conv(a), conv(b)) + }) + .collect(); + + // Sanity: the fused port agrees with the wired generic path. + for (a, b) in pairs.iter().take(64) { + assert_eq!((*a * *b).coeffs, fused_mul(a.coeffs, b.coeffs)); + assert_eq!(Ring::square(a).coeffs, fused_square(a.coeffs)); + } + + let generic_mul_ns = measure(&pairs, |(a, b)| (a * b).coeffs[0]); + let fused_mul_ns = measure(&pairs, |(a, b)| fused_mul(a.coeffs, b.coeffs)[0]); + let base_mul_ns = measure(&base_pairs, |(a, b)| (a * b).coeffs[0]); + + let generic_sq_ns = measure(&pairs, |(a, _)| Ring::square(&a).coeffs[0]); + let fused_sq_ns = measure(&pairs, |(a, _)| fused_square(a.coeffs)[0]); + let base_sq_ns = measure(&base_pairs, |(a, _)| base::RingCore::square(&a).coeffs[0]); + + println!("ext4 over Prime32Offset99, {N} elements x {REPS} reps, best of {TRIALS}"); + println!(" mul generic default (wired): {generic_mul_ns:7.2} ns/op"); + println!(" mul fused port (dropped) : {fused_mul_ns:7.2} ns/op"); + println!(" mul baseline fused override: {base_mul_ns:7.2} ns/op"); + println!( + " mul fused/generic : {:.2}x", + fused_mul_ns / generic_mul_ns + ); + println!(" square generic default (wired): {generic_sq_ns:7.2} ns/op"); + println!(" square fused port (dropped) : {fused_sq_ns:7.2} ns/op"); + println!(" square baseline fused override: {base_sq_ns:7.2} ns/op"); + println!( + " square fused/generic : {:.2}x", + fused_sq_ns / generic_sq_ns + ); +} diff --git a/crates/jolt-field-two/src/algebra.rs b/crates/jolt-field-two/src/algebra.rs index c8fd1969f1..dd5c4b711a 100644 --- a/crates/jolt-field-two/src/algebra.rs +++ b/crates/jolt-field-two/src/algebra.rs @@ -201,6 +201,34 @@ pub trait Field: Ring { pub trait PseudoMersenne: Field + CanonicalEncoding { /// Offset `c` in `2^k − c`. const OFFSET: u128; + + /// Degree-4 extension multiply kernel in the `[1, e1, e2, e3]` basis. + /// + /// Defaults to the generic coefficient schedule; base fields whose + /// representation supports fusing product sums before reduction + /// override it (`Fp32` accumulates raw products in `u128`). + #[inline(always)] + fn ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { + crate::schedules::ext4_mul_coeffs(a, b) + } + + /// Degree-4 extension squaring kernel in the `[1, e1, e2, e3]` basis. + #[inline(always)] + fn ext4_square(a: [Self; 4]) -> [Self; 4] { + crate::schedules::ext4_square_coeffs(a) + } + + /// Degree-8 extension multiply kernel in the `[1, e1, ..., e7]` basis. + #[inline(always)] + fn ext8_mul(a: [Self; 8], b: [Self; 8]) -> [Self; 8] { + crate::schedules::ext8_mul_coeffs(a, b) + } + + /// Degree-8 extension squaring kernel in the `[1, e1, ..., e7]` basis. + #[inline(always)] + fn ext8_square(a: [Self; 8]) -> [Self; 8] { + crate::schedules::ext8_square_coeffs(a) + } } /// Canonical little-endian representation: the Fiat-Shamir transcript surface diff --git a/crates/jolt-field-two/src/extension.rs b/crates/jolt-field-two/src/extension.rs new file mode 100644 index 0000000000..1dd94cad84 --- /dev/null +++ b/crates/jolt-field-two/src/extension.rs @@ -0,0 +1,113 @@ +//! Extension-field contracts: the tower surface over a base field. +//! +//! [`ExtField`] is the degree-`d` extension contract (embedding, coefficient +//! access in the canonical basis, Frobenius); [`Ext2Config`] configures a +//! quadratic extension `F[u]/(u^2 − NR)` through a zero-sized type, with the +//! [`NegOneNr`] and [`TwoNr`] presets. +//! +//! `MulBaseUnreduced` (deferred ext×base multiply) is deferred to the +//! `Unreduced` checkpoint: its contract is stated in terms of +//! `Unreduced::Product`. + +use crate::{Field, Ring}; +use std::ops::{Add, Mul, Sub}; + +/// An algebraic extension of the base field `F`. +/// +/// Provides the extension degree, embedding of and multiplication by base +/// elements, coefficient access in the canonical basis `{1, e1, ...}`, and +/// Frobenius powers `x -> x^(q^power)` for `q = |F|`. +pub trait ExtField: Field { + /// Extension degree `[Self : F]`. + const DEGREE: usize; + + /// Embeds `x ∈ F` as the constant coefficient. + fn lift_base(x: F) -> Self; + + /// Returns `self * x` where `x` is a base-field scalar, scaling each + /// base coordinate directly (no full extension multiply). + fn mul_base(self, x: F) -> Self; + + /// Constructs from a coefficient slice `[c0, c1, ..., c_{d−1}]`. + /// + /// # Panics + /// + /// Panics if `coeffs.len() != Self::DEGREE`. + fn from_base_slice(coeffs: &[F]) -> Self; + + /// Returns the base-field coefficients in the canonical basis. + fn to_base_vec(&self) -> Vec; + + /// Applies `x -> x^(q^power)`, where `q = |F|`. + fn frobenius_pow(self, power: usize) -> Self; + + /// Applies the inverse Frobenius power: since `x -> x^q` has order + /// `DEGREE` on `Self`, this is `frobenius_pow(DEGREE − power)`. + #[inline] + fn frobenius_inv_pow(self, power: usize) -> Self { + let d = Self::DEGREE; + self.frobenius_pow((d - (power % d)) % d) + } +} + +/// Parameters for a quadratic extension `F[u]/(u^2 − NR)` over `F`. +/// +/// Implemented by zero-sized config types so the non-residue choice is a +/// compile-time property of the extension type. +pub trait Ext2Config { + /// Whether the non-residue is −1: multiplication by `NR` is then a free + /// negation and the Karatsuba/squaring routines save a base multiply. + const IS_NEG_ONE: bool = false; + + /// The quadratic non-residue `NR` with `u^2 = NR`. + fn non_residue() -> F; + + /// Multiplies a coefficient by the non-residue, generic over the lane + /// type `A` (field elements or unreduced accumulator lanes); + /// `from_base` embeds a base constant into `A`. + #[inline] + fn mul_non_residue(x: A, from_base: B) -> A + where + A: Copy + Add + Sub + Mul, + B: FnOnce(F) -> A, + { + if Self::IS_NEG_ONE { + from_base(F::zero()) - x + } else { + from_base(Self::non_residue()) * x + } + } +} + +/// [`Ext2Config`] with non-residue −1; valid when `p ≡ 3 (mod 4)`. +pub struct NegOneNr; + +impl Ext2Config for NegOneNr { + const IS_NEG_ONE: bool = true; + + #[inline] + fn non_residue() -> F { + -F::one() + } +} + +/// [`Ext2Config`] with non-residue 2; valid when `p ≡ 5 (mod 8)`, which +/// holds for every registered pseudo-Mersenne prime (`2^k − c`, `c ≡ 3 mod 8`). +pub struct TwoNr; + +impl Ext2Config for TwoNr { + #[inline] + fn non_residue() -> F { + F::from_u64(2) + } + + /// Multiplication by 2 is a doubling: one add, no multiply. + #[inline] + fn mul_non_residue(x: A, _from_base: B) -> A + where + A: Copy + Add + Sub + Mul, + B: FnOnce(F) -> A, + { + x + x + } +} diff --git a/crates/jolt-field-two/src/lib.rs b/crates/jolt-field-two/src/lib.rs index c32c0e869b..067d205691 100644 --- a/crates/jolt-field-two/src/lib.rs +++ b/crates/jolt-field-two/src/lib.rs @@ -20,8 +20,10 @@ mod algebra; #[cfg(feature = "bn254")] mod bn254; +mod extension; mod limbs; mod ops; +mod schedules; pub mod signed; #[cfg(feature = "solinas")] pub mod solinas; @@ -32,15 +34,18 @@ pub use algebra::{ }; #[cfg(feature = "bn254")] pub use bn254::{Fq, Fr, WideAccumulator}; +pub use extension::{Ext2Config, ExtField, NegOneNr, TwoNr}; pub use limbs::Limbs; pub use num_traits::{One, Zero}; #[cfg(feature = "solinas")] pub use solinas::{ - balanced_digit_lut, is_registered_prime_offset, pseudo_mersenne_modulus, - registered_prime_offset_spec, Fp128, Fp32, Fp64, Prime128Offset159, Prime128Offset2355, - Prime128Offset275, Prime128OffsetA7F7, Prime24Offset3, Prime30Offset35, Prime31Offset19, - Prime32Offset99, Prime40Offset195, Prime48Offset59, Prime56Offset27, Prime64Offset59, - PrimeOffsetSpec, PRIME_OFFSET_IMPLEMENTED_MAX_BITS, PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, + balanced_digit_lut, canonical_frobenius_thetas, is_registered_prime_offset, + pseudo_mersenne_modulus, registered_prime_offset_spec, solve_frobenius_moore, + validate_canonical_frobenius_thetas, Ext2, Fp128, Fp32, Fp64, FpExt2, FpExt4, FpExt8, + Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, Prime24Offset3, + Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime48Offset59, + Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, PRIME_OFFSET_IMPLEMENTED_MAX_BITS, + PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, }; /// Backend-independent input and shape failures. diff --git a/crates/jolt-field-two/src/schedules.rs b/crates/jolt-field-two/src/schedules.rs new file mode 100644 index 0000000000..0b090f389c --- /dev/null +++ b/crates/jolt-field-two/src/schedules.rs @@ -0,0 +1,186 @@ +//! Coefficient schedules for the degree-4/8 cyclotomic extension multiply, +//! generic over the lane type. +//! +//! One fold-algebra source of truth per schedule: the same formulas serve +//! the scalar [`PseudoMersenne`](crate::PseudoMersenne) kernel-hook defaults +//! and (via caller-supplied add/sub/mul) the packed SIMD and unreduced +//! integer-lane kernels, which is why they live in the unconditional layer +//! rather than a backend module. Coefficients are in the ring-subfield +//! basis `[1, e1, ...]` with `e_j = zeta^(jm) + zeta^(-jm)`; the basis +//! relations (`e_i·e_j = e_{i+j} + e_{|i−j|}` with `e_0 = 2`, `e_d = 0`, +//! `e_{d+k} = −e_{d−k}`) are already folded into the formulas. +//! +//! Every schedule is purely an additive combination of products, so callers +//! that reduce per operation (field lanes) and callers that defer reduction +//! (integer lanes) are both correct, provided the accumulator has headroom. + +use crate::Ring; +use std::ops::{Add, Mul, Sub}; + +/// Multiplies degree-4 coefficient arrays in the `[1, e1, e2, e3]` basis. +#[inline(always)] +pub(crate) fn ext4_mul_coeffs(a: [A; 4], b: [A; 4]) -> [A; 4] +where + A: Copy + Add + Sub + Mul, +{ + let [a0, a1, a2, a3] = a; + let [b0, b1, b2, b3] = b; + let tail = a1 * b1 + a2 * b2 + a3 * b3; + [ + a0 * b0 + tail + tail, + a0 * b1 + a1 * b0 + a1 * b2 + a2 * b1 + a2 * b3 + a3 * b2, + a0 * b2 + a2 * b0 + a1 * b1 + a1 * b3 + a3 * b1 - a3 * b3, + a0 * b3 + a3 * b0 + a1 * b2 + a2 * b1 - a2 * b3 - a3 * b2, + ] +} + +/// Squares one degree-4 coefficient array in the `[1, e1, e2, e3]` basis. +/// +/// Decomposes `a` over the degree-2 subfield generated by `e2` (with +/// `e2^2 = 2`) as `x + y·e1`, squares with three subfield multiplies, and +/// maps back — fewer base multiplies than the generic 4×4 product. +#[inline(always)] +pub(crate) fn ext4_square_coeffs(a: [A; 4]) -> [A; 4] +where + A: Copy + Add + Sub + Mul, +{ + let [a0, a1, a2, a3] = a; + let x0 = a0; + let x1 = a2; + let y0 = a1 - a3; + let y1 = a3; + + let x0x1 = x0 * x1; + let y0y1 = y0 * y1; + let x1_square = x1 * x1; + let y1_square = y1 * y1; + let aa = (x0 * x0 + x1_square + x1_square, x0x1 + x0x1); + let bb = (y0 * y0 + y1_square + y1_square, y0y1 + y0y1); + + let v0 = x0 * y0; + let v1 = x1 * y1; + let ab = (v0 + v1 + v1, (x0 + x1) * (y0 + y1) - v0 - v1); + let constant = (bb.0 + bb.0 + bb.1 + bb.1, bb.0 + bb.1 + bb.1); + let coeff_e1 = (ab.0 + ab.0, ab.1 + ab.1); + + [ + aa.0 + constant.0, + coeff_e1.0 + coeff_e1.1, + aa.1 + constant.1, + coeff_e1.1, + ] +} + +/// Chebyshev `φ` fold-back for a degree-8 accumulator: maps the product +/// index `k` onto the `[1, e1, ..., e7]` basis (`k = 0 → 2·constant`, +/// `1 ≤ k ≤ 7 → +e_k`, `k = 8 → 0`, `9 ≤ k ≤ 15 → −e_{16−k}`). +#[inline(always)] +fn ext8_add_phi( + out: &mut [V; 8], + idx: usize, + value: V, + add: &impl Fn(V, V) -> V, + sub: &impl Fn(V, V) -> V, +) { + match idx { + 0 => out[0] = add(out[0], add(value, value)), + 1..=7 => out[idx] = add(out[idx], value), + 8 => {} + 9..=15 => out[16 - idx] = sub(out[16 - idx], value), + _ => unreachable!("ext8 Chebyshev index out of range"), + } +} + +/// Karatsuba schedule for the degree-8 multiply in the Chebyshev basis, +/// generic over a lane type `V` and its add/sub/mul. +#[inline(always)] +pub(crate) fn ext8_mul_schedule( + a: [V; 8], + b: [V; 8], + zero: V, + add: A, + sub: S, + mul: M, +) -> [V; 8] +where + V: Copy, + A: Fn(V, V) -> V, + S: Fn(V, V) -> V, + M: Fn(V, V) -> V, +{ + let diag: [V; 8] = std::array::from_fn(|i| mul(a[i], b[i])); + let mut out = [zero; 8]; + out[0] = diag[0]; + + for k in 1..8 { + let mixed = sub(sub(mul(add(a[0], a[k]), add(b[0], b[k])), diag[0]), diag[k]); + out[k] = add(out[k], mixed); + } + + for (i, &diag_i) in diag.iter().enumerate().skip(1) { + out[0] = add(out[0], add(diag_i, diag_i)); + ext8_add_phi(&mut out, i + i, diag_i, &add, &sub); + } + + for i in 1..8 { + for j in (i + 1)..8 { + let mixed = sub(sub(mul(add(a[i], a[j]), add(b[i], b[j])), diag[i]), diag[j]); + ext8_add_phi(&mut out, i + j, mixed, &add, &sub); + ext8_add_phi(&mut out, j - i, mixed, &add, &sub); + } + } + + out +} + +/// [`ext8_mul_schedule`] instantiated at a ring's own arithmetic (the +/// [`PseudoMersenne::ext8_mul`](crate::PseudoMersenne::ext8_mul) default). +#[inline(always)] +pub(crate) fn ext8_mul_coeffs(a: [R; 8], b: [R; 8]) -> [R; 8] { + ext8_mul_schedule(a, b, R::zero(), |x, y| x + y, |x, y| x - y, |x, y| x * y) +} + +/// [`ext8_square_schedule`] instantiated at a ring's own arithmetic (the +/// [`PseudoMersenne::ext8_square`](crate::PseudoMersenne::ext8_square) default). +#[inline(always)] +pub(crate) fn ext8_square_coeffs(a: [R; 8]) -> [R; 8] { + ext8_square_schedule(a, R::zero(), |x, y| x + y, |x, y| x - y, |x, y| x * y) +} + +/// Squaring schedule for the degree-8 extension, generic over a lane type. +/// +/// Uses direct cross products `a_i·a_j` doubled once, saving one add and +/// two subs per cross term versus the Karatsuba mul schedule. +#[inline(always)] +pub(crate) fn ext8_square_schedule(a: [V; 8], zero: V, add: A, sub: S, mul: M) -> [V; 8] +where + V: Copy, + A: Fn(V, V) -> V, + S: Fn(V, V) -> V, + M: Fn(V, V) -> V, +{ + let sq: [V; 8] = std::array::from_fn(|i| mul(a[i], a[i])); + let mut out = [zero; 8]; + out[0] = sq[0]; + + for k in 1..8 { + let cross = mul(a[0], a[k]); + out[k] = add(out[k], add(cross, cross)); + } + + for (i, &sq_i) in sq.iter().enumerate().skip(1) { + out[0] = add(out[0], add(sq_i, sq_i)); + ext8_add_phi(&mut out, i + i, sq_i, &add, &sub); + } + + for i in 1..8 { + for j in (i + 1)..8 { + let cross = mul(a[i], a[j]); + let doubled = add(cross, cross); + ext8_add_phi(&mut out, i + j, doubled, &add, &sub); + ext8_add_phi(&mut out, j - i, doubled, &add, &sub); + } + } + + out +} diff --git a/crates/jolt-field-two/src/solinas/ext.rs b/crates/jolt-field-two/src/solinas/ext.rs new file mode 100644 index 0000000000..9a748a3447 --- /dev/null +++ b/crates/jolt-field-two/src/solinas/ext.rs @@ -0,0 +1,774 @@ +//! Extension towers over the Solinas prime fields: quadratic ([`FpExt2`]), +//! quartic ([`FpExt4`]), and octic ([`FpExt8`]), plus the Frobenius/Moore +//! machinery. +//! +//! `FpExt4`/`FpExt8` use the cyclotomic ring-subfield basis `[1, e1, ...]` +//! (`e_j = zeta^(jm) + zeta^(-jm)`) aligned with trace reduction; there is no +//! alternate power- or tower-basis quartic implementation. Their multiply +//! dispatches through the [`PseudoMersenne`] kernel hooks; every base field +//! keeps the generic-schedule defaults (`crate::schedules`) — the baseline's +//! fused u128-accumulation `Fp32` override lost the checkpoint-6 bench gate +//! (see SPEC.md and `benches/ext4_kernels.rs`). +//! +//! Frobenius powers are intentionally algebraic (raise to powers of the base +//! modulus) rather than basis-specific: one auditable contract first; +//! cheaper specializations can come later. + +#![expect( + clippy::expect_used, + reason = "registered pseudo-Mersenne parameters are a field-type invariant" +)] + +use crate::solinas::pseudo_mersenne_modulus; +use crate::{Ext2Config, ExtField, Field, FieldError, PseudoMersenne, Ring}; +use num_traits::Zero; +use rand_core::RngCore; +use std::marker::PhantomData; + +/// Quadratic extension element `c0 + c1·u` with `u^2 = NR` given by the +/// config `C`. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[cfg_attr( + feature = "allocative", + allocative(bound = "F: Field + allocative::Allocative, C: Ext2Config") +)] +#[repr(transparent)] +pub struct FpExt2> { + /// Coefficients `[c0, c1]` in basis `[1, u]`. + pub coeffs: [F; 2], + _cfg: PhantomData C>, +} + +/// Default quadratic extension used by the Solinas backend. +pub type Ext2 = FpExt2; + +impl> FpExt2 { + /// Constructs `c0 + c1·u`. + #[inline] + pub fn new(c0: F, c1: F) -> Self { + Self { + coeffs: [c0, c1], + _cfg: PhantomData, + } + } + + /// Degree-0 coefficient. + #[inline] + pub fn c0(&self) -> F { + self.coeffs[0] + } + + /// Degree-1 coefficient. + #[inline] + pub fn c1(&self) -> F { + self.coeffs[1] + } + + /// Multiplies a base-field element by the non-residue (a free negation + /// when `C::IS_NEG_ONE`). + #[inline(always)] + fn mul_nr(x: F) -> F { + C::mul_non_residue(x, |base| base) + } + + /// Returns the conjugate `c0 − c1·u`. + #[inline] + pub fn conjugate(self) -> Self { + Self::new(self.coeffs[0], -self.coeffs[1]) + } + + /// Returns the norm in the base field: `c0² − NR·c1²`. + #[inline] + pub fn norm(self) -> F { + (self.coeffs[0] * self.coeffs[0]) - Self::mul_nr(self.coeffs[1] * self.coeffs[1]) + } +} + +// Manual std impls: derives would impose their bounds on the config ZST `C`. +impl> Clone for FpExt2 { + #[inline] + fn clone(&self) -> Self { + *self + } +} +impl> Copy for FpExt2 {} +impl> Default for FpExt2 { + fn default() -> Self { + Self::new(F::zero(), F::zero()) + } +} +impl> PartialEq for FpExt2 { + fn eq(&self, other: &Self) -> bool { + self.coeffs == other.coeffs + } +} +impl> Eq for FpExt2 {} +impl> std::hash::Hash for FpExt2 { + fn hash(&self, state: &mut H) { + self.coeffs.hash(state); + } +} +impl> std::fmt::Debug for FpExt2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FpExt2") + .field("coeffs", &self.coeffs) + .finish() + } +} +impl> std::fmt::Display for FpExt2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.coeffs[0], self.coeffs[1]) + } +} + +crate::impl_ring_ops!(impl[F: Field, C: Ext2Config] FpExt2 { + add(a, b): FpExt2::new(a.coeffs[0] + b.coeffs[0], a.coeffs[1] + b.coeffs[1]), + sub(a, b): FpExt2::new(a.coeffs[0] - b.coeffs[0], a.coeffs[1] - b.coeffs[1]), + // Karatsuba: 3 base multiplies (2 when NR = −1 makes mul_nr free). + mul(a, b): { + let v0 = a.coeffs[0] * b.coeffs[0]; + let v1 = a.coeffs[1] * b.coeffs[1]; + let cross = (a.coeffs[0] + a.coeffs[1]) * (b.coeffs[0] + b.coeffs[1]); + FpExt2::new(v0 + Self::mul_nr(v1), cross - v0 - v1) + }, + neg(a): FpExt2::new(-a.coeffs[0], -a.coeffs[1]), + zero: FpExt2::new(F::zero(), F::zero()), + one: FpExt2::new(F::one(), F::zero()), +}); + +impl> Ring for FpExt2 { + #[inline] + fn from_u64(v: u64) -> Self { + Self::new(F::from_u64(v), F::zero()) + } + #[inline] + fn from_i64(v: i64) -> Self { + Self::new(F::from_i64(v), F::zero()) + } + #[inline] + fn from_u128(v: u128) -> Self { + Self::new(F::from_u128(v), F::zero()) + } + #[inline] + fn from_i128(v: i128) -> Self { + Self::new(F::from_i128(v), F::zero()) + } + + /// Specialized squaring, 2 base multiplies instead of 3: + /// `(c0 + c1·u)² = (c0² + NR·c1²) + (2·c0·c1)·u`. + #[inline(always)] + fn square(&self) -> Self { + let v0 = self.coeffs[0] * self.coeffs[0]; + let v1 = self.coeffs[1] * self.coeffs[1]; + Self::new( + v0 + Self::mul_nr(v1), + (self.coeffs[0] + self.coeffs[0]) * self.coeffs[1], + ) + } +} + +impl> Field for FpExt2 { + /// Inversion via the norm: `x^{-1} = conjugate(x) / norm(x)`. + fn inverse(&self) -> Option { + if self.is_zero() { + return None; + } + let inv_n = self.norm().inverse()?; + Some(Self::new(self.coeffs[0] * inv_n, (-self.coeffs[1]) * inv_n)) + } + + fn random(rng: &mut R) -> Self { + Self::new(F::random(rng), F::random(rng)) + } + + #[inline] + fn half(self) -> Self { + Self::new(self.coeffs[0].half(), self.coeffs[1].half()) + } + + #[inline] + fn two_inv() -> Self { + Self::new(F::two_inv(), F::zero()) + } +} + +impl> serde::Serialize for FpExt2 { + fn serialize(&self, serializer: S) -> Result { + self.coeffs.serialize(serializer) + } +} +impl<'de, F, C> serde::Deserialize<'de> for FpExt2 +where + F: Field + serde::Deserialize<'de>, + C: Ext2Config, +{ + fn deserialize>(deserializer: D) -> Result { + let [c0, c1] = <[F; 2]>::deserialize(deserializer)?; + Ok(Self::new(c0, c1)) + } +} + +/// Quartic extension element in the cyclotomic ring-subfield basis +/// `[1, e1, e2, e3]`. Multiplication dispatches through +/// [`PseudoMersenne::ext4_mul`]. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[cfg_attr( + feature = "allocative", + allocative(bound = "F: Field + allocative::Allocative") +)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Debug)] +#[repr(transparent)] +pub struct FpExt4 { + /// Coefficients in basis `[1, e1, e2, e3]`. + pub coeffs: [F; 4], +} + +impl FpExt4 { + /// Constructs from basis coefficients `[c0, c1, c2, c3]`. + #[inline] + pub fn new(coeffs: [F; 4]) -> Self { + Self { coeffs } + } + + // Arithmetic in the degree-2 subfield generated by e2 (`e2² = 2`), + // used by `inverse` to reduce quartic inversion to base inversion. + #[inline(always)] + fn ext2_mul_by_e2_nr(lhs: (F, F), rhs: (F, F)) -> (F, F) { + let (a0, a1) = lhs; + let (b0, b1) = rhs; + let v0 = a0 * b0; + let v1 = a1 * b1; + let c1 = (a0 + a1) * (b0 + b1) - v0 - v1; + (v0 + v1 + v1, c1) + } + + #[inline(always)] + fn ext2_square_by_e2_nr(x: (F, F)) -> (F, F) { + let (a0, a1) = x; + let a0a1 = a0 * a1; + (a0.square() + a1.square() + a1.square(), a0a1 + a0a1) + } + + #[inline(always)] + fn ext2_mul_by_e1_nr(x: (F, F)) -> (F, F) { + let (x0, x1) = x; + (x0 + x0 + x1 + x1, x0 + x1 + x1) + } + + #[inline(always)] + fn ext2_inverse_by_e2_nr(x: (F, F)) -> Option<(F, F)> { + let (x0, x1) = x; + let inv_norm = (x0.square() - (x1.square() + x1.square())).inverse()?; + Some((x0 * inv_norm, -x1 * inv_norm)) + } +} + +impl std::fmt::Display for FpExt4 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let [c0, c1, c2, c3] = self.coeffs; + write!(f, "({c0}, {c1}, {c2}, {c3})") + } +} + +crate::impl_ring_ops!(impl[F: PseudoMersenne] FpExt4 { + add(a, b): FpExt4::new(std::array::from_fn(|i| a.coeffs[i] + b.coeffs[i])), + sub(a, b): FpExt4::new(std::array::from_fn(|i| a.coeffs[i] - b.coeffs[i])), + mul(a, b): FpExt4::new(F::ext4_mul(a.coeffs, b.coeffs)), + neg(a): FpExt4::new(std::array::from_fn(|i| -a.coeffs[i])), + zero: FpExt4::new([F::zero(); 4]), + one: FpExt4::new([F::one(), F::zero(), F::zero(), F::zero()]), +}); + +impl Ring for FpExt4 { + #[inline] + fn from_u64(v: u64) -> Self { + Self::new([F::from_u64(v), F::zero(), F::zero(), F::zero()]) + } + #[inline] + fn from_i64(v: i64) -> Self { + Self::new([F::from_i64(v), F::zero(), F::zero(), F::zero()]) + } + #[inline] + fn from_u128(v: u128) -> Self { + Self::new([F::from_u128(v), F::zero(), F::zero(), F::zero()]) + } + #[inline] + fn from_i128(v: i128) -> Self { + Self::new([F::from_i128(v), F::zero(), F::zero(), F::zero()]) + } + + #[inline(always)] + fn square(&self) -> Self { + Self::new(F::ext4_square(self.coeffs)) + } +} + +impl Field for FpExt4 { + /// Inversion via the subfield tower: write `self = a + b·e1` over the + /// e2-subfield, invert the norm `a² − e1²·b²` there, then one base + /// inversion. + fn inverse(&self) -> Option { + if self.is_zero() { + return None; + } + let [a0, a1, a2, a3] = self.coeffs; + let a = (a0, a2); + let b = (a1 - a3, a3); + + let aa = Self::ext2_square_by_e2_nr(a); + let bb = Self::ext2_square_by_e2_nr(b); + let norm = { + let nr_bb = Self::ext2_mul_by_e1_nr(bb); + (aa.0 - nr_bb.0, aa.1 - nr_bb.1) + }; + let inv_norm = Self::ext2_inverse_by_e2_nr(norm)?; + let constant = Self::ext2_mul_by_e2_nr(a, inv_norm); + let e1_coeff = Self::ext2_mul_by_e2_nr((-b.0, -b.1), inv_norm); + + Some(Self::new([ + constant.0, + e1_coeff.0 + e1_coeff.1, + constant.1, + e1_coeff.1, + ])) + } + + fn random(rng: &mut R) -> Self { + Self::new(std::array::from_fn(|_| F::random(rng))) + } + + #[inline] + fn half(self) -> Self { + Self::new(self.coeffs.map(F::half)) + } + + #[inline] + fn two_inv() -> Self { + Self::new([F::two_inv(), F::zero(), F::zero(), F::zero()]) + } +} + +impl serde::Serialize for FpExt4 { + fn serialize(&self, serializer: S) -> Result { + self.coeffs.serialize(serializer) + } +} +impl<'de, F: Field + serde::Deserialize<'de>> serde::Deserialize<'de> for FpExt4 { + fn deserialize>(deserializer: D) -> Result { + Ok(Self::new(<[F; 4]>::deserialize(deserializer)?)) + } +} + +/// Octic extension element in the Chebyshev basis `[1, e1, ..., e7]`. +/// Multiplication dispatches through [`PseudoMersenne::ext8_mul`]. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[cfg_attr( + feature = "allocative", + allocative(bound = "F: Field + allocative::Allocative") +)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Debug)] +#[repr(transparent)] +pub struct FpExt8 { + /// Coefficients in basis `[1, e1, ..., e7]`. + pub coeffs: [F; 8], +} + +impl FpExt8 { + /// Constructs from canonical basis coefficients. + #[inline] + pub fn new(coeffs: [F; 8]) -> Self { + Self { coeffs } + } + + #[inline] + fn from_constant(c: F) -> Self { + let mut coeffs = [F::zero(); 8]; + coeffs[0] = c; + Self::new(coeffs) + } +} + +impl std::fmt::Display for FpExt8 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let [c0, c1, c2, c3, c4, c5, c6, c7] = self.coeffs; + write!(f, "({c0}, {c1}, {c2}, {c3}, {c4}, {c5}, {c6}, {c7})") + } +} + +crate::impl_ring_ops!(impl[F: PseudoMersenne] FpExt8 { + add(a, b): FpExt8::new(std::array::from_fn(|i| a.coeffs[i] + b.coeffs[i])), + sub(a, b): FpExt8::new(std::array::from_fn(|i| a.coeffs[i] - b.coeffs[i])), + mul(a, b): FpExt8::new(F::ext8_mul(a.coeffs, b.coeffs)), + neg(a): FpExt8::new(std::array::from_fn(|i| -a.coeffs[i])), + zero: FpExt8::new([F::zero(); 8]), + one: FpExt8::from_constant(F::one()), +}); + +impl Ring for FpExt8 { + #[inline] + fn from_u64(v: u64) -> Self { + Self::from_constant(F::from_u64(v)) + } + #[inline] + fn from_i64(v: i64) -> Self { + Self::from_constant(F::from_i64(v)) + } + #[inline] + fn from_u128(v: u128) -> Self { + Self::from_constant(F::from_u128(v)) + } + #[inline] + fn from_i128(v: i128) -> Self { + Self::from_constant(F::from_i128(v)) + } + + /// Squaring via the dedicated schedule (fewer base ops than the mul + /// schedule; identical field result). + #[inline(always)] + fn square(&self) -> Self { + Self::new(F::ext8_square(self.coeffs)) + } +} + +impl Field for FpExt8 { + /// Inversion by dense Gaussian elimination on the 8×8 multiplication + /// matrix — explicit and auditable; octic inversion is not a hot path. + fn inverse(&self) -> Option { + if self.is_zero() { + return None; + } + + let mut aug = [[F::zero(); 9]; 8]; + for col in 0..8 { + let mut basis = [F::zero(); 8]; + basis[col] = F::one(); + let product = *self * Self::new(basis); + for (row, coeff) in product.coeffs.iter().copied().enumerate() { + aug[row][col] = coeff; + } + } + aug[0][8] = F::one(); + + for col in 0..8 { + let pivot = (col..8).find(|&row| !aug[row][col].is_zero())?; + if pivot != col { + aug.swap(col, pivot); + } + let inv = aug[col][col].inverse()?; + for entry in &mut aug[col][col..=8] { + *entry *= inv; + } + for row in 0..8 { + if row == col { + continue; + } + let factor = aug[row][col]; + if factor.is_zero() { + continue; + } + let pivot_row = aug[col]; + for (target, pivot) in aug[row][col..=8] + .iter_mut() + .zip(pivot_row[col..=8].iter().copied()) + { + *target -= factor * pivot; + } + } + } + + Some(Self::new(std::array::from_fn(|i| aug[i][8]))) + } + + fn random(rng: &mut R) -> Self { + Self::new(std::array::from_fn(|_| F::random(rng))) + } + + #[inline] + fn half(self) -> Self { + Self::new(self.coeffs.map(F::half)) + } + + #[inline] + fn two_inv() -> Self { + Self::from_constant(F::two_inv()) + } +} + +impl serde::Serialize for FpExt8 { + fn serialize(&self, serializer: S) -> Result { + self.coeffs.serialize(serializer) + } +} +impl<'de, F: Field + serde::Deserialize<'de>> serde::Deserialize<'de> for FpExt8 { + fn deserialize>(deserializer: D) -> Result { + Ok(Self::new(<[F; 8]>::deserialize(deserializer)?)) + } +} + +#[inline] +fn field_pow_u128(mut base: E, mut exp: u128) -> E { + let mut acc = E::one(); + while exp > 0 { + if (exp & 1) == 1 { + acc *= base; + } + base *= base; + exp >>= 1; + } + acc +} + +#[inline] +fn base_modulus() -> u128 { + pseudo_mersenne_modulus(F::MODULUS_BITS, F::OFFSET) + .expect("pseudo-Mersenne modulus parameters must be valid") +} + +fn frobenius_pow_via_base_modulus(value: E, power: usize) -> E +where + F: PseudoMersenne, + E: ExtField, +{ + let q = base_modulus::(); + let mut out = value; + for _ in 0..(power % E::DEGREE.max(1)) { + out = field_pow_u128(out, q); + } + out +} + +/// A pseudo-Mersenne base field is its own degree-1 extension. +impl ExtField for F { + const DEGREE: usize = 1; + + #[inline] + fn lift_base(x: F) -> Self { + x + } + + #[inline] + fn mul_base(self, x: F) -> Self { + self * x + } + + #[inline] + fn from_base_slice(coeffs: &[F]) -> Self { + assert_eq!(coeffs.len(), 1); + coeffs[0] + } + + #[inline] + fn to_base_vec(&self) -> Vec { + vec![*self] + } + + /// Frobenius is the identity on the prime field. + #[inline] + fn frobenius_pow(self, _power: usize) -> Self { + self + } +} + +impl> ExtField for FpExt2 { + const DEGREE: usize = 2; + + #[inline] + fn lift_base(x: F) -> Self { + Self::new(x, F::zero()) + } + + #[inline] + fn mul_base(self, x: F) -> Self { + Self::new(self.coeffs[0] * x, self.coeffs[1] * x) + } + + #[inline] + fn from_base_slice(coeffs: &[F]) -> Self { + assert_eq!(coeffs.len(), 2); + Self::new(coeffs[0], coeffs[1]) + } + + #[inline] + fn to_base_vec(&self) -> Vec { + self.coeffs.to_vec() + } + + #[inline] + fn frobenius_pow(self, power: usize) -> Self { + frobenius_pow_via_base_modulus::(self, power) + } +} + +impl ExtField for FpExt4 { + const DEGREE: usize = 4; + + #[inline] + fn lift_base(x: F) -> Self { + Self::new([x, F::zero(), F::zero(), F::zero()]) + } + + #[inline] + fn mul_base(self, x: F) -> Self { + Self::new(self.coeffs.map(|c| c * x)) + } + + #[inline] + fn from_base_slice(coeffs: &[F]) -> Self { + assert_eq!(coeffs.len(), 4); + Self::new([coeffs[0], coeffs[1], coeffs[2], coeffs[3]]) + } + + #[inline] + fn to_base_vec(&self) -> Vec { + self.coeffs.to_vec() + } + + #[inline] + fn frobenius_pow(self, power: usize) -> Self { + frobenius_pow_via_base_modulus::(self, power) + } +} + +impl ExtField for FpExt8 { + const DEGREE: usize = 8; + + #[inline] + fn lift_base(x: F) -> Self { + Self::from_constant(x) + } + + #[inline] + fn mul_base(self, x: F) -> Self { + Self::new(self.coeffs.map(|c| c * x)) + } + + #[inline] + fn from_base_slice(coeffs: &[F]) -> Self { + assert_eq!(coeffs.len(), 8); + Self::new(std::array::from_fn(|i| coeffs[i])) + } + + #[inline] + fn to_base_vec(&self) -> Vec { + self.coeffs.to_vec() + } + + #[inline] + fn frobenius_pow(self, power: usize) -> Self { + frobenius_pow_via_base_modulus::(self, power) + } +} + +/// Returns the first `width` elements of the canonical extension basis. +/// +/// For [`FpExt4`]/[`FpExt8`] this is the ring-subfield basis `[1, e1, ...]`, +/// so the chosen Moore-type theta family is aligned with the coefficient +/// packing basis. +/// +/// # Errors +/// +/// Returns an error if `width > E::DEGREE`. +pub fn canonical_frobenius_thetas(width: usize) -> Result, FieldError> +where + F: Field, + E: ExtField, +{ + if width > E::DEGREE { + return Err(FieldError::InvalidInput(format!( + "Frobenius theta width {width} exceeds extension degree {}", + E::DEGREE + ))); + } + Ok((0..width) + .map(|idx| { + let mut coeffs = vec![F::zero(); E::DEGREE]; + coeffs[idx] = F::one(); + E::from_base_slice(&coeffs) + }) + .collect()) +} + +/// Solves `M_t(theta) z = r`, where `M_t(theta)_{j,h} = theta_h^(q^-j)`. +/// +/// Dense elimination on purpose: supported Frobenius widths are tiny +/// (`≤ [E:F]`) and explicit validation beats a clever specialized solver. +/// +/// # Errors +/// +/// Returns an error on a dimension mismatch or a singular Moore-type matrix. +pub fn solve_frobenius_moore(thetas: &[E], rhs: &[E]) -> Result, FieldError> +where + F: PseudoMersenne, + E: ExtField, +{ + let n = thetas.len(); + if rhs.len() != n { + return Err(FieldError::InvalidSize { + expected: n, + actual: rhs.len(), + }); + } + let mut matrix = (0..n) + .map(|row| { + thetas + .iter() + .map(|&theta| theta.frobenius_inv_pow(row)) + .collect::>() + }) + .collect::>(); + let mut values = rhs.to_vec(); + + for col in 0..n { + let pivot = (col..n) + .find(|&row| !matrix[row][col].is_zero()) + .ok_or_else(|| { + FieldError::InvalidInput("singular Frobenius Moore-type matrix".to_string()) + })?; + if pivot != col { + matrix.swap(col, pivot); + values.swap(col, pivot); + } + let inv = matrix[col][col].inverse().ok_or_else(|| { + FieldError::InvalidInput("singular Frobenius Moore-type matrix".to_string()) + })?; + for entry in &mut matrix[col][col..] { + *entry *= inv; + } + values[col] *= inv; + + let pivot_tail = matrix[col][col..].to_vec(); + let pivot_value = values[col]; + for row in 0..n { + if row == col { + continue; + } + let factor = matrix[row][col]; + if factor.is_zero() { + continue; + } + for (entry, &pivot_entry) in matrix[row][col..].iter_mut().zip(pivot_tail.iter()) { + *entry -= factor * pivot_entry; + } + values[row] -= factor * pivot_value; + } + } + Ok(values) +} + +/// Validates that the canonical theta family gives a nonsingular Moore-type +/// matrix for `width`. +/// +/// # Errors +/// +/// Returns an error if theta construction fails or the Moore solve rejects. +pub fn validate_canonical_frobenius_thetas(width: usize) -> Result<(), FieldError> +where + F: PseudoMersenne, + E: ExtField, +{ + let thetas = canonical_frobenius_thetas::(width)?; + let rhs = (0..width) + .map(|idx| E::lift_base(F::from_u64((idx + 1) as u64))) + .collect::>(); + solve_frobenius_moore::(&thetas, &rhs).map(|_| ()) +} diff --git a/crates/jolt-field-two/src/solinas/mod.rs b/crates/jolt-field-two/src/solinas/mod.rs index 4517ebbe58..9fd5f0955b 100644 --- a/crates/jolt-field-two/src/solinas/mod.rs +++ b/crates/jolt-field-two/src/solinas/mod.rs @@ -4,9 +4,14 @@ //! algebra; `fp128.rs` is the hand-written two-limb field; this module holds //! the family trait, the `2^k − offset` registry, and shared helpers. +mod ext; mod fp128; mod word; +pub use ext::{ + canonical_frobenius_thetas, solve_frobenius_moore, validate_canonical_frobenius_thetas, Ext2, + FpExt2, FpExt4, FpExt8, +}; pub use fp128::Fp128; pub use word::{Fp32, Fp64}; diff --git a/crates/jolt-field-two/src/solinas/word.rs b/crates/jolt-field-two/src/solinas/word.rs index e8e8c169b6..1dc7a77ac9 100644 --- a/crates/jolt-field-two/src/solinas/word.rs +++ b/crates/jolt-field-two/src/solinas/word.rs @@ -345,6 +345,10 @@ macro_rules! define_solinas_prime { type Accumulator = NaiveAccumulator; } + // The ext-mul kernel hooks keep their generic-schedule defaults: + // the baseline's fused u128-accumulation Fp32 override lost the + // checkpoint-6 bench gate (see SPEC.md dropped-specialization + // evidence and benches/ext4_kernels.rs). impl PseudoMersenne for $name

{ const OFFSET: u128 = Self::C as u128; } diff --git a/crates/jolt-field-two/tests/solinas_ext_differential.rs b/crates/jolt-field-two/tests/solinas_ext_differential.rs new file mode 100644 index 0000000000..3a3198f231 --- /dev/null +++ b/crates/jolt-field-two/tests/solinas_ext_differential.rs @@ -0,0 +1,692 @@ +//! Differential tests for the extension towers (`FpExt2`/`FpExt4`/`FpExt8`) +//! against jolt-field, with an independent schoolbook oracle: polynomial +//! multiplication modulo the defining relation implemented directly here +//! over `u128` values (256-bit limb multiply + binary long division for the +//! base-field modular ops — no Solinas folding, no shared code). +//! +//! Coverage: both `FpExt2` non-residue configs and the quartic/octic towers +//! over `Fp32`/`Fp64`/`Fp128` bases (registered primes plus `Fp32<251>`, +//! the one small prime with `p ≡ 3 mod 4` where `NegOneNr` is a genuine +//! field, and `Fp64<2^32 − 99>`, the base the baseline's own ext tests +//! use). Where the extension is not known to be a field (reducible defining +//! polynomial), every check is strict parity with the baseline rather than +//! a field identity. + +#![cfg(feature = "solinas")] +#![expect(clippy::unwrap_used, reason = "test code")] + +use jolt_field as base; +use jolt_field_two as two; + +use base::{ + CanonicalField, ExtField as BaseExtField, FieldCore, FromPrimitiveInt, HalvingField, RingCore, +}; +use num_traits::{One, Zero}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use two::{CanonicalEncoding, ExtField, Field, Ring}; + +fn rng() -> ChaCha20Rng { + ChaCha20Rng::seed_from_u64(0xE87_D1FF) +} + +/// 128×128 → 256-bit schoolbook multiply over 64-bit halves (independent of +/// both crates' `mul_wide`). +fn oracle_mul_256(a: u128, b: u128) -> [u64; 4] { + let (a0, a1) = (a as u64 as u128, a >> 64); + let (b0, b1) = (b as u64 as u128, b >> 64); + let (p00, p01, p10, p11) = (a0 * b0, a0 * b1, a1 * b0, a1 * b1); + const LO: u128 = u64::MAX as u128; + let mid = (p00 >> 64) + (p01 & LO) + (p10 & LO); + let hi = (p01 >> 64) + (p10 >> 64) + (p11 & LO) + (mid >> 64); + let top = (p11 >> 64) + (hi >> 64); + [p00 as u64, mid as u64, hi as u64, top as u64] +} + +/// Little-endian limbs mod `p` by binary long division — no Solinas folding. +fn oracle_mod(limbs: &[u64], p: u128) -> u128 { + let mut r: u128 = 0; + for &limb in limbs.iter().rev() { + for i in (0..64).rev() { + let top = r >> 127; + let mut v = (r << 1) | ((limb >> i) & 1) as u128; + if top == 1 { + v = v.wrapping_add(0u128.wrapping_sub(p)); + } else if v >= p { + v -= p; + } + r = v; + } + } + r +} + +fn mulmod(a: u128, b: u128, p: u128) -> u128 { + oracle_mod(&oracle_mul_256(a, b), p) +} + +fn addmod(a: u128, b: u128, p: u128) -> u128 { + let (s, overflow) = a.overflowing_add(b); + oracle_mod(&[s as u64, (s >> 64) as u64, overflow as u64], p) +} + +fn submod(a: u128, b: u128, p: u128) -> u128 { + addmod(a, p - b, p) +} + +/// Schoolbook multiply in `F[u]/(u² − nr)`. +fn quad_mul_oracle(a: &[u128], b: &[u128], nr: u128, p: u128) -> Vec { + vec![ + addmod( + mulmod(a[0], b[0], p), + mulmod(nr, mulmod(a[1], b[1], p), p), + p, + ), + addmod(mulmod(a[0], b[1], p), mulmod(a[1], b[0], p), p), + ] +} + +/// Schoolbook multiply in the Chebyshev ring-subfield basis `[1, e1, ..., +/// e_{d−1}]` with `e_j = ζ^{jm} + ζ^{−jm}`: `e_i·e_j = φ(i+j) + φ(|i−j|)` +/// where `φ(0) = 2`, `φ(k) = e_k` for `k < d`, `φ(d) = 0`, and +/// `φ(k) = −e_{2d−k}` for `k > d` — implemented as a plain double loop. +fn cheb_mul_oracle(a: &[u128], b: &[u128], p: u128) -> Vec { + let d = a.len(); + assert_eq!(b.len(), d); + let mut out = vec![0u128; d]; + let phi = |out: &mut Vec, k: usize, t: u128| { + if k == 0 { + out[0] = addmod(out[0], addmod(t, t, p), p); + } else if k < d { + out[k] = addmod(out[k], t, p); + } else if k > d { + let m = 2 * d - k; + out[m] = submod(out[m], t, p); + } + }; + for i in 0..d { + for j in 0..d { + let t = mulmod(a[i], b[j], p); + if i == 0 && j == 0 { + out[0] = addmod(out[0], t, p); + } else if i == 0 { + out[j] = addmod(out[j], t, p); + } else if j == 0 { + out[i] = addmod(out[i], t, p); + } else { + phi(&mut out, i + j, t); + phi(&mut out, i.abs_diff(j), t); + } + } + } + out +} + +/// Full differential + oracle sweep for one paired extension instantiation. +/// +/// `is_field: false` keeps every check strict parity with the baseline but +/// does not require nonzero elements to invert (reducible defining +/// polynomial over that base). +macro_rules! check_ext { + ($E2:ty, $EB:ty, $F2:ty, $FB:ty, $p:expr, $d:expr, $oracle:expr, is_field: $is_field:expr, $rng:expr) => {{ + let p: u128 = $p; + let d: usize = $d; + let oracle = $oracle; + assert_eq!(<$E2 as ExtField<$F2>>::DEGREE, d); + assert_eq!(<$EB as BaseExtField<$FB>>::EXT_DEGREE, d); + assert_eq!( + std::mem::size_of::<$E2>(), + std::mem::size_of::<[$F2; $d]>(), + "extension must be a plain coefficient array" + ); + + let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); + let fb = |v: u128| <$FB as CanonicalField>::from_canonical_u128_checked(v).unwrap(); + let mk2 = |vals: &[u128]| { + <$E2 as ExtField<$F2>>::from_base_slice( + &vals.iter().map(|&v| f2(v)).collect::>(), + ) + }; + let mkb = |vals: &[u128]| { + <$EB as BaseExtField<$FB>>::from_base_slice( + &vals.iter().map(|&v| fb(v)).collect::>(), + ) + }; + let vec2 = |e: &$E2| { + <$E2 as ExtField<$F2>>::to_base_vec(e) + .iter() + .map(|c| c.to_u128_checked().unwrap()) + .collect::>() + }; + let vecb = |e: &$EB| { + <$EB as BaseExtField<$FB>>::to_base_vec(e) + .iter() + .map(|c| c.to_canonical_u128()) + .collect::>() + }; + let sample = |rng: &mut ChaCha20Rng| -> Vec { + (0..d).map(|_| rng.gen::() % p).collect() + }; + + let cfg = bincode::config::standard(); + for _ in 0..48 { + let (va, vb, vc) = (sample($rng), sample($rng), sample($rng)); + let (xa, ba) = (mk2(&va), mkb(&va)); + let (ya, yb) = (mk2(&vb), mkb(&vb)); + let (za, _zb) = (mk2(&vc), mkb(&vc)); + + // from_base_slice / to_base_vec round trip. + assert_eq!(vec2(&xa), va); + assert_eq!(vecb(&ba), va); + + // Arithmetic vs baseline, mul/square also vs the schoolbook oracle. + assert_eq!(vec2(&(xa + ya)), vecb(&(ba + yb))); + assert_eq!(vec2(&(xa - ya)), vecb(&(ba - yb))); + assert_eq!(vec2(&(-xa)), vecb(&(-ba))); + let prod = xa * ya; + assert_eq!(vec2(&prod), vecb(&(ba * yb))); + assert_eq!(vec2(&prod), oracle(&va, &vb), "mul vs schoolbook oracle"); + let sq = Ring::square(&xa); + assert_eq!(vec2(&sq), vecb(&RingCore::square(&ba))); + assert_eq!(vec2(&sq), oracle(&va, &va), "square vs schoolbook oracle"); + + // By-ref and assigning operator forms agree with the owned ones. + assert_eq!(xa + &ya, xa + ya); + assert_eq!(xa - &ya, xa - ya); + assert_eq!(xa * &ya, xa * ya); + let (mut s, mut t, mut u) = (xa, xa, xa); + s += ya; + t -= ya; + u *= ya; + assert_eq!((s, t, u), (xa + ya, xa - ya, xa * ya)); + + // Ring identities. + assert_eq!((xa + ya) * za, xa * za + ya * za, "distributivity"); + assert_eq!((xa * ya) * za, xa * (ya * za), "associativity"); + + // Inversion: strict parity, plus the field identity when Some. + match (xa.inverse(), ba.inverse()) { + (Some(ti), Some(bi)) => { + assert_eq!(vec2(&ti), vecb(&bi)); + assert_eq!(ti * xa, <$E2 as One>::one()); + } + (ti, bi) => { + assert_eq!(ti.is_none(), bi.is_none(), "inverse parity"); + assert!(!$is_field || xa.is_zero(), "field ext must invert nonzero"); + } + } + + // Halving. + let h = xa.half(); + assert_eq!(vec2(&h), vecb(&ba.half())); + assert_eq!(h + h, xa); + + // lift_base / mul_base against the full extension multiply. + let sv = $rng.gen::() % p; + let (s2, sb) = (f2(sv), fb(sv)); + let m2 = xa.mul_base(s2); + assert_eq!( + m2, + xa * <$E2 as ExtField<$F2>>::lift_base(s2), + "mul_base vs full multiply" + ); + assert_eq!(vec2(&m2), vecb(&ba.mul_base(sb))); + assert_eq!( + vec2(&<$E2 as ExtField<$F2>>::lift_base(s2)), + vecb(&<$EB as BaseExtField<$FB>>::lift_base(sb)) + ); + + // Integer embeddings. + let (w64, i64v): (u64, i64) = ($rng.gen(), $rng.gen()); + let (w128, i128v): (u128, i128) = ($rng.gen(), $rng.gen()); + assert_eq!( + vec2(&<$E2 as Ring>::from_u64(w64)), + vecb(&<$EB as FromPrimitiveInt>::from_u64(w64)) + ); + assert_eq!( + vec2(&<$E2 as Ring>::from_i64(i64v)), + vecb(&<$EB as FromPrimitiveInt>::from_i64(i64v)) + ); + assert_eq!( + vec2(&<$E2 as Ring>::from_u128(w128)), + vecb(&<$EB as FromPrimitiveInt>::from_u128(w128)) + ); + assert_eq!( + vec2(&<$E2 as Ring>::from_i128(i128v)), + vecb(&<$EB as FromPrimitiveInt>::from_i128(i128v)) + ); + + // Wire bytes: baseline equality, structural shape, round trip. + let t_bytes = bincode::serde::encode_to_vec(xa, cfg).unwrap(); + let b_bytes = bincode::serde::encode_to_vec(ba, cfg).unwrap(); + assert_eq!(t_bytes, b_bytes, "wire bytes diverge"); + let expected: Vec = <$E2 as ExtField<$F2>>::to_base_vec(&xa) + .iter() + .flat_map(|c| c.to_bytes_le_vec()) + .collect(); + assert_eq!(t_bytes, expected, "wire bytes = concatenated coeff bytes"); + let (back, _): ($E2, usize) = bincode::serde::decode_from_slice(&t_bytes, cfg).unwrap(); + assert_eq!(back, xa); + } + + // Frobenius powers 0..2·degree: parity for pow, inv_pow, and the + // roundtrip (the roundtrip equals the identity in a genuine field). + for _ in 0..2 { + let v = sample($rng); + let (x2, xb2) = (mk2(&v), mkb(&v)); + for power in 0..=(2 * d) { + let ft = <$E2 as ExtField<$F2>>::frobenius_pow(x2, power); + let fb2 = <$EB as BaseExtField<$FB>>::frobenius_pow(xb2, power); + assert_eq!(vec2(&ft), vecb(&fb2), "frobenius_pow({power})"); + let gt = <$E2 as ExtField<$F2>>::frobenius_inv_pow(x2, power); + let gb = <$EB as BaseExtField<$FB>>::frobenius_inv_pow(xb2, power); + assert_eq!(vec2(>), vecb(&gb), "frobenius_inv_pow({power})"); + let rt = <$E2 as ExtField<$F2>>::frobenius_inv_pow(ft, power); + let rb = <$EB as BaseExtField<$FB>>::frobenius_inv_pow(fb2, power); + assert_eq!(vec2(&rt), vecb(&rb), "frobenius roundtrip parity"); + if $is_field { + assert_eq!(rt, x2, "frobenius roundtrip is the identity"); + } + } + } + + // Boundary coefficient patterns: all-zero, all-max, single-nonzero + // (1 and p−1) per position — worst cases for the fused Fp32 kernel's + // column sums. + let mut patterns: Vec> = vec![vec![0; d], vec![p - 1; d]]; + for i in 0..d { + let mut v = vec![0u128; d]; + v[i] = 1; + patterns.push(v.clone()); + v[i] = p - 1; + patterns.push(v); + } + for va in &patterns { + for vb in &patterns { + let (x2, xb2) = (mk2(va), mkb(va)); + let (y2, yb2) = (mk2(vb), mkb(vb)); + let prod = x2 * y2; + assert_eq!(vec2(&prod), vecb(&(xb2 * yb2)), "boundary {va:?}·{vb:?}"); + assert_eq!(vec2(&prod), oracle(va, vb), "boundary vs oracle"); + } + let x2 = mk2(va); + assert_eq!(vec2(&Ring::square(&x2)), oracle(va, va), "boundary square"); + } + + // Zero/One and iterator Sum/Product (owned and by-ref). + assert!(<$E2 as Zero>::zero().is_zero()); + let one_vec = { + let mut v = vec![0u128; d]; + v[0] = 1; + v + }; + assert_eq!(vec2(&<$E2 as One>::one()), one_vec); + let xs: Vec<$E2> = (0..7).map(|_| mk2(&sample($rng))).collect(); + let expected_sum = xs.iter().fold(<$E2 as Zero>::zero(), |a, &x| a + x); + let expected_prod = xs.iter().fold(<$E2 as One>::one(), |a, &x| a * x); + assert_eq!(xs.iter().copied().sum::<$E2>(), expected_sum); + assert_eq!(xs.iter().sum::<$E2>(), expected_sum); + assert_eq!(xs.iter().copied().product::<$E2>(), expected_prod); + assert_eq!(xs.iter().product::<$E2>(), expected_prod); + + // Canonical rejection: a wire encoding whose first coefficient is + // `p` itself must be rejected by both crates; so must short input. + let nb = <$F2 as CanonicalEncoding>::NUM_BYTES; + let mut bad = vec![0u8; nb * d]; + bad[..nb].copy_from_slice(&p.to_le_bytes()[..nb]); + assert!( + bincode::serde::decode_from_slice::<$E2, _>(&bad, cfg).is_err(), + "non-canonical coefficient must be rejected" + ); + assert!( + bincode::serde::decode_from_slice::<$EB, _>(&bad, cfg).is_err(), + "baseline rejects the same encoding" + ); + assert!( + bincode::serde::decode_from_slice::<$E2, _>(&bad[..nb * d - 1], cfg).is_err(), + "truncated encoding must be rejected" + ); + + // Identical random sampling: same seed, same element stream. + let (mut r1, mut r2) = ( + ChaCha20Rng::seed_from_u64(0x5EED_0001), + ChaCha20Rng::seed_from_u64(0x5EED_0001), + ); + for _ in 0..20 { + let t: $E2 = Field::random(&mut r1); + let b: $EB = FieldCore::random(&mut r2); + assert_eq!(vec2(&t), vecb(&b), "random stream diverges"); + } + }}; +} + +/// Frobenius/Moore machinery parity: canonical thetas, validate, solve +/// (values and Ok/Err), plus rejection cases. +macro_rules! check_moore { + ($E2:ty, $EB:ty, $F2:ty, $FB:ty, $p:expr, $d:expr, $rng:expr) => {{ + let p: u128 = $p; + let d: usize = $d; + let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); + let fb = |v: u128| <$FB as CanonicalField>::from_canonical_u128_checked(v).unwrap(); + let mk2 = |vals: &[u128]| { + <$E2 as ExtField<$F2>>::from_base_slice( + &vals.iter().map(|&v| f2(v)).collect::>(), + ) + }; + let mkb = |vals: &[u128]| { + <$EB as BaseExtField<$FB>>::from_base_slice( + &vals.iter().map(|&v| fb(v)).collect::>(), + ) + }; + let vec2 = |e: &$E2| { + <$E2 as ExtField<$F2>>::to_base_vec(e) + .iter() + .map(|c| c.to_u128_checked().unwrap()) + .collect::>() + }; + let vecb = |e: &$EB| { + <$EB as BaseExtField<$FB>>::to_base_vec(e) + .iter() + .map(|c| c.to_canonical_u128()) + .collect::>() + }; + + for w in 1..=d { + let t = two::canonical_frobenius_thetas::<$F2, $E2>(w).unwrap(); + let b = base::canonical_frobenius_thetas::<$FB, $EB>(w).unwrap(); + assert_eq!(t.len(), w); + for (idx, (te, be)) in t.iter().zip(b.iter()).enumerate() { + assert_eq!(vec2(te), vecb(be), "theta {idx} parity"); + let mut basis = vec![0u128; d]; + basis[idx] = 1; + assert_eq!(vec2(te), basis, "thetas are the packing basis"); + } + let tv = two::validate_canonical_frobenius_thetas::<$F2, $E2>(w); + let bv = base::validate_canonical_frobenius_thetas::<$FB, $EB>(w); + assert_eq!(tv.is_ok(), bv.is_ok(), "validate parity at width {w}"); + } + assert!(two::canonical_frobenius_thetas::<$F2, $E2>(d + 1).is_err()); + assert!(base::canonical_frobenius_thetas::<$FB, $EB>(d + 1).is_err()); + + let thetas_t = two::canonical_frobenius_thetas::<$F2, $E2>(d).unwrap(); + let thetas_b = base::canonical_frobenius_thetas::<$FB, $EB>(d).unwrap(); + let rhs_vals: Vec> = (0..d) + .map(|_| (0..d).map(|_| $rng.gen::() % p).collect()) + .collect(); + let rhs_t: Vec<$E2> = rhs_vals.iter().map(|v| mk2(v)).collect(); + let rhs_b: Vec<$EB> = rhs_vals.iter().map(|v| mkb(v)).collect(); + let st = two::solve_frobenius_moore::<$F2, $E2>(&thetas_t, &rhs_t); + let sb = base::solve_frobenius_moore::<$FB, $EB>(&thetas_b, &rhs_b); + assert_eq!(st.is_ok(), sb.is_ok(), "solve parity"); + if let (Ok(zt), Ok(zb)) = (st, sb) { + for (a, b) in zt.iter().zip(zb.iter()) { + assert_eq!(vec2(a), vecb(b), "Moore solution parity"); + } + // The solution satisfies the Moore system in rebuilt arithmetic. + for (row, want) in rhs_t.iter().enumerate() { + let got = thetas_t + .iter() + .zip(zt.iter()) + .fold(<$E2 as Zero>::zero(), |acc, (&th, &z)| { + acc + <$E2 as ExtField<$F2>>::frobenius_inv_pow(th, row) * z + }); + assert_eq!(got, *want, "Moore row {row} unsatisfied"); + } + } + + // Rejections: duplicate thetas (singular) and dimension mismatch. + if d >= 2 { + let one2 = <$E2 as One>::one(); + let oneb = <$EB as One>::one(); + assert!( + two::solve_frobenius_moore::<$F2, $E2>(&[one2, one2], &[one2, one2]).is_err(), + "duplicate thetas must be singular" + ); + assert!(base::solve_frobenius_moore::<$FB, $EB>(&[oneb, oneb], &[oneb, oneb]).is_err()); + } + assert!( + two::solve_frobenius_moore::<$F2, $E2>(&thetas_t, &rhs_t[..d - 1]).is_err(), + "dimension mismatch must be rejected" + ); + }}; +} + +const P32: u128 = (1 << 32) - 99; +const P64: u128 = (1 << 64) - 59; +const P128: u128 = u128::MAX - 274; +const P251: u128 = 251; + +// `2^32 − 99` as a u64-backed field: the base the baseline's own ext tests +// exercise (`Fp64<4294967197>`). +type F64Small2 = two::Fp64<4_294_967_197>; +type F64SmallB = base::Fp64<4_294_967_197>; + +macro_rules! ext_suite { + ($name2:ident, $name4:ident, $name8:ident, $moore:ident, $F2:ty, $FB:ty, $p:expr, e2_field: $e2f:expr, neg_one_field: $nof:expr, e48_field: $e48f:expr) => { + #[test] + fn $name2() { + let mut rng = rng(); + check_ext!( + two::FpExt2<$F2, two::TwoNr>, + base::FpExt2<$FB, base::TwoNr>, + $F2, + $FB, + $p, + 2, + |a: &[u128], b: &[u128]| quad_mul_oracle(a, b, 2, $p), + is_field: $e2f, + &mut rng + ); + check_ext!( + two::FpExt2<$F2, two::NegOneNr>, + base::FpExt2<$FB, base::NegOneNr>, + $F2, + $FB, + $p, + 2, + |a: &[u128], b: &[u128]| quad_mul_oracle(a, b, $p - 1, $p), + is_field: $nof, + &mut rng + ); + } + + #[test] + fn $name4() { + let mut rng = rng(); + check_ext!( + two::FpExt4<$F2>, + base::FpExt4<$FB>, + $F2, + $FB, + $p, + 4, + |a: &[u128], b: &[u128]| cheb_mul_oracle(a, b, $p), + is_field: $e48f, + &mut rng + ); + } + + #[test] + fn $name8() { + let mut rng = rng(); + check_ext!( + two::FpExt8<$F2>, + base::FpExt8<$FB>, + $F2, + $FB, + $p, + 8, + |a: &[u128], b: &[u128]| cheb_mul_oracle(a, b, $p), + is_field: $e48f, + &mut rng + ); + } + + #[test] + fn $moore() { + let mut rng = rng(); + check_moore!( + two::FpExt2<$F2, two::TwoNr>, + base::FpExt2<$FB, base::TwoNr>, + $F2, + $FB, + $p, + 2, + &mut rng + ); + check_moore!(two::FpExt4<$F2>, base::FpExt4<$FB>, $F2, $FB, $p, 4, &mut rng); + check_moore!(two::FpExt8<$F2>, base::FpExt8<$FB>, $F2, $FB, $p, 8, &mut rng); + } + }; +} + +ext_suite!( + ext2_over_prime32_offset99, + ext4_over_prime32_offset99, + ext8_over_prime32_offset99, + moore_over_prime32_offset99, + two::Prime32Offset99, + base::Prime32Offset99, + P32, + e2_field: true, + neg_one_field: false, + e48_field: true +); + +ext_suite!( + ext2_over_fp32_251, + ext4_over_fp32_251, + ext8_over_fp32_251, + moore_over_fp32_251, + two::Fp32<251>, + base::Fp32<251>, + P251, + e2_field: true, + neg_one_field: true, + e48_field: false +); + +ext_suite!( + ext2_over_prime64_offset59, + ext4_over_prime64_offset59, + ext8_over_prime64_offset59, + moore_over_prime64_offset59, + two::Prime64Offset59, + base::Prime64Offset59, + P64, + e2_field: true, + neg_one_field: false, + e48_field: false +); + +ext_suite!( + ext2_over_fp64_2pow32_99, + ext4_over_fp64_2pow32_99, + ext8_over_fp64_2pow32_99, + moore_over_fp64_2pow32_99, + F64Small2, + F64SmallB, + P32, + e2_field: true, + neg_one_field: false, + e48_field: true +); + +ext_suite!( + ext2_over_prime128_offset275, + ext4_over_prime128_offset275, + ext8_over_prime128_offset275, + moore_over_prime128_offset275, + two::Prime128Offset275, + base::Prime128Offset275, + P128, + e2_field: true, + neg_one_field: false, + e48_field: false +); + +/// `Ext2` is the `TwoNr` alias in both crates. +#[test] +fn ext2_alias_matches() { + let mut r = rng(); + let v: u128 = r.gen::() % P64; + let w: u128 = r.gen::() % P64; + let a: two::Ext2 = >::new( + ::from_u128_checked(v).unwrap(), + ::from_u128_checked(w).unwrap(), + ); + let b: base::Ext2 = base::FpExt2::new( + base::Prime64Offset59::from_canonical_u128_checked(v).unwrap(), + base::Prime64Offset59::from_canonical_u128_checked(w).unwrap(), + ); + assert_eq!( + Ring::square(&a) + .conjugate() + .norm() + .to_u128_checked() + .unwrap(), + RingCore::square(&b).conjugate().norm().to_canonical_u128(), + "conjugate/norm parity" + ); +} + +/// The reflexive impl: a pseudo-Mersenne base field is its own degree-1 +/// extension in both crates. +#[test] +fn degree_one_reflexive_ext_matches() { + let mut r = rng(); + type F2 = two::Prime64Offset59; + type FB = base::Prime64Offset59; + assert_eq!(>::DEGREE, 1); + assert_eq!(>::EXT_DEGREE, 1); + for _ in 0..32 { + let v = r.gen::() % P64; + let s = r.gen::() % P64; + let (x2, s2) = ( + ::from_u128_checked(v).unwrap(), + ::from_u128_checked(s).unwrap(), + ); + let (xb, sb) = ( + FB::from_canonical_u128_checked(v).unwrap(), + FB::from_canonical_u128_checked(s).unwrap(), + ); + assert_eq!(>::lift_base(s2), s2); + assert_eq!( + x2.mul_base(s2).to_u128_checked().unwrap(), + xb.mul_base(sb).to_canonical_u128() + ); + assert_eq!(>::from_base_slice(&[x2]), x2); + assert_eq!(>::to_base_vec(&x2), vec![x2]); + assert_eq!(>::frobenius_pow(x2, 3), x2); + assert_eq!(>::frobenius_inv_pow(x2, 5), x2); + } +} + +#[test] +#[should_panic(expected = "assertion")] +fn ext2_from_base_slice_wrong_length_panics() { + let one = ::from_u64(1); + let _ = + as ExtField>::from_base_slice(&[ + one, + ]); +} + +#[test] +#[should_panic(expected = "assertion")] +fn ext4_from_base_slice_wrong_length_panics() { + let one = ::from_u64(1); + let _ = + as ExtField>::from_base_slice(&[ + one, one, one, + ]); +} + +#[test] +#[should_panic(expected = "assertion")] +fn ext8_from_base_slice_wrong_length_panics() { + let one = ::from_u64(1); + let _ = as ExtField>::from_base_slice( + &[one; 7], + ); +} From d4959dd31c10daf4c5f9f772a8753d699fe854eb Mon Sep 17 00:00:00 2001 From: acentelles Date: Thu, 30 Jul 2026 19:34:26 -0400 Subject: [PATCH 23/38] feat(jolt-field-two): unreduced deferred-reduction surface (checkpoint 7) Contracts (src/unreduced.rs, 18/70): one fused Unreduced companion surface (Product/SmallProduct/Wide, SUM_IS_EXACT, widening muls, reduce_product/reduce_small_product/reduce_wide, scale_wide) replacing the baseline's HasUnreducedOps + HasWide + ReduceTo, and Fold (precompute/fold_one) replacing HasOptimizedFold, documented as the multilinear bind. The checkpoint-6-deferred MulBaseUnreduced lands in extension.rs (60/60 exactly). Impls (src/solinas/unreduced.rs, 501/530): the i32-lane wide types, u128-slot product accumulators, AccumPair, fold matrices, and the fused kernels with explicit carry tracking everywhere two full-width products meet (overflowing adds; the P^2-bias branch tracks add-carry and sub-borrow jointly, with the impossible underflow guarded by debug panic). One wrapping-ops policy across product accumulators (the baseline mixed conventions in Fp128MulU64Accum); FpExt8 gets single generic impls where the baseline stamped three copies. Baseline corrections found by re-derivation, recorded in SPEC.md: - lane headroom is exactly 32768 additions, not the baseline's "~32,769" (32769 * 0xFFFF > 2^31 - 1); tested at the boundary and one-past asserts in debug - FpExt4Fp32ProductAccum docs claimed 7*P^2 ~ 2^65 and 2^63 terms; correct is < 2^67 and >= 2^61, re-derived per slot - the fused FpExt2 path silently assumed NR in {-1, 2} while generic over any config; now debug-asserted - Fp128 accumulator headroom sharpened to 2^64 - 1 terms (the reduce carry chain binds) NEON intrinsics dropped with evidence: LLVM auto-vectorizes the portable [i32; N] ops to the identical add.4s/sub.4s/neg.4s on aarch64 (checked via --emit asm), plus mul.4s for lane scaling which the baseline never vectorized; ~120 unsafe lines gone and debug builds now catch lane overflow. Tests: 38 new (102/102 all-features): per-accumulator exactness vs per-term multiplication and an independent limb-schoolbook oracle, adversarial all-max batches over every carry corner, wrap-through sequences, 18 fold-parity instantiations vs baseline, MulBaseUnreduced override parity, SUM_IS_EXACT flag parity. Budget flag for review: solinas/mod.rs sits at 108 vs 90 (was 102 before this checkpoint; re-exports and registry accumulate across checkpoints) - needs the budget discussion, not golf. --- crates/jolt-field-two/SPEC.md | 33 +- crates/jolt-field-two/src/extension.rs | 31 +- crates/jolt-field-two/src/lib.rs | 16 +- crates/jolt-field-two/src/solinas/mod.rs | 6 + .../jolt-field-two/src/solinas/unreduced.rs | 748 ++++++++++++++ crates/jolt-field-two/src/unreduced.rs | 93 ++ .../tests/solinas_unreduced_differential.rs | 921 ++++++++++++++++++ 7 files changed, 1834 insertions(+), 14 deletions(-) create mode 100644 crates/jolt-field-two/src/solinas/unreduced.rs create mode 100644 crates/jolt-field-two/src/unreduced.rs create mode 100644 crates/jolt-field-two/tests/solinas_unreduced_differential.rs diff --git a/crates/jolt-field-two/SPEC.md b/crates/jolt-field-two/SPEC.md index 8f6c0989dd..1c6944716d 100644 --- a/crates/jolt-field-two/SPEC.md +++ b/crates/jolt-field-two/SPEC.md @@ -57,7 +57,7 @@ capability subset with defaulted members". | `PseudoMersenne` (defined unconditionally in `algebra.rs`, per the file table) | `PseudoMersenneField` + `ExtMulBackend` | `const OFFSET: u128` (bits live on `CanonicalEncoding`) + the degree-4/8 ext-mul/square kernel hooks (`ext4_mul`, `ext4_square`, `ext8_mul`, `ext8_square`) with generic coefficient-formula defaults (`schedules.rs`). No base field overrides them: the baseline's fused-accumulation `Fp32` override lost the checkpoint-6 bench gate (see dropped-specialization evidence) | | `ExtField` | same | degree, `lift_base`, `mul_base`, coeff access, Frobenius | | `Ext2Config` | `FpExt2Config` | quadratic non-residue config (ZST pattern), `IS_NEG_ONE` fast path | -| `MulBaseUnreduced` | same | tiny overridable ext×base deferred multiply — **deferred to checkpoint 7**: its contract is stated in terms of `Unreduced::Product`, which does not exist until the unreduced checkpoint | +| `MulBaseUnreduced` | same | tiny overridable ext×base deferred multiply, stated in terms of `Unreduced::Product` (deferred from checkpoint 6, landed with checkpoint 7; lives in `extension.rs` with a degree-1 blanket impl) | | `Unreduced` | `HasUnreducedOps` + `HasWide` + `ReduceTo` | **one deferred-reduction companion surface**: `type Product`, `type SmallProduct`, `type Wide` (i32-lane), `SUM_IS_EXACT`, widening muls + `reduce_*` for each, `scale_wide`. Rationale: these were three fragments of one concept — "the unreduced value algebra around a field"; routing reduction through the field type kills `ReduceTo`'s ambiguity workarounds | | `Fold` | `HasOptimizedFold` | `precompute(r) -> Ctx`, `fold_one(ctx, even, odd)` — documented honestly as the multilinear bind `even + r·(odd − even)`, a protocol-support hook that lives here because implementations exploit field representation | | `Packed` | `PackedField` | lanes: `Scalar`, `WIDTH`, `from_fn`/`extract`/`broadcast` + defaulted slice helpers + packed ext2 kernel hook | @@ -140,6 +140,11 @@ and fold matrices; `S64`–`S256` + hi32 variants; `Limbs`; rayon helpers; baseline definition (`mul_base_to_product_accum`) returns `Unreduced::Product`, which does not exist until the unreduced surface lands; inventing a placeholder shape now would just be churn. + **Un-deferred with checkpoint 7:** landed in `extension.rs` as + `mul_base_unreduced` with a lift-then-`mul_unreduced` default body, a + degree-1 blanket impl, the coordinate-scaling `FpExt4` override, + and default-body impls for `FpExt2` and the identity-shape + extension variants. - **Added (not in baseline):** an `ext8_square` hook on `PseudoMersenne` defaulting to the deg-8 squaring schedule. The baseline computed `FpExt8::square` as a full multiply and used its square schedule only in @@ -147,6 +152,30 @@ and fold matrices; `S64`–`S256` + hi32 variants; `Limbs`; rayon helpers; value-identical (pure ring ops), saves base ops, and gives the schedule its scalar consumer. +**Dropped-specialization evidence (checkpoint 7, unreduced):** + +- **Dropped:** the baseline's aarch64 NEON intrinsic `Add`/`Sub`/`Neg` + paths on the `i32`-lane wide accumulators (`Fp64x4i32`, `Fp128x8i32`; + ~120 source lines of `unsafe` intrinsics). Evidence: `rustc -O` compiles + the portable element-wise `[i32; N]` code to the identical instructions + the intrinsics hand-write — `ldr/ldp q` + `add.4s`/`sub.4s`/`neg.4s` + (and `mul.4s` for lane scaling, which the baseline never vectorized) — + verified by inspecting `--emit asm` output for 4- and 8-lane add, sub, + neg, and scale on this machine (aarch64, Apple M4). The portable path + additionally panics on lane overflow in debug builds, turning headroom + violations into test failures instead of silent wrapping. +- **Corrected (baseline doc bugs, no code change):** the baseline's lane + headroom comment says `i32::MAX / u16::MAX ≈ 32,769` additions; the safe + count is 32768 (`32769 · 0xFFFF > 2^31 − 1`). Its `FpExt4` accum + comment claims per-term slot contributions of `7·P² ≈ 2^65` and `2^63` + accumulations; the correct figures are `7·P² < 2^67` and `2^61` terms. + Both re-derived and documented in `solinas/unreduced.rs`, with the + 32768-boundary case tested exactly (one past asserted to panic in debug). +- **Restricted (baseline latent footgun):** the fused `FpExt2` + product accumulation is only correct for non-residues −1 and 2, but the + baseline compiled its two-case body for arbitrary `FpExt2Config`s; the + port debug-asserts `NR ∈ {−1, 2}`. + ## Design pillars 1. **Const-generic scalar core**: `Fp64` etc., fold constants @@ -200,7 +229,7 @@ on them. | **Contract layer (root, unconditional)** | | | | `src/lib.rs` | 70 | crate docs, feature gates, re-exports, `FieldError` | | `src/algebra.rs` | 260 | spine: 7 traits + `NaiveAccumulator` + `PseudoMersenne` | -| `src/extension.rs` | 60 | contracts: `ExtField`, `Ext2Config` + NR config ZSTs (`MulBaseUnreduced` lands with checkpoint 7) | +| `src/extension.rs` | 60 | contracts: `ExtField`, `Ext2Config` + NR config ZSTs, `MulBaseUnreduced` | | `src/unreduced.rs` | 70 | contracts: `Unreduced`, `Fold` | | `src/packed.rs` | 90 | contracts: `Packed`, `WithPacking` + generic `NoPacking` | | `src/ops.rs` | 180 | `impl_ring_ops!`, `impl_serde_bytes!` (backend-neutral) | diff --git a/crates/jolt-field-two/src/extension.rs b/crates/jolt-field-two/src/extension.rs index 1dd94cad84..32f8c9f0e0 100644 --- a/crates/jolt-field-two/src/extension.rs +++ b/crates/jolt-field-two/src/extension.rs @@ -5,11 +5,10 @@ //! quadratic extension `F[u]/(u^2 − NR)` through a zero-sized type, with the //! [`NegOneNr`] and [`TwoNr`] presets. //! -//! `MulBaseUnreduced` (deferred ext×base multiply) is deferred to the -//! `Unreduced` checkpoint: its contract is stated in terms of -//! `Unreduced::Product`. +//! [`MulBaseUnreduced`] is the deferred ext×base multiply, stated in terms +//! of [`Unreduced::Product`]. -use crate::{Field, Ring}; +use crate::{Field, PseudoMersenne, Ring, Unreduced}; use std::ops::{Add, Mul, Sub}; /// An algebraic extension of the base field `F`. @@ -45,11 +44,31 @@ pub trait ExtField: Field { /// `DEGREE` on `Self`, this is `frobenius_pow(DEGREE − power)`. #[inline] fn frobenius_inv_pow(self, power: usize) -> Self { - let d = Self::DEGREE; - self.frobenius_pow((d - (power % d)) % d) + self.frobenius_pow((Self::DEGREE - (power % Self::DEGREE)) % Self::DEGREE) } } +/// Deferred-reduction extension-times-base multiply. +/// +/// Scales `self` by a base scalar `x` into [`Unreduced::Product`] without +/// reducing, so a batch of `E × F` products can be summed and reduced once. +/// When [`Unreduced::SUM_IS_EXACT`] holds, the reduced sum equals the +/// per-term [`ExtField::mul_base`] sum within the accumulator's headroom. +/// +/// `E × F` has no cross terms, so the default body (lift `x` and reuse +/// [`Unreduced::mul_unreduced`]) is correct everywhere; extensions whose +/// product-accumulator layout admits cheaper coordinate scaling override it. +pub trait MulBaseUnreduced: ExtField + Unreduced { + /// Accumulates `self · x` (extension times base scalar) unreduced. + #[inline] + fn mul_base_unreduced(self, x: F) -> Self::Product { + self.mul_unreduced(Self::lift_base(x)) + } +} + +/// A base field is its own degree-1 extension; the default body is exact. +impl> MulBaseUnreduced for F {} + /// Parameters for a quadratic extension `F[u]/(u^2 − NR)` over `F`. /// /// Implemented by zero-sized config types so the non-residue choice is a diff --git a/crates/jolt-field-two/src/lib.rs b/crates/jolt-field-two/src/lib.rs index 067d205691..45403a0cf5 100644 --- a/crates/jolt-field-two/src/lib.rs +++ b/crates/jolt-field-two/src/lib.rs @@ -27,6 +27,7 @@ mod schedules; pub mod signed; #[cfg(feature = "solinas")] pub mod solinas; +mod unreduced; pub use algebra::{ Accumulator, AdditiveGroup, CanonicalEncoding, Field, JoltField, NaiveAccumulator, @@ -34,19 +35,22 @@ pub use algebra::{ }; #[cfg(feature = "bn254")] pub use bn254::{Fq, Fr, WideAccumulator}; -pub use extension::{Ext2Config, ExtField, NegOneNr, TwoNr}; +pub use extension::{Ext2Config, ExtField, MulBaseUnreduced, NegOneNr, TwoNr}; pub use limbs::Limbs; pub use num_traits::{One, Zero}; #[cfg(feature = "solinas")] pub use solinas::{ balanced_digit_lut, canonical_frobenius_thetas, is_registered_prime_offset, pseudo_mersenne_modulus, registered_prime_offset_spec, solve_frobenius_moore, - validate_canonical_frobenius_thetas, Ext2, Fp128, Fp32, Fp64, FpExt2, FpExt4, FpExt8, - Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, Prime24Offset3, - Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime48Offset59, - Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, PRIME_OFFSET_IMPLEMENTED_MAX_BITS, - PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, + validate_canonical_frobenius_thetas, AccumPair, Ext2, FoldMatrixFp32, FoldMatrixFp64, Fp128, + Fp128MulU64Accum, Fp128ProductAccum, Fp128x8i32, Fp32, Fp32ProductAccum, Fp32x2i32, Fp64, + Fp64ProductAccum, Fp64x4i32, FpExt2, FpExt2Fp64ProductAccum, FpExt4, FpExt4Fp32ProductAccum, + FpExt8, Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, + Prime24Offset3, Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, + Prime48Offset59, Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, + PRIME_OFFSET_IMPLEMENTED_MAX_BITS, PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, }; +pub use unreduced::{Fold, Unreduced}; /// Backend-independent input and shape failures. #[derive(Debug, thiserror::Error)] diff --git a/crates/jolt-field-two/src/solinas/mod.rs b/crates/jolt-field-two/src/solinas/mod.rs index 9fd5f0955b..6157bf8c72 100644 --- a/crates/jolt-field-two/src/solinas/mod.rs +++ b/crates/jolt-field-two/src/solinas/mod.rs @@ -6,6 +6,7 @@ mod ext; mod fp128; +mod unreduced; mod word; pub use ext::{ @@ -13,6 +14,11 @@ pub use ext::{ FpExt2, FpExt4, FpExt8, }; pub use fp128::Fp128; +pub use unreduced::{ + AccumPair, FoldMatrixFp32, FoldMatrixFp64, Fp128MulU64Accum, Fp128ProductAccum, Fp128x8i32, + Fp32ProductAccum, Fp32x2i32, Fp64ProductAccum, Fp64x4i32, FpExt2Fp64ProductAccum, + FpExt4Fp32ProductAccum, +}; pub use word::{Fp32, Fp64}; use crate::Ring; diff --git a/crates/jolt-field-two/src/solinas/unreduced.rs b/crates/jolt-field-two/src/solinas/unreduced.rs new file mode 100644 index 0000000000..9602720900 --- /dev/null +++ b/crates/jolt-field-two/src/solinas/unreduced.rs @@ -0,0 +1,748 @@ +//! Deferred-reduction backend: `i32`-lane wide accumulators, `u128`-slot +//! product accumulators, challenge-fold matrices, and the [`Unreduced`], +//! [`Fold`], and [`MulBaseUnreduced`] impls for every Solinas field and +//! extension. +//! +//! # Accumulator semantics and headroom +//! +//! Product accumulators are `[u128; N]` with **wrapping** per-slot ops, +//! i.e. the group `(Z/2^128)^N`. Reduction reads each slot as a plain +//! integer, so a sum reduces exactly iff the *final* integer value of every +//! slot lies in `[0, 2^128)` — intermediate dips below zero cancel exactly +//! under wrapping, and no runtime check enforces the bound. The per-type +//! headroom (worst-case per-term slot contribution, hence how many fmadds +//! fit) is derived in each accumulation formula's comment. +//! +//! Wide accumulators are `[i32; N]` (16 data bits per lane) with +//! **non-wrapping** ops: lane overflow panics in debug builds, which is the +//! only runtime enforcement of the lane headroom. Splitting a canonical +//! element gives lanes in `[0, 2^16)`, so at least +//! `⌊(2^31 − 1) / (2^16 − 1)⌋ = 32768` same-sign accumulations (or +//! `k` accumulations scaled by `s` with `k·|s|·(2^16 − 1) < 2^31`) fit +//! before any lane can overflow. +//! +//! The baseline's NEON intrinsic Add/Sub/Neg lane paths are dropped: LLVM +//! auto-vectorizes the element-wise `[i32; N]` code to the identical +//! `add.4s`/`sub.4s`/`neg.4s` (and `mul.4s` for scaling) instructions at +//! opt-level 3 (see SPEC.md dropped-specialization evidence). + +use super::{Fp128, Fp32, Fp64, FpExt2, FpExt4, FpExt8}; +use crate::{ + CanonicalEncoding, Ext2Config, ExtField, Fold, MulBaseUnreduced, PseudoMersenne, Ring, + Unreduced, +}; + +/// Splits a canonical value into 16-bit digits stored one per `i32` lane. +#[inline(always)] +fn split16(v: u128) -> [i32; N] { + std::array::from_fn(|i| ((v >> (16 * i)) & 0xFFFF) as i32) +} + +/// `Σᵢ laneᵢ · 2^{16i}` as a signed integer. Max magnitude for ≤ 4 lanes: +/// `4 · 2^31 · 2^48 = 2^81`, far inside `i128` (the 8-lane type does not +/// use this — its top lane alone would need 2^{112+31} bits). +#[inline(always)] +fn recombine16(lanes: &[i32]) -> i128 { + lanes + .iter() + .enumerate() + .map(|(i, &lane)| (lane as i128) << (16 * i)) + .sum() +} + +macro_rules! wide_lanes { + ($($(#[$doc:meta])* $name:ident: $n:literal;)*) => {$( + $(#[$doc])* + #[cfg_attr(feature = "allocative", derive(allocative::Allocative))] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[repr(C)] + pub struct $name(pub [i32; $n]); + + $crate::impl_group_ops!(impl[] $name { + add(a, b): $name(std::array::from_fn(|i| a.0[i] + b.0[i])), + sub(a, b): $name(std::array::from_fn(|i| a.0[i] - b.0[i])), + neg(a): $name(std::array::from_fn(|i| -a.0[i])), + zero: $name([0; $n]), + }); + + impl $name { + /// Multiplies every lane by a small signed scalar. + /// + /// Safe when `|small| · max_lane_magnitude < 2^31`; for lanes + /// fresh from a canonical split (`< 2^16`) any `|small| ≤ 32768` + /// fits a single product. + #[inline] + pub fn scale_i32(self, small: i32) -> Self { + Self(self.0.map(|lane| lane * small)) + } + } + )*}; +} + +wide_lanes! { + /// Wide unreduced accumulator for [`Fp32`]: 2 × `i32` lanes. + Fp32x2i32: 2; + /// Wide unreduced accumulator for [`Fp64`]: 4 × `i32` lanes. + Fp64x4i32: 4; + /// Wide unreduced accumulator for [`Fp128`]: 8 × `i32` lanes (one + /// 256-bit vector register on AVX2, two 128-bit on NEON). + Fp128x8i32: 8; +} + +macro_rules! product_accum { + ($($(#[$doc:meta])* $name:ident: $n:literal;)*) => {$( + $(#[$doc])* + #[cfg_attr(feature = "allocative", derive(allocative::Allocative))] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct $name(pub [u128; $n]); + + $crate::impl_group_ops!(impl[] $name { + add(a, b): $name(std::array::from_fn(|i| a.0[i].wrapping_add(b.0[i]))), + sub(a, b): $name(std::array::from_fn(|i| a.0[i].wrapping_sub(b.0[i]))), + neg(a): $name(std::array::from_fn(|i| a.0[i].wrapping_neg())), + zero: $name([0; $n]), + }); + )*}; +} + +product_accum! { + /// Accumulator for `Fp32 × Fp32` and `Fp32 × u64` products. + /// + /// Slot semantics: `value = s0 + s1·2^64`. Per term: an `Fp32 × Fp32` + /// product (`< 2^64`) lands whole in `s0`; an `Fp32 × u64` product + /// (`< 2^96`) is split at bit 64 (`s0 += lo64 < 2^64`, + /// `s1 += hi < 2^32`). Headroom: `2^128 / 2^64 = 2^64` terms. + Fp32ProductAccum: 2; + /// Accumulator for `Fp64 × Fp64` and `Fp64 × u64` products. + /// + /// Slot semantics: `value = s0 + s1·2^64`; each `< 2^128` product is + /// split at bit 64, so both slots grow by `< 2^64` per term. Headroom: + /// `2^64` terms. + Fp64ProductAccum: 2; + /// Accumulator for `Fp128 × u64` products (3 result limbs of + /// `mul_wide_u64`, one per slot). Each slot grows by `< 2^64` per term; + /// headroom `2^64 − 1` terms (the reduction's carry chain needs + /// `sᵢ + carry < 2^128`, see `Fp128::reduce_small_product`). + Fp128MulU64Accum: 3; + /// Accumulator for `Fp128 × Fp128` products (4 result limbs of + /// `mul_wide`, one per slot). Headroom `2^64 − 1` terms, as for + /// [`Fp128MulU64Accum`]. + Fp128ProductAccum: 4; + /// Accumulator for `FpExt4` products with delayed reduction: one + /// slot per ring-subfield coefficient. The φ(X) ring reduction is fused + /// into the accumulation formulas (`fp_ext4_mul_to_accum_fp32`); only + /// the per-coefficient Solinas reduction is deferred. + /// + /// Headroom: each term contributes at most `7·P² < 7·2^64 < 2^67` per + /// slot (slot 0: `p00 + 2(p11 + p22 + p33) ≤ 7(P−1)²`; the biased slots + /// stay ≤ `7P²`), so at least `2^128 / 2^67 = 2^61` terms fit. (The + /// baseline documented `7·P² ≈ 2^65` and `2^63` accumulations — both + /// off; the safe bound is `2^61`.) + FpExt4Fp32ProductAccum: 4; + /// Accumulator for `FpExt2` products with delayed reduction: + /// slots `[c0_lo, c0_hi, c1_lo, c1_hi]`, each coefficient a + /// base-2^64 limb pair reduced like [`Fp64ProductAccum`]. + /// + /// Headroom: per term `*_lo` grows by `< 2^64` and `*_hi` by `< 3·2^64` + /// (the limb split keeps a carry of ≤ 2 in bits ≥ 64, see + /// `fp_ext2_mul_to_accum_fp64`), so at least `2^128 / 3·2^64 > 2^62` + /// terms fit. + FpExt2Fp64ProductAccum: 4; +} + +/// Lifts of a canonical element into its accumulator/lane shapes. +macro_rules! impl_from { + ($(impl[$($g:tt)*] $src:ty => $dst:ty { $x:ident => $body:expr })*) => {$( + impl<$($g)*> From<$src> for $dst { + #[inline] + fn from($x: $src) -> Self { + $body + } + } + )*}; +} + +impl_from! { + impl[const P: u32] Fp32

=> Fp32x2i32 { x => Self(split16(x.to_limbs() as u128)) } + impl[const P: u64] Fp64

=> Fp64x4i32 { x => Self(split16(x.to_limbs() as u128)) } + impl[const P: u128] Fp128

=> Fp128x8i32 { x => Self(split16(x.to_canonical_u128())) } + impl[const P: u32] Fp32

=> Fp32ProductAccum { x => Self([x.to_limbs() as u128, 0]) } + impl[const P: u64] Fp64

=> Fp64ProductAccum { x => Self([x.to_limbs() as u128, 0]) } + impl[const P: u128] Fp128

=> Fp128MulU64Accum { + x => { let [lo, hi] = x.to_limbs(); Self([lo as u128, hi as u128, 0]) } + } + impl[const P: u128] Fp128

=> Fp128ProductAccum { + x => { let [lo, hi] = x.to_limbs(); Self([lo as u128, hi as u128, 0, 0]) } + } +} + +/// Pair accumulator for quadratic extensions: two base accumulators, +/// component-wise. +#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AccumPair(pub A, pub A); + +crate::impl_group_ops!(impl[A: crate::AdditiveGroup + PartialEq] AccumPair { + add(a, b): AccumPair(a.0 + b.0, a.1 + b.1), + sub(a, b): AccumPair(a.0 - b.0, a.1 - b.1), + neg(a): AccumPair(-a.0, -a.1), + zero: AccumPair(A::zero(), A::zero()), +}); + +impl Unreduced for Fp32

{ + type Product = Fp32ProductAccum; + type SmallProduct = Fp32ProductAccum; + type Wide = Fp32x2i32; + + #[inline] + fn mul_unreduced(self, other: Self) -> Fp32ProductAccum { + Fp32ProductAccum([self.mul_wide(other) as u128, 0]) + } + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Fp32ProductAccum { + let wide = (self.to_limbs() as u128) * (small as u128); + Fp32ProductAccum([wide as u64 as u128, wide >> 64]) + } + + #[inline] + fn scale_wide(self, small: i32) -> Fp32x2i32 { + Fp32x2i32::from(self).scale_i32(small) + } + + /// `s0 + s1·2^64 (mod p)`, exact for any slot values (each slot is + /// reduced independently, then recombined in the field). + #[inline] + fn reduce_product(accum: Fp32ProductAccum) -> Self { + let [s0, s1] = accum.0; + let shift64 = Self::from_u128_reduced(1u128 << 64); + Self::from_u128_reduced(s0) + Self::from_u128_reduced(s1) * shift64 + } + + #[inline] + fn reduce_small_product(accum: Fp32ProductAccum) -> Self { + Self::reduce_product(accum) + } + + #[inline] + fn reduce_wide(wide: Fp32x2i32) -> Self { + Self::from_i128(recombine16(&wide.0)) + } +} + +impl Unreduced for Fp64

{ + type Product = Fp64ProductAccum; + type SmallProduct = Fp64ProductAccum; + type Wide = Fp64x4i32; + + #[inline] + fn mul_unreduced(self, other: Self) -> Fp64ProductAccum { + let wide = self.mul_wide(other); + Fp64ProductAccum([wide as u64 as u128, wide >> 64]) + } + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Fp64ProductAccum { + let wide = self.mul_wide_u64(small); + Fp64ProductAccum([wide as u64 as u128, wide >> 64]) + } + + #[inline] + fn scale_wide(self, small: i32) -> Fp64x4i32 { + Fp64x4i32::from(self).scale_i32(small) + } + + /// `s0 + s1·2^64 (mod p)`, exact for any slot values. + #[inline] + fn reduce_product(accum: Fp64ProductAccum) -> Self { + let [s0, s1] = accum.0; + Self::solinas_reduce(s0) + Self::solinas_reduce(s1) * Self::solinas_reduce(1u128 << 64) + } + + #[inline] + fn reduce_small_product(accum: Fp64ProductAccum) -> Self { + Self::reduce_product(accum) + } + + #[inline] + fn reduce_wide(wide: Fp64x4i32) -> Self { + Self::from_i128(recombine16(&wide.0)) + } +} + +impl Unreduced for Fp128

{ + type Product = Fp128ProductAccum; + type SmallProduct = Fp128MulU64Accum; + type Wide = Fp128x8i32; + + #[inline] + fn mul_unreduced(self, other: Self) -> Fp128ProductAccum { + let [r0, r1, r2, r3] = self.mul_wide(other); + Fp128ProductAccum([r0 as u128, r1 as u128, r2 as u128, r3 as u128]) + } + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Fp128MulU64Accum { + let [lo, mid, hi] = self.mul_wide_u64(small); + Fp128MulU64Accum([lo as u128, mid as u128, hi as u128]) + } + + #[inline] + fn scale_wide(self, small: i32) -> Fp128x8i32 { + Fp128x8i32::from(self).scale_i32(small) + } + + /// Carry-propagates the slot sums into base-2^64 limbs, then Solinas + /// reduction. With `k ≤ 2^64 − 1` terms each slot is `≤ k(2^64 − 1)` + /// and each carry `< k`, so `sᵢ + carry ≤ k·2^64 < 2^128` never + /// overflows (debug-checked by the non-wrapping `+`). + #[inline] + fn reduce_product(accum: Fp128ProductAccum) -> Self { + let [s0, s1, s2, s3] = accum.0; + let t1 = s1 + (s0 >> 64); + let t2 = s2 + (t1 >> 64); + let t3 = s3 + (t2 >> 64); + Self::solinas_reduce(&[ + s0 as u64, + t1 as u64, + t2 as u64, + t3 as u64, + (t3 >> 64) as u64, + ]) + } + + /// Same carry chain and headroom as + /// [`reduce_product`](Self::reduce_product), one limb shorter. + #[inline] + fn reduce_small_product(accum: Fp128MulU64Accum) -> Self { + let [s0, s1, s2] = accum.0; + let t1 = s1 + (s0 >> 64); + let t2 = s2 + (t1 >> 64); + Self::solinas_reduce(&[s0 as u64, t1 as u64, t2 as u64, (t2 >> 64) as u64]) + } + + /// Carry-propagates the signed lanes into 16-bit digits plus a signed + /// top carry, then reduces `digits + carry·2^128 ≡ digits + carry·C`. + /// + /// With lanes in `(−2^31, 2^31)` the running carry stays within + /// `±2^15 − 1` after each step (`|v| < 2^31 + 2^15`), so + /// `|carry|·C < 2^16 · 2^32 = 2^48 < p` and both sign branches stay + /// canonical. + #[inline] + fn reduce_wide(wide: Fp128x8i32) -> Self { + let mut carry: i64 = 0; + let mut digits = [0u64; 8]; + for (digit, &lane) in digits.iter_mut().zip(wide.0.iter()) { + let v = lane as i64 + carry; + *digit = (v & 0xFFFF) as u64; + carry = v >> 16; + } + let lo = digits[0] | digits[1] << 16 | digits[2] << 32 | digits[3] << 48; + let hi = digits[4] | digits[5] << 16 | digits[6] << 32 | digits[7] << 48; + if carry >= 0 { + Self::solinas_reduce(&[lo, hi, carry as u64]) + } else { + let base = lo as u128 | (hi as u128) << 64; + let sub = (-carry) as u128 * Self::C; + Self::from_u128_reduced(if base >= sub { + base - sub + } else { + P - (sub - base) + }) + } + } +} + +/// Widening `FpExt4` multiply into one `u128` slot per coefficient: +/// the deg-4 schedule (`crate::schedules::ext4_mul_coeffs`) over raw +/// products, with subtracted terms biased by `P² ≡ 0 (mod p)` so every +/// per-term slot contribution is non-negative. +/// +/// Per-term slot bounds (`pᵢⱼ ≤ (P−1)²`): slot 0 `≤ 7(P−1)²`; slot 1 +/// `≤ 6(P−1)²`; slot 2 `≤ 5(P−1)² + P²` (the `−p33` cannot underflow the +/// `+P²` bias); slot 3 `≤ 4(P−1)² + 2P²`. All `≤ 7P² < 2^67`, giving the +/// `2^61`-term headroom documented on [`FpExt4Fp32ProductAccum`]. +/// Subtractions are evaluated after every addition (left-to-right), so no +/// intermediate dips below zero either. +#[inline(always)] +fn fp_ext4_mul_to_accum_fp32( + a: [Fp32

; 4], + b: [Fp32

; 4], +) -> FpExt4Fp32ProductAccum { + #[inline(always)] + fn product(a: Fp32

, b: Fp32

) -> u128 { + (a.to_limbs() as u128) * (b.to_limbs() as u128) + } + let [a0, a1, a2, a3] = a; + let [b0, b1, b2, b3] = b; + let p_sq = (P as u128) * (P as u128); + FpExt4Fp32ProductAccum([ + product(a0, b0) + 2 * (product(a1, b1) + product(a2, b2) + product(a3, b3)), + product(a0, b1) + + product(a1, b0) + + product(a1, b2) + + product(a2, b1) + + product(a2, b3) + + product(a3, b2), + product(a0, b2) + + product(a2, b0) + + product(a1, b1) + + product(a1, b3) + + product(a3, b1) + + p_sq + - product(a3, b3), + product(a0, b3) + product(a3, b0) + product(a1, b2) + product(a2, b1) + 2 * p_sq + - product(a2, b3) + - product(a3, b2), + ]) +} + +impl Unreduced for FpExt4> { + type Product = FpExt4Fp32ProductAccum; + type SmallProduct = Self; + type Wide = Self; + + // `fp_ext4_mul_to_accum_fp32` keeps every per-term slot contribution + // non-negative and `< 2^67`, so a summed batch within the documented + // headroom reduces to exactly the per-term product sum. + const SUM_IS_EXACT: bool = true; + + #[inline] + fn mul_unreduced(self, other: Self) -> FpExt4Fp32ProductAccum { + fp_ext4_mul_to_accum_fp32(self.coeffs, other.coeffs) + } + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Self { + self.mul_base(Fp32::from_u64(small)) + } + + #[inline] + fn scale_wide(self, small: i32) -> Self { + self.mul_base(Fp32::from_i64(small as i64)) + } + + #[inline] + fn reduce_product(accum: FpExt4Fp32ProductAccum) -> Self { + Self::new(accum.0.map(Fp32::from_u128_reduced)) + } + + #[inline] + fn reduce_small_product(accum: Self) -> Self { + accum + } + + #[inline] + fn reduce_wide(wide: Self) -> Self { + wide + } +} + +impl MulBaseUnreduced> for FpExt4> { + /// `E × F` scales each coordinate into its own slot; each product is + /// `< P² < 2^64`, so batches inherit the accumulator headroom. + #[inline] + fn mul_base_unreduced(self, x: Fp32

) -> FpExt4Fp32ProductAccum { + let x = x.to_limbs() as u128; + FpExt4Fp32ProductAccum(self.coeffs.map(|c| (c.to_limbs() as u128) * x)) + } +} + +/// Splits `value = lo128 + hi_carry·2^128` into base-2^64 limbs +/// `[bits 0..64, bits 64..]` for a limb-pair slot. +/// +/// The high limb carries `hi_carry` (≤ 2 here) in bits ≥ 64 and the +/// reduction reconstructs `lo + hi·2^64` exactly, so the full (> 128-bit) +/// coefficient survives without the mod-2^128 wrap a single `u128` +/// intermediate would incur — the wrap is **not** congruent mod `p` and +/// was the baseline's historical Fp64 bug pattern. +#[inline(always)] +fn fp64_accum_limbs(lo128: u128, hi_carry: u128) -> [u128; 2] { + [lo128 as u64 as u128, (lo128 >> 64) | (hi_carry << 64)] +} + +/// Widening `FpExt2` multiply with delayed reduction and explicit +/// carry tracking. +/// +/// Coefficient bounds (`pᵢⱼ ≤ (P−1)² < 2^128`): +/// - `NR = −1`: `c0 = p00 + P² − p11 ∈ [0, 2P²) ⊂ [0, 2^129)`. The add +/// carry and sub borrow satisfy `carry ≥ borrow` (a borrow with no carry +/// would need `p00 + P² < p11 < P²`, impossible), so +/// `hi_carry = carry − borrow ∈ {0, 1}`. +/// - `NR = 2`: `c0 = p00 + 2·p11 < 3P² < 2^130`, carry ∈ {0, 1, 2}. +/// - `c1 = p01 + p10 < 2P² < 2^129`, carry ∈ {0, 1}. +/// +/// Only these two non-residues are supported (debug-asserted); a third +/// `Ext2Config` would need its own carry analysis. (The baseline compiled +/// the same two-case body for arbitrary configs without checking.) +#[inline(always)] +fn fp_ext2_mul_to_accum_fp64>>( + a: [Fp64

; 2], + b: [Fp64

; 2], +) -> FpExt2Fp64ProductAccum { + debug_assert!( + C::IS_NEG_ONE || C::non_residue() == Fp64::

::from_u64(2), + "fp_ext2_mul_to_accum_fp64 supports NR ∈ {{−1, 2}} only" + ); + let p00 = a[0].mul_wide(b[0]); + let p11 = a[1].mul_wide(b[1]); + let p01 = a[0].mul_wide(b[1]); + let p10 = a[1].mul_wide(b[0]); + + let [c0_lo, c0_hi] = if C::IS_NEG_ONE { + // c0 = p00 + P² − p11 (the P² bias keeps it non-negative and is + // invisible mod p). + let modulus_sq = (P as u128) * (P as u128); + let (sum, carry_add) = p00.overflowing_add(modulus_sq); + let (diff, borrow) = sum.overflowing_sub(p11); + let hi_carry = (carry_add as u128) - (borrow as u128); + fp64_accum_limbs(diff, hi_carry) + } else { + // c0 = p00 + 2·p11. + let (sum1, carry1) = p00.overflowing_add(p11); + let (sum2, carry2) = sum1.overflowing_add(p11); + fp64_accum_limbs(sum2, (carry1 as u128) + (carry2 as u128)) + }; + let (c1_sum, c1_carry) = p01.overflowing_add(p10); + let [c1_lo, c1_hi] = fp64_accum_limbs(c1_sum, c1_carry as u128); + + FpExt2Fp64ProductAccum([c0_lo, c0_hi, c1_lo, c1_hi]) +} + +impl>> Unreduced for FpExt2, C> { + type Product = FpExt2Fp64ProductAccum; + type SmallProduct = AccumPair; + type Wide = Self; + + // `fp_ext2_mul_to_accum_fp64` keeps the full > 128-bit coefficients via + // carry-aware base-2^64 limbs, so summing a batch and reducing once + // equals the per-term `Mul` sum within the documented headroom. + const SUM_IS_EXACT: bool = true; + + #[inline] + fn mul_unreduced(self, other: Self) -> FpExt2Fp64ProductAccum { + fp_ext2_mul_to_accum_fp64::(self.coeffs, other.coeffs) + } + + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Self::SmallProduct { + AccumPair( + self.coeffs[0].mul_u64_unreduced(small), + self.coeffs[1].mul_u64_unreduced(small), + ) + } + + #[inline] + fn scale_wide(self, small: i32) -> Self { + self.mul_base(Fp64::from_i64(small as i64)) + } + + #[inline] + fn reduce_product(accum: FpExt2Fp64ProductAccum) -> Self { + let [c0_lo, c0_hi, c1_lo, c1_hi] = accum.0; + Self::new( + Fp64::reduce_product(Fp64ProductAccum([c0_lo, c0_hi])), + Fp64::reduce_product(Fp64ProductAccum([c1_lo, c1_hi])), + ) + } + + #[inline] + fn reduce_small_product(accum: Self::SmallProduct) -> Self { + Self::new( + Fp64::reduce_small_product(accum.0), + Fp64::reduce_small_product(accum.1), + ) + } + + #[inline] + fn reduce_wide(wide: Self) -> Self { + wide + } +} + +impl>> MulBaseUnreduced> for FpExt2, C> {} + +/// Identity-shape [`Unreduced`] for extension variants without a dedicated +/// accumulator: every "unreduced" op reduces immediately (`Product = Self`), +/// which is trivially exact per term — `SUM_IS_EXACT` keeps its +/// conservative `false` so callers do not switch to batched reduction. +macro_rules! unreduced_identity { + (impl[$($g:tt)*] $ty:ty, base: $base:ty) => { + impl<$($g)*> Unreduced for $ty { + type Product = Self; + type SmallProduct = Self; + type Wide = Self; + + #[inline] + fn mul_unreduced(self, other: Self) -> Self { + self * other + } + #[inline] + fn mul_u64_unreduced(self, small: u64) -> Self { + self.mul_base(<$base>::from_u64(small)) + } + #[inline] + fn scale_wide(self, small: i32) -> Self { + self.mul_base(<$base>::from_i64(small as i64)) + } + #[inline] + fn reduce_product(accum: Self) -> Self { + accum + } + #[inline] + fn reduce_small_product(accum: Self) -> Self { + accum + } + #[inline] + fn reduce_wide(wide: Self) -> Self { + wide + } + } + + impl<$($g)*> MulBaseUnreduced<$base> for $ty {} + }; +} + +unreduced_identity!(impl[const P: u32, C: Ext2Config>] FpExt2, C>, base: Fp32

); +unreduced_identity!(impl[const P: u128, C: Ext2Config>] FpExt2, C>, base: Fp128

); +unreduced_identity!(impl[const P: u64] FpExt4>, base: Fp64

); +unreduced_identity!(impl[const P: u128] FpExt4>, base: Fp128

); +unreduced_identity!(impl[F: PseudoMersenne] FpExt8, base: F); + +/// Default [`Fold`]: no precomputation, one generic multiply per pair. +macro_rules! fold_default { + (impl[$($g:tt)*] $ty:ty) => { + impl<$($g)*> Fold for $ty { + type Ctx = Self; + + #[inline] + fn precompute(r: Self) -> Self { + r + } + #[inline] + fn fold_one(r: &Self, even: Self, odd: Self) -> Self { + even + *r * (odd - even) + } + } + }; +} + +// Base fields and the extension variants without a specialized fold +// matrix. (A blanket `impl Fold for F` would be +// rejected by coherence against the `FpExt2<_, C>` impls: `C` is an open +// type parameter, so the compiler cannot rule out a downstream +// `PseudoMersenne` impl for a quadratic extension.) +fold_default!(impl[const P: u32] Fp32

); +fold_default!(impl[const P: u64] Fp64

); +fold_default!(impl[const P: u128] Fp128

); +fold_default!(impl[const P: u32, C: Ext2Config>] FpExt2, C>); +fold_default!(impl[const P: u128, C: Ext2Config>] FpExt2, C>); +fold_default!(impl[const P: u64] FpExt4>); +fold_default!(impl[const P: u128] FpExt4>); +fold_default!(impl[F: PseudoMersenne] FpExt8); + +/// Precomputed fold context for `FpExt4`: the 4×4 multiply-by-`r` +/// matrix in the `[1, e1, e2, e3]` basis, as canonical `u32` residues. +#[derive(Debug, Clone, Copy)] +pub struct FoldMatrixFp32(pub(crate) [[u32; 4]; 4]); + +impl Fold for FpExt4> { + type Ctx = FoldMatrixFp32; + + /// Columns of the deg-4 schedule (`ext4_mul_coeffs`) with `a = r`: + /// signed schedule entries like `r1 − r3` are baked as canonical + /// residues, so the fold is 16 unsigned multiply-adds. + #[inline] + fn precompute(r: Self) -> FoldMatrixFp32 { + let [r0, r1, r2, r3] = r.coeffs; + let two = Fp32::

::from_u64(2); + let lim = |x: Fp32

| x.to_limbs(); + FoldMatrixFp32([ + [lim(r0), lim(two * r1), lim(two * r2), lim(two * r3)], + [lim(r1), lim(r0 + r2), lim(r1 + r3), lim(r2)], + [lim(r2), lim(r1 + r3), lim(r0), lim(r1 - r3)], + [lim(r3), lim(r2), lim(r1 - r3), lim(r0 - r2)], + ]) + } + + /// `even + r·(odd − even)` via 4 base multiply-adds per coefficient + /// instead of the 22-product generic multiply. For `P < 2^31` each + /// product is `< 2^62` and a row sum of 4 fits `u64`; otherwise + /// products are `< 2^64` and the row sum (`< 2^66`) uses `u128`. + #[inline] + fn fold_one(ctx: &FoldMatrixFp32, even: Self, odd: Self) -> Self { + let m = &ctx.0; + let d: [u32; 4] = std::array::from_fn(|j| (odd.coeffs[j] - even.coeffs[j]).to_limbs()); + let folded: [Fp32

; 4] = if P < (1u32 << 31) { + std::array::from_fn(|row| { + let acc = (0..4) + .map(|j| (m[row][j] as u64) * (d[j] as u64)) + .sum::(); + Fp32::from_u64(acc) + even.coeffs[row] + }) + } else { + std::array::from_fn(|row| { + let acc = (0..4) + .map(|j| (m[row][j] as u128) * (d[j] as u128)) + .sum::(); + Fp32::from_u128_reduced(acc) + even.coeffs[row] + }) + }; + Self::new(folded) + } +} + +/// Reduces the integer sum `w0 + w1` of two products of canonical `Fp64` +/// residues (each `< 2^{2·BITS}`). +/// +/// - Sub-word primes (`BITS < 64`): the sum is `< 2^127`, reduce directly. +/// - Full-word primes: the sum reaches `< 2^129`; fold bits 64.. with +/// `2^64 ≡ C` and the overflow bit with `2^128 ≡ C²` (both need +/// `BITS = 64`). The folded value is `< 2^64 + 2^96 + 2^64 < 2^97`. +#[inline(always)] +fn fp64_reduce_sum_of_two_products(w0: u128, w1: u128) -> Fp64

{ + if Fp64::

::BITS < 64 { + Fp64::solinas_reduce(w0 + w1) + } else { + let (s, carry) = w0.overflowing_add(w1); + let c = Fp64::

::C as u128; + Fp64::solinas_reduce( + (s as u64 as u128) + ((s >> 64) as u64 as u128) * c + (carry as u128) * c * c, + ) + } +} + +/// Precomputed fold context for `FpExt2`: the 2×2 multiply-by-`r` +/// matrix `[[r0, NR·r1], [r1, r0]]` in the `[1, u]` basis (`u² = NR`), as +/// canonical `u64` residues. +#[derive(Debug, Clone, Copy)] +pub struct FoldMatrixFp64(pub(crate) [[u64; 2]; 2]); + +impl>> Fold for FpExt2, C> { + type Ctx = FoldMatrixFp64; + + #[inline] + fn precompute(r: Self) -> FoldMatrixFp64 { + let [r0, r1] = r.coeffs; + let nr_r1 = C::mul_non_residue(r1, |base| base); + FoldMatrixFp64([ + [r0.to_limbs(), nr_r1.to_limbs()], + [r1.to_limbs(), r0.to_limbs()], + ]) + } + + /// `even + r·(odd − even)`: each output coordinate is two `u64 × u64` + /// products with one delayed reduction + /// ([`fp64_reduce_sum_of_two_products`]) — schoolbook with 2 reductions + /// versus the generic Karatsuba's 3. Canonical, hence byte-identical to + /// the generic fold. + #[inline] + fn fold_one(ctx: &FoldMatrixFp64, even: Self, odd: Self) -> Self { + let m = &ctx.0; + let d0 = (odd.coeffs[0] - even.coeffs[0]).to_limbs() as u128; + let d1 = (odd.coeffs[1] - even.coeffs[1]).to_limbs() as u128; + let c0 = fp64_reduce_sum_of_two_products((m[0][0] as u128) * d0, (m[0][1] as u128) * d1); + let c1 = fp64_reduce_sum_of_two_products((m[1][0] as u128) * d0, (m[1][1] as u128) * d1); + Self::new(even.coeffs[0] + c0, even.coeffs[1] + c1) + } +} diff --git a/crates/jolt-field-two/src/unreduced.rs b/crates/jolt-field-two/src/unreduced.rs new file mode 100644 index 0000000000..08250898d0 --- /dev/null +++ b/crates/jolt-field-two/src/unreduced.rs @@ -0,0 +1,93 @@ +//! Deferred-reduction contracts: the unreduced value algebra around a field +//! ([`Unreduced`]) and the per-element multilinear-bind hook ([`Fold`]). +//! +//! The CPU prover's hot loops sum hundreds of products per output slot. +//! Reducing every product is wasted work when the products can be widened +//! into integer accumulators, summed with plain (carry-free or wrapping) +//! adds, and reduced once at the end. [`Unreduced`] is the single surface +//! for that pattern: it names the accumulator types and routes every +//! reduction back through the field type, so a backend's unreduced algebra +//! is enumerable from one `impl`. + +use crate::{AdditiveGroup, Field}; + +/// The deferred-reduction companion surface of a field. +/// +/// Three accumulator shapes cover the prover's patterns: +/// +/// - [`Product`](Self::Product): full `Self × Self` widening products +/// ([`mul_unreduced`](Self::mul_unreduced)). Addition is wrapping +/// per-slot, i.e. the accumulator is the group `(Z/2^128)^n`; a sum +/// reduces exactly whenever the final integer value of every slot is +/// below `2^128` (intermediate dips below zero cancel exactly). +/// - [`SmallProduct`](Self::SmallProduct): narrower `Self × u64` products +/// ([`mul_u64_unreduced`](Self::mul_u64_unreduced)). +/// - [`Wide`](Self::Wide): a carry-free signed accumulator over `i32` +/// lanes for sums of small-scalar multiples +/// ([`scale_wide`](Self::scale_wide)); lane overflow bounds are +/// documented on the concrete lane types. +/// +/// Each shape has a matching `reduce_*` back to a canonical element. The +/// per-type headroom (how many products fit before a slot can overflow) is +/// documented where the accumulation formula lives; **no runtime check +/// enforces it** beyond debug-mode overflow panics on the non-wrapping +/// lane types. +pub trait Unreduced: Field { + /// Accumulator for full `Self × Self` widening products. + type Product: AdditiveGroup; + + /// Accumulator for `Self × u64` widening products. + type SmallProduct: AdditiveGroup; + + /// Carry-free `i32`-lane accumulator for small-scalar multiples. + type Wide: AdditiveGroup + From; + + /// Whether delayed reduction over [`Product`](Self::Product) is exact: + /// `reduce_product(Σᵢ mul_unreduced(aᵢ, bᵢ)) = Σᵢ aᵢ·bᵢ` for batches + /// within the accumulator's documented headroom. + /// + /// Conservative default `false`; a field opts in only once its + /// accumulator is proven exact. Callers that must stay term-for-term + /// identical to `Mul` keep the per-term reduce path when this is + /// `false`. + const SUM_IS_EXACT: bool = false; + + /// Widening `self × other` with no reduction. + fn mul_unreduced(self, other: Self) -> Self::Product; + + /// Widening `self × small` with no reduction. + fn mul_u64_unreduced(self, small: u64) -> Self::SmallProduct; + + /// `self × small` as a wide lane value (equal to + /// `Self::Wide::from(self)` scaled lane-wise by `small`). + fn scale_wide(self, small: i32) -> Self::Wide; + + /// Reduces a full-product accumulator to a canonical element. + fn reduce_product(accum: Self::Product) -> Self; + + /// Reduces a small-product accumulator to a canonical element. + fn reduce_small_product(accum: Self::SmallProduct) -> Self; + + /// Reduces a wide lane accumulator to a canonical element. + fn reduce_wide(wide: Self::Wide) -> Self; +} + +/// Per-element multilinear bind: `even + r·(odd − even)` for a challenge +/// `r` fixed across a whole polynomial-binding round. +/// +/// This is a protocol-support hook rather than field algebra; it lives here +/// because implementations exploit the field representation — precomputing +/// a multiplication-by-`r` matrix from the challenge and folding each pair +/// with fewer reductions than a generic extension multiply. Implementations +/// must return exactly the canonical value of `even + r·(odd − even)`; the +/// loop structure and parallelism belong to the caller. +pub trait Fold: Field { + /// Precomputed context for folding by a fixed challenge `r`. + type Ctx: Copy + Send + Sync; + + /// Builds the fold context from the challenge `r`. + fn precompute(r: Self) -> Self::Ctx; + + /// Folds one pair: `even + r·(odd − even)`. + fn fold_one(ctx: &Self::Ctx, even: Self, odd: Self) -> Self; +} diff --git a/crates/jolt-field-two/tests/solinas_unreduced_differential.rs b/crates/jolt-field-two/tests/solinas_unreduced_differential.rs new file mode 100644 index 0000000000..13a2d8ab9a --- /dev/null +++ b/crates/jolt-field-two/tests/solinas_unreduced_differential.rs @@ -0,0 +1,921 @@ +//! Differential tests for the deferred-reduction machinery (`Unreduced`, +//! `Fold`, `MulBaseUnreduced`) against jolt-field, with an independent +//! schoolbook oracle (256-bit limb multiply + binary long division — no +//! Solinas folding, no shared code). +//! +//! Coverage per accumulator type: exactness of delayed sums vs direct +//! reduced multiplication over random batches AND adversarial batches +//! (all-max operands, wrap-through add/sub sequences), plus strict parity +//! with the baseline's `HasUnreducedOps`/`HasWide`/`ReduceTo`/ +//! `HasOptimizedFold`/`MulBaseUnreduced` machinery under identical inputs +//! and challenge constants. +//! +//! Headroom boundaries: the `i32`-lane bound (32768 max-lane accumulations) +//! is tested exactly, with the one-past case asserted to panic in debug +//! builds. The `u128`-slot headrooms (≥ 2^61 terms) are analytically +//! derived in `solinas/unreduced.rs` and computationally untestable; the +//! adversarial all-max batches here exercise the worst per-term slot +//! contributions those derivations bound. + +#![cfg(feature = "solinas")] +// NB: no `expect(clippy::unwrap_used)` — every unwrap here sits inside a +// local `macro_rules!` expansion, where the lint does not fire. + +use jolt_field as base; +use jolt_field_two as two; + +use base::unreduced::{HasOptimizedFold, HasUnreducedOps, HasWide, ReduceTo}; +use base::{CanonicalField, MulBaseUnreduced as BaseMulBaseUnreduced}; +use num_traits::Zero; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use two::{CanonicalEncoding, ExtField, Fold, MulBaseUnreduced, Ring, Unreduced}; + +const M61: u64 = (1 << 61) - 1; + +/// 128×128 → 256-bit schoolbook multiply over 64-bit halves (independent of +/// both crates' `mul_wide`). +fn oracle_mul_256(a: u128, b: u128) -> [u64; 4] { + let (a0, a1) = (a as u64 as u128, a >> 64); + let (b0, b1) = (b as u64 as u128, b >> 64); + let (p00, p01, p10, p11) = (a0 * b0, a0 * b1, a1 * b0, a1 * b1); + const LO: u128 = u64::MAX as u128; + let mid = (p00 >> 64) + (p01 & LO) + (p10 & LO); + let hi = (p01 >> 64) + (p10 >> 64) + (p11 & LO) + (mid >> 64); + let top = (p11 >> 64) + (hi >> 64); + [p00 as u64, mid as u64, hi as u64, top as u64] +} + +/// Little-endian limbs mod `p` by binary long division — no Solinas folding. +fn oracle_mod(limbs: &[u64], p: u128) -> u128 { + let mut r: u128 = 0; + for &limb in limbs.iter().rev() { + for i in (0..64).rev() { + let top = r >> 127; + let mut v = (r << 1) | ((limb >> i) & 1) as u128; + if top == 1 { + v = v.wrapping_add(0u128.wrapping_sub(p)); + } else if v >= p { + v -= p; + } + r = v; + } + } + r +} + +fn mulmod(a: u128, b: u128, p: u128) -> u128 { + oracle_mod(&oracle_mul_256(a, b), p) +} + +fn addmod(a: u128, b: u128, p: u128) -> u128 { + let (s, overflow) = a.overflowing_add(b); + oracle_mod(&[s as u64, (s >> 64) as u64, overflow as u64], p) +} + +fn submod(a: u128, b: u128, p: u128) -> u128 { + addmod(a, p - b, p) +} + +/// Full `Unreduced` + `HasWide` sweep for one paired base-field +/// instantiation: product/small-product batch exactness (random, +/// all-max, wrap-through), wide-lane roundtrip/group-ops/scaling — all +/// against the field ops, the schoolbook oracle, and the baseline. +macro_rules! base_field_suite { + ($name:ident, $F2:ty, $FB:ty, $p:expr, $seed:expr) => { + #[test] + fn $name() { + let p: u128 = $p; + let mut rng = ChaCha20Rng::seed_from_u64($seed); + let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); + let fb = |v: u128| <$FB as CanonicalField>::from_canonical_u128_checked(v).unwrap(); + let val2 = |x: &$F2| x.to_u128_checked().unwrap(); + let valb = |x: &$FB| x.to_canonical_u128(); + + assert_eq!( + <$F2 as Unreduced>::SUM_IS_EXACT, + <$FB as HasUnreducedOps>::DELAYED_PRODUCT_SUM_IS_EXACT, + "SUM_IS_EXACT parity" + ); + + // Σ aᵢ·bᵢ: delayed vs per-term vs oracle vs baseline. + let check_products = |pairs: &[(u128, u128)]| { + let expect = pairs + .iter() + .fold(0u128, |acc, &(a, b)| addmod(acc, mulmod(a, b, p), p)); + let acc2 = pairs.iter().fold( + <<$F2 as Unreduced>::Product as Zero>::zero(), + |acc, &(a, b)| acc + f2(a).mul_unreduced(f2(b)), + ); + assert_eq!(val2(&<$F2 as Unreduced>::reduce_product(acc2)), expect); + let per_term = pairs + .iter() + .fold(<$F2 as Zero>::zero(), |acc, &(a, b)| acc + f2(a) * f2(b)); + assert_eq!(val2(&per_term), expect, "per-term vs oracle"); + let accb = pairs.iter().fold( + <<$FB as HasUnreducedOps>::ProductAccum as Zero>::zero(), + |acc, &(a, b)| acc + fb(a).mul_to_product_accum(fb(b)), + ); + assert_eq!( + valb(&<$FB as HasUnreducedOps>::reduce_product_accum(accb)), + expect, + "baseline parity" + ); + }; + for &n in &[1usize, 2, 7, 501] { + let pairs: Vec<(u128, u128)> = (0..n) + .map(|_| (rng.gen::() % p, rng.gen::() % p)) + .collect(); + check_products(&pairs); + } + check_products(&vec![(p - 1, p - 1); 512]); + + // Σ aᵢ·bᵢ with raw u64 scalars (including u64::MAX and 0). + let check_small = |pairs: &[(u128, u64)]| { + let expect = pairs.iter().fold(0u128, |acc, &(a, b)| { + addmod(acc, mulmod(a, b as u128, p), p) + }); + let acc2 = pairs.iter().fold( + <<$F2 as Unreduced>::SmallProduct as Zero>::zero(), + |acc, &(a, b)| acc + f2(a).mul_u64_unreduced(b), + ); + assert_eq!( + val2(&<$F2 as Unreduced>::reduce_small_product(acc2)), + expect + ); + let accb = pairs.iter().fold( + <<$FB as HasUnreducedOps>::MulU64Accum as Zero>::zero(), + |acc, &(a, b)| acc + fb(a).mul_u64_unreduced(b), + ); + assert_eq!( + valb(&<$FB as HasUnreducedOps>::reduce_mul_u64_accum(accb)), + expect, + "baseline small-product parity" + ); + }; + for &n in &[1usize, 3, 400] { + let pairs: Vec<(u128, u64)> = (0..n) + .map(|_| (rng.gen::() % p, rng.gen::())) + .collect(); + check_small(&pairs); + } + check_small(&[(p - 1, u64::MAX), (p - 1, 0), (0, u64::MAX)]); + check_small(&vec![(p - 1, u64::MAX); 400]); + + // Wrap-through subtraction: t1 − t2 + t2 = t1 must be exact even + // though the intermediate slots dip below zero (wrapping group). + let (a1, b1) = (rng.gen::() % p, rng.gen::() % p); + let t1 = f2(a1).mul_unreduced(f2(b1)); + let t2 = f2(p - 1).mul_unreduced(f2(p - 1)); + assert_eq!( + val2(&<$F2 as Unreduced>::reduce_product(t1 - t2 + t2)), + mulmod(a1, b1, p), + "wrap-through sub/add" + ); + assert_eq!( + val2( + &(<$F2 as Unreduced>::reduce_product(t1) + - <$F2 as Unreduced>::reduce_product(t2)) + ), + submod(mulmod(a1, b1, p), mulmod(p - 1, p - 1, p), p), + "separate pos/neg accumulators" + ); + + // Wide lanes: roundtrip, group ops, scaling; vs baseline. + let mut vals: Vec = vec![0, 1, 2, p / 2, p - 2, p - 1]; + vals.extend((0..200).map(|_| rng.gen::() % p)); + for &x in &vals { + let w2 = <$F2 as Unreduced>::Wide::from(f2(x)); + assert_eq!(val2(&<$F2 as Unreduced>::reduce_wide(w2)), x, "roundtrip"); + let wb = <$FB as HasWide>::Wide::from(fb(x)); + assert_eq!(valb(&ReduceTo::<$FB>::reduce(wb)), x, "baseline roundtrip"); + + let y = rng.gen::() % p; + let wy = <$F2 as Unreduced>::Wide::from(f2(y)); + assert_eq!( + val2(&<$F2 as Unreduced>::reduce_wide(w2 + wy)), + addmod(x, y, p) + ); + assert_eq!( + val2(&<$F2 as Unreduced>::reduce_wide(w2 - wy)), + submod(x, y, p) + ); + assert_eq!(val2(&<$F2 as Unreduced>::reduce_wide(-w2)), submod(0, x, p)); + + for s in [-32768i32, -12345, -1, 0, 1, 2, 12345, 32768] { + let got = <$F2 as Unreduced>::reduce_wide(f2(x).scale_wide(s)); + assert_eq!(got, f2(x) * <$F2 as Ring>::from_i64(s as i64), "scale_wide"); + let gotb = ReduceTo::<$FB>::reduce(fb(x).mul_small_to_wide(s)); + assert_eq!(val2(&got), valb(&gotb), "baseline scale parity"); + } + } + + // Mixed-sign wide accumulation (magnitudes stay within lane + // headroom: ≤ 300 canonical terms). + let mut w2 = <<$F2 as Unreduced>::Wide as Zero>::zero(); + let mut expect = 0u128; + for (i, &x) in vals.iter().enumerate() { + if i % 3 == 0 { + w2 -= <$F2 as Unreduced>::Wide::from(f2(x)); + expect = submod(expect, x, p); + } else { + w2 += <$F2 as Unreduced>::Wide::from(f2(x)); + expect = addmod(expect, x, p); + } + } + assert_eq!( + val2(&<$F2 as Unreduced>::reduce_wide(w2)), + expect, + "mixed signs" + ); + + // Degree-1 MulBaseUnreduced blanket: default body is the plain + // unreduced product. + let (x, s) = (rng.gen::() % p, rng.gen::() % p); + assert_eq!( + val2(&<$F2 as Unreduced>::reduce_product( + f2(x).mul_base_unreduced(f2(s)) + )), + mulmod(x, s, p), + "degree-1 mul_base_unreduced" + ); + } + }; +} + +base_field_suite!( + fp32_prime24_unreduced, + two::Prime24Offset3, + base::Prime24Offset3, + (1 << 24) - 3, + 0x0724_0001 +); +base_field_suite!( + fp32_prime32_unreduced, + two::Prime32Offset99, + base::Prime32Offset99, + (1 << 32) - 99, + 0x0732_0002 +); +base_field_suite!( + fp64_prime40_unreduced, + two::Prime40Offset195, + base::Prime40Offset195, + (1 << 40) - 195, + 0x0740_0003 +); +base_field_suite!( + fp64_prime64_unreduced, + two::Prime64Offset59, + base::Prime64Offset59, + u64::MAX as u128 - 58, + 0x0764_0004 +); +base_field_suite!( + fp128_prime275_unreduced, + two::Prime128Offset275, + base::Prime128Offset275, + u128::MAX - 274, + 0x0728_0005 +); +base_field_suite!( + fp128_prime_a7f7_unreduced, + two::Prime128OffsetA7F7, + base::Prime128OffsetA7F7, + u128::MAX - 0xFFFF_A7F6, + 0x0728_0006 +); + +/// The `i32`-lane headroom boundary: 32768 all-max-lane accumulations are +/// exact (32768 · 0xFFFF = 2147450880 ≤ i32::MAX). +#[test] +fn wide_lane_headroom_boundary_is_exact() { + type F = two::Prime128Offset275; + let unit = two::Fp128x8i32([0xFFFF; 8]); // value = 2^128 − 1 + let mut acc = two::Fp128x8i32([0; 8]); + for _ in 0..32768 { + acc += unit; + } + let expect = ::from_u128(u128::MAX) * ::from_u64(32768); + assert_eq!(::reduce_wide(acc), expect); +} + +/// One past the lane headroom overflows an `i32` lane; the non-wrapping +/// lane ops turn that into a debug-build panic (the only runtime +/// enforcement the contract has). +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "attempt to add with overflow")] +fn wide_lane_one_past_headroom_panics_in_debug() { + let unit = two::Fp128x8i32([0xFFFF; 8]); + let mut acc = two::Fp128x8i32([0; 8]); + for _ in 0..32769 { + acc += unit; + } + let _ = std::hint::black_box(acc); +} + +/// `FpExt4` fused accumulator: delayed batch sums vs per-term ring +/// multiplication and the baseline, plus the coordinate-scaling +/// `MulBaseUnreduced` override. +macro_rules! ext4_fp32_suite { + ($name:ident, $F2:ty, $FB:ty, $p:expr, $seed:expr) => { + #[test] + fn $name() { + type E2 = two::FpExt4<$F2>; + type EB = base::FpExt4<$FB>; + let p: u128 = $p; + let mut rng = ChaCha20Rng::seed_from_u64($seed); + let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); + let fb = |v: u128| <$FB as CanonicalField>::from_canonical_u128_checked(v).unwrap(); + let mk2 = |v: [u128; 4]| E2::new(v.map(f2)); + let mkb = |v: [u128; 4]| EB::new(v.map(fb)); + let vec2 = |e: &E2| e.coeffs.map(|c| c.to_u128_checked().unwrap()); + let vecb = |e: &EB| e.coeffs.map(|c| c.to_canonical_u128()); + let sample = |rng: &mut ChaCha20Rng| -> [u128; 4] { + std::array::from_fn(|_| rng.gen::() % p) + }; + + assert!(::SUM_IS_EXACT); + assert!(::DELAYED_PRODUCT_SUM_IS_EXACT); + + let check = |pairs: &[([u128; 4], [u128; 4])]| { + let acc2 = pairs.iter().fold( + <::Product as Zero>::zero(), + |acc, &(a, b)| acc + mk2(a).mul_unreduced(mk2(b)), + ); + let per_term = pairs + .iter() + .fold(::zero(), |acc, &(a, b)| acc + mk2(a) * mk2(b)); + assert_eq!( + ::reduce_product(acc2), + per_term, + "delayed sum vs per-term" + ); + let accb = pairs.iter().fold( + <::ProductAccum as Zero>::zero(), + |acc, &(a, b)| acc + mkb(a).mul_to_product_accum(mkb(b)), + ); + assert_eq!( + vec2(&per_term), + vecb(&::reduce_product_accum(accb)), + "baseline parity" + ); + }; + for &n in &[1usize, 2, 33, 512] { + let pairs: Vec<_> = (0..n) + .map(|_| (sample(&mut rng), sample(&mut rng))) + .collect(); + check(&pairs); + } + // Adversarial: all coefficients at p − 1 maximizes every fused + // column sum (the 7·P² per-term worst case). + check(&vec![([p - 1; 4], [p - 1; 4]); 512]); + + // Wrap-through subtraction on the fused accumulator. + let (a, b) = (sample(&mut rng), sample(&mut rng)); + let t1 = mk2(a).mul_unreduced(mk2(b)); + let t2 = mk2([p - 1; 4]).mul_unreduced(mk2([p - 1; 4])); + assert_eq!( + ::reduce_product(t1 - t2 + t2), + mk2(a) * mk2(b), + "wrap-through sub/add" + ); + + // MulBaseUnreduced override: vs mul_base, vs the default + // lift-then-mul body, vs the baseline; batched. + let mut acc2 = <::Product as Zero>::zero(); + let mut accb = <::ProductAccum as Zero>::zero(); + let mut per_term = ::zero(); + for _ in 0..300 { + let (xv, sv) = (sample(&mut rng), rng.gen::() % p); + let (x2, xb) = (mk2(xv), mkb(xv)); + let over = x2.mul_base_unreduced(f2(sv)); + assert_eq!( + ::reduce_product(over), + x2.mul_base(f2(sv)), + "override vs mul_base" + ); + assert_eq!( + ::reduce_product(over), + ::reduce_product( + x2.mul_unreduced(>::lift_base(f2(sv))) + ), + "override vs default lift-then-mul" + ); + acc2 += over; + accb += xb.mul_base_to_product_accum(fb(sv)); + per_term += x2.mul_base(f2(sv)); + } + assert_eq!(::reduce_product(acc2), per_term); + assert_eq!( + vec2(&per_term), + vecb(&::reduce_product_accum(accb)), + "baseline mul-base batch parity" + ); + } + }; +} + +ext4_fp32_suite!( + ext4_fp32_prime24_accum, + two::Prime24Offset3, + base::Prime24Offset3, + (1 << 24) - 3, + 0x0E44_0001 +); +ext4_fp32_suite!( + ext4_fp32_prime32_accum, + two::Prime32Offset99, + base::Prime32Offset99, + (1 << 32) - 99, + 0x0E44_0002 +); + +/// `FpExt2` carry-tracked accumulator: batch exactness vs per-term +/// multiplication and the baseline (both non-residue configs), plus the +/// `AccumPair` small-product path. +macro_rules! ext2_fp64_suite { + ($name:ident, $F2:ty, $FB:ty, $C2:ty, $CB:ty, $p:expr, $seed:expr) => { + #[test] + fn $name() { + type E2 = two::FpExt2<$F2, $C2>; + type EB = base::FpExt2<$FB, $CB>; + let p: u128 = $p; + let mut rng = ChaCha20Rng::seed_from_u64($seed); + let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); + let fb = |v: u128| <$FB as CanonicalField>::from_canonical_u128_checked(v).unwrap(); + let mk2 = |v: [u128; 2]| E2::new(f2(v[0]), f2(v[1])); + let mkb = |v: [u128; 2]| EB::new(fb(v[0]), fb(v[1])); + let vec2 = |e: &E2| e.coeffs.map(|c| c.to_u128_checked().unwrap()); + let vecb = |e: &EB| e.coeffs.map(|c| c.to_canonical_u128()); + let sample = |rng: &mut ChaCha20Rng| -> [u128; 2] { + std::array::from_fn(|_| rng.gen::() % p) + }; + + assert!(::SUM_IS_EXACT); + assert!(::DELAYED_PRODUCT_SUM_IS_EXACT); + + let check = |pairs: &[([u128; 2], [u128; 2])]| { + let acc2 = pairs.iter().fold( + <::Product as Zero>::zero(), + |acc, &(a, b)| acc + mk2(a).mul_unreduced(mk2(b)), + ); + let per_term = pairs + .iter() + .fold(::zero(), |acc, &(a, b)| acc + mk2(a) * mk2(b)); + assert_eq!( + ::reduce_product(acc2), + per_term, + "delayed sum vs per-term" + ); + let accb = pairs.iter().fold( + <::ProductAccum as Zero>::zero(), + |acc, &(a, b)| acc + mkb(a).mul_to_product_accum(mkb(b)), + ); + assert_eq!( + vec2(&per_term), + vecb(&::reduce_product_accum(accb)), + "baseline parity" + ); + }; + for &n in &[1usize, 2, 33, 512] { + let pairs: Vec<_> = (0..n) + .map(|_| (sample(&mut rng), sample(&mut rng))) + .collect(); + check(&pairs); + } + // All-max coefficients maximize p00/p11 and force the c0 carry + // paths (P² bias wrap for NR = −1, double-add carries for + // NR = 2) on every term. + check(&vec![([p - 1; 2], [p - 1; 2]); 512]); + // Single products at the corners of the carry analysis. + for corner in [[0u128, p - 1], [p - 1, 0], [1, p - 1], [p - 1, 1]] { + check(&[(corner, [p - 1; 2])]); + } + + // Wrap-through subtraction. + let (a, b) = (sample(&mut rng), sample(&mut rng)); + let t1 = mk2(a).mul_unreduced(mk2(b)); + let t2 = mk2([p - 1; 2]).mul_unreduced(mk2([p - 1; 2])); + assert_eq!( + ::reduce_product(t1 - t2 + t2), + mk2(a) * mk2(b), + "wrap-through sub/add" + ); + + // Small products through the AccumPair path. + let pairs: Vec<([u128; 2], u64)> = (0..300) + .map(|_| (sample(&mut rng), rng.gen::())) + .collect(); + let acc2 = pairs.iter().fold( + <::SmallProduct as Zero>::zero(), + |acc, &(x, s)| acc + mk2(x).mul_u64_unreduced(s), + ); + let per_term = pairs.iter().fold(::zero(), |acc, &(x, s)| { + acc + mk2(x) * ::from_u64(s) + }); + assert_eq!( + ::reduce_small_product(acc2), + per_term, + "small-product delayed sum" + ); + let accb = pairs.iter().fold( + <::MulU64Accum as Zero>::zero(), + |acc, &(x, s)| acc + mkb(x).mul_u64_unreduced(s), + ); + assert_eq!( + vec2(&per_term), + vecb(&::reduce_mul_u64_accum(accb)), + "baseline small-product parity" + ); + + // MulBaseUnreduced default body routes through the fused accum. + let (xv, sv) = (sample(&mut rng), rng.gen::() % p); + assert_eq!( + ::reduce_product(mk2(xv).mul_base_unreduced(f2(sv))), + mk2(xv).mul_base(f2(sv)), + "mul_base_unreduced" + ); + } + }; +} + +ext2_fp64_suite!( + ext2_fp64_prime40_two_nr_accum, + two::Prime40Offset195, + base::Prime40Offset195, + two::TwoNr, + base::TwoNr, + (1 << 40) - 195, + 0x0E22_0001 +); +ext2_fp64_suite!( + ext2_fp64_prime64_two_nr_accum, + two::Prime64Offset59, + base::Prime64Offset59, + two::TwoNr, + base::TwoNr, + u64::MAX as u128 - 58, + 0x0E22_0002 +); +// Mersenne-61 is the one convenient u64 prime with p ≡ 3 (mod 4), where +// NegOneNr is a genuine non-residue — exercises the P²-bias carry branch. +ext2_fp64_suite!( + ext2_fp64_m61_neg_one_nr_accum, + two::Fp64, + base::Fp64, + two::NegOneNr, + base::NegOneNr, + M61 as u128, + 0x0E22_0003 +); + +/// Fold parity for one paired instantiation: `fold_one(precompute(r), e, o)` +/// must equal the field identity `e + r·(o − e)` AND the baseline's +/// optimized fold under the same challenge constants. +macro_rules! fold_parity { + ($name:ident, $E2:ty, $EB:ty, $F2:ty, $FB:ty, $d:expr, $p:expr, $seed:expr) => { + #[test] + fn $name() { + let p: u128 = $p; + let d: usize = $d; + let mut rng = ChaCha20Rng::seed_from_u64($seed); + let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); + let fb = |v: u128| <$FB as CanonicalField>::from_canonical_u128_checked(v).unwrap(); + let mk2 = |vals: &[u128]| { + <$E2 as ExtField<$F2>>::from_base_slice( + &vals.iter().map(|&v| f2(v)).collect::>(), + ) + }; + let mkb = |vals: &[u128]| { + <$EB as base::ExtField<$FB>>::from_base_slice( + &vals.iter().map(|&v| fb(v)).collect::>(), + ) + }; + let vec2 = |e: &$E2| { + <$E2 as ExtField<$F2>>::to_base_vec(e) + .iter() + .map(|c| c.to_u128_checked().unwrap()) + .collect::>() + }; + let vecb = |e: &$EB| { + <$EB as base::ExtField<$FB>>::to_base_vec(e) + .iter() + .map(|c| c.to_canonical_u128()) + .collect::>() + }; + let sample = |rng: &mut ChaCha20Rng| -> Vec { + (0..d).map(|_| rng.gen::() % p).collect() + }; + + for _ in 0..8 { + let rv = sample(&mut rng); + let (r2, rb) = (mk2(&rv), mkb(&rv)); + let ctx2 = <$E2 as Fold>::precompute(r2); + let ctxb = <$EB as HasOptimizedFold>::precompute_fold(rb); + + let mut cases: Vec<(Vec, Vec)> = vec![ + (vec![0; d], vec![p - 1; d]), + (vec![p - 1; d], vec![0; d]), + (vec![p - 1; d], vec![p - 1; d]), + (vec![0; d], vec![0; d]), + ]; + cases.extend((0..24).map(|_| (sample(&mut rng), sample(&mut rng)))); + for (ev, ov) in cases { + let (e2, o2) = (mk2(&ev), mk2(&ov)); + let got = <$E2 as Fold>::fold_one(&ctx2, e2, o2); + assert_eq!(got, e2 + r2 * (o2 - e2), "fold vs field identity"); + let gotb = <$EB as HasOptimizedFold>::fold_one(&ctxb, mkb(&ev), mkb(&ov)); + assert_eq!(vec2(&got), vecb(&gotb), "fold vs baseline"); + } + } + } + }; +} + +fold_parity!( + fold_fp32_prime24, + two::Prime24Offset3, + base::Prime24Offset3, + two::Prime24Offset3, + base::Prime24Offset3, + 1, + (1 << 24) - 3, + 0x0F01 +); +fold_parity!( + fold_fp32_prime32, + two::Prime32Offset99, + base::Prime32Offset99, + two::Prime32Offset99, + base::Prime32Offset99, + 1, + (1 << 32) - 99, + 0x0F02 +); +fold_parity!( + fold_fp64_prime40, + two::Prime40Offset195, + base::Prime40Offset195, + two::Prime40Offset195, + base::Prime40Offset195, + 1, + (1 << 40) - 195, + 0x0F03 +); +fold_parity!( + fold_fp64_prime64, + two::Prime64Offset59, + base::Prime64Offset59, + two::Prime64Offset59, + base::Prime64Offset59, + 1, + u64::MAX as u128 - 58, + 0x0F04 +); +fold_parity!( + fold_fp128_prime275, + two::Prime128Offset275, + base::Prime128Offset275, + two::Prime128Offset275, + base::Prime128Offset275, + 1, + u128::MAX - 274, + 0x0F05 +); +fold_parity!( + fold_fp128_prime_a7f7, + two::Prime128OffsetA7F7, + base::Prime128OffsetA7F7, + two::Prime128OffsetA7F7, + base::Prime128OffsetA7F7, + 1, + u128::MAX - 0xFFFF_A7F6, + 0x0F06 +); +// FpExt2 fold matrices (the specialized EOR fold), all three configs. +fold_parity!( + fold_ext2_fp64_prime40, + two::Ext2, + base::Ext2, + two::Prime40Offset195, + base::Prime40Offset195, + 2, + (1 << 40) - 195, + 0x0F07 +); +fold_parity!( + fold_ext2_fp64_prime64, + two::Ext2, + base::Ext2, + two::Prime64Offset59, + base::Prime64Offset59, + 2, + u64::MAX as u128 - 58, + 0x0F08 +); +fold_parity!( + fold_ext2_fp64_m61_neg_one, + two::FpExt2, two::NegOneNr>, + base::FpExt2, base::NegOneNr>, + two::Fp64, + base::Fp64, + 2, + M61 as u128, + 0x0F09 +); +// FpExt2 default folds over the other bases. +fold_parity!( + fold_ext2_fp32_prime32, + two::Ext2, + base::Ext2, + two::Prime32Offset99, + base::Prime32Offset99, + 2, + (1 << 32) - 99, + 0x0F0A +); +fold_parity!( + fold_ext2_fp128_prime275, + two::Ext2, + base::Ext2, + two::Prime128Offset275, + base::Prime128Offset275, + 2, + u128::MAX - 274, + 0x0F0B +); +// FpExt4 fold matrix, both reduction paths (P < 2^31 and P ≥ 2^31). +fold_parity!( + fold_ext4_fp32_prime24, + two::FpExt4, + base::FpExt4, + two::Prime24Offset3, + base::Prime24Offset3, + 4, + (1 << 24) - 3, + 0x0F0C +); +fold_parity!( + fold_ext4_fp32_prime32, + two::FpExt4, + base::FpExt4, + two::Prime32Offset99, + base::Prime32Offset99, + 4, + (1 << 32) - 99, + 0x0F0D +); +// FpExt4 default folds over the other bases. +fold_parity!( + fold_ext4_fp64_prime40, + two::FpExt4, + base::FpExt4, + two::Prime40Offset195, + base::Prime40Offset195, + 4, + (1 << 40) - 195, + 0x0F0E +); +fold_parity!( + fold_ext4_fp128_prime275, + two::FpExt4, + base::FpExt4, + two::Prime128Offset275, + base::Prime128Offset275, + 4, + u128::MAX - 274, + 0x0F0F +); +// FpExt8 default folds across all three widths. +fold_parity!( + fold_ext8_fp32_prime24, + two::FpExt8, + base::FpExt8, + two::Prime24Offset3, + base::Prime24Offset3, + 8, + (1 << 24) - 3, + 0x0F10 +); +fold_parity!( + fold_ext8_fp64_prime40, + two::FpExt8, + base::FpExt8, + two::Prime40Offset195, + base::Prime40Offset195, + 8, + (1 << 40) - 195, + 0x0F11 +); +fold_parity!( + fold_ext8_fp128_prime275, + two::FpExt8, + base::FpExt8, + two::Prime128Offset275, + base::Prime128Offset275, + 8, + u128::MAX - 274, + 0x0F12 +); + +/// Identity-shape extensions: the `MulBaseUnreduced` default body must +/// match `mul_base`, and the identity `Unreduced` ops must match plain +/// ring arithmetic. +macro_rules! identity_unreduced_suite { + ($name:ident, $E2:ty, $F2:ty, $d:expr, $p:expr, $seed:expr) => { + #[test] + fn $name() { + let p: u128 = $p; + let d: usize = $d; + let mut rng = ChaCha20Rng::seed_from_u64($seed); + let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); + let mk2 = |vals: &[u128]| { + <$E2 as ExtField<$F2>>::from_base_slice( + &vals.iter().map(|&v| f2(v)).collect::>(), + ) + }; + assert!(!<$E2 as Unreduced>::SUM_IS_EXACT); + for _ in 0..32 { + let xv: Vec = (0..d).map(|_| rng.gen::() % p).collect(); + let yv: Vec = (0..d).map(|_| rng.gen::() % p).collect(); + let (x, y) = (mk2(&xv), mk2(&yv)); + let (s64, s32, sf) = (rng.gen::(), rng.gen::(), rng.gen::() % p); + assert_eq!( + <$E2 as Unreduced>::reduce_product(x.mul_unreduced(y)), + x * y + ); + assert_eq!( + <$E2 as Unreduced>::reduce_small_product(x.mul_u64_unreduced(s64)), + x * <$E2 as Ring>::from_u64(s64) + ); + assert_eq!( + <$E2 as Unreduced>::reduce_wide(x.scale_wide(s32)), + x * <$E2 as Ring>::from_i64(s32 as i64) + ); + assert_eq!( + <$E2 as Unreduced>::reduce_product(x.mul_base_unreduced(f2(sf))), + x.mul_base(f2(sf)) + ); + } + } + }; +} + +identity_unreduced_suite!( + identity_ext2_fp32, + two::Ext2, + two::Prime32Offset99, + 2, + (1 << 32) - 99, + 0x1D01 +); +identity_unreduced_suite!( + identity_ext2_fp128, + two::Ext2, + two::Prime128Offset275, + 2, + u128::MAX - 274, + 0x1D02 +); +identity_unreduced_suite!( + identity_ext4_fp64, + two::FpExt4, + two::Prime40Offset195, + 4, + (1 << 40) - 195, + 0x1D03 +); +identity_unreduced_suite!( + identity_ext4_fp128, + two::FpExt4, + two::Prime128OffsetA7F7, + 4, + u128::MAX - 0xFFFF_A7F6, + 0x1D04 +); +identity_unreduced_suite!( + identity_ext8_fp32, + two::FpExt8, + two::Prime24Offset3, + 8, + (1 << 24) - 3, + 0x1D05 +); +identity_unreduced_suite!( + identity_ext8_fp64, + two::FpExt8, + two::Prime64Offset59, + 8, + u64::MAX as u128 - 58, + 0x1D06 +); +identity_unreduced_suite!( + identity_ext8_fp128, + two::FpExt8, + two::Prime128Offset275, + 8, + u128::MAX - 274, + 0x1D07 +); From 19e88a7cec24fa7f3c260ea8b5f46446e5ede7ef Mon Sep 17 00:00:00 2001 From: acentelles Date: Thu, 30 Jul 2026 20:14:56 -0400 Subject: [PATCH 24/38] feat(jolt-field-two): packed SIMD backends (checkpoint 8) One packed-algebra source of truth: the entire Solinas fold/ canonicalize algebra (fp32 2-or-3-fold with TWO_FOLD_OK, C = 2^a +/- 1 shift-add offset multiply, N-ary deferred-reduction dot products with the BITS==32 prefold, fused deg-4 kernels; fp64 dual-path add/sub and both reduce128 variants; fp128 SoA carry-chain add/sub) written once in engine.rs/fp128.rs, generic over a SimdWord vocabulary; the genuinely algorithmic per-ISA differences confined to simd.rs (AVX2's missing 64-bit multiply assembled from 32x32 products, AVX-512 mask-register selects and native mullo_epi64, NEON's vqdmulhq_s32 kernel for the 31-bit prime). Note for review: the SPEC pillar says engine *macro*; generic types over SimdWord achieve the same single-source outcome with less expansion (recorded in SPEC.md). Packed ext consumes the shared schedules.rs; select_packing! cfg cascade ported token-identically. Counted LOC 1,067 vs the 1,600 budget (engine 284/550, fp128 99/350, ext 191/230, simd 352/350). Two overages flagged for review: packed.rs 105/90 (the ext4/8 kernel hooks on Packed - overridable defaults are the only stable-Rust route for the fp32 engines' fused kernels) and packed/mod.rs 36/30 (cfg formatting of the cascade). Improvements over baseline: packed Fp128 mul calls the scalar kernel per lane on all ISAs (on aarch64 that is the inline-asm multiply, strictly better than the baseline NEON backend's duplicated portable fold); the baseline's implicit C < 2^(64-BITS) assumption in reduce128_small_k is now debug-asserted. Dropped with SPEC evidence: Mersenne31 C==1 kernels (no registered prime), BITS==31 fold clones, NEON per-C shift-add chains, NEON dot-product carry tracking (unified on prefold), vectorized ext2/4 inverses (inversion is lane-serial in every formulation). Baseline oddity: NEON PackedFp64::fp_ext2_mul override is byte-identical to the trait default (dead specialization). cfg coverage: every introduced region compiled - native NEON (tests RUN), +avx2 and +avx512f,+avx512dq x86_64-apple-darwin checks, plain x86 fallback, aarch64 -neon fallback, no-default-features and solinas-only. 14 new differential tests (116/116 all-features): packed-vs-scalar for all 12 registered primes across widths, packed ext over fp32/fp64/fp128 bases and both NR configs, lane/slice laws, NEON lane-exact differentials vs the baseline packed types. x86 backends are compile-verified only on this machine; performance re-evaluation of dropped specializations is bench-gated per SPEC. --- crates/jolt-field-two/SPEC.md | 46 ++ crates/jolt-field-two/src/lib.rs | 9 +- crates/jolt-field-two/src/packed.rs | 169 ++++++ crates/jolt-field-two/src/solinas/mod.rs | 4 + .../src/solinas/packed/engine.rs | 366 ++++++++++++ .../jolt-field-two/src/solinas/packed/ext.rs | 244 ++++++++ .../src/solinas/packed/fp128.rs | 129 ++++ .../jolt-field-two/src/solinas/packed/mod.rs | 59 ++ .../jolt-field-two/src/solinas/packed/simd.rs | 455 ++++++++++++++ .../tests/solinas_packed_differential.rs | 559 ++++++++++++++++++ 10 files changed, 2037 insertions(+), 3 deletions(-) create mode 100644 crates/jolt-field-two/src/packed.rs create mode 100644 crates/jolt-field-two/src/solinas/packed/engine.rs create mode 100644 crates/jolt-field-two/src/solinas/packed/ext.rs create mode 100644 crates/jolt-field-two/src/solinas/packed/fp128.rs create mode 100644 crates/jolt-field-two/src/solinas/packed/mod.rs create mode 100644 crates/jolt-field-two/src/solinas/packed/simd.rs create mode 100644 crates/jolt-field-two/tests/solinas_packed_differential.rs diff --git a/crates/jolt-field-two/SPEC.md b/crates/jolt-field-two/SPEC.md index 1c6944716d..c0de003847 100644 --- a/crates/jolt-field-two/SPEC.md +++ b/crates/jolt-field-two/SPEC.md @@ -176,6 +176,52 @@ and fold matrices; `S64`–`S256` + hi32 variants; `Limbs`; rayon helpers; baseline compiled its two-case body for arbitrary `FpExt2Config`s; the port debug-asserts `NR ∈ {−1, 2}`. +**Dropped-specialization evidence (checkpoint 8, packed):** + +- **Mechanism note:** the "engine macro stamped per width × ISA" pillar is + realized as generic types (`PackedFp32/64/128`) over a + per-ISA vocabulary trait in `simd.rs`, with `macro_rules!` used only for + operator stamping (`impl_packed_arith!`) and vocabulary forwarding + (`fwd!`). Same one-source-of-truth outcome, stronger type checking, and + it moved weight from `engine.rs` (284/550) into `simd.rs` (352/350). +- **Added (not in baseline's contract shape):** `ext4_mul`/`ext4_square`/ + `ext8_mul`/`ext8_square` kernel hooks on `Packed` with schedule defaults + (`schedules.rs`), so the fp32 engines can override the deg-4 kernels with + fused deferred-reduction dot products and every backend shares one + formula source. This puts `packed.rs` at 105 vs its 90 budget; the hooks + cannot live elsewhere (overridable defaults need the trait). +- **Kept:** the NEON 31-bit pseudo-Mersenne multiply kernel (`mul_pm31`, + all lanes stay 32-bit via `vqdmulhq_s32`), generalized to cover `C = 1`: + it serves the registered `Prime31Offset19` on the benched native ISA. +- **Dropped:** the dedicated Mersenne31 (`C == 1`) multiply kernels on all + three ISAs — no registered prime has `C = 1`; NEON's kept `mul_pm31` + subsumes the case, x86 falls back to the value-identical generic fold. +- **Dropped:** the `BITS == 31` immediate-shift fold variants + (`solinas_reduce_bits31` and friends, per-ISA) — value-identical + micro-opts duplicating the whole fold; 31-bit packed multiplies now go + through `mul_pm31` on NEON anyway, so only the ext dot products take the + variable-shift 64-bit fold. +- **Dropped:** the NEON per-C shift-add chains for `C ∈ {19, 35, 99}`; + replaced by an ISA-generic `C = 2^a ± 1` shift-add fast path in the + shared engine (`mul_by_offset`, covers `C = 3`) — no in-tree benchmark + existed for the chains, and the generic `mul_small` handles the rest. +- **Dropped:** the NEON `BITS == 32` dot-product carry-tracking machinery + (`add_u64_with_carry`/`carry_correction`/`SHIFT64_MOD_P`); all ISAs now + use the x86 per-product prefold strategy (value-identical, comparable op + count, one shared bound argument). +- **Dropped:** the vectorized packed ext2/ext4 inverse formulas; all packed + inversion is lane-wise scalar (the `Packed::inverse` default). Every + formulation performs one lane-serial base-field Fermat inversion per + lane, which dominates; only ~20 non-inversion multiplies per lane change + from packed to scalar, on a cold path. +- **Changed:** packed `Fp128` multiplication calls the scalar kernel per + lane on every ISA — on AArch64 that is the inline-asm multiply, strictly + better than the baseline NEON backend's duplicated portable fold + (avx2/avx512 baselines already went lane-by-lane). +- **No unreduced coupling:** the packed layer consumes only `schedules.rs` + and the scalar field types; nothing awaits the checkpoint-7 `Unreduced` + surface. + ## Design pillars 1. **Const-generic scalar core**: `Fp64` etc., fold constants diff --git a/crates/jolt-field-two/src/lib.rs b/crates/jolt-field-two/src/lib.rs index 45403a0cf5..cb9989cebf 100644 --- a/crates/jolt-field-two/src/lib.rs +++ b/crates/jolt-field-two/src/lib.rs @@ -23,6 +23,7 @@ mod bn254; mod extension; mod limbs; mod ops; +mod packed; mod schedules; pub mod signed; #[cfg(feature = "solinas")] @@ -38,14 +39,16 @@ pub use bn254::{Fq, Fr, WideAccumulator}; pub use extension::{Ext2Config, ExtField, MulBaseUnreduced, NegOneNr, TwoNr}; pub use limbs::Limbs; pub use num_traits::{One, Zero}; +pub use packed::{NoPacking, Packed, WithPacking}; #[cfg(feature = "solinas")] pub use solinas::{ balanced_digit_lut, canonical_frobenius_thetas, is_registered_prime_offset, pseudo_mersenne_modulus, registered_prime_offset_spec, solve_frobenius_moore, validate_canonical_frobenius_thetas, AccumPair, Ext2, FoldMatrixFp32, FoldMatrixFp64, Fp128, - Fp128MulU64Accum, Fp128ProductAccum, Fp128x8i32, Fp32, Fp32ProductAccum, Fp32x2i32, Fp64, - Fp64ProductAccum, Fp64x4i32, FpExt2, FpExt2Fp64ProductAccum, FpExt4, FpExt4Fp32ProductAccum, - FpExt8, Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, + Fp128MulU64Accum, Fp128Packing, Fp128ProductAccum, Fp128x8i32, Fp32, Fp32Packing, + Fp32ProductAccum, Fp32x2i32, Fp64, Fp64Packing, Fp64ProductAccum, Fp64x4i32, FpExt2, + FpExt2Fp64ProductAccum, FpExt4, FpExt4Fp32ProductAccum, FpExt8, PackedFpExt2, PackedFpExt4, + PackedFpExt8, Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, Prime24Offset3, Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime48Offset59, Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, PRIME_OFFSET_IMPLEMENTED_MAX_BITS, PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, diff --git a/crates/jolt-field-two/src/packed.rs b/crates/jolt-field-two/src/packed.rs new file mode 100644 index 0000000000..b01a230734 --- /dev/null +++ b/crates/jolt-field-two/src/packed.rs @@ -0,0 +1,169 @@ +//! Packed-lane contracts: [`Packed`] (`WIDTH` parallel scalar lanes), +//! [`WithPacking`] (scalar → packed association), and the [`NoPacking`] +//! one-lane fallback used on targets without a SIMD backend. +//! +//! The extension kernel hooks default to the shared coefficient schedules +//! (`crate::schedules`), so every backend computes the same field values; +//! SIMD backends override the degree-4 hooks with fused deferred-reduction +//! dot products. + +use crate::{Ext2Config, Field}; +use num_traits::Zero; +use std::ops::{Add, Mul, Sub}; + +/// `WIDTH` scalar field lanes with element-wise arithmetic. +/// +/// # Invariants +/// +/// - Every lane of every value is a canonical scalar; `extract` after any +/// operation equals the same operation on the extracted inputs. +/// - `from_fn`/`extract`/`broadcast` are mutually consistent: +/// `Self::from_fn(f).extract(i) == f(i)` for `i < WIDTH`. +pub trait Packed: + 'static + Copy + Send + Sync + Add + Sub + Mul +{ + /// Scalar field type of one lane. + type Scalar: Field; + + /// Number of scalar lanes. + const WIDTH: usize; + + /// Builds a packed value from a lane generator. + fn from_fn(f: impl FnMut(usize) -> Self::Scalar) -> Self; + + /// Extracts one lane. + fn extract(&self, lane: usize) -> Self::Scalar; + + /// Broadcasts one scalar across all lanes. + fn broadcast(value: Self::Scalar) -> Self; + + /// Packs a scalar slice into packed values. + /// + /// # Panics + /// + /// Panics if the length is not divisible by [`WIDTH`](Self::WIDTH). + #[inline] + fn pack_slice(buf: &[Self::Scalar]) -> Vec { + assert_eq!(buf.len() % Self::WIDTH, 0, "length not divisible by width"); + buf.chunks_exact(Self::WIDTH) + .map(|chunk| Self::from_fn(|i| chunk[i])) + .collect() + } + + /// Splits into a packed prefix and a scalar suffix shorter than `WIDTH`. + #[inline] + fn pack_slice_with_suffix(buf: &[Self::Scalar]) -> (Vec, &[Self::Scalar]) { + let (packed, suffix) = buf.split_at(buf.len() - buf.len() % Self::WIDTH); + (Self::pack_slice(packed), suffix) + } + + /// Unpacks packed values into a flat scalar vector. + #[inline] + fn unpack_slice(buf: &[Self]) -> Vec { + buf.iter() + .flat_map(|p| (0..Self::WIDTH).map(move |i| p.extract(i))) + .collect() + } + + /// Squares one packed value. + #[inline(always)] + fn square(self) -> Self { + self * self + } + + /// Lane-wise inversion; `None` if any lane is zero. + #[inline] + fn inverse(self) -> Option { + let lanes: Option> = (0..Self::WIDTH) + .map(|i| self.extract(i).inverse()) + .collect(); + lanes.map(|lanes| Self::from_fn(|i| lanes[i])) + } + + /// Kernel hook: packed quadratic-extension multiply in coefficient form + /// (Karatsuba; the non-residue fast paths come from [`Ext2Config`]). + #[inline(always)] + fn ext2_mul>( + a0: Self, + a1: Self, + b0: Self, + b1: Self, + ) -> (Self, Self) { + let v0 = a0 * b0; + let v1 = a1 * b1; + let cross = (a0 + a1) * (b0 + b1); + ( + v0 + C::mul_non_residue(v1, Self::broadcast), + cross - v0 - v1, + ) + } + + /// Kernel hook: packed degree-4 extension multiply in the `[1, e1, e2, + /// e3]` basis. + #[inline(always)] + fn ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { + crate::schedules::ext4_mul_coeffs(a, b) + } + + /// Kernel hook: packed degree-4 extension squaring. + #[inline(always)] + fn ext4_square(a: [Self; 4]) -> [Self; 4] { + crate::schedules::ext4_square_coeffs(a) + } + + /// Kernel hook: packed degree-8 extension multiply in the `[1, e1, ..., + /// e7]` basis. + #[inline(always)] + fn ext8_mul(a: [Self; 8], b: [Self; 8]) -> [Self; 8] { + let zero = Self::broadcast(Self::Scalar::zero()); + crate::schedules::ext8_mul_schedule(a, b, zero, |x, y| x + y, |x, y| x - y, |x, y| x * y) + } + + /// Kernel hook: packed degree-8 extension squaring. + #[inline(always)] + fn ext8_square(a: [Self; 8]) -> [Self; 8] { + let zero = Self::broadcast(Self::Scalar::zero()); + crate::schedules::ext8_square_schedule(a, zero, |x, y| x + y, |x, y| x - y, |x, y| x * y) + } +} + +/// Associates a packed representation with a scalar field. +pub trait WithPacking: Field { + /// Packed representation (the target's widest available backend). + type Packing: Packed; +} + +/// One-lane fallback with no SIMD path: plain scalar arithmetic per "lane". +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[repr(transparent)] +pub struct NoPacking(pub [T; 1]); + +crate::impl_ring_ops!(impl[T: Field] NoPacking { + add(a, b): NoPacking([a.0[0] + b.0[0]]), + sub(a, b): NoPacking([a.0[0] - b.0[0]]), + mul(a, b): NoPacking([a.0[0] * b.0[0]]), + neg(a): NoPacking([-a.0[0]]), + zero: NoPacking([T::zero()]), + one: NoPacking([T::one()]), +}); + +impl Packed for NoPacking { + type Scalar = T; + const WIDTH: usize = 1; + + #[inline] + fn from_fn(mut f: impl FnMut(usize) -> T) -> Self { + Self([f(0)]) + } + + #[inline] + fn extract(&self, lane: usize) -> T { + debug_assert_eq!(lane, 0); + self.0[0] + } + + #[inline] + fn broadcast(value: T) -> Self { + Self([value]) + } +} diff --git a/crates/jolt-field-two/src/solinas/mod.rs b/crates/jolt-field-two/src/solinas/mod.rs index 6157bf8c72..c6428de4ab 100644 --- a/crates/jolt-field-two/src/solinas/mod.rs +++ b/crates/jolt-field-two/src/solinas/mod.rs @@ -6,6 +6,7 @@ mod ext; mod fp128; +mod packed; mod unreduced; mod word; @@ -14,6 +15,9 @@ pub use ext::{ FpExt2, FpExt4, FpExt8, }; pub use fp128::Fp128; +pub use packed::{ + Fp128Packing, Fp32Packing, Fp64Packing, PackedFpExt2, PackedFpExt4, PackedFpExt8, +}; pub use unreduced::{ AccumPair, FoldMatrixFp32, FoldMatrixFp64, Fp128MulU64Accum, Fp128ProductAccum, Fp128x8i32, Fp32ProductAccum, Fp32x2i32, Fp64ProductAccum, Fp64x4i32, FpExt2Fp64ProductAccum, diff --git a/crates/jolt-field-two/src/solinas/packed/engine.rs b/crates/jolt-field-two/src/solinas/packed/engine.rs new file mode 100644 index 0000000000..da17118df1 --- /dev/null +++ b/crates/jolt-field-two/src/solinas/packed/engine.rs @@ -0,0 +1,366 @@ +//! Shared packed Solinas algebra for the word-sized fields, written once +//! against the [`SimdWord`] vocabulary and instantiated per ISA through the +//! marker type parameter `I` — the one source of truth for the packed +//! fold/canonicalize structure across NEON, AVX2, and AVX-512. +//! +//! [`PackedFp32`] is the u32-lane engine (widen to 64-bit products, two or +//! three Solinas folds, fused deferred-reduction dot products for the +//! degree-4 extension kernels); [`PackedFp64`] is the u64-lane engine +//! (128-bit products folded through `2^BITS ≡ C`). The fold constants are +//! taken from the scalar field types, so the `C(C+1) < P` precondition is +//! asserted in exactly one place per width (`word.rs`). + +#![cfg(any( + all(target_arch = "aarch64", target_feature = "neon"), + all(target_arch = "x86_64", target_feature = "avx2") +))] + +use super::simd::SimdWord; +use crate::solinas::{Fp32, Fp64}; +use crate::Packed; + +pub(crate) use super::fp128::PackedFp128; + +/// Stamps `Clone`/`Copy` and the operator matrix from `add_raw`/`sub_raw`/ +/// `mul_raw` inherent methods. Shared by all three packed engines. +macro_rules! impl_packed_arith { + (impl[$($g:tt)*] $ty:ty) => { + impl<$($g)*> Clone for $ty { + #[inline(always)] + fn clone(&self) -> Self { + *self + } + } + impl<$($g)*> Copy for $ty {} + impl<$($g)*> ::core::ops::Add for $ty { + type Output = Self; + #[inline(always)] + fn add(self, rhs: Self) -> Self { + Self::add_raw(self, rhs) + } + } + impl<$($g)*> ::core::ops::Sub for $ty { + type Output = Self; + #[inline(always)] + fn sub(self, rhs: Self) -> Self { + Self::sub_raw(self, rhs) + } + } + impl<$($g)*> ::core::ops::Mul for $ty { + type Output = Self; + #[inline(always)] + fn mul(self, rhs: Self) -> Self { + Self::mul_raw(self, rhs) + } + } + impl<$($g)*> ::core::ops::AddAssign for $ty { + #[inline(always)] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } + } + impl<$($g)*> ::core::ops::SubAssign for $ty { + #[inline(always)] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } + } + impl<$($g)*> ::core::ops::MulAssign for $ty { + #[inline(always)] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } + } + }; +} +pub(crate) use impl_packed_arith; + +/// `c·v` on 64-bit lanes for a compile-time-constant offset `c < 2^32`: +/// shift/add when `c = 2^a ± 1`, otherwise the ISA's small multiply. +/// Callers guarantee the exact product fits 64 bits. +#[inline(always)] +fn mul_by_offset(v: I::V64, c: u64) -> I::V64 { + if c == 1 { + v + } else if (c - 1).is_power_of_two() { + I::add64(I::shl64(v, (c - 1).trailing_zeros()), v) + } else if (c + 1).is_power_of_two() { + I::sub64(I::shl64(v, (c + 1).trailing_zeros()), v) + } else { + I::mul_small(v, c) + } +} + +/// Packed `Fp32` lanes over ISA `I` (`I::W32` lanes). +#[repr(transparent)] +pub struct PackedFp32(I::V32); + +impl PackedFp32 { + const BITS: u32 = Fp32::

::BITS; + const C: u32 = Fp32::

::C; + const MASK: u64 = if Self::BITS == 32 { + u32::MAX as u64 + } else { + (1u64 << Self::BITS) - 1 + }; + /// Whether two Solinas folds bring a sum of up to four `(P−1)²` + /// products into `[0, 2P)` for the final canonicalize step. + const TWO_FOLD_OK: bool = { + let c = Self::C as u64; + 4 * c * c + 3 * c <= (1u64 << Self::BITS) + }; + + #[inline(always)] + fn add_raw(a: Self, b: Self) -> Self { + let t = I::add32(a.0, b.0); + let t = if Self::BITS == 32 { + // The carry out of u32 is 2^32 ≡ C; fold it before canonicalizing. + I::select32(I::lt_u32(t, a.0), I::add32(t, I::splat32(Self::C)), t) + } else { + t + }; + Self(I::min_u32(t, I::sub32(t, I::splat32(P)))) + } + + #[inline(always)] + fn sub_raw(a: Self, b: Self) -> Self { + let t = I::sub32(a.0, b.0); + if Self::BITS == 32 { + // A wrap adds 2^32 ≡ C, so subtract C where a < b. + Self(I::select32( + I::lt_u32(a.0, b.0), + I::sub32(t, I::splat32(Self::C)), + t, + )) + } else { + Self(I::min_u32(t, I::add32(t, I::splat32(P)))) + } + } + + #[inline(always)] + fn mul_raw(a: Self, b: Self) -> Self { + if Self::BITS == 31 { + // ISAs with a 32-bit high-multiply reduce 31-bit primes without + // ever widening to 64-bit lanes. + if let Some(r) = I::mul_pm31(a.0, b.0, P, Self::C) { + return Self(r); + } + } + Self(Self::reduce(I::widen_mul(a.0, b.0))) + } + + /// One Solinas fold: `(v & MASK) + C·(v >> BITS)`. + #[inline(always)] + fn fold(v: I::V64) -> I::V64 { + I::add64( + I::and64(v, I::splat64(Self::MASK)), + mul_by_offset::(I::shr64(v, Self::BITS), Self::C as u64), + ) + } + + /// Two/three-fold reduction of widened products (or sums of up to four + /// products, pre-folded when `BITS == 32`) to canonical 32-bit lanes. + #[inline(always)] + fn reduce(x: [I::V64; 2]) -> I::V32 { + let f = x.map(|v| Self::fold(Self::fold(v))); + let f = if Self::TWO_FOLD_OK { + f + } else { + f.map(Self::fold) + }; + if Self::BITS == 32 { + // The two-fold residue can exceed 2^32 (up to 2^32 + C²), so + // canonicalize on the 64-bit lanes before packing. + let p = I::splat64(u64::from(P)); + I::narrow_pack(f.map(|v| I::select64(I::lt_u64(v, p), v, I::sub64(v, p)))) + } else { + let packed = I::narrow_pack(f); + I::min_u32(packed, I::sub32(packed, I::splat32(P))) + } + } + + /// `Σ a_i·b_i` with one deferred end-reduction (`N ≤ 4`). For + /// `BITS ≤ 31` the raw 64-bit products sum without overflow; for + /// `BITS == 32` each product is pre-folded once (`< (C+1)·2^32`) so four + /// terms still sum carry-free. + #[inline(always)] + fn dot(a: [Self; N], b: [Self; N]) -> Self { + let term = |i: usize| { + let p = I::widen_mul(a[i].0, b[i].0); + if Self::BITS == 32 { + p.map(Self::fold) + } else { + p + } + }; + let mut sums = term(0); + for i in 1..N { + let t = term(i); + sums = [I::add64(sums[0], t[0]), I::add64(sums[1], t[1])]; + } + Self(Self::reduce(sums)) + } +} + +impl_packed_arith!(impl[const P: u32, I: SimdWord] PackedFp32); + +impl Packed for PackedFp32 { + type Scalar = Fp32

; + const WIDTH: usize = I::W32; + + #[inline] + fn from_fn(mut f: impl FnMut(usize) -> Fp32

) -> Self { + Self(I::v32_from_fn(|i| f(i).0)) + } + + #[inline] + fn extract(&self, lane: usize) -> Fp32

{ + debug_assert!(lane < I::W32); + Fp32(I::v32_lane(self.0, lane)) + } + + #[inline] + fn broadcast(value: Fp32

) -> Self { + Self(I::splat32(value.0)) + } + + /// Fused kernel: each output coefficient is one deferred-reduction dot + /// product instead of six independently reduced multiplies. + #[inline(always)] + fn ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { + let [b0, b1, b2, b3] = b; + [ + Self::dot(a, [b0, b1 + b1, b2 + b2, b3 + b3]), + Self::dot(a, [b1, b0 + b2, b1 + b3, b2]), + Self::dot(a, [b2, b1 + b3, b0, b1 - b3]), + Self::dot(a, [b3, b2, b1 - b3, b0 - b2]), + ] + } + + /// Fused kernel: squaring via three- and four-term dot products. + #[inline(always)] + fn ext4_square(a: [Self; 4]) -> [Self; 4] { + let [a0, a1, a2, a3] = a; + let zero = Self::broadcast(Fp32(0)); + [ + Self::dot(a, [a0, a1 + a1, a2 + a2, a3 + a3]), + Self::dot([a0, a1, a2], [a1 + a1, a2 + a2, a3 + a3]), + Self::dot([a0, a1, a1, a3], [a2 + a2, a1, a3 + a3, zero - a3]), + Self::dot([a0, a1, a2], [a3 + a3, a2 + a2, zero - (a3 + a3)]), + ] + } +} + +/// Packed `Fp64` lanes over ISA `I` (`I::W64` lanes). +#[repr(transparent)] +pub struct PackedFp64(I::V64); + +impl PackedFp64 { + const BITS: u32 = Fp64::

::BITS; + // The scalar invariant C(C+1) < P < 2^64 implies C < 2^32, which the + // `mul_small`-based reduction below relies on. + const C: u64 = Fp64::

::C; + const MASK: u64 = if Self::BITS == 64 { + u64::MAX + } else { + (1u64 << Self::BITS) - 1 + }; + + #[inline(always)] + fn add_raw(a: Self, b: Self) -> Self { + let p = I::splat64(P); + let s = I::add64(a.0, b.0); + if Self::BITS <= 62 { + // a + b < 2P < 2^63: no wrap possible. + Self(I::select64(I::lt_u64(s, p), s, I::sub64(s, p))) + } else { + let no_wrap = I::select64(I::lt_u64(s, p), s, I::sub64(s, p)); + // On wrap the true sum is s + 2^64 ≡ s + C, already canonical + // (s < 2P − 2^64 = P − C). + Self(I::select64( + I::lt_u64(s, a.0), + I::add64(s, I::splat64(Self::C)), + no_wrap, + )) + } + } + + #[inline(always)] + fn sub_raw(a: Self, b: Self) -> Self { + let d = I::sub64(a.0, b.0); + // On wrap, +P ≡ −C (mod 2^64) restores the canonical value for any + // BITS, including 64. + Self(I::select64( + I::lt_u64(a.0, b.0), + I::add64(d, I::splat64(P)), + d, + )) + } + + #[inline(always)] + fn mul_raw(a: Self, b: Self) -> Self { + if I::FP64_MUL_BY_LANES { + Self(I::v64_from_fn(|i| { + (Fp64::

(I::v64_lane(a.0, i)) * Fp64::

(I::v64_lane(b.0, i))).0 + })) + } else { + let [lo, hi] = I::mul64_wide(a.0, b.0); + Self(Self::reduce128(lo, hi)) + } + } + + /// Solinas reduction of per-lane 128-bit products `hi·2^64 + lo`. + #[inline(always)] + fn reduce128(lo: I::V64, hi: I::V64) -> I::V64 { + let p = I::splat64(P); + if Self::BITS < 64 { + // Both folds stay in u64 whenever C < 2^(64−BITS) — true for + // every registered sub-64-bit prime (the scalar field switches + // to its u128 path in exactly the same regime). + debug_assert!(u128::from(Self::C) < 1u128 << (64 - Self::BITS)); + let mask = I::splat64(Self::MASK); + let hi_k = I::add64(I::shr64(lo, Self::BITS), I::shl64(hi, 64 - Self::BITS)); + let f1 = I::add64(I::and64(lo, mask), mul_by_offset::(hi_k, Self::C)); + let f2 = I::add64( + I::and64(f1, mask), + mul_by_offset::(I::shr64(f1, Self::BITS), Self::C), + ); + I::select64(I::lt_u64(f2, p), f2, I::sub64(f2, p)) + } else { + // BITS == 64: hi·2^64 ≡ C·hi, a 96-bit product. Fold its carry + // limb once more; the final +C correction cannot cascade + // (r < C·2^32 after a wrap) and one subtract canonicalizes + // (everything is < 2^64 = P + C < 2P). + let [cl, ch] = I::mul_small_wide(hi, Self::C); + let s = I::add64(lo, cl); + let k1 = I::select64(I::lt_u64(s, lo), I::splat64(1), I::splat64(0)); + let m = I::mul_small(I::add64(ch, k1), Self::C); + let r = I::add64(s, m); + let r = I::select64(I::lt_u64(r, s), I::add64(r, I::splat64(Self::C)), r); + I::select64(I::lt_u64(r, p), r, I::sub64(r, p)) + } + } +} + +impl_packed_arith!(impl[const P: u64, I: SimdWord] PackedFp64); + +impl Packed for PackedFp64 { + type Scalar = Fp64

; + const WIDTH: usize = I::W64; + + #[inline] + fn from_fn(mut f: impl FnMut(usize) -> Fp64

) -> Self { + Self(I::v64_from_fn(|i| f(i).0)) + } + + #[inline] + fn extract(&self, lane: usize) -> Fp64

{ + debug_assert!(lane < I::W64); + Fp64(I::v64_lane(self.0, lane)) + } + + #[inline] + fn broadcast(value: Fp64

) -> Self { + Self(I::splat64(value.0)) + } +} diff --git a/crates/jolt-field-two/src/solinas/packed/ext.rs b/crates/jolt-field-two/src/solinas/packed/ext.rs new file mode 100644 index 0000000000..c0d6354b94 --- /dev/null +++ b/crates/jolt-field-two/src/solinas/packed/ext.rs @@ -0,0 +1,244 @@ +//! Packed extension towers in transpose layout: coefficient `j` of every +//! lane lives in packed base vector `j`, so `WIDTH` extension values +//! multiply in parallel through the [`Packed`] kernel hooks — which consume +//! the shared coefficient schedules (`crate::schedules`) unless a SIMD +//! engine overrides them with fused kernels. +//! +//! Inversion is lane-wise scalar throughout (the [`Packed::inverse`] +//! default): the base-field Fermat inversion is lane-serial in any +//! formulation and dominates the cost. + +use crate::solinas::{FpExt2, FpExt4, FpExt8}; +use crate::{Ext2Config, Field, Packed, PseudoMersenne, WithPacking}; +use std::marker::PhantomData; +use std::ops::{Add, Mul, Sub}; + +/// Packed [`FpExt2`]: `WIDTH` quadratic-extension lanes as two packed +/// coefficient vectors. +pub struct PackedFpExt2> { + /// Packed degree-0 coefficients. + pub c0: PF, + /// Packed degree-1 coefficients. + pub c1: PF, + _cfg: PhantomData C>, +} + +impl> PackedFpExt2 { + /// Constructs from packed coefficient vectors. + #[inline] + pub fn new(c0: PF, c1: PF) -> Self { + Self { + c0, + c1, + _cfg: PhantomData, + } + } +} + +// Manual std impls: derives would demand `C: Clone` etc. on the config ZST. +impl> Clone for PackedFpExt2 { + #[inline] + fn clone(&self) -> Self { + *self + } +} +impl> Copy for PackedFpExt2 {} + +impl> Add for PackedFpExt2 { + type Output = Self; + #[inline(always)] + fn add(self, rhs: Self) -> Self { + Self::new(self.c0 + rhs.c0, self.c1 + rhs.c1) + } +} +impl> Sub for PackedFpExt2 { + type Output = Self; + #[inline(always)] + fn sub(self, rhs: Self) -> Self { + Self::new(self.c0 - rhs.c0, self.c1 - rhs.c1) + } +} +impl> Mul for PackedFpExt2 { + type Output = Self; + #[inline(always)] + fn mul(self, rhs: Self) -> Self { + let (c0, c1) = PF::ext2_mul::(self.c0, self.c1, rhs.c0, rhs.c1); + Self::new(c0, c1) + } +} + +impl + 'static> Packed for PackedFpExt2 { + type Scalar = FpExt2; + const WIDTH: usize = PF::WIDTH; + + fn from_fn(f: impl FnMut(usize) -> Self::Scalar) -> Self { + let vals: Vec = (0..PF::WIDTH).map(f).collect(); + Self::new( + PF::from_fn(|i| vals[i].coeffs[0]), + PF::from_fn(|i| vals[i].coeffs[1]), + ) + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Scalar { + FpExt2::new(self.c0.extract(lane), self.c1.extract(lane)) + } + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self::new( + PF::broadcast(value.coeffs[0]), + PF::broadcast(value.coeffs[1]), + ) + } +} + +impl WithPacking for FpExt2 +where + F: Field + WithPacking, + C: Ext2Config + 'static, +{ + type Packing = PackedFpExt2; +} + +/// Packed [`FpExt4`]: `WIDTH` quartic lanes as four packed coefficient +/// vectors in the `[1, e1, e2, e3]` basis. +#[derive(Clone, Copy)] +pub struct PackedFpExt4 { + /// Packed coefficients in basis order. + pub coeffs: [PF; 4], +} + +impl PackedFpExt4 { + /// Constructs from packed coefficient vectors. + #[inline] + pub fn new(coeffs: [PF; 4]) -> Self { + Self { coeffs } + } +} + +impl Add for PackedFpExt4 { + type Output = Self; + #[inline(always)] + fn add(self, rhs: Self) -> Self { + Self::new(std::array::from_fn(|i| self.coeffs[i] + rhs.coeffs[i])) + } +} +impl Sub for PackedFpExt4 { + type Output = Self; + #[inline(always)] + fn sub(self, rhs: Self) -> Self { + Self::new(std::array::from_fn(|i| self.coeffs[i] - rhs.coeffs[i])) + } +} +impl Mul for PackedFpExt4 { + type Output = Self; + #[inline(always)] + fn mul(self, rhs: Self) -> Self { + Self::new(PF::ext4_mul(self.coeffs, rhs.coeffs)) + } +} + +impl Packed for PackedFpExt4 +where + PF::Scalar: PseudoMersenne, +{ + type Scalar = FpExt4; + const WIDTH: usize = PF::WIDTH; + + fn from_fn(f: impl FnMut(usize) -> Self::Scalar) -> Self { + let vals: Vec = (0..PF::WIDTH).map(f).collect(); + Self::new(std::array::from_fn(|j| PF::from_fn(|i| vals[i].coeffs[j]))) + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Scalar { + FpExt4::new(std::array::from_fn(|j| self.coeffs[j].extract(lane))) + } + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self::new(value.coeffs.map(PF::broadcast)) + } + + /// Squaring via the dedicated kernel hook (fewer base multiplies). + #[inline(always)] + fn square(self) -> Self { + Self::new(PF::ext4_square(self.coeffs)) + } +} + +impl WithPacking for FpExt4 { + type Packing = PackedFpExt4; +} + +/// Packed [`FpExt8`]: `WIDTH` octic lanes as eight packed coefficient +/// vectors in the `[1, e1, ..., e7]` basis. +#[derive(Clone, Copy)] +pub struct PackedFpExt8 { + /// Packed coefficients in basis order. + pub coeffs: [PF; 8], +} + +impl PackedFpExt8 { + /// Constructs from packed coefficient vectors. + #[inline] + pub fn new(coeffs: [PF; 8]) -> Self { + Self { coeffs } + } +} + +impl Add for PackedFpExt8 { + type Output = Self; + #[inline(always)] + fn add(self, rhs: Self) -> Self { + Self::new(std::array::from_fn(|i| self.coeffs[i] + rhs.coeffs[i])) + } +} +impl Sub for PackedFpExt8 { + type Output = Self; + #[inline(always)] + fn sub(self, rhs: Self) -> Self { + Self::new(std::array::from_fn(|i| self.coeffs[i] - rhs.coeffs[i])) + } +} +impl Mul for PackedFpExt8 { + type Output = Self; + #[inline(always)] + fn mul(self, rhs: Self) -> Self { + Self::new(PF::ext8_mul(self.coeffs, rhs.coeffs)) + } +} + +impl Packed for PackedFpExt8 +where + PF::Scalar: PseudoMersenne, +{ + type Scalar = FpExt8; + const WIDTH: usize = PF::WIDTH; + + fn from_fn(f: impl FnMut(usize) -> Self::Scalar) -> Self { + let vals: Vec = (0..PF::WIDTH).map(f).collect(); + Self::new(std::array::from_fn(|j| PF::from_fn(|i| vals[i].coeffs[j]))) + } + + #[inline] + fn extract(&self, lane: usize) -> Self::Scalar { + FpExt8::new(std::array::from_fn(|j| self.coeffs[j].extract(lane))) + } + + #[inline] + fn broadcast(value: Self::Scalar) -> Self { + Self::new(value.coeffs.map(PF::broadcast)) + } + + /// Squaring via the dedicated kernel hook. + #[inline(always)] + fn square(self) -> Self { + Self::new(PF::ext8_square(self.coeffs)) + } +} + +impl WithPacking for FpExt8 { + type Packing = PackedFpExt8; +} diff --git a/crates/jolt-field-two/src/solinas/packed/fp128.rs b/crates/jolt-field-two/src/solinas/packed/fp128.rs new file mode 100644 index 0000000000..cdc2012442 --- /dev/null +++ b/crates/jolt-field-two/src/solinas/packed/fp128.rs @@ -0,0 +1,129 @@ +//! Packed two-limb field: `I::W64` [`Fp128`] lanes in SoA layout +//! (`lo`/`hi` limb vectors), shared across ISAs like the word engines. +//! +//! Add/sub vectorize the 128-bit carry chains with fused reduction; +//! multiplication goes lane-by-lane through the scalar kernel (which is the +//! AArch64 inline-asm multiply on that target) — no ISA in the baseline had +//! a vectorized 128-bit multiply either. + +#![cfg(any( + all(target_arch = "aarch64", target_feature = "neon"), + all(target_arch = "x86_64", target_feature = "avx2") +))] + +use super::engine::impl_packed_arith; +use super::simd::SimdWord; +use crate::solinas::Fp128; +use crate::Packed; + +/// Packed `Fp128` lanes over ISA `I`: `lo[i]`/`hi[i]` are lane `i`'s limbs. +pub struct PackedFp128 { + lo: I::V64, + hi: I::V64, +} + +impl PackedFp128 { + const C_LO: u64 = Fp128::

::C_LO; + const P_LO: u64 = P as u64; + const P_HI: u64 = (P >> 64) as u64; + + /// Carry-chain add with fused reduction (see the scalar + /// [`Fp128`] `add_raw` for the case analysis): compute `s = a + b` + /// tracking the 128-bit wrap, then `t = s + C ≡ s − p (mod 2^128)` and + /// select `t` where the sum wrapped or `t` carried (`s ≥ p`). + #[inline(always)] + fn add_raw(a: Self, b: Self) -> Self { + let s_lo = I::add64(a.lo, b.lo); + let carry_lo = I::lt_u64(s_lo, a.lo); + let h1 = I::add64(a.hi, b.hi); + let wrap1 = I::lt_u64(h1, a.hi); + // Subtracting an all-ones carry mask adds one. + let s_hi = I::sub64(h1, carry_lo); + let wrap2 = I::lt_u64(s_hi, h1); + let wrapped = I::or64(wrap1, wrap2); + + let t_lo = I::add64(s_lo, I::splat64(Self::C_LO)); + let carry_c = I::lt_u64(t_lo, s_lo); + let t_hi = I::sub64(s_hi, carry_c); + let carried = I::lt_u64(t_hi, s_hi); + + let use_t = I::or64(wrapped, carried); + Self { + lo: I::select64(use_t, t_lo, s_lo), + hi: I::select64(use_t, t_hi, s_hi), + } + } + + /// Subtract with borrow-conditional modulus add-back. + #[inline(always)] + fn sub_raw(a: Self, b: Self) -> Self { + let one = I::splat64(1); + let d_lo = I::sub64(a.lo, b.lo); + let borrow_lo = I::and64(I::lt_u64(a.lo, b.lo), one); + let h1 = I::sub64(a.hi, b.hi); + let bw1 = I::lt_u64(a.hi, b.hi); + let d_hi = I::sub64(h1, borrow_lo); + let bw2 = I::lt_u64(h1, borrow_lo); + let borrowed = I::or64(bw1, bw2); + + let corr_lo = I::add64(d_lo, I::splat64(Self::P_LO)); + let carry = I::and64(I::lt_u64(corr_lo, d_lo), one); + let corr_hi = I::add64(I::add64(d_hi, I::splat64(Self::P_HI)), carry); + Self { + lo: I::select64(borrowed, corr_lo, d_lo), + hi: I::select64(borrowed, corr_hi, d_hi), + } + } + + /// Lane-by-lane scalar multiply (inline-asm kernel on AArch64). + #[inline(always)] + fn mul_raw(a: Self, b: Self) -> Self { + let mut lo = [0u64; 8]; + let mut hi = [0u64; 8]; + for (i, (l, h)) in lo.iter_mut().zip(hi.iter_mut()).enumerate().take(I::W64) { + let x = Fp128::

([I::v64_lane(a.lo, i), I::v64_lane(a.hi, i)]); + let y = Fp128::

([I::v64_lane(b.lo, i), I::v64_lane(b.hi, i)]); + let r = (x * y).0; + (*l, *h) = (r[0], r[1]); + } + Self { + lo: I::v64_from_fn(|i| lo[i]), + hi: I::v64_from_fn(|i| hi[i]), + } + } +} + +impl_packed_arith!(impl[const P: u128, I: SimdWord] PackedFp128); + +impl Packed for PackedFp128 { + type Scalar = Fp128

; + const WIDTH: usize = I::W64; + + #[inline] + fn from_fn(mut f: impl FnMut(usize) -> Fp128

) -> Self { + let mut lo = [0u64; 8]; + let mut hi = [0u64; 8]; + for (i, (l, h)) in lo.iter_mut().zip(hi.iter_mut()).enumerate().take(I::W64) { + let v = f(i).0; + (*l, *h) = (v[0], v[1]); + } + Self { + lo: I::v64_from_fn(|i| lo[i]), + hi: I::v64_from_fn(|i| hi[i]), + } + } + + #[inline] + fn extract(&self, lane: usize) -> Fp128

{ + debug_assert!(lane < I::W64); + Fp128([I::v64_lane(self.lo, lane), I::v64_lane(self.hi, lane)]) + } + + #[inline] + fn broadcast(value: Fp128

) -> Self { + Self { + lo: I::splat64(value.0[0]), + hi: I::splat64(value.0[1]), + } + } +} diff --git a/crates/jolt-field-two/src/solinas/packed/mod.rs b/crates/jolt-field-two/src/solinas/packed/mod.rs new file mode 100644 index 0000000000..aabc33c3f1 --- /dev/null +++ b/crates/jolt-field-two/src/solinas/packed/mod.rs @@ -0,0 +1,59 @@ +//! Packed SIMD backend selection for the Solinas fields. +//! +//! `simd.rs` holds the per-ISA primitive vocabularies, `engine.rs` and +//! `fp128.rs` the shared packed algebra stamped per (width × ISA), and +//! `ext.rs` the packed extension towers. This module picks the widest +//! backend the compilation target supports, falling back to the one-lane +//! [`NoPacking`]. + +mod engine; +mod ext; +mod fp128; +mod simd; + +pub use ext::{PackedFpExt2, PackedFpExt4, PackedFpExt8}; + +use crate::solinas::{Fp128, Fp32, Fp64}; +use crate::WithPacking; + +/// Selects the packed backend for a Solinas prime at compile time: +/// NEON on aarch64, AVX-512 then AVX2 on x86-64, scalar [`NoPacking`] +/// otherwise. +macro_rules! select_packing { + ($alias:ident<$p:ident: $ty:ty>, $scalar:ident, $engine:ident) => { + /// Selected packed backend for this prime width. + #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] + pub type $alias = engine::$engine<$p, simd::Neon>; + + /// Selected packed backend for this prime width. + #[cfg(all( + target_arch = "x86_64", + target_feature = "avx512f", + target_feature = "avx512dq" + ))] + pub type $alias = engine::$engine<$p, simd::Avx512>; + + /// Selected packed backend for this prime width. + #[cfg(all( + target_arch = "x86_64", + target_feature = "avx2", + not(all(target_feature = "avx512f", target_feature = "avx512dq")) + ))] + pub type $alias = engine::$engine<$p, simd::Avx2>; + + /// Selected packed backend for this prime width. + #[cfg(not(any( + all(target_arch = "aarch64", target_feature = "neon"), + all(target_arch = "x86_64", target_feature = "avx2") + )))] + pub type $alias = crate::NoPacking<$scalar<$p>>; + + impl WithPacking for $scalar<$p> { + type Packing = $alias<$p>; + } + }; +} + +select_packing!(Fp32Packing, Fp32, PackedFp32); +select_packing!(Fp64Packing, Fp64, PackedFp64); +select_packing!(Fp128Packing, Fp128, PackedFp128); diff --git a/crates/jolt-field-two/src/solinas/packed/simd.rs b/crates/jolt-field-two/src/solinas/packed/simd.rs new file mode 100644 index 0000000000..88f307ff34 --- /dev/null +++ b/crates/jolt-field-two/src/solinas/packed/simd.rs @@ -0,0 +1,455 @@ +//! Per-ISA SIMD primitive vocabularies: [`SimdWord`] is the instruction-set +//! contract the shared packed algebra (`engine.rs`, `fp128.rs`) is written +//! against, implemented by the [`Neon`], [`Avx2`], and [`Avx512`] markers. +//! +//! Only *algorithmic* per-ISA differences live here — e.g. AVX2 has no +//! 64-bit widening multiply (emulated from 32×32→64 partial products), +//! AVX-512 comparisons produce mask registers (converted to lane masks), +//! and only NEON has a 32-bit high-multiply (`mul_pm31`). Comparison +//! results are all-ones lane masks on every ISA. + +#![cfg(any( + all(target_arch = "aarch64", target_feature = "neon"), + all(target_arch = "x86_64", target_feature = "avx2") +))] + +/// One SIMD instruction set's primitive vocabulary over vectors of `u32` +/// and `u64` lanes. +/// +/// # Invariants +/// +/// - `lt_*` return all-ones lane masks (unsigned compare); `select*` takes +/// such a mask and picks `t`/`f` lane-wise. +/// - `mul_small`/`mul_small_wide` require `c < 2^32`. +/// - Shift amounts satisfy `0 < k < 64`. +/// - `narrow_pack` is the layout inverse of `widen_mul`: packing the +/// (reduced) halves restores the original lane order. +pub trait SimdWord: 'static { + /// Vector of [`W32`](Self::W32) `u32` lanes. + type V32: Copy + Send + Sync; + /// Vector of [`W64`](Self::W64) `u64` lanes. + type V64: Copy + Send + Sync; + /// `u32` lanes per vector. + const W32: usize; + /// `u64` lanes per vector. + const W64: usize; + /// Whether packed `Fp64` multiplication should go lane-by-lane through + /// the scalar kernel (no efficient 64×64 vector multiply on this ISA). + const FP64_MUL_BY_LANES: bool; + + fn v32_from_fn(f: impl FnMut(usize) -> u32) -> Self::V32; + fn v32_lane(v: Self::V32, lane: usize) -> u32; + fn v64_from_fn(f: impl FnMut(usize) -> u64) -> Self::V64; + fn v64_lane(v: Self::V64, lane: usize) -> u64; + fn splat32(x: u32) -> Self::V32; + fn splat64(x: u64) -> Self::V64; + fn add32(a: Self::V32, b: Self::V32) -> Self::V32; + fn sub32(a: Self::V32, b: Self::V32) -> Self::V32; + fn min_u32(a: Self::V32, b: Self::V32) -> Self::V32; + fn lt_u32(a: Self::V32, b: Self::V32) -> Self::V32; + fn select32(m: Self::V32, t: Self::V32, f: Self::V32) -> Self::V32; + fn add64(a: Self::V64, b: Self::V64) -> Self::V64; + fn sub64(a: Self::V64, b: Self::V64) -> Self::V64; + fn and64(a: Self::V64, b: Self::V64) -> Self::V64; + fn or64(a: Self::V64, b: Self::V64) -> Self::V64; + fn shr64(v: Self::V64, k: u32) -> Self::V64; + fn shl64(v: Self::V64, k: u32) -> Self::V64; + fn lt_u64(a: Self::V64, b: Self::V64) -> Self::V64; + fn select64(m: Self::V64, t: Self::V64, f: Self::V64) -> Self::V64; + /// Low 64 bits of `v * c` per lane. + fn mul_small(v: Self::V64, c: u64) -> Self::V64; + /// `[lo, hi]` of the full `v * c` per lane. + fn mul_small_wide(v: Self::V64, c: u64) -> [Self::V64; 2]; + /// `[lo, hi]` of the full 128-bit `a * b` per lane. + fn mul64_wide(a: Self::V64, b: Self::V64) -> [Self::V64; 2]; + /// 32×32→64 widening multiply of all `u32` lanes; the two half-vectors + /// are in an ISA-specific layout (see `narrow_pack`). + fn widen_mul(a: Self::V32, b: Self::V32) -> [Self::V64; 2]; + /// Low 32 bits of each 64-bit lane, reassembled in original lane order. + fn narrow_pack(x: [Self::V64; 2]) -> Self::V32; + + /// Multiply for 31-bit pseudo-Mersenne primes `p = 2^31 − c` entirely in + /// 32-bit lanes, when the ISA has a 32-bit high-multiply. Inputs must be + /// canonical and `p` must be exactly 31 bits with `c(c+1) < p`. + #[inline(always)] + fn mul_pm31(_a: Self::V32, _b: Self::V32, _p: u32, _c: u32) -> Option { + None + } +} + +/// Stamps vocabulary methods whose body is a single (possibly block) +/// intrinsic expression, wrapped in the requisite `unsafe` block. +macro_rules! fwd { + ($($name:ident($($arg:ident: $ty:ty),*) -> $ret:ty = $body:expr;)*) => { + $( + #[inline(always)] + fn $name($($arg: $ty),*) -> $ret { + unsafe { $body } + } + )* + }; +} + +#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] +pub use neon::Neon; + +#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] +#[expect( + clippy::undocumented_unsafe_blocks, + reason = "register-only NEON intrinsics over plain integer lanes; the per-fn contracts are documented on the SimdWord trait" +)] +mod neon { + use super::SimdWord; + use core::arch::aarch64::{ + uint32x4_t, uint64x2_t, vaddq_u32, vaddq_u64, vandq_u32, vandq_u64, vbslq_u32, vbslq_u64, + vcltq_u32, vcltq_u64, vcombine_u32, vdup_n_u32, vdupq_n_s64, vdupq_n_u32, vdupq_n_u64, + vget_low_u32, vminq_u32, vmovn_u64, vmull_high_u32, vmull_u32, vmulq_u32, vorrq_u64, + vqdmulhq_s32, vreinterpretq_s32_u32, vreinterpretq_u32_s32, vshlq_n_u64, vshlq_u64, + vshrq_n_u32, vshrq_n_u64, vsubq_u32, vsubq_u64, + }; + use core::mem::transmute; + + /// AArch64 NEON: 128-bit vectors (4 × u32, 2 × u64). + pub enum Neon {} + + impl SimdWord for Neon { + type V32 = uint32x4_t; + type V64 = uint64x2_t; + const W32: usize = 4; + const W64: usize = 2; + // No 64×64 vector multiply: per-lane scalar folds win at width 2. + const FP64_MUL_BY_LANES: bool = true; + + fwd! { + v32_from_fn(f: impl FnMut(usize) -> u32) -> uint32x4_t = + transmute::<[u32; 4], uint32x4_t>(std::array::from_fn(f)); + v32_lane(v: uint32x4_t, lane: usize) -> u32 = + transmute::(v)[lane]; + v64_from_fn(f: impl FnMut(usize) -> u64) -> uint64x2_t = + transmute::<[u64; 2], uint64x2_t>(std::array::from_fn(f)); + v64_lane(v: uint64x2_t, lane: usize) -> u64 = + transmute::(v)[lane]; + splat32(x: u32) -> uint32x4_t = vdupq_n_u32(x); + splat64(x: u64) -> uint64x2_t = vdupq_n_u64(x); + add32(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t = vaddq_u32(a, b); + sub32(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t = vsubq_u32(a, b); + min_u32(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t = vminq_u32(a, b); + lt_u32(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t = vcltq_u32(a, b); + select32(m: uint32x4_t, t: uint32x4_t, f: uint32x4_t) -> uint32x4_t = + vbslq_u32(m, t, f); + add64(a: uint64x2_t, b: uint64x2_t) -> uint64x2_t = vaddq_u64(a, b); + sub64(a: uint64x2_t, b: uint64x2_t) -> uint64x2_t = vsubq_u64(a, b); + and64(a: uint64x2_t, b: uint64x2_t) -> uint64x2_t = vandq_u64(a, b); + or64(a: uint64x2_t, b: uint64x2_t) -> uint64x2_t = vorrq_u64(a, b); + shr64(v: uint64x2_t, k: u32) -> uint64x2_t = vshlq_u64(v, vdupq_n_s64(-i64::from(k))); + shl64(v: uint64x2_t, k: u32) -> uint64x2_t = vshlq_u64(v, vdupq_n_s64(i64::from(k))); + lt_u64(a: uint64x2_t, b: uint64x2_t) -> uint64x2_t = vcltq_u64(a, b); + select64(m: uint64x2_t, t: uint64x2_t, f: uint64x2_t) -> uint64x2_t = + vbslq_u64(m, t, f); + // v*c = (lo32(v)·c) + ((hi32(v)·c) << 32): two vmull widening muls. + mul_small(v: uint64x2_t, c: u64) -> uint64x2_t = { + let c32 = vdup_n_u32(c as u32); + let lo = vmull_u32(vmovn_u64(v), c32); + let hi = vmull_u32(vmovn_u64(vshrq_n_u64::<32>(v)), c32); + vaddq_u64(lo, vshlq_n_u64::<32>(hi)) + }; + // Cold on NEON (only the vectorized fp64 reduce uses it). + mul_small_wide(v: uint64x2_t, c: u64) -> [uint64x2_t; 2] = { + let p = + transmute::(v).map(|x| u128::from(x) * u128::from(c)); + [ + Self::v64_from_fn(|i| p[i] as u64), + Self::v64_from_fn(|i| (p[i] >> 64) as u64), + ] + }; + // Cold on NEON: packed fp64 multiplies go lane-by-lane instead. + mul64_wide(a: uint64x2_t, b: uint64x2_t) -> [uint64x2_t; 2] = { + let x = transmute::(a); + let y = transmute::(b); + let p: [u128; 2] = std::array::from_fn(|i| u128::from(x[i]) * u128::from(y[i])); + [ + Self::v64_from_fn(|i| p[i] as u64), + Self::v64_from_fn(|i| (p[i] >> 64) as u64), + ] + }; + widen_mul(a: uint32x4_t, b: uint32x4_t) -> [uint64x2_t; 2] = + [vmull_u32(vget_low_u32(a), vget_low_u32(b)), vmull_high_u32(a, b)]; + narrow_pack(x: [uint64x2_t; 2]) -> uint32x4_t = + vcombine_u32(vmovn_u64(x[0]), vmovn_u64(x[1])); + } + + /// Packed multiply for 31-bit pseudo-Mersenne primes `p = 2^31 − c`, + /// reducing entirely in 32-bit lanes via two `vqdmulhq_s32` + /// high-multiplies (no 64-bit widening). + /// + /// # Correctness (exact, no estimation) + /// + /// Precondition: lanes `a, b ∈ [0, p)`; `c(c+1) < p = 2^31 − c` is + /// equivalent to `c(c+2) < 2^31` and gives `c² < p`. Write + /// `z = a·b < 2^62`. + /// + /// 1. `h = sqdmulh(a,b) = ⌊2z/2^32⌋ = ⌊z/2^31⌋` (exact, `2z < 2^63` + /// so no saturation), `z_lo31 = z mod 2^31`, so `z = h·2^31 + z_lo31`. + /// 2. `2^31 ≡ c (mod p)` ⇒ `z ≡ t = c·h + z_lo31`. + /// 3. `hh = ⌊c·h/2^31⌋ < c` and `ch_lo31 = c·h mod 2^31`, so + /// `s = ch_lo31 + z_lo31 < 2^32` (no u32 overflow); with + /// `hp = hh + (s ≫ 31) ≤ c` and `lo31p = s mod 2^31`, + /// `t = hp·2^31 + lo31p`. + /// 4. `t ≡ t' = c·hp + lo31p` with `c·hp ≤ c² < 2^31` (exact 32-bit + /// multiply) and `t' < c² + 2^31 < 2p` (from `c(c+2) < 2^31`), so + /// one min-subtract canonicalizes. + #[inline(always)] + fn mul_pm31(a: uint32x4_t, b: uint32x4_t, p: u32, c: u32) -> Option { + unsafe { + let mask31 = vdupq_n_u32((1u32 << 31) - 1); + let cvec = vdupq_n_u32(c); + let pvec = vdupq_n_u32(p); + let h = vreinterpretq_u32_s32(vqdmulhq_s32( + vreinterpretq_s32_u32(a), + vreinterpretq_s32_u32(b), + )); + let z_lo31 = vandq_u32(vmulq_u32(a, b), mask31); + let hh = vreinterpretq_u32_s32(vqdmulhq_s32( + vreinterpretq_s32_u32(h), + vreinterpretq_s32_u32(cvec), + )); + let ch_lo31 = vandq_u32(vmulq_u32(h, cvec), mask31); + let s = vaddq_u32(ch_lo31, z_lo31); + let hp = vaddq_u32(hh, vshrq_n_u32::<31>(s)); + let lo31p = vandq_u32(s, mask31); + let tprime = vaddq_u32(vmulq_u32(hp, cvec), lo31p); + Some(vminq_u32(tprime, vsubq_u32(tprime, pvec))) + } + } + } +} + +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx2", + not(all(target_feature = "avx512f", target_feature = "avx512dq")) +))] +pub use avx2::Avx2; + +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx2", + not(all(target_feature = "avx512f", target_feature = "avx512dq")) +))] +#[expect( + clippy::undocumented_unsafe_blocks, + reason = "register-only AVX2 intrinsics over plain integer lanes; the per-fn contracts are documented on the SimdWord trait" +)] +mod avx2 { + use super::SimdWord; + use core::arch::x86_64::*; + use core::mem::transmute; + + /// x86-64 AVX2: 256-bit vectors (8 × u32, 4 × u64). No native unsigned + /// compares (sign-bit-XOR trick) and no 64-bit widening multiply + /// (assembled from `_mm256_mul_epu32` 32×32→64 partial products). + pub enum Avx2 {} + + /// Duplicate the high 32 bits of each 64-bit lane into the low 32 bits. + /// The float `movehdup` runs on port 5, off the multiply ports. + #[inline(always)] + unsafe fn movehdup_epi32(x: __m256i) -> __m256i { + unsafe { _mm256_castps_si256(_mm256_movehdup_ps(_mm256_castsi256_ps(x))) } + } + + #[inline(always)] + unsafe fn moveldup_epi32(x: __m256i) -> __m256i { + unsafe { _mm256_castps_si256(_mm256_moveldup_ps(_mm256_castsi256_ps(x))) } + } + + impl SimdWord for Avx2 { + type V32 = __m256i; + type V64 = __m256i; + const W32: usize = 8; + const W64: usize = 4; + const FP64_MUL_BY_LANES: bool = false; + + fwd! { + v32_from_fn(f: impl FnMut(usize) -> u32) -> __m256i = + transmute::<[u32; 8], __m256i>(std::array::from_fn(f)); + v32_lane(v: __m256i, lane: usize) -> u32 = transmute::<__m256i, [u32; 8]>(v)[lane]; + v64_from_fn(f: impl FnMut(usize) -> u64) -> __m256i = + transmute::<[u64; 4], __m256i>(std::array::from_fn(f)); + v64_lane(v: __m256i, lane: usize) -> u64 = transmute::<__m256i, [u64; 4]>(v)[lane]; + splat32(x: u32) -> __m256i = _mm256_set1_epi32(x as i32); + splat64(x: u64) -> __m256i = _mm256_set1_epi64x(x as i64); + add32(a: __m256i, b: __m256i) -> __m256i = _mm256_add_epi32(a, b); + sub32(a: __m256i, b: __m256i) -> __m256i = _mm256_sub_epi32(a, b); + min_u32(a: __m256i, b: __m256i) -> __m256i = _mm256_min_epu32(a, b); + lt_u32(a: __m256i, b: __m256i) -> __m256i = { + let s = _mm256_set1_epi32(i32::MIN); + _mm256_cmpgt_epi32(_mm256_xor_si256(b, s), _mm256_xor_si256(a, s)) + }; + select32(m: __m256i, t: __m256i, f: __m256i) -> __m256i = _mm256_blendv_epi8(f, t, m); + add64(a: __m256i, b: __m256i) -> __m256i = _mm256_add_epi64(a, b); + sub64(a: __m256i, b: __m256i) -> __m256i = _mm256_sub_epi64(a, b); + and64(a: __m256i, b: __m256i) -> __m256i = _mm256_and_si256(a, b); + or64(a: __m256i, b: __m256i) -> __m256i = _mm256_or_si256(a, b); + shr64(v: __m256i, k: u32) -> __m256i = + _mm256_srl_epi64(v, _mm_set_epi64x(0, i64::from(k))); + shl64(v: __m256i, k: u32) -> __m256i = + _mm256_sll_epi64(v, _mm_set_epi64x(0, i64::from(k))); + lt_u64(a: __m256i, b: __m256i) -> __m256i = { + let s = _mm256_set1_epi64x(i64::MIN); + _mm256_cmpgt_epi64(_mm256_xor_si256(b, s), _mm256_xor_si256(a, s)) + }; + select64(m: __m256i, t: __m256i, f: __m256i) -> __m256i = _mm256_blendv_epi8(f, t, m); + // No 64-bit multiply: v*c = (v_lo·c) + ((v_hi·c) << 32) mod 2^64. + mul_small(v: __m256i, c: u64) -> __m256i = { + let cv = _mm256_set1_epi64x(c as i64); + let lo = _mm256_mul_epu32(v, cv); + let hi = _mm256_mul_epu32(_mm256_srli_epi64::<32>(v), cv); + _mm256_add_epi64(lo, _mm256_slli_epi64::<32>(hi)) + }; + mul_small_wide(v: __m256i, c: u64) -> [__m256i; 2] = { + let cv = _mm256_set1_epi64x(c as i64); + let lo_p = _mm256_mul_epu32(v, cv); + let hi_p = _mm256_mul_epu32(_mm256_srli_epi64::<32>(v), cv); + let lo = _mm256_add_epi64(lo_p, _mm256_slli_epi64::<32>(hi_p)); + // Subtracting an all-ones carry mask adds one. + let carry = Self::lt_u64(lo, lo_p); + let hi = _mm256_sub_epi64(_mm256_srli_epi64::<32>(hi_p), carry); + [lo, hi] + }; + // Schoolbook 64×64→128 from 32×32→64 partial products + // (plonky2/plonky3 Goldilocks technique). + mul64_wide(x: __m256i, y: __m256i) -> [__m256i; 2] = { + let x_hi = movehdup_epi32(x); + let y_hi = movehdup_epi32(y); + let mul_ll = _mm256_mul_epu32(x, y); + let mul_lh = _mm256_mul_epu32(x, y_hi); + let mul_hl = _mm256_mul_epu32(x_hi, y); + let mul_hh = _mm256_mul_epu32(x_hi, y_hi); + let t0 = _mm256_add_epi64(mul_hl, _mm256_srli_epi64::<32>(mul_ll)); + let t0_lo = _mm256_and_si256(t0, _mm256_set1_epi64x(0xFFFF_FFFF_i64)); + let t1 = _mm256_add_epi64(mul_lh, t0_lo); + let t2 = _mm256_add_epi64(mul_hh, _mm256_srli_epi64::<32>(t0)); + let hi = _mm256_add_epi64(t2, _mm256_srli_epi64::<32>(t1)); + let lo = _mm256_blend_epi32::<0b1010_1010>(mul_ll, moveldup_epi32(t1)); + [lo, hi] + }; + widen_mul(a: __m256i, b: __m256i) -> [__m256i; 2] = + [_mm256_mul_epu32(a, b), _mm256_mul_epu32(movehdup_epi32(a), movehdup_epi32(b))]; + narrow_pack(x: [__m256i; 2]) -> __m256i = + _mm256_blend_epi32::<0b1010_1010>(x[0], _mm256_slli_epi64::<32>(x[1])); + } + } +} + +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx512f", + target_feature = "avx512dq" +))] +pub use avx512::Avx512; + +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx512f", + target_feature = "avx512dq" +))] +#[expect( + clippy::undocumented_unsafe_blocks, + reason = "register-only AVX-512 intrinsics over plain integer lanes; the per-fn contracts are documented on the SimdWord trait" +)] +mod avx512 { + use super::SimdWord; + use core::arch::x86_64::*; + use core::mem::transmute; + + /// x86-64 AVX-512 (F + DQ): 512-bit vectors (16 × u32, 8 × u64). Native + /// unsigned compares produce mask registers, converted to all-ones lane + /// masks with `movm`; selects use one `vpternlogq` (truth table `0xCA` + /// computes `(m & t) | (!m & f)`). + pub enum Avx512 {} + + #[inline(always)] + unsafe fn movehdup_epi32_512(x: __m512i) -> __m512i { + unsafe { _mm512_castps_si512(_mm512_movehdup_ps(_mm512_castsi512_ps(x))) } + } + + #[inline(always)] + unsafe fn moveldup_epi32_512(x: __m512i) -> __m512i { + unsafe { _mm512_castps_si512(_mm512_moveldup_ps(_mm512_castsi512_ps(x))) } + } + + impl SimdWord for Avx512 { + type V32 = __m512i; + type V64 = __m512i; + const W32: usize = 16; + const W64: usize = 8; + const FP64_MUL_BY_LANES: bool = false; + + fwd! { + v32_from_fn(f: impl FnMut(usize) -> u32) -> __m512i = + transmute::<[u32; 16], __m512i>(std::array::from_fn(f)); + v32_lane(v: __m512i, lane: usize) -> u32 = transmute::<__m512i, [u32; 16]>(v)[lane]; + v64_from_fn(f: impl FnMut(usize) -> u64) -> __m512i = + transmute::<[u64; 8], __m512i>(std::array::from_fn(f)); + v64_lane(v: __m512i, lane: usize) -> u64 = transmute::<__m512i, [u64; 8]>(v)[lane]; + splat32(x: u32) -> __m512i = _mm512_set1_epi32(x as i32); + splat64(x: u64) -> __m512i = _mm512_set1_epi64(x as i64); + add32(a: __m512i, b: __m512i) -> __m512i = _mm512_add_epi32(a, b); + sub32(a: __m512i, b: __m512i) -> __m512i = _mm512_sub_epi32(a, b); + min_u32(a: __m512i, b: __m512i) -> __m512i = _mm512_min_epu32(a, b); + lt_u32(a: __m512i, b: __m512i) -> __m512i = + _mm512_movm_epi32(_mm512_cmplt_epu32_mask(a, b)); + select32(m: __m512i, t: __m512i, f: __m512i) -> __m512i = + _mm512_ternarylogic_epi64::<0xCA>(m, t, f); + add64(a: __m512i, b: __m512i) -> __m512i = _mm512_add_epi64(a, b); + sub64(a: __m512i, b: __m512i) -> __m512i = _mm512_sub_epi64(a, b); + and64(a: __m512i, b: __m512i) -> __m512i = _mm512_and_si512(a, b); + or64(a: __m512i, b: __m512i) -> __m512i = _mm512_or_si512(a, b); + shr64(v: __m512i, k: u32) -> __m512i = + _mm512_srl_epi64(v, _mm_set_epi64x(0, i64::from(k))); + shl64(v: __m512i, k: u32) -> __m512i = + _mm512_sll_epi64(v, _mm_set_epi64x(0, i64::from(k))); + lt_u64(a: __m512i, b: __m512i) -> __m512i = + _mm512_movm_epi64(_mm512_cmplt_epu64_mask(a, b)); + select64(m: __m512i, t: __m512i, f: __m512i) -> __m512i = + _mm512_ternarylogic_epi64::<0xCA>(m, t, f); + // AVX-512DQ has a true 64-bit low multiply. + mul_small(v: __m512i, c: u64) -> __m512i = + _mm512_mullo_epi64(v, _mm512_set1_epi64(c as i64)); + mul_small_wide(v: __m512i, c: u64) -> [__m512i; 2] = { + let cv = _mm512_set1_epi64(c as i64); + let lo_p = _mm512_mul_epu32(v, cv); + let hi_p = _mm512_mul_epu32(_mm512_srli_epi64::<32>(v), cv); + let lo = _mm512_add_epi64(lo_p, _mm512_slli_epi64::<32>(hi_p)); + let carry = _mm512_cmplt_epu64_mask(lo, lo_p); + let hi_base = _mm512_srli_epi64::<32>(hi_p); + let hi = _mm512_mask_add_epi64(hi_base, carry, hi_base, _mm512_set1_epi64(1)); + [lo, hi] + }; + // Schoolbook 64×64→128 from 32×32→64 partial products + // (plonky3 Goldilocks AVX-512 technique). + mul64_wide(x: __m512i, y: __m512i) -> [__m512i; 2] = { + let x_hi = movehdup_epi32_512(x); + let y_hi = movehdup_epi32_512(y); + let mul_ll = _mm512_mul_epu32(x, y); + let mul_lh = _mm512_mul_epu32(x, y_hi); + let mul_hl = _mm512_mul_epu32(x_hi, y); + let mul_hh = _mm512_mul_epu32(x_hi, y_hi); + let t0 = _mm512_add_epi64(mul_hl, _mm512_srli_epi64::<32>(mul_ll)); + let t0_lo = _mm512_and_si512(t0, _mm512_set1_epi64(0xFFFF_FFFF_i64)); + let t1 = _mm512_add_epi64(mul_lh, t0_lo); + let t2 = _mm512_add_epi64(mul_hh, _mm512_srli_epi64::<32>(t0)); + let hi = _mm512_add_epi64(t2, _mm512_srli_epi64::<32>(t1)); + let lo = + _mm512_mask_blend_epi32(0b0101_0101_0101_0101, moveldup_epi32_512(t1), mul_ll); + [lo, hi] + }; + widen_mul(a: __m512i, b: __m512i) -> [__m512i; 2] = [ + _mm512_mul_epu32(a, b), + _mm512_mul_epu32(movehdup_epi32_512(a), movehdup_epi32_512(b)), + ]; + narrow_pack(x: [__m512i; 2]) -> __m512i = + _mm512_mask_blend_epi32(0b1010_1010_1010_1010, x[0], _mm512_slli_epi64::<32>(x[1])); + } + } +} diff --git a/crates/jolt-field-two/tests/solinas_packed_differential.rs b/crates/jolt-field-two/tests/solinas_packed_differential.rs new file mode 100644 index 0000000000..c7180496f6 --- /dev/null +++ b/crates/jolt-field-two/tests/solinas_packed_differential.rs @@ -0,0 +1,559 @@ +//! Differential tests for the packed SIMD backends. +//! +//! Packed-vs-scalar equivalence on the native ISA for every width +//! (32/64/128) and every packed extension type, over random inputs and +//! boundary lane patterns (all-max lanes, mixed canonical extremes, +//! single-lane-nonzero); lane-access and slice-helper laws; +//! `WithPacking` associated-type sanity for every field type; `NoPacking` +//! equivalence; and, on aarch64/NEON, lane-exact differentials against +//! jolt-field's packed types (including the packed ext2 kernel hook and +//! the fused degree-4/8 kernels). + +#![cfg(feature = "solinas")] +#![expect(clippy::unwrap_used, reason = "test code")] + +use jolt_field_two as two; + +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; +use two::{ + pseudo_mersenne_modulus, CanonicalEncoding, ExtField, Field, NoPacking, Packed, Ring, + WithPacking, +}; + +/// Packed ops must equal per-lane scalar ops (add/sub/mul/square/inverse). +fn check_packed_matches_scalar(lhs: &[PF::Scalar], rhs: &[PF::Scalar]) { + let w = PF::WIDTH; + assert_eq!(lhs.len() % w, 0); + assert_eq!(lhs.len(), rhs.len()); + for (la, ra) in lhs.chunks_exact(w).zip(rhs.chunks_exact(w)) { + let a = PF::from_fn(|i| la[i]); + let b = PF::from_fn(|i| ra[i]); + let (sum, diff, prod, sq) = (a + b, a - b, a * b, a.square()); + for i in 0..w { + assert_eq!(sum.extract(i), la[i] + ra[i], "add lane {i}"); + assert_eq!(diff.extract(i), la[i] - ra[i], "sub lane {i}"); + assert_eq!(prod.extract(i), la[i] * ra[i], "mul lane {i}"); + assert_eq!(sq.extract(i), la[i] * la[i], "square lane {i}"); + } + let packed_inv = a.inverse(); + let scalar_inv: Option> = la.iter().map(|x| x.inverse()).collect(); + assert_eq!(packed_inv.is_some(), scalar_inv.is_some(), "inverse parity"); + if let (Some(pi), Some(si)) = (packed_inv, scalar_inv) { + for (i, s) in si.iter().enumerate() { + assert_eq!(pi.extract(i), *s, "inverse lane {i}"); + } + } + } +} + +/// Boundary lane patterns for a prime field with modulus `p`: all-max +/// lanes, mixed canonical extremes, and single-lane-nonzero, crossed. +fn check_boundary_patterns(p: u128) +where + PF: Packed, + PF::Scalar: CanonicalEncoding, +{ + let w = PF::WIDTH; + let f = |v: u128| PF::Scalar::from_u128_checked(v).unwrap(); + let all_max = vec![f(p - 1); w]; + let mixed: Vec<_> = (0..w).map(|i| f([0, 1, p - 2, p - 1][i % 4])).collect(); + check_packed_matches_scalar::(&all_max, &all_max); + check_packed_matches_scalar::(&mixed, &all_max); + check_packed_matches_scalar::(&all_max, &mixed); + check_packed_matches_scalar::(&mixed, &mixed); + for lane in 0..w { + let single: Vec<_> = (0..w) + .map(|i| if i == lane { f(p - 1) } else { f(0) }) + .collect(); + check_packed_matches_scalar::(&single, &all_max); + check_packed_matches_scalar::(&single, &single); + } +} + +/// Random packed-vs-scalar equivalence plus boundary patterns. +fn check_prime_field(p: u128, seed: u64) +where + PF: Packed, + PF::Scalar: CanonicalEncoding, +{ + let mut rng = ChaCha20Rng::seed_from_u64(seed); + let n = PF::WIDTH * 16; + let lhs: Vec = (0..n).map(|_| Field::random(&mut rng)).collect(); + let rhs: Vec = (0..n).map(|_| Field::random(&mut rng)).collect(); + check_packed_matches_scalar::(&lhs, &rhs); + check_boundary_patterns::(p); +} + +/// `from_fn`/`extract`/`broadcast` and the slice-helper laws. +fn check_lane_laws(vals: &[PF::Scalar]) { + let w = PF::WIDTH; + assert!(w >= 1); + let p = PF::from_fn(|i| vals[i % vals.len()]); + for i in 0..w { + assert_eq!( + p.extract(i), + vals[i % vals.len()], + "from_fn/extract lane {i}" + ); + } + let b = PF::broadcast(vals[0]); + for i in 0..w { + assert_eq!(b.extract(i), vals[0], "broadcast lane {i}"); + } + let len = w * 3 + (w - 1); + let buf: Vec<_> = (0..len).map(|i| vals[i % vals.len()]).collect(); + let (packed, suffix) = PF::pack_slice_with_suffix(&buf); + assert_eq!(packed.len(), 3); + assert_eq!(suffix.len(), w - 1); + let mut out = PF::unpack_slice(&packed); + out.extend_from_slice(suffix); + assert_eq!(out, buf, "pack/unpack roundtrip"); + assert_eq!(PF::pack_slice(&buf[..w * 3]).len(), 3); +} + +/// `WithPacking` associated-type sanity: the packing's scalar is the field +/// itself and the lane laws hold. +fn check_with_packing(seed: u64) { + let mut rng = ChaCha20Rng::seed_from_u64(seed); + let vals: Vec = (0..::WIDTH.max(4)) + .map(|_| Field::random(&mut rng)) + .collect(); + check_lane_laws::(&vals); +} + +fn pm(bits: u32, offset: u128) -> u128 { + pseudo_mersenne_modulus(bits, offset).unwrap() +} + +#[test] +fn packed_fp32_matches_scalar() { + check_prime_field::<::Packing>(pm(24, 3), 0x2401); + check_prime_field::<::Packing>(pm(30, 35), 0x3001); + check_prime_field::<::Packing>(pm(31, 19), 0x3101); + check_prime_field::<::Packing>(pm(32, 99), 0x3201); +} + +#[test] +fn packed_fp64_matches_scalar() { + check_prime_field::<::Packing>(pm(40, 195), 0x4001); + check_prime_field::<::Packing>(pm(48, 59), 0x4801); + check_prime_field::<::Packing>(pm(56, 27), 0x5601); + check_prime_field::<::Packing>(pm(64, 59), 0x6401); +} + +#[test] +fn packed_fp128_matches_scalar() { + check_prime_field::<::Packing>(pm(128, 275), 0x12801); + check_prime_field::<::Packing>(pm(128, 159), 0x12802); + check_prime_field::<::Packing>(pm(128, 2355), 0x12803); + check_prime_field::<::Packing>( + pm(128, 0xFFFF_A7F7), + 0x12804, + ); +} + +/// Extension boundary lanes: all-max coefficient vectors and mixed extremes. +fn check_ext_boundaries(p: u128) +where + F: Field + CanonicalEncoding, + PF: Packed, + PF::Scalar: ExtField, +{ + let f = |v: u128| F::from_u128_checked(v).unwrap(); + let d = >::DEGREE; + let all_max = PF::Scalar::from_base_slice(&vec![f(p - 1); d]); + let mixed = PF::Scalar::from_base_slice( + &(0..d) + .map(|i| f([0, 1, p - 2, p - 1][i % 4])) + .collect::>(), + ); + let w = PF::WIDTH; + let max_lanes = vec![all_max; w]; + let mixed_lanes: Vec<_> = (0..w) + .map(|i| if i % 2 == 0 { all_max } else { mixed }) + .collect(); + check_packed_matches_scalar::(&max_lanes, &max_lanes); + check_packed_matches_scalar::(&mixed_lanes, &max_lanes); + check_packed_matches_scalar::(&mixed_lanes, &mixed_lanes); +} + +/// Packed extension towers vs scalar extension arithmetic. +fn check_ext_field(p: u128, seed: u64) +where + F: Field + CanonicalEncoding, + PF: Packed, + PF::Scalar: ExtField, +{ + let mut rng = ChaCha20Rng::seed_from_u64(seed); + let n = PF::WIDTH * 8; + let lhs: Vec = (0..n).map(|_| Field::random(&mut rng)).collect(); + let rhs: Vec = (0..n).map(|_| Field::random(&mut rng)).collect(); + check_packed_matches_scalar::(&lhs, &rhs); + check_ext_boundaries::(p); +} + +#[test] +fn packed_ext2_matches_scalar() { + type F32 = two::Prime32Offset99; + type E2 = two::Ext2; + check_ext_field::<::Packing, F32>(pm(32, 99), 0xE201); + // NegOneNr is a genuine field over p ≡ 3 (mod 4). + type F251 = two::Fp32<251>; + type E2Neg = two::FpExt2; + check_ext_field::<::Packing, F251>(251, 0xE202); + type F64 = two::Prime64Offset59; + check_ext_field::< as WithPacking>::Packing, F64>(pm(64, 59), 0xE203); + type F128 = two::Prime128Offset275; + check_ext_field::< as WithPacking>::Packing, F128>(pm(128, 275), 0xE204); +} + +#[test] +fn packed_ext4_matches_scalar() { + type F32 = two::Prime32Offset99; + check_ext_field::< as WithPacking>::Packing, F32>(pm(32, 99), 0xE401); + type F31 = two::Prime31Offset19; + check_ext_field::< as WithPacking>::Packing, F31>(pm(31, 19), 0xE402); + type F64 = two::Prime64Offset59; + check_ext_field::< as WithPacking>::Packing, F64>(pm(64, 59), 0xE403); + type F128 = two::Prime128OffsetA7F7; + check_ext_field::< as WithPacking>::Packing, F128>( + pm(128, 0xFFFF_A7F7), + 0xE404, + ); +} + +#[test] +fn packed_ext8_matches_scalar() { + type F32 = two::Prime32Offset99; + check_ext_field::< as WithPacking>::Packing, F32>(pm(32, 99), 0xE801); + type F64 = two::Prime48Offset59; + check_ext_field::< as WithPacking>::Packing, F64>(pm(48, 59), 0xE802); + type F128 = two::Prime128Offset275; + check_ext_field::< as WithPacking>::Packing, F128>(pm(128, 275), 0xE803); +} + +#[test] +fn with_packing_associated_types() { + check_with_packing::(0x5101); + check_with_packing::(0x5102); + check_with_packing::(0x5103); + check_with_packing::(0x5104); + check_with_packing::(0x5105); + check_with_packing::(0x5106); + check_with_packing::(0x5107); + check_with_packing::(0x5108); + check_with_packing::(0x5109); + check_with_packing::(0x510a); + check_with_packing::(0x510b); + check_with_packing::(0x510c); + check_with_packing::>(0x510d); + check_with_packing::, two::NegOneNr>>(0x510e); + check_with_packing::>(0x510f); + check_with_packing::>(0x5110); + check_with_packing::>(0x5111); + check_with_packing::>(0x5112); +} + +#[test] +fn no_packing_equivalence() { + // A type with no SIMD path: NoPacking over a word field, exercised + // through the same laws and differentials as the SIMD backends. + type PF = NoPacking; + check_prime_field::(pm(32, 99), 0x0001); + let mut rng = ChaCha20Rng::seed_from_u64(0x0002); + let vals: Vec = (0..4).map(|_| Field::random(&mut rng)).collect(); + check_lane_laws::(&vals); + assert_eq!(PF::WIDTH, 1); +} + +/// Lane-exact differentials against jolt-field's packed types on the +/// native NEON backend: same canonical inputs, same lane results. +#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] +mod baseline_diff { + use super::*; + use jolt_field as base; + + use base::packed::{HasPacking, PackedField}; + use base::{CanonicalField, FromPrimitiveInt}; + use rand::Rng; + + /// Random + boundary lane vectors of canonical representatives. + fn canonical_inputs(p: u128, w: usize, seed: u64) -> Vec { + let mut rng = ChaCha20Rng::seed_from_u64(seed); + let mut out = vec![p - 1; w]; + out.extend((0..w).map(|i| [0, 1, p - 2, p - 1][i % 4])); + for lane in 0..w { + out.extend((0..w).map(|i| if i == lane { p - 1 } else { 0 })); + } + out.extend((0..w * 16).map(|_| rng.gen::() % p)); + out + } + + fn diff_prime(p: u128, seed: u64) + where + NP: Packed, + NP::Scalar: CanonicalEncoding, + BP: PackedField, + BP::Scalar: CanonicalField + FromPrimitiveInt, + { + assert_eq!(NP::WIDTH, BP::WIDTH, "lane width mismatch vs baseline"); + let w = NP::WIDTH; + let lhs = canonical_inputs(p, w, seed); + let rhs = canonical_inputs(p, w, seed ^ 0xFFFF); + for (la, ra) in lhs.chunks_exact(w).zip(rhs.chunks_exact(w)) { + let na = NP::from_fn(|i| ::from_u128(la[i])); + let nb = NP::from_fn(|i| ::from_u128(ra[i])); + let ba = BP::from_fn(|i| BP::Scalar::from_u128(la[i])); + let bb = BP::from_fn(|i| BP::Scalar::from_u128(ra[i])); + let pairs = [(na + nb, ba + bb), (na - nb, ba - bb), (na * nb, ba * bb)]; + for (op, (n, b)) in ["add", "sub", "mul"].iter().zip(pairs) { + for i in 0..w { + assert_eq!( + n.extract(i).to_u128_checked().unwrap(), + b.extract(i).to_canonical_u128(), + "{op} lane {i} differs from baseline" + ); + } + } + } + } + + #[test] + fn fp32_lanes_match_baseline() { + assert_eq!(::Packing::WIDTH, 4); + diff_prime::< + ::Packing, + ::Packing, + >(pm(24, 3), 0xB3201); + diff_prime::< + ::Packing, + ::Packing, + >(pm(30, 35), 0xB3202); + diff_prime::< + ::Packing, + ::Packing, + >(pm(31, 19), 0xB3203); + diff_prime::< + ::Packing, + ::Packing, + >(pm(32, 99), 0xB3204); + } + + #[test] + fn fp64_lanes_match_baseline() { + assert_eq!(::Packing::WIDTH, 2); + diff_prime::< + ::Packing, + ::Packing, + >(pm(40, 195), 0xB6401); + diff_prime::< + ::Packing, + ::Packing, + >(pm(48, 59), 0xB6402); + diff_prime::< + ::Packing, + ::Packing, + >(pm(56, 27), 0xB6403); + diff_prime::< + ::Packing, + ::Packing, + >(pm(64, 59), 0xB6404); + } + + #[test] + fn fp128_lanes_match_baseline() { + assert_eq!(::Packing::WIDTH, 2); + diff_prime::< + ::Packing, + ::Packing, + >(pm(128, 275), 0x00B1_2801); + diff_prime::< + ::Packing, + ::Packing, + >(pm(128, 0xFFFF_A7F7), 0x00B1_2802); + } + + /// Coefficient matrices for extension lanes. + fn coeff_lanes(p: u128, w: usize, seed: u64) -> Vec<[u128; D]> { + let mut rng = ChaCha20Rng::seed_from_u64(seed); + let mut out = vec![[p - 1; D]; w]; + out.extend( + (0..w).map(|lane| std::array::from_fn(|j| [0, 1, p - 2, p - 1][(lane + j) % 4])), + ); + out.extend((0..w * 8).map(|_| std::array::from_fn(|_| rng.gen::() % p))); + out + } + + fn diff_ext( + p: u128, + seed: u64, + mk_new: impl Fn([u128; D]) -> NP::Scalar, + mk_base: impl Fn([u128; D]) -> BP::Scalar, + canon_new: impl Fn(&NP::Scalar) -> Vec, + canon_base: impl Fn(&BP::Scalar) -> Vec, + ) where + NP: Packed, + BP: PackedField, + { + assert_eq!(NP::WIDTH, BP::WIDTH, "ext lane width mismatch vs baseline"); + let w = NP::WIDTH; + let lhs = coeff_lanes::(p, w, seed); + let rhs = coeff_lanes::(p, w, seed ^ 0xFFFF); + for (la, ra) in lhs.chunks_exact(w).zip(rhs.chunks_exact(w)) { + let na = NP::from_fn(|i| mk_new(la[i])); + let nb = NP::from_fn(|i| mk_new(ra[i])); + let ba = BP::from_fn(|i| mk_base(la[i])); + let bb = BP::from_fn(|i| mk_base(ra[i])); + let pairs = [ + (na + nb, ba + bb), + (na - nb, ba - bb), + (na * nb, ba * bb), + (na.square(), ba.square()), + ]; + for (op, (n, b)) in ["add", "sub", "mul", "square"].iter().zip(pairs) { + for i in 0..w { + assert_eq!( + canon_new(&n.extract(i)), + canon_base(&b.extract(i)), + "ext {op} lane {i} differs from baseline" + ); + } + } + } + } + + /// The packed ext2 kernel hook, differentially vs the baseline hook. + #[test] + fn ext2_kernel_matches_baseline() { + type NF = two::Prime32Offset99; + type BF = base::Prime32Offset99; + type NP = as WithPacking>::Packing; + type BP = base::packed::PackedFpExt2::Packing>; + diff_ext::<2, NP, BP>( + pm(32, 99), + 0xBE201, + |c| two::FpExt2::new(Ring::from_u128(c[0]), Ring::from_u128(c[1])), + |c| base::FpExt2::new(BF::from_u128(c[0]), BF::from_u128(c[1])), + |x| { + x.coeffs + .iter() + .map(|c| c.to_u128_checked().unwrap()) + .collect() + }, + |x| x.coeffs.iter().map(|c| c.to_canonical_u128()).collect(), + ); + type NF251 = two::Fp32<251>; + type BF251 = base::Fp32<251>; + type NPn = as WithPacking>::Packing; + type BPn = + base::packed::PackedFpExt2::Packing>; + diff_ext::<2, NPn, BPn>( + 251, + 0xBE202, + |c| two::FpExt2::new(Ring::from_u128(c[0]), Ring::from_u128(c[1])), + |c| base::FpExt2::new(BF251::from_u128(c[0]), BF251::from_u128(c[1])), + |x| { + x.coeffs + .iter() + .map(|c| c.to_u128_checked().unwrap()) + .collect() + }, + |x| x.coeffs.iter().map(|c| c.to_canonical_u128()).collect(), + ); + } + + /// Fused degree-4 kernels (dot products on fp32) vs the baseline NEON + /// kernels, plus the schedule-default paths on wider bases. + #[test] + fn ext4_kernels_match_baseline() { + macro_rules! diff_ext4 { + ($nf:ty, $bf:ty, $p:expr, $seed:expr) => { + diff_ext::< + 4, + as WithPacking>::Packing, + base::packed::PackedFpExt4<$bf, <$bf as HasPacking>::Packing>, + >( + $p, + $seed, + |c| two::FpExt4::new(c.map(Ring::from_u128)), + |c| base::FpExt4::new(c.map(<$bf>::from_u128)), + |x| { + x.coeffs + .iter() + .map(|c| c.to_u128_checked().unwrap()) + .collect() + }, + |x| x.coeffs.iter().map(|c| c.to_canonical_u128()).collect(), + ); + }; + } + diff_ext4!( + two::Prime32Offset99, + base::Prime32Offset99, + pm(32, 99), + 0xBE401 + ); + diff_ext4!( + two::Prime31Offset19, + base::Prime31Offset19, + pm(31, 19), + 0xBE402 + ); + diff_ext4!( + two::Prime64Offset59, + base::Prime64Offset59, + pm(64, 59), + 0xBE403 + ); + diff_ext4!( + two::Prime128Offset275, + base::Prime128Offset275, + pm(128, 275), + 0xBE404 + ); + } + + #[test] + fn ext8_kernels_match_baseline() { + macro_rules! diff_ext8 { + ($nf:ty, $bf:ty, $p:expr, $seed:expr) => { + diff_ext::< + 8, + as WithPacking>::Packing, + base::packed::PackedFpExt8<$bf, <$bf as HasPacking>::Packing>, + >( + $p, + $seed, + |c| two::FpExt8::new(c.map(Ring::from_u128)), + |c| base::FpExt8::new(c.map(<$bf>::from_u128)), + |x| { + x.coeffs + .iter() + .map(|c| c.to_u128_checked().unwrap()) + .collect() + }, + |x| x.coeffs.iter().map(|c| c.to_canonical_u128()).collect(), + ); + }; + } + diff_ext8!( + two::Prime32Offset99, + base::Prime32Offset99, + pm(32, 99), + 0xBE801 + ); + diff_ext8!( + two::Prime64Offset59, + base::Prime64Offset59, + pm(64, 59), + 0xBE802 + ); + diff_ext8!( + two::Prime128Offset275, + base::Prime128Offset275, + pm(128, 275), + 0xBE803 + ); + } +} From 58785fa1c6e3015b84bca2c24737b197f42f8f3c Mon Sep 17 00:00:00 2001 From: acentelles Date: Thu, 30 Jul 2026 22:24:52 -0400 Subject: [PATCH 25/38] feat(jolt-field-two): parallel helpers, crate docs, final audit (checkpoint 9) src/solinas/parallel.rs (78/80 counted): the baseline rayon helpers ported whole, byte-faithful (expansion-site cfg so consumers dispatch on their own parallel feature). The consumer audit found that none of the seven cfg_*! macros has a single consumer anywhere in the workspace or the rebuild - no crate even enables jolt-field's parallel feature (crates that parallelize carry their own rayon deps) - so the component is recorded in SPEC.md as a deletion candidate at the replacement PR; parity scope names it explicitly, hence the whole port rather than an unevidenced subset. New tests/parallel_macros.rs proves serial/rayon equivalence for every macro (green under both configurations). Crate docs on lib.rs (two-layer architecture, backends, features, byte-compatibility invariants); code lines drop to 43/70 since docs are free under the counting rules. Final audit written into SPEC.md with actuals beside budgets: 5,103 counted LOC vs the 6,240 budget (18% under; 55% below the 11,410-LOC baseline). Six files over their file budgets, all in one consolidated budget-trades note: packed.rs 105/90 (ext kernel hooks), solinas/mod.rs 113/90 (re-exports accumulate), limbs.rs 237/220, bn254/mont.rs 310/300, packed/mod.rs 36/30, packed/simd.rs 352/350. Feature matrix all clean (check + clippy -D warnings): no-default, solinas, bn254, solinas+parallel, solinas+allocative, all-features, plus both x86_64 target-feature cross-checks in two feature configs. 121/121 tests. SPEC status: all nine checkpoints built; remaining before replacement recorded (x86 runtime validation, bench re-evaluations, the replacement PR itself). --- crates/jolt-field-two/SPEC.md | 125 ++++++++++++++---- crates/jolt-field-two/src/lib.rs | 67 ++++++++-- crates/jolt-field-two/src/solinas/mod.rs | 1 + crates/jolt-field-two/src/solinas/parallel.rs | 118 +++++++++++++++++ .../jolt-field-two/tests/parallel_macros.rs | 86 ++++++++++++ 5 files changed, 360 insertions(+), 37 deletions(-) create mode 100644 crates/jolt-field-two/src/solinas/parallel.rs create mode 100644 crates/jolt-field-two/tests/parallel_macros.rs diff --git a/crates/jolt-field-two/SPEC.md b/crates/jolt-field-two/SPEC.md index c0de003847..8a4a0d2ba2 100644 --- a/crates/jolt-field-two/SPEC.md +++ b/crates/jolt-field-two/SPEC.md @@ -2,7 +2,7 @@ | Field | Value | |---------|----------------------------------------------------| -| Status | approved — building (checkpoint 1) | +| Status | built — all nine checkpoints complete; final audit: 5,103 counted LOC (budget 6,240), feature matrix + full test suite green | | Baseline| `jolt-field` @ PR #1684 head (`fe1d5d41f`) | | Goal | functional parity at ≤ 6,300 counted LOC (baseline: 11,410) | @@ -222,6 +222,71 @@ and fold matrices; `S64`–`S256` + hi32 variants; `Limbs`; rayon helpers; and the scalar field types; nothing awaits the checkpoint-7 `Unreduced` surface. +**Dropped-specialization evidence (checkpoint 9, parallel):** + +- **Audit finding:** none of the baseline's seven `cfg_*!` macros + (`cfg_iter`, `cfg_iter_mut`, `cfg_into_iter`, `cfg_chunks`, + `cfg_chunks_mut`, `cfg_join`, `cfg_fold_reduce`) has a single consumer — + not in this workspace branch, not in the main checkout, and not in the + rebuild's own code. No workspace crate even enables `jolt-field`'s + `parallel` feature; its only in-tree activation is the baseline's own + `solinas_field_arith` bench, which uses rayon directly rather than + through the macros (workspace crates that parallelize — `jolt-poly`, + `jolt-kernels` — carry their own rayon deps). +- **Kept whole anyway:** the parity scope explicitly names the rayon + helpers, and with all seven macros equally unconsumed there is no + evidence basis for a partial subset. Ported unchanged (78 counted LOC vs + 80 budget) as `solinas/parallel.rs`, wired exactly as the baseline: + optional `rayon` dep behind `parallel = ["dep:rayon"]`, module gated on + `solinas`, macros `#[macro_export]`ed with expansion-site `cfg` so they + dispatch on the consuming crate's own `parallel` feature. **Flag for the + replacement PR:** if no consumer materializes when consumers rebind, the + whole component (and possibly the feature) is a deletion candidate. +- **Coverage:** `tests/parallel_macros.rs` exercises every macro against an + explicitly serial computation; the suite runs in both configurations + (without `parallel` → sequential expansions, `--all-features` → rayon), + so a green run in both is the serial-vs-parallel equivalence proof. + +## Final LOC audit (checkpoint 9) + +Final per-file actuals are recorded in the file-structure table below +(counted with the awk counter above): **5,103 total vs the 6,240 budget +(−1,137, 18% under; 55% below the 11,410 baseline).** + +**Budget trades for review** (every over-budget file, consolidated): + +- `src/solinas/mod.rs` 113/90 (+26%): the registry grew four `Fp128` + aliases plus the `reduce_le_bytes_mod_order` shared helper; worst + percentage overrun, but it is the crate's declarative registry — golfing + it means deleting doc-typed aliases. +- `src/packed.rs` 105/90 (+17%): the four `ext4/ext8` kernel hooks with + schedule defaults must live on the `Packed` trait (overridable defaults + need the trait); recorded at checkpoint 8. +- `src/solinas/packed/mod.rs` 36/30 (+20%): per-ISA backend selection for + three ISAs plus `NoPacking` fallback; component (packed selection + 120 = 90 + 30) is at 141 (+17.5%). +- `src/limbs.rs` 237/220 (+8%): within the ≤10% discussion band. +- `src/bn254/mont.rs` 310/300 (+3%): within the band; offset by + `bn254/mod.rs` at −126 (component 700 budget → 584 actual). +- `src/solinas/packed/simd.rs` 352/350 (+1%): weight moved here from + `engine.rs` (284/550) by the generic-types-over-vocabulary design; + recorded at checkpoint 8. + +## Remaining before replacement + +1. **x86-64 runtime validation:** AVX2/AVX-512 packed backends and the + fp128 portable mul path are `cargo check`-validated with + `-C target-feature` only (checkpoint 8/9 acceptance); run the packed + differential suite and the fp128 differentials on real x86-64 hardware. +2. **Bench re-evaluation entries:** rerun the fused deg-4 kernel bench + (`benches/ext4_kernels.rs`) on x86-64 before deciding the + `PseudoMersenne` hook overrides stay generic (checkpoint 6 caveat); + thin comparison bench vs baseline for the scalar/packed hot paths. +3. **The replacement PR itself:** rebind consumers to the new trait names, + delete `jolt-field`, re-point the `jolt-field` workspace alias; decide + the fate of the unconsumed parallel helpers (checkpoint 9 audit above); + CI wiring with a target-feature lane so SIMD is not CI-dark. + ## Design pillars 1. **Const-generic scalar core**: `Fp64` etc., fold constants @@ -270,34 +335,36 @@ on them. ## File structure and budgets -| File | Budget | Contents | -|---|---|---| -| **Contract layer (root, unconditional)** | | | -| `src/lib.rs` | 70 | crate docs, feature gates, re-exports, `FieldError` | -| `src/algebra.rs` | 260 | spine: 7 traits + `NaiveAccumulator` + `PseudoMersenne` | -| `src/extension.rs` | 60 | contracts: `ExtField`, `Ext2Config` + NR config ZSTs, `MulBaseUnreduced` | -| `src/unreduced.rs` | 70 | contracts: `Unreduced`, `Fold` | -| `src/packed.rs` | 90 | contracts: `Packed`, `WithPacking` + generic `NoPacking` | -| `src/ops.rs` | 180 | `impl_ring_ops!`, `impl_serde_bytes!` (backend-neutral) | -| `src/schedules.rs` | 140 | lane-generic deg-4/8 ext coefficient schedules — unconditional because the `PseudoMersenne` hook defaults (algebra.rs) and the packed lanes (checkpoint 8) share them; carved out of the old `ext.rs` budget (890 → 750, component total unchanged) | -| `src/limbs.rs` | 220 | `Limbs` | -| `src/signed.rs` | 420 | signed bigint families (consumer-audited surface) | -| **bn254 backend** | | | -| `src/bn254/mod.rs` | 400 | `Fr`, `Fq` via one wrapping macro; serde; transcript bytes | -| `src/bn254/mont.rs` | 300 | Barrett/Montgomery kernel + `WideAccumulator` | -| **solinas backend** | | | -| `src/solinas/mod.rs` | 90 | offset registry, aliases, shared helpers | -| `src/solinas/word.rs` | 380 | `define_solinas_prime!` → `Fp32`, `Fp64` | -| `src/solinas/fp128.rs` | 700 | two-limb add/sub/mul/reduce/wide | -| `src/solinas/ext.rs` | 750 | FpExt2/4/8 impls, ExtField impls, Frobenius + Moore | -| `src/solinas/unreduced.rs` | 530 | lane accumulators, fold matrices, contract impls | -| `src/solinas/parallel.rs` | 80 | rayon helpers | -| `src/solinas/packed/mod.rs` | 30 | backend selection | -| `src/solinas/packed/simd.rs` | 350 | per-ISA primitive vocabulary (neon/avx2/avx512) | -| `src/solinas/packed/engine.rs` | 550 | shared packed algebra, stamped per width × ISA | -| `src/solinas/packed/fp128.rs` | 350 | 128-bit-lane engine | -| `src/solinas/packed/ext.rs` | 230 | packed FpExt2/4/8 | -| **Total** | **6,240** | vs 11,410 baseline (−45%) | +Actuals are the checkpoint-9 final audit (awk counter above). + +| File | Budget | Actual | Contents | +|---|---|---|---| +| **Contract layer (root, unconditional)** | | | | +| `src/lib.rs` | 70 | 43 | crate docs, feature gates, re-exports, `FieldError` | +| `src/algebra.rs` | 260 | 252 | spine: 7 traits + `NaiveAccumulator` + `PseudoMersenne` | +| `src/extension.rs` | 60 | 60 | contracts: `ExtField`, `Ext2Config` + NR config ZSTs, `MulBaseUnreduced` | +| `src/unreduced.rs` | 70 | 18 | contracts: `Unreduced`, `Fold` | +| `src/packed.rs` | 90 | **105** | contracts: `Packed`, `WithPacking` + generic `NoPacking` | +| `src/ops.rs` | 180 | 160 | `impl_ring_ops!`, `impl_serde_bytes!` (backend-neutral) | +| `src/schedules.rs` | 140 | 133 | lane-generic deg-4/8 ext coefficient schedules — unconditional because the `PseudoMersenne` hook defaults (algebra.rs) and the packed lanes (checkpoint 8) share them; carved out of the old `ext.rs` budget (890 → 750, component total unchanged) | +| `src/limbs.rs` | 220 | **237** | `Limbs` | +| `src/signed.rs` | 420 | 357 | signed bigint families (consumer-audited surface) | +| **bn254 backend** | | | | +| `src/bn254/mod.rs` | 400 | 274 | `Fr`, `Fq` via one wrapping macro; serde; transcript bytes | +| `src/bn254/mont.rs` | 300 | **310** | Barrett/Montgomery kernel + `WideAccumulator` | +| **solinas backend** | | | | +| `src/solinas/mod.rs` | 90 | **113** | offset registry, aliases, shared helpers | +| `src/solinas/word.rs` | 380 | 355 | `define_solinas_prime!` → `Fp32`, `Fp64` | +| `src/solinas/fp128.rs` | 700 | 528 | two-limb add/sub/mul/reduce/wide | +| `src/solinas/ext.rs` | 750 | 617 | FpExt2/4/8 impls, ExtField impls, Frobenius + Moore | +| `src/solinas/unreduced.rs` | 530 | 501 | lane accumulators, fold matrices, contract impls | +| `src/solinas/parallel.rs` | 80 | 78 | rayon helpers | +| `src/solinas/packed/mod.rs` | 30 | **36** | backend selection | +| `src/solinas/packed/simd.rs` | 350 | **352** | per-ISA primitive vocabulary (neon/avx2/avx512) | +| `src/solinas/packed/engine.rs` | 550 | 284 | shared packed algebra, stamped per width × ISA | +| `src/solinas/packed/fp128.rs` | 350 | 99 | 128-bit-lane engine | +| `src/solinas/packed/ext.rs` | 230 | 191 | packed FpExt2/4/8 | +| **Total** | **6,240** | **5,103** | vs 11,410 baseline (budget −45%, actual −55%) | Component budgets are unchanged — the contract/impl split carves each component's contracts out of its old single-file budget (extensions 950 = diff --git a/crates/jolt-field-two/src/lib.rs b/crates/jolt-field-two/src/lib.rs index cb9989cebf..31fb32944d 100644 --- a/crates/jolt-field-two/src/lib.rs +++ b/crates/jolt-field-two/src/lib.rs @@ -4,18 +4,69 @@ //! orthogonal capabilities: [`CanonicalEncoding`] (the Fiat-Shamir transcript //! surface) and [`WithAccumulator`] (deferred-reduction fused multiply-add). //! [`JoltField`] is the blanket-implemented bundle of everything Jolt's -//! protocol stack requires of a scalar field. +//! protocol stack requires of a scalar field: `Field + CanonicalEncoding + +//! WithAccumulator + Serialize + DeserializeOwned`. Because the impl is a +//! blanket, no field type can forget to opt in. //! -//! Proof/wire serialization is serde + bincode over canonical bytes (see -//! [`impl_serde_bytes!`]); transcript bytes use [`CanonicalEncoding`]'s explicit -//! little-endian encoding and never go through a serialization library. +//! # Architecture: contracts and backends +//! +//! The crate is two layers with a one-way dependency: +//! +//! 1. **Contract layer** (crate root, unconditional): every trait the crate +//! defines — the spine above plus the capability contracts +//! [`PseudoMersenne`], [`ExtField`], [`Ext2Config`], [`MulBaseUnreduced`], +//! [`Unreduced`], [`Fold`], [`Packed`], [`WithPacking`] — together with +//! the stamping macros ([`impl_ring_ops!`], [`impl_group_ops!`], +//! [`impl_serde_bytes!`]) and the backend-neutral value types +//! ([`Limbs`], the [`signed`] bigint families). Contract files contain +//! trait definitions only; the crate's full capability surface is +//! readable from the root regardless of enabled features. +//! 2. **Backend layer** (feature-gated modules): implementations of the +//! contracts. Backends never reference each other and the contract layer +//! never references a backend, so a backend can be deleted or added +//! without touching the contracts. A new backend implements the spine +//! (serde and the [`JoltField`] umbrella come free via the exported +//! macros and the blanket impl) and opts into whichever capability +//! contracts it can serve. //! //! # Backends //! -//! - `bn254` (default): BN254 `Fr`/`Fq` via arkworks, plus a 9-limb wide -//! accumulator with deferred Montgomery reduction. -//! - `solinas`: 32/64/128-bit pseudo-Mersenne prime fields, extension -//! towers, packed NEON/AVX2/AVX-512 backends, unreduced accumulators. +//! - `bn254` (default): BN254 `Fr`/`Fq` wrapping arkworks, plus +//! `WideAccumulator`, a 9-limb accumulator with deferred Montgomery +//! reduction (first-party Barrett/Montgomery kernels). +//! - `solinas`: fully first-party pseudo-Mersenne fields `p = 2^k − c` — +//! `Fp32`/`Fp64` stamped from one fold algebra plus the hand-written +//! two-limb `Fp128`; cyclotomic extension towers `FpExt2`/`FpExt4`/ +//! `FpExt8` with Frobenius/Moore machinery; unreduced lane accumulators +//! and fold matrices; packed SIMD backends (NEON, AVX2, AVX-512) for +//! 32/64/128-bit lanes and packed extensions. +//! +//! # Feature flags +//! +//! - `bn254` (default) — the arkworks-backed BN254 backend. +//! - `solinas` — the pseudo-Mersenne backend (scalar, extension, unreduced, +//! packed, and the conditional-parallelism helpers in +//! `solinas::parallel`). +//! - `parallel` — activates rayon behind the `cfg_*!` helper macros. +//! - `allocative` — `Allocative` derives on the concrete field types for +//! memory profiling. +//! +//! # Byte compatibility (hard invariants) +//! +//! Wire and transcript encodings are byte-identical to `jolt-field` at the +//! rebuild baseline, for both backends, so replacing that crate cannot +//! change proof bytes: +//! +//! - Proof/wire serialization is serde + bincode over canonical +//! little-endian bytes (see [`impl_serde_bytes!`]); deserialization +//! rejects non-canonical encodings uniformly via +//! [`CanonicalEncoding::from_bytes_le_checked`]. +//! - Fiat-Shamir transcript bytes use [`CanonicalEncoding`]'s explicit +//! little-endian encoding ([`CanonicalEncoding::to_bytes_le`]) and never +//! go through a serialization library. +//! +//! Both invariants are enforced by differential tests against `jolt-field` +//! as the oracle (`tests/*_differential.rs`). mod algebra; #[cfg(feature = "bn254")] diff --git a/crates/jolt-field-two/src/solinas/mod.rs b/crates/jolt-field-two/src/solinas/mod.rs index c6428de4ab..c116b25ae2 100644 --- a/crates/jolt-field-two/src/solinas/mod.rs +++ b/crates/jolt-field-two/src/solinas/mod.rs @@ -7,6 +7,7 @@ mod ext; mod fp128; mod packed; +pub mod parallel; mod unreduced; mod word; diff --git a/crates/jolt-field-two/src/solinas/parallel.rs b/crates/jolt-field-two/src/solinas/parallel.rs new file mode 100644 index 0000000000..daa8cbc7f6 --- /dev/null +++ b/crates/jolt-field-two/src/solinas/parallel.rs @@ -0,0 +1,118 @@ +//! Conditional parallelism helpers. +//! +//! The `cfg_*!` macros expand to rayon parallel iterators when a `parallel` +//! feature is enabled and to the standard sequential equivalents otherwise. +//! +//! WARNING: the `#[cfg(feature = "parallel")]` inside each macro body is +//! resolved at the *expansion* site, so the macros dispatch on the consuming +//! crate's own `parallel` feature (which must activate rayon there). This is +//! the baseline's design, ported unchanged; within this crate the expansion +//! site is this crate, so its `parallel` feature governs. +//! +//! Consumer audit (checkpoint 9): no crate in the workspace, and nothing in +//! this rebuild, currently expands any of these macros — see the +//! dropped-specialization notes in `SPEC.md`. The component is ported whole +//! because the approved parity scope names the rayon helpers and all seven +//! macros are equally (un)consumed, leaving no evidence basis for a partial +//! subset. + +#[cfg(feature = "parallel")] +pub use rayon::prelude::*; + +/// Returns `.par_iter()` when `parallel` is enabled, `.iter()` otherwise. +#[macro_export] +macro_rules! cfg_iter { + ($e:expr) => {{ + #[cfg(feature = "parallel")] + let it = $e.par_iter(); + #[cfg(not(feature = "parallel"))] + let it = $e.iter(); + it + }}; +} + +/// Returns `.par_iter_mut()` when `parallel` is enabled, `.iter_mut()` otherwise. +#[macro_export] +macro_rules! cfg_iter_mut { + ($e:expr) => {{ + #[cfg(feature = "parallel")] + let it = $e.par_iter_mut(); + #[cfg(not(feature = "parallel"))] + let it = $e.iter_mut(); + it + }}; +} + +/// Returns `.into_par_iter()` when `parallel` is enabled, `.into_iter()` otherwise. +#[macro_export] +macro_rules! cfg_into_iter { + ($e:expr) => {{ + #[cfg(feature = "parallel")] + let it = $e.into_par_iter(); + #[cfg(not(feature = "parallel"))] + let it = $e.into_iter(); + it + }}; +} + +/// Returns `.par_chunks(n)` when `parallel` is enabled, `.chunks(n)` otherwise. +#[macro_export] +macro_rules! cfg_chunks { + ($e:expr, $n:expr) => {{ + #[cfg(feature = "parallel")] + let it = $e.par_chunks($n); + #[cfg(not(feature = "parallel"))] + let it = $e.chunks($n); + it + }}; +} + +/// Returns `.par_chunks_mut(n)` when `parallel` is enabled, `.chunks_mut(n)` otherwise. +#[macro_export] +macro_rules! cfg_chunks_mut { + ($e:expr, $n:expr) => {{ + #[cfg(feature = "parallel")] + let it = $e.par_chunks_mut($n); + #[cfg(not(feature = "parallel"))] + let it = $e.chunks_mut($n); + it + }}; +} + +/// Runs two closures potentially in parallel via `rayon::join`. +/// +/// Without `parallel`: runs them sequentially and returns the pair. +#[macro_export] +macro_rules! cfg_join { + ($f_a:expr, $f_b:expr) => {{ + #[cfg(feature = "parallel")] + let result = rayon::join($f_a, $f_b); + #[cfg(not(feature = "parallel"))] + let result = ($f_a(), $f_b()); + result + }}; +} + +/// Parallel fold-reduce over a range. +/// +/// With `parallel`: `range.into_par_iter().fold(identity, fold_op).reduce(identity, reduce_op)`. +/// Without: `range.into_iter().fold(identity(), fold_op)` — `reduce_op` is +/// unused, so serial and parallel results agree only when `reduce_op` is +/// consistent with `fold_op` (associative combination of partials). +#[macro_export] +macro_rules! cfg_fold_reduce { + ($range:expr, $identity:expr, $fold_op:expr, $reduce_op:expr) => {{ + #[cfg(feature = "parallel")] + let result = $range + .into_par_iter() + .fold($identity, $fold_op) + .reduce($identity, $reduce_op); + #[cfg(not(feature = "parallel"))] + let result = $range.into_iter().fold(($identity)(), $fold_op); + result + }}; +} + +pub use crate::{ + cfg_chunks, cfg_chunks_mut, cfg_fold_reduce, cfg_into_iter, cfg_iter, cfg_iter_mut, cfg_join, +}; diff --git a/crates/jolt-field-two/tests/parallel_macros.rs b/crates/jolt-field-two/tests/parallel_macros.rs new file mode 100644 index 0000000000..58e1eddfb4 --- /dev/null +++ b/crates/jolt-field-two/tests/parallel_macros.rs @@ -0,0 +1,86 @@ +//! Serial-vs-parallel equivalence for the `cfg_*!` conditional-parallelism +//! macros (`solinas::parallel`). +//! +//! The macros dispatch on the `parallel` feature at the expansion site, so +//! this suite exercises the sequential expansions when run without the +//! feature and the rayon expansions when run with it (`--all-features`); +//! every assertion compares against an explicitly serial computation, so a +//! green run in both configurations is the equivalence proof. + +#![cfg(feature = "solinas")] + +use jolt_field_two::{Prime64Offset59, Ring, Zero}; + +#[cfg(feature = "parallel")] +use jolt_field_two::solinas::parallel::*; +use jolt_field_two::{ + cfg_chunks, cfg_chunks_mut, cfg_fold_reduce, cfg_into_iter, cfg_iter, cfg_iter_mut, cfg_join, +}; + +type F = Prime64Offset59; + +fn inputs() -> Vec { + (0..1000u64).map(F::from_u64).collect() +} + +fn serial_sum(v: &[F]) -> F { + v.iter().fold(F::zero(), |acc, x| acc + *x) +} + +#[test] +fn iter_and_into_iter_match_serial() { + let v = inputs(); + let expected = serial_sum(&v); + let sum: F = cfg_iter!(v).copied().sum(); + assert_eq!(sum, expected); + let sum: F = cfg_into_iter!(v.clone()).sum(); + assert_eq!(sum, expected); +} + +#[test] +fn iter_mut_and_chunks_mut_match_serial() { + let v = inputs(); + let two = F::from_u64(2); + let expected: Vec = v.iter().map(|x| *x * two).collect(); + + let mut a = v.clone(); + cfg_iter_mut!(a).for_each(|x| *x *= two); + assert_eq!(a, expected); + + let mut b = v; + cfg_chunks_mut!(b, 7).for_each(|chunk| { + for x in chunk { + *x *= two; + } + }); + assert_eq!(b, expected); +} + +#[test] +fn chunks_match_serial() { + let v = inputs(); + let expected: Vec = v.chunks(13).map(serial_sum).collect(); + let sums: Vec = cfg_chunks!(v, 13).map(serial_sum).collect(); + assert_eq!(sums, expected); +} + +#[test] +fn join_returns_both_results() { + let v = inputs(); + let (lo, hi) = v.split_at(v.len() / 2); + let (a, b) = cfg_join!(|| serial_sum(lo), || serial_sum(hi)); + assert_eq!(a + b, serial_sum(&v)); +} + +#[test] +fn fold_reduce_matches_serial_sum_of_squares() { + let v = inputs(); + let expected = v.iter().fold(F::zero(), |acc, x| acc + x.square()); + let got = cfg_fold_reduce!( + 0..v.len(), + F::zero, + |acc: F, i: usize| acc + v[i].square(), + |a: F, b: F| a + b + ); + assert_eq!(got, expected); +} From 5b3e39ece1c27586a1f7cc77f24e718cb5d73e10 Mon Sep 17 00:00:00 2001 From: acentelles Date: Fri, 31 Jul 2026 14:28:50 -0400 Subject: [PATCH 26/38] docs(specs): move jolt-field-two SPEC.md to specs/jolt-field-rebuild.md --- crates/jolt-field-two/benches/ext4_kernels.rs | 2 +- crates/jolt-field-two/src/solinas/ext.rs | 2 +- crates/jolt-field-two/src/solinas/parallel.rs | 2 +- crates/jolt-field-two/src/solinas/unreduced.rs | 2 +- crates/jolt-field-two/src/solinas/word.rs | 2 +- crates/jolt-field-two/SPEC.md => specs/jolt-field-rebuild.md | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename crates/jolt-field-two/SPEC.md => specs/jolt-field-rebuild.md (100%) diff --git a/crates/jolt-field-two/benches/ext4_kernels.rs b/crates/jolt-field-two/benches/ext4_kernels.rs index 05dc5009ae..fe503c9c60 100644 --- a/crates/jolt-field-two/benches/ext4_kernels.rs +++ b/crates/jolt-field-two/benches/ext4_kernels.rs @@ -5,7 +5,7 @@ //! muls and squares over `Prime32Offset99`. The baseline `FpExt4` //! (which ships the fused override) is included for context. //! -//! Outcome recorded in SPEC.md: the fused port LOST on aarch64/Apple M4 +//! Outcome recorded in specs/jolt-field-rebuild.md: the fused port LOST on aarch64/Apple M4 //! (generic ≈ 2.5x faster on mul, ≈ 1.85x on square; the port reproduces //! the baseline override's timing exactly), so the override was dropped //! and the crate keeps the generic defaults. This harness stays as the diff --git a/crates/jolt-field-two/src/solinas/ext.rs b/crates/jolt-field-two/src/solinas/ext.rs index 9a748a3447..4344e24e3e 100644 --- a/crates/jolt-field-two/src/solinas/ext.rs +++ b/crates/jolt-field-two/src/solinas/ext.rs @@ -8,7 +8,7 @@ //! dispatches through the [`PseudoMersenne`] kernel hooks; every base field //! keeps the generic-schedule defaults (`crate::schedules`) — the baseline's //! fused u128-accumulation `Fp32` override lost the checkpoint-6 bench gate -//! (see SPEC.md and `benches/ext4_kernels.rs`). +//! (see specs/jolt-field-rebuild.md and `benches/ext4_kernels.rs`). //! //! Frobenius powers are intentionally algebraic (raise to powers of the base //! modulus) rather than basis-specific: one auditable contract first; diff --git a/crates/jolt-field-two/src/solinas/parallel.rs b/crates/jolt-field-two/src/solinas/parallel.rs index daa8cbc7f6..a032496e61 100644 --- a/crates/jolt-field-two/src/solinas/parallel.rs +++ b/crates/jolt-field-two/src/solinas/parallel.rs @@ -11,7 +11,7 @@ //! //! Consumer audit (checkpoint 9): no crate in the workspace, and nothing in //! this rebuild, currently expands any of these macros — see the -//! dropped-specialization notes in `SPEC.md`. The component is ported whole +//! dropped-specialization notes in `specs/jolt-field-rebuild.md`. The component is ported whole //! because the approved parity scope names the rayon helpers and all seven //! macros are equally (un)consumed, leaving no evidence basis for a partial //! subset. diff --git a/crates/jolt-field-two/src/solinas/unreduced.rs b/crates/jolt-field-two/src/solinas/unreduced.rs index 9602720900..454d7a3c54 100644 --- a/crates/jolt-field-two/src/solinas/unreduced.rs +++ b/crates/jolt-field-two/src/solinas/unreduced.rs @@ -24,7 +24,7 @@ //! The baseline's NEON intrinsic Add/Sub/Neg lane paths are dropped: LLVM //! auto-vectorizes the element-wise `[i32; N]` code to the identical //! `add.4s`/`sub.4s`/`neg.4s` (and `mul.4s` for scaling) instructions at -//! opt-level 3 (see SPEC.md dropped-specialization evidence). +//! opt-level 3 (see specs/jolt-field-rebuild.md dropped-specialization evidence). use super::{Fp128, Fp32, Fp64, FpExt2, FpExt4, FpExt8}; use crate::{ diff --git a/crates/jolt-field-two/src/solinas/word.rs b/crates/jolt-field-two/src/solinas/word.rs index 1dc7a77ac9..5068ae573e 100644 --- a/crates/jolt-field-two/src/solinas/word.rs +++ b/crates/jolt-field-two/src/solinas/word.rs @@ -347,7 +347,7 @@ macro_rules! define_solinas_prime { // The ext-mul kernel hooks keep their generic-schedule defaults: // the baseline's fused u128-accumulation Fp32 override lost the - // checkpoint-6 bench gate (see SPEC.md dropped-specialization + // checkpoint-6 bench gate (see specs/jolt-field-rebuild.md dropped-specialization // evidence and benches/ext4_kernels.rs). impl PseudoMersenne for $name

{ const OFFSET: u128 = Self::C as u128; diff --git a/crates/jolt-field-two/SPEC.md b/specs/jolt-field-rebuild.md similarity index 100% rename from crates/jolt-field-two/SPEC.md rename to specs/jolt-field-rebuild.md From db8e650a9590449112037d7943451ab30eb08c85 Mon Sep 17 00:00:00 2001 From: acentelles Date: Fri, 31 Jul 2026 15:20:14 -0400 Subject: [PATCH 27/38] feat(jolt-field-two): CanonicalBytes supertrait split + akita bootstrap adapter Pre-replacement surface work: split the byte surface out of CanonicalEncoding as a bare CanonicalBytes supertrait (same decision as the jolt-field split: transcript absorption must not require the field decode contract, e.g. NoCommitment), and port the temporary akita bootstrap adapter to the new spine so the akita lanes survive the replacement. 121/121 tests, clippy clean on all feature lanes. --- Cargo.lock | 2 + crates/jolt-field-two/Cargo.toml | 5 + crates/jolt-field-two/src/akita.rs | 115 ++++++++++++++++++ crates/jolt-field-two/src/algebra.rs | 42 ++++--- crates/jolt-field-two/src/bn254/mod.rs | 13 +- crates/jolt-field-two/src/lib.rs | 17 +-- crates/jolt-field-two/src/ops.rs | 4 +- crates/jolt-field-two/src/solinas/fp128.rs | 11 +- crates/jolt-field-two/src/solinas/word.rs | 9 +- .../tests/bn254_differential.rs | 2 +- .../tests/solinas_ext_differential.rs | 4 +- .../tests/solinas_fp128_differential.rs | 10 +- .../tests/solinas_words_differential.rs | 10 +- crates/jolt-field-two/tests/spine.rs | 11 +- 14 files changed, 202 insertions(+), 53 deletions(-) create mode 100644 crates/jolt-field-two/src/akita.rs diff --git a/Cargo.lock b/Cargo.lock index de4d9314ec..46ab0cddbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3315,6 +3315,8 @@ dependencies = [ name = "jolt-field-two" version = "0.1.0" dependencies = [ + "akita-config", + "akita-field", "allocative", "ark-bn254 0.5.0 (git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout)", "ark-ff 0.5.0 (git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout)", diff --git a/crates/jolt-field-two/Cargo.toml b/crates/jolt-field-two/Cargo.toml index 2621969c3f..6dcb9907c0 100644 --- a/crates/jolt-field-two/Cargo.toml +++ b/crates/jolt-field-two/Cargo.toml @@ -12,6 +12,10 @@ categories = ["cryptography"] workspace = true [dependencies] +# Temporary bootstrap edge for the pre-cutover akita-field types; removed in +# the final migration PR together with the `akita` feature. +akita-config = { workspace = true, optional = true } +akita-field = { workspace = true, optional = true } ark-ff = { workspace = true, optional = true } ark-serialize = { workspace = true, optional = true } ark-bn254 = { workspace = true, features = ["curve"], optional = true } @@ -24,6 +28,7 @@ thiserror = { workspace = true } [features] default = ["bn254"] +akita = ["dep:akita-config", "dep:akita-field"] bn254 = ["dep:ark-ff", "dep:ark-serialize", "dep:ark-bn254"] solinas = [] parallel = ["dep:rayon"] diff --git a/crates/jolt-field-two/src/akita.rs b/crates/jolt-field-two/src/akita.rs new file mode 100644 index 0000000000..6c732d8230 --- /dev/null +++ b/crates/jolt-field-two/src/akita.rs @@ -0,0 +1,115 @@ +//! Temporary bootstrap adapter for the pre-cutover `akita-field` type. +//! +//! Implements this crate's contracts for Akita's proof-optimized fp128 field +//! so the adapter stays buildable until the Akita cutover; it is a bootstrap +//! edge, not the target architecture, and is removed in the final migration +//! PR together with the `akita` feature. + +use akita_config::proof_optimized::fp128::Field as AkitaField; +use rand_core::RngCore; + +use crate::{ + AdditiveGroup, CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, + WithAccumulator, +}; + +impl AdditiveGroup for AkitaField {} + +impl Ring for AkitaField { + #[inline] + fn from_u64(v: u64) -> Self { + ::from_u64(v) + } + + #[inline] + fn from_i64(v: i64) -> Self { + ::from_i64(v) + } + + #[inline] + fn from_u128(v: u128) -> Self { + ::from_u128(v) + } + + #[inline] + fn from_i128(v: i128) -> Self { + ::from_i128(v) + } +} + +impl Field for AkitaField { + #[inline] + fn inverse(&self) -> Option { + ::inverse(self) + } + + #[inline] + fn random(rng: &mut R) -> Self { + ::random(rng) + } +} + +impl CanonicalBytes for AkitaField { + const NUM_BYTES: usize = ::NUM_BYTES; + + #[inline(always)] + fn to_bytes_le(&self, out: &mut [u8]) { + ::to_bytes_le(self, out); + } +} + +impl CanonicalEncoding for AkitaField { + // Akita's proof-optimized field is a 128-bit pseudo-Mersenne prime. + const MODULUS_BITS: u32 = 128; + + #[inline(always)] + fn from_bytes_le_reduced(bytes: &[u8]) -> Self { + ::from_le_bytes_mod_order(bytes) + } + + #[inline] + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + if bytes.len() != ::NUM_BYTES { + return None; + } + let value = Self::from_bytes_le_reduced(bytes); + // Canonical iff decoding round-trips to the identical bytes. + (value.to_bytes_le_vec() == bytes).then_some(value) + } + + #[inline] + fn to_u128_checked(&self) -> Option { + let mut buf = [0u8; 16]; + CanonicalBytes::to_bytes_le(self, &mut buf); + Some(u128::from_le_bytes(buf)) + } + + #[inline] + fn from_u128_checked(v: u128) -> Option { + let value = ::from_u128(v); + (value.to_u128_checked() == Some(v)).then_some(value) + } + + #[inline] + fn from_u128_reduced(v: u128) -> Self { + ::from_u128(v) + } + + #[inline] + fn num_bits(&self) -> u32 { + ::num_bits(self) + } + + /// Legacy convention: digest bytes are interpreted as a big-endian + /// integer before reduction. + #[inline] + fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { + let mut buf = bytes.to_vec(); + buf.reverse(); + Self::from_bytes_le_reduced(&buf) + } +} + +impl WithAccumulator for AkitaField { + type Accumulator = NaiveAccumulator; +} diff --git a/crates/jolt-field-two/src/algebra.rs b/crates/jolt-field-two/src/algebra.rs index dd5c4b711a..3c602ef5c5 100644 --- a/crates/jolt-field-two/src/algebra.rs +++ b/crates/jolt-field-two/src/algebra.rs @@ -231,30 +231,25 @@ pub trait PseudoMersenne: Field + CanonicalEncoding { } } -/// Canonical little-endian representation: the Fiat-Shamir transcript surface -/// and the single source of canonicity for wire serialization. +/// Fixed-size canonical little-endian byte encoding: the transcript +/// absorption surface. /// -/// Transcript absorption and challenge derivation use these explicit -/// encodings so the hashed byte stream is specified independently of any -/// serialization library. Proof/wire serialization goes through serde + -/// bincode, reusing [`from_bytes_le_checked`](Self::from_bytes_le_checked) -/// so non-canonical encodings are rejected uniformly. +/// This is deliberately the *narrow* claim, "this value has one canonical +/// byte encoding", implementable by non-field types (e.g. zero-sized +/// commitment placeholders) that must be transcript-absorbable without +/// pretending to be decodable field elements. Field types get the full +/// decode surface via [`CanonicalEncoding`]. /// /// # Invariants /// -/// - The encoding is injective on canonical representatives: equal elements -/// produce equal bytes, distinct elements produce distinct bytes. +/// - The encoding is injective on canonical representatives: equal values +/// produce equal bytes, distinct values produce distinct bytes. /// - [`to_bytes_le`](Self::to_bytes_le) always writes exactly /// [`NUM_BYTES`](Self::NUM_BYTES) bytes of the unique representative. -pub trait CanonicalEncoding: - Sized + Copy + Default + PartialEq + Eq + Debug + Hash + Send + Sync + 'static -{ +pub trait CanonicalBytes { /// Byte length of the fixed-size canonical encoding. const NUM_BYTES: usize; - /// Bit length of the field order `|F|` (for prime fields, the modulus). - const MODULUS_BITS: u32; - /// Writes the canonical little-endian encoding into `out`. fn to_bytes_le(&self, out: &mut [u8]); @@ -265,6 +260,23 @@ pub trait CanonicalEncoding: self.to_bytes_le(&mut out); out } +} + +/// Canonical decode-and-introspect surface of a field element, on top of the +/// [`CanonicalBytes`] encoding: the single source of canonicity for wire +/// serialization. +/// +/// Transcript absorption and challenge derivation use the explicit +/// [`CanonicalBytes`] encoding so the hashed byte stream is specified +/// independently of any serialization library. Proof/wire serialization goes +/// through serde + bincode, reusing +/// [`from_bytes_le_checked`](Self::from_bytes_le_checked) so non-canonical +/// encodings are rejected uniformly. +pub trait CanonicalEncoding: + CanonicalBytes + Sized + Copy + Default + PartialEq + Eq + Debug + Hash + Send + Sync + 'static +{ + /// Bit length of the field order `|F|` (for prime fields, the modulus). + const MODULUS_BITS: u32; /// Decodes little-endian bytes of any length by reducing into the field. fn from_bytes_le_reduced(bytes: &[u8]) -> Self; diff --git a/crates/jolt-field-two/src/bn254/mod.rs b/crates/jolt-field-two/src/bn254/mod.rs index 4b2b11cecd..7557e1b0de 100644 --- a/crates/jolt-field-two/src/bn254/mod.rs +++ b/crates/jolt-field-two/src/bn254/mod.rs @@ -10,7 +10,7 @@ mod mont; pub use mont::WideAccumulator; -use crate::{CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; +use crate::{CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; use ark_ff::{BigInteger, PrimeField, UniformRand}; use rand_core::RngCore; @@ -94,18 +94,21 @@ macro_rules! wrap_bn254 { } } - impl CanonicalEncoding for $ty { + impl CanonicalBytes for $ty { const NUM_BYTES: usize = 32; - const MODULUS_BITS: u32 = 254; #[inline] fn to_bytes_le(&self, out: &mut [u8]) { - assert_eq!(out.len(), ::NUM_BYTES); + assert_eq!(out.len(), ::NUM_BYTES); use ark_serialize::CanonicalSerialize; self.0 .serialize_compressed(out) .expect("BN254 element serializes to 32 bytes"); } + } + + impl CanonicalEncoding for $ty { + const MODULUS_BITS: u32 = 254; #[inline] fn from_bytes_le_reduced(bytes: &[u8]) -> Self { @@ -115,7 +118,7 @@ macro_rules! wrap_bn254 { #[inline] fn from_bytes_le_checked(bytes: &[u8]) -> Option { use ark_serialize::CanonicalDeserialize; - if bytes.len() != ::NUM_BYTES { + if bytes.len() != ::NUM_BYTES { return None; } <$inner>::deserialize_compressed(bytes).ok().map($ty) diff --git a/crates/jolt-field-two/src/lib.rs b/crates/jolt-field-two/src/lib.rs index 31fb32944d..cbe61ae81c 100644 --- a/crates/jolt-field-two/src/lib.rs +++ b/crates/jolt-field-two/src/lib.rs @@ -1,8 +1,9 @@ //! Field and ring abstractions for the Jolt zkVM. //! //! A slim algebraic ladder — [`AdditiveGroup`] → [`Ring`] → [`Field`] — with -//! orthogonal capabilities: [`CanonicalEncoding`] (the Fiat-Shamir transcript -//! surface) and [`WithAccumulator`] (deferred-reduction fused multiply-add). +//! orthogonal capabilities: [`CanonicalBytes`]/[`CanonicalEncoding`] (the +//! Fiat-Shamir transcript surface and the field decode surface on top of it) +//! and [`WithAccumulator`] (deferred-reduction fused multiply-add). //! [`JoltField`] is the blanket-implemented bundle of everything Jolt's //! protocol stack requires of a scalar field: `Field + CanonicalEncoding + //! WithAccumulator + Serialize + DeserializeOwned`. Because the impl is a @@ -61,13 +62,15 @@ //! little-endian bytes (see [`impl_serde_bytes!`]); deserialization //! rejects non-canonical encodings uniformly via //! [`CanonicalEncoding::from_bytes_le_checked`]. -//! - Fiat-Shamir transcript bytes use [`CanonicalEncoding`]'s explicit -//! little-endian encoding ([`CanonicalEncoding::to_bytes_le`]) and never -//! go through a serialization library. +//! - Fiat-Shamir transcript bytes use the explicit little-endian encoding +//! ([`CanonicalBytes::to_bytes_le`]) and never go through a serialization +//! library. //! //! Both invariants are enforced by differential tests against `jolt-field` //! as the oracle (`tests/*_differential.rs`). +#[cfg(feature = "akita")] +mod akita; mod algebra; #[cfg(feature = "bn254")] mod bn254; @@ -82,8 +85,8 @@ pub mod solinas; mod unreduced; pub use algebra::{ - Accumulator, AdditiveGroup, CanonicalEncoding, Field, JoltField, NaiveAccumulator, - PseudoMersenne, Ring, WithAccumulator, + Accumulator, AdditiveGroup, CanonicalBytes, CanonicalEncoding, Field, JoltField, + NaiveAccumulator, PseudoMersenne, Ring, WithAccumulator, }; #[cfg(feature = "bn254")] pub use bn254::{Fq, Fr, WideAccumulator}; diff --git a/crates/jolt-field-two/src/ops.rs b/crates/jolt-field-two/src/ops.rs index 907e9c907b..11792ac5b9 100644 --- a/crates/jolt-field-two/src/ops.rs +++ b/crates/jolt-field-two/src/ops.rs @@ -170,9 +170,9 @@ macro_rules! impl_serde_bytes { (impl[$($g:tt)*] $ty:ty, $n:expr) => { impl<$($g)*> ::serde::Serialize for $ty { fn serialize(&self, serializer: S) -> Result { - debug_assert_eq!($n, <$ty as $crate::CanonicalEncoding>::NUM_BYTES); + debug_assert_eq!($n, <$ty as $crate::CanonicalBytes>::NUM_BYTES); let mut buf = [0u8; $n]; - $crate::CanonicalEncoding::to_bytes_le(self, &mut buf); + $crate::CanonicalBytes::to_bytes_le(self, &mut buf); <[u8; $n]>::serialize(&buf, serializer) } } diff --git a/crates/jolt-field-two/src/solinas/fp128.rs b/crates/jolt-field-two/src/solinas/fp128.rs index 684f129342..a5e67b6598 100644 --- a/crates/jolt-field-two/src/solinas/fp128.rs +++ b/crates/jolt-field-two/src/solinas/fp128.rs @@ -17,7 +17,7 @@ use super::word::mul64_wide; use crate::PseudoMersenne; -use crate::{CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; +use crate::{CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; use rand_core::RngCore; #[cfg(target_arch = "aarch64")] use std::arch::asm; @@ -714,16 +714,19 @@ impl Field for Fp128

{ } } -impl CanonicalEncoding for Fp128

{ +impl CanonicalBytes for Fp128

{ const NUM_BYTES: usize = 16; - // C < 2^32 implies p > 2^127, so the modulus is exactly 128 bits. - const MODULUS_BITS: u32 = 128; #[inline(always)] fn to_bytes_le(&self, out: &mut [u8]) { assert_eq!(out.len(), Self::NUM_BYTES); out.copy_from_slice(&join(self.0).to_le_bytes()); } +} + +impl CanonicalEncoding for Fp128

{ + // C < 2^32 implies p > 2^127, so the modulus is exactly 128 bits. + const MODULUS_BITS: u32 = 128; #[inline(always)] fn from_bytes_le_reduced(bytes: &[u8]) -> Self { diff --git a/crates/jolt-field-two/src/solinas/word.rs b/crates/jolt-field-two/src/solinas/word.rs index 5068ae573e..105714aad2 100644 --- a/crates/jolt-field-two/src/solinas/word.rs +++ b/crates/jolt-field-two/src/solinas/word.rs @@ -9,7 +9,7 @@ //! path for sub-word primes, with a BMI2 variant on x86-64). use crate::PseudoMersenne; -use crate::{CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; +use crate::{CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; use rand_core::RngCore; /// Trial-division primality check, cheap enough for CTFE at u32 scale. @@ -292,15 +292,18 @@ macro_rules! define_solinas_prime { } } - impl CanonicalEncoding for $name

{ + impl CanonicalBytes for $name

{ const NUM_BYTES: usize = (<$word>::BITS / 8) as usize; - const MODULUS_BITS: u32 = Self::BITS; #[inline(always)] fn to_bytes_le(&self, out: &mut [u8]) { assert_eq!(out.len(), Self::NUM_BYTES); out.copy_from_slice(&self.0.to_le_bytes()); } + } + + impl CanonicalEncoding for $name

{ + const MODULUS_BITS: u32 = Self::BITS; #[inline(always)] fn from_bytes_le_reduced(bytes: &[u8]) -> Self { diff --git a/crates/jolt-field-two/tests/bn254_differential.rs b/crates/jolt-field-two/tests/bn254_differential.rs index 62d9bf869e..27bd289e27 100644 --- a/crates/jolt-field-two/tests/bn254_differential.rs +++ b/crates/jolt-field-two/tests/bn254_differential.rs @@ -14,7 +14,7 @@ use base::{ }; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use two::{Accumulator as _, CanonicalEncoding, Field as _, Ring}; +use two::{Accumulator as _, CanonicalBytes as _, CanonicalEncoding, Field as _, Ring}; fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0xb254_b254) diff --git a/crates/jolt-field-two/tests/solinas_ext_differential.rs b/crates/jolt-field-two/tests/solinas_ext_differential.rs index 3a3198f231..af6067bc53 100644 --- a/crates/jolt-field-two/tests/solinas_ext_differential.rs +++ b/crates/jolt-field-two/tests/solinas_ext_differential.rs @@ -24,7 +24,7 @@ use base::{ use num_traits::{One, Zero}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use two::{CanonicalEncoding, ExtField, Field, Ring}; +use two::{CanonicalBytes, CanonicalEncoding, ExtField, Field, Ring}; fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0xE87_D1FF) @@ -331,7 +331,7 @@ macro_rules! check_ext { // Canonical rejection: a wire encoding whose first coefficient is // `p` itself must be rejected by both crates; so must short input. - let nb = <$F2 as CanonicalEncoding>::NUM_BYTES; + let nb = <$F2 as CanonicalBytes>::NUM_BYTES; let mut bad = vec![0u8; nb * d]; bad[..nb].copy_from_slice(&p.to_le_bytes()[..nb]); assert!( diff --git a/crates/jolt-field-two/tests/solinas_fp128_differential.rs b/crates/jolt-field-two/tests/solinas_fp128_differential.rs index 565b9ada74..08ec6899e8 100644 --- a/crates/jolt-field-two/tests/solinas_fp128_differential.rs +++ b/crates/jolt-field-two/tests/solinas_fp128_differential.rs @@ -10,12 +10,12 @@ use jolt_field as base; use jolt_field_two as two; use base::{ - CanonicalBytes, CanonicalField, CanonicalRepr, FieldCore, FromPrimitiveInt, HalvingField, + CanonicalBytes as BaseCanonicalBytes, CanonicalField, CanonicalRepr, FieldCore, FromPrimitiveInt, HalvingField, PseudoMersenneField, RingCore, }; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use two::{Accumulator as _, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, Ring}; +use two::{Accumulator as _, CanonicalBytes, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, Ring}; fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0xf128_a5a5) @@ -101,10 +101,10 @@ macro_rules! check_prime128 { <$base as PseudoMersenneField>::MODULUS_OFFSET ); assert_eq!( - <$two as CanonicalEncoding>::NUM_BYTES, - <$base as CanonicalBytes>::NUM_BYTES + <$two as CanonicalBytes>::NUM_BYTES, + <$base as BaseCanonicalBytes>::NUM_BYTES ); - assert_eq!(<$two as CanonicalEncoding>::NUM_BYTES, 16); + assert_eq!(<$two as CanonicalBytes>::NUM_BYTES, 16); let cfg = bincode::config::standard(); for _ in 0..200 { diff --git a/crates/jolt-field-two/tests/solinas_words_differential.rs b/crates/jolt-field-two/tests/solinas_words_differential.rs index 574046329b..238a3bf05e 100644 --- a/crates/jolt-field-two/tests/solinas_words_differential.rs +++ b/crates/jolt-field-two/tests/solinas_words_differential.rs @@ -9,12 +9,12 @@ use jolt_field as base; use jolt_field_two as two; use base::{ - CanonicalBytes, CanonicalField, CanonicalRepr, FromPrimitiveInt, HalvingField, + CanonicalBytes as BaseCanonicalBytes, CanonicalField, CanonicalRepr, FromPrimitiveInt, HalvingField, PseudoMersenneField, RingCore, }; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use two::{Accumulator as _, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, Ring}; +use two::{Accumulator as _, CanonicalBytes, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, Ring}; fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0x5011_a5a5) @@ -48,8 +48,8 @@ macro_rules! check_prime { <$base as PseudoMersenneField>::MODULUS_OFFSET ); assert_eq!( - <$two as CanonicalEncoding>::NUM_BYTES, - <$base as CanonicalBytes>::NUM_BYTES + <$two as CanonicalBytes>::NUM_BYTES, + <$base as BaseCanonicalBytes>::NUM_BYTES ); let cfg = bincode::config::standard(); @@ -176,7 +176,7 @@ macro_rules! check_prime { ); // Non-canonical wire encodings rejected (encode p itself). - let n = <$two as CanonicalEncoding>::NUM_BYTES; + let n = <$two as CanonicalBytes>::NUM_BYTES; let p_bytes = &p.to_le_bytes()[..n]; assert_eq!( <$two as CanonicalEncoding>::from_bytes_le_checked(p_bytes), diff --git a/crates/jolt-field-two/tests/spine.rs b/crates/jolt-field-two/tests/spine.rs index bc8064a1fe..757205995b 100644 --- a/crates/jolt-field-two/tests/spine.rs +++ b/crates/jolt-field-two/tests/spine.rs @@ -7,8 +7,8 @@ #![expect(clippy::unwrap_used, reason = "test code")] use jolt_field_two::{ - impl_ring_ops, impl_serde_bytes, Accumulator, CanonicalEncoding, Field, JoltField, - NaiveAccumulator, One, Ring, WithAccumulator, Zero, + impl_ring_ops, impl_serde_bytes, Accumulator, CanonicalBytes, CanonicalEncoding, Field, + JoltField, NaiveAccumulator, One, Ring, WithAccumulator, Zero, }; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; @@ -80,12 +80,15 @@ impl Field for M61 { } } -impl CanonicalEncoding for M61 { +impl CanonicalBytes for M61 { const NUM_BYTES: usize = 8; - const MODULUS_BITS: u32 = 61; fn to_bytes_le(&self, out: &mut [u8]) { out.copy_from_slice(&self.0.to_le_bytes()); } +} + +impl CanonicalEncoding for M61 { + const MODULUS_BITS: u32 = 61; fn from_bytes_le_reduced(bytes: &[u8]) -> Self { let base = M61::from_u64(256); bytes From 2b800e1ca0565154562ef57b7ee5960233ef0d93 Mon Sep 17 00:00:00 2001 From: acentelles Date: Fri, 31 Jul 2026 16:21:33 -0400 Subject: [PATCH 28/38] test(jolt-field-two): oracle-free test suite (num-bigint oracles + golden byte fixtures) - tests/bn254_differential.rs: jolt-field oracle replaced with num-bigint arithmetic mod r/q (modpow inverses, exact wide-accumulator sums, and the legacy Fr Montgomery-form / Fq plain challenge models); wire bytes now checked structurally, absolute bytes pinned in golden_bytes.rs. - tests/limbs_signed_differential.rs: Limbs/SignedBigInt/SignedBigIntHi32 parity vs jolt-field replaced with exact num-bigint integer oracles (wrapping sign-magnitude model); existing u128/i128 oracles kept. - tests/solinas_words_differential.rs: baseline arms dropped; u128/bigint oracles extended to half, num_bits, challenge decodes, balanced-digit LUT, and the random-stream spec; registry and PRIME_OFFSET_* constants pinned as fixture data. - tests/solinas_fp128_differential.rs: baseline arms dropped; limb oracle extended to wide multiplies, conversions, and decodes; the rejection-sampling stream checked against a test-local reimplementation of its spec. - tests/solinas_ext_differential.rs: baseline parity dropped; schoolbook oracle extended to add/sub/neg and integer embeddings; frobenius_pow checked against x^(q^k) computed with the oracle-verified multiply; Moore checks made intrinsic (basis thetas, solution satisfies system, rejections, must-succeed in genuine fields). - tests/solinas_unreduced_differential.rs: baseline HasUnreducedOps arms dropped; delayed-sum exactness kept vs the schoolbook oracle and per-term ring ops; SUM_IS_EXACT values pinned; fold_one checked against the field identity e + r(o - e). - tests/solinas_packed_differential.rs: aarch64 baseline_diff module removed (packed==scalar plus scalar==oracle in the other suites covers it transitively); expected NEON lane widths pinned. - tests/golden_bytes.rs: NEW. Golden fixtures generated from jolt-field at 5b3e39ece1c27586a1f7cc77f24e718cb5d73e10 pinning bincode and to_bytes_le encodings for every registered prime field, the ext towers over three base widths, BN254 Fr/Fq, and the legacy Fr/Fq challenge derivations (generator deleted after use; regeneration instructions in the file header). - tests/parallel_macros.rs, tests/spine.rs: untouched (no oracle use). - benches/ext4_kernels.rs: jolt-field baseline timing columns removed; the generic-vs-fused-port comparison is unchanged. - Cargo.toml: jolt-field dev-dependency removed; num-bigint (workspace) added as a dev-dependency. --- Cargo.lock | 2 +- crates/jolt-field-two/Cargo.toml | 3 +- crates/jolt-field-two/benches/ext4_kernels.rs | 31 +- .../tests/bn254_differential.rs | 311 +++-- crates/jolt-field-two/tests/golden_bytes.rs | 1056 +++++++++++++++++ .../tests/limbs_signed_differential.rs | 267 +++-- .../tests/solinas_ext_differential.rs | 327 +++-- .../tests/solinas_fp128_differential.rs | 224 ++-- .../tests/solinas_packed_differential.rs | 304 +---- .../tests/solinas_unreduced_differential.rs | 209 +--- .../tests/solinas_words_differential.rs | 307 +++-- 11 files changed, 1861 insertions(+), 1180 deletions(-) create mode 100644 crates/jolt-field-two/tests/golden_bytes.rs diff --git a/Cargo.lock b/Cargo.lock index 46ab0cddbf..7dd8ed1d0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3322,7 +3322,7 @@ dependencies = [ "ark-ff 0.5.0 (git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout)", "ark-serialize 0.5.0 (git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout)", "bincode 2.0.1", - "jolt-field", + "num-bigint", "num-traits", "rand 0.8.7", "rand_chacha 0.3.1", diff --git a/crates/jolt-field-two/Cargo.toml b/crates/jolt-field-two/Cargo.toml index 6dcb9907c0..b8c40dfedc 100644 --- a/crates/jolt-field-two/Cargo.toml +++ b/crates/jolt-field-two/Cargo.toml @@ -36,8 +36,7 @@ allocative = ["dep:allocative"] [dev-dependencies] bincode = { workspace = true } -# Differential-testing oracle: the crate this one is rebuilding. -jolt-field = { path = "../jolt-field", features = ["solinas"] } +num-bigint = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } diff --git a/crates/jolt-field-two/benches/ext4_kernels.rs b/crates/jolt-field-two/benches/ext4_kernels.rs index fe503c9c60..8be5cbaf60 100644 --- a/crates/jolt-field-two/benches/ext4_kernels.rs +++ b/crates/jolt-field-two/benches/ext4_kernels.rs @@ -2,8 +2,9 @@ //! acceptance): the generic coefficient-formula schedule (what the crate //! ships as the `PseudoMersenne` hook default) vs a local port of the //! baseline's fused u128-accumulation `Fp32` override, on batched degree-4 -//! muls and squares over `Prime32Offset99`. The baseline `FpExt4` -//! (which ships the fused override) is included for context. +//! muls and squares over `Prime32Offset99`. (The original jolt-field +//! baseline, which shipped the fused override, timed identically to the +//! local port while both crates coexisted.) //! //! Outcome recorded in specs/jolt-field-rebuild.md: the fused port LOST on aarch64/Apple M4 //! (generic ≈ 2.5x faster on mul, ≈ 1.85x on square; the port reproduces @@ -13,13 +14,8 @@ //! //! Run: `cargo bench -p jolt-field-two --features solinas --bench ext4_kernels` -#![expect( - clippy::unwrap_used, - clippy::print_stdout, - reason = "bench harness: canonical conversions of canonical values; stdout is the report" -)] +#![expect(clippy::print_stdout, reason = "bench harness: stdout is the report")] -use jolt_field as base; use jolt_field_two as two; use rand::SeedableRng; @@ -31,8 +27,6 @@ use two::{CanonicalEncoding, Field, Ring}; type Fp = two::Prime32Offset99; type E4 = two::FpExt4; -type BaseFp = base::Prime32Offset99; -type BaseE4 = base::FpExt4; const N: usize = 1 << 12; const REPS: usize = 100; @@ -132,19 +126,6 @@ fn main() { let pairs: Vec<(E4, E4)> = (0..N) .map(|_| (E4::random(&mut rng), E4::random(&mut rng))) .collect(); - let base_pairs: Vec<(BaseE4, BaseE4)> = pairs - .iter() - .map(|(a, b)| { - let conv = |x: &E4| { - BaseE4::new(x.coeffs.map(|c| { - base::CanonicalField::from_canonical_u128_checked(c.to_u128_checked().unwrap()) - .unwrap() - })) - }; - (conv(a), conv(b)) - }) - .collect(); - // Sanity: the fused port agrees with the wired generic path. for (a, b) in pairs.iter().take(64) { assert_eq!((*a * *b).coeffs, fused_mul(a.coeffs, b.coeffs)); @@ -153,23 +134,19 @@ fn main() { let generic_mul_ns = measure(&pairs, |(a, b)| (a * b).coeffs[0]); let fused_mul_ns = measure(&pairs, |(a, b)| fused_mul(a.coeffs, b.coeffs)[0]); - let base_mul_ns = measure(&base_pairs, |(a, b)| (a * b).coeffs[0]); let generic_sq_ns = measure(&pairs, |(a, _)| Ring::square(&a).coeffs[0]); let fused_sq_ns = measure(&pairs, |(a, _)| fused_square(a.coeffs)[0]); - let base_sq_ns = measure(&base_pairs, |(a, _)| base::RingCore::square(&a).coeffs[0]); println!("ext4 over Prime32Offset99, {N} elements x {REPS} reps, best of {TRIALS}"); println!(" mul generic default (wired): {generic_mul_ns:7.2} ns/op"); println!(" mul fused port (dropped) : {fused_mul_ns:7.2} ns/op"); - println!(" mul baseline fused override: {base_mul_ns:7.2} ns/op"); println!( " mul fused/generic : {:.2}x", fused_mul_ns / generic_mul_ns ); println!(" square generic default (wired): {generic_sq_ns:7.2} ns/op"); println!(" square fused port (dropped) : {fused_sq_ns:7.2} ns/op"); - println!(" square baseline fused override: {base_sq_ns:7.2} ns/op"); println!( " square fused/generic : {:.2}x", fused_sq_ns / generic_sq_ns diff --git a/crates/jolt-field-two/tests/bn254_differential.rs b/crates/jolt-field-two/tests/bn254_differential.rs index 27bd289e27..631c4ee53e 100644 --- a/crates/jolt-field-two/tests/bn254_differential.rs +++ b/crates/jolt-field-two/tests/bn254_differential.rs @@ -1,17 +1,14 @@ -//! Differential tests: jolt-field-two's BN254 backend against jolt-field. -//! -//! jolt-field (the crate being rebuilt) is the oracle: every operation, -//! serde byte stream, and transcript byte stream must match exactly. +//! Differential tests: jolt-field-two's BN254 backend against exact +//! num-bigint modular arithmetic. The canonical value of an element is read +//! through `to_bytes_le`, whose faithfulness is pinned by the golden +//! fixtures in golden_bytes.rs. #![cfg(feature = "bn254")] #![expect(clippy::unwrap_used, reason = "test code")] -use jolt_field as base; use jolt_field_two as two; -use base::{ - Accumulator as _, CanonicalBytes, CanonicalRepr, FieldCore, FromPrimitiveInt, RingCore, -}; +use num_bigint::{BigInt, BigUint, Sign}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; use two::{Accumulator as _, CanonicalBytes as _, CanonicalEncoding, Field as _, Ring}; @@ -20,59 +17,98 @@ fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0xb254_b254) } -/// Sample a matched (baseline, rebuilt) element pair from the same bytes. -fn sample_pair(rng: &mut ChaCha20Rng) -> (base::Fr, two::Fr) { - let bytes: [u8; 32] = rng.gen(); - let b = ::from_le_bytes_mod_order(&bytes); - let t = ::from_bytes_le_reduced(&bytes); - assert_matches(t, b); - (b, t) +/// BN254 scalar-field modulus r. +fn p_fr() -> BigUint { + BigUint::parse_bytes( + b"30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001", + 16, + ) + .unwrap() +} + +/// BN254 base-field modulus q. +fn p_fq() -> BigUint { + BigUint::parse_bytes( + b"30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47", + 16, + ) + .unwrap() +} + +/// Canonical value of an element via its (fixture-pinned) LE encoding. +fn val(x: &F) -> BigUint { + BigUint::from_bytes_le(&x.to_bytes_le_vec()) } -/// Byte-level equality between the two crates' elements. -fn assert_matches(ours: two::Fr, theirs: base::Fr) { - assert_eq!(ours.to_bytes_le_vec(), theirs.to_bytes_le_vec()); +fn assert_val(x: &F, expected: &BigUint) { + assert_eq!(val(x), *expected); +} + +/// `v mod p` for a possibly negative BigInt. +fn imod(v: &BigInt, p: &BigUint) -> BigUint { + let p_int = BigInt::from_biguint(Sign::Plus, p.clone()); + let r = ((v % &p_int) + &p_int) % &p_int; + r.to_biguint().unwrap() +} + +/// Sample an element together with its oracle value from the same bytes. +fn sample_fr(rng: &mut ChaCha20Rng, p: &BigUint) -> (two::Fr, BigUint) { + let bytes: [u8; 32] = rng.gen(); + let t = ::from_bytes_le_reduced(&bytes); + let v = BigUint::from_bytes_le(&bytes) % p; + assert_val(&t, &v); + (t, v) } -fn assert_matches_fq(ours: two::Fq, theirs: base::Fq) { - assert_eq!(ours.to_bytes_le_vec(), theirs.to_bytes_le_vec()); +#[test] +fn moduli_are_consistent() { + // The hardcoded moduli agree with the crate: -1 encodes p − 1, and the + // reducing decode sends p to zero. + for (minus_one_bytes, p) in [ + (two::Fr::from_i64(-1).to_bytes_le_vec(), p_fr()), + (two::Fq::from_i64(-1).to_bytes_le_vec(), p_fq()), + ] { + assert_eq!(BigUint::from_bytes_le(&minus_one_bytes) + 1u32, p); + } } #[test] fn arithmetic_matches() { + let p = p_fr(); let mut rng = rng(); for _ in 0..500 { - let (b1, t1) = sample_pair(&mut rng); - let (b2, t2) = sample_pair(&mut rng); - assert_matches(t1 + t2, b1 + b2); - assert_matches(t1 - t2, b1 - b2); - assert_matches(t1 * t2, b1 * b2); - assert_matches(-t1, -b1); - assert_matches(Ring::square(&t1), RingCore::square(&b1)); - let (ti, bi) = (t1.inverse(), b1.inverse()); - assert_eq!(ti.is_some(), bi.is_some(), "inverse disagreement"); - if let (Some(ti), Some(bi)) = (ti, bi) { - assert_matches(ti, bi); + let (t1, v1) = sample_fr(&mut rng, &p); + let (t2, v2) = sample_fr(&mut rng, &p); + assert_val(&(t1 + t2), &((&v1 + &v2) % &p)); + assert_val(&(t1 - t2), &((&v1 + &p - &v2) % &p)); + assert_val(&(t1 * t2), &(&v1 * &v2 % &p)); + assert_val(&(-t1), &((&p - &v1) % &p)); + assert_val(&Ring::square(&t1), &(&v1 * &v1 % &p)); + match t1.inverse() { + Some(ti) => { + assert_val(&ti, &v1.modpow(&(&p - 2u32), &p)); + assert_eq!(ti * t1, two::Fr::from_u64(1)); + } + None => assert_eq!(v1, BigUint::ZERO, "inverse must exist for nonzero"), } - if t2 != two::Zero::zero() { - assert_matches(t1 / t2, b1 / b2); + if v2 != BigUint::ZERO { + let inv2 = v2.modpow(&(&p - 2u32), &p); + assert_val(&(t1 / t2), &(&v1 * inv2 % &p)); } } } #[test] fn integer_conversions_match() { + let p = p_fr(); let mut rng = rng(); let check = |v_u64: u64, v_i64: i64, v_u128: u128, v_i128: i128| { - assert_matches(two::Fr::from_u64(v_u64), FromPrimitiveInt::from_u64(v_u64)); - assert_matches(two::Fr::from_i64(v_i64), FromPrimitiveInt::from_i64(v_i64)); - assert_matches( - two::Fr::from_u128(v_u128), - FromPrimitiveInt::from_u128(v_u128), - ); - assert_matches( - two::Fr::from_i128(v_i128), - FromPrimitiveInt::from_i128(v_i128), + assert_val(&two::Fr::from_u64(v_u64), &(BigUint::from(v_u64) % &p)); + assert_val(&two::Fr::from_i64(v_i64), &imod(&BigInt::from(v_i64), &p)); + assert_val(&two::Fr::from_u128(v_u128), &(BigUint::from(v_u128) % &p)); + assert_val( + &two::Fr::from_i128(v_i128), + &imod(&BigInt::from(v_i128), &p), ); }; // Boundary values, including both sides of the Montgomery precomp table. @@ -84,74 +120,99 @@ fn integer_conversions_match() { for _ in 0..300 { check(rng.gen(), rng.gen(), rng.gen(), rng.gen()); } - assert_matches(two::Fr::from_bool(true), FromPrimitiveInt::from_bool(true)); + assert_val(&two::Fr::from_bool(true), &BigUint::from(1u32)); } #[test] fn scalar_mul_fast_paths_match() { + let p = p_fr(); let mut rng = rng(); for _ in 0..300 { - let (b, t) = sample_pair(&mut rng); + let (t, v) = sample_fr(&mut rng, &p); let s64: u64 = rng.gen(); let s128: u128 = rng.gen(); let si64: i64 = rng.gen(); let si128: i128 = rng.gen(); - assert_matches(t.mul_u64(s64), b.mul_u64(s64)); - assert_matches(t.mul_i64(si64), b.mul_i64(si64)); - assert_matches(t.mul_u128(s128), b.mul_u128(s128)); - assert_matches(t.mul_i128(si128), b.mul_i128(si128)); + assert_val(&t.mul_u64(s64), &(&v * s64 % &p)); + assert_val( + &t.mul_i64(si64), + &imod(&(BigInt::from_biguint(Sign::Plus, v.clone()) * si64), &p), + ); + assert_val(&t.mul_u128(s128), &(&v * s128 % &p)); + assert_val( + &t.mul_i128(si128), + &imod(&(BigInt::from_biguint(Sign::Plus, v.clone()) * si128), &p), + ); // Low-limb-only u128 exercises the single-round Barrett path. - assert_matches(t.mul_u128(s64 as u128), b.mul_u128(s64 as u128)); + assert_val(&t.mul_u128(s64 as u128), &(&v * s64 % &p)); for edge in [0u64, 1, 2] { - assert_matches(t.mul_u64(edge), b.mul_u64(edge)); + assert_val(&t.mul_u64(edge), &(&v * edge % &p)); } } } #[test] fn serde_bytes_match() { + let p = p_fr(); let mut rng = rng(); let cfg = bincode::config::standard(); for _ in 0..100 { - let (b, t) = sample_pair(&mut rng); - let b_bytes = bincode::serde::encode_to_vec(b, cfg).unwrap(); + let (t, v) = sample_fr(&mut rng, &p); let t_bytes = bincode::serde::encode_to_vec(t, cfg).unwrap(); - assert_eq!(b_bytes, t_bytes, "wire bytes diverge"); - // Cross-decode: each crate accepts the other's encoding. - let (b_back, _): (base::Fr, usize) = + // Wire format is the canonical 32-byte LE encoding (absolute bytes + // pinned by the golden fixtures). + assert_eq!(t_bytes, t.to_bytes_le_vec(), "wire = transcript bytes"); + let (t_back, read): (two::Fr, usize) = bincode::serde::decode_from_slice(&t_bytes, cfg).unwrap(); - let (t_back, _): (two::Fr, usize) = - bincode::serde::decode_from_slice(&b_bytes, cfg).unwrap(); - assert_matches(t_back, b_back); + assert_eq!(read, 32); + assert_val(&t_back, &v); } - // Non-canonical wire bytes rejected by both. + // Non-canonical wire bytes rejected. let bad = bincode::serde::encode_to_vec([0xffu8; 32], cfg).unwrap(); - assert!(bincode::serde::decode_from_slice::(&bad, cfg).is_err()); assert!(bincode::serde::decode_from_slice::(&bad, cfg).is_err()); } +/// The legacy 125-bit shifted challenge for Fr: the masked value is placed +/// in the two HIGH limbs of a raw Montgomery representation, so the field +/// value is `(low·2^128 + high·2^192) · R⁻¹ mod r` with `R = 2^256 mod r`. +fn fr_challenge_model(bytes: &[u8], p: &BigUint) -> BigUint { + let mut buf = [0u8; 16]; + let len = bytes.len().min(16); + buf[..len].copy_from_slice(&bytes[..len]); + let value = u128::from_le_bytes(buf); + let low = BigUint::from(value as u64); + let high = BigUint::from(((value >> 64) as u64) & (u64::MAX >> 3)); + let integer = (low << 128u32) + (high << 192u32); + let r_pow = (BigUint::from(1u32) << 256u32) % p; + let r_inv = r_pow.modpow(&(p - 2u32), p); + integer * r_inv % p +} + #[test] fn transcript_surface_matches() { + let p = p_fr(); let mut rng = rng(); for _ in 0..200 { - let (b, t) = sample_pair(&mut rng); - assert_eq!(CanonicalEncoding::num_bits(&t), CanonicalRepr::num_bits(&b)); - assert_eq!(t.to_u64_checked(), b.to_canonical_u64_checked()); + let (t, v) = sample_fr(&mut rng, &p); + assert_eq!(CanonicalEncoding::num_bits(&t) as u64, v.bits()); + let expected_u64 = + (v.bits() <= 64).then(|| v.to_u64_digits().first().copied().unwrap_or(0)); + assert_eq!(t.to_u64_checked(), expected_u64); let challenge: [u8; 16] = rng.gen(); - assert_matches( - ::from_challenge_bytes(&challenge), - ::from_challenge_bytes(&challenge), + assert_val( + &::from_challenge_bytes(&challenge), + &fr_challenge_model(&challenge, &p), ); let digest: [u8; 32] = rng.gen(); - assert_matches( - ::from_scalar_challenge_bytes(&digest), - ::from_scalar_challenge_bytes(&digest), + assert_val( + &::from_scalar_challenge_bytes(&digest), + &(BigUint::from_bytes_be(&digest) % &p), ); let wide: [u8; 48] = std::array::from_fn(|_| rng.gen()); - assert_matches( - ::from_bytes_le_reduced(&wide), - ::from_le_bytes_mod_order(&wide), + assert_val( + &::from_bytes_le_reduced(&wide), + &(BigUint::from_bytes_le(&wide) % &p), ); } // Small-value integer views agree with construction. @@ -168,81 +229,97 @@ fn transcript_surface_matches() { #[test] fn wide_accumulator_matches() { + let p = p_fr(); let mut rng = rng(); - let mut base_acc = ::Accumulator::default(); - let mut two_acc = ::Accumulator::default(); + let mut acc = ::Accumulator::default(); + let mut expect = BigUint::ZERO; for _ in 0..1000 { - let (b1, t1) = sample_pair(&mut rng); - let (b2, t2) = sample_pair(&mut rng); - base_acc.fmadd(b1, b2); - two_acc.fmadd(t1, t2); + let (t1, v1) = sample_fr(&mut rng, &p); + let (t2, v2) = sample_fr(&mut rng, &p); + acc.fmadd(t1, t2); + expect = (expect + v1 * v2) % &p; } - assert_matches(two_acc.reduce(), base_acc.reduce()); + assert_val(&acc.reduce(), &expect); - // add / small-scalar fmadds / merge parity. - let (b, t) = sample_pair(&mut rng); - let mut base_acc = ::Accumulator::default(); - let mut two_acc = ::Accumulator::default(); - base_acc.add(b); - two_acc.add(t); - base_acc.fmadd_u8(b, 200); - two_acc.fmadd_u8(t, 200); - base_acc.fmadd_u64(b, u64::MAX); - two_acc.fmadd_u64(t, u64::MAX); - base_acc.fmadd_i64(b, -12345); - two_acc.fmadd_i64(t, -12345); - base_acc.fmadd_bool(b, true); - two_acc.fmadd_bool(t, true); + // add / small-scalar fmadds / merge, mirrored in exact integers. + let (t, v) = sample_fr(&mut rng, &p); + let vi = BigInt::from_biguint(Sign::Plus, v.clone()); + let mut acc = ::Accumulator::default(); + let mut expect = BigInt::ZERO; + acc.add(t); + expect += &vi; + acc.fmadd_u8(t, 200); + expect += &vi * 200; + acc.fmadd_u64(t, u64::MAX); + expect += &vi * u64::MAX; + acc.fmadd_i64(t, -12345); + expect += &vi * -12345i64; + acc.fmadd_bool(t, true); + expect += &vi; - let mut base_other = ::Accumulator::default(); - let mut two_other = ::Accumulator::default(); - base_other.fmadd(b, b); - two_other.fmadd(t, t); - base_acc.merge(base_other); - two_acc.merge(two_other); - assert_matches(two_acc.reduce(), base_acc.reduce()); + let mut other = ::Accumulator::default(); + other.fmadd(t, t); + expect += &vi * &vi; + acc.merge(other); + assert_val(&acc.reduce(), &imod(&expect, &p)); // Empty accumulators reduce to zero. let empty = ::Accumulator::default(); assert_eq!(empty.reduce(), two::Fr::from_u64(0)); } +/// The legacy challenge for Fq places the masked value in the high limbs of +/// a checked (non-Montgomery) bigint, so the field value is the integer +/// itself: `low·2^128 + high·2^192 < q`. +fn fq_challenge_model(bytes: &[u8]) -> BigUint { + let mut buf = [0u8; 16]; + let len = bytes.len().min(16); + buf[..len].copy_from_slice(&bytes[..len]); + let value = u128::from_le_bytes(buf); + let low = BigUint::from(value as u64); + let high = BigUint::from(((value >> 64) as u64) & (u64::MAX >> 3)); + (low << 128u32) + (high << 192u32) +} + #[test] fn fq_matches() { + let p = p_fq(); let mut rng = rng(); + let cfg = bincode::config::standard(); for _ in 0..300 { let bytes: [u8; 32] = rng.gen(); - let b1 = ::from_le_bytes_mod_order(&bytes); let t1 = ::from_bytes_le_reduced(&bytes); - assert_matches_fq(t1, b1); + let v1 = BigUint::from_bytes_le(&bytes) % &p; + assert_val(&t1, &v1); let bytes2: [u8; 32] = rng.gen(); - let b2 = ::from_le_bytes_mod_order(&bytes2); let t2 = ::from_bytes_le_reduced(&bytes2); + let v2 = BigUint::from_bytes_le(&bytes2) % &p; - assert_matches_fq(t1 + t2, b1 + b2); - assert_matches_fq(t1 * t2, b1 * b2); - assert_matches_fq(-t1, -b1); - if let (Some(ti), Some(bi)) = (t1.inverse(), b1.inverse()) { - assert_matches_fq(ti, bi); + assert_val(&(t1 + t2), &((&v1 + &v2) % &p)); + assert_val(&(t1 * t2), &(&v1 * &v2 % &p)); + assert_val(&(-t1), &((&p - &v1) % &p)); + match t1.inverse() { + Some(ti) => assert_val(&ti, &v1.modpow(&(&p - 2u32), &p)), + None => assert_eq!(v1, BigUint::ZERO), } let v: u64 = rng.gen(); - assert_matches_fq(two::Fq::from_u64(v), FromPrimitiveInt::from_u64(v)); + assert_val(&two::Fq::from_u64(v), &BigUint::from(v)); let challenge: [u8; 16] = rng.gen(); - assert_matches_fq( - ::from_challenge_bytes(&challenge), - ::from_challenge_bytes(&challenge), + assert_val( + &::from_challenge_bytes(&challenge), + &fq_challenge_model(&challenge), ); - assert_matches_fq( - ::from_scalar_challenge_bytes(&challenge), - ::from_scalar_challenge_bytes(&challenge), + assert_val( + &::from_scalar_challenge_bytes(&challenge), + &(BigUint::from_bytes_be(&challenge) % &p), ); - let cfg = bincode::config::standard(); assert_eq!( bincode::serde::encode_to_vec(t1, cfg).unwrap(), - bincode::serde::encode_to_vec(b1, cfg).unwrap(), + t1.to_bytes_le_vec(), + "wire = transcript bytes" ); } } diff --git a/crates/jolt-field-two/tests/golden_bytes.rs b/crates/jolt-field-two/tests/golden_bytes.rs new file mode 100644 index 0000000000..c16e9b76a3 --- /dev/null +++ b/crates/jolt-field-two/tests/golden_bytes.rs @@ -0,0 +1,1056 @@ +//! Golden byte-compatibility fixtures. +//! +//! These pin the exact wire (bincode) and transcript (`to_bytes_le`) +//! encodings — and the BN254 legacy challenge derivations — to the byte +//! streams produced by the original `jolt-field` crate, guarding the hard +//! invariant that replacing that crate does not change proof bytes. +//! +//! GENERATED from jolt-field at commit +//! 5b3e39ece1c27586a1f7cc77f24e718cb5d73e10 (branch +//! feat/jolt-field-replacement) by a one-off generator test (deleted in the +//! same change; see this file's history). To regenerate: check out that +//! commit, restore `tests/golden_gen.rs` and the `jolt-field` +//! dev-dependency, run +//! `cargo nextest run -p jolt-field-two --all-features generate_golden_fixtures`, +//! and splice `target/tmp/golden_fixtures.txt` into the const blocks below. +//! +//! Row format: `(input hex, expected hex)` where the element is +//! `from_bytes_le_reduced(input)` (prime fields) or +//! `from_challenge_bytes` / `from_scalar_challenge_bytes(input)` +//! (challenge fixtures), and `expected` is the canonical LE encoding. +//! Extension rows are `(canonical coefficients, bincode wire hex)`. + +#![expect(clippy::unwrap_used, reason = "test code")] + +use jolt_field_two as two; + +use two::CanonicalEncoding; + +fn unhex(s: &str) -> Vec { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) + .collect() +} + +/// Element from the reducing decode; canonical bytes and bincode wire must +/// equal the fixture, and the checked decode must round-trip. +fn check_prime_rows(rows: &[(&str, &str)]) +where + F: CanonicalEncoding + + serde::Serialize + + serde::de::DeserializeOwned + + PartialEq + + std::fmt::Debug + + Copy, +{ + let cfg = bincode::config::standard(); + for (input, expected) in rows { + let (input, expected) = (unhex(input), unhex(expected)); + let e = F::from_bytes_le_reduced(&input); + assert_eq!(e.to_bytes_le_vec(), expected, "transcript bytes diverge"); + let wire = bincode::serde::encode_to_vec(e, cfg).unwrap(); + assert_eq!(wire, expected, "wire bytes diverge"); + assert_eq!( + F::from_bytes_le_checked(&expected), + Some(e), + "checked decode round-trip" + ); + let (back, read): (F, usize) = bincode::serde::decode_from_slice(&expected, cfg).unwrap(); + assert_eq!((back, read), (e, expected.len())); + } +} + +const FIX_BN254_FR: &[(&str, &str)] = &[ + ( + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "0100000000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", + ), + ( + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + ), + ( + "000000f093f5e1439170b97948e833285d588181b64550b829a031e1724e6430", + "000000f093f5e1439170b97948e833285d588181b64550b829a031e1724e6430", + ), + ( + "010000f093f5e1439170b97948e833285d588181b64550b829a031e1724e6430", + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "faffff4f1c3496ac29cd609f9576fc362e4679786fa36e662fdf079ac1770a0e", + ), + ( + "dae623d2aa29f41845b9a32a1d819bb65e7ca3106f280413e0638df8a0029a4d", + "d9e623e2163412d5b348eab0d498678e0124228fb8e2b35ab6c35b172eb4351d", + ), + ( + "7c41df5c178ab67358fb5375991fe1ba909e7d5a185156624bd8be7123e5d34b", + "7b41df6c8394d42fc78a9afb5037ad923346fcd8610b06aa21388d90b0966f1b", + ), + ( + "f0496a0db0d44aecb71a5457bac1cd1f004ed4e1b8a4ad0fda75893f75f9e34b", + "ef496a1d1cdf68a826aa9add71d999f7a2f55260025f5d57b0d5575e02ab7f1b", + ), + ( + "a3697a40313c6c012a94dd33469644182c96bcbf573df48ae896963ea3675f9e", + "a0697a70755bc6357642b1c66cdda89f148d383b346c03626bb6019b4a7c320d", + ), + ( + "cd49341848ecb614e2aee4b81d74bc679583ce92d4db5b516e29348f760fc986", + "cb4934382001f38cbfcd71c58ca35417dbd2cb8f6750bbe01ae9d0cc90720026", + ), + ( + "4388d11302c44ae8b30ca39efc96d6cf965cba921500e6806331d6ea881d2dcc", + "3f88d153b2edc2d86e4abdb7daf5062f22fbb48c3be9a49fbcb00f66bde39b0a", + ), + ( + "f8cef80001d2e51cdc6e48ab3ae7fb4ef2c2118736b66a9ae1e2a4741d0cc498", + "f5cef83045f13f51281d1c3e612e60d6dab98d0213e57971640210d1c4209707", + ), + ( + "26ebe17a6b629d695def5007330a4a49270921edea58aa1d6891972609397536", + "25ebe18ad76cbb25cc7e978dea211621cab09f6b34135a653ef1654596ea1006", + ), +]; +const FIX_BN254_FQ: &[(&str, &str)] = &[ + ( + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "0100000000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", + ), + ( + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + ), + ( + "46fd7cd8168c203c8dca7168916a81975d588181b64550b829a031e1724e6430", + "46fd7cd8168c203c8dca7168916a81975d588181b64550b829a031e1724e6430", + ), + ( + "47fd7cd8168c203c8dca7168916a81975d588181b64550b829a031e1724e6430", + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "9c0d8fc58d435dd33d0bc7f528eb780a2c4679786fa36e662fdf079ac1770a0e", + ), + ( + "dae623d2aa29f41845b9a32a1d819bb65e7ca3106f280413e0638df8a0029a4d", + "93e9a6f9939dd3dcb7ee31c28b161a1f0124228fb8e2b35ab6c35b172eb4351d", + ), + ( + "7c41df5c178ab67358fb5375991fe1ba909e7d5a185156624bd8be7123e5d34b", + "3544628400fe9537cb30e20c08b55f233346fcd8610b06aa21388d90b0966f1b", + ), + ( + "f0496a0db0d44aecb71a5457bac1cd1f004ed4e1b8a4ad0fda75893f75f9e34b", + "a94ced3499482ab02a50e2ee28574c88a2f55260025f5d57b0d5575e02ab7f1b", + ), + ( + "a3697a40313c6c012a94dd33469644182c96bcbf573df48ae896963ea3675f9e", + "ce7103b7ec970a4d823488fa9156c051138d383b346c03626bb6019b4a7c320d", + ), + ( + "cd49341848ecb614e2aee4b81d74bc679583ce92d4db5b516e29348f760fc986", + "3f4f3a671ad4759cc71901e8fa9eb938dad2cb8f6750bbe01ae9d0cc90720026", + ), + ( + "4388d11302c44ae8b30ca39efc96d6cf965cba921500e6806331d6ea881d2dcc", + "2793ddb1a693c8f77ee2dbfcb6ecd07120fbb48c3be9a49fbcb00f66bde39b0a", + ), + ( + "f8cef80001d2e51cdc6e48ab3ae7fb4ef2c2118736b66a9ae1e2a4741d0cc498", + "23d78177bc2d8468340ff37186a77788d9b98d0213e57971640210d1c4209707", + ), + ( + "26ebe17a6b629d695def5007330a4a49270921edea58aa1d6891972609397536", + "dfed64a254d67c2dd024df9ea19fc8b1c9b09f6b34135a653ef1654596ea1006", + ), +]; +const FIX_BN254_FR_CHALLENGE: &[(&str, &str)] = &[ + ( + "00000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "ffffffffffffffffffffffffffffffff", + "922306fba4417702705b58c72aed7f2e72a8da6190063056ab362c65cdc32617", + ), + ( + "dae623d2aa29f41845b9a32a1d819bb6", + "3b6bccb3eae3ebb5d9133924500858a3fac6c42854d0d35ae53b1e8f9f71200e", + ), + ( + "5e7ca3106f280413e0638df8a0029a4d", + "7f37873e17701c776900992bbf4d097271100afd8855cf1859fe2dc56226e00b", + ), + ( + "7c41df5c178ab67358fb5375991fe1ba", + "e77a83d4edd8d4a65ada9fd99b800bf3afba689fbd890e2c3dc10db8948c790f", + ), + ( + "909e7d5a185156624bd8be7123e5d34b", + "3c1f49d192ebb7e7f8a0be53b8e30491a1a98c2e45558f3b7cd443d88538c814", + ), + ( + "f0496a0db0d44aecb71a5457bac1cd1f", + "2be6dfd869df0b3ae480a5ad51a4fc21150cb9ffca8faca48897f93285b78b22", + ), + ( + "004ed4e1b8a4ad0fda75893f75f9e34b", + "f8c2174ea5e97eb646b7b0fd9a6a047dca24236aa1a61e02e964d8aaa051c926", + ), +]; +const FIX_BN254_FR_SCALAR_CHALLENGE: &[(&str, &str)] = &[ + ( + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "faffff4f1c3496ac29cd609f9576fc362e4679786fa36e662fdf079ac1770a0e", + ), + ( + "dae623d2aa29f41845b9a32a1d819bb65e7ca3106f280413e0638df8a0029a4d", + "499a02e0a8b7dbd0ce414288ee01adbd413a7c17508c78647173632507ea5419", + ), + ( + "7c41df5c178ab67358fb5375991fe1ba909e7d5a185156624bd8be7123e5d34b", + "49d3e54349d314c43f75de24c9ac364000311d9608c85ae81f7627557642791b", + ), + ( + "f0496a0db0d44aecb71a5457bac1cd1f004ed4e1b8a4ad0fda75893f75f9e34b", + "47e3f9b5efb2edcacaeabed1bf337f5faa6bbcb47d3dd9d545ca0d2c4230b82e", + ), + ( + "a3697a40313c6c012a94dd33469644182c96bcbf573df48ae896963ea3675f9e", + "9b5f67d382b5f01cd7a211eae503fbb3003b12c20f0ca401848ba78de78e3c12", + ), + ( + "cd49341848ecb614e2aee4b81d74bc679583ce92d4db5b516e29348f760fc986", + "82c90fb63f5ea15e0c99f5ed702db4f4f25a6f17decd6d016e3526c44cfab70b", + ), + ( + "4388d11302c44ae8b30ca39efc96d6cf965cba921500e6806331d6ea881d2dcc", + "cb2d1d9856e14f1fef75479b49d2286e727e157be85dbcfabeaa9221a0822413", + ), +]; +const FIX_BN254_FQ_CHALLENGE: &[(&str, &str)] = &[ + ( + "00000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "ffffffffffffffffffffffffffffffff", + "00000000000000000000000000000000ffffffffffffffffffffffffffffff1f", + ), + ( + "dae623d2aa29f41845b9a32a1d819bb6", + "00000000000000000000000000000000dae623d2aa29f41845b9a32a1d819b16", + ), + ( + "5e7ca3106f280413e0638df8a0029a4d", + "000000000000000000000000000000005e7ca3106f280413e0638df8a0029a0d", + ), + ( + "7c41df5c178ab67358fb5375991fe1ba", + "000000000000000000000000000000007c41df5c178ab67358fb5375991fe11a", + ), + ( + "909e7d5a185156624bd8be7123e5d34b", + "00000000000000000000000000000000909e7d5a185156624bd8be7123e5d30b", + ), + ( + "f0496a0db0d44aecb71a5457bac1cd1f", + "00000000000000000000000000000000f0496a0db0d44aecb71a5457bac1cd1f", + ), + ( + "004ed4e1b8a4ad0fda75893f75f9e34b", + "00000000000000000000000000000000004ed4e1b8a4ad0fda75893f75f9e30b", + ), +]; +const FIX_BN254_FQ_SCALAR_CHALLENGE: &[(&str, &str)] = &[ + ( + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "9c0d8fc58d435dd33d0bc7f528eb780a2c4679786fa36e662fdf079ac1770a0e", + ), + ( + "dae623d2aa29f41845b9a32a1d819bb65e7ca3106f280413e0638df8a0029a4d", + "31a50e3e9d5de1efded960cdcaf87600403a7c17508c78647173632507ea5419", + ), + ( + "7c41df5c178ab67358fb5375991fe1ba909e7d5a185156624bd8be7123e5d34b", + "bdd8eb7243a697d347c16d4737a89b61ff301d9608c85ae81f7627557642791b", + ), + ( + "f0496a0db0d44aecb71a5457bac1cd1f004ed4e1b8a4ad0fda75893f75f9e34b", + "2fee0514e458f3e9da82dd169c2a49a2a86bbcb47d3dd9d545ca0d2c4230b82e", + ), + ( + "a3697a40313c6c012a94dd33469644182c96bcbf573df48ae896963ea3675f9e", + "c967f019faf13434e394e81d0b7d1266ff3a12c20f0ca401848ba78de78e3c12", + ), + ( + "cd49341848ecb614e2aee4b81d74bc679583ce92d4db5b516e29348f760fc986", + "6ad41b143404a77d1c3114334d247e37f15a6f17decd6d016e3526c44cfab70b", + ), + ( + "4388d11302c44ae8b30ca39efc96d6cf965cba921500e6806331d6ea881d2dcc", + "8530a0afd34a1127f31b8fac0050dbfe717e157be85dbcfabeaa9221a0822413", + ), +]; + +#[cfg(feature = "bn254")] +mod bn254 { + use super::*; + + fn check_challenge_rows(rows: &[(&str, &str)], scalar: bool) { + for (input, expected) in rows { + let (input, expected) = (unhex(input), unhex(expected)); + let e = if scalar { + F::from_scalar_challenge_bytes(&input) + } else { + F::from_challenge_bytes(&input) + }; + assert_eq!(e.to_bytes_le_vec(), expected, "challenge bytes diverge"); + } + } + + #[test] + fn fr_bytes_match_fixtures() { + check_prime_rows::(FIX_BN254_FR); + } + + #[test] + fn fq_bytes_match_fixtures() { + check_prime_rows::(FIX_BN254_FQ); + } + + #[test] + fn fr_challenges_match_fixtures() { + check_challenge_rows::(FIX_BN254_FR_CHALLENGE, false); + check_challenge_rows::(FIX_BN254_FR_SCALAR_CHALLENGE, true); + } + + #[test] + fn fq_challenges_match_fixtures() { + check_challenge_rows::(FIX_BN254_FQ_CHALLENGE, false); + check_challenge_rows::(FIX_BN254_FQ_SCALAR_CHALLENGE, true); + } +} + +#[cfg(feature = "solinas")] +mod solinas { + #![expect(clippy::unreadable_literal, reason = "generated fixture data")] + + use super::*; + use two::ExtField; + + const FIX_PRIME24_OFFSET3: &[(&str, &str)] = &[ + ("00000000", "00000000"), + ("01000000", "01000000"), + ("02000000", "02000000"), + ("fcffff00", "fcffff00"), + ("fdffff00", "00000000"), + ("ffffffff", "ff020000"), + ("dae623d2", "50e92300"), + ("aa29f418", "f229f400"), + ("45b9a32a", "c3b9a300"), + ("1d819bb6", "3f839b00"), + ("5e7ca310", "8e7ca300"), + ("6f280413", "a8280400"), + ("e0638df8", "c8668d00"), + ("a0029a4d", "87039a00"), + ]; + const FIX_PRIME30_OFFSET35: &[(&str, &str)] = &[ + ("00000000", "00000000"), + ("01000000", "01000000"), + ("02000000", "02000000"), + ("dcffff3f", "dcffff3f"), + ("ddffff3f", "00000000"), + ("ffffffff", "8b000000"), + ("dae623d2", "43e72312"), + ("aa29f418", "aa29f418"), + ("45b9a32a", "45b9a32a"), + ("1d819bb6", "63819b36"), + ("5e7ca310", "5e7ca310"), + ("6f280413", "6f280413"), + ("e0638df8", "49648d38"), + ("a0029a4d", "c3029a0d"), + ]; + const FIX_PRIME31_OFFSET19: &[(&str, &str)] = &[ + ("00000000", "00000000"), + ("01000000", "01000000"), + ("02000000", "02000000"), + ("ecffff7f", "ecffff7f"), + ("edffff7f", "00000000"), + ("ffffffff", "25000000"), + ("dae623d2", "ede62352"), + ("aa29f418", "aa29f418"), + ("45b9a32a", "45b9a32a"), + ("1d819bb6", "30819b36"), + ("5e7ca310", "5e7ca310"), + ("6f280413", "6f280413"), + ("e0638df8", "f3638d78"), + ("a0029a4d", "a0029a4d"), + ]; + const FIX_PRIME32_OFFSET99: &[(&str, &str)] = &[ + ("00000000", "00000000"), + ("01000000", "01000000"), + ("02000000", "02000000"), + ("9cffffff", "9cffffff"), + ("9dffffff", "00000000"), + ("ffffffff", "62000000"), + ("dae623d2", "dae623d2"), + ("aa29f418", "aa29f418"), + ("45b9a32a", "45b9a32a"), + ("1d819bb6", "1d819bb6"), + ("5e7ca310", "5e7ca310"), + ("6f280413", "6f280413"), + ("e0638df8", "e0638df8"), + ("a0029a4d", "a0029a4d"), + ]; + const FIX_PRIME40_OFFSET195: &[(&str, &str)] = &[ + ("0000000000000000", "0000000000000000"), + ("0100000000000000", "0100000000000000"), + ("0200000000000000", "0200000000000000"), + ("3cffffffff000000", "3cffffffff000000"), + ("3dffffffff000000", "0000000000000000"), + ("ffffffffffffffff", "ffffffc200000000"), + ("dae623d2aa29f418", "15e225e5aa000000"), + ("45b9a32a1d819bb6", "882cbcb51d000000"), + ("5e7ca3106f280413", "d6a61f1f6f000000"), + ("e0638df8a0029a4d", "66b3a933a1000000"), + ("7c41df5c178ab673", "9a4c03b517000000"), + ("58fb5375991fe1ba", "f575ad039a000000"), + ("909e7d5a18515662", "435e65a518000000"), + ("4bd8be7123e5d34b", "ba3f81ab23000000"), + ]; + const FIX_PRIME48_OFFSET59: &[(&str, &str)] = &[ + ("0000000000000000", "0000000000000000"), + ("0100000000000000", "0100000000000000"), + ("0200000000000000", "0200000000000000"), + ("c4ffffffffff0000", "c4ffffffffff0000"), + ("c5ffffffffff0000", "0000000000000000"), + ("ffffffffffffffff", "ffff3a0000000000"), + ("dae623d2aa29f418", "16a729d2aa290000"), + ("45b9a32a1d819bb6", "fececd2a1d810000"), + ("5e7ca3106f280413", "4adea7106f280000"), + ("e0638df8a0029a4d", "5e469ff8a0020000"), + ("7c41df5c178ab673", "6eecf95c178a0000"), + ("58fb5375991fe1ba", "330d7f75991f0000"), + ("909e7d5a18515662", "6248945a18510000"), + ("4bd8be7123e5d34b", "ec51d07123e50000"), + ]; + const FIX_PRIME56_OFFSET27: &[(&str, &str)] = &[ + ("0000000000000000", "0000000000000000"), + ("0100000000000000", "0100000000000000"), + ("0200000000000000", "0200000000000000"), + ("e4ffffffffffff00", "e4ffffffffffff00"), + ("e5ffffffffffff00", "0000000000000000"), + ("ffffffffffffffff", "ff1a000000000000"), + ("dae623d2aa29f418", "62e923d2aa29f400"), + ("45b9a32a1d819bb6", "77cca32a1d819b00"), + ("5e7ca3106f280413", "5f7ea3106f280400"), + ("e0638df8a0029a4d", "ff6b8df8a0029a00"), + ("7c41df5c178ab673", "9d4ddf5c178ab600"), + ("58fb5375991fe1ba", "f60e5475991fe100"), + ("909e7d5a18515662", "e6a87d5a18515600"), + ("4bd8be7123e5d34b", "34e0be7123e5d300"), + ]; + const FIX_PRIME64_OFFSET59: &[(&str, &str)] = &[ + ("0000000000000000", "0000000000000000"), + ("0100000000000000", "0100000000000000"), + ("0200000000000000", "0200000000000000"), + ("c4ffffffffffffff", "c4ffffffffffffff"), + ("c5ffffffffffffff", "0000000000000000"), + ("ffffffffffffffff", "3a00000000000000"), + ("dae623d2aa29f418", "dae623d2aa29f418"), + ("45b9a32a1d819bb6", "45b9a32a1d819bb6"), + ("5e7ca3106f280413", "5e7ca3106f280413"), + ("e0638df8a0029a4d", "e0638df8a0029a4d"), + ("7c41df5c178ab673", "7c41df5c178ab673"), + ("58fb5375991fe1ba", "58fb5375991fe1ba"), + ("909e7d5a18515662", "909e7d5a18515662"), + ("4bd8be7123e5d34b", "4bd8be7123e5d34b"), + ]; + const FIX_PRIME128_OFFSET275: &[(&str, &str)] = &[ + ( + "00000000000000000000000000000000", + "00000000000000000000000000000000", + ), + ( + "01000000000000000000000000000000", + "01000000000000000000000000000000", + ), + ( + "02000000000000000000000000000000", + "02000000000000000000000000000000", + ), + ( + "ecfeffffffffffffffffffffffffffff", + "ecfeffffffffffffffffffffffffffff", + ), + ( + "edfeffffffffffffffffffffffffffff", + "00000000000000000000000000000000", + ), + ( + "ffffffffffffffffffffffffffffffff", + "12010000000000000000000000000000", + ), + ( + "dae623d2aa29f41845b9a32a1d819bb6", + "dae623d2aa29f41845b9a32a1d819bb6", + ), + ( + "5e7ca3106f280413e0638df8a0029a4d", + "5e7ca3106f280413e0638df8a0029a4d", + ), + ( + "7c41df5c178ab67358fb5375991fe1ba", + "7c41df5c178ab67358fb5375991fe1ba", + ), + ( + "909e7d5a185156624bd8be7123e5d34b", + "909e7d5a185156624bd8be7123e5d34b", + ), + ( + "f0496a0db0d44aecb71a5457bac1cd1f", + "f0496a0db0d44aecb71a5457bac1cd1f", + ), + ( + "004ed4e1b8a4ad0fda75893f75f9e34b", + "004ed4e1b8a4ad0fda75893f75f9e34b", + ), + ( + "a3697a40313c6c012a94dd3346964418", + "a3697a40313c6c012a94dd3346964418", + ), + ( + "2c96bcbf573df48ae896963ea3675f9e", + "2c96bcbf573df48ae896963ea3675f9e", + ), + ]; + const FIX_PRIME128_OFFSET159: &[(&str, &str)] = &[ + ( + "00000000000000000000000000000000", + "00000000000000000000000000000000", + ), + ( + "01000000000000000000000000000000", + "01000000000000000000000000000000", + ), + ( + "02000000000000000000000000000000", + "02000000000000000000000000000000", + ), + ( + "60ffffffffffffffffffffffffffffff", + "60ffffffffffffffffffffffffffffff", + ), + ( + "61ffffffffffffffffffffffffffffff", + "00000000000000000000000000000000", + ), + ( + "ffffffffffffffffffffffffffffffff", + "9e000000000000000000000000000000", + ), + ( + "dae623d2aa29f41845b9a32a1d819bb6", + "dae623d2aa29f41845b9a32a1d819bb6", + ), + ( + "5e7ca3106f280413e0638df8a0029a4d", + "5e7ca3106f280413e0638df8a0029a4d", + ), + ( + "7c41df5c178ab67358fb5375991fe1ba", + "7c41df5c178ab67358fb5375991fe1ba", + ), + ( + "909e7d5a185156624bd8be7123e5d34b", + "909e7d5a185156624bd8be7123e5d34b", + ), + ( + "f0496a0db0d44aecb71a5457bac1cd1f", + "f0496a0db0d44aecb71a5457bac1cd1f", + ), + ( + "004ed4e1b8a4ad0fda75893f75f9e34b", + "004ed4e1b8a4ad0fda75893f75f9e34b", + ), + ( + "a3697a40313c6c012a94dd3346964418", + "a3697a40313c6c012a94dd3346964418", + ), + ( + "2c96bcbf573df48ae896963ea3675f9e", + "2c96bcbf573df48ae896963ea3675f9e", + ), + ]; + const FIX_PRIME128_OFFSET2355: &[(&str, &str)] = &[ + ( + "00000000000000000000000000000000", + "00000000000000000000000000000000", + ), + ( + "01000000000000000000000000000000", + "01000000000000000000000000000000", + ), + ( + "02000000000000000000000000000000", + "02000000000000000000000000000000", + ), + ( + "ccf6ffffffffffffffffffffffffffff", + "ccf6ffffffffffffffffffffffffffff", + ), + ( + "cdf6ffffffffffffffffffffffffffff", + "00000000000000000000000000000000", + ), + ( + "ffffffffffffffffffffffffffffffff", + "32090000000000000000000000000000", + ), + ( + "dae623d2aa29f41845b9a32a1d819bb6", + "dae623d2aa29f41845b9a32a1d819bb6", + ), + ( + "5e7ca3106f280413e0638df8a0029a4d", + "5e7ca3106f280413e0638df8a0029a4d", + ), + ( + "7c41df5c178ab67358fb5375991fe1ba", + "7c41df5c178ab67358fb5375991fe1ba", + ), + ( + "909e7d5a185156624bd8be7123e5d34b", + "909e7d5a185156624bd8be7123e5d34b", + ), + ( + "f0496a0db0d44aecb71a5457bac1cd1f", + "f0496a0db0d44aecb71a5457bac1cd1f", + ), + ( + "004ed4e1b8a4ad0fda75893f75f9e34b", + "004ed4e1b8a4ad0fda75893f75f9e34b", + ), + ( + "a3697a40313c6c012a94dd3346964418", + "a3697a40313c6c012a94dd3346964418", + ), + ( + "2c96bcbf573df48ae896963ea3675f9e", + "2c96bcbf573df48ae896963ea3675f9e", + ), + ]; + const FIX_PRIME128_OFFSETA7F7: &[(&str, &str)] = &[ + ( + "00000000000000000000000000000000", + "00000000000000000000000000000000", + ), + ( + "01000000000000000000000000000000", + "01000000000000000000000000000000", + ), + ( + "02000000000000000000000000000000", + "02000000000000000000000000000000", + ), + ( + "08580000ffffffffffffffffffffffff", + "08580000ffffffffffffffffffffffff", + ), + ( + "09580000ffffffffffffffffffffffff", + "00000000000000000000000000000000", + ), + ( + "ffffffffffffffffffffffffffffffff", + "f6a7ffff000000000000000000000000", + ), + ( + "dae623d2aa29f41845b9a32a1d819bb6", + "dae623d2aa29f41845b9a32a1d819bb6", + ), + ( + "5e7ca3106f280413e0638df8a0029a4d", + "5e7ca3106f280413e0638df8a0029a4d", + ), + ( + "7c41df5c178ab67358fb5375991fe1ba", + "7c41df5c178ab67358fb5375991fe1ba", + ), + ( + "909e7d5a185156624bd8be7123e5d34b", + "909e7d5a185156624bd8be7123e5d34b", + ), + ( + "f0496a0db0d44aecb71a5457bac1cd1f", + "f0496a0db0d44aecb71a5457bac1cd1f", + ), + ( + "004ed4e1b8a4ad0fda75893f75f9e34b", + "004ed4e1b8a4ad0fda75893f75f9e34b", + ), + ( + "a3697a40313c6c012a94dd3346964418", + "a3697a40313c6c012a94dd3346964418", + ), + ( + "2c96bcbf573df48ae896963ea3675f9e", + "2c96bcbf573df48ae896963ea3675f9e", + ), + ]; + const FIX_EXT2_P32: &[(&[u128], &str)] = &[ + (&[0x0, 0x0], "0000000000000000"), + (&[0xffffff9c, 0xffffff9c], "9cffffff9cffffff"), + (&[0x1, 0x0], "0100000000000000"), + (&[0x73d60714, 0xf544c309], "1407d67309c344f5"), + (&[0x6c49d008, 0x9b0ea42d], "08d0496c2da40e9b"), + (&[0xfefbf7, 0x72cde54], "f7fbfe0054de2c07"), + (&[0x5cee8e85, 0x8fba2793], "858eee5c9327ba8f"), + (&[0xbbe03ed8, 0xe21d84c1], "d83ee0bbc1841de2"), + ]; + const FIX_EXT4_P32: &[(&[u128], &str)] = &[ + (&[0x0, 0x0, 0x0, 0x0], "00000000000000000000000000000000"), + ( + &[0xffffff9c, 0xffffff9c, 0xffffff9c, 0xffffff9c], + "9cffffff9cffffff9cffffff9cffffff", + ), + (&[0x1, 0x0, 0x0, 0x0], "01000000000000000000000000000000"), + ( + &[0x73d60714, 0xf544c309, 0x6c49d008, 0x9b0ea42d], + "1407d67309c344f508d0496c2da40e9b", + ), + ( + &[0xfefbf7, 0x72cde54, 0x5cee8e85, 0x8fba2793], + "f7fbfe0054de2c07858eee5c9327ba8f", + ), + ( + &[0xbbe03ed8, 0xe21d84c1, 0xe3b06389, 0x56be17ea], + "d83ee0bbc1841de28963b0e3ea17be56", + ), + ( + &[0x644d482d, 0xa75fce19, 0xebf7eb94, 0xc4f0921b], + "2d484d6419ce5fa794ebf7eb1b92f0c4", + ), + ( + &[0x84dac822, 0x62f6ac1c, 0xb5549d13, 0x723a51cd], + "22c8da841cacf662139d54b5cd513a72", + ), + ]; + const FIX_EXT8_P32: &[(&[u128], &str)] = &[ + ( + &[0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0], + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + &[ + 0xffffff9c, 0xffffff9c, 0xffffff9c, 0xffffff9c, 0xffffff9c, 0xffffff9c, 0xffffff9c, + 0xffffff9c, + ], + "9cffffff9cffffff9cffffff9cffffff9cffffff9cffffff9cffffff9cffffff", + ), + ( + &[0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0], + "0100000000000000000000000000000000000000000000000000000000000000", + ), + ( + &[ + 0x73d60714, 0xf544c309, 0x6c49d008, 0x9b0ea42d, 0xfefbf7, 0x72cde54, 0x5cee8e85, + 0x8fba2793, + ], + "1407d67309c344f508d0496c2da40e9bf7fbfe0054de2c07858eee5c9327ba8f", + ), + ( + &[ + 0xbbe03ed8, 0xe21d84c1, 0xe3b06389, 0x56be17ea, 0x644d482d, 0xa75fce19, 0xebf7eb94, + 0xc4f0921b, + ], + "d83ee0bbc1841de28963b0e3ea17be562d484d6419ce5fa794ebf7eb1b92f0c4", + ), + ( + &[ + 0x84dac822, 0x62f6ac1c, 0xb5549d13, 0x723a51cd, 0x56bd52bf, 0x4247ba16, 0xb9d843ba, + 0xe19d078e, + ], + "22c8da841cacf662139d54b5cd513a72bf52bd5616ba4742ba43d8b98e079de1", + ), + ( + &[ + 0x2f8b79d, 0xec18080b, 0xa31f1e7d, 0x1471072, 0xeb5039b2, 0xa11bc9b4, 0x9c0517ed, + 0x81f10ed0, + ], + "9db7f8020b0818ec7d1e1fa372104701b23950ebb4c91ba1ed17059cd00ef181", + ), + ( + &[ + 0xed9bed57, 0x29d9826, 0xceed3883, 0x60414ccc, 0xccc2b5c7, 0xaa6c7139, 0xcd6b7546, + 0xd9701579, + ], + "57ed9bed26989d028338edcecc4c4160c7b5c2cc39716caa46756bcd791570d9", + ), + ]; + const FIX_EXT2_P64: &[(&[u128], &str)] = &[ + (&[0x0, 0x0], "00000000000000000000000000000000"), + ( + &[0xffffffffffffffc4, 0xffffffffffffffc4], + "c4ffffffffffffffc4ffffffffffffff", + ), + (&[0x1, 0x0], "01000000000000000000000000000000"), + ( + &[0xd609cf72b02c0e61, 0x79a6c5d7584f8181], + "610e2cb072cf09d681814f58d7c5a679", + ), + ( + &[0x4d78586fe207e611, 0xc3d8b17f0be956fa], + "11e607e26f58784dfa56e90b7fb1d8c3", + ), + ( + &[0xaa67d93208600cb0, 0xe93623b792a8a5d1], + "b00c600832d967aad1a5a892b72336e9", + ), + ( + &[0x436b82b5306a7737, 0x74a285c1b520e858], + "37776a30b5826b4358e820b5c185a274", + ), + ( + &[0x5ab69a96e218a143, 0x36706831fafb6742], + "43a118e2969ab65a4267fbfa31687036", + ), + ]; + const FIX_EXT4_P64: &[(&[u128], &str)] = &[ + ( + &[0x0, 0x0, 0x0, 0x0], + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + &[ + 0xffffffffffffffc4, + 0xffffffffffffffc4, + 0xffffffffffffffc4, + 0xffffffffffffffc4, + ], + "c4ffffffffffffffc4ffffffffffffffc4ffffffffffffffc4ffffffffffffff", + ), + ( + &[0x1, 0x0, 0x0, 0x0], + "0100000000000000000000000000000000000000000000000000000000000000", + ), + ( + &[ + 0xd609cf72b02c0e61, + 0x79a6c5d7584f8181, + 0x4d78586fe207e611, + 0xc3d8b17f0be956fa, + ], + "610e2cb072cf09d681814f58d7c5a67911e607e26f58784dfa56e90b7fb1d8c3", + ), + ( + &[ + 0xaa67d93208600cb0, + 0xe93623b792a8a5d1, + 0x436b82b5306a7737, + 0x74a285c1b520e858, + ], + "b00c600832d967aad1a5a892b72336e937776a30b5826b4358e820b5c185a274", + ), + ( + &[ + 0x5ab69a96e218a143, + 0x36706831fafb6742, + 0x9ee6382725ed15a0, + 0x17930127615c009a, + ], + "43a118e2969ab65a4267fbfa31687036a015ed252738e69e9a005c6127019317", + ), + ( + &[ + 0x300e066995c7449, + 0x8bfc74fb11786f73, + 0x4cee8f1453becffe, + 0x6f3bdb40c4eede10, + ], + "49745c9966e00003736f7811fb74fc8bfecfbe53148fee4c10deeec440db3b6f", + ), + ( + &[ + 0x67d84e6f52ff5ee7, + 0xa15bc70e42ea593, + 0x84acf5c3d050739, + 0xeb457d2053f67e6b, + ], + "e75eff526f4ed86793a52ee470bc150a3907053d5ccf4a086b7ef653207d45eb", + ), + ]; + const FIX_EXT8_P64: &[(&[u128], &str)] = &[ + (&[0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0], "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + (&[0xffffffffffffffc4, 0xffffffffffffffc4, 0xffffffffffffffc4, 0xffffffffffffffc4, 0xffffffffffffffc4, 0xffffffffffffffc4, 0xffffffffffffffc4, 0xffffffffffffffc4], "c4ffffffffffffffc4ffffffffffffffc4ffffffffffffffc4ffffffffffffffc4ffffffffffffffc4ffffffffffffffc4ffffffffffffffc4ffffffffffffff"), + (&[0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0], "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + (&[0xd609cf72b02c0e61, 0x79a6c5d7584f8181, 0x4d78586fe207e611, 0xc3d8b17f0be956fa, 0xaa67d93208600cb0, 0xe93623b792a8a5d1, 0x436b82b5306a7737, 0x74a285c1b520e858], "610e2cb072cf09d681814f58d7c5a67911e607e26f58784dfa56e90b7fb1d8c3b00c600832d967aad1a5a892b72336e937776a30b5826b4358e820b5c185a274"), + (&[0x5ab69a96e218a143, 0x36706831fafb6742, 0x9ee6382725ed15a0, 0x17930127615c009a, 0x300e066995c7449, 0x8bfc74fb11786f73, 0x4cee8f1453becffe, 0x6f3bdb40c4eede10], "43a118e2969ab65a4267fbfa31687036a015ed252738e69e9a005c612701931749745c9966e00003736f7811fb74fc8bfecfbe53148fee4c10deeec440db3b6f"), + (&[0x67d84e6f52ff5ee7, 0xa15bc70e42ea593, 0x84acf5c3d050739, 0xeb457d2053f67e6b, 0x75e8506326be2e79, 0xfe788b2a7fd8a05b, 0x441c8c24b6702e2f, 0x3f2cf962f21f6a4c], "e75eff526f4ed86793a52ee470bc150a3907053d5ccf4a086b7ef653207d45eb792ebe266350e8755ba0d87f2a8b78fe2f2e70b6248c1c444c6a1ff262f92c3f"), + (&[0x3a6b892c9811490e, 0x7e2e2981b2e0bac4, 0x5686aa90270939dc, 0xb46fec366af7a378, 0xbb7409b32081c57, 0xd0adcc227d135fcd, 0xb9de091a840ae78d, 0x7c7665093663d736], "0e4911982c896b3ac4bae0b281292e7edc39092790aa865678a3f76a36ec6fb4571c08329b40b70bcd5f137d22ccadd08de70a841a09deb936d763360965767c"), + (&[0x204cf30bb30096d9, 0x7e4097b92c507936, 0x9b1d5c1eaf86cccf, 0x56f6215dbf2e4694, 0x4ffad9390b05e0bc, 0x2712c7a4f32f68b7, 0x91dd924eb2b0e58d, 0xbe3dec166c0263b4], "d99600b30bf34c203679502cb997407ecfcc86af1e5c1d9b94462ebf5d21f656bce0050b39d9fa4fb7682ff3a4c712278de5b0b24e92dd91b463026c16ec3dbe"), + ]; + const FIX_EXT2_P128: &[(&[u128], &str)] = &[ + ( + &[0x0, 0x0], + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + &[ + 0xfffffffffffffffffffffffffffffeec, + 0xfffffffffffffffffffffffffffffeec, + ], + "ecfeffffffffffffffffffffffffffffecfeffffffffffffffffffffffffffff", + ), + ( + &[0x1, 0x0], + "0100000000000000000000000000000000000000000000000000000000000000", + ), + ( + &[ + 0x95ec2d2a50b6523ad32e8e6a68bbeda, + 0x8ee0c318a649d9f48bd9cf29054a3eaa, + ], + "dabe8ba6e6e832ad23650ba5d2c25e09aa3e4a0529cfd98bf4d949a618c3e08e", + ), + ( + &[ + 0x5849c2a2605eea307e85ab91ea9e645, + 0xbee878b6326d409bc444df816cbb691d, + ], + "45e6a91eb95ae807a3ee05262a9c84051d69bb6c81df44c49b406d32b678e8be", + ), + ( + &[ + 0xdfc90e1016f022a317119b7cbf08055e, + 0xae1cf13c68e8d04672b6a28cfce256f, + ], + "5e0508bf7c9b1117a322f016100ec9df6f25cecf286a2b67048d8ec613cfe10a", + ), + ( + &[ + 0x229e76f8b4d1758d48e6176384245de0, + 0x3e5f384d01415d9a14b08c026b1052a0, + ], + "e05d24846317e6488d75d1b4f8769e22a052106b028cb0149a5d41014d385f3e", + ), + ( + &[ + 0x1afc7c5c909d9ddf2285f1418dc53d7c, + 0x75332873a5d3f2b633a6158ac3227117, + ], + "7c3dc58d41f18522df9d9d905c7cfc1a177122c38a15a633b6f2d3a573283375", + ), + ]; + const FIX_EXT4_P128: &[(&[u128], &str)] = &[ + (&[0x0, 0x0, 0x0, 0x0], "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + (&[0xfffffffffffffffffffffffffffffeec, 0xfffffffffffffffffffffffffffffeec, 0xfffffffffffffffffffffffffffffeec, 0xfffffffffffffffffffffffffffffeec], "ecfeffffffffffffffffffffffffffffecfeffffffffffffffffffffffffffffecfeffffffffffffffffffffffffffffecfeffffffffffffffffffffffffffff"), + (&[0x1, 0x0, 0x0, 0x0], "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + (&[0x95ec2d2a50b6523ad32e8e6a68bbeda, 0x8ee0c318a649d9f48bd9cf29054a3eaa, 0x5849c2a2605eea307e85ab91ea9e645, 0xbee878b6326d409bc444df816cbb691d], "dabe8ba6e6e832ad23650ba5d2c25e09aa3e4a0529cfd98bf4d949a618c3e08e45e6a91eb95ae807a3ee05262a9c84051d69bb6c81df44c49b406d32b678e8be"), + (&[0xdfc90e1016f022a317119b7cbf08055e, 0xae1cf13c68e8d04672b6a28cfce256f, 0x229e76f8b4d1758d48e6176384245de0, 0x3e5f384d01415d9a14b08c026b1052a0], "5e0508bf7c9b1117a322f016100ec9df6f25cecf286a2b67048d8ec613cfe10ae05d24846317e6488d75d1b4f8769e22a052106b028cb0149a5d41014d385f3e"), + (&[0x1afc7c5c909d9ddf2285f1418dc53d7c, 0x75332873a5d3f2b633a6158ac3227117, 0x17f99d75e2c2cb53185eedfbe3083858, 0x4bc8e3bab667e4e1a046851f576a3c99], "7c3dc58d41f18522df9d9d905c7cfc1a177122c38a15a633b6f2d3a573283375583808e3fbed5e1853cbc2e2759df917993c6a571f8546a0e1e467b6bae3c84b"), + (&[0x8231505a2d6fb47d01a35b9e209dd490, 0x2f22436254aff856af16ed518cea3118, 0xbb73cc71d26357be193e70d8d6d98d4b, 0x9d19904b4e75a8d33a5799e5afd0ed23], "90d49d209e5ba3017db46f2d5a5031821831ea8c51ed16af56f8af546243222f4b8dd9d6d8703e19be5763d271cc73bb23edd0afe599573ad3a8754e4b90199d"), + (&[0x2e8f3b0da794056aacd5b249b3e21cf0, 0xc9c392ecf3c2744a8a02dfd4b65dcdb0, 0x4ac6ac57e4534954cc81171a9dd31cb7, 0xeb2d691ffa99bccdb7ce42c19287eeba], "f01ce2b349b2d5ac6a0594a70d3b8f2eb0cd5db6d4df028a4a74c2f3ec92c3c9b71cd39d1a1781cc544953e457acc64abaee8792c142ceb7cdbc99fa1f692deb"), + ]; + const FIX_EXT8_P128: &[(&[u128], &str)] = &[ + (&[0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0], "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + (&[0xfffffffffffffffffffffffffffffeec, 0xfffffffffffffffffffffffffffffeec, 0xfffffffffffffffffffffffffffffeec, 0xfffffffffffffffffffffffffffffeec, 0xfffffffffffffffffffffffffffffeec, 0xfffffffffffffffffffffffffffffeec, 0xfffffffffffffffffffffffffffffeec, 0xfffffffffffffffffffffffffffffeec], "ecfeffffffffffffffffffffffffffffecfeffffffffffffffffffffffffffffecfeffffffffffffffffffffffffffffecfeffffffffffffffffffffffffffffecfeffffffffffffffffffffffffffffecfeffffffffffffffffffffffffffffecfeffffffffffffffffffffffffffffecfeffffffffffffffffffffffffffff"), + (&[0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0], "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + (&[0x95ec2d2a50b6523ad32e8e6a68bbeda, 0x8ee0c318a649d9f48bd9cf29054a3eaa, 0x5849c2a2605eea307e85ab91ea9e645, 0xbee878b6326d409bc444df816cbb691d, 0xdfc90e1016f022a317119b7cbf08055e, 0xae1cf13c68e8d04672b6a28cfce256f, 0x229e76f8b4d1758d48e6176384245de0, 0x3e5f384d01415d9a14b08c026b1052a0], "dabe8ba6e6e832ad23650ba5d2c25e09aa3e4a0529cfd98bf4d949a618c3e08e45e6a91eb95ae807a3ee05262a9c84051d69bb6c81df44c49b406d32b678e8be5e0508bf7c9b1117a322f016100ec9df6f25cecf286a2b67048d8ec613cfe10ae05d24846317e6488d75d1b4f8769e22a052106b028cb0149a5d41014d385f3e"), + (&[0x1afc7c5c909d9ddf2285f1418dc53d7c, 0x75332873a5d3f2b633a6158ac3227117, 0x17f99d75e2c2cb53185eedfbe3083858, 0x4bc8e3bab667e4e1a046851f576a3c99, 0x8231505a2d6fb47d01a35b9e209dd490, 0x2f22436254aff856af16ed518cea3118, 0xbb73cc71d26357be193e70d8d6d98d4b, 0x9d19904b4e75a8d33a5799e5afd0ed23], "7c3dc58d41f18522df9d9d905c7cfc1a177122c38a15a633b6f2d3a573283375583808e3fbed5e1853cbc2e2759df917993c6a571f8546a0e1e467b6bae3c84b90d49d209e5ba3017db46f2d5a5031821831ea8c51ed16af56f8af546243222f4b8dd9d6d8703e19be5763d271cc73bb23edd0afe599573ad3a8754e4b90199d"), + (&[0x2e8f3b0da794056aacd5b249b3e21cf0, 0xc9c392ecf3c2744a8a02dfd4b65dcdb0, 0x4ac6ac57e4534954cc81171a9dd31cb7, 0xeb2d691ffa99bccdb7ce42c19287eeba, 0x1e2f46e1fa26a6d48102fa4e7fd5ba00, 0x368c360f492c47ad6c2815a4a2a418b8, 0xa497383fb616bd8955429575bf3276da, 0xd71f974b8bb550e3aae51af9bf55ba75], "f01ce2b349b2d5ac6a0594a70d3b8f2eb0cd5db6d4df028a4a74c2f3ec92c3c9b71cd39d1a1781cc544953e457acc64abaee8792c142ceb7cdbc99fa1f692deb00bad57f4efa0281d4a626fae1462f1eb818a4a2a415286cad472c490f368c36da7632bf7595425589bd16b63f3897a475ba55bff91ae5aae350b58b4b971fd7"), + (&[0x637068400d8c717a4f83826978b31ca3, 0x7d5682012c73756c9b3e333c7444a431, 0x6a467433fdab3dddd849e294b091f22a, 0xda8bc3186680c6445639f596cb49e646, 0x59db1ebf000b2cbc56372a962f74c82c, 0x3ff0528a61b7faf4144ac63df7ac8657, 0x2745433efaca6b96ace78996b76419e8, 0xb4ec249ef0d94b5fca09f467b44f6ea3], "a31cb3786982834f7a718c0d4068706331a444743c333e9b6c75732c0182567d2af291b094e249d8dd3dabfd3374466a46e649cb96f5395644c6806618c38bda2cc8742f962a3756bc2c0b00bf1edb595786acf73dc64a14f4fab7618a52f03fe81964b79689e7ac966bcafa3e434527a36e4fb467f409ca5f4bd9f09e24ecb4"), + (&[0xce146918fcdfd134a198ba496b6b54cd, 0xe0645714d27314b6c72085ecabcaa748, 0x535d0db82474e0e464ab32ae4896f3e2, 0xf6a32e67c18093bc7f5a6f74268c2d1d, 0xed84f79242e677ce9255ca839fe83795, 0x811838518589bb5b667dccdb2c7133d4, 0xf800638fd1130f3469c6a029834c576e, 0x4ce9d686a152cec904597b0f3decb776], "cd546b6b49ba98a134d1dffc186914ce48a7caabec8520c7b61473d2145764e0e2f39648ae32ab64e4e07424b80d5d531d2d8c26746f5a7fbc9380c1672ea3f69537e89f83ca5592ce77e64292f784edd433712cdbcc7d665bbb8985513818816e574c8329a0c669340f13d18f6300f876b7ec3d0f7b5904c9ce52a186d6e94c"), + ]; + + /// Extension element from canonical coefficients; the bincode wire must + /// equal the fixture and decode back. + fn check_ext_rows(rows: &[(&[u128], &str)]) + where + F: CanonicalEncoding + two::Field, + E: ExtField + + serde::Serialize + + serde::de::DeserializeOwned + + PartialEq + + std::fmt::Debug + + Copy, + { + let cfg = bincode::config::standard(); + for (coeffs, expected) in rows { + let expected = unhex(expected); + let e = E::from_base_slice( + &coeffs + .iter() + .map(|&v| F::from_u128_checked(v).unwrap()) + .collect::>(), + ); + let wire = bincode::serde::encode_to_vec(e, cfg).unwrap(); + assert_eq!(wire, expected, "ext wire bytes diverge"); + let (back, read): (E, usize) = + bincode::serde::decode_from_slice(&expected, cfg).unwrap(); + assert_eq!((back, read), (e, expected.len())); + } + } + + #[test] + fn fp32_bytes_match_fixtures() { + check_prime_rows::(FIX_PRIME24_OFFSET3); + check_prime_rows::(FIX_PRIME30_OFFSET35); + check_prime_rows::(FIX_PRIME31_OFFSET19); + check_prime_rows::(FIX_PRIME32_OFFSET99); + } + + #[test] + fn fp64_bytes_match_fixtures() { + check_prime_rows::(FIX_PRIME40_OFFSET195); + check_prime_rows::(FIX_PRIME48_OFFSET59); + check_prime_rows::(FIX_PRIME56_OFFSET27); + check_prime_rows::(FIX_PRIME64_OFFSET59); + } + + #[test] + fn fp128_bytes_match_fixtures() { + check_prime_rows::(FIX_PRIME128_OFFSET275); + check_prime_rows::(FIX_PRIME128_OFFSET159); + check_prime_rows::(FIX_PRIME128_OFFSET2355); + check_prime_rows::(FIX_PRIME128_OFFSETA7F7); + } + + #[test] + fn ext_bytes_match_fixtures() { + type F32 = two::Prime32Offset99; + type F64 = two::Prime64Offset59; + type F128 = two::Prime128Offset275; + check_ext_rows::>(FIX_EXT2_P32); + check_ext_rows::>(FIX_EXT4_P32); + check_ext_rows::>(FIX_EXT8_P32); + check_ext_rows::>(FIX_EXT2_P64); + check_ext_rows::>(FIX_EXT4_P64); + check_ext_rows::>(FIX_EXT8_P64); + check_ext_rows::>(FIX_EXT2_P128); + check_ext_rows::>(FIX_EXT4_P128); + check_ext_rows::>(FIX_EXT8_P128); + } +} diff --git a/crates/jolt-field-two/tests/limbs_signed_differential.rs b/crates/jolt-field-two/tests/limbs_signed_differential.rs index d1f504677f..04906ea0dc 100644 --- a/crates/jolt-field-two/tests/limbs_signed_differential.rs +++ b/crates/jolt-field-two/tests/limbs_signed_differential.rs @@ -1,55 +1,79 @@ //! Differential tests for `Limbs` and the signed bigint families against -//! jolt-field, plus u128/i128 oracles for the widths that fit. +//! exact num-bigint integer arithmetic, plus u128/i128 oracles for the +//! widths that fit. -use jolt_field as base; use jolt_field_two as two; +use num_bigint::{BigInt, BigUint, Sign}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; +use std::cmp::Ordering; fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0x11b5_519d) } +fn uint(limbs: &[u64]) -> BigUint { + limbs + .iter() + .rev() + .fold(BigUint::ZERO, |acc, &l| (acc << 64u32) + l) +} + +/// Truncate to the low `N` limbs, little-endian. +fn to_limbs(v: &BigUint) -> [u64; N] { + let mut digits = v.to_u64_digits(); + digits.resize(N.max(digits.len()), 0); + std::array::from_fn(|i| digits[i]) +} + #[test] -fn limbs_arithmetic_matches() { +fn limbs_arithmetic_vs_bigint() { let mut rng = rng(); for _ in 0..500 { let a: [u64; 4] = rng.gen(); let b: [u64; 4] = rng.gen(); - let (ba, bb) = (base::Limbs::new(a), base::Limbs::new(b)); let (ta, tb) = (two::Limbs::new(a), two::Limbs::new(b)); + let (va, vb) = (uint(&a), uint(&b)); + let prod = &va * &vb; - assert_eq!(ta.mul_trunc::<4, 6>(&tb).0, ba.mul_trunc::<4, 6>(&bb).0); - assert_eq!(ta.mul_trunc::<4, 2>(&tb).0, ba.mul_trunc::<4, 2>(&bb).0); - assert_eq!(ta.mul_low(&tb).0, ba.mul_low(&bb).0); - assert_eq!(ta.add_trunc::<4, 4>(&tb).0, ba.add_trunc::<4, 4>(&bb).0); - assert_eq!(ta.sub_trunc::<4, 4>(&tb).0, ba.sub_trunc::<4, 4>(&bb).0); - assert_eq!(ta.cmp(&tb), ba.cmp(&bb)); - assert_eq!(ta.num_bits(), ba.num_bits()); - assert_eq!(format!("{ta}"), format!("{ba}")); - assert_eq!(format!("{ta:?}"), format!("{ba:?}")); + assert_eq!(ta.mul_trunc::<4, 6>(&tb).0, to_limbs::<6>(&prod)); + assert_eq!(ta.mul_trunc::<4, 2>(&tb).0, to_limbs::<2>(&prod)); + assert_eq!(ta.mul_low(&tb).0, to_limbs::<4>(&prod)); + assert_eq!(ta.add_trunc::<4, 4>(&tb).0, to_limbs::<4>(&(&va + &vb))); + let diff = (&va + (BigUint::from(1u32) << 256u32)) - &vb; + assert_eq!(ta.sub_trunc::<4, 4>(&tb).0, to_limbs::<4>(&diff)); + assert_eq!(ta.cmp(&tb), va.cmp(&vb)); + assert_eq!(ta.num_bits() as u64, va.bits()); + assert_eq!(format!("{ta}"), format!("{va:x}")); + let debug: Vec = a.iter().map(|l| format!("{l:#018x}")).collect(); + assert_eq!(format!("{ta:?}"), format!("Limbs([{}])", debug.join(", "))); - let (mut ca, mut cb) = (ta, ba); - assert_eq!(ca.add_with_carry(&tb), cb.add_with_carry(&bb)); - assert_eq!(ca.0, cb.0); - assert_eq!(ca.sub_with_borrow(&tb), cb.sub_with_borrow(&bb)); - assert_eq!(ca.0, cb.0); + let mut ca = ta; + let sum = &va + &vb; + assert_eq!(ca.add_with_carry(&tb), sum.bits() > 256); + assert_eq!(ca.0, to_limbs::<4>(&sum)); + // Subtraction runs on the carried (truncated) value. + let cur = uint(&ca.0); + assert_eq!(ca.sub_with_borrow(&tb), cur < vb); + let diff = (&cur + (BigUint::from(1u32) << 256u32)) - &vb; + assert_eq!(ca.0, to_limbs::<4>(&diff)); } } #[test] -fn limbs_fmadd_matches() { +fn limbs_fmadd_vs_bigint() { let mut rng = rng(); - let mut base_acc = base::Limbs::<5>::zero(); - let mut two_acc = two::Limbs::<5>::zero(); + let mut acc = two::Limbs::<5>::zero(); + let mut expect = BigUint::ZERO; + let mask = (BigUint::from(1u32) << 320u32) - 1u32; for _ in 0..5000 { let a: [u64; 2] = rng.gen(); let b: [u64; 2] = rng.gen(); - base_acc.fmadd::<2, 2>(&base::Limbs::new(a), &base::Limbs::new(b)); - two_acc.fmadd::<2, 2>(&two::Limbs::new(a), &two::Limbs::new(b)); + acc.fmadd::<2, 2>(&two::Limbs::new(a), &two::Limbs::new(b)); + expect = (expect + uint(&a) * uint(&b)) & &mask; } - assert_eq!(two_acc.0, base_acc.0); + assert_eq!(acc.0, to_limbs::<5>(&expect)); } #[test] @@ -64,50 +88,117 @@ fn limbs_u128_oracle() { } } -fn to_base_signed( - t: &two::signed::SignedBigInt, -) -> base::signed::SignedBigInt { - base::signed::SignedBigInt::new(t.magnitude.0, t.is_positive) +/// Sign-magnitude oracle mirroring the mathematical sign-magnitude rules +/// with magnitudes wrapping at `width_bits` (matching truncated limb ops). +#[derive(Clone)] +struct SignedOracle { + mag: BigUint, + pos: bool, + width_bits: u32, +} + +impl SignedOracle { + fn new(mag: BigUint, pos: bool, width_bits: u32) -> Self { + Self { + mag, + pos, + width_bits, + } + } + + fn mask(&self) -> BigUint { + (BigUint::from(1u32) << self.width_bits) - 1u32 + } + + fn add(&self, rhs: &Self) -> Self { + if self.pos == rhs.pos { + Self::new( + (&self.mag + &rhs.mag) & self.mask(), + self.pos, + self.width_bits, + ) + } else if self.mag >= rhs.mag { + Self::new(&self.mag - &rhs.mag, self.pos, self.width_bits) + } else { + Self::new(&rhs.mag - &self.mag, rhs.pos, self.width_bits) + } + } + + fn sub(&self, rhs: &Self) -> Self { + self.add(&Self::new(rhs.mag.clone(), !rhs.pos, rhs.width_bits)) + } + + fn mul(&self, rhs: &Self) -> Self { + self.mul_to_width(rhs, self.width_bits) + } + + fn mul_to_width(&self, rhs: &Self, width_bits: u32) -> Self { + let mask = (BigUint::from(1u32) << width_bits) - 1u32; + Self::new( + (&self.mag * &rhs.mag) & mask, + self.pos == rhs.pos, + width_bits, + ) + } + + fn neg(&self) -> Self { + Self::new(self.mag.clone(), !self.pos, self.width_bits) + } + + fn value(&self) -> BigInt { + let sign = if self.pos { Sign::Plus } else { Sign::Minus }; + BigInt::from_biguint(sign, self.mag.clone()) + } + + /// Signed comparison; treats `+0` and `-0` as equal. + fn cmp(&self, rhs: &Self) -> Ordering { + self.value().cmp(&rhs.value()) + } +} + +fn oracle_of_signed(t: &two::signed::SignedBigInt) -> SignedOracle { + SignedOracle::new(uint(&t.magnitude.0), t.is_positive, 64 * N as u32) } fn assert_signed_matches( ours: two::signed::SignedBigInt, - theirs: base::signed::SignedBigInt, + oracle: &SignedOracle, ) { - assert_eq!(ours.magnitude.0, theirs.magnitude.0, "magnitude diverges"); + assert_eq!( + ours.magnitude.0, + to_limbs::(&oracle.mag), + "magnitude diverges" + ); if !ours.magnitude.is_zero() { - assert_eq!(ours.is_positive, theirs.is_positive, "sign diverges"); + assert_eq!(ours.is_positive, oracle.pos, "sign diverges"); } } #[test] -fn signed_bigint_ops_match() { +fn signed_bigint_ops_match_bigint() { let mut rng = rng(); for _ in 0..500 { let (la, sa): ([u64; 2], bool) = (rng.gen(), rng.gen()); let (lb, sb): ([u64; 2], bool) = (rng.gen(), rng.gen()); let ta = two::signed::SignedBigInt::new(la, sa); let tb = two::signed::SignedBigInt::new(lb, sb); - let (ba, bb) = (to_base_signed(&ta), to_base_signed(&tb)); + let (oa, ob) = (oracle_of_signed(&ta), oracle_of_signed(&tb)); - assert_signed_matches(ta + tb, ba + bb); - assert_signed_matches(ta - tb, ba - bb); - assert_signed_matches(ta * tb, ba * bb); - assert_signed_matches(-ta, -ba); - assert_eq!(ta.cmp(&tb), ba.cmp(&bb)); - assert_eq!(ta == tb, ba == bb); - assert_signed_matches(ta.mul_trunc::<2, 3>(&tb), ba.mul_trunc::<2, 3>(&bb)); - assert_eq!(ta.magnitude_limbs(), ba.magnitude_limbs()); + assert_signed_matches(ta + tb, &oa.add(&ob)); + assert_signed_matches(ta - tb, &oa.sub(&ob)); + assert_signed_matches(ta * tb, &oa.mul(&ob)); + assert_signed_matches(-ta, &oa.neg()); + assert_eq!(ta.cmp(&tb), oa.cmp(&ob)); + assert_eq!(ta == tb, oa.cmp(&ob) == Ordering::Equal); + assert_signed_matches(ta.mul_trunc::<2, 3>(&tb), &oa.mul_to_width(&ob, 192)); + assert_eq!(ta.magnitude_limbs(), la); let mut x = ta; x += tb; x *= ta; x -= tb; - let mut bx = ba; - bx += bb; - bx *= ba; - bx -= bb; - assert_signed_matches(x, bx); + let ox = oa.add(&ob).mul(&oa).sub(&ob); + assert_signed_matches(x, &ox); } } @@ -151,71 +242,70 @@ fn signed_bigint_i128_oracle() { assert_eq!(pos_zero.cmp(&neg_zero), std::cmp::Ordering::Equal); } -fn to_base_hi32( - t: &two::signed::SignedBigIntHi32, -) -> base::signed::SignedBigIntHi32 { - base::signed::SignedBigIntHi32::new(*t.magnitude_lo(), t.magnitude_hi(), t.is_positive()) +fn oracle_of_hi32(t: &two::signed::SignedBigIntHi32) -> SignedOracle { + let mag = uint(t.magnitude_lo()) + (BigUint::from(t.magnitude_hi()) << (64 * N as u32)); + SignedOracle::new(mag, t.is_positive(), 64 * N as u32 + 32) } fn assert_hi32_matches( ours: two::signed::SignedBigIntHi32, - theirs: base::signed::SignedBigIntHi32, + oracle: &SignedOracle, ) { - assert_eq!(ours.magnitude_lo(), theirs.magnitude_lo(), "lo diverges"); - assert_eq!(ours.magnitude_hi(), theirs.magnitude_hi(), "hi diverges"); + assert_eq!( + ours.magnitude_lo(), + &to_limbs::(&oracle.mag), + "lo diverges" + ); + let hi: BigUint = &oracle.mag >> (64 * N as u32); + assert_eq!(BigUint::from(ours.magnitude_hi()), hi, "hi diverges"); let zero = ours.magnitude_hi() == 0 && ours.magnitude_lo().iter().all(|&l| l == 0); if !zero { - assert_eq!(ours.is_positive(), theirs.is_positive(), "sign diverges"); + assert_eq!(ours.is_positive(), oracle.pos, "sign diverges"); } } #[test] -fn hi32_ops_match() { +fn hi32_ops_match_bigint() { let mut rng = rng(); // S96 (N=1), S160 (N=2), S224 (N=3): exercises the general schoolbook - // multiply against the baseline's hand-unrolled N=1/N=2 kernels. + // multiply at every stamped width, wrapping at 64N + 32 bits. for _ in 0..500 { let a96 = two::signed::S96::new([rng.gen()], rng.gen(), rng.gen()); let b96 = two::signed::S96::new([rng.gen()], rng.gen(), rng.gen()); - assert_hi32_matches(a96 + b96, to_base_hi32(&a96) + to_base_hi32(&b96)); - assert_hi32_matches(a96 - b96, to_base_hi32(&a96) - to_base_hi32(&b96)); - assert_hi32_matches(a96 * b96, to_base_hi32(&a96) * to_base_hi32(&b96)); + let (oa96, ob96) = (oracle_of_hi32(&a96), oracle_of_hi32(&b96)); + assert_hi32_matches(a96 + b96, &oa96.add(&ob96)); + assert_hi32_matches(a96 - b96, &oa96.sub(&ob96)); + assert_hi32_matches(a96 * b96, &oa96.mul(&ob96)); let a160 = two::signed::S160::new(rng.gen(), rng.gen(), rng.gen()); let b160 = two::signed::S160::new(rng.gen(), rng.gen(), rng.gen()); - assert_hi32_matches(a160 + b160, to_base_hi32(&a160) + to_base_hi32(&b160)); - assert_hi32_matches(a160 - b160, to_base_hi32(&a160) - to_base_hi32(&b160)); - assert_eq!( - a160.cmp(&b160), - to_base_hi32(&a160).cmp(&to_base_hi32(&b160)) - ); - // Baseline's unrolled S160 mul kernel originally overflowed u128 in - // its cross-term sum for large second limbs; fixed on the PR #1684 - // branch, so the vs-baseline comparison runs on the full range. - assert_hi32_matches(a160 * b160, to_base_hi32(&a160) * to_base_hi32(&b160)); + let (oa, ob) = (oracle_of_hi32(&a160), oracle_of_hi32(&b160)); + assert_hi32_matches(a160 + b160, &oa.add(&ob)); + assert_hi32_matches(a160 - b160, &oa.sub(&ob)); + assert_eq!(a160.cmp(&b160), oa.cmp(&ob)); + // The overflow-safe mac chains must stay exact on the full range + // (jolt-field's original unrolled S160 kernel wrapped u128 here). + assert_hi32_matches(a160 * b160, &oa.mul(&ob)); let a224 = two::signed::S224::new(rng.gen(), rng.gen(), rng.gen()); let b224 = two::signed::S224::new(rng.gen(), rng.gen(), rng.gen()); - assert_hi32_matches(a224 + b224, to_base_hi32(&a224) + to_base_hi32(&b224)); - assert_hi32_matches(a224 * b224, to_base_hi32(&a224) * to_base_hi32(&b224)); + let (oa224, ob224) = (oracle_of_hi32(&a224), oracle_of_hi32(&b224)); + assert_hi32_matches(a224 + b224, &oa224.add(&ob224)); + assert_hi32_matches(a224 * b224, &oa224.mul(&ob224)); // Neg and assign forms. let mut x = a160; x += b160; x -= a160; - let mut bx = to_base_hi32(&a160); - bx += to_base_hi32(&b160); - bx -= to_base_hi32(&a160); - assert_hi32_matches(x, bx); - assert_hi32_matches(-a96, -to_base_hi32(&a96)); + assert_hi32_matches(x, &oa.add(&ob).sub(&oa)); + assert_hi32_matches(-a96, &oa96.neg()); } } #[test] fn hi32_mul_full_range_oracle() { // Full-limb SignedBigInt<3> multiplication (overflow-safe mac chains) as - // the reference for S160 multiply across the ENTIRE input domain, - // including where the baseline hi32 kernel wraps. + // a second reference for S160 multiply across the ENTIRE input domain. let mut rng = rng(); for _ in 0..1000 { let a = two::signed::S160::new(rng.gen(), rng.gen(), rng.gen()); @@ -233,22 +323,27 @@ fn hi32_mul_full_range_oracle() { } #[test] -fn hi32_conversions_match() { +fn hi32_conversions_match_bigint() { let mut rng = rng(); for _ in 0..300 { - let v = two::signed::S160::new(rng.gen(), rng.gen(), rng.gen()); - let bv = to_base_hi32(&v); + let (lo, hi): ([u64; 2], u32) = (rng.gen(), rng.gen()); + let v = two::signed::S160::new(lo, hi, rng.gen()); let sb = v.to_signed_bigint_nplus1::<3>(); - let bsb = bv.to_signed_bigint_nplus1::<3>(); - assert_signed_matches(sb, bsb); + assert_eq!(sb.magnitude_limbs(), [lo[0], lo[1], hi as u64]); + if hi != 0 || lo != [0, 0] { + assert_eq!(sb.is_positive, v.is_positive()); + } let u: u128 = rng.gen(); - assert_hi32_matches(two::signed::S160::from(u), base::signed::S160::from(u)); + let from = two::signed::S160::from(u); + assert_eq!(from.magnitude_lo(), &[u as u64, (u >> 64) as u64]); + assert_eq!(from.magnitude_hi(), 0); + assert!(from.is_positive()); } // Addition carries into the u32 tail through the public ops. let big = two::signed::S160::from(u128::MAX); let sum = big + big; - let base_sum = base::signed::S160::from(u128::MAX) + base::signed::S160::from(u128::MAX); - assert_hi32_matches(sum, base_sum); + let expect = oracle_of_hi32(&big).add(&oracle_of_hi32(&big)); + assert_hi32_matches(sum, &expect); assert_eq!(sum.magnitude_hi(), 1); } diff --git a/crates/jolt-field-two/tests/solinas_ext_differential.rs b/crates/jolt-field-two/tests/solinas_ext_differential.rs index af6067bc53..22a2f30e42 100644 --- a/crates/jolt-field-two/tests/solinas_ext_differential.rs +++ b/crates/jolt-field-two/tests/solinas_ext_differential.rs @@ -1,37 +1,33 @@ //! Differential tests for the extension towers (`FpExt2`/`FpExt4`/`FpExt8`) -//! against jolt-field, with an independent schoolbook oracle: polynomial -//! multiplication modulo the defining relation implemented directly here -//! over `u128` values (256-bit limb multiply + binary long division for the -//! base-field modular ops — no Solinas folding, no shared code). +//! against an independent schoolbook oracle: polynomial multiplication +//! modulo the defining relation implemented directly here over `u128` +//! values (256-bit limb multiply + binary long division for the base-field +//! modular ops — no Solinas folding, no shared code). //! //! Coverage: both `FpExt2` non-residue configs and the quartic/octic towers //! over `Fp32`/`Fp64`/`Fp128` bases (registered primes plus `Fp32<251>`, //! the one small prime with `p ≡ 3 mod 4` where `NegOneNr` is a genuine -//! field, and `Fp64<2^32 − 99>`, the base the baseline's own ext tests -//! use). Where the extension is not known to be a field (reducible defining -//! polynomial), every check is strict parity with the baseline rather than -//! a field identity. +//! field, and `Fp64<2^32 − 99>`). Where the extension is not known to be a +//! field (reducible defining polynomial), inversion is only verified when +//! it succeeds (`x · x⁻¹ = 1`); a spurious `None` for an invertible element +//! is not detectable without a polynomial-gcd oracle. #![cfg(feature = "solinas")] #![expect(clippy::unwrap_used, reason = "test code")] -use jolt_field as base; use jolt_field_two as two; -use base::{ - CanonicalField, ExtField as BaseExtField, FieldCore, FromPrimitiveInt, HalvingField, RingCore, -}; use num_traits::{One, Zero}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use two::{CanonicalBytes, CanonicalEncoding, ExtField, Field, Ring}; +use two::{CanonicalBytes, CanonicalEncoding, ExtField, Field, PseudoMersenne, Ring}; fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0xE87_D1FF) } /// 128×128 → 256-bit schoolbook multiply over 64-bit halves (independent of -/// both crates' `mul_wide`). +/// the crate's `mul_wide`). fn oracle_mul_256(a: u128, b: u128) -> [u64; 4] { let (a0, a1) = (a as u64 as u128, a >> 64); let (b0, b1) = (b as u64 as u128, b >> 64); @@ -122,18 +118,31 @@ fn cheb_mul_oracle(a: &[u128], b: &[u128], p: u128) -> Vec { out } -/// Full differential + oracle sweep for one paired extension instantiation. +/// `x^e` via square-and-multiply over the crate's extension multiply (which +/// the same suite verifies against the schoolbook oracle). +fn ext_pow(mut base: E, mut e: u128) -> E { + let mut acc = E::one(); + while e > 0 { + if (e & 1) == 1 { + acc *= base; + } + base *= base; + e >>= 1; + } + acc +} + +/// Full oracle sweep for one extension instantiation. /// -/// `is_field: false` keeps every check strict parity with the baseline but -/// does not require nonzero elements to invert (reducible defining -/// polynomial over that base). +/// `is_field: false` runs every ring-level check but does not require +/// nonzero elements to invert (reducible defining polynomial over that +/// base). macro_rules! check_ext { - ($E2:ty, $EB:ty, $F2:ty, $FB:ty, $p:expr, $d:expr, $oracle:expr, is_field: $is_field:expr, $rng:expr) => {{ + ($E2:ty, $F2:ty, $p:expr, $d:expr, $oracle:expr, is_field: $is_field:expr, $rng:expr) => {{ let p: u128 = $p; let d: usize = $d; let oracle = $oracle; assert_eq!(<$E2 as ExtField<$F2>>::DEGREE, d); - assert_eq!(<$EB as BaseExtField<$FB>>::EXT_DEGREE, d); assert_eq!( std::mem::size_of::<$E2>(), std::mem::size_of::<[$F2; $d]>(), @@ -141,29 +150,17 @@ macro_rules! check_ext { ); let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); - let fb = |v: u128| <$FB as CanonicalField>::from_canonical_u128_checked(v).unwrap(); let mk2 = |vals: &[u128]| { <$E2 as ExtField<$F2>>::from_base_slice( &vals.iter().map(|&v| f2(v)).collect::>(), ) }; - let mkb = |vals: &[u128]| { - <$EB as BaseExtField<$FB>>::from_base_slice( - &vals.iter().map(|&v| fb(v)).collect::>(), - ) - }; let vec2 = |e: &$E2| { <$E2 as ExtField<$F2>>::to_base_vec(e) .iter() .map(|c| c.to_u128_checked().unwrap()) .collect::>() }; - let vecb = |e: &$EB| { - <$EB as BaseExtField<$FB>>::to_base_vec(e) - .iter() - .map(|c| c.to_canonical_u128()) - .collect::>() - }; let sample = |rng: &mut ChaCha20Rng| -> Vec { (0..d).map(|_| rng.gen::() % p).collect() }; @@ -171,23 +168,31 @@ macro_rules! check_ext { let cfg = bincode::config::standard(); for _ in 0..48 { let (va, vb, vc) = (sample($rng), sample($rng), sample($rng)); - let (xa, ba) = (mk2(&va), mkb(&va)); - let (ya, yb) = (mk2(&vb), mkb(&vb)); - let (za, _zb) = (mk2(&vc), mkb(&vc)); + let xa = mk2(&va); + let ya = mk2(&vb); + let za = mk2(&vc); // from_base_slice / to_base_vec round trip. assert_eq!(vec2(&xa), va); - assert_eq!(vecb(&ba), va); - // Arithmetic vs baseline, mul/square also vs the schoolbook oracle. - assert_eq!(vec2(&(xa + ya)), vecb(&(ba + yb))); - assert_eq!(vec2(&(xa - ya)), vecb(&(ba - yb))); - assert_eq!(vec2(&(-xa)), vecb(&(-ba))); + // Arithmetic vs the coefficient-wise / schoolbook oracles. + let add_expect: Vec = va + .iter() + .zip(vb.iter()) + .map(|(&x, &y)| addmod(x, y, p)) + .collect(); + let sub_expect: Vec = va + .iter() + .zip(vb.iter()) + .map(|(&x, &y)| submod(x, y, p)) + .collect(); + let neg_expect: Vec = va.iter().map(|&x| submod(0, x, p)).collect(); + assert_eq!(vec2(&(xa + ya)), add_expect, "add vs oracle"); + assert_eq!(vec2(&(xa - ya)), sub_expect, "sub vs oracle"); + assert_eq!(vec2(&(-xa)), neg_expect, "neg vs oracle"); let prod = xa * ya; - assert_eq!(vec2(&prod), vecb(&(ba * yb))); assert_eq!(vec2(&prod), oracle(&va, &vb), "mul vs schoolbook oracle"); let sq = Ring::square(&xa); - assert_eq!(vec2(&sq), vecb(&RingCore::square(&ba))); assert_eq!(vec2(&sq), oracle(&va, &va), "square vs schoolbook oracle"); // By-ref and assigning operator forms agree with the owned ones. @@ -204,62 +209,55 @@ macro_rules! check_ext { assert_eq!((xa + ya) * za, xa * za + ya * za, "distributivity"); assert_eq!((xa * ya) * za, xa * (ya * za), "associativity"); - // Inversion: strict parity, plus the field identity when Some. - match (xa.inverse(), ba.inverse()) { - (Some(ti), Some(bi)) => { - assert_eq!(vec2(&ti), vecb(&bi)); - assert_eq!(ti * xa, <$E2 as One>::one()); - } - (ti, bi) => { - assert_eq!(ti.is_none(), bi.is_none(), "inverse parity"); - assert!(!$is_field || xa.is_zero(), "field ext must invert nonzero"); - } + // Inversion: `x · x⁻¹ = 1` pins the value (multiply is + // oracle-verified); in a genuine field nonzero must invert. + match xa.inverse() { + Some(ti) => assert_eq!(ti * xa, <$E2 as One>::one()), + None => assert!(!$is_field || xa.is_zero(), "field ext must invert nonzero"), } // Halving. let h = xa.half(); - assert_eq!(vec2(&h), vecb(&ba.half())); assert_eq!(h + h, xa); // lift_base / mul_base against the full extension multiply. let sv = $rng.gen::() % p; - let (s2, sb) = (f2(sv), fb(sv)); + let s2 = f2(sv); + let lifted = <$E2 as ExtField<$F2>>::lift_base(s2); + let mut lift_expect = vec![0u128; d]; + lift_expect[0] = sv; + assert_eq!(vec2(&lifted), lift_expect, "lift_base embeds at coeff 0"); let m2 = xa.mul_base(s2); - assert_eq!( - m2, - xa * <$E2 as ExtField<$F2>>::lift_base(s2), - "mul_base vs full multiply" - ); - assert_eq!(vec2(&m2), vecb(&ba.mul_base(sb))); - assert_eq!( - vec2(&<$E2 as ExtField<$F2>>::lift_base(s2)), - vecb(&<$EB as BaseExtField<$FB>>::lift_base(sb)) - ); + assert_eq!(m2, xa * lifted, "mul_base vs full multiply"); + let scale_expect: Vec = va.iter().map(|&x| mulmod(x, sv, p)).collect(); + assert_eq!(vec2(&m2), scale_expect, "mul_base scales coefficients"); - // Integer embeddings. + // Integer embeddings: base-field embedding at coefficient 0. let (w64, i64v): (u64, i64) = ($rng.gen(), $rng.gen()); let (w128, i128v): (u128, i128) = ($rng.gen(), $rng.gen()); + let embed = |v: u128, neg: bool| { + let mut out = vec![0u128; d]; + let r = oracle_mod(&[v as u64, (v >> 64) as u64], p); + out[0] = if neg { submod(0, r, p) } else { r }; + out + }; assert_eq!( vec2(&<$E2 as Ring>::from_u64(w64)), - vecb(&<$EB as FromPrimitiveInt>::from_u64(w64)) + embed(w64 as u128, false) ); assert_eq!( vec2(&<$E2 as Ring>::from_i64(i64v)), - vecb(&<$EB as FromPrimitiveInt>::from_i64(i64v)) - ); - assert_eq!( - vec2(&<$E2 as Ring>::from_u128(w128)), - vecb(&<$EB as FromPrimitiveInt>::from_u128(w128)) + embed(i64v.unsigned_abs() as u128, i64v < 0) ); + assert_eq!(vec2(&<$E2 as Ring>::from_u128(w128)), embed(w128, false)); assert_eq!( vec2(&<$E2 as Ring>::from_i128(i128v)), - vecb(&<$EB as FromPrimitiveInt>::from_i128(i128v)) + embed(i128v.unsigned_abs(), i128v < 0) ); - // Wire bytes: baseline equality, structural shape, round trip. + // Wire bytes: structural shape and round trip (absolute bytes + // are pinned by the golden fixtures in golden_bytes.rs). let t_bytes = bincode::serde::encode_to_vec(xa, cfg).unwrap(); - let b_bytes = bincode::serde::encode_to_vec(ba, cfg).unwrap(); - assert_eq!(t_bytes, b_bytes, "wire bytes diverge"); let expected: Vec = <$E2 as ExtField<$F2>>::to_base_vec(&xa) .iter() .flat_map(|c| c.to_bytes_le_vec()) @@ -269,21 +267,34 @@ macro_rules! check_ext { assert_eq!(back, xa); } - // Frobenius powers 0..2·degree: parity for pow, inv_pow, and the - // roundtrip (the roundtrip equals the identity in a genuine field). + // Frobenius powers 0..2·degree against the semantic definition: + // one Frobenius application is `x ↦ x^q` (computed with a test-local + // square-and-multiply over the oracle-verified extension multiply), + // `frobenius_pow(·, k)` applies it `k mod d` times, and + // `frobenius_inv_pow` is its inverse power. + let q = two::pseudo_mersenne_modulus( + <$F2 as CanonicalEncoding>::MODULUS_BITS, + <$F2 as PseudoMersenne>::OFFSET, + ) + .unwrap(); for _ in 0..2 { let v = sample($rng); - let (x2, xb2) = (mk2(&v), mkb(&v)); + let x2 = mk2(&v); for power in 0..=(2 * d) { + let mut expect = x2; + for _ in 0..(power % d) { + expect = ext_pow(expect, q); + } let ft = <$E2 as ExtField<$F2>>::frobenius_pow(x2, power); - let fb2 = <$EB as BaseExtField<$FB>>::frobenius_pow(xb2, power); - assert_eq!(vec2(&ft), vecb(&fb2), "frobenius_pow({power})"); + assert_eq!(ft, expect, "frobenius_pow({power}) vs x^(q^k)"); let gt = <$E2 as ExtField<$F2>>::frobenius_inv_pow(x2, power); - let gb = <$EB as BaseExtField<$FB>>::frobenius_inv_pow(xb2, power); - assert_eq!(vec2(>), vecb(&gb), "frobenius_inv_pow({power})"); + let inv_power = (d - (power % d)) % d; + let mut inv_expect = x2; + for _ in 0..inv_power { + inv_expect = ext_pow(inv_expect, q); + } + assert_eq!(gt, inv_expect, "frobenius_inv_pow({power})"); let rt = <$E2 as ExtField<$F2>>::frobenius_inv_pow(ft, power); - let rb = <$EB as BaseExtField<$FB>>::frobenius_inv_pow(fb2, power); - assert_eq!(vec2(&rt), vecb(&rb), "frobenius roundtrip parity"); if $is_field { assert_eq!(rt, x2, "frobenius roundtrip is the identity"); } @@ -303,11 +314,9 @@ macro_rules! check_ext { } for va in &patterns { for vb in &patterns { - let (x2, xb2) = (mk2(va), mkb(va)); - let (y2, yb2) = (mk2(vb), mkb(vb)); + let (x2, y2) = (mk2(va), mk2(vb)); let prod = x2 * y2; - assert_eq!(vec2(&prod), vecb(&(xb2 * yb2)), "boundary {va:?}·{vb:?}"); - assert_eq!(vec2(&prod), oracle(va, vb), "boundary vs oracle"); + assert_eq!(vec2(&prod), oracle(va, vb), "boundary {va:?}·{vb:?}"); } let x2 = mk2(va); assert_eq!(vec2(&Ring::square(&x2)), oracle(va, va), "boundary square"); @@ -330,7 +339,7 @@ macro_rules! check_ext { assert_eq!(xs.iter().product::<$E2>(), expected_prod); // Canonical rejection: a wire encoding whose first coefficient is - // `p` itself must be rejected by both crates; so must short input. + // `p` itself must be rejected; so must short input. let nb = <$F2 as CanonicalBytes>::NUM_BYTES; let mut bad = vec![0u8; nb * d]; bad[..nb].copy_from_slice(&p.to_le_bytes()[..nb]); @@ -338,90 +347,72 @@ macro_rules! check_ext { bincode::serde::decode_from_slice::<$E2, _>(&bad, cfg).is_err(), "non-canonical coefficient must be rejected" ); - assert!( - bincode::serde::decode_from_slice::<$EB, _>(&bad, cfg).is_err(), - "baseline rejects the same encoding" - ); assert!( bincode::serde::decode_from_slice::<$E2, _>(&bad[..nb * d - 1], cfg).is_err(), "truncated encoding must be rejected" ); - // Identical random sampling: same seed, same element stream. + // Random sampling spec: with the same seed, `random` draws the `d` + // base-field coefficients in order. let (mut r1, mut r2) = ( ChaCha20Rng::seed_from_u64(0x5EED_0001), ChaCha20Rng::seed_from_u64(0x5EED_0001), ); for _ in 0..20 { let t: $E2 = Field::random(&mut r1); - let b: $EB = FieldCore::random(&mut r2); - assert_eq!(vec2(&t), vecb(&b), "random stream diverges"); + let coeffs: Vec<$F2> = (0..d).map(|_| <$F2 as Field>::random(&mut r2)).collect(); + let expected = <$E2 as ExtField<$F2>>::from_base_slice(&coeffs); + assert_eq!(t, expected, "random stream diverges from coefficient draws"); } }}; } -/// Frobenius/Moore machinery parity: canonical thetas, validate, solve -/// (values and Ok/Err), plus rejection cases. +/// Frobenius/Moore machinery: canonical thetas are the packing basis, +/// solutions satisfy the Moore system in oracle-verified arithmetic, and +/// singular/mismatched inputs are rejected. In a genuine field the basis +/// thetas are linearly independent, so validate/solve must succeed. macro_rules! check_moore { - ($E2:ty, $EB:ty, $F2:ty, $FB:ty, $p:expr, $d:expr, $rng:expr) => {{ + ($E2:ty, $F2:ty, $p:expr, $d:expr, is_field: $is_field:expr, $rng:expr) => {{ let p: u128 = $p; let d: usize = $d; let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); - let fb = |v: u128| <$FB as CanonicalField>::from_canonical_u128_checked(v).unwrap(); let mk2 = |vals: &[u128]| { <$E2 as ExtField<$F2>>::from_base_slice( &vals.iter().map(|&v| f2(v)).collect::>(), ) }; - let mkb = |vals: &[u128]| { - <$EB as BaseExtField<$FB>>::from_base_slice( - &vals.iter().map(|&v| fb(v)).collect::>(), - ) - }; let vec2 = |e: &$E2| { <$E2 as ExtField<$F2>>::to_base_vec(e) .iter() .map(|c| c.to_u128_checked().unwrap()) .collect::>() }; - let vecb = |e: &$EB| { - <$EB as BaseExtField<$FB>>::to_base_vec(e) - .iter() - .map(|c| c.to_canonical_u128()) - .collect::>() - }; for w in 1..=d { let t = two::canonical_frobenius_thetas::<$F2, $E2>(w).unwrap(); - let b = base::canonical_frobenius_thetas::<$FB, $EB>(w).unwrap(); assert_eq!(t.len(), w); - for (idx, (te, be)) in t.iter().zip(b.iter()).enumerate() { - assert_eq!(vec2(te), vecb(be), "theta {idx} parity"); + for (idx, te) in t.iter().enumerate() { let mut basis = vec![0u128; d]; basis[idx] = 1; assert_eq!(vec2(te), basis, "thetas are the packing basis"); } let tv = two::validate_canonical_frobenius_thetas::<$F2, $E2>(w); - let bv = base::validate_canonical_frobenius_thetas::<$FB, $EB>(w); - assert_eq!(tv.is_ok(), bv.is_ok(), "validate parity at width {w}"); + if $is_field { + assert!(tv.is_ok(), "basis thetas must validate in a field"); + } } assert!(two::canonical_frobenius_thetas::<$F2, $E2>(d + 1).is_err()); - assert!(base::canonical_frobenius_thetas::<$FB, $EB>(d + 1).is_err()); let thetas_t = two::canonical_frobenius_thetas::<$F2, $E2>(d).unwrap(); - let thetas_b = base::canonical_frobenius_thetas::<$FB, $EB>(d).unwrap(); let rhs_vals: Vec> = (0..d) .map(|_| (0..d).map(|_| $rng.gen::() % p).collect()) .collect(); let rhs_t: Vec<$E2> = rhs_vals.iter().map(|v| mk2(v)).collect(); - let rhs_b: Vec<$EB> = rhs_vals.iter().map(|v| mkb(v)).collect(); let st = two::solve_frobenius_moore::<$F2, $E2>(&thetas_t, &rhs_t); - let sb = base::solve_frobenius_moore::<$FB, $EB>(&thetas_b, &rhs_b); - assert_eq!(st.is_ok(), sb.is_ok(), "solve parity"); - if let (Ok(zt), Ok(zb)) = (st, sb) { - for (a, b) in zt.iter().zip(zb.iter()) { - assert_eq!(vec2(a), vecb(b), "Moore solution parity"); - } + if $is_field { + assert!(st.is_ok(), "Moore solve must succeed in a field"); + } + if let Ok(zt) = st { // The solution satisfies the Moore system in rebuilt arithmetic. for (row, want) in rhs_t.iter().enumerate() { let got = thetas_t @@ -437,12 +428,10 @@ macro_rules! check_moore { // Rejections: duplicate thetas (singular) and dimension mismatch. if d >= 2 { let one2 = <$E2 as One>::one(); - let oneb = <$EB as One>::one(); assert!( two::solve_frobenius_moore::<$F2, $E2>(&[one2, one2], &[one2, one2]).is_err(), "duplicate thetas must be singular" ); - assert!(base::solve_frobenius_moore::<$FB, $EB>(&[oneb, oneb], &[oneb, oneb]).is_err()); } assert!( two::solve_frobenius_moore::<$F2, $E2>(&thetas_t, &rhs_t[..d - 1]).is_err(), @@ -456,21 +445,17 @@ const P64: u128 = (1 << 64) - 59; const P128: u128 = u128::MAX - 274; const P251: u128 = 251; -// `2^32 − 99` as a u64-backed field: the base the baseline's own ext tests -// exercise (`Fp64<4294967197>`). +// `2^32 − 99` as a u64-backed field: exercises the sub-word Fp64 towers. type F64Small2 = two::Fp64<4_294_967_197>; -type F64SmallB = base::Fp64<4_294_967_197>; macro_rules! ext_suite { - ($name2:ident, $name4:ident, $name8:ident, $moore:ident, $F2:ty, $FB:ty, $p:expr, e2_field: $e2f:expr, neg_one_field: $nof:expr, e48_field: $e48f:expr) => { + ($name2:ident, $name4:ident, $name8:ident, $moore:ident, $F2:ty, $p:expr, e2_field: $e2f:expr, neg_one_field: $nof:expr, e48_field: $e48f:expr) => { #[test] fn $name2() { let mut rng = rng(); check_ext!( two::FpExt2<$F2, two::TwoNr>, - base::FpExt2<$FB, base::TwoNr>, $F2, - $FB, $p, 2, |a: &[u128], b: &[u128]| quad_mul_oracle(a, b, 2, $p), @@ -479,9 +464,7 @@ macro_rules! ext_suite { ); check_ext!( two::FpExt2<$F2, two::NegOneNr>, - base::FpExt2<$FB, base::NegOneNr>, $F2, - $FB, $p, 2, |a: &[u128], b: &[u128]| quad_mul_oracle(a, b, $p - 1, $p), @@ -495,9 +478,7 @@ macro_rules! ext_suite { let mut rng = rng(); check_ext!( two::FpExt4<$F2>, - base::FpExt4<$FB>, $F2, - $FB, $p, 4, |a: &[u128], b: &[u128]| cheb_mul_oracle(a, b, $p), @@ -511,9 +492,7 @@ macro_rules! ext_suite { let mut rng = rng(); check_ext!( two::FpExt8<$F2>, - base::FpExt8<$FB>, $F2, - $FB, $p, 8, |a: &[u128], b: &[u128]| cheb_mul_oracle(a, b, $p), @@ -527,15 +506,14 @@ macro_rules! ext_suite { let mut rng = rng(); check_moore!( two::FpExt2<$F2, two::TwoNr>, - base::FpExt2<$FB, base::TwoNr>, $F2, - $FB, $p, 2, + is_field: $e2f, &mut rng ); - check_moore!(two::FpExt4<$F2>, base::FpExt4<$FB>, $F2, $FB, $p, 4, &mut rng); - check_moore!(two::FpExt8<$F2>, base::FpExt8<$FB>, $F2, $FB, $p, 8, &mut rng); + check_moore!(two::FpExt4<$F2>, $F2, $p, 4, is_field: $e48f, &mut rng); + check_moore!(two::FpExt8<$F2>, $F2, $p, 8, is_field: $e48f, &mut rng); } }; } @@ -546,7 +524,6 @@ ext_suite!( ext8_over_prime32_offset99, moore_over_prime32_offset99, two::Prime32Offset99, - base::Prime32Offset99, P32, e2_field: true, neg_one_field: false, @@ -559,7 +536,6 @@ ext_suite!( ext8_over_fp32_251, moore_over_fp32_251, two::Fp32<251>, - base::Fp32<251>, P251, e2_field: true, neg_one_field: true, @@ -572,7 +548,6 @@ ext_suite!( ext8_over_prime64_offset59, moore_over_prime64_offset59, two::Prime64Offset59, - base::Prime64Offset59, P64, e2_field: true, neg_one_field: false, @@ -585,7 +560,6 @@ ext_suite!( ext8_over_fp64_2pow32_99, moore_over_fp64_2pow32_99, F64Small2, - F64SmallB, P32, e2_field: true, neg_one_field: false, @@ -598,47 +572,47 @@ ext_suite!( ext8_over_prime128_offset275, moore_over_prime128_offset275, two::Prime128Offset275, - base::Prime128Offset275, P128, e2_field: true, neg_one_field: false, e48_field: false ); -/// `Ext2` is the `TwoNr` alias in both crates. +/// `Ext2` is the `TwoNr` alias; conjugate negates the `u` coefficient and +/// `norm(x) = x · conj(x) = x₀² − nr·x₁²` lands in the base field. #[test] -fn ext2_alias_matches() { +fn ext2_alias_and_conjugate_norm() { let mut r = rng(); - let v: u128 = r.gen::() % P64; - let w: u128 = r.gen::() % P64; - let a: two::Ext2 = >::new( - ::from_u128_checked(v).unwrap(), - ::from_u128_checked(w).unwrap(), - ); - let b: base::Ext2 = base::FpExt2::new( - base::Prime64Offset59::from_canonical_u128_checked(v).unwrap(), - base::Prime64Offset59::from_canonical_u128_checked(w).unwrap(), - ); - assert_eq!( - Ring::square(&a) - .conjugate() - .norm() - .to_u128_checked() - .unwrap(), - RingCore::square(&b).conjugate().norm().to_canonical_u128(), - "conjugate/norm parity" - ); + type F = two::Prime64Offset59; + for _ in 0..32 { + let v: u128 = r.gen::() % P64; + let w: u128 = r.gen::() % P64; + let a: two::Ext2 = >::new( + ::from_u128_checked(v).unwrap(), + ::from_u128_checked(w).unwrap(), + ); + let conj = a.conjugate(); + assert_eq!(conj.coeffs[0], a.coeffs[0]); + assert_eq!(conj.coeffs[1], -a.coeffs[1]); + let prod = a * conj; + assert_eq!( + prod, + as ExtField>::lift_base(a.norm()), + "norm is x · conj(x)" + ); + let sq = Ring::square(&a); + // norm(conj(a²)) = norm(a²) = norm(a)², all in oracle-verified mul. + assert_eq!(sq.conjugate().norm(), a.norm() * a.norm()); + } } /// The reflexive impl: a pseudo-Mersenne base field is its own degree-1 -/// extension in both crates. +/// extension. #[test] fn degree_one_reflexive_ext_matches() { let mut r = rng(); type F2 = two::Prime64Offset59; - type FB = base::Prime64Offset59; assert_eq!(>::DEGREE, 1); - assert_eq!(>::EXT_DEGREE, 1); for _ in 0..32 { let v = r.gen::() % P64; let s = r.gen::() % P64; @@ -646,15 +620,8 @@ fn degree_one_reflexive_ext_matches() { ::from_u128_checked(v).unwrap(), ::from_u128_checked(s).unwrap(), ); - let (xb, sb) = ( - FB::from_canonical_u128_checked(v).unwrap(), - FB::from_canonical_u128_checked(s).unwrap(), - ); assert_eq!(>::lift_base(s2), s2); - assert_eq!( - x2.mul_base(s2).to_u128_checked().unwrap(), - xb.mul_base(sb).to_canonical_u128() - ); + assert_eq!(x2.mul_base(s2), x2 * s2); assert_eq!(>::from_base_slice(&[x2]), x2); assert_eq!(>::to_base_vec(&x2), vec![x2]); assert_eq!(>::frobenius_pow(x2, 3), x2); diff --git a/crates/jolt-field-two/tests/solinas_fp128_differential.rs b/crates/jolt-field-two/tests/solinas_fp128_differential.rs index 08ec6899e8..ec015bfda9 100644 --- a/crates/jolt-field-two/tests/solinas_fp128_differential.rs +++ b/crates/jolt-field-two/tests/solinas_fp128_differential.rs @@ -1,19 +1,15 @@ -//! Differential tests for the two-limb Solinas field (`Fp128`) against -//! jolt-field across every 128-bit prime offset, with a 4×64-limb schoolbook -//! multiply + binary long division as the independent oracle (`u128` cannot -//! hold the 256-bit intermediates, and num-bigint is not a dev-dependency). +//! Differential tests for the two-limb Solinas field (`Fp128`) across every +//! 128-bit prime offset, with a 4x64-limb schoolbook multiply + binary long +//! division as the independent oracle (`u128` cannot hold the 256-bit +//! intermediates). #![cfg(feature = "solinas")] -#![expect(clippy::unwrap_used, reason = "test code")] +// NB: no `expect(clippy::unwrap_used)` — every unwrap here sits inside a +// local `macro_rules!` expansion, where the lint does not fire. -use jolt_field as base; use jolt_field_two as two; -use base::{ - CanonicalBytes as BaseCanonicalBytes, CanonicalField, CanonicalRepr, FieldCore, FromPrimitiveInt, HalvingField, - PseudoMersenneField, RingCore, -}; -use rand::{Rng, SeedableRng}; +use rand::{Rng, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; use two::{Accumulator as _, CanonicalBytes, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, Ring}; @@ -21,7 +17,7 @@ fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0xf128_a5a5) } -/// 128×128 → 256-bit schoolbook multiply over 64-bit halves; independent of +/// 128x128 -> 256-bit schoolbook multiply over 64-bit halves; independent of /// the crate's `mul_wide` (different limb/carry structure, no shared code). fn oracle_mul_256(a: u128, b: u128) -> [u64; 4] { let (a0, a1) = (a as u64 as u128, a >> 64); @@ -69,58 +65,43 @@ fn oracle_sub(a: u128, b: u128, p: u128) -> u128 { oracle_add(a, p - b, p) } -/// Full differential + oracle sweep for one (rebuilt, baseline, modulus) -/// triple. `inverses: false` skips inverse checks for moduli of unverified -/// primality (used by the `C = 2^a ± 1` shift-path coverage). +/// Full oracle sweep for one (field type, modulus) pair. `inverses: false` +/// skips inverse checks for moduli of unverified primality (used by the +/// `C = 2^a ± 1` shift-path coverage). macro_rules! check_prime128 { - ($two:ty, $base:ty, $p:expr, $rng:expr) => { - check_prime128!($two, $base, $p, $rng, inverses: true) + ($two:ty, $p:expr, $rng:expr) => { + check_prime128!($two, $p, $rng, inverses: true) }; - ($two:ty, $base:ty, $p:expr, $rng:expr, inverses: $inverses:expr) => {{ + ($two:ty, $p:expr, $rng:expr, inverses: $inverses:expr) => {{ let p: u128 = $p; let c: u128 = 0u128.wrapping_sub(p); - let sample = |rng: &mut ChaCha20Rng| -> ($two, $base, u128) { + let sample = |rng: &mut ChaCha20Rng| -> ($two, u128) { let raw: u128 = rng.gen(); let v = oracle_mod(&[raw as u64, (raw >> 64) as u64], p); let t = <$two as CanonicalEncoding>::from_u128_reduced(raw); assert_eq!(t.to_u128_checked(), Some(v), "reduction vs oracle"); - let b = <$base as CanonicalField>::from_canonical_u128_reduced(raw); - assert_eq!(b.to_canonical_u128(), v, "baseline reduction vs oracle"); - let b = <$base as CanonicalField>::from_canonical_u128_checked(v).unwrap(); - (t, b, v) + (t, v) }; - // Metadata parity. - assert_eq!( - <$two as CanonicalEncoding>::MODULUS_BITS, - <$base as CanonicalField>::modulus_bits() - ); + // Metadata. + assert_eq!(<$two as CanonicalEncoding>::MODULUS_BITS, 128); assert_eq!(<$two as PseudoMersenne>::OFFSET, c); - assert_eq!( - <$two as PseudoMersenne>::OFFSET, - <$base as PseudoMersenneField>::MODULUS_OFFSET - ); - assert_eq!( - <$two as CanonicalBytes>::NUM_BYTES, - <$base as BaseCanonicalBytes>::NUM_BYTES - ); assert_eq!(<$two as CanonicalBytes>::NUM_BYTES, 16); let cfg = bincode::config::standard(); for _ in 0..200 { - let (ta, ba, va) = sample($rng); - let (tb, bb, vb) = sample($rng); + let (ta, va) = sample($rng); + let (tb, vb) = sample($rng); - // Arithmetic vs baseline and vs the limb oracle. - let cases: [($two, $base, u128); 4] = [ - (ta + tb, ba + bb, oracle_add(va, vb, p)), - (ta - tb, ba - bb, oracle_sub(va, vb, p)), - (ta * tb, ba * bb, oracle_mul(va, vb, p)), - (-ta, -ba, oracle_sub(0, va, p)), + // Arithmetic vs the limb oracle. + let cases: [($two, u128); 4] = [ + (ta + tb, oracle_add(va, vb, p)), + (ta - tb, oracle_sub(va, vb, p)), + (ta * tb, oracle_mul(va, vb, p)), + (-ta, oracle_sub(0, va, p)), ]; - for (t, b, v) in cases { + for (t, v) in cases { assert_eq!(t.to_u128_checked(), Some(v)); - assert_eq!(b.to_canonical_u128(), v); } // By-ref and assigning operator forms agree with the owned ones. @@ -137,42 +118,41 @@ macro_rules! check_prime128 { Ring::square(&ta).to_u128_checked(), Some(oracle_mul(va, va, p)) ); - assert_eq!( - Ring::square(&ta).to_u128_checked().unwrap(), - RingCore::square(&ba).to_canonical_u128() - ); - assert_eq!( - ta.half().to_u128_checked().unwrap(), - ba.half().to_canonical_u128() - ); + let half = if va % 2 == 0 { + va / 2 + } else { + // (va + p) / 2 without overflowing u128. + (va >> 1) + (p >> 1) + 1 + }; + assert_eq!(ta.half().to_u128_checked(), Some(half)); assert_eq!((ta.half() + ta.half()).to_u128_checked(), Some(va)); if $inverses { - match (ta.inverse(), ba.inverse()) { - (Some(ti), Some(bi)) => { - assert_eq!(ti.to_u128_checked().unwrap(), bi.to_canonical_u128()); - assert_eq!((ti * ta).to_u128_checked(), Some(1)); - } - (ti, bi) => assert_eq!(ti.is_none(), bi.is_none()), + // Inverse is unique given the oracle-verified multiply, so + // `ti * ta == 1` pins the value; None only at zero. + match ta.inverse() { + Some(ti) => assert_eq!((ti * ta).to_u128_checked(), Some(1)), + None => assert_eq!(va, 0, "inverse must exist for nonzero"), } } - // Wide multiplies: limb parity with the baseline, then round-trip - // through solinas_reduce against the independent oracle. - assert_eq!(ta.to_limbs(), ba.to_limbs()); - assert_eq!(ta.mul_wide(tb), ba.mul_wide(bb)); + // Wide multiplies: limb equality with the independent oracle, + // then round-trip through solinas_reduce. + assert_eq!(ta.to_limbs(), [va as u64, (va >> 64) as u64]); assert_eq!(ta.mul_wide(tb), oracle_mul_256(va, vb)); assert_eq!( <$two>::solinas_reduce(&ta.mul_wide(tb)).to_u128_checked(), Some(oracle_mul(va, vb, p)) ); let x64: u64 = $rng.gen(); - assert_eq!(ta.mul_wide_u64(x64), ba.mul_wide_u64(x64)); + let wide64 = oracle_mul_256(va, x64 as u128); + assert_eq!(wide64[3], 0); + assert_eq!(ta.mul_wide_u64(x64), [wide64[0], wide64[1], wide64[2]]); assert_eq!( <$two>::solinas_reduce(&ta.mul_wide_u64(x64)).to_u128_checked(), Some(oracle_mul(va, x64 as u128, p)) ); let x128: u128 = $rng.gen::() % p; - assert_eq!(ta.mul_wide_u128(x128), ba.mul_wide_u128(x128)); + assert_eq!(ta.mul_wide_u128(x128), oracle_mul_256(va, x128)); assert_eq!( <$two>::solinas_reduce(&ta.mul_wide_u128(x128)).to_u128_checked(), Some(oracle_mul(va, x128, p)) @@ -181,26 +161,35 @@ macro_rules! check_prime128 { // Integer conversions. let xi: i64 = $rng.gen(); assert_eq!( - <$two as Ring>::from_u64(x64).to_u128_checked().unwrap(), - <$base as FromPrimitiveInt>::from_u64(x64).to_canonical_u128() + <$two as Ring>::from_u64(x64).to_u128_checked(), + Some(x64 as u128 % p) ); + let xi_expected = if xi >= 0 { + xi as u128 % p + } else { + oracle_sub(0, xi.unsigned_abs() as u128 % p, p) + }; assert_eq!( - <$two as Ring>::from_i64(xi).to_u128_checked().unwrap(), - <$base as FromPrimitiveInt>::from_i64(xi).to_canonical_u128() + <$two as Ring>::from_i64(xi).to_u128_checked(), + Some(xi_expected) ); assert_eq!( - ta.mul_u64(x64).to_u128_checked().unwrap(), - ba.mul_u64(x64).to_canonical_u128() + ta.mul_u64(x64).to_u128_checked(), + Some(oracle_mul(va, x64 as u128, p)) ); // Transcript surface: bytes, reducing decodes, challenges. - assert_eq!(ta.to_bytes_le_vec(), ba.to_bytes_le_vec()); assert_eq!(ta.to_bytes_le_vec(), va.to_le_bytes().to_vec()); assert_eq!( CanonicalEncoding::num_bits(&ta), - CanonicalRepr::num_bits(&ba) + 128 - va.leading_zeros(), + "num_bits vs oracle" + ); + assert_eq!( + ta.to_u64_checked(), + (va >> 64 == 0).then_some(va as u64), + "to_u64_checked vs oracle" ); - assert_eq!(ta.to_u64_checked(), ba.to_canonical_u64_checked()); let challenge: [u8; 32] = $rng.gen(); for len in [8usize, 16, 32] { let ours = <$two as CanonicalEncoding>::from_bytes_le_reduced(&challenge[..len]); @@ -210,36 +199,31 @@ macro_rules! check_prime128 { "challenge derivation defaults to the reducing decode" ); assert_eq!( - <$two as CanonicalEncoding>::from_scalar_challenge_bytes(&challenge[..len]) - .to_u128_checked(), - Some( - <$base as CanonicalRepr>::from_scalar_challenge_bytes(&challenge[..len]) - .to_canonical_u128() - ), - "scalar challenge derivation diverges from baseline" + <$two as CanonicalEncoding>::from_scalar_challenge_bytes(&challenge[..len]), + ours, + "scalar challenge derivation defaults to the reducing decode" ); let mut padded = [0u8; 32]; padded[..len].copy_from_slice(&challenge[..len]); - let limbs: [u64; 4] = - std::array::from_fn(|i| u64::from_le_bytes(padded[8 * i..8 * i + 8].try_into().unwrap())); + let limbs: [u64; 4] = std::array::from_fn(|i| { + u64::from_le_bytes(padded[8 * i..8 * i + 8].try_into().unwrap()) + }); assert_eq!( ours.to_u128_checked(), Some(oracle_mod(&limbs, p)), "decode vs oracle" ); - assert_eq!( - ours.to_u128_checked().unwrap(), - <$base as CanonicalRepr>::from_le_bytes_mod_order(&challenge[..len]) - .to_canonical_u128() - ); } - // Wire bytes: equality, cross-decode, canonical rejection. + // Wire bytes: canonical LE encoding, decode round-trip. let t_bytes = bincode::serde::encode_to_vec(ta, cfg).unwrap(); - let b_bytes = bincode::serde::encode_to_vec(ba, cfg).unwrap(); - assert_eq!(t_bytes, b_bytes, "wire bytes diverge"); + assert_eq!( + t_bytes, + va.to_le_bytes().to_vec(), + "wire bytes are the canonical LE encoding" + ); let (t_back, _): ($two, usize) = - bincode::serde::decode_from_slice(&b_bytes, cfg).unwrap(); + bincode::serde::decode_from_slice(&t_bytes, cfg).unwrap(); assert_eq!(t_back.to_u128_checked(), Some(va)); } @@ -294,11 +278,6 @@ macro_rules! check_prime128 { for limbs in &limb_cases { let got = <$two>::solinas_reduce(limbs).to_u128_checked().unwrap(); assert_eq!(got, oracle_mod(limbs, p), "solinas_reduce vs oracle: {limbs:?}"); - assert_eq!( - got, - <$base>::solinas_reduce(limbs).to_canonical_u128(), - "solinas_reduce vs baseline: {limbs:?}" - ); } // Zero/One and iterator Sum/Product (owned and by-ref). @@ -343,51 +322,30 @@ macro_rules! check_prime128 { #[test] fn fp128_offset275_matches() { let mut rng = rng(); - check_prime128!( - two::Prime128Offset275, - base::Prime128Offset275, - u128::MAX - 274, - &mut rng - ); + check_prime128!(two::Prime128Offset275, u128::MAX - 274, &mut rng); } #[test] fn fp128_offset159_matches() { let mut rng = rng(); - check_prime128!( - two::Prime128Offset159, - base::Prime128Offset159, - u128::MAX - 158, - &mut rng - ); + check_prime128!(two::Prime128Offset159, u128::MAX - 158, &mut rng); } #[test] fn fp128_offset2355_matches() { let mut rng = rng(); - check_prime128!( - two::Prime128Offset2355, - base::Prime128Offset2355, - u128::MAX - 2354, - &mut rng - ); + check_prime128!(two::Prime128Offset2355, u128::MAX - 2354, &mut rng); } #[test] fn fp128_offset_a7f7_matches() { let mut rng = rng(); - check_prime128!( - two::Prime128OffsetA7F7, - base::Prime128OffsetA7F7, - u128::MAX - 0xFFFF_A7F6, - &mut rng - ); + check_prime128!(two::Prime128OffsetA7F7, u128::MAX - 0xFFFF_A7F6, &mut rng); assert_eq!( two::pseudo_mersenne_modulus(128, 0xFFFF_A7F7), Some(u128::MAX - 0xFFFF_A7F6) ); - // Registered coverage stops at PRIME_OFFSET_MAX; A7F7 is above it, like - // the baseline registry. + // Registered coverage stops at PRIME_OFFSET_MAX; A7F7 is above it. assert!(!two::is_registered_prime_offset(128, 0xFFFF_A7F7)); assert!(two::is_registered_prime_offset(128, 275)); } @@ -402,7 +360,6 @@ fn fp128_shift_kind_c_paths_match() { // C = 5 = 2^2 + 1 (shift-kind +1). check_prime128!( two::Fp128<{ u128::MAX - 4 }>, - base::Fp128<{ u128::MAX - 4 }>, u128::MAX - 4, &mut rng, inverses: false @@ -410,22 +367,29 @@ fn fp128_shift_kind_c_paths_match() { // C = 7 = 2^3 − 1 (shift-kind −1). check_prime128!( two::Fp128<{ u128::MAX - 6 }>, - base::Fp128<{ u128::MAX - 6 }>, u128::MAX - 6, &mut rng, inverses: false ); } -/// Identical rejection sampling: same seed, same element stream. +/// The rejection-sampling stream is pinned against a test-local +/// reimplementation of its spec: draw (lo, hi) words, accept if < p. #[test] -fn fp128_random_matches_baseline() { +fn fp128_random_matches_spec() { fn check() { let (mut r1, mut r2) = (rng(), rng()); for _ in 0..100 { let t: two::Fp128

= two::Field::random(&mut r1); - let b: base::Fp128

= FieldCore::random(&mut r2); - assert_eq!(t.to_u128_checked().unwrap(), b.to_canonical_u128()); + let expected = loop { + let lo = r2.next_u64(); + let hi = r2.next_u64(); + let v = lo as u128 | (hi as u128) << 64; + if v < P { + break v; + } + }; + assert_eq!(t.to_u128_checked(), Some(expected)); } } check::<{ u128::MAX - 274 }>(); diff --git a/crates/jolt-field-two/tests/solinas_packed_differential.rs b/crates/jolt-field-two/tests/solinas_packed_differential.rs index c7180496f6..214c80a178 100644 --- a/crates/jolt-field-two/tests/solinas_packed_differential.rs +++ b/crates/jolt-field-two/tests/solinas_packed_differential.rs @@ -5,9 +5,9 @@ //! boundary lane patterns (all-max lanes, mixed canonical extremes, //! single-lane-nonzero); lane-access and slice-helper laws; //! `WithPacking` associated-type sanity for every field type; `NoPacking` -//! equivalence; and, on aarch64/NEON, lane-exact differentials against -//! jolt-field's packed types (including the packed ext2 kernel hook and -//! the fused degree-4/8 kernels). +//! equivalence; and, on aarch64/NEON, the expected lane widths. Scalar +//! arithmetic is verified against independent oracles in the other suites, +//! so packed-vs-scalar equivalence transitively pins the packed kernels. #![cfg(feature = "solinas")] #![expect(clippy::unwrap_used, reason = "test code")] @@ -17,8 +17,7 @@ use jolt_field_two as two; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; use two::{ - pseudo_mersenne_modulus, CanonicalEncoding, ExtField, Field, NoPacking, Packed, Ring, - WithPacking, + pseudo_mersenne_modulus, CanonicalEncoding, ExtField, Field, NoPacking, Packed, WithPacking, }; /// Packed ops must equal per-lane scalar ops (add/sub/mul/square/inverse). @@ -267,293 +266,12 @@ fn no_packing_equivalence() { assert_eq!(PF::WIDTH, 1); } -/// Lane-exact differentials against jolt-field's packed types on the -/// native NEON backend: same canonical inputs, same lane results. +/// Expected NEON lane widths on aarch64 (previously asserted against +/// jolt-field's packed types; the widths are part of the layout contract). #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -mod baseline_diff { - use super::*; - use jolt_field as base; - - use base::packed::{HasPacking, PackedField}; - use base::{CanonicalField, FromPrimitiveInt}; - use rand::Rng; - - /// Random + boundary lane vectors of canonical representatives. - fn canonical_inputs(p: u128, w: usize, seed: u64) -> Vec { - let mut rng = ChaCha20Rng::seed_from_u64(seed); - let mut out = vec![p - 1; w]; - out.extend((0..w).map(|i| [0, 1, p - 2, p - 1][i % 4])); - for lane in 0..w { - out.extend((0..w).map(|i| if i == lane { p - 1 } else { 0 })); - } - out.extend((0..w * 16).map(|_| rng.gen::() % p)); - out - } - - fn diff_prime(p: u128, seed: u64) - where - NP: Packed, - NP::Scalar: CanonicalEncoding, - BP: PackedField, - BP::Scalar: CanonicalField + FromPrimitiveInt, - { - assert_eq!(NP::WIDTH, BP::WIDTH, "lane width mismatch vs baseline"); - let w = NP::WIDTH; - let lhs = canonical_inputs(p, w, seed); - let rhs = canonical_inputs(p, w, seed ^ 0xFFFF); - for (la, ra) in lhs.chunks_exact(w).zip(rhs.chunks_exact(w)) { - let na = NP::from_fn(|i| ::from_u128(la[i])); - let nb = NP::from_fn(|i| ::from_u128(ra[i])); - let ba = BP::from_fn(|i| BP::Scalar::from_u128(la[i])); - let bb = BP::from_fn(|i| BP::Scalar::from_u128(ra[i])); - let pairs = [(na + nb, ba + bb), (na - nb, ba - bb), (na * nb, ba * bb)]; - for (op, (n, b)) in ["add", "sub", "mul"].iter().zip(pairs) { - for i in 0..w { - assert_eq!( - n.extract(i).to_u128_checked().unwrap(), - b.extract(i).to_canonical_u128(), - "{op} lane {i} differs from baseline" - ); - } - } - } - } - - #[test] - fn fp32_lanes_match_baseline() { - assert_eq!(::Packing::WIDTH, 4); - diff_prime::< - ::Packing, - ::Packing, - >(pm(24, 3), 0xB3201); - diff_prime::< - ::Packing, - ::Packing, - >(pm(30, 35), 0xB3202); - diff_prime::< - ::Packing, - ::Packing, - >(pm(31, 19), 0xB3203); - diff_prime::< - ::Packing, - ::Packing, - >(pm(32, 99), 0xB3204); - } - - #[test] - fn fp64_lanes_match_baseline() { - assert_eq!(::Packing::WIDTH, 2); - diff_prime::< - ::Packing, - ::Packing, - >(pm(40, 195), 0xB6401); - diff_prime::< - ::Packing, - ::Packing, - >(pm(48, 59), 0xB6402); - diff_prime::< - ::Packing, - ::Packing, - >(pm(56, 27), 0xB6403); - diff_prime::< - ::Packing, - ::Packing, - >(pm(64, 59), 0xB6404); - } - - #[test] - fn fp128_lanes_match_baseline() { - assert_eq!(::Packing::WIDTH, 2); - diff_prime::< - ::Packing, - ::Packing, - >(pm(128, 275), 0x00B1_2801); - diff_prime::< - ::Packing, - ::Packing, - >(pm(128, 0xFFFF_A7F7), 0x00B1_2802); - } - - /// Coefficient matrices for extension lanes. - fn coeff_lanes(p: u128, w: usize, seed: u64) -> Vec<[u128; D]> { - let mut rng = ChaCha20Rng::seed_from_u64(seed); - let mut out = vec![[p - 1; D]; w]; - out.extend( - (0..w).map(|lane| std::array::from_fn(|j| [0, 1, p - 2, p - 1][(lane + j) % 4])), - ); - out.extend((0..w * 8).map(|_| std::array::from_fn(|_| rng.gen::() % p))); - out - } - - fn diff_ext( - p: u128, - seed: u64, - mk_new: impl Fn([u128; D]) -> NP::Scalar, - mk_base: impl Fn([u128; D]) -> BP::Scalar, - canon_new: impl Fn(&NP::Scalar) -> Vec, - canon_base: impl Fn(&BP::Scalar) -> Vec, - ) where - NP: Packed, - BP: PackedField, - { - assert_eq!(NP::WIDTH, BP::WIDTH, "ext lane width mismatch vs baseline"); - let w = NP::WIDTH; - let lhs = coeff_lanes::(p, w, seed); - let rhs = coeff_lanes::(p, w, seed ^ 0xFFFF); - for (la, ra) in lhs.chunks_exact(w).zip(rhs.chunks_exact(w)) { - let na = NP::from_fn(|i| mk_new(la[i])); - let nb = NP::from_fn(|i| mk_new(ra[i])); - let ba = BP::from_fn(|i| mk_base(la[i])); - let bb = BP::from_fn(|i| mk_base(ra[i])); - let pairs = [ - (na + nb, ba + bb), - (na - nb, ba - bb), - (na * nb, ba * bb), - (na.square(), ba.square()), - ]; - for (op, (n, b)) in ["add", "sub", "mul", "square"].iter().zip(pairs) { - for i in 0..w { - assert_eq!( - canon_new(&n.extract(i)), - canon_base(&b.extract(i)), - "ext {op} lane {i} differs from baseline" - ); - } - } - } - } - - /// The packed ext2 kernel hook, differentially vs the baseline hook. - #[test] - fn ext2_kernel_matches_baseline() { - type NF = two::Prime32Offset99; - type BF = base::Prime32Offset99; - type NP = as WithPacking>::Packing; - type BP = base::packed::PackedFpExt2::Packing>; - diff_ext::<2, NP, BP>( - pm(32, 99), - 0xBE201, - |c| two::FpExt2::new(Ring::from_u128(c[0]), Ring::from_u128(c[1])), - |c| base::FpExt2::new(BF::from_u128(c[0]), BF::from_u128(c[1])), - |x| { - x.coeffs - .iter() - .map(|c| c.to_u128_checked().unwrap()) - .collect() - }, - |x| x.coeffs.iter().map(|c| c.to_canonical_u128()).collect(), - ); - type NF251 = two::Fp32<251>; - type BF251 = base::Fp32<251>; - type NPn = as WithPacking>::Packing; - type BPn = - base::packed::PackedFpExt2::Packing>; - diff_ext::<2, NPn, BPn>( - 251, - 0xBE202, - |c| two::FpExt2::new(Ring::from_u128(c[0]), Ring::from_u128(c[1])), - |c| base::FpExt2::new(BF251::from_u128(c[0]), BF251::from_u128(c[1])), - |x| { - x.coeffs - .iter() - .map(|c| c.to_u128_checked().unwrap()) - .collect() - }, - |x| x.coeffs.iter().map(|c| c.to_canonical_u128()).collect(), - ); - } - - /// Fused degree-4 kernels (dot products on fp32) vs the baseline NEON - /// kernels, plus the schedule-default paths on wider bases. - #[test] - fn ext4_kernels_match_baseline() { - macro_rules! diff_ext4 { - ($nf:ty, $bf:ty, $p:expr, $seed:expr) => { - diff_ext::< - 4, - as WithPacking>::Packing, - base::packed::PackedFpExt4<$bf, <$bf as HasPacking>::Packing>, - >( - $p, - $seed, - |c| two::FpExt4::new(c.map(Ring::from_u128)), - |c| base::FpExt4::new(c.map(<$bf>::from_u128)), - |x| { - x.coeffs - .iter() - .map(|c| c.to_u128_checked().unwrap()) - .collect() - }, - |x| x.coeffs.iter().map(|c| c.to_canonical_u128()).collect(), - ); - }; - } - diff_ext4!( - two::Prime32Offset99, - base::Prime32Offset99, - pm(32, 99), - 0xBE401 - ); - diff_ext4!( - two::Prime31Offset19, - base::Prime31Offset19, - pm(31, 19), - 0xBE402 - ); - diff_ext4!( - two::Prime64Offset59, - base::Prime64Offset59, - pm(64, 59), - 0xBE403 - ); - diff_ext4!( - two::Prime128Offset275, - base::Prime128Offset275, - pm(128, 275), - 0xBE404 - ); - } - - #[test] - fn ext8_kernels_match_baseline() { - macro_rules! diff_ext8 { - ($nf:ty, $bf:ty, $p:expr, $seed:expr) => { - diff_ext::< - 8, - as WithPacking>::Packing, - base::packed::PackedFpExt8<$bf, <$bf as HasPacking>::Packing>, - >( - $p, - $seed, - |c| two::FpExt8::new(c.map(Ring::from_u128)), - |c| base::FpExt8::new(c.map(<$bf>::from_u128)), - |x| { - x.coeffs - .iter() - .map(|c| c.to_u128_checked().unwrap()) - .collect() - }, - |x| x.coeffs.iter().map(|c| c.to_canonical_u128()).collect(), - ); - }; - } - diff_ext8!( - two::Prime32Offset99, - base::Prime32Offset99, - pm(32, 99), - 0xBE801 - ); - diff_ext8!( - two::Prime64Offset59, - base::Prime64Offset59, - pm(64, 59), - 0xBE802 - ); - diff_ext8!( - two::Prime128Offset275, - base::Prime128Offset275, - pm(128, 275), - 0xBE803 - ); - } +#[test] +fn neon_lane_widths() { + assert_eq!(::Packing::WIDTH, 4); + assert_eq!(::Packing::WIDTH, 2); + assert_eq!(::Packing::WIDTH, 2); } diff --git a/crates/jolt-field-two/tests/solinas_unreduced_differential.rs b/crates/jolt-field-two/tests/solinas_unreduced_differential.rs index 13a2d8ab9a..0c4dca6597 100644 --- a/crates/jolt-field-two/tests/solinas_unreduced_differential.rs +++ b/crates/jolt-field-two/tests/solinas_unreduced_differential.rs @@ -1,14 +1,13 @@ //! Differential tests for the deferred-reduction machinery (`Unreduced`, -//! `Fold`, `MulBaseUnreduced`) against jolt-field, with an independent -//! schoolbook oracle (256-bit limb multiply + binary long division — no -//! Solinas folding, no shared code). +//! `Fold`, `MulBaseUnreduced`) against an independent schoolbook oracle +//! (256-bit limb multiply + binary long division — no Solinas folding, no +//! shared code). //! //! Coverage per accumulator type: exactness of delayed sums vs direct //! reduced multiplication over random batches AND adversarial batches -//! (all-max operands, wrap-through add/sub sequences), plus strict parity -//! with the baseline's `HasUnreducedOps`/`HasWide`/`ReduceTo`/ -//! `HasOptimizedFold`/`MulBaseUnreduced` machinery under identical inputs -//! and challenge constants. +//! (all-max operands, wrap-through add/sub sequences). The extension +//! accumulators are compared per-term against the ring multiply, which the +//! ext suite verifies against its own schoolbook oracle. //! //! Headroom boundaries: the `i32`-lane bound (32768 max-lane accumulations) //! is tested exactly, with the one-past case asserted to panic in debug @@ -21,11 +20,8 @@ // NB: no `expect(clippy::unwrap_used)` — every unwrap here sits inside a // local `macro_rules!` expansion, where the lint does not fire. -use jolt_field as base; use jolt_field_two as two; -use base::unreduced::{HasOptimizedFold, HasUnreducedOps, HasWide, ReduceTo}; -use base::{CanonicalField, MulBaseUnreduced as BaseMulBaseUnreduced}; use num_traits::Zero; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -34,7 +30,7 @@ use two::{CanonicalEncoding, ExtField, Fold, MulBaseUnreduced, Ring, Unreduced}; const M61: u64 = (1 << 61) - 1; /// 128×128 → 256-bit schoolbook multiply over 64-bit halves (independent of -/// both crates' `mul_wide`). +/// the crate's `mul_wide`). fn oracle_mul_256(a: u128, b: u128) -> [u64; 4] { let (a0, a1) = (a as u64 as u128, a >> 64); let (b0, b1) = (b as u64 as u128, b >> 64); @@ -77,28 +73,24 @@ fn submod(a: u128, b: u128, p: u128) -> u128 { addmod(a, p - b, p) } -/// Full `Unreduced` + `HasWide` sweep for one paired base-field -/// instantiation: product/small-product batch exactness (random, -/// all-max, wrap-through), wide-lane roundtrip/group-ops/scaling — all -/// against the field ops, the schoolbook oracle, and the baseline. +/// Full `Unreduced` sweep for one base-field instantiation: +/// product/small-product batch exactness (random, all-max, wrap-through), +/// wide-lane roundtrip/group-ops/scaling — all against the field ops and +/// the schoolbook oracle. macro_rules! base_field_suite { - ($name:ident, $F2:ty, $FB:ty, $p:expr, $seed:expr) => { + ($name:ident, $F2:ty, $p:expr, $seed:expr) => { #[test] fn $name() { let p: u128 = $p; let mut rng = ChaCha20Rng::seed_from_u64($seed); let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); - let fb = |v: u128| <$FB as CanonicalField>::from_canonical_u128_checked(v).unwrap(); let val2 = |x: &$F2| x.to_u128_checked().unwrap(); - let valb = |x: &$FB| x.to_canonical_u128(); - assert_eq!( - <$F2 as Unreduced>::SUM_IS_EXACT, - <$FB as HasUnreducedOps>::DELAYED_PRODUCT_SUM_IS_EXACT, - "SUM_IS_EXACT parity" - ); + // Scalar prime fields do not advertise exact delayed sums + // (value pinned while the jolt-field baseline coexisted). + assert!(!<$F2 as Unreduced>::SUM_IS_EXACT); - // Σ aᵢ·bᵢ: delayed vs per-term vs oracle vs baseline. + // Σ aᵢ·bᵢ: delayed vs per-term vs oracle. let check_products = |pairs: &[(u128, u128)]| { let expect = pairs .iter() @@ -112,15 +104,6 @@ macro_rules! base_field_suite { .iter() .fold(<$F2 as Zero>::zero(), |acc, &(a, b)| acc + f2(a) * f2(b)); assert_eq!(val2(&per_term), expect, "per-term vs oracle"); - let accb = pairs.iter().fold( - <<$FB as HasUnreducedOps>::ProductAccum as Zero>::zero(), - |acc, &(a, b)| acc + fb(a).mul_to_product_accum(fb(b)), - ); - assert_eq!( - valb(&<$FB as HasUnreducedOps>::reduce_product_accum(accb)), - expect, - "baseline parity" - ); }; for &n in &[1usize, 2, 7, 501] { let pairs: Vec<(u128, u128)> = (0..n) @@ -143,15 +126,6 @@ macro_rules! base_field_suite { val2(&<$F2 as Unreduced>::reduce_small_product(acc2)), expect ); - let accb = pairs.iter().fold( - <<$FB as HasUnreducedOps>::MulU64Accum as Zero>::zero(), - |acc, &(a, b)| acc + fb(a).mul_u64_unreduced(b), - ); - assert_eq!( - valb(&<$FB as HasUnreducedOps>::reduce_mul_u64_accum(accb)), - expect, - "baseline small-product parity" - ); }; for &n in &[1usize, 3, 400] { let pairs: Vec<(u128, u64)> = (0..n) @@ -181,14 +155,12 @@ macro_rules! base_field_suite { "separate pos/neg accumulators" ); - // Wide lanes: roundtrip, group ops, scaling; vs baseline. + // Wide lanes: roundtrip, group ops, scaling. let mut vals: Vec = vec![0, 1, 2, p / 2, p - 2, p - 1]; vals.extend((0..200).map(|_| rng.gen::() % p)); for &x in &vals { let w2 = <$F2 as Unreduced>::Wide::from(f2(x)); assert_eq!(val2(&<$F2 as Unreduced>::reduce_wide(w2)), x, "roundtrip"); - let wb = <$FB as HasWide>::Wide::from(fb(x)); - assert_eq!(valb(&ReduceTo::<$FB>::reduce(wb)), x, "baseline roundtrip"); let y = rng.gen::() % p; let wy = <$F2 as Unreduced>::Wide::from(f2(y)); @@ -205,8 +177,6 @@ macro_rules! base_field_suite { for s in [-32768i32, -12345, -1, 0, 1, 2, 12345, 32768] { let got = <$F2 as Unreduced>::reduce_wide(f2(x).scale_wide(s)); assert_eq!(got, f2(x) * <$F2 as Ring>::from_i64(s as i64), "scale_wide"); - let gotb = ReduceTo::<$FB>::reduce(fb(x).mul_small_to_wide(s)); - assert_eq!(val2(&got), valb(&gotb), "baseline scale parity"); } } @@ -246,42 +216,36 @@ macro_rules! base_field_suite { base_field_suite!( fp32_prime24_unreduced, two::Prime24Offset3, - base::Prime24Offset3, (1 << 24) - 3, 0x0724_0001 ); base_field_suite!( fp32_prime32_unreduced, two::Prime32Offset99, - base::Prime32Offset99, (1 << 32) - 99, 0x0732_0002 ); base_field_suite!( fp64_prime40_unreduced, two::Prime40Offset195, - base::Prime40Offset195, (1 << 40) - 195, 0x0740_0003 ); base_field_suite!( fp64_prime64_unreduced, two::Prime64Offset59, - base::Prime64Offset59, u64::MAX as u128 - 58, 0x0764_0004 ); base_field_suite!( fp128_prime275_unreduced, two::Prime128Offset275, - base::Prime128Offset275, u128::MAX - 274, 0x0728_0005 ); base_field_suite!( fp128_prime_a7f7_unreduced, two::Prime128OffsetA7F7, - base::Prime128OffsetA7F7, u128::MAX - 0xFFFF_A7F6, 0x0728_0006 ); @@ -316,28 +280,22 @@ fn wide_lane_one_past_headroom_panics_in_debug() { } /// `FpExt4` fused accumulator: delayed batch sums vs per-term ring -/// multiplication and the baseline, plus the coordinate-scaling -/// `MulBaseUnreduced` override. +/// multiplication (oracle-verified in the ext suite), plus the +/// coordinate-scaling `MulBaseUnreduced` override. macro_rules! ext4_fp32_suite { - ($name:ident, $F2:ty, $FB:ty, $p:expr, $seed:expr) => { + ($name:ident, $F2:ty, $p:expr, $seed:expr) => { #[test] fn $name() { type E2 = two::FpExt4<$F2>; - type EB = base::FpExt4<$FB>; let p: u128 = $p; let mut rng = ChaCha20Rng::seed_from_u64($seed); let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); - let fb = |v: u128| <$FB as CanonicalField>::from_canonical_u128_checked(v).unwrap(); let mk2 = |v: [u128; 4]| E2::new(v.map(f2)); - let mkb = |v: [u128; 4]| EB::new(v.map(fb)); - let vec2 = |e: &E2| e.coeffs.map(|c| c.to_u128_checked().unwrap()); - let vecb = |e: &EB| e.coeffs.map(|c| c.to_canonical_u128()); let sample = |rng: &mut ChaCha20Rng| -> [u128; 4] { std::array::from_fn(|_| rng.gen::() % p) }; assert!(::SUM_IS_EXACT); - assert!(::DELAYED_PRODUCT_SUM_IS_EXACT); let check = |pairs: &[([u128; 4], [u128; 4])]| { let acc2 = pairs.iter().fold( @@ -352,15 +310,6 @@ macro_rules! ext4_fp32_suite { per_term, "delayed sum vs per-term" ); - let accb = pairs.iter().fold( - <::ProductAccum as Zero>::zero(), - |acc, &(a, b)| acc + mkb(a).mul_to_product_accum(mkb(b)), - ); - assert_eq!( - vec2(&per_term), - vecb(&::reduce_product_accum(accb)), - "baseline parity" - ); }; for &n in &[1usize, 2, 33, 512] { let pairs: Vec<_> = (0..n) @@ -382,14 +331,13 @@ macro_rules! ext4_fp32_suite { "wrap-through sub/add" ); - // MulBaseUnreduced override: vs mul_base, vs the default - // lift-then-mul body, vs the baseline; batched. + // MulBaseUnreduced override: vs mul_base and vs the default + // lift-then-mul body; batched. let mut acc2 = <::Product as Zero>::zero(); - let mut accb = <::ProductAccum as Zero>::zero(); let mut per_term = ::zero(); for _ in 0..300 { let (xv, sv) = (sample(&mut rng), rng.gen::() % p); - let (x2, xb) = (mk2(xv), mkb(xv)); + let x2 = mk2(xv); let over = x2.mul_base_unreduced(f2(sv)); assert_eq!( ::reduce_product(over), @@ -404,15 +352,9 @@ macro_rules! ext4_fp32_suite { "override vs default lift-then-mul" ); acc2 += over; - accb += xb.mul_base_to_product_accum(fb(sv)); per_term += x2.mul_base(f2(sv)); } assert_eq!(::reduce_product(acc2), per_term); - assert_eq!( - vec2(&per_term), - vecb(&::reduce_product_accum(accb)), - "baseline mul-base batch parity" - ); } }; } @@ -420,41 +362,33 @@ macro_rules! ext4_fp32_suite { ext4_fp32_suite!( ext4_fp32_prime24_accum, two::Prime24Offset3, - base::Prime24Offset3, (1 << 24) - 3, 0x0E44_0001 ); ext4_fp32_suite!( ext4_fp32_prime32_accum, two::Prime32Offset99, - base::Prime32Offset99, (1 << 32) - 99, 0x0E44_0002 ); /// `FpExt2` carry-tracked accumulator: batch exactness vs per-term -/// multiplication and the baseline (both non-residue configs), plus the -/// `AccumPair` small-product path. +/// multiplication (both non-residue configs), plus the `AccumPair` +/// small-product path. macro_rules! ext2_fp64_suite { - ($name:ident, $F2:ty, $FB:ty, $C2:ty, $CB:ty, $p:expr, $seed:expr) => { + ($name:ident, $F2:ty, $C2:ty, $p:expr, $seed:expr) => { #[test] fn $name() { type E2 = two::FpExt2<$F2, $C2>; - type EB = base::FpExt2<$FB, $CB>; let p: u128 = $p; let mut rng = ChaCha20Rng::seed_from_u64($seed); let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); - let fb = |v: u128| <$FB as CanonicalField>::from_canonical_u128_checked(v).unwrap(); let mk2 = |v: [u128; 2]| E2::new(f2(v[0]), f2(v[1])); - let mkb = |v: [u128; 2]| EB::new(fb(v[0]), fb(v[1])); - let vec2 = |e: &E2| e.coeffs.map(|c| c.to_u128_checked().unwrap()); - let vecb = |e: &EB| e.coeffs.map(|c| c.to_canonical_u128()); let sample = |rng: &mut ChaCha20Rng| -> [u128; 2] { std::array::from_fn(|_| rng.gen::() % p) }; assert!(::SUM_IS_EXACT); - assert!(::DELAYED_PRODUCT_SUM_IS_EXACT); let check = |pairs: &[([u128; 2], [u128; 2])]| { let acc2 = pairs.iter().fold( @@ -469,15 +403,6 @@ macro_rules! ext2_fp64_suite { per_term, "delayed sum vs per-term" ); - let accb = pairs.iter().fold( - <::ProductAccum as Zero>::zero(), - |acc, &(a, b)| acc + mkb(a).mul_to_product_accum(mkb(b)), - ); - assert_eq!( - vec2(&per_term), - vecb(&::reduce_product_accum(accb)), - "baseline parity" - ); }; for &n in &[1usize, 2, 33, 512] { let pairs: Vec<_> = (0..n) @@ -520,15 +445,6 @@ macro_rules! ext2_fp64_suite { per_term, "small-product delayed sum" ); - let accb = pairs.iter().fold( - <::MulU64Accum as Zero>::zero(), - |acc, &(x, s)| acc + mkb(x).mul_u64_unreduced(s), - ); - assert_eq!( - vec2(&per_term), - vecb(&::reduce_mul_u64_accum(accb)), - "baseline small-product parity" - ); // MulBaseUnreduced default body routes through the fused accum. let (xv, sv) = (sample(&mut rng), rng.gen::() % p); @@ -544,18 +460,14 @@ macro_rules! ext2_fp64_suite { ext2_fp64_suite!( ext2_fp64_prime40_two_nr_accum, two::Prime40Offset195, - base::Prime40Offset195, two::TwoNr, - base::TwoNr, (1 << 40) - 195, 0x0E22_0001 ); ext2_fp64_suite!( ext2_fp64_prime64_two_nr_accum, two::Prime64Offset59, - base::Prime64Offset59, two::TwoNr, - base::TwoNr, u64::MAX as u128 - 58, 0x0E22_0002 ); @@ -564,56 +476,35 @@ ext2_fp64_suite!( ext2_fp64_suite!( ext2_fp64_m61_neg_one_nr_accum, two::Fp64, - base::Fp64, two::NegOneNr, - base::NegOneNr, M61 as u128, 0x0E22_0003 ); -/// Fold parity for one paired instantiation: `fold_one(precompute(r), e, o)` -/// must equal the field identity `e + r·(o − e)` AND the baseline's -/// optimized fold under the same challenge constants. +/// Fold semantics for one instantiation: `fold_one(precompute(r), e, o)` +/// must equal the field identity `e + r·(o − e)` (whose operands the ext +/// and prime-field suites verify against their oracles). macro_rules! fold_parity { - ($name:ident, $E2:ty, $EB:ty, $F2:ty, $FB:ty, $d:expr, $p:expr, $seed:expr) => { + ($name:ident, $E2:ty, $F2:ty, $d:expr, $p:expr, $seed:expr) => { #[test] fn $name() { let p: u128 = $p; let d: usize = $d; let mut rng = ChaCha20Rng::seed_from_u64($seed); let f2 = |v: u128| <$F2 as CanonicalEncoding>::from_u128_checked(v).unwrap(); - let fb = |v: u128| <$FB as CanonicalField>::from_canonical_u128_checked(v).unwrap(); let mk2 = |vals: &[u128]| { <$E2 as ExtField<$F2>>::from_base_slice( &vals.iter().map(|&v| f2(v)).collect::>(), ) }; - let mkb = |vals: &[u128]| { - <$EB as base::ExtField<$FB>>::from_base_slice( - &vals.iter().map(|&v| fb(v)).collect::>(), - ) - }; - let vec2 = |e: &$E2| { - <$E2 as ExtField<$F2>>::to_base_vec(e) - .iter() - .map(|c| c.to_u128_checked().unwrap()) - .collect::>() - }; - let vecb = |e: &$EB| { - <$EB as base::ExtField<$FB>>::to_base_vec(e) - .iter() - .map(|c| c.to_canonical_u128()) - .collect::>() - }; let sample = |rng: &mut ChaCha20Rng| -> Vec { (0..d).map(|_| rng.gen::() % p).collect() }; for _ in 0..8 { let rv = sample(&mut rng); - let (r2, rb) = (mk2(&rv), mkb(&rv)); + let r2 = mk2(&rv); let ctx2 = <$E2 as Fold>::precompute(r2); - let ctxb = <$EB as HasOptimizedFold>::precompute_fold(rb); let mut cases: Vec<(Vec, Vec)> = vec![ (vec![0; d], vec![p - 1; d]), @@ -626,8 +517,6 @@ macro_rules! fold_parity { let (e2, o2) = (mk2(&ev), mk2(&ov)); let got = <$E2 as Fold>::fold_one(&ctx2, e2, o2); assert_eq!(got, e2 + r2 * (o2 - e2), "fold vs field identity"); - let gotb = <$EB as HasOptimizedFold>::fold_one(&ctxb, mkb(&ev), mkb(&ov)); - assert_eq!(vec2(&got), vecb(&gotb), "fold vs baseline"); } } } @@ -637,9 +526,7 @@ macro_rules! fold_parity { fold_parity!( fold_fp32_prime24, two::Prime24Offset3, - base::Prime24Offset3, two::Prime24Offset3, - base::Prime24Offset3, 1, (1 << 24) - 3, 0x0F01 @@ -647,9 +534,7 @@ fold_parity!( fold_parity!( fold_fp32_prime32, two::Prime32Offset99, - base::Prime32Offset99, two::Prime32Offset99, - base::Prime32Offset99, 1, (1 << 32) - 99, 0x0F02 @@ -657,9 +542,7 @@ fold_parity!( fold_parity!( fold_fp64_prime40, two::Prime40Offset195, - base::Prime40Offset195, two::Prime40Offset195, - base::Prime40Offset195, 1, (1 << 40) - 195, 0x0F03 @@ -667,9 +550,7 @@ fold_parity!( fold_parity!( fold_fp64_prime64, two::Prime64Offset59, - base::Prime64Offset59, two::Prime64Offset59, - base::Prime64Offset59, 1, u64::MAX as u128 - 58, 0x0F04 @@ -677,9 +558,7 @@ fold_parity!( fold_parity!( fold_fp128_prime275, two::Prime128Offset275, - base::Prime128Offset275, two::Prime128Offset275, - base::Prime128Offset275, 1, u128::MAX - 274, 0x0F05 @@ -687,9 +566,7 @@ fold_parity!( fold_parity!( fold_fp128_prime_a7f7, two::Prime128OffsetA7F7, - base::Prime128OffsetA7F7, two::Prime128OffsetA7F7, - base::Prime128OffsetA7F7, 1, u128::MAX - 0xFFFF_A7F6, 0x0F06 @@ -698,9 +575,7 @@ fold_parity!( fold_parity!( fold_ext2_fp64_prime40, two::Ext2, - base::Ext2, two::Prime40Offset195, - base::Prime40Offset195, 2, (1 << 40) - 195, 0x0F07 @@ -708,9 +583,7 @@ fold_parity!( fold_parity!( fold_ext2_fp64_prime64, two::Ext2, - base::Ext2, two::Prime64Offset59, - base::Prime64Offset59, 2, u64::MAX as u128 - 58, 0x0F08 @@ -718,9 +591,7 @@ fold_parity!( fold_parity!( fold_ext2_fp64_m61_neg_one, two::FpExt2, two::NegOneNr>, - base::FpExt2, base::NegOneNr>, two::Fp64, - base::Fp64, 2, M61 as u128, 0x0F09 @@ -729,9 +600,7 @@ fold_parity!( fold_parity!( fold_ext2_fp32_prime32, two::Ext2, - base::Ext2, two::Prime32Offset99, - base::Prime32Offset99, 2, (1 << 32) - 99, 0x0F0A @@ -739,9 +608,7 @@ fold_parity!( fold_parity!( fold_ext2_fp128_prime275, two::Ext2, - base::Ext2, two::Prime128Offset275, - base::Prime128Offset275, 2, u128::MAX - 274, 0x0F0B @@ -750,9 +617,7 @@ fold_parity!( fold_parity!( fold_ext4_fp32_prime24, two::FpExt4, - base::FpExt4, two::Prime24Offset3, - base::Prime24Offset3, 4, (1 << 24) - 3, 0x0F0C @@ -760,9 +625,7 @@ fold_parity!( fold_parity!( fold_ext4_fp32_prime32, two::FpExt4, - base::FpExt4, two::Prime32Offset99, - base::Prime32Offset99, 4, (1 << 32) - 99, 0x0F0D @@ -771,9 +634,7 @@ fold_parity!( fold_parity!( fold_ext4_fp64_prime40, two::FpExt4, - base::FpExt4, two::Prime40Offset195, - base::Prime40Offset195, 4, (1 << 40) - 195, 0x0F0E @@ -781,9 +642,7 @@ fold_parity!( fold_parity!( fold_ext4_fp128_prime275, two::FpExt4, - base::FpExt4, two::Prime128Offset275, - base::Prime128Offset275, 4, u128::MAX - 274, 0x0F0F @@ -792,9 +651,7 @@ fold_parity!( fold_parity!( fold_ext8_fp32_prime24, two::FpExt8, - base::FpExt8, two::Prime24Offset3, - base::Prime24Offset3, 8, (1 << 24) - 3, 0x0F10 @@ -802,9 +659,7 @@ fold_parity!( fold_parity!( fold_ext8_fp64_prime40, two::FpExt8, - base::FpExt8, two::Prime40Offset195, - base::Prime40Offset195, 8, (1 << 40) - 195, 0x0F11 @@ -812,9 +667,7 @@ fold_parity!( fold_parity!( fold_ext8_fp128_prime275, two::FpExt8, - base::FpExt8, two::Prime128Offset275, - base::Prime128Offset275, 8, u128::MAX - 274, 0x0F12 diff --git a/crates/jolt-field-two/tests/solinas_words_differential.rs b/crates/jolt-field-two/tests/solinas_words_differential.rs index 238a3bf05e..57624089e1 100644 --- a/crates/jolt-field-two/tests/solinas_words_differential.rs +++ b/crates/jolt-field-two/tests/solinas_words_differential.rs @@ -1,17 +1,14 @@ -//! Differential tests for the Solinas word fields (`Fp32`/`Fp64`) against -//! jolt-field across every registered ≤64-bit prime offset, with u128 -//! modular arithmetic as the independent oracle. +//! Differential tests for the Solinas word fields (`Fp32`/`Fp64`) across +//! every registered ≤64-bit prime offset, with u128 modular arithmetic (and +//! num-bigint for oversized byte decodes) as the independent oracle. #![cfg(feature = "solinas")] -#![expect(clippy::unwrap_used, reason = "test code")] +// NB: no `expect(clippy::unwrap_used)` — every unwrap here sits inside a +// local `macro_rules!` expansion, where the lint does not fire. -use jolt_field as base; use jolt_field_two as two; -use base::{ - CanonicalBytes as BaseCanonicalBytes, CanonicalField, CanonicalRepr, FromPrimitiveInt, HalvingField, - PseudoMersenneField, RingCore, -}; +use num_bigint::BigUint; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; use two::{Accumulator as _, CanonicalBytes, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, Ring}; @@ -20,68 +17,57 @@ fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0x5011_a5a5) } -/// Full differential + oracle sweep for one (rebuilt, baseline, modulus) triple. +/// Little-endian bytes mod `p`, via exact bigint arithmetic. +fn bytes_mod(bytes: &[u8], p: u128) -> u128 { + let mut v = (BigUint::from_bytes_le(bytes) % p).to_u64_digits(); + v.resize(2, 0); + v[0] as u128 | (v[1] as u128) << 64 +} + +/// Full oracle sweep for one (field type, modulus) pair. All expected +/// values come from u128/bigint modular arithmetic; wire and transcript +/// bytes are checked structurally against the canonical LE encoding +/// (absolute bytes are pinned by the golden fixtures in golden_bytes.rs). macro_rules! check_prime { - ($two:ty, $base:ty, $p:expr, $rng:expr) => {{ + ($two:ty, $p:expr, $bytes:expr, $rng:expr) => {{ let p: u128 = $p; - // Baseline Fp64::reduce_u128 originally truncated the fold's high - // part for sub-word primes; fixed on the PR #1684 branch, so parity - // is asserted on the full u128 domain. - let sample = |rng: &mut ChaCha20Rng| -> ($two, $base, u128) { + let sample = |rng: &mut ChaCha20Rng| -> ($two, u128) { let raw: u128 = rng.gen(); let v = raw % p; let t = <$two as CanonicalEncoding>::from_u128_reduced(raw); assert_eq!(t.to_u128_checked(), Some(v), "reduction vs oracle"); - let b = <$base as CanonicalField>::from_canonical_u128_reduced(raw); - assert_eq!(b.to_canonical_u128(), v, "baseline reduction vs oracle"); - let b = <$base as CanonicalField>::from_canonical_u128_checked(v).unwrap(); - (t, b, v) + (t, v) }; - // Metadata parity. - assert_eq!( - <$two as CanonicalEncoding>::MODULUS_BITS, - <$base as CanonicalField>::modulus_bits() - ); - assert_eq!( - <$two as PseudoMersenne>::OFFSET, - <$base as PseudoMersenneField>::MODULUS_OFFSET - ); - assert_eq!( - <$two as CanonicalBytes>::NUM_BYTES, - <$base as BaseCanonicalBytes>::NUM_BYTES - ); + // Metadata: modulus bit width, pseudo-Mersenne offset, byte width. + let bits = 128 - p.leading_zeros(); + assert_eq!(<$two as CanonicalEncoding>::MODULUS_BITS, bits); + assert_eq!(<$two as PseudoMersenne>::OFFSET, (1u128 << bits) - p); + assert_eq!(<$two as CanonicalBytes>::NUM_BYTES, $bytes); let cfg = bincode::config::standard(); for _ in 0..200 { - let (ta, ba, va) = sample($rng); - let (tb, bb, vb) = sample($rng); + let (ta, va) = sample($rng); + let (tb, vb) = sample($rng); - // Arithmetic vs baseline and vs the u128 oracle. - let cases: [($two, $base, u128); 4] = [ - (ta + tb, ba + bb, (va + vb) % p), - (ta - tb, ba - bb, (va + p - vb) % p), - (ta * tb, ba * bb, (va * vb) % p), - (-ta, -ba, (p - va) % p), + // Arithmetic vs the u128 oracle. + let cases: [($two, u128); 4] = [ + (ta + tb, (va + vb) % p), + (ta - tb, (va + p - vb) % p), + (ta * tb, (va * vb) % p), + (-ta, (p - va) % p), ]; - for (t, b, v) in cases { + for (t, v) in cases { assert_eq!(t.to_u128_checked(), Some(v)); - assert_eq!(b.to_canonical_u128(), v); } - assert_eq!( - Ring::square(&ta).to_u128_checked().unwrap(), - RingCore::square(&ba).to_canonical_u128() - ); - assert_eq!( - ta.half().to_u128_checked().unwrap(), - ba.half().to_canonical_u128() - ); - match (ta.inverse(), ba.inverse()) { - (Some(ti), Some(bi)) => { - assert_eq!(ti.to_u128_checked().unwrap(), bi.to_canonical_u128()); - assert_eq!((ti * ta).to_u128_checked(), Some(1)); - } - (ti, bi) => assert_eq!(ti.is_none(), bi.is_none()), + assert_eq!(Ring::square(&ta).to_u128_checked(), Some((va * va) % p)); + let half = if va % 2 == 0 { va / 2 } else { (va + p) / 2 }; + assert_eq!(ta.half().to_u128_checked(), Some(half)); + // Inverse is unique given the oracle-verified multiply, so + // `ti * ta == 1` pins the value; None only at zero (p prime). + match ta.inverse() { + Some(ti) => assert_eq!((ti * ta).to_u128_checked(), Some(1)), + None => assert_eq!(va, 0, "inverse must exist for nonzero"), } // Widening multiply + explicit reduction round-trip. @@ -94,61 +80,60 @@ macro_rules! check_prime { let x64: u64 = $rng.gen(); let xi: i64 = $rng.gen(); assert_eq!( - <$two as Ring>::from_u64(x64).to_u128_checked().unwrap(), - <$base as FromPrimitiveInt>::from_u64(x64).to_canonical_u128() + <$two as Ring>::from_u64(x64).to_u128_checked(), + Some(x64 as u128 % p) ); + let xi_expected = if xi >= 0 { + xi as u128 % p + } else { + (p - (xi.unsigned_abs() as u128 % p)) % p + }; assert_eq!( - <$two as Ring>::from_i64(xi).to_u128_checked().unwrap(), - <$base as FromPrimitiveInt>::from_i64(xi).to_canonical_u128() + <$two as Ring>::from_i64(xi).to_u128_checked(), + Some(xi_expected) ); assert_eq!( - ta.mul_u64(x64).to_u128_checked().unwrap(), - ba.mul_u64(x64).to_canonical_u128() + ta.mul_u64(x64).to_u128_checked(), + Some(va * (x64 as u128 % p) % p) ); // Transcript surface: bytes, reducing decodes, challenges. - assert_eq!(ta.to_bytes_le_vec(), ba.to_bytes_le_vec()); + assert_eq!(ta.to_bytes_le_vec(), va.to_le_bytes()[..$bytes].to_vec()); assert_eq!( CanonicalEncoding::num_bits(&ta), - CanonicalRepr::num_bits(&ba) + 128 - va.leading_zeros(), + "num_bits vs oracle" ); - assert_eq!(ta.to_u64_checked(), ba.to_canonical_u64_checked()); + assert_eq!(ta.to_u64_checked(), Some(va as u64)); let challenge: [u8; 32] = $rng.gen(); for len in [8usize, 16, 32] { let ours = <$two as CanonicalEncoding>::from_bytes_le_reduced(&challenge[..len]); + assert_eq!( + ours.to_u128_checked(), + Some(bytes_mod(&challenge[..len], p)), + "decode vs oracle" + ); assert_eq!( <$two as CanonicalEncoding>::from_challenge_bytes(&challenge[..len]), ours, "challenge derivation defaults to the reducing decode" ); assert_eq!( - <$two as CanonicalEncoding>::from_scalar_challenge_bytes(&challenge[..len]) - .to_u128_checked(), - Some( - <$base as CanonicalRepr>::from_scalar_challenge_bytes(&challenge[..len]) - .to_canonical_u128() - ), - "scalar challenge derivation diverges from baseline" - ); - if len <= 16 { - let mut padded = [0u8; 16]; - padded[..len].copy_from_slice(&challenge[..len]); - let raw = u128::from_le_bytes(padded); - assert_eq!(ours.to_u128_checked(), Some(raw % p), "decode vs oracle"); - } - assert_eq!( - ours.to_u128_checked().unwrap(), - <$base as CanonicalRepr>::from_le_bytes_mod_order(&challenge[..len]) - .to_canonical_u128() + <$two as CanonicalEncoding>::from_scalar_challenge_bytes(&challenge[..len]), + ours, + "scalar challenge derivation defaults to the reducing decode" ); } - // Wire bytes: equality, cross-decode, canonical rejection. + // Wire bytes: canonical LE encoding, decode round-trip. let t_bytes = bincode::serde::encode_to_vec(ta, cfg).unwrap(); - let b_bytes = bincode::serde::encode_to_vec(ba, cfg).unwrap(); - assert_eq!(t_bytes, b_bytes, "wire bytes diverge"); + assert_eq!( + t_bytes, + va.to_le_bytes()[..$bytes].to_vec(), + "wire bytes are the canonical LE encoding" + ); let (t_back, _): ($two, usize) = - bincode::serde::decode_from_slice(&b_bytes, cfg).unwrap(); + bincode::serde::decode_from_slice(&t_bytes, cfg).unwrap(); assert_eq!(t_back.to_u128_checked(), Some(va)); } @@ -175,6 +160,26 @@ macro_rules! check_prime { "i128::MIN vs oracle" ); + // Identical rejection-free sampling stream: `random` reduces one + // word (Fp32) or two words (Fp64) drawn from the RNG. + { + let seed: u64 = $rng.gen(); + let mut r1 = ChaCha20Rng::seed_from_u64(seed); + let mut r2 = ChaCha20Rng::seed_from_u64(seed); + use rand::RngCore; + for _ in 0..50 { + let t: $two = two::Field::random(&mut r1); + let expected = if $bytes <= 4 { + r2.next_u64() as u128 % p + } else { + let lo = r2.next_u64() as u128; + let hi = r2.next_u64() as u128; + (lo | (hi << 64)) % p + }; + assert_eq!(t.to_u128_checked(), Some(expected), "random stream"); + } + } + // Non-canonical wire encodings rejected (encode p itself). let n = <$two as CanonicalBytes>::NUM_BYTES; let p_bytes = &p.to_le_bytes()[..n]; @@ -193,87 +198,51 @@ macro_rules! check_prime { #[test] fn fp32_offsets_match() { let mut rng = rng(); - check_prime!( - two::Prime24Offset3, - base::Prime24Offset3, - (1 << 24) - 3, - &mut rng - ); - check_prime!( - two::Prime30Offset35, - base::Prime30Offset35, - (1 << 30) - 35, - &mut rng - ); - check_prime!( - two::Prime31Offset19, - base::Prime31Offset19, - (1 << 31) - 19, - &mut rng - ); - check_prime!( - two::Prime32Offset99, - base::Prime32Offset99, - (1 << 32) - 99, - &mut rng - ); + check_prime!(two::Prime24Offset3, (1 << 24) - 3, 4, &mut rng); + check_prime!(two::Prime30Offset35, (1 << 30) - 35, 4, &mut rng); + check_prime!(two::Prime31Offset19, (1 << 31) - 19, 4, &mut rng); + check_prime!(two::Prime32Offset99, (1 << 32) - 99, 4, &mut rng); // Ad hoc small prime and Mersenne31 (unregistered but instantiable). - check_prime!(two::Fp32<251>, base::Fp32<251>, 251, &mut rng); - check_prime!( - two::Fp32<{ (1 << 31) - 1 }>, - base::Fp32<{ (1 << 31) - 1 }>, - (1 << 31) - 1, - &mut rng - ); + check_prime!(two::Fp32<251>, 251, 4, &mut rng); + check_prime!(two::Fp32<{ (1 << 31) - 1 }>, (1 << 31) - 1, 4, &mut rng); } #[test] fn fp64_offsets_match() { let mut rng = rng(); - check_prime!( - two::Prime40Offset195, - base::Prime40Offset195, - (1 << 40) - 195, - &mut rng - ); - check_prime!( - two::Prime48Offset59, - base::Prime48Offset59, - (1 << 48) - 59, - &mut rng - ); - check_prime!( - two::Prime56Offset27, - base::Prime56Offset27, - (1 << 56) - 27, - &mut rng - ); - check_prime!( - two::Prime64Offset59, - base::Prime64Offset59, - (1 << 64) - 59, - &mut rng - ); + check_prime!(two::Prime40Offset195, (1 << 40) - 195, 8, &mut rng); + check_prime!(two::Prime48Offset59, (1 << 48) - 59, 8, &mut rng); + check_prime!(two::Prime56Offset27, (1 << 56) - 27, 8, &mut rng); + check_prime!(two::Prime64Offset59, (1 << 64) - 59, 8, &mut rng); // Mersenne61: sub-word u64 prime exercising C = 1. - check_prime!( - two::Fp64<{ (1 << 61) - 1 }>, - base::Fp64<{ (1 << 61) - 1 }>, - (1 << 61) - 1, - &mut rng - ); + check_prime!(two::Fp64<{ (1 << 61) - 1 }>, (1 << 61) - 1, 8, &mut rng); } +/// The registered prime-offset table, pinned as data. Generated from +/// jolt-field at commit 5b3e39ece1c27586a1f7cc77f24e718cb5d73e10 (the +/// registries were asserted identical while both crates coexisted). +const REGISTRY: &[(u32, u16)] = &[ + (24, 3), + (30, 35), + (31, 19), + (32, 99), + (40, 195), + (48, 59), + (56, 27), + (64, 59), + (128, 275), +]; + #[test] fn registry_matches() { - assert_eq!( - two::PRIME_OFFSET_SPECS.len(), - base::PRIME_OFFSET_SPECS.len() - ); - for (t, b) in two::PRIME_OFFSET_SPECS - .iter() - .zip(base::PRIME_OFFSET_SPECS.iter()) - { - assert_eq!((t.bits, t.offset, t.modulus), (b.bits, b.offset, b.modulus)); + assert_eq!(two::PRIME_OFFSET_SPECS.len(), REGISTRY.len()); + for (t, &(bits, offset)) in two::PRIME_OFFSET_SPECS.iter().zip(REGISTRY.iter()) { + assert_eq!((t.bits, t.offset), (bits, offset)); + assert_eq!( + BigUint::from(t.modulus), + (BigUint::from(1u32) << bits) - offset, + "modulus is 2^bits - offset" + ); assert!(two::is_registered_prime_offset(t.bits, t.offset as u128)); assert_eq!( two::pseudo_mersenne_modulus(t.bits, t.offset as u128), @@ -284,11 +253,8 @@ fn registry_matches() { Some(t.modulus) ); } - assert_eq!(two::PRIME_OFFSET_MAX, base::PRIME_OFFSET_MAX); - assert_eq!( - two::PRIME_OFFSET_IMPLEMENTED_MAX_BITS, - base::PRIME_OFFSET_IMPLEMENTED_MAX_BITS - ); + assert_eq!(two::PRIME_OFFSET_MAX, 65536); + assert_eq!(two::PRIME_OFFSET_IMPLEMENTED_MAX_BITS, 128); assert!(!two::is_registered_prime_offset(61, 1)); assert_eq!(two::pseudo_mersenne_modulus(0, 3), None); assert_eq!( @@ -299,11 +265,20 @@ fn registry_matches() { #[test] fn balanced_digit_lut_matches() { + const P: u128 = (1 << 64) - 59; for log_basis in 1..=6 { - let t: [two::Prime64Offset59; 64] = two::balanced_digit_lut(log_basis); - let b: [base::Prime64Offset59; 64] = base::balanced_digit_lut(log_basis); - for (x, y) in t.iter().zip(b.iter()) { - assert_eq!(x.to_u128_checked().unwrap(), y.to_canonical_u128()); + let lut: [two::Prime64Offset59; 64] = two::balanced_digit_lut(log_basis); + let basis = 1usize << log_basis; + let half = (basis / 2) as u128; + for (i, x) in lut.iter().enumerate() { + let expected = if i >= basis { + 0 + } else if i as u128 >= half { + i as u128 - half + } else { + P - (half - i as u128) + }; + assert_eq!(x.to_u128_checked(), Some(expected), "lut[{i}]"); } } } From cf8a66ae381fb9c23eff9afe825b55b72c67a2ab Mon Sep 17 00:00:00 2001 From: acentelles Date: Fri, 31 Jul 2026 17:20:11 -0400 Subject: [PATCH 29/38] refactor(jolt-field): replace the crate with the jolt-field-two rebuild Delete the baseline crates/jolt-field (11,410 counted LOC) and move the rebuild into its place under the package name jolt-field (5,103 counted LOC). MontgomeryConstants dies with the old crate: zero consumers in the workspace, closing the spec OPEN item. The old crate's differential evidence survives via the oracle-free suites and golden byte fixtures generated from it before deletion. Consumers rebind in the follow-up commits; the workspace does not build between this commit and the rebind. 125/125 tests and clippy clean on -p jolt-field, all features/targets. --- Cargo.lock | 63 -- Cargo.toml | 1 - crates/jolt-field-two/Cargo.toml | 46 - crates/jolt-field-two/src/akita.rs | 115 -- crates/jolt-field-two/src/algebra.rs | 435 -------- crates/jolt-field-two/src/lib.rs | 121 --- crates/jolt-field-two/src/limbs.rs | 291 ----- crates/jolt-field/Cargo.toml | 27 +- .../benches/ext4_kernels.rs | 4 +- crates/jolt-field/benches/field_arith.rs | 61 -- .../jolt-field/benches/solinas_field_arith.rs | 34 - .../benches/solinas_field_arith/arithmetic.rs | 689 ------------ .../benches/solinas_field_arith/base.rs | 64 -- .../benches/solinas_field_arith/cases.rs | 25 - .../benches/solinas_field_arith/comparison.rs | 63 -- .../benches/solinas_field_arith/data.rs | 17 - .../benches/solinas_field_arith/ext2.rs | 47 - .../benches/solinas_field_arith/ext4.rs | 53 - .../benches/solinas_field_arith/kernel.rs | 147 --- .../benches/solinas_field_arith/mod.rs | 21 - .../benches/solinas_field_arith/parallel.rs | 233 ---- .../benches/solinas_field_arith/params.rs | 55 - .../benches/solinas_field_arith/plonky3.rs | 957 ----------------- .../benches/solinas_field_arith/wide.rs | 121 --- crates/jolt-field/fuzz/.gitignore | 4 - crates/jolt-field/fuzz/Cargo.lock | 571 ---------- crates/jolt-field/fuzz/Cargo.toml | 40 - .../fuzz/fuzz_targets/field_arith.rs | 34 - .../fuzz/fuzz_targets/from_bytes.rs | 14 - .../fuzz/fuzz_targets/solinas_field_arith.rs | 40 - .../fuzz_targets/wide_accumulator_fmadd.rs | 32 - .../fuzz_targets/wide_accumulator_merge.rs | 39 - crates/jolt-field/fuzz/rust-toolchain.toml | 2 - crates/jolt-field/src/accumulator.rs | 118 --- crates/jolt-field/src/akita.rs | 87 +- crates/jolt-field/src/algebra.rs | 377 ++++++- crates/jolt-field/src/arkworks/bn254.rs | 516 --------- crates/jolt-field/src/arkworks/bn254_fq.rs | 450 -------- crates/jolt-field/src/arkworks/bn254_ops.rs | 643 ----------- crates/jolt-field/src/arkworks/mod.rs | 27 - .../src/arkworks/montgomery_impl.rs | 129 --- .../src/arkworks/wide_accumulator.rs | 138 --- .../src/bn254/mod.rs | 0 .../src/bn254/mont.rs | 0 crates/jolt-field/src/canonical.rs | 123 --- crates/jolt-field/src/ext/fp_ext2.rs | 549 ---------- crates/jolt-field/src/ext/fp_ext4.rs | 699 ------------ crates/jolt-field/src/ext/fp_ext8.rs | 513 --------- crates/jolt-field/src/ext/lift.rs | 416 -------- crates/jolt-field/src/ext/mod.rs | 31 - crates/jolt-field/src/ext/tests.rs | 568 ---------- .../src/extension.rs | 0 crates/jolt-field/src/field.rs | 11 - crates/jolt-field/src/field_error.rs | 16 - crates/jolt-field/src/lib.rs | 175 +-- crates/jolt-field/src/limbs.rs | 178 +--- crates/jolt-field/src/montgomery_constants.rs | 38 - crates/jolt-field/src/native_algebra.rs | 212 ---- .../{jolt-field-two => jolt-field}/src/ops.rs | 0 .../src/packed.rs | 0 crates/jolt-field/src/packed/avx2/fp128.rs | 228 ---- crates/jolt-field/src/packed/avx2/fp32.rs | 686 ------------ crates/jolt-field/src/packed/avx2/fp64.rs | 264 ----- crates/jolt-field/src/packed/avx2/mod.rs | 65 -- crates/jolt-field/src/packed/avx512/fp128.rs | 203 ---- crates/jolt-field/src/packed/avx512/fp32.rs | 679 ------------ crates/jolt-field/src/packed/avx512/fp64.rs | 243 ----- crates/jolt-field/src/packed/avx512/mod.rs | 64 -- crates/jolt-field/src/packed/ext/mod.rs | 455 -------- crates/jolt-field/src/packed/ext/tests.rs | 484 --------- crates/jolt-field/src/packed/mod.rs | 330 ------ crates/jolt-field/src/packed/neon/fp128.rs | 311 ------ crates/jolt-field/src/packed/neon/fp32.rs | 821 -------------- crates/jolt-field/src/packed/neon/fp64.rs | 221 ---- crates/jolt-field/src/packed/neon/mod.rs | 43 - crates/jolt-field/src/packed/tests.rs | 344 ------ crates/jolt-field/src/parallel.rs | 104 -- crates/jolt-field/src/prime/fp128/add_sub.rs | 409 ------- crates/jolt-field/src/prime/fp128/core.rs | 107 -- crates/jolt-field/src/prime/fp128/mod.rs | 61 -- crates/jolt-field/src/prime/fp128/mul.rs | 376 ------- crates/jolt-field/src/prime/fp128/primes.rs | 30 - crates/jolt-field/src/prime/fp128/reduce.rs | 195 ---- crates/jolt-field/src/prime/fp128/tests.rs | 194 ---- crates/jolt-field/src/prime/fp128/traits.rs | 136 --- crates/jolt-field/src/prime/fp128/wide.rs | 302 ------ crates/jolt-field/src/prime/fp32.rs | 581 ---------- crates/jolt-field/src/prime/fp64.rs | 643 ----------- crates/jolt-field/src/prime/mod.rs | 34 - .../jolt-field/src/prime/native_capability.rs | 168 --- .../jolt-field/src/prime/pseudo_mersenne.rs | 174 --- crates/jolt-field/src/prime/traits.rs | 52 - crates/jolt-field/src/prime/util.rs | 46 - .../src/schedules.rs | 0 .../src/signed.rs | 0 crates/jolt-field/src/signed/mod.rs | 73 -- crates/jolt-field/src/signed/signed_bigint.rs | 684 ------------ .../src/signed/signed_bigint_hi32.rs | 680 ------------ .../src/solinas/ext.rs | 0 .../src/solinas/fp128.rs | 0 .../src/solinas/mod.rs | 0 .../src/solinas/packed/engine.rs | 0 .../src/solinas/packed/ext.rs | 0 .../src/solinas/packed/fp128.rs | 0 .../src/solinas/packed/mod.rs | 0 .../src/solinas/packed/simd.rs | 0 .../src/solinas/parallel.rs | 0 .../src/solinas/unreduced.rs | 0 .../src/solinas/word.rs | 0 .../src/unreduced.rs | 0 crates/jolt-field/src/unreduced/accum.rs | 556 ---------- crates/jolt-field/src/unreduced/mod.rs | 807 -------------- crates/jolt-field/src/unreduced/tests.rs | 262 ----- .../tests/binary_field_core_compat.rs | 202 ---- .../tests/bn254_differential.rs | 4 +- crates/jolt-field/tests/coverage.rs | 998 ------------------ crates/jolt-field/tests/field_operations.rs | 261 ----- .../tests/golden_bytes.rs | 4 +- .../tests/limbs_signed_differential.rs | 2 +- .../tests/parallel_macros.rs | 6 +- crates/jolt-field/tests/serde_roundtrip.rs | 114 -- .../tests/solinas_ext_differential.rs | 2 +- .../tests/solinas_fp128_differential.rs | 2 +- .../tests/solinas_packed_differential.rs | 2 +- .../tests/solinas_unreduced_differential.rs | 2 +- .../tests/solinas_words_differential.rs | 2 +- .../tests/spine.rs | 2 +- specs/jolt-field-rebuild.md | 7 +- 128 files changed, 519 insertions(+), 23686 deletions(-) delete mode 100644 crates/jolt-field-two/Cargo.toml delete mode 100644 crates/jolt-field-two/src/akita.rs delete mode 100644 crates/jolt-field-two/src/algebra.rs delete mode 100644 crates/jolt-field-two/src/lib.rs delete mode 100644 crates/jolt-field-two/src/limbs.rs rename crates/{jolt-field-two => jolt-field}/benches/ext4_kernels.rs (98%) delete mode 100644 crates/jolt-field/benches/field_arith.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/arithmetic.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/base.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/cases.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/comparison.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/data.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/ext2.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/ext4.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/kernel.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/mod.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/parallel.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/params.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/plonky3.rs delete mode 100644 crates/jolt-field/benches/solinas_field_arith/wide.rs delete mode 100644 crates/jolt-field/fuzz/.gitignore delete mode 100644 crates/jolt-field/fuzz/Cargo.lock delete mode 100644 crates/jolt-field/fuzz/Cargo.toml delete mode 100644 crates/jolt-field/fuzz/fuzz_targets/field_arith.rs delete mode 100644 crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs delete mode 100644 crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs delete mode 100644 crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs delete mode 100644 crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs delete mode 100644 crates/jolt-field/fuzz/rust-toolchain.toml delete mode 100644 crates/jolt-field/src/accumulator.rs delete mode 100644 crates/jolt-field/src/arkworks/bn254.rs delete mode 100644 crates/jolt-field/src/arkworks/bn254_fq.rs delete mode 100644 crates/jolt-field/src/arkworks/bn254_ops.rs delete mode 100644 crates/jolt-field/src/arkworks/mod.rs delete mode 100644 crates/jolt-field/src/arkworks/montgomery_impl.rs delete mode 100644 crates/jolt-field/src/arkworks/wide_accumulator.rs rename crates/{jolt-field-two => jolt-field}/src/bn254/mod.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/bn254/mont.rs (100%) delete mode 100644 crates/jolt-field/src/canonical.rs delete mode 100644 crates/jolt-field/src/ext/fp_ext2.rs delete mode 100644 crates/jolt-field/src/ext/fp_ext4.rs delete mode 100644 crates/jolt-field/src/ext/fp_ext8.rs delete mode 100644 crates/jolt-field/src/ext/lift.rs delete mode 100644 crates/jolt-field/src/ext/mod.rs delete mode 100644 crates/jolt-field/src/ext/tests.rs rename crates/{jolt-field-two => jolt-field}/src/extension.rs (100%) delete mode 100644 crates/jolt-field/src/field.rs delete mode 100644 crates/jolt-field/src/field_error.rs delete mode 100644 crates/jolt-field/src/montgomery_constants.rs delete mode 100644 crates/jolt-field/src/native_algebra.rs rename crates/{jolt-field-two => jolt-field}/src/ops.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/packed.rs (100%) delete mode 100644 crates/jolt-field/src/packed/avx2/fp128.rs delete mode 100644 crates/jolt-field/src/packed/avx2/fp32.rs delete mode 100644 crates/jolt-field/src/packed/avx2/fp64.rs delete mode 100644 crates/jolt-field/src/packed/avx2/mod.rs delete mode 100644 crates/jolt-field/src/packed/avx512/fp128.rs delete mode 100644 crates/jolt-field/src/packed/avx512/fp32.rs delete mode 100644 crates/jolt-field/src/packed/avx512/fp64.rs delete mode 100644 crates/jolt-field/src/packed/avx512/mod.rs delete mode 100644 crates/jolt-field/src/packed/ext/mod.rs delete mode 100644 crates/jolt-field/src/packed/ext/tests.rs delete mode 100644 crates/jolt-field/src/packed/mod.rs delete mode 100644 crates/jolt-field/src/packed/neon/fp128.rs delete mode 100644 crates/jolt-field/src/packed/neon/fp32.rs delete mode 100644 crates/jolt-field/src/packed/neon/fp64.rs delete mode 100644 crates/jolt-field/src/packed/neon/mod.rs delete mode 100644 crates/jolt-field/src/packed/tests.rs delete mode 100644 crates/jolt-field/src/parallel.rs delete mode 100644 crates/jolt-field/src/prime/fp128/add_sub.rs delete mode 100644 crates/jolt-field/src/prime/fp128/core.rs delete mode 100644 crates/jolt-field/src/prime/fp128/mod.rs delete mode 100644 crates/jolt-field/src/prime/fp128/mul.rs delete mode 100644 crates/jolt-field/src/prime/fp128/primes.rs delete mode 100644 crates/jolt-field/src/prime/fp128/reduce.rs delete mode 100644 crates/jolt-field/src/prime/fp128/tests.rs delete mode 100644 crates/jolt-field/src/prime/fp128/traits.rs delete mode 100644 crates/jolt-field/src/prime/fp128/wide.rs delete mode 100644 crates/jolt-field/src/prime/fp32.rs delete mode 100644 crates/jolt-field/src/prime/fp64.rs delete mode 100644 crates/jolt-field/src/prime/mod.rs delete mode 100644 crates/jolt-field/src/prime/native_capability.rs delete mode 100644 crates/jolt-field/src/prime/pseudo_mersenne.rs delete mode 100644 crates/jolt-field/src/prime/traits.rs delete mode 100644 crates/jolt-field/src/prime/util.rs rename crates/{jolt-field-two => jolt-field}/src/schedules.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/signed.rs (100%) delete mode 100644 crates/jolt-field/src/signed/mod.rs delete mode 100644 crates/jolt-field/src/signed/signed_bigint.rs delete mode 100644 crates/jolt-field/src/signed/signed_bigint_hi32.rs rename crates/{jolt-field-two => jolt-field}/src/solinas/ext.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/solinas/fp128.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/solinas/mod.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/solinas/packed/engine.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/solinas/packed/ext.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/solinas/packed/fp128.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/solinas/packed/mod.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/solinas/packed/simd.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/solinas/parallel.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/solinas/unreduced.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/solinas/word.rs (100%) rename crates/{jolt-field-two => jolt-field}/src/unreduced.rs (100%) delete mode 100644 crates/jolt-field/src/unreduced/accum.rs delete mode 100644 crates/jolt-field/src/unreduced/mod.rs delete mode 100644 crates/jolt-field/src/unreduced/tests.rs delete mode 100644 crates/jolt-field/tests/binary_field_core_compat.rs rename crates/{jolt-field-two => jolt-field}/tests/bn254_differential.rs (99%) delete mode 100644 crates/jolt-field/tests/coverage.rs delete mode 100644 crates/jolt-field/tests/field_operations.rs rename crates/{jolt-field-two => jolt-field}/tests/golden_bytes.rs (99%) rename crates/{jolt-field-two => jolt-field}/tests/limbs_signed_differential.rs (99%) rename crates/{jolt-field-two => jolt-field}/tests/parallel_macros.rs (95%) delete mode 100644 crates/jolt-field/tests/serde_roundtrip.rs rename crates/{jolt-field-two => jolt-field}/tests/solinas_ext_differential.rs (99%) rename crates/{jolt-field-two => jolt-field}/tests/solinas_fp128_differential.rs (99%) rename crates/{jolt-field-two => jolt-field}/tests/solinas_packed_differential.rs (99%) rename crates/{jolt-field-two => jolt-field}/tests/solinas_unreduced_differential.rs (99%) rename crates/{jolt-field-two => jolt-field}/tests/solinas_words_differential.rs (99%) rename crates/{jolt-field-two => jolt-field}/tests/spine.rs (99%) diff --git a/Cargo.lock b/Cargo.lock index 7dd8ed1d0c..f174cf7c20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3288,32 +3288,6 @@ dependencies = [ [[package]] name = "jolt-field" version = "0.1.0" -dependencies = [ - "akita-config", - "akita-field", - "allocative", - "ark-bn254 0.5.0 (git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout)", - "ark-ff 0.5.0 (git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout)", - "ark-serialize 0.5.0 (git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout)", - "ark-std 0.5.0", - "bincode 2.0.1", - "criterion", - "num-traits", - "p3-baby-bear", - "p3-field", - "p3-koala-bear", - "p3-mersenne-31", - "rand 0.8.7", - "rand_chacha 0.3.1", - "rand_core 0.6.4", - "rayon", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "jolt-field-two" -version = "0.1.0" dependencies = [ "akita-config", "akita-field", @@ -4579,22 +4553,6 @@ dependencies = [ "jolt-sdk", ] -[[package]] -name = "p3-baby-bear" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bc665d4650710aedd2424a59d88c19cb94d85375defc87bf6264d4be25ae6fa" -dependencies = [ - "p3-challenger", - "p3-field", - "p3-mds", - "p3-monty-31", - "p3-poseidon1", - "p3-poseidon2", - "p3-symmetric", - "rand 0.10.2", -] - [[package]] name = "p3-challenger" version = "0.5.3" @@ -4690,27 +4648,6 @@ dependencies = [ "rand 0.10.2", ] -[[package]] -name = "p3-mersenne-31" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "012351fb727ba404175ea1cc159fb6234fa370ce9399317ba04d38ca43e55bfa" -dependencies = [ - "itertools 0.14.0", - "num-bigint", - "p3-challenger", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-mds", - "p3-poseidon2", - "p3-symmetric", - "p3-util", - "paste", - "rand 0.10.2", - "serde", -] - [[package]] name = "p3-monty-31" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index 5147c833bc..24d22bcd9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,6 @@ members = [ "crates/jolt-kernels", "crates/jolt-kernels-derive", "crates/jolt-prover", - "crates/jolt-field-two", "crates/jolt-prover-legacy", "tracer", "common", diff --git a/crates/jolt-field-two/Cargo.toml b/crates/jolt-field-two/Cargo.toml deleted file mode 100644 index b8c40dfedc..0000000000 --- a/crates/jolt-field-two/Cargo.toml +++ /dev/null @@ -1,46 +0,0 @@ -[package] -name = "jolt-field-two" -version = "0.1.0" -edition = "2021" -license = "MIT OR Apache-2.0" -description = "Minimal-LOC rebuild of jolt-field: shared field abstractions with BN254 and Solinas backends" -repository = "https://github.com/a16z/jolt" -keywords = ["SNARK", "cryptography", "finite-fields", "BN254", "Solinas"] -categories = ["cryptography"] - -[lints] -workspace = true - -[dependencies] -# Temporary bootstrap edge for the pre-cutover akita-field types; removed in -# the final migration PR together with the `akita` feature. -akita-config = { workspace = true, optional = true } -akita-field = { workspace = true, optional = true } -ark-ff = { workspace = true, optional = true } -ark-serialize = { workspace = true, optional = true } -ark-bn254 = { workspace = true, features = ["curve"], optional = true } -num-traits = { workspace = true } -serde = { workspace = true, features = ["derive"] } -allocative = { workspace = true, optional = true } -rand_core = { workspace = true } -rayon = { workspace = true, optional = true } -thiserror = { workspace = true } - -[features] -default = ["bn254"] -akita = ["dep:akita-config", "dep:akita-field"] -bn254 = ["dep:ark-ff", "dep:ark-serialize", "dep:ark-bn254"] -solinas = [] -parallel = ["dep:rayon"] -allocative = ["dep:allocative"] - -[dev-dependencies] -bincode = { workspace = true } -num-bigint = { workspace = true } -rand = { workspace = true } -rand_chacha = { workspace = true } - -[[bench]] -name = "ext4_kernels" -harness = false -required-features = ["solinas"] diff --git a/crates/jolt-field-two/src/akita.rs b/crates/jolt-field-two/src/akita.rs deleted file mode 100644 index 6c732d8230..0000000000 --- a/crates/jolt-field-two/src/akita.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! Temporary bootstrap adapter for the pre-cutover `akita-field` type. -//! -//! Implements this crate's contracts for Akita's proof-optimized fp128 field -//! so the adapter stays buildable until the Akita cutover; it is a bootstrap -//! edge, not the target architecture, and is removed in the final migration -//! PR together with the `akita` feature. - -use akita_config::proof_optimized::fp128::Field as AkitaField; -use rand_core::RngCore; - -use crate::{ - AdditiveGroup, CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, - WithAccumulator, -}; - -impl AdditiveGroup for AkitaField {} - -impl Ring for AkitaField { - #[inline] - fn from_u64(v: u64) -> Self { - ::from_u64(v) - } - - #[inline] - fn from_i64(v: i64) -> Self { - ::from_i64(v) - } - - #[inline] - fn from_u128(v: u128) -> Self { - ::from_u128(v) - } - - #[inline] - fn from_i128(v: i128) -> Self { - ::from_i128(v) - } -} - -impl Field for AkitaField { - #[inline] - fn inverse(&self) -> Option { - ::inverse(self) - } - - #[inline] - fn random(rng: &mut R) -> Self { - ::random(rng) - } -} - -impl CanonicalBytes for AkitaField { - const NUM_BYTES: usize = ::NUM_BYTES; - - #[inline(always)] - fn to_bytes_le(&self, out: &mut [u8]) { - ::to_bytes_le(self, out); - } -} - -impl CanonicalEncoding for AkitaField { - // Akita's proof-optimized field is a 128-bit pseudo-Mersenne prime. - const MODULUS_BITS: u32 = 128; - - #[inline(always)] - fn from_bytes_le_reduced(bytes: &[u8]) -> Self { - ::from_le_bytes_mod_order(bytes) - } - - #[inline] - fn from_bytes_le_checked(bytes: &[u8]) -> Option { - if bytes.len() != ::NUM_BYTES { - return None; - } - let value = Self::from_bytes_le_reduced(bytes); - // Canonical iff decoding round-trips to the identical bytes. - (value.to_bytes_le_vec() == bytes).then_some(value) - } - - #[inline] - fn to_u128_checked(&self) -> Option { - let mut buf = [0u8; 16]; - CanonicalBytes::to_bytes_le(self, &mut buf); - Some(u128::from_le_bytes(buf)) - } - - #[inline] - fn from_u128_checked(v: u128) -> Option { - let value = ::from_u128(v); - (value.to_u128_checked() == Some(v)).then_some(value) - } - - #[inline] - fn from_u128_reduced(v: u128) -> Self { - ::from_u128(v) - } - - #[inline] - fn num_bits(&self) -> u32 { - ::num_bits(self) - } - - /// Legacy convention: digest bytes are interpreted as a big-endian - /// integer before reduction. - #[inline] - fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { - let mut buf = bytes.to_vec(); - buf.reverse(); - Self::from_bytes_le_reduced(&buf) - } -} - -impl WithAccumulator for AkitaField { - type Accumulator = NaiveAccumulator; -} diff --git a/crates/jolt-field-two/src/algebra.rs b/crates/jolt-field-two/src/algebra.rs deleted file mode 100644 index 3c602ef5c5..0000000000 --- a/crates/jolt-field-two/src/algebra.rs +++ /dev/null @@ -1,435 +0,0 @@ -//! The trait spine: the algebraic ladder, the canonical (transcript) -//! representation, and deferred-reduction accumulators. -//! -//! ```text -//! AdditiveGroup -> Ring -> Field -//! ``` -//! -//! [`CanonicalEncoding`] and [`WithAccumulator`] are orthogonal capabilities; -//! [`JoltField`] is the blanket-implemented bundle of everything Jolt's -//! protocol stack requires of a scalar field. - -use num_traits::{One, Zero}; -use rand_core::RngCore; -use serde::{de::DeserializeOwned, Serialize}; -use std::fmt::{Debug, Display}; -use std::hash::Hash; -use std::iter::{Product, Sum}; -use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; - -/// Minimal additive group shared by fields, rings, and wide accumulators. -pub trait AdditiveGroup: - Sized - + Clone - + Copy - + Send - + Sync - + Zero - + Add - + for<'a> Add<&'a Self, Output = Self> - + AddAssign - + Sub - + for<'a> Sub<&'a Self, Output = Self> - + SubAssign - + Neg -{ -} - -/// Unital ring: additive group plus multiplication, one, and the integer -/// embedding. -/// -/// The embedding lives here rather than on a separate trait because every -/// unital ring embeds the integers; only the four widest conversions are -/// required, everything else is defaulted on top of them. -pub trait Ring: - AdditiveGroup - + One - + PartialEq - + Eq - + Default - + Debug - + Display - + Hash - + Mul - + for<'a> Mul<&'a Self, Output = Self> - + MulAssign - + Sum - + for<'a> Sum<&'a Self> - + Product - + for<'a> Product<&'a Self> -{ - fn from_u64(v: u64) -> Self; - fn from_i64(v: i64) -> Self; - fn from_u128(v: u128) -> Self; - fn from_i128(v: i128) -> Self; - - #[inline] - fn from_bool(v: bool) -> Self { - Self::from_u64(v as u64) - } - - #[inline] - fn from_u8(v: u8) -> Self { - Self::from_u64(v as u64) - } - - #[inline] - fn from_i8(v: i8) -> Self { - Self::from_i64(v as i64) - } - - #[inline] - fn from_u16(v: u16) -> Self { - Self::from_u64(v as u64) - } - - #[inline] - fn from_i16(v: i16) -> Self { - Self::from_i64(v as i64) - } - - #[inline] - fn from_u32(v: u32) -> Self { - Self::from_u64(v as u64) - } - - #[inline] - fn from_i32(v: i32) -> Self { - Self::from_i64(v as i64) - } - - /// Returns `self * self`. - #[inline] - fn square(&self) -> Self { - *self * *self - } - - /// Returns the ring element `2^exponent`. - #[inline] - fn pow2(exponent: usize) -> Self { - let mut result = Self::one(); - let mut base = Self::one() + Self::one(); - let mut remaining = exponent; - while remaining > 0 { - if remaining % 2 == 1 { - result *= base; - } - remaining /= 2; - if remaining > 0 { - base = base.square(); - } - } - result - } - - /// Multiplies by a `u64`. - #[inline(always)] - fn mul_u64(&self, n: u64) -> Self { - *self * Self::from_u64(n) - } - - /// Multiplies by an `i64`. - #[inline(always)] - fn mul_i64(&self, n: i64) -> Self { - *self * Self::from_i64(n) - } - - /// Multiplies by a `u128`. - #[inline(always)] - fn mul_u128(&self, n: u128) -> Self { - *self * Self::from_u128(n) - } - - /// Multiplies by an `i128`. - #[inline(always)] - fn mul_i128(&self, n: i128) -> Self { - *self * Self::from_i128(n) - } - - /// Multiplies this ring element by the integer `2^pow`. - #[inline] - fn mul_pow_2(&self, pow: usize) -> Self { - assert!(pow <= 255, "pow > 255"); - let mut res = *self; - let mut p = pow; - while p >= 64 { - res *= Self::from_u64(1 << 63); - p -= 63; - } - res * Self::from_u64(1 << p) - } -} - -/// Algebraic field: ring arithmetic plus inversion, sampling, and halving. -pub trait Field: Ring { - /// Multiplicative inverse, or `None` for the zero element. - fn inverse(&self) -> Option; - - /// Multiplicative inverse with zero mapped to zero. - #[inline] - fn inv_or_zero(self) -> Self { - self.inverse().unwrap_or_else(Self::zero) - } - - /// Samples a random element (RNG-backed, for tests and witnesses). - fn random(rng: &mut R) -> Self; - - /// The multiplicative inverse of two. - /// - /// Defaulted via [`inverse`](Self::inverse); fields with a cheap shift - /// implementation override [`half`](Self::half) and this together. - #[inline] - #[expect(clippy::expect_used, reason = "characteristic two is unsupported")] - fn two_inv() -> Self { - Self::from_u64(2) - .inverse() - .expect("field has characteristic two") - } - - /// Divides this element by two. - #[inline] - fn half(self) -> Self { - self * Self::two_inv() - } -} - -/// Metadata contract for a pseudo-Mersenne field `p = 2^k − c`. -/// -/// The exponent `k` is [`CanonicalEncoding::MODULUS_BITS`]; implementing -/// this contract lights up the generic machinery bounded on it (extension -/// towers, packed backends). -pub trait PseudoMersenne: Field + CanonicalEncoding { - /// Offset `c` in `2^k − c`. - const OFFSET: u128; - - /// Degree-4 extension multiply kernel in the `[1, e1, e2, e3]` basis. - /// - /// Defaults to the generic coefficient schedule; base fields whose - /// representation supports fusing product sums before reduction - /// override it (`Fp32` accumulates raw products in `u128`). - #[inline(always)] - fn ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { - crate::schedules::ext4_mul_coeffs(a, b) - } - - /// Degree-4 extension squaring kernel in the `[1, e1, e2, e3]` basis. - #[inline(always)] - fn ext4_square(a: [Self; 4]) -> [Self; 4] { - crate::schedules::ext4_square_coeffs(a) - } - - /// Degree-8 extension multiply kernel in the `[1, e1, ..., e7]` basis. - #[inline(always)] - fn ext8_mul(a: [Self; 8], b: [Self; 8]) -> [Self; 8] { - crate::schedules::ext8_mul_coeffs(a, b) - } - - /// Degree-8 extension squaring kernel in the `[1, e1, ..., e7]` basis. - #[inline(always)] - fn ext8_square(a: [Self; 8]) -> [Self; 8] { - crate::schedules::ext8_square_coeffs(a) - } -} - -/// Fixed-size canonical little-endian byte encoding: the transcript -/// absorption surface. -/// -/// This is deliberately the *narrow* claim, "this value has one canonical -/// byte encoding", implementable by non-field types (e.g. zero-sized -/// commitment placeholders) that must be transcript-absorbable without -/// pretending to be decodable field elements. Field types get the full -/// decode surface via [`CanonicalEncoding`]. -/// -/// # Invariants -/// -/// - The encoding is injective on canonical representatives: equal values -/// produce equal bytes, distinct values produce distinct bytes. -/// - [`to_bytes_le`](Self::to_bytes_le) always writes exactly -/// [`NUM_BYTES`](Self::NUM_BYTES) bytes of the unique representative. -pub trait CanonicalBytes { - /// Byte length of the fixed-size canonical encoding. - const NUM_BYTES: usize; - - /// Writes the canonical little-endian encoding into `out`. - fn to_bytes_le(&self, out: &mut [u8]); - - /// Returns the canonical little-endian encoding as a vector. - #[inline] - fn to_bytes_le_vec(&self) -> Vec { - let mut out = vec![0u8; Self::NUM_BYTES]; - self.to_bytes_le(&mut out); - out - } -} - -/// Canonical decode-and-introspect surface of a field element, on top of the -/// [`CanonicalBytes`] encoding: the single source of canonicity for wire -/// serialization. -/// -/// Transcript absorption and challenge derivation use the explicit -/// [`CanonicalBytes`] encoding so the hashed byte stream is specified -/// independently of any serialization library. Proof/wire serialization goes -/// through serde + bincode, reusing -/// [`from_bytes_le_checked`](Self::from_bytes_le_checked) so non-canonical -/// encodings are rejected uniformly. -pub trait CanonicalEncoding: - CanonicalBytes + Sized + Copy + Default + PartialEq + Eq + Debug + Hash + Send + Sync + 'static -{ - /// Bit length of the field order `|F|` (for prime fields, the modulus). - const MODULUS_BITS: u32; - - /// Decodes little-endian bytes of any length by reducing into the field. - fn from_bytes_le_reduced(bytes: &[u8]) -> Self; - - /// Decodes exactly [`NUM_BYTES`](Self::NUM_BYTES) canonical bytes; - /// `None` on wrong length or a non-canonical value. - fn from_bytes_le_checked(bytes: &[u8]) -> Option; - - /// Returns the canonical representative if it fits in a `u128`. - /// - /// For extension fields: the constant coefficient, when all higher - /// coefficients are zero. - fn to_u128_checked(&self) -> Option; - - /// Returns the canonical representative if it fits in a `u64`. - #[inline] - fn to_u64_checked(&self) -> Option { - self.to_u128_checked().and_then(|v| u64::try_from(v).ok()) - } - - /// Constructs an element when `v` is a canonical representative. - fn from_u128_checked(v: u128) -> Option; - - /// Constructs an element by reducing `v` modulo the field order. - fn from_u128_reduced(v: u128) -> Self; - - /// Number of significant bits in this element's canonical representative. - /// - /// Zero is considered to have zero significant bits. - fn num_bits(&self) -> u32; - - /// Constructs a Fiat-Shamir challenge from squeezed transcript bytes. - #[inline] - fn from_challenge_bytes(bytes: &[u8]) -> Self { - Self::from_bytes_le_reduced(bytes) - } - - /// Constructs a non-optimized scalar challenge from transcript bytes. - #[inline] - fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { - Self::from_challenge_bytes(bytes) - } -} - -/// Accumulates sums and products with potentially deferred modular reduction. -/// -/// The hot-loop pattern `acc += a * b` repeated hundreds of times per output -/// slot dominates the CPU prover. Implementations for specific fields can -/// accumulate unreduced wide products and reduce once at the end. -/// -/// # Invariants -/// -/// - [`fmadd`](Self::fmadd) must be equivalent to `acc += a * b` in the field. -/// - [`merge`](Self::merge) must be equivalent to adding another -/// accumulator's partial result (used for parallel reduction). -/// - [`reduce`](Self::reduce) must return the element equal to the -/// accumulated sum of products. -pub trait Accumulator: Default + Copy + Send + Sync { - /// The element type this accumulator reduces to. - type Element: Ring; - - /// Adds one element into the accumulator. - fn add(&mut self, value: Self::Element); - - /// Merges another accumulator's partial sum into this one. - fn merge(&mut self, other: Self); - - /// Finalizes: reduces the accumulated value to an element. - fn reduce(self) -> Self::Element; - - /// Fused multiply-add: `self += a * b` without intermediate reduction. - fn fmadd(&mut self, a: Self::Element, b: Self::Element); - - /// Fused multiply-add with a `u8` scalar: `self += a * F::from(b)`. - #[inline] - fn fmadd_u8(&mut self, a: Self::Element, b: u8) { - self.fmadd(a, Self::Element::from_u8(b)); - } - - /// Fused multiply-add with a `u64` scalar: `self += a * F::from(b)`. - #[inline] - fn fmadd_u64(&mut self, a: Self::Element, b: u64) { - self.fmadd(a, Self::Element::from_u64(b)); - } - - /// Fused multiply-add with an `i64` scalar: `self += a * F::from(b)`. - #[inline] - fn fmadd_i64(&mut self, a: Self::Element, b: i64) { - self.fmadd(a, Self::Element::from_i64(b)); - } - - /// Fused multiply-add with a `bool` scalar: `self += a` when `b` is true. - #[inline] - fn fmadd_bool(&mut self, a: Self::Element, b: bool) { - if b { - self.add(a); - } - } -} - -/// Associates a deferred-reduction accumulator with an element type. -pub trait WithAccumulator: Ring { - /// Accumulator type. - type Accumulator: Accumulator; -} - -/// Fallback accumulator using standard ring arithmetic: every -/// [`fmadd`](Accumulator::fmadd) performs a full multiply and add. -#[derive(Clone, Copy)] -pub struct NaiveAccumulator(R); - -impl Default for NaiveAccumulator { - #[inline] - fn default() -> Self { - Self(R::zero()) - } -} - -impl Accumulator for NaiveAccumulator { - type Element = R; - - #[inline] - fn add(&mut self, value: R) { - self.0 += value; - } - - #[inline] - fn merge(&mut self, other: Self) { - self.0 += other.0; - } - - #[inline] - fn reduce(self) -> R { - self.0 - } - - #[inline] - fn fmadd(&mut self, a: R, b: R) { - self.0 += a * b; - } -} - -/// Everything Jolt's protocol stack requires of a scalar field: field -/// algebra, a canonical transcript encoding, an accumulator, and a serde -/// wire format. -/// -/// Blanket-implemented — implement the component traits and this follows. -pub trait JoltField: - Field + CanonicalEncoding + WithAccumulator + Serialize + DeserializeOwned -{ -} - -impl JoltField - for T -{ -} diff --git a/crates/jolt-field-two/src/lib.rs b/crates/jolt-field-two/src/lib.rs deleted file mode 100644 index cbe61ae81c..0000000000 --- a/crates/jolt-field-two/src/lib.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Field and ring abstractions for the Jolt zkVM. -//! -//! A slim algebraic ladder — [`AdditiveGroup`] → [`Ring`] → [`Field`] — with -//! orthogonal capabilities: [`CanonicalBytes`]/[`CanonicalEncoding`] (the -//! Fiat-Shamir transcript surface and the field decode surface on top of it) -//! and [`WithAccumulator`] (deferred-reduction fused multiply-add). -//! [`JoltField`] is the blanket-implemented bundle of everything Jolt's -//! protocol stack requires of a scalar field: `Field + CanonicalEncoding + -//! WithAccumulator + Serialize + DeserializeOwned`. Because the impl is a -//! blanket, no field type can forget to opt in. -//! -//! # Architecture: contracts and backends -//! -//! The crate is two layers with a one-way dependency: -//! -//! 1. **Contract layer** (crate root, unconditional): every trait the crate -//! defines — the spine above plus the capability contracts -//! [`PseudoMersenne`], [`ExtField`], [`Ext2Config`], [`MulBaseUnreduced`], -//! [`Unreduced`], [`Fold`], [`Packed`], [`WithPacking`] — together with -//! the stamping macros ([`impl_ring_ops!`], [`impl_group_ops!`], -//! [`impl_serde_bytes!`]) and the backend-neutral value types -//! ([`Limbs`], the [`signed`] bigint families). Contract files contain -//! trait definitions only; the crate's full capability surface is -//! readable from the root regardless of enabled features. -//! 2. **Backend layer** (feature-gated modules): implementations of the -//! contracts. Backends never reference each other and the contract layer -//! never references a backend, so a backend can be deleted or added -//! without touching the contracts. A new backend implements the spine -//! (serde and the [`JoltField`] umbrella come free via the exported -//! macros and the blanket impl) and opts into whichever capability -//! contracts it can serve. -//! -//! # Backends -//! -//! - `bn254` (default): BN254 `Fr`/`Fq` wrapping arkworks, plus -//! `WideAccumulator`, a 9-limb accumulator with deferred Montgomery -//! reduction (first-party Barrett/Montgomery kernels). -//! - `solinas`: fully first-party pseudo-Mersenne fields `p = 2^k − c` — -//! `Fp32`/`Fp64` stamped from one fold algebra plus the hand-written -//! two-limb `Fp128`; cyclotomic extension towers `FpExt2`/`FpExt4`/ -//! `FpExt8` with Frobenius/Moore machinery; unreduced lane accumulators -//! and fold matrices; packed SIMD backends (NEON, AVX2, AVX-512) for -//! 32/64/128-bit lanes and packed extensions. -//! -//! # Feature flags -//! -//! - `bn254` (default) — the arkworks-backed BN254 backend. -//! - `solinas` — the pseudo-Mersenne backend (scalar, extension, unreduced, -//! packed, and the conditional-parallelism helpers in -//! `solinas::parallel`). -//! - `parallel` — activates rayon behind the `cfg_*!` helper macros. -//! - `allocative` — `Allocative` derives on the concrete field types for -//! memory profiling. -//! -//! # Byte compatibility (hard invariants) -//! -//! Wire and transcript encodings are byte-identical to `jolt-field` at the -//! rebuild baseline, for both backends, so replacing that crate cannot -//! change proof bytes: -//! -//! - Proof/wire serialization is serde + bincode over canonical -//! little-endian bytes (see [`impl_serde_bytes!`]); deserialization -//! rejects non-canonical encodings uniformly via -//! [`CanonicalEncoding::from_bytes_le_checked`]. -//! - Fiat-Shamir transcript bytes use the explicit little-endian encoding -//! ([`CanonicalBytes::to_bytes_le`]) and never go through a serialization -//! library. -//! -//! Both invariants are enforced by differential tests against `jolt-field` -//! as the oracle (`tests/*_differential.rs`). - -#[cfg(feature = "akita")] -mod akita; -mod algebra; -#[cfg(feature = "bn254")] -mod bn254; -mod extension; -mod limbs; -mod ops; -mod packed; -mod schedules; -pub mod signed; -#[cfg(feature = "solinas")] -pub mod solinas; -mod unreduced; - -pub use algebra::{ - Accumulator, AdditiveGroup, CanonicalBytes, CanonicalEncoding, Field, JoltField, - NaiveAccumulator, PseudoMersenne, Ring, WithAccumulator, -}; -#[cfg(feature = "bn254")] -pub use bn254::{Fq, Fr, WideAccumulator}; -pub use extension::{Ext2Config, ExtField, MulBaseUnreduced, NegOneNr, TwoNr}; -pub use limbs::Limbs; -pub use num_traits::{One, Zero}; -pub use packed::{NoPacking, Packed, WithPacking}; -#[cfg(feature = "solinas")] -pub use solinas::{ - balanced_digit_lut, canonical_frobenius_thetas, is_registered_prime_offset, - pseudo_mersenne_modulus, registered_prime_offset_spec, solve_frobenius_moore, - validate_canonical_frobenius_thetas, AccumPair, Ext2, FoldMatrixFp32, FoldMatrixFp64, Fp128, - Fp128MulU64Accum, Fp128Packing, Fp128ProductAccum, Fp128x8i32, Fp32, Fp32Packing, - Fp32ProductAccum, Fp32x2i32, Fp64, Fp64Packing, Fp64ProductAccum, Fp64x4i32, FpExt2, - FpExt2Fp64ProductAccum, FpExt4, FpExt4Fp32ProductAccum, FpExt8, PackedFpExt2, PackedFpExt4, - PackedFpExt8, Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, - Prime24Offset3, Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, - Prime48Offset59, Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, - PRIME_OFFSET_IMPLEMENTED_MAX_BITS, PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, -}; -pub use unreduced::{Fold, Unreduced}; - -/// Backend-independent input and shape failures. -#[derive(Debug, thiserror::Error)] -pub enum FieldError { - /// Invalid input parameter or value. - #[error("invalid input: {0}")] - InvalidInput(String), - /// Length mismatch between an expected and provided shape. - #[error("invalid size: expected {expected}, actual {actual}")] - InvalidSize { expected: usize, actual: usize }, -} diff --git a/crates/jolt-field-two/src/limbs.rs b/crates/jolt-field-two/src/limbs.rs deleted file mode 100644 index 81d030a515..0000000000 --- a/crates/jolt-field-two/src/limbs.rs +++ /dev/null @@ -1,291 +0,0 @@ -//! Fixed-width limb array for multi-precision arithmetic. -//! -//! [`Limbs`] is a `#[repr(transparent)]` newtype over `[u64; N]`. -//! All truncated arithmetic lives here as inherent methods. - -use core::cmp::Ordering; - -/// Fixed-width array of `N` 64-bit limbs in little-endian order. -/// -/// Used as the magnitude type for [`SignedBigInt`](crate::signed::SignedBigInt) -/// and as the output of truncated multiplication in unreduced arithmetic. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -#[repr(transparent)] -pub struct Limbs(pub [u64; N]); - -impl Default for Limbs { - #[inline] - fn default() -> Self { - Self::zero() - } -} - -impl Limbs { - #[inline] - pub const fn new(limbs: [u64; N]) -> Self { - Self(limbs) - } - - #[inline] - pub const fn zero() -> Self { - Self([0u64; N]) - } - - #[inline] - pub fn is_zero(&self) -> bool { - self.0.iter().all(|&l| l == 0) - } - - /// Number of significant bits in the value. - #[inline] - pub fn num_bits(&self) -> u32 { - let mut i = N; - while i > 0 { - i -= 1; - if self.0[i] != 0 { - return (i as u32) * 64 + (64 - self.0[i].leading_zeros()); - } - } - 0 - } - - /// Constructs from a single `u64`, placed in the lowest limb. - #[inline] - pub fn from_u64(val: u64) -> Self { - let mut limbs = [0u64; N]; - if N > 0 { - limbs[0] = val; - } - Self(limbs) - } - - /// In-place addition with carry propagation. - /// Returns `true` if the final carry overflowed. - #[inline] - pub fn add_with_carry(&mut self, other: &Self) -> bool { - let mut carry = 0u64; - for i in 0..N { - let sum = (self.0[i] as u128) + (other.0[i] as u128) + (carry as u128); - self.0[i] = sum as u64; - carry = (sum >> 64) as u64; - } - carry != 0 - } - - /// In-place subtraction with borrow propagation. - /// Returns `true` if the final borrow underflowed. - #[inline] - pub fn sub_with_borrow(&mut self, other: &Self) -> bool { - let mut borrow = false; - for i in 0..N { - let (d1, b1) = self.0[i].overflowing_sub(other.0[i]); - let (d2, b2) = d1.overflowing_sub(u64::from(borrow)); - self.0[i] = d2; - borrow = b1 || b2; - } - borrow - } - - /// Truncated multiplication: `self * other`, keeping the low `P` limbs. - #[inline(always)] - pub fn mul_trunc(&self, other: &Limbs) -> Limbs

{ - let mut res = Limbs::

::zero(); - fm_limbs_into::(&self.0, &other.0, &mut res.0); - res - } - - /// Truncated addition: `self + other`, keeping the low `P` limbs. - #[inline] - pub fn add_trunc(&self, other: &Limbs) -> Limbs

{ - let mut acc = Limbs::

::zero(); - let copy_len = if P < N { P } else { N }; - acc.0[..copy_len].copy_from_slice(&self.0[..copy_len]); - acc.add_assign_trunc::(other); - acc - } - - /// Truncated subtraction: `self - other`, keeping the low `P` limbs. - #[inline] - pub fn sub_trunc(&self, other: &Limbs) -> Limbs

{ - let mut acc = Limbs::

::zero(); - let copy_len = if P < N { P } else { N }; - acc.0[..copy_len].copy_from_slice(&self.0[..copy_len]); - acc.sub_assign_trunc::(other); - acc - } - - /// In-place truncated addition: `self += other`, keeping `N` limbs. - #[inline] - pub fn add_assign_trunc(&mut self, other: &Limbs) { - debug_assert!(M <= N, "add_assign_trunc: right operand wider than self"); - let mut carry = 0u64; - for i in 0..N { - let rhs = if i < M { other.0[i] } else { 0 }; - let sum = (self.0[i] as u128) + (rhs as u128) + (carry as u128); - self.0[i] = sum as u64; - carry = (sum >> 64) as u64; - } - } - - /// In-place truncated subtraction: `self -= other`, keeping `N` limbs. - #[inline] - pub fn sub_assign_trunc(&mut self, other: &Limbs) { - debug_assert!(M <= N, "sub_assign_trunc: right operand wider than self"); - let mut borrow = 0u64; - for i in 0..N { - let rhs = if i < M { other.0[i] } else { 0 }; - let diff = (self.0[i] as u128) - .wrapping_sub(rhs as u128) - .wrapping_sub(borrow as u128); - self.0[i] = diff as u64; - borrow = u64::from(diff > u64::MAX as u128); - } - } - - /// Fused multiply-add: `self += a * b`, keeping `N` limbs, with full - /// carry propagation through all higher limbs. - /// - /// Required when accumulating many products to avoid silent overflow at - /// each row's spill position. - #[inline] - pub fn fmadd(&mut self, a: &Limbs, b: &Limbs) { - let i_limit = if A < N { A } else { N }; - for i in 0..i_limit { - let mut carry = 0u64; - let j_limit = if B < (N - i) { B } else { N - i }; - for j in 0..j_limit { - let idx = i + j; - let prod = - (a.0[i] as u128) * (b.0[j] as u128) + (self.0[idx] as u128) + (carry as u128); - self.0[idx] = prod as u64; - carry = (prod >> 64) as u64; - } - let mut k = i + j_limit; - while carry != 0 && k < N { - let sum = (self.0[k] as u128) + (carry as u128); - self.0[k] = sum as u64; - carry = (sum >> 64) as u64; - k += 1; - } - } - } - - /// Multiply and keep only the low `N` limbs (same width as self). - #[inline(always)] - pub fn mul_low(&self, other: &Self) -> Self { - self.mul_trunc::(other) - } - - /// Zero-extend a narrower `Limbs` into `Limbs`. - #[inline] - pub fn zero_extend_from(smaller: &Limbs) -> Limbs { - debug_assert!(M <= N, "cannot zero-extend from a wider source"); - let mut limbs = [0u64; N]; - let copy_len = if M < N { M } else { N }; - limbs[..copy_len].copy_from_slice(&smaller.0[..copy_len]); - Limbs(limbs) - } -} - -impl From for Limbs { - #[inline] - fn from(val: u64) -> Self { - Self::from_u64(val) - } -} - -impl AsRef<[u64]> for Limbs { - #[inline] - fn as_ref(&self) -> &[u64] { - &self.0 - } -} - -impl PartialOrd for Limbs { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Limbs { - #[inline] - fn cmp(&self, other: &Self) -> Ordering { - let mut i = N; - while i > 0 { - i -= 1; - match self.0[i].cmp(&other.0[i]) { - Ordering::Equal => {} - ord => return ord, - } - } - Ordering::Equal - } -} - -impl core::fmt::Debug for Limbs { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "Limbs([")?; - for (i, limb) in self.0.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - write!(f, "{limb:#018x}")?; - } - write!(f, "])") - } -} - -impl core::fmt::Display for Limbs { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let mut started = false; - for &limb in self.0.iter().rev() { - if started { - write!(f, "{limb:016x}")?; - } else if limb != 0 { - write!(f, "{limb:x}")?; - started = true; - } - } - if !started { - write!(f, "0")?; - } - Ok(()) - } -} - -#[cfg(feature = "allocative")] -impl allocative::Allocative for Limbs { - fn visit<'a, 'b: 'a>(&self, visitor: &'a mut allocative::Visitor<'b>) { - visitor.visit_simple_sized::(); - } -} - -/// Core schoolbook multiplication accumulator: `acc += a * b`, keeping only -/// the low `P` limbs. -#[inline(always)] -fn fm_limbs_into( - a: &[u64; N], - b: &[u64; M], - acc: &mut [u64; P], -) { - for (j, &mul_limb) in b.iter().enumerate() { - if mul_limb == 0 { - continue; - } - let mut carry = 0u64; - for (i, &a_limb) in a.iter().enumerate() { - let idx = j + i; - if idx < P { - let prod = - (a_limb as u128) * (mul_limb as u128) + (acc[idx] as u128) + (carry as u128); - acc[idx] = prod as u64; - carry = (prod >> 64) as u64; - } - } - let next = j + N; - if next < P { - acc[next] = acc[next].wrapping_add(carry); - } - } -} diff --git a/crates/jolt-field/Cargo.toml b/crates/jolt-field/Cargo.toml index 0886ce57ff..d1f4c669f5 100644 --- a/crates/jolt-field/Cargo.toml +++ b/crates/jolt-field/Cargo.toml @@ -3,7 +3,7 @@ name = "jolt-field" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" -description = "Shared field abstractions and optimized BN254 and Solinas backends for Jolt" +description = "Shared field abstractions with BN254 and Solinas backends" repository = "https://github.com/a16z/jolt" keywords = ["SNARK", "cryptography", "finite-fields", "BN254", "Solinas"] categories = ["cryptography"] @@ -12,9 +12,8 @@ categories = ["cryptography"] workspace = true [dependencies] -# Temporary bootstrap edge for the staged Akita migration: keeps the legacy -# `akita` adapter buildable until the Akita cutover lands. Removed together -# with `src/akita.rs` in the final migration PR. +# Temporary bootstrap edge for the pre-cutover akita-field types; removed in +# the final migration PR together with the `akita` feature. akita-config = { workspace = true, optional = true } akita-field = { workspace = true, optional = true } ark-ff = { workspace = true, optional = true } @@ -23,15 +22,12 @@ ark-bn254 = { workspace = true, features = ["curve"], optional = true } num-traits = { workspace = true } serde = { workspace = true, features = ["derive"] } allocative = { workspace = true, optional = true } -rand = { workspace = true } rand_core = { workspace = true } rayon = { workspace = true, optional = true } thiserror = { workspace = true } [features] -# Preserve Jolt's existing BN254 default during the coordinated cutover. default = ["bn254"] -# Temporary bootstrap feature; see the akita-* dependency note above. akita = ["dep:akita-config", "dep:akita-field"] bn254 = ["dep:ark-ff", "dep:ark-serialize", "dep:ark-bn254"] solinas = [] @@ -40,20 +36,11 @@ allocative = ["dep:allocative"] [dev-dependencies] bincode = { workspace = true } -ark-std = { workspace = true } +num-bigint = { workspace = true } +rand = { workspace = true } rand_chacha = { workspace = true } -criterion = { workspace = true } -p3-field = "=0.5.3" -p3-mersenne-31 = "=0.5.3" -p3-baby-bear = "=0.5.3" -p3-koala-bear = "=0.5.3" - -[[bench]] -name = "field_arith" -harness = false -required-features = ["bn254"] [[bench]] -name = "solinas_field_arith" +name = "ext4_kernels" harness = false -required-features = ["bn254", "solinas", "parallel"] +required-features = ["solinas"] diff --git a/crates/jolt-field-two/benches/ext4_kernels.rs b/crates/jolt-field/benches/ext4_kernels.rs similarity index 98% rename from crates/jolt-field-two/benches/ext4_kernels.rs rename to crates/jolt-field/benches/ext4_kernels.rs index 8be5cbaf60..5ad8fa93a3 100644 --- a/crates/jolt-field-two/benches/ext4_kernels.rs +++ b/crates/jolt-field/benches/ext4_kernels.rs @@ -12,11 +12,11 @@ //! and the crate keeps the generic defaults. This harness stays as the //! reproducible evidence; rerun it before reintroducing an override. //! -//! Run: `cargo bench -p jolt-field-two --features solinas --bench ext4_kernels` +//! Run: `cargo bench -p jolt-field --features solinas --bench ext4_kernels` #![expect(clippy::print_stdout, reason = "bench harness: stdout is the report")] -use jolt_field_two as two; +use jolt_field as two; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; diff --git a/crates/jolt-field/benches/field_arith.rs b/crates/jolt-field/benches/field_arith.rs deleted file mode 100644 index 90e9152790..0000000000 --- a/crates/jolt-field/benches/field_arith.rs +++ /dev/null @@ -1,61 +0,0 @@ -#![expect(unused_results)] - -use std::hint::black_box; - -use criterion::{criterion_group, criterion_main, Criterion}; -use jolt_field::{CanonicalBytes, CanonicalRepr, FieldCore, Fr, FromPrimitiveInt}; -use rand_chacha::ChaCha20Rng; -use rand_core::SeedableRng; - -fn bench_field_mul(c: &mut Criterion) { - let mut rng = ChaCha20Rng::seed_from_u64(0); - let a: Fr = ::random(&mut rng); - let b: Fr = ::random(&mut rng); - - c.bench_function("Fr * Fr", |bench| { - bench.iter(|| black_box(a) * black_box(b)); - }); -} - -fn bench_mul_u64(c: &mut Criterion) { - let mut rng = ChaCha20Rng::seed_from_u64(1); - let a: Fr = ::random(&mut rng); - let n = 0xDEAD_BEEF_CAFE_BABEu64; - - c.bench_function("Fr::mul_u64", |bench| { - bench.iter(|| ::mul_u64(black_box(&a), black_box(n))); - }); -} - -fn bench_mul_u128(c: &mut Criterion) { - let mut rng = ChaCha20Rng::seed_from_u64(2); - let a: Fr = ::random(&mut rng); - let n = 0xDEAD_BEEF_CAFE_BABE_1234_5678_9ABC_DEF0u128; - - c.bench_function("Fr::mul_u128", |bench| { - bench.iter(|| ::mul_u128(black_box(&a), black_box(n))); - }); -} - -fn bench_to_from_bytes(c: &mut Criterion) { - let mut rng = ChaCha20Rng::seed_from_u64(4); - let a: Fr = ::random(&mut rng); - let bytes = a.to_bytes_le_vec(); - - c.bench_function("Fr::to_bytes", |bench| { - bench.iter(|| black_box(a).to_bytes_le_vec()); - }); - - c.bench_function("Fr::from_bytes", |bench| { - bench.iter(|| ::from_le_bytes_mod_order(black_box(&bytes))); - }); -} - -criterion_group!( - benches, - bench_field_mul, - bench_mul_u64, - bench_mul_u128, - bench_to_from_bytes, -); -criterion_main!(benches); diff --git a/crates/jolt-field/benches/solinas_field_arith.rs b/crates/jolt-field/benches/solinas_field_arith.rs deleted file mode 100644 index d240dc8d1f..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith.rs +++ /dev/null @@ -1,34 +0,0 @@ -#![expect( - deprecated, - unused_results, - clippy::expect_used, - clippy::explicit_iter_loop, - clippy::map_unwrap_or, - clippy::semicolon_if_nothing_returned, - reason = "ported benchmark preserves the established measurement harness" -)] - -#[path = "solinas_field_arith/mod.rs"] -mod field_arith_suite; - -use criterion::{criterion_group, criterion_main}; -use field_arith_suite::{ - bench_base_field_matrix, bench_comparisons, bench_ext2_matrix, bench_ext4_matrix, - bench_kernel_patterns, bench_p3_base_matrix, bench_p3_ext4_matrix, bench_p3_ext5_matrix, - bench_parallel_throughput, bench_wide_ops, -}; - -criterion_group!( - field_arith, - bench_base_field_matrix, - bench_ext2_matrix, - bench_ext4_matrix, - bench_p3_base_matrix, - bench_p3_ext4_matrix, - bench_p3_ext5_matrix, - bench_wide_ops, - bench_kernel_patterns, - bench_comparisons, - bench_parallel_throughput -); -criterion_main!(field_arith); diff --git a/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs b/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs deleted file mode 100644 index 46f6af6591..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/arithmetic.rs +++ /dev/null @@ -1,689 +0,0 @@ -use std::ops::{AddAssign, MulAssign, SubAssign}; -use std::time::Instant; - -use criterion::{black_box, Criterion, Throughput}; -use jolt_field::packed::PackedField; -use jolt_field::FieldCore; -use rand::{rngs::StdRng, SeedableRng}; - -use super::data::duration_per_logical_op; -use super::params::ArithmeticBenchParams; - -pub(crate) fn bench_arithmetic_case( - c: &mut Criterion, - family: &str, - label: &str, - seed: u64, - params: ArithmeticBenchParams, -) where - F: FieldCore + AddAssign + SubAssign + MulAssign + 'static, - PF: PackedField + Copy + 'static, -{ - let mut rng = StdRng::seed_from_u64(seed); - let scalar_latency_inputs: Vec = (0..params.latency_iters) - .map(|_| F::random(&mut rng)) - .collect(); - let packed_latency_inputs: Vec = (0..params.latency_iters) - .map(|_| PF::from_fn(|_| F::random(&mut rng))) - .collect(); - let scalar_stream_lanes: Vec<(F, F)> = (0..params.streams) - .map(|_| (F::random(&mut rng), F::random(&mut rng))) - .collect(); - let packed_stream_lanes: Vec<(PF, PF)> = (0..params.streams) - .map(|_| { - ( - PF::from_fn(|_| F::random(&mut rng)), - PF::from_fn(|_| F::random(&mut rng)), - ) - }) - .collect(); - - let mut latency_group = c.benchmark_group(format!( - "field_arith/{family}/latency_chain/{label}_w{}", - PF::WIDTH - )); - - bench_scalar_latency::( - &mut latency_group, - "add", - params.latency_iters, - &scalar_latency_inputs, - |mut acc, x| { - acc += x; - acc - }, - F::zero(), - ); - bench_scalar_latency::( - &mut latency_group, - "sub", - params.latency_iters, - &scalar_latency_inputs, - |mut acc, x| { - acc -= x; - acc - }, - F::zero(), - ); - bench_scalar_unary_latency::( - &mut latency_group, - "neg", - params.latency_iters, - &scalar_latency_inputs, - |acc| -acc, - ); - bench_scalar_unary_latency::( - &mut latency_group, - "double", - params.latency_iters, - &scalar_latency_inputs, - |acc| acc + acc, - ); - bench_scalar_latency::( - &mut latency_group, - "add_neg", - params.latency_iters, - &scalar_latency_inputs, - |acc, x| -(acc + x), - F::zero(), - ); - bench_scalar_latency::( - &mut latency_group, - "double_add", - params.latency_iters, - &scalar_latency_inputs, - |acc, x| acc + acc + x, - F::zero(), - ); - bench_scalar_latency::( - &mut latency_group, - "mul", - params.latency_iters, - &scalar_latency_inputs, - |mut acc, x| { - acc *= x; - acc - }, - F::one(), - ); - bench_scalar_latency::( - &mut latency_group, - "mul_add", - params.latency_iters, - &scalar_latency_inputs, - |acc, x| acc * x + acc, - F::one(), - ); - - latency_group.throughput(Throughput::Elements(1)); - latency_group.bench_function( - format!("scalar_square_chain/{}_ns_per_op", params.latency_iters), - |b| { - b.iter_custom(|iters| { - let mut acc = black_box(scalar_latency_inputs[0]); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.latency_iters { - acc = acc.square(); - } - } - black_box(acc); - duration_per_logical_op(start.elapsed(), params.latency_iters as u64) - }) - }, - ); - - latency_group.throughput(Throughput::Elements(1)); - latency_group.bench_function( - format!("scalar_mul_self_chain/{}_ns_per_op", params.latency_iters), - |b| { - b.iter_custom(|iters| { - let mut acc = black_box(scalar_latency_inputs[0]); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.latency_iters { - acc = acc * acc; - } - } - black_box(acc); - duration_per_logical_op(start.elapsed(), params.latency_iters as u64) - }) - }, - ); - - latency_group.throughput(Throughput::Elements(1)); - latency_group.bench_function( - format!( - "scalar_inverse_chain/{}_ns_per_op", - params.inverse_latency_iters - ), - |b| { - b.iter_custom(|iters| { - let inputs = black_box(&scalar_latency_inputs[..params.inverse_latency_iters]); - let mut acc = F::one(); - let start = Instant::now(); - for _ in 0..iters { - for x in inputs { - acc = (acc + *x).inverse().unwrap_or_else(F::one); - } - } - black_box(acc); - duration_per_logical_op(start.elapsed(), params.inverse_latency_iters as u64) - }) - }, - ); - - bench_packed_latency::( - &mut latency_group, - "add", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc + x, - PF::broadcast(F::zero()), - ); - bench_packed_latency::( - &mut latency_group, - "sub", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc - x, - PF::broadcast(F::zero()), - ); - let packed_zero = PF::broadcast(F::zero()); - bench_packed_unary_latency::( - &mut latency_group, - "neg", - params.latency_iters, - &packed_latency_inputs, - |acc| packed_zero - acc, - ); - bench_packed_unary_latency::( - &mut latency_group, - "double", - params.latency_iters, - &packed_latency_inputs, - |acc| acc + acc, - ); - bench_packed_latency::( - &mut latency_group, - "add_neg", - params.latency_iters, - &packed_latency_inputs, - |acc, x| packed_zero - (acc + x), - packed_zero, - ); - bench_packed_latency::( - &mut latency_group, - "double_add", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc + acc + x, - PF::broadcast(F::zero()), - ); - bench_packed_latency::( - &mut latency_group, - "mul", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc * x, - PF::broadcast(F::one()), - ); - bench_packed_latency::( - &mut latency_group, - "mul_add", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc * x + acc, - PF::broadcast(F::one()), - ); - bench_packed_unary_latency::( - &mut latency_group, - "square", - params.latency_iters, - &packed_latency_inputs, - |acc| acc.square(), - ); - bench_packed_unary_latency::( - &mut latency_group, - "mul_self", - params.latency_iters, - &packed_latency_inputs, - |acc| acc * acc, - ); - latency_group.throughput(Throughput::Elements(1)); - latency_group.bench_function( - format!( - "packed_inverse_chain/{}x{}_ns_lane", - params.inverse_latency_iters, - PF::WIDTH - ), - |b| { - b.iter_custom(|iters| { - let inputs = black_box(&packed_latency_inputs[..params.inverse_latency_iters]); - let mut acc = PF::broadcast(F::one()); - let start = Instant::now(); - for _ in 0..iters { - for x in inputs { - acc = (acc + *x) - .inverse() - .unwrap_or_else(|| PF::broadcast(F::one())); - } - } - black_box(acc.extract(0)); - duration_per_logical_op( - start.elapsed(), - (params.inverse_latency_iters * PF::WIDTH) as u64, - ) - }) - }, - ); - - latency_group.finish(); - - let mut throughput_group = c.benchmark_group(format!( - "field_arith/{family}/throughput_stream/{label}_w{}", - PF::WIDTH - )); - - bench_scalar_throughput::( - &mut throughput_group, - "add", - params, - &scalar_stream_lanes, - |mut acc, x| { - acc += x; - acc - }, - |a, b| a + b, - ); - bench_scalar_throughput::( - &mut throughput_group, - "sub", - params, - &scalar_stream_lanes, - |mut acc, x| { - acc -= x; - acc - }, - |a, b| a - b, - ); - bench_scalar_throughput::( - &mut throughput_group, - "mul", - params, - &scalar_stream_lanes, - |mut acc, x| { - acc *= x; - acc - }, - |a, b| a * b, - ); - - throughput_group.throughput(Throughput::Elements(1)); - throughput_group.bench_function( - format!( - "scalar_square_stream/{}x{}_ns_per_op", - params.streams, params.throughput_iters - ), - |b| { - b.iter_custom(|iters| { - let lanes = black_box(&scalar_stream_lanes); - let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.throughput_iters { - for acc_i in acc.iter_mut() { - *acc_i = acc_i.square(); - } - } - } - black_box(acc[0]); - duration_per_logical_op( - start.elapsed(), - (params.streams * params.throughput_iters) as u64, - ) - }) - }, - ); - - throughput_group.throughput(Throughput::Elements(1)); - throughput_group.bench_function( - format!( - "scalar_inverse_stream/{}x{}_ns_per_op", - params.streams, params.inverse_throughput_iters - ), - |b| { - b.iter_custom(|iters| { - let lanes = black_box(&scalar_stream_lanes); - let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.inverse_throughput_iters { - for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { - *acc_i = (*acc_i + lane.0).inverse().unwrap_or_else(F::one); - } - } - } - black_box(acc[0]); - duration_per_logical_op( - start.elapsed(), - (params.streams * params.inverse_throughput_iters) as u64, - ) - }) - }, - ); - - bench_packed_throughput::( - &mut throughput_group, - "add", - params, - &packed_stream_lanes, - |acc, x| acc + x, - |a, b| a + b, - ); - bench_packed_throughput::( - &mut throughput_group, - "sub", - params, - &packed_stream_lanes, - |acc, x| acc - x, - |a, b| a - b, - ); - bench_packed_throughput::( - &mut throughput_group, - "mul", - params, - &packed_stream_lanes, - |acc, x| acc * x, - |a, b| a * b, - ); - - throughput_group.throughput(Throughput::Elements(1)); - throughput_group.bench_function( - format!( - "packed_square_stream/{}x{}x{}_ns_lane", - params.streams, - PF::WIDTH, - params.throughput_iters - ), - |b| { - b.iter_custom(|iters| { - let lanes = black_box(&packed_stream_lanes); - let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.throughput_iters { - for acc_i in acc.iter_mut() { - *acc_i = acc_i.square(); - } - } - } - black_box(acc[0].extract(0)); - duration_per_logical_op( - start.elapsed(), - (params.streams * PF::WIDTH * params.throughput_iters) as u64, - ) - }) - }, - ); - - throughput_group.throughput(Throughput::Elements(1)); - throughput_group.bench_function( - format!( - "packed_mul_self_stream/{}x{}x{}_ns_lane", - params.streams, - PF::WIDTH, - params.throughput_iters - ), - |b| { - b.iter_custom(|iters| { - let lanes = black_box(&packed_stream_lanes); - let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.throughput_iters { - for acc_i in acc.iter_mut() { - let x = *acc_i; - *acc_i = x * x; - } - } - } - black_box(acc[0].extract(0)); - duration_per_logical_op( - start.elapsed(), - (params.streams * PF::WIDTH * params.throughput_iters) as u64, - ) - }) - }, - ); - - throughput_group.throughput(Throughput::Elements(1)); - throughput_group.bench_function( - format!( - "packed_inverse_stream/{}x{}x{}_ns_lane", - params.streams, - PF::WIDTH, - params.inverse_throughput_iters - ), - |b| { - b.iter_custom(|iters| { - let lanes = black_box(&packed_stream_lanes); - let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.inverse_throughput_iters { - for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { - *acc_i = (*acc_i + lane.0) - .inverse() - .unwrap_or_else(|| PF::broadcast(F::one())); - } - } - } - black_box(acc[0].extract(0)); - duration_per_logical_op( - start.elapsed(), - (params.streams * PF::WIDTH * params.inverse_throughput_iters) as u64, - ) - }) - }, - ); - - throughput_group.finish(); -} - -fn bench_scalar_latency( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - op: &str, - latency_iters: usize, - inputs: &[F], - step: impl Fn(F, F) -> F, - init: F, -) where - F: FieldCore, -{ - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!("scalar_{op}_chain/{latency_iters}_ns_per_op"), - |b| { - b.iter_custom(|iters| { - let inputs = black_box(inputs); - let mut acc = init; - let start = Instant::now(); - for _ in 0..iters { - for x in inputs { - acc = step(acc, *x); - } - } - black_box(acc); - duration_per_logical_op(start.elapsed(), latency_iters as u64) - }) - }, - ); -} - -fn bench_scalar_unary_latency( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - op: &str, - latency_iters: usize, - inputs: &[F], - step: impl Fn(F) -> F, -) where - F: FieldCore, -{ - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!("scalar_{op}_chain/{latency_iters}_ns_per_op"), - |b| { - b.iter_custom(|iters| { - let mut acc = black_box(inputs[0]); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..latency_iters { - acc = step(acc); - } - } - black_box(acc); - duration_per_logical_op(start.elapsed(), latency_iters as u64) - }) - }, - ); -} - -fn bench_packed_latency( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - op: &str, - latency_iters: usize, - inputs: &[PF], - step: impl Fn(PF, PF) -> PF, - init: PF, -) where - F: FieldCore, - PF: PackedField + Copy, -{ - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!("packed_{op}_chain/{latency_iters}x{}_ns_lane", PF::WIDTH), - |b| { - b.iter_custom(|iters| { - let inputs = black_box(inputs); - let mut acc = init; - let start = Instant::now(); - for _ in 0..iters { - for x in inputs { - acc = step(acc, *x); - } - } - black_box(acc.extract(0)); - duration_per_logical_op(start.elapsed(), (latency_iters * PF::WIDTH) as u64) - }) - }, - ); -} - -fn bench_packed_unary_latency( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - op: &str, - latency_iters: usize, - inputs: &[PF], - step: impl Fn(PF) -> PF, -) where - F: FieldCore, - PF: PackedField + Copy, -{ - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!("packed_{op}_chain/{latency_iters}x{}_ns_lane", PF::WIDTH), - |b| { - b.iter_custom(|iters| { - let mut acc = black_box(inputs[0]); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..latency_iters { - acc = step(acc); - } - } - black_box(acc.extract(0)); - duration_per_logical_op(start.elapsed(), (latency_iters * PF::WIDTH) as u64) - }) - }, - ); -} - -fn bench_scalar_throughput( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - op: &str, - params: ArithmeticBenchParams, - lanes: &[(F, F)], - step: impl Fn(F, F) -> F, - init: impl Fn(F, F) -> F, -) where - F: FieldCore, -{ - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!( - "scalar_{op}_stream/{}x{}_ns_per_op", - params.streams, params.throughput_iters - ), - |b| { - b.iter_custom(|iters| { - let lanes = black_box(lanes); - let mut acc: Vec = lanes.iter().map(|(a, b)| init(*a, *b)).collect(); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.throughput_iters { - for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { - *acc_i = step(*acc_i, lane.0); - } - } - } - black_box(acc[0]); - duration_per_logical_op( - start.elapsed(), - (params.streams * params.throughput_iters) as u64, - ) - }) - }, - ); -} - -fn bench_packed_throughput( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - op: &str, - params: ArithmeticBenchParams, - lanes: &[(PF, PF)], - step: impl Fn(PF, PF) -> PF, - init: impl Fn(PF, PF) -> PF, -) where - F: FieldCore, - PF: PackedField + Copy, -{ - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!( - "packed_{op}_stream/{}x{}x{}_ns_lane", - params.streams, - PF::WIDTH, - params.throughput_iters - ), - |b| { - b.iter_custom(|iters| { - let lanes = black_box(lanes); - let mut acc: Vec = lanes.iter().map(|(a, b)| init(*a, *b)).collect(); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.throughput_iters { - for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { - *acc_i = step(*acc_i, lane.0); - } - } - } - black_box(acc[0].extract(0)); - duration_per_logical_op( - start.elapsed(), - (params.streams * PF::WIDTH * params.throughput_iters) as u64, - ) - }) - }, - ); -} diff --git a/crates/jolt-field/benches/solinas_field_arith/base.rs b/crates/jolt-field/benches/solinas_field_arith/base.rs deleted file mode 100644 index 67858732d8..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/base.rs +++ /dev/null @@ -1,64 +0,0 @@ -use criterion::Criterion; -use jolt_field::{ - Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime48Offset59, Prime56Offset27, - Prime64Offset59, -}; - -use super::arithmetic::bench_arithmetic_case; -use super::cases::*; -use super::params::ArithmeticBenchParams; - -pub(crate) fn bench_base_field_matrix(c: &mut Criterion) { - let params = ArithmeticBenchParams::from_env("AKITA_BENCH_BASE_ARITH", 2048, 256); - - bench_arithmetic_case::( - c, - "base", - PRIME31_OFFSET19, - 0xba5e_0031, - params, - ); - bench_arithmetic_case::( - c, - "base", - MERSENNE31, - 0xba5e_3131, - params, - ); - bench_arithmetic_case::( - c, - "base", - PRIME32_OFFSET99, - 0xba5e_0032, - params, - ); - bench_arithmetic_case::( - c, - "base", - PRIME40_OFFSET195, - 0xba5e_0040, - params, - ); - bench_arithmetic_case::( - c, - "base", - PRIME48_OFFSET59, - 0xba5e_0048, - params, - ); - bench_arithmetic_case::( - c, - "base", - PRIME56_OFFSET27, - 0xba5e_0056, - params, - ); - bench_arithmetic_case::( - c, - "base", - PRIME64_OFFSET59, - 0xba5e_0064, - params, - ); - bench_arithmetic_case::(c, "base", PRIME128_OFFSET275, 0xba5e_0128, params); -} diff --git a/crates/jolt-field/benches/solinas_field_arith/cases.rs b/crates/jolt-field/benches/solinas_field_arith/cases.rs deleted file mode 100644 index aafb95bf93..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/cases.rs +++ /dev/null @@ -1,25 +0,0 @@ -use jolt_field::packed::{Fp32Packing, HasPacking}; -use jolt_field::{ - Fp32, Prime128Offset275, Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime48Offset59, - Prime56Offset27, Prime64Offset59, -}; - -pub(crate) type Mersenne31 = Fp32<{ (1u32 << 31) - 1 }>; -pub(crate) type PackedMersenne31 = Fp32Packing<{ (1u32 << 31) - 1 }>; -pub(crate) type P31O19 = ::Packing; -pub(crate) type P32O99 = ::Packing; -pub(crate) type P40O195 = ::Packing; -pub(crate) type P48O59 = ::Packing; -pub(crate) type P56O27 = ::Packing; -pub(crate) type P64O59 = ::Packing; -pub(crate) type P128O275 = ::Packing; -pub(crate) type F128 = Prime128Offset275; - -pub(crate) const PRIME31_OFFSET19: &str = "prime31_offset19"; -pub(crate) const MERSENNE31: &str = "mersenne31"; -pub(crate) const PRIME32_OFFSET99: &str = "prime32_offset99"; -pub(crate) const PRIME40_OFFSET195: &str = "prime40_offset195"; -pub(crate) const PRIME48_OFFSET59: &str = "prime48_offset59"; -pub(crate) const PRIME56_OFFSET27: &str = "prime56_offset27"; -pub(crate) const PRIME64_OFFSET59: &str = "prime64_offset59"; -pub(crate) const PRIME128_OFFSET275: &str = "prime128_offset275"; diff --git a/crates/jolt-field/benches/solinas_field_arith/comparison.rs b/crates/jolt-field/benches/solinas_field_arith/comparison.rs deleted file mode 100644 index 7551726504..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/comparison.rs +++ /dev/null @@ -1,63 +0,0 @@ -use ark_bn254::Fr as BN254Fr; -use ark_ff::{AdditiveGroup, Field, UniformRand}; -use criterion::{black_box, Criterion}; -use rand::{rngs::StdRng, SeedableRng}; - -pub(crate) fn bench_comparisons(c: &mut Criterion) { - let mut rng = StdRng::seed_from_u64(0x5eed); - let inputs: Vec = (0..2048).map(|_| BN254Fr::rand(&mut rng)).collect(); - - let mut group = c.benchmark_group("field_arith/comparison/bn254"); - - group.bench_function("mul_add_chain_2048", |b| { - b.iter(|| { - let mut acc = BN254Fr::ONE; - for x in inputs.iter() { - acc = acc * x + acc; - } - black_box(acc) - }) - }); - - group.bench_function("mul_chain_2048", |b| { - b.iter(|| { - let mut acc = BN254Fr::ONE; - for x in inputs.iter() { - acc *= x; - } - black_box(acc) - }) - }); - - group.bench_function("mul_parallel_1024", |b| { - b.iter(|| { - let mut sum = BN254Fr::ZERO; - for pair in inputs.chunks_exact(2) { - sum += pair[0] * pair[1]; - } - black_box(sum) - }) - }); - - group.bench_function("sqr_chain_2048", |b| { - b.iter(|| { - let mut acc = inputs[0]; - for _ in 0..2048 { - acc.square_in_place(); - } - black_box(acc) - }) - }); - - group.bench_function("inv_256", |b| { - b.iter(|| { - let mut acc = BN254Fr::ONE; - for x in inputs[..256].iter() { - acc *= x.inverse().unwrap_or(BN254Fr::ZERO); - } - black_box(acc) - }) - }); - - group.finish(); -} diff --git a/crates/jolt-field/benches/solinas_field_arith/data.rs b/crates/jolt-field/benches/solinas_field_arith/data.rs deleted file mode 100644 index bede964e4f..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/data.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::time::Duration; - -use rand::RngCore; - -pub(crate) fn rand_u128(rng: &mut R) -> u128 { - let lo = rng.next_u64() as u128; - let hi = rng.next_u64() as u128; - lo | (hi << 64) -} - -/// Per-logical-op time returned from `iter_custom`. -/// -/// Criterion divides the returned duration by its batch `iters` again; only count logical ops -/// inside one batch (e.g. `latency_iters` or `latency_iters * WIDTH`). -pub(crate) fn duration_per_logical_op(elapsed: Duration, logical_ops_per_batch: u64) -> Duration { - Duration::from_secs_f64(elapsed.as_secs_f64() / logical_ops_per_batch.max(1) as f64) -} diff --git a/crates/jolt-field/benches/solinas_field_arith/ext2.rs b/crates/jolt-field/benches/solinas_field_arith/ext2.rs deleted file mode 100644 index a469712c5d..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/ext2.rs +++ /dev/null @@ -1,47 +0,0 @@ -use criterion::Criterion; -use jolt_field::packed::{HasPacking, PackedFpExt2}; -use jolt_field::{FpExt2, Prime31Offset19, Prime32Offset99, Prime64Offset59, TwoNr}; - -use super::arithmetic::bench_arithmetic_case; -use super::params::ArithmeticBenchParams; - -pub(crate) fn bench_ext2_matrix(c: &mut Criterion) { - type F31 = Prime31Offset19; - type PF31 = ::Packing; - type F31FpExt2 = FpExt2; - type PF31FpExt2 = PackedFpExt2; - - type F32 = Prime32Offset99; - type PF32 = ::Packing; - type F32FpExt2 = FpExt2; - type PF32FpExt2 = PackedFpExt2; - - type F64 = Prime64Offset59; - type PF64 = ::Packing; - type F64FpExt2 = FpExt2; - type PF64FpExt2 = PackedFpExt2; - - let params = ArithmeticBenchParams::from_env("AKITA_BENCH_EXT2_ARITH", 512, 128); - - bench_arithmetic_case::( - c, - "ext2", - "prime31_offset19_fp_ext2", - 0xe200_0031, - params, - ); - bench_arithmetic_case::( - c, - "ext2", - "prime32_offset99_fp_ext2", - 0xe200_0032, - params, - ); - bench_arithmetic_case::( - c, - "ext2", - "prime64_offset59_fp_ext2", - 0xe200_0064, - params, - ); -} diff --git a/crates/jolt-field/benches/solinas_field_arith/ext4.rs b/crates/jolt-field/benches/solinas_field_arith/ext4.rs deleted file mode 100644 index e1d5dffd6e..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/ext4.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Degree-4 extension microbenches. -//! -//! Criterion directory names are capped at 64 characters (`MAX_DIRECTORY_NAME_LEN`). -//! Use the short `label` strings below (≤ 12 chars before `_w{width}`) so groups are not -//! truncated. - -use criterion::Criterion; -use jolt_field::packed::HasPacking; -use jolt_field::{FpExt4, Prime31Offset19, Prime32Offset99}; - -use super::arithmetic::bench_arithmetic_case; -use super::cases::Mersenne31; -use super::params::ArithmeticBenchParams; - -pub(crate) fn bench_ext4_matrix(c: &mut Criterion) { - type F31Mersenne = Mersenne31; - type F31MersenneFpExt4 = FpExt4; - type PF31MersenneFpExt4 = ::Packing; - - type F31 = Prime31Offset19; - type F31FpExt4 = FpExt4; - type PF31FpExt4 = ::Packing; - - type F32 = Prime32Offset99; - type F32FpExt4 = FpExt4; - type PF32FpExt4 = ::Packing; - - let params = ArithmeticBenchParams::from_env("AKITA_BENCH_EXT4_ARITH", 512, 128); - - bench_arithmetic_case::( - c, - "ext4", - "m31_fp_ext4", - 0xe400_3031_00a1, - params, - ); - - bench_arithmetic_case::( - c, - "ext4", - "p31o19_fp_ext4", - 0xe400_3031, - params, - ); - - bench_arithmetic_case::( - c, - "ext4", - "p32o99_fp_ext4", - 0xe400_3032, - params, - ); -} diff --git a/crates/jolt-field/benches/solinas_field_arith/kernel.rs b/crates/jolt-field/benches/solinas_field_arith/kernel.rs deleted file mode 100644 index e5e52f896f..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/kernel.rs +++ /dev/null @@ -1,147 +0,0 @@ -use criterion::{black_box, Criterion, Throughput}; -use jolt_field::packed::PackedField; -use jolt_field::{CanonicalField, FieldCore, Prime128Offset275}; -use rand::{rngs::StdRng, RngCore, SeedableRng}; - -use super::cases::*; -use super::data::rand_u128; - -pub(crate) fn bench_kernel_patterns(c: &mut Criterion) { - bench_packed_sumcheck_mix(c); - bench_fp128_accumulator_pattern(c); -} - -fn bench_packed_sumcheck_mix(c: &mut Criterion) { - let n = 4096u64; - let mut rng = StdRng::seed_from_u64(0x5151_cafe); - - let mut group = c.benchmark_group("field_arith/kernel/packed_macc"); - group.throughput(Throughput::Elements(n)); - - use jolt_field::{Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime64Offset59}; - - sumcheck_bench::(&mut group, PRIME31_OFFSET19, &mut rng, n); - sumcheck_bench::(&mut group, MERSENNE31, &mut rng, n); - sumcheck_bench::(&mut group, PRIME32_OFFSET99, &mut rng, n); - sumcheck_bench::(&mut group, PRIME40_OFFSET195, &mut rng, n); - sumcheck_bench::(&mut group, PRIME64_OFFSET59, &mut rng, n); - sumcheck_bench::(&mut group, PRIME128_OFFSET275, &mut rng, n); - - group.finish(); -} - -fn sumcheck_bench( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - label: &str, - rng: &mut StdRng, - n: u64, -) where - F: FieldCore + 'static, - PF: PackedField + Copy + 'static, -{ - let eq: Vec = (0..n).map(|_| F::random(rng)).collect(); - let poly: Vec = (0..n).map(|_| F::random(rng)).collect(); - let eq_p = PF::pack_slice(&eq); - let poly_p = PF::pack_slice(&poly); - - group.bench_function(format!("{label}_packed_macc"), |b| { - b.iter(|| { - let e = black_box(&eq_p); - let p_v = black_box(&poly_p); - let mut acc = PF::broadcast(F::zero()); - for i in 0..e.len() { - acc = acc + e[i] * p_v[i]; - } - black_box(acc) - }) - }); -} - -fn bench_fp128_accumulator_pattern(c: &mut Criterion) { - type F = Prime128Offset275; - - let mut rng = StdRng::seed_from_u64(0xacc0_1a70_0002); - let inputs_a: Vec = (0..256) - .map(|_| F::from_canonical_u128_reduced(rand_u128(&mut rng))) - .collect(); - let inputs_b_u64: Vec = (0..256).map(|_| rng.next_u64()).collect(); - let inputs_b_f: Vec = (0..256) - .map(|_| F::from_canonical_u128_reduced(rand_u128(&mut rng))) - .collect(); - - let mut group = c.benchmark_group("field_arith/kernel/fp128_accumulator"); - - for &n in &[16, 64, 256] { - group.bench_function(format!("eager_mul_u64_{n}"), |bench| { - bench.iter(|| { - let a_s = black_box(&inputs_a[..n]); - let b_s = black_box(&inputs_b_u64[..n]); - let mut acc = F::zero(); - for i in 0..n { - acc += a_s[i] * F::from_u64(b_s[i]); - } - black_box(acc) - }) - }); - - group.bench_function(format!("widening_accum_u64_{n}"), |bench| { - bench.iter(|| { - let a_s = black_box(&inputs_a[..n]); - let b_s = black_box(&inputs_b_u64[..n]); - let mut acc = [0u64; 5]; - for i in 0..n { - let wide = a_s[i].mul_wide_u64(b_s[i]); - let mut carry: u64 = 0; - for j in 0..3 { - let sum = acc[j] as u128 + wide[j] as u128 + carry as u128; - acc[j] = sum as u64; - carry = (sum >> 64) as u64; - } - for item in &mut acc[3..5] { - let sum = *item as u128 + carry as u128; - *item = sum as u64; - carry = (sum >> 64) as u64; - } - } - black_box(F::solinas_reduce(&acc)) - }) - }); - - group.bench_function(format!("eager_mul_full_{n}"), |bench| { - bench.iter(|| { - let a_s = black_box(&inputs_a[..n]); - let b_s = black_box(&inputs_b_f[..n]); - let mut acc = F::zero(); - for i in 0..n { - acc += a_s[i] * b_s[i]; - } - black_box(acc) - }) - }); - - group.bench_function(format!("widening_accum_full_{n}"), |bench| { - bench.iter(|| { - let a_s = black_box(&inputs_a[..n]); - let b_s = black_box(&inputs_b_f[..n]); - let mut acc = [0u64; 6]; - for i in 0..n { - let wide = a_s[i].mul_wide(b_s[i]); - let mut carry: u64 = 0; - for j in 0..4 { - let sum = acc[j] as u128 + wide[j] as u128 + carry as u128; - acc[j] = sum as u64; - carry = (sum >> 64) as u64; - } - for item in &mut acc[4..6] { - let sum = *item as u128 + carry as u128; - *item = sum as u64; - carry = (sum >> 64) as u64; - } - } - black_box(F::solinas_reduce(&acc)) - }) - }); - } - - group.finish(); -} diff --git a/crates/jolt-field/benches/solinas_field_arith/mod.rs b/crates/jolt-field/benches/solinas_field_arith/mod.rs deleted file mode 100644 index 0a36950d56..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub(crate) mod arithmetic; -pub(crate) mod base; -pub(crate) mod cases; -pub(crate) mod comparison; -pub(crate) mod data; -pub(crate) mod ext2; -pub(crate) mod ext4; -pub(crate) mod kernel; -pub(crate) mod parallel; -pub(crate) mod params; -pub(crate) mod plonky3; -pub(crate) mod wide; - -pub(crate) use base::bench_base_field_matrix; -pub(crate) use comparison::bench_comparisons; -pub(crate) use ext2::bench_ext2_matrix; -pub(crate) use ext4::bench_ext4_matrix; -pub(crate) use kernel::bench_kernel_patterns; -pub(crate) use parallel::bench_parallel_throughput; -pub(crate) use plonky3::{bench_p3_base_matrix, bench_p3_ext4_matrix, bench_p3_ext5_matrix}; -pub(crate) use wide::bench_wide_ops; diff --git a/crates/jolt-field/benches/solinas_field_arith/parallel.rs b/crates/jolt-field/benches/solinas_field_arith/parallel.rs deleted file mode 100644 index fe023b0ac2..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/parallel.rs +++ /dev/null @@ -1,233 +0,0 @@ -#[cfg(feature = "parallel")] -use std::env; -#[cfg(feature = "parallel")] -use std::thread; - -#[cfg(feature = "parallel")] -use criterion::{black_box, Criterion, Throughput}; -#[cfg(feature = "parallel")] -use jolt_field::packed::PackedField; -#[cfg(feature = "parallel")] -use jolt_field::{CanonicalField, FieldCore, Prime128Offset275, Prime31Offset19, Prime64Offset59}; -#[cfg(feature = "parallel")] -use rand::{rngs::StdRng, SeedableRng}; -#[cfg(feature = "parallel")] -use rayon::prelude::*; -#[cfg(feature = "parallel")] -use rayon::ThreadPoolBuilder; - -#[cfg(feature = "parallel")] -use super::cases::*; -#[cfg(feature = "parallel")] -use super::data::rand_u128; -#[cfg(feature = "parallel")] -use super::params::env_usize; - -#[cfg(feature = "parallel")] -pub(crate) fn bench_parallel_throughput(c: &mut Criterion) { - let profile = env::var("AKITA_BENCH_PAR_PROFILE").unwrap_or_else(|_| "dev".to_string()); - let default_n = match profile.as_str() { - "scale" | "large" => 1 << 20, - "xlarge" => 1 << 22, - _ => 1 << 15, - }; - let n = env_usize("AKITA_BENCH_PAR_N", default_n); - let default_chunk = match profile.as_str() { - "scale" | "large" => 1 << 14, - "xlarge" => 1 << 15, - _ => 1 << 12, - }; - let chunk = env_usize("AKITA_BENCH_PAR_CHUNK", default_chunk); - let threads = env_usize( - "AKITA_BENCH_PAR_THREADS", - thread::available_parallelism() - .map(|v| v.get()) - .unwrap_or(1), - ); - - assert!(threads > 0, "AKITA_BENCH_PAR_THREADS must be > 0"); - assert!(n > 0, "AKITA_BENCH_PAR_N must be > 0"); - assert!(chunk > 0, "AKITA_BENCH_PAR_CHUNK must be > 0"); - - let pool = ThreadPoolBuilder::new() - .num_threads(threads) - .build() - .expect("build benchmark rayon pool"); - - let mut rng = StdRng::seed_from_u64(0x7061_7261_0001); - let lhs31: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - let rhs31: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - let lhs64: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - let rhs64: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - let lhs128: Vec = (0..n) - .map(|_| Prime128Offset275::from_canonical_u128_reduced(rand_u128(&mut rng))) - .collect(); - let rhs128: Vec = (0..n) - .map(|_| Prime128Offset275::from_canonical_u128_reduced(rand_u128(&mut rng))) - .collect(); - - let lhs31_p = P31O19::pack_slice(&lhs31); - let rhs31_p = P31O19::pack_slice(&rhs31); - let lhs64_p = P64O59::pack_slice(&lhs64); - let rhs64_p = P64O59::pack_slice(&rhs64); - let lhs128_p = P128O275::pack_slice(&lhs128); - let rhs128_p = P128O275::pack_slice(&rhs128); - - let mut out31 = vec![Prime31Offset19::zero(); n]; - let mut out64 = vec![Prime64Offset59::zero(); n]; - let mut out128 = vec![F128::zero(); n]; - let mut out31_p = vec![P31O19::broadcast(Prime31Offset19::zero()); lhs31_p.len()]; - let mut out64_p = vec![P64O59::broadcast(Prime64Offset59::zero()); lhs64_p.len()]; - let mut out128_p = vec![P128O275::broadcast(F128::zero()); lhs128_p.len()]; - - let mut group = c.benchmark_group(format!( - "field_arith/parallel/{profile}/n{n}/chunk{chunk}/threads{threads}" - )); - group.throughput(Throughput::Elements(n as u64)); - - bench_scalar_parallel( - &mut group, - &pool, - PRIME31_OFFSET19, - &lhs31, - &rhs31, - &mut out31, - chunk, - ); - bench_scalar_parallel( - &mut group, - &pool, - PRIME64_OFFSET59, - &lhs64, - &rhs64, - &mut out64, - chunk, - ); - bench_scalar_parallel( - &mut group, - &pool, - PRIME128_OFFSET275, - &lhs128, - &rhs128, - &mut out128, - chunk, - ); - bench_packed_parallel( - &mut group, - &pool, - PRIME31_OFFSET19, - &lhs31_p, - &rhs31_p, - &mut out31_p, - (chunk / P31O19::WIDTH).max(1), - ); - bench_packed_parallel( - &mut group, - &pool, - PRIME64_OFFSET59, - &lhs64_p, - &rhs64_p, - &mut out64_p, - (chunk / P64O59::WIDTH).max(1), - ); - bench_packed_parallel( - &mut group, - &pool, - PRIME128_OFFSET275, - &lhs128_p, - &rhs128_p, - &mut out128_p, - (chunk / P128O275::WIDTH).max(1), - ); - - group.finish(); -} - -#[cfg(feature = "parallel")] -fn bench_scalar_parallel( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - pool: &rayon::ThreadPool, - label: &str, - lhs: &[F], - rhs: &[F], - out: &mut [F], - chunk: usize, -) where - F: jolt_field::FieldCore + Send + Sync, -{ - group.bench_function(format!("{label}_mul_seq"), |b| { - b.iter(|| { - let a = black_box(lhs); - let b_v = black_box(rhs); - for i in 0..out.len() { - out[i] = a[i] * b_v[i]; - } - black_box(out[0]) - }) - }); - - group.bench_function(format!("{label}_mul_par_chunked"), |b| { - b.iter(|| { - let a = black_box(lhs); - let b_v = black_box(rhs); - pool.install(|| { - out.par_chunks_mut(chunk) - .enumerate() - .for_each(|(chunk_idx, out_chunk)| { - let start = chunk_idx * chunk; - for (j, dst) in out_chunk.iter_mut().enumerate() { - let idx = start + j; - *dst = a[idx] * b_v[idx]; - } - }); - }); - black_box(out[0]) - }) - }); -} - -#[cfg(feature = "parallel")] -fn bench_packed_parallel( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - pool: &rayon::ThreadPool, - label: &str, - lhs: &[PF], - rhs: &[PF], - out: &mut [PF], - chunk: usize, -) where - PF: PackedField + Copy + Send + Sync, -{ - group.bench_function(format!("{label}_packed_mul_seq"), |b| { - b.iter(|| { - let a = black_box(lhs); - let b_v = black_box(rhs); - for i in 0..out.len() { - out[i] = a[i] * b_v[i]; - } - black_box(out[0].extract(0)) - }) - }); - - group.bench_function(format!("{label}_packed_mul_par_chunked"), |b| { - b.iter(|| { - let a = black_box(lhs); - let b_v = black_box(rhs); - pool.install(|| { - out.par_chunks_mut(chunk) - .enumerate() - .for_each(|(chunk_idx, out_chunk)| { - let start = chunk_idx * chunk; - for (j, dst) in out_chunk.iter_mut().enumerate() { - let idx = start + j; - *dst = a[idx] * b_v[idx]; - } - }); - }); - black_box(out[0].extract(0)) - }) - }); -} - -#[cfg(not(feature = "parallel"))] -pub(crate) fn bench_parallel_throughput(_: &mut criterion::Criterion) {} diff --git a/crates/jolt-field/benches/solinas_field_arith/params.rs b/crates/jolt-field/benches/solinas_field_arith/params.rs deleted file mode 100644 index 9b73f733c1..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/params.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::env; - -#[derive(Clone, Copy)] -pub(crate) struct ArithmeticBenchParams { - pub(crate) latency_iters: usize, - pub(crate) inverse_latency_iters: usize, - pub(crate) throughput_iters: usize, - pub(crate) inverse_throughput_iters: usize, - pub(crate) streams: usize, -} - -impl ArithmeticBenchParams { - pub(crate) fn from_env( - prefix: &str, - latency_default: usize, - throughput_default: usize, - ) -> Self { - let latency_iters = env_usize(&format!("{prefix}_LATENCY_ITERS"), latency_default); - let inverse_latency_iters = - env_usize(&format!("{prefix}_INVERSE_LATENCY_ITERS"), 128).min(latency_iters); - let throughput_iters = env_usize(&format!("{prefix}_THROUGHPUT_ITERS"), throughput_default); - let inverse_throughput_iters = env_usize(&format!("{prefix}_INVERSE_THROUGHPUT_ITERS"), 32); - let streams = env_usize(&format!("{prefix}_STREAMS"), 8); - - assert!(latency_iters > 0, "{prefix}_LATENCY_ITERS must be > 0"); - assert!( - inverse_latency_iters > 0, - "{prefix}_INVERSE_LATENCY_ITERS must be > 0" - ); - assert!( - throughput_iters > 0, - "{prefix}_THROUGHPUT_ITERS must be > 0" - ); - assert!( - inverse_throughput_iters > 0, - "{prefix}_INVERSE_THROUGHPUT_ITERS must be > 0" - ); - assert!(streams > 0, "{prefix}_STREAMS must be > 0"); - - Self { - latency_iters, - inverse_latency_iters, - throughput_iters, - inverse_throughput_iters, - streams, - } - } -} - -pub(crate) fn env_usize(name: &str, default: usize) -> usize { - env::var(name) - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(default) -} diff --git a/crates/jolt-field/benches/solinas_field_arith/plonky3.rs b/crates/jolt-field/benches/solinas_field_arith/plonky3.rs deleted file mode 100644 index 73ada8a18b..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/plonky3.rs +++ /dev/null @@ -1,957 +0,0 @@ -use std::time::Instant; - -use criterion::{black_box, Criterion, Throughput}; -use p3_baby_bear::BabyBear; -use p3_field::extension::{BinomialExtensionField, QuinticTrinomialExtensionField}; -use p3_field::{ - BasedVectorSpace, ExtensionField, Field, PackedField, PackedFieldExtension, PackedValue, - PrimeCharacteristicRing, -}; -use p3_koala_bear::KoalaBear; -use p3_mersenne_31::Mersenne31; -use rand::{rngs::StdRng, RngCore, SeedableRng}; - -use super::data::duration_per_logical_op; -use super::params::ArithmeticBenchParams; - -fn sample_base(rng: &mut StdRng) -> F { - F::from_u64(rng.next_u64()) -} - -fn sample_ext + BasedVectorSpace>( - rng: &mut StdRng, -) -> EF { - EF::from_basis_coefficients_fn(|_| sample_base::(rng)) -} - -pub(crate) fn bench_p3_base_case( - c: &mut Criterion, - family: &str, - label: &str, - seed: u64, - params: ArithmeticBenchParams, -) where - F: Field + Copy, - F::Packing: PackedField + Copy, -{ - let mut rng = StdRng::seed_from_u64(seed); - let scalar_latency_inputs: Vec = (0..params.latency_iters) - .map(|_| sample_base(&mut rng)) - .collect(); - let packed_latency_inputs: Vec = (0..params.latency_iters) - .map(|_| F::Packing::from_fn(|_| sample_base(&mut rng))) - .collect(); - let scalar_stream_lanes: Vec<(F, F)> = (0..params.streams) - .map(|_| (sample_base(&mut rng), sample_base(&mut rng))) - .collect(); - let packed_stream_lanes: Vec<(F::Packing, F::Packing)> = (0..params.streams) - .map(|_| { - ( - F::Packing::from_fn(|_| sample_base(&mut rng)), - F::Packing::from_fn(|_| sample_base(&mut rng)), - ) - }) - .collect(); - - let width = ::WIDTH; - - let mut latency_group = c.benchmark_group(format!( - "field_arith/{family}/latency_chain/{label}_w{width}" - )); - - p3_bench_scalar_suite_latency(&mut latency_group, params, &scalar_latency_inputs); - - let packed_zero = F::Packing::broadcast(F::ZERO); - let packed_one = F::Packing::broadcast(F::ONE); - - p3_bench_packed_latency( - &mut latency_group, - width, - "add", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc + x, - packed_zero, - ); - p3_bench_packed_latency( - &mut latency_group, - width, - "sub", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc - x, - packed_zero, - ); - p3_bench_packed_unary_latency( - &mut latency_group, - width, - "neg", - params.latency_iters, - &packed_latency_inputs, - |acc| packed_zero - acc, - ); - p3_bench_packed_unary_latency( - &mut latency_group, - width, - "double", - params.latency_iters, - &packed_latency_inputs, - |acc| acc + acc, - ); - p3_bench_packed_latency( - &mut latency_group, - width, - "add_neg", - params.latency_iters, - &packed_latency_inputs, - |acc, x| packed_zero - (acc + x), - packed_zero, - ); - p3_bench_packed_latency( - &mut latency_group, - width, - "double_add", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc + acc + x, - packed_zero, - ); - p3_bench_packed_latency( - &mut latency_group, - width, - "mul", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc * x, - packed_one, - ); - p3_bench_packed_latency( - &mut latency_group, - width, - "mul_add", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc * x + acc, - packed_one, - ); - p3_bench_packed_unary_latency( - &mut latency_group, - width, - "square", - params.latency_iters, - &packed_latency_inputs, - |acc| acc.square(), - ); - p3_bench_packed_unary_latency( - &mut latency_group, - width, - "mul_self", - params.latency_iters, - &packed_latency_inputs, - |acc| acc * acc, - ); - - latency_group.throughput(Throughput::Elements(1)); - latency_group.bench_function( - format!( - "packed_inverse_chain/{}x{width}_ns_lane", - params.inverse_latency_iters - ), - |b| { - b.iter_custom(|iters| { - let inputs = black_box(&packed_latency_inputs[..params.inverse_latency_iters]); - let mut acc = packed_one; - let start = Instant::now(); - for _ in 0..iters { - for x in inputs { - acc = F::Packing::from_fn(|lane| { - (PackedValue::extract(&acc, lane) + PackedValue::extract(x, lane)) - .inverse() - }); - } - } - black_box(PackedValue::extract(&acc, 0)); - duration_per_logical_op( - start.elapsed(), - (params.inverse_latency_iters * width) as u64, - ) - }) - }, - ); - - latency_group.finish(); - - let mut throughput_group = c.benchmark_group(format!( - "field_arith/{family}/throughput_stream/{label}_w{width}" - )); - - p3_bench_scalar_suite_throughput(&mut throughput_group, params, &scalar_stream_lanes); - - p3_bench_packed_throughput( - &mut throughput_group, - width, - "add", - params, - &packed_stream_lanes, - |acc, x| acc + x, - |a, b| a + b, - ); - p3_bench_packed_throughput( - &mut throughput_group, - width, - "sub", - params, - &packed_stream_lanes, - |acc, x| acc - x, - |a, b| a - b, - ); - p3_bench_packed_throughput( - &mut throughput_group, - width, - "mul", - params, - &packed_stream_lanes, - |acc, x| acc * x, - |a, b| a * b, - ); - p3_bench_packed_throughput( - &mut throughput_group, - width, - "square", - params, - &packed_stream_lanes, - |acc, _| acc.square(), - |a, _| a.square(), - ); - - throughput_group.throughput(Throughput::Elements(1)); - throughput_group.bench_function( - format!( - "packed_inverse_stream/{}x{width}x{}_ns_lane", - params.streams, params.inverse_throughput_iters - ), - |b| { - b.iter_custom(|iters| { - let lanes = black_box(&packed_stream_lanes); - let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.inverse_throughput_iters { - for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { - let next = F::Packing::from_fn(|i| { - (PackedValue::extract(acc_i, i) + PackedValue::extract(&lane.0, i)) - .inverse() - }); - *acc_i = next; - } - } - } - black_box(PackedValue::extract(&acc[0], 0)); - duration_per_logical_op( - start.elapsed(), - (params.streams * width * params.inverse_throughput_iters) as u64, - ) - }) - }, - ); - - throughput_group.finish(); -} - -pub(crate) fn bench_p3_ext_case( - c: &mut Criterion, - family: &str, - label: &str, - seed: u64, - params: ArithmeticBenchParams, -) where - Base: Field + Copy, - Base::Packing: PackedField + Copy, - EF: ExtensionField + BasedVectorSpace + Copy, - EF::ExtensionPacking: PackedFieldExtension + Copy, -{ - let width = ::WIDTH; - - let mut rng = StdRng::seed_from_u64(seed); - let scalar_latency_inputs: Vec = (0..params.latency_iters) - .map(|_| sample_ext::(&mut rng)) - .collect(); - let packed_latency_inputs: Vec = (0..params.latency_iters) - .map(|_| { - let ext_vals: Vec = (0..width) - .map(|_| sample_ext::(&mut rng)) - .collect(); - EF::ExtensionPacking::from_ext_slice(&ext_vals) - }) - .collect(); - let scalar_stream_lanes: Vec<(EF, EF)> = (0..params.streams) - .map(|_| (sample_ext(&mut rng), sample_ext(&mut rng))) - .collect(); - let packed_stream_lanes: Vec<(EF::ExtensionPacking, EF::ExtensionPacking)> = (0..params - .streams) - .map(|_| { - let a: Vec = (0..width) - .map(|_| sample_ext::(&mut rng)) - .collect(); - let b: Vec = (0..width) - .map(|_| sample_ext::(&mut rng)) - .collect(); - ( - EF::ExtensionPacking::from_ext_slice(&a), - EF::ExtensionPacking::from_ext_slice(&b), - ) - }) - .collect(); - - let mut latency_group = c.benchmark_group(format!( - "field_arith/{family}/latency_chain/{label}_w{width}" - )); - - p3_bench_scalar_suite_latency(&mut latency_group, params, &scalar_latency_inputs); - - let packed_zero = broadcast_ext::(EF::ZERO, width); - let packed_one = broadcast_ext::(EF::ONE, width); - - p3_bench_packed_ext_latency( - &mut latency_group, - width, - "add", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc + x, - packed_zero, - ); - p3_bench_packed_ext_latency( - &mut latency_group, - width, - "sub", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc - x, - packed_zero, - ); - p3_bench_packed_ext_unary_latency( - &mut latency_group, - width, - "neg", - params.latency_iters, - &packed_latency_inputs, - |acc| packed_zero - acc, - ); - p3_bench_packed_ext_unary_latency( - &mut latency_group, - width, - "double", - params.latency_iters, - &packed_latency_inputs, - |acc| acc + acc, - ); - p3_bench_packed_ext_latency( - &mut latency_group, - width, - "add_neg", - params.latency_iters, - &packed_latency_inputs, - |acc, x| packed_zero - (acc + x), - packed_zero, - ); - p3_bench_packed_ext_latency( - &mut latency_group, - width, - "double_add", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc + acc + x, - packed_zero, - ); - p3_bench_packed_ext_latency( - &mut latency_group, - width, - "mul", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc * x, - packed_one, - ); - p3_bench_packed_ext_latency( - &mut latency_group, - width, - "mul_add", - params.latency_iters, - &packed_latency_inputs, - |acc, x| acc * x + acc, - packed_one, - ); - p3_bench_packed_ext_unary_latency( - &mut latency_group, - width, - "square", - params.latency_iters, - &packed_latency_inputs, - |acc| acc.square(), - ); - p3_bench_packed_ext_unary_latency( - &mut latency_group, - width, - "mul_self", - params.latency_iters, - &packed_latency_inputs, - |acc| acc * acc, - ); - - latency_group.finish(); - - let mut throughput_group = c.benchmark_group(format!( - "field_arith/{family}/throughput_stream/{label}_w{width}" - )); - - p3_bench_scalar_suite_throughput(&mut throughput_group, params, &scalar_stream_lanes); - - p3_bench_packed_ext_throughput( - &mut throughput_group, - width, - "add", - params, - &packed_stream_lanes, - |acc, x| acc + x, - |a, b| a + b, - ); - p3_bench_packed_ext_throughput( - &mut throughput_group, - width, - "sub", - params, - &packed_stream_lanes, - |acc, x| acc - x, - |a, b| a - b, - ); - p3_bench_packed_ext_throughput( - &mut throughput_group, - width, - "mul", - params, - &packed_stream_lanes, - |acc, x| acc * x, - |a, b| a * b, - ); - p3_bench_packed_ext_throughput( - &mut throughput_group, - width, - "square", - params, - &packed_stream_lanes, - |acc, _| acc.square(), - |a, _| a.square(), - ); - - throughput_group.finish(); -} - -fn broadcast_ext + BasedVectorSpace>( - value: EF, - width: usize, -) -> EF::ExtensionPacking -where - EF::ExtensionPacking: PackedFieldExtension, -{ - EF::ExtensionPacking::from_ext_slice(&(0..width).map(|_| value).collect::>()) -} - -pub(crate) fn bench_p3_base_matrix(c: &mut Criterion) { - let params = ArithmeticBenchParams::from_env("AKITA_BENCH_BASE_ARITH", 2048, 256); - - bench_p3_base_case::(c, "base", "p3_mersenne31", 0xba5e_3131_0003, params); - bench_p3_base_case::(c, "base", "p3_baby_bear", 0xba5e_babe_0003, params); - bench_p3_base_case::(c, "base", "p3_koala_bear", 0xba5e_c0a1_a003, params); -} - -pub(crate) fn bench_p3_ext4_matrix(c: &mut Criterion) { - let params = ArithmeticBenchParams::from_env("AKITA_BENCH_EXT4_ARITH", 512, 128); - - bench_p3_ext_case::>( - c, - "ext4", - "p3_baby_bear_ext4", - 0xe400_babe_0004, - params, - ); - bench_p3_ext_case::>( - c, - "ext4", - "p3_koala_bear_ext4", - 0xe400_c0a1_a004, - params, - ); -} - -pub(crate) fn bench_p3_ext5_matrix(c: &mut Criterion) { - let params = ArithmeticBenchParams::from_env("AKITA_BENCH_EXT5_ARITH", 512, 128); - - bench_p3_ext_case::>( - c, - "ext5", - "p3_baby_bear_ext5", - 0xe500_babe_0005, - params, - ); - bench_p3_ext_case::>( - c, - "ext5", - "p3_koala_bear_ext5", - 0xe500_c0a1_a005, - params, - ); -} - -/// Full scalar latency-chain op set, shared by the base and extension matrices -/// (both operate on a `Field`, only the concrete type differs). -fn p3_bench_scalar_suite_latency( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - params: ArithmeticBenchParams, - inputs: &[S], -) { - p3_bench_scalar_latency( - group, - "add", - params.latency_iters, - inputs, - |acc, x| acc + x, - S::ZERO, - ); - p3_bench_scalar_latency( - group, - "sub", - params.latency_iters, - inputs, - |acc, x| acc - x, - S::ZERO, - ); - p3_bench_scalar_unary_latency(group, "neg", params.latency_iters, inputs, |acc| -acc); - p3_bench_scalar_unary_latency(group, "double", params.latency_iters, inputs, |acc| { - acc.double() - }); - p3_bench_scalar_latency( - group, - "add_neg", - params.latency_iters, - inputs, - |acc, x| -(acc + x), - S::ZERO, - ); - p3_bench_scalar_latency( - group, - "double_add", - params.latency_iters, - inputs, - |acc, x| acc + acc + x, - S::ZERO, - ); - p3_bench_scalar_latency( - group, - "mul", - params.latency_iters, - inputs, - |acc, x| acc * x, - S::ONE, - ); - p3_bench_scalar_latency( - group, - "mul_add", - params.latency_iters, - inputs, - |acc, x| acc * x + acc, - S::ONE, - ); - - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!("scalar_square_chain/{}_ns_per_op", params.latency_iters), - |b| { - b.iter_custom(|iters| { - let mut acc = black_box(inputs[0]); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.latency_iters { - acc = acc.square(); - } - } - black_box(acc); - duration_per_logical_op(start.elapsed(), params.latency_iters as u64) - }) - }, - ); - - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!("scalar_mul_self_chain/{}_ns_per_op", params.latency_iters), - |b| { - b.iter_custom(|iters| { - let mut acc = black_box(inputs[0]); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.latency_iters { - acc = acc * acc; - } - } - black_box(acc); - duration_per_logical_op(start.elapsed(), params.latency_iters as u64) - }) - }, - ); - - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!( - "scalar_inverse_chain/{}_ns_per_op", - params.inverse_latency_iters - ), - |b| { - b.iter_custom(|iters| { - let inputs = black_box(&inputs[..params.inverse_latency_iters]); - let mut acc = S::ONE; - let start = Instant::now(); - for _ in 0..iters { - for x in inputs { - acc = (acc + *x).inverse(); - } - } - black_box(acc); - duration_per_logical_op(start.elapsed(), params.inverse_latency_iters as u64) - }) - }, - ); -} - -/// Full scalar throughput-stream op set, shared by the base and extension matrices. -fn p3_bench_scalar_suite_throughput( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - params: ArithmeticBenchParams, - lanes: &[(S, S)], -) { - p3_bench_scalar_throughput(group, "add", params, lanes, |acc, x| acc + x, |a, b| a + b); - p3_bench_scalar_throughput(group, "sub", params, lanes, |acc, x| acc - x, |a, b| a - b); - p3_bench_scalar_throughput(group, "mul", params, lanes, |acc, x| acc * x, |a, b| a * b); - p3_bench_scalar_throughput( - group, - "square", - params, - lanes, - |acc, _| acc.square(), - |a, _| a.square(), - ); - - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!( - "scalar_inverse_stream/{}x{}_ns_per_op", - params.streams, params.inverse_throughput_iters - ), - |b| { - b.iter_custom(|iters| { - let lanes = black_box(lanes); - let mut acc: Vec = lanes.iter().map(|(a, _)| *a).collect(); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.inverse_throughput_iters { - for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { - *acc_i = (*acc_i + lane.0).inverse(); - } - } - } - black_box(acc[0]); - duration_per_logical_op( - start.elapsed(), - (params.streams * params.inverse_throughput_iters) as u64, - ) - }) - }, - ); -} - -fn p3_bench_scalar_latency( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - op: &str, - latency_iters: usize, - inputs: &[F], - step: impl Fn(F, F) -> F, - init: F, -) { - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!("scalar_{op}_chain/{latency_iters}_ns_per_op"), - |b| { - b.iter_custom(|iters| { - let inputs = black_box(inputs); - let mut acc = init; - let start = Instant::now(); - for _ in 0..iters { - for x in inputs { - acc = step(acc, *x); - } - } - black_box(acc); - duration_per_logical_op(start.elapsed(), latency_iters as u64) - }) - }, - ); -} - -fn p3_bench_scalar_unary_latency( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - op: &str, - latency_iters: usize, - inputs: &[F], - step: impl Fn(F) -> F, -) { - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!("scalar_{op}_chain/{latency_iters}_ns_per_op"), - |b| { - b.iter_custom(|iters| { - let mut acc = black_box(inputs[0]); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..latency_iters { - acc = step(acc); - } - } - black_box(acc); - duration_per_logical_op(start.elapsed(), latency_iters as u64) - }) - }, - ); -} - -fn p3_bench_packed_latency( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - width: usize, - op: &str, - latency_iters: usize, - inputs: &[PF], - step: impl Fn(PF, PF) -> PF, - init: PF, -) { - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!("packed_{op}_chain/{latency_iters}x{width}_ns_lane"), - |b| { - b.iter_custom(|iters| { - let inputs = black_box(inputs); - let mut acc = init; - let start = Instant::now(); - for _ in 0..iters { - for x in inputs { - acc = step(acc, *x); - } - } - black_box(::extract(&acc, 0)); - duration_per_logical_op(start.elapsed(), (latency_iters * width) as u64) - }) - }, - ); -} - -fn p3_bench_packed_unary_latency( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - width: usize, - op: &str, - latency_iters: usize, - inputs: &[PF], - step: impl Fn(PF) -> PF, -) { - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!("packed_{op}_chain/{latency_iters}x{width}_ns_lane"), - |b| { - b.iter_custom(|iters| { - let mut acc = black_box(inputs[0]); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..latency_iters { - acc = step(acc); - } - } - black_box(::extract(&acc, 0)); - duration_per_logical_op(start.elapsed(), (latency_iters * width) as u64) - }) - }, - ); -} - -fn p3_bench_scalar_throughput( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - op: &str, - params: ArithmeticBenchParams, - lanes: &[(F, F)], - step: impl Fn(F, F) -> F, - init: impl Fn(F, F) -> F, -) { - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!( - "scalar_{op}_stream/{}x{}_ns_per_op", - params.streams, params.throughput_iters - ), - |b| { - b.iter_custom(|iters| { - let lanes = black_box(lanes); - let mut acc: Vec = lanes.iter().map(|(a, b)| init(*a, *b)).collect(); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.throughput_iters { - for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { - *acc_i = step(*acc_i, lane.0); - } - } - } - black_box(acc[0]); - duration_per_logical_op( - start.elapsed(), - (params.streams * params.throughput_iters) as u64, - ) - }) - }, - ); -} - -fn p3_bench_packed_throughput( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - width: usize, - op: &str, - params: ArithmeticBenchParams, - lanes: &[(PF, PF)], - step: impl Fn(PF, PF) -> PF, - init: impl Fn(PF, PF) -> PF, -) { - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!( - "packed_{op}_stream/{}x{width}x{}_ns_lane", - params.streams, params.throughput_iters - ), - |b| { - b.iter_custom(|iters| { - let lanes = black_box(lanes); - let mut acc: Vec = lanes.iter().map(|(a, b)| init(*a, *b)).collect(); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.throughput_iters { - for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { - *acc_i = step(*acc_i, lane.0); - } - } - } - black_box(::extract(&acc[0], 0)); - duration_per_logical_op( - start.elapsed(), - (params.streams * width * params.throughput_iters) as u64, - ) - }) - }, - ); -} - -fn p3_bench_packed_ext_latency( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - width: usize, - op: &str, - latency_iters: usize, - inputs: &[EP], - step: impl Fn(EP, EP) -> EP, - init: EP, -) where - Base: Field, - EF: ExtensionField, - EP: PackedFieldExtension + Copy, -{ - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!("packed_{op}_chain/{latency_iters}x{width}_ns_lane"), - |b| { - b.iter_custom(|iters| { - let inputs = black_box(inputs); - let mut acc = init; - let start = Instant::now(); - for _ in 0..iters { - for x in inputs { - acc = step(acc, *x); - } - } - black_box(PackedFieldExtension::extract(&acc, 0)); - duration_per_logical_op(start.elapsed(), (latency_iters * width) as u64) - }) - }, - ); -} - -fn p3_bench_packed_ext_unary_latency( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - width: usize, - op: &str, - latency_iters: usize, - inputs: &[EP], - step: impl Fn(EP) -> EP, -) where - Base: Field, - EF: ExtensionField, - EP: PackedFieldExtension + Copy, -{ - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!("packed_{op}_chain/{latency_iters}x{width}_ns_lane"), - |b| { - b.iter_custom(|iters| { - let mut acc = black_box(inputs[0]); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..latency_iters { - acc = step(acc); - } - } - black_box(PackedFieldExtension::extract(&acc, 0)); - duration_per_logical_op(start.elapsed(), (latency_iters * width) as u64) - }) - }, - ); -} - -fn p3_bench_packed_ext_throughput( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, - width: usize, - op: &str, - params: ArithmeticBenchParams, - lanes: &[(EP, EP)], - step: impl Fn(EP, EP) -> EP, - init: impl Fn(EP, EP) -> EP, -) where - Base: Field, - EF: ExtensionField, - EP: PackedFieldExtension + Copy, -{ - group.throughput(Throughput::Elements(1)); - group.bench_function( - format!( - "packed_{op}_stream/{}x{width}x{}_ns_lane", - params.streams, params.throughput_iters - ), - |b| { - b.iter_custom(|iters| { - let lanes = black_box(lanes); - let mut acc: Vec = lanes.iter().map(|(a, b)| init(*a, *b)).collect(); - let start = Instant::now(); - for _ in 0..iters { - for _ in 0..params.throughput_iters { - for (acc_i, lane) in acc.iter_mut().zip(lanes.iter()) { - *acc_i = step(*acc_i, lane.0); - } - } - } - black_box(acc[0].extract(0)); - duration_per_logical_op( - start.elapsed(), - (params.streams * width * params.throughput_iters) as u64, - ) - }) - }, - ); -} diff --git a/crates/jolt-field/benches/solinas_field_arith/wide.rs b/crates/jolt-field/benches/solinas_field_arith/wide.rs deleted file mode 100644 index 13805a3cdd..0000000000 --- a/crates/jolt-field/benches/solinas_field_arith/wide.rs +++ /dev/null @@ -1,121 +0,0 @@ -use criterion::{black_box, Criterion}; -use jolt_field::{CanonicalField, Prime128Offset275}; -use rand::{rngs::StdRng, RngCore, SeedableRng}; - -use super::data::rand_u128; - -pub(crate) fn bench_wide_ops(c: &mut Criterion) { - type F = Prime128Offset275; - - let mut rng = StdRng::seed_from_u64(0x01de_be0c_0001); - let a = F::from_canonical_u128_reduced(rand_u128(&mut rng)); - let b = F::from_canonical_u128_reduced(rand_u128(&mut rng)); - let b_u64 = rng.next_u64(); - - let mut group = c.benchmark_group("field_arith/wide/prime128_offset275"); - - group.bench_function("mul_wide_u64_only", |bench| { - bench.iter(|| black_box(black_box(a).mul_wide_u64(black_box(b_u64)))) - }); - - group.bench_function("mul_wide_only", |bench| { - bench.iter(|| black_box(black_box(a).mul_wide(black_box(b)))) - }); - - let limbs3 = [rng.next_u64(), rng.next_u64(), rng.next_u64()]; - let limbs4 = [ - rng.next_u64(), - rng.next_u64(), - rng.next_u64(), - rng.next_u64(), - ]; - - group.bench_function("mul_wide_limbs_3_to_5_only", |bench| { - bench.iter(|| black_box(black_box(a).mul_wide_limbs::<3, 5>(black_box(limbs3)))) - }); - group.bench_function("mul_wide_limbs_3_to_4_only", |bench| { - bench.iter(|| black_box(black_box(a).mul_wide_limbs::<3, 4>(black_box(limbs3)))) - }); - group.bench_function("mul_wide_limbs_4_to_5_only", |bench| { - bench.iter(|| black_box(black_box(a).mul_wide_limbs::<4, 5>(black_box(limbs4)))) - }); - group.bench_function("mul_wide_limbs_4_to_4_only", |bench| { - bench.iter(|| black_box(black_box(a).mul_wide_limbs::<4, 4>(black_box(limbs4)))) - }); - - group.bench_function("full_mul_u64_reduce", |bench| { - bench.iter(|| black_box(black_box(a) * F::from_u64(black_box(b_u64)))) - }); - - group.bench_function("full_mul_reduce", |bench| { - bench.iter(|| black_box(black_box(a) * black_box(b))) - }); - - let wide3 = a.mul_wide_u64(b_u64); - let wide4 = a.mul_wide(b); - let wide5 = { - let mut l = [0u64; 5]; - l[..3].copy_from_slice(&wide3); - l[4] = rng.next_u64() & 0xFF; - l - }; - - group.bench_function("solinas_reduce_3_limbs", |bench| { - bench.iter(|| black_box(F::solinas_reduce(black_box(&wide3)))) - }); - - group.bench_function("solinas_reduce_4_limbs", |bench| { - bench.iter(|| black_box(F::solinas_reduce(black_box(&wide4)))) - }); - - group.bench_function("solinas_reduce_5_limbs", |bench| { - bench.iter(|| black_box(F::solinas_reduce(black_box(&wide5)))) - }); - - group.bench_function("mul_wide_u64_roundtrip", |bench| { - bench.iter(|| { - let x = black_box(a); - let y = black_box(b_u64); - black_box(F::solinas_reduce(&x.mul_wide_u64(y))) - }) - }); - - group.bench_function("mul_wide_roundtrip", |bench| { - bench.iter(|| { - let x = black_box(a); - let y = black_box(b); - black_box(F::solinas_reduce(&x.mul_wide(y))) - }) - }); - - group.bench_function("mul_wide_limbs_3_to_5_roundtrip", |bench| { - bench.iter(|| { - let x = black_box(a); - let m = black_box(limbs3); - black_box(F::solinas_reduce(&x.mul_wide_limbs::<3, 5>(m))) - }) - }); - group.bench_function("mul_wide_limbs_3_to_4_roundtrip", |bench| { - bench.iter(|| { - let x = black_box(a); - let m = black_box(limbs3); - black_box(F::solinas_reduce(&x.mul_wide_limbs::<3, 4>(m))) - }) - }); - group.bench_function("mul_wide_limbs_4_to_5_roundtrip", |bench| { - bench.iter(|| { - let x = black_box(a); - let m = black_box(limbs4); - black_box(F::solinas_reduce(&x.mul_wide_limbs::<4, 5>(m))) - }) - }); - group.bench_function("mul_wide_limbs_4_to_4_roundtrip", |bench| { - bench.iter(|| { - let x = black_box(a); - let m = black_box(limbs4); - black_box(F::solinas_reduce(&x.mul_wide_limbs::<4, 4>(m))) - }) - }); - - group.finish(); -} diff --git a/crates/jolt-field/fuzz/.gitignore b/crates/jolt-field/fuzz/.gitignore deleted file mode 100644 index fe68c971b7..0000000000 --- a/crates/jolt-field/fuzz/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target/ -corpus/ -artifacts/ -coverage/ diff --git a/crates/jolt-field/fuzz/Cargo.lock b/crates/jolt-field/fuzz/Cargo.lock deleted file mode 100644 index 5828fd19d7..0000000000 --- a/crates/jolt-field/fuzz/Cargo.lock +++ /dev/null @@ -1,571 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "ark-bn254" -version = "0.5.0" -source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" -dependencies = [ - "ark-ec", - "ark-ff", - "ark-serialize", - "ark-std", -] - -[[package]] -name = "ark-ec" -version = "0.5.0" -source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" -dependencies = [ - "ahash", - "ark-ff", - "ark-poly", - "ark-serialize", - "ark-std", - "educe", - "fnv", - "hashbrown", - "itertools", - "num-bigint", - "num-integer", - "num-traits", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.5.0" -source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" -dependencies = [ - "ark-ff-asm", - "ark-ff-macros", - "ark-serialize", - "ark-std", - "arrayvec", - "digest", - "educe", - "itertools", - "num-bigint", - "num-traits", - "paste", - "zeroize", -] - -[[package]] -name = "ark-ff-asm" -version = "0.5.0" -source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "ark-ff-macros" -version = "0.5.0" -source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ark-poly" -version = "0.5.0" -source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" -dependencies = [ - "ahash", - "ark-ff", - "ark-serialize", - "ark-std", - "educe", - "fnv", - "hashbrown", -] - -[[package]] -name = "ark-serialize" -version = "0.5.0" -source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" -dependencies = [ - "ark-serialize-derive", - "ark-std", - "arrayvec", - "digest", - "num-bigint", -] - -[[package]] -name = "ark-serialize-derive" -version = "0.5.0" -source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ark-std" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" -dependencies = [ - "num-traits", - "rand", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "cc" -version = "1.2.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "crypto-common", -] - -[[package]] -name = "educe" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "enum-ordinalize" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom", - "libc", -] - -[[package]] -name = "jolt-field" -version = "0.1.0" -dependencies = [ - "ark-bn254", - "ark-ff", - "ark-serialize", - "num-traits", - "rand", - "rand_core", - "serde", - "thiserror", -] - -[[package]] -name = "jolt-field-fuzz" -version = "0.0.0" -dependencies = [ - "jolt-field", - "libfuzzer-sys", - "num-traits", -] - -[[package]] -name = "libc" -version = "0.2.184" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" - -[[package]] -name = "libfuzzer-sys" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" -dependencies = [ - "arbitrary", - "cc", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/crates/jolt-field/fuzz/Cargo.toml b/crates/jolt-field/fuzz/Cargo.toml deleted file mode 100644 index 97af7c75a8..0000000000 --- a/crates/jolt-field/fuzz/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[workspace] - -[package] -name = "jolt-field-fuzz" -version = "0.0.0" -publish = false -edition = "2021" - -[package.metadata] -cargo-fuzz = true - -[dependencies] -libfuzzer-sys = "0.4" -jolt-field = { path = "..", default-features = false, features = ["bn254", "solinas"] } -num-traits = "0.2" - -[[bin]] -name = "from_bytes" -path = "fuzz_targets/from_bytes.rs" -doc = false - -[[bin]] -name = "field_arith" -path = "fuzz_targets/field_arith.rs" -doc = false - -[[bin]] -name = "wide_accumulator_fmadd" -path = "fuzz_targets/wide_accumulator_fmadd.rs" -doc = false - -[[bin]] -name = "wide_accumulator_merge" -path = "fuzz_targets/wide_accumulator_merge.rs" -doc = false - -[[bin]] -name = "solinas_field_arith" -path = "fuzz_targets/solinas_field_arith.rs" -doc = false diff --git a/crates/jolt-field/fuzz/fuzz_targets/field_arith.rs b/crates/jolt-field/fuzz/fuzz_targets/field_arith.rs deleted file mode 100644 index f7635c1dde..0000000000 --- a/crates/jolt-field/fuzz/fuzz_targets/field_arith.rs +++ /dev/null @@ -1,34 +0,0 @@ -#![no_main] -use jolt_field::{Fr, FromPrimitiveInt, FieldCore, CanonicalRepr}; -use libfuzzer_sys::fuzz_target; -use num_traits::Zero; - -fuzz_target!(|data: &[u8]| { - if data.len() < 64 { - return; - } - let a = ::from_le_bytes_mod_order(&data[..32]); - let b = ::from_le_bytes_mod_order(&data[32..64]); - - // Arithmetic operations must not panic - let sum = a + b; - let diff = a - b; - let prod = a * b; - let sq = a * a; - - // (a + b) - b == a - assert_eq!(sum - b, a); - // (a - b) + b == a - assert_eq!(diff + b, a); - // a * 0 == 0 - assert!((a * Fr::zero()).is_zero()); - - // inverse must not panic - if !a.is_zero() { - let inv = a.inverse().expect("nonzero element must have inverse"); - assert_eq!(a * inv, Fr::from_u64(1)); - } - - // Prevent optimizing away - let _ = (prod, sq); -}); diff --git a/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs b/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs deleted file mode 100644 index 436af75a5f..0000000000 --- a/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs +++ /dev/null @@ -1,14 +0,0 @@ -#![no_main] -use jolt_field::{CanonicalBytes, CanonicalRepr, Fr}; -use libfuzzer_sys::fuzz_target; - -fuzz_target!(|data: &[u8]| { - // from_bytes should never panic on arbitrary input - let a = ::from_le_bytes_mod_order(data); - - // Round-trip: from_bytes → to_bytes → from_bytes must be stable - let bytes = a.to_bytes_le_vec(); - let b = ::from_le_bytes_mod_order(&bytes); - let bytes2 = b.to_bytes_le_vec(); - assert_eq!(bytes, bytes2, "from_bytes round-trip is not stable"); -}); diff --git a/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs b/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs deleted file mode 100644 index 7a581819b3..0000000000 --- a/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs +++ /dev/null @@ -1,40 +0,0 @@ -#![no_main] - -use jolt_field::{ - FpExt4, FromPrimitiveInt, FieldCore, Prime128Offset275, Prime31Offset19, CanonicalRepr, -}; -use libfuzzer_sys::fuzz_target; -use num_traits::Zero; - -fuzz_target!(|data: &[u8]| { - if data.len() < 64 { - return; - } - - let a31 = Prime31Offset19::from_le_bytes_mod_order(&data[..16]); - let b31 = Prime31Offset19::from_le_bytes_mod_order(&data[16..32]); - assert_eq!((a31 + b31) - b31, a31); - assert_eq!((a31 - b31) + b31, a31); - if !a31.is_zero() { - assert_eq!(a31 * a31.inverse().unwrap(), Prime31Offset19::from_u64(1)); - } - - let a128 = Prime128Offset275::from_le_bytes_mod_order(&data[..32]); - let b128 = Prime128Offset275::from_le_bytes_mod_order(&data[32..64]); - assert_eq!((a128 + b128) - b128, a128); - assert_eq!((a128 - b128) + b128, a128); - if !a128.is_zero() { - assert_eq!( - a128 * a128.inverse().unwrap(), - Prime128Offset275::from_u64(1) - ); - } - - let extension = FpExt4::new([a31, b31, a31 + b31, a31 - b31]); - if !extension.is_zero() { - assert_eq!( - extension * extension.inverse().unwrap(), - FpExt4::::from_u64(1) - ); - } -}); diff --git a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs deleted file mode 100644 index 904710862c..0000000000 --- a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs +++ /dev/null @@ -1,32 +0,0 @@ -#![no_main] -use jolt_field::{Accumulator, Fr, CanonicalRepr, WideAccumulator}; -use libfuzzer_sys::fuzz_target; -use num_traits::Zero; - -fuzz_target!(|data: &[u8]| { - // Each pair of field elements needs 64 bytes (2 x 32-byte chunks). - // Silently skip inputs that don't contain at least one complete pair. - if data.len() < 64 { - return; - } - - let mut acc = WideAccumulator::default(); - let mut naive_sum = Fr::zero(); - - let pairs = data.len() / 64; - for i in 0..pairs { - let offset = i * 64; - let a = ::from_le_bytes_mod_order(&data[offset..offset + 32]); - let b = - ::from_le_bytes_mod_order(&data[offset + 32..offset + 64]); - - acc.fmadd(a, b); - naive_sum += a * b; - } - - assert_eq!( - acc.reduce(), - naive_sum, - "WideAccumulator diverged from naive field arithmetic after {pairs} fmadd calls" - ); -}); diff --git a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs deleted file mode 100644 index 528f8b191a..0000000000 --- a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs +++ /dev/null @@ -1,39 +0,0 @@ -#![no_main] -use jolt_field::{Accumulator, Fr, CanonicalRepr, WideAccumulator}; -use libfuzzer_sys::fuzz_target; - -fuzz_target!(|data: &[u8]| { - // Need at least two pairs (128 bytes) so each half gets at least one. - if data.len() < 128 { - return; - } - - let pairs = data.len() / 64; - let split = pairs / 2; - - let mut acc1 = WideAccumulator::default(); - let mut acc2 = WideAccumulator::default(); - let mut acc_all = WideAccumulator::default(); - - for i in 0..pairs { - let offset = i * 64; - let a = ::from_le_bytes_mod_order(&data[offset..offset + 32]); - let b = - ::from_le_bytes_mod_order(&data[offset + 32..offset + 64]); - - if i < split { - acc1.fmadd(a, b); - } else { - acc2.fmadd(a, b); - } - acc_all.fmadd(a, b); - } - - acc1.merge(acc2); - - assert_eq!( - acc1.reduce(), - acc_all.reduce(), - "merge+reduce diverged from single-accumulator reduce ({pairs} pairs, split at {split})" - ); -}); diff --git a/crates/jolt-field/fuzz/rust-toolchain.toml b/crates/jolt-field/fuzz/rust-toolchain.toml deleted file mode 100644 index 5d56faf9ae..0000000000 --- a/crates/jolt-field/fuzz/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "nightly" diff --git a/crates/jolt-field/src/accumulator.rs b/crates/jolt-field/src/accumulator.rs deleted file mode 100644 index 59ca235d9a..0000000000 --- a/crates/jolt-field/src/accumulator.rs +++ /dev/null @@ -1,118 +0,0 @@ -//! Deferred-reduction accumulators. -//! -//! In sumcheck inner loops, many products are summed before the final result -//! is needed. [`Accumulator`] lets implementations defer modular reduction -//! by accumulating in wider integer types, reducing once at the end. This -//! amortizes the expensive reduction across hundreds of multiply-add steps. -//! -//! - [`NaiveAccumulator`] — fallback using standard field arithmetic. -//! - `WideAccumulator` (BN254, in `arkworks/`) — 9-limb wide integer accumulator -//! that defers Montgomery reduction. - -use crate::{FromPrimitiveInt, RingCore}; -use num_traits::One; - -/// Accumulates sums and products with potentially deferred modular reduction. -/// -/// The hot loop pattern `acc += a * b` repeated hundreds of times per output -/// slot dominates the CPU prover. Standard field arithmetic reduces mod p -/// after every multiply and every add. Implementations for specific fields -/// (e.g., BN254 Fr) can instead accumulate unreduced wide products and -/// reduce once at the end via [`reduce`](Self::reduce). -/// -/// # Invariants -/// -/// - [`fmadd`](Self::fmadd) must be equivalent to `result += a * b` in the field. -/// - [`merge`](Self::merge) must be equivalent to adding another accumulator's -/// partial result (used for parallel reduction). -/// - [`reduce`](Self::reduce) must return the field element equal to the -/// accumulated sum of products. -pub trait Accumulator: Default + Copy + Send + Sync { - /// The element type this accumulator reduces to. - type Element: RingCore + FromPrimitiveInt; - - /// Adds one element into the accumulator. - fn add(&mut self, value: Self::Element); - - /// Merge another accumulator's partial sum into this one. - fn merge(&mut self, other: Self); - - /// Finalize: reduce the accumulated value to an element. - fn reduce(self) -> Self::Element; - - /// Fused multiply-add: `self += a * b` without intermediate reduction. - fn fmadd(&mut self, a: Self::Element, b: Self::Element); - - /// Fused multiply-add with a `u8` scalar: `self += a * F::from(b)`. - /// - /// Implementations may override for optimized small-scalar multiplication - /// (e.g., 4×1 limb schoolbook instead of 4×4). - #[inline] - fn fmadd_u8(&mut self, a: Self::Element, b: u8) { - self.fmadd(a, Self::Element::from_u8(b)); - } - - /// Fused multiply-add with a `u64` scalar: `self += a * F::from(b)`. - #[inline] - fn fmadd_u64(&mut self, a: Self::Element, b: u64) { - self.fmadd(a, Self::Element::from_u64(b)); - } - - /// Fused multiply-add with an `i64` scalar: `self += a * F::from(b)`. - #[inline] - fn fmadd_i64(&mut self, a: Self::Element, b: i64) { - self.fmadd(a, Self::Element::from_i64(b)); - } - - /// Fused multiply-add with a `bool` scalar: `self += a` when `b` is true. - #[inline] - fn fmadd_bool(&mut self, a: Self::Element, b: bool) { - if b { - self.fmadd(a, ::one()); - } - } -} - -/// Associates a redundant accumulator representation with an element type. -pub trait WithAccumulator: RingCore + FromPrimitiveInt { - /// Accumulator type. - type Accumulator: Accumulator; -} - -/// Naive accumulator using standard field arithmetic. -/// -/// Every [`fmadd`](Accumulator::fmadd) performs a full modular multiply -/// and add. Used as a fallback for fields without wide-integer optimization. -#[derive(Clone, Copy)] -pub struct NaiveAccumulator(R); - -impl Default for NaiveAccumulator { - #[inline] - fn default() -> Self { - Self(R::zero()) - } -} - -impl Accumulator for NaiveAccumulator { - type Element = R; - - #[inline] - fn add(&mut self, value: R) { - self.0 += value; - } - - #[inline] - fn merge(&mut self, other: Self) { - self.0 += other.0; - } - - #[inline] - fn reduce(self) -> R { - self.0 - } - - #[inline] - fn fmadd(&mut self, a: R, b: R) { - self.0 += a * b; - } -} diff --git a/crates/jolt-field/src/akita.rs b/crates/jolt-field/src/akita.rs index a572e9fc5f..6c732d8230 100644 --- a/crates/jolt-field/src/akita.rs +++ b/crates/jolt-field/src/akita.rs @@ -1,28 +1,21 @@ +//! Temporary bootstrap adapter for the pre-cutover `akita-field` type. +//! +//! Implements this crate's contracts for Akita's proof-optimized fp128 field +//! so the adapter stays buildable until the Akita cutover; it is a bootstrap +//! edge, not the target architecture, and is removed in the final migration +//! PR together with the `akita` feature. + use akita_config::proof_optimized::fp128::Field as AkitaField; use rand_core::RngCore; use crate::{ - AdditiveGroup, CanonicalBytes, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, - NaiveAccumulator, RingCore, WithAccumulator, + AdditiveGroup, CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, + WithAccumulator, }; impl AdditiveGroup for AkitaField {} -impl RingCore for AkitaField {} - -impl FieldCore for AkitaField { - #[inline] - fn inverse(&self) -> Option { - ::inverse(self) - } - - #[inline] - fn random(rng: &mut R) -> Self { - ::random(rng) - } -} - -impl FromPrimitiveInt for AkitaField { +impl Ring for AkitaField { #[inline] fn from_u64(v: u64) -> Self { ::from_u64(v) @@ -44,6 +37,18 @@ impl FromPrimitiveInt for AkitaField { } } +impl Field for AkitaField { + #[inline] + fn inverse(&self) -> Option { + ::inverse(self) + } + + #[inline] + fn random(rng: &mut R) -> Self { + ::random(rng) + } +} + impl CanonicalBytes for AkitaField { const NUM_BYTES: usize = ::NUM_BYTES; @@ -53,34 +58,58 @@ impl CanonicalBytes for AkitaField { } } -impl CanonicalRepr for AkitaField { +impl CanonicalEncoding for AkitaField { + // Akita's proof-optimized field is a 128-bit pseudo-Mersenne prime. + const MODULUS_BITS: u32 = 128; + #[inline(always)] - fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { + fn from_bytes_le_reduced(bytes: &[u8]) -> Self { ::from_le_bytes_mod_order(bytes) } #[inline] - fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { - // Scalar challenges match the legacy transcript convention: digest bytes - // are interpreted as a big-endian integer before reduction. - let mut buf = bytes.to_vec(); - buf.reverse(); - ::from_le_bytes_mod_order(&buf) + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + if bytes.len() != ::NUM_BYTES { + return None; + } + let value = Self::from_bytes_le_reduced(bytes); + // Canonical iff decoding round-trips to the identical bytes. + (value.to_bytes_le_vec() == bytes).then_some(value) } #[inline] - fn to_canonical_u64_checked(&self) -> Option { - ::to_canonical_u64_checked(self) + fn to_u128_checked(&self) -> Option { + let mut buf = [0u8; 16]; + CanonicalBytes::to_bytes_le(self, &mut buf); + Some(u128::from_le_bytes(buf)) + } + + #[inline] + fn from_u128_checked(v: u128) -> Option { + let value = ::from_u128(v); + (value.to_u128_checked() == Some(v)).then_some(value) + } + + #[inline] + fn from_u128_reduced(v: u128) -> Self { + ::from_u128(v) } #[inline] fn num_bits(&self) -> u32 { ::num_bits(self) } + + /// Legacy convention: digest bytes are interpreted as a big-endian + /// integer before reduction. + #[inline] + fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { + let mut buf = bytes.to_vec(); + buf.reverse(); + Self::from_bytes_le_reduced(&buf) + } } impl WithAccumulator for AkitaField { type Accumulator = NaiveAccumulator; } - -impl Field for AkitaField {} diff --git a/crates/jolt-field/src/algebra.rs b/crates/jolt-field/src/algebra.rs index b08b02863b..3c602ef5c5 100644 --- a/crates/jolt-field/src/algebra.rs +++ b/crates/jolt-field/src/algebra.rs @@ -1,14 +1,23 @@ -//! Core algebraic ladder: additive groups, rings, and fields, plus -//! primitive-integer embedding. +//! The trait spine: the algebraic ladder, the canonical (transcript) +//! representation, and deferred-reduction accumulators. +//! +//! ```text +//! AdditiveGroup -> Ring -> Field +//! ``` +//! +//! [`CanonicalEncoding`] and [`WithAccumulator`] are orthogonal capabilities; +//! [`JoltField`] is the blanket-implemented bundle of everything Jolt's +//! protocol stack requires of a scalar field. use num_traits::{One, Zero}; use rand_core::RngCore; +use serde::{de::DeserializeOwned, Serialize}; use std::fmt::{Debug, Display}; use std::hash::Hash; use std::iter::{Product, Sum}; use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -/// Minimal additive group operations shared by fields, rings, and accumulators. +/// Minimal additive group shared by fields, rings, and wide accumulators. pub trait AdditiveGroup: Sized + Clone @@ -26,8 +35,13 @@ pub trait AdditiveGroup: { } -/// Core ring arithmetic: additive group plus multiplication and one. -pub trait RingCore: +/// Unital ring: additive group plus multiplication, one, and the integer +/// embedding. +/// +/// The embedding lives here rather than on a separate trait because every +/// unital ring embeds the integers; only the four widest conversions are +/// required, everything else is defaulted on top of them. +pub trait Ring: AdditiveGroup + One + PartialEq @@ -44,56 +58,14 @@ pub trait RingCore: + Product + for<'a> Product<&'a Self> { - /// Returns `self * self`. - #[inline] - fn square(&self) -> Self { - *self * *self - } - - #[inline] - fn pow2(exponent: usize) -> Self { - let mut result = Self::one(); - let mut base = Self::one() + Self::one(); - let mut remaining = exponent; - - while remaining > 0 { - if remaining % 2 == 1 { - result *= base; - } - remaining /= 2; - if remaining > 0 { - base = base.square(); - } - } - - result - } -} - -/// Algebraic field: ring arithmetic plus explicit inversion and sampling. -pub trait FieldCore: RingCore { - /// Multiplicative inverse, or `None` for the zero element. - fn inverse(&self) -> Option; - - /// Multiplicative inverse with zero mapped to zero. - #[inline] - fn inv_or_zero(self) -> Self { - self.inverse().unwrap_or_else(Self::zero) - } - - /// Samples a random element (RNG-backed, for tests and witnesses). - fn random(rng: &mut R) -> Self; -} + fn from_u64(v: u64) -> Self; + fn from_i64(v: i64) -> Self; + fn from_u128(v: u128) -> Self; + fn from_i128(v: i128) -> Self; -/// Embed primitive integer values and multiply by primitive integer scalars. -pub trait FromPrimitiveInt: RingCore { #[inline] fn from_bool(v: bool) -> Self { - if v { - Self::from_u64(1) - } else { - Self::from_u64(0) - } + Self::from_u64(v as u64) } #[inline] @@ -126,10 +98,29 @@ pub trait FromPrimitiveInt: RingCore { Self::from_i64(v as i64) } - fn from_u64(v: u64) -> Self; - fn from_i64(v: i64) -> Self; - fn from_u128(v: u128) -> Self; - fn from_i128(v: i128) -> Self; + /// Returns `self * self`. + #[inline] + fn square(&self) -> Self { + *self * *self + } + + /// Returns the ring element `2^exponent`. + #[inline] + fn pow2(exponent: usize) -> Self { + let mut result = Self::one(); + let mut base = Self::one() + Self::one(); + let mut remaining = exponent; + while remaining > 0 { + if remaining % 2 == 1 { + result *= base; + } + remaining /= 2; + if remaining > 0 { + base = base.square(); + } + } + result + } /// Multiplies by a `u64`. #[inline(always)] @@ -168,3 +159,277 @@ pub trait FromPrimitiveInt: RingCore { res * Self::from_u64(1 << p) } } + +/// Algebraic field: ring arithmetic plus inversion, sampling, and halving. +pub trait Field: Ring { + /// Multiplicative inverse, or `None` for the zero element. + fn inverse(&self) -> Option; + + /// Multiplicative inverse with zero mapped to zero. + #[inline] + fn inv_or_zero(self) -> Self { + self.inverse().unwrap_or_else(Self::zero) + } + + /// Samples a random element (RNG-backed, for tests and witnesses). + fn random(rng: &mut R) -> Self; + + /// The multiplicative inverse of two. + /// + /// Defaulted via [`inverse`](Self::inverse); fields with a cheap shift + /// implementation override [`half`](Self::half) and this together. + #[inline] + #[expect(clippy::expect_used, reason = "characteristic two is unsupported")] + fn two_inv() -> Self { + Self::from_u64(2) + .inverse() + .expect("field has characteristic two") + } + + /// Divides this element by two. + #[inline] + fn half(self) -> Self { + self * Self::two_inv() + } +} + +/// Metadata contract for a pseudo-Mersenne field `p = 2^k − c`. +/// +/// The exponent `k` is [`CanonicalEncoding::MODULUS_BITS`]; implementing +/// this contract lights up the generic machinery bounded on it (extension +/// towers, packed backends). +pub trait PseudoMersenne: Field + CanonicalEncoding { + /// Offset `c` in `2^k − c`. + const OFFSET: u128; + + /// Degree-4 extension multiply kernel in the `[1, e1, e2, e3]` basis. + /// + /// Defaults to the generic coefficient schedule; base fields whose + /// representation supports fusing product sums before reduction + /// override it (`Fp32` accumulates raw products in `u128`). + #[inline(always)] + fn ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { + crate::schedules::ext4_mul_coeffs(a, b) + } + + /// Degree-4 extension squaring kernel in the `[1, e1, e2, e3]` basis. + #[inline(always)] + fn ext4_square(a: [Self; 4]) -> [Self; 4] { + crate::schedules::ext4_square_coeffs(a) + } + + /// Degree-8 extension multiply kernel in the `[1, e1, ..., e7]` basis. + #[inline(always)] + fn ext8_mul(a: [Self; 8], b: [Self; 8]) -> [Self; 8] { + crate::schedules::ext8_mul_coeffs(a, b) + } + + /// Degree-8 extension squaring kernel in the `[1, e1, ..., e7]` basis. + #[inline(always)] + fn ext8_square(a: [Self; 8]) -> [Self; 8] { + crate::schedules::ext8_square_coeffs(a) + } +} + +/// Fixed-size canonical little-endian byte encoding: the transcript +/// absorption surface. +/// +/// This is deliberately the *narrow* claim, "this value has one canonical +/// byte encoding", implementable by non-field types (e.g. zero-sized +/// commitment placeholders) that must be transcript-absorbable without +/// pretending to be decodable field elements. Field types get the full +/// decode surface via [`CanonicalEncoding`]. +/// +/// # Invariants +/// +/// - The encoding is injective on canonical representatives: equal values +/// produce equal bytes, distinct values produce distinct bytes. +/// - [`to_bytes_le`](Self::to_bytes_le) always writes exactly +/// [`NUM_BYTES`](Self::NUM_BYTES) bytes of the unique representative. +pub trait CanonicalBytes { + /// Byte length of the fixed-size canonical encoding. + const NUM_BYTES: usize; + + /// Writes the canonical little-endian encoding into `out`. + fn to_bytes_le(&self, out: &mut [u8]); + + /// Returns the canonical little-endian encoding as a vector. + #[inline] + fn to_bytes_le_vec(&self) -> Vec { + let mut out = vec![0u8; Self::NUM_BYTES]; + self.to_bytes_le(&mut out); + out + } +} + +/// Canonical decode-and-introspect surface of a field element, on top of the +/// [`CanonicalBytes`] encoding: the single source of canonicity for wire +/// serialization. +/// +/// Transcript absorption and challenge derivation use the explicit +/// [`CanonicalBytes`] encoding so the hashed byte stream is specified +/// independently of any serialization library. Proof/wire serialization goes +/// through serde + bincode, reusing +/// [`from_bytes_le_checked`](Self::from_bytes_le_checked) so non-canonical +/// encodings are rejected uniformly. +pub trait CanonicalEncoding: + CanonicalBytes + Sized + Copy + Default + PartialEq + Eq + Debug + Hash + Send + Sync + 'static +{ + /// Bit length of the field order `|F|` (for prime fields, the modulus). + const MODULUS_BITS: u32; + + /// Decodes little-endian bytes of any length by reducing into the field. + fn from_bytes_le_reduced(bytes: &[u8]) -> Self; + + /// Decodes exactly [`NUM_BYTES`](Self::NUM_BYTES) canonical bytes; + /// `None` on wrong length or a non-canonical value. + fn from_bytes_le_checked(bytes: &[u8]) -> Option; + + /// Returns the canonical representative if it fits in a `u128`. + /// + /// For extension fields: the constant coefficient, when all higher + /// coefficients are zero. + fn to_u128_checked(&self) -> Option; + + /// Returns the canonical representative if it fits in a `u64`. + #[inline] + fn to_u64_checked(&self) -> Option { + self.to_u128_checked().and_then(|v| u64::try_from(v).ok()) + } + + /// Constructs an element when `v` is a canonical representative. + fn from_u128_checked(v: u128) -> Option; + + /// Constructs an element by reducing `v` modulo the field order. + fn from_u128_reduced(v: u128) -> Self; + + /// Number of significant bits in this element's canonical representative. + /// + /// Zero is considered to have zero significant bits. + fn num_bits(&self) -> u32; + + /// Constructs a Fiat-Shamir challenge from squeezed transcript bytes. + #[inline] + fn from_challenge_bytes(bytes: &[u8]) -> Self { + Self::from_bytes_le_reduced(bytes) + } + + /// Constructs a non-optimized scalar challenge from transcript bytes. + #[inline] + fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { + Self::from_challenge_bytes(bytes) + } +} + +/// Accumulates sums and products with potentially deferred modular reduction. +/// +/// The hot-loop pattern `acc += a * b` repeated hundreds of times per output +/// slot dominates the CPU prover. Implementations for specific fields can +/// accumulate unreduced wide products and reduce once at the end. +/// +/// # Invariants +/// +/// - [`fmadd`](Self::fmadd) must be equivalent to `acc += a * b` in the field. +/// - [`merge`](Self::merge) must be equivalent to adding another +/// accumulator's partial result (used for parallel reduction). +/// - [`reduce`](Self::reduce) must return the element equal to the +/// accumulated sum of products. +pub trait Accumulator: Default + Copy + Send + Sync { + /// The element type this accumulator reduces to. + type Element: Ring; + + /// Adds one element into the accumulator. + fn add(&mut self, value: Self::Element); + + /// Merges another accumulator's partial sum into this one. + fn merge(&mut self, other: Self); + + /// Finalizes: reduces the accumulated value to an element. + fn reduce(self) -> Self::Element; + + /// Fused multiply-add: `self += a * b` without intermediate reduction. + fn fmadd(&mut self, a: Self::Element, b: Self::Element); + + /// Fused multiply-add with a `u8` scalar: `self += a * F::from(b)`. + #[inline] + fn fmadd_u8(&mut self, a: Self::Element, b: u8) { + self.fmadd(a, Self::Element::from_u8(b)); + } + + /// Fused multiply-add with a `u64` scalar: `self += a * F::from(b)`. + #[inline] + fn fmadd_u64(&mut self, a: Self::Element, b: u64) { + self.fmadd(a, Self::Element::from_u64(b)); + } + + /// Fused multiply-add with an `i64` scalar: `self += a * F::from(b)`. + #[inline] + fn fmadd_i64(&mut self, a: Self::Element, b: i64) { + self.fmadd(a, Self::Element::from_i64(b)); + } + + /// Fused multiply-add with a `bool` scalar: `self += a` when `b` is true. + #[inline] + fn fmadd_bool(&mut self, a: Self::Element, b: bool) { + if b { + self.add(a); + } + } +} + +/// Associates a deferred-reduction accumulator with an element type. +pub trait WithAccumulator: Ring { + /// Accumulator type. + type Accumulator: Accumulator; +} + +/// Fallback accumulator using standard ring arithmetic: every +/// [`fmadd`](Accumulator::fmadd) performs a full multiply and add. +#[derive(Clone, Copy)] +pub struct NaiveAccumulator(R); + +impl Default for NaiveAccumulator { + #[inline] + fn default() -> Self { + Self(R::zero()) + } +} + +impl Accumulator for NaiveAccumulator { + type Element = R; + + #[inline] + fn add(&mut self, value: R) { + self.0 += value; + } + + #[inline] + fn merge(&mut self, other: Self) { + self.0 += other.0; + } + + #[inline] + fn reduce(self) -> R { + self.0 + } + + #[inline] + fn fmadd(&mut self, a: R, b: R) { + self.0 += a * b; + } +} + +/// Everything Jolt's protocol stack requires of a scalar field: field +/// algebra, a canonical transcript encoding, an accumulator, and a serde +/// wire format. +/// +/// Blanket-implemented — implement the component traits and this follows. +pub trait JoltField: + Field + CanonicalEncoding + WithAccumulator + Serialize + DeserializeOwned +{ +} + +impl JoltField + for T +{ +} diff --git a/crates/jolt-field/src/arkworks/bn254.rs b/crates/jolt-field/src/arkworks/bn254.rs deleted file mode 100644 index cf2fd296d2..0000000000 --- a/crates/jolt-field/src/arkworks/bn254.rs +++ /dev/null @@ -1,516 +0,0 @@ -//! Newtype wrapper around `ark_bn254::Fr` that decouples the public API from arkworks. -//! -//! [`Fr`] is `#[repr(transparent)]` over the inner arkworks scalar field element, -//! so it has identical layout and can be transmuted where needed. -use crate::{ - AdditiveGroup, CanonicalBytes, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, Limbs, - RingCore, WithAccumulator, -}; -use ark_ff::{prelude::*, PrimeField, UniformRand}; -use rand_core::RngCore; - -use super::bn254_ops; - -type InnerFr = ark_bn254::Fr; - -/// BN254 scalar field element. -/// -/// A `#[repr(transparent)]` newtype over `ark_bn254::Fr`. -#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)] -#[repr(transparent)] -pub struct Fr(pub(crate) InnerFr); - -impl From for Fr { - #[inline(always)] - fn from(inner: ark_bn254::Fr) -> Self { - Fr(inner) - } -} - -impl From for ark_bn254::Fr { - #[inline(always)] - fn from(wrapper: Fr) -> Self { - wrapper.0 - } -} - -impl From for Fr { - #[inline(always)] - fn from(v: bool) -> Self { - ::from_bool(v) - } -} - -impl From for Fr { - #[inline(always)] - fn from(v: u8) -> Self { - ::from_u64(v as u64) - } -} - -impl From for Fr { - #[inline(always)] - fn from(v: u16) -> Self { - ::from_u64(v as u64) - } -} - -impl From for Fr { - #[inline(always)] - fn from(v: u32) -> Self { - ::from_u64(v as u64) - } -} - -impl From for Fr { - #[inline(always)] - fn from(v: u64) -> Self { - ::from_u64(v) - } -} - -impl From for Fr { - #[inline(always)] - fn from(v: i64) -> Self { - ::from_i64(v) - } -} - -impl From for Fr { - #[inline(always)] - fn from(v: i128) -> Self { - ::from_i128(v) - } -} - -impl From for Fr { - #[inline(always)] - fn from(v: u128) -> Self { - ::from_u128(v) - } -} - -impl std::fmt::Debug for Fr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(&self.0, f) - } -} - -impl std::fmt::Display for Fr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) - } -} - -macro_rules! delegate_binop { - ($Trait:ident, $method:ident) => { - impl std::ops::$Trait for Fr { - type Output = Fr; - #[inline(always)] - fn $method(self, rhs: Fr) -> Fr { - Fr(std::ops::$Trait::$method(self.0, rhs.0)) - } - } - - impl std::ops::$Trait<&Fr> for Fr { - type Output = Fr; - #[inline(always)] - fn $method(self, rhs: &Fr) -> Fr { - Fr(std::ops::$Trait::$method(self.0, &rhs.0)) - } - } - - impl std::ops::$Trait for &Fr { - type Output = Fr; - #[inline(always)] - fn $method(self, rhs: Fr) -> Fr { - Fr(std::ops::$Trait::$method(self.0, rhs.0)) - } - } - - impl<'a, 'b> std::ops::$Trait<&'b Fr> for &'a Fr { - type Output = Fr; - #[inline(always)] - fn $method(self, rhs: &'b Fr) -> Fr { - Fr(std::ops::$Trait::$method(self.0, &rhs.0)) - } - } - }; -} - -delegate_binop!(Add, add); -delegate_binop!(Sub, sub); -delegate_binop!(Mul, mul); -delegate_binop!(Div, div); - -impl std::ops::Neg for Fr { - type Output = Fr; - #[inline(always)] - fn neg(self) -> Fr { - Fr(self.0.neg()) - } -} - -impl std::ops::AddAssign for Fr { - #[inline(always)] - fn add_assign(&mut self, rhs: Fr) { - self.0.add_assign(rhs.0); - } -} - -impl std::ops::SubAssign for Fr { - #[inline(always)] - fn sub_assign(&mut self, rhs: Fr) { - self.0.sub_assign(rhs.0); - } -} - -impl std::ops::MulAssign for Fr { - #[inline(always)] - fn mul_assign(&mut self, rhs: Fr) { - self.0.mul_assign(rhs.0); - } -} - -impl std::iter::Sum for Fr { - fn sum>(iter: I) -> Self { - Fr(iter.map(|f| f.0).sum()) - } -} - -impl<'a> std::iter::Sum<&'a Fr> for Fr { - fn sum>(iter: I) -> Self { - Fr(iter.map(|f| f.0).sum()) - } -} - -impl std::iter::Product for Fr { - fn product>(iter: I) -> Self { - Fr(iter.map(|f| f.0).product()) - } -} - -impl<'a> std::iter::Product<&'a Fr> for Fr { - fn product>(iter: I) -> Self { - Fr(iter.map(|f| f.0).product()) - } -} - -impl num_traits::Zero for Fr { - #[inline(always)] - fn zero() -> Self { - Fr(InnerFr::zero()) - } - - #[inline(always)] - fn is_zero(&self) -> bool { - self.0.is_zero() - } -} - -impl num_traits::One for Fr { - #[inline(always)] - fn one() -> Self { - Fr(InnerFr::one()) - } - - #[inline(always)] - fn is_one(&self) -> bool { - self.0.is_one() - } -} - -impl serde::Serialize for Fr { - fn serialize(&self, serializer: S) -> Result { - use ark_serialize::CanonicalSerialize; - let mut buf = [0u8; 32]; - self.0 - .serialize_compressed(&mut buf[..]) - .map_err(serde::ser::Error::custom)?; - <[u8; 32]>::serialize(&buf, serializer) - } -} - -impl<'de> serde::Deserialize<'de> for Fr { - fn deserialize>(deserializer: D) -> Result { - use ark_serialize::CanonicalDeserialize; - let buf = <[u8; 32]>::deserialize(deserializer)?; - let inner = InnerFr::deserialize_compressed(&buf[..]).map_err(serde::de::Error::custom)?; - Ok(Fr(inner)) - } -} - -impl ark_serialize::CanonicalSerialize for Fr { - fn serialize_with_mode( - &self, - writer: W, - compress: ark_serialize::Compress, - ) -> Result<(), ark_serialize::SerializationError> { - self.0.serialize_with_mode(writer, compress) - } - - fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { - self.0.serialized_size(compress) - } -} - -impl ark_serialize::Valid for Fr { - fn check(&self) -> Result<(), ark_serialize::SerializationError> { - self.0.check() - } -} - -impl ark_serialize::CanonicalDeserialize for Fr { - fn deserialize_with_mode( - reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - ) -> Result { - InnerFr::deserialize_with_mode(reader, compress, validate).map(Fr) - } -} - -impl UniformRand for Fr { - fn rand(rng: &mut R) -> Self { - Fr(::rand(rng)) - } -} - -#[cfg(feature = "allocative")] -impl allocative::Allocative for Fr { - fn visit<'a, 'b: 'a>(&self, visitor: &'a mut allocative::Visitor<'b>) { - visitor.visit_simple_sized::(); - } -} - -impl Fr { - /// Deserializes from little-endian bytes, reducing modulo the field prime. - #[inline] - pub fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { - Fr(InnerFr::from_le_bytes_mod_order(bytes)) - } - - /// Converts a limb array to a field element without checking that it is - /// less than the modulus. - #[inline] - pub fn from_bigint_unchecked(limbs: Limbs<4>) -> Self { - Fr(bn254_ops::from_bigint_unchecked(limbs.into())) - } - - /// Access the internal Montgomery-form limbs. - /// - /// Used by [`WideAccumulator`](super::wide_accumulator::WideAccumulator) - /// for deferred-reduction fused multiply-add. - #[inline(always)] - pub fn inner_limbs(self) -> Limbs<4> { - Limbs((self.0).0 .0) - } - - /// Construct from the inner arkworks element. - #[inline(always)] - pub(crate) fn from_inner(inner: InnerFr) -> Self { - Fr(inner) - } -} - -impl AdditiveGroup for Fr {} - -impl RingCore for Fr { - #[inline] - fn square(&self) -> Self { - Fr(::square(&self.0)) - } -} - -impl FieldCore for Fr { - #[inline] - fn inverse(&self) -> Option { - ::inverse(&self.0).map(Fr) - } - - #[inline] - fn random(rng: &mut R) -> Self { - Fr(::rand(rng)) - } -} - -impl CanonicalBytes for Fr { - const NUM_BYTES: usize = 32; - - #[expect(clippy::expect_used)] - #[inline] - fn to_bytes_le(&self, out: &mut [u8]) { - assert_eq!(out.len(), ::NUM_BYTES); - use ark_serialize::CanonicalSerialize; - self.0 - .serialize_compressed(out) - .expect("BN254 Fr always serializes to 32 bytes"); - } -} - -impl CanonicalRepr for Fr { - #[inline] - fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { - Fr::from_le_bytes_mod_order(bytes) - } - - #[inline] - fn to_canonical_u64_checked(&self) -> Option { - let bigint = ::into_bigint(self.0); - let limbs: &[u64] = bigint.as_ref(); - let result = limbs[0]; - - if ::from_u64(result) != *self { - None - } else { - Some(result) - } - } - - #[inline] - fn num_bits(&self) -> u32 { - ::into_bigint(self.0).num_bits() - } - - #[inline] - fn from_challenge_bytes(bytes: &[u8]) -> Self { - let mut buf = [0u8; 16]; - let len = bytes.len().min(buf.len()); - buf[..len].copy_from_slice(&bytes[..len]); - let value = u128::from_le_bytes(buf); - let low = value as u64; - // Top 3 bits of high limb are zeroed to ensure value < BN254 modulus. - let high = ((value >> 64) as u64) & (u64::MAX >> 3); - let Some(inner) = InnerFr::from_bigint_unchecked(ark_ff::BigInt::new([0, 0, low, high])) - else { - unreachable!("masked 125-bit shifted challenge fits in BN254 Fr") - }; - Fr(inner) - } - - #[inline] - fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { - let mut buf = bytes.to_vec(); - // Scalar challenges match the legacy transcript convention: digest bytes - // are interpreted as a big-endian integer before reduction. - buf.reverse(); - Fr::from_le_bytes_mod_order(&buf) - } -} - -impl FromPrimitiveInt for Fr { - #[inline] - fn from_u64(n: u64) -> Self { - Fr(bn254_ops::from_u64(n)) - } - - #[inline] - fn from_i64(val: i64) -> Self { - if val.is_negative() { - -Fr(bn254_ops::from_u64(val.unsigned_abs())) - } else { - Fr(bn254_ops::from_u64(val as u64)) - } - } - - #[inline] - fn from_i128(val: i128) -> Self { - if val.is_negative() { - -Fr(bn254_ops::from_u128(val.unsigned_abs())) - } else { - Fr(bn254_ops::from_u128(val as u128)) - } - } - - #[inline] - fn from_u128(val: u128) -> Self { - Fr(bn254_ops::from_u128(val)) - } - - #[inline] - fn mul_u64(&self, n: u64) -> Self { - Fr(bn254_ops::mul_u64(self.0, n)) - } - - #[inline(always)] - fn mul_i64(&self, n: i64) -> Self { - Fr(bn254_ops::mul_i64(self.0, n)) - } - - #[inline(always)] - fn mul_u128(&self, n: u128) -> Self { - Fr(bn254_ops::mul_u128(self.0, n)) - } - - #[inline] - fn mul_i128(&self, n: i128) -> Self { - Fr(bn254_ops::mul_i128(self.0, n)) - } -} - -impl WithAccumulator for Fr { - type Accumulator = super::wide_accumulator::WideAccumulator; -} - -impl Field for Fr {} - -#[cfg(test)] -#[expect(clippy::unwrap_used)] -mod tests { - use super::*; - use crate::CanonicalRepr; - - #[test] - fn field_arithmetic_basic() { - let a = Fr::from_u64(7); - let b = Fr::from_u64(6); - assert_eq!(a * b, Fr::from_u64(42)); - assert_eq!(a + b, Fr::from_u64(13)); - assert_eq!(b - a, Fr::from_i64(-1)); - } - - #[test] - fn from_signed() { - let neg_one = Fr::from_i64(-1); - let one = Fr::one(); - assert_eq!(neg_one + one, Fr::zero()); - - let neg_big = Fr::from_i128(-1_000_000_000_000i128); - let pos_big = Fr::from_u128(1_000_000_000_000u128); - assert_eq!(neg_big + pos_big, Fr::zero()); - } - - #[test] - fn serialization_roundtrip() { - let val = Fr::from_u64(123_456_789); - let bytes = val.to_bytes_le_vec(); - let recovered = ::from_le_bytes_mod_order(&bytes); - assert_eq!(val, recovered); - } - - #[test] - fn inverse_and_square() { - let a = Fr::from_u64(42); - let inv = a.inverse().unwrap(); - assert_eq!(a * inv, Fr::one()); - assert!(Fr::zero().inverse().is_none()); - - assert_eq!(a.square(), a * a); - } - - #[test] - fn to_u64_roundtrip() { - assert_eq!(Fr::from_u64(999).to_canonical_u64_checked(), Some(999)); - // Large field element should not fit in u64 - let big = Fr::from_u128(u128::MAX / 2); - assert_eq!(big.to_canonical_u64_checked(), None); - } - - #[test] - fn inner_limbs_roundtrip() { - let val = Fr::from_u64(42); - let limbs = val.inner_limbs(); - let recovered = Fr::from_bigint_unchecked(limbs); - assert_eq!(val, recovered); - } -} diff --git a/crates/jolt-field/src/arkworks/bn254_fq.rs b/crates/jolt-field/src/arkworks/bn254_fq.rs deleted file mode 100644 index 41c67363d9..0000000000 --- a/crates/jolt-field/src/arkworks/bn254_fq.rs +++ /dev/null @@ -1,450 +0,0 @@ -//! Newtype wrapper around `ark_bn254::Fq` that decouples the public API from arkworks. -//! -//! [`Fq`] is the BN254 base field. In the BN254/Grumpkin cycle it is also the -//! scalar field of Grumpkin. - -use crate::{ - AdditiveGroup, CanonicalBytes, CanonicalRepr, Field, FieldCore, FromPrimitiveInt, Limbs, - NaiveAccumulator, RingCore, WithAccumulator, -}; -use ark_ff::{prelude::*, PrimeField, UniformRand}; -use rand_core::RngCore; - -type InnerFq = ark_bn254::Fq; - -/// BN254 base field element. -/// -/// A `#[repr(transparent)]` newtype over `ark_bn254::Fq`. -#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)] -#[repr(transparent)] -pub struct Fq(pub(crate) InnerFq); - -impl From for Fq { - #[inline(always)] - fn from(v: bool) -> Self { - ::from_bool(v) - } -} - -impl From for Fq { - #[inline(always)] - fn from(v: u8) -> Self { - ::from_u64(v as u64) - } -} - -impl From for Fq { - #[inline(always)] - fn from(v: u16) -> Self { - ::from_u64(v as u64) - } -} - -impl From for Fq { - #[inline(always)] - fn from(v: u32) -> Self { - ::from_u64(v as u64) - } -} - -impl From for Fq { - #[inline(always)] - fn from(v: u64) -> Self { - ::from_u64(v) - } -} - -impl From for Fq { - #[inline(always)] - fn from(v: i64) -> Self { - ::from_i64(v) - } -} - -impl From for Fq { - #[inline(always)] - fn from(v: i128) -> Self { - ::from_i128(v) - } -} - -impl From for Fq { - #[inline(always)] - fn from(v: u128) -> Self { - ::from_u128(v) - } -} - -impl std::fmt::Debug for Fq { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(&self.0, f) - } -} - -impl std::fmt::Display for Fq { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) - } -} - -macro_rules! delegate_binop { - ($Trait:ident, $method:ident) => { - impl std::ops::$Trait for Fq { - type Output = Fq; - #[inline(always)] - fn $method(self, rhs: Fq) -> Fq { - Fq(std::ops::$Trait::$method(self.0, rhs.0)) - } - } - - impl std::ops::$Trait<&Fq> for Fq { - type Output = Fq; - #[inline(always)] - fn $method(self, rhs: &Fq) -> Fq { - Fq(std::ops::$Trait::$method(self.0, rhs.0)) - } - } - - impl std::ops::$Trait for &Fq { - type Output = Fq; - #[inline(always)] - fn $method(self, rhs: Fq) -> Fq { - Fq(std::ops::$Trait::$method(self.0, rhs.0)) - } - } - - impl<'a, 'b> std::ops::$Trait<&'b Fq> for &'a Fq { - type Output = Fq; - #[inline(always)] - fn $method(self, rhs: &'b Fq) -> Fq { - Fq(std::ops::$Trait::$method(self.0, rhs.0)) - } - } - }; -} - -delegate_binop!(Add, add); -delegate_binop!(Sub, sub); -delegate_binop!(Mul, mul); -delegate_binop!(Div, div); - -impl std::ops::Neg for Fq { - type Output = Fq; - - #[inline(always)] - fn neg(self) -> Fq { - Fq(self.0.neg()) - } -} - -impl std::ops::AddAssign for Fq { - #[inline(always)] - fn add_assign(&mut self, rhs: Fq) { - self.0.add_assign(rhs.0); - } -} - -impl std::ops::SubAssign for Fq { - #[inline(always)] - fn sub_assign(&mut self, rhs: Fq) { - self.0.sub_assign(rhs.0); - } -} - -impl std::ops::MulAssign for Fq { - #[inline(always)] - fn mul_assign(&mut self, rhs: Fq) { - self.0.mul_assign(rhs.0); - } -} - -impl std::iter::Sum for Fq { - fn sum>(iter: I) -> Self { - Fq(iter.map(|f| f.0).sum()) - } -} - -impl<'a> std::iter::Sum<&'a Fq> for Fq { - fn sum>(iter: I) -> Self { - Fq(iter.map(|f| f.0).sum()) - } -} - -impl std::iter::Product for Fq { - fn product>(iter: I) -> Self { - Fq(iter.map(|f| f.0).product()) - } -} - -impl<'a> std::iter::Product<&'a Fq> for Fq { - fn product>(iter: I) -> Self { - Fq(iter.map(|f| f.0).product()) - } -} - -impl num_traits::Zero for Fq { - #[inline(always)] - fn zero() -> Self { - Fq(InnerFq::zero()) - } - - #[inline(always)] - fn is_zero(&self) -> bool { - self.0.is_zero() - } -} - -impl num_traits::One for Fq { - #[inline(always)] - fn one() -> Self { - Fq(InnerFq::one()) - } - - #[inline(always)] - fn is_one(&self) -> bool { - self.0.is_one() - } -} - -impl serde::Serialize for Fq { - fn serialize(&self, serializer: S) -> Result { - use ark_serialize::CanonicalSerialize; - let mut buf = [0u8; 32]; - self.0 - .serialize_compressed(&mut buf[..]) - .map_err(serde::ser::Error::custom)?; - <[u8; 32]>::serialize(&buf, serializer) - } -} - -impl<'de> serde::Deserialize<'de> for Fq { - fn deserialize>(deserializer: D) -> Result { - use ark_serialize::CanonicalDeserialize; - let buf = <[u8; 32]>::deserialize(deserializer)?; - let inner = InnerFq::deserialize_compressed(&buf[..]).map_err(serde::de::Error::custom)?; - Ok(Fq(inner)) - } -} - -impl ark_serialize::CanonicalSerialize for Fq { - fn serialize_with_mode( - &self, - writer: W, - compress: ark_serialize::Compress, - ) -> Result<(), ark_serialize::SerializationError> { - self.0.serialize_with_mode(writer, compress) - } - - fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { - self.0.serialized_size(compress) - } -} - -impl ark_serialize::Valid for Fq { - fn check(&self) -> Result<(), ark_serialize::SerializationError> { - self.0.check() - } -} - -impl ark_serialize::CanonicalDeserialize for Fq { - fn deserialize_with_mode( - reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - ) -> Result { - InnerFq::deserialize_with_mode(reader, compress, validate).map(Fq) - } -} - -impl UniformRand for Fq { - fn rand(rng: &mut R) -> Self { - Fq(::rand(rng)) - } -} - -#[cfg(feature = "allocative")] -impl allocative::Allocative for Fq { - fn visit<'a, 'b: 'a>(&self, visitor: &'a mut allocative::Visitor<'b>) { - visitor.visit_simple_sized::(); - } -} - -impl Fq { - /// Deserializes from little-endian bytes, reducing modulo the field prime. - #[inline] - pub fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { - Fq(InnerFq::from_le_bytes_mod_order(bytes)) - } - - /// Converts a limb array to a field element without checking that it is - /// less than the modulus. - #[inline] - pub fn from_bigint_unchecked(limbs: Limbs<4>) -> Self { - let Some(inner) = InnerFq::from_bigint(ark_ff::BigInt::new(limbs.0)) else { - unreachable!("unchecked BN254 Fq construction received non-canonical limbs") - }; - Fq(inner) - } - - /// Access the internal Montgomery-form limbs. - #[inline(always)] - pub fn inner_limbs(self) -> Limbs<4> { - Limbs((self.0).0 .0) - } -} - -impl AdditiveGroup for Fq {} - -impl RingCore for Fq { - #[inline] - fn square(&self) -> Self { - Fq(::square(&self.0)) - } -} - -impl FieldCore for Fq { - #[inline] - fn inverse(&self) -> Option { - ::inverse(&self.0).map(Fq) - } - - #[inline] - fn random(rng: &mut R) -> Self { - Fq(::rand(rng)) - } -} - -impl CanonicalBytes for Fq { - const NUM_BYTES: usize = 32; - - #[expect(clippy::expect_used)] - #[inline] - fn to_bytes_le(&self, out: &mut [u8]) { - assert_eq!(out.len(), ::NUM_BYTES); - use ark_serialize::CanonicalSerialize; - self.0 - .serialize_compressed(out) - .expect("BN254 Fq always serializes to 32 bytes"); - } -} - -impl CanonicalRepr for Fq { - #[inline] - fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { - Fq::from_le_bytes_mod_order(bytes) - } - - #[inline] - fn from_challenge_bytes(bytes: &[u8]) -> Self { - let mut buf = [0u8; 16]; - let len = bytes.len().min(buf.len()); - buf[..len].copy_from_slice(&bytes[..len]); - let value = u128::from_le_bytes(buf); - let low = value as u64; - let high = ((value >> 64) as u64) & (u64::MAX >> 3); - let Some(inner) = InnerFq::from_bigint(ark_ff::BigInt::new([0, 0, low, high])) else { - unreachable!("masked 125-bit shifted challenge fits in BN254 Fq") - }; - Fq(inner) - } - - #[inline] - fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { - let mut buf = bytes.to_vec(); - buf.reverse(); - Fq::from_le_bytes_mod_order(&buf) - } - - #[inline] - fn to_canonical_u64_checked(&self) -> Option { - let bigint = ::into_bigint(self.0); - let limbs: &[u64] = bigint.as_ref(); - let result = limbs[0]; - - if ::from_u64(result) != *self { - None - } else { - Some(result) - } - } - - #[inline] - fn num_bits(&self) -> u32 { - ::into_bigint(self.0).num_bits() - } -} - -impl FromPrimitiveInt for Fq { - #[inline] - fn from_u64(n: u64) -> Self { - Fq(InnerFq::from(n)) - } - - #[inline] - fn from_i64(val: i64) -> Self { - if val.is_negative() { - -Fq(InnerFq::from(val.unsigned_abs())) - } else { - Fq(InnerFq::from(val as u64)) - } - } - - #[inline] - fn from_i128(val: i128) -> Self { - if val.is_negative() { - -Fq(InnerFq::from(val.unsigned_abs())) - } else { - Fq(InnerFq::from(val as u128)) - } - } - - #[inline] - fn from_u128(val: u128) -> Self { - Fq(InnerFq::from(val)) - } -} - -impl WithAccumulator for Fq { - type Accumulator = NaiveAccumulator; -} - -impl Field for Fq {} - -#[cfg(test)] -#[expect(clippy::unwrap_used)] -mod tests { - use super::*; - use crate::CanonicalRepr; - - #[test] - fn field_arithmetic_basic() { - let a = Fq::from_u64(7); - let b = Fq::from_u64(6); - assert_eq!(a * b, Fq::from_u64(42)); - assert_eq!(a + b, Fq::from_u64(13)); - assert_eq!(b - a, Fq::from_i64(-1)); - } - - #[test] - fn serialization_roundtrip() { - let val = Fq::from_u64(123_456_789); - let bytes = val.to_bytes_le_vec(); - let recovered = ::from_le_bytes_mod_order(&bytes); - assert_eq!(val, recovered); - } - - #[test] - fn inverse_and_square() { - let a = Fq::from_u64(42); - let inv = a.inverse().unwrap(); - assert_eq!(a * inv, Fq::one()); - assert!(Fq::zero().inverse().is_none()); - assert_eq!(a.square(), a * a); - } - - #[test] - fn to_u64_roundtrip() { - let value = Fq::from_u64(12345); - assert_eq!(value.to_canonical_u64_checked(), Some(12345)); - } -} diff --git a/crates/jolt-field/src/arkworks/bn254_ops.rs b/crates/jolt-field/src/arkworks/bn254_ops.rs deleted file mode 100644 index aabb3cc378..0000000000 --- a/crates/jolt-field/src/arkworks/bn254_ops.rs +++ /dev/null @@ -1,643 +0,0 @@ -//! BN254 Fr field arithmetic operations. -//! -//! Low-level field arithmetic (Montgomery/Barrett reduction, scalar multiplication, -//! precomputed lookup tables). -use ark_bn254::FrConfig; -use ark_ff::{BigInt, Fp, MontConfig}; -use num_traits::Zero; - -type Fr = ark_bn254::Fr; - -/// a + b * c + carry → (result, new carry) -#[inline(always)] -fn mac_with_carry(a: u64, b: u64, c: u64, carry: &mut u64) -> u64 { - let tmp = (a as u128) + (b as u128) * (c as u128) + (*carry as u128); - *carry = (tmp >> 64) as u64; - tmp as u64 -} - -/// *a += b + carry → new carry -#[inline(always)] -fn adc(a: &mut u64, b: u64, carry: u64) -> u64 { - let tmp = (*a as u128) + (b as u128) + (carry as u128); - *a = tmp as u64; - (tmp >> 64) as u64 -} - -/// *a -= b + borrow → new borrow (1 if underflow) -#[inline(always)] -fn sbb(a: &mut u64, b: u64, borrow: u64) -> u64 { - let tmp = (1u128 << 64) + (*a as u128) - (b as u128) - (borrow as u128); - *a = tmp as u64; - u64::from(tmp >> 64 == 0) -} - -const N: usize = 4; - -const MODULUS: [u64; N] = >::MODULUS.0; -const INV: u64 = >::INV; -const R: BigInt = >::R; - -const MODULUS_HAS_SPARE_BIT: bool = MODULUS[N - 1] >> 63 == 0; -const MODULUS_NUM_SPARE_BITS: u32 = MODULUS[N - 1].leading_zeros(); - -/// 2*p as ([u64; 4], u64) — low N limbs and carry -const MODULUS_TIMES_2: ([u64; N], u64) = { - let mut lo = [0u64; N]; - let mut carry = 0u64; - let mut i = 0; - while i < N { - let doubled = (MODULUS[i] as u128) * 2 + carry as u128; - lo[i] = doubled as u64; - carry = (doubled >> 64) as u64; - i += 1; - } - (lo, carry) -}; - -/// 3*p as ([u64; 4], u64) — low N limbs and carry -const MODULUS_TIMES_3: ([u64; N], u64) = { - let (m2_lo, m2_hi) = MODULUS_TIMES_2; - let mut lo = [0u64; N]; - let mut carry = 0u64; - let mut i = 0; - while i < N { - let sum = (MODULUS[i] as u128) + (m2_lo[i] as u128) + (carry as u128); - lo[i] = sum as u64; - carry = (sum >> 64) as u64; - i += 1; - } - (lo, m2_hi + carry) -}; - -/// Barrett mu = floor(2^(N*64 + 64 - spare_bits - 1) / MODULUS) -/// -/// Computed via normalized Knuth long division. The quotient fits in a single u64. -const BARRETT_MU: u64 = { - // Dividend = 2^(319 - spare_bits). For BN254 (spare_bits=2): 2^317 - // Represented as 5 limbs: [0, 0, 0, 0, 1 << (63 - spare_bits)] - let shift = MODULUS_NUM_SPARE_BITS; - - // Normalize divisor: shift left by `shift` so MSB of top limb is set - let p_hi = if shift > 0 { - (MODULUS[3] << shift) | (MODULUS[2] >> (64 - shift)) - } else { - MODULUS[3] - }; - let p_lo = if shift > 0 { - (MODULUS[2] << shift) | (MODULUS[1] >> (64 - shift)) - } else { - MODULUS[2] - }; - - // Normalized dividend top two limbs: [1 << 63, 0] - // (original top limb 1<<(63-shift), shifted left by shift → 1<<63) - let dn4 = 1u64 << 63; - - // q_hat = floor((dn4 * 2^64) / p_hi) - let dividend_top = (dn4 as u128) << 64; - let mut q = dividend_top / (p_hi as u128); - - // Knuth refinement: while q * p_lo > remainder * 2^64, decrement q - let mut r = dividend_top - q * (p_hi as u128); - while r < (1u128 << 64) && q * (p_lo as u128) > (r << 64) { - q -= 1; - r += p_hi as u128; - } - - q as u64 -}; - -/// 16384-entry lookup table mapping small integers to their Montgomery form. -const PRECOMP_TABLE_SIZE: usize = 1 << 14; - -/// `PRECOMP_TABLE[i]` = Montgomery form of `i` for BN254 Fr. -/// -/// Uses `Fp::new()` which converts standard form → Montgomery form at compile time. -static PRECOMP_TABLE: [Fr; PRECOMP_TABLE_SIZE] = { - let mut table: [Fr; PRECOMP_TABLE_SIZE] = - [Fp::new_unchecked(BigInt([0u64; N])); PRECOMP_TABLE_SIZE]; - let mut i = 1usize; - while i < PRECOMP_TABLE_SIZE { - let mut limbs = [0u64; N]; - limbs[0] = i as u64; - table[i] = Fp::new(BigInt::new(limbs)); - i += 1; - } - table -}; - -/// Pack (low_limb, [u64; N]) into BigInt<5>: low_limb at index 0, rest at 1..5 -#[inline(always)] -fn nplus1_from_low_and_high(low: u64, high: [u64; N]) -> BigInt<5> { - let mut limbs = [0u64; 5]; - limbs[0] = low; - limbs[1] = high[0]; - limbs[2] = high[1]; - limbs[3] = high[2]; - limbs[4] = high[3]; - BigInt(limbs) -} - -/// Pack ([u64; N], high_limb) into BigInt<5>: N limbs then high_limb -#[inline(always)] -fn nplus1_from_low_n_and_top(low_n: [u64; N], top: u64) -> BigInt<5> { - let mut limbs = [0u64; 5]; - limbs[0] = low_n[0]; - limbs[1] = low_n[1]; - limbs[2] = low_n[2]; - limbs[3] = low_n[3]; - limbs[4] = top; - BigInt(limbs) -} - -/// Conditional subtraction for Barrett reduction: reduce a 5-limb intermediate -/// that is known to be < 4p down to < p (4 limbs). -#[inline(always)] -fn barrett_cond_subtract(r_tmp: BigInt<5>) -> BigInt { - let (m2_lo, _m2_hi) = MODULUS_TIMES_2; - let (m3_lo, _m3_hi) = MODULUS_TIMES_3; - - // BN254 has MODULUS_NUM_SPARE_BITS = 2, so 2p and 3p both fit in N limbs. - // This means r_tmp.0[4] == 0 for all branches below. - - let r_n: [u64; N] = [r_tmp.0[0], r_tmp.0[1], r_tmp.0[2], r_tmp.0[3]]; - - if compare_4(r_n, m2_lo) != core::cmp::Ordering::Less { - // r_tmp >= 2p - if compare_4(r_n, m3_lo) != core::cmp::Ordering::Less { - // r_tmp >= 3p → subtract 3p - BigInt(sub_4(r_n, m3_lo)) - } else { - // 2p <= r_tmp < 3p → subtract 2p - BigInt(sub_4(r_n, m2_lo)) - } - } else if compare_4(r_n, MODULUS) != core::cmp::Ordering::Less { - // p <= r_tmp < 2p → subtract p - BigInt(sub_4(r_n, MODULUS)) - } else { - // r_tmp < p → no subtraction - BigInt(r_n) - } -} - -/// Compare two 4-limb numbers (big-endian comparison) -#[inline(always)] -fn compare_4(a: [u64; N], b: [u64; N]) -> core::cmp::Ordering { - let mut i = N; - while i > 0 { - i -= 1; - if a[i] != b[i] { - return if a[i] > b[i] { - core::cmp::Ordering::Greater - } else { - core::cmp::Ordering::Less - }; - } - } - core::cmp::Ordering::Equal -} - -/// Subtract two 4-limb numbers: a - b. Caller guarantees a >= b. -#[inline(always)] -fn sub_4(a: [u64; N], b: [u64; N]) -> [u64; N] { - let mut result = a; - let mut borrow = 0u64; - borrow = sbb(&mut result[0], b[0], borrow); - borrow = sbb(&mut result[1], b[1], borrow); - borrow = sbb(&mut result[2], b[2], borrow); - let _ = sbb(&mut result[3], b[3], borrow); - result -} - -/// Barrett reduction kernel: reduce 5 limbs → 4 limbs (mod p). -/// -/// Input `c` is a BigInt<5>. Computes `c mod p` via one Barrett estimate step. -#[inline(always)] -fn barrett_reduce_5_to_4(c: BigInt<5>) -> BigInt { - // Compute tilde_c = floor(c / R') where R' = 2^modulus_bits - let tilde_c: u64 = if MODULUS_HAS_SPARE_BIT { - let high = c.0[N]; - let second_high = c.0[N - 1]; - (high << MODULUS_NUM_SPARE_BITS) + (second_high >> (64 - MODULUS_NUM_SPARE_BITS)) - } else { - c.0[N] - }; - - // Estimate m = floor(tilde_c * mu / 2^64) - let m: u64 = ((tilde_c as u128 * BARRETT_MU as u128) >> 64) as u64; - - // Compute m * 2p (result fits in 5 limbs) - let (m2p_lo, m2p_hi) = MODULUS_TIMES_2; - let mut m2p = nplus1_from_low_n_and_top(m2p_lo, m2p_hi); - // Multiply m2p by the scalar m in place - mul_bigint5_by_u64_in_place(&mut m2p, m); - - // Compute r_tmp = c - m * 2p - let mut r_tmp = c.0; - let mut borrow = 0u64; - for (r, &m) in r_tmp.iter_mut().zip(m2p.0.iter()) { - borrow = sbb(r, m, borrow); - } - debug_assert!(borrow == 0, "Borrow in Barrett c - m*2p"); - - barrett_cond_subtract(BigInt(r_tmp)) -} - -/// Multiply a BigInt<5> by a u64 scalar in place. -#[inline(always)] -fn mul_bigint5_by_u64_in_place(a: &mut BigInt<5>, b: u64) { - let mut carry = 0u64; - for limb in &mut a.0 { - let prod = (*limb as u128) * (b as u128) + (carry as u128); - *limb = prod as u64; - carry = (prod >> 64) as u64; - } - // Overflow is discarded (caller ensures result fits in 5 limbs) -} - -/// Perform N Montgomery reduction steps on a mutable buffer of L >= 2N limbs. -/// Returns carry from the final step. -#[inline(always)] -#[expect(clippy::needless_range_loop)] -fn montgomery_reduce_in_place(limbs: &mut [u64; L]) -> u64 { - debug_assert!(L >= 2 * N); - let mut carry2 = 0u64; - for i in 0..N { - let tmp = limbs[i].wrapping_mul(INV); - let mut carry = 0u64; - // Discard low word: limbs[i] + tmp * MODULUS[0] → carry only - let _ = mac_with_carry(limbs[i], tmp, MODULUS[0], &mut carry); - for j in 1..N { - let k = i + j; - limbs[k] = mac_with_carry(limbs[k], tmp, MODULUS[j], &mut carry); - } - carry2 = adc(&mut limbs[i + N], carry, carry2); - } - carry2 -} - -/// Montgomery reduce an L-limb BigInt (L >= 2N) to a field element. -/// -/// For L > 2N, first folds the tail (indices N..L) via Barrett, then runs -/// the standard N-step Montgomery REDC. -#[inline(always)] -pub(crate) fn from_montgomery_reduce(unreduced: BigInt) -> Fr { - debug_assert!(L >= 2 * N, "montgomery_reduce requires L >= 2N"); - let mut buf = unreduced.0; - - // If L > 2N, fold excess high limbs down via Barrett - if L > 2 * N { - let mut acc = BigInt::([0u64; N]); - let mut i = L; - while i > N { - i -= 1; - let c5 = nplus1_from_low_and_high(buf[i], acc.0); - acc = barrett_reduce_5_to_4(c5); - } - buf[N..N + N].copy_from_slice(&acc.0); - for slot in &mut buf[2 * N..L] { - *slot = 0; - } - } - - let carry = montgomery_reduce_in_place(&mut buf); - - let mut result_limbs = [0u64; N]; - result_limbs.copy_from_slice(&buf[N..N + N]); - let mut result = Fp::new_unchecked(BigInt::(result_limbs)); - - // Final conditional subtraction - let needs_sub = if MODULUS_HAS_SPARE_BIT { - compare_4(result.0 .0, MODULUS) != core::cmp::Ordering::Less - } else { - carry != 0 || compare_4(result.0 .0, MODULUS) != core::cmp::Ordering::Less - }; - if needs_sub { - result.0 = BigInt(sub_4(result.0 .0, MODULUS)); - } - result -} - -/// Multiply BigInt<4> by u64, producing BigInt<5>. -#[inline(always)] -fn bigint4_mul_u64(a: &BigInt, b: u64) -> BigInt<5> { - let mut res = BigInt::<5>([0u64; 5]); - let mut carry = 0u64; - for i in 0..N { - res.0[i] = mac_with_carry(0, a.0[i], b, &mut carry); - } - res.0[N] = carry; - res -} - -/// Multiply BigInt<4> by u128, producing BigInt<6>. -#[inline(always)] -fn bigint4_mul_u128(a: &BigInt, b: u128) -> BigInt<6> { - if b == 0 { - return BigInt::<6>([0u64; 6]); - } - let b_lo = b as u64; - let b_hi = (b >> 64) as u64; - - let mut res = BigInt::<6>([0u64; 6]); - - // Pass 1: res += a * b_lo - let mut carry = 0u64; - for i in 0..N { - res.0[i] = mac_with_carry(res.0[i], a.0[i], b_lo, &mut carry); - } - res.0[N] = carry; - - // Pass 2: res[1..] += a * b_hi - let mut carry2 = 0u64; - for i in 0..N { - res.0[i + 1] = mac_with_carry(res.0[i + 1], a.0[i], b_hi, &mut carry2); - } - res.0[N + 1] = carry2; - - res -} - -/// Barrett reduce BigInt<5> → Fr (N+1 → field element) -#[inline(always)] -fn from_unchecked_nplus1(element: BigInt<5>) -> Fr { - let r = barrett_reduce_5_to_4(element); - Fp::new_unchecked(r) -} - -/// Barrett reduce BigInt<6> → Fr via two rounds -#[inline(always)] -fn from_unchecked_nplus2(element: BigInt<6>) -> Fr { - // Round 1: reduce top 5 limbs (indices 1..6) - let c1 = BigInt::<5>([ - element.0[1], - element.0[2], - element.0[3], - element.0[4], - element.0[5], - ]); - let r1 = barrett_reduce_5_to_4(c1); - - // Round 2: reduce [element[0], r1] - let c2 = nplus1_from_low_and_high(element.0[0], r1.0); - let r2 = barrett_reduce_5_to_4(c2); - Fp::new_unchecked(r2) -} - -/// Multiply a field element by u64. -#[inline(always)] -pub(crate) fn mul_u64(a: Fr, b: u64) -> Fr { - if b == 0 || Zero::is_zero(&a) { - return Fr::zero(); - } - if b == 1 { - return a; - } - let prod = bigint4_mul_u64(&a.0, b); - from_unchecked_nplus1(prod) -} - -/// Multiply a field element by i64. -#[inline(always)] -pub(crate) fn mul_i64(a: Fr, b: i64) -> Fr { - let abs = b.unsigned_abs(); - let res = mul_u64(a, abs); - if b < 0 { - -res - } else { - res - } -} - -/// Multiply a field element by u128. -#[inline(always)] -pub(crate) fn mul_u128(a: Fr, b: u128) -> Fr { - if b >> 64 == 0 { - mul_u64(a, b as u64) - } else { - let prod = bigint4_mul_u128(&a.0, b); - from_unchecked_nplus2(prod) - } -} - -/// Multiply a field element by i128. -#[inline(always)] -pub(crate) fn mul_i128(a: Fr, b: i128) -> Fr { - if b == 0 || Zero::is_zero(&a) { - return Fr::zero(); - } - if b == 1 { - return a; - } - let abs = b.unsigned_abs(); - let res = if abs <= u64::MAX as u128 { - mul_u64(a, abs as u64) - } else { - let prod = bigint4_mul_u128(&a.0, abs); - from_unchecked_nplus2(prod) - }; - if b < 0 { - -res - } else { - res - } -} - -/// Convert u64 → Fr using precomp table for small values, mul_u64(R, n) otherwise. -#[inline(always)] -pub(crate) fn from_u64(n: u64) -> Fr { - if n < PRECOMP_TABLE_SIZE as u64 { - PRECOMP_TABLE[n as usize] - } else { - mul_u64(Fp::new_unchecked(R), n) - } -} - -/// Convert u128 → Fr using precomp table for small values, mul_u128(R, n) otherwise. -#[inline(always)] -pub(crate) fn from_u128(n: u128) -> Fr { - if n < PRECOMP_TABLE_SIZE as u128 { - PRECOMP_TABLE[n as usize] - } else { - mul_u128(Fp::new_unchecked(R), n) - } -} - -/// Wrap a raw BigInt<4> as Fr without any reduction (caller guarantees it's valid). -#[inline(always)] -pub(crate) fn from_bigint_unchecked(r: BigInt) -> Fr { - Fp::new_unchecked(r) -} - -#[cfg(test)] -#[expect(clippy::unwrap_used)] -mod tests { - use super::*; - use ark_ff::{PrimeField, UniformRand}; - use ark_std::test_rng; - use rand::Rng; - - #[test] - fn barrett_mu_sanity() { - assert_ne!(BARRETT_MU, 0); - } - - #[test] - fn modulus_times_2_correct() { - let (lo, hi) = MODULUS_TIMES_2; - // Verify 2*MODULUS by manual doubling - let mut expected = [0u64; N]; - let mut carry = 0u128; - for i in 0..N { - let doubled = (MODULUS[i] as u128) * 2 + carry; - expected[i] = doubled as u64; - carry = doubled >> 64; - } - assert_eq!(lo, expected); - assert_eq!(hi, carry as u64); - } - - #[test] - fn modulus_times_3_correct() { - let (lo, hi) = MODULUS_TIMES_3; - // Verify 3*MODULUS by tripling - let mut expected = [0u64; N]; - let mut carry = 0u128; - for i in 0..N { - let tripled = (MODULUS[i] as u128) * 3 + carry; - expected[i] = tripled as u64; - carry = tripled >> 64; - } - assert_eq!(lo, expected); - assert_eq!(hi, carry as u64); - } - - #[test] - fn precomp_table_spot_check() { - // PRECOMP_TABLE[i] should equal Montgomery form of i - assert_eq!(PRECOMP_TABLE[0], Fr::from(0u64)); - assert_eq!(PRECOMP_TABLE[1], Fr::from(1u64)); - assert_eq!(PRECOMP_TABLE[42], Fr::from(42u64)); - assert_eq!(PRECOMP_TABLE[16383], Fr::from(16383u64)); - } - - #[test] - fn from_u64_matches() { - let mut rng = test_rng(); - for _ in 0..200 { - let val: u64 = rng.gen(); - let expected = Fr::from(val); - let got = from_u64(val); - assert_eq!(got, expected, "from_u64 mismatch for {}", val); - } - assert_eq!(from_u64(0), Fr::from(0u64)); - assert_eq!(from_u64(1), Fr::from(1u64)); - assert_eq!(from_u64(u64::MAX), Fr::from(u64::MAX)); - } - - #[test] - fn from_u128_matches() { - let mut rng = test_rng(); - for _ in 0..200 { - let val: u128 = ((rng.gen::() as u128) << 64) | (rng.gen::() as u128); - let expected = { - let bigint = BigInt::new([val as u64, (val >> 64) as u64, 0, 0]); - Fr::from_bigint(bigint).unwrap() - }; - let got = from_u128(val); - assert_eq!(got, expected, "from_u128 mismatch for {}", val); - } - } - - #[test] - fn mul_u64_correct() { - let mut rng = test_rng(); - for _ in 0..200 { - let a = Fr::rand(&mut rng); - let b: u64 = rng.gen(); - let expected = a * Fr::from(b); - let got = mul_u64(a, b); - assert_eq!(got, expected, "mul_u64 mismatch: b={}", b); - } - // Edge cases - let a = Fr::rand(&mut rng); - assert_eq!(mul_u64(a, 0), Fr::zero()); - assert_eq!(mul_u64(a, 1), a); - } - - #[test] - fn mul_i64_correct() { - let mut rng = test_rng(); - for _ in 0..200 { - let a = Fr::rand(&mut rng); - let b: i64 = rng.gen(); - let expected = if b >= 0 { - a * Fr::from(b as u64) - } else { - -(a * Fr::from((-b) as u64)) - }; - let got = mul_i64(a, b); - assert_eq!(got, expected, "mul_i64 mismatch: b={}", b); - } - } - - #[test] - fn mul_u128_correct() { - let mut rng = test_rng(); - for _ in 0..200 { - let a = Fr::rand(&mut rng); - let b: u128 = ((rng.gen::() as u128) << 64) | (rng.gen::() as u128); - let b_fr = { - let bigint = BigInt::new([b as u64, (b >> 64) as u64, 0, 0]); - Fr::from_bigint(bigint).unwrap() - }; - let expected = a * b_fr; - let got = mul_u128(a, b); - assert_eq!(got, expected, "mul_u128 mismatch"); - } - } - - #[test] - fn mul_i128_correct() { - let mut rng = test_rng(); - for _ in 0..200 { - let a = Fr::rand(&mut rng); - let b: i128 = rng.gen(); - let abs_b = b.unsigned_abs(); - let b_fr = { - let bigint = BigInt::new([abs_b as u64, (abs_b >> 64) as u64, 0, 0]); - Fr::from_bigint(bigint).unwrap() - }; - let expected = if b >= 0 { a * b_fr } else { -(a * b_fr) }; - let got = mul_i128(a, b); - assert_eq!(got, expected, "mul_i128 mismatch"); - } - } - - #[test] - fn montgomery_reduce_roundtrip() { - let mut rng = test_rng(); - // Multiply the raw Montgomery-form BigInts: a_mont * b_mont = (aR)(bR). - // Montgomery reduce divides by R → abR = Montgomery form of a*b. - for _ in 0..200 { - let a = Fr::rand(&mut rng); - let b = Fr::rand(&mut rng); - let expected = a * b; - - // Access internal Montgomery representation directly - let a_mont = (a.0).0; - let b_mont = (b.0).0; - let mut prod = BigInt::<8>::zero(); - for (i, &ai) in a_mont.iter().enumerate() { - let mut carry = 0u64; - for (j, &bj) in b_mont.iter().enumerate() { - prod.0[i + j] = mac_with_carry(prod.0[i + j], ai, bj, &mut carry); - } - prod.0[i + N] = carry; - } - let got = from_montgomery_reduce::<8>(prod); - assert_eq!(got, expected, "Montgomery reduce roundtrip mismatch"); - } - } -} diff --git a/crates/jolt-field/src/arkworks/mod.rs b/crates/jolt-field/src/arkworks/mod.rs deleted file mode 100644 index 3014d7ce24..0000000000 --- a/crates/jolt-field/src/arkworks/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Arkworks-backed field implementations. -//! -//! Provides the BN254 scalar field (`Fr`) and its low-level arithmetic -//! (Montgomery/Barrett reduction, precomputed lookup tables, sparse multiplication). - -use crate::Limbs; -use ark_ff::BigInt; - -pub mod bn254; -pub mod bn254_fq; -pub(crate) mod bn254_ops; -pub mod montgomery_impl; -pub mod wide_accumulator; - -impl From> for BigInt { - #[inline] - fn from(limbs: Limbs) -> Self { - BigInt(limbs.0) - } -} - -impl From> for Limbs { - #[inline] - fn from(bigint: BigInt) -> Self { - Limbs(bigint.0) - } -} diff --git a/crates/jolt-field/src/arkworks/montgomery_impl.rs b/crates/jolt-field/src/arkworks/montgomery_impl.rs deleted file mode 100644 index 3945fc3813..0000000000 --- a/crates/jolt-field/src/arkworks/montgomery_impl.rs +++ /dev/null @@ -1,129 +0,0 @@ -use ark_bn254::FrConfig; -use ark_ff::MontConfig; - -use crate::{Fr, MontgomeryConstants}; - -// The u32 limbs are derived from arkworks' FrConfig u64 limbs by splitting -// each u64 into (lo, hi) u32 pairs. This matches the little-endian byte layout -// on ARM64 — the same bytes that represent [u64; 4] on CPU are read as [u32; 8] -// by the Metal shader. - -const MODULUS: [u64; 4] = >::MODULUS.0; -const R: [u64; 4] = >::R.0; -const R2: [u64; 4] = >::R2.0; -const INV64: u64 = >::INV; - -const fn u64s_to_u32s(limbs: &[u64; 4]) -> [u32; 8] { - [ - limbs[0] as u32, - (limbs[0] >> 32) as u32, - limbs[1] as u32, - (limbs[1] >> 32) as u32, - limbs[2] as u32, - (limbs[2] >> 32) as u32, - limbs[3] as u32, - (limbs[3] >> 32) as u32, - ] -} - -static MODULUS_U32: [u32; 8] = u64s_to_u32s(&MODULUS); -static R2_U32: [u32; 8] = u64s_to_u32s(&R2); -static ONE_U32: [u32; 8] = u64s_to_u32s(&R); - -/// `-r^{-1} mod 2^{32}` derived from the arkworks 64-bit `INV` value. -/// arkworks stores `-r^{-1} mod 2^{64}`; the low 32 bits give `mod 2^{32}`. -const INV32: u32 = INV64 as u32; - -impl MontgomeryConstants for Fr { - const NUM_U32_LIMBS: usize = 8; - const ACC_U32_LIMBS: usize = 18; // 2*8 + 2 - const FIELD_BYTE_SIZE: usize = 32; // 8 * 4 - - fn modulus_u32() -> &'static [u32] { - &MODULUS_U32 - } - - fn inv32() -> u32 { - INV32 - } - - fn r2_u32() -> &'static [u32] { - &R2_U32 - } - - fn one_u32() -> &'static [u32] { - &ONE_U32 - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn bn254_modulus_matches_shader() { - // These are the constants from the original bn254_fr.metal shader. - let expected: [u32; 8] = [ - 0xf000_0001, - 0x43e1_f593, - 0x79b9_7091, - 0x2833_e848, - 0x8181_585d, - 0xb850_45b6, - 0xe131_a029, - 0x3064_4e72, - ]; - assert_eq!(MODULUS_U32, expected); - } - - #[test] - fn bn254_inv32_matches_shader() { - assert_eq!(INV32, 0xefff_ffff); - } - - #[test] - fn bn254_r2_matches_shader() { - let expected: [u32; 8] = [ - 0xae21_6da7, - 0x1bb8_e645, - 0xe35c_59e3, - 0x53fe_3ab1, - 0x53bb_8085, - 0x8c49_833d, - 0x7f4e_44a5, - 0x0216_d0b1, - ]; - assert_eq!(R2_U32, expected); - } - - #[test] - fn bn254_one_matches_shader() { - let expected: [u32; 8] = [ - 0x4fff_fffb, - 0xac96_341c, - 0x9f60_cd29, - 0x36fc_7695, - 0x7879_462e, - 0x666e_a36f, - 0x9a07_df2f, - 0x0e0a_77c1, - ]; - assert_eq!(ONE_U32, expected); - } - - #[test] - fn acc_limbs_invariant() { - assert_eq!( - ::ACC_U32_LIMBS, - 2 * ::NUM_U32_LIMBS + 2 - ); - } - - #[test] - fn field_byte_size_invariant() { - assert_eq!( - ::FIELD_BYTE_SIZE, - ::NUM_U32_LIMBS * 4 - ); - } -} diff --git a/crates/jolt-field/src/arkworks/wide_accumulator.rs b/crates/jolt-field/src/arkworks/wide_accumulator.rs deleted file mode 100644 index 1ccede1f02..0000000000 --- a/crates/jolt-field/src/arkworks/wide_accumulator.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! Wide-integer accumulator for BN254 Fr deferred reduction. -//! -//! Accumulates `sum += a * b` as folded 4x4 limb products, deferring -//! carry propagation and Montgomery reduction to a single call at the end. -//! -//! # Capacity -//! -//! Each Fr element is 4 limbs (256 bits). The product of two elements is -//! accumulated into eight positional `u128` slots. Carry headroom in each -//! slot lets the hot loop avoid carry propagation until reduction. - -use crate::accumulator::Accumulator; -use crate::arkworks::bn254::Fr; -use ark_ff::BigInt; - -use super::bn254_ops; - -/// Folded 4x4 product accumulator for BN254 Fr deferred reduction. -/// -/// Stores the running sum of Montgomery-form products in positional `u128` -/// slots. Converting to a field element requires one carry propagation pass -/// and one Montgomery reduction via [`Accumulator::reduce`]. -#[derive(Clone, Copy)] -pub struct WideAccumulator { - slots: [u128; 8], -} - -impl Default for WideAccumulator { - #[inline] - fn default() -> Self { - Self { slots: [0; 8] } - } -} - -impl Accumulator for WideAccumulator { - type Element = Fr; - - #[inline(always)] - fn add(&mut self, value: Fr) { - self.fmadd(value, ::one()); - } - - #[inline(always)] - fn merge(&mut self, other: Self) { - for (lhs, rhs) in self.slots.iter_mut().zip(other.slots) { - *lhs += rhs; - } - } - - fn reduce(self) -> Fr { - // The accumulator holds Montgomery-form products and/or elements. - // Montgomery reduction divides product terms by R. - Fr::from_inner(bn254_ops::from_montgomery_reduce(self.normalize())) - } - - #[inline(always)] - fn fmadd(&mut self, a: Fr, b: Fr) { - let a = a.inner_limbs(); - let b = b.inner_limbs(); - for i in 0..4 { - for j in 0..4 { - let product = (a.0[i] as u128) * (b.0[j] as u128); - self.slots[i + j] += (product as u64) as u128; - self.slots[i + j + 1] += ((product >> 64) as u64) as u128; - } - } - } -} - -impl WideAccumulator { - #[inline] - fn normalize(self) -> BigInt<9> { - let mut out = [0u64; 9]; - let mut carry = 0u128; - for (index, slot) in self.slots.into_iter().enumerate() { - let (sum, overflow) = slot.overflowing_add(carry); - out[index] = sum as u64; - carry = (sum >> 64) + ((overflow as u128) << 64); - } - out[8] = carry as u64; - BigInt::new(out) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{Accumulator, FromPrimitiveInt}; - - #[test] - fn single_fmadd() { - let a = Fr::from_u64(7); - let b = Fr::from_u64(6); - let mut acc = WideAccumulator::default(); - acc.fmadd(a, b); - assert_eq!(acc.reduce(), Fr::from_u64(42)); - } - - #[test] - fn multiple_fmadd() { - let mut acc = WideAccumulator::default(); - acc.fmadd(Fr::from_u64(3), Fr::from_u64(4)); - acc.fmadd(Fr::from_u64(5), Fr::from_u64(6)); - // 3*4 + 5*6 = 12 + 30 = 42 - assert_eq!(acc.reduce(), Fr::from_u64(42)); - } - - #[test] - fn merge_two_accumulators() { - let mut acc1 = WideAccumulator::default(); - acc1.fmadd(Fr::from_u64(10), Fr::from_u64(10)); - - let mut acc2 = WideAccumulator::default(); - acc2.fmadd(Fr::from_u64(20), Fr::from_u64(20)); - - acc1.merge(acc2); - // 10*10 + 20*20 = 100 + 400 = 500 - assert_eq!(acc1.reduce(), Fr::from_u64(500)); - } - - #[test] - fn empty_reduces_to_zero() { - let acc = WideAccumulator::default(); - assert_eq!(acc.reduce(), Fr::from_u64(0)); - } - - #[test] - fn large_accumulation() { - let mut acc = WideAccumulator::default(); - let n = 10_000u64; - let a = Fr::from_u64(1); - let b = Fr::from_u64(1); - for _ in 0..n { - acc.fmadd(a, b); - } - assert_eq!(acc.reduce(), Fr::from_u64(n)); - } -} diff --git a/crates/jolt-field-two/src/bn254/mod.rs b/crates/jolt-field/src/bn254/mod.rs similarity index 100% rename from crates/jolt-field-two/src/bn254/mod.rs rename to crates/jolt-field/src/bn254/mod.rs diff --git a/crates/jolt-field-two/src/bn254/mont.rs b/crates/jolt-field/src/bn254/mont.rs similarity index 100% rename from crates/jolt-field-two/src/bn254/mont.rs rename to crates/jolt-field/src/bn254/mont.rs diff --git a/crates/jolt-field/src/canonical.rs b/crates/jolt-field/src/canonical.rs deleted file mode 100644 index 7d51847577..0000000000 --- a/crates/jolt-field/src/canonical.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Canonical byte representation: the Fiat-Shamir transcript surface. - -use std::fmt::Debug; -use std::hash::Hash; - -/// Fixed-size canonical little-endian byte encoding: the transcript -/// absorption surface. -/// -/// Fiat-Shamir absorption uses this explicit canonical encoding so the -/// hashed byte stream is specified independently of any serialization -/// library. Proof and wire serialization go through serde + bincode -/// instead; the two must not be conflated. -/// -/// This is deliberately the *narrow* claim, "this value has one canonical -/// byte encoding", implementable by non-field types (e.g. zero-sized -/// commitment placeholders) that must be transcript-absorbable without -/// pretending to be decodable field elements. Field types get the full -/// decode surface via [`CanonicalRepr`]. -/// -/// # Invariants -/// -/// - The encoding is injective on canonical representatives: equal values -/// produce equal bytes, distinct values produce distinct bytes. -/// - [`to_bytes_le`](Self::to_bytes_le) always writes exactly -/// [`NUM_BYTES`](Self::NUM_BYTES) bytes of the unique representative. -pub trait CanonicalBytes { - /// Byte length of the fixed-size canonical encoding. - const NUM_BYTES: usize; - - /// Writes the canonical little-endian encoding into `out`. - fn to_bytes_le(&self, out: &mut [u8]); - - /// Returns the canonical little-endian encoding as a vector. - #[inline] - fn to_bytes_le_vec(&self) -> Vec { - let mut out = vec![0u8; Self::NUM_BYTES]; - self.to_bytes_le(&mut out); - out - } -} - -/// Canonical decode-and-introspect surface of a field element: reducing -/// byte/challenge constructors and canonical-integer views, on top of the -/// [`CanonicalBytes`] encoding. -pub trait CanonicalRepr: - CanonicalBytes + Sized + Copy + Default + PartialEq + Eq + Debug + Hash + Sync + Send + 'static -{ - /// Deserializes little-endian bytes by reducing into this type. - fn from_le_bytes_mod_order(bytes: &[u8]) -> Self; - - /// Returns the canonical representative as `u64` if it fits. - fn to_canonical_u64_checked(&self) -> Option; - - /// Number of significant bits in this element's canonical representative. - /// - /// Zero is considered to have zero significant bits. - fn num_bits(&self) -> u32; - - /// Constructs a Fiat-Shamir challenge from squeezed transcript bytes. - #[inline] - fn from_challenge_bytes(bytes: &[u8]) -> Self { - Self::from_le_bytes_mod_order(bytes) - } - - /// Constructs a non-optimized scalar challenge from transcript bytes. - #[inline] - fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { - Self::from_challenge_bytes(bytes) - } -} - -#[cfg(test)] -mod tests { - #[cfg(any(feature = "bn254", feature = "akita"))] - use super::CanonicalRepr; - - /// Every scalar-challenge field must use the legacy transcript - /// convention the Blake2b transcripts squeeze against: interpret the - /// digest as a big-endian integer (reverse the bytes, then reduce - /// little-endian). A prover field and verifier field diverging here - /// surfaces as an opaque stage-claim mismatch deep in an e2e test — this - /// pins every implementation to one formula. - #[cfg(any(feature = "bn254", feature = "akita"))] - fn assert_legacy_scalar_convention() { - let mut low_byte_set = [0u8; 16]; - low_byte_set[0] = 1; - let probes: [[u8; 16]; 4] = [[0u8; 16], low_byte_set, *b"jolt-fiat-shamir", [0xff; 16]]; - for probe in probes { - let mut reversed = probe; - reversed.reverse(); - assert_eq!( - F::from_scalar_challenge_bytes(&probe), - F::from_le_bytes_mod_order(&reversed), - "scalar challenge must reduce the byte-reversed digest" - ); - } - // Direction sensitivity: an asymmetric digest must not decode the - // same unreversed, or the reversal has been silently dropped. - assert_ne!( - F::from_scalar_challenge_bytes(&low_byte_set), - F::from_le_bytes_mod_order(&low_byte_set), - "scalar challenge convention must be direction-sensitive" - ); - } - - #[cfg(feature = "bn254")] - #[test] - fn fr_uses_the_legacy_scalar_challenge_convention() { - assert_legacy_scalar_convention::(); - } - - #[cfg(feature = "bn254")] - #[test] - fn fq_uses_the_legacy_scalar_challenge_convention() { - assert_legacy_scalar_convention::(); - } - - #[cfg(feature = "akita")] - #[test] - fn akita_field_matches_the_legacy_scalar_challenge_convention() { - assert_legacy_scalar_convention::(); - } -} diff --git a/crates/jolt-field/src/ext/fp_ext2.rs b/crates/jolt-field/src/ext/fp_ext2.rs deleted file mode 100644 index 5c5eb346ad..0000000000 --- a/crates/jolt-field/src/ext/fp_ext2.rs +++ /dev/null @@ -1,549 +0,0 @@ -use super::*; - -/// `FpExt2Config` with non-residue = -1. -/// -/// Valid when `p ≡ 3 (mod 4)`, i.e. -1 is a quadratic non-residue. -pub struct NegOneNr; - -impl FpExt2Config for NegOneNr { - const IS_NEG_ONE: bool = true; - - fn non_residue() -> F { - -F::one() - } -} - -/// `FpExt2Config` with non-residue = 2. -/// -/// Valid when `p ≡ 5 (mod 8)`, i.e. 2 is a quadratic non-residue. -/// All Akita pseudo-Mersenne primes (`2^k - c` with `c ≡ 3 mod 8`) -/// satisfy this. -pub struct TwoNr; - -impl FpExt2Config for TwoNr { - fn non_residue() -> F { - F::from_u64(2) - } - - #[inline] - fn mul_non_residue(x: A, _from_base: B) -> A - where - A: Copy + Add + Sub + Mul, - B: FnOnce(F) -> A, - { - x + x - } -} - -/// Parameters for an `FpExt2` quadratic extension over base field `F`. -pub trait FpExt2Config { - /// Whether the non-residue is -1. - /// - /// When `true`, multiplication by the non-residue is a free negation and - /// the Karatsuba/squaring routines can avoid a base-field multiply. - const IS_NEG_ONE: bool = false; - - /// Non-residue `NR` such that `u^2 = NR`. - fn non_residue() -> F; - - /// Multiply a coefficient by the quadratic non-residue. - #[inline] - fn mul_non_residue(x: A, from_base: B) -> A - where - A: Copy + Add + Sub + Mul, - B: FnOnce(F) -> A, - { - if Self::IS_NEG_ONE { - from_base(F::zero()) - x - } else { - from_base(Self::non_residue()) * x - } - } -} - -/// Quadratic extension element `c0 + c1 * u` with `u^2 = NR`. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[cfg_attr( - feature = "allocative", - allocative(bound = "F: FieldCore + allocative::Allocative, C: FpExt2Config") -)] -#[repr(transparent)] -pub struct FpExt2> { - /// Coefficients `[c0, c1]` in basis `[1, u]`. - pub coeffs: [F; 2], - _cfg: PhantomData C>, -} - -impl> FpExt2 { - /// Construct `c0 + c1 * u`. - #[inline] - pub fn new(c0: F, c1: F) -> Self { - Self { - coeffs: [c0, c1], - _cfg: PhantomData, - } - } - - /// Degree-0 coefficient. - #[inline] - pub fn c0(&self) -> F { - self.coeffs[0] - } - - /// Degree-1 coefficient. - #[inline] - pub fn c1(&self) -> F { - self.coeffs[1] - } - - /// Additive identity. - #[inline] - pub fn zero() -> Self { - Self::new(F::zero(), F::zero()) - } - - /// Multiplicative identity. - #[inline] - pub fn one() -> Self { - Self::new(F::one(), F::zero()) - } - - /// Check whether this element is zero. - #[inline] - pub fn is_zero(&self) -> bool { - self.coeffs[0].is_zero() && self.coeffs[1].is_zero() - } - - /// Construct from a `u64` embedded in the base field. - #[inline] - pub fn from_u64(val: u64) -> Self - where - F: FromPrimitiveInt, - { - Self::new(F::from_u64(val), F::zero()) - } - - /// Construct from an `i64` embedded in the base field. - #[inline] - pub fn from_i64(val: i64) -> Self - where - F: FromPrimitiveInt, - { - Self::new(F::from_i64(val), F::zero()) - } - - /// Multiply a base-field element by the non-residue. - /// - /// When `IS_NEG_ONE` is true this is just a negation (no multiply). - #[inline(always)] - fn mul_nr(x: F) -> F { - C::mul_non_residue(x, |base| base) - } - - /// Return the conjugate `c0 - c1 * u`. - #[inline] - pub fn conjugate(self) -> Self { - Self::new(self.coeffs[0], -self.coeffs[1]) - } - - /// Return the norm in the base field: `c0^2 - NR * c1^2`. - #[inline] - pub fn norm(self) -> F { - (self.coeffs[0] * self.coeffs[0]) - Self::mul_nr(self.coeffs[1] * self.coeffs[1]) - } -} - -impl> std::fmt::Debug for FpExt2 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("FpExt2") - .field("coeffs", &self.coeffs) - .finish() - } -} - -impl> Clone for FpExt2 { - fn clone(&self) -> Self { - *self - } -} - -impl> Copy for FpExt2 {} - -impl> Default for FpExt2 { - fn default() -> Self { - Self::new(F::zero(), F::zero()) - } -} - -impl> PartialEq for FpExt2 { - fn eq(&self, other: &Self) -> bool { - self.coeffs[0] == other.coeffs[0] && self.coeffs[1] == other.coeffs[1] - } -} - -impl> Eq for FpExt2 {} - -impl> Add for FpExt2 { - type Output = Self; - #[inline(always)] - fn add(self, rhs: Self) -> Self::Output { - Self::new( - self.coeffs[0] + rhs.coeffs[0], - self.coeffs[1] + rhs.coeffs[1], - ) - } -} -impl> Sub for FpExt2 { - type Output = Self; - #[inline(always)] - fn sub(self, rhs: Self) -> Self::Output { - Self::new( - self.coeffs[0] - rhs.coeffs[0], - self.coeffs[1] - rhs.coeffs[1], - ) - } -} -impl> Neg for FpExt2 { - type Output = Self; - #[inline(always)] - fn neg(self) -> Self::Output { - Self::new(-self.coeffs[0], -self.coeffs[1]) - } -} -impl> AddAssign for FpExt2 { - #[inline] - fn add_assign(&mut self, rhs: Self) { - self.coeffs[0] = self.coeffs[0] + rhs.coeffs[0]; - self.coeffs[1] = self.coeffs[1] + rhs.coeffs[1]; - } -} -impl> SubAssign for FpExt2 { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - self.coeffs[0] = self.coeffs[0] - rhs.coeffs[0]; - self.coeffs[1] = self.coeffs[1] - rhs.coeffs[1]; - } -} -impl> Mul for FpExt2 { - type Output = Self; - #[inline(always)] - fn mul(self, rhs: Self) -> Self::Output { - let v0 = self.coeffs[0] * rhs.coeffs[0]; - let v1 = self.coeffs[1] * rhs.coeffs[1]; - let cross = (self.coeffs[0] + self.coeffs[1]) * (rhs.coeffs[0] + rhs.coeffs[1]); - Self::new(v0 + Self::mul_nr(v1), cross - v0 - v1) - } -} -impl> MulAssign for FpExt2 { - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl<'a, F: FieldCore, C: FpExt2Config> Add<&'a Self> for FpExt2 { - type Output = Self; - fn add(self, rhs: &'a Self) -> Self::Output { - self + *rhs - } -} -impl<'a, F: FieldCore, C: FpExt2Config> Sub<&'a Self> for FpExt2 { - type Output = Self; - fn sub(self, rhs: &'a Self) -> Self::Output { - self - *rhs - } -} -impl<'a, F: FieldCore, C: FpExt2Config> Mul<&'a Self> for FpExt2 { - type Output = Self; - fn mul(self, rhs: &'a Self) -> Self::Output { - self * *rhs - } -} - -impl> RingCore for FpExt2 { - /// Specialized squaring: 2 base-field multiplications instead of 3. - /// - /// `(c0 + c1·u)^2 = (c0^2 + NR·c1^2) + (2·c0·c1)·u` - #[inline(always)] - fn square(&self) -> Self { - let v0 = self.coeffs[0] * self.coeffs[0]; - let v1 = self.coeffs[1] * self.coeffs[1]; - Self::new( - v0 + Self::mul_nr(v1), - (self.coeffs[0] + self.coeffs[0]) * self.coeffs[1], - ) - } -} - -impl> FieldCore for FpExt2 { - fn inverse(&self) -> Option { - if self.is_zero() { - return None; - } - let inv_n = self.norm().inverse()?; - Some(Self::new(self.coeffs[0] * inv_n, (-self.coeffs[1]) * inv_n)) - } - - fn random(rng: &mut R) -> Self { - Self::new(F::random(rng), F::random(rng)) - } -} - -impl> HalvingField for FpExt2 { - #[inline] - fn half(self) -> Self { - Self::new(self.coeffs[0].half(), self.coeffs[1].half()) - } -} - -impl> FromPrimitiveInt for FpExt2 { - fn from_u64(val: u64) -> Self { - Self::from_u64(val) - } - - fn from_i64(val: i64) -> Self { - Self::from_i64(val) - } - - fn from_u128(val: u128) -> Self { - Self::new(F::from_u128(val), F::zero()) - } - - fn from_i128(val: i128) -> Self { - Self::new(F::from_i128(val), F::zero()) - } -} - -/// Identity-stub `HasUnreducedOps` for `FpExt2` variants without a dedicated -/// delayed-reduction accumulator. `ProductAccum = Self`, so every multiply -/// reduces immediately. Same pattern as `FpExt4` and -/// `FpExt8<*>`. -macro_rules! impl_fp_ext2_unreduced_identity { - ($base:ident<$p:ident: $pty:ty>) => { - impl>> HasUnreducedOps for FpExt2<$base<$p>, C> { - type MulU64Accum = Self; - type ProductAccum = Self; - - #[inline] - fn mul_u64_unreduced(self, small: u64) -> Self { - self * Self::from_u64(small) - } - #[inline] - fn mul_to_product_accum(self, other: Self) -> Self { - self * other - } - #[inline] - fn reduce_mul_u64_accum(accum: Self) -> Self { - accum - } - #[inline] - fn reduce_product_accum(accum: Self) -> Self { - accum - } - } - - impl>> MulBaseUnreduced<$base<$p>> - for FpExt2<$base<$p>, C> - { - } - }; -} - -impl_fp_ext2_unreduced_identity!(Fp32); -impl_fp_ext2_unreduced_identity!(Fp128); - -macro_rules! impl_fp_ext2_default_optimized_fold { - ($base:ident<$p:ident: $pty:ty>) => { - impl>> HasOptimizedFold for FpExt2<$base<$p>, C> { - type FoldCtx = Self; - #[inline] - fn precompute_fold(r: Self) -> Self { - r - } - #[inline] - fn fold_one(r: &Self, even: Self, odd: Self) -> Self { - even + *r * (odd - even) - } - } - }; -} - -impl_fp_ext2_default_optimized_fold!(Fp32); -impl_fp_ext2_default_optimized_fold!(Fp128); - -/// Specialized EOR fold for `FpExt2, C>`. -/// -/// Mirrors `FpExt4`: precompute the "multiply by `r`" matrix -/// once per round, then fold each pair as `even + r·(odd − even)` using -/// base-field (`u64`) products with a single delayed reduction per output -/// coordinate. Only `Fp64` bases are specialized; other bases keep the generic -/// `FpExt2` fold via `impl_fp_ext2_default_optimized_fold`. -impl>> HasOptimizedFold for FpExt2, C> { - type FoldCtx = FoldMatrixFp64; - - /// Build the 2×2 "multiply by `r`" matrix in the `[1, u]` basis. - /// - /// For `r = r0 + r1·u` and `u² = NR`, multiplying `(a0, a1)` by `r` yields - /// `(r0·a0 + NR·r1·a1, r1·a0 + r0·a1)`, i.e. the matrix - /// `[[r0, NR·r1], [r1, r0]]`. `NR·r1` is materialized once via `mul_nr` - /// (a free negation for `IS_NEG_ONE`, a doubling for the `NR = 2` preset). - #[inline] - fn precompute_fold(r: Self) -> FoldMatrixFp64 { - let r0 = r.coeffs[0]; - let r1 = r.coeffs[1]; - let nr_r1 = Self::mul_nr(r1); - FoldMatrixFp64([ - [r0.to_limbs(), nr_r1.to_limbs()], - [r1.to_limbs(), r0.to_limbs()], - ]) - } - - /// Fold one pair: `even + r·(odd − even)`. - /// - /// Each output coordinate is the sum of two `u64×u64 → u128` base products, - /// reduced once by `Fp64::reduce_sum_of_two_products`. This is the - /// schoolbook product (4 base multiplies, 2 reductions) with delayed - /// reduction, versus the generic Karatsuba multiply (3 multiplies, 3 - /// reductions). The reduced coordinates are canonical, so the result is - /// byte-identical to the generic fold. - #[inline] - fn fold_one(ctx: &FoldMatrixFp64, even: Self, odd: Self) -> Self { - let m = &ctx.0; - let d0 = (odd.coeffs[0] - even.coeffs[0]).to_limbs() as u128; - let d1 = (odd.coeffs[1] - even.coeffs[1]).to_limbs() as u128; - let c0 = - Fp64::

::reduce_sum_of_two_products((m[0][0] as u128) * d0, (m[0][1] as u128) * d1); - let c1 = - Fp64::

::reduce_sum_of_two_products((m[1][0] as u128) * d0, (m[1][1] as u128) * d1); - Self::new(even.coeffs[0] + c0, even.coeffs[1] + c1) - } -} - -/// Split `value = lo128 + hi_carry * 2^128` into base-2^64 limbs -/// `[bits 0..64, bits 64..]` for a `Fp64ProductAccum` slot pair. -/// -/// The high limb may exceed 64 bits (it carries `hi_carry` in bits 64.., which -/// is small — at most 2 here), and the accumulator's `reduce` reconstructs -/// `lo + hi * 2^64` exactly, so the full (>128-bit) coefficient survives without -/// the wrap-mod-2^128 that a single-`u128` intermediate would incur. -#[inline(always)] -fn fp64_accum_limbs(lo128: u128, hi_carry: u128) -> [u128; 2] { - [lo128 as u64 as u128, (lo128 >> 64) | (hi_carry << 64)] -} - -/// Widening `FpExt2, C>` multiplication with delayed reduction. -/// -/// Each coefficient is a combination of base products that can exceed 128 bits -/// — `c0` reaches `p00 + p^2` (IS_NEG_ONE) or `p00 + 2*p11` (just under 2^130), -/// and `c1 = p01 + p10` reaches ~2^129. Forming them in a single `u128` would -/// drop the carry into bit 128 (wrap mod 2^128), which is *not* congruent mod -/// `p` and corrupts the delayed sum. We instead track the carry explicitly and -/// store base-2^64 limbs via [`fp64_accum_limbs`], so summing a batch and -/// reducing once is exact. For `IS_NEG_ONE` configs the `p^2` bias keeps `c0` -/// non-negative (and `p^2 == 0 (mod p)`, so it is invisible after reduction). -#[inline(always)] -pub(crate) fn fp_ext2_mul_to_accum_fp64>>( - a: [Fp64

; 2], - b: [Fp64

; 2], -) -> FpExt2Fp64ProductAccum { - let p00: u128 = a[0].mul_wide(b[0]); - let p11 = a[1].mul_wide(b[1]); - let p01 = a[0].mul_wide(b[1]); - let p10 = a[1].mul_wide(b[0]); - - let [c0_lo, c0_hi] = if C::IS_NEG_ONE { - // c0 = p00 + p^2 - p11, non-negative and < 2^129. - let modulus_sq = (P as u128) * (P as u128); - let (sum, carry_add) = p00.overflowing_add(modulus_sq); - let (diff, borrow) = sum.overflowing_sub(p11); - // c0 >= 0 guarantees carry_add >= borrow, so this stays in {0, 1}. - let hi_carry = (carry_add as u128) - (borrow as u128); - fp64_accum_limbs(diff, hi_carry) - } else { - // c0 = p00 + 2*p11, < 3*p^2 < 2^130 (carry in {0, 1, 2}). - let (sum1, carry1) = p00.overflowing_add(p11); - let (sum2, carry2) = sum1.overflowing_add(p11); - let hi_carry = (carry1 as u128) + (carry2 as u128); - fp64_accum_limbs(sum2, hi_carry) - }; - // c1 = p01 + p10, < 2*p^2 < 2^129 (carry in {0, 1}). - let (c1_sum, c1_carry) = p01.overflowing_add(p10); - let [c1_lo, c1_hi] = fp64_accum_limbs(c1_sum, c1_carry as u128); - - FpExt2Fp64ProductAccum([c0_lo, c0_hi, c1_lo, c1_hi]) -} - -impl>> HasUnreducedOps for FpExt2, C> { - type MulU64Accum = AccumPair< as HasUnreducedOps>::MulU64Accum>; - type ProductAccum = FpExt2Fp64ProductAccum; - - // `fp_ext2_mul_to_accum_fp64` keeps the full >128-bit coefficient via carry-aware - // base-2^64 limbs, so summing a batch and reducing once equals per-term `Mul`. - // Covered by the `Ext2` rounds in - // `sparse_tensor_factor_matches_dense_factor_rounds`. - const DELAYED_PRODUCT_SUM_IS_EXACT: bool = true; - - #[inline] - fn mul_u64_unreduced(self, small: u64) -> Self::MulU64Accum { - AccumPair( - self.coeffs[0].mul_u64_unreduced(small), - self.coeffs[1].mul_u64_unreduced(small), - ) - } - - #[inline] - fn mul_to_product_accum(self, other: Self) -> FpExt2Fp64ProductAccum { - fp_ext2_mul_to_accum_fp64::(self.coeffs, other.coeffs) - } - - #[inline] - fn reduce_mul_u64_accum(accum: Self::MulU64Accum) -> Self { - Self::new( - Fp64::

::reduce_mul_u64_accum(accum.0), - Fp64::

::reduce_mul_u64_accum(accum.1), - ) - } - - #[inline] - fn reduce_product_accum(accum: FpExt2Fp64ProductAccum) -> Self { - let [c0, c1] = accum.reduce::

(); - Self::new(c0, c1) - } -} - -impl>> MulBaseUnreduced> for FpExt2, C> {} - -/// Default quadratic extension used by the Solinas backend tests and helpers. -pub type Ext2 = FpExt2; - -impl> serde::Serialize for FpExt2 { - fn serialize(&self, serializer: S) -> Result { - self.coeffs.serialize(serializer) - } -} - -impl<'de, F, C> serde::Deserialize<'de> for FpExt2 -where - F: FieldCore + serde::Deserialize<'de>, - C: FpExt2Config, -{ - fn deserialize>(deserializer: D) -> Result { - let [c0, c1] = <[F; 2]>::deserialize(deserializer)?; - Ok(Self::new(c0, c1)) - } -} - -use crate::native_algebra::impl_native_ring_algebra; - -impl_native_ring_algebra!( - impl[F: FieldCore, C: FpExt2Config] FpExt2 { - zero: Self::new(F::zero(), F::zero()), - is_zero(x): ::num_traits::Zero::is_zero(&x.coeffs[0]) && ::num_traits::Zero::is_zero(&x.coeffs[1]), - one: Self::new(F::one(), F::zero()), - display(x, f): write!(f, "({}, {})", x.coeffs[0], x.coeffs[1]), - hash(x, state): { - ::std::hash::Hash::hash(&x.coeffs[0], state); - ::std::hash::Hash::hash(&x.coeffs[1], state); - }, - } -); diff --git a/crates/jolt-field/src/ext/fp_ext4.rs b/crates/jolt-field/src/ext/fp_ext4.rs deleted file mode 100644 index e59a98b166..0000000000 --- a/crates/jolt-field/src/ext/fp_ext4.rs +++ /dev/null @@ -1,699 +0,0 @@ -//! Akita's only degree-4 extension field (cyclotomic ring-subfield basis). -//! -//! Coefficients are stored in the `[1, e1, e2, e3]` basis used by trace reduction -//! and production fp32 presets. - -#![expect( - clippy::expl_impl_clone_on_copy, - reason = "manual Clone avoids adding irrelevant generic Clone bounds" -)] - -use super::*; - -/// Multiply ring-subfield quartic coefficient arrays in `[1, e1, e2, e3]` basis. -#[inline] -pub(crate) fn fp_ext4_mul_coeffs(a: [A; 4], b: [A; 4]) -> [A; 4] -where - A: Copy + Add + Sub + Mul, -{ - let [a0, a1, a2, a3] = a; - let [b0, b1, b2, b3] = b; - let tail0 = a1 * b1 + a2 * b2 + a3 * b3; - [ - a0 * b0 + tail0 + tail0, - a0 * b1 + a1 * b0 + a1 * b2 + a2 * b1 + a2 * b3 + a3 * b2, - a0 * b2 + a2 * b0 + a1 * b1 + a1 * b3 + a3 * b1 - a3 * b3, - a0 * b3 + a3 * b0 + a1 * b2 + a2 * b1 - a2 * b3 - a3 * b2, - ] -} - -/// Square ring-subfield quartic coefficient arrays in `[1, e1, e2, e3]` basis. -#[inline] -pub(crate) fn fp_ext4_square_coeffs(a: [A; 4]) -> [A; 4] -where - A: Copy + Add + Sub + Mul, -{ - let [a0, a1, a2, a3] = a; - let x0 = a0; - let x1 = a2; - let y0 = a1 - a3; - let y1 = a3; - - let x0x1 = x0 * x1; - let y0y1 = y0 * y1; - let x1_square = x1 * x1; - let y1_square = y1 * y1; - let aa = (x0 * x0 + x1_square + x1_square, x0x1 + x0x1); - let bb = (y0 * y0 + y1_square + y1_square, y0y1 + y0y1); - - let v0 = x0 * y0; - let v1 = x1 * y1; - let ab = (v0 + v1 + v1, (x0 + x1) * (y0 + y1) - v0 - v1); - let constant = (bb.0 + bb.0 + bb.1 + bb.1, bb.0 + bb.1 + bb.1); - let coeff_e1 = (ab.0 + ab.0, ab.1 + ab.1); - - [ - aa.0 + constant.0, - coeff_e1.0 + coeff_e1.1, - aa.1 + constant.1, - coeff_e1.1, - ] -} - -#[inline(always)] -fn fp32_product(a: Fp32

, b: Fp32

) -> u128 { - ((a.to_limbs() as u64) * (b.to_limbs() as u64)) as u128 -} - -#[inline(always)] -fn fp32_square_product(a: Fp32

) -> u128 { - fp32_product(a, a) -} - -#[inline(always)] -fn fp32_reduce_accum(x: u128) -> Fp32

{ - Fp32::

::from_canonical_u128_reduced(x) -} - -#[inline(always)] -fn fp32_modulus_square() -> u128 { - (P as u128) * (P as u128) -} - -#[inline(always)] -fn fp32_modulus_bits() -> u32 { - 32 - P.leading_zeros() -} - -/// Backend hook for scalar ring-subfield extension multiplication (degree 4 and 8). -/// -/// The default is the generic coefficient formula. Concrete base fields can -/// override this when their representation supports fusing product sums before -/// reduction. -pub trait ExtMulBackend: FieldCore { - /// Multiply two ring-subfield coefficient arrays in `[1, e1, e2, e3]` basis. - #[inline(always)] - fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { - fp_ext4_mul_coeffs::(a, b) - } - - /// Square one ring-subfield coefficient array in `[1, e1, e2, e3]` basis. - #[inline(always)] - fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { - fp_ext4_square_coeffs::(a) - } - - /// Multiply coefficient arrays in `[1, e1, ..., e7]` basis. - #[inline(always)] - fn fp_ext8_mul(a: [Self; 8], b: [Self; 8]) -> [Self; 8] { - fp_ext8_mul_coeffs::(a, b) - } -} - -impl ExtMulBackend for Fp64

{} -impl ExtMulBackend for Fp128

{} - -impl ExtMulBackend for Fp32

{ - #[inline(always)] - fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { - let [a0, a1, a2, a3] = a; - let [b0, b1, b2, b3] = b; - let modulus_square = fp32_modulus_square::

(); - [ - fp32_reduce_accum( - fp32_product(a0, b0) - + 2 * (fp32_product(a1, b1) + fp32_product(a2, b2) + fp32_product(a3, b3)), - ), - fp32_reduce_accum( - fp32_product(a0, b1) - + fp32_product(a1, b0) - + fp32_product(a1, b2) - + fp32_product(a2, b1) - + fp32_product(a2, b3) - + fp32_product(a3, b2), - ), - fp32_reduce_accum( - fp32_product(a0, b2) - + fp32_product(a2, b0) - + fp32_product(a1, b1) - + fp32_product(a1, b3) - + fp32_product(a3, b1) - + modulus_square - - fp32_product(a3, b3), - ), - fp32_reduce_accum( - fp32_product(a0, b3) - + fp32_product(a3, b0) - + fp32_product(a1, b2) - + fp32_product(a2, b1) - + 2 * modulus_square - - fp32_product(a2, b3) - - fp32_product(a3, b2), - ), - ] - } - - #[inline(always)] - fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { - if fp32_modulus_bits::

() != 32 { - return Self::fp_ext4_mul(a, a); - } - - let [a0, a1, a2, a3] = a; - let modulus_square = fp32_modulus_square::

(); - let a0_square = fp32_square_product(a0); - let a1_square = fp32_square_product(a1); - let a2_square = fp32_square_product(a2); - let a3_square = fp32_square_product(a3); - let a0a1 = fp32_product(a0, a1); - let a0a2 = fp32_product(a0, a2); - let a0a3 = fp32_product(a0, a3); - let a1a2 = fp32_product(a1, a2); - let a1a3 = fp32_product(a1, a3); - let a2a3 = fp32_product(a2, a3); - - [ - fp32_reduce_accum(a0_square + 2 * (a1_square + a2_square + a3_square)), - fp32_reduce_accum(2 * (a0a1 + a1a2 + a2a3)), - fp32_reduce_accum(2 * a0a2 + a1_square + 2 * a1a3 + modulus_square - a3_square), - fp32_reduce_accum(2 * (a0a3 + a1a2 + modulus_square - a2a3)), - ] - } -} - -/// Widening `FpExt4>` multiplication that skips per-coefficient -/// Solinas reduction, returning `FpExt4Fp32ProductAccum` instead. -/// -/// The φ(X) ring reduction is already fused into the formulas — only the -/// base-field modular reduction is deferred. -#[inline(always)] -pub(crate) fn fp_ext4_mul_to_accum_fp32( - a: [Fp32

; 4], - b: [Fp32

; 4], -) -> FpExt4Fp32ProductAccum { - #[inline(always)] - fn product(a: Fp32

, b: Fp32

) -> u128 { - (a.to_limbs() as u128) * (b.to_limbs() as u128) - } - - let [a0, a1, a2, a3] = a; - let [b0, b1, b2, b3] = b; - let modulus_square = (P as u128) * (P as u128); - FpExt4Fp32ProductAccum([ - product(a0, b0) + 2 * (product(a1, b1) + product(a2, b2) + product(a3, b3)), - product(a0, b1) - + product(a1, b0) - + product(a1, b2) - + product(a2, b1) - + product(a2, b3) - + product(a3, b2), - product(a0, b2) - + product(a2, b0) - + product(a1, b1) - + product(a1, b3) - + product(a3, b1) - + modulus_square - - product(a3, b3), - product(a0, b3) + product(a3, b0) + product(a1, b2) + product(a2, b1) + 2 * modulus_square - - product(a2, b3) - - product(a3, b2), - ]) -} - -/// Quartic fixed-subfield element in the Akita cyclotomic basis. -/// -/// Coordinates are `[c0, c1, c2, c3]` in basis `[1, e1, e2, e3]`, where -/// `e_j = zeta^(jm) + zeta^(-jm)` for `m = D / 8` inside a compatible -/// cyclotomic ring. The scalar arithmetic is independent of the concrete ring -/// dimension `D`. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[cfg_attr( - feature = "allocative", - allocative(bound = "F: FieldCore + allocative::Allocative") -)] -#[repr(transparent)] -pub struct FpExt4 { - /// Coefficients in basis `[1, e1, e2, e3]`. - pub coeffs: [F; 4], -} - -impl FpExt4 { - /// Construct from ring-subfield basis coefficients `[c0, c1, c2, c3]`. - #[inline] - pub fn new(coeffs: [F; 4]) -> Self { - Self { coeffs } - } - - /// Additive identity. - #[inline] - pub fn zero() -> Self { - Self::new([F::zero(); 4]) - } - - /// Multiplicative identity. - #[inline] - pub fn one() -> Self { - Self::new([F::one(), F::zero(), F::zero(), F::zero()]) - } - - /// Check whether this element is zero. - #[inline] - pub fn is_zero(&self) -> bool { - self.coeffs.iter().all(|coeff| coeff.is_zero()) - } - - /// Construct from a `u64` embedded in the base field. - #[inline] - pub fn from_u64(val: u64) -> Self - where - F: FromPrimitiveInt, - { - Self::new([F::from_u64(val), F::zero(), F::zero(), F::zero()]) - } - - /// Construct from an `i64` embedded in the base field. - #[inline] - pub fn from_i64(val: i64) -> Self - where - F: FromPrimitiveInt, - { - Self::new([F::from_i64(val), F::zero(), F::zero(), F::zero()]) - } - - #[inline(always)] - fn fp_ext2_mul_by_e2_nr(lhs: (F, F), rhs: (F, F)) -> (F, F) { - let (a0, a1) = lhs; - let (b0, b1) = rhs; - let v0 = a0 * b0; - let v1 = a1 * b1; - let c1 = (a0 + a1) * (b0 + b1) - v0 - v1; - let c0 = v0 + v1 + v1; - (c0, c1) - } - - #[inline(always)] - fn fp_ext2_square_by_e2_nr(x: (F, F)) -> (F, F) { - let (a0, a1) = x; - let a0a1 = a0 * a1; - (a0.square() + a1.square() + a1.square(), a0a1 + a0a1) - } - - #[inline(always)] - fn fp_ext2_mul_by_e1_nr(x: (F, F)) -> (F, F) { - let (x0, x1) = x; - (x0 + x0 + x1 + x1, x0 + x1 + x1) - } - - #[inline(always)] - fn fp_ext2_inverse_by_e2_nr(x: (F, F)) -> Option<(F, F)> { - let (x0, x1) = x; - let inv_norm = (x0.square() - (x1.square() + x1.square())).inverse()?; - Some((x0 * inv_norm, -x1 * inv_norm)) - } -} - -impl std::fmt::Debug for FpExt4 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("FpExt4") - .field("coeffs", &self.coeffs) - .finish() - } -} - -impl Clone for FpExt4 { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for FpExt4 {} - -impl Default for FpExt4 { - fn default() -> Self { - Self::zero() - } -} - -impl PartialEq for FpExt4 { - fn eq(&self, other: &Self) -> bool { - self.coeffs == other.coeffs - } -} - -impl Eq for FpExt4 {} - -impl Add for FpExt4 { - type Output = Self; - - #[inline(always)] - fn add(self, rhs: Self) -> Self::Output { - Self::new([ - self.coeffs[0] + rhs.coeffs[0], - self.coeffs[1] + rhs.coeffs[1], - self.coeffs[2] + rhs.coeffs[2], - self.coeffs[3] + rhs.coeffs[3], - ]) - } -} - -impl Sub for FpExt4 { - type Output = Self; - - #[inline(always)] - fn sub(self, rhs: Self) -> Self::Output { - Self::new([ - self.coeffs[0] - rhs.coeffs[0], - self.coeffs[1] - rhs.coeffs[1], - self.coeffs[2] - rhs.coeffs[2], - self.coeffs[3] - rhs.coeffs[3], - ]) - } -} - -impl Neg for FpExt4 { - type Output = Self; - - #[inline(always)] - fn neg(self) -> Self::Output { - Self::new([ - -self.coeffs[0], - -self.coeffs[1], - -self.coeffs[2], - -self.coeffs[3], - ]) - } -} - -impl AddAssign for FpExt4 { - #[inline] - fn add_assign(&mut self, rhs: Self) { - self.coeffs[0] = self.coeffs[0] + rhs.coeffs[0]; - self.coeffs[1] = self.coeffs[1] + rhs.coeffs[1]; - self.coeffs[2] = self.coeffs[2] + rhs.coeffs[2]; - self.coeffs[3] = self.coeffs[3] + rhs.coeffs[3]; - } -} - -impl SubAssign for FpExt4 { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - self.coeffs[0] = self.coeffs[0] - rhs.coeffs[0]; - self.coeffs[1] = self.coeffs[1] - rhs.coeffs[1]; - self.coeffs[2] = self.coeffs[2] - rhs.coeffs[2]; - self.coeffs[3] = self.coeffs[3] - rhs.coeffs[3]; - } -} - -impl Mul for FpExt4 { - type Output = Self; - - #[inline(always)] - fn mul(self, rhs: Self) -> Self::Output { - Self::new(F::fp_ext4_mul(self.coeffs, rhs.coeffs)) - } -} - -impl MulAssign for FpExt4 { - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl<'a, F: FieldCore> Add<&'a Self> for FpExt4 { - type Output = Self; - - fn add(self, rhs: &'a Self) -> Self::Output { - self + *rhs - } -} - -impl<'a, F: FieldCore> Sub<&'a Self> for FpExt4 { - type Output = Self; - - fn sub(self, rhs: &'a Self) -> Self::Output { - self - *rhs - } -} - -impl<'a, F: ExtMulBackend> Mul<&'a Self> for FpExt4 { - type Output = Self; - - fn mul(self, rhs: &'a Self) -> Self::Output { - self * *rhs - } -} - -impl RingCore for FpExt4 { - #[inline(always)] - fn square(&self) -> Self { - Self::new(F::fp_ext4_square(self.coeffs)) - } -} - -impl FieldCore for FpExt4 { - fn random(rng: &mut R) -> Self { - Self::new(std::array::from_fn(|_| F::random(rng))) - } - - fn inverse(&self) -> Option { - if self.is_zero() { - return None; - } - - let [a0, a1, a2, a3] = self.coeffs; - let a = (a0, a2); - let b = (a1 - a3, a3); - - let aa = Self::fp_ext2_square_by_e2_nr(a); - let bb = Self::fp_ext2_square_by_e2_nr(b); - let norm = { - let nr_bb = Self::fp_ext2_mul_by_e1_nr(bb); - (aa.0 - nr_bb.0, aa.1 - nr_bb.1) - }; - let inv_norm = Self::fp_ext2_inverse_by_e2_nr(norm)?; - let constant = Self::fp_ext2_mul_by_e2_nr(a, inv_norm); - let e1_coeff = Self::fp_ext2_mul_by_e2_nr((-b.0, -b.1), inv_norm); - - Some(Self::new([ - constant.0, - e1_coeff.0 + e1_coeff.1, - constant.1, - e1_coeff.1, - ])) - } -} - -impl HalvingField for FpExt4 { - #[inline] - fn half(self) -> Self { - Self::new(std::array::from_fn(|i| self.coeffs[i].half())) - } -} - -impl FromPrimitiveInt for FpExt4 { - fn from_u64(val: u64) -> Self { - Self::from_u64(val) - } - - fn from_i64(val: i64) -> Self { - Self::from_i64(val) - } - - fn from_u128(val: u128) -> Self { - Self::new([F::from_u128(val), F::zero(), F::zero(), F::zero()]) - } - - fn from_i128(val: i128) -> Self { - Self::new([F::from_i128(val), F::zero(), F::zero(), F::zero()]) - } -} - -impl HasUnreducedOps for FpExt4> { - type MulU64Accum = Self; - type ProductAccum = FpExt4Fp32ProductAccum; - - // `fp_ext4_mul_to_accum_fp32` widens each Fp32 limb product - // (< 7·p² ≈ 2^65) into a u128 slot with no `mod 2^128` wrap, so summing a - // batch and reducing once matches per-limb reduce-then-add exactly. Covered - // by `fp_ext4_fp32_accum_summation`. - const DELAYED_PRODUCT_SUM_IS_EXACT: bool = true; - - #[inline] - fn mul_u64_unreduced(self, small: u64) -> Self::MulU64Accum { - let small = Fp32::

::from_u64(small); - Self::new(self.coeffs.map(|coeff| coeff * small)) - } - - #[inline] - fn mul_to_product_accum(self, other: Self) -> Self::ProductAccum { - fp_ext4_mul_to_accum_fp32(self.coeffs, other.coeffs) - } - - #[inline] - fn reduce_mul_u64_accum(accum: Self::MulU64Accum) -> Self { - accum - } - - #[inline] - fn reduce_product_accum(accum: Self::ProductAccum) -> Self { - Self::new(accum.reduce::

()) - } -} - -impl MulBaseUnreduced> for FpExt4> { - #[inline] - fn mul_base_to_product_accum(self, x: Fp32

) -> Self::ProductAccum { - // E × F has no cross terms: scale each base coordinate into its own - // u128 slot. Each product is `< p² < 2^62`, so a summed batch reduces - // exactly (see `DELAYED_PRODUCT_SUM_IS_EXACT`). - let x = x.to_limbs() as u128; - let [a0, a1, a2, a3] = self.coeffs; - FpExt4Fp32ProductAccum([ - (a0.to_limbs() as u128) * x, - (a1.to_limbs() as u128) * x, - (a2.to_limbs() as u128) * x, - (a3.to_limbs() as u128) * x, - ]) - } -} - -impl HasOptimizedFold for FpExt4> { - type FoldCtx = FoldMatrixFp32; - - #[inline] - fn precompute_fold(r: Self) -> FoldMatrixFp32 { - let [r0, r1, r2, r3] = r.coeffs; - let two = Fp32::

::from_u64(2); - FoldMatrixFp32([ - [ - r0.to_limbs(), - (two * r1).to_limbs(), - (two * r2).to_limbs(), - (two * r3).to_limbs(), - ], - [ - r1.to_limbs(), - (r0 + r2).to_limbs(), - (r1 + r3).to_limbs(), - r2.to_limbs(), - ], - [ - r2.to_limbs(), - (r1 + r3).to_limbs(), - r0.to_limbs(), - (r1 - r3).to_limbs(), - ], - [ - r3.to_limbs(), - r2.to_limbs(), - (r1 - r3).to_limbs(), - (r0 - r2).to_limbs(), - ], - ]) - } - - #[inline] - fn fold_one(ctx: &FoldMatrixFp32, even: Self, odd: Self) -> Self { - let m = &ctx.0; - let d: [u32; 4] = std::array::from_fn(|j| (odd.coeffs[j] - even.coeffs[j]).to_limbs()); - let folded: [Fp32

; 4] = if P < (1u32 << 31) { - // P < 2^31: each product < 2^62, sum of 4 < 2^64, fits in u64. - std::array::from_fn(|row| { - let acc: u64 = (m[row][0] as u64) * (d[0] as u64) - + (m[row][1] as u64) * (d[1] as u64) - + (m[row][2] as u64) * (d[2] as u64) - + (m[row][3] as u64) * (d[3] as u64); - Fp32::

::from_u64(acc) + even.coeffs[row] - }) - } else { - std::array::from_fn(|row| { - let acc: u128 = (m[row][0] as u128) * (d[0] as u128) - + (m[row][1] as u128) * (d[1] as u128) - + (m[row][2] as u128) * (d[2] as u128) - + (m[row][3] as u128) * (d[3] as u128); - Fp32::

::from_canonical_u128_reduced(acc) + even.coeffs[row] - }) - }; - FpExt4::new(folded) - } -} - -macro_rules! impl_fp_ext4_unreduced_identity { - ($base:ident<$p:ident: $pty:ty>) => { - impl HasUnreducedOps for FpExt4<$base<$p>> { - type MulU64Accum = Self; - type ProductAccum = Self; - - #[inline] - fn mul_u64_unreduced(self, small: u64) -> Self { - let small = $base::<$p>::from_u64(small); - Self::new(self.coeffs.map(|coeff| coeff * small)) - } - #[inline] - fn mul_to_product_accum(self, other: Self) -> Self { - self * other - } - #[inline] - fn reduce_mul_u64_accum(accum: Self) -> Self { - accum - } - #[inline] - fn reduce_product_accum(accum: Self) -> Self { - accum - } - } - - impl MulBaseUnreduced<$base<$p>> for FpExt4<$base<$p>> {} - }; -} - -impl_fp_ext4_unreduced_identity!(Fp64); -impl_fp_ext4_unreduced_identity!(Fp128); - -macro_rules! impl_fp_ext4_default_optimized_fold { - ($base:ident<$p:ident: $pty:ty>) => { - impl HasOptimizedFold for FpExt4<$base<$p>> { - type FoldCtx = Self; - #[inline] - fn precompute_fold(r: Self) -> Self { - r - } - #[inline] - fn fold_one(r: &Self, even: Self, odd: Self) -> Self { - even + *r * (odd - even) - } - } - }; -} - -impl_fp_ext4_default_optimized_fold!(Fp64); -impl_fp_ext4_default_optimized_fold!(Fp128); - -impl serde::Serialize for FpExt4 { - fn serialize(&self, serializer: S) -> Result { - self.coeffs.serialize(serializer) - } -} - -impl<'de, F: FieldCore + serde::Deserialize<'de>> serde::Deserialize<'de> for FpExt4 { - fn deserialize>(deserializer: D) -> Result { - Ok(Self::new(<[F; 4]>::deserialize(deserializer)?)) - } -} - -use crate::native_algebra::impl_native_ring_algebra; - -impl_native_ring_algebra!( - impl[F: FieldCore + ExtMulBackend] FpExt4 { - zero: Self::new([F::zero(); 4]), - is_zero(x): x.coeffs.iter().all(|c| ::num_traits::Zero::is_zero(c)), - one: Self::new([F::one(), F::zero(), F::zero(), F::zero()]), - display(x, f): write!( - f, - "({}, {}, {}, {})", - x.coeffs[0], x.coeffs[1], x.coeffs[2], x.coeffs[3] - ), - hash(x, state): ::std::hash::Hash::hash(&x.coeffs, state), - } -); diff --git a/crates/jolt-field/src/ext/fp_ext8.rs b/crates/jolt-field/src/ext/fp_ext8.rs deleted file mode 100644 index 912cd05876..0000000000 --- a/crates/jolt-field/src/ext/fp_ext8.rs +++ /dev/null @@ -1,513 +0,0 @@ -//! Akita's only degree-8 extension field (cyclotomic ring-subfield basis). -//! -//! Coefficients are stored in the Chebyshev basis `[1, e1, ..., e7]`. - -#![expect( - clippy::expl_impl_clone_on_copy, - reason = "manual Clone avoids adding irrelevant generic Clone bounds" -)] - -use super::*; - -/// Chebyshev `φ` fold-back for a degree-8 accumulator, using caller-supplied -/// add/sub so the same routine serves scalar, `i64`, and SIMD lane types. -/// -/// `φ(k)` maps a product onto the `[1, e1, ..., e7]` basis: -/// `k = 0 → 2·constant`, `1 ≤ k ≤ 7 → +e_k`, `k = 8 → 0`, -/// `9 ≤ k ≤ 15 → −e_{16−k}`. -#[inline(always)] -fn fp_ext8_add_phi( - out: &mut [V; 8], - idx: usize, - value: V, - add: &impl Fn(V, V) -> V, - sub: &impl Fn(V, V) -> V, -) { - match idx { - 0 => out[0] = add(out[0], add(value, value)), - 1..=7 => out[idx] = add(out[idx], value), - 8 => {} - 9..=15 => out[16 - idx] = sub(out[16 - idx], value), - _ => unreachable!("fp_ext8 Chebyshev index out of range"), - } -} - -/// Karatsuba schedule for `FpExt8` multiplication in the Chebyshev -/// basis, generic over a lane type `V` and its add/sub/mul. -/// -/// One schedule serves every backend: the scalar field default and the NEON / -/// AVX2 / AVX-512 SIMD kernels. The schedule is purely an additive combination -/// of products, so callers that reduce per operation (field or intrinsic ops) -/// and callers that defer reduction to the end are both correct, provided the -/// accumulator does not overflow. -#[inline(always)] -pub(crate) fn fp_ext8_mul_schedule( - a: [V; 8], - b: [V; 8], - zero: V, - add: A, - sub: S, - mul: M, -) -> [V; 8] -where - V: Copy, - A: Fn(V, V) -> V, - S: Fn(V, V) -> V, - M: Fn(V, V) -> V, -{ - let diag: [V; 8] = std::array::from_fn(|i| mul(a[i], b[i])); - let mut out = [zero; 8]; - out[0] = diag[0]; - - for k in 1..8 { - let mixed = sub(sub(mul(add(a[0], a[k]), add(b[0], b[k])), diag[0]), diag[k]); - out[k] = add(out[k], mixed); - } - - for (i, &diag_i) in diag.iter().enumerate().skip(1) { - out[0] = add(out[0], add(diag_i, diag_i)); - fp_ext8_add_phi(&mut out, i + i, diag_i, &add, &sub); - } - - for i in 1..8 { - for j in (i + 1)..8 { - let mixed = sub(sub(mul(add(a[i], a[j]), add(b[i], b[j])), diag[i]), diag[j]); - fp_ext8_add_phi(&mut out, i + j, mixed, &add, &sub); - fp_ext8_add_phi(&mut out, j - i, mixed, &add, &sub); - } - } - - out -} - -/// Squaring schedule for `FpExt8`, generic over a lane type `V`. -/// -/// Uses `(a_i + a_j)² − a_i² − a_j² = 2·a_i·a_j` to compute `a_i·a_j` directly -/// and double, saving one add and two subs per cross-term versus the Karatsuba -/// form. Shares `fp_ext8_add_phi` with [`fp_ext8_mul_schedule`]. -#[inline(always)] -pub(crate) fn fp_ext8_square_schedule( - a: [V; 8], - zero: V, - add: A, - sub: S, - mul: M, -) -> [V; 8] -where - V: Copy, - A: Fn(V, V) -> V, - S: Fn(V, V) -> V, - M: Fn(V, V) -> V, -{ - let sq: [V; 8] = std::array::from_fn(|i| mul(a[i], a[i])); - let mut out = [zero; 8]; - out[0] = sq[0]; - - for k in 1..8 { - let cross = mul(a[0], a[k]); - out[k] = add(out[k], add(cross, cross)); - } - - for (i, &sq_i) in sq.iter().enumerate().skip(1) { - out[0] = add(out[0], add(sq_i, sq_i)); - fp_ext8_add_phi(&mut out, i + i, sq_i, &add, &sub); - } - - for i in 1..8 { - for j in (i + 1)..8 { - let cross = mul(a[i], a[j]); - let doubled = add(cross, cross); - fp_ext8_add_phi(&mut out, i + j, doubled, &add, &sub); - fp_ext8_add_phi(&mut out, j - i, doubled, &add, &sub); - } - } - - out -} - -#[inline(always)] -pub(crate) fn fp_ext8_mul_coeffs(a: [F; 8], b: [F; 8]) -> [F; 8] { - fp_ext8_mul_schedule(a, b, F::zero(), |x, y| x + y, |x, y| x - y, |x, y| x * y) -} - -/// Degree-8 ring subfield element in canonical basis `[1, e1, ..., e7]`. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[cfg_attr( - feature = "allocative", - allocative(bound = "F: FieldCore + allocative::Allocative") -)] -#[repr(transparent)] -pub struct FpExt8 { - /// Coefficients in basis `[1, e1, ..., e7]`. - pub coeffs: [F; 8], -} - -impl FpExt8 { - /// Construct from canonical ring-subfield basis coefficients. - #[inline] - pub fn new(coeffs: [F; 8]) -> Self { - Self { coeffs } - } - - /// Additive identity. - #[inline] - pub fn zero() -> Self { - Self::new([F::zero(); 8]) - } - - /// Multiplicative identity. - #[inline] - pub fn one() -> Self { - Self::new(std::array::from_fn(|i| { - if i == 0 { - F::one() - } else { - F::zero() - } - })) - } - - /// Check whether this element is zero. - #[inline] - pub fn is_zero(&self) -> bool { - self.coeffs.iter().all(|coeff| coeff.is_zero()) - } - - /// Construct from a `u64` embedded in the base field. - #[inline] - pub fn from_u64(val: u64) -> Self - where - F: FromPrimitiveInt, - { - Self::new(std::array::from_fn(|i| { - if i == 0 { - F::from_u64(val) - } else { - F::zero() - } - })) - } - - /// Construct from an `i64` embedded in the base field. - #[inline] - pub fn from_i64(val: i64) -> Self - where - F: FromPrimitiveInt, - { - Self::new(std::array::from_fn(|i| { - if i == 0 { - F::from_i64(val) - } else { - F::zero() - } - })) - } -} - -impl std::fmt::Debug for FpExt8 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("FpExt8") - .field("coeffs", &self.coeffs) - .finish() - } -} - -impl Clone for FpExt8 { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for FpExt8 {} - -impl Default for FpExt8 { - fn default() -> Self { - Self::zero() - } -} - -impl PartialEq for FpExt8 { - fn eq(&self, other: &Self) -> bool { - self.coeffs == other.coeffs - } -} - -impl Eq for FpExt8 {} - -impl Add for FpExt8 { - type Output = Self; - - #[inline(always)] - fn add(self, rhs: Self) -> Self::Output { - Self::new(std::array::from_fn(|i| self.coeffs[i] + rhs.coeffs[i])) - } -} - -impl Sub for FpExt8 { - type Output = Self; - - #[inline(always)] - fn sub(self, rhs: Self) -> Self::Output { - Self::new(std::array::from_fn(|i| self.coeffs[i] - rhs.coeffs[i])) - } -} - -impl Neg for FpExt8 { - type Output = Self; - - #[inline(always)] - fn neg(self) -> Self::Output { - Self::new(std::array::from_fn(|i| -self.coeffs[i])) - } -} - -impl AddAssign for FpExt8 { - #[inline] - fn add_assign(&mut self, rhs: Self) { - for i in 0..8 { - self.coeffs[i] += rhs.coeffs[i]; - } - } -} - -impl SubAssign for FpExt8 { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - for i in 0..8 { - self.coeffs[i] -= rhs.coeffs[i]; - } - } -} - -impl Mul for FpExt8 { - type Output = Self; - - #[inline(always)] - fn mul(self, rhs: Self) -> Self::Output { - Self::new(F::fp_ext8_mul(self.coeffs, rhs.coeffs)) - } -} - -impl MulAssign for FpExt8 { - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl<'a, F: FieldCore> Add<&'a Self> for FpExt8 { - type Output = Self; - - fn add(self, rhs: &'a Self) -> Self::Output { - self + *rhs - } -} - -impl<'a, F: FieldCore> Sub<&'a Self> for FpExt8 { - type Output = Self; - - fn sub(self, rhs: &'a Self) -> Self::Output { - self - *rhs - } -} - -impl<'a, F: ExtMulBackend> Mul<&'a Self> for FpExt8 { - type Output = Self; - - fn mul(self, rhs: &'a Self) -> Self::Output { - self * *rhs - } -} - -impl RingCore for FpExt8 { - #[inline(always)] - fn square(&self) -> Self { - *self * *self - } -} - -impl FieldCore for FpExt8 { - fn random(rng: &mut R) -> Self { - Self::new(std::array::from_fn(|_| F::random(rng))) - } - - fn inverse(&self) -> Option { - if self.is_zero() { - return None; - } - - let mut aug = [[F::zero(); 9]; 8]; - for col in 0..8 { - let mut basis = [F::zero(); 8]; - basis[col] = F::one(); - let product = *self * Self::new(basis); - for (row, coeff) in product.coeffs.iter().copied().enumerate() { - aug[row][col] = coeff; - } - } - aug[0][8] = F::one(); - - for col in 0..8 { - let pivot = (col..8).find(|&row| !aug[row][col].is_zero())?; - if pivot != col { - aug.swap(col, pivot); - } - let inv = aug[col][col].inverse()?; - for entry in &mut aug[col][col..=8] { - *entry *= inv; - } - for row in 0..8 { - if row == col { - continue; - } - let factor = aug[row][col]; - if factor.is_zero() { - continue; - } - let pivot_row = aug[col]; - for (target, pivot) in aug[row][col..=8] - .iter_mut() - .zip(pivot_row[col..=8].iter().copied()) - { - *target -= factor * pivot; - } - } - } - - Some(Self::new(std::array::from_fn(|i| aug[i][8]))) - } -} - -impl HalvingField for FpExt8 { - #[inline] - fn half(self) -> Self { - Self::new(std::array::from_fn(|i| self.coeffs[i].half())) - } -} - -impl FromPrimitiveInt for FpExt8 { - fn from_u64(val: u64) -> Self { - Self::from_u64(val) - } - - fn from_i64(val: i64) -> Self { - Self::from_i64(val) - } - - fn from_u128(val: u128) -> Self { - Self::new(std::array::from_fn(|i| { - if i == 0 { - F::from_u128(val) - } else { - F::zero() - } - })) - } - - fn from_i128(val: i128) -> Self { - Self::new(std::array::from_fn(|i| { - if i == 0 { - F::from_i128(val) - } else { - F::zero() - } - })) - } -} - -macro_rules! impl_fp_ext8_unreduced_identity { - ($base:ident<$p:ident: $pty:ty>) => { - impl HasUnreducedOps for FpExt8<$base<$p>> { - type MulU64Accum = Self; - type ProductAccum = Self; - - #[inline] - fn mul_u64_unreduced(self, small: u64) -> Self { - let small = $base::<$p>::from_u64(small); - Self::new(self.coeffs.map(|coeff| coeff * small)) - } - #[inline] - fn mul_to_product_accum(self, other: Self) -> Self { - self * other - } - #[inline] - fn reduce_mul_u64_accum(accum: Self) -> Self { - accum - } - #[inline] - fn reduce_product_accum(accum: Self) -> Self { - accum - } - } - - impl MulBaseUnreduced<$base<$p>> for FpExt8<$base<$p>> {} - }; -} - -impl_fp_ext8_unreduced_identity!(Fp32); -impl_fp_ext8_unreduced_identity!(Fp64); -impl_fp_ext8_unreduced_identity!(Fp128); - -macro_rules! impl_fp_ext8_default_optimized_fold { - ($base:ident<$p:ident: $pty:ty>) => { - impl HasOptimizedFold for FpExt8<$base<$p>> { - type FoldCtx = Self; - #[inline] - fn precompute_fold(r: Self) -> Self { - r - } - #[inline] - fn fold_one(r: &Self, even: Self, odd: Self) -> Self { - even + *r * (odd - even) - } - } - }; -} - -impl_fp_ext8_default_optimized_fold!(Fp32); -impl_fp_ext8_default_optimized_fold!(Fp64); -impl_fp_ext8_default_optimized_fold!(Fp128); - -impl serde::Serialize for FpExt8 { - fn serialize(&self, serializer: S) -> Result { - self.coeffs.serialize(serializer) - } -} - -impl<'de, F: FieldCore + serde::Deserialize<'de>> serde::Deserialize<'de> for FpExt8 { - fn deserialize>(deserializer: D) -> Result { - Ok(Self::new(<[F; 8]>::deserialize(deserializer)?)) - } -} - -use crate::native_algebra::impl_native_ring_algebra; - -impl_native_ring_algebra!( - impl[F: FieldCore + ExtMulBackend] FpExt8 { - zero: Self::new([F::zero(); 8]), - is_zero(x): x.coeffs.iter().all(|c| ::num_traits::Zero::is_zero(c)), - one: Self::new([ - F::one(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - ]), - display(x, f): write!( - f, - "({}, {}, {}, {}, {}, {}, {}, {})", - x.coeffs[0], - x.coeffs[1], - x.coeffs[2], - x.coeffs[3], - x.coeffs[4], - x.coeffs[5], - x.coeffs[6], - x.coeffs[7] - ), - hash(x, state): ::std::hash::Hash::hash(&x.coeffs, state), - } -); diff --git a/crates/jolt-field/src/ext/lift.rs b/crates/jolt-field/src/ext/lift.rs deleted file mode 100644 index 19cc3f50cf..0000000000 --- a/crates/jolt-field/src/ext/lift.rs +++ /dev/null @@ -1,416 +0,0 @@ -//! The extension-field abstraction over a base field. -//! -//! [`FpExt4`] and [`FpExt8`] use the cyclotomic ring-subfield basis aligned with -//! trace reduction and production fp32 presets. - -#![expect( - clippy::expect_used, - reason = "registered pseudo-Mersenne parameters are a field-type invariant" -)] - -use crate::ext::{ExtMulBackend, FpExt2, FpExt2Config, FpExt4, FpExt8}; -use crate::unreduced::HasUnreducedOps; -use crate::{ - pseudo_mersenne_modulus, FieldCore, FieldError, FromPrimitiveInt, PseudoMersenneField, -}; - -/// An algebraic extension of base field `F`. -/// -/// Provides the extension degree, embedding of and multiplication by base -/// elements, coefficient access in the canonical basis `{1, u, u^2, ...}`, -/// and Frobenius powers. -pub trait ExtField: FieldCore + FromPrimitiveInt { - /// Extension degree: `[Self : F]`. - const EXT_DEGREE: usize; - - /// Embed `x ∈ F` as a constant in `Self`. - /// - /// This is intentionally small: for extension towers we embed into the - /// constant term. - fn lift_base(x: F) -> Self; - - /// Return `self * x`, where `x` is interpreted as a base-field scalar. - /// - /// This avoids materializing the base scalar as an extension element and - /// then using a full extension multiply. For tower extensions this scales - /// each base-field coordinate directly. - fn mul_base(self, x: F) -> Self; - - /// Construct from a coefficient slice `[c0, c1, ..., c_{d-1}]`. - /// - /// # Panics - /// Panics if `coeffs.len() != Self::EXT_DEGREE`. - fn from_base_slice(coeffs: &[F]) -> Self; - - /// Return base-field coefficients in the canonical basis. - fn to_base_vec(&self) -> Vec; - - /// Apply `x -> x^(q^power)`, where `q = |F|`. - /// - /// The provided implementations are intentionally algebraic rather than - /// basis-specific: they raise to powers of the base-field modulus. - /// Specialized extension types can add cheaper implementations later, but - /// this gives the protocol a single auditable contract first. - fn frobenius_pow(self, power: usize) -> Self; - - /// Apply the inverse Frobenius power. Since `x -> x^q` has order - /// `[Self:F]` on `Self`, this is `frobenius_pow(EXT_DEGREE - power)`. - fn frobenius_inv_pow(self, power: usize) -> Self { - let degree = Self::EXT_DEGREE; - if degree == 0 { - return self; - } - self.frobenius_pow((degree - (power % degree)) % degree) - } -} - -/// Deferred-reduction extension-times-base multiply. -/// -/// `mul_base_to_product_accum` scales `self` by a base scalar `x` and writes the -/// result into [`HasUnreducedOps::ProductAccum`] without reducing, so a batch of -/// `E × F` products can be summed and reduced once. When -/// [`HasUnreducedOps::DELAYED_PRODUCT_SUM_IS_EXACT`] holds, the reduced sum equals -/// the per-term [`ExtField::mul_base`] sum within the accumulator's headroom. -/// -/// `E × F` has no cross terms, so the default body (lift `x` and reuse -/// [`HasUnreducedOps::mul_to_product_accum`]) is correct everywhere; extensions -/// whose product-accumulator layout admits cheaper coordinate scaling override it. -pub trait MulBaseUnreduced: ExtField + HasUnreducedOps { - /// Accumulate `self * x` (extension times base scalar) without reducing. - #[inline] - fn mul_base_to_product_accum(self, x: F) -> Self::ProductAccum { - self.mul_to_product_accum(Self::lift_base(x)) - } -} - -impl MulBaseUnreduced for F {} - -#[inline] -fn field_pow_u128(mut base: E, mut exp: u128) -> E { - let mut acc = E::one(); - while exp > 0 { - if (exp & 1) == 1 { - acc *= base; - } - base *= base; - exp >>= 1; - } - acc -} - -#[inline] -fn base_modulus() -> u128 { - pseudo_mersenne_modulus(F::MODULUS_BITS, F::MODULUS_OFFSET) - .expect("pseudo-Mersenne modulus parameters must be valid") -} - -fn frobenius_pow_via_base_modulus(value: E, power: usize) -> E -where - F: PseudoMersenneField, - E: ExtField, -{ - let q = base_modulus::(); - let mut out = value; - for _ in 0..(power % E::EXT_DEGREE.max(1)) { - out = field_pow_u128(out, q); - } - out -} - -/// Return the first `width` elements of the canonical extension basis. -/// -/// For [`FpExt4`] and [`FpExt8`] this is the fixed -/// ring-subfield basis `[1, e1, ...]`, so the chosen Moore-type theta family -/// is aligned with the coefficient packing basis used by `embed_subfield`. -/// -/// # Errors -/// -/// Returns an error if `width > E::EXT_DEGREE`. -pub fn canonical_frobenius_thetas(width: usize) -> Result, FieldError> -where - F: FieldCore, - E: ExtField, -{ - if width > E::EXT_DEGREE { - return Err(FieldError::InvalidInput(format!( - "Frobenius theta width {width} exceeds extension degree {}", - E::EXT_DEGREE - ))); - } - Ok((0..width) - .map(|idx| { - let mut coeffs = vec![F::zero(); E::EXT_DEGREE]; - coeffs[idx] = F::one(); - E::from_base_slice(&coeffs) - }) - .collect()) -} - -/// Solve `M_t(theta) z = r`, where -/// `M_t(theta)_{j,h} = theta_h^(q^-j)`. -/// -/// This intentionally uses dense elimination: supported Frobenius widths are -/// tiny (`<= [E:F]`) and explicit validation is more valuable here than a -/// clever specialized solver. -/// -/// # Errors -/// -/// Returns an error if the matrix is not square, the dimensions do not match, -/// or the Moore-type matrix is singular. -pub fn solve_frobenius_moore(thetas: &[E], rhs: &[E]) -> Result, FieldError> -where - F: PseudoMersenneField, - E: ExtField, -{ - let n = thetas.len(); - if rhs.len() != n { - return Err(FieldError::InvalidSize { - expected: n, - actual: rhs.len(), - }); - } - let mut matrix = (0..n) - .map(|row| { - thetas - .iter() - .map(|&theta| theta.frobenius_inv_pow(row)) - .collect::>() - }) - .collect::>(); - let mut values = rhs.to_vec(); - - for col in 0..n { - let pivot = (col..n) - .find(|&row| !matrix[row][col].is_zero()) - .ok_or_else(|| { - FieldError::InvalidInput("singular Frobenius Moore-type matrix".to_string()) - })?; - if pivot != col { - matrix.swap(col, pivot); - values.swap(col, pivot); - } - let inv = matrix[col][col].inverse().ok_or_else(|| { - FieldError::InvalidInput("singular Frobenius Moore-type matrix".to_string()) - })?; - for entry in &mut matrix[col][col..] { - *entry *= inv; - } - values[col] *= inv; - - let pivot_tail = matrix[col][col..].to_vec(); - let pivot_value = values[col]; - for row in 0..n { - if row == col { - continue; - } - let factor = matrix[row][col]; - if factor.is_zero() { - continue; - } - for (entry, &pivot_entry) in matrix[row][col..].iter_mut().zip(pivot_tail.iter()) { - *entry -= factor * pivot_entry; - } - values[row] -= factor * pivot_value; - } - } - Ok(values) -} - -/// Validate that the canonical theta family gives a nonsingular Moore-type -/// matrix for `width`. -/// -/// # Errors -/// -/// Returns an error if theta construction fails or the Moore solve rejects. -pub fn validate_canonical_frobenius_thetas(width: usize) -> Result<(), FieldError> -where - F: PseudoMersenneField, - E: ExtField, -{ - let thetas = canonical_frobenius_thetas::(width)?; - let rhs = (0..width) - .map(|idx| E::lift_base(F::from_u64((idx + 1) as u64))) - .collect::>(); - solve_frobenius_moore::(&thetas, &rhs).map(|_| ()) -} - -impl ExtField for F { - const EXT_DEGREE: usize = 1; - - #[inline] - fn lift_base(x: F) -> Self { - x - } - - #[inline] - fn mul_base(self, x: F) -> Self { - self * x - } - - #[inline] - fn from_base_slice(coeffs: &[F]) -> Self { - assert_eq!(coeffs.len(), 1); - coeffs[0] - } - - #[inline] - fn to_base_vec(&self) -> Vec { - vec![*self] - } - - #[inline] - fn frobenius_pow(self, power: usize) -> Self { - let _ = power; - self - } -} - -impl ExtField for FpExt2 -where - F: PseudoMersenneField, - C: FpExt2Config, -{ - const EXT_DEGREE: usize = 2; - - #[inline] - fn lift_base(x: F) -> Self { - Self::new(x, F::zero()) - } - - #[inline] - fn mul_base(self, x: F) -> Self { - Self::new(self.coeffs[0] * x, self.coeffs[1] * x) - } - - #[inline] - fn from_base_slice(coeffs: &[F]) -> Self { - assert_eq!(coeffs.len(), 2); - Self::new(coeffs[0], coeffs[1]) - } - - #[inline] - fn to_base_vec(&self) -> Vec { - vec![self.coeffs[0], self.coeffs[1]] - } - - #[inline] - fn frobenius_pow(self, power: usize) -> Self { - frobenius_pow_via_base_modulus::(self, power) - } -} - -impl ExtField for FpExt4 -where - F: PseudoMersenneField + ExtMulBackend, -{ - const EXT_DEGREE: usize = 4; - - #[inline] - fn lift_base(x: F) -> Self { - Self::new([x, F::zero(), F::zero(), F::zero()]) - } - - #[inline] - fn mul_base(self, x: F) -> Self { - Self::new(std::array::from_fn(|i| self.coeffs[i] * x)) - } - - #[inline] - fn from_base_slice(coeffs: &[F]) -> Self { - assert_eq!(coeffs.len(), 4); - Self::new([coeffs[0], coeffs[1], coeffs[2], coeffs[3]]) - } - - #[inline] - fn to_base_vec(&self) -> Vec { - self.coeffs.to_vec() - } - - #[inline] - fn frobenius_pow(self, power: usize) -> Self { - frobenius_pow_via_base_modulus::(self, power) - } -} - -impl ExtField for FpExt8 -where - F: PseudoMersenneField + ExtMulBackend, -{ - const EXT_DEGREE: usize = 8; - - #[inline] - fn lift_base(x: F) -> Self { - Self::new([ - x, - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - ]) - } - - #[inline] - fn mul_base(self, x: F) -> Self { - Self::new(std::array::from_fn(|i| self.coeffs[i] * x)) - } - - #[inline] - fn from_base_slice(coeffs: &[F]) -> Self { - assert_eq!(coeffs.len(), 8); - Self::new([ - coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4], coeffs[5], coeffs[6], coeffs[7], - ]) - } - - #[inline] - fn to_base_vec(&self) -> Vec { - self.coeffs.to_vec() - } - - #[inline] - fn frobenius_pow(self, power: usize) -> Self { - frobenius_pow_via_base_modulus::(self, power) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{Fp32, NegOneNr}; - - type F = Fp32<251>; - type E2 = FpExt2; - type E4 = FpExt4; - - #[test] - fn mul_base_matches_full_multiply_for_base_field() { - let x = F::from_u64(7); - let scalar = F::from_u64(11); - - assert_eq!(x.mul_base(scalar), x * scalar); - } - - #[test] - fn mul_base_matches_full_multiply_for_fp_ext2() { - let x = E2::new(F::from_u64(3), F::from_u64(5)); - let scalar = F::from_u64(11); - - assert_eq!(x.mul_base(scalar), x * E2::lift_base(scalar)); - } - - #[test] - fn mul_base_matches_full_multiply_for_fp_ext4() { - let x = E4::new([ - F::from_u64(3), - F::from_u64(5), - F::from_u64(7), - F::from_u64(13), - ]); - let scalar = F::from_u64(11); - - assert_eq!(x.mul_base(scalar), x * E4::lift_base(scalar)); - } -} diff --git a/crates/jolt-field/src/ext/mod.rs b/crates/jolt-field/src/ext/mod.rs deleted file mode 100644 index 89695e96e9..0000000000 --- a/crates/jolt-field/src/ext/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Quadratic, quartic, and octic extension fields. -//! -//! Akita supports one concrete degree-4 and degree-8 extension over each prime -//! base field (`FpExt4`, `FpExt8`): the cyclotomic ring-subfield basis used by -//! trace reduction and production fp32 presets. There is no alternate power- or -//! tower-basis quartic implementation. - -mod fp_ext2; -mod fp_ext4; -mod fp_ext8; -pub(crate) mod lift; -#[cfg(test)] -mod tests; - -use super::prime::{Fp128, Fp32, Fp64}; -use super::unreduced::{ - AccumPair, FoldMatrixFp32, FoldMatrixFp64, FpExt2Fp64ProductAccum, FpExt4Fp32ProductAccum, - HasOptimizedFold, HasUnreducedOps, -}; -use crate::{ - CanonicalField, FieldCore, FromPrimitiveInt, HalvingField, MulBaseUnreduced, RingCore, -}; -use rand_core::RngCore; -use std::marker::PhantomData; -use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; - -pub use fp_ext2::{Ext2, FpExt2, FpExt2Config, NegOneNr, TwoNr}; -pub(crate) use fp_ext4::{fp_ext4_mul_coeffs, fp_ext4_square_coeffs}; -pub use fp_ext4::{ExtMulBackend, FpExt4}; -pub use fp_ext8::FpExt8; -pub(crate) use fp_ext8::{fp_ext8_mul_coeffs, fp_ext8_mul_schedule, fp_ext8_square_schedule}; diff --git a/crates/jolt-field/src/ext/tests.rs b/crates/jolt-field/src/ext/tests.rs deleted file mode 100644 index 51ccd8429d..0000000000 --- a/crates/jolt-field/src/ext/tests.rs +++ /dev/null @@ -1,568 +0,0 @@ -#![expect( - clippy::expect_used, - clippy::unreadable_literal, - clippy::unwrap_used, - reason = "tests assert field identities and retain copied field constants" -)] - -use super::*; -use crate::ext::lift::{ - canonical_frobenius_thetas, solve_frobenius_moore, validate_canonical_frobenius_thetas, - ExtField, -}; -use crate::Fp64; -use crate::{FieldCore, FromPrimitiveInt}; -use rand::rngs::StdRng; -use rand::SeedableRng; - -type F = Fp64<4294967197>; -type E2 = Ext2; -type E4 = FpExt4; -type R4 = FpExt4; -type R8 = FpExt8; - -#[test] -fn fp_ext2_add_sub_identity() { - let a = E2::new(F::from_u64(3), F::from_u64(5)); - let b = E2::new(F::from_u64(7), F::from_u64(11)); - let c = a + b; - assert_eq!(c - b, a); - assert_eq!(c - a, b); -} - -#[test] -fn fp_ext2_mul_one() { - let a = E2::new(F::from_u64(42), F::from_u64(13)); - assert_eq!(a * E2::one(), a); - assert_eq!(E2::one() * a, a); -} - -#[test] -fn fp_ext2_mul_commutativity() { - let mut rng = StdRng::seed_from_u64(1234); - let a = E2::random(&mut rng); - let b = E2::random(&mut rng); - assert_eq!(a * b, b * a); -} - -#[test] -fn fp_ext2_karatsuba_matches_schoolbook() { - let mut rng = StdRng::seed_from_u64(5678); - for _ in 0..100 { - let a = E2::random(&mut rng); - let b = E2::random(&mut rng); - let nr = >::non_residue(); - let expected = E2::new( - (a.coeffs[0] * b.coeffs[0]) + (nr * (a.coeffs[1] * b.coeffs[1])), - (a.coeffs[0] * b.coeffs[1]) + (a.coeffs[1] * b.coeffs[0]), - ); - assert_eq!(a * b, expected); - } -} - -#[test] -fn fp_ext2_square_matches_mul() { - let mut rng = StdRng::seed_from_u64(9012); - for _ in 0..100 { - let a = E2::random(&mut rng); - assert_eq!(a.square(), a * a, "square mismatch for {a:?}"); - } -} - -#[test] -fn fp_ext2_inv() { - let mut rng = StdRng::seed_from_u64(3456); - for _ in 0..50 { - let a = E2::random(&mut rng); - if !a.is_zero() { - let inv = a.inverse().unwrap(); - assert_eq!(a * inv, E2::one()); - } - } -} - -#[test] -fn fp_ext4_mul_commutativity() { - let mut rng = StdRng::seed_from_u64(7890); - let a = E4::random(&mut rng); - let b = E4::random(&mut rng); - assert_eq!(a * b, b * a); -} - -#[test] -fn fp_ext4_square_matches_mul() { - let mut rng = StdRng::seed_from_u64(1111); - for _ in 0..50 { - let a = E4::random(&mut rng); - assert_eq!(a.square(), a * a); - } -} - -#[test] -fn fp_ext4_inv() { - let mut rng = StdRng::seed_from_u64(2222); - for _ in 0..50 { - let a = E4::random(&mut rng); - if !a.is_zero() { - let inv = a.inverse().unwrap(); - assert_eq!(a * inv, E4::one()); - } - } -} - -#[test] -fn fp_ext4_multiplication_table() { - let two = F::from_u64(2); - let e1 = R4::new([F::zero(), F::one(), F::zero(), F::zero()]); - let e2 = R4::new([F::zero(), F::zero(), F::one(), F::zero()]); - let e3 = R4::new([F::zero(), F::zero(), F::zero(), F::one()]); - let two_const = R4::new([two, F::zero(), F::zero(), F::zero()]); - - assert_eq!(e1 * e1, two_const + e2); - assert_eq!(e1 * e2, e1 + e3); - assert_eq!(e1 * e3, e2); - assert_eq!(e2 * e2, two_const); - assert_eq!(e2 * e3, e1 - e3); - assert_eq!(e3 * e3, two_const - e2); -} - -#[test] -fn fp_ext8_multiplication_table_spot_checks() { - let two = F::from_u64(2); - let e = |idx: usize| { - R8::new(std::array::from_fn(|i| { - if i == idx { - F::one() - } else { - F::zero() - } - })) - }; - let two_const = R8::new([ - two, - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - F::zero(), - ]); - - assert_eq!(e(1) * e(1), two_const + e(2)); - assert_eq!(e(2) * e(2), two_const + e(4)); - assert_eq!(e(4) * e(4), two_const); - assert_eq!(e(7) * e(7), two_const - e(2)); - assert_eq!(e(5) * e(7), e(2) - e(4)); -} - -#[test] -fn fp_ext8_square_matches_mul() { - let mut rng = StdRng::seed_from_u64(7777); - for _ in 0..50 { - let a = R8::random(&mut rng); - assert_eq!(a.square(), a * a); - } -} - -#[test] -fn fp_ext8_inv() { - let mut rng = StdRng::seed_from_u64(8888); - for _ in 0..50 { - let a = R8::random(&mut rng); - if !a.is_zero() { - let inv = a.inverse().unwrap(); - assert_eq!(a * inv, R8::one()); - } - } -} - -#[test] -fn frobenius_fp_ext2_is_conjugation() { - let x = E2::new(F::from_u64(13), F::from_u64(21)); - assert_eq!(>::frobenius_pow(x, 0), x); - assert_eq!(>::frobenius_pow(x, 1), x.conjugate()); - assert_eq!(>::frobenius_pow(x, 2), x); - assert_eq!(>::frobenius_inv_pow(x, 1), x.conjugate()); -} - -#[test] -fn canonical_moore_thetas_solve_fp_ext2() { - validate_canonical_frobenius_thetas::(2).unwrap(); - let thetas = canonical_frobenius_thetas::(2).unwrap(); - let z = [ - E2::new(F::from_u64(3), F::from_u64(5)), - E2::new(F::from_u64(7), F::from_u64(11)), - ]; - let r = (0..2) - .map(|row| { - thetas - .iter() - .zip(z.iter()) - .fold(E2::zero(), |acc, (&theta, &z_h)| { - acc + >::frobenius_inv_pow(theta, row) * z_h - }) - }) - .collect::>(); - assert_eq!( - solve_frobenius_moore::(&thetas, &r).unwrap(), - z.to_vec() - ); -} - -#[test] -fn canonical_ring_subfield_thetas_are_the_packing_basis() { - let thetas = canonical_frobenius_thetas::(4).unwrap(); - assert_eq!( - thetas[0], - R4::new([F::one(), F::zero(), F::zero(), F::zero()]) - ); - assert_eq!( - thetas[1], - R4::new([F::zero(), F::one(), F::zero(), F::zero()]) - ); - assert_eq!( - thetas[2], - R4::new([F::zero(), F::zero(), F::one(), F::zero()]) - ); - assert_eq!( - thetas[3], - R4::new([F::zero(), F::zero(), F::zero(), F::one()]) - ); - validate_canonical_frobenius_thetas::(4).unwrap(); -} - -#[test] -fn canonical_fp_ext8_thetas_are_the_packing_basis() { - let thetas = canonical_frobenius_thetas::(8).unwrap(); - for (idx, theta) in thetas.iter().enumerate().take(8) { - assert_eq!( - *theta, - R8::new(std::array::from_fn(|i| { - if i == idx { - F::one() - } else { - F::zero() - } - })) - ); - } - validate_canonical_frobenius_thetas::(8).unwrap(); -} - -#[test] -fn duplicate_moore_theta_rejects() { - let theta = E2::one(); - let err = solve_frobenius_moore::(&[theta, theta], &[E2::one(), E2::one()]) - .expect_err("duplicate theta should be singular"); - assert!(format!("{err}").contains("singular")); -} - -#[test] -fn from_small_int_fp_ext2() { - let a = E2::from_u64(42); - assert_eq!(a, E2::new(F::from_u64(42), F::zero())); - - let b = E2::from_i64(-3); - assert_eq!(b, E2::new(F::from_i64(-3), F::zero())); - - let c = E2::from_u8(7); - assert_eq!(c, E2::from_u64(7)); - - let d = E2::from_u32(100_000); - assert_eq!(d, E2::from_u64(100_000)); -} - -#[test] -fn from_small_int_fp_ext4() { - let a = E4::from_u64(42); - assert_eq!( - a, - E4::new([F::from_u64(42), F::zero(), F::zero(), F::zero(),]) - ); - - let b = E4::from_i64(-7); - assert_eq!( - b, - E4::new([F::from_i64(-7), F::zero(), F::zero(), F::zero(),]) - ); -} - -#[test] -fn ext_field_degree() { - assert_eq!(>::EXT_DEGREE, 1); - assert_eq!(>::EXT_DEGREE, 2); - assert_eq!(>::EXT_DEGREE, 4); - assert_eq!(>::EXT_DEGREE, 4); - assert_eq!(>::EXT_DEGREE, 8); -} - -#[test] -fn ext_field_from_base_slice() { - let c0 = F::from_u64(3); - let c1 = F::from_u64(5); - let e2 = E2::from_base_slice(&[c0, c1]); - assert_eq!(e2, E2::new(c0, c1)); - - let c2 = F::from_u64(7); - let c3 = F::from_u64(11); - let e4 = E4::from_base_slice(&[c0, c1, c2, c3]); - assert_eq!(e4, E4::new([c0, c1, c2, c3])); - - let r4 = R4::from_base_slice(&[c0, c1, c2, c3]); - assert_eq!(r4, R4::new([c0, c1, c2, c3])); - - let c4 = F::from_u64(13); - let c5 = F::from_u64(17); - let c6 = F::from_u64(19); - let c7 = F::from_u64(23); - let r8 = R8::from_base_slice(&[c0, c1, c2, c3, c4, c5, c6, c7]); - assert_eq!(r8, R8::new([c0, c1, c2, c3, c4, c5, c6, c7])); -} - -#[test] -fn extension_fields_are_array_layouts() { - assert_eq!(core::mem::size_of::(), core::mem::size_of::<[F; 2]>()); - assert_eq!(core::mem::align_of::(), core::mem::align_of::<[F; 2]>()); - assert_eq!(core::mem::size_of::(), core::mem::size_of::<[F; 4]>()); - assert_eq!(core::mem::align_of::(), core::mem::align_of::<[F; 4]>()); -} - -#[test] -fn eq_impl() { - let a = E2::new(F::from_u64(1), F::from_u64(2)); - let b = E2::new(F::from_u64(1), F::from_u64(2)); - let c = E2::new(F::from_u64(1), F::from_u64(3)); - assert_eq!(a, b); - assert_ne!(a, c); -} - -#[test] -fn fp_ext4_fp32_product_accum_matches_direct_mul() { - use super::fp_ext4::fp_ext4_mul_to_accum_fp32; - use crate::unreduced::FpExt4Fp32ProductAccum; - use crate::Fp32; - use num_traits::Zero; - - type Fp = Fp32<251>; - type R4Fp32 = FpExt4; - - let mut rng = StdRng::seed_from_u64(0xACC0); - for _ in 0..200 { - let a = R4Fp32::random(&mut rng); - let b = R4Fp32::random(&mut rng); - let direct = a * b; - let accum = fp_ext4_mul_to_accum_fp32(a.coeffs, b.coeffs); - let reduced = R4Fp32::new(accum.reduce::<251>()); - assert_eq!(direct, reduced, "accum mismatch for a={a:?} b={b:?}"); - } - - let zero_accum = FpExt4Fp32ProductAccum::ZERO; - assert!(zero_accum.is_zero()); - let reduced_zero = R4Fp32::new(zero_accum.reduce::<251>()); - assert_eq!(reduced_zero, R4Fp32::zero()); -} - -#[test] -fn fp_ext4_fp32_accum_summation() { - use crate::Fp32; - use num_traits::Zero; - - type Fp = Fp32<251>; - type R4Fp32 = FpExt4; - - let mut rng = StdRng::seed_from_u64(0xACC1); - let n = 1024; - let pairs: Vec<(R4Fp32, R4Fp32)> = (0..n) - .map(|_| (R4Fp32::random(&mut rng), R4Fp32::random(&mut rng))) - .collect(); - - let direct_sum: R4Fp32 = pairs - .iter() - .map(|(a, b)| *a * *b) - .fold(R4Fp32::zero(), |s, p| s + p); - - let accum_sum = pairs.iter().fold( - ::ProductAccum::zero(), - |s, (a, b)| s + a.mul_to_product_accum(*b), - ); - let reduced = R4Fp32::reduce_product_accum(accum_sum); - - assert_eq!( - direct_sum, reduced, - "accumulated sum of {n} products mismatched" - ); -} - -#[test] -fn mul_base_to_product_accum_matches_mul_base_sum() { - use crate::{Fp32, MulBaseUnreduced}; - use num_traits::Zero; - - fn check(seed: u64) - where - Base: FieldCore, - Ext: MulBaseUnreduced + Zero, - { - let mut rng = StdRng::seed_from_u64(seed); - let n = 1024; - let pairs: Vec<(Ext, Base)> = (0..n) - .map(|_| (Ext::random(&mut rng), Base::random(&mut rng))) - .collect(); - - let direct: Ext = pairs - .iter() - .map(|(w, x)| w.mul_base(*x)) - .fold(Ext::zero(), |s, p| s + p); - - let accum = pairs.iter().fold( - ::ProductAccum::zero(), - |s, (w, x)| s + w.mul_base_to_product_accum(*x), - ); - - assert_eq!( - direct, - Ext::reduce_product_accum(accum), - "delayed base-scaling mismatch over {n} terms" - ); - } - - // fp_ext4/Fp32 takes the optimal coordinate-scaling override; fp_ext2/Fp64 - // takes the lifted default body. Both defer reduction. - check::, FpExt4>>(0xB001); - check::>(0xB002); -} - -// Regression guard for the `FpExt2` delayed-reduction accumulator. The earlier -// bug dropped the carry into bit 128 because each FpExt2 coefficient (c0 up to ~2^130, -// c1 up to ~2^129) was formed in a single `u128`. It only surfaces with near-`p` -// operands -- products around 2^128 -- which the small-modulus tests never reach, -// so these use the real 2^64-59 prime and cover both FpExt2 configs. -#[test] -fn fp_ext2_fp64_product_accum_matches_direct_mul_large_operands() { - use crate::Prime64Offset59; - - let mut rng = StdRng::seed_from_u64(0xF64A); - for _ in 0..256 { - // TwoNr (IS_NEG_ONE = false): c0 = p00 + 2*p11. - let a = Ext2::::random(&mut rng); - let b = Ext2::::random(&mut rng); - assert_eq!( - a * b, - Ext2::::reduce_product_accum(a.mul_to_product_accum(b)), - "TwoNr accum mismatch a={a:?} b={b:?}" - ); - - // NegOneNr (IS_NEG_ONE = true): c0 = p00 + p^2 - p11. - let c = FpExt2::::random(&mut rng); - let d = FpExt2::::random(&mut rng); - assert_eq!( - c * d, - FpExt2::::reduce_product_accum(c.mul_to_product_accum(d)), - "NegOneNr accum mismatch c={c:?} d={d:?}" - ); - } -} - -#[test] -fn fp_ext2_fp64_accum_summation_large_operands() { - use crate::Prime64Offset59; - use num_traits::Zero; - - type E = Ext2; - - let mut rng = StdRng::seed_from_u64(0xF64C); - let n = 1024; - let pairs: Vec<(E, E)> = (0..n) - .map(|_| (E::random(&mut rng), E::random(&mut rng))) - .collect(); - - let direct_sum: E = pairs - .iter() - .map(|(a, b)| *a * *b) - .fold(E::zero(), |s, p| s + p); - - let accum_sum = pairs - .iter() - .fold(::ProductAccum::zero(), |s, (a, b)| { - s + a.mul_to_product_accum(*b) - }); - - assert_eq!( - direct_sum, - E::reduce_product_accum(accum_sum), - "fp_ext2 accumulated sum of {n} products mismatched" - ); -} - -// The specialized `FpExt2` EOR fold must be byte-identical to the generic -// `even + r·(odd − even)`. Full-word `Prime64Offset59` exercises the -// carry-folding reduction path (products near 2^128, sum near 2^129); -// sub-word `Prime40Offset195` exercises the no-overflow path. Random operands -// reach carry=1 roughly half the time; the explicit max-coordinate cases pin -// the worst case. Covers both `FpExt2Config`s (TwoNr and NegOneNr). -#[test] -fn fp_ext2_fp64_optimized_fold_matches_generic() { - use crate::{Prime40Offset195, Prime64Offset59}; - - macro_rules! check_fold { - ($E:ty, $r:expr, $even:expr, $odd:expr) => {{ - let r: $E = $r; - let even: $E = $even; - let odd: $E = $odd; - let generic = even + r * (odd - even); - let ctx = <$E as HasOptimizedFold>::precompute_fold(r); - let optimized = <$E as HasOptimizedFold>::fold_one(&ctx, even, odd); - assert_eq!( - generic, - optimized, - "{} fold mismatch r={r:?} even={even:?} odd={odd:?}", - stringify!($E) - ); - }}; - } - - let mut rng = StdRng::seed_from_u64(0xF01D); - for _ in 0..512 { - check_fold!( - Ext2, - Ext2::random(&mut rng), - Ext2::random(&mut rng), - Ext2::random(&mut rng) - ); - check_fold!( - FpExt2, - FpExt2::random(&mut rng), - FpExt2::random(&mut rng), - FpExt2::random(&mut rng) - ); - check_fold!( - Ext2, - Ext2::random(&mut rng), - Ext2::random(&mut rng), - Ext2::random(&mut rng) - ); - check_fold!( - FpExt2, - FpExt2::random(&mut rng), - FpExt2::random(&mut rng), - FpExt2::random(&mut rng) - ); - } - - // Worst case for the full-word carry fold: all coordinates at p-1, so each - // base product is ≈ p² ≈ 2^128 and the per-coordinate sum is ≈ 2^129. - let max64 = Prime64Offset59::zero() - Prime64Offset59::one(); - check_fold!( - Ext2, - Ext2::new(max64, max64), - Ext2::zero(), - Ext2::new(max64, max64) - ); - check_fold!( - FpExt2, - FpExt2::new(max64, max64), - FpExt2::zero(), - FpExt2::new(max64, max64) - ); -} diff --git a/crates/jolt-field-two/src/extension.rs b/crates/jolt-field/src/extension.rs similarity index 100% rename from crates/jolt-field-two/src/extension.rs rename to crates/jolt-field/src/extension.rs diff --git a/crates/jolt-field/src/field.rs b/crates/jolt-field/src/field.rs deleted file mode 100644 index e77db71999..0000000000 --- a/crates/jolt-field/src/field.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::{CanonicalRepr, FieldCore, FromPrimitiveInt, WithAccumulator}; - -/// Prime field element abstraction used throughout Jolt. -/// -/// This trait provides a backend-agnostic interface over a prime-order scalar -/// field. -/// -/// All arithmetic is modular over the field's prime order. Elements are `Copy`, -/// thread-safe, and cheaply serializable. Negative integers are mapped via -/// their canonical representative modulo `p`. -pub trait Field: FieldCore + FromPrimitiveInt + CanonicalRepr + WithAccumulator {} diff --git a/crates/jolt-field/src/field_error.rs b/crates/jolt-field/src/field_error.rs deleted file mode 100644 index 39e1b0243a..0000000000 --- a/crates/jolt-field/src/field_error.rs +++ /dev/null @@ -1,16 +0,0 @@ -/// Errors produced by backend-independent field helper algorithms. -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] -pub enum FieldError { - /// A caller supplied values with an invalid shape. - #[error("invalid field input: {0}")] - InvalidInput(String), - - /// A caller supplied a slice with an unexpected length. - #[error("invalid field input size: expected {expected}, got {actual}")] - InvalidSize { - /// Required number of elements. - expected: usize, - /// Supplied number of elements. - actual: usize, - }, -} diff --git a/crates/jolt-field/src/lib.rs b/crates/jolt-field/src/lib.rs index cfaa981129..cbe61ae81c 100644 --- a/crates/jolt-field/src/lib.rs +++ b/crates/jolt-field/src/lib.rs @@ -1,102 +1,121 @@ //! Field and ring abstractions for the Jolt zkVM. //! -//! This crate exposes a slim algebraic hierarchy under Jolt's compatibility -//! [`Field`] bundle: +//! A slim algebraic ladder — [`AdditiveGroup`] → [`Ring`] → [`Field`] — with +//! orthogonal capabilities: [`CanonicalBytes`]/[`CanonicalEncoding`] (the +//! Fiat-Shamir transcript surface and the field decode surface on top of it) +//! and [`WithAccumulator`] (deferred-reduction fused multiply-add). +//! [`JoltField`] is the blanket-implemented bundle of everything Jolt's +//! protocol stack requires of a scalar field: `Field + CanonicalEncoding + +//! WithAccumulator + Serialize + DeserializeOwned`. Because the impl is a +//! blanket, no field type can forget to opt in. //! -//! ```text -//! AdditiveGroup -> RingCore -> FieldCore -//! ``` +//! # Architecture: contracts and backends //! -//! [`CanonicalRepr`] (the Fiat-Shamir transcript surface), primitive-integer -//! embedding, and accumulator support are separate capabilities so non-BN254 -//! fields and rings opt into only the surface they actually provide. Proof -//! and wire serialization use serde + bincode, never the canonical transcript -//! encoding. +//! The crate is two layers with a one-way dependency: //! -//! # Core traits +//! 1. **Contract layer** (crate root, unconditional): every trait the crate +//! defines — the spine above plus the capability contracts +//! [`PseudoMersenne`], [`ExtField`], [`Ext2Config`], [`MulBaseUnreduced`], +//! [`Unreduced`], [`Fold`], [`Packed`], [`WithPacking`] — together with +//! the stamping macros ([`impl_ring_ops!`], [`impl_group_ops!`], +//! [`impl_serde_bytes!`]) and the backend-neutral value types +//! ([`Limbs`], the [`signed`] bigint families). Contract files contain +//! trait definitions only; the crate's full capability surface is +//! readable from the root regardless of enabled features. +//! 2. **Backend layer** (feature-gated modules): implementations of the +//! contracts. Backends never reference each other and the contract layer +//! never references a backend, so a backend can be deleted or added +//! without touching the contracts. A new backend implements the spine +//! (serde and the [`JoltField`] umbrella come free via the exported +//! macros and the blanket impl) and opts into whichever capability +//! contracts it can serve. //! -//! - [`Field`] — Jolt compatibility umbrella -//! - [`Accumulator`] — deferred-reduction fused multiply-add -//! - [`MontgomeryConstants`] — Montgomery form constants for GPU backends +//! # Backends //! -//! # BN254 types (feature `bn254`) +//! - `bn254` (default): BN254 `Fr`/`Fq` wrapping arkworks, plus +//! `WideAccumulator`, a 9-limb accumulator with deferred Montgomery +//! reduction (first-party Barrett/Montgomery kernels). +//! - `solinas`: fully first-party pseudo-Mersenne fields `p = 2^k − c` — +//! `Fp32`/`Fp64` stamped from one fold algebra plus the hand-written +//! two-limb `Fp128`; cyclotomic extension towers `FpExt2`/`FpExt4`/ +//! `FpExt8` with Frobenius/Moore machinery; unreduced lane accumulators +//! and fold matrices; packed SIMD backends (NEON, AVX2, AVX-512) for +//! 32/64/128-bit lanes and packed extensions. //! -//! - [`Fr`] — BN254 scalar field element -//! - [`Fq`] — BN254 base field element -//! - [`WideAccumulator`] — 9-limb deferred Montgomery reduction +//! # Feature flags //! -//! # Solinas types (feature `solinas`) +//! - `bn254` (default) — the arkworks-backed BN254 backend. +//! - `solinas` — the pseudo-Mersenne backend (scalar, extension, unreduced, +//! packed, and the conditional-parallelism helpers in +//! `solinas::parallel`). +//! - `parallel` — activates rayon behind the `cfg_*!` helper macros. +//! - `allocative` — `Allocative` derives on the concrete field types for +//! memory profiling. //! -//! The Solinas backend provides optimized 32-, 64-, and 128-bit prime fields, -//! extension fields, packed NEON/AVX2/AVX-512 implementations, and unreduced -//! accumulators. Akita adopts these types -//! directly in its cutover to `jolt-field`. Until that cutover lands, the -//! temporary `akita` feature retains the legacy adapter for the pre-cutover -//! `akita-field` types; it is a bootstrap edge, not the target architecture, -//! and is removed in the final migration PR. +//! # Byte compatibility (hard invariants) //! -//! # Multi-precision arithmetic +//! Wire and transcript encodings are byte-identical to `jolt-field` at the +//! rebuild baseline, for both backends, so replacing that crate cannot +//! change proof bytes: //! -//! - [`Limbs`] — fixed-width limb array for unreduced arithmetic -//! - [`signed`] module — `S64`, `S128`, `S192`, `S256` and half-limb variants +//! - Proof/wire serialization is serde + bincode over canonical +//! little-endian bytes (see [`impl_serde_bytes!`]); deserialization +//! rejects non-canonical encodings uniformly via +//! [`CanonicalEncoding::from_bytes_le_checked`]. +//! - Fiat-Shamir transcript bytes use the explicit little-endian encoding +//! ([`CanonicalBytes::to_bytes_le`]) and never go through a serialization +//! library. +//! +//! Both invariants are enforced by differential tests against `jolt-field` +//! as the oracle (`tests/*_differential.rs`). -mod accumulator; #[cfg(feature = "akita")] mod akita; mod algebra; -mod canonical; -mod field; -mod field_error; -mod montgomery_constants; -#[cfg(feature = "solinas")] -mod native_algebra; - -pub use accumulator::{Accumulator, NaiveAccumulator, WithAccumulator}; -pub use algebra::{AdditiveGroup, FieldCore, FromPrimitiveInt, RingCore}; -pub use canonical::{CanonicalBytes, CanonicalRepr}; -pub use field::Field; -pub use field_error::FieldError; -pub use montgomery_constants::MontgomeryConstants; -pub use num_traits::{One, Zero}; - -pub mod limbs; -pub use limbs::Limbs; - +#[cfg(feature = "bn254")] +mod bn254; +mod extension; +mod limbs; +mod ops; +mod packed; +mod schedules; pub mod signed; - -#[cfg(feature = "solinas")] -mod ext; -#[cfg(feature = "solinas")] -pub mod packed; #[cfg(feature = "solinas")] -pub mod parallel; -#[cfg(feature = "solinas")] -mod prime; -#[cfg(feature = "solinas")] -pub mod unreduced; +pub mod solinas; +mod unreduced; -#[cfg(feature = "solinas")] -pub use ext::lift::{ - canonical_frobenius_thetas, solve_frobenius_moore, validate_canonical_frobenius_thetas, - ExtField, MulBaseUnreduced, +pub use algebra::{ + Accumulator, AdditiveGroup, CanonicalBytes, CanonicalEncoding, Field, JoltField, + NaiveAccumulator, PseudoMersenne, Ring, WithAccumulator, }; +#[cfg(feature = "bn254")] +pub use bn254::{Fq, Fr, WideAccumulator}; +pub use extension::{Ext2Config, ExtField, MulBaseUnreduced, NegOneNr, TwoNr}; +pub use limbs::Limbs; +pub use num_traits::{One, Zero}; +pub use packed::{NoPacking, Packed, WithPacking}; #[cfg(feature = "solinas")] -pub use ext::{Ext2, ExtMulBackend, FpExt2, FpExt2Config, FpExt4, FpExt8, NegOneNr, TwoNr}; -#[cfg(feature = "solinas")] -pub use prime::{ - balanced_digit_lut, is_registered_prime_offset, pseudo_mersenne_modulus, - registered_prime_offset_spec, CanonicalField, Fp128, Fp32, Fp64, HalvingField, - Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, Prime24Offset3, - Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, Prime48Offset59, - Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, PseudoMersenneField, +pub use solinas::{ + balanced_digit_lut, canonical_frobenius_thetas, is_registered_prime_offset, + pseudo_mersenne_modulus, registered_prime_offset_spec, solve_frobenius_moore, + validate_canonical_frobenius_thetas, AccumPair, Ext2, FoldMatrixFp32, FoldMatrixFp64, Fp128, + Fp128MulU64Accum, Fp128Packing, Fp128ProductAccum, Fp128x8i32, Fp32, Fp32Packing, + Fp32ProductAccum, Fp32x2i32, Fp64, Fp64Packing, Fp64ProductAccum, Fp64x4i32, FpExt2, + FpExt2Fp64ProductAccum, FpExt4, FpExt4Fp32ProductAccum, FpExt8, PackedFpExt2, PackedFpExt4, + PackedFpExt8, Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, + Prime24Offset3, Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, + Prime48Offset59, Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, PRIME_OFFSET_IMPLEMENTED_MAX_BITS, PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, }; +pub use unreduced::{Fold, Unreduced}; -#[cfg(feature = "bn254")] -pub mod arkworks; -#[cfg(feature = "bn254")] -pub use arkworks::bn254::Fr; -#[cfg(feature = "bn254")] -pub use arkworks::bn254_fq::Fq; -#[cfg(feature = "bn254")] -pub use arkworks::wide_accumulator::WideAccumulator; +/// Backend-independent input and shape failures. +#[derive(Debug, thiserror::Error)] +pub enum FieldError { + /// Invalid input parameter or value. + #[error("invalid input: {0}")] + InvalidInput(String), + /// Length mismatch between an expected and provided shape. + #[error("invalid size: expected {expected}, actual {actual}")] + InvalidSize { expected: usize, actual: usize }, +} diff --git a/crates/jolt-field/src/limbs.rs b/crates/jolt-field/src/limbs.rs index f5f35cf208..81d030a515 100644 --- a/crates/jolt-field/src/limbs.rs +++ b/crates/jolt-field/src/limbs.rs @@ -16,7 +16,7 @@ pub struct Limbs(pub [u64; N]); impl Default for Limbs { #[inline] fn default() -> Self { - Self([0u64; N]) + Self::zero() } } @@ -142,11 +142,11 @@ impl Limbs { } } - /// Fused multiply-add: `self += a * b`, keeping `N` limbs, with full carry propagation. + /// Fused multiply-add: `self += a * b`, keeping `N` limbs, with full + /// carry propagation through all higher limbs. /// - /// Unlike [`fmadd_trunc`](Self::fmadd_trunc), the carry from each row's - /// spill position is propagated through all remaining higher limbs. - /// This is required when accumulating many products to avoid silent overflow. + /// Required when accumulating many products to avoid silent overflow at + /// each row's spill position. #[inline] pub fn fmadd(&mut self, a: &Limbs, b: &Limbs) { let i_limit = if A < N { A } else { N }; @@ -173,18 +173,13 @@ impl Limbs { /// Multiply and keep only the low `N` limbs (same width as self). #[inline(always)] pub fn mul_low(&self, other: &Self) -> Self { - let mut res = Limbs::::zero(); - fm_limbs_into::(&self.0, &other.0, &mut res.0); - res + self.mul_trunc::(other) } /// Zero-extend a narrower `Limbs` into `Limbs`. #[inline] pub fn zero_extend_from(smaller: &Limbs) -> Limbs { - debug_assert!( - M <= N, - "cannot zero-extend: source has more limbs than destination" - ); + debug_assert!(M <= N, "cannot zero-extend from a wider source"); let mut limbs = [0u64; N]; let copy_len = if M < N { M } else { N }; limbs[..copy_len].copy_from_slice(&smaller.0[..copy_len]); @@ -245,13 +240,11 @@ impl core::fmt::Display for Limbs { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let mut started = false; for &limb in self.0.iter().rev() { - if !started { - if limb != 0 { - write!(f, "{limb:x}")?; - started = true; - } - } else { + if started { write!(f, "{limb:016x}")?; + } else if limb != 0 { + write!(f, "{limb:x}")?; + started = true; } } if !started { @@ -268,9 +261,8 @@ impl allocative::Allocative for Limbs { } } -/// Core schoolbook multiplication accumulator. -/// -/// Computes `acc += a[0..N] * b[0..M]`, keeping only the low `P` limbs. +/// Core schoolbook multiplication accumulator: `acc += a * b`, keeping only +/// the low `P` limbs. #[inline(always)] fn fm_limbs_into( a: &[u64; N], @@ -281,10 +273,9 @@ fn fm_limbs_into( if mul_limb == 0 { continue; } - let base = j; let mut carry = 0u64; for (i, &a_limb) in a.iter().enumerate() { - let idx = base + i; + let idx = j + i; if idx < P { let prod = (a_limb as u128) * (mul_limb as u128) + (acc[idx] as u128) + (carry as u128); @@ -292,146 +283,9 @@ fn fm_limbs_into( carry = (prod >> 64) as u64; } } - let next = base + N; + let next = j + N; if next < P { - let (v, _) = acc[next].overflowing_add(carry); - acc[next] = v; - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn mul_trunc_small() { - let a = Limbs::<1>([7u64]); - let b = Limbs::<1>([6u64]); - let c: Limbs<1> = a.mul_trunc::<1, 1>(&b); - assert_eq!(c.0[0], 42); - } - - #[test] - fn mul_trunc_wider_output() { - let a = Limbs::<1>([u64::MAX]); - let b = Limbs::<1>([2u64]); - let c: Limbs<2> = a.mul_trunc::<1, 2>(&b); - let expected = (u64::MAX as u128) * 2; - assert_eq!(c.0[0], expected as u64); - assert_eq!(c.0[1], (expected >> 64) as u64); - } - - #[test] - fn add_trunc_basic() { - let a = Limbs::<2>([u64::MAX, 0]); - let b = Limbs::<2>([1u64, 0]); - let c: Limbs<2> = a.add_trunc::<2, 2>(&b); - assert_eq!(c.0[0], 0); - assert_eq!(c.0[1], 1); - } - - #[test] - fn sub_trunc_basic() { - let a = Limbs::<2>([0, 1]); - let b = Limbs::<2>([1, 0]); - let c: Limbs<2> = a.sub_trunc::<2, 2>(&b); - assert_eq!(c.0[0], u64::MAX); - assert_eq!(c.0[1], 0); - } - - #[test] - fn zero_extend() { - let small = Limbs::<1>([42u64]); - let big: Limbs<4> = Limbs::<4>::zero_extend_from::<1>(&small); - assert_eq!(big.0[0], 42); - assert_eq!(big.0[1], 0); - assert_eq!(big.0[2], 0); - assert_eq!(big.0[3], 0); - } - - #[test] - fn mul_low_basic() { - let a = Limbs::<2>([3, 0]); - let b = Limbs::<2>([5, 0]); - let c = a.mul_low(&b); - assert_eq!(c.0[0], 15); - assert_eq!(c.0[1], 0); - } - - #[test] - fn fmadd_basic() { - let a = Limbs::<1>([3u64]); - let b = Limbs::<1>([4u64]); - let mut acc = Limbs::<2>([10, 0]); - acc.fmadd::<1, 1>(&a, &b); - assert_eq!(acc.0[0], 22); // 10 + 3*4 - } - - #[test] - fn fmadd_carry_propagation() { - // Accumulate many large products into a wide accumulator. - // Use fmadd (full carry) vs add_with_carry reference to verify correctness. - let a = Limbs::<2>([u64::MAX, u64::MAX >> 1]); - let b = Limbs::<2>([u64::MAX, u64::MAX >> 1]); - - // Compute a single product via mul_trunc as reference - let single_product: Limbs<5> = a.mul_trunc::<2, 5>(&b); - - let mut acc = Limbs::<5>::zero(); - let count = 10_000u64; - for _ in 0..count { - acc.fmadd::<2, 2>(&a, &b); - } - - // Build expected: single_product * count via repeated addition - let mut expected = Limbs::<5>::zero(); - // Multiply single_product by count using schoolbook with u64 scalar - let mut carry = 0u128; - for i in 0..5 { - let prod = (single_product.0[i] as u128) * (count as u128) + carry; - expected.0[i] = prod as u64; - carry = prod >> 64; + acc[next] = acc[next].wrapping_add(carry); } - - assert_eq!( - acc, expected, - "fmadd should match reference after {count} products" - ); - } - - #[test] - fn add_sub_with_carry_borrow() { - let mut a = Limbs::<2>([u64::MAX, 0]); - let b = Limbs::<2>([1, 0]); - let carry = a.add_with_carry(&b); - assert!(!carry); - assert_eq!(a.0[0], 0); - assert_eq!(a.0[1], 1); - - let borrow = a.sub_with_borrow(&b); - assert!(!borrow); - assert_eq!(a.0[0], u64::MAX); - assert_eq!(a.0[1], 0); - } - - #[test] - fn ordering() { - let a = Limbs::<2>([0, 1]); - let b = Limbs::<2>([u64::MAX, 0]); - assert!(a > b); - assert_eq!(a.cmp(&a), Ordering::Equal); - } - - #[test] - fn display_formatting() { - let z = Limbs::<2>([0, 0]); - assert_eq!(format!("{z}"), "0"); - - let one = Limbs::<1>([1]); - assert_eq!(format!("{one}"), "1"); - - let big = Limbs::<2>([0, 1]); - assert_eq!(format!("{big}"), "10000000000000000"); } } diff --git a/crates/jolt-field/src/montgomery_constants.rs b/crates/jolt-field/src/montgomery_constants.rs deleted file mode 100644 index b4b998a435..0000000000 --- a/crates/jolt-field/src/montgomery_constants.rs +++ /dev/null @@ -1,38 +0,0 @@ -/// Montgomery field constants -/// -/// Provides the constants needed to generate field-arithmetic shaders for any -/// Montgomery-form prime field. Values are in little-endian u32 limbs matching -/// the shader representation. -/// -/// # Safety invariants -/// -/// Implementations must guarantee the CIOS unreduced chaining property: -/// `4 * r^2 / R < 2r` where `R = 2^(32 * NUM_U32_LIMBS)`. This ensures that -/// intermediate products from `fr_mul_unreduced` remain in `[0, 2r)` and can -/// be safely fed into the next CIOS multiplication without explicit reduction. -pub trait MontgomeryConstants: 'static { - /// Number of 32-bit limbs in the Montgomery representation. - /// 4 for 128-bit fields, 8 for BN254 (256-bit). - const NUM_U32_LIMBS: usize; - - /// Number of 32-bit limbs in the wide accumulator: `2 * NUM_U32_LIMBS + 2`. - /// Provides headroom for accumulating ~2^32 unreduced products. - const ACC_U32_LIMBS: usize; - - /// Byte size of a single field element: `NUM_U32_LIMBS * 4`. - const FIELD_BYTE_SIZE: usize; - - /// The field modulus `r` as little-endian u32 limbs (`NUM_U32_LIMBS` elements). - fn modulus_u32() -> &'static [u32]; - - /// `-r^{-1} mod 2^{32}` — the Montgomery reduction constant. - fn inv32() -> u32; - - /// `R^2 mod r` as little-endian u32 limbs (`NUM_U32_LIMBS` elements), - /// where `R = 2^(32 * NUM_U32_LIMBS)`. - fn r2_u32() -> &'static [u32]; - - /// `R mod r` as little-endian u32 limbs (`NUM_U32_LIMBS` elements) — - /// the Montgomery representation of 1. - fn one_u32() -> &'static [u32]; -} diff --git a/crates/jolt-field/src/native_algebra.rs b/crates/jolt-field/src/native_algebra.rs deleted file mode 100644 index 74caf520ae..0000000000 --- a/crates/jolt-field/src/native_algebra.rs +++ /dev/null @@ -1,212 +0,0 @@ -//! Shared macros for the mechanical supertrait obligations of the algebra -//! hierarchy. -//! -//! Each macro is invoked from the concrete type's own file, so a type's full -//! trait surface stays visible where the type is defined; only the expansion -//! is shared. - -/// Implements `Zero`, `One`, `Display`, `Hash`, owned and by-reference -/// `Sum`/`Product`, and the `AdditiveGroup` marker for a ring-like type from -/// per-type leaf expressions. -macro_rules! impl_native_ring_algebra { - ( - impl[$($g:tt)*] $ty:ty { - zero: $zero:expr, - is_zero($isz:ident): $is_zero:expr, - one: $one:expr, - display($dv:ident, $f:ident): $display:expr, - hash($hv:ident, $st:ident): $hash:expr $(,)? - } - ) => { - impl<$($g)*> ::num_traits::Zero for $ty { - #[inline] - fn zero() -> Self { - $zero - } - - #[inline] - fn is_zero(&self) -> bool { - let $isz = self; - $is_zero - } - } - - impl<$($g)*> ::num_traits::One for $ty { - #[inline] - fn one() -> Self { - $one - } - } - - impl<$($g)*> ::std::fmt::Display for $ty { - fn fmt(&self, $f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - let $dv = self; - $display - } - } - - impl<$($g)*> ::std::hash::Hash for $ty { - fn hash(&self, $st: &mut JfHasher) { - let $hv = self; - $hash - } - } - - impl<$($g)*> ::std::iter::Sum for $ty { - fn sum>(iter: I) -> Self { - iter.fold(::zero(), |acc, x| acc + x) - } - } - - impl<'jf_ref, $($g)*> ::std::iter::Sum<&'jf_ref Self> for $ty { - fn sum>(iter: I) -> Self { - iter.fold(::zero(), |acc, x| acc + *x) - } - } - - impl<$($g)*> ::std::iter::Product for $ty { - fn product>(iter: I) -> Self { - iter.fold(::one(), |acc, x| acc * x) - } - } - - impl<'jf_ref, $($g)*> ::std::iter::Product<&'jf_ref Self> for $ty { - fn product>(iter: I) -> Self { - iter.fold(::one(), |acc, x| acc * *x) - } - } - - impl<$($g)*> $crate::AdditiveGroup for $ty {} - }; -} - -/// Implements `Zero`, the by-reference `Add`/`Sub` forwarders, and the -/// `AdditiveGroup` marker for wide accumulator types (no multiplication, no -/// multiplicative identity). -macro_rules! impl_native_additive { - ( - impl[$($g:tt)*] $ty:ty { - zero: $zero:expr, - is_zero($isz:ident): $is_zero:expr $(,)? - } - ) => { - impl<$($g)*> ::num_traits::Zero for $ty { - #[inline] - fn zero() -> Self { - $zero - } - - #[inline] - fn is_zero(&self) -> bool { - let $isz = self; - $is_zero - } - } - - impl<'jf_ref, $($g)*> ::std::ops::Add<&'jf_ref Self> for $ty { - type Output = Self; - - #[inline] - fn add(self, rhs: &'jf_ref Self) -> Self::Output { - self + *rhs - } - } - - impl<'jf_ref, $($g)*> ::std::ops::Sub<&'jf_ref Self> for $ty { - type Output = Self; - - #[inline] - fn sub(self, rhs: &'jf_ref Self) -> Self::Output { - self - *rhs - } - } - - impl<$($g)*> $crate::AdditiveGroup for $ty {} - }; -} - -/// Implements the value, assignment, and by-reference operator matrix for a -/// const-generic Solinas prime type by delegating to its `add_raw`/`sub_raw`/ -/// `mul_raw` kernels. The reduction logic itself stays hand-written per type. -macro_rules! impl_prime_ops { - ($ty:ident<$p:ident: $p_ty:ty>, zero_raw: $zero_raw:expr) => { - impl ::std::ops::Add for $ty<$p> { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self::Output { - Self(Self::add_raw(self.0, rhs.0)) - } - } - - impl ::std::ops::Sub for $ty<$p> { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self::Output { - Self(Self::sub_raw(self.0, rhs.0)) - } - } - - impl ::std::ops::Mul for $ty<$p> { - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self::Output { - Self(Self::mul_raw(self.0, rhs.0)) - } - } - - impl ::std::ops::Neg for $ty<$p> { - type Output = Self; - #[inline] - fn neg(self) -> Self::Output { - Self(Self::sub_raw($zero_raw, self.0)) - } - } - - impl ::std::ops::AddAssign for $ty<$p> { - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } - } - - impl ::std::ops::SubAssign for $ty<$p> { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } - } - - impl ::std::ops::MulAssign for $ty<$p> { - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } - } - - impl<'jf_ref, const $p: $p_ty> ::std::ops::Add<&'jf_ref Self> for $ty<$p> { - type Output = Self; - #[inline] - fn add(self, rhs: &'jf_ref Self) -> Self::Output { - self + *rhs - } - } - - impl<'jf_ref, const $p: $p_ty> ::std::ops::Sub<&'jf_ref Self> for $ty<$p> { - type Output = Self; - #[inline] - fn sub(self, rhs: &'jf_ref Self) -> Self::Output { - self - *rhs - } - } - - impl<'jf_ref, const $p: $p_ty> ::std::ops::Mul<&'jf_ref Self> for $ty<$p> { - type Output = Self; - #[inline] - fn mul(self, rhs: &'jf_ref Self) -> Self::Output { - self * *rhs - } - } - }; -} - -pub(crate) use {impl_native_additive, impl_native_ring_algebra, impl_prime_ops}; diff --git a/crates/jolt-field-two/src/ops.rs b/crates/jolt-field/src/ops.rs similarity index 100% rename from crates/jolt-field-two/src/ops.rs rename to crates/jolt-field/src/ops.rs diff --git a/crates/jolt-field-two/src/packed.rs b/crates/jolt-field/src/packed.rs similarity index 100% rename from crates/jolt-field-two/src/packed.rs rename to crates/jolt-field/src/packed.rs diff --git a/crates/jolt-field/src/packed/avx2/fp128.rs b/crates/jolt-field/src/packed/avx2/fp128.rs deleted file mode 100644 index a464c71db6..0000000000 --- a/crates/jolt-field/src/packed/avx2/fp128.rs +++ /dev/null @@ -1,228 +0,0 @@ -use super::*; - -/// Number of `Fp128` lanes in an AVX2 packed vector. -pub(crate) const FP128_WIDTH: usize = 4; - -/// AVX2 packed arithmetic for `Fp128

`, 4 lanes in SoA layout. -/// -/// Stores 4 elements as separate `lo` and `hi` `u64` arrays, enabling -/// vectorized add/sub via `__m256i`. Mul remains scalar per-lane. -#[derive(Clone, Copy)] -pub struct PackedFp128Avx2 { - lo: [u64; FP128_WIDTH], - hi: [u64; FP128_WIDTH], -} - -impl PackedFp128Avx2

{ - const P_LO: u64 = P as u64; - const P_HI: u64 = (P >> 64) as u64; -} - -impl Default for PackedFp128Avx2

{ - #[inline] - fn default() -> Self { - Self { - lo: [0; FP128_WIDTH], - hi: [0; FP128_WIDTH], - } - } -} - -impl fmt::Debug for PackedFp128Avx2

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let elems: Vec<_> = (0..FP128_WIDTH).map(|i| self.extract(i)).collect(); - f.debug_tuple("PackedFp128Avx2").field(&elems).finish() - } -} - -impl PartialEq for PackedFp128Avx2

{ - #[inline] - fn eq(&self, other: &Self) -> bool { - self.lo == other.lo && self.hi == other.hi - } -} - -impl Eq for PackedFp128Avx2

{} - -impl Add for PackedFp128Avx2

{ - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - unsafe { - let a_lo = _mm256_loadu_si256(self.lo.as_ptr().cast()); - let a_hi = _mm256_loadu_si256(self.hi.as_ptr().cast()); - let b_lo = _mm256_loadu_si256(rhs.lo.as_ptr().cast()); - let b_hi = _mm256_loadu_si256(rhs.hi.as_ptr().cast()); - let p_lo = _mm256_set1_epi64x(Self::P_LO as i64); - let p_hi = _mm256_set1_epi64x(Self::P_HI as i64); - let sign = _mm256_set1_epi64x(i64::MIN); - let one = _mm256_set1_epi64x(1); - - // 128-bit add with unsigned compare emulation (XOR sign bit) - let sum_lo = _mm256_add_epi64(a_lo, b_lo); - let carry_lo = - _mm256_cmpgt_epi64(_mm256_xor_si256(a_lo, sign), _mm256_xor_si256(sum_lo, sign)); - let carry_lo_bit = _mm256_and_si256(carry_lo, one); - - let hi_tmp = _mm256_add_epi64(a_hi, b_hi); - let ov1 = - _mm256_cmpgt_epi64(_mm256_xor_si256(a_hi, sign), _mm256_xor_si256(hi_tmp, sign)); - let sum_hi = _mm256_add_epi64(hi_tmp, carry_lo_bit); - let ov2 = _mm256_cmpgt_epi64( - _mm256_xor_si256(hi_tmp, sign), - _mm256_xor_si256(sum_hi, sign), - ); - let carry_128 = _mm256_or_si256(ov1, ov2); - - // 128-bit subtract P - let red_lo = _mm256_sub_epi64(sum_lo, p_lo); - let borrow_lo = - _mm256_cmpgt_epi64(_mm256_xor_si256(p_lo, sign), _mm256_xor_si256(sum_lo, sign)); - let borrow_lo_bit = _mm256_and_si256(borrow_lo, one); - - let red_hi_tmp = _mm256_sub_epi64(sum_hi, p_hi); - let bw1 = - _mm256_cmpgt_epi64(_mm256_xor_si256(p_hi, sign), _mm256_xor_si256(sum_hi, sign)); - let red_hi = _mm256_sub_epi64(red_hi_tmp, borrow_lo_bit); - let bw2 = _mm256_cmpgt_epi64( - _mm256_xor_si256(borrow_lo_bit, sign), - _mm256_xor_si256(red_hi_tmp, sign), - ); - let borrow = _mm256_or_si256(bw1, bw2); - - // use_reduced = carry_128 | !borrow - let not_borrow = _mm256_xor_si256(borrow, _mm256_set1_epi64x(-1)); - let use_reduced = _mm256_or_si256(carry_128, not_borrow); - let out_lo = _mm256_blendv_epi8(sum_lo, red_lo, use_reduced); - let out_hi = _mm256_blendv_epi8(sum_hi, red_hi, use_reduced); - - let mut result = Self::default(); - _mm256_storeu_si256(result.lo.as_mut_ptr().cast(), out_lo); - _mm256_storeu_si256(result.hi.as_mut_ptr().cast(), out_hi); - result - } - } -} - -impl Sub for PackedFp128Avx2

{ - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - unsafe { - let a_lo = _mm256_loadu_si256(self.lo.as_ptr().cast()); - let a_hi = _mm256_loadu_si256(self.hi.as_ptr().cast()); - let b_lo = _mm256_loadu_si256(rhs.lo.as_ptr().cast()); - let b_hi = _mm256_loadu_si256(rhs.hi.as_ptr().cast()); - let p_lo = _mm256_set1_epi64x(Self::P_LO as i64); - let p_hi = _mm256_set1_epi64x(Self::P_HI as i64); - let sign = _mm256_set1_epi64x(i64::MIN); - let one = _mm256_set1_epi64x(1); - - // 128-bit sub - let diff_lo = _mm256_sub_epi64(a_lo, b_lo); - let borrow_lo = - _mm256_cmpgt_epi64(_mm256_xor_si256(b_lo, sign), _mm256_xor_si256(a_lo, sign)); - let borrow_lo_bit = _mm256_and_si256(borrow_lo, one); - - let hi_tmp = _mm256_sub_epi64(a_hi, b_hi); - let bw1 = - _mm256_cmpgt_epi64(_mm256_xor_si256(b_hi, sign), _mm256_xor_si256(a_hi, sign)); - let diff_hi = _mm256_sub_epi64(hi_tmp, borrow_lo_bit); - let bw2 = _mm256_cmpgt_epi64( - _mm256_xor_si256(borrow_lo_bit, sign), - _mm256_xor_si256(hi_tmp, sign), - ); - let borrow_128 = _mm256_or_si256(bw1, bw2); - - // Correction: add P back where underflow occurred - let corr_lo = _mm256_add_epi64(diff_lo, p_lo); - let carry_lo = _mm256_cmpgt_epi64( - _mm256_xor_si256(diff_lo, sign), - _mm256_xor_si256(corr_lo, sign), - ); - let carry_lo_bit = _mm256_and_si256(carry_lo, one); - let corr_hi = _mm256_add_epi64(diff_hi, p_hi); - let corr_hi = _mm256_add_epi64(corr_hi, carry_lo_bit); - - let out_lo = _mm256_blendv_epi8(diff_lo, corr_lo, borrow_128); - let out_hi = _mm256_blendv_epi8(diff_hi, corr_hi, borrow_128); - - let mut result = Self::default(); - _mm256_storeu_si256(result.lo.as_mut_ptr().cast(), out_lo); - _mm256_storeu_si256(result.hi.as_mut_ptr().cast(), out_hi); - result - } - } -} - -impl Mul for PackedFp128Avx2

{ - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self { - let mut out = Self::default(); - for i in 0..FP128_WIDTH { - let a = Fp128::

([self.lo[i], self.hi[i]]); - let b = Fp128::

([rhs.lo[i], rhs.hi[i]]); - let r = a * b; - out.lo[i] = r.0[0]; - out.hi[i] = r.0[1]; - } - out - } -} - -impl AddAssign for PackedFp128Avx2

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for PackedFp128Avx2

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for PackedFp128Avx2

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl PackedField for PackedFp128Avx2

{ - const WIDTH: usize = FP128_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Scalar, - { - let mut lo = [0u64; FP128_WIDTH]; - let mut hi = [0u64; FP128_WIDTH]; - for i in 0..FP128_WIDTH { - let v = f(i); - lo[i] = v.0[0]; - hi[i] = v.0[1]; - } - Self { lo, hi } - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Scalar { - debug_assert!(lane < FP128_WIDTH); - Fp128([self.lo[lane], self.hi[lane]]) - } - - type Scalar = Fp128

; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self { - lo: [value.0[0]; FP128_WIDTH], - hi: [value.0[1]; FP128_WIDTH], - } - } -} diff --git a/crates/jolt-field/src/packed/avx2/fp32.rs b/crates/jolt-field/src/packed/avx2/fp32.rs deleted file mode 100644 index ac3ba9c47d..0000000000 --- a/crates/jolt-field/src/packed/avx2/fp32.rs +++ /dev/null @@ -1,686 +0,0 @@ -use super::*; - -/// Number of `Fp32` lanes in an AVX2 packed vector. -pub(crate) const FP32_WIDTH: usize = 8; - -/// AVX2 packed arithmetic for `Fp32

`, processing 8 lanes. -#[derive(Clone, Copy)] -#[repr(transparent)] -pub struct PackedFp32Avx2(pub [Fp32

; FP32_WIDTH]); - -impl PackedFp32Avx2

{ - const BITS: u32 = 32 - P.leading_zeros(); - - const C: u32 = { - let c = if Self::BITS == 32 { - 0u32.wrapping_sub(P) - } else { - (1u32 << Self::BITS) - P - }; - assert!(P != 0, "modulus must be nonzero"); - assert!(P & 1 == 1, "modulus must be odd"); - assert!( - (c as u64) * (c as u64 + 1) < P as u64, - "C(C+1) < P required for fused canonicalize" - ); - c - }; - - const MASK_U64: u64 = if Self::BITS == 32 { - u32::MAX as u64 - } else { - (1u64 << Self::BITS) - 1 - }; - - /// Whether two Solinas folds suffice to bring the sum of four - /// `(P-1)^2` products into `[0, 2*P)` for the final canonicalize step. - /// Mirrors `PackedFp32Neon::TWO_FOLD_FOUR_PRODUCT_OK`. When `false`, - /// `solinas_reduce` must do a third fold before handing off to - /// `pack_and_canonicalize`. - const TWO_FOLD_FOUR_PRODUCT_OK: bool = { - let c = Self::C as u64; - 4 * c * c + 3 * c <= (1u64 << Self::BITS) - }; - - #[inline(always)] - fn to_vec(self) -> __m256i { - unsafe { transmute(self) } - } - - #[inline(always)] - unsafe fn from_vec(v: __m256i) -> Self { - unsafe { transmute(v) } - } - - /// Multiply each `u64` lane by `C`. Building block of Solinas reduction; - /// the `C == 1` fast path skips the multiply entirely for Mersenne-like - /// primes. Mirrors `PackedFp32Neon::mul_c_u64`. - /// - /// AVX2 has no native 64×64-bit multiply, so we split `x` into two 32-bit - /// halves, multiply each by `C` with `_mm256_mul_epu32` (32×32→64), then - /// recombine: `x*C = x_lo*C + ((x_hi*C) << 32)` (mod 2^64). The previous - /// implementation used a single `_mm256_mul_epu32(x, c_vec)` which only - /// reads the *low 32 bits* of `x` and silently dropped bit 32+ — fine for - /// `BITS == 32` (where the caller's `prod >> 32` always fits in 32 bits) - /// but wrong for `BITS == 31` and `C != 1` where `prod >> 31` can occupy - /// 33 bits. - #[inline(always)] - unsafe fn mul_c_u64(x: __m256i) -> __m256i { - if Self::C == 1 { - return x; - } - let c_vec = _mm256_set1_epi64x(Self::C as i64); - let lo_part = _mm256_mul_epu32(x, c_vec); - let hi_part = _mm256_mul_epu32(_mm256_srli_epi64::<32>(x), c_vec); - _mm256_add_epi64(lo_part, _mm256_slli_epi64::<32>(hi_part)) - } - - /// One Solinas fold of a single 64-bit product lane (BITS == 32 only): - /// `(x & (2^32-1)) + C*(x >> 32)`. For a single product `x < 2^64` the - /// high word `x >> 32 < 2^32`, so the result is `< 2^40`. Lets the - /// `BITS == 32` dot-product sum up to four folded terms (each `< 2^40`) - /// below `2^42` without `u64` overflow, removing the per-product carry - /// tracking. Mirrors `PackedFp32Avx512::fold_product_once`. - #[inline(always)] - unsafe fn fold_product_once(x: __m256i) -> __m256i { - let lo = _mm256_and_si256(x, _mm256_set1_epi64x(Self::MASK_U64 as i64)); - let hi = _mm256_srli_epi64::<32>(x); - _mm256_add_epi64(lo, Self::mul_c_u64(hi)) - } - - /// Plonky3-style Mersenne31 multiply (P = 2^31 - 1). Specialized fold - /// using `_mm256_srli_epi64::<31>` shifts. Used by the `Mul` impl when - /// `Self::BITS == 31 && Self::C == 1`. - #[inline(always)] - unsafe fn mul_mersenne31_vec(a: __m256i, b: __m256i) -> __m256i { - unsafe { - let lhs_odd_dbl = _mm256_srli_epi64::<31>(a); - let rhs_odd = movehdup_epi32(b); - - let prod_odd_dbl = _mm256_mul_epu32(rhs_odd, lhs_odd_dbl); - let prod_evn = _mm256_mul_epu32(b, a); - - let prod_odd_lo_dirty = _mm256_slli_epi64::<31>(prod_odd_dbl); - let prod_evn_hi = _mm256_srli_epi64::<31>(prod_evn); - - let prod_lo_dirty = _mm256_blend_epi32::<0b1010_1010>(prod_evn, prod_odd_lo_dirty); - let prod_hi = _mm256_blend_epi32::<0b1010_1010>(prod_evn_hi, prod_odd_dbl); - - let p = _mm256_set1_epi32(P as i32); - let prod_lo = _mm256_and_si256(prod_lo_dirty, p); - let folded = _mm256_add_epi32(prod_lo, prod_hi); - _mm256_min_epu32(folded, _mm256_sub_epi32(folded, p)) - } - } - - /// Vector form of field add: 8-lane add + canonicalize to `[0, P)`. - /// Mirrors `PackedFp32Neon::add_vec`. - #[inline(always)] - unsafe fn add_vec(a: __m256i, b: __m256i) -> __m256i { - let p = _mm256_set1_epi32(P as i32); - if Self::BITS <= 31 { - let t = _mm256_add_epi32(a, b); - let u = _mm256_sub_epi32(t, p); - _mm256_min_epu32(t, u) - } else { - // BITS == 32: a + b may overflow u32. Detect via unsigned compare - // (sign-bit-XOR trick), correct by adding C (since 2^32 ≡ C mod P), - // then conditional subtract P. - let c = _mm256_set1_epi32(Self::C as i32); - let t = _mm256_add_epi32(a, b); - let sign32 = _mm256_set1_epi32(i32::MIN); - let overflow = - _mm256_cmpgt_epi32(_mm256_xor_si256(a, sign32), _mm256_xor_si256(t, sign32)); - let t2 = _mm256_add_epi32(t, _mm256_and_si256(overflow, c)); - let r = _mm256_sub_epi32(t2, p); - _mm256_min_epu32(t2, r) - } - } - - /// Vector form of field sub: 8-lane sub + canonicalize to `[0, P)`. - /// Mirrors `PackedFp32Neon::sub_vec`. - #[inline(always)] - unsafe fn sub_vec(a: __m256i, b: __m256i) -> __m256i { - let p = _mm256_set1_epi32(P as i32); - if Self::BITS <= 31 { - let t = _mm256_sub_epi32(a, b); - let u = _mm256_add_epi32(t, p); - _mm256_min_epu32(t, u) - } else { - // BITS == 32: t = a - b may underflow. If a < b, t wraps to - // t + 2^32; we want t + P = t + 2^32 - C, i.e. subtract C. - let t = _mm256_sub_epi32(a, b); - let sign32 = _mm256_set1_epi32(i32::MIN); - let underflow = - _mm256_cmpgt_epi32(_mm256_xor_si256(b, sign32), _mm256_xor_si256(a, sign32)); - let c = _mm256_set1_epi32(Self::C as i32); - _mm256_sub_epi32(t, _mm256_and_si256(underflow, c)) - } - } - - /// Vector form of field mul: 8-lane Solinas multiply + canonicalize. - /// Mirrors `PackedFp32Neon::mul_vec`. - #[inline(always)] - unsafe fn mul_vec(a: __m256i, b: __m256i) -> __m256i { - let prod_evn = _mm256_mul_epu32(a, b); - let a_odd = movehdup_epi32(a); - let b_odd = movehdup_epi32(b); - let prod_odd = _mm256_mul_epu32(a_odd, b_odd); - Self::solinas_reduce(prod_evn, prod_odd) - } - - /// 4-way fused multiply-accumulate with a single end-reduction. - /// Computes `sum_i a[i] * b[i]` lane-wise and canonicalizes. The key - /// fused operation for `FpExt4` and power-basis FpExt4 multiply. - /// Mirrors `PackedFp32Neon::dot_product_4_vec`. For `BITS <= 31`, four - /// `(2^31 - 1)^2` products sum below `2^64`, so the raw products - /// accumulate without overflow. For `BITS == 32`, each product is - /// pre-folded once (`< 2^40`) so four folds sum below `2^42`, again - /// overflow-free. Both branches end in a single carry-free - /// `solinas_reduce`; the `if` is a const condition resolved at compile - /// time. - #[inline(always)] - unsafe fn dot_product_4_vec(a: [__m256i; 4], b: [__m256i; 4]) -> __m256i { - let mut sum_evn = _mm256_mul_epu32(a[0], b[0]); - let mut sum_odd = _mm256_mul_epu32(movehdup_epi32(a[0]), movehdup_epi32(b[0])); - - if Self::BITS <= 31 { - for i in 1..4 { - let prod_evn = _mm256_mul_epu32(a[i], b[i]); - let prod_odd = _mm256_mul_epu32(movehdup_epi32(a[i]), movehdup_epi32(b[i])); - sum_evn = _mm256_add_epi64(sum_evn, prod_evn); - sum_odd = _mm256_add_epi64(sum_odd, prod_odd); - } - return Self::solinas_reduce(sum_evn, sum_odd); - } - - // BITS == 32: four 32-bit products overflow a `u64` sum, so pre-fold each - // product once (`< 2^40`) and accumulate the folds (`< 4*2^40 < 2^42`), - // which is carry-free, then a single carry-free `solinas_reduce`. - let mut sum_evn = Self::fold_product_once(sum_evn); - let mut sum_odd = Self::fold_product_once(sum_odd); - for i in 1..4 { - let prod_evn = Self::fold_product_once(_mm256_mul_epu32(a[i], b[i])); - let prod_odd = Self::fold_product_once(_mm256_mul_epu32( - movehdup_epi32(a[i]), - movehdup_epi32(b[i]), - )); - sum_evn = _mm256_add_epi64(sum_evn, prod_evn); - sum_odd = _mm256_add_epi64(sum_odd, prod_odd); - } - Self::solinas_reduce(sum_evn, sum_odd) - } - - /// 3-way fused multiply-accumulate with a single end-reduction. - #[inline(always)] - unsafe fn dot_product_3_vec(a: [__m256i; 3], b: [__m256i; 3]) -> __m256i { - let mut sum_evn = _mm256_mul_epu32(a[0], b[0]); - let mut sum_odd = _mm256_mul_epu32(movehdup_epi32(a[0]), movehdup_epi32(b[0])); - - if Self::BITS <= 31 { - for i in 1..3 { - let prod_evn = _mm256_mul_epu32(a[i], b[i]); - let prod_odd = _mm256_mul_epu32(movehdup_epi32(a[i]), movehdup_epi32(b[i])); - sum_evn = _mm256_add_epi64(sum_evn, prod_evn); - sum_odd = _mm256_add_epi64(sum_odd, prod_odd); - } - return Self::solinas_reduce(sum_evn, sum_odd); - } - - // BITS == 32: pre-fold (see `dot_product_4_vec`). - let mut sum_evn = Self::fold_product_once(sum_evn); - let mut sum_odd = Self::fold_product_once(sum_odd); - for i in 1..3 { - let prod_evn = Self::fold_product_once(_mm256_mul_epu32(a[i], b[i])); - let prod_odd = Self::fold_product_once(_mm256_mul_epu32( - movehdup_epi32(a[i]), - movehdup_epi32(b[i]), - )); - sum_evn = _mm256_add_epi64(sum_evn, prod_evn); - sum_odd = _mm256_add_epi64(sum_odd, prod_odd); - } - Self::solinas_reduce(sum_evn, sum_odd) - } - - /// Multiply by an `FpExt2` non-residue (used by `fp_ext2_mul`). Recognizes the - /// `nr == -1` and `nr == 2` fast paths to avoid full multiplies. - /// Mirrors `PackedFp32Neon::mul_nr_vec`. - #[inline(always)] - unsafe fn mul_nr_vec(x: __m256i) -> __m256i - where - C: FpExt2Config>, - { - if C::IS_NEG_ONE { - Self::sub_vec(_mm256_setzero_si256(), x) - } else if C::non_residue().0 == 2 { - Self::add_vec(x, x) - } else { - C::mul_non_residue(Self::from_vec(x), Self::broadcast).to_vec() - } - } - - /// Two-or-three-fold Solinas reduction of 4+4 `u64` products → 8 `u32` - /// lanes. Inputs are the even-lane and odd-lane product vectors from - /// `_mm256_mul_epu32`. Mirrors `PackedFp32Neon::solinas_reduce`. - /// - /// The `Self::BITS == 31` branches use immediate-shift - /// `_mm256_srli_epi64::<31>` instead of the generic variable-shift - /// `_mm256_srl_epi64(.., shift)`, mirroring the same specialisation - /// the base-field `Mul` impl uses on Mersenne31, so extension-field - /// operations on Mersenne31 get the same per-shift win. - /// - /// Two folds always suffice when `Self::TWO_FOLD_FOUR_PRODUCT_OK`. When - /// it doesn't (large `C` such that `4*C^2 + 3*C > 2^BITS`), we run a - /// third fold so `pack_and_canonicalize`'s single subtract-and-min step - /// is enough to land in `[0, P)`. - #[inline(always)] - unsafe fn solinas_reduce(prod_evn: __m256i, prod_odd: __m256i) -> __m256i { - let mask = _mm256_set1_epi64x(Self::MASK_U64 as i64); - let shift = _mm_set_epi64x(0, Self::BITS as i64); - - // Fold 1 - let evn_lo = _mm256_and_si256(prod_evn, mask); - let evn_hi = if Self::BITS == 31 { - _mm256_srli_epi64::<31>(prod_evn) - } else { - _mm256_srl_epi64(prod_evn, shift) - }; - let evn_f1 = _mm256_add_epi64(evn_lo, Self::mul_c_u64(evn_hi)); - - let odd_lo = _mm256_and_si256(prod_odd, mask); - let odd_hi = if Self::BITS == 31 { - _mm256_srli_epi64::<31>(prod_odd) - } else { - _mm256_srl_epi64(prod_odd, shift) - }; - let odd_f1 = _mm256_add_epi64(odd_lo, Self::mul_c_u64(odd_hi)); - - // Fold 2 - let evn_f1_lo = _mm256_and_si256(evn_f1, mask); - let evn_f1_hi = if Self::BITS == 31 { - _mm256_srli_epi64::<31>(evn_f1) - } else { - _mm256_srl_epi64(evn_f1, shift) - }; - let evn_f2 = _mm256_add_epi64(evn_f1_lo, Self::mul_c_u64(evn_f1_hi)); - - let odd_f1_lo = _mm256_and_si256(odd_f1, mask); - let odd_f1_hi = if Self::BITS == 31 { - _mm256_srli_epi64::<31>(odd_f1) - } else { - _mm256_srl_epi64(odd_f1, shift) - }; - let odd_f2 = _mm256_add_epi64(odd_f1_lo, Self::mul_c_u64(odd_f1_hi)); - - // Optional third fold for large-C primes (e.g. Generic31Offset32787) - // where two folds leave residue > 2*P. - let (evn_final, odd_final) = if Self::TWO_FOLD_FOUR_PRODUCT_OK { - (evn_f2, odd_f2) - } else { - let evn_f2_lo = _mm256_and_si256(evn_f2, mask); - let evn_f2_hi = if Self::BITS == 31 { - _mm256_srli_epi64::<31>(evn_f2) - } else { - _mm256_srl_epi64(evn_f2, shift) - }; - let odd_f2_lo = _mm256_and_si256(odd_f2, mask); - let odd_f2_hi = if Self::BITS == 31 { - _mm256_srli_epi64::<31>(odd_f2) - } else { - _mm256_srl_epi64(odd_f2, shift) - }; - ( - _mm256_add_epi64(evn_f2_lo, Self::mul_c_u64(evn_f2_hi)), - _mm256_add_epi64(odd_f2_lo, Self::mul_c_u64(odd_f2_hi)), - ) - }; - - Self::pack_and_canonicalize(evn_final, odd_final) - } - - /// Combine 4+4 `u64` lanes (in range `[0, 2P)`) into 8 `u32` lanes - /// canonicalized to `[0, P)`. For `BITS < 32` the values fit in `u32`, - /// so we can pack first and subtract `P` at `u32` width. For `BITS == 32` - /// the worst case can exceed `u32::MAX`, so we conditionally subtract `P` - /// at `u64` width first, then pack. Mirrors the post-fold tail of - /// `PackedFp32Neon::solinas_reduce`. - #[inline(always)] - unsafe fn pack_and_canonicalize(evn_f2: __m256i, odd_f2: __m256i) -> __m256i { - if Self::BITS < 32 { - let odd_shifted = _mm256_slli_epi64::<32>(odd_f2); - let combined = _mm256_blend_epi32::<0b10101010>(evn_f2, odd_shifted); - let p = _mm256_set1_epi32(P as i32); - let reduced = _mm256_sub_epi32(combined, p); - _mm256_min_epu32(combined, reduced) - } else { - let p_u64 = _mm256_set1_epi64x(P as i64); - let sign = _mm256_set1_epi64x(i64::MIN); - let p_s = _mm256_xor_si256(p_u64, sign); - - let red_evn = _mm256_sub_epi64(evn_f2, p_u64); - let evn_s = _mm256_xor_si256(evn_f2, sign); - let keep_evn = _mm256_cmpgt_epi64(p_s, evn_s); - let out_evn = _mm256_blendv_epi8(red_evn, evn_f2, keep_evn); - - let red_odd = _mm256_sub_epi64(odd_f2, p_u64); - let odd_s = _mm256_xor_si256(odd_f2, sign); - let keep_odd = _mm256_cmpgt_epi64(p_s, odd_s); - let out_odd = _mm256_blendv_epi8(red_odd, odd_f2, keep_odd); - - let odd_shifted = _mm256_slli_epi64::<32>(out_odd); - _mm256_blend_epi32::<0b10101010>(out_evn, odd_shifted) - } - } -} - -impl Default for PackedFp32Avx2

{ - #[inline] - fn default() -> Self { - Self([Fp32(0); FP32_WIDTH]) - } -} - -impl fmt::Debug for PackedFp32Avx2

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("PackedFp32Avx2").field(&self.0).finish() - } -} - -impl PartialEq for PackedFp32Avx2

{ - #[inline] - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Eq for PackedFp32Avx2

{} - -impl Add for PackedFp32Avx2

{ - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - unsafe { Self::from_vec(Self::add_vec(self.to_vec(), rhs.to_vec())) } - } -} - -impl Sub for PackedFp32Avx2

{ - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - unsafe { Self::from_vec(Self::sub_vec(self.to_vec(), rhs.to_vec())) } - } -} - -impl Mul for PackedFp32Avx2

{ - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self { - unsafe { - let a = self.to_vec(); - let b = rhs.to_vec(); - - if Self::BITS == 31 && Self::C == 1 { - return Self::from_vec(Self::mul_mersenne31_vec(a, b)); - } - - let prod_evn = _mm256_mul_epu32(a, b); - let a_odd = movehdup_epi32(a); - let b_odd = movehdup_epi32(b); - let prod_odd = _mm256_mul_epu32(a_odd, b_odd); - - let mask = _mm256_set1_epi64x(Self::MASK_U64 as i64); - let shift = _mm_set_epi64x(0, Self::BITS as i64); - - // Fold 1 - let evn_lo = _mm256_and_si256(prod_evn, mask); - let evn_hi = if Self::BITS == 31 { - _mm256_srli_epi64::<31>(prod_evn) - } else { - _mm256_srl_epi64(prod_evn, shift) - }; - let evn_f1 = _mm256_add_epi64(evn_lo, Self::mul_c_u64(evn_hi)); - - let odd_lo = _mm256_and_si256(prod_odd, mask); - let odd_hi = if Self::BITS == 31 { - _mm256_srli_epi64::<31>(prod_odd) - } else { - _mm256_srl_epi64(prod_odd, shift) - }; - let odd_f1 = _mm256_add_epi64(odd_lo, Self::mul_c_u64(odd_hi)); - - // Fold 2 - let evn_f1_lo = _mm256_and_si256(evn_f1, mask); - let evn_f1_hi = if Self::BITS == 31 { - _mm256_srli_epi64::<31>(evn_f1) - } else { - _mm256_srl_epi64(evn_f1, shift) - }; - let evn_f2 = _mm256_add_epi64(evn_f1_lo, Self::mul_c_u64(evn_f1_hi)); - - let odd_f1_lo = _mm256_and_si256(odd_f1, mask); - let odd_f1_hi = if Self::BITS == 31 { - _mm256_srli_epi64::<31>(odd_f1) - } else { - _mm256_srl_epi64(odd_f1, shift) - }; - let odd_f2 = _mm256_add_epi64(odd_f1_lo, Self::mul_c_u64(odd_f1_hi)); - - // Recombine + canonicalize. For `BITS == 32` the two-fold residue - // can land in `[2^32, 2*P)` (up to `2^32 + C^2`), so the subtract - // must happen on the full 64-bit lanes before packing; a 32-bit - // recombine would drop bit 32. `pack_and_canonicalize` does the - // 64-bit subtract for `BITS == 32` and is identical to the inline - // 32-bit recombine for `BITS < 32`. - Self::from_vec(Self::pack_and_canonicalize(evn_f2, odd_f2)) - } - } -} - -impl AddAssign for PackedFp32Avx2

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for PackedFp32Avx2

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for PackedFp32Avx2

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl PackedField for PackedFp32Avx2

{ - const WIDTH: usize = FP32_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Scalar, - { - Self([f(0), f(1), f(2), f(3), f(4), f(5), f(6), f(7)]) - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Scalar { - debug_assert!(lane < FP32_WIDTH); - self.0[lane] - } - - type Scalar = Fp32

; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self([value; FP32_WIDTH]) - } - - #[inline(always)] - fn fp_ext2_mul(a0: Self, a1: Self, b0: Self, b1: Self) -> (Self, Self) - where - C: FpExt2Config, - { - unsafe { - let a0 = a0.to_vec(); - let a1 = a1.to_vec(); - let b0 = b0.to_vec(); - let b1 = b1.to_vec(); - - let v0 = Self::mul_vec(a0, b0); - let v1 = Self::mul_vec(a1, b1); - let cross = Self::mul_vec(Self::add_vec(a0, a1), Self::add_vec(b0, b1)); - - ( - Self::from_vec(Self::add_vec(v0, Self::mul_nr_vec::(v1))), - Self::from_vec(Self::sub_vec(Self::sub_vec(cross, v0), v1)), - ) - } - } - - #[inline(always)] - fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { - unsafe { - let [a0, a1, a2, a3] = a.map(Self::to_vec); - let [b0, b1, b2, b3] = b.map(Self::to_vec); - let two_b1 = Self::add_vec(b1, b1); - let two_b2 = Self::add_vec(b2, b2); - let two_b3 = Self::add_vec(b3, b3); - let b0_plus_b2 = Self::add_vec(b0, b2); - let b1_plus_b3 = Self::add_vec(b1, b3); - let b1_minus_b3 = Self::sub_vec(b1, b3); - let b0_minus_b2 = Self::sub_vec(b0, b2); - [ - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [b0, two_b1, two_b2, two_b3], - )), - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [b1, b0_plus_b2, b1_plus_b3, b2], - )), - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [b2, b1_plus_b3, b0, b1_minus_b3], - )), - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [b3, b2, b1_minus_b3, b0_minus_b2], - )), - ] - } - } - - #[inline(always)] - fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { - unsafe { - let [a0, a1, a2, a3] = a.map(Self::to_vec); - let zero = _mm256_setzero_si256(); - let two_a1 = Self::add_vec(a1, a1); - let two_a2 = Self::add_vec(a2, a2); - let two_a3 = Self::add_vec(a3, a3); - let neg_a3 = Self::sub_vec(zero, a3); - let neg_two_a3 = Self::sub_vec(zero, two_a3); - [ - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [a0, two_a1, two_a2, two_a3], - )), - Self::from_vec(Self::dot_product_3_vec( - [a0, a1, a2], - [two_a1, two_a2, two_a3], - )), - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a1, a3], - [two_a2, a1, two_a3, neg_a3], - )), - Self::from_vec(Self::dot_product_3_vec( - [a0, a1, a2], - [two_a3, two_a2, neg_two_a3], - )), - ] - } - } - - #[inline(always)] - fn fp_ext4_inverse(a: [Self; 4]) -> Option<[Self; 4]> - where - Self::Scalar: FieldCore, - { - unsafe { - let [a0, a1, a2, a3] = a.map(Self::to_vec); - let zero = _mm256_setzero_si256(); - let x0 = a0; - let x1 = a2; - let y0 = Self::sub_vec(a1, a3); - let y1 = a3; - - let x1_square = Self::mul_vec(x1, x1); - let y1_square = Self::mul_vec(y1, y1); - let aa0 = Self::add_vec(Self::mul_vec(x0, x0), Self::add_vec(x1_square, x1_square)); - let aa1 = { - let x0x1 = Self::mul_vec(x0, x1); - Self::add_vec(x0x1, x0x1) - }; - let bb0 = Self::add_vec(Self::mul_vec(y0, y0), Self::add_vec(y1_square, y1_square)); - let bb1 = { - let y0y1 = Self::mul_vec(y0, y1); - Self::add_vec(y0y1, y0y1) - }; - let nr_bb0 = Self::add_vec(Self::add_vec(bb0, bb0), Self::add_vec(bb1, bb1)); - let nr_bb1 = Self::add_vec(bb0, Self::add_vec(bb1, bb1)); - let norm0 = Self::sub_vec(aa0, nr_bb0); - let norm1 = Self::sub_vec(aa1, nr_bb1); - - let inv_norm_base = { - let norm1_square = Self::mul_vec(norm1, norm1); - let norm_base = Self::sub_vec( - Self::mul_vec(norm0, norm0), - Self::add_vec(norm1_square, norm1_square), - ); - Self::from_vec(norm_base).inverse()?.to_vec() - }; - let inv_norm0 = Self::mul_vec(norm0, inv_norm_base); - let inv_norm1 = Self::mul_vec(Self::sub_vec(zero, norm1), inv_norm_base); - - let v0 = Self::mul_vec(x0, inv_norm0); - let v1 = Self::mul_vec(x1, inv_norm1); - let constant0 = Self::add_vec(v0, Self::add_vec(v1, v1)); - let constant1 = Self::sub_vec( - Self::sub_vec( - Self::mul_vec(Self::add_vec(x0, x1), Self::add_vec(inv_norm0, inv_norm1)), - v0, - ), - v1, - ); - - let neg_y0 = Self::sub_vec(zero, y0); - let neg_y1 = Self::sub_vec(zero, y1); - let w0 = Self::mul_vec(neg_y0, inv_norm0); - let w1 = Self::mul_vec(neg_y1, inv_norm1); - let e1_coeff0 = Self::add_vec(w0, Self::add_vec(w1, w1)); - let e1_coeff1 = Self::sub_vec( - Self::sub_vec( - Self::mul_vec( - Self::add_vec(neg_y0, neg_y1), - Self::add_vec(inv_norm0, inv_norm1), - ), - w0, - ), - w1, - ); - - Some([ - Self::from_vec(constant0), - Self::from_vec(Self::add_vec(e1_coeff0, e1_coeff1)), - Self::from_vec(constant1), - Self::from_vec(e1_coeff1), - ]) - } - } -} diff --git a/crates/jolt-field/src/packed/avx2/fp64.rs b/crates/jolt-field/src/packed/avx2/fp64.rs deleted file mode 100644 index a4074154b6..0000000000 --- a/crates/jolt-field/src/packed/avx2/fp64.rs +++ /dev/null @@ -1,264 +0,0 @@ -use super::*; - -/// Number of `Fp64` lanes in an AVX2 packed vector. -pub(crate) const FP64_WIDTH: usize = 4; - -/// AVX2 packed arithmetic for `Fp64

`, processing 4 lanes. -#[derive(Clone, Copy)] -#[repr(transparent)] -pub struct PackedFp64Avx2(pub [Fp64

; FP64_WIDTH]); - -impl PackedFp64Avx2

{ - const BITS: u32 = 64 - P.leading_zeros(); - - const C_LO: u64 = { - let c = if Self::BITS == 64 { - 0u64.wrapping_sub(P) - } else { - (1u64 << Self::BITS) - P - }; - assert!(P != 0, "modulus must be nonzero"); - assert!(P & 1 == 1, "modulus must be odd"); - c - }; - - const MASK64: u64 = if Self::BITS < 64 { - (1u64 << Self::BITS) - 1 - } else { - u64::MAX - }; - - #[inline(always)] - fn to_vec(self) -> __m256i { - unsafe { transmute(self) } - } - - #[inline(always)] - unsafe fn from_vec(v: __m256i) -> Self { - unsafe { transmute(v) } - } - - #[inline] - unsafe fn reduce128_vec(hi: __m256i, lo: __m256i) -> __m256i { - if Self::BITS < 64 { - Self::reduce128_small_k(hi, lo) - } else { - Self::reduce128_full_k(hi, lo) - } - } - - /// Reduction for BITS < 64. All intermediates fit in u64 — no overflow. - #[inline] - unsafe fn reduce128_small_k(hi: __m256i, lo: __m256i) -> __m256i { - let mask_k = _mm256_set1_epi64x(Self::MASK64 as i64); - let c_vec = _mm256_set1_epi64x(Self::C_LO as i64); - let p_vec = _mm256_set1_epi64x(P as i64); - let shift_k = _mm_set_epi64x(0, Self::BITS as i64); - let shift_64mk = _mm_set_epi64x(0, (64 - Self::BITS) as i64); - - let lo_k = _mm256_and_si256(lo, mask_k); - let lo_upper = _mm256_srl_epi64(lo, shift_k); - let hi_shifted = _mm256_sll_epi64(hi, shift_64mk); - let hi_k = _mm256_or_si256(lo_upper, hi_shifted); - - let c_hi_lo = _mm256_mul_epu32(c_vec, hi_k); - let hi_k_top = _mm256_srli_epi64::<32>(hi_k); - let c_hi_top = _mm256_mul_epu32(c_vec, hi_k_top); - let c_hi_top_shifted = _mm256_slli_epi64::<32>(c_hi_top); - let c_hi_full = _mm256_add_epi64(c_hi_lo, c_hi_top_shifted); - - let fold1 = _mm256_add_epi64(lo_k, c_hi_full); - - let fold1_lo_k = _mm256_and_si256(fold1, mask_k); - let fold1_hi = _mm256_srl_epi64(fold1, shift_k); - let c_fold1_hi = _mm256_mul_epu32(c_vec, fold1_hi); - let fold2 = _mm256_add_epi64(fold1_lo_k, c_fold1_hi); - - let reduced = _mm256_sub_epi64(fold2, p_vec); - let sign = _mm256_set1_epi64x(i64::MIN); - let fold2_s = _mm256_xor_si256(fold2, sign); - let reduced_s = _mm256_xor_si256(reduced, sign); - let fold2_lt = _mm256_cmpgt_epi64(reduced_s, fold2_s); - _mm256_blendv_epi8(reduced, fold2, fold2_lt) - } - - /// Reduction for BITS == 64. Uses XOR-with-SIGN_BIT trick for unsigned - /// overflow detection. - #[inline] - unsafe fn reduce128_full_k(hi: __m256i, lo: __m256i) -> __m256i { - let c_vec = _mm256_set1_epi64x(Self::C_LO as i64); - let p_vec = _mm256_set1_epi64x(P as i64); - let sign = _mm256_set1_epi64x(i64::MIN); - let c_hi_lo = _mm256_mul_epu32(c_vec, hi); - let hi_hi = _mm256_srli_epi64::<32>(hi); - let c_hi_hi = _mm256_mul_epu32(c_vec, hi_hi); - - let c_hi_hi_lo32 = _mm256_slli_epi64::<32>(c_hi_hi); - let c_hi_carry = _mm256_srli_epi64::<32>(c_hi_hi); - - let sum_lo = _mm256_add_epi64(c_hi_lo, c_hi_hi_lo32); - let c_hi_lo_s = _mm256_xor_si256(c_hi_lo, sign); - let sum_lo_s = _mm256_xor_si256(sum_lo, sign); - let carry0 = _mm256_cmpgt_epi64(c_hi_lo_s, sum_lo_s); - let overflow = _mm256_sub_epi64(c_hi_carry, carry0); - - let s = _mm256_add_epi64(lo, sum_lo); - let lo_s = _mm256_xor_si256(lo, sign); - let s_s = _mm256_xor_si256(s, sign); - let carry1 = _mm256_cmpgt_epi64(lo_s, s_s); - let total_overflow = _mm256_sub_epi64(overflow, carry1); - - let final_corr = _mm256_mul_epu32(c_vec, total_overflow); - let result = _mm256_add_epi64(s, final_corr); - let s2_s = _mm256_xor_si256(s, sign); - let result_s = _mm256_xor_si256(result, sign); - let carry_f = _mm256_cmpgt_epi64(s2_s, result_s); - let corr_f = _mm256_and_si256(carry_f, c_vec); - let result = _mm256_add_epi64(result, corr_f); - - let result_s2 = _mm256_xor_si256(result, sign); - let p_s = _mm256_xor_si256(p_vec, sign); - let lt_p = _mm256_cmpgt_epi64(p_s, result_s2); - let sub_amt = _mm256_andnot_si256(lt_p, p_vec); - _mm256_sub_epi64(result, sub_amt) - } -} - -impl Default for PackedFp64Avx2

{ - #[inline] - fn default() -> Self { - Self([Fp64(0); FP64_WIDTH]) - } -} - -impl fmt::Debug for PackedFp64Avx2

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("PackedFp64Avx2").field(&self.0).finish() - } -} - -impl PartialEq for PackedFp64Avx2

{ - #[inline] - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Eq for PackedFp64Avx2

{} - -impl Add for PackedFp64Avx2

{ - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - unsafe { - let a = self.to_vec(); - let b = rhs.to_vec(); - let p = _mm256_set1_epi64x(P as i64); - - let result = if Self::BITS <= 62 { - // a + b < 2P < 2^63: no overflow. - let s = _mm256_add_epi64(a, b); - let r = _mm256_sub_epi64(s, p); - // s < P? Use signed compare after shift trick. - let sign = _mm256_set1_epi64x(i64::MIN); - let s_s = _mm256_xor_si256(s, sign); - let p_s = _mm256_xor_si256(p, sign); - let borrow = _mm256_cmpgt_epi64(p_s, s_s); - _mm256_blendv_epi8(r, s, borrow) - } else { - // a + b can overflow u64. - let s = _mm256_add_epi64(a, b); - let sign = _mm256_set1_epi64x(i64::MIN); - let a_s = _mm256_xor_si256(a, sign); - let s_s = _mm256_xor_si256(s, sign); - let overflow = _mm256_cmpgt_epi64(a_s, s_s); - let c = _mm256_set1_epi64x(Self::C_LO as i64); - let s_plus_c = _mm256_add_epi64(s, c); - let s_minus_p = _mm256_sub_epi64(s, p); - let p_s = _mm256_xor_si256(p, sign); - let lt_p = _mm256_cmpgt_epi64(p_s, s_s); - let no_of = _mm256_blendv_epi8(s_minus_p, s, lt_p); - _mm256_blendv_epi8(no_of, s_plus_c, overflow) - }; - - Self::from_vec(result) - } - } -} - -impl Sub for PackedFp64Avx2

{ - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - unsafe { - let a = self.to_vec(); - let b = rhs.to_vec(); - let p = _mm256_set1_epi64x(P as i64); - let d = _mm256_sub_epi64(a, b); - - let sign = _mm256_set1_epi64x(i64::MIN); - let a_s = _mm256_xor_si256(a, sign); - let b_s = _mm256_xor_si256(b, sign); - let underflow = _mm256_cmpgt_epi64(b_s, a_s); - let corrected = _mm256_add_epi64(d, p); - Self::from_vec(_mm256_blendv_epi8(d, corrected, underflow)) - } - } -} - -impl Mul for PackedFp64Avx2

{ - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self { - unsafe { - let (hi, lo) = mul64_64_256(self.to_vec(), rhs.to_vec()); - Self::from_vec(Self::reduce128_vec(hi, lo)) - } - } -} - -impl AddAssign for PackedFp64Avx2

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for PackedFp64Avx2

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for PackedFp64Avx2

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl PackedField for PackedFp64Avx2

{ - const WIDTH: usize = FP64_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Scalar, - { - Self([f(0), f(1), f(2), f(3)]) - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Scalar { - debug_assert!(lane < FP64_WIDTH); - self.0[lane] - } - - type Scalar = Fp64

; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self([value; FP64_WIDTH]) - } -} diff --git a/crates/jolt-field/src/packed/avx2/mod.rs b/crates/jolt-field/src/packed/avx2/mod.rs deleted file mode 100644 index 2e1e427aed..0000000000 --- a/crates/jolt-field/src/packed/avx2/mod.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! AVX2 packed backends for Fp32, Fp64, Fp128. -//! -//! Techniques adapted from plonky2 (Goldilocks) and plonky3 (Mersenne-31). - -#![expect( - clippy::undocumented_unsafe_blocks, - reason = "ported AVX2 kernels retain their audited intrinsic-level invariants" -)] - -use super::PackedField; -use crate::ext::FpExt2Config; -use crate::FieldCore; -use crate::{Fp128, Fp32, Fp64}; -use core::arch::x86_64::*; -use core::fmt; -use core::mem::transmute; -use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; - -/// Duplicate high 32 bits of each 64-bit lane into the low 32 bits. -/// Uses the float `movehdup` instruction which runs on port 5 (doesn't compete -/// with multiply on ports 0/1). -#[inline(always)] -unsafe fn movehdup_epi32(x: __m256i) -> __m256i { - _mm256_castps_si256(_mm256_movehdup_ps(_mm256_castsi256_ps(x))) -} - -#[inline(always)] -unsafe fn moveldup_epi32(x: __m256i) -> __m256i { - _mm256_castps_si256(_mm256_moveldup_ps(_mm256_castsi256_ps(x))) -} - -/// 64×64→128 schoolbook multiply using 32×32→64 partial products. -/// Returns (hi, lo) representing the 128-bit product. -#[inline] -unsafe fn mul64_64_256(x: __m256i, y: __m256i) -> (__m256i, __m256i) { - let x_hi = movehdup_epi32(x); - let y_hi = movehdup_epi32(y); - - let mul_ll = _mm256_mul_epu32(x, y); - let mul_lh = _mm256_mul_epu32(x, y_hi); - let mul_hl = _mm256_mul_epu32(x_hi, y); - let mul_hh = _mm256_mul_epu32(x_hi, y_hi); - - let mul_ll_hi = _mm256_srli_epi64::<32>(mul_ll); - let t0 = _mm256_add_epi64(mul_hl, mul_ll_hi); - let mask32 = _mm256_set1_epi64x(0xFFFF_FFFF_i64); - let t0_lo = _mm256_and_si256(t0, mask32); - let t0_hi = _mm256_srli_epi64::<32>(t0); - let t1 = _mm256_add_epi64(mul_lh, t0_lo); - let t2 = _mm256_add_epi64(mul_hh, t0_hi); - let t1_hi = _mm256_srli_epi64::<32>(t1); - let res_hi = _mm256_add_epi64(t2, t1_hi); - - let t1_lo = moveldup_epi32(t1); - let res_lo = _mm256_blend_epi32::<0b10101010>(mul_ll, t1_lo); - - (res_hi, res_lo) -} - -mod fp128; -mod fp32; -mod fp64; -pub(crate) use fp128::*; -pub(crate) use fp32::*; -pub(crate) use fp64::*; diff --git a/crates/jolt-field/src/packed/avx512/fp128.rs b/crates/jolt-field/src/packed/avx512/fp128.rs deleted file mode 100644 index f0bbcc49ed..0000000000 --- a/crates/jolt-field/src/packed/avx512/fp128.rs +++ /dev/null @@ -1,203 +0,0 @@ -use super::*; - -/// Number of `Fp128` lanes in an AVX-512 packed vector. -pub(crate) const FP128_WIDTH: usize = 8; - -/// AVX-512 packed arithmetic for `Fp128

`, 8 lanes in SoA layout. -/// -/// Stores 8 elements as separate `lo` and `hi` `u64` arrays, enabling -/// vectorized add/sub via `__m512i`. Mul remains scalar per-lane. -#[derive(Clone, Copy)] -pub struct PackedFp128Avx512 { - lo: [u64; FP128_WIDTH], - hi: [u64; FP128_WIDTH], -} - -impl PackedFp128Avx512

{ - const P_LO: u64 = P as u64; - const P_HI: u64 = (P >> 64) as u64; -} - -impl Default for PackedFp128Avx512

{ - #[inline] - fn default() -> Self { - Self { - lo: [0; FP128_WIDTH], - hi: [0; FP128_WIDTH], - } - } -} - -impl fmt::Debug for PackedFp128Avx512

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let elems: Vec<_> = (0..FP128_WIDTH).map(|i| self.extract(i)).collect(); - f.debug_tuple("PackedFp128Avx512").field(&elems).finish() - } -} - -impl PartialEq for PackedFp128Avx512

{ - #[inline] - fn eq(&self, other: &Self) -> bool { - self.lo == other.lo && self.hi == other.hi - } -} - -impl Eq for PackedFp128Avx512

{} - -impl Add for PackedFp128Avx512

{ - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - unsafe { - let a_lo = _mm512_loadu_si512(self.lo.as_ptr().cast()); - let a_hi = _mm512_loadu_si512(self.hi.as_ptr().cast()); - let b_lo = _mm512_loadu_si512(rhs.lo.as_ptr().cast()); - let b_hi = _mm512_loadu_si512(rhs.hi.as_ptr().cast()); - let p_lo = _mm512_set1_epi64(Self::P_LO as i64); - let p_hi = _mm512_set1_epi64(Self::P_HI as i64); - let one = _mm512_set1_epi64(1); - - // 128-bit add: (sum_hi, sum_lo) = (a_hi, a_lo) + (b_hi, b_lo) - let sum_lo = _mm512_add_epi64(a_lo, b_lo); - let carry_lo = _mm512_cmplt_epu64_mask(sum_lo, a_lo); - let hi_tmp = _mm512_add_epi64(a_hi, b_hi); - let ov1 = _mm512_cmplt_epu64_mask(hi_tmp, a_hi); - let sum_hi = _mm512_mask_add_epi64(hi_tmp, carry_lo, hi_tmp, one); - let ov2 = _mm512_cmplt_epu64_mask(sum_hi, hi_tmp); - let carry_128 = ov1 | ov2; - - // 128-bit subtract P: (red_hi, red_lo) = (sum_hi, sum_lo) - P - let red_lo = _mm512_sub_epi64(sum_lo, p_lo); - let borrow_lo = _mm512_cmplt_epu64_mask(sum_lo, p_lo); - let red_hi_tmp = _mm512_sub_epi64(sum_hi, p_hi); - let bw1 = _mm512_cmplt_epu64_mask(sum_hi, p_hi); - let red_hi = _mm512_mask_sub_epi64(red_hi_tmp, borrow_lo, red_hi_tmp, one); - let bw2 = _mm512_cmplt_epu64_mask(red_hi_tmp, _mm512_maskz_mov_epi64(borrow_lo, one)); - let borrow = bw1 | bw2; - - // Use reduced if: overflow happened OR subtraction didn't borrow - let use_reduced = carry_128 | !borrow; - let out_lo = _mm512_mask_blend_epi64(use_reduced, sum_lo, red_lo); - let out_hi = _mm512_mask_blend_epi64(use_reduced, sum_hi, red_hi); - - let mut result = Self::default(); - _mm512_storeu_si512(result.lo.as_mut_ptr().cast(), out_lo); - _mm512_storeu_si512(result.hi.as_mut_ptr().cast(), out_hi); - result - } - } -} - -impl Sub for PackedFp128Avx512

{ - type Output = Self; - // `bw1 | bw2` below is correct 128-bit borrow wiring (mask OR), not an - // arithmetic bug; suppress the lint locally rather than module-wide. - #[expect(clippy::suspicious_arithmetic_impl)] - #[inline] - fn sub(self, rhs: Self) -> Self { - unsafe { - let a_lo = _mm512_loadu_si512(self.lo.as_ptr().cast()); - let a_hi = _mm512_loadu_si512(self.hi.as_ptr().cast()); - let b_lo = _mm512_loadu_si512(rhs.lo.as_ptr().cast()); - let b_hi = _mm512_loadu_si512(rhs.hi.as_ptr().cast()); - let p_lo = _mm512_set1_epi64(Self::P_LO as i64); - let p_hi = _mm512_set1_epi64(Self::P_HI as i64); - let one = _mm512_set1_epi64(1); - - // 128-bit sub: (diff_hi, diff_lo) = (a_hi, a_lo) - (b_hi, b_lo) - let diff_lo = _mm512_sub_epi64(a_lo, b_lo); - let borrow_lo = _mm512_cmplt_epu64_mask(a_lo, b_lo); - let hi_tmp = _mm512_sub_epi64(a_hi, b_hi); - let bw1 = _mm512_cmplt_epu64_mask(a_hi, b_hi); - let diff_hi = _mm512_mask_sub_epi64(hi_tmp, borrow_lo, hi_tmp, one); - let bw2 = _mm512_cmplt_epu64_mask(hi_tmp, _mm512_maskz_mov_epi64(borrow_lo, one)); - let borrow_128 = bw1 | bw2; - - // Correction: add P back where underflow occurred - let corr_lo = _mm512_add_epi64(diff_lo, p_lo); - let carry_lo = _mm512_cmplt_epu64_mask(corr_lo, diff_lo); - let corr_hi = _mm512_add_epi64(diff_hi, p_hi); - let corr_hi = _mm512_mask_add_epi64(corr_hi, carry_lo, corr_hi, one); - - let out_lo = _mm512_mask_blend_epi64(borrow_128, diff_lo, corr_lo); - let out_hi = _mm512_mask_blend_epi64(borrow_128, diff_hi, corr_hi); - - let mut result = Self::default(); - _mm512_storeu_si512(result.lo.as_mut_ptr().cast(), out_lo); - _mm512_storeu_si512(result.hi.as_mut_ptr().cast(), out_hi); - result - } - } -} - -impl Mul for PackedFp128Avx512

{ - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self { - let mut out = Self::default(); - for i in 0..FP128_WIDTH { - let a = Fp128::

([self.lo[i], self.hi[i]]); - let b = Fp128::

([rhs.lo[i], rhs.hi[i]]); - let r = a * b; - out.lo[i] = r.0[0]; - out.hi[i] = r.0[1]; - } - out - } -} - -impl AddAssign for PackedFp128Avx512

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for PackedFp128Avx512

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for PackedFp128Avx512

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl PackedField for PackedFp128Avx512

{ - const WIDTH: usize = FP128_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Scalar, - { - let mut lo = [0u64; FP128_WIDTH]; - let mut hi = [0u64; FP128_WIDTH]; - for i in 0..FP128_WIDTH { - let v = f(i); - lo[i] = v.0[0]; - hi[i] = v.0[1]; - } - Self { lo, hi } - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Scalar { - debug_assert!(lane < FP128_WIDTH); - Fp128([self.lo[lane], self.hi[lane]]) - } - - type Scalar = Fp128

; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self { - lo: [value.0[0]; FP128_WIDTH], - hi: [value.0[1]; FP128_WIDTH], - } - } -} diff --git a/crates/jolt-field/src/packed/avx512/fp32.rs b/crates/jolt-field/src/packed/avx512/fp32.rs deleted file mode 100644 index a53895e22a..0000000000 --- a/crates/jolt-field/src/packed/avx512/fp32.rs +++ /dev/null @@ -1,679 +0,0 @@ -use super::*; - -/// Number of `Fp32` lanes in an AVX-512 packed vector. -pub(crate) const FP32_WIDTH: usize = 16; - -/// AVX-512 packed arithmetic for `Fp32

`, processing 16 lanes. -#[derive(Clone, Copy)] -#[repr(transparent)] -pub struct PackedFp32Avx512(pub [Fp32

; FP32_WIDTH]); - -impl PackedFp32Avx512

{ - const BITS: u32 = 32 - P.leading_zeros(); - - const C: u32 = { - let c = if Self::BITS == 32 { - 0u32.wrapping_sub(P) - } else { - (1u32 << Self::BITS) - P - }; - assert!(P != 0, "modulus must be nonzero"); - assert!(P & 1 == 1, "modulus must be odd"); - assert!( - (c as u64) * (c as u64 + 1) < P as u64, - "C(C+1) < P required for fused canonicalize" - ); - c - }; - - const MASK_U64: u64 = if Self::BITS == 32 { - u32::MAX as u64 - } else { - (1u64 << Self::BITS) - 1 - }; - - /// Whether two Solinas folds suffice to bring the sum of four - /// `(P-1)^2` products into `[0, 2*P)` for the final canonicalize step. - /// Mirrors `PackedFp32Neon::TWO_FOLD_FOUR_PRODUCT_OK`. When `false`, - /// `solinas_reduce` must do a third fold before handing off to - /// `pack_and_canonicalize`. - const TWO_FOLD_FOUR_PRODUCT_OK: bool = { - let c = Self::C as u64; - 4 * c * c + 3 * c <= (1u64 << Self::BITS) - }; - - #[inline(always)] - fn to_vec(self) -> __m512i { - unsafe { transmute(self) } - } - - #[inline(always)] - unsafe fn from_vec(v: __m512i) -> Self { - unsafe { transmute(v) } - } - - /// Multiply each `u64` lane by `C`. Building block of Solinas reduction; - /// the `C == 1` fast path skips the multiply entirely for Mersenne-like - /// primes. - /// - /// Uses `_mm512_mullo_epi64` (AVX-512DQ, single `vpmullq`) for full - /// 64-bit width. The previous implementation used `_mm512_mul_epu32` - /// which only reads the *low 32 bits* of each lane and silently dropped - /// bit 32+ of the input — fine for `BITS == 32` (where the caller's - /// `prod >> 32` always fits in 32 bits) but wrong for `BITS == 31` and - /// `C != 1` where `prod >> 31` can occupy 33 bits. - #[inline(always)] - unsafe fn mul_c_u64(x: __m512i) -> __m512i { - if Self::C == 1 { - x - } else { - let c_vec = _mm512_set1_epi64(Self::C as i64); - _mm512_mullo_epi64(x, c_vec) - } - } - - /// One Solinas fold of a single 64-bit product lane (BITS == 32 only): - /// `(x & (2^32-1)) + C*(x >> 32)`. For a single product `x < 2^64` the - /// high word `x >> 32 < 2^32`, so the result is `< 2^32 + C*2^32 < 2^40`. - /// Used by the `BITS == 32` dot-product path to pre-fold each product so - /// that up to four folded terms (each `< 2^40`) sum below `2^42` without - /// overflowing a `u64`, removing the per-product carry tracking. - #[inline(always)] - unsafe fn fold_product_once(x: __m512i) -> __m512i { - let lo = _mm512_and_si512(x, _mm512_set1_epi64(Self::MASK_U64 as i64)); - let hi = _mm512_srli_epi64::<32>(x); - _mm512_add_epi64(lo, Self::mul_c_u64(hi)) - } - - /// Plonky3-style Mersenne31 multiply (P = 2^31 - 1). Specialized using - /// `_mm512_srli_epi64::<31>` shifts and 16-lane mask blends. Used by - /// the `Mul` impl when `Self::BITS == 31 && Self::C == 1`. - #[inline(always)] - unsafe fn mul_mersenne31_vec(a: __m512i, b: __m512i) -> __m512i { - unsafe { - const EVENS: __mmask16 = 0b0101_0101_0101_0101; - const ODDS: __mmask16 = 0b1010_1010_1010_1010; - - let lhs_evn_dbl = _mm512_add_epi32(a, a); - let rhs_odd = movehdup_epi32_512(b); - let lhs_odd_dbl = _mm512_srli_epi64::<31>(a); - - let prod_odd_dbl = _mm512_mul_epu32(lhs_odd_dbl, rhs_odd); - let prod_evn_dbl = _mm512_mul_epu32(lhs_evn_dbl, b); - - let prod_lo_dbl = - _mm512_mask_blend_epi32(ODDS, prod_evn_dbl, moveldup_epi32_512(prod_odd_dbl)); - let prod_hi = - _mm512_mask_blend_epi32(EVENS, prod_odd_dbl, movehdup_epi32_512(prod_evn_dbl)); - let prod_lo = _mm512_srli_epi32::<1>(prod_lo_dbl); - - let p = _mm512_set1_epi32(P as i32); - let folded = _mm512_add_epi32(prod_lo, prod_hi); - _mm512_min_epu32(folded, _mm512_sub_epi32(folded, p)) - } - } - - /// Vector form of field add: 16-lane add + canonicalize to `[0, P)`. - /// Mirrors `PackedFp32Avx2::add_vec` with native AVX-512 mask compares. - #[inline(always)] - unsafe fn add_vec(a: __m512i, b: __m512i) -> __m512i { - let p = _mm512_set1_epi32(P as i32); - if Self::BITS <= 31 { - let t = _mm512_add_epi32(a, b); - let u = _mm512_sub_epi32(t, p); - _mm512_min_epu32(t, u) - } else { - let c = _mm512_set1_epi32(Self::C as i32); - let t = _mm512_add_epi32(a, b); - let overflow = _mm512_cmplt_epu32_mask(t, a); - let t2 = _mm512_mask_add_epi32(t, overflow, t, c); - let geq_p = _mm512_cmpge_epu32_mask(t2, p); - _mm512_mask_sub_epi32(t2, geq_p, t2, p) - } - } - - /// Vector form of field sub: 16-lane sub + canonicalize to `[0, P)`. - /// Mirrors `PackedFp32Avx2::sub_vec` with native AVX-512 mask compares. - #[inline(always)] - unsafe fn sub_vec(a: __m512i, b: __m512i) -> __m512i { - let p = _mm512_set1_epi32(P as i32); - if Self::BITS <= 31 { - let t = _mm512_sub_epi32(a, b); - let u = _mm512_add_epi32(t, p); - _mm512_min_epu32(t, u) - } else { - let t = _mm512_sub_epi32(a, b); - let underflow = _mm512_cmplt_epu32_mask(a, b); - _mm512_mask_add_epi32(t, underflow, t, p) - } - } - - /// Vector form of field mul: 16-lane Solinas multiply + canonicalize. - #[inline(always)] - unsafe fn mul_vec(a: __m512i, b: __m512i) -> __m512i { - let prod_evn = _mm512_mul_epu32(a, b); - let a_odd = movehdup_epi32_512(a); - let b_odd = movehdup_epi32_512(b); - let prod_odd = _mm512_mul_epu32(a_odd, b_odd); - Self::solinas_reduce(prod_evn, prod_odd) - } - - /// 4-way fused multiply-accumulate with a single end-reduction. - /// Mirrors `PackedFp32Avx2::dot_product_4_vec` at 16 lanes. For - /// `BITS <= 31`, four `(2^31 - 1)^2` products sum below `2^64`, so the - /// raw products accumulate without overflow. For `BITS == 32`, each - /// product is pre-folded once (`< 2^40`) so four folds sum below `2^42`, - /// again overflow-free. Both branches end in a single carry-free - /// `solinas_reduce`; the `if` is a const condition resolved at compile - /// time. - #[inline(always)] - unsafe fn dot_product_4_vec(a: [__m512i; 4], b: [__m512i; 4]) -> __m512i { - let mut sum_evn = _mm512_mul_epu32(a[0], b[0]); - let mut sum_odd = _mm512_mul_epu32(movehdup_epi32_512(a[0]), movehdup_epi32_512(b[0])); - - if Self::BITS <= 31 { - for i in 1..4 { - let prod_evn = _mm512_mul_epu32(a[i], b[i]); - let prod_odd = _mm512_mul_epu32(movehdup_epi32_512(a[i]), movehdup_epi32_512(b[i])); - sum_evn = _mm512_add_epi64(sum_evn, prod_evn); - sum_odd = _mm512_add_epi64(sum_odd, prod_odd); - } - return Self::solinas_reduce(sum_evn, sum_odd); - } - - // BITS == 32: four 32-bit products overflow a `u64` sum, so pre-fold each - // product once (`< 2^40`) and accumulate the folds (`< 4*2^40 < 2^42`), - // which is carry-free, then a single carry-free `solinas_reduce`. - let mut sum_evn = Self::fold_product_once(sum_evn); - let mut sum_odd = Self::fold_product_once(sum_odd); - for i in 1..4 { - let prod_evn = Self::fold_product_once(_mm512_mul_epu32(a[i], b[i])); - let prod_odd = Self::fold_product_once(_mm512_mul_epu32( - movehdup_epi32_512(a[i]), - movehdup_epi32_512(b[i]), - )); - sum_evn = _mm512_add_epi64(sum_evn, prod_evn); - sum_odd = _mm512_add_epi64(sum_odd, prod_odd); - } - Self::solinas_reduce(sum_evn, sum_odd) - } - - /// 3-way fused multiply-accumulate with a single end-reduction. - #[inline(always)] - unsafe fn dot_product_3_vec(a: [__m512i; 3], b: [__m512i; 3]) -> __m512i { - let mut sum_evn = _mm512_mul_epu32(a[0], b[0]); - let mut sum_odd = _mm512_mul_epu32(movehdup_epi32_512(a[0]), movehdup_epi32_512(b[0])); - - if Self::BITS <= 31 { - for i in 1..3 { - let prod_evn = _mm512_mul_epu32(a[i], b[i]); - let prod_odd = _mm512_mul_epu32(movehdup_epi32_512(a[i]), movehdup_epi32_512(b[i])); - sum_evn = _mm512_add_epi64(sum_evn, prod_evn); - sum_odd = _mm512_add_epi64(sum_odd, prod_odd); - } - return Self::solinas_reduce(sum_evn, sum_odd); - } - - // BITS == 32: pre-fold (see `dot_product_4_vec`). - let mut sum_evn = Self::fold_product_once(sum_evn); - let mut sum_odd = Self::fold_product_once(sum_odd); - for i in 1..3 { - let prod_evn = Self::fold_product_once(_mm512_mul_epu32(a[i], b[i])); - let prod_odd = Self::fold_product_once(_mm512_mul_epu32( - movehdup_epi32_512(a[i]), - movehdup_epi32_512(b[i]), - )); - sum_evn = _mm512_add_epi64(sum_evn, prod_evn); - sum_odd = _mm512_add_epi64(sum_odd, prod_odd); - } - Self::solinas_reduce(sum_evn, sum_odd) - } - - /// Multiply by an `FpExt2` non-residue. Recognizes `nr == -1` and `nr == 2` - /// fast paths. - #[inline(always)] - unsafe fn mul_nr_vec(x: __m512i) -> __m512i - where - C: FpExt2Config>, - { - if C::IS_NEG_ONE { - Self::sub_vec(_mm512_setzero_si512(), x) - } else if C::non_residue().0 == 2 { - Self::add_vec(x, x) - } else { - C::mul_non_residue(Self::from_vec(x), Self::broadcast).to_vec() - } - } - - /// Two-or-three-fold Solinas reduction of 8+8 `u64` products → 16 `u32` - /// lanes. - /// - /// The `Self::BITS == 31` branches use immediate-shift - /// `_mm512_srli_epi64::<31>` instead of the generic variable-shift - /// `_mm512_srl_epi64(.., shift)`, mirroring the same specialisation - /// the base-field `Mul` impl uses on Mersenne31, so extension-field - /// operations on Mersenne31 get the same per-shift win. - /// - /// Two folds always suffice when `Self::TWO_FOLD_FOUR_PRODUCT_OK`. When - /// it doesn't (large `C` such that `4*C^2 + 3*C > 2^BITS`), we run a - /// third fold so `pack_and_canonicalize`'s single subtract-and-min step - /// is enough to land in `[0, P)`. Mirrors `PackedFp32Neon::solinas_reduce`. - #[inline(always)] - unsafe fn solinas_reduce(prod_evn: __m512i, prod_odd: __m512i) -> __m512i { - let mask = _mm512_set1_epi64(Self::MASK_U64 as i64); - let shift = _mm_set_epi64x(0, Self::BITS as i64); - - // Fold 1 - let evn_lo = _mm512_and_si512(prod_evn, mask); - let evn_hi = if Self::BITS == 31 { - _mm512_srli_epi64::<31>(prod_evn) - } else { - _mm512_srl_epi64(prod_evn, shift) - }; - let evn_f1 = _mm512_add_epi64(evn_lo, Self::mul_c_u64(evn_hi)); - - let odd_lo = _mm512_and_si512(prod_odd, mask); - let odd_hi = if Self::BITS == 31 { - _mm512_srli_epi64::<31>(prod_odd) - } else { - _mm512_srl_epi64(prod_odd, shift) - }; - let odd_f1 = _mm512_add_epi64(odd_lo, Self::mul_c_u64(odd_hi)); - - // Fold 2 - let evn_f1_lo = _mm512_and_si512(evn_f1, mask); - let evn_f1_hi = if Self::BITS == 31 { - _mm512_srli_epi64::<31>(evn_f1) - } else { - _mm512_srl_epi64(evn_f1, shift) - }; - let evn_f2 = _mm512_add_epi64(evn_f1_lo, Self::mul_c_u64(evn_f1_hi)); - - let odd_f1_lo = _mm512_and_si512(odd_f1, mask); - let odd_f1_hi = if Self::BITS == 31 { - _mm512_srli_epi64::<31>(odd_f1) - } else { - _mm512_srl_epi64(odd_f1, shift) - }; - let odd_f2 = _mm512_add_epi64(odd_f1_lo, Self::mul_c_u64(odd_f1_hi)); - - // Optional third fold for large-C primes (e.g. Generic31Offset32787) - // where two folds leave residue > 2*P. - let (evn_final, odd_final) = if Self::TWO_FOLD_FOUR_PRODUCT_OK { - (evn_f2, odd_f2) - } else { - let evn_f2_lo = _mm512_and_si512(evn_f2, mask); - let evn_f2_hi = if Self::BITS == 31 { - _mm512_srli_epi64::<31>(evn_f2) - } else { - _mm512_srl_epi64(evn_f2, shift) - }; - let odd_f2_lo = _mm512_and_si512(odd_f2, mask); - let odd_f2_hi = if Self::BITS == 31 { - _mm512_srli_epi64::<31>(odd_f2) - } else { - _mm512_srl_epi64(odd_f2, shift) - }; - ( - _mm512_add_epi64(evn_f2_lo, Self::mul_c_u64(evn_f2_hi)), - _mm512_add_epi64(odd_f2_lo, Self::mul_c_u64(odd_f2_hi)), - ) - }; - - Self::pack_and_canonicalize(evn_final, odd_final) - } - - /// Combine 8+8 `u64` lanes into 16 `u32` lanes canonicalized to `[0, P)`. - /// AVX-512 uses native unsigned compare masks and `_mm512_min_epu64`, - /// simplifying both branches vs the AVX2 implementation. - #[inline(always)] - unsafe fn pack_and_canonicalize(evn_f2: __m512i, odd_f2: __m512i) -> __m512i { - if Self::BITS < 32 { - let odd_shifted = _mm512_slli_epi64::<32>(odd_f2); - let combined = _mm512_mask_blend_epi32(0b1010_1010_1010_1010, evn_f2, odd_shifted); - let p = _mm512_set1_epi32(P as i32); - let reduced = _mm512_sub_epi32(combined, p); - _mm512_min_epu32(combined, reduced) - } else { - let p_u64 = _mm512_set1_epi64(P as i64); - - let red_evn = _mm512_sub_epi64(evn_f2, p_u64); - let out_evn = _mm512_min_epu64(evn_f2, red_evn); - - let red_odd = _mm512_sub_epi64(odd_f2, p_u64); - let out_odd = _mm512_min_epu64(odd_f2, red_odd); - - let odd_shifted = _mm512_slli_epi64::<32>(out_odd); - _mm512_mask_blend_epi32(0b1010_1010_1010_1010, out_evn, odd_shifted) - } - } -} - -impl Default for PackedFp32Avx512

{ - #[inline] - fn default() -> Self { - Self([Fp32(0); FP32_WIDTH]) - } -} - -impl fmt::Debug for PackedFp32Avx512

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("PackedFp32Avx512").field(&self.0).finish() - } -} - -impl PartialEq for PackedFp32Avx512

{ - #[inline] - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Eq for PackedFp32Avx512

{} - -impl Add for PackedFp32Avx512

{ - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - unsafe { Self::from_vec(Self::add_vec(self.to_vec(), rhs.to_vec())) } - } -} - -impl Sub for PackedFp32Avx512

{ - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - unsafe { Self::from_vec(Self::sub_vec(self.to_vec(), rhs.to_vec())) } - } -} - -impl Mul for PackedFp32Avx512

{ - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self { - unsafe { - let a = self.to_vec(); - let b = rhs.to_vec(); - - if Self::BITS == 31 && Self::C == 1 { - return Self::from_vec(Self::mul_mersenne31_vec(a, b)); - } - - let prod_evn = _mm512_mul_epu32(a, b); - let a_odd = movehdup_epi32_512(a); - let b_odd = movehdup_epi32_512(b); - let prod_odd = _mm512_mul_epu32(a_odd, b_odd); - - let mask = _mm512_set1_epi64(Self::MASK_U64 as i64); - let shift = _mm_set_epi64x(0, Self::BITS as i64); - - // Fold 1 - let evn_lo = _mm512_and_si512(prod_evn, mask); - let evn_hi = if Self::BITS == 31 { - _mm512_srli_epi64::<31>(prod_evn) - } else { - _mm512_srl_epi64(prod_evn, shift) - }; - let evn_f1 = _mm512_add_epi64(evn_lo, Self::mul_c_u64(evn_hi)); - - let odd_lo = _mm512_and_si512(prod_odd, mask); - let odd_hi = if Self::BITS == 31 { - _mm512_srli_epi64::<31>(prod_odd) - } else { - _mm512_srl_epi64(prod_odd, shift) - }; - let odd_f1 = _mm512_add_epi64(odd_lo, Self::mul_c_u64(odd_hi)); - - // Fold 2 - let evn_f1_lo = _mm512_and_si512(evn_f1, mask); - let evn_f1_hi = if Self::BITS == 31 { - _mm512_srli_epi64::<31>(evn_f1) - } else { - _mm512_srl_epi64(evn_f1, shift) - }; - let evn_f2 = _mm512_add_epi64(evn_f1_lo, Self::mul_c_u64(evn_f1_hi)); - - let odd_f1_lo = _mm512_and_si512(odd_f1, mask); - let odd_f1_hi = if Self::BITS == 31 { - _mm512_srli_epi64::<31>(odd_f1) - } else { - _mm512_srl_epi64(odd_f1, shift) - }; - let odd_f2 = _mm512_add_epi64(odd_f1_lo, Self::mul_c_u64(odd_f1_hi)); - - // Recombine + canonicalize. For `BITS == 32` the two-fold residue - // can land in `[2^32, 2*P)` (up to `2^32 + C^2`), so the subtract - // must happen on the full 64-bit lanes before packing; a 32-bit - // recombine would drop bit 32. `pack_and_canonicalize` does the - // 64-bit subtract for `BITS == 32` and is identical to the inline - // 32-bit recombine for `BITS < 32`. - Self::from_vec(Self::pack_and_canonicalize(evn_f2, odd_f2)) - } - } -} - -impl AddAssign for PackedFp32Avx512

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for PackedFp32Avx512

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for PackedFp32Avx512

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl PackedField for PackedFp32Avx512

{ - const WIDTH: usize = FP32_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Scalar, - { - Self([ - f(0), - f(1), - f(2), - f(3), - f(4), - f(5), - f(6), - f(7), - f(8), - f(9), - f(10), - f(11), - f(12), - f(13), - f(14), - f(15), - ]) - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Scalar { - debug_assert!(lane < FP32_WIDTH); - self.0[lane] - } - - type Scalar = Fp32

; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self([value; FP32_WIDTH]) - } - - #[inline(always)] - fn fp_ext2_mul(a0: Self, a1: Self, b0: Self, b1: Self) -> (Self, Self) - where - C: FpExt2Config, - { - unsafe { - let a0 = a0.to_vec(); - let a1 = a1.to_vec(); - let b0 = b0.to_vec(); - let b1 = b1.to_vec(); - - let v0 = Self::mul_vec(a0, b0); - let v1 = Self::mul_vec(a1, b1); - let cross = Self::mul_vec(Self::add_vec(a0, a1), Self::add_vec(b0, b1)); - - ( - Self::from_vec(Self::add_vec(v0, Self::mul_nr_vec::(v1))), - Self::from_vec(Self::sub_vec(Self::sub_vec(cross, v0), v1)), - ) - } - } - - #[inline(always)] - fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { - unsafe { - let [a0, a1, a2, a3] = a.map(Self::to_vec); - let [b0, b1, b2, b3] = b.map(Self::to_vec); - let two_b1 = Self::add_vec(b1, b1); - let two_b2 = Self::add_vec(b2, b2); - let two_b3 = Self::add_vec(b3, b3); - let b0_plus_b2 = Self::add_vec(b0, b2); - let b1_plus_b3 = Self::add_vec(b1, b3); - let b1_minus_b3 = Self::sub_vec(b1, b3); - let b0_minus_b2 = Self::sub_vec(b0, b2); - [ - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [b0, two_b1, two_b2, two_b3], - )), - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [b1, b0_plus_b2, b1_plus_b3, b2], - )), - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [b2, b1_plus_b3, b0, b1_minus_b3], - )), - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [b3, b2, b1_minus_b3, b0_minus_b2], - )), - ] - } - } - - #[inline(always)] - fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { - unsafe { - let [a0, a1, a2, a3] = a.map(Self::to_vec); - let zero = _mm512_setzero_si512(); - let two_a1 = Self::add_vec(a1, a1); - let two_a2 = Self::add_vec(a2, a2); - let two_a3 = Self::add_vec(a3, a3); - let neg_a3 = Self::sub_vec(zero, a3); - let neg_two_a3 = Self::sub_vec(zero, two_a3); - [ - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [a0, two_a1, two_a2, two_a3], - )), - Self::from_vec(Self::dot_product_3_vec( - [a0, a1, a2], - [two_a1, two_a2, two_a3], - )), - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a1, a3], - [two_a2, a1, two_a3, neg_a3], - )), - Self::from_vec(Self::dot_product_3_vec( - [a0, a1, a2], - [two_a3, two_a2, neg_two_a3], - )), - ] - } - } - - #[inline(always)] - fn fp_ext4_inverse(a: [Self; 4]) -> Option<[Self; 4]> - where - Self::Scalar: FieldCore, - { - unsafe { - let [a0, a1, a2, a3] = a.map(Self::to_vec); - let zero = _mm512_setzero_si512(); - let x0 = a0; - let x1 = a2; - let y0 = Self::sub_vec(a1, a3); - let y1 = a3; - - let x1_square = Self::mul_vec(x1, x1); - let y1_square = Self::mul_vec(y1, y1); - let aa0 = Self::add_vec(Self::mul_vec(x0, x0), Self::add_vec(x1_square, x1_square)); - let aa1 = { - let x0x1 = Self::mul_vec(x0, x1); - Self::add_vec(x0x1, x0x1) - }; - let bb0 = Self::add_vec(Self::mul_vec(y0, y0), Self::add_vec(y1_square, y1_square)); - let bb1 = { - let y0y1 = Self::mul_vec(y0, y1); - Self::add_vec(y0y1, y0y1) - }; - let nr_bb0 = Self::add_vec(Self::add_vec(bb0, bb0), Self::add_vec(bb1, bb1)); - let nr_bb1 = Self::add_vec(bb0, Self::add_vec(bb1, bb1)); - let norm0 = Self::sub_vec(aa0, nr_bb0); - let norm1 = Self::sub_vec(aa1, nr_bb1); - - let inv_norm_base = { - let norm1_square = Self::mul_vec(norm1, norm1); - let norm_base = Self::sub_vec( - Self::mul_vec(norm0, norm0), - Self::add_vec(norm1_square, norm1_square), - ); - Self::from_vec(norm_base).inverse()?.to_vec() - }; - let inv_norm0 = Self::mul_vec(norm0, inv_norm_base); - let inv_norm1 = Self::mul_vec(Self::sub_vec(zero, norm1), inv_norm_base); - - let v0 = Self::mul_vec(x0, inv_norm0); - let v1 = Self::mul_vec(x1, inv_norm1); - let constant0 = Self::add_vec(v0, Self::add_vec(v1, v1)); - let constant1 = Self::sub_vec( - Self::sub_vec( - Self::mul_vec(Self::add_vec(x0, x1), Self::add_vec(inv_norm0, inv_norm1)), - v0, - ), - v1, - ); - - let neg_y0 = Self::sub_vec(zero, y0); - let neg_y1 = Self::sub_vec(zero, y1); - let w0 = Self::mul_vec(neg_y0, inv_norm0); - let w1 = Self::mul_vec(neg_y1, inv_norm1); - let e1_coeff0 = Self::add_vec(w0, Self::add_vec(w1, w1)); - let e1_coeff1 = Self::sub_vec( - Self::sub_vec( - Self::mul_vec( - Self::add_vec(neg_y0, neg_y1), - Self::add_vec(inv_norm0, inv_norm1), - ), - w0, - ), - w1, - ); - - Some([ - Self::from_vec(constant0), - Self::from_vec(Self::add_vec(e1_coeff0, e1_coeff1)), - Self::from_vec(constant1), - Self::from_vec(e1_coeff1), - ]) - } - } -} diff --git a/crates/jolt-field/src/packed/avx512/fp64.rs b/crates/jolt-field/src/packed/avx512/fp64.rs deleted file mode 100644 index 6d60e6ca9d..0000000000 --- a/crates/jolt-field/src/packed/avx512/fp64.rs +++ /dev/null @@ -1,243 +0,0 @@ -use super::*; - -/// Number of `Fp64` lanes in an AVX-512 packed vector. -pub(crate) const FP64_WIDTH: usize = 8; - -/// AVX-512 packed arithmetic for `Fp64

`, processing 8 lanes. -#[derive(Clone, Copy)] -#[repr(transparent)] -pub struct PackedFp64Avx512(pub [Fp64

; FP64_WIDTH]); - -impl PackedFp64Avx512

{ - const BITS: u32 = 64 - P.leading_zeros(); - - const C_LO: u64 = { - let c = if Self::BITS == 64 { - 0u64.wrapping_sub(P) - } else { - (1u64 << Self::BITS) - P - }; - assert!(P != 0, "modulus must be nonzero"); - assert!(P & 1 == 1, "modulus must be odd"); - c - }; - - const MASK64: u64 = if Self::BITS < 64 { - (1u64 << Self::BITS) - 1 - } else { - u64::MAX - }; - - #[inline(always)] - fn to_vec(self) -> __m512i { - unsafe { transmute(self) } - } - - #[inline(always)] - unsafe fn from_vec(v: __m512i) -> Self { - unsafe { transmute(v) } - } - - /// Vectorized 128-bit Solinas reduction for p = 2^BITS - C. - /// Given (hi, lo) = 128-bit product, computes result ≡ (hi*2^64 + lo) mod p. - #[inline] - unsafe fn reduce128_vec(hi: __m512i, lo: __m512i) -> __m512i { - if Self::BITS < 64 { - Self::reduce128_small_k(hi, lo) - } else { - Self::reduce128_full_k(hi, lo) - } - } - - /// Reduction for BITS < 64 (e.g. 40-bit prime). No overflow issues: all - /// intermediates fit in u64. - #[inline] - unsafe fn reduce128_small_k(hi: __m512i, lo: __m512i) -> __m512i { - let mask_k = _mm512_set1_epi64(Self::MASK64 as i64); - let c_vec = _mm512_set1_epi64(Self::C_LO as i64); - let p_vec = _mm512_set1_epi64(P as i64); - let shift_k = _mm_set_epi64x(0, Self::BITS as i64); - let shift_64mk = _mm_set_epi64x(0, (64 - Self::BITS) as i64); - - let lo_k = _mm512_and_si512(lo, mask_k); - let lo_upper = _mm512_srl_epi64(lo, shift_k); - let hi_shifted = _mm512_sll_epi64(hi, shift_64mk); - let hi_k = _mm512_or_si512(lo_upper, hi_shifted); - - // c * hi_k: hi_k may exceed 32 bits, split into lo32 and top - let c_hi_lo = _mm512_mul_epu32(c_vec, hi_k); - let hi_k_top = _mm512_srli_epi64::<32>(hi_k); - let c_hi_top = _mm512_mul_epu32(c_vec, hi_k_top); - let c_hi_top_shifted = _mm512_slli_epi64::<32>(c_hi_top); - let c_hi_full = _mm512_add_epi64(c_hi_lo, c_hi_top_shifted); - - let fold1 = _mm512_add_epi64(lo_k, c_hi_full); - - let fold1_lo_k = _mm512_and_si512(fold1, mask_k); - let fold1_hi = _mm512_srl_epi64(fold1, shift_k); - let c_fold1_hi = _mm512_mul_epu32(c_vec, fold1_hi); - let fold2 = _mm512_add_epi64(fold1_lo_k, c_fold1_hi); - - let reduced = _mm512_sub_epi64(fold2, p_vec); - _mm512_min_epu64(fold2, reduced) - } - - /// Reduction for BITS == 64 (e.g. p = 2^64 - 87). Tracks overflow from - /// c*hi exceeding 64 bits, using native unsigned comparisons. - #[inline] - unsafe fn reduce128_full_k(hi: __m512i, lo: __m512i) -> __m512i { - let c_vec = _mm512_set1_epi64(Self::C_LO as i64); - let p_vec = _mm512_set1_epi64(P as i64); - let one = _mm512_set1_epi64(1); - - // c * hi_lo32 - let c_hi_lo = _mm512_mul_epu32(c_vec, hi); - // c * hi_hi32 - let hi_hi = _mm512_srli_epi64::<32>(hi); - let c_hi_hi = _mm512_mul_epu32(c_vec, hi_hi); - - let c_hi_hi_lo32 = _mm512_slli_epi64::<32>(c_hi_hi); - let c_hi_carry = _mm512_srli_epi64::<32>(c_hi_hi); - - // Lower 64 bits of c * hi - let sum_lo = _mm512_add_epi64(c_hi_lo, c_hi_hi_lo32); - let carry0 = _mm512_cmplt_epu64_mask(sum_lo, c_hi_lo); - let overflow = _mm512_mask_add_epi64(c_hi_carry, carry0, c_hi_carry, one); - - // lo + sum_lo - let s = _mm512_add_epi64(lo, sum_lo); - let carry1 = _mm512_cmplt_epu64_mask(s, lo); - let total_overflow = _mm512_mask_add_epi64(overflow, carry1, overflow, one); - - // Fold overflow: total_overflow * c (at most ~2^15) - let final_corr = _mm512_mul_epu32(c_vec, total_overflow); - let result = _mm512_add_epi64(s, final_corr); - let carry_f = _mm512_cmplt_epu64_mask(result, s); - let result = _mm512_mask_add_epi64(result, carry_f, result, c_vec); - - let ge_mask = _mm512_cmpge_epu64_mask(result, p_vec); - _mm512_mask_sub_epi64(result, ge_mask, result, p_vec) - } -} - -impl Default for PackedFp64Avx512

{ - #[inline] - fn default() -> Self { - Self([Fp64(0); FP64_WIDTH]) - } -} - -impl fmt::Debug for PackedFp64Avx512

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("PackedFp64Avx512").field(&self.0).finish() - } -} - -impl PartialEq for PackedFp64Avx512

{ - #[inline] - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Eq for PackedFp64Avx512

{} - -impl Add for PackedFp64Avx512

{ - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - unsafe { - let a = self.to_vec(); - let b = rhs.to_vec(); - let p = _mm512_set1_epi64(P as i64); - - let result = if Self::BITS <= 62 { - let s = _mm512_add_epi64(a, b); - let geq_p = _mm512_cmpge_epu64_mask(s, p); - _mm512_mask_sub_epi64(s, geq_p, s, p) - } else { - let s = _mm512_add_epi64(a, b); - let overflow = _mm512_cmplt_epu64_mask(s, a); - let c = _mm512_set1_epi64(Self::C_LO as i64); - let geq_p = _mm512_cmpge_epu64_mask(s, p); - let no_of = _mm512_mask_sub_epi64(s, geq_p, s, p); - let s_plus_c = _mm512_add_epi64(s, c); - _mm512_mask_blend_epi64(overflow, no_of, s_plus_c) - }; - - Self::from_vec(result) - } - } -} - -impl Sub for PackedFp64Avx512

{ - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - unsafe { - let a = self.to_vec(); - let b = rhs.to_vec(); - let p = _mm512_set1_epi64(P as i64); - let d = _mm512_sub_epi64(a, b); - let underflow = _mm512_cmplt_epu64_mask(a, b); - Self::from_vec(_mm512_mask_add_epi64(d, underflow, d, p)) - } - } -} - -impl Mul for PackedFp64Avx512

{ - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self { - unsafe { - let (hi, lo) = mul64_64_512(self.to_vec(), rhs.to_vec()); - Self::from_vec(Self::reduce128_vec(hi, lo)) - } - } -} - -impl AddAssign for PackedFp64Avx512

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for PackedFp64Avx512

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for PackedFp64Avx512

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl PackedField for PackedFp64Avx512

{ - const WIDTH: usize = FP64_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Scalar, - { - Self([f(0), f(1), f(2), f(3), f(4), f(5), f(6), f(7)]) - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Scalar { - debug_assert!(lane < FP64_WIDTH); - self.0[lane] - } - - type Scalar = Fp64

; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self([value; FP64_WIDTH]) - } -} diff --git a/crates/jolt-field/src/packed/avx512/mod.rs b/crates/jolt-field/src/packed/avx512/mod.rs deleted file mode 100644 index 67bc5d74a9..0000000000 --- a/crates/jolt-field/src/packed/avx512/mod.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! AVX-512 packed backends for Fp32, Fp64, Fp128. -//! -//! Requires AVX-512F + AVX-512DQ. Uses native unsigned comparisons and mask -//! registers for branchless conditionals. - -#![expect( - clippy::undocumented_unsafe_blocks, - reason = "ported AVX-512 kernels retain their audited intrinsic-level invariants" -)] - -use super::PackedField; -use crate::ext::FpExt2Config; -use crate::FieldCore; -use crate::{Fp128, Fp32, Fp64}; -use core::arch::x86_64::*; -use core::fmt; -use core::mem::transmute; -use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; - -#[inline(always)] -unsafe fn movehdup_epi32_512(x: __m512i) -> __m512i { - _mm512_castps_si512(_mm512_movehdup_ps(_mm512_castsi512_ps(x))) -} - -#[inline(always)] -unsafe fn moveldup_epi32_512(x: __m512i) -> __m512i { - _mm512_castps_si512(_mm512_moveldup_ps(_mm512_castsi512_ps(x))) -} - -/// 64×64→128 schoolbook multiply using 32×32→64 partial products. -/// Returns (hi, lo) representing the 128-bit product. -/// Adapted from plonky3's Goldilocks AVX-512 backend. -#[inline] -unsafe fn mul64_64_512(x: __m512i, y: __m512i) -> (__m512i, __m512i) { - let x_hi = movehdup_epi32_512(x); - let y_hi = movehdup_epi32_512(y); - - let mul_ll = _mm512_mul_epu32(x, y); - let mul_lh = _mm512_mul_epu32(x, y_hi); - let mul_hl = _mm512_mul_epu32(x_hi, y); - let mul_hh = _mm512_mul_epu32(x_hi, y_hi); - - let mul_ll_hi = _mm512_srli_epi64::<32>(mul_ll); - let t0 = _mm512_add_epi64(mul_hl, mul_ll_hi); - let mask32 = _mm512_set1_epi64(0xFFFF_FFFF_i64); - let t0_lo = _mm512_and_si512(t0, mask32); - let t0_hi = _mm512_srli_epi64::<32>(t0); - let t1 = _mm512_add_epi64(mul_lh, t0_lo); - let t2 = _mm512_add_epi64(mul_hh, t0_hi); - let t1_hi = _mm512_srli_epi64::<32>(t1); - let res_hi = _mm512_add_epi64(t2, t1_hi); - - let t1_lo = moveldup_epi32_512(t1); - let res_lo = _mm512_mask_blend_epi32(0b0101_0101_0101_0101, t1_lo, mul_ll); - - (res_hi, res_lo) -} - -mod fp128; -mod fp32; -mod fp64; -pub(crate) use fp128::*; -pub(crate) use fp32::*; -pub(crate) use fp64::*; diff --git a/crates/jolt-field/src/packed/ext/mod.rs b/crates/jolt-field/src/packed/ext/mod.rs deleted file mode 100644 index b7e73402b4..0000000000 --- a/crates/jolt-field/src/packed/ext/mod.rs +++ /dev/null @@ -1,455 +0,0 @@ -//! Packed extension field types using transpose-based packing. -//! -//! A `PackedFpExt2` stores `[PF; 2]` where `PF` is the packed base field. -//! Each `PF` lane contains the corresponding coefficient of an `FpExt2` element. -//! This enables WIDTH-fold parallel arithmetic over `FpExt2` using existing SIMD -//! base-field operations. - -#![expect( - clippy::expl_impl_clone_on_copy, - reason = "manual Clone avoids adding irrelevant generic Clone bounds" -)] - -use crate::ext::{ExtMulBackend, FpExt2, FpExt2Config, FpExt4, FpExt8}; -use crate::packed::{HasPacking, PackedField}; -use crate::FieldCore; -use core::ops::{Add, Mul, Sub}; - -/// Packed `FpExt2` elements stored in transpose layout: `[PF; 2]`. -/// -/// If `PF` has width `W`, this represents `W` parallel `FpExt2` values. -pub struct PackedFpExt2, PF: PackedField> { - /// Degree-0 coefficient (packed across SIMD lanes). - pub c0: PF, - /// Degree-1 coefficient (packed across SIMD lanes). - pub c1: PF, - _marker: std::marker::PhantomData (F, C)>, -} - -impl, PF: PackedField> Clone - for PackedFpExt2 -{ - fn clone(&self) -> Self { - *self - } -} - -impl, PF: PackedField> Copy - for PackedFpExt2 -{ -} - -impl, PF: PackedField> std::fmt::Debug - for PackedFpExt2 -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PackedFpExt2").finish_non_exhaustive() - } -} - -impl, PF: PackedField> PackedFpExt2 { - /// Create a `PackedFpExt2` from its two packed coefficients. - #[inline] - pub fn new(c0: PF, c1: PF) -> Self { - Self { - c0, - c1, - _marker: std::marker::PhantomData, - } - } -} - -impl Add for PackedFpExt2 -where - F: FieldCore, - C: FpExt2Config, - PF: PackedField, -{ - type Output = Self; - #[inline(always)] - fn add(self, rhs: Self) -> Self { - Self::new(self.c0 + rhs.c0, self.c1 + rhs.c1) - } -} - -impl Sub for PackedFpExt2 -where - F: FieldCore, - C: FpExt2Config, - PF: PackedField, -{ - type Output = Self; - #[inline(always)] - fn sub(self, rhs: Self) -> Self { - Self::new(self.c0 - rhs.c0, self.c1 - rhs.c1) - } -} - -impl Mul for PackedFpExt2 -where - F: FieldCore, - C: FpExt2Config, - PF: PackedField, -{ - type Output = Self; - #[inline(always)] - fn mul(self, rhs: Self) -> Self { - let (c0, c1) = PF::fp_ext2_mul::(self.c0, self.c1, rhs.c0, rhs.c1); - Self::new(c0, c1) - } -} - -impl PackedField for PackedFpExt2 -where - F: FieldCore + 'static, - C: FpExt2Config + 'static, - PF: PackedField, -{ - const WIDTH: usize = PF::WIDTH; - - fn from_fn(mut f: G) -> Self - where - G: FnMut(usize) -> Self::Scalar, - { - let mut c0s = Vec::with_capacity(PF::WIDTH); - let mut c1s = Vec::with_capacity(PF::WIDTH); - for i in 0..PF::WIDTH { - let val = f(i); - c0s.push(val.coeffs[0]); - c1s.push(val.coeffs[1]); - } - Self::new(PF::from_fn(|i| c0s[i]), PF::from_fn(|i| c1s[i])) - } - - fn extract(&self, lane: usize) -> Self::Scalar { - FpExt2::new(self.c0.extract(lane), self.c1.extract(lane)) - } - type Scalar = FpExt2; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self::new( - PF::broadcast(value.coeffs[0]), - PF::broadcast(value.coeffs[1]), - ) - } - - #[inline(always)] - fn inverse(self) -> Option - where - Self::Scalar: FieldCore, - { - let norm = self.c0 * self.c0 - C::mul_non_residue(self.c1 * self.c1, PF::broadcast); - let inv_norm = norm.inverse()?; - let zero = PF::broadcast(F::zero()); - Some(Self::new(self.c0 * inv_norm, (zero - self.c1) * inv_norm)) - } -} - -impl HasPacking for FpExt2 -where - F: FieldCore + HasPacking + 'static, - C: FpExt2Config + 'static, -{ - type Packing = PackedFpExt2; -} - -/// Packed `FpExt4` elements stored as `[PF; 4]`. -pub struct PackedFpExt4> { - /// Packed coefficients in `[1, e1, e2, e3]` order. - pub coeffs: [PF; 4], - _marker: std::marker::PhantomData F>, -} - -impl Clone for PackedFpExt4 -where - F: FieldCore, - PF: PackedField, -{ - fn clone(&self) -> Self { - *self - } -} - -impl Copy for PackedFpExt4 -where - F: FieldCore, - PF: PackedField, -{ -} - -impl std::fmt::Debug for PackedFpExt4 -where - F: FieldCore, - PF: PackedField, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PackedFpExt4").finish_non_exhaustive() - } -} - -impl PackedFpExt4 -where - F: FieldCore, - PF: PackedField, -{ - /// Create a packed value from packed ring-subfield coefficients. - #[inline] - pub fn new(coeffs: [PF; 4]) -> Self { - Self { - coeffs, - _marker: std::marker::PhantomData, - } - } - - /// Square using the packed ring-subfield backend hook. - #[inline(always)] - pub fn square(self) -> Self { - Self::new(PF::fp_ext4_square(self.coeffs)) - } -} - -impl Add for PackedFpExt4 -where - F: FieldCore, - PF: PackedField, -{ - type Output = Self; - #[inline(always)] - fn add(self, rhs: Self) -> Self { - let [a0, a1, a2, a3] = self.coeffs; - let [b0, b1, b2, b3] = rhs.coeffs; - Self::new([a0 + b0, a1 + b1, a2 + b2, a3 + b3]) - } -} - -impl Sub for PackedFpExt4 -where - F: FieldCore, - PF: PackedField, -{ - type Output = Self; - #[inline(always)] - fn sub(self, rhs: Self) -> Self { - let [a0, a1, a2, a3] = self.coeffs; - let [b0, b1, b2, b3] = rhs.coeffs; - Self::new([a0 - b0, a1 - b1, a2 - b2, a3 - b3]) - } -} - -impl Mul for PackedFpExt4 -where - F: FieldCore, - PF: PackedField, -{ - type Output = Self; - #[inline(always)] - fn mul(self, rhs: Self) -> Self { - Self::new(PF::fp_ext4_mul(self.coeffs, rhs.coeffs)) - } -} - -impl PackedField for PackedFpExt4 -where - F: FieldCore + ExtMulBackend + 'static, - PF: PackedField, -{ - const WIDTH: usize = PF::WIDTH; - - fn from_fn(mut f: G) -> Self - where - G: FnMut(usize) -> Self::Scalar, - { - let mut coeffs: [Vec; 4] = std::array::from_fn(|_| Vec::with_capacity(PF::WIDTH)); - for i in 0..PF::WIDTH { - let val = f(i); - for (j, coeff) in val.coeffs.into_iter().enumerate() { - coeffs[j].push(coeff); - } - } - Self::new(std::array::from_fn(|j| PF::from_fn(|i| coeffs[j][i]))) - } - - fn extract(&self, lane: usize) -> Self::Scalar { - FpExt4::new(std::array::from_fn(|j| self.coeffs[j].extract(lane))) - } - type Scalar = FpExt4; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self::new(std::array::from_fn(|i| PF::broadcast(value.coeffs[i]))) - } - - #[inline(always)] - fn square(self) -> Self { - Self::new(PF::fp_ext4_square(self.coeffs)) - } - - #[inline(always)] - fn inverse(self) -> Option - where - Self::Scalar: FieldCore, - { - Some(Self::new(PF::fp_ext4_inverse(self.coeffs)?)) - } -} - -impl HasPacking for FpExt4 -where - F: FieldCore + HasPacking + ExtMulBackend + 'static, -{ - type Packing = PackedFpExt4; -} - -/// Packed `FpExt8` elements stored in transpose layout: `[PF; 8]`. -/// -/// Each `PF` lane contains one coefficient of a degree-8 Chebyshev-basis element. -pub struct PackedFpExt8> { - /// Packed coefficients in `[1, e1, ..., e7]` order. - pub coeffs: [PF; 8], - _marker: std::marker::PhantomData F>, -} - -impl Clone for PackedFpExt8 -where - F: FieldCore, - PF: PackedField, -{ - fn clone(&self) -> Self { - *self - } -} - -impl Copy for PackedFpExt8 -where - F: FieldCore, - PF: PackedField, -{ -} - -impl std::fmt::Debug for PackedFpExt8 -where - F: FieldCore, - PF: PackedField, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PackedFpExt8").finish_non_exhaustive() - } -} - -impl PackedFpExt8 -where - F: FieldCore, - PF: PackedField, -{ - /// Create a packed value from packed ring-subfield coefficients. - #[inline] - pub fn new(coeffs: [PF; 8]) -> Self { - Self { - coeffs, - _marker: std::marker::PhantomData, - } - } -} - -impl Add for PackedFpExt8 -where - F: FieldCore, - PF: PackedField, -{ - type Output = Self; - #[inline(always)] - fn add(self, rhs: Self) -> Self { - Self::new(std::array::from_fn(|i| self.coeffs[i] + rhs.coeffs[i])) - } -} - -impl Sub for PackedFpExt8 -where - F: FieldCore, - PF: PackedField, -{ - type Output = Self; - #[inline(always)] - fn sub(self, rhs: Self) -> Self { - Self::new(std::array::from_fn(|i| self.coeffs[i] - rhs.coeffs[i])) - } -} - -impl Mul for PackedFpExt8 -where - F: FieldCore, - PF: PackedField, -{ - type Output = Self; - #[inline(always)] - fn mul(self, rhs: Self) -> Self { - Self::new(PF::fp_ext8_mul(self.coeffs, rhs.coeffs)) - } -} - -impl PackedField for PackedFpExt8 -where - F: FieldCore + ExtMulBackend + 'static, - PF: PackedField, -{ - const WIDTH: usize = PF::WIDTH; - - fn from_fn(mut f: G) -> Self - where - G: FnMut(usize) -> Self::Scalar, - { - let mut coeffs: [Vec; 8] = std::array::from_fn(|_| Vec::with_capacity(PF::WIDTH)); - for i in 0..PF::WIDTH { - let val = f(i); - for (j, coeff) in val.coeffs.into_iter().enumerate() { - coeffs[j].push(coeff); - } - } - Self::new(std::array::from_fn(|j| PF::from_fn(|i| coeffs[j][i]))) - } - - fn extract(&self, lane: usize) -> Self::Scalar { - FpExt8::new(std::array::from_fn(|j| self.coeffs[j].extract(lane))) - } - type Scalar = FpExt8; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self::new(std::array::from_fn(|i| PF::broadcast(value.coeffs[i]))) - } - - #[inline(always)] - fn square(self) -> Self { - Self::new(PF::fp_ext8_square(self.coeffs)) - } - - #[inline(always)] - fn inverse(self) -> Option - where - Self::Scalar: FieldCore, - { - // FpExt8 inversion uses Gaussian elimination — delegate lane by lane. - let mut coeffs: [Vec; 8] = std::array::from_fn(|_| Vec::with_capacity(PF::WIDTH)); - for lane in 0..PF::WIDTH { - let scalar = self.extract(lane); - let inv = scalar.inverse()?; - for (j, c) in inv.coeffs.into_iter().enumerate() { - coeffs[j].push(c); - } - } - Some(Self::new(std::array::from_fn(|j| { - PF::from_fn(|i| coeffs[j][i]) - }))) - } -} - -impl HasPacking for FpExt8 -where - F: FieldCore + HasPacking + ExtMulBackend + 'static, -{ - type Packing = PackedFpExt8; -} - -#[cfg(test)] -mod tests; diff --git a/crates/jolt-field/src/packed/ext/tests.rs b/crates/jolt-field/src/packed/ext/tests.rs deleted file mode 100644 index b66d0a5dae..0000000000 --- a/crates/jolt-field/src/packed/ext/tests.rs +++ /dev/null @@ -1,484 +0,0 @@ -#![expect( - clippy::unreadable_literal, - clippy::unwrap_used, - reason = "tests assert field identities and retain copied field constants" -)] - -use super::*; -use crate::ext::{Ext2, FpExt2, FpExt4, TwoNr}; -use crate::FieldCore; -use crate::Fp32; -use crate::Fp64; -use crate::Prime31Offset19; -use crate::Prime32Offset99; -use crate::Prime64Offset59; -use crate::RingCore; -use rand::rngs::StdRng; -use rand::SeedableRng; - -type F = Fp64<4294967197>; -type E2 = Ext2; -type R4 = FpExt4; -type PE2 = PackedFpExt2::Packing>; -type PR4 = PackedFpExt4::Packing>; -type Mersenne31 = Fp32<{ (1u32 << 31) - 1 }>; -type Generic30Offset16397 = Fp32<{ (1u32 << 30) - 16_397 }>; -type Generic31Offset61 = Fp32<{ (1u32 << 31) - 61 }>; -type Generic31Offset32787 = Fp32<{ (1u32 << 31) - 32_787 }>; -type PR4Prime31 = PackedFpExt4::Packing>; -type PR4Mersenne31 = PackedFpExt4::Packing>; -type PR4Generic30Offset16397 = - PackedFpExt4::Packing>; -type PR4Generic31Offset61 = - PackedFpExt4::Packing>; -type PR4Generic31Offset32787 = - PackedFpExt4::Packing>; -type R4Prime32 = FpExt4; -type PR4Prime32 = PackedFpExt4::Packing>; -type E2Full = FpExt2; -type PE2Full = PackedFpExt2::Packing>; - -fn fp32_ext_edge_values() -> [Fp32

; 4] { - [ - Fp32::

::from_canonical_u32(P - 1), - Fp32::

::from_canonical_u32(P - 2), - Fp32::

::from_canonical_u32((P - 1) / 2), - Fp32::

::one(), - ] -} - -fn check_packed_fp_ext4_edge() -where - PR4: PackedField>>, -{ - let values = fp32_ext_edge_values::

(); - let elem = |offset: usize| { - FpExt4::>::new(std::array::from_fn(|j| values[(offset + j) % values.len()])) - }; - let a = PR4::from_fn(elem); - let b = PR4::from_fn(|i| elem(i + 1)); - let product = a * b; - let square = a.square(); - - for lane in 0..PR4::WIDTH { - let lhs = elem(lane); - let rhs = elem(lane + 1); - assert_eq!( - product.extract(lane), - lhs * rhs, - "packed FpExt4 edge mul mismatch at lane {lane}" - ); - assert_eq!( - square.extract(lane), - lhs.square(), - "packed FpExt4 edge square mismatch at lane {lane}" - ); - } -} - -#[test] -fn packed_fp_ext2_add() { - let mut rng = StdRng::seed_from_u64(100); - let width = ::WIDTH; - let a_elems: Vec = (0..width).map(|_| E2::random(&mut rng)).collect(); - let b_elems: Vec = (0..width).map(|_| E2::random(&mut rng)).collect(); - - let pa = PE2::from_fn(|i| a_elems[i]); - let pb = PE2::from_fn(|i| b_elems[i]); - let pc = pa + pb; - - for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { - assert_eq!(pc.extract(i), *a + *b); - } -} - -#[test] -fn packed_fp_ext2_mul() { - let mut rng = StdRng::seed_from_u64(200); - let width = ::WIDTH; - let a_elems: Vec = (0..width).map(|_| E2::random(&mut rng)).collect(); - let b_elems: Vec = (0..width).map(|_| E2::random(&mut rng)).collect(); - - let pa = PE2::from_fn(|i| a_elems[i]); - let pb = PE2::from_fn(|i| b_elems[i]); - let pc = pa * pb; - - for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { - assert_eq!( - pc.extract(i), - *a * *b, - "packed FpExt2 mul mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext2_mul_full_word_fp64() { - let mut rng = StdRng::seed_from_u64(201); - let width = ::WIDTH; - let a_elems: Vec = (0..width).map(|_| E2Full::random(&mut rng)).collect(); - let b_elems: Vec = (0..width).map(|_| E2Full::random(&mut rng)).collect(); - - let pa = PE2Full::from_fn(|i| a_elems[i]); - let pb = PE2Full::from_fn(|i| b_elems[i]); - let pc = pa * pb; - - for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { - assert_eq!( - pc.extract(i), - *a * *b, - "full-word packed FpExt2 mul mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext2_broadcast() { - let val = E2::new(F::from_u64(7), F::from_u64(11)); - let packed = PE2::broadcast(val); - let width = ::WIDTH; - for i in 0..width { - assert_eq!(packed.extract(i), val); - } -} - -#[test] -fn packed_fp_ext4_add() { - let mut rng = StdRng::seed_from_u64(360); - let width = ::WIDTH; - let a_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); - let b_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); - - let pa = PR4::from_fn(|i| a_elems[i]); - let pb = PR4::from_fn(|i| b_elems[i]); - let pc = pa + pb; - - for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { - assert_eq!( - pc.extract(i), - *a + *b, - "packed FpExt4 add mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext4_sub() { - let mut rng = StdRng::seed_from_u64(361); - let width = ::WIDTH; - let a_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); - let b_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); - - let pa = PR4::from_fn(|i| a_elems[i]); - let pb = PR4::from_fn(|i| b_elems[i]); - let pc = pa - pb; - - for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { - assert_eq!( - pc.extract(i), - *a - *b, - "packed FpExt4 sub mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext4_mul() { - let mut rng = StdRng::seed_from_u64(362); - let width = ::WIDTH; - let a_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); - let b_elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); - - let pa = PR4::from_fn(|i| a_elems[i]); - let pb = PR4::from_fn(|i| b_elems[i]); - let pc = pa * pb; - - for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { - assert_eq!( - pc.extract(i), - *a * *b, - "packed FpExt4 mul mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext4_mul_prime32() { - let mut rng = StdRng::seed_from_u64(365); - let width = ::WIDTH; - let a_elems: Vec = (0..width).map(|_| R4Prime32::random(&mut rng)).collect(); - let b_elems: Vec = (0..width).map(|_| R4Prime32::random(&mut rng)).collect(); - - let pa = PR4Prime32::from_fn(|i| a_elems[i]); - let pb = PR4Prime32::from_fn(|i| b_elems[i]); - let pc = pa * pb; - - for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { - assert_eq!( - pc.extract(i), - *a * *b, - "Prime32 packed FpExt4 mul mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext4_prime31_edge_lanes() { - check_packed_fp_ext4_edge::< - { crate::prime::pseudo_mersenne::PRIME31_OFFSET19_MODULUS }, - PR4Prime31, - >(); -} - -#[test] -fn packed_fp_ext4_mersenne31_edge_lanes() { - check_packed_fp_ext4_edge::<{ (1u32 << 31) - 1 }, PR4Mersenne31>(); -} - -#[test] -fn packed_fp_ext4_prime32_edge_lanes() { - check_packed_fp_ext4_edge::< - { crate::prime::pseudo_mersenne::PRIME32_OFFSET99_MODULUS }, - PR4Prime32, - >(); -} - -#[test] -fn packed_fp_ext4_generic31_edge_lanes() { - check_packed_fp_ext4_edge::<{ (1u32 << 31) - 61 }, PR4Generic31Offset61>(); -} - -#[test] -fn packed_fp_ext4_large_generic30_edge_lanes() { - check_packed_fp_ext4_edge::<{ (1u32 << 30) - 16_397 }, PR4Generic30Offset16397>(); -} - -#[test] -fn packed_fp_ext4_large_generic31_edge_lanes() { - check_packed_fp_ext4_edge::<{ (1u32 << 31) - 32_787 }, PR4Generic31Offset32787>(); -} - -#[test] -fn packed_fp_ext4_square() { - let mut rng = StdRng::seed_from_u64(363); - let width = ::WIDTH; - let elems: Vec = (0..width).map(|_| R4::random(&mut rng)).collect(); - - let packed = PR4::from_fn(|i| elems[i]); - let squared = packed.square(); - - for (i, elem) in elems.iter().enumerate() { - assert_eq!( - squared.extract(i), - elem.square(), - "packed FpExt4 square mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext4_square_prime32() { - let mut rng = StdRng::seed_from_u64(366); - let width = ::WIDTH; - let elems: Vec = (0..width).map(|_| R4Prime32::random(&mut rng)).collect(); - - let packed = PR4Prime32::from_fn(|i| elems[i]); - let squared = packed.square(); - - for (i, elem) in elems.iter().enumerate() { - assert_eq!( - squared.extract(i), - elem.square(), - "Prime32 packed FpExt4 square mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext4_square_mersenne31() { - let mut rng = StdRng::seed_from_u64(367); - type R4M31 = FpExt4; - let width = ::WIDTH; - let elems: Vec = (0..width).map(|_| R4M31::random(&mut rng)).collect(); - - let packed = PR4Mersenne31::from_fn(|i| elems[i]); - let squared = packed.square(); - - for (i, elem) in elems.iter().enumerate() { - assert_eq!( - squared.extract(i), - elem.square(), - "Mersenne31 packed FpExt4 square mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext4_inverse() { - let mut rng = StdRng::seed_from_u64(367); - let width = ::WIDTH; - let elems: Vec = (0..width) - .map(|_| { - let x = R4::random(&mut rng); - if x.is_zero() { - R4::one() - } else { - x - } - }) - .collect(); - - let packed = PR4::from_fn(|i| elems[i]); - let inverted = packed.inverse().unwrap(); - - for (i, elem) in elems.iter().enumerate() { - assert_eq!( - inverted.extract(i), - elem.inverse().unwrap(), - "packed FpExt4 inverse mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext4_broadcast() { - let val = R4::new([ - F::from_u64(7), - F::from_u64(11), - F::from_u64(13), - F::from_u64(17), - ]); - let packed = PR4::broadcast(val); - let width = ::WIDTH; - for i in 0..width { - assert_eq!(packed.extract(i), val); - } -} - -#[test] -fn packed_fp_ext4_pack_unpack() { - let mut rng = StdRng::seed_from_u64(364); - let width = ::WIDTH; - let elems: Vec = (0..width * 3).map(|_| R4::random(&mut rng)).collect(); - - let packed = PR4::pack_slice(&elems); - let unpacked = PR4::unpack_slice(&packed); - - assert_eq!(elems, unpacked); -} - -#[test] -fn pack_unpack_roundtrip_fp_ext2() { - let mut rng = StdRng::seed_from_u64(400); - let width = ::WIDTH; - let elems: Vec = (0..width * 3).map(|_| E2::random(&mut rng)).collect(); - - let packed = PE2::pack_slice(&elems); - let unpacked = PE2::unpack_slice(&packed); - - assert_eq!(elems, unpacked); -} - -type R8Fp64 = FpExt8; -type PR8Fp64 = PackedFpExt8::Packing>; -type R8Prime31 = FpExt8; -type PR8Prime31 = PackedFpExt8::Packing>; -type R8Prime32 = FpExt8; -type PR8Prime32 = PackedFpExt8::Packing>; - -#[test] -fn packed_fp_ext8_mul_fp64() { - let mut rng = StdRng::seed_from_u64(500); - let width = ::WIDTH; - let a_elems: Vec = (0..width).map(|_| R8Fp64::random(&mut rng)).collect(); - let b_elems: Vec = (0..width).map(|_| R8Fp64::random(&mut rng)).collect(); - - let pa = PR8Fp64::from_fn(|i| a_elems[i]); - let pb = PR8Fp64::from_fn(|i| b_elems[i]); - let pc = pa * pb; - - for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { - assert_eq!( - pc.extract(i), - *a * *b, - "packed FpExt8 mul mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext8_mul_prime31() { - let mut rng = StdRng::seed_from_u64(501); - let width = ::WIDTH; - let a_elems: Vec = (0..width).map(|_| R8Prime31::random(&mut rng)).collect(); - let b_elems: Vec = (0..width).map(|_| R8Prime31::random(&mut rng)).collect(); - - let pa = PR8Prime31::from_fn(|i| a_elems[i]); - let pb = PR8Prime31::from_fn(|i| b_elems[i]); - let pc = pa * pb; - - for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { - assert_eq!( - pc.extract(i), - *a * *b, - "packed FpExt8 mul mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext8_mul_prime32() { - let mut rng = StdRng::seed_from_u64(502); - let width = ::WIDTH; - let a_elems: Vec = (0..width).map(|_| R8Prime32::random(&mut rng)).collect(); - let b_elems: Vec = (0..width).map(|_| R8Prime32::random(&mut rng)).collect(); - - let pa = PR8Prime32::from_fn(|i| a_elems[i]); - let pb = PR8Prime32::from_fn(|i| b_elems[i]); - let pc = pa * pb; - - for (i, (a, b)) in a_elems.iter().zip(&b_elems).enumerate() { - assert_eq!( - pc.extract(i), - *a * *b, - "packed FpExt8 mul mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext8_square() { - let mut rng = StdRng::seed_from_u64(504); - let width = ::WIDTH; - let a_elems: Vec = (0..width).map(|_| R8Prime31::random(&mut rng)).collect(); - - let pa = PR8Prime31::from_fn(|i| a_elems[i]); - let sq = pa.square(); - - for (i, a) in a_elems.iter().enumerate() { - assert_eq!( - sq.extract(i), - a.square(), - "packed FpExt8 square mismatch at lane {i}" - ); - } -} - -#[test] -fn packed_fp_ext8_broadcast() { - let val = R8Fp64::new([ - F::from_u64(1), - F::from_u64(2), - F::from_u64(3), - F::from_u64(4), - F::from_u64(5), - F::from_u64(6), - F::from_u64(7), - F::from_u64(8), - ]); - let packed = PR8Fp64::broadcast(val); - let width = ::WIDTH; - for i in 0..width { - assert_eq!(packed.extract(i), val); - } -} diff --git a/crates/jolt-field/src/packed/mod.rs b/crates/jolt-field/src/packed/mod.rs deleted file mode 100644 index 675d1d4fd8..0000000000 --- a/crates/jolt-field/src/packed/mod.rs +++ /dev/null @@ -1,330 +0,0 @@ -//! Packed field abstractions and architecture-specific SIMD backends. - -#[cfg(all( - target_arch = "x86_64", - target_feature = "avx2", - not(all(target_feature = "avx512f", target_feature = "avx512dq")) -))] -pub(crate) mod avx2; -#[cfg(all( - target_arch = "x86_64", - target_feature = "avx512f", - target_feature = "avx512dq" -))] -pub(crate) mod avx512; -pub(crate) mod ext; -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -pub(crate) mod neon; - -pub use ext::{PackedFpExt2, PackedFpExt4, PackedFpExt8}; - -use crate::ext::{ - fp_ext4_mul_coeffs, fp_ext4_square_coeffs, fp_ext8_mul_schedule, fp_ext8_square_schedule, - FpExt2Config, -}; -use crate::{FieldCore, Fp128, Fp32, Fp64}; -use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; -use num_traits::Zero; - -/// Packed arithmetic over a scalar field. -pub trait PackedField: - 'static + Copy + Send + Sync + Add + Sub + Mul -{ - /// Scalar field type. - type Scalar: FieldCore; - - /// Number of scalar lanes. - const WIDTH: usize; - - /// Build from a lane generator. - fn from_fn(f: F) -> Self - where - F: FnMut(usize) -> Self::Scalar; - - /// Extract one lane. - fn extract(&self, lane: usize) -> Self::Scalar; - - /// Pack a scalar slice into packed values. - /// - /// # Panics - /// - /// Panics if the length is not divisible by `WIDTH`. - #[inline] - fn pack_slice(buf: &[Self::Scalar]) -> Vec { - assert!( - buf.len() % Self::WIDTH == 0, - "slice length {} must be divisible by WIDTH {}", - buf.len(), - Self::WIDTH - ); - buf.chunks_exact(Self::WIDTH) - .map(|chunk| Self::from_fn(|i| chunk[i])) - .collect() - } - - /// Packed prefix + scalar suffix split. - #[inline] - fn pack_slice_with_suffix(buf: &[Self::Scalar]) -> (Vec, &[Self::Scalar]) { - let split = buf.len() - (buf.len() % Self::WIDTH); - let (packed, suffix) = buf.split_at(split); - (Self::pack_slice(packed), suffix) - } - - /// Unpack packed values into a flat scalar vector. - #[inline] - fn unpack_slice(buf: &[Self]) -> Vec { - let mut out = Vec::with_capacity(buf.len() * Self::WIDTH); - for packed in buf { - for lane in 0..Self::WIDTH { - out.push(packed.extract(lane)); - } - } - out - } - - /// Broadcast one scalar across all lanes. - fn broadcast(value: Self::Scalar) -> Self; - - /// Square one packed value. - #[inline(always)] - fn square(self) -> Self { - self * self - } - - /// Invert one packed value lane-wise. - #[inline] - fn inverse(self) -> Option - where - Self::Scalar: FieldCore, - { - let mut inverses = Vec::with_capacity(Self::WIDTH); - for lane in 0..Self::WIDTH { - inverses.push(self.extract(lane).inverse()?); - } - Some(Self::from_fn(|i| inverses[i])) - } - - /// Backend hook for multiplying two packed `FpExt2` values in coefficient form. - #[inline(always)] - fn fp_ext2_mul(a0: Self, a1: Self, b0: Self, b1: Self) -> (Self, Self) - where - C: FpExt2Config, - { - let v0 = a0 * b0; - let v1 = a1 * b1; - let cross = (a0 + a1) * (b0 + b1); - ( - v0 + C::mul_non_residue(v1, Self::broadcast), - cross - v0 - v1, - ) - } - - /// Backend hook for multiplying packed ring-subfield quartics. - #[inline(always)] - fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { - fp_ext4_mul_coeffs::(a, b) - } - - /// Backend hook for squaring packed ring-subfield quartics. - #[inline(always)] - fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { - fp_ext4_square_coeffs::(a) - } - - /// Backend hook for inverting packed ring-subfield quartics. - #[inline(always)] - fn fp_ext4_inverse(a: [Self; 4]) -> Option<[Self; 4]> - where - Self::Scalar: FieldCore, - { - let zero = Self::broadcast(Self::Scalar::zero()); - let [a0, a1, a2, a3] = a; - let x0 = a0; - let x1 = a2; - let y0 = a1 - a3; - let y1 = a3; - - let x0x1 = x0 * x1; - let y0y1 = y0 * y1; - let x1_square = x1 * x1; - let y1_square = y1 * y1; - let aa = (x0 * x0 + x1_square + x1_square, x0x1 + x0x1); - let bb = (y0 * y0 + y1_square + y1_square, y0y1 + y0y1); - let nr_bb = (bb.0 + bb.0 + bb.1 + bb.1, bb.0 + bb.1 + bb.1); - let norm = (aa.0 - nr_bb.0, aa.1 - nr_bb.1); - let inv_norm_base = (norm.0 * norm.0 - (norm.1 * norm.1 + norm.1 * norm.1)).inverse()?; - let inv_norm = (norm.0 * inv_norm_base, (zero - norm.1) * inv_norm_base); - - let v0 = x0 * inv_norm.0; - let v1 = x1 * inv_norm.1; - let constant = ( - v0 + v1 + v1, - (x0 + x1) * (inv_norm.0 + inv_norm.1) - v0 - v1, - ); - let neg_y0 = zero - y0; - let neg_y1 = zero - y1; - let w0 = neg_y0 * inv_norm.0; - let w1 = neg_y1 * inv_norm.1; - let e1_coeff = ( - w0 + w1 + w1, - (neg_y0 + neg_y1) * (inv_norm.0 + inv_norm.1) - w0 - w1, - ); - - Some([constant.0, e1_coeff.0 + e1_coeff.1, constant.1, e1_coeff.1]) - } - - /// Backend hook for multiplying packed ring-subfield degree-8 elements. - #[inline(always)] - fn fp_ext8_mul(a: [Self; 8], b: [Self; 8]) -> [Self; 8] { - fp_ext8_mul_schedule( - a, - b, - Self::broadcast(Self::Scalar::zero()), - |x, y| x + y, - |x, y| x - y, - |x, y| x * y, - ) - } - - /// Backend hook for squaring packed ring-subfield degree-8 elements. - #[inline(always)] - fn fp_ext8_square(a: [Self; 8]) -> [Self; 8] { - fp_ext8_square_schedule( - a, - Self::broadcast(Self::Scalar::zero()), - |x, y| x + y, - |x, y| x - y, - |x, y| x * y, - ) - } -} - -/// Scalar fallback packed type with one lane. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -#[repr(transparent)] -pub struct NoPacking(pub [T; 1]); - -impl Add for NoPacking { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - Self([self.0[0] + rhs.0[0]]) - } -} - -impl Sub for NoPacking { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - Self([self.0[0] - rhs.0[0]]) - } -} - -impl Mul for NoPacking { - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self { - Self([self.0[0] * rhs.0[0]]) - } -} - -impl AddAssign for NoPacking { - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for NoPacking { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for NoPacking { - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl PackedField for NoPacking { - const WIDTH: usize = 1; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Scalar, - { - Self([f(0)]) - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Scalar { - debug_assert_eq!(lane, 0); - self.0[0] - } - type Scalar = T; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self([value]) - } -} - -/// Scalar field -> packed field association. -pub trait HasPacking: FieldCore { - /// Packed representation for this scalar field. - type Packing: PackedField; -} - -/// Selects the packed backend for a Solinas prime at compile time: -/// NEON on aarch64, AVX-512 then AVX2 on x86_64, scalar `NoPacking` otherwise. -macro_rules! select_packing { - ($alias:ident<$p:ident: $p_ty:ty>, $scalar:ident, $neon:ident, $avx512:ident, $avx2:ident) => { - /// Selected packed backend for this prime width. - #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] - pub type $alias = neon::$neon<$p>; - - /// Selected packed backend for this prime width. - #[cfg(all( - target_arch = "x86_64", - target_feature = "avx512f", - target_feature = "avx512dq" - ))] - pub type $alias = avx512::$avx512<$p>; - - /// Selected packed backend for this prime width. - #[cfg(all( - target_arch = "x86_64", - target_feature = "avx2", - not(all(target_feature = "avx512f", target_feature = "avx512dq")) - ))] - pub type $alias = avx2::$avx2<$p>; - - /// Selected packed backend for this prime width. - #[cfg(not(any( - all(target_arch = "aarch64", target_feature = "neon"), - all(target_arch = "x86_64", target_feature = "avx2") - )))] - pub type $alias = NoPacking<$scalar<$p>>; - - impl HasPacking for $scalar<$p> { - type Packing = $alias<$p>; - } - }; -} - -select_packing!(Fp32Packing, Fp32, PackedFp32Neon, PackedFp32Avx512, PackedFp32Avx2); -select_packing!(Fp64Packing, Fp64, PackedFp64Neon, PackedFp64Avx512, PackedFp64Avx2); -select_packing!( - Fp128Packing, - Fp128, - PackedFp128Neon, - PackedFp128Avx512, - PackedFp128Avx2 -); - -#[cfg(test)] -mod tests; diff --git a/crates/jolt-field/src/packed/neon/fp128.rs b/crates/jolt-field/src/packed/neon/fp128.rs deleted file mode 100644 index 7bc75bce0e..0000000000 --- a/crates/jolt-field/src/packed/neon/fp128.rs +++ /dev/null @@ -1,311 +0,0 @@ -use super::*; - -/// Number of packed `Fp128` lanes in this backend. -pub(crate) const FP128_WIDTH: usize = 2; - -/// True SoA layout for two packed `Fp128` lanes. -/// -/// `lo = [lane0.lo, lane1.lo]` -/// `hi = [lane0.hi, lane1.hi]` -#[derive(Clone, Copy)] -pub struct PackedFp128Neon { - lo: [u64; 2], - hi: [u64; 2], -} -#[inline(always)] -const fn modulus_lo() -> u64 { - P as u64 -} - -#[inline(always)] -const fn modulus_hi() -> u64 { - (P >> 64) as u64 -} - -use crate::prime::util::{is_pow2_u64, log2_pow2_u64}; -impl PackedFp128Neon

{ - const C: u128 = { - let c = 0u128.wrapping_sub(P); - assert!(P != 0, "modulus must be nonzero"); - assert!(P & 1 == 1, "modulus must be odd"); - assert!(c < (1u128 << 64), "P must be 2^128 - c with c < 2^64"); - assert!( - c * (c + 1) < P, - "C(C+1) < P required for fused canonicalize" - ); - c - }; - const C_LO: u64 = Self::C as u64; - const C_SHIFT_KIND: i8 = { - let c = Self::C_LO; - if c > 1 && is_pow2_u64(c - 1) { - 1 - } else if c == u64::MAX || is_pow2_u64(c + 1) { - -1 - } else { - 0 - } - }; - const C_SHIFT: u32 = { - let c = Self::C_LO; - if Self::C_SHIFT_KIND == 1 { - log2_pow2_u64(c - 1) - } else if Self::C_SHIFT_KIND == -1 { - if c == u64::MAX { - 64 - } else { - log2_pow2_u64(c + 1) - } - } else { - 0 - } - }; - - #[inline(always)] - fn mul_wide_u64(a: u64, b: u64) -> (u64, u64) { - let prod = (a as u128) * (b as u128); - (prod as u64, (prod >> 64) as u64) - } - - #[inline(always)] - fn mul_c_wide(x: u64) -> (u64, u64) { - if Self::C_SHIFT_KIND == 1 { - let v = ((x as u128) << Self::C_SHIFT) + x as u128; - (v as u64, (v >> 64) as u64) - } else if Self::C_SHIFT_KIND == -1 { - let v = ((x as u128) << Self::C_SHIFT) - x as u128; - (v as u64, (v >> 64) as u64) - } else { - Self::mul_wide_u64(Self::C_LO, x) - } - } - - #[inline(always)] - fn fold2_canonicalize(t0: u64, t1: u64, t2: u64) -> (u64, u64) { - let (ct2_lo, ct2_hi) = Self::mul_c_wide(t2); - - let (s0, carry0) = t0.overflowing_add(ct2_lo); - let (s1a, carry1a) = t1.overflowing_add(ct2_hi); - let (s1, carry1b) = s1a.overflowing_add(carry0 as u64); - let overflow = carry1a | carry1b; - - let (r0, carry2) = s0.overflowing_add(Self::C_LO); - let (r1, carry3) = s1.overflowing_add(carry2 as u64); - - if overflow | carry3 { - (r0, r1) - } else { - (s0, s1) - } - } - - #[inline(always)] - fn mul_raw_lane(a0: u64, a1: u64, b0: u64, b1: u64) -> (u64, u64) { - let (p00_lo, p00_hi) = Self::mul_wide_u64(a0, b0); - let (p01_lo, p01_hi) = Self::mul_wide_u64(a0, b1); - let (p10_lo, p10_hi) = Self::mul_wide_u64(a1, b0); - let (p11_lo, p11_hi) = Self::mul_wide_u64(a1, b1); - - let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; - let r0 = p00_lo; - let r1 = row1 as u64; - let carry1 = (row1 >> 64) as u64; - - let row2 = p01_hi as u128 + p10_hi as u128 + p11_lo as u128 + carry1 as u128; - let r2 = row2 as u64; - let carry2 = (row2 >> 64) as u64; - - let row3 = p11_hi as u128 + carry2 as u128; - let r3 = row3 as u64; - debug_assert_eq!(row3 >> 64, 0); - - let (cr2_lo, cr2_hi) = Self::mul_c_wide(r2); - let (cr3_lo, cr3_hi) = Self::mul_c_wide(r3); - - let t0_sum = r0 as u128 + cr2_lo as u128; - let t0 = t0_sum as u64; - let carryf = (t0_sum >> 64) as u64; - - let t1_sum = r1 as u128 + cr2_hi as u128 + cr3_lo as u128 + carryf as u128; - let t1 = t1_sum as u64; - - let t2_sum = cr3_hi as u128 + (t1_sum >> 64); - let t2 = t2_sum as u64; - debug_assert_eq!(t2_sum >> 64, 0); - - Self::fold2_canonicalize(t0, t1, t2) - } -} - -impl Default for PackedFp128Neon

{ - #[inline] - fn default() -> Self { - Self::broadcast(Fp128::zero()) - } -} - -impl fmt::Debug for PackedFp128Neon

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("PackedFp128Neon") - .field(&[self.extract(0), self.extract(1)]) - .finish() - } -} - -impl PartialEq for PackedFp128Neon

{ - #[inline] - fn eq(&self, other: &Self) -> bool { - self.extract(0) == other.extract(0) && self.extract(1) == other.extract(1) - } -} - -impl Eq for PackedFp128Neon

{} - -impl Add for PackedFp128Neon

{ - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - let lo_a = to_vec(self.lo); - let hi_a = to_vec(self.hi); - let lo_b = to_vec(rhs.lo); - let hi_b = to_vec(rhs.hi); - - let (out_lo, out_hi) = unsafe { - let c_vec = vdupq_n_u64(Self::C_LO); - - // s = a + b (128-bit, two lanes). - // Carry propagation uses raw comparison masks with sub: subtracting - // a lane of all-1s is equivalent to adding 1 in wrapping arithmetic. - let sum_lo = vaddq_u64(lo_a, lo_b); - let carry_lo = vcltq_u64(sum_lo, lo_a); - - let hi_tmp = vaddq_u64(hi_a, hi_b); - let carry_hi1 = vcltq_u64(hi_tmp, hi_a); - let sum_hi = vsubq_u64(hi_tmp, carry_lo); - let carry_hi2 = vcltq_u64(sum_hi, hi_tmp); - let overflow = vorrq_u64(carry_hi1, carry_hi2); - - // t = s + C. Since p = 2^128 - C, this is s - p (mod 2^128). - // If s + C >= 2^128 then s >= p, so the reduced value t is correct. - let t_lo = vaddq_u64(sum_lo, c_vec); - let carry_c = vcltq_u64(t_lo, sum_lo); - let t_hi = vsubq_u64(sum_hi, carry_c); - let carry_t = vcltq_u64(t_hi, sum_hi); - - let use_reduced = vorrq_u64(overflow, carry_t); - let out_lo = vbslq_u64(use_reduced, t_lo, sum_lo); - let out_hi = vbslq_u64(use_reduced, t_hi, sum_hi); - (out_lo, out_hi) - }; - - Self { - lo: from_vec(out_lo), - hi: from_vec(out_hi), - } - } -} - -impl Sub for PackedFp128Neon

{ - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - let lo_a = to_vec(self.lo); - let hi_a = to_vec(self.hi); - let lo_b = to_vec(rhs.lo); - let hi_b = to_vec(rhs.hi); - - let (out_lo, out_hi) = unsafe { - let p_lo = vdupq_n_u64(modulus_lo::

()); - let p_hi = vdupq_n_u64(modulus_hi::

()); - - let diff_lo = vsubq_u64(lo_a, lo_b); - let borrow_lo = mask_to_bit(vcltq_u64(lo_a, lo_b)); - - let diff_hi_tmp = vsubq_u64(hi_a, hi_b); - let borrow_hi1 = vcltq_u64(hi_a, hi_b); - let diff_hi = vsubq_u64(diff_hi_tmp, borrow_lo); - let borrow_hi2 = vcltq_u64(diff_hi_tmp, borrow_lo); - let borrow_128 = vorrq_u64(borrow_hi1, borrow_hi2); - - let corr_lo = vaddq_u64(diff_lo, p_lo); - let carry_lo = mask_to_bit(vcltq_u64(corr_lo, diff_lo)); - - let corr_hi_tmp = vaddq_u64(diff_hi, p_hi); - let corr_hi = vaddq_u64(corr_hi_tmp, carry_lo); - - let out_lo = vbslq_u64(borrow_128, corr_lo, diff_lo); - let out_hi = vbslq_u64(borrow_128, corr_hi, diff_hi); - (out_lo, out_hi) - }; - - Self { - lo: from_vec(out_lo), - hi: from_vec(out_hi), - } - } -} - -impl Mul for PackedFp128Neon

{ - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self { - let (o0_lo, o0_hi) = Self::mul_raw_lane(self.lo[0], self.hi[0], rhs.lo[0], rhs.hi[0]); - let (o1_lo, o1_hi) = Self::mul_raw_lane(self.lo[1], self.hi[1], rhs.lo[1], rhs.hi[1]); - - Self { - lo: [o0_lo, o1_lo], - hi: [o0_hi, o1_hi], - } - } -} - -impl AddAssign for PackedFp128Neon

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for PackedFp128Neon

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for PackedFp128Neon

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl PackedField for PackedFp128Neon

{ - const WIDTH: usize = FP128_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Scalar, - { - let x0 = f(0); - let x1 = f(1); - Self { - lo: [x0.0[0], x1.0[0]], - hi: [x0.0[1], x1.0[1]], - } - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Scalar { - debug_assert!(lane < FP128_WIDTH); - Fp128([self.lo[lane], self.hi[lane]]) - } - - type Scalar = Fp128

; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self::from_fn(|_| value) - } -} diff --git a/crates/jolt-field/src/packed/neon/fp32.rs b/crates/jolt-field/src/packed/neon/fp32.rs deleted file mode 100644 index abae91537d..0000000000 --- a/crates/jolt-field/src/packed/neon/fp32.rs +++ /dev/null @@ -1,821 +0,0 @@ -use super::*; - -/// Number of packed `Fp32` lanes. -pub(crate) const FP32_WIDTH: usize = 4; - -/// NEON packed `Fp32` backend: 4 lanes in `uint32x4_t`. -#[derive(Clone, Copy)] -pub struct PackedFp32Neon { - vals: [u32; 4], -} - -#[inline(always)] -fn to_vec32(x: [u32; 4]) -> uint32x4_t { - unsafe { transmute::<[u32; 4], uint32x4_t>(x) } -} - -#[inline(always)] -fn from_vec32(v: uint32x4_t) -> [u32; 4] { - unsafe { transmute::(v) } -} - -impl PackedFp32Neon

{ - const BITS: u32 = 32 - P.leading_zeros(); - - const C: u32 = { - let c = if Self::BITS == 32 { - 0u32.wrapping_sub(P) - } else { - (1u32 << Self::BITS) - P - }; - assert!(P != 0, "modulus must be nonzero"); - assert!(P & 1 == 1, "modulus must be odd"); - assert!( - (c as u64) * (c as u64 + 1) < P as u64, - "C(C+1) < P required for fused canonicalize" - ); - c - }; - - const MASK_U64: u64 = if Self::BITS == 32 { - u32::MAX as u64 - } else { - (1u64 << Self::BITS) - 1 - }; - - const TWO_FOLD_FOUR_PRODUCT_OK: bool = { - let c = Self::C as u64; - 4 * c * c + 3 * c <= (1u64 << Self::BITS) - }; - - #[inline(always)] - fn to_vec(self) -> uint32x4_t { - to_vec32(self.vals) - } - - #[inline(always)] - fn from_vec(v: uint32x4_t) -> Self { - Self { - vals: from_vec32(v), - } - } - - #[inline(always)] - fn add_vec(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t { - unsafe { - let p = vdupq_n_u32(P); - if Self::BITS <= 31 { - let t = vaddq_u32(a, b); - vminq_u32(t, vsubq_u32(t, p)) - } else { - let c = vdupq_n_u32(Self::C); - let t = vaddq_u32(a, b); - let overflow = vcltq_u32(t, a); - let folded = vaddq_u32(t, vandq_u32(overflow, c)); - vminq_u32(folded, vsubq_u32(folded, p)) - } - } - } - - #[inline(always)] - fn sub_vec(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t { - unsafe { - let p = vdupq_n_u32(P); - if Self::BITS <= 31 { - let t = vsubq_u32(a, b); - vminq_u32(t, vaddq_u32(t, p)) - } else { - let t = vsubq_u32(a, b); - let underflow = vcltq_u32(a, b); - vsubq_u32(t, vandq_u32(underflow, vdupq_n_u32(Self::C))) - } - } - } - - #[inline(always)] - fn mul_vec(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t { - unsafe { - if Self::BITS == 31 { - return if Self::C == 1 { - Self::mul_mersenne31_vec(a, b) - } else { - Self::mul_pmersenne31_vec(a, b) - }; - } - let prod_lo = vmull_u32(vget_low_u32(a), vget_low_u32(b)); - let prod_hi = vmull_high_u32(a, b); - Self::solinas_reduce(prod_lo, prod_hi) - } - } - - #[inline(always)] - unsafe fn mul_mersenne31_vec(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t { - unsafe { - let p = vdupq_n_u32(P); - let prod_hi31 = vreinterpretq_u32_s32(vqdmulhq_s32( - vreinterpretq_s32_u32(a), - vreinterpretq_s32_u32(b), - )); - let prod_lo32 = vmulq_u32(a, b); - let folded = vmlsq_u32(prod_lo32, prod_hi31, p); - vminq_u32(folded, vsubq_u32(folded, p)) - } - } - - /// Packed multiply for 31-bit pseudo-Mersenne primes `P = 2^31 - C` - /// (`BITS == 31`, `C > 1`), reducing entirely in 32-bit lanes. - /// - /// This generalises the `C == 1` Mersenne kernel - /// ([`Self::mul_mersenne31_vec`]) to any small `C` admitted by the - /// `Fp32

` invariant `C(C+1) < P`, replacing the 64-bit-widening - /// [`Self::solinas_reduce`] path. It keeps all four lanes in `uint32x4_t` - /// and uses two `vqdmulhq_s32` high-multiplies (the same instruction the - /// Mersenne path uses) to extract Solinas fold high words without ever - /// forming a 64-bit intermediate. - /// - /// # Correctness (exact, no estimation) - /// - /// Precondition: lanes `a, b ∈ [0, P)` (the `Add`/`Sub`/`Mul` impls all - /// return canonical lanes, so every `mul_vec` input is canonical). Write - /// `z = a*b`, so `0 ≤ z ≤ (P-1)^2 < 2^62`. All steps are exact integer - /// identities; the only inequality used is the compile-time invariant - /// `C(C+1) < P`, which gives `C^2 < P < 2^31` and `C(C+2) < 2^31`. - /// - /// 1. `h = sqdmulh(a,b) = floor(2z / 2^32) = floor(z / 2^31)`, exact - /// because `2z < 2^63` (no saturation), and `h ∈ [0, 2^31)`. - /// 2. `z_lo31 = (z mod 2^32) & (2^31-1) = z mod 2^31`, so - /// `z = h·2^31 + z_lo31` exactly. - /// 3. Since `2^31 = P + C ≡ C (mod P)`, `z ≡ C·h + z_lo31 =: t (mod P)`. - /// 4. Fold `t`: `hh = sqdmulh(h, C) = floor(C·h / 2^31) ∈ [0, C)` (exact, - /// `2·h·C < 2^63`), and `ch_lo31 = (C·h mod 2^32) & (2^31-1) - /// = C·h mod 2^31`, so `C·h = hh·2^31 + ch_lo31`. - /// 5. `s = ch_lo31 + z_lo31 < 2^32` (sum of two sub-`2^31` values, no u32 - /// overflow). With `hp = hh + (s >> 31)` and `lo31p = s & (2^31-1)`, - /// `t = hh·2^31 + s = hp·2^31 + lo31p`, and `hp ≤ (C-1) + 1 = C`. - /// 6. Fold again: `t ≡ C·hp + lo31p =: t' (mod P)`. Since `hp ≤ C`, - /// `C·hp ≤ C^2 < 2^31` (so `vmulq_u32(hp, C)` is exact, no wrap), and - /// `t' = C·hp + lo31p < C^2 + 2^31 < 2^32` (no u32 overflow). - /// 7. `t' < C^2 + 2^31 ≤ 2P` because `C(C+2) < 2^31 ⇔ C^2 + 2^31 < 2P`. - /// Thus `t' ≡ z (mod P)` and `t' ∈ [0, 2P)`, i.e. `t' ∈ {r, r+P}` for - /// `r = z mod P`. The final `vminq_u32(t', t' - P)` (wrapping sub) - /// returns the canonical `r ∈ [0, P)`. - #[inline(always)] - unsafe fn mul_pmersenne31_vec(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t { - unsafe { - let mask31 = vdupq_n_u32((1u32 << 31) - 1); - let cvec = vdupq_n_u32(Self::C); - let p = vdupq_n_u32(P); - - // Step 1-2: high/low split of z = a*b. - let h = vreinterpretq_u32_s32(vqdmulhq_s32( - vreinterpretq_s32_u32(a), - vreinterpretq_s32_u32(b), - )); - let z_lo31 = vandq_u32(vmulq_u32(a, b), mask31); - - // Step 3-5: first Solinas fold t = C*h + z_lo31 = hp*2^31 + lo31p. - let hh = vreinterpretq_u32_s32(vqdmulhq_s32( - vreinterpretq_s32_u32(h), - vreinterpretq_s32_u32(cvec), - )); - let ch_lo31 = vandq_u32(vmulq_u32(h, cvec), mask31); - let s = vaddq_u32(ch_lo31, z_lo31); - let hp = vaddq_u32(hh, vshrq_n_u32::<31>(s)); - let lo31p = vandq_u32(s, mask31); - - // Step 6-7: second fold t' = C*hp + lo31p in [0, 2P), canonicalize. - let tprime = vaddq_u32(vmulq_u32(hp, cvec), lo31p); - vminq_u32(tprime, vsubq_u32(tprime, p)) - } - } - - #[inline(always)] - fn add_u64_with_carry( - sum: uint64x2_t, - rhs: uint64x2_t, - carry: uint64x2_t, - ) -> (uint64x2_t, uint64x2_t) { - unsafe { - let next = vaddq_u64(sum, rhs); - let overflow = vcltq_u64(next, sum); - (next, vaddq_u64(carry, mask_to_bit(overflow))) - } - } - - #[inline(always)] - fn carry_correction(carry: uint64x2_t) -> uint64x2_t { - unsafe { vmull_u32(vmovn_u64(carry), vdup_n_u32(Fp32::

::SHIFT64_MOD_P)) } - } - - #[inline(always)] - fn dot_product_4_vec(a: [uint32x4_t; 4], b: [uint32x4_t; 4]) -> uint32x4_t { - unsafe { - let mut sum_lo = vmull_u32(vget_low_u32(a[0]), vget_low_u32(b[0])); - let mut sum_hi = vmull_high_u32(a[0], b[0]); - - if Self::BITS <= 31 { - sum_lo = vaddq_u64(sum_lo, vmull_u32(vget_low_u32(a[1]), vget_low_u32(b[1]))); - sum_hi = vaddq_u64(sum_hi, vmull_high_u32(a[1], b[1])); - sum_lo = vaddq_u64(sum_lo, vmull_u32(vget_low_u32(a[2]), vget_low_u32(b[2]))); - sum_hi = vaddq_u64(sum_hi, vmull_high_u32(a[2], b[2])); - sum_lo = vaddq_u64(sum_lo, vmull_u32(vget_low_u32(a[3]), vget_low_u32(b[3]))); - sum_hi = vaddq_u64(sum_hi, vmull_high_u32(a[3], b[3])); - - return Self::solinas_reduce(sum_lo, sum_hi); - } - - let mut carry_lo = vdupq_n_u64(0); - let mut carry_hi = vdupq_n_u64(0); - - let prod_lo_1 = vmull_u32(vget_low_u32(a[1]), vget_low_u32(b[1])); - let prod_hi_1 = vmull_high_u32(a[1], b[1]); - (sum_lo, carry_lo) = Self::add_u64_with_carry(sum_lo, prod_lo_1, carry_lo); - (sum_hi, carry_hi) = Self::add_u64_with_carry(sum_hi, prod_hi_1, carry_hi); - - let prod_lo_2 = vmull_u32(vget_low_u32(a[2]), vget_low_u32(b[2])); - let prod_hi_2 = vmull_high_u32(a[2], b[2]); - (sum_lo, carry_lo) = Self::add_u64_with_carry(sum_lo, prod_lo_2, carry_lo); - (sum_hi, carry_hi) = Self::add_u64_with_carry(sum_hi, prod_hi_2, carry_hi); - - let prod_lo_3 = vmull_u32(vget_low_u32(a[3]), vget_low_u32(b[3])); - let prod_hi_3 = vmull_high_u32(a[3], b[3]); - (sum_lo, carry_lo) = Self::add_u64_with_carry(sum_lo, prod_lo_3, carry_lo); - (sum_hi, carry_hi) = Self::add_u64_with_carry(sum_hi, prod_hi_3, carry_hi); - - Self::solinas_reduce_with_carry(sum_lo, sum_hi, carry_lo, carry_hi) - } - } - - #[inline(always)] - fn dot_product_3_vec(a: [uint32x4_t; 3], b: [uint32x4_t; 3]) -> uint32x4_t { - unsafe { - let mut sum_lo = vmull_u32(vget_low_u32(a[0]), vget_low_u32(b[0])); - let mut sum_hi = vmull_high_u32(a[0], b[0]); - - if Self::BITS <= 31 { - sum_lo = vaddq_u64(sum_lo, vmull_u32(vget_low_u32(a[1]), vget_low_u32(b[1]))); - sum_hi = vaddq_u64(sum_hi, vmull_high_u32(a[1], b[1])); - sum_lo = vaddq_u64(sum_lo, vmull_u32(vget_low_u32(a[2]), vget_low_u32(b[2]))); - sum_hi = vaddq_u64(sum_hi, vmull_high_u32(a[2], b[2])); - - return Self::solinas_reduce(sum_lo, sum_hi); - } - - let mut carry_lo = vdupq_n_u64(0); - let mut carry_hi = vdupq_n_u64(0); - - let prod_lo_1 = vmull_u32(vget_low_u32(a[1]), vget_low_u32(b[1])); - let prod_hi_1 = vmull_high_u32(a[1], b[1]); - (sum_lo, carry_lo) = Self::add_u64_with_carry(sum_lo, prod_lo_1, carry_lo); - (sum_hi, carry_hi) = Self::add_u64_with_carry(sum_hi, prod_hi_1, carry_hi); - - let prod_lo_2 = vmull_u32(vget_low_u32(a[2]), vget_low_u32(b[2])); - let prod_hi_2 = vmull_high_u32(a[2], b[2]); - (sum_lo, carry_lo) = Self::add_u64_with_carry(sum_lo, prod_lo_2, carry_lo); - (sum_hi, carry_hi) = Self::add_u64_with_carry(sum_hi, prod_hi_2, carry_hi); - - Self::solinas_reduce_with_carry(sum_lo, sum_hi, carry_lo, carry_hi) - } - } - - #[inline(always)] - fn mul_nr_vec(x: uint32x4_t) -> uint32x4_t - where - C: FpExt2Config>, - { - if C::IS_NEG_ONE { - Self::sub_vec(unsafe { vdupq_n_u32(0) }, x) - } else if C::non_residue().0 == 2 { - Self::add_vec(x, x) - } else { - C::mul_non_residue(Self::from_vec(x), Self::broadcast).to_vec() - } - } - - #[inline(always)] - fn mul_c_u64(hi: uint64x2_t, c: uint32x2_t) -> uint64x2_t { - unsafe { - if Self::C == 1 { - return hi; - } - if Self::C == 3 { - return vaddq_u64(vshlq_n_u64::<1>(hi), hi); - } - if Self::C == 19 { - return vaddq_u64(vaddq_u64(vshlq_n_u64::<4>(hi), vshlq_n_u64::<1>(hi)), hi); - } - if Self::C == 35 { - return vaddq_u64(vaddq_u64(vshlq_n_u64::<5>(hi), vshlq_n_u64::<1>(hi)), hi); - } - if Self::C == 99 { - return vaddq_u64( - vaddq_u64(vshlq_n_u64::<6>(hi), vshlq_n_u64::<5>(hi)), - vaddq_u64(vshlq_n_u64::<1>(hi), hi), - ); - } - let lo = vmull_u32(vmovn_u64(hi), c); - let hi = vmull_u32(vmovn_u64(vshrq_n_u64::<32>(hi)), c); - vaddq_u64(lo, vshlq_n_u64::<32>(hi)) - } - } - - #[inline(always)] - fn solinas_reduce(prod_lo: uint64x2_t, prod_hi: uint64x2_t) -> uint32x4_t { - unsafe { - if Self::BITS == 31 { - return Self::solinas_reduce_bits31(prod_lo, prod_hi); - } - - let mask = vdupq_n_u64(Self::MASK_U64); - let neg_bits = vdupq_n_s64(-(Self::BITS as i64)); - let c = vdup_n_u32(Self::C); - - let f1_lo = vaddq_u64( - vandq_u64(prod_lo, mask), - Self::mul_c_u64(vshlq_u64(prod_lo, neg_bits), c), - ); - let f1_hi = vaddq_u64( - vandq_u64(prod_hi, mask), - Self::mul_c_u64(vshlq_u64(prod_hi, neg_bits), c), - ); - - let f2_lo = vaddq_u64( - vandq_u64(f1_lo, mask), - Self::mul_c_u64(vshlq_u64(f1_lo, neg_bits), c), - ); - let f2_hi = vaddq_u64( - vandq_u64(f1_hi, mask), - Self::mul_c_u64(vshlq_u64(f1_hi, neg_bits), c), - ); - - if Self::BITS < 32 { - let (reduced_lo, reduced_hi) = if Self::TWO_FOLD_FOUR_PRODUCT_OK { - (f2_lo, f2_hi) - } else { - ( - vaddq_u64( - vandq_u64(f2_lo, mask), - Self::mul_c_u64(vshlq_u64(f2_lo, neg_bits), c), - ), - vaddq_u64( - vandq_u64(f2_hi, mask), - Self::mul_c_u64(vshlq_u64(f2_hi, neg_bits), c), - ), - ) - }; - - let result = vcombine_u32(vmovn_u64(reduced_lo), vmovn_u64(reduced_hi)); - let p = vdupq_n_u32(P); - vminq_u32(result, vsubq_u32(result, p)) - } else { - let p_u64 = vdupq_n_u64(P as u64); - - let red_lo = vsubq_u64(f2_lo, p_u64); - let keep_lo = vcltq_u64(f2_lo, p_u64); - let out_lo = vbslq_u64(keep_lo, f2_lo, red_lo); - - let red_hi = vsubq_u64(f2_hi, p_u64); - let keep_hi = vcltq_u64(f2_hi, p_u64); - let out_hi = vbslq_u64(keep_hi, f2_hi, red_hi); - - vcombine_u32(vmovn_u64(out_lo), vmovn_u64(out_hi)) - } - } - } - - #[inline(always)] - fn solinas_reduce_bits31(prod_lo: uint64x2_t, prod_hi: uint64x2_t) -> uint32x4_t { - unsafe { - let mask = vdupq_n_u64((1u64 << 31) - 1); - let c = vdup_n_u32(Self::C); - - let f1_lo = vaddq_u64( - vandq_u64(prod_lo, mask), - Self::mul_c_u64(vshrq_n_u64::<31>(prod_lo), c), - ); - let f1_hi = vaddq_u64( - vandq_u64(prod_hi, mask), - Self::mul_c_u64(vshrq_n_u64::<31>(prod_hi), c), - ); - - let f2_lo = vaddq_u64( - vandq_u64(f1_lo, mask), - Self::mul_c_u64(vshrq_n_u64::<31>(f1_lo), c), - ); - let f2_hi = vaddq_u64( - vandq_u64(f1_hi, mask), - Self::mul_c_u64(vshrq_n_u64::<31>(f1_hi), c), - ); - - let (reduced_lo, reduced_hi) = if Self::TWO_FOLD_FOUR_PRODUCT_OK { - (f2_lo, f2_hi) - } else { - ( - vaddq_u64( - vandq_u64(f2_lo, mask), - Self::mul_c_u64(vshrq_n_u64::<31>(f2_lo), c), - ), - vaddq_u64( - vandq_u64(f2_hi, mask), - Self::mul_c_u64(vshrq_n_u64::<31>(f2_hi), c), - ), - ) - }; - - let result = vcombine_u32(vmovn_u64(reduced_lo), vmovn_u64(reduced_hi)); - let p = vdupq_n_u32(P); - vminq_u32(result, vsubq_u32(result, p)) - } - } - - #[inline(always)] - fn solinas_reduce_with_carry( - prod_lo: uint64x2_t, - prod_hi: uint64x2_t, - carry_lo: uint64x2_t, - carry_hi: uint64x2_t, - ) -> uint32x4_t { - unsafe { - if Self::BITS == 31 { - return Self::solinas_reduce_with_carry_bits31( - prod_lo, prod_hi, carry_lo, carry_hi, - ); - } - - let mask = vdupq_n_u64(Self::MASK_U64); - let neg_bits = vdupq_n_s64(-(Self::BITS as i64)); - let c = vdup_n_u32(Self::C); - - let f1_lo = vaddq_u64( - vaddq_u64( - vandq_u64(prod_lo, mask), - Self::mul_c_u64(vshlq_u64(prod_lo, neg_bits), c), - ), - Self::carry_correction(carry_lo), - ); - let f1_hi = vaddq_u64( - vaddq_u64( - vandq_u64(prod_hi, mask), - Self::mul_c_u64(vshlq_u64(prod_hi, neg_bits), c), - ), - Self::carry_correction(carry_hi), - ); - - let f2_lo = vaddq_u64( - vandq_u64(f1_lo, mask), - Self::mul_c_u64(vshlq_u64(f1_lo, neg_bits), c), - ); - let f2_hi = vaddq_u64( - vandq_u64(f1_hi, mask), - Self::mul_c_u64(vshlq_u64(f1_hi, neg_bits), c), - ); - - if Self::BITS < 32 { - let (reduced_lo, reduced_hi) = if Self::TWO_FOLD_FOUR_PRODUCT_OK { - (f2_lo, f2_hi) - } else { - ( - vaddq_u64( - vandq_u64(f2_lo, mask), - Self::mul_c_u64(vshlq_u64(f2_lo, neg_bits), c), - ), - vaddq_u64( - vandq_u64(f2_hi, mask), - Self::mul_c_u64(vshlq_u64(f2_hi, neg_bits), c), - ), - ) - }; - - let result = vcombine_u32(vmovn_u64(reduced_lo), vmovn_u64(reduced_hi)); - let p = vdupq_n_u32(P); - vminq_u32(result, vsubq_u32(result, p)) - } else { - let p_u64 = vdupq_n_u64(P as u64); - - let red_lo = vsubq_u64(f2_lo, p_u64); - let keep_lo = vcltq_u64(f2_lo, p_u64); - let out_lo = vbslq_u64(keep_lo, f2_lo, red_lo); - - let red_hi = vsubq_u64(f2_hi, p_u64); - let keep_hi = vcltq_u64(f2_hi, p_u64); - let out_hi = vbslq_u64(keep_hi, f2_hi, red_hi); - - vcombine_u32(vmovn_u64(out_lo), vmovn_u64(out_hi)) - } - } - } - - /// `solinas_reduce_with_carry` specialised for `BITS == 31` (Mersenne31 - /// and any pseudo-Mersenne `Fp32

` with `P = 2^31 - C`). Sibling of - /// `solinas_reduce_bits31`: a separate function that swaps the - /// variable-amount `vshlq_u64(.., neg_bits)` for the immediate-shift - /// `vshrq_n_u64::<31>`, reducing shift-count register pressure and - /// dispatch port pressure. Since `BITS == 31` implies `BITS < 32`, the - /// `else` branch of the canonicalisation can be dropped. - #[inline(always)] - fn solinas_reduce_with_carry_bits31( - prod_lo: uint64x2_t, - prod_hi: uint64x2_t, - carry_lo: uint64x2_t, - carry_hi: uint64x2_t, - ) -> uint32x4_t { - unsafe { - let mask = vdupq_n_u64((1u64 << 31) - 1); - let c = vdup_n_u32(Self::C); - - // Fold 1 with carry correction - let f1_lo = vaddq_u64( - vaddq_u64( - vandq_u64(prod_lo, mask), - Self::mul_c_u64(vshrq_n_u64::<31>(prod_lo), c), - ), - Self::carry_correction(carry_lo), - ); - let f1_hi = vaddq_u64( - vaddq_u64( - vandq_u64(prod_hi, mask), - Self::mul_c_u64(vshrq_n_u64::<31>(prod_hi), c), - ), - Self::carry_correction(carry_hi), - ); - - // Fold 2 - let f2_lo = vaddq_u64( - vandq_u64(f1_lo, mask), - Self::mul_c_u64(vshrq_n_u64::<31>(f1_lo), c), - ); - let f2_hi = vaddq_u64( - vandq_u64(f1_hi, mask), - Self::mul_c_u64(vshrq_n_u64::<31>(f1_hi), c), - ); - - let (reduced_lo, reduced_hi) = if Self::TWO_FOLD_FOUR_PRODUCT_OK { - (f2_lo, f2_hi) - } else { - ( - vaddq_u64( - vandq_u64(f2_lo, mask), - Self::mul_c_u64(vshrq_n_u64::<31>(f2_lo), c), - ), - vaddq_u64( - vandq_u64(f2_hi, mask), - Self::mul_c_u64(vshrq_n_u64::<31>(f2_hi), c), - ), - ) - }; - - let result = vcombine_u32(vmovn_u64(reduced_lo), vmovn_u64(reduced_hi)); - let p = vdupq_n_u32(P); - vminq_u32(result, vsubq_u32(result, p)) - } - } -} - -impl Default for PackedFp32Neon

{ - #[inline] - fn default() -> Self { - Self { vals: [0; 4] } - } -} - -impl fmt::Debug for PackedFp32Neon

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("PackedFp32Neon").field(&self.vals).finish() - } -} - -impl PartialEq for PackedFp32Neon

{ - #[inline] - fn eq(&self, other: &Self) -> bool { - self.vals == other.vals - } -} - -impl Eq for PackedFp32Neon

{} - -impl Add for PackedFp32Neon

{ - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - Self::from_vec(Self::add_vec(self.to_vec(), rhs.to_vec())) - } -} - -impl Sub for PackedFp32Neon

{ - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - Self::from_vec(Self::sub_vec(self.to_vec(), rhs.to_vec())) - } -} - -impl Mul for PackedFp32Neon

{ - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self { - Self::from_vec(Self::mul_vec(self.to_vec(), rhs.to_vec())) - } -} - -impl AddAssign for PackedFp32Neon

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for PackedFp32Neon

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for PackedFp32Neon

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl PackedField for PackedFp32Neon

{ - const WIDTH: usize = FP32_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Scalar, - { - Self { - vals: [f(0).0, f(1).0, f(2).0, f(3).0], - } - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Scalar { - debug_assert!(lane < FP32_WIDTH); - Fp32(self.vals[lane]) - } - - type Scalar = Fp32

; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self { vals: [value.0; 4] } - } - - #[inline(always)] - fn fp_ext2_mul(a0: Self, a1: Self, b0: Self, b1: Self) -> (Self, Self) - where - C: FpExt2Config, - { - let a0 = a0.to_vec(); - let a1 = a1.to_vec(); - let b0 = b0.to_vec(); - let b1 = b1.to_vec(); - - let v0 = Self::mul_vec(a0, b0); - let v1 = Self::mul_vec(a1, b1); - let cross = Self::mul_vec(Self::add_vec(a0, a1), Self::add_vec(b0, b1)); - - ( - Self::from_vec(Self::add_vec(v0, Self::mul_nr_vec::(v1))), - Self::from_vec(Self::sub_vec(Self::sub_vec(cross, v0), v1)), - ) - } - - #[inline(always)] - fn fp_ext4_mul(a: [Self; 4], b: [Self; 4]) -> [Self; 4] { - let [a0, a1, a2, a3] = a.map(Self::to_vec); - let [b0, b1, b2, b3] = b.map(Self::to_vec); - let two_b1 = Self::add_vec(b1, b1); - let two_b2 = Self::add_vec(b2, b2); - let two_b3 = Self::add_vec(b3, b3); - let b0_plus_b2 = Self::add_vec(b0, b2); - let b1_plus_b3 = Self::add_vec(b1, b3); - let b1_minus_b3 = Self::sub_vec(b1, b3); - let b0_minus_b2 = Self::sub_vec(b0, b2); - [ - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [b0, two_b1, two_b2, two_b3], - )), - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [b1, b0_plus_b2, b1_plus_b3, b2], - )), - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [b2, b1_plus_b3, b0, b1_minus_b3], - )), - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [b3, b2, b1_minus_b3, b0_minus_b2], - )), - ] - } - - #[inline(always)] - fn fp_ext4_square(a: [Self; 4]) -> [Self; 4] { - let [a0, a1, a2, a3] = a.map(Self::to_vec); - let zero = unsafe { vdupq_n_u32(0) }; - let two_a1 = Self::add_vec(a1, a1); - let two_a2 = Self::add_vec(a2, a2); - let two_a3 = Self::add_vec(a3, a3); - let neg_a3 = Self::sub_vec(zero, a3); - let neg_two_a3 = Self::sub_vec(zero, two_a3); - [ - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a2, a3], - [a0, two_a1, two_a2, two_a3], - )), - Self::from_vec(Self::dot_product_3_vec( - [a0, a1, a2], - [two_a1, two_a2, two_a3], - )), - Self::from_vec(Self::dot_product_4_vec( - [a0, a1, a1, a3], - [two_a2, a1, two_a3, neg_a3], - )), - Self::from_vec(Self::dot_product_3_vec( - [a0, a1, a2], - [two_a3, two_a2, neg_two_a3], - )), - ] - } - - #[inline(always)] - fn fp_ext4_inverse(a: [Self; 4]) -> Option<[Self; 4]> - where - Self::Scalar: FieldCore, - { - let [a0, a1, a2, a3] = a.map(Self::to_vec); - let zero = unsafe { vdupq_n_u32(0) }; - let x0 = a0; - let x1 = a2; - let y0 = Self::sub_vec(a1, a3); - let y1 = a3; - - let x1_square = Self::mul_vec(x1, x1); - let y1_square = Self::mul_vec(y1, y1); - let aa0 = Self::add_vec(Self::mul_vec(x0, x0), Self::add_vec(x1_square, x1_square)); - let aa1 = { - let x0x1 = Self::mul_vec(x0, x1); - Self::add_vec(x0x1, x0x1) - }; - let bb0 = Self::add_vec(Self::mul_vec(y0, y0), Self::add_vec(y1_square, y1_square)); - let bb1 = { - let y0y1 = Self::mul_vec(y0, y1); - Self::add_vec(y0y1, y0y1) - }; - let nr_bb0 = Self::add_vec(Self::add_vec(bb0, bb0), Self::add_vec(bb1, bb1)); - let nr_bb1 = Self::add_vec(bb0, Self::add_vec(bb1, bb1)); - let norm0 = Self::sub_vec(aa0, nr_bb0); - let norm1 = Self::sub_vec(aa1, nr_bb1); - - let inv_norm_base = { - let norm1_square = Self::mul_vec(norm1, norm1); - let norm_base = Self::sub_vec( - Self::mul_vec(norm0, norm0), - Self::add_vec(norm1_square, norm1_square), - ); - Self::from_vec(norm_base).inverse()?.to_vec() - }; - let inv_norm0 = Self::mul_vec(norm0, inv_norm_base); - let inv_norm1 = Self::mul_vec(Self::sub_vec(zero, norm1), inv_norm_base); - - let v0 = Self::mul_vec(x0, inv_norm0); - let v1 = Self::mul_vec(x1, inv_norm1); - let constant0 = Self::add_vec(v0, Self::add_vec(v1, v1)); - let constant1 = Self::sub_vec( - Self::sub_vec( - Self::mul_vec(Self::add_vec(x0, x1), Self::add_vec(inv_norm0, inv_norm1)), - v0, - ), - v1, - ); - - let neg_y0 = Self::sub_vec(zero, y0); - let neg_y1 = Self::sub_vec(zero, y1); - let w0 = Self::mul_vec(neg_y0, inv_norm0); - let w1 = Self::mul_vec(neg_y1, inv_norm1); - let e1_coeff0 = Self::add_vec(w0, Self::add_vec(w1, w1)); - let e1_coeff1 = Self::sub_vec( - Self::sub_vec( - Self::mul_vec( - Self::add_vec(neg_y0, neg_y1), - Self::add_vec(inv_norm0, inv_norm1), - ), - w0, - ), - w1, - ); - - Some([ - Self::from_vec(constant0), - Self::from_vec(Self::add_vec(e1_coeff0, e1_coeff1)), - Self::from_vec(constant1), - Self::from_vec(e1_coeff1), - ]) - } -} diff --git a/crates/jolt-field/src/packed/neon/fp64.rs b/crates/jolt-field/src/packed/neon/fp64.rs deleted file mode 100644 index 0b18f23f0e..0000000000 --- a/crates/jolt-field/src/packed/neon/fp64.rs +++ /dev/null @@ -1,221 +0,0 @@ -use super::*; - -/// Number of packed `Fp64` lanes. -pub(crate) const FP64_WIDTH: usize = 2; - -/// NEON packed `Fp64` backend: 2 lanes in `uint64x2_t`. -#[derive(Clone, Copy)] -pub struct PackedFp64Neon { - vals: [u64; 2], -} - -impl PackedFp64Neon

{ - const BITS: u32 = 64 - P.leading_zeros(); - - const C_LO: u64 = { - let c = if Self::BITS == 64 { - 0u64.wrapping_sub(P) - } else { - (1u64 << Self::BITS) - P - }; - assert!(P != 0, "modulus must be nonzero"); - assert!(P & 1 == 1, "modulus must be odd"); - c - }; - - const MASK64: u64 = if Self::BITS < 64 { - (1u64 << Self::BITS) - 1 - } else { - u64::MAX - }; - - const MASK_U128: u128 = if Self::BITS == 64 { - u64::MAX as u128 - } else { - (1u128 << Self::BITS) - 1 - }; - - const FOLD_IN_U64: bool = - Self::BITS < 64 && (Self::C_LO as u128) < (1u128 << (64 - Self::BITS)); - - #[inline(always)] - fn mul_c_narrow(x: u64) -> u64 { - Self::C_LO.wrapping_mul(x) - } - - #[inline(always)] - fn reduce_product(x: u128) -> u64 { - if Self::FOLD_IN_U64 { - let lo = x as u64; - let hi = (x >> 64) as u64; - let high = (lo >> Self::BITS) | (hi << (64 - Self::BITS)); - let f1 = (lo & Self::MASK64).wrapping_add(Self::mul_c_narrow(high)); - let f2 = (f1 & Self::MASK64).wrapping_add(Self::mul_c_narrow(f1 >> Self::BITS)); - let reduced = f2.wrapping_sub(P); - let borrow = reduced >> 63; - reduced.wrapping_add(borrow.wrapping_neg() & P) - } else { - let f1 = - (x & Self::MASK_U128) + (Self::C_LO as u128) * ((x >> Self::BITS) as u64 as u128); - let f2 = - (f1 & Self::MASK_U128) + (Self::C_LO as u128) * ((f1 >> Self::BITS) as u64 as u128); - let reduced = f2.wrapping_sub(P as u128); - let borrow = reduced >> 127; - reduced.wrapping_add(borrow.wrapping_neg() & (P as u128)) as u64 - } - } -} - -impl Default for PackedFp64Neon

{ - #[inline] - fn default() -> Self { - Self { vals: [0; 2] } - } -} - -impl fmt::Debug for PackedFp64Neon

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("PackedFp64Neon").field(&self.vals).finish() - } -} - -impl PartialEq for PackedFp64Neon

{ - #[inline] - fn eq(&self, other: &Self) -> bool { - self.vals == other.vals - } -} - -impl Eq for PackedFp64Neon

{} - -impl Add for PackedFp64Neon

{ - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - let a = to_vec(self.vals); - let b = to_vec(rhs.vals); - let result = unsafe { - let p = vdupq_n_u64(P); - if Self::BITS == 64 { - let s = vaddq_u64(a, b); - let overflow = vcltq_u64(s, a); - let folded = vaddq_u64(s, vandq_u64(overflow, vdupq_n_u64(Self::C_LO))); - let reduced = vsubq_u64(folded, p); - let borrow = vcltq_u64(folded, p); - vbslq_u64(borrow, folded, reduced) - } else if Self::BITS <= 62 { - let s = vaddq_u64(a, b); - let r = vsubq_u64(s, p); - let borrow = vcltq_u64(s, p); - vbslq_u64(borrow, s, r) - } else { - let s = vaddq_u64(a, b); - let overflow = vcltq_u64(s, a); - let c = vdupq_n_u64(Self::C_LO); - let s_plus_c = vaddq_u64(s, c); - let s_minus_p = vsubq_u64(s, p); - let borrow = vcltq_u64(s, p); - let no_of = vbslq_u64(borrow, s, s_minus_p); - vbslq_u64(overflow, s_plus_c, no_of) - } - }; - Self { - vals: from_vec(result), - } - } -} - -impl Sub for PackedFp64Neon

{ - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - let a = to_vec(self.vals); - let b = to_vec(rhs.vals); - let result = unsafe { - let d = vsubq_u64(a, b); - let underflow = vcltq_u64(a, b); - if Self::BITS == 64 { - vsubq_u64(d, vandq_u64(underflow, vdupq_n_u64(Self::C_LO))) - } else { - vbslq_u64(underflow, vaddq_u64(d, vdupq_n_u64(P)), d) - } - }; - Self { - vals: from_vec(result), - } - } -} - -impl Mul for PackedFp64Neon

{ - type Output = Self; - #[inline] - fn mul(self, rhs: Self) -> Self { - let x0 = (self.vals[0] as u128) * (rhs.vals[0] as u128); - let x1 = (self.vals[1] as u128) * (rhs.vals[1] as u128); - let r0 = Self::reduce_product(x0); - let r1 = Self::reduce_product(x1); - Self { vals: [r0, r1] } - } -} - -impl AddAssign for PackedFp64Neon

{ - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for PackedFp64Neon

{ - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl MulAssign for PackedFp64Neon

{ - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl PackedField for PackedFp64Neon

{ - const WIDTH: usize = FP64_WIDTH; - - #[inline] - fn from_fn(mut f: F) -> Self - where - F: FnMut(usize) -> Self::Scalar, - { - Self { - vals: [f(0).0, f(1).0], - } - } - - #[inline] - fn extract(&self, lane: usize) -> Self::Scalar { - debug_assert!(lane < FP64_WIDTH); - Fp64(self.vals[lane]) - } - - type Scalar = Fp64

; - - #[inline] - fn broadcast(value: Self::Scalar) -> Self { - Self { vals: [value.0; 2] } - } - - #[inline(always)] - fn fp_ext2_mul(a0: Self, a1: Self, b0: Self, b1: Self) -> (Self, Self) - where - C: FpExt2Config, - { - let v0 = a0 * b0; - let v1 = a1 * b1; - let cross = (a0 + a1) * (b0 + b1); - ( - v0 + C::mul_non_residue(v1, Self::broadcast), - cross - v0 - v1, - ) - } -} diff --git a/crates/jolt-field/src/packed/neon/mod.rs b/crates/jolt-field/src/packed/neon/mod.rs deleted file mode 100644 index 28a8d2b2d6..0000000000 --- a/crates/jolt-field/src/packed/neon/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! AArch64 NEON packed backends for Fp32, Fp64, Fp128. - -#![expect( - clippy::undocumented_unsafe_blocks, - reason = "ported NEON kernels retain their audited intrinsic-level invariants" -)] - -use super::PackedField; -use crate::ext::FpExt2Config; -use crate::FieldCore; -use crate::{Fp128, Fp32, Fp64}; -use core::arch::aarch64::{ - uint32x2_t, uint32x4_t, uint64x2_t, vaddq_u32, vaddq_u64, vandq_u32, vandq_u64, vbslq_u64, - vcltq_u32, vcltq_u64, vcombine_u32, vdup_n_u32, vdupq_n_s64, vdupq_n_u32, vdupq_n_u64, - vget_low_u32, vminq_u32, vmlsq_u32, vmovn_u64, vmull_high_u32, vmull_u32, vmulq_u32, vorrq_u64, - vqdmulhq_s32, vreinterpretq_s32_u32, vreinterpretq_u32_s32, vshlq_n_u64, vshlq_u64, - vshrq_n_u32, vshrq_n_u64, vsubq_u32, vsubq_u64, -}; -use core::fmt; -use core::mem::transmute; -use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; - -#[inline(always)] -fn to_vec(x: [u64; 2]) -> uint64x2_t { - unsafe { transmute::<[u64; 2], uint64x2_t>(x) } -} - -#[inline(always)] -fn from_vec(v: uint64x2_t) -> [u64; 2] { - unsafe { transmute::(v) } -} - -#[inline(always)] -fn mask_to_bit(mask: uint64x2_t) -> uint64x2_t { - unsafe { vandq_u64(mask, vdupq_n_u64(1)) } -} - -mod fp128; -mod fp32; -mod fp64; -pub(crate) use fp128::*; -pub(crate) use fp32::*; -pub(crate) use fp64::*; diff --git a/crates/jolt-field/src/packed/tests.rs b/crates/jolt-field/src/packed/tests.rs deleted file mode 100644 index 595bc13f53..0000000000 --- a/crates/jolt-field/src/packed/tests.rs +++ /dev/null @@ -1,344 +0,0 @@ -#![expect( - clippy::unreadable_literal, - reason = "packed regression vectors retain their generated decimal form" -)] - -use super::{HasPacking, PackedField}; -use crate::{ - CanonicalField, FieldCore, Fp32, Prime128Offset275, Prime24Offset3, Prime31Offset19, - Prime32Offset99, Prime40Offset195, Prime64Offset59, -}; -use rand::{rngs::StdRng, RngCore, SeedableRng}; - -fn rand_u128(rng: &mut R) -> u128 { - let lo = rng.next_u64() as u128; - let hi = rng.next_u64() as u128; - lo | (hi << 64) -} - -fn check_packed_add_sub_mul(seed: u64) -where - F: FieldCore + PartialEq + std::fmt::Debug, - PF: PackedField, -{ - let mut rng = StdRng::seed_from_u64(seed); - let len = PF::WIDTH * 17 + 3; - let lhs: Vec = (0..len).map(|_| FieldCore::random(&mut rng)).collect(); - let rhs: Vec = (0..len).map(|_| FieldCore::random(&mut rng)).collect(); - - let (lhs_p, lhs_s) = PF::pack_slice_with_suffix(&lhs); - let (rhs_p, rhs_s) = PF::pack_slice_with_suffix(&rhs); - - let add_p: Vec = lhs_p - .iter() - .zip(rhs_p.iter()) - .map(|(&a, &b)| a + b) - .collect(); - let sub_p: Vec = lhs_p - .iter() - .zip(rhs_p.iter()) - .map(|(&a, &b)| a - b) - .collect(); - let mul_p: Vec = lhs_p - .iter() - .zip(rhs_p.iter()) - .map(|(&a, &b)| a * b) - .collect(); - - let mut add_out = PF::unpack_slice(&add_p); - let mut sub_out = PF::unpack_slice(&sub_p); - let mut mul_out = PF::unpack_slice(&mul_p); - - for (&a, &b) in lhs_s.iter().zip(rhs_s.iter()) { - add_out.push(a + b); - sub_out.push(a - b); - mul_out.push(a * b); - } - - for i in 0..len { - assert_eq!( - add_out[i], - lhs[i] + rhs[i], - "packed add mismatch at lane {i}" - ); - assert_eq!( - sub_out[i], - lhs[i] - rhs[i], - "packed sub mismatch at lane {i}" - ); - assert_eq!( - mul_out[i], - lhs[i] * rhs[i], - "packed mul mismatch at lane {i}" - ); - } -} - -fn check_broadcast_roundtrip(val: F) -where - F: FieldCore + PartialEq + std::fmt::Debug, - PF: PackedField, -{ - let p = PF::broadcast(val); - for lane in 0..PF::WIDTH { - assert_eq!(p.extract(lane), val); - } -} - -fn check_packed_fp32_edge_lanes() -where - PF: PackedField>, -{ - let p_minus_one = Fp32::

::from_canonical_u32(P - 1); - let p_minus_two = Fp32::

::from_canonical_u32(P - 2); - let values = [ - Fp32::

::zero(), - Fp32::

::one(), - p_minus_two, - p_minus_one, - ]; - let a = PF::from_fn(|i| values[i % values.len()]); - let b = PF::from_fn(|i| values[(i + 1) % values.len()]); - - let add = a + b; - let sub = a - b; - let mul = a * b; - - for lane in 0..PF::WIDTH { - let lhs = values[lane % values.len()]; - let rhs = values[(lane + 1) % values.len()]; - assert_eq!(add.extract(lane), lhs + rhs, "packed add edge lane {lane}"); - assert_eq!(sub.extract(lane), lhs - rhs, "packed sub edge lane {lane}"); - assert_eq!(mul.extract(lane), lhs * rhs, "packed mul edge lane {lane}"); - } -} - -#[test] -fn packed_fp128_add_sub_mul_match_scalar() { - type F = Prime128Offset275; - type PF = ::Packing; - - let mut rng = StdRng::seed_from_u64(0x55aa_4422_1177_0033); - let len = PF::WIDTH * 17 + 3; - let lhs: Vec = (0..len) - .map(|_| F::from_canonical_u128_reduced(rand_u128(&mut rng))) - .collect(); - let rhs: Vec = (0..len) - .map(|_| F::from_canonical_u128_reduced(rand_u128(&mut rng))) - .collect(); - - let (lhs_p, lhs_s) = PF::pack_slice_with_suffix(&lhs); - let (rhs_p, rhs_s) = PF::pack_slice_with_suffix(&rhs); - - let add_p: Vec = lhs_p - .iter() - .zip(rhs_p.iter()) - .map(|(&a, &b)| a + b) - .collect(); - let sub_p: Vec = lhs_p - .iter() - .zip(rhs_p.iter()) - .map(|(&a, &b)| a - b) - .collect(); - let mul_p: Vec = lhs_p - .iter() - .zip(rhs_p.iter()) - .map(|(&a, &b)| a * b) - .collect(); - - let mut add_out = PF::unpack_slice(&add_p); - let mut sub_out = PF::unpack_slice(&sub_p); - let mut mul_out = PF::unpack_slice(&mul_p); - - for (&a, &b) in lhs_s.iter().zip(rhs_s.iter()) { - add_out.push(a + b); - sub_out.push(a - b); - mul_out.push(a * b); - } - - for i in 0..len { - assert_eq!( - add_out[i], - lhs[i] + rhs[i], - "packed add mismatch at lane {i}" - ); - assert_eq!( - sub_out[i], - lhs[i] - rhs[i], - "packed sub mismatch at lane {i}" - ); - assert_eq!( - mul_out[i], - lhs[i] * rhs[i], - "packed mul mismatch at lane {i}" - ); - } -} - -#[test] -fn fp128_broadcast_and_extract_roundtrip() { - type F = Prime128Offset275; - type PF = ::Packing; - check_broadcast_roundtrip::(F::from_u64(42)); -} - -#[test] -fn packed_fp32_24b_add_sub_mul() { - type F = Prime24Offset3; - type PF = ::Packing; - check_packed_add_sub_mul::(0xaa24_bb24_cc24_dd24); -} - -#[test] -fn packed_fp32_31b_add_sub_mul() { - type F = Prime31Offset19; - type PF = ::Packing; - check_packed_add_sub_mul::(0xaa31_bb31_cc31_dd31); -} - -#[test] -fn packed_fp32_31b_edge_lanes() { - type F = Prime31Offset19; - type PF = ::Packing; - check_packed_fp32_edge_lanes::<{ crate::prime::pseudo_mersenne::PRIME31_OFFSET19_MODULUS }, PF>( - ); -} - -#[test] -fn packed_mersenne31_edge_lanes() { - type F = Fp32<{ (1u32 << 31) - 1 }>; - type PF = ::Packing; - check_packed_fp32_edge_lanes::<{ (1u32 << 31) - 1 }, PF>(); -} - -/// Stress the 31-bit pseudo-Mersenne (`C > 1`) packed multiply against the -/// scalar reference across boundary values and a large random sweep. This -/// confirms (does not justify) the exact correctness proof on -/// `mul_pmersenne31_vec`: the tightest cases are `z = (P-1)^2` and inputs -/// that drive the second fold's `t'` toward `2P`. -#[test] -fn packed_fp32_31b_mul_matches_scalar_stress() { - type F = Prime31Offset19; - type PF = ::Packing; - const P: u32 = crate::prime::pseudo_mersenne::PRIME31_OFFSET19_MODULUS; - - let boundary = [ - 0u32, - 1, - 2, - 3, - 19, - 1 << 15, - 1 << 30, - (1 << 30) + 1, - (P - 1) / 2, - P - 3, - P - 2, - P - 1, - ]; - - let mut inputs: Vec = boundary.iter().map(|&v| F::from_canonical_u32(v)).collect(); - let mut rng = StdRng::seed_from_u64(0x31be_19ca_fe00_1357); - for _ in 0..(1 << 16) { - inputs.push(F::from_canonical_u32(rng.next_u32() % P)); - } - - let lhs: Vec = inputs.clone(); - let rhs: Vec = { - let mut r = inputs.clone(); - r.rotate_left(1); - r - }; - - let (lhs_p, lhs_s) = PF::pack_slice_with_suffix(&lhs); - let (rhs_p, rhs_s) = PF::pack_slice_with_suffix(&rhs); - let mul_p: Vec = lhs_p - .iter() - .zip(rhs_p.iter()) - .map(|(&a, &b)| a * b) - .collect(); - let mut mul_out = PF::unpack_slice(&mul_p); - for (&a, &b) in lhs_s.iter().zip(rhs_s.iter()) { - mul_out.push(a * b); - } - for i in 0..lhs.len() { - assert_eq!(mul_out[i], lhs[i] * rhs[i], "packed mul mismatch at {i}"); - } - - // Full boundary x boundary cross product (every tight combination). - for &x in &boundary { - for &y in &boundary { - let a = PF::broadcast(F::from_canonical_u32(x)); - let b = PF::broadcast(F::from_canonical_u32(y)); - let got = (a * b).extract(0); - let want = F::from_canonical_u32(x) * F::from_canonical_u32(y); - assert_eq!(got, want, "boundary mul {x}*{y}"); - } - } -} - -#[test] -fn packed_fp32_32b_add_sub_mul() { - type F = Prime32Offset99; - type PF = ::Packing; - check_packed_add_sub_mul::(0xaa32_bb32_cc32_dd32); -} - -/// Regression guard for the 32-bit (`BITS == 32`) packed base multiply. -/// -/// For these primes the two-fold Solinas residue can land in `[2^32, 2*P)` -/// (up to `2^32 + C^2`). The packed `Mul` recombine must subtract `P` on the -/// full 64-bit lanes before packing; a 32-bit recombine drops bit 32 and -/// returns a result that is `C` too small. The probability of hitting this -/// window with uniform random inputs is `~C/2^32 ≈ 2e-6`, so the random -/// parity sweep misses it; these vectors hit it deterministically. They were -/// found by exhaustively comparing the truncating recombine to the true -/// modular product (all land in the overflow window on `Prime32Offset99`). -#[test] -fn packed_fp32_32b_mul_two_fold_overflow_window() { - type F = Prime32Offset99; - type PF = ::Packing; - const VECTORS: [(u32, u32); 7] = [ - (3136721438, 3536064673), - (2498152412, 1827148629), - (2062525777, 3207684599), - (4027016701, 3739597742), - (2476582663, 3902052967), - (4161561975, 3109742861), - (1924659530, 1057556213), - ]; - for (x, y) in VECTORS { - let a = F::from_canonical_u32(x); - let b = F::from_canonical_u32(y); - let got = (PF::broadcast(a) * PF::broadcast(b)).extract(0); - assert_eq!(got, a * b, "packed 32b mul mismatch for {x} * {y}"); - } -} - -#[test] -fn fp32_broadcast_and_extract_roundtrip() { - type F = Prime24Offset3; - type PF = ::Packing; - check_broadcast_roundtrip::(F::from_u64(42)); -} - -#[test] -fn packed_fp64_40b_add_sub_mul() { - type F = Prime40Offset195; - type PF = ::Packing; - check_packed_add_sub_mul::(0xaa40_bb40_cc40_dd40); -} - -#[test] -fn packed_fp64_64b_add_sub_mul() { - type F = Prime64Offset59; - type PF = ::Packing; - check_packed_add_sub_mul::(0xaa64_bb64_cc64_dd64); -} - -#[test] -fn fp64_broadcast_and_extract_roundtrip() { - type F = Prime40Offset195; - type PF = ::Packing; - check_broadcast_roundtrip::(F::from_u64(42)); -} diff --git a/crates/jolt-field/src/parallel.rs b/crates/jolt-field/src/parallel.rs deleted file mode 100644 index c334773ce0..0000000000 --- a/crates/jolt-field/src/parallel.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! Conditional parallelism utilities. -//! -//! When the `parallel` feature is enabled, the `cfg_iter!` family of macros -//! expand to rayon's parallel iterators. Otherwise they fall back to standard -//! sequential iterators. - -#[cfg(feature = "parallel")] -pub use rayon::prelude::*; - -/// Returns `.par_iter()` when `parallel` is enabled, `.iter()` otherwise. -#[macro_export] -macro_rules! cfg_iter { - ($e:expr) => {{ - #[cfg(feature = "parallel")] - let it = $e.par_iter(); - #[cfg(not(feature = "parallel"))] - let it = $e.iter(); - it - }}; -} - -/// Returns `.par_iter_mut()` when `parallel` is enabled, `.iter_mut()` otherwise. -#[macro_export] -macro_rules! cfg_iter_mut { - ($e:expr) => {{ - #[cfg(feature = "parallel")] - let it = $e.par_iter_mut(); - #[cfg(not(feature = "parallel"))] - let it = $e.iter_mut(); - it - }}; -} - -/// Returns `.into_par_iter()` when `parallel` is enabled, `.into_iter()` otherwise. -#[macro_export] -macro_rules! cfg_into_iter { - ($e:expr) => {{ - #[cfg(feature = "parallel")] - let it = $e.into_par_iter(); - #[cfg(not(feature = "parallel"))] - let it = $e.into_iter(); - it - }}; -} - -/// Returns `.par_chunks(n)` when `parallel` is enabled, `.chunks(n)` otherwise. -#[macro_export] -macro_rules! cfg_chunks { - ($e:expr, $n:expr) => {{ - #[cfg(feature = "parallel")] - let it = $e.par_chunks($n); - #[cfg(not(feature = "parallel"))] - let it = $e.chunks($n); - it - }}; -} - -/// Returns `.par_chunks_mut(n)` when `parallel` is enabled, `.chunks_mut(n)` otherwise. -#[macro_export] -macro_rules! cfg_chunks_mut { - ($e:expr, $n:expr) => {{ - #[cfg(feature = "parallel")] - let it = $e.par_chunks_mut($n); - #[cfg(not(feature = "parallel"))] - let it = $e.chunks_mut($n); - it - }}; -} - -/// Runs two closures potentially in parallel via `rayon::join`. -/// -/// Without `parallel`: runs them sequentially and returns the pair. -#[macro_export] -macro_rules! cfg_join { - ($f_a:expr, $f_b:expr) => {{ - #[cfg(feature = "parallel")] - let result = rayon::join($f_a, $f_b); - #[cfg(not(feature = "parallel"))] - let result = ($f_a(), $f_b()); - result - }}; -} - -/// Parallel fold-reduce over a range. -/// -/// With `parallel`: `range.into_par_iter().fold(identity, fold_op).reduce(identity, reduce_op)`. -/// Without: `range.into_iter().fold(identity(), fold_op)`. -#[macro_export] -macro_rules! cfg_fold_reduce { - ($range:expr, $identity:expr, $fold_op:expr, $reduce_op:expr) => {{ - #[cfg(feature = "parallel")] - let result = $range - .into_par_iter() - .fold($identity, $fold_op) - .reduce($identity, $reduce_op); - #[cfg(not(feature = "parallel"))] - let result = $range.into_iter().fold(($identity)(), $fold_op); - result - }}; -} - -pub use crate::{ - cfg_chunks, cfg_chunks_mut, cfg_fold_reduce, cfg_into_iter, cfg_iter, cfg_iter_mut, cfg_join, -}; diff --git a/crates/jolt-field/src/prime/fp128/add_sub.rs b/crates/jolt-field/src/prime/fp128/add_sub.rs deleted file mode 100644 index ac026b290d..0000000000 --- a/crates/jolt-field/src/prime/fp128/add_sub.rs +++ /dev/null @@ -1,409 +0,0 @@ -#![cfg_attr( - any(target_arch = "aarch64", target_arch = "x86_64"), - expect( - clippy::undocumented_unsafe_blocks, - reason = "ported inline-assembly kernels retain their audited flag-flow invariants" - ) -)] - -use super::*; - -impl Fp128

{ - #[inline(always)] - pub(super) fn add_raw(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - #[cfg(target_arch = "aarch64")] - { - // On AArch64 we can keep the reduction predicate in flags via `ccmp`, - // which is materially better than the generic `u128` lowering. - Self::add_raw_aarch64_dispatch(a, b) - } - - #[cfg(target_arch = "x86_64")] - { - // On x86-64, `sbb reg, reg` turns carry1 into a 0/-1 mask without - // leaving flags. After computing `s + C`, one more `adc mask, mask` - // makes ZF encode "need reduction", so the final select stays on - // the flag path via `cmovne`. - Self::add_raw_x86_64_dispatch(a, b) - } - - #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] - { - Self::add_raw_portable(a, b) - } - } - - #[cfg_attr( - any(target_arch = "aarch64", target_arch = "x86_64"), - expect( - dead_code, - reason = "target-specific helper is intentionally unused on some architectures" - ) - )] - #[inline(always)] - fn add_raw_portable(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - // Compute s = a + b as two limbs. - let (s0, carry0) = a[0].overflowing_add(b[0]); - let (s1a, carry1a) = a[1].overflowing_add(b[1]); - let (s1, carry1b) = s1a.overflowing_add(carry0 as u64); - let overflow = carry1a | carry1b; - - // Since p = 2^128 - C and C < 2^64, reducing s modulo p is just - // adding C into the low limb and propagating that carry. - let (r0, carry2) = s0.overflowing_add(Self::C_LO); - let (r1, carry3) = s1.overflowing_add(carry2 as u64); - - pack( - if overflow | carry3 { r0 } else { s0 }, - if overflow | carry3 { r1 } else { s1 }, - ) - } - - #[cfg(target_arch = "aarch64")] - #[inline(always)] - fn add_raw_aarch64_dispatch(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - // The immediate form is best when C < 4096 (the AArch64 add-immediate - // encoding limit). Stable Rust does not let us feed `Self::C_LO` - // directly into an `asm!(..., const ...)` operand, so the known - // built-in offsets are spelled out here and everything else uses the - // register form. - match Self::C_LO { - 275 => Self::add_raw_aarch64_imm::<275>(a, b), - 159 => Self::add_raw_aarch64_imm::<159>(a, b), - 2355 => Self::add_raw_aarch64_imm::<2355>(a, b), - _ => Self::add_raw_aarch64_reg(a, b, Self::C_LO), - } - } - - #[cfg(target_arch = "aarch64")] - #[inline(always)] - fn add_raw_aarch64_imm(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - let out_lo: u64; - let out_hi: u64; - unsafe { - // carry1 is the overflow bit from a + b. - // carry2 is the overflow bit from s + C, equivalently s >= p. - // `ccmp` folds `carry1 | carry2` back into flags so the final - // select stays branchless and never round-trips through GPR logic. - asm!( - "adds {s_lo}, {a_lo}, {b_lo}", - "adcs {s_hi}, {a_hi}, {b_hi}", - "cset {carry1:w}, hs", - "adds {t_lo}, {s_lo}, #{c}", - "adcs {t_hi}, {s_hi}, xzr", - "ccmp {carry1:w}, #0, #0, lo", - "csel {out_lo}, {t_lo}, {s_lo}, ne", - "csel {out_hi}, {t_hi}, {s_hi}, ne", - c = const C, - a_lo = in(reg) a[0], - a_hi = in(reg) a[1], - b_lo = in(reg) b[0], - b_hi = in(reg) b[1], - s_lo = out(reg) _, - s_hi = out(reg) _, - t_lo = out(reg) _, - t_hi = out(reg) _, - carry1 = out(reg) _, - out_lo = lateout(reg) out_lo, - out_hi = lateout(reg) out_hi, - options(pure, nomem, nostack), - ); - } - pack(out_lo, out_hi) - } - - #[cfg(target_arch = "aarch64")] - #[inline(always)] - fn add_raw_aarch64_reg(a: [u64; 2], b: [u64; 2], c: u64) -> [u64; 2] { - let out_lo: u64; - let out_hi: u64; - unsafe { - // Same flag flow as the immediate path above, but with C supplied in - // a register for offsets that are not encodable as add immediates. - asm!( - "adds {s_lo}, {a_lo}, {b_lo}", - "adcs {s_hi}, {a_hi}, {b_hi}", - "cset {carry1:w}, hs", - "adds {t_lo}, {s_lo}, {c}", - "adcs {t_hi}, {s_hi}, xzr", - "ccmp {carry1:w}, #0, #0, lo", - "csel {out_lo}, {t_lo}, {s_lo}, ne", - "csel {out_hi}, {t_hi}, {s_hi}, ne", - c = in(reg) c, - a_lo = in(reg) a[0], - a_hi = in(reg) a[1], - b_lo = in(reg) b[0], - b_hi = in(reg) b[1], - s_lo = out(reg) _, - s_hi = out(reg) _, - t_lo = out(reg) _, - t_hi = out(reg) _, - carry1 = out(reg) _, - out_lo = lateout(reg) out_lo, - out_hi = lateout(reg) out_hi, - options(pure, nomem, nostack), - ); - } - pack(out_lo, out_hi) - } - - #[cfg(target_arch = "x86_64")] - #[inline(always)] - fn add_raw_x86_64_dispatch(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - // As on AArch64, stable Rust does not let us feed `Self::C_LO` - // directly into a const asm operand. The built-in offsets get the - // immediate form and everything else uses the register form. - match Self::C_LO { - 275 => Self::add_raw_x86_64_imm::<275>(a, b), - 159 => Self::add_raw_x86_64_imm::<159>(a, b), - 2355 => Self::add_raw_x86_64_imm::<2355>(a, b), - // For C >= 2^31 the i32 immediate form is unusable: `add r64, - // imm32` sign-extends the immediate, which would silently - // corrupt the high limb. Such offsets fall through to the - // register form below (`Prime128OffsetA7F7` lands here). - _ => Self::add_raw_x86_64_reg(a, b, Self::C_LO), - } - } - - #[cfg(target_arch = "x86_64")] - #[inline(always)] - fn add_raw_x86_64_imm(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - let mut out_lo = a[0]; - let mut out_hi = a[1]; - let _mask: u64; - let _t_lo: u64; - let _t_hi: u64; - unsafe { - // After `s = a + b`, `sbb mask, mask` materializes carry1 as 0/-1. - // After `t = s + C`, `adc mask, mask` leaves ZF=1 iff neither - // carry1 nor carry2 was set. `cmovne` then picks `t` exactly when - // reduction is needed. - asm!( - "add {out_lo}, {b_lo}", - "adc {out_hi}, {b_hi}", - "sbb {mask}, {mask}", - "mov {t_lo}, {out_lo}", - "mov {t_hi}, {out_hi}", - "add {t_lo}, {c}", - "adc {t_hi}, 0", - "adc {mask}, {mask}", - "cmovne {out_lo}, {t_lo}", - "cmovne {out_hi}, {t_hi}", - out_lo = inout(reg) out_lo, - out_hi = inout(reg) out_hi, - b_lo = in(reg) b[0], - b_hi = in(reg) b[1], - mask = out(reg) _mask, - t_lo = out(reg) _t_lo, - t_hi = out(reg) _t_hi, - c = const C, - options(pure, nomem, nostack), - ); - } - pack(out_lo, out_hi) - } - - #[cfg(target_arch = "x86_64")] - #[inline(always)] - fn add_raw_x86_64_reg(a: [u64; 2], b: [u64; 2], c: u64) -> [u64; 2] { - let mut out_lo = a[0]; - let mut out_hi = a[1]; - let _mask: u64; - let _t_lo: u64; - let _t_hi: u64; - unsafe { - asm!( - "add {out_lo}, {b_lo}", - "adc {out_hi}, {b_hi}", - "sbb {mask}, {mask}", - "mov {t_lo}, {out_lo}", - "mov {t_hi}, {out_hi}", - "add {t_lo}, {c}", - "adc {t_hi}, 0", - "adc {mask}, {mask}", - "cmovne {out_lo}, {t_lo}", - "cmovne {out_hi}, {t_hi}", - out_lo = inout(reg) out_lo, - out_hi = inout(reg) out_hi, - b_lo = in(reg) b[0], - b_hi = in(reg) b[1], - c = in(reg) c, - mask = out(reg) _mask, - t_lo = out(reg) _t_lo, - t_hi = out(reg) _t_hi, - options(pure, nomem, nostack), - ); - } - pack(out_lo, out_hi) - } - - #[inline(always)] - pub(super) fn sub_raw(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - #[cfg(target_arch = "aarch64")] - { - // The const path still uses `sub_raw_portable`, but at runtime on - // AArch64 we can keep subtraction in limbs and reduce with `-C` - // instead of materializing `P = 2^128 - C`. - Self::sub_raw_aarch64_dispatch(a, b) - } - - #[cfg(target_arch = "x86_64")] - { - // On x86-64, `sbb reg, reg` turns the final borrow into a 0/-1 mask. - // Masking that with C lets us keep the same "select 0 or C, then do - // one final subtract" structure that worked well on AArch64. - Self::sub_raw_x86_64_dispatch(a, b) - } - - #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] - { - Self::sub_raw_portable(a, b) - } - } - - #[inline(always)] - pub(super) const fn sub_raw_portable(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - let (diff, borrow) = to_u128(a).overflowing_sub(to_u128(b)); - from_u128(if borrow { diff.wrapping_add(P) } else { diff }) - } - - #[cfg(target_arch = "aarch64")] - #[inline(always)] - fn sub_raw_aarch64_dispatch(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - // As in add_raw, stable Rust cannot feed `Self::C_LO` directly into a - // `const` asm operand, so the built-in offsets get immediate forms and - // everything else falls back to the register form. - match Self::C_LO { - 275 => Self::sub_raw_aarch64_imm::<275>(a, b), - 159 => Self::sub_raw_aarch64_imm::<159>(a, b), - 2355 => Self::sub_raw_aarch64_imm::<2355>(a, b), - _ => Self::sub_raw_aarch64_reg(a, b, Self::C_LO), - } - } - - #[cfg(target_arch = "aarch64")] - #[inline(always)] - fn sub_raw_aarch64_imm(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - let out_lo: u64; - let out_hi: u64; - unsafe { - // If `a - b` borrows, then modulo `p = 2^128 - C` we need - // `diff + p = diff - C (mod 2^128)`. Instead of round-tripping the - // borrow bit through a GPR with `cset`/`cmp`, select the subtrahend - // (`0` or `C`) directly from flags and do one final subtract. - asm!( - "mov {c_tmp}, #{c}", - "subs {out_lo}, {a_lo}, {b_lo}", - "sbcs {out_hi}, {a_hi}, {b_hi}", - "csel {c_tmp}, xzr, {c_tmp}, hs", - "subs {out_lo}, {out_lo}, {c_tmp}", - "sbc {out_hi}, {out_hi}, xzr", - c = const C, - a_lo = in(reg) a[0], - a_hi = in(reg) a[1], - b_lo = in(reg) b[0], - b_hi = in(reg) b[1], - c_tmp = out(reg) _, - out_lo = out(reg) out_lo, - out_hi = out(reg) out_hi, - options(pure, nomem, nostack), - ); - } - pack(out_lo, out_hi) - } - - #[cfg(target_arch = "aarch64")] - #[inline(always)] - fn sub_raw_aarch64_reg(a: [u64; 2], b: [u64; 2], c: u64) -> [u64; 2] { - let out_lo: u64; - let out_hi: u64; - unsafe { - asm!( - "subs {out_lo}, {a_lo}, {b_lo}", - "sbcs {out_hi}, {a_hi}, {b_hi}", - "csel {c_tmp}, xzr, {c}, hs", - "subs {out_lo}, {out_lo}, {c_tmp}", - "sbc {out_hi}, {out_hi}, xzr", - c = in(reg) c, - a_lo = in(reg) a[0], - a_hi = in(reg) a[1], - b_lo = in(reg) b[0], - b_hi = in(reg) b[1], - c_tmp = out(reg) _, - out_lo = out(reg) out_lo, - out_hi = out(reg) out_hi, - options(pure, nomem, nostack), - ); - } - pack(out_lo, out_hi) - } - - #[cfg(target_arch = "x86_64")] - #[inline(always)] - fn sub_raw_x86_64_dispatch(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - // The immediate form keeps C out of the input register set for the - // built-in offsets. Stable Rust does not let us pass `Self::C_LO` - // directly as a const asm operand, so the known built-ins are spelled - // out here and everything else uses the register form. - match Self::C_LO { - 275 => Self::sub_raw_x86_64_imm::<275>(a, b), - 159 => Self::sub_raw_x86_64_imm::<159>(a, b), - 2355 => Self::sub_raw_x86_64_imm::<2355>(a, b), - // See the matching note in `add_raw_x86_64_dispatch`: offsets - // with C >= 2^31 cannot use the i32 immediate form because the - // sign-extended `and r64, imm32` would corrupt the mask, so - // they fall through to the register path here. - _ => Self::sub_raw_x86_64_reg(a, b, Self::C_LO), - } - } - - #[cfg(target_arch = "x86_64")] - #[inline(always)] - fn sub_raw_x86_64_imm(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - let mut out_lo = a[0]; - let mut out_hi = a[1]; - unsafe { - asm!( - "sub {out_lo}, {b_lo}", - "sbb {out_hi}, {b_hi}", - "sbb {mask}, {mask}", - "and {mask}, {c}", - "sub {out_lo}, {mask}", - "sbb {out_hi}, 0", - out_lo = inout(reg) out_lo, - out_hi = inout(reg) out_hi, - b_lo = in(reg) b[0], - b_hi = in(reg) b[1], - mask = out(reg) _, - c = const C, - options(pure, nomem, nostack), - ); - } - pack(out_lo, out_hi) - } - - #[cfg(target_arch = "x86_64")] - #[inline(always)] - fn sub_raw_x86_64_reg(a: [u64; 2], b: [u64; 2], c: u64) -> [u64; 2] { - let mut out_lo = a[0]; - let mut out_hi = a[1]; - unsafe { - asm!( - "sub {out_lo}, {b_lo}", - "sbb {out_hi}, {b_hi}", - "sbb {mask}, {mask}", - "and {mask}, {c}", - "sub {out_lo}, {mask}", - "sbb {out_hi}, 0", - out_lo = inout(reg) out_lo, - out_hi = inout(reg) out_hi, - b_lo = in(reg) b[0], - b_hi = in(reg) b[1], - c = in(reg) c, - mask = out(reg) _, - options(pure, nomem, nostack), - ); - } - pack(out_lo, out_hi) - } -} diff --git a/crates/jolt-field/src/prime/fp128/core.rs b/crates/jolt-field/src/prime/fp128/core.rs deleted file mode 100644 index 0913076522..0000000000 --- a/crates/jolt-field/src/prime/fp128/core.rs +++ /dev/null @@ -1,107 +0,0 @@ -use super::*; - -/// 128-bit prime field element for primes of the form `p = 2^128 - c`. -/// -/// Stored as `[u64; 2]` (lo, hi) for 8-byte alignment and direct limb access. -/// -/// The offset `c = 2^128 - p` and all derived constants are computed at -/// compile time from the const-generic `P`. Instantiating `Fp128` with a -/// modulus that is not of this form is a compile-time error. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, Default)] -pub struct Fp128(pub(crate) [u64; 2]); - -impl PartialEq for Fp128

{ - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Eq for Fp128

{} - -impl Fp128

{ - /// Offset `c = 2^128 − p`. Validated at compile time. - pub const C: u128 = { - let c = 0u128.wrapping_sub(P); - assert!(P != 0, "modulus must be nonzero"); - assert!(P & 1 == 1, "modulus must be odd"); - assert!( - c < (1u128 << 32), - "C must be < 2^32 (asm fold-2 uses single mul)" - ); - assert!( - c * (c + 1) < P, - "C(C+1) < P required for fused canonicalize" - ); - c - }; - /// Low 64 bits of `C` (always equals `C` since `C < 2^32`). - pub const C_LO: u64 = Self::C as u64; - - /// Create from a canonical representative in `[0, p)`. - #[inline] - pub fn from_canonical_u128(x: u128) -> Self { - debug_assert!(x < P); - Self(from_u128(x)) - } - - /// Additive identity. - #[inline] - pub fn zero() -> Self { - Self(pack(0, 0)) - } - - /// Multiplicative identity. - #[inline] - pub fn one() -> Self { - Self(pack(1, 0)) - } - - /// Check whether this element is zero. - #[inline] - pub fn is_zero(&self) -> bool { - self.0 == [0, 0] - } - - /// Multiplicative inverse, or `None` for zero. - #[inline] - pub fn inverse(&self) -> Option { - ::inverse(self) - } - - /// Construct from a `u64` reduced modulo the field modulus. - #[inline] - pub fn from_u64(val: u64) -> Self { - Self(from_u128(val as u128)) - } - - /// Construct from an `i64` reduced modulo the field modulus. - #[inline] - pub fn from_i64(val: i64) -> Self { - Self::from_i64_const(val) - } - - /// Construct from an `i8` reduced modulo the field modulus. - #[inline] - pub fn from_i8(val: i8) -> Self { - Self::from_i64(val as i64) - } - - /// Return the canonical representative in `[0, p)`. - #[inline] - pub fn to_canonical_u128(self) -> u128 { - to_u128(self.0) - } - - /// Const-evaluable `from_i64`. Embeds a small signed integer into `Fp`. - pub const fn from_i64_const(val: i64) -> Self { - if val >= 0 { - Self(from_u128(val as u128)) - } else { - Self(Self::sub_raw_portable( - pack(0, 0), - from_u128(val.unsigned_abs() as u128), - )) - } - } -} diff --git a/crates/jolt-field/src/prime/fp128/mod.rs b/crates/jolt-field/src/prime/fp128/mod.rs deleted file mode 100644 index 9c22f72a67..0000000000 --- a/crates/jolt-field/src/prime/fp128/mod.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! 128-bit prime field for primes of the form `p = 2^128 − c` with `c < 2^32`. -//! -//! Uses Solinas-style two-fold reduction: no Montgomery form, ~23 cycles/mul -//! on both AArch64 and x86-64. The offset `c` is computed at compile time -//! from the const-generic modulus `P`. -//! -//! ## Built-in primes -//! -//! Two built-in protocol primes are exposed: -//! -//! - `Prime128OffsetA7F7` (`p = 2^128 − 2^32 + 22537`, `C = 0xFFFFA7F7`), -//! whose multiplicative group has a smooth subgroup of order -//! `2^3 · 3^7 = 17 496` (with a clean radix-3 substructure of order -//! `3^7 = 2187`). This is the default protocol prime. -//! - `Prime128Offset2355` (`p = 2^128 − 2355`), with smooth subgroup -//! `2² · 3 · 5² · 7² = 14 700`, supported as a peer prime. -//! -//! A secondary split-NTT-only prime `Prime128Offset159` -//! (`p = 2^128 − 159`, `p ≡ 33 mod 64`) is kept for the algebra benchmark/test -//! path that only needs 32-way roots of unity. - -mod add_sub; -mod core; -mod mul; -mod primes; -mod reduce; -#[cfg(test)] -mod tests; -mod traits; -mod wide; - -#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] -use ::core::arch::asm; - -use crate::{FieldCore, FromPrimitiveInt}; -use rand_core::RngCore; - -use crate::{CanonicalField, HalvingField, PseudoMersenneField}; - -use super::util::{is_pow2_u64, log2_pow2_u64, mul64_wide}; - -pub use self::core::Fp128; -pub use primes::{Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7}; - -/// Pack two u64 limbs into `[lo, hi]`. -#[inline(always)] -pub(super) const fn pack(lo: u64, hi: u64) -> [u64; 2] { - [lo, hi] -} - -/// Convert `u128` → `[u64; 2]`. -#[inline(always)] -pub(super) const fn from_u128(x: u128) -> [u64; 2] { - [x as u64, (x >> 64) as u64] -} - -/// Convert `[u64; 2]` → `u128`. -#[inline(always)] -pub(super) const fn to_u128(x: [u64; 2]) -> u128 { - x[0] as u128 | (x[1] as u128) << 64 -} diff --git a/crates/jolt-field/src/prime/fp128/mul.rs b/crates/jolt-field/src/prime/fp128/mul.rs deleted file mode 100644 index 88bf5da2e5..0000000000 --- a/crates/jolt-field/src/prime/fp128/mul.rs +++ /dev/null @@ -1,376 +0,0 @@ -#![cfg_attr( - target_arch = "aarch64", - expect( - clippy::undocumented_unsafe_blocks, - reason = "ported inline-assembly kernels retain their audited carry-flow invariants" - ) -)] - -use super::*; - -impl Fp128

{ - #[inline(always)] - pub(super) fn mul_raw(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - #[cfg(target_arch = "aarch64")] - { - Self::mul_raw_aarch64(a, b) - } - - #[cfg(not(target_arch = "aarch64"))] - { - Self::mul_raw_portable(a, b) - } - } - - #[cfg_attr( - target_arch = "aarch64", - expect( - dead_code, - reason = "target-specific helper is intentionally unused on some architectures" - ) - )] - #[inline(always)] - fn mul_raw_portable(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - let [r0, r1, r2, r3] = Self(a).mul_wide(Self(b)); - Self::reduce_4(r0, r1, r2, r3) - } - - #[inline(always)] - fn mul_add_raw(a: [u64; 2], b: [u64; 2], addend: [u64; 2]) -> [u64; 2] { - #[cfg(target_arch = "aarch64")] - { - Self::mul_add_raw_aarch64(a, b, addend) - } - - #[cfg(not(target_arch = "aarch64"))] - { - Self::mul_add_raw_portable(a, b, addend) - } - } - - #[cfg_attr( - target_arch = "aarch64", - expect( - dead_code, - reason = "target-specific helper is intentionally unused on some architectures" - ) - )] - #[inline(always)] - fn mul_add_raw_portable(a: [u64; 2], b: [u64; 2], addend: [u64; 2]) -> [u64; 2] { - let prod = Self(a).mul_wide(Self(b)); - let [s0, s1, s2, s3] = Self::add_128_into_256(prod, addend); - Self::reduce_4(s0, s1, s2, s3) - } - - #[cfg(target_arch = "aarch64")] - #[inline(always)] - fn mul_add_raw_aarch64(a: [u64; 2], b: [u64; 2], addend: [u64; 2]) -> [u64; 2] { - let out_lo: u64; - let out_hi: u64; - unsafe { - asm!( - // Schoolbook 2×2 → 256-bit product [r0,r1,r2,r3] - "mul {p00l}, {a0}, {b0}", - "umulh {p00h}, {a0}, {b0}", - "mul {p01l}, {a0}, {b1}", - "umulh {p01h}, {a0}, {b1}", - "mul {p10l}, {a1}, {b0}", - "umulh {p10h}, {a1}, {b0}", - "mul {p11l}, {a1}, {b1}", - "umulh {p11h}, {a1}, {b1}", - - // Carry accumulation into [r0=p00l, r1=p00h, r2=p01h, r3=p11h] - "adds {p00h}, {p00h}, {p01l}", - "cset {p01l:w}, hs", - "adds {p01h}, {p01h}, {p10h}", - "cset {p10h:w}, hs", - "adds {p01h}, {p01h}, {p11l}", - "cinc {p10h}, {p10h}, hs", - "adds {p00h}, {p00h}, {p10l}", - "adcs {p01h}, {p01h}, {p01l}", - "adc {p11h}, {p11h}, {p10h}", - - // Fuse the addend into the low 128 bits before the Solinas fold. - "adds {p00l}, {p00l}, {add_lo}", - "adcs {p00h}, {p00h}, {add_hi}", - "adcs {p01h}, {p01h}, xzr", - "adc {p11h}, {p11h}, xzr", - - // Fold-1: [t0,t1,t2] = [r0,r1] + C·[r2,r3] - "mul {p01l}, {p01h}, {c}", - "umulh {p10l}, {p01h}, {c}", - "mul {p10h}, {p11h}, {c}", - "umulh {p11l}, {p11h}, {c}", - - "adds {p00l}, {p00l}, {p01l}", - "adcs {p00h}, {p00h}, {p10l}", - "cset {p01h:w}, hs", - "adds {p00h}, {p00h}, {p10h}", - "adc {p11h}, {p11l}, {p01h}", - - // Fold-2 + canonicalize via ccmp - "mul {p01l}, {p11h}, {c}", - "adds {p00l}, {p00l}, {p01l}", - "adcs {p00h}, {p00h}, xzr", - "cset {p01l:w}, hs", - "adds {p10l}, {p00l}, {c}", - "adcs {p10h}, {p00h}, xzr", - "ccmp {p01l:w}, #0, #0, lo", - "csel {out_lo}, {p10l}, {p00l}, ne", - "csel {out_hi}, {p10h}, {p00h}, ne", - - a0 = in(reg) a[0], - a1 = in(reg) a[1], - b0 = in(reg) b[0], - b1 = in(reg) b[1], - add_lo = in(reg) addend[0], - add_hi = in(reg) addend[1], - c = in(reg) Self::C_LO, - p00l = out(reg) _, - p00h = out(reg) _, - p01l = out(reg) _, - p01h = out(reg) _, - p10l = out(reg) _, - p10h = out(reg) _, - p11l = out(reg) _, - p11h = out(reg) _, - out_lo = lateout(reg) out_lo, - out_hi = lateout(reg) out_hi, - options(pure, nomem, nostack), - ); - } - pack(out_lo, out_hi) - } - - /// 35-instruction AArch64 inline-asm multiply with Solinas reduction. - /// - /// Saves 6 instructions vs LLVM's codegen by: - /// - Fold-1 carry chain: direct adds/adcs/adc (5 vs 8 instructions), - /// avoiding intermediate cset/cinc shuttling of carries. - /// - Fold-2 + canonicalize: `ccmp` folds the overflow predicate with - /// the ≥p check (8 vs 10 instructions). - /// - /// Benchmarked at 1.29x throughput improvement on Apple M4. - #[cfg(target_arch = "aarch64")] - #[inline(always)] - fn mul_raw_aarch64(a: [u64; 2], b: [u64; 2]) -> [u64; 2] { - let out_lo: u64; - let out_hi: u64; - unsafe { - asm!( - // Schoolbook 2×2 → 256-bit product [r0,r1,r2,r3] - "mul {p00l}, {a0}, {b0}", - "umulh {p00h}, {a0}, {b0}", - "mul {p01l}, {a0}, {b1}", - "umulh {p01h}, {a0}, {b1}", - "mul {p10l}, {a1}, {b0}", - "umulh {p10h}, {a1}, {b0}", - "mul {p11l}, {a1}, {b1}", - "umulh {p11h}, {a1}, {b1}", - - // Carry accumulation into [r0=p00l, r1=p00h, r2=p01h, r3=p11h] - "adds {p00h}, {p00h}, {p01l}", - "cset {p01l:w}, hs", - "adds {p01h}, {p01h}, {p10h}", - "cset {p10h:w}, hs", - "adds {p01h}, {p01h}, {p11l}", - "cinc {p10h}, {p10h}, hs", - "adds {p00h}, {p00h}, {p10l}", - "adcs {p01h}, {p01h}, {p01l}", - "adc {p11h}, {p11h}, {p10h}", - - // Fold-1: [t0,t1,t2] = [r0,r1] + C·[r2,r3] - "mul {p01l}, {p01h}, {c}", - "umulh {p10l}, {p01h}, {c}", - "mul {p10h}, {p11h}, {c}", - "umulh {p11l}, {p11h}, {c}", - - "adds {p00l}, {p00l}, {p01l}", - "adcs {p00h}, {p00h}, {p10l}", - "cset {p01h:w}, hs", - "adds {p00h}, {p00h}, {p10h}", - "adc {p11h}, {p11l}, {p01h}", - - // Fold-2 + canonicalize via ccmp (C < 2^32 ⇒ C·t2 fits in 64 bits) - "mul {p01l}, {p11h}, {c}", - "adds {p00l}, {p00l}, {p01l}", - "adcs {p00h}, {p00h}, xzr", - "cset {p01l:w}, hs", - "adds {p10l}, {p00l}, {c}", - "adcs {p10h}, {p00h}, xzr", - "ccmp {p01l:w}, #0, #0, lo", - "csel {out_lo}, {p10l}, {p00l}, ne", - "csel {out_hi}, {p10h}, {p00h}, ne", - - a0 = in(reg) a[0], - a1 = in(reg) a[1], - b0 = in(reg) b[0], - b1 = in(reg) b[1], - c = in(reg) Self::C_LO, - p00l = out(reg) _, - p00h = out(reg) _, - p01l = out(reg) _, - p01h = out(reg) _, - p10l = out(reg) _, - p10h = out(reg) _, - p11l = out(reg) _, - p11h = out(reg) _, - out_lo = lateout(reg) out_lo, - out_hi = lateout(reg) out_hi, - options(pure, nomem, nostack), - ); - } - pack(out_lo, out_hi) - } - - #[inline(always)] - fn sqr_wide(self) -> [u64; 4] { - let (a0, a1) = (self.0[0], self.0[1]); - let (p00_lo, p00_hi) = mul64_wide(a0, a0); - let (p01_lo, p01_hi) = mul64_wide(a0, a1); - let (p11_lo, p11_hi) = mul64_wide(a1, a1); - - let row1 = p00_hi as u128 + (p01_lo as u128) * 2; - let r0 = p00_lo; - let r1 = row1 as u64; - let carry1 = (row1 >> 64) as u64; - - let row2 = (p01_hi as u128) * 2 + p11_lo as u128 + carry1 as u128; - let r2 = row2 as u64; - let carry2 = (row2 >> 64) as u64; - - let row3 = p11_hi as u128 + carry2 as u128; - let r3 = row3 as u64; - debug_assert_eq!(row3 >> 64, 0); - - [r0, r1, r2, r3] - } - - #[inline(always)] - fn sqr_raw(a: [u64; 2]) -> [u64; 2] { - #[cfg(target_arch = "aarch64")] - { - Self::sqr_raw_aarch64(a) - } - - #[cfg(not(target_arch = "aarch64"))] - { - Self::sqr_raw_portable(a) - } - } - - #[cfg_attr( - target_arch = "aarch64", - expect( - dead_code, - reason = "target-specific helper is intentionally unused on some architectures" - ) - )] - #[inline(always)] - fn sqr_raw_portable(a: [u64; 2]) -> [u64; 2] { - let [r0, r1, r2, r3] = Self(a).sqr_wide(); - Self::reduce_4(r0, r1, r2, r3) - } - - /// 31-instruction AArch64 inline-asm squaring with Solinas reduction. - /// - /// Uses 3 widening multiplies (vs 4 for general mul) and doubles the - /// cross term via shifted-register operands. Same fold-1 + ccmp - /// canonicalize as `mul_raw_aarch64`. - #[cfg(target_arch = "aarch64")] - #[inline(always)] - fn sqr_raw_aarch64(a: [u64; 2]) -> [u64; 2] { - let out_lo: u64; - let out_hi: u64; - unsafe { - asm!( - // Squaring schoolbook: 3 widening muls - "mul {p00l}, {a0}, {a0}", - "umulh {p00h}, {a0}, {a0}", - "mul {p01l}, {a0}, {a1}", - "umulh {p01h}, {a0}, {a1}", - "mul {p11l}, {a1}, {a1}", - "umulh {p11h}, {a1}, {a1}", - - // Carry accumulation with doubled cross term - // row1 = p00h + 2*p01l, row2 = 2*p01h + p11l, r3 = p11h + carries - "lsr {t0}, {p01l}, #63", - "lsr {t1}, {p01h}, #63", - "adds {p01h}, {p11l}, {p01h}, lsl #1", - "cinc {t1}, {t1}, hs", - "adds {p00h}, {p00h}, {p01l}, lsl #1", - "adcs {p01h}, {p01h}, {t0}", - "adc {p11h}, {p11h}, {t1}", - - // At this point: r0=p00l, r1=p00h, r2=p01h, r3=p11h - - // Fold-1: [t0,t1,t2] = [r0,r1] + C·[r2,r3] - "mul {t0}, {p01h}, {c}", - "umulh {t1}, {p01h}, {c}", - "mul {p01l}, {p11h}, {c}", - "umulh {p11l}, {p11h}, {c}", - - "adds {p00l}, {p00l}, {t0}", - "adcs {p00h}, {p00h}, {t1}", - "cset {t0:w}, hs", - "adds {p00h}, {p00h}, {p01l}", - "adc {p11h}, {p11l}, {t0}", - - // Fold-2 + canonicalize via ccmp (C < 2^32 ⇒ C·t2 fits in 64 bits) - "mul {t0}, {p11h}, {c}", - "adds {p00l}, {p00l}, {t0}", - "adcs {p00h}, {p00h}, xzr", - "cset {t0:w}, hs", - "adds {t1}, {p00l}, {c}", - "adcs {p01l}, {p00h}, xzr", - "ccmp {t0:w}, #0, #0, lo", - "csel {out_lo}, {t1}, {p00l}, ne", - "csel {out_hi}, {p01l}, {p00h}, ne", - - a0 = in(reg) a[0], - a1 = in(reg) a[1], - c = in(reg) Self::C_LO, - p00l = out(reg) _, - p00h = out(reg) _, - p01l = out(reg) _, - p01h = out(reg) _, - p11l = out(reg) _, - p11h = out(reg) _, - t0 = out(reg) _, - t1 = out(reg) _, - out_lo = lateout(reg) out_lo, - out_hi = lateout(reg) out_hi, - options(pure, nomem, nostack), - ); - } - pack(out_lo, out_hi) - } - - /// Squaring, equivalent to `self * self`. - #[inline(always)] - pub fn square(self) -> Self { - Self(Self::sqr_raw(self.0)) - } - - /// Fused multiply-add, equivalent to `self * rhs + addend`. - /// - /// This widens the product, adds the canonical addend before reduction, - /// and performs a single final Solinas reduction. - #[inline(always)] - pub fn mul_add(self, rhs: Self, addend: Self) -> Self { - Self(Self::mul_add_raw(self.0, rhs.0, addend.0)) - } - - pub(super) fn pow_u128(self, mut exp: u128) -> Self { - let mut base = self; - let mut acc = Self::one(); - while exp > 0 { - if (exp & 1) == 1 { - acc *= base; - } - base = Self(Self::sqr_raw(base.0)); - exp >>= 1; - } - acc - } -} diff --git a/crates/jolt-field/src/prime/fp128/primes.rs b/crates/jolt-field/src/prime/fp128/primes.rs deleted file mode 100644 index 9f49d8084c..0000000000 --- a/crates/jolt-field/src/prime/fp128/primes.rs +++ /dev/null @@ -1,30 +0,0 @@ -use super::*; - -/// `p = 2^128 − 275` (C = 275). -pub type Prime128Offset275 = Fp128<0xfffffffffffffffffffffffffffffeed>; -/// `p = 2^128 − 159` (C = 159). Split-NTT-only helper prime. -pub type Prime128Offset159 = Fp128<0xffffffffffffffffffffffffffffff61>; -/// `p = 2^128 − 2355` (C = 2355, p ≡ 5 mod 8). -/// -/// Smooth multiplicative subgroup of order 14700 = 2² × 3 × 5² × 7², -/// supporting mixed-radix FFT up to size 14700 (e.g. 1470 = 2·3·5·7² -/// for RS encoding with 256+1024 ≥ 1280 evaluations). -/// -/// Factorization: `p − 1 = 2² · 3 · 5² · 7² · 701 · 2955365183 · 11173595356596918495491`. -pub type Prime128Offset2355 = Fp128<0xfffffffffffffffffffffffffffff6cd>; - -/// `p = 2^128 − 2^32 + 22537` (C = 2^32 − 22537 = 0xFFFFA7F7). -/// -/// Solinas-form prime sharing the same CPU reduction cost as -/// `Prime128Offset2355` on x86_64 / AArch64 (both go through the generic -/// 32-bit-C `mul_c_wide` path; neither C is of the form `2^a ± 1`). The -/// multiplicative group contains a smooth subgroup of order -/// `2^3 · 3^7 = 17 496` with a pure radix-3 subgroup of order -/// `3^7 = 2187`, enabling a low-mul mixed-radix FFT. -/// -/// Factorization of `p − 1` includes `2^3 · 3^7 · 19 · 41 · 459 647 · …`. -/// -/// Subgroup sizes available for FFT-based RS encoding include -/// `1458 = 2 · 3^6`, `2187 = 3^7`, `4374 = 2 · 3^7`, `8748 = 2^2 · 3^7`, -/// and the full `17 496 = 2^3 · 3^7`. -pub type Prime128OffsetA7F7 = Fp128<0xffffffffffffffffffffffff00005809>; diff --git a/crates/jolt-field/src/prime/fp128/reduce.rs b/crates/jolt-field/src/prime/fp128/reduce.rs deleted file mode 100644 index 242edf9d84..0000000000 --- a/crates/jolt-field/src/prime/fp128/reduce.rs +++ /dev/null @@ -1,195 +0,0 @@ -use super::*; - -impl Fp128

{ - /// +1 means `C = 2^a + 1`, -1 means `C = 2^a - 1`, 0 means generic. - const C_SHIFT_KIND: i8 = { - let c = Self::C_LO; - if c > 1 && is_pow2_u64(c - 1) { - 1 - } else if c == u64::MAX || is_pow2_u64(c + 1) { - -1 - } else { - 0 - } - }; - const C_SHIFT: u32 = { - let c = Self::C_LO; - if Self::C_SHIFT_KIND == 1 { - log2_pow2_u64(c - 1) - } else if Self::C_SHIFT_KIND == -1 { - if c == u64::MAX { - 64 - } else { - log2_pow2_u64(c + 1) - } - } else { - 0 - } - }; - - /// Multiply by `C = 2^128 - P`. For `C = 2^a ± 1`, this is shift/add or - /// shift/sub only; otherwise it falls back to generic widening multiply. - #[inline(always)] - fn mul_c_wide(x: u64) -> (u64, u64) { - if Self::C_SHIFT_KIND == 1 { - let v = ((x as u128) << Self::C_SHIFT) + x as u128; - (v as u64, (v >> 64) as u64) - } else if Self::C_SHIFT_KIND == -1 { - let v = ((x as u128) << Self::C_SHIFT) - x as u128; - (v as u64, (v >> 64) as u64) - } else { - mul64_wide(Self::C_LO, x) - } - } - - /// Fold 2 + canonicalize: reduce `[t0, t1] + t2·2^128` into `[0, p)`. - /// - /// Correctness argument for the fused overflow+canonicalize: - /// - /// Let `v = base + C·t2` (mathematical, not mod 2^128). - /// From the fold-1 mac chain, `t2 ≤ C`, so `C·t2 ≤ C²`. - /// - /// - **No overflow** (`v < 2^128`): `s = v`, and the standard - /// canonicalize applies — `s + C` carries iff `s ≥ P`. - /// - **Overflow** (`v ≥ 2^128`): `s = v − 2^128`, so `s < C·t2 ≤ C²`. - /// The correct reduced value is `s + C` (since `2^128 ≡ C mod P`). - /// Because `s + C < C² + C = C(C+1)` and `C(C+1) < P` for all - /// `C < 2^64`, the value `s + C` is already in `[0, P)` — no - /// further canonicalization is needed, and `s + C < 2^128` so the - /// add does NOT carry. - /// - /// Therefore `if (overflow | carry) { s + C } else { s }` is correct - /// in both cases, fusing the overflow correction with canonicalization. - #[inline(always)] - fn fold2_canonicalize(t0: u64, t1: u64, t2: u64) -> [u64; 2] { - let (ct2_lo, ct2_hi) = Self::mul_c_wide(t2); - - let (s0, carry0) = t0.overflowing_add(ct2_lo); - let (s1a, carry1a) = t1.overflowing_add(ct2_hi); - let (s1, carry1b) = s1a.overflowing_add(carry0 as u64); - let overflow = carry1a | carry1b; - - let (r0, carry2) = s0.overflowing_add(Self::C_LO); - let (r1, carry3) = s1.overflowing_add(carry2 as u64); - - pack( - if overflow | carry3 { r0 } else { s0 }, - if overflow | carry3 { r1 } else { s1 }, - ) - } - - /// Solinas fold for exactly 4 limbs: `[r0,r1] + C·[r2,r3]` → 3 limbs, - /// then `fold2_canonicalize`. - #[inline(always)] - pub(super) fn reduce_4(r0: u64, r1: u64, r2: u64, r3: u64) -> [u64; 2] { - let (cr2_lo, cr2_hi) = Self::mul_c_wide(r2); - let (cr3_lo, cr3_hi) = Self::mul_c_wide(r3); - - let t0_sum = r0 as u128 + cr2_lo as u128; - let t0 = t0_sum as u64; - let carryf = (t0_sum >> 64) as u64; - - let t1_sum = r1 as u128 + cr2_hi as u128 + cr3_lo as u128 + carryf as u128; - let t1 = t1_sum as u64; - - let t2_sum = cr3_hi as u128 + (t1_sum >> 64); - let t2 = t2_sum as u64; - debug_assert_eq!(t2_sum >> 64, 0); - - Self::fold2_canonicalize(t0, t1, t2) - } - - /// Add a canonical 128-bit value into a 256-bit little-endian limb array. - /// - /// Since both multiplicands and addends are canonical field elements, - /// `a * b + c < 2^256`, so the top carry is guaranteed to be zero. - #[inline(always)] - pub(super) fn add_128_into_256(prod: [u64; 4], addend: [u64; 2]) -> [u64; 4] { - let (s0, carry0) = prod[0].overflowing_add(addend[0]); - let (s1a, carry1a) = prod[1].overflowing_add(addend[1]); - let (s1, carry1b) = s1a.overflowing_add(carry0 as u64); - let carry1 = carry1a | carry1b; - let (s2, carry2) = prod[2].overflowing_add(carry1 as u64); - let (s3, carry3) = prod[3].overflowing_add(carry2 as u64); - debug_assert!(!carry3); - [s0, s1, s2, s3] - } - - /// Reduce an arbitrary-width little-endian limb array to a canonical - /// field element via iterated Solinas folding. - /// - /// Each fold splits at the 128-bit boundary and replaces - /// `hi · 2^128` with `hi · C`, reducing width by one limb per - /// iteration. Supports 0–10 input limbs (up to 640 bits). - /// - /// # Panics - /// - /// Panics if `limbs.len() > 10`. - #[inline(always)] - pub fn solinas_reduce(limbs: &[u64]) -> Self { - match limbs.len() { - 0 => Self::zero(), - 1 => Self(pack(limbs[0], 0)), - 2 => Self::from_canonical_u128_reduced(to_u128([limbs[0], limbs[1]])), - 3 => Self(Self::fold2_canonicalize(limbs[0], limbs[1], limbs[2])), - 4 => Self(Self::reduce_4(limbs[0], limbs[1], limbs[2], limbs[3])), - 5 => { - let (l0, l1, l2, l3, l4) = (limbs[0], limbs[1], limbs[2], limbs[3], limbs[4]); - let (c2_lo, c2_hi) = Self::mul_c_wide(l2); - let (c3_lo, c3_hi) = Self::mul_c_wide(l3); - let (c4_lo, c4_hi) = Self::mul_c_wide(l4); - - let s0 = l0 as u128 + c2_lo as u128; - let s1 = l1 as u128 + c2_hi as u128 + c3_lo as u128 + (s0 >> 64); - let s2 = c3_hi as u128 + c4_lo as u128 + (s1 >> 64); - let s3 = c4_hi as u128 + (s2 >> 64); - debug_assert_eq!(s3 >> 64, 0); - - Self(Self::reduce_4(s0 as u64, s1 as u64, s2 as u64, s3 as u64)) - } - n => { - assert!(n <= 10, "solinas_reduce supports at most 10 limbs"); - let mut buf = [0u64; 11]; - buf[..n].copy_from_slice(limbs); - let mut len = n; - let c = Self::C_LO; - - while len > 5 { - let high_len = len - 2; - let mut next = [0u64; 11]; - - let mut carry: u64 = 0; - for i in 0..high_len { - let wide = c as u128 * buf[i + 2] as u128 + carry as u128; - next[i] = wide as u64; - carry = (wide >> 64) as u64; - } - next[high_len] = carry; - - let s0 = next[0] as u128 + buf[0] as u128; - next[0] = s0 as u64; - let s1 = next[1] as u128 + buf[1] as u128 + (s0 >> 64); - next[1] = s1 as u64; - let mut c_out = (s1 >> 64) as u64; - for limb in &mut next[2..=high_len] { - if c_out == 0 { - break; - } - let s = *limb as u128 + c_out as u128; - *limb = s as u64; - c_out = (s >> 64) as u64; - } - debug_assert_eq!(c_out, 0); - - buf = next; - len -= 1; - while len > 5 && buf[len - 1] == 0 { - len -= 1; - } - } - - Self::solinas_reduce(&buf[..len]) - } - } - } -} diff --git a/crates/jolt-field/src/prime/fp128/tests.rs b/crates/jolt-field/src/prime/fp128/tests.rs deleted file mode 100644 index 8ddeb5dce0..0000000000 --- a/crates/jolt-field/src/prime/fp128/tests.rs +++ /dev/null @@ -1,194 +0,0 @@ -use super::*; -use crate::{FieldCore, PseudoMersenneField}; -use rand::rngs::StdRng; -use rand::SeedableRng; -use rand_core::RngCore; - -type F = Prime128Offset275; - -#[test] -fn to_limbs_roundtrip() { - let mut rng = StdRng::seed_from_u64(0xdead_beef_cafe_1234); - for _ in 0..1000 { - let a: F = FieldCore::random(&mut rng); - assert_eq!(Fp128(a.to_limbs()), a); - } -} - -#[test] -fn mul_wide_u64_matches_full_mul() { - let mut rng = StdRng::seed_from_u64(0x1122_3344_5566_7788); - for _ in 0..1000 { - let a: F = FieldCore::random(&mut rng); - let b = rng.next_u64(); - let expected = a * F::from_u64(b); - let reduced = F::solinas_reduce(&a.mul_wide_u64(b)); - assert_eq!(reduced, expected); - } -} - -#[test] -fn mul_wide_matches_full_mul() { - let mut rng = StdRng::seed_from_u64(0xaabb_ccdd_eeff_0011); - for _ in 0..1000 { - let a: F = FieldCore::random(&mut rng); - let b: F = FieldCore::random(&mut rng); - let expected = a * b; - let reduced = F::solinas_reduce(&a.mul_wide(b)); - assert_eq!(reduced, expected); - } -} - -#[test] -fn mul_add_matches_mul_then_add() { - let mut rng = StdRng::seed_from_u64(0x3141_5926_5358_9793); - for _ in 0..1000 { - let a: F = FieldCore::random(&mut rng); - let b: F = FieldCore::random(&mut rng); - let c: F = FieldCore::random(&mut rng); - assert_eq!(a.mul_add(b, c), a * b + c); - } - - let near = -F::one(); - assert_eq!(near.mul_add(near, near), near * near + near); -} - -#[test] -fn mul_wide_u128_matches_full_mul() { - let mut rng = StdRng::seed_from_u64(0x9988_7766_5544_3322); - for _ in 0..1000 { - let a: F = FieldCore::random(&mut rng); - let b = rng.next_u64() as u128 | ((rng.next_u64() as u128) << 64); - let expected = a * F::from_canonical_u128_reduced(b); - let reduced = F::solinas_reduce(&a.mul_wide_u128(b)); - assert_eq!(reduced, expected); - } -} - -#[test] -fn mul_wide_limbs_roundtrips_through_reduction() { - let mut rng = StdRng::seed_from_u64(0x1bad_f00d_0ddc_afe1); - for _ in 0..1000 { - let a: F = FieldCore::random(&mut rng); - let b3 = [rng.next_u64(), rng.next_u64(), rng.next_u64()]; - let b4 = [ - rng.next_u64(), - rng.next_u64(), - rng.next_u64(), - rng.next_u64(), - ]; - - let got3_full = a.mul_wide_limbs::<3, 5>(b3); - let got3_trunc = a.mul_wide_limbs::<3, 4>(b3); - assert_eq!( - got3_trunc, - [got3_full[0], got3_full[1], got3_full[2], got3_full[3]] - ); - let exp3 = a * F::solinas_reduce(&b3); - assert_eq!(F::solinas_reduce(&got3_full), exp3); - - let got4_full = a.mul_wide_limbs::<4, 6>(b4); - let got4_trunc = a.mul_wide_limbs::<4, 4>(b4); - assert_eq!( - got4_trunc, - [got4_full[0], got4_full[1], got4_full[2], got4_full[3]] - ); - let exp4 = a * F::solinas_reduce(&b4); - assert_eq!(F::solinas_reduce(&got4_full), exp4); - } -} - -#[test] -fn solinas_reduce_small_inputs() { - assert_eq!(F::solinas_reduce(&[]), F::zero()); - assert_eq!(F::solinas_reduce(&[42]), F::from_u64(42)); - let one_shifted = F::from_canonical_u128_reduced(1u128 << 64); - assert_eq!(F::solinas_reduce(&[0, 1]), one_shifted); -} - -#[test] -fn solinas_reduce_4_limbs_max() { - // 2^256 - 1 ≡ C² - 1 (mod P), since 2^128 ≡ C - let c = F::from_canonical_u128_reduced(::MODULUS_OFFSET); - let expected = c * c - F::one(); - assert_eq!(F::solinas_reduce(&[u64::MAX; 4]), expected); -} - -#[test] -fn solinas_reduce_9_limbs() { - // 1 + 2^512 = 1 + (2^128)^4 ≡ 1 + C^4 - let c = F::from_canonical_u128_reduced(::MODULUS_OFFSET); - let expected = F::one() + c * c * c * c; - assert_eq!(F::solinas_reduce(&[1, 0, 0, 0, 0, 0, 0, 0, 1]), expected); -} - -#[test] -fn solinas_reduce_accumulated_products() { - let mut rng = StdRng::seed_from_u64(0xfeed_face_0bad_c0de); - let mut acc = [0u64; 5]; - let mut expected = F::zero(); - - for _ in 0..200 { - let a: F = FieldCore::random(&mut rng); - let b = rng.next_u64(); - let wide = a.mul_wide_u64(b); - - let mut carry: u64 = 0; - for j in 0..5 { - let addend = if j < 3 { wide[j] } else { 0 }; - let sum = acc[j] as u128 + addend as u128 + carry as u128; - acc[j] = sum as u64; - carry = (sum >> 64) as u64; - } - assert_eq!(carry, 0); - expected += a * F::from_u64(b); - } - - assert_eq!(F::solinas_reduce(&acc), expected); -} - -#[test] -fn solinas_reduce_cross_prime() { - type G = Prime128Offset275; - let c = G::from_canonical_u128_reduced(::MODULUS_OFFSET); - let expected = c * c - G::one(); - assert_eq!(G::solinas_reduce(&[u64::MAX; 4]), expected); -} - -#[test] -fn from_i64_handles_min_without_overflow() { - let x = F::from_i64(i64::MIN); - let y = F::from_u64(i64::MIN.unsigned_abs()); - assert_eq!(x + y, F::zero()); -} - -#[test] -fn prime128_offset_a7f7_constants() { - // p = 2^128 − 2^32 + 22537, so C = 2^32 − 22537 = 0xFFFFA7F7. - assert_eq!( - ::MODULUS_OFFSET, - 0xFFFFA7F7, - ); - assert_eq!(Prime128OffsetA7F7::C, 0xFFFFA7F7); - assert_eq!(Prime128OffsetA7F7::C_LO, 0xFFFFA7F7); - // Round-trip through the field arithmetic: p ≡ 0 (mod p), so - // Fp(2^128 − C) + Fp(C) = 0. - let neg_c = -Prime128OffsetA7F7::from_canonical_u128_reduced(0xFFFFA7F7); - assert_eq!( - neg_c + Prime128OffsetA7F7::from_canonical_u128_reduced(0xFFFFA7F7), - Prime128OffsetA7F7::zero() - ); -} - -#[test] -fn prime128_offset_a7f7_mul_wide_matches_full_mul() { - type G = Prime128OffsetA7F7; - let mut rng = StdRng::seed_from_u64(0xa7f7_a7f7_a7f7_a7f7); - for _ in 0..1000 { - let a: G = FieldCore::random(&mut rng); - let b: G = FieldCore::random(&mut rng); - let expected = a * b; - let reduced = G::solinas_reduce(&a.mul_wide(b)); - assert_eq!(reduced, expected); - } -} diff --git a/crates/jolt-field/src/prime/fp128/traits.rs b/crates/jolt-field/src/prime/fp128/traits.rs deleted file mode 100644 index 054eb43f8e..0000000000 --- a/crates/jolt-field/src/prime/fp128/traits.rs +++ /dev/null @@ -1,136 +0,0 @@ -use super::*; - -use crate::native_algebra::{impl_native_ring_algebra, impl_prime_ops}; -use crate::prime::native_capability::impl_prime_native_capability; -use crate::RingCore; - -impl_prime_ops!(Fp128, zero_raw: pack(0, 0)); - -impl FieldCore for Fp128

{ - #[inline(always)] - fn inverse(&self) -> Option { - let inv = self.inv_or_zero(); - if self.is_zero() { - None - } else { - Some(inv) - } - } - - #[inline(always)] - fn inv_or_zero(self) -> Self { - let candidate = self.pow_u128(P.wrapping_sub(2)); - let v = to_u128(self.0); - let nz = ((v | v.wrapping_neg()) >> 127) & 1; - let mask = 0u128.wrapping_sub(nz); - let masked = to_u128(candidate.0) & mask; - Self(from_u128(masked)) - } - - #[inline(always)] - fn random(rng: &mut R) -> Self { - loop { - let lo = rng.next_u64(); - let hi = rng.next_u64(); - let x = lo as u128 | (hi as u128) << 64; - if x < P { - return Self(pack(lo, hi)); - } - } - } -} - -impl HalvingField for Fp128

{ - #[inline] - fn half(self) -> Self { - let x = to_u128(self.0); - let half = (x >> 1) + (x & 1) * ((P >> 1) + 1); - Self(from_u128(half)) - } -} - -impl FromPrimitiveInt for Fp128

{ - #[inline(always)] - fn from_u64(val: u64) -> Self { - // For Fp128 pseudo-Mersenne primes, p = 2^128 - c with c < 2^64. - // Therefore any u64 is always canonical (< p), so this can be a - // direct limb construction with no reduction path. - Self::from_u64(val) - } - - #[inline(always)] - fn from_i64(val: i64) -> Self { - Self::from_i64(val) - } - - #[inline(always)] - fn from_u128(val: u128) -> Self { - Self::from_canonical_u128_reduced(val) - } - - #[inline(always)] - fn from_i128(val: i128) -> Self { - if val >= 0 { - Self::from_u128(val as u128) - } else { - -Self::from_u128(val.unsigned_abs()) - } - } -} - -impl CanonicalField for Fp128

{ - fn to_canonical_u128(self) -> u128 { - to_u128(self.0) - } - - fn modulus_bits() -> u32 { - u128::BITS - P.leading_zeros() - } - - fn from_canonical_u128_checked(val: u128) -> Option { - if val < P { - Some(Self(from_u128(val))) - } else { - None - } - } - - fn from_canonical_u128_reduced(val: u128) -> Self { - let (sub, borrow) = val.overflowing_sub(P); - Self(from_u128(if borrow { val } else { sub })) - } -} - -impl PseudoMersenneField for Fp128

{ - const MODULUS_BITS: u32 = 128; - const MODULUS_OFFSET: u128 = Self::C; -} - -impl_native_ring_algebra!( - impl[const P: u128] Fp128

{ - zero: Self::default(), - is_zero(x): x.to_canonical_u128() == 0, - one: if P > 1 { Self::from_canonical_u128(1) } else { Self::default() }, - display(x, f): write!(f, "{}", x.to_canonical_u128()), - hash(x, state): ::std::hash::Hash::hash(&x.to_canonical_u128(), state), - } -); - -impl RingCore for Fp128

{} - -impl_prime_native_capability!(Fp128, 16); - -impl serde::Serialize for Fp128

{ - fn serialize(&self, serializer: S) -> Result { - let buf = self.to_canonical_u128().to_le_bytes(); - <[u8; 16]>::serialize(&buf, serializer) - } -} - -impl<'de, const P: u128> serde::Deserialize<'de> for Fp128

{ - fn deserialize>(deserializer: D) -> Result { - let buf = <[u8; 16]>::deserialize(deserializer)?; - Self::from_canonical_u128_checked(u128::from_le_bytes(buf)) - .ok_or_else(|| serde::de::Error::custom("non-canonical Fp128 encoding")) - } -} diff --git a/crates/jolt-field/src/prime/fp128/wide.rs b/crates/jolt-field/src/prime/fp128/wide.rs deleted file mode 100644 index a797f4d93b..0000000000 --- a/crates/jolt-field/src/prime/fp128/wide.rs +++ /dev/null @@ -1,302 +0,0 @@ -use super::*; - -impl Fp128

{ - /// Extract the canonical `[lo, hi]` limb representation. - #[inline(always)] - pub fn to_limbs(self) -> [u64; 2] { - self.0 - } - - /// 128×64 → 192-bit widening multiply, **no reduction**. - /// - /// Returns `[lo, mid, hi]` representing `self · other` as a 192-bit - /// integer. Cost: 2 widening `mul64`. - #[inline(always)] - pub fn mul_wide_u64(self, other: u64) -> [u64; 3] { - let (a0, a1) = (self.0[0], self.0[1]); - let (p0_lo, p0_hi) = mul64_wide(a0, other); - let (p1_lo, p1_hi) = mul64_wide(a1, other); - let mid = p0_hi as u128 + p1_lo as u128; - let hi = p1_hi + (mid >> 64) as u64; - [p0_lo, mid as u64, hi] - } - - /// 128×128 → 256-bit widening multiply, **no reduction**. - /// - /// Returns `[r0, r1, r2, r3]` representing `self · other` as a 256-bit - /// integer. This is the schoolbook 2×2 portion of the Solinas multiply, - /// without the reduction fold. Cost: 4 widening `mul64`. - #[inline(always)] - pub fn mul_wide(self, other: Self) -> [u64; 4] { - let (a0, a1) = (self.0[0], self.0[1]); - let (b0, b1) = (other.0[0], other.0[1]); - let (p00_lo, p00_hi) = mul64_wide(a0, b0); - let (p01_lo, p01_hi) = mul64_wide(a0, b1); - let (p10_lo, p10_hi) = mul64_wide(a1, b0); - let (p11_lo, p11_hi) = mul64_wide(a1, b1); - - let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; - let r0 = p00_lo; - let r1 = row1 as u64; - let carry1 = (row1 >> 64) as u64; - - let row2 = p01_hi as u128 + p10_hi as u128 + p11_lo as u128 + carry1 as u128; - let r2 = row2 as u64; - let carry2 = (row2 >> 64) as u64; - - let row3 = p11_hi as u128 + carry2 as u128; - let r3 = row3 as u64; - debug_assert_eq!(row3 >> 64, 0); - - [r0, r1, r2, r3] - } - - /// 128×128 → 256-bit widening multiply with a raw `u128` operand, - /// **no reduction**. - #[inline(always)] - pub fn mul_wide_u128(self, other: u128) -> [u64; 4] { - self.mul_wide(Self(from_u128(other))) - } - - /// 128×(64*M) → (64*OUT) widening multiply, **no reduction**. - /// - /// Multiplies a canonical Fp128 value (`[u64; 2]`) by an arbitrary - /// little-endian limb array and returns the little-endian product - /// truncated/extended to `OUT` limbs. - #[inline(always)] - pub fn mul_wide_limbs(self, other: [u64; M]) -> [u64; OUT] { - let (a0, a1) = (self.0[0], self.0[1]); - - // Hot-path specializations used by Jolt (M in {3,4}, OUT in {4,5}). - // These avoid loop/control-flow overhead in tight sumcheck FMAs. - if M == 3 && OUT == 5 { - let b0 = other[0]; - let b1 = other[1]; - let b2 = other[2]; - - let (p00_lo, p00_hi) = mul64_wide(a0, b0); - let (p01_lo, p01_hi) = mul64_wide(a0, b1); - let (p02_lo, p02_hi) = mul64_wide(a0, b2); - let (p10_lo, p10_hi) = mul64_wide(a1, b0); - let (p11_lo, p11_hi) = mul64_wide(a1, b1); - let (p12_lo, p12_hi) = mul64_wide(a1, b2); - - let r0 = p00_lo; - - let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; - let r1 = row1 as u64; - let carry1 = row1 >> 64; - - let row2 = p01_hi as u128 + p02_lo as u128 + p10_hi as u128 + p11_lo as u128 + carry1; - let r2 = row2 as u64; - let carry2 = row2 >> 64; - - let row3 = p02_hi as u128 + p11_hi as u128 + p12_lo as u128 + carry2; - let r3 = row3 as u64; - let carry3 = row3 >> 64; - - let row4 = p12_hi as u128 + carry3; - let r4 = row4 as u64; - debug_assert_eq!(row4 >> 64, 0); - - let mut out = [0u64; OUT]; - out[0] = r0; - out[1] = r1; - out[2] = r2; - out[3] = r3; - out[4] = r4; - return out; - } - if M == 3 && OUT == 4 { - let b0 = other[0]; - let b1 = other[1]; - let b2 = other[2]; - - let (p00_lo, p00_hi) = mul64_wide(a0, b0); - let (p01_lo, p01_hi) = mul64_wide(a0, b1); - let (p02_lo, p02_hi) = mul64_wide(a0, b2); - let (p10_lo, p10_hi) = mul64_wide(a1, b0); - let (p11_lo, p11_hi) = mul64_wide(a1, b1); - let p12_lo = a1.wrapping_mul(b2); - - let r0 = p00_lo; - - let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; - let r1 = row1 as u64; - let carry1 = row1 >> 64; - - let row2 = p01_hi as u128 + p02_lo as u128 + p10_hi as u128 + p11_lo as u128 + carry1; - let r2 = row2 as u64; - let carry2 = row2 >> 64; - - let row3 = p02_hi as u128 + p11_hi as u128 + p12_lo as u128 + carry2; - let r3 = row3 as u64; - - let mut out = [0u64; OUT]; - out[0] = r0; - out[1] = r1; - out[2] = r2; - out[3] = r3; - return out; - } - if M == 4 && OUT == 6 { - let b0 = other[0]; - let b1 = other[1]; - let b2 = other[2]; - let b3 = other[3]; - - let (p00_lo, p00_hi) = mul64_wide(a0, b0); - let (p01_lo, p01_hi) = mul64_wide(a0, b1); - let (p02_lo, p02_hi) = mul64_wide(a0, b2); - let (p03_lo, p03_hi) = mul64_wide(a0, b3); - let (p10_lo, p10_hi) = mul64_wide(a1, b0); - let (p11_lo, p11_hi) = mul64_wide(a1, b1); - let (p12_lo, p12_hi) = mul64_wide(a1, b2); - let (p13_lo, p13_hi) = mul64_wide(a1, b3); - - let r0 = p00_lo; - - let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; - let r1 = row1 as u64; - let carry1 = row1 >> 64; - - let row2 = p01_hi as u128 + p02_lo as u128 + p10_hi as u128 + p11_lo as u128 + carry1; - let r2 = row2 as u64; - let carry2 = row2 >> 64; - - let row3 = p02_hi as u128 + p03_lo as u128 + p11_hi as u128 + p12_lo as u128 + carry2; - let r3 = row3 as u64; - let carry3 = row3 >> 64; - - let row4 = p03_hi as u128 + p12_hi as u128 + p13_lo as u128 + carry3; - let r4 = row4 as u64; - let carry4 = row4 >> 64; - - let row5 = p13_hi as u128 + carry4; - let r5 = row5 as u64; - debug_assert_eq!(row5 >> 64, 0); - - let mut out = [0u64; OUT]; - out[0] = r0; - out[1] = r1; - out[2] = r2; - out[3] = r3; - out[4] = r4; - out[5] = r5; - return out; - } - if M == 4 && OUT == 5 { - let b0 = other[0]; - let b1 = other[1]; - let b2 = other[2]; - let b3 = other[3]; - - let (p00_lo, p00_hi) = mul64_wide(a0, b0); - let (p01_lo, p01_hi) = mul64_wide(a0, b1); - let (p02_lo, p02_hi) = mul64_wide(a0, b2); - let (p03_lo, p03_hi) = mul64_wide(a0, b3); - let (p10_lo, p10_hi) = mul64_wide(a1, b0); - let (p11_lo, p11_hi) = mul64_wide(a1, b1); - let (p12_lo, p12_hi) = mul64_wide(a1, b2); - let p13_lo = a1.wrapping_mul(b3); - - let r0 = p00_lo; - - let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; - let r1 = row1 as u64; - let carry1 = row1 >> 64; - - let row2 = p01_hi as u128 + p02_lo as u128 + p10_hi as u128 + p11_lo as u128 + carry1; - let r2 = row2 as u64; - let carry2 = row2 >> 64; - - let row3 = p02_hi as u128 + p03_lo as u128 + p11_hi as u128 + p12_lo as u128 + carry2; - let r3 = row3 as u64; - let carry3 = row3 >> 64; - - let row4 = p03_hi as u128 + p12_hi as u128 + p13_lo as u128 + carry3; - let r4 = row4 as u64; - - let mut out = [0u64; OUT]; - out[0] = r0; - out[1] = r1; - out[2] = r2; - out[3] = r3; - out[4] = r4; - return out; - } - if M == 4 && OUT == 4 { - let b0 = other[0]; - let b1 = other[1]; - let b2 = other[2]; - let b3 = other[3]; - - let (p00_lo, p00_hi) = mul64_wide(a0, b0); - let (p01_lo, p01_hi) = mul64_wide(a0, b1); - let (p02_lo, p02_hi) = mul64_wide(a0, b2); - let p03_lo = a0.wrapping_mul(b3); - let (p10_lo, p10_hi) = mul64_wide(a1, b0); - let (p11_lo, p11_hi) = mul64_wide(a1, b1); - let p12_lo = a1.wrapping_mul(b2); - - let r0 = p00_lo; - - let row1 = p00_hi as u128 + p01_lo as u128 + p10_lo as u128; - let r1 = row1 as u64; - let carry1 = row1 >> 64; - - let row2 = p01_hi as u128 + p02_lo as u128 + p10_hi as u128 + p11_lo as u128 + carry1; - let r2 = row2 as u64; - let carry2 = row2 >> 64; - - let row3 = p02_hi as u128 + p03_lo as u128 + p11_hi as u128 + p12_lo as u128 + carry2; - let r3 = row3 as u64; - - let mut out = [0u64; OUT]; - out[0] = r0; - out[1] = r1; - out[2] = r2; - out[3] = r3; - return out; - } - - let mut out = [0u64; OUT]; - - for (i, &b) in other.iter().enumerate() { - if i >= OUT { - break; - } - - let (p0_lo, p0_hi) = mul64_wide(a0, b); - let (p1_lo, p1_hi) = mul64_wide(a1, b); - - let s0 = out[i] as u128 + p0_lo as u128; - out[i] = s0 as u64; - let mut carry = s0 >> 64; - - if i + 1 >= OUT { - continue; - } - let s1 = out[i + 1] as u128 + p0_hi as u128 + p1_lo as u128 + carry; - out[i + 1] = s1 as u64; - carry = s1 >> 64; - - if i + 2 >= OUT { - continue; - } - let s2 = out[i + 2] as u128 + p1_hi as u128 + carry; - out[i + 2] = s2 as u64; - - let mut carry_hi = s2 >> 64; - let mut j = i + 3; - while carry_hi != 0 && j < OUT { - let sj = out[j] as u128 + carry_hi; - out[j] = sj as u64; - carry_hi = sj >> 64; - j += 1; - } - } - - out - } -} diff --git a/crates/jolt-field/src/prime/fp32.rs b/crates/jolt-field/src/prime/fp32.rs deleted file mode 100644 index 73e4f6c426..0000000000 --- a/crates/jolt-field/src/prime/fp32.rs +++ /dev/null @@ -1,581 +0,0 @@ -//! Prime field for primes of the form `p = 2^k − c` with `c` small, backed -//! by `u32` storage. -//! -//! Uses Solinas-style two-fold reduction: the offset `c` and fold point `k` -//! are computed at compile time from the const-generic modulus `P`. - -use crate::native_algebra::{impl_native_ring_algebra, impl_prime_ops}; -use crate::prime::native_capability::impl_prime_native_capability; -use crate::{FieldCore, FromPrimitiveInt, RingCore}; -use rand_core::RngCore; - -use crate::{CanonicalField, HalvingField, PseudoMersenneField}; - -/// Prime field element for primes `p = 2^k − c` stored as `u32`. -/// -/// The fold point `k` and offset `c = 2^k − p` are computed at compile time -/// from the const-generic `P`. Instantiating with a modulus that does not -/// satisfy the prime Solinas conditions is a compile-time error. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct Fp32(pub(crate) u32); - -impl Fp32

{ - /// Fold point: smallest `k` such that `P ≤ 2^k`. - const BITS: u32 = 32 - P.leading_zeros(); - - /// Offset `c = 2^k − P`. - pub const C: u32 = { - let c = if Self::BITS == 32 { - 0u32.wrapping_sub(P) - } else { - (1u32 << Self::BITS) - P - }; - assert!(P != 0, "modulus must be nonzero"); - assert!(P & 1 == 1, "modulus must be odd"); - assert!(Self::is_prime_modulus(P), "modulus must be prime"); - assert!( - (c as u64) * (c as u64 + 1) < P as u64, - "C(C+1) < P required for fused canonicalize" - ); - c - }; - - const fn is_prime_modulus(n: u32) -> bool { - if n < 2 { - return false; - } - if n.is_multiple_of(2) { - return n == 2; - } - let mut d = 3u32; - while (d as u64) * (d as u64) <= n as u64 { - if n.is_multiple_of(d) { - return false; - } - d += 2; - } - true - } - - /// Mask for extracting the low `BITS` bits from a u64. - const MASK: u64 = if Self::BITS == 32 { - u32::MAX as u64 - } else { - (1u64 << Self::BITS) - 1 - }; - - pub(crate) const SHIFT64_MOD_P: u32 = { - let c = Self::C as u128; - let bits = Self::BITS; - let mask = if bits == 32 { - u32::MAX as u128 - } else { - (1u128 << bits) - 1 - }; - let mut v = 1u128 << 64; - while v >> bits != 0 { - v = (v & mask) + c * (v >> bits); - } - let reduced = (v as u64).wrapping_sub(P as u64); - let borrow = reduced >> 63; - reduced.wrapping_add(borrow.wrapping_neg() & (P as u64)) as u32 - }; - - #[inline(always)] - fn canonicalize_folded(v: u64) -> u32 { - if Self::BITS <= 31 { - let x = v as u32; - x.min(x.wrapping_sub(P)) - } else { - let reduced = v.wrapping_sub(P as u64); - let borrow = reduced >> 63; - reduced.wrapping_add(borrow.wrapping_neg() & (P as u64)) as u32 - } - } - - /// Create from a canonical representative in `[0, P)`. - #[inline] - pub fn from_canonical_u32(x: u32) -> Self { - debug_assert!(x < P); - Self(x) - } - - /// Additive identity. - #[inline] - pub fn zero() -> Self { - Self(0) - } - - /// Multiplicative identity. - #[inline] - pub fn one() -> Self { - Self(u32::from(P > 1)) - } - - /// Check whether this element is zero. - #[inline] - pub fn is_zero(&self) -> bool { - self.0 == 0 - } - - /// Multiplicative inverse, or `None` for zero. - #[inline] - pub fn inverse(&self) -> Option { - ::inverse(self) - } - - /// Construct from a `u64` reduced modulo the field modulus. - #[inline] - pub fn from_u64(val: u64) -> Self { - Self(Self::reduce_u64(val)) - } - - /// Construct from an `i64` reduced modulo the field modulus. - #[inline] - pub fn from_i64(val: i64) -> Self { - if val >= 0 { - Self::from_u64(val as u64) - } else { - -Self::from_u64(val.unsigned_abs()) - } - } - - /// Construct from an `i8` reduced modulo the field modulus. - #[inline] - pub fn from_i8(val: i8) -> Self { - Self::from_i64(val as i64) - } - - /// Return the canonical representative in `[0, P)`. - #[inline] - pub fn to_canonical_u32(self) -> u32 { - self.0 - } - - /// Solinas reduction: fold a u64 at bit `BITS` until the value fits, - /// then conditionally subtract `P`. - /// - /// For multiplication products (< 2^{2·BITS}) exactly 2 folds suffice; - /// for arbitrary u64 inputs (e.g. `from_u64`) the loop runs at most - /// `ceil(64 / BITS)` iterations. - #[inline(always)] - fn reduce_u64(x: u64) -> u32 { - let c = Self::C as u64; - let mut v = x; - while v >> Self::BITS != 0 { - v = (v & Self::MASK) + c * (v >> Self::BITS); - } - Self::canonicalize_folded(v) - } - - /// Reduce a `u128` to canonical form (for `from_canonical_u128_reduced`). - #[inline(always)] - fn reduce_u128(x: u128) -> u32 { - let c = Self::C as u128; - let bits = Self::BITS; - let mask = if bits == 32 { - u32::MAX as u128 - } else { - (1u128 << bits) - 1 - }; - let mut v = x; - while v >> bits != 0 { - v = (v & mask) + c * (v >> bits); - } - Self::canonicalize_folded(v as u64) - } - - /// Two-fold Solinas reduction for multiplication products. - /// - /// Input must be < 2^{2·BITS} (guaranteed for `a*b` where `a,b < P`). - /// Exactly 2 folds + conditional subtract, no loop. - #[inline(always)] - fn reduce_product(x: u64) -> u32 { - let c = Self::C as u64; - let f1 = (x & Self::MASK) + c * (x >> Self::BITS); - let f2 = (f1 & Self::MASK) + c * (f1 >> Self::BITS); - Self::canonicalize_folded(f2) - } - - #[inline(always)] - fn add_raw(a: u32, b: u32) -> u32 { - if Self::BITS <= 31 { - let sum = a.wrapping_add(b); - sum.min(sum.wrapping_sub(P)) - } else { - let s = (a as u64) + (b as u64); - let reduced = s.wrapping_sub(P as u64); - let borrow = reduced >> 63; - reduced.wrapping_add(borrow.wrapping_neg() & (P as u64)) as u32 - } - } - - #[inline(always)] - fn sub_raw(a: u32, b: u32) -> u32 { - if Self::BITS <= 31 { - let diff = a.wrapping_sub(b); - diff.min(diff.wrapping_add(P)) - } else { - let diff = (a as u64).wrapping_sub(b as u64); - let borrow = diff >> 63; - diff.wrapping_add(borrow.wrapping_neg() & (P as u64)) as u32 - } - } - - #[inline(always)] - fn mul_raw(a: u32, b: u32) -> u32 { - Self::reduce_product((a as u64) * (b as u64)) - } - - #[inline(always)] - fn sqr_raw(a: u32) -> u32 { - Self::mul_raw(a, a) - } - - /// Squaring, equivalent to `self * self`. - #[inline(always)] - pub fn square(self) -> Self { - Self(Self::sqr_raw(self.0)) - } - - fn pow(self, mut exp: u64) -> Self { - let mut base = self; - let mut acc = Self::one(); - while exp > 0 { - if (exp & 1) == 1 { - acc *= base; - } - base = base.square(); - exp >>= 1; - } - acc - } - - /// Extract the canonical value. - #[inline(always)] - pub fn to_limbs(self) -> u32 { - self.0 - } - - /// 32×32 → 64-bit widening multiply, **no reduction**. - #[inline(always)] - pub fn mul_wide(self, other: Self) -> u64 { - (self.0 as u64) * (other.0 as u64) - } - - /// 32×32 → 64-bit widening multiply with a raw `u32` operand, - /// **no reduction**. - #[inline(always)] - pub fn mul_wide_u32(self, other: u32) -> u64 { - (self.0 as u64) * (other as u64) - } - - /// Reduce a u64 value via Solinas folding to a canonical field element. - #[inline(always)] - pub fn solinas_reduce(x: u64) -> Self { - Self(Self::reduce_u64(x)) - } -} - -impl_prime_ops!(Fp32, zero_raw: 0); - -impl FieldCore for Fp32

{ - #[inline(always)] - fn inverse(&self) -> Option { - let inv = self.inv_or_zero(); - if self.is_zero() { - None - } else { - Some(inv) - } - } - - #[inline(always)] - fn inv_or_zero(self) -> Self { - let candidate = self.pow((P as u64).wrapping_sub(2)); - let nz = ((self.0 | self.0.wrapping_neg()) >> 31) & 1; - let mask = 0u32.wrapping_sub(nz); - Self(candidate.0 & mask) - } - - #[inline(always)] - fn random(rng: &mut R) -> Self { - Self(Self::reduce_u64(rng.next_u64())) - } -} - -impl HalvingField for Fp32

{ - #[inline] - fn half(self) -> Self { - if Self::BITS == 31 && Self::C == 1 { - Self((self.0 >> 1) | ((self.0 & 1) << 30)) - } else { - let half_p_plus_one = (P >> 1) + 1; - let correction = 0u32.wrapping_sub(self.0 & 1) & half_p_plus_one; - Self((self.0 >> 1) + correction) - } - } -} - -impl FromPrimitiveInt for Fp32

{ - #[inline(always)] - fn from_u64(val: u64) -> Self { - Self::from_u64(val) - } - - #[inline(always)] - fn from_i64(val: i64) -> Self { - Self::from_i64(val) - } - - #[inline(always)] - fn from_u128(val: u128) -> Self { - Self(Self::reduce_u128(val)) - } - - #[inline(always)] - fn from_i128(val: i128) -> Self { - if val >= 0 { - Self::from_u128(val as u128) - } else { - -Self::from_u128(val.unsigned_abs()) - } - } -} - -impl CanonicalField for Fp32

{ - fn to_canonical_u128(self) -> u128 { - self.0 as u128 - } - - fn modulus_bits() -> u32 { - Self::BITS - } - - fn from_canonical_u128_checked(val: u128) -> Option { - if val < P as u128 { - Some(Self(val as u32)) - } else { - None - } - } - - fn from_canonical_u128_reduced(val: u128) -> Self { - Self(Self::reduce_u128(val)) - } -} - -impl PseudoMersenneField for Fp32

{ - const MODULUS_BITS: u32 = Self::BITS; - const MODULUS_OFFSET: u128 = Self::C as u128; -} - -impl_native_ring_algebra!( - impl[const P: u32] Fp32

{ - zero: Self::default(), - is_zero(x): x.to_canonical_u128() == 0, - one: if P > 1 { Self::from_canonical_u32(1) } else { Self::default() }, - display(x, f): write!(f, "{}", x.to_canonical_u128()), - hash(x, state): ::std::hash::Hash::hash(&x.to_canonical_u128(), state), - } -); - -impl RingCore for Fp32

{} - -impl_prime_native_capability!(Fp32, 4); - -impl serde::Serialize for Fp32

{ - fn serialize(&self, serializer: S) -> Result { - let buf = (self.to_canonical_u128() as u32).to_le_bytes(); - <[u8; 4]>::serialize(&buf, serializer) - } -} - -impl<'de, const P: u32> serde::Deserialize<'de> for Fp32

{ - fn deserialize>(deserializer: D) -> Result { - let buf = <[u8; 4]>::deserialize(deserializer)?; - Self::from_canonical_u128_checked(u32::from_le_bytes(buf) as u128) - .ok_or_else(|| serde::de::Error::custom("non-canonical Fp32 encoding")) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::rngs::StdRng; - use rand::SeedableRng; - - type F = Fp32<251>; // 2^8 - 5 - - #[test] - fn solinas_constants() { - assert_eq!(F::BITS, 8); - assert_eq!(F::C, 5); - assert_eq!(F::MASK, 255); - - type G = Fp32<{ (1u32 << 24) - 3 }>; // 2^24 - 3 - assert_eq!(G::BITS, 24); - assert_eq!(G::C, 3); - } - - #[test] - fn basic_arithmetic() { - let a = F::from_u64(100); - let b = F::from_u64(200); - assert_eq!((a + b).to_canonical_u32(), (100 + 200) % 251); - assert_eq!((a * b).to_canonical_u32(), (100 * 200) % 251); - assert_eq!((b - a).to_canonical_u32(), 100); - assert_eq!((-a).to_canonical_u32(), 251 - 100); - } - - #[test] - fn prime31_fast_path_edges() { - const P31: u32 = (1u32 << 31) - 19; - type G = Fp32; - - assert_eq!(G::BITS, 31); - assert_eq!(G::C, 19); - - let zero = G::zero(); - let one = G::one(); - let p_minus_one = G::from_canonical_u32(P31 - 1); - let p_minus_two = G::from_canonical_u32(P31 - 2); - - assert_eq!((p_minus_one + one).to_canonical_u32(), 0); - assert_eq!((p_minus_one + p_minus_one).to_canonical_u32(), P31 - 2); - assert_eq!((zero - one).to_canonical_u32(), P31 - 1); - assert_eq!((one - p_minus_one).to_canonical_u32(), 2); - assert_eq!((-zero).to_canonical_u32(), 0); - assert_eq!((-one).to_canonical_u32(), P31 - 1); - assert_eq!((p_minus_one * p_minus_one).to_canonical_u32(), 1); - assert_eq!((p_minus_two * p_minus_two).to_canonical_u32(), 4); - - for x in [zero, one, p_minus_two, p_minus_one] { - assert_eq!(x.half() + x.half(), x); - } - - type M = Fp32<{ (1u32 << 31) - 1 }>; - for x in [ - M::zero(), - M::one(), - M::from_canonical_u32((1u32 << 31) - 3), - M::from_canonical_u32((1u32 << 31) - 2), - ] { - assert_eq!(x.half() + x.half(), x); - } - } - - #[test] - fn prime31_random_arithmetic_matches_u64_modulus() { - const P31: u32 = (1u32 << 31) - 19; - type G = Fp32; - - let mut rng = StdRng::seed_from_u64(0x31_31_31_31); - for _ in 0..1000 { - let a_raw = rng.next_u32() & ((1u32 << 31) - 1); - let b_raw = rng.next_u32() & ((1u32 << 31) - 1); - let a = G::from_u64(a_raw as u64); - let b = G::from_u64(b_raw as u64); - let p = P31 as u64; - let a_can = (a_raw as u64) % p; - let b_can = (b_raw as u64) % p; - - assert_eq!((a + b).to_canonical_u32() as u64, (a_can + b_can) % p); - assert_eq!((a - b).to_canonical_u32() as u64, (a_can + p - b_can) % p); - assert_eq!((a * b).to_canonical_u32() as u64, (a_can * b_can) % p); - } - - assert_eq!( - G::from_u64(u64::MAX).to_canonical_u32() as u64, - u64::MAX % (P31 as u64) - ); - } - - #[test] - fn fp31_u128_reduction_matches_modulus() { - fn check(inputs: &[u128]) { - for &input in inputs { - assert_eq!( - Fp32::

::from_canonical_u128_reduced(input).to_canonical_u32() as u128, - input % (P as u128), - "u128 reduction mismatch for P={P}, input={input}" - ); - } - } - - const PRIME31: u32 = (1u32 << 31) - 19; - const MERSENNE31: u32 = (1u32 << 31) - 1; - const GENERIC30: u32 = (1u32 << 30) - 16_397; - const GENERIC31: u32 = (1u32 << 31) - 32_787; - let inputs = [ - 0, - 1, - PRIME31 as u128 - 1, - PRIME31 as u128, - PRIME31 as u128 + 1, - (PRIME31 as u128) * (PRIME31 as u128) - 1, - 1u128 << 63, - (1u128 << 96) + 123_456_789, - u128::MAX, - ]; - - check::(&inputs); - check::(&inputs); - check::(&inputs); - check::(&inputs); - } - - #[test] - fn mul_wide_matches_full_mul() { - let mut rng = StdRng::seed_from_u64(0x1234_5678); - for _ in 0..1000 { - let a: F = FieldCore::random(&mut rng); - let b: F = FieldCore::random(&mut rng); - let expected = a * b; - let reduced = F::solinas_reduce(a.mul_wide(b)); - assert_eq!(reduced, expected); - } - } - - #[test] - fn mul_wide_u32_matches() { - let mut rng = StdRng::seed_from_u64(0xabcd_ef01); - for _ in 0..1000 { - let a: F = FieldCore::random(&mut rng); - let b = rng.next_u32() % 251; - let expected = a * F::from_canonical_u32(b); - let reduced = F::solinas_reduce(a.mul_wide_u32(b)); - assert_eq!(reduced, expected); - } - } - - #[test] - fn reduce_large_values() { - assert_eq!( - F::from_u64(u64::MAX).to_canonical_u32(), - (u64::MAX % 251) as u32 - ); - assert_eq!(F::from_u64(0).to_canonical_u32(), 0); - assert_eq!(F::from_u64(251).to_canonical_u32(), 0); - assert_eq!(F::from_u64(252).to_canonical_u32(), 1); - } - - #[test] - fn pseudo_mersenne_trait() { - assert_eq!(::MODULUS_BITS, 8); - assert_eq!(::MODULUS_OFFSET, 5); - } - - #[test] - fn cross_prime_32bit() { - type G = Fp32<{ u32::MAX - 98 }>; // 2^32 - 99 - assert_eq!(G::BITS, 32); - assert_eq!(G::C, 99); - - let a = G::from_u64(1_000_000); - let b = G::from_u64(2_000_000); - let product = (1_000_000u64 * 2_000_000u64) % ((1u64 << 32) - 99); - assert_eq!((a * b).to_canonical_u32(), product as u32); - } -} diff --git a/crates/jolt-field/src/prime/fp64.rs b/crates/jolt-field/src/prime/fp64.rs deleted file mode 100644 index 1c57d5ef1b..0000000000 --- a/crates/jolt-field/src/prime/fp64.rs +++ /dev/null @@ -1,643 +0,0 @@ -//! Prime field for primes of the form `p = 2^k − c` with `c` small, backed -//! by `u64` storage. -//! -//! Uses Solinas-style two-fold reduction. For `c = 2^a ± 1` the fold -//! multiply is replaced by shift+add/sub, saving a u128 widening multiply. - -use crate::native_algebra::{impl_native_ring_algebra, impl_prime_ops}; -use crate::prime::native_capability::impl_prime_native_capability; -use crate::{FieldCore, FromPrimitiveInt, RingCore}; -use rand_core::RngCore; - -use crate::{CanonicalField, HalvingField, PseudoMersenneField}; - -use super::util::{is_pow2_u64, log2_pow2_u64, mul64_wide}; - -/// Prime field element for primes `p = 2^k − c` stored as `u64`. -/// -/// The fold point `k` and offset `c = 2^k − p` are computed at compile time -/// from the const-generic `P`. For `c = 2^a ± 1`, the fold multiply is -/// replaced by shift+add/sub. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct Fp64(pub(crate) u64); - -impl Fp64

{ - /// Fold point: smallest `k` such that `P ≤ 2^k`. - const BITS: u32 = 64 - P.leading_zeros(); - - /// Offset `c = 2^k − P`. - pub const C: u64 = { - let c = if Self::BITS == 64 { - 0u64.wrapping_sub(P) - } else { - (1u64 << Self::BITS) - P - }; - assert!(P != 0, "modulus must be nonzero"); - assert!(P & 1 == 1, "modulus must be odd"); - assert!( - (c as u128) * (c as u128 + 1) < P as u128, - "C(C+1) < P required for fused canonicalize" - ); - c - }; - - /// +1 means `C = 2^a + 1`, -1 means `C = 2^a - 1`, 0 means generic. - const C_SHIFT_KIND: i8 = { - let c = Self::C; - if c > 1 && is_pow2_u64(c - 1) { - 1 - } else if c == u64::MAX || is_pow2_u64(c + 1) { - -1 - } else { - 0 - } - }; - - const C_SHIFT: u32 = { - let c = Self::C; - if Self::C_SHIFT_KIND == 1 { - log2_pow2_u64(c - 1) - } else if Self::C_SHIFT_KIND == -1 { - if c == u64::MAX { - 64 - } else { - log2_pow2_u64(c + 1) - } - } else { - 0 - } - }; - - /// Mask for extracting the low `BITS` bits from a u128. - const MASK: u128 = if Self::BITS == 64 { - u64::MAX as u128 - } else { - (1u128 << Self::BITS) - 1 - }; - - /// u64-width mask (only valid when BITS < 64). - const MASK64: u64 = if Self::BITS < 64 { - (1u64 << Self::BITS) - 1 - } else { - u64::MAX - }; - - /// Whether Solinas folding of a multiplication product can stay - /// entirely in u64. True when BITS < 64 and C·2^BITS < 2^64. - const FOLD_IN_U64: bool = Self::BITS < 64 && (Self::C as u128) < (1u128 << (64 - Self::BITS)); - - /// u64 multiply by C, split into u32-wide halves so LLVM emits - /// `umull` (32×32→64) instead of promoting to u128. - /// Only valid when C fits in u32 (always true: C < sqrt(P) < 2^32). - #[inline(always)] - fn mul_c_narrow(x: u64) -> u64 { - #[cfg(target_arch = "x86_64")] - { - // x86_64 has fast scalar 64-bit multiply; use one multiply instead - // of two widened 32-bit multiplies in the fold hot path. - Self::C.wrapping_mul(x) - } - #[cfg(not(target_arch = "x86_64"))] - { - let c = Self::C as u32; - let x_lo = x as u32; - let x_hi = (x >> 32) as u32; - (c as u64 * x_lo as u64).wrapping_add((c as u64 * x_hi as u64) << 32) - } - } - - /// Multiply `x` by `C`. For `C = 2^a ± 1` uses shift+add/sub. - #[inline(always)] - fn mul_c(x: u64) -> u128 { - if Self::C_SHIFT_KIND == 1 { - ((x as u128) << Self::C_SHIFT) + x as u128 - } else if Self::C_SHIFT_KIND == -1 { - ((x as u128) << Self::C_SHIFT) - x as u128 - } else { - (Self::C as u128) * (x as u128) - } - } - - /// Create from a canonical representative in `[0, P)`. - #[inline] - pub fn from_canonical_u64(x: u64) -> Self { - debug_assert!(x < P); - Self(x) - } - - /// Additive identity. - #[inline] - pub fn zero() -> Self { - Self(0) - } - - /// Multiplicative identity. - #[inline] - pub fn one() -> Self { - Self(u64::from(P > 1)) - } - - /// Check whether this element is zero. - #[inline] - pub fn is_zero(&self) -> bool { - self.0 == 0 - } - - /// Multiplicative inverse, or `None` for zero. - #[inline] - pub fn inverse(&self) -> Option { - ::inverse(self) - } - - /// Construct from a `u64` reduced modulo the field modulus. - #[inline] - pub fn from_u64(val: u64) -> Self { - Self(Self::reduce_u128(val as u128)) - } - - /// Construct from an `i64` reduced modulo the field modulus. - #[inline] - pub fn from_i64(val: i64) -> Self { - if val >= 0 { - Self::from_u64(val as u64) - } else { - -Self::from_u64(val.unsigned_abs()) - } - } - - /// Construct from an `i8` reduced modulo the field modulus. - #[inline] - pub fn from_i8(val: i8) -> Self { - Self::from_i64(val as i64) - } - - /// Return the canonical representative in `[0, P)`. - #[inline] - pub fn to_canonical_u64(self) -> u64 { - self.0 - } - - /// Solinas reduction: fold a u128 at bit `BITS` until the value fits, - /// then conditionally subtract `P`. - /// - /// For multiplication products (< 2^{2·BITS}) exactly 2 folds suffice; - /// for arbitrary u128 inputs the loop runs at most `ceil(128 / BITS)` - /// iterations. - #[inline(always)] - fn reduce_u128(x: u128) -> u64 { - let mut v = x; - while v >> Self::BITS != 0 { - // The fold's high part `v >> BITS` can exceed 64 bits for - // sub-word primes (BITS < 64), so the multiply by `C` must stay - // in u128. It cannot overflow: `v >> BITS < 2^(128 - BITS)` and - // `C < 2^(BITS - 1)`, so the product is below `2^127`. - v = (v & Self::MASK) + (v >> Self::BITS) * (Self::C as u128); - } - let reduced = v.wrapping_sub(P as u128); - let borrow = reduced >> 127; - reduced.wrapping_add(borrow.wrapping_neg() & (P as u128)) as u64 - } - - /// Two-fold Solinas reduction for multiplication products. - /// - /// Input must be < 2^{2·BITS} (guaranteed for `a*b` where `a,b < P`). - /// Exactly 2 folds + conditional subtract, no loop. - /// - /// When `FOLD_IN_U64` is true the entire reduction stays in u64, - /// avoiding expensive u128 mask/shift on sub-word primes. - #[inline(always)] - fn reduce_product(x: u128) -> u64 { - if Self::FOLD_IN_U64 { - let lo = x as u64; - let hi = (x >> 64) as u64; - let high = (lo >> Self::BITS) | (hi << (64 - Self::BITS)); - let f1 = (lo & Self::MASK64) + Self::mul_c_narrow(high); - let f2 = (f1 & Self::MASK64) + Self::mul_c_narrow(f1 >> Self::BITS); - let reduced = f2.wrapping_sub(P); - let borrow = reduced >> 63; - reduced.wrapping_add(borrow.wrapping_neg() & P) - } else { - let f1 = (x & Self::MASK) + Self::mul_c((x >> Self::BITS) as u64); - let f2 = (f1 & Self::MASK) + Self::mul_c((f1 >> Self::BITS) as u64); - let reduced = f2.wrapping_sub(P as u128); - let borrow = reduced >> 127; - reduced.wrapping_add(borrow.wrapping_neg() & (P as u128)) as u64 - } - } - - /// BMI2 fast path: avoid re-materializing `u128` product in the common - /// sub-word configuration where reduction stays in `u64`. - #[cfg(all(target_arch = "x86_64", target_feature = "bmi2"))] - #[inline(always)] - fn reduce_product_wide(lo: u64, hi: u64) -> u64 { - if Self::FOLD_IN_U64 { - let high = (lo >> Self::BITS) | (hi << (64 - Self::BITS)); - let f1 = (lo & Self::MASK64) + Self::mul_c_narrow(high); - let f2 = (f1 & Self::MASK64) + Self::mul_c_narrow(f1 >> Self::BITS); - let reduced = f2.wrapping_sub(P); - let borrow = reduced >> 63; - reduced.wrapping_add(borrow.wrapping_neg() & P) - } else { - Self::reduce_product(lo as u128 | ((hi as u128) << 64)) - } - } - - #[inline(always)] - fn add_raw(a: u64, b: u64) -> u64 { - if Self::BITS == 64 { - let (s, overflow) = a.overflowing_add(b); - let folded = s.wrapping_add((overflow as u64).wrapping_neg() & Self::C); - let reduced = folded.wrapping_sub(P); - let borrow = (folded < P) as u64; - reduced.wrapping_add(borrow.wrapping_neg() & P) - } else if Self::BITS <= 62 { - let s = a + b; - let reduced = s.wrapping_sub(P); - let borrow = reduced >> 63; - reduced.wrapping_add(borrow.wrapping_neg() & P) - } else { - let s = (a as u128) + (b as u128); - let reduced = s.wrapping_sub(P as u128); - let borrow = reduced >> 127; - reduced.wrapping_add(borrow.wrapping_neg() & (P as u128)) as u64 - } - } - - #[inline(always)] - fn sub_raw(a: u64, b: u64) -> u64 { - if Self::BITS == 64 { - let (diff, underflow) = a.overflowing_sub(b); - diff.wrapping_sub((underflow as u64).wrapping_neg() & Self::C) - } else if Self::BITS <= 62 { - let diff = a.wrapping_sub(b); - let borrow = diff >> 63; - diff.wrapping_add(borrow.wrapping_neg() & P) - } else { - let diff = (a as u128).wrapping_sub(b as u128); - let borrow = diff >> 127; - diff.wrapping_add(borrow.wrapping_neg() & (P as u128)) as u64 - } - } - - #[inline(always)] - fn mul_raw(a: u64, b: u64) -> u64 { - #[cfg(all(target_arch = "x86_64", target_feature = "bmi2"))] - { - let (lo, hi) = mul64_wide(a, b); - Self::reduce_product_wide(lo, hi) - } - #[cfg(not(all(target_arch = "x86_64", target_feature = "bmi2")))] - { - Self::reduce_product((a as u128) * (b as u128)) - } - } - - #[inline(always)] - fn sqr_raw(a: u64) -> u64 { - Self::mul_raw(a, a) - } - - /// Squaring, equivalent to `self * self`. - #[inline(always)] - pub fn square(self) -> Self { - Self(Self::sqr_raw(self.0)) - } - - fn pow(self, mut exp: u64) -> Self { - let mut base = self; - let mut acc = Self::one(); - while exp > 0 { - if (exp & 1) == 1 { - acc *= base; - } - base = base.square(); - exp >>= 1; - } - acc - } - - /// Extract the canonical value. - #[inline(always)] - pub fn to_limbs(self) -> u64 { - self.0 - } - - /// 64×64 → 128-bit widening multiply, **no reduction**. - #[inline(always)] - pub fn mul_wide(self, other: Self) -> u128 { - let (lo, hi) = mul64_wide(self.0, other.0); - lo as u128 | ((hi as u128) << 64) - } - - /// 64×64 → 128-bit widening multiply with a raw `u64` operand, - /// **no reduction**. - #[inline(always)] - pub fn mul_wide_u64(self, other: u64) -> u128 { - let (lo, hi) = mul64_wide(self.0, other); - lo as u128 | ((hi as u128) << 64) - } - - /// Reduce a u128 value via Solinas folding to a canonical field element. - #[inline(always)] - pub fn solinas_reduce(x: u128) -> Self { - Self(Self::reduce_u128(x)) - } - - /// Reduce the integer sum `w0 + w1` of two products of canonical residues - /// (each `a·b` with `a, b < P`, so each `< 2^{2·BITS}`) to a canonical - /// field element. - /// - /// Used by the specialized `FpExt2` EOR fold, which forms each output - /// coordinate as a sum of two base-field products before a single - /// reduction. - /// - /// - Sub-word primes (`BITS < 64`): each product is `< 2^{2·BITS} ≤ 2^126`, - /// so the sum is `< 2^127` and never overflows `u128`; reduce directly. - /// - Full-word primes (`BITS == 64`, `P = 2^64 − C` with `C < 2^32`): the - /// sum can reach `< 2^129`. Split it into 64-bit limbs and fold the high - /// parts with `2^64 ≡ C` and `2^128 ≡ C² (mod P)`. The folded value is - /// `< 2^97`, which `solinas_reduce` finishes. The result is congruent to - /// `w0 + w1 (mod P)`, hence byte-identical to the canonical reduction. - #[inline(always)] - pub(crate) fn reduce_sum_of_two_products(w0: u128, w1: u128) -> Self { - if Self::BITS < 64 { - Self::solinas_reduce(w0.wrapping_add(w1)) - } else { - let (s, carry) = w0.overflowing_add(w1); - let cc = Self::C as u128; - let folded = - (s as u64 as u128) + ((s >> 64) as u64 as u128) * cc + (carry as u128) * cc * cc; - Self::solinas_reduce(folded) - } - } -} - -impl_prime_ops!(Fp64, zero_raw: 0); - -impl FieldCore for Fp64

{ - #[inline(always)] - fn inverse(&self) -> Option { - let inv = self.inv_or_zero(); - if self.is_zero() { - None - } else { - Some(inv) - } - } - - #[inline(always)] - fn inv_or_zero(self) -> Self { - let candidate = self.pow(P.wrapping_sub(2)); - let nz = ((self.0 | self.0.wrapping_neg()) >> 63) & 1; - let mask = 0u64.wrapping_sub(nz); - Self(candidate.0 & mask) - } - - #[inline(always)] - fn random(rng: &mut R) -> Self { - let lo = rng.next_u64() as u128; - let hi = rng.next_u64() as u128; - Self(Self::reduce_u128(lo | (hi << 64))) - } -} - -impl HalvingField for Fp64

{ - #[inline] - fn half(self) -> Self { - let x = self.0 as u128; - Self(((x + (x & 1) * P as u128) >> 1) as u64) - } -} - -impl FromPrimitiveInt for Fp64

{ - #[inline(always)] - fn from_u64(val: u64) -> Self { - Self::from_u64(val) - } - - #[inline(always)] - fn from_i64(val: i64) -> Self { - Self::from_i64(val) - } - - #[inline(always)] - fn from_u128(val: u128) -> Self { - Self(Self::reduce_u128(val)) - } - - #[inline(always)] - fn from_i128(val: i128) -> Self { - if val >= 0 { - Self::from_u128(val as u128) - } else { - -Self::from_u128(val.unsigned_abs()) - } - } -} - -impl CanonicalField for Fp64

{ - fn to_canonical_u128(self) -> u128 { - self.0 as u128 - } - - fn modulus_bits() -> u32 { - Self::BITS - } - - fn from_canonical_u128_checked(val: u128) -> Option { - if val < P as u128 { - Some(Self(val as u64)) - } else { - None - } - } - - fn from_canonical_u128_reduced(val: u128) -> Self { - Self(Self::reduce_u128(val)) - } -} - -impl PseudoMersenneField for Fp64

{ - const MODULUS_BITS: u32 = Self::BITS; - const MODULUS_OFFSET: u128 = Self::C as u128; -} - -impl_native_ring_algebra!( - impl[const P: u64] Fp64

{ - zero: Self::default(), - is_zero(x): x.to_canonical_u128() == 0, - one: if P > 1 { Self::from_canonical_u64(1) } else { Self::default() }, - display(x, f): write!(f, "{}", x.to_canonical_u128()), - hash(x, state): ::std::hash::Hash::hash(&x.to_canonical_u128(), state), - } -); - -impl RingCore for Fp64

{} - -impl_prime_native_capability!(Fp64, 8); - -impl serde::Serialize for Fp64

{ - fn serialize(&self, serializer: S) -> Result { - let buf = (self.to_canonical_u128() as u64).to_le_bytes(); - <[u8; 8]>::serialize(&buf, serializer) - } -} - -impl<'de, const P: u64> serde::Deserialize<'de> for Fp64

{ - fn deserialize>(deserializer: D) -> Result { - let buf = <[u8; 8]>::deserialize(deserializer)?; - Self::from_canonical_u128_checked(u64::from_le_bytes(buf) as u128) - .ok_or_else(|| serde::de::Error::custom("non-canonical Fp64 encoding")) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::rngs::StdRng; - use rand::SeedableRng; - - type F40 = Fp64<{ (1u64 << 40) - 195 }>; // 2^40 - 195 - type F64 = Fp64<{ u64::MAX - 58 }>; // 2^64 - 59 - - #[test] - fn solinas_constants() { - assert_eq!(F40::BITS, 40); - assert_eq!(F40::C, 195); - - assert_eq!(F64::BITS, 64); - assert_eq!(F64::C, 59); - } - - #[test] - fn basic_arithmetic_sub_word() { - let a = F40::from_u64(1_000_000); - let b = F40::from_u64(2_000_000); - let p = (1u64 << 40) - 195; - assert_eq!((a + b).to_canonical_u64(), 3_000_000); - assert_eq!( - (a * b).to_canonical_u64(), - (1_000_000u128 * 2_000_000u128 % p as u128) as u64 - ); - } - - #[test] - fn basic_arithmetic_full_word() { - let a = F64::from_u64(1_000_000_000); - let b = F64::from_u64(2_000_000_000); - let p = u64::MAX - 58; - assert_eq!( - (a * b).to_canonical_u64(), - (1_000_000_000u128 * 2_000_000_000u128 % p as u128) as u64 - ); - } - - #[test] - fn mul_wide_matches_full_mul() { - let mut rng = StdRng::seed_from_u64(0xdead_beef); - for _ in 0..1000 { - let a: F40 = FieldCore::random(&mut rng); - let b: F40 = FieldCore::random(&mut rng); - let expected = a * b; - let reduced = F40::solinas_reduce(a.mul_wide(b)); - assert_eq!(reduced, expected); - } - } - - #[test] - fn mul_wide_u64_matches() { - let mut rng = StdRng::seed_from_u64(0xcafe_d00d); - for _ in 0..1000 { - let a: F40 = FieldCore::random(&mut rng); - let b = rng.next_u64() % ((1u64 << 40) - 195); - let expected = a * F40::from_canonical_u64(b); - let reduced = F40::solinas_reduce(a.mul_wide_u64(b)); - assert_eq!(reduced, expected); - } - } - - #[test] - fn pseudo_mersenne_trait() { - assert_eq!(::MODULUS_BITS, 40); - assert_eq!(::MODULUS_OFFSET, 195); - assert_eq!(::MODULUS_BITS, 64); - assert_eq!(::MODULUS_OFFSET, 59); - } - - #[test] - fn shift_optimization_detected() { - type G = Fp64<{ (1u64 << 56) - 27 }>; // C = 27, not 2^a±1 - assert_eq!(G::C_SHIFT_KIND, 0); - - type H = Fp64<{ u64::MAX - 58 }>; // C = 59, not 2^a±1 - assert_eq!(H::C_SHIFT_KIND, 0); - } - - #[test] - fn reduce_u128_subword_primes() { - // Regression: the fold's high part `v >> BITS` exceeds 64 bits for - // sub-word primes once the input reaches 2^(64 + BITS); the pre-fix - // kernel truncated it with `as u64`. 16-byte challenge inputs are - // essentially always in that domain. - fn check() { - let bits = 64 - P.leading_zeros(); - // For the full-word prime (BITS = 64) the truncation threshold - // 2^(64 + BITS) is out of u128 range; clamp the shift and rely on - // the u128::MAX cases. - let shift = (64 + bits).min(127); - let cases: [u128; 6] = [ - u128::MAX, - 1u128 << shift, - (1u128 << shift) + 12_345, - (1u128 << shift) - 1, - u128::MAX - P as u128, - (P as u128) << 63, - ]; - for x in cases { - let expected = (x % P as u128) as u64; - assert_eq!( - Fp64::

::from_canonical_u128_reduced(x).to_canonical_u64(), - expected, - "P = {P}, x = {x}" - ); - } - } - check::<{ (1u64 << 40) - 195 }>(); - check::<{ (1u64 << 48) - 59 }>(); - check::<{ (1u64 << 56) - 27 }>(); - check::<{ u64::MAX - 58 }>(); - } - - #[test] - fn challenge_bytes_subword_primes() { - // 16-byte Fiat-Shamir challenge derivation must agree with plain - // u128 modular reduction for every registered Fp64 prime. - let bytes: [u8; 16] = [ - 0xEF, 0xBE, 0xAD, 0xDE, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, - 0xBA, 0x98, - ]; - let x = u128::from_le_bytes(bytes); - fn check(x: u128, bytes: &[u8]) { - use crate::CanonicalRepr; - let expected = (x % P as u128) as u64; - assert_eq!( - Fp64::

::from_challenge_bytes(bytes).to_canonical_u64(), - expected, - "P = {P}" - ); - } - check::<{ (1u64 << 40) - 195 }>(x, &bytes); - check::<{ (1u64 << 48) - 59 }>(x, &bytes); - check::<{ (1u64 << 56) - 27 }>(x, &bytes); - check::<{ u64::MAX - 58 }>(x, &bytes); - } - - #[test] - fn reduce_u128_large() { - assert_eq!(F64::from_canonical_u128_reduced(u128::MAX), { - let p = u64::MAX as u128 - 58; - F64::from_canonical_u64((u128::MAX % p) as u64) - }); - } -} diff --git a/crates/jolt-field/src/prime/mod.rs b/crates/jolt-field/src/prime/mod.rs deleted file mode 100644 index e4d57bf643..0000000000 --- a/crates/jolt-field/src/prime/mod.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Pseudo-Mersenne prime fields (`p = 2^k − c`) and their named instances. -//! -//! The leaf layer of the field DAG: the `u32`/`u64`/`u128`-backed -//! `Fp{32,64,128}` field types, the `2^k − offset` registry -//! (`pseudo_mersenne`), and the shared low-level arithmetic helpers (`util`). -//! The extension towers (`ext`), packing (`packed`), and wide accumulators -//! (`unreduced`) all build on top of this module. - -#![expect( - clippy::unreadable_literal, - reason = "ported modulus and regression constants retain their audited spelling" -)] - -pub(crate) mod fp128; -pub(crate) mod fp32; -pub(crate) mod fp64; -pub(crate) mod native_capability; -pub(crate) mod pseudo_mersenne; -pub(crate) mod util; - -mod traits; -pub use traits::{balanced_digit_lut, CanonicalField, HalvingField, PseudoMersenneField}; - -pub use fp128::{ - Fp128, Prime128Offset159, Prime128Offset2355, Prime128Offset275, Prime128OffsetA7F7, -}; -pub use fp32::Fp32; -pub use fp64::Fp64; -pub use pseudo_mersenne::{ - is_registered_prime_offset, pseudo_mersenne_modulus, registered_prime_offset_spec, - Prime24Offset3, Prime30Offset35, Prime31Offset19, Prime32Offset99, Prime40Offset195, - Prime48Offset59, Prime56Offset27, Prime64Offset59, PrimeOffsetSpec, - PRIME_OFFSET_IMPLEMENTED_MAX_BITS, PRIME_OFFSET_MAX, PRIME_OFFSET_SPECS, -}; diff --git a/crates/jolt-field/src/prime/native_capability.rs b/crates/jolt-field/src/prime/native_capability.rs deleted file mode 100644 index eea6276f0f..0000000000 --- a/crates/jolt-field/src/prime/native_capability.rs +++ /dev/null @@ -1,168 +0,0 @@ -//! Native capability-trait impls for the prime fields: the canonical -//! byte/transcript surface and the `WithAccumulator` association (native -//! `NaiveAccumulator`). -//! -//! `FromPrimitiveInt`/`FieldCore` carry per-type logic and stay in the prime -//! modules; this module owns the shared derived-capability implementations used -//! directly by both Jolt and Akita. - -use crate::{FieldCore, FromPrimitiveInt}; - -macro_rules! impl_prime_native_capability { - ($ty:ident<$p:ident: $p_ty:ty>, $bytes:expr) => { - impl $crate::CanonicalBytes for $ty<$p> { - const NUM_BYTES: usize = $bytes; - - #[inline(always)] - fn to_bytes_le(&self, out: &mut [u8]) { - assert_eq!(out.len(), ::NUM_BYTES); - out.copy_from_slice( - &self.to_canonical_u128().to_le_bytes() - [..::NUM_BYTES], - ); - } - } - - impl $crate::CanonicalRepr for $ty<$p> { - #[inline(always)] - fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { - if bytes.len() <= ::std::mem::size_of::() { - let mut padded = [0u8; ::std::mem::size_of::()]; - padded[..bytes.len()].copy_from_slice(bytes); - return ::from_u128(u128::from_le_bytes( - padded, - )); - } - - $crate::prime::native_capability::reduce_le_bytes_mod_order(bytes) - } - - #[inline] - fn to_canonical_u64_checked(&self) -> Option { - self.to_canonical_u128().try_into().ok() - } - - #[inline] - fn num_bits(&self) -> u32 { - let value = self.to_canonical_u128(); - u128::BITS - value.leading_zeros() - } - } - - impl $crate::WithAccumulator for $ty<$p> { - type Accumulator = $crate::NaiveAccumulator; - } - - impl $crate::Field for $ty<$p> {} - }; -} - -/// Horner reduction of arbitrary-length little-endian bytes modulo the field -/// order (the >16-byte path of `CanonicalRepr::from_le_bytes_mod_order`). -#[inline(always)] -pub(crate) fn reduce_le_bytes_mod_order(bytes: &[u8]) -> F { - let base = F::from_u64(256); - bytes.iter().rev().fold(F::zero(), |acc, &byte| { - acc * base + F::from_u64(byte as u64) - }) -} - -pub(crate) use impl_prime_native_capability; - -#[cfg(test)] -mod tests { - //! Native byte / transcript / accumulator capability tests. - //! - //! These exercise the Solinas backend directly, so they run under - //! `--no-default-features --features solinas` as well as combined builds. - use super::*; - use crate::{ - Accumulator, CanonicalBytes, CanonicalField, CanonicalRepr, Fp32, Fp64, Prime128Offset275, - WithAccumulator, - }; - - /// Asserts the full canonical byte round-trip on the native traits. - fn assert_native_byte_roundtrip(value: F, expected: [u8; N]) - where - F: CanonicalField + CanonicalRepr + std::fmt::Debug + Eq, - { - assert_eq!(::NUM_BYTES, N); - - // to_bytes_le (the audited method) into a correctly sized buffer, plus - // the vec convenience wrapper — both must agree. - let mut buf = [0u8; N]; - value.to_bytes_le(&mut buf); - assert_eq!(buf, expected); - assert_eq!(value.to_bytes_le_vec(), expected.to_vec()); - - // Reducing / challenge constructors all invert the encoding. - assert_eq!(F::from_le_bytes_mod_order(&buf), value); - assert_eq!(F::from_challenge_bytes(&buf), value); - - assert_eq!( - value.num_bits(), - u128::BITS - value.to_canonical_u128().leading_zeros() - ); - } - - #[test] - fn prime_fields_native_byte_capabilities() { - type F32 = Fp32<251>; - type F64 = Fp64<4294967197>; - type F128 = Prime128Offset275; - - assert_native_byte_roundtrip::(F32::from_u64(42), 42u32.to_le_bytes()); - assert_native_byte_roundtrip::(F64::from_u64(42), 42u64.to_le_bytes()); - assert_native_byte_roundtrip::( - F128::from_canonical_u128(0x0102_0304_0506_0708), - 0x0102_0304_0506_0708u128.to_le_bytes(), - ); - - // Reducing constructor on a short slice: 255 mod 251 == 4. - assert_eq!(F32::from_le_bytes_mod_order(&[255, 0]), F32::from_u64(4)); - assert_eq!(F32::from_challenge_bytes(&[255, 0]), F32::from_u64(4)); - - // Over-long slice (> 16 bytes) takes the Horner-reduction path; trailing - // zero limbs must not change the value (and must not panic). - let value = F128::from_canonical_u128(0x00DE_AD00_BEEF); - let mut over_long = value.to_bytes_le_vec(); - over_long.extend_from_slice(&[0u8; 8]); - assert!(over_long.len() > 16); - assert_eq!(F128::from_le_bytes_mod_order(&over_long), value); - - // Bit-length + checked-u64 extraction. - assert_eq!(F32::zero().num_bits(), 0); - assert_eq!(F64::from_u64(7).to_canonical_u64_checked(), Some(7)); - assert_eq!( - F128::from_canonical_u128(1u128 << 65).to_canonical_u64_checked(), - None - ); - } - - #[test] - fn prime_fields_native_mul_capabilities() { - type F32 = Fp32<251>; - type F64 = Fp64<4294967197>; - - // mul_pow_2: 3 * 2^4 == 48. - assert_eq!(F32::from_u64(3).mul_pow_2(4), F32::from_u64(48)); - assert_eq!(F64::from_u64(5).mul_pow_2(0), F64::from_u64(5)); - // Large shift still agrees with repeated doubling. - let doubled = (0..40).fold(F64::from_u64(1), |acc, _| acc + acc); - assert_eq!(F64::from_u64(1).mul_pow_2(40), doubled); - - // mul_u64 / mul_i64. - assert_eq!(F64::from_u64(9).mul_u64(7), F64::from_u64(63)); - assert_eq!(F64::from_u64(9).mul_i64(-1), -F64::from_u64(9)); - } - - #[test] - fn prime_fields_native_accumulator() { - type F64 = Fp64<4294967197>; - - let mut acc = ::Accumulator::default(); - acc.fmadd(F64::from_u64(9), F64::from_u64(7)); - acc.add(F64::from_u64(2)); - assert_eq!(acc.reduce(), F64::from_u64(65)); - } -} diff --git a/crates/jolt-field/src/prime/pseudo_mersenne.rs b/crates/jolt-field/src/prime/pseudo_mersenne.rs deleted file mode 100644 index 3795c432bd..0000000000 --- a/crates/jolt-field/src/prime/pseudo_mersenne.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! `2^k - offset` pseudo-Mersenne registry and field aliases. -//! -//! Concrete aliases include both coordinates of `q = 2^k - offset` so adding -//! another prime at the same bit width does not create an implicit canonical -//! choice. - -use super::{Fp32, Fp64}; - -/// Maximum supported offset in this `2^k - offset` specialization. -pub const PRIME_OFFSET_MAX: u128 = 1u128 << 16; - -/// Current active bit-size bound for concrete field aliases in this phase. -pub const PRIME_OFFSET_IMPLEMENTED_MAX_BITS: u32 = 128; - -/// Metadata describing a `2^k - offset` pseudo-Mersenne modulus. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PrimeOffsetSpec { - /// `k` in `2^k - offset`. - pub bits: u32, - /// `offset` in `2^k - offset`. - pub offset: u16, - /// Modulus value. - pub modulus: u128, -} - -/// Compute `2^k - offset` for `k <= 128`. -pub const fn pseudo_mersenne_modulus(bits: u32, offset: u128) -> Option { - if bits == 0 || bits > 128 || offset == 0 { - return None; - } - if bits == 128 { - Some(u128::MAX - (offset - 1)) - } else { - Some((1u128 << bits) - offset) - } -} - -/// Return the registered prime spec for exactly `(bits, offset)`. -pub const fn registered_prime_offset_spec(bits: u32, offset: u128) -> Option { - let mut i = 0; - while i < PRIME_OFFSET_SPECS.len() { - let spec = PRIME_OFFSET_SPECS[i]; - if spec.bits == bits && (spec.offset as u128) == offset { - return Some(spec); - } - i += 1; - } - None -} - -/// Check whether `(k, offset)` is an explicitly registered `2^k - offset` prime. -pub const fn is_registered_prime_offset(bits: u32, offset: u128) -> bool { - if bits > PRIME_OFFSET_IMPLEMENTED_MAX_BITS || offset > PRIME_OFFSET_MAX { - return false; - } - registered_prime_offset_spec(bits, offset).is_some() -} - -/// `offset` for `k = 24`. -pub(crate) const PRIME24_OFFSET3_OFFSET: u16 = 3; -/// `offset` for `k = 30`. -pub(crate) const PRIME30_OFFSET35_OFFSET: u16 = 35; -/// `offset` for `k = 31`. -pub(crate) const PRIME31_OFFSET19_OFFSET: u16 = 19; -/// `offset` for `k = 32`. -pub(crate) const PRIME32_OFFSET99_OFFSET: u16 = 99; -/// `offset` for `k = 40`. -pub(crate) const PRIME40_OFFSET195_OFFSET: u16 = 195; -/// `offset` for `k = 48`. -pub(crate) const PRIME48_OFFSET59_OFFSET: u16 = 59; -/// `offset` for `k = 56`. -pub(crate) const PRIME56_OFFSET27_OFFSET: u16 = 27; -/// `offset` for `k = 64`. -pub(crate) const PRIME64_OFFSET59_OFFSET: u16 = 59; -/// `offset` for `k = 128`. -pub(crate) const PRIME128_OFFSET275_OFFSET: u16 = 275; - -/// `2^24 - 3`. -pub(crate) const PRIME24_OFFSET3_MODULUS: u32 = - ((1u128 << 24) - (PRIME24_OFFSET3_OFFSET as u128)) as u32; -/// `2^30 - 35`. -pub(crate) const PRIME30_OFFSET35_MODULUS: u32 = - ((1u128 << 30) - (PRIME30_OFFSET35_OFFSET as u128)) as u32; -/// `2^31 - 19`. -pub(crate) const PRIME31_OFFSET19_MODULUS: u32 = - ((1u128 << 31) - (PRIME31_OFFSET19_OFFSET as u128)) as u32; -/// `2^32 - 99`. -pub(crate) const PRIME32_OFFSET99_MODULUS: u32 = - ((1u128 << 32) - (PRIME32_OFFSET99_OFFSET as u128)) as u32; -/// `2^40 - 195`. -pub(crate) const PRIME40_OFFSET195_MODULUS: u64 = - ((1u128 << 40) - (PRIME40_OFFSET195_OFFSET as u128)) as u64; -/// `2^48 - 59`. -pub(crate) const PRIME48_OFFSET59_MODULUS: u64 = - ((1u128 << 48) - (PRIME48_OFFSET59_OFFSET as u128)) as u64; -/// `2^56 - 27`. -pub(crate) const PRIME56_OFFSET27_MODULUS: u64 = - ((1u128 << 56) - (PRIME56_OFFSET27_OFFSET as u128)) as u64; -/// `2^64 - 59`. -pub(crate) const PRIME64_OFFSET59_MODULUS: u64 = u64::MAX - ((PRIME64_OFFSET59_OFFSET as u64) - 1); -/// `2^128 - 275`. -pub(crate) const PRIME128_OFFSET275_MODULUS: u128 = - u128::MAX - (PRIME128_OFFSET275_OFFSET as u128 - 1); - -/// Prime field for `2^24 - 3`. -pub type Prime24Offset3 = Fp32; -/// Prime field for `2^30 - 35`. -pub type Prime30Offset35 = Fp32; -/// Prime field for `2^31 - 19`. -pub type Prime31Offset19 = Fp32; -/// Prime field for `2^32 - 99`. -pub type Prime32Offset99 = Fp32; -/// Prime field for `2^40 - 195`. -pub type Prime40Offset195 = Fp64; -/// Prime field for `2^48 - 59`. -pub type Prime48Offset59 = Fp64; -/// Prime field for `2^56 - 27`. -pub type Prime56Offset27 = Fp64; -/// Prime field for `2^64 - 59`. -pub type Prime64Offset59 = Fp64; - -/// `2^k - offset` profiles currently enabled in-code. -/// -/// Every enabled entry satisfies the current in-code `2^k - offset` policy. -pub const PRIME_OFFSET_SPECS: [PrimeOffsetSpec; 9] = [ - PrimeOffsetSpec { - bits: 24, - offset: PRIME24_OFFSET3_OFFSET, - modulus: PRIME24_OFFSET3_MODULUS as u128, - }, - PrimeOffsetSpec { - bits: 30, - offset: PRIME30_OFFSET35_OFFSET, - modulus: PRIME30_OFFSET35_MODULUS as u128, - }, - PrimeOffsetSpec { - bits: 31, - offset: PRIME31_OFFSET19_OFFSET, - modulus: PRIME31_OFFSET19_MODULUS as u128, - }, - PrimeOffsetSpec { - bits: 32, - offset: PRIME32_OFFSET99_OFFSET, - modulus: PRIME32_OFFSET99_MODULUS as u128, - }, - PrimeOffsetSpec { - bits: 40, - offset: PRIME40_OFFSET195_OFFSET, - modulus: PRIME40_OFFSET195_MODULUS as u128, - }, - PrimeOffsetSpec { - bits: 48, - offset: PRIME48_OFFSET59_OFFSET, - modulus: PRIME48_OFFSET59_MODULUS as u128, - }, - PrimeOffsetSpec { - bits: 56, - offset: PRIME56_OFFSET27_OFFSET, - modulus: PRIME56_OFFSET27_MODULUS as u128, - }, - PrimeOffsetSpec { - bits: 64, - offset: PRIME64_OFFSET59_OFFSET, - modulus: PRIME64_OFFSET59_MODULUS as u128, - }, - PrimeOffsetSpec { - bits: 128, - offset: PRIME128_OFFSET275_OFFSET, - modulus: PRIME128_OFFSET275_MODULUS, - }, -]; - -// All PseudoMersenneField impls for Fp32/Fp64/Fp128 are blanket impls in -// their respective modules (fp32.rs, fp64.rs, fp128.rs). diff --git a/crates/jolt-field/src/prime/traits.rs b/crates/jolt-field/src/prime/traits.rs deleted file mode 100644 index e147273d13..0000000000 --- a/crates/jolt-field/src/prime/traits.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::{FieldCore, FromPrimitiveInt}; -use num_traits::Zero; - -/// Canonical integer representation for a prime-field element. -pub trait CanonicalField: FieldCore + FromPrimitiveInt { - /// Returns the unique representative in `[0, p)`. - fn to_canonical_u128(self) -> u128; - - /// Returns the bit width of the field modulus. - fn modulus_bits() -> u32; - - /// Constructs an element when `val` is a canonical representative. - fn from_canonical_u128_checked(val: u128) -> Option; - - /// Constructs an element by reducing `val` modulo the field modulus. - fn from_canonical_u128_reduced(val: u128) -> Self; -} - -/// Field types with a cheap division-by-two operation. -pub trait HalvingField: FieldCore { - /// Divides this element by two. - fn half(self) -> Self; - - /// Returns the multiplicative inverse of two. - #[inline] - fn two_inv() -> Self { - Self::one().half() - } -} - -/// Builds the balanced signed-digit table for `1 <= log_basis <= 6`. -pub fn balanced_digit_lut(log_basis: u32) -> [F; 64] { - debug_assert!(log_basis > 0 && log_basis <= 6); - let basis = 1usize << log_basis; - let half_basis = (basis >> 1) as i64; - std::array::from_fn(|i| { - if i < basis { - F::from_i64(i as i64 - half_basis) - } else { - F::zero() - } - }) -} - -/// Metadata for a pseudo-Mersenne modulus `2^k - c`. -pub trait PseudoMersenneField: CanonicalField { - /// Exponent `k` in `2^k - c`. - const MODULUS_BITS: u32; - - /// Offset `c` in `2^k - c`. - const MODULUS_OFFSET: u128; -} diff --git a/crates/jolt-field/src/prime/util.rs b/crates/jolt-field/src/prime/util.rs deleted file mode 100644 index 5252ace6de..0000000000 --- a/crates/jolt-field/src/prime/util.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Shared helpers for field arithmetic backends. - -#![cfg_attr( - all(target_arch = "x86_64", target_feature = "bmi2"), - expect( - clippy::undocumented_unsafe_blocks, - reason = "the BMI2 intrinsic is gated by its required target feature" - ) -)] - -#[inline(always)] -pub(crate) const fn is_pow2_u64(x: u64) -> bool { - x.is_power_of_two() -} - -#[inline(always)] -pub(crate) const fn log2_pow2_u64(mut x: u64) -> u32 { - let mut k = 0u32; - while x > 1 { - x >>= 1; - k += 1; - } - k -} - -/// `a * b` widening to 128 bits; returns `(lo64, hi64)`. -#[inline(always)] -pub(crate) fn mul64_wide(a: u64, b: u64) -> (u64, u64) { - #[cfg(all(target_arch = "x86_64", target_feature = "bmi2"))] - { - unsafe { mul64_wide_bmi2(a, b) } - } - #[cfg(not(all(target_arch = "x86_64", target_feature = "bmi2")))] - { - let prod = (a as u128) * (b as u128); - (prod as u64, (prod >> 64) as u64) - } -} - -#[cfg(all(target_arch = "x86_64", target_feature = "bmi2"))] -#[inline(always)] -unsafe fn mul64_wide_bmi2(a: u64, b: u64) -> (u64, u64) { - let mut hi = 0; - let lo = unsafe { std::arch::x86_64::_mulx_u64(a, b, &mut hi) }; - (lo, hi) -} diff --git a/crates/jolt-field-two/src/schedules.rs b/crates/jolt-field/src/schedules.rs similarity index 100% rename from crates/jolt-field-two/src/schedules.rs rename to crates/jolt-field/src/schedules.rs diff --git a/crates/jolt-field-two/src/signed.rs b/crates/jolt-field/src/signed.rs similarity index 100% rename from crates/jolt-field-two/src/signed.rs rename to crates/jolt-field/src/signed.rs diff --git a/crates/jolt-field/src/signed/mod.rs b/crates/jolt-field/src/signed/mod.rs deleted file mode 100644 index 9c50077b27..0000000000 --- a/crates/jolt-field/src/signed/mod.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! Signed big integer types for the Jolt prover. -//! -//! These types represent signed integers with configurable bit widths using -//! sign-magnitude representation. -//! -//! Two families are provided: -//! -//! - [`SignedBigInt`]: magnitude stored as `Limbs` (width = `N * 64` bits) -//! - [`SignedBigIntHi32`]: magnitude stored as `[u64; N]` + `u32` tail (width = `N * 64 + 32` bits) -//! -//! Common type aliases: -//! - `S64`, `S128`, `S192`, `S256` (from `SignedBigInt`) -//! - `S96`, `S160`, `S224` (from `SignedBigIntHi32`) - -mod signed_bigint; -mod signed_bigint_hi32; - -pub use signed_bigint::*; -pub use signed_bigint_hi32::*; - -/// Generates the 5 standard operator impls for each `(Op, OpAssign)` pair: -/// val-val, OpAssign-val, val-ref, OpAssign-ref, ref-ref. -/// -/// Each operator delegates to an `&self`-taking `_assign_in_place` method. -macro_rules! impl_signed_assign_ops { - ($T:ident { - $($Op:ident, $OpAssign:ident, $method:ident, $assign_method:ident => $assign_fn:ident;)* - }) => { $( - impl $Op for $T { - type Output = Self; - #[inline] - fn $method(mut self, rhs: Self) -> Self { - self.$assign_fn(&rhs); - self - } - } - - impl $OpAssign for $T { - #[inline] - fn $assign_method(&mut self, rhs: Self) { - self.$assign_fn(&rhs); - } - } - - impl $Op<&$T> for $T { - type Output = $T; - #[inline] - fn $method(mut self, rhs: &$T) -> $T { - self.$assign_fn(rhs); - self - } - } - - impl $OpAssign<&$T> for $T { - #[inline] - fn $assign_method(&mut self, rhs: &$T) { - self.$assign_fn(rhs); - } - } - - impl $Op for &$T { - type Output = $T; - #[inline] - fn $method(self, rhs: Self) -> $T { - let mut out = *self; - out.$assign_fn(rhs); - out - } - } - )* }; -} - -pub(crate) use impl_signed_assign_ops; diff --git a/crates/jolt-field/src/signed/signed_bigint.rs b/crates/jolt-field/src/signed/signed_bigint.rs deleted file mode 100644 index 17976f4dda..0000000000 --- a/crates/jolt-field/src/signed/signed_bigint.rs +++ /dev/null @@ -1,684 +0,0 @@ -//! Sign-magnitude big integer with `N * 64`-bit width. - -use core::cmp::Ordering; -use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use num_traits::Zero; - -use crate::Limbs; - -/// A signed big integer using `Limbs` for magnitude and a sign bit. -/// -/// Zero is not canonicalized: a zero magnitude can be paired with either sign. -/// Structural equality distinguishes `+0` and `-0`, but ordering treats them -/// as equal. -#[derive(Clone, Copy, Debug)] -pub struct SignedBigInt { - pub magnitude: Limbs, - pub is_positive: bool, -} - -impl PartialEq for SignedBigInt { - #[inline] - fn eq(&self, other: &Self) -> bool { - if self.magnitude.is_zero() && other.magnitude.is_zero() { - return true; - } - self.is_positive == other.is_positive && self.magnitude == other.magnitude - } -} - -impl Eq for SignedBigInt {} - -#[cfg(feature = "allocative")] -impl allocative::Allocative for SignedBigInt { - fn visit<'a, 'b: 'a>(&self, visitor: &'a mut allocative::Visitor<'b>) { - visitor.visit_simple_sized::(); - } -} - -impl Default for SignedBigInt { - #[inline] - fn default() -> Self { - Self::zero() - } -} - -impl Zero for SignedBigInt { - #[inline] - fn zero() -> Self { - Self::zero() - } - - #[inline] - fn is_zero(&self) -> bool { - self.magnitude.is_zero() - } -} - -pub type S64 = SignedBigInt<1>; -pub type S128 = SignedBigInt<2>; -pub type S192 = SignedBigInt<3>; -pub type S256 = SignedBigInt<4>; - -impl SignedBigInt { - #[inline] - fn cmp_magnitude_mixed(&self, rhs: &SignedBigInt) -> Ordering { - let max_limbs = if N > M { N } else { M }; - let mut i = max_limbs; - while i > 0 { - let idx = i - 1; - let a = if idx < N { self.magnitude.0[idx] } else { 0u64 }; - let b = if idx < M { rhs.magnitude.0[idx] } else { 0u64 }; - if a > b { - return Ordering::Greater; - } - if a < b { - return Ordering::Less; - } - i -= 1; - } - Ordering::Equal - } - - #[inline] - pub fn new(limbs: [u64; N], is_positive: bool) -> Self { - Self { - magnitude: Limbs::new(limbs), - is_positive, - } - } - - #[inline] - pub fn from_limbs(magnitude: Limbs, is_positive: bool) -> Self { - Self { - magnitude, - is_positive, - } - } - - #[inline] - pub fn zero() -> Self { - Self { - magnitude: Limbs::from_u64(0), - is_positive: true, - } - } - - #[inline] - pub fn one() -> Self { - Self { - magnitude: Limbs::from_u64(1), - is_positive: true, - } - } - - #[inline] - pub fn as_magnitude(&self) -> &Limbs { - &self.magnitude - } - - #[inline] - pub fn magnitude_limbs(&self) -> [u64; N] { - self.magnitude.0 - } - - #[inline] - pub fn magnitude_slice(&self) -> &[u64] { - self.magnitude.as_ref() - } - - #[inline] - pub fn sign(&self) -> bool { - self.is_positive - } - - #[inline] - pub fn negate(self) -> Self { - Self::from_limbs(self.magnitude, !self.is_positive) - } - - #[inline(always)] - fn add_assign_in_place(&mut self, rhs: &Self) { - if self.is_positive == rhs.is_positive { - let _carry = self.magnitude.add_with_carry(&rhs.magnitude); - } else { - match self.magnitude.cmp(&rhs.magnitude) { - Ordering::Greater | Ordering::Equal => { - let _borrow = self.magnitude.sub_with_borrow(&rhs.magnitude); - } - Ordering::Less => { - let old = core::mem::replace(&mut self.magnitude, rhs.magnitude); - let _borrow = self.magnitude.sub_with_borrow(&old); - self.is_positive = rhs.is_positive; - } - } - } - } - - #[inline(always)] - fn sub_assign_in_place(&mut self, rhs: &Self) { - if self.is_positive != rhs.is_positive { - let _carry = self.magnitude.add_with_carry(&rhs.magnitude); - } else { - match self.magnitude.cmp(&rhs.magnitude) { - Ordering::Greater | Ordering::Equal => { - let _borrow = self.magnitude.sub_with_borrow(&rhs.magnitude); - } - Ordering::Less => { - let old = core::mem::replace(&mut self.magnitude, rhs.magnitude); - let _borrow = self.magnitude.sub_with_borrow(&old); - self.is_positive = !self.is_positive; - } - } - } - } - - #[inline(always)] - fn mul_assign_in_place(&mut self, rhs: &Self) { - let low = self.magnitude.mul_low(&rhs.magnitude); - self.magnitude = low; - self.is_positive = self.is_positive == rhs.is_positive; - } - - #[inline] - pub fn zero_extend_from(smaller: &SignedBigInt) -> SignedBigInt { - debug_assert!( - M <= N, - "cannot zero-extend: source has more limbs than destination" - ); - let widened_mag = Limbs::::zero_extend_from::(&smaller.magnitude); - SignedBigInt::from_limbs(widened_mag, smaller.is_positive) - } -} - -impl SignedBigInt { - /// Adds two values and truncates the result to `M` limbs. - #[inline] - pub fn add_trunc(&self, rhs: &SignedBigInt) -> SignedBigInt { - if self.is_positive == rhs.is_positive { - let mag = self.magnitude.add_trunc::(&rhs.magnitude); - return SignedBigInt:: { - magnitude: mag, - is_positive: self.is_positive, - }; - } - match self.magnitude.cmp(&rhs.magnitude) { - Ordering::Greater | Ordering::Equal => { - let mag = self.magnitude.sub_trunc::(&rhs.magnitude); - SignedBigInt:: { - magnitude: mag, - is_positive: self.is_positive, - } - } - Ordering::Less => { - let mag = rhs.magnitude.sub_trunc::(&self.magnitude); - SignedBigInt:: { - magnitude: mag, - is_positive: rhs.is_positive, - } - } - } - } - - /// Subtracts and truncates the result to `M` limbs. - #[inline] - pub fn sub_trunc(&self, rhs: &SignedBigInt) -> SignedBigInt { - if self.is_positive != rhs.is_positive { - let mag = self.magnitude.add_trunc::(&rhs.magnitude); - return SignedBigInt:: { - magnitude: mag, - is_positive: self.is_positive, - }; - } - match self.magnitude.cmp(&rhs.magnitude) { - Ordering::Greater | Ordering::Equal => { - let mag = self.magnitude.sub_trunc::(&rhs.magnitude); - SignedBigInt:: { - magnitude: mag, - is_positive: self.is_positive, - } - } - Ordering::Less => { - let mag = rhs.magnitude.sub_trunc::(&self.magnitude); - SignedBigInt:: { - magnitude: mag, - is_positive: !self.is_positive, - } - } - } - } - - /// Adds values of different widths (`N` and `M` limbs) and truncates to `P` limbs. - #[inline] - pub fn add_trunc_mixed( - &self, - rhs: &SignedBigInt, - ) -> SignedBigInt

{ - if self.is_positive == rhs.is_positive { - let mag = self.magnitude.add_trunc::(&rhs.magnitude); - return SignedBigInt::

{ - magnitude: mag, - is_positive: self.is_positive, - }; - } - match self.cmp_magnitude_mixed(rhs) { - Ordering::Greater | Ordering::Equal => { - let mag = self.magnitude.sub_trunc::(&rhs.magnitude); - SignedBigInt::

{ - magnitude: mag, - is_positive: self.is_positive, - } - } - Ordering::Less => { - let mag = rhs.magnitude.sub_trunc::(&self.magnitude); - SignedBigInt::

{ - magnitude: mag, - is_positive: rhs.is_positive, - } - } - } - } - - /// Subtracts values of different widths and truncates to `P` limbs. - #[inline] - pub fn sub_trunc_mixed( - &self, - rhs: &SignedBigInt, - ) -> SignedBigInt

{ - if self.is_positive != rhs.is_positive { - let mag = self.magnitude.add_trunc::(&rhs.magnitude); - return SignedBigInt::

{ - magnitude: mag, - is_positive: self.is_positive, - }; - } - match self.cmp_magnitude_mixed(rhs) { - Ordering::Greater | Ordering::Equal => { - let mag = self.magnitude.sub_trunc::(&rhs.magnitude); - SignedBigInt::

{ - magnitude: mag, - is_positive: self.is_positive, - } - } - Ordering::Less => { - let mag = rhs.magnitude.sub_trunc::(&self.magnitude); - SignedBigInt::

{ - magnitude: mag, - is_positive: !self.is_positive, - } - } - } - } - - /// Multiplies and truncates the result to `P` limbs. - #[inline] - pub fn mul_trunc( - &self, - rhs: &SignedBigInt, - ) -> SignedBigInt

{ - let mag = self.magnitude.mul_trunc::(&rhs.magnitude); - let sign = self.is_positive == rhs.is_positive; - SignedBigInt::

{ - magnitude: mag, - is_positive: sign, - } - } - - /// Fused multiply-add: `acc += self * rhs`, truncated to `P` limbs. - #[inline] - pub fn fmadd_trunc( - &self, - rhs: &SignedBigInt, - acc: &mut SignedBigInt

, - ) { - let prod_mag = self.magnitude.mul_trunc::(&rhs.magnitude); - let prod_sign = self.is_positive == rhs.is_positive; - if acc.is_positive == prod_sign { - let _ = acc.magnitude.add_with_carry(&prod_mag); - } else { - match acc.magnitude.cmp(&prod_mag) { - Ordering::Greater | Ordering::Equal => { - let _ = acc.magnitude.sub_with_borrow(&prod_mag); - } - Ordering::Less => { - let old = core::mem::replace(&mut acc.magnitude, prod_mag); - let _ = acc.magnitude.sub_with_borrow(&old); - acc.is_positive = prod_sign; - } - } - } - } -} - -impl SignedBigInt { - #[inline] - pub fn from_u64(value: u64) -> Self { - Self::from_limbs(Limbs::from_u64(value), true) - } - - #[inline] - pub fn from_u64_with_sign(value: u64, is_positive: bool) -> Self { - Self::from_limbs(Limbs::from_u64(value), is_positive) - } - - #[inline] - pub fn from_i64(value: i64) -> Self { - if value >= 0 { - Self::from_limbs(Limbs::from_u64(value as u64), true) - } else { - Self::from_limbs(Limbs::from_u64(value.wrapping_neg() as u64), false) - } - } - - #[inline] - pub fn from_u128(value: u128) -> Self { - debug_assert!(N >= 2, "from_u128 requires at least 2 limbs"); - let mut limbs = [0u64; N]; - limbs[0] = value as u64; - limbs[1] = (value >> 64) as u64; - Self::from_limbs(Limbs::new(limbs), true) - } - - #[inline] - pub fn from_i128(value: i128) -> Self { - debug_assert!(N >= 2, "from_i128 requires at least 2 limbs"); - if value >= 0 { - let mut limbs = [0u64; N]; - let v = value as u128; - limbs[0] = v as u64; - limbs[1] = (v >> 64) as u64; - Self::from_limbs(Limbs::new(limbs), true) - } else { - let mag = value.unsigned_abs(); - let mut limbs = [0u64; N]; - limbs[0] = mag as u64; - limbs[1] = (mag >> 64) as u64; - Self::from_limbs(Limbs::new(limbs), false) - } - } -} - -impl From for SignedBigInt { - #[inline] - fn from(value: u64) -> Self { - Self::from_u64(value) - } -} - -impl From for SignedBigInt { - #[inline] - fn from(value: i64) -> Self { - Self::from_i64(value) - } -} - -impl From<(u64, bool)> for SignedBigInt { - #[inline] - fn from(value_and_sign: (u64, bool)) -> Self { - Self::from_u64_with_sign(value_and_sign.0, value_and_sign.1) - } -} - -impl From for SignedBigInt { - #[inline] - fn from(value: u128) -> Self { - debug_assert!(N >= 2, "From requires at least 2 limbs"); - Self::from_u128(value) - } -} - -impl From for SignedBigInt { - #[inline] - fn from(value: i128) -> Self { - debug_assert!(N >= 2, "From requires at least 2 limbs"); - Self::from_i128(value) - } -} - -impl S64 { - #[inline] - pub fn to_i128(&self) -> i128 { - let magnitude = self.magnitude.0[0]; - if self.is_positive { - magnitude as i128 - } else { - -(magnitude as i128) - } - } - - #[inline] - pub fn magnitude_as_u64(&self) -> u64 { - self.magnitude.0[0] - } - - #[inline(always)] - pub fn from_diff_u64s(a: u64, b: u64) -> Self { - if a < b { - Self::new([b - a], false) - } else { - Self::new([a - b], true) - } - } -} - -impl S128 { - #[inline] - pub fn to_i128(&self) -> Option { - let hi = self.magnitude.0[1]; - let lo = self.magnitude.0[0]; - let hi_top_bit = hi >> 63; - if self.is_positive { - if hi_top_bit != 0 { - return None; - } - let mag = ((hi as u128) << 64) | (lo as u128); - Some(mag as i128) - } else if hi_top_bit == 0 { - let mag = ((hi as u128) << 64) | (lo as u128); - Some(-(mag as i128)) - } else if hi == (1u64 << 63) && lo == 0 { - Some(i128::MIN) - } else { - None - } - } - - #[inline] - pub fn magnitude_as_u128(&self) -> u128 { - (self.magnitude.0[1] as u128) << 64 | (self.magnitude.0[0] as u128) - } - - #[inline] - pub fn from_u128_and_sign(value: u128, is_positive: bool) -> Self { - Self::new([value as u64, (value >> 64) as u64], is_positive) - } - - #[inline] - pub fn from_u64_mul_i64(u: u64, s: i64) -> Self { - let mag = (u as u128) * (s.unsigned_abs() as u128); - Self::from_u128_and_sign(mag, s >= 0) - } - - #[inline] - pub fn from_i64_mul_u64(s: i64, u: u64) -> Self { - Self::from_u64_mul_i64(u, s) - } - - #[inline] - pub fn from_u64_mul_u64(a: u64, b: u64) -> Self { - let mag = (a as u128) * (b as u128); - Self::from_u128_and_sign(mag, true) - } - - #[inline] - pub fn from_i64_mul_i64(a: i64, b: i64) -> Self { - let mag = (a.unsigned_abs() as u128) * (b.unsigned_abs() as u128); - let is_positive = (a >= 0) == (b >= 0); - Self::from_u128_and_sign(mag, is_positive) - } -} - -super::impl_signed_assign_ops!(SignedBigInt { - Add, AddAssign, add, add_assign => add_assign_in_place; - Sub, SubAssign, sub, sub_assign => sub_assign_in_place; - Mul, MulAssign, mul, mul_assign => mul_assign_in_place; -}); - -impl Neg for SignedBigInt { - type Output = Self; - #[inline] - fn neg(self) -> Self::Output { - self.negate() - } -} - -impl PartialOrd for SignedBigInt { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for SignedBigInt { - #[inline] - fn cmp(&self, other: &Self) -> Ordering { - if self.magnitude.is_zero() && other.magnitude.is_zero() { - return Ordering::Equal; - } - match (self.is_positive, other.is_positive) { - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, - _ => { - let ord = self.magnitude.cmp(&other.magnitude); - if self.is_positive { - ord - } else { - ord.reverse() - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn s64_basic_arithmetic() { - let a = S64::from_i64(10); - let b = S64::from_i64(-3); - let c = a + b; - assert_eq!(c.to_i128(), 7); - - let d = a - b; - assert_eq!(d.to_i128(), 13); - } - - #[test] - fn s64_from_diff() { - let d = S64::from_diff_u64s(5, 10); - assert!(!d.is_positive); - assert_eq!(d.magnitude_as_u64(), 5); - - let d2 = S64::from_diff_u64s(10, 5); - assert!(d2.is_positive); - assert_eq!(d2.magnitude_as_u64(), 5); - } - - #[test] - fn s128_mul_i64() { - let r = S128::from_i64_mul_i64(-3, 7); - assert_eq!(r.to_i128(), Some(-21)); - - let r2 = S128::from_u64_mul_u64(100, 200); - assert_eq!(r2.to_i128(), Some(20000)); - } - - #[test] - fn s128_magnitude() { - let v = S128::from_i128(-12_345_678_901_234_567_890_i128); - assert!(!v.is_positive); - assert_eq!(v.magnitude_as_u128(), 12_345_678_901_234_567_890_u128); - } - - #[test] - fn mul_trunc_s64_to_s128() { - let a = S64::from_i64(-5); - let b = S64::from_i64(7); - let c: S128 = a.mul_trunc::<1, 2>(&b); - assert_eq!(c.to_i128(), Some(-35)); - } - - #[test] - fn ordering() { - let a = S64::from_i64(5); - let b = S64::from_i64(-5); - let z1 = S64::from_u64(0); - let z2 = S64::new([0], false); // negative zero - assert!(a > b); - assert_eq!(z1.cmp(&z2), Ordering::Equal); - assert_eq!(z1, z2); - } - - #[test] - fn zero_extend() { - let s = S64::from_i64(-42); - let wide: S128 = SignedBigInt::zero_extend_from(&s); - assert!(!wide.is_positive); - assert_eq!(wide.magnitude.0[0], 42); - assert_eq!(wide.magnitude.0[1], 0); - } - - #[test] - fn add_trunc_mixed() { - let a = S64::from_i64(100); - let b = S128::from_i128(200); - let c: S128 = a.add_trunc_mixed::<2, 2>(&b); - assert_eq!(c.to_i128(), Some(300)); - } - - #[test] - fn fmadd_trunc() { - let a = S64::from_i64(3); - let b = S64::from_i64(4); - let mut acc = S128::from_i128(10); - a.fmadd_trunc::<1, 2>(&b, &mut acc); - assert_eq!(acc.to_i128(), Some(22)); // 10 + 3*4 - } - - #[test] - fn s128_to_i128_out_of_range() { - // Magnitude exceeding i128::MAX for positive - let big_positive = S128::new([0, 1u64 << 63], true); - assert_eq!(big_positive.to_i128(), None); - - // Magnitude exceeding i128::MIN for negative (not exactly MIN) - let big_negative = S128::new([1, 1u64 << 63], false); - assert_eq!(big_negative.to_i128(), None); - - // Exactly i128::MIN is representable - let min_val = S128::new([0, 1u64 << 63], false); - assert_eq!(min_val.to_i128(), Some(i128::MIN)); - } - - #[test] - fn fmadd_trunc_sign_flip() { - // Positive accumulator, subtract larger product → sign flips - let a = S64::from_i64(-10); - let b = S64::from_i64(5); - let mut acc = S128::from_i128(3); - a.fmadd_trunc::<1, 2>(&b, &mut acc); - // 3 + (-10 * 5) = 3 - 50 = -47 - assert_eq!(acc.to_i128(), Some(-47)); - assert!(!acc.is_positive); - } - - #[test] - fn s64_from_diff_u64s_zero_zero() { - let d = S64::from_diff_u64s(0, 0); - assert!(d.is_positive); - assert!(d.is_zero()); - assert_eq!(d.magnitude_as_u64(), 0); - } -} diff --git a/crates/jolt-field/src/signed/signed_bigint_hi32.rs b/crates/jolt-field/src/signed/signed_bigint_hi32.rs deleted file mode 100644 index 06ee1ba125..0000000000 --- a/crates/jolt-field/src/signed/signed_bigint_hi32.rs +++ /dev/null @@ -1,680 +0,0 @@ -//! Sign-magnitude big integer with `N * 64 + 32`-bit width. - -#[cfg(feature = "allocative")] -use allocative::Allocative; - -use core::cmp::Ordering; -use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; - -use super::{SignedBigInt, S128, S64}; -use crate::Limbs; - -/// Compact signed big-integer with width `N * 64 + 32` bits. -/// -/// Uses `[u64; N]` for the low limbs and a `u32` for the high tail. -/// This representation saves 4 bytes per value compared to using `N + 1` -/// full 64-bit limbs, which matters when millions of these are stored -/// in witness polynomials. -/// -#[derive(Clone, Copy, Debug)] -#[cfg_attr(feature = "allocative", derive(Allocative))] -pub struct SignedBigIntHi32 { - magnitude_lo: [u64; N], - magnitude_hi: u32, - is_positive: bool, -} - -pub type S96 = SignedBigIntHi32<1>; -pub type S160 = SignedBigIntHi32<2>; -pub type S224 = SignedBigIntHi32<3>; - -impl SignedBigIntHi32 { - pub const fn new(magnitude_lo: [u64; N], magnitude_hi: u32, is_positive: bool) -> Self { - Self { - magnitude_lo, - magnitude_hi, - is_positive, - } - } - - pub const fn zero() -> Self { - Self { - magnitude_lo: [0; N], - magnitude_hi: 0, - is_positive: true, - } - } - - pub fn one() -> Self { - let mut magnitude_lo = [0; N]; - let magnitude_hi; - if N == 0 { - magnitude_hi = 1; - } else { - magnitude_lo[0] = 1; - magnitude_hi = 0; - } - Self { - magnitude_lo, - magnitude_hi, - is_positive: true, - } - } - - pub const fn magnitude_lo(&self) -> &[u64; N] { - &self.magnitude_lo - } - - pub const fn magnitude_hi(&self) -> u32 { - self.magnitude_hi - } - - pub const fn is_positive(&self) -> bool { - self.is_positive - } - - pub const fn is_zero(&self) -> bool { - let mut lo_is_zero = true; - let mut i = 0; - while i < N { - if self.magnitude_lo[i] != 0 { - lo_is_zero = false; - break; - } - i += 1; - } - self.magnitude_hi == 0 && lo_is_zero - } - - fn compare_magnitudes(&self, other: &Self) -> Ordering { - if self.magnitude_hi != other.magnitude_hi { - return self.magnitude_hi.cmp(&other.magnitude_hi); - } - for i in (0..N).rev() { - if self.magnitude_lo[i] != other.magnitude_lo[i] { - return self.magnitude_lo[i].cmp(&other.magnitude_lo[i]); - } - } - Ordering::Equal - } - - #[inline(always)] - fn add_assign_in_place(&mut self, rhs: &Self) { - if self.is_positive == rhs.is_positive { - let (lo, hi, _carry) = self.add_magnitudes_with_carry(rhs); - self.magnitude_lo = lo; - self.magnitude_hi = hi; - } else { - match self.compare_magnitudes(rhs) { - Ordering::Greater | Ordering::Equal => { - let (lo, hi, _borrow) = self.sub_magnitudes_with_borrow(rhs); - self.magnitude_lo = lo; - self.magnitude_hi = hi; - } - Ordering::Less => { - let (lo, hi, _borrow) = rhs.sub_magnitudes_with_borrow(self); - self.magnitude_lo = lo; - self.magnitude_hi = hi; - self.is_positive = rhs.is_positive; - } - } - } - } - - #[inline(always)] - fn sub_assign_in_place(&mut self, rhs: &Self) { - if self.is_positive != rhs.is_positive { - let (lo, hi, _carry) = self.add_magnitudes_with_carry(rhs); - self.magnitude_lo = lo; - self.magnitude_hi = hi; - } else { - match self.compare_magnitudes(rhs) { - Ordering::Greater | Ordering::Equal => { - let (lo, hi, _borrow) = self.sub_magnitudes_with_borrow(rhs); - self.magnitude_lo = lo; - self.magnitude_hi = hi; - } - Ordering::Less => { - let (lo, hi, _borrow) = rhs.sub_magnitudes_with_borrow(self); - self.magnitude_lo = lo; - self.magnitude_hi = hi; - self.is_positive = !self.is_positive; - } - } - } - } - - #[inline(always)] - fn mul_assign_in_place(&mut self, rhs: &Self) { - let (lo, hi) = self.mul_magnitudes(rhs); - self.is_positive = self.is_positive == rhs.is_positive; - self.magnitude_lo = lo; - self.magnitude_hi = hi; - } - - fn mul_magnitudes(&self, other: &Self) -> ([u64; N], u32) { - if N == 0 { - let a2 = self.magnitude_hi as u64; - let b2 = other.magnitude_hi as u64; - let prod = a2.wrapping_mul(b2); - let hi = (prod & 0xFFFF_FFFF) as u32; - let lo: [u64; N] = [0u64; N]; - return (lo, hi); - } - - if N == 1 { - let a0 = self.magnitude_lo[0]; - let a1 = self.magnitude_hi as u64; - let b0 = other.magnitude_lo[0]; - let b1 = other.magnitude_hi as u64; - - let t0 = (a0 as u128) * (b0 as u128); - let lo0 = t0 as u64; - let cross = (t0 >> 64) + (a0 as u128) * (b1 as u128) + (a1 as u128) * (b0 as u128); - let hi = (cross as u64 & 0xFFFF_FFFF) as u32; - let mut lo = [0u64; N]; - lo[0] = lo0; - return (lo, hi); - } - - if N == 2 { - let a0 = self.magnitude_lo[0]; - let a1 = self.magnitude_lo[1]; - let a2 = self.magnitude_hi as u64; - let b0 = other.magnitude_lo[0]; - let b1 = other.magnitude_lo[1]; - let b2 = other.magnitude_hi as u64; - - let t0 = (a0 as u128) * (b0 as u128); - let r0 = t0 as u64; - let carry0 = t0 >> 64; - - // Word 1 sums two full 64x64 products plus the carry, which can - // exceed u128 (2 * (2^64 - 1)^2 > 2^128); one product plus the - // carry always fits, so add the second with overflow tracking. - // (The old fused sum's failure mode was a debug-only panic: its - // wrapped carries cancel modulo 2^64, and only the low 32 bits - // of word 2 survive, so release output happened to be correct.) - let p01 = (a0 as u128) * (b1 as u128); - let p10 = (a1 as u128) * (b0 as u128); - let (sum1, overflow1) = (p01 + carry0).overflowing_add(p10); - let r1 = sum1 as u64; - // True carry into word 2 (at most 2^65 - 3): high half plus the - // overflowed 2^128 bit, which contributes 2^64 to the carry. - let carry1 = (sum1 >> 64) + ((overflow1 as u128) << 64); - - // Only the low 32 bits of word 2 survive in the 160-bit result, - // and wrapping addition preserves low bits exactly, so word 2 - // needs no overflow tracking. - let sum2 = carry1 - .wrapping_add((a0 as u128) * (b2 as u128)) - .wrapping_add((a1 as u128) * (b1 as u128)) - .wrapping_add((a2 as u128) * (b0 as u128)); - let r2 = sum2 as u64; - - let hi = (r2 & 0xFFFF_FFFF) as u32; - let mut lo = [0u64; N]; - lo[0] = r0; - lo[1] = r1; - return (lo, hi); - } - - // General path — reads limbs inline to avoid heap allocation. - // Stack buffer covers up to N=7 (2*(7+1) = 16 entries). - let num_limbs = N + 1; - let mut prod = [0u64; 16]; - debug_assert!( - 2 * num_limbs <= prod.len(), - "N too large for stack-allocated product buffer" - ); - - let limb_a = |i: usize| -> u64 { - if i < N { - self.magnitude_lo[i] - } else { - self.magnitude_hi as u64 - } - }; - let limb_b = |j: usize| -> u64 { - if j < N { - other.magnitude_lo[j] - } else { - other.magnitude_hi as u64 - } - }; - - for i in 0..num_limbs { - let a_limb = limb_a(i); - let mut carry: u128 = 0; - for j in 0..num_limbs { - let idx = i + j; - let p = (a_limb as u128) * (limb_b(j) as u128) + (prod[idx] as u128) + carry; - prod[idx] = p as u64; - carry = p >> 64; - } - if carry > 0 { - let spill = i + num_limbs; - if spill < prod.len() { - prod[spill] = prod[spill].wrapping_add(carry as u64); - } - } - } - - let mut magnitude_lo = [0u64; N]; - magnitude_lo[..N].copy_from_slice(&prod[..N]); - let magnitude_hi = (prod[N] & 0xFFFF_FFFF) as u32; - (magnitude_lo, magnitude_hi) - } - - fn add_magnitudes_with_carry(&self, other: &Self) -> ([u64; N], u32, bool) { - let mut magnitude_lo = [0; N]; - let mut carry: u128 = 0; - for (i, out) in magnitude_lo.iter_mut().enumerate() { - let sum = (self.magnitude_lo[i] as u128) + (other.magnitude_lo[i] as u128) + carry; - *out = sum as u64; - carry = sum >> 64; - } - let sum_hi = (self.magnitude_hi as u128) + (other.magnitude_hi as u128) + carry; - let magnitude_hi = sum_hi as u32; - let final_carry = (sum_hi >> 32) != 0; - (magnitude_lo, magnitude_hi, final_carry) - } - - fn sub_magnitudes_with_borrow(&self, other: &Self) -> ([u64; N], u32, bool) { - let mut magnitude_lo = [0u64; N]; - let mut borrow = false; - for (i, out) in magnitude_lo.iter_mut().enumerate() { - let (d1, b1) = self.magnitude_lo[i].overflowing_sub(other.magnitude_lo[i]); - let (d2, b2) = d1.overflowing_sub(u64::from(borrow)); - *out = d2; - borrow = b1 || b2; - } - let (hi1, b1) = self.magnitude_hi.overflowing_sub(other.magnitude_hi); - let (hi2, b2) = hi1.overflowing_sub(u32::from(borrow)); - let final_borrow = b1 || b2; - (magnitude_lo, hi2, final_borrow) - } - - /// Return the unsigned magnitude as `Limbs`. - /// Debug-asserts `NPLUS1 == N + 1`. - #[inline] - pub fn magnitude_as_limbs_nplus1(&self) -> Limbs { - assert!( - NPLUS1 == N + 1, - "NPLUS1 must be N+1 for SignedBigIntHi32 magnitude pack" - ); - let mut limbs = [0u64; NPLUS1]; - if N > 0 { - limbs[..N].copy_from_slice(&self.magnitude_lo); - } - limbs[N] = self.magnitude_hi as u64; - Limbs::new(limbs) - } - - #[inline] - pub fn zero_extend_from(smaller: &SignedBigIntHi32) -> SignedBigIntHi32 { - debug_assert!( - M <= N, - "cannot zero-extend: source has more limbs than destination" - ); - if N == M { - let mut lo = [0u64; N]; - if N > 0 { - lo.copy_from_slice(smaller.magnitude_lo()); - } - return SignedBigIntHi32::::new(lo, smaller.magnitude_hi(), smaller.is_positive()); - } - // N > M: place hi32 into limb M - let mut lo = [0u64; N]; - if M > 0 { - lo[..M].copy_from_slice(smaller.magnitude_lo()); - } - lo[M] = smaller.magnitude_hi() as u64; - SignedBigIntHi32::::new(lo, 0u32, smaller.is_positive()) - } - - /// Convert into a `SignedBigInt`. - /// Debug-asserts `NPLUS1 == N + 1`. - #[inline] - pub fn to_signed_bigint_nplus1(&self) -> SignedBigInt { - assert!( - NPLUS1 == N + 1, - "to_signed_bigint_nplus1 requires NPLUS1 = N + 1" - ); - let mut limbs = [0u64; NPLUS1]; - if N > 0 { - limbs[..N].copy_from_slice(self.magnitude_lo()); - } - limbs[N] = self.magnitude_hi() as u64; - SignedBigInt::from_limbs(Limbs::new(limbs), self.is_positive()) - } -} - -impl Neg for SignedBigIntHi32 { - type Output = Self; - fn neg(self) -> Self::Output { - Self::new(self.magnitude_lo, self.magnitude_hi, !self.is_positive) - } -} - -impl Neg for &SignedBigIntHi32 { - type Output = SignedBigIntHi32; - fn neg(self) -> Self::Output { - SignedBigIntHi32::new(self.magnitude_lo, self.magnitude_hi, !self.is_positive) - } -} - -super::impl_signed_assign_ops!(SignedBigIntHi32 { - Add, AddAssign, add, add_assign => add_assign_in_place; - Sub, SubAssign, sub, sub_assign => sub_assign_in_place; - Mul, MulAssign, mul, mul_assign => mul_assign_in_place; -}); - -impl PartialEq for SignedBigIntHi32 { - fn eq(&self, other: &Self) -> bool { - if self.is_zero() && other.is_zero() { - return true; - } - self.is_positive == other.is_positive - && self.magnitude_hi == other.magnitude_hi - && self.magnitude_lo == other.magnitude_lo - } -} - -impl Eq for SignedBigIntHi32 {} - -impl PartialOrd for SignedBigIntHi32 { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for SignedBigIntHi32 { - #[inline] - fn cmp(&self, other: &Self) -> Ordering { - if self.is_zero() && other.is_zero() { - return Ordering::Equal; - } - match (self.is_positive, other.is_positive) { - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, - _ => { - let ord = self.compare_magnitudes(other); - if self.is_positive { - ord - } else { - ord.reverse() - } - } - } - } -} - -impl From for S96 { - #[inline] - fn from(val: i64) -> Self { - Self::new([val.unsigned_abs()], 0, val >= 0) - } -} - -impl From for S96 { - #[inline] - fn from(val: u64) -> Self { - Self::new([val], 0, true) - } -} - -impl From for S96 { - #[inline] - fn from(val: S64) -> Self { - Self::new([val.magnitude.0[0]], 0, val.is_positive) - } -} - -impl From for S160 { - #[inline] - fn from(val: i64) -> Self { - Self::new([val.unsigned_abs(), 0], 0, val >= 0) - } -} - -impl From for S160 { - #[inline] - fn from(val: u64) -> Self { - Self::new([val, 0], 0, true) - } -} - -impl From for S160 { - #[inline] - fn from(val: S64) -> Self { - Self::new([val.magnitude.0[0], 0], 0, val.is_positive) - } -} - -impl From for S160 { - #[inline] - fn from(val: u128) -> Self { - let lo = val as u64; - let hi = (val >> 64) as u64; - Self::new([lo, hi], 0, true) - } -} - -impl From for S160 { - #[inline] - fn from(val: i128) -> Self { - let is_positive = val >= 0; - let mag = val.unsigned_abs(); - let lo = mag as u64; - let hi = (mag >> 64) as u64; - Self::new([lo, hi], 0, is_positive) - } -} - -impl From for S160 { - #[inline] - fn from(val: S128) -> Self { - Self::new([val.magnitude.0[0], val.magnitude.0[1]], 0, val.is_positive) - } -} - -impl From for Limbs<4> { - #[inline] - fn from(val: S224) -> Self { - debug_assert!(val.is_positive(), "From for Limbs<4> discards sign"); - let lo = val.magnitude_lo(); - let hi = val.magnitude_hi() as u64; - Limbs([lo[0], lo[1], lo[2], hi]) - } -} - -impl S160 { - /// Computes the signed difference `a - b` as an `S160`. - #[inline] - pub fn from_diff_u64(a: u64, b: u64) -> Self { - let mag = a.abs_diff(b); - let is_positive = a >= b; - S160::new([mag, 0], 0, is_positive) - } - - /// Creates an `S160` from a `u128` magnitude and sign. - #[inline] - pub fn from_magnitude_u128(mag: u128, is_positive: bool) -> Self { - let lo = mag as u64; - let hi = (mag >> 64) as u64; - S160::new([lo, hi], 0, is_positive) - } - - /// Computes the signed difference `u1 - u2` as an `S160`. - #[inline] - pub fn from_diff_u128(u1: u128, u2: u128) -> Self { - if u1 >= u2 { - S160::from_magnitude_u128(u1 - u2, true) - } else { - S160::from_magnitude_u128(u2 - u1, false) - } - } - - /// Computes `u1 + u2` as an `S160`, handling carry into the hi32 limb. - #[inline] - pub fn from_sum_u128(u1: u128, u2: u128) -> Self { - let u1_lo = u1 as u64; - let u1_hi = (u1 >> 64) as u64; - let u2_lo = u2 as u64; - let u2_hi = (u2 >> 64) as u64; - let (sum_lo, carry0) = u1_lo.overflowing_add(u2_lo); - let (sum_hi1, carry1) = u1_hi.overflowing_add(u2_hi); - let (sum_hi, carry2) = sum_hi1.overflowing_add(u64::from(carry0)); - let carry_out = carry1 || carry2; - S160::new([sum_lo, sum_hi], u32::from(carry_out), true) - } - - /// Computes `u - i` as an `S160`. - #[inline] - pub fn from_u128_minus_i128(u: u128, i: i128) -> Self { - if i >= 0 { - S160::from_diff_u128(u, i as u128) - } else { - let abs_i: u128 = i.unsigned_abs(); - S160::from_sum_u128(u, abs_i) - } - } -} - -impl Default for S160 { - fn default() -> Self { - Self::zero() - } -} - -#[cfg(test)] -mod tests { - - fn oracle_mul_160(a: &S160, b: &S160) -> ([u64; 2], u32) { - // 32-bit digit schoolbook, truncated to 160 bits: the independent - // reference for the unrolled N == 2 kernel. - let digits = |v: &S160| -> [u32; 5] { - [ - v.magnitude_lo[0] as u32, - (v.magnitude_lo[0] >> 32) as u32, - v.magnitude_lo[1] as u32, - (v.magnitude_lo[1] >> 32) as u32, - v.magnitude_hi, - ] - }; - let (a, b) = (digits(a), digits(b)); - let mut cols = [0u128; 10]; - for i in 0..5 { - for j in 0..5 { - cols[i + j] += (a[i] as u128) * (b[j] as u128); - } - } - let mut out = [0u32; 10]; - let mut carry = 0u128; - for k in 0..10 { - let v = cols[k] + carry; - out[k] = (v & 0xFFFF_FFFF) as u32; - carry = v >> 32; - } - ( - [ - out[0] as u64 | (out[1] as u64) << 32, - out[2] as u64 | (out[3] as u64) << 32, - ], - out[4], - ) - } - - #[test] - fn s160_mul_magnitudes_large_second_limbs() { - // Regression: the pre-fix kernel fused two (three) full 64x64 - // products into one u128 sum, which overflows once both operands - // have large second limbs (debug panic, release wraparound). - let make = |lo: [u64; 2], hi: u32| S160 { - magnitude_lo: lo, - magnitude_hi: hi, - is_positive: true, - }; - let cases = [ - (make([u64::MAX, u64::MAX], 0), make([u64::MAX, u64::MAX], 0)), - ( - make([u64::MAX, u64::MAX], u32::MAX), - make([u64::MAX, u64::MAX], u32::MAX), - ), - (make([0, u64::MAX], 0), make([0, u64::MAX], 0)), - (make([1, u64::MAX], 0), make([u64::MAX, 1], 0)), - ( - make([0xDEAD_BEEF_0123_4567, 0x89AB_CDEF_FEDC_BA98], 0x0F0F_0F0F), - make([0x1111_2222_3333_4444, 0x5555_6666_7777_8888], 0xFFFF_0000), - ), - (make([42, 0], 0), make([37, 0], 0)), - ]; - for (a, b) in cases { - let got = a.mul_magnitudes(&b); - let want = oracle_mul_160(&a, &b); - assert_eq!(got, want, "a = {a:?}, b = {b:?}"); - } - } - - use super::*; - - #[test] - fn s160_from_diff_u64() { - let d = S160::from_diff_u64(10, 3); - assert!(d.is_positive()); - assert_eq!(d.magnitude_lo()[0], 7); - - let d2 = S160::from_diff_u64(3, 10); - assert!(!d2.is_positive()); - assert_eq!(d2.magnitude_lo()[0], 7); - } - - #[test] - fn s160_addition() { - let a = S160::from(100u64); - let b = S160::from(200u64); - let c = a + b; - assert!(c.is_positive()); - assert_eq!(c.magnitude_lo()[0], 300); - } - - #[test] - fn s160_subtraction() { - let a = S160::from(100u64); - let b = S160::from(200u64); - let c = a - b; - assert!(!c.is_positive()); - assert_eq!(c.magnitude_lo()[0], 100); - } - - #[test] - fn s160_to_signed_bigint() { - let v = S160::new([42, 0], 7, false); - let sb: SignedBigInt<3> = v.to_signed_bigint_nplus1::<3>(); - assert!(!sb.is_positive); - assert_eq!(sb.magnitude.0[0], 42); - assert_eq!(sb.magnitude.0[1], 0); - assert_eq!(sb.magnitude.0[2], 7); - } - - #[test] - fn s160_from_u128_minus_i128() { - let v = S160::from_u128_minus_i128(100, -50); - assert!(v.is_positive()); - assert_eq!(v.magnitude_lo()[0], 150); - - let v2 = S160::from_u128_minus_i128(100, 150); - assert!(!v2.is_positive()); - assert_eq!(v2.magnitude_lo()[0], 50); - } - - #[test] - fn zero_extend() { - let s = S96::from(42u64); - let wide: S160 = SignedBigIntHi32::zero_extend_from(&s); - assert!(wide.is_positive()); - assert_eq!(wide.magnitude_lo()[0], 42); - } -} diff --git a/crates/jolt-field-two/src/solinas/ext.rs b/crates/jolt-field/src/solinas/ext.rs similarity index 100% rename from crates/jolt-field-two/src/solinas/ext.rs rename to crates/jolt-field/src/solinas/ext.rs diff --git a/crates/jolt-field-two/src/solinas/fp128.rs b/crates/jolt-field/src/solinas/fp128.rs similarity index 100% rename from crates/jolt-field-two/src/solinas/fp128.rs rename to crates/jolt-field/src/solinas/fp128.rs diff --git a/crates/jolt-field-two/src/solinas/mod.rs b/crates/jolt-field/src/solinas/mod.rs similarity index 100% rename from crates/jolt-field-two/src/solinas/mod.rs rename to crates/jolt-field/src/solinas/mod.rs diff --git a/crates/jolt-field-two/src/solinas/packed/engine.rs b/crates/jolt-field/src/solinas/packed/engine.rs similarity index 100% rename from crates/jolt-field-two/src/solinas/packed/engine.rs rename to crates/jolt-field/src/solinas/packed/engine.rs diff --git a/crates/jolt-field-two/src/solinas/packed/ext.rs b/crates/jolt-field/src/solinas/packed/ext.rs similarity index 100% rename from crates/jolt-field-two/src/solinas/packed/ext.rs rename to crates/jolt-field/src/solinas/packed/ext.rs diff --git a/crates/jolt-field-two/src/solinas/packed/fp128.rs b/crates/jolt-field/src/solinas/packed/fp128.rs similarity index 100% rename from crates/jolt-field-two/src/solinas/packed/fp128.rs rename to crates/jolt-field/src/solinas/packed/fp128.rs diff --git a/crates/jolt-field-two/src/solinas/packed/mod.rs b/crates/jolt-field/src/solinas/packed/mod.rs similarity index 100% rename from crates/jolt-field-two/src/solinas/packed/mod.rs rename to crates/jolt-field/src/solinas/packed/mod.rs diff --git a/crates/jolt-field-two/src/solinas/packed/simd.rs b/crates/jolt-field/src/solinas/packed/simd.rs similarity index 100% rename from crates/jolt-field-two/src/solinas/packed/simd.rs rename to crates/jolt-field/src/solinas/packed/simd.rs diff --git a/crates/jolt-field-two/src/solinas/parallel.rs b/crates/jolt-field/src/solinas/parallel.rs similarity index 100% rename from crates/jolt-field-two/src/solinas/parallel.rs rename to crates/jolt-field/src/solinas/parallel.rs diff --git a/crates/jolt-field-two/src/solinas/unreduced.rs b/crates/jolt-field/src/solinas/unreduced.rs similarity index 100% rename from crates/jolt-field-two/src/solinas/unreduced.rs rename to crates/jolt-field/src/solinas/unreduced.rs diff --git a/crates/jolt-field-two/src/solinas/word.rs b/crates/jolt-field/src/solinas/word.rs similarity index 100% rename from crates/jolt-field-two/src/solinas/word.rs rename to crates/jolt-field/src/solinas/word.rs diff --git a/crates/jolt-field-two/src/unreduced.rs b/crates/jolt-field/src/unreduced.rs similarity index 100% rename from crates/jolt-field-two/src/unreduced.rs rename to crates/jolt-field/src/unreduced.rs diff --git a/crates/jolt-field/src/unreduced/accum.rs b/crates/jolt-field/src/unreduced/accum.rs deleted file mode 100644 index 5ba68ff90e..0000000000 --- a/crates/jolt-field/src/unreduced/accum.rs +++ /dev/null @@ -1,556 +0,0 @@ -//! Delayed-reduction product accumulators. -//! -//! Each accumulator widens field products into `u128` limbs so a batch of -//! products can be summed without intermediate modular reduction, then -//! reduced once via the owning field's `HasUnreducedOps` impl. - -use super::*; - -/// Accumulator for `Fp32 × u64` and `Fp32 × Fp32` products. -/// -/// Products are split into two 64-bit limbs stored as u128 slots. The second -/// limb is zero for `Fp32 × Fp32` products. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Fp32ProductAccum(pub [u128; 2]); - -impl Fp32ProductAccum { - /// Additive identity accumulator. - pub const ZERO: Self = Self([0; 2]); - - /// Reduce accumulated products to a canonical `Fp32

`. - #[inline] - pub fn reduce(self) -> Fp32

{ - let [s0, s1] = self.0; - let a = Fp32::

::from_canonical_u128_reduced(s0); - let b = Fp32::

::from_canonical_u128_reduced(s1); - let shift = Fp32::

::from_canonical_u32(Fp32::

::SHIFT64_MOD_P); - a + b * shift - } -} - -impl From> for Fp32ProductAccum { - #[inline] - fn from(x: Fp32

) -> Self { - Self([x.to_limbs() as u128, 0]) - } -} - -impl Add for Fp32ProductAccum { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - Self([ - self.0[0].wrapping_add(rhs.0[0]), - self.0[1].wrapping_add(rhs.0[1]), - ]) - } -} -impl AddAssign for Fp32ProductAccum { - #[inline] - fn add_assign(&mut self, rhs: Self) { - self.0[0] = self.0[0].wrapping_add(rhs.0[0]); - self.0[1] = self.0[1].wrapping_add(rhs.0[1]); - } -} -impl Sub for Fp32ProductAccum { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - Self([ - self.0[0].wrapping_sub(rhs.0[0]), - self.0[1].wrapping_sub(rhs.0[1]), - ]) - } -} -impl SubAssign for Fp32ProductAccum { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - self.0[0] = self.0[0].wrapping_sub(rhs.0[0]); - self.0[1] = self.0[1].wrapping_sub(rhs.0[1]); - } -} -impl Neg for Fp32ProductAccum { - type Output = Self; - #[inline] - fn neg(self) -> Self { - Self([self.0[0].wrapping_neg(), self.0[1].wrapping_neg()]) - } -} - -/// Accumulator for `FpExt4` products with delayed reduction. -/// -/// Each slot holds the unreduced u128 sum for one of the 4 ring-subfield -/// coefficients. The fused polynomial-multiply + φ(X)-reduction is already -/// applied in the formulas — only the per-coefficient Solinas reduction -/// (`from_canonical_u128_reduced`) is deferred. -/// -/// Headroom: each single product contributes at most 7 × P² ≈ 2^65 per -/// slot (slot 0 is the worst case). The u128 capacity of 2^128 allows up -/// to 2^63 accumulations before overflow. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct FpExt4Fp32ProductAccum(pub [u128; 4]); - -impl FpExt4Fp32ProductAccum { - /// Additive identity accumulator. - pub const ZERO: Self = Self([0; 4]); - - /// Reduce accumulated unreduced coefficients to a canonical - /// `FpExt4>`. - #[inline] - pub fn reduce(self) -> [Fp32

; 4] { - [ - Fp32::

::from_canonical_u128_reduced(self.0[0]), - Fp32::

::from_canonical_u128_reduced(self.0[1]), - Fp32::

::from_canonical_u128_reduced(self.0[2]), - Fp32::

::from_canonical_u128_reduced(self.0[3]), - ] - } -} - -impl Add for FpExt4Fp32ProductAccum { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - Self([ - self.0[0].wrapping_add(rhs.0[0]), - self.0[1].wrapping_add(rhs.0[1]), - self.0[2].wrapping_add(rhs.0[2]), - self.0[3].wrapping_add(rhs.0[3]), - ]) - } -} -impl AddAssign for FpExt4Fp32ProductAccum { - #[inline] - fn add_assign(&mut self, rhs: Self) { - self.0[0] = self.0[0].wrapping_add(rhs.0[0]); - self.0[1] = self.0[1].wrapping_add(rhs.0[1]); - self.0[2] = self.0[2].wrapping_add(rhs.0[2]); - self.0[3] = self.0[3].wrapping_add(rhs.0[3]); - } -} -impl Sub for FpExt4Fp32ProductAccum { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - Self([ - self.0[0].wrapping_sub(rhs.0[0]), - self.0[1].wrapping_sub(rhs.0[1]), - self.0[2].wrapping_sub(rhs.0[2]), - self.0[3].wrapping_sub(rhs.0[3]), - ]) - } -} -impl SubAssign for FpExt4Fp32ProductAccum { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - self.0[0] = self.0[0].wrapping_sub(rhs.0[0]); - self.0[1] = self.0[1].wrapping_sub(rhs.0[1]); - self.0[2] = self.0[2].wrapping_sub(rhs.0[2]); - self.0[3] = self.0[3].wrapping_sub(rhs.0[3]); - } -} -impl Neg for FpExt4Fp32ProductAccum { - type Output = Self; - #[inline] - fn neg(self) -> Self { - Self([ - self.0[0].wrapping_neg(), - self.0[1].wrapping_neg(), - self.0[2].wrapping_neg(), - self.0[3].wrapping_neg(), - ]) - } -} - -/// Accumulator for `Fp64 × u64` products (also used for `Fp64 × Fp64`). -/// -/// Each product is ≤ 128 bits, split into two u64 halves stored as u128 slots. -/// Headroom: 2^64 additions per slot before overflow. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Fp64ProductAccum(pub [u128; 2]); - -impl Fp64ProductAccum { - /// Additive identity accumulator. - pub const ZERO: Self = Self([0; 2]); - - /// Reduce accumulated products to a canonical `Fp64

`. - #[inline] - pub fn reduce(self) -> Fp64

{ - let [s0, s1] = self.0; - // s0 = Σ lo_i, s1 = Σ hi_i; value = s0 + s1 * 2^64 - let a = Fp64::

::solinas_reduce(s0); - let b = Fp64::

::solinas_reduce(s1); - let shift = Fp64::

::solinas_reduce(1u128 << 64); - let b_shifted = Fp64::

::solinas_reduce(b.mul_wide_u64(shift.to_limbs())); - a + b_shifted - } -} - -impl From> for Fp64ProductAccum { - #[inline] - fn from(x: Fp64

) -> Self { - Self([x.to_limbs() as u128, 0]) - } -} - -impl Add for Fp64ProductAccum { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - Self([ - self.0[0].wrapping_add(rhs.0[0]), - self.0[1].wrapping_add(rhs.0[1]), - ]) - } -} -impl AddAssign for Fp64ProductAccum { - #[inline] - fn add_assign(&mut self, rhs: Self) { - self.0[0] = self.0[0].wrapping_add(rhs.0[0]); - self.0[1] = self.0[1].wrapping_add(rhs.0[1]); - } -} -impl Sub for Fp64ProductAccum { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - Self([ - self.0[0].wrapping_sub(rhs.0[0]), - self.0[1].wrapping_sub(rhs.0[1]), - ]) - } -} -impl SubAssign for Fp64ProductAccum { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - self.0[0] = self.0[0].wrapping_sub(rhs.0[0]); - self.0[1] = self.0[1].wrapping_sub(rhs.0[1]); - } -} -impl Neg for Fp64ProductAccum { - type Output = Self; - #[inline] - fn neg(self) -> Self { - Self([self.0[0].wrapping_neg(), self.0[1].wrapping_neg()]) - } -} - -/// Accumulator for `FpExt2` products with delayed reduction. -/// -/// Each coefficient is stored as an `Fp64ProductAccum` (lo64/hi64 limb-split). -/// This avoids carry-chain arithmetic -- addition is `wrapping_add` per slot. -/// Reduction delegates to `Fp64ProductAccum::reduce` per coefficient. -/// -/// Headroom: each `Fp64ProductAccum` slot holds u64 halves in u128, -/// so 2^64 accumulations before overflow. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct FpExt2Fp64ProductAccum(pub [u128; 4]); - -impl FpExt2Fp64ProductAccum { - /// Additive identity accumulator. - pub const ZERO: Self = Self([0; 4]); - - /// Reduce accumulated products to a canonical `[Fp64

; 2]`. - #[inline] - pub fn reduce(self) -> [Fp64

; 2] { - [ - Fp64ProductAccum([self.0[0], self.0[1]]).reduce::

(), - Fp64ProductAccum([self.0[2], self.0[3]]).reduce::

(), - ] - } -} - -impl Add for FpExt2Fp64ProductAccum { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - Self([ - self.0[0].wrapping_add(rhs.0[0]), - self.0[1].wrapping_add(rhs.0[1]), - self.0[2].wrapping_add(rhs.0[2]), - self.0[3].wrapping_add(rhs.0[3]), - ]) - } -} -impl AddAssign for FpExt2Fp64ProductAccum { - #[inline] - fn add_assign(&mut self, rhs: Self) { - self.0[0] = self.0[0].wrapping_add(rhs.0[0]); - self.0[1] = self.0[1].wrapping_add(rhs.0[1]); - self.0[2] = self.0[2].wrapping_add(rhs.0[2]); - self.0[3] = self.0[3].wrapping_add(rhs.0[3]); - } -} -impl Sub for FpExt2Fp64ProductAccum { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - Self([ - self.0[0].wrapping_sub(rhs.0[0]), - self.0[1].wrapping_sub(rhs.0[1]), - self.0[2].wrapping_sub(rhs.0[2]), - self.0[3].wrapping_sub(rhs.0[3]), - ]) - } -} -impl SubAssign for FpExt2Fp64ProductAccum { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - self.0[0] = self.0[0].wrapping_sub(rhs.0[0]); - self.0[1] = self.0[1].wrapping_sub(rhs.0[1]); - self.0[2] = self.0[2].wrapping_sub(rhs.0[2]); - self.0[3] = self.0[3].wrapping_sub(rhs.0[3]); - } -} -impl Neg for FpExt2Fp64ProductAccum { - type Output = Self; - #[inline] - fn neg(self) -> Self { - Self([ - self.0[0].wrapping_neg(), - self.0[1].wrapping_neg(), - self.0[2].wrapping_neg(), - self.0[3].wrapping_neg(), - ]) - } -} - -/// Accumulator for `Fp128 × u64` products. -/// -/// Each `mul_wide_u64` produces 3 u64 limbs; stored as `[u128; 3]`. -/// Headroom: 2^64 additions per slot. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Fp128MulU64Accum(pub [u128; 3]); - -impl Fp128MulU64Accum { - /// Additive identity accumulator. - pub const ZERO: Self = Self([0; 3]); - - /// Reduce to canonical `Fp128

`. - #[inline] - pub fn reduce(self) -> Fp128

{ - let [s0, s1, s2] = self.0; - let c0 = s0 >> 64; - let r0 = s0 as u64; - let t1 = s1 + c0; - let r1 = t1 as u64; - let c1 = t1 >> 64; - let t2 = s2 + c1; - let r2 = t2 as u64; - let r3 = (t2 >> 64) as u64; - Fp128::

::solinas_reduce(&[r0, r1, r2, r3]) - } -} - -impl From> for Fp128MulU64Accum { - #[inline] - fn from(x: Fp128

) -> Self { - let [lo, hi] = x.to_limbs(); - Self([lo as u128, hi as u128, 0]) - } -} - -impl Add for Fp128MulU64Accum { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - Self([ - self.0[0] + rhs.0[0], - self.0[1] + rhs.0[1], - self.0[2] + rhs.0[2], - ]) - } -} -impl AddAssign for Fp128MulU64Accum { - #[inline] - fn add_assign(&mut self, rhs: Self) { - self.0[0] += rhs.0[0]; - self.0[1] += rhs.0[1]; - self.0[2] += rhs.0[2]; - } -} -impl Sub for Fp128MulU64Accum { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - Self([ - self.0[0].wrapping_sub(rhs.0[0]), - self.0[1].wrapping_sub(rhs.0[1]), - self.0[2].wrapping_sub(rhs.0[2]), - ]) - } -} -impl SubAssign for Fp128MulU64Accum { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - self.0[0] = self.0[0].wrapping_sub(rhs.0[0]); - self.0[1] = self.0[1].wrapping_sub(rhs.0[1]); - self.0[2] = self.0[2].wrapping_sub(rhs.0[2]); - } -} -impl Neg for Fp128MulU64Accum { - type Output = Self; - #[inline] - fn neg(self) -> Self { - Self([ - self.0[0].wrapping_neg(), - self.0[1].wrapping_neg(), - self.0[2].wrapping_neg(), - ]) - } -} - -/// Accumulator for `Fp128 × Fp128` products. -/// -/// Each `mul_wide` produces 4 u64 limbs; stored as `[u128; 4]`. -/// Headroom: 2^64 additions per slot. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Fp128ProductAccum(pub [u128; 4]); - -impl Fp128ProductAccum { - /// Additive identity accumulator. - pub const ZERO: Self = Self([0; 4]); - - /// Reduce to canonical `Fp128

`. - #[inline] - pub fn reduce(self) -> Fp128

{ - let [s0, s1, s2, s3] = self.0; - let c0 = s0 >> 64; - let r0 = s0 as u64; - let t1 = s1 + c0; - let r1 = t1 as u64; - let c1 = t1 >> 64; - let t2 = s2 + c1; - let r2 = t2 as u64; - let c2 = t2 >> 64; - let t3 = s3 + c2; - let r3 = t3 as u64; - let r4 = (t3 >> 64) as u64; - Fp128::

::solinas_reduce(&[r0, r1, r2, r3, r4]) - } -} - -impl From> for Fp128ProductAccum { - #[inline] - fn from(x: Fp128

) -> Self { - let [lo, hi] = x.to_limbs(); - Self([lo as u128, hi as u128, 0, 0]) - } -} - -impl Add for Fp128ProductAccum { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - Self([ - self.0[0].wrapping_add(rhs.0[0]), - self.0[1].wrapping_add(rhs.0[1]), - self.0[2].wrapping_add(rhs.0[2]), - self.0[3].wrapping_add(rhs.0[3]), - ]) - } -} -impl AddAssign for Fp128ProductAccum { - #[inline] - fn add_assign(&mut self, rhs: Self) { - self.0[0] = self.0[0].wrapping_add(rhs.0[0]); - self.0[1] = self.0[1].wrapping_add(rhs.0[1]); - self.0[2] = self.0[2].wrapping_add(rhs.0[2]); - self.0[3] = self.0[3].wrapping_add(rhs.0[3]); - } -} -impl Sub for Fp128ProductAccum { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - Self([ - self.0[0].wrapping_sub(rhs.0[0]), - self.0[1].wrapping_sub(rhs.0[1]), - self.0[2].wrapping_sub(rhs.0[2]), - self.0[3].wrapping_sub(rhs.0[3]), - ]) - } -} -impl SubAssign for Fp128ProductAccum { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - self.0[0] = self.0[0].wrapping_sub(rhs.0[0]); - self.0[1] = self.0[1].wrapping_sub(rhs.0[1]); - self.0[2] = self.0[2].wrapping_sub(rhs.0[2]); - self.0[3] = self.0[3].wrapping_sub(rhs.0[3]); - } -} -impl Neg for Fp128ProductAccum { - type Output = Self; - #[inline] - fn neg(self) -> Self { - Self([ - self.0[0].wrapping_neg(), - self.0[1].wrapping_neg(), - self.0[2].wrapping_neg(), - self.0[3].wrapping_neg(), - ]) - } -} - -/// Pair accumulator for extension fields. -/// -/// Wraps two base-field accumulators `(c0, c1)` component-wise. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AccumPair(pub A, pub A); - -impl Add for AccumPair { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - Self(self.0 + rhs.0, self.1 + rhs.1) - } -} -impl AddAssign for AccumPair { - #[inline] - fn add_assign(&mut self, rhs: Self) { - self.0 += rhs.0; - self.1 += rhs.1; - } -} -impl Sub for AccumPair { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - Self(self.0 - rhs.0, self.1 - rhs.1) - } -} -impl SubAssign for AccumPair { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - self.0 -= rhs.0; - self.1 -= rhs.1; - } -} -impl Neg for AccumPair { - type Output = Self; - #[inline] - fn neg(self) -> Self { - Self(-self.0, -self.1) - } -} - -use crate::native_algebra::impl_native_additive; - -impl_native_additive!(impl[] Fp32ProductAccum { zero: Fp32ProductAccum([0; 2]), is_zero(x): *x == Self::zero() }); -impl_native_additive!(impl[] Fp64ProductAccum { zero: Fp64ProductAccum([0; 2]), is_zero(x): *x == Self::zero() }); -impl_native_additive!(impl[] Fp128MulU64Accum { zero: Fp128MulU64Accum([0; 3]), is_zero(x): *x == Self::zero() }); -impl_native_additive!(impl[] Fp128ProductAccum { zero: Fp128ProductAccum([0; 4]), is_zero(x): *x == Self::zero() }); -impl_native_additive!(impl[] FpExt4Fp32ProductAccum { zero: FpExt4Fp32ProductAccum([0; 4]), is_zero(x): *x == Self::zero() }); -impl_native_additive!(impl[] FpExt2Fp64ProductAccum { zero: FpExt2Fp64ProductAccum([0; 4]), is_zero(x): *x == Self::zero() }); -impl_native_additive!(impl[A: crate::AdditiveGroup] AccumPair { - zero: Self(A::zero(), A::zero()), - is_zero(x): ::num_traits::Zero::is_zero(&x.0) && ::num_traits::Zero::is_zero(&x.1), -}); diff --git a/crates/jolt-field/src/unreduced/mod.rs b/crates/jolt-field/src/unreduced/mod.rs deleted file mode 100644 index 59b8ee5a15..0000000000 --- a/crates/jolt-field/src/unreduced/mod.rs +++ /dev/null @@ -1,807 +0,0 @@ -//! Wide unreduced field accumulators for carry-free signed addition. -//! -//! Each type splits a canonical field element into 16-bit limbs stored in -//! `i32` slots. Addition and negation are element-wise i32 ops — no carry -//! propagation, no modular reduction. Reduction back to canonical form -//! happens once after accumulation via -//! [`reduce`](crate::unreduced::Fp128x8i32::reduce). -//! -//! The i32 overflow budget is `i32::MAX / u16::MAX ≈ 32,769` signed -//! additions before any limb can overflow. - -#![cfg_attr( - target_arch = "aarch64", - expect( - clippy::undocumented_unsafe_blocks, - reason = "ported NEON accumulator operations retain their audited lane invariants" - ) -)] - -use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; - -use crate::{AdditiveGroup, CanonicalField, FieldCore}; - -use super::prime::{Fp128, Fp32, Fp64}; - -mod accum; -pub use accum::*; - -/// Wide unreduced accumulator for `Fp32`: 2 × i32 limbs (16-bit data each). -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(C)] -pub struct Fp32x2i32(pub [i32; 2]); - -impl Fp32x2i32 { - /// Additive identity accumulator. - pub const ZERO: Self = Self([0; 2]); - - /// Returns the zero accumulator. - #[inline] - pub fn zero() -> Self { - Self::ZERO - } -} - -impl From> for Fp32x2i32 { - #[inline] - fn from(x: Fp32

) -> Self { - let v = x.0; - Self([(v & 0xFFFF) as i32, (v >> 16) as i32]) - } -} - -impl Fp32x2i32 { - /// Multiply every limb by a small signed scalar. - /// - /// Safe when `|small| * max_limb_magnitude` fits in i32. After `From`, - /// limbs are in `[0, 0xFFFF]`, so `|small| ≤ 32_767` is safe for a single - /// product. For accumulation of `k` scaled values, require - /// `k * |small| * 0xFFFF < i32::MAX`, i.e. roughly `k * |small| < 32_768`. - #[inline] - pub fn scale_i32(self, small: i32) -> Self { - Self([self.0[0] * small, self.0[1] * small]) - } - - /// Reduce back to canonical `Fp32

`. - /// - /// Carry-propagates the i32 limbs into a signed value, normalizes to - /// `[0, p)`, and returns the canonical field element. - #[inline] - pub fn reduce(self) -> Fp32

{ - let [l0, l1] = self.0; - // Carry-propagate: value = l0 + l1 * 2^16 - let wide = l0 as i64 + (l1 as i64) * (1i64 << 16); - // Normalize to [0, p) - let p = P as i64; - let normalized = ((wide % p) + p) % p; - Fp32::from_canonical_u32(normalized as u32) - } -} - -impl Add for Fp32x2i32 { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - Self([self.0[0] + rhs.0[0], self.0[1] + rhs.0[1]]) - } -} - -impl AddAssign for Fp32x2i32 { - #[inline] - fn add_assign(&mut self, rhs: Self) { - self.0[0] += rhs.0[0]; - self.0[1] += rhs.0[1]; - } -} - -impl Sub for Fp32x2i32 { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - Self([self.0[0] - rhs.0[0], self.0[1] - rhs.0[1]]) - } -} - -impl SubAssign for Fp32x2i32 { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - self.0[0] -= rhs.0[0]; - self.0[1] -= rhs.0[1]; - } -} - -impl Neg for Fp32x2i32 { - type Output = Self; - #[inline] - fn neg(self) -> Self { - Self([-self.0[0], -self.0[1]]) - } -} - -/// Wide unreduced accumulator for `Fp64`: 4 × i32 limbs (16-bit data each). -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(C)] -pub struct Fp64x4i32(pub [i32; 4]); - -impl Fp64x4i32 { - /// Additive identity accumulator. - pub const ZERO: Self = Self([0; 4]); - - /// Returns the zero accumulator. - #[inline] - pub fn zero() -> Self { - Self::ZERO - } -} - -impl From> for Fp64x4i32 { - #[inline] - fn from(x: Fp64

) -> Self { - let v = x.0; - Self([ - (v & 0xFFFF) as i32, - ((v >> 16) & 0xFFFF) as i32, - ((v >> 32) & 0xFFFF) as i32, - ((v >> 48) & 0xFFFF) as i32, - ]) - } -} - -impl Fp64x4i32 { - /// Multiply every limb by a small signed scalar. See [`Fp32x2i32::scale_i32`]. - #[inline] - pub fn scale_i32(self, small: i32) -> Self { - Self([ - self.0[0] * small, - self.0[1] * small, - self.0[2] * small, - self.0[3] * small, - ]) - } - - /// Reduce back to canonical `Fp64

`. - #[inline] - pub fn reduce(self) -> Fp64

{ - let [l0, l1, l2, l3] = self.0; - // Carry-propagate: value = l0 + l1*2^16 + l2*2^32 + l3*2^48 - let wide = l0 as i128 - + (l1 as i128) * (1i128 << 16) - + (l2 as i128) * (1i128 << 32) - + (l3 as i128) * (1i128 << 48); - let p = P as i128; - let normalized = ((wide % p) + p) % p; - Fp64::

::from_canonical_u64(normalized as u64) - } -} - -#[cfg(target_arch = "aarch64")] -impl Add for Fp64x4i32 { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - unsafe { - use std::arch::aarch64::*; - let a = vld1q_s32(self.0.as_ptr()); - let b = vld1q_s32(rhs.0.as_ptr()); - let mut out = [0i32; 4]; - vst1q_s32(out.as_mut_ptr(), vaddq_s32(a, b)); - Self(out) - } - } -} - -#[cfg(target_arch = "aarch64")] -impl AddAssign for Fp64x4i32 { - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -#[cfg(target_arch = "aarch64")] -impl Sub for Fp64x4i32 { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - unsafe { - use std::arch::aarch64::*; - let a = vld1q_s32(self.0.as_ptr()); - let b = vld1q_s32(rhs.0.as_ptr()); - let mut out = [0i32; 4]; - vst1q_s32(out.as_mut_ptr(), vsubq_s32(a, b)); - Self(out) - } - } -} - -#[cfg(target_arch = "aarch64")] -impl SubAssign for Fp64x4i32 { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -#[cfg(target_arch = "aarch64")] -impl Neg for Fp64x4i32 { - type Output = Self; - #[inline] - fn neg(self) -> Self { - unsafe { - use std::arch::aarch64::*; - let a = vld1q_s32(self.0.as_ptr()); - let mut out = [0i32; 4]; - vst1q_s32(out.as_mut_ptr(), vnegq_s32(a)); - Self(out) - } - } -} - -#[cfg(not(target_arch = "aarch64"))] -impl Add for Fp64x4i32 { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - Self([ - self.0[0] + rhs.0[0], - self.0[1] + rhs.0[1], - self.0[2] + rhs.0[2], - self.0[3] + rhs.0[3], - ]) - } -} - -#[cfg(not(target_arch = "aarch64"))] -impl AddAssign for Fp64x4i32 { - #[inline] - fn add_assign(&mut self, rhs: Self) { - self.0[0] += rhs.0[0]; - self.0[1] += rhs.0[1]; - self.0[2] += rhs.0[2]; - self.0[3] += rhs.0[3]; - } -} - -#[cfg(not(target_arch = "aarch64"))] -impl Sub for Fp64x4i32 { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - Self([ - self.0[0] - rhs.0[0], - self.0[1] - rhs.0[1], - self.0[2] - rhs.0[2], - self.0[3] - rhs.0[3], - ]) - } -} - -#[cfg(not(target_arch = "aarch64"))] -impl SubAssign for Fp64x4i32 { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - self.0[0] -= rhs.0[0]; - self.0[1] -= rhs.0[1]; - self.0[2] -= rhs.0[2]; - self.0[3] -= rhs.0[3]; - } -} - -#[cfg(not(target_arch = "aarch64"))] -impl Neg for Fp64x4i32 { - type Output = Self; - #[inline] - fn neg(self) -> Self { - Self([-self.0[0], -self.0[1], -self.0[2], -self.0[3]]) - } -} - -/// Wide unreduced accumulator for `Fp128`: 8 × i32 limbs (16-bit data each). -/// -/// On AVX2, one element fits a single 256-bit YMM register. On NEON, it -/// spans two 128-bit Q registers. All arithmetic is carry-free element-wise -/// i32 operations. -#[cfg_attr(feature = "allocative", derive(allocative::Allocative))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(C)] -pub struct Fp128x8i32(pub [i32; 8]); - -impl Fp128x8i32 { - /// Additive identity accumulator. - pub const ZERO: Self = Self([0; 8]); - - /// Returns the zero accumulator. - #[inline] - pub fn zero() -> Self { - Self::ZERO - } -} - -impl From> for Fp128x8i32 { - #[inline] - fn from(x: Fp128

) -> Self { - let lo = x.0[0]; - let hi = x.0[1]; - Self([ - (lo & 0xFFFF) as i32, - ((lo >> 16) & 0xFFFF) as i32, - ((lo >> 32) & 0xFFFF) as i32, - ((lo >> 48) & 0xFFFF) as i32, - (hi & 0xFFFF) as i32, - ((hi >> 16) & 0xFFFF) as i32, - ((hi >> 32) & 0xFFFF) as i32, - ((hi >> 48) & 0xFFFF) as i32, - ]) - } -} - -impl Fp128x8i32 { - /// Multiply every limb by a small signed scalar. See [`Fp32x2i32::scale_i32`]. - #[inline] - pub fn scale_i32(self, small: i32) -> Self { - Self([ - self.0[0] * small, - self.0[1] * small, - self.0[2] * small, - self.0[3] * small, - self.0[4] * small, - self.0[5] * small, - self.0[6] * small, - self.0[7] * small, - ]) - } - - /// Reduce back to canonical `Fp128

`. - /// - /// Carry-propagates the 8 × i32 limbs into unsigned u64 limbs, then - /// applies Solinas reduction. - #[inline] - pub fn reduce(self) -> Fp128

{ - let limbs = self.0; - - // Carry-propagate from low to high, accumulating into i64 slots. - // Each i32 limb can be in [-32769*65535, 32769*65535] ≈ ±2^31. - // After propagation, each 16-bit "digit" is in [0, 65535] and we - // may have a signed residual in the top that overflows 128 bits. - let mut carry: i64 = 0; - let mut digits = [0u16; 8]; - for i in 0..8 { - let v = limbs[i] as i64 + carry; - // Arithmetic right-shift to propagate sign correctly - digits[i] = (v & 0xFFFF) as u16; - carry = v >> 16; - } - - // Reassemble into u64 limbs - let lo = digits[0] as u64 - | (digits[1] as u64) << 16 - | (digits[2] as u64) << 32 - | (digits[3] as u64) << 48; - let hi = digits[4] as u64 - | (digits[5] as u64) << 16 - | (digits[6] as u64) << 32 - | (digits[7] as u64) << 48; - - // p = 2^128 - c, so 2^128 ≡ c (mod p). - // value = lo + hi*2^64 + carry*2^128 ≡ lo + hi*2^64 + carry*c (mod p). - let c = Fp128::

::C_LO; - match carry.cmp(&0) { - std::cmp::Ordering::Equal => { - Fp128::

::from_canonical_u128_reduced(lo as u128 | (hi as u128) << 64) - } - std::cmp::Ordering::Greater => Fp128::

::solinas_reduce(&[lo, hi, carry as u64]), - std::cmp::Ordering::Less => { - // carry < 0: value = base - |carry|*c. - let neg_carry = (-carry) as u64; - let sub = neg_carry as u128 * c as u128; - let base = lo as u128 | (hi as u128) << 64; - if base >= sub { - Fp128::

::from_canonical_u128_reduced(base - sub) - } else { - let diff = sub - base; - Fp128::

::from_canonical_u128_reduced(P - diff) - } - } - } - } -} - -#[cfg(target_arch = "aarch64")] -impl Add for Fp128x8i32 { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - unsafe { - use std::arch::aarch64::*; - let a0 = vld1q_s32(self.0.as_ptr()); - let a1 = vld1q_s32(self.0.as_ptr().add(4)); - let b0 = vld1q_s32(rhs.0.as_ptr()); - let b1 = vld1q_s32(rhs.0.as_ptr().add(4)); - let mut out = [0i32; 8]; - vst1q_s32(out.as_mut_ptr(), vaddq_s32(a0, b0)); - vst1q_s32(out.as_mut_ptr().add(4), vaddq_s32(a1, b1)); - Self(out) - } - } -} - -#[cfg(target_arch = "aarch64")] -impl AddAssign for Fp128x8i32 { - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -#[cfg(target_arch = "aarch64")] -impl Sub for Fp128x8i32 { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - unsafe { - use std::arch::aarch64::*; - let a0 = vld1q_s32(self.0.as_ptr()); - let a1 = vld1q_s32(self.0.as_ptr().add(4)); - let b0 = vld1q_s32(rhs.0.as_ptr()); - let b1 = vld1q_s32(rhs.0.as_ptr().add(4)); - let mut out = [0i32; 8]; - vst1q_s32(out.as_mut_ptr(), vsubq_s32(a0, b0)); - vst1q_s32(out.as_mut_ptr().add(4), vsubq_s32(a1, b1)); - Self(out) - } - } -} - -#[cfg(target_arch = "aarch64")] -impl SubAssign for Fp128x8i32 { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -#[cfg(target_arch = "aarch64")] -impl Neg for Fp128x8i32 { - type Output = Self; - #[inline] - fn neg(self) -> Self { - unsafe { - use std::arch::aarch64::*; - let a0 = vld1q_s32(self.0.as_ptr()); - let a1 = vld1q_s32(self.0.as_ptr().add(4)); - let mut out = [0i32; 8]; - vst1q_s32(out.as_mut_ptr(), vnegq_s32(a0)); - vst1q_s32(out.as_mut_ptr().add(4), vnegq_s32(a1)); - Self(out) - } - } -} - -#[cfg(not(target_arch = "aarch64"))] -impl Add for Fp128x8i32 { - type Output = Self; - #[inline] - fn add(self, rhs: Self) -> Self { - Self([ - self.0[0] + rhs.0[0], - self.0[1] + rhs.0[1], - self.0[2] + rhs.0[2], - self.0[3] + rhs.0[3], - self.0[4] + rhs.0[4], - self.0[5] + rhs.0[5], - self.0[6] + rhs.0[6], - self.0[7] + rhs.0[7], - ]) - } -} - -#[cfg(not(target_arch = "aarch64"))] -impl AddAssign for Fp128x8i32 { - #[inline] - fn add_assign(&mut self, rhs: Self) { - self.0[0] += rhs.0[0]; - self.0[1] += rhs.0[1]; - self.0[2] += rhs.0[2]; - self.0[3] += rhs.0[3]; - self.0[4] += rhs.0[4]; - self.0[5] += rhs.0[5]; - self.0[6] += rhs.0[6]; - self.0[7] += rhs.0[7]; - } -} - -#[cfg(not(target_arch = "aarch64"))] -impl Sub for Fp128x8i32 { - type Output = Self; - #[inline] - fn sub(self, rhs: Self) -> Self { - Self([ - self.0[0] - rhs.0[0], - self.0[1] - rhs.0[1], - self.0[2] - rhs.0[2], - self.0[3] - rhs.0[3], - self.0[4] - rhs.0[4], - self.0[5] - rhs.0[5], - self.0[6] - rhs.0[6], - self.0[7] - rhs.0[7], - ]) - } -} - -#[cfg(not(target_arch = "aarch64"))] -impl SubAssign for Fp128x8i32 { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - self.0[0] -= rhs.0[0]; - self.0[1] -= rhs.0[1]; - self.0[2] -= rhs.0[2]; - self.0[3] -= rhs.0[3]; - self.0[4] -= rhs.0[4]; - self.0[5] -= rhs.0[5]; - self.0[6] -= rhs.0[6]; - self.0[7] -= rhs.0[7]; - } -} - -#[cfg(not(target_arch = "aarch64"))] -impl Neg for Fp128x8i32 { - type Output = Self; - #[inline] - fn neg(self) -> Self { - Self([ - -self.0[0], -self.0[1], -self.0[2], -self.0[3], -self.0[4], -self.0[5], -self.0[6], - -self.0[7], - ]) - } -} - -/// Reduce a wide unreduced accumulator back to a canonical field element. -pub trait ReduceTo { - /// Carry-propagate and reduce to a canonical field element. - fn reduce(self) -> F; - - /// Scale each element by `small`. - fn scale_i32(self, small: i32) -> Self; -} - -impl ReduceTo> for Fp32x2i32 { - #[inline] - fn reduce(self) -> Fp32

{ - Fp32x2i32::reduce::

(self) - } - - #[inline] - fn scale_i32(self, small: i32) -> Self { - self.scale_i32(small) - } -} - -impl ReduceTo> for Fp64x4i32 { - #[inline] - fn reduce(self) -> Fp64

{ - Fp64x4i32::reduce::

(self) - } - - #[inline] - fn scale_i32(self, small: i32) -> Self { - self.scale_i32(small) - } -} - -impl ReduceTo> for Fp128x8i32 { - #[inline] - fn reduce(self) -> Fp128

{ - Fp128x8i32::reduce::

(self) - } - - #[inline] - fn scale_i32(self, small: i32) -> Self { - self.scale_i32(small) - } -} - -/// Precomputed fold context for `FpExt4>`. -/// -/// Stores a 4×4 multiplication matrix derived from the challenge `r`, -/// enabling fold via 4 scalar multiply-accumulates per coefficient -/// instead of the general 22-product ring multiplication. -#[derive(Debug, Clone, Copy)] -pub struct FoldMatrixFp32(pub(crate) [[u32; 4]; 4]); - -/// Precomputed fold context for `FpExt2, C>`. -/// -/// Stores the 2×2 "multiply by the challenge `r`" matrix in the `[1, u]` -/// basis (`u² = NR`) as canonical `u64` limbs. Folding then uses two -/// base-field products per output coordinate with a single delayed -/// reduction, instead of the generic per-element Karatsuba multiply that -/// reduces three times. -#[derive(Debug, Clone, Copy)] -pub struct FoldMatrixFp64(pub(crate) [[u64; 2]; 2]); - -/// Per-element fold optimization trait. -/// -/// Allows field types to precompute a fold context from challenge `r` -/// (e.g. a multiplication matrix) and apply it per-element. The loop -/// structure and parallelism live in the caller (`fold_evals_in_place`). -pub trait HasOptimizedFold: FieldCore { - /// Precomputed context for folding by a fixed challenge `r`. - type FoldCtx: Copy + Send + Sync; - - /// Build the fold context from challenge `r`. - fn precompute_fold(r: Self) -> Self::FoldCtx; - - /// Fold one element pair: `even + r*(odd - even)`. - fn fold_one(ctx: &Self::FoldCtx, even: Self, odd: Self) -> Self; -} - -/// Multi-level unreduced multiplication hierarchy. -/// -/// Provides `field × u64` and `field × field` widening multiplies that return -/// accumulator types supporting carry-free addition. Reduction back to a -/// canonical field element happens once after accumulation. -pub trait HasUnreducedOps: FieldCore { - /// Accumulator for `self × u64` products (narrower than full product). - type MulU64Accum: AdditiveGroup; - /// Accumulator for `self × self` products. - type ProductAccum: AdditiveGroup; - - /// Whether delayed reduction over `ProductAccum` is exact relative to - /// per-term `Mul` for the small product batches used by inner products. - /// - /// When `true`, `reduce_product_accum(sum_i mul_to_product_accum(a_i, b_i))` - /// equals `sum_i a_i * b_i` for batch sizes within the accumulator's - /// non-wrapping headroom. The conservative default is `false`; a field opts - /// in only once its accumulator is proven exact (see `FpExt4` - /// and `FpExt2`). Fields that leave it `false` keep the per-term reduce - /// path, so callers that must stay byte-identical to `Mul` are unaffected. - const DELAYED_PRODUCT_SUM_IS_EXACT: bool = false; - - /// Widening `self × small` with no reduction. - fn mul_u64_unreduced(self, small: u64) -> Self::MulU64Accum; - /// Widening `self × other` with no reduction. - fn mul_to_product_accum(self, other: Self) -> Self::ProductAccum; - - /// Reduce a narrow-mul accumulator to a canonical field element. - fn reduce_mul_u64_accum(accum: Self::MulU64Accum) -> Self; - /// Reduce a full-product accumulator to a canonical field element. - fn reduce_product_accum(accum: Self::ProductAccum) -> Self; -} - -macro_rules! impl_default_optimized_fold { - ($base:ident<$p:ident: $pty:ty>) => { - impl HasOptimizedFold for $base<$p> { - type FoldCtx = Self; - #[inline] - fn precompute_fold(r: Self) -> Self { - r - } - #[inline] - fn fold_one(r: &Self, even: Self, odd: Self) -> Self { - even + *r * (odd - even) - } - } - }; -} - -impl_default_optimized_fold!(Fp64); -impl_default_optimized_fold!(Fp32); -impl_default_optimized_fold!(Fp128); - -impl HasUnreducedOps for Fp64

{ - type MulU64Accum = Fp64ProductAccum; - type ProductAccum = Fp64ProductAccum; - - #[inline] - fn mul_u64_unreduced(self, small: u64) -> Fp64ProductAccum { - let wide = self.mul_wide_u64(small); - Fp64ProductAccum([wide & u64::MAX as u128, wide >> 64]) - } - - #[inline] - fn mul_to_product_accum(self, other: Self) -> Fp64ProductAccum { - let wide = self.mul_wide(other); - Fp64ProductAccum([wide & u64::MAX as u128, wide >> 64]) - } - - #[inline] - fn reduce_mul_u64_accum(accum: Fp64ProductAccum) -> Self { - accum.reduce::

() - } - - #[inline] - fn reduce_product_accum(accum: Fp64ProductAccum) -> Self { - accum.reduce::

() - } -} - -impl HasUnreducedOps for Fp32

{ - type MulU64Accum = Fp32ProductAccum; - type ProductAccum = Fp32ProductAccum; - - #[inline] - fn mul_u64_unreduced(self, small: u64) -> Fp32ProductAccum { - let wide = (self.to_limbs() as u128) * (small as u128); - Fp32ProductAccum([wide & u64::MAX as u128, wide >> 64]) - } - - #[inline] - fn mul_to_product_accum(self, other: Self) -> Fp32ProductAccum { - Fp32ProductAccum([self.mul_wide(other) as u128, 0]) - } - - #[inline] - fn reduce_mul_u64_accum(accum: Fp32ProductAccum) -> Self { - accum.reduce::

() - } - - #[inline] - fn reduce_product_accum(accum: Fp32ProductAccum) -> Self { - accum.reduce::

() - } -} - -impl HasUnreducedOps for Fp128

{ - type MulU64Accum = Fp128MulU64Accum; - type ProductAccum = Fp128ProductAccum; - - #[inline] - fn mul_u64_unreduced(self, small: u64) -> Fp128MulU64Accum { - let [lo, mid, hi] = self.mul_wide_u64(small); - Fp128MulU64Accum([lo as u128, mid as u128, hi as u128]) - } - - #[inline] - fn mul_to_product_accum(self, other: Self) -> Fp128ProductAccum { - let [r0, r1, r2, r3] = self.mul_wide(other); - Fp128ProductAccum([r0 as u128, r1 as u128, r2 as u128, r3 as u128]) - } - - #[inline] - fn reduce_mul_u64_accum(accum: Fp128MulU64Accum) -> Self { - accum.reduce::

() - } - - #[inline] - fn reduce_product_accum(accum: Fp128ProductAccum) -> Self { - accum.reduce::

() - } -} - -/// Element-wise scaling of a wide accumulator by a small signed integer. -/// Associates a field type with its wide unreduced accumulator. -pub trait HasWide: FieldCore { - /// The wide accumulator type. - type Wide: AdditiveGroup + From + ReduceTo; - - /// Convert `self` to wide form and scale every limb by `small`. - /// - /// Equivalent to `Self::Wide::from(self).scale_i32(small)` but avoids - /// the trait-method ambiguity at call sites. - #[inline] - fn mul_small_to_wide(self, small: i32) -> Self::Wide { - Self::Wide::from(self).scale_i32(small) - } -} - -impl HasWide for Fp32

{ - type Wide = Fp32x2i32; -} - -impl HasWide for Fp64

{ - type Wide = Fp64x4i32; -} - -impl HasWide for Fp128

{ - type Wide = Fp128x8i32; -} - -#[cfg(test)] -mod tests; - -use crate::native_algebra::impl_native_additive; - -impl_native_additive!(impl[] Fp32x2i32 { zero: Fp32x2i32([0; 2]), is_zero(x): *x == Self::zero() }); -impl_native_additive!(impl[] Fp64x4i32 { zero: Fp64x4i32([0; 4]), is_zero(x): *x == Self::zero() }); -impl_native_additive!(impl[] Fp128x8i32 { zero: Fp128x8i32([0; 8]), is_zero(x): *x == Self::zero() }); diff --git a/crates/jolt-field/src/unreduced/tests.rs b/crates/jolt-field/src/unreduced/tests.rs deleted file mode 100644 index ff918edd8e..0000000000 --- a/crates/jolt-field/src/unreduced/tests.rs +++ /dev/null @@ -1,262 +0,0 @@ -#![expect( - clippy::unreadable_literal, - reason = "regression tests retain copied modulus constants" -)] - -use super::*; -use crate::FieldCore; -use crate::{Prime128Offset275, Prime24Offset3, Prime40Offset195}; -use rand::rngs::StdRng; -use rand::SeedableRng; -use rand_core::RngCore; - -type F128 = Prime128Offset275; -type F32 = Prime24Offset3; -type F64 = Prime40Offset195; - -const P128: u128 = 0xfffffffffffffffffffffffffffffeed; -const P32: u32 = (1 << 24) - 3; -const P64: u64 = (1 << 40) - 195; - -#[test] -fn fp128_roundtrip() { - let mut rng = StdRng::seed_from_u64(0xdead_1234); - for _ in 0..1000 { - let a: F128 = FieldCore::random(&mut rng); - let wide = Fp128x8i32::from(a); - let back = wide.reduce::(); - assert_eq!(a, back, "roundtrip failed for {a:?}"); - } -} - -#[test] -fn fp128_accumulate_matches_scalar() { - let mut rng = StdRng::seed_from_u64(0xbeef_cafe_4321); - let n = 1000; - let vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - - let scalar_sum = vals.iter().fold(F128::zero(), |acc, &x| acc + x); - - let wide_sum = vals - .iter() - .fold(Fp128x8i32::zero(), |acc, &x| acc + Fp128x8i32::from(x)); - let reduced = wide_sum.reduce::(); - - assert_eq!(scalar_sum, reduced); -} - -#[test] -fn fp128_add_sub_neg_match_scalar() { - let mut rng = StdRng::seed_from_u64(0x1122_3344_5566); - for _ in 0..500 { - let a: F128 = FieldCore::random(&mut rng); - let b: F128 = FieldCore::random(&mut rng); - - let wa = Fp128x8i32::from(a); - let wb = Fp128x8i32::from(b); - - assert_eq!((wa + wb).reduce::(), a + b); - assert_eq!((wa - wb).reduce::(), a - b); - assert_eq!((-wa).reduce::(), -a); - } -} - -#[test] -fn fp128_mixed_add_sub_stress() { - let mut rng = StdRng::seed_from_u64(0xaaaa_bbbb_cccc); - let n = 500; - let vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - - let mut scalar = F128::zero(); - let mut wide = Fp128x8i32::zero(); - for (i, &v) in vals.iter().enumerate() { - let wv = Fp128x8i32::from(v); - if i % 3 == 0 { - scalar -= v; - wide -= wv; - } else { - scalar += v; - wide += wv; - } - } - assert_eq!(wide.reduce::(), scalar); -} - -#[test] -fn fp32_roundtrip() { - let mut rng = StdRng::seed_from_u64(0x3232_3232); - for _ in 0..1000 { - let a: F32 = FieldCore::random(&mut rng); - let wide = Fp32x2i32::from(a); - let back = wide.reduce::(); - assert_eq!(a, back); - } -} - -#[test] -fn fp32_accumulate_matches_scalar() { - let mut rng = StdRng::seed_from_u64(0x3232_abcd); - let n = 1000; - let vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - - let scalar_sum = vals.iter().fold(F32::zero(), |acc, &x| acc + x); - let wide_sum = vals - .iter() - .fold(Fp32x2i32::zero(), |acc, &x| acc + Fp32x2i32::from(x)); - assert_eq!(wide_sum.reduce::(), scalar_sum); -} - -#[test] -fn fp64_roundtrip() { - let mut rng = StdRng::seed_from_u64(0x6464_6464); - for _ in 0..1000 { - let a: F64 = FieldCore::random(&mut rng); - let wide = Fp64x4i32::from(a); - let back = wide.reduce::(); - assert_eq!(a, back); - } -} - -#[test] -fn fp64_accumulate_matches_scalar() { - let mut rng = StdRng::seed_from_u64(0x6464_beef); - let n = 1000; - let vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - - let scalar_sum = vals.iter().fold(F64::zero(), |acc, &x| acc + x); - let wide_sum = vals - .iter() - .fold(Fp64x4i32::zero(), |acc, &x| acc + Fp64x4i32::from(x)); - assert_eq!(wide_sum.reduce::(), scalar_sum); -} - -#[test] -fn fp64_product_accum_matches_scalar() { - let mut rng = StdRng::seed_from_u64(0x6464_4444); - let n = 500; - let a_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - let b_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - - let scalar_sum: F64 = a_vals - .iter() - .zip(b_vals.iter()) - .fold(F64::zero(), |acc, (&a, &b)| acc + a * b); - - let accum_sum = a_vals - .iter() - .zip(b_vals.iter()) - .fold(Fp64ProductAccum::ZERO, |acc, (&a, &b)| { - acc + a.mul_to_product_accum(b) - }); - assert_eq!(F64::reduce_product_accum(accum_sum), scalar_sum); -} - -#[test] -fn fp64_ext2_product_accum_matches_scalar() { - use crate::Ext2; - - type E = Ext2; - - let mut rng = StdRng::seed_from_u64(0x6464_4445); - let n = 500; - let a_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - let b_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - - let scalar_sum: E = a_vals - .iter() - .zip(b_vals.iter()) - .fold(E::zero(), |acc, (&a, &b)| acc + a * b); - - let accum_sum = a_vals.iter().zip(b_vals.iter()).fold( - <::ProductAccum as num_traits::Zero>::zero(), - |acc, (&a, &b)| acc + a.mul_to_product_accum(b), - ); - assert_eq!(E::reduce_product_accum(accum_sum), scalar_sum); -} - -#[test] -fn fp64_mul_u64_accum_matches_scalar() { - let mut rng = StdRng::seed_from_u64(0x6464_5555); - let n = 500; - let a_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - let b_vals: Vec = (0..n).map(|_| rng.next_u64() >> 32).collect(); - - let scalar_sum: F64 = a_vals - .iter() - .zip(b_vals.iter()) - .fold(F64::zero(), |acc, (&a, &b)| acc + a * F64::from_u64(b)); - - let accum_sum = a_vals - .iter() - .zip(b_vals.iter()) - .fold(Fp64ProductAccum::ZERO, |acc, (&a, &b)| { - acc + a.mul_u64_unreduced(b) - }); - assert_eq!(F64::reduce_mul_u64_accum(accum_sum), scalar_sum); -} - -#[test] -fn fp128_product_accum_matches_scalar() { - let mut rng = StdRng::seed_from_u64(0x0128_6666); - let n = 500; - let a_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - let b_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - - let scalar_sum: F128 = a_vals - .iter() - .zip(b_vals.iter()) - .fold(F128::zero(), |acc, (&a, &b)| acc + a * b); - - let accum_sum = a_vals - .iter() - .zip(b_vals.iter()) - .fold(Fp128ProductAccum::ZERO, |acc, (&a, &b)| { - acc + a.mul_to_product_accum(b) - }); - assert_eq!(F128::reduce_product_accum(accum_sum), scalar_sum); -} - -#[test] -fn fp128_mul_u64_accum_matches_scalar() { - let mut rng = StdRng::seed_from_u64(0x0128_7777); - let n = 500; - let a_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - let b_vals: Vec = (0..n).map(|_| rng.next_u64()).collect(); - - let scalar_sum: F128 = a_vals - .iter() - .zip(b_vals.iter()) - .fold(F128::zero(), |acc, (&a, &b)| acc + a * F128::from_u64(b)); - - let accum_sum = a_vals - .iter() - .zip(b_vals.iter()) - .fold(Fp128MulU64Accum::ZERO, |acc, (&a, &b)| { - acc + a.mul_u64_unreduced(b) - }); - assert_eq!(F128::reduce_mul_u64_accum(accum_sum), scalar_sum); -} - -#[test] -fn fp128_product_accum_sub_neg() { - let mut rng = StdRng::seed_from_u64(0x0128_8888); - let n = 500; - let a_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - let b_vals: Vec = (0..n).map(|_| FieldCore::random(&mut rng)).collect(); - - let mut scalar_sum = F128::zero(); - let mut accum_pos = Fp128ProductAccum::ZERO; - let mut accum_neg = Fp128ProductAccum::ZERO; - for (i, (&a, &b)) in a_vals.iter().zip(b_vals.iter()).enumerate() { - let prod = a.mul_to_product_accum(b); - if i % 2 == 0 { - scalar_sum += a * b; - accum_pos += prod; - } else { - scalar_sum -= a * b; - accum_neg += prod; - } - } - let result = F128::reduce_product_accum(accum_pos) - F128::reduce_product_accum(accum_neg); - assert_eq!(result, scalar_sum); -} diff --git a/crates/jolt-field/tests/binary_field_core_compat.rs b/crates/jolt-field/tests/binary_field_core_compat.rs deleted file mode 100644 index d12cc58331..0000000000 --- a/crates/jolt-field/tests/binary_field_core_compat.rs +++ /dev/null @@ -1,202 +0,0 @@ -//! Compatibility test for the slim algebraic trait layer. -//! -//! `Gf2` is intentionally a toy characteristic-2 field. It proves that the -//! core field traits do not bake in prime-field or large-field assumptions, but -//! it is not a production proving field. Real sumcheck soundness over such a -//! small base field would require an appropriately large extension field. - -use std::{ - fmt::{Debug, Display}, - hash::Hash, - iter::{Product, Sum}, - ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}, -}; - -use jolt_field::{AdditiveGroup, CanonicalBytes, CanonicalRepr, FieldCore, RingCore}; -use num_traits::{One, Zero}; - -#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)] -struct Gf2(bool); - -impl Debug for Gf2 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Debug::fmt(&(self.0 as u8), f) - } -} - -impl Display for Gf2 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Display::fmt(&(self.0 as u8), f) - } -} - -impl Zero for Gf2 { - fn zero() -> Self { - Self(false) - } - - fn is_zero(&self) -> bool { - !self.0 - } -} - -impl One for Gf2 { - fn one() -> Self { - Self(true) - } - - fn is_one(&self) -> bool { - self.0 - } -} - -#[expect(clippy::suspicious_arithmetic_impl)] -impl Add for Gf2 { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 ^ rhs.0) - } -} - -impl Add<&Self> for Gf2 { - type Output = Self; - - fn add(self, rhs: &Self) -> Self::Output { - self + *rhs - } -} - -impl AddAssign for Gf2 { - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -#[expect(clippy::suspicious_arithmetic_impl)] -impl Sub for Gf2 { - type Output = Self; - - fn sub(self, rhs: Self) -> Self::Output { - self + rhs - } -} - -impl Sub<&Self> for Gf2 { - type Output = Self; - - fn sub(self, rhs: &Self) -> Self::Output { - self - *rhs - } -} - -impl SubAssign for Gf2 { - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl Neg for Gf2 { - type Output = Self; - - fn neg(self) -> Self::Output { - self - } -} - -#[expect(clippy::suspicious_arithmetic_impl)] -impl Mul for Gf2 { - type Output = Self; - - fn mul(self, rhs: Self) -> Self::Output { - Self(self.0 & rhs.0) - } -} - -impl Mul<&Self> for Gf2 { - type Output = Self; - - fn mul(self, rhs: &Self) -> Self::Output { - self * *rhs - } -} - -impl MulAssign for Gf2 { - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl Sum for Gf2 { - fn sum>(iter: I) -> Self { - iter.fold(Self::zero(), |acc, x| acc + x) - } -} - -impl<'a> Sum<&'a Gf2> for Gf2 { - fn sum>(iter: I) -> Self { - iter.copied().sum() - } -} - -impl Product for Gf2 { - fn product>(iter: I) -> Self { - iter.fold(Self::one(), |acc, x| acc * x) - } -} - -impl<'a> Product<&'a Gf2> for Gf2 { - fn product>(iter: I) -> Self { - iter.copied().product() - } -} - -impl AdditiveGroup for Gf2 {} -impl RingCore for Gf2 {} - -impl FieldCore for Gf2 { - fn inverse(&self) -> Option { - if self.is_zero() { - None - } else { - Some(Self::one()) - } - } - - fn random(rng: &mut R) -> Self { - Self(rng.next_u32() & 1 == 1) - } -} - -impl CanonicalBytes for Gf2 { - const NUM_BYTES: usize = 1; - - fn to_bytes_le(&self, out: &mut [u8]) { - assert_eq!(out.len(), 1); - out[0] = self.0 as u8; - } -} - -impl CanonicalRepr for Gf2 { - fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { - Self(bytes.iter().fold(0u8, |acc, b| acc ^ (b & 1)) == 1) - } - - fn to_canonical_u64_checked(&self) -> Option { - Some(self.0 as u64) - } - - fn num_bits(&self) -> u32 { - self.0 as u32 - } -} - -fn accepts_field_core(x: F) -> F { - x.square() -} - -#[test] -fn characteristic_two_field_fits_algebraic_layer() { - assert_eq!(accepts_field_core(Gf2::one()), Gf2::one()); - assert_eq!(Gf2::one() + Gf2::one(), Gf2::zero()); -} diff --git a/crates/jolt-field-two/tests/bn254_differential.rs b/crates/jolt-field/tests/bn254_differential.rs similarity index 99% rename from crates/jolt-field-two/tests/bn254_differential.rs rename to crates/jolt-field/tests/bn254_differential.rs index 631c4ee53e..7c10cb01a7 100644 --- a/crates/jolt-field-two/tests/bn254_differential.rs +++ b/crates/jolt-field/tests/bn254_differential.rs @@ -1,4 +1,4 @@ -//! Differential tests: jolt-field-two's BN254 backend against exact +//! Differential tests: jolt-field's BN254 backend against exact //! num-bigint modular arithmetic. The canonical value of an element is read //! through `to_bytes_le`, whose faithfulness is pinned by the golden //! fixtures in golden_bytes.rs. @@ -6,7 +6,7 @@ #![cfg(feature = "bn254")] #![expect(clippy::unwrap_used, reason = "test code")] -use jolt_field_two as two; +use jolt_field as two; use num_bigint::{BigInt, BigUint, Sign}; use rand::{Rng, SeedableRng}; diff --git a/crates/jolt-field/tests/coverage.rs b/crates/jolt-field/tests/coverage.rs deleted file mode 100644 index 15fd5652d2..0000000000 --- a/crates/jolt-field/tests/coverage.rs +++ /dev/null @@ -1,998 +0,0 @@ -#![cfg(feature = "bn254")] -//! Targeted tests to improve code coverage across the jolt-field crate. -//! -//! Covers: NaiveAccumulator, WideAccumulator, -//! Field default methods, SignedBigInt uncovered paths, -//! SignedBigIntHi32 uncovered paths, and macro-generated operator variants. - -use ark_std::test_rng; -use jolt_field::signed::*; -use jolt_field::{ - Accumulator, CanonicalBytes, CanonicalRepr, FieldCore, Fr, FromPrimitiveInt, Limbs, - NaiveAccumulator, -}; -use num_traits::{One, Zero}; - -#[test] -fn naive_accumulator_fmadd() { - let a = ::from_u64(7); - let b = ::from_u64(11); - let c = ::from_u64(3); - let d = ::from_u64(5); - - let mut acc = NaiveAccumulator::::default(); - acc.fmadd(a, b); - acc.fmadd(c, d); - // 7*11 + 3*5 = 77 + 15 = 92 - assert_eq!(acc.reduce(), ::from_u64(92)); -} - -#[test] -fn naive_accumulator_merge() { - let mut acc1 = NaiveAccumulator::::default(); - acc1.fmadd( - ::from_u64(2), - ::from_u64(3), - ); - - let mut acc2 = NaiveAccumulator::::default(); - acc2.fmadd( - ::from_u64(4), - ::from_u64(5), - ); - - acc1.merge(acc2); - // 2*3 + 4*5 = 6 + 20 = 26 - assert_eq!(acc1.reduce(), ::from_u64(26)); -} - -#[test] -fn naive_accumulator_reduce_empty() { - let acc = NaiveAccumulator::::default(); - assert!(acc.reduce().is_zero()); -} - -#[test] -fn wide_accumulator_fmadd() { - use jolt_field::WideAccumulator; - - let a = ::from_u64(13); - let b = ::from_u64(17); - - let mut acc = WideAccumulator::default(); - acc.fmadd(a, b); - assert_eq!(acc.reduce(), ::from_u64(13 * 17)); -} - -#[test] -fn wide_accumulator_merge() { - use jolt_field::WideAccumulator; - - let mut acc1 = WideAccumulator::default(); - acc1.fmadd( - ::from_u64(10), - ::from_u64(20), - ); - - let mut acc2 = WideAccumulator::default(); - acc2.fmadd( - ::from_u64(30), - ::from_u64(40), - ); - - acc1.merge(acc2); - // 10*20 + 30*40 = 200 + 1200 = 1400 - assert_eq!(acc1.reduce(), ::from_u64(1400)); -} - -#[test] -fn wide_accumulator_reduce_empty() { - use jolt_field::WideAccumulator; - - let acc = WideAccumulator::default(); - assert!(acc.reduce().is_zero()); -} - -#[test] -fn wide_accumulator_many_fmadds() { - use jolt_field::WideAccumulator; - - let mut acc = WideAccumulator::default(); - let mut expected = Fr::zero(); - let mut rng = test_rng(); - for _ in 0..500 { - let a: Fr = ::random(&mut rng); - let b: Fr = ::random(&mut rng); - acc.fmadd(a, b); - expected += a * b; - } - assert_eq!(acc.reduce(), expected); -} - -#[test] -fn field_from_bool_edge() { - assert_eq!(::from_bool(true), Fr::one()); - assert_eq!(::from_bool(false), Fr::zero()); -} - -#[test] -fn field_from_small_types_boundary() { - assert_eq!(::from_u8(0), Fr::zero()); - assert_eq!( - ::from_u8(255), - ::from_u64(255) - ); - assert_eq!(::from_u16(0), Fr::zero()); - assert_eq!( - ::from_u16(65535), - ::from_u64(65535) - ); - assert_eq!(::from_u32(0), Fr::zero()); - assert_eq!( - ::from_u32(u32::MAX), - ::from_u64(u32::MAX as u64) - ); -} - -#[test] -fn field_mul_pow_2_boundary() { - let f = ::from_u64(1); - // pow=0 -> f * 1 = f - assert_eq!(::mul_pow_2(&f, 0), f); - // pow=1 -> f * 2 - assert_eq!( - ::mul_pow_2(&f, 1), - ::from_u64(2) - ); - // pow=64 -> goes through while loop at least once - let result = ::mul_pow_2(&f, 64); - let mut expected = f; - for _ in 0..64 { - expected = expected + expected; - } - assert_eq!(result, expected); -} - -#[test] -#[should_panic(expected = "pow > 255")] -fn field_mul_pow_2_overflow() { - let f = ::from_u64(1); - let _ = ::mul_pow_2(&f, 256); -} - -#[test] -fn signed_bigint_neg() { - let a = S64::from_i64(42); - let b = -a; - assert!(!b.is_positive); - assert_eq!(b.magnitude_as_u64(), 42); - - let c = -b; - assert!(c.is_positive); -} - -#[test] -fn signed_bigint_from_u128() { - let v = 0xDEAD_BEEF_CAFE_BABEu128; - let s = S128::from_u128(v); - assert!(s.is_positive); - assert_eq!(s.magnitude_as_u128(), v); -} - -#[test] -fn signed_bigint_from_i128_positive() { - let v = 123_456_789_012_345_678i128; - let s = S128::from_i128(v); - assert!(s.is_positive); - assert_eq!(s.to_i128(), Some(v)); -} - -#[test] -fn signed_bigint_from_i128_negative() { - let v = -123_456_789_012_345_678i128; - let s = S128::from_i128(v); - assert!(!s.is_positive); - assert_eq!(s.to_i128(), Some(v)); -} - -#[test] -fn signed_bigint_from_u128_trait() { - let v = 42u128; - let s: S128 = v.into(); - assert!(s.is_positive); - assert_eq!(s.magnitude_as_u128(), 42); -} - -#[test] -fn signed_bigint_from_i128_trait() { - let s: S128 = (-99i128).into(); - assert!(!s.is_positive); - assert_eq!(s.to_i128(), Some(-99)); -} - -#[test] -fn signed_bigint_sub_trunc() { - // Same sign, |self| > |rhs| - let a = S128::from_i128(100); - let b = S128::from_i128(30); - let c: S128 = a.sub_trunc::<2>(&b); - assert_eq!(c.to_i128(), Some(70)); - - // Same sign, |self| < |rhs| => sign flips - let d: S128 = b.sub_trunc::<2>(&a); - assert_eq!(d.to_i128(), Some(-70)); - - // Different signs: positive - negative = add magnitudes - let e = S128::from_i128(50); - let f = S128::from_i128(-30); - let g: S128 = e.sub_trunc::<2>(&f); - assert_eq!(g.to_i128(), Some(80)); -} - -#[test] -fn signed_bigint_sub_trunc_mixed() { - // Same sign, |self| > |rhs| - let a = S128::from_i128(100); - let b = S64::from_i64(30); - let c: S128 = a.sub_trunc_mixed::<1, 2>(&b); - assert_eq!(c.to_i128(), Some(70)); - - // Same sign, |self| < |rhs| - let d = S64::from_i64(30); - let e = S128::from_i128(100); - let f: S128 = d.sub_trunc_mixed::<2, 2>(&e); - assert_eq!(f.to_i128(), Some(-70)); - - // Different signs - let g = S128::from_i128(50); - let h = S64::from_i64(-20); - let i: S128 = g.sub_trunc_mixed::<1, 2>(&h); - assert_eq!(i.to_i128(), Some(70)); -} - -#[test] -fn signed_bigint_mul_trunc_widths() { - // S64 * S128 -> S128 - let a = S64::from_i64(-7); - let b = S128::from_i128(11); - let c: S128 = a.mul_trunc::<2, 2>(&b); - assert_eq!(c.to_i128(), Some(-77)); - - // S128 * S128 -> S256 - let d = S128::from_i128(1_000_000); - let e = S128::from_i128(-2_000_000); - let f: S256 = d.mul_trunc::<2, 4>(&e); - assert!(!f.is_positive); -} - -#[test] -fn signed_bigint_from_u64_mul_i64() { - let r = S128::from_u64_mul_i64(100, -7); - assert_eq!(r.to_i128(), Some(-700)); - - let r2 = S128::from_u64_mul_i64(100, 7); - assert_eq!(r2.to_i128(), Some(700)); -} - -#[test] -fn signed_bigint_from_i64_mul_u64() { - let r = S128::from_i64_mul_u64(-3, 100); - assert_eq!(r.to_i128(), Some(-300)); -} - -#[test] -fn signed_bigint_ordering_negative_magnitudes() { - // Both negative: larger magnitude = smaller value - let a = S64::from_i64(-10); - let b = S64::from_i64(-5); - assert!(a < b); - - // Both positive: larger magnitude = larger value - let c = S64::from_i64(10); - let d = S64::from_i64(5); - assert!(c > d); -} - -#[test] -fn s96_arithmetic() { - let a = S96::from(10i64); - let b = S96::from(3i64); - - let sum = a + b; - assert!(sum.is_positive()); - assert_eq!(sum.magnitude_lo()[0], 13); - - let diff = a - b; - assert!(diff.is_positive()); - assert_eq!(diff.magnitude_lo()[0], 7); - - let prod = a * b; - assert!(prod.is_positive()); - assert_eq!(prod.magnitude_lo()[0], 30); -} - -#[test] -fn s96_from_negative() { - let a = S96::from(-5i64); - assert!(!a.is_positive()); - assert_eq!(a.magnitude_lo()[0], 5); -} - -#[test] -fn s96_from_s64() { - let s = S64::from_i64(-42); - let wide = S96::from(s); - assert!(!wide.is_positive()); - assert_eq!(wide.magnitude_lo()[0], 42); -} - -#[test] -fn s224_operations() { - let a = S224::new([1, 0, 0], 0, true); - let b = S224::new([2, 0, 0], 0, true); - let sum = a + b; - assert!(sum.is_positive()); - assert_eq!(sum.magnitude_lo()[0], 3); - - let diff = a - b; - assert!(!diff.is_positive()); - assert_eq!(diff.magnitude_lo()[0], 1); - - let prod = a * b; - assert!(prod.is_positive()); - assert_eq!(prod.magnitude_lo()[0], 2); -} - -#[test] -fn s224_to_limbs4() { - let v = S224::new([0xAAAA, 0xBBBB, 0xCCCC], 0xDD, true); - let limbs: Limbs<4> = v.into(); - assert_eq!(limbs.0[0], 0xAAAA); - assert_eq!(limbs.0[1], 0xBBBB); - assert_eq!(limbs.0[2], 0xCCCC); - assert_eq!(limbs.0[3], 0xDD); -} - -#[test] -fn magnitude_as_limbs_nplus1_s96() { - let v = S96::new([42], 7, true); - let limbs: Limbs<2> = v.magnitude_as_limbs_nplus1::<2>(); - assert_eq!(limbs.0[0], 42); - assert_eq!(limbs.0[1], 7); -} - -#[test] -fn magnitude_as_limbs_nplus1_s160() { - let v = S160::new([1, 2], 3, false); - let limbs: Limbs<3> = v.magnitude_as_limbs_nplus1::<3>(); - assert_eq!(limbs.0[0], 1); - assert_eq!(limbs.0[1], 2); - assert_eq!(limbs.0[2], 3); -} - -#[test] -fn magnitude_as_limbs_nplus1_s224() { - let v = S224::new([10, 20, 30], 40, true); - let limbs: Limbs<4> = v.magnitude_as_limbs_nplus1::<4>(); - assert_eq!(limbs.0[0], 10); - assert_eq!(limbs.0[1], 20); - assert_eq!(limbs.0[2], 30); - assert_eq!(limbs.0[3], 40); -} - -#[test] -fn zero_extend_from_s96_to_s160() { - let s = S96::new([42], 7, false); - let wide: S160 = SignedBigIntHi32::zero_extend_from(&s); - assert!(!wide.is_positive()); - // When N > M, hi32 is placed into limb M as u64, new hi32 = 0 - assert_eq!(wide.magnitude_lo()[0], 42); - assert_eq!(wide.magnitude_lo()[1], 7); - assert_eq!(wide.magnitude_hi(), 0); -} - -#[test] -fn zero_extend_from_s96_to_s96() { - // N == M case - let s = S96::new([42], 7, true); - let same: S96 = SignedBigIntHi32::zero_extend_from(&s); - assert_eq!(same.magnitude_lo()[0], 42); - assert_eq!(same.magnitude_hi(), 7); - assert!(same.is_positive()); -} - -#[test] -fn s160_ordering() { - let a = S160::from(100u64); - let b = S160::from(200u64); - assert!(a < b); - - let c = S160::new([0, 0], 0, true); // positive zero - let d = S160::new([0, 0], 0, false); // negative zero - assert_eq!(c.cmp(&d), std::cmp::Ordering::Equal); - - // Positive > Negative - let pos = S160::from(1u64); - let neg = S160::new([1, 0], 0, false); - assert!(pos > neg); -} - -#[test] -fn s160_ordering_negative_magnitudes() { - // Both negative: larger magnitude = smaller value - let a = S160::new([10, 0], 0, false); - let b = S160::new([5, 0], 0, false); - assert!(a < b); -} - -#[test] -fn s160_ordering_hi32_tiebreak() { - let a = S160::new([0, 0], 1, true); - let b = S160::new([0, 0], 2, true); - assert!(a < b); -} - -#[test] -fn s160_from_sum_u128() { - let a = u128::MAX / 2; - let b = u128::MAX / 2; - let s = S160::from_sum_u128(a, b); - assert!(s.is_positive()); - // No overflow into hi32 for this case - assert_eq!(s.magnitude_hi(), 0); - - // Force carry into hi32 - let s2 = S160::from_sum_u128(u128::MAX, 1); - assert!(s2.is_positive()); - assert_eq!(s2.magnitude_hi(), 1); - assert_eq!(s2.magnitude_lo()[0], 0); - assert_eq!(s2.magnitude_lo()[1], 0); -} - -#[test] -fn s160_from_diff_u128() { - let a = S160::from_diff_u128(100, 200); - assert!(!a.is_positive()); - assert_eq!(a.magnitude_lo()[0], 100); - - let b = S160::from_diff_u128(200, 100); - assert!(b.is_positive()); - assert_eq!(b.magnitude_lo()[0], 100); -} - -#[test] -fn s160_from_magnitude_u128() { - let s = S160::from_magnitude_u128(0xDEAD_BEEF_CAFE_BABEu128, false); - assert!(!s.is_positive()); - assert_eq!(s.magnitude_lo()[0], 0xDEAD_BEEF_CAFE_BABEu128 as u64); - assert_eq!( - s.magnitude_lo()[1], - (0xDEAD_BEEF_CAFE_BABEu128 >> 64) as u64 - ); -} - -#[test] -fn s160_from_u128_minus_i128_negative_i() { - // u - (-i) = u + |i| (sum path) - let v = S160::from_u128_minus_i128(100, -50); - assert!(v.is_positive()); - assert_eq!(v.magnitude_lo()[0], 150); -} - -#[test] -fn s160_from_u128_minus_i128_positive_i_larger() { - // u - i where i > u (diff path, negative result) - let v = S160::from_u128_minus_i128(10, 100); - assert!(!v.is_positive()); - assert_eq!(v.magnitude_lo()[0], 90); -} - -#[test] -fn signed_bigint_hi32_neg() { - let a = S160::from(42u64); - let b = -a; - assert!(!b.is_positive()); - assert_eq!(b.magnitude_lo()[0], 42); - - // Neg for &SignedBigIntHi32 - let c = -(&a); - assert!(!c.is_positive()); - assert_eq!(c.magnitude_lo()[0], 42); -} - -#[test] -fn signed_bigint_hi32_one() { - let one = S96::one(); - assert!(one.is_positive()); - assert_eq!(one.magnitude_lo()[0], 1); - assert_eq!(one.magnitude_hi(), 0); -} - -#[test] -fn signed_bigint_hi32_is_zero() { - let z = S160::zero(); - assert!(z.is_zero()); - - let nz = S160::from(1u64); - assert!(!nz.is_zero()); -} - -#[test] -fn s160_from_i128() { - let pos: S160 = 42i128.into(); - assert!(pos.is_positive()); - assert_eq!(pos.magnitude_lo()[0], 42); - - let neg: S160 = (-42i128).into(); - assert!(!neg.is_positive()); - assert_eq!(neg.magnitude_lo()[0], 42); -} - -#[test] -fn s160_from_u128() { - let v: S160 = 0xDEAD_BEEF_CAFE_BABEu128.into(); - assert!(v.is_positive()); - assert_eq!(v.magnitude_lo()[0], 0xDEAD_BEEF_CAFE_BABEu128 as u64); -} - -#[test] -fn s160_from_s128() { - let s = S128::from_i128(-999); - let wide = S160::from(s); - assert!(!wide.is_positive()); - assert_eq!(wide.magnitude_lo()[0], 999); -} - -#[test] -#[expect(clippy::op_ref)] -fn signed_bigint_operator_variants() { - let a = S64::from_i64(10); - let b = S64::from_i64(3); - - // val-val - let _ = a + b; - let _ = a - b; - let _ = a * b; - - // val-ref - let _ = a + &b; - let _ = a - &b; - let _ = a * &b; - - // ref-ref - let _ = &a + &b; - let _ = &a - &b; - let _ = &a * &b; - - // OpAssign-val - let mut c = a; - c += b; - assert_eq!(c, S64::from_i64(13)); - c -= b; - assert_eq!(c, S64::from_i64(10)); - c *= b; - assert_eq!(c, S64::from_i64(30)); - - // OpAssign-ref - let mut d = a; - d += &b; - assert_eq!(d, S64::from_i64(13)); - d -= &b; - assert_eq!(d, S64::from_i64(10)); - d *= &b; - assert_eq!(d, S64::from_i64(30)); -} - -#[test] -#[expect(clippy::op_ref)] -fn signed_bigint_hi32_operator_variants() { - let a = S160::from(10u64); - let b = S160::from(3u64); - - // val-val - let _ = a + b; - let _ = a - b; - let _ = a * b; - - // val-ref - let _ = a + &b; - let _ = a - &b; - let _ = a * &b; - - // ref-ref - let _ = &a + &b; - let _ = &a - &b; - let _ = &a * &b; - - // OpAssign-val - let mut c = a; - c += b; - assert!(c.is_positive()); - assert_eq!(c.magnitude_lo()[0], 13); - c -= b; - assert_eq!(c.magnitude_lo()[0], 10); - c *= b; - assert_eq!(c.magnitude_lo()[0], 30); - - // OpAssign-ref - let mut d = a; - d += &b; - assert_eq!(d.magnitude_lo()[0], 13); - d -= &b; - assert_eq!(d.magnitude_lo()[0], 10); - d *= &b; - assert_eq!(d.magnitude_lo()[0], 30); -} - -#[test] -fn s96_mul_magnitudes_n1() { - // N=1 specialization: single lo limb + hi32 - let a = S96::new([u64::MAX], 0, true); - let b = S96::new([2], 0, true); - let prod = a * b; - // u64::MAX * 2 = 0x1_FFFF_FFFE, truncated to 96 bits - assert!(prod.is_positive()); -} - -#[test] -fn s160_mul_magnitudes_n2() { - // N=2 specialization - let a = S160::new([3, 0], 0, true); - let b = S160::new([7, 0], 0, true); - let prod = a * b; - assert_eq!(prod.magnitude_lo()[0], 21); - assert!(prod.is_positive()); -} - -#[test] -fn s224_mul_magnitudes_n3_general() { - // N=3: general path (N >= 3) - let a = S224::new([2, 0, 0], 0, true); - let b = S224::new([3, 0, 0], 0, true); - let prod = a * b; - assert_eq!(prod.magnitude_lo()[0], 6); - assert!(prod.is_positive()); -} - -#[test] -fn s160_sub_smaller_from_larger() { - // Tests the sign-flip path in sub_assign_in_place (via add_assign_in_place with neg) - let a = S160::from(3u64); - let b = S160::from(10u64); - let c = a - b; - assert!(!c.is_positive()); - assert_eq!(c.magnitude_lo()[0], 7); -} - -#[test] -fn s160_add_opposite_signs_self_larger() { - let a = S160::new([10, 0], 0, true); - let b = S160::new([3, 0], 0, false); - let c = a + b; - assert!(c.is_positive()); - assert_eq!(c.magnitude_lo()[0], 7); -} - -#[test] -fn s160_add_opposite_signs_rhs_larger() { - let a = S160::new([3, 0], 0, true); - let b = S160::new([10, 0], 0, false); - let c = a + b; - assert!(!c.is_positive()); - assert_eq!(c.magnitude_lo()[0], 7); -} - -#[test] -fn s160_mul_mixed_signs() { - let a = S160::new([5, 0], 0, true); - let b = S160::new([3, 0], 0, false); - let prod = a * b; - assert!(!prod.is_positive()); - assert_eq!(prod.magnitude_lo()[0], 15); - - let prod2 = b * b; - assert!(prod2.is_positive()); - assert_eq!(prod2.magnitude_lo()[0], 9); -} - -#[test] -fn s160_from_i64() { - let v: S160 = (-7i64).into(); - assert!(!v.is_positive()); - assert_eq!(v.magnitude_lo()[0], 7); - assert_eq!(v.magnitude_lo()[1], 0); - assert_eq!(v.magnitude_hi(), 0); -} - -#[test] -fn s160_from_u64() { - let v: S160 = 42u64.into(); - assert!(v.is_positive()); - assert_eq!(v.magnitude_lo()[0], 42); -} - -#[test] -fn s160_from_s64() { - let s = S64::from_i64(-99); - let v = S160::from(s); - assert!(!v.is_positive()); - assert_eq!(v.magnitude_lo()[0], 99); -} - -#[test] -fn signed_bigint_add_opposite_signs_self_smaller() { - let a = S64::from_i64(3); - let b = S64::from_i64(-10); - let c = a + b; - assert!(!c.is_positive); - assert_eq!(c.magnitude_as_u64(), 7); -} - -#[test] -fn signed_bigint_sub_opposite_signs() { - // positive - negative = add magnitudes - let a = S64::from_i64(5); - let b = S64::from_i64(-3); - let c = a - b; - assert!(c.is_positive); - assert_eq!(c.magnitude_as_u64(), 8); -} - -#[test] -fn signed_bigint_sub_same_sign_smaller_magnitude() { - // Same sign, |self| < |rhs| => sign flips - let a = S64::from_i64(3); - let b = S64::from_i64(10); - let c = a - b; - assert!(!c.is_positive); - assert_eq!(c.magnitude_as_u64(), 7); -} - -#[test] -fn signed_bigint_add_trunc_mixed_opposite_signs_self_smaller() { - let a = S64::from_i64(3); - let b = S128::from_i128(-100); - let c: S128 = a.add_trunc_mixed::<2, 2>(&b); - assert_eq!(c.to_i128(), Some(-97)); -} - -#[test] -fn signed_bigint_add_trunc_mixed_opposite_signs_self_larger() { - let a = S128::from_i128(100); - let b = S64::from_i64(-3); - let c: S128 = a.add_trunc_mixed::<1, 2>(&b); - assert_eq!(c.to_i128(), Some(97)); -} - -#[test] -fn s96_add_with_carry_into_hi32() { - let a = S96::new([u64::MAX], 0, true); - let b = S96::new([1], 0, true); - let c = a + b; - assert!(c.is_positive()); - assert_eq!(c.magnitude_lo()[0], 0); - assert_eq!(c.magnitude_hi(), 1); -} - -#[test] -fn s96_sub_with_borrow() { - let a = S96::new([0], 1, true); - let b = S96::new([1], 0, true); - let c = a - b; - assert!(c.is_positive()); - assert_eq!(c.magnitude_lo()[0], u64::MAX); - assert_eq!(c.magnitude_hi(), 0); -} - -#[test] -fn signed_bigint_hi32_default() { - let d = S160::default(); - assert!(d.is_zero()); - assert!(d.is_positive()); -} - -#[test] -fn signed_bigint_zero_extend_s64_to_s256() { - let s = S64::from_i64(-7); - let wide: S256 = SignedBigInt::zero_extend_from(&s); - assert!(!wide.is_positive); - assert_eq!(wide.magnitude.0[0], 7); - assert_eq!(wide.magnitude.0[1], 0); - assert_eq!(wide.magnitude.0[2], 0); - assert_eq!(wide.magnitude.0[3], 0); -} - -#[test] -fn signed_bigint_default_is_zero() { - let d = S64::default(); - assert!(d.is_zero()); -} - -#[test] -fn signed_bigint_one() { - let o = S64::one(); - assert!(o.is_positive); - assert_eq!(o.magnitude_as_u64(), 1); -} - -#[test] -fn signed_bigint_accessors() { - let s = S128::from_i128(-42); - assert!(!s.sign()); - assert_eq!(s.magnitude_slice(), &[42, 0]); - assert_eq!(s.magnitude_limbs(), [42, 0]); - let _ = s.as_magnitude(); -} - -#[test] -fn signed_bigint_negate() { - let s = S64::from_i64(10); - let n = s.negate(); - assert!(!n.is_positive); - assert_eq!(n.magnitude_as_u64(), 10); -} - -#[test] -fn fr_from_bool() { - let t: Fr = From::from(true); - let f: Fr = From::from(false); - assert_eq!(t, Fr::one()); - assert_eq!(f, Fr::zero()); -} - -#[test] -fn fr_from_small_types() { - // Exercise the From for Fr trait impls in bn254.rs - let from_u8: Fr = >::from(42); - assert_eq!(from_u8, Fr::from_u64(42)); - - let from_u16: Fr = >::from(1000); - assert_eq!(from_u16, Fr::from_u64(1000)); - - let from_u32: Fr = >::from(100_000); - assert_eq!(from_u32, Fr::from_u64(100_000)); - - let from_u64: Fr = >::from(123_456_789); - assert_eq!(from_u64, Fr::from_u64(123_456_789)); - - let from_i64: Fr = >::from(-42); - assert_eq!(from_i64, Fr::from_i64(-42)); - - let from_u128: Fr = >::from(999_999_999_999); - assert_eq!(from_u128, Fr::from_u128(999_999_999_999)); - - let from_i128: Fr = >::from(-999); - assert_eq!(from_i128, Fr::from_i128(-999)); - - let from_bool: Fr = >::from(true); - assert_eq!(from_bool, Fr::one()); - let from_bool_f: Fr = >::from(false); - assert_eq!(from_bool_f, Fr::zero()); -} - -#[test] -#[expect(clippy::op_ref)] -fn fr_ref_arithmetic() { - let a = Fr::from_u64(7); - let b = Fr::from_u64(11); - - // val op &ref - let sum_vr = a + &b; - assert_eq!(sum_vr, Fr::from_u64(18)); - let diff_vr = a - &b; - assert_eq!(diff_vr, Fr::from_i64(-4)); - let prod_vr = a * &b; - assert_eq!(prod_vr, Fr::from_u64(77)); - let div_vr = a / &b; - assert_eq!(div_vr * b, a); - - // &ref op val - let sum_rv = &a + b; - assert_eq!(sum_rv, Fr::from_u64(18)); - let diff_rv = &a - b; - assert_eq!(diff_rv, Fr::from_i64(-4)); - let prod_rv = &a * b; - assert_eq!(prod_rv, Fr::from_u64(77)); - let div_rv = &a / b; - assert_eq!(div_rv * b, a); - - // &ref op &ref - let sum_rr = &a + &b; - assert_eq!(sum_rr, Fr::from_u64(18)); - let diff_rr = &a - &b; - assert_eq!(diff_rr, Fr::from_i64(-4)); - let prod_rr = &a * &b; - assert_eq!(prod_rr, Fr::from_u64(77)); - let div_rr = &a / &b; - assert_eq!(div_rr * b, a); -} - -#[test] -fn fr_neg() { - let a = Fr::from_u64(42); - let neg_a = -a; - assert_eq!(a + neg_a, Fr::zero()); - - let neg_zero = -Fr::zero(); - assert_eq!(neg_zero, Fr::zero()); -} - -#[test] -fn fr_inner_roundtrip() { - // Test Fr(ark_bn254::Fr) -> ark_bn254::Fr conversion (inner type) - let a = Fr::from_u64(12345); - let bytes = a.to_bytes_le_vec(); - let b = ::from_le_bytes_mod_order(&bytes); - assert_eq!(a, b); -} - -#[test] -fn fr_mul_assign_sub_assign() { - let mut a = Fr::from_u64(10); - a -= Fr::from_u64(3); - assert_eq!(a, Fr::from_u64(7)); - - a *= Fr::from_u64(6); - assert_eq!(a, Fr::from_u64(42)); - - a += Fr::from_u64(8); - assert_eq!(a, Fr::from_u64(50)); -} - -#[test] -fn fr_sum_and_product_iterators() { - let vals: Vec = (1..=5).map(Fr::from_u64).collect(); - let sum: Fr = vals.iter().copied().sum(); - assert_eq!(sum, Fr::from_u64(15)); - - let sum_ref: Fr = vals.iter().sum(); - assert_eq!(sum_ref, Fr::from_u64(15)); - - let prod: Fr = vals.iter().copied().product(); - assert_eq!(prod, Fr::from_u64(120)); - - let prod_ref: Fr = vals.iter().product(); - assert_eq!(prod_ref, Fr::from_u64(120)); -} - -#[test] -fn wide_accumulator_reduce_matches_field() { - use jolt_field::WideAccumulator; - let mut rng = test_rng(); - - let mut acc = WideAccumulator::default(); - let mut expected = Fr::zero(); - - for _ in 0..200 { - let a = Fr::random(&mut rng); - let b = Fr::random(&mut rng); - acc.fmadd(a, b); - expected += a * b; - } - assert_eq!(acc.reduce(), expected); -} - -#[test] -fn wide_accumulator_merge_reduce() { - use jolt_field::WideAccumulator; - let mut rng = test_rng(); - - let mut acc1 = WideAccumulator::default(); - let mut acc2 = WideAccumulator::default(); - let mut expected = Fr::zero(); - - for _ in 0..100 { - let a = Fr::random(&mut rng); - let b = Fr::random(&mut rng); - acc1.fmadd(a, b); - expected += a * b; - } - for _ in 0..100 { - let a = Fr::random(&mut rng); - let b = Fr::random(&mut rng); - acc2.fmadd(a, b); - expected += a * b; - } - acc1.merge(acc2); - assert_eq!(acc1.reduce(), expected); -} diff --git a/crates/jolt-field/tests/field_operations.rs b/crates/jolt-field/tests/field_operations.rs deleted file mode 100644 index 5fb166621e..0000000000 --- a/crates/jolt-field/tests/field_operations.rs +++ /dev/null @@ -1,261 +0,0 @@ -#![cfg(feature = "bn254")] - -use ark_std::rand::Rng; -use ark_std::{test_rng, One, Zero}; -use jolt_field::{CanonicalRepr, FieldCore, Fr, FromPrimitiveInt}; -use rand_chacha::rand_core::RngCore; - -#[test] -fn implicit_montgomery_conversion() { - let mut rng = test_rng(); - - for _ in 0..256 { - let x = rng.next_u64(); - assert_eq!( - ::from_u64(x), - Fr::one() * ::from_u64(x) - ); - } - - for _ in 0..256 { - let x = rng.next_u64(); - let y: Fr = ::random(&mut rng); - assert_eq!( - y * ::from_u64(x), - y * ::from_u64(x) - ); - } -} - -#[test] -fn field_arithmetic() { - let mut rng = test_rng(); - - let x = ::from_u64(rng.next_u64()); - let y = ::from_u64(rng.next_u64()); - - let sum = x + y; - assert_eq!(sum, y + x); - - let product = x * y; - assert_eq!(product, y * x); - - let diff = x - y; - assert_eq!(diff + y, x); - - if !y.is_zero() { - let quotient = x / y; - assert_eq!(quotient * y, x); - } -} - -#[test] -fn field_conversions() { - let mut rng = test_rng(); - - assert_eq!(::from_bool(true), Fr::one()); - assert_eq!(::from_bool(false), Fr::zero()); - - for _ in 0..100 { - let val = rng.gen::(); - let field_elem = ::from_u8(val); - assert_eq!(field_elem, ::from_u64(val as u64)); - } - - for _ in 0..100 { - let val = rng.gen::(); - let field_elem = ::from_u16(val); - assert_eq!(field_elem, ::from_u64(val as u64)); - } - - for _ in 0..100 { - let val = rng.gen::(); - let field_elem = ::from_u32(val); - assert_eq!(field_elem, ::from_u64(val as u64)); - } - - for _ in 0..100 { - let val = rng.gen::(); - let field_elem = ::from_u128(val); - assert!(!field_elem.is_zero() || val == 0); - } -} - -#[test] -fn bytes_conversion() { - let mut rng = test_rng(); - - for &len in &[1, 8, 16, 32, 48, 64] { - let mut bytes = vec![0u8; len]; - rng.fill_bytes(&mut bytes); - let _field_elem = ::from_le_bytes_mod_order(&bytes); - } -} - -#[test] -fn signed_conversions() { - let mut rng = test_rng(); - - for _ in 0..100 { - let val = rng.gen::(); - let field_elem = ::from_i64(val); - - if val >= 0 { - assert_eq!(field_elem, ::from_u64(val as u64)); - } else { - assert_eq!( - field_elem, - -::from_u64(val.unsigned_abs()) - ); - } - } - - for _ in 0..100 { - let val = rng.gen::(); - let field_elem = ::from_i128(val); - - if val >= 0 { - assert_eq!(field_elem, ::from_u128(val as u128)); - } else { - assert_eq!( - field_elem, - -::from_u128(val.unsigned_abs()) - ); - } - } -} - -#[test] -fn mul_u64_method() { - let mut rng = test_rng(); - - for _ in 0..100 { - let field_elem: Fr = ::random(&mut rng); - let n = rng.next_u64(); - - // Use UFCS to call trait method (arkworks has inherent mul_u64 with different signature) - let result = ::mul_u64(&field_elem, n); - let expected = field_elem * ::from_u64(n); - assert_eq!(result, expected); - } -} - -#[test] -fn mul_i64_method() { - let mut rng = test_rng(); - - for _ in 0..100 { - let field_elem: Fr = ::random(&mut rng); - let n = rng.gen::(); - - let result = ::mul_i64(&field_elem, n); - let expected = field_elem * ::from_i64(n); - assert_eq!(result, expected); - } -} - -#[test] -fn mul_u128_method() { - let mut rng = test_rng(); - - for _ in 0..100 { - let field_elem: Fr = ::random(&mut rng); - let n = rng.gen::(); - - let result = ::mul_u128(&field_elem, n); - let expected = field_elem * ::from_u128(n); - assert_eq!(result, expected); - } -} - -#[test] -fn mul_i128_method() { - let mut rng = test_rng(); - - for _ in 0..100 { - let field_elem: Fr = ::random(&mut rng); - let n = rng.gen::(); - - let result = ::mul_i128(&field_elem, n); - let expected = field_elem * ::from_i128(n); - assert_eq!(result, expected); - } -} - -#[test] -fn mul_pow_2_method() { - let mut rng = test_rng(); - - for _ in 0..10 { - let field_elem: Fr = ::random(&mut rng); - - for pow in [0, 1, 2, 7, 16, 32, 63, 64, 127, 128, 255] { - let result = ::mul_pow_2(&field_elem, pow); - let mut expected = field_elem; - for _ in 0..pow { - expected = expected + expected; - } - assert_eq!(result, expected, "Failed for pow={pow}"); - } - } -} - -#[test] -fn mul_by_small_values() { - let mut rng = test_rng(); - - for _ in 0..100 { - let field_elem: Fr = ::random(&mut rng); - let small_val = rng.gen_range(0u64..1000); - - let result1 = field_elem * ::from_u64(small_val); - - let mut result2 = Fr::zero(); - for _ in 0..small_val { - result2 += field_elem; - } - - assert_eq!(result1, result2); - } -} - -#[test] -fn special_values() { - let mut rng = test_rng(); - let field_elem: Fr = ::random(&mut rng); - - assert_eq!( - field_elem * ::from_u64(0), - Fr::zero() - ); - assert_eq!( - field_elem * ::from_u64(1), - field_elem - ); - assert!((Fr::zero() * ::from_u64(rng.next_u64())).is_zero()); - - assert_eq!( - ::mul_u64(&field_elem, 0), - Fr::zero() - ); - assert_eq!( - ::mul_u64(&field_elem, 1), - field_elem - ); - assert_eq!( - ::mul_u64(&Fr::zero(), 42), - Fr::zero() - ); -} - -#[test] -fn to_u64_conversion() { - for i in 0..1000u64 { - let field_elem = ::from_u64(i); - assert_eq!(field_elem.to_canonical_u64_checked(), Some(i)); - } - - let mut rng = test_rng(); - let large_field: Fr = ::random(&mut rng); - let _ = large_field.to_canonical_u64_checked(); -} diff --git a/crates/jolt-field-two/tests/golden_bytes.rs b/crates/jolt-field/tests/golden_bytes.rs similarity index 99% rename from crates/jolt-field-two/tests/golden_bytes.rs rename to crates/jolt-field/tests/golden_bytes.rs index c16e9b76a3..3a051e05a2 100644 --- a/crates/jolt-field-two/tests/golden_bytes.rs +++ b/crates/jolt-field/tests/golden_bytes.rs @@ -11,7 +11,7 @@ //! same change; see this file's history). To regenerate: check out that //! commit, restore `tests/golden_gen.rs` and the `jolt-field` //! dev-dependency, run -//! `cargo nextest run -p jolt-field-two --all-features generate_golden_fixtures`, +//! `cargo nextest run -p jolt-field --all-features generate_golden_fixtures`, //! and splice `target/tmp/golden_fixtures.txt` into the const blocks below. //! //! Row format: `(input hex, expected hex)` where the element is @@ -22,7 +22,7 @@ #![expect(clippy::unwrap_used, reason = "test code")] -use jolt_field_two as two; +use jolt_field as two; use two::CanonicalEncoding; diff --git a/crates/jolt-field-two/tests/limbs_signed_differential.rs b/crates/jolt-field/tests/limbs_signed_differential.rs similarity index 99% rename from crates/jolt-field-two/tests/limbs_signed_differential.rs rename to crates/jolt-field/tests/limbs_signed_differential.rs index 04906ea0dc..9080d0784f 100644 --- a/crates/jolt-field-two/tests/limbs_signed_differential.rs +++ b/crates/jolt-field/tests/limbs_signed_differential.rs @@ -2,7 +2,7 @@ //! exact num-bigint integer arithmetic, plus u128/i128 oracles for the //! widths that fit. -use jolt_field_two as two; +use jolt_field as two; use num_bigint::{BigInt, BigUint, Sign}; use rand::{Rng, SeedableRng}; diff --git a/crates/jolt-field-two/tests/parallel_macros.rs b/crates/jolt-field/tests/parallel_macros.rs similarity index 95% rename from crates/jolt-field-two/tests/parallel_macros.rs rename to crates/jolt-field/tests/parallel_macros.rs index 58e1eddfb4..f80e6327e9 100644 --- a/crates/jolt-field-two/tests/parallel_macros.rs +++ b/crates/jolt-field/tests/parallel_macros.rs @@ -9,11 +9,11 @@ #![cfg(feature = "solinas")] -use jolt_field_two::{Prime64Offset59, Ring, Zero}; +use jolt_field::{Prime64Offset59, Ring, Zero}; #[cfg(feature = "parallel")] -use jolt_field_two::solinas::parallel::*; -use jolt_field_two::{ +use jolt_field::solinas::parallel::*; +use jolt_field::{ cfg_chunks, cfg_chunks_mut, cfg_fold_reduce, cfg_into_iter, cfg_iter, cfg_iter_mut, cfg_join, }; diff --git a/crates/jolt-field/tests/serde_roundtrip.rs b/crates/jolt-field/tests/serde_roundtrip.rs deleted file mode 100644 index ca551fcd1f..0000000000 --- a/crates/jolt-field/tests/serde_roundtrip.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! Wire-format guarantees for the Solinas types: bincode round-trips, exact -//! per-element sizes (`NUM_BYTES`, no per-element overhead), and rejection of -//! non-canonical encodings. -#![cfg(feature = "solinas")] -#![expect(clippy::unwrap_used)] - -use jolt_field::{ - CanonicalBytes, CanonicalRepr, Ext2, FieldCore, FpExt4, FpExt8, Prime128Offset275, - Prime32Offset99, Prime64Offset59, -}; -use rand::rngs::StdRng; -use rand::SeedableRng; -use serde::de::DeserializeOwned; -use serde::Serialize; - -type F32 = Prime32Offset99; -type F64 = Prime64Offset59; -type F128 = Prime128Offset275; - -fn assert_roundtrip_with_size(value: &T, expected_len: usize) -where - T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug, -{ - let bytes = bincode::serde::encode_to_vec(value, bincode::config::standard()).unwrap(); - assert_eq!( - bytes.len(), - expected_len, - "serialized size must be exactly the canonical byte length" - ); - let (decoded, consumed): (T, usize) = - bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap(); - assert_eq!(consumed, bytes.len()); - assert_eq!(&decoded, value); -} - -#[test] -fn prime_field_elements_encode_to_num_bytes() { - let mut rng = StdRng::seed_from_u64(7); - for _ in 0..32 { - assert_roundtrip_with_size(&F32::random(&mut rng), ::NUM_BYTES); - assert_roundtrip_with_size(&F64::random(&mut rng), ::NUM_BYTES); - assert_roundtrip_with_size(&F128::random(&mut rng), ::NUM_BYTES); - } -} - -#[test] -fn extension_field_elements_encode_to_num_coeffs_times_num_bytes() { - let mut rng = StdRng::seed_from_u64(8); - for _ in 0..16 { - assert_roundtrip_with_size(&Ext2::::random(&mut rng), 2 * 8); - assert_roundtrip_with_size(&FpExt4::::random(&mut rng), 4 * 4); - assert_roundtrip_with_size(&FpExt8::::random(&mut rng), 8 * 4); - } -} - -#[test] -fn vectors_add_only_a_single_length_prefix() { - let mut rng = StdRng::seed_from_u64(9); - for n in [0usize, 1, 17, 200] { - let v: Vec = (0..n).map(|_| F64::random(&mut rng)).collect(); - let bytes = bincode::serde::encode_to_vec(&v, bincode::config::standard()).unwrap(); - // bincode's standard config uses a varint length prefix: 1 byte for - // lengths below 251. - let prefix = if n < 251 { 1 } else { 3 }; - assert_eq!(bytes.len(), prefix + n * ::NUM_BYTES); - let (decoded, _): (Vec, usize) = - bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap(); - assert_eq!(decoded, v); - } -} - -#[test] -fn non_canonical_encodings_are_rejected() { - // The modulus itself is not a canonical representative. - let p32: u32 = u32::MAX - 98; - let bytes = - bincode::serde::encode_to_vec(p32.to_le_bytes(), bincode::config::standard()).unwrap(); - assert!( - bincode::serde::decode_from_slice::(&bytes, bincode::config::standard()).is_err() - ); - - let p64: u64 = u64::MAX - 58; - let bytes = - bincode::serde::encode_to_vec(p64.to_le_bytes(), bincode::config::standard()).unwrap(); - assert!( - bincode::serde::decode_from_slice::(&bytes, bincode::config::standard()).is_err() - ); - - let p128: u128 = u128::MAX - 274; - let bytes = - bincode::serde::encode_to_vec(p128.to_le_bytes(), bincode::config::standard()).unwrap(); - assert!( - bincode::serde::decode_from_slice::(&bytes, bincode::config::standard()).is_err() - ); -} - -#[test] -fn canonical_transcript_bytes_and_serde_bytes_agree_for_prime_fields() { - // For the prime fields both encodings are the canonical little-endian - // representative; pin that so neither drifts. - let mut rng = StdRng::seed_from_u64(10); - for _ in 0..16 { - let x = F128::random(&mut rng); - let wire = bincode::serde::encode_to_vec(x, bincode::config::standard()).unwrap(); - assert_eq!(wire, x.to_bytes_le_vec()); - } -} - -#[test] -fn from_u64_sanity() { - let x = F32::from_u64(42); - let bytes = bincode::serde::encode_to_vec(x, bincode::config::standard()).unwrap(); - assert_eq!(bytes, 42u32.to_le_bytes()); -} diff --git a/crates/jolt-field-two/tests/solinas_ext_differential.rs b/crates/jolt-field/tests/solinas_ext_differential.rs similarity index 99% rename from crates/jolt-field-two/tests/solinas_ext_differential.rs rename to crates/jolt-field/tests/solinas_ext_differential.rs index 22a2f30e42..1c9b544013 100644 --- a/crates/jolt-field-two/tests/solinas_ext_differential.rs +++ b/crates/jolt-field/tests/solinas_ext_differential.rs @@ -15,7 +15,7 @@ #![cfg(feature = "solinas")] #![expect(clippy::unwrap_used, reason = "test code")] -use jolt_field_two as two; +use jolt_field as two; use num_traits::{One, Zero}; use rand::{Rng, SeedableRng}; diff --git a/crates/jolt-field-two/tests/solinas_fp128_differential.rs b/crates/jolt-field/tests/solinas_fp128_differential.rs similarity index 99% rename from crates/jolt-field-two/tests/solinas_fp128_differential.rs rename to crates/jolt-field/tests/solinas_fp128_differential.rs index ec015bfda9..721246e04a 100644 --- a/crates/jolt-field-two/tests/solinas_fp128_differential.rs +++ b/crates/jolt-field/tests/solinas_fp128_differential.rs @@ -7,7 +7,7 @@ // NB: no `expect(clippy::unwrap_used)` — every unwrap here sits inside a // local `macro_rules!` expansion, where the lint does not fire. -use jolt_field_two as two; +use jolt_field as two; use rand::{Rng, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; diff --git a/crates/jolt-field-two/tests/solinas_packed_differential.rs b/crates/jolt-field/tests/solinas_packed_differential.rs similarity index 99% rename from crates/jolt-field-two/tests/solinas_packed_differential.rs rename to crates/jolt-field/tests/solinas_packed_differential.rs index 214c80a178..fddfcfbf73 100644 --- a/crates/jolt-field-two/tests/solinas_packed_differential.rs +++ b/crates/jolt-field/tests/solinas_packed_differential.rs @@ -12,7 +12,7 @@ #![cfg(feature = "solinas")] #![expect(clippy::unwrap_used, reason = "test code")] -use jolt_field_two as two; +use jolt_field as two; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; diff --git a/crates/jolt-field-two/tests/solinas_unreduced_differential.rs b/crates/jolt-field/tests/solinas_unreduced_differential.rs similarity index 99% rename from crates/jolt-field-two/tests/solinas_unreduced_differential.rs rename to crates/jolt-field/tests/solinas_unreduced_differential.rs index 0c4dca6597..fa3a9c17a0 100644 --- a/crates/jolt-field-two/tests/solinas_unreduced_differential.rs +++ b/crates/jolt-field/tests/solinas_unreduced_differential.rs @@ -20,7 +20,7 @@ // NB: no `expect(clippy::unwrap_used)` — every unwrap here sits inside a // local `macro_rules!` expansion, where the lint does not fire. -use jolt_field_two as two; +use jolt_field as two; use num_traits::Zero; use rand::{Rng, SeedableRng}; diff --git a/crates/jolt-field-two/tests/solinas_words_differential.rs b/crates/jolt-field/tests/solinas_words_differential.rs similarity index 99% rename from crates/jolt-field-two/tests/solinas_words_differential.rs rename to crates/jolt-field/tests/solinas_words_differential.rs index 57624089e1..9cbc4a4d09 100644 --- a/crates/jolt-field-two/tests/solinas_words_differential.rs +++ b/crates/jolt-field/tests/solinas_words_differential.rs @@ -6,7 +6,7 @@ // NB: no `expect(clippy::unwrap_used)` — every unwrap here sits inside a // local `macro_rules!` expansion, where the lint does not fire. -use jolt_field_two as two; +use jolt_field as two; use num_bigint::BigUint; use rand::{Rng, SeedableRng}; diff --git a/crates/jolt-field-two/tests/spine.rs b/crates/jolt-field/tests/spine.rs similarity index 99% rename from crates/jolt-field-two/tests/spine.rs rename to crates/jolt-field/tests/spine.rs index 757205995b..60e1acd738 100644 --- a/crates/jolt-field-two/tests/spine.rs +++ b/crates/jolt-field/tests/spine.rs @@ -6,7 +6,7 @@ #![expect(clippy::unwrap_used, reason = "test code")] -use jolt_field_two::{ +use jolt_field::{ impl_ring_ops, impl_serde_bytes, Accumulator, CanonicalBytes, CanonicalEncoding, Field, JoltField, NaiveAccumulator, One, Ring, WithAccumulator, Zero, }; diff --git a/specs/jolt-field-rebuild.md b/specs/jolt-field-rebuild.md index 8a4a0d2ba2..a47e94aa7e 100644 --- a/specs/jolt-field-rebuild.md +++ b/specs/jolt-field-rebuild.md @@ -2,7 +2,7 @@ | Field | Value | |---------|----------------------------------------------------| -| Status | built — all nine checkpoints complete; final audit: 5,103 counted LOC (budget 6,240), feature matrix + full test suite green | +| Status | replaced-in — all nine checkpoints complete (5,103 counted LOC, budget 6,240); the crate now lives at `crates/jolt-field` and the baseline implementation is deleted | | Baseline| `jolt-field` @ PR #1684 head (`fe1d5d41f`) | | Goal | functional parity at ≤ 6,300 counted LOC (baseline: 11,410) | @@ -12,8 +12,9 @@ Rebuild `crates/jolt-field` from first principles minimizing source LOC while preserving functionality: both backends (BN254 arkworks + full Solinas stack), wire/transcript **byte** compatibility, and static dispatch. Trait names and boundaries are redesigned from scratch — old-name compatibility is explicitly -NOT a goal (approved); consumers rebind at replacement time. The crate lives -at `crates/jolt-field-two` until ready to replace `jolt-field`. +NOT a goal (approved); consumers rebind at replacement time. The crate lived +at `crates/jolt-field-two` during the rebuild; at replacement it took over +`crates/jolt-field` and the package name `jolt-field`. ## Counting rules From 079356e3042a1f0627c8050cb0c080021ae5fb1a Mon Sep 17 00:00:00 2001 From: acentelles Date: Fri, 31 Jul 2026 19:14:49 -0400 Subject: [PATCH 30/38] refactor(workspace): rebind all consumers to the rebuilt jolt-field spine Scripted, import-guided rename over ~400 files: umbrella Field -> JoltField, FieldCore -> Field, RingCore/FromPrimitiveInt -> Ring, CanonicalRepr -> CanonicalEncoding, from_le_bytes_mod_order -> from_bytes_le_reduced, to_canonical_u64_checked -> to_u64_checked, arkworks::bn254 paths -> root exports. Position-aware for Field: associated types named Field (CommitmentScheme, syn::Field) and all ::-qualified paths keep their names; ark_ff::Field collision files and akita-field's shared trait names handled by hand. Two surface restorations in jolt-field, recorded as deviations in specs/jolt-field-rebuild.md: bn254 From impls (94+ call sites relied on the plain arkworks conversions), and JoltField drops its serde bounds while the akita bootstrap edge exists (the foreign pre-cutover type cannot implement serde here; restore at cutover). mersenne61_compat reworked against the new spine, proving the slim hierarchy remains implementable without arkworks. Gates: clippy --all-targets -D warnings green on host, host+zk, akita x3, field-inline, and +avx2/+avx512 cross-checks to x86_64-apple-darwin. --- crates/jolt-akita/benches/akita_paths.rs | 15 ++-- crates/jolt-akita/tests/pathologies.rs | 4 +- crates/jolt-blindfold/src/builder.rs | 4 +- crates/jolt-blindfold/src/error.rs | 6 +- crates/jolt-blindfold/src/proof.rs | 4 +- crates/jolt-blindfold/src/protocol.rs | 22 +++--- crates/jolt-blindfold/src/prove.rs | 68 +++++++++---------- crates/jolt-blindfold/src/r1cs.rs | 16 ++--- crates/jolt-blindfold/src/relaxed.rs | 8 +-- crates/jolt-blindfold/src/verify.rs | 28 ++++---- crates/jolt-blindfold/tests/support/mod.rs | 4 +- crates/jolt-claims-derive/src/lib.rs | 10 +-- crates/jolt-claims/src/claim_data.rs | 12 ++-- crates/jolt-claims/src/claims.rs | 18 ++--- crates/jolt-claims/src/ops.rs | 46 ++++++------- .../field_inline/geometry/bytecode.rs | 16 ++--- .../field_inline/geometry/dimensions.rs | 6 +- .../relations/claim_reductions/increments.rs | 8 +-- .../relations/claim_reductions/registers.rs | 8 +-- .../field_inline/relations/product.rs | 8 +-- .../field_inline/relations/registers.rs | 12 ++-- .../src/protocols/jolt/geometry/booleanity.rs | 6 +- .../src/protocols/jolt/geometry/bytecode.rs | 48 ++++++------- .../jolt/geometry/claim_reductions/advice.rs | 12 ++-- .../geometry/claim_reductions/bytecode.rs | 20 +++--- .../claim_reductions/hamming_weight.rs | 8 +-- .../geometry/claim_reductions/increments.rs | 4 +- .../geometry/claim_reductions/instruction.rs | 4 +- .../geometry/claim_reductions/precommitted.rs | 28 ++++---- .../claim_reductions/program_image.rs | 16 ++--- .../jolt/geometry/committed_openings.rs | 10 +-- .../src/protocols/jolt/geometry/dimensions.rs | 16 ++--- .../protocols/jolt/geometry/instruction.rs | 16 ++--- .../src/protocols/jolt/geometry/ram.rs | 8 +-- .../src/protocols/jolt/geometry/spartan.rs | 16 ++--- .../src/protocols/jolt/lattice/geometry.rs | 12 ++-- .../relations/advice_reconstruction.rs | 14 ++-- .../jolt/lattice/relations/booleanity.rs | 12 ++-- .../relations/bytecode_reconstruction.rs | 10 +-- .../jolt/lattice/relations/hamming_weight.rs | 8 +-- .../relations/program_image_reconstruction.rs | 8 +-- .../jolt/lattice/relations/read_raf.rs | 20 +++--- .../src/protocols/jolt/lattice/strategy.rs | 6 +- .../relations/booleanity/address_phase.rs | 10 +-- .../jolt/relations/booleanity/cycle_phase.rs | 6 +- .../jolt/relations/booleanity/monolith.rs | 8 +-- .../jolt/relations/bytecode/read_raf.rs | 8 +-- .../bytecode/read_raf_address_phase.rs | 8 +-- .../bytecode/read_raf_cycle_phase.rs | 6 +- .../read_raf_cycle_phase_committed.rs | 6 +- .../claim_reductions/advice/address_phase.rs | 12 ++-- .../claim_reductions/advice/cycle_phase.rs | 12 ++-- .../bytecode/address_phase.rs | 8 +-- .../claim_reductions/bytecode/cycle_phase.rs | 8 +-- .../claim_reductions/hamming_weight.rs | 8 +-- .../relations/claim_reductions/increments.rs | 8 +-- .../relations/claim_reductions/instruction.rs | 8 +-- .../program_image/address_phase.rs | 8 +-- .../program_image/cycle_phase.rs | 6 +- .../relations/claim_reductions/registers.rs | 8 +-- .../instruction/input_virtualization.rs | 8 +-- .../instruction/ra_virtualization.rs | 8 +-- .../jolt/relations/instruction/read_raf.rs | 8 +-- .../jolt/relations/ram/hamming_booleanity.rs | 10 +-- .../jolt/relations/ram/output_check.rs | 12 ++-- .../jolt/relations/ram/ra_claim_reduction.rs | 8 +-- .../jolt/relations/ram/ra_virtualization.rs | 8 +-- .../jolt/relations/ram/raf_evaluation.rs | 8 +-- .../jolt/relations/ram/read_write_checking.rs | 8 +-- .../protocols/jolt/relations/ram/val_check.rs | 8 +-- .../registers/read_write_checking.rs | 8 +-- .../relations/registers/val_evaluation.rs | 8 +-- .../jolt/relations/spartan/outer_remainder.rs | 8 +-- .../jolt/relations/spartan/outer_uniskip.rs | 8 +-- .../relations/spartan/product_remainder.rs | 8 +-- .../jolt/relations/spartan/product_uniskip.rs | 6 +- .../protocols/jolt/relations/spartan/shift.rs | 8 +-- crates/jolt-claims/src/symbolic.rs | 8 +-- crates/jolt-claims/tests/lattice_semantics.rs | 2 +- crates/jolt-crypto/benches/crypto.rs | 2 +- .../fuzz/fuzz_targets/group_arith.rs | 6 +- .../fuzz/fuzz_targets/pedersen_commit.rs | 6 +- crates/jolt-crypto/src/commitment.rs | 16 ++--- crates/jolt-crypto/src/ec/bn254/gt.rs | 6 +- crates/jolt-crypto/src/ec/bn254/mod.rs | 12 ++-- crates/jolt-crypto/src/ec/group.rs | 6 +- crates/jolt-crypto/src/ec/pairing.rs | 4 +- crates/jolt-crypto/tests/coverage.rs | 2 +- crates/jolt-crypto/tests/group_laws.rs | 2 +- crates/jolt-crypto/tests/pairing.rs | 2 +- crates/jolt-crypto/tests/pedersen.rs | 2 +- crates/jolt-crypto/tests/serialization.rs | 2 +- crates/jolt-dory/benches/dory.rs | 28 ++++---- .../fuzz/fuzz_targets/verify_tampered.rs | 2 +- crates/jolt-dory/src/scheme.rs | 19 +++--- crates/jolt-dory/src/streaming.rs | 16 ++--- crates/jolt-dory/src/types.rs | 10 +-- crates/jolt-dory/tests/commit_open_verify.rs | 42 ++++++------ crates/jolt-field/src/algebra.rs | 22 +++--- crates/jolt-field/src/bn254/mod.rs | 16 +++++ crates/jolt-field/src/lib.rs | 5 +- .../tests/solinas_fp128_differential.rs | 5 +- .../tests/solinas_words_differential.rs | 5 +- crates/jolt-hyperkzg/benches/hyperkzg.rs | 2 +- .../fuzz/fuzz_targets/commit_open_verify.rs | 2 +- .../fuzz/fuzz_targets/tampered_proof.rs | 2 +- .../fuzz/fuzz_targets/wrong_eval.rs | 2 +- crates/jolt-hyperkzg/src/kzg.rs | 10 +-- crates/jolt-hyperkzg/src/scheme.rs | 2 +- crates/jolt-hyperkzg/src/types.rs | 2 +- .../jolt-hyperkzg/tests/commit_open_verify.rs | 2 +- crates/jolt-kernels/src/backend.rs | 10 +-- crates/jolt-kernels/src/commitment.rs | 4 +- crates/jolt-kernels/src/committed_program.rs | 6 +- crates/jolt-kernels/src/error.rs | 4 +- crates/jolt-kernels/src/kernel.rs | 8 +-- crates/jolt-kernels/src/opening.rs | 6 +- .../src/precommitted_reduction.rs | 36 +++++----- .../src/reference/advice_claim_reduction.rs | 12 ++-- .../jolt-kernels/src/reference/booleanity.rs | 16 ++--- .../src/reference/bytecode_claim_reduction.rs | 6 +- .../src/reference/bytecode_read_raf.rs | 16 ++--- .../jolt-kernels/src/reference/commitment.rs | 16 ++--- .../hamming_weight_claim_reduction.rs | 4 +- .../src/reference/inc_claim_reduction.rs | 4 +- .../reference/instruction_claim_reduction.rs | 4 +- .../src/reference/instruction_input.rs | 4 +- .../instruction_ra_virtualization.rs | 4 +- .../src/reference/instruction_read_raf.rs | 22 +++--- crates/jolt-kernels/src/reference/mod.rs | 4 +- crates/jolt-kernels/src/reference/naive.rs | 28 ++++---- crates/jolt-kernels/src/reference/opening.rs | 8 +-- .../src/reference/precommitted_reduction.rs | 4 +- .../program_image_claim_reduction.rs | 6 +- .../src/reference/ram_hamming_booleanity.rs | 4 +- .../src/reference/ram_output_check.rs | 4 +- .../src/reference/ram_ra_claim_reduction.rs | 4 +- .../src/reference/ram_ra_virtualization.rs | 4 +- .../src/reference/ram_raf_evaluation.rs | 4 +- .../src/reference/ram_read_write.rs | 4 +- .../src/reference/ram_val_check.rs | 4 +- .../reference/registers_claim_reduction.rs | 4 +- .../src/reference/registers_read_write.rs | 4 +- .../src/reference/registers_val_evaluation.rs | 4 +- .../src/reference/spartan_outer.rs | 16 ++--- .../src/reference/spartan_product.rs | 10 +-- .../src/reference/spartan_shift.rs | 4 +- crates/jolt-kernels/src/reference/views.rs | 18 ++--- crates/jolt-kernels/src/uniskip.rs | 4 +- .../jolt-lookup-tables/src/challenge_ops.rs | 8 +-- crates/jolt-lookup-tables/src/tables/and.rs | 6 +- crates/jolt-lookup-tables/src/tables/andn.rs | 6 +- crates/jolt-lookup-tables/src/tables/equal.rs | 6 +- .../src/tables/halfword_alignment.rs | 6 +- .../src/tables/lower_half_word.rs | 6 +- crates/jolt-lookup-tables/src/tables/mod.rs | 12 ++-- .../src/tables/mulu_no_overflow.rs | 6 +- .../src/tables/not_equal.rs | 6 +- crates/jolt-lookup-tables/src/tables/or.rs | 6 +- crates/jolt-lookup-tables/src/tables/pow2.rs | 6 +- .../jolt-lookup-tables/src/tables/pow2_w.rs | 6 +- .../src/tables/prefixes/and.rs | 4 +- .../src/tables/prefixes/andn.rs | 4 +- .../src/tables/prefixes/change_divisor.rs | 4 +- .../src/tables/prefixes/change_divisor_w.rs | 4 +- .../src/tables/prefixes/div_by_zero.rs | 4 +- .../src/tables/prefixes/eq.rs | 4 +- .../src/tables/prefixes/left_is_zero.rs | 4 +- .../src/tables/prefixes/left_operand_msb.rs | 4 +- .../src/tables/prefixes/left_shift.rs | 4 +- .../src/tables/prefixes/left_shift_helper.rs | 4 +- .../src/tables/prefixes/left_shift_w.rs | 4 +- .../tables/prefixes/left_shift_w_helper.rs | 4 +- .../src/tables/prefixes/lower_half_word.rs | 4 +- .../src/tables/prefixes/lower_word.rs | 4 +- .../src/tables/prefixes/lsb.rs | 4 +- .../src/tables/prefixes/lt.rs | 4 +- .../src/tables/prefixes/mod.rs | 8 +-- .../negative_divisor_equals_remainder.rs | 4 +- ...negative_divisor_greater_than_remainder.rs | 4 +- .../negative_divisor_zero_remainder.rs | 4 +- .../src/tables/prefixes/or.rs | 4 +- .../src/tables/prefixes/overflow_bits_zero.rs | 4 +- .../positive_remainder_equals_divisor.rs | 4 +- .../positive_remainder_less_than_divisor.rs | 4 +- .../src/tables/prefixes/pow2.rs | 4 +- .../src/tables/prefixes/pow2_w.rs | 4 +- .../src/tables/prefixes/rev8w.rs | 4 +- .../src/tables/prefixes/right_is_zero.rs | 4 +- .../src/tables/prefixes/right_operand.rs | 4 +- .../src/tables/prefixes/right_operand_msb.rs | 4 +- .../src/tables/prefixes/right_operand_w.rs | 4 +- .../src/tables/prefixes/right_shift.rs | 4 +- .../src/tables/prefixes/right_shift_w.rs | 4 +- .../src/tables/prefixes/sign_extension.rs | 4 +- .../prefixes/sign_extension_right_operand.rs | 4 +- .../prefixes/sign_extension_upper_half.rs | 4 +- .../src/tables/prefixes/two_lsb.rs | 4 +- .../src/tables/prefixes/upper_word.rs | 4 +- .../src/tables/prefixes/xor.rs | 4 +- .../src/tables/prefixes/xor_rot.rs | 4 +- .../src/tables/prefixes/xor_rotw.rs | 4 +- .../src/tables/range_check.rs | 6 +- .../src/tables/range_check_aligned.rs | 6 +- .../src/tables/shift_right_bitmask.rs | 6 +- .../src/tables/sign_extend_half_word.rs | 6 +- .../src/tables/sign_mask.rs | 6 +- .../src/tables/signed_greater_than_equal.rs | 6 +- .../src/tables/signed_less_than.rs | 6 +- .../src/tables/suffixes/mod.rs | 4 +- .../src/tables/test_utils.rs | 17 +++-- .../src/tables/unsigned_greater_than_equal.rs | 6 +- .../src/tables/unsigned_less_than.rs | 6 +- .../src/tables/unsigned_less_than_equal.rs | 6 +- .../src/tables/upper_word.rs | 6 +- .../src/tables/valid_div0.rs | 6 +- .../src/tables/valid_unsigned_remainder.rs | 6 +- .../src/tables/virtual_change_divisor.rs | 6 +- .../src/tables/virtual_change_divisor_w.rs | 6 +- .../src/tables/virtual_rev8w.rs | 6 +- .../src/tables/virtual_rotr.rs | 6 +- .../src/tables/virtual_rotrw.rs | 6 +- .../src/tables/virtual_sra.rs | 6 +- .../src/tables/virtual_srl.rs | 6 +- .../src/tables/virtual_xor_rot.rs | 6 +- .../src/tables/virtual_xor_rotw.rs | 6 +- .../src/tables/word_alignment.rs | 6 +- crates/jolt-lookup-tables/src/tables/xor.rs | 6 +- crates/jolt-lookup-tables/src/traits.rs | 4 +- crates/jolt-openings/src/claims.rs | 12 ++-- crates/jolt-openings/src/packing.rs | 40 +++++------ crates/jolt-openings/src/schemes.rs | 16 ++--- crates/jolt-openings/tests/packing.rs | 2 +- crates/jolt-openings/tests/support/common.rs | 2 +- crates/jolt-openings/tests/support/mock.rs | 26 +++---- crates/jolt-openings/tests/support/packed.rs | 6 +- crates/jolt-poly/benches/poly_ops.rs | 2 +- .../fuzz/fuzz_targets/dense_poly_ops.rs | 6 +- crates/jolt-poly/src/compressed_univariate.rs | 10 +-- crates/jolt-poly/src/dense.rs | 38 +++++------ crates/jolt-poly/src/eq.rs | 20 +++--- crates/jolt-poly/src/eq_plus_one.rs | 12 ++-- crates/jolt-poly/src/identity.rs | 8 +-- crates/jolt-poly/src/lagrange.rs | 26 +++---- crates/jolt-poly/src/lt.rs | 12 ++-- crates/jolt-poly/src/mle.rs | 14 ++-- crates/jolt-poly/src/multilinear.rs | 26 +++---- crates/jolt-poly/src/one_hot.rs | 8 +-- crates/jolt-poly/src/split_eq.rs | 12 ++-- crates/jolt-poly/src/univariate.rs | 36 +++++----- crates/jolt-poly/tests/integration.rs | 2 +- .../src/transcripts/verifier_native.rs | 4 +- .../src/zkvm/clear_claims.rs | 52 +++++++------- crates/jolt-prover-legacy/src/zkvm/packed.rs | 10 +-- .../src/zkvm/packed_witness.rs | 8 +-- crates/jolt-prover-legacy/src/zkvm/proof.rs | 2 +- crates/jolt-prover/src/config.rs | 4 +- crates/jolt-prover/src/driver.rs | 20 +++--- crates/jolt-prover/src/error.rs | 6 +- crates/jolt-prover/src/prover.rs | 4 +- crates/jolt-prover/src/stages/drivers.rs | 16 ++--- crates/jolt-prover/src/stages/stage0.rs | 4 +- crates/jolt-prover/src/stages/stage1.rs | 6 +- crates/jolt-prover/src/stages/stage2.rs | 6 +- crates/jolt-prover/src/stages/stage3.rs | 6 +- crates/jolt-prover/src/stages/stage4.rs | 6 +- crates/jolt-prover/src/stages/stage5.rs | 6 +- crates/jolt-prover/src/stages/stage6a.rs | 6 +- crates/jolt-prover/src/stages/stage6b.rs | 6 +- crates/jolt-prover/src/stages/stage7.rs | 6 +- crates/jolt-prover/src/stages/stage8.rs | 4 +- crates/jolt-prover/tests/byte_diff.rs | 2 +- crates/jolt-prover/tests/engine_twins.rs | 4 +- crates/jolt-r1cs/src/builder.rs | 18 ++--- crates/jolt-r1cs/src/constraint.rs | 24 +++---- .../src/constraints/field_constraints.rs | 14 ++-- crates/jolt-r1cs/src/constraints/jolt.rs | 22 +++--- crates/jolt-r1cs/src/constraints/rv64.rs | 22 +++--- crates/jolt-r1cs/src/key.rs | 8 +-- crates/jolt-r1cs/src/lowering.rs | 20 +++--- crates/jolt-r1cs/src/provider.rs | 8 +-- .../fuzz/fuzz_targets/sumcheck_verifier.rs | 4 +- .../fuzz/fuzz_targets/valid_prefix_proof.rs | 4 +- crates/jolt-sumcheck/src/batch.rs | 4 +- crates/jolt-sumcheck/src/claim.rs | 8 +-- crates/jolt-sumcheck/src/committed.rs | 12 ++-- crates/jolt-sumcheck/src/error.rs | 4 +- crates/jolt-sumcheck/src/proof.rs | 12 ++-- crates/jolt-sumcheck/src/prover.rs | 18 ++--- crates/jolt-sumcheck/src/r1cs.rs | 18 ++--- crates/jolt-sumcheck/src/recorder.rs | 20 +++--- crates/jolt-sumcheck/src/round_proof.rs | 22 +++--- crates/jolt-sumcheck/src/scalar.rs | 20 +++--- crates/jolt-sumcheck/src/tests.rs | 6 +- crates/jolt-sumcheck/src/verifier.rs | 6 +- crates/jolt-sumcheck/tests/committed.rs | 2 +- .../jolt-sumcheck/tests/mersenne61_compat.rs | 34 +++++++--- crates/jolt-sumcheck/tests/roundtrip.rs | 2 +- crates/jolt-sumcheck/tests/soundness.rs | 2 +- crates/jolt-transcript/src/digest.rs | 10 +-- crates/jolt-transcript/src/legacy.rs | 12 ++-- crates/jolt-transcript/tests/blake2b_tests.rs | 2 +- crates/jolt-verifier-derive/src/lib.rs | 18 ++--- crates/jolt-verifier/src/proof.rs | 10 +-- crates/jolt-verifier/src/stages/mod.rs | 4 +- crates/jolt-verifier/src/stages/relations.rs | 38 +++++------ .../src/stages/stage1/outer_remainder.rs | 14 ++-- .../src/stages/stage1/outputs.rs | 18 ++--- .../jolt-verifier/src/stages/stage1/verify.rs | 2 +- .../stage2/instruction_claim_reduction.rs | 10 +-- .../src/stages/stage2/outputs.rs | 18 ++--- .../src/stages/stage2/product_remainder.rs | 12 ++-- .../src/stages/stage2/product_uniskip.rs | 10 +-- .../src/stages/stage2/ram_output_check.rs | 12 ++-- .../src/stages/stage2/ram_raf_evaluation.rs | 12 ++-- .../stages/stage2/ram_read_write_checking.rs | 12 ++-- .../jolt-verifier/src/stages/stage2/verify.rs | 10 +-- .../src/stages/stage3/instruction_input.rs | 10 +-- .../src/stages/stage3/outputs.rs | 16 ++--- .../stage3/registers_claim_reduction.rs | 10 +-- .../src/stages/stage3/spartan_shift.rs | 10 +-- .../jolt-verifier/src/stages/stage3/verify.rs | 4 +- .../src/stages/stage4/outputs.rs | 20 +++--- .../src/stages/stage4/ram_val_check.rs | 30 ++++---- .../stage4/registers_read_write_checking.rs | 12 ++-- .../jolt-verifier/src/stages/stage4/verify.rs | 6 +- .../src/stages/stage5/instruction_read_raf.rs | 18 ++--- .../src/stages/stage5/outputs.rs | 16 ++--- .../stages/stage5/ram_ra_claim_reduction.rs | 14 ++-- .../stages/stage5/registers_val_evaluation.rs | 14 ++-- .../jolt-verifier/src/stages/stage5/verify.rs | 6 +- .../jolt-verifier/src/stages/stage6a/batch.rs | 6 +- .../src/stages/stage6a/booleanity.rs | 8 +-- .../src/stages/stage6a/bytecode_read_raf.rs | 16 ++--- .../src/stages/stage6a/outputs.rs | 16 ++--- .../src/stages/stage6a/verify.rs | 2 +- .../jolt-verifier/src/stages/stage6b/batch.rs | 8 +-- .../src/stages/stage6b/booleanity.rs | 8 +-- .../src/stages/stage6b/bytecode_read_raf.rs | 38 +++++------ .../committed_reduction_cycle_phase.rs | 36 +++++----- .../src/stages/stage6b/inc_claim_reduction.rs | 12 ++-- .../stage6b/instruction_ra_virtualization.rs | 14 ++-- .../src/stages/stage6b/outputs.rs | 24 +++---- .../stages/stage6b/ram_hamming_booleanity.rs | 8 +-- .../stages/stage6b/ram_ra_virtualization.rs | 12 ++-- .../src/stages/stage6b/verify.rs | 14 ++-- .../src/stages/stage7/advice_address_phase.rs | 18 ++--- .../committed_reduction_address_phase.rs | 14 ++-- .../stage7/hamming_weight_claim_reduction.rs | 12 ++-- .../src/stages/stage7/outputs.rs | 16 ++--- .../jolt-verifier/src/stages/stage7/verify.rs | 8 +-- .../src/stages/stage8/outputs.rs | 10 +-- .../jolt-verifier/src/stages/stage8/packed.rs | 14 ++-- .../src/stages/stage8/precommitted.rs | 16 ++--- .../src/stages/stage8/reconstruction.rs | 40 +++++------ .../jolt-verifier/src/stages/stage8/verify.rs | 10 +-- crates/jolt-verifier/src/stages/uniskip.rs | 12 ++-- .../src/stages/zk/blindfold/mod.rs | 24 +++---- .../src/stages/zk/blindfold/stage1.rs | 2 +- .../src/stages/zk/blindfold/stage2.rs | 4 +- .../src/stages/zk/blindfold/stage6b.rs | 2 +- .../jolt-verifier/src/stages/zk/committed.rs | 4 +- crates/jolt-verifier/src/verifier.rs | 8 +-- crates/jolt-verifier/tests/completeness/zk.rs | 4 +- .../tests/soundness/tampering/akita.rs | 22 +++--- .../tests/soundness/tampering/openings.rs | 2 +- .../tests/soundness/tampering/proof_shape.rs | 2 +- .../tests/soundness/tampering/sumcheck.rs | 2 +- .../tests/soundness/tampering/zk.rs | 8 +-- .../tests/statistical_independence/zk.rs | 2 +- .../tests/support/proof_claims.rs | 20 +++--- .../tests/support/tamper_manifest.rs | 4 +- .../jolt-verifier/tests/support/zk_audit.rs | 6 +- crates/jolt-witness/src/backend/fixed.rs | 6 +- crates/jolt-witness/src/backend/mod.rs | 14 ++-- .../jolt-witness/src/backend/trace/advice.rs | 8 ++- .../jolt-witness/src/backend/trace/cycle.rs | 10 ++- crates/jolt-witness/src/backend/trace/mod.rs | 2 +- .../jolt-witness/src/backend/trace/oracle.rs | 2 +- crates/jolt-witness/src/backend/trace/ram.rs | 8 +-- .../src/backend/trace/registers.rs | 2 +- .../jolt-witness/src/backend/trace/tests.rs | 2 +- crates/jolt-witness/src/bundle.rs | 2 +- crates/jolt-witness/src/field_inline/mod.rs | 32 +++++---- .../src/field_inline/witnesses.rs | 20 +++--- crates/jolt-witness/src/witnesses/flags.rs | 4 +- .../jolt-witness/src/witnesses/increments.rs | 6 +- crates/jolt-witness/src/witnesses/lookups.rs | 6 +- crates/jolt-witness/src/witnesses/mod.rs | 4 +- crates/jolt-witness/src/witnesses/operands.rs | 14 ++-- crates/jolt-witness/src/witnesses/pc.rs | 10 +-- crates/jolt-witness/src/witnesses/ram.rs | 10 +-- .../jolt-witness/src/witnesses/registers.rs | 8 +-- .../tests/field_inline_witness.rs | 2 +- jolt-eval/src/invariant/field_mul_scalar.rs | 10 +-- .../src/invariant/transcript_symmetry.rs | 6 +- .../src/objective/performance/field_mul.rs | 12 ++-- specs/jolt-field-rebuild.md | 16 +++++ tracer/src/instruction/field_inline.rs | 6 +- 399 files changed, 1991 insertions(+), 1923 deletions(-) diff --git a/crates/jolt-akita/benches/akita_paths.rs b/crates/jolt-akita/benches/akita_paths.rs index 042ec7fd9e..36f82be63b 100644 --- a/crates/jolt-akita/benches/akita_paths.rs +++ b/crates/jolt-akita/benches/akita_paths.rs @@ -54,7 +54,7 @@ use jolt_akita::{ AkitaScheme, AkitaSetupParams, AKITA_ONE_HOT_K256, }; use jolt_dory::{DoryCommitment, DoryHint, DoryScheme}; -use jolt_field::{Field, Fr, FromPrimitiveInt}; +use jolt_field::{Fr, JoltField, Ring}; use jolt_openings::{ prove_packed_openings, BatchOpeningScheme, CommitmentScheme, EvaluationClaim, PackedOpeningProof, PackedProverGroup, PackedProverObject, PrefixPackedStatement, @@ -233,15 +233,18 @@ fn criterion_filter_matches(group_name: &str) -> bool { }) } -fn field(value: u64) -> F { +fn field(value: u64) -> F { F::from_u64(value) } -fn deterministic_dense_poly(num_vars: usize) -> Polynomial { +fn deterministic_dense_poly(num_vars: usize) -> Polynomial { deterministic_dense_poly_with_offset(num_vars, 0) } -fn deterministic_dense_poly_with_offset(num_vars: usize, offset: u64) -> Polynomial { +fn deterministic_dense_poly_with_offset( + num_vars: usize, + offset: u64, +) -> Polynomial { let len = 1usize << num_vars; let evals = (0..len) .map(|i| field::(((i as u64 * 17 + offset * 19 + 5) % 31) + 1)) @@ -249,7 +252,7 @@ fn deterministic_dense_poly_with_offset(num_vars: usize, offset: u64) Polynomial::new(evals) } -fn deterministic_point(num_vars: usize) -> Vec { +fn deterministic_point(num_vars: usize) -> Vec { (0..num_vars) .map(|i| field::(((i as u64 * 7 + 11) % 97) + 2)) .collect() @@ -263,7 +266,7 @@ fn sparse_one_hot(num_vars: usize) -> OneHotPolynomial { OneHotPolynomial::new(AKITA_ONE_HOT_K256, indices) } -fn materialize_sparse(poly: &OneHotPolynomial) -> Polynomial { +fn materialize_sparse(poly: &OneHotPolynomial) -> Polynomial { let mut evals = vec![F::zero(); 1usize << poly.num_vars()]; >::for_each_one(poly, &mut |index| { evals[index] = F::one(); diff --git a/crates/jolt-akita/tests/pathologies.rs b/crates/jolt-akita/tests/pathologies.rs index 189bdcf773..41d526c8ca 100644 --- a/crates/jolt-akita/tests/pathologies.rs +++ b/crates/jolt-akita/tests/pathologies.rs @@ -13,7 +13,7 @@ use jolt_akita::{ AkitaBackendFlavor, AkitaBatchProof, AkitaCommitment, AkitaField, AkitaNativeBatchStatement, AkitaNativeBatching, AkitaScheme, }; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::{ BatchOpeningScheme, CommitmentScheme, OpeningsError, ZkBatchOpeningScheme, ZkOpeningScheme, }; @@ -24,7 +24,7 @@ use support::{batch_polynomials, f, layout, native_setup, polynomial, setup_for} type VerifierSetup = ::VerifierSetup; -fn require_jolt_field() {} +fn require_jolt_field() {} #[test] fn akita_field_satisfies_jolt_field_bundle() { diff --git a/crates/jolt-blindfold/src/builder.rs b/crates/jolt-blindfold/src/builder.rs index 636b71f3dc..0383ee95d6 100644 --- a/crates/jolt-blindfold/src/builder.rs +++ b/crates/jolt-blindfold/src/builder.rs @@ -1,5 +1,5 @@ use jolt_claims::Expr; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_sumcheck::{ CommittedOutputClaims, CommittedSumcheckConsistency, SumcheckDomainSpec, SumcheckStatement, }; @@ -64,7 +64,7 @@ impl Default for BlindFoldProtocolBuilder { impl BlindFoldProtocolBuilder where - F: Field + Clone, + F: JoltField + Clone, O: Clone + PartialEq, Com: Clone, P: Clone + PartialEq, diff --git a/crates/jolt-blindfold/src/error.rs b/crates/jolt-blindfold/src/error.rs index 3e356d615c..4ea870cb3a 100644 --- a/crates/jolt-blindfold/src/error.rs +++ b/crates/jolt-blindfold/src/error.rs @@ -1,5 +1,5 @@ use jolt_crypto::VectorOpeningError; -use jolt_field::FieldCore; +use jolt_field::Field; use jolt_r1cs::{ClaimLoweringError, ConstraintMatrixEvalError}; use jolt_sumcheck::{SumcheckError, SumcheckR1csError}; use thiserror::Error as ThisError; @@ -85,7 +85,7 @@ pub enum RelaxedError { } #[derive(Debug, ThisError)] -pub enum ProverError { +pub enum ProverError { #[error(transparent)] Relaxed(#[from] RelaxedError), #[error(transparent)] @@ -138,7 +138,7 @@ pub enum ProverError { } #[derive(Debug, ThisError)] -pub enum VerificationError { +pub enum VerificationError { #[error("claims have {claim_stages} stages but proof has {proof_stages}")] StageCountMismatch { claim_stages: usize, diff --git a/crates/jolt-blindfold/src/proof.rs b/crates/jolt-blindfold/src/proof.rs index 6f2fe189e5..6507e3b125 100644 --- a/crates/jolt-blindfold/src/proof.rs +++ b/crates/jolt-blindfold/src/proof.rs @@ -1,5 +1,5 @@ use jolt_crypto::VectorCommitmentOpening; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_sumcheck::CompressedSumcheckProof; use serde::{Deserialize, Serialize}; @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; serialize = "F: Serialize, Com: Serialize", deserialize = "F: for<'a> Deserialize<'a>, Com: Deserialize<'de>" ))] -pub struct BlindFoldProof { +pub struct BlindFoldProof { pub auxiliary_row_commitments: Vec, pub random_round_commitments: Vec, pub random_output_claim_row_commitments: Vec, diff --git a/crates/jolt-blindfold/src/protocol.rs b/crates/jolt-blindfold/src/protocol.rs index 6f206b2d07..eb3d4a7e99 100644 --- a/crates/jolt-blindfold/src/protocol.rs +++ b/crates/jolt-blindfold/src/protocol.rs @@ -1,7 +1,7 @@ use std::ops::Range; use jolt_crypto::HomomorphicCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_r1cs::{ConstraintMatrices, R1csBuilder, Variable}; use jolt_sumcheck::{CommittedOutputClaims, CommittedSumcheckConsistency}; @@ -11,7 +11,7 @@ use crate::{ }; #[derive(Clone, Debug)] -pub struct BlindFoldProtocol { +pub struct BlindFoldProtocol { pub sumcheck_consistency: Vec>, pub committed_output_claims: Vec>, pub r1cs: ConstraintMatrices, @@ -60,7 +60,7 @@ pub struct FinalOpeningWitnessCoordinates { impl BlindFoldProtocol where - F: Field, + F: JoltField, { pub fn builder() -> BlindFoldProtocolBuilder { BlindFoldProtocolBuilder::new() @@ -69,7 +69,7 @@ where impl BlindFoldProtocol where - F: Field + Clone, + F: JoltField + Clone, Com: Clone, { pub(crate) fn from_parts( @@ -103,7 +103,7 @@ where impl BlindFoldProtocol where - F: Field, + F: JoltField, Com: Clone + HomomorphicCommitment, { pub fn committed_relaxed_instance( @@ -166,7 +166,7 @@ where impl BlindFoldProtocol where - F: Field, + F: JoltField, { pub fn validate_cross_term_error_rows( &self, @@ -236,7 +236,7 @@ where impl BlindFoldProtocol where - F: Field, + F: JoltField, Com: Clone + HomomorphicCommitment, { pub fn random_relaxed_instance( @@ -310,7 +310,7 @@ where impl BlindFoldStatement where - F: Field + Clone, + F: JoltField + Clone, O: Clone + PartialEq, Com: Clone, P: Clone + PartialEq, @@ -443,7 +443,7 @@ where } impl Layout { - fn dimensions( + fn dimensions( &self, r1cs: &ConstraintMatrices, sumcheck_consistency: &[CommittedSumcheckConsistency], @@ -563,7 +563,7 @@ fn pad_rows( name: &'static str, ) -> Result<(), RelaxedError> where - F: Field, + F: JoltField, Com: Clone + HomomorphicCommitment, { if rows.len() > target_len { @@ -613,7 +613,7 @@ mod tests { }; use jolt_claims::{constant, opening, Expr}; use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_sumcheck::{ CommittedOutputClaims, CommittedRound, CommittedSumcheckProof, SumcheckDomainSpec, SumcheckError, SumcheckStatement, diff --git a/crates/jolt-blindfold/src/prove.rs b/crates/jolt-blindfold/src/prove.rs index 58c1f90065..d1038fddc0 100644 --- a/crates/jolt-blindfold/src/prove.rs +++ b/crates/jolt-blindfold/src/prove.rs @@ -1,5 +1,5 @@ use jolt_crypto::{HomomorphicCommitment, VectorCommitment, VectorCommitmentOpening}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, EqPolynomial, Polynomial, UnivariatePoly}; use jolt_r1cs::{ConstraintMatrices, ConstraintMatrixEvalError, SparseRow}; use jolt_sumcheck::{CompressedSumcheckProof, SUMCHECK_ROUND_TRANSCRIPT_LABEL}; @@ -14,7 +14,7 @@ const INNER_SUMCHECK_DEGREE: usize = 2; const INNER_SUMCHECK_LABEL: &[u8] = b"inner_sumcheck_poly"; #[derive(Clone, Copy, Debug)] -pub struct BlindFoldWitness<'a, F: Field> { +pub struct BlindFoldWitness<'a, F: JoltField> { pub rows: &'a [Vec], pub blindings: &'a [F], pub eval_outputs: &'a [F], @@ -23,7 +23,7 @@ pub struct BlindFoldWitness<'a, F: Field> { pub trait BlindFoldRowCommitter where - F: Field, + F: JoltField, VC: VectorCommitment, { fn commit_rows( @@ -136,7 +136,7 @@ pub struct DirectBlindFoldRowCommitter; impl BlindFoldRowCommitter for DirectBlindFoldRowCommitter where - F: Field, + F: JoltField, VC: VectorCommitment, { fn commit_rows( @@ -158,7 +158,7 @@ pub fn prove( rng: &mut R, ) -> Result, ProverError> where - F: Field + AppendToTranscript, + F: JoltField + AppendToTranscript, VC: VectorCommitment, VC::Output: HomomorphicCommitment + AppendToTranscript, T: Transcript, @@ -184,7 +184,7 @@ pub fn prove_with_row_committer( row_committer: &mut C, ) -> Result, ProverError> where - F: Field + AppendToTranscript, + F: JoltField + AppendToTranscript, VC: VectorCommitment, VC::Output: HomomorphicCommitment + AppendToTranscript, T: Transcript, @@ -590,7 +590,7 @@ fn validate_witness( witness: BlindFoldWitness<'_, F>, ) -> Result<(), ProverError> where - F: Field, + F: JoltField, VC: VectorCommitment, { let _ = log2_power_of_two("witness row count", protocol.dimensions.witness.row_count)?; @@ -638,7 +638,7 @@ fn ensure_row_capacity( row_len: usize, ) -> Result<(), ProverError> where - F: Field, + F: JoltField, VC: VectorCommitment, { let capacity = VC::capacity(setup); @@ -659,7 +659,7 @@ fn commit_rows( name: &'static str, ) -> Result, ProverError> where - F: Field, + F: JoltField, VC: VectorCommitment, { ensure_len(name, rows.len(), blindings.len())?; @@ -689,7 +689,7 @@ fn open_committed_rows( name: &'static str, ) -> Result<(VectorCommitmentOpening, F), ProverError> where - F: Field, + F: JoltField, VC: VectorCommitment, { let row_count = basis_len_from_point_len("row point", row_point.len())?; @@ -722,7 +722,7 @@ fn basis_len_from_point_len( point_len: usize, ) -> Result> where - F: Field, + F: JoltField, { if point_len >= usize::BITS as usize { return Err(ProverError::DimensionOverflow { @@ -734,7 +734,7 @@ where } #[derive(Clone, Debug)] -struct SumcheckTrace { +struct SumcheckTrace { proof: CompressedSumcheckProof, point: Vec, } @@ -748,7 +748,7 @@ fn prove_outer_sumcheck( transcript: &mut T, ) -> Result, ProverError> where - F: Field + AppendToTranscript, + F: JoltField + AppendToTranscript, T: Transcript, { let num_vars = log2_power_of_two("outer folded R1CS sumcheck", error_values.len())?; @@ -857,7 +857,7 @@ fn prove_inner_sumcheck( transcript: &mut T, ) -> Result, ProverError> where - F: Field + AppendToTranscript, + F: JoltField + AppendToTranscript, T: Transcript, { let witness_values = flatten(witness_rows); @@ -922,7 +922,7 @@ where fn matrix_vector_product(rows: &[SparseRow], vector: &[F]) -> Vec where - F: Field, + F: JoltField, { rows.par_iter().map(|row| dot(row, vector)).collect() } @@ -935,7 +935,7 @@ fn linear_form_project_columns( weights: [F; 3], ) -> Result, ProverError> where - F: Field, + F: JoltField, { if row_weights.len() < r1cs.num_constraints { return Err(ConstraintMatrixEvalError::RowWeightsLengthMismatch { @@ -988,7 +988,7 @@ fn project_matrix_columns( end_col: usize, weight: F, ) where - F: Field, + F: JoltField, { if weight.is_zero() { return; @@ -1005,7 +1005,7 @@ fn project_matrix_columns( fn abc_at_point(r1cs: &ConstraintMatrices, u: F, witness: &[F], point: &[F]) -> (F, F, F) where - F: Field, + F: JoltField, { let row_weights = EqPolynomial::::evals(point, None); let z = z_vector(u, witness); @@ -1029,7 +1029,7 @@ fn open_witness_coordinate( name: &'static str, ) -> Result<(VectorCommitmentOpening, F), ProverError> where - F: Field, + F: JoltField, VC: VectorCommitment, C: BlindFoldRowCommitter, { @@ -1099,7 +1099,7 @@ fn append_vector_opening( fn random_rows(row_count: usize, row_len: usize, rng: &mut R) -> Vec> where - F: Field, + F: JoltField, R: RngCore, { (0..row_count) @@ -1107,7 +1107,7 @@ where .collect() } -fn zero_rows(row_count: usize, row_len: usize) -> Vec> { +fn zero_rows(row_count: usize, row_len: usize) -> Vec> { vec![vec![F::zero(); row_len]; row_count] } @@ -1117,7 +1117,7 @@ fn fold_rows( challenge: F, ) -> Result>, ProverError> where - F: Field, + F: JoltField, { ensure_len("random witness rows", real.len(), random.len())?; let mut folded = Vec::with_capacity(real.len()); @@ -1147,7 +1147,7 @@ fn fold_scalars( challenge: F, ) -> Result, ProverError> where - F: Field, + F: JoltField, { ensure_len(name, real.len(), random.len())?; Ok(real @@ -1164,7 +1164,7 @@ fn fold_error_rows( challenge: F, ) -> Result>, ProverError> where - F: Field, + F: JoltField, { ensure_len("cross-term error rows", real.len(), cross.len())?; ensure_len("random error rows", real.len(), random.len())?; @@ -1209,7 +1209,7 @@ fn fold_error_scalars( challenge: F, ) -> Result, ProverError> where - F: Field, + F: JoltField, { ensure_len(name, real.len(), cross.len())?; ensure_len(name, real.len(), random.len())?; @@ -1230,7 +1230,7 @@ fn error_rows_for( row_len: usize, ) -> Result>, ProverError> where - F: Field, + F: JoltField, { let _ = log2_power_of_two("error row length", row_len)?; let target_len = row_count @@ -1260,7 +1260,7 @@ fn cross_term_error_rows_for( row_len: usize, ) -> Result>, ProverError> where - F: Field, + F: JoltField, { let _ = log2_power_of_two("error row length", row_len)?; let target_len = row_count @@ -1285,7 +1285,7 @@ where fn boolean_point(index: usize, num_vars: usize) -> Vec where - F: Field, + F: JoltField, { (0..num_vars) .map(|bit| { @@ -1301,7 +1301,7 @@ fn pad_to_len( target_len: usize, ) -> Result<(), ProverError> where - F: Field, + F: JoltField, { if values.len() > target_len { return Err(ProverError::LengthMismatch { @@ -1316,7 +1316,7 @@ where fn z_vector(u: F, witness: &[F]) -> Vec where - F: Field, + F: JoltField, { let mut z = Vec::with_capacity(witness.len() + 1); z.push(u); @@ -1326,7 +1326,7 @@ where fn dot(row: &[(usize, F)], witness: &[F]) -> F where - F: Field, + F: JoltField, { row.iter() .map(|&(column, coefficient)| coefficient * witness[column]) @@ -1335,14 +1335,14 @@ where fn flatten(rows: &[Vec]) -> Vec where - F: Field, + F: JoltField, { rows.iter().flat_map(|row| row.iter().copied()).collect() } fn ensure_len(name: &'static str, expected: usize, actual: usize) -> Result<(), ProverError> where - F: Field, + F: JoltField, { if expected != actual { return Err(ProverError::LengthMismatch { @@ -1356,7 +1356,7 @@ where fn log2_power_of_two(name: &'static str, value: usize) -> Result> where - F: Field, + F: JoltField, { if value == 0 || !value.is_power_of_two() { return Err(ProverError::InvalidPowerOfTwo { name, value }); diff --git a/crates/jolt-blindfold/src/r1cs.rs b/crates/jolt-blindfold/src/r1cs.rs index 382f2e4de0..e5b1a720f3 100644 --- a/crates/jolt-blindfold/src/r1cs.rs +++ b/crates/jolt-blindfold/src/r1cs.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use jolt_r1cs::{ assert_claim_expr_eq, ClaimSourceTable, ClaimSources, LinearCombination, R1csBuilder, Variable, }; @@ -55,7 +55,7 @@ pub struct FinalOpeningLayout { impl BlindFoldStatement where - F: Field, + F: JoltField, O: Clone + PartialEq, P: Clone + PartialEq, Ch: Clone + PartialEq, @@ -82,7 +82,7 @@ where impl BlindFoldStatement where - F: Field, + F: JoltField, { pub fn build( &self, @@ -245,7 +245,7 @@ where } } -fn allocate_private_row_scalar( +fn allocate_private_row_scalar( builder: &mut R1csBuilder, witness_row_len: usize, ) -> Variable { @@ -257,7 +257,7 @@ fn allocate_private_row_scalar( variable } -fn pad_to_witness_row_boundary(builder: &mut R1csBuilder, witness_row_len: usize) { +fn pad_to_witness_row_boundary(builder: &mut R1csBuilder, witness_row_len: usize) { while !(builder.num_vars() - 1).is_multiple_of(witness_row_len) { let _ = builder.alloc(F::zero()); } @@ -294,7 +294,7 @@ fn allocate_output_claim_rows( witness_row_len: usize, ) -> Vec where - F: Field, + F: JoltField, { let row_count = stage.output_claim_rows.commitments.commitments.len(); let row_len = stage.output_claim_rows.row_len; @@ -325,7 +325,7 @@ fn insert_output_claim_sources( claim_sources: &mut ClaimSourceTable, ) -> Result<(), Error> where - F: Field, + F: JoltField, O: Clone + PartialEq, { let mut inserted = Vec::<(O, Variable)>::new(); @@ -411,7 +411,7 @@ mod tests { use super::*; use crate::{BlindFoldStage, BlindFoldStatement, CommittedClaimRows, OpeningAlias}; use jolt_claims::{challenge, constant, derived, opening, Expr}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_r1cs::{ClaimLoweringError, ClaimSourceTable, R1csBuilderError}; use jolt_sumcheck::{ CommittedOutputClaims, CommittedSumcheckConsistency, SumcheckDomainSpec, SumcheckR1csError, diff --git a/crates/jolt-blindfold/src/relaxed.rs b/crates/jolt-blindfold/src/relaxed.rs index 7398247d32..39ca4af0dc 100644 --- a/crates/jolt-blindfold/src/relaxed.rs +++ b/crates/jolt-blindfold/src/relaxed.rs @@ -1,5 +1,5 @@ use jolt_crypto::HomomorphicCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use crate::RelaxedError; @@ -29,7 +29,7 @@ impl RelaxedInstance { impl RelaxedInstance where - F: Field, + F: JoltField, Com: HomomorphicCommitment, { pub fn fold( @@ -118,7 +118,7 @@ impl RelaxedWitness { } } -impl RelaxedWitness { +impl RelaxedWitness { pub fn fold( &self, random: &Self, @@ -218,7 +218,7 @@ fn ensure_len(name: &'static str, expected: usize, actual: usize) -> Result<(), mod tests { use super::*; use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn f(value: u64) -> Fr { Fr::from_u64(value) diff --git a/crates/jolt-blindfold/src/verify.rs b/crates/jolt-blindfold/src/verify.rs index 45c5a6f889..e786c7e85b 100644 --- a/crates/jolt-blindfold/src/verify.rs +++ b/crates/jolt-blindfold/src/verify.rs @@ -1,5 +1,5 @@ use jolt_crypto::{HomomorphicCommitment, VectorCommitment, VectorCommitmentOpening}; -use jolt_field::{Field, FieldCore}; +use jolt_field::{Field, JoltField}; use jolt_poly::EqPolynomial; use jolt_r1cs::{ConstraintMatrices, MatrixColumnContributions}; use jolt_sumcheck::{BooleanHypercube, SumcheckClaim, SUMCHECK_ROUND_TRANSCRIPT_LABEL}; @@ -16,7 +16,7 @@ const INNER_SUMCHECK_LABEL: &[u8] = b"inner_sumcheck_poly"; impl BlindFoldProtocol where - F: Field + AppendToTranscript, + F: JoltField + AppendToTranscript, Com: Copy + HomomorphicCommitment + AppendToTranscript, { pub fn verify( @@ -50,7 +50,7 @@ where impl BlindFoldProtocol where - F: Field + AppendToTranscript, + F: JoltField + AppendToTranscript, Com: Clone + HomomorphicCommitment + AppendToTranscript, { fn folded_instance_from_proof( @@ -123,7 +123,7 @@ where impl BlindFoldProtocol where - F: Field + AppendToTranscript, + F: JoltField + AppendToTranscript, Com: Copy + HomomorphicCommitment + AppendToTranscript, { fn verify_outer_folded_r1cs( @@ -214,7 +214,7 @@ where impl BlindFoldProof where - F: Field, + F: JoltField, Com: Copy + AppendToTranscript, { fn verify_folded_eval_commitments( @@ -243,7 +243,7 @@ where impl BlindFoldProtocol where - F: Field + AppendToTranscript, + F: JoltField + AppendToTranscript, Com: Copy + HomomorphicCommitment + AppendToTranscript, { fn verify_folded_eval_witness_bindings( @@ -338,7 +338,7 @@ where } impl WitnessCoordinate { - fn require_dedicated_row( + fn require_dedicated_row( self, opening: &VectorCommitmentOpening, kind: &'static str, @@ -359,7 +359,7 @@ impl WitnessCoordinate { opening: &VectorCommitmentOpening, ) -> Result> where - F: Field, + F: JoltField, VC: VectorCommitment, VC::Output: Copy + HomomorphicCommitment, { @@ -394,7 +394,7 @@ impl WitnessCoordinate { impl BlindFoldProtocol where - F: Field + AppendToTranscript, + F: JoltField + AppendToTranscript, Com: Copy + HomomorphicCommitment + AppendToTranscript, { fn verify_inner_folded_r1cs( @@ -511,7 +511,7 @@ fn public_contributions( u: F, ) -> Result, VerificationError> where - F: Field, + F: JoltField, { let eq_rx = EqPolynomial::::evals(rx, None); Ok(r1cs.public_column_contributions(&eq_rx, 0, u)?) @@ -526,7 +526,7 @@ fn compute_l_w_at_ry( rc: F, ) -> Result> where - F: Field, + F: JoltField, { let eq_rx = EqPolynomial::::evals(rx, None); let eq_ry = EqPolynomial::::evals(ry, None); @@ -536,7 +536,7 @@ where fn power_of_two_len(name: &'static str, num_vars: usize) -> Result> where - F: FieldCore, + F: Field, { if num_vars >= usize::BITS as usize { return Err(VerificationError::InvalidPowerOfTwo { @@ -549,7 +549,7 @@ where fn boolean_point(index: usize, num_vars: usize) -> Result, VerificationError> where - F: Field, + F: JoltField, { let len = power_of_two_len::("boolean point dimension", num_vars)?; if index >= len { @@ -592,7 +592,7 @@ mod tests { use jolt_crypto::{ Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment, VectorOpeningError, }; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_poly::CompressedPoly; use jolt_r1cs::ConstraintMatrices; use jolt_sumcheck::CompressedSumcheckProof; diff --git a/crates/jolt-blindfold/tests/support/mod.rs b/crates/jolt-blindfold/tests/support/mod.rs index c314f3f190..ed591fb2e3 100644 --- a/crates/jolt-blindfold/tests/support/mod.rs +++ b/crates/jolt-blindfold/tests/support/mod.rs @@ -12,7 +12,7 @@ use jolt_claims::{challenge, constant, derived, opening, Expr}; use jolt_crypto::{ Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment, VectorCommitmentOpening, }; -use jolt_field::{CanonicalBytes, FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{CanonicalBytes, Field, Fr, Ring}; use jolt_poly::{CompressedPoly, EqPolynomial}; use jolt_r1cs::{ClaimSourceTable, ConstraintMatrices, R1csBuilder}; use jolt_sumcheck::{ @@ -108,7 +108,7 @@ pub fn f(value: u64) -> F { pub fn rng_field(rng: &mut impl RngCore) -> F { let mut bytes = [0u8; 32]; rng.fill_bytes(&mut bytes); - ::from_le_bytes_mod_order(&bytes) + ::from_bytes_le_reduced(&bytes) } pub fn inverse(value: F) -> F { diff --git a/crates/jolt-claims-derive/src/lib.rs b/crates/jolt-claims-derive/src/lib.rs index bcabfd95b8..b562f62711 100644 --- a/crates/jolt-claims-derive/src/lib.rs +++ b/crates/jolt-claims-derive/src/lib.rs @@ -464,7 +464,7 @@ fn expand_output(input: DeriveInput) -> syn::Result { Ok(quote! { // The value resolver lives on the value cell (`C = F`): each field is read // as `F` (or `Vec` / `Option`) directly. - impl ::jolt_claims::OutputClaims for #name { + impl ::jolt_claims::OutputClaims for #name { fn canonical_order(&self) -> ::std::vec::Vec<#id_ty> { ::core::iter::empty::<#id_ty>() #(#order_chains)* @@ -492,7 +492,7 @@ fn expand_output(input: DeriveInput) -> syn::Result { // (`C = Vec`): each field is a `Vec` point (or `Vec>` / // `Option>`). A field and its accessor share a name; `x` reads the // field, `x()` calls the accessor. - impl #name<::std::vec::Vec> { + impl #name<::std::vec::Vec> { #(#point_accessors)* } }) @@ -581,7 +581,7 @@ fn expand_input(input: DeriveInput) -> syn::Result { let point_accessors = plans.iter().map(point_accessor); Ok(quote! { - impl ::jolt_claims::InputClaims for #name { + impl ::jolt_claims::InputClaims for #name { fn canonical_order(&self) -> ::std::vec::Vec<#id_ty> { ::core::iter::empty::<#id_ty>() #(#order_chains)* @@ -597,7 +597,7 @@ fn expand_input(input: DeriveInput) -> syn::Result { } } - impl #name<::std::vec::Vec> { + impl #name<::std::vec::Vec> { #(#point_accessors)* } }) @@ -706,7 +706,7 @@ fn expand_challenges(input: DeriveInput) -> syn::Result { } Ok(quote! { - impl<#field: ::jolt_field::Field> ::jolt_claims::SumcheckChallenges<#field> for #name<#field> { + impl<#field: ::jolt_field::JoltField> ::jolt_claims::SumcheckChallenges<#field> for #name<#field> { fn from_transcript_values<__I: ::core::iter::Iterator>( values: __I, ) -> ::core::result::Result { diff --git a/crates/jolt-claims/src/claim_data.rs b/crates/jolt-claims/src/claim_data.rs index aecfdee47d..79c91c7b2c 100644 --- a/crates/jolt-claims/src/claim_data.rs +++ b/crates/jolt-claims/src/claim_data.rs @@ -15,7 +15,7 @@ //! verifier-side `append_openings` is a thin consumer of [`OutputClaims::opening_values`], //! so it cannot disagree with the canonical order defined here. -use jolt_field::Field; +use jolt_field::JoltField; use thiserror::Error; use crate::protocols::jolt::{JoltChallengeId, JoltOpeningId}; @@ -59,7 +59,7 @@ pub struct MissingOpeningValue { /// /// Generic over the opening-id type `O` (defaulting to [`JoltOpeningId`]) so the /// trait can live in the framework half and be reused by other protocol families. -pub trait OutputClaims { +pub trait OutputClaims { /// Produced opening scalars in canonical (field-declaration) order. Built from /// [`canonical_order`](Self::canonical_order) + [`resolve_output`](Self::resolve_output), /// both derived from the same fields, so `opening_values()[k]` is exactly @@ -111,7 +111,7 @@ pub trait OutputClaims { /// (populated by explicit cross-stage wiring). /// /// Generic over the opening-id type `O` (defaulting to [`JoltOpeningId`]). -pub trait InputClaims { +pub trait InputClaims { /// The consumed opening ids in canonical (field-declaration) order. Takes /// `&self` because `Vec`/`Option` fields make the length and presence /// instance-dependent. @@ -129,7 +129,7 @@ pub trait InputClaims { /// reads each field's value directly. /// /// Generic over the challenge-id type `C` (defaulting to [`JoltChallengeId`]). -pub trait SumcheckChallenges: Sized { +pub trait SumcheckChallenges: Sized { /// Build this `Challenges` struct from already-drawn Fiat-Shamir scalars, /// consuming one value per field in canonical (field-declaration) order. /// @@ -158,7 +158,7 @@ pub trait SumcheckChallenges: Sized { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct NoChallenges(::core::marker::PhantomData); -impl SumcheckChallenges for NoChallenges { +impl SumcheckChallenges for NoChallenges { fn from_transcript_values>( _values: I, ) -> Result { @@ -183,7 +183,7 @@ mod sumcheck_challenges_tests { // The `SumcheckChallenges` re-export from the crate root covers both the trait // (type namespace) and the derive macro (macro namespace). use crate::{ChallengeDrawError, SumcheckChallenges}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { Fr::from_u64(value) diff --git a/crates/jolt-claims/src/claims.rs b/crates/jolt-claims/src/claims.rs index 5d3f401f10..7d285d6f1b 100644 --- a/crates/jolt-claims/src/claims.rs +++ b/crates/jolt-claims/src/claims.rs @@ -1,4 +1,4 @@ -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; /// An atomic value used inside a symbolic claim expression. @@ -30,7 +30,7 @@ impl Term { } } -impl Term { +impl Term { pub fn source(source: Source) -> Self { Self { coefficient: F::one(), @@ -55,7 +55,7 @@ impl Expr { } } -impl Expr { +impl Expr { pub fn one() -> Self { Self { terms: vec![Term::constant(F::one())], @@ -125,7 +125,7 @@ impl Expr { } } -impl Expr { +impl Expr { pub fn pow(self, mut exponent: usize) -> Self { let mut result = Self::one(); let mut base = self; @@ -145,35 +145,35 @@ impl Expr { } /// Builds an opening source expression. -pub fn opening(id: impl Into) -> Expr { +pub fn opening(id: impl Into) -> Expr { Expr { terms: vec![Term::source(Source::Opening(id.into()))], } } /// Builds a Fiat-Shamir challenge source expression. -pub fn challenge(id: impl Into) -> Expr { +pub fn challenge(id: impl Into) -> Expr { Expr { terms: vec![Term::source(Source::Challenge(id.into()))], } } /// Builds a named derived-value source expression. -pub fn derived(id: impl Into

) -> Expr { +pub fn derived(id: impl Into

) -> Expr { Expr { terms: vec![Term::source(Source::Derived(id.into()))], } } /// Builds a constant expression. -pub fn constant(value: F) -> Expr { +pub fn constant(value: F) -> Expr { Expr::constant(value) } #[cfg(test)] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt, RingCore}; + use jolt_field::{Fr, Ring}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] enum Opening { diff --git a/crates/jolt-claims/src/ops.rs b/crates/jolt-claims/src/ops.rs index aa3c1fa0eb..5d848bdb86 100644 --- a/crates/jolt-claims/src/ops.rs +++ b/crates/jolt-claims/src/ops.rs @@ -1,10 +1,10 @@ use std::ops::{Add, Mul, Neg, Sub}; -use jolt_field::{FromPrimitiveInt, RingCore}; +use jolt_field::Ring; use crate::{Expr, Term}; -impl From> for Expr { +impl From> for Expr { fn from(term: Term) -> Self { if term.coefficient.is_zero() && term.factors.is_empty() { Self::zero() @@ -14,7 +14,7 @@ impl From> for Expr { } } -impl From for Expr { +impl From for Expr { fn from(value: i128) -> Self { Self::constant(F::from_i128(value)) } @@ -54,7 +54,7 @@ impl Add<&Expr> for &Expr Add for Expr { +impl Add for Expr { type Output = Self; fn add(self, rhs: i128) -> Self::Output { @@ -62,7 +62,7 @@ impl Add for Expr { } } -impl Add> for i128 { +impl Add> for i128 { type Output = Expr; fn add(self, rhs: Expr) -> Self::Output { @@ -70,7 +70,7 @@ impl Add> for i128 { } } -impl Sub for Expr { +impl Sub for Expr { type Output = Self; fn sub(self, rhs: Self) -> Self::Output { @@ -78,9 +78,7 @@ impl Sub for Expr { } } -impl Sub<&Expr> - for Expr -{ +impl Sub<&Expr> for Expr { type Output = Self; fn sub(self, rhs: &Expr) -> Self::Output { @@ -88,9 +86,7 @@ impl Sub<&Expr> } } -impl Sub> - for &Expr -{ +impl Sub> for &Expr { type Output = Expr; fn sub(self, rhs: Expr) -> Self::Output { @@ -98,9 +94,7 @@ impl Sub> } } -impl Sub<&Expr> - for &Expr -{ +impl Sub<&Expr> for &Expr { type Output = Expr; fn sub(self, rhs: &Expr) -> Self::Output { @@ -108,7 +102,7 @@ impl Sub<&Expr> } } -impl Sub for Expr { +impl Sub for Expr { type Output = Self; fn sub(self, rhs: i128) -> Self::Output { @@ -116,7 +110,7 @@ impl Sub for Expr { } } -impl Sub> for i128 { +impl Sub> for i128 { type Output = Expr; fn sub(self, rhs: Expr) -> Self::Output { @@ -124,7 +118,7 @@ impl Sub> for i128 { } } -impl Neg for Expr { +impl Neg for Expr { type Output = Self; fn neg(mut self) -> Self::Output { @@ -135,7 +129,7 @@ impl Neg for Expr { } } -impl Neg for &Expr { +impl Neg for &Expr { type Output = Expr; fn neg(self) -> Self::Output { @@ -143,7 +137,7 @@ impl Neg for &Expr Mul for Expr { +impl Mul for Expr { type Output = Self; fn mul(self, rhs: Self) -> Self::Output { @@ -166,7 +160,7 @@ impl Mul for Expr { } } -impl Mul<&Expr> for Expr { +impl Mul<&Expr> for Expr { type Output = Self; fn mul(self, rhs: &Expr) -> Self::Output { @@ -174,7 +168,7 @@ impl Mul<&Expr> for Expr< } } -impl Mul> for &Expr { +impl Mul> for &Expr { type Output = Expr; fn mul(self, rhs: Expr) -> Self::Output { @@ -182,7 +176,7 @@ impl Mul> for &Expr< } } -impl Mul<&Expr> for &Expr { +impl Mul<&Expr> for &Expr { type Output = Expr; fn mul(self, rhs: &Expr) -> Self::Output { @@ -190,7 +184,7 @@ impl Mul<&Expr> for &Expr } } -impl Mul for Expr { +impl Mul for Expr { type Output = Self; fn mul(mut self, rhs: i128) -> Self::Output { @@ -205,7 +199,7 @@ impl Mul for Expr { } } -impl Mul> for i128 { +impl Mul> for i128 { type Output = Expr; fn mul(self, rhs: Expr) -> Self::Output { @@ -216,7 +210,7 @@ impl Mul> for i128 { #[cfg(test)] mod tests { use crate::{challenge, constant, opening, Expr}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] enum Opening { diff --git a/crates/jolt-claims/src/protocols/field_inline/geometry/bytecode.rs b/crates/jolt-claims/src/protocols/field_inline/geometry/bytecode.rs index e3ee46670e..51fef5233b 100644 --- a/crates/jolt-claims/src/protocols/field_inline/geometry/bytecode.rs +++ b/crates/jolt-claims/src/protocols/field_inline/geometry/bytecode.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use jolt_lookup_tables::{LookupTableKind, XLEN}; use jolt_poly::EqPolynomial; use jolt_riscv::NUM_CIRCUIT_FLAGS; @@ -144,7 +144,7 @@ pub fn validate_bytecode_rows( } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct FieldInlineBytecodeReadRafPublicValues { +pub struct FieldInlineBytecodeReadRafPublicValues { pub stage_values: [F; 5], } @@ -167,7 +167,7 @@ pub fn read_raf_public_values( inputs: FieldInlineBytecodeReadRafEvaluationInputs<'_, F>, ) -> Result, FieldInlineBytecodeReadRafError> where - F: Field, + F: JoltField, { require_len( inputs.stage1_gammas, @@ -245,7 +245,7 @@ pub fn read_raf_register_eq_evals( field_register_val_evaluation_point: &[F], ) -> FieldInlineBytecodeReadRafRegisterEqEvals where - F: Field, + F: JoltField, { FieldInlineBytecodeReadRafRegisterEqEvals { read_write: EqPolynomial::::evals(field_register_read_write_point, None), @@ -257,7 +257,7 @@ pub fn read_raf_stage_values( inputs: FieldInlineBytecodeReadRafStageValueInputs<'_, F>, ) -> Vec<[F; 5]> where - F: Field, + F: JoltField, { let field_register_eq = read_raf_register_eq_evals( inputs.field_register_read_write_point, @@ -288,7 +288,7 @@ pub fn read_raf_row_values( stage5_gammas: &[F], ) -> [F; 5] where - F: Field, + F: JoltField, { let mut stage1 = F::zero(); for (index, flag) in FIELD_INLINE_BYTECODE_STAGE1_FLAGS.into_iter().enumerate() { @@ -393,7 +393,7 @@ fn validate_register( Ok(()) } -fn register_eq(register: Option, eq: &[F]) -> F { +fn register_eq(register: Option, eq: &[F]) -> F { register .and_then(|register| eq.get(register as usize)) .copied() @@ -415,7 +415,7 @@ fn require_len(values: &[F], expected: usize) -> Result<(), FieldInlineByteco #[cfg(test)] #[expect(clippy::panic)] mod tests { - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_poly::EqPolynomial; use super::*; diff --git a/crates/jolt-claims/src/protocols/field_inline/geometry/dimensions.rs b/crates/jolt-claims/src/protocols/field_inline/geometry/dimensions.rs index c567d71037..746a39f995 100644 --- a/crates/jolt-claims/src/protocols/field_inline/geometry/dimensions.rs +++ b/crates/jolt-claims/src/protocols/field_inline/geometry/dimensions.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::protocols::jolt::geometry::dimensions::JoltFormulaPointError; @@ -60,7 +60,7 @@ impl FieldRegistersReadWriteDimensions { self.log_t + self.log_k } - pub fn read_write_opening_point( + pub fn read_write_opening_point( self, challenges: &[F], ) -> Result, JoltFormulaPointError> { @@ -112,7 +112,7 @@ impl FieldRegistersReadWriteDimensions { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct FieldRegistersReadWriteOpeningPoint { +pub struct FieldRegistersReadWriteOpeningPoint { pub r_address: Vec, pub r_cycle: Vec, pub opening_point: Vec, diff --git a/crates/jolt-claims/src/protocols/field_inline/relations/claim_reductions/increments.rs b/crates/jolt-claims/src/protocols/field_inline/relations/claim_reductions/increments.rs index f85c83dfa5..2a896549b9 100644 --- a/crates/jolt-claims/src/protocols/field_inline/relations/claim_reductions/increments.rs +++ b/crates/jolt-claims/src/protocols/field_inline/relations/claim_reductions/increments.rs @@ -1,6 +1,6 @@ //! field_inline rd-inc claim-reduction symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use crate::protocols::field_inline::geometry::claim_reductions::increments::{ field_rd_inc_read_write, field_rd_inc_reduced, field_rd_inc_val_evaluation, @@ -45,13 +45,13 @@ impl SymbolicSumcheck for ClaimReduction { 2 } - fn input_expression(&self) -> FieldInlineExpr { + fn input_expression(&self) -> FieldInlineExpr { let eta = challenge(FieldRegistersIncClaimReductionChallenge::Gamma); opening(field_rd_inc_read_write()) + eta * opening(field_rd_inc_val_evaluation()) } - fn output_expression(&self) -> FieldInlineExpr { + fn output_expression(&self) -> FieldInlineExpr { let eta = challenge(FieldRegistersIncClaimReductionChallenge::Gamma); let output_coeff = derived(FieldRegistersIncClaimReductionPublic::EqReadWrite) @@ -64,7 +64,7 @@ impl SymbolicSumcheck for ClaimReduction { mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn dimensions() -> FieldRegistersTraceDimensions { FieldRegistersTraceDimensions::new(5) diff --git a/crates/jolt-claims/src/protocols/field_inline/relations/claim_reductions/registers.rs b/crates/jolt-claims/src/protocols/field_inline/relations/claim_reductions/registers.rs index 7efd16c4a2..b1a4e36b10 100644 --- a/crates/jolt-claims/src/protocols/field_inline/relations/claim_reductions/registers.rs +++ b/crates/jolt-claims/src/protocols/field_inline/relations/claim_reductions/registers.rs @@ -1,6 +1,6 @@ //! field_inline registers claim-reduction symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use crate::protocols::field_inline::geometry::claim_reductions::registers::{ field_rd_value_reduced, field_rd_value_spartan, field_rs1_value_reduced, @@ -46,7 +46,7 @@ impl SymbolicSumcheck for ClaimReduction { 2 } - fn input_expression(&self) -> FieldInlineExpr { + fn input_expression(&self) -> FieldInlineExpr { let gamma = challenge(FieldRegistersClaimReductionChallenge::Gamma); opening(field_rd_value_spartan()) @@ -54,7 +54,7 @@ impl SymbolicSumcheck for ClaimReduction { + gamma.clone().pow(2) * opening(field_rs2_value_spartan()) } - fn output_expression(&self) -> FieldInlineExpr { + fn output_expression(&self) -> FieldInlineExpr { let gamma = challenge(FieldRegistersClaimReductionChallenge::Gamma); let eq_spartan = derived(FieldRegistersClaimReductionPublic::EqSpartan); @@ -68,7 +68,7 @@ impl SymbolicSumcheck for ClaimReduction { mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn dimensions() -> FieldRegistersTraceDimensions { FieldRegistersTraceDimensions::new(5) diff --git a/crates/jolt-claims/src/protocols/field_inline/relations/product.rs b/crates/jolt-claims/src/protocols/field_inline/relations/product.rs index 9beed8b11b..65c3977e0a 100644 --- a/crates/jolt-claims/src/protocols/field_inline/relations/product.rs +++ b/crates/jolt-claims/src/protocols/field_inline/relations/product.rs @@ -1,6 +1,6 @@ //! field_inline native product symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use crate::opening; use crate::protocols::field_inline::geometry::product::{ @@ -44,11 +44,11 @@ impl SymbolicSumcheck for FieldProduct { 2 } - fn input_expression(&self) -> FieldInlineExpr { + fn input_expression(&self) -> FieldInlineExpr { opening(field_product_opening()) } - fn output_expression(&self) -> FieldInlineExpr { + fn output_expression(&self) -> FieldInlineExpr { opening(field_rs1_value_product()) * opening(field_rs2_value_product()) } } @@ -61,7 +61,7 @@ mod tests { selected_product_remainder_output_openings, selected_product_uniskip_input_openings, FieldRegistersProductLane, }; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn dimensions() -> FieldRegistersTraceDimensions { FieldRegistersTraceDimensions::new(5) diff --git a/crates/jolt-claims/src/protocols/field_inline/relations/registers.rs b/crates/jolt-claims/src/protocols/field_inline/relations/registers.rs index 6cdfb24f75..f221222620 100644 --- a/crates/jolt-claims/src/protocols/field_inline/relations/registers.rs +++ b/crates/jolt-claims/src/protocols/field_inline/relations/registers.rs @@ -1,6 +1,6 @@ //! field_inline registers symbolic sumcheck relations. -use jolt_field::RingCore; +use jolt_field::Ring; use crate::protocols::field_inline::geometry::registers::{ field_rd_inc_read_write, field_rd_inc_val_evaluation, field_rd_value_claim, @@ -49,14 +49,14 @@ impl SymbolicSumcheck for ReadWriteChecking { 3 } - fn input_expression(&self) -> FieldInlineExpr { + fn input_expression(&self) -> FieldInlineExpr { let gamma = challenge(FieldRegistersReadWriteChallenge::Gamma); opening(field_rd_value_claim()) + gamma.clone() * opening(field_rs1_value_claim()) + gamma.clone().pow(2) * opening(field_rs2_value_claim()) } - fn output_expression(&self) -> FieldInlineExpr { + fn output_expression(&self) -> FieldInlineExpr { let gamma = challenge(FieldRegistersReadWriteChallenge::Gamma); let eq_cycle = derived(FieldRegistersReadWritePublic::EqCycle); eq_cycle.clone() * opening(field_rd_wa_read_write()) * opening(field_rd_inc_read_write()) @@ -106,11 +106,11 @@ impl SymbolicSumcheck for ValEvaluation { 3 } - fn input_expression(&self) -> FieldInlineExpr { + fn input_expression(&self) -> FieldInlineExpr { opening(field_registers_val_read_write()) } - fn output_expression(&self) -> FieldInlineExpr { + fn output_expression(&self) -> FieldInlineExpr { derived(FieldRegistersValEvaluationPublic::LtCycle) * opening(field_rd_inc_val_evaluation()) * opening(field_rd_wa_val_evaluation()) @@ -121,7 +121,7 @@ impl SymbolicSumcheck for ValEvaluation { mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn trace_dimensions() -> FieldRegistersTraceDimensions { FieldRegistersTraceDimensions::new(5) diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/booleanity.rs b/crates/jolt-claims/src/protocols/jolt/geometry/booleanity.rs index 61dd59c52c..30bbbd7943 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/booleanity.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/booleanity.rs @@ -1,4 +1,4 @@ -use jolt_field::RingCore; +use jolt_field::Ring; use crate::{challenge, derived, opening}; @@ -31,7 +31,7 @@ impl BooleanityDimensions { pub(crate) fn booleanity_cycle_output(dimensions: BooleanityDimensions) -> JoltExpr where - F: RingCore, + F: Ring, { booleanity_output(booleanity_output_openings(dimensions.layout)) } @@ -41,7 +41,7 @@ where /// so the formula has one owner. pub(crate) fn booleanity_output(openings: impl IntoIterator) -> JoltExpr where - F: RingCore, + F: Ring, { let gamma = challenge(BooleanityChallenge::Gamma); let eq_address_cycle = derived(BooleanityPublic::EqAddressCycle); diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/bytecode.rs b/crates/jolt-claims/src/protocols/jolt/geometry/bytecode.rs index 0c222c7070..6b3f913f4a 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/bytecode.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/bytecode.rs @@ -1,4 +1,4 @@ -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use jolt_lookup_tables::{InstructionLookupTable, LookupTableKind, XLEN}; use jolt_poly::{EqPolynomial, IdentityPolynomial, MultilinearEvaluation}; use jolt_riscv::{ @@ -84,7 +84,7 @@ impl BytecodeReadRafDimensions { /// the constant entry term at the next three powers. pub(crate) fn read_raf_address_input_fold(extra_stage_claims: Vec>) -> JoltExpr where - F: RingCore, + F: Ring, { let gamma = challenge(BytecodeReadRafChallenge::Gamma); let base_stages = BYTECODE_STAGE_GAMMA_COUNTS.len(); @@ -108,7 +108,7 @@ pub(crate) fn read_raf_cycle_output( num_val_stages: usize, ) -> JoltExpr where - F: RingCore, + F: Ring, { let gamma = challenge(BytecodeReadRafChallenge::Gamma); let mut output_coeff = JoltExpr::zero(); @@ -129,7 +129,7 @@ pub(crate) fn read_raf_cycle_output_committed( num_val_stages: usize, ) -> JoltExpr where - F: RingCore, + F: Ring, { let gamma = challenge(BytecodeReadRafChallenge::Gamma); // The staged Val factor multiplies after the RA product so the lowered @@ -183,7 +183,7 @@ pub fn fused_inc_read_raf_opening() -> JoltOpeningId { /// `FusedInc` opening as a cycle factor (degree +1 over the base relation). pub(crate) fn read_raf_cycle_output_lattice(dimensions: BytecodeReadRafDimensions) -> JoltExpr where - F: RingCore, + F: Ring, { let gamma = challenge(BytecodeReadRafChallenge::Gamma); let base_stages = BYTECODE_STAGE_GAMMA_COUNTS.len(); @@ -217,7 +217,7 @@ pub(crate) fn read_raf_cycle_output_committed_lattice( dimensions: BytecodeReadRafDimensions, ) -> JoltExpr where - F: RingCore, + F: Ring, { let gamma = challenge(BytecodeReadRafChallenge::Gamma); let base_stages = BYTECODE_STAGE_GAMMA_COUNTS.len(); @@ -268,14 +268,14 @@ pub fn bytecode_read_raf_address_phase_opening() -> JoltOpeningId { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct BytecodeReadRafPublicValues { +pub struct BytecodeReadRafPublicValues { pub stage_values: [F; 5], pub spartan_outer_raf: F, pub spartan_shift_raf: F, pub entry: F, } -impl BytecodeReadRafPublicValues { +impl BytecodeReadRafPublicValues { /// Returns `None` for committed-mode publics (`StageCycleEq`) and /// out-of-range stage indices so a wrong-mode formula fails loudly at the /// source instead of evaluating with a silently zeroed term. @@ -296,14 +296,14 @@ impl BytecodeReadRafPublicValues { /// per relation stage — five in base mode, nine in lattice mode (the four /// fused-inc consumer stages follow the base five). #[derive(Clone, Debug, PartialEq, Eq)] -pub struct BytecodeReadRafCommittedPublicValues { +pub struct BytecodeReadRafCommittedPublicValues { pub stage_cycle_eqs: [F; READ_RAF_CYCLE_STAGES], pub spartan_outer_raf: F, pub spartan_shift_raf: F, pub entry: F, } -impl BytecodeReadRafCommittedPublicValues { +impl BytecodeReadRafCommittedPublicValues { /// Returns `None` for full-mode publics (`StageValue`) and out-of-range /// stage indices so a wrong-mode formula fails loudly at the source /// instead of evaluating with a silently zeroed term. @@ -331,7 +331,7 @@ pub fn read_raf_committed_public_values( inputs: BytecodeReadRafCommittedEvaluationInputs<'_, F>, ) -> BytecodeReadRafCommittedPublicValues where - F: Field, + F: JoltField, { let stage_cycle_eqs = inputs .stage_cycle_points @@ -363,7 +363,7 @@ fn read_raf_raf_entry_publics( entry_bytecode_index: usize, ) -> (F, F, F) where - F: Field, + F: JoltField, { let identity = IdentityPolynomial::new(r_address.len()).evaluate(r_address); let spartan_outer_raf = identity * outer_stage_cycle_eq; @@ -416,7 +416,7 @@ fn read_raf_register_eq_evals( register_val_evaluation_point: &[F], ) -> BytecodeReadRafRegisterEqEvals where - F: Field, + F: JoltField, { BytecodeReadRafRegisterEqEvals { read_write: EqPolynomial::::evals(register_read_write_point, None), @@ -431,7 +431,7 @@ pub fn read_raf_stage_values( inputs: BytecodeReadRafStageValueInputs<'_, F>, ) -> Vec<[F; NUM_BYTECODE_VAL_STAGES]> where - F: Field, + F: JoltField, { let register_eq = read_raf_register_eq_evals( inputs.register_read_write_point, @@ -459,7 +459,7 @@ pub fn read_raf_public_values( inputs: BytecodeReadRafEvaluationInputs<'_, F>, ) -> Result, JoltFormulaPointError> where - F: Field, + F: JoltField, { require_len(inputs.stage1_gammas, BYTECODE_STAGE_GAMMA_COUNTS[0])?; require_len(inputs.stage2_gammas, BYTECODE_STAGE_GAMMA_COUNTS[1])?; @@ -541,7 +541,7 @@ fn read_raf_row_values( stage5_gammas: &[F], ) -> [F; NUM_BYTECODE_VAL_STAGES] where - F: Field, + F: JoltField, { let decoded = JoltInstruction::try_from(*instruction) .unwrap_or(JoltInstruction::Noop(Noop(*instruction))); @@ -620,7 +620,7 @@ where } } -fn register_eq(register: Option, eq: &[F]) -> F { +fn register_eq(register: Option, eq: &[F]) -> F { register .and_then(|register| eq.get(register as usize)) .copied() @@ -643,7 +643,7 @@ pub fn read_raf_consistency_openings() -> [(JoltOpeningId, JoltOpeningId); 1] { pub(crate) fn stage1_claim() -> JoltExpr where - F: RingCore, + F: Ring, { let beta = challenge(BytecodeReadRafChallenge::Stage1Gamma); let mut claim = @@ -658,7 +658,7 @@ where pub(crate) fn stage2_claim() -> JoltExpr where - F: RingCore, + F: Ring, { let beta = challenge(BytecodeReadRafChallenge::Stage2Gamma); @@ -670,7 +670,7 @@ where pub(crate) fn stage3_claim() -> JoltExpr where - F: RingCore, + F: Ring, { let beta = challenge(BytecodeReadRafChallenge::Stage3Gamma); @@ -693,7 +693,7 @@ where pub(crate) fn stage4_claim() -> JoltExpr where - F: RingCore, + F: Ring, { let beta = challenge(BytecodeReadRafChallenge::Stage4Gamma); @@ -704,7 +704,7 @@ where pub(crate) fn stage5_claim() -> JoltExpr where - F: RingCore, + F: Ring, { let beta = challenge(BytecodeReadRafChallenge::Stage5Gamma); let mut claim = @@ -719,7 +719,7 @@ where fn bytecode_ra_product(dimensions: BytecodeReadRafDimensions) -> JoltExpr where - F: RingCore, + F: Ring, { let mut product = JoltExpr::one(); for i in 0..dimensions.num_committed_ra_polys() { @@ -795,7 +795,7 @@ pub fn bytecode_ra(index: usize) -> JoltOpeningId { #[cfg(test)] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_poly::EqPolynomial; use jolt_riscv::{JoltInstructionKind, NormalizedOperands}; diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/advice.rs b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/advice.rs index 38ada04e1b..11110b6f12 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/advice.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/advice.rs @@ -2,7 +2,7 @@ use std::{cmp::min, ops::Range}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::EqPolynomial; use super::super::super::{JoltAdviceKind, JoltOpeningId, JoltRelationId}; @@ -97,7 +97,7 @@ impl AdviceClaimReductionLayout { /// `FinalScale` value when the reduction completes in the cycle phase /// (i.e. no active address-phase rounds remain). - pub fn cycle_phase_final_output_scale( + pub fn cycle_phase_final_output_scale( &self, reference_opening_point: &[F], challenges: &[F], @@ -113,7 +113,7 @@ impl AdviceClaimReductionLayout { /// opening point, rather than re-deriving it from the sumcheck challenges. /// Lets the cycle-phase relation object's `resolve_public` recover the scale /// from the opening point it produced in `derive_opening_points`. - pub fn cycle_phase_scale_at_opening_point( + pub fn cycle_phase_scale_at_opening_point( &self, reference_opening_point: &[F], opening_point: &[F], @@ -126,7 +126,7 @@ impl AdviceClaimReductionLayout { } /// `FinalScale` value when the reduction completes in the address phase. - pub fn address_phase_final_output_scale( + pub fn address_phase_final_output_scale( &self, reference_opening_point: &[F], cycle_var_challenges: &[F], @@ -142,7 +142,7 @@ impl AdviceClaimReductionLayout { /// opening point, rather than re-deriving it from the cycle/sumcheck /// challenges. Lets the stage 7 relation object's `resolve_public` recover the /// scale from the opening point it produced in `derive_opening_points`. - pub fn address_phase_scale_at_opening_point( + pub fn address_phase_scale_at_opening_point( &self, reference_opening_point: &[F], opening_point: &[F], @@ -158,7 +158,7 @@ impl PrecommittedReductionLayout for AdviceClaimReductionLayout { } } -fn final_advice_eq_eval( +fn final_advice_eq_eval( reference_opening_point: &[F], opening_point: &[F], ) -> Result { diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/bytecode.rs b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/bytecode.rs index a438800181..9565b3c307 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/bytecode.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/bytecode.rs @@ -9,7 +9,7 @@ //! Mirrors `jolt-prover-legacy`'s `zkvm/claim_reductions/bytecode.rs` and the //! committed-bytecode geometry of `zkvm/bytecode/chunks.rs`. -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use jolt_lookup_tables::{LookupTableKind, XLEN}; use jolt_poly::EqPolynomial; use jolt_riscv::{CircuitFlags, InstructionFlags, NUM_CIRCUIT_FLAGS, NUM_INSTRUCTION_FLAGS}; @@ -220,7 +220,7 @@ impl BytecodeClaimReductionLayout { /// Split the full bytecode address point (the `BytecodeReadRafAddrClaim` /// opening point) into per-chunk eq weights over the dropped high bits and /// the chunk-local cycle point shared by all chunks. - pub fn split_address_point( + pub fn split_address_point( &self, r_bc_full: &[F], ) -> Result, JoltFormulaPointError> { @@ -248,7 +248,7 @@ impl BytecodeClaimReductionLayout { /// challenges. Lets the cycle-phase relation object's `resolve_public` /// recover the weights from the opening point it produced in /// `derive_opening_points`. - pub fn cycle_phase_final_output_weights_at_opening_point( + pub fn cycle_phase_final_output_weights_at_opening_point( &self, inputs: BytecodeOutputWeightInputs<'_, F>, opening_point: &[F], @@ -263,7 +263,7 @@ impl BytecodeClaimReductionLayout { /// `ChunkOutputWeight(i)` values when the reduction completes in the /// address phase. - pub fn address_phase_final_output_weights( + pub fn address_phase_final_output_weights( &self, inputs: BytecodeOutputWeightInputs<'_, F>, cycle_var_challenges: &[F], @@ -280,7 +280,7 @@ impl BytecodeClaimReductionLayout { /// cycle/sumcheck challenges. Lets the stage 7 relation object's /// `resolve_public` recover the weights from the opening point it produced in /// `derive_opening_points`. - pub fn address_phase_final_output_weights_at_opening_point( + pub fn address_phase_final_output_weights_at_opening_point( &self, inputs: BytecodeOutputWeightInputs<'_, F>, opening_point: &[F], @@ -293,7 +293,7 @@ impl BytecodeClaimReductionLayout { /// Evaluate the gamma-weighted lane selector against the chunk opening /// point: `(sum_lane lane_weights[lane] * eq(r_lane)[lane]) * eq(r_cycle, /// r_bc)`, with the lane/cycle split determined by the trace layout. - fn eq_combined( + fn eq_combined( &self, inputs: &BytecodeOutputWeightInputs<'_, F>, opening_point: &[F], @@ -339,7 +339,7 @@ impl BytecodeClaimReductionLayout { Ok(lane_weight_eval * eq_cycle) } - fn chunk_output_weights( + fn chunk_output_weights( &self, chunk_rbc_weights: &[F], scale: F, @@ -398,7 +398,7 @@ pub struct BytecodeLaneWeightInputs<'a, F> { /// Fold the five staged bytecode read-RAF combinations into one weight per /// committed lane, so `sum_lane weights[lane] * lane_value(row, lane)` equals /// `sum_stage eta^stage * stage_value(row)` for every bytecode row. -pub fn lane_weights( +pub fn lane_weights( inputs: BytecodeLaneWeightInputs<'_, F>, ) -> Result, JoltFormulaPointError> { require_len(inputs.stage1_gammas, BYTECODE_STAGE_GAMMA_COUNTS[0])?; @@ -498,7 +498,7 @@ pub fn lane_weights( pub(crate) fn final_output_expr(chunk_count: usize) -> JoltExpr where - F: RingCore, + F: Ring, { let mut output = JoltExpr::zero(); for chunk_idx in 0..chunk_count { @@ -561,7 +561,7 @@ mod tests { use super::super::super::bytecode::{read_raf_public_values, BytecodeReadRafEvaluationInputs}; use super::*; use crate::protocols::jolt::JoltPolynomialId; - use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; + use jolt_field::{Field, Fr, Ring}; use jolt_lookup_tables::InstructionLookupTable; use jolt_riscv::{ instructions::Noop, Flags, InterleavedBitsMarker, JoltInstruction, JoltInstructionKind, diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/hamming_weight.rs b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/hamming_weight.rs index 98fc75b7db..9d2219e722 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/hamming_weight.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/hamming_weight.rs @@ -1,4 +1,4 @@ -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use crate::opening; @@ -21,7 +21,7 @@ impl HammingWeightClaimReductionDimensions { } } - pub fn opening_point( + pub fn opening_point( self, challenges: &[F], r_cycle: &[F], @@ -81,7 +81,7 @@ pub fn claim_reduction_output_openings( pub(crate) fn hamming_weight_claim(polynomial: JoltRaPolynomial) -> JoltExpr where - F: RingCore, + F: Ring, { match polynomial { JoltRaPolynomial::Instruction(_) | JoltRaPolynomial::Bytecode(_) => JoltExpr::one(), @@ -116,7 +116,7 @@ pub(crate) fn reduced_claim(polynomial: JoltRaPolynomial) -> JoltOpeningId { mod tests { use super::super::super::dimensions::JoltFormulaDimensionsError; use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn layout( instruction: usize, diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/increments.rs b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/increments.rs index 5c2740106e..20893f3ad4 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/increments.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/increments.rs @@ -1,4 +1,4 @@ -use jolt_field::RingCore; +use jolt_field::Ring; use super::super::super::{JoltCommittedPolynomial, JoltExpr, JoltOpeningId, JoltRelationId}; use super::super::ram::{ram_inc, ram_inc_val_check}; @@ -10,7 +10,7 @@ use crate::opening; /// which must consume exactly the same set with the same γ order. pub(crate) fn inc_consumers_input(gamma: JoltExpr) -> JoltExpr where - F: RingCore, + F: Ring, { opening(ram_inc()) + gamma.clone() * opening(ram_inc_val_check()) diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/instruction.rs b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/instruction.rs index c710582d22..f9f098c562 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/instruction.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/instruction.rs @@ -1,4 +1,4 @@ -use jolt_field::RingCore; +use jolt_field::Ring; use crate::{challenge, opening}; @@ -15,7 +15,7 @@ pub(crate) fn weighted_claims( right_instruction_input: JoltOpeningId, ) -> JoltExpr where - F: RingCore, + F: Ring, { let gamma = challenge(InstructionClaimReductionChallenge::Gamma); diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/precommitted.rs b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/precommitted.rs index 2206e2048d..7335c67fd7 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/precommitted.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/precommitted.rs @@ -8,7 +8,7 @@ //! `zkvm/claim_reductions/precommitted.rs`, with the Dory globals //! (`main_k`, `main_t`, layout, configured column count) parameter-passed. -use jolt_field::Field; +use jolt_field::JoltField; use super::super::dimensions::{CommitmentMatrixShape, TracePolynomialOrder}; use super::super::error::JoltFormulaPointError; @@ -68,14 +68,14 @@ pub trait PrecommittedReductionLayout { self.precommitted().reduction_dimensions() } - fn cycle_phase_opening_point( + fn cycle_phase_opening_point( &self, challenges: &[F], ) -> Result, JoltFormulaPointError> { self.precommitted().cycle_phase_opening_point(challenges) } - fn cycle_phase_variable_challenges( + fn cycle_phase_variable_challenges( &self, challenges: &[F], ) -> Result, JoltFormulaPointError> { @@ -83,7 +83,7 @@ pub trait PrecommittedReductionLayout { .cycle_phase_variable_challenges(challenges) } - fn address_phase_opening_point( + fn address_phase_opening_point( &self, cycle_var_challenges: &[F], challenges: &[F], @@ -320,11 +320,11 @@ impl PrecommittedClaimReduction { /// rounds. This is the cycle-only counterpart of /// [`precommitted_skip_round_scale`], used when the reduction finishes in /// the cycle phase. - pub fn cycle_phase_skip_scale(&self) -> F { + pub fn cycle_phase_skip_scale(&self) -> F { skip_round_scale(self.cycle_phase_total_rounds - self.cycle_phase_rounds.len()) } - fn cycle_challenge_for_round( + fn cycle_challenge_for_round( &self, cycle_var_challenges: &[F], round: usize, @@ -343,7 +343,7 @@ impl PrecommittedClaimReduction { /// Cycle-phase challenges this polynomial actively binds, in ascending /// round order. The verifier carries these into the address phase. - pub fn cycle_phase_variable_challenges( + pub fn cycle_phase_variable_challenges( &self, challenges: &[F], ) -> Result, JoltFormulaPointError> { @@ -361,7 +361,7 @@ impl PrecommittedClaimReduction { } /// Big-endian opening point cached at the cycle-phase handoff. - pub fn cycle_phase_opening_point( + pub fn cycle_phase_opening_point( &self, challenges: &[F], ) -> Result, JoltFormulaPointError> { @@ -373,7 +373,7 @@ impl PrecommittedClaimReduction { /// Big-endian permutation-ordered point for a reduction that completes in /// the cycle phase. Errors if the polynomial still has active /// address-phase rounds. - pub fn cycle_phase_permuted_opening_point( + pub fn cycle_phase_permuted_opening_point( &self, challenges: &[F], ) -> Result, JoltFormulaPointError> { @@ -410,7 +410,7 @@ impl PrecommittedClaimReduction { /// `*_at_opening_point` helpers. /// /// [`cycle_phase_permuted_opening_point`]: Self::cycle_phase_permuted_opening_point - pub fn cycle_phase_permuted_from_opening_point( + pub fn cycle_phase_permuted_from_opening_point( &self, opening_point: &[F], ) -> Result, JoltFormulaPointError> { @@ -440,7 +440,7 @@ impl PrecommittedClaimReduction { /// Big-endian final opening point in Dory opening-round order, assembled /// from the recorded cycle-phase challenges and the address-phase /// sumcheck challenges. - pub fn address_phase_opening_point( + pub fn address_phase_opening_point( &self, cycle_var_challenges: &[F], challenges: &[F], @@ -474,13 +474,13 @@ impl PrecommittedClaimReduction { /// The `(1/2)^gap` factor contributed by all rounds (cycle and address) this /// polynomial skips across both phases. Used for the final output claim when /// the reduction completes in the address phase. -pub fn precommitted_skip_round_scale(precommitted: &PrecommittedClaimReduction) -> F { +pub fn precommitted_skip_round_scale(precommitted: &PrecommittedClaimReduction) -> F { let gap = (precommitted.cycle_phase_total_rounds - precommitted.cycle_phase_rounds.len()) + (precommitted.address_phase_total_rounds - precommitted.address_phase_rounds.len()); skip_round_scale(gap) } -fn skip_round_scale(gap: usize) -> F { +fn skip_round_scale(gap: usize) -> F { if gap == 0 { return F::one(); } @@ -494,7 +494,7 @@ mod tests { use super::*; use crate::protocols::jolt::geometry::dimensions::TracePolynomialOrder; - use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; + use jolt_field::{Field, Fr, Ring}; #[test] fn cycle_skip_scale_counts_inactive_cycle_rounds() { diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/program_image.rs b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/program_image.rs index 7460fd253a..ff1434d70c 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/program_image.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/claim_reductions/program_image.rs @@ -7,7 +7,7 @@ //! `ProgramImageInit` commitment over the shared precommitted schedule. //! Mirrors `jolt-prover-legacy`'s `zkvm/claim_reductions/program_image.rs`. -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use crate::{derived, opening}; @@ -84,7 +84,7 @@ impl ProgramImageClaimReductionLayout { /// `FinalScale` value when the reduction completes in the cycle phase /// (i.e. no active address-phase rounds remain). `r_addr_rw` is the RAM /// address component of the `RamVal` opening from RAM read-write checking. - pub fn cycle_phase_final_output_scale( + pub fn cycle_phase_final_output_scale( &self, r_addr_rw: &[F], challenges: &[F], @@ -101,7 +101,7 @@ impl ProgramImageClaimReductionLayout { /// opening point, rather than re-deriving it from the sumcheck challenges. /// Lets the cycle-phase relation object's `resolve_public` recover the scale /// from the opening point it produced in `derive_opening_points`. - pub fn cycle_phase_scale_at_opening_point( + pub fn cycle_phase_scale_at_opening_point( &self, r_addr_rw: &[F], opening_point: &[F], @@ -115,7 +115,7 @@ impl ProgramImageClaimReductionLayout { } /// `FinalScale` value when the reduction completes in the address phase. - pub fn address_phase_final_output_scale( + pub fn address_phase_final_output_scale( &self, r_addr_rw: &[F], cycle_var_challenges: &[F], @@ -131,7 +131,7 @@ impl ProgramImageClaimReductionLayout { /// opening point, rather than re-deriving it from the cycle/sumcheck /// challenges. Lets the stage 7 relation object's `resolve_public` recover the /// scale from the opening point it produced in `derive_opening_points`. - pub fn address_phase_scale_at_opening_point( + pub fn address_phase_scale_at_opening_point( &self, r_addr_rw: &[F], opening_point: &[F], @@ -150,7 +150,7 @@ impl PrecommittedReductionLayout for ProgramImageClaimReductionLayout { pub(crate) fn final_output_expr() -> JoltExpr where - F: RingCore, + F: Ring, { derived(JoltDerivedId::from( ProgramImageClaimReductionPublic::FinalScale, @@ -201,7 +201,7 @@ pub fn final_program_image_opening() -> JoltOpeningId { /// `carry_use` (y-bit 0 produced output 1, consuming the incoming carry). /// Window bits at and above `m` are fixed to zero, so `r_y = 0` there. The /// final carry-out is dropped, i.e. addresses wrap mod `2^ell`. -fn eval_shifted_eq_poly_at_opening_point( +fn eval_shifted_eq_poly_at_opening_point( r_addr_be: &[F], start_index: usize, opening_point_be: &[F], @@ -241,7 +241,7 @@ mod tests { #![expect(clippy::panic, reason = "tests fail loudly on unexpected errors")] use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_poly::EqPolynomial; fn fr(value: u64) -> Fr { diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/committed_openings.rs b/crates/jolt-claims/src/protocols/jolt/geometry/committed_openings.rs index 41e5c8ac64..fca183dc88 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/committed_openings.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/committed_openings.rs @@ -1,6 +1,6 @@ //! Jolt committed-polynomial proof and final-opening orders. -use jolt_field::Field; +use jolt_field::JoltField; use super::super::{JoltCommittedPolynomial, JoltOpeningId, JoltRelationId}; use super::dimensions::TracePolynomialOrder; @@ -99,7 +99,7 @@ fn final_opening_relation(polynomial: JoltCommittedPolynomial) -> JoltRelationId /// Lagrange factor for embedding a smaller polynomial's opening into the /// top-left block of the unified final opening point: `1` on variables the /// embedded point binds, `1 - r` on the rest. -pub fn commitment_embedding_scale( +pub fn commitment_embedding_scale( opening_point: &[F], embedded_opening_point: &[F], ) -> F { @@ -123,7 +123,7 @@ pub fn commitment_embedding_scale( /// Inputs to [`final_opening_point`], gathered from earlier verification /// stages. -pub struct FinalOpeningPointInputs<'a, F: Field> { +pub struct FinalOpeningPointInputs<'a, F: JoltField> { pub log_t: usize, pub log_k_chunk: usize, pub trace_order: TracePolynomialOrder, @@ -144,7 +144,7 @@ pub struct FinalOpeningPointInputs<'a, F: Field> { /// agree). Otherwise the point is assembled from the stage 6 cycle challenges /// and the stage 7 address challenges in the order the active trace layout /// expects. -pub fn final_opening_point( +pub fn final_opening_point( inputs: FinalOpeningPointInputs<'_, F>, ) -> Result, JoltFormulaPointError> { let native_main_vars = inputs.log_t + inputs.log_k_chunk; @@ -202,7 +202,7 @@ mod tests { #![expect(clippy::panic, reason = "tests fail loudly on unexpected errors")] use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn layout() -> JoltRaPolynomialLayout { JoltRaPolynomialLayout::new(2, 1, 2).unwrap_or_else(|error| { diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs b/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs index 2989c8c575..f905411359 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; pub use super::error::{JoltFormulaDimensionsError, JoltFormulaPointError}; @@ -71,7 +71,7 @@ impl TraceDimensions { self.log_t } - pub fn cycle_opening_point( + pub fn cycle_opening_point( self, challenges: &[F], ) -> Result, JoltFormulaPointError> { @@ -141,7 +141,7 @@ impl ReadWriteDimensions { self.log_t + self.log_k - self.phase1_num_rounds } - pub fn read_write_opening_point( + pub fn read_write_opening_point( self, challenges: &[F], ) -> Result, JoltFormulaPointError> { @@ -179,7 +179,7 @@ impl ReadWriteDimensions { }) } - pub fn address_opening_point( + pub fn address_opening_point( self, challenges: &[F], ) -> Result, JoltFormulaPointError> { @@ -215,7 +215,7 @@ impl ReadWriteDimensions { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ReadWriteOpeningPoint { +pub struct ReadWriteOpeningPoint { pub r_address: Vec, pub r_cycle: Vec, pub opening_point: Vec, @@ -327,7 +327,7 @@ impl JoltOneHotConfig { self.lookups_ra_virtual_log_k_chunk as usize } - pub fn committed_address_chunks(self, r_address: &[F]) -> Vec> { + pub fn committed_address_chunks(self, r_address: &[F]) -> Vec> { committed_address_chunks(r_address, self.committed_chunk_bits()) } @@ -349,7 +349,7 @@ impl JoltOneHotConfig { } } -pub fn committed_address_chunks(r_address: &[F], chunk_bits: usize) -> Vec> { +pub fn committed_address_chunks(r_address: &[F], chunk_bits: usize) -> Vec> { if chunk_bits == 0 { return Vec::new(); } @@ -490,7 +490,7 @@ mod tests { PrecommittedClaimReduction, PrecommittedReductionLayout, }; use super::*; - use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; + use jolt_field::{Field, Fr, Ring}; use jolt_poly::EqPolynomial; fn dimensions() -> JoltOneHotDimensions { diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/instruction.rs b/crates/jolt-claims/src/protocols/jolt/geometry/instruction.rs index 0d36b2ff72..2fb0e64770 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/instruction.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/instruction.rs @@ -1,6 +1,6 @@ use std::num::NonZeroUsize; -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use jolt_lookup_tables::{LookupTableKind, XLEN}; use jolt_riscv::InstructionFlags; @@ -51,7 +51,7 @@ pub const CANONICAL_INSTRUCTION_ADDRESS: bool = cfg!(feature = "akita"); /// product. Taking the trailing half instead would both miss the aliases and /// reject honest cycles — `SUB(2^64-1, 0)` has index `0x1_FFFF_FFFF_FFFF_FFFF`, /// whose low limb is all ones. -pub fn upper_half_all_ones(r_address: &[F]) -> F { +pub fn upper_half_all_ones(r_address: &[F]) -> F { r_address[..r_address.len() / 2] .iter() .copied() @@ -164,7 +164,7 @@ impl InstructionReadRafDimensions { self.instruction_address_bits + self.log_t } - pub fn opening_point( + pub fn opening_point( self, challenges: &[F], ) -> Result, JoltFormulaPointError> { @@ -190,7 +190,7 @@ impl InstructionReadRafDimensions { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct InstructionReadRafOpeningPoint { +pub struct InstructionReadRafOpeningPoint { pub r_address: Vec, pub r_cycle: Vec, pub opening_point: Vec, @@ -374,7 +374,7 @@ pub(crate) fn weighted_instruction_ra_sum( gamma: JoltExpr, ) -> JoltExpr where - F: RingCore, + F: Ring, { let mut sum = JoltExpr::zero(); for i in 0..dimensions.num_virtual_ra_polys() { @@ -385,7 +385,7 @@ where pub(crate) fn instruction_ra_product(dimensions: InstructionReadRafDimensions) -> JoltExpr where - F: RingCore, + F: Ring, { let mut product = JoltExpr::one(); for i in 0..dimensions.num_virtual_ra_polys() { @@ -399,7 +399,7 @@ pub(crate) fn committed_instruction_ra_product( virtual_index: usize, ) -> JoltExpr where - F: RingCore, + F: Ring, { let mut product = JoltExpr::one(); let start = virtual_index * dimensions.num_committed_per_virtual(); @@ -497,7 +497,7 @@ pub fn imm() -> JoltOpeningId { #[expect(clippy::panic)] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; #[test] fn read_raf_rejects_empty_dimensions() { diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/ram.rs b/crates/jolt-claims/src/protocols/jolt/geometry/ram.rs index 4cf779ad0f..3c721811d4 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/ram.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/ram.rs @@ -1,4 +1,4 @@ -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use crate::opening; @@ -144,13 +144,13 @@ pub fn val_check_advice_opening(kind: JoltAdviceKind) -> JoltOpeningId { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RamRaClaimReductionPublicValues { +pub struct RamRaClaimReductionPublicValues { pub eq_cycle_raf: F, pub eq_cycle_read_write: F, pub eq_cycle_val_check: F, } -impl RamRaClaimReductionPublicValues { +impl RamRaClaimReductionPublicValues { pub fn value(&self, id: RamRaClaimReductionPublic) -> F { match id { RamRaClaimReductionPublic::EqCycleRaf => self.eq_cycle_raf, @@ -162,7 +162,7 @@ impl RamRaClaimReductionPublicValues { pub(crate) fn committed_ram_ra_product(dimensions: RamRaVirtualizationDimensions) -> JoltExpr where - F: RingCore, + F: Ring, { let mut product = JoltExpr::one(); for index in 0..dimensions.num_committed_ra_polys() { diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/spartan.rs b/crates/jolt-claims/src/protocols/jolt/geometry/spartan.rs index 9b8abafea6..a57683d8ac 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/spartan.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/spartan.rs @@ -1,6 +1,6 @@ use std::fmt; -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use jolt_poly::{ lagrange::{centered_lagrange_evals, centered_lagrange_kernel, CenteredIntegerDomainError}, EqPolynomial, @@ -179,7 +179,7 @@ impl SpartanOuterRemainderPlan { .collect() } - pub fn row_weights( + pub fn row_weights( &self, r0: F, r_stream: F, @@ -202,7 +202,7 @@ impl SpartanOuterRemainderPlan { Ok(weights) } - pub fn tau_kernel( + pub fn tau_kernel( &self, tau: &[F], r0: F, @@ -224,7 +224,7 @@ impl SpartanOuterRemainderPlan { Ok(tau_high_bound_r0 * EqPolynomial::::mle(&tau[..tau.len() - 1], &reversed_challenges)) } - pub fn public_claims( + pub fn public_claims( &self, tau_kernel: F, linear_forms: &SpartanOuterLinearForms, @@ -292,7 +292,7 @@ fn spartan_outer_r1cs_input_index( pub(crate) fn product_weight(index: usize) -> JoltExpr where - F: RingCore, + F: Ring, { derived(JoltDerivedId::from( SpartanProductVirtualizationPublic::LagrangeWeight(index), @@ -301,7 +301,7 @@ where pub(crate) fn product_uniskip_weight(index: usize) -> JoltExpr where - F: RingCore, + F: Ring, { derived(JoltDerivedId::from( SpartanProductVirtualizationPublic::UniskipLagrangeWeight(index), @@ -310,7 +310,7 @@ where pub(crate) fn product_tau_kernel() -> JoltExpr where - F: RingCore, + F: Ring, { derived(JoltDerivedId::from( SpartanProductVirtualizationPublic::TauKernel, @@ -444,7 +444,7 @@ pub fn is_noop_shift() -> JoltOpeningId { #[expect(clippy::panic, clippy::unwrap_used)] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn outer_dimensions() -> SpartanOuterDimensions { match SpartanOuterDimensions::new( diff --git a/crates/jolt-claims/src/protocols/jolt/lattice/geometry.rs b/crates/jolt-claims/src/protocols/jolt/lattice/geometry.rs index 84bf8ca7ba..019ee29986 100644 --- a/crates/jolt-claims/src/protocols/jolt/lattice/geometry.rs +++ b/crates/jolt-claims/src/protocols/jolt/lattice/geometry.rs @@ -3,7 +3,7 @@ //! relations' deriveds. Vocabulary is inherited — see the //! [module doc](super). -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use jolt_poly::math::Math; use jolt_poly::{eq_index_msb, IdentityPolynomial, MultilinearEvaluation}; use thiserror::Error; @@ -80,7 +80,7 @@ impl UnsignedIncChunking { /// The place value `2^(chunk_width * index)` weighting chunk `index` in /// the little-endian reconstruction of the low 64 bits. - pub fn place_value(self, index: usize) -> F { + pub fn place_value(self, index: usize) -> F { F::pow2(self.chunk_width * index) } } @@ -120,7 +120,7 @@ pub fn word_byte_num_vars(log_words: usize) -> usize { /// `Π_position ((256^(2^(bits − 1 − position)) − 1) · point[position] + 1)`. /// The radix half of the byte decode; the value half is `jolt-poly`'s /// `IdentityPolynomial`. -pub fn place_value_weight(point: &[F]) -> F { +pub fn place_value_weight(point: &[F]) -> F { let bits = point.len(); point .iter() @@ -137,7 +137,7 @@ pub fn place_value_weight(point: &[F]) -> F { /// decode(byte, place) · Bytes(byte ‖ place ‖ instance)`. This is the /// semantic definition of the `ByteDecode` deriveds of the reconstruction /// relations. -pub fn byte_decode_weight(byte_point: &[F], place_point: &[F]) -> F { +pub fn byte_decode_weight(byte_point: &[F], place_point: &[F]) -> F { IdentityPolynomial::new(byte_point.len()).evaluate(byte_point) * place_value_weight(place_point) } @@ -147,7 +147,7 @@ pub fn byte_decode_weight(byte_point: &[F], place_point: &[F]) -> F { /// `LookupSelectorWeight` deriveds (a one-hot selector's lane-eq weights, one /// lane per register / table index); the `LaneWeight(lane)` deriveds of the /// direct 0/1 flag lanes are plain `eq_index_msb(lane_point, lane)`. -pub fn selector_block_weight( +pub fn selector_block_weight( lane_point: &[F], block_start: usize, value_point: &[F], @@ -165,7 +165,7 @@ pub fn selector_block_weight( #[expect(clippy::unwrap_used)] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_poly::{boolean_point_msb, EqPolynomial}; #[test] diff --git a/crates/jolt-claims/src/protocols/jolt/lattice/relations/advice_reconstruction.rs b/crates/jolt-claims/src/protocols/jolt/lattice/relations/advice_reconstruction.rs index 199ee66b42..bf8fa7e125 100644 --- a/crates/jolt-claims/src/protocols/jolt/lattice/relations/advice_reconstruction.rs +++ b/crates/jolt-claims/src/protocols/jolt/lattice/relations/advice_reconstruction.rs @@ -34,7 +34,7 @@ //! point is fixed by the incoming claim, mirroring how the inc chunk //! reconstruction fixes its cycle point). -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::claim_reductions::advice::final_advice_opening; @@ -97,7 +97,7 @@ pub struct UntrustedAdviceReconstructionChallenges { pub gamma: F, } -impl SumcheckChallenges for UntrustedAdviceReconstructionChallenges { +impl SumcheckChallenges for UntrustedAdviceReconstructionChallenges { fn from_transcript_values>( _values: I, ) -> Result { @@ -143,12 +143,12 @@ impl SymbolicSumcheck for UntrustedAdviceReconstruction { /// The booleanity leg sums to zero, the hamming leg to one, and the /// decode leg to the incoming word claim. - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { let gamma = challenge(UntrustedAdviceReconstructionChallenge::Gamma); gamma.clone() + gamma.pow(2) * opening(final_advice_opening(JoltAdviceKind::Untrusted)) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let gamma = challenge(UntrustedAdviceReconstructionChallenge::Gamma); let bytes = opening(untrusted_advice_bytes_opening()); @@ -220,11 +220,11 @@ impl SymbolicSumcheck for TrustedAdviceReconstruction { 2 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(final_advice_opening(JoltAdviceKind::Trusted)) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { derived(TrustedAdviceReconstructionPublic::ByteDecode) * opening(trusted_advice_bytes_opening()) } @@ -241,7 +241,7 @@ pub fn trusted_advice_bytes_opening() -> JoltOpeningId { mod tests { use super::*; use crate::protocols::jolt::JoltDerivedId; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; #[test] fn untrusted_reconstruction_evaluates_like_core_formula() { diff --git a/crates/jolt-claims/src/protocols/jolt/lattice/relations/booleanity.rs b/crates/jolt-claims/src/protocols/jolt/lattice/relations/booleanity.rs index 9994d0495a..2a74a0c171 100644 --- a/crates/jolt-claims/src/protocols/jolt/lattice/relations/booleanity.rs +++ b/crates/jolt-claims/src/protocols/jolt/lattice/relations/booleanity.rs @@ -4,7 +4,7 @@ //! sharing a relation id across mode variants: the full/committed bytecode //! read-raf pair. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::opening; @@ -99,11 +99,11 @@ impl SymbolicSumcheck for LatticeBooleanity { 3 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { JoltExpr::zero() } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { booleanity_output(lattice_booleanity_output_openings(self.shape)) } } @@ -144,11 +144,11 @@ impl SymbolicSumcheck for LatticeBooleanityCyclePhase { 3 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(booleanity_address_phase_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { booleanity_output(lattice_booleanity_output_openings(self.shape)) } } @@ -188,7 +188,7 @@ mod tests { use crate::protocols::jolt::{ BooleanityChallenge, BooleanityPublic, JoltChallengeId, JoltDerivedId, }; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn dimensions() -> LatticeBooleanityDimensions { let layout = JoltRaPolynomialLayout::new(1, 0, 0).unwrap(); diff --git a/crates/jolt-claims/src/protocols/jolt/lattice/relations/bytecode_reconstruction.rs b/crates/jolt-claims/src/protocols/jolt/lattice/relations/bytecode_reconstruction.rs index 58523f42d5..175a10fad4 100644 --- a/crates/jolt-claims/src/protocols/jolt/lattice/relations/bytecode_reconstruction.rs +++ b/crates/jolt-claims/src/protocols/jolt/lattice/relations/bytecode_reconstruction.rs @@ -22,7 +22,7 @@ //! lattice booleanity's msb). Every leg is at most a product of two //! multilinears per bound variable, hence degree 2. -use jolt_field::RingCore; +use jolt_field::Ring; use jolt_poly::math::Math; use jolt_riscv::{NUM_CIRCUIT_FLAGS, NUM_INSTRUCTION_FLAGS}; use serde::{Deserialize, Serialize}; @@ -144,7 +144,7 @@ impl BytecodeChunkReconstructionOutputClaims { } } -impl OutputClaims for BytecodeChunkReconstructionOutputClaims { +impl OutputClaims for BytecodeChunkReconstructionOutputClaims { fn canonical_order(&self) -> Vec { self.leaves().map(|(id, _)| id).collect() } @@ -247,14 +247,14 @@ impl SymbolicSumcheck for BytecodeChunkReconstruction { 2 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { let gamma = challenge(BytecodeChunkReconstructionChallenge::Gamma); (0..self.shape.chunks).fold(JoltExpr::zero(), |acc, chunk| { acc + gamma.clone().pow(chunk) * opening(final_bytecode_chunk_opening(chunk)) }) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let gamma = challenge(BytecodeChunkReconstructionChallenge::Gamma); let layout = BYTECODE_LANE_LAYOUT; let mut output = JoltExpr::::zero(); @@ -361,7 +361,7 @@ pub fn bytecode_imm_bytes_opening(chunk: usize) -> JoltOpeningId { mod tests { use super::*; use crate::protocols::jolt::JoltDerivedId; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn dimensions() -> BytecodeReconstructionDimensions { BytecodeReconstructionDimensions { diff --git a/crates/jolt-claims/src/protocols/jolt/lattice/relations/hamming_weight.rs b/crates/jolt-claims/src/protocols/jolt/lattice/relations/hamming_weight.rs index ac5babd752..8c184565e7 100644 --- a/crates/jolt-claims/src/protocols/jolt/lattice/relations/hamming_weight.rs +++ b/crates/jolt-claims/src/protocols/jolt/lattice/relations/hamming_weight.rs @@ -1,7 +1,7 @@ //! Lattice-mode Hamming-weight claim reduction, extended with the fused //! increment's one-hot decomposition. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::claim_reductions::hamming_weight::{ @@ -134,7 +134,7 @@ impl SymbolicSumcheck for LatticeHammingWeightClaimReduction { 2 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { let gamma = challenge(HammingWeightClaimReductionChallenge::Gamma); let mut input = JoltExpr::zero(); for (i, polynomial) in self.shape.layout.polynomials().enumerate() { @@ -160,7 +160,7 @@ impl SymbolicSumcheck for LatticeHammingWeightClaimReduction { * (opening(fused_inc_read_raf_opening()) + constant(F::pow2(UNSIGNED_INC_BITS))) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let gamma = challenge(HammingWeightClaimReductionChallenge::Gamma); let eq_booleanity = derived(HammingWeightClaimReductionPublic::EqBooleanity); let identity = derived(HammingWeightClaimReductionPublic::IdentityAtAddress); @@ -215,7 +215,7 @@ mod tests { HammingWeightClaimReductionChallenge, JoltChallengeId, JoltCommittedPolynomial, JoltDerivedId, }; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; #[test] fn fused_increment_terms_extend_the_ra_reduction() { diff --git a/crates/jolt-claims/src/protocols/jolt/lattice/relations/program_image_reconstruction.rs b/crates/jolt-claims/src/protocols/jolt/lattice/relations/program_image_reconstruction.rs index 82a3c27e37..d342b80edc 100644 --- a/crates/jolt-claims/src/protocols/jolt/lattice/relations/program_image_reconstruction.rs +++ b/crates/jolt-claims/src/protocols/jolt/lattice/relations/program_image_reconstruction.rs @@ -9,7 +9,7 @@ //! offline, so only the decode leg is spent. Binds the `(byte ‖ place)` //! variables; the word point is fixed by the incoming claim. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::claim_reductions::program_image::final_program_image_opening; @@ -70,11 +70,11 @@ impl SymbolicSumcheck for ProgramImageReconstruction { 2 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(final_program_image_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { derived(ProgramImageReconstructionPublic::ByteDecode) * opening(program_image_bytes_opening()) } @@ -91,7 +91,7 @@ pub fn program_image_bytes_opening() -> JoltOpeningId { mod tests { use super::*; use crate::protocols::jolt::JoltDerivedId; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; #[test] fn program_image_reconstruction_evaluates_like_core_formula() { diff --git a/crates/jolt-claims/src/protocols/jolt/lattice/relations/read_raf.rs b/crates/jolt-claims/src/protocols/jolt/lattice/relations/read_raf.rs index 7b00b3572d..4e0d9d1089 100644 --- a/crates/jolt-claims/src/protocols/jolt/lattice/relations/read_raf.rs +++ b/crates/jolt-claims/src/protocols/jolt/lattice/relations/read_raf.rs @@ -23,7 +23,7 @@ //! disjointness check on the public bytecode re-verifies this per row at //! preprocessing. -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::bytecode::{ @@ -59,7 +59,7 @@ pub struct LatticeReadRafAddressPhaseInputClaims { pub inc: IncClaimReductionInputClaims, } -impl InputClaims for LatticeReadRafAddressPhaseInputClaims { +impl InputClaims for LatticeReadRafAddressPhaseInputClaims { fn canonical_order(&self) -> Vec { let mut order = self.base.canonical_order(); order.extend(InputClaims::::canonical_order(&self.inc)); @@ -74,7 +74,7 @@ impl InputClaims for LatticeReadRafAddressPhaseInputClaims { } /// The four consumed inc claims in stage order (`γ^5..8`). -fn fused_inc_stage_claims() -> Vec> { +fn fused_inc_stage_claims() -> Vec> { vec![ opening(ram_inc()), opening(ram_inc_val_check()), @@ -116,11 +116,11 @@ impl SymbolicSumcheck for LatticeReadRafAddressPhase { self.shape.num_committed_ra_polys() + 1 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { read_raf_address_input_fold(fused_inc_stage_claims()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { opening(bytecode_read_raf_address_phase_opening()) } } @@ -173,11 +173,11 @@ impl SymbolicSumcheck for LatticeReadRafCyclePhase { self.shape.num_committed_ra_polys() + 2 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(bytecode_read_raf_address_phase_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { read_raf_cycle_output_lattice(self.shape) } } @@ -215,11 +215,11 @@ impl SymbolicSumcheck for LatticeReadRafCyclePhaseCommitted { self.shape.num_committed_ra_polys() + 2 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(bytecode_read_raf_address_phase_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { read_raf_cycle_output_committed_lattice(self.shape) } } @@ -234,7 +234,7 @@ mod tests { use crate::protocols::jolt::geometry::spartan::pc_shift; use crate::protocols::jolt::BytecodeReadRafPublic; use crate::SymbolicSumcheck; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn dimensions() -> BytecodeReadRafDimensions { BytecodeReadRafDimensions::new(5, 10, 2) diff --git a/crates/jolt-claims/src/protocols/jolt/lattice/strategy.rs b/crates/jolt-claims/src/protocols/jolt/lattice/strategy.rs index 9ac778c941..a83b8dea43 100644 --- a/crates/jolt-claims/src/protocols/jolt/lattice/strategy.rs +++ b/crates/jolt-claims/src/protocols/jolt/lattice/strategy.rs @@ -5,7 +5,7 @@ //! common `(cycle || address)` point. use blake2::{digest::consts::U32, Blake2b, Digest}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::OpeningsError; use super::super::JoltCommittedPolynomial; @@ -101,7 +101,7 @@ impl OneHotTraceLayout { /// Maps a column's leaf-claim point from `(address || cycle)` to the /// row-major committed order `(cycle || address)`. - pub fn column_point( + pub fn column_point( &self, polynomial: JoltCommittedPolynomial, chunk_width: usize, @@ -144,7 +144,7 @@ fn append_usize(hasher: &mut Blake2b, value: usize) { mod tests { use super::*; use crate::protocols::jolt::geometry::ra::JoltRaPolynomialLayout; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn shape(log_t: usize) -> OneHotTraceShape { OneHotTraceShape { diff --git a/crates/jolt-claims/src/protocols/jolt/relations/booleanity/address_phase.rs b/crates/jolt-claims/src/protocols/jolt/relations/booleanity/address_phase.rs index 0731951b04..fef777aedb 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/booleanity/address_phase.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/booleanity/address_phase.rs @@ -2,7 +2,7 @@ use core::marker::PhantomData; -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use serde::{Deserialize, Serialize}; use crate::opening; @@ -39,7 +39,7 @@ impl Default for BooleanityAddressPhaseInputClaims { } } -impl InputClaims for BooleanityAddressPhaseInputClaims { +impl InputClaims for BooleanityAddressPhaseInputClaims { fn canonical_order(&self) -> Vec { Vec::new() } @@ -71,7 +71,7 @@ pub struct BooleanityAddressPhaseChallenges { pub gamma: F, } -impl SumcheckChallenges for BooleanityAddressPhaseChallenges { +impl SumcheckChallenges for BooleanityAddressPhaseChallenges { fn from_transcript_values>( _values: I, ) -> Result { @@ -116,11 +116,11 @@ impl SymbolicSumcheck for BooleanityAddressPhase { 3 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { JoltExpr::zero() } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { opening(booleanity_address_phase_opening()) } } diff --git a/crates/jolt-claims/src/protocols/jolt/relations/booleanity/cycle_phase.rs b/crates/jolt-claims/src/protocols/jolt/relations/booleanity/cycle_phase.rs index 3307d89620..40d0fad85f 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/booleanity/cycle_phase.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/booleanity/cycle_phase.rs @@ -1,6 +1,6 @@ //! The cycle-phase split of the booleanity symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use super::monolith::{BooleanityInputClaims, BooleanityOutputClaims}; use crate::opening; @@ -53,11 +53,11 @@ impl SymbolicSumcheck for BooleanityCyclePhase { 3 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(booleanity_address_phase_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { booleanity_cycle_output(self.shape) } } diff --git a/crates/jolt-claims/src/protocols/jolt/relations/booleanity/monolith.rs b/crates/jolt-claims/src/protocols/jolt/relations/booleanity/monolith.rs index 7590a60602..b977a9e893 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/booleanity/monolith.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/booleanity/monolith.rs @@ -1,6 +1,6 @@ //! The full (monolithic) booleanity symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::booleanity::{booleanity_cycle_output, BooleanityDimensions}; @@ -76,11 +76,11 @@ impl SymbolicSumcheck for Booleanity { 3 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { JoltExpr::zero() } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { booleanity_cycle_output(self.shape) } } @@ -94,7 +94,7 @@ mod tests { BooleanityChallenge, BooleanityPublic, JoltChallengeId, JoltCommittedPolynomial, JoltDerivedId, JoltOpeningId, }; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn dimensions(instruction: usize, bytecode: usize, ram: usize) -> BooleanityDimensions { let layout = JoltRaPolynomialLayout::new(instruction, bytecode, ram).unwrap(); diff --git a/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf.rs b/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf.rs index 64f7d16643..ad5611fae7 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf.rs @@ -1,6 +1,6 @@ //! The full bytecode read-RAF symbolic sumcheck (monolith). -use jolt_field::RingCore; +use jolt_field::Ring; use crate::protocols::jolt::geometry::bytecode::{ read_raf_address_input_fold, read_raf_cycle_output, BytecodeReadRafDimensions, @@ -63,11 +63,11 @@ impl SymbolicSumcheck for ReadRaf { self.shape.num_committed_ra_polys() + 1 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { read_raf_address_input_fold(Vec::new()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { read_raf_cycle_output(self.shape, NUM_BYTECODE_VAL_STAGES) } } @@ -87,7 +87,7 @@ mod tests { use crate::protocols::jolt::geometry::spartan::pc_shift; use crate::protocols::jolt::geometry::spartan::unexpanded_pc_shift; use crate::protocols::jolt::{BytecodeReadRafPublic, JoltPolynomialId, JoltVirtualPolynomial}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_lookup_tables::{LookupTableKind, XLEN}; use jolt_riscv::{CircuitFlags, InstructionFlags, CIRCUIT_FLAGS}; diff --git a/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_address_phase.rs b/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_address_phase.rs index 5b46984c6d..0d0a287ec0 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_address_phase.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_address_phase.rs @@ -1,6 +1,6 @@ //! The address phase of the bytecode read-RAF symbolic sumcheck. -use jolt_field::RingCore; +use jolt_field::Ring; use jolt_riscv::{CircuitFlags, InstructionFlags}; use serde::{Deserialize, Serialize}; @@ -134,7 +134,7 @@ pub struct BytecodeReadRafAddressPhaseChallenges { pub stage5_gamma: F, } -impl BytecodeReadRafAddressPhaseChallenges { +impl BytecodeReadRafAddressPhaseChallenges { /// Expand the five drawn per-stage scalars into the gamma-power vectors the /// bytecode folds consume (`[1, γ, γ², …]` — the recurrence the prover's /// `challenge_scalar_powers` applies to its single squeezed scalar), sized @@ -190,11 +190,11 @@ impl SymbolicSumcheck for ReadRafAddressPhase { self.shape.num_committed_ra_polys() + 1 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { read_raf_address_input_fold(Vec::new()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { opening(bytecode_read_raf_address_phase_opening()) } } diff --git a/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_cycle_phase.rs b/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_cycle_phase.rs index 023d192f22..7afa11fd7e 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_cycle_phase.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_cycle_phase.rs @@ -1,6 +1,6 @@ //! The cycle phase of the bytecode read-RAF symbolic sumcheck. -use jolt_field::RingCore; +use jolt_field::Ring; use super::{BytecodeReadRafCycleShape, BytecodeReadRafInputClaims, BytecodeReadRafOutputClaims}; use crate::protocols::jolt::geometry::bytecode::{ @@ -53,11 +53,11 @@ impl SymbolicSumcheck for ReadRafCyclePhase { self.shape.0.num_committed_ra_polys() + 1 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(bytecode_read_raf_address_phase_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { read_raf_cycle_output(self.shape.0, self.shape.1) } } diff --git a/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_cycle_phase_committed.rs b/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_cycle_phase_committed.rs index a5e81e7600..3169626b8b 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_cycle_phase_committed.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_cycle_phase_committed.rs @@ -1,6 +1,6 @@ //! The committed-program cycle phase of the bytecode read-RAF symbolic sumcheck. -use jolt_field::RingCore; +use jolt_field::Ring; use super::{BytecodeReadRafCycleShape, BytecodeReadRafInputClaims, BytecodeReadRafOutputClaims}; use crate::protocols::jolt::geometry::bytecode::{ @@ -54,11 +54,11 @@ impl SymbolicSumcheck for ReadRafCyclePhaseCommitted { self.shape.0.num_committed_ra_polys() + 1 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(bytecode_read_raf_address_phase_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { read_raf_cycle_output_committed(self.shape.0, self.shape.1) } } diff --git a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/advice/address_phase.rs b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/advice/address_phase.rs index b32d364f53..c08eb014b4 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/advice/address_phase.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/advice/address_phase.rs @@ -8,7 +8,7 @@ //! trusted-advice opening, so no runtime `kind → slot` match (with off-kind `None` //! filling) is needed. `FinalScale` is keyed by the now type-fixed kind. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::claim_reductions::advice::{ @@ -92,11 +92,11 @@ impl SymbolicSumcheck for TrustedAddressPhase { TWO_PHASE_DEGREE_BOUND } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(cycle_phase_advice_opening(JoltAdviceKind::Trusted)) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { derived(JoltDerivedId::from(AdviceClaimReductionPublic::FinalScale( JoltAdviceKind::Trusted, ))) * opening(final_advice_opening(JoltAdviceKind::Trusted)) @@ -136,11 +136,11 @@ impl SymbolicSumcheck for UntrustedAddressPhase { TWO_PHASE_DEGREE_BOUND } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(cycle_phase_advice_opening(JoltAdviceKind::Untrusted)) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { derived(JoltDerivedId::from(AdviceClaimReductionPublic::FinalScale( JoltAdviceKind::Untrusted, ))) * opening(final_advice_opening(JoltAdviceKind::Untrusted)) @@ -151,7 +151,7 @@ impl SymbolicSumcheck for UntrustedAddressPhase { mod tests { use super::*; use crate::protocols::jolt::PrecommittedReductionDimensions; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn with_address_phase() -> PrecommittedReductionDimensions { PrecommittedReductionDimensions::new(4, 3, true) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/advice/cycle_phase.rs b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/advice/cycle_phase.rs index 5c65455117..3af5740b29 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/advice/cycle_phase.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/advice/cycle_phase.rs @@ -8,7 +8,7 @@ //! trusted-advice opening, so no runtime `kind → slot` match (with off-kind `None` //! filling) is needed. `FinalScale` is keyed by the now type-fixed kind. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::claim_reductions::advice::{ @@ -95,11 +95,11 @@ impl SymbolicSumcheck for TrustedCyclePhase { TWO_PHASE_DEGREE_BOUND } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(ram_val_check_advice_opening(JoltAdviceKind::Trusted)) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { if self.dimensions.has_address_phase() { opening(cycle_phase_advice_opening(JoltAdviceKind::Trusted)) } else { @@ -144,11 +144,11 @@ impl SymbolicSumcheck for UntrustedCyclePhase { TWO_PHASE_DEGREE_BOUND } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(ram_val_check_advice_opening(JoltAdviceKind::Untrusted)) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { if self.dimensions.has_address_phase() { opening(cycle_phase_advice_opening(JoltAdviceKind::Untrusted)) } else { @@ -163,7 +163,7 @@ impl SymbolicSumcheck for UntrustedCyclePhase { mod tests { use super::*; use crate::protocols::jolt::PrecommittedReductionDimensions; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn with_address_phase() -> PrecommittedReductionDimensions { PrecommittedReductionDimensions::new(4, 3, true) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/bytecode/address_phase.rs b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/bytecode/address_phase.rs index 742464c57a..b93f96b2d4 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/bytecode/address_phase.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/bytecode/address_phase.rs @@ -1,6 +1,6 @@ //! Address phase of the two-phase committed-bytecode claim-reduction relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use super::BytecodeReductionShape; @@ -68,11 +68,11 @@ impl SymbolicSumcheck for AddressPhase { TWO_PHASE_DEGREE_BOUND } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(cycle_phase_intermediate_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { final_output_expr(self.shape.1) } } @@ -82,7 +82,7 @@ mod tests { use super::*; use crate::protocols::jolt::geometry::claim_reductions::bytecode::final_bytecode_chunk_opening; use crate::protocols::jolt::{BytecodeClaimReductionPublic, PrecommittedReductionDimensions}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { Fr::from_u64(value) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/bytecode/cycle_phase.rs b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/bytecode/cycle_phase.rs index 0f723ef7cf..5c50e577d0 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/bytecode/cycle_phase.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/bytecode/cycle_phase.rs @@ -1,6 +1,6 @@ //! Cycle phase of the two-phase committed-bytecode claim-reduction relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use super::BytecodeReductionShape; @@ -81,7 +81,7 @@ impl SymbolicSumcheck for CyclePhase { TWO_PHASE_DEGREE_BOUND } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { let eta = challenge(BytecodeClaimReductionChallenge::Eta); let mut input = JoltExpr::zero(); for stage in 0..NUM_BYTECODE_VAL_STAGES { @@ -90,7 +90,7 @@ impl SymbolicSumcheck for CyclePhase { input } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let (dimensions, chunk_count) = self.shape; if dimensions.has_address_phase() { opening(cycle_phase_intermediate_opening()) @@ -105,7 +105,7 @@ mod tests { use super::*; use crate::protocols::jolt::{BooleanityChallenge, PrecommittedReductionDimensions}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { Fr::from_u64(value) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/hamming_weight.rs b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/hamming_weight.rs index 8c0bf9ec8d..3792da1129 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/hamming_weight.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/hamming_weight.rs @@ -1,6 +1,6 @@ //! Hamming-weight claim-reduction symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::claim_reductions::hamming_weight::{ @@ -97,7 +97,7 @@ impl SymbolicSumcheck for ClaimReduction { 2 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { let gamma = challenge(HammingWeightClaimReductionChallenge::Gamma); let mut input = JoltExpr::zero(); @@ -111,7 +111,7 @@ impl SymbolicSumcheck for ClaimReduction { input } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let gamma = challenge(HammingWeightClaimReductionChallenge::Gamma); let mut output = JoltExpr::zero(); @@ -134,7 +134,7 @@ mod tests { use crate::protocols::jolt::geometry::dimensions::JoltFormulaDimensionsError; use crate::protocols::jolt::geometry::ra::{JoltRaPolynomial, JoltRaPolynomialLayout}; use crate::protocols::jolt::geometry::ram::ram_hamming_weight; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn layout( instruction: usize, diff --git a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/increments.rs b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/increments.rs index c7199f8859..7efee339c5 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/increments.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/increments.rs @@ -1,6 +1,6 @@ //! Increment claim-reduction symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::claim_reductions::increments::{ @@ -82,11 +82,11 @@ impl SymbolicSumcheck for ClaimReduction { 2 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { inc_consumers_input(challenge(IncClaimReductionChallenge::Gamma)) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let gamma = challenge(IncClaimReductionChallenge::Gamma); let ram_output_coeff = derived(IncClaimReductionPublic::EqRamReadWrite) @@ -103,7 +103,7 @@ mod tests { use super::*; use crate::protocols::jolt::geometry::ram::{ram_inc, ram_inc_val_check}; use crate::protocols::jolt::geometry::registers::{rd_inc_read_write, rd_inc_val_evaluation}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn dimensions() -> TraceDimensions { TraceDimensions::new(5) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/instruction.rs b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/instruction.rs index 7f4cc9256c..9a02bd1113 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/instruction.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/instruction.rs @@ -1,6 +1,6 @@ //! Instruction claim-reduction symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::claim_reductions::instruction::{ @@ -97,7 +97,7 @@ impl SymbolicSumcheck for ClaimReduction { 2 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { weighted_claims( lookup_output_spartan(), left_lookup_operand_spartan(), @@ -107,7 +107,7 @@ impl SymbolicSumcheck for ClaimReduction { ) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { derived(InstructionClaimReductionPublic::EqSpartan) * weighted_claims( lookup_output_reduced(), @@ -123,7 +123,7 @@ impl SymbolicSumcheck for ClaimReduction { mod tests { use super::*; use crate::protocols::jolt::InstructionClaimReductionChallenge; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn dimensions() -> TraceDimensions { TraceDimensions::new(5) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/program_image/address_phase.rs b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/program_image/address_phase.rs index 8a18ec685c..9e22b6197f 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/program_image/address_phase.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/program_image/address_phase.rs @@ -1,6 +1,6 @@ //! Address phase of the two-phase program-image (initial RAM) claim-reduction relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::claim_reductions::precommitted::TWO_PHASE_DEGREE_BOUND; @@ -66,11 +66,11 @@ impl SymbolicSumcheck for AddressPhase { TWO_PHASE_DEGREE_BOUND } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(cycle_phase_program_image_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { final_output_expr() } } @@ -80,7 +80,7 @@ mod tests { use super::*; use crate::protocols::jolt::geometry::claim_reductions::program_image::final_program_image_opening; use crate::protocols::jolt::ProgramImageClaimReductionPublic; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { Fr::from_u64(value) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/program_image/cycle_phase.rs b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/program_image/cycle_phase.rs index 69e04f1a7c..8d1abb2975 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/program_image/cycle_phase.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/program_image/cycle_phase.rs @@ -1,6 +1,6 @@ //! Cycle phase of the two-phase program-image (initial RAM) claim-reduction relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::claim_reductions::precommitted::TWO_PHASE_DEGREE_BOUND; @@ -67,11 +67,11 @@ impl SymbolicSumcheck for CyclePhase { TWO_PHASE_DEGREE_BOUND } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(ram_val_check_contribution_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { if self.shape.has_address_phase() { opening(cycle_phase_program_image_opening()) } else { diff --git a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/registers.rs b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/registers.rs index e51823ca09..91731f92a1 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/registers.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/registers.rs @@ -1,6 +1,6 @@ //! Registers claim-reduction symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::claim_reductions::registers::{ @@ -87,7 +87,7 @@ impl SymbolicSumcheck for ClaimReduction { 2 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { let gamma = challenge(RegistersClaimReductionChallenge::Gamma); opening(rd_write_value_spartan()) @@ -95,7 +95,7 @@ impl SymbolicSumcheck for ClaimReduction { + gamma.clone().pow(2) * opening(rs2_value_spartan()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let gamma = challenge(RegistersClaimReductionChallenge::Gamma); let eq_spartan = derived(RegistersClaimReductionPublic::EqSpartan); @@ -108,7 +108,7 @@ impl SymbolicSumcheck for ClaimReduction { #[cfg(test)] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn dimensions() -> TraceDimensions { TraceDimensions::new(5) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/instruction/input_virtualization.rs b/crates/jolt-claims/src/protocols/jolt/relations/instruction/input_virtualization.rs index d8f8ae15d8..0c48329c05 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/instruction/input_virtualization.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/instruction/input_virtualization.rs @@ -15,7 +15,7 @@ use crate::protocols::jolt::{ }; use crate::SymbolicSumcheck; use crate::{challenge, derived, opening, InputClaims, OutputClaims, SumcheckChallenges}; -use jolt_field::RingCore; +use jolt_field::Ring; /// Produced instruction-input virtualization openings (the left/right operand /// selector flags and their operand values), all sharing the single @@ -97,13 +97,13 @@ impl SymbolicSumcheck for InputVirtualization { INPUT_VIRTUALIZATION_DEGREE } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(right_instruction_input_product()) + challenge(InstructionInputChallenge::Gamma) * opening(left_instruction_input_product()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { derived(InstructionInputPublic::EqProduct) * opening(right_operand_is_rs2()) * opening(rs2_value()) @@ -125,7 +125,7 @@ impl SymbolicSumcheck for InputVirtualization { mod tests { use super::*; use crate::protocols::jolt::{JoltChallengeId, JoltDerivedId}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn trace_dimensions() -> TraceDimensions { TraceDimensions::new(5) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/instruction/ra_virtualization.rs b/crates/jolt-claims/src/protocols/jolt/relations/instruction/ra_virtualization.rs index 462ed4c29e..34c9e30075 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/instruction/ra_virtualization.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/instruction/ra_virtualization.rs @@ -1,6 +1,6 @@ //! Instruction RA-virtualization symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::instruction::{ @@ -74,12 +74,12 @@ impl SymbolicSumcheck for RaVirtualization { self.shape.num_committed_per_virtual() + 1 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { let gamma = challenge(InstructionRaVirtualizationChallenge::Gamma); weighted_instruction_ra_sum(self.shape, gamma) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let gamma = challenge(InstructionRaVirtualizationChallenge::Gamma); let eq_cycle = derived(InstructionRaVirtualizationPublic::EqCycle); let mut output = JoltExpr::zero(); @@ -101,7 +101,7 @@ mod tests { JoltChallengeId, JoltCommittedPolynomial, JoltDerivedId, JoltOpeningId, JoltPolynomialId, JoltVirtualPolynomial, }; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn ra_virtualization_dimensions( num_virtual_ra_polys: usize, diff --git a/crates/jolt-claims/src/protocols/jolt/relations/instruction/read_raf.rs b/crates/jolt-claims/src/protocols/jolt/relations/instruction/read_raf.rs index fd0a746b0c..9b3b3b8fbf 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/instruction/read_raf.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/instruction/read_raf.rs @@ -1,6 +1,6 @@ //! Instruction read-RAF symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use jolt_lookup_tables::{LookupTableKind, XLEN}; use serde::{Deserialize, Serialize}; @@ -85,14 +85,14 @@ impl SymbolicSumcheck for ReadRaf { self.shape.num_virtual_ra_polys() + READ_RAF_BASE_DEGREE } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { let gamma = challenge(InstructionReadRafChallenge::Gamma); opening(lookup_output_reduced()) + gamma.clone() * opening(left_lookup_operand_reduced()) + gamma.pow(2) * opening(right_lookup_operand_reduced()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let ra_product = instruction_ra_product(self.shape); let mut output = JoltExpr::zero(); @@ -121,7 +121,7 @@ mod tests { use crate::protocols::jolt::{ JoltChallengeId, JoltDerivedId, JoltOpeningId, JoltPolynomialId, JoltVirtualPolynomial, }; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn read_raf_dimensions(num_virtual_ra_polys: usize) -> InstructionReadRafDimensions { InstructionReadRafDimensions::try_from((5, 128, num_virtual_ra_polys)) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/ram/hamming_booleanity.rs b/crates/jolt-claims/src/protocols/jolt/relations/ram/hamming_booleanity.rs index b413dfb359..d74526afab 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/ram/hamming_booleanity.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/ram/hamming_booleanity.rs @@ -2,7 +2,7 @@ use core::marker::PhantomData; -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::ram::ram_hamming_weight; @@ -37,7 +37,7 @@ impl Default for RamHammingBooleanityInputClaims { } } -impl InputClaims for RamHammingBooleanityInputClaims { +impl InputClaims for RamHammingBooleanityInputClaims { fn canonical_order(&self) -> Vec { Vec::new() } @@ -81,11 +81,11 @@ impl SymbolicSumcheck for HammingBooleanity { 3 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { JoltExpr::zero() } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let eq_cycle = derived(RamHammingBooleanityPublic::EqCycle); let h = opening(ram_hamming_weight()); eq_cycle * (h.clone() * h.clone() - h) @@ -96,7 +96,7 @@ impl SymbolicSumcheck for HammingBooleanity { mod tests { use super::*; use crate::protocols::jolt::JoltDerivedId; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn trace_dimensions() -> TraceDimensions { TraceDimensions::new(5) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/ram/output_check.rs b/crates/jolt-claims/src/protocols/jolt/relations/ram/output_check.rs index c942730863..eb6976879a 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/ram/output_check.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/ram/output_check.rs @@ -2,7 +2,7 @@ use core::marker::PhantomData; -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::ram::ram_val_final; @@ -41,7 +41,7 @@ impl Default for RamOutputCheckInputClaims { } } -impl InputClaims for RamOutputCheckInputClaims { +impl InputClaims for RamOutputCheckInputClaims { fn canonical_order(&self) -> Vec { Vec::new() } @@ -65,7 +65,7 @@ pub struct RamOutputCheckChallenges { pub output_address: Vec, } -impl SumcheckChallenges for RamOutputCheckChallenges { +impl SumcheckChallenges for RamOutputCheckChallenges { fn from_transcript_values>( _values: I, ) -> Result { @@ -111,11 +111,11 @@ impl SymbolicSumcheck for OutputCheck { 3 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { JoltExpr::zero() } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { derived(RamOutputCheckPublic::EqAddress) * derived(RamOutputCheckPublic::IoMask) * opening(ram_val_final()) @@ -129,7 +129,7 @@ impl SymbolicSumcheck for OutputCheck { mod tests { use super::*; use crate::protocols::jolt::JoltDerivedId; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn read_write_dimensions() -> ReadWriteDimensions { ReadWriteDimensions::new(5, 4, 2, 1) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/ram/ra_claim_reduction.rs b/crates/jolt-claims/src/protocols/jolt/relations/ram/ra_claim_reduction.rs index a87711ef03..d7196b8bb0 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/ram/ra_claim_reduction.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/ram/ra_claim_reduction.rs @@ -1,6 +1,6 @@ //! RAM `ra` claim-reduction symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::ram::{ @@ -81,14 +81,14 @@ impl SymbolicSumcheck for RaClaimReduction { 2 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { let gamma = challenge(RamRaClaimReductionChallenge::Gamma); opening(ram_ra_raf_evaluation()) + gamma.clone() * opening(ram_ra()) + gamma.clone().pow(2) * opening(ram_ra_val_check()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let gamma = challenge(RamRaClaimReductionChallenge::Gamma); (derived(RamRaClaimReductionPublic::EqCycleRaf) + gamma.clone() * derived(RamRaClaimReductionPublic::EqCycleReadWrite) @@ -102,7 +102,7 @@ mod tests { use super::*; use crate::protocols::jolt::geometry::ram::RamRaClaimReductionPublicValues; use crate::protocols::jolt::{JoltChallengeId, JoltDerivedId}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn trace_dimensions() -> TraceDimensions { TraceDimensions::new(5) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/ram/ra_virtualization.rs b/crates/jolt-claims/src/protocols/jolt/relations/ram/ra_virtualization.rs index 7fb2063143..33d574ec96 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/ram/ra_virtualization.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/ram/ra_virtualization.rs @@ -1,6 +1,6 @@ //! RAM `ra` virtualization symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::ram::{ @@ -62,11 +62,11 @@ impl SymbolicSumcheck for RaVirtualization { self.shape.num_committed_ra_polys() + 1 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(ram_ra_claim_reduction()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { derived(RamRaVirtualizationPublic::EqCycle) * committed_ram_ra_product(self.shape) } } @@ -76,7 +76,7 @@ mod tests { use super::*; use crate::protocols::jolt::geometry::ram::committed_ram_ra; use crate::protocols::jolt::JoltDerivedId; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn ra_virtualization_dimensions(committed_ra_polys: usize) -> RamRaVirtualizationDimensions { RamRaVirtualizationDimensions::new(5, committed_ra_polys) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/ram/raf_evaluation.rs b/crates/jolt-claims/src/protocols/jolt/relations/ram/raf_evaluation.rs index 76e3dd9bdd..24a65ed68c 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/ram/raf_evaluation.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/ram/raf_evaluation.rs @@ -1,6 +1,6 @@ //! RAM RAF-evaluation symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::ram::{ @@ -67,11 +67,11 @@ impl SymbolicSumcheck for RafEvaluation { 2 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { constant(F::pow2(self.shape.phase3_cycle_rounds())) * opening(ram_address_spartan()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { derived(RamRafEvaluationPublic::UnmapAddress) * opening(ram_ra_raf_evaluation()) } } @@ -81,7 +81,7 @@ impl SymbolicSumcheck for RafEvaluation { mod tests { use super::*; use crate::protocols::jolt::{JoltDerivedId, ReadWriteDimensions}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn read_write_dimensions() -> ReadWriteDimensions { ReadWriteDimensions::new(5, 4, 2, 1) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/ram/read_write_checking.rs b/crates/jolt-claims/src/protocols/jolt/relations/ram/read_write_checking.rs index bcd530f824..83714ed8c6 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/ram/read_write_checking.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/ram/read_write_checking.rs @@ -1,6 +1,6 @@ //! RAM read/write-checking symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::ram::{ @@ -83,12 +83,12 @@ impl SymbolicSumcheck for ReadWriteChecking { 3 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(ram_read_value()) + challenge(RamReadWriteChallenge::Gamma) * opening(ram_write_value()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { derived(RamReadWritePublic::EqCycle) * opening(ram_ra()) * opening(ram_val()) + derived(RamReadWritePublic::EqCycle) * challenge(RamReadWriteChallenge::Gamma) @@ -105,7 +105,7 @@ impl SymbolicSumcheck for ReadWriteChecking { mod tests { use super::*; use crate::protocols::jolt::{BooleanityChallenge, JoltChallengeId, JoltDerivedId}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn read_write_dimensions() -> ReadWriteDimensions { ReadWriteDimensions::new(5, 4, 2, 1) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/ram/val_check.rs b/crates/jolt-claims/src/protocols/jolt/relations/ram/val_check.rs index ee839dbd4f..04a45e04d3 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/ram/val_check.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/ram/val_check.rs @@ -1,6 +1,6 @@ //! RAM value-check symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::ram::{ @@ -130,7 +130,7 @@ impl SymbolicSumcheck for RamValCheck { 3 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { let gamma = challenge(JoltChallengeId::from(RamValCheckChallenge::Gamma)); let mut init = derived(JoltDerivedId::from(RamValCheckPublic::InitEval)); for contribution in &self.shape.contributions { @@ -142,7 +142,7 @@ impl SymbolicSumcheck for RamValCheck { - (JoltExpr::one() + gamma) * init } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { derived(JoltDerivedId::from(RamValCheckPublic::LtCyclePlusGamma)) * opening(ram_inc_val_check()) * opening(ram_ra_val_check()) @@ -152,7 +152,7 @@ impl SymbolicSumcheck for RamValCheck { #[cfg(test)] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn trace_dimensions() -> TraceDimensions { TraceDimensions::new(5) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/registers/read_write_checking.rs b/crates/jolt-claims/src/protocols/jolt/relations/registers/read_write_checking.rs index 186b34afcb..1808959756 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/registers/read_write_checking.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/registers/read_write_checking.rs @@ -1,6 +1,6 @@ //! registers read-write checking symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::registers::{ @@ -90,14 +90,14 @@ impl SymbolicSumcheck for ReadWriteChecking { 3 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { let gamma = challenge(RegistersReadWriteChallenge::Gamma); opening(rd_write_value_claim()) + gamma.clone() * opening(rs1_value_claim()) + gamma.clone().pow(2) * opening(rs2_value_claim()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let gamma = challenge(RegistersReadWriteChallenge::Gamma); let eq_cycle = derived(RegistersReadWritePublic::EqCycle); eq_cycle.clone() * opening(rd_wa_read_write()) * opening(rd_inc_read_write()) @@ -117,7 +117,7 @@ impl SymbolicSumcheck for ReadWriteChecking { mod tests { use super::*; use crate::protocols::jolt::{JoltChallengeId, JoltDerivedId}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn read_write_dimensions() -> ReadWriteDimensions { ReadWriteDimensions::new(5, 7, 2, 1) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/registers/val_evaluation.rs b/crates/jolt-claims/src/protocols/jolt/relations/registers/val_evaluation.rs index 319f920657..1c8c532471 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/registers/val_evaluation.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/registers/val_evaluation.rs @@ -1,6 +1,6 @@ //! registers val-evaluation symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::registers::{ @@ -66,11 +66,11 @@ impl SymbolicSumcheck for ValEvaluation { 3 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(registers_val_read_write()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { derived(RegistersValEvaluationPublic::LtCycle) * opening(rd_inc_val_evaluation()) * opening(rd_wa_val_evaluation()) @@ -81,7 +81,7 @@ impl SymbolicSumcheck for ValEvaluation { mod tests { use super::*; use crate::protocols::jolt::JoltDerivedId; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn trace_dimensions() -> TraceDimensions { TraceDimensions::new(5) diff --git a/crates/jolt-claims/src/protocols/jolt/relations/spartan/outer_remainder.rs b/crates/jolt-claims/src/protocols/jolt/relations/spartan/outer_remainder.rs index 54d808fd4d..0b263221e3 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/spartan/outer_remainder.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/spartan/outer_remainder.rs @@ -1,6 +1,6 @@ //! Spartan outer remainder symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use jolt_riscv::CircuitFlags; use serde::{Deserialize, Serialize}; @@ -139,11 +139,11 @@ impl SymbolicSumcheck for OuterRemainder { OUTER_REMAINDER_DEGREE } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(outer_uniskip_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { // The factored quadratic form `tau_kernel · Az · Bz` with each linear // form expanded over its per-column weights — every derived leaf one // multilinear (the weights are linear in the stream variable). @@ -170,7 +170,7 @@ impl SymbolicSumcheck for OuterRemainder { mod tests { use super::*; use crate::protocols::jolt::JoltVirtualPolynomial; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_riscv::CIRCUIT_FLAGS; /// The expanded `output_expression` reproduces the factored quadratic form diff --git a/crates/jolt-claims/src/protocols/jolt/relations/spartan/outer_uniskip.rs b/crates/jolt-claims/src/protocols/jolt/relations/spartan/outer_uniskip.rs index b85d2db49e..330c9f169b 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/spartan/outer_uniskip.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/spartan/outer_uniskip.rs @@ -2,7 +2,7 @@ use core::marker::PhantomData; -use jolt_field::{Field, RingCore}; +use jolt_field::{JoltField, Ring}; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::dimensions::{ @@ -27,7 +27,7 @@ impl Default for OuterUniskipInputClaims { } } -impl InputClaims for OuterUniskipInputClaims { +impl InputClaims for OuterUniskipInputClaims { fn canonical_order(&self) -> Vec { Vec::new() } @@ -87,11 +87,11 @@ impl SymbolicSumcheck for OuterUniskip { OUTER_UNISKIP_FIRST_ROUND_DEGREE } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { JoltExpr::zero() } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { opening(outer_uniskip_opening()) } } diff --git a/crates/jolt-claims/src/protocols/jolt/relations/spartan/product_remainder.rs b/crates/jolt-claims/src/protocols/jolt/relations/spartan/product_remainder.rs index 678570d20f..ecff998207 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/spartan/product_remainder.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/spartan/product_remainder.rs @@ -1,6 +1,6 @@ //! Spartan product remainder symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use jolt_riscv::{CircuitFlags, InstructionFlags}; use serde::{Deserialize, Serialize}; @@ -86,11 +86,11 @@ impl SymbolicSumcheck for ProductRemainder { PRODUCT_REMAINDER_DEGREE } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(product_uniskip_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let left = product_weight(0) * opening(left_instruction_input_product()) + product_weight(1) * opening(lookup_output_product()) + product_weight(2) * opening(jump_flag_product()); @@ -106,7 +106,7 @@ impl SymbolicSumcheck for ProductRemainder { mod tests { use super::*; use crate::protocols::jolt::{JoltDerivedId, SpartanProductVirtualizationPublic}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; #[test] fn product_remainder_evaluates_like_core_formula() { diff --git a/crates/jolt-claims/src/protocols/jolt/relations/spartan/product_uniskip.rs b/crates/jolt-claims/src/protocols/jolt/relations/spartan/product_uniskip.rs index 20d8950721..d9bd0faab8 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/spartan/product_uniskip.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/spartan/product_uniskip.rs @@ -1,6 +1,6 @@ //! Spartan product univariate-skip symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use serde::{Deserialize, Serialize}; use crate::protocols::jolt::geometry::dimensions::{ @@ -79,13 +79,13 @@ impl SymbolicSumcheck for ProductUniskip { PRODUCT_UNISKIP_FIRST_ROUND_DEGREE } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { product_uniskip_weight(0) * opening(product_outer_opening()) + product_uniskip_weight(1) * opening(product_should_branch_outer_opening()) + product_uniskip_weight(2) * opening(product_should_jump_outer_opening()) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { opening(product_uniskip_opening()) } } diff --git a/crates/jolt-claims/src/protocols/jolt/relations/spartan/shift.rs b/crates/jolt-claims/src/protocols/jolt/relations/spartan/shift.rs index 05e25f0a4d..2c427e118f 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/spartan/shift.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/spartan/shift.rs @@ -1,6 +1,6 @@ //! Spartan shift symbolic sumcheck relation. -use jolt_field::RingCore; +use jolt_field::Ring; use jolt_riscv::{CircuitFlags, InstructionFlags}; use serde::{Deserialize, Serialize}; @@ -97,7 +97,7 @@ impl SymbolicSumcheck for Shift { SHIFT_DEGREE } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { let gamma = challenge(SpartanShiftChallenge::Gamma); opening(next_unexpanded_pc_outer()) + gamma.clone() * opening(next_pc_outer()) @@ -106,7 +106,7 @@ impl SymbolicSumcheck for Shift { + gamma.pow(4) * (JoltExpr::one() - opening(next_is_noop_product())) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { let gamma = challenge(SpartanShiftChallenge::Gamma); derived(SpartanShiftPublic::EqPlusOneOuter) * (opening(unexpanded_pc_shift()) @@ -123,7 +123,7 @@ impl SymbolicSumcheck for Shift { mod tests { use super::*; use crate::protocols::jolt::{JoltChallengeId, JoltDerivedId}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn gamma_power(gamma: Fr, exponent: usize) -> Fr { let mut value = Fr::from_u64(1); diff --git a/crates/jolt-claims/src/symbolic.rs b/crates/jolt-claims/src/symbolic.rs index d314817a15..dd4b4637d2 100644 --- a/crates/jolt-claims/src/symbolic.rs +++ b/crates/jolt-claims/src/symbolic.rs @@ -1,4 +1,4 @@ -use jolt_field::RingCore; +use jolt_field::Ring; use crate::{Expr, Source, SumcheckDomain}; @@ -54,10 +54,10 @@ pub trait SymbolicSumcheck { /// The per-round degree bound, derived from [`Shape`](Self::Shape). fn degree(&self) -> usize; - fn input_expression( + fn input_expression( &self, ) -> Expr; - fn output_expression( + fn output_expression( &self, ) -> Expr; @@ -69,7 +69,7 @@ pub trait SymbolicSumcheck { /// holds because the output check constrains every produced opening (an /// unconstrained produced opening would be unsound). The field `F` only /// instantiates the expression — the ids are field-independent. - fn expected_output_openings(&self) -> std::collections::BTreeSet + fn expected_output_openings(&self) -> std::collections::BTreeSet where Self::OpeningId: Ord, { diff --git a/crates/jolt-claims/tests/lattice_semantics.rs b/crates/jolt-claims/tests/lattice_semantics.rs index 03a741a08b..821e000616 100644 --- a/crates/jolt-claims/tests/lattice_semantics.rs +++ b/crates/jolt-claims/tests/lattice_semantics.rs @@ -13,7 +13,7 @@ use jolt_claims::protocols::jolt::lattice::{ one_hot_trace_columns, OneHotTraceShape, UnsignedIncChunking, }; use jolt_claims::protocols::jolt::{BytecodeRegisterLane, JoltCommittedPolynomial}; -use jolt_field::{Fr, FromPrimitiveInt, RingCore}; +use jolt_field::{Fr, Ring}; use jolt_lookup_tables::{LookupTableKind, XLEN}; use jolt_poly::math::Math; use jolt_poly::{boolean_point_msb, eq_index_msb, EqPolynomial, Polynomial}; diff --git a/crates/jolt-crypto/benches/crypto.rs b/crates/jolt-crypto/benches/crypto.rs index 23ae34fd64..21ec4d1092 100644 --- a/crates/jolt-crypto/benches/crypto.rs +++ b/crates/jolt-crypto/benches/crypto.rs @@ -5,7 +5,7 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use jolt_crypto::{ Bn254, Bn254G1, Bn254G2, JoltGroup, PairingGroup, Pedersen, PedersenSetup, VectorCommitment, }; -use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{Field, Fr, Ring}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-crypto/fuzz/fuzz_targets/group_arith.rs b/crates/jolt-crypto/fuzz/fuzz_targets/group_arith.rs index 5a4fa15080..a33b156bd9 100644 --- a/crates/jolt-crypto/fuzz/fuzz_targets/group_arith.rs +++ b/crates/jolt-crypto/fuzz/fuzz_targets/group_arith.rs @@ -1,14 +1,14 @@ #![no_main] use jolt_crypto::{Bn254, Bn254G1, JoltGroup}; -use jolt_field::{Fr, CanonicalRepr}; +use jolt_field::{Fr, CanonicalEncoding}; use libfuzzer_sys::fuzz_target; fn parse_input(data: &[u8]) -> Option<(Fr, Fr, Bn254G1)> { if data.len() < 64 { return None; } - let s1 = ::from_le_bytes_mod_order(&data[..32]); - let s2 = ::from_le_bytes_mod_order(&data[32..64]); + let s1 = ::from_bytes_le_reduced(&data[..32]); + let s2 = ::from_bytes_le_reduced(&data[32..64]); let g = Bn254::g1_generator(); let p = g.scalar_mul(&s1); Some((s1, s2, p)) diff --git a/crates/jolt-crypto/fuzz/fuzz_targets/pedersen_commit.rs b/crates/jolt-crypto/fuzz/fuzz_targets/pedersen_commit.rs index 3e7aa53797..dd14da8ace 100644 --- a/crates/jolt-crypto/fuzz/fuzz_targets/pedersen_commit.rs +++ b/crates/jolt-crypto/fuzz/fuzz_targets/pedersen_commit.rs @@ -1,6 +1,6 @@ #![no_main] use jolt_crypto::{Bn254, Bn254G1, VectorCommitment, JoltGroup, Pedersen, PedersenSetup}; -use jolt_field::{Fr, FromPrimitiveInt, CanonicalRepr}; +use jolt_field::{Fr, Ring, CanonicalEncoding}; use libfuzzer_sys::fuzz_target; /// Fixed small setup (4 generators) — deterministic so we don't waste fuzzer @@ -23,9 +23,9 @@ fuzz_target!(|data: &[u8]| { let setup = fixed_setup(); let values: Vec = (0..4) - .map(|i| ::from_le_bytes_mod_order(&data[i * 32..(i + 1) * 32])) + .map(|i| ::from_bytes_le_reduced(&data[i * 32..(i + 1) * 32])) .collect(); - let blinding = ::from_le_bytes_mod_order(&data[128..160]); + let blinding = ::from_bytes_le_reduced(&data[128..160]); // Commit-verify round-trip let c = Pedersen::::commit(&setup, &values, &blinding); diff --git a/crates/jolt-crypto/src/commitment.rs b/crates/jolt-crypto/src/commitment.rs index 55b5b7c0d6..3301fed214 100644 --- a/crates/jolt-crypto/src/commitment.rs +++ b/crates/jolt-crypto/src/commitment.rs @@ -3,7 +3,7 @@ use std::{ fmt::{self, Debug}, }; -use jolt_field::{Accumulator, Field, WithAccumulator}; +use jolt_field::{Accumulator, JoltField, WithAccumulator}; use jolt_poly::EqPolynomial; use jolt_transcript::AppendToTranscript; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -31,7 +31,7 @@ pub trait Commitment: Clone + Debug + Eq + Send + Sync + 'static { pub trait VectorCommitment: Commitment { - type Field: Field; + type Field: JoltField; /// Transparent setup parameters (generators, public parameters, etc.). type Setup: Clone + Send + Sync; @@ -223,7 +223,7 @@ impl Error for VectorOpeningError {} /// Blanket-implemented for [`JoltGroup`](crate::JoltGroup) over any field /// (via `scalar_mul` + addition). Non-group commitment types (e.g., lattice /// vectors) can implement this trait directly for their native scalar field. -pub trait HomomorphicCommitment: Clone + Default { +pub trait HomomorphicCommitment: Clone + Default { /// Computes `c1 + c2`. #[must_use] fn add(c1: &Self, c2: &Self) -> Self; @@ -270,7 +270,7 @@ fn point_len_to_basis_len(point_len: usize) -> Result } #[cfg(feature = "parallel")] -fn combine_rows( +fn combine_rows( flattened_rows: &[F], row_len: usize, row_weights: &[F], @@ -309,7 +309,7 @@ fn combine_rows( } #[cfg(not(feature = "parallel"))] -fn combine_rows( +fn combine_rows( flattened_rows: &[F], row_len: usize, row_weights: &[F], @@ -330,7 +330,7 @@ fn combine_rows( combined_vector } -fn inner_product(lhs: &[F], rhs: &[F]) -> F { +fn inner_product(lhs: &[F], rhs: &[F]) -> F { #[cfg(feature = "parallel")] { if lhs.len() >= PAR_THRESHOLD { @@ -366,7 +366,7 @@ fn inner_product(lhs: &[F], rhs: &[F]) -> F { fn combine_commitments(commitments: &[C], weights: &[F]) -> C where - F: Field, + F: JoltField, C: HomomorphicCommitment + Copy + Send + Sync, { #[cfg(feature = "parallel")] @@ -390,7 +390,7 @@ where }) } -impl HomomorphicCommitment for G { +impl HomomorphicCommitment for G { #[inline] fn add(c1: &G, c2: &G) -> G { *c1 + c2 diff --git a/crates/jolt-crypto/src/ec/bn254/gt.rs b/crates/jolt-crypto/src/ec/bn254/gt.rs index 294ad75f98..cf3f8cf2d5 100644 --- a/crates/jolt-crypto/src/ec/bn254/gt.rs +++ b/crates/jolt-crypto/src/ec/bn254/gt.rs @@ -3,7 +3,7 @@ use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use ark_bn254::{Fq12, Fr}; use ark_ff::{AdditiveGroup, Field as ArkField, PrimeField}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_transcript::{AppendToTranscript, Transcript}; @@ -170,14 +170,14 @@ impl JoltGroup for Bn254GT { } #[inline] - fn scalar_mul(&self, scalar: &F) -> Self { + fn scalar_mul(&self, scalar: &F) -> Self { // GT exponentiation: self^scalar (written additively as scalar * self). let fr = field_to_fr(scalar); Self(self.0.pow(fr.into_bigint())) } #[inline] - fn msm(bases: &[Self], scalars: &[F]) -> Self { + fn msm(bases: &[Self], scalars: &[F]) -> Self { debug_assert_eq!(bases.len(), scalars.len()); // GT "MSM" is Π bases[i]^scalars[i] (written additively as Σ scalars[i] * bases[i]). let mut acc = Fq12::ONE; diff --git a/crates/jolt-crypto/src/ec/bn254/mod.rs b/crates/jolt-crypto/src/ec/bn254/mod.rs index 38f713e6b9..29173ff37c 100644 --- a/crates/jolt-crypto/src/ec/bn254/mod.rs +++ b/crates/jolt-crypto/src/ec/bn254/mod.rs @@ -157,12 +157,12 @@ macro_rules! impl_jolt_group_wrapper { } #[inline] - fn scalar_mul(&self, scalar: &F) -> Self { + fn scalar_mul(&self, scalar: &F) -> Self { Self(self.0 * super::field_to_fr(scalar)) } #[inline] - fn msm(bases: &[Self], scalars: &[F]) -> Self { + fn msm(bases: &[Self], scalars: &[F]) -> Self { use ::ark_ec::{CurveGroup, VariableBaseMSM}; use ::ark_ff::PrimeField; debug_assert_eq!(bases.len(), scalars.len()); @@ -197,7 +197,7 @@ use ark_bn254::Bn254 as ArkBn254; use ark_ec::pairing::Pairing; use ark_ec::CurveGroup; use ark_ff::PrimeField as _; -use jolt_field::Field; +use jolt_field::JoltField; use crate::PairingGroup; @@ -248,16 +248,16 @@ impl PairingGroup for Bn254 { } } -/// Converts a generic `Field` element to an arkworks `Fr` via serialization. +/// Converts a generic `JoltField` element to an arkworks `Fr` via serialization. /// -/// This is the bridge between jolt-field's backend-agnostic `Field` trait and +/// This is the bridge between jolt-field's backend-agnostic `JoltField` trait and /// arkworks' concrete scalar type. The conversion goes through little-endian /// byte serialization. /// /// In debug builds, asserts that the source value fits in the BN254 Fr modulus — /// catches silent modular reduction when `F` has a larger modulus than BN254 Fr. #[inline] -pub(crate) fn field_to_fr(f: &F) -> ark_bn254::Fr { +pub(crate) fn field_to_fr(f: &F) -> ark_bn254::Fr { let mut bytes = vec![0u8; F::NUM_BYTES]; f.to_bytes_le(&mut bytes); #[cfg(debug_assertions)] diff --git a/crates/jolt-crypto/src/ec/group.rs b/crates/jolt-crypto/src/ec/group.rs index 9268a1bdc4..3086e1f894 100644 --- a/crates/jolt-crypto/src/ec/group.rs +++ b/crates/jolt-crypto/src/ec/group.rs @@ -1,7 +1,7 @@ use std::fmt::Debug; use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_transcript::AppendToTranscript; use serde::{Deserialize, Serialize}; @@ -51,7 +51,7 @@ pub trait JoltGroup: /// Scalar multiplication: `scalar * self`. #[must_use] - fn scalar_mul(&self, scalar: &F) -> Self; + fn scalar_mul(&self, scalar: &F) -> Self; /// Multi-scalar multiplication: `Σᵢ scalars[i] * bases[i]`. /// @@ -59,5 +59,5 @@ pub trait JoltGroup: /// /// Debug-asserts that `bases.len() == scalars.len()`. #[must_use] - fn msm(bases: &[Self], scalars: &[F]) -> Self; + fn msm(bases: &[Self], scalars: &[F]) -> Self; } diff --git a/crates/jolt-crypto/src/ec/pairing.rs b/crates/jolt-crypto/src/ec/pairing.rs index ab9c164262..bf65f2b5ef 100644 --- a/crates/jolt-crypto/src/ec/pairing.rs +++ b/crates/jolt-crypto/src/ec/pairing.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use std::fmt::Debug; use super::group::JoltGroup; @@ -14,7 +14,7 @@ use super::group::JoltGroup; /// is Fq12 multiplication. See `Bn254GT` for the mapping. pub trait PairingGroup: Clone + Debug + Eq + Sync + Send + 'static { /// Scalar field for G1 and G2 (e.g., BN254 Fr). - type ScalarField: Field; + type ScalarField: JoltField; type G1: JoltGroup; type G2: JoltGroup; type GT: JoltGroup; diff --git a/crates/jolt-crypto/tests/coverage.rs b/crates/jolt-crypto/tests/coverage.rs index c3f1239b83..7887c54ab7 100644 --- a/crates/jolt-crypto/tests/coverage.rs +++ b/crates/jolt-crypto/tests/coverage.rs @@ -7,7 +7,7 @@ use jolt_crypto::ec::bn254::glv; use jolt_crypto::{ Bn254, Bn254G1, Bn254G2, Bn254GT, HomomorphicCommitment, JoltGroup, PairingGroup, }; -use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{Field, Fr, Ring}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-crypto/tests/group_laws.rs b/crates/jolt-crypto/tests/group_laws.rs index ef07767f79..1e774494a8 100644 --- a/crates/jolt-crypto/tests/group_laws.rs +++ b/crates/jolt-crypto/tests/group_laws.rs @@ -1,7 +1,7 @@ //! Algebraic group law tests for BN254 G1 and G2. use jolt_crypto::{Bn254, Bn254G1, Bn254G2, JoltGroup}; -use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{Field, Fr, Ring}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-crypto/tests/pairing.rs b/crates/jolt-crypto/tests/pairing.rs index a5a5d8be8b..f12041d3c8 100644 --- a/crates/jolt-crypto/tests/pairing.rs +++ b/crates/jolt-crypto/tests/pairing.rs @@ -1,7 +1,7 @@ //! Pairing bilinearity and consistency tests for BN254. use jolt_crypto::{Bn254, Bn254G2, Bn254GT, JoltGroup, PairingGroup}; -use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{Field, Fr, Ring}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-crypto/tests/pedersen.rs b/crates/jolt-crypto/tests/pedersen.rs index 47509232b0..89daf1166e 100644 --- a/crates/jolt-crypto/tests/pedersen.rs +++ b/crates/jolt-crypto/tests/pedersen.rs @@ -6,7 +6,7 @@ use jolt_crypto::{ Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment, VectorCommitmentOpening, VectorOpeningError, }; -use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{Field, Fr, Ring}; use jolt_poly::EqPolynomial; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-crypto/tests/serialization.rs b/crates/jolt-crypto/tests/serialization.rs index d543789d24..9aa7ca3e45 100644 --- a/crates/jolt-crypto/tests/serialization.rs +++ b/crates/jolt-crypto/tests/serialization.rs @@ -2,7 +2,7 @@ //! Serialization round-trip tests for all BN254 types. use jolt_crypto::{Bn254, Bn254G1, Bn254G2, Bn254GT, JoltGroup, PairingGroup, PedersenSetup}; -use jolt_field::{Fr, FromPrimitiveInt}; +use jolt_field::{Fr, Ring}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-dory/benches/dory.rs b/crates/jolt-dory/benches/dory.rs index 2fd3d13c70..d4e474f01b 100644 --- a/crates/jolt-dory/benches/dory.rs +++ b/crates/jolt-dory/benches/dory.rs @@ -7,7 +7,7 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use jolt_dory::{DoryScheme, DoryVerifierSetup}; -use jolt_field::{FieldCore, Fr}; +use jolt_field::{Field, Fr}; use jolt_openings::{CommitmentScheme, StreamingCommitment, ZkOpeningScheme}; use jolt_poly::{OneHotPolynomial, Polynomial}; use jolt_transcript::Transcript; @@ -62,9 +62,8 @@ fn bench_open(c: &mut Criterion) { || { let mut rng = ChaCha20Rng::seed_from_u64(0); let poly = Polynomial::::random(nv, &mut rng); - let point: Vec = (0..nv) - .map(|_| ::random(&mut rng)) - .collect(); + let point: Vec = + (0..nv).map(|_| ::random(&mut rng)).collect(); let eval = poly.evaluate(&point); (poly, point, eval) }, @@ -93,9 +92,8 @@ fn bench_verify(c: &mut Criterion) { || { let mut rng = ChaCha20Rng::seed_from_u64(0); let poly = Polynomial::::random(nv, &mut rng); - let point: Vec = (0..nv) - .map(|_| ::random(&mut rng)) - .collect(); + let point: Vec = + (0..nv).map(|_| ::random(&mut rng)).collect(); let eval = poly.evaluate(&point); let (commitment, _) = DoryScheme::commit(poly.evaluations(), &setup).unwrap(); @@ -166,8 +164,8 @@ fn bench_combine(c: &mut Criterion) { let poly_b = Polynomial::::random(num_vars, &mut rng); let (commit_a, _) = DoryScheme::commit(poly_a.evaluations(), &setup).unwrap(); let (commit_b, _) = DoryScheme::commit(poly_b.evaluations(), &setup).unwrap(); - let s_a = ::random(&mut rng); - let s_b = ::random(&mut rng); + let s_a = ::random(&mut rng); + let s_b = ::random(&mut rng); group.bench_with_input(BenchmarkId::from_parameter(num_vars), &num_vars, |b, _| { b.iter(|| { @@ -192,7 +190,7 @@ fn bench_combine_hints(c: &mut Criterion) { .map(|_| { let poly = Polynomial::::random(num_vars, &mut rng); let (_, hint) = DoryScheme::commit(poly.evaluations(), &setup).unwrap(); - (hint, ::random(&mut rng)) + (hint, ::random(&mut rng)) }) .collect(); let hints: Vec<_> = hints_and_scalars.iter().map(|(h, _)| h.clone()).collect(); @@ -229,9 +227,8 @@ fn bench_open_zk(c: &mut Criterion) { || { let mut rng = ChaCha20Rng::seed_from_u64(0); let poly = Polynomial::::random(nv, &mut rng); - let point: Vec = (0..nv) - .map(|_| ::random(&mut rng)) - .collect(); + let point: Vec = + (0..nv).map(|_| ::random(&mut rng)).collect(); let eval = poly.evaluate(&point); let (_, hint) = ::commit_zk(poly.evaluations(), &setup) @@ -264,9 +261,8 @@ fn bench_verify_zk(c: &mut Criterion) { || { let mut rng = ChaCha20Rng::seed_from_u64(0); let poly = Polynomial::::random(nv, &mut rng); - let point: Vec = (0..nv) - .map(|_| ::random(&mut rng)) - .collect(); + let point: Vec = + (0..nv).map(|_| ::random(&mut rng)).collect(); let eval = poly.evaluate(&point); let (commitment, hint) = ::commit_zk(poly.evaluations(), &setup) diff --git a/crates/jolt-dory/fuzz/fuzz_targets/verify_tampered.rs b/crates/jolt-dory/fuzz/fuzz_targets/verify_tampered.rs index bb39462066..82b6ee19bb 100644 --- a/crates/jolt-dory/fuzz/fuzz_targets/verify_tampered.rs +++ b/crates/jolt-dory/fuzz/fuzz_targets/verify_tampered.rs @@ -3,7 +3,7 @@ use std::sync::OnceLock; use jolt_dory::{DoryCommitment, DoryProof, DoryScheme, DoryVerifierSetup}; -use jolt_field::{Fr, FieldCore}; +use jolt_field::{Fr, Field}; use jolt_openings::CommitmentScheme; use jolt_poly::Polynomial; use jolt_transcript::Blake2bTranscript; diff --git a/crates/jolt-dory/src/scheme.rs b/crates/jolt-dory/src/scheme.rs index f62584e6fd..8e5542c032 100644 --- a/crates/jolt-dory/src/scheme.rs +++ b/crates/jolt-dory/src/scheme.rs @@ -557,7 +557,7 @@ mod tests { use super::*; use jolt_crypto::{Pedersen, VectorCommitment}; - use jolt_field::{FieldCore, FromPrimitiveInt}; + use jolt_field::{Field, Ring}; use jolt_poly::Polynomial; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; @@ -572,7 +572,7 @@ mod tests { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); @@ -624,10 +624,7 @@ mod tests { let combined = DoryScheme::combine( &[commit_a, commit_b], - &[ - ::from_u64(1), - ::from_u64(1), - ], + &[::from_u64(1), ::from_u64(1)], ); assert_eq!( @@ -646,7 +643,7 @@ mod tests { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); @@ -689,11 +686,11 @@ mod tests { ); let values = vec![ - ::from_u64(1), - ::from_u64(2), - ::from_u64(3), + ::from_u64(1), + ::from_u64(2), + ::from_u64(3), ]; - let blinding = ::from_u64(42); + let blinding = ::from_u64(42); let commitment = as VectorCommitment>::commit(&vc_setup, &values, &blinding); assert!( as VectorCommitment>::verify( diff --git a/crates/jolt-dory/src/streaming.rs b/crates/jolt-dory/src/streaming.rs index 70d9d89cc9..6a08ebb898 100644 --- a/crates/jolt-dory/src/streaming.rs +++ b/crates/jolt-dory/src/streaming.rs @@ -352,8 +352,8 @@ fn scalar_affine_bases<'a>( mod tests { #![expect(clippy::unwrap_used, reason = "tests unwrap successful PCS operations")] - use jolt_field::FieldCore; - use jolt_field::FromPrimitiveInt; + use jolt_field::Field; + use jolt_field::Ring; use jolt_openings::{ CommitmentScheme, StreamingCommitment, ZkOpeningScheme, ZkStreamingCommitment, }; @@ -376,7 +376,7 @@ mod tests { let prover_setup = DoryScheme::setup_prover(num_vars); let evals: Vec = (0..num_rows * num_cols) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let poly = jolt_poly::Polynomial::new(evals.clone()); @@ -394,7 +394,7 @@ mod tests { ); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"stream-open"); @@ -451,7 +451,7 @@ mod tests { ); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"u64-stream-open"); @@ -516,7 +516,7 @@ mod tests { let mut rng = ChaCha20Rng::seed_from_u64(313); let point = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect::>(); let eval = Fr::from_u64(0); let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"zero-zk-open"); @@ -623,7 +623,7 @@ mod tests { let mut rng = ChaCha20Rng::seed_from_u64(317); let point = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect::>(); let eval = poly.evaluate(&point); let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"one-hot-zk-open"); @@ -686,7 +686,7 @@ mod tests { ); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"i128-stream-open"); diff --git a/crates/jolt-dory/src/types.rs b/crates/jolt-dory/src/types.rs index 50f2027556..dc7c1dfa8e 100644 --- a/crates/jolt-dory/src/types.rs +++ b/crates/jolt-dory/src/types.rs @@ -52,7 +52,7 @@ impl AppendToTranscript for DoryCommitment { } } -impl HomomorphicCommitment for DoryCommitment { +impl HomomorphicCommitment for DoryCommitment { #[inline] fn add(c1: &Self, c2: &Self) -> Self { Self(>::add(&c1.0, &c2.0)) @@ -174,7 +174,7 @@ fn validate_proof_round_count(buf: &[u8]) -> Result<(), String> { )] mod tests { use super::*; - use jolt_field::FieldCore; + use jolt_field::Field; use jolt_openings::CommitmentScheme; use jolt_poly::Polynomial; use jolt_transcript::Transcript; @@ -213,7 +213,7 @@ mod tests { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = @@ -254,7 +254,7 @@ mod tests { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); @@ -290,7 +290,7 @@ mod tests { let prover_setup = crate::DoryScheme::setup_prover(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); diff --git a/crates/jolt-dory/tests/commit_open_verify.rs b/crates/jolt-dory/tests/commit_open_verify.rs index 5e4b768b8d..41771c0651 100644 --- a/crates/jolt-dory/tests/commit_open_verify.rs +++ b/crates/jolt-dory/tests/commit_open_verify.rs @@ -11,7 +11,7 @@ use dory::backends::arkworks::ArkG1; use jolt_dory::DoryScheme; -use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{Field, Fr, Ring}; use jolt_openings::{ AdditivelyHomomorphic, CommitmentScheme, StreamingCommitment, ZkOpeningScheme, }; @@ -26,7 +26,7 @@ fn round_trip>(num_vars: usize, seed: u64, label: let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); @@ -130,7 +130,7 @@ fn streaming_zk_commitment_is_blinded_and_verifies() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); @@ -170,7 +170,7 @@ fn wrong_eval_rejected() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); @@ -200,7 +200,7 @@ fn wrong_point_rejected() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); @@ -235,8 +235,8 @@ fn combine_linear_combination() { let (commit_a, _) = DoryScheme::commit(poly_a.evaluations(), &prover_setup).unwrap(); let (commit_b, _) = DoryScheme::commit(poly_b.evaluations(), &prover_setup).unwrap(); - let c1 = ::random(&mut rng); - let c2 = ::random(&mut rng); + let c1 = ::random(&mut rng); + let c2 = ::random(&mut rng); let combined = DoryScheme::combine(&[commit_a, commit_b], &[c1, c2]); @@ -294,7 +294,7 @@ fn wrong_commitment_rejected() { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); @@ -329,7 +329,7 @@ fn wrong_transcript_domain_rejected() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); @@ -356,7 +356,7 @@ fn zk_round_trip>(num_vars: usize, seed: u64, labe let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = @@ -396,7 +396,7 @@ fn transparent_verify_rejects_zk_opening_proof() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = @@ -424,7 +424,7 @@ fn zk_wrong_commitment_rejected() { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = @@ -455,7 +455,7 @@ fn transparent_commitment_rejected_for_zk_blinded_proof() { let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (transparent_commitment, _) = @@ -496,8 +496,8 @@ fn zk_combined_commitment_and_hint_verify() { let (commit_b, hint_b) = ::commit_zk(poly_b.evaluations(), &prover_setup).unwrap(); - let c1 = ::random(&mut rng); - let c2 = ::random(&mut rng); + let c1 = ::random(&mut rng); + let c2 = ::random(&mut rng); let combined_commitment = DoryScheme::combine(&[commit_a, commit_b], &[c1, c2]); let combined_hint = DoryScheme::combine_hints(vec![hint_a, hint_b], &[c1, c2]); @@ -509,7 +509,7 @@ fn zk_combined_commitment_and_hint_verify() { .collect(); let weighted_poly = Polynomial::new(weighted_evals); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = weighted_poly.evaluate(&point); @@ -545,7 +545,7 @@ fn wrong_eval_commitment_rejected_zk() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = @@ -574,7 +574,7 @@ fn zk_wrong_transcript_domain_rejected() { let verifier_setup = DoryScheme::setup_verifier(num_vars); let poly = Polynomial::::random(num_vars, &mut rng); let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); let (commitment, hint) = @@ -619,8 +619,8 @@ fn ragged_hint_combination_verifies() { } let (narrow_commit, narrow_hint) = DoryScheme::finish_with_hint(partial, &prover_setup); - let c1 = ::random(&mut rng); - let c2 = ::random(&mut rng); + let c1 = ::random(&mut rng); + let c2 = ::random(&mut rng); let combined_commitment = DoryScheme::combine(&[wide_commit, narrow_commit], &[c1, c2]); let combined_hint = DoryScheme::combine_hints(vec![wide_hint, narrow_hint], &[c1, c2]); @@ -635,7 +635,7 @@ fn ragged_hint_combination_verifies() { .collect::>(), ); let point: Vec = (0..wide_vars) - .map(|_| ::random(&mut rng)) + .map(|_| ::random(&mut rng)) .collect(); let eval = joint.evaluate(&point); diff --git a/crates/jolt-field/src/algebra.rs b/crates/jolt-field/src/algebra.rs index 3c602ef5c5..129c45eb64 100644 --- a/crates/jolt-field/src/algebra.rs +++ b/crates/jolt-field/src/algebra.rs @@ -11,7 +11,6 @@ use num_traits::{One, Zero}; use rand_core::RngCore; -use serde::{de::DeserializeOwned, Serialize}; use std::fmt::{Debug, Display}; use std::hash::Hash; use std::iter::{Product, Sum}; @@ -420,16 +419,15 @@ impl Accumulator for NaiveAccumulator { } /// Everything Jolt's protocol stack requires of a scalar field: field -/// algebra, a canonical transcript encoding, an accumulator, and a serde -/// wire format. +/// algebra, a canonical transcript encoding, and an accumulator. /// /// Blanket-implemented — implement the component traits and this follows. -pub trait JoltField: - Field + CanonicalEncoding + WithAccumulator + Serialize + DeserializeOwned -{ -} - -impl JoltField - for T -{ -} +/// +/// The bundle deliberately does NOT require `Serialize + DeserializeOwned` +/// while the temporary `akita` bootstrap edge exists: the pre-cutover +/// `akita-field` type is foreign and cannot be given serde impls here. +/// Restore the serde bounds when the akita cutover removes that edge; every +/// first-party field type already implements them (`impl_serde_bytes!`). +pub trait JoltField: Field + CanonicalEncoding + WithAccumulator {} + +impl JoltField for T {} diff --git a/crates/jolt-field/src/bn254/mod.rs b/crates/jolt-field/src/bn254/mod.rs index 7557e1b0de..e1ff53e57c 100644 --- a/crates/jolt-field/src/bn254/mod.rs +++ b/crates/jolt-field/src/bn254/mod.rs @@ -14,6 +14,17 @@ use crate::{CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, Wi use ark_ff::{BigInteger, PrimeField, UniformRand}; use rand_core::RngCore; +macro_rules! from_primitives { + ($ty:ident: $via:ident[$($prim:ty),*]) => { + $(impl From<$prim> for $ty { + #[inline(always)] + fn from(v: $prim) -> Self { + <$ty as Ring>::$via(v as _) + } + })* + }; +} + /// Stamps a BN254 field wrapper: operators, conversions, serde (canonical /// 32-byte LE), ark-serialize interop, and the canonical-encoding surface. macro_rules! wrap_bn254 { @@ -45,6 +56,11 @@ macro_rules! wrap_bn254 { } } + // Primitive-integer From conversions (reducing), matching the surface + // the plain arkworks types exposed to consumers. + from_primitives!($ty: from_u128[bool, u8, u16, u32, u64, u128]); + from_primitives!($ty: from_i128[i8, i16, i32, i64, i128]); + impl std::fmt::Debug for $ty { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(&self.0, f) diff --git a/crates/jolt-field/src/lib.rs b/crates/jolt-field/src/lib.rs index cbe61ae81c..4f2dc2f81b 100644 --- a/crates/jolt-field/src/lib.rs +++ b/crates/jolt-field/src/lib.rs @@ -6,8 +6,9 @@ //! and [`WithAccumulator`] (deferred-reduction fused multiply-add). //! [`JoltField`] is the blanket-implemented bundle of everything Jolt's //! protocol stack requires of a scalar field: `Field + CanonicalEncoding + -//! WithAccumulator + Serialize + DeserializeOwned`. Because the impl is a -//! blanket, no field type can forget to opt in. +//! WithAccumulator`. Because the impl is a blanket, no field type can forget +//! to opt in. (The serde bounds are deliberately absent while the temporary +//! `akita` bootstrap edge exists; see [`JoltField`].) //! //! # Architecture: contracts and backends //! diff --git a/crates/jolt-field/tests/solinas_fp128_differential.rs b/crates/jolt-field/tests/solinas_fp128_differential.rs index 721246e04a..d86dbd4d5e 100644 --- a/crates/jolt-field/tests/solinas_fp128_differential.rs +++ b/crates/jolt-field/tests/solinas_fp128_differential.rs @@ -11,7 +11,10 @@ use jolt_field as two; use rand::{Rng, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; -use two::{Accumulator as _, CanonicalBytes, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, Ring}; +use two::{ + Accumulator as _, CanonicalBytes, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, + Ring, +}; fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0xf128_a5a5) diff --git a/crates/jolt-field/tests/solinas_words_differential.rs b/crates/jolt-field/tests/solinas_words_differential.rs index 9cbc4a4d09..5dfaebf39b 100644 --- a/crates/jolt-field/tests/solinas_words_differential.rs +++ b/crates/jolt-field/tests/solinas_words_differential.rs @@ -11,7 +11,10 @@ use jolt_field as two; use num_bigint::BigUint; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use two::{Accumulator as _, CanonicalBytes, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, Ring}; +use two::{ + Accumulator as _, CanonicalBytes, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, + Ring, +}; fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0x5011_a5a5) diff --git a/crates/jolt-hyperkzg/benches/hyperkzg.rs b/crates/jolt-hyperkzg/benches/hyperkzg.rs index ddc041a06d..6c2317c1fa 100644 --- a/crates/jolt-hyperkzg/benches/hyperkzg.rs +++ b/crates/jolt-hyperkzg/benches/hyperkzg.rs @@ -6,7 +6,7 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use jolt_crypto::Bn254; -use jolt_field::{FieldCore, Fr}; +use jolt_field::{Field, Fr}; use jolt_hyperkzg::{HyperKZGProverSetup, HyperKZGScheme, HyperKZGVerifierSetup}; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme}; use jolt_poly::Polynomial; diff --git a/crates/jolt-hyperkzg/fuzz/fuzz_targets/commit_open_verify.rs b/crates/jolt-hyperkzg/fuzz/fuzz_targets/commit_open_verify.rs index 51c8079b54..32d943d971 100644 --- a/crates/jolt-hyperkzg/fuzz/fuzz_targets/commit_open_verify.rs +++ b/crates/jolt-hyperkzg/fuzz/fuzz_targets/commit_open_verify.rs @@ -3,7 +3,7 @@ //! Fuzz: random polynomial + random point must always commit-open-verify successfully. use jolt_crypto::Bn254; -use jolt_field::{Field, Fr}; +use jolt_field::{JoltField, Fr}; use jolt_hyperkzg::HyperKZGScheme; use jolt_openings::CommitmentScheme; use jolt_poly::Polynomial; diff --git a/crates/jolt-hyperkzg/fuzz/fuzz_targets/tampered_proof.rs b/crates/jolt-hyperkzg/fuzz/fuzz_targets/tampered_proof.rs index 1363e2487c..811fbf5b55 100644 --- a/crates/jolt-hyperkzg/fuzz/fuzz_targets/tampered_proof.rs +++ b/crates/jolt-hyperkzg/fuzz/fuzz_targets/tampered_proof.rs @@ -6,7 +6,7 @@ //! an intermediate commitment, or a witness commitment using fuzzer-chosen bytes. use jolt_crypto::{Bn254, JoltGroup}; -use jolt_field::{Field, Fr}; +use jolt_field::{JoltField, Fr}; use jolt_hyperkzg::HyperKZGScheme; use jolt_openings::CommitmentScheme; use jolt_poly::Polynomial; diff --git a/crates/jolt-hyperkzg/fuzz/fuzz_targets/wrong_eval.rs b/crates/jolt-hyperkzg/fuzz/fuzz_targets/wrong_eval.rs index 4c15d1af2f..70e2b62435 100644 --- a/crates/jolt-hyperkzg/fuzz/fuzz_targets/wrong_eval.rs +++ b/crates/jolt-hyperkzg/fuzz/fuzz_targets/wrong_eval.rs @@ -6,7 +6,7 @@ //! verifier checks against a fuzzer-derived wrong evaluation. Must reject. use jolt_crypto::Bn254; -use jolt_field::{Field, Fr}; +use jolt_field::{JoltField, Fr}; use jolt_hyperkzg::HyperKZGScheme; use jolt_openings::CommitmentScheme; use jolt_poly::Polynomial; diff --git a/crates/jolt-hyperkzg/src/kzg.rs b/crates/jolt-hyperkzg/src/kzg.rs index 00dcff0ea1..f4ee6005d5 100644 --- a/crates/jolt-hyperkzg/src/kzg.rs +++ b/crates/jolt-hyperkzg/src/kzg.rs @@ -4,7 +4,7 @@ //! All operations are generic over `P: PairingGroup`. use jolt_crypto::{JoltGroup, PairingGroup}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_transcript::{AppendToTranscript, Transcript}; use num_traits::{One, Zero}; @@ -30,7 +30,7 @@ pub(crate) fn kzg_commit( /// Uses Horner's method in reverse: `h[i-1] = f[i] + h[i] * u`. /// The remainder is `f(u)`, but we don't need it since the verifier /// can derive it from the evaluation vectors. -pub(crate) fn compute_witness_polynomial(f: &[F], u: F) -> Vec { +pub(crate) fn compute_witness_polynomial(f: &[F], u: F) -> Vec { let d = f.len(); if d <= 1 { return vec![]; @@ -46,7 +46,7 @@ pub(crate) fn compute_witness_polynomial(f: &[F], u: F) -> Vec { /// Evaluates a polynomial (in evaluation/coefficient form) at a point. /// /// Standard Horner evaluation: `f(u) = f[0] + f[1]*u + f[2]*u^2 + ...` -pub(crate) fn eval_univariate(coeffs: &[F], u: F) -> F { +pub(crate) fn eval_univariate(coeffs: &[F], u: F) -> F { let mut result = F::zero(); let mut power = F::one(); for &c in coeffs { @@ -202,7 +202,7 @@ where } /// Computes `[1, c, c^2, ..., c^{n-1}]`. -pub(crate) fn challenge_powers(c: F, n: usize) -> Vec { +pub(crate) fn challenge_powers(c: F, n: usize) -> Vec { let mut powers = Vec::with_capacity(n); let mut cur = F::one(); for _ in 0..n { @@ -215,7 +215,7 @@ pub(crate) fn challenge_powers(c: F, n: usize) -> Vec { #[cfg(test)] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use num_traits::Zero; #[test] diff --git a/crates/jolt-hyperkzg/src/scheme.rs b/crates/jolt-hyperkzg/src/scheme.rs index 5568362246..c01babd8bd 100644 --- a/crates/jolt-hyperkzg/src/scheme.rs +++ b/crates/jolt-hyperkzg/src/scheme.rs @@ -11,7 +11,7 @@ use std::marker::PhantomData; use jolt_crypto::{Commitment, DeriveSetup, JoltGroup, PairingGroup, PedersenSetup}; -use jolt_field::{FieldCore, FromPrimitiveInt}; +use jolt_field::{Field, Ring}; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, OpeningsError}; use jolt_poly::MultilinearPoly; use jolt_transcript::{AppendToTranscript, Transcript}; diff --git a/crates/jolt-hyperkzg/src/types.rs b/crates/jolt-hyperkzg/src/types.rs index d8f28c7d44..c78594412d 100644 --- a/crates/jolt-hyperkzg/src/types.rs +++ b/crates/jolt-hyperkzg/src/types.rs @@ -54,7 +54,7 @@ impl PartialEq for HyperKZGCommitment

{ impl Eq for HyperKZGCommitment

{} -impl HomomorphicCommitment for HyperKZGCommitment

{ +impl HomomorphicCommitment for HyperKZGCommitment

{ #[inline] fn add(c1: &Self, c2: &Self) -> Self { Self { diff --git a/crates/jolt-hyperkzg/tests/commit_open_verify.rs b/crates/jolt-hyperkzg/tests/commit_open_verify.rs index 24d40d55e4..c25b9af308 100644 --- a/crates/jolt-hyperkzg/tests/commit_open_verify.rs +++ b/crates/jolt-hyperkzg/tests/commit_open_verify.rs @@ -7,7 +7,7 @@ )] use jolt_crypto::Bn254; -use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{Field, Fr, Ring}; use jolt_hyperkzg::{HyperKZGProverSetup, HyperKZGScheme, HyperKZGVerifierSetup}; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme}; use jolt_poly::Polynomial; diff --git a/crates/jolt-kernels/src/backend.rs b/crates/jolt-kernels/src/backend.rs index d864eca908..f55c64f9a0 100644 --- a/crates/jolt-kernels/src/backend.rs +++ b/crates/jolt-kernels/src/backend.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use jolt_claims::protocols::jolt::JoltChallengeId; use jolt_claims::{InputClaims, OutputClaims, SumcheckChallenges}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_kernels_derive::KernelSlots; use jolt_openings::CommitmentScheme; use jolt_verifier::stages::relations::{ @@ -86,7 +86,7 @@ use crate::KernelError; /// surfaces the same distant way. pub trait PrepareKernel where - F: Field, + F: JoltField, R: ConcreteSumcheck, SumcheckInputClaims: InputClaims, SumcheckOutputClaims: OutputClaims, @@ -113,7 +113,7 @@ where #[kernel_slots(crate = "crate")] pub struct JoltBackend where - F: Field, + F: JoltField, PCS: CommitmentScheme, { pub commit: Box>, @@ -158,7 +158,7 @@ where impl JoltBackend where - F: Field, + F: JoltField, PCS: CommitmentScheme, { /// Open the proof-scoped session that slot state lives in. One session @@ -260,7 +260,7 @@ mod kernel_slots_derive_tests { // delegation is unrepresentable. #[derive(KernelSlots)] #[kernel_slots(crate = "crate")] - struct ToyRegistry { + struct ToyRegistry { label: String, shift: Box>>, slot_count: usize, diff --git a/crates/jolt-kernels/src/commitment.rs b/crates/jolt-kernels/src/commitment.rs index e5139cbe07..a826ac8ae5 100644 --- a/crates/jolt-kernels/src/commitment.rs +++ b/crates/jolt-kernels/src/commitment.rs @@ -7,7 +7,7 @@ //! when they share row geometry. use jolt_claims::protocols::jolt::{JoltCommittedPolynomial, TracePolynomialOrder}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::CommitmentScheme; use jolt_witness::witnesses::{LookupIndex, MappedPc, RamInc, RdInc, RemappedRamAddress}; use jolt_witness::{JoltWitnessOracle, RowSource, WitnessBundle}; @@ -89,7 +89,7 @@ pub struct WitnessCommitment { /// caller absorbs the returned commitments. pub trait CommitWitness where - F: Field, + F: JoltField, PCS: CommitmentScheme, { fn commit_witness( diff --git a/crates/jolt-kernels/src/committed_program.rs b/crates/jolt-kernels/src/committed_program.rs index 11a0a977ff..8bc58133ed 100644 --- a/crates/jolt-kernels/src/committed_program.rs +++ b/crates/jolt-kernels/src/committed_program.rs @@ -18,7 +18,7 @@ use jolt_claims::protocols::jolt::geometry::claim_reductions::bytecode::{ COMMITTED_BYTECODE_LANE_CAPACITY, }; use jolt_claims::protocols::jolt::TracePolynomialOrder; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_lookup_tables::{InstructionLookupTable, XLEN}; use jolt_riscv::instructions::Noop; use jolt_riscv::{ @@ -40,7 +40,7 @@ const INSTRUCTION_FLAG_ORDER: [InstructionFlags; NUM_INSTRUCTION_FLAGS] = [ ]; /// The sparse `(lane, value)` encoding of one committed bytecode row. -fn for_each_active_lane_value( +fn for_each_active_lane_value( instruction: &JoltInstructionRow, mut visit: impl FnMut(usize, F), ) { @@ -87,7 +87,7 @@ fn for_each_active_lane_value( /// Build the per-chunk committed bytecode coefficient grids, interleaved by /// the proof's trace order. -pub fn build_committed_bytecode_chunk_coeffs( +pub fn build_committed_bytecode_chunk_coeffs( instructions: &[JoltInstructionRow], chunk_count: usize, order: TracePolynomialOrder, diff --git a/crates/jolt-kernels/src/error.rs b/crates/jolt-kernels/src/error.rs index 46f117cb45..46765b80d3 100644 --- a/crates/jolt-kernels/src/error.rs +++ b/crates/jolt-kernels/src/error.rs @@ -1,14 +1,14 @@ use crate::SumcheckKernelError; use jolt_claims::protocols::jolt::{JoltChallengeId, JoltDerivedId, JoltOpeningId}; use jolt_claims::MissingOpeningValue; -use jolt_field::FieldCore; +use jolt_field::Field; use jolt_sumcheck::SumcheckError; use jolt_verifier::VerifierError; use jolt_witness::WitnessError; use thiserror::Error; #[derive(Debug, Error)] -pub enum KernelError { +pub enum KernelError { #[error(transparent)] Witness(#[from] WitnessError), diff --git a/crates/jolt-kernels/src/kernel.rs b/crates/jolt-kernels/src/kernel.rs index b2e3a30a55..a197a702ca 100644 --- a/crates/jolt-kernels/src/kernel.rs +++ b/crates/jolt-kernels/src/kernel.rs @@ -7,7 +7,7 @@ use jolt_claims::protocols::jolt::{JoltChallengeId, JoltDerivedId, JoltOpeningId}; use jolt_claims::{InputClaims, MissingOpeningValue, OutputClaims, SumcheckChallenges}; -use jolt_field::{Field, FieldCore}; +use jolt_field::{Field, JoltField}; use jolt_sumcheck::ProveRounds; use jolt_verifier::stages::relations::{ ConcreteSumcheck, ConcreteSumcheckChallenges, SumcheckInputClaims, SumcheckInputPoints, @@ -23,7 +23,7 @@ use crate::ProofSession; /// [`KernelError`](crate::KernelError), which wraps this one; only the /// failures the *typed extraction seam* can produce live here. #[derive(Debug, thiserror::Error)] -pub enum SumcheckKernelError { +pub enum SumcheckKernelError { /// Relation-level failures (claim wiring, point derivation): kernels run /// the verifier's own relation methods as hard self-checks. #[error(transparent)] @@ -62,7 +62,7 @@ pub enum SumcheckKernelError { /// [`validate_derived_tables`](Self::validate_derived_tables). A kernel that /// does keep a copy (the naive tier clones the driver-supplied instance) /// must treat the threaded-in relation as authoritative. -pub trait SumcheckKernel: ProveRounds +pub trait SumcheckKernel: ProveRounds where SumcheckInputClaims: InputClaims, SumcheckOutputClaims: OutputClaims, @@ -129,7 +129,7 @@ where /// of [`PrepareKernel::prepare`](crate::PrepareKernel::prepare). pub struct ProverInputs<'a, F, R> where - F: Field, + F: JoltField, R: ConcreteSumcheck, SumcheckInputClaims: InputClaims, SumcheckOutputClaims: OutputClaims, diff --git a/crates/jolt-kernels/src/opening.rs b/crates/jolt-kernels/src/opening.rs index 27e48b858c..9a01f7c5de 100644 --- a/crates/jolt-kernels/src/opening.rs +++ b/crates/jolt-kernels/src/opening.rs @@ -24,7 +24,7 @@ use std::collections::BTreeMap; use jolt_claims::protocols::jolt::{JoltAdviceKind, JoltCommittedPolynomial}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::MultilinearPoly; use jolt_witness::JoltWitnessOracle; @@ -36,7 +36,7 @@ use crate::{KernelError, ProofSession}; /// `precommitted_tables` carries the committed-program polynomials (bytecode /// chunks, program image) the recipe materialized from the prover-retained /// full program — they are preprocessing data, not witness oracles. -pub trait JointOpeningPolynomials { +pub trait JointOpeningPolynomials { fn prepare( &self, session: &mut ProofSession, @@ -53,7 +53,7 @@ pub trait JointOpeningPolynomials { /// opening evaluation), so it keeps a hand-shaped trait; the advice /// polynomial's REDUCTION duties are ordinary `PrepareKernel` members /// (`precommitted_reduction`). -pub trait AdviceOpeningEvaluation { +pub trait AdviceOpeningEvaluation { fn evaluate( &self, session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/precommitted_reduction.rs b/crates/jolt-kernels/src/precommitted_reduction.rs index 9cf936f70f..63a8425d95 100644 --- a/crates/jolt-kernels/src/precommitted_reduction.rs +++ b/crates/jolt-kernels/src/precommitted_reduction.rs @@ -39,7 +39,7 @@ use std::marker::PhantomData; use jolt_claims::protocols::jolt::PrecommittedClaimReduction; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::UnivariatePoly; use jolt_sumcheck::{ProveRounds, SumcheckError}; use jolt_verifier::stages::stage6b::committed_reduction_cycle_phase::{ @@ -74,7 +74,7 @@ struct PrecommittedTables { two_inv: F, } -impl PrecommittedTables { +impl PrecommittedTables { /// The round polynomial for member-local state: the constant `claim/2` on /// an inactive round, else the hinted `{0,1,2}` interpolation (see the /// module doc for why the padded claim, not the true sum, feeds `s(1)`). @@ -201,13 +201,13 @@ pub struct PrecommittedReductionCarry { /// The stage-6b cycle-phase batch member for relation `R`: binds the cycle /// window, and — per kind — assembles the typed wire claims (resolving /// intermediate-vs-final from the schedule) and parks the 6b→7 carry. -pub struct CycleReductionKernel { +pub struct CycleReductionKernel { reduction: PrecommittedClaimReduction, tables: PrecommittedTables, _relation: PhantomData R>, } -impl CycleReductionKernel { +impl CycleReductionKernel { /// Build a member from tables ALREADY permuted into Dory opening-round /// order (see [`lsb_permutation`] / [`permute_coefficients`]). pub fn new( @@ -281,7 +281,7 @@ impl CycleReductionKernel { } } -impl ProveRounds for CycleReductionKernel { +impl ProveRounds for CycleReductionKernel { fn num_rounds(&self) -> usize { self.reduction.cycle_phase_total_rounds() } @@ -311,13 +311,13 @@ impl ProveRounds for CycleReductionKernel { /// The stage-7 address-phase batch member for relation `R`: resumes binding /// from the reclaimed 6b carry (running scale included) and — per kind — /// extracts the final openings from the fully bound tables. -pub struct AddressReductionKernel { +pub struct AddressReductionKernel { reduction: PrecommittedClaimReduction, tables: PrecommittedTables, _relation: PhantomData R>, } -impl AddressReductionKernel { +impl AddressReductionKernel { pub fn new(carry: PrecommittedReductionCarry) -> Self { Self { reduction: carry.reduction, @@ -327,7 +327,7 @@ impl AddressReductionKernel { } } -impl ProveRounds for AddressReductionKernel { +impl ProveRounds for AddressReductionKernel { fn num_rounds(&self) -> usize { self.reduction.address_phase_total_rounds() } @@ -354,7 +354,7 @@ impl ProveRounds for AddressReductionKernel { } } -impl SumcheckKernel for CycleReductionKernel> { +impl SumcheckKernel for CycleReductionKernel> { type Relation = TrustedAdviceCyclePhase; fn output_claims( @@ -371,7 +371,7 @@ impl SumcheckKernel for CycleReductionKernel SumcheckKernel for CycleReductionKernel> { +impl SumcheckKernel for CycleReductionKernel> { type Relation = UntrustedAdviceCyclePhase; fn output_claims( @@ -388,7 +388,7 @@ impl SumcheckKernel for CycleReductionKernel SumcheckKernel for CycleReductionKernel> { +impl SumcheckKernel for CycleReductionKernel> { type Relation = BytecodeReductionCyclePhase; fn output_claims( @@ -416,7 +416,9 @@ impl SumcheckKernel for CycleReductionKernel SumcheckKernel for CycleReductionKernel> { +impl SumcheckKernel + for CycleReductionKernel> +{ type Relation = ProgramImageReductionCyclePhase; fn output_claims( @@ -433,7 +435,7 @@ impl SumcheckKernel for CycleReductionKernel SumcheckKernel for AddressReductionKernel> { +impl SumcheckKernel for AddressReductionKernel> { type Relation = TrustedAdviceAddressPhase; fn output_claims( @@ -446,7 +448,7 @@ impl SumcheckKernel for AddressReductionKernel SumcheckKernel for AddressReductionKernel> { +impl SumcheckKernel for AddressReductionKernel> { type Relation = UntrustedAdviceAddressPhase; fn output_claims( @@ -459,7 +461,9 @@ impl SumcheckKernel for AddressReductionKernel SumcheckKernel for AddressReductionKernel> { +impl SumcheckKernel + for AddressReductionKernel> +{ type Relation = BytecodeReductionAddressPhase; fn output_claims( @@ -472,7 +476,7 @@ impl SumcheckKernel for AddressReductionKernel SumcheckKernel +impl SumcheckKernel for AddressReductionKernel> { type Relation = ProgramImageReductionAddressPhase; diff --git a/crates/jolt-kernels/src/reference/advice_claim_reduction.rs b/crates/jolt-kernels/src/reference/advice_claim_reduction.rs index 7c9f5edde5..9fd43cf213 100644 --- a/crates/jolt-kernels/src/reference/advice_claim_reduction.rs +++ b/crates/jolt-kernels/src/reference/advice_claim_reduction.rs @@ -14,7 +14,7 @@ use jolt_claims::protocols::jolt::geometry::claim_reductions::advice::ram_val_ch use jolt_claims::protocols::jolt::{ AdviceClaimReductionLayout, JoltAdviceKind, PrecommittedReductionLayout, }; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_witness::JoltWitnessOracle; use crate::ProverInputs; @@ -30,7 +30,7 @@ use crate::precommitted_reduction::{ }; use crate::{KernelError, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel}; -impl AdviceOpeningEvaluation for ReferenceBackend { +impl AdviceOpeningEvaluation for ReferenceBackend { fn evaluate( &self, _session: &mut ProofSession, @@ -48,7 +48,7 @@ impl AdviceOpeningEvaluation for ReferenceBackend { } } -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, @@ -73,7 +73,7 @@ impl PrepareKernel> for ReferenceBackend } } -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, @@ -104,7 +104,7 @@ impl PrepareKernel> for ReferenceBacke /// The advice reduction's cycle-phase kernel: the advice polynomial as the /// value table and the eq table of the staged RAM value-check point, both /// permuted into Dory opening-round order. -fn advice_reduction_kernel( +fn advice_reduction_kernel( kind: JoltAdviceKind, layout: &AdviceClaimReductionLayout, r_val: &[F], @@ -137,7 +137,7 @@ fn advice_reduction_kernel( CycleReductionKernel::new(reduction, value, eq, Vec::new()) } -fn advice_table( +fn advice_table( witness: &dyn JoltWitnessOracle, kind: JoltAdviceKind, expected_vars: usize, diff --git a/crates/jolt-kernels/src/reference/booleanity.rs b/crates/jolt-kernels/src/reference/booleanity.rs index 157fbf102b..167ebf303e 100644 --- a/crates/jolt-kernels/src/reference/booleanity.rs +++ b/crates/jolt-kernels/src/reference/booleanity.rs @@ -25,7 +25,7 @@ use std::collections::BTreeMap; use crate::ProverInputs; use jolt_claims::protocols::jolt::geometry::booleanity::BooleanityDimensions; use jolt_claims::protocols::jolt::{BooleanityPublic, JoltDerivedId, JoltRelationId}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{try_eq_mle, BindingOrder, Polynomial, UnivariatePoly}; use jolt_sumcheck::{ProveRounds, SumcheckError}; use jolt_verifier::stages::relations::{ConcreteSumcheck, SumcheckInputClaims}; @@ -41,7 +41,7 @@ use crate::{ SumcheckKernel, SumcheckKernelError, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, @@ -62,7 +62,7 @@ impl PrepareKernel> for ReferenceBackend } } -pub struct BooleanityAddressKernel { +pub struct BooleanityAddressKernel { rounds: usize, /// Per checked polynomial, its `γ^{2i}` batching weight, in the layout's /// canonical order. @@ -76,7 +76,7 @@ pub struct BooleanityAddressKernel { rounds_bound: usize, } -impl BooleanityAddressKernel { +impl BooleanityAddressKernel { pub fn new( relation: &BooleanityAddressPhase, dimensions: BooleanityDimensions, @@ -144,7 +144,7 @@ impl BooleanityAddressKernel { } } -impl BooleanityAddressKernel { +impl BooleanityAddressKernel { fn bind(&mut self, challenge: F) { let one_minus_sqr = (F::one() - challenge) * (F::one() - challenge); let challenge_sqr = challenge * challenge; @@ -164,7 +164,7 @@ impl BooleanityAddressKernel { } } -impl ProveRounds for BooleanityAddressKernel { +impl ProveRounds for BooleanityAddressKernel { fn num_rounds(&self) -> usize { self.rounds } @@ -225,7 +225,7 @@ impl ProveRounds for BooleanityAddressKernel { } } -impl SumcheckKernel for BooleanityAddressKernel { +impl SumcheckKernel for BooleanityAddressKernel { type Relation = BooleanityAddressPhase; fn output_claims( @@ -252,7 +252,7 @@ impl SumcheckKernel for BooleanityAddressKernel { } } -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/bytecode_claim_reduction.rs b/crates/jolt-kernels/src/reference/bytecode_claim_reduction.rs index f3dd58ce11..7bd39e696e 100644 --- a/crates/jolt-kernels/src/reference/bytecode_claim_reduction.rs +++ b/crates/jolt-kernels/src/reference/bytecode_claim_reduction.rs @@ -16,7 +16,7 @@ use jolt_claims::protocols::jolt::{ BytecodeClaimReductionLayout, JoltCommittedPolynomial, PrecommittedReductionLayout, }; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_riscv::JoltInstructionRow; use jolt_verifier::stages::stage6b::outputs::BytecodeReductionWeights; @@ -29,7 +29,7 @@ use crate::committed_program::{build_committed_bytecode_chunk_coeffs, chunk_inde use crate::precommitted_reduction::{permute_tables, CycleReductionKernel}; use crate::{KernelError, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel}; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, @@ -49,7 +49,7 @@ impl PrepareKernel> for ReferenceBac /// The committed-bytecode reduction's cycle-phase kernel — see the module doc /// for the value/eq/aux table construction. -fn bytecode_reduction_kernel( +fn bytecode_reduction_kernel( layout: &BytecodeClaimReductionLayout, weights: &BytecodeReductionWeights, bytecode: &[JoltInstructionRow], diff --git a/crates/jolt-kernels/src/reference/bytecode_read_raf.rs b/crates/jolt-kernels/src/reference/bytecode_read_raf.rs index 33ca8c955a..2b5ac54746 100644 --- a/crates/jolt-kernels/src/reference/bytecode_read_raf.rs +++ b/crates/jolt-kernels/src/reference/bytecode_read_raf.rs @@ -34,7 +34,7 @@ use jolt_claims::protocols::jolt::geometry::dimensions::{ }; use jolt_claims::protocols::jolt::relations::bytecode::BytecodeReadRafAddressPhaseChallenges; use jolt_claims::protocols::jolt::{BytecodeReadRafPublic, JoltDerivedId}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{ BindingOrder, IdentityPolynomial, MultilinearEvaluation, Polynomial, UnivariatePoly, }; @@ -60,7 +60,7 @@ pub struct BytecodeReadRafWitness { pub bytecode_pc: BytecodePc, } -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, @@ -102,7 +102,7 @@ impl PrepareKernel> for ReferenceBac } } -pub struct BytecodeReadRafAddressKernel { +pub struct BytecodeReadRafAddressKernel { rounds: usize, /// Committed-program mode stages the five raw bound `Val_s` wire claims. committed_program: bool, @@ -123,7 +123,7 @@ pub struct BytecodeReadRafAddressKernel { rounds_bound: usize, } -impl BytecodeReadRafAddressKernel { +impl BytecodeReadRafAddressKernel { pub fn new( relation: &BytecodeReadRafAddressPhase, dimensions: BytecodeReadRafDimensions, @@ -215,7 +215,7 @@ impl BytecodeReadRafAddressKernel { } } -impl BytecodeReadRafAddressKernel { +impl BytecodeReadRafAddressKernel { fn bind(&mut self, challenge: F) { for table in self.pushforwards.iter_mut().chain(self.values.iter_mut()) { table.bind_with_order(challenge, BindingOrder::LowToHigh); @@ -230,7 +230,7 @@ impl BytecodeReadRafAddressKernel { } } -impl ProveRounds for BytecodeReadRafAddressKernel { +impl ProveRounds for BytecodeReadRafAddressKernel { fn num_rounds(&self) -> usize { self.rounds } @@ -281,7 +281,7 @@ impl ProveRounds for BytecodeReadRafAddressKernel { } } -impl SumcheckKernel for BytecodeReadRafAddressKernel { +impl SumcheckKernel for BytecodeReadRafAddressKernel { type Relation = BytecodeReadRafAddressPhase; fn output_claims( @@ -314,7 +314,7 @@ impl SumcheckKernel for BytecodeReadRafAddressKernel { } } -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/commitment.rs b/crates/jolt-kernels/src/reference/commitment.rs index 9d0ce2a1d1..e39607ee15 100644 --- a/crates/jolt-kernels/src/reference/commitment.rs +++ b/crates/jolt-kernels/src/reference/commitment.rs @@ -23,7 +23,7 @@ use jolt_claims::protocols::jolt::{ JoltCommittedPolynomial, JoltPolynomialId, TracePolynomialOrder, }; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::{CommitmentScheme, StreamingCommitment}; use jolt_witness::witnesses::RaChunkSelector; use jolt_witness::{stream_witnesses, JoltWitnessOracle, RowSource, StreamConsumer}; @@ -35,7 +35,7 @@ use crate::{KernelError, ProofSession, ReferenceBackend}; impl CommitWitness for ReferenceBackend where - F: Field, + F: JoltField, PCS: CommitmentScheme + StreamingCommitment, { fn commit_witness( @@ -160,7 +160,7 @@ impl ColumnKind { /// Resolve `ids` to column derivations. Family sizes come from the ids /// themselves (the committed order carries whole families); the chunk width /// is the grid's. -fn column_kinds( +fn column_kinds( ids: &[JoltCommittedPolynomial], grid: CommitmentGrid, ) -> Result, KernelError> { @@ -198,7 +198,7 @@ fn column_kinds( /// The fused cycle-major commit consumer: every column's in-progress /// commitment, advanced per row window. -struct FusedColumns<'a, F: Field, PCS: CommitmentScheme + StreamingCommitment> { +struct FusedColumns<'a, F: JoltField, PCS: CommitmentScheme + StreamingCommitment> { columns: Vec>, one_hot_k: usize, setup: &'a PCS::ProverSetup, @@ -223,7 +223,7 @@ enum ColumnCommitState { }, } -impl<'a, F: Field, PCS: CommitmentScheme + StreamingCommitment> +impl<'a, F: JoltField, PCS: CommitmentScheme + StreamingCommitment> FusedColumns<'a, F, PCS> { fn begin( @@ -274,7 +274,7 @@ impl<'a, F: Field, PCS: CommitmentScheme + StreamingCommitment> } } -impl + StreamingCommitment> StreamConsumer +impl + StreamingCommitment> StreamConsumer for FusedColumns<'_, F, PCS> { type Witness = CommittedColumnsWitness; @@ -320,7 +320,7 @@ struct MaterializedColumn { flat_cycles: Option, } -impl MaterializedColumn { +impl MaterializedColumn { fn begin(kind: ColumnKind, grid: CommitmentGrid) -> Self { // Widened cycle-major grids materialize one-hots as the flat (K × T) // matrix and dense columns in the plain cycle-major layout; @@ -356,7 +356,7 @@ impl MaterializedColumn { } } -impl StreamConsumer for MaterializedColumn { +impl StreamConsumer for MaterializedColumn { type Witness = CommittedColumnsWitness; fn consume(&mut self, chunk: &[CommittedColumnsWitness]) { diff --git a/crates/jolt-kernels/src/reference/hamming_weight_claim_reduction.rs b/crates/jolt-kernels/src/reference/hamming_weight_claim_reduction.rs index ec99fdb1e3..c30d26ec13 100644 --- a/crates/jolt-kernels/src/reference/hamming_weight_claim_reduction.rs +++ b/crates/jolt-kernels/src/reference/hamming_weight_claim_reduction.rs @@ -17,7 +17,7 @@ use crate::ProverInputs; use jolt_claims::protocols::jolt::{ HammingWeightClaimReductionPublic, JoltDerivedId, JoltRelationId, }; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::stage7::hamming_weight_claim_reduction::HammingWeightClaimReduction; use jolt_witness::JoltWitnessPlane; @@ -27,7 +27,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/inc_claim_reduction.rs b/crates/jolt-kernels/src/reference/inc_claim_reduction.rs index 53ad23c860..b66f772ed1 100644 --- a/crates/jolt-kernels/src/reference/inc_claim_reduction.rs +++ b/crates/jolt-kernels/src/reference/inc_claim_reduction.rs @@ -15,7 +15,7 @@ use jolt_claims::protocols::jolt::geometry::claim_reductions::increments::{ ram_inc_reduced, rd_inc_reduced, }; use jolt_claims::protocols::jolt::{IncClaimReductionPublic, JoltDerivedId}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::relations::ConcreteSumcheck; use jolt_verifier::stages::stage6b::inc_claim_reduction::IncClaimReduction; @@ -26,7 +26,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/instruction_claim_reduction.rs b/crates/jolt-kernels/src/reference/instruction_claim_reduction.rs index e1292bf399..1c1a425150 100644 --- a/crates/jolt-kernels/src/reference/instruction_claim_reduction.rs +++ b/crates/jolt-kernels/src/reference/instruction_claim_reduction.rs @@ -16,7 +16,7 @@ use jolt_claims::protocols::jolt::geometry::claim_reductions::instruction::{ right_instruction_input_reduced, right_lookup_operand_reduced, }; use jolt_claims::protocols::jolt::{InstructionClaimReductionPublic, JoltDerivedId}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::stage2::instruction_claim_reduction::InstructionClaimReduction; use jolt_witness::JoltWitnessPlane; @@ -26,7 +26,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/instruction_input.rs b/crates/jolt-kernels/src/reference/instruction_input.rs index e9fe8479d1..a047a9c22c 100644 --- a/crates/jolt-kernels/src/reference/instruction_input.rs +++ b/crates/jolt-kernels/src/reference/instruction_input.rs @@ -15,7 +15,7 @@ use jolt_claims::protocols::jolt::geometry::instruction::{ rs1_value, rs2_value, unexpanded_pc, }; use jolt_claims::protocols::jolt::{InstructionInputPublic, JoltDerivedId}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::stage3::outputs::InstructionInput; use jolt_witness::JoltWitnessPlane; @@ -25,7 +25,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/instruction_ra_virtualization.rs b/crates/jolt-kernels/src/reference/instruction_ra_virtualization.rs index 6e3594adac..30953bd7e1 100644 --- a/crates/jolt-kernels/src/reference/instruction_ra_virtualization.rs +++ b/crates/jolt-kernels/src/reference/instruction_ra_virtualization.rs @@ -12,7 +12,7 @@ use crate::ProverInputs; use jolt_claims::protocols::jolt::geometry::dimensions::committed_address_chunks; use jolt_claims::protocols::jolt::geometry::instruction::committed_instruction_ra; use jolt_claims::protocols::jolt::{InstructionRaVirtualizationPublic, JoltDerivedId}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::stage6b::instruction_ra_virtualization::InstructionRaVirtualization; use jolt_witness::JoltWitnessPlane; @@ -22,7 +22,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/instruction_read_raf.rs b/crates/jolt-kernels/src/reference/instruction_read_raf.rs index 5328102b61..2c20fa143f 100644 --- a/crates/jolt-kernels/src/reference/instruction_read_raf.rs +++ b/crates/jolt-kernels/src/reference/instruction_read_raf.rs @@ -38,7 +38,7 @@ use jolt_claims::protocols::jolt::geometry::instruction::{ InstructionReadRafDimensions, CANONICAL_INSTRUCTION_ADDRESS, }; use jolt_claims::protocols::jolt::relations::instruction::InstructionReadRafOutputClaims; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_lookup_tables::tables::prefixes::{PrefixEval, ALL_PREFIXES}; use jolt_lookup_tables::tables::suffixes::SuffixEval; use jolt_lookup_tables::{LookupBits, LookupTableKind, XLEN as RISCV_XLEN}; @@ -71,7 +71,7 @@ pub struct InstructionReadRafWitness { const CHUNK_LEN: usize = 8; const CHUNK_SIZE: usize = 1 << CHUNK_LEN; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, @@ -96,14 +96,14 @@ impl PrepareKernel> for ReferenceBackend { /// address identity): `poly(k) = P(chunk) · Q_shift + Q_value` over the /// current phase's chunk domain, with the fully bound `P` becoming the next /// phase's checkpoint. -struct RafDecomposition { +struct RafDecomposition { prefix: Polynomial, q_shift: Polynomial, q_value: Polynomial, checkpoint: F, } -impl RafDecomposition { +impl RafDecomposition { fn empty() -> Self { Self { prefix: Polynomial::new(vec![F::zero()]), @@ -144,7 +144,7 @@ impl RafDecomposition { /// The linear extension of a dense table's current top variable: `evals[b]` /// at 0, `evals[b + half]` at 1, `2·hi − lo` at 2. -fn extension_eval(evals: &[F], b: usize, half: usize, c: usize) -> F { +fn extension_eval(evals: &[F], b: usize, half: usize, c: usize) -> F { let lo = evals[b]; let hi = evals[b + half]; match c { @@ -157,13 +157,13 @@ fn extension_eval(evals: &[F], b: usize, half: usize, c: usize) -> F { /// Cycle-indexed tables for the last `log_T` rounds: `eq(r_reduction, ·)`, /// the combined `Val + γ·RafVal` at the bound address, and the virtual `ra` /// chunk selectors. -struct CycleTables { +struct CycleTables { eq_reduction: Polynomial, combined_val: Polynomial, ra: Vec>, } -pub struct InstructionReadRafKernel { +pub struct InstructionReadRafKernel { dimensions: InstructionReadRafDimensions, gamma: F, r_reduction: Vec, @@ -198,7 +198,7 @@ pub struct InstructionReadRafKernel { rounds_bound: usize, } -impl InstructionReadRafKernel { +impl InstructionReadRafKernel { pub fn new( dimensions: InstructionReadRafDimensions, r_reduction: &[F], @@ -616,7 +616,7 @@ impl InstructionReadRafKernel { } } -impl ProveRounds for InstructionReadRafKernel { +impl ProveRounds for InstructionReadRafKernel { fn num_rounds(&self) -> usize { self.dimensions.sumcheck_rounds() } @@ -651,7 +651,7 @@ impl ProveRounds for InstructionReadRafKernel { } } -impl InstructionReadRafKernel { +impl InstructionReadRafKernel { fn bind(&mut self, challenge: F) -> Result<(), SumcheckError> { if self.rounds_bound < self.address_bits() { for table in &mut self.prefix_tables { @@ -712,7 +712,7 @@ impl InstructionReadRafKernel { } } -impl SumcheckKernel for InstructionReadRafKernel { +impl SumcheckKernel for InstructionReadRafKernel { type Relation = InstructionReadRaf; fn output_claims( diff --git a/crates/jolt-kernels/src/reference/mod.rs b/crates/jolt-kernels/src/reference/mod.rs index b6896633e2..c3200b15b0 100644 --- a/crates/jolt-kernels/src/reference/mod.rs +++ b/crates/jolt-kernels/src/reference/mod.rs @@ -12,7 +12,7 @@ //! fallback partial backends compose over; it is eager-dense throughout — a //! test oracle at harness scale, never a performance path. -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::{CommitmentScheme, StreamingCommitment}; use crate::JoltBackend; @@ -57,7 +57,7 @@ pub struct ReferenceBackend; impl JoltBackend where - F: Field, + F: JoltField, PCS: CommitmentScheme, { /// The always-present reference backend: every slot served by the naive diff --git a/crates/jolt-kernels/src/reference/naive.rs b/crates/jolt-kernels/src/reference/naive.rs index 7c2eed745c..6053152eae 100644 --- a/crates/jolt-kernels/src/reference/naive.rs +++ b/crates/jolt-kernels/src/reference/naive.rs @@ -34,7 +34,7 @@ use std::collections::BTreeMap; use jolt_claims::protocols::jolt::{JoltChallengeId, JoltDerivedId, JoltOpeningId}; use jolt_claims::{InputClaims, OutputClaims, Source, SumcheckChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial, UnivariatePoly}; use jolt_sumcheck::{ProveRounds, SumcheckError}; use jolt_verifier::stages::relations::{ @@ -52,7 +52,7 @@ use crate::{KernelError, ProverInputs, SumcheckKernel, SumcheckKernelError}; /// the round loop cannot miss a leaf. pub struct NaiveSumcheckProver where - F: Field, + F: JoltField, R: ConcreteSumcheck, SumcheckInputClaims: InputClaims, SumcheckOutputClaims: OutputClaims, @@ -75,7 +75,7 @@ where impl NaiveSumcheckProver where - F: Field, + F: JoltField, R: ConcreteSumcheck, SumcheckInputClaims: InputClaims, SumcheckOutputClaims: OutputClaims, @@ -189,7 +189,7 @@ where impl ProveRounds for NaiveSumcheckProver where - F: Field, + F: JoltField, R: ConcreteSumcheck, SumcheckInputClaims: InputClaims, SumcheckOutputClaims: OutputClaims, @@ -278,7 +278,7 @@ where impl SumcheckKernel for NaiveSumcheckProver where - F: Field, + F: JoltField, R: ConcreteSumcheck, SumcheckInputClaims: InputClaims, SumcheckOutputClaims: OutputClaims, @@ -354,7 +354,7 @@ mod tests { use jolt_claims::{ challenge, derived, opening, OutputClaims, SumcheckChallenges, SymbolicSumcheck, }; - use jolt_field::{Field, Fr, FromPrimitiveInt, RingCore}; + use jolt_field::{Fr, JoltField, Ring}; use jolt_poly::{BindingOrder, EqPolynomial, Polynomial}; use jolt_sumcheck::{ append_sumcheck_claim, prove_batch, BatchMember, BatchPrelude, ClearSumcheckRecorder, @@ -435,11 +435,11 @@ mod tests { 3 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(virt(JoltVirtualPolynomial::UnexpandedPC)) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { opening(virt(JoltVirtualPolynomial::LookupOutput)) * opening(virt(JoltVirtualPolynomial::LeftLookupOperand)) * derived(JoltDerivedId::Test) @@ -451,12 +451,12 @@ mod tests { } #[derive(Clone)] - struct ToyRelation { + struct ToyRelation { symbolic: ToySymbolic, reference_point: Vec, } - impl ConcreteSumcheck for ToyRelation { + impl ConcreteSumcheck for ToyRelation { type Symbolic = ToySymbolic; fn symbolic(&self) -> &ToySymbolic { @@ -873,22 +873,22 @@ mod tests { self.0.degree() } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(virt(JoltVirtualPolynomial::LookupOutput)) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { self.0.output_expression::() } } #[derive(Clone)] - struct ToyLeafRelation { + struct ToyLeafRelation { symbolic: ToyLeafSymbolic, reference_point: Vec, } - impl ConcreteSumcheck for ToyLeafRelation { + impl ConcreteSumcheck for ToyLeafRelation { type Symbolic = ToyLeafSymbolic; fn symbolic(&self) -> &ToyLeafSymbolic { diff --git a/crates/jolt-kernels/src/reference/opening.rs b/crates/jolt-kernels/src/reference/opening.rs index 14adfd9857..6f04b19161 100644 --- a/crates/jolt-kernels/src/reference/opening.rs +++ b/crates/jolt-kernels/src/reference/opening.rs @@ -7,7 +7,7 @@ use std::collections::BTreeMap; use jolt_claims::protocols::jolt::geometry::committed_openings::final_opening_id; use jolt_claims::protocols::jolt::{JoltCommittedPolynomial, TracePolynomialOrder}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::MultilinearPoly; use jolt_witness::JoltWitnessOracle; @@ -16,7 +16,7 @@ use crate::commitment::CommitmentGrid; use crate::opening::JointOpeningPolynomials; use crate::{KernelError, ProofSession, ReferenceBackend}; -impl JointOpeningPolynomials for ReferenceBackend { +impl JointOpeningPolynomials for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, @@ -66,7 +66,7 @@ impl JointOpeningPolynomials for ReferenceBackend { /// a one-hot table's native `k · T + t` view permutes to `t · cycle_stride + /// k · one_hot_stride`; a dense (per-cycle) table sits at each cycle block's /// address slot zero. -fn address_major_embed( +fn address_major_embed( table: &[F], grid: CommitmentGrid, polynomial: JoltCommittedPolynomial, @@ -120,7 +120,7 @@ fn address_major_embed( /// Embed an advice polynomial's balanced matrix into the grid matrix's /// top-left block: advice coefficient `row · 2^σ_a + col` lands at grid index /// `row · 2^σ_main + col`. -fn block_embed( +fn block_embed( table: &[F], grid: CommitmentGrid, polynomial: JoltCommittedPolynomial, diff --git a/crates/jolt-kernels/src/reference/precommitted_reduction.rs b/crates/jolt-kernels/src/reference/precommitted_reduction.rs index 90edb5c395..35e26b238b 100644 --- a/crates/jolt-kernels/src/reference/precommitted_reduction.rs +++ b/crates/jolt-kernels/src/reference/precommitted_reduction.rs @@ -9,7 +9,7 @@ use std::marker::PhantomData; use jolt_claims::protocols::jolt::JoltChallengeId; use jolt_claims::{InputClaims, OutputClaims, SumcheckChallenges}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_verifier::stages::relations::{ ConcreteSumcheck, ConcreteSumcheckChallenges, SumcheckInputClaims, SumcheckOutputClaims, }; @@ -39,7 +39,7 @@ impl ReferencePrecommittedAddress { impl PrepareKernel for ReferencePrecommittedAddress where - F: Field, + F: JoltField, R: ConcreteSumcheck + 'static, AddressReductionKernel: SumcheckKernel, SumcheckInputClaims: InputClaims, diff --git a/crates/jolt-kernels/src/reference/program_image_claim_reduction.rs b/crates/jolt-kernels/src/reference/program_image_claim_reduction.rs index 3755f89115..95539e3fa2 100644 --- a/crates/jolt-kernels/src/reference/program_image_claim_reduction.rs +++ b/crates/jolt-kernels/src/reference/program_image_claim_reduction.rs @@ -11,7 +11,7 @@ //! the staged stage-4 contribution. use jolt_claims::protocols::jolt::{PrecommittedReductionLayout, ProgramImageClaimReductionLayout}; -use jolt_field::Field; +use jolt_field::JoltField; use crate::ProverInputs; use jolt_verifier::stages::stage6b::committed_reduction_cycle_phase::ProgramImageReductionCyclePhase; @@ -22,7 +22,7 @@ use crate::committed_program::program_image_words_padded; use crate::precommitted_reduction::{permute_tables, CycleReductionKernel}; use crate::{KernelError, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel}; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, @@ -45,7 +45,7 @@ impl PrepareKernel> for Referenc /// The program-image reduction's cycle-phase kernel — see the module doc for /// the value/eq table construction. -fn program_image_reduction_kernel( +fn program_image_reduction_kernel( layout: &ProgramImageClaimReductionLayout, r_addr_rw: &[F], start_index: usize, diff --git a/crates/jolt-kernels/src/reference/ram_hamming_booleanity.rs b/crates/jolt-kernels/src/reference/ram_hamming_booleanity.rs index 409164fde3..cac3e68524 100644 --- a/crates/jolt-kernels/src/reference/ram_hamming_booleanity.rs +++ b/crates/jolt-kernels/src/reference/ram_hamming_booleanity.rs @@ -13,7 +13,7 @@ use std::collections::BTreeMap; use crate::ProverInputs; use jolt_claims::protocols::jolt::geometry::ram::ram_hamming_weight; use jolt_claims::protocols::jolt::{JoltDerivedId, RamHammingBooleanityPublic}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::stage6b::ram_hamming_booleanity::RamHammingBooleanity; use jolt_witness::JoltWitnessPlane; @@ -23,7 +23,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/ram_output_check.rs b/crates/jolt-kernels/src/reference/ram_output_check.rs index 8b4486fcae..873530cd2b 100644 --- a/crates/jolt-kernels/src/reference/ram_output_check.rs +++ b/crates/jolt-kernels/src/reference/ram_output_check.rs @@ -19,7 +19,7 @@ use std::collections::BTreeMap; use crate::ProverInputs; use jolt_claims::protocols::jolt::geometry::ram::ram_val_final; use jolt_claims::protocols::jolt::{JoltDerivedId, RamOutputCheckPublic}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::stage2::ram_output_check::RamOutputCheck; use jolt_witness::JoltWitnessPlane; @@ -29,7 +29,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/ram_ra_claim_reduction.rs b/crates/jolt-kernels/src/reference/ram_ra_claim_reduction.rs index a032324207..957003275c 100644 --- a/crates/jolt-kernels/src/reference/ram_ra_claim_reduction.rs +++ b/crates/jolt-kernels/src/reference/ram_ra_claim_reduction.rs @@ -14,7 +14,7 @@ use std::collections::BTreeMap; use crate::ProverInputs; use jolt_claims::protocols::jolt::geometry::ram::ram_ra_claim_reduction; use jolt_claims::protocols::jolt::{JoltDerivedId, RamRaClaimReductionPublic}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::stage5::ram_ra_claim_reduction::RamRaClaimReduction; use jolt_witness::JoltWitnessPlane; @@ -24,7 +24,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/ram_ra_virtualization.rs b/crates/jolt-kernels/src/reference/ram_ra_virtualization.rs index 74358cfb21..bccac9c361 100644 --- a/crates/jolt-kernels/src/reference/ram_ra_virtualization.rs +++ b/crates/jolt-kernels/src/reference/ram_ra_virtualization.rs @@ -12,7 +12,7 @@ use crate::ProverInputs; use jolt_claims::protocols::jolt::geometry::dimensions::committed_address_chunks; use jolt_claims::protocols::jolt::geometry::ram::committed_ram_ra; use jolt_claims::protocols::jolt::{JoltDerivedId, RamRaVirtualizationPublic}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::stage6b::ram_ra_virtualization::RamRaVirtualization; use jolt_witness::JoltWitnessPlane; @@ -22,7 +22,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/ram_raf_evaluation.rs b/crates/jolt-kernels/src/reference/ram_raf_evaluation.rs index dccab2edf6..a563483279 100644 --- a/crates/jolt-kernels/src/reference/ram_raf_evaluation.rs +++ b/crates/jolt-kernels/src/reference/ram_raf_evaluation.rs @@ -16,7 +16,7 @@ use std::collections::BTreeMap; use crate::ProverInputs; use jolt_claims::protocols::jolt::geometry::ram::ram_ra_raf_evaluation; use jolt_claims::protocols::jolt::{JoltDerivedId, RamRafEvaluationPublic}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::stage2::ram_raf_evaluation::RamRafEvaluation; use jolt_witness::JoltWitnessPlane; @@ -26,7 +26,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/ram_read_write.rs b/crates/jolt-kernels/src/reference/ram_read_write.rs index 2799a4936b..877144c498 100644 --- a/crates/jolt-kernels/src/reference/ram_read_write.rs +++ b/crates/jolt-kernels/src/reference/ram_read_write.rs @@ -14,7 +14,7 @@ use std::collections::BTreeMap; use crate::ProverInputs; use jolt_claims::protocols::jolt::geometry::ram::{ram_inc, ram_ra, ram_val}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::stage2::ram_read_write_checking::RamReadWriteChecking; use jolt_witness::JoltWitnessPlane; @@ -24,7 +24,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/ram_val_check.rs b/crates/jolt-kernels/src/reference/ram_val_check.rs index 8360e560cc..bd5194b292 100644 --- a/crates/jolt-kernels/src/reference/ram_val_check.rs +++ b/crates/jolt-kernels/src/reference/ram_val_check.rs @@ -18,7 +18,7 @@ use std::collections::BTreeMap; use crate::ProverInputs; use jolt_claims::protocols::jolt::geometry::ram::{ram_inc_val_check, ram_ra_val_check}; use jolt_claims::protocols::jolt::{JoltDerivedId, RamValCheckPublic}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, LtPolynomial, Polynomial}; use jolt_verifier::stages::stage4::ram_val_check::RamValCheck; use jolt_witness::JoltWitnessPlane; @@ -28,7 +28,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/registers_claim_reduction.rs b/crates/jolt-kernels/src/reference/registers_claim_reduction.rs index 50575b0204..944f2db8b3 100644 --- a/crates/jolt-kernels/src/reference/registers_claim_reduction.rs +++ b/crates/jolt-kernels/src/reference/registers_claim_reduction.rs @@ -14,7 +14,7 @@ use jolt_claims::protocols::jolt::geometry::claim_reductions::registers::{ rd_write_value_reduced, rs1_value_reduced, rs2_value_reduced, }; use jolt_claims::protocols::jolt::{JoltDerivedId, RegistersClaimReductionPublic}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::stage3::outputs::RegistersClaimReduction; use jolt_witness::JoltWitnessPlane; @@ -24,7 +24,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/registers_read_write.rs b/crates/jolt-kernels/src/reference/registers_read_write.rs index 0b4727cb9a..f51b602500 100644 --- a/crates/jolt-kernels/src/reference/registers_read_write.rs +++ b/crates/jolt-kernels/src/reference/registers_read_write.rs @@ -17,7 +17,7 @@ use jolt_claims::protocols::jolt::geometry::registers::{ rs2_ra_read_write, }; use jolt_claims::protocols::jolt::{JoltDerivedId, RegistersReadWritePublic}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, Polynomial}; use jolt_verifier::stages::stage4::registers_read_write_checking::RegistersReadWriteChecking; use jolt_witness::JoltWitnessPlane; @@ -27,7 +27,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/registers_val_evaluation.rs b/crates/jolt-kernels/src/reference/registers_val_evaluation.rs index 923c5cbcfc..163b6588a3 100644 --- a/crates/jolt-kernels/src/reference/registers_val_evaluation.rs +++ b/crates/jolt-kernels/src/reference/registers_val_evaluation.rs @@ -15,7 +15,7 @@ use jolt_claims::protocols::jolt::geometry::registers::{ rd_inc_val_evaluation, rd_wa_val_evaluation, }; use jolt_claims::protocols::jolt::{JoltDerivedId, RegistersValEvaluationPublic}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, LtPolynomial, Polynomial}; use jolt_verifier::stages::stage5::registers_val_evaluation::RegistersValEvaluation; use jolt_witness::JoltWitnessPlane; @@ -25,7 +25,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/spartan_outer.rs b/crates/jolt-kernels/src/reference/spartan_outer.rs index ec517590e6..9e27e698f8 100644 --- a/crates/jolt-kernels/src/reference/spartan_outer.rs +++ b/crates/jolt-kernels/src/reference/spartan_outer.rs @@ -23,7 +23,7 @@ use std::collections::BTreeMap; use jolt_claims::protocols::jolt::geometry::dimensions::OUTER_UNISKIP_DOMAIN_SIZE; use jolt_claims::protocols::jolt::geometry::spartan::{outer_opening, SpartanOuterDimensions}; use jolt_claims::protocols::jolt::{JoltDerivedId, JoltOpeningId, SpartanOuterPublic}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::lagrange::{centered_lagrange_evals, centered_lagrange_kernel, poly_mul}; use jolt_poly::{BindingOrder, EqPolynomial, Polynomial, UnivariatePoly}; use jolt_r1cs::constraint::ConstraintMatrices; @@ -39,7 +39,7 @@ use crate::{ }; use jolt_witness::JoltWitnessPlane; -impl UniskipKernel> for ReferenceBackend { +impl UniskipKernel> for ReferenceBackend { fn prepare( &self, session: &mut ProofSession, @@ -69,7 +69,7 @@ impl UniskipKernel> for ReferenceBackend { /// uni-skip slot parked and binds it into the batch member. pub struct ReferenceOuterRemainder; -impl PrepareKernel> for ReferenceOuterRemainder { +impl PrepareKernel> for ReferenceOuterRemainder { fn prepare( &self, session: &mut ProofSession, @@ -88,7 +88,7 @@ impl PrepareKernel> for ReferenceOuterRemainder { /// The shared stage-1 compute state: the 35 R1CS input tables, the /// per-constraint Az/Bz row-value tables, and `eq(τ_low, ·)` — everything the /// uni-skip polynomial and the remainder member both consume. -pub struct SpartanOuterKernel { +pub struct SpartanOuterKernel { log_t: usize, tau: Vec, matrices: ConstraintMatrices, @@ -105,7 +105,7 @@ pub struct SpartanOuterKernel { eq_table: Vec, } -impl SpartanOuterKernel { +impl SpartanOuterKernel { /// Materialize the stage's compute state from the witness. `tau` is the /// stage's full challenge vector (`log_t + 2` entries). pub fn prepare( @@ -286,7 +286,7 @@ impl SpartanOuterKernel { /// Materialize the 35 R1CS input polynomials (cycle-indexed, big-endian) in /// the relation's variable order. -fn materialize_input_tables( +fn materialize_input_tables( witness: &dyn JoltWitnessOracle, dimensions: &SpartanOuterDimensions, ) -> Result>, KernelError> { @@ -300,7 +300,7 @@ fn materialize_input_tables( /// Per-constraint-row Az/Bz value tables over the cycle domain: /// `az_rows[r][t] = Σ_(v,α)∈A_r α · z_t[v]` with `z_t[0] = 1` and /// `z_t[1 + k] = input_tables[k][t]`. -fn row_value_tables( +fn row_value_tables( matrices: &ConstraintMatrices, input_tables: &[Vec], ) -> (Vec>, Vec>) { @@ -329,7 +329,7 @@ fn row_value_tables( #[cfg(test)] mod orientation_probes { - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_poly::{BindingOrder, EqPolynomial, Polynomial}; /// Pin the composite orientation assumption: an `EqPolynomial` table diff --git a/crates/jolt-kernels/src/reference/spartan_product.rs b/crates/jolt-kernels/src/reference/spartan_product.rs index cb3a3b2876..818419cf9f 100644 --- a/crates/jolt-kernels/src/reference/spartan_product.rs +++ b/crates/jolt-kernels/src/reference/spartan_product.rs @@ -24,7 +24,7 @@ use jolt_claims::protocols::jolt::geometry::spartan::{ write_lookup_output_to_rd_product, }; use jolt_claims::protocols::jolt::{JoltDerivedId, SpartanProductVirtualizationPublic}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::lagrange::{ centered_lagrange_evals, centered_lagrange_kernel, interpolate_to_coeffs, poly_mul, }; @@ -40,7 +40,7 @@ use crate::{ }; use jolt_witness::JoltWitnessPlane; -impl UniskipKernel> for ReferenceBackend { +impl UniskipKernel> for ReferenceBackend { /// Runs on `tau_low` only — `τ_high` is drawn after this call and reaches /// the slot as the single `late_tau` entry of /// [`first_round_poly`](UniskipKernel::first_round_poly). @@ -78,7 +78,7 @@ impl UniskipKernel> for ReferenceBackend { /// the uni-skip slot parked and binds it into the batch member. pub struct ReferenceProductRemainder; -impl PrepareKernel> for ReferenceProductRemainder { +impl PrepareKernel> for ReferenceProductRemainder { fn prepare( &self, session: &mut ProofSession, @@ -97,7 +97,7 @@ impl PrepareKernel> for ReferenceProductRemaind /// The shared product compute state: the eight cycle-indexed factor/wire /// tables and `eq(τ_low, ·)` — everything the uni-skip polynomial and the /// remainder member both consume. -pub struct SpartanProductKernel { +pub struct SpartanProductKernel { log_t: usize, eq_cycle: Vec, left_instruction_input: Vec, @@ -110,7 +110,7 @@ pub struct SpartanProductKernel { virtual_instruction: Vec, } -impl SpartanProductKernel { +impl SpartanProductKernel { pub fn prepare( log_t: usize, tau_low: &[F], diff --git a/crates/jolt-kernels/src/reference/spartan_shift.rs b/crates/jolt-kernels/src/reference/spartan_shift.rs index 0c8699e169..c393976401 100644 --- a/crates/jolt-kernels/src/reference/spartan_shift.rs +++ b/crates/jolt-kernels/src/reference/spartan_shift.rs @@ -16,7 +16,7 @@ use jolt_claims::protocols::jolt::geometry::spartan::{ is_first_in_sequence_shift, is_noop_shift, is_virtual_shift, pc_shift, unexpanded_pc_shift, }; use jolt_claims::protocols::jolt::{JoltDerivedId, SpartanShiftPublic}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{BindingOrder, EqPlusOnePolynomial, Polynomial}; use jolt_verifier::stages::stage3::outputs::SpartanShift; use jolt_witness::JoltWitnessPlane; @@ -26,7 +26,7 @@ use crate::{ KernelError, NaiveSumcheckProver, PrepareKernel, ProofSession, ReferenceBackend, SumcheckKernel, }; -impl PrepareKernel> for ReferenceBackend { +impl PrepareKernel> for ReferenceBackend { fn prepare( &self, _session: &mut ProofSession, diff --git a/crates/jolt-kernels/src/reference/views.rs b/crates/jolt-kernels/src/reference/views.rs index 342db95cde..c2c7501864 100644 --- a/crates/jolt-kernels/src/reference/views.rs +++ b/crates/jolt-kernels/src/reference/views.rs @@ -1,14 +1,14 @@ //! Shared witness-view and table helpers for the per-relation kernels. use jolt_claims::protocols::jolt::JoltOpeningId; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::EqPolynomial; use jolt_witness::JoltWitnessOracle; use crate::KernelError; /// Materialize a dense field-element table of the oracle behind `opening`. -pub(crate) fn dense_view( +pub(crate) fn dense_view( witness: &dyn JoltWitnessOracle, opening: JoltOpeningId, ) -> Result, KernelError> { @@ -16,14 +16,14 @@ pub(crate) fn dense_view( } /// `eq(point, ·)` evaluations, big-endian (`point[0]` pairs the index MSB). -pub(crate) fn eq_table(point: &[F]) -> Vec { +pub(crate) fn eq_table(point: &[F]) -> Vec { EqPolynomial::new(point.to_vec()).evaluations() } /// Fold the address dimension of an address-major `(K × T)` oracle grid by the /// eq weights of `point` (big-endian, `K = 2^point.len()`): /// `out[j] = Σ_k eq(point, k) · grid[(k << log_t) | j]`. -pub(crate) fn address_fold( +pub(crate) fn address_fold( witness: &dyn JoltWitnessOracle, opening: JoltOpeningId, log_t: usize, @@ -52,7 +52,7 @@ pub(crate) fn address_fold( /// Fold the cycle dimension of an address-major `(K × T)` oracle grid by the /// eq weights of `point` (big-endian, `T = 2^point.len()`): /// `out[k] = Σ_j eq(point, j) · grid[(k << log_t) | j]`. -pub(crate) fn cycle_fold( +pub(crate) fn cycle_fold( witness: &dyn JoltWitnessOracle, opening: JoltOpeningId, log_k: usize, @@ -81,7 +81,7 @@ pub(crate) fn cycle_fold( /// Tile `base` `copies` times: the `(address ‖ cycle)`-indexed replication of a /// cycle-indexed table across the address dimension (address bits are the high /// bits of the joint index). -pub(crate) fn tile(base: &[F], copies: usize) -> Vec { +pub(crate) fn tile(base: &[F], copies: usize) -> Vec { let mut out = Vec::with_capacity(base.len() * copies); for _ in 0..copies { out.extend_from_slice(base); @@ -91,7 +91,7 @@ pub(crate) fn tile(base: &[F], copies: usize) -> Vec { /// Replicate a cycle-indexed table across the stream bit at the index LSB /// (`out[(t << 1) | s] = base[t]`). -pub(crate) fn replicate_stream_lsb(base: &[F]) -> Vec { +pub(crate) fn replicate_stream_lsb(base: &[F]) -> Vec { let mut out = Vec::with_capacity(base.len() * 2); for &value in base { out.push(value); @@ -102,7 +102,7 @@ pub(crate) fn replicate_stream_lsb(base: &[F]) -> Vec { /// A per-stream constant table over the `(cycle ‖ stream)` domain with the /// stream bit at the index LSB (`out[(t << 1) | s] = values[s]`). -pub(crate) fn stream_pair_lsb(values: [F; 2], cycles: usize) -> Vec { +pub(crate) fn stream_pair_lsb(values: [F; 2], cycles: usize) -> Vec { let mut out = Vec::with_capacity(cycles * 2); for _ in 0..cycles { out.push(values[0]); @@ -117,7 +117,7 @@ mod tests { use jolt_claims::protocols::jolt::{ JoltOpeningId, JoltPolynomialId, JoltRelationId, JoltVirtualPolynomial, }; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_witness::{FixedBackend, PolynomialEncoding, Shape}; use super::{address_fold, cycle_fold, dense_view}; diff --git a/crates/jolt-kernels/src/uniskip.rs b/crates/jolt-kernels/src/uniskip.rs index 47db8c5f95..e065e9548f 100644 --- a/crates/jolt-kernels/src/uniskip.rs +++ b/crates/jolt-kernels/src/uniskip.rs @@ -12,7 +12,7 @@ use jolt_claims::protocols::jolt::JoltChallengeId; use jolt_claims::{InputClaims, OutputClaims, SumcheckChallenges}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::UnivariatePoly; use jolt_verifier::stages::relations::{ ConcreteSumcheck, ConcreteSumcheckChallenges, SumcheckInputClaims, SumcheckOutputClaims, @@ -27,7 +27,7 @@ use crate::{KernelError, ProofSession}; /// fields. pub trait UniskipKernel where - F: Field, + F: JoltField, R: ConcreteSumcheck, SumcheckInputClaims: InputClaims, SumcheckOutputClaims: OutputClaims, diff --git a/crates/jolt-lookup-tables/src/challenge_ops.rs b/crates/jolt-lookup-tables/src/challenge_ops.rs index 6280c85350..24c17ba002 100644 --- a/crates/jolt-lookup-tables/src/challenge_ops.rs +++ b/crates/jolt-lookup-tables/src/challenge_ops.rs @@ -5,16 +5,16 @@ //! needed for prefix/suffix MLE computation. //! //! Since challenges are now just field elements (`C = F`), these traits are trivially -//! satisfied by any `F: Field`. They remain as named bounds for readability at use sites. +//! satisfied by any `F: JoltField`. They remain as named bounds for readability at use sites. -use jolt_field::Field; +use jolt_field::JoltField; use std::ops::{Add, Mul, Sub}; /// A challenge value that can do arithmetic with field elements and other challenges. /// /// The key property is that all arithmetic produces field elements `F`, even /// operations between two challenges (`C * C -> F`). -pub trait ChallengeOps: +pub trait ChallengeOps: Copy + Send + Sync @@ -31,7 +31,7 @@ pub trait ChallengeOps: { } -impl ChallengeOps for C where +impl ChallengeOps for C where C: Copy + Send + Sync diff --git a/crates/jolt-lookup-tables/src/tables/and.rs b/crates/jolt-lookup-tables/src/tables/and.rs index 9afcec236b..68270a1b8d 100644 --- a/crates/jolt-lookup-tables/src/tables/and.rs +++ b/crates/jolt-lookup-tables/src/tables/and.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -20,7 +20,7 @@ impl LookupTable for AndTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let mut result = F::zero(); @@ -43,7 +43,7 @@ impl PrefixSuffixDecomposition for AndTable { } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { let [one, and] = suffixes.try_into().unwrap(); prefixes[Prefixes::And] * one + and } diff --git a/crates/jolt-lookup-tables/src/tables/andn.rs b/crates/jolt-lookup-tables/src/tables/andn.rs index 6742aeec61..6a3b9b3f83 100644 --- a/crates/jolt-lookup-tables/src/tables/andn.rs +++ b/crates/jolt-lookup-tables/src/tables/andn.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -20,7 +20,7 @@ impl LookupTable for AndnTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let mut result = F::zero(); @@ -43,7 +43,7 @@ impl PrefixSuffixDecomposition for AndnTable { } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { let [one, andn] = suffixes.try_into().unwrap(); prefixes[Prefixes::Andn] * one + andn } diff --git a/crates/jolt-lookup-tables/src/tables/equal.rs b/crates/jolt-lookup-tables/src/tables/equal.rs index 276a9b53b2..18330789ea 100644 --- a/crates/jolt-lookup-tables/src/tables/equal.rs +++ b/crates/jolt-lookup-tables/src/tables/equal.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -20,7 +20,7 @@ impl LookupTable for EqualTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert!(r.len().is_multiple_of(2)); let mut result = F::one(); @@ -43,7 +43,7 @@ impl PrefixSuffixDecomposition for EqualTable { } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [eq] = suffixes.try_into().unwrap(); prefixes[Prefixes::Eq] * eq diff --git a/crates/jolt-lookup-tables/src/tables/halfword_alignment.rs b/crates/jolt-lookup-tables/src/tables/halfword_alignment.rs index c644eb6d13..84cfeebcee 100644 --- a/crates/jolt-lookup-tables/src/tables/halfword_alignment.rs +++ b/crates/jolt-lookup-tables/src/tables/halfword_alignment.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -18,7 +18,7 @@ impl LookupTable for HalfwordAlignmentTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { let lsb = r[r.len() - 1]; F::one() - lsb @@ -35,7 +35,7 @@ impl PrefixSuffixDecomposition for HalfwordAlignmentTab } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, lsb] = suffixes.try_into().unwrap(); one - prefixes[Prefixes::Lsb] * lsb diff --git a/crates/jolt-lookup-tables/src/tables/lower_half_word.rs b/crates/jolt-lookup-tables/src/tables/lower_half_word.rs index c44472a484..3a7c4780e3 100644 --- a/crates/jolt-lookup-tables/src/tables/lower_half_word.rs +++ b/crates/jolt-lookup-tables/src/tables/lower_half_word.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -21,7 +21,7 @@ impl LookupTable for LowerHalfWordTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let half_word_size = XLEN / 2; @@ -43,7 +43,7 @@ impl PrefixSuffixDecomposition for LowerHalfWordTable(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { let [one, lower_half_word] = suffixes.try_into().unwrap(); prefixes[Prefixes::LowerHalfWord] * one + lower_half_word } diff --git a/crates/jolt-lookup-tables/src/tables/mod.rs b/crates/jolt-lookup-tables/src/tables/mod.rs index 658231071a..bd27d07e2b 100644 --- a/crates/jolt-lookup-tables/src/tables/mod.rs +++ b/crates/jolt-lookup-tables/src/tables/mod.rs @@ -9,7 +9,7 @@ //! All tables are generic over `const XLEN: usize`. The supported word sizes //! are `XLEN = 64` (production) and `XLEN = 8` (full-hypercube tests). -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -230,7 +230,7 @@ impl LookupTableKind { pub fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { dispatch!(self, t => t.evaluate_mle(r)) } @@ -243,7 +243,11 @@ impl LookupTableKind { dispatch!(self, t => PrefixSuffixDecomposition::prefixes(t)) } - pub fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + pub fn combine( + &self, + prefixes: &[PrefixEval], + suffixes: &[SuffixEval], + ) -> F { dispatch!(self, t => PrefixSuffixDecomposition::combine(t, prefixes, suffixes)) } } @@ -265,7 +269,7 @@ pub trait PrefixSuffixDecomposition: crate::LookupTable + Def fn suffixes(&self) -> &'static [Suffixes]; /// Recombine evaluated prefix and suffix values into the table's MLE evaluation. - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F; + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F; /// Generate a random lookup index for testing. /// diff --git a/crates/jolt-lookup-tables/src/tables/mulu_no_overflow.rs b/crates/jolt-lookup-tables/src/tables/mulu_no_overflow.rs index 4e97e0a974..dbcc0eb0d8 100644 --- a/crates/jolt-lookup-tables/src/tables/mulu_no_overflow.rs +++ b/crates/jolt-lookup-tables/src/tables/mulu_no_overflow.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -19,7 +19,7 @@ impl LookupTable for MulUNoOverflowTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let mut result = F::one(); @@ -40,7 +40,7 @@ impl PrefixSuffixDecomposition for MulUNoOverflowTable< } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [overflow_bits_zero] = suffixes.try_into().unwrap(); prefixes[Prefixes::OverflowBitsZero] * overflow_bits_zero diff --git a/crates/jolt-lookup-tables/src/tables/not_equal.rs b/crates/jolt-lookup-tables/src/tables/not_equal.rs index 6b68426193..dfbaaedfcb 100644 --- a/crates/jolt-lookup-tables/src/tables/not_equal.rs +++ b/crates/jolt-lookup-tables/src/tables/not_equal.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -21,7 +21,7 @@ impl LookupTable for NotEqualTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { F::one() - EqualTable::.evaluate_mle::(r) } @@ -37,7 +37,7 @@ impl PrefixSuffixDecomposition for NotEqualTable } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, eq] = suffixes.try_into().unwrap(); one - prefixes[Prefixes::Eq] * eq diff --git a/crates/jolt-lookup-tables/src/tables/or.rs b/crates/jolt-lookup-tables/src/tables/or.rs index 18afb451b5..41d729ad31 100644 --- a/crates/jolt-lookup-tables/src/tables/or.rs +++ b/crates/jolt-lookup-tables/src/tables/or.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -20,7 +20,7 @@ impl LookupTable for OrTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let mut result = F::zero(); @@ -43,7 +43,7 @@ impl PrefixSuffixDecomposition for OrTable { } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { let [one, or] = suffixes.try_into().unwrap(); prefixes[Prefixes::Or] * one + or } diff --git a/crates/jolt-lookup-tables/src/tables/pow2.rs b/crates/jolt-lookup-tables/src/tables/pow2.rs index d47b488afb..c27cb6e136 100644 --- a/crates/jolt-lookup-tables/src/tables/pow2.rs +++ b/crates/jolt-lookup-tables/src/tables/pow2.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -19,7 +19,7 @@ impl LookupTable for Pow2Table { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let log_xlen = XLEN.trailing_zeros() as usize; @@ -41,7 +41,7 @@ impl PrefixSuffixDecomposition for Pow2Table { } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { let [pow2] = suffixes.try_into().unwrap(); prefixes[Prefixes::Pow2] * pow2 } diff --git a/crates/jolt-lookup-tables/src/tables/pow2_w.rs b/crates/jolt-lookup-tables/src/tables/pow2_w.rs index 3d541bc339..23925164a3 100644 --- a/crates/jolt-lookup-tables/src/tables/pow2_w.rs +++ b/crates/jolt-lookup-tables/src/tables/pow2_w.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -18,7 +18,7 @@ impl LookupTable for Pow2WTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let log_half = (XLEN / 2).trailing_zeros() as usize; @@ -40,7 +40,7 @@ impl PrefixSuffixDecomposition for Pow2WTable { } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [pow2w] = suffixes.try_into().unwrap(); prefixes[Prefixes::Pow2W] * pow2w diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/and.rs b/crates/jolt-lookup-tables/src/tables/prefixes/and.rs index 46a2250020..4121c34660 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/and.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/and.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum AndPrefix {} -impl SparseDensePrefix for AndPrefix { +impl SparseDensePrefix for AndPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/andn.rs b/crates/jolt-lookup-tables/src/tables/prefixes/andn.rs index 8e8c248c07..6a7027569b 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/andn.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/andn.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum AndnPrefix {} -impl SparseDensePrefix for AndnPrefix { +impl SparseDensePrefix for AndnPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/change_divisor.rs b/crates/jolt-lookup-tables/src/tables/prefixes/change_divisor.rs index 9bf40dbb14..90ca212666 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/change_divisor.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/change_divisor.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum ChangeDivisorPrefix {} -impl SparseDensePrefix for ChangeDivisorPrefix { +impl SparseDensePrefix for ChangeDivisorPrefix { fn default_checkpoint() -> F { F::from_u64(2) - F::from_u128(1u128 << XLEN) } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/change_divisor_w.rs b/crates/jolt-lookup-tables/src/tables/prefixes/change_divisor_w.rs index f4d3848c2e..526f70b7d0 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/change_divisor_w.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/change_divisor_w.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum ChangeDivisorWPrefix {} -impl SparseDensePrefix for ChangeDivisorWPrefix { +impl SparseDensePrefix for ChangeDivisorWPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/div_by_zero.rs b/crates/jolt-lookup-tables/src/tables/prefixes/div_by_zero.rs index bd44cca56d..f52ee1c779 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/div_by_zero.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/div_by_zero.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum DivByZeroPrefix {} -impl SparseDensePrefix for DivByZeroPrefix { +impl SparseDensePrefix for DivByZeroPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/eq.rs b/crates/jolt-lookup-tables/src/tables/prefixes/eq.rs index 036f6b0973..09b1a26b5b 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/eq.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/eq.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum EqPrefix {} -impl SparseDensePrefix for EqPrefix { +impl SparseDensePrefix for EqPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/left_is_zero.rs b/crates/jolt-lookup-tables/src/tables/prefixes/left_is_zero.rs index 03a2960eac..1be4878f83 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/left_is_zero.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/left_is_zero.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum LeftOperandIsZeroPrefix {} -impl SparseDensePrefix for LeftOperandIsZeroPrefix { +impl SparseDensePrefix for LeftOperandIsZeroPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/left_operand_msb.rs b/crates/jolt-lookup-tables/src/tables/prefixes/left_operand_msb.rs index ccbd5e9055..6ebeec7dc4 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/left_operand_msb.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/left_operand_msb.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum LeftOperandMsbPrefix {} -impl SparseDensePrefix for LeftOperandMsbPrefix { +impl SparseDensePrefix for LeftOperandMsbPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/left_shift.rs b/crates/jolt-lookup-tables/src/tables/prefixes/left_shift.rs index 6201582dc0..89ca43f9e9 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/left_shift.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/left_shift.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum LeftShiftPrefix {} -impl SparseDensePrefix for LeftShiftPrefix { +impl SparseDensePrefix for LeftShiftPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/left_shift_helper.rs b/crates/jolt-lookup-tables/src/tables/prefixes/left_shift_helper.rs index a387737840..0f6c06af84 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/left_shift_helper.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/left_shift_helper.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum LeftShiftHelperPrefix {} -impl SparseDensePrefix for LeftShiftHelperPrefix { +impl SparseDensePrefix for LeftShiftHelperPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/left_shift_w.rs b/crates/jolt-lookup-tables/src/tables/prefixes/left_shift_w.rs index a97ce62e68..28d6e65bb9 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/left_shift_w.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/left_shift_w.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum LeftShiftWPrefix {} -impl SparseDensePrefix for LeftShiftWPrefix { +impl SparseDensePrefix for LeftShiftWPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/left_shift_w_helper.rs b/crates/jolt-lookup-tables/src/tables/prefixes/left_shift_w_helper.rs index 6576ff5558..a2f2411cf8 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/left_shift_w_helper.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/left_shift_w_helper.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum LeftShiftWHelperPrefix {} -impl SparseDensePrefix for LeftShiftWHelperPrefix { +impl SparseDensePrefix for LeftShiftWHelperPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/lower_half_word.rs b/crates/jolt-lookup-tables/src/tables/prefixes/lower_half_word.rs index fc0d3bc296..208292b523 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/lower_half_word.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/lower_half_word.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum LowerHalfWordPrefix {} -impl SparseDensePrefix for LowerHalfWordPrefix { +impl SparseDensePrefix for LowerHalfWordPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/lower_word.rs b/crates/jolt-lookup-tables/src/tables/prefixes/lower_word.rs index ec6c76f01d..a1abd94a4f 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/lower_word.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/lower_word.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum LowerWordPrefix {} -impl SparseDensePrefix for LowerWordPrefix { +impl SparseDensePrefix for LowerWordPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/lsb.rs b/crates/jolt-lookup-tables/src/tables/prefixes/lsb.rs index bc0aa9f050..8f5c8a5f80 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/lsb.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/lsb.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, SparseDensePrefix}; pub enum LsbPrefix {} -impl SparseDensePrefix for LsbPrefix { +impl SparseDensePrefix for LsbPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/lt.rs b/crates/jolt-lookup-tables/src/tables/prefixes/lt.rs index 3b6ed55183..75ced4455e 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/lt.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/lt.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum LessThanPrefix {} -impl SparseDensePrefix for LessThanPrefix { +impl SparseDensePrefix for LessThanPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/mod.rs b/crates/jolt-lookup-tables/src/tables/prefixes/mod.rs index 8c8b30f14f..b91df4b3e3 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/mod.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/mod.rs @@ -48,7 +48,7 @@ pub mod xor; pub mod xor_rot; pub mod xor_rotw; -use jolt_field::Field; +use jolt_field::JoltField; use std::fmt::Display; use std::ops::Index; @@ -60,7 +60,7 @@ use crate::lookup_bits::LookupBits; /// - `default_checkpoint()`: the initial checkpoint value before any phases /// - `evaluate()`: the prefix value at a binary point, given accumulated /// checkpoints from previous phases -pub trait SparseDensePrefix: 'static + Sync { +pub trait SparseDensePrefix: 'static + Sync { /// Default checkpoint value for this prefix before any phases have run. fn default_checkpoint() -> F; @@ -222,12 +222,12 @@ macro_rules! dispatch_prefix { impl Prefixes { /// Return the default checkpoint value for this prefix variant. - pub fn default_checkpoint(&self) -> PrefixEval { + pub fn default_checkpoint(&self) -> PrefixEval { PrefixEval(dispatch_prefix!(self, default_checkpoint)) } /// Evaluate this prefix at binary point `b`. - pub fn evaluate( + pub fn evaluate( &self, checkpoints: &[PrefixEval], b: LookupBits, diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/negative_divisor_equals_remainder.rs b/crates/jolt-lookup-tables/src/tables/prefixes/negative_divisor_equals_remainder.rs index 1fb58ef498..c0a8ebc82f 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/negative_divisor_equals_remainder.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/negative_divisor_equals_remainder.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum NegativeDivisorEqualsRemainderPrefix {} -impl SparseDensePrefix for NegativeDivisorEqualsRemainderPrefix { +impl SparseDensePrefix for NegativeDivisorEqualsRemainderPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/negative_divisor_greater_than_remainder.rs b/crates/jolt-lookup-tables/src/tables/prefixes/negative_divisor_greater_than_remainder.rs index bc54a6e669..d1fa166cb6 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/negative_divisor_greater_than_remainder.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/negative_divisor_greater_than_remainder.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum NegativeDivisorGreaterThanRemainderPrefix {} -impl SparseDensePrefix for NegativeDivisorGreaterThanRemainderPrefix { +impl SparseDensePrefix for NegativeDivisorGreaterThanRemainderPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/negative_divisor_zero_remainder.rs b/crates/jolt-lookup-tables/src/tables/prefixes/negative_divisor_zero_remainder.rs index 64bd96faec..a1371665ff 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/negative_divisor_zero_remainder.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/negative_divisor_zero_remainder.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum NegativeDivisorZeroRemainderPrefix {} -impl SparseDensePrefix for NegativeDivisorZeroRemainderPrefix { +impl SparseDensePrefix for NegativeDivisorZeroRemainderPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/or.rs b/crates/jolt-lookup-tables/src/tables/prefixes/or.rs index 1432fc6bd0..1efbe59daf 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/or.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/or.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum OrPrefix {} -impl SparseDensePrefix for OrPrefix { +impl SparseDensePrefix for OrPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/overflow_bits_zero.rs b/crates/jolt-lookup-tables/src/tables/prefixes/overflow_bits_zero.rs index 80261694bc..a24690942e 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/overflow_bits_zero.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/overflow_bits_zero.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum OverflowBitsZeroPrefix {} -impl SparseDensePrefix for OverflowBitsZeroPrefix { +impl SparseDensePrefix for OverflowBitsZeroPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/positive_remainder_equals_divisor.rs b/crates/jolt-lookup-tables/src/tables/prefixes/positive_remainder_equals_divisor.rs index 08ac240270..c8ee6ddeeb 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/positive_remainder_equals_divisor.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/positive_remainder_equals_divisor.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum PositiveRemainderEqualsDivisorPrefix {} -impl SparseDensePrefix for PositiveRemainderEqualsDivisorPrefix { +impl SparseDensePrefix for PositiveRemainderEqualsDivisorPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/positive_remainder_less_than_divisor.rs b/crates/jolt-lookup-tables/src/tables/prefixes/positive_remainder_less_than_divisor.rs index d6855acd1c..8f22c7da21 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/positive_remainder_less_than_divisor.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/positive_remainder_less_than_divisor.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum PositiveRemainderLessThanDivisorPrefix {} -impl SparseDensePrefix for PositiveRemainderLessThanDivisorPrefix { +impl SparseDensePrefix for PositiveRemainderLessThanDivisorPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/pow2.rs b/crates/jolt-lookup-tables/src/tables/prefixes/pow2.rs index af2d0184e7..e5a43eda47 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/pow2.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/pow2.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum Pow2Prefix {} -impl SparseDensePrefix for Pow2Prefix { +impl SparseDensePrefix for Pow2Prefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/pow2_w.rs b/crates/jolt-lookup-tables/src/tables/prefixes/pow2_w.rs index 469041a9dd..fa06c10ca9 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/pow2_w.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/pow2_w.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum Pow2WPrefix {} -impl SparseDensePrefix for Pow2WPrefix { +impl SparseDensePrefix for Pow2WPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/rev8w.rs b/crates/jolt-lookup-tables/src/tables/prefixes/rev8w.rs index 6e255d56b9..fb4faf3bef 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/rev8w.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/rev8w.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::tables::virtual_rev8w::rev8w; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum Rev8WPrefix {} -impl SparseDensePrefix for Rev8WPrefix { +impl SparseDensePrefix for Rev8WPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/right_is_zero.rs b/crates/jolt-lookup-tables/src/tables/prefixes/right_is_zero.rs index ad23d8a22e..ff69a6cfdb 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/right_is_zero.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/right_is_zero.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum RightOperandIsZeroPrefix {} -impl SparseDensePrefix for RightOperandIsZeroPrefix { +impl SparseDensePrefix for RightOperandIsZeroPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/right_operand.rs b/crates/jolt-lookup-tables/src/tables/prefixes/right_operand.rs index b224a48407..0b3fd6b4f1 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/right_operand.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/right_operand.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum RightOperandPrefix {} -impl SparseDensePrefix for RightOperandPrefix { +impl SparseDensePrefix for RightOperandPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/right_operand_msb.rs b/crates/jolt-lookup-tables/src/tables/prefixes/right_operand_msb.rs index 1047d96667..25245ae821 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/right_operand_msb.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/right_operand_msb.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum RightOperandMsbPrefix {} -impl SparseDensePrefix for RightOperandMsbPrefix { +impl SparseDensePrefix for RightOperandMsbPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/right_operand_w.rs b/crates/jolt-lookup-tables/src/tables/prefixes/right_operand_w.rs index dd94559776..a1785ede61 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/right_operand_w.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/right_operand_w.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum RightOperandWPrefix {} -impl SparseDensePrefix for RightOperandWPrefix { +impl SparseDensePrefix for RightOperandWPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/right_shift.rs b/crates/jolt-lookup-tables/src/tables/prefixes/right_shift.rs index 6c566bffe6..dcf5d5d877 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/right_shift.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/right_shift.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum RightShiftPrefix {} -impl SparseDensePrefix for RightShiftPrefix { +impl SparseDensePrefix for RightShiftPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/right_shift_w.rs b/crates/jolt-lookup-tables/src/tables/prefixes/right_shift_w.rs index 4fb44ea27d..e6f2ff7eb3 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/right_shift_w.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/right_shift_w.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum RightShiftWPrefix {} -impl SparseDensePrefix for RightShiftWPrefix { +impl SparseDensePrefix for RightShiftWPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/sign_extension.rs b/crates/jolt-lookup-tables/src/tables/prefixes/sign_extension.rs index 266574aed1..4b32dd2664 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/sign_extension.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/sign_extension.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum SignExtensionPrefix {} -impl SparseDensePrefix for SignExtensionPrefix { +impl SparseDensePrefix for SignExtensionPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/sign_extension_right_operand.rs b/crates/jolt-lookup-tables/src/tables/prefixes/sign_extension_right_operand.rs index 3a5f895910..8f0fbba0cc 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/sign_extension_right_operand.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/sign_extension_right_operand.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum SignExtensionRightOperandPrefix {} -impl SparseDensePrefix for SignExtensionRightOperandPrefix { +impl SparseDensePrefix for SignExtensionRightOperandPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/sign_extension_upper_half.rs b/crates/jolt-lookup-tables/src/tables/prefixes/sign_extension_upper_half.rs index 1619ce36ea..a1ac1ec3e2 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/sign_extension_upper_half.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/sign_extension_upper_half.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum SignExtensionUpperHalfPrefix {} -impl SparseDensePrefix for SignExtensionUpperHalfPrefix { +impl SparseDensePrefix for SignExtensionUpperHalfPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/two_lsb.rs b/crates/jolt-lookup-tables/src/tables/prefixes/two_lsb.rs index 917b02f652..70e6f336bf 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/two_lsb.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/two_lsb.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum TwoLsbPrefix {} -impl SparseDensePrefix for TwoLsbPrefix { +impl SparseDensePrefix for TwoLsbPrefix { fn default_checkpoint() -> F { F::one() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/upper_word.rs b/crates/jolt-lookup-tables/src/tables/prefixes/upper_word.rs index afce313ed0..35aacc0452 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/upper_word.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/upper_word.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum UpperWordPrefix {} -impl SparseDensePrefix for UpperWordPrefix { +impl SparseDensePrefix for UpperWordPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/xor.rs b/crates/jolt-lookup-tables/src/tables/prefixes/xor.rs index 8b99429628..43936345a9 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/xor.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/xor.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; @@ -6,7 +6,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum XorPrefix {} -impl SparseDensePrefix for XorPrefix { +impl SparseDensePrefix for XorPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/xor_rot.rs b/crates/jolt-lookup-tables/src/tables/prefixes/xor_rot.rs index bacfb2c827..e0d240aacf 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/xor_rot.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/xor_rot.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum XorRotPrefix {} -impl SparseDensePrefix for XorRotPrefix { +impl SparseDensePrefix for XorRotPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/prefixes/xor_rotw.rs b/crates/jolt-lookup-tables/src/tables/prefixes/xor_rotw.rs index a4d87570b8..dde995c93e 100644 --- a/crates/jolt-lookup-tables/src/tables/prefixes/xor_rotw.rs +++ b/crates/jolt-lookup-tables/src/tables/prefixes/xor_rotw.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use crate::lookup_bits::LookupBits; use crate::XLEN; @@ -7,7 +7,7 @@ use super::{PrefixEval, Prefixes, SparseDensePrefix}; pub enum XorRotWPrefix {} -impl SparseDensePrefix for XorRotWPrefix { +impl SparseDensePrefix for XorRotWPrefix { fn default_checkpoint() -> F { F::zero() } diff --git a/crates/jolt-lookup-tables/src/tables/range_check.rs b/crates/jolt-lookup-tables/src/tables/range_check.rs index 3124485b88..93b11b2f85 100644 --- a/crates/jolt-lookup-tables/src/tables/range_check.rs +++ b/crates/jolt-lookup-tables/src/tables/range_check.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -18,7 +18,7 @@ impl LookupTable for RangeCheckTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let mut result = F::zero(); @@ -40,7 +40,7 @@ impl PrefixSuffixDecomposition for RangeCheckTable(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { let [one, lower_word] = suffixes.try_into().unwrap(); prefixes[Prefixes::LowerWord] * one + lower_word } diff --git a/crates/jolt-lookup-tables/src/tables/range_check_aligned.rs b/crates/jolt-lookup-tables/src/tables/range_check_aligned.rs index dfd1615435..c8fc20e7e8 100644 --- a/crates/jolt-lookup-tables/src/tables/range_check_aligned.rs +++ b/crates/jolt-lookup-tables/src/tables/range_check_aligned.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -18,7 +18,7 @@ impl LookupTable for RangeCheckAlignedTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let mut result = F::zero(); @@ -41,7 +41,7 @@ impl PrefixSuffixDecomposition for RangeCheckAlignedTab } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { let [one, lower_word, lsb] = suffixes.try_into().unwrap(); let lower_word_contribution = prefixes[Prefixes::LowerWord] * one + lower_word; let lsb_contribution = prefixes[Prefixes::Lsb] * lsb; diff --git a/crates/jolt-lookup-tables/src/tables/shift_right_bitmask.rs b/crates/jolt-lookup-tables/src/tables/shift_right_bitmask.rs index e31fabc6de..be533dd3aa 100644 --- a/crates/jolt-lookup-tables/src/tables/shift_right_bitmask.rs +++ b/crates/jolt-lookup-tables/src/tables/shift_right_bitmask.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -20,7 +20,7 @@ impl LookupTable for ShiftRightBitmaskTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let log_w = XLEN.trailing_zeros() as usize; @@ -54,7 +54,7 @@ impl PrefixSuffixDecomposition for ShiftRightBitmaskTab } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, pow2] = suffixes.try_into().unwrap(); F::from_u128(1 << XLEN) * one - prefixes[Prefixes::Pow2] * pow2 diff --git a/crates/jolt-lookup-tables/src/tables/sign_extend_half_word.rs b/crates/jolt-lookup-tables/src/tables/sign_extend_half_word.rs index 9809194456..effc84b476 100644 --- a/crates/jolt-lookup-tables/src/tables/sign_extend_half_word.rs +++ b/crates/jolt-lookup-tables/src/tables/sign_extend_half_word.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -28,7 +28,7 @@ impl LookupTable for SignExtendHalfWordTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let half_word_size = XLEN / 2; @@ -63,7 +63,7 @@ impl PrefixSuffixDecomposition for SignExtendHalfWordTa } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { let [one, lower_half_word, sign_extension_upper_half] = suffixes.try_into().unwrap(); prefixes[Prefixes::LowerHalfWord] * one + lower_half_word diff --git a/crates/jolt-lookup-tables/src/tables/sign_mask.rs b/crates/jolt-lookup-tables/src/tables/sign_mask.rs index 34b2ef53f5..d9de418f33 100644 --- a/crates/jolt-lookup-tables/src/tables/sign_mask.rs +++ b/crates/jolt-lookup-tables/src/tables/sign_mask.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -25,7 +25,7 @@ impl LookupTable for SignMaskTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let sign_bit = r[0]; @@ -44,7 +44,7 @@ impl PrefixSuffixDecomposition for SignMaskTable } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { let [one] = suffixes.try_into().unwrap(); let ones: u64 = ((1u128 << XLEN) - 1) as u64; F::from_u64(ones) * prefixes[Prefixes::LeftOperandMsb] * one diff --git a/crates/jolt-lookup-tables/src/tables/signed_greater_than_equal.rs b/crates/jolt-lookup-tables/src/tables/signed_greater_than_equal.rs index 8f91996d47..c3741f6d85 100644 --- a/crates/jolt-lookup-tables/src/tables/signed_greater_than_equal.rs +++ b/crates/jolt-lookup-tables/src/tables/signed_greater_than_equal.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -25,7 +25,7 @@ impl LookupTable for SignedGreaterThanEqualTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { F::one() - SignedLessThanTable::.evaluate_mle(r) } @@ -46,7 +46,7 @@ impl PrefixSuffixDecomposition for SignedGreaterThanEqu } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, less_than] = suffixes.try_into().unwrap(); // 1 - LT(x, y) = 1 - (isNegative(x) && isPositive(y)) - LTU(x, y) diff --git a/crates/jolt-lookup-tables/src/tables/signed_less_than.rs b/crates/jolt-lookup-tables/src/tables/signed_less_than.rs index 397e4f5054..374ba2a668 100644 --- a/crates/jolt-lookup-tables/src/tables/signed_less_than.rs +++ b/crates/jolt-lookup-tables/src/tables/signed_less_than.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -24,7 +24,7 @@ impl LookupTable for SignedLessThanTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { let x_sign = r[0]; let y_sign = r[1]; @@ -57,7 +57,7 @@ impl PrefixSuffixDecomposition for SignedLessThanTable< } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, less_than] = suffixes.try_into().unwrap(); prefixes[Prefixes::LeftOperandMsb] * one - prefixes[Prefixes::RightOperandMsb] * one diff --git a/crates/jolt-lookup-tables/src/tables/suffixes/mod.rs b/crates/jolt-lookup-tables/src/tables/suffixes/mod.rs index 7341bdb8cd..cbbd882dde 100644 --- a/crates/jolt-lookup-tables/src/tables/suffixes/mod.rs +++ b/crates/jolt-lookup-tables/src/tables/suffixes/mod.rs @@ -86,7 +86,7 @@ use xor::XorSuffix; use xor_rot::XorRotSuffix; use xor_rotw::XorRotWSuffix; -use jolt_field::Field; +use jolt_field::JoltField; /// A suffix polynomial: evaluates on unbound Boolean variables during sumcheck. /// @@ -228,7 +228,7 @@ impl Suffixes { /// Evaluate and promote to a field element. #[inline] - pub fn evaluate(&self, b: LookupBits) -> SuffixEval { + pub fn evaluate(&self, b: LookupBits) -> SuffixEval { F::from_u64(self.suffix_mle(b)) } } diff --git a/crates/jolt-lookup-tables/src/tables/test_utils.rs b/crates/jolt-lookup-tables/src/tables/test_utils.rs index 16b7db8b49..5dcc55a0d0 100644 --- a/crates/jolt-lookup-tables/src/tables/test_utils.rs +++ b/crates/jolt-lookup-tables/src/tables/test_utils.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use rand::prelude::*; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -9,7 +9,10 @@ use crate::tables::suffixes::SuffixEval; use crate::tables::PrefixSuffixDecomposition; use crate::traits::LookupTable; -pub fn index_to_field_bitvector>(value: u128, bits: usize) -> Vec { +pub fn index_to_field_bitvector>( + value: u128, + bits: usize, +) -> Vec { if bits != 128 { assert!(value < 1u128 << bits); } @@ -40,7 +43,7 @@ pub fn gen_bitmask_lookup_index(rng: &mut StdRng) -> u128 { /// has `2^16` entries. pub fn mle_full_hypercube_test() where - F: Field + FieldOps + ChallengeOps, + F: JoltField + FieldOps + ChallengeOps, T: LookupTable + Default, { assert!( @@ -59,7 +62,7 @@ where pub fn mle_random_test() where - F: Field + FieldOps + ChallengeOps, + F: JoltField + FieldOps + ChallengeOps, T: LookupTable + Default, { let mut rng = StdRng::seed_from_u64(12345); @@ -87,7 +90,7 @@ where /// corresponding (partially random) evaluation point. pub fn prefix_suffix_test() where - F: Field + FieldOps + ChallengeOps, + F: JoltField + FieldOps + ChallengeOps, T: PrefixSuffixDecomposition, { prefix_suffix_materialization_test::(16, 3); @@ -98,7 +101,7 @@ fn prefix_suffix_materialization_test( rounds_per_phase: usize, num_runs: usize, ) where - F: Field + FieldOps + ChallengeOps, + F: JoltField + FieldOps + ChallengeOps, T: PrefixSuffixDecomposition, { let total_bits = XLEN * 2; @@ -215,7 +218,7 @@ fn prefix_suffix_materialization_test( } } -fn format_prefix_evals(evals: &[PrefixEval]) -> String { +fn format_prefix_evals(evals: &[PrefixEval]) -> String { use std::fmt::Write; let mut out = String::new(); diff --git a/crates/jolt-lookup-tables/src/tables/unsigned_greater_than_equal.rs b/crates/jolt-lookup-tables/src/tables/unsigned_greater_than_equal.rs index 79a599d20c..8ce47d2c57 100644 --- a/crates/jolt-lookup-tables/src/tables/unsigned_greater_than_equal.rs +++ b/crates/jolt-lookup-tables/src/tables/unsigned_greater_than_equal.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -21,7 +21,7 @@ impl LookupTable for UnsignedGreaterThanEqualTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { F::one() - UnsignedLessThanTable::.evaluate_mle::(r) } @@ -37,7 +37,7 @@ impl PrefixSuffixDecomposition for UnsignedGreaterThanE } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, less_than] = suffixes.try_into().unwrap(); // 1 - LTU(x, y) diff --git a/crates/jolt-lookup-tables/src/tables/unsigned_less_than.rs b/crates/jolt-lookup-tables/src/tables/unsigned_less_than.rs index 7e04803177..d6e7a94386 100644 --- a/crates/jolt-lookup-tables/src/tables/unsigned_less_than.rs +++ b/crates/jolt-lookup-tables/src/tables/unsigned_less_than.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -20,7 +20,7 @@ impl LookupTable for UnsignedLessThanTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); @@ -47,7 +47,7 @@ impl PrefixSuffixDecomposition for UnsignedLessThanTabl } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, less_than] = suffixes.try_into().unwrap(); prefixes[Prefixes::LessThan] * one + prefixes[Prefixes::Eq] * less_than diff --git a/crates/jolt-lookup-tables/src/tables/unsigned_less_than_equal.rs b/crates/jolt-lookup-tables/src/tables/unsigned_less_than_equal.rs index 285422771c..7f1caf7c3d 100644 --- a/crates/jolt-lookup-tables/src/tables/unsigned_less_than_equal.rs +++ b/crates/jolt-lookup-tables/src/tables/unsigned_less_than_equal.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -20,7 +20,7 @@ impl LookupTable for UnsignedLessThanEqualTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); @@ -47,7 +47,7 @@ impl PrefixSuffixDecomposition for UnsignedLessThanEqua } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, less_than, eq] = suffixes.try_into().unwrap(); // LT(x, y) + EQ(x, y) diff --git a/crates/jolt-lookup-tables/src/tables/upper_word.rs b/crates/jolt-lookup-tables/src/tables/upper_word.rs index 302ce08637..7f1afc3095 100644 --- a/crates/jolt-lookup-tables/src/tables/upper_word.rs +++ b/crates/jolt-lookup-tables/src/tables/upper_word.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -18,7 +18,7 @@ impl LookupTable for UpperWordTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let mut result = F::zero(); @@ -39,7 +39,7 @@ impl PrefixSuffixDecomposition for UpperWordTable } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { let [one, upper_word] = suffixes.try_into().unwrap(); prefixes[Prefixes::UpperWord] * one + upper_word } diff --git a/crates/jolt-lookup-tables/src/tables/valid_div0.rs b/crates/jolt-lookup-tables/src/tables/valid_div0.rs index 0489bda202..0af4bd4aec 100644 --- a/crates/jolt-lookup-tables/src/tables/valid_div0.rs +++ b/crates/jolt-lookup-tables/src/tables/valid_div0.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -26,7 +26,7 @@ impl LookupTable for ValidDiv0Table { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { let mut divisor_is_zero = F::one(); let mut is_valid_div_by_zero = F::one(); @@ -56,7 +56,7 @@ impl PrefixSuffixDecomposition for ValidDiv0Table } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, left_operand_is_zero, div_by_zero] = suffixes.try_into().unwrap(); one - prefixes[Prefixes::LeftOperandIsZero] * left_operand_is_zero diff --git a/crates/jolt-lookup-tables/src/tables/valid_unsigned_remainder.rs b/crates/jolt-lookup-tables/src/tables/valid_unsigned_remainder.rs index aa4bc1516b..ab811ba488 100644 --- a/crates/jolt-lookup-tables/src/tables/valid_unsigned_remainder.rs +++ b/crates/jolt-lookup-tables/src/tables/valid_unsigned_remainder.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -20,7 +20,7 @@ impl LookupTable for ValidUnsignedRemainderTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { let mut divisor_is_zero = F::one(); let mut lt = F::zero(); @@ -56,7 +56,7 @@ impl PrefixSuffixDecomposition for ValidUnsignedRemaind } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, less_than, right_operand_is_zero] = suffixes.try_into().unwrap(); prefixes[Prefixes::RightOperandIsZero] * right_operand_is_zero diff --git a/crates/jolt-lookup-tables/src/tables/virtual_change_divisor.rs b/crates/jolt-lookup-tables/src/tables/virtual_change_divisor.rs index bac218227d..f59ad3ceaf 100644 --- a/crates/jolt-lookup-tables/src/tables/virtual_change_divisor.rs +++ b/crates/jolt-lookup-tables/src/tables/virtual_change_divisor.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -29,7 +29,7 @@ impl LookupTable for VirtualChangeDivisorTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); @@ -70,7 +70,7 @@ impl PrefixSuffixDecomposition for VirtualChangeDivisor } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, right_operand, change_divisor] = suffixes.try_into().unwrap(); diff --git a/crates/jolt-lookup-tables/src/tables/virtual_change_divisor_w.rs b/crates/jolt-lookup-tables/src/tables/virtual_change_divisor_w.rs index 8a27bc6c4b..182ad179c5 100644 --- a/crates/jolt-lookup-tables/src/tables/virtual_change_divisor_w.rs +++ b/crates/jolt-lookup-tables/src/tables/virtual_change_divisor_w.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -36,7 +36,7 @@ impl LookupTable for VirtualChangeDivisorWTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); @@ -85,7 +85,7 @@ impl PrefixSuffixDecomposition for VirtualChangeDivisor } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, right_operand_w, change_divisor_w, sign_extension] = suffixes.try_into().unwrap(); prefixes[Prefixes::RightOperandW] * one diff --git a/crates/jolt-lookup-tables/src/tables/virtual_rev8w.rs b/crates/jolt-lookup-tables/src/tables/virtual_rev8w.rs index 3b75bf6396..aea4498584 100644 --- a/crates/jolt-lookup-tables/src/tables/virtual_rev8w.rs +++ b/crates/jolt-lookup-tables/src/tables/virtual_rev8w.rs @@ -1,7 +1,7 @@ use std::array; use std::iter; -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -28,7 +28,7 @@ impl LookupTable for VirtualRev8WTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { let mut bits = r.iter().rev(); let mut bytes = iter::from_fn(|| { @@ -61,7 +61,7 @@ impl PrefixSuffixDecomposition for VirtualRev8WTable(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { let [one, rev8w] = suffixes.try_into().unwrap(); prefixes[Prefixes::Rev8W] * one + rev8w } diff --git a/crates/jolt-lookup-tables/src/tables/virtual_rotr.rs b/crates/jolt-lookup-tables/src/tables/virtual_rotr.rs index f0d1776ad7..e7322a59d0 100644 --- a/crates/jolt-lookup-tables/src/tables/virtual_rotr.rs +++ b/crates/jolt-lookup-tables/src/tables/virtual_rotr.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -34,7 +34,7 @@ impl LookupTable for VirtualROTRTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { assert_eq!(r.len() % 2, 0, "r must have even length"); assert_eq!(r.len() / 2, XLEN, "r must have length 2 * XLEN"); @@ -79,7 +79,7 @@ impl PrefixSuffixDecomposition for VirtualROTRTable(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [right_shift_helper, right_shift, left_shift, one] = suffixes.try_into().unwrap(); prefixes[Prefixes::RightShift] * right_shift_helper diff --git a/crates/jolt-lookup-tables/src/tables/virtual_rotrw.rs b/crates/jolt-lookup-tables/src/tables/virtual_rotrw.rs index 43d2c48528..7b4e34b9fb 100644 --- a/crates/jolt-lookup-tables/src/tables/virtual_rotrw.rs +++ b/crates/jolt-lookup-tables/src/tables/virtual_rotrw.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -34,7 +34,7 @@ impl LookupTable for VirtualROTRWTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { assert_eq!(r.len() % 2, 0, "r must have even length"); assert_eq!(r.len() / 2, XLEN, "r must have length 2 * XLEN"); @@ -79,7 +79,7 @@ impl PrefixSuffixDecomposition for VirtualROTRWTable(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [right_shift_w_helper, right_shift_w, left_shift_w, one] = suffixes.try_into().unwrap(); prefixes[Prefixes::RightShiftW] * right_shift_w_helper diff --git a/crates/jolt-lookup-tables/src/tables/virtual_sra.rs b/crates/jolt-lookup-tables/src/tables/virtual_sra.rs index f0a39f5f59..9f271b763e 100644 --- a/crates/jolt-lookup-tables/src/tables/virtual_sra.rs +++ b/crates/jolt-lookup-tables/src/tables/virtual_sra.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -36,7 +36,7 @@ impl LookupTable for VirtualSRATable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let mut result = F::zero(); @@ -73,7 +73,7 @@ impl PrefixSuffixDecomposition for VirtualSRATable(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, right_shift, right_shift_helper, sign_extension] = suffixes.try_into().unwrap(); prefixes[Prefixes::RightShift] * right_shift_helper diff --git a/crates/jolt-lookup-tables/src/tables/virtual_srl.rs b/crates/jolt-lookup-tables/src/tables/virtual_srl.rs index 91ffe43a9a..337f844085 100644 --- a/crates/jolt-lookup-tables/src/tables/virtual_srl.rs +++ b/crates/jolt-lookup-tables/src/tables/virtual_srl.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -31,7 +31,7 @@ impl LookupTable for VirtualSRLTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let mut result = F::zero(); @@ -55,7 +55,7 @@ impl PrefixSuffixDecomposition for VirtualSRLTable(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [right_shift, right_shift_helper] = suffixes.try_into().unwrap(); prefixes[Prefixes::RightShift] * right_shift_helper + right_shift diff --git a/crates/jolt-lookup-tables/src/tables/virtual_xor_rot.rs b/crates/jolt-lookup-tables/src/tables/virtual_xor_rot.rs index 226c888f60..ee53ec837d 100644 --- a/crates/jolt-lookup-tables/src/tables/virtual_xor_rot.rs +++ b/crates/jolt-lookup-tables/src/tables/virtual_xor_rot.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -24,7 +24,7 @@ impl LookupTable for VirtualXORROTTable< fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let mut result = F::zero(); @@ -65,7 +65,7 @@ impl PrefixSuffixDecomposition } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(XLEN, 64); debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, xor_rot] = suffixes.try_into().unwrap(); diff --git a/crates/jolt-lookup-tables/src/tables/virtual_xor_rotw.rs b/crates/jolt-lookup-tables/src/tables/virtual_xor_rotw.rs index 37888914b1..a8be123932 100644 --- a/crates/jolt-lookup-tables/src/tables/virtual_xor_rotw.rs +++ b/crates/jolt-lookup-tables/src/tables/virtual_xor_rotw.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -25,7 +25,7 @@ impl LookupTable for VirtualXORROTWTable fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let mut result = F::zero(); @@ -67,7 +67,7 @@ impl PrefixSuffixDecomposition } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(XLEN, 64); debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [one, xor_rot] = suffixes.try_into().unwrap(); diff --git a/crates/jolt-lookup-tables/src/tables/word_alignment.rs b/crates/jolt-lookup-tables/src/tables/word_alignment.rs index 63ee4378a4..4af8a28ab7 100644 --- a/crates/jolt-lookup-tables/src/tables/word_alignment.rs +++ b/crates/jolt-lookup-tables/src/tables/word_alignment.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -18,7 +18,7 @@ impl LookupTable for WordAlignmentTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { let lsb0 = r[r.len() - 1]; let lsb1 = r[r.len() - 2]; @@ -36,7 +36,7 @@ impl PrefixSuffixDecomposition for WordAlignmentTable(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { debug_assert_eq!(self.suffixes().len(), suffixes.len()); let [two_lsb] = suffixes.try_into().unwrap(); prefixes[Prefixes::TwoLsb] * two_lsb diff --git a/crates/jolt-lookup-tables/src/tables/xor.rs b/crates/jolt-lookup-tables/src/tables/xor.rs index 5a4b77fc5f..e6ae3feed7 100644 --- a/crates/jolt-lookup-tables/src/tables/xor.rs +++ b/crates/jolt-lookup-tables/src/tables/xor.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -20,7 +20,7 @@ impl LookupTable for XorTable { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps, + F: JoltField + FieldOps, { debug_assert_eq!(r.len(), 2 * XLEN); let mut result = F::zero(); @@ -44,7 +44,7 @@ impl PrefixSuffixDecomposition for XorTable { } #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { + fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { let [one, xor] = suffixes.try_into().unwrap(); prefixes[Prefixes::Xor] * one + xor } diff --git a/crates/jolt-lookup-tables/src/traits.rs b/crates/jolt-lookup-tables/src/traits.rs index 1c018d3cb0..5ade2a8fec 100644 --- a/crates/jolt-lookup-tables/src/traits.rs +++ b/crates/jolt-lookup-tables/src/traits.rs @@ -1,6 +1,6 @@ //! Lookup-table-related traits. -use jolt_field::Field; +use jolt_field::JoltField; #[cfg(feature = "field-inline")] use jolt_riscv::instructions::{ FieldAdd, FieldAssertEq, FieldInv, FieldLoadFromX, FieldLoadImm, FieldMul, FieldStoreToX, @@ -20,7 +20,7 @@ pub trait LookupTable: Clone + Debug + Send + Sync { fn evaluate_mle(&self, r: &[C]) -> F where C: ChallengeOps, - F: Field + FieldOps; + F: JoltField + FieldOps; } /// Maps an instruction to the lookup table it decomposes into for the proving system. diff --git a/crates/jolt-openings/src/claims.rs b/crates/jolt-openings/src/claims.rs index 46f715d49a..644727ed44 100644 --- a/crates/jolt-openings/src/claims.rs +++ b/crates/jolt-openings/src/claims.rs @@ -1,6 +1,6 @@ //! Stateless claim types for PCS operations. -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{Point, HIGH_TO_LOW}; use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript}; use serde::{Deserialize, Serialize}; @@ -22,7 +22,7 @@ impl EvaluationClaim { impl AppendToTranscript for EvaluationClaim where - F: Field, + F: JoltField, { fn append_to_transcript(&self, transcript: &mut T) { transcript.append(&LabelWithCount(b"opening_point", self.point.len() as u64)); @@ -51,7 +51,7 @@ impl<'a, F, C> ZkEvaluationClaim<'a, F, C> { impl AppendToTranscript for ZkEvaluationClaim<'_, F, C> where - F: Field, + F: JoltField, C: AppendToTranscript, { fn append_to_transcript(&self, transcript: &mut T) { @@ -69,16 +69,16 @@ where /// Verifier-side opening claim: commitment, point, and claimed value. #[derive(Clone, Debug)] -pub struct VerifierOpeningClaim { +pub struct VerifierOpeningClaim { pub commitment: C, pub evaluation: EvaluationClaim, } -pub(crate) struct VerifierRlcClaims<'a, F: Field, C>(pub &'a [VerifierOpeningClaim]); +pub(crate) struct VerifierRlcClaims<'a, F: JoltField, C>(pub &'a [VerifierOpeningClaim]); impl AppendToTranscript for VerifierRlcClaims<'_, F, C> where - F: Field, + F: JoltField, { fn append_to_transcript(&self, transcript: &mut T) { transcript.append(&LabelWithCount(b"rlc_claims", self.0.len() as u64)); diff --git a/crates/jolt-openings/src/packing.rs b/crates/jolt-openings/src/packing.rs index f6198eef58..33d66ecdbc 100644 --- a/crates/jolt-openings/src/packing.rs +++ b/crates/jolt-openings/src/packing.rs @@ -101,7 +101,7 @@ use std::{ ops::Index, }; -use jolt_field::{Field, FromPrimitiveInt}; +use jolt_field::{JoltField, Ring}; use jolt_poly::{ boolean_bits_msb, eq_index_msb, math::Math, thread::unsafe_allocate_zero_vec, EqPolynomial, MultilinearPoly, Polynomial, @@ -269,7 +269,7 @@ where } /// Forms the physical point `prefix_point || logical_point`. - pub fn pack_point( + pub fn pack_point( &self, prefix_point: &[F], logical_point: &[F], @@ -288,7 +288,7 @@ where } /// Extracts the logical suffix point for `id` from a full packed point. - pub fn logical_point( + pub fn logical_point( &self, id: &Id, packed_point: &[F], @@ -311,7 +311,7 @@ where statement: &'a PrefixPackedStatement, ) -> Result, OpeningsError> where - F: Field, + F: JoltField, Id: Debug, { let claims = statement.claims.as_slice(); @@ -399,7 +399,7 @@ impl PrefixPackedStatement { } } -pub struct PreparedPrefixPackedStatement<'a, F: Field, C> { +pub struct PreparedPrefixPackedStatement<'a, F: JoltField, C> { packed_num_vars: usize, pub(crate) commitment: &'a C, ordered_claims: Vec<(&'a EvaluationClaim, &'a PrefixSlot)>, @@ -407,7 +407,7 @@ pub struct PreparedPrefixPackedStatement<'a, F: Field, C> { impl PreparedPrefixPackedStatement<'_, F, C> where - F: Field, + F: JoltField, { pub fn num_claims(&self) -> usize { self.ordered_claims.len() @@ -461,7 +461,7 @@ where impl AppendToTranscript for PreparedPrefixPackedStatement<'_, F, C> where - F: Field, + F: JoltField, C: AppendToTranscript, { fn append_to_transcript(&self, transcript: &mut T) { @@ -500,7 +500,7 @@ const PARALLEL_MIN_CHUNK: usize = 1 << 12; /// The dense round fold shared by the dense object path and the sparse /// stepper's dense tail: `[Σ s_lo·w_lo, Σ s_hi·w_hi, Σ (s_hi−s_lo)(w_hi−w_lo)]`. -fn dense_round_evaluations( +fn dense_round_evaluations( selector_low: &[F], selector_high: &[F], witness_low: &[F], @@ -541,7 +541,7 @@ struct SparseSelectorSlot<'a, F> { point: &'a [F], } -impl<'a, F: Field> SparseSelectorSlot<'a, F> { +impl<'a, F: JoltField> SparseSelectorSlot<'a, F> { fn new(alpha: F, slot: &'a PrefixSlot, point: &'a [F]) -> Self { Self { scalar: alpha, @@ -587,7 +587,7 @@ struct RoundSelectorSlot { low: Vec, } -impl RoundSelectorSlot { +impl RoundSelectorSlot { fn new(slot: &SparseSelectorSlot<'_, F>, bound: usize, remaining_vars: usize) -> Self { let (block, point) = slot.remaining(bound, remaining_vars); let low_vars = point.len() / 2; @@ -613,7 +613,7 @@ struct GroupedRoundSelector<'a, F> { slots: &'a [RoundSelectorSlot], } -impl<'a, F: Field> GroupedRoundSelector<'a, F> { +impl<'a, F: JoltField> GroupedRoundSelector<'a, F> { fn new(slots: &'a [RoundSelectorSlot], remaining_vars: usize) -> Self { let mut groups: Vec<(usize, Vec>)> = Vec::new(); for (slot_index, slot) in slots.iter().enumerate() { @@ -649,7 +649,7 @@ impl<'a, F: Field> GroupedRoundSelector<'a, F> { /// Dense remaining-domain selector and witness tables for the delegated tail /// rounds of the sparse stepper. -fn materialize_remaining( +fn materialize_remaining( slots: &[SparseSelectorSlot<'_, F>], positions: &[usize], bound_weights: &[F], @@ -685,7 +685,7 @@ fn materialize_remaining( /// the number of one-positions, the (now small) selector/witness tables are /// materialized and later rounds run dense. The check happens at the start /// of `round_evaluations`. -struct SparseReductionInstance<'a, F: Field> { +struct SparseReductionInstance<'a, F: JoltField> { slots: Vec>, positions: Vec, /// Bound challenges (msb-first). A position's accumulated weight is @@ -699,7 +699,7 @@ struct SparseReductionInstance<'a, F: Field> { dense: Option<(Polynomial, Polynomial)>, } -impl<'a, F: Field> SparseReductionInstance<'a, F> { +impl<'a, F: JoltField> SparseReductionInstance<'a, F> { #[tracing::instrument( skip_all, name = "SparseReductionInstance::new", @@ -942,7 +942,7 @@ fn packed_opening_challenges( transcript: &mut T, ) -> (Vec>, Vec) where - F: Field, + F: JoltField, C: AppendToTranscript, T: Transcript, { @@ -1005,14 +1005,14 @@ where // (one-hot) witnesses — same field values, no `2^n` materialization — // plus the rounds to wait before the object's variables bind and its // total `Σ_z E(z)·W(z)` (the constant while padded). - enum ObjectState<'a, F: Field> { + enum ObjectState<'a, F: JoltField> { Dense { selector: Polynomial, witness: Polynomial, }, Sparse(SparseReductionInstance<'a, F>), } - struct ObjectProver<'a, F: Field> { + struct ObjectProver<'a, F: JoltField> { state: ObjectState<'a, F>, padding_rounds: usize, total: F, @@ -1292,7 +1292,7 @@ fn verify_reduction_sumcheck( transcript: &mut T, ) -> Result<(Vec, F), OpeningsError> where - F: Field, + F: JoltField, T: Transcript, { if round_polynomials.len() != num_rounds { @@ -1319,7 +1319,7 @@ where fn append_round_polynomial(coefficients: &[F; 3], transcript: &mut T) where - F: Field, + F: JoltField, T: Transcript, { transcript.append(&Label(b"packed_reduction_round")); @@ -1341,7 +1341,7 @@ fn coefficient_count(num_vars: usize) -> Result { #[expect(clippy::unwrap_used)] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn field(value: u64) -> Fr { Fr::from_u64(value) diff --git a/crates/jolt-openings/src/schemes.rs b/crates/jolt-openings/src/schemes.rs index d9433625a8..9a6a6efaf5 100644 --- a/crates/jolt-openings/src/schemes.rs +++ b/crates/jolt-openings/src/schemes.rs @@ -9,7 +9,7 @@ use std::{fmt::Debug, marker::PhantomData}; use jolt_crypto::{Commitment, HomomorphicCommitment}; -use jolt_field::{Field, FromPrimitiveInt}; +use jolt_field::{JoltField, Ring}; use jolt_poly::{MultilinearPoly, Point, RlcSource, HIGH_TO_LOW}; use jolt_transcript::{AppendToTranscript, Transcript}; use serde::{de::DeserializeOwned, Serialize}; @@ -41,7 +41,7 @@ pub trait GroupSetupMetadata { /// Commit to f: F^n -> F, then prove f(r) = v for verifier-chosen r. pub trait CommitmentScheme: Commitment { - type Field: Field; + type Field: JoltField; type Proof: Clone + Debug + Eq + Send + Sync + 'static + Serialize + DeserializeOwned; type ProverSetup: Clone + Send + Sync; type VerifierSetup: Clone + Send + Sync + Serialize + DeserializeOwned; @@ -164,7 +164,7 @@ pub trait StreamingCommitment: CommitmentScheme { let values: Vec = chunk .iter() .copied() - .map(::from_u64) + .map(::from_u64) .collect(); Self::feed(partial, &values, setup); } @@ -173,7 +173,7 @@ pub trait StreamingCommitment: CommitmentScheme { let values: Vec = chunk .iter() .copied() - .map(::from_i128) + .map(::from_i128) .collect(); Self::feed(partial, &values, setup); } @@ -291,7 +291,7 @@ pub trait ZkStreamingCommitment: StreamingCommitment + ZkOpeningScheme { /// - [`Hints`](Self::Hints) are the commit-time auxiliary data /// ([`CommitmentScheme::OpeningHint`]) the PCS reuses when opening. pub trait BatchOpeningScheme { - type Field: Field; + type Field: JoltField; type ProverSetup; type VerifierSetup; /// Public opening claims plus the commitments they refer to. @@ -477,14 +477,14 @@ where } } -struct HomomorphicBatchStatement<'a, F: Field, C> { +struct HomomorphicBatchStatement<'a, F: JoltField, C> { claims: &'a [VerifierOpeningClaim], point: Point, } impl<'a, F, C> HomomorphicBatchStatement<'a, F, C> where - F: Field, + F: JoltField, C: Clone, { fn new(claims: &'a [VerifierOpeningClaim]) -> Result { @@ -522,7 +522,7 @@ where impl AppendToTranscript for HomomorphicBatchStatement<'_, F, C> where - F: Field, + F: JoltField, { fn append_to_transcript(&self, transcript: &mut T) { VerifierRlcClaims(self.claims).append_to_transcript(transcript); diff --git a/crates/jolt-openings/tests/packing.rs b/crates/jolt-openings/tests/packing.rs index d5c977412e..fb75d15ae2 100644 --- a/crates/jolt-openings/tests/packing.rs +++ b/crates/jolt-openings/tests/packing.rs @@ -1,6 +1,6 @@ #![expect(clippy::expect_used, reason = "tests may panic on assertion failures")] -use jolt_field::{FieldCore, Fr}; +use jolt_field::{Field, Fr}; use jolt_openings::{OpeningsError, PrefixPacking}; use jolt_poly::{boolean_point_msb, eq_index_msb, Polynomial}; use rand_chacha::ChaCha20Rng; diff --git a/crates/jolt-openings/tests/support/common.rs b/crates/jolt-openings/tests/support/common.rs index 4f8af2a95a..e01a4359bd 100644 --- a/crates/jolt-openings/tests/support/common.rs +++ b/crates/jolt-openings/tests/support/common.rs @@ -1,5 +1,5 @@ use jolt_crypto::Bn254; -use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{Field, Fr, Ring}; use jolt_hyperkzg::{HyperKZGProverSetup, HyperKZGScheme, HyperKZGVerifierSetup}; use jolt_openings::{CommitmentScheme, EvaluationClaim, VerifierOpeningClaim}; use jolt_poly::{MultilinearPoly, Point, Polynomial, HIGH_TO_LOW}; diff --git a/crates/jolt-openings/tests/support/mock.rs b/crates/jolt-openings/tests/support/mock.rs index 23abc7f12b..832bb72271 100644 --- a/crates/jolt-openings/tests/support/mock.rs +++ b/crates/jolt-openings/tests/support/mock.rs @@ -1,7 +1,7 @@ use std::marker::PhantomData; use jolt_crypto::{Commitment, HomomorphicCommitment}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, OpeningsError, ZkOpeningScheme}; use jolt_poly::{MultilinearPoly, Polynomial}; use jolt_transcript::{AppendToTranscript, Transcript}; @@ -9,15 +9,15 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound = "")] -pub struct MockCommitmentScheme(PhantomData); +pub struct MockCommitmentScheme(PhantomData); #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: DeserializeOwned"))] -pub struct MockCommitment { +pub struct MockCommitment { evaluations: Vec, } -impl Default for MockCommitment { +impl Default for MockCommitment { fn default() -> Self { Self { evaluations: Vec::new(), @@ -27,11 +27,11 @@ impl Default for MockCommitment { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: DeserializeOwned"))] -pub struct MockProof { +pub struct MockProof { evaluations: Vec, } -impl AppendToTranscript for MockCommitment { +impl AppendToTranscript for MockCommitment { fn append_to_transcript(&self, transcript: &mut T) { for evaluation in &self.evaluations { evaluation.append_to_transcript(transcript); @@ -41,14 +41,14 @@ impl AppendToTranscript for MockCommitment { impl Commitment for MockCommitmentScheme where - F: Field + Serialize + DeserializeOwned, + F: JoltField + Serialize + DeserializeOwned, { type Output = MockCommitment; } impl CommitmentScheme for MockCommitmentScheme where - F: Field + Serialize + DeserializeOwned, + F: JoltField + Serialize + DeserializeOwned, { type Field = F; type Proof = MockProof; @@ -102,7 +102,7 @@ where } } -impl HomomorphicCommitment for MockCommitment { +impl HomomorphicCommitment for MockCommitment { fn add(c1: &Self, c2: &Self) -> Self { Self::linear_combine(c1, c2, &F::one()) } @@ -123,7 +123,7 @@ impl HomomorphicCommitment for MockCommitment { impl AdditivelyHomomorphic for MockCommitmentScheme where - F: Field + Serialize + DeserializeOwned, + F: JoltField + Serialize + DeserializeOwned, { fn combine(commitments: &[Self::Output], scalars: &[Self::Field]) -> Self::Output { assert_eq!(commitments.len(), scalars.len()); @@ -142,11 +142,11 @@ where #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: DeserializeOwned"))] -pub struct MockHidingCommitment { +pub struct MockHidingCommitment { pub eval: F, } -impl AppendToTranscript for MockHidingCommitment { +impl AppendToTranscript for MockHidingCommitment { fn append_to_transcript(&self, transcript: &mut T) { self.eval.append_to_transcript(transcript); } @@ -154,7 +154,7 @@ impl AppendToTranscript for MockHidingCommitment { impl ZkOpeningScheme for MockCommitmentScheme where - F: Field + Serialize + DeserializeOwned, + F: JoltField + Serialize + DeserializeOwned, { type HidingCommitment = MockHidingCommitment; type Blind = (); diff --git a/crates/jolt-openings/tests/support/packed.rs b/crates/jolt-openings/tests/support/packed.rs index ddfd5a8647..f5ac7f9cb6 100644 --- a/crates/jolt-openings/tests/support/packed.rs +++ b/crates/jolt-openings/tests/support/packed.rs @@ -1,4 +1,4 @@ -use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{Field, Fr, Ring}; use jolt_openings::{EvaluationClaim, OpeningsError, PrefixPacking}; use jolt_poly::Polynomial; use rand_chacha::ChaCha20Rng; @@ -72,7 +72,7 @@ pub fn independent_claims( .collect() } -use jolt_field::Field; +use jolt_field::JoltField; #[derive(Clone, Debug, PartialEq, Eq)] pub struct MaterializedPackedWitness { @@ -85,7 +85,7 @@ pub fn materialize_packed( ) -> Result, OpeningsError> where Id: Clone + Ord, - F: Field, + F: JoltField, { let packing = PrefixPacking::new( polynomials diff --git a/crates/jolt-poly/benches/poly_ops.rs b/crates/jolt-poly/benches/poly_ops.rs index f9b089e2ab..a6b61660d3 100644 --- a/crates/jolt-poly/benches/poly_ops.rs +++ b/crates/jolt-poly/benches/poly_ops.rs @@ -1,7 +1,7 @@ #![expect(unused_results)] use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use jolt_field::{FieldCore, Fr}; +use jolt_field::{Field, Fr}; use jolt_poly::{EqPolynomial, Polynomial}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/fuzz/fuzz_targets/dense_poly_ops.rs b/crates/jolt-poly/fuzz/fuzz_targets/dense_poly_ops.rs index a6372ea6a0..88ec7da71f 100644 --- a/crates/jolt-poly/fuzz/fuzz_targets/dense_poly_ops.rs +++ b/crates/jolt-poly/fuzz/fuzz_targets/dense_poly_ops.rs @@ -1,5 +1,5 @@ #![no_main] -use jolt_field::{Fr, CanonicalRepr}; +use jolt_field::{Fr, CanonicalEncoding}; use jolt_poly::Polynomial; use libfuzzer_sys::fuzz_target; @@ -20,7 +20,7 @@ fuzz_target!(|data: &[u8]| { // Build evaluation vector from fuzzer data let evals: Vec = (0..n) - .map(|i| ::from_le_bytes_mod_order(&data[i * 32..(i + 1) * 32])) + .map(|i| ::from_bytes_le_reduced(&data[i * 32..(i + 1) * 32])) .collect(); let poly = Polynomial::new(evals); @@ -28,7 +28,7 @@ fuzz_target!(|data: &[u8]| { let point_start = n * 32; let point: Vec = (0..num_vars) .map(|i| { - ::from_le_bytes_mod_order( + ::from_bytes_le_reduced( &data[point_start + i * 32..point_start + (i + 1) * 32], ) }) diff --git a/crates/jolt-poly/src/compressed_univariate.rs b/crates/jolt-poly/src/compressed_univariate.rs index 0637b89ba8..4fc59cc1e7 100644 --- a/crates/jolt-poly/src/compressed_univariate.rs +++ b/crates/jolt-poly/src/compressed_univariate.rs @@ -3,7 +3,7 @@ //! Used in sumcheck proofs to save one field element per round polynomial. //! The linear term is recoverable from the sumcheck claim `f(0) + f(1)`. -use jolt_field::Field; +use jolt_field::JoltField; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -19,11 +19,11 @@ use crate::univariate::{UnivariatePoly, UnivariatePolynomial}; /// serialization (32 bytes for BN254). #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: DeserializeOwned"))] -pub struct CompressedPoly { +pub struct CompressedPoly { coeffs_except_linear_term: Vec, } -impl UnivariatePolynomial for CompressedPoly { +impl UnivariatePolynomial for CompressedPoly { /// Degree of the polynomial. /// /// A degree-d polynomial has d+1 coefficients; the compressed form stores @@ -33,7 +33,7 @@ impl UnivariatePolynomial for CompressedPoly { } } -impl CompressedPoly { +impl CompressedPoly { /// Creates a compressed polynomial from the stored coefficients `[c0, c2, c3, ...]`. pub fn new(coeffs_except_linear_term: Vec) -> Self { Self { @@ -103,7 +103,7 @@ impl CompressedPoly { #[expect(clippy::unwrap_used)] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use num_traits::{One, Zero}; /// Helper: build a standard polynomial p(x) = c0 + c1*x + c2*x^2 + ... diff --git a/crates/jolt-poly/src/dense.rs b/crates/jolt-poly/src/dense.rs index 6d474ce9ef..a59e89b5fa 100644 --- a/crates/jolt-poly/src/dense.rs +++ b/crates/jolt-poly/src/dense.rs @@ -2,7 +2,7 @@ use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}; -use jolt_field::Field; +use jolt_field::JoltField; use rand_core::RngCore; use serde::{Deserialize, Serialize}; @@ -126,7 +126,7 @@ impl Polynomial { /// /// When `T = F`, the `From` conversion is the identity and the compiler /// eliminates it, making this equivalent to an allocating bind. - pub fn bind_to_field>(&self, scalar: F) -> Polynomial { + pub fn bind_to_field>(&self, scalar: F) -> Polynomial { assert!(self.num_vars > 0, "cannot bind a zero-variable polynomial"); let half = self.evals.len() / 2; let mut result = Vec::with_capacity(half); @@ -142,7 +142,7 @@ impl Polynomial { } } -impl Polynomial { +impl Polynomial { /// Creates the zero polynomial with $2^n$ evaluations all equal to zero. pub fn zeros(num_vars: usize) -> Self { Self { @@ -393,13 +393,13 @@ impl Polynomial { } } -impl From> for Polynomial { +impl From> for Polynomial { fn from(evaluations: Vec) -> Self { Self::new(evaluations) } } -impl crate::MultilinearEvaluation for Polynomial { +impl crate::MultilinearEvaluation for Polynomial { #[inline] fn num_vars(&self) -> usize { self.num_vars @@ -415,14 +415,14 @@ impl crate::MultilinearEvaluation for Polynomial { } } -impl crate::MultilinearBinding for Polynomial { +impl crate::MultilinearBinding for Polynomial { fn bind(&mut self, scalar: F) { Polynomial::bind(self, scalar); } } #[inline] -fn assert_matching_dims(a: &Polynomial, b: &Polynomial) -> (usize, usize) { +fn assert_matching_dims(a: &Polynomial, b: &Polynomial) -> (usize, usize) { assert_eq!( a.num_vars, b.num_vars, "num_vars mismatch: {} vs {}", @@ -431,7 +431,7 @@ fn assert_matching_dims(a: &Polynomial, b: &Polynomial) -> (usiz (a.num_vars, a.evals.len()) } -impl Add for Polynomial { +impl Add for Polynomial { type Output = Self; fn add(mut self, rhs: Self) -> Self { @@ -440,7 +440,7 @@ impl Add for Polynomial { } } -impl Add<&Self> for Polynomial { +impl Add<&Self> for Polynomial { type Output = Self; fn add(mut self, rhs: &Self) -> Self { @@ -449,13 +449,13 @@ impl Add<&Self> for Polynomial { } } -impl AddAssign for Polynomial { +impl AddAssign for Polynomial { fn add_assign(&mut self, rhs: Self) { *self += &rhs; } } -impl AddAssign<&Self> for Polynomial { +impl AddAssign<&Self> for Polynomial { fn add_assign(&mut self, rhs: &Self) { let (_nv, len) = assert_matching_dims(self, rhs); @@ -477,7 +477,7 @@ impl AddAssign<&Self> for Polynomial { } } -impl Sub for Polynomial { +impl Sub for Polynomial { type Output = Self; fn sub(mut self, rhs: Self) -> Self { @@ -486,7 +486,7 @@ impl Sub for Polynomial { } } -impl Sub<&Self> for Polynomial { +impl Sub<&Self> for Polynomial { type Output = Self; fn sub(mut self, rhs: &Self) -> Self { @@ -495,13 +495,13 @@ impl Sub<&Self> for Polynomial { } } -impl SubAssign for Polynomial { +impl SubAssign for Polynomial { fn sub_assign(&mut self, rhs: Self) { *self -= &rhs; } } -impl SubAssign<&Self> for Polynomial { +impl SubAssign<&Self> for Polynomial { fn sub_assign(&mut self, rhs: &Self) { let (_nv, len) = assert_matching_dims(self, rhs); @@ -523,7 +523,7 @@ impl SubAssign<&Self> for Polynomial { } } -impl Mul for Polynomial { +impl Mul for Polynomial { type Output = Self; fn mul(mut self, rhs: F) -> Self { @@ -545,7 +545,7 @@ impl Mul for Polynomial { } } -impl Mul for &Polynomial { +impl Mul for &Polynomial { type Output = Polynomial; fn mul(self, rhs: F) -> Polynomial { @@ -553,7 +553,7 @@ impl Mul for &Polynomial { } } -impl Neg for Polynomial { +impl Neg for Polynomial { type Output = Self; fn neg(mut self) -> Self { @@ -580,7 +580,7 @@ impl Neg for Polynomial { mod tests { use super::*; use jolt_field::Fr; - use jolt_field::{FieldCore, FromPrimitiveInt}; + use jolt_field::{Field, Ring}; use num_traits::{One, Zero}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/src/eq.rs b/crates/jolt-poly/src/eq.rs index cf47672ac4..bf79bd6b88 100644 --- a/crates/jolt-poly/src/eq.rs +++ b/crates/jolt-poly/src/eq.rs @@ -2,7 +2,7 @@ use std::ops::{Mul, SubAssign}; -use jolt_field::Field; +use jolt_field::JoltField; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -21,7 +21,7 @@ use crate::thread::unsafe_allocate_zero_vec; /// $$f(r) = \sum_{x \in \{0,1\}^n} f(x) \cdot \widetilde{eq}(x, r)$$ #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: DeserializeOwned"))] -pub struct EqPolynomial { +pub struct EqPolynomial { point: Vec, } @@ -29,7 +29,7 @@ pub struct EqPolynomial { #[cfg(feature = "parallel")] const PAR_THRESHOLD: usize = 1024; -impl EqPolynomial { +impl EqPolynomial { /// Creates a new equality polynomial for the given point $r \in \mathbb{F}^n$. pub fn new(point: Vec) -> Self { Self { point } @@ -118,7 +118,7 @@ impl EqPolynomial { } } -pub fn try_eq_mle(left: &[F], right: &[F]) -> Result { +pub fn try_eq_mle(left: &[F], right: &[F]) -> Result { if left.len() != right.len() { return Err(MleError::EqualityArityMismatch { left: left.len(), @@ -128,7 +128,7 @@ pub fn try_eq_mle(left: &[F], right: &[F]) -> Result { Ok(EqPolynomial::::mle(left, right)) } -pub fn eq_index_msb(point: &[F], index: u128) -> F { +pub fn eq_index_msb(point: &[F], index: u128) -> F { let mut eq = F::one(); for (position, challenge) in point.iter().enumerate() { let shift = point.len() - 1 - position; @@ -160,14 +160,14 @@ pub fn boolean_bits_msb(num_vars: usize, index: usize) -> Vec { .collect() } -pub fn boolean_point_msb(num_vars: usize, index: usize) -> Vec { +pub fn boolean_point_msb(num_vars: usize, index: usize) -> Vec { boolean_bits_msb(num_vars, index) .into_iter() .map(|bit| F::from_u64(bit as u64)) .collect() } -pub fn boolean_index_msb(point: &[F]) -> Option { +pub fn boolean_index_msb(point: &[F]) -> Option { let mut index = 0usize; for value in point { index = index.checked_shl(1)?; @@ -185,7 +185,7 @@ pub fn boolean_index_msb(point: &[F]) -> Option { /// These accept challenge or field-element slices and produce materialized /// tables without constructing an `EqPolynomial` instance. They are used /// by split-eq evaluators and sumcheck witnesses. -impl EqPolynomial { +impl EqPolynomial { /// Computes `eq(x, y) = Π_i (x_i y_i + (1 - x_i)(1 - y_i))` for two slices. pub fn mle(x: &[C], y: &[C]) -> F where @@ -458,7 +458,7 @@ impl EqPolynomial { } } -impl crate::MultilinearEvaluation for EqPolynomial { +impl crate::MultilinearEvaluation for EqPolynomial { fn num_vars(&self) -> usize { self.point.len() } @@ -476,7 +476,7 @@ impl crate::MultilinearEvaluation for EqPolynomial { mod tests { use super::*; use jolt_field::Fr; - use jolt_field::{FieldCore, FromPrimitiveInt}; + use jolt_field::{Field, Ring}; use num_traits::{One, Zero}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/src/eq_plus_one.rs b/crates/jolt-poly/src/eq_plus_one.rs index 99eb8b06f2..13b0445e82 100644 --- a/crates/jolt-poly/src/eq_plus_one.rs +++ b/crates/jolt-poly/src/eq_plus_one.rs @@ -9,7 +9,7 @@ //! //! Both `x` and `y` are in **big-endian** bit ordering (`point[0]` = MSB). -use jolt_field::Field; +use jolt_field::JoltField; use crate::thread::unsafe_allocate_zero_vec; use crate::EqPolynomial; @@ -18,12 +18,12 @@ use crate::EqPolynomial; /// /// Stores a fixed point `x` in big-endian order. Call [`evaluate`](Self::evaluate) /// to compute `eq+1(x, y)` at any `y`. -pub struct EqPlusOnePolynomial { +pub struct EqPlusOnePolynomial { /// Fixed point (big-endian: `point[0]` = MSB). point: Vec, } -impl EqPlusOnePolynomial { +impl EqPlusOnePolynomial { pub fn new(point: Vec) -> Self { Self { point } } @@ -130,7 +130,7 @@ impl EqPlusOnePolynomial { /// half of the shift sumcheck to operate on √N-sized buffers rather than N. /// /// See (Appendix A). -pub struct EqPlusOnePrefixSuffix { +pub struct EqPlusOnePrefixSuffix { /// Evals of `eq+1(r_lo, j)` for `j ∈ {0,1}^{n/2}`. pub prefix_0: Vec, /// Evals of `eq(r_hi, j)` for `j ∈ {0,1}^{n/2}`. @@ -141,7 +141,7 @@ pub struct EqPlusOnePrefixSuffix { pub suffix_1: Vec, } -impl EqPlusOnePrefixSuffix { +impl EqPlusOnePrefixSuffix { /// Creates the decomposition from a big-endian point `r`. /// /// Splits at `r.len() / 2`: the first half is `r_hi`, the second is `r_lo`. @@ -170,7 +170,7 @@ impl EqPlusOnePrefixSuffix { #[cfg(test)] mod tests { use super::*; - use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; + use jolt_field::{Field, Fr, Ring}; use num_traits::{One, Zero}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/src/identity.rs b/crates/jolt-poly/src/identity.rs index 1de0ea798d..9ec7ab686f 100644 --- a/crates/jolt-poly/src/identity.rs +++ b/crates/jolt-poly/src/identity.rs @@ -1,6 +1,6 @@ //! Identity polynomial evaluating to the integer index on the Boolean hypercube. -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -25,7 +25,7 @@ impl OperandPolynomial { } } -impl crate::MultilinearEvaluation for OperandPolynomial { +impl crate::MultilinearEvaluation for OperandPolynomial { fn num_vars(&self) -> usize { self.num_vars } @@ -78,7 +78,7 @@ impl IdentityPolynomial { } } -impl crate::MultilinearEvaluation for IdentityPolynomial { +impl crate::MultilinearEvaluation for IdentityPolynomial { fn num_vars(&self) -> usize { self.num_vars } @@ -106,7 +106,7 @@ mod tests { use super::*; use crate::MultilinearEvaluation; use jolt_field::Fr; - use jolt_field::FromPrimitiveInt; + use jolt_field::Ring; use num_traits::{One, Zero}; #[test] diff --git a/crates/jolt-poly/src/lagrange.rs b/crates/jolt-poly/src/lagrange.rs index 9510f9132c..8989767207 100644 --- a/crates/jolt-poly/src/lagrange.rs +++ b/crates/jolt-poly/src/lagrange.rs @@ -6,7 +6,7 @@ use std::{fmt, marker::PhantomData}; -use jolt_field::Field; +use jolt_field::JoltField; /// Evaluates all Lagrange basis polynomials $L_0(r), \ldots, L_{N-1}(r)$ over /// the domain $\{s, s+1, \ldots, s+N-1\}$ where $s$ = `domain_start`. @@ -17,7 +17,7 @@ use jolt_field::Field; /// # Panics /// Panics if `domain_size` is zero. #[expect(clippy::expect_used)] -pub fn lagrange_evals(domain_start: i64, domain_size: usize, r: F) -> Vec { +pub fn lagrange_evals(domain_start: i64, domain_size: usize, r: F) -> Vec { assert!(domain_size > 0, "domain_size must be positive"); // Check if r coincides with a grid point (early exit) @@ -65,7 +65,7 @@ pub fn lagrange_evals(domain_start: i64, domain_size: usize, r: F) -> /// Evaluates all Lagrange basis polynomials over the centered consecutive /// integer domain used by univariate-skip protocols. -pub fn centered_lagrange_evals( +pub fn centered_lagrange_evals( domain_size: usize, r: F, ) -> Result, CenteredIntegerDomainError> { @@ -76,7 +76,7 @@ pub fn centered_lagrange_evals( )) } -pub fn centered_lagrange_evals_array( +pub fn centered_lagrange_evals_array( r: F, ) -> Result<[F; N], CenteredIntegerDomainError> { let evals = centered_lagrange_evals(N, r)?; @@ -89,7 +89,7 @@ pub fn centered_lagrange_evals_array( /// Computes `sum_i L_i(x) * L_i(y)` over the centered consecutive integer /// domain used by univariate-skip protocols. -pub fn centered_lagrange_kernel( +pub fn centered_lagrange_kernel( domain_size: usize, x: F, y: F, @@ -191,9 +191,9 @@ impl LagrangeHelper { } } -pub struct LagrangePolynomial(PhantomData); +pub struct LagrangePolynomial(PhantomData); -impl LagrangePolynomial { +impl LagrangePolynomial { #[inline] fn start_i64() -> i64 { -(((N - 1) / 2) as i64) @@ -439,7 +439,7 @@ impl LagrangePolynomial { } } -pub fn centered_lagrange_evaluate( +pub fn centered_lagrange_evaluate( values: &[F; N], r: F, ) -> Result { @@ -447,7 +447,7 @@ pub fn centered_lagrange_evaluate( Ok(LagrangePolynomial::::evaluate::(values, r)) } -pub fn centered_lagrange_evaluate_many( +pub fn centered_lagrange_evaluate_many( values: &[F; N], points: &[F], ) -> Result, CenteredIntegerDomainError> { @@ -455,7 +455,7 @@ pub fn centered_lagrange_evaluate_many( Ok(LagrangePolynomial::::evaluate_many::(values, points)) } -pub fn centered_interpolate_coeffs_array( +pub fn centered_interpolate_coeffs_array( values: &[F; N], ) -> Result<[F; N], CenteredIntegerDomainError> { let _ = centered_domain_start(N)?; @@ -525,7 +525,7 @@ pub fn centered_power_sums( /// coefficients of $p \cdot q$ of length `a.len() + b.len() - 1`. /// /// Returns empty if either input is empty. -pub fn poly_mul(a: &[F], b: &[F]) -> Vec { +pub fn poly_mul(a: &[F], b: &[F]) -> Vec { if a.is_empty() || b.is_empty() { return Vec::new(); } @@ -550,7 +550,7 @@ pub fn poly_mul(a: &[F], b: &[F]) -> Vec { /// # Panics /// Panics if `values` is empty. #[expect(clippy::expect_used)] -pub fn interpolate_to_coeffs(domain_start: i64, values: &[F]) -> Vec { +pub fn interpolate_to_coeffs(domain_start: i64, values: &[F]) -> Vec { let n = values.len(); assert!(n > 0, "cannot interpolate zero values"); @@ -597,7 +597,7 @@ pub fn interpolate_to_coeffs(domain_start: i64, values: &[F]) -> Vec { +pub struct LtPolynomial { lt_lo: Vec, lt_hi: Vec, eq_hi: Vec, @@ -42,7 +42,7 @@ pub struct LtPolynomial { n_hi_vars: usize, } -impl LtPolynomial { +impl LtPolynomial { /// Creates a split LT polynomial for the fixed point `r` (big-endian). /// /// Splits at `r.len() / 2`: the first half is `r_hi`, the second is `r_lo`. @@ -141,7 +141,7 @@ impl LtPolynomial { /// - Right half `y`: `y' = x·r_i` (propagates eq term through x_i=1) /// /// Time: O(n·2^n). Space: O(2^n). -fn lt_evals(r: &[F]) -> Vec { +fn lt_evals(r: &[F]) -> Vec { let n = r.len(); let mut evals = crate::thread::unsafe_allocate_zero_vec(1usize << n); for (i, &ri) in r.iter().rev().enumerate() { @@ -156,7 +156,7 @@ fn lt_evals(r: &[F]) -> Vec { /// In-place HighToLow bind: `v[j] = v[j] + challenge · (v[j+half] - v[j])`. #[inline] -fn bind_in_place(v: &mut Vec, challenge: F) { +fn bind_in_place(v: &mut Vec, challenge: F) { let half = v.len() / 2; for j in 0..half { let lo = v[j]; @@ -169,7 +169,7 @@ fn bind_in_place(v: &mut Vec, challenge: F) { #[cfg(test)] mod tests { use super::*; - use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; + use jolt_field::{Field, Fr, Ring}; use num_traits::{One, Zero}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/src/mle.rs b/crates/jolt-poly/src/mle.rs index 0dafdff96e..5f95f0bc64 100644 --- a/crates/jolt-poly/src/mle.rs +++ b/crates/jolt-poly/src/mle.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use thiserror::Error; use crate::eq_index_msb; @@ -21,7 +21,7 @@ pub enum MleError { BlockEndOutOfDomain { end: u128, domain_size: u128 }, } -pub fn sparse_mle_msb(start_index: u128, values: &[u64], point: &[F]) -> F { +pub fn sparse_mle_msb(start_index: u128, values: &[u64], point: &[F]) -> F { values .iter() .enumerate() @@ -33,7 +33,7 @@ pub fn sparse_mle_msb(start_index: u128, values: &[u64], point: &[F]) pub fn sparse_segments_mle_msb<'a, F, I>(segments: I, point: &[F]) -> F where - F: Field, + F: JoltField, I: IntoIterator, { segments @@ -42,7 +42,7 @@ where .sum() } -pub fn block_selector_mle_msb( +pub fn block_selector_mle_msb( start_index: u128, block_num_vars: usize, point: &[F], @@ -86,7 +86,7 @@ pub fn block_selector_mle_msb( Ok(eq_index_msb(&point[..selector_point_len], block_index)) } -pub fn range_mask_mle_msb( +pub fn range_mask_mle_msb( range_start: u128, range_end: u128, point: &[F], @@ -110,7 +110,7 @@ pub fn range_mask_mle_msb( Ok(less_than_mle_msb(range_end, point) - less_than_mle_msb(range_start, point)) } -fn less_than_mle_msb(bound: u128, point: &[F]) -> F { +fn less_than_mle_msb(bound: u128, point: &[F]) -> F { if Some(bound) == 1u128.checked_shl(point.len() as u32) { return F::one(); } @@ -142,7 +142,7 @@ mod tests { use super::*; use crate::{eq_index_msb, try_eq_mle}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use num_traits::{One, Zero}; #[test] diff --git a/crates/jolt-poly/src/multilinear.rs b/crates/jolt-poly/src/multilinear.rs index cf9e3ec9ce..54f89363b4 100644 --- a/crates/jolt-poly/src/multilinear.rs +++ b/crates/jolt-poly/src/multilinear.rs @@ -13,7 +13,7 @@ use std::borrow::Cow; -use jolt_field::Field; +use jolt_field::JoltField; use crate::Polynomial; @@ -27,7 +27,7 @@ use crate::Polynomial; /// determined by its $2^n$ evaluations on the Boolean hypercube. This trait /// exposes point evaluation and dimensional metadata without prescribing how /// the evaluations are stored. -pub trait MultilinearEvaluation: Send + Sync { +pub trait MultilinearEvaluation: Send + Sync { /// Number of variables $n$. The polynomial has $2^n$ evaluations. fn num_vars(&self) -> usize; @@ -50,7 +50,7 @@ pub trait MultilinearEvaluation: Send + Sync { /// $$g(x_2, \ldots, x_n) = (1 - s) \cdot f(0, x_2, \ldots, x_n) + s \cdot f(1, x_2, \ldots, x_n)$$ /// /// After calling `bind`, `num_vars` decreases by 1 and `len` halves. -pub trait MultilinearBinding: Send + Sync { +pub trait MultilinearBinding: Send + Sync { fn bind(&mut self, scalar: F); } @@ -71,7 +71,7 @@ pub trait MultilinearBinding: Send + Sync { /// - [`fold_rows`](Self::fold_rows): matrix-vector product $v \cdot M$ (opening protocols) /// - [`is_one_hot`](Self::is_one_hot) / [`for_each_one`](Self::for_each_one): unit-entry /// one-hot hints for PCS commit optimization (e.g., batch addition instead of MSM) -pub trait MultilinearPoly: Send + Sync { +pub trait MultilinearPoly: Send + Sync { /// Number of variables $n$. The polynomial has $2^n$ evaluations. fn num_vars(&self) -> usize; @@ -180,7 +180,7 @@ pub trait MultilinearPoly: Send + Sync { // MultilinearPoly impls for Polynomial, [F], Vec, and source pointers. // --------------------------------------------------------------------------- -impl MultilinearPoly for Polynomial { +impl MultilinearPoly for Polynomial { #[inline] fn num_vars(&self) -> usize { Polynomial::num_vars(self) @@ -221,7 +221,7 @@ impl MultilinearPoly for Polynomial { } } -impl MultilinearPoly for [F] { +impl MultilinearPoly for [F] { #[inline] fn num_vars(&self) -> usize { if self.is_empty() { @@ -264,7 +264,7 @@ impl MultilinearPoly for [F] { } } -impl MultilinearPoly for Vec { +impl MultilinearPoly for Vec { #[inline] fn num_vars(&self) -> usize { self.as_slice().num_vars() @@ -292,7 +292,7 @@ macro_rules! forward_multilinear_poly { ($($wrapper:ty),* $(,)?) => {$( impl MultilinearPoly for $wrapper where - F: Field, + F: JoltField, P: MultilinearPoly + ?Sized, { #[inline] @@ -355,13 +355,13 @@ forward_multilinear_poly!(&P, Box

, std::sync::Arc

); /// - [`fold_rows`](MultilinearPoly::fold_rows): $\sum_i s_i \cdot (v \cdot M_i)$ — /// each polynomial computes its own fold, results are combined with scalars. /// No evaluation table is ever materialized. -pub struct RlcSource> { +pub struct RlcSource> { sources: Vec, scalars: Vec, num_vars: usize, } -impl> RlcSource { +impl> RlcSource { /// Creates a lazy RLC composition. /// /// # Panics @@ -391,7 +391,7 @@ impl> RlcSource { } } -impl> MultilinearPoly for RlcSource { +impl> MultilinearPoly for RlcSource { fn num_vars(&self) -> usize { self.num_vars } @@ -465,7 +465,7 @@ impl> MultilinearPoly for RlcSource { #[cfg(test)] mod tests { use super::*; - use jolt_field::{FieldCore, Fr}; + use jolt_field::{Field, Fr}; use num_traits::Zero; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; @@ -658,7 +658,7 @@ mod tests { } /// Calls the default `fold_rows` implementation (via `for_each_row`). - fn default_fold_rows( + fn default_fold_rows( source: &impl MultilinearPoly, left: &[F], sigma: usize, diff --git a/crates/jolt-poly/src/one_hot.rs b/crates/jolt-poly/src/one_hot.rs index 14efc48b80..7ea6c59529 100644 --- a/crates/jolt-poly/src/one_hot.rs +++ b/crates/jolt-poly/src/one_hot.rs @@ -7,7 +7,7 @@ //! a factor of `k` and enables ~254× faster commitment via generator lookup //! instead of full MSM. -use jolt_field::Field; +use jolt_field::JoltField; use crate::multilinear::MultilinearPoly; @@ -121,7 +121,7 @@ impl OneHotPolynomial { } } -impl MultilinearPoly for OneHotPolynomial { +impl MultilinearPoly for OneHotPolynomial { #[inline] fn num_vars(&self) -> usize { self.num_vars @@ -210,7 +210,7 @@ impl MultilinearPoly for OneHotPolynomial { mod tests { use super::*; use crate::Polynomial; - use jolt_field::{FieldCore, Fr}; + use jolt_field::{Field, Fr}; use num_traits::Zero; use rand_chacha::ChaCha20Rng; use rand_core::{RngCore, SeedableRng}; @@ -219,7 +219,7 @@ mod tests { OneHotPolynomial::new(k, indices.to_vec()) } - fn to_dense(oh: &OneHotPolynomial) -> Polynomial { + fn to_dense(oh: &OneHotPolynomial) -> Polynomial { let total = 1usize << oh.num_vars; let mut table = vec![F::zero(); total]; for (row, &opt_col) in oh.indices.iter().enumerate() { diff --git a/crates/jolt-poly/src/split_eq.rs b/crates/jolt-poly/src/split_eq.rs index c1300debc0..beb606b232 100644 --- a/crates/jolt-poly/src/split_eq.rs +++ b/crates/jolt-poly/src/split_eq.rs @@ -1,19 +1,19 @@ //! Split equality tables for sqrt-memory sumcheck kernels. -use jolt_field::Field; +use jolt_field::JoltField; #[cfg(feature = "parallel")] use rayon::prelude::*; use crate::{BindingOrder, EqPolynomial, Polynomial, UnivariatePoly}; #[derive(Clone, Debug, PartialEq, Eq)] -pub struct TensorEqTable { +pub struct TensorEqTable { e_out: Vec, e_in: Vec, in_bits: usize, } -impl TensorEqTable { +impl TensorEqTable { pub fn new(point: &[F]) -> Self { let split = point.len() / 2; let (out_point, in_point) = point.split_at(split); @@ -156,7 +156,7 @@ impl TensorEqTable { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct GruenSplitEqPolynomial { +pub struct GruenSplitEqPolynomial { current_index: usize, current_scalar: F, point: Vec, @@ -165,7 +165,7 @@ pub struct GruenSplitEqPolynomial { binding_order: BindingOrder, } -impl GruenSplitEqPolynomial { +impl GruenSplitEqPolynomial { pub fn new(point: &[F], binding_order: BindingOrder) -> Self { Self::new_with_scaling(point, binding_order, None) } @@ -495,7 +495,7 @@ impl GruenSplitEqPolynomial { #[cfg(test)] mod tests { - use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; + use jolt_field::{Field, Fr, Ring}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; diff --git a/crates/jolt-poly/src/univariate.rs b/crates/jolt-poly/src/univariate.rs index 1735a862da..248b3419da 100644 --- a/crates/jolt-poly/src/univariate.rs +++ b/crates/jolt-poly/src/univariate.rs @@ -2,7 +2,7 @@ use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use jolt_field::Field; +use jolt_field::JoltField; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; /// access are deliberately left as inherent methods because the two representations /// require different calling conventions (compressed evaluation needs an external /// hint value). -pub trait UnivariatePolynomial: Send + Sync { +pub trait UnivariatePolynomial: Send + Sync { /// Degree of the polynomial, or 0 for the zero polynomial. fn degree(&self) -> usize; } @@ -24,11 +24,11 @@ pub trait UnivariatePolynomial: Send + Sync { /// coefficient of $x^i$. An empty coefficient vector represents the zero polynomial. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: DeserializeOwned"))] -pub struct UnivariatePoly { +pub struct UnivariatePoly { coefficients: Vec, } -impl UnivariatePolynomial for UnivariatePoly { +impl UnivariatePolynomial for UnivariatePoly { fn degree(&self) -> usize { if self.coefficients.is_empty() { 0 @@ -38,7 +38,7 @@ impl UnivariatePolynomial for UnivariatePoly { } } -impl UnivariatePoly { +impl UnivariatePoly { /// Creates a polynomial from coefficients in ascending degree order. pub fn new(coefficients: Vec) -> Self { Self { coefficients } @@ -346,7 +346,7 @@ impl UnivariatePoly { } } -impl Neg for UnivariatePoly { +impl Neg for UnivariatePoly { type Output = Self; fn neg(mut self) -> Self { @@ -357,7 +357,7 @@ impl Neg for UnivariatePoly { } } -impl Add for UnivariatePoly { +impl Add for UnivariatePoly { type Output = Self; fn add(mut self, rhs: Self) -> Self { @@ -366,7 +366,7 @@ impl Add for UnivariatePoly { } } -impl Add for &UnivariatePoly { +impl Add for &UnivariatePoly { type Output = UnivariatePoly; fn add(self, rhs: Self) -> UnivariatePoly { @@ -383,7 +383,7 @@ impl Add for &UnivariatePoly { } } -impl AddAssign<&Self> for UnivariatePoly { +impl AddAssign<&Self> for UnivariatePoly { fn add_assign(&mut self, rhs: &Self) { if rhs.coefficients.len() > self.coefficients.len() { self.coefficients.resize(rhs.coefficients.len(), F::zero()); @@ -394,7 +394,7 @@ impl AddAssign<&Self> for UnivariatePoly { } } -impl Sub for UnivariatePoly { +impl Sub for UnivariatePoly { type Output = Self; fn sub(mut self, rhs: Self) -> Self { @@ -403,7 +403,7 @@ impl Sub for UnivariatePoly { } } -impl Sub for &UnivariatePoly { +impl Sub for &UnivariatePoly { type Output = UnivariatePoly; fn sub(self, rhs: Self) -> UnivariatePoly { @@ -419,7 +419,7 @@ impl Sub for &UnivariatePoly { } } -impl SubAssign<&Self> for UnivariatePoly { +impl SubAssign<&Self> for UnivariatePoly { fn sub_assign(&mut self, rhs: &Self) { if rhs.coefficients.len() > self.coefficients.len() { self.coefficients.resize(rhs.coefficients.len(), F::zero()); @@ -430,7 +430,7 @@ impl SubAssign<&Self> for UnivariatePoly { } } -impl Mul for UnivariatePoly { +impl Mul for UnivariatePoly { type Output = Self; fn mul(mut self, rhs: F) -> Self { @@ -439,7 +439,7 @@ impl Mul for UnivariatePoly { } } -impl Mul for &UnivariatePoly { +impl Mul for &UnivariatePoly { type Output = UnivariatePoly; fn mul(self, rhs: F) -> UnivariatePoly { @@ -447,7 +447,7 @@ impl Mul for &UnivariatePoly { } } -impl MulAssign for UnivariatePoly { +impl MulAssign for UnivariatePoly { fn mul_assign(&mut self, rhs: F) { for c in &mut self.coefficients { *c *= rhs; @@ -456,7 +456,7 @@ impl MulAssign for UnivariatePoly { } /// Gaussian elimination on a Vandermonde system for evaluations at `0, 1, ..., n-1`. -fn gaussian_elimination_vandermonde(evals: &[F]) -> Vec { +fn gaussian_elimination_vandermonde(evals: &[F]) -> Vec { let n = evals.len(); let xs: Vec = (0..n).map(|x| F::from_u64(x as u64)).collect(); @@ -485,7 +485,7 @@ fn gaussian_elimination_vandermonde(evals: &[F]) -> Vec { /// /// Panics if the matrix is singular (no nonzero pivot in some column). #[expect(clippy::expect_used)] -fn gaussian_elimination_augmented(matrix: &mut [Vec]) -> Vec { +fn gaussian_elimination_augmented(matrix: &mut [Vec]) -> Vec { let size = matrix.len(); debug_assert_eq!(size, matrix[0].len() - 1); @@ -545,7 +545,7 @@ fn gaussian_elimination_augmented(matrix: &mut [Vec]) -> Vec { mod tests { use super::*; use jolt_field::Fr; - use jolt_field::FromPrimitiveInt; + use jolt_field::Ring; use num_traits::{One, Zero}; #[test] diff --git a/crates/jolt-poly/tests/integration.rs b/crates/jolt-poly/tests/integration.rs index 5ab0f61798..8c4445074d 100644 --- a/crates/jolt-poly/tests/integration.rs +++ b/crates/jolt-poly/tests/integration.rs @@ -5,7 +5,7 @@ //! (Polynomial, EqPolynomial, UnivariatePoly, IdentityPolynomial, RlcSource) //! that are used throughout the proving system. -use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; +use jolt_field::{Field, Fr, Ring}; use jolt_poly::{ EqPolynomial, IdentityPolynomial, MultilinearEvaluation, MultilinearPoly, Polynomial, RlcSource, UnivariatePoly, diff --git a/crates/jolt-prover-legacy/src/transcripts/verifier_native.rs b/crates/jolt-prover-legacy/src/transcripts/verifier_native.rs index 289fb8a115..87f782b6b2 100644 --- a/crates/jolt-prover-legacy/src/transcripts/verifier_native.rs +++ b/crates/jolt-prover-legacy/src/transcripts/verifier_native.rs @@ -12,13 +12,13 @@ //! `H(state ‖ 28 zero bytes ‖ n_rounds_be ‖ payload)` — pinned by the //! parity test at the bottom of this file. -use jolt_field::CanonicalRepr; +use jolt_field::CanonicalEncoding; use jolt_transcript::{LegacyBlake2bTranscript, Transcript as VerifierTranscript}; use super::Transcript; use crate::field::JoltField; -impl Transcript for LegacyBlake2bTranscript { +impl Transcript for LegacyBlake2bTranscript { fn new(label: &'static [u8]) -> Self { // Identical label framing on both engines: the label is right-padded // with zeros into one 32-byte block and hashed as the initial state. diff --git a/crates/jolt-prover-legacy/src/zkvm/clear_claims.rs b/crates/jolt-prover-legacy/src/zkvm/clear_claims.rs index 0085973fc0..75fbd67ead 100644 --- a/crates/jolt-prover-legacy/src/zkvm/clear_claims.rs +++ b/crates/jolt-prover-legacy/src/zkvm/clear_claims.rs @@ -19,7 +19,7 @@ use jolt_claims::protocols::jolt::{ geometry::{claim_reductions::increments, spartan::outer_uniskip_opening}, JoltCommittedPolynomial, JoltOpeningId, JoltRelationId, }; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_lookup_tables::{LookupTableKind, XLEN as RISCV_XLEN}; use jolt_riscv::CircuitFlags; #[cfg(not(feature = "akita"))] @@ -79,7 +79,7 @@ use jolt_verifier::{ // `ClearProofClaims` itself; the base builder and its stage-6b/7 pieces // target the base wire shape and are compiled out with it. #[cfg(not(feature = "akita"))] -pub(crate) fn build_clear_claims( +pub(crate) fn build_clear_claims( claims: impl IntoIterator, _trace_length: usize, ) -> Result, VerifierError> { @@ -101,7 +101,7 @@ pub(crate) fn build_clear_claims( }) } -fn spartan_outer_claims_from_openings( +fn spartan_outer_claims_from_openings( claims: &OpeningClaimMap, ) -> Result, VerifierError> { let outer_claim = |variable| claims.require(outer_opening(variable)); @@ -148,7 +148,7 @@ fn spartan_outer_claims_from_openings( }) } -fn stage2_claims_from_openings( +fn stage2_claims_from_openings( claims: &OpeningClaimMap, ) -> Result, VerifierError> { let product_remainder = ProductRemainderOutputClaims { @@ -200,7 +200,7 @@ fn stage2_claims_from_openings( }) } -fn stage3_claims_from_openings( +fn stage3_claims_from_openings( claims: &OpeningClaimMap, ) -> Result, VerifierError> { let shift = SpartanShiftOutputClaims { @@ -239,7 +239,7 @@ fn stage3_claims_from_openings( }) } -fn stage4_claims_from_openings( +fn stage4_claims_from_openings( claims: &OpeningClaimMap, ) -> Result, VerifierError> { Ok(Stage4OutputClaims { @@ -263,7 +263,7 @@ fn stage4_claims_from_openings( }) } -fn stage5_claims_from_openings( +fn stage5_claims_from_openings( claims: &OpeningClaimMap, ) -> Result, VerifierError> { let lookup_table_flags = LookupTableKind::::iter() @@ -297,7 +297,7 @@ fn stage5_claims_from_openings( }) } -fn stage6a_claims_from_openings( +fn stage6a_claims_from_openings( claims: &OpeningClaimMap, ) -> Result, VerifierError> { let bytecode_read_raf_address = bytecode::bytecode_read_raf_address_phase_opening(); @@ -315,7 +315,7 @@ fn stage6a_claims_from_openings( } #[cfg(not(feature = "akita"))] -fn stage6b_claims_from_openings( +fn stage6b_claims_from_openings( claims: &OpeningClaimMap, ) -> Result, VerifierError> { let mut bytecode_ra = Vec::new(); @@ -438,7 +438,7 @@ fn stage6b_claims_from_openings( }) } -fn trusted_advice_cycle_phase_claim_from_openings( +fn trusted_advice_cycle_phase_claim_from_openings( claims: &OpeningClaimMap, ) -> Option> { let opening_claim = claims @@ -449,7 +449,7 @@ fn trusted_advice_cycle_phase_claim_from_openings( }) } -fn untrusted_advice_cycle_phase_claim_from_openings( +fn untrusted_advice_cycle_phase_claim_from_openings( claims: &OpeningClaimMap, ) -> Option> { let opening_claim = claims @@ -462,7 +462,7 @@ fn untrusted_advice_cycle_phase_claim_from_openings( }) } -fn bytecode_val_stage_claims_from_openings( +fn bytecode_val_stage_claims_from_openings( claims: &OpeningClaimMap, ) -> Result, VerifierError> { if claims @@ -486,7 +486,7 @@ fn bytecode_val_stage_claims_from_openings( Ok(stage_claims) } -fn bytecode_cycle_phase_claims_from_openings( +fn bytecode_cycle_phase_claims_from_openings( claims: &OpeningClaimMap, ) -> Option> { if let Some(intermediate) = @@ -504,7 +504,7 @@ fn bytecode_cycle_phase_claims_from_openings( }) } -fn final_bytecode_chunk_claims_from_openings(claims: &OpeningClaimMap) -> Vec { +fn final_bytecode_chunk_claims_from_openings(claims: &OpeningClaimMap) -> Vec { let mut chunks = Vec::new(); for chunk_idx in 0.. { let Some(opening_claim) = claims.get( @@ -518,7 +518,7 @@ fn final_bytecode_chunk_claims_from_openings(claims: &OpeningClaimMap< } #[cfg(not(feature = "akita"))] -fn stage7_claims_from_openings( +fn stage7_claims_from_openings( claims: &OpeningClaimMap, ) -> Result, VerifierError> { let mut instruction_ra = Vec::new(); @@ -581,7 +581,7 @@ fn stage7_claims_from_openings( }) } -fn advice_address_phase_claim_from_openings( +fn advice_address_phase_claim_from_openings( claims: &OpeningClaimMap, kind: JoltAdviceKind, ) -> Option { @@ -589,7 +589,7 @@ fn advice_address_phase_claim_from_openings( claims.get(advice::final_advice_opening(kind)) } -fn bytecode_address_phase_claims_from_openings( +fn bytecode_address_phase_claims_from_openings( claims: &OpeningClaimMap, ) -> Option> { let _ = claims.get(bytecode_claim_reduction::cycle_phase_intermediate_opening())?; @@ -597,7 +597,7 @@ fn bytecode_address_phase_claims_from_openings( (!chunks.is_empty()).then_some(BytecodeReductionAddressPhaseOutputClaims { chunks }) } -fn program_image_address_phase_claim_from_openings( +fn program_image_address_phase_claim_from_openings( claims: &OpeningClaimMap, ) -> Option> { let _ = claims.get(program_image::cycle_phase_program_image_opening())?; @@ -607,11 +607,11 @@ fn program_image_address_phase_claim_from_openings( } #[derive(Clone, Debug)] -struct OpeningClaimMap { +struct OpeningClaimMap { claims: Vec<(jolt::JoltOpeningId, F)>, } -impl OpeningClaimMap { +impl OpeningClaimMap { fn get(&self, id: jolt::JoltOpeningId) -> Option { self.claims .iter() @@ -664,7 +664,7 @@ mod packed { /// lattice stage-6b/7 shapes (the read-raf carries the fused-inc opening; /// booleanity carries the unsigned-inc columns; there is no stage-6b inc /// slot). - pub(crate) fn build_packed_clear_claims( + pub(crate) fn build_packed_clear_claims( claims: impl IntoIterator, ) -> Result, VerifierError> { let claims = OpeningClaimMap { @@ -686,7 +686,7 @@ mod packed { }) } - fn indexed_family( + fn indexed_family( claims: &OpeningClaimMap, id: impl Fn(usize) -> JoltOpeningId, ) -> Vec { @@ -700,7 +700,7 @@ mod packed { family } - fn packed_stage6b_claims_from_openings( + fn packed_stage6b_claims_from_openings( claims: &OpeningClaimMap, ) -> Result, VerifierError> { let bytecode_ra = indexed_family(claims, |index| { @@ -794,7 +794,7 @@ mod packed { }) } - fn packed_stage7_claims_from_openings( + fn packed_stage7_claims_from_openings( claims: &OpeningClaimMap, ) -> Result, VerifierError> { let instruction_ra = indexed_family(claims, |index| { @@ -855,7 +855,7 @@ mod packed { }) } - fn reconstruction_claims_from_openings( + fn reconstruction_claims_from_openings( claims: &OpeningClaimMap, ) -> ReconstructionOutputClaims { ReconstructionOutputClaims { @@ -874,7 +874,7 @@ mod packed { /// Every per-chunk lane family, in the relation's family-major layout; /// `None` when no bytecode reconstruction ran (full-program mode). - fn bytecode_reconstruction_claims_from_openings( + fn bytecode_reconstruction_claims_from_openings( claims: &OpeningClaimMap, ) -> Option> { let mut chunk_count = 0; diff --git a/crates/jolt-prover-legacy/src/zkvm/packed.rs b/crates/jolt-prover-legacy/src/zkvm/packed.rs index 99fdf67ab7..b5854977cb 100644 --- a/crates/jolt-prover-legacy/src/zkvm/packed.rs +++ b/crates/jolt-prover-legacy/src/zkvm/packed.rs @@ -2299,7 +2299,7 @@ mod committed_tests { } use jolt_crypto::{Commitment, HomomorphicCommitment, VectorCommitment}; -use jolt_field::{CanonicalBytes, Field}; +use jolt_field::{CanonicalBytes, JoltField}; use serde::{Deserialize, Serialize}; use std::fmt::{self, Debug}; @@ -2336,7 +2336,7 @@ pub struct NoCommitment; // `AppendToTranscript` comes from jolt-transcript's blanket impl over // `CanonicalBytes`: an empty canonical encoding, so absorbing a -// `NoCommitment` is a no-op. Deliberately NOT `CanonicalRepr`: a commitment +// `NoCommitment` is a no-op. Deliberately NOT `CanonicalEncoding`: a commitment // placeholder is not a decodable field element. impl CanonicalBytes for NoCommitment { const NUM_BYTES: usize = 0; @@ -2344,7 +2344,7 @@ impl CanonicalBytes for NoCommitment { fn to_bytes_le(&self, _out: &mut [u8]) {} } -impl HomomorphicCommitment for NoCommitment { +impl HomomorphicCommitment for NoCommitment { fn add(_c1: &Self, _c2: &Self) -> Self { Self } @@ -2354,11 +2354,11 @@ impl HomomorphicCommitment for NoCommitment { } } -impl Commitment for NoVectorCommitment { +impl Commitment for NoVectorCommitment { type Output = NoCommitment; } -impl VectorCommitment for NoVectorCommitment { +impl VectorCommitment for NoVectorCommitment { type Field = F; type Setup = (); diff --git a/crates/jolt-prover-legacy/src/zkvm/packed_witness.rs b/crates/jolt-prover-legacy/src/zkvm/packed_witness.rs index 3fe211424b..16af138229 100644 --- a/crates/jolt-prover-legacy/src/zkvm/packed_witness.rs +++ b/crates/jolt-prover-legacy/src/zkvm/packed_witness.rs @@ -29,7 +29,7 @@ pub struct SparseUnitPolynomial { _field: core::marker::PhantomData, } -impl SparseUnitPolynomial { +impl SparseUnitPolynomial { /// Sorts the positions ascending once here — the invariant /// `for_each_row`'s row scan and `for_each_one`'s yield order rely on. /// Duplicates are neither deduplicated nor rejected. @@ -59,7 +59,7 @@ impl SparseUnitPolynomial { } } -impl jolt_poly::MultilinearPoly for SparseUnitPolynomial { +impl jolt_poly::MultilinearPoly for SparseUnitPolynomial { fn num_vars(&self) -> usize { self.num_vars } @@ -372,7 +372,7 @@ mod tests { #[test] fn sparse_unit_positions_sort_ascending_on_construction() { - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_poly::MultilinearPoly; let poly = SparseUnitPolynomial::::new(4, vec![9, 2, 11, 0, 2]); @@ -404,7 +404,7 @@ mod precommitted_tests { }; use jolt_claims::protocols::jolt::lattice::{precommitted_packing, PrecommittedPackingShape}; use jolt_field::Fr as ClaimsFr; - use jolt_field::FromPrimitiveInt; + use jolt_field::Ring; use jolt_riscv::{JoltInstructionKind, NormalizedOperands}; fn row( diff --git a/crates/jolt-prover-legacy/src/zkvm/proof.rs b/crates/jolt-prover-legacy/src/zkvm/proof.rs index fbeddf5a54..779be6b4dc 100644 --- a/crates/jolt-prover-legacy/src/zkvm/proof.rs +++ b/crates/jolt-prover-legacy/src/zkvm/proof.rs @@ -30,7 +30,7 @@ use jolt_crypto::{ PedersenSetup, VectorCommitment as VerifierVectorCommitment, }; use jolt_dory::{DoryCommitment, DoryProof, DoryScheme, DoryVerifierSetup}; -use jolt_field::{Field as VerifierFieldTrait, Fr as VerifierFr}; +use jolt_field::{Fr as VerifierFr, JoltField as VerifierFieldTrait}; #[cfg(not(feature = "akita"))] use jolt_lookup_tables::XLEN as RISCV_XLEN; use jolt_openings::CommitmentScheme as VerifierCommitmentScheme; diff --git a/crates/jolt-prover/src/config.rs b/crates/jolt-prover/src/config.rs index 1114e07097..29cf367c7a 100644 --- a/crates/jolt-prover/src/config.rs +++ b/crates/jolt-prover/src/config.rs @@ -10,7 +10,7 @@ use common::constants::{ONEHOT_CHUNK_THRESHOLD_LOG_T, REGISTER_COUNT, XLEN}; use common::jolt_device::MemoryLayout; use jolt_claims::protocols::jolt::{JoltOneHotConfig, JoltReadWriteConfig, TracePolynomialOrder}; -use jolt_field::FieldCore; +use jolt_field::Field; use jolt_program::execution::{RamAccess, TraceRow}; use crate::ProverError; @@ -43,7 +43,7 @@ impl ProverConfig { /// final no-op), size RAM to the highest touched (remapped) address or the /// program image extent, and pick the chunking policies from `log_T`. #[expect(non_snake_case)] - pub fn derive( + pub fn derive( rows: &[TraceRow], memory_layout: &MemoryLayout, min_bytecode_address: u64, diff --git a/crates/jolt-prover/src/driver.rs b/crates/jolt-prover/src/driver.rs index abda59954d..f6f738a851 100644 --- a/crates/jolt-prover/src/driver.rs +++ b/crates/jolt-prover/src/driver.rs @@ -23,7 +23,7 @@ use jolt_claims::protocols::jolt::JoltChallengeId; use jolt_claims::{InputClaims, OutputClaims, SumcheckChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_kernels::{ PrepareKernel, ProofSession, ProverInputs, SumcheckKernel, SumcheckKernelError, }; @@ -44,7 +44,7 @@ use crate::ProverError; /// [`Kernels`](Self::Kernels) is the typed kernel bundle — one boxed /// [`SumcheckKernel`] per member, `Option`-wrapped for a conditional member, /// in declaration order — that [`KernelSource::prepare_members`] mints. -pub trait StageProver: Sized { +pub trait StageProver: Sized { type InputClaims; type InputPoints; type Challenges; @@ -95,7 +95,7 @@ pub trait StageProver: Sized { /// `prepare_members` mints the typed kernel bundle in declaration order /// (`Option` members gated on presence, mismatched presence attributed to the /// member's relation id). -pub trait KernelSource> { +pub trait KernelSource> { fn prepare_members( &self, batch: &S, @@ -112,7 +112,7 @@ pub trait KernelSource> { /// witness for a committed recorder), the typed output claims and derived /// opening points, and the batch's final running claim (already hard-checked /// against the generated `expected_final_claim`). -pub struct Proved, C> { +pub struct Proved, C> { pub recorded: RecordedSumcheck, pub output_claims: S::OutputClaims, pub output_points: S::OutputPoints, @@ -131,7 +131,7 @@ pub fn prepare_required( challenges: &ConcreteSumcheckChallenges, ) -> Result>, ProverError> where - F: Field, + F: JoltField, R: ConcreteSumcheck, B: PrepareKernel + ?Sized, SumcheckInputClaims: InputClaims, @@ -169,7 +169,7 @@ pub fn prepare_optional( challenges: Option<&ConcreteSumcheckChallenges>, ) -> Result>>, ProverError> where - F: Field, + F: JoltField, R: ConcreteSumcheck, B: PrepareKernel + ?Sized, SumcheckInputClaims: InputClaims, @@ -208,7 +208,7 @@ pub fn validate_optional_tables( challenges: Option<&ConcreteSumcheckChallenges>, ) -> Result<(), SumcheckKernelError> where - F: Field, + F: JoltField, R: ConcreteSumcheck, SumcheckInputClaims: InputClaims, SumcheckOutputClaims: OutputClaims, @@ -236,7 +236,7 @@ pub fn extract_optional( inputs: Option<&SumcheckInputClaims>, ) -> Result>, SumcheckKernelError> where - F: Field, + F: JoltField, R: ConcreteSumcheck, SumcheckInputClaims: InputClaims, SumcheckOutputClaims: OutputClaims, @@ -379,7 +379,7 @@ macro_rules! impl_stage_prover { $({ name: $member:ident, relation: $relation:ident, presence: $presence:ident },)+ ] ) => { - impl $crate::driver::StageProver for $batch { + impl $crate::driver::StageProver for $batch { type InputClaims = $input_claims; type InputPoints = $input_points; type Challenges = $challenges_ty; @@ -487,7 +487,7 @@ macro_rules! impl_stage_prover { } } - impl $crate::driver::KernelSource> for B + impl $crate::driver::KernelSource> for B where B: ?Sized $(+ ::jolt_kernels::PrepareKernel>)+, { diff --git a/crates/jolt-prover/src/error.rs b/crates/jolt-prover/src/error.rs index 81fd307446..29fffe9fa0 100644 --- a/crates/jolt-prover/src/error.rs +++ b/crates/jolt-prover/src/error.rs @@ -1,4 +1,4 @@ -use jolt_field::FieldCore; +use jolt_field::Field; use jolt_kernels::{KernelError, SumcheckKernelError}; use jolt_sumcheck::SumcheckError; use jolt_verifier::VerifierError; @@ -10,7 +10,7 @@ use thiserror::Error; /// [`VerifierError`] — the prover runs the verifier's own relation methods as /// hard self-checks, so their errors are prover errors here. #[derive(Debug, Error)] -pub enum ProverError { +pub enum ProverError { #[error(transparent)] Sumcheck(#[from] SumcheckError), @@ -40,7 +40,7 @@ pub enum ProverError { /// [`ProverError::Kernel`] — [`KernelError`] already wraps /// [`SumcheckKernelError`] transparently, so a dedicated variant would surface /// the same failure under two names depending on path. -impl From> for ProverError { +impl From> for ProverError { fn from(error: SumcheckKernelError) -> Self { Self::Kernel(error.into()) } diff --git a/crates/jolt-prover/src/prover.rs b/crates/jolt-prover/src/prover.rs index 05c5a7be3d..2ed31dbda6 100644 --- a/crates/jolt-prover/src/prover.rs +++ b/crates/jolt-prover/src/prover.rs @@ -4,7 +4,7 @@ use common::jolt_device::JoltDevice; use jolt_crypto::{HomomorphicCommitment, VectorCommitment}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_kernels::JoltBackend; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme}; use jolt_transcript::{AppendToTranscript, Transcript}; @@ -53,7 +53,7 @@ pub fn prove( public_io: &JoltDevice, ) -> Result, ProverError> where - F: Field, + F: JoltField, PCS: CommitmentScheme + AdditivelyHomomorphic, PCS::Output: AppendToTranscript + HomomorphicCommitment, VC: VectorCommitment, diff --git a/crates/jolt-prover/src/stages/drivers.rs b/crates/jolt-prover/src/stages/drivers.rs index d0ffd5aa6b..382db416a1 100644 --- a/crates/jolt-prover/src/stages/drivers.rs +++ b/crates/jolt-prover/src/stages/drivers.rs @@ -167,7 +167,7 @@ mod twin_tests { JoltExpr, JoltOpeningId, JoltRelationId, JoltVirtualPolynomial, }; use jolt_claims::{opening, NoChallenges, OutputClaims as _, SymbolicSumcheck}; - use jolt_field::{Field, Fr, FromPrimitiveInt, RingCore}; + use jolt_field::{Fr, JoltField, Ring}; use jolt_kernels::{ KernelError, KernelSlots, PrepareKernel, ProofSession, ProverInputs, SumcheckKernel, SumcheckKernelError, @@ -249,14 +249,14 @@ mod twin_tests { 1 } - fn input_expression(&self) -> JoltExpr { + fn input_expression(&self) -> JoltExpr { opening(JoltOpeningId::virtual_polynomial( JoltVirtualPolynomial::$input, JoltRelationId::$rel, )) } - fn output_expression(&self) -> JoltExpr { + fn output_expression(&self) -> JoltExpr { opening(JoltOpeningId::virtual_polynomial( JoltVirtualPolynomial::$output, JoltRelationId::$rel, @@ -265,12 +265,12 @@ mod twin_tests { } #[derive(Clone)] - struct $relation { + struct $relation { symbolic: $symbolic, _field: PhantomData, } - impl $relation { + impl $relation { fn new(rounds: usize) -> Self { Self { symbolic: $symbolic::new(rounds), @@ -279,7 +279,7 @@ mod twin_tests { } } - impl ConcreteSumcheck for $relation { + impl ConcreteSumcheck for $relation { type Symbolic = $symbolic; fn symbolic(&self) -> &$symbolic { @@ -362,7 +362,7 @@ mod twin_tests { ); #[derive(SumcheckBatch)] - struct ToyDriverSumchecks { + struct ToyDriverSumchecks { alpha: ToyAlpha, beta: Option>, gamma: ToyGamma, @@ -372,7 +372,7 @@ mod twin_tests { /// member active from round 0, whose final bind the engine delivers only /// after the trailing dummy rounds (the delayed `finish_rounds` path). #[derive(SumcheckBatch)] - struct ToyHeadSumchecks { + struct ToyHeadSumchecks { alpha: ToyAlpha, delta: ToyDelta, } diff --git a/crates/jolt-prover/src/stages/stage0.rs b/crates/jolt-prover/src/stages/stage0.rs index 6cc60b9d94..138b41196a 100644 --- a/crates/jolt-prover/src/stages/stage0.rs +++ b/crates/jolt-prover/src/stages/stage0.rs @@ -12,7 +12,7 @@ use common::jolt_device::JoltDevice; use jolt_claims::protocols::jolt::JoltPolynomialId; use jolt_claims::protocols::jolt::{JoltCommittedPolynomial, TracePolynomialOrder}; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_kernels::reference::bytecode_read_raf::BytecodeReadRafWitness; use jolt_kernels::reference::instruction_read_raf::InstructionReadRafWitness; use jolt_kernels::{CommitmentGrid, JoltBackend, ProofSession, WitnessCommitment}; @@ -68,7 +68,7 @@ pub fn prove_stage0( public_io: &JoltDevice, ) -> Result, ProverError> where - F: Field, + F: JoltField, PCS: CommitmentScheme, PCS::Output: AppendToTranscript, VC: VectorCommitment, diff --git a/crates/jolt-prover/src/stages/stage1.rs b/crates/jolt-prover/src/stages/stage1.rs index 090ef71c28..d5549feeda 100644 --- a/crates/jolt-prover/src/stages/stage1.rs +++ b/crates/jolt-prover/src/stages/stage1.rs @@ -11,7 +11,7 @@ use jolt_claims::protocols::jolt::geometry::dimensions::{ OUTER_UNISKIP_DOMAIN_SIZE, OUTER_UNISKIP_FIRST_ROUND_DEGREE, }; use jolt_claims::protocols::jolt::geometry::spartan::SpartanOuterDimensions; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; use jolt_sumcheck::{prove_uniskip_clear, ClearSumcheckRecorder, SumcheckProof}; @@ -29,7 +29,7 @@ use crate::{ProverError, StageProver as _}; /// Stage 1's outputs: the two wire proofs, the wire claims, and the /// verifier-typed cross-stage carrier downstream stages consume. -pub struct Stage1ProverOutput { +pub struct Stage1ProverOutput { pub uniskip_proof: SumcheckProof, pub sumcheck_proof: SumcheckProof, pub claims: Stage1OutputClaims, @@ -45,7 +45,7 @@ pub fn prove_stage1( transcript: &mut T, ) -> Result, ProverError> where - F: Field, + F: JoltField, PCS: CommitmentScheme, C: Clone + AppendToTranscript, T: Transcript, diff --git a/crates/jolt-prover/src/stages/stage2.rs b/crates/jolt-prover/src/stages/stage2.rs index dff1849a88..626a68d2d8 100644 --- a/crates/jolt-prover/src/stages/stage2.rs +++ b/crates/jolt-prover/src/stages/stage2.rs @@ -16,7 +16,7 @@ use jolt_claims::protocols::jolt::geometry::ram::RamRafEvaluationDimensions; use jolt_claims::protocols::jolt::geometry::spartan::SpartanProductDimensions; use jolt_claims::protocols::jolt::{JoltRelationId, TraceDimensions}; use jolt_claims::NoChallenges; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; use jolt_program::preprocess::PublicIoMemory; @@ -44,7 +44,7 @@ use crate::{ProverConfig, ProverError, StageProver as _}; /// Stage 2's outputs: the two wire proofs, the wire claims, and the /// verifier-typed cross-stage carrier downstream stages consume. -pub struct Stage2ProverOutput { +pub struct Stage2ProverOutput { pub uniskip_proof: SumcheckProof, pub sumcheck_proof: SumcheckProof, pub claims: Stage2OutputClaims, @@ -62,7 +62,7 @@ pub fn prove_stage2( transcript: &mut T, ) -> Result, ProverError> where - F: Field, + F: JoltField, PCS: CommitmentScheme, C: Clone + AppendToTranscript, T: Transcript, diff --git a/crates/jolt-prover/src/stages/stage3.rs b/crates/jolt-prover/src/stages/stage3.rs index 3fc481f0f1..6a46736def 100644 --- a/crates/jolt-prover/src/stages/stage3.rs +++ b/crates/jolt-prover/src/stages/stage3.rs @@ -9,7 +9,7 @@ //! backend's slots. use jolt_claims::protocols::jolt::TraceDimensions; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; use jolt_sumcheck::{ClearSumcheckRecorder, SumcheckProof}; @@ -28,7 +28,7 @@ use crate::{ProverConfig, ProverError, StageProver as _}; /// Stage 3's outputs: the wire proof, the wire claims (the raw batch /// aggregate — no uni-skip wrapper), and the verifier-typed cross-stage /// carrier downstream stages consume. -pub struct Stage3ProverOutput { +pub struct Stage3ProverOutput { pub sumcheck_proof: SumcheckProof, pub claims: Stage3OutputClaims, pub clear_output: Stage3ClearOutput, @@ -45,7 +45,7 @@ pub fn prove_stage3( transcript: &mut T, ) -> Result, ProverError> where - F: Field, + F: JoltField, PCS: CommitmentScheme, C: Clone + AppendToTranscript, T: Transcript, diff --git a/crates/jolt-prover/src/stages/stage4.rs b/crates/jolt-prover/src/stages/stage4.rs index 32b68a96f2..51c4a3e0ce 100644 --- a/crates/jolt-prover/src/stages/stage4.rs +++ b/crates/jolt-prover/src/stages/stage4.rs @@ -14,7 +14,7 @@ use jolt_claims::protocols::jolt::geometry::dimensions::REGISTER_ADDRESS_BITS; use jolt_claims::protocols::jolt::{JoltRelationId, TraceDimensions}; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; use jolt_poly::sparse_segments_mle_msb; @@ -39,7 +39,7 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _ /// Stage 4's outputs: the wire proof, the wire claims, and the verifier-typed /// cross-stage carrier downstream stages consume. -pub struct Stage4ProverOutput { +pub struct Stage4ProverOutput { pub sumcheck_proof: SumcheckProof, pub claims: Stage4OutputClaims, pub clear_output: Stage4ClearOutput, @@ -59,7 +59,7 @@ pub fn prove_stage4( transcript: &mut T, ) -> Result, ProverError> where - F: Field, + F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, C: Clone + AppendToTranscript, diff --git a/crates/jolt-prover/src/stages/stage5.rs b/crates/jolt-prover/src/stages/stage5.rs index 5a808c44f7..71462b88af 100644 --- a/crates/jolt-prover/src/stages/stage5.rs +++ b/crates/jolt-prover/src/stages/stage5.rs @@ -13,7 +13,7 @@ use jolt_claims::protocols::jolt::JoltRelationId; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; use jolt_sumcheck::{ClearSumcheckRecorder, SumcheckProof}; @@ -36,7 +36,7 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _ /// Stage 5's outputs: the wire proof, the wire claims, and the verifier-typed /// cross-stage carrier downstream stages consume. -pub struct Stage5ProverOutput { +pub struct Stage5ProverOutput { pub sumcheck_proof: SumcheckProof, pub claims: Stage5OutputClaims, pub clear_output: Stage5ClearOutput, @@ -56,7 +56,7 @@ pub fn prove_stage5( transcript: &mut T, ) -> Result, ProverError> where - F: Field, + F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, C: Clone + AppendToTranscript, diff --git a/crates/jolt-prover/src/stages/stage6a.rs b/crates/jolt-prover/src/stages/stage6a.rs index 2f34dca603..956e1ce800 100644 --- a/crates/jolt-prover/src/stages/stage6a.rs +++ b/crates/jolt-prover/src/stages/stage6a.rs @@ -16,7 +16,7 @@ use jolt_claims::protocols::jolt::JoltRelationId; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; use jolt_sumcheck::{ClearSumcheckRecorder, SumcheckProof}; @@ -40,7 +40,7 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _ /// Stage 6a's outputs: the wire proof, the wire claims, and the verifier-typed /// cross-stage carrier stage 6b consumes. -pub struct Stage6aProverOutput { +pub struct Stage6aProverOutput { pub sumcheck_proof: SumcheckProof, pub claims: Stage6aOutputClaims, pub clear_output: Stage6aClearOutput, @@ -63,7 +63,7 @@ pub fn prove_stage6a( transcript: &mut T, ) -> Result, ProverError> where - F: Field, + F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, C: Clone + AppendToTranscript, diff --git a/crates/jolt-prover/src/stages/stage6b.rs b/crates/jolt-prover/src/stages/stage6b.rs index 793bf8d61c..24999dd2bd 100644 --- a/crates/jolt-prover/src/stages/stage6b.rs +++ b/crates/jolt-prover/src/stages/stage6b.rs @@ -22,7 +22,7 @@ use jolt_claims::protocols::jolt::{JoltAdviceKind, JoltRelationId}; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; use jolt_sumcheck::{ClearSumcheckRecorder, SumcheckProof}; @@ -50,7 +50,7 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _ /// cross-stage carrier stage 7 consumes. The precommitted reduction state /// that spans into stage 7's address phase travels as `ProofSession` carries, /// not output fields. -pub struct Stage6bProverOutput { +pub struct Stage6bProverOutput { pub sumcheck_proof: SumcheckProof, pub claims: Stage6bOutputClaims, pub clear_output: Stage6bClearOutput, @@ -74,7 +74,7 @@ pub fn prove_stage6b( transcript: &mut T, ) -> Result, ProverError> where - F: Field, + F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, C: Clone + AppendToTranscript, diff --git a/crates/jolt-prover/src/stages/stage7.rs b/crates/jolt-prover/src/stages/stage7.rs index 073284a453..6316fb06db 100644 --- a/crates/jolt-prover/src/stages/stage7.rs +++ b/crates/jolt-prover/src/stages/stage7.rs @@ -14,7 +14,7 @@ use jolt_claims::protocols::jolt::geometry::claim_reductions::hamming_weight::HammingWeightClaimReductionDimensions; use jolt_claims::protocols::jolt::JoltRelationId; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; use jolt_sumcheck::{ClearSumcheckRecorder, SumcheckProof}; @@ -30,7 +30,7 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _ /// Stage 7's outputs: the wire proof, the wire claims, and the verifier-typed /// cross-stage carrier stage 8 consumes. -pub struct Stage7ProverOutput { +pub struct Stage7ProverOutput { pub sumcheck_proof: SumcheckProof, pub claims: Stage7OutputClaims, pub clear_output: Stage7ClearOutput, @@ -50,7 +50,7 @@ pub fn prove_stage7( transcript: &mut T, ) -> Result, ProverError> where - F: Field, + F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, C: Clone + AppendToTranscript, diff --git a/crates/jolt-prover/src/stages/stage8.rs b/crates/jolt-prover/src/stages/stage8.rs index 6325562b89..ddb1f4c0a6 100644 --- a/crates/jolt-prover/src/stages/stage8.rs +++ b/crates/jolt-prover/src/stages/stage8.rs @@ -22,7 +22,7 @@ use jolt_claims::protocols::jolt::geometry::committed_openings::{ use jolt_claims::protocols::jolt::geometry::dimensions::JoltFormulaDimensions; use jolt_claims::protocols::jolt::{JoltCommittedPolynomial, JoltRelationId}; use jolt_crypto::{HomomorphicCommitment, VectorCommitment}; -use jolt_field::Field; +use jolt_field::JoltField; use std::collections::BTreeMap; use jolt_kernels::committed_program::{ @@ -69,7 +69,7 @@ pub fn prove_stage8( transcript: &mut T, ) -> Result, ProverError> where - F: Field, + F: JoltField, PCS: CommitmentScheme + AdditivelyHomomorphic, PCS::Output: HomomorphicCommitment, VC: VectorCommitment, diff --git a/crates/jolt-prover/tests/byte_diff.rs b/crates/jolt-prover/tests/byte_diff.rs index 799cc36ea5..52f6105cd9 100644 --- a/crates/jolt-prover/tests/byte_diff.rs +++ b/crates/jolt-prover/tests/byte_diff.rs @@ -32,7 +32,7 @@ mod support { use jolt_claims::protocols::jolt::{JoltCommittedPolynomial, TracePolynomialOrder}; use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::{DoryCommitment, DoryScheme}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_kernels::committed_program::{ build_committed_bytecode_chunk_coeffs, program_image_words_padded, }; diff --git a/crates/jolt-prover/tests/engine_twins.rs b/crates/jolt-prover/tests/engine_twins.rs index 3c75ca4992..759aa43e8c 100644 --- a/crates/jolt-prover/tests/engine_twins.rs +++ b/crates/jolt-prover/tests/engine_twins.rs @@ -11,7 +11,7 @@ use jolt_claims::protocols::jolt::geometry::instruction::InstructionReadRafDimen use jolt_claims::protocols::jolt::relations::instruction::InstructionReadRafInputClaims; use jolt_claims::protocols::jolt::relations::registers::RegistersValEvaluationInputClaims; use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup}; -use jolt_field::{Field, Fr, FromPrimitiveInt}; +use jolt_field::{Fr, JoltField, Ring}; use jolt_poly::{UnivariatePoly, UnivariatePolynomial}; use jolt_sumcheck::{ prove_batch, prove_uniskip_clear, CenteredIntegerDomain, ClearRound, ClearSumcheckRecorder, @@ -24,7 +24,7 @@ use jolt_verifier::stages::stage5::{InstructionReadRaf, RegistersValEvaluation}; use jolt_verifier::stages::uniskip::{self, UniskipParams}; #[derive(SumcheckBatch)] -struct TwinFixtureSumchecks { +struct TwinFixtureSumchecks { instruction_read_raf: InstructionReadRaf, registers_val_evaluation: RegistersValEvaluation, } diff --git a/crates/jolt-r1cs/src/builder.rs b/crates/jolt-r1cs/src/builder.rs index 9f41b7e867..46c79d584f 100644 --- a/crates/jolt-r1cs/src/builder.rs +++ b/crates/jolt-r1cs/src/builder.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use std::ops::{Add, Neg, Sub}; -use jolt_field::Field; +use jolt_field::JoltField; use thiserror::Error; use crate::constraint::SparseRow; @@ -45,7 +45,7 @@ impl LinearCombination { } } -impl LinearCombination { +impl LinearCombination { pub fn one() -> Self { Self::constant(F::one()) } @@ -122,7 +122,7 @@ impl LinearCombination { } } -impl From for LinearCombination { +impl From for LinearCombination { fn from(variable: Variable) -> Self { Self::variable(variable) } @@ -137,7 +137,7 @@ impl Add for LinearCombination { } } -impl Sub for LinearCombination { +impl Sub for LinearCombination { type Output = Self; fn sub(self, rhs: Self) -> Self::Output { @@ -145,7 +145,7 @@ impl Sub for LinearCombination { } } -impl Neg for LinearCombination { +impl Neg for LinearCombination { type Output = Self; fn neg(mut self) -> Self::Output { @@ -157,20 +157,20 @@ impl Neg for LinearCombination { } #[derive(Clone, Debug)] -pub struct R1csBuilder { +pub struct R1csBuilder { witness: Vec>, a: Vec>, b: Vec>, c: Vec>, } -impl Default for R1csBuilder { +impl Default for R1csBuilder { fn default() -> Self { Self::new() } } -impl R1csBuilder { +impl R1csBuilder { pub fn new() -> Self { Self { witness: vec![Some(F::one())], @@ -300,7 +300,7 @@ impl R1csBuilder { #[expect(clippy::expect_used, reason = "tests may panic on assertion failures")] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; #[test] fn builder_checks_satisfied_product() { diff --git a/crates/jolt-r1cs/src/constraint.rs b/crates/jolt-r1cs/src/constraint.rs index db8a940b53..53be1e01ac 100644 --- a/crates/jolt-r1cs/src/constraint.rs +++ b/crates/jolt-r1cs/src/constraint.rs @@ -1,6 +1,6 @@ //! Sparse per-cycle R1CS constraint matrices. -use jolt_field::Field; +use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use thiserror::Error as ThisError; @@ -38,7 +38,7 @@ pub enum ConstraintMatrixEvalError { bound(serialize = "F: Serialize", deserialize = "F: for<'a> Deserialize<'a>"), try_from = "RawConstraintMatrices" )] -pub struct ConstraintMatrices { +pub struct ConstraintMatrices { pub num_constraints: usize, pub num_vars: usize, pub a: Vec>, @@ -47,14 +47,14 @@ pub struct ConstraintMatrices { } #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct WeightedMatrixColumns { +pub struct WeightedMatrixColumns { pub a: Vec, pub b: Vec, pub c: Vec, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct MatrixColumnContributions { +pub struct MatrixColumnContributions { pub a: F, pub b: F, pub c: F, @@ -63,7 +63,7 @@ pub struct MatrixColumnContributions { /// Deserialization helper; never exposed directly. #[derive(Deserialize)] #[serde(bound(deserialize = "F: for<'a> Deserialize<'a>"))] -struct RawConstraintMatrices { +struct RawConstraintMatrices { num_constraints: usize, num_vars: usize, a: Vec>, @@ -71,7 +71,7 @@ struct RawConstraintMatrices { c: Vec>, } -impl TryFrom> for ConstraintMatrices { +impl TryFrom> for ConstraintMatrices { type Error = String; fn try_from(raw: RawConstraintMatrices) -> Result { @@ -93,7 +93,7 @@ impl TryFrom> for ConstraintMatrices { } } -fn check_invariants( +fn check_invariants( num_constraints: usize, num_vars: usize, a: &[SparseRow], @@ -120,7 +120,7 @@ fn check_invariants( Ok(()) } -impl ConstraintMatrices { +impl ConstraintMatrices { /// Builds constraint matrices from sparse rows. /// /// # Panics @@ -253,7 +253,7 @@ impl ConstraintMatrices { } #[inline] -fn dot(row: &[(usize, F)], witness: &[F]) -> F { +fn dot(row: &[(usize, F)], witness: &[F]) -> F { let mut acc = F::zero(); for &(col, coeff) in row { acc += coeff * witness[col]; @@ -261,7 +261,7 @@ fn dot(row: &[(usize, F)], witness: &[F]) -> F { acc } -fn matrix_column_eval( +fn matrix_column_eval( rows: &[SparseRow], row_weights: &[F], column: usize, @@ -284,7 +284,7 @@ fn matrix_column_eval( Ok(acc) } -fn matrix_bilinear_eval_columns( +fn matrix_bilinear_eval_columns( rows: &[SparseRow], row_weights: &[F], column_weights: &[F], @@ -326,7 +326,7 @@ fn matrix_bilinear_eval_columns( #[expect(clippy::expect_used, reason = "tests should fail loudly")] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; #[test] fn satisfied_constraint() { diff --git a/crates/jolt-r1cs/src/constraints/field_constraints.rs b/crates/jolt-r1cs/src/constraints/field_constraints.rs index e378a922fc..721e875b32 100644 --- a/crates/jolt-r1cs/src/constraints/field_constraints.rs +++ b/crates/jolt-r1cs/src/constraints/field_constraints.rs @@ -6,7 +6,7 @@ //! layered on once those bridge payloads are explicit in the trace. use crate::constraint::SparseRow; -use jolt_field::Field; +use jolt_field::JoltField; type ConstraintRows = (Vec>, Vec>, Vec>); @@ -61,7 +61,7 @@ pub const fn input_column(input_index: usize) -> Option { } } -fn row(entries: &[(usize, i64)]) -> SparseRow { +fn row(entries: &[(usize, i64)]) -> SparseRow { entries .iter() .filter(|(_, coefficient)| *coefficient != 0) @@ -69,7 +69,7 @@ fn row(entries: &[(usize, i64)]) -> SparseRow { .collect() } -fn field_eq_constraint_rows() -> ConstraintRows { +fn field_eq_constraint_rows() -> ConstraintRows { let mut a_rows = Vec::with_capacity(NUM_EQ_CONSTRAINTS); let mut b_rows = Vec::with_capacity(NUM_EQ_CONSTRAINTS); let mut c_rows = Vec::with_capacity(NUM_EQ_CONSTRAINTS); @@ -122,7 +122,7 @@ fn field_eq_constraint_rows() -> ConstraintRows { (a_rows, b_rows, c_rows) } -fn append_product_constraints( +fn append_product_constraints( a_rows: &mut Vec>, b_rows: &mut Vec>, c_rows: &mut Vec>, @@ -140,7 +140,7 @@ fn append_product_constraints( /// /// Product constraints are intentionally excluded for consumers that handle the /// field multiplication checks in a separate protocol step. -pub fn field_inline_spartan_outer_constraints() -> crate::ConstraintMatrices { +pub fn field_inline_spartan_outer_constraints() -> crate::ConstraintMatrices { let (a_rows, b_rows, c_rows) = field_eq_constraint_rows(); crate::ConstraintMatrices::new( NUM_EQ_CONSTRAINTS, @@ -156,7 +156,7 @@ pub fn field_inline_spartan_outer_constraints() -> crate::ConstraintMa /// Returns 10 constraints over 17 variables per cycle: /// - 8 equality-conditional rows: `guard * (left - right) = 0` /// - 2 product rows for `FieldProduct` and `FieldInvProduct` -pub fn field_inline_trace_constraints() -> crate::ConstraintMatrices { +pub fn field_inline_trace_constraints() -> crate::ConstraintMatrices { let (mut a_rows, mut b_rows, mut c_rows) = field_eq_constraint_rows(); a_rows.reserve(NUM_PRODUCT_CONSTRAINTS); b_rows.reserve(NUM_PRODUCT_CONSTRAINTS); @@ -176,7 +176,7 @@ pub fn field_inline_trace_constraints() -> crate::ConstraintMatrices Vec { diff --git a/crates/jolt-r1cs/src/constraints/jolt.rs b/crates/jolt-r1cs/src/constraints/jolt.rs index c01e8c882b..842c6bcf5a 100644 --- a/crates/jolt-r1cs/src/constraints/jolt.rs +++ b/crates/jolt-r1cs/src/constraints/jolt.rs @@ -1,6 +1,6 @@ //! Compile-time Jolt R1CS composition. -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{ lagrange::{centered_lagrange_evals, centered_lagrange_kernel, CenteredIntegerDomainError}, EqPolynomial, @@ -108,7 +108,7 @@ pub const SPARTAN_OUTER_SECOND_GROUP_ROWS: [usize; SPARTAN_OUTER_SECOND_GROUP_RO rv64::NUM_EQ_CONSTRAINTS + field_constraints::ROW_LOAD_IMM, ]; -pub fn spartan_outer_constraints() -> ConstraintMatrices { +pub fn spartan_outer_constraints() -> ConstraintMatrices { let constraints = rv64::rv64_spartan_outer_constraints(); #[cfg(feature = "field-inline")] { @@ -123,7 +123,7 @@ pub fn spartan_outer_constraints() -> ConstraintMatrices { } } -pub fn trace_constraints() -> ConstraintMatrices { +pub fn trace_constraints() -> ConstraintMatrices { let constraints = rv64::rv64_trace_constraints(); #[cfg(feature = "field-inline")] { @@ -138,7 +138,7 @@ pub fn trace_constraints() -> ConstraintMatrices { } } -pub fn spartan_outer_row_weights( +pub fn spartan_outer_row_weights( uniskip: F, stream: F, ) -> Result, CenteredIntegerDomainError> { @@ -199,7 +199,7 @@ pub enum JoltSpartanOuterRemainderError { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct JoltSpartanOuterRemainder { +pub struct JoltSpartanOuterRemainder { tau_kernel: F, az_coefficients: Vec, bz_coefficients: Vec, @@ -214,7 +214,7 @@ pub struct JoltSpartanOuterRemainderChallenges<'a, F> { pub remainder: &'a [F], } -impl JoltSpartanOuterRemainder { +impl JoltSpartanOuterRemainder { pub fn new( challenges: JoltSpartanOuterRemainderChallenges<'_, F>, ) -> Result { @@ -282,7 +282,7 @@ impl JoltSpartanOuterRemainder { } } -fn spartan_outer_tau_kernel( +fn spartan_outer_tau_kernel( tau: &[F], uniskip: F, remainder_challenges: &[F], @@ -303,7 +303,7 @@ fn spartan_outer_tau_kernel( Ok(tau_high_bound_r0 * EqPolynomial::::mle(&tau[..tau.len() - 1], &reversed_challenges)) } -fn eval_linear_form(coefficients: &[F], constant: F, inputs: &[F]) -> F { +fn eval_linear_form(coefficients: &[F], constant: F, inputs: &[F]) -> F { coefficients .iter() .zip(inputs) @@ -345,7 +345,7 @@ pub const fn field_inline_input_column(input_index: usize) -> Option { } #[cfg(feature = "field-inline")] -fn append_field_inline_columns( +fn append_field_inline_columns( base: ConstraintMatrices, extension: ConstraintMatrices, ) -> ConstraintMatrices { @@ -363,7 +363,7 @@ fn append_field_inline_columns( } #[cfg(feature = "field-inline")] -fn remap_rows(rows: Vec>) -> Vec> { +fn remap_rows(rows: Vec>) -> Vec> { rows.into_iter() .map(|row| { row.into_iter() @@ -400,7 +400,7 @@ mod tests { use jolt_claims::protocols::jolt::{ geometry::spartan::SpartanOuterDimensions, SpartanOuterPublic, }; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; #[cfg(feature = "field-inline")] use num_traits::Zero; diff --git a/crates/jolt-r1cs/src/constraints/rv64.rs b/crates/jolt-r1cs/src/constraints/rv64.rs index 669382e27a..020934fbbe 100644 --- a/crates/jolt-r1cs/src/constraints/rv64.rs +++ b/crates/jolt-r1cs/src/constraints/rv64.rs @@ -91,7 +91,7 @@ use jolt_claims::protocols::jolt::{ }, SpartanOuterPublic, }; -use jolt_field::Field; +use jolt_field::JoltField; use thiserror::Error as ThisError; type ConstraintRows = (Vec>, Vec>, Vec>); @@ -129,7 +129,7 @@ pub enum Rv64SpartanOuterRemainderError { /// Coefficients needed to evaluate the RV64 Spartan outer remainder claim. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Rv64SpartanOuterRemainder { +pub struct Rv64SpartanOuterRemainder { tau_kernel: F, linear_forms: SpartanOuterLinearForms, } @@ -142,7 +142,7 @@ pub struct Rv64SpartanOuterRemainderChallenges<'a, F> { pub remainder: &'a [F], } -impl Rv64SpartanOuterRemainder { +impl Rv64SpartanOuterRemainder { /// Derives the verifier-side remainder claim coefficients for RV64. pub fn new( dimensions: &SpartanOuterDimensions, @@ -220,7 +220,7 @@ impl Rv64SpartanOuterRemainder { } } -fn eval_linear_form(coefficients: &[F], constant: F, inputs: &[F]) -> F { +fn eval_linear_form(coefficients: &[F], constant: F, inputs: &[F]) -> F { coefficients .iter() .zip(inputs) @@ -238,7 +238,7 @@ fn eval_linear_form(coefficients: &[F], constant: F, inputs: &[F]) -> clippy::expect_used, reason = "compile-time constant table; silent i128→i64 truncation would be a correctness bug" )] -fn row(entries: &[(usize, i128)]) -> SparseRow { +fn row(entries: &[(usize, i128)]) -> SparseRow { entries .iter() .filter(|(_, c)| *c != 0) @@ -251,7 +251,7 @@ fn row(entries: &[(usize, i128)]) -> SparseRow { /// Helper: sparse row entry from i128 coefficient, handling large constants /// that don't fit in i64 (e.g. 2^64 bias). -fn row_wide(entries: &[(usize, i128)]) -> SparseRow { +fn row_wide(entries: &[(usize, i128)]) -> SparseRow { entries .iter() .filter(|(_, c)| *c != 0) @@ -259,7 +259,7 @@ fn row_wide(entries: &[(usize, i128)]) -> SparseRow { .collect() } -fn rv64_eq_constraint_rows() -> ConstraintRows { +fn rv64_eq_constraint_rows() -> ConstraintRows { let mut a_rows: Vec> = Vec::with_capacity(NUM_EQ_CONSTRAINTS); let mut b_rows: Vec> = Vec::with_capacity(NUM_EQ_CONSTRAINTS); let mut c_rows: Vec> = Vec::with_capacity(NUM_EQ_CONSTRAINTS); @@ -507,7 +507,7 @@ fn rv64_eq_constraint_rows() -> ConstraintRows { (a_rows, b_rows, c_rows) } -fn append_product_constraints( +fn append_product_constraints( a_rows: &mut Vec>, b_rows: &mut Vec>, c_rows: &mut Vec>, @@ -537,7 +537,7 @@ fn append_product_constraints( /// standard 38-variable per-cycle witness layout. Product constraints are /// intentionally excluded for consumers that handle multiplication checks in /// a separate protocol step. -pub fn rv64_spartan_outer_constraints() -> crate::ConstraintMatrices { +pub fn rv64_spartan_outer_constraints() -> crate::ConstraintMatrices { let (a_rows, b_rows, c_rows) = rv64_eq_constraint_rows(); crate::ConstraintMatrices::new( NUM_EQ_CONSTRAINTS, @@ -556,7 +556,7 @@ pub fn rv64_spartan_outer_constraints() -> crate::ConstraintMatrices() -> crate::ConstraintMatrices { +pub fn rv64_trace_constraints() -> crate::ConstraintMatrices { let (mut a_rows, mut b_rows, mut c_rows) = rv64_eq_constraint_rows(); a_rows.reserve(NUM_PRODUCT_CONSTRAINTS); b_rows.reserve(NUM_PRODUCT_CONSTRAINTS); @@ -576,7 +576,7 @@ pub fn rv64_trace_constraints() -> crate::ConstraintMatrices { #[expect(clippy::expect_used, reason = "tests may unwind via panic")] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use num_traits::Zero; /// A no-op cycle: const=1, all else zero. All eq-conditional guards diff --git a/crates/jolt-r1cs/src/key.rs b/crates/jolt-r1cs/src/key.rs index b097575afb..5eb97b6cd4 100644 --- a/crates/jolt-r1cs/src/key.rs +++ b/crates/jolt-r1cs/src/key.rs @@ -15,7 +15,7 @@ //! Matrix MLE factors as: //! $$\tilde{M}(r_x, r_y) = \widetilde{eq}(r_x^{cyc}, r_y^{cyc}) \cdot \tilde{M}_{local}(r_x^{con}, r_y^{var})$$ -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::EqPolynomial; use serde::{Deserialize, Serialize}; @@ -27,14 +27,14 @@ use crate::constraint::ConstraintMatrices; /// All evaluation methods exploit the uniform (repeated-constraint) structure. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: for<'a> Deserialize<'a>"))] -pub struct R1csKey { +pub struct R1csKey { pub matrices: ConstraintMatrices, pub num_cycles: usize, pub num_constraints_padded: usize, pub num_vars_padded: usize, } -impl R1csKey { +impl R1csKey { /// Creates a new key from per-cycle constraints and cycle count. /// /// # Panics @@ -280,7 +280,7 @@ impl R1csKey { mod tests { use super::*; use crate::constraint::ConstraintMatrices; - use jolt_field::{FieldCore, Fr, FromPrimitiveInt}; + use jolt_field::{Field, Fr, Ring}; use num_traits::{One, Zero}; /// x * x = y, y * x = z — 2 constraints, 4 vars [1, x, y, z] diff --git a/crates/jolt-r1cs/src/lowering.rs b/crates/jolt-r1cs/src/lowering.rs index 7bc7b9d5a0..9b8cfccd69 100644 --- a/crates/jolt-r1cs/src/lowering.rs +++ b/crates/jolt-r1cs/src/lowering.rs @@ -1,5 +1,5 @@ use jolt_claims::{Expr, Source}; -use jolt_field::Field; +use jolt_field::JoltField; use thiserror::Error; use crate::{LinearCombination, R1csBuilder, Variable}; @@ -30,7 +30,7 @@ pub enum SourceValue { LinearCombination(LinearCombination), } -impl SourceValue { +impl SourceValue { pub fn variable(variable: Variable) -> Self { Self::LinearCombination(LinearCombination::variable(variable)) } @@ -65,7 +65,7 @@ impl ClaimSourceTable { pub fn insert_opening(&mut self, id: O, variable: Variable) where - F: Field, + F: JoltField, O: PartialEq, { self.insert_opening_source(id, SourceValue::variable(variable)); @@ -73,7 +73,7 @@ impl ClaimSourceTable { pub fn insert_opening_lc(&mut self, id: O, linear_combination: LinearCombination) where - F: Field, + F: JoltField, O: PartialEq, { self.insert_opening_source(id, SourceValue::linear_combination(linear_combination)); @@ -99,7 +99,7 @@ impl ClaimSourceTable { pub fn insert_challenge_lc(&mut self, id: C, linear_combination: LinearCombination) where - F: Field, + F: JoltField, C: PartialEq, { self.insert_challenge_source(id, SourceValue::linear_combination(linear_combination)); @@ -128,7 +128,7 @@ impl ClaimSourceTable { pub fn insert_public_lc(&mut self, id: P, linear_combination: LinearCombination) where - F: Field, + F: JoltField, P: PartialEq, { self.insert_public_source(id, SourceValue::linear_combination(linear_combination)); @@ -181,7 +181,7 @@ pub fn lower_claim_expr( sources: &mut R, ) -> Result, ClaimLoweringError> where - F: Field, + F: JoltField, R: ClaimSources, { let mut result = LinearCombination::zero(); @@ -217,7 +217,7 @@ pub fn assert_claim_expr_eq( sources: &mut R, ) -> Result<(), ClaimLoweringError> where - F: Field, + F: JoltField, R: ClaimSources, Expected: Into>, { @@ -226,7 +226,7 @@ where Ok(()) } -fn lower_product( +fn lower_product( builder: &mut R1csBuilder, coefficient: F, factors: Vec>, @@ -252,7 +252,7 @@ fn lower_product( mod tests { use super::*; use jolt_claims::{challenge, constant, derived, opening, Expr}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Opening { diff --git a/crates/jolt-r1cs/src/provider.rs b/crates/jolt-r1cs/src/provider.rs index 79f52c9509..7fd0d575f5 100644 --- a/crates/jolt-r1cs/src/provider.rs +++ b/crates/jolt-r1cs/src/provider.rs @@ -5,7 +5,7 @@ //! //! Used internally by `ProverData` — not a standalone `BufferProvider`. -use jolt_field::Field; +use jolt_field::JoltField; use crate::column::R1csColumn; use crate::key::R1csKey; @@ -25,13 +25,13 @@ pub struct SpartanChallenges { /// Handles `PolySource::R1cs(column)`: Az, Bz, Cz (sparse matvec), /// CombinedRow (linear combination with Spartan challenges), and /// Variable(i) column extraction from the per-cycle witness vector. -pub struct R1csSource<'a, F: Field> { +pub struct R1csSource<'a, F: JoltField> { key: &'a R1csKey, witness: &'a [F], challenges: Option>, } -impl<'a, F: Field> R1csSource<'a, F> { +impl<'a, F: JoltField> R1csSource<'a, F> { pub fn new(key: &'a R1csKey, witness: &'a [F]) -> Self { Self { key, @@ -147,7 +147,7 @@ impl<'a, F: Field> R1csSource<'a, F> { mod tests { use super::*; use crate::constraint::ConstraintMatrices; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use num_traits::{One, Zero}; #[test] diff --git a/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs b/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs index d735da97d0..92f06de7b8 100644 --- a/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs +++ b/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs @@ -8,7 +8,7 @@ #![no_main] -use jolt_field::{Fr, CanonicalRepr}; +use jolt_field::{Fr, CanonicalEncoding}; use jolt_poly::UnivariatePoly; use jolt_sumcheck::{BooleanHypercube, SumcheckClaim, SumcheckVerifier}; use jolt_transcript::{Blake2bTranscript, Transcript}; @@ -72,5 +72,5 @@ fuzz_target!(|data: &[u8]| { #[inline] fn read_scalar(bytes: &[u8]) -> Fr { debug_assert_eq!(bytes.len(), SCALAR_BYTES); - ::from_le_bytes_mod_order(bytes) + ::from_bytes_le_reduced(bytes) } diff --git a/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs b/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs index 98708458fe..c1be71bd5d 100644 --- a/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs +++ b/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs @@ -22,7 +22,7 @@ #![no_main] -use jolt_field::{Fr, CanonicalRepr}; +use jolt_field::{Fr, CanonicalEncoding}; use jolt_poly::UnivariatePoly; use jolt_sumcheck::{BooleanHypercube, SumcheckClaim, SumcheckVerifier}; use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; @@ -138,5 +138,5 @@ fuzz_target!(|data: &[u8]| { #[inline] fn read_scalar(bytes: &[u8]) -> Fr { debug_assert_eq!(bytes.len(), SCALAR_BYTES); - ::from_le_bytes_mod_order(bytes) + ::from_bytes_le_reduced(bytes) } diff --git a/crates/jolt-sumcheck/src/batch.rs b/crates/jolt-sumcheck/src/batch.rs index ec57710e08..b0daaedee7 100644 --- a/crates/jolt-sumcheck/src/batch.rs +++ b/crates/jolt-sumcheck/src/batch.rs @@ -8,7 +8,7 @@ //! head's output in engine form: plain positional data with no per-stage //! types, so this crate's provers can consume it without naming any stage. -use jolt_field::Field; +use jolt_field::JoltField; /// One present batch member: its input claim (the member's initial running /// claim), its batching coefficient, its round count, and its activation @@ -41,7 +41,7 @@ pub struct BatchPrelude { pub max_degree: usize, } -impl BatchPrelude { +impl BatchPrelude { /// Combine `members` into the batch's initial running claim. The /// `2^(max_num_vars − rounds)` scale is each shorter member's dummy-round /// padding — its summand extended constantly over the batch's extra diff --git a/crates/jolt-sumcheck/src/claim.rs b/crates/jolt-sumcheck/src/claim.rs index 7e9172e818..f10b081318 100644 --- a/crates/jolt-sumcheck/src/claim.rs +++ b/crates/jolt-sumcheck/src/claim.rs @@ -1,6 +1,6 @@ //! Sumcheck claim: the public statement that the protocol proves. -use jolt_field::FieldCore; +use jolt_field::Field; pub use jolt_openings::EvaluationClaim; @@ -26,7 +26,7 @@ impl SumcheckStatement { } } -impl From<&SumcheckClaim> for SumcheckStatement { +impl From<&SumcheckClaim> for SumcheckStatement { fn from(claim: &SumcheckClaim) -> Self { Self { num_vars: claim.num_vars, @@ -46,7 +46,7 @@ impl From<&SumcheckClaim> for SumcheckStatement { /// For a product of $k$ multilinear polynomials, `degree = k`. /// * `claimed_sum` -- the value $C$ that the prover claims the sum equals. #[derive(Clone, Debug)] -pub struct SumcheckClaim { +pub struct SumcheckClaim { /// Number of Boolean variables in the summation. pub num_vars: usize, /// Maximum degree of each round polynomial. @@ -55,7 +55,7 @@ pub struct SumcheckClaim { pub claimed_sum: F, } -impl SumcheckClaim { +impl SumcheckClaim { /// Construct a sumcheck claim. /// /// # Panics diff --git a/crates/jolt-sumcheck/src/committed.rs b/crates/jolt-sumcheck/src/committed.rs index 5355a9489a..08cf0a5793 100644 --- a/crates/jolt-sumcheck/src/committed.rs +++ b/crates/jolt-sumcheck/src/committed.rs @@ -1,7 +1,7 @@ //! Committed sumcheck round messages. use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::UnivariatePoly; use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript}; use rand_core::RngCore; @@ -98,14 +98,14 @@ impl CommittedSumcheckConsistency { /// precommitted claim-reduction phases) bind the leading challenges and need /// their offset supplied explicitly via [`Self::try_instance_point_at`]. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct BatchedCommittedSumcheckConsistency { +pub struct BatchedCommittedSumcheckConsistency { pub consistency: CommittedSumcheckConsistency, pub batching_coefficients: Vec, pub max_num_vars: usize, pub max_degree: usize, } -impl BatchedCommittedSumcheckConsistency { +impl BatchedCommittedSumcheckConsistency { /// Returns the tail-aligned default offset (`max_num_vars - num_vars`) /// for an instance with `num_vars` — the suffix start when the instance's /// dummy rounds are front-loaded. Head-aligned instances must not use @@ -199,7 +199,7 @@ impl CommittedSumcheckWitness { /// caller owns the randomness source, so a fixed seed reproduces the proof. pub struct CommittedSumcheckBuilder<'a, F, VC, R> where - F: Field, + F: JoltField, VC: VectorCommitment, R: RngCore, { @@ -211,7 +211,7 @@ where impl<'a, F, VC, R> CommittedSumcheckBuilder<'a, F, VC, R> where - F: Field, + F: JoltField, VC: VectorCommitment, R: RngCore, { @@ -303,7 +303,7 @@ pub struct CommittedRoundWitness { pub blinding: F, } -impl CommittedRoundWitness { +impl CommittedRoundWitness { pub fn commit( &self, setup: &VC::Setup, diff --git a/crates/jolt-sumcheck/src/error.rs b/crates/jolt-sumcheck/src/error.rs index 43a48ecce2..4a0e2966f7 100644 --- a/crates/jolt-sumcheck/src/error.rs +++ b/crates/jolt-sumcheck/src/error.rs @@ -1,6 +1,6 @@ //! Error types for sumcheck protocol verification failures. -use jolt_field::FieldCore; +use jolt_field::Field; /// Errors that can occur during sumcheck verification. /// @@ -9,7 +9,7 @@ use jolt_field::FieldCore; /// diverged. #[derive(Debug, thiserror::Error)] #[non_exhaustive] -pub enum SumcheckError { +pub enum SumcheckError { /// Round check failed: the domain sum did not match the expected value /// carried forward from the previous round. #[error("round {round}: expected sum {expected}, got {actual}")] diff --git a/crates/jolt-sumcheck/src/proof.rs b/crates/jolt-sumcheck/src/proof.rs index eff1b59dfe..f09b48f5d8 100644 --- a/crates/jolt-sumcheck/src/proof.rs +++ b/crates/jolt-sumcheck/src/proof.rs @@ -25,26 +25,26 @@ use serde::{Deserialize, Serialize}; /// $(r_1, \ldots, r_n)$. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: for<'a> Deserialize<'a>"))] -pub struct ClearSumcheckProof { +pub struct ClearSumcheckProof { /// Round polynomials $s_1, \ldots, s_n$ in the order they were generated. pub round_polynomials: Vec>, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: for<'a> Deserialize<'a>"))] -pub struct CompressedSumcheckProof { +pub struct CompressedSumcheckProof { /// Boolean-hypercube round polynomials with the linear coefficient omitted. pub round_polynomials: Vec>, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: for<'a> Deserialize<'a>"))] -pub enum ClearProof { +pub enum ClearProof { Full(ClearSumcheckProof), Compressed(CompressedSumcheckProof), } -impl Default for ClearProof { +impl Default for ClearProof { fn default() -> Self { Self::Full(ClearSumcheckProof::default()) } @@ -55,12 +55,12 @@ impl Default for ClearProof { serialize = "F: Serialize, C: Serialize", deserialize = "F: for<'a> Deserialize<'a>, C: Deserialize<'de>" ))] -pub enum SumcheckProof { +pub enum SumcheckProof { Clear(ClearProof), Committed(CommittedSumcheckProof), } -impl SumcheckProof { +impl SumcheckProof { pub fn is_committed(&self) -> bool { matches!(self, Self::Committed(_)) } diff --git a/crates/jolt-sumcheck/src/prover.rs b/crates/jolt-sumcheck/src/prover.rs index db81baa5e4..6130a5dd72 100644 --- a/crates/jolt-sumcheck/src/prover.rs +++ b/crates/jolt-sumcheck/src/prover.rs @@ -17,7 +17,7 @@ //! through the recorder. use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::UnivariatePoly; use jolt_transcript::Transcript; use rand_core::RngCore; @@ -48,7 +48,7 @@ use crate::OPENING_CLAIM_TRANSCRIPT_LABEL; /// engine threads the challenge bookkeeping: `bind` is `None` exactly on the /// member's first active round, and the final active round's challenge arrives /// through the terminal [`finish_rounds`](Self::finish_rounds). -pub trait ProveRounds { +pub trait ProveRounds { /// The number of rounds/variables in this member's sumcheck. fn num_rounds(&self) -> usize; @@ -85,7 +85,7 @@ pub struct ProvedBatch { /// compressed wire form requires. The batched polynomial is assembled over /// `max_degree + 1` slots, so rounds where every active member's degree is /// lower carry trailing zeros that must not reach the wire. -fn trim_round_polynomial(mut coefficients: Vec) -> UnivariatePoly { +fn trim_round_polynomial(mut coefficients: Vec) -> UnivariatePoly { while coefficients.len() > 2 && coefficients.last().is_some_and(|value| *value == F::zero()) { let _ = coefficients.pop(); } @@ -118,7 +118,7 @@ pub fn prove_batch( transcript: &mut T, ) -> Result, SumcheckError> where - F: Field, + F: JoltField, R: SumcheckRecorder, T: Transcript, { @@ -254,7 +254,7 @@ where /// challenge — the batch driver absorbs it again as the remainder's input /// claim). #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ProvedUniskip { +pub struct ProvedUniskip { pub proof: SumcheckProof, pub challenge: F, pub output_claim: F, @@ -264,7 +264,7 @@ pub struct ProvedUniskip { /// witness (for BlindFold), the reduction challenge, and the (prover-internal, /// never absorbed) output claim. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ProvedUniskipCommitted { +pub struct ProvedUniskipCommitted { pub proof: SumcheckProof, pub witness: CommittedSumcheckWitness, pub challenge: F, @@ -274,7 +274,7 @@ pub struct ProvedUniskipCommitted { /// Self-check the uni-skip round polynomial against the verifier's round /// checks before anything reaches the transcript: degree bound and /// centered-integer-domain round sum. -fn check_uniskip_round( +fn check_uniskip_round( round_poly: &UnivariatePoly, input_claim: F, degree: usize, @@ -307,7 +307,7 @@ pub fn prove_uniskip_clear( transcript: &mut T, ) -> Result, SumcheckError> where - F: Field, + F: JoltField, T: Transcript, { check_uniskip_round(&round_poly, input_claim, degree, domain_size)?; @@ -341,7 +341,7 @@ pub fn prove_uniskip_committed( transcript: &mut T, ) -> Result, SumcheckError> where - F: Field, + F: JoltField, VC: VectorCommitment, T: Transcript, R: RngCore, diff --git a/crates/jolt-sumcheck/src/r1cs.rs b/crates/jolt-sumcheck/src/r1cs.rs index c1214987b9..11b372051c 100644 --- a/crates/jolt-sumcheck/src/r1cs.rs +++ b/crates/jolt-sumcheck/src/r1cs.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use jolt_r1cs::{LinearCombination, R1csBuilder, Variable}; use thiserror::Error; @@ -98,7 +98,7 @@ pub fn allocate_sumcheck_r1cs_layout( rounds: &[R], ) -> Result where - F: Field, + F: JoltField, R: SumcheckR1csRound, { validate_rounds_statement(statement, rounds)?; @@ -135,7 +135,7 @@ pub fn append_sumcheck_r1cs_constraints( layout: &SumcheckR1csLayout, ) -> Result<(), SumcheckR1csError> where - F: Field, + F: JoltField, R: SumcheckR1csRound, { append_sumcheck_r1cs_constraints_for_domain( @@ -155,7 +155,7 @@ pub fn append_sumcheck_r1cs_constraints_for_domain( domain: D, ) -> Result<(), SumcheckR1csError> where - F: Field, + F: JoltField, R: SumcheckR1csRound, D: SumcheckDomain, { @@ -190,7 +190,7 @@ fn validate_layout( layout: &SumcheckR1csLayout, ) -> Result<(), SumcheckR1csError> where - F: Field, + F: JoltField, R: SumcheckR1csRound, { validate_rounds_statement(statement, rounds)?; @@ -280,7 +280,7 @@ fn validate_variable(variable: Variable, num_vars: usize) -> Result<(), Sumcheck Ok(()) } -fn append_round_constraints( +fn append_round_constraints( builder: &mut R1csBuilder, round_index: usize, round: &SumcheckR1csRoundLayout, @@ -296,7 +296,7 @@ fn append_round_constraints( Ok(()) } -fn round_sum_lc( +fn round_sum_lc( round_index: usize, round: &SumcheckR1csRoundLayout, round_sum_coefficients: &[F], @@ -317,7 +317,7 @@ fn round_sum_lc( )) } -fn polynomial_eval_lc(coefficients: &[Variable], point: F) -> LinearCombination { +fn polynomial_eval_lc(coefficients: &[Variable], point: F) -> LinearCombination { let mut result = LinearCombination::zero(); let mut power = F::one(); @@ -333,7 +333,7 @@ fn polynomial_eval_lc(coefficients: &[Variable], point: F) -> LinearCo #[expect(clippy::expect_used, reason = "tests may panic on assertion failures")] mod tests { use super::*; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Round { diff --git a/crates/jolt-sumcheck/src/recorder.rs b/crates/jolt-sumcheck/src/recorder.rs index 5e9c65b746..43323e90d7 100644 --- a/crates/jolt-sumcheck/src/recorder.rs +++ b/crates/jolt-sumcheck/src/recorder.rs @@ -18,7 +18,7 @@ use std::marker::PhantomData; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{CompressedPoly, UnivariatePoly}; use jolt_transcript::Transcript; use rand_core::RngCore; @@ -33,7 +33,7 @@ use crate::{append_sumcheck_claim, OPENING_CLAIM_TRANSCRIPT_LABEL}; /// (ZK) recording: `absorb_input_claims` once (from `begin_batch`), /// `absorb_round` per round (returning the Fiat-Shamir challenge), then /// `finish` with the flattened output-claim values. -pub trait SumcheckRecorder { +pub trait SumcheckRecorder { /// The proof's commitment type parameter (`SumcheckProof`). Phantom /// for a clear recorder; the vector-commitment output for a committed one. type Commitment; @@ -74,7 +74,7 @@ pub trait SumcheckRecorder { /// retained witness — round coefficients, output-claim rows, and their /// blindings — that BlindFold later opens. `None` for a clear recorder. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RecordedSumcheck { +pub struct RecordedSumcheck { pub proof: SumcheckProof, pub committed_witness: Option>, } @@ -83,18 +83,18 @@ pub struct RecordedSumcheck { /// transcript in the clear and collects the rounds into a /// [`CompressedSumcheckProof`]. Its transcript writes are byte-identical to /// what the clear verifier reads back. -pub struct ClearSumcheckRecorder { +pub struct ClearSumcheckRecorder { round_polynomials: Vec>, _commitment: PhantomData, } -impl Default for ClearSumcheckRecorder { +impl Default for ClearSumcheckRecorder { fn default() -> Self { Self::new() } } -impl ClearSumcheckRecorder { +impl ClearSumcheckRecorder { pub fn new() -> Self { Self { round_polynomials: Vec::new(), @@ -103,7 +103,7 @@ impl ClearSumcheckRecorder { } } -impl SumcheckRecorder for ClearSumcheckRecorder { +impl SumcheckRecorder for ClearSumcheckRecorder { type Commitment = C; fn absorb_input_claims(&mut self, input_claims: &[F], transcript: &mut T) @@ -157,7 +157,7 @@ impl SumcheckRecorder for ClearSumcheckRecorder { /// [`finish`](SumcheckRecorder::finish) for BlindFold. pub struct CommittedSumcheckRecorder<'a, F, VC, R> where - F: Field, + F: JoltField, VC: VectorCommitment, R: RngCore, { @@ -166,7 +166,7 @@ where impl<'a, F, VC, R> CommittedSumcheckRecorder<'a, F, VC, R> where - F: Field, + F: JoltField, VC: VectorCommitment, R: RngCore, { @@ -179,7 +179,7 @@ where impl SumcheckRecorder for CommittedSumcheckRecorder<'_, F, VC, R> where - F: Field, + F: JoltField, VC: VectorCommitment, R: RngCore, { diff --git a/crates/jolt-sumcheck/src/round_proof.rs b/crates/jolt-sumcheck/src/round_proof.rs index 9d9ecef3a2..57dd9a0d32 100644 --- a/crates/jolt-sumcheck/src/round_proof.rs +++ b/crates/jolt-sumcheck/src/round_proof.rs @@ -1,6 +1,6 @@ //! Per-round sumcheck messages. -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{UnivariatePoly, UnivariatePolynomial}; use jolt_transcript::{AppendToTranscript, LabelWithCount, Transcript}; @@ -26,7 +26,7 @@ pub trait ClearRound: RoundMessage { } } -impl RoundMessage for UnivariatePoly { +impl RoundMessage for UnivariatePoly { fn degree(&self) -> usize { UnivariatePolynomial::degree(self) } @@ -38,7 +38,7 @@ impl RoundMessage for UnivariatePoly { } } -impl ClearRound for UnivariatePoly { +impl ClearRound for UnivariatePoly { fn evaluate(&self, challenge: F) -> F { UnivariatePoly::evaluate(self, challenge) } @@ -53,12 +53,12 @@ impl ClearRound for UnivariatePoly { } /// Round polynomial paired with a Fiat-Shamir domain-separation label. -pub struct LabeledRoundPoly<'a, F: Field> { +pub struct LabeledRoundPoly<'a, F: JoltField> { poly: &'a UnivariatePoly, label: &'static [u8], } -impl<'a, F: Field> LabeledRoundPoly<'a, F> { +impl<'a, F: JoltField> LabeledRoundPoly<'a, F> { pub fn new(poly: &'a UnivariatePoly, label: &'static [u8]) -> Self { Self { poly, label } } @@ -72,7 +72,7 @@ impl<'a, F: Field> LabeledRoundPoly<'a, F> { } } -impl RoundMessage for LabeledRoundPoly<'_, F> { +impl RoundMessage for LabeledRoundPoly<'_, F> { fn degree(&self) -> usize { as RoundMessage>::degree(self.poly) } @@ -86,7 +86,7 @@ impl RoundMessage for LabeledRoundPoly<'_, F> { } } -impl ClearRound for LabeledRoundPoly<'_, F> { +impl ClearRound for LabeledRoundPoly<'_, F> { fn evaluate(&self, challenge: F) -> F { as ClearRound>::evaluate(self.poly, challenge) } @@ -102,12 +102,12 @@ impl ClearRound for LabeledRoundPoly<'_, F> { /// Compressed round polynomial with label. Wire format omits the linear /// coefficient `c_1`; the verifier recovers it from the sum-check invariant /// `running_sum = s(0) + s(1) = 2·c_0 + c_1 + c_2 + … + c_d`. -pub struct CompressedLabeledRoundPoly<'a, F: Field> { +pub struct CompressedLabeledRoundPoly<'a, F: JoltField> { poly: &'a UnivariatePoly, label: &'static [u8], } -impl<'a, F: Field> CompressedLabeledRoundPoly<'a, F> { +impl<'a, F: JoltField> CompressedLabeledRoundPoly<'a, F> { pub fn new(poly: &'a UnivariatePoly, label: &'static [u8]) -> Self { Self { poly, label } } @@ -121,7 +121,7 @@ impl<'a, F: Field> CompressedLabeledRoundPoly<'a, F> { } } -impl RoundMessage for CompressedLabeledRoundPoly<'_, F> { +impl RoundMessage for CompressedLabeledRoundPoly<'_, F> { fn degree(&self) -> usize { as RoundMessage>::degree(self.poly) } @@ -136,7 +136,7 @@ impl RoundMessage for CompressedLabeledRoundPoly<'_, F> { } } -impl ClearRound for CompressedLabeledRoundPoly<'_, F> { +impl ClearRound for CompressedLabeledRoundPoly<'_, F> { fn evaluate(&self, challenge: F) -> F { as ClearRound>::evaluate(self.poly, challenge) } diff --git a/crates/jolt-sumcheck/src/scalar.rs b/crates/jolt-sumcheck/src/scalar.rs index fb97127f94..163db126b5 100644 --- a/crates/jolt-sumcheck/src/scalar.rs +++ b/crates/jolt-sumcheck/src/scalar.rs @@ -3,13 +3,13 @@ use std::{ hash::Hash, }; -use jolt_field::{CanonicalRepr, FieldCore, FromPrimitiveInt}; +use jolt_field::{CanonicalEncoding, Field, Ring}; /// Scalar capabilities used by the verifier-side sumcheck crate. pub trait SumcheckScalar: - FieldCore - + FromPrimitiveInt - + CanonicalRepr + Field + + Ring + + CanonicalEncoding + Copy + Default + Eq @@ -23,12 +23,12 @@ pub trait SumcheckScalar: } impl SumcheckScalar for F where - F: FieldCore - + FromPrimitiveInt - + FromPrimitiveInt - + CanonicalRepr - + CanonicalRepr - + CanonicalRepr + F: Field + + Ring + + Ring + + CanonicalEncoding + + CanonicalEncoding + + CanonicalEncoding + Copy + Default + Eq diff --git a/crates/jolt-sumcheck/src/tests.rs b/crates/jolt-sumcheck/src/tests.rs index b16e3fc47b..079d63abaa 100644 --- a/crates/jolt-sumcheck/src/tests.rs +++ b/crates/jolt-sumcheck/src/tests.rs @@ -7,7 +7,7 @@ )] use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment}; -use jolt_field::{Fr, FromPrimitiveInt}; +use jolt_field::{Fr, Ring}; use jolt_poly::{CompressedPoly, UnivariatePoly}; use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Label, LabelWithCount, Transcript}; @@ -1298,7 +1298,7 @@ fn prove_batch_clear_twin_matches_compressed_verifier_with_padding() { use crate::prover::{prove_batch, ProveRounds}; use crate::recorder::{ClearSumcheckRecorder, SumcheckRecorder}; use crate::{append_sumcheck_claim, OPENING_CLAIM_TRANSCRIPT_LABEL}; - use jolt_field::FromPrimitiveInt; + use jolt_field::Ring; let sum_long = F::from_u64(1234); let sum_short = F::from_u64(777); @@ -1384,7 +1384,7 @@ fn prove_batch_clear_twin_head_aligned_member() { use crate::prover::{prove_batch, ProveRounds}; use crate::recorder::{ClearSumcheckRecorder, SumcheckRecorder}; use crate::{append_sumcheck_claim, OPENING_CLAIM_TRANSCRIPT_LABEL}; - use jolt_field::{FieldCore, FromPrimitiveInt}; + use jolt_field::{Field, Ring}; let sum_long = F::from_u64(1234); let sum_short = F::from_u64(777); diff --git a/crates/jolt-sumcheck/src/verifier.rs b/crates/jolt-sumcheck/src/verifier.rs index c2e340546c..8e98bfdde3 100644 --- a/crates/jolt-sumcheck/src/verifier.rs +++ b/crates/jolt-sumcheck/src/verifier.rs @@ -1,6 +1,6 @@ //! Sumcheck verifier: checks round polynomials against the claimed sum. -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::UnivariatePolynomial; use jolt_transcript::{AppendToTranscript, LabelWithCount, Transcript}; @@ -93,7 +93,7 @@ impl SumcheckVerifier { transcript: &mut T, ) -> Result, SumcheckError> where - F: Field, + F: JoltField, T: Transcript, { if proof.round_polynomials.len() != claim.num_vars { @@ -176,7 +176,7 @@ impl SumcheckVerifier { impl CompressedSumcheckProof where - F: Field, + F: JoltField, { pub fn verify( &self, diff --git a/crates/jolt-sumcheck/tests/committed.rs b/crates/jolt-sumcheck/tests/committed.rs index af24a1e167..f5a8d37421 100644 --- a/crates/jolt-sumcheck/tests/committed.rs +++ b/crates/jolt-sumcheck/tests/committed.rs @@ -1,7 +1,7 @@ #![expect(clippy::unwrap_used, reason = "tests may panic on assertion failures")] use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup}; -use jolt_field::{Fr, FromPrimitiveInt}; +use jolt_field::{Fr, Ring}; use jolt_sumcheck::round_proof::RoundMessage; use jolt_sumcheck::{ CommittedOutputClaims, CommittedRound, CommittedRoundWitness, SumcheckError, SumcheckStatement, diff --git a/crates/jolt-sumcheck/tests/mersenne61_compat.rs b/crates/jolt-sumcheck/tests/mersenne61_compat.rs index 2d91a38429..56608e6efd 100644 --- a/crates/jolt-sumcheck/tests/mersenne61_compat.rs +++ b/crates/jolt-sumcheck/tests/mersenne61_compat.rs @@ -15,8 +15,8 @@ use std::{ }; use jolt_field::{ - AdditiveGroup, CanonicalBytes, CanonicalRepr, FieldCore, FromPrimitiveInt, NaiveAccumulator, - RingCore, WithAccumulator, + AdditiveGroup, CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, + WithAccumulator, }; use jolt_sumcheck::{ BooleanHypercube, ClearRound, EvaluationClaim, RoundMessage, SumcheckClaim, SumcheckVerifier, @@ -197,9 +197,8 @@ impl<'a> Product<&'a Mersenne61> for Mersenne61 { } impl AdditiveGroup for Mersenne61 {} -impl RingCore for Mersenne61 {} -impl FieldCore for Mersenne61 { +impl Field for Mersenne61 { fn inverse(&self) -> Option { if self.is_zero() { None @@ -213,7 +212,7 @@ impl FieldCore for Mersenne61 { } } -impl FromPrimitiveInt for Mersenne61 { +impl Ring for Mersenne61 { fn from_u64(v: u64) -> Self { Self::reduce_u128(v as u128) } @@ -248,18 +247,37 @@ impl CanonicalBytes for Mersenne61 { } } -impl CanonicalRepr for Mersenne61 { - fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { +impl CanonicalEncoding for Mersenne61 { + const MODULUS_BITS: u32 = 61; + + fn from_bytes_le_reduced(bytes: &[u8]) -> Self { let mut buf = [0u8; 16]; let len = bytes.len().min(16); buf[..len].copy_from_slice(&bytes[..len]); Self::from_u128(u128::from_le_bytes(buf)) } - fn to_canonical_u64_checked(&self) -> Option { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + let arr: [u8; 8] = bytes.try_into().ok()?; + Self::from_u128_checked(u64::from_le_bytes(arr) as u128) + } + + fn to_u128_checked(&self) -> Option { + Some(self.0 as u128) + } + + fn to_u64_checked(&self) -> Option { Some(self.0) } + fn from_u128_checked(v: u128) -> Option { + (v < MODULUS as u128).then_some(Self(v as u64)) + } + + fn from_u128_reduced(v: u128) -> Self { + Self::reduce_u128(v) + } + fn num_bits(&self) -> u32 { u64::BITS - self.0.leading_zeros() } diff --git a/crates/jolt-sumcheck/tests/roundtrip.rs b/crates/jolt-sumcheck/tests/roundtrip.rs index f33c046f01..c6a6378f99 100644 --- a/crates/jolt-sumcheck/tests/roundtrip.rs +++ b/crates/jolt-sumcheck/tests/roundtrip.rs @@ -6,7 +6,7 @@ #![expect(clippy::unwrap_used, reason = "tests may panic on assertion failures")] -use jolt_field::{Fr, FromPrimitiveInt}; +use jolt_field::{Fr, Ring}; use jolt_poly::{Polynomial, UnivariatePoly}; use jolt_sumcheck::claim::{EvaluationClaim, SumcheckClaim}; use jolt_sumcheck::proof::ClearSumcheckProof; diff --git a/crates/jolt-sumcheck/tests/soundness.rs b/crates/jolt-sumcheck/tests/soundness.rs index fe1371e83a..62dc05c2ce 100644 --- a/crates/jolt-sumcheck/tests/soundness.rs +++ b/crates/jolt-sumcheck/tests/soundness.rs @@ -6,7 +6,7 @@ #![expect(clippy::unwrap_used, reason = "tests may panic on assertion failures")] -use jolt_field::{Fr, FromPrimitiveInt}; +use jolt_field::{Fr, Ring}; use jolt_poly::{Polynomial, UnivariatePoly}; use jolt_sumcheck::claim::{EvaluationClaim, SumcheckClaim}; use jolt_sumcheck::error::SumcheckError; diff --git a/crates/jolt-transcript/src/digest.rs b/crates/jolt-transcript/src/digest.rs index bfd111844e..66386f039d 100644 --- a/crates/jolt-transcript/src/digest.rs +++ b/crates/jolt-transcript/src/digest.rs @@ -33,7 +33,7 @@ pub struct DigestTranscript + 'static, F> { impl Clone for DigestTranscript where D: Digest, - F: jolt_field::CanonicalRepr, + F: jolt_field::CanonicalEncoding, { fn clone(&self) -> Self { Self { @@ -54,7 +54,7 @@ where impl Default for DigestTranscript where D: Digest, - F: jolt_field::CanonicalRepr, + F: jolt_field::CanonicalEncoding, { fn default() -> Self { Self::new(b"") @@ -64,7 +64,7 @@ where impl std::fmt::Debug for DigestTranscript where D: Digest, - F: jolt_field::CanonicalRepr, + F: jolt_field::CanonicalEncoding, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DigestTranscript") @@ -77,7 +77,7 @@ where impl DigestTranscript where D: Digest, - F: jolt_field::CanonicalRepr, + F: jolt_field::CanonicalEncoding, { /// Raw multi-byte squeeze backing jolt-prover-legacy's challenge /// decoding, so its legacy `Transcript` vocabulary can drive this engine @@ -140,7 +140,7 @@ where impl Transcript for DigestTranscript where D: Digest, - F: jolt_field::CanonicalRepr, + F: jolt_field::CanonicalEncoding, { type Challenge = F; diff --git a/crates/jolt-transcript/src/legacy.rs b/crates/jolt-transcript/src/legacy.rs index 803f25c49d..ed60f03719 100644 --- a/crates/jolt-transcript/src/legacy.rs +++ b/crates/jolt-transcript/src/legacy.rs @@ -7,7 +7,7 @@ use std::marker::PhantomData; -use jolt_field::{CanonicalBytes, CanonicalRepr, Field, FromPrimitiveInt}; +use jolt_field::{CanonicalBytes, CanonicalEncoding, JoltField, Ring}; use spongefish::{DuplexSpongeInterface, Encoding}; use crate::codec::BytesMsg; @@ -30,7 +30,7 @@ pub const MAX_LABEL_LEN: usize = 32; /// barriers. pub trait Transcript: Default + Sync + Send + 'static { /// The challenge type produced by this transcript. - type Challenge: CanonicalRepr; + type Challenge: CanonicalEncoding; /// Creates a new transcript with the given domain separation label. /// @@ -81,7 +81,7 @@ pub trait Transcript: Default + Sync + Send + 'static { #[must_use] fn challenge_scalar_powers(&mut self, len: usize) -> Vec where - Self::Challenge: Field, + Self::Challenge: JoltField, { let gamma = self.challenge_scalar(); let mut powers = vec![Self::Challenge::from_u64(1); len]; @@ -204,7 +204,7 @@ impl AppendToTranscript for U64Word { pub struct SpongeTranscript where H: DuplexSpongeInterface + Clone + Default + Send + Sync + 'static, - F: CanonicalRepr, + F: CanonicalEncoding, { sponge: H, _field: PhantomData, @@ -213,7 +213,7 @@ where impl Default for SpongeTranscript where H: DuplexSpongeInterface + Clone + Default + Send + Sync + 'static, - F: CanonicalRepr, + F: CanonicalEncoding, { fn default() -> Self { Self::new(b"") @@ -239,7 +239,7 @@ fn peek_state + Clone>(sponge: &H) -> [u8; 32] impl Transcript for SpongeTranscript where H: DuplexSpongeInterface + Clone + Default + Send + Sync + 'static, - F: CanonicalRepr, + F: CanonicalEncoding, { type Challenge = F; diff --git a/crates/jolt-transcript/tests/blake2b_tests.rs b/crates/jolt-transcript/tests/blake2b_tests.rs index c6f97c6783..7834f8693d 100644 --- a/crates/jolt-transcript/tests/blake2b_tests.rs +++ b/crates/jolt-transcript/tests/blake2b_tests.rs @@ -2,7 +2,7 @@ mod common; -use jolt_field::{Fr, FromPrimitiveInt}; +use jolt_field::{Fr, Ring}; use jolt_transcript::Blake2bTranscript; type B2b = Blake2bTranscript; diff --git a/crates/jolt-verifier-derive/src/lib.rs b/crates/jolt-verifier-derive/src/lib.rs index 1c614a932e..6924576f60 100644 --- a/crates/jolt-verifier-derive/src/lib.rs +++ b/crates/jolt-verifier-derive/src/lib.rs @@ -6,7 +6,7 @@ //! //! ```ignore //! #[derive(SumcheckBatch)] -//! struct Stage5Sumchecks { +//! struct Stage5Sumchecks { //! instruction_read_raf: InstructionReadRaf, //! ram_ra_claim_reduction: RamRaClaimReduction, //! registers_val_evaluation: RegistersValEvaluation, @@ -1077,7 +1077,7 @@ fn expand(input: DeriveInput) -> syn::Result { }; let driver_impl = quote! { - impl<#f: ::jolt_field::Field> #name<#f> { + impl<#f: ::jolt_field::JoltField> #name<#f> { #draw_challenges_method #begin_batch_method @@ -1096,7 +1096,7 @@ fn expand(input: DeriveInput) -> syn::Result { // holds the wire *values* (`Inputs` / `Outputs`); `*Points` holds the // derived opening points (`Inputs>` / `Outputs>`). Only the // `OutputClaims` (values) aggregate is serialized (the wire form), so it alone - // derives serde. `F: Field` does not imply the serde traits, so the bounds are + // derives serde. `F: JoltField` does not imply the serde traits, so the bounds are // spelled explicitly (the workspace convention for claim structs), fully // qualified so call sites need no serde imports. let serialize_bound = format!("{f}: ::serde::Serialize"); @@ -1104,33 +1104,33 @@ fn expand(input: DeriveInput) -> syn::Result { Ok(quote! { #[derive(Clone, Debug, PartialEq, Eq)] - #vis struct #input_claims_name<#f: ::jolt_field::Field> { + #vis struct #input_claims_name<#f: ::jolt_field::JoltField> { #(#input_claims_fields,)* } #[derive(Clone, Debug, PartialEq, Eq)] - #vis struct #input_points_name<#f: ::jolt_field::Field> { + #vis struct #input_points_name<#f: ::jolt_field::JoltField> { #(#input_points_fields,)* } #[derive(Clone, Debug, PartialEq, Eq, ::serde::Serialize, ::serde::Deserialize)] #[serde(bound(serialize = #serialize_bound, deserialize = #deserialize_bound))] - #vis struct #output_claims_name<#f: ::jolt_field::Field> { + #vis struct #output_claims_name<#f: ::jolt_field::JoltField> { #(#output_claims_fields,)* } #[derive(Clone, Debug, PartialEq, Eq)] - #vis struct #output_points_name<#f: ::jolt_field::Field> { + #vis struct #output_points_name<#f: ::jolt_field::JoltField> { #(#output_points_fields,)* } #[derive(Clone, Debug, PartialEq, Eq)] - #vis struct #challenges_name<#f: ::jolt_field::Field> { + #vis struct #challenges_name<#f: ::jolt_field::JoltField> { #(#challenge_fields,)* } #[derive(Clone, Debug, PartialEq, Eq)] - #vis struct #batching_coefficients_name<#f: ::jolt_field::Field> { + #vis struct #batching_coefficients_name<#f: ::jolt_field::JoltField> { #(#batching_coefficient_fields,)* } diff --git a/crates/jolt-verifier/src/proof.rs b/crates/jolt-verifier/src/proof.rs index 4b27cb876a..4d3dad4479 100644 --- a/crates/jolt-verifier/src/proof.rs +++ b/crates/jolt-verifier/src/proof.rs @@ -4,7 +4,7 @@ use jolt_blindfold::BlindFoldProof; pub use jolt_claims::protocols::jolt::TracePolynomialOrder; use jolt_claims::protocols::jolt::{JoltOneHotConfig, JoltReadWriteConfig}; use jolt_crypto::{Commitment, VectorCommitment}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::CommitmentScheme; use jolt_sumcheck::SumcheckProof; use serde::{Deserialize, Serialize}; @@ -130,7 +130,7 @@ impl JoltCommitments { ))] pub enum JoltProofClaims where - F: Field, + F: JoltField, { Clear(ClearProofClaims), Zk { blindfold_proof: ZkProof }, @@ -138,7 +138,7 @@ where impl JoltProofClaims where - F: Field, + F: JoltField, { pub const fn is_zk(&self) -> bool { matches!(self, Self::Zk { .. }) @@ -147,7 +147,7 @@ where #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: for<'a> Deserialize<'a>"))] -pub struct ClearProofClaims { +pub struct ClearProofClaims { pub stage1: stage1::outputs::Stage1OutputClaims, pub stage2: stage2::outputs::Stage2OutputClaims, pub stage3: stage3::outputs::Stage3OutputClaims, @@ -169,7 +169,7 @@ pub struct ClearProofClaims { ))] pub struct JoltStageProofs where - F: Field, + F: JoltField, VC: VectorCommitment, { pub stage1_uni_skip_first_round_proof: SumcheckProof, diff --git a/crates/jolt-verifier/src/stages/mod.rs b/crates/jolt-verifier/src/stages/mod.rs index 049d611551..8072ad9230 100644 --- a/crates/jolt-verifier/src/stages/mod.rs +++ b/crates/jolt-verifier/src/stages/mod.rs @@ -9,7 +9,7 @@ use jolt_claims::protocols::jolt::{ TracePolynomialOrder, }; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_lookup_tables::XLEN as RISCV_XLEN; use jolt_openings::CommitmentScheme; @@ -79,7 +79,7 @@ pub fn formula_dimensions_from_parts( }) } -pub(crate) fn stage6_checked_split<'a, F: Field>( +pub(crate) fn stage6_checked_split<'a, F: JoltField>( label: &'static str, point: &'a [F], split_at: usize, diff --git a/crates/jolt-verifier/src/stages/relations.rs b/crates/jolt-verifier/src/stages/relations.rs index eec1a75334..19e012f9f0 100644 --- a/crates/jolt-verifier/src/stages/relations.rs +++ b/crates/jolt-verifier/src/stages/relations.rs @@ -24,7 +24,7 @@ use std::collections::BTreeSet; use jolt_claims::protocols::jolt::{JoltChallengeId, JoltDerivedId, JoltOpeningId, JoltRelationId}; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_transcript::Transcript; use crate::VerifierError; @@ -36,7 +36,7 @@ use crate::VerifierError; /// `Transcript`; `jolt-claims` stays transcript-free. It is a blanket extension /// over every `OutputClaims` implementor, so the Fiat-Shamir order is /// single-sourced by [`OutputClaims::opening_values`] and cannot disagree with it. -pub trait OutputAppend: OutputClaims { +pub trait OutputAppend: OutputClaims { /// Append every produced opening to the transcript in canonical /// ([`OutputClaims::opening_values`]) order, each under the `b"opening_claim"` /// label. This is the Fiat-Shamir order and MUST match the order in which the @@ -48,7 +48,7 @@ pub trait OutputAppend: OutputClaims { } } -impl> OutputAppend for C {} +impl> OutputAppend for C {} /// The drawn Fiat-Shamir challenges of a [`ConcreteSumcheck`] instance: a readable /// alias for the relation's `Challenges` projection through its symbolic @@ -84,7 +84,7 @@ pub type SumcheckOutputPoints = /// in both modes; methods that read values ([`input_claim`](Self::input_claim), /// [`expected_output`](Self::expected_output)) take the Values forms. This makes /// "a ZK opening carries no value" a compile-time fact. -pub trait ConcreteSumcheck: Clone + Send + Sync +pub trait ConcreteSumcheck: Clone + Send + Sync where SumcheckInputClaims: InputClaims, SumcheckOutputClaims: OutputClaims, @@ -332,7 +332,7 @@ where /// per member, in member declaration order. pub fn absorbed_opening_values(claims: &SumcheckOutputClaims) -> Vec where - F: Field, + F: JoltField, I: ConcreteSumcheck, SumcheckOutputClaims: OutputClaims, { @@ -359,7 +359,7 @@ pub fn validate_member_presence( claims: Option<&SumcheckOutputClaims>, ) -> Result<(), VerifierError> where - F: Field, + F: JoltField, I: ConcreteSumcheck, SumcheckOutputClaims: OutputClaims, { @@ -392,7 +392,7 @@ pub fn validate_member_aliases( resolve_source: impl Fn(&JoltOpeningId) -> Option, ) -> Result<(), VerifierError> where - F: Field, + F: JoltField, I: ConcreteSumcheck, SumcheckOutputClaims: OutputClaims, { @@ -424,7 +424,7 @@ pub fn validate_member_output_shape( claims: &SumcheckOutputClaims, ) -> Result<(), VerifierError> where - F: Field, + F: JoltField, I: ConcreteSumcheck, SumcheckOutputClaims: OutputClaims, { @@ -463,7 +463,7 @@ where /// the squeeze that produced it. #[cfg(test)] pub(crate) mod draw_recording { - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_transcript::Transcript; /// One observable transcript operation a `draw_challenges` performs. @@ -528,7 +528,7 @@ pub(crate) mod draw_recording { /// locks: unlike the challenge recorder, it observes only byte appends. #[cfg(test)] pub(crate) mod append_recording { - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_transcript::Transcript; /// A minimal `Transcript` double that records each appended byte chunk, so @@ -568,7 +568,7 @@ mod tests { JoltCommittedPolynomial, JoltOpeningId, JoltRelationId, JoltVirtualPolynomial, }; use jolt_claims_derive::{InputClaims, OutputClaims}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_riscv::CircuitFlags; fn fr(value: u64) -> Fr { @@ -1061,7 +1061,7 @@ mod sumcheck_batch_derive_tests { }; use jolt_claims::protocols::jolt::geometry::dimensions::TraceDimensions; use jolt_claims::protocols::jolt::geometry::instruction::InstructionReadRafDimensions; - use jolt_field::{Field, Fr, FromPrimitiveInt}; + use jolt_field::{Fr, JoltField, Ring}; fn instruction_read_raf() -> InstructionReadRaf { InstructionReadRaf::new(InstructionReadRafDimensions::try_from((5, 128, 3)).unwrap()) @@ -1076,7 +1076,7 @@ mod sumcheck_batch_derive_tests { // The generated absorb resolves the alias skip-sets statically (no instance // state), so this alias-free fixture's members are never read. #[expect(dead_code)] - struct FixtureSumchecks { + struct FixtureSumchecks { instruction_read_raf: InstructionReadRaf, registers_val_evaluation: RegistersValEvaluation, } @@ -1108,7 +1108,7 @@ mod sumcheck_batch_derive_tests { #[derive(SumcheckBatch)] #[sumcheck_batch(crate = "crate")] - struct FixtureOptionSumchecks { + struct FixtureOptionSumchecks { instruction_read_raf: InstructionReadRaf, registers_val_evaluation: Option>, } @@ -1204,7 +1204,7 @@ mod sumcheck_batch_derive_tests { #[sumcheck_batch(no_opening_values, crate = "crate")] // The custom absorb below never reads the members (no aliased sets to consult). #[expect(dead_code)] - struct FixtureCustomSumchecks { + struct FixtureCustomSumchecks { instruction_read_raf: InstructionReadRaf, registers_val_evaluation: RegistersValEvaluation, } @@ -1265,12 +1265,12 @@ mod sumcheck_batch_derive_tests { #[derive(SumcheckBatch)] #[sumcheck_batch(no_draw_challenges, crate = "crate")] #[expect(dead_code)] - struct FixtureNoDrawSumchecks { + struct FixtureNoDrawSumchecks { instruction_read_raf: InstructionReadRaf, registers_val_evaluation: RegistersValEvaluation, } - impl FixtureNoDrawSumchecks { + impl FixtureNoDrawSumchecks { #[expect(dead_code, clippy::unused_self)] fn draw_challenges(&self) {} } @@ -1286,13 +1286,13 @@ mod begin_batch_tests { use jolt_claims::protocols::jolt::geometry::instruction::InstructionReadRafDimensions; use jolt_claims::protocols::jolt::relations::instruction::InstructionReadRafInputClaims; use jolt_claims::protocols::jolt::relations::registers::RegistersValEvaluationInputClaims; - use jolt_field::{Field, Fr, FromPrimitiveInt}; + use jolt_field::{Fr, JoltField, Ring}; use jolt_sumcheck::{append_sumcheck_claim, BatchMember, ClearSumcheckRecorder}; use jolt_transcript::Transcript; #[derive(super::SumcheckBatch)] #[sumcheck_batch(crate = "crate")] - struct HeadFixtureSumchecks { + struct HeadFixtureSumchecks { instruction_read_raf: InstructionReadRaf, registers_val_evaluation: Option>, } diff --git a/crates/jolt-verifier/src/stages/stage1/outer_remainder.rs b/crates/jolt-verifier/src/stages/stage1/outer_remainder.rs index 93ece44efc..01878288cb 100644 --- a/crates/jolt-verifier/src/stages/stage1/outer_remainder.rs +++ b/crates/jolt-verifier/src/stages/stage1/outer_remainder.rs @@ -25,7 +25,7 @@ pub use jolt_claims::protocols::jolt::relations::spartan::{ }; use jolt_claims::protocols::jolt::{relations, JoltDerivedId, JoltRelationId, SpartanOuterPublic}; use jolt_claims::{NoChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_r1cs::constraints::jolt::{ JoltSpartanOuterPublic, JoltSpartanOuterRemainder, JoltSpartanOuterRemainderChallenges, }; @@ -36,7 +36,7 @@ use crate::VerifierError; /// Wire the consumed opening *value* from the Spartan outer uni-skip's reduced output /// claim: only the value feeds the input claim (the output point comes from this /// relation's own sumcheck point). -pub fn outer_remainder_input_values_from_uniskip_output( +pub fn outer_remainder_input_values_from_uniskip_output( uniskip_output_claim: F, ) -> OuterRemainderInputClaims { OuterRemainderInputClaims { @@ -57,7 +57,7 @@ struct OuterRemainderCoefficients { bz_constant: F, } -impl OuterRemainderCoefficients { +impl OuterRemainderCoefficients { fn from_public_coefficients( variable_count: usize, coefficients: Vec<(JoltSpartanOuterPublic, F)>, @@ -97,7 +97,7 @@ impl OuterRemainderCoefficients { } #[derive(Clone)] -pub struct OuterRemainder { +pub struct OuterRemainder { symbolic: relations::spartan::OuterRemainder, variable_count: usize, /// The stage-1 `tau` draw and the uni-skip reduction challenge — two of the @@ -115,7 +115,7 @@ pub struct OuterRemainder { coefficients: std::sync::OnceLock>, } -impl OuterRemainder { +impl OuterRemainder { pub fn new(dimensions: SpartanOuterDimensions, tau: Vec, uniskip_challenge: F) -> Self { let variable_count = dimensions.variables().len(); Self { @@ -171,7 +171,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for OuterRemainder { +impl ConcreteSumcheck for OuterRemainder { type Symbolic = relations::spartan::OuterRemainder; fn symbolic(&self) -> &Self::Symbolic { @@ -257,7 +257,7 @@ mod tests { use crate::stages::relations::OutputClaims; use jolt_claims::protocols::jolt::geometry::spartan::SPARTAN_OUTER_R1CS_INPUTS; use jolt_claims::protocols::jolt::JoltOpeningId; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; /// The produced `OuterRemainderOutputClaims` field (declaration) order is the /// canonical `SPARTAN_OUTER_R1CS_INPUTS` order, so the generated absorb diff --git a/crates/jolt-verifier/src/stages/stage1/outputs.rs b/crates/jolt-verifier/src/stages/stage1/outputs.rs index c23ec052db..73dd02acd3 100644 --- a/crates/jolt-verifier/src/stages/stage1/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage1/outputs.rs @@ -1,7 +1,7 @@ //! Typed inputs consumed and outputs produced by stage 1 verification. use jolt_claims::protocols::jolt::JoltRelationId; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_sumcheck::{BatchedCommittedSumcheckConsistency, CommittedSumcheckConsistency}; use serde::{Deserialize, Serialize}; @@ -12,7 +12,7 @@ use crate::VerifierError; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: for<'a> Deserialize<'a>"))] -pub struct Stage1OutputClaims { +pub struct Stage1OutputClaims { pub uniskip_output_claim: F, pub outer: Stage1BatchOutputClaims, } @@ -36,7 +36,7 @@ pub struct Stage1OutputClaims { /// the point and the first `derive_output_term` call builds the table. #[derive(SumcheckBatch)] #[sumcheck_batch(crate = "crate")] -pub struct Stage1BatchSumchecks { +pub struct Stage1BatchSumchecks { /// On the prove side the remainder kernel is minted from the state the /// uni-skip slot parked in the proof session, through its regular /// universal backend slot. @@ -54,13 +54,13 @@ pub struct Stage1BatchSumchecks { /// coefficient is likewise /// read from `remainder_consistency` on the ZK path. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage1Challenges { +pub struct Stage1Challenges { pub tau: Vec, pub uniskip_challenge: F, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage1ClearOutput { +pub struct Stage1ClearOutput { /// The produced remainder opening *values* (wire form). The opening point is /// derived from the remainder's sumcheck point; later stages read values through /// `.outer_remainder.`. @@ -71,7 +71,7 @@ pub struct Stage1ClearOutput { pub output_points: Stage1BatchOutputPoints, } -impl Stage1ClearOutput { +impl Stage1ClearOutput { /// The raw (un-reversed) Spartan outer remainder reduction point: the /// clear path stores the openings at the REVERSED point /// (`derive_opening_points`), so this reverses it back. All 35 stage-1 @@ -113,7 +113,7 @@ fn empty_remainder_point(stage: JoltRelationId) -> VerifierError { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage1ZkOutput { +pub struct Stage1ZkOutput { pub challenges: Stage1Challenges, pub uniskip_consistency: CommittedSumcheckConsistency, pub uniskip_output_claims: CommittedOutputClaimOutput, @@ -126,12 +126,12 @@ pub struct Stage1ZkOutput { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Stage1Output { +pub enum Stage1Output { Clear(Stage1ClearOutput), Zk(Stage1ZkOutput), } -impl Stage1Output { +impl Stage1Output { /// The raw (un-reversed) Spartan outer remainder sumcheck reduction point, /// available regardless of proving mode. The remainder is a singleton batch, so /// the clear-path bound point and the ZK committed round challenges are the same diff --git a/crates/jolt-verifier/src/stages/stage1/verify.rs b/crates/jolt-verifier/src/stages/stage1/verify.rs index f257017311..bca6f22ec8 100644 --- a/crates/jolt-verifier/src/stages/stage1/verify.rs +++ b/crates/jolt-verifier/src/stages/stage1/verify.rs @@ -1,6 +1,6 @@ use jolt_claims::protocols::jolt::{geometry::spartan::SpartanOuterDimensions, JoltRelationId}; use jolt_crypto::VectorCommitment; -use jolt_field::FromPrimitiveInt; +use jolt_field::Ring; use jolt_openings::CommitmentScheme; use jolt_transcript::Transcript; diff --git a/crates/jolt-verifier/src/stages/stage2/instruction_claim_reduction.rs b/crates/jolt-verifier/src/stages/stage2/instruction_claim_reduction.rs index c359cd6a51..cc043d886e 100644 --- a/crates/jolt-verifier/src/stages/stage2/instruction_claim_reduction.rs +++ b/crates/jolt-verifier/src/stages/stage2/instruction_claim_reduction.rs @@ -24,7 +24,7 @@ use jolt_claims::protocols::jolt::{ JoltDerivedId, JoltOpeningId, JoltRelationId, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::try_eq_mle; use crate::stages::relations::ConcreteSumcheck; @@ -35,7 +35,7 @@ use crate::VerifierError; /// sumcheck. (Verifier-side constructor for the moved /// [`InstructionClaimReductionInputClaims`] — it reads the verifier-only /// [`Stage1ClearOutput`], so it cannot live in `jolt-claims`.) -pub fn instruction_claim_reduction_input_values_from_upstream( +pub fn instruction_claim_reduction_input_values_from_upstream( stage1: &Stage1ClearOutput, ) -> InstructionClaimReductionInputClaims { let outer = &stage1.output_values.outer_remainder; @@ -49,12 +49,12 @@ pub fn instruction_claim_reduction_input_values_from_upstream( } #[derive(Clone)] -pub struct InstructionClaimReduction { +pub struct InstructionClaimReduction { symbolic: relations::claim_reductions::instruction::ClaimReduction, tau_low: Vec, } -impl InstructionClaimReduction { +impl InstructionClaimReduction { pub fn new(trace_dimensions: TraceDimensions, tau_low: Vec) -> Self { Self { symbolic: relations::claim_reductions::instruction::ClaimReduction::new( @@ -76,7 +76,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for InstructionClaimReduction { +impl ConcreteSumcheck for InstructionClaimReduction { type Symbolic = relations::claim_reductions::instruction::ClaimReduction; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage2/outputs.rs b/crates/jolt-verifier/src/stages/stage2/outputs.rs index 40d41fa803..1689cd91d0 100644 --- a/crates/jolt-verifier/src/stages/stage2/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage2/outputs.rs @@ -1,6 +1,6 @@ //! Typed inputs consumed and outputs produced by stage 2 verification. -use jolt_field::Field; +use jolt_field::JoltField; use jolt_sumcheck::{BatchedCommittedSumcheckConsistency, CommittedSumcheckConsistency}; use serde::{Deserialize, Serialize}; @@ -17,7 +17,7 @@ pub use super::ram_read_write_checking::{RamReadWriteChecking, RamReadWriteOutpu #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound(serialize = "F: Serialize", deserialize = "F: for<'a> Deserialize<'a>"))] -pub struct Stage2OutputClaims { +pub struct Stage2OutputClaims { pub product_uniskip_output_claim: F, pub batch_outputs: Stage2BatchOutputClaims, } @@ -43,7 +43,7 @@ pub struct Stage2OutputClaims { /// The two RAM relations slice their point at the phase-1 `instance_point_offset`. #[derive(SumcheckBatch)] #[sumcheck_batch(crate = "crate")] -pub struct Stage2BatchSumchecks { +pub struct Stage2BatchSumchecks { pub ram_read_write: RamReadWriteChecking, /// On the prove side the remainder kernel is minted from the state the /// product uni-skip slot parked in the proof session, through its @@ -56,7 +56,7 @@ pub struct Stage2BatchSumchecks { /// The shared per-relation opening-point accessors over the point-only stage-2 /// batch aggregate. -impl Stage2BatchOutputPoints { +impl Stage2BatchOutputPoints { /// The RAM read-write opening point (shared by `val`/`ra`/`inc`). pub fn ram_read_write_point(&self) -> &[F] { self.ram_read_write.val() @@ -84,7 +84,7 @@ impl Stage2BatchOutputPoints { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage2ClearOutput { +pub struct Stage2ClearOutput { /// The produced batch opening *values* (wire form); later stages read each /// opening's value directly off these fields. pub output_values: Stage2BatchOutputClaims, @@ -111,7 +111,7 @@ pub struct Stage2ClearOutput { /// [`Stage2Output::product_tau_low`]; BlindFold independently recomputes it from /// `stage1.remainder_consistency`. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage2ZkOutput { +pub struct Stage2ZkOutput { pub challenges: Stage2BatchChallenges, pub product_uniskip_challenge: F, pub product_tau_low: Vec, @@ -126,12 +126,12 @@ pub struct Stage2ZkOutput { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Stage2Output { +pub enum Stage2Output { Clear(Stage2ClearOutput), Zk(Stage2ZkOutput), } -impl Stage2Output { +impl Stage2Output { /// The product uni-skip `tau_low` (stage 1's remainder point low half, /// reversed), available regardless of proving mode. Stage 3's relation /// construction evaluates its `EqPlusOne`/`EqSpartan` publics against it. @@ -177,7 +177,7 @@ mod tests { ram::RamRafEvaluationDimensions, spartan::SpartanProductDimensions, }; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_program::preprocess::PublicIoMemory; use jolt_transcript::Transcript; diff --git a/crates/jolt-verifier/src/stages/stage2/product_remainder.rs b/crates/jolt-verifier/src/stages/stage2/product_remainder.rs index 9c517b7917..7e1aeea1da 100644 --- a/crates/jolt-verifier/src/stages/stage2/product_remainder.rs +++ b/crates/jolt-verifier/src/stages/stage2/product_remainder.rs @@ -17,7 +17,7 @@ use jolt_claims::protocols::jolt::{ JoltRelationId, SpartanProductVirtualizationPublic, }; use jolt_claims::{NoChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{ lagrange::{centered_lagrange_evals, centered_lagrange_kernel}, try_eq_mle, @@ -29,7 +29,7 @@ use crate::VerifierError; /// Wire the consumed opening *value* from the product uni-skip's reduced output /// claim (the output point comes from this relation's own sumcheck point). -pub fn product_remainder_input_values_from_uniskip_output( +pub fn product_remainder_input_values_from_uniskip_output( product_uniskip_output_claim: F, ) -> ProductRemainderInputClaims { ProductRemainderInputClaims { @@ -37,7 +37,7 @@ pub fn product_remainder_input_values_from_uniskip_output( } } -impl ProductRemainder { +impl ProductRemainder { pub fn uniskip_challenge(&self) -> F { self.uniskip_challenge } @@ -48,14 +48,14 @@ impl ProductRemainder { } #[derive(Clone)] -pub struct ProductRemainder { +pub struct ProductRemainder { symbolic: relations::spartan::ProductRemainder, uniskip_challenge: F, tau_high: F, tau_low: Vec, } -impl ProductRemainder { +impl ProductRemainder { pub fn new( dimensions: SpartanProductDimensions, uniskip_challenge: F, @@ -78,7 +78,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for ProductRemainder { +impl ConcreteSumcheck for ProductRemainder { type Symbolic = relations::spartan::ProductRemainder; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage2/product_uniskip.rs b/crates/jolt-verifier/src/stages/stage2/product_uniskip.rs index 9b153c36ed..7c1a11f84d 100644 --- a/crates/jolt-verifier/src/stages/stage2/product_uniskip.rs +++ b/crates/jolt-verifier/src/stages/stage2/product_uniskip.rs @@ -22,7 +22,7 @@ use jolt_claims::protocols::jolt::{ SpartanProductVirtualizationPublic, }; use jolt_claims::{NoChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::lagrange::centered_lagrange_evals; use jolt_r1cs::constraints::jolt::SPARTAN_PRODUCT_UNISKIP_DOMAIN_SIZE; @@ -33,7 +33,7 @@ use crate::VerifierError; /// Wire the three consumed Spartan-outer opening *values* from the stage 1 outer /// output. Only the values feed the input claim (the uni-skip's output point comes /// from its own sumcheck point), so the input points are left empty. -pub fn product_uniskip_input_values_from_stage1( +pub fn product_uniskip_input_values_from_stage1( stage1: &Stage1ClearOutput, ) -> ProductUniskipInputClaims { let outer = &stage1.output_values.outer_remainder; @@ -45,12 +45,12 @@ pub fn product_uniskip_input_values_from_stage1( } #[derive(Clone)] -pub struct ProductUniskip { +pub struct ProductUniskip { symbolic: relations::spartan::ProductUniskip, tau_high: F, } -impl ProductUniskip { +impl ProductUniskip { pub fn new(dimensions: SpartanProductDimensions, tau_high: F) -> Self { Self { symbolic: relations::spartan::ProductUniskip::new(dimensions), @@ -66,7 +66,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for ProductUniskip { +impl ConcreteSumcheck for ProductUniskip { type Symbolic = relations::spartan::ProductUniskip; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs b/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs index dc6c1d6919..7bc1e2b67a 100644 --- a/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs +++ b/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs @@ -15,7 +15,7 @@ use jolt_claims::protocols::jolt::{ geometry::dimensions::ReadWriteDimensions, JoltDerivedId, JoltRelationId, RamOutputCheckPublic, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{range_mask_mle_msb, sparse_segments_mle_msb, try_eq_mle}; use jolt_program::preprocess::PublicIoMemory; use jolt_transcript::Transcript; @@ -24,14 +24,14 @@ use crate::stages::relations::ConcreteSumcheck; use crate::VerifierError; #[derive(Clone)] -pub struct RamOutputCheck { +pub struct RamOutputCheck { symbolic: relations::ram::OutputCheck, read_write_dimensions: ReadWriteDimensions, public_memory: PublicIoMemory, _field: core::marker::PhantomData, } -impl RamOutputCheck { +impl RamOutputCheck { pub fn new(read_write_dimensions: ReadWriteDimensions, public_memory: PublicIoMemory) -> Self { Self { symbolic: relations::ram::OutputCheck::new(read_write_dimensions), @@ -47,7 +47,7 @@ impl RamOutputCheck { /// `[io_start, io_end)` range mask, and the committed public-IO value. Shared /// by the stage-2 clear path and the BlindFold statement builder so the /// algebra lives in one place. -pub(crate) fn ram_output_check_publics( +pub(crate) fn ram_output_check_publics( public_memory: &PublicIoMemory, output_address_challenges: &[F], ram_output_address: &[F], @@ -92,7 +92,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl RamOutputCheck { +impl RamOutputCheck { pub fn read_write_dimensions(&self) -> ReadWriteDimensions { self.read_write_dimensions } @@ -102,7 +102,7 @@ impl RamOutputCheck { } } -impl ConcreteSumcheck for RamOutputCheck { +impl ConcreteSumcheck for RamOutputCheck { type Symbolic = relations::ram::OutputCheck; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage2/ram_raf_evaluation.rs b/crates/jolt-verifier/src/stages/stage2/ram_raf_evaluation.rs index 6e82c4bec1..85a9b96eb4 100644 --- a/crates/jolt-verifier/src/stages/stage2/ram_raf_evaluation.rs +++ b/crates/jolt-verifier/src/stages/stage2/ram_raf_evaluation.rs @@ -17,7 +17,7 @@ use jolt_claims::protocols::jolt::{ JoltDerivedId, JoltRelationId, RamRafEvaluationPublic, }; use jolt_claims::{NoChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{IdentityPolynomial, MultilinearEvaluation}; use crate::stages::relations::ConcreteSumcheck; @@ -26,7 +26,7 @@ use crate::VerifierError; /// Wire the consumed RAM address opening *value* from stage 1's outer sumcheck. /// (Verifier-side constructor for the moved [`RamRafEvaluationInputClaims`].) -pub fn ram_raf_evaluation_input_values_from_upstream( +pub fn ram_raf_evaluation_input_values_from_upstream( stage1: &Stage1ClearOutput, ) -> RamRafEvaluationInputClaims { RamRafEvaluationInputClaims { @@ -35,7 +35,7 @@ pub fn ram_raf_evaluation_input_values_from_upstream( } #[derive(Clone)] -pub struct RamRafEvaluation { +pub struct RamRafEvaluation { symbolic: relations::ram::RafEvaluation, read_write_dimensions: ReadWriteDimensions, ram_log_k: usize, @@ -43,7 +43,7 @@ pub struct RamRafEvaluation { tau_low: Vec, } -impl RamRafEvaluation { +impl RamRafEvaluation { pub fn new( read_write_dimensions: ReadWriteDimensions, raf_dimensions: RamRafEvaluationDimensions, @@ -68,7 +68,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl RamRafEvaluation { +impl RamRafEvaluation { pub fn read_write_dimensions(&self) -> ReadWriteDimensions { self.read_write_dimensions } @@ -86,7 +86,7 @@ impl RamRafEvaluation { } } -impl ConcreteSumcheck for RamRafEvaluation { +impl ConcreteSumcheck for RamRafEvaluation { type Symbolic = relations::ram::RafEvaluation; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage2/ram_read_write_checking.rs b/crates/jolt-verifier/src/stages/stage2/ram_read_write_checking.rs index 5b08189915..eae2307044 100644 --- a/crates/jolt-verifier/src/stages/stage2/ram_read_write_checking.rs +++ b/crates/jolt-verifier/src/stages/stage2/ram_read_write_checking.rs @@ -12,7 +12,7 @@ use jolt_claims::protocols::jolt::{ geometry::dimensions::ReadWriteDimensions, JoltDerivedId, JoltRelationId, RamReadWritePublic, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::try_eq_mle; use crate::stages::relations::ConcreteSumcheck; @@ -21,7 +21,7 @@ use crate::VerifierError; /// Wire the consumed RAM read/write value opening *values* from stage 1's outer /// sumcheck. (Verifier-side constructor for the moved [`RamReadWriteInputClaims`].) -pub fn ram_read_write_input_values_from_upstream( +pub fn ram_read_write_input_values_from_upstream( stage1: &Stage1ClearOutput, ) -> RamReadWriteInputClaims { let outer = &stage1.output_values.outer_remainder; @@ -32,14 +32,14 @@ pub fn ram_read_write_input_values_from_upstream( } #[derive(Clone)] -pub struct RamReadWriteChecking { +pub struct RamReadWriteChecking { symbolic: relations::ram::ReadWriteChecking, dimensions: ReadWriteDimensions, ram_log_k: usize, product_tau_low: Vec, } -impl RamReadWriteChecking { +impl RamReadWriteChecking { pub fn new(dimensions: ReadWriteDimensions, ram_log_k: usize, product_tau_low: Vec) -> Self { Self { symbolic: relations::ram::ReadWriteChecking::new(dimensions), @@ -57,7 +57,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl RamReadWriteChecking { +impl RamReadWriteChecking { pub fn dimensions(&self) -> ReadWriteDimensions { self.dimensions } @@ -71,7 +71,7 @@ impl RamReadWriteChecking { } } -impl ConcreteSumcheck for RamReadWriteChecking { +impl ConcreteSumcheck for RamReadWriteChecking { type Symbolic = relations::ram::ReadWriteChecking; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage2/verify.rs b/crates/jolt-verifier/src/stages/stage2/verify.rs index a5a04d6adc..fd263a527d 100644 --- a/crates/jolt-verifier/src/stages/stage2/verify.rs +++ b/crates/jolt-verifier/src/stages/stage2/verify.rs @@ -7,7 +7,7 @@ use jolt_claims::protocols::jolt::{ }; use jolt_claims::NoChallenges; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::CommitmentScheme; use jolt_program::preprocess::PublicIoMemory; use jolt_transcript::Transcript; @@ -42,14 +42,14 @@ use crate::{ /// reduction challenge are extracted mode-agnostically (clear: the single-entry /// reduction point; ZK: the committed round challenge) so the batch relations — /// `ProductRemainder::new` in particular — can be built before the mode branch. -struct ProductUniskipStep { +struct ProductUniskipStep { tau_low: Vec, tau_high: F, challenge: F, verified: ProductUniskipVerified, } -enum ProductUniskipVerified { +enum ProductUniskipVerified { Clear, Zk(uniskip::UniskipZk), } @@ -59,7 +59,7 @@ enum ProductUniskipVerified { /// `*_from_upstream` helper wires which upstream opening feeds which downstream /// input. The product-remainder input is the product uni-skip's output claim (a /// separate stage-2 sub-sumcheck), not an upstream stage's opening. -pub fn stage2_batch_input_values_from_upstream( +pub fn stage2_batch_input_values_from_upstream( stage1: &Stage1ClearOutput, product_uniskip_output_claim: F, ) -> Stage2BatchInputClaims { @@ -224,7 +224,7 @@ where /// The product uni-skip's low binding tau_low: the tail (`[1..]`) of stage /// 1's raw remainder point, reversed. Shared by `verify_product_uniskip` and /// the prove-side stage-2 recipe, so the derivation cannot drift. -pub fn product_tau_low( +pub fn product_tau_low( stage1_remainder: &[F], log_t: usize, ) -> Result, VerifierError> { diff --git a/crates/jolt-verifier/src/stages/stage3/instruction_input.rs b/crates/jolt-verifier/src/stages/stage3/instruction_input.rs index 0763533c7c..78bbaf4837 100644 --- a/crates/jolt-verifier/src/stages/stage3/instruction_input.rs +++ b/crates/jolt-verifier/src/stages/stage3/instruction_input.rs @@ -24,7 +24,7 @@ use jolt_claims::protocols::jolt::{ JoltDerivedId, JoltOpeningId, JoltRelationId, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::try_eq_mle; use crate::stages::relations::ConcreteSumcheck; @@ -33,7 +33,7 @@ use crate::VerifierError; /// Wire the consumed opening *values* from stage 2's product-remainder left/right /// instruction inputs. Takes the ZK-agnostic stage-2 output-claims aggregate. -pub fn instruction_input_input_values_from_upstream( +pub fn instruction_input_input_values_from_upstream( stage2: &Stage2BatchOutputClaims, ) -> InstructionInputInputClaims { let product_remainder = &stage2.product_remainder; @@ -44,12 +44,12 @@ pub fn instruction_input_input_values_from_upstream( } #[derive(Clone)] -pub struct InstructionInput { +pub struct InstructionInput { symbolic: relations::instruction::InputVirtualization, product_remainder_opening_point: Vec, } -impl InstructionInput { +impl InstructionInput { pub fn new(trace_dimensions: TraceDimensions, product_remainder_opening_point: Vec) -> Self { Self { symbolic: relations::instruction::InputVirtualization::new(trace_dimensions), @@ -62,7 +62,7 @@ impl InstructionInput { } } -impl ConcreteSumcheck for InstructionInput { +impl ConcreteSumcheck for InstructionInput { type Symbolic = relations::instruction::InputVirtualization; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage3/outputs.rs b/crates/jolt-verifier/src/stages/stage3/outputs.rs index 6ce43cf362..495d5bb402 100644 --- a/crates/jolt-verifier/src/stages/stage3/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage3/outputs.rs @@ -1,6 +1,6 @@ //! Typed inputs consumed and outputs produced by stage 3 verification. -use jolt_field::Field; +use jolt_field::JoltField; use jolt_sumcheck::BatchedCommittedSumcheckConsistency; use crate::stages::relations::SumcheckBatch; @@ -30,13 +30,13 @@ pub use super::spartan_shift::{SpartanShift, SpartanShiftOutputClaims}; /// copies equal their sources. #[derive(SumcheckBatch)] #[sumcheck_batch(crate = "crate")] -pub struct Stage3Sumchecks { +pub struct Stage3Sumchecks { pub shift: SpartanShift, pub instruction_input: InstructionInput, pub registers_claim_reduction: RegistersClaimReduction, } -impl Stage3OutputPoints { +impl Stage3OutputPoints { /// The shift relation's shared opening point (every shift output carries it). pub fn shift_opening_point(&self) -> &[F] { self.shift.unexpanded_pc() @@ -44,7 +44,7 @@ impl Stage3OutputPoints { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage3ClearOutput { +pub struct Stage3ClearOutput { /// The produced stage-3 opening *values* (wire form); read by later stages and /// the Fiat-Shamir opening-claim encoder. pub output_values: Stage3OutputClaims, @@ -54,7 +54,7 @@ pub struct Stage3ClearOutput { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage3ZkOutput { +pub struct Stage3ZkOutput { pub challenges: Stage3Challenges, pub batch_consistency: BatchedCommittedSumcheckConsistency, pub batch_output_claims: CommittedOutputClaimOutput, @@ -64,12 +64,12 @@ pub struct Stage3ZkOutput { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Stage3Output { +pub enum Stage3Output { Clear(Stage3ClearOutput), Zk(Stage3ZkOutput), } -impl Stage3Output { +impl Stage3Output { /// The produced opening points, available regardless of proving mode. pub fn output_points(&self) -> &Stage3OutputPoints { match self { @@ -99,7 +99,7 @@ mod tests { use super::*; use crate::stages::relations::ConcreteSumcheck; use jolt_claims::protocols::jolt::geometry::dimensions::TraceDimensions; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { Fr::from_u64(value) diff --git a/crates/jolt-verifier/src/stages/stage3/registers_claim_reduction.rs b/crates/jolt-verifier/src/stages/stage3/registers_claim_reduction.rs index f5b460e3e7..74003dc4f1 100644 --- a/crates/jolt-verifier/src/stages/stage3/registers_claim_reduction.rs +++ b/crates/jolt-verifier/src/stages/stage3/registers_claim_reduction.rs @@ -20,7 +20,7 @@ use jolt_claims::protocols::jolt::{ JoltRelationId, RegistersClaimReductionPublic, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::try_eq_mle; use crate::stages::relations::ConcreteSumcheck; @@ -29,7 +29,7 @@ use crate::VerifierError; /// Wire the consumed opening *values* from stage 1's outer sumcheck register /// values. Takes the ZK-agnostic stage-1 output-claims aggregate. -pub fn registers_claim_reduction_input_values_from_upstream( +pub fn registers_claim_reduction_input_values_from_upstream( stage1: &Stage1BatchOutputClaims, ) -> RegistersClaimReductionInputClaims { let outer = &stage1.outer_remainder; @@ -41,12 +41,12 @@ pub fn registers_claim_reduction_input_values_from_upstream( } #[derive(Clone)] -pub struct RegistersClaimReduction { +pub struct RegistersClaimReduction { symbolic: relations::claim_reductions::registers::ClaimReduction, product_uniskip_tau_low: Vec, } -impl RegistersClaimReduction { +impl RegistersClaimReduction { pub fn new(trace_dimensions: TraceDimensions, product_uniskip_tau_low: Vec) -> Self { Self { symbolic: relations::claim_reductions::registers::ClaimReduction::new(trace_dimensions), @@ -59,7 +59,7 @@ impl RegistersClaimReduction { } } -impl ConcreteSumcheck for RegistersClaimReduction { +impl ConcreteSumcheck for RegistersClaimReduction { type Symbolic = relations::claim_reductions::registers::ClaimReduction; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage3/spartan_shift.rs b/crates/jolt-verifier/src/stages/stage3/spartan_shift.rs index 5cd94594d3..d33a47a105 100644 --- a/crates/jolt-verifier/src/stages/stage3/spartan_shift.rs +++ b/crates/jolt-verifier/src/stages/stage3/spartan_shift.rs @@ -12,7 +12,7 @@ use jolt_claims::protocols::jolt::{ geometry::dimensions::TraceDimensions, JoltDerivedId, SpartanShiftPublic, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::EqPlusOnePolynomial; use crate::stages::relations::ConcreteSumcheck; @@ -23,7 +23,7 @@ use crate::VerifierError; /// Wire shift's consumed opening *values* from stage 1's outer sumcheck (`Next*` /// PC/flag values) and stage 2's product-remainder `next_is_noop`. Takes the /// ZK-agnostic upstream output-claims aggregates. -pub fn spartan_shift_input_values_from_upstream( +pub fn spartan_shift_input_values_from_upstream( stage1: &Stage1BatchOutputClaims, stage2: &Stage2BatchOutputClaims, ) -> SpartanShiftInputClaims { @@ -38,13 +38,13 @@ pub fn spartan_shift_input_values_from_upstream( } #[derive(Clone)] -pub struct SpartanShift { +pub struct SpartanShift { symbolic: relations::spartan::Shift, product_uniskip_tau_low: Vec, product_remainder_opening_point: Vec, } -impl SpartanShift { +impl SpartanShift { pub fn new( trace_dimensions: TraceDimensions, product_uniskip_tau_low: Vec, @@ -66,7 +66,7 @@ impl SpartanShift { } } -impl ConcreteSumcheck for SpartanShift { +impl ConcreteSumcheck for SpartanShift { type Symbolic = relations::spartan::Shift; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage3/verify.rs b/crates/jolt-verifier/src/stages/stage3/verify.rs index b67b957a08..6dd2002ad7 100644 --- a/crates/jolt-verifier/src/stages/stage3/verify.rs +++ b/crates/jolt-verifier/src/stages/stage3/verify.rs @@ -2,7 +2,7 @@ use jolt_claims::protocols::jolt::{geometry::dimensions::TraceDimensions, JoltRelationId}; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::CommitmentScheme; use jolt_transcript::Transcript; @@ -31,7 +31,7 @@ use crate::{ /// the generated `Stage3InputClaims` aggregate. This is the single place the /// stage's Outputs→Inputs dataflow is expressed: each per-relation `*_from_upstream` /// helper wires which upstream opening feeds which downstream input. -pub fn stage3_input_values_from_upstream( +pub fn stage3_input_values_from_upstream( stage1: &Stage1BatchOutputClaims, stage2: &Stage2BatchOutputClaims, ) -> Stage3InputClaims { diff --git a/crates/jolt-verifier/src/stages/stage4/outputs.rs b/crates/jolt-verifier/src/stages/stage4/outputs.rs index 8e315d0aac..85691564b6 100644 --- a/crates/jolt-verifier/src/stages/stage4/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage4/outputs.rs @@ -1,6 +1,6 @@ //! Typed inputs consumed and outputs produced by stage 4 verification. -use jolt_field::Field; +use jolt_field::JoltField; use jolt_sumcheck::BatchedCommittedSumcheckConsistency; use jolt_transcript::Transcript; @@ -35,12 +35,12 @@ use super::registers_read_write_checking::RegistersReadWriteChecking; /// count/validator cover their presence and count. #[derive(SumcheckBatch)] #[sumcheck_batch(no_opening_values, crate = "crate")] -pub struct Stage4Sumchecks { +pub struct Stage4Sumchecks { pub registers_read_write: RegistersReadWriteChecking, pub ram_val_check: RamValCheck, } -impl Stage4Sumchecks { +impl Stage4Sumchecks { /// The hand-written replacement for the absorb method the /// `no_opening_values` opt-out suppresses: stage 4's canonical order /// interleaves the RAM value-check's staged openings around the register @@ -52,7 +52,7 @@ impl Stage4Sumchecks { } } -impl Stage4OutputClaims { +impl Stage4OutputClaims { /// The produced opening claims in canonical (Fiat-Shamir) order, matching the /// prover's commitment (flush) order exactly: the `Val_init` advice openings, /// the committed program-image contribution, the register read-write openings, @@ -81,7 +81,7 @@ impl Stage4OutputClaims { } /// The shared opening-point accessors over the point-only stage-4 aggregate. -impl Stage4OutputPoints { +impl Stage4OutputPoints { /// The register read-write opening point (shared by all five register /// openings). pub fn registers_read_write_point(&self) -> &[F] { @@ -95,7 +95,7 @@ impl Stage4OutputPoints { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage4ClearOutput { +pub struct Stage4ClearOutput { /// The produced stage-4 opening *values* (wire form); read by later stages and /// the Fiat-Shamir opening-claim encoder. pub output_values: Stage4OutputClaims, @@ -109,7 +109,7 @@ pub struct Stage4ClearOutput { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage4ZkOutput { +pub struct Stage4ZkOutput { pub challenges: Stage4Challenges, pub batch_consistency: BatchedCommittedSumcheckConsistency, pub batch_output_claims: CommittedOutputClaimOutput, @@ -122,12 +122,12 @@ pub struct Stage4ZkOutput { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Stage4Output { +pub enum Stage4Output { Clear(Stage4ClearOutput), Zk(Stage4ZkOutput), } -impl Stage4Output { +impl Stage4Output { /// The produced opening points, available regardless of proving mode. pub fn output_points(&self) -> &Stage4OutputPoints { match self { @@ -156,7 +156,7 @@ mod tests { use super::*; use jolt_claims::protocols::jolt::relations::ram::RamValCheckOutputClaims; use jolt_claims::protocols::jolt::relations::registers::RegistersReadWriteOutputClaims; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { Fr::from_u64(value) diff --git a/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs b/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs index fb25a3b92a..d38d4d1814 100644 --- a/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs +++ b/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs @@ -27,7 +27,7 @@ use jolt_claims::protocols::jolt::{ JoltAdviceKind, JoltDerivedId, JoltOpeningId, JoltRelationId, RamValCheckPublic, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::{block_selector_mle_msb, LtPolynomial}; use jolt_transcript::{LabelWithCount, Transcript}; @@ -43,7 +43,7 @@ use super::outputs::Stage4OutputClaims; /// same advice / program-image openings the init evaluation is decomposed /// into). Only these values feed the input claim; clear-only because the values /// come from proof claims. -pub fn ram_val_check_input_values_from_upstream( +pub fn ram_val_check_input_values_from_upstream( stage2: &Stage2BatchOutputClaims, init: &RamValCheckInitialEvaluation, ) -> RamValCheckInputClaims { @@ -65,7 +65,7 @@ pub fn ram_val_check_input_values_from_upstream( /// (carried for completeness though only the values feed the input claim). /// ZK-agnostic: it reads the stage-2 point aggregate and the pre-branch init /// structure, so the same wiring serves both paths. -pub fn ram_val_check_input_points_from_upstream( +pub fn ram_val_check_input_points_from_upstream( stage2: &Stage2BatchOutputPoints, structure: &RamValCheckInitStructure, ) -> RamValCheckInputClaims> { @@ -84,7 +84,7 @@ pub fn ram_val_check_input_points_from_upstream( } #[derive(Clone)] -pub struct RamValCheck { +pub struct RamValCheck { symbolic: RamValCheckSymbolic, trace_dimensions: TraceDimensions, ram_log_k: usize, @@ -101,7 +101,7 @@ pub struct RamValCheck { contribution_openings: Vec, } -impl RamValCheck { +impl RamValCheck { /// Build the relation from its per-proof init decomposition. `init` carries /// the public initial-RAM evaluation plus the present advice/program-image /// contributions; their *structure* feeds the symbolic input `Expr` and their @@ -161,7 +161,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for RamValCheck { +impl ConcreteSumcheck for RamValCheck { type Symbolic = RamValCheckSymbolic; fn symbolic(&self) -> &Self::Symbolic { @@ -289,7 +289,7 @@ impl ConcreteSumcheck for RamValCheck { /// /// [`decomposition`]: Self::decomposition #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RamValCheckInitStructure { +pub struct RamValCheckInitStructure { pub public_eval: F, /// The staged program-image contribution's opening point (committed program /// mode only): the full RAM address point. @@ -299,7 +299,7 @@ pub struct RamValCheckInitStructure { pub advice_blocks: Vec<(JoltAdviceKind, RamValCheckAdviceBlock)>, } -impl RamValCheckInitStructure { +impl RamValCheckInitStructure { pub fn advice_block(&self, kind: JoltAdviceKind) -> Option<&RamValCheckAdviceBlock> { self.advice_blocks .iter() @@ -334,7 +334,7 @@ impl RamValCheckInitStructure { /// geometry. Runs before the zk/clear branch in both modes; the advice selectors /// and opening points come from [`ram_val_check_advice_block`], the same /// computation the prover uses. -pub fn ram_val_check_init_structure( +pub fn ram_val_check_init_structure( checked: &CheckedInputs, untrusted_advice_present: bool, r_address: &[F], @@ -370,7 +370,7 @@ pub fn ram_val_check_init_structure( /// [`RamValCheckInitStructure`] and the proof's claimed opening values; consumed by /// the stage-4 input wiring and the downstream stage-6/7 address-phase reductions. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RamValCheckInitialEvaluation { +pub struct RamValCheckInitialEvaluation { pub public_eval: F, /// The staged program-image contribution's opening point (the full RAM address /// point) and value; committed-program mode only. @@ -378,7 +378,7 @@ pub struct RamValCheckInitialEvaluation { pub advice_contributions: Vec>, } -impl RamValCheckInitialEvaluation { +impl RamValCheckInitialEvaluation { pub fn advice_contribution( &self, kind: JoltAdviceKind, @@ -390,7 +390,7 @@ impl RamValCheckInitialEvaluation { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct VerifiedRamValCheckAdviceContribution { +pub struct VerifiedRamValCheckAdviceContribution { pub kind: JoltAdviceKind, pub selector: F, /// The advice block opening *point* (the address sub-point it was evaluated @@ -405,7 +405,7 @@ pub struct VerifiedRamValCheckAdviceContribution { /// exactly when its contribution is. Clear-only (the values come from proof /// claims); mirrors the prover's own init reconstruction so both decompose /// `Val_init` identically. -pub(crate) fn ram_val_check_initial_evaluation( +pub(crate) fn ram_val_check_initial_evaluation( structure: &RamValCheckInitStructure, claims: &Stage4OutputClaims, ) -> Result, VerifierError> { @@ -464,7 +464,7 @@ pub(crate) fn ram_val_check_initial_evaluation( /// WARNING: the ZK path recomputes the same geometry in `zk::blindfold`'s /// `advice_selector`, so the two must stay in lockstep. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RamValCheckAdviceBlock { +pub struct RamValCheckAdviceBlock { pub selector: F, pub opening_point: Vec, } @@ -474,7 +474,7 @@ pub struct RamValCheckAdviceBlock { /// /// WARNING: the ZK path recomputes the same geometry in `zk::blindfold`'s /// `advice_selector`, so the two must stay in lockstep. -fn ram_val_check_advice_block( +fn ram_val_check_advice_block( kind: JoltAdviceKind, checked: &CheckedInputs, r_address: &[F], diff --git a/crates/jolt-verifier/src/stages/stage4/registers_read_write_checking.rs b/crates/jolt-verifier/src/stages/stage4/registers_read_write_checking.rs index 5149ebf05a..080f108092 100644 --- a/crates/jolt-verifier/src/stages/stage4/registers_read_write_checking.rs +++ b/crates/jolt-verifier/src/stages/stage4/registers_read_write_checking.rs @@ -12,7 +12,7 @@ use jolt_claims::protocols::jolt::{ JoltDerivedId, JoltRelationId, RegistersReadWritePublic, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::try_eq_mle; use crate::stages::relations::ConcreteSumcheck; @@ -21,7 +21,7 @@ use crate::VerifierError; /// Wire the consumed opening *values* from stage 3's registers claim-reduction /// output. Takes the ZK-agnostic stage-3 output-claims aggregate. -pub fn registers_read_write_input_values_from_upstream( +pub fn registers_read_write_input_values_from_upstream( stage3: &Stage3OutputClaims, ) -> RegistersReadWriteInputClaims { let reduction = &stage3.registers_claim_reduction; @@ -35,7 +35,7 @@ pub fn registers_read_write_input_values_from_upstream( /// Wire the consumed opening *points* from stage 3's registers claim-reduction /// output, all sharing that relation's opening point. Takes the ZK-agnostic /// stage-3 output-points aggregate. -pub fn registers_read_write_input_points_from_upstream( +pub fn registers_read_write_input_points_from_upstream( stage3: &Stage3OutputPoints, ) -> RegistersReadWriteInputClaims> { let reduction = &stage3.registers_claim_reduction; @@ -47,13 +47,13 @@ pub fn registers_read_write_input_points_from_upstream( } #[derive(Clone)] -pub struct RegistersReadWriteChecking { +pub struct RegistersReadWriteChecking { symbolic: relations::registers::ReadWriteChecking, register_dimensions: ReadWriteDimensions, _field: core::marker::PhantomData, } -impl RegistersReadWriteChecking { +impl RegistersReadWriteChecking { pub fn new(register_dimensions: ReadWriteDimensions) -> Self { Self { symbolic: relations::registers::ReadWriteChecking::new(register_dimensions), @@ -74,7 +74,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for RegistersReadWriteChecking { +impl ConcreteSumcheck for RegistersReadWriteChecking { type Symbolic = relations::registers::ReadWriteChecking; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage4/verify.rs b/crates/jolt-verifier/src/stages/stage4/verify.rs index 49c24827c7..fdcb61880b 100644 --- a/crates/jolt-verifier/src/stages/stage4/verify.rs +++ b/crates/jolt-verifier/src/stages/stage4/verify.rs @@ -6,7 +6,7 @@ use jolt_claims::protocols::jolt::{ JoltRelationId, }; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::CommitmentScheme; use jolt_poly::sparse_segments_mle_msb; use jolt_program::preprocess::PublicInitialRam; @@ -45,7 +45,7 @@ use crate::{ /// come from stage 3's registers claim-reduction, and the RAM value-check inputs /// come from stage 2's RAM `val`/`val_final` plus the reconstructed `Val_init` /// decomposition (advice / program-image contributions). -pub fn stage4_input_values_from_upstream( +pub fn stage4_input_values_from_upstream( stage2: &Stage2BatchOutputClaims, stage3: &Stage3OutputClaims, ram_val_check_init: &RamValCheckInitialEvaluation, @@ -60,7 +60,7 @@ pub fn stage4_input_values_from_upstream( /// aggregates and the pre-branch init structure. ZK-agnostic: both the clear and /// ZK upstream outputs expose these, so the same wiring builds the input points in /// either mode. -pub fn stage4_input_points_from_upstream( +pub fn stage4_input_points_from_upstream( stage2: &Stage2BatchOutputPoints, stage3: &Stage3OutputPoints, structure: &RamValCheckInitStructure, diff --git a/crates/jolt-verifier/src/stages/stage5/instruction_read_raf.rs b/crates/jolt-verifier/src/stages/stage5/instruction_read_raf.rs index 8f520ab7f9..7925e683b7 100644 --- a/crates/jolt-verifier/src/stages/stage5/instruction_read_raf.rs +++ b/crates/jolt-verifier/src/stages/stage5/instruction_read_raf.rs @@ -18,7 +18,7 @@ use jolt_claims::protocols::jolt::{ InstructionReadRafPublic, JoltDerivedId, JoltRelationId, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_lookup_tables::{LookupTableKind, XLEN as RISCV_XLEN}; use jolt_poly::{ try_eq_mle, IdentityPolynomial, MultilinearEvaluation, OperandPolynomial, OperandSide, @@ -34,7 +34,7 @@ use crate::VerifierError; /// `expected_final_claim`) enforces their equality before this wiring reads it. /// Takes the ZK-agnostic stage-2 output-claims aggregate (both the clear and ZK /// stage-2 outputs expose it). -pub fn instruction_read_raf_input_values_from_upstream( +pub fn instruction_read_raf_input_values_from_upstream( stage2: &Stage2BatchOutputClaims, ) -> InstructionReadRafInputClaims { let reduction = &stage2.instruction_claim_reduction; @@ -47,7 +47,7 @@ pub fn instruction_read_raf_input_values_from_upstream( /// Wire the consumed opening *points* from the upstream instruction claim-reduction /// (stage 2). All three share the claim-reduction opening point. -pub fn instruction_read_raf_input_points_from_upstream( +pub fn instruction_read_raf_input_points_from_upstream( stage2: &Stage2BatchOutputPoints, ) -> InstructionReadRafInputClaims> { let point = stage2.instruction_claim_reduction_point().to_vec(); @@ -59,13 +59,13 @@ pub fn instruction_read_raf_input_points_from_upstream( } #[derive(Clone)] -pub struct InstructionReadRaf { +pub struct InstructionReadRaf { symbolic: relations::instruction::ReadRaf, dimensions: InstructionReadRafDimensions, _field: core::marker::PhantomData, } -impl InstructionReadRaf { +impl InstructionReadRaf { pub fn new(dimensions: InstructionReadRafDimensions) -> Self { Self { symbolic: relations::instruction::ReadRaf::new(dimensions), @@ -85,7 +85,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { /// Reconstruct the instruction address point from the virtual-RA opening points: /// each RA opening point is `chunk ++ r_cycle`, and the chunks tile the address /// in order, so stripping the trailing cycle and concatenating recovers it. -pub(crate) fn reconstruct_r_address( +pub(crate) fn reconstruct_r_address( output_points: &InstructionReadRafOutputClaims>, cycle_len: usize, ) -> Vec { @@ -96,13 +96,13 @@ pub(crate) fn reconstruct_r_address( .collect() } -impl InstructionReadRaf { +impl InstructionReadRaf { pub fn dimensions(&self) -> InstructionReadRafDimensions { self.dimensions } } -impl ConcreteSumcheck for InstructionReadRaf { +impl ConcreteSumcheck for InstructionReadRaf { type Symbolic = relations::instruction::ReadRaf; fn symbolic(&self) -> &Self::Symbolic { @@ -214,7 +214,7 @@ mod tests { use crate::stages::relations::ConcreteSumcheck; use jolt_claims::protocols::jolt::geometry::instruction::read_raf_output_openings; use jolt_claims::SymbolicSumcheck; - use jolt_field::{Fr, FromPrimitiveInt as _}; + use jolt_field::{Fr, Ring as _}; /// Locks the `expected_output_openings` invariant for the one stage-5 relation /// with a size-parameter-dependent shape: the openings the read-RAF output `Expr` diff --git a/crates/jolt-verifier/src/stages/stage5/outputs.rs b/crates/jolt-verifier/src/stages/stage5/outputs.rs index 6f4e19a52a..b6944b5161 100644 --- a/crates/jolt-verifier/src/stages/stage5/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage5/outputs.rs @@ -1,6 +1,6 @@ //! Typed inputs consumed and outputs produced by stage 5 verification. -use jolt_field::Field; +use jolt_field::JoltField; use jolt_sumcheck::BatchedCommittedSumcheckConsistency; use crate::stages::relations::SumcheckBatch; @@ -20,14 +20,14 @@ use super::registers_val_evaluation::RegistersValEvaluation; /// absorbed into the transcript, which must match the prover's commitment order. #[derive(SumcheckBatch)] #[sumcheck_batch(crate = "crate")] -pub struct Stage5Sumchecks { +pub struct Stage5Sumchecks { pub instruction_read_raf: InstructionReadRaf, pub ram_ra_claim_reduction: RamRaClaimReduction, pub registers_val_evaluation: RegistersValEvaluation, } /// The shared opening-point accessors over the point-only stage-5 aggregate. -impl Stage5OutputPoints { +impl Stage5OutputPoints { /// The instruction read-RAF cycle point (shared by the lookup-table-flag /// and RAF-flag openings). pub fn instruction_r_cycle(&self) -> &[F] { @@ -52,7 +52,7 @@ impl Stage5OutputPoints { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage5ClearOutput { +pub struct Stage5ClearOutput { pub challenges: Stage5Challenges, /// The produced stage-5 opening *values* (wire form); read by later stages and /// the Fiat-Shamir opening-claim encoder. @@ -69,7 +69,7 @@ pub struct Stage5ClearOutput { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage5ZkOutput { +pub struct Stage5ZkOutput { pub challenges: Stage5Challenges, pub batch_consistency: BatchedCommittedSumcheckConsistency, pub batch_output_claims: CommittedOutputClaimOutput, @@ -87,12 +87,12 @@ pub struct Stage5ZkOutput { // consistency and output-claim commitments. Boxing the common clear variant to // shrink the rarer ZK one would add indirection to every clear-path access. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Stage5Output { +pub enum Stage5Output { Clear(Stage5ClearOutput), Zk(Stage5ZkOutput), } -impl Stage5Output { +impl Stage5Output { /// The produced opening points, available regardless of proving mode. pub fn output_points(&self) -> &Stage5OutputPoints { match self { @@ -134,7 +134,7 @@ mod tests { use jolt_claims::protocols::jolt::relations::instruction::InstructionReadRafOutputClaims; use jolt_claims::protocols::jolt::relations::ram::RamRaClaimReductionOutputClaims; use jolt_claims::protocols::jolt::relations::registers::RegistersValEvaluationOutputClaims; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { Fr::from_u64(value) diff --git a/crates/jolt-verifier/src/stages/stage5/ram_ra_claim_reduction.rs b/crates/jolt-verifier/src/stages/stage5/ram_ra_claim_reduction.rs index 847b2135b8..c74b673a7f 100644 --- a/crates/jolt-verifier/src/stages/stage5/ram_ra_claim_reduction.rs +++ b/crates/jolt-verifier/src/stages/stage5/ram_ra_claim_reduction.rs @@ -13,7 +13,7 @@ use jolt_claims::protocols::jolt::{ geometry::dimensions::TraceDimensions, JoltDerivedId, JoltRelationId, RamRaClaimReductionPublic, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::try_eq_mle; use crate::stages::relations::ConcreteSumcheck; @@ -24,7 +24,7 @@ use crate::VerifierError; /// Wire this relation's consumed opening *values* from the upstream outputs: the /// RAF-evaluation and read-write openings (stage 2) and the val-check opening /// (stage 4). Takes the ZK-agnostic output-claims aggregates. -pub fn ram_ra_claim_reduction_input_values_from_upstream( +pub fn ram_ra_claim_reduction_input_values_from_upstream( stage2: &Stage2BatchOutputClaims, stage4: &Stage4OutputClaims, ) -> RamRaClaimReductionInputClaims { @@ -37,7 +37,7 @@ pub fn ram_ra_claim_reduction_input_values_from_upstream( /// Wire this relation's consumed opening *points* from the upstream output-points /// aggregates. -pub fn ram_ra_claim_reduction_input_points_from_upstream( +pub fn ram_ra_claim_reduction_input_points_from_upstream( stage2: &Stage2BatchOutputPoints, stage4: &Stage4OutputPoints, ) -> RamRaClaimReductionInputClaims> { @@ -49,14 +49,14 @@ pub fn ram_ra_claim_reduction_input_points_from_upstream( } #[derive(Clone)] -pub struct RamRaClaimReduction { +pub struct RamRaClaimReduction { symbolic: relations::ram::RaClaimReduction, trace_dimensions: TraceDimensions, ram_log_k: usize, _field: core::marker::PhantomData, } -impl RamRaClaimReduction { +impl RamRaClaimReduction { pub fn new(trace_dimensions: TraceDimensions, ram_log_k: usize) -> Self { Self { symbolic: relations::ram::RaClaimReduction::new(trace_dimensions), @@ -74,7 +74,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl RamRaClaimReduction { +impl RamRaClaimReduction { pub fn trace_dimensions(&self) -> TraceDimensions { self.trace_dimensions } @@ -84,7 +84,7 @@ impl RamRaClaimReduction { } } -impl ConcreteSumcheck for RamRaClaimReduction { +impl ConcreteSumcheck for RamRaClaimReduction { type Symbolic = relations::ram::RaClaimReduction; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage5/registers_val_evaluation.rs b/crates/jolt-verifier/src/stages/stage5/registers_val_evaluation.rs index e436f9fd3a..5ae0d8ac76 100644 --- a/crates/jolt-verifier/src/stages/stage5/registers_val_evaluation.rs +++ b/crates/jolt-verifier/src/stages/stage5/registers_val_evaluation.rs @@ -11,7 +11,7 @@ use jolt_claims::protocols::jolt::{ JoltDerivedId, JoltRelationId, RegistersValEvaluationPublic, }; use jolt_claims::{NoChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::LtPolynomial; use crate::stages::relations::ConcreteSumcheck; @@ -20,7 +20,7 @@ use crate::VerifierError; /// Wire the consumed `RegistersVal` opening *value* from the upstream register /// read-write checking (stage 4). Takes the ZK-agnostic output-claims aggregate. -pub fn registers_val_evaluation_input_values_from_upstream( +pub fn registers_val_evaluation_input_values_from_upstream( stage4: &Stage4OutputClaims, ) -> RegistersValEvaluationInputClaims { RegistersValEvaluationInputClaims { @@ -30,7 +30,7 @@ pub fn registers_val_evaluation_input_values_from_upstream( /// Wire the consumed `RegistersVal` opening *point* from the upstream register /// read-write checking (stage 4). -pub fn registers_val_evaluation_input_points_from_upstream( +pub fn registers_val_evaluation_input_points_from_upstream( stage4: &Stage4OutputPoints, ) -> RegistersValEvaluationInputClaims> { RegistersValEvaluationInputClaims { @@ -39,13 +39,13 @@ pub fn registers_val_evaluation_input_points_from_upstream( } #[derive(Clone)] -pub struct RegistersValEvaluation { +pub struct RegistersValEvaluation { symbolic: relations::registers::ValEvaluation, trace_dimensions: TraceDimensions, _field: PhantomData, } -impl RegistersValEvaluation { +impl RegistersValEvaluation { pub fn new(trace_dimensions: TraceDimensions) -> Self { Self { symbolic: relations::registers::ValEvaluation::new(trace_dimensions), @@ -62,13 +62,13 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl RegistersValEvaluation { +impl RegistersValEvaluation { pub fn trace_dimensions(&self) -> TraceDimensions { self.trace_dimensions } } -impl ConcreteSumcheck for RegistersValEvaluation { +impl ConcreteSumcheck for RegistersValEvaluation { type Symbolic = relations::registers::ValEvaluation; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage5/verify.rs b/crates/jolt-verifier/src/stages/stage5/verify.rs index 0b2a94b2b2..ad763e51df 100644 --- a/crates/jolt-verifier/src/stages/stage5/verify.rs +++ b/crates/jolt-verifier/src/stages/stage5/verify.rs @@ -1,6 +1,6 @@ use jolt_claims::protocols::jolt::{geometry::dimensions::JoltFormulaDimensions, JoltRelationId}; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::CommitmentScheme; use jolt_transcript::Transcript; @@ -38,7 +38,7 @@ use crate::{ /// Outputs→Inputs dataflow is expressed: each per-relation `*_from_upstream` helper /// wires which upstream opening feeds which downstream input. Public because the /// prover's stage-5 recipe builds its batch inputs through the same wiring. -pub fn stage5_input_values_from_upstream( +pub fn stage5_input_values_from_upstream( stage2: &Stage2BatchOutputClaims, stage4: &Stage4OutputClaims, ) -> Stage5InputClaims { @@ -52,7 +52,7 @@ pub fn stage5_input_values_from_upstream( /// Assemble the stage-5 consumed opening *points* from the upstream output-points /// aggregates. ZK-agnostic: both the clear and ZK stage-2/stage-4 outputs expose /// these, so the same wiring builds the input points in either mode. -pub fn stage5_input_points_from_upstream( +pub fn stage5_input_points_from_upstream( stage2: &Stage2BatchOutputPoints, stage4: &Stage4OutputPoints, ) -> Stage5InputPoints { diff --git a/crates/jolt-verifier/src/stages/stage6a/batch.rs b/crates/jolt-verifier/src/stages/stage6a/batch.rs index 5e4de9de19..50e7aea687 100644 --- a/crates/jolt-verifier/src/stages/stage6a/batch.rs +++ b/crates/jolt-verifier/src/stages/stage6a/batch.rs @@ -9,7 +9,7 @@ use jolt_claims::protocols::jolt::geometry::{ booleanity::BooleanityDimensions, dimensions::JoltFormulaDimensions, }; -use jolt_field::Field; +use jolt_field::JoltField; use super::booleanity::BooleanityAddressPhase; use super::bytecode_read_raf::{bytecode_stage_points, BytecodeReadRafAddressPhase}; @@ -23,7 +23,7 @@ use crate::VerifierError; /// The batch legs [`Stage6aSumchecks::build_from_parts`] assembles the members /// from: protocol geometry and the mode-agnostic upstream opening points. /// Every field is data both the verifier and the prover hold. -pub struct Stage6aBuildParts<'a, F: Field> { +pub struct Stage6aBuildParts<'a, F: JoltField> { pub formula_dimensions: &'a JoltFormulaDimensions, pub committed_chunk_bits: usize, pub committed_program: bool, @@ -35,7 +35,7 @@ pub struct Stage6aBuildParts<'a, F: Field> { pub stage5_points: &'a Stage5OutputPoints, } -impl Stage6aSumchecks { +impl Stage6aSumchecks { /// Assemble the address-phase batch: the bytecode member carries the /// upstream cycle/register points and the entry index (full geometry at /// construction — the prover's kernel read path; the verifier itself diff --git a/crates/jolt-verifier/src/stages/stage6a/booleanity.rs b/crates/jolt-verifier/src/stages/stage6a/booleanity.rs index 1c15841386..4b61d2808e 100644 --- a/crates/jolt-verifier/src/stages/stage6a/booleanity.rs +++ b/crates/jolt-verifier/src/stages/stage6a/booleanity.rs @@ -12,14 +12,14 @@ pub use jolt_claims::protocols::jolt::relations::booleanity::{ BooleanityAddressPhaseOutputClaims, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_transcript::Transcript; use crate::stages::relations::ConcreteSumcheck; use crate::VerifierError; #[derive(Clone)] -pub struct BooleanityAddressPhase { +pub struct BooleanityAddressPhase { symbolic: relations::booleanity::BooleanityAddressPhase, dimensions: BooleanityDimensions, /// The stage-5 instruction read-RAF opening points (big-endian) the @@ -32,7 +32,7 @@ pub struct BooleanityAddressPhase { instruction_r_cycle: Vec, } -impl BooleanityAddressPhase { +impl BooleanityAddressPhase { pub fn new( dimensions: BooleanityDimensions, instruction_r_address: Vec, @@ -60,7 +60,7 @@ impl BooleanityAddressPhase { } } -impl ConcreteSumcheck for BooleanityAddressPhase { +impl ConcreteSumcheck for BooleanityAddressPhase { type Symbolic = relations::booleanity::BooleanityAddressPhase; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs b/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs index 69dd704f03..6806d7f03b 100644 --- a/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs +++ b/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs @@ -23,7 +23,7 @@ use jolt_claims::protocols::jolt::{ JoltOpeningId, JoltRelationId, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use crate::stages::relations::{ConcreteSumcheck, SumcheckInputPoints}; use crate::stages::stage2::Stage2BatchOutputPoints; @@ -42,13 +42,13 @@ use crate::VerifierError; /// stage-1 binding is the raw remainder tail, re-reversed), plus the register /// opening points whose 7-var address prefixes feed the stage-value folds. #[derive(Clone)] -pub struct BytecodeStagePoints { +pub struct BytecodeStagePoints { pub stage_cycle_points: [Vec; 5], pub register_read_write_point: Vec, pub register_val_evaluation_point: Vec, } -impl BytecodeStagePoints { +impl BytecodeStagePoints { /// The stage-4 register read-write cycle leg (`stage_cycle_points[3]`). pub fn register_read_write_cycle(&self) -> &[F] { &self.stage_cycle_points[3] @@ -66,7 +66,7 @@ impl BytecodeStagePoints { /// paths. The BlindFold ZK input derivation (`crate::stages::zk::blindfold`) /// assembles its own legs from the committed consistency points and does not /// route through this helper. -pub fn bytecode_stage_points( +pub fn bytecode_stage_points( stage1_cycle_binding: &[F], stage2: &Stage2BatchOutputPoints, stage3: &Stage3OutputPoints, @@ -113,7 +113,7 @@ type AddressPhaseSymbolic = /// field-for-field read from the stage-1 outer remainder; the input claim reads /// only values, so the consumed input *points* are the generated all-empty /// `empty_input_points`. -pub fn bytecode_read_raf_address_phase_input_values_from_upstream( +pub fn bytecode_read_raf_address_phase_input_values_from_upstream( stage1: &Stage1BatchOutputClaims, stage2: &Stage2BatchOutputClaims, stage3: &Stage3OutputClaims, @@ -168,7 +168,7 @@ pub fn bytecode_read_raf_address_phase_input_values_from_upstream( } #[derive(Clone)] -pub struct BytecodeReadRafAddressPhase { +pub struct BytecodeReadRafAddressPhase { symbolic: AddressPhaseSymbolic, dimensions: BytecodeReadRafDimensions, /// Committed-program mode stages the `BytecodeValClaim` wire claims. @@ -182,7 +182,7 @@ pub struct BytecodeReadRafAddressPhase { entry_bytecode_index: usize, } -impl BytecodeReadRafAddressPhase { +impl BytecodeReadRafAddressPhase { pub fn new( dimensions: BytecodeReadRafDimensions, committed_program: bool, @@ -238,7 +238,7 @@ impl BytecodeReadRafAddressPhase { } } -impl ConcreteSumcheck for BytecodeReadRafAddressPhase { +impl ConcreteSumcheck for BytecodeReadRafAddressPhase { type Symbolic = AddressPhaseSymbolic; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage6a/outputs.rs b/crates/jolt-verifier/src/stages/stage6a/outputs.rs index 51419db1ce..14d07bd488 100644 --- a/crates/jolt-verifier/src/stages/stage6a/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage6a/outputs.rs @@ -3,7 +3,7 @@ use jolt_claims::protocols::jolt::relations::booleanity::BooleanityAddressPhaseChallenges; use jolt_claims::protocols::jolt::relations::bytecode::BytecodeReadRafAddressPhaseChallenges; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_sumcheck::BatchedCommittedSumcheckConsistency; use crate::stages::relations::SumcheckBatch; @@ -41,7 +41,7 @@ use super::bytecode_read_raf::BytecodeReadRafAddressPhase; /// call it directly. #[derive(SumcheckBatch)] #[sumcheck_batch(crate = "crate")] -pub struct Stage6aSumchecks { +pub struct Stage6aSumchecks { pub bytecode_read_raf: BytecodeReadRafAddressPhase, pub booleanity: BooleanityAddressPhase, } @@ -53,7 +53,7 @@ pub struct Stage6aSumchecks { /// 6b's members consume them as well, so 6a carries them downstream as typed /// upstream values (the same idiom as `Stage2ZkOutput`'s `product_tau_high`). #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage6aCarriedChallenges { +pub struct Stage6aCarriedChallenges { /// The bytecode read-RAF address-phase draws (the fold gamma plus the five /// per-stage gammas), verbatim. Consumers folding with power vectors expand /// them via `stage_gamma_powers`. @@ -65,7 +65,7 @@ pub struct Stage6aCarriedChallenges { pub booleanity: BooleanityAddressPhaseChallenges, } -impl From<&Stage6aChallenges> for Stage6aCarriedChallenges { +impl From<&Stage6aChallenges> for Stage6aCarriedChallenges { fn from(challenges: &Stage6aChallenges) -> Self { Self { bytecode_read_raf: challenges.bytecode_read_raf, @@ -75,7 +75,7 @@ impl From<&Stage6aChallenges> for Stage6aCarriedChallenges { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage6aClearOutput { +pub struct Stage6aClearOutput { /// The produced address-phase opening *values* (the staged intermediates /// and, in committed-program mode, the `BytecodeValClaim` claims), read by /// stage 6b as its bytecode/booleanity input claims. @@ -89,7 +89,7 @@ pub struct Stage6aClearOutput { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage6aZkOutput { +pub struct Stage6aZkOutput { pub challenges: Stage6aCarriedChallenges, pub consistency: BatchedCommittedSumcheckConsistency, pub output_claims: CommittedOutputClaimOutput, @@ -99,12 +99,12 @@ pub struct Stage6aZkOutput { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Stage6aOutput { +pub enum Stage6aOutput { Clear(Stage6aClearOutput), Zk(Stage6aZkOutput), } -impl Stage6aOutput { +impl Stage6aOutput { /// The produced address-phase opening *points*, available regardless of mode. pub fn output_points(&self) -> &Stage6aOutputPoints { match self { diff --git a/crates/jolt-verifier/src/stages/stage6a/verify.rs b/crates/jolt-verifier/src/stages/stage6a/verify.rs index fb57fcfb66..1946480967 100644 --- a/crates/jolt-verifier/src/stages/stage6a/verify.rs +++ b/crates/jolt-verifier/src/stages/stage6a/verify.rs @@ -170,7 +170,7 @@ mod tests { use jolt_claims::protocols::jolt::geometry::booleanity::BooleanityDimensions; use jolt_claims::protocols::jolt::geometry::bytecode::BytecodeReadRafDimensions; use jolt_claims::protocols::jolt::geometry::ra::JoltRaPolynomialLayout; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { Fr::from_u64(value) diff --git a/crates/jolt-verifier/src/stages/stage6b/batch.rs b/crates/jolt-verifier/src/stages/stage6b/batch.rs index f76dfa3c22..95b922cc01 100644 --- a/crates/jolt-verifier/src/stages/stage6b/batch.rs +++ b/crates/jolt-verifier/src/stages/stage6b/batch.rs @@ -20,7 +20,7 @@ use jolt_claims::protocols::jolt::{ }; use jolt_claims::NoChallenges; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::CommitmentScheme; use jolt_riscv::JoltInstructionRow; use jolt_transcript::Transcript; @@ -66,7 +66,7 @@ use crate::VerifierError; /// draws, the mode-agnostic upstream opening points, and the clear-only value /// aux (each empty/`None` in ZK, where `input_claim`/`expected_output` never /// run). Every field is data both the verifier and the prover hold. -pub struct Stage6bBuildParts<'a, F: Field> { +pub struct Stage6bBuildParts<'a, F: JoltField> { pub formula_dimensions: &'a JoltFormulaDimensions, pub ram_log_k: usize, pub committed_chunk_bits: usize, @@ -103,7 +103,7 @@ pub struct Stage6bDraws { pub eta: Option, } -impl Stage6bDraws { +impl Stage6bDraws { pub fn draw>( transcript: &mut T, committed_bytecode: bool, @@ -119,7 +119,7 @@ impl Stage6bDraws { } } -impl Stage6bSumchecks { +impl Stage6bSumchecks { #[expect( clippy::too_many_arguments, reason = "Stage 6b's batch is built from the stage-6a output plus all five prior stage outputs directly; bundling them would reintroduce the removed `Stage6bParams` pack/unpack indirection." diff --git a/crates/jolt-verifier/src/stages/stage6b/booleanity.rs b/crates/jolt-verifier/src/stages/stage6b/booleanity.rs index 9fa0dc08f7..98050b5813 100644 --- a/crates/jolt-verifier/src/stages/stage6b/booleanity.rs +++ b/crates/jolt-verifier/src/stages/stage6b/booleanity.rs @@ -23,7 +23,7 @@ use jolt_claims::protocols::jolt::{ geometry::booleanity::BooleanityDimensions, BooleanityPublic, JoltDerivedId, JoltRelationId, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::try_eq_mle; use crate::stages::relations::{ConcreteSumcheck, SumcheckInputPoints, SumcheckOutputPoints}; @@ -43,7 +43,7 @@ pub type BooleanityCycleDimensions = BooleanityDimensions; pub type BooleanityCycleDimensions = lattice_booleanity::LatticeBooleanityDimensions; #[derive(Clone)] -pub struct Booleanity { +pub struct Booleanity { symbolic: CyclePhaseSymbolic, dimensions: BooleanityCycleDimensions, /// The address opening prefix from the stage-6a phase. @@ -53,7 +53,7 @@ pub struct Booleanity { reference_cycle: Vec, } -impl Booleanity { +impl Booleanity { pub fn new( dimensions: BooleanityCycleDimensions, r_address: Vec, @@ -104,7 +104,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for Booleanity { +impl ConcreteSumcheck for Booleanity { type Symbolic = CyclePhaseSymbolic; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage6b/bytecode_read_raf.rs b/crates/jolt-verifier/src/stages/stage6b/bytecode_read_raf.rs index 0f329f4b58..89772c48c7 100644 --- a/crates/jolt-verifier/src/stages/stage6b/bytecode_read_raf.rs +++ b/crates/jolt-verifier/src/stages/stage6b/bytecode_read_raf.rs @@ -28,7 +28,7 @@ use jolt_claims::protocols::jolt::{ BytecodeReadRafChallenge, JoltChallengeId, JoltDerivedId, JoltRelationId, }; use jolt_claims::{SumcheckChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::EqPolynomial; use jolt_riscv::JoltInstructionRow; @@ -58,7 +58,7 @@ pub type BytecodeReadRafCycleOutputClaims = LatticeBytecodeReadRafOutputClaim /// weight each row. Consumed at construction ([`BytecodeReadRaf::new`] folds the /// table against `eq(r_address)` immediately), so nothing borrowed is stored and /// the relation stays lifetime-free. -pub struct BytecodeReadRafTableFoldInputs<'a, F: Field> { +pub struct BytecodeReadRafTableFoldInputs<'a, F: JoltField> { pub bytecode: &'a [JoltInstructionRow], pub register_read_write_point: &'a [F], pub register_val_evaluation_point: &'a [F], @@ -70,7 +70,7 @@ pub struct BytecodeReadRafTableFoldInputs<'a, F: Field> { /// `stage_cycle_points` are the verifier's per-stage cycle bindings. /// `table_fold` is `Some` only in clear mode — ZK never runs `expected_output`, /// so it skips the `O(2^log_k)` fold entirely. -pub struct BytecodeReadRafCycleInputs<'a, F: Field> { +pub struct BytecodeReadRafCycleInputs<'a, F: JoltField> { pub dimensions: BytecodeReadRafDimensions, pub r_address: Vec, pub stage_cycle_points: [Vec; READ_RAF_CYCLE_STAGES], @@ -111,7 +111,7 @@ fn cycle_symbolic_committed(dimensions: BytecodeReadRafDimensions) -> CycleSymbo /// attached in [`ConcreteSumcheck::expected_output`], which it OVERRIDES to /// evaluate the publics once and reuse the [`expected_output_from_publics`] helper. #[derive(Clone)] -pub struct BytecodeReadRaf { +pub struct BytecodeReadRaf { symbolic: CycleSymbolic, dimensions: BytecodeReadRafDimensions, r_address: Vec, @@ -126,7 +126,7 @@ pub struct BytecodeReadRaf { stage_values_at_r_address: Option<[F; NUM_BYTECODE_VAL_STAGES]>, } -impl BytecodeReadRaf { +impl BytecodeReadRaf { pub fn new(inputs: BytecodeReadRafCycleInputs<'_, F>) -> Result { let stage_values_at_r_address = inputs .table_fold @@ -149,7 +149,7 @@ impl BytecodeReadRaf { /// the lattice store stage as its last element) folded against /// `eq(r_address)`. The cycle-eq factors are attached later, at /// `expected_output` time, so the fold can run before the cycle sumcheck. -fn fold_stage_values( +fn fold_stage_values( r_address: &[F], fold: BytecodeReadRafTableFoldInputs<'_, F>, ) -> Result<[F; NUM_BYTECODE_VAL_STAGES], VerifierError> { @@ -191,7 +191,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { /// The `log_t`-variable cycle suffix of a produced `BytecodeRa` opening point /// (`chunk ++ r_cycle`). -fn r_cycle_suffix(log_t: usize, opening_point: &[F]) -> Result<&[F], VerifierError> { +fn r_cycle_suffix(log_t: usize, opening_point: &[F]) -> Result<&[F], VerifierError> { opening_point .get(opening_point.len() - log_t..) .ok_or_else(|| public_input_failed("bytecode cycle opening point shorter than log_t")) @@ -200,7 +200,7 @@ fn r_cycle_suffix(log_t: usize, opening_point: &[F]) -> Result<&[F], V /// Evaluate the full-program bytecode read-RAF output expression at the produced /// `BytecodeRa` openings and public values. #[cfg(not(feature = "akita"))] -fn expected_output_from_publics( +fn expected_output_from_publics( dimensions: BytecodeReadRafDimensions, public_values: &bytecode::BytecodeReadRafPublicValues, bytecode_ra: &[F], @@ -240,7 +240,7 @@ fn expected_output_from_publics( ) } -impl ConcreteSumcheck for BytecodeReadRaf { +impl ConcreteSumcheck for BytecodeReadRaf { type Symbolic = CycleSymbolic; fn symbolic(&self) -> &Self::Symbolic { @@ -368,7 +368,7 @@ impl ConcreteSumcheck for BytecodeReadRaf { /// Derive the cycle-phase produced opening points: one `(chunk ++ r_cycle)` /// point per committed `BytecodeRa` chunk, plus (packed) the `FusedInc` cycle /// point. -fn derive_cycle_opening_points( +fn derive_cycle_opening_points( r_address: &[F], committed_chunk_bits: usize, r_cycle: Vec, @@ -393,7 +393,7 @@ fn derive_cycle_opening_points( /// Construction inputs for the committed-program bytecode cycle relation. /// One cycle point per relation stage — five in base mode, nine on the packed /// path (the four fused-inc consumer points follow the base five). -pub struct BytecodeReadRafCommittedCycleInputs { +pub struct BytecodeReadRafCommittedCycleInputs { pub dimensions: BytecodeReadRafDimensions, pub r_address: Vec, pub stage_cycle_points: [Vec; READ_RAF_CYCLE_STAGES], @@ -413,7 +413,7 @@ pub struct BytecodeReadRafCommittedCycleInputs { /// [`ConcreteSumcheck::expected_output`]: the staged Val openings are inputs mixed /// into the output, and the committed public values are evaluated once. #[derive(Clone)] -pub struct BytecodeReadRafCommitted { +pub struct BytecodeReadRafCommitted { symbolic: CycleSymbolicCommitted, dimensions: BytecodeReadRafDimensions, r_address: Vec, @@ -423,7 +423,7 @@ pub struct BytecodeReadRafCommitted { val_stages: Vec, } -impl BytecodeReadRafCommitted { +impl BytecodeReadRafCommitted { pub fn new(inputs: BytecodeReadRafCommittedCycleInputs) -> Self { Self { symbolic: cycle_symbolic_committed(inputs.dimensions), @@ -437,7 +437,7 @@ impl BytecodeReadRafCommitted { } } -impl ConcreteSumcheck for BytecodeReadRafCommitted { +impl ConcreteSumcheck for BytecodeReadRafCommitted { type Symbolic = CycleSymbolicCommitted; fn symbolic(&self) -> &Self::Symbolic { @@ -513,7 +513,7 @@ impl ConcreteSumcheck for BytecodeReadRafCommitted { } #[derive(Clone)] -enum BytecodeReadRafCycleVariant { +enum BytecodeReadRafCycleVariant { Full(BytecodeReadRaf), Committed(BytecodeReadRafCommitted), } @@ -523,13 +523,13 @@ enum BytecodeReadRafCycleVariant { /// ([`BytecodeReadRafCommitted`]). Lifetime-free so it can be a /// `Stage6bSumchecks` member directly. #[derive(Clone)] -pub struct BytecodeReadRafCycle { +pub struct BytecodeReadRafCycle { /// The `ConcreteSumcheck` anchor symbolic (see the invariant on the impl). anchor: CycleSymbolicCommitted, variant: BytecodeReadRafCycleVariant, } -impl BytecodeReadRafCycle { +impl BytecodeReadRafCycle { pub fn full(inputs: BytecodeReadRafCycleInputs<'_, F>) -> Result { Ok(Self { anchor: cycle_symbolic_committed(inputs.dimensions), @@ -545,7 +545,7 @@ impl BytecodeReadRafCycle { } } -impl BytecodeReadRafCycle { +impl BytecodeReadRafCycle { pub fn dimensions(&self) -> BytecodeReadRafDimensions { match &self.variant { BytecodeReadRafCycleVariant::Full(relation) => relation.dimensions, @@ -616,7 +616,7 @@ impl BytecodeReadRafCycle { /// those overrides stand and the batch keeps `no_output_shape` (the /// committed output `Expr` references the staged `BytecodeValClaim` openings, /// which the full mode never produces). -impl ConcreteSumcheck for BytecodeReadRafCycle { +impl ConcreteSumcheck for BytecodeReadRafCycle { type Symbolic = CycleSymbolicCommitted; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage6b/committed_reduction_cycle_phase.rs b/crates/jolt-verifier/src/stages/stage6b/committed_reduction_cycle_phase.rs index e05b886488..e2b6ef82f2 100644 --- a/crates/jolt-verifier/src/stages/stage6b/committed_reduction_cycle_phase.rs +++ b/crates/jolt-verifier/src/stages/stage6b/committed_reduction_cycle_phase.rs @@ -37,7 +37,7 @@ use jolt_claims::protocols::jolt::{ PrecommittedReductionLayout, ProgramImageClaimReductionLayout, }; use jolt_claims::{NoChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use super::outputs::BytecodeReductionWeights; use crate::stages::relations::ConcreteSumcheck; @@ -47,7 +47,7 @@ use crate::VerifierError; /// Wire the consumed RAM value-check trusted-advice opening *value* off the RAM /// value-check initial evaluation. Clear-only. Errors if the RAM value-check /// produced no trusted-advice contribution (the reduction runs only when it did). -pub fn trusted_advice_cycle_phase_input_values_from_upstream( +pub fn trusted_advice_cycle_phase_input_values_from_upstream( ram_val_check_init: &RamValCheckInitialEvaluation, ) -> Result, VerifierError> { let trusted = ram_val_check_init @@ -60,7 +60,7 @@ pub fn trusted_advice_cycle_phase_input_values_from_upstream( } /// Wire the consumed RAM value-check untrusted-advice opening *value*. Clear-only. -pub fn untrusted_advice_cycle_phase_input_values_from_upstream( +pub fn untrusted_advice_cycle_phase_input_values_from_upstream( ram_val_check_init: &RamValCheckInitialEvaluation, ) -> Result, VerifierError> { let untrusted = ram_val_check_init @@ -76,7 +76,7 @@ pub fn untrusted_advice_cycle_phase_input_values_from_upstream( /// value-check initial evaluation — the clear-only reference the advice /// `FinalScale` terms read. `None` when the RAM value-check produced no /// contribution of this kind. -pub fn advice_reference_point_from_upstream( +pub fn advice_reference_point_from_upstream( ram_val_check_init: &RamValCheckInitialEvaluation, kind: JoltAdviceKind, ) -> Option> { @@ -93,7 +93,7 @@ fn advice_public_failed(reason: impl ToString) -> VerifierError { } #[derive(Clone)] -pub struct TrustedAdviceCyclePhase { +pub struct TrustedAdviceCyclePhase { symbolic: relations::claim_reductions::advice::TrustedCyclePhase, layout: AdviceClaimReductionLayout, /// The RAM address point of the staged advice opening from RAM value-check; @@ -102,7 +102,7 @@ pub struct TrustedAdviceCyclePhase { reference_opening_point: Option>, } -impl TrustedAdviceCyclePhase { +impl TrustedAdviceCyclePhase { pub fn new( layout: &AdviceClaimReductionLayout, reference_opening_point: Option>, @@ -127,7 +127,7 @@ impl TrustedAdviceCyclePhase { } } -impl ConcreteSumcheck for TrustedAdviceCyclePhase { +impl ConcreteSumcheck for TrustedAdviceCyclePhase { type Symbolic = relations::claim_reductions::advice::TrustedCyclePhase; fn symbolic(&self) -> &Self::Symbolic { @@ -182,7 +182,7 @@ impl ConcreteSumcheck for TrustedAdviceCyclePhase { } #[derive(Clone)] -pub struct UntrustedAdviceCyclePhase { +pub struct UntrustedAdviceCyclePhase { symbolic: relations::claim_reductions::advice::UntrustedCyclePhase, layout: AdviceClaimReductionLayout, /// The RAM address point of the staged advice opening from RAM value-check; @@ -191,7 +191,7 @@ pub struct UntrustedAdviceCyclePhase { reference_opening_point: Option>, } -impl UntrustedAdviceCyclePhase { +impl UntrustedAdviceCyclePhase { pub fn new( layout: &AdviceClaimReductionLayout, reference_opening_point: Option>, @@ -216,7 +216,7 @@ impl UntrustedAdviceCyclePhase { } } -impl ConcreteSumcheck for UntrustedAdviceCyclePhase { +impl ConcreteSumcheck for UntrustedAdviceCyclePhase { type Symbolic = relations::claim_reductions::advice::UntrustedCyclePhase; fn symbolic(&self) -> &Self::Symbolic { @@ -270,7 +270,7 @@ impl ConcreteSumcheck for UntrustedAdviceCyclePhase { /// Wire the consumed RAM value-check program-image contribution *value*. /// Clear-only. -pub fn program_image_reduction_cycle_phase_input_values_from_upstream( +pub fn program_image_reduction_cycle_phase_input_values_from_upstream( ram_val_check_init: &RamValCheckInitialEvaluation, ) -> Result, VerifierError> { let (_, value) = ram_val_check_init @@ -285,7 +285,7 @@ pub fn program_image_reduction_cycle_phase_input_values_from_upstream( } #[derive(Clone)] -pub struct ProgramImageReductionCyclePhase { +pub struct ProgramImageReductionCyclePhase { symbolic: relations::claim_reductions::program_image::CyclePhase, layout: ProgramImageClaimReductionLayout, /// The RAM address component of the `RamVal` opening from RAM read-write @@ -294,7 +294,7 @@ pub struct ProgramImageReductionCyclePhase { r_addr_rw: Vec, } -impl ProgramImageReductionCyclePhase { +impl ProgramImageReductionCyclePhase { pub fn new(layout: &ProgramImageClaimReductionLayout, r_addr_rw: Vec) -> Self { Self { symbolic: relations::claim_reductions::program_image::CyclePhase::new( @@ -323,7 +323,7 @@ fn program_image_public_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for ProgramImageReductionCyclePhase { +impl ConcreteSumcheck for ProgramImageReductionCyclePhase { type Symbolic = relations::claim_reductions::program_image::CyclePhase; fn symbolic(&self) -> &Self::Symbolic { @@ -371,14 +371,14 @@ impl ConcreteSumcheck for ProgramImageReductionCyclePhase { } #[derive(Clone)] -pub struct BytecodeReductionCyclePhase { +pub struct BytecodeReductionCyclePhase { symbolic: relations::claim_reductions::bytecode::CyclePhase, layout: BytecodeClaimReductionLayout, weights: BytecodeReductionWeights, chunk_count: usize, } -impl BytecodeReductionCyclePhase { +impl BytecodeReductionCyclePhase { pub fn new( layout: &BytecodeClaimReductionLayout, weights: BytecodeReductionWeights, @@ -413,7 +413,7 @@ impl BytecodeReductionCyclePhase { /// vectors into the public [`BytecodeReductionWeights`] (the per-chunk `r_bc` /// weights and the gamma-folded lane weights) consumed by the bytecode /// claim-reduction cycle and address phases. -pub fn bytecode_reduction_weights( +pub fn bytecode_reduction_weights( layout: &BytecodeClaimReductionLayout, lane_inputs: BytecodeLaneWeightInputs<'_, F>, bytecode_r_address: &[F], @@ -436,7 +436,7 @@ fn bytecode_public_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for BytecodeReductionCyclePhase { +impl ConcreteSumcheck for BytecodeReductionCyclePhase { type Symbolic = relations::claim_reductions::bytecode::CyclePhase; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage6b/inc_claim_reduction.rs b/crates/jolt-verifier/src/stages/stage6b/inc_claim_reduction.rs index e7d37680d2..605f8063a8 100644 --- a/crates/jolt-verifier/src/stages/stage6b/inc_claim_reduction.rs +++ b/crates/jolt-verifier/src/stages/stage6b/inc_claim_reduction.rs @@ -14,7 +14,7 @@ use jolt_claims::protocols::jolt::{ geometry::dimensions::TraceDimensions, IncClaimReductionPublic, JoltDerivedId, JoltRelationId, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::try_eq_mle; use crate::stages::relations::ConcreteSumcheck; @@ -27,7 +27,7 @@ use crate::VerifierError; /// Wire the four reduced `Inc` opening *values* from the read-write / value /// relations of RAM and registers. Clear-only. -pub fn inc_claim_reduction_input_values_from_upstream( +pub fn inc_claim_reduction_input_values_from_upstream( stage2: &Stage2BatchOutputClaims, stage4: &Stage4OutputClaims, stage5: &Stage5OutputClaims, @@ -42,7 +42,7 @@ pub fn inc_claim_reduction_input_values_from_upstream( /// Wire the four reduced `Inc` opening *points* from the read-write / value /// relations of RAM and registers. ZK-agnostic. -pub fn inc_claim_reduction_input_points_from_upstream( +pub fn inc_claim_reduction_input_points_from_upstream( stage2: &Stage2BatchOutputPoints, stage4: &Stage4OutputPoints, stage5: &Stage5OutputPoints, @@ -56,7 +56,7 @@ pub fn inc_claim_reduction_input_points_from_upstream( } #[derive(Clone)] -pub struct IncClaimReduction { +pub struct IncClaimReduction { symbolic: relations::claim_reductions::increments::ClaimReduction, ram_read_write_cycle: Vec, ram_val_check_cycle: Vec, @@ -64,7 +64,7 @@ pub struct IncClaimReduction { registers_val_evaluation_cycle: Vec, } -impl IncClaimReduction { +impl IncClaimReduction { pub fn new( trace_dimensions: TraceDimensions, ram_read_write_cycle: Vec, @@ -102,7 +102,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for IncClaimReduction { +impl ConcreteSumcheck for IncClaimReduction { type Symbolic = relations::claim_reductions::increments::ClaimReduction; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage6b/instruction_ra_virtualization.rs b/crates/jolt-verifier/src/stages/stage6b/instruction_ra_virtualization.rs index 9b3092cc4b..2111abc4f6 100644 --- a/crates/jolt-verifier/src/stages/stage6b/instruction_ra_virtualization.rs +++ b/crates/jolt-verifier/src/stages/stage6b/instruction_ra_virtualization.rs @@ -19,7 +19,7 @@ use jolt_claims::protocols::jolt::{ InstructionRaVirtualizationPublic, JoltDerivedId, JoltRelationId, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::try_eq_mle; use crate::stages::relations::ConcreteSumcheck; @@ -28,7 +28,7 @@ use crate::VerifierError; /// Wire the per-virtual reduced `InstructionRa` opening *values* from the stage-5 /// instruction read-RAF. Clear-only. -pub fn instruction_ra_virtualization_input_values_from_upstream( +pub fn instruction_ra_virtualization_input_values_from_upstream( stage5: &Stage5OutputClaims, ) -> InstructionRaVirtualizationInputClaims { InstructionRaVirtualizationInputClaims { @@ -38,7 +38,7 @@ pub fn instruction_ra_virtualization_input_values_from_upstream( /// Wire the per-virtual reduced `InstructionRa` opening *points* from the stage-5 /// instruction read-RAF. ZK-agnostic. -pub fn instruction_ra_virtualization_input_points_from_upstream( +pub fn instruction_ra_virtualization_input_points_from_upstream( stage5: &Stage5OutputPoints, ) -> InstructionRaVirtualizationInputClaims> { InstructionRaVirtualizationInputClaims { @@ -47,7 +47,7 @@ pub fn instruction_ra_virtualization_input_points_from_upstream( } #[derive(Clone)] -pub struct InstructionRaVirtualization { +pub struct InstructionRaVirtualization { symbolic: relations::instruction::RaVirtualization, dimensions: InstructionRaVirtualizationDimensions, /// The stage-5 instruction address point, chunked into the per-chunk committed @@ -58,7 +58,7 @@ pub struct InstructionRaVirtualization { committed_chunk_bits: usize, } -impl InstructionRaVirtualization { +impl InstructionRaVirtualization { pub fn new( dimensions: InstructionRaVirtualizationDimensions, instruction_address: Vec, @@ -82,7 +82,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl InstructionRaVirtualization { +impl InstructionRaVirtualization { pub fn dimensions(&self) -> InstructionRaVirtualizationDimensions { self.dimensions } @@ -100,7 +100,7 @@ impl InstructionRaVirtualization { } } -impl ConcreteSumcheck for InstructionRaVirtualization { +impl ConcreteSumcheck for InstructionRaVirtualization { type Symbolic = relations::instruction::RaVirtualization; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage6b/outputs.rs b/crates/jolt-verifier/src/stages/stage6b/outputs.rs index 85b1432eae..52484a3610 100644 --- a/crates/jolt-verifier/src/stages/stage6b/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage6b/outputs.rs @@ -2,7 +2,7 @@ //! verification. use jolt_claims::protocols::jolt::geometry::claim_reductions::bytecode::BytecodeOutputWeightInputs; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_sumcheck::BatchedCommittedSumcheckConsistency; use crate::stages::relations::SumcheckBatch; @@ -71,7 +71,7 @@ use super::ram_ra_virtualization::RamRaVirtualization; no_output_shape, crate = "crate" )] -pub struct Stage6bSumchecks { +pub struct Stage6bSumchecks { pub bytecode_read_raf: BytecodeReadRafCycle, pub booleanity: Booleanity, pub ram_hamming_booleanity: RamHammingBooleanity, @@ -96,7 +96,7 @@ pub struct Stage6bSumchecks { /// cells. The per-reduction `cycle_phase_variables` are recovered as /// `reverse(opening_point)` (see `cycle_phase_opening_point` in `jolt-claims` /// `claim_reductions::precommitted`). -impl Stage6bOutputPoints { +impl Stage6bOutputPoints { /// The shared booleanity opening point (`r_address ++ r_cycle`); every /// produced booleanity RA opening uses it. `None` only if booleanity produced /// no openings (never in practice — at least one RA family is always present). @@ -211,7 +211,7 @@ impl Stage6bOutputPoints { } } -impl Stage6bOutputClaims { +impl Stage6bOutputClaims { /// The consumed cycle-phase advice opening *value* for `kind` (the trusted / /// untrusted slot of that advice member), present only when the advice /// reduction ran a cycle phase. Read by stage 7's advice input wiring and stage @@ -230,7 +230,7 @@ impl Stage6bOutputClaims { } } -fn reversed(point: &[F]) -> Vec { +fn reversed(point: &[F]) -> Vec { point.iter().rev().copied().collect() } @@ -238,7 +238,7 @@ fn reversed(point: &[F]) -> Vec { /// instruction-RA and increment gammas, and (committed-program only) the bytecode /// claim-reduction `eta`. Kept as field names greppable from BlindFold. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage6bCarriedChallenges { +pub struct Stage6bCarriedChallenges { pub instruction_ra_gamma: F, #[cfg(not(feature = "akita"))] pub inc_gamma: F, @@ -248,7 +248,7 @@ pub struct Stage6bCarriedChallenges { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage6bClearOutput { +pub struct Stage6bClearOutput { /// The produced opening *values* (wire form); read by later stages and the /// Fiat-Shamir opening-claim encoder. pub output_values: Stage6bOutputClaims, @@ -264,7 +264,7 @@ pub struct Stage6bClearOutput { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage6bZkOutput { +pub struct Stage6bZkOutput { pub challenges: Stage6bCarriedChallenges, pub batch_consistency: BatchedCommittedSumcheckConsistency, pub batch_output_claims: CommittedOutputClaimOutput, @@ -279,12 +279,12 @@ pub struct Stage6bZkOutput { // The clear variant carries the located opening claims read on the hot path; the // ZK variant carries committed consistency plus the point-only `output_points`. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Stage6bOutput { +pub enum Stage6bOutput { Clear(Stage6bClearOutput), Zk(Stage6bZkOutput), } -impl Stage6bOutput { +impl Stage6bOutput { /// The produced opening *points*, available regardless of proving mode. pub fn output_points(&self) -> &Stage6bOutputPoints { match self { @@ -314,13 +314,13 @@ impl Stage6bOutput { /// phases: the per-chunk weights over dropped address bits, the chunk-local /// cycle point, and the gamma-folded lane weights. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct BytecodeReductionWeights { +pub struct BytecodeReductionWeights { pub r_bc: Vec, pub chunk_rbc_weights: Vec, pub lane_weights: Vec, } -impl BytecodeReductionWeights { +impl BytecodeReductionWeights { /// Borrow the weights as the jolt-claims `BytecodeOutputWeightInputs` the /// bytecode reduction's output-weight publics resolve against. pub(crate) fn as_inputs(&self) -> BytecodeOutputWeightInputs<'_, F> { diff --git a/crates/jolt-verifier/src/stages/stage6b/ram_hamming_booleanity.rs b/crates/jolt-verifier/src/stages/stage6b/ram_hamming_booleanity.rs index 59c2456c53..193421b7e5 100644 --- a/crates/jolt-verifier/src/stages/stage6b/ram_hamming_booleanity.rs +++ b/crates/jolt-verifier/src/stages/stage6b/ram_hamming_booleanity.rs @@ -14,14 +14,14 @@ use jolt_claims::protocols::jolt::{ RamHammingBooleanityPublic, }; use jolt_claims::{NoChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::try_eq_mle; use crate::stages::relations::ConcreteSumcheck; use crate::VerifierError; #[derive(Clone)] -pub struct RamHammingBooleanity { +pub struct RamHammingBooleanity { symbolic: relations::ram::HammingBooleanity, trace_dimensions: TraceDimensions, /// The stage-1 Spartan-outer cycle binding that `EqCycle` compares the raw @@ -29,7 +29,7 @@ pub struct RamHammingBooleanity { stage1_cycle_binding: Vec, } -impl RamHammingBooleanity { +impl RamHammingBooleanity { pub fn new(trace_dimensions: TraceDimensions, stage1_cycle_binding: Vec) -> Self { Self { symbolic: relations::ram::HammingBooleanity::new(trace_dimensions), @@ -54,7 +54,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for RamHammingBooleanity { +impl ConcreteSumcheck for RamHammingBooleanity { type Symbolic = relations::ram::HammingBooleanity; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage6b/ram_ra_virtualization.rs b/crates/jolt-verifier/src/stages/stage6b/ram_ra_virtualization.rs index d19ebd0e4b..e47052c257 100644 --- a/crates/jolt-verifier/src/stages/stage6b/ram_ra_virtualization.rs +++ b/crates/jolt-verifier/src/stages/stage6b/ram_ra_virtualization.rs @@ -16,7 +16,7 @@ use jolt_claims::protocols::jolt::{ JoltDerivedId, JoltRelationId, RamRaVirtualizationPublic, }; use jolt_claims::{NoChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::try_eq_mle; use crate::stages::relations::ConcreteSumcheck; @@ -25,7 +25,7 @@ use crate::VerifierError; /// Wire the single reduced `RamRa` opening *value* from the stage-5 RAM RA claim /// reduction. Clear-only (the values aggregate exists only in clear mode). -pub fn ram_ra_virtualization_input_values_from_upstream( +pub fn ram_ra_virtualization_input_values_from_upstream( stage5: &Stage5OutputClaims, ) -> RamRaVirtualizationInputClaims { RamRaVirtualizationInputClaims { @@ -35,7 +35,7 @@ pub fn ram_ra_virtualization_input_values_from_upstream( /// Wire the single reduced `RamRa` opening *point* from the stage-5 RAM RA claim /// reduction. ZK-agnostic: both proving modes expose the stage-5 output points. -pub fn ram_ra_virtualization_input_points_from_upstream( +pub fn ram_ra_virtualization_input_points_from_upstream( stage5: &Stage5OutputPoints, ) -> RamRaVirtualizationInputClaims> { RamRaVirtualizationInputClaims { @@ -44,7 +44,7 @@ pub fn ram_ra_virtualization_input_points_from_upstream( } #[derive(Clone)] -pub struct RamRaVirtualization { +pub struct RamRaVirtualization { symbolic: relations::ram::RaVirtualization, dimensions: RamRaVirtualizationDimensions, /// The stage-5 reduced address prefix, chunked into the per-chunk committed @@ -56,7 +56,7 @@ pub struct RamRaVirtualization { committed_chunk_bits: usize, } -impl RamRaVirtualization { +impl RamRaVirtualization { pub fn new( dimensions: RamRaVirtualizationDimensions, ram_reduced_address: Vec, @@ -96,7 +96,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for RamRaVirtualization { +impl ConcreteSumcheck for RamRaVirtualization { type Symbolic = relations::ram::RaVirtualization; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage6b/verify.rs b/crates/jolt-verifier/src/stages/stage6b/verify.rs index cd74de8bd9..5f680909af 100644 --- a/crates/jolt-verifier/src/stages/stage6b/verify.rs +++ b/crates/jolt-verifier/src/stages/stage6b/verify.rs @@ -5,7 +5,7 @@ use jolt_claims::protocols::jolt::{ }; use jolt_claims::OutputClaims; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::CommitmentScheme; use jolt_transcript::Transcript; @@ -243,7 +243,7 @@ where /// intermediate-vs-chunks shape. Member presence is enforced separately by the /// hand-listed `validate_member_presence` calls; a missing advice inner opening is caught by /// `expected_final_claim` (the advice cycle phase's `expected_output`). -fn validate_cycle_phase_claim_shape( +fn validate_cycle_phase_claim_shape( formula_dimensions: &JoltFormulaDimensions, claims: &Stage6bOutputClaims, bytecode_reduction_layout: Option<&BytecodeClaimReductionLayout>, @@ -321,7 +321,7 @@ fn validate_cycle_phase_claim_shape( /// aggregate. The `Option` cells track member presence, so a present member always /// has its input cell populated. Public because the prover's stage-6b recipe /// builds its batch inputs through the same wiring. -pub fn stage6b_input_values_from_upstream( +pub fn stage6b_input_values_from_upstream( sumchecks: &Stage6bSumchecks, address_claims: &Stage6aOutputClaims, #[cfg_attr(feature = "akita", expect(unused_variables))] stage2: &Stage2BatchOutputClaims, @@ -383,7 +383,7 @@ pub fn stage6b_input_values_from_upstream( /// and read no input point, so their cells come from the generated /// `empty_input_points` (empty, and present for present `Option` members exactly as /// the generated `derive_opening_points` requires). -pub fn stage6b_input_points_from_upstream( +pub fn stage6b_input_points_from_upstream( sumchecks: &Stage6bSumchecks, #[cfg_attr(feature = "akita", expect(unused_variables))] stage2: &Stage2BatchOutputPoints, #[cfg_attr(feature = "akita", expect(unused_variables))] stage4: &Stage4OutputPoints, @@ -407,7 +407,7 @@ pub fn stage6b_input_points_from_upstream( /// against the bytecode-read-RAF points (a runtime point-equality the output /// `Expr`s cannot express). Public because the prover's recorder absorbs the /// same curated sequence. -pub fn stage6b_opening_values( +pub fn stage6b_opening_values( claims: &Stage6bOutputClaims, bytecode_read_raf_points: &[Vec], booleanity_point: &[F], @@ -457,7 +457,7 @@ fn append_opening_claims( bytecode_read_raf_points: &[Vec], booleanity_point: &[F], ) where - F: Field, + F: JoltField, T: Transcript, { // Full relations and the optional members delegate to their derived @@ -526,7 +526,7 @@ mod tests { use super::super::ram_ra_virtualization::RamRaVirtualizationOutputClaims; use super::*; use crate::stages::relations::append_recording::RecordingTranscript; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { Fr::from_u64(value) diff --git a/crates/jolt-verifier/src/stages/stage7/advice_address_phase.rs b/crates/jolt-verifier/src/stages/stage7/advice_address_phase.rs index 6e70f524c1..f90fe065e5 100644 --- a/crates/jolt-verifier/src/stages/stage7/advice_address_phase.rs +++ b/crates/jolt-verifier/src/stages/stage7/advice_address_phase.rs @@ -23,7 +23,7 @@ use jolt_claims::protocols::jolt::{ JoltRelationId, PrecommittedReductionLayout, }; use jolt_claims::{NoChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use crate::stages::relations::ConcreteSumcheck; use crate::stages::stage6b::outputs::Stage6bOutputClaims; @@ -32,7 +32,7 @@ use crate::VerifierError; /// The consumed cycle-phase trusted-advice opening *value*, read off the stage-6b /// cycle-phase output. Errors if the cycle phase produced no trusted-advice opening /// (the address phase runs only when it did). -pub fn trusted_advice_input_values_from_upstream( +pub fn trusted_advice_input_values_from_upstream( cycle_phase: &Stage6bOutputClaims, ) -> Result, VerifierError> { let trusted = cycle_phase @@ -44,7 +44,7 @@ pub fn trusted_advice_input_values_from_upstream( } /// The consumed cycle-phase untrusted-advice opening *value*. -pub fn untrusted_advice_input_values_from_upstream( +pub fn untrusted_advice_input_values_from_upstream( cycle_phase: &Stage6bOutputClaims, ) -> Result, VerifierError> { let untrusted = cycle_phase @@ -63,7 +63,7 @@ fn advice_public_failed(reason: impl ToString) -> VerifierError { } #[derive(Clone)] -pub struct TrustedAdviceAddressPhase { +pub struct TrustedAdviceAddressPhase { symbolic: relations::claim_reductions::advice::TrustedAddressPhase, layout: AdviceClaimReductionLayout, cycle_phase_variables: Vec, @@ -74,7 +74,7 @@ pub struct TrustedAdviceAddressPhase { reference_opening_point: Option>, } -impl TrustedAdviceAddressPhase { +impl TrustedAdviceAddressPhase { /// `reference_opening_point` is the RAM address point of the staged advice /// opening from RAM value-check (stage 4), `None` in ZK (clear-only aux). It and /// the cycle-phase variables are known before the stage-7 sumcheck. @@ -94,7 +94,7 @@ impl TrustedAdviceAddressPhase { } } -impl ConcreteSumcheck for TrustedAdviceAddressPhase { +impl ConcreteSumcheck for TrustedAdviceAddressPhase { type Symbolic = relations::claim_reductions::advice::TrustedAddressPhase; fn symbolic(&self) -> &Self::Symbolic { @@ -147,7 +147,7 @@ impl ConcreteSumcheck for TrustedAdviceAddressPhase { } #[derive(Clone)] -pub struct UntrustedAdviceAddressPhase { +pub struct UntrustedAdviceAddressPhase { symbolic: relations::claim_reductions::advice::UntrustedAddressPhase, layout: AdviceClaimReductionLayout, cycle_phase_variables: Vec, @@ -158,7 +158,7 @@ pub struct UntrustedAdviceAddressPhase { reference_opening_point: Option>, } -impl UntrustedAdviceAddressPhase { +impl UntrustedAdviceAddressPhase { pub fn new( layout: &AdviceClaimReductionLayout, reference_opening_point: Option>, @@ -175,7 +175,7 @@ impl UntrustedAdviceAddressPhase { } } -impl ConcreteSumcheck for UntrustedAdviceAddressPhase { +impl ConcreteSumcheck for UntrustedAdviceAddressPhase { type Symbolic = relations::claim_reductions::advice::UntrustedAddressPhase; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage7/committed_reduction_address_phase.rs b/crates/jolt-verifier/src/stages/stage7/committed_reduction_address_phase.rs index 6ca360b8a6..99aaa07a85 100644 --- a/crates/jolt-verifier/src/stages/stage7/committed_reduction_address_phase.rs +++ b/crates/jolt-verifier/src/stages/stage7/committed_reduction_address_phase.rs @@ -26,14 +26,14 @@ use jolt_claims::protocols::jolt::{ ProgramImageClaimReductionLayout, ProgramImageClaimReductionPublic, }; use jolt_claims::{NoChallenges, SymbolicSumcheck}; -use jolt_field::Field; +use jolt_field::JoltField; use crate::stages::relations::ConcreteSumcheck; use crate::stages::stage6b::outputs::BytecodeReductionWeights; use crate::VerifierError; #[derive(Clone)] -pub struct BytecodeReductionAddressPhase { +pub struct BytecodeReductionAddressPhase { symbolic: relations::claim_reductions::bytecode::AddressPhase, layout: BytecodeClaimReductionLayout, cycle_phase_variables: Vec, @@ -43,7 +43,7 @@ pub struct BytecodeReductionAddressPhase { weights: Option>, } -impl BytecodeReductionAddressPhase { +impl BytecodeReductionAddressPhase { /// `weights` are the stage-6b bytecode cycle-phase outputs (`None` in ZK, /// clear-only aux); `cycle_phase_variables` and the layout are known before the /// stage-7 sumcheck, so a single construction serves both the input claim and @@ -84,7 +84,7 @@ fn bytecode_public_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for BytecodeReductionAddressPhase { +impl ConcreteSumcheck for BytecodeReductionAddressPhase { type Symbolic = relations::claim_reductions::bytecode::AddressPhase; fn symbolic(&self) -> &Self::Symbolic { @@ -146,7 +146,7 @@ impl ConcreteSumcheck for BytecodeReductionAddressPhase { } #[derive(Clone)] -pub struct ProgramImageReductionAddressPhase { +pub struct ProgramImageReductionAddressPhase { symbolic: relations::claim_reductions::program_image::AddressPhase, layout: ProgramImageClaimReductionLayout, cycle_phase_variables: Vec, @@ -157,7 +157,7 @@ pub struct ProgramImageReductionAddressPhase { reference_opening_point: Option>, } -impl ProgramImageReductionAddressPhase { +impl ProgramImageReductionAddressPhase { /// `reference_opening_point` is the RAM address point of the staged /// `ProgramImageInitContributionRw` opening (from stage 4), `None` in ZK /// (clear-only aux). It and the cycle-phase variables are known before the @@ -185,7 +185,7 @@ fn program_image_public_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for ProgramImageReductionAddressPhase { +impl ConcreteSumcheck for ProgramImageReductionAddressPhase { type Symbolic = relations::claim_reductions::program_image::AddressPhase; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage7/hamming_weight_claim_reduction.rs b/crates/jolt-verifier/src/stages/stage7/hamming_weight_claim_reduction.rs index 8f881e186b..b1b02e8d71 100644 --- a/crates/jolt-verifier/src/stages/stage7/hamming_weight_claim_reduction.rs +++ b/crates/jolt-verifier/src/stages/stage7/hamming_weight_claim_reduction.rs @@ -25,7 +25,7 @@ use jolt_claims::protocols::jolt::{ HammingWeightClaimReductionPublic, JoltDerivedId, JoltRelationId, }; use jolt_claims::SymbolicSumcheck; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_poly::try_eq_mle; #[cfg(feature = "akita")] pub use lattice_hamming::{ @@ -40,7 +40,7 @@ use crate::VerifierError; /// The hamming reduction's consumed opening *values*, wired from the stage-6b /// cycle-phase output claims. The relation reads only their values (its produced /// points are derived from its own sumcheck point), so no input points are needed. -pub fn hamming_weight_input_values_from_upstream( +pub fn hamming_weight_input_values_from_upstream( cycle_phase: &Stage6bOutputClaims, ) -> HammingWeightClaimReductionInputClaims { HammingWeightClaimReductionInputClaims { @@ -67,7 +67,7 @@ pub fn hamming_weight_input_values_from_upstream( /// `EqVirtualization` publics compare against, in canonical (instruction, bytecode, /// RAM) order: the leading `log_k_chunk` coordinates of each stage-6b RA /// virtualization opening point. -pub fn stage7_hamming_virtualization_address_points( +pub fn stage7_hamming_virtualization_address_points( dimensions: HammingDimensions, stage6_points: &Stage6bOutputPoints, ) -> Result>, VerifierError> { @@ -104,7 +104,7 @@ pub fn stage7_hamming_virtualization_address_points( } #[derive(Clone)] -pub struct HammingWeightClaimReduction { +pub struct HammingWeightClaimReduction { symbolic: HammingSymbolic, dimensions: HammingDimensions, /// The shared cycle suffix appended to every produced opening point (the @@ -117,7 +117,7 @@ pub struct HammingWeightClaimReduction { virtualization_points: Vec>, } -impl HammingWeightClaimReduction { +impl HammingWeightClaimReduction { pub fn new( dimensions: HammingDimensions, r_cycle: Vec, @@ -185,7 +185,7 @@ fn public_input_failed(reason: impl ToString) -> VerifierError { } } -impl ConcreteSumcheck for HammingWeightClaimReduction { +impl ConcreteSumcheck for HammingWeightClaimReduction { type Symbolic = HammingSymbolic; fn symbolic(&self) -> &Self::Symbolic { diff --git a/crates/jolt-verifier/src/stages/stage7/outputs.rs b/crates/jolt-verifier/src/stages/stage7/outputs.rs index 96ee2651d0..9b2bfeca27 100644 --- a/crates/jolt-verifier/src/stages/stage7/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage7/outputs.rs @@ -1,7 +1,7 @@ //! Typed inputs consumed and outputs produced by stage 7 verification. use jolt_claims::protocols::jolt::JoltAdviceKind; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_sumcheck::BatchedCommittedSumcheckConsistency; use crate::stages::relations::SumcheckBatch; @@ -33,7 +33,7 @@ use super::hamming_weight_claim_reduction::HammingWeightClaimReduction; /// relation type whose produced claims carry a single non-`Option` slot. #[derive(SumcheckBatch)] #[sumcheck_batch(crate = "crate")] -pub struct Stage7Sumchecks { +pub struct Stage7Sumchecks { pub hamming_weight_claim_reduction: HammingWeightClaimReduction, /// Final `TrustedAdvice` claim from the trusted advice reduction's address /// phase; present only when that phase runs. On the prove side the kernel @@ -53,7 +53,7 @@ pub struct Stage7Sumchecks { /// The shared opening-point accessors over the point-only stage-7 aggregate. /// Stages 7/8 read each produced opening's point off these cells. -impl Stage7OutputPoints { +impl Stage7OutputPoints { /// The hamming-weight reduction's shared opening point (the own point of the /// one-hot `Ra` polynomials): the first non-empty per-family RA cell. `None` /// only if the reduction produced no openings (never in practice — at least one @@ -94,7 +94,7 @@ impl Stage7OutputPoints { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage7ClearOutput { +pub struct Stage7ClearOutput { /// The produced stage-7 opening *values* (wire form); read by later stages and /// the Fiat-Shamir opening-claim encoder. pub output_values: Stage7OutputClaims, @@ -116,7 +116,7 @@ pub struct Stage7ClearOutput { /// `challenges.hamming_weight_claim_reduction.gamma`, matching the /// `input.stageN.challenges..` idiom used by stages 3–5. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Stage7ZkOutput { +pub struct Stage7ZkOutput { pub challenges: Stage7Challenges, pub batch_consistency: BatchedCommittedSumcheckConsistency, pub batch_output_claims: CommittedOutputClaimOutput, @@ -128,12 +128,12 @@ pub struct Stage7ZkOutput { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Stage7Output { +pub enum Stage7Output { Clear(Stage7ClearOutput), Zk(Stage7ZkOutput), } -impl Stage7Output { +impl Stage7Output { pub fn clear(&self) -> Result<&Stage7ClearOutput, crate::VerifierError> { match self { Self::Clear(output) => Ok(output), @@ -166,7 +166,7 @@ mod tests { #[cfg(not(feature = "akita"))] use jolt_claims::protocols::jolt::relations::claim_reductions::hamming_weight::HammingWeightClaimReductionOutputClaims; use jolt_claims::protocols::jolt::relations::claim_reductions::program_image::ProgramImageReductionAddressPhaseOutputClaims; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { Fr::from_u64(value) diff --git a/crates/jolt-verifier/src/stages/stage7/verify.rs b/crates/jolt-verifier/src/stages/stage7/verify.rs index 3044dfe177..4c1f1e447d 100644 --- a/crates/jolt-verifier/src/stages/stage7/verify.rs +++ b/crates/jolt-verifier/src/stages/stage7/verify.rs @@ -10,7 +10,7 @@ use jolt_claims::protocols::jolt::{ JoltAdviceKind, JoltOpeningId, JoltRelationId, PrecommittedReductionLayout, }; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::CommitmentScheme; use jolt_transcript::Transcript; @@ -166,7 +166,7 @@ where /// `derive_output_term` never runs). An address phase is present exactly when its /// precommitted layout is committed and its dimensions carry active address rounds /// — the presence flag the input / challenge aggregates track in lockstep. -pub fn build_stage7_sumchecks( +pub fn build_stage7_sumchecks( hamming_dimensions: HammingDimensions, schedule: &PrecommittedSchedule, stage6_points: &Stage6bOutputPoints, @@ -267,7 +267,7 @@ pub fn build_stage7_sumchecks( /// with active address rounds first (an absent layout yields `Ok(None)`, matching /// the member's presence flag), then lift missing stage-6b cycle-phase variables /// to `MissingOpeningClaim` before building the instance. -fn address_phase_member( +fn address_phase_member( layout: Option<&L>, cycle_phase_variables: Option>, missing_cycle_opening: JoltOpeningId, @@ -289,7 +289,7 @@ fn address_phase_member( /// phase runs (tracking each `Stage7Sumchecks` member's presence), so a present /// member always has its input cell populated. Public because the prover's /// stage-7 recipe builds its batch inputs through the same wiring. -pub fn stage7_input_values_from_upstream( +pub fn stage7_input_values_from_upstream( sumchecks: &Stage7Sumchecks, stage6: &Stage6bClearOutput, ) -> Result, VerifierError> { diff --git a/crates/jolt-verifier/src/stages/stage8/outputs.rs b/crates/jolt-verifier/src/stages/stage8/outputs.rs index ab519d6287..63f34af851 100644 --- a/crates/jolt-verifier/src/stages/stage8/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage8/outputs.rs @@ -1,12 +1,12 @@ use jolt_claims::protocols::jolt::JoltOpeningId; -use jolt_field::Field; +use jolt_field::JoltField; #[cfg(not(feature = "akita"))] use jolt_openings::VerifierOpeningClaim; use jolt_poly::{Point, HIGH_TO_LOW}; #[cfg(not(feature = "akita"))] #[derive(Clone, Debug)] -pub struct Stage8ClearOutput { +pub struct Stage8ClearOutput { pub opening_claims: Vec>, pub opening_ids: Vec, pub constraint_coefficients: Vec, @@ -16,7 +16,7 @@ pub struct Stage8ClearOutput { } #[derive(Clone, Debug)] -pub struct Stage8ZkOutput { +pub struct Stage8ZkOutput { pub opening_ids: Vec, pub constraint_coefficients: Vec, pub pcs_opening_point: Point, @@ -25,7 +25,7 @@ pub struct Stage8ZkOutput { } #[derive(Clone, Debug)] -pub enum Stage8Output { +pub enum Stage8Output { #[cfg(not(feature = "akita"))] Clear(Stage8ClearOutput), /// The akita build's clear stage 8 verifies to completion inside @@ -36,7 +36,7 @@ pub enum Stage8Output { Zk(Stage8ZkOutput), } -impl Stage8Output { +impl Stage8Output { pub fn zk(&self) -> Result<&Stage8ZkOutput, crate::VerifierError> { match self { Self::Zk(output) => Ok(output), diff --git a/crates/jolt-verifier/src/stages/stage8/packed.rs b/crates/jolt-verifier/src/stages/stage8/packed.rs index e45eb56356..b992c45f88 100644 --- a/crates/jolt-verifier/src/stages/stage8/packed.rs +++ b/crates/jolt-verifier/src/stages/stage8/packed.rs @@ -18,7 +18,7 @@ use jolt_claims::protocols::jolt::lattice::strategy::{ use jolt_claims::protocols::jolt::{ JoltAdviceKind, JoltCommittedPolynomial, JoltOneHotConfig, JoltOpeningId, JoltPolynomialId, }; -use jolt_field::{CanonicalBytes, Field}; +use jolt_field::{CanonicalBytes, JoltField}; use jolt_openings::{ verify_packed_openings, CommitmentScheme, EvaluationClaim, PackedObjectGroup, PackedVerifierObject, PrefixPackedStatement, PrefixPacking, @@ -348,7 +348,7 @@ fn object_statement( leaves: &BTreeMap>, ) -> Result, VerifierError> where - F: Field, + F: JoltField, { let claims = packing .iter() @@ -371,16 +371,16 @@ where /// reconstruction outputs and keyed by committed polynomial. Coverage against /// the packings is machine-checked downstream by `prepare_statement` /// (one-claim-per-slot, no gaps, per-slot point arity). -fn leaf_claims( +fn leaf_claims( stage7: &Stage7ClearOutput, reconstruction: &ReconstructionClearOutput, ) -> BTreeMap> { use JoltCommittedPolynomial as Poly; - fn leaf(value: F, point: &[F]) -> EvaluationClaim { + fn leaf(value: F, point: &[F]) -> EvaluationClaim { EvaluationClaim::new(Point::high_to_low(point.to_vec()), value) } - fn insert( + fn insert( leaves: &mut BTreeMap>, polynomial: JoltCommittedPolynomial, claim: EvaluationClaim, @@ -388,7 +388,7 @@ fn leaf_claims( // Keys are distinct by construction, so no entry is ever displaced. let _previous = BTreeMap::insert(leaves, polynomial, claim); } - fn insert_indexed( + fn insert_indexed( leaves: &mut BTreeMap>, values: &[F], points: &[Vec], @@ -512,7 +512,7 @@ mod tests { use jolt_claims::protocols::jolt::lattice::relations::bytecode_reconstruction::BytecodeChunkReconstructionOutputClaims; use jolt_claims::protocols::jolt::lattice::relations::program_image_reconstruction::ProgramImageReconstructionOutputClaims; use jolt_claims::protocols::jolt::BytecodeRegisterLane; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_poly::math::Math; use jolt_riscv::{NUM_CIRCUIT_FLAGS, NUM_INSTRUCTION_FLAGS}; diff --git a/crates/jolt-verifier/src/stages/stage8/precommitted.rs b/crates/jolt-verifier/src/stages/stage8/precommitted.rs index e3ae7d0f2f..7047d1f85c 100644 --- a/crates/jolt-verifier/src/stages/stage8/precommitted.rs +++ b/crates/jolt-verifier/src/stages/stage8/precommitted.rs @@ -16,7 +16,7 @@ use jolt_claims::protocols::jolt::{ JoltCommittedPolynomial, JoltRelationId, PrecommittedReductionLayout, ProgramImageClaimReductionLayout, }; -use jolt_field::Field; +use jolt_field::JoltField; use crate::stages::stage6b::outputs::{Stage6bOutputClaims, Stage6bOutputPoints}; use crate::stages::stage7::outputs::{Stage7OutputClaims, Stage7OutputPoints}; @@ -28,7 +28,7 @@ use crate::VerifierError; /// phase). Stage 8 consumes these as anchors and batch members of the final /// PCS opening. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct PrecommittedFinalOpening { +pub struct PrecommittedFinalOpening { pub polynomial: JoltCommittedPolynomial, pub point: Vec, /// `None` in ZK mode, where opening claims stay committed. @@ -66,7 +66,7 @@ impl<'a, F, T> PrecommittedFinalSource<'a, F, T> { /// opening claim stays committed (`None`) and only points are read; in clear mode a /// source requires both its point and its value. The walk order — trusted advice, /// untrusted advice, bytecode chunks, program image — fixes stage 8's anchor order. -pub fn precommitted_final_openings( +pub fn precommitted_final_openings( schedule: &PrecommittedSchedule, stage7_points: &Stage7OutputPoints, stage6_points: &Stage6bOutputPoints, @@ -159,7 +159,7 @@ pub fn precommitted_final_openings( /// In clear mode both the point and the value must be present (the `zip` semantics /// the twin clear/zk drivers had); in ZK only the point is read and the claim stays /// committed (`None`). -fn resolve_source( +fn resolve_source( is_clear: bool, point: Option<&[F]>, value: Option, @@ -175,7 +175,7 @@ fn resolve_source( /// The stage-7 advice address-phase output *value* for `kind` (only that kind's /// slot is filled on the wire). -fn advice_address_value( +fn advice_address_value( claims: &Stage7OutputClaims, kind: JoltAdviceKind, ) -> Option { @@ -191,7 +191,7 @@ fn advice_address_value( /// Resolves the final opening of an advice polynomial from whichever phase /// completed its reduction: this stage's address phase, or the stage 6b cycle /// phase when no active address rounds remain. -fn advice_final_opening( +fn advice_final_opening( kind: JoltAdviceKind, layout: &AdviceClaimReductionLayout, address_phase: Option>, @@ -219,7 +219,7 @@ fn advice_final_opening( /// Resolves the final per-chunk openings of the committed bytecode from whichever /// phase completed the reduction: this stage's address phase, or the stage 6b /// cycle phase when no active address rounds remain. -fn bytecode_final_openings( +fn bytecode_final_openings( layout: &BytecodeClaimReductionLayout, address_phase: Option>>, cycle_phase: Option>>, @@ -259,7 +259,7 @@ fn bytecode_final_openings( /// Resolves the final opening of the committed program image from whichever phase /// completed the reduction: this stage's address phase, or the stage 6b cycle /// phase when no active address rounds remain. -fn program_image_final_opening( +fn program_image_final_opening( layout: &ProgramImageClaimReductionLayout, address_phase: Option>, cycle_phase: Option>, diff --git a/crates/jolt-verifier/src/stages/stage8/reconstruction.rs b/crates/jolt-verifier/src/stages/stage8/reconstruction.rs index 575a7a7d70..82d1b8f34a 100644 --- a/crates/jolt-verifier/src/stages/stage8/reconstruction.rs +++ b/crates/jolt-verifier/src/stages/stage8/reconstruction.rs @@ -38,7 +38,7 @@ use jolt_claims::protocols::jolt::{ UntrustedAdviceReconstructionPublic, }; use jolt_claims::{NoChallenges, SymbolicSumcheck}; -use jolt_field::{CanonicalBytes, Field}; +use jolt_field::{CanonicalBytes, JoltField}; use jolt_poly::math::Math; use jolt_poly::{eq_index_msb, try_eq_mle}; use jolt_sumcheck::SumcheckProof; @@ -76,7 +76,7 @@ fn image_public_failed(reason: impl ToString) -> VerifierError { /// The single-leg decode publics shared by the trusted-advice and /// program-image instances: [`byte_decode_weight`] at the bound /// `(byte ‖ place)` prefix of the produced opening point. -fn byte_decode_leg( +fn byte_decode_leg( opening_point: &[F], bound: usize, fail: fn(&'static str) -> VerifierError, @@ -91,12 +91,12 @@ fn byte_decode_leg( /// The untrusted advice reconstruction: booleanity + hamming + decode legs /// over the full `(byte ‖ place ‖ word)` cell domain. #[derive(Clone)] -pub struct UntrustedAdviceReconstructionInstance { +pub struct UntrustedAdviceReconstructionInstance { symbolic: UntrustedSymbolic, _field: core::marker::PhantomData, } -impl ConcreteSumcheck for UntrustedAdviceReconstructionInstance { +impl ConcreteSumcheck for UntrustedAdviceReconstructionInstance { type Symbolic = UntrustedSymbolic; fn symbolic(&self) -> &Self::Symbolic { @@ -175,12 +175,12 @@ impl ConcreteSumcheck for UntrustedAdviceReconstructionInstance /// The trusted advice reconstruction: the decode leg alone over the /// `(byte ‖ place)` variables, the word point fixed by the incoming claim. #[derive(Clone)] -pub struct TrustedAdviceReconstructionInstance { +pub struct TrustedAdviceReconstructionInstance { symbolic: TrustedSymbolic, _field: core::marker::PhantomData, } -impl ConcreteSumcheck for TrustedAdviceReconstructionInstance { +impl ConcreteSumcheck for TrustedAdviceReconstructionInstance { type Symbolic = TrustedSymbolic; fn symbolic(&self) -> &Self::Symbolic { @@ -231,7 +231,7 @@ impl ConcreteSumcheck for TrustedAdviceReconstructionInstance { /// `Π_missing (1 − v_i) · column(v_own ‖ r_row)`, with the `Π` folded into /// the derived and the claim landing at the column's own packed-slot point. #[derive(Clone)] -pub struct BytecodeChunkReconstructionInstance { +pub struct BytecodeChunkReconstructionInstance { symbolic: BytecodeSymbolic, dimensions: BytecodeReconstructionDimensions, /// The lane half of the completed chunk claims' shared point. @@ -240,7 +240,7 @@ pub struct BytecodeChunkReconstructionInstance { r_row: Vec, } -impl BytecodeChunkReconstructionInstance { +impl BytecodeChunkReconstructionInstance { fn own_vars(&self) -> BytecodeLegVars { BytecodeLegVars { total: SymbolicSumcheck::rounds(&self.symbolic), @@ -268,12 +268,12 @@ impl BytecodeLegVars { /// The zero-pin factor of a leg's missing high coordinates: /// `eq(v_missing, 0) = Π (1 − v_i)`. - fn zero_pin(&self, bound: &[F], own: usize) -> F { + fn zero_pin(&self, bound: &[F], own: usize) -> F { eq_index_msb(&bound[..self.total - own], 0) } } -impl ConcreteSumcheck for BytecodeChunkReconstructionInstance { +impl ConcreteSumcheck for BytecodeChunkReconstructionInstance { type Symbolic = BytecodeSymbolic; fn symbolic(&self) -> &Self::Symbolic { @@ -375,12 +375,12 @@ impl ConcreteSumcheck for BytecodeChunkReconstructionInstance { /// The program-image reconstruction: the trusted-advice decode shape over the /// program image byte column. #[derive(Clone)] -pub struct ProgramImageReconstructionInstance { +pub struct ProgramImageReconstructionInstance { symbolic: ProgramImageSymbolic, _field: core::marker::PhantomData, } -impl ConcreteSumcheck for ProgramImageReconstructionInstance { +impl ConcreteSumcheck for ProgramImageReconstructionInstance { type Symbolic = ProgramImageSymbolic; fn symbolic(&self) -> &Self::Symbolic { @@ -422,19 +422,19 @@ impl ConcreteSumcheck for ProgramImageReconstructionInstance { /// Each is present exactly when its object exists in the public shape. #[derive(SumcheckBatch)] #[sumcheck_batch(crate = "crate")] -pub struct ReconstructionSumchecks { +pub struct ReconstructionSumchecks { pub untrusted_advice: Option>, pub trusted_advice: Option>, pub bytecode: Option>, pub program_image: Option>, } -pub struct ReconstructionClearOutput { +pub struct ReconstructionClearOutput { pub output_values: ReconstructionOutputClaims, pub output_points: ReconstructionOutputPoints, } -impl ReconstructionClearOutput { +impl ReconstructionClearOutput { fn empty() -> Self { Self { output_values: ReconstructionOutputClaims { @@ -464,7 +464,7 @@ struct CompletedClaim { /// The address-phase-else-cycle-terminus fallback shared by every completed /// claim: take the stage-7 pair when the address phase ran, else the stage-6b /// pair, else fail with `error`. -fn completed( +fn completed( address_phase: Option<(V, &[F])>, cycle_phase: Option<(V, &[F])>, error: impl FnOnce() -> VerifierError, @@ -475,7 +475,7 @@ fn completed( .ok_or_else(error) } -fn completed_advice_claim( +fn completed_advice_claim( kind: JoltAdviceKind, stage6b: &Stage6bClearOutput, stage7: &Stage7ClearOutput, @@ -510,7 +510,7 @@ fn completed_advice_claim( Ok(CompletedClaim { value, point }) } -fn completed_chunk_claims( +fn completed_chunk_claims( stage6b: &Stage6bClearOutput, stage7: &Stage7ClearOutput, ) -> Result<(Vec, Vec), VerifierError> { @@ -536,7 +536,7 @@ fn completed_chunk_claims( ) } -fn completed_program_image_claim( +fn completed_program_image_claim( stage6b: &Stage6bClearOutput, stage7: &Stage7ClearOutput, ) -> Result, VerifierError> { @@ -567,7 +567,7 @@ pub fn verify( stage7: &Stage7ClearOutput, ) -> Result, VerifierError> where - F: Field, + F: JoltField, C: Clone + jolt_transcript::AppendToTranscript, T: Transcript, { diff --git a/crates/jolt-verifier/src/stages/stage8/verify.rs b/crates/jolt-verifier/src/stages/stage8/verify.rs index 3c09b40ff9..2fe945427f 100644 --- a/crates/jolt-verifier/src/stages/stage8/verify.rs +++ b/crates/jolt-verifier/src/stages/stage8/verify.rs @@ -31,7 +31,7 @@ use jolt_claims::protocols::jolt::{ #[cfg(not(feature = "akita"))] use jolt_crypto::HomomorphicCommitment; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::CommitmentScheme; #[cfg(not(feature = "akita"))] use jolt_openings::{ @@ -48,7 +48,7 @@ use jolt_transcript::{AppendToTranscript, Transcript}; /// One assembled final-opening batch entry. Public because the prover's /// stage-8 recipe assembles its PCS batch statement through the same /// [`batch_entries`] wiring. -pub struct Stage8BatchEntry<'a, F: Field, C> { +pub struct Stage8BatchEntry<'a, F: JoltField, C> { pub id: JoltOpeningId, pub commitment: &'a C, /// `None` in ZK mode, where opening claims stay committed. @@ -74,7 +74,7 @@ pub fn verify( stage7: &Stage7Output, ) -> Result, VerifierError> where - F: Field, + F: JoltField, PCS: CommitmentScheme + AdditivelyHomomorphic + ZkOpeningScheme, @@ -272,7 +272,7 @@ pub fn batch_entries<'a, F, PCS, VC>( clear_claims: Option<(&Stage6bOutputClaims, &Stage7OutputClaims)>, ) -> Result>, VerifierError> where - F: Field, + F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, { @@ -447,7 +447,7 @@ pub fn verify( stage7: &Stage7Output, ) -> Result, VerifierError> where - F: Field, + F: JoltField, PCS: CommitmentScheme, PCS::Output: Clone + AppendToTranscript + super::OneHotTraceCommitmentMetadata, PCS::VerifierSetup: super::OneHotTraceSetupMetadata, diff --git a/crates/jolt-verifier/src/stages/uniskip.rs b/crates/jolt-verifier/src/stages/uniskip.rs index 311670a17e..d227b3bcbc 100644 --- a/crates/jolt-verifier/src/stages/uniskip.rs +++ b/crates/jolt-verifier/src/stages/uniskip.rs @@ -10,7 +10,7 @@ //! shared here. use jolt_claims::protocols::jolt::JoltRelationId; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_r1cs::constraints::jolt::{ SPARTAN_OUTER_UNISKIP_DOMAIN_SIZE, SPARTAN_OUTER_UNISKIP_FIRST_ROUND_DEGREE, SPARTAN_PRODUCT_UNISKIP_DOMAIN_SIZE, SPARTAN_PRODUCT_UNISKIP_FIRST_ROUND_DEGREE, @@ -92,7 +92,7 @@ impl UniskipParams { /// single-sourced. pub fn draw_spartan_outer_tau(transcript: &mut T, log_t: usize) -> Vec where - F: Field, + F: JoltField, T: Transcript, { transcript.challenge_vector(log_t + 2) @@ -105,7 +105,7 @@ where /// without changing the transcript bytes. pub fn draw_spartan_product_tau_high(transcript: &mut T) -> F where - F: Field, + F: JoltField, T: Transcript, { transcript.challenge() @@ -114,7 +114,7 @@ where /// The ZK uni-skip step's outputs: the committed round consistency and output /// claim commitments (carried downstream for BlindFold), plus the reduction /// challenge. -pub(crate) struct UniskipZk { +pub(crate) struct UniskipZk { pub consistency: CommittedSumcheckConsistency, pub output_claims: CommittedOutputClaimOutput, pub challenge: F, @@ -138,7 +138,7 @@ pub fn verify_clear( transcript: &mut T, ) -> Result where - F: Field, + F: JoltField, T: Transcript, { let reduction = proof @@ -176,7 +176,7 @@ pub(crate) fn verify_zk( transcript: &mut T, ) -> Result, VerifierError> where - F: Field, + F: JoltField, C: Clone + AppendToTranscript, T: Transcript, { diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs b/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs index d1a18e8e50..332342edcb 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs @@ -101,7 +101,7 @@ use jolt_claims::{ Expr, OutputClaims, Source, SymbolicSumcheck, Term, }; use jolt_crypto::VectorCommitment; -use jolt_field::{Field, FromPrimitiveInt, RingCore}; +use jolt_field::{JoltField, Ring}; use jolt_lookup_tables::{LookupTableKind, XLEN as RISCV_XLEN}; use jolt_openings::CommitmentScheme; use jolt_poly::{ @@ -157,7 +157,7 @@ impl From for VerifierPublicId { } #[derive(Default)] -struct SourceValues { +struct SourceValues { publics: Vec<(VerifierPublicId, F)>, } @@ -217,7 +217,7 @@ fn add_batched_stage( aliases: Vec>, ) -> Result, VerifierError> where - F: Field, + F: JoltField, C: Clone, { if claims.is_empty() { @@ -280,7 +280,7 @@ fn add_stage( output_claim: VerifierExpr, ) -> Result, VerifierError> where - F: Field, + F: JoltField, C: Clone, { require_expr_sources(name, "input claim", &input_claim, values)?; @@ -314,7 +314,7 @@ where /// Lower one symbolic relation into its `(rounds, input, output)` batch tuple. fn relation_claim(relation: &S) -> (usize, VerifierExpr, VerifierExpr) where - F: Field, + F: JoltField, S: SymbolicSumcheck< OpeningId = JoltOpeningId, DerivedId = JoltDerivedId, @@ -328,7 +328,7 @@ where ) } -fn scale_expr(mut expr: VerifierExpr, scale: F) -> VerifierExpr { +fn scale_expr(mut expr: VerifierExpr, scale: F) -> VerifierExpr { if scale.is_zero() { return VerifierExpr::zero(); } @@ -338,7 +338,7 @@ fn scale_expr(mut expr: VerifierExpr, scale: F) -> VerifierExpr expr } -fn map_jolt_expr(expr: JoltExpr) -> VerifierExpr { +fn map_jolt_expr(expr: JoltExpr) -> VerifierExpr { Expr { terms: expr .terms @@ -359,7 +359,7 @@ fn map_jolt_expr(expr: JoltExpr) -> VerifierExpr { } } -fn require_expr_sources( +fn require_expr_sources( stage: &'static str, expression: &'static str, expr: &VerifierExpr, @@ -895,7 +895,7 @@ where ) } -fn add_bytecode_chunk_weight_publics( +fn add_bytecode_chunk_weight_publics( values: &mut SourceValues, chunk_weights: Vec, ) -> Result<(), VerifierError> { @@ -1103,7 +1103,7 @@ where .collect() } -fn hamming_virtualization_address_point( +fn hamming_virtualization_address_point( log_k_chunk: usize, point: &[F], ) -> Result, VerifierError> { @@ -1118,7 +1118,7 @@ fn hamming_virtualization_address_point( }) } -impl SourceValues { +impl SourceValues { fn public(&mut self, id: impl Into, value: F) -> Result<(), VerifierError> { let id = id.into(); if let Some((_, existing)) = self.publics.iter().find(|(candidate, _)| *candidate == id) { @@ -1138,7 +1138,7 @@ impl SourceValues { } } -fn stage_sumcheck_error( +fn stage_sumcheck_error( stage: JoltRelationId, error: jolt_sumcheck::SumcheckError, ) -> VerifierError { diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage1.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage1.rs index b8b880458a..3a0b9157e6 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage1.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage1.rs @@ -86,7 +86,7 @@ where ) } -fn stage1_spartan_outer_output_expr( +fn stage1_spartan_outer_output_expr( openings: &[JoltVirtualPolynomial], ) -> VerifierExpr { // The factored quadratic form, mirroring the jolt-claims relation: each diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs index ffa30e6183..06248472b3 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs @@ -265,7 +265,7 @@ where ) } -fn selected_product_uniskip_input_expr( +fn selected_product_uniskip_input_expr( weights: &[F], ) -> Result, VerifierError> { let [product_weight, should_branch_weight, should_jump_weight, rest @ ..] = weights else { @@ -298,7 +298,7 @@ fn selected_product_uniskip_input_expr( Ok(expr) } -fn selected_product_remainder_output_expr( +fn selected_product_remainder_output_expr( weights: &[F], tau_kernel: F, ) -> Result, VerifierError> { diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs index e91c3f1ed9..409f829eff 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs @@ -163,7 +163,7 @@ where ) } -fn stage6_cycle_output_openings_and_aliases( +fn stage6_cycle_output_openings_and_aliases( formula_dimensions: JoltFormulaDimensions, bytecode_ra_opening_points: &[Vec], booleanity_opening_point: &[F], diff --git a/crates/jolt-verifier/src/stages/zk/committed.rs b/crates/jolt-verifier/src/stages/zk/committed.rs index f827ec151b..020243bc53 100644 --- a/crates/jolt-verifier/src/stages/zk/committed.rs +++ b/crates/jolt-verifier/src/stages/zk/committed.rs @@ -1,7 +1,7 @@ //! Shared checks for committed sumcheck stage boundaries. use jolt_claims::protocols::jolt::JoltRelationId; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_sumcheck::SumcheckProof; use crate::{verifier::CheckedInputs, VerifierError}; @@ -16,7 +16,7 @@ pub(crate) fn verify_output_claim_commitments( stage: JoltRelationId, ) -> Result, VerifierError> where - F: Field, + F: JoltField, C: Clone, { // Invariant: Some(capacity) implies capacity >= MAX_BLINDFOLD_GENERATORS, diff --git a/crates/jolt-verifier/src/verifier.rs b/crates/jolt-verifier/src/verifier.rs index 454ea05514..ca1edf6c3d 100644 --- a/crates/jolt-verifier/src/verifier.rs +++ b/crates/jolt-verifier/src/verifier.rs @@ -6,7 +6,7 @@ use jolt_claims::protocols::jolt::{JoltOneHotConfig, JoltReadWriteConfig}; #[cfg(not(feature = "akita"))] use jolt_crypto::HomomorphicCommitment; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::CommitmentScheme; #[cfg(not(feature = "akita"))] use jolt_openings::{AdditivelyHomomorphic, ZkOpeningScheme}; @@ -37,7 +37,7 @@ pub fn verify( trusted_advice_commitment: Option<&PCS::Output>, ) -> Result<(), VerifierError> where - F: Field + AppendToTranscript, + F: JoltField + AppendToTranscript, PCS: CommitmentScheme + AdditivelyHomomorphic + ZkOpeningScheme, @@ -175,7 +175,7 @@ pub fn verify( trusted_advice_commitment: Option<&PCS::Output>, ) -> Result<(), VerifierError> where - F: Field + AppendToTranscript, + F: JoltField + AppendToTranscript, PCS: CommitmentScheme, PCS::Output: Clone + AppendToTranscript + stage8::OneHotTraceCommitmentMetadata, PCS::VerifierSetup: stage8::OneHotTraceSetupMetadata, @@ -537,7 +537,7 @@ pub(crate) fn validate_sumcheck_representation( zk: bool, ) -> Result<(), VerifierError> where - F: Field, + F: JoltField, { if proof.is_committed() == zk { return Ok(()); diff --git a/crates/jolt-verifier/tests/completeness/zk.rs b/crates/jolt-verifier/tests/completeness/zk.rs index 146e1851d9..e09f6305b0 100644 --- a/crates/jolt-verifier/tests/completeness/zk.rs +++ b/crates/jolt-verifier/tests/completeness/zk.rs @@ -178,7 +178,7 @@ fn blindfold_proof_shape( #[cfg(all(feature = "prover-fixtures", feature = "zk"))] fn committed_round_rows(proof: &SumcheckProof) -> usize where - F: jolt_field::Field, + F: jolt_field::JoltField, { proof .as_committed() @@ -190,7 +190,7 @@ where #[cfg(all(feature = "prover-fixtures", feature = "zk"))] fn committed_output_claim_rows(proof: &SumcheckProof) -> usize where - F: jolt_field::Field, + F: jolt_field::JoltField, { proof .as_committed() diff --git a/crates/jolt-verifier/tests/soundness/tampering/akita.rs b/crates/jolt-verifier/tests/soundness/tampering/akita.rs index c4590ece69..4177239739 100644 --- a/crates/jolt-verifier/tests/soundness/tampering/akita.rs +++ b/crates/jolt-verifier/tests/soundness/tampering/akita.rs @@ -34,7 +34,7 @@ use jolt_claims::protocols::jolt::lattice::relations::{ program_image_reconstruction::ProgramImageReconstructionOutputClaims, read_raf::LatticeBytecodeReadRafOutputClaims, }; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_prover_legacy::zkvm::packed::{AkitaField, AkitaJoltProof, AkitaScheme}; use jolt_verifier::proof::{ClearProofClaims, JoltProofClaims}; use jolt_verifier::stages::{ @@ -106,7 +106,7 @@ fn clear_claims_mut(proof: &mut AkitaJoltProof) -> &mut ClearProofClaims(claims: &mut ClearProofClaims, f: &mut impl FnMut(&mut F)) { +fn for_each_scalar_mut(claims: &mut ClearProofClaims, f: &mut impl FnMut(&mut F)) { let f: &mut dyn FnMut(&mut F) = f; let ClearProofClaims { stage1, @@ -130,7 +130,7 @@ fn for_each_scalar_mut(claims: &mut ClearProofClaims, f: &mut impl visit_reconstruction(reconstruction, f); } -fn visit_stage1(claims: &mut Stage1OutputClaims, f: &mut dyn FnMut(&mut F)) { +fn visit_stage1(claims: &mut Stage1OutputClaims, f: &mut dyn FnMut(&mut F)) { let Stage1OutputClaims { uniskip_output_claim, outer, @@ -215,7 +215,7 @@ fn visit_stage1(claims: &mut Stage1OutputClaims, f: &mut dyn FnMut( } } -fn visit_stage2(claims: &mut Stage2OutputClaims, f: &mut dyn FnMut(&mut F)) { +fn visit_stage2(claims: &mut Stage2OutputClaims, f: &mut dyn FnMut(&mut F)) { let Stage2OutputClaims { product_uniskip_output_claim, batch_outputs, @@ -272,7 +272,7 @@ fn visit_stage2(claims: &mut Stage2OutputClaims, f: &mut dyn FnMut( } } -fn visit_stage3(claims: &mut Stage3OutputClaims, f: &mut dyn FnMut(&mut F)) { +fn visit_stage3(claims: &mut Stage3OutputClaims, f: &mut dyn FnMut(&mut F)) { let Stage3OutputClaims { shift, instruction_input, @@ -322,7 +322,7 @@ fn visit_stage3(claims: &mut Stage3OutputClaims, f: &mut dyn FnMut( } } -fn visit_stage4(claims: &mut Stage4OutputClaims, f: &mut dyn FnMut(&mut F)) { +fn visit_stage4(claims: &mut Stage4OutputClaims, f: &mut dyn FnMut(&mut F)) { let Stage4OutputClaims { registers_read_write, ram_val_check, @@ -355,7 +355,7 @@ fn visit_stage4(claims: &mut Stage4OutputClaims, f: &mut dyn FnMut( } } -fn visit_stage5(claims: &mut Stage5OutputClaims, f: &mut dyn FnMut(&mut F)) { +fn visit_stage5(claims: &mut Stage5OutputClaims, f: &mut dyn FnMut(&mut F)) { let Stage5OutputClaims { instruction_read_raf, ram_ra_claim_reduction, @@ -381,7 +381,7 @@ fn visit_stage5(claims: &mut Stage5OutputClaims, f: &mut dyn FnMut( } } -fn visit_stage6a(claims: &mut Stage6aOutputClaims, f: &mut dyn FnMut(&mut F)) { +fn visit_stage6a(claims: &mut Stage6aOutputClaims, f: &mut dyn FnMut(&mut F)) { let Stage6aOutputClaims { bytecode_read_raf, booleanity, @@ -400,7 +400,7 @@ fn visit_stage6a(claims: &mut Stage6aOutputClaims, f: &mut dyn FnMu f(booleanity_intermediate); } -fn visit_stage6b(claims: &mut Stage6bOutputClaims, f: &mut dyn FnMut(&mut F)) { +fn visit_stage6b(claims: &mut Stage6bOutputClaims, f: &mut dyn FnMut(&mut F)) { let Stage6bOutputClaims { bytecode_read_raf, booleanity, @@ -479,7 +479,7 @@ fn visit_stage6b(claims: &mut Stage6bOutputClaims, f: &mut dyn FnMu } } -fn visit_stage7(claims: &mut Stage7OutputClaims, f: &mut dyn FnMut(&mut F)) { +fn visit_stage7(claims: &mut Stage7OutputClaims, f: &mut dyn FnMut(&mut F)) { let Stage7OutputClaims { hamming_weight_claim_reduction, trusted_advice, @@ -525,7 +525,7 @@ fn visit_stage7(claims: &mut Stage7OutputClaims, f: &mut dyn FnMut( } } -fn visit_reconstruction( +fn visit_reconstruction( claims: &mut ReconstructionOutputClaims, f: &mut dyn FnMut(&mut F), ) { diff --git a/crates/jolt-verifier/tests/soundness/tampering/openings.rs b/crates/jolt-verifier/tests/soundness/tampering/openings.rs index abd9fb11b2..932748c340 100644 --- a/crates/jolt-verifier/tests/soundness/tampering/openings.rs +++ b/crates/jolt-verifier/tests/soundness/tampering/openings.rs @@ -19,7 +19,7 @@ use jolt_claims::protocols::jolt::{ JoltOpeningId, }; #[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_field::{Fr, FromPrimitiveInt}; +use jolt_field::{Fr, Ring}; #[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] use jolt_lookup_tables::XLEN as RISCV_XLEN; diff --git a/crates/jolt-verifier/tests/soundness/tampering/proof_shape.rs b/crates/jolt-verifier/tests/soundness/tampering/proof_shape.rs index 8df3b1137d..c1fc78867e 100644 --- a/crates/jolt-verifier/tests/soundness/tampering/proof_shape.rs +++ b/crates/jolt-verifier/tests/soundness/tampering/proof_shape.rs @@ -10,7 +10,7 @@ use jolt_verifier::proof::JoltProofClaims; use { jolt_blindfold::BlindFoldProof, jolt_crypto::VectorCommitmentOpening, - jolt_field::{Fr, FromPrimitiveInt}, + jolt_field::{Fr, Ring}, jolt_sumcheck::CompressedSumcheckProof, }; diff --git a/crates/jolt-verifier/tests/soundness/tampering/sumcheck.rs b/crates/jolt-verifier/tests/soundness/tampering/sumcheck.rs index 82ca8b986a..f2162432d6 100644 --- a/crates/jolt-verifier/tests/soundness/tampering/sumcheck.rs +++ b/crates/jolt-verifier/tests/soundness/tampering/sumcheck.rs @@ -34,7 +34,7 @@ use jolt_claims::protocols::jolt::{ #[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] use jolt_claims::{protocols::jolt::geometry::spartan, protocols::jolt::relations, OutputClaims}; #[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_field::{Fr, FromPrimitiveInt}; +use jolt_field::{Fr, Ring}; #[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] use jolt_lookup_tables::{LookupTableKind, XLEN as RISCV_XLEN}; #[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] diff --git a/crates/jolt-verifier/tests/soundness/tampering/zk.rs b/crates/jolt-verifier/tests/soundness/tampering/zk.rs index d4dc089a14..7281429463 100644 --- a/crates/jolt-verifier/tests/soundness/tampering/zk.rs +++ b/crates/jolt-verifier/tests/soundness/tampering/zk.rs @@ -12,7 +12,7 @@ use crate::support; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] use crate::support::tamper_manifest; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_field::FromPrimitiveInt as _; +use jolt_field::Ring as _; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] use jolt_verifier::JoltProofClaims; @@ -299,7 +299,7 @@ fn with_zk_verifier_stack(test: impl FnOnce() + Send + 'static) { #[cfg(all(feature = "prover-fixtures", feature = "zk"))] fn pop_committed_round(proof: &mut jolt_sumcheck::SumcheckProof) where - F: jolt_field::Field, + F: jolt_field::JoltField, { let jolt_sumcheck::SumcheckProof::Committed(proof) = proof else { panic!("ZK fixture must use committed sumcheck proofs"); @@ -310,7 +310,7 @@ where #[cfg(all(feature = "prover-fixtures", feature = "zk"))] fn exceed_first_committed_round_degree_bound(proof: &mut jolt_sumcheck::SumcheckProof) where - F: jolt_field::Field, + F: jolt_field::JoltField, { let jolt_sumcheck::SumcheckProof::Committed(proof) = proof else { panic!("ZK fixture must use committed sumcheck proofs"); @@ -324,7 +324,7 @@ where #[cfg(all(feature = "prover-fixtures", feature = "zk"))] fn pop_committed_output_claim_row(proof: &mut jolt_sumcheck::SumcheckProof) where - F: jolt_field::Field, + F: jolt_field::JoltField, { let jolt_sumcheck::SumcheckProof::Committed(proof) = proof else { panic!("ZK fixture must use committed sumcheck proofs"); diff --git a/crates/jolt-verifier/tests/statistical_independence/zk.rs b/crates/jolt-verifier/tests/statistical_independence/zk.rs index 7a846ca833..3b888ab137 100644 --- a/crates/jolt-verifier/tests/statistical_independence/zk.rs +++ b/crates/jolt-verifier/tests/statistical_independence/zk.rs @@ -14,7 +14,7 @@ use std::collections::BTreeMap; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] use ark_serialize::CanonicalSerialize; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_field::{CanonicalBytes, CanonicalRepr, Fr}; +use jolt_field::{CanonicalBytes, CanonicalEncoding, Fr}; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] use jolt_sumcheck::SumcheckProof; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] diff --git a/crates/jolt-verifier/tests/support/proof_claims.rs b/crates/jolt-verifier/tests/support/proof_claims.rs index 02b227ac32..7587577faf 100644 --- a/crates/jolt-verifier/tests/support/proof_claims.rs +++ b/crates/jolt-verifier/tests/support/proof_claims.rs @@ -14,7 +14,7 @@ use jolt_claims::protocols::jolt::{ JoltAdviceKind, JoltCommittedPolynomial, JoltOpeningId, JoltRelationId, JoltVirtualPolynomial, }; use jolt_crypto::VectorCommitment; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_lookup_tables::{LookupTableKind, XLEN as RISCV_XLEN}; use jolt_openings::CommitmentScheme; use jolt_riscv::CircuitFlags; @@ -96,7 +96,7 @@ where claim_mut_from_clear(claims, proof.trace_length, id) } -fn claim_from_clear( +fn claim_from_clear( claims: &ClearProofClaims, trace_length: usize, id: native::JoltOpeningId, @@ -107,7 +107,7 @@ fn claim_from_clear( claim_mut_from_clear(&mut copy, trace_length, id).map(|value| *value) } -fn claim_mut_from_clear( +fn claim_mut_from_clear( claims: &mut ClearProofClaims, trace_length: usize, id: native::JoltOpeningId, @@ -130,7 +130,7 @@ fn claim_mut_from_clear( .or_else(|| claim_mut_from_stage6_outputs(&mut claims.stage6a, &mut claims.stage6b, id)) } -fn claim_mut_from_spartan_outer( +fn claim_mut_from_spartan_outer( claims: &mut Stage1BatchOutputClaims, variable: JoltVirtualPolynomial, ) -> Option<&mut F> { @@ -189,7 +189,7 @@ fn stage1_outer_variable( .find(|variable| id == outer_opening(*variable)) } -fn claim_mut_from_stage2_batch_outputs( +fn claim_mut_from_stage2_batch_outputs( claims: &mut Stage2BatchOutputClaims, id: native::JoltOpeningId, ) -> Option<&mut F> { @@ -255,7 +255,7 @@ fn claim_mut_from_stage2_batch_outputs( } } -fn claim_mut_from_stage3_outputs( +fn claim_mut_from_stage3_outputs( claims: &mut Stage3OutputClaims, id: native::JoltOpeningId, ) -> Option<&mut F> { @@ -309,7 +309,7 @@ fn claim_mut_from_stage3_outputs( } } -fn claim_mut_from_stage4_outputs( +fn claim_mut_from_stage4_outputs( claims: &mut Stage4OutputClaims, id: native::JoltOpeningId, ) -> Option<&mut F> { @@ -340,7 +340,7 @@ fn claim_mut_from_stage4_outputs( } } -fn claim_mut_from_stage5_outputs( +fn claim_mut_from_stage5_outputs( claims: &mut Stage5OutputClaims, id: native::JoltOpeningId, ) -> Option<&mut F> { @@ -379,7 +379,7 @@ fn claim_mut_from_stage5_outputs( } } -fn claim_mut_from_stage6_outputs<'a, F: Field>( +fn claim_mut_from_stage6_outputs<'a, F: JoltField>( stage6a: &'a mut Stage6aOutputClaims, stage6b: &'a mut Stage6bOutputClaims, id: native::JoltOpeningId, @@ -476,7 +476,7 @@ fn claim_mut_from_stage6_outputs<'a, F: Field>( } } -fn claim_mut_from_stage7_outputs( +fn claim_mut_from_stage7_outputs( claims: &mut Stage7OutputClaims, id: native::JoltOpeningId, ) -> Option<&mut F> { diff --git a/crates/jolt-verifier/tests/support/tamper_manifest.rs b/crates/jolt-verifier/tests/support/tamper_manifest.rs index 7d769bcd73..5a247e7317 100644 --- a/crates/jolt-verifier/tests/support/tamper_manifest.rs +++ b/crates/jolt-verifier/tests/support/tamper_manifest.rs @@ -1,6 +1,6 @@ use std::collections::BTreeSet; -use jolt_field::{Field, Fr}; +use jolt_field::{Fr, JoltField}; use jolt_verifier::{ proof::ClearProofClaims, stages::{stage1, stage2, stage3, stage4, stage5, stage6a, stage6b, stage7}, @@ -1349,7 +1349,7 @@ fn collect_leaf_paths(prefix: &str, value: &Value, paths: &mut BTreeSet) } } -pub fn clear_claims(fill_optionals: bool) -> ClearProofClaims { +pub fn clear_claims(fill_optionals: bool) -> ClearProofClaims { let zero = F::zero(); let optional = fill_optionals.then_some(zero); diff --git a/crates/jolt-verifier/tests/support/zk_audit.rs b/crates/jolt-verifier/tests/support/zk_audit.rs index 64c10d32af..f259a34338 100644 --- a/crates/jolt-verifier/tests/support/zk_audit.rs +++ b/crates/jolt-verifier/tests/support/zk_audit.rs @@ -4,7 +4,7 @@ use common::jolt_device::JoltDevice; use jolt_blindfold::BlindFoldProtocol; use jolt_claims::protocols::jolt::JoltRelationId; use jolt_crypto::{HomomorphicCommitment, VectorCommitment}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, ZkOpeningScheme}; use jolt_transcript::{AppendToTranscript, Transcript}; @@ -33,7 +33,7 @@ pub struct ZkBlindFoldProtocolShape { impl ZkBlindFoldProtocolShape { fn from_protocol(protocol: &BlindFoldProtocol) -> Self where - F: Field, + F: JoltField, { Self { coefficient_rows: protocol.dimensions.coefficient_rows, @@ -55,7 +55,7 @@ pub fn audit_zk_blindfold_protocol_shape( trusted_advice_commitment: Option<&PCS::Output>, ) -> Result where - F: Field + AppendToTranscript, + F: JoltField + AppendToTranscript, PCS: CommitmentScheme + AdditivelyHomomorphic + ZkOpeningScheme, diff --git a/crates/jolt-witness/src/backend/fixed.rs b/crates/jolt-witness/src/backend/fixed.rs index e50851a3b5..85b11c2ea3 100644 --- a/crates/jolt-witness/src/backend/fixed.rs +++ b/crates/jolt-witness/src/backend/fixed.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use jolt_claims::protocols::jolt::{JoltCommittedPolynomial, JoltPolynomialId}; -use jolt_field::Field; +use jolt_field::JoltField; use crate::{JoltWitnessOracle, Shape, WitnessError}; @@ -58,7 +58,7 @@ impl FixedBackend { } } -impl JoltWitnessOracle for FixedBackend { +impl JoltWitnessOracle for FixedBackend { fn shape(&self, id: JoltPolynomialId) -> Result { self.column(id).map(|(shape, _)| *shape) } @@ -76,7 +76,7 @@ impl JoltWitnessOracle for FixedBackend { #[expect(clippy::unwrap_used, reason = "test module")] mod tests { use jolt_claims::protocols::jolt::JoltVirtualPolynomial; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use super::*; use crate::PolynomialEncoding; diff --git a/crates/jolt-witness/src/backend/mod.rs b/crates/jolt-witness/src/backend/mod.rs index b3a7b307c8..786644348d 100644 --- a/crates/jolt-witness/src/backend/mod.rs +++ b/crates/jolt-witness/src/backend/mod.rs @@ -1,7 +1,7 @@ //! Witness backends: implementors of the id-indexed oracle surface. use jolt_claims::protocols::jolt::{JoltCommittedPolynomial, JoltPolynomialId}; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_program::preprocess::JoltProgramPreprocessing; use crate::{RowSource, Shape, WitnessBundle, WitnessError}; @@ -14,7 +14,7 @@ pub mod trace; /// sets and the config's committed set — must be servable before witness /// generation starts. The servable set is the backend's exhaustive match /// (its `shape` resolving), never a curated list. -pub fn validate_servable( +pub fn validate_servable( oracle: &dyn JoltWitnessOracle, ids: impl IntoIterator, ) -> Result<(), WitnessError> { @@ -44,7 +44,7 @@ pub trait BundleSource { /// Typed consumers (bundles over `stream_witnesses`) are statically /// dispatched and do not go through this trait; both paths meet at the same /// `Extract` impls. -pub trait JoltWitnessOracle { +pub trait JoltWitnessOracle { fn shape(&self, id: JoltPolynomialId) -> Result; /// Materializes the oracle's dense field-element evaluations, row-major @@ -70,7 +70,9 @@ pub trait ProgramSource { /// typed bundles through [`crate::collect_bundles`], so no stage recipe /// stages row vectors on the side), and the program view. /// Blanket-implemented; the supertrait set is exactly what kernels consume. -pub trait JoltWitnessPlane: JoltWitnessOracle + RowSource + ProgramSource {} +pub trait JoltWitnessPlane: JoltWitnessOracle + RowSource + ProgramSource {} -impl JoltWitnessPlane for T where T: JoltWitnessOracle + RowSource + ProgramSource -{} +impl JoltWitnessPlane for T where + T: JoltWitnessOracle + RowSource + ProgramSource +{ +} diff --git a/crates/jolt-witness/src/backend/trace/advice.rs b/crates/jolt-witness/src/backend/trace/advice.rs index 0c34f47907..d9d4ab1071 100644 --- a/crates/jolt-witness/src/backend/trace/advice.rs +++ b/crates/jolt-witness/src/backend/trace/advice.rs @@ -4,7 +4,7 @@ use super::*; impl TraceBackend<'_, T> { - pub(crate) fn materialize_trusted_advice(&self) -> Result, WitnessError> { + pub(crate) fn materialize_trusted_advice(&self) -> Result, WitnessError> { materialize_advice( "trusted", &self.trace.device.trusted_advice, @@ -12,7 +12,9 @@ impl TraceBackend<'_, T> { ) } - pub(crate) fn materialize_untrusted_advice(&self) -> Result, WitnessError> { + pub(crate) fn materialize_untrusted_advice( + &self, + ) -> Result, WitnessError> { materialize_advice( "untrusted", &self.trace.device.untrusted_advice, @@ -28,7 +30,7 @@ pub(super) fn advice_words(max_bytes: usize) -> usize { (max_bytes / 8).next_power_of_two().max(1) } -fn materialize_advice( +fn materialize_advice( kind: &str, bytes: &[u8], max_bytes: usize, diff --git a/crates/jolt-witness/src/backend/trace/cycle.rs b/crates/jolt-witness/src/backend/trace/cycle.rs index 46077ba3aa..750e8d5d17 100644 --- a/crates/jolt-witness/src/backend/trace/cycle.rs +++ b/crates/jolt-witness/src/backend/trace/cycle.rs @@ -11,7 +11,7 @@ use crate::{BundleSource, RowSource, WitnessBundle}; impl TraceBackend<'_, T> { /// Materializes one cycle-domain witness column by walking the trace /// once; all per-witness logic lives on `W`. - pub(crate) fn materialize_cycle( + pub(crate) fn materialize_cycle( &self, ) -> Result, WitnessError> { self.walk_cycles(|row, next, env| W::extract(row, next, env).map(ToField::to_field)) @@ -19,7 +19,11 @@ impl TraceBackend<'_, T> { /// [`Self::materialize_cycle`] for indexed witness families; `index` /// selects the family member. - pub(crate) fn materialize_cycle_indexed + ToField, I: Copy>( + pub(crate) fn materialize_cycle_indexed< + F: JoltField, + W: ExtractIndexed + ToField, + I: Copy, + >( &self, index: I, ) -> Result, WitnessError> { @@ -44,7 +48,7 @@ impl TraceBackend<'_, T> { chunk_bits: usize, ) -> Result, WitnessError> where - F: Field, + F: JoltField, W: ExtractIndexed + Into>, { let selector = RaChunkSelector::new(index, chunks, chunk_bits)?; diff --git a/crates/jolt-witness/src/backend/trace/mod.rs b/crates/jolt-witness/src/backend/trace/mod.rs index 9f2e0372e4..95b3d5acb5 100644 --- a/crates/jolt-witness/src/backend/trace/mod.rs +++ b/crates/jolt-witness/src/backend/trace/mod.rs @@ -5,7 +5,7 @@ use jolt_claims::protocols::jolt::{ geometry::{committed_openings, dimensions::REGISTER_ADDRESS_BITS, ra::JoltRaPolynomialLayout}, JoltCommittedPolynomial, JoltFormulaDimensions, JoltOneHotConfig, JoltVirtualPolynomial, }; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_lookup_tables::LookupTableKind; use jolt_program::{ execution::{JoltProgram, RamAccess, TraceOutput, TraceRow, TraceSource}, diff --git a/crates/jolt-witness/src/backend/trace/oracle.rs b/crates/jolt-witness/src/backend/trace/oracle.rs index c21d88e6f6..a751ce70d7 100644 --- a/crates/jolt-witness/src/backend/trace/oracle.rs +++ b/crates/jolt-witness/src/backend/trace/oracle.rs @@ -158,7 +158,7 @@ impl TraceBackend<'_, T> { } } -impl JoltWitnessOracle for TraceBackend<'_, T> { +impl JoltWitnessOracle for TraceBackend<'_, T> { fn shape(&self, id: JoltPolynomialId) -> Result { self.shape_of(id) } diff --git a/crates/jolt-witness/src/backend/trace/ram.rs b/crates/jolt-witness/src/backend/trace/ram.rs index 675f57bdad..97ee918b0d 100644 --- a/crates/jolt-witness/src/backend/trace/ram.rs +++ b/crates/jolt-witness/src/backend/trace/ram.rs @@ -3,7 +3,7 @@ use super::*; impl TraceBackend<'_, T> { - pub(crate) fn materialize_ram_read_write_virtual( + pub(crate) fn materialize_ram_read_write_virtual( &self, id: JoltVirtualPolynomial, ) -> Result, WitnessError> { @@ -16,7 +16,7 @@ impl TraceBackend<'_, T> { } } - pub(crate) fn materialize_ram_val(&self) -> Result, WitnessError> { + pub(crate) fn materialize_ram_val(&self) -> Result, WitnessError> { let cycles = checked_pow2(self.config.log_t)?; let addresses = self.config.ram_k; let mut state = self.initial_ram_state()?; @@ -50,7 +50,7 @@ impl TraceBackend<'_, T> { Ok(values) } - pub(crate) fn materialize_ram_ra(&self) -> Result, WitnessError> { + pub(crate) fn materialize_ram_ra(&self) -> Result, WitnessError> { let cycles = checked_pow2(self.config.log_t)?; let addresses = self.config.ram_k; let mut values = vec![F::zero(); addresses * cycles]; @@ -70,7 +70,7 @@ impl TraceBackend<'_, T> { Ok(values) } - pub(crate) fn materialize_ram_val_final(&self) -> Result, WitnessError> { + pub(crate) fn materialize_ram_val_final(&self) -> Result, WitnessError> { self.final_ram_state() .map(|state| state.into_iter().map(F::from_u64).collect()) } diff --git a/crates/jolt-witness/src/backend/trace/registers.rs b/crates/jolt-witness/src/backend/trace/registers.rs index 229d2bba6a..ac9542819d 100644 --- a/crates/jolt-witness/src/backend/trace/registers.rs +++ b/crates/jolt-witness/src/backend/trace/registers.rs @@ -3,7 +3,7 @@ use super::*; impl TraceBackend<'_, T> { - pub(crate) fn materialize_register_read_write_virtual( + pub(crate) fn materialize_register_read_write_virtual( &self, id: JoltVirtualPolynomial, ) -> Result, WitnessError> { diff --git a/crates/jolt-witness/src/backend/trace/tests.rs b/crates/jolt-witness/src/backend/trace/tests.rs index a5adf8ea97..34c7bc6e88 100644 --- a/crates/jolt-witness/src/backend/trace/tests.rs +++ b/crates/jolt-witness/src/backend/trace/tests.rs @@ -2,7 +2,7 @@ use common::{ constants::RAM_START_ADDRESS, jolt_device::{JoltDevice, MemoryConfig, MemoryLayout}, }; -use jolt_field::{Fr, FromPrimitiveInt}; +use jolt_field::{Fr, Ring}; use jolt_program::{ execution::{ JoltProgram, MemoryImage, OwnedTrace, RamAccess, RamRead, RamWrite, RegisterRead, diff --git a/crates/jolt-witness/src/bundle.rs b/crates/jolt-witness/src/bundle.rs index c3f5c84ff3..f4676d8795 100644 --- a/crates/jolt-witness/src/bundle.rs +++ b/crates/jolt-witness/src/bundle.rs @@ -32,7 +32,7 @@ pub trait WitnessBundle: Sized { #[expect(clippy::unwrap_used, reason = "test module")] mod tests { use jolt_claims::protocols::jolt::{JoltPolynomialId, JoltVirtualPolynomial}; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_riscv::CircuitFlags; use super::WitnessBundle; diff --git a/crates/jolt-witness/src/field_inline/mod.rs b/crates/jolt-witness/src/field_inline/mod.rs index 0b3277c607..1cdb3362ff 100644 --- a/crates/jolt-witness/src/field_inline/mod.rs +++ b/crates/jolt-witness/src/field_inline/mod.rs @@ -2,7 +2,7 @@ use jolt_claims::protocols::field_inline::{ FieldInlineCommittedPolynomial, FieldInlinePolynomialId, FieldInlineVirtualPolynomial, FIELD_REGISTERS_LOG_K, }; -use jolt_field::Field; +use jolt_field::JoltField; use jolt_program::{ execution::{JoltProgram, TraceOutput, TraceRow, TraceSource}, field_inline::{ @@ -28,27 +28,27 @@ pub mod witnesses; pub const FIELD_INLINE_LABEL: &str = "jolt_vm.field_inline"; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct FieldInlineRegisterReadRow { +pub struct FieldInlineRegisterReadRow { pub register: u8, pub value: F, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct FieldInlineRegisterWriteRow { +pub struct FieldInlineRegisterWriteRow { pub register: u8, pub pre_value: F, pub post_value: F, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct FieldInlineRegisterReadWriteRow { +pub struct FieldInlineRegisterReadWriteRow { pub rs1: Option>, pub rs2: Option>, pub rd: Option>, pub rd_increment: F, } -pub trait FieldInlineRegisterReadWriteRows { +pub trait FieldInlineRegisterReadWriteRows { fn field_inline_register_read_write_rows( &self, ) -> Result>, WitnessError>; @@ -199,14 +199,18 @@ impl<'a> TraceBackedFieldInlineWitness<'a> { /// Materializes one cycle-domain witness column; rows beyond the trace /// are zero. All per-witness logic lives on `W`. - fn materialize_cycle + Send>( + fn materialize_cycle + Send>( &self, ) -> Result, WitnessError> { self.walk_cycles(|row, env| W::extract(row, None, env).map(FieldValue::value)) } /// [`Self::materialize_cycle`] for indexed witness families. - fn materialize_cycle_indexed + FieldValue, I: Copy + Sync>( + fn materialize_cycle_indexed< + F: JoltField, + W: ExtractIndexed + FieldValue, + I: Copy + Sync, + >( &self, index: I, ) -> Result, WitnessError> { @@ -215,7 +219,7 @@ impl<'a> TraceBackedFieldInlineWitness<'a> { }) } - fn walk_cycles( + fn walk_cycles( &self, value: impl Fn(&TraceRow, &WitnessEnv<'_>) -> Result + Sync, ) -> Result, WitnessError> { @@ -233,7 +237,7 @@ impl<'a> TraceBackedFieldInlineWitness<'a> { Ok(values) } - fn materialize_register_virtual( + fn materialize_register_virtual( &self, id: FieldInlineVirtualPolynomial, ) -> Result, WitnessError> { @@ -310,7 +314,7 @@ impl TraceBackedFieldInlineWitness<'_> { } } - pub fn oracle_table( + pub fn oracle_table( &self, id: FieldInlinePolynomialId, ) -> Result, WitnessError> { @@ -339,7 +343,7 @@ impl TraceBackedFieldInlineWitness<'_> { } } -impl FieldInlineRegisterReadWriteRows for TraceBackedFieldInlineWitness<'_> { +impl FieldInlineRegisterReadWriteRows for TraceBackedFieldInlineWitness<'_> { fn field_inline_register_read_write_rows( &self, ) -> Result>, WitnessError> { @@ -385,7 +389,7 @@ impl<'a, T: TraceSource> TraceBackend<'a, T> { } } -impl FieldInlineRegisterReadWriteRows for TraceBackend<'_, T> { +impl FieldInlineRegisterReadWriteRows for TraceBackend<'_, T> { fn field_inline_register_read_write_rows( &self, ) -> Result>, WitnessError> { @@ -394,7 +398,7 @@ impl FieldInlineRegisterReadWriteRows for TraceBack } } -fn field_register_row( +fn field_register_row( row: &TraceRow, env: &WitnessEnv<'_>, ) -> Result, WitnessError> { @@ -632,7 +636,7 @@ mod tests { use jolt_claims::protocols::jolt::{ JoltCommittedPolynomial, JoltOneHotConfig, JoltPolynomialId, }; - use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_field::{Fr, Ring}; use jolt_program::{ execution::{ JoltProgram, OwnedTrace, RegisterRead, RegisterState, RegisterWrite, TraceOutput, diff --git a/crates/jolt-witness/src/field_inline/witnesses.rs b/crates/jolt-witness/src/field_inline/witnesses.rs index 0570027b7c..d1a9938bf1 100644 --- a/crates/jolt-witness/src/field_inline/witnesses.rs +++ b/crates/jolt-witness/src/field_inline/witnesses.rs @@ -8,7 +8,7 @@ //! extract to zero / false. use jolt_claims::protocols::field_inline::FieldInlineOpFlag; -use jolt_field::{CanonicalRepr, Field}; +use jolt_field::{CanonicalEncoding, JoltField}; use jolt_program::{execution::TraceRow, field_inline::FieldEncodedValue}; use jolt_riscv::FieldInlineOp; @@ -69,13 +69,13 @@ field_value!( FieldRdInc, ); -impl FieldValue for FieldOpFlag { +impl FieldValue for FieldOpFlag { fn value(self) -> F { F::from_bool(self.0) } } -impl Extract for FieldRs1Value { +impl Extract for FieldRs1Value { fn extract( row: &TraceRow, _next: Option<&TraceRow>, @@ -90,7 +90,7 @@ impl Extract for FieldRs1Value { } } -impl Extract for FieldRs2Value { +impl Extract for FieldRs2Value { fn extract( row: &TraceRow, _next: Option<&TraceRow>, @@ -105,7 +105,7 @@ impl Extract for FieldRs2Value { } } -impl Extract for FieldRdValue { +impl Extract for FieldRdValue { fn extract( row: &TraceRow, _next: Option<&TraceRow>, @@ -120,7 +120,7 @@ impl Extract for FieldRdValue { } } -impl Extract for FieldProduct { +impl Extract for FieldProduct { fn extract( row: &TraceRow, next: Option<&TraceRow>, @@ -132,7 +132,7 @@ impl Extract for FieldProduct { } } -impl Extract for FieldInvProduct { +impl Extract for FieldInvProduct { fn extract( row: &TraceRow, next: Option<&TraceRow>, @@ -159,7 +159,7 @@ impl ExtractIndexed for FieldOpFlag { } } -impl Extract for FieldRdInc { +impl Extract for FieldRdInc { fn extract( row: &TraceRow, _next: Option<&TraceRow>, @@ -176,13 +176,13 @@ impl Extract for FieldRdInc { } } -pub(crate) fn decode_value(value: FieldEncodedValue) -> F { +pub(crate) fn decode_value(value: FieldEncodedValue) -> F { if value.bytes_le[8..].iter().all(|byte| *byte == 0) { let mut bytes = [0u8; 8]; bytes.copy_from_slice(&value.bytes_le[..8]); return F::from_u64(u64::from_le_bytes(bytes)); } - ::from_le_bytes_mod_order(&value.bytes_le) + ::from_bytes_le_reduced(&value.bytes_le) } pub(crate) const fn op(flag: FieldInlineOpFlag) -> FieldInlineOp { diff --git a/crates/jolt-witness/src/witnesses/flags.rs b/crates/jolt-witness/src/witnesses/flags.rs index 195c6c6b16..6a9cc5e238 100644 --- a/crates/jolt-witness/src/witnesses/flags.rs +++ b/crates/jolt-witness/src/witnesses/flags.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use jolt_lookup_tables::{InstructionLookupTable, LookupQuery}; use jolt_program::execution::TraceRow; use jolt_riscv::{ @@ -64,7 +64,7 @@ pub struct LookupTableFlag(pub bool); macro_rules! bool_to_field { ($($name:ident),* $(,)?) => { $(impl ToField for $name { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_bool(self.0) } })* diff --git a/crates/jolt-witness/src/witnesses/increments.rs b/crates/jolt-witness/src/witnesses/increments.rs index f65da3f376..e80f040f45 100644 --- a/crates/jolt-witness/src/witnesses/increments.rs +++ b/crates/jolt-witness/src/witnesses/increments.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use jolt_program::execution::{RamAccess, TraceRow}; use super::{Extract, ToField, WitnessEnv}; @@ -14,7 +14,7 @@ pub struct RdInc(pub i128); pub struct RamInc(pub i128); impl ToField for RdInc { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_i128(self.0) } } @@ -33,7 +33,7 @@ impl Extract for RdInc { } impl ToField for RamInc { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_i128(self.0) } } diff --git a/crates/jolt-witness/src/witnesses/lookups.rs b/crates/jolt-witness/src/witnesses/lookups.rs index 7ea23a23fc..ddc68506a2 100644 --- a/crates/jolt-witness/src/witnesses/lookups.rs +++ b/crates/jolt-witness/src/witnesses/lookups.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use jolt_lookup_tables::{InstructionLookupTable, LookupQuery}; use jolt_program::execution::TraceRow; use jolt_riscv::JoltInstruction; @@ -21,7 +21,7 @@ pub struct LookupIndex(pub u128); pub struct TableIndex(pub Option); impl ToField for LookupOutput { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } @@ -39,7 +39,7 @@ impl Extract for LookupOutput { } impl ToField for LookupIndex { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u128(self.0) } } diff --git a/crates/jolt-witness/src/witnesses/mod.rs b/crates/jolt-witness/src/witnesses/mod.rs index fc5d2d95f6..f64c5c5a92 100644 --- a/crates/jolt-witness/src/witnesses/mod.rs +++ b/crates/jolt-witness/src/witnesses/mod.rs @@ -14,7 +14,7 @@ //! is a function of rows `t` and `t + 1`, with padding semantics at //! `T - 1`) and the environment ([`WitnessEnv`]). -use jolt_field::Field; +use jolt_field::JoltField; use jolt_lookup_tables::JoltLookupQuery; use jolt_program::{execution::TraceRow, preprocess::JoltProgramPreprocessing}; use jolt_riscv::{Flags, JoltInstruction, JoltInstructionKind}; @@ -56,7 +56,7 @@ pub struct WitnessEnv<'a> { /// The field encoding of an atomic witness value. pub trait ToField { - fn to_field(self) -> F; + fn to_field(self) -> F; } /// The single-sourced derivation of one atomic witness from a trace row. diff --git a/crates/jolt-witness/src/witnesses/operands.rs b/crates/jolt-witness/src/witnesses/operands.rs index 8a6d6996c0..2d761eff16 100644 --- a/crates/jolt-witness/src/witnesses/operands.rs +++ b/crates/jolt-witness/src/witnesses/operands.rs @@ -1,6 +1,6 @@ use jolt_field::{ signed::{S128, S64}, - Field, + JoltField, }; use jolt_lookup_tables::LookupQuery; use jolt_program::execution::TraceRow; @@ -35,7 +35,7 @@ pub struct Product(pub S128); pub struct Imm(pub i128); impl ToField for LeftLookupOperand { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } @@ -52,7 +52,7 @@ impl Extract for LeftLookupOperand { } impl ToField for RightLookupOperand { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u128(self.0) } } @@ -69,7 +69,7 @@ impl Extract for RightLookupOperand { } impl ToField for LeftInstructionInput { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } @@ -86,7 +86,7 @@ impl Extract for LeftInstructionInput { } impl ToField for RightInstructionInput { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_i128(self.0) } } @@ -105,7 +105,7 @@ impl Extract for RightInstructionInput { impl ToField for Product { /// The product may exceed `i128`: fall back to the sign/magnitude split /// when the truncated representation does not fit. - fn to_field(self) -> F { + fn to_field(self) -> F { if let Some(value) = self.0.to_i128() { F::from_i128(value) } else { @@ -133,7 +133,7 @@ impl Extract for Product { } impl ToField for Imm { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_i128(self.0) } } diff --git a/crates/jolt-witness/src/witnesses/pc.rs b/crates/jolt-witness/src/witnesses/pc.rs index 2ccb068db4..11465168e9 100644 --- a/crates/jolt-witness/src/witnesses/pc.rs +++ b/crates/jolt-witness/src/witnesses/pc.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use jolt_program::execution::TraceRow; use super::{pc_for_row, row_is_noop, Extract, ToField, WitnessEnv}; @@ -61,7 +61,7 @@ pub struct NextPc(pub u64); pub struct NextUnexpandedPc(pub u64); impl ToField for Pc { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } @@ -77,7 +77,7 @@ impl Extract for Pc { } impl ToField for UnexpandedPc { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } @@ -93,7 +93,7 @@ impl Extract for UnexpandedPc { } impl ToField for NextPc { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } @@ -113,7 +113,7 @@ impl Extract for NextPc { } impl ToField for NextUnexpandedPc { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } diff --git a/crates/jolt-witness/src/witnesses/ram.rs b/crates/jolt-witness/src/witnesses/ram.rs index 54880a861d..bd2aee5f7d 100644 --- a/crates/jolt-witness/src/witnesses/ram.rs +++ b/crates/jolt-witness/src/witnesses/ram.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use jolt_program::execution::{RamAccess, TraceRow}; use super::{Extract, ToField, WitnessEnv}; @@ -37,7 +37,7 @@ pub struct RamHammingWeight(pub bool); pub struct RemappedRamAddress(pub Option); impl ToField for RamAddress { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } @@ -53,7 +53,7 @@ impl Extract for RamAddress { } impl ToField for RamReadValue { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } @@ -73,7 +73,7 @@ impl Extract for RamReadValue { } impl ToField for RamWriteValue { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } @@ -93,7 +93,7 @@ impl Extract for RamWriteValue { } impl ToField for RamHammingWeight { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_bool(self.0) } } diff --git a/crates/jolt-witness/src/witnesses/registers.rs b/crates/jolt-witness/src/witnesses/registers.rs index 0684c7434a..94d8aaeb0c 100644 --- a/crates/jolt-witness/src/witnesses/registers.rs +++ b/crates/jolt-witness/src/witnesses/registers.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::JoltField; use jolt_program::execution::TraceRow; use super::{Extract, ToField, WitnessEnv}; @@ -17,7 +17,7 @@ pub struct Rs2Value(pub u64); pub struct RdWriteValue(pub u64); impl ToField for Rs1Value { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } @@ -33,7 +33,7 @@ impl Extract for Rs1Value { } impl ToField for Rs2Value { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } @@ -49,7 +49,7 @@ impl Extract for Rs2Value { } impl ToField for RdWriteValue { - fn to_field(self) -> F { + fn to_field(self) -> F { F::from_u64(self.0) } } diff --git a/crates/jolt-witness/tests/field_inline_witness.rs b/crates/jolt-witness/tests/field_inline_witness.rs index 73a2e87446..f3233e64a1 100644 --- a/crates/jolt-witness/tests/field_inline_witness.rs +++ b/crates/jolt-witness/tests/field_inline_witness.rs @@ -9,7 +9,7 @@ use jolt_claims::protocols::{ }, jolt::{JoltCommittedPolynomial, JoltOneHotConfig, JoltPolynomialId}, }; -use jolt_field::{Fr, FromPrimitiveInt}; +use jolt_field::{Fr, Ring}; use jolt_program::{ execution::{ JoltProgram, OwnedTrace, RegisterRead, RegisterState, RegisterWrite, TraceOutput, TraceRow, diff --git a/jolt-eval/src/invariant/field_mul_scalar.rs b/jolt-eval/src/invariant/field_mul_scalar.rs index 35d725b09e..51d6d1ffb2 100644 --- a/jolt-eval/src/invariant/field_mul_scalar.rs +++ b/jolt-eval/src/invariant/field_mul_scalar.rs @@ -1,6 +1,6 @@ use arbitrary::{Arbitrary, Unstructured}; -use jolt_field::arkworks::bn254::Fr; -use jolt_field::FromPrimitiveInt; +use jolt_field::Ring; +use jolt_field::{CanonicalEncoding, Fr}; use crate::invariant::{CheckError, Invariant, InvariantViolation}; @@ -19,7 +19,7 @@ impl<'a> Arbitrary<'a> for FieldMulScalarInput { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { let bytes: [u8; 32] = u.arbitrary()?; Ok(Self { - field: Fr::from_le_bytes_mod_order(&bytes), + field: Fr::from_bytes_le_reduced(&bytes), u64_scalar: u.arbitrary()?, i64_scalar: u.arbitrary()?, u128_scalar: u.arbitrary()?, @@ -28,7 +28,7 @@ impl<'a> Arbitrary<'a> for FieldMulScalarInput { } } -/// `Field::mul_{u64,i64,u128,i128}` must agree with the reference +/// `JoltField::mul_{u64,i64,u128,i128}` must agree with the reference /// formula `self * Self::from_*(scalar)` for every input. #[jolt_eval_macros::invariant(Test, Fuzz)] #[derive(Default)] @@ -43,7 +43,7 @@ impl Invariant for FieldMulScalarInvariant { } fn description(&self) -> String { - "The optimized Field::mul_{u64,i64,u128,i128} methods on BN254 Fr \ + "The optimized JoltField::mul_{u64,i64,u128,i128} methods on BN254 Fr \ must produce the same result as the reference `self * Self::from_*(n)`." .to_string() } diff --git a/jolt-eval/src/invariant/transcript_symmetry.rs b/jolt-eval/src/invariant/transcript_symmetry.rs index b9c7f588e8..672a284726 100644 --- a/jolt-eval/src/invariant/transcript_symmetry.rs +++ b/jolt-eval/src/invariant/transcript_symmetry.rs @@ -4,7 +4,7 @@ //! verifier challenges. use arbitrary::{Arbitrary, Unstructured}; -use jolt_field::{CanonicalBytes, Fr as JFr}; +use jolt_field::{CanonicalBytes, CanonicalEncoding, Fr as JFr}; use spongefish::instantiations::{Blake2b512, Keccak}; use jolt_transcript::{prover_transcript, verifier_transcript, BytesMsg, PoseidonSponge}; @@ -61,7 +61,7 @@ fn arb_bytes(u: &mut Unstructured<'_>) -> arbitrary::Result> { fn arb_scalar(u: &mut Unstructured<'_>) -> arbitrary::Result { let bytes: [u8; 32] = u.arbitrary()?; - Ok(JFr::from_le_bytes_mod_order(&bytes)) + Ok(JFr::from_bytes_le_reduced(&bytes)) } fn run_check(input: &Input, build_sponge: impl Fn() -> H) -> Result<(), CheckError> @@ -145,7 +145,7 @@ fn mismatch(what: &str, op_idx: usize) -> CheckError { } fn seed_corpus_shared() -> Vec { - let scalar = JFr::from_le_bytes_mod_order(&[0xABu8; 32]); + let scalar = JFr::from_bytes_le_reduced(&[0xABu8; 32]); let mut mixed_1k = Vec::with_capacity(1000); for i in 0..1000u64 { mixed_1k.push(match i % 5 { diff --git a/jolt-eval/src/objective/performance/field_mul.rs b/jolt-eval/src/objective/performance/field_mul.rs index 99ff669b51..4ef070cefc 100644 --- a/jolt-eval/src/objective/performance/field_mul.rs +++ b/jolt-eval/src/objective/performance/field_mul.rs @@ -1,5 +1,5 @@ -use jolt_field::arkworks::bn254::Fr; -use jolt_field::{FieldCore, FromPrimitiveInt}; +use jolt_field::Fr; +use jolt_field::{Field, Ring}; use crate::objective::{Objective, OptimizationObjective, PerformanceObjective}; @@ -27,7 +27,7 @@ impl Objective for MulU64Objective { } fn description(&self) -> String { - format!("Wall-clock time of Field::mul_u64 ({NUM_ITERS} iterations)") + format!("Wall-clock time of JoltField::mul_u64 ({NUM_ITERS} iterations)") } fn setup(&self) -> Self::Setup { @@ -62,7 +62,7 @@ impl Objective for MulI64Objective { } fn description(&self) -> String { - format!("Wall-clock time of Field::mul_i64 ({NUM_ITERS} iterations)") + format!("Wall-clock time of JoltField::mul_i64 ({NUM_ITERS} iterations)") } fn setup(&self) -> Self::Setup { @@ -97,7 +97,7 @@ impl Objective for MulU128Objective { } fn description(&self) -> String { - format!("Wall-clock time of Field::mul_u128 ({NUM_ITERS} iterations)") + format!("Wall-clock time of JoltField::mul_u128 ({NUM_ITERS} iterations)") } fn setup(&self) -> Self::Setup { @@ -132,7 +132,7 @@ impl Objective for MulI128Objective { } fn description(&self) -> String { - format!("Wall-clock time of Field::mul_i128 ({NUM_ITERS} iterations)") + format!("Wall-clock time of JoltField::mul_i128 ({NUM_ITERS} iterations)") } fn setup(&self) -> Self::Setup { diff --git a/specs/jolt-field-rebuild.md b/specs/jolt-field-rebuild.md index a47e94aa7e..57c1e5cdb4 100644 --- a/specs/jolt-field-rebuild.md +++ b/specs/jolt-field-rebuild.md @@ -273,6 +273,22 @@ Final per-file actuals are recorded in the file-structure table below `engine.rs` (284/550) by the generic-types-over-vocabulary design; recorded at checkpoint 8. +## Replacement-time deviations + +1. **`JoltField` drops the serde bounds** (`Serialize + DeserializeOwned`) + while the temporary `akita` bootstrap edge exists: the pre-cutover + `akita-field` type is foreign, so the orphan rule forbids giving it serde + impls here, and it must satisfy `JoltField` for the akita lanes to build. + This matches the old umbrella (`Field` never carried serde bounds); every + first-party type keeps its `impl_serde_bytes!` impls. Restore the bounds + at the akita cutover when the bootstrap edge is deleted. +2. **`CanonicalEncoding` re-split**: byte surface extracted as a bare + `CanonicalBytes` supertrait (transcript absorption and `NoCommitment` + bind to bytes only; same decision as the baseline's ff5bf9c split). +3. **bn254 `From` impls added** (`from_primitives!`): the old + crate's `Fr`/`Fq` exposed the plain arkworks `From` conversions; 94+ + consumer call sites rely on them. + ## Remaining before replacement 1. **x86-64 runtime validation:** AVX2/AVX-512 packed backends and the diff --git a/tracer/src/instruction/field_inline.rs b/tracer/src/instruction/field_inline.rs index bad3e3895c..64ede973b3 100644 --- a/tracer/src/instruction/field_inline.rs +++ b/tracer/src/instruction/field_inline.rs @@ -3,7 +3,7 @@ reason = "Tracer concrete instruction names mirror generated Jolt instruction constants" )] -use jolt_field::{CanonicalBytes, CanonicalRepr, FieldCore, Fr}; +use jolt_field::{CanonicalBytes, CanonicalEncoding, Field, Fr}; use jolt_program::field_inline::{ FieldEncodedValue, FieldInlineBridge, FieldInlineTraceData, FieldRegisterRead, FieldRegisterWrite, @@ -285,7 +285,7 @@ fn execute_store_to_x( // records both the full `field_value` and the truncated `x_value`, so any constraint // binding this bridge must enforce that `x_value == field_value mod 2^64`. let x_value = decode_field(field_value) - .to_canonical_u64_checked() + .to_u64_checked() .unwrap_or_else(|| { u64::from_le_bytes(field_value.bytes_le[..8].try_into().unwrap_or([0; 8])) }); @@ -329,7 +329,7 @@ fn execute_load_imm( } fn decode_field(value: FieldEncodedValue) -> Fr { - ::from_le_bytes_mod_order(&value.bytes_le) + ::from_bytes_le_reduced(&value.bytes_le) } fn encode_field(value: Fr) -> FieldEncodedValue { From b2e3086ee58f98485a153feab837bedcc88bdecb Mon Sep 17 00:00:00 2001 From: acentelles Date: Sat, 1 Aug 2026 07:09:53 -0400 Subject: [PATCH 31/38] docs(specs): record replacement validation evidence in jolt-field-rebuild Proof bytes verified unchanged: standard-mode muldiv proofs from the pre-replacement build and the replacement branch are byte-identical in all 63,372 bytes under a pinned Dory URS; ZK sizes equal at 65,947. Test battery: 3,901 tests green across workspace default, prover-legacy host/zk/akita, verifier akita fixtures, and solinas-only field lanes; muldiv e2e passes both modes. All clippy lanes and x86 target-feature cross-checks clean. Remaining-work list updated to post-replacement items (x86 runtime validation, bench re-evals, CI SIMD lane, parallel helpers, akita cutover follow-ups). --- specs/jolt-field-rebuild.md | 40 ++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/specs/jolt-field-rebuild.md b/specs/jolt-field-rebuild.md index 57c1e5cdb4..f72c14df7d 100644 --- a/specs/jolt-field-rebuild.md +++ b/specs/jolt-field-rebuild.md @@ -289,7 +289,32 @@ Final per-file actuals are recorded in the file-structure table below crate's `Fr`/`Fq` exposed the plain arkworks `From` conversions; 94+ consumer call sites rely on them. -## Remaining before replacement +## Replacement validation evidence + +The replacement (swap `cf8a66ae3`, consumer rebind `079356e30`) is +validated by the following battery, all green: + +- **Proof bytes unchanged (the hard invariant, verified at the strongest + applicable strength):** a standard-mode `muldiv` proof built from the + pre-replacement commit (`2b800e1ca`, old crate) and one from the + replacement branch, with the Dory URS cache pinned to a shared directory, + are byte-identical in all 63,372 bytes (`cmp` finds no difference). ZK + proofs are randomized (BlindFold), so the applicable check is size + equality: 65,947 bytes on both builds. (Earlier recorded baselines + 63,371/65,946 predate the PR branch's merge of main; the one-byte drift + exists identically on both sides of the replacement.) +- **e2e:** `muldiv` passes in `--features host` and `--features host,zk` + (3/3 each, including the committed-program variants). +- **Test lanes:** workspace default sweep 2,338/2,338; jolt-prover-legacy + host 444/444, zk 480/480, akita 445/445; jolt-verifier + akita,prover-fixtures 84/84; jolt-field solinas-only 110/110. The crate's + own oracle-free suite: 125/125 all-features. +- **Clippy lanes (`--all-targets -- -D warnings`):** host; host,zk; + jolt-verifier akita and akita,prover-fixtures; jolt-prover-legacy akita; + field-inline; plus `+avx2` and `+avx512f,+avx512dq` `cargo check` + cross-compiles to `x86_64-apple-darwin`. + +## Remaining after replacement 1. **x86-64 runtime validation:** AVX2/AVX-512 packed backends and the fp128 portable mul path are `cargo check`-validated with @@ -297,12 +322,13 @@ Final per-file actuals are recorded in the file-structure table below differential suite and the fp128 differentials on real x86-64 hardware. 2. **Bench re-evaluation entries:** rerun the fused deg-4 kernel bench (`benches/ext4_kernels.rs`) on x86-64 before deciding the - `PseudoMersenne` hook overrides stay generic (checkpoint 6 caveat); - thin comparison bench vs baseline for the scalar/packed hot paths. -3. **The replacement PR itself:** rebind consumers to the new trait names, - delete `jolt-field`, re-point the `jolt-field` workspace alias; decide - the fate of the unconsumed parallel helpers (checkpoint 9 audit above); - CI wiring with a target-feature lane so SIMD is not CI-dark. + `PseudoMersenne` hook overrides stay generic (checkpoint 6 caveat). +3. **CI wiring:** a target-feature lane so SIMD is not CI-dark. +4. **Parallel helpers:** still zero consumers after the rebind (the old + crate's `parallel` module also had none); delete `solinas::parallel` + and the `parallel` feature, or wire a consumer. +5. **Akita cutover follow-ups:** delete the `akita` bootstrap edge and + restore `JoltField`'s serde bounds (deviation 1 above). ## Design pillars From a79b894c91782e7c17b51333b80705950c05fcf7 Mon Sep 17 00:00:00 2001 From: acentelles Date: Mon, 3 Aug 2026 22:23:39 -0400 Subject: [PATCH 32/38] fix(ci): repair the five lanes broken by the replacement - check-shared-field-identity.sh: pass --color never to cargo tree; CI's colorized (*) dedup marker defeated the sed strip, making one identity count as two. The dependency graph was always correct. - typos: exclude the golden byte fixtures (hex substrings false-positive). - ext4_kernels bench: compile to a stub without the solinas feature so the bench workflow's --bench '*' succeeds under default features; the print_stdout expectation moves onto the gated module. - fuzz: restore the five fuzz targets deleted with the old crate, rebound to the rebuilt trait names (the fuzz workspace is invisible to workspace clippy and the fuzz CI job cd's into it). - jolt-verifier: drop an unused import only compiled under the prover-fixtures,zk lane. --- crates/jolt-field/Cargo.toml | 1 - crates/jolt-field/benches/ext4_kernels.rs | 256 ++++---- crates/jolt-field/fuzz/.gitignore | 4 + crates/jolt-field/fuzz/Cargo.lock | 565 ++++++++++++++++++ crates/jolt-field/fuzz/Cargo.toml | 40 ++ .../fuzz/fuzz_targets/field_arith.rs | 34 ++ .../fuzz/fuzz_targets/from_bytes.rs | 14 + .../fuzz/fuzz_targets/solinas_field_arith.rs | 40 ++ .../fuzz_targets/wide_accumulator_fmadd.rs | 32 + .../fuzz_targets/wide_accumulator_merge.rs | 39 ++ crates/jolt-field/fuzz/rust-toolchain.toml | 2 + .../tests/statistical_independence/zk.rs | 2 +- scripts/check-shared-field-identity.sh | 2 +- typos.toml | 4 +- 14 files changed, 909 insertions(+), 126 deletions(-) create mode 100644 crates/jolt-field/fuzz/.gitignore create mode 100644 crates/jolt-field/fuzz/Cargo.lock create mode 100644 crates/jolt-field/fuzz/Cargo.toml create mode 100644 crates/jolt-field/fuzz/fuzz_targets/field_arith.rs create mode 100644 crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs create mode 100644 crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs create mode 100644 crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs create mode 100644 crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs create mode 100644 crates/jolt-field/fuzz/rust-toolchain.toml diff --git a/crates/jolt-field/Cargo.toml b/crates/jolt-field/Cargo.toml index d1f4c669f5..7bc2089f58 100644 --- a/crates/jolt-field/Cargo.toml +++ b/crates/jolt-field/Cargo.toml @@ -43,4 +43,3 @@ rand_chacha = { workspace = true } [[bench]] name = "ext4_kernels" harness = false -required-features = ["solinas"] diff --git a/crates/jolt-field/benches/ext4_kernels.rs b/crates/jolt-field/benches/ext4_kernels.rs index 5ad8fa93a3..f0509124e3 100644 --- a/crates/jolt-field/benches/ext4_kernels.rs +++ b/crates/jolt-field/benches/ext4_kernels.rs @@ -14,141 +14,153 @@ //! //! Run: `cargo bench -p jolt-field --features solinas --bench ext4_kernels` -#![expect(clippy::print_stdout, reason = "bench harness: stdout is the report")] +// The harness needs the solinas backend; under other feature sets this +// bench compiles to an empty stub so `cargo bench --bench '*'` succeeds. +#[cfg(feature = "solinas")] +#[expect(clippy::print_stdout, reason = "bench harness: stdout is the report")] +mod harness { + use jolt_field as two; -use jolt_field as two; + use rand::SeedableRng; + use rand_chacha::ChaCha20Rng; + use std::hint::black_box; + use std::time::Instant; -use rand::SeedableRng; -use rand_chacha::ChaCha20Rng; -use std::hint::black_box; -use std::time::Instant; + use two::{CanonicalEncoding, Field, Ring}; -use two::{CanonicalEncoding, Field, Ring}; + type Fp = two::Prime32Offset99; + type E4 = two::FpExt4; -type Fp = two::Prime32Offset99; -type E4 = two::FpExt4; + const N: usize = 1 << 12; + const REPS: usize = 100; + const TRIALS: usize = 7; -const N: usize = 1 << 12; -const REPS: usize = 100; -const TRIALS: usize = 7; + /// Widening product of canonical `Fp32` values, exact in `u64` + /// (`a·b < P² < 2^64`), widened to `u128` for column accumulation. + #[inline(always)] + fn product(a: Fp, b: Fp) -> u128 { + ((a.to_limbs() as u64) * (b.to_limbs() as u64)) as u128 + } -/// Widening product of canonical `Fp32` values, exact in `u64` -/// (`a·b < P² < 2^64`), widened to `u128` for column accumulation. -#[inline(always)] -fn product(a: Fp, b: Fp) -> u128 { - ((a.to_limbs() as u64) * (b.to_limbs() as u64)) as u128 -} + const P: u32 = 4_294_967_197; // 2^32 − 99 -const P: u32 = 4_294_967_197; // 2^32 − 99 - -/// Port of the baseline's fused `Fp32` degree-4 multiply: accumulate the -/// raw products of each output coefficient in a `u128`, reduce once. -/// -/// Bounds (every term `< P² < 2^64`, sums evaluated left to right): -/// `c0 ≤ 7·P² < 2^67`; `c1 ≤ 6·P²`; `c2` has a `P²` bias ≥ the single -/// subtrahend `p33`; `c3` has a `2·P²` bias ≥ `p23 + p32`. No `u128` wrap, -/// biases are multiples of `P`, so results equal the generic schedule's. -#[inline(always)] -fn fused_mul(a: [Fp; 4], b: [Fp; 4]) -> [Fp; 4] { - let [a0, a1, a2, a3] = a; - let [b0, b1, b2, b3] = b; - let msq = (P as u128) * (P as u128); - [ - Fp::from_u128_reduced( - product(a0, b0) + 2 * (product(a1, b1) + product(a2, b2) + product(a3, b3)), - ), - Fp::from_u128_reduced( - product(a0, b1) - + product(a1, b0) - + product(a1, b2) - + product(a2, b1) - + product(a2, b3) - + product(a3, b2), - ), - Fp::from_u128_reduced( - product(a0, b2) - + product(a2, b0) - + product(a1, b1) - + product(a1, b3) - + product(a3, b1) - + msq - - product(a3, b3), - ), - Fp::from_u128_reduced( - product(a0, b3) + product(a3, b0) + product(a1, b2) + product(a2, b1) + 2 * msq - - product(a2, b3) - - product(a3, b2), - ), - ] -} + /// Port of the baseline's fused `Fp32` degree-4 multiply: accumulate the + /// raw products of each output coefficient in a `u128`, reduce once. + /// + /// Bounds (every term `< P² < 2^64`, sums evaluated left to right): + /// `c0 ≤ 7·P² < 2^67`; `c1 ≤ 6·P²`; `c2` has a `P²` bias ≥ the single + /// subtrahend `p33`; `c3` has a `2·P²` bias ≥ `p23 + p32`. No `u128` wrap, + /// biases are multiples of `P`, so results equal the generic schedule's. + #[inline(always)] + fn fused_mul(a: [Fp; 4], b: [Fp; 4]) -> [Fp; 4] { + let [a0, a1, a2, a3] = a; + let [b0, b1, b2, b3] = b; + let msq = (P as u128) * (P as u128); + [ + Fp::from_u128_reduced( + product(a0, b0) + 2 * (product(a1, b1) + product(a2, b2) + product(a3, b3)), + ), + Fp::from_u128_reduced( + product(a0, b1) + + product(a1, b0) + + product(a1, b2) + + product(a2, b1) + + product(a2, b3) + + product(a3, b2), + ), + Fp::from_u128_reduced( + product(a0, b2) + + product(a2, b0) + + product(a1, b1) + + product(a1, b3) + + product(a3, b1) + + msq + - product(a3, b3), + ), + Fp::from_u128_reduced( + product(a0, b3) + product(a3, b0) + product(a1, b2) + product(a2, b1) + 2 * msq + - product(a2, b3) + - product(a3, b2), + ), + ] + } -/// Port of the baseline's fused `Fp32` degree-4 squaring (10 products); -/// same bound structure as [`fused_mul`], every column `< 8·P² < 2^67`. -#[inline(always)] -fn fused_square(a: [Fp; 4]) -> [Fp; 4] { - let [a0, a1, a2, a3] = a; - let msq = (P as u128) * (P as u128); - let a0_square = product(a0, a0); - let a1_square = product(a1, a1); - let a2_square = product(a2, a2); - let a3_square = product(a3, a3); - let a0a1 = product(a0, a1); - let a0a2 = product(a0, a2); - let a0a3 = product(a0, a3); - let a1a2 = product(a1, a2); - let a1a3 = product(a1, a3); - let a2a3 = product(a2, a3); - [ - Fp::from_u128_reduced(a0_square + 2 * (a1_square + a2_square + a3_square)), - Fp::from_u128_reduced(2 * (a0a1 + a1a2 + a2a3)), - Fp::from_u128_reduced(2 * a0a2 + a1_square + 2 * a1a3 + msq - a3_square), - Fp::from_u128_reduced(2 * (a0a3 + a1a2 + msq - a2a3)), - ] -} + /// Port of the baseline's fused `Fp32` degree-4 squaring (10 products); + /// same bound structure as [`fused_mul`], every column `< 8·P² < 2^67`. + #[inline(always)] + fn fused_square(a: [Fp; 4]) -> [Fp; 4] { + let [a0, a1, a2, a3] = a; + let msq = (P as u128) * (P as u128); + let a0_square = product(a0, a0); + let a1_square = product(a1, a1); + let a2_square = product(a2, a2); + let a3_square = product(a3, a3); + let a0a1 = product(a0, a1); + let a0a2 = product(a0, a2); + let a0a3 = product(a0, a3); + let a1a2 = product(a1, a2); + let a1a3 = product(a1, a3); + let a2a3 = product(a2, a3); + [ + Fp::from_u128_reduced(a0_square + 2 * (a1_square + a2_square + a3_square)), + Fp::from_u128_reduced(2 * (a0a1 + a1a2 + a2a3)), + Fp::from_u128_reduced(2 * a0a2 + a1_square + 2 * a1a3 + msq - a3_square), + Fp::from_u128_reduced(2 * (a0a3 + a1a2 + msq - a2a3)), + ] + } -fn measure(inputs: &[T], mut op: impl FnMut(T) -> R) -> f64 { - let mut best = f64::INFINITY; - for _ in 0..TRIALS { - let start = Instant::now(); - for _ in 0..REPS { - for &x in inputs { - let _ = black_box(op(x)); + fn measure(inputs: &[T], mut op: impl FnMut(T) -> R) -> f64 { + let mut best = f64::INFINITY; + for _ in 0..TRIALS { + let start = Instant::now(); + for _ in 0..REPS { + for &x in inputs { + let _ = black_box(op(x)); + } } + let ns = start.elapsed().as_nanos() as f64 / (REPS * inputs.len()) as f64; + best = best.min(ns); } - let ns = start.elapsed().as_nanos() as f64 / (REPS * inputs.len()) as f64; - best = best.min(ns); + best } - best -} -fn main() { - let mut rng = ChaCha20Rng::seed_from_u64(0xE4B_E4B); - let pairs: Vec<(E4, E4)> = (0..N) - .map(|_| (E4::random(&mut rng), E4::random(&mut rng))) - .collect(); - // Sanity: the fused port agrees with the wired generic path. - for (a, b) in pairs.iter().take(64) { - assert_eq!((*a * *b).coeffs, fused_mul(a.coeffs, b.coeffs)); - assert_eq!(Ring::square(a).coeffs, fused_square(a.coeffs)); + pub(crate) fn run() { + let mut rng = ChaCha20Rng::seed_from_u64(0xE4B_E4B); + let pairs: Vec<(E4, E4)> = (0..N) + .map(|_| (E4::random(&mut rng), E4::random(&mut rng))) + .collect(); + // Sanity: the fused port agrees with the wired generic path. + for (a, b) in pairs.iter().take(64) { + assert_eq!((*a * *b).coeffs, fused_mul(a.coeffs, b.coeffs)); + assert_eq!(Ring::square(a).coeffs, fused_square(a.coeffs)); + } + + let generic_mul_ns = measure(&pairs, |(a, b)| (a * b).coeffs[0]); + let fused_mul_ns = measure(&pairs, |(a, b)| fused_mul(a.coeffs, b.coeffs)[0]); + + let generic_sq_ns = measure(&pairs, |(a, _)| Ring::square(&a).coeffs[0]); + let fused_sq_ns = measure(&pairs, |(a, _)| fused_square(a.coeffs)[0]); + + println!("ext4 over Prime32Offset99, {N} elements x {REPS} reps, best of {TRIALS}"); + println!(" mul generic default (wired): {generic_mul_ns:7.2} ns/op"); + println!(" mul fused port (dropped) : {fused_mul_ns:7.2} ns/op"); + println!( + " mul fused/generic : {:.2}x", + fused_mul_ns / generic_mul_ns + ); + println!(" square generic default (wired): {generic_sq_ns:7.2} ns/op"); + println!(" square fused port (dropped) : {fused_sq_ns:7.2} ns/op"); + println!( + " square fused/generic : {:.2}x", + fused_sq_ns / generic_sq_ns + ); } +} - let generic_mul_ns = measure(&pairs, |(a, b)| (a * b).coeffs[0]); - let fused_mul_ns = measure(&pairs, |(a, b)| fused_mul(a.coeffs, b.coeffs)[0]); - - let generic_sq_ns = measure(&pairs, |(a, _)| Ring::square(&a).coeffs[0]); - let fused_sq_ns = measure(&pairs, |(a, _)| fused_square(a.coeffs)[0]); - - println!("ext4 over Prime32Offset99, {N} elements x {REPS} reps, best of {TRIALS}"); - println!(" mul generic default (wired): {generic_mul_ns:7.2} ns/op"); - println!(" mul fused port (dropped) : {fused_mul_ns:7.2} ns/op"); - println!( - " mul fused/generic : {:.2}x", - fused_mul_ns / generic_mul_ns - ); - println!(" square generic default (wired): {generic_sq_ns:7.2} ns/op"); - println!(" square fused port (dropped) : {fused_sq_ns:7.2} ns/op"); - println!( - " square fused/generic : {:.2}x", - fused_sq_ns / generic_sq_ns - ); +#[cfg(feature = "solinas")] +fn main() { + harness::run(); } + +#[cfg(not(feature = "solinas"))] +fn main() {} diff --git a/crates/jolt-field/fuzz/.gitignore b/crates/jolt-field/fuzz/.gitignore new file mode 100644 index 0000000000..fe68c971b7 --- /dev/null +++ b/crates/jolt-field/fuzz/.gitignore @@ -0,0 +1,4 @@ +target/ +corpus/ +artifacts/ +coverage/ diff --git a/crates/jolt-field/fuzz/Cargo.lock b/crates/jolt-field/fuzz/Cargo.lock new file mode 100644 index 0000000000..fd37e174c2 --- /dev/null +++ b/crates/jolt-field/fuzz/Cargo.lock @@ -0,0 +1,565 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown", + "itertools", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest", + "educe", + "itertools", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "crypto-common", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "jolt-field" +version = "0.1.0" +dependencies = [ + "ark-bn254", + "ark-ff", + "ark-serialize", + "num-traits", + "rand_core", + "serde", + "thiserror", +] + +[[package]] +name = "jolt-field-fuzz" +version = "0.0.0" +dependencies = [ + "jolt-field", + "libfuzzer-sys", + "num-traits", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/crates/jolt-field/fuzz/Cargo.toml b/crates/jolt-field/fuzz/Cargo.toml new file mode 100644 index 0000000000..97af7c75a8 --- /dev/null +++ b/crates/jolt-field/fuzz/Cargo.toml @@ -0,0 +1,40 @@ +[workspace] + +[package] +name = "jolt-field-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +jolt-field = { path = "..", default-features = false, features = ["bn254", "solinas"] } +num-traits = "0.2" + +[[bin]] +name = "from_bytes" +path = "fuzz_targets/from_bytes.rs" +doc = false + +[[bin]] +name = "field_arith" +path = "fuzz_targets/field_arith.rs" +doc = false + +[[bin]] +name = "wide_accumulator_fmadd" +path = "fuzz_targets/wide_accumulator_fmadd.rs" +doc = false + +[[bin]] +name = "wide_accumulator_merge" +path = "fuzz_targets/wide_accumulator_merge.rs" +doc = false + +[[bin]] +name = "solinas_field_arith" +path = "fuzz_targets/solinas_field_arith.rs" +doc = false diff --git a/crates/jolt-field/fuzz/fuzz_targets/field_arith.rs b/crates/jolt-field/fuzz/fuzz_targets/field_arith.rs new file mode 100644 index 0000000000..f8c56df400 --- /dev/null +++ b/crates/jolt-field/fuzz/fuzz_targets/field_arith.rs @@ -0,0 +1,34 @@ +#![no_main] +use jolt_field::{Fr, Ring, Field, CanonicalEncoding}; +use libfuzzer_sys::fuzz_target; +use num_traits::Zero; + +fuzz_target!(|data: &[u8]| { + if data.len() < 64 { + return; + } + let a = ::from_bytes_le_reduced(&data[..32]); + let b = ::from_bytes_le_reduced(&data[32..64]); + + // Arithmetic operations must not panic + let sum = a + b; + let diff = a - b; + let prod = a * b; + let sq = a * a; + + // (a + b) - b == a + assert_eq!(sum - b, a); + // (a - b) + b == a + assert_eq!(diff + b, a); + // a * 0 == 0 + assert!((a * Fr::zero()).is_zero()); + + // inverse must not panic + if !a.is_zero() { + let inv = a.inverse().expect("nonzero element must have inverse"); + assert_eq!(a * inv, Fr::from_u64(1)); + } + + // Prevent optimizing away + let _ = (prod, sq); +}); diff --git a/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs b/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs new file mode 100644 index 0000000000..7d7f0ce3c7 --- /dev/null +++ b/crates/jolt-field/fuzz/fuzz_targets/from_bytes.rs @@ -0,0 +1,14 @@ +#![no_main] +use jolt_field::{CanonicalBytes, CanonicalEncoding, Fr}; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // from_bytes should never panic on arbitrary input + let a = ::from_bytes_le_reduced(data); + + // Round-trip: from_bytes → to_bytes → from_bytes must be stable + let bytes = a.to_bytes_le_vec(); + let b = ::from_bytes_le_reduced(&bytes); + let bytes2 = b.to_bytes_le_vec(); + assert_eq!(bytes, bytes2, "from_bytes round-trip is not stable"); +}); diff --git a/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs b/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs new file mode 100644 index 0000000000..5f4df7ea84 --- /dev/null +++ b/crates/jolt-field/fuzz/fuzz_targets/solinas_field_arith.rs @@ -0,0 +1,40 @@ +#![no_main] + +use jolt_field::{ + FpExt4, Ring, Field, Prime128Offset275, Prime31Offset19, CanonicalEncoding, +}; +use libfuzzer_sys::fuzz_target; +use num_traits::Zero; + +fuzz_target!(|data: &[u8]| { + if data.len() < 64 { + return; + } + + let a31 = Prime31Offset19::from_bytes_le_reduced(&data[..16]); + let b31 = Prime31Offset19::from_bytes_le_reduced(&data[16..32]); + assert_eq!((a31 + b31) - b31, a31); + assert_eq!((a31 - b31) + b31, a31); + if !a31.is_zero() { + assert_eq!(a31 * a31.inverse().unwrap(), Prime31Offset19::from_u64(1)); + } + + let a128 = Prime128Offset275::from_bytes_le_reduced(&data[..32]); + let b128 = Prime128Offset275::from_bytes_le_reduced(&data[32..64]); + assert_eq!((a128 + b128) - b128, a128); + assert_eq!((a128 - b128) + b128, a128); + if !a128.is_zero() { + assert_eq!( + a128 * a128.inverse().unwrap(), + Prime128Offset275::from_u64(1) + ); + } + + let extension = FpExt4::new([a31, b31, a31 + b31, a31 - b31]); + if !extension.is_zero() { + assert_eq!( + extension * extension.inverse().unwrap(), + FpExt4::::from_u64(1) + ); + } +}); diff --git a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs new file mode 100644 index 0000000000..d43c3053f4 --- /dev/null +++ b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_fmadd.rs @@ -0,0 +1,32 @@ +#![no_main] +use jolt_field::{Accumulator, Fr, CanonicalEncoding, WideAccumulator}; +use libfuzzer_sys::fuzz_target; +use num_traits::Zero; + +fuzz_target!(|data: &[u8]| { + // Each pair of field elements needs 64 bytes (2 x 32-byte chunks). + // Silently skip inputs that don't contain at least one complete pair. + if data.len() < 64 { + return; + } + + let mut acc = WideAccumulator::default(); + let mut naive_sum = Fr::zero(); + + let pairs = data.len() / 64; + for i in 0..pairs { + let offset = i * 64; + let a = ::from_bytes_le_reduced(&data[offset..offset + 32]); + let b = + ::from_bytes_le_reduced(&data[offset + 32..offset + 64]); + + acc.fmadd(a, b); + naive_sum += a * b; + } + + assert_eq!( + acc.reduce(), + naive_sum, + "WideAccumulator diverged from naive field arithmetic after {pairs} fmadd calls" + ); +}); diff --git a/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs new file mode 100644 index 0000000000..6772d0052d --- /dev/null +++ b/crates/jolt-field/fuzz/fuzz_targets/wide_accumulator_merge.rs @@ -0,0 +1,39 @@ +#![no_main] +use jolt_field::{Accumulator, Fr, CanonicalEncoding, WideAccumulator}; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // Need at least two pairs (128 bytes) so each half gets at least one. + if data.len() < 128 { + return; + } + + let pairs = data.len() / 64; + let split = pairs / 2; + + let mut acc1 = WideAccumulator::default(); + let mut acc2 = WideAccumulator::default(); + let mut acc_all = WideAccumulator::default(); + + for i in 0..pairs { + let offset = i * 64; + let a = ::from_bytes_le_reduced(&data[offset..offset + 32]); + let b = + ::from_bytes_le_reduced(&data[offset + 32..offset + 64]); + + if i < split { + acc1.fmadd(a, b); + } else { + acc2.fmadd(a, b); + } + acc_all.fmadd(a, b); + } + + acc1.merge(acc2); + + assert_eq!( + acc1.reduce(), + acc_all.reduce(), + "merge+reduce diverged from single-accumulator reduce ({pairs} pairs, split at {split})" + ); +}); diff --git a/crates/jolt-field/fuzz/rust-toolchain.toml b/crates/jolt-field/fuzz/rust-toolchain.toml new file mode 100644 index 0000000000..5d56faf9ae --- /dev/null +++ b/crates/jolt-field/fuzz/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/crates/jolt-verifier/tests/statistical_independence/zk.rs b/crates/jolt-verifier/tests/statistical_independence/zk.rs index 3b888ab137..b6b79782de 100644 --- a/crates/jolt-verifier/tests/statistical_independence/zk.rs +++ b/crates/jolt-verifier/tests/statistical_independence/zk.rs @@ -14,7 +14,7 @@ use std::collections::BTreeMap; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] use ark_serialize::CanonicalSerialize; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_field::{CanonicalBytes, CanonicalEncoding, Fr}; +use jolt_field::{CanonicalBytes, Fr}; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] use jolt_sumcheck::SumcheckProof; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] diff --git a/scripts/check-shared-field-identity.sh b/scripts/check-shared-field-identity.sh index cae2e6e5c6..6fc76d9e5d 100755 --- a/scripts/check-shared-field-identity.sh +++ b/scripts/check-shared-field-identity.sh @@ -10,7 +10,7 @@ set -euo pipefail # local path. The final migration PR replaces this check with one that rejects # every `akita-field` identity. -tree="$(cargo tree --workspace --edges normal,build --prefix none)" +tree="$(cargo tree --workspace --edges normal,build --prefix none --color never)" jolt_identities="$( grep '^jolt-field v' <<<"$tree" \ diff --git a/typos.toml b/typos.toml index c93b7e99ec..bf64535e35 100644 --- a/typos.toml +++ b/typos.toml @@ -1,5 +1,7 @@ [files] -extend-exclude = ["patches/*.patch"] +# golden_bytes.rs is hex fixture data (byte-encoding pins); hex substrings +# trigger false positives ("ede", "ba"). +extend-exclude = ["patches/*.patch", "crates/jolt-field/tests/golden_bytes.rs"] [default.extend-words] "groth" = "groth" From 7a6337c31658a2632840dd4a8181190db8dd263f Mon Sep 17 00:00:00 2001 From: acentelles Date: Tue, 4 Aug 2026 06:05:19 -0400 Subject: [PATCH 33/38] fix(ci): taplo-format the restored fuzz manifest --- crates/jolt-field/fuzz/Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/jolt-field/fuzz/Cargo.toml b/crates/jolt-field/fuzz/Cargo.toml index 97af7c75a8..d6651ece1b 100644 --- a/crates/jolt-field/fuzz/Cargo.toml +++ b/crates/jolt-field/fuzz/Cargo.toml @@ -11,7 +11,10 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" -jolt-field = { path = "..", default-features = false, features = ["bn254", "solinas"] } +jolt-field = { path = "..", default-features = false, features = [ + "bn254", + "solinas", +] } num-traits = "0.2" [[bin]] From 2e75e666229824c1b4b59bb066ff9708939721f5 Mon Sep 17 00:00:00 2001 From: acentelles Date: Tue, 4 Aug 2026 06:21:01 -0400 Subject: [PATCH 34/38] fix(ci): feature-gate golden-byte fixtures for the shared-field matrix lanes The BN254 fixture constants and shared helpers compiled as dead code under solinas-only and backend-free lanes, which build with -Dwarnings in CI. --- crates/jolt-field/tests/golden_bytes.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/jolt-field/tests/golden_bytes.rs b/crates/jolt-field/tests/golden_bytes.rs index 3a051e05a2..600be7ba5e 100644 --- a/crates/jolt-field/tests/golden_bytes.rs +++ b/crates/jolt-field/tests/golden_bytes.rs @@ -24,8 +24,10 @@ use jolt_field as two; +#[cfg(any(feature = "bn254", feature = "solinas"))] use two::CanonicalEncoding; +#[cfg(any(feature = "bn254", feature = "solinas"))] fn unhex(s: &str) -> Vec { (0..s.len()) .step_by(2) @@ -35,6 +37,7 @@ fn unhex(s: &str) -> Vec { /// Element from the reducing decode; canonical bytes and bincode wire must /// equal the fixture, and the checked decode must round-trip. +#[cfg(any(feature = "bn254", feature = "solinas"))] fn check_prime_rows(rows: &[(&str, &str)]) where F: CanonicalEncoding @@ -61,6 +64,7 @@ where } } +#[cfg(feature = "bn254")] const FIX_BN254_FR: &[(&str, &str)] = &[ ( "0000000000000000000000000000000000000000000000000000000000000000", @@ -119,6 +123,7 @@ const FIX_BN254_FR: &[(&str, &str)] = &[ "25ebe18ad76cbb25cc7e978dea211621cab09f6b34135a653ef1654596ea1006", ), ]; +#[cfg(feature = "bn254")] const FIX_BN254_FQ: &[(&str, &str)] = &[ ( "0000000000000000000000000000000000000000000000000000000000000000", @@ -177,6 +182,7 @@ const FIX_BN254_FQ: &[(&str, &str)] = &[ "dfed64a254d67c2dd024df9ea19fc8b1c9b09f6b34135a653ef1654596ea1006", ), ]; +#[cfg(feature = "bn254")] const FIX_BN254_FR_CHALLENGE: &[(&str, &str)] = &[ ( "00000000000000000000000000000000", @@ -211,6 +217,7 @@ const FIX_BN254_FR_CHALLENGE: &[(&str, &str)] = &[ "f8c2174ea5e97eb646b7b0fd9a6a047dca24236aa1a61e02e964d8aaa051c926", ), ]; +#[cfg(feature = "bn254")] const FIX_BN254_FR_SCALAR_CHALLENGE: &[(&str, &str)] = &[ ( "0000000000000000000000000000000000000000000000000000000000000000", @@ -245,6 +252,7 @@ const FIX_BN254_FR_SCALAR_CHALLENGE: &[(&str, &str)] = &[ "cb2d1d9856e14f1fef75479b49d2286e727e157be85dbcfabeaa9221a0822413", ), ]; +#[cfg(feature = "bn254")] const FIX_BN254_FQ_CHALLENGE: &[(&str, &str)] = &[ ( "00000000000000000000000000000000", @@ -279,6 +287,7 @@ const FIX_BN254_FQ_CHALLENGE: &[(&str, &str)] = &[ "00000000000000000000000000000000004ed4e1b8a4ad0fda75893f75f9e30b", ), ]; +#[cfg(feature = "bn254")] const FIX_BN254_FQ_SCALAR_CHALLENGE: &[(&str, &str)] = &[ ( "0000000000000000000000000000000000000000000000000000000000000000", From d5ee0d5059eb1db1f3ffdfafa5e0aa4c124f6c9f Mon Sep 17 00:00:00 2001 From: acentelles Date: Tue, 4 Aug 2026 06:22:37 -0400 Subject: [PATCH 35/38] fix(ci): gate the golden-bytes fixture file at file level for backend-free builds --- crates/jolt-field/tests/golden_bytes.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/jolt-field/tests/golden_bytes.rs b/crates/jolt-field/tests/golden_bytes.rs index 600be7ba5e..f2806eacb6 100644 --- a/crates/jolt-field/tests/golden_bytes.rs +++ b/crates/jolt-field/tests/golden_bytes.rs @@ -21,13 +21,14 @@ //! Extension rows are `(canonical coefficients, bincode wire hex)`. #![expect(clippy::unwrap_used, reason = "test code")] +// The whole file is backend fixture data; without a backend there is nothing +// to pin and every item would be dead code under -Dwarnings. +#![cfg(any(feature = "bn254", feature = "solinas"))] use jolt_field as two; -#[cfg(any(feature = "bn254", feature = "solinas"))] use two::CanonicalEncoding; -#[cfg(any(feature = "bn254", feature = "solinas"))] fn unhex(s: &str) -> Vec { (0..s.len()) .step_by(2) @@ -37,7 +38,6 @@ fn unhex(s: &str) -> Vec { /// Element from the reducing decode; canonical bytes and bincode wire must /// equal the fixture, and the checked decode must round-trip. -#[cfg(any(feature = "bn254", feature = "solinas"))] fn check_prime_rows(rows: &[(&str, &str)]) where F: CanonicalEncoding @@ -64,7 +64,6 @@ where } } -#[cfg(feature = "bn254")] const FIX_BN254_FR: &[(&str, &str)] = &[ ( "0000000000000000000000000000000000000000000000000000000000000000", @@ -123,7 +122,6 @@ const FIX_BN254_FR: &[(&str, &str)] = &[ "25ebe18ad76cbb25cc7e978dea211621cab09f6b34135a653ef1654596ea1006", ), ]; -#[cfg(feature = "bn254")] const FIX_BN254_FQ: &[(&str, &str)] = &[ ( "0000000000000000000000000000000000000000000000000000000000000000", @@ -182,7 +180,6 @@ const FIX_BN254_FQ: &[(&str, &str)] = &[ "dfed64a254d67c2dd024df9ea19fc8b1c9b09f6b34135a653ef1654596ea1006", ), ]; -#[cfg(feature = "bn254")] const FIX_BN254_FR_CHALLENGE: &[(&str, &str)] = &[ ( "00000000000000000000000000000000", @@ -217,7 +214,6 @@ const FIX_BN254_FR_CHALLENGE: &[(&str, &str)] = &[ "f8c2174ea5e97eb646b7b0fd9a6a047dca24236aa1a61e02e964d8aaa051c926", ), ]; -#[cfg(feature = "bn254")] const FIX_BN254_FR_SCALAR_CHALLENGE: &[(&str, &str)] = &[ ( "0000000000000000000000000000000000000000000000000000000000000000", @@ -252,7 +248,6 @@ const FIX_BN254_FR_SCALAR_CHALLENGE: &[(&str, &str)] = &[ "cb2d1d9856e14f1fef75479b49d2286e727e157be85dbcfabeaa9221a0822413", ), ]; -#[cfg(feature = "bn254")] const FIX_BN254_FQ_CHALLENGE: &[(&str, &str)] = &[ ( "00000000000000000000000000000000", @@ -287,7 +282,6 @@ const FIX_BN254_FQ_CHALLENGE: &[(&str, &str)] = &[ "00000000000000000000000000000000004ed4e1b8a4ad0fda75893f75f9e30b", ), ]; -#[cfg(feature = "bn254")] const FIX_BN254_FQ_SCALAR_CHALLENGE: &[(&str, &str)] = &[ ( "0000000000000000000000000000000000000000000000000000000000000000", From a6bbd6244af14f11ccf960f03241d5697f9eeb3a Mon Sep 17 00:00:00 2001 From: acentelles Date: Tue, 4 Aug 2026 06:26:04 -0400 Subject: [PATCH 36/38] fix(ci): per-backend gates on golden-byte fixtures alongside the file-level gate The file-level cfg covers backend-free builds; the BN254 fixture constants additionally need their own gate for the solinas-only lane. All four matrix lanes verified locally with -Dwarnings. --- crates/jolt-field/tests/golden_bytes.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/jolt-field/tests/golden_bytes.rs b/crates/jolt-field/tests/golden_bytes.rs index f2806eacb6..adc72b49eb 100644 --- a/crates/jolt-field/tests/golden_bytes.rs +++ b/crates/jolt-field/tests/golden_bytes.rs @@ -64,6 +64,7 @@ where } } +#[cfg(feature = "bn254")] const FIX_BN254_FR: &[(&str, &str)] = &[ ( "0000000000000000000000000000000000000000000000000000000000000000", @@ -122,6 +123,7 @@ const FIX_BN254_FR: &[(&str, &str)] = &[ "25ebe18ad76cbb25cc7e978dea211621cab09f6b34135a653ef1654596ea1006", ), ]; +#[cfg(feature = "bn254")] const FIX_BN254_FQ: &[(&str, &str)] = &[ ( "0000000000000000000000000000000000000000000000000000000000000000", @@ -180,6 +182,7 @@ const FIX_BN254_FQ: &[(&str, &str)] = &[ "dfed64a254d67c2dd024df9ea19fc8b1c9b09f6b34135a653ef1654596ea1006", ), ]; +#[cfg(feature = "bn254")] const FIX_BN254_FR_CHALLENGE: &[(&str, &str)] = &[ ( "00000000000000000000000000000000", @@ -214,6 +217,7 @@ const FIX_BN254_FR_CHALLENGE: &[(&str, &str)] = &[ "f8c2174ea5e97eb646b7b0fd9a6a047dca24236aa1a61e02e964d8aaa051c926", ), ]; +#[cfg(feature = "bn254")] const FIX_BN254_FR_SCALAR_CHALLENGE: &[(&str, &str)] = &[ ( "0000000000000000000000000000000000000000000000000000000000000000", @@ -248,6 +252,7 @@ const FIX_BN254_FR_SCALAR_CHALLENGE: &[(&str, &str)] = &[ "cb2d1d9856e14f1fef75479b49d2286e727e157be85dbcfabeaa9221a0822413", ), ]; +#[cfg(feature = "bn254")] const FIX_BN254_FQ_CHALLENGE: &[(&str, &str)] = &[ ( "00000000000000000000000000000000", @@ -282,6 +287,7 @@ const FIX_BN254_FQ_CHALLENGE: &[(&str, &str)] = &[ "00000000000000000000000000000000004ed4e1b8a4ad0fda75893f75f9e30b", ), ]; +#[cfg(feature = "bn254")] const FIX_BN254_FQ_SCALAR_CHALLENGE: &[(&str, &str)] = &[ ( "0000000000000000000000000000000000000000000000000000000000000000", From faefab882bd65da9f63709cbfbeec8cc2489bba6 Mon Sep 17 00:00:00 2001 From: acentelles Date: Tue, 4 Aug 2026 08:50:55 -0400 Subject: [PATCH 37/38] fix(ci): structural shared-field identity check via cargo metadata Upgrade from the --color never mitigation to rendering-independent package-ID parsing (requested in Akita #307 review: prefer cargo metadata over cargo tree text). CARGO_TERM_COLOR=always is forced inside the script as a permanent regression guard for the colorized (*) marker bug. --- scripts/check-shared-field-identity.sh | 29 ++++++++++++-------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/scripts/check-shared-field-identity.sh b/scripts/check-shared-field-identity.sh index 6fc76d9e5d..1589476b77 100755 --- a/scripts/check-shared-field-identity.sh +++ b/scripts/check-shared-field-identity.sh @@ -9,15 +9,17 @@ set -euo pipefail # `akita` feature, but only from Jolt's immutable Akita Git pin, never from a # local path. The final migration PR replaces this check with one that rejects # every `akita-field` identity. +# +# Structural check over `cargo metadata` package IDs: immune to `cargo tree` +# rendering (CARGO_TERM_COLOR=always colorizes the `(*)` dedup marker, which +# broke the previous text parse). The worst-case color environment is forced +# below as a permanent regression guard. +export CARGO_TERM_COLOR=always -tree="$(cargo tree --workspace --edges normal,build --prefix none --color never)" +metadata="$(cargo metadata --format-version 1 --locked)" -jolt_identities="$( - grep '^jolt-field v' <<<"$tree" \ - | sed 's/ (\*)$//' \ - | sort -u -)" -jolt_count="$(grep -c '^jolt-field v' <<<"$jolt_identities" || true)" +jolt_identities="$(jq -r '.packages[] | select(.name == "jolt-field") | .id' <<<"$metadata" | sort -u)" +jolt_count="$(grep -c . <<<"$jolt_identities" || true)" if [[ "$jolt_count" -ne 1 ]]; then echo "error: expected exactly one jolt-field package identity, found $jolt_count" >&2 @@ -25,22 +27,17 @@ if [[ "$jolt_count" -ne 1 ]]; then exit 1 fi -akita_identities="$( - { grep '^akita-field v' <<<"$tree" || true; } \ - | sed 's/ (\*)$//' \ - | sort -u -)" - -if [[ -n "$akita_identities" ]]; then - akita_count="$(grep -c '^akita-field v' <<<"$akita_identities" || true)" +akita_identities="$(jq -r '.packages[] | select(.name == "akita-field") | .id' <<<"$metadata" | sort -u)" +akita_count="$(grep -c . <<<"$akita_identities" || true)" +if [[ "$akita_count" -gt 0 ]]; then if [[ "$akita_count" -ne 1 ]]; then echo "error: expected at most one bootstrap akita-field identity, found $akita_count" >&2 printf '%s\n' "$akita_identities" >&2 exit 1 fi - if ! grep -q 'https://github.com/LayerZero-Labs/akita' <<<"$akita_identities"; then + if ! grep -q 'github.com/LayerZero-Labs/akita' <<<"$akita_identities"; then echo "error: bootstrap akita-field must resolve from the pinned Akita Git source" >&2 printf '%s\n' "$akita_identities" >&2 exit 1 From eb93a031027ee735f0827edb4539e971ab5fcc0b Mon Sep 17 00:00:00 2001 From: acentelles Date: Thu, 6 Aug 2026 09:42:13 -0400 Subject: [PATCH 38/38] fix(field): exact-uniform canonical rejection sampling Port the upstream Akita sampling change (LayerZero-Labs/akita@03f29087) to the shared Solinas backend: Field::random on Fp32, Fp64, and Fp128 now samples through sample_uniform_below, which reads exactly ceil(modulus_bits / 8) little-endian bytes per attempt, clears unused high bits, and rejects non-canonical candidates. The byte-consumption contract is deterministic for a fixed RngCore stream and documented on Field::random; extension fields sample base coefficients independently through the same contract. Replaces the previous biased single-reduction paths on the word fields and aligns the differential-test oracle with the new contract. --- crates/jolt-field/src/algebra.rs | 8 +- crates/jolt-field/src/solinas/fp128.rs | 13 +- crates/jolt-field/src/solinas/mod.rs | 132 ++++++++++++++++++ crates/jolt-field/src/solinas/word.rs | 8 +- .../tests/solinas_words_differential.rs | 23 ++- 5 files changed, 161 insertions(+), 23 deletions(-) diff --git a/crates/jolt-field/src/algebra.rs b/crates/jolt-field/src/algebra.rs index 129c45eb64..eef395bbe3 100644 --- a/crates/jolt-field/src/algebra.rs +++ b/crates/jolt-field/src/algebra.rs @@ -170,7 +170,13 @@ pub trait Field: Ring { self.inverse().unwrap_or_else(Self::zero) } - /// Samples a random element (RNG-backed, for tests and witnesses). + /// Samples an exactly uniform element using canonical rejection sampling. + /// + /// Prime fields consume the minimum whole-byte candidate width covering + /// the modulus, clear unused high bits, and reject candidates outside the + /// canonical range. This byte-consumption contract is deterministic for a + /// fixed [`RngCore`] stream. Extension fields sample their base + /// coefficients independently through the same contract. fn random(rng: &mut R) -> Self; /// The multiplicative inverse of two. diff --git a/crates/jolt-field/src/solinas/fp128.rs b/crates/jolt-field/src/solinas/fp128.rs index a5e67b6598..d4f3e86dcd 100644 --- a/crates/jolt-field/src/solinas/fp128.rs +++ b/crates/jolt-field/src/solinas/fp128.rs @@ -686,17 +686,12 @@ impl Field for Fp128

{ Self(split(join(candidate.0) & mask)) } - /// Rejection sampling: draws `(lo, hi)` until the value is canonical. - /// The rejection probability is `C / 2^128 < 2^-96` per draw. + /// Canonical rejection sampling: each attempt reads exactly 16 + /// little-endian bytes and rejects non-canonical candidates (probability + /// `C / 2^128 < 2^-96` per draw). #[inline(always)] fn random(rng: &mut R) -> Self { - loop { - let lo = rng.next_u64(); - let hi = rng.next_u64(); - if join(pack(lo, hi)) < P { - return Self(pack(lo, hi)); - } - } + Self(split(super::sample_uniform_below(rng, P, u128::BITS))) } /// Halving via shift: `(x + (x odd)·p) / 2`, computed as diff --git a/crates/jolt-field/src/solinas/mod.rs b/crates/jolt-field/src/solinas/mod.rs index c116b25ae2..6c74c3d08f 100644 --- a/crates/jolt-field/src/solinas/mod.rs +++ b/crates/jolt-field/src/solinas/mod.rs @@ -70,6 +70,37 @@ const fn pm(bits: u32, offset: u128) -> u128 { } } +/// Sample uniformly from `[0, modulus)` with canonical byte consumption. +/// +/// `modulus_bits` is the significant bit length of `modulus`. Each attempt +/// reads exactly `ceil(modulus_bits / 8)` little-endian bytes, clears unused +/// high bits, and rejects candidates greater than or equal to `modulus`. This +/// byte-consumption contract is deterministic for a fixed +/// [`rand_core::RngCore`] stream. +#[inline] +pub(crate) fn sample_uniform_below( + rng: &mut R, + modulus: u128, + modulus_bits: u32, +) -> u128 { + debug_assert!(modulus > 0); + debug_assert_eq!(modulus_bits, u128::BITS - modulus.leading_zeros()); + let byte_len = modulus_bits.div_ceil(8) as usize; + let mask = if modulus_bits == u128::BITS { + u128::MAX + } else { + (1u128 << modulus_bits) - 1 + }; + loop { + let mut bytes = [0u8; 16]; + rng.fill_bytes(&mut bytes[..byte_len]); + let candidate = u128::from_le_bytes(bytes) & mask; + if candidate < modulus { + return candidate; + } + } +} + const fn spec(bits: u32, offset: u16) -> PrimeOffsetSpec { PrimeOffsetSpec { bits, @@ -162,3 +193,104 @@ pub(crate) fn reduce_le_bytes_mod_order(bytes: &[u8]) -> F { acc * base + F::from_u64(byte as u64) }) } + +#[cfg(test)] +mod sampling_tests { + use super::{pm, sample_uniform_below, Prime128OffsetA7F7, Prime32Offset99, Prime64Offset59}; + use crate::Field; + use rand_core::{Error, RngCore}; + + struct ScriptedRng { + bytes: Vec, + cursor: usize, + } + + impl ScriptedRng { + fn new(bytes: Vec) -> Self { + Self { bytes, cursor: 0 } + } + } + + impl RngCore for ScriptedRng { + fn next_u32(&mut self) -> u32 { + let mut bytes = [0u8; 4]; + self.fill_bytes(&mut bytes); + u32::from_le_bytes(bytes) + } + + fn next_u64(&mut self) -> u64 { + let mut bytes = [0u8; 8]; + self.fill_bytes(&mut bytes); + u64::from_le_bytes(bytes) + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + let end = self.cursor + dest.len(); + dest.copy_from_slice(&self.bytes[self.cursor..end]); + self.cursor = end; + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> { + self.fill_bytes(dest); + Ok(()) + } + } + + #[test] + fn rejection_consumes_candidates_and_resumes_at_the_cursor() { + let mut rng = ScriptedRng::new(vec![251, 7, 250]); + assert_eq!(sample_uniform_below(&mut rng, 251, 8), 7); + assert_eq!(rng.cursor, 2); + assert_eq!(sample_uniform_below(&mut rng, 251, 8), 250); + assert_eq!(rng.cursor, 3); + } + + #[test] + fn non_byte_aligned_modulus_masks_unused_high_bits() { + // 0xff_ff_ff_ff becomes 0x3f_ff_ff_ff at a 30-bit modulus width, then + // is rejected. The following little-endian candidate 42 is accepted. + let mut rng = ScriptedRng::new(vec![0xff, 0xff, 0xff, 0xff, 42, 0, 0, 0]); + assert_eq!(sample_uniform_below(&mut rng, (1u128 << 30) - 35, 30), 42); + assert_eq!(rng.cursor, 8); + } + + #[test] + fn sub_word_modulus_reads_only_its_canonical_byte_width() { + let mut rng = ScriptedRng::new(vec![42, 0, 0, 99]); + assert_eq!(sample_uniform_below(&mut rng, (1u128 << 24) - 3, 24), 42); + assert_eq!(rng.cursor, 3); + } + + #[test] + fn prime_fields_share_canonical_rejection_and_byte_consumption() { + let fp32_modulus = pm(32, 99) as u32; + let mut fp32_bytes = Vec::from(fp32_modulus.to_le_bytes()); + fp32_bytes.extend_from_slice(&42u32.to_le_bytes()); + let mut fp32_rng = ScriptedRng::new(fp32_bytes); + assert_eq!( + Prime32Offset99::random(&mut fp32_rng), + Prime32Offset99::from_canonical_u32(42) + ); + assert_eq!(fp32_rng.cursor, 8); + + let fp64_modulus = pm(64, 59) as u64; + let mut fp64_bytes = Vec::from(fp64_modulus.to_le_bytes()); + fp64_bytes.extend_from_slice(&42u64.to_le_bytes()); + let mut fp64_rng = ScriptedRng::new(fp64_bytes); + assert_eq!( + Prime64Offset59::random(&mut fp64_rng), + Prime64Offset59::from_canonical_u64(42) + ); + assert_eq!(fp64_rng.cursor, 16); + + let fp128_modulus = pm(128, 0xFFFF_A7F7); + let mut fp128_bytes = Vec::from(fp128_modulus.to_le_bytes()); + fp128_bytes.extend_from_slice(&42u128.to_le_bytes()); + let mut fp128_rng = ScriptedRng::new(fp128_bytes); + assert_eq!( + Prime128OffsetA7F7::random(&mut fp128_rng), + Prime128OffsetA7F7::from_canonical_u128(42) + ); + assert_eq!(fp128_rng.cursor, 32); + } +} diff --git a/crates/jolt-field/src/solinas/word.rs b/crates/jolt-field/src/solinas/word.rs index 105714aad2..674edea575 100644 --- a/crates/jolt-field/src/solinas/word.rs +++ b/crates/jolt-field/src/solinas/word.rs @@ -367,7 +367,7 @@ define_solinas_prime!( double: u64, mul_wide_raw: mul_wide_u32(u32), mul(a, b): Self::reduce_product((a as u64) * (b as u64)), - random(rng): Self(Self::reduce_double(rng.next_u64())), + random(rng): Self(super::sample_uniform_below(rng, P as u128, Self::BITS) as u32), ); define_solinas_prime!( @@ -385,11 +385,7 @@ define_solinas_prime!( Self::reduce_product((a as u128) * (b as u128)) } }, - random(rng): { - let lo = rng.next_u64() as u128; - let hi = rng.next_u64() as u128; - Self(Self::reduce_u128(lo | (hi << 64))) - }, + random(rng): Self(super::sample_uniform_below(rng, P as u128, Self::BITS) as u64), ); /// Whether the two-fold product reduction stays entirely in `u64` for the diff --git a/crates/jolt-field/tests/solinas_words_differential.rs b/crates/jolt-field/tests/solinas_words_differential.rs index 5dfaebf39b..02292aaba5 100644 --- a/crates/jolt-field/tests/solinas_words_differential.rs +++ b/crates/jolt-field/tests/solinas_words_differential.rs @@ -163,8 +163,9 @@ macro_rules! check_prime { "i128::MIN vs oracle" ); - // Identical rejection-free sampling stream: `random` reduces one - // word (Fp32) or two words (Fp64) drawn from the RNG. + // Identical exact-uniform sampling stream: each attempt reads the + // minimal whole-byte candidate width covering the modulus, clears + // unused high bits, and rejects non-canonical candidates. { let seed: u64 = $rng.gen(); let mut r1 = ChaCha20Rng::seed_from_u64(seed); @@ -172,12 +173,20 @@ macro_rules! check_prime { use rand::RngCore; for _ in 0..50 { let t: $two = two::Field::random(&mut r1); - let expected = if $bytes <= 4 { - r2.next_u64() as u128 % p + let bits = 128 - p.leading_zeros(); + let byte_len = bits.div_ceil(8) as usize; + let mask = if bits == 128 { + u128::MAX } else { - let lo = r2.next_u64() as u128; - let hi = r2.next_u64() as u128; - (lo | (hi << 64)) % p + (1u128 << bits) - 1 + }; + let expected = loop { + let mut bytes = [0u8; 16]; + r2.fill_bytes(&mut bytes[..byte_len]); + let candidate = u128::from_le_bytes(bytes) & mask; + if candidate < p { + break candidate; + } }; assert_eq!(t.to_u128_checked(), Some(expected), "random stream"); }