Make stake calculation consistent in dijkstra by moving SNAP to the end of the EPOCH - #5986
Make stake calculation consistent in dijkstra by moving SNAP to the end of the EPOCH#5986aniketd wants to merge 5 commits into
dijkstra by moving SNAP to the end of the EPOCH#5986Conversation
The test demonstrates the inconsistency in calculations by failing the correct expected condition. This test is expected to pass once the EPOCH rule for dijkstra is implemented with consistent stake calculations.
There was a problem hiding this comment.
Pull request overview
This PR addresses inconsistent voting stake calculation (issue #5014) by changing Dijkstra’s epoch-boundary processing so the SNAP stake snapshot is taken after refunds/withdrawals are applied, aligning SPO voting stake with DRep voting stake. It also adds regression tests (Conway) and corresponding “fixed behavior” tests (Dijkstra), plus the necessary exports and dependency bumps to share logic.
Changes:
- Implement a Dijkstra-specific
EPOCHSTS rule that runsSNAPat the end of the epoch transition (after POOLREAP + governance refunds/withdrawals). - Add Dijkstra imp tests asserting SPO voting stake matches DRep voting stake in scenarios involving refunds/withdrawals; add Conway imp tests reproducing #5014.
- Export helper functions from
cardano-ledger-conwayneeded by Dijkstra, and adjust bounds/changelogs accordingly.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| eras/dijkstra/impl/testlib/Test/Cardano/Ledger/Dijkstra/Imp/SnapSpec.hs | New Dijkstra imp tests asserting SPO and DRep voting stake are consistent after refunds/withdrawals. |
| eras/dijkstra/impl/testlib/Test/Cardano/Ledger/Dijkstra/Imp.hs | Wires the new SnapSpec into the Dijkstra imp test suite. |
| eras/dijkstra/impl/src/Cardano/Ledger/Dijkstra/Rules/Epoch.hs | New Dijkstra EPOCH transition implementing “SNAP-at-end” ordering. |
| eras/dijkstra/impl/src/Cardano/Ledger/Dijkstra/Rules.hs | Ensures the new Dijkstra EPOCH rule module is linked/loaded. |
| eras/dijkstra/impl/src/Cardano/Ledger/Dijkstra/Era.hs | Introduces a Dijkstra-specific EPOCH rule type and points EraRule "EPOCH" to it. |
| eras/dijkstra/impl/CHANGELOG.md | Documents the new Dijkstra EPOCH behavior and rationale (#5014). |
| eras/dijkstra/impl/cardano-ledger-dijkstra.cabal | Adds the new module/test module and bumps cardano-ledger-conway lower bound to include needed exports. |
| eras/conway/impl/testlib/Test/Cardano/Ledger/Conway/Imp/SnapSpec.hs | Adds tests reproducing the lag described in #5014 (baseline behavior). |
| eras/conway/impl/src/Cardano/Ledger/Conway/Rules/Epoch.hs | Exports helper functions used by Dijkstra’s EPOCH implementation. |
| eras/conway/impl/CHANGELOG.md | Notes newly exported functions in cardano-ledger-conway. |
Suppressed comments (1)
eras/dijkstra/impl/testlib/Test/Cardano/Ledger/Dijkstra/Imp/SnapSpec.hs:118
- In this combined scenario, the proposal id is discarded (
_ <- submitProposal ...), so the test can’t assert that the proposal actually expired (and its deposit was refunded), and similarly the withdrawal action’s enactment/removal isn’t asserted. Because the core stake assertion is just equality, the test could pass even if the intended refunds/withdrawal never took effect. Capturing the ids and assertingexpectMissingGovActionIdafterpassNEpochs 2makes the test validate the setup.
submitTx_ $ mkBasicTx mkBasicTxBody & bodyTxL . treasuryDonationTxBodyL .~ Coin 1_000_000
_ <- submitProposal =<< mkProposalWithAccountAddress InfoAction returnAddr
poolToRetire <- freshKeyHash
registerPoolWithAccountAddress poolToRetire returnAddr
passEpoch
curEpochNo <- getsNES nesELL
submitTxAnn_ "Retire the temporary pool" $
mkBasicTx mkBasicTxBody
& bodyTxL . certsTxBodyL
.~ SSeq.singleton (RetirePoolTxCert poolToRetire (addEpochInterval curEpochNo (EpochInterval 2)))
modifyPParams $ \pp -> pp & ppGovActionLifetimeL .~ EpochInterval 30
govActionId <- submitTreasuryWithdrawals [(returnAddr, Coin 1_000_000)]
submitYesVote_ (DRepVoter drep) govActionId
submitYesVoteCCs_ committeeCs govActionId
passNEpochs 2
drepVotingStake <- getDRepVotingStake drep
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| govActionId <- submitTreasuryWithdrawals [(returnAddr, Coin 1_000_000)] | ||
| submitYesVote_ (DRepVoter drep) govActionId | ||
| submitYesVoteCCs_ committeeCs govActionId | ||
| passNEpochs 2 | ||
| drepVotingStake <- getDRepVotingStake drep | ||
| spoVotingStake <- getSpoVotingStake pool |
teodanciu
left a comment
There was a problem hiding this comment.
After a most educative incursion into stake calculation, I finally understand what this PR is solving.
It looks to me as if it's following what was discussed, and it's testing the fix.
But what worries me is that it's testing only the fix.
I believe this change will have consequences for the rewards calculation (since the post-refund balances will now be snapshotted), and also on pool removal (because POOLREAP runs before SNAP) - so the pool will disappear from mark immediately I believe?
Also, I'm not sure how (if at all, likely ont) TICKF is affected.
Conformance tests are also failing - do we need the spec to be updated as well?
I suppose we can write more tests in future PRs, I find it difficult to understand the consequences without seeing the differences in the consumers of the snapshots, so I I will leave it to someone more knowledgeable to approve!
| & lsCertStateL .~ certState2 | ||
| & lsUTxOStateL .~ utxoState2 | ||
| snapshots1 <- | ||
| trans @(EraRule "SNAP" era) $ TRC (Shelley.SnapEnv ledgerState1 curPParams, snapshots0, ()) |
There was a problem hiding this comment.
Why before and not after the HARDFORK rule? If HARDFORK modifies the state, then the pulser will get the accounts from the post-HARDFORK state and the stakePoolDistr from the pre-HARDFORK snapshot, so kinda similar problem to what we have today no?
Is there a specific reason to have it here that I'm missing?
| & lsCertStateL .~ certState2 | ||
| & lsUTxOStateL .~ utxoState2 | ||
| snapshots1 <- | ||
| trans @(EraRule "SNAP" era) $ TRC (Shelley.SnapEnv ledgerState1 curPParams, snapshots0, ()) |
There was a problem hiding this comment.
Aren't curPParams stale here? The govstate has been replaced at this point, no?
| -- Conway's reproduction (Conway.Imp.SnapSpec.conwayOnlySpec) asserts the SPO trails the DRep by | ||
| -- the refunded deposit: (drepVotingStake <-> spoVotingStake) `shouldBe` govActionDeposit. | ||
| -- The Dijkstra SNAP fix removes that one-epoch lag, so here the two stakes are equal instead. |
There was a problem hiding this comment.
I wonder if instead of duplicating the conditions of the test in Conway, we could have a protocol-version switch in conway (that would be called in Dijkstra too), that clearly shows the difference in behavior?
Not so much to avoid duplication, but because I think it would be a nice documentation to see the change between protocol versions.
| -- the refunded deposit: (drepVotingStake <-> spoVotingStake) `shouldBe` govActionDeposit. | ||
| -- The Dijkstra SNAP fix removes that one-epoch lag, so here the two stakes are equal instead. | ||
| impAnn "SPO voting stake reflects the refund immediately, like the DRep stake" $ | ||
| spoVotingStake `shouldBe` drepVotingStake |
There was a problem hiding this comment.
Wonder if we can make this check stronger, because the property I believe is: spoVotingStake should be leader-election stake + activeProposalDeposits delegated to that pool.
I think it would be more difficult for this to pass for the wrong reason, than checking against the drepVotingStake.
| getDRepVotingStake drep = do | ||
| drepDistr <- getsNES $ nesEsL . epochStateDRepPulsingStateL . psDRepDistrG | ||
| pure $ fromCompact $ drepDistr Map.! DRepCredential drep | ||
| it "SPO voting stake equals DRep voting stake after a refunded deposit" $ do |
There was a problem hiding this comment.
I haven't tried it, but it seems to me that this kind of checks would lend themselves to some property tests instead or in addition to crafting these scenarios (the property being: "for every account delegated to both a pool and a DRep, its contribution to the two distributions agrees").
I don't know how feasible it is atm, since we can't generate arbitrary correct ledger states.
Another thing we could try - I don't know if it's feasible - but to have some sort of check-hook in Dijkstra imp test to check the invariant, so we get to test it whenever we run imp tests?
Description
Addresses a part of #5014: add more tests and implement the switch of moving
SNAPto the end of theEPOCHindijkstra.Best way to analyse the changes is to
difftheEPOCHrule betweenconwayanddijkstra.Checklist
CHANGELOG.mdfiles updated for packages with externally visible changes.NOTE: New section is never added with the code changes. (See RELEASING.md).
.cabalandCHANGELOG.mdfiles when necessary, according to theversioning process.
.cabalfiles updated when necessary.NOTE: If bounds change in a cabal file, that package itself must have a version increase. (See RELEASING.md).
scripts/fourmolize.sh).scripts/cabal-format.sh).cleret cabal run generate-cddl)hie.yamlupdated (usescripts/gen-hie.sh).