From 5590b8bc2a0bc8529fbb0bfb77d487782fca541b Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Wed, 6 May 2026 18:37:43 -0600 Subject: [PATCH] primeorder: faster `lincomb_vartime` when `alloc` enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion PR to RustCrypto/group#14, which adds `WnafBase::multiscalar_mul` using Straus' method. When the `alloc` feature is enabled, this is used to compute `LinearCombination::lincomb_vartime`. Also includes updated tests and benchmarks for `p256` to ensure `lincomb_vartime` computes the same results as using the `Add` and `Mul` traits, and show the relative performance improvement, which is a ~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) --- Cargo.lock | 3 +- Cargo.toml | 2 ++ p256/benches/point.rs | 67 ++++++++++++++++++++++++++++++++---- p256/benches/scalar.rs | 41 ++-------------------- p256/tests/projective.rs | 27 ++++++++++++--- primeorder/src/projective.rs | 25 +++++++++++++- 6 files changed, 113 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3412ac9e4..6f524ee64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1223,8 +1223,7 @@ dependencies = [ [[package]] name = "rustcrypto-group" version = "0.14.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "369f9b61aa45933c062c9f6b5c3c50ab710687eca83dd3802653b140b43f85ed" +source = "git+https://github.com/RustCrypto/group#d5ef2775cb66a10085c9ec48432c51cad4e7b3ee" dependencies = [ "rand_core 0.10.1", "rustcrypto-ff", diff --git a/Cargo.toml b/Cargo.toml index 879ffdb4d..dfd952412 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,5 @@ ed448-goldilocks = { path = "ed448-goldilocks" } hash2curve = { path = "hash2curve" } primefield = { path = "primefield" } primeorder = { path = "primeorder" } + +rustcrypto-group = { git = "https://github.com/RustCrypto/group" } diff --git a/p256/benches/point.rs b/p256/benches/point.rs index 5af7a25d0..417bb7452 100644 --- a/p256/benches/point.rs +++ b/p256/benches/point.rs @@ -1,18 +1,73 @@ //! p256 `ProjectivePoint` benchmarks -use criterion::{Criterion, criterion_group, criterion_main}; -use p256::ProjectivePoint; +use criterion::{ + BenchmarkGroup, Criterion, criterion_group, criterion_main, measurement::Measurement, +}; +use hex_literal::hex; +use p256::{ + ProjectivePoint, Scalar, + elliptic_curve::{ + Group, PrimeField, + ops::{LinearCombination, MulByGeneratorVartime, MulVartime}, + }, +}; use primefield::subtle::ConstantTimeEq; +use std::hint::black_box; -fn bench_projective(c: &mut Criterion) { - let mut group = c.benchmark_group("ProjectivePoint constant time operations"); +fn test_scalar() -> Scalar { + Scalar::from_repr( + hex!("519b423d715f8b581f4fa8ee59f4771a5b44c8130b4e3eacca54a56dda72b464").into(), + ) + .unwrap() +} + +fn bench_point_lincomb(group: &mut BenchmarkGroup<'_, M>) { + let p = ProjectivePoint::GENERATOR; + let s = test_scalar(); + + group.bench_function("point-scalar lincomb", |b| { + b.iter(|| ProjectivePoint::lincomb(&[(p, s), (p, s), (p, s)])) + }); + group.bench_function("point-scalar lincomb (variable-time)", |b| { + b.iter(|| ProjectivePoint::lincomb_vartime(&[(p, s), (p, s), (p, s)])) + }); +} + +fn bench_point_mul(group: &mut BenchmarkGroup<'_, M>) { + let p = ProjectivePoint::GENERATOR; + let s = test_scalar(); + group.bench_function("point-scalar mul", |b| { + b.iter(|| black_box(p) * black_box(s)) + }); + group.bench_function("point-scalar mul (variable-time)", |b| { + b.iter(|| black_box(p).mul_vartime(&black_box(s))) + }); +} + +fn bench_point_mul_by_generator(group: &mut BenchmarkGroup<'_, M>) { + let m = test_scalar(); + let s = Scalar::from_repr(m.into()).unwrap(); + group.bench_function("generator-scalar mul", |b| { + b.iter(|| ProjectivePoint::mul_by_generator(&black_box(s))) + }); + group.bench_function("generator-scalar mul (variable-time)", |b| { + b.iter(|| ProjectivePoint::mul_by_generator_vartime(&black_box(s))) + }); +} + +fn bench_point(c: &mut Criterion) { + let mut group = c.benchmark_group("ProjectivePoint operations"); + + bench_point_lincomb(&mut group); + bench_point_mul(&mut group); + bench_point_mul_by_generator(&mut group); - group.bench_function("point_generator_ct_eq", |b| { + group.bench_function("ct_eq", |b| { b.iter(|| ProjectivePoint::GENERATOR.ct_eq(&ProjectivePoint::GENERATOR)) }); group.finish(); } -criterion_group!(benches, bench_projective); +criterion_group!(benches, bench_point); criterion_main!(benches); diff --git a/p256/benches/scalar.rs b/p256/benches/scalar.rs index a63e30049..fc74e0c86 100644 --- a/p256/benches/scalar.rs +++ b/p256/benches/scalar.rs @@ -4,14 +4,7 @@ use criterion::{ BenchmarkGroup, Criterion, criterion_group, criterion_main, measurement::Measurement, }; use hex_literal::hex; -use p256::{ - ProjectivePoint, Scalar, - elliptic_curve::{ - Group, - group::ff::PrimeField, - ops::{MulByGeneratorVartime, MulVartime}, - }, -}; +use p256::{Scalar, elliptic_curve::PrimeField}; use std::hint::black_box; fn test_scalar_x() -> Scalar { @@ -28,29 +21,6 @@ fn test_scalar_y() -> Scalar { .unwrap() } -fn bench_point_mul(group: &mut BenchmarkGroup<'_, M>) { - let p = ProjectivePoint::GENERATOR; - let m = test_scalar_x(); - let s = Scalar::from_repr(m.into()).unwrap(); - group.bench_function("point-scalar mul", |b| { - b.iter(|| black_box(p) * black_box(s)) - }); - group.bench_function("point-scalar mul (variable-time)", |b| { - b.iter(|| black_box(p).mul_vartime(&black_box(s))) - }); -} - -fn bench_point_mul_by_generator(group: &mut BenchmarkGroup<'_, M>) { - let m = test_scalar_x(); - let s = Scalar::from_repr(m.into()).unwrap(); - group.bench_function("generator-scalar mul", |b| { - b.iter(|| ProjectivePoint::mul_by_generator(&black_box(s))) - }); - group.bench_function("generator-scalar mul (variable-time)", |b| { - b.iter(|| ProjectivePoint::mul_by_generator_vartime(&black_box(s))) - }); -} - fn bench_scalar_sub(group: &mut BenchmarkGroup<'_, M>) { let x = test_scalar_x(); let y = test_scalar_y(); @@ -79,22 +49,15 @@ fn bench_scalar_invert(group: &mut BenchmarkGroup<'_, M>) { group.bench_function("invert", |b| b.iter(|| x.invert())); } -fn bench_point(c: &mut Criterion) { - let mut group = c.benchmark_group("point operations"); - bench_point_mul(&mut group); - group.finish(); -} - fn bench_scalar(c: &mut Criterion) { let mut group = c.benchmark_group("scalar operations"); bench_scalar_sub(&mut group); bench_scalar_add(&mut group); bench_scalar_mul(&mut group); - bench_point_mul_by_generator(&mut group); bench_scalar_negate(&mut group); bench_scalar_invert(&mut group); group.finish(); } -criterion_group!(benches, bench_point, bench_scalar); +criterion_group!(benches, bench_scalar); criterion_main!(benches); diff --git a/p256/tests/projective.rs b/p256/tests/projective.rs index 24e1cae16..00510a89b 100644 --- a/p256/tests/projective.rs +++ b/p256/tests/projective.rs @@ -105,9 +105,11 @@ proptest! { s1 in scalar(), p2 in projective(), s2 in scalar(), + p3 in projective(), + s3 in scalar(), ) { - let reference = p1 * s1 + p2 * s2; - let test = ProjectivePoint::lincomb(&[(p1, s1), (p2, s2)]); + let reference = p1 * s1 + p2 * s2 + p3 * s3; + let test = ProjectivePoint::lincomb(&[(p1, s1), (p2, s2), (p3, s3)]); assert_eq!(reference, test); } @@ -118,9 +120,26 @@ proptest! { s1 in scalar(), p2 in projective(), s2 in scalar(), + p3 in projective(), + s3 in scalar(), ) { - let reference = p1 * s1 + p2 * s2; - let test = ProjectivePoint::lincomb(vec![(p1, s1), (p2, s2)].as_slice()); + let reference = p1 * s1 + p2 * s2 + p3 * s3; + let test = ProjectivePoint::lincomb(vec![(p1, s1), (p2, s2), (p3, s3)].as_slice()); + assert_eq!(reference, test); + } + + #[test] + #[cfg(feature = "alloc")] + fn lincomb_vartime( + p1 in projective(), + s1 in scalar(), + p2 in projective(), + s2 in scalar(), + p3 in projective(), + s3 in scalar(), + ) { + let reference = p1 * s1 + p2 * s2 + p3 * s3; + let test = ProjectivePoint::lincomb_vartime(vec![(p1, s1), (p2, s2), (p3, s3)].as_slice()); assert_eq!(reference, test); } } diff --git a/primeorder/src/projective.rs b/primeorder/src/projective.rs index ac8c66b06..0c169b71b 100644 --- a/primeorder/src/projective.rs +++ b/primeorder/src/projective.rs @@ -31,7 +31,7 @@ use elliptic_curve::{ #[cfg(feature = "alloc")] use { alloc::vec::Vec, - elliptic_curve::group::{Wnaf, WnafGroup}, + elliptic_curve::group::{Wnaf, WnafBase, WnafGroup, WnafScalar}, }; #[cfg(all(feature = "alloc", feature = "basepoint-table"))] @@ -494,6 +494,23 @@ where lincomb::(&mut ks, &mut pcs) } + + #[cfg(feature = "alloc")] + fn lincomb_vartime(points_and_scalars: &[(Self, Scalar)]) -> Self { + // TODO(tarcieri): make this customizable? + const WINDOW_SIZE: usize = 4; + let points = points_and_scalars + .iter() + .map(|(point, _)| WnafBase::::new(*point)) + .collect::>(); + + let scalars = points_and_scalars + .iter() + .map(|(_, scalar)| WnafScalar::, WINDOW_SIZE>::new(scalar)) + .collect::>(); + + WnafBase::multiscalar_mul(scalars.iter(), points.iter()) + } } impl LinearCombination<[(Self, Scalar); N]> for ProjectivePoint @@ -508,6 +525,11 @@ where lincomb::(&mut ks, &mut pcs) } + + #[cfg(feature = "alloc")] + fn lincomb_vartime(points_and_scalars: &[(Self, Scalar); N]) -> Self { + Self::lincomb_vartime(points_and_scalars.as_slice()) + } } fn lincomb( @@ -950,6 +972,7 @@ where // When we're guaranteed *not* to have basepoint tables available (because they need `alloc`) // use linear combinations for this computation, but they're slower when they are available + // TODO(tarcieri): `WnafBase::multiscalar_mul` w\ basepoint table when `alloc` *is* available #[cfg(not(feature = "alloc"))] fn mul_by_generator_and_mul_add_vartime( a: &Self::Scalar,