fix(export): cap hook export backlog

Enforce the pending export cap for hook-emitted ttEXPORT work before commit. Replace the non-present sfEmittedTxn template field when building ltEMITTED_TXN entries so in-flight ledger checks see the emitted wrapper.

Overflowing xport emission now returns tecDIR_FULL and leaves the emitted backlog capped at ExportLimits::maxPendingExports.
This commit is contained in:
Nicholas Dudfield
2026-04-27 22:55:23 +07:00
parent 0c2c59d258
commit c58da3da58
7 changed files with 197 additions and 6 deletions

View File

@@ -23,6 +23,7 @@
#include <test/jtx/import.h>
#include <test/jtx/xpop.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/Feature.h>
@@ -526,6 +527,64 @@ struct Export_test : public beast::unit_test::suite
BEAST_EXPECT(dirIsEmpty(*env.current(), emittedDirKey));
}
void
testXportEmissionLimit(FeatureBitset features)
{
testcase("Xport emitted export limit");
using namespace jtx;
Env env{*this, exportTestConfig(), features};
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(10000), alice, bob, carol);
env.close();
env(ripple::test::jtx::hook(alice, {{hso(xport_wasm)}}, 0),
HSFEE,
ter(tesSUCCESS));
env.close();
auto params = makeDstParams(carol.id());
for (std::uint32_t i = 0; i <= ExportLimits::maxPendingExports; ++i)
{
env(pay(bob, alice, XRP(1)),
fee(XRP(1)),
json(jss::HookParameters, params),
ter(tesSUCCESS));
}
env.close();
std::uint32_t accepted = 0;
std::uint32_t capped = 0;
for (auto const& [stx, meta] : env.closed()->txs)
{
if (stx->getTxnType() != ttPAYMENT ||
stx->getAccountID(sfAccount) != bob.id())
{
continue;
}
if ((*meta)[sfTransactionResult] ==
static_cast<std::uint8_t>(TERtoInt(tesSUCCESS)))
++accepted;
else if (
(*meta)[sfTransactionResult] ==
static_cast<std::uint8_t>(TERtoInt(tecDIR_FULL)))
++capped;
}
BEAST_EXPECT(accepted == ExportLimits::maxPendingExports);
BEAST_EXPECT(capped == 1);
BEAST_EXPECT(
ExportLedgerOps::pendingExportEmissionCount(*env.current()) ==
ExportLimits::maxPendingExports);
}
void
testExportTxnOpenLedger(FeatureBitset features)
{
@@ -997,6 +1056,7 @@ struct Export_test : public beast::unit_test::suite
testXportPayment(allWithExport);
testXportRejectsLocalNetworkID(allWithExport);
testXportRejectsUnconfiguredNetworkID(allWithExport);
testXportEmissionLimit(allWithExport);
// ttEXPORT transactor tests
testExportTxnOpenLedger(allWithExport);