Skip to main content

commonware_utils/
rng.rs

1//! Utilities for random number generation.
2
3use rand::{rngs::StdRng, CryptoRng, RngCore, SeedableRng};
4use std::mem::size_of;
5
6/// Returns a seeded RNG for deterministic testing.
7///
8/// Uses seed 0 by default to ensure reproducible test results.
9pub fn test_rng() -> StdRng {
10    StdRng::seed_from_u64(0)
11}
12
13/// Returns a seeded RNG with a custom seed for deterministic testing.
14///
15/// Use this when you need multiple independent RNG streams in the same test,
16/// or when a helper function needs its own RNG that won't collide with the caller's.
17pub fn test_rng_seeded(seed: u64) -> StdRng {
18    StdRng::seed_from_u64(seed)
19}
20
21/// Domain-separation constant for the mixing step. This ensures the mixed stream
22/// is not derived from `word ^ ctr` alone and helps avoid accidental fixed points
23/// when fuzz input has low structure (for example empty or repeated bytes).
24const FUZZ_RNG_MIX_DOMAIN: u64 = 0x9e3779b97f4a7c15;
25/// Width of each source window in bytes.
26const BLOCK_BYTES: usize = size_of::<u64>();
27
28/// An RNG that expands a fuzzer byte slice into an infinite deterministic stream.
29///
30/// # Design
31///
32/// `FuzzRng` maps a fuzzer-controlled byte slice to output blocks.
33///
34/// For each block counter `ctr`, it:
35/// 1. Reads a wrapping `u64`-wide window from the input bytes.
36/// 2. Xors in `ctr` and a domain constant.
37/// 3. Applies a SplitMix64-style finalizer.
38///
39/// ```text
40/// input bytes (len = N):
41///   [b0 b1 b2 ... b(N-1)]
42///
43/// block ctr = i:
44///   word_i bytes = [b(i+0)%N, b(i+1)%N, ... b(i+7)%N]
45///   word_i       = big-endian u64 of those bytes
46///   out_i        = mix64(word_i ^ i ^ DOMAIN)
47/// ```
48///
49/// # Why this mapping
50///
51/// Hashing the full input once and then seeding a PRNG makes tiny input changes
52/// look globally unrelated. This adapter avoids that by using a sliding window
53/// keyed by the block counter.
54///
55/// ```text
56/// byte k affects anchors:
57///   i in [k-(BLOCK_BYTES-1), ..., k] (mod N)
58/// ```
59///
60/// # Worked Example
61///
62/// With `N = 4`, input bytes repeat inside each block:
63///
64/// ```text
65/// input: [a b c d]
66///
67/// ctr=0: word bytes [a b c d a b c d]
68/// ctr=1: word bytes [b c d a b c d a]
69/// ctr=2: word bytes [c d a b c d a b]
70/// ...
71/// ```
72///
73/// Even for low-entropy input like `[0 0 0 0]`, output still changes because
74/// `ctr` is mixed into every block before finalization.
75///
76/// `fill_bytes` serves output from cached block bytes so callers get a stable
77/// byte stream regardless of whether they request randomness as `next_u64`,
78/// `next_u32`, or arbitrary byte slices.
79pub struct FuzzRng {
80    bytes: Vec<u8>,
81    ctr: u64,
82    cache: [u8; BLOCK_BYTES],
83    cache_pos: usize,
84}
85
86impl FuzzRng {
87    /// Creates a new `FuzzRng` from a byte buffer.
88    pub const fn new(bytes: Vec<u8>) -> Self {
89        Self {
90            bytes,
91            ctr: 0,
92            cache: [0u8; BLOCK_BYTES],
93            cache_pos: BLOCK_BYTES,
94        }
95    }
96
97    /// Generates the next mixed `u64` block from the fuzz input.
98    ///
99    /// Conceptually:
100    /// 1. Build `word` from a wrapping `BLOCK_BYTES` window anchored at `ctr`.
101    /// 2. Compute `mixed = mix64(word ^ ctr ^ FUZZ_RNG_MIX_DOMAIN)`.
102    /// 3. Increment `ctr`.
103    ///
104    /// This keeps the output deterministic while preserving local mutation
105    /// influence: one input-byte mutation only affects nearby anchor counters.
106    #[inline]
107    fn next_block_u64(&mut self) -> u64 {
108        // Build a wrapping u64-width source word anchored at this block counter.
109        // A single fuzz-byte mutation only impacts nearby anchors.
110        let mut bytes = [0u8; BLOCK_BYTES];
111        if !self.bytes.is_empty() {
112            let len = self.bytes.len() as u64;
113            for (i, byte) in bytes.iter_mut().enumerate() {
114                *byte = self.bytes[(self.ctr.wrapping_add(i as u64) % len) as usize];
115            }
116        }
117        let word = u64::from_be_bytes(bytes);
118
119        // Mix the structured word into a high-quality output block without
120        // hashing the entire seed into an avalanche-style global state.
121        let mut out = word ^ self.ctr ^ FUZZ_RNG_MIX_DOMAIN;
122        out ^= out >> 30;
123        out = out.wrapping_mul(0xbf58476d1ce4e5b9);
124        out ^= out >> 27;
125        out = out.wrapping_mul(0x94d049bb133111eb);
126        out ^= out >> 31;
127
128        self.ctr = self.ctr.wrapping_add(1);
129        out
130    }
131}
132
133impl RngCore for FuzzRng {
134    fn next_u32(&mut self) -> u32 {
135        let mut buf = [0u8; 4];
136        self.fill_bytes(&mut buf);
137        u32::from_be_bytes(buf)
138    }
139
140    fn next_u64(&mut self) -> u64 {
141        let mut buf = [0u8; BLOCK_BYTES];
142        self.fill_bytes(&mut buf);
143        u64::from_be_bytes(buf)
144    }
145
146    fn fill_bytes(&mut self, dest: &mut [u8]) {
147        let mut written = 0;
148        while written < dest.len() {
149            if self.cache_pos == self.cache.len() {
150                // Cache block bytes so outputs are stable regardless of whether
151                // callers pull randomness as bytes or words:
152                //
153                // next_u64() stream bytes == fill_bytes() stream bytes.
154                self.cache = self.next_block_u64().to_be_bytes();
155                self.cache_pos = 0;
156            }
157
158            let available = self.cache.len() - self.cache_pos;
159            let need = dest.len() - written;
160            let take = available.min(need);
161            dest[written..written + take]
162                .copy_from_slice(&self.cache[self.cache_pos..self.cache_pos + take]);
163            self.cache_pos += take;
164            written += take;
165        }
166    }
167
168    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> {
169        self.fill_bytes(dest);
170        Ok(())
171    }
172}
173
174// SAFETY: FuzzRng is not cryptographically secure. It implements CryptoRng
175// only because the consensus fuzzer requires CryptoRng-bounded RNG. This type
176// must never be used outside of fuzz/test contexts.
177impl CryptoRng for FuzzRng {}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_empty_bytes_not_constant() {
185        let mut rng = FuzzRng::new(vec![]);
186
187        let values: Vec<_> = (0..BLOCK_BYTES).map(|_| rng.next_u64()).collect();
188        assert!(values.windows(2).any(|w| w[0] != w[1]));
189    }
190
191    #[test]
192    fn test_empty_bytes_deterministic() {
193        let mut rng1 = FuzzRng::new(vec![]);
194        let mut rng2 = FuzzRng::new(vec![]);
195
196        for _ in 0..256 {
197            assert_eq!(rng1.next_u64(), rng2.next_u64());
198        }
199    }
200
201    #[test]
202    fn test_all_zero_bytes_not_constant() {
203        let bytes = vec![0; BLOCK_BYTES];
204        let mut rng = FuzzRng::new(bytes);
205        let values: Vec<_> = (0..BLOCK_BYTES).map(|_| rng.next_u64()).collect();
206        assert!(values.windows(2).any(|w| w[0] != w[1]));
207    }
208
209    #[test]
210    fn test_deterministic_with_same_input() {
211        let bytes = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
212
213        let mut rng1 = FuzzRng::new(bytes.clone());
214        let mut rng2 = FuzzRng::new(bytes);
215
216        for _ in 0..1000 {
217            assert_eq!(rng1.next_u64(), rng2.next_u64());
218        }
219    }
220
221    #[test]
222    fn test_short_input_wraparound() {
223        for len in 1..=3 {
224            let bytes = vec![0xAB; len];
225            let mut rng1 = FuzzRng::new(bytes.clone());
226            let mut rng2 = FuzzRng::new(bytes);
227            let out1: Vec<_> = (0..32).map(|_| rng1.next_u64()).collect();
228            let out2: Vec<_> = (0..32).map(|_| rng2.next_u64()).collect();
229            assert_eq!(out1, out2);
230            assert!(out1.windows(2).any(|w| w[0] != w[1]));
231        }
232    }
233
234    #[test]
235    fn test_small_mutation_locality() {
236        let mut base = vec![0u8; 64];
237        for (i, byte) in base.iter_mut().enumerate() {
238            *byte = i as u8;
239        }
240        let mut mutated = base.clone();
241        let mutated_pos = 20usize;
242        mutated[mutated_pos] ^= 0x01;
243
244        let mut rng_a = FuzzRng::new(base);
245        let mut rng_b = FuzzRng::new(mutated);
246
247        let draws = 40usize;
248        let mut diff_indices = Vec::new();
249        for i in 0..draws {
250            if rng_a.next_u64() != rng_b.next_u64() {
251                diff_indices.push(i);
252            }
253        }
254
255        let expected: Vec<usize> = ((mutated_pos - 7)..=mutated_pos).collect();
256        assert_eq!(diff_indices, expected);
257    }
258
259    #[test]
260    fn test_small_mutation_locality_wraparound() {
261        let mut base = vec![0u8; 64];
262        for (i, byte) in base.iter_mut().enumerate() {
263            *byte = i as u8;
264        }
265        let mut mutated = base.clone();
266        let mutated_pos = 2usize;
267        mutated[mutated_pos] ^= 0x01;
268
269        let mut rng_a = FuzzRng::new(base);
270        let mut rng_b = FuzzRng::new(mutated);
271
272        let draws = 64usize;
273        let mut diff_indices = Vec::new();
274        for i in 0..draws {
275            if rng_a.next_u64() != rng_b.next_u64() {
276                diff_indices.push(i);
277            }
278        }
279
280        assert_eq!(diff_indices, vec![0, 1, 2, 59, 60, 61, 62, 63]);
281    }
282
283    #[test]
284    fn test_fill_bytes_shape_stability() {
285        let bytes: Vec<u8> = (0..32u8).collect();
286
287        let mut from_u64_rng = FuzzRng::new(bytes.clone());
288        let mut from_u64 = Vec::with_capacity(128);
289        for _ in 0..16 {
290            from_u64.extend_from_slice(&from_u64_rng.next_u64().to_be_bytes());
291        }
292
293        let mut from_fill_rng = FuzzRng::new(bytes);
294        let mut from_fill = vec![0u8; from_u64.len()];
295        let chunk_sizes = [3usize, 1, 7, 2, 11, 5, 13, 17];
296        let mut offset = 0;
297        let mut idx = 0;
298        while offset < from_fill.len() {
299            let chunk = chunk_sizes[idx % chunk_sizes.len()].min(from_fill.len() - offset);
300            from_fill_rng.fill_bytes(&mut from_fill[offset..offset + chunk]);
301            offset += chunk;
302            idx += 1;
303        }
304        assert_eq!(from_u64, from_fill);
305    }
306
307    #[test]
308    fn test_next_u32_consistency_with_fill_bytes() {
309        let bytes: Vec<u8> = (0..16u8).collect();
310
311        let mut from_u32_rng = FuzzRng::new(bytes.clone());
312        let mut from_u32 = Vec::with_capacity(64);
313        for _ in 0..16 {
314            from_u32.extend_from_slice(&from_u32_rng.next_u32().to_be_bytes());
315        }
316
317        let mut from_fill_rng = FuzzRng::new(bytes);
318        let mut from_fill = vec![0u8; from_u32.len()];
319        from_fill_rng.fill_bytes(&mut from_fill);
320        assert_eq!(from_u32, from_fill);
321    }
322
323    #[test]
324    fn test_try_fill_bytes_consistency_with_fill_bytes() {
325        let bytes: Vec<u8> = (0..16u8).collect();
326
327        let mut fill_rng = FuzzRng::new(bytes.clone());
328        let mut try_fill_rng = FuzzRng::new(bytes);
329
330        let mut fill_out = vec![0u8; 257];
331        fill_rng.fill_bytes(&mut fill_out);
332
333        let mut try_out = vec![0u8; 257];
334        try_fill_rng
335            .try_fill_bytes(&mut try_out)
336            .expect("try_fill_bytes should never fail");
337
338        assert_eq!(fill_out, try_out);
339    }
340
341    #[test]
342    fn test_next_u64_includes_counter_in_mix_input() {
343        // Use a constant source window so any change between blocks comes from
344        // counter mixing, not from different window bytes.
345        let bytes = vec![0xAA; BLOCK_BYTES];
346        let mut rng = FuzzRng::new(bytes.clone());
347
348        let mut source = [0u8; BLOCK_BYTES];
349        source.copy_from_slice(&bytes[..BLOCK_BYTES]);
350        let word = u64::from_be_bytes(source);
351
352        let mix = |mut x: u64| {
353            x ^= x >> 30;
354            x = x.wrapping_mul(0xbf58476d1ce4e5b9);
355            x ^= x >> 27;
356            x = x.wrapping_mul(0x94d049bb133111eb);
357            x ^= x >> 31;
358            x
359        };
360
361        #[allow(clippy::identity_op)]
362        let expected0 = mix(word ^ 0 ^ FUZZ_RNG_MIX_DOMAIN);
363        let expected1 = mix(word ^ 1 ^ FUZZ_RNG_MIX_DOMAIN);
364
365        assert_eq!(rng.next_u64(), expected0);
366        assert_eq!(rng.next_u64(), expected1);
367    }
368
369    #[cfg(feature = "arbitrary")]
370    mod conformance {
371        use super::*;
372        use commonware_conformance::Conformance;
373        use rand::Rng;
374
375        /// Conformance wrapper for FuzzRng that tests output stability.
376        ///
377        /// Derives both the input length and content from a seeded RNG so
378        /// conformance covers variable-length inputs including non-aligned
379        /// lengths that exercise wrapping.
380        struct FuzzRngConformance;
381
382        impl Conformance for FuzzRngConformance {
383            async fn commit(seed: u64) -> Vec<u8> {
384                let mut seed_rng = test_rng_seeded(seed);
385                let len = seed_rng.gen_range(1..=64);
386                let mut input = vec![0u8; len];
387                seed_rng.fill_bytes(&mut input);
388
389                let mut rng = FuzzRng::new(input);
390                const CONFORMANCE_BLOCKS: usize = 32;
391
392                // Generate enough output to exercise wrapping and mixing.
393                let mut output = Vec::with_capacity(CONFORMANCE_BLOCKS * BLOCK_BYTES);
394                for _ in 0..CONFORMANCE_BLOCKS {
395                    output.extend_from_slice(&rng.next_u64().to_be_bytes());
396                }
397                output
398            }
399        }
400
401        commonware_conformance::conformance_tests! {
402            FuzzRngConformance => 1024,
403        }
404    }
405}