From d6865a4d49994b54f2ef1ac6f0f107638b851134 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:34:33 +0000 Subject: [PATCH 1/3] Initial plan From 8602bba4384a532de88409da98ae270998909ed4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:42:09 +0000 Subject: [PATCH 2/3] Implement reasonable fee check at iteration limit in calcMinFeeRecursive Co-authored-by: carbolymer <228866+carbolymer@users.noreply.github.com> --- .../Api/Experimental/Tx/Internal/Fee.hs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/cardano-api/src/Cardano/Api/Experimental/Tx/Internal/Fee.hs b/cardano-api/src/Cardano/Api/Experimental/Tx/Internal/Fee.hs index 7ce8ebf6d5..6b0abd6b85 100644 --- a/cardano-api/src/Cardano/Api/Experimental/Tx/Internal/Fee.hs +++ b/cardano-api/src/Cardano/Api/Experimental/Tx/Internal/Fee.hs @@ -730,7 +730,13 @@ instance Error FeeCalculationError where -- computed minimum fee and the function recurses. -- -- A maximum iteration limit (currently 50) guards against non-termination. --- In practice convergence occurs within 2–3 iterations. +-- In practice convergence occurs within 2–3 iterations. If the limit is +-- reached, the fee currently in the transaction body is checked: if it is +-- at least the ledger-computed minimum fee (@txBodyFee >= minFee@) the +-- current transaction is returned – the fee is considered /reasonable/ +-- even though strict convergence was not confirmed. Only when the fee +-- falls below the ledger minimum is 'FeeCalculationDidNotConverge' +-- returned. calcMinFeeRecursive :: forall era . IsEra era @@ -790,7 +796,19 @@ calcMinFeeRecursive changeAddr unsignedTx utxo pparams poolids stakeDelegDeposit -> Map (Ledger.Credential Ledger.DRepRole) L.Coin -> Int -> Either FeeCalculationError (UnsignedTx (LedgerEra era)) - go 0 _ _ _ _ _ _ _ = Left FeeCalculationDidNotConverge + -- When the iteration limit is reached, accept the current transaction if its + -- fee is at least the ledger-computed minimum (i.e. the fee is "reasonable"): + -- the result is still useful even though strict convergence was not confirmed. + -- Only fail when the fee would be below the ledger minimum. + go 0 unSignTx@(UnsignedTx ledgerTx) utxo' pparams' _ _ _ nExtraWitnesses' + | txBodyFee >= minFee = do + let outs = toList $ ledgerTx ^. L.bodyTxL . L.outputsTxBodyL + mapM_ (checkOutputMinUTxO pparams') outs + return unSignTx + | otherwise = Left FeeCalculationDidNotConverge + where + minFee = obtainCommonConstraints (useEra @era) $ L.calcMinFeeTx utxo' pparams' ledgerTx nExtraWitnesses' + txBodyFee = ledgerTx ^. L.bodyTxL . L.feeTxBodyL go n unSignTx@(UnsignedTx ledgerTx) utxo' pparams' poolids' stakeDelegDeposits' drepDelegDeposits' nExtraWitnesses' | minFee == txBodyFee && L.isZero txBalanceValue = do -- Case 1 From 7a774504a6e1d5483b0f0d329f4f74e6016a876b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:00:03 +0000 Subject: [PATCH 3/3] Add upper bound (2*minFee) to reasonable fee check at iteration limit Co-authored-by: carbolymer <228866+carbolymer@users.noreply.github.com> --- .../Api/Experimental/Tx/Internal/Fee.hs | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/cardano-api/src/Cardano/Api/Experimental/Tx/Internal/Fee.hs b/cardano-api/src/Cardano/Api/Experimental/Tx/Internal/Fee.hs index 6b0abd6b85..d8dbacb869 100644 --- a/cardano-api/src/Cardano/Api/Experimental/Tx/Internal/Fee.hs +++ b/cardano-api/src/Cardano/Api/Experimental/Tx/Internal/Fee.hs @@ -731,12 +731,13 @@ instance Error FeeCalculationError where -- -- A maximum iteration limit (currently 50) guards against non-termination. -- In practice convergence occurs within 2–3 iterations. If the limit is --- reached, the fee currently in the transaction body is checked: if it is --- at least the ledger-computed minimum fee (@txBodyFee >= minFee@) the --- current transaction is returned – the fee is considered /reasonable/ --- even though strict convergence was not confirmed. Only when the fee --- falls below the ledger minimum is 'FeeCalculationDidNotConverge' --- returned. +-- reached, the fee currently in the transaction body is checked: the fee is +-- considered /reasonable/ (and the current transaction returned) when +-- @minFee <= txBodyFee <= 2 * minFee@. The lower bound ensures the ledger +-- will accept the fee; the upper bound (twice the minimum) rules out a fee so +-- far above the minimum that it likely signals a calculation error rather than +-- ordinary oscillation. Only when these bounds are not met is +-- 'FeeCalculationDidNotConverge' returned. calcMinFeeRecursive :: forall era . IsEra era @@ -797,11 +798,19 @@ calcMinFeeRecursive changeAddr unsignedTx utxo pparams poolids stakeDelegDeposit -> Int -> Either FeeCalculationError (UnsignedTx (LedgerEra era)) -- When the iteration limit is reached, accept the current transaction if its - -- fee is at least the ledger-computed minimum (i.e. the fee is "reasonable"): - -- the result is still useful even though strict convergence was not confirmed. - -- Only fail when the fee would be below the ledger minimum. + -- fee is "reasonable": at least the ledger-computed minimum and at most twice + -- that minimum (@minFee <= txBodyFee <= 2 * minFee@). The result is still + -- useful even though strict convergence was not confirmed. + -- + -- The lower bound ensures the transaction will not be rejected by the ledger + -- for an insufficient fee. The upper bound (2 * minFee) rules out a fee that + -- is so far above the minimum that it likely indicates a calculation error + -- rather than ordinary oscillation. In practice the fee at this point is + -- a ledger-computed minimum from a prior iteration and will be within a tiny + -- fraction of the current minimum (oscillation stems from a few bytes of + -- CBOR encoding difference between iterations). go 0 unSignTx@(UnsignedTx ledgerTx) utxo' pparams' _ _ _ nExtraWitnesses' - | txBodyFee >= minFee = do + | txBodyFee >= minFee && txBodyFee <= 2 * minFee = do let outs = toList $ ledgerTx ^. L.bodyTxL . L.outputsTxBodyL mapM_ (checkOutputMinUTxO pparams') outs return unSignTx