diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index 469f7e3ca..a1bbc40d2 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -1725,12 +1725,16 @@ RCLConsensus::Adaptor::injectEntropyPseudoTx( if (hasEntropy) { // Account Zero convention for pseudo-transactions (same as ttFEE, etc) + auto const entropyCount = static_cast( + entropyFailed_ || pendingReveals_.empty() ? 0 + : pendingReveals_.size()); STTx tx(ttCONSENSUS_ENTROPY, [&](auto& obj) { obj.setFieldU32(sfLedgerSequence, seq); obj.setAccountID(sfAccount, AccountID{}); obj.setFieldU32(sfSequence, 0); obj.setFieldAmount(sfFee, STAmount{}); obj.setFieldH256(sfDigest, finalEntropy); + obj.setFieldU16(sfEntropyCount, entropyCount); }); retriableTxs.insert(std::make_shared(std::move(tx))); diff --git a/src/ripple/app/hook/Enum.h b/src/ripple/app/hook/Enum.h index 9728b1040..7ada2b50b 100644 --- a/src/ripple/app/hook/Enum.h +++ b/src/ripple/app/hook/Enum.h @@ -350,7 +350,8 @@ enum hook_return_code : int64_t { MEM_OVERLAP = -43, // one or more specified buffers are the same memory TOO_MANY_STATE_MODIFICATIONS = -44, // more than 5000 modified state // entires in the combined hook chains - TOO_MANY_NAMESPACES = -45 + TOO_MANY_NAMESPACES = -45, + TOO_LITTLE_ENTROPY = -46, }; enum ExitType : uint8_t { @@ -469,6 +470,14 @@ static const APIWhitelist import_whitelist_1{ // clang-format on }; +// featureConsensusEntropy +static const APIWhitelist import_whitelist_entropy{ + // clang-format off + HOOK_API_DEFINITION(I64, dice, (I32)), + HOOK_API_DEFINITION(I64, random, (I32, I32)), + // clang-format on +}; + #undef HOOK_API_DEFINITION #undef I32 #undef I64 diff --git a/src/ripple/app/hook/Guard.h b/src/ripple/app/hook/Guard.h index f395af448..eb9be6d34 100644 --- a/src/ripple/app/hook/Guard.h +++ b/src/ripple/app/hook/Guard.h @@ -1034,6 +1034,13 @@ validateGuards( { // PASS, this is a version 1 api } + else if ( + (rulesVersion & 0x04U) && + hook_api::import_whitelist_entropy.find(import_name) != + hook_api::import_whitelist_entropy.end()) + { + // PASS, this is a consensus entropy api + } else { GUARDLOG(hook::log::IMPORT_ILLEGAL) @@ -1262,8 +1269,13 @@ validateGuards( hook_api::import_whitelist.find(api_name) != hook_api::import_whitelist.end() ? hook_api::import_whitelist.find(api_name)->second - : hook_api::import_whitelist_1.find(api_name) - ->second; + : hook_api::import_whitelist_1.find(api_name) != + hook_api::import_whitelist_1.end() + ? hook_api::import_whitelist_1.find(api_name) + ->second + : hook_api::import_whitelist_entropy + .find(api_name) + ->second; if (!first_signature) { diff --git a/src/ripple/app/hook/applyHook.h b/src/ripple/app/hook/applyHook.h index 2c1f250bb..52cc6d15b 100644 --- a/src/ripple/app/hook/applyHook.h +++ b/src/ripple/app/hook/applyHook.h @@ -406,6 +406,9 @@ DECLARE_HOOK_FUNCTION( uint32_t slot_no_tx, uint32_t slot_no_meta); +DECLARE_HOOK_FUNCTION(int64_t, dice, uint32_t sides); +DECLARE_HOOK_FUNCTION(int64_t, random, uint32_t write_ptr, uint32_t write_len); + /* DECLARE_HOOK_FUNCTION(int64_t, str_find, uint32_t hread_ptr, uint32_t hread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t mode, @@ -513,6 +516,8 @@ struct HookResult false; // hook_again allows strong pre-apply to nominate // additional weak post-apply execution std::shared_ptr provisionalMeta; + uint64_t rngCallCounter{ + 0}; // used to ensure conseq. rng calls don't return same data }; class HookExecutor; diff --git a/src/ripple/app/hook/impl/applyHook.cpp b/src/ripple/app/hook/impl/applyHook.cpp index f27b7d23b..7e8fc1761 100644 --- a/src/ripple/app/hook/impl/applyHook.cpp +++ b/src/ripple/app/hook/impl/applyHook.cpp @@ -6156,6 +6156,117 @@ DEFINE_HOOK_FUNCTION( HOOK_TEARDOWN(); } + +// byteCount must be a multiple of 32 +inline std::vector +fairRng(ApplyContext& applyCtx, hook::HookResult& hr, uint32_t byteCount) +{ + if (byteCount > 512) + byteCount = 512; + + // force the byte count to be a multiple of 32 + byteCount &= ~0b11111; + + if (byteCount == 0) + return {}; + + auto& view = applyCtx.view(); + + auto const sleEntropy = view.peek(ripple::keylet::consensusEntropy()); + auto const seq = view.info().seq; + + if (!sleEntropy || sleEntropy->getFieldU32(sfLedgerSequence) != seq || + sleEntropy->getFieldU16(sfEntropyCount) < 5) + return {}; + + // we'll generate bytes in lots of 32 + + uint256 rndData = sha512Half( + view.info().seq, + applyCtx.tx.getTransactionID(), + hr.otxnAccount, + hr.hookHash, + hr.account, + hr.hookChainPosition, + hr.executeAgainAsWeak ? std::string("weak") : std::string("strong"), + sleEntropy->getFieldH256(sfDigest), + hr.rngCallCounter++); + + std::vector bytesOut; + bytesOut.resize(byteCount); + + uint8_t* ptr = bytesOut.data(); + while (1) + { + std::memcpy(ptr, rndData.data(), 32); + ptr += 32; + + if (ptr - bytesOut.data() >= byteCount) + break; + + rndData = sha512Half(rndData); + } + + return bytesOut; +} + +DEFINE_HOOK_FUNCTION(int64_t, dice, uint32_t sides) +{ + HOOK_SETUP(); + + auto vec = fairRng(applyCtx, hookCtx.result, 32); + + if (vec.empty()) + return TOO_LITTLE_ENTROPY; + + if (vec.size() != 32) + return INTERNAL_ERROR; + + uint32_t value; + std::memcpy(&value, vec.data(), sizeof(uint32_t)); + + return value % sides; + + HOOK_TEARDOWN(); +} + +DEFINE_HOOK_FUNCTION(int64_t, random, uint32_t write_ptr, uint32_t write_len) +{ + HOOK_SETUP(); + + if (write_len == 0) + return TOO_SMALL; + + if (write_len > 512) + return TOO_BIG; + + uint32_t required = write_len; + + if ((required & ~0b11111) == required) + { + // already a multiple of 32 bytes + } + else + { + // round up + required &= ~0b11111; + required += 32; + } + + if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + auto vec = fairRng(applyCtx, hookCtx.result, required); + + if (vec.empty()) + return TOO_LITTLE_ENTROPY; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, vec.data(), vec.size(), memory, memory_length); + + HOOK_TEARDOWN(); +} + /* DEFINE_HOOK_FUNCTION( diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 73d683bd7..94694cf5c 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -245,6 +245,8 @@ Change::applyConsensusEntropy() sle = std::make_shared(keylet::consensusEntropy()); sle->setFieldH256(sfDigest, entropy); + sle->setFieldU16(sfEntropyCount, ctx_.tx.getFieldU16(sfEntropyCount)); + sle->setFieldU32(sfLedgerSequence, view().info().seq); // Note: sfPreviousTxnID and sfPreviousTxnLgrSeq are set automatically // by ApplyStateTable::threadItem() because isThreadedType() returns true // for ledger entries that have sfPreviousTxnID in their format. diff --git a/src/ripple/app/tx/impl/SetHook.cpp b/src/ripple/app/tx/impl/SetHook.cpp index 9ca829557..c4cec7eef 100644 --- a/src/ripple/app/tx/impl/SetHook.cpp +++ b/src/ripple/app/tx/impl/SetHook.cpp @@ -491,7 +491,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj) logger, hsacc, (ctx.rules.enabled(featureHooksUpdate1) ? 1 : 0) + - (ctx.rules.enabled(fix20250131) ? 2 : 0)); + (ctx.rules.enabled(fix20250131) ? 2 : 0) + + (ctx.rules.enabled(featureConsensusEntropy) ? 4 : 0)); if (ctx.j.trace()) { diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index f507b2232..ef90bbef3 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -355,6 +355,7 @@ extern SF_UINT16 const sfHookEmitCount; extern SF_UINT16 const sfHookExecutionIndex; extern SF_UINT16 const sfHookApiVersion; extern SF_UINT16 const sfHookStateScale; +extern SF_UINT16 const sfEntropyCount; // 32-bit integers (common) extern SF_UINT32 const sfNetworkID; diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index ed986f6a4..375f066d8 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -385,6 +385,8 @@ LedgerFormats::LedgerFormats() ltCONSENSUS_ENTROPY, { {sfDigest, soeREQUIRED}, // The consensus-derived entropy + {sfEntropyCount, soeREQUIRED}, // Number of validators that contributed + {sfLedgerSequence, soeREQUIRED}, // Ledger this entropy is for {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, }, diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index c4f2ef85a..d7d8c6df2 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -103,6 +103,7 @@ CONSTRUCT_TYPED_SFIELD(sfHookEmitCount, "HookEmitCount", UINT16, CONSTRUCT_TYPED_SFIELD(sfHookExecutionIndex, "HookExecutionIndex", UINT16, 19); CONSTRUCT_TYPED_SFIELD(sfHookApiVersion, "HookApiVersion", UINT16, 20); CONSTRUCT_TYPED_SFIELD(sfHookStateScale, "HookStateScale", UINT16, 21); +CONSTRUCT_TYPED_SFIELD(sfEntropyCount, "EntropyCount", UINT16, 99); // 32-bit integers (common) CONSTRUCT_TYPED_SFIELD(sfNetworkID, "NetworkID", UINT32, 1); diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 0f266d89a..7d0dc9c2d 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -496,6 +496,7 @@ TxFormats::TxFormats() { {sfLedgerSequence, soeREQUIRED}, {sfDigest, soeREQUIRED}, + {sfEntropyCount, soeREQUIRED}, {sfBlob, soeOPTIONAL}, // Proposal proof for SHAMap entries }, commonFields);