Monorepo for Tangled
tangled.org
1use std::sync::atomic::{AtomicU64, Ordering};
2
3pub trait Entropy: Send + Sync + 'static {
4 fn next_u64(&self) -> u64;
5}
6
7#[derive(Clone, Copy, Debug, Default)]
8pub struct OsEntropy;
9
10impl Entropy for OsEntropy {
11 fn next_u64(&self) -> u64 {
12 let mut buf = [0u8; 8];
13 getrandom::fill(&mut buf).expect("os entropy source unavailable");
14 u64::from_le_bytes(buf)
15 }
16}
17
18#[derive(Debug)]
19pub struct SeededEntropy {
20 state: AtomicU64,
21}
22
23impl SeededEntropy {
24 const GOLDEN: u64 = 0x9E37_79B9_7F4A_7C15;
25
26 pub fn new(seed: u64) -> Self {
27 Self {
28 state: AtomicU64::new(seed),
29 }
30 }
31}
32
33impl Entropy for SeededEntropy {
34 fn next_u64(&self) -> u64 {
35 let prev = self.state.fetch_add(Self::GOLDEN, Ordering::Relaxed);
36 let z = prev.wrapping_add(Self::GOLDEN);
37 let z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
38 let z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
39 z ^ (z >> 31)
40 }
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46 use std::sync::Arc;
47 use std::sync::atomic::{AtomicU64, Ordering};
48
49 struct CountingEntropy(AtomicU64);
50
51 impl Entropy for CountingEntropy {
52 fn next_u64(&self) -> u64 {
53 self.0.fetch_add(1, Ordering::SeqCst)
54 }
55 }
56
57 #[test]
58 fn external_impl_works_behind_dyn_arc() {
59 let e: Arc<dyn Entropy> = Arc::new(CountingEntropy(AtomicU64::new(100)));
60 assert_eq!(e.next_u64(), 100);
61 assert_eq!(e.next_u64(), 101);
62 }
63
64 #[test]
65 fn seeded_entropy_two_constructions_with_same_seed_yield_same_stream() {
66 let a = SeededEntropy::new(0xCAFE_F00D);
67 let b = SeededEntropy::new(0xCAFE_F00D);
68 for _ in 0..32 {
69 assert_eq!(
70 a.next_u64(),
71 b.next_u64(),
72 "splitmix stream must reproduce exactly across constructions with same seed",
73 );
74 }
75 }
76
77 #[test]
78 fn seeded_entropy_different_seeds_diverge() {
79 let a = SeededEntropy::new(1);
80 let b = SeededEntropy::new(2);
81 let mut all_match = true;
82 for _ in 0..32 {
83 if a.next_u64() != b.next_u64() {
84 all_match = false;
85 break;
86 }
87 }
88 assert!(
89 !all_match,
90 "two seeded entropies with distinct seeds must diverge inside 32 draws",
91 );
92 }
93
94 #[test]
95 fn seeded_entropy_does_not_repeat_inside_short_window() {
96 let e = SeededEntropy::new(0);
97 let mut seen = std::collections::HashSet::new();
98 for _ in 0..1024 {
99 assert!(
100 seen.insert(e.next_u64()),
101 "splitmix collided inside 1024 draws"
102 );
103 }
104 }
105
106 #[test]
107 fn os_entropy_does_not_collide_on_back_to_back_calls() {
108 let e = OsEntropy;
109 let a = e.next_u64();
110 let b = e.next_u64();
111 assert_ne!(
112 a, b,
113 "back-to-back os entropy collided, getrandom likely not actually wired up",
114 );
115 }
116}