Skip to content

Commit dca28fb

Browse files
authored
Make random_bits_core platform independent (#781)
In its current state, `random_bits_core` behaves differently on 32-bit and 64-bit targets in two ways: 1. The platforms order the randomly sampled bytes in a different order, generating different outputs. 2. For bit_length $\mod 64 \in [1, 32]$, the 64-bit platform samples four random bytes more than the 32-bit platform. a. this means the state of the RNG after calling this function is platform-dependent. This makes it impossible to use this function as an rng-seed-to-integer map. This patch introduces: - benchmarks to gauge the performance of the `random_bits_core` function, - a patch to the `random_bits_core` function, and - a test ensuring the `random_bits_core` output and rng state after calling it are platform-independent.
1 parent 22cfab7 commit dca28fb

File tree

2 files changed

+112
-5
lines changed

2 files changed

+112
-5
lines changed

benches/uint.rs

+22
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,27 @@ fn bench_random(c: &mut Criterion) {
121121
});
122122
}
123123

124+
fn bench_random_bits(c: &mut Criterion) {
125+
let mut group = c.benchmark_group("random_bits");
126+
127+
let mut rng = make_rng();
128+
group.bench_function("random_bits, U256, full", |b| {
129+
b.iter(|| black_box(U256::random_bits(&mut rng, U256::BITS)));
130+
});
131+
132+
group.bench_function("random_bits, U256, bounded", |b| {
133+
b.iter(|| black_box(U256::random_bits(&mut rng, 219)));
134+
});
135+
136+
group.bench_function("random_bits, U2048, full", |b| {
137+
b.iter(|| black_box(U2048::random_bits(&mut rng, U2048::BITS)));
138+
});
139+
140+
group.bench_function("random_bits, U2048, bounded", |b| {
141+
b.iter(|| black_box(U2048::random_bits(&mut rng, 1947)));
142+
});
143+
}
144+
124145
fn bench_mul(c: &mut Criterion) {
125146
let mut group = c.benchmark_group("wrapping ops");
126147

@@ -493,6 +514,7 @@ fn bench_sqrt(c: &mut Criterion) {
493514
criterion_group!(
494515
benches,
495516
bench_random,
517+
bench_random_bits,
496518
bench_mul,
497519
bench_division,
498520
bench_gcd,

src/uint/rand.rs

+90-5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ impl<const LIMBS: usize> Random for Uint<LIMBS> {
2020
/// Fill the given limbs slice with random bits.
2121
///
2222
/// NOTE: Assumes that the limbs in the given slice are zeroed!
23+
///
24+
/// When combined with a platform-independent "4-byte sequential" `rng`, this function is
25+
/// platform-independent. We consider an RNG "`X`-byte sequential" whenever
26+
/// `rng.fill_bytes(&mut bytes[..i]); rng.fill_bytes(&mut bytes[i..])` constructs the same `bytes`,
27+
/// as long as `i` is a multiple of `X`.
28+
/// Note that the `TryRngCore` trait does _not_ require this behaviour from `rng`.
2329
pub(crate) fn random_bits_core<R: TryRngCore + ?Sized>(
2430
rng: &mut R,
2531
zeroed_limbs: &mut [Limb],
@@ -39,12 +45,26 @@ pub(crate) fn random_bits_core<R: TryRngCore + ?Sized>(
3945
for i in 0..nonzero_limbs - 1 {
4046
rng.try_fill_bytes(&mut buffer)
4147
.map_err(RandomBitsError::RandCore)?;
42-
zeroed_limbs[i] = Limb(Word::from_be_bytes(buffer));
48+
zeroed_limbs[i] = Limb(Word::from_le_bytes(buffer));
4349
}
4450

45-
rng.try_fill_bytes(&mut buffer)
51+
// This algorithm should sample the same number of random bytes, regardless of the pointer width
52+
// of the target platform. To this end, special attention has to be paid to the case where
53+
// bit_length - 1 < 32 mod 64. Bit strings of that size can be represented using `2X+1` 32-bit
54+
// words or `X+1` 64-bit words. Note that 64*(X+1) - 32*(2X+1) = 32. Hence, if we sample full
55+
// words only, a 64-bit platform will sample 32 bits more than a 32-bit platform. We prevent
56+
// this by forcing both platforms to only sample 4 bytes for the last word in this case.
57+
let slice = if partial_limb > 0 && partial_limb <= 32 {
58+
// Note: we do not have to zeroize the second half of the buffer, as the mask will take
59+
// care of this in the end.
60+
&mut buffer[0..4]
61+
} else {
62+
buffer.as_mut_slice()
63+
};
64+
65+
rng.try_fill_bytes(slice)
4666
.map_err(RandomBitsError::RandCore)?;
47-
zeroed_limbs[nonzero_limbs - 1] = Limb(Word::from_be_bytes(buffer) & mask);
67+
zeroed_limbs[nonzero_limbs - 1] = Limb(Word::from_le_bytes(buffer) & mask);
4868

4969
Ok(())
5070
}
@@ -144,9 +164,31 @@ where
144164

145165
#[cfg(test)]
146166
mod tests {
147-
use crate::{Limb, NonZero, RandomBits, RandomMod, U256};
167+
use crate::uint::rand::random_bits_core;
168+
use crate::{Limb, NonZero, Random, RandomBits, RandomMod, U256, U1024, Uint};
148169
use rand_chacha::ChaCha8Rng;
149-
use rand_core::SeedableRng;
170+
use rand_core::{RngCore, SeedableRng};
171+
172+
const RANDOM_OUTPUT: U1024 = Uint::from_be_hex(concat![
173+
"A484C4C693EECC47C3B919AE0D16DF2259CD1A8A9B8EA8E0862878227D4B40A3",
174+
"C54302F2EB1E2F69E17653A37F1BCC44277FA208E6B31E08CDC4A23A7E88E660",
175+
"EF781C7DD2D368BAD438539D6A2E923C8CAE14CB947EB0BDE10D666732024679",
176+
"0F6760A48F9B887CB2FB0D3281E2A6E67746A55FBAD8C037B585F767A79A3B6C"
177+
]);
178+
179+
/// Construct a 4-sequential `rng`, i.e., an `rng` such that
180+
/// `rng.fill_bytes(&mut buffer[..x]); rng.fill_bytes(&mut buffer[x..])` will construct the
181+
/// same `buffer`, for `x` any in `0..buffer.len()` that is `0 mod 4`.
182+
fn get_four_sequential_rng() -> ChaCha8Rng {
183+
ChaCha8Rng::seed_from_u64(0)
184+
}
185+
186+
/// Make sure the random value constructed is consistent across platforms
187+
#[test]
188+
fn random_platform_independence() {
189+
let mut rng = get_four_sequential_rng();
190+
assert_eq!(U1024::random(&mut rng), RANDOM_OUTPUT);
191+
}
150192

151193
#[test]
152194
fn random_mod() {
@@ -219,4 +261,47 @@ mod tests {
219261
assert_eq!(res, U256::ZERO);
220262
}
221263
}
264+
265+
/// Make sure the random_bits output is consistent across platforms
266+
#[test]
267+
fn random_bits_platform_independence() {
268+
let mut rng = get_four_sequential_rng();
269+
270+
let bit_length = 989;
271+
let mut val = U1024::ZERO;
272+
random_bits_core(&mut rng, val.as_limbs_mut(), bit_length).expect("safe");
273+
274+
assert_eq!(
275+
val,
276+
RANDOM_OUTPUT.bitand(&U1024::ONE.shl(bit_length).wrapping_sub(&Uint::ONE))
277+
);
278+
279+
// Test that the RNG is in the same state
280+
let mut state = [0u8; 16];
281+
rng.fill_bytes(&mut state);
282+
283+
assert_eq!(
284+
state,
285+
[
286+
198, 196, 132, 164, 240, 211, 223, 12, 36, 189, 139, 48, 94, 1, 123, 253
287+
]
288+
);
289+
}
290+
291+
/// Test that random bytes are sampled consecutively.
292+
#[test]
293+
fn random_bits_4_bytes_sequential() {
294+
// Test for multiples of 4 bytes, i.e., multiples of 32 bits.
295+
let bit_lengths = [0, 32, 64, 128, 192, 992];
296+
297+
for bit_length in bit_lengths {
298+
let mut rng = get_four_sequential_rng();
299+
let mut first = U1024::ZERO;
300+
let mut second = U1024::ZERO;
301+
random_bits_core(&mut rng, first.as_limbs_mut(), bit_length).expect("safe");
302+
random_bits_core(&mut rng, second.as_limbs_mut(), U1024::BITS - bit_length)
303+
.expect("safe");
304+
assert_eq!(second.shl(bit_length).bitor(&first), RANDOM_OUTPUT);
305+
}
306+
}
222307
}

0 commit comments

Comments
 (0)