feat(platform)!: yes/no masternode vote poll kind with supermajority and minimum voting power - #4899
QuantumExplorer wants to merge 20 commits into
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks 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 |
|
🕓 Review not started yet because this PR is a draft.
Commit 49df27f. Normal review starts when eligible; priority review starts as soon as a slot is available. |
|
📖 Book Preview built successfully. Download the preview from the workflow artifacts. Updated at 2026-09-23T13:34:38.714Z |
|
🌳 GroveDB structure This pull request changes the described GroveDB structure. Open it in the structure viewer: new nodes glow, removed ones stay as ghosts, and the tour walks through each change. Added (12 nodes)
Changed (4 nodes)
Compared |
|
@coderabbitai review No review for |
|
Bots are done — your move: post |
…h or signedness (PV14) (#4925) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… (PV14) (#4919) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…onstraints, untrusted lists, Swift refusal (#4924) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…4918) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… (PV14) (#4926) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…4916) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ties (#4927) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…and minimum voting power A second kind of masternode vote poll for the moderation team challenge and charter amendment votes (#4875, decisions D21 and D41 of #4865). `YesNoVotePoll` is keyed by a caller-chosen resource path and carries its own rule: it passes when yes power is strictly more than numerator/denominator of yes plus no power (abstain left out) and yes plus no reaches the minimum. Its unique id hashes the serialized `VotePoll`, and the prefunded balance the votes are paid from has the same id. `YesNoVote` answers it with yes, no or abstain; a masternode vote carrying one is active from protocol version 14. Storage lives under the votes tree's decisions branch: one tree per poll with the stored info and three vote sum trees, and an identity votes index keyed by pro tx hash. The trees are created at genesis (vote setup v1) and on the first block of protocol version 14. `check_for_ended_vote_polls` v1 tallies a poll whose time came, records the result in its stored info, hands it to the `on_yes_no_vote_poll_finished` hook and cleans the votes up; the stored info stays as the record. `clean_up_after_vote_polls_end` v1 removes every finished poll of either kind from the shared end date index itself, since the contested clean-up's count of contested polls alone would delete an end date's tree that still held a yes/no entry; `clean_up_after_contested_resources_vote_polls_end` v2 leaves the index to it. Masternode vote transform v1 looks up the previous yes/no answer for the vote change rules, state v1 requires the poll to exist and be started (new `YesNoVotePollNotAvailableForVotingError`, 40308). Proofs: a poll state query with verification and a masternode yes/no vote verifier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c04fea0 to
db5cc32
Compare
…tocol version 14 A masternode vote carrying a yes/no vote was already active from protocol version 14 only, but a resource vote whose poll is a yes/no poll stayed active at every version. Binaries from before the yes/no poll kind cannot decode either, while a new binary at protocol version 13 decoded the resource vote and ran the shipped (v0) masternode vote validation on it, where the fund pre-check refused it. Both kinds are now inactive before 14, so decoding refuses them without charging, exactly as older binaries do, and no validator selected before 14 meets a yes/no poll. That makes the yes/no arms this change added to shipped generations unreachable by construction. Each now says why at the edited lines, and a protocol version 13 test runs a DPNS contest through the shipped transform, state validation, vote registration, end of poll check and clean-up with the outcome unchanged, and shows both vote kinds naming a yes/no poll stripped before any of them runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he derive's consumed byte count (#4931) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…(PV14) (#4930) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#4930 appended ReferencedDocumentLookupInvalidError as StateError discriminant 145, so YesNoVotePollNotAvailableForVotingError moves to 146; the frozen discriminant test pins both, and wasm-dpp maps both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@coderabbitai review No review for |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 2 only (queue backlog)
Confirmed two blocking defects: partial end-date batches can abort the new poll-cleanup lifecycle, and the generic vote verifier accepts authenticated absence proofs from the wrong storage branch for yes/no votes. Both were independently reproduced against the assigned head; source inspection also confirms that the exported TypeScript serialization types exclude the newly supported variants. The worktree remains unchanged.
🔴 2 blocking | 🟡 1 suggestion(s)
Review provenance
Source: reviewer 1: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 2: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6-astra (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6-astra (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)
- Triage:
criticalbygpt-6-astra(effort low) — This large, intricate change adds consensus-critical vote validation and poll resolution in transform_into_action/v1 and check_for_ended_vote_polls/v1, and migrates voting storage during protocol activation in perform_events_on_first_block_of_protocol_change/v0. - Phase 1 reviewers: not run (skipped for throughput: 18 PRs queued, above the 10 limit)
- Fresh verifier:
gpt-6-astra— final-verifier; agentastra-verifier - Phase 2 reviewers:
gpt-6-astra— general (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs`:
- [BLOCKING] packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs:55-65: Do not delete an end-date tree based on its partial batch count
`maximum_vote_polls_to_process` limits the query across all timestamps, not each timestamp separately. With the current limit of two, one due poll at T1 and two at T2 return one poll from each timestamp. Both per-timestamp counts are below two, so this helper schedules deletion of T2's parent while an unprocessed poll remains. An independently rebuilt regression using three yes/no polls and production batch settings fails with `GroveDB(InvalidBatchOperation("modification of tree when it will be deleted"))`; `clean_up_after_vote_polls_end_v1` propagates that error into block processing. Although the contested-only predecessor contains the same assumption, this PR introduces the shared helper and makes the new yes/no lifecycle depend on it, so this is required for the advertised cleanup behavior to work. Check actual remaining entries, accounting for queued removals, before deleting each timestamp tree, and add a regression where a globally limited batch only partially consumes its final timestamp.
In `packages/rs-dpp/src/voting/votes/mod.rs`:
- [BLOCKING] packages/rs-dpp/src/voting/votes/mod.rs:70-73: Dispatch or reject yes/no votes in the existing generic proof verifier
The widened `Vote` enum now supplies yes/no poll IDs to the existing public `Drive::verify_masternode_vote`, but that verifier still unconditionally queries `Votes/c/i/<pro_tx_hash>`. Yes/no votes are stored under `Votes/d/i/<pro_tx_hash>`. A proof-serving node can therefore supply a genuine contested-branch absence proof and have the client authenticate a false absence result for an existing yes/no vote. An independently rebuilt regression registered a yes/no vote, supplied the wrong-branch absence proof, and received `Ok((root_hash, None))`, with the root matching the database containing the vote. The exported `wasm-drive-verify` `verifyMasternodeVote` function also reaches this path because it deserializes `Vote` and delegates directly to the generic verifier. Routing in state-transition verification does not protect these callers. Dispatch by vote kind at the shared verifier boundary, or explicitly reject unsupported vote/poll combinations, and add a wrong-branch absence-proof regression.
In `packages/wasm-dpp2/src/voting/vote_poll.rs`:
- [SUGGESTION] packages/wasm-dpp2/src/voting/vote_poll.rs:128: Include yes/no variants in the exported TypeScript wire types
The bindings now expose yes/no polls and votes, but `VotePollObject` and `VotePollJSON` still require `$type: "contestedDocumentResourceVotePoll"` and contested-only fields. Likewise, `voting/vote.rs` declares `VoteObject` and `VoteJSON` exclusively as resource votes. The five-argument `impl_wasm_conversions_inner!` macro uses these declarations for both conversion inputs and outputs, so valid yes/no `fromJSON`/`fromObject` calls fail TypeScript checking, and serialized results cannot be correctly narrowed or inspected without bypassing the published types. This affects the serialization API added here, independently of deferred client query APIs. Make all four declarations discriminated unions matching DPP's wire shapes, including the untagged poll body inside a yes/no vote, and add a TypeScript conversion fixture.
…ting, genesis trees, bindings - The end-date clean-up deleted an end date's tree whenever fewer than maximum_vote_polls_to_process polls closed there, but the block's fetch shares that limit across every due end date. A partly fetched last end date lost its tree in the same batch as the fetched entry, GroveDB refused the batch and the block failed. The tree now goes only when the entries it holds are all being removed; regression test with one poll at T1 and two at T2. - Drive::verify_masternode_vote queried the contested resource votes index for any vote, so a genuine absence proof from that branch verified a yes/no vote as "not voted". It now routes a yes/no vote to verify_masternode_yes_no_vote and refuses a resource vote naming a yes/no poll; wrong-branch regression. - Genesis creates the decisions trees through insert_vote_decisions_trees, the helper the protocol version 14 upgrade uses (vote setup v1 removed). - Closing a poll no longer copies the pending batch on every delete (quadratic in voters) and deletes the choice sum trees without re-reading every vote. - validate_parameters refuses a zero numerator; poll ids hash a domain tag before the serialized poll, so the two poll kinds never share an id. - The yes/no vote verifier reuses the prover's IdentityBasedVoteDriveQuery and the bounded untrusted decoder. - SDK: the "already exists" recovery no longer queries contested votes for a yes/no vote; DPNS contest pagination continues while pages are full. - wasm-dpp2 / wasm-sdk: resource-vote paths refuse a yes/no poll; TypeScript Vote and VotePoll object/JSON types are unions with the yes/no shapes. - Tests: CheckTx root-invariance guard on every yes/no vote, shared test helpers, no stale InternalError tolerance, error 40308 reached by re-funding a finished poll; change-cap wording (votes, first included). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… share of the total
A yes/no poll's minimum voting power was a fixed number. It is now either:
- `Absolute(voting_power)`, at most `max_yes_no_vote_poll_minimum_voting_power`
(2000), 0 included; or
- `FractionOfTotal { numerator, denominator, rounding }`: that share of the total
voting power of the masternode list (masternode 1, evonode 4) in the block that
closes the poll, rounded up or down, at most
`max_yes_no_vote_poll_minimum_voting_power_percent_of_total` (50) percent of it.
1/3 rounded up represents ceil(total / 3).
The closing block sums its masternode list as votes are weighed, resolves the
minimum, and records the resolved bar as `required_voting_power` in the poll's
result, so a finished poll shows what it was judged against. Both caps are new
SystemLimits fields, backfilled into every table.
Wire shape: `"minimumVotingPower": { "absolute": 400 }` or
`{ "fractionOfTotal": { "numerator": 1, "denominator": 3, "rounding": "up" } }`;
the wasm-dpp2 TypeScript types follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nus one `VotingPowerRounding` gains `DownPlusOne` (floor + 1) and `UpMinusOne` (ceil - 1, never below zero), appended after `Down` and `Up`. Half the total rounded down plus one is a strict majority of it: 6 of 10, 5 of 9. JSON names are `downPlusOne` and `upMinusOne`; the wasm-dpp2 TypeScript type lists all four. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Session notes: where this PR standsA summary of the working session on 2026-09-23, written so the work can be picked up again later. The PR is now a draft against Status
What was done in the session
How a yes/no vote can start todayIt cannot yet.
Decisions taken
Open design questionsThese are the five decisions I was least sure of: what the PR does, why it is uncertain, the alternative, and which way I lean.
Where to focus a review
Skim only: the version-table and Not in this PR
|
Issue being fixed or feature implemented
Closes #4875, part of decentralized moderation teams (#4865).
Masternodes can only vote on one kind of poll today: the contested document resource poll that decides who gets a premium DPNS name. That poll picks a winner among contenders for a document. Decentralized moderation teams (#4865) need a different question answered: "should this happen, yes or no?"
A contested poll cannot express either one: it has no contenders to choose between, no document to award, and its tally is a plurality with Lock and Abstain, not a supermajority with a turnout floor. So this PR adds a second poll kind, the yes/no poll, with its own vote, storage, closing and proofs.
flowchart LR TEAMS["Decentralized moderation teams, 4865"] CONTENDER["Contender poll without Lock, PR 4907 (merged)"] YESNO["Yes/no poll kind (this PR)"] FUND["Poll windows and fund from the target contract, 4876"] CHALLENGE["Team challenges (not filed yet)"] AMEND["Charter amendments (not filed yet)"] TEAMS --> CONTENDER TEAMS --> YESNO TEAMS --> FUND CONTENDER --> CHALLENGE YESNO --> CHALLENGE YESNO --> AMEND FUND --> CHALLENGE FUND --> AMENDThis PR is the poll machinery only. Nothing opens a yes/no poll yet:
Drive::open_yes_no_vote_pollexists and is tested, but only the team challenge and charter amendment features will call it, as a side effect of their own transitions, and the funding work in #4876 funds the poll. Until then no yes/no poll can exist on any chain, so every yes/no vote is refused.TODO
v4.3-dev, so everything it gates at 14 moves to 15, once protocol version 15 exists (v4.3-devonly hasv14.rsso far):v4.3-dev(againstv4.3-devthe diff also showsv4.2-devcommits it does not have yet);StateTransition::active_version_range: a masternode vote naming a yes/no poll becomes active from 15;DRIVE_VOTE_METHOD_VERSIONS_V3,DRIVE_ABCI_METHOD_VERSIONS_V10andDRIVE_ABCI_VALIDATION_VERSIONS_V10;check_for_ended_vote_pollsv1 and masternode votestatev1 (both introduced by #4907), and ofclean_up_after_vote_polls_endand the contested clean-up split, need new generations selected at 15;create_initial_state_structurev4 andtransition_to_version_14to the protocol version 15 genesis structure andtransition_to_version_15;SystemLimits: the four yes/no limits move to the protocol version 15 table and are backfilled in the earlier ones;YesNoVotePollNotAvailableForVotingErrorat the end ofStateErroragain, with its code and the frozen discriminant test, if anything landed after it;grovedb-structure.json.Session notes, decisions taken and open design questions are in the PR comment "Session notes: where this PR stands".
What was done?
The life of a yes/no poll
stateDiagram-v2 [*] --> Started: opened and funded by a later feature Started --> Started: a masternode votes or changes its answer Started --> Finished: the first block at or after the end date tallies it Finished --> [*]: the stored info stays as the record of the decisionDrive::open_yes_no_vote_poll, no production caller yet): writes the stored info asStarted(block), creates the three vote sum trees and adds the poll to the end date index. The opener also funds the poll's prefunded balance, which pays for every vote.MasternodeVotecarrying aYesNoVoteputs the masternode's strength (masternode 1, evonode 4, as for contested polls) into the sum tree of its answer. Changing the answer moves the whole strength.on_yes_no_vote_poll_finishedreceives the result. Its v0 does nothing on purpose: the challenge and amendment features add the code that seats a challenger or swaps a charter.Why the rule lives in the poll
YesNoVotePollcarries its own rule:supermajority_numerator,supermajority_denominatorandminimum_voting_power, next to an opaqueresource_paththat the opening feature chooses (a contract id and a purpose, for example).The poll passes when
yes * denominator > (yes + no) * numeratorandyes + noreaches the minimum. Abstain counts toward neither side, so abstaining neither helps nor blocks a decision. It only shows the masternode took part.The minimum is set per poll, in one of two forms:
Absolute(n): from 0 (no floor) up to 2000.FractionOfTotal { numerator, denominator, rounding }: that fraction of the total voting power of the masternode list (masternode 1, evonode 4) in the block that closes the poll, at most 50% of it. The share is rounded one of four ways:down(floor),up(ceil),downPlusOne(floor + 1) orupMinusOne(ceil - 1, never below 0).1/10rounded up is a tenth of the network;1/3rounded up isceil(total / 3);1/2rounded down plus one is a strict majority (6 of 10, 5 of 9).Why both: a fixed floor is predictable, while a share keeps the same meaning as the network grows or shrinks. The share is measured when the poll closes, so the bar reflects the network that decides. The bar the poll was judged against is recorded in its result (
required_voting_power), so a finished poll shows why it passed or failed. The two caps (2000 and 50%) areSystemLimitsfields.With the challenge rule (2/3, a fixed minimum of 400):
With a share instead (2/3, at least 1/10 of the total rounded up) and a masternode list worth 3,801 voting power, the bar is 381: 381 yes and 0 no passes, 380 fails, and the same 381 would fail once the list is worth 3,811.
The rule is part of the poll's identity. The poll's unique id is a double SHA-256 over a yes/no domain tag followed by the serialized
VotePoll, so:validate_parametersrefuses rules no poll should run with: an empty or oversized resource path, a zero denominator, a zero numerator (any single yes would pass), a numerator not below the denominator, a fixed minimum above 2000, and a share minimum with a zero denominator or above 50%. The path is bounded (SystemLimits: 16 segments, 1024 bytes) because every vote carries the whole poll.A finished poll keeps its stored info forever as the record and cannot be reopened under the same id. A feature that asks the same question again puts a round marker in the resource path.
Why the storage looks like this
flowchart TD V["Votes"] --> C["c: contested resources (unchanged)"] V --> E["e: end date queries, shared by both poll kinds"] V --> D["d: decisions (reserved and empty since genesis)"] E --> ET["end time"] ET --> EP["poll id: the serialized VotePoll, either kind"] D --> P["p: active polls"] P --> PID["poll id"] PID --> S0["0: stored info, Started or Finished with the result"] PID --> S1["1: yes votes, sum tree of pro_tx_hash to strength"] PID --> S2["2: no votes, sum tree"] PID --> S3["3: abstain votes, sum tree"] D --> IV["i: identity votes"] IV --> MN["pro_tx_hash"] MN --> ANS["poll id: the answer and how many times it voted"]dhas existed, empty, since genesis for this purpose, so yes/no storage stays apart from contested storage.igives the previous answer for the change rules, finds every vote of a masternode that leaves the list, and lets a client prove one masternode's answer.panditrees are created by one helper,insert_vote_decisions_trees. Genesis (create_initial_state_structurev4) and the upgrade to protocol version 14 (transition_to_version_14) both call it, in the same order, so a chain born at 14 and an upgraded chain build the same votes Merk. A test compares the two.eis shared with contested polls. This is deliberate. One query at block end finds every due poll of either kind under one per-block limit, instead of two sweeps that would compete for it. The cost is the client compatibility break described under Breaking Changes.Voting
flowchart TD T["MasternodeVote carrying a YesNoVote"] --> PV{"protocol version 14 or later?"} PV -- no --> R1["refused at decode, unpaid"] PV -- yes --> SIG["signature and nonce"] SIG --> FUND{"does the poll's fund cover one vote?"} FUND -- no --> R2["refused, unpaid: PrefundedSpecializedBalanceNotFound or Insufficient"] FUND -- yes --> PREV{"this masternode's previous answer"} PREV -- "the same answer" --> R3["MasternodeVoteAlreadyPresentError"] PREV -- "already voted 5 times" --> R4["MasternodeVotedTooManyTimesError"] PREV -- "none, or a different answer" --> ST{"is the poll open?"} ST -- "not opened" --> R5["VotePollNotFoundError"] ST -- "Finished" --> R6["YesNoVotePollNotAvailableForVotingError, 40308"] ST -- "Started" --> OK["pay the vote from the fund, move the strength to the chosen sum tree, update the identity index"]StateTransition::active_version_range). That covers both aYesNoVoteand aResourceVotewhose poll is a yes/no poll. Binaries from before this change cannot decode such a vote, so an earlier protocol version refuses it at decode, unpaid, exactly as they do. No shipped validator ever sees one.votes_allowed_per_masternode(5) times, its first vote included.contested_document_single_vote_cost. The opener funds the poll.YesNoVotePollNotAvailableForVotingError(code 40308,StateErrordiscriminant 146) refuses a vote on a finished poll. AResourceVotenaming a yes/no poll is refused asVotePollNotFoundError, since no contested poll has that id.Closing at the end of a block
flowchart TD B["end of every block, protocol version 14"] --> RM["remove the votes of masternodes that left the list, both kinds"] RM --> Q["fetch the due polls from the shared end date index: at most 2 per block, across all end dates"] Q --> K{"poll kind"} K -- contested --> CT["tally and award, as before"] K -- "yes/no" --> YT["read the three sums, measure a share minimum against this block's masternode list, apply the poll's rule"] YT --> REC["write Finished with the result into the stored info"] REC --> HOOK["on_yes_no_vote_poll_finished: no-op until challenges and amendments exist"] CT --> CL["clean_up_after_vote_polls_end v1"] HOOK --> CL CL --> IDX["remove the closed polls' end date entries; an end date's tree goes only when nothing is left under it"] IDX --> CC["contested clean-up v2: documents, votes, contenders, fund"] IDX --> YC["yes/no clean-up: votes, sum trees, identity index entries, the rest of the fund to the processing pool"]Why the end date clean-up moved up a level. With two poll kinds under one end date, a clean-up that only knows its own kind can delete an end date's tree that still holds the other kind's entry, and deleting a non-empty tree fails the block.
clean_up_after_vote_polls_endv1 therefore removes every closed poll's entry itself, whatever its kind, and the contested clean-up v2 leaves the index alone. The per-block limit spans every due end date, so the last end date a block reaches can be only partly processed. An end date's tree is removed only when every entry it holds is being removed in the same batch, never inferred from how many polls closed. Tests close a DPNS contest and a yes/no poll that end in the same millisecond, and three polls over two end dates that need two blocks.Proofs and clients
YesNoVotePollStateDriveQueryproves a poll's stored info and tallies (verify_yes_no_vote_poll_state_proof).verify_masternode_yes_no_voteproves it, using the prover's ownIdentityBasedVoteDriveQuerypath query and the bounded untrusted decoder.prove_state_transitionandverify_state_transition_was_executed_with_proofhandle the new vote kind.Drive::verify_masternode_voteroutes a yes/no vote to the yes/no verifier. A genuine absence proof from the contested index can no longer pass as "this masternode did not vote". It refuses a resource vote that names a yes/no poll.VoteandVotePollexpose the kind (kind,yesNoChoice), and their TypeScript object and JSON types are unions that include the yes/no shapes.VoteandResourceVote, wasm-sdkmasternodeVote) refuse a yes/no poll.get_current_dpns_contestsskips yes/no entries and keeps paginating while pages are full.How a yes/no vote looks on the wire (JSON). A share minimum is written
{ "fractionOfTotal": { "numerator": 1, "denominator": 3, "rounding": "up" } }:{ "$type": "yesNoVote", "$formatVersion": "0", "votePoll": { "resourcePath": ["wcHBwQ=="], "supermajorityNumerator": 2, "supermajorityDenominator": 3, "minimumVotingPower": { "absolute": 400 } }, "voteChoice": "yes" }The
YesNoAbstainVoteChoicetype already existed in rs-dpp, unused. Its variants are nowYes,NoandAbstain: the oldrename_allproduced"yES"on the wire.Not in this PR
on_yes_no_vote_poll_finished, which the challenge and amendment features add.FromProofand the SDKFetch, which are the client PR's.Versioning
Every new behaviour is selected at protocol version 14 only, and every table is amended in place:
DRIVE_VOTE_METHOD_VERSIONS_V3(new, drive v9).DRIVE_ABCI_METHOD_VERSIONS_V10:check_for_ended_vote_polls1,clean_up_after_vote_poll_end1,clean_up_after_contested_resources_vote_poll_end2, and four new yes/no slots.DRIVE_ABCI_VALIDATION_VERSIONS_V10: masternode votestate1,transform_into_action1.The contender poll change (#4907) merged first and had already introduced
check_for_ended_vote_pollsv1 and masternode votestatev1. This PR's changes to those methods are folded into the same v1 generations, so protocol version 14 has one v1 of each. New slots are 0 in every earlier table and unreachable there. The shipped generations this change touches are listed in the next section.In-place changes to shipped generations
Every edit below is unreachable at each protocol version that selects the edited generation, and the edited lines say why. The root is in dpp: a masternode vote that names a yes/no poll (a
YesNoVote, or aResourceVotewhose poll is aYesNoVotePoll) is active from protocol version 14 only (StateTransition::active_version_range). Before 14 it is refused at decode, unpaid, as a binary that cannot decode it refuses it, so no v0 validator ever sees one.transform_into_actionv0Vote::YesNoVoteand for a resource vote namingVotePoll::YesNoVotePollvalidate_statev0ResolvedVote::YesNoVoteandResolvedVotePoll::YesNoVotePollDrive::register_identity_votev0 (both functions)DRIVE_VOTE_METHOD_VERSIONS_V1,V2)check_for_ended_vote_pollsv0 (both branches)ResolvedVotePoll::YesNoVotePollDrive::open_yes_no_vote_poll, which writes underVotes/d/p, a tree that exists from 14 only (genesis structure v4,transition_to_version_14)clean_up_after_vote_polls_endv0YesNoVotePollWithVotescheck_for_ended_vote_pollsv1 builds that variant, and every table that selects this v0 selects check v0perform_events_on_first_block_of_protocol_changev0 (v1 runs its body at 13 and 14)VotePoll::YesNoVotePollarm intransition_to_version_8;insert_vote_decisions_treesintransition_to_version_14prove_state_transitionv0 body (v1 reuses it)verify_state_transition_was_executed_with_proofv0 body (v1 reuses it)verify_masternode_yes_no_vote; a resource vote naming a yes/no poll is refusedAlso consensus-neutral at shipped versions: the
remove_all_votes_given_by_identitiesdispatcher now reads its own slot instead ofremove_specific_vote_references_given_by_identity; both are 0 in every shipped table, so the untouched v0 still runs there. The new slots in shipped tables (DRIVE_ABCI_METHOD_VERSIONS_V1toV9,DRIVE_VOTE_METHOD_VERSIONS_V1andV2,DRIVE_VERIFY_METHOD_VERSIONS_V1,VOTING_VERSION_V1andV2(dpp),SYSTEM_LIMITS_V1toV3) are backfills read only by yes/no code; no existing value changed.should_run_a_contest_unchanged_and_strip_yes_no_polls_at_protocol_version_13runs a DPNS contest at protocol version 13 through transform v0, state v0,register_identity_votev0, check v0 and clean-up v0: the vote counts, the contender is awarded and the end date index empties. The same test shows both vote kinds naming a yes/no poll stripped before any validator runs, with the voter's nonce left unused.Before and after for the one behaviour change at a shipped version (a new binary at protocol version 13, a
ResourceVotenaming a yes/no poll):UnpaidConsensusError(PrefundedSpecializedBalanceNotFoundError).StateTransitionIsNotActiveError(stripped from the block, like every transition not active yet). An older binary strips it too, withSerializedObjectParsingError. Both outcomes keep it out of a proposal and reject a proposal that carries it.How Has This Been Tested?
Locally, run on the latest head:
the rs-dpp voting and state transition tests;
the rs-drive votes, query, verify, structure and initialization tests;
the rs-drive-abci masternode vote, voting event, processor and protocol upgrade tests, including the existing contested-poll suites;
the wasm32 builds of wasm-dpp, wasm-dpp2, wasm-sdk and wasm-drive-verify;
the verify-only rs-drive build;
clippy and fmt.
rs-dpp:
u32::MAX;StateErrordiscriminants;rs-drive:
grovedb-structure.jsonregenerated.rs-drive-abci:
Every yes/no vote in the tests goes through CheckTx first. That checks committed state stays untouched, and a vote CheckTx admits must pass at both levels.
Breaking Changes
Consensus: new
VotePollandVotevariants, a new state error, and new genesis trees, all at protocol version 14 (unreleased).Clients: yes/no polls share the
getVotePollsByEndDateindex with contested polls. A client built before this change decodes every entry as a contested-onlyVotePoll. Once a yes/no poll is open, any page whose range covers its end date fails to verify as a whole, DPNS contest listings included.getVotePollsByEndDateonly ever returns contested document resource polls.VotePoll::YesNoVotePollentries. This change's rs-sdk skips them inget_current_dpns_contests, and the wasm bindings expose the kind.Nothing in this change opens a yes/no poll, so the break starts with the first opener: moderation team challenges or charter amendments, after protocol version 14. SDKs, Dash Evo Tool and the mobile clients must upgrade before then.
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