diff --git a/core/src/hash.rs b/core/src/hash.rs new file mode 100644 index 0000000..7442e87 --- /dev/null +++ b/core/src/hash.rs @@ -0,0 +1,17 @@ +//! FNV-1a hashing for state checksums. +//! +//! The hash only has to reveal accidental divergence between peers. It is +//! not meant to resist tampering. + +const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325; +const FNV_PRIME: u64 = 0x0000_0100_0000_01b3; + +/// Returns the 64-bit FNV-1a hash of `bytes`. +pub fn fnv1a64(bytes: &[u8]) -> u64 { + let mut hash = FNV_OFFSET_BASIS; + for &byte in bytes { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(FNV_PRIME); + } + hash +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 127e8a1..b640c90 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -7,3 +7,5 @@ //! world and exchange nothing but commands. pub mod codec; +pub mod hash; +pub mod rng; diff --git a/core/src/rng.rs b/core/src/rng.rs new file mode 100644 index 0000000..53e269f --- /dev/null +++ b/core/src/rng.rs @@ -0,0 +1,34 @@ +//! Repeatable pseudo random numbers (SplitMix64). +//! +//! The core never uses any other source of randomness. The same seed always +//! gives the same sequence, on every machine. + +/// Pseudo random number generator with 64 bits of state. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Rng { + state: u64, +} + +impl Rng { + pub fn new(seed: u64) -> Self { + Self { state: seed } + } + + /// Returns the next value of the sequence. + pub fn next_u64(&mut self) -> u64 { + self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.state; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + + /// Returns a value in `0..bound`. + /// + /// # Panics + /// Panics if `bound` is zero. + pub fn below(&mut self, bound: u32) -> u32 { + assert!(bound > 0, "bound must be greater than zero"); + ((u128::from(self.next_u64()) * u128::from(bound)) >> 64) as u32 + } +} diff --git a/core/tests/hash.rs b/core/tests/hash.rs new file mode 100644 index 0000000..96784db --- /dev/null +++ b/core/tests/hash.rs @@ -0,0 +1,13 @@ +use grovewright_core::hash::fnv1a64; + +#[test] +fn matches_the_published_fnv1a_test_vectors() { + assert_eq!(fnv1a64(b""), 0xcbf2_9ce4_8422_2325); + assert_eq!(fnv1a64(b"a"), 0xaf63_dc4c_8601_ec8c); + assert_eq!(fnv1a64(b"foobar"), 0x8594_4171_f739_67e8); +} + +#[test] +fn the_order_of_bytes_matters() { + assert_ne!(fnv1a64(&[0, 1]), fnv1a64(&[1, 0])); +} diff --git a/core/tests/rng.rs b/core/tests/rng.rs new file mode 100644 index 0000000..4060e80 --- /dev/null +++ b/core/tests/rng.rs @@ -0,0 +1,49 @@ +use grovewright_core::rng::Rng; + +#[test] +fn matches_the_splitmix64_reference_values() { + let mut rng = Rng::new(0); + assert_eq!(rng.next_u64(), 0xE220_A839_7B1D_CDAF); + assert_eq!(rng.next_u64(), 0x6E78_9E6A_A1B9_65F4); + assert_eq!(rng.next_u64(), 0x06C4_5D18_8009_454F); +} + +#[test] +fn the_same_seed_gives_the_same_sequence() { + let mut a = Rng::new(2026); + let mut b = Rng::new(2026); + for _ in 0..1_000 { + assert_eq!(a.next_u64(), b.next_u64()); + } +} + +#[test] +fn different_seeds_give_different_sequences() { + assert_ne!(Rng::new(1).next_u64(), Rng::new(2).next_u64()); +} + +#[test] +fn below_stays_inside_the_bound() { + let mut rng = Rng::new(7); + for bound in [1u32, 2, 3, 10, 100, 1_000_000, u32::MAX] { + for _ in 0..1_000 { + assert!(rng.below(bound) < bound); + } + } +} + +#[test] +fn below_reaches_every_value_of_a_small_bound() { + let mut rng = Rng::new(99); + let mut seen = [false; 6]; + for _ in 0..1_000 { + seen[rng.below(6) as usize] = true; + } + assert_eq!(seen, [true; 6]); +} + +#[test] +#[should_panic(expected = "bound must be greater than zero")] +fn below_refuses_a_bound_of_zero() { + Rng::new(1).below(0); +}