Skip to content

Commit 9b14d3d

Browse files
committed
random: refactor: move rand* utilities to RandomMixin
Rather than make all the useful types of randomness be exclusive to FastRandomContext, move it to a separate RandomMixin class where it can be reused by other RNGs. A Curiously Recurring Template Pattern (CRTP) is used for this, to provide the ability for individual RNG classes to override one or more randomness functions, without needing the runtime-cost of virtual classes. Specifically, RNGs are expected to only provide fillrand and rand64, while all others are derived from those: - randbits - randrange - randbytes - rand32 - rand256 - randbool - rand_uniform_delay - rand_uniform_duration - min(), max(), operator()(), to comply with C++ URBG concept.
1 parent 40dd86f commit 9b14d3d

File tree

2 files changed

+102
-46
lines changed

2 files changed

+102
-46
lines changed

src/random.cpp

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ void FastRandomContext::fillrand(Span<std::byte> output) noexcept
665665
rng.Keystream(output);
666666
}
667667

668-
FastRandomContext::FastRandomContext(const uint256& seed) noexcept : requires_seed(false), rng(MakeByteSpan(seed)), bitbuf_size(0) {}
668+
FastRandomContext::FastRandomContext(const uint256& seed) noexcept : requires_seed(false), rng(MakeByteSpan(seed)) {}
669669

670670
bool Random_SanityCheck()
671671
{
@@ -715,7 +715,7 @@ bool Random_SanityCheck()
715715

716716
static constexpr std::array<std::byte, ChaCha20::KEYLEN> ZERO_KEY{};
717717

718-
FastRandomContext::FastRandomContext(bool fDeterministic) noexcept : requires_seed(!fDeterministic), rng(ZERO_KEY), bitbuf_size(0)
718+
FastRandomContext::FastRandomContext(bool fDeterministic) noexcept : requires_seed(!fDeterministic), rng(ZERO_KEY)
719719
{
720720
// Note that despite always initializing with ZERO_KEY, requires_seed is set to true if not
721721
// fDeterministic. That means the rng will be reinitialized with a secure random key upon first
@@ -726,10 +726,8 @@ FastRandomContext& FastRandomContext::operator=(FastRandomContext&& from) noexce
726726
{
727727
requires_seed = from.requires_seed;
728728
rng = from.rng;
729-
bitbuf = from.bitbuf;
730-
bitbuf_size = from.bitbuf_size;
731729
from.requires_seed = true;
732-
from.bitbuf_size = 0;
730+
static_cast<RandomMixin<FastRandomContext>&>(*this) = std::move(from);
733731
return *this;
734732
}
735733

src/random.h

Lines changed: 99 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
#include <bit>
1515
#include <cassert>
1616
#include <chrono>
17+
#include <concepts>
1718
#include <cstdint>
1819
#include <limits>
20+
#include <type_traits>
1921
#include <vector>
2022

2123
/**
@@ -135,50 +137,68 @@ void RandAddPeriodic() noexcept;
135137
*/
136138
void RandAddEvent(const uint32_t event_info) noexcept;
137139

138-
/**
139-
* Fast randomness source. This is seeded once with secure random data, but
140-
* is completely deterministic and does not gather more entropy after that.
140+
// Forward declaration of RandomMixin, used in RandomNumberGenerator concept.
141+
template<typename T>
142+
class RandomMixin;
143+
144+
/** A concept for RandomMixin-based random number generators. */
145+
template<typename T>
146+
concept RandomNumberGenerator = requires(T& rng, Span<std::byte> s) {
147+
// A random number generator must provide rand64().
148+
{ rng.rand64() } noexcept -> std::same_as<uint64_t>;
149+
// A random number generator must provide randfill(Span<std::byte>).
150+
{ rng.fillrand(s) } noexcept;
151+
// A random number generator must derive from RandomMixin, which adds other rand* functions.
152+
requires std::derived_from<std::remove_reference_t<T>, RandomMixin<std::remove_reference_t<T>>>;
153+
};
154+
155+
/** Mixin class that provides helper randomness functions.
141156
*
142-
* This class is not thread-safe.
157+
* Intended to be used through CRTP: https://en.cppreference.com/w/cpp/language/crtp.
158+
* An RNG class FunkyRNG would derive publicly from RandomMixin<FunkyRNG>. This permits
159+
* RandomMixin from accessing the derived class's rand64() function, while also allowing
160+
* the derived class to provide more.
161+
*
162+
* The derived class must satisfy the RandomNumberGenerator concept.
143163
*/
144-
class FastRandomContext
164+
template<typename T>
165+
class RandomMixin
145166
{
146167
private:
147-
bool requires_seed;
148-
ChaCha20 rng;
149-
150168
uint64_t bitbuf;
151-
int bitbuf_size;
169+
int bitbuf_size{0};
152170

153-
void RandomSeed() noexcept;
171+
/** Access the underlying generator.
172+
*
173+
* This also enforces the RandomNumberGenerator concept. We cannot declare that in the template
174+
* (no template<RandomNumberGenerator T>) because the type isn't fully instantiated yet there.
175+
*/
176+
RandomNumberGenerator auto& Impl() noexcept { return static_cast<T&>(*this); }
154177

155178
void FillBitBuffer() noexcept
156179
{
157-
bitbuf = rand64();
180+
bitbuf = Impl().rand64();
158181
bitbuf_size = 64;
159182
}
160183

161184
public:
162-
explicit FastRandomContext(bool fDeterministic = false) noexcept;
185+
RandomMixin() noexcept = default;
163186

164-
/** Initialize with explicit seed (only for testing) */
165-
explicit FastRandomContext(const uint256& seed) noexcept;
187+
// Do not permit copying an RNG.
188+
RandomMixin(const RandomMixin&) = delete;
189+
RandomMixin& operator=(const RandomMixin&) = delete;
166190

167-
// Do not permit copying a FastRandomContext (move it, or create a new one to get reseeded).
168-
FastRandomContext(const FastRandomContext&) = delete;
169-
FastRandomContext(FastRandomContext&&) = delete;
170-
FastRandomContext& operator=(const FastRandomContext&) = delete;
171-
172-
/** Move a FastRandomContext. If the original one is used again, it will be reseeded. */
173-
FastRandomContext& operator=(FastRandomContext&& from) noexcept;
191+
RandomMixin(RandomMixin&& other) noexcept : bitbuf(other.bitbuf), bitbuf_size(other.bitbuf_size)
192+
{
193+
other.bitbuf_size = 0;
194+
}
174195

175-
/** Generate a random 64-bit integer. */
176-
uint64_t rand64() noexcept
196+
RandomMixin& operator=(RandomMixin&& other) noexcept
177197
{
178-
if (requires_seed) RandomSeed();
179-
std::array<std::byte, 8> buf;
180-
rng.Keystream(buf);
181-
return ReadLE64(UCharCast(buf.data()));
198+
bitbuf = other.bitbuf;
199+
bitbuf_size = other.bitbuf_size;
200+
other.bitbuf_size = 0;
201+
return *this;
182202
}
183203

184204
/** Generate a random (bits)-bit integer. */
@@ -187,7 +207,7 @@ class FastRandomContext
187207
if (bits == 0) {
188208
return 0;
189209
} else if (bits > 32) {
190-
return rand64() >> (64 - bits);
210+
return Impl().rand64() >> (64 - bits);
191211
} else {
192212
if (bitbuf_size < bits) FillBitBuffer();
193213
uint64_t ret = bitbuf & (~uint64_t{0} >> (64 - bits));
@@ -206,7 +226,7 @@ class FastRandomContext
206226
--range;
207227
int bits = std::bit_width(range);
208228
while (true) {
209-
uint64_t ret = randbits(bits);
229+
uint64_t ret = Impl().randbits(bits);
210230
if (ret <= range) return ret;
211231
}
212232
}
@@ -216,49 +236,87 @@ class FastRandomContext
216236
std::vector<B> randbytes(size_t len) noexcept
217237
{
218238
std::vector<B> ret(len);
219-
fillrand(MakeWritableByteSpan(ret));
239+
Impl().fillrand(MakeWritableByteSpan(ret));
220240
return ret;
221241
}
222242

223-
/** Fill a byte Span with random bytes. */
224-
void fillrand(Span<std::byte> output) noexcept;
225-
226243
/** Generate a random 32-bit integer. */
227-
uint32_t rand32() noexcept { return randbits(32); }
244+
uint32_t rand32() noexcept { return Impl().randbits(32); }
228245

229246
/** generate a random uint256. */
230247
uint256 rand256() noexcept
231248
{
232249
uint256 ret;
233-
fillrand(MakeWritableByteSpan(ret));
250+
Impl().fillrand(MakeWritableByteSpan(ret));
234251
return ret;
235252
}
236253

237254
/** Generate a random boolean. */
238-
bool randbool() noexcept { return randbits(1); }
255+
bool randbool() noexcept { return Impl().randbits(1); }
239256

240257
/** Return the time point advanced by a uniform random duration. */
241258
template <typename Tp>
242259
Tp rand_uniform_delay(const Tp& time, typename Tp::duration range) noexcept
243260
{
244-
return time + rand_uniform_duration<Tp>(range);
261+
return time + Impl().template rand_uniform_duration<Tp>(range);
245262
}
246263

247264
/** Generate a uniform random duration in the range from 0 (inclusive) to range (exclusive). */
248265
template <typename Chrono>
249266
typename Chrono::duration rand_uniform_duration(typename Chrono::duration range) noexcept
250267
{
251268
using Dur = typename Chrono::duration;
252-
return range.count() > 0 ? /* interval [0..range) */ Dur{randrange(range.count())} :
253-
range.count() < 0 ? /* interval (range..0] */ -Dur{randrange(-range.count())} :
269+
return range.count() > 0 ? /* interval [0..range) */ Dur{Impl().randrange(range.count())} :
270+
range.count() < 0 ? /* interval (range..0] */ -Dur{Impl().randrange(-range.count())} :
254271
/* interval [0..0] */ Dur{0};
255272
};
256273

257274
// Compatibility with the UniformRandomBitGenerator concept
258275
typedef uint64_t result_type;
259276
static constexpr uint64_t min() noexcept { return 0; }
260277
static constexpr uint64_t max() noexcept { return std::numeric_limits<uint64_t>::max(); }
261-
inline uint64_t operator()() noexcept { return rand64(); }
278+
inline uint64_t operator()() noexcept { return Impl().rand64(); }
279+
};
280+
281+
/**
282+
* Fast randomness source. This is seeded once with secure random data, but
283+
* is completely deterministic and does not gather more entropy after that.
284+
*
285+
* This class is not thread-safe.
286+
*/
287+
class FastRandomContext : public RandomMixin<FastRandomContext>
288+
{
289+
private:
290+
bool requires_seed;
291+
ChaCha20 rng;
292+
293+
void RandomSeed() noexcept;
294+
295+
public:
296+
explicit FastRandomContext(bool fDeterministic = false) noexcept;
297+
298+
/** Initialize with explicit seed (only for testing) */
299+
explicit FastRandomContext(const uint256& seed) noexcept;
300+
301+
// Do not permit copying a FastRandomContext (move it, or create a new one to get reseeded).
302+
FastRandomContext(const FastRandomContext&) = delete;
303+
FastRandomContext(FastRandomContext&&) = delete;
304+
FastRandomContext& operator=(const FastRandomContext&) = delete;
305+
306+
/** Move a FastRandomContext. If the original one is used again, it will be reseeded. */
307+
FastRandomContext& operator=(FastRandomContext&& from) noexcept;
308+
309+
/** Generate a random 64-bit integer. */
310+
uint64_t rand64() noexcept
311+
{
312+
if (requires_seed) RandomSeed();
313+
std::array<std::byte, 8> buf;
314+
rng.Keystream(buf);
315+
return ReadLE64(UCharCast(buf.data()));
316+
}
317+
318+
/** Fill a byte Span with random bytes. */
319+
void fillrand(Span<std::byte> output) noexcept;
262320
};
263321

264322
/** More efficient than using std::shuffle on a FastRandomContext.
@@ -271,7 +329,7 @@ class FastRandomContext
271329
* debug mode detects and panics on. This is a known issue, see
272330
* https://stackoverflow.com/questions/22915325/avoiding-self-assignment-in-stdshuffle
273331
*/
274-
template <typename I, typename R>
332+
template <typename I, RandomNumberGenerator R>
275333
void Shuffle(I first, I last, R&& rng)
276334
{
277335
while (first != last) {

0 commit comments

Comments
 (0)