Skip to content

Commit 1567a40

Browse files
committed
Fix flaky genTinySurplusTx by computing surplus dynamically
The hardcoded surplus range assumed a fixed F1 for all inputs, but the fee varies with generated address structure (credential types, staking references). Compute F1 per-test via calcMinFeeTx and set surplus to [F1+4, F1+10]. Also deduplicates genTinySurplusTx — it now lives only in Fee.hs — and adds a deterministic fee gap invariant test.
1 parent e917f12 commit 1567a40

3 files changed

Lines changed: 55 additions & 109 deletions

File tree

cardano-api/cardano-api.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ test-suite cardano-api-test
376376
cardano-ledger-binary,
377377
cardano-ledger-conway,
378378
cardano-ledger-core,
379+
cardano-ledger-dijkstra,
379380
cardano-ledger-mary,
380381
cardano-ledger-shelley,
381382
cardano-protocol-tpraos,

cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental.hs

Lines changed: 12 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE FlexibleContexts #-}
2+
{-# LANGUAGE LambdaCase #-}
23
{-# LANGUAGE NumericUnderscores #-}
34
{-# LANGUAGE OverloadedLists #-}
45
{-# LANGUAGE ScopedTypeVariables #-}
@@ -8,6 +9,7 @@
89
module Test.Cardano.Api.Experimental
910
( tests
1011
, exampleProtocolParams
12+
, exampleProtocolParamsEra
1113
)
1214
where
1315

@@ -27,6 +29,7 @@ import Cardano.Ledger.Api qualified as UnexportedLedger
2729
import Cardano.Ledger.Babbage.TxBody qualified as L
2830
import Cardano.Ledger.Conway qualified as L
2931
import Cardano.Ledger.Core qualified as L
32+
import Cardano.Ledger.Dijkstra.Genesis (DijkstraGenesis (..))
3033
import Cardano.Ledger.Mary.Value qualified as Mary
3134
import Cardano.Ledger.Plutus.Data qualified as L
3235
import Cardano.Slotting.EpochInfo qualified as Slotting
@@ -109,9 +112,6 @@ tests =
109112
, testProperty
110113
"Case 2: transaction with no outputs creates change output"
111114
prop_calcMinFeeRecursive_no_tx_outs
112-
, testProperty
113-
"Tiny surplus consumed by fee increase yields NotEnoughAdaForChangeOutput"
114-
prop_calcMinFeeRecursive_tiny_surplus_not_enough_ada
115115
]
116116
]
117117

@@ -354,6 +354,14 @@ prop_balance_transaction_two_ways = H.propertyOnce $ do
354354
newTx H.=== oldTx
355355
H.success
356356

357+
exampleProtocolParamsEra :: Exp.Era era -> L.PParams (Exp.LedgerEra era)
358+
exampleProtocolParamsEra = \case
359+
Exp.ConwayEra -> exampleProtocolParams
360+
Exp.DijkstraEra ->
361+
UnexportedLedger.upgradePParams
362+
(dgUpgradePParams Genesis.dijkstraGenesisDefaults)
363+
exampleProtocolParams
364+
357365
exampleProtocolParams :: Ledger.PParams UnexportedLedger.ConwayEra
358366
exampleProtocolParams =
359367
UnexportedLedger.upgradePParams conwayUpgrade $
@@ -606,7 +614,7 @@ genFundedSimpleTx era = do
606614
]
607615
-- Surplus of 2–17 ADA ensures funding always exceeds sendCoin + fees.
608616
-- Fees are typically < 1000 lovelace with test protocol parameters
609-
-- (minFeeA=1, minFeeB=0).
617+
-- (feePerByte=1, feeFixed=0).
610618
surplus <- L.Coin <$> Gen.integral (Range.linear 2_000_000 17_000_000)
611619
let fundingCoin = sendCoin + surplus
612620
let ledgerTxIn = Api.toShelleyTxIn txIn
@@ -916,77 +924,3 @@ prop_calcMinFeeRecursive_no_tx_outs = H.property $ do
916924
let outs = toList $ resultLedgerTx ^. L.bodyTxL . L.outputsTxBodyL
917925
-- The result should have exactly one output (the change output)
918926
length outs H.=== 1
919-
920-
-- ---------------------------------------------------------------------------
921-
-- Border case: tiny surplus consumed by fee increase
922-
-- ---------------------------------------------------------------------------
923-
924-
-- | Generates a transaction where the surplus (funding - output) is barely
925-
-- above the fee for the 1-output transaction, but once a change output is
926-
-- appended (increasing the tx size and therefore the fee), the new higher fee
927-
-- exceeds the surplus, driving the change output balance negative.
928-
--
929-
-- Concretely, with test protocol parameters:
930-
-- Fee for 1-output tx (F1) ≈ 236 lovelace
931-
-- Fee for 2-output tx (F2) ≈ 259 lovelace
932-
-- Delta = F2 - F1 ≈ 23
933-
-- A surplus of F1 + 1 to F1 + 15 ensures:
934-
-- 1. After fee convergence at F1, a positive balance triggers Case 2.
935-
-- 2. Adding the change output raises the fee to F2.
936-
-- 3. The change is updated: (surplus - F1) + (F1 - F2) = surplus - F2 < 0.
937-
-- 4. balanceTxOuts returns NotEnoughAdaForChangeOutput.
938-
genTinySurplusTx
939-
:: Exp.Era era
940-
-> Gen
941-
( Exp.UnsignedTx (Exp.LedgerEra era)
942-
, L.UTxO (Exp.LedgerEra era)
943-
, L.Addr
944-
)
945-
genTinySurplusTx era = do
946-
let sbe = convert era
947-
txIn <- genTxIn
948-
addr <- Api.toShelleyAddr <$> genAddressInEra sbe
949-
changeAddr <- Api.toShelleyAddr <$> genAddressInEra sbe
950-
sendCoin <- L.Coin <$> Gen.integral (Range.linear 2_000_000 5_000_000)
951-
-- Tiny margin above F1 but below F2. The exact fee F1 depends on the
952-
-- generated address, but with test protocol params it's around 230–240.
953-
-- A surplus of 240 + small_delta is enough to pass the first fee
954-
-- convergence but not survive the fee increase from adding a change output.
955-
-- We use a narrow range to stay within the F1-to-F2 gap (~23 lovelace).
956-
surplus <- L.Coin <$> Gen.integral (Range.linear 237 250)
957-
let fundingCoin = sendCoin + surplus
958-
let ledgerTxIn = Api.toShelleyTxIn txIn
959-
fundingTxOut =
960-
Exp.obtainCommonConstraints era $
961-
L.mkBasicTxOut addr (L.MaryValue fundingCoin mempty)
962-
utxo = L.UTxO $ Map.singleton ledgerTxIn fundingTxOut
963-
sendTxOut =
964-
Exp.obtainCommonConstraints era $
965-
Exp.TxOut $
966-
Ledger.mkBasicTxOut addr (L.MaryValue sendCoin mempty)
967-
txBodyContent =
968-
Exp.defaultTxBodyContent
969-
& Exp.setTxIns [(txIn, Exp.AnyKeyWitnessPlaceholder)]
970-
& Exp.setTxOuts [sendTxOut]
971-
& Exp.setTxFee 0
972-
return (Exp.makeUnsignedTx era txBodyContent, utxo, changeAddr)
973-
974-
-- | When the surplus is just barely enough to cover the initial fee but not
975-
-- the higher fee after adding a change output, the change output balance
976-
-- goes negative and the function returns NotEnoughAdaForChangeOutput.
977-
prop_calcMinFeeRecursive_tiny_surplus_not_enough_ada :: Property
978-
prop_calcMinFeeRecursive_tiny_surplus_not_enough_ada = H.property $ do
979-
(unsignedTx, utxo, changeAddr) <- H.forAll $ genTinySurplusTx Exp.ConwayEra
980-
case Exp.calcMinFeeRecursive changeAddr unsignedTx utxo exampleProtocolParams mempty mempty mempty 0 of
981-
Left (Exp.NotEnoughAdaForChangeOutput deficit) -> do
982-
H.annotate $ "Deficit: " <> show deficit
983-
H.assert $ deficit < L.Coin 0
984-
Left (Exp.MinUTxONotMet actual required) -> do
985-
-- If surplus - F2 >= 0 (barely), we may land in MinUTxONotMet instead.
986-
-- This is also a valid failure for this border region.
987-
H.annotate $ "Change output ADA: " <> show actual <> ", minUTxO: " <> show required
988-
H.assert $ actual < required
989-
Left err -> H.annotateShow err >> H.failure
990-
Right _ ->
991-
H.annotate "Expected NotEnoughAdaForChangeOutput or MinUTxONotMet but tx balanced successfully"
992-
>> H.failure

cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental/Fee.hs

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Cardano.Api.Ledger qualified as L
1919
import Cardano.Ledger.Api qualified as UnexportedLedger
2020
import Cardano.Ledger.Core qualified as L
2121
import Cardano.Ledger.Mary.Value qualified as Mary
22+
import Cardano.Ledger.Tools qualified as L (calcMinFeeTx)
2223

2324
import Data.Foldable (toList)
2425
import Data.Map.Strict qualified as Map
@@ -27,7 +28,7 @@ import Lens.Micro
2728

2829
import Test.Gen.Cardano.Api.Typed (genAddressInEra, genStakeCredential, genTxIn)
2930

30-
import Test.Cardano.Api.Experimental (exampleProtocolParams)
31+
import Test.Cardano.Api.Experimental (exampleProtocolParams, exampleProtocolParamsEra)
3132

3233
import Hedgehog (Gen, Property)
3334
import Hedgehog qualified as H
@@ -127,7 +128,7 @@ genFundedSimpleTx era = do
127128
]
128129
-- Surplus of 2–17 ADA ensures funding always exceeds sendCoin + fees.
129130
-- Fees are typically < 1000 lovelace with test protocol parameters
130-
-- (minFeeA=1, minFeeB=0).
131+
-- (feePerByte=1, feeFixed=0).
131132
surplus <- L.Coin <$> Gen.integral (Range.linear 2_000_000 17_000_000)
132133
let fundingCoin = sendCoin + surplus
133134
let ledgerTxIn = Api.toShelleyTxIn txIn
@@ -328,55 +329,65 @@ genNoOutputsTx era = do
328329
& Exp.setTxFee 0
329330
return (Exp.makeUnsignedTx era txBodyContent, utxo, changeAddr)
330331

331-
-- | Generates a transaction where the surplus (funding - output) is barely
332-
-- above the fee for the 1-output transaction, but once a change output is
333-
-- appended (increasing the tx size and therefore the fee), the new higher fee
334-
-- exceeds the surplus, driving the change output balance negative.
332+
-- | Generates a transaction designed to trigger 'NotEnoughAdaForChangeOutput'.
335333
--
336-
-- Concretely, with test protocol parameters:
337-
-- Fee for 1-output tx (F1) ≈ 236 lovelace
338-
-- Fee for 2-output tx (F2) ≈ 259 lovelace
339-
-- Delta = F2 - F1 ≈ 23
340-
-- A surplus of F1 + 1 to F1 + 15 ensures:
341-
-- 1. After fee convergence at F1, a positive balance triggers Case 2.
342-
-- 2. Adding the change output raises the fee to F2.
343-
-- 3. The change is updated: (surplus - F1) + (F1 - F2) = surplus - F2 < 0.
344-
-- 4. balanceTxOuts returns NotEnoughAdaForChangeOutput.
334+
-- The generator:
335+
--
336+
-- 1. Generates random tx parts (TxIn, addresses, send amount 2–5 ADA).
337+
-- 2. Builds a 1-output tx with fee=0.
338+
-- 3. Computes F1 via 'calcMinFeeTx' — the actual min fee for this specific
339+
-- tx. F1 varies per run because different random addresses have different
340+
-- serialized sizes.
341+
-- 4. Picks a surplus in @[F1+4, F1+10]@ — enough for 'calcMinFeeRecursive'
342+
-- to converge the fee and attempt a change output, but not enough to
343+
-- survive the fee increase (~23 bytes) caused by that extra output.
344+
-- 5. Builds the UTxO with @sendCoin + surplus@ as the funding amount.
345+
--
346+
-- The property test calls 'calcMinFeeRecursive' on the result and expects
347+
-- failure: the change output drives the balance negative.
345348
genTinySurplusTx
346349
:: Exp.Era era
347350
-> Gen
348351
( Exp.UnsignedTx (Exp.LedgerEra era)
349352
, L.UTxO (Exp.LedgerEra era)
350353
, L.Addr
351354
)
352-
genTinySurplusTx era = do
355+
genTinySurplusTx era = Exp.obtainCommonConstraints era $ do
353356
let sbe = convert era
354357
txIn <- genTxIn
355358
addr <- Api.toShelleyAddr <$> genAddressInEra sbe
356359
changeAddr <- Api.toShelleyAddr <$> genAddressInEra sbe
357360
sendCoin <- L.Coin <$> Gen.integral (Range.linear 2_000_000 5_000_000)
358-
-- Tiny margin above F1 but below F2. The exact fee F1 depends on the
359-
-- generated address, but with test protocol params it's around 230–240.
360-
-- A surplus of 240 + small_delta is enough to pass the first fee
361-
-- convergence but not survive the fee increase from adding a change output.
362-
-- We use a narrow range to stay within the F1-to-F2 gap (~23 lovelace).
363-
surplus <- L.Coin <$> Gen.integral (Range.linear 237 250)
364-
let fundingCoin = sendCoin + surplus
361+
-- Build a preliminary tx to measure F1 (min fee for the 1-output shape).
362+
-- The fee depends on serialized tx size, which varies with the generated
363+
-- address structure. We use sendCoin as the funding amount; adding
364+
-- a few hundred lovelace of surplus won't change the CBOR encoding size
365+
-- of the multi-million lovelace coin value.
365366
let ledgerTxIn = Api.toShelleyTxIn txIn
366-
fundingTxOut =
367-
Exp.obtainCommonConstraints era $
368-
L.mkBasicTxOut addr (L.MaryValue fundingCoin mempty)
369-
utxo = L.UTxO $ Map.singleton ledgerTxIn fundingTxOut
367+
prelimFundingTxOut =
368+
L.mkBasicTxOut addr (L.MaryValue sendCoin mempty)
369+
prelimUtxo = L.UTxO $ Map.singleton ledgerTxIn prelimFundingTxOut
370370
sendTxOut =
371-
Exp.obtainCommonConstraints era $
372-
Exp.TxOut $
373-
L.mkBasicTxOut addr (L.MaryValue sendCoin mempty)
371+
Exp.TxOut $
372+
L.mkBasicTxOut addr (L.MaryValue sendCoin mempty)
374373
txBodyContent =
375374
Exp.defaultTxBodyContent
376375
& Exp.setTxIns [(txIn, Exp.AnyKeyWitnessPlaceholder)]
377376
& Exp.setTxOuts [sendTxOut]
378377
& Exp.setTxFee 0
379-
return (Exp.makeUnsignedTx era txBodyContent, utxo, changeAddr)
378+
unsignedTx = Exp.makeUnsignedTx era txBodyContent
379+
-- Compute F1 via case match on UnsignedTx (needed to bring EraTx into scope)
380+
L.Coin f1 = case unsignedTx of
381+
Exp.UnsignedTx prelimLedgerTx ->
382+
L.calcMinFeeTx prelimUtxo (exampleProtocolParamsEra era) prelimLedgerTx 0
383+
-- Surplus just above F1 but well below F2 (≈ F1 + 23). This is enough
384+
-- to pass fee convergence but not survive adding a change output.
385+
surplus <- L.Coin <$> Gen.integral (Range.linear (f1 + 4) (f1 + 10))
386+
let fundingCoin = sendCoin + surplus
387+
fundingTxOut =
388+
L.mkBasicTxOut addr (L.MaryValue fundingCoin mempty)
389+
utxo = L.UTxO $ Map.singleton ledgerTxIn fundingTxOut
390+
return (unsignedTx, utxo, changeAddr)
380391

381392
-- ---------------------------------------------------------------------------
382393
-- Property tests

0 commit comments

Comments
 (0)