From 4d08a9e603eb65bf474ddb466c2d9e254fd41abd Mon Sep 17 00:00:00 2001 From: tequ Date: Tue, 29 Sep 2026 04:02:37 +0900 Subject: [PATCH] etxn_fee_base: compute the fee against the applied view (fix20261001) (#842) --- src/test/app/HookAPI_test.cpp | 51 +++++++++++++++++++++++++++ src/xrpld/app/hook/detail/HookAPI.cpp | 22 ++++++++---- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/test/app/HookAPI_test.cpp b/src/test/app/HookAPI_test.cpp index 162574513..c0db18c98 100644 --- a/src/test/app/HookAPI_test.cpp +++ b/src/test/app/HookAPI_test.cpp @@ -732,6 +732,56 @@ public: else BEAST_EXPECT(result.value() == baseFee + memoSize); } + { + // fix20261001: the minimum fee is computed against the view + // being applied, not the node-local open ledger. Simulate that + // by inserting a hook definition (with a callback fee) into + // applyCtx.view() only -- it is never inserted into the open + // ledger the node otherwise reads. + uint256 const hookHash{1}; + + STTx const emittedTx = STTx(ttINVOKE, [&](STObject& obj) { + obj[sfAccount] = alice.id(); + obj[sfSequence] = 0; + obj[sfSigningPubKey] = Slice{}; + obj[sfFirstLedgerSequence] = ov.seq() + 1; + obj[sfLastLedgerSequence] = ov.seq() + 5; + obj[sfFee] = env.closed()->fees().base; + + auto& emitDetails = obj.peekFieldObject(sfEmitDetails); + emitDetails[sfEmitGeneration] = 1; + emitDetails[sfEmitBurden] = 1; + emitDetails[sfEmitParentTxnID] = invokeTx.getTransactionID(); + emitDetails[sfEmitNonce] = uint256(); + emitDetails[sfEmitHookHash] = hookHash; + }); + Serializer const emittedTxSerializer = emittedTx.getSerializer(); + + auto const baseline = + api.etxn_fee_base(emittedTxSerializer.slice()); + BEAST_EXPECT(baseline.has_value()); + + auto hookDef = + std::make_shared(keylet::hookDefinition(hookHash)); + hookDef->setFieldH256(sfHookHash, hookHash); + hookDef->setFieldH256(sfHookNamespace, uint256()); + hookDef->setFieldArray(sfHookParameters, STArray{}); + hookDef->setFieldU16(sfHookApiVersion, 0); + hookDef->setFieldVL(sfCreateCode, std::vector{}); + hookDef->setFieldH256(sfHookSetTxnID, invokeTx.getTransactionID()); + hookDef->setFieldU64(sfReferenceCount, 1); + hookDef->setFieldAmount(sfFee, XRPAmount{0}); + hookDef->setFieldAmount(sfHookCallbackFee, XRPAmount{12345}); + applyCtx.view().insert(hookDef); + + auto const result = api.etxn_fee_base(emittedTxSerializer.slice()); + BEAST_EXPECT(result.has_value()); + + if (env.closed()->rules().enabled(fix20261001)) + BEAST_EXPECT(result.value() == baseline.value() + 12345); + else + BEAST_EXPECT(result.value() == baseline.value()); + } } void @@ -4718,6 +4768,7 @@ public: test_otxn_generation(features); test_etxn_details(features); test_etxn_fee_base(features - fixHookAPI20251128); + test_etxn_fee_base(features - fix20261001); test_etxn_fee_base(features); test_etxn_nonce(features); test_etxn_reserve(features); diff --git a/src/xrpld/app/hook/detail/HookAPI.cpp b/src/xrpld/app/hook/detail/HookAPI.cpp index ec2e5f512..ad594ea1d 100644 --- a/src/xrpld/app/hook/detail/HookAPI.cpp +++ b/src/xrpld/app/hook/detail/HookAPI.cpp @@ -868,14 +868,22 @@ HookAPI::etxn_fee_base(ripple::Slice const& txBlob) const std::unique_ptr stpTrans = std::make_unique(std::ref(sitTrans)); - if (!hookCtx.applyCtx.view().rules().enabled(fixHookAPI20251128)) - return Transactor::calculateBaseFee( - *(applyCtx.app.openLedger().current()), *stpTrans) - .drops(); + // fix20261001: the minimum fee decides whether emit() accepts the + // txn, which is consensus-visible. Compute it against the view being + // applied, not the node-local open ledger (whose hook definitions can + // differ between nodes). + std::shared_ptr hold; + ReadView const* feeView = &applyCtx.view(); + if (!applyCtx.view().rules().enabled(fix20261001)) + { + hold = applyCtx.app.openLedger().current(); + feeView = hold.get(); + } - return invoke_calculateBaseFee( - *(applyCtx.app.openLedger().current()), *stpTrans) - .drops(); + if (!hookCtx.applyCtx.view().rules().enabled(fixHookAPI20251128)) + return Transactor::calculateBaseFee(*feeView, *stpTrans).drops(); + + return invoke_calculateBaseFee(*feeView, *stpTrans).drops(); } catch (std::exception const& e) {