mirror of
https://github.com/Xahau/xahaud.git
synced 2026-08-03 04:00:55 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -1662,6 +1662,8 @@ hook::finalizeHookResult(
|
||||
std::vector<std::pair<uint256 /* txnid */, uint256 /* emit nonce */>>
|
||||
emission_txnid;
|
||||
std::vector<uint256 /* txnid */> exported_txnid;
|
||||
std::size_t pendingExportEmissions =
|
||||
ExportLedgerOps::pendingExportEmissionCount(applyCtx.view());
|
||||
|
||||
if (doEmit)
|
||||
{
|
||||
@@ -1676,12 +1678,29 @@ hook::finalizeHookResult(
|
||||
|
||||
std::shared_ptr<const ripple::STTx> ptr =
|
||||
tpTrans->getSTransaction();
|
||||
bool const isExportEmission =
|
||||
ExportLedgerOps::isPendingExportWorkTxn(*ptr);
|
||||
|
||||
auto emittedId = keylet::emittedTxn(id);
|
||||
auto sleEmitted = applyCtx.view().peek(emittedId);
|
||||
|
||||
if (!sleEmitted)
|
||||
{
|
||||
if (isExportEmission)
|
||||
{
|
||||
if (pendingExportEmissions >=
|
||||
ExportLimits::maxPendingExports)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "HookExport[" << HR_ACC()
|
||||
<< "]: export emission limit reached pending="
|
||||
<< pendingExportEmissions
|
||||
<< " max=" << +ExportLimits::maxPendingExports;
|
||||
return tecDIR_FULL;
|
||||
}
|
||||
++pendingExportEmissions;
|
||||
}
|
||||
|
||||
auto const& emitDetails = const_cast<ripple::STTx&>(*ptr)
|
||||
.getField(sfEmitDetails)
|
||||
.downcast<STObject>();
|
||||
@@ -1696,7 +1715,8 @@ hook::finalizeHookResult(
|
||||
ptr->add(s);
|
||||
SerialIter sit(s.slice());
|
||||
|
||||
sleEmitted->emplace_back(ripple::STObject(sit, sfEmittedTxn));
|
||||
sleEmitted->set(
|
||||
std::make_unique<ripple::STObject>(sit, sfEmittedTxn));
|
||||
auto page = applyCtx.view().dirInsert(
|
||||
keylet::emittedDir(), emittedId, [&](SLE::ref sle) {
|
||||
(*sle)[sfFlags] = lsfEmittedDir;
|
||||
|
||||
@@ -1711,7 +1711,7 @@ TxQ::accept(Application& app, OpenView& view)
|
||||
// set
|
||||
if (fls >= view.info().seq)
|
||||
{
|
||||
if (ExportLedgerOps::isPendingExportTxn(*stpTrans))
|
||||
if (ExportLedgerOps::isPendingExportWorkTxn(*stpTrans))
|
||||
{
|
||||
auto const pending =
|
||||
ExportLedgerOps::exportTxnCount(view);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/tx/detail/ApplyContext.h>
|
||||
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
|
||||
#include <xrpld/app/tx/detail/InvariantCheck.h>
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -156,4 +157,48 @@ ApplyContext::checkInvariants(TER const result, XRPAmount const fee)
|
||||
std::make_index_sequence<std::tuple_size<InvariantChecks>::value>{});
|
||||
}
|
||||
|
||||
TER
|
||||
ApplyContext::checkExportEmissionLimit(TER const result)
|
||||
{
|
||||
if (!isTesSuccess(result))
|
||||
return result;
|
||||
|
||||
bool addedPendingExport = false;
|
||||
int delta = 0;
|
||||
|
||||
visit([&addedPendingExport, &delta](
|
||||
uint256 const&,
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after) {
|
||||
bool const beforePending =
|
||||
before && ExportLedgerOps::isPendingExportEmission(*before);
|
||||
bool const afterPending =
|
||||
after && ExportLedgerOps::isPendingExportEmission(*after);
|
||||
|
||||
if (!beforePending && afterPending)
|
||||
{
|
||||
addedPendingExport = true;
|
||||
++delta;
|
||||
}
|
||||
else if (beforePending && !afterPending)
|
||||
{
|
||||
--delta;
|
||||
}
|
||||
});
|
||||
|
||||
if (!addedPendingExport)
|
||||
return result;
|
||||
|
||||
auto const pending = ExportLedgerOps::pendingExportEmissionCount(base_);
|
||||
auto const projected = static_cast<int>(pending) + delta;
|
||||
if (projected <= static_cast<int>(ExportLimits::maxPendingExports))
|
||||
return result;
|
||||
|
||||
JLOG(journal.warn()) << "Export emission limit reached pending="
|
||||
<< projected
|
||||
<< " max=" << +ExportLimits::maxPendingExports;
|
||||
return tecDIR_FULL;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -122,6 +122,9 @@ public:
|
||||
TER
|
||||
checkInvariants(TER const result, XRPAmount const fee);
|
||||
|
||||
TER
|
||||
checkExportEmissionLimit(TER const result);
|
||||
|
||||
bool
|
||||
isEmittedTxn()
|
||||
{
|
||||
|
||||
@@ -28,18 +28,50 @@ isPendingExportTxn(STTx const& stx)
|
||||
return isExportTxn(stx) && stx.isFieldPresent(sfExportedTxn);
|
||||
}
|
||||
|
||||
inline bool
|
||||
isPendingExportWorkTxn(STTx const& stx)
|
||||
{
|
||||
return isExportTxn(stx) &&
|
||||
(stx.isFieldPresent(sfExportedTxn) ||
|
||||
stx.isFieldPresent(sfEmitDetails));
|
||||
}
|
||||
|
||||
inline std::size_t
|
||||
exportTxnCount(ReadView const& view)
|
||||
{
|
||||
std::size_t count = 0;
|
||||
for (auto const& tx : view.txs)
|
||||
{
|
||||
if (tx.first && isPendingExportTxn(*tx.first))
|
||||
if (tx.first && isPendingExportWorkTxn(*tx.first))
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
inline bool
|
||||
isPendingExportEmission(SLE const& sle)
|
||||
{
|
||||
if (sle.getType() != ltEMITTED_TXN || !sle.isFieldPresent(sfEmittedTxn))
|
||||
return false;
|
||||
|
||||
auto const& emittedObj = sle.peekAtField(sfEmittedTxn).downcast<STObject>();
|
||||
return emittedObj.isFieldPresent(sfTransactionType) &&
|
||||
emittedObj.getFieldU16(sfTransactionType) == ttEXPORT &&
|
||||
emittedObj.isFieldPresent(sfExportedTxn);
|
||||
}
|
||||
|
||||
inline std::size_t
|
||||
pendingExportEmissionCount(ReadView const& view)
|
||||
{
|
||||
std::size_t count = 0;
|
||||
forEachItem(
|
||||
view, keylet::emittedDir(), [&](std::shared_ptr<SLE const> const& sle) {
|
||||
if (sle && isPendingExportEmission(*sle))
|
||||
++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
inline std::size_t
|
||||
shadowTicketCount(ReadView const& view, AccountID const& account)
|
||||
{
|
||||
|
||||
@@ -1972,7 +1972,12 @@ Transactor::operator()()
|
||||
|
||||
// write state if all chains executed successfully
|
||||
if (isTesSuccess(result))
|
||||
hook::finalizeHookState(stateMap, ctx_, ctx_.tx.getTransactionID());
|
||||
{
|
||||
if (auto const ter = hook::finalizeHookState(
|
||||
stateMap, ctx_, ctx_.tx.getTransactionID());
|
||||
!isTesSuccess(ter))
|
||||
result = ter;
|
||||
}
|
||||
|
||||
// write hook results
|
||||
// this happens irrespective of whether final result was a tesSUCCESS
|
||||
@@ -1981,8 +1986,12 @@ Transactor::operator()()
|
||||
|
||||
for (auto& hookResult : hookResults)
|
||||
{
|
||||
hook::finalizeHookResult(hookResult, ctx_, isTesSuccess(result));
|
||||
if (hookResult.executeAgainAsWeak)
|
||||
if (auto const ter = hook::finalizeHookResult(
|
||||
hookResult, ctx_, isTesSuccess(result));
|
||||
isTesSuccess(result) && !isTesSuccess(ter))
|
||||
result = ter;
|
||||
|
||||
if (isTesSuccess(result) && hookResult.executeAgainAsWeak)
|
||||
{
|
||||
if (aawMap.find(hookResult.account) == aawMap.end())
|
||||
aawMap[hookResult.account] = {hookResult.hookHash};
|
||||
@@ -2278,6 +2287,28 @@ Transactor::operator()()
|
||||
result = tecOVERSIZE;
|
||||
}
|
||||
|
||||
if (applied)
|
||||
{
|
||||
auto const limitResult = ctx_.checkExportEmissionLimit(result);
|
||||
if (!isTesSuccess(limitResult))
|
||||
{
|
||||
result = limitResult;
|
||||
|
||||
auto const resetResult = reset(fee);
|
||||
if (!isTesSuccess(resetResult.first))
|
||||
{
|
||||
result = resetResult.first;
|
||||
applied = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
fee = resetResult.second;
|
||||
result = ctx_.checkInvariants(result, fee);
|
||||
applied = isTesSuccess(result) || isTecClaim(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<TxMeta> metadata;
|
||||
if (applied)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user