From 4de7e7dafaa2d8fe1d6ad4fd6bf176455100b618 Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Thu, 29 Oct 2026 16:18:18 +0100 Subject: [PATCH] Reduce SubTx roundtrip trials and update Cabal caching Run each of the four codec properties for 50 independent trials across the original 0-99 size range. Keep the shared generators unchanged and add deterministic guard-count and Coin-width boundary cases. Use cabal-cache 0.0.1.1 to avoid redundant saves after exact restores. Document the reduced random sample count in the maintenance fragment. --- ...028_cardano-api_haskell_ci_performance.yml | 8 ++ .github/workflows/haskell.yml | 2 +- .../Test/Cardano/Api/Experimental.hs | 87 +++++++++++++++++-- 3 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 .changes/20261028_cardano-api_haskell_ci_performance.yml diff --git a/.changes/20261028_cardano-api_haskell_ci_performance.yml b/.changes/20261028_cardano-api_haskell_ci_performance.yml new file mode 100644 index 0000000000..bd883e22c1 --- /dev/null +++ b/.changes/20261028_cardano-api_haskell_ci_performance.yml @@ -0,0 +1,8 @@ +project: cardano-api +pr: 1401 +kind: + - maintenance +description: | + Reduce each of the four sub-transaction serialisation properties from 100 to 50 random trials while sampling across the original 0–99 size range. Fewer random trials reduce the chance of finding rare serialisation bugs, so regressions caught by the larger sample may pass CI. + + Add two deterministic cases with 23 and 24 guards because the ledger encoder switches from definite-length to indefinite-length collection encoding at 24 elements. Add a third case covering donation values 0 and 1 because zero omits the field, plus Coin pairs 23/24, 255/256, 65535/65536 and 4294967295/4294967296 because CBOR changes from inline integers to 1-, 2-, 4- and 8-byte integer payloads at those boundaries. These fixed cases exercise those format transitions; they do not replace the removed random trials. diff --git a/.github/workflows/haskell.yml b/.github/workflows/haskell.yml index 1e4a45da57..c246c04c1b 100644 --- a/.github/workflows/haskell.yml +++ b/.github/workflows/haskell.yml @@ -123,7 +123,7 @@ jobs: pkg-config --cflags --libs snappy - name: Cache and install Cabal dependencies - uses: input-output-hk/cardano-dev/actions/cabal-cache@cabal-cache-0.0.1.0 + uses: input-output-hk/cardano-dev/actions/cabal-cache@cabal-cache-0.0.1.1 with: cabal-store: ${{ steps.setup-haskell.outputs.cabal-store }} cache-version: ${{ env.CABAL_CACHE_VERSION }} diff --git a/cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental.hs b/cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental.hs index 408d6acb10..bf2c80ffb5 100644 --- a/cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental.hs +++ b/cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental.hs @@ -47,6 +47,7 @@ import Cardano.Slotting.EpochInfo qualified as Slotting import Cardano.Slotting.Slot qualified as Slotting import Cardano.Slotting.Time qualified as Slotting +import Control.Monad (forM_) import Control.Monad.Identity (Identity) import Data.Bifunctor (first) import Data.ByteString qualified as BS @@ -157,6 +158,12 @@ tests = "Top-level body keeps required top-level guards and starting account balance intervals" prop_makeUnsignedTx_dijkstra_top_level_only_fields ] + , testGroup + "SubTx CBOR boundary examples" + [ testProperty "23 guards" $ H.propertyOnce $ checkSubTxBoundaryRoundtrips 23 0 + , testProperty "24 guards" $ H.propertyOnce $ checkSubTxBoundaryRoundtrips 24 0 + , testProperty "Treasury donation integer widths" prop_roundtrip_sub_tx_coin_boundaries + ] , testGroup "makeUnsignedTx" [ testProperty @@ -198,29 +205,44 @@ tests = ] ] +-- | Check unsigned CBOR roundtrips with 50 independent samples across sizes 0–99. prop_roundtrip_cbor_unsigned_sub_tx :: Property -prop_roundtrip_cbor_unsigned_sub_tx = H.property $ do - subTx <- H.forAll genUnsignedSubTx +prop_roundtrip_cbor_unsigned_sub_tx = H.withTests 50 . H.property $ do + subTx <- H.forAll $ Gen.scale subTxRoundtripSize genUnsignedSubTx H.tripping subTx Api.serialiseToCBOR (Api.deserialiseFromCBOR Exp.AsUnsignedSubTx) +-- | Check signed CBOR roundtrips with 50 independent samples across sizes 0–99. prop_roundtrip_cbor_signed_sub_tx :: Property -prop_roundtrip_cbor_signed_sub_tx = H.property $ do - subTx <- H.forAll genSignedSubTx +prop_roundtrip_cbor_signed_sub_tx = H.withTests 50 . H.property $ do + subTx <- H.forAll $ Gen.scale subTxRoundtripSize genSignedSubTx H.tripping subTx Api.serialiseToCBOR (Api.deserialiseFromCBOR Exp.AsSignedSubTx) +-- | Check unsigned TextEnvelope roundtrips with 50 independent samples across sizes 0–99. prop_roundtrip_text_envelope_unsigned_sub_tx :: Property -prop_roundtrip_text_envelope_unsigned_sub_tx = H.property $ do - subTx <- H.forAll genUnsignedSubTx +prop_roundtrip_text_envelope_unsigned_sub_tx = H.withTests 50 . H.property $ do + subTx <- H.forAll $ Gen.scale subTxRoundtripSize genUnsignedSubTx H.tripping subTx (Api.serialiseToTextEnvelope Nothing) Api.deserialiseFromTextEnvelope +-- | Check signed TextEnvelope roundtrips with 50 independent samples across sizes 0–99. prop_roundtrip_text_envelope_signed_sub_tx :: Property -prop_roundtrip_text_envelope_signed_sub_tx = H.property $ do - subTx <- H.forAll genSignedSubTx +prop_roundtrip_text_envelope_signed_sub_tx = H.withTests 50 . H.property $ do + subTx <- H.forAll $ Gen.scale subTxRoundtripSize genSignedSubTx H.tripping subTx (Api.serialiseToTextEnvelope Nothing) Api.deserialiseFromTextEnvelope +-- | Exercise donation and integer encoding branches with fixed SubTx inputs through all four codecs. +-- These cases target known boundaries; they do not replace the removed random trials. +-- The ledger omits donation 0 and emits 1. The other pairs cross CBOR integer payload widths: +-- 23/24: inline to 1 byte; 255/256: 1 to 2 bytes; +-- 65535/65536: 2 to 4 bytes; 4294967295/4294967296: 4 to 8 bytes. +prop_roundtrip_sub_tx_coin_boundaries :: Property +prop_roundtrip_sub_tx_coin_boundaries = H.propertyOnce $ do + let amounts :: [Integer] + amounts = [0, 1, 23, 24, 255, 256, 65535, 65536, 4294967295, 4294967296] + forM_ amounts $ \amount -> checkSubTxBoundaryRoundtrips 0 $ L.Coin amount + prop_sub_tx_envelope_types :: Property prop_sub_tx_envelope_types = H.propertyOnce $ do Api.textEnvelopeType Exp.AsUnsignedSubTx @@ -1309,3 +1331,52 @@ prop_calcMinFeeRecursive_no_tx_outs = H.property $ do let outs = toList $ resultLedgerTx ^. L.bodyTxL . L.outputsTxBodyL -- The result should have exactly one output (the change output) length outs H.=== 1 + +-- | Check a deterministic SubTx fixture through all four codec paths, +-- including its constructed guard count and donation. +-- Counts 23/24 select the ledger OSet encoder's definite and indefinite length paths. +-- This threshold is a ledger encoding choice, not a universal CBOR collection rule. +-- These roundtrips exercise the selected branches; they do not assert golden encoded bytes. +checkSubTxBoundaryRoundtrips + :: H.MonadTest m + => Int + -- ^ Number of distinct script guards. + -> L.Coin + -- ^ Treasury donation, including zero for the omitted-field case. + -> m () +checkSubTxBoundaryRoundtrips guardCount donation = do + H.annotateShow (guardCount, donation) + let credentials = + [ L.ScriptHashObj + . Api.toShelleyScriptHash + . Api.hashScript + . Api.SimpleScript + . Api.RequireTimeAfter + . Api.SlotNo + $ fromIntegral index + | index <- [1 .. guardCount] + ] + guards <- H.evalEither $ Exp.mkTxGuards [(credential, Nothing) | credential <- credentials] + unsigned@(Exp.UnsignedSubTx ledgerTx) <- + H.evalEither . Exp.makeUnsignedSubTx $ + Exp.defaultSubTxBodyContent + & Exp.setTxGuards guards + & Exp.setTxTreasuryDonation donation + let body = ledgerTx ^. L.bodyTxL + signed = Exp.SignedSubTx ledgerTx + length (body ^. UnexportedLedger.guardsTxBodyL) H.=== guardCount + body ^. L.treasuryDonationTxBodyL H.=== donation + H.tripping unsigned Api.serialiseToCBOR (Api.deserialiseFromCBOR Exp.AsUnsignedSubTx) + H.tripping signed Api.serialiseToCBOR (Api.deserialiseFromCBOR Exp.AsSignedSubTx) + H.tripping unsigned (Api.serialiseToTextEnvelope Nothing) Api.deserialiseFromTextEnvelope + H.tripping signed (Api.serialiseToTextEnvelope Nothing) Api.deserialiseFromTextEnvelope + +-- Hedgehog otherwise runs only sizes 0–49 when the test limit is 50. + +-- | Map 50 trials' sizes 0–49 to 0–99; fewer trials lower the chance of finding rare or unexpected field combinations. +-- Cap at 99 for replay; higher trial limits repeat 99 and change the size distribution. +subTxRoundtripSize + :: Range.Size + -- ^ Hedgehog runner size before remapping; the generator remains unchanged. + -> Range.Size +subTxRoundtripSize (Range.Size sampleSize) = Range.Size $ min 99 $ sampleSize * 99 `div` 49