Repository navigation
feat(platform)!: yes/no masternode vote poll kind with supermajority and minimum voting power - #4899
Conversation
|
Important Review skippedToo many files! This PR contains 172 files, which is 72 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.
|
|
🕓 Review not started yet because the new head is waiting for the 30-minute push debounce.
Commit fe6482b. 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-10-28T20:13:33.412Z |
|
🌳 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 |
…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>
#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>
Conflicts, both sides kept: - DRIVE_VERSION_V10 (added on both sides): compiled contract storage's DRIVE_CONTRACT_METHOD_VERSIONS_V5 next to the yes/no vote V4 and verify V4 tables. - DRIVE_ABCI_VALIDATION_VERSIONS_V11: contract update transform_into_action 1 next to masternode vote transform_into_action 1. - v15.rs: compiled contract storage keeps note 8; yes/no vote polls become note 9. - nullifier_batch_guard: base's validation hashes; the latest-version root and nine settled hashes re-pinned for compiled contracts plus the vote decisions trees. SYSTEM_LIMITS_V5 builds on V4, so it carries the yes/no limits and vote weights. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Waiting for bot review — thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
The protocol-15 yes/no voting implementation is broadly coherent and the prior consensus, storage, proof, and binding findings are addressed. Four non-blocking compatibility and pagination issues remain: empty end-date pages can still truncate SDK results, an existing JSON wire format changed, a shipped public document-method return type narrowed in place, and the new yes/no state query executes before its proof verifier is active. CI has no reported failures on this head, but the Rust workspace and several test checks are still running or queued.
🟡 4 suggestion(s)
1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 8: gpt-6.1-sol (agent: phase2-reviewer, role: tenderdash-consensus-security); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 13: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 14: gpt-6.1-sol (agent: phase2-reviewer, role: tenderdash-consensus-security); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — This is a large, intricate change that modifies consensus-critical voting rules, protocol-version activation and peer-facing serialization, persistent vote storage and cleanup, state-transition validation, and proofs across rs-dpp, rs-drive, and rs-drive-abci. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— tenderdash-consensus-security (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— tenderdash-consensus-security (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-dpp/src/voting/vote_choices/yes_no_abstain_vote_choice/mod.rs`:
- [SUGGESTION] packages/rs-dpp/src/voting/vote_choices/yes_no_abstain_vote_choice/mod.rs:26-35: Preserve the existing JSON wire names for yes/no vote choices
This change renames the existing enum variants from `YES`, `NO`, and `ABSTAIN` to idiomatic Rust names while retaining the unversioned `serde(rename_all = "camelCase")` representation. As a result, JSON and Value serialization changes from the established `"yES"`, `"nO"`, and `"aBSTAIN"` strings to `"yes"`, `"no"`, and `"abstain"`. The bincode discriminants are unchanged, but existing JSON/Value consumers and persisted representations can no longer round-trip the prior format. Preserve the old serde names, optionally accept the new spellings as aliases, or introduce an explicitly versioned wire representation.
In `packages/rs-dpp/src/data_contract/document_type/methods/mod.rs`:
- [SUGGESTION] packages/rs-dpp/src/data_contract/document_type/methods/mod.rs:1045-1070: Do not narrow the shipped v0 document-method return type in place
Both `DocumentTypeV0Methods::contested_vote_poll_for_document` and `contested_vote_poll_for_document_properties` change their public return type from `Result<Option<VotePoll>, ProtocolError>` to `Result<Option<ContestedDocumentResourceVotePoll>, ProtocolError>`, while the existing method-version slot remains v0 for all protocol versions. The implementation may return the same concrete poll today, but downstream Rust callers compiled against the public v0 trait API no longer type-check. Keep the v0 signature and add a compatibility-preserving helper or a new versioned method for the concrete return type.
In `packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs`:
- [SUGGESTION] packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs:116-145: Gate yes/no poll state query execution consistently with its proof verifier
`execute_with_proof` and `execute_no_proof` directly execute the new decisions-branch query for any `PlatformVersion`; unlike `verify_yes_no_vote_poll_state_proof`, neither consults an optional protocol-version method slot or returns `VersionNotActive` before protocol 15. On a pre-15 platform snapshot, the no-proof method can silently return an empty state and the proof method can generate a proof that the inactive verifier will reject. Add a versioned query slot and inactive dispatch, or explicitly reject pre-15 versions at both execution entry points.
In `packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs`:
- [SUGGESTION] packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs:494-502: Do not treat a page consumed entirely by empty end dates as exhaustion
(existing thread: https://github.com/dashpay/platform/pull/4899#discussion_r4341361723)
The loop still terminates when the verified page contains no timestamp groups. The underlying GroveDB query counts empty end-date trees toward its limit, while proof reconstruction drops those trees before producing `VotePollsGroupedByTimestamp`, so a page can be empty even though a later timestamp contains a live DPNS contest. Retrying once at `DEFAULT_QUERY_LIMIT` does not establish exhaustion when more empty trees remain, and the proof exposes no authenticated continuation cursor for the skipped trees. The SDK can therefore return a successful but incomplete contest map. Preserve an authenticated last-scanned end-date cursor, retain empty timestamp groups through verification, or return an explicit incomplete-enumeration error instead of breaking.
|
Bots are done — your move: post |
…n 15 YesNoVotePollStateDriveQuery::execute_with_proof and execute_no_proof read the decisions branch at any protocol version, while the verifier of their proof is not active before 15. They now check the new yes/no slot fetch_yes_no_vote_poll_state (None before 15, Some(0) in DRIVE_VOTE_METHOD_VERSIONS_V4) and answer VersionNotActive before 15, like every other yes/no method. The only server caller, the end-of-block tally, is itself not active before 15. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Waiting for bot review — thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
The head fixes the previously identified activation, proof-routing, cleanup, decoding, binding, and versioned-policy defects. Three issues remain: the new yes/no poll identity and persisted-key derivation is not selected through a platform-version slot, and the SDK can silently truncate results both for an empty page of end-date trees and for an explicit zero limit. CI has no reported code failure, but policy reconciliation remains pending and the Kotlin SDK job is queued.
🔴 1 blocking | 🟡 3 suggestion(s)
2 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 8: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — This large, intricate diff changes consensus-critical voting rules, protocol-version activation, peer-facing vote deserialization, persistent vote storage and cleanup, and state proofs in the rs-dpp, rs-drive, and rs-drive-abci implementations. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— 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-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs`:
- [BLOCKING] packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs:413-426: Version the yes/no poll identity and persisted-key derivation
`sha256_2_hash`, `specialized_balance_id`, and `unique_id` determine the yes/no poll's active-poll key, end-date index key, and prefunded-balance identifier, but they are ordinary unversioned methods. The protocol-15 implementation therefore has no `PlatformVersion`-selected identity generation that can be frozen while a later protocol changes the format; editing this code in place would address different persisted trees and balances during replay. Add an optional DPP voting method slot for the yes/no poll identity and dispatch this generation through the active platform version, preserving it for protocol 15.
In `packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs`:
- [SUGGESTION] packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs:481-482: Preserve the existing zero-limit behavior
An explicit `limit: Some(0)` now sets `query_limit` to zero, but the empty-page branch treats every limit below `DEFAULT_QUERY_LIMIT` as a retry and changes it to the default. The method can therefore return contests even though the caller requested zero results, whereas the previous implementation returned an empty map. Handle an explicit zero before entering the pagination loop.
- [SUGGESTION] packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs:494-503: Do not treat a page consumed entirely by empty end dates as exhaustion
(existing thread: https://github.com/dashpay/platform/pull/4899#discussion_r4341361723)
`VotePollsByEndDateDriveQuery` applies its limit while traversing timestamp trees, including empty trees. The proof verifier then drops those empty elements at `verify_vote_polls_by_end_date_proof_v0` when it evaluates `element?`, so a page can decode to an empty `VotePollsGroupedByTimestamp` even though later timestamps contain live DPNS contests. This loop retries only once at `DEFAULT_QUERY_LIMIT` and then breaks, returning an incomplete map without an error when enough empty trees precede a live contest. Preserve an authenticated continuation marker or empty timestamp keys, or return an explicit incomplete-enumeration error instead of treating the empty decoded page as EOF.
In `packages/rs-dpp/src/data_contract/document_type/methods/mod.rs`:
- [SUGGESTION] packages/rs-dpp/src/data_contract/document_type/methods/mod.rs:1045-1087: Do not narrow the shipped v0 document-method return type in place
(existing thread: https://github.com/dashpay/platform/pull/4899#discussion_r4400373228)
Both public `DocumentTypeV0Methods` methods now return `Result<Option<ContestedDocumentResourceVotePoll>, _>` while their existing method-version slot remains v0 for every protocol version. Downstream Rust callers that used the shipped `Result<Option<VotePoll>, _>` API no longer type-check, even though the implementation still produces the same contested poll. The `feat!` marker makes an intentional break possible, but it does not preserve the existing v0 API; either retain the old signatures and add a concrete helper, or document and explicitly version the Rust API migration.
|
Your move: thepastaclaw requested changes on this head; dismiss the review or push a fix; thepastaclaw left review threads unresolved; resolve them. |
get_current_dpns_contests reads an empty page at a small limit again at the default one, so an explicit limit of zero listed contests. A zero limit now answers an empty map without a request, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orm version A yes/no poll's id keys its trees and its end date index entry and names its prefunded balance. YesNoVotePoll::unique_id now dispatches on the dpp voting slot yes_no_vote_poll_id (None before protocol version 15, Some(0) from it), so a later id format is a new generation while 15 keeps addressing what it wrote. VotePoll::unique_id, VotePoll::specialized_balance_id, Vote::vote_poll_unique_id, Vote::specialized_balance_id, ResourceVoteV0::vote_poll_unique_id, ResolvedVote::specialized_balance_id, the yes/no poll path helpers and IdentityBasedVoteDriveQuery's path query take the platform version. A contested poll's id does not read it, and no yes/no poll or vote reaches the shipped generations that now pass it before 15. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Waiting for bot review — thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
v5.1-dev added protocol version 15 note 9 (token configuration updates on read-only contracts), so the yes/no vote polls note becomes note 10 and the table comments that name it follow. DRIVE_ABCI_VALIDATION_VERSIONS_V11 keeps both the batch advanced_structure 2 and the masternode vote transform_into_action 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Waiting for bot review — thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
|
Reviewed |
…et-escrow-contract v5.1-dev adds the yes/no vote polls (#4899) and the read-only token config update refusal (#5632). DRIVE_VERSION_V10 keeps both sides: the marketplace's methods with the yes/no vote and verify tables. v15 notes 9 and 10 are v5.1-dev's; the generated token decimals become note 11 and the marketplace state note 12. transition_to_version_15 creates the vote decisions trees and then the marketplace's escrow trees and contract. The shielded characterization roots are re-pinned for the merged genesis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#4899 took protocol version 15 note 10, so the single read is note 11. DRIVE_VERSION_V10 lists all three of its changes: compiled contract storage, the yes/no poll kind and the consume's single read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Basic explanation
What this does: Masternodes can already vote on one kind of question: which of several identities gets a contested name, such as a DPNS username more than one person wants. This adds a second kind of question they can vote on: a plain "yes or no?" poll, for example "should this moderation team be replaced?". A poll passes when the yes votes are more than a set share of the yes and no votes (two thirds, say) and enough voting power took part. This PR builds the voting machinery only: casting a vote, counting, closing the poll at its end date, recording the result, and proofs. Nothing opens such a poll yet; the moderation team challenge and charter amendment features will. It all activates with protocol version 15.
Value: Decentralized moderation teams (#4865) need the network to answer yes/no questions with a supermajority and a turnout floor, which the contested name poll cannot express. With this in place, each later feature only opens a poll and acts on its result, without new voting consensus code.
Risks: Medium. It is consensus code: from protocol version 15 the end of every block also closes due yes/no polls, and a mistake there fails the block on every node. Until something opens a yes/no poll that path never runs, and every yes/no vote is refused. At protocol version 14 and earlier nothing changes: a vote naming a yes/no poll is refused when it is decoded, as older binaries refuse it, and tests run a DPNS contest at 13 and at 14 to show it unchanged. Clients built before this change will fail to read
getVotePollsByEndDatepages once the first yes/no poll exists, so they must upgrade before a feature opens one (see Breaking Changes).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.Status (2026-10-20): this branch now sits on
v5.1-devand everything it adds activates at protocol version 15 (it was parked at 14). The open design questions are listed in the comment "Session notes: where this PR stands"; they are worth settling before protocol version 15 ships, since the poll's format freezes then.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 end date must come after the opening block and at most four weeks later (SystemLimits::max_yes_no_vote_poll_duration_ms), which also keeps it belowi64::MAX, past which the end date index would sort it before every block time. The opener writes no balance: the feature that opens the poll funds its prefunded balance, which pays for every vote, under the poll'sspecialized_balance_id. The poll's end date entry goes through the same writer as a contest's (add_vote_poll_end_date_query_operations). Openings in a batch follow the contract of document operations: earlier operations go in as the previous operations, so a poll the batch already opens is refused, a tree the batch already queues is queued once (an end date tree a contest queued keeps its contender's storage flags, and a contest after the opening adds only its entry), and a queued deletion of the end date's tree (a contest leaving its join-window end date) is cancelled.MasternodeVotecarrying aYesNoVoteputs the masternode's strength (masternode 1, evonode 4, as for contested polls; theSystemLimitsfieldsyes_no_vote_poll_masternode_vote_strengthandyes_no_vote_poll_evonode_vote_strength, which the share minimum's masternode list total reads too) 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 { voting_power }: 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_parameters(selected by the dpp voting slotyes_no_vote_poll_parameters) refuses 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, behind>= YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION(15) on the chain's initial protocol version) and the upgrade to protocol version 15 (transition_to_version_15) both call it, in the same order, so a chain born at 15 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 15 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 feature that opens the poll funds that balance; the opener writes none.YesNoVotePollNotAvailableForVotingError(code 40308,StateErrordiscriminant 176) 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 15"] --> 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, already finished" --> LO["a leftover end date entry: only the entry goes"] 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 v0 and contested clean-up v1, as at 14: end date entries, documents, votes, contenders, fund"] CL --> YC["then the yes/no clean-up, in its own batch: end date entries, the three vote trees (dropped flat), identity index entries, the rest of the fund to the processing pool"] HOOK --> YC LO --> YCWhy each clean-up removes its own end date entries. With two poll kinds under one end date, an end date's tree must go only once nothing of either kind is left under it, and deleting a non-empty tree fails the block. The contested end date removal v2 (#4996, protocol version 14) already decides that from what it reads under the end date, whatever the kind, so contested polls keep their clean-up (
clean_up_after_vote_polls_endv0, contested clean-up v1) at protocol version 15 unchanged.check_for_ended_vote_pollsv1 hands the yes/no polls it closed to the yes/no clean-up after the contested one, and that clean-up removes its own polls' entries through the same body (remove_vote_poll_ids_from_end_dates_operations_v2). Each clean-up applies its own batch, so the second reads what the first left: an end date holding a contest and a yes/no poll goes with the second batch.A poll listed twice. Two openings of one poll that cannot see each other (each built with no previous operations) would list it under two end dates. The poll closes at the first; when a later entry comes due for a poll that is already finished, only that entry is removed, rather than the block failing on a finished poll. The per-block limit spans every due end date, so the last end date a block reaches can be only partly processed; the read keeps its tree. 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.get_current_dpns_contestsskips yes/no entries and pages until a page comes back empty (the limit also counts empty end date trees, so a short page can still be cut, and an empty page is read again at the full page size before the listing ends). It never asks for a range that starts past the caller's end time, and refuses rather than drops an end date holding more polls than one page can list.How a yes/no vote looks on the wire (JSON). The minimum is internally tagged like every sum type; a share minimum is written
{ "$type": "fractionOfTotal", "numerator": 1, "denominator": 3, "rounding": "up" }:{ "$type": "yesNoVote", "$formatVersion": "0", "votePoll": { "resourcePath": ["wcHBwQ=="], "supermajorityNumerator": 2, "supermajorityDenominator": 3, "minimumVotingPower": { "$type": "absolute", "votingPower": 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 15 only. Protocol version 15 is unreleased, so its tables are amended in place:
DRIVE_VERSION_V10(protocol version 15 only, shared with compiled contract storage, which selects its contract methods) withDRIVE_VOTE_METHOD_VERSIONS_V4(new):insert.register_identity_vote1,remove_all_votes_given_by_identities1, everyyes_nomethodSome(0)(each anOptionalFeatureVersion,Nonein every earlier table, so opening, voting on and closing a yes/no poll are not active there),cleanup.remove_vote_poll_end_date_query_operationsSome(0), andcontested_resource_insert.add_vote_poll_end_date_query_operations1, which adds only the entry when the batch already queues the end date's tree with other storage flags (a contest and an opening sharing an end date), where 0 queued the tree twice. Contests in one batch share their owner and epoch, so their trees never differ in flags: for contests, 1 queues what 0 does.DRIVE_VERSION_V10also selectsDRIVE_VERIFY_METHOD_VERSIONS_V4(new): the yes/no proof verifiersSome(0).VOTING_VERSION_V3(dpp, new, protocol version 15 only): the yes/no poll's stored info, id, parameter rules and decision rulesSome(0).DRIVE_ABCI_METHOD_VERSIONS_V11: the yes/no poll's tally, record, clean-up and finish hookSome(0).DRIVE_ABCI_VALIDATION_VERSIONS_V11: masternode votetransform_into_action1.create_initial_state_structure4 creates the decisions trees behind>= YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION(15, infeature_initial_protocol_versions.rs, which the masternode vote'sactive_version_rangealso reads) on the chain's initial protocol version, and the newtransition_to_version_15rung creates them on an upgrading chain. Opening a poll also creates them when missing, for a chain that reached 15 before they existed.Every method slot the yes/no kind adds is an
OptionalFeatureVersion,None(not active) in every earlier table andSome(0)at 15, so each yes/no method refuses to run before 15 (should_activate_every_yes_no_vote_poll_method_at_protocol_version_15checks every protocol version's tables). Among them, the dpp voting slotsyes_no_vote_poll_parametersandyes_no_vote_poll_decisionselect the poll's parameter rules and its decision (required_voting_power,passes), so a later protocol version can change either without touching the generation 15 decides with.yes_no_vote_poll_idselects how a poll's id is derived (YesNoVotePoll::unique_id): the id keys the poll's trees and its end date entry and names its prefunded balance, so a later id format would be a new generation while 15 keeps addressing what it wrote. Every caller of a poll's id or balance id (VotePoll::unique_id,Vote::vote_poll_unique_id, thespecialized_balance_idmethods, the yes/no path helpers and the identity vote query) passes the platform version; a contested poll's id does not read it. The sevenSystemLimitsfields (five caps and the two vote weights) carry the same values in every table, since nothing reads them before 15;should_weigh_yes_no_votes_one_for_a_masternode_and_four_for_an_evonode_at_protocol_version_15pins the weights 15 tallies with. The new generations build on the shipped ones:transform_into_actionv1,register_identity_votev1 andremove_all_votes_given_by_identitiesv1 hand resource votes to their v0, and the yes/no clean-up reuses the contested end date removal and fund drain.check_for_ended_vote_pollsv1 and masternode votestatev1 (from #4907) are selected at both 14 and 15. They take their yes/no handling in place instead of through new v2 copies: no yes/no poll or vote can exist at 14, so it cannot run there. The next section lists every generation edited this way.In-place changes to shipped generations
Protocol version 14 is not live on mainnet yet, but
v5.0-devships it, so the generations it selects are treated as shipped here. Every edit below is unreachable at each protocol version up to 14 that selects the edited generation, or leaves its operations unchanged, and the edited lines say why. There are two roots of unreachability:YesNoVote, or aResourceVotewhose poll is aYesNoVotePoll) is active from protocol version 15 only (StateTransition::active_version_range). Before 15 it is refused at decode, unpaid, as a binary that cannot decode it refuses it, so no validator selected before 15 ever sees one.Drive::open_yes_no_vote_poll, which is not active before protocol version 15 (everyyes_nodrive method,open_yes_no_vote_pollincluded, isNonein every table before 15).transform_into_actionv0Vote::YesNoVoteand for a resource vote namingVotePoll::YesNoVotePollvalidate_statev0ResolvedVote::YesNoVotevalidate_statev1VotePollNotFoundError; finished: 40308)Drive::register_identity_votev0 (both functions)DRIVE_VOTE_METHOD_VERSIONS_V1toV3)ResolvedVote::YesNoVoteregister_identity_votedispatcherinsert.register_identity_voteinstead ofcontested_resource_insert.register_identity_votecheck_for_ended_vote_pollsv1finish_yes_no_vote_poll), or their leftover entry noted; the contested loop is as on base, and the yes/no clean-up runs after the contested onetransform_into_actionv0previous_vote_to_replace_v0andvoting_masternode_v0, which v1 also calls and which are marked shipped (v0 keeps its own 1/4 strength)clean_up_after_contested_resources_vote_polls_endv1empty_vote_poll_prefunded_balances_operations_v1, which the yes/no clean-up also callsremove_all_votes_given_by_identitiesv0identity_vote_entries_of_identities_v0, which v1 calls with the yes/no index pathremove_specific_vote_references_given_by_identityv0remove_identity_vote_references_operations_v0, which the yes/no reference removal also callsremove_contested_resource_vote_poll_end_date_query_operationsv2remove_vote_poll_ids_from_end_dates_operations_v2, which the yes/no clean-up also callscontested_vote_poll_for_document[_properties]v0 (dpp) and document createadvanced_structurev1ContestedDocumentResourceVotePollit always built instead of wrapping it inVotePoll; the caller matches it directlycreate_initial_state_structurev4insert_vote_decisions_treesbehind>= YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION(15) on the chain's initial protocol versionperform_events_on_first_block_of_protocol_changev0 (v1 and v2 run its body)VotePoll::YesNoVotePollarm intransition_to_version_8; thetransition_to_version_15rungprove_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 refusedverify_masternode_votedispatcherverify_masternode_yes_no_voteand a resource vote naming a yes/no poll is refusedadd_vote_poll_end_date_query_operationsv0 (v1 runs it)platform_versiontoVotePoll::unique_idadd_contested_document_for_contract_operationsv1platform_versiontoVotePoll::unique_idof the contest's pollmasternode_vote_transition0)platform_versiontoVote::specialized_balance_id/ResolvedVote::specialized_balance_idverify_masternode_votev0,prove_state_transitionv0 body andprove_multiple_state_transition_resultsv0platform_versionto the poll id and toIdentityBasedVoteDriveQuery::construct_path_queryAlso consensus-neutral up to 14: the
remove_all_votes_given_by_identitiesdispatcher now reads its own slot instead ofremove_specific_vote_references_given_by_identity, and the now unreadcontested_resource_insert.register_identity_voteslot is removed; both slots are 0 in every table up to 14, so the untouched v0 still runs there. Resolving aVotePollnow refuses a yes/no poll, which no path resolves (the end of block check closes it first, and a resource vote naming one is refused before resolution or does not decode). The new method slots in earlier tables (DRIVE_ABCI_METHOD_VERSIONS_V1toV10,DRIVE_VOTE_METHOD_VERSIONS_V1toV3,DRIVE_VERIFY_METHOD_VERSIONS_V1,VOTING_VERSION_V1andV2(dpp)) areNone, so the yes/no methods they select refuse there, and the newSYSTEM_LIMITS_V1toV4fields (whichSYSTEM_LIMITS_V5, protocol version 15's table, takes fromV4) are read only by yes/no code; no existing value changed.should_run_a_contest_unchanged_and_strip_yes_no_polls_at_protocol_version_13and..._14run a DPNS contest at each version through the generations above: the vote counts, the contender is awarded and the end date index empties. Both also show the two vote kinds naming a yes/no poll stripped before any validator runs, with the voter's nonce left unused. The end date clean-up tests (end_date_cleanup_tests, and the contests without locking whose end dates are left empty or shared) run at 14 and at the latest version.Before and after for the one behaviour change up to 14 (a
ResourceVotenaming a yes/no poll):SerializedObjectParsingError.StateTransitionNotActiveErrorconsensus error at 13 and at 14 (decode_raw_state_transitions0 and 1). Every one of these outcomes keeps it out of a proposal and rejects a proposal that carries it.How Has This Been Tested?
Locally, after the merge of
v5.1-devand the move to protocol version 15:rs-dpp: the voting tests, the version gate and the frozen error discriminants (38 tests);
rs-drive: the votes, verify, query, structure, initialization and unsigned-key migration tests (245 tests), with
grovedb-structure.jsonregenerated;rs-drive-abci: the masternode vote tests (yes/no, end date clean-up, charter elections, contests without locking), the voting event tests and the protocol upgrade tests (186 tests);
cargo check --testsof platform-version, dpp, drive, drive-abci, dash-sdk and drive-proof-verifier, and a wasm32cargo checkof wasm-dpp, wasm-dpp2, wasm-sdk and wasm-drive-verify;the verify-only rs-drive build, clippy on the touched crates and
cargo fmt --all.rs-dpp:
u32::MAX;StateErrordiscriminants;rs-drive:
grovedb-structure.jsonregenerated.rs-drive-abci:
Every yes/no vote in the tests goes through CheckTx first, which checks committed state stays untouched. A vote the block accepts must pass CheckTx at both levels, and a vote the block refuses must be refused by CheckTx's first check (except a refusal for a missing or short fund: CheckTx does not run the prefunded balance pre-check, for contested votes either).
Added after the review:
vote_poll_end_dates, which sees an emptied end date tree; each close is its own block.VotePollandVotethroughtoJSON/fromJSON/toObject/fromObject, thekindandyesNoChoicegetters and the refused contested getters.Added after the second review:
remove_all_votes_given_by_identitiesv0 tests run at protocol version 14.Added after the third review:
i64::MAXis refused; openings in one batch follow the previous-operations contract, and a contest's end date tree queued earlier in the batch keeps its contender's flags while the poll adds only its entry.Added after the fourth review:
kindequals the$typeoftoJSON()for both poll kinds.Added for the review of the fourth round's head:
ResourceVoteV0::try_newrefuses a yes/no poll; the wasm-dpp2VoteandResourceVoteconstructors andpollsetters and wasm-sdk'smasternodeVotebuild resource votes through it (specs check the refusal).VersionNotActiveat protocol version 14.Added for the final validation:
Nonebefore 15 andSome(0)from it in every protocol version's tables, and the vote weights are 1 and 4 at 15.Added for the review of the merge:
execute_with_proof,execute_no_proof) answersVersionNotActiveat protocol version 14 (slotfetch_yes_no_vote_poll_state).Added for the review of the merge's head:
unique_idandspecialized_balance_idanswer not active at protocol version 14 (slotyes_no_vote_poll_id), and the id is unchanged at 15.yes_no_vote_poll_idis among the slots checkedNonebefore 15 andSome(0)from it.get_current_dpns_contestswith a limit of zero answers an empty map without sending a request, as before this change (an empty page at a small limit is read again at the default one, which a zero limit no longer reaches).Re-run after versioning the yes/no poll id: platform-version yes/no gating (2 tests), rs-dpp voting (104), rs-drive votes (106), voting verifiers (20), proofs (64) and the masternode vote conversion (5), rs-drive-abci masternode vote (105), voting events (10), protocol upgrade (45), processor (5) and offset controls (5), rs-sdk contested queries (12) and the vote poll fetch tests (15); clippy on the touched crates,
cargo fmt --alland the verify-only rs-drive build.Re-run after merging v5.1-dev 60cc5f4 (compiled contract storage): platform-version (27 tests), rs-dpp voting (105), rs-drive votes, verify and initialization (136) and
deterministic_root_hash, rs-drive-abci yes/no, masternode vote, protocol upgrade and the nullifier batch guard pins (182).Re-run after merging v5.1-dev 0d91c13 and the review of the fourth round's head: rs-dpp voting (108 tests), rs-drive votes, queries, structure, fees and the contract migration (257), rs-drive-abci masternode vote, voting event, protocol upgrade, contests and DPNS (303), rs-sdk contested queries with mocks (11); clippy on the touched crates,
cargo fmt --alland a wasm32 check of wasm-dpp2 and wasm-sdk. The protocol version 13 stripped-vote test now expects the unpaid not-active error that v5.1-dev'sdecode_raw_state_transitionsv0 reports.Re-run after the fourth review: rs-dpp voting (107 tests), rs-drive votes, queries, structure, fees and pending operations (246), rs-drive-abci masternode vote, voting event and protocol upgrade (187), contests (120), rs-sdk paging (3); clippy on the touched crates,
cargo fmt --alland a wasm32 check of wasm-dpp2.Re-run after the third review: rs-dpp voting (108 tests), rs-drive votes, verify, query, structure and the document counters (304), rs-drive-abci masternode vote, voting event, protocol upgrade and contests (269), the CheckTx-guard users (107), rs-sdk paging (3); clippy on the touched crates,
cargo fmt --all, the verify-only rs-drive build and a wasm32 check of wasm-dpp2, wasm-sdk and wasm-drive-verify.Re-run after the second review: rs-dpp voting (108 tests), rs-drive votes, verify, query and structure (273), rs-drive-abci masternode vote, voting event and protocol upgrade (191), contests and the CheckTx-guard users (245), rs-sdk paging (3); clippy on the touched crates,
cargo fmt --all, the verify-only rs-drive build and a wasm32 check of wasm-dpp2, wasm-sdk and wasm-drive-verify.Breaking Changes
Consensus: new
VotePollandVotevariants, a new state error (40308,StateErrordiscriminant 176), and new trees underVotes/d, all at protocol version 15 (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.Rust API:
DocumentTypeV0Methods::contested_vote_poll_for_documentandcontested_vote_poll_for_document_propertiesreturnOption<ContestedDocumentResourceVotePoll>instead ofOption<VotePoll>. They always built a contested poll, and withVotePollgaining a yes/no variant the narrower type spares callers an arm that cannot occur. A caller wrapping the result keeps working with.map(VotePoll::from).VotePoll::unique_id,VotePoll::specialized_balance_id,Vote::vote_poll_unique_id,Vote::specialized_balance_idandResourceVoteV0::vote_poll_unique_id(dpp), and Drive'sResolvedVote::specialized_balance_idandIdentityBasedVoteDriveQuery::construct_path_query, take a&PlatformVersion, because a yes/no poll's id is derived as the version selects. A contested poll's id is unchanged; callers pass the version they run (sdk.version()in the SDK).Nothing in this change opens a yes/no poll, so the break starts with the first opener: moderation team challenges or charter amendments, at protocol version 15 or later. 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
PR Hygiene ·
fe6482b/skip-botsproceeds without the ones not yet reported/self-revieweddpp— you own itrs-drive-abci— you own itrs-drive— you own itrust-sdk(packages/rs-sdk/src/platform/documents/contest_fund.rs,packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs,packages/rs-sdk/src/platform/transition/vote.rsand 4 more) — lklimek or shumkovjs-wasm-sdk(packages/wasm-sdk/src/state_transitions/identity.rs) — shumkovWhen every merge requirement is met, the
PR Hygienecheck passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.