Skip to content

Commit 9e9226b

Browse files
committed
Simplify BatchInvert trait
Companion PR to RustCrypto/traits#2455, which switches to in-place batch inversion. This massively simplifies the batch normalization implementation which no longer requires a litany of generic parameters to abstract over slices versus arrays because it accepts a temporary storage buffer the caller can allocate however they choose. It requires every curve add an `impl BatchInvert for FieldElement` but also lets curves provide their own implementation, which is needed for `k256` due to its lazy normalization.
1 parent 9c6e851 commit 9e9226b

21 files changed

Lines changed: 163 additions & 194 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,5 @@ hash2curve = { path = "hash2curve" }
2828
primefield = { path = "primefield" }
2929
primeorder = { path = "primeorder" }
3030
wnaf = { path = "wnaf" }
31+
32+
elliptic-curve = { git = "https://github.com/RustCrypto/traits", branch = "batchinvert-redux" }

bignp256/src/arithmetic/field.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::U256;
1717
use elliptic_curve::{
1818
bigint::cpubits,
1919
ff::PrimeField,
20+
ops::BatchInvert,
2021
subtle::{Choice, ConstantTimeEq, CtOption},
2122
};
2223

@@ -92,6 +93,8 @@ primefield::fiat_monty_field_arithmetic! {
9293
selectnz: fiat_bignp256_selectznz
9394
}
9495

96+
impl BatchInvert for FieldElement {}
97+
9598
#[cfg(test)]
9699
mod tests {
97100
use super::{FieldElement, U256};

bp256/src/arithmetic/field.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::U256;
1414
use elliptic_curve::{
1515
bigint::cpubits,
1616
ff::PrimeField,
17+
ops::BatchInvert,
1718
subtle::{Choice, ConstantTimeEq, CtOption},
1819
};
1920

@@ -94,6 +95,8 @@ primefield::fiat_monty_field_arithmetic! {
9495
selectnz: fiat_bp256_selectznz
9596
}
9697

98+
impl BatchInvert for FieldElement {}
99+
97100
#[cfg(test)]
98101
mod tests {
99102
use super::{FieldElement, U256};

bp384/src/arithmetic/field.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::U384;
1414
use elliptic_curve::{
1515
bigint::cpubits,
1616
ff::PrimeField,
17+
ops::BatchInvert,
1718
subtle::{Choice, ConstantTimeEq, CtOption},
1819
};
1920

@@ -94,6 +95,8 @@ primefield::fiat_monty_field_arithmetic! {
9495
selectnz: fiat_bp384_selectznz
9596
}
9697

98+
impl BatchInvert for FieldElement {}
99+
97100
#[cfg(test)]
98101
mod tests {
99102
use super::{FieldElement, U384};

ed448-goldilocks/src/edwards/extended.rs

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,9 @@ impl CurveGroup for EdwardsPoint {
322322
#[inline]
323323
fn batch_normalize(projective: &[Self], affine: &mut [Self::Affine]) {
324324
assert_eq!(projective.len(), affine.len());
325-
let mut zs = alloc::vec![FieldElement::ONE; projective.len()];
326-
batch_normalize_generic(projective, zs.as_mut_slice(), affine);
325+
let mut zs = vec![FieldElement::ZERO; projective.len()];
326+
let mut scratch = vec![FieldElement::ZERO; projective.len()];
327+
batch_normalize_generic(projective, &mut zs, &mut scratch, affine);
327328
}
328329
}
329330

@@ -768,9 +769,10 @@ impl<const N: usize> BatchNormalize<[EdwardsPoint; N]> for EdwardsPoint {
768769

769770
#[inline]
770771
fn batch_normalize(points: &[Self; N]) -> [<Self as CurveGroup>::Affine; N] {
771-
let zs = [FieldElement::ONE; N];
772+
let mut zs = [FieldElement::ZERO; N];
773+
let mut scratch = [FieldElement::ZERO; N];
772774
let mut affine_points = [AffinePoint::IDENTITY; N];
773-
batch_normalize_generic(points, zs, &mut affine_points);
775+
batch_normalize_generic(points, &mut zs, &mut scratch, &mut affine_points);
774776
affine_points
775777
}
776778
}
@@ -783,43 +785,36 @@ impl BatchNormalize<[EdwardsPoint]> for EdwardsPoint {
783785
fn batch_normalize(points: &[Self]) -> Vec<<Self as CurveGroup>::Affine> {
784786
use alloc::vec;
785787

786-
let mut zs = vec![FieldElement::ONE; points.len()];
788+
let mut zs = vec![FieldElement::ZERO; points.len()];
789+
let mut scratch = vec![FieldElement::ZERO; points.len()];
787790
let mut affine_points = vec![AffinePoint::IDENTITY; points.len()];
788-
batch_normalize_generic(points, zs.as_mut_slice(), &mut affine_points);
791+
batch_normalize_generic(points, &mut zs, &mut scratch, &mut affine_points);
789792
affine_points
790793
}
791794
}
792795

793-
/// Generic implementation of batch normalization.
794-
fn batch_normalize_generic<P, Z, I, O>(points: &P, mut zs: Z, out: &mut O)
795-
where
796-
FieldElement: BatchInvert<Z, Output = CtOption<I>>,
797-
P: AsRef<[EdwardsPoint]> + ?Sized,
798-
Z: AsMut<[FieldElement]>,
799-
I: AsRef<[FieldElement]>,
800-
O: AsMut<[AffinePoint]> + ?Sized,
801-
{
802-
let points = points.as_ref();
803-
let out = out.as_mut();
796+
fn batch_normalize_generic(
797+
points: &[EdwardsPoint],
798+
zs: &mut [FieldElement],
799+
scratch: &mut [FieldElement],
800+
out: &mut [AffinePoint],
801+
) {
802+
debug_assert_eq!(points.len(), zs.len());
803+
debug_assert_eq!(points.len(), scratch.len());
804+
debug_assert_eq!(points.len(), out.len());
804805

805-
for (i, point) in points.iter().enumerate() {
806-
// Even a single zero value will fail inversion for the entire batch.
807-
// Put a dummy value (above `FieldElement::ONE`) so inversion succeeds
808-
// and treat that case specially later-on.
809-
zs.as_mut()[i].conditional_assign(&point.Z, !point.Z.ct_eq(&FieldElement::ZERO));
806+
for (z, point) in zs.iter_mut().zip(points) {
807+
*z = point.Z;
810808
}
811809

812-
// This is safe to unwrap since we assured that all elements are non-zero
813-
let zs_inverses = <FieldElement as BatchInvert<Z>>::batch_invert(zs)
814-
.expect("all elements should be non-zero");
810+
// Zero `zs` (identity) are handled explicitly below, so the `Choice` here is informational only
811+
let _ = FieldElement::batch_invert_in_place(zs, scratch);
815812

816813
for i in 0..out.len() {
817-
// If the `z` coordinate is non-zero, we can use it to invert;
818-
// otherwise it defaults to the `IDENTITY` value.
819814
out[i] = AffinePoint::conditional_select(
820815
&AffinePoint {
821-
x: points[i].X * zs_inverses.as_ref()[i],
822-
y: points[i].Y * zs_inverses.as_ref()[i],
816+
x: points[i].X * zs[i],
817+
y: points[i].Y * zs[i],
823818
},
824819
&AffinePoint::IDENTITY,
825820
points[i].Z.ct_eq(&FieldElement::ZERO),

ed448-goldilocks/src/field/element.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use crate::{
77
AffinePoint, Decaf448, DecafPoint, Ed448, EdwardsPoint,
88
curve::twedwards::extended::ExtendedPoint as TwistedExtendedPoint,
99
};
10-
use elliptic_curve::ops::Reduce;
1110
use elliptic_curve::{
1211
Field,
1312
array::Array,
@@ -16,6 +15,7 @@ use elliptic_curve::{
1615
consts::{U28, U56, U84, U88},
1716
modular::ConstMontyParams,
1817
},
18+
ops::{BatchInvert, Reduce},
1919
zeroize::DefaultIsZeroes,
2020
};
2121
use hash2curve::MapToCurve;
@@ -52,6 +52,8 @@ impl UpperHex for FieldElement {
5252
}
5353
}
5454

55+
impl BatchInvert for FieldElement {}
56+
5557
impl ConstantTimeEq for FieldElement {
5658
fn ct_eq(&self, other: &Self) -> Choice {
5759
self.0.ct_eq(&other.0)

ed448-goldilocks/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
//! Please see type-specific documentation for more information.
4141
4242
#[cfg(feature = "alloc")]
43+
#[macro_use]
4344
extern crate alloc;
4445
#[cfg(feature = "std")]
4546
extern crate std;

k256/src/arithmetic/field.rs

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use elliptic_curve::{
3030
Generate,
3131
bigint::{Odd, U256, modular::Retrieve},
3232
ff::{self, Field, PrimeField},
33-
ops::Invert,
33+
ops::{BatchInvert, Invert},
3434
rand_core::{TryCryptoRng, TryRng},
3535
subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption},
3636
zeroize::DefaultIsZeroes,
@@ -230,6 +230,47 @@ impl FieldElement {
230230
}
231231
}
232232

233+
impl BatchInvert for FieldElement {
234+
fn batch_invert_in_place(elements: &mut [Self], scratch: &mut [Self]) -> Choice {
235+
// Implements "Montgomery's trick", a trick for computing many modular inverses at once.
236+
//
237+
// "Montgomery's trick" works by reducing the problem of computing `n` inverses
238+
// to computing a single inversion, plus some storage and `O(n)` extra multiplications.
239+
//
240+
// See: https://iacr.org/archive/pkc2004/29470042/29470042.pdf section 2.2.
241+
assert_eq!(elements.len(), scratch.len());
242+
243+
// TODO(tarcieri): share implementation with `elliptic-curve` somehow
244+
// The only difference from the default impl: we need to normalize the elements first
245+
for e in elements.iter_mut() {
246+
*e = e.normalize();
247+
}
248+
249+
let mut acc = Self::ONE;
250+
let mut all_nonzero = Choice::from(1u8);
251+
252+
for (tmp, e) in scratch.iter_mut().zip(elements.iter()) {
253+
// $ a_n = a_{n-1}*x_n $
254+
*tmp = acc;
255+
let is_zero = e.ct_eq(&Self::ZERO);
256+
all_nonzero &= !is_zero;
257+
acc = Self::conditional_select(&(acc * e), &acc, is_zero);
258+
}
259+
260+
// `acc` is the product of every nonzero element, so this can't fail.
261+
acc = acc.invert().unwrap_or(Self::ONE);
262+
263+
for (e, tmp) in elements.iter_mut().zip(scratch.iter()).rev() {
264+
let is_zero = e.ct_eq(&Self::ZERO);
265+
let new_acc = Self::conditional_select(&(acc * *e), &acc, is_zero);
266+
*e = Self::conditional_select(&(acc * *tmp), e, is_zero);
267+
acc = new_acc;
268+
}
269+
270+
all_nonzero
271+
}
272+
}
273+
233274
impl Invert for FieldElement {
234275
type Output = CtOption<Self>;
235276

@@ -698,26 +739,14 @@ mod tests {
698739

699740
#[test]
700741
#[cfg(feature = "getrandom")]
701-
fn batch_invert_array() {
702-
let k: FieldElement = FieldElement::generate();
703-
let l: FieldElement = FieldElement::generate();
704-
705-
let expected = [k.invert().unwrap(), l.invert().unwrap()];
706-
let actual = <FieldElement as BatchInvert<_>>::batch_invert([k, l]).unwrap();
707-
708-
assert_eq!(expected[0], actual[0].normalize());
709-
assert_eq!(expected[1], actual[1].normalize());
710-
}
711-
712-
#[test]
713-
#[cfg(all(feature = "alloc", feature = "getrandom"))]
714742
fn batch_invert() {
715743
let k: FieldElement = FieldElement::generate();
716744
let l: FieldElement = FieldElement::generate();
717745

718-
let expected = vec![k.invert().unwrap(), l.invert().unwrap()];
719-
let field_elements = vec![k, l]; // to test impl of `BatchInvert` for `Vec`
720-
let actual = <FieldElement as BatchInvert<_>>::batch_invert(field_elements).unwrap();
746+
let expected = [k.invert().unwrap(), l.invert().unwrap()];
747+
let mut actual = [k, l];
748+
let mut scratch = [FieldElement::ZERO; 2];
749+
FieldElement::batch_invert_in_place(&mut actual, &mut scratch);
721750

722751
assert_eq!(expected[0], actual[0].normalize());
723752
assert_eq!(expected[1], actual[1].normalize());

k256/src/arithmetic/projective.rs

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,10 @@ impl<const N: usize> BatchNormalize<[ProjectivePoint; N]> for ProjectivePoint {
257257

258258
#[inline]
259259
fn batch_normalize(points: &[Self; N]) -> [<Self as CurveGroup>::Affine; N] {
260-
let zs = [FieldElement::ONE; N];
260+
let mut zs = [FieldElement::ZERO; N];
261+
let mut scratch = [FieldElement::ZERO; N];
261262
let mut affine_points = [AffinePoint::IDENTITY; N];
262-
batch_normalize_generic(points, zs, &mut affine_points);
263+
batch_normalize(points, &mut zs, &mut scratch, &mut affine_points);
263264
affine_points
264265
}
265266
}
@@ -270,40 +271,34 @@ impl BatchNormalize<[ProjectivePoint]> for ProjectivePoint {
270271

271272
#[inline]
272273
fn batch_normalize(points: &[Self]) -> Vec<<Self as CurveGroup>::Affine> {
273-
let zs = vec![FieldElement::ONE; points.len()];
274+
let mut zs = vec![FieldElement::ZERO; points.len()];
275+
let mut scratch = vec![FieldElement::ZERO; points.len()];
274276
let mut affine_points = vec![AffinePoint::IDENTITY; points.len()];
275-
batch_normalize_generic(points, zs, &mut affine_points);
277+
batch_normalize(points, &mut zs, &mut scratch, &mut affine_points);
276278
affine_points
277279
}
278280
}
279281

280-
fn batch_normalize_generic<P, Z, I, O>(points: &P, mut zs: Z, out: &mut O)
281-
where
282-
FieldElement: BatchInvert<Z, Output = CtOption<I>>,
283-
P: AsRef<[ProjectivePoint]> + ?Sized,
284-
Z: AsMut<[FieldElement]>,
285-
I: AsRef<[FieldElement]>,
286-
O: AsMut<[AffinePoint]> + ?Sized,
287-
{
288-
let points = points.as_ref();
289-
let out = out.as_mut();
282+
fn batch_normalize(
283+
points: &[ProjectivePoint],
284+
zs: &mut [FieldElement],
285+
scratch: &mut [FieldElement],
286+
out: &mut [AffinePoint],
287+
) {
288+
debug_assert_eq!(points.len(), zs.len());
289+
debug_assert_eq!(points.len(), scratch.len());
290+
debug_assert_eq!(points.len(), out.len());
290291

291-
for i in 0..points.len() {
292-
// Even a single zero value will fail inversion for the entire batch.
293-
// Put a dummy value (above `FieldElement::ONE`) so inversion succeeds
294-
// and treat that case specially later-on.
295-
zs.as_mut()[i].conditional_assign(&points[i].z, !points[i].z.normalizes_to_zero());
292+
for (z, point) in zs.iter_mut().zip(points) {
293+
*z = point.z;
296294
}
297295

298-
// This is safe to unwrap since we assured that all elements are non-zero
299-
let zs_inverses = <FieldElement as BatchInvert<Z>>::batch_invert(zs)
300-
.expect("all elements should be non-zero");
296+
// Zero `zs` (identity) are handled explicitly below, so the `Choice` here is informational only
297+
let _ = FieldElement::batch_invert_in_place(zs, scratch);
301298

302299
for i in 0..out.len() {
303-
// If the `z` coordinate is non-zero, we can use it to invert;
304-
// otherwise it defaults to the `IDENTITY` value.
305300
out[i] = AffinePoint::conditional_select(
306-
&points[i].to_affine_internal(zs_inverses.as_ref()[i]),
301+
&points[i].to_affine_internal(zs[i]),
307302
&AffinePoint::IDENTITY,
308303
points[i].z.normalizes_to_zero(),
309304
);
@@ -455,8 +450,9 @@ impl CurveGroup for ProjectivePoint {
455450
#[inline]
456451
fn batch_normalize(projective: &[Self], affine: &mut [Self::Affine]) {
457452
assert_eq!(projective.len(), affine.len());
458-
let mut zs = vec![FieldElement::ONE; projective.len()];
459-
batch_normalize_generic(projective, zs.as_mut_slice(), affine);
453+
let mut zs = vec![FieldElement::ZERO; projective.len()];
454+
let mut scratch = vec![FieldElement::ZERO; projective.len()];
455+
batch_normalize(projective, &mut zs, &mut scratch, affine);
460456
}
461457
}
462458

0 commit comments

Comments
 (0)