diff --git a/CHANGELOG.md b/CHANGELOG.md index ee4e29b..a93c8b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Safe conversions between the arkworks wrappers and their inner types: + `into_inner()` plus `From` impls in both directions for + `ArkFr`/`ark_bn254::Fr`, `ArkG1`/`G1Projective`, and `ArkG2`/`G2Projective`. + +### Changed + +- Faster arkworks `DoryRoutines`: when MSM bases are not already normalized + (e.g. folded vectors mid reduce-round), `msm` converts them to affine with a + single batched inversion (`normalize_batch`) instead of one field inversion + per point; already-normalized bases (setup generators) keep the + inversion-free per-element path. `fixed_base_vector_scalar_mul`, + `fixed_scalar_mul_bases_then_add`, `fixed_scalar_mul_vs_then_add`, and + `fold_field_vectors` are parallelized under the `parallel` feature. Group + results are unchanged, so commitments and proofs are byte-identical. + ## [0.4.0] - 2026-07-03 ### Security diff --git a/src/backends/arkworks/ark_field.rs b/src/backends/arkworks/ark_field.rs index dbfb94b..7e0bc28 100644 --- a/src/backends/arkworks/ark_field.rs +++ b/src/backends/arkworks/ark_field.rs @@ -12,6 +12,28 @@ use ark_std::ops::{Add, Mul, Neg, Sub}; #[repr(transparent)] pub struct ArkFr(pub Fr); +impl ArkFr { + /// Returns the wrapped `ark_bn254::Fr`. + #[inline] + pub fn into_inner(self) -> Fr { + self.0 + } +} + +impl From for ArkFr { + #[inline] + fn from(value: Fr) -> Self { + ArkFr(value) + } +} + +impl From for Fr { + #[inline] + fn from(value: ArkFr) -> Self { + value.0 + } +} + impl Field for ArkFr { fn zero() -> Self { ArkFr(Fr::from(0u64)) diff --git a/src/backends/arkworks/ark_group.rs b/src/backends/arkworks/ark_group.rs index 55c5c31..d18d594 100644 --- a/src/backends/arkworks/ark_group.rs +++ b/src/backends/arkworks/ark_group.rs @@ -5,7 +5,7 @@ #![allow(clippy::missing_panics_doc)] use super::ark_field::ArkFr; -use crate::primitives::arithmetic::{DoryRoutines, Group}; +use crate::primitives::arithmetic::{DoryRoutines, Field, Group}; use ark_bn254::{Bn254, Fq12, G1Affine, G1Projective, G2Affine, G2Projective}; use ark_ec::pairing::{Pairing, PairingOutput}; use ark_ec::{CurveGroup, VariableBaseMSM}; @@ -13,14 +13,61 @@ use ark_ff::{Field as ArkField, One, PrimeField, UniformRand, Zero as ArkZero}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::ops::{Add, Mul, Neg, Sub}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; + #[derive(Default, Clone, Copy, PartialEq, Eq, Debug, CanonicalSerialize, CanonicalDeserialize)] #[repr(transparent)] pub struct ArkG1(pub G1Projective); +impl ArkG1 { + /// Returns the wrapped `ark_bn254::G1Projective`. + #[inline] + pub fn into_inner(self) -> G1Projective { + self.0 + } +} + +impl From for ArkG1 { + #[inline] + fn from(value: G1Projective) -> Self { + ArkG1(value) + } +} + +impl From for G1Projective { + #[inline] + fn from(value: ArkG1) -> Self { + value.0 + } +} + #[derive(Default, Clone, Copy, PartialEq, Eq, Debug, CanonicalSerialize, CanonicalDeserialize)] #[repr(transparent)] pub struct ArkG2(pub G2Projective); +impl ArkG2 { + /// Returns the wrapped `ark_bn254::G2Projective`. + #[inline] + pub fn into_inner(self) -> G2Projective { + self.0 + } +} + +impl From for ArkG2 { + #[inline] + fn from(value: G2Projective) -> Self { + ArkG2(value) + } +} + +impl From for G2Projective { + #[inline] + fn from(value: ArkG2) -> Self { + value.0 + } +} + #[derive(Clone, Copy, PartialEq, Eq, Debug, CanonicalSerialize, CanonicalDeserialize)] #[repr(transparent)] pub struct ArkGT(pub PairingOutput); @@ -255,6 +302,96 @@ impl<'a> Mul<&'a ArkGT> for ArkFr { } } +/// Converts projective bases to affine for an MSM. +/// +/// Already-normalized bases (`z` is one, e.g. fresh setup generators) and +/// identities convert with no field inversions, so they take the per-element +/// path. Anything else (e.g. folded vectors mid reduce-round) pays one batched +/// inversion for the whole slice instead of one inversion per point. +macro_rules! msm_bases_to_affine { + ($proj:ty, $bases:expr) => {{ + if $bases + .iter() + .all(|b| b.0.z.is_one() || ArkZero::is_zero(&b.0)) + { + $bases.iter().map(|b| b.0.into_affine()).collect() + } else { + let proj: Vec<$proj> = $bases.iter().map(|b| b.0).collect(); + <$proj>::normalize_batch(&proj) + } + }}; +} + +/// `[base * scalars[0], base * scalars[1], ...]`, parallelized when the +/// `parallel` feature is enabled. +fn fixed_base_scalar_muls(base: &G, scalars: &[G::Scalar]) -> Vec { + #[cfg(feature = "parallel")] + { + scalars.par_iter().map(|s| base.scale(s)).collect() + } + #[cfg(not(feature = "parallel"))] + { + scalars.iter().map(|s| base.scale(s)).collect() + } +} + +/// `vs[i] = vs[i] + scalar * bases[i]`, parallelized when the `parallel` +/// feature is enabled. +fn add_scaled_bases(bases: &[G], vs: &mut [G], scalar: &G::Scalar) { + assert_eq!(bases.len(), vs.len(), "Lengths must match"); + + #[cfg(feature = "parallel")] + { + vs.par_iter_mut() + .zip(bases.par_iter()) + .for_each(|(v, base)| *v = v.add(&base.scale(scalar))); + } + #[cfg(not(feature = "parallel"))] + { + for (v, base) in vs.iter_mut().zip(bases.iter()) { + *v = v.add(&base.scale(scalar)); + } + } +} + +/// `vs[i] = scalar * vs[i] + addends[i]`, parallelized when the `parallel` +/// feature is enabled. +fn scale_vs_then_add(vs: &mut [G], addends: &[G], scalar: &G::Scalar) { + assert_eq!(vs.len(), addends.len(), "Lengths must match"); + + #[cfg(feature = "parallel")] + { + vs.par_iter_mut() + .zip(addends.par_iter()) + .for_each(|(v, addend)| *v = v.scale(scalar).add(addend)); + } + #[cfg(not(feature = "parallel"))] + { + for (v, addend) in vs.iter_mut().zip(addends.iter()) { + *v = v.scale(scalar).add(addend); + } + } +} + +/// `left[i] = left[i] * scalar + right[i]`, parallelized when the `parallel` +/// feature is enabled. +fn fold_scalars(left: &mut [F], right: &[F], scalar: &F) { + assert_eq!(left.len(), right.len(), "Lengths must match"); + + #[cfg(feature = "parallel")] + { + left.par_iter_mut() + .zip(right.par_iter()) + .for_each(|(l, r)| *l = *l * *scalar + *r); + } + #[cfg(not(feature = "parallel"))] + { + for (l, r) in left.iter_mut().zip(right.iter()) { + *l = *l * *scalar + *r; + } + } +} + pub struct G1Routines; impl DoryRoutines for G1Routines { @@ -270,30 +407,26 @@ impl DoryRoutines for G1Routines { return ArkG1::identity(); } - let bases_affine: Vec = bases.iter().map(|b| b.0.into_affine()).collect(); + let bases_affine: Vec = msm_bases_to_affine!(G1Projective, bases); let scalars_fr: Vec = scalars.iter().map(|s| s.0).collect(); - ArkG1(G1Projective::msm(&bases_affine, &scalars_fr).expect("MSM failed")) + ArkG1(G1Projective::msm_unchecked(&bases_affine, &scalars_fr)) } fn fixed_base_vector_scalar_mul(base: &ArkG1, scalars: &[ArkFr]) -> Vec { - scalars.iter().map(|s| base.scale(s)).collect() + fixed_base_scalar_muls(base, scalars) } fn fixed_scalar_mul_bases_then_add(bases: &[ArkG1], vs: &mut [ArkG1], scalar: &ArkFr) { - assert_eq!(bases.len(), vs.len(), "Lengths must match"); - - for (v, base) in vs.iter_mut().zip(bases.iter()) { - *v = v.add(&base.scale(scalar)); - } + add_scaled_bases(bases, vs, scalar); } fn fixed_scalar_mul_vs_then_add(vs: &mut [ArkG1], addends: &[ArkG1], scalar: &ArkFr) { - assert_eq!(vs.len(), addends.len(), "Lengths must match"); + scale_vs_then_add(vs, addends, scalar); + } - for (v, addend) in vs.iter_mut().zip(addends.iter()) { - *v = v.scale(scalar).add(addend); - } + fn fold_field_vectors(left: &mut [ArkFr], right: &[ArkFr], scalar: &ArkFr) { + fold_scalars(left, right, scalar); } } @@ -312,29 +445,134 @@ impl DoryRoutines for G2Routines { return ArkG2::identity(); } - let bases_affine: Vec = bases.iter().map(|b| b.0.into_affine()).collect(); + let bases_affine: Vec = msm_bases_to_affine!(G2Projective, bases); let scalars_fr: Vec = scalars.iter().map(|s| s.0).collect(); - ArkG2(G2Projective::msm(&bases_affine, &scalars_fr).expect("MSM failed")) + ArkG2(G2Projective::msm_unchecked(&bases_affine, &scalars_fr)) } fn fixed_base_vector_scalar_mul(base: &ArkG2, scalars: &[ArkFr]) -> Vec { - scalars.iter().map(|s| base.scale(s)).collect() + fixed_base_scalar_muls(base, scalars) } fn fixed_scalar_mul_bases_then_add(bases: &[ArkG2], vs: &mut [ArkG2], scalar: &ArkFr) { - assert_eq!(bases.len(), vs.len(), "Lengths must match"); - - for (v, base) in vs.iter_mut().zip(bases.iter()) { - *v = v.add(&base.scale(scalar)); - } + add_scaled_bases(bases, vs, scalar); } fn fixed_scalar_mul_vs_then_add(vs: &mut [ArkG2], addends: &[ArkG2], scalar: &ArkFr) { - assert_eq!(vs.len(), addends.len(), "Lengths must match"); + scale_vs_then_add(vs, addends, scalar); + } - for (v, addend) in vs.iter_mut().zip(addends.iter()) { - *v = v.scale(scalar).add(addend); + fn fold_field_vectors(left: &mut [ArkFr], right: &[ArkFr], scalar: &ArkFr) { + fold_scalars(left, right, scalar); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn naive_msm(bases: &[G], scalars: &[G::Scalar]) -> G { + bases + .iter() + .zip(scalars.iter()) + .fold(G::identity(), |acc, (base, scalar)| { + acc.add(&base.scale(scalar)) + }) + } + + /// Random fixtures with an identity point and a zero scalar mixed in: + /// those exercise the batch-normalization (point at infinity) and MSM + /// bucket edge cases that purely random inputs miss. Odd-index bases are + /// re-scaled so they are not affine-normalized (`z` is not one), forcing + /// `msm` onto the batch-inversion path (`G::random()` yields `z` = one). + fn fixtures>(len: usize) -> (Vec, Vec) { + let mut bases: Vec = (0..len).map(|_| G::random()).collect(); + let mut scalars: Vec = (0..len).map(|_| ArkFr::random()).collect(); + for base in bases.iter_mut().skip(1).step_by(2) { + *base = base.scale(&ArkFr::random()); } + bases[len / 2] = G::identity(); + scalars[len / 3] = ArkFr::zero(); + (bases, scalars) + } + + fn routines_match_naive(len: usize) + where + G: Group + std::fmt::Debug, + R: DoryRoutines, + { + let (bases, scalars) = fixtures::(len); + let scalar = ArkFr::random(); + + // Mixed-z bases take the batch-normalization path. + assert_eq!(R::msm(&bases, &scalars), naive_msm(&bases, &scalars)); + // Already-normalized bases take the per-element path. + let normalized: Vec = (0..len).map(|_| G::random()).collect(); + assert_eq!( + R::msm(&normalized, &scalars), + naive_msm(&normalized, &scalars) + ); + assert_eq!(R::msm(&[], &[]), G::identity()); + + let expected: Vec = scalars.iter().map(|s| bases[0].scale(s)).collect(); + assert_eq!( + R::fixed_base_vector_scalar_mul(&bases[0], &scalars), + expected + ); + let expected: Vec = scalars.iter().map(|s| G::identity().scale(s)).collect(); + assert_eq!( + R::fixed_base_vector_scalar_mul(&G::identity(), &scalars), + expected + ); + assert_eq!(R::fixed_base_vector_scalar_mul(&bases[0], &[]), vec![]); + + let (mut vs, _) = fixtures::(len); + let mut expected = vs.clone(); + R::fixed_scalar_mul_bases_then_add(&bases, &mut vs, &scalar); + for (v, base) in expected.iter_mut().zip(bases.iter()) { + *v = v.add(&base.scale(&scalar)); + } + assert_eq!(vs, expected); + + R::fixed_scalar_mul_vs_then_add(&mut vs, &bases, &scalar); + for (v, addend) in expected.iter_mut().zip(bases.iter()) { + *v = v.scale(&scalar).add(addend); + } + assert_eq!(vs, expected); + + let right: Vec = (0..len).map(|_| ArkFr::random()).collect(); + let mut left: Vec = (0..len).map(|_| ArkFr::random()).collect(); + let mut expected = left.clone(); + R::fold_field_vectors(&mut left, &right, &scalar); + for (l, r) in expected.iter_mut().zip(right.iter()) { + *l = *l * scalar + *r; + } + assert_eq!(left, expected); + } + + #[test] + fn g1_routines_match_naive() { + routines_match_naive::(33); + } + + #[test] + fn g2_routines_match_naive() { + routines_match_naive::(17); + } + + #[test] + fn wrapper_conversions_round_trip() { + let fr = ArkFr::random(); + assert_eq!(ArkFr::from(fr.into_inner()), fr); + assert_eq!(ArkFr::from(ark_bn254::Fr::from(fr)), fr); + + let g1 = ArkG1::random(); + assert_eq!(ArkG1::from(g1.into_inner()), g1); + assert_eq!(ArkG1::from(G1Projective::from(g1)), g1); + + let g2 = ArkG2::random(); + assert_eq!(ArkG2::from(g2.into_inner()), g2); + assert_eq!(ArkG2::from(G2Projective::from(g2)), g2); } }