Merge remote-tracking branch 'origin/multiple-application-deterministic-tests' into dsf-portable-rng

This commit is contained in:
Nicholas Dudfield
2026-09-23 16:31:38 +07:00
2 changed files with 369 additions and 14 deletions

View File

@@ -22,6 +22,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstring>
@@ -50,30 +51,85 @@ namespace detail {
template <class Engine, class Result = typename Engine::result_type>
using is_engine = std::is_invocable_r<Result, Engine>;
// 64 bits from the engine, independent of how the standard library maps
// engine output onto an integer range.
// 64 bits from the engine. Width comes from max()-min(), not from the
// storage type: a 32-bit engine may use a 64-bit result_type.
template <class Engine>
std::uint64_t
randomU64(Engine& engine)
{
using Result = typename Engine::result_type;
constexpr int digits = std::numeric_limits<Result>::digits;
static_assert(std::is_unsigned_v<typename Engine::result_type>);
static_assert(
std::numeric_limits<typename Engine::result_type>::digits <= 64);
static_assert(Engine::min() < Engine::max());
auto const draw = [&engine]() -> std::uint64_t {
return static_cast<std::uint64_t>(engine() - Engine::min());
};
if constexpr (digits >= 64)
return draw();
std::uint64_t value = 0;
int filled = 0;
while (filled < 64)
// A wrapped cardinality of 0 means 2^64 values: one full-range draw.
constexpr auto span =
static_cast<std::uint64_t>(Engine::max() - Engine::min());
constexpr std::uint64_t range = span + 1u;
constexpr bool full = range == 0;
constexpr bool powerOfTwo = full || (range & (range - 1u)) == 0;
if constexpr (powerOfTwo)
{
auto const take = digits < (64 - filled) ? digits : (64 - filled);
auto const mask = (std::uint64_t{1} << take) - 1;
value |= (draw() & mask) << filled;
filled += take;
constexpr int width = full ? 64 : std::bit_width(range) - 1;
if constexpr (width >= 64)
return draw();
std::uint64_t value = 0;
int filled = 0;
while (filled < 64)
{
auto const take = width < (64 - filled) ? width : (64 - filled);
auto const mask = (std::uint64_t{1} << take) - 1;
value |= (draw() & mask) << filled;
filled += take;
}
return value;
}
else
{
// [rand.adapt.ibits] for w = 64. R is not a power of two.
constexpr int m = std::bit_width(range) - 1;
constexpr int nCeil = (64 + m - 1) / m;
constexpr int w0Try = 64 / nCeil;
constexpr auto y0Try = (std::uint64_t{1} << w0Try) * (range >> w0Try);
constexpr bool bump =
(range - y0Try) > (y0Try / static_cast<std::uint64_t>(nCeil));
constexpr int n = bump ? nCeil + 1 : nCeil;
constexpr int w0 = 64 / n;
constexpr int n0 = n - (64 % n);
constexpr auto y0 = (std::uint64_t{1} << w0) * (range >> w0);
constexpr auto y1 =
(std::uint64_t{1} << (w0 + 1)) * (range >> (w0 + 1));
// R == 3 gives n == 65 and w0 == 0. The first group still
// consumes its draw even though it contributes no output bits.
static_assert(w0 >= 0 && w0 < 63);
std::uint64_t word = 0;
for (int k = 0; k != n0; ++k)
{
std::uint64_t u;
do
{
u = draw();
} while (u >= y0);
word = (word << w0) + (u & ((std::uint64_t{1} << w0) - 1));
}
for (int k = n0; k != n; ++k)
{
std::uint64_t u;
do
{
u = draw();
} while (u >= y1);
constexpr int bits = w0 + 1;
word = (word << bits) + (u & ((std::uint64_t{1} << bits) - 1));
}
return word;
}
return value;
}
} // namespace detail

View File

@@ -0,0 +1,299 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpl/basics/random.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <array>
#include <cstdint>
#include <limits>
#include <random>
namespace ripple {
namespace test {
namespace {
// 32-bit output stored in a 32-bit result.
struct Bits32
{
using result_type = std::uint32_t;
result_type n = 0;
static constexpr result_type
min()
{
return 0;
}
static constexpr result_type
max()
{
return 0xffffffffu;
}
result_type
operator()()
{
return n++;
}
};
// 32-bit output stored in a 64-bit result. This is the Linux mt19937 shape.
struct Bits32In64
{
using result_type = std::uint64_t;
result_type n = 0;
static constexpr result_type
min()
{
return 0;
}
static constexpr result_type
max()
{
return 0xffffffffu;
}
result_type
operator()()
{
return n++;
}
};
// Nonzero minimum, power-of-two range of 256 values.
struct NonzeroMin
{
using result_type = std::uint32_t;
result_type n = 0;
static constexpr result_type
min()
{
return 5;
}
static constexpr result_type
max()
{
return 5 + 255;
}
result_type
operator()()
{
return static_cast<result_type>(min() + (n++ % 256));
}
};
// A valid three-value engine exercises the zero-bit first group in
// independent_bits_engine. Cycling values make draw consumption explicit.
template <class Result, Result Minimum = 0>
struct ThreeValues
{
using result_type = Result;
std::size_t calls = 0;
static constexpr result_type
min()
{
return Minimum;
}
static constexpr result_type
max()
{
return Minimum + 2;
}
result_type
operator()()
{
return static_cast<result_type>(Minimum + (calls++ % 3));
}
};
} // namespace
class random_test : public beast::unit_test::suite
{
// 8-bit model of reject-then-modulo. slack = 2^w % count, drop the
// low slack words, then modulo. Every accepted result must have the
// same number of preimages.
void
testReducedMapping()
{
testcase("8-bit reject-then-modulo is uniform");
constexpr int universe = 256;
for (int count = 1; count <= universe; ++count)
{
int const slack = universe % count;
std::array<int, 256> hits{};
int accepted = 0;
for (int raw = 0; raw < universe; ++raw)
{
if (raw < slack)
continue;
++hits[raw % count];
++accepted;
}
BEAST_EXPECT(accepted % count == 0);
auto const each = accepted / count;
for (int result = 0; result < count; ++result)
BEAST_EXPECT(hits[result] == each);
}
}
template <class Engine, class Integral>
void
expectInRange(Engine& engine, Integral min, Integral max, int samples)
{
for (int i = 0; i < samples; ++i)
{
auto const value = rand_int(engine, min, max);
BEAST_EXPECT(value >= min);
BEAST_EXPECT(value <= max);
}
}
public:
void
testEngineVectors()
{
testcase("engine range vectors and draw consumption");
beast::xor_shift_engine full{1};
beast::xor_shift_engine fullTwin{1};
BEAST_EXPECT(detail::randomU64(full) == fullTwin());
BEAST_EXPECT(full() == fullTwin());
Bits32 narrow;
Bits32 narrowTwin;
auto const narrowWord = detail::randomU64(narrow);
auto const nLow = static_cast<std::uint64_t>(narrowTwin());
auto const nHigh = static_cast<std::uint64_t>(narrowTwin());
BEAST_EXPECT(narrowWord == (nLow | (nHigh << 32)));
BEAST_EXPECT(narrow() == narrowTwin());
Bits32In64 wideStore;
Bits32In64 wideStoreTwin;
auto const wideWord = detail::randomU64(wideStore);
auto const wLow = wideStoreTwin();
auto const wHigh = wideStoreTwin();
BEAST_EXPECT(wideWord == (wLow | (wHigh << 32)));
BEAST_EXPECT(wideStore() == wideStoreTwin());
NonzeroMin shifted;
NonzeroMin shiftedTwin;
std::uint64_t composed = 0;
for (int i = 0; i < 8; ++i)
{
auto const piece =
static_cast<std::uint64_t>(shiftedTwin() - NonzeroMin::min());
composed |= piece << (8 * i);
}
BEAST_EXPECT(detail::randomU64(shifted) == composed);
BEAST_EXPECT(shifted() == shiftedTwin());
std::minstd_rand uneven{12345};
std::minstd_rand unevenCopy{12345};
std::independent_bits_engine<std::minstd_rand, 64, std::uint64_t> ibits{
unevenCopy};
for (int i = 0; i < 8; ++i)
BEAST_EXPECT(detail::randomU64(uneven) == ibits());
}
void
testZeroBitGroup()
{
testcase("three-value engines retain the zero-bit group draw");
auto const check = [&](auto engine) {
using Engine = decltype(engine);
std::independent_bits_engine<Engine, 64, std::uint64_t> reference{
engine};
// R=3: n=65, w0=0, n0=1, y0=3, y1=2. Discard the
// first draw, then take 64 bits, rejecting normalized value 2.
// For this cycle the accepted bits are 1010...10 (97 draws).
auto const first = detail::randomU64(engine);
BEAST_EXPECT(first == 0xaaaaaaaaaaaaaaaaULL);
BEAST_EXPECT(engine.calls == 97);
BEAST_EXPECT(first == reference());
BEAST_EXPECT(engine.calls == reference.base().calls);
for (int i = 0; i < 16; ++i)
{
BEAST_EXPECT(detail::randomU64(engine) == reference());
BEAST_EXPECT(engine.calls == reference.base().calls);
}
// Also exercise the public closed-range mapper with the same
// normalized stream, including the runtime fault-hook range.
for (auto const count : {10u, 10'000u})
{
auto const n = static_cast<std::uint64_t>(count);
auto const slack = static_cast<std::uint64_t>(-n) % n;
std::uint64_t expected;
do
{
expected = reference();
} while (expected < slack);
BEAST_EXPECT(rand_int(engine, 0u, count - 1) == expected % n);
BEAST_EXPECT(engine.calls == reference.base().calls);
}
};
check(ThreeValues<std::uint32_t>{});
check(ThreeValues<std::uint64_t>{});
check(ThreeValues<std::uint32_t, 5>{});
check(ThreeValues<
std::uint64_t,
std::numeric_limits<std::uint64_t>::max() - 2>{});
}
void
testBounds()
{
testcase("closed bounds, including signed endpoints and 2^40");
auto check = [&](auto engine) {
expectInRange(engine, 0, 10, 64);
expectInRange(engine, 0, 9999, 64);
expectInRange(engine, -20, 20, 64);
expectInRange(engine, -5, 15, 64);
expectInRange(
engine, std::uint64_t{0}, (std::uint64_t{1} << 40) + 123u, 8);
};
check(beast::xor_shift_engine{7});
check(Bits32{});
check(Bits32In64{});
check(NonzeroMin{});
check(std::minstd_rand{7});
check(std::mt19937{7});
}
void
run() override
{
testReducedMapping();
testEngineVectors();
testZeroBitGroup();
testBounds();
}
};
BEAST_DEFINE_TESTSUITE(random, basics, ripple);
} // namespace test
} // namespace ripple