diff --git a/include/xrpl/basics/random.h b/include/xrpl/basics/random.h index 23fe17c65..e5b5b7ad0 100644 --- a/include/xrpl/basics/random.h +++ b/include/xrpl/basics/random.h @@ -58,11 +58,14 @@ std::uint64_t randomU64(Engine& engine) { 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()); }; - // 0 means the span is 2^64. That is one full-range draw. + // 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; @@ -101,7 +104,9 @@ randomU64(Engine& engine) constexpr auto y0 = (std::uint64_t{1} << w0) * (range >> w0); constexpr auto y1 = (std::uint64_t{1} << (w0 + 1)) * (range >> (w0 + 1)); - static_assert(w0 > 0 && w0 < 63); + // 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) diff --git a/src/test/basics/random_test.cpp b/src/test/basics/random_test.cpp index 8355347a9..319d00855 100644 --- a/src/test/basics/random_test.cpp +++ b/src/test/basics/random_test.cpp @@ -23,6 +23,7 @@ #include #include +#include #include namespace ripple { @@ -95,6 +96,33 @@ struct NonzeroMin } }; +// 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 @@ -185,6 +213,54 @@ public: 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() { @@ -212,6 +288,7 @@ public: { testReducedMapping(); testEngineVectors(); + testZeroBitGroup(); testBounds(); } };