Skip to content

fix(drive-abci): refuse a masternode vote on an unfunded poll as an unpaid consensus error - #4904

Merged
QuantumExplorer merged 4 commits into
v4.2-devfrom
claude/optimistic-blackwell-0d1f03
Sep 22, 2026
Merged

QuantumExplorer merged 4 commits into
v4.2-devfrom
claude/optimistic-blackwell-0d1f03

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

A masternode vote is paid by its vote poll's prefunded specialized balance, never by the voter. The processor (process_state_transition v0) ran the pre-check on that fund but bound its result to _prefunded_balances and never read it. A vote on a poll whose fund is missing, or too small, therefore reached execution during block processing and failed there, when the vote's cost was deducted, as

InternalError("storage: drive: prefunded specialized balance does not exist: trying to deduct ...")

logged at error level, instead of the PrefundedSpecializedBalanceNotFoundError / PrefundedSpecializedBalanceInsufficientError the pre-check already produces.

What happens around it today, established before changing anything:

  • check_tx admits such a vote at both levels. Neither the first-time check nor the recheck runs the pre-check, and the vote's execution event is a PaidFixedCost, whose fee validation applies the operations in estimation mode; the stateless deduction treats a missing fund as unbounded. A probe through Platform::check_tx on an unfunded poll returned valid with a zero fee at FirstTimeCheck and Recheck. This PR does not change check_tx (see follow-up below).
  • The proposer strips it, validators reject it. prepare_proposal maps both InternalError and UnpaidConsensusError to TxAction::Removed (with the savepoint rollback), and process_proposal rejects any block whose execution produced either (unexpected_execution_results). So such a vote never enters a block from an honest proposer under either result, and blocks are unaffected by this change.
  • The pre-check's threshold was too low. It required the fund to cover the vote's minimum fee (state_transition_min_fees.masternode_vote, 100,000 credits), while execution deducts the single vote cost (contested_document_single_vote_cost, 10,000,000 credits). A fund between the two passed the pre-check and the vote still failed inside execution.

What was done?

In place, not versioned: both the old and the new outcome keep the vote out of every block (the proposer strips it, a validator rejects a block that carries it), so no block on any chain can hold such a vote and replay is unaffected; only the error a client sees and the log level change.

  • process_state_transition v0 returns the pre-check's errors as an unpaid consensus error, from the place the pre-check already ran. Because settling a poll deletes its pot, a vote that reaches a block after the poll ended is now refused for the missing pot rather than for the poll's status (test_new_vote_after_document_distribution and test_new_vote_after_lock updated); voters never see that difference, since check_tx validates the poll's status first and refuses a late vote at broadcast with VotePollNotAvailableForVotingError.
  • The pre-check (masternode_vote/balance/v0) requires the fund to cover the single vote cost the vote deducts, instead of the vote's minimum fee.
  • The book's validation-pipeline chapter says where the vote's fund check sits and why. No version table changes.

How Has This Been Tested?

New tests in processor/v0/mod.rs, through process_raw_state_transitions on a DPNS name contest:

  • no fund: UnpaidConsensusError(PrefundedSpecializedBalanceNotFoundError), nothing recorded;
  • fund one credit below the single vote cost (which still satisfies the old minimum-fee threshold): UnpaidConsensusError(PrefundedSpecializedBalanceInsufficientError) with the balance and the required amount, nothing recorded;
  • fund exactly the single vote cost: the vote executes and the fund goes to zero;
  • two votes in one block on a fund covering one: the second is refused for the credits the first took, read through the block transaction;
  • the same unfunded vote at protocol version 13 is refused the same way.

The two existing late-vote tests now expect the missing-pot error in the block path.

Run locally: cargo test -p drive-abci --all-features -- masternode_vote:: processor:: (121 passed), cargo clippy -p drive-abci -p platform-version --all-features --all-targets -- -D warnings clean.

Breaking Changes

None for blocks: the vote never enters a block before or after. The consensus error a client sees for such a vote changes from an internal error to the two prefunded balance errors, at every protocol version once the node is upgraded.

Follow-up

check_tx (state_transition_to_execution_event_for_check_tx, shipped v0) could run the same pre-check at both levels so an unfunded vote is refused at broadcast time rather than sitting in the mempool until a proposer strips it. That is a new generation of a 700-line method for a dozen lines, so it is left out of this PR deliberately.

The test should_refuse_a_vote_when_the_poll_has_no_fund in #4899 is being changed there to accept both the v0 InternalError and the UnpaidConsensusError(PrefundedSpecializedBalanceNotFoundError) this PR produces, so the two PRs merge independently in either order. Pinning the consensus error alone is a one-line follow-up once both are in v4.2-dev.

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 added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed
  • If I added or changed GroveDB structure, I described it in the area's structure.rs, regenerated grovedb-structure.json, and checked the structure viewer link posted on this pull request

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

PR Hygiene · f846178

  • Bots — coderabbitai not yet · thepastaclaw not yet — /skip-bots proceeds without the ones not yet reported
  • Self-review — post /self-reviewed once the bots are done
  • Within your 5 open PRs
  • Build running
  • Approvals — you own every area touched; none needed

When every box is checked the PR Hygiene check passes and this can merge.

…npaid consensus error

A masternode vote is paid by its vote poll's prefunded specialized balance.
The processor ran the pre-check on that fund but never read its result, so
a vote on a poll whose fund is missing, or too small, reached execution and
failed there when the vote's cost was deducted, as an InternalError logged
at error level, instead of the PrefundedSpecializedBalanceNotFoundError or
PrefundedSpecializedBalanceInsufficientError the pre-check produces.

check_tx never runs the pre-check and estimates the vote's fee statelessly,
so it admits such a vote at both levels; the vote was only ever caught by
the proposer, which strips any InternalError result from its block, and by
validators, which reject a block carrying one. That does not change.

Protocol version 14 (DRIVE_ABCI_VALIDATION_VERSIONS_V10):

* process_state_transition 1 returns the pre-check's errors as an unpaid
  consensus error. The fund is checked after state validation, once the
  poll is known to be open: settling a poll deletes its fund, so a check
  ahead of state validation would report a missing fund for every late
  vote and hide the poll's status.
* masternode_vote_state_transition_balance_pre_check 1 requires the fund
  to cover the single vote cost the vote deducts (10_000_000 credits); v0
  required only the vote's minimum fee (100_000 credits), so a fund between
  the two passed the pre-check and the vote still failed inside execution.

Blocks are unaffected under either generation: proposers strip the vote and
validators reject a block that carries it, so replay is identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 21, 2026
@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Next included review available in 12 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: dashpay/platform/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 90955675-4a08-4093-a3dd-ef3d36311a2a

📥 Commits

Reviewing files that changed from the base of the PR and between 4ad4994 and f846178.

📒 Files selected for processing (3)
  • book/src/state-transitions/validation-pipeline.md
  • packages/rs-drive-abci/src/execution/validation/state_transition/processor/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: dashpay/platform/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: a7162c95-7585-4b00-909f-26981a840394

📥 Commits

Reviewing files that changed from the base of the PR and between 7aff030 and 4ad4994.

📒 Files selected for processing (3)
  • book/src/state-transitions/validation-pipeline.md
  • packages/rs-drive-abci/src/execution/validation/state_transition/processor/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/balance/v0/mod.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change validates masternode vote funding after poll state validation, uses the configured single-vote cost, refactors vote test helpers, adds v0 processing tests, and updates the validation-pipeline documentation.

Changes

Masternode vote funding validation

Layer / File(s) Summary
Single-vote balance validation
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/balance/v0/mod.rs
The balance pre-check compares funds with contested_document_single_vote_cost and reports insufficient balances against that cost.
Post-validation processing and coverage
packages/rs-drive-abci/src/execution/validation/state_transition/processor/v0/mod.rs, packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs
The processor checks prefunded balances after valid state validation. Test helpers now build serialized DPNS votes separately. Tests cover missing, insufficient, exact, consumed, and missing poll funds across protocol versions.
Validation-pipeline behavior
book/src/state-transitions/validation-pipeline.md
The documentation states that poll status is checked before missing or insufficient prefunded balance errors for open-poll votes.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 42 functions across 9 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: refusing masternode votes on unfunded polls as unpaid consensus errors.
Full details: Docstring Coverage

Explanation

Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 42 functions across 9 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Sep 21, 2026
@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

📖 Book Preview built successfully.

Download the preview from the workflow artifacts.
To view locally: download the artifact, unzip, and open index.html.

Updated at 2026-09-22T01:30:50.205Z

@thepastaclaw

thepastaclaw commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

⚠️ DEGRADED — Queued for automated review — 17th in line, estimated start in ~13 h (commit f846178)
Estimated review time once started: ~1.5 h (two-phase automated review; median of recent runs).
The primary review models are currently out of quota; this review will run on stand-in models and be marked as degraded.

  • Request priority review — click to move this review to the front of the queue.

QuantumExplorer added a commit that referenced this pull request Sep 21, 2026
…ound the yes/no poll path

Review fixes on the yes/no poll kind:

- A resource vote that names a yes/no poll is refused as
  VotePollNotFoundError in both the transform and the state validation
  instead of ending in an internal error, with a test.
- The masternode removal sweep skips the decisions identity index when a
  chain that activated protocol version 14 before the trees existed has
  none, instead of failing the block.
- The poll state query returns the empty state without proof for a poll
  that was never opened, as the proved path does.
- The resource path is bounded by two new SystemLimits (16 segments, 1024
  bytes), since every vote carries the poll.
- A yes/no vote is proved executed end to end through the state
  transition prover and verifier.
- The testnet corrupted-reference branch leaves yes/no polls that have
  not ended alone.
- The vote method table names the slot register_identity_vote dispatches
  on, the drive error type is imported, and the unfunded-poll test
  accepts the unpaid consensus error #4904 introduces.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
QuantumExplorer and others added 3 commits September 22, 2026 06:12
* The book names the single vote cost, not the vote fee, as the threshold.
* The DPNS vote transition builder and vote poll move into the shared test
  helpers (`serialized_dpns_name_vote`, `dpns_name_vote_poll`), used by
  `perform_vote` and by the processor v1 tests, which drop their own copy
  and their single-use wrappers.
* A test casts two votes in one block on a fund that covers one: the
  second is refused for the credits the first took, through the block
  transaction.
* Comments state the cost of checking the fund after state validation and
  why a vote's validation returns its errors without an action.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ad of a new processor generation

Both outcomes keep the vote out of every block, so no block on any chain
can hold such a vote and nothing needs a version gate: the processor v0
returns the pre-check's errors unpaid after state validation, the pre-check
v0 requires the single vote cost, the tests live under v0, and the version
tables and the v14 changelog are untouched. The protocol version 13 test
now shows the same refusal there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dy ran

The pre-check stays at its original place in the processor and its result
is read there, instead of being moved after state validation. A vote that
reaches a block after its poll settled is therefore refused for the deleted
pot rather than for the poll's status; the two late-vote tests expect that
now. Voters never see the difference: check_tx validates the poll's status
first and refuses a late vote at broadcast with the status error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@QuantumExplorer QuantumExplorer left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed

@QuantumExplorer
QuantumExplorer merged commit 73f6d0c into v4.2-dev Sep 22, 2026
19 of 20 checks passed
@QuantumExplorer
QuantumExplorer deleted the claude/optimistic-blackwell-0d1f03 branch September 22, 2026 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-bots Waiting for the review bots to report on this head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants