Add conway query gov-action-status command - #1401
Conversation
3f20389 to
ff5173c
Compare
Implements `cardano-cli conway query gov-action-status --gov-action-id <id>` which queries a live node and displays ratification stage, expiry epoch, DRep vote ratio (stake-weighted, mirroring ledger logic), CC vote ratio (with resignation accounting via JSON introspection), and SPO vote counts.
ff5173c to
7cc634c
Compare
carbolymer
left a comment
There was a problem hiding this comment.
I suppose the feature could be useful 👍🏻 But the code needs more work.
| | QueryBackwardCompatibleError | ||
| !Text | ||
| !SomeException | ||
| | QueryCmdGovActionNotFound !L.GovActionId |
There was a problem hiding this comment.
This is not used anywhere.
| then putStrLn "Ratify stage: RATIFIED (pending enactment at epoch end)" | ||
| else if isExpired | ||
| then putStrLn $ "Ratify stage: EXPIRED (expired " <> show (curEpochN - expiresAfterN) <> " epoch(s) ago)" | ||
| else putStrLn "Ratify stage: Active (not yet ratified)" |
There was a problem hiding this comment.
rendering of the output should be separate from the computation.
| Nothing -> do | ||
| liftIO $ putStrLn $ "Governance action not found in current proposals: " <> bech32Id | ||
| liftIO $ putStrLn "(Action may have been enacted or expired and removed from the ledger state.)" | ||
| exitSuccess |
There was a problem hiding this comment.
- well this is definitely not a success. a different error code should be used.
- Error messages go to stderr
| showAda (L.Coin lovelace) = | ||
| let ada = lovelace `div` 1_000_000 | ||
| frac = lovelace `mod` 1_000_000 | ||
| in show ada <> if frac == 0 then "" else "." <> take 2 (show frac) |
There was a problem hiding this comment.
First of all this is unaware of the locale, so it assumes one arbitrary formatting. Secondly, this should be implemented as a newtype wrapper + Pretty instance e.g. newtype DecimalFormat foo
| whole = pct `div` 100 | ||
| frac = pct `mod` 100 | ||
| pad2 n = if n < 10 then "0" <> show n else show n | ||
| in show whole <> "." <> pad2 frac <> "%" |
| else if isExpired | ||
| then putStrLn $ "Ratify stage: EXPIRED (expired " <> show (curEpochN - expiresAfterN) <> " epoch(s) ago)" | ||
| else putStrLn "Ratify stage: Active (not yet ratified)" | ||
| putStrLn "" |
There was a problem hiding this comment.
It should support different output formats e.g. json / yaml similarly to other commands.
carbolymer
left a comment
There was a problem hiding this comment.
Three correctness observations on the vote-ratio logic, verified against the ledger's ratification code (Cardano.Ledger.Conway.Rules.Ratify / Governance.Internal). The DRep AlwaysAbstain/AlwaysNoConfidence/expiry handling in calcDRepRatio itself matches the ledger's dRepAcceptedRatio - the issues below are in the threshold selection and the CC denominator.
| spoRequired :: String -> Bool | ||
| spoRequired = \case | ||
| "NoConfidence" -> True | ||
| "HardForkInitiation" -> True | ||
| "UpdateCommittee" -> True | ||
| _ -> False | ||
|
|
||
| drepThresholdFor :: String -> L.DRepVotingThresholds -> Maybe Double | ||
| drepThresholdFor tag L.DRepVotingThresholds{..} = fmap (fromRational . L.unboundRational) $ case tag of | ||
| "TreasuryWithdrawals" -> Just dvtTreasuryWithdrawal | ||
| "HardForkInitiation" -> Just dvtHardForkInitiation | ||
| "NoConfidence" -> Just dvtMotionNoConfidence | ||
| "UpdateCommittee" -> Just dvtCommitteeNormal | ||
| "NewConstitution" -> Just dvtUpdateToConstitution | ||
| "ParameterChange" -> Just dvtPPGovGroup | ||
| "InfoAction" -> Nothing | ||
| _ -> Nothing |
There was a problem hiding this comment.
ParameterChange thresholds diverge from ledger ratification logic.
drepThresholdFor "ParameterChange" = Just dvtPPGovGroup hardcodes a single group, but the ledger (votingDRepThresholdInternal in Cardano.Ledger.Conway.Governance.Internal) computes the threshold as the maximum over all PParam groups the proposal actually touches (Network / Economic / Technical / Gov). A proposal changing, say, an economic and a technical parameter will show the wrong threshold here.
Similarly, spoRequired "ParameterChange" = False unconditionally, but the ledger requires SPO votes (at pvtPPSecurityGroup) whenever the proposal touches a SecurityGroup parameter (fee params, max block/tx sizes, coins-per-UTxO-byte, ...). As written, the command omits the SPO section exactly for the security-relevant parameter changes where it matters most.
Both need to inspect the ParameterChange payload's PParamsUpdate and derive the touched groups, as the ledger does, rather than dispatching on the action tag alone.
| activeNR = Map.filter (\v -> isActive v && not (isResigned v)) memberJsons | ||
| resignedCount = Map.size $ Map.filter (\v -> isActive v && isResigned v) memberJsons | ||
| expiredCount = Map.size $ Map.filter (\v -> getStatus v == Just "Expired") memberJsons | ||
| totalInState = Map.size csCommittee | ||
| activeCount = Map.size activeNR | ||
| yesVotes = Map.size $ Map.filter (== L.VoteYes) gasCommitteeVotes | ||
| noVotes = Map.size $ Map.filter (== L.VoteNo) gasCommitteeVotes | ||
| abstainVotes = Map.size $ Map.filter (== L.Abstain) gasCommitteeVotes | ||
| ccRatio = if activeCount == 0 then 0.0 else fromIntegral yesVotes / fromIntegral activeCount :: Double | ||
| ccThreshD = maybe 0.0 (fromRational . L.unboundRational) csThreshold | ||
| ccNeeded = max 0 (ceiling (ccThreshD * fromIntegral activeCount) - yesVotes) :: Int |
There was a problem hiding this comment.
CC ratio denominator disagrees with the ledger: explicit Abstain votes must be excluded.
activeCount counts every active, non-resigned member regardless of how they voted, but the ledger's committeeAcceptedRatio (Cardano.Ledger.Conway.Rules.Ratify) removes members who explicitly voted Abstain from the denominator as well (same treatment as resigned/expired/unregistered).
With the current code the reported ratio is systematically lower than the ledger's whenever anyone abstains, so the command can print NOT PASSING for an action the ledger will in fact ratify. Example: 7 active members, threshold 66.67%, 4 yes, 1 abstain - ledger ratio 4/6 = 66.7% (passing), this code 4/7 = 57.1% (reported not passing).
| spoThresholdFor tag L.PoolVotingThresholds{..} = fmap (fromRational . L.unboundRational) $ case tag of | ||
| "NoConfidence" -> Just pvtMotionNoConfidence | ||
| "HardForkInitiation" -> Just pvtHardForkInitiation | ||
| "UpdateCommittee" -> Just pvtCommitteeNormal |
There was a problem hiding this comment.
UpdateCommittee always uses the *Normal thresholds; the ledger switches to *NoConfidence when no committee is seated.
dvtCommitteeNormal (above) and pvtCommitteeNormal (here) are only correct while a committee exists. After a successful NoConfidence action (state of no confidence), the ledger uses dvtCommitteeNoConfidence / pvtCommitteeNoConfidence instead. The committee presence is available from the gov/ratify state already being queried, so this can be dispatched on rather than hardcoded.
Implements
cardano-cli conway query gov-action-status --gov-action-id <id>which queries a live node and displays ratification stage, expiry epoch, DRep vote ratio (stake-weighted, mirroring ledger logic), CC vote ratio (with resignation accounting via JSON introspection), and SPO vote counts.Example output:
Context
Additional context for the PR goes here. If the PR fixes a particular issue please provide a link to the issue.
How to trust this PR
Highlight important bits of the PR that will make the review faster. If there are commands the reviewer can run to observe the new behavior, describe them.
Checklist
.changes/