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.
pull/29625/head
Pieter Wuille 8 months ago
parent 40dd86fc3b
commit 9b14d3d2da

@ -665,7 +665,7 @@ void FastRandomContext::fillrand(Span<std::byte> output) noexcept
rng.Keystream(output);
}
FastRandomContext::FastRandomContext(const uint256& seed) noexcept : requires_seed(false), rng(MakeByteSpan(seed)), bitbuf_size(0) {}
FastRandomContext::FastRandomContext(const uint256& seed) noexcept : requires_seed(false), rng(MakeByteSpan(seed)) {}
bool Random_SanityCheck()
{
@ -715,7 +715,7 @@ bool Random_SanityCheck()
static constexpr std::array<std::byte, ChaCha20::KEYLEN> ZERO_KEY{};
FastRandomContext::FastRandomContext(bool fDeterministic) noexcept : requires_seed(!fDeterministic), rng(ZERO_KEY), bitbuf_size(0)
FastRandomContext::FastRandomContext(bool fDeterministic) noexcept : requires_seed(!fDeterministic), rng(ZERO_KEY)
{
// Note that despite always initializing with ZERO_KEY, requires_seed is set to true if not
// 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
{
requires_seed = from.requires_seed;
rng = from.rng;
bitbuf = from.bitbuf;
bitbuf_size = from.bitbuf_size;
from.requires_seed = true;
from.bitbuf_size = 0;
static_cast<RandomMixin<FastRandomContext>&>(*this) = std::move(from);
return *this;
}

@ -14,8 +14,10 @@
#include <bit>
#include <cassert>
#include <chrono>
#include <concepts>
#include <cstdint>
#include <limits>
#include <type_traits>
#include <vector>
/**
@ -135,50 +137,68 @@ void RandAddPeriodic() noexcept;
*/
void RandAddEvent(const uint32_t event_info) noexcept;
/**
* Fast randomness source. This is seeded once with secure random data, but
* is completely deterministic and does not gather more entropy after that.
// Forward declaration of RandomMixin, used in RandomNumberGenerator concept.
template<typename T>
class RandomMixin;
/** A concept for RandomMixin-based random number generators. */
template<typename T>
concept RandomNumberGenerator = requires(T& rng, Span<std::byte> s) {
// A random number generator must provide rand64().
{ rng.rand64() } noexcept -> std::same_as<uint64_t>;
// A random number generator must provide randfill(Span<std::byte>).
{ rng.fillrand(s) } noexcept;
// A random number generator must derive from RandomMixin, which adds other rand* functions.
requires std::derived_from<std::remove_reference_t<T>, RandomMixin<std::remove_reference_t<T>>>;
};
/** Mixin class that provides helper randomness functions.
*
* This class is not thread-safe.
* Intended to be used through CRTP: https://en.cppreference.com/w/cpp/language/crtp.
* An RNG class FunkyRNG would derive publicly from RandomMixin<FunkyRNG>. This permits
* RandomMixin from accessing the derived class's rand64() function, while also allowing
* the derived class to provide more.
*
* The derived class must satisfy the RandomNumberGenerator concept.
*/
class FastRandomContext
template<typename T>
class RandomMixin
{
private:
bool requires_seed;
ChaCha20 rng;
uint64_t bitbuf;
int bitbuf_size;
int bitbuf_size{0};
void RandomSeed() noexcept;
/** Access the underlying generator.
*
* This also enforces the RandomNumberGenerator concept. We cannot declare that in the template
* (no template<RandomNumberGenerator T>) because the type isn't fully instantiated yet there.
*/
RandomNumberGenerator auto& Impl() noexcept { return static_cast<T&>(*this); }
void FillBitBuffer() noexcept
{
bitbuf = rand64();
bitbuf = Impl().rand64();
bitbuf_size = 64;
}
public:
explicit FastRandomContext(bool fDeterministic = false) noexcept;
RandomMixin() noexcept = default;
/** Initialize with explicit seed (only for testing) */
explicit FastRandomContext(const uint256& seed) noexcept;
// Do not permit copying an RNG.
RandomMixin(const RandomMixin&) = delete;
RandomMixin& operator=(const RandomMixin&) = delete;
// Do not permit copying a FastRandomContext (move it, or create a new one to get reseeded).
FastRandomContext(const FastRandomContext&) = delete;
FastRandomContext(FastRandomContext&&) = delete;
FastRandomContext& operator=(const FastRandomContext&) = delete;
/** Move a FastRandomContext. If the original one is used again, it will be reseeded. */
FastRandomContext& operator=(FastRandomContext&& from) noexcept;
RandomMixin(RandomMixin&& other) noexcept : bitbuf(other.bitbuf), bitbuf_size(other.bitbuf_size)
{
other.bitbuf_size = 0;
}
/** Generate a random 64-bit integer. */
uint64_t rand64() noexcept
RandomMixin& operator=(RandomMixin&& other) noexcept
{
if (requires_seed) RandomSeed();
std::array<std::byte, 8> buf;
rng.Keystream(buf);
return ReadLE64(UCharCast(buf.data()));
bitbuf = other.bitbuf;
bitbuf_size = other.bitbuf_size;
other.bitbuf_size = 0;
return *this;
}
/** Generate a random (bits)-bit integer. */
@ -187,7 +207,7 @@ public:
if (bits == 0) {
return 0;
} else if (bits > 32) {
return rand64() >> (64 - bits);
return Impl().rand64() >> (64 - bits);
} else {
if (bitbuf_size < bits) FillBitBuffer();
uint64_t ret = bitbuf & (~uint64_t{0} >> (64 - bits));
@ -206,7 +226,7 @@ public:
--range;
int bits = std::bit_width(range);
while (true) {
uint64_t ret = randbits(bits);
uint64_t ret = Impl().randbits(bits);
if (ret <= range) return ret;
}
}
@ -216,32 +236,29 @@ public:
std::vector<B> randbytes(size_t len) noexcept
{
std::vector<B> ret(len);
fillrand(MakeWritableByteSpan(ret));
Impl().fillrand(MakeWritableByteSpan(ret));
return ret;
}
/** Fill a byte Span with random bytes. */
void fillrand(Span<std::byte> output) noexcept;
/** Generate a random 32-bit integer. */
uint32_t rand32() noexcept { return randbits(32); }
uint32_t rand32() noexcept { return Impl().randbits(32); }
/** generate a random uint256. */
uint256 rand256() noexcept
{
uint256 ret;
fillrand(MakeWritableByteSpan(ret));
Impl().fillrand(MakeWritableByteSpan(ret));
return ret;
}
/** Generate a random boolean. */
bool randbool() noexcept { return randbits(1); }
bool randbool() noexcept { return Impl().randbits(1); }
/** Return the time point advanced by a uniform random duration. */
template <typename Tp>
Tp rand_uniform_delay(const Tp& time, typename Tp::duration range) noexcept
{
return time + rand_uniform_duration<Tp>(range);
return time + Impl().template rand_uniform_duration<Tp>(range);
}
/** Generate a uniform random duration in the range from 0 (inclusive) to range (exclusive). */
@ -249,8 +266,8 @@ public:
typename Chrono::duration rand_uniform_duration(typename Chrono::duration range) noexcept
{
using Dur = typename Chrono::duration;
return range.count() > 0 ? /* interval [0..range) */ Dur{randrange(range.count())} :
range.count() < 0 ? /* interval (range..0] */ -Dur{randrange(-range.count())} :
return range.count() > 0 ? /* interval [0..range) */ Dur{Impl().randrange(range.count())} :
range.count() < 0 ? /* interval (range..0] */ -Dur{Impl().randrange(-range.count())} :
/* interval [0..0] */ Dur{0};
};
@ -258,7 +275,48 @@ public:
typedef uint64_t result_type;
static constexpr uint64_t min() noexcept { return 0; }
static constexpr uint64_t max() noexcept { return std::numeric_limits<uint64_t>::max(); }
inline uint64_t operator()() noexcept { return rand64(); }
inline uint64_t operator()() noexcept { return Impl().rand64(); }
};
/**
* Fast randomness source. This is seeded once with secure random data, but
* is completely deterministic and does not gather more entropy after that.
*
* This class is not thread-safe.
*/
class FastRandomContext : public RandomMixin<FastRandomContext>
{
private:
bool requires_seed;
ChaCha20 rng;
void RandomSeed() noexcept;
public:
explicit FastRandomContext(bool fDeterministic = false) noexcept;
/** Initialize with explicit seed (only for testing) */
explicit FastRandomContext(const uint256& seed) noexcept;
// Do not permit copying a FastRandomContext (move it, or create a new one to get reseeded).
FastRandomContext(const FastRandomContext&) = delete;
FastRandomContext(FastRandomContext&&) = delete;
FastRandomContext& operator=(const FastRandomContext&) = delete;
/** Move a FastRandomContext. If the original one is used again, it will be reseeded. */
FastRandomContext& operator=(FastRandomContext&& from) noexcept;
/** Generate a random 64-bit integer. */
uint64_t rand64() noexcept
{
if (requires_seed) RandomSeed();
std::array<std::byte, 8> buf;
rng.Keystream(buf);
return ReadLE64(UCharCast(buf.data()));
}
/** Fill a byte Span with random bytes. */
void fillrand(Span<std::byte> output) noexcept;
};
/** More efficient than using std::shuffle on a FastRandomContext.
@ -271,7 +329,7 @@ public:
* debug mode detects and panics on. This is a known issue, see
* https://stackoverflow.com/questions/22915325/avoiding-self-assignment-in-stdshuffle
*/
template <typename I, typename R>
template <typename I, RandomNumberGenerator R>
void Shuffle(I first, I last, R&& rng)
{
while (first != last) {

Loading…
Cancel
Save