fix(entropy): canonicalize dice word order

This commit is contained in:
Nicholas Dudfield
2026-07-16 19:41:55 +07:00
parent 8ef38062af
commit fa763268e2
3 changed files with 51 additions and 5 deletions

View File

@@ -30,6 +30,7 @@
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <limits>
namespace ripple {
namespace test {
@@ -57,6 +58,30 @@ standaloneContributorMask(std::uint16_t denominator, std::uint16_t count)
return mask;
}
std::uint32_t
expectedDice(uint256 block, std::uint32_t sides)
{
auto const sampleRange =
std::uint64_t{std::numeric_limits<std::uint32_t>::max()} + 1;
auto const acceptLimit = sampleRange - (sampleRange % sides);
for (;;)
{
for (std::size_t i = 0; i < block.size(); i += sizeof(std::uint32_t))
{
auto const* candidate = block.data() + i;
std::uint32_t const value =
(std::uint32_t{candidate[0]} << 24U) |
(std::uint32_t{candidate[1]} << 16U) |
(std::uint32_t{candidate[2]} << 8U) |
std::uint32_t{candidate[3]};
if (value < acceptLimit)
return value % sides;
}
block = sha512Half(Slice{block.data(), block.size()});
}
}
} // namespace
class ConsensusEntropy_test : public beast::unit_test::suite
@@ -208,6 +233,11 @@ class ConsensusEntropy_test : public beast::unit_test::suite
HSFEE);
env.close();
auto const entropy = env.le(keylet::consensusEntropy());
BEAST_REQUIRE(entropy);
auto const entropyDigest = entropy->getFieldH256(sfDigest);
auto const drawLedgerSeq = env.current()->info().seq;
// Invoke the hook
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
@@ -222,10 +252,21 @@ class ConsensusEntropy_test : public beast::unit_test::suite
BEAST_REQUIRE(hookExecutions.size() == 1);
auto const returnCode = hookExecutions[0].getFieldU64(sfHookReturnCode);
auto const firstBlock = sha512Half(
drawLedgerSeq,
env.tx()->getTransactionID(),
alice.id(),
hookExecutions[0].getFieldH256(sfHookHash),
alice.id(),
std::uint8_t{0},
std::string{"strong"},
std::string{"direct"},
entropyDigest,
std::uint64_t{0});
auto const expected = expectedDice(firstBlock, 6);
std::cerr << " dice(6) returnCode = " << returnCode << " (hex 0x"
<< std::hex << returnCode << std::dec << ")\n";
// dice(6) returns 0..5
BEAST_EXPECT(returnCode <= 5);
BEAST_EXPECT(returnCode == expected);
// Result should be 3 (accept)
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);

View File

@@ -230,7 +230,8 @@ entropyDigest, i)`. The counter is local to one Hook execution role and is
post-incremented once when a draw stream passes snapshot admission; rejected
arguments or entropy do not consume it. Further blocks are
`sha512Half(previousBlock)`. `dice` rejects zero sides and uses deterministic
32-bit rejection sampling rather than biased modulo reduction. `random` accepts
unsigned big-endian 32-bit rejection sampling rather than biased modulo
reduction. `random` accepts
one through 512 requested bytes, rounds its internal generation length to a
32-byte boundary, and writes only the requested prefix. Missing, malformed,
future, older-than-one-ledger, or below-tier entropy makes either draw return

View File

@@ -4204,8 +4204,12 @@ DEFINE_HOOK_FUNCTION(int64_t, dice, uint32_t sides, uint32_t min_tier)
for (std::size_t i = 0; i + sizeof(std::uint32_t) <= bytes.size();
i += sizeof(std::uint32_t))
{
std::uint32_t value;
std::memcpy(&value, bytes.data() + i, sizeof(std::uint32_t));
auto const* candidate = bytes.data() + i;
std::uint32_t const value =
(std::uint32_t{candidate[0]} << 24U) |
(std::uint32_t{candidate[1]} << 16U) |
(std::uint32_t{candidate[2]} << 8U) |
std::uint32_t{candidate[3]};
if (value < acceptLimit)
return value % sides;
}