diff --git a/hook/genesis/headers/sfcodes.h b/hook/genesis/headers/sfcodes.h index 24db95744..16350f894 100644 --- a/hook/genesis/headers/sfcodes.h +++ b/hook/genesis/headers/sfcodes.h @@ -26,6 +26,7 @@ #define sfLedgerFixType ((1U << 16U) + 22U) #define sfHookExportCount ((1U << 16U) + 98U) #define sfEntropyCount ((1U << 16U) + 99U) +#define sfEntropyDenominator ((1U << 16U) + 100U) #define sfNetworkID ((2U << 16U) + 1U) #define sfFlags ((2U << 16U) + 2U) #define sfSourceTag ((2U << 16U) + 3U) diff --git a/hook/sfcodes.h b/hook/sfcodes.h index 24db95744..16350f894 100644 --- a/hook/sfcodes.h +++ b/hook/sfcodes.h @@ -26,6 +26,7 @@ #define sfLedgerFixType ((1U << 16U) + 22U) #define sfHookExportCount ((1U << 16U) + 98U) #define sfEntropyCount ((1U << 16U) + 99U) +#define sfEntropyDenominator ((1U << 16U) + 100U) #define sfNetworkID ((2U << 16U) + 1U) #define sfFlags ((2U << 16U) + 2U) #define sfSourceTag ((2U << 16U) + 3U) diff --git a/include/xrpl/protocol/EntropyTier.h b/include/xrpl/protocol/EntropyTier.h index 2ba45074d..3d8cf7fca 100644 --- a/include/xrpl/protocol/EntropyTier.h +++ b/include/xrpl/protocol/EntropyTier.h @@ -9,9 +9,12 @@ namespace ripple { /// on the ttCONSENSUS_ENTROPY pseudo-transaction and the ConsensusEntropy /// ledger entry. /// -/// EntropyCount says how many validators contributed; EntropyTier says which -/// gate the result passed. Values are strength-ordered so consumers can gate -/// with a numeric comparison (tier >= required). +/// EntropyCount says how many validators contributed; EntropyDenominator says +/// how many active validators were in the ledger-anchored view for that +/// non-fallback result; EntropyTier says which gate the result passed. Fallback +/// entropy carries count=0/denominator=0 because no validator-derived +/// denominator was accepted. Tier values are strength-ordered so consumers can +/// gate with a numeric comparison (tier >= required). /// /// RESIDUAL BIAS — applies to EVERY tier, including participant_aligned and /// validator_quorum, not just the fallback. This is a commit/reveal scheme: a diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 2bc45d29e..340e090ba 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -232,6 +232,7 @@ LEDGER_ENTRY(ltURI_TOKEN, 0x0055, URIToken, uri_token, ({ LEDGER_ENTRY_DUPLICATE(ltCONSENSUS_ENTROPY, 0x0058, ConsensusEntropy, consensus_entropy, ({ {sfDigest, soeREQUIRED}, {sfEntropyCount, soeREQUIRED}, + {sfEntropyDenominator, soeREQUIRED}, {sfEntropyTier, soeREQUIRED}, {sfLedgerSequence, soeREQUIRED}, {sfPreviousTxnID, soeREQUIRED}, diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index c153e5c47..839ad0a59 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -63,6 +63,7 @@ TYPED_SFIELD(sfHookStateScale, UINT16, 21) TYPED_SFIELD(sfLedgerFixType, UINT16, 22) TYPED_SFIELD(sfHookExportCount, UINT16, 98) TYPED_SFIELD(sfEntropyCount, UINT16, 99) +TYPED_SFIELD(sfEntropyDenominator, UINT16, 100) // 32-bit integers (common) TYPED_SFIELD(sfNetworkID, UINT32, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 3b6a97bc0..0f2f8b968 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -622,6 +622,7 @@ TRANSACTION(ttCONSENSUS_ENTROPY, 105, ConsensusEntropy, ({ {sfLedgerSequence, soeREQUIRED}, {sfDigest, soeREQUIRED}, {sfEntropyCount, soeREQUIRED}, + {sfEntropyDenominator, soeREQUIRED}, {sfEntropyTier, soeREQUIRED}, })) diff --git a/src/test/app/ConsensusEntropy_test.cpp b/src/test/app/ConsensusEntropy_test.cpp index 95166476a..60785a757 100644 --- a/src/test/app/ConsensusEntropy_test.cpp +++ b/src/test/app/ConsensusEntropy_test.cpp @@ -84,6 +84,7 @@ class ConsensusEntropy_test : public beast::unit_test::suite auto const count = sle->getFieldU16(sfEntropyCount); BEAST_EXPECT(count >= 5); + BEAST_EXPECT(sle->getFieldU16(sfEntropyDenominator) >= count); auto const sleSeq = sle->getFieldU32(sfLedgerSequence); BEAST_EXPECT(sleSeq == env.closed()->seq()); @@ -445,7 +446,8 @@ class ConsensusEntropy_test : public beast::unit_test::suite BEAST_REQUIRE(env.le(keylet::consensusEntropy())); - // Standalone entropy carries EntropyCount=20 / tier validator_quorum. + // Standalone entropy carries EntropyCount=20, + // EntropyDenominator=20, and tier validator_quorum. // 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. diff --git a/src/test/app/PseudoTx_test.cpp b/src/test/app/PseudoTx_test.cpp index fe06d50de..2818651e2 100644 --- a/src/test/app/PseudoTx_test.cpp +++ b/src/test/app/PseudoTx_test.cpp @@ -65,6 +65,7 @@ struct PseudoTx_test : public beast::unit_test::suite obj.setFieldAmount(sfFee, STAmount{}); obj.setFieldH256(sfDigest, uint256(3)); obj.setFieldU16(sfEntropyCount, 1); + obj.setFieldU16(sfEntropyDenominator, 1); obj.setFieldU8(sfEntropyTier, entropyTierValidatorQuorum); })); diff --git a/src/test/consensus/ConsensusExtensions_test.cpp b/src/test/consensus/ConsensusExtensions_test.cpp index 70f4f0491..b435ce149 100644 --- a/src/test/consensus/ConsensusExtensions_test.cpp +++ b/src/test/consensus/ConsensusExtensions_test.cpp @@ -48,6 +48,7 @@ #include #include #include +#include namespace ripple { namespace test { @@ -1238,6 +1239,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite BEAST_EXPECT(tx->getFieldH256(sfDigest) == expected); BEAST_EXPECT(tx->getFieldH256(sfDigest) != uint256{}); BEAST_EXPECT(tx->getFieldU16(sfEntropyCount) == 0); + BEAST_EXPECT(tx->getFieldU16(sfEntropyDenominator) == 0); BEAST_EXPECT( tx->getFieldU8(sfEntropyTier) == entropyTierConsensusFallback); @@ -1283,6 +1285,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite txSetHash, fallbackDigest, static_cast(entropyTierConsensusFallback), + static_cast(0), static_cast(0)); auto const salt = ce.txnOrderingSalt(txSetHash, seq); @@ -1351,6 +1354,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite BEAST_EXPECT( tx->getFieldH256(sfDigest) == expectedEntropy(publicKey, reveal)); BEAST_EXPECT(tx->getFieldU16(sfEntropyCount) == 1); + BEAST_EXPECT(tx->getFieldU16(sfEntropyDenominator) == 1); BEAST_EXPECT( tx->getFieldU8(sfEntropyTier) == entropyTierValidatorQuorum); } @@ -1420,6 +1424,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite BEAST_EXPECT( tx->getFieldH256(sfDigest) == expectedEntropy(publicKey, reveal)); BEAST_EXPECT(tx->getFieldU16(sfEntropyCount) == 1); + BEAST_EXPECT(tx->getFieldU16(sfEntropyDenominator) == 1); BEAST_EXPECT( tx->getFieldU8(sfEntropyTier) == entropyTierValidatorQuorum); } @@ -1476,7 +1481,8 @@ class ConsensusExtensions_test : public beast::unit_test::suite auto const txSetHash = makeHash("tier2-txset"); // Harvest commit+reveal from `revealers` of the 6 validators, build the - // agreed entropy set, inject, and return the labelled (tier, count). + // agreed entropy set, inject, and return the labelled + // (tier, count, denominator). auto runWith = [&](std::size_t revealers) { ConsensusExtensions ce{env.app(), activeNoopJournal()}; ce.cacheUNLReport(viewLedger); @@ -1501,28 +1507,32 @@ class ConsensusExtensions_test : public beast::unit_test::suite CanonicalTXSet txs{makeHash("tier2-salt")}; ce.onPreBuild(txs, seq, txSetHash); auto const tx = singleCanonicalTx(txs); - std::pair out{-1, 0}; + std::tuple out{-1, 0, 0}; if (tx) out = { tx->getFieldU8(sfEntropyTier), - tx->getFieldU16(sfEntropyCount)}; + tx->getFieldU16(sfEntropyCount), + tx->getFieldU16(sfEntropyDenominator)}; return out; }; // 5 of 6 aligned -> validator_quorum (count >= quorum 5). auto const q = runWith(5); - BEAST_EXPECT(q.first == entropyTierValidatorQuorum); - BEAST_EXPECT(q.second == 5); + BEAST_EXPECT(std::get<0>(q) == entropyTierValidatorQuorum); + BEAST_EXPECT(std::get<1>(q) == 5); + BEAST_EXPECT(std::get<2>(q) == kValidators); // 4 of 6 aligned -> participant_aligned (count >= tier2 4, < quorum 5). auto const p = runWith(4); - BEAST_EXPECT(p.first == entropyTierParticipantAligned); - BEAST_EXPECT(p.second == 4); + BEAST_EXPECT(std::get<0>(p) == entropyTierParticipantAligned); + BEAST_EXPECT(std::get<1>(p) == 4); + BEAST_EXPECT(std::get<2>(p) == kValidators); // 3 of 6 aligned -> below the tier-2 floor -> consensus_fallback. auto const f = runWith(3); - BEAST_EXPECT(f.first == entropyTierConsensusFallback); - BEAST_EXPECT(f.second == 0); + BEAST_EXPECT(std::get<0>(f) == entropyTierConsensusFallback); + BEAST_EXPECT(std::get<1>(f) == 0); + BEAST_EXPECT(std::get<2>(f) == 0); } void @@ -1669,25 +1679,29 @@ class ConsensusExtensions_test : public beast::unit_test::suite CanonicalTXSet txs{makeHash("tier2-nunl-salt")}; ce.onPreBuild(txs, seq, txSetHash); auto const tx = singleCanonicalTx(txs); - std::pair out{-1, 0}; + std::tuple out{-1, 0, 0}; if (tx) out = { tx->getFieldU8(sfEntropyTier), - tx->getFieldU16(sfEntropyCount)}; + tx->getFieldU16(sfEntropyCount), + tx->getFieldU16(sfEntropyDenominator)}; return out; }; auto const q = runWith(16); - BEAST_EXPECT(q.first == entropyTierValidatorQuorum); - BEAST_EXPECT(q.second == 16); + BEAST_EXPECT(std::get<0>(q) == entropyTierValidatorQuorum); + BEAST_EXPECT(std::get<1>(q) == 16); + BEAST_EXPECT(std::get<2>(q) == kOriginal - kDisabled); auto const p = runWith(15); - BEAST_EXPECT(p.first == entropyTierParticipantAligned); - BEAST_EXPECT(p.second == 15); + BEAST_EXPECT(std::get<0>(p) == entropyTierParticipantAligned); + BEAST_EXPECT(std::get<1>(p) == 15); + BEAST_EXPECT(std::get<2>(p) == kOriginal - kDisabled); auto const f = runWith(12); - BEAST_EXPECT(f.first == entropyTierConsensusFallback); - BEAST_EXPECT(f.second == 0); + BEAST_EXPECT(std::get<0>(f) == entropyTierConsensusFallback); + BEAST_EXPECT(std::get<1>(f) == 0); + BEAST_EXPECT(std::get<2>(f) == 0); } void @@ -2321,6 +2335,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite tx->getFieldH256(sfDigest) == sha512Half(std::string("standalone-entropy"), seq)); BEAST_EXPECT(tx->getFieldU16(sfEntropyCount) == 20); + BEAST_EXPECT(tx->getFieldU16(sfEntropyDenominator) == 20); BEAST_EXPECT( tx->getFieldU8(sfEntropyTier) == entropyTierValidatorQuorum); @@ -2353,6 +2368,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite obj.setFieldVL(sfSigningPubKey, Slice{}); // pseudo-tx convention obj.setFieldH256(sfDigest, digest); obj.setFieldU16(sfEntropyCount, count); + obj.setFieldU16(sfEntropyDenominator, count); obj.setFieldU8(sfEntropyTier, entropyTierConsensusFallback); return std::make_shared(makeSTTx(obj)); }; diff --git a/src/test/consensus/ConsensusRng_test.cpp b/src/test/consensus/ConsensusRng_test.cpp index 3b9721bd6..366aff648 100644 --- a/src/test/consensus/ConsensusRng_test.cpp +++ b/src/test/consensus/ConsensusRng_test.cpp @@ -66,6 +66,8 @@ public: { BEAST_EXPECT(!peer->ce().lastEntropyWasFallback_); BEAST_EXPECT(peer->ce().lastEntropyCount_ > 0); + BEAST_EXPECT( + peer->ce().lastEntropyDenominator_ == peers.size()); BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{}); } } @@ -908,6 +910,7 @@ public: peer->ce().lastEntropyDigest_ == peers[0]->ce().lastEntropyDigest_); BEAST_EXPECT(peer->ce().lastEntropyCount_ == 0); + BEAST_EXPECT(peer->ce().lastEntropyDenominator_ == 0); } } @@ -981,6 +984,7 @@ public: BEAST_EXPECT( peer->ce().lastEntropyTier_ == entropyTierConsensusFallback); BEAST_EXPECT(peer->ce().lastEntropyCount_ == 0); + BEAST_EXPECT(peer->ce().lastEntropyDenominator_ == 0); BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{}); } BEAST_EXPECT( @@ -1038,12 +1042,14 @@ public: BEAST_EXPECT( peers[i]->ce().lastEntropyTier_ == entropyTierValidatorQuorum); BEAST_EXPECT(peers[i]->ce().lastEntropyCount_ == 4); + BEAST_EXPECT(peers[i]->ce().lastEntropyDenominator_ == 5); } BEAST_EXPECT(peers[0]->ce().lastEntropyWasFallback_); BEAST_EXPECT( peers[0]->ce().lastEntropyTier_ == entropyTierConsensusFallback); BEAST_EXPECT(peers[0]->ce().lastEntropyCount_ == 0); + BEAST_EXPECT(peers[0]->ce().lastEntropyDenominator_ == 0); BEAST_EXPECT(peers[0]->ce().lastEntropyDigest_ != cohortDigest); } diff --git a/src/test/csf/Peer.h b/src/test/csf/Peer.h index c74a91627..5e0b6f9c8 100644 --- a/src/test/csf/Peer.h +++ b/src/test/csf/Peer.h @@ -352,6 +352,7 @@ struct Peer // Last round summary (for test assertions) uint256 lastEntropyDigest_; std::uint16_t lastEntropyCount_ = 0; + std::uint16_t lastEntropyDenominator_ = 0; bool lastEntropyWasFallback_ = true; EntropyTier lastEntropyTier_ = entropyTierNone; bool lastExportSucceeded_ = false; @@ -819,6 +820,7 @@ struct Peer // zero digest as the "none" marker. lastEntropyDigest_.zero(); lastEntropyCount_ = 0; + lastEntropyDenominator_ = 0; lastEntropyWasFallback_ = true; lastEntropyTier_ = entropyTierNone; return; @@ -829,6 +831,7 @@ struct Peer auto const fallback = [&] { lastEntropyDigest_ = fallbackEntropy(); lastEntropyCount_ = 0; + lastEntropyDenominator_ = 0; lastEntropyWasFallback_ = true; lastEntropyTier_ = entropyTierConsensusFallback; }; @@ -902,6 +905,8 @@ struct Peer } lastEntropyDigest_ = digest; lastEntropyCount_ = static_cast(count); + lastEntropyDenominator_ = + static_cast(unlNodes_.size()); lastEntropyWasFallback_ = false; } diff --git a/src/xrpld/app/consensus/ConsensusEntropyIntent.md b/src/xrpld/app/consensus/ConsensusEntropyIntent.md index df28d4ba2..1f1f0961f 100644 --- a/src/xrpld/app/consensus/ConsensusEntropyIntent.md +++ b/src/xrpld/app/consensus/ConsensusEntropyIntent.md @@ -25,11 +25,12 @@ determinism or liveness.** **INV-1 — Determinism of the injected object.** Given the same parent ledger and the same *agreed* entropy sidecar, every honest -node injects the byte-identical `ttCONSENSUS_ENTROPY` (digest, tier, count). That +node injects the byte-identical `ttCONSENSUS_ENTROPY` (digest, tier, count, +denominator). That object is ledger state. Therefore **non-fallback entropy must not read mutable local collector state or timing-derived state.** The selector derives non-fallback -`(digest, tier, count)` only from the accepted `entropySetMap_` (matched to the -hash the gate accepted) plus the parent-ledger active view. Local timeout or +`(digest, tier, count, denominator)` only from the accepted `entropySetMap_` +(matched to the hash the gate accepted) plus the parent-ledger active view. Local timeout or diagnostic state such as `entropyFailed_` must not override an accepted root at injection time; a node that never accepts a root falls back through the normal missing-accepted-root path. diff --git a/src/xrpld/app/consensus/ConsensusExtensions.cpp b/src/xrpld/app/consensus/ConsensusExtensions.cpp index cb1e305ef..65d4f03b1 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.cpp +++ b/src/xrpld/app/consensus/ConsensusExtensions.cpp @@ -782,6 +782,7 @@ ConsensusExtensions::selectEntropy( agreedTxSetHash, seq), entropyTierConsensusFallback, + 0, 0}; }; //@@end entropy-selector-fallback @@ -792,6 +793,7 @@ ConsensusExtensions::selectEntropy( return { sha512Half(std::string("standalone-entropy"), seq), entropyTierValidatorQuorum, + 20, 20}; //@@end entropy-selector-standalone @@ -836,9 +838,9 @@ ConsensusExtensions::selectEntropy( // Derive from the AGREED entropySetMap_ — NOT local pendingReveals_. The // map's hash was published in proposals and accepted by the gate, so every // node holding the same entropySetHash produces byte-identical entropy and - // the same tier/count. Read leaves through the shared sidecar admission - // helper so accepted-map consumption enforces the same content-address/type - // contract as snapshot construction. + // the same tier/count/denominator labels. Read leaves through the shared + // sidecar admission helper so accepted-map consumption enforces the same + // content-address/type contract as snapshot construction. std::vector> sorted; entropySetMap_->visitLeaves( [&](boost::intrusive_ptr const& item) { @@ -887,6 +889,7 @@ ConsensusExtensions::selectEntropy( } auto const digest = sha512Half(s.slice()); auto const count = static_cast(sorted.size()); + auto const denominator = static_cast(validatorView->size()); //@@start entropy-selector-tier-ladder // Tier ladder over the AGREED participant count — deterministic on every @@ -901,7 +904,7 @@ ConsensusExtensions::selectEntropy( validatorView->size(), validatorView->originalViewSize); if (tier != entropyTierConsensusFallback) - return {digest, static_cast(tier), count}; + return {digest, static_cast(tier), count, denominator}; return fallback(); //@@end entropy-selector-tier-ladder } @@ -926,7 +929,8 @@ ConsensusExtensions::txnOrderingSalt( agreedTxSetHash, selection.digest, selection.tier, - selection.count); + selection.count, + selection.denominator); } bool @@ -1711,19 +1715,21 @@ ConsensusExtensions::onPreBuild( //@@start rng-inject-entropy-selection // One deterministic selector over the AGREED entropySetMap_ chooses the - // digest and its tier/count. Every node derives the same entropy for - // the same agreed round inputs. txSetHash is the agreed pre-injection - // consensus tx set hash. + // digest and its tier/count/denominator labels. Every node derives the + // same entropy for the same agreed round inputs. txSetHash is the + // agreed pre-injection consensus tx set hash. auto const selection = selectEntropy(txSetHash, seq); uint256 const finalEntropy = selection.digest; std::uint8_t const entropyTier = selection.tier; std::uint16_t const entropyCount = selection.count; + std::uint16_t const entropyDenominator = selection.denominator; //@@end rng-inject-entropy-selection JLOG(j_.info()) << "RNG: entropy selected" << " seq=" << seq << " tier=" << static_cast(entropyTier) << " count=" << entropyCount + << " denominator=" << entropyDenominator << " digest=" << finalEntropy; //@@start rng-inject-pseudotx @@ -1755,6 +1761,7 @@ ConsensusExtensions::onPreBuild( obj.setFieldAmount(sfFee, STAmount{}); obj.setFieldH256(sfDigest, finalEntropy); obj.setFieldU16(sfEntropyCount, entropyCount); + obj.setFieldU16(sfEntropyDenominator, entropyDenominator); obj.setFieldU8(sfEntropyTier, entropyTier); }); @@ -1820,6 +1827,11 @@ ConsensusExtensions::onPreBuild( << (pres.isFieldPresent(sfEntropyCount) ? std::to_string( pres.getFieldU16(sfEntropyCount)) + : std::string{""}) + << " presentDenominator=" + << (pres.isFieldPresent(sfEntropyDenominator) + ? std::to_string( + pres.getFieldU16(sfEntropyDenominator)) : std::string{""}); } } diff --git a/src/xrpld/app/consensus/ConsensusExtensions.h b/src/xrpld/app/consensus/ConsensusExtensions.h index 38c2eae4a..d328f0f6e 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.h +++ b/src/xrpld/app/consensus/ConsensusExtensions.h @@ -257,13 +257,15 @@ public: clearAcceptedEntropySet(); /// Result of the shared deterministic entropy selector: the digest to - /// inject plus its tier/count labels. Both injection paths derive these - /// identically from the AGREED entropySetMap_ so they cannot drift. + /// inject plus its tier/count/denominator labels. Both injection paths + /// derive these identically from the AGREED entropySetMap_ so they cannot + /// drift. struct EntropySelection { uint256 digest; std::uint8_t tier = 0; // EntropyTier; the selector always sets this std::uint16_t count = 0; + std::uint16_t denominator = 0; }; /// Deterministically choose the entropy to inject for this round from the diff --git a/src/xrpld/app/consensus/ConsensusExtensionsDesign.md b/src/xrpld/app/consensus/ConsensusExtensionsDesign.md index e3383bad3..0bfed93b5 100644 --- a/src/xrpld/app/consensus/ConsensusExtensionsDesign.md +++ b/src/xrpld/app/consensus/ConsensusExtensionsDesign.md @@ -347,8 +347,11 @@ non-UNLReport (config-fallback) view, every case below instead mints The fallback pseudo-transaction is deterministic — every node derives the same digest from `(HashPrefix::entropyFallback, parentLedgerHash, agreedTxSetHash, -seq)` — and labeled with `EntropyTier = consensus_fallback` and -`EntropyCount = 0`. Hooks state their own requirements via the required +seq)` — and labeled with `EntropyTier = consensus_fallback`, +`EntropyCount = 0`, and `EntropyDenominator = 0`. Non-fallback entropy records +both the contributor count and the active-validator denominator used for the +validator-quorum threshold; the participant-aligned floor still uses the +original pre-NegativeUNL view internally. Hooks state their own requirements via the required `min_tier`/`min_count` arguments to `dice()`/`random()`: a hook that demands validator-tier entropy fails closed with `TOO_LITTLE_ENTROPY` on fallback ledgers, while a hook that opts into fallback-grade randomness must do so diff --git a/src/xrpld/app/tx/detail/Change.cpp b/src/xrpld/app/tx/detail/Change.cpp index f28c40144..2e16f2bc9 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -309,6 +309,8 @@ Change::applyConsensusEntropy() sle->setFieldH256(sfDigest, entropy); sle->setFieldU16(sfEntropyCount, ctx_.tx.getFieldU16(sfEntropyCount)); + sle->setFieldU16( + sfEntropyDenominator, ctx_.tx.getFieldU16(sfEntropyDenominator)); sle->setFieldU8(sfEntropyTier, ctx_.tx.getFieldU8(sfEntropyTier)); sle->setFieldU32(sfLedgerSequence, view().info().seq); // Note: sfPreviousTxnID and sfPreviousTxnLgrSeq are set automatically