use rand_chacha::{
ChaCha20Rng,
rand_core::{Rng, SeedableRng},
};
use std::cell::RefCell;
use thiserror::Error as ThisError;
thread_local! {
static RNG: RefCell<Option<ChaCha20Rng>> = const { RefCell::new(None) };
}
#[derive(Debug, ThisError)]
pub enum RngError {
#[error("Randomness is not initialized. Please try again later")]
NotInitialized,
}
pub fn seed_from(seed: [u8; 32]) {
RNG.with_borrow_mut(|rng| {
*rng = Some(ChaCha20Rng::from_seed(seed));
});
}
#[must_use]
pub fn is_seeded() -> bool {
RNG.with_borrow(Option::is_some)
}
fn with_rng<T>(f: impl FnOnce(&mut ChaCha20Rng) -> T) -> Result<T, RngError> {
RNG.with_borrow_mut(|rng| match rng.as_mut() {
Some(rand) => Ok(f(rand)),
None => Err(RngError::NotInitialized),
})
}
pub fn fill_bytes(dest: &mut [u8]) -> Result<(), RngError> {
with_rng(|rand| rand.fill_bytes(dest))
}
pub fn random_bytes(size: usize) -> Result<Vec<u8>, RngError> {
let mut buf = vec![0u8; size];
fill_bytes(&mut buf)?;
Ok(buf)
}
pub fn next_u8() -> Result<u8, RngError> {
Ok((next_u16()? & 0xFF) as u8)
}
#[expect(clippy::cast_possible_truncation)]
pub fn next_u16() -> Result<u16, RngError> {
with_rng(|rand| rand.next_u32() as u16)
}
pub fn next_u32() -> Result<u32, RngError> {
with_rng(Rng::next_u32)
}
pub fn next_u64() -> Result<u64, RngError> {
with_rng(Rng::next_u64)
}
pub fn next_u128() -> Result<u128, RngError> {
with_rng(|rand| {
let hi = u128::from(rand.next_u64());
let lo = u128::from(rand.next_u64());
(hi << 64) | lo
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unique_u64s() {
use std::collections::HashSet;
seed_from([7; 32]);
let mut set = HashSet::new();
while set.len() < 1000 {
let random_value = next_u64().expect("seeded RNG");
assert!(set.insert(random_value), "value already in set");
}
}
#[test]
fn test_rng_reseeding() {
seed_from([1; 32]);
let first = next_u64().expect("seeded RNG");
seed_from([2; 32]);
let second = next_u64().expect("seeded RNG");
assert_ne!(
first, second,
"RNGs with different seeds unexpectedly produced the same value"
);
}
#[test]
fn test_determinism_with_fixed_seed() {
let seed = [42u8; 32];
seed_from(seed);
let values: Vec<u64> = (0..100).map(|_| next_u64().expect("seeded RNG")).collect();
seed_from(seed);
for value in values {
assert_eq!(next_u64().expect("seeded RNG"), value);
}
}
#[test]
fn test_missing_seed_errors() {
RNG.with_borrow_mut(|rng| {
*rng = None;
});
assert!(matches!(random_bytes(8), Err(RngError::NotInitialized)));
}
#[test]
fn test_bit_entropy() {
seed_from([3; 32]);
let mut bits = 0u64;
for _ in 0..100 {
bits |= next_u64().expect("seeded RNG");
}
let bit_count = bits.count_ones();
assert!(
bit_count > 8,
"Low entropy: only {bit_count} bits set in 100 samples",
);
}
}