snarkvm_utilities/rand.rs
1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use rand::{
17 Rng,
18 SeedableRng,
19 distributions::{Distribution, Standard},
20 rngs::StdRng,
21};
22use rand_xorshift::XorShiftRng;
23
24/// A trait for a uniform random number generator.
25pub trait Uniform: Sized {
26 /// Samples a random value from a uniform distribution.
27 fn rand<R: Rng + ?Sized>(rng: &mut R) -> Self;
28}
29
30impl<T> Uniform for T
31where
32 Standard: Distribution<T>,
33{
34 #[inline]
35 fn rand<R: Rng + ?Sized>(rng: &mut R) -> Self {
36 rng.sample(Standard)
37 }
38}
39
40/// A fast RNG used **solely** for testing and benchmarking, **not** for any real world purposes.
41pub struct TestRng {
42 seed: u64,
43 rng: XorShiftRng,
44 calls: usize,
45}
46
47impl Default for TestRng {
48 fn default() -> Self {
49 // Obtain the initial seed using entropy provided by the OS.
50 let seed = StdRng::from_entropy().r#gen();
51
52 // Use it as the basis for the underlying Rng.
53 Self::fixed(seed)
54 }
55}
56
57impl TestRng {
58 pub fn fixed(seed: u64) -> Self {
59 // Print the seed, so it's displayed if any of the tests using `test_rng` fails.
60 println!("\nInitializing 'TestRng' with seed '{seed}'\n");
61
62 // Use the seed to initialize a fast, non-cryptographic Rng.
63 Self::from_seed(seed)
64 }
65
66 // This is the preferred method to use once the main instance of TestRng had already
67 // been initialized in a test or benchmark and an auxiliary one is desired without
68 // spamming the stdout.
69 pub fn from_seed(seed: u64) -> Self {
70 Self { seed, rng: XorShiftRng::seed_from_u64(seed), calls: 0 }
71 }
72
73 /// Returns a randomly-sampled `String`, given the maximum size in bytes and an RNG.
74 ///
75 /// Some of the snarkVM internal tests involve the random generation of strings,
76 /// which are parsed and tested against the original ones. However, since the string parser
77 /// rejects certain characters, if those characters are randomly generated, the tests fail.
78 ///
79 /// To prevent these failures, as we randomly generate the characters,
80 /// we ensure that they are not among the ones rejected by the parser;
81 /// if they are, we adjust them to be allowed characters.
82 ///
83 /// Note that the randomness of the characters is strictly for **testing** purposes;
84 /// also note that the disallowed characters are a small fraction of the total set of characters,
85 /// and thus the adjustments rarely occur.
86 pub fn next_string(&mut self, max_bytes: u32, is_fixed_size: bool) -> String {
87 /// Adjust an unsafe character.
88 ///
89 /// As our parser rejects certain potentially unsafe characters (see `Sanitizer::parse_safe_char`),
90 /// we need to avoid generating them randomly. This function acts as an adjusting filter:
91 /// it changes an unsafe character to `'0'` (other choices are possible), and leaves other
92 /// characters unchanged.
93 fn adjust_unsafe_char(ch: char) -> char {
94 let code = ch as u32;
95 if code < 9
96 || code == 11
97 || code == 12
98 || (14..=31).contains(&code)
99 || code == 127
100 || (0x202a..=0x202e).contains(&code)
101 || (0x2066..=0x2069).contains(&code)
102 {
103 '0'
104 } else {
105 ch
106 }
107 }
108
109 /// Adjust a backslash and a double quote.
110 ///
111 /// Aside from the characters rejected through the function [adjust_unsafe_char],
112 /// the syntax of strings allows backslash and double quotes only in certain circumstances:
113 /// backslash is used to introduce an escape, and there are constraints on what can occur
114 /// after a backslash; double quotes is only used in escaped form just after a backslash.
115 ///
116 /// If we randomly sample characters, we may end up generating backslashes with
117 /// malformed escape syntax, or double quotes not preceded by backslash. Thus,
118 /// we also adjust backslashes and double quotes as we randomly sample characters.
119 ///
120 /// Note that, this way, we do not test the parsing of any escape sequences;
121 /// to do that, we would need to reify the possible elements of strings,
122 /// namely characters and escapes, and randomly generate such elements.
123 fn adjust_backslash_and_doublequote(ch: char) -> char {
124 if ch == '\\' || ch == '\"' { '0' } else { ch }
125 }
126
127 let range = match is_fixed_size {
128 true => 0..max_bytes,
129 false => 0..self.gen_range(0..max_bytes),
130 };
131
132 range.map(|_| self.r#gen::<char>()).map(adjust_unsafe_char).map(adjust_backslash_and_doublequote).collect()
133 }
134}
135
136impl rand::RngCore for TestRng {
137 fn next_u32(&mut self) -> u32 {
138 self.calls += 1;
139 self.rng.next_u32()
140 }
141
142 fn next_u64(&mut self) -> u64 {
143 self.calls += 1;
144 self.rng.next_u64()
145 }
146
147 fn fill_bytes(&mut self, dest: &mut [u8]) {
148 self.calls += 1;
149 self.rng.fill_bytes(dest)
150 }
151
152 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> {
153 self.calls += 1;
154 self.rng.try_fill_bytes(dest)
155 }
156}
157
158impl rand::CryptoRng for TestRng {}
159
160impl Drop for TestRng {
161 fn drop(&mut self) {
162 println!("Called TestRng with seed {} {} times", self.seed, self.calls);
163 }
164}