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.
This commit is contained in:
Nicholas Dudfield
2026-01-20 13:44:38 +07:00
parent 652b181b5d
commit c01b9a657b
11 changed files with 407 additions and 97 deletions

View File

@@ -35,6 +35,7 @@
#include <ripple/app/misc/TxQ.h>
#include <ripple/app/misc/ValidatorKeys.h>
#include <ripple/app/misc/ValidatorList.h>
#include <ripple/app/tx/impl/Change.h>
#include <ripple/basics/random.h>
#include <ripple/beast/core/LexicalCast.h>
#include <ripple/consensus/LedgerTiming.h>
@@ -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);
});

View File

@@ -599,6 +599,12 @@ public:
return validatorKeys_.publicKey;
}
SecretKey const&
getValidationSecretKey() const override
{
return validatorKeys_.secretKey;
}
ValidatorKeys const&
getValidatorKeys() const override
{

View File

@@ -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&

View File

@@ -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<ripple::Serializer>();
exported.add(*s);
SerialIter sitTrans(s->slice());
try
{
auto const& stpTrans =
std::make_shared<STTx const>(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<STObject>(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<ripple::Serializer>();
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<SeqProxy> 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);

View File

@@ -23,14 +23,17 @@
#include <ripple/app/main/Application.h>
#include <ripple/app/misc/AmendmentTable.h>
#include <ripple/app/misc/NetworkOPs.h>
#include <ripple/app/misc/ValidatorKeys.h>
#include <ripple/app/tx/impl/Change.h>
#include <ripple/app/tx/impl/SetSignerList.h>
#include <ripple/app/tx/impl/XahauGenesis.h>
#include <ripple/basics/Log.h>
#include <ripple/ledger/OpenView.h>
#include <ripple/ledger/Sandbox.h>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/Sign.h>
#include <ripple/protocol/TxFlags.h>
#include <string_view>
@@ -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<std::shared_ptr<STTx const>>
makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j)
{
std::vector<std::shared_ptr<STTx const>> 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<SLE const> 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<LedgerEntryType>((*sleItem)[sfLedgerEntryType])};
if (nodeType != ltEXPORTED_TXN)
{
JLOG(j.warn()) << "ExportedTxn processing: exported directory "
"contained non ltEXPORTED_TXN type";
continue;
}
auto const& exported = const_cast<ripple::STLedgerEntry&>(*sleItem)
.getField(sfExportedTxn)
.downcast<STObject>();
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<ripple::Serializer>();
exported.add(*s);
SerialIter sitTrans(s->slice());
try
{
auto const& stpTrans =
std::make_shared<STTx const>(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<STTx>(ttEXPORT_SIGN, [&](auto& obj) {
obj.set(([&]() {
auto inner = std::make_unique<STObject>(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

View File

@@ -25,6 +25,7 @@
#include <ripple/app/misc/NetworkOPs.h>
#include <ripple/app/tx/impl/Transactor.h>
#include <ripple/basics/Log.h>
#include <ripple/ledger/OpenView.h>
#include <ripple/protocol/Indexes.h>
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<std::shared_ptr<STTx const>>
makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j);
} // namespace ripple
#endif

View File

@@ -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;

View File

@@ -147,12 +147,16 @@ invoke_preflight(PreflightContext const& ctx)
return invoke_preflight_helper<CreateTicket>(ctx);
case ttTRUST_SET:
return invoke_preflight_helper<SetTrust>(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<Change>(ctx);
//@@end export-sign-preflight
case ttHOOK_SET:
return invoke_preflight_helper<SetHook>(ctx);
case ttNFTOKEN_MINT:
@@ -278,12 +282,16 @@ invoke_preclaim(PreclaimContext const& ctx)
return invoke_preclaim<SetTrust>(ctx);
case ttHOOK_SET:
return invoke_preclaim<SetHook>(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<Change>(ctx);
//@@end export-sign-preclaim
case ttNFTOKEN_MINT:
return invoke_preclaim<NFTokenMint>(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();

View File

@@ -20,6 +20,8 @@
#ifndef RIPPLE_LEDGER_VIEW_H_INCLUDED
#define RIPPLE_LEDGER_VIEW_H_INCLUDED
#include <ripple/app/main/Application.h>
#include <ripple/app/misc/Manifest.h>
#include <ripple/basics/Log.h>
#include <ripple/beast/utility/Journal.h>
#include <ripple/core/Config.h>
@@ -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 <class V>
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<AccountID, bool> 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 <class V>
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

View File

@@ -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)
{
}

View File

@@ -615,7 +615,19 @@ isPseudoTx(STObject const& tx)
auto tt = safe_cast<TxType>(*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<TxType>(*t);
return tt == ttEXPORT_SIGN;
}
} // namespace ripple