diff --git a/src/test/app/HookAPI_test.cpp b/src/test/app/HookAPI_test.cpp index a5f37d2d0..06fff5af1 100644 --- a/src/test/app/HookAPI_test.cpp +++ b/src/test/app/HookAPI_test.cpp @@ -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( diff --git a/src/xrpld/app/consensus/ExportIntent.md b/src/xrpld/app/consensus/ExportIntent.md index c0aa54278..744c45646 100644 --- a/src/xrpld/app/consensus/ExportIntent.md +++ b/src/xrpld/app/consensus/ExportIntent.md @@ -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 diff --git a/src/xrpld/app/hook/detail/HookAPI.cpp b/src/xrpld/app/hook/detail/HookAPI.cpp index 6b0b0f1c7..7db74f04c 100644 --- a/src/xrpld/app/hook/detail/HookAPI.cpp +++ b/src/xrpld/app/hook/detail/HookAPI.cpp @@ -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)) {