canic_utils/
rand.rs

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