From 87fcee0f6c49d0420a32995ef62d6753b7f41d4e Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Sun, 1 Feb 2026 13:26:57 -0700 Subject: [PATCH 01/17] Bump `rand_core` to v0.10; MSRV 1.85 NOTE: depends on zkcrypto/ff#149 Release notes for `rand_core` v0.10.0: https://github.com/rust-random/rand_core/releases/tag/v0.10.0 --- .github/workflows/ci.yml | 3 +- Cargo.lock | 73 +++++----------------------------------- Cargo.toml | 11 +++--- rust-toolchain.toml | 2 +- src/lib.rs | 6 ++-- 5 files changed, 20 insertions(+), 75 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39f9e7b..6607146 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: strategy: matrix: target: - - wasm32-wasi + - wasm32-wasip1 - thumbv6m-none-eabi - thumbv7em-none-eabihf steps: @@ -81,7 +81,6 @@ jobs: - uses: actions/checkout@v4 - run: cargo fetch # Requires #![deny(rustdoc::broken_intra_doc_links)] in crates. - - run: sudo apt-get -y install libfontconfig1-dev - name: Check intra-doc links run: cargo doc --all-features --document-private-items diff --git a/Cargo.lock b/Cargo.lock index cffd341..f4a1376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,12 +1,11 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ff" version = "0.14.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d42dd26f5790eda47c1a2158ea4120e32c35ddc9a7743c98a292accc01b54ef3" +source = "git+https://github.com/tarcieri/ff?branch=rand_core%2Fv0.10#810bfd0c67ee31663b696e6af26d1b2cea13c409" dependencies = [ "rand_core", "subtle", @@ -30,45 +29,26 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - [[package]] name = "rand" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "rand_core", - "zerocopy", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" [[package]] name = "rand_xorshift" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf" dependencies = [ "rand_core", ] @@ -78,40 +58,3 @@ name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "zerocopy" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/Cargo.toml b/Cargo.toml index a153e74..2cedce0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ authors = [ "Jack Grigg ", ] edition = "2021" -rust-version = "1.63" +rust-version = "1.85" readme = "README.md" license = "MIT/Apache-2.0" @@ -17,9 +17,9 @@ repository = "https://github.com/zkcrypto/group" [dependencies] ff = { version = "=0.14.0-pre.0", default-features = false } -rand = { version = "0.9", optional = true, default-features = false } -rand_core = { version = "0.9", default-features = false } -rand_xorshift = { version = "0.4", optional = true } +rand = { version = "0.10", optional = true, default-features = false } +rand_core = { version = "0.10", default-features = false } +rand_xorshift = { version = "0.5", optional = true } subtle = { version = "2.2.1", default-features = false } # Crate for exposing the dynamic memory usage of the w-NAF structs. @@ -33,3 +33,6 @@ wnaf-memuse = ["alloc", "memuse"] [badges] maintenance = { status = "actively-developed" } + +[patch.crates-io] +ff = { git = "https://github.com/tarcieri/ff", branch = "rand_core/v0.10" } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 3eebdfe..efd3308 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.63.0" +channel = "1.85.0" components = [ "clippy", "rustfmt" ] diff --git a/src/lib.rs b/src/lib.rs index cd3cf5e..d0c9b10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ use core::fmt; use core::iter::Sum; use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use ff::PrimeField; -use rand_core::{RngCore, TryRngCore}; +use rand_core::{Rng, TryRng}; use subtle::{Choice, CtOption}; pub mod cofactor; @@ -77,7 +77,7 @@ pub trait Group: /// this group. /// /// This function is non-deterministic, and samples from the user-provided RNG. - fn random(rng: &mut R) -> Self { + fn random(rng: &mut R) -> Self { Self::try_from_rng(rng) .map_err(|e: Infallible| e) .expect("Infallible failed") @@ -92,7 +92,7 @@ pub trait Group: /// this group. /// /// This function is non-deterministic, and samples from the user-provided RNG. - fn try_from_rng(rng: &mut R) -> Result; + fn try_from_rng(rng: &mut R) -> Result; /// Returns the additive identity, also known as the "neutral element". fn identity() -> Self; From 21104854da7f316c470213ab4a2ace239d24dd28 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sat, 29 Jul 2023 17:32:01 +0000 Subject: [PATCH 02/17] Introduce `CurveAffine` trait This unifies the methods previously exposed by the `PrimeCurveAffine` and `CofactorCurveAffine` traits. The prime-order and cofactor traits are now all marker traits, and their affine-specific traits are automatically derived. --- CHANGELOG.md | 17 +++++++++++++++++ src/cofactor.rs | 48 ++++------------------------------------------- src/lib.rs | 49 +++++++++++++++++++++++++++++++++++++++++------- src/prime.rs | 44 ++++--------------------------------------- src/tests/mod.rs | 8 ++------ 5 files changed, 69 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7434ec..44138f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `group::CurveAffine` + ### Changed - MSRV is now 1.63.0. - Migrated to `ff 0.14`, `rand_core 0.9`. @@ -14,6 +17,20 @@ and this library adheres to Rust's notion of trait object as the RNG. - `group::Group::try_from_rng` is a new trait method that must be implemented by downstreams. `Group::random` now has a default implementation that calls it. +- The curve-related traits have been refactored around the new `CurveAffine` + trait: + - `group::Curve::AffineRepr` has been renamed to `Curve::Affine`. + - All of the trait methods and associated types on the following traits have + been removed (use `group::Curve::Affine` or the `group::CurveAffine` trait + instead; trait implementors must implement `group::CurveAffine` instead + using the same logic): + - `group::cofactor::CofactorCurve` + - `group::cofactor::CofactorCurveAffine` + - `group::prime::PrimeCurve` + - `group::prime::PrimeCurveAffine` + - `group::cofactor::CofactorCurveAffine` and `group::prime::PrimeCurveAffine` + now have blanket implementations for all types `C: group::CurveAffine` where + `C::Curve` implements `CofactorCurve` or `PrimeCurve` respectively. ## [0.13.0] - 2022-12-06 ### Changed diff --git a/src/cofactor.rs b/src/cofactor.rs index 84bfe0a..fae0a89 100644 --- a/src/cofactor.rs +++ b/src/cofactor.rs @@ -1,9 +1,6 @@ -use core::fmt; -use core::ops::{Mul, Neg}; -use ff::PrimeField; use subtle::{Choice, CtOption}; -use crate::{prime::PrimeGroup, Curve, Group, GroupEncoding, GroupOps, GroupOpsOwned}; +use crate::{prime::PrimeGroup, Curve, CurveAffine, Group, GroupEncoding, GroupOps, GroupOpsOwned}; /// This trait represents an element of a cryptographic group with a large prime-order /// subgroup and a comparatively-small cofactor. @@ -54,47 +51,10 @@ pub trait CofactorGroup: /// Efficient representation of an elliptic curve point guaranteed to be /// in the correct prime order subgroup. -pub trait CofactorCurve: - Curve::Affine> + CofactorGroup -{ - type Affine: CofactorCurveAffine - + Mul - + for<'r> Mul<&'r Self::Scalar, Output = Self>; -} +pub trait CofactorCurve: Curve + CofactorGroup {} /// Affine representation of an elliptic curve point guaranteed to be /// in the correct prime order subgroup. -pub trait CofactorCurveAffine: - GroupEncoding - + Copy - + Clone - + Sized - + Send - + Sync - + fmt::Debug - + PartialEq - + Eq - + 'static - + Neg - + Mul<::Scalar, Output = ::Curve> - + for<'r> Mul< - &'r ::Scalar, - Output = ::Curve, - > -{ - type Scalar: PrimeField; - type Curve: CofactorCurve; - - /// Returns the additive identity. - fn identity() -> Self; +pub trait CofactorCurveAffine: CurveAffine {} - /// Returns a fixed generator of unknown exponent. - fn generator() -> Self; - - /// Determines if this point represents the point at infinity; the - /// additive identity. - fn is_identity(&self) -> Choice; - - /// Converts this element to its curve representation. - fn to_curve(&self) -> Self::Curve; -} +impl CofactorCurveAffine for C where C::Curve: CofactorCurve {} diff --git a/src/lib.rs b/src/lib.rs index cd3cf5e..4395525 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,16 +114,16 @@ pub trait Group: } } -/// Efficient representation of an elliptic curve point guaranteed. -pub trait Curve: - Group + GroupOps<::AffineRepr> + GroupOpsOwned<::AffineRepr> -{ +/// Efficient representation of an elliptic curve point. +pub trait Curve: Group + GroupOps + GroupOpsOwned { /// The affine representation for this elliptic curve. - type AffineRepr; + type Affine: CurveAffine + + Mul + + for<'r> Mul<&'r Self::Scalar, Output = Self>; /// Converts a batch of projective elements into affine elements. This function will /// panic if `p.len() != q.len()`. - fn batch_normalize(p: &[Self], q: &mut [Self::AffineRepr]) { + fn batch_normalize(p: &[Self], q: &mut [Self::Affine]) { assert_eq!(p.len(), q.len()); for (p, q) in p.iter().zip(q.iter_mut()) { @@ -132,7 +132,42 @@ pub trait Curve: } /// Converts this element into its affine representation. - fn to_affine(&self) -> Self::AffineRepr; + fn to_affine(&self) -> Self::Affine; +} + +/// Affine representation of an elliptic curve point. +pub trait CurveAffine: + GroupEncoding + + Copy + + fmt::Debug + + Eq + + Send + + Sync + + 'static + + Neg + + Mul<::Scalar, Output = Self::Curve> + + for<'r> Mul<&'r ::Scalar, Output = Self::Curve> +{ + /// The efficient representation for this elliptic curve. + type Curve: Curve; + + /// Scalars modulo the order of this group's scalar field. + /// + /// This associated type is temporary, and will be removed once downstream users have + /// migrated to using `Curve` as the primary generic bound. + type Scalar: PrimeField; + + /// Returns the additive identity. + fn identity() -> Self; + + /// Returns a fixed generator of unknown exponent. + fn generator() -> Self; + + /// Determines if this point represents the additive identity. + fn is_identity(&self) -> Choice; + + /// Converts this affine point to its efficient representation. + fn to_curve(&self) -> Self::Curve; } pub trait GroupEncoding: Sized { diff --git a/src/prime.rs b/src/prime.rs index 174888e..0964782 100644 --- a/src/prime.rs +++ b/src/prime.rs @@ -1,50 +1,14 @@ -use core::fmt; -use core::ops::{Mul, Neg}; -use ff::PrimeField; -use subtle::Choice; - -use crate::{Curve, Group, GroupEncoding}; +use crate::{Curve, CurveAffine, Group, GroupEncoding}; /// This trait represents an element of a prime-order cryptographic group. pub trait PrimeGroup: Group + GroupEncoding {} /// Efficient representation of an elliptic curve point guaranteed to be /// in the correct prime order subgroup. -pub trait PrimeCurve: Curve::Affine> + PrimeGroup { - type Affine: PrimeCurveAffine - + Mul - + for<'r> Mul<&'r Self::Scalar, Output = Self>; -} +pub trait PrimeCurve: Curve + PrimeGroup {} /// Affine representation of an elliptic curve point guaranteed to be /// in the correct prime order subgroup. -pub trait PrimeCurveAffine: GroupEncoding - + Copy - + Clone - + Sized - + Send - + Sync - + fmt::Debug - + PartialEq - + Eq - + 'static - + Neg - + Mul<::Scalar, Output = ::Curve> - + for<'r> Mul<&'r ::Scalar, Output = ::Curve> -{ - type Scalar: PrimeField; - type Curve: PrimeCurve; - - /// Returns the additive identity. - fn identity() -> Self; - - /// Returns a fixed generator of unknown exponent. - fn generator() -> Self; - - /// Determines if this point represents the point at infinity; the - /// additive identity. - fn is_identity(&self) -> Choice; +pub trait PrimeCurveAffine: CurveAffine {} - /// Converts this element to its curve representation. - fn to_curve(&self) -> Self::Curve; -} +impl PrimeCurveAffine for C where C::Curve: PrimeCurve {} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index c81a926..a598e50 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -4,11 +4,7 @@ use ff::{Field, PrimeField}; use rand::SeedableRng; use rand_xorshift::XorShiftRng; -use crate::{ - prime::{PrimeCurve, PrimeCurveAffine}, - wnaf::WnafGroup, - GroupEncoding, UncompressedEncoding, -}; +use crate::{prime::PrimeCurve, wnaf::WnafGroup, CurveAffine, GroupEncoding, UncompressedEncoding}; pub fn curve_tests() { let mut rng = XorShiftRng::from_seed([ @@ -426,7 +422,7 @@ fn random_compressed_encoding_tests() { pub fn random_uncompressed_encoding_tests() where - ::Affine: UncompressedEncoding, + G::Affine: UncompressedEncoding, { let mut rng = XorShiftRng::from_seed([ 0x59, 0x62, 0xbe, 0x5d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06, 0xbc, From 92f221bf647d0e782a38e707db411b1ee8f6534e Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Thu, 21 May 2026 14:51:42 -0600 Subject: [PATCH 03/17] Update patch to retarget ff 0.14.0 release branch --- Cargo.lock | 2 +- Cargo.toml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4a1376..ce53cf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,7 +5,7 @@ version = 4 [[package]] name = "ff" version = "0.14.0-pre.0" -source = "git+https://github.com/tarcieri/ff?branch=rand_core%2Fv0.10#810bfd0c67ee31663b696e6af26d1b2cea13c409" +source = "git+https://github.com/zkcrypto/ff?rev=bdd74a3f75c4c378d69e954944a61bb0930532a0#bdd74a3f75c4c378d69e954944a61bb0930532a0" dependencies = [ "rand_core", "subtle", diff --git a/Cargo.toml b/Cargo.toml index 2cedce0..f75736b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,4 +35,5 @@ wnaf-memuse = ["alloc", "memuse"] maintenance = { status = "actively-developed" } [patch.crates-io] -ff = { git = "https://github.com/tarcieri/ff", branch = "rand_core/v0.10" } +# targets zkcrypto's `release-0.14.0` branch +ff = { git = "https://github.com/zkcrypto/ff", rev = "bdd74a3f75c4c378d69e954944a61bb0930532a0" } From 326b36b0c43dcb1507b89017323f43a57c3bffd8 Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Thu, 21 May 2026 14:55:39 -0600 Subject: [PATCH 04/17] Rename `Group::try_from_rng` to `Group::try_random` Matches the naming adopted in zkcrypto/rfcs#1. --- CHANGELOG.md | 2 +- src/lib.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44138f9..197044f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this library adheres to Rust's notion of - `group::Group::random(rng: impl RngCore) -> Self` has been changed to `Group::random(rng: &mut R) -> Self`, to enable passing a trait object as the RNG. -- `group::Group::try_from_rng` is a new trait method that must be implemented by +- `group::Group::try_random` is a new trait method that must be implemented by downstreams. `Group::random` now has a default implementation that calls it. - The curve-related traits have been refactored around the new `CurveAffine` trait: diff --git a/src/lib.rs b/src/lib.rs index 1055d76..114bc95 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,12 +78,12 @@ pub trait Group: /// /// This function is non-deterministic, and samples from the user-provided RNG. fn random(rng: &mut R) -> Self { - Self::try_from_rng(rng) + Self::try_random(rng) .map_err(|e: Infallible| e) .expect("Infallible failed") // NOTE: once MSRV gets to 1.82 remove the map_err/expect and use - // let Ok(out) = Self::try_from_rng(rng); + // let Ok(out) = Self::try_random(rng); // out // See: https://blog.rust-lang.org/2024/10/17/Rust-1.82.0.html#omitting-empty-types-in-pattern-matching } @@ -92,7 +92,7 @@ pub trait Group: /// this group. /// /// This function is non-deterministic, and samples from the user-provided RNG. - fn try_from_rng(rng: &mut R) -> Result; + fn try_random(rng: &mut R) -> Result; /// Returns the additive identity, also known as the "neutral element". fn identity() -> Self; From d24dd0dd455bd99cc6555b04dd9ccad78adc4823 Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Thu, 21 May 2026 17:01:32 -0600 Subject: [PATCH 05/17] Update to `ff 0.14.0-pre.1` --- Cargo.lock | 5 +++-- Cargo.toml | 6 +----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce53cf9..bcb364f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,8 +4,9 @@ version = 4 [[package]] name = "ff" -version = "0.14.0-pre.0" -source = "git+https://github.com/zkcrypto/ff?rev=bdd74a3f75c4c378d69e954944a61bb0930532a0#bdd74a3f75c4c378d69e954944a61bb0930532a0" +version = "0.14.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d1fa20678d31fcd57b1e146f5ca2c24282428601f45a1002a0d2a54136ef681" dependencies = [ "rand_core", "subtle", diff --git a/Cargo.toml b/Cargo.toml index f75736b..b3f660b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ homepage = "https://github.com/zkcrypto/group" repository = "https://github.com/zkcrypto/group" [dependencies] -ff = { version = "=0.14.0-pre.0", default-features = false } +ff = { version = "=0.14.0-pre.1", default-features = false } rand = { version = "0.10", optional = true, default-features = false } rand_core = { version = "0.10", default-features = false } rand_xorshift = { version = "0.5", optional = true } @@ -33,7 +33,3 @@ wnaf-memuse = ["alloc", "memuse"] [badges] maintenance = { status = "actively-developed" } - -[patch.crates-io] -# targets zkcrypto's `release-0.14.0` branch -ff = { git = "https://github.com/zkcrypto/ff", rev = "bdd74a3f75c4c378d69e954944a61bb0930532a0" } From b8220c804f2dc5402d6ad03e50de36041686249d Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Thu, 21 May 2026 17:06:35 -0600 Subject: [PATCH 06/17] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 197044f..b798a0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,12 @@ and this library adheres to Rust's notion of ## [Unreleased] ### Added - `group::CurveAffine` +- `group::Group::mul_by_generator`, with a default implementation. Implementors + can override it to take advantage of precomputed tables. ### Changed -- MSRV is now 1.63.0. -- Migrated to `ff 0.14`, `rand_core 0.9`. +- MSRV is now 1.85.0. +- Migrated to `ff 0.14`, `rand_core 0.10`. - `group::Group::random(rng: impl RngCore) -> Self` has been changed to `Group::random(rng: &mut R) -> Self`, to enable passing a trait object as the RNG. From b71b98a1c115097a6f8988c13d944638589211a9 Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Thu, 21 May 2026 17:07:54 -0600 Subject: [PATCH 07/17] Preview 0.14.0-pre.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bcb364f..83e233a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,7 +14,7 @@ dependencies = [ [[package]] name = "group" -version = "0.14.0-pre.0" +version = "0.14.0-pre.1" dependencies = [ "ff", "memuse", diff --git a/Cargo.toml b/Cargo.toml index b3f660b..730d5d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "group" -version = "0.14.0-pre.0" +version = "0.14.0-pre.1" authors = [ "Sean Bowe ", "Jack Grigg ", From 0562c202198ba167dbfed7aa578863b57ee1b035 Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Sat, 30 May 2026 09:54:31 -0600 Subject: [PATCH 08/17] Update CHANGELOG.md --- CHANGELOG.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b798a0d..1e3dbd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,18 +7,21 @@ and this library adheres to Rust's notion of ## [Unreleased] ### Added -- `group::CurveAffine` +- `group::CurveAffine`, an affine-representation trait that the curve-specific + affine traits are now built on top of. - `group::Group::mul_by_generator`, with a default implementation. Implementors can override it to take advantage of precomputed tables. +- `group::Group::try_random(rng: &mut R) -> Result`, + a new trait method that must be implemented by downstreams. It samples a + non-identity group element using a fallible RNG and propagates the RNG's error. ### Changed - MSRV is now 1.85.0. - Migrated to `ff 0.14`, `rand_core 0.10`. - `group::Group::random(rng: impl RngCore) -> Self` has been changed to - `Group::random(rng: &mut R) -> Self`, to enable passing a - trait object as the RNG. -- `group::Group::try_random` is a new trait method that must be implemented by - downstreams. `Group::random` now has a default implementation that calls it. + `Group::random(rng: &mut R) -> Self`, to enable passing a + trait object as the RNG. It now has a default implementation in terms of + `Group::try_random`. - The curve-related traits have been refactored around the new `CurveAffine` trait: - `group::Curve::AffineRepr` has been renamed to `Curve::Affine`. From 8b8e53b3547d4f7714ecc4b7fc322a17e622ab90 Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Sat, 30 May 2026 09:55:10 -0600 Subject: [PATCH 09/17] Update MSRV in README.md to 1.85 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index efff81c..fd57744 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ wider discussion. ## Minimum Supported Rust Version -Requires Rust **1.63** or higher. +Requires Rust **1.85** or higher. Minimum supported Rust version can be changed in the future, but it will be done with a minor version bump. From d0bed6d55c90bea5edf8c5d4da32a31c11d011cb Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Sat, 30 May 2026 09:56:19 -0600 Subject: [PATCH 10/17] Update to ff 0.14.0 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 83e233a..1564ee2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "ff" -version = "0.14.0-pre.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d1fa20678d31fcd57b1e146f5ca2c24282428601f45a1002a0d2a54136ef681" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ "rand_core", "subtle", diff --git a/Cargo.toml b/Cargo.toml index 730d5d2..857b0c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ homepage = "https://github.com/zkcrypto/group" repository = "https://github.com/zkcrypto/group" [dependencies] -ff = { version = "=0.14.0-pre.1", default-features = false } +ff = { version = "0.14", default-features = false } rand = { version = "0.10", optional = true, default-features = false } rand_core = { version = "0.10", default-features = false } rand_xorshift = { version = "0.5", optional = true } From 1d0579607e23ec2875cdb4dd03295d35092f5342 Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Sat, 30 May 2026 09:57:19 -0600 Subject: [PATCH 11/17] Use consistent CHANGELOG language for ff bump --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e3dbd9..04cac04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ and this library adheres to Rust's notion of ### Changed - MSRV is now 1.85.0. -- Migrated to `ff 0.14`, `rand_core 0.10`. +- Bumped dependencies to `ff 0.14`, `rand_core 0.10`. - `group::Group::random(rng: impl RngCore) -> Self` has been changed to `Group::random(rng: &mut R) -> Self`, to enable passing a trait object as the RNG. It now has a default implementation in terms of From 5e5b9b9791bae1a9880cd46b02cc113184f13759 Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Sat, 30 May 2026 09:59:53 -0600 Subject: [PATCH 12/17] Fix lints and minor nits --- src/lib.rs | 11 ++--------- src/tests/mod.rs | 2 +- src/wnaf.rs | 6 +++--- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 114bc95..5d0c520 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,6 @@ extern crate alloc; // Re-export ff to make version-matching easier. pub use ff; -use core::convert::Infallible; use core::fmt; use core::iter::Sum; use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; @@ -78,14 +77,8 @@ pub trait Group: /// /// This function is non-deterministic, and samples from the user-provided RNG. fn random(rng: &mut R) -> Self { - Self::try_random(rng) - .map_err(|e: Infallible| e) - .expect("Infallible failed") - - // NOTE: once MSRV gets to 1.82 remove the map_err/expect and use - // let Ok(out) = Self::try_random(rng); - // out - // See: https://blog.rust-lang.org/2024/10/17/Rust-1.82.0.html#omitting-empty-types-in-pattern-matching + let Ok(out) = Self::try_random(rng); + out } /// Returns an element chosen uniformly at random from the non-identity elements of diff --git a/src/tests/mod.rs b/src/tests/mod.rs index a598e50..d5f6b1f 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -305,7 +305,7 @@ fn random_addition_tests() { assert_eq!(aplusa, aplusamixed); } - let mut tmp = vec![G::identity(); 6]; + let mut tmp = [G::identity(); 6]; // (a + b) + c tmp[0] = a; diff --git a/src/wnaf.rs b/src/wnaf.rs index fdcf432..5adaf66 100644 --- a/src/wnaf.rs +++ b/src/wnaf.rs @@ -340,7 +340,7 @@ impl<'a, G: Group> Wnaf> { } #[cfg(feature = "wnaf-memuse")] -impl<'a, G: Group> memuse::DynamicUsage for Wnaf> { +impl memuse::DynamicUsage for Wnaf> { fn dynamic_usage(&self) -> usize { // The heap memory for the window table is counted in the parent `Wnaf`. self.scalar.dynamic_usage() @@ -365,7 +365,7 @@ impl<'a, G: Group> Wnaf, &'a [i64]> { } #[cfg(feature = "wnaf-memuse")] -impl<'a, G: Group + memuse::DynamicUsage> memuse::DynamicUsage for Wnaf, &'a [i64]> { +impl memuse::DynamicUsage for Wnaf, &[i64]> { fn dynamic_usage(&self) -> usize { // The heap memory for the scalar representation is counted in the parent `Wnaf`. self.base.dynamic_usage() @@ -432,7 +432,7 @@ impl WnafScalar { WnafScalar { wnaf, - field: PhantomData::default(), + field: PhantomData, } } } From ab8653349d5c440ce829ff929d79b096a7f5d76b Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Sat, 30 May 2026 11:36:04 -0600 Subject: [PATCH 13/17] Simplify Curve::Affine bounds --- src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5d0c520..d6b1c84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,9 +110,7 @@ pub trait Group: /// Efficient representation of an elliptic curve point. pub trait Curve: Group + GroupOps + GroupOpsOwned { /// The affine representation for this elliptic curve. - type Affine: CurveAffine - + Mul - + for<'r> Mul<&'r Self::Scalar, Output = Self>; + type Affine: CurveAffine; /// Converts a batch of projective elements into affine elements. This function will /// panic if `p.len() != q.len()`. From 7fc1ccc1c2a75e3ddbdb29638dd28b17549a05af Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Sat, 30 May 2026 11:38:11 -0600 Subject: [PATCH 14/17] Align group random docs with ff --- src/lib.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d6b1c84..46cd0a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,18 +73,22 @@ pub trait Group: type Scalar: PrimeField; /// Returns an element chosen uniformly at random from the non-identity elements of - /// this group. + /// this group using a user-provided infallible RNG. /// - /// This function is non-deterministic, and samples from the user-provided RNG. + /// This is a convenience wrapper around [`Group::try_random`] for RNGs that cannot + /// fail. Use [`Group::try_random`] if your RNG may fail (for example, an OS-backed + /// entropy source). fn random(rng: &mut R) -> Self { let Ok(out) = Self::try_random(rng); out } /// Returns an element chosen uniformly at random from the non-identity elements of - /// this group. + /// this group using a user-provided fallible RNG. /// - /// This function is non-deterministic, and samples from the user-provided RNG. + /// Returns `Err` propagating the RNG's error if the underlying RNG fails to produce + /// the randomness required to sample an element. Implementors of `Group` must + /// provide this method; [`Group::random`] is derived from it for infallible RNGs. fn try_random(rng: &mut R) -> Result; /// Returns the additive identity, also known as the "neutral element". From f9a84a7587f47fab4ad65a25b58c8a01d1b26d39 Mon Sep 17 00:00:00 2001 From: Sean Bowe Date: Sat, 30 May 2026 10:04:20 -0600 Subject: [PATCH 15/17] Release 0.14.0 --- CHANGELOG.md | 2 ++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04cac04..7cf6177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [0.14.0] - 2026-06-01 ### Added - `group::CurveAffine`, an affine-representation trait that the curve-specific affine traits are now built on top of. diff --git a/Cargo.lock b/Cargo.lock index 1564ee2..a00b43c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,7 +14,7 @@ dependencies = [ [[package]] name = "group" -version = "0.14.0-pre.1" +version = "0.14.0" dependencies = [ "ff", "memuse", diff --git a/Cargo.toml b/Cargo.toml index 857b0c1..236d244 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "group" -version = "0.14.0-pre.1" +version = "0.14.0" authors = [ "Sean Bowe ", "Jack Grigg ", From 2cf6afcb879ab49d47fe57133b0148a0048d2069 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Wed, 6 May 2026 19:26:53 -0600 Subject: [PATCH 16/17] Add `WnafBase::multiscalar_mul` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Computes a sum-of-products `aA + bB + ...` in variable time with w-NAF multi-exponentiation using the interleaved window method, also known as Straus' method. The key insight is that when computing this sum by means of additions and doublings, the doublings can be shared by performing the additions within an inner loop. The API and implementation are inspired in part by `curve25519-dalek`, namely the `VartimeMultiscalarMul` trait and corresponding implementation in `straus.rs`. This results in ~28% speedup on `p256` for a 3 scalar/point input: ProjectivePoint operations/point-scalar lincomb (variable-time) time: [149.13 µs 149.80 µs 150.84 µs] change: [−27.999% −27.645% −27.267%] (p = 0.00 < 0.05) --- src/wnaf.rs | 50 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/src/wnaf.rs b/src/wnaf.rs index 5adaf66..419fc0f 100644 --- a/src/wnaf.rs +++ b/src/wnaf.rs @@ -150,23 +150,42 @@ pub(crate) fn wnaf_form>(wnaf: &mut Vec, c: S, window: usize /// /// This function must be provided a `table` and `wnaf` that were constructed with /// the same window size; otherwise, it may panic or produce invalid results. +#[inline] pub(crate) fn wnaf_exp(table: &[G], wnaf: &[i64]) -> G { - let mut result = G::identity(); + wnaf_multi_exp(&[table], &[wnaf]) +} +/// Performs w-NAF multi-exponentiation using the interleaved window method, also known as +/// Straus' method. +/// +/// The key insight is that when computing this sum by means of additions and doublings, the +/// doublings can be shared by performing the additions within an inner loop. +/// +/// This function must be provided with `tables` and `wnafs` that were constructed with +/// the same window size; otherwise, it may panic or produce invalid results. +pub(crate) fn wnaf_multi_exp(tables: &[&[G]], wnafs: &[&[i64]]) -> G { + debug_assert_eq!(tables.len(), wnafs.len()); + let window_size = wnafs.iter().map(|w| w.len()).max().unwrap_or(0); + + let mut result = G::identity(); let mut found_one = false; - for n in wnaf.iter().rev() { + for i in (0..window_size).rev() { + // Only double once per iteration of the loop if found_one { result = result.double(); } - if *n != 0 { - found_one = true; + for (&table, &wnaf) in tables.iter().zip(wnafs.iter()) { + let n = wnaf.get(i).copied().unwrap_or(0); + if n != 0 { + found_one = true; - if *n > 0 { - result += &table[(n / 2) as usize]; - } else { - result -= &table[((-n) / 2) as usize]; + if n > 0 { + result += &table[(n / 2) as usize]; + } else { + result -= &table[((-n) / 2) as usize]; + } } } } @@ -499,6 +518,21 @@ impl WnafBase { WnafBase { table } } + + /// Perform a multiscalar multiplication. + pub fn multiscalar_mul<'a, I, J>(scalars: I, bases: J) -> G + where + I: Iterator>, + J: Iterator, + { + let wnafs = scalars + .map(|scalar| scalar.wnaf.as_slice()) + .collect::>(); + + let tables = bases.map(|base| base.table.as_slice()).collect::>(); + + wnaf_multi_exp(tables.as_slice(), wnafs.as_slice()) + } } impl Mul<&WnafScalar> From 2bac7bc506a2b961b48502244dd063c052689f9a Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Tue, 2 Jun 2026 07:04:10 -0600 Subject: [PATCH 17/17] Use `IntoIterator` for `multiscalar_mul` bounds This is closer to the `VartimeMultiscalarMul` trait in `curve25519-dalek`. --- src/wnaf.rs | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/wnaf.rs b/src/wnaf.rs index 419fc0f..8946141 100644 --- a/src/wnaf.rs +++ b/src/wnaf.rs @@ -163,9 +163,12 @@ pub(crate) fn wnaf_exp(table: &[G], wnaf: &[i64]) -> G { /// /// This function must be provided with `tables` and `wnafs` that were constructed with /// the same window size; otherwise, it may panic or produce invalid results. -pub(crate) fn wnaf_multi_exp(tables: &[&[G]], wnafs: &[&[i64]]) -> G { +pub(crate) fn wnaf_multi_exp, W: AsRef<[i64]>>( + tables: &[T], + wnafs: &[W], +) -> G { debug_assert_eq!(tables.len(), wnafs.len()); - let window_size = wnafs.iter().map(|w| w.len()).max().unwrap_or(0); + let window_size = wnafs.iter().map(|w| w.as_ref().len()).max().unwrap_or(0); let mut result = G::identity(); let mut found_one = false; @@ -176,15 +179,15 @@ pub(crate) fn wnaf_multi_exp(tables: &[&[G]], wnafs: &[&[i64]]) -> G { result = result.double(); } - for (&table, &wnaf) in tables.iter().zip(wnafs.iter()) { - let n = wnaf.get(i).copied().unwrap_or(0); + for (table, wnaf) in tables.iter().zip(wnafs.iter()) { + let n = wnaf.as_ref().get(i).copied().unwrap_or(0); if n != 0 { found_one = true; if n > 0 { - result += &table[(n / 2) as usize]; + result += table.as_ref()[(n / 2) as usize]; } else { - result -= &table[((-n) / 2) as usize]; + result -= table.as_ref()[((-n) / 2) as usize]; } } } @@ -520,17 +523,16 @@ impl WnafBase { } /// Perform a multiscalar multiplication. - pub fn multiscalar_mul<'a, I, J>(scalars: I, bases: J) -> G + /// + /// Computes a sum-of-products `aA + bB + ...` in variable time with w-NAF multi-exponentiation + /// using the interleaved window method, also known as Straus' method. + pub fn multiscalar_mul(scalars: I, bases: J) -> G where - I: Iterator>, - J: Iterator, + I: IntoIterator>, + J: IntoIterator, { - let wnafs = scalars - .map(|scalar| scalar.wnaf.as_slice()) - .collect::>(); - - let tables = bases.map(|base| base.table.as_slice()).collect::>(); - + let wnafs = scalars.into_iter().map(|s| s.wnaf).collect::>(); + let tables = bases.into_iter().map(|b| b.table).collect::>(); wnaf_multi_exp(tables.as_slice(), wnafs.as_slice()) } }