Skip to content

Fix flaky ProposeNewConstitution test with per-test genesis override and two-round voting #6593

@carbolymer

Description

@carbolymer

Summary

The hprop_ledger_events_propose_new_constitution test is flaky because CLI query proposals reads from the pulsing snapshot, which lags the live ledger state by one epoch.
Combined with a 0% dvtUpdateToConstitution threshold, any vote triggers immediate ratification, creating a tiny window where vote assertions fail.

Root cause

Two facts interact to produce the race:

  1. Pulsing snapshot lag - query proposals reads from the DRep pulsing snapshot, which is set once at the epoch boundary and not modified during pulsing. New proposals and votes only appear in this snapshot after the next epoch transition.
  2. Zero ratification threshold - dvtUpdateToConstitution defaults to 0 %! 2 in Testnet.Defaults, meaning the proposal is ratified as soon as pulsing completes, regardless of votes.

On master, the test submits all 9 votes in a single transaction, then checks for ratification via foldEpochState, and only afterwards queries CLI proposals.
By this point the proposal may already have been ratified and removed from gov-state, causing length proposalsArray === 1 to fail on an empty array.
The vote count assertions (length proposalsDRepVotes === numVotes) similarly fail when the snapshot has not yet refreshed or the proposal is gone.

Current workaround (PR #6575)

PR #6575 reworked the test ordering - moving the CLI proposal-structure query before voting (race-free) and adding unless guards around the post-vote CLI assertions.
Reviewers (@Jimbo4350, @palas) correctly flagged the unless guards as weakening the test - they silently skip the vote-count checks whenever the snapshot is stale or the proposal is already ratified.

Proposed fix

A. Override Conway genesis per-test

Follow the pattern from the NoConfidence test: split createAndRunTestnet into createTestnetEnv + H.rewriteJsonFile + cardanoTestnet.
Override two parameters:

  • dvtUpdateToConstitution = 51 %! 100 - a 51% threshold, ensuring a minority of yes-votes cannot trigger ratification.
  • ucppGovActionLifetime = EpochInterval 4 - 80s lifetime at genesisEpochLength = 200, giving ample time for snapshot refresh. The master default of EpochInterval 1 (20s) is too tight.

No committee setup is needed.
With ucppCommitteeMinSize = 0 (the default), committeeAccepted returns True vacuously.

B. Stage votes in two rounds

  • Round 1: Submit 3 yes votes out of 9 DReps = 33.3% of active stake. This is below the 51% threshold, so the proposal stays in gov-state indefinitely.
  • Round 2: Submit +1 yes, +3 no, +2 abstain. The new ratio is 4 yes out of 7 participating (abstain DReps are excluded from the denominator) = 57.1%, which exceeds 51% and triggers ratification.

C. Deterministic assertions - no unless guards

  • After round 1: Use retryUntilJustM until the pulsing snapshot shows the proposal with exactly 3 DRep votes. Assert vote counts unconditionally. The proposal cannot be ratified at 33.3% < 51%, so it will always be present.
  • After round 2: Use retryUntilJustM until the snapshot shows the full vote counts (4 yes, 3 no, 2 abstain). Assert unconditionally, then verify ratification via the existing foldEpochState check.

D. Technical notes

  • Vote ratio is stake-weighted: non-voting DReps count in the denominator, abstain DReps are excluded.
  • The pulsing snapshot's proposals map is set once at the epoch boundary and not modified mid-epoch - only the vote tallies are updated as pulsing progresses.
  • The retryUntilJustM guard condition must match the expected postcondition exactly (proposal present AND vote count matches), otherwise the retry exits early and the assertion fails.

Acceptance criteria

  • The test runs without unless guards or other conditional skips around assertions.
  • CLI query proposals vote counts are checked unconditionally in both rounds.
  • The test passes reliably in CI (no flaky failures over 10+ runs).
  • No changes to Testnet.Defaults - all overrides are local to this test.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions