From 467720a55d4a1433c9f5a7604a719ddcecd13e7c Mon Sep 17 00:00:00 2001 From: ljedrz Date: Fri, 16 Jan 2026 15:08:04 +0100 Subject: [PATCH] feat: introduce the SWJ Bucket Signed-off-by: ljedrz --- algorithms/src/msm/variable_base/batched.rs | 9 +- algorithms/src/msm/variable_base/standard.rs | 12 +- .../short_weierstrass_jacobian/bucket.rs | 369 ++++++++++++++++++ .../short_weierstrass_jacobian/mod.rs | 3 + .../short_weierstrass_jacobian/projective.rs | 43 +- .../twisted_edwards_extended/projective.rs | 6 + curves/src/traits/group.rs | 12 + 7 files changed, 443 insertions(+), 11 deletions(-) create mode 100644 curves/src/templates/short_weierstrass_jacobian/bucket.rs diff --git a/algorithms/src/msm/variable_base/batched.rs b/algorithms/src/msm/variable_base/batched.rs index 8db2e8bc7a..c3bfcd5986 100644 --- a/algorithms/src/msm/variable_base/batched.rs +++ b/algorithms/src/msm/variable_base/batched.rs @@ -22,6 +22,7 @@ use rayon::prelude::*; #[cfg(target_arch = "x86_64")] use crate::{prefetch_slice, prefetch_slice_write}; +use std::ops::AddAssign; #[derive(Copy, Clone, Debug)] pub struct BucketPosition { @@ -359,14 +360,14 @@ fn batched_window( let buckets = batch_add(num_buckets, bases, &mut bucket_positions); - let mut res = G::Projective::zero(); - let mut running_sum = G::Projective::zero(); + let mut res = G::Projective::zero_bucket(); + let mut running_sum = G::Projective::zero_bucket(); for b in buckets.into_iter().rev() { - running_sum.add_assign_mixed(&b); + running_sum.add_assign(&b.to_projective()); res += &running_sum; } - (res, window_size) + (res.into(), window_size) } pub fn msm(bases: &[G], scalars: &[::BigInteger]) -> G::Projective { diff --git a/algorithms/src/msm/variable_base/standard.rs b/algorithms/src/msm/variable_base/standard.rs index cfdc9edb82..0cfe1a9a5f 100644 --- a/algorithms/src/msm/variable_base/standard.rs +++ b/algorithms/src/msm/variable_base/standard.rs @@ -25,7 +25,7 @@ fn update_buckets( mut scalar: ::BigInteger, w_start: usize, c: usize, - buckets: &mut [G::Projective], + buckets: &mut [::Bucket], ) { // We right-shift by w_start, thus getting rid of the lower bits. scalar.divn(w_start as u32); @@ -36,7 +36,7 @@ fn update_buckets( // If the scalar is non-zero, we update the corresponding bucket. // (Recall that `buckets` doesn't have a zero bucket.) if scalar != 0 { - buckets[(scalar - 1) as usize].add_assign_mixed(base); + buckets[(scalar - 1) as usize].into().add_assign_mixed(base); } } @@ -58,7 +58,7 @@ fn standard_window( // We don't need the "zero" bucket, so we only have 2^c - 1 buckets let window_size = if (w_start % c) != 0 { w_start % c } else { c }; - let mut buckets = vec![G::Projective::zero(); (1 << window_size) - 1]; + let mut buckets = vec![G::Projective::zero_bucket(); (1 << window_size) - 1]; scalars .iter() .zip(bases) @@ -66,11 +66,11 @@ fn standard_window( .for_each(|(&scalar, base)| update_buckets(base, scalar, w_start, c, &mut buckets)); // G::Projective::batch_normalization(&mut buckets); - for running_sum in buckets.into_iter().rev().scan(G::Projective::zero(), |sum, b| { - *sum += b; + for running_sum in buckets.into_iter().rev().scan(G::Projective::zero_bucket(), |sum, b| { + *sum += &b; Some(*sum) }) { - res += running_sum; + res += running_sum.into(); } (res, window_size) diff --git a/curves/src/templates/short_weierstrass_jacobian/bucket.rs b/curves/src/templates/short_weierstrass_jacobian/bucket.rs new file mode 100644 index 0000000000..c9e634f1f3 --- /dev/null +++ b/curves/src/templates/short_weierstrass_jacobian/bucket.rs @@ -0,0 +1,369 @@ +// Copyright (c) 2019-2025 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{Affine, Projective}; +use crate::traits::{ShortWeierstrassParameters as Parameters, group::AffineCurve}; +// use crate::AffineCurve as AffineRepr; +use snarkvm_fields::{Field, One, Zero}; +use std::{ + fmt::{Debug, Formatter, Result as FmtResult}, + hash::{Hash, Hasher}, + ops::{Add, AddAssign, Neg, SubAssign}, + // One, Zero, +}; + +/// Extended Jacobian coordinates for a point on an elliptic curve in short Weierstrass +/// form, over the base field `P::BaseField`. +/// This struct implements arithmetic via the extended Jacobian arithmetic outlined here: +/// +#[derive(Copy, Clone)] +#[must_use] +pub struct Bucket { + /// `X / ZZ` projection of the affine `X` + pub x: P::BaseField, + /// `Y / ZZZ` projection of the affine `Y` + pub y: P::BaseField, + /// Bucket multiplicative inverse. Will be `0` only at infinity. + pub zz: P::BaseField, + /// Bucket multiplicative inverse. Will be `0` only at infinity. + pub zzz: P::BaseField, +} + +impl Debug for Bucket

{ + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self.is_zero() { + true => write!(f, "infinity"), + false => write!(f, "({}, {}, {}, {})", self.x, self.y, self.zz, self.zzz), + } + } +} + +impl Eq for Bucket

{} + +impl PartialEq for Bucket

{ + fn eq(&self, other: &Self) -> bool { + if self.is_zero() { + return other.is_zero(); + } + + if other.is_zero() { + return false; + } + + // The points (X, Y, Z) and (X', Y', Z') + // are equal when (X * Z^2) = (X' * Z'^2) + // and (Y * Z^3) = (Y' * Z'^3). + if self.x * other.zz == other.x * self.zz { self.y * other.zzz == other.y * self.zzz } else { false } + } +} + +impl Hash for Bucket

{ + fn hash(&self, state: &mut H) { + Affine::from(*self).hash(state) + } +} + +impl Default for Bucket

{ + #[inline] + fn default() -> Self { + Self::zero() + } +} + +impl Bucket

{ + /// Constructs a new group element without checking whether the coordinates + /// specify a point in the subgroup. + pub const fn new_unchecked(x: P::BaseField, y: P::BaseField, zz: P::BaseField, zzz: P::BaseField) -> Self { + Self { x, y, zz, zzz } + } + + /// Returns the point at infinity, which always has Z = 0. + #[inline] + pub fn zero() -> Self { + Self::new_unchecked(P::BaseField::one(), P::BaseField::one(), P::BaseField::zero(), P::BaseField::zero()) + } + + /// Checks whether `self.z.is_zero()`. + #[inline] + pub fn is_zero(&self) -> bool { + self.zz == P::BaseField::zero() && self.zzz == P::BaseField::zero() + } + + pub fn double_in_place(&mut self) { + // From https://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 + // U = 2*Y1 + let mut u = self.y; + u.double_in_place(); + + // V = U^2 + let mut v = u; + v.square_in_place(); + + // W = U*V + let mut w = u; + w *= &v; + + // S = X1*V + let mut s = self.x; + s *= &v; + + // M = 3*X1^2+a*ZZ1^2 + let mut m = self.x.square(); + m += m.double(); + if P::WEIERSTRASS_A != P::BaseField::zero() { + m += P::mul_by_a(&self.zz.square()); + } + // X3 = M^2-2*S + self.x = m; + self.x.square_in_place(); + self.x -= &s.double(); + // Y3 = M*(S-X3)-W*Y1 + self.y = P::BaseField::sum_of_products(&[m, -w], &[(s - self.x), self.y]); + // ZZ3 = V*ZZ1 + self.zz *= v; + // ZZZ3 = W*ZZZ1 + self.zzz *= &w; + } +} + +impl Neg for Bucket

{ + type Output = Self; + + #[inline] + fn neg(mut self) -> Self { + self.y = -self.y; + self + } +} + +impl AddAssign<&Projective

> for Bucket

{ + /// Using + fn add_assign(&mut self, other: &Projective

) { + // If the other point is not at infinity, set `self` to the other point. + // If the other point *is* at infinity, `other.xy()` will be `None`, and we + // will do nothing + if !other.is_zero() { + if self.is_zero() { + self.x = other.x; + self.y = other.y; + self.zz = P::BaseField::one(); + self.zzz = P::BaseField::one(); + return; + } + + let z1z1 = self.zz; + + // U2 = X2*Z1Z1 + let mut u2 = other.x; + u2 *= &z1z1; + + // S2 = Y2*Z1*Z1Z1 + let s2 = other.y * self.zzz; + + if self.x == u2 { + if self.y == s2 { + // The two points are equal, so we double. + *self = other.double_to_bucket(); + } else { + // a + (-a) = 0 + *self = Self::zero() + } + } else { + // P = (U2 - X1); + let mut p = u2; + p -= &self.x; + + // R = S2-Y1 + let mut r = s2; + r -= &self.y; + + // PP = P^2 + let mut pp = p; + pp.square_in_place(); + + // PPP = P*PP + let mut ppp = pp; + ppp *= &p; + + // Q = X1 * PP; + let mut q = self.x; + q *= &pp; + + // X3 = r^2 -PPP - 2*Q + self.x = r.square(); + self.x -= &ppp; + self.x -= &q.double(); + + // Y3 = R*(Q-X3)-Y1*PPP + q -= &self.x; + self.y = P::BaseField::sum_of_products(&[r, -self.y], &[q, ppp]); + + // ZZ3 = ZZ1*PP + // ZZZ3 = ZZZ1*PPP + self.zz *= &pp; + self.zzz *= &ppp; + } + } + } +} + +impl<'a, P: Parameters> Add<&'a Self> for Bucket

{ + type Output = Self; + + #[inline] + fn add(mut self, other: &'a Self) -> Self { + self += other; + self + } +} + +impl<'a, P: Parameters> AddAssign<&'a Self> for Bucket

{ + fn add_assign(&mut self, other: &'a Self) { + if self.is_zero() { + *self = *other; + return; + } + + if other.is_zero() { + return; + } + + // https://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#addition-add-2008-s + // Works for all curves. + + // Z1Z1 = Z1^2 + let z1z1 = self.zz; + + // Z2Z2 = Z2^2 + let z2z2 = other.zz; + + // U1 = X1*Z2Z2 + let mut u1 = self.x; + u1 *= &z2z2; + + // U2 = X2*Z1Z1 + let mut u2 = other.x; + u2 *= &z1z1; + + // S1 = Y1*Z2*Z2Z2 + let s1 = self.y * other.zzz; + + // S2 = Y2*Z1*Z1Z1 + let s2 = other.y * self.zzz; + + if u1 == u2 { + if s1 == s2 { + // The two points are equal, so we double. + self.double_in_place(); + } else { + // a + (-a) = 0 + *self = Self::zero(); + } + } else { + // P = U2-U1 + let mut p = u2; + p -= &u1; + + // R = S2-S1 + let mut r = s2; + r -= &s1; + + // PP = P^2 + let mut pp = p; + pp.square_in_place(); + + // PPP = P*PP + let mut ppp = pp; + ppp *= &p; + + // Q = U1*PP + let mut q = u1; + q *= &pp; + + // X3 = R^2 - PPP - 2*Q + self.x = r.square(); + self.x -= &ppp; + self.x -= &q.double(); + + // Y3 = R*(Q-X3)-S1*PPP + q -= &self.x; + self.y = P::BaseField::sum_of_products(&[r, -s1], &[q, ppp]); + + // ZZ3 = ZZ1*ZZ2*PP + self.zz *= &pp; + self.zz *= &other.zz; + + // ZZZ3 = ZZZ1*ZZZ2*PPP + self.zzz *= &ppp; + self.zzz *= &other.zzz; + } + } +} + +impl<'a, P: Parameters> SubAssign<&'a Self> for Bucket

{ + fn sub_assign(&mut self, other: &'a Self) { + *self += &(-(*other)); + } +} + +impl<'a, P: Parameters> AddAssign<&'a Bucket

> for Projective

{ + fn add_assign(&mut self, other: &'a Bucket

) { + if self.is_zero() { + *self = (*other).into(); + return; + } + + if other.is_zero() { + return; + } + + // TODO: optimize using an explicit formula. + *self += Self::from(*other); + } +} + +// The affine point X, Y is represented in the Jacobian +// coordinates with Z = 1. +impl From> for Bucket

{ + #[inline] + fn from(p: Affine

) -> Self { + if p.infinity { + Self::zero() + } else { + Self { x: p.to_x_coordinate(), y: p.to_y_coordinate(), zz: P::BaseField::one(), zzz: P::BaseField::one() } + } + } +} + +// The affine point X, Y is represented in the Jacobian +// coordinates with Z = 1. +impl From> for Affine

{ + #[inline] + fn from(p: Bucket

) -> Self { + p.zzz.inverse().map_or(Affine::zero(), |zzz_inv| { + let b = p.zz.square(); + let x = p.x * b; + let y = p.y * zzz_inv; + Affine::from_coordinates_unchecked((x, y, p == Bucket::

::zero())) + }) + } +} + +impl From> for Projective

{ + #[inline] + fn from(p: Bucket

) -> Self { + if p.is_zero() { Projective::zero() } else { Projective::new(p.x * p.zz, p.y * p.zzz, p.zz) } + } +} diff --git a/curves/src/templates/short_weierstrass_jacobian/mod.rs b/curves/src/templates/short_weierstrass_jacobian/mod.rs index 1034d10e53..e29ad49121 100644 --- a/curves/src/templates/short_weierstrass_jacobian/mod.rs +++ b/curves/src/templates/short_weierstrass_jacobian/mod.rs @@ -16,6 +16,9 @@ pub mod affine; pub use affine::*; +pub mod bucket; +pub use bucket::*; + pub mod projective; pub use projective::*; diff --git a/curves/src/templates/short_weierstrass_jacobian/projective.rs b/curves/src/templates/short_weierstrass_jacobian/projective.rs index 8613113431..e10f71fb42 100644 --- a/curves/src/templates/short_weierstrass_jacobian/projective.rs +++ b/curves/src/templates/short_weierstrass_jacobian/projective.rs @@ -14,7 +14,7 @@ // limitations under the License. use crate::{ - templates::short_weierstrass_jacobian::Affine, + templates::short_weierstrass_jacobian::{Affine, Bucket}, traits::{AffineCurve, ProjectiveCurve, ShortWeierstrassParameters as Parameters}, }; use snarkvm_fields::{Field, One, Zero, impl_add_sub_from_field_ref}; @@ -42,6 +42,41 @@ impl Projective

{ pub const fn new(x: P::BaseField, y: P::BaseField, z: P::BaseField) -> Self { Self { x, y, z } } + + /// TODO + pub fn double_to_bucket(&self) -> Bucket

{ + if self.is_zero() { + Bucket::zero() + } else { + // https://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-mdbl-2008-s-1 + // U = 2*Y1 + let u = self.y.double(); + // V = U^2 + let v = u.square(); + // W = U*V + let w = u * v; + // S = X1*V + let s = self.x * v; + // M = 3*X1^2+a + let mut m = self.x.square(); + m += m.double(); + if !P::WEIERSTRASS_A.is_zero() { + m += P::WEIERSTRASS_A; + } + // X3 = M^2-2*S + let x = m.square() - s.double(); + // Y3 = M*(S-X3)-W*Y1 + let y = m * (s - x) - w * self.y; + Bucket { + x, + y, + // ZZ3 = V + zz: v, + // ZZZ3 = W + zzz: w, + } + } + } } impl Zero for Projective

{ @@ -153,8 +188,14 @@ impl FromBytes for Projective

{ impl ProjectiveCurve for Projective

{ type Affine = Affine

; type BaseField = P::BaseField; + type Bucket = Bucket

; type ScalarField = P::ScalarField; + /// TODO + fn zero_bucket() -> Self::Bucket { + Bucket::

::zero() + } + #[inline] fn prime_subgroup_generator() -> Self { Affine::prime_subgroup_generator().into() diff --git a/curves/src/templates/twisted_edwards_extended/projective.rs b/curves/src/templates/twisted_edwards_extended/projective.rs index f5fc512230..de7fe1d570 100644 --- a/curves/src/templates/twisted_edwards_extended/projective.rs +++ b/curves/src/templates/twisted_edwards_extended/projective.rs @@ -145,8 +145,14 @@ impl FromBytes for Projective

{ impl ProjectiveCurve for Projective

{ type Affine = Affine

; type BaseField = P::BaseField; + type Bucket = Self; type ScalarField = P::ScalarField; + /// TODO + fn zero_bucket() -> Self::Bucket { + Self::zero() + } + fn prime_subgroup_generator() -> Self { Affine::prime_subgroup_generator().into() } diff --git a/curves/src/traits/group.rs b/curves/src/traits/group.rs index 8d4eadc148..8da786a585 100644 --- a/curves/src/traits/group.rs +++ b/curves/src/traits/group.rs @@ -62,6 +62,18 @@ pub trait ProjectiveCurve: type Affine: AffineCurve + From + Into; type BaseField: Field; type ScalarField: PrimeField + SquareRootField + Into<::BigInteger>; + type Bucket: Default + + Copy + + Clone + + for<'a> AddAssign<&'a Self::Bucket> + + for<'a> SubAssign<&'a Self::Bucket> + + for<'a> AddAssign<&'a Self> + + Send + + Sync + + Into; + + /// TODO + fn zero_bucket() -> Self::Bucket; /// Returns a fixed generator of unknown exponent. #[must_use]