fix(hooks): require xport for export emission

This commit is contained in:
Nicholas Dudfield
2026-07-16 19:33:04 +07:00
parent 8f74614857
commit 516fda4536
3 changed files with 32 additions and 1 deletions

View File

@@ -339,6 +339,25 @@ public:
auto const result = api.emit(tx.getSerializer().slice());
BEAST_EXPECT(result.error() == EMISSION_FAILURE);
}
{
// Export wrappers require xport() so they cannot bypass its
// committee check or per-Hook Export reservation.
auto hookCtx = makeStubHookContext(
applyCtx,
alice.id(),
alice.id(),
{
.expected_etxn_count = 1,
.nonce_used = {{uint256(0), true}},
});
auto& api = hookCtx.api();
auto const inner = makeExportedPayment(alice.id(), bob.id());
auto const wrapper = makeExportWrapper(alice.id(), inner);
auto const result = api.emit(wrapper.getSerializer().slice());
BEAST_EXPECT(result.error() == EMISSION_FAILURE);
BEAST_EXPECT(hookCtx.result.emittedTxn.empty());
BEAST_EXPECT(hookCtx.export_count == 0);
}
{
// HookCanEmit (non-SetHook)
auto hookCtx = makeStubHookContext(

View File

@@ -263,7 +263,9 @@ execution may reserve at most `maxExportsPerHook` exports (currently two), and
those wrappers also consume the normal emitted-transaction allowance. An
emitted `ttEXPORT` must carry an intent and reference a pre-existing committee
digest; it cannot inline-create a committee roster. The protocol constant and
the Hook ABI's `max_export` constant must remain equal.
the Hook ABI's `max_export` constant must remain equal. Generic `emit()` rejects
`ttEXPORT`; Hook-created Export wrappers must use `xport()` so those accounting
and committee-admission rules cannot be bypassed.
**INV-13 - Return callbacks remain owner-authorized Imports.**
An XPOP whose proven target transaction carries `sfTicketSequence` takes the

View File

@@ -536,6 +536,16 @@ HookAPI::emit(Slice const& txBlob) const
ripple::TxType txType = stpTrans->getTxnType();
// Export wrappers must flow through xport(), which applies the dedicated
// per-Hook cap and verifies the referenced account-owned committee before
// queueing the emitted transaction.
if (txType == ttEXPORT)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: Export wrappers require xport().";
return Unexpected(EMISSION_FAILURE);
}
ripple::uint256 const& hookCanEmit = hookCtx.result.hookCanEmit;
if (!hook::canEmit(txType, hookCanEmit))
{