Files
xahaud/src/test/app/ConsensusEntropy_test.cpp
2026-07-16 20:09:20 +07:00

1100 lines
38 KiB
C++

//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL Labs
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 <test/app/ConsensusEntropy_test_hooks.h>
#include <test/jtx.h>
#include <test/jtx/hook.h>
#include <xrpld/app/hook/applyHook.h>
#include <xrpld/app/misc/RuntimeConfig.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/hook/Enum.h>
#include <xrpl/protocol/EntropyTier.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <limits>
namespace ripple {
namespace test {
using TestHook = std::vector<uint8_t> const&;
#define BEAST_REQUIRE(x) \
{ \
BEAST_EXPECT(!!(x)); \
if (!(x)) \
return; \
}
#define HSFEE fee(100'000'000)
#define M(m) memo(m, "", "")
namespace {
Blob
standaloneContributorMask(std::uint16_t denominator, std::uint16_t count)
{
Blob mask((denominator + 7) / 8, 0);
for (std::uint16_t i = 0; i < std::min(denominator, count); ++i)
mask[i / 8] |= static_cast<std::uint8_t>(1u << (i % 8));
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
{
static void
overrideFlag(Json::Value& jv)
{
jv[jss::Flags] = hsfOVERRIDE;
}
static int64_t
hookReturnCode(STObject const& hookExecution)
{
auto const rawCode = hookExecution.getFieldU64(sfHookReturnCode);
return (rawCode & 0x8000000000000000ULL)
? -static_cast<int64_t>(rawCode & 0x7FFFFFFFFFFFFFFFULL)
: static_cast<int64_t>(rawCode);
}
void
testSLECreated()
{
testcase("SLE created on ledger close");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
BEAST_EXPECT(!env.le(keylet::consensusEntropy()));
env.close();
auto const sle = env.le(keylet::consensusEntropy());
BEAST_REQUIRE(sle);
auto const digest = sle->getFieldH256(sfDigest);
BEAST_EXPECT(digest != uint256{});
auto const count = sle->getFieldU16(sfEntropyCount);
BEAST_EXPECT(count >= 5);
BEAST_EXPECT(sle->getFieldU16(sfEntropyDenominator) == count);
BEAST_EXPECT(
sle->getFieldVL(sfEntropyContributors) ==
standaloneContributorMask(count, count));
BEAST_EXPECT(
sle->getFieldU8(sfEntropyTier) == entropyTierValidatorFull);
auto const sleSeq = sle->getFieldU32(sfLedgerSequence);
BEAST_EXPECT(sleSeq == env.closed()->seq());
}
void
testSLEUpdatedOnSubsequentClose()
{
testcase("SLE updated on subsequent ledger close");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
env.close();
auto const sle1 = env.le(keylet::consensusEntropy());
BEAST_REQUIRE(sle1);
auto const digest1 = sle1->getFieldH256(sfDigest);
auto const seq1 = sle1->getFieldU32(sfLedgerSequence);
env.close();
auto const sle2 = env.le(keylet::consensusEntropy());
BEAST_REQUIRE(sle2);
auto const digest2 = sle2->getFieldH256(sfDigest);
auto const seq2 = sle2->getFieldU32(sfLedgerSequence);
BEAST_EXPECT(digest2 != digest1);
BEAST_EXPECT(seq2 == seq1 + 1);
}
void
testNoSLEWithoutAmendment()
{
testcase("No SLE without amendment");
using namespace jtx;
Env env{*this, supported_amendments()};
env.close();
env.close();
BEAST_EXPECT(!env.le(keylet::consensusEntropy()));
}
void
testDice()
{
testcase("Hook dice() API");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
// Entropy SLE must exist before hook can use dice()
BEAST_REQUIRE(env.le(keylet::consensusEntropy()));
// Set the hook
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 rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
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)
{
_g(1,1);
// A wide range makes this a useful byte-order known answer.
int64_t result = dice(1000000, 3);
// negative means error
if (result < 0)
rollback(0, 0, result);
if (result >= 1000000)
rollback(0, 0, -1);
// return the dice result as the accept code
return accept(0, 0, result);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set dice hook"),
HSFEE);
env.close();
// Invoke the hook
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test dice"), 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);
auto const entropy = env.le(keylet::consensusEntropy());
BEAST_REQUIRE(entropy);
auto const entropyDigest = entropy->getFieldH256(sfDigest);
auto const drawLedgerSeq = entropy->getFieldU32(sfLedgerSequence);
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, 1000000);
std::cerr << " dice(1000000) returnCode = " << returnCode << " (hex 0x"
<< std::hex << returnCode << std::dec << ")\n";
BEAST_EXPECT(returnCode == expected);
// Result should be 3 (accept)
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
}
void
testRandom()
{
testcase("Hook random() API");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
BEAST_REQUIRE(env.le(keylet::consensusEntropy()));
// Hook calls random() to fill a 32-byte buffer, then checks
// the buffer is not all zeroes.
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 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);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
int64_t hook(uint32_t r)
{
_g(1,1);
uint8_t buf[32];
for (int i = 0; GUARD(32), i < 32; ++i)
buf[i] = 0;
int64_t result = random((uint32_t)buf, 32, 3);
// Should return 32 (bytes written)
if (result != 32)
rollback(0, 0, result);
// Verify buffer is not all zeroes
int nonzero = 0;
for (int i = 0; GUARD(32), i < 32; ++i)
if (buf[i] != 0) nonzero = 1;
if (!nonzero)
rollback(0, 0, -2);
return accept(0, 0, 0);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set random hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test random"), 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);
// Return code 0 = all checks passed in the hook
BEAST_EXPECT(hookExecutions[0].getFieldU64(sfHookReturnCode) == 0);
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
}
void
testDiceConsecutiveCallsDiffer()
{
testcase("Hook dice() consecutive calls return different values");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
BEAST_REQUIRE(env.le(keylet::consensusEntropy()));
// dice(1000000) twice — large range makes collision near-impossible
// encode r1 in low 20 bits, r2 in high bits
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 rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
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);
if (r1 < 0)
rollback(0, 0, r1);
int64_t r2 = dice(1000000, 3);
if (r2 < 0)
rollback(0, 0, r2);
// consecutive calls should differ (rngCallCounter)
if (r1 == r2)
rollback(0, 0, -1);
return accept(0, 0, r1 | (r2 << 20));
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set dice hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test dice consecutive"), 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);
auto const rc = hookExecutions[0].getFieldU64(sfHookReturnCode);
auto const r1 = rc & 0xFFFFF;
auto const r2 = (rc >> 20) & 0xFFFFF;
std::cerr << " two-call dice(1000000): returnCode=" << rc << " hex=0x"
<< std::hex << rc << std::dec << " r1=" << r1 << " r2=" << r2
<< "\n";
// hookResult 3 = accept (would be 1 if r1==r2 triggered rollback)
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
BEAST_EXPECT(r1 < 1000000);
BEAST_EXPECT(r2 < 1000000);
BEAST_EXPECT(r1 != r2);
}
void
testDiceZeroSides()
{
testcase("Hook dice(0) returns INVALID_ARGUMENT");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
BEAST_REQUIRE(env.le(keylet::consensusEntropy()));
// Hook calls dice(0) and returns whatever dice returns.
// dice(0) should return INVALID_ARGUMENT (-7).
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);
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t result = dice(0, 3);
// dice(0) should return negative error code, pass it through
return accept(0, 0, result);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set dice0 hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test dice(0)"), 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);
// INVALID_ARGUMENT = -7, encoded as 0x8000000000000000 + abs(code)
// (see applyHook.cpp unsigned_exit_code encoding)
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(0) returnCode = " << returnCode << " (raw 0x"
<< std::hex << rawCode << std::dec << ")\n";
BEAST_EXPECT(returnCode == -7);
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
}
void
testEntropyStatus()
{
testcase("Hook entropy_status() metadata and policy recipes");
using namespace jtx;
Env env{
*this,
envconfig(),
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();
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);
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 entropy_status(void);
#define ENTROPY_TIER(x) (((uint64_t)(x) >> 32U) & 0xFFU)
#define ENTROPY_COUNT(x) (((uint64_t)(x) >> 16U) & 0xFFFFU)
#define ENTROPY_DENOMINATOR(x) ((uint64_t)(x) & 0xFFFFU)
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t status = entropy_status();
if (status < 0)
return accept(0, 0, 13);
uint64_t expected =
((uint64_t)3 << 32U) | ((uint64_t)19 << 16U) | 20U;
if ((uint64_t)status != expected)
return accept(0, 0, 14);
uint32_t tier = ENTROPY_TIER(status);
uint32_t count = ENTROPY_COUNT(status);
uint32_t denominator = ENTROPY_DENOMINATOR(status);
if (tier != 3 || count != 19 || denominator != 20)
return accept(0, 0, 15);
// Common caller-side policies: tolerate one absent, require
// 4/5 participation, and require an absolute floor of 19.
if (tier < 2 || denominator - count > 1)
return accept(0, 0, 16);
if ((uint64_t)5 * count < (uint64_t)4 * denominator)
return accept(0, 0, 17);
if (count < 19)
return accept(0, 0, 18);
return accept(0, 0, 0);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set entropy-status hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test 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);
}
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(void);
#define ENTROPY_TIER(x) (((uint64_t)(x) >> 32U) & 0xFFU)
#define ENTROPY_COUNT(x) (((uint64_t)(x) >> 16U) & 0xFFFFU)
#define ENTROPY_DENOMINATOR(x) ((uint64_t)(x) & 0xFFFFU)
#define TOO_LITTLE_ENTROPY (-48)
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t status = entropy_status();
if (status < 0)
return accept(0, 0, 20);
if (ENTROPY_TIER(status) != 1 || ENTROPY_COUNT(status) != 0 ||
ENTROPY_DENOMINATOR(status) != 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, 0);
}
)[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);
}
void
testStaleEntropyStatus()
{
testcase(
"Hook entropy_status() observes stale metadata while draws fail");
using namespace jtx;
Env env{
*this,
envconfig(),
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();
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 random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier);
extern int64_t entropy_status(void);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
#define TOO_LITTLE_ENTROPY (-48)
int64_t hook(uint32_t r)
{
_g(1,1);
uint64_t expected =
((uint64_t)3 << 32U) | ((uint64_t)19 << 16U) | 20U;
if ((uint64_t)entropy_status() != expected)
return accept(0, 0, 40);
int64_t dice_result = dice(6, 1);
if (dice_result != TOO_LITTLE_ENTROPY)
return accept(0, 0, 41);
uint8_t buf[32];
for (int i = 0; GUARD(32), i < 32; ++i)
buf[i] = 0xA5;
if (random((uint32_t)buf, 32, 1) != TOO_LITTLE_ENTROPY)
return accept(0, 0, 42);
for (int i = 0; GUARD(32), i < 32; ++i)
if (buf[i] != 0xA5)
return accept(0, 0, 43);
return accept(0, 0, 0);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set stale entropy hook"),
HSFEE);
env.close();
OpenView view{*env.current()};
auto const openSeq = view.info().seq;
BEAST_REQUIRE(openSeq > 2);
auto const entropy = view.read(keylet::consensusEntropy());
BEAST_REQUIRE(entropy);
auto replacement = std::make_shared<SLE>(*entropy, entropy->key());
replacement->setFieldU32(sfLedgerSequence, openSeq - 2);
view.rawReplace(replacement);
auto const hookArray = view.read(keylet::hook(alice.id()));
BEAST_REQUIRE(hookArray);
auto const& hooks = hookArray->getFieldArray(sfHooks);
BEAST_REQUIRE(hooks.size() == 1);
auto const& hookObj = hooks[0];
auto const hookHash = hookObj.getFieldH256(sfHookHash);
auto const hookDefView = view.read(keylet::hookDefinition(hookHash));
BEAST_REQUIRE(hookDefView);
auto const hookDef =
std::make_shared<SLE>(*hookDefView, hookDefView->key());
STTx const invokeTx = STTx(ttINVOKE, [&](STObject& obj) {
obj.setAccountID(sfAccount, alice.id());
});
ApplyContext applyCtx{
env.app(),
view,
invokeTx,
tesSUCCESS,
env.current()->fees().base,
tapNONE,
env.journal};
hook::HookStateMap stateMap;
std::map<std::vector<uint8_t>, std::vector<uint8_t>> parameters;
BEAST_REQUIRE(!hook::gatherHookParameters(
hookDef, hookObj, parameters, env.journal));
auto const hookNamespace = hookObj.isFieldPresent(sfHookNamespace)
? hookObj.getFieldH256(sfHookNamespace)
: hookDef->getFieldH256(sfHookNamespace);
auto result = hook::apply(
hookDef->getFieldH256(sfHookSetTxnID),
hookHash,
hook::getHookCanEmit(hookObj, hookDef),
hookNamespace,
hookDef->getFieldVL(sfCreateCode),
parameters,
{},
stateMap,
applyCtx,
alice.id(),
hookDef->isFieldPresent(sfHookCallbackFee),
false,
true,
0,
0,
{});
BEAST_EXPECT(result.exitType == hook_api::ExitType::ACCEPT);
BEAST_EXPECT(result.exitCode == 0);
BEAST_EXPECT(result.rngCallCounter == 0);
}
void
testDiceTierRequirementNotMet()
{
testcase("Hook dice() fails closed below min_tier");
using namespace jtx;
Env env{
*this,
envconfig(),
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();
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);
BEAST_EXPECT(
sle->getFieldVL(sfEntropyContributors) ==
standaloneContributorMask(20, 19));
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);
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t result = dice(6, 4);
return accept(0, 0, result);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set dice-tier-requirement hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test dice min_tier unmet"), 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]) == -48);
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
}
void
testDiceWithoutAmendment()
{
testcase("Hook entropy imports independently require amendment");
using namespace jtx;
Env env{*this, supported_amendments()};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
BEAST_EXPECT(!env.le(keylet::consensusEntropy()));
TestHook diceHook = 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);
int64_t hook(uint32_t r)
{
_g(1,1);
return accept(0, 0, dice(6, 3));
}
)[test.hook]"];
TestHook randomHook = 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 random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier);
int64_t hook(uint32_t r)
{
_g(1,1);
uint8_t buf[32];
return accept(0, 0, random((uint32_t)buf, 32, 3));
}
)[test.hook]"];
TestHook statusHook = 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 entropy_status(void);
int64_t hook(uint32_t r)
{
_g(1,1);
return accept(0, 0, entropy_status());
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(diceHook, overrideFlag)}}, 0),
M("set dice-no-amendment hook"),
HSFEE,
ter(temMALFORMED));
env(ripple::test::jtx::hook(
alice, {{hso(randomHook, overrideFlag)}}, 0),
M("set random-no-amendment hook"),
HSFEE,
ter(temMALFORMED));
env(ripple::test::jtx::hook(
alice, {{hso(statusHook, overrideFlag)}}, 0),
M("set entropy-status-no-amendment hook"),
HSFEE,
ter(temMALFORMED));
}
void
testRandomTierRequirementNotMet()
{
testcase("Hook random() fails before write below min_tier");
using namespace jtx;
Env env{
*this,
envconfig(),
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();
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 random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
#define TOO_LITTLE_ENTROPY (-48)
int64_t hook(uint32_t r)
{
_g(1,1);
uint8_t buf[32];
for (int i = 0; GUARD(32), i < 32; ++i)
buf[i] = 0xA5;
if (random((uint32_t)buf, 32, 4) != TOO_LITTLE_ENTROPY)
return accept(0, 0, 30);
for (int i = 0; GUARD(32), i < 32; ++i)
if (buf[i] != 0xA5)
return accept(0, 0, 31);
return accept(0, 0, 0);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set random-tier-requirement hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test random min_tier unmet"), 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);
}
void
testInvalidEntropyRequirements()
{
testcase("Hook dice/random reject invalid entropy requirements");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
BEAST_REQUIRE(env.le(keylet::consensusEntropy()));
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 random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier);
#define INVALID_ARGUMENT (-7)
int64_t hook(uint32_t r)
{
_g(1,1);
uint8_t buf[32];
int64_t bad_min_tier = dice(6, 0);
if (bad_min_tier != INVALID_ARGUMENT)
return accept(0, 0, 100);
int64_t bad_high_tier = dice(6, 5);
if (bad_high_tier != INVALID_ARGUMENT)
return accept(0, 0, 101);
int64_t bad_random_low = random((uint32_t)buf, 32, 0);
if (bad_random_low != INVALID_ARGUMENT)
return accept(0, 0, 102);
int64_t bad_random_high = random((uint32_t)buf, 32, 5);
if (bad_random_high != INVALID_ARGUMENT)
return accept(0, 0, 103);
// Failed calls must not consume the shared RNG call counter.
// The test pins the first valid draw as a known-answer vector.
return accept(0, 0, dice(1000000, 4));
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set invalid entropy requirement hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test invalid entropy requirements"), 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);
auto const entropy = env.le(keylet::consensusEntropy());
BEAST_REQUIRE(entropy);
auto const firstBlock = sha512Half(
entropy->getFieldU32(sfLedgerSequence),
env.tx()->getTransactionID(),
alice.id(),
hookExecutions[0].getFieldH256(sfHookHash),
alice.id(),
std::uint8_t{0},
std::string{"strong"},
std::string{"direct"},
entropy->getFieldH256(sfDigest),
std::uint64_t{0});
auto const actual = hookReturnCode(hookExecutions[0]);
BEAST_EXPECT(actual == expectedDice(firstBlock, 1000000));
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
}
void
run() override
{
testSLECreated();
testSLEUpdatedOnSubsequentClose();
testNoSLEWithoutAmendment();
testDice();
testDiceZeroSides();
testEntropyStatus();
testEntropyStatusFallback();
testStaleEntropyStatus();
testDiceTierRequirementNotMet();
testDiceWithoutAmendment();
testRandomTierRequirementNotMet();
testInvalidEntropyRequirements();
testRandom();
testDiceConsecutiveCallsDiffer();
}
};
BEAST_DEFINE_TESTSUITE(ConsensusEntropy, app, ripple);
} // namespace test
} // namespace ripple