diff --git a/include/xrpl/basics/random.h b/include/xrpl/basics/random.h index 12ae16c0d1..e5b5b7ad01 100644 --- a/include/xrpl/basics/random.h +++ b/include/xrpl/basics/random.h @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -50,30 +51,85 @@ namespace detail { template using is_engine = std::is_invocable_r; -// 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 std::uint64_t randomU64(Engine& engine) { - using Result = typename Engine::result_type; - constexpr int digits = std::numeric_limits::digits; + static_assert(std::is_unsigned_v); + static_assert( + std::numeric_limits::digits <= 64); + static_assert(Engine::min() < Engine::max()); auto const draw = [&engine]() -> std::uint64_t { return static_cast(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(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(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 diff --git a/src/test/basics/random_test.cpp b/src/test/basics/random_test.cpp new file mode 100644 index 0000000000..319d008559 --- /dev/null +++ b/src/test/basics/random_test.cpp @@ -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 +#include +#include + +#include +#include +#include +#include + +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(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 +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(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 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 + 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(narrowTwin()); + auto const nHigh = static_cast(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(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 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 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(count); + auto const slack = static_cast(-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{}); + check(ThreeValues{}); + check(ThreeValues{}); + check(ThreeValues< + std::uint64_t, + std::numeric_limits::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