Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Rework module and feature flag for better v4/v4.2 split #13

Merged
merged 9 commits into from
Apr 18, 2024
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,10 @@ jobs:
- name: Run cargo check
run: cargo check --all --all-features --all-targets
if: startsWith(matrix.rust, 'nightly')
- name: Run cargo check (without dev-dependencies to catch missing feature flags)
if: startsWith(matrix.rust, 'nightly')
run: cargo check -Z features=dev_dep
- name: Full feature + v4.2 testing
- name: Full feature testing
run: cargo test --all-features
- name: v4 compatibility testing
run: cargo test --no-default-features --features debug,rand_core,wyhash,randomised_wyhash
- name: No default compatibility testing
run: cargo test --no-default-features --features debug,rand_core,wyhash

msrv:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ wyhash = []
randomised_wyhash = ["wyhash", "dep:getrandom"]
fully_randomised_wyhash = ["randomised_wyhash"]
threadrng_wyhash = ["dep:rand", "randomised_wyhash"]
v4_2 = []
legacy_v4 = []

[dependencies]
getrandom = { version = "0.2", optional = true }
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A fast & portable non-cryptographic pseudorandom number generator written in Rus

The implementations for both the PRNG and hasher are based on the C reference implementation [wyhash](https://github.com/wangyi-fudan/wyhash), a simple and fast hasher but **not** cryptographically secure. It's known to be extremely fast and performant while still having great statistical properties.

This crate provides both the v4.2 final implementation of the WyRand/WyHash algorithm, or the older final v4 implementation. The two versions have different outputs due to changes in the algorithm and also with the constants used. Currently by default, the older final v4 algorithm will be used. In the future, this will be changed to the newer algorithm to be the default, but the old implementation will remain for backwards compatibility reasons.
This crate provides both the v4.2 final implementation of the WyRand/WyHash algorithm, or the older final v4 implementation. The two versions have different outputs due to changes in the algorithm and also with the constants used. By default, the final v4.2 algorithm will be used. If one needs to use the older, legacy v4 implementation for compatibility/stability reasons, the legacy hasher and PRNG can be exposed by enabling the `legacy_v4` feature flag.

This crate can be used on its own or be integrated with `rand_core`/`rand`, and it is `no-std` compatible. Minimum compatible Rust version is 1.70. This crate is also implemented with no unsafe code via `#![forbid(unsafe_code)]`.

Expand Down Expand Up @@ -38,7 +38,7 @@ The crate will always export `WyRand` and will do so when set as `default-featur
- **`randomised_wyhash`** - Enables `RandomWyHashState`, a means to source a randomised state for `WyHash` for use in collections like `HashMap`/`HashSet`. Enables `wyhash` feature if it is not already enabled.
- **`fully_randomised_wyhash`** - Randomises not just the seed for `RandomWyHashState`, but also the secret. The new secret is generated once per runtime, and then is used for every subsequent new `WyHash` (with each `WyHash` instance having its own unique seed). Enables `randomised_wyhash` if not already enabled, and requires `std` environments.
- **`threadrng_wyhash`** - Enables sourcing entropy from `rand`'s `thread_rng()` method. Much quicker than `getrandom`. Enables `randomised_wyhash` if not already enabled. Requires `std` environments.
- **`v4_2`** - Switches the PRNG/Hashing algorithms to use the final v4.2 implementation.
- **`legacy_v4`** - Exposes the legacy PRNG/Hashing algorithms that use the final v4 implementation.

## Building for WASM/Web

Expand Down
37 changes: 36 additions & 1 deletion benches/rand_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn wyrand_benchmark(c: &mut Criterion) {

#[cfg(feature = "wyhash")]
fn wyhash_benchmark(c: &mut Criterion) {
use std::hash::Hasher;
use std::hash::{Hash, Hasher};

use criterion::BenchmarkId;
use wyrand::WyHash;
Expand Down Expand Up @@ -67,6 +67,41 @@ fn wyhash_benchmark(c: &mut Criterion) {
);
});

c.bench_function("Hash integer single", |b| {
b.iter(|| {
let mut hasher = WyHash::new_with_default_secret(black_box(42));

hasher.write_u64(black_box(256));

hasher.finish()
});
});

c.bench_function("Hash integer multiple", |b| {
let big_value = u64::MAX as u128 + 125;

b.iter(|| {
let mut hasher = WyHash::new_with_default_secret(black_box(42));

hasher.write_u64(black_box(256));
hasher.write_u128(black_box(big_value));

hasher.finish()
});
});

c.bench_function("Hash integer tuple", |b| {
let big_value = u64::MAX as u128 + 125;

b.iter(|| {
let mut hasher = WyHash::new_with_default_secret(black_box(42));

(black_box(42), black_box(big_value)).hash(&mut hasher);

hasher.finish()
});
});

c.bench_function("Hash new with default secret", |b| {
b.iter(|| WyHash::new_with_default_secret(black_box(42)));
});
Expand Down
31 changes: 6 additions & 25 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,6 @@
#[cfg(not(feature = "v4_2"))]
mod v4 {
pub(crate) const WY0: u64 = 0xa076_1d64_78bd_642f;
pub(crate) const WY1: u64 = 0xe703_7ed1_a0b4_28db;
#[cfg(feature = "wyhash")]
pub(crate) const WY2: u64 = 0x8ebc_6af0_9c88_c6e3;
#[cfg(feature = "wyhash")]
pub(crate) const WY3: u64 = 0x5899_65cc_7537_4cc3;
}

#[cfg(feature = "v4_2")]
mod v4_2 {
pub(crate) const WY0: u64 = 0x2d35_8dcc_aa6c_78a5;
pub(crate) const WY1: u64 = 0x8bb8_4b93_962e_acc9;
#[cfg(feature = "wyhash")]
pub(crate) const WY2: u64 = 0x4b33_a62e_d433_d4a3;
#[cfg(feature = "wyhash")]
pub(crate) const WY3: u64 = 0x4d5a_2da5_1de1_aa47;
}

#[cfg(not(feature = "v4_2"))]
pub(crate) use v4::*;

#[cfg(feature = "v4_2")]
pub(crate) use v4_2::*;
pub(crate) const C_VALUES: &[u8] = &[
15, 23, 27, 29, 30, 39, 43, 45, 46, 51, 53, 54, 57, 58, 60, 71, 75, 77, 78, 83, 85, 86, 89, 90,
92, 99, 101, 102, 105, 106, 108, 113, 114, 116, 120, 135, 139, 141, 142, 147, 149, 150, 153,
154, 156, 163, 165, 166, 169, 170, 172, 177, 178, 180, 184, 195, 197, 198, 201, 202, 204, 209,
210, 212, 216, 225, 226, 228, 232, 240,
];
24 changes: 24 additions & 0 deletions src/final_v4_2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#[cfg(feature = "randomised_wyhash")]
mod builder;
mod constants;
#[cfg(feature = "wyhash")]
mod hasher;
#[cfg(feature = "wyhash")]
mod primes;
#[cfg(feature = "wyhash")]
mod secret;
mod wyrand;

#[cfg(feature = "randomised_wyhash")]
#[cfg_attr(docsrs, doc(cfg(feature = "randomised_wyhash")))]
pub use builder::RandomWyHashState;

#[cfg(feature = "wyhash")]
#[cfg_attr(docsrs, doc(cfg(feature = "wyhash")))]
pub use hasher::WyHash;

#[cfg(feature = "wyhash")]
#[cfg_attr(docsrs, doc(cfg(feature = "wyhash")))]
pub use secret::Secret;

pub use wyrand::WyRand;
40 changes: 19 additions & 21 deletions src/hasher/builder.rs → src/final_v4_2/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ use core::fmt::Debug;
#[cfg(feature = "fully_randomised_wyhash")]
use std::sync::OnceLock;

use crate::WyHash;
use super::{secret::Secret, WyHash};

#[cfg(feature = "fully_randomised_wyhash")]
static SECRET: OnceLock<[u64; 4]> = OnceLock::new();
static SECRET: OnceLock<Secret> = OnceLock::new();

#[inline]
fn get_random_u64() -> u64 {
Expand Down Expand Up @@ -37,21 +37,20 @@ fn get_random_u64() -> u64 {
}
}

#[cfg_attr(docsrs, doc(cfg(feature = "randomised_wyhash")))]
#[derive(Clone, Copy)]
#[derive(Clone)]
/// Randomised state constructor for [`WyHash`]. This builder will source entropy in order
/// to provide random seeds for [`WyHash`]. If the `fully_randomised_wyhash` feature is enabled,
/// this will yield a hasher with not just a random seed, but also a new random secret,
/// granting extra protection against DOS and prediction attacks.
pub struct RandomWyHashState {
state: u64,
secret: [u64; 4],
secret: Secret,
}

impl RandomWyHashState {
/// Create a new [`RandomWyHashState`] instance. Calling this method will attempt to
/// draw entropy from hardware/OS sources. If `fully_randomised_wyhash` feature is enabled,
/// then it will use a randomised `secret` as well, otherwise it uses the default wyhash constants.
/// then it will use a randomised `secret` as well, otherwise it uses the default wyhash constants.
///
/// # Panics
///
Expand All @@ -70,23 +69,22 @@ impl RandomWyHashState {
#[must_use]
#[inline]
pub fn new() -> Self {
#[cfg(feature = "fully_randomised_wyhash")]
use crate::hasher::secret::make_secret;

#[cfg(not(feature = "fully_randomised_wyhash"))]
use crate::constants::{WY0, WY1, WY2, WY3};
use super::constants::{WY0, WY1, WY2, WY3};
#[cfg(feature = "fully_randomised_wyhash")]
use super::secret::make_secret;

#[cfg(feature = "fully_randomised_wyhash")]
let secret = *SECRET.get_or_init(|| make_secret(get_random_u64()));
let secret = SECRET.get_or_init(|| make_secret(get_random_u64())).clone();
#[cfg(not(feature = "fully_randomised_wyhash"))]
let secret = [WY0, WY1, WY2, WY3];
let secret = Secret::new(WY0, WY1, WY2, WY3);

Self::new_with_secret(secret)
}

/// Create a new [`RandomWyHashState`] instance with a provided secret. Calling this method
/// will attempt to draw entropy from hardware/OS sources, and assumes the user provided the
/// secret via [`super::secret::make_secret`].
/// secret via [`WyHash::make_secret`].
///
/// # Panics
///
Expand All @@ -95,16 +93,16 @@ impl RandomWyHashState {
/// # Examples
///
/// ```
/// use wyrand::{RandomWyHashState, make_secret};
/// use core::hash::BuildHasher;
/// use wyrand::{RandomWyHashState, WyHash};
///
/// let s = RandomWyHashState::new_with_secret(make_secret(42));
/// let s = RandomWyHashState::new_with_secret(WyHash::make_secret(42));
///
/// let mut hasher = s.build_hasher(); // Creates a WyHash instance with random state
/// ```
#[must_use]
#[inline]
pub fn new_with_secret(secret: [u64; 4]) -> Self {
pub fn new_with_secret(secret: Secret) -> Self {
Self {
state: get_random_u64(),
secret,
Expand All @@ -117,7 +115,7 @@ impl BuildHasher for RandomWyHashState {

#[inline]
fn build_hasher(&self) -> Self::Hasher {
WyHash::new_with_secret(self.state, self.secret)
WyHash::new_with_secret(self.state, self.secret.clone())
}
}

Expand All @@ -131,7 +129,7 @@ impl Default for RandomWyHashState {
#[cfg(feature = "debug")]
impl Debug for RandomWyHashState {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("RandomisedWyHashBuilder")
f.debug_struct("RandomisedWyHashState")
.finish_non_exhaustive()
}
}
Expand All @@ -151,7 +149,7 @@ mod tests {

assert_eq!(
format!("{builder:?}"),
"RandomisedWyHashBuilder { .. }",
"RandomisedWyHashState { .. }",
"Debug should not be leaking internal state"
);
}
Expand All @@ -171,9 +169,9 @@ mod tests {
// same as the default secret.
#[cfg(feature = "fully_randomised_wyhash")]
{
use crate::constants::{WY0, WY1, WY2, WY3};
use super::super::constants::{WY0, WY1, WY2, WY3};

let default_secret = [WY0, WY1, WY2, WY3];
let default_secret = Secret::new(WY0, WY1, WY2, WY3);

assert_ne!(&builder1.secret, &default_secret);
assert_ne!(&builder2.secret, &default_secret);
Expand Down
6 changes: 6 additions & 0 deletions src/final_v4_2/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub(super) const WY0: u64 = 0x2d35_8dcc_aa6c_78a5;
pub(super) const WY1: u64 = 0x8bb8_4b93_962e_acc9;
#[cfg(feature = "wyhash")]
pub(super) const WY2: u64 = 0x4b33_a62e_d433_d4a3;
#[cfg(feature = "wyhash")]
pub(super) const WY3: u64 = 0x4d5a_2da5_1de1_aa47;
Loading