Skip to content

Commit dd5bd10

Browse files
authored
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.
1 parent e3b5328 commit dd5bd10

File tree

2 files changed

+101
-15
lines changed

2 files changed

+101
-15
lines changed

curve25519-dalek/benches/dalek_benchmarks.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ mod edwards_benches {
2222
c.bench_function("EdwardsPoint compression", move |b| b.iter(|| B.compress()));
2323
}
2424

25+
#[cfg(feature = "alloc")]
26+
fn compress_batch<M: Measurement>(c: &mut BenchmarkGroup<M>) {
27+
for batch_size in BATCH_SIZES {
28+
c.bench_with_input(
29+
BenchmarkId::new("Batch EdwardsPoint compression", batch_size),
30+
&batch_size,
31+
|b, &size| {
32+
let mut rng = OsRng;
33+
let points: Vec<EdwardsPoint> =
34+
(0..size).map(|_| EdwardsPoint::random(&mut rng)).collect();
35+
b.iter(|| EdwardsPoint::compress_batch(&points));
36+
},
37+
);
38+
}
39+
}
40+
2541
fn decompress<M: Measurement>(c: &mut BenchmarkGroup<M>) {
2642
let B_comp = &constants::ED25519_BASEPOINT_COMPRESSED;
2743
c.bench_function("EdwardsPoint decompression", move |b| {
@@ -62,6 +78,8 @@ mod edwards_benches {
6278

6379
compress(&mut g);
6480
decompress(&mut g);
81+
#[cfg(feature = "alloc")]
82+
compress_batch(&mut g);
6583
consttime_fixed_base_scalar_mul(&mut g);
6684
consttime_variable_base_scalar_mul(&mut g);
6785
vartime_double_base_scalar_mul(&mut g);

curve25519-dalek/src/edwards.rs

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
// affine and projective cakes and eat both of them too.
9494
#![allow(non_snake_case)]
9595

96+
use cfg_if::cfg_if;
9697
use core::array::TryFromSliceError;
9798
use core::borrow::Borrow;
9899
use core::fmt::Debug;
@@ -101,8 +102,6 @@ use core::ops::{Add, Neg, Sub};
101102
use core::ops::{AddAssign, SubAssign};
102103
use core::ops::{Mul, MulAssign};
103104

104-
use cfg_if::cfg_if;
105-
106105
#[cfg(feature = "digest")]
107106
use digest::{generic_array::typenum::U64, Digest};
108107

@@ -112,7 +111,7 @@ use {
112111
subtle::CtOption,
113112
};
114113

115-
#[cfg(feature = "group")]
114+
#[cfg(any(test, feature = "rand_core"))]
116115
use rand_core::RngCore;
117116

118117
use subtle::Choice;
@@ -151,6 +150,8 @@ use crate::traits::{Identity, IsIdentity};
151150
use crate::traits::MultiscalarMul;
152151
#[cfg(feature = "alloc")]
153152
use crate::traits::{VartimeMultiscalarMul, VartimePrecomputedMultiscalarMul};
153+
#[cfg(feature = "alloc")]
154+
use alloc::vec::Vec;
154155

155156
// ------------------------------------------------------------------------
156157
// Compressed points
@@ -567,9 +568,31 @@ impl EdwardsPoint {
567568
let recip = self.Z.invert();
568569
let x = &self.X * &recip;
569570
let y = &self.Y * &recip;
570-
let mut s: [u8; 32];
571+
Self::compress_affine(x, y)
572+
}
571573

572-
s = y.as_bytes();
574+
/// Compress several `EdwardsPoint`s into `CompressedEdwardsY` format, using a batch inversion
575+
/// for a significant speedup.
576+
#[cfg(feature = "alloc")]
577+
pub fn compress_batch(inputs: &[EdwardsPoint]) -> Vec<CompressedEdwardsY> {
578+
let mut zs = inputs.iter().map(|input| input.Z).collect::<Vec<_>>();
579+
FieldElement::batch_invert(&mut zs);
580+
581+
inputs
582+
.iter()
583+
.zip(&zs)
584+
.map(|(input, recip)| {
585+
let x = &input.X * recip;
586+
let y = &input.Y * recip;
587+
Self::compress_affine(x, y)
588+
})
589+
.collect()
590+
}
591+
592+
/// Compress affine Edwards coordinates into `CompressedEdwardsY` format.
593+
#[inline]
594+
fn compress_affine(x: FieldElement, y: FieldElement) -> CompressedEdwardsY {
595+
let mut s = y.as_bytes();
573596
s[31] ^= x.is_negative().unwrap_u8() << 7;
574597
CompressedEdwardsY(s)
575598
}
@@ -605,6 +628,33 @@ impl EdwardsPoint {
605628
.expect("Montgomery conversion to Edwards point in Elligator failed")
606629
.mul_by_cofactor()
607630
}
631+
632+
/// Return an `EdwardsPoint` chosen uniformly at random using a user-provided RNG.
633+
///
634+
/// # Inputs
635+
///
636+
/// * `rng`: any RNG which implements `RngCore`
637+
///
638+
/// # Returns
639+
///
640+
/// A random `EdwardsPoint`.
641+
///
642+
/// # Implementation
643+
///
644+
/// Uses rejection sampling, generating a random `CompressedEdwardsY` and then attempting point
645+
/// decompression, rejecting invalid points.
646+
#[cfg(any(test, feature = "rand_core"))]
647+
pub fn random(mut rng: impl RngCore) -> Self {
648+
let mut repr = CompressedEdwardsY([0u8; 32]);
649+
loop {
650+
rng.fill_bytes(&mut repr.0);
651+
if let Some(p) = repr.decompress() {
652+
if !IsIdentity::is_identity(&p) {
653+
break p;
654+
}
655+
}
656+
}
657+
}
608658
}
609659

610660
// ------------------------------------------------------------------------
@@ -1291,16 +1341,9 @@ impl Debug for EdwardsPoint {
12911341
impl group::Group for EdwardsPoint {
12921342
type Scalar = Scalar;
12931343

1294-
fn random(mut rng: impl RngCore) -> Self {
1295-
let mut repr = CompressedEdwardsY([0u8; 32]);
1296-
loop {
1297-
rng.fill_bytes(&mut repr.0);
1298-
if let Some(p) = repr.decompress() {
1299-
if !IsIdentity::is_identity(&p) {
1300-
break p;
1301-
}
1302-
}
1303-
}
1344+
fn random(rng: impl RngCore) -> Self {
1345+
// Call the inherent `pub fn random` defined above
1346+
Self::random(rng)
13041347
}
13051348

13061349
fn identity() -> Self {
@@ -2019,6 +2062,31 @@ mod test {
20192062
EdwardsPoint::identity().compress(),
20202063
CompressedEdwardsY::identity()
20212064
);
2065+
2066+
#[cfg(feature = "alloc")]
2067+
{
2068+
let compressed = EdwardsPoint::compress_batch(&[EdwardsPoint::identity()]);
2069+
assert_eq!(&compressed, &[CompressedEdwardsY::identity()]);
2070+
}
2071+
}
2072+
2073+
#[cfg(feature = "alloc")]
2074+
#[test]
2075+
fn compress_batch() {
2076+
let mut rng = rand::thread_rng();
2077+
2078+
// TODO(tarcieri): proptests?
2079+
// Make some points deterministically then randomly
2080+
let mut points = (1u64..16)
2081+
.map(|n| constants::ED25519_BASEPOINT_POINT * Scalar::from(n))
2082+
.collect::<Vec<_>>();
2083+
points.extend(core::iter::repeat_with(|| EdwardsPoint::random(&mut rng)).take(100));
2084+
let compressed = EdwardsPoint::compress_batch(&points);
2085+
2086+
// Check that the batch-compressed points match the individually compressed ones
2087+
for (point, compressed) in points.iter().zip(&compressed) {
2088+
assert_eq!(&point.compress(), compressed);
2089+
}
20222090
}
20232091

20242092
#[test]

0 commit comments

Comments
 (0)