1use rand::{rngs::StdRng, CryptoRng, RngCore, SeedableRng};
4use std::mem::size_of;
5
6pub fn test_rng() -> StdRng {
10 StdRng::seed_from_u64(0)
11}
12
13pub fn test_rng_seeded(seed: u64) -> StdRng {
18 StdRng::seed_from_u64(seed)
19}
20
21const FUZZ_RNG_MIX_DOMAIN: u64 = 0x9e3779b97f4a7c15;
25const BLOCK_BYTES: usize = size_of::<u64>();
27
28pub struct FuzzRng {
80 bytes: Vec<u8>,
81 ctr: u64,
82 cache: [u8; BLOCK_BYTES],
83 cache_pos: usize,
84}
85
86impl FuzzRng {
87 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 #[inline]
107 fn next_block_u64(&mut self) -> u64 {
108 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 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 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
174impl 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 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 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 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}