feat(export): wip export system limits

- max_export per hook: 4 → 2
- maxPendingExports: cap exported directory at 8 entries (tecDIR_FULL)
- clamp inbound signature processing in PeerImp to directory cap

The directory cap is the root DoS constraint: each pending export
requires every validator to sign and broadcast every round. Inbound
processing and signing throughput are transitively bounded by it.
This commit is contained in:
Nicholas Dudfield
2026-03-16 13:58:36 +07:00
parent b65d9faf12
commit 89274b5387
4 changed files with 86 additions and 2 deletions

View File

@@ -402,7 +402,7 @@ const uint16_t max_state_modifications = 256;
const uint8_t max_slots = 255;
const uint8_t max_nonce = 255;
const uint8_t max_emit = 255;
const uint8_t max_export = 4;
const uint8_t max_export = 2;
const uint8_t max_params = 16;
const double fee_base_multiplier = 1.1f;

View File

@@ -0,0 +1,32 @@
#ifndef RIPPLE_HOOK_EXPORT_LIMITS_H_INCLUDED
#define RIPPLE_HOOK_EXPORT_LIMITS_H_INCLUDED
#include <cstdint>
namespace ripple {
// Export system caps.
//
// These limits bound the DoS surface of the export signature system:
// - Each pending export requires every validator to sign it every round
// (sign-once, broadcast-many via TMValidation)
// - Inbound signature processing involves crypto verification per sig
// - The directory cap (maxPendingExports) is the root constraint;
// signing throughput and inbound processing are transitively bounded by it
struct ExportLimits
{
// Maximum exports a single hook execution may produce
// (also enforced by hook_api::max_export in Enum.h)
static constexpr std::uint8_t maxExportsPerHook = 2;
// Maximum pending exports in the exported directory at any time.
// This transitively caps:
// - signatures per TMValidation message (1 per pending export)
// - inbound signature processing in PeerImp (clamped to this)
// - validator signing work per round
static constexpr std::uint8_t maxPendingExports = 8;
};
} // namespace ripple
#endif

View File

@@ -7,8 +7,10 @@
#include <xrpld/app/misc/TxQ.h>
#include <xrpld/app/tx/detail/Import.h>
#include <xrpld/app/tx/detail/NFTokenUtils.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/hook/ExportLimits.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/st.h>
@@ -1735,6 +1737,43 @@ hook::finalizeHookResult(
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 (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);

View File

@@ -39,6 +39,7 @@
#include <xrpl/basics/random.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/core/LexicalCast.h>
#include <xrpl/hook/ExportLimits.h>
#include <xrpl/protocol/digest.h>
#include <boost/algorithm/string/predicate.hpp>
@@ -3065,7 +3066,19 @@ PeerImp::checkValidation(
{
auto const currentSeq = val->getFieldU32(sfLedgerSequence);
for (int i = 0; i < packet->exportsignatures_size(); ++i)
// Clamp inbound export signatures to the directory cap.
// A legitimate validator can only have maxPendingExports
// pending, so anything beyond that is either a bug or abuse.
auto const sigCount = std::min(
packet->exportsignatures_size(),
static_cast<int>(ExportLimits::maxPendingExports));
if (sigCount < packet->exportsignatures_size())
{
JLOG(p_journal_.warn())
<< "Export: clamping " << packet->exportsignatures_size()
<< " signatures to cap " << sigCount;
}
for (int i = 0; i < sigCount; ++i)
{
try
{