Skip to content

Commit 21ce9d8

Browse files
committed
random: Improve RandomMixin::randbits
The previous randbits code would, when requesting more randomness than available in its random bits buffer, discard the remaining entropy and generate new. Benchmarks show that it's usually better to first consume the existing randomness and only then generate new ones. This adds some complexity to randbits, but it doesn't weigh up against the reduced need to generate more randomness.
1 parent 9b14d3d commit 21ce9d8

File tree

3 files changed

+71
-18
lines changed

3 files changed

+71
-18
lines changed

src/random.h

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <crypto/common.h>
1111
#include <span.h>
1212
#include <uint256.h>
13+
#include <util/check.h>
1314

1415
#include <bit>
1516
#include <cassert>
@@ -165,7 +166,7 @@ template<typename T>
165166
class RandomMixin
166167
{
167168
private:
168-
uint64_t bitbuf;
169+
uint64_t bitbuf{0};
169170
int bitbuf_size{0};
170171

171172
/** Access the underlying generator.
@@ -175,12 +176,6 @@ class RandomMixin
175176
*/
176177
RandomNumberGenerator auto& Impl() noexcept { return static_cast<T&>(*this); }
177178

178-
void FillBitBuffer() noexcept
179-
{
180-
bitbuf = Impl().rand64();
181-
bitbuf_size = 64;
182-
}
183-
184179
public:
185180
RandomMixin() noexcept = default;
186181

@@ -190,31 +185,42 @@ class RandomMixin
190185

191186
RandomMixin(RandomMixin&& other) noexcept : bitbuf(other.bitbuf), bitbuf_size(other.bitbuf_size)
192187
{
188+
other.bitbuf = 0;
193189
other.bitbuf_size = 0;
194190
}
195191

196192
RandomMixin& operator=(RandomMixin&& other) noexcept
197193
{
198194
bitbuf = other.bitbuf;
199195
bitbuf_size = other.bitbuf_size;
196+
other.bitbuf = 0;
200197
other.bitbuf_size = 0;
201198
return *this;
202199
}
203200

204201
/** Generate a random (bits)-bit integer. */
205202
uint64_t randbits(int bits) noexcept
206203
{
207-
if (bits == 0) {
208-
return 0;
209-
} else if (bits > 32) {
210-
return Impl().rand64() >> (64 - bits);
211-
} else {
212-
if (bitbuf_size < bits) FillBitBuffer();
213-
uint64_t ret = bitbuf & (~uint64_t{0} >> (64 - bits));
204+
Assume(bits <= 64);
205+
// Requests for the full 64 bits are passed through.
206+
if (bits == 64) return Impl().rand64();
207+
uint64_t ret;
208+
if (bits <= bitbuf_size) {
209+
// If there is enough entropy left in bitbuf, return its bottom bits bits.
210+
ret = bitbuf;
214211
bitbuf >>= bits;
215212
bitbuf_size -= bits;
216-
return ret;
213+
} else {
214+
// If not, return all of bitbuf, supplemented with the (bits - bitbuf_size) bottom
215+
// bits of a newly generated 64-bit number on top. The remainder of that generated
216+
// number becomes the new bitbuf.
217+
uint64_t gen = Impl().rand64();
218+
ret = (gen << bitbuf_size) | bitbuf;
219+
bitbuf = gen >> (bits - bitbuf_size);
220+
bitbuf_size = 64 + bitbuf_size - bits;
217221
}
222+
// Return the bottom bits bits of ret.
223+
return ret & ((uint64_t{1} << bits) - 1);
218224
}
219225

220226
/** Generate a random integer in the range [0..range).

src/test/random_tests.cpp

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ BOOST_AUTO_TEST_CASE(fastrandom_tests)
3939
BOOST_CHECK_EQUAL(7, ctx.rand_uniform_delay(time_point, 9s).time_since_epoch().count());
4040
BOOST_CHECK_EQUAL(-6, ctx.rand_uniform_delay(time_point, -9s).time_since_epoch().count());
4141
BOOST_CHECK_EQUAL(1, ctx.rand_uniform_delay(time_point, 0s).time_since_epoch().count());
42-
BOOST_CHECK_EQUAL(1467825113502396065, ctx.rand_uniform_delay(time_point, 9223372036854775807s).time_since_epoch().count());
43-
BOOST_CHECK_EQUAL(-970181367944767837, ctx.rand_uniform_delay(time_point, -9223372036854775807s).time_since_epoch().count());
44-
BOOST_CHECK_EQUAL(24761, ctx.rand_uniform_delay(time_point, 9h).time_since_epoch().count());
42+
BOOST_CHECK_EQUAL(4652286523065884857, ctx.rand_uniform_delay(time_point, 9223372036854775807s).time_since_epoch().count());
43+
BOOST_CHECK_EQUAL(-8813961240025683129, ctx.rand_uniform_delay(time_point, -9223372036854775807s).time_since_epoch().count());
44+
BOOST_CHECK_EQUAL(26443, ctx.rand_uniform_delay(time_point, 9h).time_since_epoch().count());
4545
}
4646
BOOST_CHECK_EQUAL(ctx1.rand32(), ctx2.rand32());
4747
BOOST_CHECK_EQUAL(ctx1.rand32(), ctx2.rand32());
@@ -103,6 +103,52 @@ BOOST_AUTO_TEST_CASE(fastrandom_randbits)
103103
}
104104
}
105105

106+
/** Verify that RandomMixin::randbits returns 0 and 1 for every requested bit. */
107+
BOOST_AUTO_TEST_CASE(randbits_test)
108+
{
109+
FastRandomContext ctx_lens; //!< RNG for producing the lengths requested from ctx_test.
110+
FastRandomContext ctx_test; //!< The RNG being tested.
111+
int ctx_test_bitsleft{0}; //!< (Assumed value of) ctx_test::bitbuf_len
112+
113+
// Run the entire test 5 times.
114+
for (int i = 0; i < 5; ++i) {
115+
// count (first) how often it has occurred, and (second) how often it was true:
116+
// - for every bit position, in every requested bits count (0 + 1 + 2 + ... + 64 = 2080)
117+
// - for every value of ctx_test_bitsleft (0..63 = 64)
118+
std::vector<std::pair<uint64_t, uint64_t>> seen(2080 * 64);
119+
while (true) {
120+
// Loop 1000 times, just to not continuously check std::all_of.
121+
for (int j = 0; j < 1000; ++j) {
122+
// Decide on a number of bits to request (0 through 64, inclusive; don't use randbits/randrange).
123+
int bits = ctx_lens.rand64() % 65;
124+
// Generate that many bits.
125+
uint64_t gen = ctx_test.randbits(bits);
126+
// Make sure the result is in range.
127+
if (bits < 64) BOOST_CHECK_EQUAL(gen >> bits, 0);
128+
// Mark all the seen bits in the output.
129+
for (int bit = 0; bit < bits; ++bit) {
130+
int idx = bit + (bits * (bits - 1)) / 2 + 2080 * ctx_test_bitsleft;
131+
seen[idx].first += 1;
132+
seen[idx].second += (gen >> bit) & 1;
133+
}
134+
// Update ctx_test_bitself.
135+
if (bits > ctx_test_bitsleft) {
136+
ctx_test_bitsleft = ctx_test_bitsleft + 64 - bits;
137+
} else {
138+
ctx_test_bitsleft -= bits;
139+
}
140+
}
141+
// Loop until every bit position/combination is seen 242 times.
142+
if (std::all_of(seen.begin(), seen.end(), [](const auto& x) { return x.first >= 242; })) break;
143+
}
144+
// Check that each bit appears within 7.78 standard deviations of 50%
145+
// (each will fail with P < 1/(2080 * 64 * 10^9)).
146+
for (const auto& val : seen) {
147+
assert(fabs(val.first * 0.5 - val.second) < sqrt(val.first * 0.25) * 7.78);
148+
}
149+
}
150+
}
151+
106152
/** Does-it-compile test for compatibility with standard library RNG interface. */
107153
BOOST_AUTO_TEST_CASE(stdrandom_test)
108154
{

test/sanitizer_suppressions/ubsan

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,4 @@ shift-base:crypto/
7676
shift-base:streams.h
7777
shift-base:FormatHDKeypath
7878
shift-base:xoroshiro128plusplus.h
79+
shift-base:RandomMixin<*>::randbits

0 commit comments

Comments
 (0)