From 42621e68a876a494eedd82794f5a10825a02ee81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Tom=C3=A9=20Corti=C3=B1as?= Date: Fri, 5 Dec 2025 12:37:05 +0100 Subject: [PATCH 1/6] Modify test to not depend on 0 threshold for CC --- .../impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs index bdd9172e0b2..5f80bfdde41 100644 --- a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs +++ b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs @@ -263,7 +263,7 @@ spoAndCCVotingSpec = do SNothing initialCommittee committeeMap - (0 %! 1) + (1 %! 1) submitYesVote_ (DRepVoter drep) committeeActionId submitYesVote_ (StakePoolVoter spo) committeeActionId passNEpochs 5 From 94b1d52de73669b58ff40e21679c3ea8f2511a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Tom=C3=A9=20Corti=C3=B1as?= Date: Fri, 5 Dec 2025 16:30:17 +0100 Subject: [PATCH 2/6] Add Imp annotations to combinators --- .../impl/testlib/Test/Cardano/Ledger/Conway/ImpTest.hs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/ImpTest.hs b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/ImpTest.hs index b250cec1df9..b8d1a2d6766 100644 --- a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/ImpTest.hs +++ b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/ImpTest.hs @@ -1049,7 +1049,8 @@ ccShouldNotBeExpired :: ccShouldNotBeExpired coldC = do curEpochNo <- getsNES nesELL ccExpiryEpochNo <- getCCExpiry coldC - curEpochNo `shouldSatisfy` (<= ccExpiryEpochNo) + impAnn "ccShouldNotBeExpired" $ + curEpochNo `shouldSatisfy` (<= ccExpiryEpochNo) ccShouldBeExpired :: (HasCallStack, ConwayEraGov era) => @@ -1058,7 +1059,8 @@ ccShouldBeExpired :: ccShouldBeExpired coldC = do curEpochNo <- getsNES nesELL ccExpiryEpochNo <- getCCExpiry coldC - curEpochNo `shouldSatisfy` (> ccExpiryEpochNo) + impAnn "ccShouldBeExpired" $ + curEpochNo `shouldSatisfy` (> ccExpiryEpochNo) getCCExpiry :: (HasCallStack, ConwayEraGov era) => From b4da67dcf276c70adbc73dcfa38839960733569c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Tom=C3=A9=20Corti=C3=B1as?= Date: Fri, 5 Dec 2025 13:34:52 +0100 Subject: [PATCH 3/6] Add Imp tests to cover more cases of active/inactive committee --- .../Cardano/Ledger/Conway/Imp/RatifySpec.hs | 230 ++++++++++++++---- 1 file changed, 188 insertions(+), 42 deletions(-) diff --git a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs index 5f80bfdde41..dad355aff19 100644 --- a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs +++ b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs @@ -143,6 +143,48 @@ spoAndCCVotingSpec = do getLastEnactedParameterChange `shouldReturn` SNothing getsPParams ppMinFeeRefScriptCostPerByteL `shouldReturn` initialRefScriptBaseFee + -- https://github.com/IntersectMBO/cardano-ledger/issues/5418 + -- TODO: Re-enable after issue is resolved, by removing this override + disableInConformanceIt "Committee proposals pass" $ + whenPostBootstrap $ do + modifyPParams $ \pp -> + pp + & ppCommitteeMinSizeL .~ 2 + & ppCommitteeMaxTermLengthL .~ EpochInterval 50 + coldCommitteeActive <- KeyHashObj <$> freshKeyHash + coldCommitteeInactive <- KeyHashObj <$> freshKeyHash + startingEpoch <- getsNES nesELL + maxTermLength <- getsPParams ppCommitteeMaxTermLengthL + (drep, _, _) <- setupSingleDRep 1_000_000_000 + (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 + let + committeeMap = + [ (coldCommitteeActive, addEpochInterval startingEpoch maxTermLength) + , (coldCommitteeInactive, addEpochInterval startingEpoch $ EpochInterval 5) + ] + initialCommittee <- getCommitteeMembers + committeeActionId <- + impAnn "Submit committee update" + . submitGovAction + $ UpdateCommittee + SNothing + initialCommittee + committeeMap + (1 %! 1) + submitYesVote_ (DRepVoter drep) committeeActionId + submitYesVote_ (StakePoolVoter spo) committeeActionId + passNEpochs 5 + getCommitteeMembers `shouldReturn` Map.keysSet committeeMap + committeeProposal <- + elements + [ NoConfidence (SJust (GovPurposeId committeeActionId)) + , UpdateCommittee (SJust (GovPurposeId committeeActionId)) Set.empty [] (0 %! 1) + ] + committeeActionId2 <- submitGovAction committeeProposal + submitYesVote_ (DRepVoter drep) committeeActionId2 + submitYesVote_ (StakePoolVoter spo) committeeActionId2 + passNEpochs 2 + getLastEnactedCommittee `shouldReturn` SJust (GovPurposeId committeeActionId2) describe "When CC threshold is 0" $ do -- During the bootstrap phase, proposals that modify the committee are not allowed, -- hence we need to directly set the threshold for the initial members @@ -234,50 +276,154 @@ spoAndCCVotingSpec = do newConstitution <- arbitrary constitutionActionId <- submitGovAction $ NewConstitution SNothing newConstitution logRatificationChecks constitutionActionId + passEpoch + ccShouldBeExpired coldCommitteeInactive + passEpoch + ccShouldNotBeExpired coldCommitteeActive + getConstitution `shouldNotReturn` newConstitution + -- https://github.com/IntersectMBO/cardano-ledger/issues/5418 + -- TODO: Re-enable after issue is resolved, by removing this override + disableInConformanceIt + "Constitution cannot be changed if committee is not active because it doesn't have registered hot credentials" + $ whenPostBootstrap + $ do + modifyPParams $ \pp -> + pp + & ppCommitteeMinSizeL .~ 2 + modifyCommittee $ fmap (committeeThresholdL .~ 0 %! 1) + (drep, _, _) <- setupSingleDRep 1_000_000_000 + SJust committee <- getCommittee + committeeThreshold committee `shouldBe` 0 %! 1 + Map.size (committeeMembers committee) `shouldBe` 2 + forM_ (Map.keys $ committeeMembers committee) ccShouldNotBeExpired + oldConstitution <- getConstitution + (proposal, _) <- mkConstitutionProposal SNothing + gaiConstitution <- submitProposal proposal + submitYesVote_ (DRepVoter drep) gaiConstitution + passNEpochs 2 + getConstitution `shouldReturn` oldConstitution + it + "Constitution can be changed when an active committee doesn't vote" + $ whenPostBootstrap + $ do + modifyPParams $ \pp -> + pp + & ppCommitteeMinSizeL .~ 2 + modifyCommittee $ fmap (committeeThresholdL .~ 0 %! 1) + (drep, _, _) <- setupSingleDRep 1_000_000_000 + SJust committee <- getCommittee + committeeThreshold committee `shouldBe` 0 %! 1 + Map.size (committeeMembers committee) `shouldBe` 2 + forM_ (Map.keys $ committeeMembers committee) ccShouldNotBeExpired + (proposal, newConstitution) <- mkConstitutionProposal SNothing + gaiConstitution <- submitProposal proposal + submitYesVote_ (DRepVoter drep) gaiConstitution + mapM_ registerCommitteeHotKey (Map.keys $ committeeMembers committee) + passNEpochs 2 + getConstitution `shouldReturn` newConstitution + it + "Constitution can be changed regardless of active committee votes" + $ whenPostBootstrap + $ do + modifyPParams $ \pp -> + pp + & ppCommitteeMinSizeL .~ 2 + modifyCommittee $ fmap (committeeThresholdL .~ 0 %! 1) + (drep, _, _) <- setupSingleDRep 1_000_000_000 + SJust committee <- getCommittee + committeeThreshold committee `shouldBe` 0 %! 1 + Map.size (committeeMembers committee) `shouldBe` 2 + forM_ (Map.keys $ committeeMembers committee) ccShouldNotBeExpired + (proposal, newConstitution) <- mkConstitutionProposal SNothing + gaiConstitution <- submitProposal proposal + submitYesVote_ (DRepVoter drep) gaiConstitution + hotKeys <- mapM registerCommitteeHotKey (Map.keys $ committeeMembers committee) + forM_ hotKeys $ \c -> + oneof + [ return () + , submitYesVote_ (CommitteeVoter c) gaiConstitution + , submitVote_ VoteNo (CommitteeVoter c) gaiConstitution + ] + passNEpochs 2 + getConstitution `shouldReturn` newConstitution + it + "Constitution can be changed if min committee size is 0" + . whenPostBootstrap + $ do + modifyPParams $ \pp -> + pp + & ppDRepVotingThresholdsL . dvtUpdateToConstitutionL .~ (0 %! 1) + & ppCommitteeMinSizeL .~ 0 + & ppCommitteeMaxTermLengthL .~ EpochInterval 50 + coldCommitteeActive <- KeyHashObj <$> freshKeyHash + coldCommitteeInactive <- KeyHashObj <$> freshKeyHash + startingEpoch <- getsNES nesELL + maxTermLength <- getsPParams ppCommitteeMaxTermLengthL + (dRep, _, _) <- setupSingleDRep 1_000_000_000 + (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 + let + committeeMap = + [ (coldCommitteeActive, addEpochInterval startingEpoch maxTermLength) + , (coldCommitteeInactive, addEpochInterval startingEpoch $ EpochInterval 5) + ] + initialCommittee <- getCommitteeMembers + committeeActionId <- + impAnn "Submit committee update" + . submitGovAction + $ UpdateCommittee + SNothing + initialCommittee + committeeMap + (0 %! 1) + submitYesVote_ (DRepVoter dRep) committeeActionId + submitYesVote_ (StakePoolVoter spo) committeeActionId + passNEpochs 2 + getCommitteeMembers `shouldReturn` Map.keysSet committeeMap + passNEpochs 3 + newConstitution <- arbitrary + constitutionActionId <- submitGovAction $ NewConstitution SNothing newConstitution + logRatificationChecks constitutionActionId + passNEpochs 2 + getConstitution `shouldReturn` newConstitution + describe "When CC threshold is not 0" $ do + it "Constitution cannot be changed if min committee size is 0" + . whenPostBootstrap + $ do + modifyPParams $ \pp -> + pp + & ppDRepVotingThresholdsL . dvtUpdateToConstitutionL .~ (0 %! 1) + & ppCommitteeMinSizeL .~ 0 + & ppCommitteeMaxTermLengthL .~ EpochInterval 50 + coldCommitteeActive <- KeyHashObj <$> freshKeyHash + coldCommitteeInactive <- KeyHashObj <$> freshKeyHash + startingEpoch <- getsNES nesELL + maxTermLength <- getsPParams ppCommitteeMaxTermLengthL + (dRep, _, _) <- setupSingleDRep 1_000_000_000 + (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 + let + committeeMap = + [ (coldCommitteeActive, addEpochInterval startingEpoch maxTermLength) + , (coldCommitteeInactive, addEpochInterval startingEpoch $ EpochInterval 5) + ] + initialCommittee <- getCommitteeMembers + committeeActionId <- + impAnn "Submit committee update" + . submitGovAction + $ UpdateCommittee + SNothing + initialCommittee + committeeMap + (1 %! 1) + submitYesVote_ (DRepVoter dRep) committeeActionId + submitYesVote_ (StakePoolVoter spo) committeeActionId + passNEpochs 2 + getCommitteeMembers `shouldReturn` Map.keysSet committeeMap + passNEpochs 3 + newConstitution <- arbitrary + constitutionActionId <- submitGovAction $ NewConstitution SNothing newConstitution + logRatificationChecks constitutionActionId passNEpochs 2 getConstitution `shouldNotReturn` newConstitution - -- https://github.com/IntersectMBO/cardano-ledger/issues/5418 - -- TODO: Re-enable after issue is resolved, by removing this override - disableInConformanceIt "Committee proposals pass with inactive committee" $ - whenPostBootstrap $ do - modifyPParams $ \pp -> - pp - & ppCommitteeMinSizeL .~ 2 - & ppCommitteeMaxTermLengthL .~ EpochInterval 50 - coldCommitteeActive <- KeyHashObj <$> freshKeyHash - coldCommitteeInactive <- KeyHashObj <$> freshKeyHash - startingEpoch <- getsNES nesELL - maxTermLength <- getsPParams ppCommitteeMaxTermLengthL - (drep, _, _) <- setupSingleDRep 1_000_000_000 - (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 - let - committeeMap = - [ (coldCommitteeActive, addEpochInterval startingEpoch maxTermLength) - , (coldCommitteeInactive, addEpochInterval startingEpoch $ EpochInterval 5) - ] - initialCommittee <- getCommitteeMembers - committeeActionId <- - impAnn "Submit committee update" - . submitGovAction - $ UpdateCommittee - SNothing - initialCommittee - committeeMap - (1 %! 1) - submitYesVote_ (DRepVoter drep) committeeActionId - submitYesVote_ (StakePoolVoter spo) committeeActionId - passNEpochs 5 - getCommitteeMembers `shouldReturn` Map.keysSet committeeMap - committeeProposal <- - elements - [ NoConfidence (SJust (GovPurposeId committeeActionId)) - , UpdateCommittee (SJust (GovPurposeId committeeActionId)) Set.empty [] (0 %! 1) - ] - committeeActionId2 <- submitGovAction committeeProposal - submitYesVote_ (DRepVoter drep) committeeActionId2 - submitYesVote_ (StakePoolVoter spo) committeeActionId2 - passNEpochs 2 - getLastEnactedCommittee `shouldReturn` SJust (GovPurposeId committeeActionId2) committeeExpiryResignationDiscountSpec :: forall era. From 51da3533d8564ca807b3e86b13e1cb76a0cbff3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Tom=C3=A9=20Corti=C3=B1as?= Date: Mon, 8 Dec 2025 12:44:25 +0100 Subject: [PATCH 4/6] Add separate test for zero/non zero active members in inactive committee --- .../Cardano/Ledger/Conway/Imp/RatifySpec.hs | 308 +++++++++--------- 1 file changed, 163 insertions(+), 145 deletions(-) diff --git a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs index dad355aff19..e5a695b4dcb 100644 --- a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs +++ b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs @@ -240,151 +240,169 @@ spoAndCCVotingSpec = do else do getLastEnactedParameterChange `shouldReturn` SNothing newRefScriptBaseFee `shouldBe` initialRefScriptBaseFee - it "Constitution cannot be changed if active committee size is below min size" - . whenPostBootstrap - $ do - modifyPParams $ \pp -> - pp - & ppDRepVotingThresholdsL . dvtUpdateToConstitutionL .~ (0 %! 1) - & ppCommitteeMinSizeL .~ 2 - & ppCommitteeMaxTermLengthL .~ EpochInterval 50 - coldCommitteeActive <- KeyHashObj <$> freshKeyHash - coldCommitteeInactive <- KeyHashObj <$> freshKeyHash - startingEpoch <- getsNES nesELL - maxTermLength <- getsPParams ppCommitteeMaxTermLengthL - (dRep, _, _) <- setupSingleDRep 1_000_000_000 - (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 - let - committeeMap = - [ (coldCommitteeActive, addEpochInterval startingEpoch maxTermLength) - , (coldCommitteeInactive, addEpochInterval startingEpoch $ EpochInterval 5) - ] - initialCommittee <- getCommitteeMembers - committeeActionId <- - impAnn "Submit committee update" - . submitGovAction - $ UpdateCommittee - SNothing - initialCommittee - committeeMap - (0 %! 1) - submitYesVote_ (DRepVoter dRep) committeeActionId - submitYesVote_ (StakePoolVoter spo) committeeActionId - passNEpochs 2 - getCommitteeMembers `shouldReturn` Map.keysSet committeeMap - passNEpochs 3 - newConstitution <- arbitrary - constitutionActionId <- submitGovAction $ NewConstitution SNothing newConstitution - logRatificationChecks constitutionActionId - passEpoch - ccShouldBeExpired coldCommitteeInactive - passEpoch - ccShouldNotBeExpired coldCommitteeActive - getConstitution `shouldNotReturn` newConstitution - -- https://github.com/IntersectMBO/cardano-ledger/issues/5418 - -- TODO: Re-enable after issue is resolved, by removing this override - disableInConformanceIt - "Constitution cannot be changed if committee is not active because it doesn't have registered hot credentials" - $ whenPostBootstrap - $ do - modifyPParams $ \pp -> - pp - & ppCommitteeMinSizeL .~ 2 - modifyCommittee $ fmap (committeeThresholdL .~ 0 %! 1) - (drep, _, _) <- setupSingleDRep 1_000_000_000 - SJust committee <- getCommittee - committeeThreshold committee `shouldBe` 0 %! 1 - Map.size (committeeMembers committee) `shouldBe` 2 - forM_ (Map.keys $ committeeMembers committee) ccShouldNotBeExpired - oldConstitution <- getConstitution - (proposal, _) <- mkConstitutionProposal SNothing - gaiConstitution <- submitProposal proposal - submitYesVote_ (DRepVoter drep) gaiConstitution - passNEpochs 2 - getConstitution `shouldReturn` oldConstitution - it - "Constitution can be changed when an active committee doesn't vote" - $ whenPostBootstrap - $ do - modifyPParams $ \pp -> - pp - & ppCommitteeMinSizeL .~ 2 - modifyCommittee $ fmap (committeeThresholdL .~ 0 %! 1) - (drep, _, _) <- setupSingleDRep 1_000_000_000 - SJust committee <- getCommittee - committeeThreshold committee `shouldBe` 0 %! 1 - Map.size (committeeMembers committee) `shouldBe` 2 - forM_ (Map.keys $ committeeMembers committee) ccShouldNotBeExpired - (proposal, newConstitution) <- mkConstitutionProposal SNothing - gaiConstitution <- submitProposal proposal - submitYesVote_ (DRepVoter drep) gaiConstitution - mapM_ registerCommitteeHotKey (Map.keys $ committeeMembers committee) - passNEpochs 2 - getConstitution `shouldReturn` newConstitution - it - "Constitution can be changed regardless of active committee votes" - $ whenPostBootstrap - $ do - modifyPParams $ \pp -> - pp - & ppCommitteeMinSizeL .~ 2 - modifyCommittee $ fmap (committeeThresholdL .~ 0 %! 1) - (drep, _, _) <- setupSingleDRep 1_000_000_000 - SJust committee <- getCommittee - committeeThreshold committee `shouldBe` 0 %! 1 - Map.size (committeeMembers committee) `shouldBe` 2 - forM_ (Map.keys $ committeeMembers committee) ccShouldNotBeExpired - (proposal, newConstitution) <- mkConstitutionProposal SNothing - gaiConstitution <- submitProposal proposal - submitYesVote_ (DRepVoter drep) gaiConstitution - hotKeys <- mapM registerCommitteeHotKey (Map.keys $ committeeMembers committee) - forM_ hotKeys $ \c -> - oneof - [ return () - , submitYesVote_ (CommitteeVoter c) gaiConstitution - , submitVote_ VoteNo (CommitteeVoter c) gaiConstitution - ] - passNEpochs 2 - getConstitution `shouldReturn` newConstitution - it - "Constitution can be changed if min committee size is 0" - . whenPostBootstrap - $ do - modifyPParams $ \pp -> - pp - & ppDRepVotingThresholdsL . dvtUpdateToConstitutionL .~ (0 %! 1) - & ppCommitteeMinSizeL .~ 0 - & ppCommitteeMaxTermLengthL .~ EpochInterval 50 - coldCommitteeActive <- KeyHashObj <$> freshKeyHash - coldCommitteeInactive <- KeyHashObj <$> freshKeyHash - startingEpoch <- getsNES nesELL - maxTermLength <- getsPParams ppCommitteeMaxTermLengthL - (dRep, _, _) <- setupSingleDRep 1_000_000_000 - (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 - let - committeeMap = - [ (coldCommitteeActive, addEpochInterval startingEpoch maxTermLength) - , (coldCommitteeInactive, addEpochInterval startingEpoch $ EpochInterval 5) - ] - initialCommittee <- getCommitteeMembers - committeeActionId <- - impAnn "Submit committee update" - . submitGovAction - $ UpdateCommittee - SNothing - initialCommittee - committeeMap - (0 %! 1) - submitYesVote_ (DRepVoter dRep) committeeActionId - submitYesVote_ (StakePoolVoter spo) committeeActionId - passNEpochs 2 - getCommitteeMembers `shouldReturn` Map.keysSet committeeMap - passNEpochs 3 - newConstitution <- arbitrary - constitutionActionId <- submitGovAction $ NewConstitution SNothing newConstitution - logRatificationChecks constitutionActionId - passNEpochs 2 - getConstitution `shouldReturn` newConstitution + describe "When min size is not 0" $ do + it "Constitution cannot be changed if active committee size is below min size" + . whenPostBootstrap + $ do + modifyPParams $ \pp -> + pp + & ppDRepVotingThresholdsL . dvtUpdateToConstitutionL .~ (0 %! 1) + & ppCommitteeMinSizeL .~ 2 + & ppCommitteeMaxTermLengthL .~ EpochInterval 50 + coldCommitteeActive <- KeyHashObj <$> freshKeyHash + coldCommitteeInactive <- KeyHashObj <$> freshKeyHash + startingEpoch <- getsNES nesELL + maxTermLength <- getsPParams ppCommitteeMaxTermLengthL + (dRep, _, _) <- setupSingleDRep 1_000_000_000 + (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 + let + committeeMap = + [ (coldCommitteeActive, addEpochInterval startingEpoch maxTermLength) + , (coldCommitteeInactive, addEpochInterval startingEpoch $ EpochInterval 5) + ] + initialCommittee <- getCommitteeMembers + committeeActionId <- + impAnn "Submit committee update" + . submitGovAction + $ UpdateCommittee + SNothing + initialCommittee + committeeMap + (0 %! 1) + submitYesVote_ (DRepVoter dRep) committeeActionId + submitYesVote_ (StakePoolVoter spo) committeeActionId + passNEpochs 2 + getCommitteeMembers `shouldReturn` Map.keysSet committeeMap + passNEpochs 3 + newConstitution <- arbitrary + constitutionActionId <- submitGovAction $ NewConstitution SNothing newConstitution + logRatificationChecks constitutionActionId + passEpoch + ccShouldBeExpired coldCommitteeInactive + passEpoch + ccShouldNotBeExpired coldCommitteeActive + getConstitution `shouldNotReturn` newConstitution + -- https://github.com/IntersectMBO/cardano-ledger/issues/5418 + -- TODO: Re-enable after issue is resolved, by removing this override + it + "Constitution cannot be changed if committee is not active because it doesn't have registered hot credentials" + $ whenPostBootstrap + $ do + modifyPParams $ \pp -> + pp + & ppCommitteeMinSizeL .~ 2 + modifyCommittee $ fmap (committeeThresholdL .~ 0 %! 1) + (drep, _, _) <- setupSingleDRep 1_000_000_000 + SJust committee <- getCommittee + committeeThreshold committee `shouldBe` 0 %! 1 + Map.size (committeeMembers committee) `shouldBe` 2 + forM_ (Map.keys $ committeeMembers committee) ccShouldNotBeExpired + oldConstitution <- getConstitution + (proposal, _) <- mkConstitutionProposal SNothing + gaiConstitution <- submitProposal proposal + submitYesVote_ (DRepVoter drep) gaiConstitution + passNEpochs 2 + getConstitution `shouldReturn` oldConstitution + it + "Constitution can be changed when an active committee doesn't vote" + $ whenPostBootstrap + $ do + modifyPParams $ \pp -> + pp + & ppCommitteeMinSizeL .~ 2 + modifyCommittee $ fmap (committeeThresholdL .~ 0 %! 1) + (drep, _, _) <- setupSingleDRep 1_000_000_000 + SJust committee <- getCommittee + committeeThreshold committee `shouldBe` 0 %! 1 + Map.size (committeeMembers committee) `shouldBe` 2 + forM_ (Map.keys $ committeeMembers committee) ccShouldNotBeExpired + (proposal, newConstitution) <- mkConstitutionProposal SNothing + gaiConstitution <- submitProposal proposal + submitYesVote_ (DRepVoter drep) gaiConstitution + mapM_ registerCommitteeHotKey (Map.keys $ committeeMembers committee) + passNEpochs 2 + getConstitution `shouldReturn` newConstitution + it + "Constitution can be changed regardless of active committee votes" + $ whenPostBootstrap + $ do + modifyPParams $ \pp -> + pp + & ppCommitteeMinSizeL .~ 2 + modifyCommittee $ fmap (committeeThresholdL .~ 0 %! 1) + (drep, _, _) <- setupSingleDRep 1_000_000_000 + SJust committee <- getCommittee + committeeThreshold committee `shouldBe` 0 %! 1 + Map.size (committeeMembers committee) `shouldBe` 2 + forM_ (Map.keys $ committeeMembers committee) ccShouldNotBeExpired + (proposal, newConstitution) <- mkConstitutionProposal SNothing + gaiConstitution <- submitProposal proposal + submitYesVote_ (DRepVoter drep) gaiConstitution + hotKeys <- mapM registerCommitteeHotKey (Map.keys $ committeeMembers committee) + forM_ hotKeys $ \c -> + oneof + [ return () + , submitYesVote_ (CommitteeVoter c) gaiConstitution + , submitVote_ VoteNo (CommitteeVoter c) gaiConstitution + ] + passNEpochs 2 + getConstitution `shouldReturn` newConstitution + describe "When min size is 0" $ do + it + "Constitution can be changed if the commitee is inactive but has some active members" + . whenPostBootstrap + $ do + modifyPParams $ \pp -> + pp + & ppDRepVotingThresholdsL . dvtUpdateToConstitutionL .~ (0 %! 1) + & ppCommitteeMinSizeL .~ 0 + & ppCommitteeMaxTermLengthL .~ EpochInterval 50 + coldCommitteeActive <- KeyHashObj <$> freshKeyHash + coldCommitteeInactive <- KeyHashObj <$> freshKeyHash + startingEpoch <- getsNES nesELL + maxTermLength <- getsPParams ppCommitteeMaxTermLengthL + (dRep, _, _) <- setupSingleDRep 1_000_000_000 + (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 + let + committeeMap = + [ (coldCommitteeActive, addEpochInterval startingEpoch maxTermLength) + , (coldCommitteeInactive, addEpochInterval startingEpoch $ EpochInterval 5) + ] + initialCommittee <- getCommitteeMembers + committeeActionId <- + impAnn "Submit committee update" + . submitGovAction + $ UpdateCommittee + SNothing + initialCommittee + committeeMap + (0 %! 1) + submitYesVote_ (DRepVoter dRep) committeeActionId + submitYesVote_ (StakePoolVoter spo) committeeActionId + passNEpochs 2 + getCommitteeMembers `shouldReturn` Map.keysSet committeeMap + passNEpochs 3 + newConstitution <- arbitrary + constitutionActionId <- submitGovAction $ NewConstitution SNothing newConstitution + logRatificationChecks constitutionActionId + passNEpochs 2 + getConstitution `shouldReturn` newConstitution + it + "Constitution can be changed if there are no active members" + . whenPostBootstrap + $ do + modifyPParams $ \pp -> + pp + & ppCommitteeMinSizeL .~ 0 + & ppDRepVotingThresholdsL . dvtUpdateToConstitutionL .~ (0 %! 1) + modifyCommittee $ fmap (committeeThresholdL .~ 0 %! 1) + SJust committee <- getCommittee + forM_ (Map.keys $ committeeMembers committee) ccShouldNotBeExpired + newConstitution <- arbitrary + constitutionActionId <- submitGovAction $ NewConstitution SNothing newConstitution + logRatificationChecks constitutionActionId + passNEpochs 2 + getConstitution `shouldReturn` newConstitution describe "When CC threshold is not 0" $ do it "Constitution cannot be changed if min committee size is 0" . whenPostBootstrap From 7c45fbd9276842ddf9c4b3da880613304667694b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Tom=C3=A9=20Corti=C3=B1as?= Date: Mon, 8 Dec 2025 13:56:46 +0100 Subject: [PATCH 5/6] Disable failing test in conformance --- .../impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs index e5a695b4dcb..3f10b0a09e7 100644 --- a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs +++ b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs @@ -284,7 +284,7 @@ spoAndCCVotingSpec = do getConstitution `shouldNotReturn` newConstitution -- https://github.com/IntersectMBO/cardano-ledger/issues/5418 -- TODO: Re-enable after issue is resolved, by removing this override - it + disableInConformanceIt "Constitution cannot be changed if committee is not active because it doesn't have registered hot credentials" $ whenPostBootstrap $ do From 886ac6273961e715abf0d89a7a8781fcb91946f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Tom=C3=A9=20Corti=C3=B1as?= Date: Mon, 8 Dec 2025 15:07:36 +0100 Subject: [PATCH 6/6] Add function to setup a mixed committee --- .../Cardano/Ledger/Conway/Imp/RatifySpec.hs | 109 +----------------- .../Test/Cardano/Ledger/Conway/ImpTest.hs | 42 +++++++ 2 files changed, 48 insertions(+), 103 deletions(-) diff --git a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs index 3f10b0a09e7..21b62f1538e 100644 --- a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs +++ b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/RatifySpec.hs @@ -151,36 +151,15 @@ spoAndCCVotingSpec = do pp & ppCommitteeMinSizeL .~ 2 & ppCommitteeMaxTermLengthL .~ EpochInterval 50 - coldCommitteeActive <- KeyHashObj <$> freshKeyHash - coldCommitteeInactive <- KeyHashObj <$> freshKeyHash - startingEpoch <- getsNES nesELL - maxTermLength <- getsPParams ppCommitteeMaxTermLengthL - (drep, _, _) <- setupSingleDRep 1_000_000_000 - (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 - let - committeeMap = - [ (coldCommitteeActive, addEpochInterval startingEpoch maxTermLength) - , (coldCommitteeInactive, addEpochInterval startingEpoch $ EpochInterval 5) - ] - initialCommittee <- getCommitteeMembers - committeeActionId <- - impAnn "Submit committee update" - . submitGovAction - $ UpdateCommittee - SNothing - initialCommittee - committeeMap - (1 %! 1) - submitYesVote_ (DRepVoter drep) committeeActionId - submitYesVote_ (StakePoolVoter spo) committeeActionId - passNEpochs 5 - getCommitteeMembers `shouldReturn` Map.keysSet committeeMap + committeeActionId <- setupActiveInactiveCCMembers 1 1 (1 %! 1) committeeProposal <- elements [ NoConfidence (SJust (GovPurposeId committeeActionId)) , UpdateCommittee (SJust (GovPurposeId committeeActionId)) Set.empty [] (0 %! 1) ] committeeActionId2 <- submitGovAction committeeProposal + (drep, _, _) <- setupSingleDRep 2_000_000_000 + (spo, _, _) <- setupPoolWithStake $ Coin 2_000_000_000 submitYesVote_ (DRepVoter drep) committeeActionId2 submitYesVote_ (StakePoolVoter spo) committeeActionId2 passNEpochs 2 @@ -249,38 +228,10 @@ spoAndCCVotingSpec = do & ppDRepVotingThresholdsL . dvtUpdateToConstitutionL .~ (0 %! 1) & ppCommitteeMinSizeL .~ 2 & ppCommitteeMaxTermLengthL .~ EpochInterval 50 - coldCommitteeActive <- KeyHashObj <$> freshKeyHash - coldCommitteeInactive <- KeyHashObj <$> freshKeyHash - startingEpoch <- getsNES nesELL - maxTermLength <- getsPParams ppCommitteeMaxTermLengthL - (dRep, _, _) <- setupSingleDRep 1_000_000_000 - (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 - let - committeeMap = - [ (coldCommitteeActive, addEpochInterval startingEpoch maxTermLength) - , (coldCommitteeInactive, addEpochInterval startingEpoch $ EpochInterval 5) - ] - initialCommittee <- getCommitteeMembers - committeeActionId <- - impAnn "Submit committee update" - . submitGovAction - $ UpdateCommittee - SNothing - initialCommittee - committeeMap - (0 %! 1) - submitYesVote_ (DRepVoter dRep) committeeActionId - submitYesVote_ (StakePoolVoter spo) committeeActionId - passNEpochs 2 - getCommitteeMembers `shouldReturn` Map.keysSet committeeMap - passNEpochs 3 + void $ setupActiveInactiveCCMembers 1 1 (0 %! 1) newConstitution <- arbitrary constitutionActionId <- submitGovAction $ NewConstitution SNothing newConstitution logRatificationChecks constitutionActionId - passEpoch - ccShouldBeExpired coldCommitteeInactive - passEpoch - ccShouldNotBeExpired coldCommitteeActive getConstitution `shouldNotReturn` newConstitution -- https://github.com/IntersectMBO/cardano-ledger/issues/5418 -- TODO: Re-enable after issue is resolved, by removing this override @@ -357,31 +308,7 @@ spoAndCCVotingSpec = do & ppDRepVotingThresholdsL . dvtUpdateToConstitutionL .~ (0 %! 1) & ppCommitteeMinSizeL .~ 0 & ppCommitteeMaxTermLengthL .~ EpochInterval 50 - coldCommitteeActive <- KeyHashObj <$> freshKeyHash - coldCommitteeInactive <- KeyHashObj <$> freshKeyHash - startingEpoch <- getsNES nesELL - maxTermLength <- getsPParams ppCommitteeMaxTermLengthL - (dRep, _, _) <- setupSingleDRep 1_000_000_000 - (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 - let - committeeMap = - [ (coldCommitteeActive, addEpochInterval startingEpoch maxTermLength) - , (coldCommitteeInactive, addEpochInterval startingEpoch $ EpochInterval 5) - ] - initialCommittee <- getCommitteeMembers - committeeActionId <- - impAnn "Submit committee update" - . submitGovAction - $ UpdateCommittee - SNothing - initialCommittee - committeeMap - (0 %! 1) - submitYesVote_ (DRepVoter dRep) committeeActionId - submitYesVote_ (StakePoolVoter spo) committeeActionId - passNEpochs 2 - getCommitteeMembers `shouldReturn` Map.keysSet committeeMap - passNEpochs 3 + void $ setupActiveInactiveCCMembers 1 1 (0 %! 1) newConstitution <- arbitrary constitutionActionId <- submitGovAction $ NewConstitution SNothing newConstitution logRatificationChecks constitutionActionId @@ -412,31 +339,7 @@ spoAndCCVotingSpec = do & ppDRepVotingThresholdsL . dvtUpdateToConstitutionL .~ (0 %! 1) & ppCommitteeMinSizeL .~ 0 & ppCommitteeMaxTermLengthL .~ EpochInterval 50 - coldCommitteeActive <- KeyHashObj <$> freshKeyHash - coldCommitteeInactive <- KeyHashObj <$> freshKeyHash - startingEpoch <- getsNES nesELL - maxTermLength <- getsPParams ppCommitteeMaxTermLengthL - (dRep, _, _) <- setupSingleDRep 1_000_000_000 - (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 - let - committeeMap = - [ (coldCommitteeActive, addEpochInterval startingEpoch maxTermLength) - , (coldCommitteeInactive, addEpochInterval startingEpoch $ EpochInterval 5) - ] - initialCommittee <- getCommitteeMembers - committeeActionId <- - impAnn "Submit committee update" - . submitGovAction - $ UpdateCommittee - SNothing - initialCommittee - committeeMap - (1 %! 1) - submitYesVote_ (DRepVoter dRep) committeeActionId - submitYesVote_ (StakePoolVoter spo) committeeActionId - passNEpochs 2 - getCommitteeMembers `shouldReturn` Map.keysSet committeeMap - passNEpochs 3 + void $ setupActiveInactiveCCMembers 1 1 (1 %! 1) newConstitution <- arbitrary constitutionActionId <- submitGovAction $ NewConstitution SNothing newConstitution logRatificationChecks constitutionActionId diff --git a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/ImpTest.hs b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/ImpTest.hs index b8d1a2d6766..147124e3bf7 100644 --- a/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/ImpTest.hs +++ b/eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/ImpTest.hs @@ -80,6 +80,7 @@ module Test.Cardano.Ledger.Conway.ImpTest ( logCurPParams, submitCommitteeElection, electBasicCommittee, + setupActiveInactiveCCMembers, proposalsShowDebug, getGovPolicy, submitFailingGovAction, @@ -1377,6 +1378,47 @@ electBasicCommittee = do hotCommitteeC <- registerCommitteeHotKey coldCommitteeC pure (drep, hotCommitteeC, GovPurposeId gaidCommitteeProp) +setupActiveInactiveCCMembers :: + forall era. + ( HasCallStack + , ConwayEraImp era + ) => + -- | Number of active committee members + Int -> + -- | Number of inactive committee members + Int -> + -- | Threshold + UnitInterval -> + ImpTestM era GovActionId +setupActiveInactiveCCMembers nActive nInactive threshold = do + coldCommitteeActive <- replicateM nActive (KeyHashObj <$> freshKeyHash) + coldCommitteeInactive <- replicateM nInactive (KeyHashObj <$> freshKeyHash) + startingEpoch <- getsNES nesELL + maxTermLength <- getsPParams ppCommitteeMaxTermLengthL + (drep, _, _) <- setupSingleDRep 1_000_000_000 + (spo, _, _) <- setupPoolWithStake $ Coin 1_000_000_000 + let + committeeMap = + Map.fromList $ + map (,addEpochInterval startingEpoch maxTermLength) coldCommitteeActive + ++ map (,addEpochInterval startingEpoch $ EpochInterval 5) coldCommitteeInactive + initialCommittee <- getCommitteeMembers + committeeActionId <- + impAnn "Submit committee update" + . submitGovAction + $ UpdateCommittee + SNothing + initialCommittee + committeeMap + threshold + submitYesVote_ (DRepVoter drep) committeeActionId + submitYesVote_ (StakePoolVoter spo) committeeActionId + passNEpochs 6 + getCommitteeMembers `shouldReturn` Map.keysSet committeeMap + forM_ coldCommitteeActive ccShouldNotBeExpired + forM_ coldCommitteeInactive ccShouldBeExpired + return committeeActionId + logCurPParams :: ( EraGov era , ToExpr (PParamsHKD Identity era)