Skip to content

Add conway query gov-action-status command - #1401

Open
disassembler wants to merge 1 commit into
masterfrom
sl/vote-check
Open

Add conway query gov-action-status command#1401
disassembler wants to merge 1 commit into
masterfrom
sl/vote-check

Conversation

@disassembler

@disassembler disassembler commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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:

Governance action: gov_action1mr0qdz2jmagvsch6r08fhqvq6vu8jakt4c8m9s7ea7z0p740vntqqqqpqyz47
Action type:       TreasuryWithdrawals
Proposed in:       Epoch 639
Expires after:     Epoch 645
Current epoch:     646
Ratify stage:      RATIFIED (pending enactment at epoch end)

=== DRep Votes ===
  Threshold: 67.00%
  YES:       2658457575.26 ADA
  Total:     3449660735.39 ADA (excl. abstain + inactive)
  Ratio:     77.06%
  Status:    PASSING

=== CC Votes ===
  Threshold:      66.67%
  Total in state: 8 (1 resigned, 0 expired)
  Active:         7 (excl. resigned)
  YES votes:      6
  NO votes:       0
  Abstain votes:  0
  Ratio:          85.71%
  Status:         PASSING

=== SPO Votes ===
  Not applicable for TreasuryWithdrawals

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

  • Commit sequence broadly makes sense and commits have useful messages
  • New tests are added if needed and existing tests are updated. See Running tests for more details
  • Self-reviewed the diff
  • Changelog fragment added in .changes/

Comment thread cardano-cli/src/Cardano/CLI/EraBased/Query/Run.hs Fixed
@disassembler
disassembler force-pushed the sl/vote-check branch 2 times, most recently from 3f20389 to ff5173c Compare July 29, 2026 01:11
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.

@carbolymer carbolymer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the feature could be useful 👍🏻 But the code needs more work.

| QueryBackwardCompatibleError
!Text
!SomeException
| QueryCmdGovActionNotFound !L.GovActionId

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. well this is definitely not a success. a different error code should be used.
  2. 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 <> "%"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

else if isExpired
then putStrLn $ "Ratify stage: EXPIRED (expired " <> show (curEpochN - expiresAfterN) <> " epoch(s) ago)"
else putStrLn "Ratify stage: Active (not yet ratified)"
putStrLn ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should support different output formats e.g. json / yaml similarly to other commands.

@carbolymer carbolymer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1779 to +1795
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1724 to +1734
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants