diff --git a/src/test/app/Export_test.cpp b/src/test/app/Export_test.cpp index b523fd61a..83fe85ee4 100644 --- a/src/test/app/Export_test.cpp +++ b/src/test/app/Export_test.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -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(TERtoInt(tesSUCCESS))) + ++accepted; + else if ( + (*meta)[sfTransactionResult] == + static_cast(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); diff --git a/src/xrpld/app/hook/detail/applyHook.cpp b/src/xrpld/app/hook/detail/applyHook.cpp index 0851b6f31..3822490f7 100644 --- a/src/xrpld/app/hook/detail/applyHook.cpp +++ b/src/xrpld/app/hook/detail/applyHook.cpp @@ -1662,6 +1662,8 @@ hook::finalizeHookResult( std::vector> emission_txnid; std::vector exported_txnid; + std::size_t pendingExportEmissions = + ExportLedgerOps::pendingExportEmissionCount(applyCtx.view()); if (doEmit) { @@ -1676,12 +1678,29 @@ hook::finalizeHookResult( std::shared_ptr 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(*ptr) .getField(sfEmitDetails) .downcast(); @@ -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(sit, sfEmittedTxn)); auto page = applyCtx.view().dirInsert( keylet::emittedDir(), emittedId, [&](SLE::ref sle) { (*sle)[sfFlags] = lsfEmittedDir; diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index b4bc9a2ac..d2d51a417 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -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); diff --git a/src/xrpld/app/tx/detail/ApplyContext.cpp b/src/xrpld/app/tx/detail/ApplyContext.cpp index 5a37f74b2..30913f568 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.cpp +++ b/src/xrpld/app/tx/detail/ApplyContext.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -156,4 +157,48 @@ ApplyContext::checkInvariants(TER const result, XRPAmount const fee) std::make_index_sequence::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 const& before, + std::shared_ptr 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(pending) + delta; + if (projected <= static_cast(ExportLimits::maxPendingExports)) + return result; + + JLOG(journal.warn()) << "Export emission limit reached pending=" + << projected + << " max=" << +ExportLimits::maxPendingExports; + return tecDIR_FULL; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/ApplyContext.h b/src/xrpld/app/tx/detail/ApplyContext.h index 104183403..94f2b4693 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.h +++ b/src/xrpld/app/tx/detail/ApplyContext.h @@ -122,6 +122,9 @@ public: TER checkInvariants(TER const result, XRPAmount const fee); + TER + checkExportEmissionLimit(TER const result); + bool isEmittedTxn() { diff --git a/src/xrpld/app/tx/detail/ExportLedgerOps.h b/src/xrpld/app/tx/detail/ExportLedgerOps.h index 5e5bfe466..1c150e7ae 100644 --- a/src/xrpld/app/tx/detail/ExportLedgerOps.h +++ b/src/xrpld/app/tx/detail/ExportLedgerOps.h @@ -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(); + 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 const& sle) { + if (sle && isPendingExportEmission(*sle)) + ++count; + }); + return count; +} + inline std::size_t shadowTicketCount(ReadView const& view, AccountID const& account) { diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 2ed792fed..a9167a457 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -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 metadata; if (applied) {