canic_utils/
rand.rs

1//!
2//! Thread-local PRNG seeded externally (raw_rand recommended).
3//!
4//! The IC executes canister code single-threaded, so `RefCell` provides
5//! sufficient interior mutability without locking or poisoning semantics.
6//!
7//! Use update calls for randomness so the PRNG state advances, and seed in
8//! init + post_upgrade via timers.
9//!
10use rand_chacha::{
11    ChaCha20Rng,
12    rand_core::{RngCore, SeedableRng},
13};
14use std::cell::RefCell;
15
16thread_local! {
17    static RNG: RefCell<Option<ChaCha20Rng>> = const { RefCell::new(None) };
18}
19
20// -----------------------------------------------------------------------------
21// Errors
22// -----------------------------------------------------------------------------
23
24///
25/// RngError
26/// Errors raised when randomness is unavailable.
27///
28
29#[derive(Debug)]
30pub enum RngError {
31    RngNotInitialized(String),
32}
33
34impl RngError {
35    fn not_initialized() -> Self {
36        Self::RngNotInitialized("Randomness is not initialized. Please try again later".to_string())
37    }
38}
39
40// -----------------------------------------------------------------------------
41// Seeding
42// -----------------------------------------------------------------------------
43
44/// Seed the RNG with a 32-byte value (e.g. management canister `raw_rand` output).
45pub fn seed_from(seed: [u8; 32]) {
46    RNG.with_borrow_mut(|rng| {
47        *rng = Some(ChaCha20Rng::from_seed(seed));
48    });
49}
50
51/// Returns true if the RNG has been seeded.
52#[must_use]
53pub fn is_seeded() -> bool {
54    RNG.with_borrow(Option::is_some)
55}
56
57fn with_rng<T>(f: impl FnOnce(&mut ChaCha20Rng) -> T) -> Result<T, RngError> {
58    RNG.with_borrow_mut(|rng| match rng.as_mut() {
59        Some(rand) => Ok(f(rand)),
60        None => Err(RngError::not_initialized()),
61    })
62}
63
64// -----------------------------------------------------------------------------
65// Random bytes
66// -----------------------------------------------------------------------------
67
68/// Fill the provided buffer with random bytes.
69pub fn fill_bytes(dest: &mut [u8]) -> Result<(), RngError> {
70    with_rng(|rand| rand.fill_bytes(dest))
71}
72
73/// Produce random bytes using the shared RNG.
74pub fn random_bytes(size: usize) -> Result<Vec<u8>, RngError> {
75    let mut buf = vec![0u8; size];
76    fill_bytes(&mut buf)?;
77    Ok(buf)
78}
79
80/// Produce hex-encoded random bytes using the shared RNG.
81pub fn random_hex(size: usize) -> Result<String, RngError> {
82    let bytes = random_bytes(size)?;
83    Ok(hex::encode(bytes))
84}
85
86/// Produce an 8-bit random value (derived from `next_u16`).
87pub fn next_u8() -> Result<u8, RngError> {
88    Ok((next_u16()? & 0xFF) as u8)
89}
90
91/// Produce a 16-bit random value from the shared RNG.
92#[allow(clippy::cast_possible_truncation)]
93pub fn next_u16() -> Result<u16, RngError> {
94    with_rng(|rand| rand.next_u32() as u16)
95}
96
97/// Produce a 32-bit random value from the shared RNG.
98pub fn next_u32() -> Result<u32, RngError> {
99    with_rng(RngCore::next_u32)
100}
101
102/// Produce a 64-bit random value from the shared RNG.
103pub fn next_u64() -> Result<u64, RngError> {
104    with_rng(RngCore::next_u64)
105}
106
107/// Produce a 128-bit random value from the shared RNG.
108pub fn next_u128() -> Result<u128, RngError> {
109    with_rng(|rand| {
110        let hi = u128::from(rand.next_u64());
111        let lo = u128::from(rand.next_u64());
112        (hi << 64) | lo
113    })
114}
115
116///
117/// TESTS
118///
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_unique_u64s() {
126        use std::collections::HashSet;
127
128        seed_from([7; 32]);
129
130        let mut set = HashSet::new();
131        while set.len() < 1000 {
132            let random_value = next_u64().expect("seeded RNG");
133            assert!(set.insert(random_value), "value already in set");
134        }
135    }
136
137    #[test]
138    fn test_rng_reseeding() {
139        seed_from([1; 32]);
140        let first = next_u64().expect("seeded RNG");
141        seed_from([2; 32]);
142        let second = next_u64().expect("seeded RNG");
143
144        assert_ne!(
145            first, second,
146            "RNGs with different seeds unexpectedly produced the same value"
147        );
148    }
149
150    #[test]
151    fn test_determinism_with_fixed_seed() {
152        let seed = [42u8; 32];
153        seed_from(seed);
154
155        let values: Vec<u64> = (0..100).map(|_| next_u64().expect("seeded RNG")).collect();
156
157        seed_from(seed);
158        for value in values {
159            assert_eq!(next_u64().expect("seeded RNG"), value);
160        }
161    }
162
163    #[test]
164    fn test_missing_seed_errors() {
165        RNG.with_borrow_mut(|rng| {
166            *rng = None;
167        });
168
169        assert!(matches!(
170            random_bytes(8),
171            Err(RngError::RngNotInitialized(_))
172        ));
173    }
174
175    #[test]
176    fn test_random_hex_length() {
177        seed_from([9; 32]);
178
179        let value = random_hex(6).expect("seeded RNG");
180        assert_eq!(value.len(), 12);
181    }
182
183    // Sanity check only: ensures bits vary across samples.
184    // This is not a statistical entropy test.
185    #[test]
186    fn test_bit_entropy() {
187        seed_from([3; 32]);
188
189        let mut bits = 0u64;
190        for _ in 0..100 {
191            bits |= next_u64().expect("seeded RNG");
192        }
193
194        let bit_count = bits.count_ones();
195        assert!(
196            bit_count > 8,
197            "Low entropy: only {bit_count} bits set in 100 samples",
198        );
199    }
200}