fix(drive-abci): refuse a masternode vote on an unfunded poll as an unpaid consensus error - #4904
Conversation
…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>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Warning Review limit reachedNext included review available in 12 minutes. View limit detailsLimit 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. Review configuration: ⚙️ Run configurationConfiguration used: Repository: dashpay/platform/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: dashpay/platform/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe 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. ChangesMasternode vote funding validation
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Change: Bug fix 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation 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 💡
🧪 Generate unit tests (beta)
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. Comment |
|
📖 Book Preview built successfully. Download the preview from the workflow artifacts. Updated at 2026-09-22T01:30:50.205Z |
|
|
…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>
* 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>
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_transitionv0) ran the pre-check on that fund but bound its result to_prefunded_balancesand 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, aslogged at error level, instead of the
PrefundedSpecializedBalanceNotFoundError/PrefundedSpecializedBalanceInsufficientErrorthe pre-check already produces.What happens around it today, established before changing anything:
PaidFixedCost, whose fee validation applies the operations in estimation mode; the stateless deduction treats a missing fund as unbounded. A probe throughPlatform::check_txon an unfunded poll returned valid with a zero fee atFirstTimeCheckandRecheck. This PR does not change check_tx (see follow-up below).prepare_proposalmaps bothInternalErrorandUnpaidConsensusErrortoTxAction::Removed(with the savepoint rollback), andprocess_proposalrejects 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.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_transitionv0 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_distributionandtest_new_vote_after_lockupdated); voters never see that difference, since check_tx validates the poll's status first and refuses a late vote at broadcast withVotePollNotAvailableForVotingError.masternode_vote/balance/v0) requires the fund to cover the single vote cost the vote deducts, instead of the vote's minimum fee.How Has This Been Tested?
New tests in
processor/v0/mod.rs, throughprocess_raw_state_transitionson a DPNS name contest:UnpaidConsensusError(PrefundedSpecializedBalanceNotFoundError), nothing recorded;UnpaidConsensusError(PrefundedSpecializedBalanceInsufficientError)with the balance and the required amount, nothing recorded;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 warningsclean.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_fundin #4899 is being changed there to accept both the v0InternalErrorand theUnpaidConsensusError(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:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull requestFor repository code-owners and collaborators only
🤖 Generated with Claude Code
PR Hygiene ·
f846178/skip-botsproceeds without the ones not yet reported/self-reviewedonce the bots are doneWhen every box is checked the
PR Hygienecheck passes and this can merge.