feat: simplify entropy Hook quality gates

This commit is contained in:
Nicholas Dudfield
2026-07-16 14:11:51 +07:00
parent 142fde5acd
commit bf6cf9c84a
8 changed files with 644 additions and 313 deletions

View File

@@ -181,7 +181,7 @@ class ConsensusEntropy_test : public beast::unit_test::suite
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
extern int64_t dice(uint32_t sides, uint32_t min_tier);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
int64_t hook(uint32_t r)
@@ -189,7 +189,7 @@ class ConsensusEntropy_test : public beast::unit_test::suite
_g(1,1);
// dice(6) should return 0..5
int64_t result = dice(6, 3, 5);
int64_t result = dice(6, 3);
// negative means error
if (result < 0)
@@ -256,7 +256,7 @@ class ConsensusEntropy_test : public beast::unit_test::suite
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier, uint32_t min_count);
extern int64_t random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
int64_t hook(uint32_t r)
@@ -267,7 +267,7 @@ class ConsensusEntropy_test : public beast::unit_test::suite
for (int i = 0; GUARD(32), i < 32; ++i)
buf[i] = 0;
int64_t result = random((uint32_t)buf, 32, 3, 5);
int64_t result = random((uint32_t)buf, 32, 3);
// Should return 32 (bytes written)
if (result != 32)
@@ -332,16 +332,16 @@ class ConsensusEntropy_test : public beast::unit_test::suite
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
extern int64_t dice(uint32_t sides, uint32_t min_tier);
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t r1 = dice(1000000, 3, 5);
int64_t r1 = dice(1000000, 3);
if (r1 < 0)
rollback(0, 0, r1);
int64_t r2 = dice(1000000, 3, 5);
int64_t r2 = dice(1000000, 3);
if (r2 < 0)
rollback(0, 0, r2);
@@ -409,12 +409,12 @@ class ConsensusEntropy_test : public beast::unit_test::suite
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
extern int64_t dice(uint32_t sides, uint32_t min_tier);
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t result = dice(0, 3, 5);
int64_t result = dice(0, 3);
// dice(0) should return negative error code, pass it through
return accept(0, 0, result);
}
@@ -450,9 +450,9 @@ class ConsensusEntropy_test : public beast::unit_test::suite
}
void
testDiceRequirementNotMet()
testEntropyStatus()
{
testcase("Hook dice() returns TOO_LITTLE_ENTROPY below requirement");
testcase("Hook entropy_status() metadata and policy recipes");
using namespace jtx;
Env env{
@@ -461,41 +461,78 @@ class ConsensusEntropy_test : public beast::unit_test::suite
supported_amendments() | featureConsensusEntropy,
nullptr};
ConsensusTestConfig cfg;
cfg.standaloneEntropyTier = entropyTierValidatorQuorum;
cfg.standaloneEntropyCount = 19;
cfg.standaloneEntropyDenominator = 20;
env.app().getRuntimeConfig().setGlobalConfig(cfg);
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
BEAST_REQUIRE(env.le(keylet::consensusEntropy()));
auto const sle = env.le(keylet::consensusEntropy());
BEAST_REQUIRE(sle);
BEAST_EXPECT(
sle->getFieldU8(sfEntropyTier) == entropyTierValidatorQuorum);
BEAST_EXPECT(sle->getFieldU16(sfEntropyCount) == 19);
BEAST_EXPECT(sle->getFieldU16(sfEntropyDenominator) == 20);
// Standalone entropy carries EntropyCount=20,
// EntropyDenominator=20, and tier validator_full.
// A hook demanding min_count=21 states a requirement this ledger
// cannot meet, so dice must fail closed with TOO_LITTLE_ENTROPY (-48)
// rather than silently serving weaker entropy.
TestHook hook = consensusentropy_test_wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
extern int64_t entropy_status(uint32_t write_ptr, uint32_t write_len);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
#define OUT_OF_BOUNDS (-1)
#define TOO_SMALL (-4)
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t result = dice(6, 3, 21);
// requirement unmet: pass the error code through
return accept(0, 0, result);
uint8_t status[5] = {0xA5, 0xA5, 0xA5, 0xA5, 0xA5};
if (entropy_status((uint32_t)status, 4) != TOO_SMALL)
return accept(0, 0, 10);
for (int i = 0; GUARD(5), i < 5; ++i)
if (status[i] != 0xA5)
return accept(0, 0, 11);
if (entropy_status(0xFFFFFFFEU, 5) != OUT_OF_BOUNDS)
return accept(0, 0, 12);
int64_t age = entropy_status((uint32_t)status, 5);
if (age < 0 || age > 1)
return accept(0, 0, 13);
uint32_t count = ((uint32_t)status[1] << 8) | status[2];
uint32_t denominator =
((uint32_t)status[3] << 8) | status[4];
if (status[0] != 3 || count != 19 || denominator != 20)
return accept(0, 0, 14);
// Common caller-side policies: tolerate one absent, require
// 4/5 participation, and require an absolute floor of 19.
if (status[0] < 2 || denominator - count > 1)
return accept(0, 0, 15);
if ((uint64_t)5 * count < (uint64_t)4 * denominator)
return accept(0, 0, 16);
if (count < 19)
return accept(0, 0, 17);
return accept(0, 0, age);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set dice-requirement hook"),
M("set entropy-status hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test dice min_count unmet"), fee(XRP(1)));
env(invoke, M("test entropy status"), fee(XRP(1)));
auto meta = env.meta();
BEAST_REQUIRE(meta);
@@ -504,13 +541,86 @@ class ConsensusEntropy_test : public beast::unit_test::suite
auto const hookExecutions = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
auto const rawCode = hookExecutions[0].getFieldU64(sfHookReturnCode);
int64_t returnCode = (rawCode & 0x8000000000000000ULL)
? -static_cast<int64_t>(rawCode & 0x7FFFFFFFFFFFFFFFULL)
: static_cast<int64_t>(rawCode);
std::cerr << " dice(6,3,21) returnCode = " << returnCode << " (raw 0x"
<< std::hex << rawCode << std::dec << ")\n";
BEAST_EXPECT(returnCode == -48); // TOO_LITTLE_ENTROPY
// Open-ledger preview accepts age 1; final ordered execution sees the
// current-ledger pseudo first and records age 0 in final metadata.
BEAST_EXPECT(hookReturnCode(hookExecutions[0]) == 0);
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
}
void
testEntropyStatusFallback()
{
testcase("Hook entropy_status() classifies fallback before arithmetic");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
ConsensusTestConfig cfg;
cfg.standaloneEntropyTier = entropyTierConsensusFallback;
cfg.standaloneEntropyCount = 0;
cfg.standaloneEntropyDenominator = 0;
env.app().getRuntimeConfig().setGlobalConfig(cfg);
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
auto const sle = env.le(keylet::consensusEntropy());
BEAST_REQUIRE(sle);
BEAST_EXPECT(
sle->getFieldU8(sfEntropyTier) == entropyTierConsensusFallback);
BEAST_EXPECT(sle->getFieldU16(sfEntropyCount) == 0);
BEAST_EXPECT(sle->getFieldU16(sfEntropyDenominator) == 0);
TestHook hook = consensusentropy_test_wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier);
extern int64_t entropy_status(uint32_t write_ptr, uint32_t write_len);
#define TOO_LITTLE_ENTROPY (-48)
int64_t hook(uint32_t r)
{
_g(1,1);
uint8_t status[5];
int64_t age = entropy_status((uint32_t)status, 5);
if (age < 0 || age > 1)
return accept(0, 0, 20);
if (status[0] != 1 || status[1] != 0 || status[2] != 0 ||
status[3] != 0 || status[4] != 0)
return accept(0, 0, 21);
int64_t allowed = dice(6, 1);
if (allowed < 0 || allowed > 5)
return accept(0, 0, 22);
if (dice(6, 2) != TOO_LITTLE_ENTROPY)
return accept(0, 0, 23);
return accept(0, 0, age);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set fallback entropy-status hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test fallback entropy status"), fee(XRP(1)));
auto meta = env.meta();
BEAST_REQUIRE(meta);
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookExecutions = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
BEAST_EXPECT(hookReturnCode(hookExecutions[0]) == 0);
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
}
@@ -550,12 +660,12 @@ class ConsensusEntropy_test : public beast::unit_test::suite
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
extern int64_t dice(uint32_t sides, uint32_t min_tier);
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t result = dice(6, 4, 0);
int64_t result = dice(6, 4);
return accept(0, 0, result);
}
)[test.hook]"];
@@ -602,12 +712,16 @@ class ConsensusEntropy_test : public beast::unit_test::suite
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
extern int64_t dice(uint32_t sides, uint32_t min_tier);
extern int64_t entropy_status(uint32_t write_ptr, uint32_t write_len);
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t result = dice(6, 3, 0);
uint8_t status[5];
int64_t result = entropy_status((uint32_t)status, 5);
if (result >= 0)
result = dice(6, 3);
return accept(0, 0, result);
}
)[test.hook]"];
@@ -640,8 +754,8 @@ class ConsensusEntropy_test : public beast::unit_test::suite
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
extern int64_t random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier, uint32_t min_count);
extern int64_t dice(uint32_t sides, uint32_t min_tier);
extern int64_t random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier);
#define INVALID_ARGUMENT (-7)
int64_t hook(uint32_t r)
@@ -649,25 +763,25 @@ class ConsensusEntropy_test : public beast::unit_test::suite
_g(1,1);
uint8_t buf[32];
int64_t bad_min_tier = dice(6, 0, 0);
int64_t bad_min_tier = dice(6, 0);
if (bad_min_tier != INVALID_ARGUMENT)
return accept(0, 0, bad_min_tier);
int64_t ok_full_tier = dice(6, 4, 0);
int64_t ok_full_tier = dice(6, 4);
if (ok_full_tier < 0 || ok_full_tier > 5)
return accept(0, 0, ok_full_tier);
int64_t bad_high_tier = dice(6, 5, 0);
int64_t bad_high_tier = dice(6, 5);
if (bad_high_tier != INVALID_ARGUMENT)
return accept(0, 0, bad_high_tier);
int64_t bad_min_count = dice(6, 3, 70000);
if (bad_min_count != INVALID_ARGUMENT)
return accept(0, 0, bad_min_count);
int64_t bad_random_low = random((uint32_t)buf, 32, 0);
if (bad_random_low != INVALID_ARGUMENT)
return accept(0, 0, bad_random_low);
int64_t bad_random_tier = random((uint32_t)buf, 32, 5, 0);
if (bad_random_tier != INVALID_ARGUMENT)
return accept(0, 0, bad_random_tier);
int64_t bad_random_high = random((uint32_t)buf, 32, 5);
if (bad_random_high != INVALID_ARGUMENT)
return accept(0, 0, bad_random_high);
// Sentinel distinct from any dice (0..5) / random result and
// from INVALID_ARGUMENT, so a regression that lets a bad
@@ -693,7 +807,7 @@ class ConsensusEntropy_test : public beast::unit_test::suite
auto const hookExecutions = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
// 42 only if all four invalid requirements were rejected; any bad
// 42 only if all invalid requirements were rejected; any bad
// requirement leaking through returns its own (non-42) code.
BEAST_EXPECT(hookReturnCode(hookExecutions[0]) == 42);
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
@@ -707,7 +821,8 @@ class ConsensusEntropy_test : public beast::unit_test::suite
testNoSLEWithoutAmendment();
testDice();
testDiceZeroSides();
testDiceRequirementNotMet();
testEntropyStatus();
testEntropyStatusFallback();
testDiceTierRequirementNotMet();
testDiceWithoutAmendment();
testInvalidEntropyRequirements();