curve: add EdwardsPoint::compress_batch and inherent ::random (#759)

* curve: add `EdwardsPoint::compress_batch` and `::random`

We've had various requests to implement batch point compression for
`EdwardsPoint`, e.g. #705.

We can leverage `FieldElement::batch_invert` to implement it, which
results in a fairly significant speedup.

The name `EdwardsPoint::compress_batch` has been chosen to match
`RistrettoPoint::double_and_compress_batch`.

For benchmarking, randomized `EdwardsPoint`s have been used. To obtain
these, an inherent `EdwardsPoint::random` has been extracted from the
existing `Group::random` implementation, which uses rejection sampling.
`Group::random` has been updated to call the inherent
`EdwardsPoint::random`. This avoids a `group` dependency just to run the
batch compression benchmarks.

The following benchmark results have been obtained:

edwards benches/EdwardsPoint compression
                        time:   [3.5029 µs 3.5098 µs 3.5171 µs]

edwards benches/Batch EdwardsPoint compression/1
                        time:   [3.6698 µs 3.6758 µs 3.6817 µs]
edwards benches/Batch EdwardsPoint compression/2
                        time:   [3.8410 µs 3.8461 µs 3.8516 µs]
edwards benches/Batch EdwardsPoint compression/4
                        time:   [4.1534 µs 4.1961 µs 4.2558 µs]
edwards benches/Batch EdwardsPoint compression/8
                        time:   [4.8466 µs 4.8533 µs 4.8600 µs]
edwards benches/Batch EdwardsPoint compression/16
                        time:   [6.1216 µs 6.1315 µs 6.1410 µs]

As you can see, it affords a fairly significant speedup, batch
compressing 16 points in less time than the standard point compression
algorithm would take to compress 2 in a row.
This commit is contained in:
Tony Arcieri 2025-05-27 22:09:49 -06:00 committed by GitHub
parent e3b5328202
commit dd5bd108d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 101 additions and 15 deletions

View file

@ -22,6 +22,22 @@ mod edwards_benches {
c.bench_function("EdwardsPoint compression", move |b| b.iter(|| B.compress())); c.bench_function("EdwardsPoint compression", move |b| b.iter(|| B.compress()));
} }
#[cfg(feature = "alloc")]
fn compress_batch<M: Measurement>(c: &mut BenchmarkGroup<M>) {
for batch_size in BATCH_SIZES {
c.bench_with_input(
BenchmarkId::new("Batch EdwardsPoint compression", batch_size),
&batch_size,
|b, &size| {
let mut rng = OsRng;
let points: Vec<EdwardsPoint> =
(0..size).map(|_| EdwardsPoint::random(&mut rng)).collect();
b.iter(|| EdwardsPoint::compress_batch(&points));
},
);
}
}
fn decompress<M: Measurement>(c: &mut BenchmarkGroup<M>) { fn decompress<M: Measurement>(c: &mut BenchmarkGroup<M>) {
let B_comp = &constants::ED25519_BASEPOINT_COMPRESSED; let B_comp = &constants::ED25519_BASEPOINT_COMPRESSED;
c.bench_function("EdwardsPoint decompression", move |b| { c.bench_function("EdwardsPoint decompression", move |b| {
@ -62,6 +78,8 @@ mod edwards_benches {
compress(&mut g); compress(&mut g);
decompress(&mut g); decompress(&mut g);
#[cfg(feature = "alloc")]
compress_batch(&mut g);
consttime_fixed_base_scalar_mul(&mut g); consttime_fixed_base_scalar_mul(&mut g);
consttime_variable_base_scalar_mul(&mut g); consttime_variable_base_scalar_mul(&mut g);
vartime_double_base_scalar_mul(&mut g); vartime_double_base_scalar_mul(&mut g);

View file

@ -93,6 +93,7 @@
// affine and projective cakes and eat both of them too. // affine and projective cakes and eat both of them too.
#![allow(non_snake_case)] #![allow(non_snake_case)]
use cfg_if::cfg_if;
use core::array::TryFromSliceError; use core::array::TryFromSliceError;
use core::borrow::Borrow; use core::borrow::Borrow;
use core::fmt::Debug; use core::fmt::Debug;
@ -101,8 +102,6 @@ use core::ops::{Add, Neg, Sub};
use core::ops::{AddAssign, SubAssign}; use core::ops::{AddAssign, SubAssign};
use core::ops::{Mul, MulAssign}; use core::ops::{Mul, MulAssign};
use cfg_if::cfg_if;
#[cfg(feature = "digest")] #[cfg(feature = "digest")]
use digest::{generic_array::typenum::U64, Digest}; use digest::{generic_array::typenum::U64, Digest};
@ -112,7 +111,7 @@ use {
subtle::CtOption, subtle::CtOption,
}; };
#[cfg(feature = "group")] #[cfg(any(test, feature = "rand_core"))]
use rand_core::RngCore; use rand_core::RngCore;
use subtle::Choice; use subtle::Choice;
@ -151,6 +150,8 @@ use crate::traits::{Identity, IsIdentity};
use crate::traits::MultiscalarMul; use crate::traits::MultiscalarMul;
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
use crate::traits::{VartimeMultiscalarMul, VartimePrecomputedMultiscalarMul}; use crate::traits::{VartimeMultiscalarMul, VartimePrecomputedMultiscalarMul};
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Compressed points // Compressed points
@ -567,9 +568,31 @@ impl EdwardsPoint {
let recip = self.Z.invert(); let recip = self.Z.invert();
let x = &self.X * &recip; let x = &self.X * &recip;
let y = &self.Y * &recip; let y = &self.Y * &recip;
let mut s: [u8; 32]; Self::compress_affine(x, y)
}
s = y.as_bytes(); /// Compress several `EdwardsPoint`s into `CompressedEdwardsY` format, using a batch inversion
/// for a significant speedup.
#[cfg(feature = "alloc")]
pub fn compress_batch(inputs: &[EdwardsPoint]) -> Vec<CompressedEdwardsY> {
let mut zs = inputs.iter().map(|input| input.Z).collect::<Vec<_>>();
FieldElement::batch_invert(&mut zs);
inputs
.iter()
.zip(&zs)
.map(|(input, recip)| {
let x = &input.X * recip;
let y = &input.Y * recip;
Self::compress_affine(x, y)
})
.collect()
}
/// Compress affine Edwards coordinates into `CompressedEdwardsY` format.
#[inline]
fn compress_affine(x: FieldElement, y: FieldElement) -> CompressedEdwardsY {
let mut s = y.as_bytes();
s[31] ^= x.is_negative().unwrap_u8() << 7; s[31] ^= x.is_negative().unwrap_u8() << 7;
CompressedEdwardsY(s) CompressedEdwardsY(s)
} }
@ -605,6 +628,33 @@ impl EdwardsPoint {
.expect("Montgomery conversion to Edwards point in Elligator failed") .expect("Montgomery conversion to Edwards point in Elligator failed")
.mul_by_cofactor() .mul_by_cofactor()
} }
/// Return an `EdwardsPoint` chosen uniformly at random using a user-provided RNG.
///
/// # Inputs
///
/// * `rng`: any RNG which implements `RngCore`
///
/// # Returns
///
/// A random `EdwardsPoint`.
///
/// # Implementation
///
/// Uses rejection sampling, generating a random `CompressedEdwardsY` and then attempting point
/// decompression, rejecting invalid points.
#[cfg(any(test, feature = "rand_core"))]
pub fn random(mut rng: impl RngCore) -> Self {
let mut repr = CompressedEdwardsY([0u8; 32]);
loop {
rng.fill_bytes(&mut repr.0);
if let Some(p) = repr.decompress() {
if !IsIdentity::is_identity(&p) {
break p;
}
}
}
}
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -1291,16 +1341,9 @@ impl Debug for EdwardsPoint {
impl group::Group for EdwardsPoint { impl group::Group for EdwardsPoint {
type Scalar = Scalar; type Scalar = Scalar;
fn random(mut rng: impl RngCore) -> Self { fn random(rng: impl RngCore) -> Self {
let mut repr = CompressedEdwardsY([0u8; 32]); // Call the inherent `pub fn random` defined above
loop { Self::random(rng)
rng.fill_bytes(&mut repr.0);
if let Some(p) = repr.decompress() {
if !IsIdentity::is_identity(&p) {
break p;
}
}
}
} }
fn identity() -> Self { fn identity() -> Self {
@ -2019,6 +2062,31 @@ mod test {
EdwardsPoint::identity().compress(), EdwardsPoint::identity().compress(),
CompressedEdwardsY::identity() CompressedEdwardsY::identity()
); );
#[cfg(feature = "alloc")]
{
let compressed = EdwardsPoint::compress_batch(&[EdwardsPoint::identity()]);
assert_eq!(&compressed, &[CompressedEdwardsY::identity()]);
}
}
#[cfg(feature = "alloc")]
#[test]
fn compress_batch() {
let mut rng = rand::thread_rng();
// TODO(tarcieri): proptests?
// Make some points deterministically then randomly
let mut points = (1u64..16)
.map(|n| constants::ED25519_BASEPOINT_POINT * Scalar::from(n))
.collect::<Vec<_>>();
points.extend(core::iter::repeat_with(|| EdwardsPoint::random(&mut rng)).take(100));
let compressed = EdwardsPoint::compress_batch(&points);
// Check that the batch-compressed points match the individually compressed ones
for (point, compressed) in points.iter().zip(&compressed) {
assert_eq!(&point.compress(), compressed);
}
} }
#[test] #[test]