From c01b9a657b71619cc0542dc35a5ae2d8aae6afae Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Tue, 20 Jan 2026 13:44:38 +0700 Subject: [PATCH] feat: implement uvtxn pattern for ttEXPORT_SIGN Port the UNL Validator Transaction (UVTxn) pattern from the RNG feature to allow validators to submit signed ttEXPORT_SIGN transactions without requiring a funded account. Changes: - Add isUVTx() to identify UVTxn transaction types - Add inUNLReport() templates to check validator UNLReport membership - Add getValidationSecretKey() to Application for signing - Modify Transactor for UVTxn bypasses (fee, seq, signature checks) - Add makeExportSignTxns() to generate validator signatures - Hook into RCLConsensus to submit ttEXPORT_SIGN during accept - Update applySteps.cpp routing for ttEXPORT_SIGN - Remove direct ttEXPORT_SIGN injection from TxQ::accept Note: Currently uses Change transactor with UVTx branches. May refactor to dedicated ExportSign transactor class. --- src/ripple/app/consensus/RCLConsensus.cpp | 11 + src/ripple/app/main/Application.cpp | 6 + src/ripple/app/main/Application.h | 4 + src/ripple/app/misc/impl/TxQ.cpp | 74 +------ src/ripple/app/tx/impl/Change.cpp | 241 +++++++++++++++++++--- src/ripple/app/tx/impl/Change.h | 8 + src/ripple/app/tx/impl/Transactor.cpp | 60 +++++- src/ripple/app/tx/impl/applySteps.cpp | 12 ++ src/ripple/ledger/View.h | 70 +++++++ src/ripple/protocol/STTx.h | 4 + src/ripple/protocol/impl/STTx.cpp | 14 +- 11 files changed, 407 insertions(+), 97 deletions(-) diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index 4fb0c5275..e39334d23 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -652,6 +653,16 @@ RCLConsensus::Adaptor::doAccept( tapNONE, "consensus", [&](OpenView& view, beast::Journal j) { + //@@start export-sign-submit + // Generate and submit ttEXPORT_SIGN UVTxns if we're a + // validator on the UNLReport + if (view.rules().enabled(featureExport)) + { + auto exportSignTxns = makeExportSignTxns(view, app_, j_); + for (auto const& tx : exportSignTxns) + app_.getOPs().submitTransaction(tx); + } + //@@end export-sign-submit return app_.getTxQ().accept(app_, view); }); diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 5d952c376..a118d9066 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -599,6 +599,12 @@ public: return validatorKeys_.publicKey; } + SecretKey const& + getValidationSecretKey() const override + { + return validatorKeys_.secretKey; + } + ValidatorKeys const& getValidatorKeys() const override { diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h index 63c9c54e4..ba6ebf924 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -240,6 +240,10 @@ public: virtual PublicKey const& getValidationPublicKey() const = 0; + + virtual SecretKey const& + getValidationSecretKey() const = 0; + virtual ValidatorKeys const& getValidatorKeys() const = 0; virtual Resource::Manager& diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index 77df92246..317a072f5 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -1580,8 +1580,6 @@ TxQ::accept(Application& app, OpenView& view) // execution to here means we're a validator and on the UNLReport - AccountID signingAcc = calcAccountID(keys.publicKey); - Keylet const exportedDirKeylet{keylet::exportedDir()}; if (dirIsEmpty(view, exportedDirKeylet)) break; @@ -1715,65 +1713,10 @@ TxQ::accept(Application& app, OpenView& view) continue; } - // this ledger is the one after the exported txn was added to - // the directory so generate the export sign txns - - auto s = std::make_shared(); - exported.add(*s); - SerialIter sitTrans(s->slice()); - try - { - auto const& stpTrans = - std::make_shared(std::ref(sitTrans)); - - if (!stpTrans->isFieldPresent(sfAccount) || - stpTrans->getAccountID(sfAccount) == beast::zero) - { - JLOG(j_.warn()) << "Hook: Export failure: " - << "sfAccount missing or zero."; - // RH TODO: if this ever happens the entry should be - // gracefully removed (somehow) - continue; - } - - auto seq = view.info().seq; - auto txnHash = stpTrans->getTransactionID(); - - Serializer s = buildMultiSigningData(*stpTrans, signingAcc); - - auto multisig = - ripple::sign(keys.publicKey, keys.secretKey, s.slice()); - - STTx exportSignTx(ttEXPORT_SIGN, [&](auto& obj) { - obj.set(([&]() { - auto inner = std::make_unique(sfSigner); - inner->setFieldVL(sfSigningPubKey, keys.publicKey); - inner->setAccountID(sfAccount, signingAcc); - inner->setFieldVL(sfTxnSignature, multisig); - return inner; - })()); - obj.setFieldU32(sfLedgerSequence, seq); - obj.setFieldH256(sfTransactionHash, txnHash); - }); - - // submit to the ledger - { - uint256 txID = exportSignTx.getTransactionID(); - auto s = std::make_shared(); - exportSignTx.add(*s); - app.getHashRouter().setFlags(txID, SF_PRIVATE2); - app.getHashRouter().setFlags(txID, SF_EMITTED); - view.rawTxInsert(txID, std::move(s), nullptr); - ledgerChanged = true; - } - } - - catch (std::exception& e) - { - JLOG(j_.warn()) - << "ExportedTxn Processing: Failure: " << e.what() - << "\n"; - } + // ttEXPORT_SIGN transactions are now submitted by validators + // as UVTxns (UNL Validator Transactions) via + // makeExportSignTxns, rather than being injected here as + // pseudo-transactions. } while (cdirNext( view, exportedDirKeylet.key, sleDirNode, uDirEntry, dirEntry)); @@ -2172,13 +2115,16 @@ TxQ::tryDirectApply( const bool isFirstImport = !sleAccount && view.rules().enabled(featureImport) && tx->getTxnType() == ttIMPORT; + // UVTxns don't require an account + const bool accRequired = !(isFirstImport || isUVTx(*tx)); + // 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]); @@ -2191,7 +2137,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/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 6f0a366e1..454788be0 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -23,14 +23,17 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include #include +#include #include #include @@ -43,34 +46,57 @@ Change::preflight(PreflightContext const& ctx) if (!isTesSuccess(ret)) return ret; - auto account = ctx.tx.getAccountID(sfAccount); - if (account != beast::zero) - { - JLOG(ctx.j.warn()) << "Change: Bad source id"; - return temBAD_SRC_ACCOUNT; - } + // ttEXPORT_SIGN is a UVTx (UNL Validator Transaction), not a pseudo-tx. + // It has a real account, signature, and goes through normal validation. + bool const isUVTx = ctx.tx.getTxnType() == ttEXPORT_SIGN; - // No point in going any further if the transaction fee is malformed. - auto const fee = ctx.tx.getFieldAmount(sfFee); - if (!fee.native() || fee != beast::zero) + if (!isUVTx) { - JLOG(ctx.j.warn()) << "Change: invalid fee"; - return temBAD_FEE; - } + auto account = ctx.tx.getAccountID(sfAccount); + if (account != beast::zero) + { + JLOG(ctx.j.warn()) << "Change: Bad source id"; + return temBAD_SRC_ACCOUNT; + } - if (!ctx.tx.getSigningPubKey().empty() || !ctx.tx.getSignature().empty() || - ctx.tx.isFieldPresent(sfSigners)) - { - JLOG(ctx.j.warn()) << "Change: Bad signature"; - return temBAD_SIGNATURE; - } + // No point in going any further if the transaction fee is malformed. + auto const fee = ctx.tx.getFieldAmount(sfFee); + if (!fee.native() || fee != beast::zero) + { + JLOG(ctx.j.warn()) << "Change: invalid fee"; + return temBAD_FEE; + } - if (ctx.tx.getFieldU32(sfSequence) != 0 || - ctx.tx.isFieldPresent(sfPreviousTxnID)) - { - JLOG(ctx.j.warn()) << "Change: Bad sequence"; - return temBAD_SEQUENCE; + if (!ctx.tx.getSigningPubKey().empty() || + !ctx.tx.getSignature().empty() || ctx.tx.isFieldPresent(sfSigners)) + { + JLOG(ctx.j.warn()) << "Change: Bad signature"; + return temBAD_SIGNATURE; + } + + if (ctx.tx.getFieldU32(sfSequence) != 0 || + ctx.tx.isFieldPresent(sfPreviousTxnID)) + { + JLOG(ctx.j.warn()) << "Change: Bad sequence"; + return temBAD_SEQUENCE; + } } + //@@start uvtx-preflight + else + { + // UVTx preflight checks + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (!ctx.rules.enabled(featureExport)) + return temDISABLED; + + if (auto const ret = preflight2(ctx); !isTesSuccess(ret)) + return ret; + + return tesSUCCESS; + } + //@@end uvtx-preflight if (ctx.tx.getTxnType() == ttUNL_MODIFY && !ctx.rules.enabled(featureNegativeUNL)) @@ -112,12 +138,38 @@ Change::preclaim(PreclaimContext const& ctx) { // If tapOPEN_LEDGER is resurrected into ApplyFlags, // this block can be moved to preflight. - if (ctx.view.open()) + // UVTxns like ttEXPORT_SIGN can be applied in the open ledger. + bool const isUVTx = ctx.tx.getTxnType() == ttEXPORT_SIGN; + if (ctx.view.open() && !isUVTx) { JLOG(ctx.j.warn()) << "Change transaction against open ledger"; return temINVALID; } + //@@start uvtx-preclaim + // UVTx (ttEXPORT_SIGN) validation: signer must be in UNLReport + if (isUVTx) + { + if (!ctx.view.rules().enabled(featureExport)) + return temDISABLED; + + auto const& pkSignerField = ctx.tx.getSigningPubKey(); + if (!publicKeyType(makeSlice(pkSignerField))) + return tefBAD_AUTH; + + PublicKey pkSigner{makeSlice(pkSignerField)}; + + if (!inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j)) + { + JLOG(ctx.j.warn()) + << "ExportSign: Txn Account isn't in the UNLReport."; + return tefFAILURE; + } + + return tesSUCCESS; + } + //@@end uvtx-preclaim + switch (ctx.tx.getTxnType()) { case ttFEE: @@ -221,8 +273,10 @@ Change::doApply() return applyUNLReport(); case ttEXPORT: return applyExport(); + //@@start export-sign-route case ttEXPORT_SIGN: return applyExportSign(); + //@@end export-sign-route default: assert(0); @@ -1293,4 +1347,143 @@ Change::applyUNLModify() return tesSUCCESS; } +std::vector> +makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j) +{ + std::vector> result; + + if (!view.rules().enabled(featureExport)) + return result; + + JLOG(j.debug()) << "EXPORT_SIGN processing: started"; + + auto const seq = view.info().seq; + + // if we're not a validator we do nothing here + if (app.getValidationPublicKey().empty()) + return result; + + auto const& keys = app.getValidatorKeys(); + + if (keys.configInvalid()) + return result; + + PublicKey pkSigning = app.getValidationPublicKey(); + auto const pk = app.validatorManifests().getMasterKey(pkSigning); + + // Only continue if we're on the UNLReport + if (!inUNLReport(view, app, pk, j)) + return result; + + AccountID signingAcc = calcAccountID(pkSigning); + + Keylet const exportedDirKeylet{keylet::exportedDir()}; + if (dirIsEmpty(view, exportedDirKeylet)) + return result; + + std::shared_ptr sleDirNode{}; + unsigned int uDirEntry{0}; + uint256 dirEntry{beast::zero}; + + if (!cdirFirst( + view, exportedDirKeylet.key, sleDirNode, uDirEntry, dirEntry)) + return result; + + do + { + Keylet const itemKeylet{ltCHILD, dirEntry}; + auto sleItem = view.read(itemKeylet); + if (!sleItem) + { + JLOG(j.warn()) + << "ExportedTxn processing: directory node in ledger " << seq + << " has index to object that is missing: " + << to_string(dirEntry); + continue; + } + + LedgerEntryType const nodeType{ + safe_cast((*sleItem)[sfLedgerEntryType])}; + + if (nodeType != ltEXPORTED_TXN) + { + JLOG(j.warn()) << "ExportedTxn processing: exported directory " + "contained non ltEXPORTED_TXN type"; + continue; + } + + auto const& exported = const_cast(*sleItem) + .getField(sfExportedTxn) + .downcast(); + + auto exportedLgrSeq = exported.getFieldU32(sfLedgerSequence); + + // Only sign transactions that were added in the previous ledger + if (exportedLgrSeq != seq - 1) + continue; + + auto s = std::make_shared(); + exported.add(*s); + SerialIter sitTrans(s->slice()); + try + { + auto const& stpTrans = + std::make_shared(std::ref(sitTrans)); + + if (!stpTrans->isFieldPresent(sfAccount) || + stpTrans->getAccountID(sfAccount) == beast::zero) + { + JLOG(j.warn()) + << "Hook: Export failure: sfAccount missing or zero."; + continue; + } + + auto txnHash = stpTrans->getTransactionID(); + + // Build the multisig for the inner exported transaction + Serializer sigData = buildMultiSigningData(*stpTrans, signingAcc); + auto multisig = + ripple::sign(keys.publicKey, keys.secretKey, sigData.slice()); + + // Create the ttEXPORT_SIGN transaction + auto exportSignTx = + std::make_shared(ttEXPORT_SIGN, [&](auto& obj) { + obj.set(([&]() { + auto inner = std::make_unique(sfSigner); + inner->setFieldVL(sfSigningPubKey, keys.publicKey); + inner->setAccountID(sfAccount, signingAcc); + inner->setFieldVL(sfTxnSignature, multisig); + return inner; + })()); + obj.setFieldU32(sfLedgerSequence, seq); + obj.setFieldH256(sfTransactionHash, txnHash); + obj.setAccountID(sfAccount, calcAccountID(pk)); + obj.setFieldU32(sfSequence, 0); + obj.setFieldVL(sfSigningPubKey, pkSigning.slice()); + obj.setFieldU32(sfFlags, tfFullyCanonicalSig); + + if (app.config().NETWORK_ID > 1024) + obj.setFieldU32(sfNetworkID, app.config().NETWORK_ID); + }); + + // Sign the outer transaction using our ephemeral key + exportSignTx->sign(pkSigning, app.getValidationSecretKey()); + + JLOG(j.debug()) + << "EXPORT_SIGN txn: " << exportSignTx->getFullText(); + + result.push_back(exportSignTx); + } + catch (std::exception& e) + { + JLOG(j.warn()) << "ExportedTxn Processing: Failure: " << e.what() + << "\n"; + } + + } while ( + cdirNext(view, exportedDirKeylet.key, sleDirNode, uDirEntry, dirEntry)); + + return result; +} + } // namespace ripple diff --git a/src/ripple/app/tx/impl/Change.h b/src/ripple/app/tx/impl/Change.h index 6785d17d9..4562a396d 100644 --- a/src/ripple/app/tx/impl/Change.h +++ b/src/ripple/app/tx/impl/Change.h @@ -25,6 +25,7 @@ #include #include #include +#include #include namespace ripple { @@ -84,6 +85,13 @@ private: applyUNLReport(); }; +/** + * If this validator is on the UNLReport, generate signed ttEXPORT_SIGN + * transactions for any exported transactions that need signing. + */ +std::vector> +makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j); + } // namespace ripple #endif diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 80646172a..1f16f12b3 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -273,6 +273,12 @@ Transactor::calculateHookChainFee( XRPAmount Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) { + //@@start uvtx-fee + // UVTxns (UNL Validator Transactions) have zero fee + if (isUVTx(tx)) + return XRPAmount{0}; + //@@end uvtx-fee + // Returns the fee in fee units. // The computation has two parts: @@ -442,8 +448,19 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) // Only check fee is sufficient when the ledger is open. if (ctx.view.open()) { - auto const feeDue = - minimumFee(ctx.app, baseFee, ctx.view.fees(), ctx.flags); + auto feeDue = minimumFee(ctx.app, baseFee, ctx.view.fees(), ctx.flags); + + // UVTxns from validators in UNLReport don't have to pay a fee + if (ctx.view.rules().enabled(featureExport) && isUVTx(ctx.tx)) + { + auto const& pkSignerField = ctx.tx.getSigningPubKey(); + if (publicKeyType(makeSlice(pkSignerField))) + { + PublicKey pkSigner{makeSlice(pkSignerField)}; + if (inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j)) + feeDue = beast::zero; + } + } if (feePaid < feeDue) { @@ -461,6 +478,10 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) auto const sle = ctx.view.read(keylet::account(id)); if (!sle) { + // UVTxns don't need an underlying account + if (isUVTx(ctx.tx)) + return tesSUCCESS; + if (ctx.tx.getTxnType() == ttIMPORT) { if (!ctx.tx.isFieldPresent(sfIssuer)) @@ -541,6 +562,16 @@ Transactor::checkSeqProxy( return tesSUCCESS; } + //@@start uvtx-seq + // UVTxns with seq=0 are allowed without an account + if (isUVTx(tx) && t_seqProx.isSeq() && tx[sfSequence] == 0) + { + JLOG(j.trace()) << "applyTransaction: allowing UVTx with seq=0 " + << toBase58(id); + return tesSUCCESS; + } + //@@end uvtx-seq + JLOG(j.trace()) << "applyTransaction: delay: source account does not exist " << toBase58(id); @@ -625,7 +656,10 @@ Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx) ctx.view.rules().enabled(featureImport) && ctx.tx.getTxnType() == ttIMPORT && !ctx.tx.isFieldPresent(sfIssuer); - if (!sle && !isFirstImport) + // UVTxns don't require an underlying account + bool const accRequired = !(isFirstImport || isUVTx(ctx.tx)); + + if (!sle && accRequired) { JLOG(ctx.j.trace()) << "applyTransaction: delay: source account does not exist " @@ -781,12 +815,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. (ttIMPORT and UVTxns) assert( sle != nullptr || account_ == beast::zero || view().rules().enabled(featureImport) && ctx_.tx.getTxnType() == ttIMPORT && - !ctx_.tx.isFieldPresent(sfIssuer)); + !ctx_.tx.isFieldPresent(sfIssuer) || + isUVTx(ctx_.tx)); if (sle) { @@ -848,19 +883,28 @@ 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)); + //@@start uvtx-sign + // 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; + //@@end uvtx-sign + if (!sleAccount) return terNO_ACCOUNT; diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 14486ef13..043e7dfc1 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -147,12 +147,16 @@ invoke_preflight(PreflightContext const& ctx) return invoke_preflight_helper(ctx); case ttTRUST_SET: return invoke_preflight_helper(ctx); + //@@start export-sign-preflight case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: case ttUNL_REPORT: case ttEMIT_FAILURE: + case ttEXPORT: + case ttEXPORT_SIGN: return invoke_preflight_helper(ctx); + //@@end export-sign-preflight case ttHOOK_SET: return invoke_preflight_helper(ctx); case ttNFTOKEN_MINT: @@ -278,12 +282,16 @@ invoke_preclaim(PreclaimContext const& ctx) return invoke_preclaim(ctx); case ttHOOK_SET: return invoke_preclaim(ctx); + //@@start export-sign-preclaim case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: case ttUNL_REPORT: case ttEMIT_FAILURE: + case ttEXPORT: + case ttEXPORT_SIGN: return invoke_preclaim(ctx); + //@@end export-sign-preclaim case ttNFTOKEN_MINT: return invoke_preclaim(ctx); case ttNFTOKEN_BURN: @@ -369,6 +377,7 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) return SetTrust::calculateBaseFee(view, tx); case ttHOOK_SET: return SetHook::calculateBaseFee(view, tx); + //@@start export-sign-fee case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: @@ -377,6 +386,7 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) case ttEXPORT_SIGN: case ttEXPORT: return Change::calculateBaseFee(view, tx); + //@@end export-sign-fee case ttNFTOKEN_MINT: return NFTokenMint::calculateBaseFee(view, tx); case ttNFTOKEN_BURN: @@ -542,6 +552,7 @@ invoke_apply(ApplyContext& ctx) SetHook p(ctx); return p(); } + //@@start export-sign-apply case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: @@ -552,6 +563,7 @@ invoke_apply(ApplyContext& ctx) Change p(ctx); return p(); } + //@@end export-sign-apply case ttNFTOKEN_MINT: { NFTokenMint p(ctx); return p(); diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index 27659ad8d..254f19021 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 @@ -1094,6 +1096,74 @@ trustTransferLockedBalance( } return tesSUCCESS; } + +/** + * Check if an account (derived from a validator's master public key) is + * in the UNLReport's ActiveValidators list. + */ +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 account is on UNLReport + auto const unlRep = view.read(keylet::UNLReport()); + if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators)) + { + JLOG(j.debug()) << "UNLReport missing"; + + // 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; +} + +/** + * Check if a public key (or its master key via manifest lookup) is + * in the UNLReport's ActiveValidators list. + */ +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/protocol/STTx.h b/src/ripple/protocol/STTx.h index c6a9e053c..59ef546b7 100644 --- a/src/ripple/protocol/STTx.h +++ b/src/ripple/protocol/STTx.h @@ -171,6 +171,10 @@ sterilize(STTx const& stx); bool isPseudoTx(STObject const& tx); +/** Check whether a transaction is a UNL Validator Transaction (UVTx) */ +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 2fbb19421..849034e07 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -615,7 +615,19 @@ 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 == ttEXPORT; +} + +bool +isUVTx(STObject const& tx) +{ + auto t = tx[~sfTransactionType]; + if (!t) + return false; + + auto tt = safe_cast(*t); + return tt == ttEXPORT_SIGN; } } // namespace ripple