feat(export): add ttEXPORT user transaction and extract ExportLedgerOps

Rename the existing ttEXPORT pseudo-tx to ttEXPORT_FINALIZE (type 90)
to make room for a user-submittable ttEXPORT (type 91).

ttEXPORT allows non-hook users to submit export transactions directly,
creating the same ltEXPORTED_TXN entries that the hook xport() API
creates inline.

Extract shared logic into ExportLedgerOps.h:
- createExportedTxn(): creates ltEXPORTED_TXN, enforces directory cap
- validateNetworkID(): self-target and unconfigured guards
- validateExportAccount(): account ownership check

Both the hook API (HookAPI.cpp) and the Export transactor now call
into ExportLedgerOps, eliminating duplicated validation and ledger
mutation code.
This commit is contained in:
Nicholas Dudfield
2026-03-17 11:43:45 +07:00
parent 42a6407815
commit bd68364f25
15 changed files with 385 additions and 140 deletions

View File

@@ -1,10 +1,10 @@
""":descr: install xport hook, trigger export, verify ttEXPORT lifecycle completes
""":descr: install xport hook, trigger export, verify ttEXPORT_FINALIZE lifecycle completes
Mirrors the C++ Export_test.cpp::testXportPaymentWithValidator flow:
1. Fund alice (hook holder), bob (trigger), carol (export destination)
2. Install xport hook on alice
3. bob pays alice with DST=carol → hook calls xport()
4. Wait for validator signature collection + ttEXPORT application
4. Wait for validator signature collection + ttEXPORT_FINALIZE application
5. Verify Export transaction appears in a subsequent ledger
"""

View File

@@ -61,7 +61,7 @@
#define ttNFTOKEN_MODIFY 70
#define ttPERMISSIONED_DOMAIN_SET 71
#define ttPERMISSIONED_DOMAIN_DELETE 72
#define ttEXPORT 90
#define ttEXPORT_FINALIZE 90
#define ttCRON 92
#define ttCRON_SET 93
#define ttREMARKS_SET 94

View File

@@ -500,12 +500,17 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
{sfDomainID, soeREQUIRED},
}))
TRANSACTION(ttEXPORT, 90, Export, ({
TRANSACTION(ttEXPORT_FINALIZE, 90, ExportFinalize, ({
{sfTransactionHash, soeREQUIRED},
{sfExportedTxn, soeREQUIRED},
{sfLedgerSequence, soeREQUIRED},
}))
/* User-submittable export: create ltEXPORTED_TXN for validator signing */
TRANSACTION(ttEXPORT, 91, Export, ({
{sfExportedTxn, soeREQUIRED},
}))
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
TRANSACTION(ttCRON, 92, Cron, ({
{sfOwner, soeREQUIRED},

View File

@@ -685,7 +685,7 @@ 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 == ttEXPORT || tt == ttCONSENSUS_ENTROPY;
tt == ttEXPORT_FINALIZE || tt == ttCONSENSUS_ENTROPY;
}
} // namespace ripple

View File

@@ -224,7 +224,7 @@ struct Export_test : public beast::unit_test::suite
// Helper: run xport test with given config
// Returns true if the exported directory is empty after the flow
// (meaning ttEXPORT cleaned up the entry)
// (meaning ttEXPORT_FINALIZE cleaned up the entry)
void
runXportTest(
FeatureBitset features,
@@ -282,8 +282,8 @@ struct Export_test : public beast::unit_test::suite
// Close additional ledgers for signing flow
env.close(); // N+1: validators sign via TMValidation
env.close(); // N+2: ttEXPORT created (rawTxInsert)
env.close(); // N+3: does ttEXPORT get applied here?
env.close(); // N+2: ttEXPORT_FINALIZE created (rawTxInsert)
env.close(); // N+3: does ttEXPORT_FINALIZE get applied here?
// Check if cleanup happened
{
@@ -303,7 +303,7 @@ struct Export_test : public beast::unit_test::suite
// With validator config, full flow should work:
// N: xport creates entry
// N+1: validator signs
// N+2: ttEXPORT cleans up
// N+2: ttEXPORT_FINALIZE cleans up
runXportTest(features, exportTestConfig, true);
}
@@ -538,6 +538,61 @@ struct Export_test : public beast::unit_test::suite
BEAST_EXPECT(dirIsEmpty(*env.current(), exportedDirKey));
}
// Build a minimal unsigned Payment STObject suitable for sfExportedTxn.
static STObject
buildExportedPayment(
AccountID const& src,
AccountID const& dst,
std::uint32_t fls,
std::uint32_t lls)
{
STObject obj(sfExportedTxn);
obj.setFieldU16(sfTransactionType, ttPAYMENT);
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfFirstLedgerSequence, fls);
obj.setFieldU32(sfLastLedgerSequence, lls);
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
obj.setFieldAmount(sfFee, XRPAmount{10});
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setAccountID(sfAccount, src);
obj.setAccountID(sfDestination, dst);
return obj;
}
void
testExportTxn(FeatureBitset features)
{
testcase("ttEXPORT_USER creates ltEXPORTED_TXN");
using namespace jtx;
Env env{*this, exportTestConfig(), features};
Account const alice{"alice"};
Account const carol{"carol"};
env.fund(XRP(10000), alice, carol);
env.close();
auto const seq = env.current()->seq();
auto innerObj =
buildExportedPayment(alice.id(), carol.id(), seq + 1, seq + 5);
// Submit ttEXPORT_USER with inner payment as STObject
Json::Value jv;
jv[jss::TransactionType] = jss::Export;
jv[jss::Account] = alice.human();
jv[sfExportedTxn.jsonName] = innerObj.getJson(JsonOptions::none);
env(jv, fee(XRP(1)), ter(tesSUCCESS));
env.close();
// Verify ltEXPORTED_TXN was created
auto const exportedDirKey = keylet::exportedDir();
BEAST_EXPECT(!dirIsEmpty(*env.current(), exportedDirKey));
}
void
testStaleSignatureCleanup(FeatureBitset features)
{
@@ -579,6 +634,7 @@ struct Export_test : public beast::unit_test::suite
testXportPaymentWithValidator(allWithExport);
testXportRejectsLocalNetworkID(allWithExport);
testXportRejectsUnconfiguredNetworkID(allWithExport);
testExportTxn(allWithExport);
testStaleSignatureCleanup(allWithExport);
}
};

View File

@@ -3,6 +3,7 @@
#include <xrpld/app/hook/HookAPI.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/ledger/TransactionMaster.h>
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
#include <xrpld/app/tx/detail/Import.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <cfenv>
@@ -1265,42 +1266,15 @@ HookAPI::xport(Slice const& txBlob) const
return Unexpected(EXPORT_FAILURE);
}
if (!stpTrans->isFieldPresent(sfAccount) ||
stpTrans->getAccountID(sfAccount) != hookCtx.result.account)
{
JLOG(j.trace()) << "HookExport[" << HC_ACC()
<< "]: Attempted to export a txn that's not for this "
"Hook's Account ID.";
if (auto ter = ExportLedgerOps::validateExportAccount(
*stpTrans, hookCtx.result.account, j);
!isTesSuccess(ter))
return Unexpected(EXPORT_FAILURE);
}
// Reject exports that could target the local network.
// An exported txn re-executing on its origin chain could cause exploits.
//
// Per XRPL rules (Transactor.cpp):
// - Networks <= 1024: sfNetworkID must NOT be present
// - Networks > 1024: sfNetworkID is REQUIRED and must match
//
// So: if the exported tx has sfNetworkID matching local → self-target.
// if local NETWORK_ID is 0 (unconfigured) → can't safely distinguish
// self-targeting from cross-chain, reject unless tx has an explicit
// non-zero NetworkID.
if (stpTrans->isFieldPresent(sfNetworkID) &&
stpTrans->getFieldU32(sfNetworkID) == app.config().NETWORK_ID)
{
JLOG(j.warn()) << "HookExport[" << HC_ACC()
<< "]: Rejected export with local NetworkID ("
<< app.config().NETWORK_ID << ").";
if (auto ter = ExportLedgerOps::validateNetworkID(
*stpTrans, app.config().NETWORK_ID, j);
!isTesSuccess(ter))
return Unexpected(EXPORT_FAILURE);
}
if (app.config().NETWORK_ID == 0 && !stpTrans->isFieldPresent(sfNetworkID))
{
JLOG(j.warn()) << "HookExport[" << HC_ACC()
<< "]: Rejected export with unconfigured NETWORK_ID. "
"Node must have a non-zero NETWORK_ID to export.";
return Unexpected(EXPORT_FAILURE);
}
std::string reason;
auto tpTrans = std::make_shared<Transaction>(stpTrans, reason, app);

View File

@@ -5,6 +5,7 @@
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
#include <xrpld/app/tx/detail/Import.h>
#include <xrpld/app/tx/detail/NFTokenUtils.h>
#include <xrpld/ledger/View.h>
@@ -587,6 +588,7 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
case ttUNL_MODIFY:
case ttEMIT_FAILURE:
case ttUNL_REPORT:
case ttEXPORT_FINALIZE:
case ttEXPORT:
case ttCONSENSUS_ENTROPY: {
break;
@@ -1725,89 +1727,16 @@ hook::finalizeHookResult(
auto& id = tpTrans->getID();
JLOG(j.trace()) << "HookExport[" << HR_ACC() << "]: " << id;
// exported txns must be marked bad by the hash router to ensure
// under no circumstances they will enter consensus on *this* chain.
applyCtx.app.getHashRouter().setFlags(id, SF_BAD);
std::shared_ptr<const ripple::STTx> ptr =
tpTrans->getSTransaction();
auto exportedId = keylet::exportedTxn(id);
auto sleExported = applyCtx.view().peek(exportedId);
TER const ter = ExportLedgerOps::createExportedTxn(
applyCtx.view(), applyCtx.app, *ptr, id, j);
if (!sleExported)
{
// Enforce maxPendingExports on the exported directory.
// Each pending export costs validator signing + broadcast
// work every round, so this is the root DoS constraint.
{
Keylet const expDirKey{keylet::exportedDir()};
std::size_t dirSize = 0;
std::shared_ptr<SLE const> sleDirNode;
unsigned int uDirEntry{0};
uint256 dirEntry{beast::zero};
if (cdirFirst(
applyCtx.view(),
expDirKey.key,
sleDirNode,
uDirEntry,
dirEntry))
{
do
{
++dirSize;
} while (cdirNext(
applyCtx.view(),
expDirKey.key,
sleDirNode,
uDirEntry,
dirEntry));
}
if (!isTesSuccess(ter))
return ter;
if (dirSize >= ExportLimits::maxPendingExports)
{
JLOG(j.warn()) << "HookError[" << HR_ACC() << "]: "
<< "Export directory at cap ("
<< ExportLimits::maxPendingExports
<< "), rejecting export " << id;
return tecDIR_FULL;
}
}
exported_txnid.emplace_back(id);
sleExported = std::make_shared<SLE>(exportedId);
// RH TODO: add a new constructor to STObject to avoid this
// serder thing
ripple::Serializer s;
ptr->add(s);
SerialIter sit(s.slice());
sleExported->emplace_back(ripple::STObject(sit, sfExportedTxn));
auto page = applyCtx.view().dirInsert(
keylet::exportedDir(), exportedId, [&](SLE::ref sle) {
(*sle)[sfFlags] = lsfEmittedDir;
});
if (page)
{
(*sleExported)[sfOwnerNode] = *page;
(*sleExported)[sfLedgerSequence] =
applyCtx.view().info().seq;
(*sleExported)[sfTransactionHash] = id;
applyCtx.view().insert(sleExported);
JLOG(j.debug())
<< "Export: created ltEXPORTED_TXN for " << id;
}
else
{
JLOG(j.warn())
<< "HookError[" << HR_ACC() << "]: "
<< "Export Directory full when trying to insert " << id;
return tecDIR_FULL;
}
}
exported_txnid.emplace_back(id);
}
}

View File

@@ -70,7 +70,8 @@ getExportUNLSize(ReadView const& view, Application& app);
occurs when accumulating signatures in ledger entries.
The collector stores signatures in memory until quorum (80% UNL) is reached,
at which point a ttEXPORT transaction can be created with all signatures.
at which point a ttEXPORT_FINALIZE transaction can be created with all
signatures.
Continuous broadcasting:
========================
@@ -84,8 +85,9 @@ getExportUNLSize(ReadView const& view, Application& app);
- Node restarts recover (re-sign from ledger state, ltEXPORTED_TXN exists)
The ltEXPORTED_TXN in the ledger is the gatekeeper - once deleted (after
ttEXPORT processed or export expired), signatures naturally stop being
broadcast. The collector clears its cache when ttEXPORT is applied.
ttEXPORT_FINALIZE processed or export expired), signatures naturally stop
being broadcast. The collector clears its cache when ttEXPORT_FINALIZE is
applied.
Thread safety: All public methods are thread-safe.
*/
@@ -183,7 +185,7 @@ public:
/** Clear signatures for a completed export.
Called after ttEXPORT is applied to clean up memory.
Called after ttEXPORT_FINALIZE is applied to clean up memory.
@param txnHash The hash of the completed export
*/

View File

@@ -1683,7 +1683,7 @@ TxQ::accept(Application& app, OpenView& view)
stpTrans->add(exportedSer);
SerialIter exportedSit(exportedSer.slice());
STTx exportTx(ttEXPORT, [&](auto& obj) {
STTx exportTx(ttEXPORT_FINALIZE, [&](auto& obj) {
obj[sfAccount] = AccountID();
obj.set(std::make_unique<STObject>(
exportedSit, sfExportedTxn));
@@ -1694,7 +1694,7 @@ TxQ::accept(Application& app, OpenView& view)
uint256 txID = exportTx.getTransactionID();
JLOG(j_.debug())
<< "Export: injecting ttEXPORT txID=" << txID
<< "Export: injecting ttEXPORT_FINALIZE txID=" << txID
<< " with " << signers.size() << " signatures";
auto txBlob = std::make_shared<ripple::Serializer>();

View File

@@ -103,7 +103,8 @@ Change::preflight(PreflightContext const& ctx)
}
}
if (ctx.tx.getTxnType() == ttEXPORT && !ctx.rules.enabled(featureExport))
if (ctx.tx.getTxnType() == ttEXPORT_FINALIZE &&
!ctx.rules.enabled(featureExport))
{
JLOG(ctx.j.warn()) << "Change: Export not enabled";
return temDISABLED;
@@ -182,7 +183,7 @@ Change::preclaim(PreclaimContext const& ctx)
case ttAMENDMENT:
case ttUNL_MODIFY:
case ttEMIT_FAILURE:
case ttEXPORT:
case ttEXPORT_FINALIZE:
case ttCONSENSUS_ENTROPY:
return tesSUCCESS;
case ttUNL_REPORT: {
@@ -239,8 +240,8 @@ Change::doApply()
return applyEmitFailure();
case ttUNL_REPORT:
return applyUNLReport();
case ttEXPORT:
return applyExport();
case ttEXPORT_FINALIZE:
return applyExportFinalize();
case ttCONSENSUS_ENTROPY:
return applyConsensusEntropy();
default:
@@ -1146,13 +1147,14 @@ Change::applyEmitFailure()
}
TER
Change::applyExport()
Change::applyExportFinalize()
{
uint256 txnID(ctx_.tx.getFieldH256(sfTransactionHash));
do
{
JLOG(j_.debug()) << "Export: processing ttEXPORT for " << txnID;
JLOG(j_.debug()) << "Export: processing ttEXPORT_FINALIZE for "
<< txnID;
// Last-line-of-defense safety check:
// Require >= 80% (ceil) cryptographically verified signatures from
@@ -1260,9 +1262,9 @@ Change::applyExport()
{
// most likely explanation is that this was somehow a double-up, so
// just ignore
JLOG(j_.warn())
<< "Export: ttEXPORT could not find ltEXPORTED_TXN for "
<< txnID;
JLOG(j_.warn()) << "Export: ttEXPORT_FINALIZE could not find "
"ltEXPORTED_TXN for "
<< txnID;
break;
}
@@ -1272,9 +1274,9 @@ Change::applyExport()
key,
false))
{
JLOG(j_.fatal())
<< "Export: ttEXPORT failed to remove directory entry for "
<< txnID;
JLOG(j_.fatal()) << "Export: ttEXPORT_FINALIZE failed to remove "
"directory entry for "
<< txnID;
return tefBAD_LEDGER;
}

View File

@@ -75,7 +75,7 @@ private:
applyEmitFailure();
TER
applyExport();
applyExportFinalize();
TER
applyUNLReport();
@@ -89,7 +89,7 @@ using SetFee = Change;
using UNLModify = Change;
using EmitFailure = Change;
using UNLReport = Change;
using Export = Change;
using ExportFinalize = Change;
using ConsensusEntropy = Change;
} // namespace ripple

View File

@@ -0,0 +1,78 @@
#include <xrpld/app/tx/detail/Export.h>
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
Export::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureExport))
return temDISABLED;
auto const ret = preflight1(ctx);
if (!isTesSuccess(ret))
return ret;
if (!ctx.tx.isFieldPresent(sfExportedTxn))
return temMALFORMED;
return preflight2(ctx);
}
TER
Export::preclaim(PreclaimContext const& ctx)
{
// Parse the inner exported transaction.
auto const& exportedObj = const_cast<STTx&>(ctx.tx)
.peekAtField(sfExportedTxn)
.downcast<STObject>();
Serializer s;
exportedObj.add(s);
SerialIter sit(s.slice());
std::shared_ptr<STTx const> stpTrans;
try
{
stpTrans = std::make_shared<STTx const>(sit);
}
catch (std::exception const&)
{
return temMALFORMED;
}
// Shared validation: account must match submitter.
if (auto ter = ExportLedgerOps::validateExportAccount(
*stpTrans, ctx.tx.getAccountID(sfAccount), ctx.j);
!isTesSuccess(ter))
return ter;
// Shared validation: NetworkID self-target guard.
if (auto ter = ExportLedgerOps::validateNetworkID(
*stpTrans, ctx.app.config().NETWORK_ID, ctx.j);
!isTesSuccess(ter))
return ter;
return tesSUCCESS;
}
TER
Export::doApply()
{
auto const& exportedObj =
ctx_.tx.peekAtField(sfExportedTxn).downcast<STObject>();
Serializer s;
exportedObj.add(s);
SerialIter sit(s.slice());
STTx exportedTx(std::ref(sit));
uint256 const txnId = exportedTx.getTransactionID();
return ExportLedgerOps::createExportedTxn(
view(), ctx_.app, exportedTx, txnId, j_);
}
} // namespace ripple

View File

@@ -0,0 +1,33 @@
#ifndef RIPPLE_TX_EXPORT_H_INCLUDED
#define RIPPLE_TX_EXPORT_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
/// User-submittable export transaction.
/// Creates an ltEXPORTED_TXN entry for validator signing.
/// This is the transaction-based entry point for non-hook users;
/// hooks use the xport() API which creates the same ledger state inline.
class Export : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit Export(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -0,0 +1,165 @@
#ifndef RIPPLE_TX_EXPORTLEDGEROPS_H_INCLUDED
#define RIPPLE_TX_EXPORTLEDGEROPS_H_INCLUDED
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
namespace ripple {
/// Shared ledger operations and validation for the export system.
/// Used by both the hook xport() API (inline path) and the
/// Export transactor (user-submitted ttEXPORT path).
namespace ExportLedgerOps {
/// Validate that the exported transaction's NetworkID doesn't target
/// the local network. Returns tesSUCCESS if OK, or a TER error code.
///
/// Rules (per upstream rippled Transactor.cpp):
/// - Networks <= 1024: sfNetworkID must NOT be present on txns
/// - Networks > 1024: sfNetworkID is REQUIRED and must match
///
/// So: if exported tx has sfNetworkID matching local → self-target.
/// if local NETWORK_ID is 0 (unconfigured) and tx has no
/// sfNetworkID → can't distinguish self from cross-chain, reject.
inline TER
validateNetworkID(
STTx const& stx,
std::uint32_t localNetworkID,
beast::Journal j)
{
if (stx.isFieldPresent(sfNetworkID) &&
stx.getFieldU32(sfNetworkID) == localNetworkID)
{
JLOG(j.warn()) << "ExportLedgerOps: rejected export targeting "
"local NetworkID ("
<< localNetworkID << ")";
return temMALFORMED;
}
if (localNetworkID == 0 && !stx.isFieldPresent(sfNetworkID))
{
JLOG(j.warn()) << "ExportLedgerOps: rejected export with "
"unconfigured NETWORK_ID";
return temMALFORMED;
}
return tesSUCCESS;
}
/// Validate that the exported transaction's Account matches the
/// expected exporting account.
inline TER
validateExportAccount(
STTx const& stx,
AccountID const& expectedAccount,
beast::Journal j)
{
if (!stx.isFieldPresent(sfAccount) ||
stx.getAccountID(sfAccount) != expectedAccount)
{
JLOG(j.warn())
<< "ExportLedgerOps: exported txn account doesn't match exporter";
return temMALFORMED;
}
return tesSUCCESS;
}
/// Create an ltEXPORTED_TXN entry in the global exportedDir().
/// Enforces maxPendingExports directory cap.
/// Marks the txn hash as SF_BAD in the hash router so it cannot
/// enter consensus on this chain.
///
/// @param view The apply view to modify
/// @param app Application reference (for hash router)
/// @param stx The serialized transaction to export
/// @param txnId Hash of the exported transaction
/// @param j Journal for logging
/// @return tesSUCCESS or tecDIR_FULL
inline TER
createExportedTxn(
ApplyView& view,
Application& app,
STTx const& stx,
uint256 const& txnId,
beast::Journal j)
{
// Mark as SF_BAD so this txn never enters consensus on this chain.
app.getHashRouter().setFlags(txnId, SF_BAD);
auto exportedId = keylet::exportedTxn(txnId);
auto sleExported = view.peek(exportedId);
if (sleExported)
{
// Already exists — duplicate export, skip.
JLOG(j.debug()) << "ExportLedgerOps: ltEXPORTED_TXN already exists for "
<< txnId;
return tesSUCCESS;
}
// Enforce maxPendingExports on the exported directory.
{
Keylet const expDirKey{keylet::exportedDir()};
std::size_t dirSize = 0;
std::shared_ptr<SLE const> sleDirNode;
unsigned int uDirEntry{0};
uint256 dirEntry{beast::zero};
if (cdirFirst(view, expDirKey.key, sleDirNode, uDirEntry, dirEntry))
{
do
{
++dirSize;
} while (
cdirNext(view, expDirKey.key, sleDirNode, uDirEntry, dirEntry));
}
if (dirSize >= ExportLimits::maxPendingExports)
{
JLOG(j.warn()) << "ExportLedgerOps: export directory at cap ("
<< ExportLimits::maxPendingExports
<< "), rejecting export " << txnId;
return tecDIR_FULL;
}
}
sleExported = std::make_shared<SLE>(exportedId);
// Serialize the STTx into an sfExportedTxn inner object.
ripple::Serializer s;
stx.add(s);
SerialIter sit(s.slice());
sleExported->emplace_back(ripple::STObject(sit, sfExportedTxn));
auto page =
view.dirInsert(keylet::exportedDir(), exportedId, [&](SLE::ref sle) {
(*sle)[sfFlags] = lsfEmittedDir;
});
if (page)
{
(*sleExported)[sfOwnerNode] = *page;
(*sleExported)[sfLedgerSequence] = view.info().seq;
(*sleExported)[sfTransactionHash] = txnId;
view.insert(sleExported);
JLOG(j.debug()) << "ExportLedgerOps: created ltEXPORTED_TXN for "
<< txnId;
return tesSUCCESS;
}
JLOG(j.warn()) << "ExportLedgerOps: directory full when inserting "
<< txnId;
return tecDIR_FULL;
}
} // namespace ExportLedgerOps
} // namespace ripple
#endif

View File

@@ -43,6 +43,7 @@
#include <xrpld/app/tx/detail/DeleteOracle.h>
#include <xrpld/app/tx/detail/DepositPreauth.h>
#include <xrpld/app/tx/detail/Escrow.h>
#include <xrpld/app/tx/detail/Export.h>
#include <xrpld/app/tx/detail/GenesisMint.h>
#include <xrpld/app/tx/detail/Import.h>
#include <xrpld/app/tx/detail/Invoke.h>