Skip to content

Commit 3918fc4

Browse files
authored
Parallel prime generation (#60)
1 parent 056781d commit 3918fc4

File tree

6 files changed

+260
-1
lines changed

6 files changed

+260
-1
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ 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+
- Parallel prime finding methods and a "multicore" feature ([#60])
11+
12+
[#60]: https://github.com/entropyxyz/crypto-primes/pull/60
13+
714
## [0.6.0-pre.2] - 2024-10-18
815

916
### Changed

Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ rust-version = "1.81"
1212
[dependencies]
1313
crypto-bigint = { version = "0.6.0-rc.6", default-features = false, features = ["rand_core"] }
1414
rand_core = { version = "0.6.4", default-features = false }
15+
16+
# Optional dependencies used in tests and benchmarks
1517
openssl = { version = "0.10.39", optional = true, features = ["vendored"] }
1618
rug = { version = "1.26", default-features = false, features = ["integer"], optional = true }
1719
glass_pumpkin = { version = "1", optional = true }
20+
rayon = { version = "1", optional = true }
1821

1922
[dev-dependencies]
2023
# need `crypto-bigint` with `alloc` to test `BoxedUint`
@@ -26,6 +29,7 @@ num-bigint = "0.4"
2629
num-integer = "0.1"
2730
proptest = "1"
2831
num-prime = "0.4.3"
32+
num_cpus = "1.16"
2933

3034
[features]
3135
default = ["default-rng"]
@@ -35,6 +39,7 @@ tests-gmp = ["rug/std"]
3539
tests-glass-pumpkin = ["glass_pumpkin"]
3640
tests-exhaustive = []
3741
tests-all = ["tests-openssl", "tests-gmp", "tests-exhaustive", "tests-glass-pumpkin"]
42+
multicore = ["rayon"]
3843

3944
[package.metadata.docs.rs]
4045
features = ["default"]
@@ -44,3 +49,7 @@ rustdoc-args = ["--cfg", "docsrs"]
4449
[[bench]]
4550
name = "bench"
4651
harness = false
52+
53+
[profile.bench]
54+
lto = true
55+
codegen-units = 1

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,39 @@ In particular:
1414
- Miller-Rabin test;
1515
- Strong and extra strong Lucas tests, and Lucas-V test.
1616

17-
See the documentation for the specific tests for more information and references.
17+
18+
The library is no-std compatible and contains no unsafe code.
19+
20+
Most users will be using the small set of functions exported from the top level, providing "pre-packaged" prime finding functionality with sane defaults.
21+
22+
## Example
23+
24+
Find a 196 bit prime returned in a 256-bit long `crypto_bigint::U256`:
25+
26+
```rust
27+
use crypto_bigint::U256;
28+
let prime = crypto_primes::generate_prime::<U256>(196);
29+
assert!(crypto_primes::is_prime(&prime));
30+
```
31+
32+
Find a 64 bit safe prime returned in a `crypto_bigint::U1024`:
33+
34+
```rust
35+
use crypto_bigint::U1024;
36+
let prime = crypto_primes::generate_safe_prime::<U1024>(64);
37+
assert!(crypto_primes::is_safe_prime(&prime));
38+
```
39+
40+
## Advanced
41+
42+
Advanced users can use the [`hazmat`][hazmat-lnk] module in the library to build a custom prime finding solution that best fit their needs, e.g. by picking different Lucas bases or running Miller-Rabin tests with particular bases.
43+
44+
## Features
45+
46+
The following features are available:
47+
48+
- `default-rng`: Use the OS default CSPRNG, `OsRng`. Enabled by default.
49+
- `multicore`: Enables additional parallel prime finding functions. Disabled by default.
1850

1951

2052
[crate-image]: https://img.shields.io/crates/v/crypto-primes.svg
@@ -26,3 +58,4 @@ See the documentation for the specific tests for more information and references
2658
[build-link]: https://github.com/entropyxyz/crypto-primes/actions?query=workflow%3Acrypto-primes
2759
[coverage-image]: https://codecov.io/gh/entropyxyz/crypto-primes/branch/master/graph/badge.svg
2860
[coverage-link]: https://codecov.io/gh/entropyxyz/crypto-primes
61+
[hazmat-lnk]: https://docs.rs/crypto-primes/latest/crypto_primes/hazmat

benches/bench.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ use crypto_primes::{
1818
},
1919
is_prime_with_rng, is_safe_prime_with_rng,
2020
};
21+
#[cfg(feature = "multicore")]
22+
use crypto_primes::{par_generate_prime_with_rng, par_generate_safe_prime_with_rng};
2123

2224
fn make_rng() -> ChaCha8Rng {
2325
ChaCha8Rng::from_seed(*b"01234567890123456789012345678901")
@@ -277,6 +279,44 @@ fn bench_presets(c: &mut Criterion) {
277279
group.finish();
278280
}
279281

282+
#[cfg(feature = "multicore")]
283+
fn bench_multicore_presets(c: &mut Criterion) {
284+
let mut group = c.benchmark_group("Presets (multicore)");
285+
let mut rng = make_rng();
286+
group.bench_function("(U128) Random prime", |b| {
287+
b.iter(|| par_generate_prime_with_rng::<U128>(&mut rng, 128, num_cpus::get()))
288+
});
289+
290+
let mut rng = make_rng();
291+
group.bench_function("(U1024) Random prime", |b| {
292+
b.iter(|| par_generate_prime_with_rng::<U1024>(&mut rng, 1024, num_cpus::get()))
293+
});
294+
295+
let mut rng = make_rng();
296+
group.bench_function("(U128) Random safe prime", |b| {
297+
b.iter(|| par_generate_safe_prime_with_rng::<U128>(&mut rng, 128, num_cpus::get()))
298+
});
299+
300+
group.sample_size(20);
301+
let mut rng = make_rng();
302+
group.bench_function("(U1024) Random safe prime", |b| {
303+
b.iter(|| par_generate_safe_prime_with_rng::<U1024>(&mut rng, 1024, num_cpus::get()))
304+
});
305+
306+
let mut rng = make_rng();
307+
group.bench_function("(Boxed128) Random safe prime", |b| {
308+
b.iter(|| par_generate_safe_prime_with_rng::<BoxedUint>(&mut rng, 128, num_cpus::get()))
309+
});
310+
311+
group.sample_size(20);
312+
let mut rng = make_rng();
313+
group.bench_function("(Boxed1024) Random safe prime", |b| {
314+
b.iter(|| par_generate_safe_prime_with_rng::<BoxedUint>(&mut rng, 1024, num_cpus::get()))
315+
});
316+
}
317+
#[cfg(not(feature = "multicore"))]
318+
fn bench_multicore_presets(_c: &mut Criterion) {}
319+
280320
#[cfg(feature = "tests-gmp")]
281321
fn bench_gmp(c: &mut Criterion) {
282322
let mut group = c.benchmark_group("GMP");
@@ -487,6 +527,7 @@ criterion_group!(
487527
bench_miller_rabin,
488528
bench_lucas,
489529
bench_presets,
530+
bench_multicore_presets,
490531
bench_gmp,
491532
bench_openssl,
492533
bench_glass_pumpkin,

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ pub use traits::RandomPrimeWithRng;
2525

2626
#[cfg(feature = "default-rng")]
2727
pub use presets::{generate_prime, generate_safe_prime, is_prime, is_safe_prime};
28+
#[cfg(all(feature = "default-rng", feature = "multicore"))]
29+
pub use presets::{par_generate_prime, par_generate_safe_prime};
30+
#[cfg(feature = "multicore")]
31+
pub use presets::{par_generate_prime_with_rng, par_generate_safe_prime_with_rng};

src/presets.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ use rand_core::CryptoRngCore;
66
#[cfg(feature = "default-rng")]
77
use rand_core::OsRng;
88

9+
#[cfg(feature = "multicore")]
10+
use rayon::iter::{ParallelBridge, ParallelIterator};
11+
912
use crate::hazmat::{lucas_test, random_odd_integer, AStarBase, LucasCheck, MillerRabin, Primality, Sieve};
1013

1114
/// Returns a random prime of size `bit_length` using [`OsRng`] as the RNG.
@@ -16,6 +19,20 @@ pub fn generate_prime<T: Integer + RandomBits + RandomMod>(bit_length: u32) -> T
1619
generate_prime_with_rng(&mut OsRng, bit_length)
1720
}
1821

22+
/// Returns a random prime of size `bit_length` using [`OsRng`] as the RNG.
23+
///
24+
/// See [`is_prime_with_rng`] for details about the performed checks.
25+
///
26+
/// Uses `threadcount` cores to parallelize the prime search.
27+
///
28+
/// Panics if `bit_length` is less than 2, or greater than the bit size of the target `Uint`.
29+
///
30+
/// Panics if the platform is unable to spawn threads.
31+
#[cfg(all(feature = "default-rng", feature = "multicore"))]
32+
pub fn par_generate_prime<T: Integer + RandomBits + RandomMod>(bit_length: u32, threadcount: usize) -> T {
33+
par_generate_prime_with_rng(&mut OsRng, bit_length, threadcount)
34+
}
35+
1936
/// Returns a random safe prime (that is, such that `(n - 1) / 2` is also prime) of size
2037
/// `bit_length` using [`OsRng`] as the RNG.
2138
///
@@ -25,6 +42,21 @@ pub fn generate_safe_prime<T: Integer + RandomBits + RandomMod>(bit_length: u32)
2542
generate_safe_prime_with_rng(&mut OsRng, bit_length)
2643
}
2744

45+
/// Returns a random safe prime (that is, such that `(n - 1) / 2` is also prime) of size
46+
/// `bit_length` using [`OsRng`] as the RNG.
47+
///
48+
/// See [`is_prime_with_rng`] for details about the performed checks.
49+
///
50+
/// Uses `threadcount` cores to parallelize the prime search.
51+
///
52+
/// Panics if `bit_length` is less than 3, or greater than the bit size of the target `Uint`.
53+
///
54+
/// Panics if the platform is unable to spawn threads.
55+
#[cfg(all(feature = "default-rng", feature = "multicore"))]
56+
pub fn par_generate_safe_prime<T: Integer + RandomBits + RandomMod>(bit_length: u32, threadcount: usize) -> T {
57+
par_generate_safe_prime_with_rng(&mut OsRng, bit_length, threadcount)
58+
}
59+
2860
/// Probabilistically checks if the given number is prime using [`OsRng`] as the RNG.
2961
///
3062
/// See [`is_prime_with_rng`] for details about the performed checks.
@@ -90,6 +122,94 @@ pub fn generate_safe_prime_with_rng<T: Integer + RandomBits + RandomMod>(
90122
}
91123
}
92124

125+
/// Returns a random prime of size `bit_length` using the provided RNG.
126+
///
127+
/// Uses `threadcount` cores to parallelize the prime search.
128+
///
129+
/// Panics if `bit_length` is less than 2, or greater than the bit size of the target `Uint`.
130+
///
131+
/// Panics if the platform is unable to spawn threads.
132+
#[cfg(feature = "multicore")]
133+
pub fn par_generate_prime_with_rng<T>(
134+
rng: &mut (impl CryptoRngCore + Send + Sync + Clone),
135+
bit_length: u32,
136+
threadcount: usize,
137+
) -> T
138+
where
139+
T: Integer + RandomBits + RandomMod,
140+
{
141+
if bit_length < 2 {
142+
panic!("`bit_length` must be 2 or greater.");
143+
}
144+
let bit_length = NonZero::new(bit_length).expect("`bit_length` should be non-zero");
145+
146+
let threadpool = rayon::ThreadPoolBuilder::new()
147+
.num_threads(threadcount)
148+
.build()
149+
.expect("If the platform can spawn threads, then this call will work.");
150+
let start = random_odd_integer::<T>(rng, bit_length).get();
151+
let sieve = Sieve::new(start, bit_length, false);
152+
153+
let prime = threadpool.install(|| {
154+
sieve.par_bridge().find_any(|c| {
155+
let mut rng = rng.clone();
156+
is_prime_with_rng(&mut rng, c)
157+
})
158+
});
159+
match prime {
160+
Some(prime) => prime,
161+
None => {
162+
drop(threadpool);
163+
par_generate_prime_with_rng(rng, bit_length.get(), threadcount)
164+
}
165+
}
166+
}
167+
168+
/// Returns a random safe prime (that is, such that `(n - 1) / 2` is also prime)
169+
/// of size `bit_length` using the provided RNG.
170+
///
171+
/// Uses `threadcount` cores to parallelize the prime search.
172+
///
173+
/// Panics if `bit_length` is less than 3, or greater than the bit size of the target `Uint`.
174+
/// Panics if the platform is unable to spawn threads.
175+
///
176+
/// See [`is_prime_with_rng`] for details about the performed checks.
177+
#[cfg(feature = "multicore")]
178+
pub fn par_generate_safe_prime_with_rng<T>(
179+
rng: &mut (impl CryptoRngCore + Send + Sync + Clone),
180+
bit_length: u32,
181+
threadcount: usize,
182+
) -> T
183+
where
184+
T: Integer + RandomBits + RandomMod,
185+
{
186+
if bit_length < 2 {
187+
panic!("`bit_length` must be 2 or greater.");
188+
}
189+
let bit_length = NonZero::new(bit_length).expect("`bit_length` should be non-zero");
190+
191+
let threadpool = rayon::ThreadPoolBuilder::new()
192+
.num_threads(threadcount)
193+
.build()
194+
.expect("If the platform can spawn threads, then this call will work.");
195+
let start = random_odd_integer::<T>(rng, bit_length).get();
196+
let sieve = Sieve::new(start, bit_length, true);
197+
198+
let prime = threadpool.install(|| {
199+
sieve.par_bridge().find_any(|c| {
200+
let mut rng = rng.clone();
201+
is_safe_prime_with_rng(&mut rng, c)
202+
})
203+
});
204+
match prime {
205+
Some(prime) => prime,
206+
None => {
207+
drop(threadpool);
208+
par_generate_safe_prime_with_rng(rng, bit_length.get(), threadcount)
209+
}
210+
}
211+
}
212+
93213
/// Probabilistically checks if the given number is prime using the provided RNG.
94214
///
95215
/// Performed checks:
@@ -367,6 +487,51 @@ mod tests {
367487
}
368488
}
369489

490+
#[cfg(all(test, feature = "multicore"))]
491+
mod multicore_tests {
492+
use super::{is_prime, par_generate_prime_with_rng};
493+
use crypto_bigint::{nlimbs, BoxedUint, U128};
494+
495+
use super::*;
496+
#[test]
497+
fn parallel_prime_generation() {
498+
for bit_length in (28..=128).step_by(10) {
499+
let p: U128 = par_generate_prime_with_rng(&mut OsRng, bit_length, 4);
500+
assert!(p.bits_vartime() == bit_length);
501+
assert!(is_prime(&p));
502+
}
503+
}
504+
505+
#[test]
506+
fn parallel_prime_generation_boxed() {
507+
for bit_length in (28..=128).step_by(10) {
508+
let p: BoxedUint = par_generate_prime_with_rng(&mut OsRng, bit_length, 2);
509+
assert!(p.bits_vartime() == bit_length);
510+
assert!(p.to_words().len() == nlimbs!(bit_length));
511+
assert!(is_prime(&p));
512+
}
513+
}
514+
515+
#[test]
516+
fn parallel_safe_prime_generation() {
517+
for bit_length in (28..=128).step_by(10) {
518+
let p: U128 = par_generate_safe_prime_with_rng(&mut OsRng, bit_length, 8);
519+
assert!(p.bits_vartime() == bit_length);
520+
assert!(is_prime(&p));
521+
}
522+
}
523+
524+
#[test]
525+
fn parallel_safe_prime_generation_boxed() {
526+
for bit_length in (28..=128).step_by(10) {
527+
let p: BoxedUint = par_generate_safe_prime_with_rng(&mut OsRng, bit_length, 4);
528+
assert!(p.bits_vartime() == bit_length);
529+
assert!(p.to_words().len() == nlimbs!(bit_length));
530+
assert!(is_prime(&p));
531+
}
532+
}
533+
}
534+
370535
#[cfg(test)]
371536
#[cfg(feature = "tests-openssl")]
372537
mod tests_openssl {

0 commit comments

Comments
 (0)