From 64cfdae9e96c32dcbf2c08430334cd6c091ab43c Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Fri, 19 Dec 2025 22:54:35 +1100 Subject: [PATCH 01/11] first part of featRNG --- src/ripple/app/ledger/impl/BuildLedger.cpp | 40 +++++++++ src/ripple/app/misc/impl/TxQ.cpp | 98 ++++++++++++++++++++++ src/ripple/app/tx/impl/Change.cpp | 3 + src/ripple/app/tx/impl/applySteps.cpp | 4 + src/ripple/protocol/Feature.h | 3 +- src/ripple/protocol/SField.h | 1 + src/ripple/protocol/TxFormats.h | 4 + src/ripple/protocol/impl/Feature.cpp | 1 + src/ripple/protocol/impl/SField.cpp | 1 + 9 files changed, 154 insertions(+), 1 deletion(-) diff --git a/src/ripple/app/ledger/impl/BuildLedger.cpp b/src/ripple/app/ledger/impl/BuildLedger.cpp index 56feda0667..203c450b45 100644 --- a/src/ripple/app/ledger/impl/BuildLedger.cpp +++ b/src/ripple/app/ledger/impl/BuildLedger.cpp @@ -103,6 +103,46 @@ applyTransactions( bool certainRetry = true; std::size_t count = 0; + if (view.rules.enabled(featureRNG)) + { + // apply the ttRNG txns first in the ledger to ensure no one can predict the outcome + for (it = txns.begin(); it != txns.end();) + { + if (it->second->getFieldU16(sfTransactionType) != ttRNG) + { + ++it; + continue; + } + + try + { + switch (applyTransaction( + app, view, *it->second, certainRetry, tapNONE, j)) + { + case ApplyResult::Success: + it = txns.erase(it); + ++count; + break; + + case ApplyResult::Fail: + failed.insert(txid); + it = txns.erase(it); + break; + + case ApplyResult::Retry: + ++it; + } + } + catch (std::exception const& ex) + { + JLOG(j.warn()) + << "Transaction " << txid << " throws: " << ex.what(); + failed.insert(txid); + it = txns.erase(it); + } + } + } + // Attempt to apply all of the retriable transactions for (int pass = 0; pass < LEDGER_TOTAL_PASSES; ++pass) { diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index 5c4af9f8bc..ee37c02c2d 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -1477,6 +1477,104 @@ TxQ::accept(Application& app, OpenView& view) } } + // Inject an RNG psuedo if we're on the UNL + if (view.rules().enabled(featureRNG)) + { + do + { + // if we're not a validator we do nothing here + if (app.getValidationPublicKey().empty()) + break; + + auto const& keys = app.getValidatorKeys(); + + if (keys.configInvalid()) + break; + + // and if we're not on the UNLReport we also do nothing + + auto const unlRep = view.read(keylet::UNLReport()); + if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators)) + { + // nothing to do without a unlreport object + break; + } + + bool found = false; + auto const& avs = unlRep->getFieldArray(sfActiveValidators); + for (auto const& av : avs) + { + if (PublicKey(av[sfPublicKey]) == keys.masterPublicKey) + { + found = true; + break; + } + } + + if (!found) + break; + + auto const seq = view.info().seq; + + AccountID acc = calcAccountID(keys.masterPublicKey); + + static auto getRnd = []() -> uint256 { + static std::ifstream rng("/dev/urandom", std::ios::binary); + uint256 out; + if (rng && rng.read(reinterpret_cast(out.data()), 32)) + return out; + std::random_device rd; + for (auto& word : out) + word = rd(); + return out; + }; + + static std::map rngMap; + + uint256 nextRnd = getRnd(); + + if (rngMap.find(seq) != rngMap.end()) + break; // should never happen + + rngMap[seq] = nextRnd; + + std::optional prevRnd; + + if (rngMap.find(seq - 1)) + prevRnd = rngMap[seq - 1]; + + // amortized cleanup, for every ledger attempt to delete two old entries + // even in the most desynced ridiculous state this is guaranteed prevent map growth + for (int i = 0; i < 2; ++i) + { + auto it = rngMap.begin(); + if (it != rngMap.end() && it->first < seq) + rngMap.erase(it->first); + } + + // create txn + STTx rngTx(ttRNG, [&](auto& obj) { + obj.setFieldU32(sfLedgerSequence, seq); + obj.setAccountID(sfValidator, acc); + if (prevRnd.has_value()) + obj.setFieldH256(sfLastSolution, *prevRnd); + obj.setFieldH256(sfDigest, sha512Half(nextRnd); + }); + + // submit to the ledger + { + uint256 txID = rngTx.getTransactionID(); + auto s = std::make_shared(); + exportTx.add(*s); + app.getHashRouter().setFlags(txID, SF_PRIVATE2); + app.getHashRouter().setFlags(txID, SF_EMITTED); + view.rawTxInsert(txID, std::move(s), nullptr); + ledgerChanged = true; + } + + } while (0); + } + // Inject cron transactions, if any if (view.rules().enabled(featureCron)) { diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 37a436feea..95d9253132 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -154,6 +154,7 @@ Change::preclaim(PreclaimContext const& ctx) case ttAMENDMENT: case ttUNL_MODIFY: case ttEMIT_FAILURE: + case ttRNG: return tesSUCCESS; case ttUNL_REPORT: { if (!ctx.tx.isFieldPresent(sfImportVLKey) || @@ -209,6 +210,8 @@ Change::doApply() return applyEmitFailure(); case ttUNL_REPORT: return applyUNLReport(); + case ttRNG: + return applyRNG(); default: assert(0); return tefFAILURE; diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index a18f78e1c2..50818caf89 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -152,6 +152,7 @@ invoke_preflight(PreflightContext const& ctx) case ttUNL_MODIFY: case ttUNL_REPORT: case ttEMIT_FAILURE: + case ttRNG: return invoke_preflight_helper(ctx); case ttHOOK_SET: return invoke_preflight_helper(ctx); @@ -283,6 +284,7 @@ invoke_preclaim(PreclaimContext const& ctx) case ttUNL_MODIFY: case ttUNL_REPORT: case ttEMIT_FAILURE: + case ttRNG: return invoke_preclaim(ctx); case ttNFTOKEN_MINT: return invoke_preclaim(ctx); @@ -374,6 +376,7 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) case ttUNL_MODIFY: case ttUNL_REPORT: case ttEMIT_FAILURE: + case ttRNG: return Change::calculateBaseFee(view, tx); case ttNFTOKEN_MINT: return NFTokenMint::calculateBaseFee(view, tx); @@ -544,6 +547,7 @@ invoke_apply(ApplyContext& ctx) case ttFEE: case ttUNL_MODIFY: case ttUNL_REPORT: + case ttRNG: case ttEMIT_FAILURE: { Change p(ctx); return p(); diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 2766859dc7..24436d1c32 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 90; +static constexpr std::size_t numFeatures = 91; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -378,6 +378,7 @@ extern uint256 const fixInvalidTxFlags; extern uint256 const featureExtendedHookState; extern uint256 const fixCronStacking; extern uint256 const fixHookAPI20251128; +extern uint256 const featureRNG; } // namespace ripple #endif diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index f507b22326..fbf181c578 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -563,6 +563,7 @@ extern SF_ACCOUNT const sfEmitCallback; extern SF_ACCOUNT const sfHookAccount; extern SF_ACCOUNT const sfNFTokenMinter; extern SF_ACCOUNT const sfInform; +extern SF_ACCOUNT const sfValidator; // path set extern SField const sfPaths; diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index bea5905cbf..54ec86e6d3 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -149,6 +149,10 @@ enum TxType : std::uint16_t ttURITOKEN_CREATE_SELL_OFFER = 48, ttURITOKEN_CANCEL_SELL_OFFER = 49, + /* A pseudo-txn used by featureRNG which allows validators to submit blinded entropy + * to a consensus based random number system */ + ttRNG = 89, + /* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */ ttCRON = 92, diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 24383f896c..ca4236c9d1 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -484,6 +484,7 @@ REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::De REGISTER_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixCronStacking, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixHookAPI20251128, Supported::yes, VoteBehavior::DefaultYes); +REGISTER_FEATURE(RNG, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index c4f2ef85a5..4c9c8023a8 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -316,6 +316,7 @@ CONSTRUCT_TYPED_SFIELD(sfEmitCallback, "EmitCallback", ACCOUNT, // account (uncommon) CONSTRUCT_TYPED_SFIELD(sfHookAccount, "HookAccount", ACCOUNT, 16); CONSTRUCT_TYPED_SFIELD(sfInform, "Inform", ACCOUNT, 99); +CONSTRUCT_TYPED_SFIELD(sfValidator, "Validator", ACCOUNT, 98); // vector of 256-bit CONSTRUCT_TYPED_SFIELD(sfIndexes, "Indexes", VECTOR256, 1, SField::sMD_Never); From e0fe289b5240f96dcaadafaaed243bf661f24549 Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Sun, 21 Dec 2025 14:41:16 +1100 Subject: [PATCH 02/11] code complete, compiling untested --- src/ripple/app/ledger/impl/BuildLedger.cpp | 6 +- src/ripple/app/misc/impl/TxQ.cpp | 20 ++- src/ripple/app/tx/impl/Change.cpp | 125 ++++++++++++++++++ src/ripple/app/tx/impl/Change.h | 3 + src/ripple/protocol/Indexes.h | 3 + src/ripple/protocol/LedgerFormats.h | 7 + src/ripple/protocol/SField.h | 6 + src/ripple/protocol/impl/Indexes.cpp | 9 ++ .../protocol/impl/InnerObjectFormats.cpp | 8 ++ src/ripple/protocol/impl/LedgerFormats.cpp | 14 ++ src/ripple/protocol/impl/SField.cpp | 6 + src/ripple/protocol/impl/STTx.cpp | 3 +- src/ripple/protocol/impl/TxFormats.cpp | 10 ++ src/ripple/protocol/jss.h | 2 + 14 files changed, 207 insertions(+), 15 deletions(-) diff --git a/src/ripple/app/ledger/impl/BuildLedger.cpp b/src/ripple/app/ledger/impl/BuildLedger.cpp index 203c450b45..85ed72106b 100644 --- a/src/ripple/app/ledger/impl/BuildLedger.cpp +++ b/src/ripple/app/ledger/impl/BuildLedger.cpp @@ -25,6 +25,7 @@ #include #include #include +#include namespace ripple { @@ -103,10 +104,10 @@ applyTransactions( bool certainRetry = true; std::size_t count = 0; - if (view.rules.enabled(featureRNG)) + if (view.rules().enabled(featureRNG)) { // apply the ttRNG txns first in the ledger to ensure no one can predict the outcome - for (it = txns.begin(); it != txns.end();) + for (auto it = txns.begin(); it != txns.end();) { if (it->second->getFieldU16(sfTransactionType) != ttRNG) { @@ -114,6 +115,7 @@ applyTransactions( continue; } + auto const txid = it->first.getTXID(); try { switch (applyTransaction( diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index ee37c02c2d..978b6c2bf0 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -30,6 +30,7 @@ #include #include #include +#include namespace ripple { @@ -1486,13 +1487,7 @@ TxQ::accept(Application& app, OpenView& view) if (app.getValidationPublicKey().empty()) break; - auto const& keys = app.getValidatorKeys(); - - if (keys.configInvalid()) - break; - // and if we're not on the UNLReport we also do nothing - auto const unlRep = view.read(keylet::UNLReport()); if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators)) { @@ -1504,7 +1499,7 @@ TxQ::accept(Application& app, OpenView& view) auto const& avs = unlRep->getFieldArray(sfActiveValidators); for (auto const& av : avs) { - if (PublicKey(av[sfPublicKey]) == keys.masterPublicKey) + if (PublicKey(av[sfPublicKey]) == app.getValidationPublicKey()) { found = true; break; @@ -1516,7 +1511,7 @@ TxQ::accept(Application& app, OpenView& view) auto const seq = view.info().seq; - AccountID acc = calcAccountID(keys.masterPublicKey); + AccountID acc = calcAccountID(app.getValidationPublicKey()); static auto getRnd = []() -> uint256 { static std::ifstream rng("/dev/urandom", std::ios::binary); @@ -1540,7 +1535,7 @@ TxQ::accept(Application& app, OpenView& view) std::optional prevRnd; - if (rngMap.find(seq - 1)) + if (rngMap.find(seq - 1) != rngMap.end()) prevRnd = rngMap[seq - 1]; // amortized cleanup, for every ledger attempt to delete two old entries @@ -1557,15 +1552,16 @@ TxQ::accept(Application& app, OpenView& view) obj.setFieldU32(sfLedgerSequence, seq); obj.setAccountID(sfValidator, acc); if (prevRnd.has_value()) - obj.setFieldH256(sfLastSolution, *prevRnd); - obj.setFieldH256(sfDigest, sha512Half(nextRnd); + obj.setFieldH256(sfRandomData, *prevRnd); + obj.setFieldH256(sfNextRandomDigest, sha512Half(nextRnd)); + // RH TODO: should we sign this?? }); // submit to the ledger { uint256 txID = rngTx.getTransactionID(); auto s = std::make_shared(); - exportTx.add(*s); + rngTx.add(*s); app.getHashRouter().setFlags(txID, SF_PRIVATE2); app.getHashRouter().setFlags(txID, SF_EMITTED); view.rawTxInsert(txID, std::move(s), nullptr); diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 95d9253132..38289c125f 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -96,6 +96,12 @@ Change::preflight(PreflightContext const& ctx) } } + if (ctx.tx.getTxnType() == ttRNG && !ctx.rules.enabled(featureRNG)) + { + JLOG(ctx.j.warn()) << "Change: FeatureRNG is not enabled."; + return temDISABLED; + } + return tesSUCCESS; } @@ -218,6 +224,125 @@ Change::doApply() } } +TER +Change::applyRNG() +{ + + auto const seq = view().info().seq; + + if (seq != ctx_.tx.getFieldU32(sfLedgerSequence)) + { + JLOG(j_.warn()) << "Change: ttRNG, wrong ledger seq=" << seq; + return tefFAILURE; + } + + auto sle = view().peek(keylet::random()); + + bool const created = !sle; + + if (created) + { + sle = std::make_shared(keylet::random()); + } + + auto lastSeq = created ? 0 : sle->getFieldU32(sfLedgerSequence); + + if (lastSeq < seq) + { + // update the ledger sequence of the object + sle->setFieldU32(sfLedgerSequence, seq); + + // reset entropy count to zero... this will probably be + // one after the below executes but its possible the digest + // doesn't match and the entropy count isn't incremented + sle->setFieldU16(sfEntropyCount, 0); + + // swap the random data out ready for this round of entropy collection + sle->setFieldH256(sfLastRandomData, sle->getFieldH256(sfRandomData)); + sle->setFieldH256(sfRandomData, beast::zero); + } + + uint256 nextDigest = ctx_.tx.getFieldH256(sfNextRandomDigest); + uint256 currentEntropy = ctx_.tx.getFieldH256(sfRandomData); + uint256 currentDigest = sha512Half(currentEntropy); + + AccountID const validator = ctx_.tx.getAccountID(sfValidator); + + // RH TODO: check if they're on the UNLReport and ignore if not + + // iterate the digest array to find the entry if it exists + STArray digestEntries = sle->getFieldArray(sfRandomDigests); + std::map entries; + + for (auto& entry : digestEntries) + { + // we'll automatically clean up really old entries by just omitting them from + // the map here + if (entry.getFieldU32(sfLedgerSequence) < seq - 5) + continue; + + entries.emplace(entry.getAccountID(sfValidator), std::move(entry)); + } + + if (auto it = entries.find(validator); it != entries.end()) + { + auto& entry = it->second; + + // ensure the precommitted digest matches the provided entropy + if (entry.getFieldH256(sfNextRandomDigest) != currentDigest) + { + if (entry.getFieldU32(sfLedgerSequence) != seq - 1) + { + // this is a skip-ahead or missed last txn somehow, so ignore, but no warning. + } + else + { + // this is a clear violation so warn (and ignore the entropy) + JLOG(j_.warn()) << "!!! Validator " << validator << " supplied entropy that " + << "does not match precommitment value !!!"; + } + } + else + { + + // contribute the new entropy to the random data field + sle->setFieldH256(sfRandomData, sha512Half(validator, sle->getFieldH256(sfRandomData), currentEntropy)); + + // increment entropy count + sle->setFieldU16(sfEntropyCount, sle->getFieldU16(sfEntropyCount) + 1); + } + + // update the digest entry + entry.setFieldH256(sfNextRandomDigest, nextDigest); + entry.setFieldU32(sfLedgerSequence, seq); + } + else + { + // this validator doesn't have an entry so create one + STObject entry{sfRandomDigestEntry}; + entry.setAccountID(sfValidator, validator); + entry.setFieldH256(sfNextRandomDigest, nextDigest); + entry.setFieldU32(sfLedgerSequence, seq); + entries.emplace(validator, std::move(entry)); + } + + // update the array + STArray newEntries(sfRandomDigests); + newEntries.reserve(entries.size()); + for (auto& [_, entry] : entries) + newEntries.push_back(std::move(entry)); + + sle->setFieldArray(sfRandomDigests, std::move(newEntries)); + + // send it off to the ledger + if (!created) + view().update(sle); + else + view().insert(sle); + + return tesSUCCESS; +} + TER Change::applyUNLReport() { diff --git a/src/ripple/app/tx/impl/Change.h b/src/ripple/app/tx/impl/Change.h index 20c701f5ce..4df511ca67 100644 --- a/src/ripple/app/tx/impl/Change.h +++ b/src/ripple/app/tx/impl/Change.h @@ -76,6 +76,9 @@ class Change : public Transactor TER applyUNLReport(); + + TER + applyRNG(); }; } // namespace ripple diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 03f9dc2a42..b52c8f3fad 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -51,6 +51,9 @@ class SeqProxy; */ namespace keylet { +Keylet const& +random() noexcept; + /** The (fixed) index of the object containing the emitted txns for the ledger. */ Keylet const& diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index f87f7ae3f0..1e6e248481 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -260,6 +260,13 @@ enum LedgerEntryType : std::uint16_t \sa keylet::emitted */ ltEMITTED_TXN = 'E', + + + /** A ledger object containing a consensus-generated random number, operated on by ttRNG + + \sa keylet::rng + */ + ltRANDOM = 0x526EU, // Rn }; // clang-format off diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index fbf181c578..bff308f3a7 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; @@ -491,6 +492,9 @@ extern SF_UINT256 const sfGovernanceFlags; extern SF_UINT256 const sfGovernanceMarks; extern SF_UINT256 const sfEmittedTxnID; extern SF_UINT256 const sfCron; +extern SF_UINT256 const sfRandomData; +extern SF_UINT256 const sfLastRandomData; +extern SF_UINT256 const sfNextRandomDigest; // currency amount (common) extern SF_AMOUNT const sfAmount; @@ -606,6 +610,7 @@ extern SField const sfHookEmission; extern SField const sfMintURIToken; extern SField const sfAmountEntry; extern SField const sfRemark; +extern SField const sfRandomDigestEntry; // array of objects (common) // ARRAY/1 is reserved for end of array @@ -635,6 +640,7 @@ extern SField const sfImportVLKeys; extern SField const sfHookEmissions; extern SField const sfAmounts; extern SField const sfRemarks; +extern SField const sfRandomDigests; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index f75faf1653..eb00941bb4 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -73,6 +73,7 @@ enum class LedgerNameSpace : std::uint16_t { IMPORT_VLSEQ = 'I', UNL_REPORT = 'R', CRON = 'L', + RANDOM = 0x526E, // Rn // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -496,6 +497,14 @@ cron(uint32_t timestamp, std::optional const& id) return {ltCRON, uint256::fromVoid(h)}; } +Keylet const& +random() noexcept +{ + static Keylet const ret{ + ltRANDOM, indexHash(LedgerNameSpace::RANDOM)}; + return ret; +} + } // namespace keylet } // namespace ripple diff --git a/src/ripple/protocol/impl/InnerObjectFormats.cpp b/src/ripple/protocol/impl/InnerObjectFormats.cpp index 4c2500ff21..cf0aec7703 100644 --- a/src/ripple/protocol/impl/InnerObjectFormats.cpp +++ b/src/ripple/protocol/impl/InnerObjectFormats.cpp @@ -167,6 +167,14 @@ InnerObjectFormats::InnerObjectFormats() {sfRemarkValue, soeOPTIONAL}, {sfFlags, soeOPTIONAL}, }); + + add(sfRandomDigestEntry.jsonName.c_str(), + sfRandomDigestEntry.getCode(), + { + {sfValidator, soeREQUIRED}, + {sfNextRandomDigest, soeREQUIRED}, + {sfLedgerSequence, soeREQUIRED}, + }); } InnerObjectFormats const& diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 5f51976be3..3dbae56a1d 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -381,6 +381,20 @@ LedgerFormats::LedgerFormats() }, commonFields); + add(jss::Random, + ltRANDOM, + { + {sfRandomData, soeREQUIRED}, + {sfLastRandomData, soeREQUIRED}, + {sfEntropyCount, soeREQUIRED}, + {sfRandomDigests, soeREQUIRED}, + {sfLedgerSequence, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + }, + commonFields); + + // clang-format on } diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 4c9c8023a8..2b16d01e02 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); @@ -244,6 +245,9 @@ CONSTRUCT_TYPED_SFIELD(sfGovernanceMarks, "GovernanceMarks", UINT256, CONSTRUCT_TYPED_SFIELD(sfEmittedTxnID, "EmittedTxnID", UINT256, 97); CONSTRUCT_TYPED_SFIELD(sfHookCanEmit, "HookCanEmit", UINT256, 96); CONSTRUCT_TYPED_SFIELD(sfCron, "Cron", UINT256, 95); +CONSTRUCT_TYPED_SFIELD(sfRandomData, "RandomData", UINT256, 94); +CONSTRUCT_TYPED_SFIELD(sfLastRandomData, "LastRandomData", UINT256, 93); +CONSTRUCT_TYPED_SFIELD(sfNextRandomDigest, "NextRandomDigest", UINT256, 92); // currency amount (common) CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1); @@ -362,6 +366,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfImportVLKey, "ImportVLKey", OBJECT, CONSTRUCT_UNTYPED_SFIELD(sfHookEmission, "HookEmission", OBJECT, 93); CONSTRUCT_UNTYPED_SFIELD(sfMintURIToken, "MintURIToken", OBJECT, 92); CONSTRUCT_UNTYPED_SFIELD(sfAmountEntry, "AmountEntry", OBJECT, 91); +CONSTRUCT_UNTYPED_SFIELD(sfRandomDigestEntry, "RandomDigestEntry", OBJECT, 89); // array of objects // ARRAY/1 is reserved for end of array @@ -388,6 +393,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfActiveValidators, "ActiveValidators", ARRAY, CONSTRUCT_UNTYPED_SFIELD(sfImportVLKeys, "ImportVLKeys", ARRAY, 94); CONSTRUCT_UNTYPED_SFIELD(sfHookEmissions, "HookEmissions", ARRAY, 93); CONSTRUCT_UNTYPED_SFIELD(sfAmounts, "Amounts", ARRAY, 92); +CONSTRUCT_UNTYPED_SFIELD(sfRandomDigests, "RandomDigests", ARRAY, 91); // clang-format on diff --git a/src/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index 2fbb194218..4bdc1413b6 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -615,7 +615,8 @@ isPseudoTx(STObject const& tx) auto tt = safe_cast(*t); return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY || - tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON; + tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON || + tt == ttRNG; } } // namespace ripple diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 789de36f2d..ceeffd182b 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -490,6 +490,16 @@ TxFormats::TxFormats() {sfStartTime, soeOPTIONAL}, }, commonFields); + + add(jss::Rng, + ttRNG, + { + {sfValidator, soeREQUIRED}, + {sfRandomData, soeREQUIRED}, + {sfNextRandomDigest, soeREQUIRED}, + {sfLedgerSequence, soeREQUIRED}, + }, + commonFields); } TxFormats const& diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 3160372689..b12c0025ac 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -123,6 +123,8 @@ JSS(PaymentChannelCreate); // transaction type. JSS(PaymentChannelFund); // transaction type. JSS(Remit); // transaction type. JSS(RippleState); // ledger type. +JSS(Rng); +JSS(Random); JSS(SLE_hit_rate); // out: GetCounts. JSS(SetFee); // transaction type. JSS(SetRemarks); // transaction type From 9efcd4521253450ac1ed19641b18e6605d04e9e0 Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Mon, 22 Dec 2025 11:57:35 +1100 Subject: [PATCH 03/11] debugging, invariant check, txns need to be injected earlier if possible --- src/ripple/app/misc/impl/TxQ.cpp | 50 +++++++++++++++-------- src/ripple/app/tx/impl/InvariantCheck.cpp | 1 + 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index 978b6c2bf0..6e02d9852f 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -1481,35 +1481,48 @@ TxQ::accept(Application& app, OpenView& view) // Inject an RNG psuedo if we're on the UNL if (view.rules().enabled(featureRNG)) { + JLOG(j_.debug()) + << "RNG processing: started"; + + auto const seq = view.info().seq; + do { // if we're not a validator we do nothing here if (app.getValidationPublicKey().empty()) break; - // and if we're not on the UNLReport we also do nothing - auto const unlRep = view.read(keylet::UNLReport()); - if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators)) + if (seq > 256) // only do the UNL check if its not a newly created test network { - // nothing to do without a unlreport object - break; - } + // and if we're not on the UNLReport we also do nothing + auto const unlRep = view.read(keylet::UNLReport()); + if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators)) + { + JLOG(j_.debug()) + << "RNG processing: UNLReport misssing"; + // nothing to do without a unlreport object + break; + } - bool found = false; - auto const& avs = unlRep->getFieldArray(sfActiveValidators); - for (auto const& av : avs) - { - if (PublicKey(av[sfPublicKey]) == app.getValidationPublicKey()) + bool found = false; + auto const& avs = unlRep->getFieldArray(sfActiveValidators); + for (auto const& av : avs) { - found = true; + if (PublicKey(av[sfPublicKey]) == app.getValidationPublicKey()) + { + found = true; + break; + } + } + + if (!found) + { + JLOG(j_.debug()) + << "RNG processing: UNLReport present but we're not on it"; break; } } - if (!found) - break; - - auto const seq = view.info().seq; AccountID acc = calcAccountID(app.getValidationPublicKey()); @@ -1560,6 +1573,11 @@ TxQ::accept(Application& app, OpenView& view) // submit to the ledger { uint256 txID = rngTx.getTransactionID(); + + JLOG(j_.debug()) + << "RNG processing: Submitting pseudo: " + << rngTx.getFullText() + << " txid: " << txID; auto s = std::make_shared(); rngTx.add(*s); app.getHashRouter().setFlags(txID, SF_PRIVATE2); diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 5679a2b47c..acb0553972 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -494,6 +494,7 @@ LedgerEntryTypesMatch::visitEntry( case ltCRON: case ltIMPORT_VLSEQ: case ltUNL_REPORT: + case ltRANDOM: break; default: invalidTypeAdded_ = true; From 1690c045f270b148275f46f3db5f759fc60f71c3 Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Wed, 24 Dec 2025 13:30:52 +1100 Subject: [PATCH 04/11] scrap ttRNG and introduce ttENTROPY and ttSHUFFLE, dual rng scheme --- src/ripple/app/tx/impl/Entropy.cpp | 312 +++++++++++++++++++++++++++++ src/ripple/app/tx/impl/Entropy.h | 64 ++++++ 2 files changed, 376 insertions(+) create mode 100644 src/ripple/app/tx/impl/Entropy.cpp create mode 100644 src/ripple/app/tx/impl/Entropy.h diff --git a/src/ripple/app/tx/impl/Entropy.cpp b/src/ripple/app/tx/impl/Entropy.cpp new file mode 100644 index 0000000000..c464fed3c1 --- /dev/null +++ b/src/ripple/app/tx/impl/Entropy.cpp @@ -0,0 +1,312 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + 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 +#include +#include +#include +#include +#include + +namespace ripple { + +TxConsequences +Entropy::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx, TxConsequences::normal}; +} + +NotTEC +Entropy::preflight(PreflightContext const& ctx) +{ + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + return preflight2(ctx); +} + +TER +Entropy::preclaim(PreclaimContext const& ctx) +{ + if (!ctx.view.rules().enabled(featureRNG)) + return temDISABLED; + + auto const seq = ctx.view.info().seq; + + auto const txLgrSeq = ctx.tx[sfLedgerSequence]; + + // we will not process and can never accept any txns that aren't introduced this ledger + // for this ledger. + if (seq != txLgrSeq) + { + JLOG(ctx.j.warn()) << "Entropy: wrong ledger seq=" << seq; + return tefFAILURE; + } + + // account must be a valid UV + if (!inUNLReport(ctx.view, ctx.tx.getAccountID(sfAccount), ctx.j)) + { + JLOG(ctx.j.warn()) + << "Entropy: Txn Account isn't in the UNLReport."; + return tefFAILURE; + } + + return tesSUCCESS; +} + +TER +Entropy::doApply() +{ + + auto const seq = view().info().seq; + + if (seq != ctx_.tx.getFieldU32(sfLedgerSequence)) + { + return tefFAILURE; + } + + auto sle = view().peek(keylet::random()); + + bool const created = !sle; + + if (created) + { + sle = std::make_shared(keylet::random()); + } + + auto lastSeq = created ? 0 : sle->getFieldU32(sfLedgerSequence); + + if (lastSeq < seq) + { + // update the ledger sequence of the object + sle->setFieldU32(sfLedgerSequence, seq); + + // reset entropy count to zero... this will probably be + // one after the below executes but its possible the digest + // doesn't match and the entropy count isn't incremented + sle->setFieldU16(sfEntropyCount, 0); + + // swap the random data out ready for this round of entropy collection + sle->setFieldH256(sfLastRandomData, sle->getFieldH256(sfRandomData)); + sle->setFieldH256(sfRandomData, beast::zero); + } + + uint256 nextDigest = ctx_.tx.getFieldH256(sfNextRandomDigest); + uint256 currentEntropy = ctx_.tx.getFieldH256(sfRandomData); + uint256 currentDigest = sha512Half(currentEntropy); + + AccountID const validator = ctx_.tx.getAccountID(sfAccount); + + // iterate the digest array to find the entry if it exists + STArray digestEntries = sle->getFieldArray(sfRandomDigests); + std::map entries; + + for (auto& entry : digestEntries) + { + // we'll automatically clean up really old entries by just omitting them from + // the map here + if (entry.getFieldU32(sfLedgerSequence) < seq - 5) + continue; + + entries.emplace(entry.getAccountID(sfValidator), std::move(entry)); + } + + if (auto it = entries.find(validator); it != entries.end()) + { + auto& entry = it->second; + + // ensure the precommitted digest matches the provided entropy + if (entry.getFieldH256(sfNextRandomDigest) != currentDigest) + { + if (entry.getFieldU32(sfLedgerSequence) != seq - 1) + { + // this is a skip-ahead or missed last txn somehow, so ignore, but no warning. + } + else + { + // this is a clear violation so warn (and ignore the entropy) + JLOG(j_.warn()) << "!!! Validator " << validator << " supplied entropy that " + << "does not match precommitment value !!!"; + } + } + else + { + + // contribute the new entropy to the random data field + sle->setFieldH256(sfRandomData, sha512Half(validator, sle->getFieldH256(sfRandomData), currentEntropy)); + + // increment entropy count + sle->setFieldU16(sfEntropyCount, sle->getFieldU16(sfEntropyCount) + 1); + } + + // update the digest entry + entry.setFieldH256(sfNextRandomDigest, nextDigest); + entry.setFieldU32(sfLedgerSequence, seq); + } + else + { + // this validator doesn't have an entry so create one + STObject entry{sfRandomDigestEntry}; + entry.setAccountID(sfValidator, validator); + entry.setFieldH256(sfNextRandomDigest, nextDigest); + entry.setFieldU32(sfLedgerSequence, seq); + entries.emplace(validator, std::move(entry)); + } + + // update the array + STArray newEntries(sfRandomDigests); + newEntries.reserve(entries.size()); + for (auto& [_, entry] : entries) + newEntries.push_back(std::move(entry)); + + sle->setFieldArray(sfRandomDigests, std::move(newEntries)); + + // send it off to the ledger + if (!created) + view().update(sle); + else + view().insert(sle); + + return tesSUCCESS; +} + + +XRPAmount +Entropy::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + XRPAmount extraFee{0}; + + if (tx.isFieldPresent(sfBlob)) + extraFee += + XRPAmount{static_cast(tx.getFieldVL(sfBlob).size())}; + + // old code (prior to fixXahauV1) + if (!view.rules().enabled(fixXahauV1)) + { + if (tx.isFieldPresent(sfHookParameters)) + { + uint64_t paramBytes = 0; + auto const& params = tx.getFieldArray(sfHookParameters); + for (auto const& param : params) + { + paramBytes += + (param.isFieldPresent(sfHookParameterName) + ? param.getFieldVL(sfHookParameterName).size() + : 0) + + (param.isFieldPresent(sfHookParameterValue) + ? param.getFieldVL(sfHookParameterValue).size() + : 0); + } + extraFee += XRPAmount{static_cast(paramBytes)}; + } + } + + return Transactor::calculateBaseFee(view, tx) + extraFee; +} + +// if this validator is on the UNLReport then return a signed ttENTROPY transaction +// to be added to the txq. +std::shared_ptr +makeEntropyTxn(OpenView& view, Application& app, beast::Journal const& j_) +{ + // Inject an RNG psuedo if we're on the UNL + if (!view.rules().enabled(featureRNG)) + return {}; + + JLOG(j_.debug()) + << "ENTROPY processing: started"; + + auto const seq = view.info().seq; + + static uint32_t lastSeq = 0; + + // we only generate once per ledger + if (lastSeq == seq) + return {}; + + lastSeq = seq; + + // if we're not a validator we do nothing here + if (app.getValidationPublicKey().empty()) + return {}; + + PublicKey pkSigning = app.getValidationPublicKey(); + + auto const pk = app.validatorManifests().getMasterKey(pkSigning); + + if (!inUNLReport(view, app, pk, j_)) + return {}; + + // build and sign a txn + + AccountID acc = calcAccountID(pk); + + static auto getRnd = []() -> uint256 { + static std::ifstream rng("/dev/urandom", std::ios::binary); + uint256 out; + if (rng && rng.read(reinterpret_cast(out.data()), 32)) + return out; + std::random_device rd; + for (auto& word : out) + word = rd(); + return out; + }; + + static std::map rngMap; + + uint256 nextRnd = getRnd(); + + if (rngMap.find(seq) != rngMap.end()) + return {}; + + rngMap[seq] = nextRnd; + + std::optional prevRnd; + + if (rngMap.find(seq - 1) != rngMap.end()) + prevRnd = rngMap[seq - 1]; + + // amortized cleanup, for every ledger attempt to delete two old entries + // even in the most desynced ridiculous state this is guaranteed prevent map growth + for (int i = 0; i < 2; ++i) + { + auto it = rngMap.begin(); + if (it != rngMap.end() && it->first < seq) + rngMap.erase(it->first); + } + + // create txn + auto rngTx = std::make_shared(ttENTROPY, [&](auto& obj) { + obj.setFieldU32(sfLedgerSequence, seq); + obj.setAccountID(sfAccount, acc); + obj.setFieldU32(sfSequence, 0); + if (prevRnd.has_value()) + obj.setFieldH256(sfRandomData, *prevRnd); + obj.setFieldH256(sfNextRandomDigest, sha512Half(nextRnd)); + obj.setFieldVL(sfSigningPubKey, pk.slice()); + obj.setFieldH256(sfParentHash, view.info().parentHash); + }); + + // sign the txn using our ephemeral key + rngTx->sign(pk, app.getValidationSecretKey()); + + return rngTx; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/Entropy.h b/src/ripple/app/tx/impl/Entropy.h new file mode 100644 index 0000000000..5af0a91a26 --- /dev/null +++ b/src/ripple/app/tx/impl/Entropy.h @@ -0,0 +1,64 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_ENTROPY_H_INCLUDED +#define RIPPLE_TX_ENTROPY_H_INCLUDED + +#include +#include +#include +#include +#include +#include + +namespace ripple { + +class Entropy : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + explicit Entropy(ApplyContext& ctx) : Transactor(ctx) + { + } + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +// if this validator is on the UNLReport then return a signed ttENTROPY transaction +// to be added to the txq. +std::shared_ptr +makeEntropyTxn(OpenView& view, Application& app, beast::Journal const& j_); + +} // namespace ripple + +#endif From d33a57f566643e5cae7ba72bcfe086ef1cc7596c Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Wed, 24 Dec 2025 18:08:16 +1100 Subject: [PATCH 05/11] ttSHUFFLE injection appears to be working --- Builds/CMake/RippledCore.cmake | 1 + src/ripple/app/consensus/RCLConsensus.cpp | 10 ++ src/ripple/app/ledger/impl/BuildLedger.cpp | 7 +- src/ripple/app/main/Application.cpp | 6 + src/ripple/app/main/Application.h | 3 + src/ripple/app/misc/NetworkOPs.cpp | 2 +- src/ripple/app/misc/NetworkOPs.h | 8 ++ src/ripple/app/misc/impl/TxQ.cpp | 111 --------------- src/ripple/app/tx/impl/Change.cpp | 151 ++++++++++----------- src/ripple/app/tx/impl/Change.h | 5 +- src/ripple/app/tx/impl/Transactor.cpp | 46 ++++++- src/ripple/app/tx/impl/applySteps.cpp | 19 ++- src/ripple/consensus/Consensus.h | 4 + src/ripple/ledger/View.h | 66 +++++++++ src/ripple/overlay/impl/PeerImp.cpp | 5 + src/ripple/protocol/LedgerFormats.h | 2 +- src/ripple/protocol/TxFormats.h | 8 +- src/ripple/protocol/impl/STTx.cpp | 3 +- src/ripple/protocol/impl/TxFormats.cpp | 15 +- src/ripple/protocol/jss.h | 2 + 20 files changed, 261 insertions(+), 213 deletions(-) diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 74ff58f601..4652c453e1 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -461,6 +461,7 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/CronSet.cpp src/ripple/app/tx/impl/DeleteAccount.cpp src/ripple/app/tx/impl/DepositPreauth.cpp + src/ripple/app/tx/impl/Entropy.cpp src/ripple/app/tx/impl/Escrow.cpp src/ripple/app/tx/impl/GenesisMint.cpp src/ripple/app/tx/impl/Import.cpp diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index 4fb0c5275f..81230e8d8a 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -44,6 +44,8 @@ #include #include #include +#include +#include #include #include @@ -225,6 +227,8 @@ RCLConsensus::Adaptor::propose(RCLCxPeerPos::Proposal const& proposal) prop.set_signature(sig.data(), sig.size()); + injectShuffleTxn(app_, sig); + auto const suppression = proposalUniqueId( proposal.position(), proposal.prevLedger(), @@ -652,6 +656,12 @@ RCLConsensus::Adaptor::doAccept( tapNONE, "consensus", [&](OpenView& view, beast::Journal j) { + if (rules->enabled(featureRNG)) + { + auto tx = makeEntropyTxn(view, app_, j_); + if (tx) + app_.getOPs().submitTransaction(tx); + } return app_.getTxQ().accept(app_, view); }); diff --git a/src/ripple/app/ledger/impl/BuildLedger.cpp b/src/ripple/app/ledger/impl/BuildLedger.cpp index 85ed72106b..e21ec4b00f 100644 --- a/src/ripple/app/ledger/impl/BuildLedger.cpp +++ b/src/ripple/app/ledger/impl/BuildLedger.cpp @@ -104,12 +104,15 @@ applyTransactions( bool certainRetry = true; std::size_t count = 0; + // apply the ttSHUFFLE txns first in the ledger to + // ensure no one can predict the outcome + // then apply ttENTROPY transactions if (view.rules().enabled(featureRNG)) + for (auto tt : {ttSHUFFLE, ttENTROPY}) { - // apply the ttRNG txns first in the ledger to ensure no one can predict the outcome for (auto it = txns.begin(); it != txns.end();) { - if (it->second->getFieldU16(sfTransactionType) != ttRNG) + if (tt != it->second->getFieldU16(sfTransactionType)) { ++it; continue; diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 20427107b1..0268ec626f 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -599,6 +599,12 @@ class ApplicationImp : public Application, public BasicApp return validatorKeys_.publicKey; } + SecretKey const& + getValidationSecretKey() const override + { + return validatorKeys_.secretKey; + } + NetworkOPs& getOPs() override { diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h index a6e58b19e9..3c45f67e6c 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -240,6 +240,9 @@ class Application : public beast::PropertyStream::Source virtual PublicKey const& getValidationPublicKey() const = 0; + + virtual SecretKey const& + getValidationSecretKey() const = 0; virtual Resource::Manager& getResourceManager() = 0; diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 0e5b8ef5f5..f27fe4970e 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -228,7 +228,7 @@ class NetworkOPsImp final : public NetworkOPs doTransactionSync( std::shared_ptr transaction, bool bUnlimited, - FailHard failType); + FailHard failType) override; /** * For transactions not submitted by a locally connected client, fire and diff --git a/src/ripple/app/misc/NetworkOPs.h b/src/ripple/app/misc/NetworkOPs.h index 350542404c..630808b859 100644 --- a/src/ripple/app/misc/NetworkOPs.h +++ b/src/ripple/app/misc/NetworkOPs.h @@ -112,6 +112,14 @@ class NetworkOPs : public InfoSub::Source bool bLocal, FailHard failType) = 0; + + // directly inject transaction, skipping checks + virtual void + doTransactionSync( + std::shared_ptr transaction, + bool bUnlimited, + FailHard failType) = 0; + //-------------------------------------------------------------------------- // // Owner functions diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index 6e02d9852f..f1737fa8cf 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -1478,117 +1478,6 @@ TxQ::accept(Application& app, OpenView& view) } } - // Inject an RNG psuedo if we're on the UNL - if (view.rules().enabled(featureRNG)) - { - JLOG(j_.debug()) - << "RNG processing: started"; - - auto const seq = view.info().seq; - - do - { - // if we're not a validator we do nothing here - if (app.getValidationPublicKey().empty()) - break; - - if (seq > 256) // only do the UNL check if its not a newly created test network - { - // and if we're not on the UNLReport we also do nothing - auto const unlRep = view.read(keylet::UNLReport()); - if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators)) - { - JLOG(j_.debug()) - << "RNG processing: UNLReport misssing"; - // nothing to do without a unlreport object - break; - } - - bool found = false; - auto const& avs = unlRep->getFieldArray(sfActiveValidators); - for (auto const& av : avs) - { - if (PublicKey(av[sfPublicKey]) == app.getValidationPublicKey()) - { - found = true; - break; - } - } - - if (!found) - { - JLOG(j_.debug()) - << "RNG processing: UNLReport present but we're not on it"; - break; - } - } - - - AccountID acc = calcAccountID(app.getValidationPublicKey()); - - static auto getRnd = []() -> uint256 { - static std::ifstream rng("/dev/urandom", std::ios::binary); - uint256 out; - if (rng && rng.read(reinterpret_cast(out.data()), 32)) - return out; - std::random_device rd; - for (auto& word : out) - word = rd(); - return out; - }; - - static std::map rngMap; - - uint256 nextRnd = getRnd(); - - if (rngMap.find(seq) != rngMap.end()) - break; // should never happen - - rngMap[seq] = nextRnd; - - std::optional prevRnd; - - if (rngMap.find(seq - 1) != rngMap.end()) - prevRnd = rngMap[seq - 1]; - - // amortized cleanup, for every ledger attempt to delete two old entries - // even in the most desynced ridiculous state this is guaranteed prevent map growth - for (int i = 0; i < 2; ++i) - { - auto it = rngMap.begin(); - if (it != rngMap.end() && it->first < seq) - rngMap.erase(it->first); - } - - // create txn - STTx rngTx(ttRNG, [&](auto& obj) { - obj.setFieldU32(sfLedgerSequence, seq); - obj.setAccountID(sfValidator, acc); - if (prevRnd.has_value()) - obj.setFieldH256(sfRandomData, *prevRnd); - obj.setFieldH256(sfNextRandomDigest, sha512Half(nextRnd)); - // RH TODO: should we sign this?? - }); - - // submit to the ledger - { - uint256 txID = rngTx.getTransactionID(); - - JLOG(j_.debug()) - << "RNG processing: Submitting pseudo: " - << rngTx.getFullText() - << " txid: " << txID; - auto s = std::make_shared(); - rngTx.add(*s); - app.getHashRouter().setFlags(txID, SF_PRIVATE2); - app.getHashRouter().setFlags(txID, SF_EMITTED); - view.rawTxInsert(txID, std::move(s), nullptr); - ledgerChanged = true; - } - - } while (0); - } - // Inject cron transactions, if any if (view.rules().enabled(featureCron)) { diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 38289c125f..074d052eea 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include namespace ripple { @@ -96,7 +97,7 @@ Change::preflight(PreflightContext const& ctx) } } - if (ctx.tx.getTxnType() == ttRNG && !ctx.rules.enabled(featureRNG)) + if (ctx.tx.getTxnType() == ttSHUFFLE && !ctx.rules.enabled(featureRNG)) { JLOG(ctx.j.warn()) << "Change: FeatureRNG is not enabled."; return temDISABLED; @@ -110,7 +111,7 @@ Change::preclaim(PreclaimContext const& ctx) { // If tapOPEN_LEDGER is resurrected into ApplyFlags, // this block can be moved to preflight. - if (ctx.view.open()) + if (ctx.view.open() && ctx.tx.getTxnType() != ttSHUFFLE) { JLOG(ctx.j.warn()) << "Change transaction against open ledger"; return temINVALID; @@ -160,7 +161,7 @@ Change::preclaim(PreclaimContext const& ctx) case ttAMENDMENT: case ttUNL_MODIFY: case ttEMIT_FAILURE: - case ttRNG: + case ttSHUFFLE: return tesSUCCESS; case ttUNL_REPORT: { if (!ctx.tx.isFieldPresent(sfImportVLKey) || @@ -216,8 +217,8 @@ Change::doApply() return applyEmitFailure(); case ttUNL_REPORT: return applyUNLReport(); - case ttRNG: - return applyRNG(); + case ttSHUFFLE: + return applyShuffle(); default: assert(0); return tefFAILURE; @@ -225,14 +226,15 @@ Change::doApply() } TER -Change::applyRNG() +Change::applyShuffle() { auto const seq = view().info().seq; + auto const txSeq = ctx_.tx.getFieldU32(sfLedgerSequence); - if (seq != ctx_.tx.getFieldU32(sfLedgerSequence)) + if (seq != txSeq) { - JLOG(j_.warn()) << "Change: ttRNG, wrong ledger seq=" << seq; + JLOG(j_.warn()) << "Change: ttSHUFFLE, wrong ledger seq. lgr=" << seq << " tx=" << txSeq; return tefFAILURE; } @@ -249,9 +251,6 @@ Change::applyRNG() if (lastSeq < seq) { - // update the ledger sequence of the object - sle->setFieldU32(sfLedgerSequence, seq); - // reset entropy count to zero... this will probably be // one after the below executes but its possible the digest // doesn't match and the entropy count isn't incremented @@ -259,82 +258,25 @@ Change::applyRNG() // swap the random data out ready for this round of entropy collection sle->setFieldH256(sfLastRandomData, sle->getFieldH256(sfRandomData)); - sle->setFieldH256(sfRandomData, beast::zero); - } - - uint256 nextDigest = ctx_.tx.getFieldH256(sfNextRandomDigest); - uint256 currentEntropy = ctx_.tx.getFieldH256(sfRandomData); - uint256 currentDigest = sha512Half(currentEntropy); - - AccountID const validator = ctx_.tx.getAccountID(sfValidator); - - // RH TODO: check if they're on the UNLReport and ignore if not - - // iterate the digest array to find the entry if it exists - STArray digestEntries = sle->getFieldArray(sfRandomDigests); - std::map entries; - - for (auto& entry : digestEntries) - { - // we'll automatically clean up really old entries by just omitting them from - // the map here - if (entry.getFieldU32(sfLedgerSequence) < seq - 5) - continue; - - entries.emplace(entry.getAccountID(sfValidator), std::move(entry)); - } - - if (auto it = entries.find(validator); it != entries.end()) - { - auto& entry = it->second; - - // ensure the precommitted digest matches the provided entropy - if (entry.getFieldH256(sfNextRandomDigest) != currentDigest) - { - if (entry.getFieldU32(sfLedgerSequence) != seq - 1) - { - // this is a skip-ahead or missed last txn somehow, so ignore, but no warning. - } - else - { - // this is a clear violation so warn (and ignore the entropy) - JLOG(j_.warn()) << "!!! Validator " << validator << " supplied entropy that " - << "does not match precommitment value !!!"; - } - } - else - { - - // contribute the new entropy to the random data field - sle->setFieldH256(sfRandomData, sha512Half(validator, sle->getFieldH256(sfRandomData), currentEntropy)); - - // increment entropy count - sle->setFieldU16(sfEntropyCount, sle->getFieldU16(sfEntropyCount) + 1); - } - - // update the digest entry - entry.setFieldH256(sfNextRandomDigest, nextDigest); - entry.setFieldU32(sfLedgerSequence, seq); + + // update the ledger sequence of the object + sle->setFieldU32(sfLedgerSequence, seq); } else { - // this validator doesn't have an entry so create one - STObject entry{sfRandomDigestEntry}; - entry.setAccountID(sfValidator, validator); - entry.setFieldH256(sfNextRandomDigest, nextDigest); - entry.setFieldU32(sfLedgerSequence, seq); - entries.emplace(validator, std::move(entry)); + // increment entropy count + sle->setFieldU16(sfEntropyCount, sle->getFieldU16(sfEntropyCount) + 1); } + + // contribute the new entropy to the random data field + sle->setFieldH256(sfRandomData, + sha512Half( + seq, + sle->getFieldU16(sfEntropyCount), + sle->getFieldH256(sfRandomData), + ctx_.tx.getFieldH256(sfRandomData))); - // update the array - STArray newEntries(sfRandomDigests); - newEntries.reserve(entries.size()); - for (auto& [_, entry] : entries) - newEntries.push_back(std::move(entry)); - - sle->setFieldArray(sfRandomDigests, std::move(newEntries)); - - // send it off to the ledger + if (!created) view().update(sle); else @@ -1327,4 +1269,49 @@ Change::applyUNLModify() return tesSUCCESS; } +void +injectShuffleTxn(Application& app, Slice const& sig) +{ + // in featureRNG we use trusted proposal signatures as shuffling entropy + // so inject a psuedo to do that here + auto ol = app.openLedger().current(); + if (ol && ol->rules().enabled(featureRNG)) + { + uint256 rnd = sha512Half(std::string("shuffler"), sig); + // create txn + STTx shuffleTx (ttSHUFFLE, [&](auto& obj) { + obj.setFieldU32(sfLedgerSequence, ol->info().seq + 1); + obj.setFieldH256(sfRandomData, rnd); + obj.setAccountID(sfAccount, AccountID()); + }); + + // inject it into the propose set and into the open ledger + uint256 txID = shuffleTx.getTransactionID(); + + JLOG(app.journal("Transaction").debug()) + << "SHUFFLE processing: Submitting pseudo: " + << shuffleTx.getFullText() + << " txid: " << txID; + app.getHashRouter().setFlags(txID, SF_PRIVATE2); + app.getHashRouter().setFlags(txID, SF_EMITTED); + + { + auto s = std::make_shared(); + shuffleTx.add(*s); + + std::unique_lock masterLock{app.getMasterMutex(), std::defer_lock}; + + std::unique_lock ledgerLock{ + app.getLedgerMaster().peekMutex(), std::defer_lock}; + std::lock(masterLock, ledgerLock); + + app.openLedger().modify([&](OpenView& view, beast::Journal j) { + view.rawTxInsert(txID, std::move(s), nullptr); + return true; + }); + + } + } +} + } // namespace ripple diff --git a/src/ripple/app/tx/impl/Change.h b/src/ripple/app/tx/impl/Change.h index 4df511ca67..748e7aa32a 100644 --- a/src/ripple/app/tx/impl/Change.h +++ b/src/ripple/app/tx/impl/Change.h @@ -78,9 +78,12 @@ class Change : public Transactor applyUNLReport(); TER - applyRNG(); + applyShuffle(); }; +void +injectShuffleTxn(Application& app, Slice const& sig); + } // namespace ripple #endif diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 80646172a0..ba76ed2ca3 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -442,9 +442,24 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) // Only check fee is sufficient when the ledger is open. if (ctx.view.open()) { - auto const feeDue = + + auto feeDue = minimumFee(ctx.app, baseFee, ctx.view.fees(), ctx.flags); + if (ctx.view.rules().enabled(featureRNG)) + { + auto const pkSignerField = ctx.tx.getSigningPubKey(); + if (publicKeyType(makeSlice(pkSignerField))) + { + PublicKey pkSigner {makeSlice(pkSignerField)}; + if (inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j)) + { + // UVTxns don't have to pay a fee + feeDue = beast::zero; + } + } + } + if (feePaid < feeDue) { JLOG(ctx.j.trace()) @@ -541,6 +556,16 @@ Transactor::checkSeqProxy( return tesSUCCESS; } + if (view.rules().enabled(featureRNG) && + tx.getTxnType() == ttENTROPY && t_seqProx.isSeq() && + tx[sfSequence] == 0) + { + JLOG(j.trace()) + << "applyTransaction: allowing ttENTROPY with seq=0 " + << toBase58(id); + return tesSUCCESS; + } + JLOG(j.trace()) << "applyTransaction: delay: source account does not exist " << toBase58(id); @@ -848,19 +873,32 @@ NotTEC Transactor::checkSingleSign(PreclaimContext const& ctx) { // Check that the value in the signing key slot is a public key. - auto const pkSigner = ctx.tx.getSigningPubKey(); - if (!publicKeyType(makeSlice(pkSigner))) + auto const pkSignerField = ctx.tx.getSigningPubKey(); + if (!publicKeyType(makeSlice(pkSignerField))) { JLOG(ctx.j.trace()) << "checkSingleSign: signing public key type is unknown"; return tefBAD_AUTH; // FIXME: should be better error! } + PublicKey pkSigner {makeSlice(pkSignerField)}; + // Look up the account. - auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner))); + auto const idSigner = calcAccountID(pkSigner); auto const idAccount = ctx.tx.getAccountID(sfAccount); auto const sleAccount = ctx.view.read(keylet::account(idAccount)); + // check if this is a UVTxn (UNL Validator transaction) + // these are signed by the UNL validators + if (ctx.view.rules().enabled(featureRNG)) + { + // UVTxns of the approved type don't need an underlying account + // and can be signed with the manifest ephemeral key + if (inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j) && + ctx.tx.getTxnType() == ttENTROPY) + return tesSUCCESS; + } + if (!sleAccount) return terNO_ACCOUNT; diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 50818caf89..57b6e6d686 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -52,6 +52,7 @@ #include #include #include +#include namespace ripple { @@ -152,7 +153,7 @@ invoke_preflight(PreflightContext const& ctx) case ttUNL_MODIFY: case ttUNL_REPORT: case ttEMIT_FAILURE: - case ttRNG: + case ttSHUFFLE: return invoke_preflight_helper(ctx); case ttHOOK_SET: return invoke_preflight_helper(ctx); @@ -188,6 +189,8 @@ invoke_preflight(PreflightContext const& ctx) return invoke_preflight_helper(ctx); case ttCRON: return invoke_preflight_helper(ctx); + case ttENTROPY: + return invoke_preflight_helper(ctx); default: assert(false); return {temUNKNOWN, TxConsequences{temUNKNOWN}}; @@ -284,7 +287,7 @@ invoke_preclaim(PreclaimContext const& ctx) case ttUNL_MODIFY: case ttUNL_REPORT: case ttEMIT_FAILURE: - case ttRNG: + case ttSHUFFLE: return invoke_preclaim(ctx); case ttNFTOKEN_MINT: return invoke_preclaim(ctx); @@ -318,6 +321,8 @@ invoke_preclaim(PreclaimContext const& ctx) return invoke_preclaim(ctx); case ttCRON: return invoke_preclaim(ctx); + case ttENTROPY: + return invoke_preclaim(ctx); default: assert(false); return temUNKNOWN; @@ -376,7 +381,7 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) case ttUNL_MODIFY: case ttUNL_REPORT: case ttEMIT_FAILURE: - case ttRNG: + case ttSHUFFLE: return Change::calculateBaseFee(view, tx); case ttNFTOKEN_MINT: return NFTokenMint::calculateBaseFee(view, tx); @@ -410,6 +415,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) return CronSet::calculateBaseFee(view, tx); case ttCRON: return Cron::calculateBaseFee(view, tx); + case ttENTROPY: + return Entropy::calculateBaseFee(view, tx); default: return XRPAmount{0}; } @@ -547,7 +554,7 @@ invoke_apply(ApplyContext& ctx) case ttFEE: case ttUNL_MODIFY: case ttUNL_REPORT: - case ttRNG: + case ttSHUFFLE: case ttEMIT_FAILURE: { Change p(ctx); return p(); @@ -612,6 +619,10 @@ invoke_apply(ApplyContext& ctx) Cron p(ctx); return p(); } + case ttENTROPY: { + Entropy p(ctx); + return p(); + } default: assert(false); return {temUNKNOWN, false}; diff --git a/src/ripple/consensus/Consensus.h b/src/ripple/consensus/Consensus.h index df5ec01cee..e7581cf18a 100644 --- a/src/ripple/consensus/Consensus.h +++ b/src/ripple/consensus/Consensus.h @@ -33,6 +33,10 @@ #include #include #include +#include +#include +#include +#include namespace ripple { diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index 27659ad8d0..a6990a47bf 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -36,6 +36,8 @@ #include #include #include +#include +#include #include #include #include @@ -1094,6 +1096,70 @@ trustTransferLockedBalance( } return tesSUCCESS; } + +template +bool inUNLReport( + V const& view, + AccountID const& id, + beast::Journal const& j) +{ + + auto const seq = view.info().seq; + static uint32_t lastLgrSeq = 0; + static std::map cache; + + // for the first 256 ledgers we're just saying everyone is in the UNLReport + // because otherwise testing is very difficult. + if (seq < 256) + return true; + + if (lastLgrSeq != seq) + { + cache.clear(); + lastLgrSeq = seq; + } + else + { + if (cache.find(id) != cache.end()) + return cache[id]; + } + + // Check if UVAcc is on UNLReport we also do nothing + auto const unlRep = view.read(keylet::UNLReport()); + if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators)) + { + JLOG(j.debug()) + << "UNLReport misssing"; + + // ensure we keep the cache invalid when in this state + lastLgrSeq = 0; + return false; + } + + auto const& avs = unlRep->getFieldArray(sfActiveValidators); + for (auto const& av : avs) + { + if (av.getAccountID(sfAccount) == id) + return cache[id] = true; + } + + return cache[id] = false; +} + + +template +bool inUNLReport( + V const& view, + Application& app, + PublicKey const& pk, + beast::Journal const& j) +{ + PublicKey uvPk = app.validatorManifests().getMasterKey(pk); + + return inUNLReport(view, calcAccountID(pk), j) || + (uvPk != pk && inUNLReport(view, calcAccountID(uvPk), j)); +} + } // namespace ripple #endif diff --git a/src/ripple/overlay/impl/PeerImp.cpp b/src/ripple/overlay/impl/PeerImp.cpp index adb05ddf62..3f4ef03b68 100644 --- a/src/ripple/overlay/impl/PeerImp.cpp +++ b/src/ripple/overlay/impl/PeerImp.cpp @@ -40,6 +40,8 @@ #include #include #include +#include +#include #include #include @@ -1955,6 +1957,9 @@ PeerImp::onMessage(std::shared_ptr const& m) if (!isTrusted && app_.config().RELAY_UNTRUSTED_PROPOSALS == -1) return; + // ttSHUFFLE is injected as part of featureRNG, based on the proposal signature + injectShuffleTxn(app_, makeSlice(set.signature())); + uint256 const proposeHash{set.currenttxhash()}; uint256 const prevLedger{set.previousledger()}; diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index 1e6e248481..7135dbfe42 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -262,7 +262,7 @@ enum LedgerEntryType : std::uint16_t ltEMITTED_TXN = 'E', - /** A ledger object containing a consensus-generated random number, operated on by ttRNG + /** A ledger object containing a consensus-generated random number, operated on by ttENTROPY \sa keylet::rng */ diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index 54ec86e6d3..e62fdbba78 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -149,9 +149,13 @@ enum TxType : std::uint16_t ttURITOKEN_CREATE_SELL_OFFER = 48, ttURITOKEN_CANCEL_SELL_OFFER = 49, - /* A pseudo-txn used by featureRNG which allows validators to submit blinded entropy + /* A pseudo-txn type used by featureRNG to shuffle and randomise the transaction set based on proposal + * signatures */ + ttSHUFFLE = 88, + + /* A UNLReport-validator only txn by featureRNG which allows validators to submit blinded entropy * to a consensus based random number system */ - ttRNG = 89, + ttENTROPY = 89, /* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */ ttCRON = 92, diff --git a/src/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index 4bdc1413b6..b16fe8e0da 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -615,8 +615,7 @@ isPseudoTx(STObject const& tx) auto tt = safe_cast(*t); return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY || - tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON || - tt == ttRNG; + tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON || tt == ttSHUFFLE; } } // namespace ripple diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index ceeffd182b..c1a483811b 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -491,15 +491,24 @@ TxFormats::TxFormats() }, commonFields); - add(jss::Rng, - ttRNG, + add(jss::Entropy, + ttENTROPY, { - {sfValidator, soeREQUIRED}, {sfRandomData, soeREQUIRED}, {sfNextRandomDigest, soeREQUIRED}, {sfLedgerSequence, soeREQUIRED}, + {sfParentHash, soeREQUIRED}, }, commonFields); + + add(jss::Shuffle, + ttSHUFFLE, + { + {sfLedgerSequence, soeREQUIRED}, + {sfRandomData, soeREQUIRED}, + }, + commonFields); + } TxFormats const& diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index b12c0025ac..1ebc658d59 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -142,6 +142,8 @@ JSS(HookState); // ledger type. JSS(HookStateData); // field. JSS(HookStateKey); // field. JSS(EmittedTxn); // ledger type. +JSS(Entropy); +JSS(Shuffle); JSS(SignerList); // ledger type. JSS(SignerListSet); // transaction type. JSS(SigningPubKey); // field. From 55438ffc45ce216c439c98f5b9514024c200a599 Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Sat, 27 Dec 2025 11:43:50 +1100 Subject: [PATCH 06/11] add msg suppression for shuffle --- src/ripple/app/misc/NetworkOPs.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index f27fe4970e..0399bc297a 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -1078,6 +1078,12 @@ NetworkOPsImp::submitTransaction(std::shared_ptr const& iTrans) return; } + if (view->rules().enabled(featureRNG) && iTrans->getTxnType() == ttSHUFFLE) + { + // as above + return; + } + // this is an asynchronous interface auto const trans = sterilize(*iTrans); From b6a23d6d3bf76df42e34c4ad86cd5e6c7e63090a Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Sat, 3 Jan 2026 15:19:03 +1100 Subject: [PATCH 07/11] ttENTROPY working --- src/ripple/app/misc/impl/TxQ.cpp | 11 +++- .../app/rdb/backend/detail/impl/Node.cpp | 2 +- src/ripple/app/tx/impl/Entropy.cpp | 62 ++++++++----------- src/ripple/app/tx/impl/Transactor.cpp | 23 +++++-- src/ripple/protocol/STTx.h | 3 + src/ripple/protocol/impl/STTx.cpp | 18 +++++- src/ripple/protocol/impl/TxFormats.cpp | 1 - 7 files changed, 74 insertions(+), 46 deletions(-) diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index f1737fa8cf..adcc8c9b47 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -1931,13 +1931,18 @@ TxQ::tryDirectApply( const bool isFirstImport = !sleAccount && view.rules().enabled(featureImport) && tx->getTxnType() == ttIMPORT; + const bool isUV = + view.rules().enabled(featureRNG) && tx->getTxnType() == ttENTROPY; + + const bool accRequired = !(isFirstImport || isUV); + // Don't attempt to direct apply if the account is not in the ledger. - if (!sleAccount && !isFirstImport) + if (!sleAccount && accRequired) return {}; std::optional txSeqProx; - if (!isFirstImport) + if (accRequired) { SeqProxy const acctSeqProx = SeqProxy::sequence((*sleAccount)[sfSequence]); @@ -1950,7 +1955,7 @@ TxQ::tryDirectApply( } FeeLevel64 const requiredFeeLevel = - isFirstImport ? FeeLevel64{0} : [this, &view, flags]() { + !accRequired ? FeeLevel64{0} : [this, &view, flags]() { std::lock_guard lock(mutex_); return getRequiredFeeLevel( view, flags, feeMetrics_.getSnapshot(), lock); diff --git a/src/ripple/app/rdb/backend/detail/impl/Node.cpp b/src/ripple/app/rdb/backend/detail/impl/Node.cpp index c80038ef74..401c869025 100644 --- a/src/ripple/app/rdb/backend/detail/impl/Node.cpp +++ b/src/ripple/app/rdb/backend/detail/impl/Node.cpp @@ -319,7 +319,7 @@ saveValidatedLedger( *db << sql; } else if (auto const& sleTxn = acceptedLedgerTx->getTxn(); - !isPseudoTx(*sleTxn)) + !isPseudoTx(*sleTxn) && !isUVTx(*sleTxn)) { // It's okay for pseudo transactions to not affect any // accounts. But otherwise... diff --git a/src/ripple/app/tx/impl/Entropy.cpp b/src/ripple/app/tx/impl/Entropy.cpp index c464fed3c1..9367a5c78c 100644 --- a/src/ripple/app/tx/impl/Entropy.cpp +++ b/src/ripple/app/tx/impl/Entropy.cpp @@ -23,6 +23,7 @@ #include #include #include +#include namespace ripple { @@ -47,17 +48,16 @@ Entropy::preclaim(PreclaimContext const& ctx) if (!ctx.view.rules().enabled(featureRNG)) return temDISABLED; - auto const seq = ctx.view.info().seq; +// auto const seq = ctx.view.info().seq; - auto const txLgrSeq = ctx.tx[sfLedgerSequence]; +// auto const txLgrSeq = ctx.tx[sfLedgerSequence]; - // we will not process and can never accept any txns that aren't introduced this ledger - // for this ledger. - if (seq != txLgrSeq) - { - JLOG(ctx.j.warn()) << "Entropy: wrong ledger seq=" << seq; - return tefFAILURE; - } + // due to circulation we'll accept up to one ledger old entropy txns +// if (seq != txLgrSeq) +// { +// JLOG(ctx.j.warn()) << "Entropy: wrong ledger txseq=" << txLgrSeq << " lgrseq=" << seq << " acc:" << ctx.tx.getAccountID(sfAccount); +// return tefFAILURE; +// } // account must be a valid UV if (!inUNLReport(ctx.view, ctx.tx.getAccountID(sfAccount), ctx.j)) @@ -76,10 +76,10 @@ Entropy::doApply() auto const seq = view().info().seq; - if (seq != ctx_.tx.getFieldU32(sfLedgerSequence)) - { - return tefFAILURE; - } +// if (seq != ctx_.tx.getFieldU32(sfLedgerSequence)) +// { +// return tefFAILURE; +// } auto sle = view().peek(keylet::random()); @@ -268,43 +268,33 @@ makeEntropyTxn(OpenView& view, Application& app, beast::Journal const& j_) return out; }; - static std::map rngMap; - - uint256 nextRnd = getRnd(); - - if (rngMap.find(seq) != rngMap.end()) - return {}; - - rngMap[seq] = nextRnd; - - std::optional prevRnd; - if (rngMap.find(seq - 1) != rngMap.end()) - prevRnd = rngMap[seq - 1]; + static std::optional prevRnd; - // amortized cleanup, for every ledger attempt to delete two old entries - // even in the most desynced ridiculous state this is guaranteed prevent map growth - for (int i = 0; i < 2; ++i) - { - auto it = rngMap.begin(); - if (it != rngMap.end() && it->first < seq) - rngMap.erase(it->first); - } + uint256 nextRnd = getRnd(); // create txn auto rngTx = std::make_shared(ttENTROPY, [&](auto& obj) { - obj.setFieldU32(sfLedgerSequence, seq); + obj.setFieldU32(sfLastLedgerSequence, seq); obj.setAccountID(sfAccount, acc); obj.setFieldU32(sfSequence, 0); if (prevRnd.has_value()) obj.setFieldH256(sfRandomData, *prevRnd); obj.setFieldH256(sfNextRandomDigest, sha512Half(nextRnd)); - obj.setFieldVL(sfSigningPubKey, pk.slice()); + obj.setFieldVL(sfSigningPubKey, pkSigning.slice()); obj.setFieldH256(sfParentHash, view.info().parentHash); + obj.setFieldU32(sfFlags, tfFullyCanonicalSig); + + if (app.config().NETWORK_ID > 1024) + obj.setFieldU32(sfNetworkID, app.config().NETWORK_ID); }); + prevRnd = nextRnd; + // sign the txn using our ephemeral key - rngTx->sign(pk, app.getValidationSecretKey()); + rngTx->sign(pkSigning, app.getValidationSecretKey()); + + JLOG(j_.debug()) << "ENTROPY txn: " << rngTx->getFullText(); return rngTx; } diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index ba76ed2ca3..a50fa47ca7 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -280,13 +280,18 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) // * The additional cost of each multisignature on the transaction. XRPAmount baseFee = view.fees().base; - if (tx.getFieldU16(sfTransactionType) == ttIMPORT) + auto const tt = tx.getFieldU16(sfTransactionType); + + if (tt == ttIMPORT) { XRPAmount const importFee = baseFee * 10; if (importFee > baseFee) baseFee = importFee; } + if (tt == ttENTROPY) + return XRPAmount{0}; + // Each signer adds one more baseFee to the minimum required fee // for the transaction. std::size_t const signerCount = @@ -476,6 +481,9 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) auto const sle = ctx.view.read(keylet::account(id)); if (!sle) { + if (ctx.tx.getTxnType() == ttENTROPY) + return tesSUCCESS; + if (ctx.tx.getTxnType() == ttIMPORT) { if (!ctx.tx.isFieldPresent(sfIssuer)) @@ -650,7 +658,13 @@ Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx) ctx.view.rules().enabled(featureImport) && ctx.tx.getTxnType() == ttIMPORT && !ctx.tx.isFieldPresent(sfIssuer); - if (!sle && !isFirstImport) + bool const isUV = + ctx.view.rules().enabled(featureRNG) && + ctx.tx.getTxnType() == ttENTROPY; + + bool const accRequired = !(isFirstImport || isUV); + + if (!sle && accRequired) { JLOG(ctx.j.trace()) << "applyTransaction: delay: source account does not exist " @@ -806,12 +820,13 @@ Transactor::apply() auto const sle = view().peek(keylet::account(account_)); // sle must exist except for transactions - // that allow zero account. (and ttIMPORT) + // that allow zero account. (and ttIMPORT and UV) assert( sle != nullptr || account_ == beast::zero || view().rules().enabled(featureImport) && ctx_.tx.getTxnType() == ttIMPORT && - !ctx_.tx.isFieldPresent(sfIssuer)); + !ctx_.tx.isFieldPresent(sfIssuer) || + ctx_.tx.getTxnType() == ttENTROPY); if (sle) { diff --git a/src/ripple/protocol/STTx.h b/src/ripple/protocol/STTx.h index c6a9e053c3..e8b7d80cb6 100644 --- a/src/ripple/protocol/STTx.h +++ b/src/ripple/protocol/STTx.h @@ -171,6 +171,9 @@ sterilize(STTx const& stx); bool isPseudoTx(STObject const& tx); +bool +isUVTx(STObject const& tx); + inline STTx::STTx(SerialIter&& sit) : STTx(sit) { } diff --git a/src/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index b16fe8e0da..06d14932a7 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -324,13 +324,18 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const fullyCanonical); } } - catch (std::exception const&) + catch (std::exception const & e) { // Assume it was a signature failure. validSig = false; + + std::cout << "Invalid cause: " << e.what() << "\n"; } if (validSig == false) + { + std::cout << "Invalid signature on tx: " << this->getFullText() << "\n"; return Unexpected("Invalid signature."); + } // Signature was verified. return {}; } @@ -618,4 +623,15 @@ isPseudoTx(STObject const& tx) tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON || tt == ttSHUFFLE; } +bool +isUVTx(STObject const& tx) +{ + auto t = tx[~sfTransactionType]; + if (!t) + return false; + + auto tt = safe_cast(*t); + return tt == ttENTROPY; +} + } // namespace ripple diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index c1a483811b..9691e4e290 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -496,7 +496,6 @@ TxFormats::TxFormats() { {sfRandomData, soeREQUIRED}, {sfNextRandomDigest, soeREQUIRED}, - {sfLedgerSequence, soeREQUIRED}, {sfParentHash, soeREQUIRED}, }, commonFields); From f738b8fd7c0c8b0b90ea193b86ff995649d6542e Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Sat, 3 Jan 2026 15:36:38 +1100 Subject: [PATCH 08/11] fix entropy count --- src/ripple/app/tx/impl/Change.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 074d052eea..757993d905 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -262,11 +262,9 @@ Change::applyShuffle() // update the ledger sequence of the object sle->setFieldU32(sfLedgerSequence, seq); } - else - { - // increment entropy count - sle->setFieldU16(sfEntropyCount, sle->getFieldU16(sfEntropyCount) + 1); - } + + // increment entropy count + sle->setFieldU16(sfEntropyCount, sle->getFieldU16(sfEntropyCount) + 1); // contribute the new entropy to the random data field sle->setFieldH256(sfRandomData, From 889cda2a569395c32c7fb33b2b33387ad40f7f17 Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Mon, 5 Jan 2026 15:10:23 +1100 Subject: [PATCH 09/11] condition clean up ahead of ttEXPORT --- src/ripple/app/misc/impl/TxQ.cpp | 5 +-- src/ripple/app/tx/impl/Entropy.cpp | 58 ++------------------------- src/ripple/app/tx/impl/Entropy.h | 7 +++- src/ripple/app/tx/impl/Transactor.cpp | 37 +++++++---------- 4 files changed, 24 insertions(+), 83 deletions(-) diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index adcc8c9b47..1ffa96aa57 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -1931,10 +1931,7 @@ TxQ::tryDirectApply( const bool isFirstImport = !sleAccount && view.rules().enabled(featureImport) && tx->getTxnType() == ttIMPORT; - const bool isUV = - view.rules().enabled(featureRNG) && tx->getTxnType() == ttENTROPY; - - const bool accRequired = !(isFirstImport || isUV); + const bool accRequired = !(isFirstImport || isUVTx(*tx)); // Don't attempt to direct apply if the account is not in the ledger. if (!sleAccount && accRequired) diff --git a/src/ripple/app/tx/impl/Entropy.cpp b/src/ripple/app/tx/impl/Entropy.cpp index 9367a5c78c..701b6e8579 100644 --- a/src/ripple/app/tx/impl/Entropy.cpp +++ b/src/ripple/app/tx/impl/Entropy.cpp @@ -39,6 +39,9 @@ Entropy::preflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; + if (!ctx.rules.enabled(featureRNG)) + return temDISABLED; + return preflight2(ctx); } @@ -48,17 +51,6 @@ Entropy::preclaim(PreclaimContext const& ctx) if (!ctx.view.rules().enabled(featureRNG)) return temDISABLED; -// auto const seq = ctx.view.info().seq; - -// auto const txLgrSeq = ctx.tx[sfLedgerSequence]; - - // due to circulation we'll accept up to one ledger old entropy txns -// if (seq != txLgrSeq) -// { -// JLOG(ctx.j.warn()) << "Entropy: wrong ledger txseq=" << txLgrSeq << " lgrseq=" << seq << " acc:" << ctx.tx.getAccountID(sfAccount); -// return tefFAILURE; -// } - // account must be a valid UV if (!inUNLReport(ctx.view, ctx.tx.getAccountID(sfAccount), ctx.j)) { @@ -76,19 +68,12 @@ Entropy::doApply() auto const seq = view().info().seq; -// if (seq != ctx_.tx.getFieldU32(sfLedgerSequence)) -// { -// return tefFAILURE; -// } - auto sle = view().peek(keylet::random()); bool const created = !sle; if (created) - { sle = std::make_shared(keylet::random()); - } auto lastSeq = created ? 0 : sle->getFieldU32(sfLedgerSequence); @@ -147,7 +132,6 @@ Entropy::doApply() } else { - // contribute the new entropy to the random data field sle->setFieldH256(sfRandomData, sha512Half(validator, sle->getFieldH256(sfRandomData), currentEntropy)); @@ -186,46 +170,11 @@ Entropy::doApply() return tesSUCCESS; } - -XRPAmount -Entropy::calculateBaseFee(ReadView const& view, STTx const& tx) -{ - XRPAmount extraFee{0}; - - if (tx.isFieldPresent(sfBlob)) - extraFee += - XRPAmount{static_cast(tx.getFieldVL(sfBlob).size())}; - - // old code (prior to fixXahauV1) - if (!view.rules().enabled(fixXahauV1)) - { - if (tx.isFieldPresent(sfHookParameters)) - { - uint64_t paramBytes = 0; - auto const& params = tx.getFieldArray(sfHookParameters); - for (auto const& param : params) - { - paramBytes += - (param.isFieldPresent(sfHookParameterName) - ? param.getFieldVL(sfHookParameterName).size() - : 0) + - (param.isFieldPresent(sfHookParameterValue) - ? param.getFieldVL(sfHookParameterValue).size() - : 0); - } - extraFee += XRPAmount{static_cast(paramBytes)}; - } - } - - return Transactor::calculateBaseFee(view, tx) + extraFee; -} - // if this validator is on the UNLReport then return a signed ttENTROPY transaction // to be added to the txq. std::shared_ptr makeEntropyTxn(OpenView& view, Application& app, beast::Journal const& j_) { - // Inject an RNG psuedo if we're on the UNL if (!view.rules().enabled(featureRNG)) return {}; @@ -250,6 +199,7 @@ makeEntropyTxn(OpenView& view, Application& app, beast::Journal const& j_) auto const pk = app.validatorManifests().getMasterKey(pkSigning); + // Only continue if we're on the UNL if (!inUNLReport(view, app, pk, j_)) return {}; diff --git a/src/ripple/app/tx/impl/Entropy.h b/src/ripple/app/tx/impl/Entropy.h index 5af0a91a26..cf5a9c48e5 100644 --- a/src/ripple/app/tx/impl/Entropy.h +++ b/src/ripple/app/tx/impl/Entropy.h @@ -37,9 +37,12 @@ class Entropy : public Transactor explicit Entropy(ApplyContext& ctx) : Transactor(ctx) { } - + static XRPAmount - calculateBaseFee(ReadView const& view, STTx const& tx); + calculateBaseFee(ReadView const& view, STTx const& tx) + { + return XRPAmount{0}; + } static TxConsequences makeTxConsequences(PreflightContext const& ctx); diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index a50fa47ca7..4716db1ad0 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -273,6 +273,9 @@ Transactor::calculateHookChainFee( XRPAmount Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) { + if (isUVTx(tx)) + return XRPAmount{0}; + // Returns the fee in fee units. // The computation has two parts: @@ -289,9 +292,6 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) baseFee = importFee; } - if (tt == ttENTROPY) - return XRPAmount{0}; - // Each signer adds one more baseFee to the minimum required fee // for the transaction. std::size_t const signerCount = @@ -481,7 +481,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) auto const sle = ctx.view.read(keylet::account(id)); if (!sle) { - if (ctx.tx.getTxnType() == ttENTROPY) + if (isUVTx(ctx.tx)) return tesSUCCESS; if (ctx.tx.getTxnType() == ttIMPORT) @@ -564,12 +564,12 @@ Transactor::checkSeqProxy( return tesSUCCESS; } - if (view.rules().enabled(featureRNG) && - tx.getTxnType() == ttENTROPY && t_seqProx.isSeq() && + if (isUVTx(tx) && + t_seqProx.isSeq() && tx[sfSequence] == 0) { JLOG(j.trace()) - << "applyTransaction: allowing ttENTROPY with seq=0 " + << "applyTransaction: allowing UVTx with seq=0 " << toBase58(id); return tesSUCCESS; } @@ -658,11 +658,7 @@ Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx) ctx.view.rules().enabled(featureImport) && ctx.tx.getTxnType() == ttIMPORT && !ctx.tx.isFieldPresent(sfIssuer); - bool const isUV = - ctx.view.rules().enabled(featureRNG) && - ctx.tx.getTxnType() == ttENTROPY; - - bool const accRequired = !(isFirstImport || isUV); + bool const accRequired = !(isFirstImport || isUVTx(ctx.tx)); if (!sle && accRequired) { @@ -826,7 +822,7 @@ Transactor::apply() view().rules().enabled(featureImport) && ctx_.tx.getTxnType() == ttIMPORT && !ctx_.tx.isFieldPresent(sfIssuer) || - ctx_.tx.getTxnType() == ttENTROPY); + isUVTx(ctx_.tx)); if (sle) { @@ -903,16 +899,11 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) auto const idAccount = ctx.tx.getAccountID(sfAccount); auto const sleAccount = ctx.view.read(keylet::account(idAccount)); - // check if this is a UVTxn (UNL Validator transaction) - // these are signed by the UNL validators - if (ctx.view.rules().enabled(featureRNG)) - { - // UVTxns of the approved type don't need an underlying account - // and can be signed with the manifest ephemeral key - if (inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j) && - ctx.tx.getTxnType() == ttENTROPY) - return tesSUCCESS; - } + // UVTxns of the approved type don't need an underlying account + // and can be signed with the manifest ephemeral key + if (isUVTx(ctx.tx) && + inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j)) + return tesSUCCESS; if (!sleAccount) return terNO_ACCOUNT; From b6518f2b65dbcdfccd63e6bc8678ae9a6459f35b Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Mon, 5 Jan 2026 15:28:58 +1100 Subject: [PATCH 10/11] clang --- src/ripple/app/consensus/RCLConsensus.cpp | 4 +- src/ripple/app/ledger/impl/BuildLedger.cpp | 66 ++++++++--------- src/ripple/app/main/Application.h | 2 +- src/ripple/app/misc/NetworkOPs.h | 1 - src/ripple/app/misc/impl/TxQ.cpp | 2 +- src/ripple/app/tx/impl/Change.cpp | 36 +++++---- src/ripple/app/tx/impl/Entropy.cpp | 38 +++++----- src/ripple/app/tx/impl/Entropy.h | 10 +-- src/ripple/app/tx/impl/Transactor.cpp | 26 +++---- src/ripple/app/tx/impl/applySteps.cpp | 2 +- src/ripple/consensus/Consensus.h | 8 +- src/ripple/ledger/View.h | 20 ++--- src/ripple/overlay/impl/PeerImp.cpp | 6 +- src/ripple/protocol/impl/Indexes.cpp | 5 +- src/ripple/protocol/impl/LedgerFormats.cpp | 1 - src/ripple/protocol/impl/STTx.cpp | 5 +- src/ripple/protocol/impl/TxFormats.cpp | 1 - src/ripple/protocol/jss.h | 86 +++++++++++----------- 18 files changed, 154 insertions(+), 165 deletions(-) diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index 81230e8d8a..b93b96dea0 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -35,6 +35,8 @@ #include #include #include +#include +#include #include #include #include @@ -44,8 +46,6 @@ #include #include #include -#include -#include #include #include diff --git a/src/ripple/app/ledger/impl/BuildLedger.cpp b/src/ripple/app/ledger/impl/BuildLedger.cpp index e21ec4b00f..c319c6e006 100644 --- a/src/ripple/app/ledger/impl/BuildLedger.cpp +++ b/src/ripple/app/ledger/impl/BuildLedger.cpp @@ -104,49 +104,49 @@ applyTransactions( bool certainRetry = true; std::size_t count = 0; - // apply the ttSHUFFLE txns first in the ledger to + // apply the ttSHUFFLE txns first in the ledger to // ensure no one can predict the outcome // then apply ttENTROPY transactions if (view.rules().enabled(featureRNG)) - for (auto tt : {ttSHUFFLE, ttENTROPY}) - { - for (auto it = txns.begin(); it != txns.end();) + for (auto tt : {ttSHUFFLE, ttENTROPY}) { - if (tt != it->second->getFieldU16(sfTransactionType)) - { - ++it; - continue; - } - - auto const txid = it->first.getTXID(); - try + for (auto it = txns.begin(); it != txns.end();) { - switch (applyTransaction( - app, view, *it->second, certainRetry, tapNONE, j)) + if (tt != it->second->getFieldU16(sfTransactionType)) { - case ApplyResult::Success: - it = txns.erase(it); - ++count; - break; - - case ApplyResult::Fail: - failed.insert(txid); - it = txns.erase(it); - break; + ++it; + continue; + } - case ApplyResult::Retry: - ++it; + auto const txid = it->first.getTXID(); + try + { + switch (applyTransaction( + app, view, *it->second, certainRetry, tapNONE, j)) + { + case ApplyResult::Success: + it = txns.erase(it); + ++count; + break; + + case ApplyResult::Fail: + failed.insert(txid); + it = txns.erase(it); + break; + + case ApplyResult::Retry: + ++it; + } + } + catch (std::exception const& ex) + { + JLOG(j.warn()) + << "Transaction " << txid << " throws: " << ex.what(); + failed.insert(txid); + it = txns.erase(it); } - } - catch (std::exception const& ex) - { - JLOG(j.warn()) - << "Transaction " << txid << " throws: " << ex.what(); - failed.insert(txid); - it = txns.erase(it); } } - } // Attempt to apply all of the retriable transactions for (int pass = 0; pass < LEDGER_TOTAL_PASSES; ++pass) diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h index 3c45f67e6c..ea0cc79fec 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -240,7 +240,7 @@ class Application : public beast::PropertyStream::Source virtual PublicKey const& getValidationPublicKey() const = 0; - + virtual SecretKey const& getValidationSecretKey() const = 0; diff --git a/src/ripple/app/misc/NetworkOPs.h b/src/ripple/app/misc/NetworkOPs.h index 630808b859..1f0eb1d4df 100644 --- a/src/ripple/app/misc/NetworkOPs.h +++ b/src/ripple/app/misc/NetworkOPs.h @@ -112,7 +112,6 @@ class NetworkOPs : public InfoSub::Source bool bLocal, FailHard failType) = 0; - // directly inject transaction, skipping checks virtual void doTransactionSync( diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index 1ffa96aa57..34172bea6b 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -25,12 +25,12 @@ #include #include #include +#include #include #include #include #include #include -#include namespace ripple { diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 757993d905..23248e91de 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -32,7 +33,6 @@ #include #include #include -#include #include namespace ripple { @@ -228,18 +228,18 @@ Change::doApply() TER Change::applyShuffle() { - auto const seq = view().info().seq; auto const txSeq = ctx_.tx.getFieldU32(sfLedgerSequence); if (seq != txSeq) { - JLOG(j_.warn()) << "Change: ttSHUFFLE, wrong ledger seq. lgr=" << seq << " tx=" << txSeq; + JLOG(j_.warn()) << "Change: ttSHUFFLE, wrong ledger seq. lgr=" << seq + << " tx=" << txSeq; return tefFAILURE; } auto sle = view().peek(keylet::random()); - + bool const created = !sle; if (created) @@ -258,23 +258,23 @@ Change::applyShuffle() // swap the random data out ready for this round of entropy collection sle->setFieldH256(sfLastRandomData, sle->getFieldH256(sfRandomData)); - + // update the ledger sequence of the object sle->setFieldU32(sfLedgerSequence, seq); } - + // increment entropy count sle->setFieldU16(sfEntropyCount, sle->getFieldU16(sfEntropyCount) + 1); - + // contribute the new entropy to the random data field - sle->setFieldH256(sfRandomData, - sha512Half( - seq, - sle->getFieldU16(sfEntropyCount), - sle->getFieldH256(sfRandomData), - ctx_.tx.getFieldH256(sfRandomData))); - - + sle->setFieldH256( + sfRandomData, + sha512Half( + seq, + sle->getFieldU16(sfEntropyCount), + sle->getFieldH256(sfRandomData), + ctx_.tx.getFieldH256(sfRandomData))); + if (!created) view().update(sle); else @@ -1277,7 +1277,7 @@ injectShuffleTxn(Application& app, Slice const& sig) { uint256 rnd = sha512Half(std::string("shuffler"), sig); // create txn - STTx shuffleTx (ttSHUFFLE, [&](auto& obj) { + STTx shuffleTx(ttSHUFFLE, [&](auto& obj) { obj.setFieldU32(sfLedgerSequence, ol->info().seq + 1); obj.setFieldH256(sfRandomData, rnd); obj.setAccountID(sfAccount, AccountID()); @@ -1288,8 +1288,7 @@ injectShuffleTxn(Application& app, Slice const& sig) JLOG(app.journal("Transaction").debug()) << "SHUFFLE processing: Submitting pseudo: " - << shuffleTx.getFullText() - << " txid: " << txID; + << shuffleTx.getFullText() << " txid: " << txID; app.getHashRouter().setFlags(txID, SF_PRIVATE2); app.getHashRouter().setFlags(txID, SF_EMITTED); @@ -1307,7 +1306,6 @@ injectShuffleTxn(Application& app, Slice const& sig) view.rawTxInsert(txID, std::move(s), nullptr); return true; }); - } } } diff --git a/src/ripple/app/tx/impl/Entropy.cpp b/src/ripple/app/tx/impl/Entropy.cpp index 701b6e8579..5fe704c954 100644 --- a/src/ripple/app/tx/impl/Entropy.cpp +++ b/src/ripple/app/tx/impl/Entropy.cpp @@ -22,8 +22,8 @@ #include #include #include -#include #include +#include namespace ripple { @@ -51,11 +51,10 @@ Entropy::preclaim(PreclaimContext const& ctx) if (!ctx.view.rules().enabled(featureRNG)) return temDISABLED; - // account must be a valid UV + // account must be a valid UV if (!inUNLReport(ctx.view, ctx.tx.getAccountID(sfAccount), ctx.j)) { - JLOG(ctx.j.warn()) - << "Entropy: Txn Account isn't in the UNLReport."; + JLOG(ctx.j.warn()) << "Entropy: Txn Account isn't in the UNLReport."; return tefFAILURE; } @@ -65,7 +64,6 @@ Entropy::preclaim(PreclaimContext const& ctx) TER Entropy::doApply() { - auto const seq = view().info().seq; auto sle = view().peek(keylet::random()); @@ -104,8 +102,8 @@ Entropy::doApply() for (auto& entry : digestEntries) { - // we'll automatically clean up really old entries by just omitting them from - // the map here + // we'll automatically clean up really old entries by just omitting them + // from the map here if (entry.getFieldU32(sfLedgerSequence) < seq - 5) continue; @@ -121,22 +119,30 @@ Entropy::doApply() { if (entry.getFieldU32(sfLedgerSequence) != seq - 1) { - // this is a skip-ahead or missed last txn somehow, so ignore, but no warning. + // this is a skip-ahead or missed last txn somehow, so ignore, + // but no warning. } else { // this is a clear violation so warn (and ignore the entropy) - JLOG(j_.warn()) << "!!! Validator " << validator << " supplied entropy that " - << "does not match precommitment value !!!"; + JLOG(j_.warn()) << "!!! Validator " << validator + << " supplied entropy that " + << "does not match precommitment value !!!"; } } else { // contribute the new entropy to the random data field - sle->setFieldH256(sfRandomData, sha512Half(validator, sle->getFieldH256(sfRandomData), currentEntropy)); + sle->setFieldH256( + sfRandomData, + sha512Half( + validator, + sle->getFieldH256(sfRandomData), + currentEntropy)); // increment entropy count - sle->setFieldU16(sfEntropyCount, sle->getFieldU16(sfEntropyCount) + 1); + sle->setFieldU16( + sfEntropyCount, sle->getFieldU16(sfEntropyCount) + 1); } // update the digest entry @@ -170,16 +176,15 @@ Entropy::doApply() return tesSUCCESS; } -// if this validator is on the UNLReport then return a signed ttENTROPY transaction -// to be added to the txq. +// if this validator is on the UNLReport then return a signed ttENTROPY +// transaction to be added to the txq. std::shared_ptr makeEntropyTxn(OpenView& view, Application& app, beast::Journal const& j_) { if (!view.rules().enabled(featureRNG)) return {}; - JLOG(j_.debug()) - << "ENTROPY processing: started"; + JLOG(j_.debug()) << "ENTROPY processing: started"; auto const seq = view.info().seq; @@ -218,7 +223,6 @@ makeEntropyTxn(OpenView& view, Application& app, beast::Journal const& j_) return out; }; - static std::optional prevRnd; uint256 nextRnd = getRnd(); diff --git a/src/ripple/app/tx/impl/Entropy.h b/src/ripple/app/tx/impl/Entropy.h index cf5a9c48e5..c9ecbbcf30 100644 --- a/src/ripple/app/tx/impl/Entropy.h +++ b/src/ripple/app/tx/impl/Entropy.h @@ -20,12 +20,12 @@ #ifndef RIPPLE_TX_ENTROPY_H_INCLUDED #define RIPPLE_TX_ENTROPY_H_INCLUDED +#include #include #include #include -#include -#include #include +#include namespace ripple { @@ -37,7 +37,7 @@ class Entropy : public Transactor explicit Entropy(ApplyContext& ctx) : Transactor(ctx) { } - + static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx) { @@ -57,8 +57,8 @@ class Entropy : public Transactor doApply() override; }; -// if this validator is on the UNLReport then return a signed ttENTROPY transaction -// to be added to the txq. +// if this validator is on the UNLReport then return a signed ttENTROPY +// transaction to be added to the txq. std::shared_ptr makeEntropyTxn(OpenView& view, Application& app, beast::Journal const& j_); diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 4716db1ad0..afb93b1e7f 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -447,16 +447,14 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) // Only check fee is sufficient when the ledger is open. if (ctx.view.open()) { - - auto feeDue = - minimumFee(ctx.app, baseFee, ctx.view.fees(), ctx.flags); + auto feeDue = minimumFee(ctx.app, baseFee, ctx.view.fees(), ctx.flags); if (ctx.view.rules().enabled(featureRNG)) - { + { auto const pkSignerField = ctx.tx.getSigningPubKey(); if (publicKeyType(makeSlice(pkSignerField))) - { - PublicKey pkSigner {makeSlice(pkSignerField)}; + { + PublicKey pkSigner{makeSlice(pkSignerField)}; if (inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j)) { // UVTxns don't have to pay a fee @@ -564,13 +562,10 @@ Transactor::checkSeqProxy( return tesSUCCESS; } - if (isUVTx(tx) && - t_seqProx.isSeq() && - tx[sfSequence] == 0) + if (isUVTx(tx) && t_seqProx.isSeq() && tx[sfSequence] == 0) { - JLOG(j.trace()) - << "applyTransaction: allowing UVTx with seq=0 " - << toBase58(id); + JLOG(j.trace()) << "applyTransaction: allowing UVTx with seq=0 " + << toBase58(id); return tesSUCCESS; } @@ -822,7 +817,7 @@ Transactor::apply() view().rules().enabled(featureImport) && ctx_.tx.getTxnType() == ttIMPORT && !ctx_.tx.isFieldPresent(sfIssuer) || - isUVTx(ctx_.tx)); + isUVTx(ctx_.tx)); if (sle) { @@ -892,7 +887,7 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) return tefBAD_AUTH; // FIXME: should be better error! } - PublicKey pkSigner {makeSlice(pkSignerField)}; + PublicKey pkSigner{makeSlice(pkSignerField)}; // Look up the account. auto const idSigner = calcAccountID(pkSigner); @@ -901,8 +896,7 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) // UVTxns of the approved type don't need an underlying account // and can be signed with the manifest ephemeral key - if (isUVTx(ctx.tx) && - inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j)) + if (isUVTx(ctx.tx) && inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j)) return tesSUCCESS; if (!sleAccount) diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 57b6e6d686..12cb323f19 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -52,7 +53,6 @@ #include #include #include -#include namespace ripple { diff --git a/src/ripple/consensus/Consensus.h b/src/ripple/consensus/Consensus.h index e7581cf18a..d1a2f462f2 100644 --- a/src/ripple/consensus/Consensus.h +++ b/src/ripple/consensus/Consensus.h @@ -20,6 +20,9 @@ #ifndef RIPPLE_CONSENSUS_CONSENSUS_H_INCLUDED #define RIPPLE_CONSENSUS_CONSENSUS_H_INCLUDED +#include +#include +#include #include #include #include @@ -29,14 +32,11 @@ #include #include #include +#include #include #include #include #include -#include -#include -#include -#include namespace ripple { diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index a6990a47bf..a541177bd8 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -20,6 +20,8 @@ #ifndef RIPPLE_LEDGER_VIEW_H_INCLUDED #define RIPPLE_LEDGER_VIEW_H_INCLUDED +#include +#include #include #include #include @@ -36,8 +38,6 @@ #include #include #include -#include -#include #include #include #include @@ -1098,12 +1098,9 @@ trustTransferLockedBalance( } template -bool inUNLReport( - V const& view, - AccountID const& id, - beast::Journal const& j) +bool +inUNLReport(V const& view, AccountID const& id, beast::Journal const& j) { - auto const seq = view.info().seq; static uint32_t lastLgrSeq = 0; static std::map cache; @@ -1128,8 +1125,7 @@ bool inUNLReport( auto const unlRep = view.read(keylet::UNLReport()); if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators)) { - JLOG(j.debug()) - << "UNLReport misssing"; + JLOG(j.debug()) << "UNLReport misssing"; // ensure we keep the cache invalid when in this state lastLgrSeq = 0; @@ -1146,9 +1142,9 @@ bool inUNLReport( return cache[id] = false; } - template -bool inUNLReport( +bool +inUNLReport( V const& view, Application& app, PublicKey const& pk, @@ -1156,7 +1152,7 @@ bool inUNLReport( { PublicKey uvPk = app.validatorManifests().getMasterKey(pk); - return inUNLReport(view, calcAccountID(pk), j) || + return inUNLReport(view, calcAccountID(pk), j) || (uvPk != pk && inUNLReport(view, calcAccountID(uvPk), j)); } diff --git a/src/ripple/overlay/impl/PeerImp.cpp b/src/ripple/overlay/impl/PeerImp.cpp index 3f4ef03b68..1ad736910a 100644 --- a/src/ripple/overlay/impl/PeerImp.cpp +++ b/src/ripple/overlay/impl/PeerImp.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -40,8 +41,6 @@ #include #include #include -#include -#include #include #include @@ -1957,7 +1956,8 @@ PeerImp::onMessage(std::shared_ptr const& m) if (!isTrusted && app_.config().RELAY_UNTRUSTED_PROPOSALS == -1) return; - // ttSHUFFLE is injected as part of featureRNG, based on the proposal signature + // ttSHUFFLE is injected as part of featureRNG, based on the proposal + // signature injectShuffleTxn(app_, makeSlice(set.signature())); uint256 const proposeHash{set.currenttxhash()}; diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index eb00941bb4..9fae6786e4 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -73,7 +73,7 @@ enum class LedgerNameSpace : std::uint16_t { IMPORT_VLSEQ = 'I', UNL_REPORT = 'R', CRON = 'L', - RANDOM = 0x526E, // Rn + RANDOM = 0x526E, // Rn // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -500,8 +500,7 @@ cron(uint32_t timestamp, std::optional const& id) Keylet const& random() noexcept { - static Keylet const ret{ - ltRANDOM, indexHash(LedgerNameSpace::RANDOM)}; + static Keylet const ret{ltRANDOM, indexHash(LedgerNameSpace::RANDOM)}; return ret; } diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 3dbae56a1d..5ff6da1a7f 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -394,7 +394,6 @@ LedgerFormats::LedgerFormats() }, commonFields); - // clang-format on } diff --git a/src/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index 06d14932a7..773758579b 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -324,7 +324,7 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const fullyCanonical); } } - catch (std::exception const & e) + catch (std::exception const& e) { // Assume it was a signature failure. validSig = false; @@ -620,7 +620,8 @@ isPseudoTx(STObject const& tx) auto tt = safe_cast(*t); return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY || - tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON || tt == ttSHUFFLE; + tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON || + tt == ttSHUFFLE; } bool diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 9691e4e290..b8d19a2800 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -507,7 +507,6 @@ TxFormats::TxFormats() {sfRandomData, soeREQUIRED}, }, commonFields); - } TxFormats const& diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 1ebc658d59..8cc9292fee 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -97,51 +97,51 @@ JSS(isSigningField); // out: RPC server_definitions JSS(isVLEncoded); // out: RPC server_definitions JSS(Import); JSS(ImportVLSequence); -JSS(Invalid); // -JSS(Invoke); // transaction type -JSS(InvoiceID); // field -JSS(LastLedgerSequence); // in: TransactionSign; field -JSS(LedgerHashes); // ledger type. -JSS(LimitAmount); // field. -JSS(NetworkID); // field. -JSS(NFTokenBurn); // transaction type. -JSS(NFTokenMint); // transaction type. -JSS(NFTokenOffer); // ledger type. -JSS(NFTokenAcceptOffer); // transaction type. -JSS(NFTokenCancelOffer); // transaction type. -JSS(NFTokenCreateOffer); // transaction type. -JSS(NFTokenPage); // ledger type. -JSS(Offer); // ledger type. -JSS(OfferCancel); // transaction type. -JSS(OfferCreate); // transaction type. -JSS(OfferSequence); // field. -JSS(Paths); // in/out: TransactionSign -JSS(PayChannel); // ledger type. -JSS(Payment); // transaction type. -JSS(PaymentChannelClaim); // transaction type. -JSS(PaymentChannelCreate); // transaction type. -JSS(PaymentChannelFund); // transaction type. -JSS(Remit); // transaction type. -JSS(RippleState); // ledger type. +JSS(Invalid); // +JSS(Invoke); // transaction type +JSS(InvoiceID); // field +JSS(LastLedgerSequence); // in: TransactionSign; field +JSS(LedgerHashes); // ledger type. +JSS(LimitAmount); // field. +JSS(NetworkID); // field. +JSS(NFTokenBurn); // transaction type. +JSS(NFTokenMint); // transaction type. +JSS(NFTokenOffer); // ledger type. +JSS(NFTokenAcceptOffer); // transaction type. +JSS(NFTokenCancelOffer); // transaction type. +JSS(NFTokenCreateOffer); // transaction type. +JSS(NFTokenPage); // ledger type. +JSS(Offer); // ledger type. +JSS(OfferCancel); // transaction type. +JSS(OfferCreate); // transaction type. +JSS(OfferSequence); // field. +JSS(Paths); // in/out: TransactionSign +JSS(PayChannel); // ledger type. +JSS(Payment); // transaction type. +JSS(PaymentChannelClaim); // transaction type. +JSS(PaymentChannelCreate); // transaction type. +JSS(PaymentChannelFund); // transaction type. +JSS(Remit); // transaction type. +JSS(RippleState); // ledger type. JSS(Rng); JSS(Random); -JSS(SLE_hit_rate); // out: GetCounts. -JSS(SetFee); // transaction type. -JSS(SetRemarks); // transaction type -JSS(UNLModify); // transaction type. -JSS(UNLReport); // transaction type. -JSS(SettleDelay); // in: TransactionSign -JSS(SendMax); // in: TransactionSign -JSS(Sequence); // in/out: TransactionSign; field. -JSS(SetFlag); // field. -JSS(SetRegularKey); // transaction type. -JSS(SetHook); // transaction type. -JSS(Hook); // ledger type. -JSS(HookDefinition); // ledger type. -JSS(HookState); // ledger type. -JSS(HookStateData); // field. -JSS(HookStateKey); // field. -JSS(EmittedTxn); // ledger type. +JSS(SLE_hit_rate); // out: GetCounts. +JSS(SetFee); // transaction type. +JSS(SetRemarks); // transaction type +JSS(UNLModify); // transaction type. +JSS(UNLReport); // transaction type. +JSS(SettleDelay); // in: TransactionSign +JSS(SendMax); // in: TransactionSign +JSS(Sequence); // in/out: TransactionSign; field. +JSS(SetFlag); // field. +JSS(SetRegularKey); // transaction type. +JSS(SetHook); // transaction type. +JSS(Hook); // ledger type. +JSS(HookDefinition); // ledger type. +JSS(HookState); // ledger type. +JSS(HookStateData); // field. +JSS(HookStateKey); // field. +JSS(EmittedTxn); // ledger type. JSS(Entropy); JSS(Shuffle); JSS(SignerList); // ledger type. From 2a8383c6a906b9a31a5f0b3f2c9b9bfdbc6bd808 Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Mon, 5 Jan 2026 16:54:02 +1100 Subject: [PATCH 11/11] hook apis for rng --- src/ripple/app/hook/Enum.h | 5 +- src/ripple/app/hook/applyHook.h | 8 ++ src/ripple/app/hook/impl/applyHook.cpp | 111 +++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/ripple/app/hook/Enum.h b/src/ripple/app/hook/Enum.h index 9728b10401..504af0c5aa 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 { @@ -459,6 +460,8 @@ static const APIWhitelist import_whitelist{ HOOK_API_DEFINITION(I64, otxn_slot, (I32)), HOOK_API_DEFINITION(I64, otxn_param, (I32, I32, I32, I32)), HOOK_API_DEFINITION(I64, meta_slot, (I32)), + HOOK_API_DEFINITION(I64, dice, (I32)), + HOOK_API_DEFINITION(I64, random, (I32, I32)), // clang-format on }; diff --git a/src/ripple/app/hook/applyHook.h b/src/ripple/app/hook/applyHook.h index 2c1f250bbf..cffb15c294 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; @@ -877,6 +882,9 @@ class HookExecutor ADD_HOOK_FUNCTION(meta_slot, ctx); ADD_HOOK_FUNCTION(xpop_slot, ctx); + ADD_HOOK_FUNCTION(dice, ctx); + ADD_HOOK_FUNCTION(random, ctx); + /* ADD_HOOK_FUNCTION(str_find, ctx); ADD_HOOK_FUNCTION(str_replace, ctx); diff --git a/src/ripple/app/hook/impl/applyHook.cpp b/src/ripple/app/hook/impl/applyHook.cpp index f27b7d23b3..c06addedfa 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 sleRandom = view.peek(ripple::keylet::random()); + auto const seq = view.info().seq; + + if (!sleRandom || sleRandom->getFieldU32(sfLedgerSequence) != seq || + sleRandom->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"), + sleRandom->getFieldH256(sfRandomData), + 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(