Skip to content

test: reach the governance vote signature path with a chain-backed fixture - #7528

Open
PastaPastaPasta wants to merge 3 commits into
dashpay:developfrom
PastaPastaPasta:test/governance-vote-signature-coverage
Open

test: reach the governance vote signature path with a chain-backed fixture#7528
PastaPastaPasta wants to merge 3 commits into
dashpay:developfrom
PastaPastaPasta:test/governance-vote-signature-coverage

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Aug 3, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

No governance unit test ever reaches CGovernanceVote::CheckSignature.

The existing governance fixtures (e.g. governance_inv_tests.cpp) are built on
TestingSetup with no chain, so m_dmnman.GetListAtChainTip() returns an empty
masternode list and every CGovernanceVote::IsValid() call short-circuits at
GetMNByCollateral() before any signature is verified. Consequently:

  • nothing proves a correctly signed vote from a registered masternode is
    accepted at all;
  • nothing proves a forged signature is rejected;
  • nothing proves the orphan-vote path (a vote that arrives before its parent
    object) actually recovers once the object shows up.

That last one is the dangerous gap: a change that made vote acceptance stricter
in the wrong place would silently break governance orphan-vote recovery
entirely, and the unit tests would stay green. feature_governance.py does not
deterministically produce a vote that arrives before its parent object, so it is
not reliable coverage for that path either.

What was done?

Added src/test/governance_vote_processing_tests.cpp: a chain-backed fixture
plus three test cases. No production code is touched.

The fixture mines a regtest chain, activates DIP3, registers one masternode with
a real ProRegTx and keeps its voting (ECDSA) and operator (BLS) keys, so votes
can be signed for real and the signature checks are genuinely exercised. It also
enables the tx index, which CGovernanceObject::IsCollateralValid() reads the
proposal fee transaction from — without it no proposal can ever be accepted.

Test cases:

  • orphan_vote_is_cached_and_applied_when_parent_arrives — a properly signed
    vote whose parent object is unknown is cached as an orphan, reported back as
    the object hash to request from the peer, and raises no peer penalty. The
    proposal fee is then burned to an OP_RETURN committing to the object hash and
    buried under the required confirmations, the proposal is accepted for real, and
    the orphan vote is replayed onto it (yes-count 1, orphan list empty). A peer
    re-sending the vote afterwards is not punished.
  • unsigned_and_unknown_masternode_votes_are_rejected — a forged signature, a
    vote from an outpoint belonging to no masternode, and a vote dated too far in
    the future are each rejected with a permanent error and a penalty of 20; a
    repeat of a vote already known to be invalid is still penalised; the object's
    vote count stays at zero.
  • proposal_funding_votes_require_the_voting_key — an operator-key-signed
    funding vote on a proposal is rejected (valid BLS signature, wrong key for that
    signal), a voting-key-signed funding vote is accepted and counted, an
    operator-key-signed VALID vote is accepted and counted, and a duplicate of an
    accepted vote is dropped without a penalty.

The chain/ProRegTx plumbing comes from src/test/util/masternode.h, the shared
module added in #7536, so nothing is duplicated here.

One preparatory commit is needed for that: FundTransaction() always paid the
change back to the payout script, which does not work for the proposal fee
transaction — its payout is an OP_RETURN burn, and IsCollateralValid()
rejects the transaction unless the change lands on a P2PKH output. An overload
taking a separate change script covers that; the existing five-argument form
keeps its behaviour and no existing call site changes.

How Has This Been Tested?

  • ./src/test/test_dash --run_test=governance_vote_processing_tests — passes,
    roughly 0.6s for all three cases.
  • ./src/test/test_dash --run_test=block_reward_reallocation_tests and
    --run_test=evo_dip3_activation_tests — pass, covering the other users of the
    shared FundTransaction() helper.
  • Full ./src/test/test_dash run — 793 cases, no errors. The pre-existing
    governance suites (governance_inv_tests, governance_validators_tests,
    governance_superblock_tests, governance_vote_wire_tests) and
    evo_dip3_activation_tests are unaffected.
  • test/lint/all-lint.py — clean apart from cppcheck warnings that already
    exist on develop in unrelated files; clang-format reports no differences on
    the new file.

The tests were checked to actually bite, by mutation:

  • Adding a masternode/signature gate in front of the orphan cache in
    CGovernanceManager::ProcessVote with its condition inverted (so that
    legitimate votes are rejected) fails
    orphan_vote_is_cached_and_applied_when_parent_arrives on 7 assertions.
  • Making both CGovernanceVote::CheckSignature overloads return true
    unconditionally fails the other two cases on 15 assertions.
  • Forcing a fixture-constructor invariant to fail leaves the tx index torn down
    correctly and the failure contained to this suite: the three cases fail and
    evo_dip3_activation_tests / txindex_tests still pass in the same binary run.

Environment: macOS (arm64), depends build, --enable-debug.

Breaking Changes

None. Test-only change.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 343f1775-8b41-4b74-ab18-af74b250d851

📥 Commits

Reviewing files that changed from the base of the PR and between b7f0cde and c4f671e.

📒 Files selected for processing (1)
  • src/test/governance_vote_processing_tests.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/governance_vote_processing_tests.cpp

Walkthrough

Adds comprehensive governance vote-processing tests for orphan replay, invalid signatures, unknown masternodes, future-dated votes, key authorization, penalties, duplicates, and vote counts. Extends FundTransaction with a separate change-script parameter and registers the new test source.

Sequence Diagram(s)

sequenceDiagram
  participant GovernanceVoteTest
  participant GovernanceManager
  participant MasternodeState
  GovernanceVoteTest->>GovernanceManager: submit governance vote
  GovernanceManager->>MasternodeState: validate masternode and signing key
  MasternodeState-->>GovernanceManager: authorization result
  GovernanceManager-->>GovernanceVoteTest: cache, reject, or count vote
Loading

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • dashpay/dash#7536: Extends the shared FundTransaction helper used by the governance vote tests.

Suggested reviewers: udjinm6

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a chain-backed fixture for governance vote signature testing.
Description check ✅ Passed The description directly explains the new chain-backed tests, helper changes, test coverage, and validation results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Aug 3, 2026

Copy link
Copy Markdown

🕓 Ready for review — 8 ahead in queue (commit c4f671e)
Queue position: 9/11 · 1 review active
ETA: start ~22:59 UTC · complete ~23:16 UTC (median 17m across 30 recent reviews; 2 slots)
Queued 16m ago · Last checked: 2026-08-03 21:50 UTC

@PastaPastaPasta

Copy link
Copy Markdown
Member Author

PR #7536 adds shared test/util/masternode.{h,cpp} helpers for this UTXO selection, funding, signing, and ProRegTx plumbing. If #7536 merges first, please rebase this PR and use <test/util/masternode.h> instead of retaining the local copy. The shared BuildSimpleUtxoMap also skips unspendable outputs.

FundTransaction always paid the change back to the payout script, which only works when that script is spendable. A test funding a governance proposal fee has to burn the amount to an OP_RETURN, so the change needs its own destination.
…xture

The existing governance unit tests run on a fixture with no chain, so the tip masternode list is empty and every CGovernanceVote::IsValid() call short-circuits at GetMNByCollateral before any signature is verified. Nothing exercised CheckSignature, and nothing proved that a legitimately signed vote is accepted at all.

Add a fixture that mines a regtest chain, registers a masternode via a real ProRegTx and keeps its voting (ECDSA) and operator (BLS) keys, so votes can be signed for real. On top of it: an orphan vote (parent object unknown) from a registered masternode is cached, requested and replayed onto the object once its fee collateral confirms and the proposal arrives; forged, unknown-masternode and future-dated votes are rejected with a peer penalty; and funding votes on a proposal are accepted only from the voting key while other signals accept the operator key.

Verified by mutation: inverting a masternode/signature gate in front of the orphan cache fails the orphan test, and making CheckSignature always succeed fails the rejection tests.
@PastaPastaPasta
PastaPastaPasta force-pushed the test/governance-vote-signature-coverage branch from fe12cc9 to b7f0cde Compare August 3, 2026 18:50
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

This pull request has conflicts, please rebase.

…the strprintf

lint-format-strings.py cannot parse raw string literals and miscounts the format specifiers, failing the Lint CI job.
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.

2 participants