From 64cfdae9e96c32dcbf2c08430334cd6c091ab43c Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Fri, 19 Dec 2025 22:54:35 +1100 Subject: [PATCH] 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 56feda066..203c450b4 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 5c4af9f8b..ee37c02c2 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 37a436fee..95d925313 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 a18f78e1c..50818caf8 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 2766859dc..24436d1c3 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 f507b2232..fbf181c57 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 bea5905cb..54ec86e6d 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 24383f896..ca4236c9d 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 c4f2ef85a..4c9c8023a 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);