Skip to main content

iota_common/
portable_random.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Sampling routines whose results depend only on the bytes an RNG produces,
5//! never on which version of `rand` is in the dependency graph.
6//!
7//! `rand`'s own `shuffle`, `choose`, `choose_multiple` and
8//! `choose_multiple_weighted` are free to change between releases, and they
9//! have: 0.9 replaced the range sampler and the weighted-selection wrapper, so
10//! the same seed produces a different permutation. That is fine for a test
11//! fixture and unacceptable for a permutation every validator has to agree on.
12//!
13//! The routines here reproduce `rand` 0.8's algorithms and are fixed by
14//! tests against captured vectors, so callers that need a permutation to
15//! stay the same forever can bump `rand` freely.
16//!
17//! Two rules for callers:
18//!
19//! - Seed [`rand::rngs::ChaCha12Rng`], not `StdRng`. `StdRng` is documented as non-portable —
20//!   `rand` reserves the right to swap the algorithm under it — whereas `ChaCha12Rng` names the
21//!   generator and is what `StdRng` currently happens to be.
22//! - Draw floats through [`f64_unit`] rather than `rand`'s `random::<f64>()`, which is another
23//!   conversion `rand` owns and could change.
24
25use rand::Rng;
26
27/// Draws a `f64` uniformly from `[0, 1)`, using the top 53 bits of one 64-bit
28/// word.
29pub fn f64_unit<R: Rng + ?Sized>(rng: &mut R) -> f64 {
30    const SCALE: f64 = 1.0 / ((1u64 << 53) as f64);
31    SCALE * ((rng.next_u64() >> 11) as f64)
32}
33
34/// Draws a `u32` uniformly from `low..=high`.
35///
36/// Lemire's multiply-shift with rejection: multiply a random word by the range
37/// width and keep the high half, rejecting the low half's biased tail.
38fn uniform_u32_inclusive<R: Rng + ?Sized>(rng: &mut R, low: u32, high: u32) -> u32 {
39    let range = high.wrapping_sub(low).wrapping_add(1);
40    if range == 0 {
41        // `low..=high` spans the whole of u32, so every word is in range.
42        return rng.next_u32();
43    }
44    let zone = (range << range.leading_zeros()).wrapping_sub(1);
45    loop {
46        let product = u64::from(rng.next_u32()) * u64::from(range);
47        let (hi, lo) = ((product >> 32) as u32, product as u32);
48        if lo <= zone {
49            return low.wrapping_add(hi);
50        }
51    }
52}
53
54/// Draws an index uniformly from `0..ubound`.
55///
56/// Sampling is done in 32 bits so that the result does not depend on the width
57/// of `usize`. Panics if `ubound` is 0 or exceeds `u32::MAX`.
58pub fn index<R: Rng + ?Sized>(rng: &mut R, ubound: usize) -> usize {
59    assert!(ubound > 0, "cannot draw an index from an empty range");
60    assert!(
61        ubound <= u32::MAX as usize,
62        "index sampling is limited to u32::MAX to stay independent of usize width"
63    );
64    uniform_u32_inclusive(rng, 0, ubound as u32 - 1) as usize
65}
66
67/// Permutes `slice` in place (Fisher-Yates, from the back).
68pub fn shuffle<T, R: Rng + ?Sized>(rng: &mut R, slice: &mut [T]) {
69    for i in (1..slice.len()).rev() {
70        slice.swap(i, index(rng, i + 1));
71    }
72}
73
74/// Returns a uniformly drawn element, or `None` if `slice` is empty.
75pub fn choose<'a, T, R: Rng + ?Sized>(rng: &mut R, slice: &'a [T]) -> Option<&'a T> {
76    if slice.is_empty() {
77        None
78    } else {
79        Some(&slice[index(rng, slice.len())])
80    }
81}
82
83/// Draws `amount` distinct indices from `0..length`, in random order.
84///
85/// Which of the two algorithms runs is decided by `length` and `amount` alone,
86/// so it is as reproducible as the draws themselves. Panics if `amount`
87/// exceeds `length`, or if the pair falls in the range where `rand` 0.8 used a
88/// third algorithm — unreachable for `amount == length`, which is how every
89/// caller here uses it, and better to fail loudly than to diverge quietly.
90pub fn sample_indices<R: Rng + ?Sized>(rng: &mut R, length: usize, amount: usize) -> Vec<usize> {
91    assert!(
92        amount <= length,
93        "cannot sample more indices than are available"
94    );
95    assert!(
96        length <= u32::MAX as usize,
97        "index sampling is limited to u32::MAX to stay independent of usize width"
98    );
99    let (amount, length) = (amount as u32, length as u32);
100
101    // Thresholds carried over verbatim from `rand` 0.8; they trade set-up cost
102    // against draw count and have no meaning beyond picking the cheaper
103    // algorithm.
104    if amount < 163 {
105        let scale = if length < 500_000 {
106            (1.6, 10.0)
107        } else {
108            (8.0 / 45.0, 70.0 / 9.0)
109        };
110        let amount_fp = amount as f32;
111        if amount > 11 && (length as f32) < (scale.1 + scale.0 * amount_fp) * amount_fp {
112            sample_in_place(rng, length, amount)
113        } else {
114            sample_floyd(rng, length, amount)
115        }
116    } else {
117        let scale = if length < 500_000 { 270.0 } else { 330.0 / 9.0 };
118        assert!(
119            (length as f32) < scale * (amount as f32),
120            "sampling {amount} of {length} indices needs the algorithm this module does not carry"
121        );
122        sample_in_place(rng, length, amount)
123    }
124}
125
126/// Floyd's combination algorithm, fully shuffled.
127fn sample_floyd<R: Rng + ?Sized>(rng: &mut R, length: u32, amount: u32) -> Vec<usize> {
128    // Floyd's insert keeps the result shuffled as it goes, but `Vec::insert`
129    // gets expensive, so past this size the shuffle is done in one pass after.
130    let shuffle_while_inserting = amount < 50;
131
132    let mut indices: Vec<u32> = Vec::with_capacity(amount as usize);
133    for j in length - amount..length {
134        let t = uniform_u32_inclusive(rng, 0, j);
135        if shuffle_while_inserting {
136            if let Some(pos) = indices.iter().position(|&x| x == t) {
137                indices.insert(pos, j);
138                continue;
139            }
140        } else if indices.contains(&t) {
141            indices.push(j);
142            continue;
143        }
144        indices.push(t);
145    }
146    if !shuffle_while_inserting {
147        for i in (1..amount).rev() {
148            let j = uniform_u32_inclusive(rng, 0, i) as usize;
149            indices.swap(i as usize, j);
150        }
151    }
152    indices.into_iter().map(|i| i as usize).collect()
153}
154
155/// Partial Fisher-Yates over the whole index range.
156fn sample_in_place<R: Rng + ?Sized>(rng: &mut R, length: u32, amount: u32) -> Vec<usize> {
157    let mut indices: Vec<u32> = (0..length).collect();
158    for i in 0..amount {
159        let j = uniform_u32_inclusive(rng, i, length - 1);
160        indices.swap(i as usize, j as usize);
161    }
162    indices.truncate(amount as usize);
163    indices.into_iter().map(|i| i as usize).collect()
164}
165
166/// Draws `amount` distinct indices from `0..length` with probability
167/// proportional to weight, in descending order of the keys drawn.
168///
169/// Efraimidis and Spirakis, <https://doi.org/10.1016/j.ipl.2005.11.003>: give
170/// index `i` the key `u^(1/w_i)` for a uniform `u`, then take the largest keys.
171/// Passing `amount == length` therefore yields a weighted permutation of every
172/// index.
173///
174/// Panics if any weight is negative or not a number.
175pub fn sample_weighted<R, F>(rng: &mut R, length: usize, weight: F, amount: usize) -> Vec<usize>
176where
177    R: Rng + ?Sized,
178    F: Fn(usize) -> f64,
179{
180    let amount = amount.min(length);
181    // `rand` 0.8 pushes every key onto a `std` `BinaryHeap` and pops the
182    // largest, which decides the order of equal keys. The heap is written out
183    // here, following `std`'s push and pop, so that order stays fixed whatever
184    // `std` does later.
185    let mut heap: Vec<(f64, usize)> = Vec::with_capacity(length);
186    for i in 0..length {
187        let w = weight(i);
188        assert!(
189            w >= 0.0,
190            "weights must be non-negative and not NaN, got {w}"
191        );
192        heap.push((f64_unit(rng).powf(1.0 / w), i));
193        heap_sift_up(&mut heap, i);
194    }
195    let mut result = Vec::with_capacity(amount);
196    while result.len() < amount {
197        let last = heap.pop().expect("heap holds at least `amount` keys");
198        let largest = if heap.is_empty() {
199            last
200        } else {
201            let largest = std::mem::replace(&mut heap[0], last);
202            heap_sift_down_to_bottom(&mut heap);
203            largest
204        };
205        result.push(largest.1);
206    }
207    result
208}
209
210/// Moves the entry at `pos` towards the root of the max-heap until its parent's
211/// key is no smaller, and returns where it ends up.
212fn heap_sift_up(heap: &mut [(f64, usize)], mut pos: usize) -> usize {
213    let entry = heap[pos];
214    while pos > 0 {
215        let parent = (pos - 1) / 2;
216        if entry.0 <= heap[parent].0 {
217            break;
218        }
219        heap[pos] = heap[parent];
220        pos = parent;
221    }
222    heap[pos] = entry;
223    pos
224}
225
226/// Restores the max-heap after the root has been replaced: walks the root's
227/// entry down to a leaf along the larger children, then back up.
228fn heap_sift_down_to_bottom(heap: &mut [(f64, usize)]) {
229    let end = heap.len();
230    let entry = heap[0];
231    let mut pos = 0;
232    let mut child = 1;
233    while child + 1 < end {
234        // On equal keys the right child is taken, as `std` does.
235        if heap[child].0 <= heap[child + 1].0 {
236            child += 1;
237        }
238        heap[pos] = heap[child];
239        pos = child;
240        child = 2 * pos + 1;
241    }
242    if child + 1 == end {
243        heap[pos] = heap[child];
244        pos = child;
245    }
246    heap[pos] = entry;
247    heap_sift_up(heap, pos);
248}
249
250#[cfg(test)]
251mod tests {
252    use rand::{SeedableRng, rngs::ChaCha12Rng};
253
254    use super::*;
255
256    fn rng(seed: u8) -> ChaCha12Rng {
257        ChaCha12Rng::from_seed([seed; 32])
258    }
259
260    /// Vectors captured from `rand` 0.8, whose algorithms these routines
261    /// reproduce. They are the permutations consensus already agreed on, so a
262    /// change here is a change to the protocol, not a test to re-record.
263    #[test]
264    fn matches_captured_vectors() {
265        let mut v: Vec<u32> = (0..10).collect();
266        shuffle(&mut rng(0), &mut v);
267        assert_eq!(v, [6, 8, 3, 0, 1, 9, 2, 5, 7, 4]);
268
269        let mut v: Vec<u32> = (0..40).collect();
270        shuffle(&mut rng(7), &mut v);
271        assert_eq!(v[..8], [13, 36, 32, 11, 39, 30, 6, 24]);
272
273        let items: Vec<u32> = (0..10).collect();
274        assert_eq!(choose(&mut rng(0), &items), Some(&4));
275        assert_eq!(choose(&mut rng(1), &items), Some(&8));
276
277        // Under 12 indices draws Floyd's, over it draws partial Fisher-Yates,
278        // and 50 crosses Floyd's shuffle-as-you-go cutoff.
279        assert_eq!(
280            sample_indices(&mut rng(0), 10, 10),
281            [1, 8, 4, 9, 7, 6, 3, 0, 5, 2]
282        );
283        assert_eq!(
284            sample_indices(&mut rng(0), 20, 20),
285            [
286                8, 7, 3, 17, 5, 14, 11, 13, 2, 6, 0, 1, 18, 12, 4, 16, 19, 15, 10, 9
287            ]
288        );
289        assert_eq!(
290            sample_indices(&mut rng(3), 63, 63)[..8],
291            [32, 62, 58, 27, 54, 2, 56, 26]
292        );
293
294        let weights = [1u64, 5, 2, 8, 3];
295        assert_eq!(
296            sample_weighted(
297                &mut rng(0),
298                weights.len(),
299                |i| weights[i] as f64,
300                weights.len()
301            ),
302            [1, 4, 3, 0, 2]
303        );
304        assert_eq!(
305            sample_weighted(&mut rng(9), weights.len(), |i| weights[i] as f64, 2),
306            [1, 0]
307        );
308
309        // Zero weights all draw the key 0.0; `rand` 0.8 gave them this order,
310        // not index order.
311        let weights = [0u64, 3, 0, 0, 5, 0, 1, 0];
312        assert_eq!(
313            sample_weighted(
314                &mut rng(0),
315                weights.len(),
316                |i| weights[i] as f64,
317                weights.len()
318            ),
319            [4, 1, 6, 5, 2, 7, 0, 3]
320        );
321        assert_eq!(
322            sample_weighted(&mut rng(5), weights.len(), |i| weights[i] as f64, 6),
323            [1, 4, 6, 5, 2, 7]
324        );
325    }
326
327    #[test]
328    fn f64_unit_lies_in_the_unit_interval() {
329        let mut gen = rng(0);
330        for _ in 0..10_000 {
331            let x = f64_unit(&mut gen);
332            assert!((0.0..1.0).contains(&x), "{x} outside [0, 1)");
333        }
334        // The conversion `rand` performs, for as long as both exist.
335        assert_eq!(
336            f64_unit(&mut rng(4)),
337            rand::RngExt::random::<f64>(&mut rng(4))
338        );
339    }
340
341    #[test]
342    fn sampling_every_index_yields_a_permutation() {
343        for len in [1usize, 2, 11, 12, 13, 49, 50, 51, 162, 163, 200] {
344            for seed in 0..4 {
345                let mut got = sample_indices(&mut rng(seed), len, len);
346                got.sort_unstable();
347                assert_eq!(got, (0..len).collect::<Vec<_>>(), "len {len} seed {seed}");
348
349                let mut got = sample_weighted(&mut rng(seed), len, |i| (i % 7 + 1) as f64, len);
350                got.sort_unstable();
351                assert_eq!(got, (0..len).collect::<Vec<_>>(), "len {len} seed {seed}");
352            }
353        }
354    }
355
356    #[test]
357    fn zero_weight_sorts_last() {
358        let weights = [0u64, 4, 4, 4];
359        let order = sample_weighted(&mut rng(2), weights.len(), |i| weights[i] as f64, 4);
360        assert_eq!(order.last(), Some(&0));
361    }
362}