Skip to content

Commit c2fad8b

Browse files
authored
Merge pull request #20 from fjarri/lucas-sequence
Lucas sequence optimization
2 parents ff4d9ac + 21a6520 commit c2fad8b

File tree

5 files changed

+117
-89
lines changed

5 files changed

+117
-89
lines changed

.github/workflows/crypto-primes.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ jobs:
111111
components: clippy
112112
override: true
113113
profile: minimal
114-
- run: cargo clippy --all --all-features --tests -- -D warnings
114+
- run: cargo clippy --all --all-features --tests --benches -- -D warnings
115115

116116
rustfmt:
117117
runs-on: ubuntu-latest

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
44
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
55

66

7+
## [Unreleased]
8+
9+
### Added
10+
11+
- An alternative propagation method for Lucas sequences improving the performance of `lucas_test()`. ([#20])
12+
13+
14+
### Fixed
15+
16+
- Some mistakes in the description of Lucas checks (the logic itself was fine). ([#20])
17+
18+
19+
[#20]: https://github.com/nucypher/rust-umbral/pull/20
20+
21+
722
## [0.2.0] - 2023-03-06
823

924
### Changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ readme = "README.md"
99
categories = ["cryptography", "no-std"]
1010

1111
[dependencies]
12-
crypto-bigint = { version = "0.5", default-features = false, features = ["rand_core"] }
12+
crypto-bigint = { version = "0.5.2", default-features = false, features = ["rand_core"] }
1313
rand_core = { version = "0.6", default-features = false }
1414
getrandom = { version = "0.2", optional = true, default-features = false, features = ["js"] }
1515
openssl = { version = "0.10", optional = true, features = ["vendored"] }

benches/bench.rs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1-
use rand_core::OsRng;
2-
31
use criterion::{
42
criterion_group, criterion_main, measurement::Measurement, BenchmarkGroup, Criterion,
53
};
64
use crypto_bigint::{U1024, U128};
5+
use rand_chacha::ChaCha8Rng;
6+
use rand_core::{OsRng, SeedableRng};
77

88
use crypto_primes::{
99
hazmat::{
10-
lucas_test, random_odd_uint, BruteForceBase, LucasCheck, MillerRabin, SelfridgeBase, Sieve,
10+
lucas_test, random_odd_uint, AStarBase, BruteForceBase, LucasCheck, MillerRabin,
11+
SelfridgeBase, Sieve,
1112
},
1213
safe_prime_with_rng,
1314
};
1415

15-
fn bench_sieve<'a, M: Measurement>(group: &mut BenchmarkGroup<'a, M>) {
16+
fn bench_sieve<M: Measurement>(group: &mut BenchmarkGroup<'_, M>) {
1617
let start: U1024 = random_odd_uint(&mut OsRng, 1024);
1718
group.bench_function("(U1024) Sieve, 1000 samples", |b| {
1819
b.iter(|| Sieve::new(&start, 1024, false).take(1000).for_each(drop))
1920
});
2021
}
2122

22-
fn bench_miller_rabin<'a, M: Measurement>(group: &mut BenchmarkGroup<'a, M>) {
23+
fn bench_miller_rabin<M: Measurement>(group: &mut BenchmarkGroup<'_, M>) {
2324
let start: U1024 = random_odd_uint(&mut OsRng, 1024);
2425
group.bench_function("(U1024) Miller-Rabin creation", |b| {
2526
b.iter(|| {
@@ -40,15 +41,28 @@ fn bench_miller_rabin<'a, M: Measurement>(group: &mut BenchmarkGroup<'a, M>) {
4041
);
4142
}
4243

43-
fn bench_lucas<'a, M: Measurement>(group: &mut BenchmarkGroup<'a, M>) {
44-
let start: U1024 = random_odd_uint(&mut OsRng, 1024);
44+
fn bench_lucas<M: Measurement>(group: &mut BenchmarkGroup<'_, M>) {
45+
let mut rng = ChaCha8Rng::from_seed(*b"01234567890123456789012345678901");
46+
47+
let start: U1024 = random_odd_uint(&mut rng, 1024);
4548
let mut sieve = Sieve::new(&start, 1024, false);
46-
group.bench_function("(U1024) Sieve + Lucas test (Selfridge base)", |b| {
49+
group.bench_function(
50+
"(U1024) Sieve + Lucas test (Selfridge base, strong check)",
51+
|b| {
52+
b.iter(|| {
53+
lucas_test(&sieve.next().unwrap(), SelfridgeBase, LucasCheck::Strong);
54+
})
55+
},
56+
);
57+
58+
let mut sieve = Sieve::new(&start, 1024, false);
59+
group.bench_function("(U1024) Sieve + Lucas test (A* base, Lucas-V check)", |b| {
4760
b.iter(|| {
48-
lucas_test(&sieve.next().unwrap(), SelfridgeBase, LucasCheck::Strong);
61+
lucas_test(&sieve.next().unwrap(), AStarBase, LucasCheck::LucasV);
4962
})
5063
});
5164

65+
let mut sieve = Sieve::new(&start, 1024, false);
5266
group.bench_function(
5367
"(U1024) Sieve + Lucas test (brute force base, almost extra strong)",
5468
|b| {
@@ -62,6 +76,7 @@ fn bench_lucas<'a, M: Measurement>(group: &mut BenchmarkGroup<'a, M>) {
6276
},
6377
);
6478

79+
let mut sieve = Sieve::new(&start, 1024, false);
6580
group.bench_function(
6681
"(U1024) Sieve + Lucas test (brute force base, extra strong)",
6782
|b| {
@@ -102,7 +117,7 @@ fn bench_primality_tests(c: &mut Criterion) {
102117
group.finish();
103118
}
104119

105-
fn bench_presets<'a, M: Measurement>(group: &mut BenchmarkGroup<'a, M>) {
120+
fn bench_presets<M: Measurement>(group: &mut BenchmarkGroup<'_, M>) {
106121
group.bench_function("(U128) Random safe prime", |b| {
107122
b.iter(|| {
108123
let p: U128 = safe_prime_with_rng(&mut OsRng, 128);

src/hazmat/lucas.rs

Lines changed: 75 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Lucas primality test.
22
use crypto_bigint::{
33
modular::runtime_mod::{DynResidue, DynResidueParams},
4-
Integer, Invert, Limb, Uint, Word,
4+
Integer, Limb, Uint, Word,
55
};
66

77
use super::{
@@ -197,7 +197,10 @@ fn decompose<const L: usize>(n: &Uint<L>) -> (u32, Uint<L>) {
197197
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
198198
pub enum LucasCheck {
199199
/// Introduced by Baillie & Wagstaff[^Baillie1980].
200-
/// If any of `V(d*2^r) == 0` for `0 <= r < s`, and `U(d) == 0`, report the number as prime.
200+
/// If either of the following is true:
201+
/// - any of `V(d*2^r) == 0` for `0 <= r < s`,
202+
/// - `U(d) == 0`,
203+
/// report the number as prime.
201204
///
202205
/// If the base is [`SelfridgeBase`], known false positives constitute OEIS:A217255[^A217255].
203206
///
@@ -210,8 +213,14 @@ pub enum LucasCheck {
210213
/// [^A217255]: <https://oeis.org/A217255>
211214
Strong,
212215

213-
/// If any of `V(d*2^r) == 0` for `0 <= r < s`, and `V(d) == ±2` report the number as prime.
214-
/// The second condition is only checked if `Q == 1`, otherwise it is considered to be true.
216+
/// A [`LucasCheck::ExtraStrong`] without checking for `U(d)`.
217+
/// That is, if either of the following is true:
218+
/// - any of `V(d*2^r) == 0` for `0 <= r < s`,
219+
/// - `V(d) == ±2`,
220+
/// report the number as prime.
221+
///
222+
/// Note: the second condition is only checked if `Q == 1`,
223+
/// otherwise it is considered to be true.
215224
///
216225
/// If the base is [`BruteForceBase`], some known false positives
217226
/// are listed by Jacobsen[^Jacobsen].
@@ -225,7 +234,9 @@ pub enum LucasCheck {
225234
AlmostExtraStrong,
226235

227236
/// Introduced by Mo[^Mo1993], and also described by Grantham[^Grantham2001].
228-
/// If [`LucasCheck::Strong`] check passes, and `V(d) == ±2`,
237+
/// If either of the following is true:
238+
/// - any of `V(d*2^r) == 0` for `0 <= r < s`,
239+
/// - `U(d) == 0` and `V(d) == ±2`,
229240
/// report the number as prime.
230241
///
231242
/// Note that this check only differs from [`LucasCheck::Strong`] if `Q == 1`.
@@ -335,102 +346,89 @@ pub fn lucas_test<const L: usize>(
335346
DynResidue::<L>::new(&Uint::<L>::from(p), params)
336347
};
337348

338-
// Compute d-th element of Lucas sequence V_d(P, Q), where:
339-
//
340-
// V_0 = 2
341-
// V_1 = P
342-
// V_k = P V_{k-1} - Q V_{k-2}.
349+
// Compute d-th element of Lucas sequence (U_d(P, Q), V_d(P, Q)), where:
343350
//
344-
// In general V(k) = α^k + β^k, where α and β are roots of x^2 - Px + Q.
345-
// [^Crandall2005], eq. (3.14) observe that for 0 <= j <= k,
351+
// V_0 = 2
352+
// U_0 = 1
346353
//
347-
// V_{j+k} = V_j V_k - Q^j * V_{k-j}.
354+
// U_{2k} = U_k V_k
355+
// V_{2k} = V_k^2 - 2 Q^k
348356
//
349-
// So in particular, to quickly double the subscript:
350-
//
351-
// V_{2k} = V_k^2 - 2 * Q^k
352-
// V_{2k+1} = V_k V_{k+1} - Q^k
357+
// U_{k+1} = (P U_k + V_k) / 2
358+
// V_{k+1} = (D U_k + P V_k) / 2
353359
//
360+
// (The propagation method is due to [^Baillie2021], Eqs. 13, 14, 16, 17)
354361
// We can therefore start with k=0 and build up to k=d in log2(d) steps.
355362

363+
// Starting with k = 0
356364
let mut vk = two; // keeps V_k
357-
let mut vk1 = p; // keeps V_{k+1}
365+
let mut uk = DynResidue::<L>::zero(params); // keeps U_k
358366
let mut qk = one; // keeps Q^k
359-
let mut qk_times_p = if p_is_one { one } else { p }; // keeps P Q^{k}
367+
368+
// D in Montgomery representation - note that it can be negative.
369+
let abs_d = DynResidue::<L>::new(&Uint::<L>::from(discriminant.abs_diff(0)), params);
370+
let d_m = if discriminant < 0 { -abs_d } else { abs_d };
360371

361372
for i in (0..d.bits_vartime()).rev() {
362-
if d.bit_vartime(i) {
363-
// k' = 2k+1
373+
// k' = k * 2
364374

365-
// V_k' = V_{2k+1} = V_k V_{k+1} - P Q^k
366-
vk = vk * vk1 - qk_times_p;
375+
let u_2k = uk * vk;
376+
let v_2k = vk.square() - (qk + qk);
377+
let q_2k = qk.square();
367378

368-
// V_{k'+1} = V_{2k+2} = V_{k+1}^2 - 2 Q^{k+1}
369-
let qk1 = qk * q; // Q^{k+1}
370-
let two_qk1 = if q_is_one { two } else { qk1 + qk1 }; // 2 Q^{k+1}
371-
vk1 = vk1.square() - two_qk1;
372-
qk *= qk1;
373-
} else {
374-
// k' = 2k
379+
uk = u_2k;
380+
vk = v_2k;
381+
qk = q_2k;
375382

376-
// V_{k'+1} = V_{2k+1} = V_k V_{k+1} - P Q^k
377-
vk1 = vk * vk1 - qk_times_p;
383+
if d.bit_vartime(i) {
384+
// k' = k + 1
378385

379-
// V_k' = V_{2k} = V_k^2 - 2 Q^k
380-
let two_qk = if q_is_one { two } else { qk + qk }; // 2 Q^k
381-
vk = vk.square() - two_qk;
382-
qk = qk.square();
383-
}
386+
let (p_uk, p_vk) = if p_is_one { (uk, vk) } else { (p * uk, p * vk) };
384387

385-
if p_is_one {
386-
qk_times_p = qk;
387-
} else {
388-
qk_times_p = qk * p;
388+
let u_k1 = (p_uk + vk).div_by_2();
389+
let v_k1 = (d_m * uk + p_vk).div_by_2();
390+
let q_k1 = qk * q;
391+
392+
uk = u_k1;
393+
vk = v_k1;
394+
qk = q_k1;
389395
}
390396
}
391397

392-
// Now k=d, so vk = V_d, vk_1 = V_{d+1}.
398+
// Now k=d, so vk = V_d and uk = U_d.
393399

394-
// Extra strong check (from [^Mo1993]): `V_d == ±2 mod n`.
395-
// Do it first since it is cheap.
396-
//
397-
// Note that it only applies if Q = 1, since it is a consequence
398-
// of a property of Lucas series: V_k^2 - 4 Q^k = D U_k^2 mod n.
399-
// If Q = 1 we can easily decompose the left side of the equation leading to the check above.
400-
let vk_equals_two = if q_is_one {
401-
vk == two || vk == minus_two
402-
} else {
403-
true
404-
};
400+
// Check for the first sufficient condition in various strong checks.
405401

406-
if vk_equals_two {
407-
// Strong check:`U_d == 0 mod n`.
408-
// As suggested by [^Jacobsen], apply Eq. (3.13) from [^Crandall2005]:
402+
if check == LucasCheck::Strong && uk == zero {
403+
// Strong check: `U_d == 0 mod n`.
404+
return Primality::ProbablyPrime;
405+
} else if check == LucasCheck::ExtraStrong || check == LucasCheck::AlmostExtraStrong {
406+
// Extra strong check (from [^Mo1993]): `V_d == ±2 mod n` and `U_d == 0 mod n`.
409407
//
410-
// U_k = D^{-1} (2 V_{k+1} - P V_k)
408+
// Note that the first identity only applies if `Q = 1`, since it is a consequence
409+
// of a property of Lucas series: `V_k^2 - 4 Q^k = D U_k^2 mod n`.
410+
// If `Q = 1` we can easily decompose the left side of the equation
411+
// leading to the check above.
411412
//
412-
// Some implementations just test for 2 V_{k+1} == P V_{k},
413-
// but we don't have any reference pseudoprime lists for this, so we are not doing it.
414-
if check == LucasCheck::Strong || check == LucasCheck::ExtraStrong {
415-
let abs_d = DynResidue::<L>::new(&Uint::<L>::from(discriminant.abs_diff(0)), params);
416-
let d_m = if discriminant < 0 { -abs_d } else { abs_d };
417-
// `d` is guaranteed non-zero by construction, so we can safely unwrap
418-
let inv_d = <DynResidue<L> as Invert>::invert(&d_m).unwrap();
419-
420-
let vk_times_p = if p_is_one { vk } else { vk * p };
421-
let uk = inv_d * (vk1 + vk1 - vk_times_p);
422-
423-
if uk == zero {
424-
return Primality::ProbablyPrime;
425-
}
426-
} else {
427-
// This is "almost extra strong check": we only checked for `V_d` earlier.
428-
if check == LucasCheck::AlmostExtraStrong {
429-
return Primality::ProbablyPrime;
430-
}
413+
// If `Q != 1` we just consider it passed (we don't have a corresponding
414+
// pseudoprime list anyway).
415+
416+
let vk_equals_two = !q_is_one || (vk == two || vk == minus_two);
417+
418+
if check == LucasCheck::ExtraStrong && uk == zero && vk_equals_two {
419+
return Primality::ProbablyPrime;
420+
}
421+
422+
// "Almost extra strong" check skips the `U_d` check.
423+
// Since we have `U_d` anyway, it does not improve performance,
424+
// so it is only here for testing purposes, since we have a corresponding pseudoprime list.
425+
if check == LucasCheck::AlmostExtraStrong && vk_equals_two {
426+
return Primality::ProbablyPrime;
431427
}
432428
}
433429

430+
// Second sufficient condition requires further propagating `V_k` up to `V_{n+1}`.
431+
434432
// Check if V_{2^t d} == 0 mod n for some 0 <= t < s.
435433
// (unless we're in Lucas-V mode, then we just propagate V_k)
436434

@@ -446,7 +444,7 @@ pub fn lucas_test<const L: usize>(
446444
}
447445

448446
// k' = 2k
449-
// V(k') = V(2k) = V(k)² - 2 * Q^k
447+
// V_{k'} = V_k^2 - 2 Q^k
450448
vk = vk * vk - qk - qk;
451449

452450
if check != LucasCheck::LucasV && vk == zero {

0 commit comments

Comments
 (0)