Skip to content

Commit a564a5c

Browse files
committed
Alternative Lucas sequence propagation
1 parent ff4d9ac commit a564a5c

File tree

3 files changed

+57
-60
lines changed

3 files changed

+57
-60
lines 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: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
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
};
@@ -41,14 +42,27 @@ fn bench_miller_rabin<'a, M: Measurement>(group: &mut BenchmarkGroup<'a, M>) {
4142
}
4243

4344
fn bench_lucas<'a, M: Measurement>(group: &mut BenchmarkGroup<'a, M>) {
44-
let start: U1024 = random_odd_uint(&mut OsRng, 1024);
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| {

src/hazmat/lucas.rs

Lines changed: 35 additions & 53 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::{
@@ -335,57 +335,52 @@ pub fn lucas_test<const L: usize>(
335335
DynResidue::<L>::new(&Uint::<L>::from(p), params)
336336
};
337337

338-
// Compute d-th element of Lucas sequence V_d(P, Q), where:
338+
// Compute d-th element of Lucas sequence (U_d(P, Q), V_d(P, Q)), where:
339339
//
340-
// V_0 = 2
341-
// V_1 = P
342-
// V_k = P V_{k-1} - Q V_{k-2}.
340+
// V_0 = 2
341+
// U_0 = 1
343342
//
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,
343+
// U_{2k} = U_k V_k
344+
// V_{2k} = V_k^2 - 2 Q^k
346345
//
347-
// V_{j+k} = V_j V_k - Q^j * V_{k-j}.
348-
//
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
346+
// U_{k+1} = (P U_k + V_k) / 2
347+
// V_{k+1} = (D U_k + P V_k) / 2
353348
//
349+
// (The propagation method is due to [^Baillie2021], Eqs. 13, 14, 16, 17)
354350
// We can therefore start with k=0 and build up to k=d in log2(d) steps.
355351

352+
// Starting with k = 0
356353
let mut vk = two; // keeps V_k
357-
let mut vk1 = p; // keeps V_{k+1}
354+
let mut uk = DynResidue::<L>::zero(params); // keeps U_k
358355
let mut qk = one; // keeps Q^k
359-
let mut qk_times_p = if p_is_one { one } else { p }; // keeps P Q^{k}
356+
357+
// D in Montgomery representation - note that it can be negative.
358+
let abs_d = DynResidue::<L>::new(&Uint::<L>::from(discriminant.abs_diff(0)), params);
359+
let d_m = if discriminant < 0 { -abs_d } else { abs_d };
360360

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

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

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
368+
uk = u_2k;
369+
vk = v_2k;
370+
qk = q_2k;
371+
372+
if d.bit_vartime(i) {
373+
// k = k + 1
375374

376-
// V_{k'+1} = V_{2k+1} = V_k V_{k+1} - P Q^k
377-
vk1 = vk * vk1 - qk_times_p;
375+
let (p_uk, p_vk) = if p_is_one { (uk, vk) } else { (p * uk, p * vk) };
378376

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-
}
377+
let u_k1 = (p_uk + vk).div_by_2();
378+
let v_k1 = (d_m * uk + p_vk).div_by_2();
379+
let q_k1 = qk * q;
384380

385-
if p_is_one {
386-
qk_times_p = qk;
387-
} else {
388-
qk_times_p = qk * p;
381+
uk = u_k1;
382+
vk = v_k1;
383+
qk = q_k1;
389384
}
390385
}
391386

@@ -404,27 +399,14 @@ pub fn lucas_test<const L: usize>(
404399
};
405400

406401
if vk_equals_two {
407-
// Strong check:`U_d == 0 mod n`.
408-
// As suggested by [^Jacobsen], apply Eq. (3.13) from [^Crandall2005]:
409-
//
410-
// U_k = D^{-1} (2 V_{k+1} - P V_k)
411-
//
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.
414402
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-
403+
// Strong check:`U_d == 0 mod n`.
423404
if uk == zero {
424405
return Primality::ProbablyPrime;
425406
}
426407
} else {
427-
// This is "almost extra strong check": we only checked for `V_d` earlier.
408+
// This is "almost extra strong check": we only checked for `V_d` earlier,
409+
// and the check for `U_d` is skipped.
428410
if check == LucasCheck::AlmostExtraStrong {
429411
return Primality::ProbablyPrime;
430412
}

0 commit comments

Comments
 (0)