From db5cc3270aad0171a423ee5f96c153b9ad70357b Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 23 Sep 2026 08:37:17 +0700 Subject: [PATCH 01/18] feat(platform)!: yes/no masternode vote poll kind with supermajority 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 --- book/src/error-handling/error-codes.md | 2 +- packages/rs-dpp/src/errors/consensus/codes.rs | 1 + .../src/errors/consensus/state/state_error.rs | 29 +- .../src/errors/consensus/state/voting/mod.rs | 1 + ...ote_poll_not_available_for_voting_error.rs | 53 + .../src/serialization/json/safe_fields.rs | 4 + packages/rs-dpp/src/state_transition/mod.rs | 57 +- .../yes_no_abstain_vote_choice/mod.rs | 79 +- .../src/voting/vote_info_storage/mod.rs | 1 + .../yes_no_vote_poll_stored_info/mod.rs | 225 ++++ .../yes_no_vote_poll_stored_info/v0/mod.rs | 54 + packages/rs-dpp/src/voting/vote_polls/mod.rs | 11 + .../voting/vote_polls/yes_no_vote_poll/mod.rs | 378 ++++++ packages/rs-dpp/src/voting/votes/mod.rs | 10 + .../voting/votes/yes_no_vote/accessors/mod.rs | 26 + .../votes/yes_no_vote/accessors/v0/mod.rs | 14 + .../src/voting/votes/yes_no_vote/mod.rs | 114 ++ .../src/voting/votes/yes_no_vote/v0/mod.rs | 45 + .../v0/mod.rs | 77 ++ .../check_for_ended_vote_polls/v0/mod.rs | 11 + .../check_for_ended_vote_polls/v1/mod.rs | 48 +- .../mod.rs | 10 +- .../v2/mod.rs | 203 ++++ .../clean_up_after_vote_polls_end/mod.rs | 10 +- .../clean_up_after_vote_polls_end/v0/mod.rs | 8 +- .../clean_up_after_vote_polls_end/v1/mod.rs | 99 ++ .../mod.rs | 52 + .../v0/mod.rs | 105 ++ .../mod.rs | 48 + .../v0/mod.rs | 53 + .../execution/platform_events/voting/mod.rs | 4 + .../on_yes_no_vote_poll_finished/mod.rs | 48 + .../on_yes_no_vote_poll_finished/v0/mod.rs | 28 + .../tally_votes_for_yes_no_vote_poll/mod.rs | 37 + .../v0/mod.rs | 27 + .../advanced_structure_v1/mod.rs | 8 + .../state_transitions/masternode_vote/mod.rs | 8 +- .../masternode_vote/state/v0/mod.rs | 10 + .../masternode_vote/state/v1/mod.rs | 42 + .../transform_into_action/mod.rs | 1 + .../transform_into_action/v0/mod.rs | 11 + .../transform_into_action/v1/mod.rs | 232 ++++ .../masternode_vote/yes_no_tests.rs | 1069 +++++++++++++++++ .../verify_state_transitions.rs | 57 +- packages/rs-drive/grovedb-structure.json | 283 ++++- .../rs-drive/src/drive/votes/cleanup/mod.rs | 3 + .../mod.rs | 13 +- .../v1/mod.rs | 234 ++++ .../mod.rs | 44 + .../v0/mod.rs | 93 ++ .../mod.rs | 47 + .../v0/mod.rs | 58 + .../mod.rs | 43 + .../v0/mod.rs | 35 + .../mod.rs | 41 + .../v0/mod.rs | 75 ++ .../fetch/fetch_identity_yes_no_vote/mod.rs | 45 + .../fetch_identity_yes_no_vote/v0/mod.rs | 48 + .../fetch_yes_no_vote_poll_stored_info/mod.rs | 36 + .../v0/mod.rs | 35 + .../rs-drive/src/drive/votes/fetch/mod.rs | 3 + .../rs-drive/src/drive/votes/insert/mod.rs | 1 + .../insert/register_identity_vote/mod.rs | 22 +- .../insert/register_identity_vote/v0/mod.rs | 13 + .../insert/register_identity_vote/v1/mod.rs | 103 ++ .../mod.rs | 68 ++ .../v0/mod.rs | 53 + .../src/drive/votes/insert/yes_no/mod.rs | 3 + .../yes_no/open_yes_no_vote_poll/mod.rs | 85 ++ .../yes_no/open_yes_no_vote_poll/v0/mod.rs | 118 ++ .../register_yes_no_identity_vote/mod.rs | 79 ++ .../register_yes_no_identity_vote/v0/mod.rs | 135 +++ packages/rs-drive/src/drive/votes/mod.rs | 39 + packages/rs-drive/src/drive/votes/paths.rs | 145 ++- .../drive/votes/resolved/vote_polls/mod.rs | 20 + .../votes/resolved/vote_polls/resolve.rs | 6 + .../src/drive/votes/resolved/votes/mod.rs | 30 +- .../src/drive/votes/resolved/votes/resolve.rs | 7 + .../votes/resolved_yes_no_vote/mod.rs | 52 + .../setup/insert_vote_decisions_trees/mod.rs | 79 ++ .../rs-drive/src/drive/votes/setup/mod.rs | 1 + .../mod.rs | 4 +- .../v1/mod.rs | 41 + .../src/drive/votes/storage_form/mod.rs | 3 + .../storage_form/vote_storage_form/mod.rs | 7 +- .../yes_no_vote_reference_storage_form/mod.rs | 43 + .../rs-drive/src/drive/votes/structure.rs | 154 ++- .../prove/prove_state_transition/v0/mod.rs | 30 +- packages/rs-drive/src/query/mod.rs | 4 + packages/rs-drive/src/query/vote_query.rs | 20 +- .../src/query/yes_no_vote_poll_state_query.rs | 286 +++++ .../identity/masternode_vote_transition.rs | 2 + packages/rs-drive/src/structure/tests.rs | 47 + .../v0/mod.rs | 181 ++- packages/rs-drive/src/verify/voting/mod.rs | 2 + .../verify_masternode_yes_no_vote/mod.rs | 154 +++ .../verify_masternode_yes_no_vote/v0/mod.rs | 74 ++ .../mod.rs | 33 + .../v0/mod.rs | 28 + .../dpp_versions/dpp_voting_versions/mod.rs | 2 + .../dpp_versions/dpp_voting_versions/v1.rs | 1 + .../dpp_versions/dpp_voting_versions/v2.rs | 1 + .../drive_abci_method_versions/mod.rs | 6 + .../drive_abci_method_versions/v1.rs | 4 + .../drive_abci_method_versions/v10.rs | 10 +- .../drive_abci_method_versions/v2.rs | 4 + .../drive_abci_method_versions/v3.rs | 4 + .../drive_abci_method_versions/v4.rs | 4 + .../drive_abci_method_versions/v5.rs | 4 + .../drive_abci_method_versions/v6.rs | 4 + .../drive_abci_method_versions/v7.rs | 4 + .../drive_abci_method_versions/v8.rs | 4 + .../drive_abci_method_versions/v9.rs | 4 + .../drive_abci_validation_versions/v10.rs | 4 +- .../drive_verify_method_versions/mod.rs | 4 + .../drive_verify_method_versions/v1.rs | 2 + .../drive_vote_method_versions/mod.rs | 22 + .../drive_vote_method_versions/v1.rs | 14 +- .../drive_vote_method_versions/v2.rs | 14 +- .../drive_vote_method_versions/v3.rs | 33 + .../src/version/drive_versions/v9.rs | 4 +- .../src/version/mocks/v2_test.rs | 2 + .../src/version/mocks/v3_test.rs | 4 + .../src/version/system_limits/mod.rs | 6 + .../src/version/system_limits/v1.rs | 2 + .../src/version/system_limits/v2.rs | 2 + .../src/version/system_limits/v3.rs | 2 + .../src/version/system_limits/v4.rs | 2 + .../rs-platform-version/src/version/v14.rs | 6 +- .../dpns_usernames/contested_queries.rs | 7 +- .../rs-sdk/src/platform/transition/vote.rs | 4 +- .../src/errors/consensus/consensus_error.rs | 4 + .../masternode_vote_transition/mod.rs | 2 + packages/wasm-dpp2/src/voting/vote.rs | 57 +- packages/wasm-dpp2/src/voting/vote_poll.rs | 54 +- 135 files changed, 6868 insertions(+), 157 deletions(-) create mode 100644 packages/rs-dpp/src/errors/consensus/state/voting/yes_no_vote_poll_not_available_for_voting_error.rs create mode 100644 packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs create mode 100644 packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/v0/mod.rs create mode 100644 packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs create mode 100644 packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/mod.rs create mode 100644 packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/v0/mod.rs create mode 100644 packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs create mode 100644 packages/rs-dpp/src/voting/votes/yes_no_vote/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v2/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs create mode 100644 packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/insert/register_identity_vote/v1/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/insert/yes_no/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/v1/mod.rs create mode 100644 packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs create mode 100644 packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs create mode 100644 packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs create mode 100644 packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs create mode 100644 packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs create mode 100644 packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/v0/mod.rs create mode 100644 packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs diff --git a/book/src/error-handling/error-codes.md b/book/src/error-handling/error-codes.md index 27e6c5f57c4..6d80d14d7a0 100644 --- a/book/src/error-handling/error-codes.md +++ b/book/src/error-handling/error-codes.md @@ -109,7 +109,7 @@ The fee category currently has a single code. The 30000 range is reserved for fu | 40000-40009 | Data Contract | `DataContractAlreadyPresentError` (40000), `DataContractIsReadonlyError` (40001), `DataContractNotFoundError` (40008) | | 40100-40134 | Documents | `DocumentAlreadyPresentError` (40100), `DocumentNotFoundError` (40101), `DuplicateUniqueIndexError` (40105), `DocumentActionFeeAgreementNotSetError` (40132), `DocumentActionFeeAgreementMismatchError` (40133), `DocumentActionFeeMultiplierNotToleratedError` (40134) | | 40200-40217 | Identity | `IdentityAlreadyExistsError` (40200), `InvalidIdentityRevisionError` (40203), `IdentityInsufficientBalanceError` (40210) | -| 40300-40307 | Voting | `MasternodeNotFoundError` (40300), `MasternodeVoteAlreadyPresentError` (40304), `VoteChoiceNotAllowedForVotePollError` (40307) | +| 40300-40308 | Voting | `MasternodeNotFoundError` (40300), `MasternodeVoteAlreadyPresentError` (40304), `VoteChoiceNotAllowedForVotePollError` (40307), `YesNoVotePollNotAvailableForVotingError` (40308) | | 40400-40401 | Prefunded Balances | `PrefundedSpecializedBalanceInsufficientError` (40400) | | 40500-40502 | Data Triggers | `DataTriggerConditionError` (40500), `DataTriggerExecutionError` (40501) | | 40600-40603 | Addresses | `AddressDoesNotExistError` (40600), `AddressNotEnoughFundsError` (40601) | diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index 9e5f25d7bde..db5f32d3981 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -396,6 +396,7 @@ impl ErrorWithCode for StateError { Self::MasternodeIncorrectVotingAddressError(_) => 40305, Self::MasternodeIncorrectVoterIdentityIdError(_) => 40306, Self::VoteChoiceNotAllowedForVotePollError(_) => 40307, + Self::YesNoVotePollNotAvailableForVotingError(_) => 40308, // Prefunded specialized balances Errors: 40400-40499 Self::PrefundedSpecializedBalanceInsufficientError(_) => 40400, diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index bad77898bd2..cecee4d0c35 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -103,6 +103,7 @@ use crate::consensus::state::voting::masternode_voted_too_many_times::Masternode use crate::consensus::state::voting::vote_poll_not_available_for_voting_error::VotePollNotAvailableForVotingError; use crate::consensus::state::voting::vote_choice_not_allowed_for_vote_poll_error::VoteChoiceNotAllowedForVotePollError; use crate::consensus::state::voting::vote_poll_not_found_error::VotePollNotFoundError; +use crate::consensus::state::voting::yes_no_vote_poll_not_available_for_voting_error::YesNoVotePollNotAvailableForVotingError; use super::document::document_timestamps_are_equal_error::DocumentTimestampsAreEqualError; @@ -592,6 +593,10 @@ pub enum StateError { // Requirements on a referenced identity key (protocol version 14). #[error(transparent)] ReferencedIdentityKeyRequirementNotMetError(ReferencedIdentityKeyRequirementNotMetError), + + // Yes/no vote polls (protocol version 14). + #[error(transparent)] + YesNoVotePollNotAvailableForVotingError(YesNoVotePollNotAvailableForVotingError), } impl From for ConsensusError { @@ -603,6 +608,7 @@ impl From for ConsensusError { #[cfg(test)] mod tests { use super::*; + use crate::block::block_info::BlockInfo; use crate::consensus::state::contract_moderation::ContractModerationCounterpartyRole; use crate::consensus::state::identity::identity_public_key_limit_not_set_error::KeyLimit; use crate::data_contract::config::moderation::ContractModerationList; @@ -614,9 +620,11 @@ mod tests { }; use crate::tokens::gas_fees_paid_by::GasFeesPaidBy; use crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; + use crate::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStatus; use crate::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; + use crate::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use crate::voting::vote_polls::VotePoll; - use platform_value::Identifier; + use platform_value::{BinaryData, Identifier}; /// `StateError` is encoded by variant position, so inserting a variant /// anywhere but the end silently reassigns the discriminant of every @@ -1067,7 +1075,7 @@ mod tests { )), 140 ); - // Elected moderation teams (protocol version 14): the tail of the enum. + // Elected moderation teams (protocol version 14). assert_eq!( discriminant_of(StateError::ContractModeratedDocumentTypeNotYetUsableError( ContractModeratedDocumentTypeNotYetUsableError::new(group_id, "post".to_string()) @@ -1103,7 +1111,7 @@ mod tests { )), 143 ); - // Requirements on a referenced identity key (protocol version 14): the tail of the enum. + // Requirements on a referenced identity key (protocol version 14). assert_eq!( discriminant_of(StateError::ReferencedIdentityKeyRequirementNotMetError( ReferencedIdentityKeyRequirementNotMetError::new( @@ -1118,5 +1126,20 @@ mod tests { )), 144 ); + // Yes/no vote polls (protocol version 14): the tail of the enum. + assert_eq!( + discriminant_of(StateError::YesNoVotePollNotAvailableForVotingError( + YesNoVotePollNotAvailableForVotingError::new( + YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![1; 32])], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: 400, + }, + YesNoVotePollStatus::Started(BlockInfo::default()) + ) + )), + 145 + ); } } diff --git a/packages/rs-dpp/src/errors/consensus/state/voting/mod.rs b/packages/rs-dpp/src/errors/consensus/state/voting/mod.rs index 0414bd7656a..35488d89dc3 100644 --- a/packages/rs-dpp/src/errors/consensus/state/voting/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/state/voting/mod.rs @@ -6,3 +6,4 @@ pub mod masternode_voted_too_many_times; pub mod vote_choice_not_allowed_for_vote_poll_error; pub mod vote_poll_not_available_for_voting_error; pub mod vote_poll_not_found_error; +pub mod yes_no_vote_poll_not_available_for_voting_error; diff --git a/packages/rs-dpp/src/errors/consensus/state/voting/yes_no_vote_poll_not_available_for_voting_error.rs b/packages/rs-dpp/src/errors/consensus/state/voting/yes_no_vote_poll_not_available_for_voting_error.rs new file mode 100644 index 00000000000..47e592f2b84 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/state/voting/yes_no_vote_poll_not_available_for_voting_error.rs @@ -0,0 +1,53 @@ +use crate::consensus::state::state_error::StateError; +use crate::consensus::ConsensusError; +use crate::errors::ProtocolError; +use crate::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStatus; +use crate::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, +}; +use thiserror::Error; + +/// A vote on a yes/no poll that exists but is no longer taking votes (protocol version 14). +#[derive( + Error, + Debug, + Clone, + PartialEq, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + DecodeUntrusted, +)] +#[error("yes/no vote poll {vote_poll} is not available for voting: {status}")] +#[platform_serialize(unversioned)] +pub struct YesNoVotePollNotAvailableForVotingError { + /* + DO NOT CHANGE ORDER OF FIELDS WITHOUT INTRODUCING OF NEW VERSION + */ + vote_poll: YesNoVotePoll, + status: YesNoVotePollStatus, +} + +impl YesNoVotePollNotAvailableForVotingError { + pub fn new(vote_poll: YesNoVotePoll, status: YesNoVotePollStatus) -> Self { + Self { vote_poll, status } + } + + pub fn vote_poll(&self) -> &YesNoVotePoll { + &self.vote_poll + } + + pub fn status(&self) -> &YesNoVotePollStatus { + &self.status + } +} + +impl From for ConsensusError { + fn from(err: YesNoVotePollNotAvailableForVotingError) -> Self { + Self::StateError(StateError::YesNoVotePollNotAvailableForVotingError(err)) + } +} diff --git a/packages/rs-dpp/src/serialization/json/safe_fields.rs b/packages/rs-dpp/src/serialization/json/safe_fields.rs index 149ba871c1f..bd3eb833301 100644 --- a/packages/rs-dpp/src/serialization/json/safe_fields.rs +++ b/packages/rs-dpp/src/serialization/json/safe_fields.rs @@ -140,6 +140,10 @@ impl JsonSafeFields { } impl JsonSafeFields for crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice {} +impl JsonSafeFields + for crate::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice +{ +} impl JsonSafeFields for crate::group::action_event::GroupActionEvent {} // TokenEvent contains u64 aliases (TokenAmount, Credits) in tuple variants that // `#[json_safe_fields]` can't auto-annotate. Developer takes responsibility for diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 3c338489596..2539d453a0e 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -160,6 +160,7 @@ use crate::state_transition::identity_update_transition::accessors::IdentityUpda use crate::state_transition::identity_update_transition::{ IdentityUpdateTransition, IdentityUpdateTransitionSignable, }; +use crate::state_transition::masternode_vote_transition::accessors::MasternodeVoteTransitionAccessorsV0; use crate::state_transition::masternode_vote_transition::MasternodeVoteTransition; use crate::state_transition::masternode_vote_transition::MasternodeVoteTransitionSignable; use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; @@ -181,6 +182,7 @@ use crate::state_transition::state_transitions::document::batch_transition::meth use crate::state_transition::unshield_transition::{ UnshieldTransition, UnshieldTransitionSignable, }; +use crate::voting::votes::Vote; use state_transitions::document::batch_transition::batched_transition::token_transition::TokenTransition; pub use state_transitions::*; @@ -1046,8 +1048,14 @@ impl StateTransition { } StateTransition::IdentityTopUp(_) | StateTransition::IdentityCreditWithdrawal(_) - | StateTransition::IdentityCreditTransfer(_) - | StateTransition::MasternodeVote(_) => ALL_VERSIONS, + | StateTransition::IdentityCreditTransfer(_) => ALL_VERSIONS, + // A vote on a yes/no poll exists from protocol version 14. Binaries from before it + // cannot decode one, so an earlier version rejects it without charging, exactly as + // they do. + StateTransition::MasternodeVote(masternode_vote) => match masternode_vote.vote() { + Vote::ResourceVote(_) => ALL_VERSIONS, + Vote::YesNoVote(_) => 14..=LATEST_VERSION, + }, StateTransition::IdentityCreditTransferToAddresses(_) | StateTransition::IdentityTopUpFromAddresses(_) | StateTransition::AddressFundsTransfer(_) @@ -2543,6 +2551,51 @@ mod tests { assert_eq!(sample_withdrawal_st().active_version_range(), ALL_VERSIONS); } + #[test] + fn test_active_version_range_masternode_vote_on_a_yes_no_poll_starts_at_14() { + use crate::serialization::PlatformSerializable; + use crate::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; + use crate::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; + use crate::voting::votes::yes_no_vote::v0::YesNoVoteV0; + use crate::voting::votes::yes_no_vote::YesNoVote; + + let v0 = MasternodeVoteTransitionV0 { + pro_tx_hash: Identifier::from([3u8; 32]), + voter_identity_id: Identifier::from([4u8; 32]), + vote: Vote::YesNoVote(YesNoVote::V0(YesNoVoteV0 { + vote_poll: YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![7u8; 32])], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: 400, + }, + vote_choice: YesNoAbstainVoteChoice::Yes, + })), + nonce: 2, + signature_public_key_id: 5, + signature: BinaryData::new(vec![9u8; 10]), + }; + let state_transition = StateTransition::MasternodeVote(MasternodeVoteTransition::V0(v0)); + assert_eq!(state_transition.active_version_range(), 14..=LATEST_VERSION); + + let bytes = state_transition + .serialize_to_bytes() + .expect("expected to serialize the vote"); + let version_13 = PlatformVersion::get(13).expect("protocol version 13"); + assert!(matches!( + StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_13), + Err(ProtocolError::StateTransitionError( + StateTransitionIsNotActiveError { .. } + )) + )); + let version_14 = PlatformVersion::get(14).expect("protocol version 14"); + assert_eq!( + StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_14) + .expect("expected the vote to be active at 14"), + state_transition + ); + } + #[test] fn test_unique_identifiers_non_empty() { let ids = sample_transfer_st().unique_identifiers(); diff --git a/packages/rs-dpp/src/voting/vote_choices/yes_no_abstain_vote_choice/mod.rs b/packages/rs-dpp/src/voting/vote_choices/yes_no_abstain_vote_choice/mod.rs index 78af1ddccee..95a835c94ec 100644 --- a/packages/rs-dpp/src/voting/vote_choices/yes_no_abstain_vote_choice/mod.rs +++ b/packages/rs-dpp/src/voting/vote_choices/yes_no_abstain_vote_choice/mod.rs @@ -1,18 +1,53 @@ use bincode::{Decode, DecodeUntrusted, Encode}; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +use std::fmt; -#[derive(Debug, Clone, Copy, Encode, Decode, PartialEq, Default, DecodeUntrusted)] +/// The answer a masternode gives a yes/no vote poll. Abstaining counts towards nothing: the +/// poll's supermajority and minimum are measured over yes plus no only. +#[derive( + Debug, + Clone, + Copy, + Encode, + Decode, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Default, + DecodeUntrusted, +)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), serde(rename_all = "camelCase") )] pub enum YesNoAbstainVoteChoice { - YES, - NO, + Yes, + No, #[default] - ABSTAIN, + Abstain, +} + +impl YesNoAbstainVoteChoice { + /// Every choice, in tree key order. + pub const ALL: [YesNoAbstainVoteChoice; 3] = [ + YesNoAbstainVoteChoice::Yes, + YesNoAbstainVoteChoice::No, + YesNoAbstainVoteChoice::Abstain, + ]; +} + +impl fmt::Display for YesNoAbstainVoteChoice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + YesNoAbstainVoteChoice::Yes => write!(f, "Yes"), + YesNoAbstainVoteChoice::No => write!(f, "No"), + YesNoAbstainVoteChoice::Abstain => write!(f, "Abstain"), + } + } } // --- canonical conversion trait impls (unification pass 1) --- @@ -33,23 +68,15 @@ mod json_convertible_tests_yesnoabstainvotechoice { use platform_value::platform_value; use serde_json::json; - // `YesNoAbstainVoteChoice` is a unit-only enum with `rename_all = "camelCase"`, - // so each variant serializes as a plain string: `"yes"` / `"no"` / `"abstain"`. - - // Surprise wire shape: the variants are SCREAMING_CASE in source - // (`YES`/`NO`/`ABSTAIN`) and the type carries `rename_all = "camelCase"`. - // serde's camelCase rule lowercases the FIRST letter only, leaving the - // rest as-is — so the wire emits `"yES"` / `"nO"` / `"aBSTAIN"` rather - // than the lowercase-clean strings a casual reader would expect. These - // tests pin that behaviour so a future "looks-like-a-typo" rename to - // lowercase doesn't silently change the on-the-wire format. + // A unit-only enum with `rename_all = "camelCase"`: each variant is a plain lowercase + // string on the wire. #[test] fn json_round_trip_yes() { use crate::serialization::JsonConvertible; - let original = YesNoAbstainVoteChoice::YES; + let original = YesNoAbstainVoteChoice::Yes; let json = original.to_json().expect("to_json"); - assert_eq!(json, json!("yES")); + assert_eq!(json, json!("yes")); let recovered = YesNoAbstainVoteChoice::from_json(json).expect("from_json"); assert_eq!(original, recovered); } @@ -57,9 +84,9 @@ mod json_convertible_tests_yesnoabstainvotechoice { #[test] fn json_round_trip_no() { use crate::serialization::JsonConvertible; - let original = YesNoAbstainVoteChoice::NO; + let original = YesNoAbstainVoteChoice::No; let json = original.to_json().expect("to_json"); - assert_eq!(json, json!("nO")); + assert_eq!(json, json!("no")); let recovered = YesNoAbstainVoteChoice::from_json(json).expect("from_json"); assert_eq!(original, recovered); } @@ -67,9 +94,9 @@ mod json_convertible_tests_yesnoabstainvotechoice { #[test] fn json_round_trip_abstain() { use crate::serialization::JsonConvertible; - let original = YesNoAbstainVoteChoice::ABSTAIN; + let original = YesNoAbstainVoteChoice::Abstain; let json = original.to_json().expect("to_json"); - assert_eq!(json, json!("aBSTAIN")); + assert_eq!(json, json!("abstain")); let recovered = YesNoAbstainVoteChoice::from_json(json).expect("from_json"); assert_eq!(original, recovered); } @@ -77,9 +104,9 @@ mod json_convertible_tests_yesnoabstainvotechoice { #[test] fn value_round_trip_yes() { use crate::serialization::ValueConvertible; - let original = YesNoAbstainVoteChoice::YES; + let original = YesNoAbstainVoteChoice::Yes; let value = original.to_object().expect("to_object"); - assert_eq!(value, platform_value!("yES")); + assert_eq!(value, platform_value!("yes")); let recovered = YesNoAbstainVoteChoice::from_object(value).expect("from_object"); assert_eq!(original, recovered); } @@ -87,9 +114,9 @@ mod json_convertible_tests_yesnoabstainvotechoice { #[test] fn value_round_trip_no() { use crate::serialization::ValueConvertible; - let original = YesNoAbstainVoteChoice::NO; + let original = YesNoAbstainVoteChoice::No; let value = original.to_object().expect("to_object"); - assert_eq!(value, platform_value!("nO")); + assert_eq!(value, platform_value!("no")); let recovered = YesNoAbstainVoteChoice::from_object(value).expect("from_object"); assert_eq!(original, recovered); } @@ -97,9 +124,9 @@ mod json_convertible_tests_yesnoabstainvotechoice { #[test] fn value_round_trip_abstain() { use crate::serialization::ValueConvertible; - let original = YesNoAbstainVoteChoice::ABSTAIN; + let original = YesNoAbstainVoteChoice::Abstain; let value = original.to_object().expect("to_object"); - assert_eq!(value, platform_value!("aBSTAIN")); + assert_eq!(value, platform_value!("abstain")); let recovered = YesNoAbstainVoteChoice::from_object(value).expect("from_object"); assert_eq!(original, recovered); } diff --git a/packages/rs-dpp/src/voting/vote_info_storage/mod.rs b/packages/rs-dpp/src/voting/vote_info_storage/mod.rs index a69f0aee63a..740f6cddc3f 100644 --- a/packages/rs-dpp/src/voting/vote_info_storage/mod.rs +++ b/packages/rs-dpp/src/voting/vote_info_storage/mod.rs @@ -1,2 +1,3 @@ pub mod contested_document_vote_poll_stored_info; pub mod contested_document_vote_poll_winner_info; +pub mod yes_no_vote_poll_stored_info; diff --git a/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs new file mode 100644 index 00000000000..e929e6ecd58 --- /dev/null +++ b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs @@ -0,0 +1,225 @@ +pub mod v0; + +use crate::block::block_info::BlockInfo; +use crate::voting::vote_info_storage::yes_no_vote_poll_stored_info::v0::YesNoVotePollStoredInfoV0; +use crate::voting::vote_polls::yes_no_vote_poll::VotingPower; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::From; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, +}; +use platform_version::version::PlatformVersion; +use std::fmt; + +/// How a yes/no vote poll ended: the final tallies and whether they passed the poll's rule. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Encode, Decode, DecodeUntrusted)] +pub struct YesNoVotePollResult { + /// Whether the poll passed. + pub passed: bool, + /// The yes voting power at the end. + pub yes_voting_power: VotingPower, + /// The no voting power at the end. + pub no_voting_power: VotingPower, + /// The abstaining voting power at the end. Recorded, but it plays no part in the result. + pub abstain_voting_power: VotingPower, + /// The block the poll opened in. + pub start_block: BlockInfo, + /// The block the poll was closed in. + pub finalization_block: BlockInfo, +} + +impl fmt::Display for YesNoVotePollResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "YesNoVotePollResult {{ passed: {}, yes: {}, no: {}, abstain: {}, start_block: {}, finalization_block: {} }}", + self.passed, + self.yes_voting_power, + self.no_voting_power, + self.abstain_voting_power, + self.start_block, + self.finalization_block + ) + } +} + +/// Where a yes/no vote poll is in its life. A poll runs once: finished, it stays finished, and +/// its result is the record of the decision. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Encode, Decode, DecodeUntrusted)] +pub enum YesNoVotePollStatus { + /// Votes are being taken since this block. + Started(BlockInfo), + /// The poll was closed with this result. + Finished(YesNoVotePollResult), +} + +impl fmt::Display for YesNoVotePollStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + YesNoVotePollStatus::Started(block_info) => write!(f, "Started({})", block_info), + YesNoVotePollStatus::Finished(result) => write!(f, "Finished({})", result), + } + } +} + +/// What Drive stores about a yes/no vote poll beside its votes. +#[derive( + Debug, + PartialEq, + Eq, + Clone, + From, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + DecodeUntrusted, +)] +#[platform_serialize(unversioned)] +pub enum YesNoVotePollStoredInfo { + V0(YesNoVotePollStoredInfoV0), +} + +impl fmt::Display for YesNoVotePollStoredInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + YesNoVotePollStoredInfo::V0(info) => write!(f, "V0({})", info), + } + } +} + +impl YesNoVotePollStoredInfo { + /// The stored info of a poll that opens in `start_block`. + pub fn new( + start_block: BlockInfo, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .voting_versions + .yes_no_vote_poll_stored_info_version + { + 0 => Ok(YesNoVotePollStoredInfoV0::new(start_block).into()), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "YesNoVotePollStoredInfo::new".to_string(), + known_versions: vec![0], + received: version, + }), + } + } + + /// Rewrites the stored info in the version the platform version wants stored. + pub fn update_to_latest_version( + self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .voting_versions + .yes_no_vote_poll_stored_info_version + { + 0 => match self { + YesNoVotePollStoredInfo::V0(_) => Ok(self), + }, + version => Err(ProtocolError::UnknownVersionMismatch { + method: "YesNoVotePollStoredInfo::update_to_latest_version".to_string(), + known_versions: vec![0], + received: version, + }), + } + } + + /// Closes a started poll with its final tallies. `passed` is the poll's own verdict on + /// them; the stored info records it without judging. + pub fn finalize_vote_poll( + &mut self, + yes_voting_power: VotingPower, + no_voting_power: VotingPower, + abstain_voting_power: VotingPower, + passed: bool, + finalization_block: BlockInfo, + ) -> Result { + match self { + YesNoVotePollStoredInfo::V0(v0) => v0.finalize_vote_poll( + yes_voting_power, + no_voting_power, + abstain_voting_power, + passed, + finalization_block, + ), + } + } + + /// Where the poll is in its life. + pub fn status(&self) -> &YesNoVotePollStatus { + match self { + YesNoVotePollStoredInfo::V0(v0) => &v0.status, + } + } + + /// The result, once the poll has finished. + pub fn result(&self) -> Option<&YesNoVotePollResult> { + match self.status() { + YesNoVotePollStatus::Started(_) => None, + YesNoVotePollStatus::Finished(result) => Some(result), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::serialization::{PlatformDeserializableTrusted, PlatformSerializable}; + + #[test] + fn should_finalize_a_started_poll_once() { + let platform_version = PlatformVersion::latest(); + let start_block = BlockInfo::default_with_time(10); + let mut stored_info = + YesNoVotePollStoredInfo::new(start_block, platform_version).expect("stored info"); + assert_eq!( + stored_info.status(), + &YesNoVotePollStatus::Started(start_block) + ); + assert!(stored_info.result().is_none()); + + let finalization_block = BlockInfo::default_with_time(20); + let result = stored_info + .finalize_vote_poll(300, 100, 7, true, finalization_block) + .expect("finalize"); + assert_eq!( + result, + YesNoVotePollResult { + passed: true, + yes_voting_power: 300, + no_voting_power: 100, + abstain_voting_power: 7, + start_block, + finalization_block, + } + ); + assert_eq!(stored_info.result(), Some(&result)); + + // A finished poll is not finalized twice. + assert!(stored_info + .finalize_vote_poll(1, 1, 1, false, finalization_block) + .is_err()); + } + + #[test] + fn should_round_trip_through_platform_serialization() { + let platform_version = PlatformVersion::latest(); + let mut stored_info = + YesNoVotePollStoredInfo::new(BlockInfo::default_with_time(10), platform_version) + .expect("stored info"); + stored_info + .finalize_vote_poll(5, 4, 3, false, BlockInfo::default_with_time(20)) + .expect("finalize"); + let bytes = stored_info.serialize_to_bytes().expect("serialize"); + let recovered = + YesNoVotePollStoredInfo::deserialize_from_bytes_trusted(&bytes).expect("deserialize"); + assert_eq!(recovered, stored_info); + } +} diff --git a/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/v0/mod.rs b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/v0/mod.rs new file mode 100644 index 00000000000..98ec0770d2b --- /dev/null +++ b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/v0/mod.rs @@ -0,0 +1,54 @@ +use crate::block::block_info::BlockInfo; +use crate::voting::vote_info_storage::yes_no_vote_poll_stored_info::{ + YesNoVotePollResult, YesNoVotePollStatus, +}; +use crate::voting::vote_polls::yes_no_vote_poll::VotingPower; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use std::fmt; + +/// Version 0 of the stored info of a yes/no vote poll: only its status. +#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode, DecodeUntrusted)] +pub struct YesNoVotePollStoredInfoV0 { + /// Where the poll is in its life. + pub status: YesNoVotePollStatus, +} + +impl fmt::Display for YesNoVotePollStoredInfoV0 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "YesNoVotePollStoredInfoV0 {{ status: {} }}", self.status) + } +} + +impl YesNoVotePollStoredInfoV0 { + pub fn new(start_block: BlockInfo) -> YesNoVotePollStoredInfoV0 { + YesNoVotePollStoredInfoV0 { + status: YesNoVotePollStatus::Started(start_block), + } + } + + pub fn finalize_vote_poll( + &mut self, + yes_voting_power: VotingPower, + no_voting_power: VotingPower, + abstain_voting_power: VotingPower, + passed: bool, + finalization_block: BlockInfo, + ) -> Result { + let YesNoVotePollStatus::Started(start_block) = self.status else { + return Err(ProtocolError::CorruptedCodeExecution( + "trying to finalize a yes/no vote poll that is not started".to_string(), + )); + }; + let result = YesNoVotePollResult { + passed, + yes_voting_power, + no_voting_power, + abstain_voting_power, + start_block, + finalization_block, + }; + self.status = YesNoVotePollStatus::Finished(result); + Ok(result) + } +} diff --git a/packages/rs-dpp/src/voting/vote_polls/mod.rs b/packages/rs-dpp/src/voting/vote_polls/mod.rs index 8664016e4e5..6f8b400be2a 100644 --- a/packages/rs-dpp/src/voting/vote_polls/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/mod.rs @@ -3,6 +3,7 @@ use crate::serialization::JsonConvertible; #[cfg(feature = "value-conversion")] use crate::serialization::ValueConvertible; use crate::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; +use crate::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use crate::ProtocolError; use bincode::{Decode, DecodeUntrusted, Encode}; use derive_more::From; @@ -15,6 +16,7 @@ use serde::{Deserialize, Serialize}; use std::fmt; pub mod contested_document_resource_vote_poll; +pub mod yes_no_vote_poll; #[cfg_attr( all(feature = "json-conversion", feature = "serde-conversion"), @@ -44,6 +46,10 @@ pub mod contested_document_resource_vote_poll; #[platform_serialize(limit = 100000)] pub enum VotePoll { ContestedDocumentResourceVotePoll(ContestedDocumentResourceVotePoll), + /// A yes/no poll keyed by a resource path, resolved by a supermajority once enough voting + /// power has been cast (protocol version 14). Appended: the bincode discriminant is the + /// variant position. + YesNoVotePoll(YesNoVotePoll), } impl fmt::Display for VotePoll { @@ -52,6 +58,7 @@ impl fmt::Display for VotePoll { VotePoll::ContestedDocumentResourceVotePoll(poll) => { write!(f, "ContestedDocumentResourceVotePoll({})", poll) } + VotePoll::YesNoVotePoll(poll) => write!(f, "YesNoVotePoll({})", poll), } } } @@ -70,6 +77,9 @@ impl VotePoll { contested_document_resource_vote_poll.specialized_balance_id()?, )) } + VotePoll::YesNoVotePoll(yes_no_vote_poll) => { + Ok(Some(yes_no_vote_poll.specialized_balance_id()?)) + } } } @@ -78,6 +88,7 @@ impl VotePoll { VotePoll::ContestedDocumentResourceVotePoll(contested_document_resource_vote_poll) => { contested_document_resource_vote_poll.unique_id() } + VotePoll::YesNoVotePoll(yes_no_vote_poll) => yes_no_vote_poll.unique_id(), } } } diff --git a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs new file mode 100644 index 00000000000..0a0dd757c7b --- /dev/null +++ b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs @@ -0,0 +1,378 @@ +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +use crate::serialization::PlatformSerializable; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; +use crate::util::hash::hash_double; +use crate::voting::vote_polls::VotePoll; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, +}; +use platform_value::{BinaryData, Identifier}; +use platform_version::version::PlatformVersion; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// Voting power: a masternode vote weighs 1, an evonode vote weighs 4. The sum over the whole +/// masternode list fits a `u32`. +pub type VotingPower = u32; + +/// A poll the masternodes answer with yes, no or abstain. +/// +/// The poll is keyed by a resource path the feature that opens it chooses (a contract id and a +/// purpose, for example). Two polls with the same resource path and the same parameters are the +/// same poll, so a feature that may run the same question again includes something that +/// distinguishes the rounds in the path. +/// +/// The poll passes when the yes voting power is strictly more than +/// `supermajority_numerator / supermajority_denominator` of the yes plus no voting power, the +/// abstaining power left out of both sides, and the yes plus no voting power is at least +/// `minimum_voting_power`. Otherwise it fails. Both numbers are carried by the poll so every +/// feature sets its own rule. Two thirds with a floor of 400 is numerator 2, denominator 3 and +/// minimum 400. +/// +/// The parameters are part of the poll's identity: [`Self::unique_id`] hashes the serialized +/// [`VotePoll`] that carries the poll, so a vote names the exact poll it answers. The prefunded +/// balance that pays for the votes has the same id. +#[cfg_attr(feature = "json-conversion", json_safe_fields)] +#[cfg_attr( + all(feature = "json-conversion", feature = "serde-conversion"), + derive(JsonConvertible) +)] +#[derive( + Debug, + Clone, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + PartialEq, + Eq, + DecodeUntrusted, +)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] +#[platform_serialize(unversioned)] +#[platform_serialize(limit = 100000)] +pub struct YesNoVotePoll { + /// What the poll decides on, as opaque segments chosen by the feature that opened it. + pub resource_path: Vec, + /// The numerator of the share of yes plus no voting power that yes must exceed. + pub supermajority_numerator: u8, + /// The denominator of the share of yes plus no voting power that yes must exceed. + pub supermajority_denominator: u8, + /// The least yes plus no voting power for the poll to pass. + pub minimum_voting_power: VotingPower, +} + +impl fmt::Display for YesNoVotePoll { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let resource_path: Vec = self + .resource_path + .iter() + .map(|segment| hex::encode(segment.as_slice())) + .collect(); + write!( + f, + "YesNoVotePoll {{ resource_path: [{}], supermajority: {}/{}, minimum_voting_power: {} }}", + resource_path.join("/"), + self.supermajority_numerator, + self.supermajority_denominator, + self.minimum_voting_power + ) + } +} + +impl YesNoVotePoll { + /// Refuses parameters no poll can be opened with: an empty or oversized resource path (the + /// poll rides in every vote, whose wire size is bounded), a zero denominator, or a + /// numerator that is not below the denominator (yes could never exceed the whole of yes + /// plus no). + pub fn validate_parameters( + &self, + platform_version: &PlatformVersion, + ) -> Result<(), ProtocolError> { + if self.resource_path.is_empty() { + return Err(ProtocolError::VoteError( + "a yes/no vote poll needs a resource path".to_string(), + )); + } + let max_segments = platform_version + .system_limits + .max_yes_no_vote_poll_resource_path_segments; + if self.resource_path.len() > max_segments as usize { + return Err(ProtocolError::VoteError(format!( + "a yes/no vote poll resource path has at most {} segments, got {}", + max_segments, + self.resource_path.len() + ))); + } + let max_bytes = platform_version + .system_limits + .max_yes_no_vote_poll_resource_path_bytes; + let bytes: usize = self.resource_path.iter().map(|segment| segment.len()).sum(); + if bytes > max_bytes as usize { + return Err(ProtocolError::VoteError(format!( + "a yes/no vote poll resource path holds at most {} bytes, got {}", + max_bytes, bytes + ))); + } + if self.supermajority_denominator == 0 { + return Err(ProtocolError::VoteError( + "a yes/no vote poll supermajority denominator must not be zero".to_string(), + )); + } + if self.supermajority_numerator >= self.supermajority_denominator { + return Err(ProtocolError::VoteError(format!( + "a yes/no vote poll supermajority numerator ({}) must be below its denominator ({})", + self.supermajority_numerator, self.supermajority_denominator + ))); + } + Ok(()) + } + + /// Whether the poll passes with these final tallies. Abstaining power plays no part. + pub fn passes(&self, yes_voting_power: VotingPower, no_voting_power: VotingPower) -> bool { + let yes = yes_voting_power as u64; + let cast = yes + no_voting_power as u64; + if cast < self.minimum_voting_power as u64 { + return false; + } + yes * self.supermajority_denominator as u64 > cast * self.supermajority_numerator as u64 + } + + /// The double SHA-256 of the serialized `VotePoll` carrying this poll. + pub fn sha256_2_hash(&self) -> Result<[u8; 32], ProtocolError> { + let encoded = VotePoll::YesNoVotePoll(self.clone()).serialize_to_bytes()?; + Ok(hash_double(encoded)) + } + + /// The prefunded balance the votes on this poll are paid from: the poll's unique id. + pub fn specialized_balance_id(&self) -> Result { + self.unique_id() + } + + /// The id the poll is stored and voted on under. + pub fn unique_id(&self) -> Result { + self.sha256_2_hash().map(Identifier::new) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn poll() -> YesNoVotePoll { + YesNoVotePoll { + resource_path: vec![ + BinaryData::new(vec![0xc1; 32]), + BinaryData::new(b"challenge".to_vec()), + ], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: 400, + } + } + + #[test] + fn should_pass_strictly_above_two_thirds_and_fail_at_or_below_it() { + // The minimum is met by every tally below; only the share decides. + let poll = YesNoVotePoll { + minimum_voting_power: 300, + ..poll() + }; + // 300 cast: two thirds is 200, so 201 passes and 200 does not. + assert!(poll.passes(201, 99)); + assert!(!poll.passes(200, 100)); + // 301 cast: two thirds is 200.67, so 201 passes and 200 does not. + assert!(poll.passes(201, 100)); + assert!(!poll.passes(200, 101)); + } + + #[test] + fn should_fail_below_the_minimum_voting_power_even_when_unanimous() { + let poll = poll(); + assert!(!poll.passes(399, 0)); + assert!(poll.passes(400, 0)); + } + + #[test] + fn should_not_overflow_on_the_largest_tallies() { + let poll = YesNoVotePoll { + supermajority_numerator: 254, + supermajority_denominator: 255, + ..poll() + }; + assert!(poll.passes(u32::MAX, 0)); + assert!(!poll.passes(u32::MAX, u32::MAX)); + } + + #[test] + fn should_refuse_parameters_no_poll_can_run_with() { + let platform_version = PlatformVersion::latest(); + assert!(poll().validate_parameters(platform_version).is_ok()); + assert!(YesNoVotePoll { + resource_path: vec![], + ..poll() + } + .validate_parameters(platform_version) + .is_err()); + assert!(YesNoVotePoll { + supermajority_denominator: 0, + ..poll() + } + .validate_parameters(platform_version) + .is_err()); + assert!(YesNoVotePoll { + supermajority_numerator: 3, + supermajority_denominator: 3, + ..poll() + } + .validate_parameters(platform_version) + .is_err()); + // A simple majority is a valid rule. + assert!(YesNoVotePoll { + supermajority_numerator: 1, + supermajority_denominator: 2, + ..poll() + } + .validate_parameters(platform_version) + .is_ok()); + } + + #[test] + fn should_bound_the_resource_path() { + let platform_version = PlatformVersion::latest(); + let max_segments = platform_version + .system_limits + .max_yes_no_vote_poll_resource_path_segments as usize; + let max_bytes = platform_version + .system_limits + .max_yes_no_vote_poll_resource_path_bytes as usize; + let at_most_segments = YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![1]); max_segments], + ..poll() + }; + assert!(at_most_segments + .validate_parameters(platform_version) + .is_ok()); + let too_many_segments = YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![1]); max_segments + 1], + ..poll() + }; + assert!(too_many_segments + .validate_parameters(platform_version) + .is_err()); + let at_most_bytes = YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![1; max_bytes])], + ..poll() + }; + assert!(at_most_bytes.validate_parameters(platform_version).is_ok()); + let too_many_bytes = YesNoVotePoll { + resource_path: vec![ + BinaryData::new(vec![1; max_bytes]), + BinaryData::new(vec![1]), + ], + ..poll() + }; + assert!(too_many_bytes + .validate_parameters(platform_version) + .is_err()); + } + + #[test] + fn should_change_the_id_with_every_parameter() { + let base = poll().unique_id().expect("id"); + let other_path = YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![0xc1; 32])], + ..poll() + }; + let other_rule = YesNoVotePoll { + supermajority_numerator: 1, + supermajority_denominator: 2, + ..poll() + }; + let other_minimum = YesNoVotePoll { + minimum_voting_power: 401, + ..poll() + }; + for other in [other_path, other_rule, other_minimum] { + assert_ne!(other.unique_id().expect("id"), base); + } + assert_eq!(poll().specialized_balance_id().expect("balance id"), base); + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + fn fixture() -> YesNoVotePoll { + YesNoVotePoll { + resource_path: vec![ + BinaryData::new(vec![0xc1; 4]), + BinaryData::new(b"challenge".to_vec()), + ], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: 400, + } + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Path segments are `BinaryData`, base64 strings in JSON. + assert_eq!( + json, + json!({ + "resourcePath": ["wcHBwQ==", "Y2hhbGxlbmdl"], + "supermajorityNumerator": 2, + "supermajorityDenominator": 3, + "minimumVotingPower": 400, + }) + ); + let recovered = YesNoVotePoll::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ + "resourcePath": [ + platform_value::Value::Bytes(vec![0xc1; 4]), + platform_value::Value::Bytes(b"challenge".to_vec()), + ], + "supermajorityNumerator": 2u8, + "supermajorityDenominator": 3u8, + "minimumVotingPower": 400u32, + }) + ); + let recovered = YesNoVotePoll::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/voting/votes/mod.rs b/packages/rs-dpp/src/voting/votes/mod.rs index 4b19cd9757b..9d4e876b040 100644 --- a/packages/rs-dpp/src/voting/votes/mod.rs +++ b/packages/rs-dpp/src/voting/votes/mod.rs @@ -1,4 +1,5 @@ pub mod resource_vote; +pub mod yes_no_vote; #[cfg(feature = "json-conversion")] use crate::serialization::JsonConvertible; @@ -6,6 +7,8 @@ use crate::serialization::JsonConvertible; use crate::serialization::ValueConvertible; use crate::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; use crate::voting::votes::resource_vote::ResourceVote; +use crate::voting::votes::yes_no_vote::accessors::v0::YesNoVoteGettersV0; +use crate::voting::votes::yes_no_vote::YesNoVote; use crate::ProtocolError; use bincode::{Decode, DecodeUntrusted, Encode}; use derive_more::From; @@ -40,6 +43,9 @@ use serde::{Deserialize, Serialize}; #[platform_serialize(limit = 15000, unversioned)] pub enum Vote { ResourceVote(ResourceVote), + /// A vote on a yes/no poll (protocol version 14). Appended: the bincode discriminant is + /// the variant position. + YesNoVote(YesNoVote), } // Manual impl because Vote is a flat enum (not versioned V0/V1). @@ -56,11 +62,15 @@ impl Vote { pub fn specialized_balance_id(&self) -> Result, ProtocolError> { match self { Vote::ResourceVote(resource_vote) => resource_vote.vote_poll().specialized_balance_id(), + Vote::YesNoVote(yes_no_vote) => { + Ok(Some(yes_no_vote.vote_poll().specialized_balance_id()?)) + } } } pub fn vote_poll_unique_id(&self) -> Result { match self { Vote::ResourceVote(resource_vote) => resource_vote.vote_poll().unique_id(), + Vote::YesNoVote(yes_no_vote) => yes_no_vote.vote_poll().unique_id(), } } } diff --git a/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/mod.rs b/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/mod.rs new file mode 100644 index 00000000000..4b6295a18ce --- /dev/null +++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/mod.rs @@ -0,0 +1,26 @@ +use crate::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use crate::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use crate::voting::votes::yes_no_vote::accessors::v0::YesNoVoteGettersV0; +use crate::voting::votes::yes_no_vote::YesNoVote; + +pub mod v0; + +impl YesNoVoteGettersV0 for YesNoVote { + fn vote_poll(&self) -> &YesNoVotePoll { + match self { + YesNoVote::V0(v0) => &v0.vote_poll, + } + } + + fn vote_poll_owned(self) -> YesNoVotePoll { + match self { + YesNoVote::V0(v0) => v0.vote_poll, + } + } + + fn vote_choice(&self) -> YesNoAbstainVoteChoice { + match self { + YesNoVote::V0(v0) => v0.vote_choice, + } + } +} diff --git a/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/v0/mod.rs b/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/v0/mod.rs new file mode 100644 index 00000000000..9ac03954776 --- /dev/null +++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/v0/mod.rs @@ -0,0 +1,14 @@ +use crate::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use crate::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; + +/// Getters of a yes/no vote. +pub trait YesNoVoteGettersV0 { + /// The poll answered. + fn vote_poll(&self) -> &YesNoVotePoll; + + /// The poll answered, owned. + fn vote_poll_owned(self) -> YesNoVotePoll; + + /// The answer. + fn vote_choice(&self) -> YesNoAbstainVoteChoice; +} diff --git a/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs b/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs new file mode 100644 index 00000000000..79f80560d20 --- /dev/null +++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs @@ -0,0 +1,114 @@ +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; +use crate::voting::votes::yes_no_vote::v0::YesNoVoteV0; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, +}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; + +pub mod accessors; +pub mod v0; + +/// A masternode's answer to a yes/no vote poll (protocol version 14). +#[cfg_attr( + all(feature = "json-conversion", feature = "serde-conversion"), + derive(JsonConvertible) +)] +#[derive( + Debug, + Clone, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + PartialEq, + Eq, + DecodeUntrusted, +)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] +#[platform_serialize(limit = 15000, unversioned)] +pub enum YesNoVote { + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(YesNoVoteV0), +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_yes_no_vote { + use super::*; + use crate::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; + use crate::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; + use platform_value::{platform_value, BinaryData}; + use serde_json::json; + + fn fixture() -> YesNoVote { + YesNoVote::V0(YesNoVoteV0 { + vote_poll: YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![0xc1; 4])], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: 400, + }, + vote_choice: YesNoAbstainVoteChoice::Yes, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "votePoll": { + "resourcePath": ["wcHBwQ=="], + "supermajorityNumerator": 2, + "supermajorityDenominator": 3, + "minimumVotingPower": 400, + }, + "voteChoice": "yes", + }) + ); + let recovered = YesNoVote::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "votePoll": { + "resourcePath": [platform_value::Value::Bytes(vec![0xc1; 4])], + "supermajorityNumerator": 2u8, + "supermajorityDenominator": 3u8, + "minimumVotingPower": 400u32, + }, + "voteChoice": "yes", + }) + ); + let recovered = YesNoVote::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/voting/votes/yes_no_vote/v0/mod.rs b/packages/rs-dpp/src/voting/votes/yes_no_vote/v0/mod.rs new file mode 100644 index 00000000000..e7df7111966 --- /dev/null +++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/v0/mod.rs @@ -0,0 +1,45 @@ +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; +use crate::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use crate::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, +}; +use platform_value::Identifier; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; + +/// Version 0 of a yes/no vote: the poll answered and the answer. +#[cfg_attr(feature = "json-conversion", json_safe_fields)] +#[derive( + Debug, + Clone, + Encode, + Decode, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + PlatformSerialize, + PartialEq, + Eq, + DecodeUntrusted, +)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +#[platform_serialize(unversioned)] +pub struct YesNoVoteV0 { + /// The poll answered. + pub vote_poll: YesNoVotePoll, + /// The answer. + pub vote_choice: YesNoAbstainVoteChoice, +} + +impl YesNoVoteV0 { + pub fn vote_poll_unique_id(&self) -> Result { + self.vote_poll.unique_id() + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 5820f88c0c3..c80b2b532a2 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -313,6 +313,9 @@ impl Platform { VotePoll::ContestedDocumentResourceVotePoll(contested) => { contested.specialized_balance_id().map_err(Error::Protocol) } + VotePoll::YesNoVotePoll(yes_no) => { + yes_no.specialized_balance_id().map_err(Error::Protocol) + } } }) .collect::, Error>>()?; @@ -812,6 +815,12 @@ impl Platform { self.drive .insert_contract_fee_pot_trees(Some(transaction), platform_version)?; + // Vote decisions trees: the active yes/no polls and the identity votes index of the + // decisions branch. Fresh chains create them at genesis + // (`add_initial_vote_tree_main_structure_operations` v1). + self.drive + .insert_vote_decisions_trees(Some(transaction), platform_version)?; + Ok(()) } } @@ -2026,6 +2035,74 @@ mod tests { ); } + /// The only way a running network gets the vote decisions trees is this upgrade hook. + /// They are absent at 13 and present after the transition, and the votes subtree is then + /// byte-identical to the one of a chain born at 14, so both node populations build the + /// same Merk when the first yes/no poll opens. + #[test] + fn test_transition_to_version_14_creates_vote_decisions_trees() { + use drive::drive::votes::paths::{ + vote_decisions_tree_path, ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY, + }; + use drive::util::grove_operations::DirectQueryType; + + let platform_version = PlatformVersion::latest(); + let born_at_14 = TestPlatformBuilder::new() + .with_initial_protocol_version(14) + .build_with_mock_rpc() + .set_genesis_state(); + let upgraded = TestPlatformBuilder::new() + .with_initial_protocol_version(13) + .build_with_mock_rpc() + .set_genesis_state(); + + let transaction = upgraded.drive.grove.start_transaction(); + + let decisions_tree_exists = |key: char, transaction: &Transaction| { + upgraded + .drive + .grove_has_raw( + (&vote_decisions_tree_path()).into(), + &[key as u8], + DirectQueryType::StatefulDirectQuery, + Some(transaction), + &mut vec![], + &platform_version.drive, + ) + .expect("expected to query the decisions tree") + }; + for key in [ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY] { + assert!( + !decisions_tree_exists(key, &transaction), + "protocol version 13 has no decisions {key} tree" + ); + } + + upgraded + .transition_to_version_14(&BlockInfo::default(), &transaction, platform_version) + .expect("expected version 14 transition to succeed"); + + for key in [ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY] { + assert!( + decisions_tree_exists(key, &transaction), + "the decisions {key} tree must exist after the transition" + ); + } + + let diffs = collect_subtree_diffs( + &born_at_14, + &upgraded, + &transaction, + vec![vec![RootTree::Votes as u8]], + ); + assert!( + diffs.is_empty(), + "the votes subtree differs between a chain born at version 14 and one upgraded to \ + it:\n{}", + diffs.join("\n") + ); + } + #[test] fn test_transition_to_version_14_creates_total_credits_history_tree() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs index 94d8ab0c3fb..378d44cc6b0 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs @@ -1,3 +1,4 @@ +use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::PlatformState; @@ -175,6 +176,11 @@ where identifiers_voting_for_contenders, )) } + ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Execution( + ExecutionError::CorruptedCodeExecution( + "yes/no vote polls need check_for_ended_vote_polls version 1", + ), + )), } }).collect::, Error>>()?; Ok((end_date, vote_polls_with_votes)) @@ -300,6 +306,11 @@ where )?; Ok(ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(resolved_contested_document_resource_vote_poll, identifiers_voting_for_contenders)) } + ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Execution( + ExecutionError::CorruptedCodeExecution( + "yes/no vote polls need check_for_ended_vote_polls version 1", + ), + )), } }).collect::, Error>>()?; Ok((end_date, vote_polls_with_votes)) diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs index 39f9378b96b..7bfe6d60a48 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs @@ -9,6 +9,7 @@ use dpp::version::PlatformVersion; use dpp::voting::contender_structs::FinalizedContender; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice::TowardsIdentity; use dpp::voting::vote_info_storage::contested_document_vote_poll_winner_info::ContestedDocumentVotePollWinnerInfo; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use drive::drive::votes::resolved::vote_polls::resolve::VotePollResolver; use drive::drive::votes::resolved::vote_polls::{ResolvedVotePoll, ResolvedVotePollWithVotes}; use drive::grovedb::TransactionArg; @@ -20,7 +21,9 @@ impl Platform where C: CoreRPCLike, { - /// Checks for ended vote polls, awarding a tie to the earliest contender + /// Checks for ended vote polls, awarding a tie to the earliest contender. Version 1 + /// (protocol version 14) also closes yes/no polls: it records their result, hands it to + /// the feature that opened them and cleans their votes up. #[inline(always)] pub(super) fn check_for_ended_vote_polls_v1( &self, @@ -172,6 +175,13 @@ where )?; Ok(ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(resolved_contested_document_resource_vote_poll, identifiers_voting_for_contenders)) } + ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll) => self + .finish_yes_no_vote_poll( + block_info, + yes_no_vote_poll, + transaction, + platform_version, + ), } }).collect::, Error>>()?; Ok((end_date, vote_polls_with_votes)) @@ -192,4 +202,40 @@ where Ok(()) } + + /// Closes one yes/no poll whose time has come: tallies it, records the result in its + /// stored info, hands the result to the feature that opened the poll, and returns the + /// voters so the clean-up can remove their votes. + fn finish_yes_no_vote_poll( + &self, + block_info: &BlockInfo, + vote_poll: YesNoVotePoll, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let tally = + self.tally_votes_for_yes_no_vote_poll(&vote_poll, transaction, platform_version)?; + let result = self.keep_record_of_finished_yes_no_vote_poll( + block_info, + &vote_poll, + &tally, + transaction, + platform_version, + )?; + self.on_yes_no_vote_poll_finished( + block_info, + &vote_poll, + &result, + transaction, + platform_version, + )?; + let voters = self.drive.fetch_identities_voting_in_yes_no_vote_poll( + &vote_poll, + transaction, + platform_version, + )?; + Ok(ResolvedVotePollWithVotes::YesNoVotePollWithVotes( + vote_poll, voters, + )) + } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/mod.rs index 32e3245fc95..69b4cafc292 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/mod.rs @@ -13,6 +13,7 @@ use std::collections::BTreeMap; mod v0; mod v1; +mod v2; impl Platform where @@ -52,9 +53,16 @@ where transaction, platform_version, ), + 2 => self.clean_up_after_contested_resources_vote_polls_end_v2( + block_info, + vote_polls, + clean_up_testnet_corrupted_reference_issue, + transaction, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "clean_up_after_contested_resources_vote_polls_end".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v2/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v2/mod.rs new file mode 100644 index 00000000000..45117693595 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v2/mod.rs @@ -0,0 +1,203 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; +use dpp::fee::Credits; +use dpp::identifier::Identifier; +use dpp::prelude::TimestampMillis; +use dpp::version::PlatformVersion; +use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use dpp::ProtocolError; +use drive::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfo; +use drive::fees::op::LowLevelDriveOperation; +use drive::grovedb::TransactionArg; +use std::collections::BTreeMap; + +impl Platform +where + C: CoreRPCLike, +{ + /// Cleans up after contested resource vote polls that ended. + /// Version 2 (protocol version 14) leaves the end date index alone: + /// `clean_up_after_vote_polls_end` removes every finished poll of every kind from it, so + /// that an end date's tree goes only once nothing is left under it. + #[inline(always)] + // TODO: Use type or struct + #[allow(clippy::type_complexity)] + pub(super) fn clean_up_after_contested_resources_vote_polls_end_v2( + &self, + block_info: &BlockInfo, + vote_polls: Vec<( + &ContestedDocumentResourceVotePollWithContractInfo, + &TimestampMillis, + &BTreeMap>, + )>, + clean_up_testnet_corrupted_reference_issue: bool, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let mut operations = self.clean_up_after_contested_resources_vote_polls_end_operations_v1( + vote_polls.as_slice(), + clean_up_testnet_corrupted_reference_issue, + transaction, + platform_version, + )?; + + // We also need to clean out the specialized balances + + let mut total_credits_to_add_to_processing: Credits = 0; + + for specialized_balance_id in vote_polls + .iter() + .map(|(vote_poll, _, _)| vote_poll.specialized_balance_id()) + { + let (credits, mut empty_specialized_balance_operation) = + self.drive.empty_prefunded_specialized_balance_operations( + specialized_balance_id?, + false, + &mut None, + transaction, + platform_version, + )?; + operations.append(&mut empty_specialized_balance_operation); + total_credits_to_add_to_processing = total_credits_to_add_to_processing + .checked_add(credits) + .ok_or(Error::Execution(ExecutionError::Overflow( + "Credits from specialized balances are overflowing", + )))?; + } + + if total_credits_to_add_to_processing > 0 { + operations.push( + self.drive + .add_epoch_processing_credits_for_distribution_operation( + &block_info.epoch, + total_credits_to_add_to_processing, + transaction, + platform_version, + )?, + ); + } + + if !operations.is_empty() { + self.drive.apply_batch_low_level_drive_operations( + None, + transaction, + operations, + &mut vec![], + &platform_version.drive, + )?; + } + + Ok(()) + } + + #[inline(always)] + // TODO: Use type or struct + #[allow(clippy::type_complexity)] + fn clean_up_after_contested_resources_vote_polls_end_operations_v1( + &self, + vote_polls: &[( + &ContestedDocumentResourceVotePollWithContractInfo, + &TimestampMillis, + &BTreeMap>, + )], + clean_up_testnet_corrupted_reference_issue: bool, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut operations = vec![]; + + // We remove the votes from under the contenders votes received + self.drive + .remove_contested_resource_vote_poll_votes_operations( + vote_polls, + true, + &mut operations, + transaction, + platform_version, + )?; + + // We remove the documents that contenders have + self.drive + .remove_contested_resource_vote_poll_documents_operations( + vote_polls, + clean_up_testnet_corrupted_reference_issue, + &mut operations, + transaction, + platform_version, + )?; + + // We remove the contenders + self.drive + .remove_contested_resource_vote_poll_contenders_operations( + vote_polls, + &mut operations, + transaction, + platform_version, + )?; + + let vote_poll_ids = vote_polls + .iter() + .map(|(vote_poll, _, _)| Ok((*vote_poll, vote_poll.unique_id()?))) + .collect::, + ProtocolError, + >>()?; + + let mut identity_to_vote_ids_map: BTreeMap<&Identifier, Vec<&Identifier>> = BTreeMap::new(); + + for (vote_poll, _, voters_for_contender) in vote_polls { + let vote_id = vote_poll_ids + .iter() + .find_map(|(vp, vid)| if vp == vote_poll { Some(vid) } else { None }) + .ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( + "the vote poll must exist in this lookup table", + )))?; + + for identifiers in voters_for_contender.values() { + for identity_id in identifiers { + identity_to_vote_ids_map + .entry(identity_id) + .or_default() + .push(vote_id); + } + } + } + + for (identity, vote_ids) in identity_to_vote_ids_map { + // We remove the identity votes given + self.drive + .remove_specific_vote_references_given_by_identity( + identity, + vote_ids.as_slice(), + &mut operations, + transaction, + platform_version, + )?; + } + + if clean_up_testnet_corrupted_reference_issue { + self.drive.remove_contested_resource_info_operations( + vote_polls, + &mut operations, + transaction, + platform_version, + )?; + // We remove the last index + self.drive + .remove_contested_resource_top_level_index_operations( + vote_polls, + &mut operations, + transaction, + platform_version, + )?; + } + + Ok(operations) + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/mod.rs index 6580b030998..b5186a05005 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/mod.rs @@ -10,6 +10,7 @@ use drive::grovedb::TransactionArg; use std::collections::BTreeMap; mod v0; +mod v1; impl Platform where @@ -37,9 +38,16 @@ where transaction, platform_version, ), + 1 => self.clean_up_after_vote_polls_end_v1( + block_info, + vote_polls, + clean_up_testnet_corrupted_reference_issue, + transaction, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "clean_up_after_vote_polls_end".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs index 1d1b4186e9f..1d7f911e996 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs @@ -1,3 +1,4 @@ +use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; @@ -40,7 +41,12 @@ where match vote_poll { ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(contested_poll, vote_info) => { contested_polls.push((contested_poll, end_date, vote_info)); - } // Add more match arms here for other types of vote polls in the future + } + ResolvedVotePollWithVotes::YesNoVotePollWithVotes(..) => { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "yes/no vote polls need clean_up_after_vote_polls_end version 1", + ))) + } } } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v1/mod.rs new file mode 100644 index 00000000000..7976540e8e6 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v1/mod.rs @@ -0,0 +1,99 @@ +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::prelude::TimestampMillis; +use dpp::version::PlatformVersion; +use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use drive::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfo; +use drive::drive::votes::resolved::vote_polls::ResolvedVotePollWithVotes; +use drive::grovedb::TransactionArg; +use std::collections::BTreeMap; + +impl Platform +where + C: CoreRPCLike, +{ + /// Cleans up after the polls that ended in this block. Version 1 (protocol version 14) + /// handles both kinds. It removes every finished poll from the end date index itself, + /// so that an end date's tree is deleted only once nothing of either kind is left under + /// it, then cleans up the polls of each kind. + #[inline(always)] + pub(super) fn clean_up_after_vote_polls_end_v1( + &self, + block_info: &BlockInfo, + vote_polls: &BTreeMap>, + clean_up_testnet_corrupted_reference_issue: bool, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + // TODO: Use type or struct + #[allow(clippy::type_complexity)] + let mut contested_polls: Vec<( + &ContestedDocumentResourceVotePollWithContractInfo, + &TimestampMillis, + &BTreeMap>, + )> = Vec::new(); + let mut yes_no_polls: Vec<( + &YesNoVotePoll, + &BTreeMap>, + )> = Vec::new(); + let mut end_date_entries: Vec<(Identifier, TimestampMillis)> = Vec::new(); + + for (end_date, vote_polls_for_time) in vote_polls { + for vote_poll in vote_polls_for_time { + match vote_poll { + ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(contested_poll, vote_info) => { + end_date_entries.push((contested_poll.unique_id()?, *end_date)); + contested_polls.push((contested_poll, end_date, vote_info)); + } + ResolvedVotePollWithVotes::YesNoVotePollWithVotes(yes_no_poll, voters) => { + end_date_entries.push((yes_no_poll.unique_id()?, *end_date)); + yes_no_polls.push((yes_no_poll, voters)); + } + } + } + } + + if !end_date_entries.is_empty() { + let mut operations = vec![]; + self.drive.remove_vote_poll_end_date_query_operations( + end_date_entries.as_slice(), + &mut operations, + transaction, + platform_version, + )?; + self.drive.apply_batch_low_level_drive_operations( + None, + transaction, + operations, + &mut vec![], + &platform_version.drive, + )?; + } + + if !contested_polls.is_empty() { + self.clean_up_after_contested_resources_vote_polls_end( + block_info, + contested_polls, + clean_up_testnet_corrupted_reference_issue, + transaction, + platform_version, + )?; + } + + if !yes_no_polls.is_empty() { + self.clean_up_after_yes_no_vote_polls_end( + block_info, + yes_no_polls, + transaction, + platform_version, + )?; + } + + Ok(()) + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs new file mode 100644 index 00000000000..3b14bb8be66 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs @@ -0,0 +1,52 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use drive::grovedb::TransactionArg; +use std::collections::BTreeMap; + +mod v0; + +impl Platform +where + C: CoreRPCLike, +{ + /// Cleans up after yes/no polls that ended: their votes and vote trees, the voters' + /// entries in the identity votes index, and what is left of the prefunded balance that + /// paid for the votes, which goes to the processing pool. The end date index was already + /// cleared by the caller. The stored info stays as the record of the decision. + pub(in crate::execution) fn clean_up_after_yes_no_vote_polls_end( + &self, + block_info: &BlockInfo, + vote_polls: Vec<( + &YesNoVotePoll, + &BTreeMap>, + )>, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive_abci + .methods + .voting + .clean_up_after_yes_no_vote_polls_end + { + 0 => self.clean_up_after_yes_no_vote_polls_end_v0( + block_info, + vote_polls, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "clean_up_after_yes_no_vote_polls_end".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs new file mode 100644 index 00000000000..278d528baf6 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs @@ -0,0 +1,105 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; +use dpp::fee::Credits; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use drive::grovedb::TransactionArg; +use std::collections::BTreeMap; + +impl Platform +where + C: CoreRPCLike, +{ + #[inline(always)] + pub(super) fn clean_up_after_yes_no_vote_polls_end_v0( + &self, + block_info: &BlockInfo, + vote_polls: Vec<( + &YesNoVotePoll, + &BTreeMap>, + )>, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let mut operations = vec![]; + + // The votes and the three vote trees of every poll. + self.drive.remove_yes_no_vote_poll_votes_operations( + vote_polls.as_slice(), + &mut operations, + transaction, + platform_version, + )?; + + // Each voter's entries in the identity votes index. + let mut poll_ids_by_voter: BTreeMap<&Identifier, Vec> = BTreeMap::new(); + for (vote_poll, voters_by_choice) in &vote_polls { + let vote_poll_id = vote_poll.unique_id()?; + for voters in voters_by_choice.values() { + for voter in voters { + poll_ids_by_voter + .entry(voter) + .or_default() + .push(vote_poll_id); + } + } + } + for (voter, vote_poll_ids) in poll_ids_by_voter { + let vote_poll_ids: Vec<&Identifier> = vote_poll_ids.iter().collect(); + self.drive + .remove_yes_no_vote_references_given_by_identity_operations( + voter, + vote_poll_ids.as_slice(), + &mut operations, + transaction, + platform_version, + )?; + } + + // What is left of the prefunded balances goes to processing. + let mut total_credits_to_add_to_processing: Credits = 0; + for (vote_poll, _) in &vote_polls { + let (credits, mut empty_specialized_balance_operations) = + self.drive.empty_prefunded_specialized_balance_operations( + vote_poll.specialized_balance_id()?, + false, + &mut None, + transaction, + platform_version, + )?; + operations.append(&mut empty_specialized_balance_operations); + total_credits_to_add_to_processing = total_credits_to_add_to_processing + .checked_add(credits) + .ok_or(Error::Execution(ExecutionError::Overflow( + "Credits from specialized balances are overflowing", + )))?; + } + if total_credits_to_add_to_processing > 0 { + operations.push( + self.drive + .add_epoch_processing_credits_for_distribution_operation( + &block_info.epoch, + total_credits_to_add_to_processing, + transaction, + platform_version, + )?, + ); + } + + if !operations.is_empty() { + self.drive.apply_batch_low_level_drive_operations( + None, + transaction, + operations, + &mut vec![], + &platform_version.drive, + )?; + } + Ok(()) + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs new file mode 100644 index 00000000000..a09380c7ac1 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs @@ -0,0 +1,48 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; +use dpp::version::PlatformVersion; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollResult; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use drive::grovedb::TransactionArg; +use drive::query::yes_no_vote_poll_state_query::YesNoVotePollState; + +mod v0; + +impl Platform +where + C: CoreRPCLike, +{ + /// Decides a finished yes/no poll from its tallies and writes the result into its stored + /// info, which stays as the record of the decision after the votes are cleaned up. + pub(in crate::execution) fn keep_record_of_finished_yes_no_vote_poll( + &self, + block_info: &BlockInfo, + vote_poll: &YesNoVotePoll, + tally: &YesNoVotePollState, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .methods + .voting + .keep_record_of_finished_yes_no_vote_poll + { + 0 => self.keep_record_of_finished_yes_no_vote_poll_v0( + block_info, + vote_poll, + tally, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "keep_record_of_finished_yes_no_vote_poll".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs new file mode 100644 index 00000000000..d9086142a8f --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs @@ -0,0 +1,53 @@ +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; +use dpp::version::PlatformVersion; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollResult; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use drive::error::drive::DriveError; +use drive::error::Error as DriveCrateError; +use drive::grovedb::TransactionArg; +use drive::query::yes_no_vote_poll_state_query::YesNoVotePollState; + +impl Platform +where + C: CoreRPCLike, +{ + #[inline(always)] + pub(super) fn keep_record_of_finished_yes_no_vote_poll_v0( + &self, + block_info: &BlockInfo, + vote_poll: &YesNoVotePoll, + tally: &YesNoVotePollState, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let stored_info_from_disk = + tally + .stored_info + .clone() + .ok_or(Error::Drive(DriveCrateError::Drive( + DriveError::CorruptedDriveState( + "there must be a record of the yes/no vote poll in the state".to_string(), + ), + )))?; + // We perform an upgrade of the stored version just in case, most of the time this does nothing + let mut stored_info = stored_info_from_disk.update_to_latest_version(platform_version)?; + let passed = vote_poll.passes(tally.yes_voting_power, tally.no_voting_power); + let result = stored_info.finalize_vote_poll( + tally.yes_voting_power, + tally.no_voting_power, + tally.abstain_voting_power, + passed, + *block_info, + )?; + self.drive.insert_stored_info_for_yes_no_vote_poll( + vote_poll, + stored_info, + transaction, + platform_version, + )?; + Ok(result) + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs index 326867bbcb7..394da430ef1 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs @@ -2,7 +2,11 @@ mod award_document_to_winner; mod check_for_ended_vote_polls; mod clean_up_after_contested_resources_vote_polls_end; mod clean_up_after_vote_polls_end; +mod clean_up_after_yes_no_vote_polls_end; +mod keep_record_of_finished_yes_no_vote_poll; mod keep_record_of_vote_poll; +mod on_yes_no_vote_poll_finished; mod remove_votes_for_removed_masternodes; mod run_dao_platform_events; mod tally_votes_for_contested_document_resource_vote_poll; +mod tally_votes_for_yes_no_vote_poll; diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs new file mode 100644 index 00000000000..640ffc7edfc --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs @@ -0,0 +1,48 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; +use dpp::version::PlatformVersion; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollResult; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use drive::grovedb::TransactionArg; + +mod v0; + +impl Platform +where + C: CoreRPCLike, +{ + /// Acts on the result of a yes/no poll that just finished, in the same block, before its + /// votes are cleaned up. The feature that opened the poll recognises it by its resource + /// path. Nothing opens a yes/no poll yet, so version 0 acts on nothing. + pub(in crate::execution) fn on_yes_no_vote_poll_finished( + &self, + block_info: &BlockInfo, + vote_poll: &YesNoVotePoll, + result: &YesNoVotePollResult, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive_abci + .methods + .voting + .on_yes_no_vote_poll_finished + { + 0 => self.on_yes_no_vote_poll_finished_v0( + block_info, + vote_poll, + result, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "on_yes_no_vote_poll_finished".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/v0/mod.rs new file mode 100644 index 00000000000..f33ba6278a4 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/v0/mod.rs @@ -0,0 +1,28 @@ +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; +use dpp::version::PlatformVersion; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollResult; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use drive::grovedb::TransactionArg; + +impl Platform +where + C: CoreRPCLike, +{ + /// No feature opens a yes/no poll yet, so no feature acts on one. The moderation team + /// challenge and charter amendment features add their arms here, matched on the poll's + /// resource path. + #[inline(always)] + pub(super) fn on_yes_no_vote_poll_finished_v0( + &self, + _block_info: &BlockInfo, + _vote_poll: &YesNoVotePoll, + _result: &YesNoVotePollResult, + _transaction: TransactionArg, + _platform_version: &PlatformVersion, + ) -> Result<(), Error> { + Ok(()) + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs new file mode 100644 index 00000000000..ccbfb229f2b --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs @@ -0,0 +1,37 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::version::PlatformVersion; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use drive::grovedb::TransactionArg; +use drive::query::yes_no_vote_poll_state_query::YesNoVotePollState; + +mod v0; + +impl Platform +where + C: CoreRPCLike, +{ + /// The tallies of a yes/no poll, read from its vote sum trees, with its stored info. + pub(in crate::execution) fn tally_votes_for_yes_no_vote_poll( + &self, + vote_poll: &YesNoVotePoll, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .methods + .voting + .tally_votes_for_yes_no_vote_poll + { + 0 => self.tally_votes_for_yes_no_vote_poll_v0(vote_poll, transaction, platform_version), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "tally_votes_for_yes_no_vote_poll".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/v0/mod.rs new file mode 100644 index 00000000000..9f249f2fb9f --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/v0/mod.rs @@ -0,0 +1,27 @@ +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::version::PlatformVersion; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use drive::grovedb::TransactionArg; +use drive::query::yes_no_vote_poll_state_query::{ + YesNoVotePollState, YesNoVotePollStateDriveQuery, +}; + +impl Platform +where + C: CoreRPCLike, +{ + #[inline(always)] + pub(super) fn tally_votes_for_yes_no_vote_poll_v0( + &self, + vote_poll: &YesNoVotePoll, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let query = YesNoVotePollStateDriveQuery { + vote_poll: vote_poll.clone(), + }; + Ok(query.execute_no_proof(&self.drive, transaction, &mut vec![], platform_version)?) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs index 50123948016..07d29aceb2f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs @@ -16,6 +16,7 @@ use dpp::voting::vote_polls::VotePoll; use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::{DocumentCreateTransitionAction, DocumentCreateTransitionActionAccessorsV0}; use dpp::version::PlatformVersion; +use crate::error::execution::ExecutionError; use crate::error::Error; pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentCreateTransitionActionStructureValidationV1 { @@ -119,6 +120,13 @@ impl DocumentCreateTransitionActionStructureValidationV1 for DocumentCreateTrans } // -->> End Introduced in V1 <<-- (None, None) => {} + // A document type's contested index only ever resolves to a contested + // document resource poll. + (Some(VotePoll::YesNoVotePoll(_)), Some(_)) => { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "a contested index never resolves to a yes/no vote poll", + ))); + } } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs index 5e0edc8c823..078fb372611 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs @@ -3,6 +3,8 @@ mod balance; mod nonce; mod state; mod transform_into_action; +#[cfg(test)] +mod yes_no_tests; use dpp::address_funds::PlatformAddress; use dpp::block::block_info::BlockInfo; @@ -24,6 +26,7 @@ use crate::rpc::core::CoreRPCLike; use crate::execution::validation::state_transition::masternode_vote::state::v0::MasternodeVoteStateTransitionStateValidationV0; use crate::execution::validation::state_transition::masternode_vote::state::v1::MasternodeVoteStateTransitionStateValidationV1; use crate::execution::validation::state_transition::masternode_vote::transform_into_action::v0::MasternodeVoteStateTransitionTransformIntoActionValidationV0; +use crate::execution::validation::state_transition::masternode_vote::transform_into_action::v1::MasternodeVoteStateTransitionTransformIntoActionValidationV1; use crate::execution::validation::state_transition::processor::state::StateTransitionStateValidation; use crate::execution::validation::state_transition::transformer::StateTransitionActionTransformer; use crate::execution::validation::state_transition::ValidationMode; @@ -52,9 +55,12 @@ impl StateTransitionActionTransformer for MasternodeVoteTransition { 0 => self .transform_into_action_v0(platform, validation_mode, tx, platform_version) .map(|result| result.map(|action| action.into())), + 1 => self + .transform_into_action_v1(platform, validation_mode, tx, platform_version) + .map(|result| result.map(|action| action.into())), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "masternode votes state transition: transform_into_action".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs index cde77903a56..6aed88f4d06 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs @@ -92,8 +92,18 @@ impl MasternodeVoteStateTransitionStateValidationV0 for MasternodeVoteTransition } } } + ResolvedVotePoll::YesNoVotePoll(_) => { + Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "yes/no vote polls need validate_state version 1", + ))) + } } } + ResolvedVote::YesNoVote(_) => { + Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "yes/no votes need validate_state version 1", + ))) + } } } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v1/mod.rs index 4ea6d6b31b4..b4340a9670a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v1/mod.rs @@ -4,6 +4,7 @@ use dpp::consensus::state::state_error::StateError; use dpp::consensus::state::voting::vote_choice_not_allowed_for_vote_poll_error::VoteChoiceNotAllowedForVotePollError; use dpp::consensus::state::voting::vote_poll_not_available_for_voting_error::VotePollNotAvailableForVotingError; use dpp::consensus::state::voting::vote_poll_not_found_error::VotePollNotFoundError; +use dpp::consensus::state::voting::yes_no_vote_poll_not_available_for_voting_error::YesNoVotePollNotAvailableForVotingError; use dpp::consensus::ConsensusError; use dpp::prelude::ConsensusValidationResult; @@ -16,6 +17,8 @@ use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use dpp::voting::vote_info_storage::contested_document_vote_poll_stored_info::{ ContestedDocumentVotePollStatus, ContestedDocumentVotePollStoredInfoV0Getters, }; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStatus; +use dpp::voting::vote_polls::VotePoll; use drive::drive::votes::resolved::vote_polls::ResolvedVotePoll; use drive::drive::votes::resolved::votes::resolved_resource_vote::accessors::v0::ResolvedResourceVoteGettersV0; use drive::drive::votes::resolved::votes::ResolvedVote; @@ -113,6 +116,45 @@ impl MasternodeVoteStateTransitionStateValidationV1 for MasternodeVoteTransition } } } + ResolvedVotePoll::YesNoVotePoll(_) => { + // A resource vote choice answers no yes/no poll: no contested + // document resource poll named by this vote exists. + Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::VotePollNotFoundError( + VotePollNotFoundError::new(vote_poll.into()), + )), + )) + } + } + } + ResolvedVote::YesNoVote(yes_no_vote) => { + let vote_poll = &yes_no_vote.vote_poll; + let Some(stored_info) = platform.drive.fetch_yes_no_vote_poll_stored_info( + vote_poll, + tx, + platform_version, + )? + else { + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::VotePollNotFoundError( + VotePollNotFoundError::new(VotePoll::YesNoVotePoll(vote_poll.clone())), + )), + )); + }; + match stored_info.status() { + YesNoVotePollStatus::Started(_) => Ok( + ConsensusValidationResult::new_with_data(masternode_vote_action.into()), + ), + status @ YesNoVotePollStatus::Finished(_) => Ok( + ConsensusValidationResult::new_with_error(ConsensusError::StateError( + StateError::YesNoVotePollNotAvailableForVotingError( + YesNoVotePollNotAvailableForVotingError::new( + vote_poll.clone(), + *status, + ), + ), + )), + ), } } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/mod.rs index 9a1925de7fc..008be12cc67 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/mod.rs @@ -1 +1,2 @@ pub(crate) mod v0; +pub(crate) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs index e76fd0eb8b6..7ac0b241c20 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs @@ -1,3 +1,4 @@ +use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::PlatformRef; use dpp::consensus::state::state_error::StateError; @@ -103,8 +104,18 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV0 for Masternode } } } + VotePoll::YesNoVotePoll(_) => { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "yes/no vote polls need transform_into_action version 1", + ))) + } } } + Vote::YesNoVote(_) => { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "yes/no votes need transform_into_action version 1", + ))) + } } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs new file mode 100644 index 00000000000..1c3830c49da --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs @@ -0,0 +1,232 @@ +use crate::error::Error; +use crate::platform_types::platform::PlatformRef; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::state::voting::masternode_not_found_error::MasternodeNotFoundError; +use dpp::consensus::state::voting::masternode_vote_already_present_error::MasternodeVoteAlreadyPresentError; +use dpp::consensus::state::voting::masternode_voted_too_many_times::MasternodeVotedTooManyTimesError; +use dpp::consensus::state::voting::vote_poll_not_found_error::VotePollNotFoundError; +use dpp::consensus::ConsensusError; +use dpp::dashcore::hashes::Hash; +use dpp::dashcore::ProTxHash; +use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeType; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::masternode_vote_transition::accessors::MasternodeVoteTransitionAccessorsV0; +use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; +use drive::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; +use drive::drive::votes::resolved::votes::ResolvedVote; +use drive::state_transition_action::identity::masternode_vote::v0::MasternodeVoteTransitionActionV0; +use drive::state_transition_action::identity::masternode_vote::MasternodeVoteTransitionAction; + +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform_state::PlatformStateV0Methods; +use dpp::version::PlatformVersion; +use dpp::voting::vote_polls::VotePoll; +use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; +use dpp::voting::votes::yes_no_vote::accessors::v0::YesNoVoteGettersV0; +use dpp::voting::votes::Vote; +use drive::grovedb::TransactionArg; + +pub(in crate::execution::validation::state_transition::state_transitions::masternode_vote) trait MasternodeVoteStateTransitionTransformIntoActionValidationV1 +{ + fn transform_into_action_v1( + &self, + platform: &PlatformRef, + validation_mode: ValidationMode, + tx: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error>; +} + +impl MasternodeVoteStateTransitionTransformIntoActionValidationV1 for MasternodeVoteTransition { + /// Version 1 (protocol version 14) also transforms a vote on a yes/no poll. The same rules + /// hold for both kinds: the same answer cannot be given twice, and a masternode may change + /// its answer only `votes_allowed_per_masternode` times. + fn transform_into_action_v1( + &self, + platform: &PlatformRef, + validation_mode: ValidationMode, + tx: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + // A resource vote choice (towards an identity, abstain, lock) answers a contested + // document resource poll. No such poll is named by a resource vote that carries a + // yes/no poll, whatever validation mode we are in. + if let Vote::ResourceVote(resource_vote) = self.vote() { + if let VotePoll::YesNoVotePoll(_) = resource_vote.vote_poll() { + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::VotePollNotFoundError( + VotePollNotFoundError::new(resource_vote.vote_poll().clone()), + )), + )); + } + } + + let mut previous_resource_vote_choice_to_remove = None; + let mut previous_yes_no_vote_choice_to_remove = None; + if validation_mode != ValidationMode::NoValidation { + // Before we transform into action we want to make sure that we have not yet voted + match self.vote() { + Vote::ResourceVote(resource_vote) => { + match resource_vote.vote_poll() { + VotePoll::ContestedDocumentResourceVotePoll(vote_poll) => { + let vote_id = vote_poll.unique_id()?; + let maybe_existing_resource_vote_choice = + platform.drive.fetch_identity_contested_resource_vote( + self.pro_tx_hash(), + vote_id, + tx, + &mut vec![], + platform_version, + )?; + if let Some((existing_resource_vote_choice, previous_vote_count)) = + maybe_existing_resource_vote_choice + { + if existing_resource_vote_choice + == resource_vote.resource_vote_choice() + { + // We are submitting a vote for something we already have + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError( + StateError::MasternodeVoteAlreadyPresentError( + MasternodeVoteAlreadyPresentError::new( + self.pro_tx_hash(), + resource_vote.vote_poll().clone(), + ), + ), + ), + )); + } else if previous_vote_count + >= platform_version + .dpp + .validation + .voting + .votes_allowed_per_masternode + { + // We are submitting a vote for something we already have + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError( + StateError::MasternodeVotedTooManyTimesError( + MasternodeVotedTooManyTimesError::new( + self.pro_tx_hash(), + previous_vote_count, + platform_version + .dpp + .validation + .voting + .votes_allowed_per_masternode, + ), + ), + ), + )); + } else { + previous_resource_vote_choice_to_remove = + Some((existing_resource_vote_choice, previous_vote_count)); + } + } + } + // Refused above. + VotePoll::YesNoVotePoll(_) => {} + } + } + Vote::YesNoVote(yes_no_vote) => { + let vote_poll = yes_no_vote.vote_poll(); + let vote_id = vote_poll.unique_id()?; + let maybe_existing_vote_choice = platform.drive.fetch_identity_yes_no_vote( + self.pro_tx_hash(), + vote_id, + tx, + &mut vec![], + platform_version, + )?; + if let Some((existing_vote_choice, previous_vote_count)) = + maybe_existing_vote_choice + { + if existing_vote_choice == yes_no_vote.vote_choice() { + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError( + StateError::MasternodeVoteAlreadyPresentError( + MasternodeVoteAlreadyPresentError::new( + self.pro_tx_hash(), + VotePoll::YesNoVotePoll(vote_poll.clone()), + ), + ), + ), + )); + } else if previous_vote_count + >= platform_version + .dpp + .validation + .voting + .votes_allowed_per_masternode + { + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError( + StateError::MasternodeVotedTooManyTimesError( + MasternodeVotedTooManyTimesError::new( + self.pro_tx_hash(), + previous_vote_count, + platform_version + .dpp + .validation + .voting + .votes_allowed_per_masternode, + ), + ), + ), + )); + } else { + previous_yes_no_vote_choice_to_remove = + Some((existing_vote_choice, previous_vote_count)); + } + } + } + } + } + + let Some(masternode) = platform + .state + .full_masternode_list() + .get(&ProTxHash::from_byte_array(self.pro_tx_hash().to_buffer())) + else { + return Ok(ConsensusValidationResult::new_with_error( + MasternodeNotFoundError::new(self.pro_tx_hash()).into(), + )); + }; + + let strength = match masternode.node_type { + MasternodeType::Regular => 1, + MasternodeType::Evo => 4, + }; + + let action = match self.vote() { + Vote::ResourceVote(_) => MasternodeVoteTransitionAction::transform_from_transition( + self, + masternode.state.voting_address, + strength, + previous_resource_vote_choice_to_remove, + platform.drive, + tx, + platform_version, + )?, + Vote::YesNoVote(yes_no_vote) => { + // A yes/no vote needs nothing from the state to be resolved; the previous + // answer found above rides with it. + let mut resolved_vote = ResolvedYesNoVote::from_vote(yes_no_vote); + resolved_vote.previous_vote_choice_to_remove = + previous_yes_no_vote_choice_to_remove; + MasternodeVoteTransitionActionV0 { + pro_tx_hash: self.pro_tx_hash(), + voter_identity_id: self.voter_identity_id(), + voting_address: masternode.state.voting_address, + vote_strength: strength, + vote: ResolvedVote::YesNoVote(resolved_vote), + previous_resource_vote_choice_to_remove: None, + nonce: self.nonce(), + } + .into() + } + }; + + Ok(ConsensusValidationResult::new_with_data(action)) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs new file mode 100644 index 00000000000..e6beb24dbfc --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs @@ -0,0 +1,1069 @@ +//! Votes on yes/no vote polls: the poll's life from opening to the record of its decision. + +use crate::execution::validation::state_transition::state_transitions::tests::{ + setup_masternode_voting_identity, take_down_masternode_identities, +}; +use crate::platform_types::platform_state::PlatformStateV0Methods; +use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; +use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult::{ + InternalError, SuccessfulExecution, UnpaidConsensusError, +}; +use crate::rpc::core::MockCoreRPCLike; +use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; +use assert_matches::assert_matches; +use dpp::block::block_info::BlockInfo; +use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::ConsensusError; +use dpp::dashcore::hashes::Hash; +use dpp::dashcore::ProTxHash; +use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeType; +use dpp::identifier::Identifier; +use dpp::identity::IdentityPublicKey; +use dpp::platform_value::BinaryData; +use dpp::prelude::IdentityNonce; +use dpp::serialization::PlatformSerializable; +use dpp::state_transition::masternode_vote_transition::methods::MasternodeVoteTransitionMethodsV0; +use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; +use dpp::version::PlatformVersion; +use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStatus; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use dpp::voting::vote_polls::VotePoll; +use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; +use dpp::voting::votes::resource_vote::ResourceVote; +use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; +use dpp::voting::votes::yes_no_vote::YesNoVote; +use dpp::voting::votes::Vote; +use drive::query::yes_no_vote_poll_state_query::YesNoVotePollStateDriveQuery; +use drive::query::VotePollsByEndDateDriveQuery; +use simple_signer::signer::SimpleSigner; +use std::ops::Deref; +use std::sync::Arc; + +/// Two weeks after genesis: the polls in these tests end here. +const END_DATE: u64 = 1_209_600_000; +/// Enough for many votes at the single vote cost. +const FUND: u64 = 100_000_000_000; + +fn two_thirds_poll(purpose: &[u8], minimum_voting_power: u32) -> YesNoVotePoll { + YesNoVotePoll { + resource_path: vec![ + BinaryData::new(vec![0xc1; 32]), + BinaryData::new(purpose.to_vec()), + ], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power, + } +} + +fn open_poll( + platform: &TempPlatform, + vote_poll: &YesNoVotePoll, + fund: u64, + platform_version: &PlatformVersion, +) { + open_poll_ending_at(platform, vote_poll, END_DATE, fund, platform_version) +} + +fn open_poll_ending_at( + platform: &TempPlatform, + vote_poll: &YesNoVotePoll, + end_date: u64, + fund: u64, + platform_version: &PlatformVersion, +) { + platform + .drive + .open_yes_no_vote_poll( + vote_poll, + end_date, + &BlockInfo::default(), + None, + platform_version, + ) + .expect("expected to open the yes/no vote poll"); + if fund > 0 { + platform + .drive + .add_prefunded_specialized_balance( + vote_poll + .specialized_balance_id() + .expect("expected a specialized balance id"), + fund, + None, + platform_version, + ) + .expect("expected to fund the poll"); + } +} + +/// A voting masternode, promoted to an evonode when asked, so its vote weighs 4. +fn setup_voter( + platform: &mut TempPlatform, + seed: u64, + evonode: bool, + platform_version: &PlatformVersion, +) -> (Identifier, SimpleSigner, IdentityPublicKey) { + let (pro_tx_hash, _identity, signer, voting_key) = + setup_masternode_voting_identity(platform, seed, platform_version); + if evonode { + let mut platform_state = platform.state.load().clone().deref().clone(); + platform_state + .full_masternode_list_mut() + .get_mut(&ProTxHash::from_byte_array(pro_tx_hash.to_buffer())) + .expect("expected the masternode") + .node_type = MasternodeType::Evo; + platform.state.store(Arc::new(platform_state)); + } + (pro_tx_hash, signer, voting_key) +} + +#[allow(clippy::too_many_arguments)] +async fn perform_yes_no_vote( + platform: &mut TempPlatform, + vote_poll: &YesNoVotePoll, + vote_choice: YesNoAbstainVoteChoice, + signer: &SimpleSigner, + pro_tx_hash: Identifier, + voting_key: &IdentityPublicKey, + nonce: IdentityNonce, + platform_version: &PlatformVersion, +) -> Result<(), StateTransitionExecutionResult> { + let vote = Vote::YesNoVote(YesNoVote::V0(YesNoVoteV0 { + vote_poll: vote_poll.clone(), + vote_choice, + })); + perform_vote_transition( + platform, + vote, + signer, + pro_tx_hash, + voting_key, + nonce, + platform_version, + ) + .await +} + +/// Processes one masternode vote transition in a block and reports how it went. +async fn perform_vote_transition( + platform: &mut TempPlatform, + vote: Vote, + signer: &SimpleSigner, + pro_tx_hash: Identifier, + voting_key: &IdentityPublicKey, + nonce: IdentityNonce, + platform_version: &PlatformVersion, +) -> Result<(), StateTransitionExecutionResult> { + let masternode_vote_transition = MasternodeVoteTransition::try_from_vote_with_signer( + vote, + signer, + pro_tx_hash, + voting_key, + nonce, + platform_version, + None, + ) + .await + .expect("expected to make transition vote"); + let serialized_transition = masternode_vote_transition + .serialize_to_bytes() + .expect("expected to serialize the vote"); + let platform_state = platform.state.load(); + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized_transition], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + match processing_result.into_execution_results().remove(0) { + SuccessfulExecution { .. } => Ok(()), + other => Err(other), + } +} + +/// Casts one vote per masternode: `count` voters, each with the choice, at consecutive seeds. +async fn cast_votes( + platform: &mut TempPlatform, + vote_poll: &YesNoVotePoll, + vote_choice: YesNoAbstainVoteChoice, + count: u64, + start_seed: u64, + platform_version: &PlatformVersion, +) -> Vec { + let mut voters = vec![]; + for seed in start_seed..start_seed + count { + let (pro_tx_hash, signer, voting_key) = + setup_voter(platform, seed, false, platform_version); + perform_yes_no_vote( + platform, + vote_poll, + vote_choice, + &signer, + pro_tx_hash, + &voting_key, + 1, + platform_version, + ) + .await + .expect("expected the vote to be accepted"); + voters.push(pro_tx_hash); + } + voters +} + +/// Moves the chain past the polls' end date and closes the polls that ended. +fn close_ended_polls( + platform: &mut TempPlatform, + platform_version: &PlatformVersion, +) { + close_ended_polls_at(platform, END_DATE + 300_000, platform_version) +} + +/// Moves the chain to `time_ms` and closes the polls that ended by then. +fn close_ended_polls_at( + platform: &mut TempPlatform, + time_ms: u64, + platform_version: &PlatformVersion, +) { + let mut platform_state = platform.state.load().clone().deref().clone(); + let block_info = BlockInfo { + time_ms, + height: 10000, + core_height: 42, + epoch: Default::default(), + }; + platform_state.set_last_committed_block_info(Some( + ExtendedBlockInfoV0 { + basic_info: block_info, + app_hash: platform + .drive + .grove + .root_hash(None, &platform_version.drive.grove_version) + .unwrap() + .unwrap(), + quorum_hash: [0u8; 32], + block_id_hash: [0u8; 32], + proposer_pro_tx_hash: [0u8; 32], + signature: [0u8; 96], + round: 0, + } + .into(), + )); + platform.state.store(Arc::new(platform_state)); + let platform_state = platform.state.load(); + let transaction = platform.drive.grove.start_transaction(); + platform + .check_for_ended_vote_polls( + &platform_state, + &platform_state, + &block_info, + Some(&transaction), + platform_version, + ) + .expect("expected to check for ended vote polls"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); +} + +fn poll_state( + platform: &TempPlatform, + vote_poll: &YesNoVotePoll, + platform_version: &PlatformVersion, +) -> drive::query::yes_no_vote_poll_state_query::YesNoVotePollState { + YesNoVotePollStateDriveQuery { + vote_poll: vote_poll.clone(), + } + .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) + .expect("expected the poll state") +} + +fn new_platform() -> TempPlatform { + TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state() +} + +#[tokio::test] +async fn should_pass_at_exactly_the_threshold_and_fail_one_power_below_it_with_abstains() { + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + // Six cast votes: two thirds is four, so five yes passes and four yes does not. + let passing = two_thirds_poll(b"passing", 6); + let failing = two_thirds_poll(b"failing", 6); + open_poll(&platform, &passing, FUND, platform_version); + open_poll(&platform, &failing, FUND, platform_version); + + cast_votes( + &mut platform, + &passing, + YesNoAbstainVoteChoice::Yes, + 5, + 100, + platform_version, + ) + .await; + cast_votes( + &mut platform, + &passing, + YesNoAbstainVoteChoice::No, + 1, + 200, + platform_version, + ) + .await; + cast_votes( + &mut platform, + &passing, + YesNoAbstainVoteChoice::Abstain, + 2, + 300, + platform_version, + ) + .await; + + cast_votes( + &mut platform, + &failing, + YesNoAbstainVoteChoice::Yes, + 4, + 400, + platform_version, + ) + .await; + cast_votes( + &mut platform, + &failing, + YesNoAbstainVoteChoice::No, + 2, + 500, + platform_version, + ) + .await; + cast_votes( + &mut platform, + &failing, + YesNoAbstainVoteChoice::Abstain, + 3, + 600, + platform_version, + ) + .await; + + let state = poll_state(&platform, &passing, platform_version); + assert_eq!( + ( + state.yes_voting_power, + state.no_voting_power, + state.abstain_voting_power + ), + (5, 1, 2) + ); + assert_matches!( + state.stored_info.expect("stored info").status(), + YesNoVotePollStatus::Started(_) + ); + + close_ended_polls(&mut platform, platform_version); + + let passing_result = *poll_state(&platform, &passing, platform_version) + .stored_info + .expect("stored info") + .result() + .expect("expected the poll to be finished"); + assert!(passing_result.passed); + assert_eq!(passing_result.yes_voting_power, 5); + assert_eq!(passing_result.no_voting_power, 1); + assert_eq!(passing_result.abstain_voting_power, 2); + assert_eq!( + passing_result.finalization_block.time_ms, + END_DATE + 300_000 + ); + + let failing_result = *poll_state(&platform, &failing, platform_version) + .stored_info + .expect("stored info") + .result() + .expect("expected the poll to be finished"); + assert!(!failing_result.passed); + assert_eq!(failing_result.yes_voting_power, 4); + assert_eq!(failing_result.no_voting_power, 2); + assert_eq!(failing_result.abstain_voting_power, 3); +} + +#[tokio::test] +async fn should_fail_below_the_minimum_voting_power_even_when_every_vote_is_yes() { + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + let vote_poll = two_thirds_poll(b"unanimous", 6); + open_poll(&platform, &vote_poll, FUND, platform_version); + cast_votes( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::Yes, + 5, + 100, + platform_version, + ) + .await; + // Abstaining power does not count towards the minimum. + cast_votes( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::Abstain, + 3, + 200, + platform_version, + ) + .await; + + close_ended_polls(&mut platform, platform_version); + + let result = *poll_state(&platform, &vote_poll, platform_version) + .stored_info + .expect("stored info") + .result() + .expect("expected the poll to be finished"); + assert!(!result.passed); + assert_eq!(result.yes_voting_power, 5); + assert_eq!(result.no_voting_power, 0); + assert_eq!(result.abstain_voting_power, 3); +} + +#[tokio::test] +async fn should_count_an_evonode_vote_as_four() { + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + let vote_poll = two_thirds_poll(b"evonode", 5); + open_poll(&platform, &vote_poll, FUND, platform_version); + + let (evonode, signer, voting_key) = setup_voter(&mut platform, 100, true, platform_version); + perform_yes_no_vote( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::Yes, + &signer, + evonode, + &voting_key, + 1, + platform_version, + ) + .await + .expect("expected the evonode vote to be accepted"); + cast_votes( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::No, + 1, + 200, + platform_version, + ) + .await; + + let state = poll_state(&platform, &vote_poll, platform_version); + assert_eq!((state.yes_voting_power, state.no_voting_power), (4, 1)); + + close_ended_polls(&mut platform, platform_version); + + // Five cast, minimum five: four is strictly more than two thirds of five. + let result = *poll_state(&platform, &vote_poll, platform_version) + .stored_info + .expect("stored info") + .result() + .expect("expected the poll to be finished"); + assert!(result.passed); + assert_eq!(result.yes_voting_power, 4); +} + +#[tokio::test] +async fn should_leave_only_the_record_behind_after_the_poll_ends() { + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + let vote_poll = two_thirds_poll(b"cleanup", 1); + open_poll(&platform, &vote_poll, FUND, platform_version); + let yes_voters = cast_votes( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::Yes, + 3, + 100, + platform_version, + ) + .await; + let no_voters = cast_votes( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::No, + 1, + 200, + platform_version, + ) + .await; + let vote_poll_id = vote_poll.unique_id().expect("id"); + let vote_cost = platform_version + .fee_version + .vote_resolution_fund_fees + .contested_document_single_vote_cost; + + // Before the end: votes, index entries, end date entry and the fund minus four votes. + let voters = platform + .drive + .fetch_identities_voting_in_yes_no_vote_poll(&vote_poll, None, platform_version) + .expect("voters"); + assert_eq!(voters[&YesNoAbstainVoteChoice::Yes].len(), 3); + assert_eq!(voters[&YesNoAbstainVoteChoice::No].len(), 1); + assert!(voters[&YesNoAbstainVoteChoice::Abstain].is_empty()); + assert_eq!( + platform + .drive + .fetch_identity_yes_no_vote( + yes_voters[0], + vote_poll_id, + None, + &mut vec![], + platform_version + ) + .expect("vote"), + Some((YesNoAbstainVoteChoice::Yes, 1)) + ); + let balance = platform + .drive + .fetch_prefunded_specialized_balance(vote_poll_id.to_buffer(), None, platform_version) + .expect("balance"); + assert_eq!(balance, Some(FUND - 4 * vote_cost)); + let end_dates = VotePollsByEndDateDriveQuery { + start_time: None, + end_time: None, + limit: None, + offset: None, + order_ascending: true, + } + .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) + .expect("end dates"); + assert_eq!(end_dates.len(), 1); + + close_ended_polls(&mut platform, platform_version); + + // After the end: the record with the result, and nothing else. + let state = poll_state(&platform, &vote_poll, platform_version); + let result = *state + .stored_info + .expect("stored info") + .result() + .expect("expected the poll to be finished"); + assert!(result.passed); + assert_eq!((result.yes_voting_power, result.no_voting_power), (3, 1)); + assert_eq!( + ( + state.yes_voting_power, + state.no_voting_power, + state.abstain_voting_power + ), + (0, 0, 0) + ); + let voters = platform + .drive + .fetch_identities_voting_in_yes_no_vote_poll(&vote_poll, None, platform_version) + .expect("voters"); + assert!(voters.values().all(|voters| voters.is_empty())); + for voter in yes_voters.iter().chain(no_voters.iter()) { + assert_eq!( + platform + .drive + .fetch_identity_yes_no_vote( + *voter, + vote_poll_id, + None, + &mut vec![], + platform_version + ) + .expect("vote"), + None + ); + } + let balance = platform + .drive + .fetch_prefunded_specialized_balance(vote_poll_id.to_buffer(), None, platform_version) + .expect("balance"); + assert!(balance.is_none() || balance == Some(0)); + let end_dates = VotePollsByEndDateDriveQuery { + start_time: None, + end_time: None, + limit: None, + offset: None, + order_ascending: true, + } + .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) + .expect("end dates"); + assert!(end_dates.is_empty()); + + // A finished poll takes no more votes, and cannot be opened again under the same id. + let (pro_tx_hash, signer, voting_key) = + setup_voter(&mut platform, 300, false, platform_version); + let error = perform_yes_no_vote( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::Yes, + &signer, + pro_tx_hash, + &voting_key, + 1, + platform_version, + ) + .await + .expect_err("expected the vote to be refused"); + // Closing the poll emptied its fund, and the fund pre-check runs before state validation, + // so the missing fund is what refuses the vote; the finished status refuses it too. + assert!( + matches!( + &error, + UnpaidConsensusError(ConsensusError::StateError( + StateError::PrefundedSpecializedBalanceNotFoundError(_) + )) | UnpaidConsensusError(ConsensusError::StateError( + StateError::YesNoVotePollNotAvailableForVotingError(_) + )) + ), + "unexpected result {:?}", + error + ); + assert!(platform + .drive + .open_yes_no_vote_poll( + &vote_poll, + END_DATE, + &BlockInfo::default(), + None, + platform_version + ) + .is_err()); +} + +#[tokio::test] +async fn should_refuse_a_vote_on_an_unknown_poll_and_the_same_answer_twice() { + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + let vote_poll = two_thirds_poll(b"rules", 1); + let unknown_poll = two_thirds_poll(b"unknown", 1); + open_poll(&platform, &vote_poll, FUND, platform_version); + // The unknown poll's fund exists, the poll does not. + platform + .drive + .add_prefunded_specialized_balance( + unknown_poll.specialized_balance_id().expect("id"), + FUND, + None, + platform_version, + ) + .expect("fund"); + let (pro_tx_hash, signer, voting_key) = + setup_voter(&mut platform, 100, false, platform_version); + + let error = perform_yes_no_vote( + &mut platform, + &unknown_poll, + YesNoAbstainVoteChoice::Yes, + &signer, + pro_tx_hash, + &voting_key, + 1, + platform_version, + ) + .await + .expect_err("expected the vote on an unknown poll to be refused"); + assert_matches!( + error, + UnpaidConsensusError(ConsensusError::StateError( + StateError::VotePollNotFoundError(_) + )) + ); + + perform_yes_no_vote( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::Yes, + &signer, + pro_tx_hash, + &voting_key, + 1, + platform_version, + ) + .await + .expect("expected the first vote to be accepted"); + let error = perform_yes_no_vote( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::Yes, + &signer, + pro_tx_hash, + &voting_key, + 2, + platform_version, + ) + .await + .expect_err("expected the same answer to be refused"); + assert_matches!( + error, + UnpaidConsensusError(ConsensusError::StateError( + StateError::MasternodeVoteAlreadyPresentError(_) + )) + ); +} + +#[tokio::test] +async fn should_refuse_a_resource_vote_that_names_a_yes_no_poll() { + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + let vote_poll = two_thirds_poll(b"cross kind", 1); + open_poll(&platform, &vote_poll, FUND, platform_version); + let (pro_tx_hash, signer, voting_key) = + setup_voter(&mut platform, 100, false, platform_version); + // A resource vote choice answers a contested document resource poll, not a yes/no one. + let vote = Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + vote_poll: VotePoll::YesNoVotePoll(vote_poll.clone()), + resource_vote_choice: ResourceVoteChoice::Abstain, + })); + let error = perform_vote_transition( + &mut platform, + vote, + &signer, + pro_tx_hash, + &voting_key, + 1, + platform_version, + ) + .await + .expect_err("expected the vote to be refused"); + assert_matches!( + error, + UnpaidConsensusError(ConsensusError::StateError( + StateError::VotePollNotFoundError(_) + )) + ); + let state = poll_state(&platform, &vote_poll, platform_version); + assert_eq!(state.abstain_voting_power, 0); +} + +#[tokio::test] +async fn should_move_a_changed_vote_and_cap_the_changes() { + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + let vote_poll = two_thirds_poll(b"changes", 1); + open_poll(&platform, &vote_poll, FUND, platform_version); + let (pro_tx_hash, signer, voting_key) = setup_voter(&mut platform, 100, true, platform_version); + let vote_poll_id = vote_poll.unique_id().expect("id"); + let allowed = platform_version + .dpp + .validation + .voting + .votes_allowed_per_masternode; + + let choices = [ + YesNoAbstainVoteChoice::Yes, + YesNoAbstainVoteChoice::No, + YesNoAbstainVoteChoice::Abstain, + ]; + let mut nonce = 1; + for (vote_number, vote_choice) in choices.iter().cycle().take(allowed as usize).enumerate() { + perform_yes_no_vote( + &mut platform, + &vote_poll, + *vote_choice, + &signer, + pro_tx_hash, + &voting_key, + nonce, + platform_version, + ) + .await + .expect("expected the vote change to be accepted"); + nonce += 1; + // Only the latest answer holds the evonode's four. + let state = poll_state(&platform, &vote_poll, platform_version); + for choice in choices { + let expected = if choice == *vote_choice { 4 } else { 0 }; + assert_eq!(state.voting_power(choice), expected, "vote {}", vote_number); + } + assert_eq!( + platform + .drive + .fetch_identity_yes_no_vote( + pro_tx_hash, + vote_poll_id, + None, + &mut vec![], + platform_version + ) + .expect("vote"), + Some((*vote_choice, vote_number as u16 + 1)) + ); + } + + let next_choice = choices[allowed as usize % choices.len()]; + let error = perform_yes_no_vote( + &mut platform, + &vote_poll, + next_choice, + &signer, + pro_tx_hash, + &voting_key, + nonce, + platform_version, + ) + .await + .expect_err("expected one change too many to be refused"); + assert_matches!( + error, + UnpaidConsensusError(ConsensusError::StateError( + StateError::MasternodeVotedTooManyTimesError(_) + )) + ); +} + +#[tokio::test] +async fn should_refuse_a_vote_when_the_poll_has_no_fund() { + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + let vote_poll = two_thirds_poll(b"unfunded", 1); + open_poll(&platform, &vote_poll, 0, platform_version); + let (pro_tx_hash, signer, voting_key) = + setup_voter(&mut platform, 100, false, platform_version); + // Block processing does not act on the prefunded balance pre-check today (its result is + // not read in the processor, for either poll kind), so the vote fails when its cost is + // deducted; #4904 turns that into an unpaid consensus error. Either way nothing of the + // vote is recorded. + let error = perform_yes_no_vote( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::Yes, + &signer, + pro_tx_hash, + &voting_key, + 1, + platform_version, + ) + .await + .expect_err("expected the vote to be refused"); + assert!( + matches!(&error, InternalError(message) if message.contains("prefunded specialized balance")) + || matches!( + &error, + UnpaidConsensusError(ConsensusError::StateError( + StateError::PrefundedSpecializedBalanceNotFoundError(_) + )) + ), + "unexpected result {:?}", + error + ); + assert_eq!( + platform + .drive + .fetch_identity_yes_no_vote( + pro_tx_hash, + vote_poll.unique_id().expect("id"), + None, + &mut vec![], + platform_version + ) + .expect("vote"), + None + ); + let state = poll_state(&platform, &vote_poll, platform_version); + assert_eq!(state.yes_voting_power, 0); +} + +#[tokio::test] +async fn should_remove_the_votes_of_masternodes_that_left_the_list() { + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + let vote_poll = two_thirds_poll(b"removals", 1); + open_poll(&platform, &vote_poll, FUND, platform_version); + let yes_voters = cast_votes( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::Yes, + 3, + 100, + platform_version, + ) + .await; + let no_voters = cast_votes( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::No, + 2, + 200, + platform_version, + ) + .await; + let vote_poll_id = vote_poll.unique_id().expect("id"); + + let platform_state_before_removals = platform.state.load().deref().clone(); + let removed = vec![yes_voters[0], yes_voters[1], no_voters[0]]; + take_down_masternode_identities(&mut platform, &removed); + let block_platform_state = platform.state.load(); + let transaction = platform.drive.grove.start_transaction(); + platform + .remove_votes_for_removed_masternodes( + &BlockInfo::default(), + &platform_state_before_removals, + &block_platform_state, + Some(&transaction), + platform_version, + ) + .expect("expected to remove votes for removed masternodes"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let state = poll_state(&platform, &vote_poll, platform_version); + assert_eq!((state.yes_voting_power, state.no_voting_power), (1, 1)); + let voters = platform + .drive + .fetch_identities_voting_in_yes_no_vote_poll(&vote_poll, None, platform_version) + .expect("voters"); + assert_eq!(voters[&YesNoAbstainVoteChoice::Yes], vec![yes_voters[2]]); + assert_eq!(voters[&YesNoAbstainVoteChoice::No], vec![no_voters[1]]); + for voter in &removed { + assert_eq!( + platform + .drive + .fetch_identity_yes_no_vote( + *voter, + vote_poll_id, + None, + &mut vec![], + platform_version + ) + .expect("vote"), + None + ); + } + assert_eq!( + platform + .drive + .fetch_identity_yes_no_vote( + yes_voters[2], + vote_poll_id, + None, + &mut vec![], + platform_version + ) + .expect("vote"), + Some((YesNoAbstainVoteChoice::Yes, 1)) + ); + + // The poll still closes cleanly with what is left. + close_ended_polls(&mut platform, platform_version); + let result = *poll_state(&platform, &vote_poll, platform_version) + .stored_info + .expect("stored info") + .result() + .expect("expected the poll to be finished"); + assert_eq!((result.yes_voting_power, result.no_voting_power), (1, 1)); + assert!(!result.passed); +} + +#[tokio::test] +async fn should_close_a_contested_poll_and_a_yes_no_poll_that_end_at_the_same_time() { + use crate::execution::validation::state_transition::state_transitions::tests::create_dpns_identity_name_contest; + + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + let platform_state = platform.state.load(); + // The DPNS contest ends two weeks after its creation at the genesis block time. + let (_contender_1, _contender_2, _dpns_contract) = create_dpns_identity_name_contest( + &mut platform, + &platform_state, + 7, + "quantum", + platform_version, + ) + .await; + // The yes/no poll ends at the very millisecond the contest does. + let contest_end_date = *VotePollsByEndDateDriveQuery { + start_time: None, + end_time: None, + limit: None, + offset: None, + order_ascending: true, + } + .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) + .expect("end dates") + .keys() + .next() + .expect("expected the contest's end date"); + let vote_poll = two_thirds_poll(b"shared end date", 1); + open_poll_ending_at( + &platform, + &vote_poll, + contest_end_date, + FUND, + platform_version, + ); + cast_votes( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::Yes, + 1, + 100, + platform_version, + ) + .await; + + let end_dates = VotePollsByEndDateDriveQuery { + start_time: None, + end_time: None, + limit: None, + offset: None, + order_ascending: true, + } + .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) + .expect("end dates"); + assert_eq!(end_dates.len(), 1, "both polls end at the same time"); + assert_eq!(end_dates.values().next().expect("polls").len(), 2); + + close_ended_polls_at(&mut platform, contest_end_date + 300_000, platform_version); + + let end_dates = VotePollsByEndDateDriveQuery { + start_time: None, + end_time: None, + limit: None, + offset: None, + order_ascending: true, + } + .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) + .expect("end dates"); + assert!(end_dates.is_empty()); + assert!( + poll_state(&platform, &vote_poll, platform_version) + .stored_info + .expect("stored info") + .result() + .expect("finished") + .passed + ); +} diff --git a/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs index 12cd94cfb46..e3399b6e697 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs @@ -970,28 +970,45 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( } } StateTransitionAction::MasternodeVoteAction(masternode_vote_action) => { - let data_contract = match masternode_vote_action.vote_ref() { - ResolvedVote::ResolvedResourceVote(resource_vote) => match resource_vote - .vote_poll() - { - ResolvedVotePoll::ContestedDocumentResourceVotePollWithContractInfo( - contested_document_resource_vote_poll, - ) => contested_document_resource_vote_poll.contract.as_ref(), - }, - }; - - let vote: Vote = masternode_vote_action.vote_ref().clone().into(); + let vote: Vote = masternode_vote_action + .vote_ref() + .clone() + .try_into() + .expect("expected a vote"); // we expect to get a vote that matches the state transition - let (root_hash_vote, maybe_vote) = Drive::verify_masternode_vote( - &response_proof.grovedb_proof, - masternode_vote_action.pro_tx_hash().into_buffer(), - &vote, - data_contract, - false, // we are not in a subset, we have just one vote - platform_version, - ) - .expect("expected to verify balance identity"); + let (root_hash_vote, maybe_vote) = match masternode_vote_action.vote_ref() { + ResolvedVote::ResolvedResourceVote(resource_vote) => { + let data_contract = match resource_vote.vote_poll() { + ResolvedVotePoll::ContestedDocumentResourceVotePollWithContractInfo( + contested_document_resource_vote_poll, + ) => contested_document_resource_vote_poll.contract.as_ref(), + ResolvedVotePoll::YesNoVotePoll(_) => { + panic!("a resource vote cannot answer a yes/no vote poll") + } + }; + Drive::verify_masternode_vote( + &response_proof.grovedb_proof, + masternode_vote_action.pro_tx_hash().into_buffer(), + &vote, + data_contract, + false, // we are not in a subset, we have just one vote + platform_version, + ) + .expect("expected to verify the masternode vote") + } + ResolvedVote::YesNoVote(yes_no_vote) => { + let (root_hash, maybe_vote) = Drive::verify_masternode_yes_no_vote( + &response_proof.grovedb_proof, + masternode_vote_action.pro_tx_hash().into_buffer(), + &yes_no_vote.clone().into(), + false, + platform_version, + ) + .expect("expected to verify the masternode yes/no vote"); + (root_hash, maybe_vote.map(Vote::YesNoVote)) + } + }; assert_eq!( &root_hash_vote, diff --git a/packages/rs-drive/grovedb-structure.json b/packages/rs-drive/grovedb-structure.json index 8a074d22cd9..f78ab73c211 100644 --- a/packages/rs-drive/grovedb-structure.json +++ b/packages/rs-drive/grovedb-structure.json @@ -3948,7 +3948,7 @@ "since": 1, "presence": "always", "source": "packages/rs-drive/src/drive/mod.rs", - "description": "Masternode votes, today on contested resources such as premium DPNS names.", + "description": "Masternode votes: on contested resources such as premium DPNS names, and on yes/no decisions.", "children": [ { "id": "votes.contested_resource", @@ -4473,8 +4473,251 @@ "since": 1, "presence": "always", "source": "packages/rs-drive/src/drive/votes/paths.rs", - "description": "Reserved for polls that decide something for the network. Nothing writes to it yet.", - "children": [] + "description": "Yes/no polls: decisions taken by a supermajority of the masternodes once enough voting power was cast.", + "children": [ + { + "id": "votes.decisions.identity_votes", + "key": { + "type": "fixed", + "hex": "69", + "label": "DecisionIdentityVotes", + "constant": "IDENTITY_VOTES_TREE_KEY", + "ascii": true + }, + "kinds": [ + "Tree" + ], + "since": 14, + "presence": "always", + "source": "packages/rs-drive/src/drive/votes/paths.rs", + "description": "The yes/no votes each masternode cast, so they can be changed and removed with the masternode.", + "children": [ + { + "id": "votes.decisions.identity_votes.voter", + "key": { + "type": "dynamic", + "name": "pro_tx_hash", + "matcher": { + "type": "len", + "len": 32 + }, + "encoding": "identifier32", + "description": "The voting masternode's pro tx hash" + }, + "kinds": [ + "Tree" + ], + "since": 14, + "presence": "always", + "source": "packages/rs-drive/src/drive/votes/paths.rs", + "description": "One masternode's yes/no votes. Stays once its votes are gone, like its contested counterpart.", + "children": [ + { + "id": "votes.decisions.identity_votes.voter.vote", + "key": { + "type": "dynamic", + "name": "vote_poll_id", + "matcher": { + "type": "len", + "len": 32 + }, + "encoding": "hash32", + "description": "The double sha256 of the serialized vote poll" + }, + "kinds": [ + "Item" + ], + "value": "bincode YesNoVoteReferenceStorageForm: the choice and how many times the masternode voted on the poll", + "since": 14, + "presence": "always", + "source": "packages/rs-drive/src/drive/votes/paths.rs", + "description": "The masternode's current answer to one poll.", + "children": [] + } + ] + } + ] + }, + { + "id": "votes.decisions.active_polls", + "key": { + "type": "fixed", + "hex": "70", + "label": "DecisionActivePolls", + "constant": "ACTIVE_POLLS_TREE_KEY", + "ascii": true + }, + "kinds": [ + "Tree" + ], + "since": 14, + "presence": "always", + "source": "packages/rs-drive/src/drive/votes/paths.rs", + "description": "The yes/no polls, by their unique id.", + "children": [ + { + "id": "votes.decisions.active_polls.poll", + "key": { + "type": "dynamic", + "name": "vote_poll_id", + "matcher": { + "type": "len", + "len": 32 + }, + "encoding": "hash32", + "description": "The double sha256 of the serialized vote poll" + }, + "kinds": [ + "Tree" + ], + "since": 14, + "presence": "always", + "source": "packages/rs-drive/src/drive/votes/paths.rs", + "description": "One yes/no poll: its stored info and its three vote sum trees. The vote trees go when the poll ends; the stored info stays as the record of the decision.", + "children": [ + { + "id": "votes.decisions.active_polls.poll.stored_info", + "key": { + "type": "fixed", + "hex": "00", + "label": "YesNoStoredInfo", + "constant": "YES_NO_VOTE_POLL_STORED_INFO_KEY" + }, + "kinds": [ + "Item" + ], + "value": "serialized YesNoVotePollStoredInfo", + "since": 14, + "presence": "always", + "source": "packages/rs-drive/src/drive/votes/paths.rs", + "description": "The poll's status and, once it ends, its result.", + "children": [] + }, + { + "id": "votes.decisions.active_polls.poll.yes", + "key": { + "type": "fixed", + "hex": "01", + "label": "YesVotes", + "constant": "YES_NO_VOTE_POLL_YES_VOTES_TREE_KEY" + }, + "kinds": [ + "SumTree" + ], + "since": 14, + "presence": "until_deleted", + "source": "packages/rs-drive/src/drive/votes/paths.rs", + "description": "Votes for yes.", + "children": [ + { + "id": "votes.decisions.active_polls.poll.yes.voter", + "key": { + "type": "dynamic", + "name": "pro_tx_hash", + "matcher": { + "type": "len", + "len": 32 + }, + "encoding": "identifier32", + "description": "The voting masternode's pro tx hash" + }, + "kinds": [ + "SumItem" + ], + "value": "vote strength: 1 for a masternode, 4 for an evonode", + "since": 14, + "presence": "always", + "source": "packages/rs-drive/src/drive/votes/paths.rs", + "description": "One masternode's vote; the tree's sum is the tally.", + "children": [] + } + ] + }, + { + "id": "votes.decisions.active_polls.poll.no", + "key": { + "type": "fixed", + "hex": "02", + "label": "NoVotes", + "constant": "YES_NO_VOTE_POLL_NO_VOTES_TREE_KEY" + }, + "kinds": [ + "SumTree" + ], + "since": 14, + "presence": "until_deleted", + "source": "packages/rs-drive/src/drive/votes/paths.rs", + "description": "Votes for no.", + "children": [ + { + "id": "votes.decisions.active_polls.poll.no.voter", + "key": { + "type": "dynamic", + "name": "pro_tx_hash", + "matcher": { + "type": "len", + "len": 32 + }, + "encoding": "identifier32", + "description": "The voting masternode's pro tx hash" + }, + "kinds": [ + "SumItem" + ], + "value": "vote strength: 1 for a masternode, 4 for an evonode", + "since": 14, + "presence": "always", + "source": "packages/rs-drive/src/drive/votes/paths.rs", + "description": "One masternode's vote; the tree's sum is the tally.", + "children": [] + } + ] + }, + { + "id": "votes.decisions.active_polls.poll.abstain", + "key": { + "type": "fixed", + "hex": "03", + "label": "AbstainVotes", + "constant": "YES_NO_VOTE_POLL_ABSTAIN_VOTES_TREE_KEY" + }, + "kinds": [ + "SumTree" + ], + "since": 14, + "presence": "until_deleted", + "source": "packages/rs-drive/src/drive/votes/paths.rs", + "description": "Votes to abstain, which count towards nothing.", + "children": [ + { + "id": "votes.decisions.active_polls.poll.abstain.voter", + "key": { + "type": "dynamic", + "name": "pro_tx_hash", + "matcher": { + "type": "len", + "len": 32 + }, + "encoding": "identifier32", + "description": "The voting masternode's pro tx hash" + }, + "kinds": [ + "SumItem" + ], + "value": "vote strength: 1 for a masternode, 4 for an evonode", + "since": 14, + "presence": "always", + "source": "packages/rs-drive/src/drive/votes/paths.rs", + "description": "One masternode's vote; the tree's sum is the tally.", + "children": [] + } + ] + } + ] + } + ] + } + ] }, { "id": "votes.end_date_queries", @@ -4509,9 +4752,10 @@ "Tree" ], "flags": [ - "EpochOwned" + "EpochOwned", + "None" ], - "flags_note": "The owner is the identity whose contested document started the poll.", + "flags_note": "The owner is the identity whose contested document started the poll. A yes/no poll is opened by the system and carries none.", "since": 1, "presence": "always", "source": "packages/rs-drive/src/drive/votes/paths.rs", @@ -4533,9 +4777,10 @@ "Item" ], "flags": [ - "EpochOwned" + "EpochOwned", + "None" ], - "flags_note": "The owner is the identity whose contested document started the poll.", + "flags_note": "The owner is the identity whose contested document started the poll. A yes/no poll is opened by the system and carries none.", "value": "serialized VotePoll", "since": 1, "presence": "always", @@ -5572,6 +5817,30 @@ } } }, + "votes.decisions": { + "origin": "genesis@14", + "tree": { + "hex": "70", + "left": { + "hex": "69" + } + } + }, + "votes.decisions.active_polls.poll": { + "origin": "fixture yes_no_vote_polls@14", + "tree": { + "hex": "02", + "left": { + "hex": "01", + "left": { + "hex": "00" + } + }, + "right": { + "hex": "03" + } + } + }, "withdrawals": { "origin": "genesis@14", "tree": { diff --git a/packages/rs-drive/src/drive/votes/cleanup/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/mod.rs index e425c9a6c4e..488a517a2c0 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/mod.rs @@ -6,3 +6,6 @@ mod remove_contested_resource_vote_poll_info_operations; mod remove_contested_resource_vote_poll_top_level_index_operations; mod remove_contested_resource_vote_poll_votes_operations; mod remove_specific_votes_given_by_identity; +mod remove_vote_poll_end_date_query_operations; +mod remove_yes_no_vote_poll_votes_operations; +mod remove_yes_no_vote_references_given_by_identity; diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/mod.rs index f93d92a424d..2f0afe81d3d 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use crate::drive::Drive; @@ -27,7 +28,7 @@ impl Drive { .methods .vote .cleanup - .remove_specific_vote_references_given_by_identity + .remove_all_votes_given_by_identities { 0 => self.remove_all_votes_given_by_identities_v0( identity_ids_as_byte_arrays, @@ -37,9 +38,17 @@ impl Drive { transaction, platform_version, ), + 1 => self.remove_all_votes_given_by_identities_v1( + identity_ids_as_byte_arrays, + block_height, + network, + chain_id, + transaction, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "remove_all_votes_given_by_identities".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs new file mode 100644 index 00000000000..750ba852ad7 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs @@ -0,0 +1,234 @@ +use crate::drive::Drive; +use std::ops::RangeFull; + +use crate::error::drive::DriveError; +use crate::error::Error; + +use crate::drive::votes::paths::{ + vote_contested_resource_identity_votes_tree_path_for_identity, + vote_contested_resource_identity_votes_tree_path_vec, + vote_decisions_active_polls_poll_tree_path_vec, + vote_decisions_identity_votes_tree_path_for_identity, + vote_decisions_identity_votes_tree_path_vec, vote_decisions_tree_path, IDENTITY_VOTES_TREE_KEY, +}; +use crate::drive::votes::storage_form::contested_document_resource_reference_storage_form::ContestedDocumentResourceVoteReferenceStorageForm; +use crate::drive::votes::storage_form::yes_no_vote_reference_storage_form::YesNoVoteReferenceStorageForm; +use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; +use crate::query::QueryItem; +use crate::util::grove_operations::{BatchDeleteApplyType, DirectQueryType}; +use dpp::dashcore::Network; +use dpp::prelude::{BlockHeight, Identifier}; +use dpp::version::PlatformVersion; +use grovedb::query_result_type::QueryResultType::QueryPathKeyElementTrioResultType; +use grovedb::{Element, MaybeTree, PathQuery, Query, SizedQuery, TransactionArg}; +use std::collections::BTreeMap; + +impl Drive { + /// Version 1 (protocol version 14) removes the yes/no votes of the removed masternodes + /// too: the vote under the poll's choice sum tree and the entry in the decisions identity + /// votes index. + pub(super) fn remove_all_votes_given_by_identities_v1( + &self, + identity_ids_as_byte_arrays: Vec>, + block_height: BlockHeight, + network: Network, + chain_id: &str, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let mut deletion_batch = vec![]; + + // Contested resource votes, exactly as version 0. + let vote_path = vote_contested_resource_identity_votes_tree_path_vec(); + let mut query = Query::new_with_direction(true); + query.insert_keys(identity_ids_as_byte_arrays.clone()); + let subquery = Query::new_single_query_item(QueryItem::RangeFull(RangeFull)); + query.set_subquery(subquery); + let path_query = PathQuery::new(vote_path.clone(), SizedQuery::new(query, None, None)); + let votes_to_remove_by_identity_id = self + .grove_get_raw_path_query( + &path_query, + transaction, + QueryPathKeyElementTrioResultType, + &mut vec![], + &platform_version.drive, + )? + .0 + .to_last_path_to_key_elements_btree_map(); + + for (identifier_bytes, votes_to_remove) in votes_to_remove_by_identity_id { + let identity_id = Identifier::from_vec(identifier_bytes.clone())?; + let vote_path_ref = vote_contested_resource_identity_votes_tree_path_for_identity( + identity_id.as_bytes(), + ); + for (vote_id, vote_to_remove) in votes_to_remove { + // We delete the vote item as reference + self.batch_delete( + vote_path_ref.as_slice().into(), + vote_id.as_slice(), + BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + }, + transaction, + &mut deletion_batch, + &platform_version.drive, + )?; + + let serialized_reference = vote_to_remove.into_item_bytes()?; + let bincode_config = bincode::config::standard() + .with_big_endian() + .with_no_limit(); + let reference: ContestedDocumentResourceVoteReferenceStorageForm = + bincode::decode_from_slice(&serialized_reference, bincode_config) + .map_err(|e| { + Error::Drive(DriveError::CorruptedSerialization(format!( + "serialization of reference {} is corrupted: {}", + hex::encode(serialized_reference), + e + ))) + })? + .0; + + let mut absolute_path = reference + .reference_path_type + .absolute_path(vote_path_ref.as_slice(), Some(vote_id.as_slice()))?; + + // we then need to add to the batch the deletion + absolute_path.pop(); // we need to get rid of the key (which is the identifier bytes) + + let absolute_path_ref: Vec<_> = + absolute_path.iter().map(|a| a.as_slice()).collect(); + + self.batch_delete( + absolute_path_ref.as_slice().into(), + identifier_bytes.as_slice(), + BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + }, + transaction, + &mut deletion_batch, + &platform_version.drive, + )?; + } + } + + // Yes/no votes: the index item names the poll and the answer, so the vote sits under + // the poll's tree at the answer's sum tree. A chain that activated protocol version 14 + // before the decisions trees existed has no identity votes index, and no yes/no votes. + let has_decisions_index = self.grove_has_raw( + (&vote_decisions_tree_path()).into(), + &[IDENTITY_VOTES_TREE_KEY as u8], + DirectQueryType::StatefulDirectQuery, + transaction, + &mut vec![], + &platform_version.drive, + )?; + let yes_no_votes_to_remove_by_identity_id = if has_decisions_index { + self.yes_no_votes_of_identities( + identity_ids_as_byte_arrays, + transaction, + platform_version, + )? + } else { + BTreeMap::new() + }; + + for (identifier_bytes, votes_to_remove) in yes_no_votes_to_remove_by_identity_id { + let identity_id = Identifier::from_vec(identifier_bytes.clone())?; + let vote_path_ref = + vote_decisions_identity_votes_tree_path_for_identity(identity_id.as_bytes()); + for (vote_poll_id, vote_to_remove) in votes_to_remove { + self.batch_delete( + vote_path_ref.as_slice().into(), + vote_poll_id.as_slice(), + BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + }, + transaction, + &mut deletion_batch, + &platform_version.drive, + )?; + + let serialized_form = vote_to_remove.into_item_bytes()?; + let storage_form = YesNoVoteReferenceStorageForm::deserialize(&serialized_form) + .map_err(|e| { + Error::Drive(DriveError::CorruptedSerialization(format!( + "serialization of yes/no vote reference {} is corrupted: {}", + hex::encode(serialized_form), + e + ))) + })?; + let vote_poll_id = Identifier::from_vec(vote_poll_id)?; + let mut votes_path = + vote_decisions_active_polls_poll_tree_path_vec(vote_poll_id.as_bytes()); + votes_path.push(vec![storage_form.vote_choice.to_tree_key()]); + self.batch_delete( + votes_path.as_slice().into(), + identifier_bytes.as_slice(), + BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + }, + transaction, + &mut deletion_batch, + &platform_version.drive, + )?; + } + } + + if !deletion_batch.is_empty() { + // We had a sequence of errors on the mainnet started since block 32326. + // We got RocksDB's "transaction is busy" error because of a bug (https://github.com/dashpay/platform/pull/2309). + // Due to another bug in Tenderdash (https://github.com/dashpay/tenderdash/pull/966), + // validators just proceeded to the next block partially committing the state + // and updating the cache (https://github.com/dashpay/platform/pull/2305). + // Full nodes are stuck and proceeded after re-sync. + // For the mainnet chain, we enable this fix at the block when we consider the state is consistent. + let transaction = + if network == Network::Mainnet && chain_id == "evo1" && block_height < 32329 { + // Old behaviour on mainnet + None + } else { + // We should use transaction + transaction + }; + + self.apply_batch_low_level_drive_operations( + None, + transaction, + deletion_batch, + &mut vec![], + &platform_version.drive, + ) + .inspect_err(|err| tracing::error!(?err, "vote deletion batch failed"))?; + } + + Ok(()) + } + + /// The yes/no vote index entries of these masternodes, by masternode then poll id. + #[allow(clippy::type_complexity)] + fn yes_no_votes_of_identities( + &self, + identity_ids_as_byte_arrays: Vec>, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, BTreeMap, Element>>, Error> { + let decisions_vote_path = vote_decisions_identity_votes_tree_path_vec(); + let mut query = Query::new_with_direction(true); + query.insert_keys(identity_ids_as_byte_arrays); + query.set_subquery(Query::new_single_query_item(QueryItem::RangeFull( + RangeFull, + ))); + let path_query = PathQuery::new(decisions_vote_path, SizedQuery::new(query, None, None)); + Ok(self + .grove_get_raw_path_query( + &path_query, + transaction, + QueryPathKeyElementTrioResultType, + &mut vec![], + &platform_version.drive, + )? + .0 + .to_last_path_to_key_elements_btree_map()) + } +} diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs new file mode 100644 index 00000000000..5e34e3b9965 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs @@ -0,0 +1,44 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::identifier::Identifier; +use dpp::identity::TimestampMillis; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +impl Drive { + /// Removes finished polls of every kind from the end date index, and each end date's tree + /// once nothing is left under it. `vote_polls` holds every poll closed in this block with + /// its end date, so the count at one end date covers both kinds: a partial count would + /// delete a tree that still holds a poll of the other kind. + pub fn remove_vote_poll_end_date_query_operations( + &self, + vote_polls: &[(Identifier, TimestampMillis)], + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive + .methods + .vote + .cleanup + .remove_vote_poll_end_date_query_operations + { + 0 => self.remove_vote_poll_end_date_query_operations_v0( + vote_polls, + batch_operations, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "remove_vote_poll_end_date_query_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs new file mode 100644 index 00000000000..b71398773f2 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs @@ -0,0 +1,93 @@ +use crate::drive::votes::paths::{ + vote_contested_resource_end_date_queries_at_time_tree_path_vec, + vote_end_date_queries_tree_path_vec, +}; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::query::VotePollsByEndDateDriveQuery; +use crate::util::common::encode::encode_u64; +use crate::util::grove_operations::BatchDeleteApplyType; +use dpp::identifier::Identifier; +use dpp::identity::TimestampMillis; +use grovedb::{MaybeTree, TransactionArg}; +use platform_version::version::PlatformVersion; +use std::collections::BTreeMap; + +impl Drive { + pub(super) fn remove_vote_poll_end_date_query_operations_v0( + &self, + vote_polls: &[(Identifier, TimestampMillis)], + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + // This is a GroveDB Tree (Not Sub Tree Merk representation) + // End Date queries + // / \ + // 15/08/2025 5PM 15/08/2025 6PM + // / \ | + // VotePoll Info 1 VotePoll Info 2 VotePoll Info 3 + let delete_apply_type = BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + }; + + let mut by_end_date: BTreeMap> = BTreeMap::new(); + for (unique_id, end_date) in vote_polls { + by_end_date.entry(*end_date).or_default().push(*unique_id); + } + + for (end_date, unique_ids) in by_end_date { + let time_path = + vote_contested_resource_end_date_queries_at_time_tree_path_vec(end_date); + let count = unique_ids.len(); + for unique_id in unique_ids { + self.batch_delete( + time_path.as_slice().into(), + unique_id.as_bytes(), + delete_apply_type, + transaction, + batch_operations, + &platform_version.drive, + )?; + } + + // The block closes at most `maximum_vote_polls_to_process` polls. Fewer than that + // at one end date means every poll ending then was fetched, so the tree empties; + // otherwise look whether more are queued behind the ones processed. + let maximum_vote_polls_to_process = platform_version + .drive_abci + .validation_and_processing + .event_constants + .maximum_vote_polls_to_process; + let should_delete_parent_time_tree = if count < maximum_vote_polls_to_process as usize { + true + } else { + let total_count = + VotePollsByEndDateDriveQuery::execute_no_proof_for_specialized_end_time_query_only_check_end_time( + end_date, + maximum_vote_polls_to_process + 1, + self, + transaction, + &mut vec![], + platform_version, + )? + .len(); + total_count <= count + }; + + if should_delete_parent_time_tree { + self.batch_delete( + vote_end_date_queries_tree_path_vec().as_slice().into(), + encode_u64(end_date).as_slice(), + delete_apply_type, + transaction, + batch_operations, + &platform_version.drive, + )?; + } + } + + Ok(()) + } +} diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs new file mode 100644 index 00000000000..5c0f72696ab --- /dev/null +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs @@ -0,0 +1,47 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use grovedb::TransactionArg; +use std::collections::BTreeMap; + +impl Drive { + /// Removes the votes of finished yes/no polls and the three sum trees that held them. The + /// polls' stored info stays as the record of the decision. + pub fn remove_yes_no_vote_poll_votes_operations( + &self, + vote_polls: &[( + &YesNoVotePoll, + &BTreeMap>, + )], + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive + .methods + .vote + .yes_no + .remove_yes_no_vote_poll_votes_operations + { + 0 => self.remove_yes_no_vote_poll_votes_operations_v0( + vote_polls, + batch_operations, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "remove_yes_no_vote_poll_votes_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs new file mode 100644 index 00000000000..fe1e0fa7c2d --- /dev/null +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs @@ -0,0 +1,58 @@ +use crate::drive::votes::paths::YesNoVotePollPaths; +use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::grove_operations::BatchDeleteApplyType; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use grovedb::{MaybeTree, TransactionArg, TreeType}; +use std::collections::BTreeMap; + +impl Drive { + pub(super) fn remove_yes_no_vote_poll_votes_operations_v0( + &self, + vote_polls: &[( + &YesNoVotePoll, + &BTreeMap>, + )], + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + for (vote_poll, voters_by_choice) in vote_polls { + let poll_path = vote_poll.poll_path_vec()?; + for vote_choice in YesNoAbstainVoteChoice::ALL { + let votes_path = vote_poll.choice_votes_path_vec(vote_choice)?; + if let Some(voters) = voters_by_choice.get(&vote_choice) { + for voter in voters { + self.batch_delete( + votes_path.as_slice().into(), + voter.as_slice(), + BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + }, + transaction, + batch_operations, + &platform_version.drive, + )?; + } + } + // The sum tree itself, now empty. + self.batch_delete( + poll_path.as_slice().into(), + &[vote_choice.to_tree_key()], + BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::Tree(TreeType::SumTree)), + }, + transaction, + batch_operations, + &platform_version.drive, + )?; + } + } + Ok(()) + } +} diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs new file mode 100644 index 00000000000..5e869ff6bb5 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs @@ -0,0 +1,43 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +impl Drive { + /// Removes a masternode's entries for these yes/no polls from the identity votes index of + /// the decisions branch. + pub fn remove_yes_no_vote_references_given_by_identity_operations( + &self, + masternode_pro_tx_hash: &Identifier, + vote_poll_ids: &[&Identifier], + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive + .methods + .vote + .yes_no + .remove_yes_no_vote_references_given_by_identity + { + 0 => self.remove_yes_no_vote_references_given_by_identity_operations_v0( + masternode_pro_tx_hash, + vote_poll_ids, + batch_operations, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "remove_yes_no_vote_references_given_by_identity_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs new file mode 100644 index 00000000000..00176cb6ee9 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs @@ -0,0 +1,35 @@ +use crate::drive::votes::paths::vote_decisions_identity_votes_tree_path_for_identity; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::grove_operations::BatchDeleteApplyType; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use grovedb::{MaybeTree, TransactionArg}; + +impl Drive { + pub(super) fn remove_yes_no_vote_references_given_by_identity_operations_v0( + &self, + masternode_pro_tx_hash: &Identifier, + vote_poll_ids: &[&Identifier], + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let path = + vote_decisions_identity_votes_tree_path_for_identity(masternode_pro_tx_hash.as_bytes()); + for vote_poll_id in vote_poll_ids { + self.batch_delete( + path.as_slice().into(), + vote_poll_id.as_slice(), + BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + }, + transaction, + batch_operations, + &platform_version.drive, + )?; + } + Ok(()) + } +} diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs new file mode 100644 index 00000000000..4cf401ad926 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs @@ -0,0 +1,41 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use grovedb::TransactionArg; +use std::collections::BTreeMap; + +impl Drive { + /// The masternodes that voted on a yes/no poll, by their answer. Every choice is present, + /// with an empty list when nobody chose it. + pub fn fetch_identities_voting_in_yes_no_vote_poll( + &self, + vote_poll: &YesNoVotePoll, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .vote + .yes_no + .fetch_identities_voting_in_yes_no_vote_poll + { + 0 => self.fetch_identities_voting_in_yes_no_vote_poll_v0( + vote_poll, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "fetch_identities_voting_in_yes_no_vote_poll".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs new file mode 100644 index 00000000000..788e6e9a976 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs @@ -0,0 +1,75 @@ +use crate::drive::votes::paths::YesNoVotePollPaths; +use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use grovedb::query_result_type::QueryResultType; +use grovedb::{PathQuery, Query, QueryItem, SizedQuery, TransactionArg}; +use std::collections::BTreeMap; +use std::ops::RangeFull; + +impl Drive { + pub(super) fn fetch_identities_voting_in_yes_no_vote_poll_v0( + &self, + vote_poll: &YesNoVotePoll, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + let path = vote_poll.poll_path_vec()?; + let mut query = Query::new_with_direction(true); + query.insert_keys( + YesNoAbstainVoteChoice::ALL + .iter() + .map(|vote_choice| vec![vote_choice.to_tree_key()]) + .collect(), + ); + query.set_subquery(Query::new_single_query_item(QueryItem::RangeFull( + RangeFull, + ))); + let path_query = PathQuery { + path, + query: SizedQuery { + query, + limit: None, + offset: None, + }, + }; + let voters_by_choice_key = self + .grove_get_path_query( + &path_query, + transaction, + QueryResultType::QueryPathKeyElementTrioResultType, + &mut vec![], + &platform_version.drive, + )? + .0 + .to_last_path_to_key_elements_btree_map(); + let mut voters_by_choice: BTreeMap> = + YesNoAbstainVoteChoice::ALL + .iter() + .map(|vote_choice| (*vote_choice, vec![])) + .collect(); + for (choice_key, voters) in voters_by_choice_key { + let vote_choice = choice_key + .first() + .filter(|_| choice_key.len() == 1) + .and_then(|key| YesNoAbstainVoteChoice::from_tree_key(*key)) + .ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState(format!( + "unexpected key {} in yes/no vote poll tree", + hex::encode(&choice_key) + ))) + })?; + let voters = voters + .into_keys() + .map(|voter| voter.try_into()) + .collect::, dpp::platform_value::Error>>()?; + voters_by_choice.insert(vote_choice, voters); + } + Ok(voters_by_choice) + } +} diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs new file mode 100644 index 00000000000..3c4324c266f --- /dev/null +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs @@ -0,0 +1,45 @@ +mod v0; + +use crate::drive::votes::resolved::votes::resolved_yes_no_vote::PreviousYesNoVoteCount; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use grovedb::TransactionArg; + +impl Drive { + /// A masternode's current answer to a yes/no poll and how many times it voted on it, or + /// `None` when it never did. + pub fn fetch_identity_yes_no_vote( + &self, + masternode_pro_tx_hash: Identifier, + vote_poll_id: Identifier, + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .vote + .yes_no + .fetch_identity_yes_no_vote + { + 0 => self.fetch_identity_yes_no_vote_v0( + masternode_pro_tx_hash, + vote_poll_id, + transaction, + drive_operations, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "fetch_identity_yes_no_vote".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs new file mode 100644 index 00000000000..7c9252f85cc --- /dev/null +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs @@ -0,0 +1,48 @@ +use crate::drive::votes::paths::vote_decisions_identity_votes_tree_path_for_identity; +use crate::drive::votes::resolved::votes::resolved_yes_no_vote::PreviousYesNoVoteCount; +use crate::drive::votes::storage_form::yes_no_vote_reference_storage_form::YesNoVoteReferenceStorageForm; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::grove_operations::DirectQueryType; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use grovedb::TransactionArg; + +impl Drive { + pub(super) fn fetch_identity_yes_no_vote_v0( + &self, + masternode_pro_tx_hash: Identifier, + vote_poll_id: Identifier, + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let path = + vote_decisions_identity_votes_tree_path_for_identity(masternode_pro_tx_hash.as_bytes()); + let maybe_element = self.grove_get_raw_optional( + (&path).into(), + vote_poll_id.as_slice(), + DirectQueryType::StatefulDirectQuery, + transaction, + drive_operations, + &platform_version.drive, + )?; + maybe_element + .map(|element| { + let bytes = element.into_item_bytes()?; + let storage_form = + YesNoVoteReferenceStorageForm::deserialize(&bytes).map_err(|e| { + Error::Drive(DriveError::CorruptedSerialization(format!( + "serialization of yes/no vote reference {} is corrupted: {}", + hex::encode(bytes), + e + ))) + })?; + Ok((storage_form.vote_choice, storage_form.identity_vote_times)) + }) + .transpose() + } +} diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs new file mode 100644 index 00000000000..32f6aec9697 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs @@ -0,0 +1,36 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use dpp::version::PlatformVersion; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStoredInfo; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use grovedb::TransactionArg; + +impl Drive { + /// The stored info of a yes/no vote poll, `None` when the poll was never opened. + pub fn fetch_yes_no_vote_poll_stored_info( + &self, + vote_poll: &YesNoVotePoll, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .vote + .yes_no + .fetch_yes_no_vote_poll_stored_info + { + 0 => { + self.fetch_yes_no_vote_poll_stored_info_v0(vote_poll, transaction, platform_version) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "fetch_yes_no_vote_poll_stored_info".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs new file mode 100644 index 00000000000..654fe0d5386 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs @@ -0,0 +1,35 @@ +use crate::drive::votes::paths::{YesNoVotePollPaths, YES_NO_VOTE_POLL_STORED_INFO_KEY}; +use crate::drive::Drive; +use crate::error::Error; +use crate::util::grove_operations::DirectQueryType; +use dpp::serialization::PlatformDeserializableTrusted; +use dpp::version::PlatformVersion; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStoredInfo; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use grovedb::TransactionArg; + +impl Drive { + pub(super) fn fetch_yes_no_vote_poll_stored_info_v0( + &self, + vote_poll: &YesNoVotePoll, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let path = vote_poll.poll_path_vec()?; + let maybe_element = self.grove_get_raw_optional( + path.as_slice().into(), + &[YES_NO_VOTE_POLL_STORED_INFO_KEY], + DirectQueryType::StatefulDirectQuery, + transaction, + &mut vec![], + &platform_version.drive, + )?; + let Some(element) = maybe_element else { + return Ok(None); + }; + let bytes = element.into_item_bytes()?; + Ok(Some( + YesNoVotePollStoredInfo::deserialize_from_bytes_trusted(&bytes)?, + )) + } +} diff --git a/packages/rs-drive/src/drive/votes/fetch/mod.rs b/packages/rs-drive/src/drive/votes/fetch/mod.rs index 74748b04f0c..982be172512 100644 --- a/packages/rs-drive/src/drive/votes/fetch/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/mod.rs @@ -1,3 +1,6 @@ mod fetch_contested_document_vote_poll_stored_info; mod fetch_identities_voting_for_contenders; +mod fetch_identities_voting_in_yes_no_vote_poll; mod fetch_identity_contested_resource_vote; +mod fetch_identity_yes_no_vote; +mod fetch_yes_no_vote_poll_stored_info; diff --git a/packages/rs-drive/src/drive/votes/insert/mod.rs b/packages/rs-drive/src/drive/votes/insert/mod.rs index 67ea229fe2a..02c9ff21d61 100644 --- a/packages/rs-drive/src/drive/votes/insert/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/mod.rs @@ -1,3 +1,4 @@ mod contested_resource; mod register_identity_vote; mod vote_poll; +mod yes_no; diff --git a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/mod.rs b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/mod.rs index 1060ea88dc1..d1460122dc9 100644 --- a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use crate::drive::Drive; @@ -66,9 +67,18 @@ impl Drive { transaction, platform_version, ), + 1 => self.register_identity_vote_v1( + voter_pro_tx_hash, + strength, + vote, + previous_resource_vote_choice_to_remove, + block_info, + transaction, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "register_identity_vote".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } @@ -123,9 +133,17 @@ impl Drive { transaction, platform_version, ), + 1 => self.register_identity_vote_operations_v1( + voter_pro_tx_hash, + strength, + vote, + previous_resource_vote_choice_to_remove, + transaction, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "register_identity_vote_operations".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v0/mod.rs index e5cd5a52fd0..3ef1536caa9 100644 --- a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v0/mod.rs @@ -2,6 +2,7 @@ use crate::drive::votes::resolved::vote_polls::ResolvedVotePoll; use crate::drive::votes::resolved::votes::resolved_resource_vote::accessors::v0::ResolvedResourceVoteGettersV0; use crate::drive::votes::resolved::votes::ResolvedVote; use crate::drive::Drive; +use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::state_transition_action::identity::masternode_vote::v0::PreviousVoteCount; @@ -39,8 +40,14 @@ impl Drive { transaction, platform_version, ), + ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Drive( + DriveError::CorruptedCodeExecution("yes/no vote polls need version 1"), + )), } } + ResolvedVote::YesNoVote(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution( + "yes/no votes need version 1", + ))), } } @@ -68,8 +75,14 @@ impl Drive { transaction, platform_version, ), + ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Drive( + DriveError::CorruptedCodeExecution("yes/no vote polls need version 1"), + )), } } + ResolvedVote::YesNoVote(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution( + "yes/no votes need version 1", + ))), } } } diff --git a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v1/mod.rs b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v1/mod.rs new file mode 100644 index 00000000000..5c98e918067 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v1/mod.rs @@ -0,0 +1,103 @@ +use crate::drive::votes::resolved::vote_polls::ResolvedVotePoll; +use crate::drive::votes::resolved::votes::resolved_resource_vote::accessors::v0::ResolvedResourceVoteGettersV0; +use crate::drive::votes::resolved::votes::ResolvedVote; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::identity::masternode_vote::v0::PreviousVoteCount; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; + +impl Drive { + /// Version 1 (protocol version 14) registers a vote of either kind: a resource vote as + /// before, and a yes/no vote under its poll in the decisions branch. + #[allow(clippy::too_many_arguments)] + pub(super) fn register_identity_vote_v1( + &self, + voter_pro_tx_hash: [u8; 32], + strength: u8, + vote: ResolvedVote, + previous_resource_vote_choice_to_remove: Option<(ResourceVoteChoice, PreviousVoteCount)>, + block_info: &BlockInfo, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match vote { + ResolvedVote::ResolvedResourceVote(resource_vote) => { + let vote_choice = resource_vote.resource_vote_choice(); + match resource_vote.vote_poll_owned() { + ResolvedVotePoll::ContestedDocumentResourceVotePollWithContractInfo( + contested_document_resource_vote_poll, + ) => self.register_contested_resource_identity_vote( + voter_pro_tx_hash, + strength, + contested_document_resource_vote_poll, + vote_choice, + previous_resource_vote_choice_to_remove, + block_info, + transaction, + platform_version, + ), + ResolvedVotePoll::YesNoVotePoll(_) => { + Err(Error::Drive(DriveError::CorruptedCodeExecution( + "a resource vote cannot answer a yes/no vote poll", + ))) + } + } + } + ResolvedVote::YesNoVote(yes_no_vote) => self.register_yes_no_identity_vote( + voter_pro_tx_hash, + strength, + yes_no_vote, + block_info, + transaction, + platform_version, + ), + } + } + + pub(super) fn register_identity_vote_operations_v1( + &self, + voter_pro_tx_hash: [u8; 32], + strength: u8, + vote: ResolvedVote, + previous_resource_vote_choice_to_remove: Option<(ResourceVoteChoice, PreviousVoteCount)>, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match vote { + ResolvedVote::ResolvedResourceVote(resource_vote) => { + let vote_choice = resource_vote.resource_vote_choice(); + match resource_vote.vote_poll_owned() { + ResolvedVotePoll::ContestedDocumentResourceVotePollWithContractInfo( + contested_document_resource_vote_poll, + ) => self.register_contested_resource_identity_vote_operations( + voter_pro_tx_hash, + strength, + contested_document_resource_vote_poll, + vote_choice, + previous_resource_vote_choice_to_remove, + transaction, + platform_version, + ), + ResolvedVotePoll::YesNoVotePoll(_) => { + Err(Error::Drive(DriveError::CorruptedCodeExecution( + "a resource vote cannot answer a yes/no vote poll", + ))) + } + } + } + ResolvedVote::YesNoVote(yes_no_vote) => self.register_yes_no_identity_vote_operations( + voter_pro_tx_hash, + strength, + yes_no_vote, + transaction, + platform_version, + ), + } + } +} diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/mod.rs new file mode 100644 index 00000000000..4ddddcd1ef2 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/mod.rs @@ -0,0 +1,68 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::version::PlatformVersion; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStoredInfo; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use grovedb::TransactionArg; + +impl Drive { + /// Writes the stored info of a yes/no vote poll, replacing what was there. + pub fn insert_stored_info_for_yes_no_vote_poll( + &self, + vote_poll: &YesNoVotePoll, + stored_info: YesNoVotePollStoredInfo, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive + .methods + .vote + .yes_no + .insert_stored_info_for_yes_no_vote_poll + { + 0 => self.insert_stored_info_for_yes_no_vote_poll_v0( + vote_poll, + stored_info, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "insert_stored_info_for_yes_no_vote_poll".to_string(), + known_versions: vec![0], + received: version, + })), + } + } + + /// The operations of [`Self::insert_stored_info_for_yes_no_vote_poll`]. + pub fn insert_stored_info_for_yes_no_vote_poll_operations( + &self, + vote_poll: &YesNoVotePoll, + stored_info: YesNoVotePollStoredInfo, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .vote + .yes_no + .insert_stored_info_for_yes_no_vote_poll + { + 0 => self.insert_stored_info_for_yes_no_vote_poll_operations_v0( + vote_poll, + stored_info, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "insert_stored_info_for_yes_no_vote_poll_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/v0/mod.rs new file mode 100644 index 00000000000..c976a961014 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/v0/mod.rs @@ -0,0 +1,53 @@ +use crate::drive::votes::paths::{YesNoVotePollPaths, YES_NO_VOTE_POLL_STORED_INFO_KEY}; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::object_size_info::PathKeyElementInfo::PathKeyElement; +use dpp::serialization::PlatformSerializable; +use dpp::version::PlatformVersion; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStoredInfo; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use grovedb::{Element, TransactionArg}; + +impl Drive { + pub(super) fn insert_stored_info_for_yes_no_vote_poll_v0( + &self, + vote_poll: &YesNoVotePoll, + stored_info: YesNoVotePollStoredInfo, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let batch_operations = self.insert_stored_info_for_yes_no_vote_poll_operations_v0( + vote_poll, + stored_info, + platform_version, + )?; + self.apply_batch_low_level_drive_operations( + None, + transaction, + batch_operations, + &mut vec![], + &platform_version.drive, + ) + } + + pub(super) fn insert_stored_info_for_yes_no_vote_poll_operations_v0( + &self, + vote_poll: &YesNoVotePoll, + stored_info: YesNoVotePollStoredInfo, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut drive_operations: Vec = vec![]; + let serialization = stored_info.serialize_consume_to_bytes()?; + self.batch_insert::<0>( + PathKeyElement(( + vote_poll.poll_path_vec()?, + vec![YES_NO_VOTE_POLL_STORED_INFO_KEY], + Element::new_item(serialization), + )), + &mut drive_operations, + &platform_version.drive, + )?; + Ok(drive_operations) + } +} diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/mod.rs new file mode 100644 index 00000000000..54327dd002c --- /dev/null +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/mod.rs @@ -0,0 +1,3 @@ +mod insert_stored_info_for_yes_no_vote_poll; +mod open_yes_no_vote_poll; +mod register_yes_no_identity_vote; diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs new file mode 100644 index 00000000000..ba99d118201 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs @@ -0,0 +1,85 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::block::block_info::BlockInfo; +use dpp::prelude::TimestampMillis; +use dpp::version::PlatformVersion; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use grovedb::TransactionArg; + +impl Drive { + /// Opens a yes/no vote poll that takes votes until `end_date`: its tree with the stored + /// info and the three vote sum trees, and its entry in the end date index. The prefunded + /// balance the votes are paid from is the caller's to fund, under the poll's + /// `specialized_balance_id`. + /// + /// Refuses parameters no poll can run with, and a poll that already exists. + pub fn open_yes_no_vote_poll( + &self, + vote_poll: &YesNoVotePoll, + end_date: TimestampMillis, + block_info: &BlockInfo, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive + .methods + .vote + .yes_no + .open_yes_no_vote_poll + { + 0 => self.open_yes_no_vote_poll_v0( + vote_poll, + end_date, + block_info, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "open_yes_no_vote_poll".to_string(), + known_versions: vec![0], + received: version, + })), + } + } + + /// The operations of [`Self::open_yes_no_vote_poll`], added to `batch_operations`. + #[allow(clippy::too_many_arguments)] + pub fn open_yes_no_vote_poll_operations( + &self, + vote_poll: &YesNoVotePoll, + end_date: TimestampMillis, + block_info: &BlockInfo, + previous_batch_operations: &mut Option<&mut Vec>, + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive + .methods + .vote + .yes_no + .open_yes_no_vote_poll + { + 0 => self.open_yes_no_vote_poll_operations_v0( + vote_poll, + end_date, + block_info, + previous_batch_operations, + batch_operations, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "open_yes_no_vote_poll_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs new file mode 100644 index 00000000000..8e20244aa1a --- /dev/null +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs @@ -0,0 +1,118 @@ +use crate::drive::votes::paths::{vote_decisions_active_polls_tree_path_vec, YesNoVotePollPaths}; +use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::grove_operations::BatchInsertTreeApplyType; +use crate::util::object_size_info::{DriveKeyInfo, PathKeyInfo}; +use dpp::block::block_info::BlockInfo; +use dpp::prelude::TimestampMillis; +use dpp::version::PlatformVersion; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStoredInfo; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use dpp::voting::vote_polls::VotePoll; +use dpp::ProtocolError; +use grovedb::{TransactionArg, TreeType}; + +impl Drive { + pub(super) fn open_yes_no_vote_poll_v0( + &self, + vote_poll: &YesNoVotePoll, + end_date: TimestampMillis, + block_info: &BlockInfo, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let mut batch_operations = vec![]; + self.open_yes_no_vote_poll_operations_v0( + vote_poll, + end_date, + block_info, + &mut None, + &mut batch_operations, + transaction, + platform_version, + )?; + self.apply_batch_low_level_drive_operations( + None, + transaction, + batch_operations, + &mut vec![], + &platform_version.drive, + ) + } + + #[allow(clippy::too_many_arguments)] + pub(super) fn open_yes_no_vote_poll_operations_v0( + &self, + vote_poll: &YesNoVotePoll, + end_date: TimestampMillis, + block_info: &BlockInfo, + previous_batch_operations: &mut Option<&mut Vec>, + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + vote_poll.validate_parameters(platform_version)?; + let poll_id = vote_poll.unique_id()?; + + // The poll's own tree. A poll that already has one was opened before. + let inserted = self.batch_insert_empty_tree_if_not_exists( + PathKeyInfo::PathKey::<0>(( + vote_decisions_active_polls_tree_path_vec(), + poll_id.to_vec(), + )), + TreeType::NormalTree, + None, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + transaction, + previous_batch_operations, + batch_operations, + &platform_version.drive, + )?; + if !inserted { + return Err(Error::Protocol(Box::new(ProtocolError::VoteError( + format!( + "yes/no vote poll {} is already open or was already decided", + vote_poll + ), + )))); + } + + // Its stored info: started in this block. + let stored_info = YesNoVotePollStoredInfo::new(*block_info, platform_version)?; + let mut stored_info_operations = self.insert_stored_info_for_yes_no_vote_poll_operations( + vote_poll, + stored_info, + platform_version, + )?; + batch_operations.append(&mut stored_info_operations); + + // One sum tree per choice; the sums are the tallies. + let poll_path = vote_poll.poll_path_vec()?; + for vote_choice in YesNoAbstainVoteChoice::ALL { + self.batch_insert_empty_sum_tree( + poll_path.iter().map(|segment| segment.as_slice()), + DriveKeyInfo::Key(vec![vote_choice.to_tree_key()]), + None, + batch_operations, + &platform_version.drive, + )?; + } + + // The end date index closes the poll when its time comes. Nobody is refunded for the + // entry: the poll is a system action, so it carries no storage flags. + self.add_vote_poll_end_date_query_operations( + None, + VotePoll::YesNoVotePoll(vote_poll.clone()), + end_date, + block_info, + &mut None, + previous_batch_operations, + batch_operations, + transaction, + platform_version, + ) + } +} diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs new file mode 100644 index 00000000000..3b9d63ec89a --- /dev/null +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs @@ -0,0 +1,79 @@ +mod v0; + +use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +impl Drive { + /// Registers a masternode's vote on a yes/no poll: its strength under the chosen answer's + /// sum tree, the previous answer removed if the vote changed, and the identity votes index + /// updated. + pub fn register_yes_no_identity_vote( + &self, + voter_pro_tx_hash: [u8; 32], + strength: u8, + vote: ResolvedYesNoVote, + block_info: &BlockInfo, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive + .methods + .vote + .yes_no + .register_yes_no_identity_vote + { + 0 => self.register_yes_no_identity_vote_v0( + voter_pro_tx_hash, + strength, + vote, + block_info, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "register_yes_no_identity_vote".to_string(), + known_versions: vec![0], + received: version, + })), + } + } + + /// The operations of [`Self::register_yes_no_identity_vote`]. + pub fn register_yes_no_identity_vote_operations( + &self, + voter_pro_tx_hash: [u8; 32], + strength: u8, + vote: ResolvedYesNoVote, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .vote + .yes_no + .register_yes_no_identity_vote + { + 0 => self.register_yes_no_identity_vote_operations_v0( + voter_pro_tx_hash, + strength, + vote, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "register_yes_no_identity_vote_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/v0/mod.rs new file mode 100644 index 00000000000..4b7beedaf1b --- /dev/null +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/v0/mod.rs @@ -0,0 +1,135 @@ +use crate::drive::votes::paths::{ + vote_decisions_identity_votes_tree_path_for_identity_vec, + vote_decisions_identity_votes_tree_path_vec, YesNoVotePollPaths, +}; +use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; +use crate::drive::votes::storage_form::yes_no_vote_reference_storage_form::YesNoVoteReferenceStorageForm; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::grove_operations::{BatchDeleteApplyType, BatchInsertTreeApplyType}; +use crate::util::object_size_info::PathKeyElementInfo::PathKeyElement; +use crate::util::object_size_info::PathKeyInfo; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::version::PlatformVersion; +use grovedb::{Element, MaybeTree, TransactionArg, TreeType}; + +impl Drive { + pub(super) fn register_yes_no_identity_vote_v0( + &self, + voter_pro_tx_hash: [u8; 32], + strength: u8, + vote: ResolvedYesNoVote, + block_info: &BlockInfo, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let batch_operations = self.register_yes_no_identity_vote_operations_v0( + voter_pro_tx_hash, + strength, + vote, + transaction, + platform_version, + )?; + let mut drive_operations: Vec = vec![]; + self.apply_batch_low_level_drive_operations( + None, + transaction, + batch_operations, + &mut drive_operations, + &platform_version.drive, + )?; + let fees = Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + self.config.epochs_per_era, + platform_version, + None, + )?; + Ok(fees) + } + + pub(super) fn register_yes_no_identity_vote_operations_v0( + &self, + voter_pro_tx_hash: [u8; 32], + strength: u8, + vote: ResolvedYesNoVote, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + // Voting has a fixed cost, so there are no estimated costs to keep. + let mut drive_operations: Vec = vec![]; + let ResolvedYesNoVote { + vote_poll, + vote_choice, + previous_vote_choice_to_remove, + } = vote; + + // The vote itself: the masternode's strength under its answer's sum tree. + let voting_path = vote_poll.choice_votes_path_vec(vote_choice)?; + self.batch_insert::<0>( + PathKeyElement(( + voting_path, + voter_pro_tx_hash.to_vec(), + Element::new_sum_item(strength as i64), + )), + &mut drive_operations, + &platform_version.drive, + )?; + + // A changed vote leaves its previous answer. + let mut identity_vote_times = 1; + if let Some((previous_vote_choice, previous_vote_count)) = previous_vote_choice_to_remove { + let previous_voting_path = vote_poll.choice_votes_path_vec(previous_vote_choice)?; + self.batch_delete( + previous_voting_path.as_slice().into(), + voter_pro_tx_hash.as_slice(), + BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + }, + transaction, + &mut drive_operations, + &platform_version.drive, + )?; + identity_vote_times += previous_vote_count; + } + + // The identity votes index: the masternode's tree, then its answer to this poll. + self.batch_insert_empty_tree_if_not_exists( + PathKeyInfo::PathKey::<0>(( + vote_decisions_identity_votes_tree_path_vec(), + voter_pro_tx_hash.to_vec(), + )), + TreeType::NormalTree, + None, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + transaction, + &mut None, + &mut drive_operations, + &platform_version.drive, + )?; + let storage_form = YesNoVoteReferenceStorageForm { + vote_choice, + identity_vote_times, + }; + let encoded = storage_form.serialize().map_err(|e| { + Error::Drive(DriveError::CorruptedSerialization(format!( + "can not encode yes/no vote reference: {}", + e + ))) + })?; + self.batch_insert::<0>( + PathKeyElement(( + vote_decisions_identity_votes_tree_path_for_identity_vec(&voter_pro_tx_hash), + vote_poll.unique_id()?.to_vec(), + Element::new_item(encoded), + )), + &mut drive_operations, + &platform_version.drive, + )?; + Ok(drive_operations) + } +} diff --git a/packages/rs-drive/src/drive/votes/mod.rs b/packages/rs-drive/src/drive/votes/mod.rs index 83078aab11b..7b1cf13217e 100644 --- a/packages/rs-drive/src/drive/votes/mod.rs +++ b/packages/rs-drive/src/drive/votes/mod.rs @@ -1,11 +1,14 @@ use crate::drive::document::paths::contract_document_type_path; use crate::drive::votes::paths::{ RESOURCE_ABSTAIN_VOTE_TREE_KEY_U8_32, RESOURCE_LOCK_VOTE_TREE_KEY_U8_32, + YES_NO_VOTE_POLL_ABSTAIN_VOTES_TREE_KEY, YES_NO_VOTE_POLL_NO_VOTES_TREE_KEY, + YES_NO_VOTE_POLL_YES_VOTES_TREE_KEY, }; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::DataContract; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_polls::VotePoll; use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; use dpp::voting::votes::resource_vote::ResourceVote; @@ -50,6 +53,11 @@ impl TreePath for Vote { fn tree_path<'a>(&'a self, contract: &'a DataContract) -> Result, ProtocolError> { match self { Vote::ResourceVote(resource_vote) => resource_vote.tree_path(contract), + // A yes/no poll lives under the decisions branch by its unique id, not under a + // contract's index. + Vote::YesNoVote(_) => Err(ProtocolError::VoteError( + "a yes/no vote has no tree path under a contract".to_string(), + )), } } } @@ -94,6 +102,9 @@ impl TreePath for ResourceVote { } Ok(path) } + VotePoll::YesNoVotePoll(_) => Err(ProtocolError::VoteError( + "a resource vote cannot answer a yes/no vote poll".to_string(), + )), } } } @@ -114,6 +125,34 @@ impl ResourceVoteChoiceToKeyTrait for ResourceVoteChoice { } } +/// The key of the sum tree holding the votes for a yes/no choice, and back. +pub trait YesNoAbstainVoteChoiceToKeyTrait: Sized { + /// The key of the choice's sum tree inside the poll's tree + fn to_tree_key(&self) -> u8; + + /// The choice a sum tree key stands for, if it is one of the three + fn from_tree_key(key: u8) -> Option; +} + +impl YesNoAbstainVoteChoiceToKeyTrait for YesNoAbstainVoteChoice { + fn to_tree_key(&self) -> u8 { + match self { + YesNoAbstainVoteChoice::Yes => YES_NO_VOTE_POLL_YES_VOTES_TREE_KEY, + YesNoAbstainVoteChoice::No => YES_NO_VOTE_POLL_NO_VOTES_TREE_KEY, + YesNoAbstainVoteChoice::Abstain => YES_NO_VOTE_POLL_ABSTAIN_VOTES_TREE_KEY, + } + } + + fn from_tree_key(key: u8) -> Option { + match key { + YES_NO_VOTE_POLL_YES_VOTES_TREE_KEY => Some(YesNoAbstainVoteChoice::Yes), + YES_NO_VOTE_POLL_NO_VOTES_TREE_KEY => Some(YesNoAbstainVoteChoice::No), + YES_NO_VOTE_POLL_ABSTAIN_VOTES_TREE_KEY => Some(YesNoAbstainVoteChoice::Abstain), + _ => None, + } + } +} + #[cfg(test)] #[cfg(feature = "server")] mod tests { diff --git a/packages/rs-drive/src/drive/votes/paths.rs b/packages/rs-drive/src/drive/votes/paths.rs index 1b35eecbc58..a466460cf5f 100644 --- a/packages/rs-drive/src/drive/votes/paths.rs +++ b/packages/rs-drive/src/drive/votes/paths.rs @@ -5,15 +5,37 @@ /// /// |- End date Queries [key: "e"] /// |- Decisions [key: "d"] +/// |- Active polls [key: "p"] +/// |- Identifier Votes Query [key: "i"] /// |- Contested Resource [key: "c"] /// |- Active polls [key: "p"] /// |- Identifier Votes Query [key: "i"] /// ``` +/// +/// Decisions are yes/no vote polls (protocol version 14). Each active poll is a tree keyed by +/// the poll's unique id under the active polls tree: +/// +/// ```text +/// Active polls [key: "p"] +/// |- +/// |- Stored info [key: 0]: serialized YesNoVotePollStoredInfo +/// |- Yes votes [key: 1]: sum tree of pro_tx_hash -> vote strength +/// |- No votes [key: 2]: sum tree of pro_tx_hash -> vote strength +/// |- Abstain votes [key: 3]: sum tree of pro_tx_hash -> vote strength +/// ``` +/// +/// and each masternode's votes are indexed by its pro tx hash under the identity votes tree: +/// +/// ```text +/// Identifier Votes Query [key: "i"] +/// |- +/// |- : serialized YesNoVoteReferenceStorageForm (choice + times voted) +/// ``` use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::{ ContestedDocumentResourceVotePollWithContractInfo, ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed, }; -use crate::drive::votes::ResourceVoteChoiceToKeyTrait; +use crate::drive::votes::{ResourceVoteChoiceToKeyTrait, YesNoAbstainVoteChoiceToKeyTrait}; use crate::drive::RootTree; use crate::error::query::QuerySyntaxError; use crate::error::Error; @@ -23,6 +45,8 @@ use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; use dpp::data_contract::document_type::IndexProperty; use dpp::identity::TimestampMillis; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use platform_version::version::PlatformVersion; /// A subtree made for polls to the network that represent decisions. @@ -64,6 +88,18 @@ pub const CONTESTED_DOCUMENT_INDEXES_TREE_KEY: u8 = 1; /// The tree key for storage pub const VOTING_STORAGE_TREE_KEY: u8 = 1; +/// In a yes/no vote poll tree, the key of the item holding the poll's stored info +pub const YES_NO_VOTE_POLL_STORED_INFO_KEY: u8 = 0; + +/// In a yes/no vote poll tree, the key of the sum tree of yes votes +pub const YES_NO_VOTE_POLL_YES_VOTES_TREE_KEY: u8 = 1; + +/// In a yes/no vote poll tree, the key of the sum tree of no votes +pub const YES_NO_VOTE_POLL_NO_VOTES_TREE_KEY: u8 = 2; + +/// In a yes/no vote poll tree, the key of the sum tree of abstaining votes +pub const YES_NO_VOTE_POLL_ABSTAIN_VOTES_TREE_KEY: u8 = 3; + /// Convenience methods to be easily able to get a path when we know the vote poll pub trait VotePollPaths { /// The root path, under this there should be the documents area and the contract itself @@ -304,6 +340,35 @@ impl VotePollPaths for ContestedDocumentResourceVotePollWithContractInfoAllowBor } } +/// The paths of a yes/no vote poll's storage, derived from its unique id. +pub trait YesNoVotePollPaths { + /// The poll's tree, holding its stored info and its three vote sum trees. + fn poll_path_vec(&self) -> Result>, Error>; + + /// The sum tree holding the votes for one choice. + fn choice_votes_path_vec( + &self, + vote_choice: YesNoAbstainVoteChoice, + ) -> Result>, Error>; +} + +impl YesNoVotePollPaths for YesNoVotePoll { + fn poll_path_vec(&self) -> Result>, Error> { + Ok(vote_decisions_active_polls_poll_tree_path_vec( + self.unique_id()?.as_bytes(), + )) + } + + fn choice_votes_path_vec( + &self, + vote_choice: YesNoAbstainVoteChoice, + ) -> Result>, Error> { + let mut path = self.poll_path_vec()?; + path.push(vec![vote_choice.to_tree_key()]); + Ok(path) + } +} + /// the root path of the voting branch pub fn vote_root_path<'a>() -> [&'a [u8]; 1] { [Into::<&[u8; 1]>::into(RootTree::Votes)] @@ -546,6 +611,84 @@ pub fn vote_contested_resource_identity_votes_tree_path_for_identity_vec( ] } +/// the active yes/no vote polls of the decisions branch +pub fn vote_decisions_active_polls_tree_path<'a>() -> [&'a [u8]; 3] { + [ + Into::<&[u8; 1]>::into(RootTree::Votes), + &[VOTE_DECISIONS_TREE_KEY as u8], + &[ACTIVE_POLLS_TREE_KEY as u8], + ] +} + +/// the active yes/no vote polls of the decisions branch as a vec +pub fn vote_decisions_active_polls_tree_path_vec() -> Vec> { + vec![ + vec![RootTree::Votes as u8], + vec![VOTE_DECISIONS_TREE_KEY as u8], + vec![ACTIVE_POLLS_TREE_KEY as u8], + ] +} + +/// the tree of one yes/no vote poll, keyed by its unique id +pub fn vote_decisions_active_polls_poll_tree_path(poll_id: &[u8; 32]) -> [&[u8]; 4] { + [ + Into::<&[u8; 1]>::into(RootTree::Votes), + &[VOTE_DECISIONS_TREE_KEY as u8], + &[ACTIVE_POLLS_TREE_KEY as u8], + poll_id, + ] +} + +/// the tree of one yes/no vote poll, keyed by its unique id, as a vec +pub fn vote_decisions_active_polls_poll_tree_path_vec(poll_id: &[u8; 32]) -> Vec> { + vec![ + vec![RootTree::Votes as u8], + vec![VOTE_DECISIONS_TREE_KEY as u8], + vec![ACTIVE_POLLS_TREE_KEY as u8], + poll_id.to_vec(), + ] +} + +/// the identity votes of the decisions branch +pub fn vote_decisions_identity_votes_tree_path<'a>() -> [&'a [u8]; 3] { + [ + Into::<&[u8; 1]>::into(RootTree::Votes), + &[VOTE_DECISIONS_TREE_KEY as u8], + &[IDENTITY_VOTES_TREE_KEY as u8], + ] +} + +/// the identity votes of the decisions branch as a vec +pub fn vote_decisions_identity_votes_tree_path_vec() -> Vec> { + vec![ + vec![RootTree::Votes as u8], + vec![VOTE_DECISIONS_TREE_KEY as u8], + vec![IDENTITY_VOTES_TREE_KEY as u8], + ] +} + +/// the yes/no votes of one masternode, by its pro tx hash +pub fn vote_decisions_identity_votes_tree_path_for_identity(pro_tx_hash: &[u8; 32]) -> [&[u8]; 4] { + [ + Into::<&[u8; 1]>::into(RootTree::Votes), + &[VOTE_DECISIONS_TREE_KEY as u8], + &[IDENTITY_VOTES_TREE_KEY as u8], + pro_tx_hash, + ] +} + +/// the yes/no votes of one masternode, by its pro tx hash, as a vec +pub fn vote_decisions_identity_votes_tree_path_for_identity_vec( + pro_tx_hash: &[u8; 32], +) -> Vec> { + vec![ + vec![RootTree::Votes as u8], + vec![VOTE_DECISIONS_TREE_KEY as u8], + vec![IDENTITY_VOTES_TREE_KEY as u8], + pro_tx_hash.to_vec(), + ] +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-drive/src/drive/votes/resolved/vote_polls/mod.rs b/packages/rs-drive/src/drive/votes/resolved/vote_polls/mod.rs index 643af76e8a1..31a3cc20617 100644 --- a/packages/rs-drive/src/drive/votes/resolved/vote_polls/mod.rs +++ b/packages/rs-drive/src/drive/votes/resolved/vote_polls/mod.rs @@ -2,6 +2,8 @@ use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_ use derive_more::From; use dpp::identifier::Identifier; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use dpp::voting::vote_polls::VotePoll; use dpp::ProtocolError; use std::collections::BTreeMap; @@ -14,12 +16,16 @@ pub mod contested_document_resource_vote_poll; pub mod resolve; /// Represents a resolved vote poll in the system. +// The contested variant carries the resolved contract; a yes/no poll is a few bytes. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, From)] pub enum ResolvedVotePoll { /// A resolved vote poll with contract information for a contested document resource. ContestedDocumentResourceVotePollWithContractInfo( ContestedDocumentResourceVotePollWithContractInfo, ), + /// A yes/no vote poll. It needs nothing from the state to be resolved. + YesNoVotePoll(YesNoVotePoll), } impl From<&ResolvedVotePoll> for VotePoll { @@ -30,11 +36,16 @@ impl From<&ResolvedVotePoll> for VotePoll { ) => VotePoll::ContestedDocumentResourceVotePoll( contested_document_resource_vote_poll.into(), ), + ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll) => { + VotePoll::YesNoVotePoll(yes_no_vote_poll.clone()) + } } } } /// Represents a resolved vote poll in the system that also contains votes. +// The contested variant carries the resolved contract; a yes/no poll is a few bytes. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, From)] pub enum ResolvedVotePollWithVotes { /// A resolved vote poll with contract information for a contested document resource. @@ -42,6 +53,11 @@ pub enum ResolvedVotePollWithVotes { ContestedDocumentResourceVotePollWithContractInfo, BTreeMap>, ), + /// A yes/no vote poll and the masternodes that voted for each choice. + YesNoVotePollWithVotes( + YesNoVotePoll, + BTreeMap>, + ), } impl ResolvedVotePoll { @@ -63,6 +79,9 @@ impl ResolvedVotePoll { ) => Ok(Some( contested_document_resource_vote_poll.specialized_balance_id()?, )), + ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll) => { + Ok(Some(yes_no_vote_poll.specialized_balance_id()?)) + } } } @@ -81,6 +100,7 @@ impl ResolvedVotePoll { ResolvedVotePoll::ContestedDocumentResourceVotePollWithContractInfo( contested_document_resource_vote_poll, ) => contested_document_resource_vote_poll.unique_id(), + ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll) => yes_no_vote_poll.unique_id(), } } } diff --git a/packages/rs-drive/src/drive/votes/resolved/vote_polls/resolve.rs b/packages/rs-drive/src/drive/votes/resolved/vote_polls/resolve.rs index eecb852f1b1..40f4b474fa8 100644 --- a/packages/rs-drive/src/drive/votes/resolved/vote_polls/resolve.rs +++ b/packages/rs-drive/src/drive/votes/resolved/vote_polls/resolve.rs @@ -63,6 +63,9 @@ impl VotePollResolver for VotePoll { ), ) } + VotePoll::YesNoVotePoll(yes_no_vote_poll) => { + Ok(ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll.clone())) + } } } @@ -84,6 +87,9 @@ impl VotePollResolver for VotePoll { ), ) } + VotePoll::YesNoVotePoll(yes_no_vote_poll) => { + Ok(ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll)) + } } } } diff --git a/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs b/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs index 99a31f50a69..ca1fdfd0957 100644 --- a/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs +++ b/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs @@ -2,10 +2,13 @@ pub(crate) mod resolve; /// Resolved resource vote module pub mod resolved_resource_vote; +/// Resolved yes/no vote module +pub mod resolved_yes_no_vote; use crate::drive::votes::resolved::vote_polls::ResolvedVotePoll; use crate::drive::votes::resolved::votes::resolved_resource_vote::accessors::v0::ResolvedResourceVoteGettersV0; use crate::drive::votes::resolved::votes::resolved_resource_vote::ResolvedResourceVote; +use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; use dpp::identifier::Identifier; use dpp::voting::vote_polls::VotePoll; use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; @@ -14,10 +17,14 @@ use dpp::voting::votes::Vote; use dpp::ProtocolError; /// Represents the different types of resolved votes within the system. +// A resource vote carries the resolved contract of its poll; a yes/no vote is a few bytes. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq)] pub enum ResolvedVote { /// A resolved vote for a specific resource. ResolvedResourceVote(ResolvedResourceVote), + /// A vote on a yes/no poll. + YesNoVote(ResolvedYesNoVote), } impl ResolvedVote { @@ -37,12 +44,20 @@ impl ResolvedVote { ResolvedVote::ResolvedResourceVote(resource_vote) => { resource_vote.vote_poll().specialized_balance_id() } + ResolvedVote::YesNoVote(yes_no_vote) => { + Ok(Some(yes_no_vote.vote_poll.specialized_balance_id()?)) + } } } } -impl From for Vote { - fn from(value: ResolvedVote) -> Self { +impl TryFrom for Vote { + type Error = ProtocolError; + + /// The vote as it was cast. A resource vote resolves to the contested document resource + /// poll it carried, so a resolved resource vote holding a yes/no poll is a programming + /// error, not a vote. + fn try_from(value: ResolvedVote) -> Result { match value { ResolvedVote::ResolvedResourceVote(resolved_resource_vote) => { match resolved_resource_vote { @@ -51,16 +66,23 @@ impl From for Vote { match v0.resolved_vote_poll { ResolvedVotePoll::ContestedDocumentResourceVotePollWithContractInfo( contested_document_resource, - ) => Self::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + ) => Ok(Self::ResourceVote(ResourceVote::V0(ResourceVoteV0 { vote_poll: VotePoll::ContestedDocumentResourceVotePoll( contested_document_resource.into(), ), resource_vote_choice: vote_choice, - })), + }))), + ResolvedVotePoll::YesNoVotePoll(_) => { + Err(ProtocolError::CorruptedCodeExecution( + "a resolved resource vote cannot hold a yes/no vote poll" + .to_string(), + )) + } } } } } + ResolvedVote::YesNoVote(yes_no_vote) => Ok(Self::YesNoVote(yes_no_vote.into())), } } } diff --git a/packages/rs-drive/src/drive/votes/resolved/votes/resolve.rs b/packages/rs-drive/src/drive/votes/resolved/votes/resolve.rs index 363a29da07f..053a181a82e 100644 --- a/packages/rs-drive/src/drive/votes/resolved/votes/resolve.rs +++ b/packages/rs-drive/src/drive/votes/resolved/votes/resolve.rs @@ -1,4 +1,5 @@ use crate::drive::votes::resolved::votes::resolved_resource_vote::resolve::ResourceVoteResolver; +use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; use crate::drive::votes::resolved::votes::ResolvedVote; use crate::drive::Drive; use crate::error::Error; @@ -53,6 +54,9 @@ impl VoteResolver for Vote { Vote::ResourceVote(resource_vote) => Ok(ResolvedVote::ResolvedResourceVote( resource_vote.resolve(drive, transaction, platform_version)?, )), + Vote::YesNoVote(yes_no_vote) => Ok(ResolvedVote::YesNoVote( + ResolvedYesNoVote::from_vote(yes_no_vote), + )), } } @@ -66,6 +70,9 @@ impl VoteResolver for Vote { Vote::ResourceVote(resource_vote) => Ok(ResolvedVote::ResolvedResourceVote( resource_vote.resolve_owned(drive, transaction, platform_version)?, )), + Vote::YesNoVote(yes_no_vote) => Ok(ResolvedVote::YesNoVote( + ResolvedYesNoVote::from_vote_owned(yes_no_vote), + )), } } } diff --git a/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs b/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs new file mode 100644 index 00000000000..425d51fd56c --- /dev/null +++ b/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs @@ -0,0 +1,52 @@ +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use dpp::voting::votes::yes_no_vote::accessors::v0::YesNoVoteGettersV0; +use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; +use dpp::voting::votes::yes_no_vote::YesNoVote; + +/// How many times a masternode already voted on a poll, before the vote being cast. +pub type PreviousYesNoVoteCount = u16; + +/// A yes/no vote as the execution layer handles it. A yes/no poll needs no contract to be +/// resolved, so this is the vote itself plus what validation learned from the state: the +/// masternode's previous answer to the same poll, which registering the vote removes. +#[derive(Debug, Clone, PartialEq)] +pub struct ResolvedYesNoVote { + /// The poll answered. + pub vote_poll: YesNoVotePoll, + /// The answer. + pub vote_choice: YesNoAbstainVoteChoice, + /// The masternode's previous answer to this poll and how many times it had voted, when it + /// changes its vote. `None` for a first vote, and whenever validation was skipped. + pub previous_vote_choice_to_remove: Option<(YesNoAbstainVoteChoice, PreviousYesNoVoteCount)>, +} + +impl ResolvedYesNoVote { + /// The vote as it came in, with no previous answer attached. + pub fn from_vote(vote: &YesNoVote) -> Self { + ResolvedYesNoVote { + vote_poll: vote.vote_poll().clone(), + vote_choice: vote.vote_choice(), + previous_vote_choice_to_remove: None, + } + } + + /// The vote as it came in, owned, with no previous answer attached. + pub fn from_vote_owned(vote: YesNoVote) -> Self { + let vote_choice = vote.vote_choice(); + ResolvedYesNoVote { + vote_poll: vote.vote_poll_owned(), + vote_choice, + previous_vote_choice_to_remove: None, + } + } +} + +impl From for YesNoVote { + fn from(value: ResolvedYesNoVote) -> Self { + YesNoVote::V0(YesNoVoteV0 { + vote_poll: value.vote_poll, + vote_choice: value.vote_choice, + }) + } +} diff --git a/packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs b/packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs new file mode 100644 index 00000000000..a6d960c092c --- /dev/null +++ b/packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs @@ -0,0 +1,79 @@ +use crate::drive::votes::paths::{ + vote_decisions_tree_path, ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY, +}; +use crate::drive::Drive; +use crate::error::Error; +use dpp::version::PlatformVersion; +use grovedb::{Element, TransactionArg}; + +impl Drive { + /// Creates the active polls and identity votes trees of the decisions branch when they are + /// missing. A chain upgrading to protocol version 14 calls this on its first block; fresh + /// chains create the same trees at genesis (`add_initial_vote_tree_main_structure_operations` + /// v1), so both populations build the same votes Merk. + pub fn insert_vote_decisions_trees( + &self, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + for subtree_key in [ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY] { + self.grove_insert_if_not_exists( + vote_decisions_tree_path().as_slice().into(), + &[subtree_key as u8], + Element::empty_tree(), + transaction, + None, + &platform_version.drive, + )?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::drive::votes::paths::{ + vote_decisions_active_polls_tree_path, vote_decisions_identity_votes_tree_path, + }; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::version::PlatformVersion; + + #[test] + fn should_create_the_decisions_trees_once_and_keep_them() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(None); + // Genesis at the latest version already made them; a second run changes nothing. + let root_hash_before = drive + .grove + .root_hash(None, &platform_version.drive.grove_version) + .unwrap() + .expect("root hash"); + drive + .insert_vote_decisions_trees(None, platform_version) + .expect("expected to insert the decisions trees"); + let root_hash_after = drive + .grove + .root_hash(None, &platform_version.drive.grove_version) + .unwrap() + .expect("root hash"); + assert_eq!(root_hash_before, root_hash_after); + for path in [ + vote_decisions_active_polls_tree_path(), + vote_decisions_identity_votes_tree_path(), + ] { + let (parent, key) = path.split_at(3); + let _ = key; + let element = drive + .grove + .get( + &parent[..2], + path[2], + None, + &platform_version.drive.grove_version, + ) + .unwrap() + .expect("expected the tree"); + assert!(element.is_any_tree()); + } + } +} diff --git a/packages/rs-drive/src/drive/votes/setup/mod.rs b/packages/rs-drive/src/drive/votes/setup/mod.rs index 00eb6e7ec30..b3391f792d9 100644 --- a/packages/rs-drive/src/drive/votes/setup/mod.rs +++ b/packages/rs-drive/src/drive/votes/setup/mod.rs @@ -1 +1,2 @@ +mod insert_vote_decisions_trees; mod setup_initial_vote_tree_main_structure; diff --git a/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/mod.rs b/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/mod.rs index 25ece38374d..dfc9ac3fddf 100644 --- a/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/mod.rs +++ b/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use crate::drive::Drive; @@ -43,9 +44,10 @@ impl Drive { .add_initial_vote_tree_main_structure_operations { 0 => Drive::add_initial_vote_tree_main_structure_operations_v0(batch), + 1 => Drive::add_initial_vote_tree_main_structure_operations_v1(batch), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "add_initial_vote_tree_main_structure_operations".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/v1/mod.rs b/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/v1/mod.rs new file mode 100644 index 00000000000..3e539311632 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/v1/mod.rs @@ -0,0 +1,41 @@ +use crate::drive::votes::paths::{ + vote_contested_resource_tree_path_vec, vote_decisions_tree_path_vec, vote_root_path_vec, + ACTIVE_POLLS_TREE_KEY, CONTESTED_RESOURCE_TREE_KEY, END_DATE_QUERIES_TREE_KEY, + IDENTITY_VOTES_TREE_KEY, VOTE_DECISIONS_TREE_KEY, +}; +use crate::drive::Drive; +use crate::error::Error; +use crate::util::batch::grovedb_op_batch::GroveDbOpBatchV0Methods; +use crate::util::batch::GroveDbOpBatch; + +impl Drive { + /// Version 1 (protocol version 14) also creates the active polls and identity votes trees + /// of the decisions branch, where yes/no vote polls live. + pub(super) fn add_initial_vote_tree_main_structure_operations_v1( + batch: &mut GroveDbOpBatch, + ) -> Result<(), Error> { + batch.add_insert_empty_tree(vote_root_path_vec(), vec![VOTE_DECISIONS_TREE_KEY as u8]); + batch.add_insert_empty_tree( + vote_root_path_vec(), + vec![CONTESTED_RESOURCE_TREE_KEY as u8], + ); + batch.add_insert_empty_tree(vote_root_path_vec(), vec![END_DATE_QUERIES_TREE_KEY as u8]); + batch.add_insert_empty_tree( + vote_contested_resource_tree_path_vec(), + vec![ACTIVE_POLLS_TREE_KEY as u8], + ); + batch.add_insert_empty_tree( + vote_contested_resource_tree_path_vec(), + vec![IDENTITY_VOTES_TREE_KEY as u8], + ); + batch.add_insert_empty_tree( + vote_decisions_tree_path_vec(), + vec![ACTIVE_POLLS_TREE_KEY as u8], + ); + batch.add_insert_empty_tree( + vote_decisions_tree_path_vec(), + vec![IDENTITY_VOTES_TREE_KEY as u8], + ); + Ok(()) + } +} diff --git a/packages/rs-drive/src/drive/votes/storage_form/mod.rs b/packages/rs-drive/src/drive/votes/storage_form/mod.rs index a797ee8c5ba..c450b46062a 100644 --- a/packages/rs-drive/src/drive/votes/storage_form/mod.rs +++ b/packages/rs-drive/src/drive/votes/storage_form/mod.rs @@ -6,3 +6,6 @@ pub mod vote_storage_form; /// Module for handling the storage form of the reference of the contested document resources. pub mod contested_document_resource_reference_storage_form; + +/// Module for handling the storage form of a masternode's vote on a yes/no poll. +pub mod yes_no_vote_reference_storage_form; diff --git a/packages/rs-drive/src/drive/votes/storage_form/vote_storage_form/mod.rs b/packages/rs-drive/src/drive/votes/storage_form/vote_storage_form/mod.rs index 2d75ebe90ce..ab8453d88db 100644 --- a/packages/rs-drive/src/drive/votes/storage_form/vote_storage_form/mod.rs +++ b/packages/rs-drive/src/drive/votes/storage_form/vote_storage_form/mod.rs @@ -60,8 +60,11 @@ impl TreePathStorageForm for VoteStorageForm { CONTESTED_RESOURCE_TREE_KEY => Ok(VoteStorageForm::ContestedDocumentResource( ContestedDocumentResourceVoteStorageForm::try_from_tree_path(path)?, )), + // A yes/no vote is keyed by the hash of its poll, which a tree path cannot be + // turned back into. The identity votes index of the decisions branch stores the + // choice itself instead of a reference. VOTE_DECISIONS_TREE_KEY => Err(ProtocolError::NotSupported( - "decision votes not supported yet".to_string(), + "yes/no votes are not addressed by tree path".to_string(), )), _ => Err(ProtocolError::VoteError(format!( "path {} second element must be a byte for CONTESTED_RESOURCE_TREE_KEY {}, got {}", @@ -175,7 +178,7 @@ mod tests { }; match err { ProtocolError::NotSupported(msg) => { - assert!(msg.contains("decision votes"), "msg: {msg}"); + assert!(msg.contains("yes/no votes"), "msg: {msg}"); } other => panic!("expected NotSupported, got {other:?}"), } diff --git a/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs b/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs new file mode 100644 index 00000000000..82fba796a9d --- /dev/null +++ b/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs @@ -0,0 +1,43 @@ +use bincode::{Decode, DecodeUntrusted, Encode}; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; + +/// What the identity votes index of the decisions branch stores for one masternode's vote on +/// one yes/no poll: the answer, and how many times the masternode voted on the poll. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, DecodeUntrusted)] +pub struct YesNoVoteReferenceStorageForm { + /// The masternode's current answer + pub vote_choice: YesNoAbstainVoteChoice, + /// How many times the masternode voted on the poll, counting this vote + pub identity_vote_times: u16, +} + +impl YesNoVoteReferenceStorageForm { + /// Encodes the form for storage. + pub fn serialize(&self) -> Result, bincode::error::EncodeError> { + bincode::encode_to_vec(self, bincode::config::standard().with_big_endian()) + } + + /// Decodes a stored form. + pub fn deserialize(bytes: &[u8]) -> Result { + bincode::decode_from_slice(bytes, bincode::config::standard().with_big_endian()) + .map(|(form, _)| form) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_round_trip() { + let form = YesNoVoteReferenceStorageForm { + vote_choice: YesNoAbstainVoteChoice::No, + identity_vote_times: 3, + }; + let bytes = form.serialize().expect("serialize"); + assert_eq!( + YesNoVoteReferenceStorageForm::deserialize(&bytes).expect("deserialize"), + form + ); + } +} diff --git a/packages/rs-drive/src/drive/votes/structure.rs b/packages/rs-drive/src/drive/votes/structure.rs index 42ef6631b51..c328bee1755 100644 --- a/packages/rs-drive/src/drive/votes/structure.rs +++ b/packages/rs-drive/src/drive/votes/structure.rs @@ -3,7 +3,9 @@ use crate::drive::votes::paths::{ CONTESTED_DOCUMENT_STORAGE_TREE_KEY, CONTESTED_RESOURCE_TREE_KEY, END_DATE_QUERIES_TREE_KEY, IDENTITY_VOTES_TREE_KEY, RESOURCE_ABSTAIN_VOTE_TREE_KEY_U8_32, RESOURCE_LOCK_VOTE_TREE_KEY_U8_32, RESOURCE_STORED_INFO_KEY_U8_32, VOTE_DECISIONS_TREE_KEY, - VOTING_STORAGE_TREE_KEY, + VOTING_STORAGE_TREE_KEY, YES_NO_VOTE_POLL_ABSTAIN_VOTES_TREE_KEY, + YES_NO_VOTE_POLL_NO_VOTES_TREE_KEY, YES_NO_VOTE_POLL_STORED_INFO_KEY, + YES_NO_VOTE_POLL_YES_VOTES_TREE_KEY, }; use crate::drive::RootTree; use crate::structure::{ElementKind, FlagsKind, KeyEncoding, KeyMatcher, StructureNode}; @@ -12,7 +14,9 @@ const SOURCE: &str = "packages/rs-drive/src/drive/votes/paths.rs"; const CONTENDER_FLAGS: &str = "The owner is the contender whose document created the level, who is \ refunded when the poll is cleaned up. Written without storage flags, it carries none."; -const POLL_FLAGS: &str = "The owner is the identity whose contested document started the poll."; +const POLL_FLAGS: &str = "The owner is the identity whose contested document started the \ + poll. A yes/no poll is opened by the system and carries none."; +const POLL: [FlagsKind; 2] = [FlagsKind::EpochOwned, FlagsKind::None]; const OWNED: [FlagsKind; 2] = [FlagsKind::EpochOwned, FlagsKind::None]; const CONTESTED_DOCUMENT: &str = "votes.contested_resource.active_polls.contract.document_type.storage.document"; @@ -55,8 +59,8 @@ pub(crate) fn structure() -> StructureNode { .kind(ElementKind::Tree) .source("packages/rs-drive/src/drive/mod.rs") .describe( - "Masternode votes, today on contested resources \ - such as premium DPNS names.", + "Masternode votes: on contested resources such as \ + premium DPNS names, and on yes/no decisions.", ) .children(vec![ StructureNode::fixed( @@ -83,9 +87,11 @@ pub(crate) fn structure() -> StructureNode { .kind(ElementKind::Tree) .source(SOURCE) .describe( - "Reserved for polls that decide something for the \ - network. Nothing writes to it yet.", - ), + "Yes/no polls: decisions taken by a supermajority \ + of the masternodes once enough voting power was \ + cast.", + ) + .children(vec![decision_identity_votes(), decision_active_polls()]), StructureNode::fixed( "end_date_queries", &[END_DATE_QUERIES_TREE_KEY as u8], @@ -109,7 +115,7 @@ pub(crate) fn structure() -> StructureNode { with the sign bit flipped", ) .kind(ElementKind::Tree) - .flags(&[FlagsKind::EpochOwned], POLL_FLAGS) + .flags(&POLL, POLL_FLAGS) .describe("The polls ending at this time.") .child( StructureNode::dynamic( @@ -120,7 +126,7 @@ pub(crate) fn structure() -> StructureNode { "The double sha256 of the serialized vote poll", ) .kind(ElementKind::Item) - .flags(&[FlagsKind::EpochOwned], POLL_FLAGS) + .flags(&POLL, POLL_FLAGS) .value("serialized VotePoll") .describe("One poll ending at this time."), ), @@ -322,3 +328,133 @@ fn index_value() -> StructureNode { ), ]) } + +fn decision_vote_choice( + segment: &str, + key: u8, + label: &str, + constant: &str, + what: &str, +) -> StructureNode { + StructureNode::fixed(segment, &[key], label, constant) + .kind(ElementKind::SumTree) + .until_deleted() + .describe(what) + .child( + StructureNode::identifier( + "voter", + "pro_tx_hash", + "The voting masternode's pro tx hash", + ) + .kind(ElementKind::SumItem) + .value("vote strength: 1 for a masternode, 4 for an evonode") + .describe("One masternode's vote; the tree's sum is the tally."), + ) +} + +fn decision_active_polls() -> StructureNode { + StructureNode::fixed( + "active_polls", + &[ACTIVE_POLLS_TREE_KEY as u8], + "DecisionActivePolls", + "ACTIVE_POLLS_TREE_KEY", + ) + .ascii() + .kind(ElementKind::Tree) + .since(14) + .source(SOURCE) + .describe("The yes/no polls, by their unique id.") + .child( + StructureNode::dynamic( + "poll", + "vote_poll_id", + KeyMatcher::Len(32), + KeyEncoding::Hash32, + "The double sha256 of the serialized vote poll", + ) + .kind(ElementKind::Tree) + .describe( + "One yes/no poll: its stored info and its three \ + vote sum trees. The vote trees go when the poll \ + ends; the stored info stays as the record of the \ + decision.", + ) + .children(vec![ + StructureNode::fixed( + "stored_info", + &[YES_NO_VOTE_POLL_STORED_INFO_KEY], + "YesNoStoredInfo", + "YES_NO_VOTE_POLL_STORED_INFO_KEY", + ) + .kind(ElementKind::Item) + .value("serialized YesNoVotePollStoredInfo") + .describe("The poll's status and, once it ends, its result."), + decision_vote_choice( + "yes", + YES_NO_VOTE_POLL_YES_VOTES_TREE_KEY, + "YesVotes", + "YES_NO_VOTE_POLL_YES_VOTES_TREE_KEY", + "Votes for yes.", + ), + decision_vote_choice( + "no", + YES_NO_VOTE_POLL_NO_VOTES_TREE_KEY, + "NoVotes", + "YES_NO_VOTE_POLL_NO_VOTES_TREE_KEY", + "Votes for no.", + ), + decision_vote_choice( + "abstain", + YES_NO_VOTE_POLL_ABSTAIN_VOTES_TREE_KEY, + "AbstainVotes", + "YES_NO_VOTE_POLL_ABSTAIN_VOTES_TREE_KEY", + "Votes to abstain, which count towards nothing.", + ), + ]), + ) +} + +fn decision_identity_votes() -> StructureNode { + StructureNode::fixed( + "identity_votes", + &[IDENTITY_VOTES_TREE_KEY as u8], + "DecisionIdentityVotes", + "IDENTITY_VOTES_TREE_KEY", + ) + .ascii() + .kind(ElementKind::Tree) + .since(14) + .source(SOURCE) + .describe( + "The yes/no votes each masternode cast, so they can \ + be changed and removed with the masternode.", + ) + .child( + StructureNode::identifier( + "voter", + "pro_tx_hash", + "The voting masternode's pro tx hash", + ) + .kind(ElementKind::Tree) + .describe( + "One masternode's yes/no votes. Stays once its \ + votes are gone, like its contested counterpart.", + ) + .child( + StructureNode::dynamic( + "vote", + "vote_poll_id", + KeyMatcher::Len(32), + KeyEncoding::Hash32, + "The double sha256 of the serialized vote poll", + ) + .kind(ElementKind::Item) + .value( + "bincode YesNoVoteReferenceStorageForm: the \ + choice and how many times the masternode voted \ + on the poll", + ) + .describe("The masternode's current answer to one poll."), + ), + ) +} diff --git a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs index 177446b4088..7ac15f98db1 100644 --- a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs +++ b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs @@ -48,7 +48,9 @@ use dpp::state_transition::masternode_vote_transition::accessors::MasternodeVote use dpp::state_transition::StateTransitionIdentityIdFromInputs; use dpp::state_transition::StateTransitionWitnessSigned; use dpp::state_transition::{StateTransition, StateTransitionLike, StateTransitionOwned}; +use dpp::voting::vote_polls::VotePoll; use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; +use dpp::voting::votes::yes_no_vote::accessors::v0::YesNoVoteGettersV0; use dpp::voting::votes::Vote; use grovedb::{PathQuery, TransactionArg}; use platform_version::version::PlatformVersion; @@ -430,20 +432,22 @@ impl Drive { StateTransition::MasternodeVote(st) => { let pro_tx_hash = st.pro_tx_hash(); - match st.vote() { - Vote::ResourceVote(resource_vote) => { - let query = IdentityBasedVoteDriveQuery { - identity_id: pro_tx_hash, - vote_poll: resource_vote.vote_poll().clone(), - }; - - // The path query construction can only fail if the serialization fails. - // Because the serialization will pretty much never fail, we can do this. - let mut path_query = query.construct_path_query()?; - path_query.query.limit = None; - path_query + let vote_poll = match st.vote() { + Vote::ResourceVote(resource_vote) => resource_vote.vote_poll().clone(), + Vote::YesNoVote(yes_no_vote) => { + VotePoll::YesNoVotePoll(yes_no_vote.vote_poll().clone()) } - } + }; + let query = IdentityBasedVoteDriveQuery { + identity_id: pro_tx_hash, + vote_poll, + }; + + // The path query construction can only fail if the serialization fails. + // Because the serialization will pretty much never fail, we can do this. + let mut path_query = query.construct_path_query()?; + path_query.query.limit = None; + path_query } StateTransition::IdentityCreditTransferToAddresses(st) => { let identity_query = Drive::revision_and_balance_path_query( diff --git a/packages/rs-drive/src/query/mod.rs b/packages/rs-drive/src/query/mod.rs index 1a5175a2f21..a9d0a7e2342 100644 --- a/packages/rs-drive/src/query/mod.rs +++ b/packages/rs-drive/src/query/mod.rs @@ -198,6 +198,10 @@ pub mod vote_polls_by_end_date_query; /// Vote polls by document type query pub mod vote_polls_by_document_type_query; +/// Yes/no vote poll state query +#[cfg(any(feature = "server", feature = "verify"))] +pub mod yes_no_vote_poll_state_query; + /// Function type for looking up a contract by identifier /// /// This function is used to look up a contract by its identifier. diff --git a/packages/rs-drive/src/query/vote_query.rs b/packages/rs-drive/src/query/vote_query.rs index c49927ae108..eab05afec88 100644 --- a/packages/rs-drive/src/query/vote_query.rs +++ b/packages/rs-drive/src/query/vote_query.rs @@ -1,4 +1,7 @@ -use crate::drive::votes::paths::vote_contested_resource_identity_votes_tree_path_for_identity_vec; +use crate::drive::votes::paths::{ + vote_contested_resource_identity_votes_tree_path_for_identity_vec, + vote_decisions_identity_votes_tree_path_for_identity_vec, +}; use crate::error::Error; use crate::query::Query; use bincode::{Decode, Encode}; @@ -18,10 +21,17 @@ pub struct IdentityBasedVoteDriveQuery { impl IdentityBasedVoteDriveQuery { /// Operations to construct a path query. pub fn construct_path_query(&self) -> Result { - // First we should get the overall document_type_path - let path = vote_contested_resource_identity_votes_tree_path_for_identity_vec( - self.identity_id.as_bytes(), - ); + // Each poll kind indexes the votes of a masternode under its own branch + let path = match &self.vote_poll { + VotePoll::ContestedDocumentResourceVotePoll(_) => { + vote_contested_resource_identity_votes_tree_path_for_identity_vec( + self.identity_id.as_bytes(), + ) + } + VotePoll::YesNoVotePoll(_) => vote_decisions_identity_votes_tree_path_for_identity_vec( + self.identity_id.as_bytes(), + ), + }; let vote_id = self.vote_poll.unique_id()?; diff --git a/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs new file mode 100644 index 00000000000..6ea1708b63c --- /dev/null +++ b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs @@ -0,0 +1,286 @@ +#[cfg(feature = "server")] +use crate::drive::votes::paths::vote_decisions_active_polls_tree_path; +use crate::drive::votes::paths::{YesNoVotePollPaths, YES_NO_VOTE_POLL_STORED_INFO_KEY}; +use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; +#[cfg(feature = "server")] +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +#[cfg(feature = "server")] +use crate::fees::op::LowLevelDriveOperation; +use crate::query::Query; +#[cfg(feature = "server")] +use crate::util::grove_operations::DirectQueryType; +use dpp::serialization::PlatformDeserializableUntrusted; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStoredInfo; +use dpp::voting::vote_polls::yes_no_vote_poll::{VotingPower, YesNoVotePoll}; +#[cfg(feature = "server")] +use grovedb::query_result_type::QueryResultType; +#[cfg(feature = "server")] +use grovedb::TransactionArg; +use grovedb::{Element, PathQuery, SizedQuery}; +#[cfg(feature = "server")] +use platform_version::version::PlatformVersion; + +/// The state of one yes/no vote poll: its stored info and its tallies. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct YesNoVotePollState { + /// The poll's status and, once finished, its result. `None` when the poll was never opened. + pub stored_info: Option, + /// The yes voting power cast so far. Zero once the poll is finished and its votes removed. + pub yes_voting_power: VotingPower, + /// The no voting power cast so far. Zero once the poll is finished and its votes removed. + pub no_voting_power: VotingPower, + /// The abstaining voting power cast so far. Zero once the poll is finished and its votes + /// removed. + pub abstain_voting_power: VotingPower, +} + +impl YesNoVotePollState { + /// The voting power cast for a choice. + pub fn voting_power(&self, vote_choice: YesNoAbstainVoteChoice) -> VotingPower { + match vote_choice { + YesNoAbstainVoteChoice::Yes => self.yes_voting_power, + YesNoAbstainVoteChoice::No => self.no_voting_power, + YesNoAbstainVoteChoice::Abstain => self.abstain_voting_power, + } + } +} + +/// The query for the state of a yes/no vote poll: its stored info item and its three vote sum +/// trees, whose sums are the tallies. +#[derive(Debug, PartialEq, Clone)] +pub struct YesNoVotePollStateDriveQuery { + /// The poll asked about + pub vote_poll: YesNoVotePoll, +} + +impl YesNoVotePollStateDriveQuery { + /// The path query: the four keys of the poll's tree. + pub fn construct_path_query(&self) -> Result { + let path = self.vote_poll.poll_path_vec()?; + let mut query = Query::new_with_direction(true); + query.insert_key(vec![YES_NO_VOTE_POLL_STORED_INFO_KEY]); + for vote_choice in YesNoAbstainVoteChoice::ALL { + query.insert_key(vec![vote_choice.to_tree_key()]); + } + // Four keys at most; absence proofs need the limit stated. + Ok(PathQuery::new( + path, + SizedQuery::new( + query, + Some(1 + YesNoAbstainVoteChoice::ALL.len() as u16), + None, + ), + )) + } + + /// Reads the state out of the elements of the poll's tree, by key. Keys that are absent + /// leave their part of the state empty: a poll that was never opened has no stored info, + /// and a finished poll has no vote trees. + pub(crate) fn state_from_key_elements( + key_elements: impl IntoIterator, Element)>, + ) -> Result { + let mut state = YesNoVotePollState::default(); + for (key, element) in key_elements { + let [key_byte] = key.as_slice() else { + return Err(Error::Drive(DriveError::CorruptedDriveState(format!( + "unexpected key {} in yes/no vote poll tree", + hex::encode(&key) + )))); + }; + if *key_byte == YES_NO_VOTE_POLL_STORED_INFO_KEY { + let bytes = element.into_item_bytes()?; + state.stored_info = Some( + YesNoVotePollStoredInfo::deserialize_from_bytes_untrusted(&bytes)?, + ); + continue; + } + let Some(vote_choice) = YesNoAbstainVoteChoice::from_tree_key(*key_byte) else { + return Err(Error::Drive(DriveError::CorruptedDriveState(format!( + "unexpected key {} in yes/no vote poll tree", + hex::encode(&key) + )))); + }; + let Element::SumTree(_, sum, _) = element else { + return Err(Error::Drive(DriveError::CorruptedDriveState(format!( + "the {} votes of a yes/no vote poll must be a sum tree", + vote_choice + )))); + }; + let voting_power: VotingPower = sum.try_into().map_err(|_| { + Error::Drive(DriveError::CorruptedDriveState(format!( + "the {} voting power of a yes/no vote poll must be between 0 and u32::MAX, got {}", + vote_choice, sum + ))) + })?; + match vote_choice { + YesNoAbstainVoteChoice::Yes => state.yes_voting_power = voting_power, + YesNoAbstainVoteChoice::No => state.no_voting_power = voting_power, + YesNoAbstainVoteChoice::Abstain => state.abstain_voting_power = voting_power, + } + } + Ok(state) + } + + /// Executes the query and returns the proof. + #[cfg(feature = "server")] + pub fn execute_with_proof( + &self, + drive: &Drive, + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let path_query = self.construct_path_query()?; + drive.grove_get_proved_path_query( + &path_query, + transaction, + drive_operations, + &platform_version.drive, + ) + } + + /// Executes the query and returns the state. + #[cfg(feature = "server")] + pub fn execute_no_proof( + &self, + drive: &Drive, + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result { + // A poll that was never opened has no tree; the proof path reports the same as an + // empty state, so does this one. + let poll_id = self.vote_poll.unique_id()?; + let exists = drive.grove_has_raw( + (&vote_decisions_active_polls_tree_path()).into(), + poll_id.as_bytes(), + DirectQueryType::StatefulDirectQuery, + transaction, + drive_operations, + &platform_version.drive, + )?; + if !exists { + return Ok(YesNoVotePollState::default()); + } + let path_query = self.construct_path_query()?; + let key_elements = drive + .grove_get_raw_path_query( + &path_query, + transaction, + QueryResultType::QueryKeyElementPairResultType, + drive_operations, + &platform_version.drive, + )? + .0 + .to_key_elements(); + Self::state_from_key_elements(key_elements) + } +} + +#[cfg(all(test, feature = "server", feature = "verify"))] +mod tests { + use super::*; + use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::platform_value::BinaryData; + use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStatus; + + fn poll() -> YesNoVotePoll { + YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![0xc1; 32])], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: 400, + } + } + + #[test] + fn should_prove_and_verify_the_state_of_a_poll_with_votes() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let vote_poll = poll(); + drive + .open_yes_no_vote_poll( + &vote_poll, + 1_000, + &BlockInfo::default(), + None, + platform_version, + ) + .expect("expected to open the poll"); + for (voter, strength, vote_choice) in [ + ([1u8; 32], 4, YesNoAbstainVoteChoice::Yes), + ([2u8; 32], 1, YesNoAbstainVoteChoice::Yes), + ([3u8; 32], 1, YesNoAbstainVoteChoice::No), + ] { + drive + .register_yes_no_identity_vote( + voter, + strength, + ResolvedYesNoVote { + vote_poll: vote_poll.clone(), + vote_choice, + previous_vote_choice_to_remove: None, + }, + &BlockInfo::default(), + None, + platform_version, + ) + .expect("expected to register the vote"); + } + let query = YesNoVotePollStateDriveQuery { vote_poll }; + let state = query + .execute_no_proof(&drive, None, &mut vec![], platform_version) + .expect("expected the state"); + assert_eq!( + ( + state.yes_voting_power, + state.no_voting_power, + state.abstain_voting_power + ), + (5, 1, 0) + ); + assert!(matches!( + state.stored_info.as_ref().expect("stored info").status(), + YesNoVotePollStatus::Started(_) + )); + + let proof = query + .execute_with_proof(&drive, None, &mut vec![], platform_version) + .expect("expected a proof"); + let (root_hash, proved_state) = query + .verify_yes_no_vote_poll_state_proof(&proof, platform_version) + .expect("expected the proof to verify"); + assert_eq!( + root_hash, + drive + .grove + .root_hash(None, &platform_version.drive.grove_version) + .unwrap() + .expect("root hash") + ); + assert_eq!(proved_state, state); + } + + #[test] + fn should_prove_and_verify_the_absence_of_a_poll() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let query = YesNoVotePollStateDriveQuery { vote_poll: poll() }; + let proof = query + .execute_with_proof(&drive, None, &mut vec![], platform_version) + .expect("expected a proof"); + let (_, proved_state) = query + .verify_yes_no_vote_poll_state_proof(&proof, platform_version) + .expect("expected the proof to verify"); + assert_eq!(proved_state, YesNoVotePollState::default()); + let state = query + .execute_no_proof(&drive, None, &mut vec![], platform_version) + .expect("expected the state of an absent poll"); + assert_eq!(state, YesNoVotePollState::default()); + } +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/identity/masternode_vote_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/identity/masternode_vote_transition.rs index 37740ef7900..317215f624f 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/identity/masternode_vote_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/identity/masternode_vote_transition.rs @@ -183,6 +183,7 @@ mod tests { assert_eq!(v0.resource_vote_choice, ResourceVoteChoice::Abstain); } }, + ResolvedVote::YesNoVote(_) => panic!("expected a resource vote"), } } other => panic!("expected MasternodeCastVote, got {:?}", other), @@ -291,6 +292,7 @@ mod tests { assert_eq!(v0.resource_vote_choice, ResourceVoteChoice::Abstain); } }, + ResolvedVote::YesNoVote(_) => panic!("expected a resource vote"), } } other => panic!("expected MasternodeCastVote, got {:?}", other), diff --git a/packages/rs-drive/src/structure/tests.rs b/packages/rs-drive/src/structure/tests.rs index 8babc356afe..39a0093f79c 100644 --- a/packages/rs-drive/src/structure/tests.rs +++ b/packages/rs-drive/src/structure/tests.rs @@ -273,6 +273,7 @@ mod fixtures { use super::*; use crate::drive::credit_pools::epochs::operations_factory::EpochOperations; use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfo; + use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; use crate::drive::Drive; use crate::fees::op::LowLevelDriveOperation; use crate::structure::export::{LayerShape, ShapeNode, StateShape}; @@ -352,7 +353,9 @@ mod fixtures { use dpp::tokens::status::TokenStatus; use dpp::tokens::token_event::TokenEvent; use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; + use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_info_storage::contested_document_vote_poll_stored_info::ContestedDocumentVotePollStoredInfo; + use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use rand::rngs::StdRng; use rand::SeedableRng; use std::collections::{BTreeMap, BTreeSet}; @@ -1194,6 +1197,49 @@ mod fixtures { conformance_of(&drive, "contested_documents", run); } + /// A yes/no vote poll with one vote per choice, so every node of the decisions branch is + /// reached: the poll's stored info and vote trees, and the voters' index entries. + fn yes_no_vote_polls(run: &mut FixtureRun) { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let vote_poll = YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![0xc1; 32])], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: 400, + }; + drive + .open_yes_no_vote_poll( + &vote_poll, + 1_000, + &BlockInfo::default(), + None, + platform_version, + ) + .expect("expected to open the yes/no vote poll"); + for (voter, strength, vote_choice) in [ + ([1; 32], 4, YesNoAbstainVoteChoice::Yes), + ([2; 32], 1, YesNoAbstainVoteChoice::No), + ([3; 32], 1, YesNoAbstainVoteChoice::Abstain), + ] { + drive + .register_yes_no_identity_vote( + voter, + strength, + ResolvedYesNoVote { + vote_poll: vote_poll.clone(), + vote_choice, + previous_vote_choice_to_remove: None, + }, + &BlockInfo::default(), + None, + platform_version, + ) + .expect("expected to register the vote"); + } + conformance_of(&drive, "yes_no_vote_polls", run); + } + fn apply_operations(drive: &Drive, operations: Vec) { drive .apply_batch_low_level_drive_operations( @@ -1510,6 +1556,7 @@ mod fixtures { address_balances(&mut run); current_then_paid_epoch(&mut run); contested_documents(&mut run); + yes_no_vote_polls(&mut run); token_distributions(&mut run); contract_groups_and_bound_keys(&mut run); spent_nullifiers(&mut run); diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 0f358c11eed..393014acf67 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -1538,31 +1538,47 @@ impl Drive { StateTransition::MasternodeVote(masternode_vote) => { let pro_tx_hash = masternode_vote.pro_tx_hash(); let vote = masternode_vote.vote(); - let contract = match vote { - Vote::ResourceVote(resource_vote) => match resource_vote.vote_poll() { - VotePoll::ContestedDocumentResourceVotePoll( - contested_document_resource_vote_poll, - ) => known_contracts_provider_fn( - &contested_document_resource_vote_poll.contract_id, + let (root_hash, vote) = match vote { + Vote::ResourceVote(resource_vote) => { + let contract = match resource_vote.vote_poll() { + VotePoll::ContestedDocumentResourceVotePoll( + contested_document_resource_vote_poll, + ) => known_contracts_provider_fn( + &contested_document_resource_vote_poll.contract_id, + )? + .ok_or(Error::Proof( + ProofError::UnknownContract(format!( + "unknown contract with id {} in resource vote verification", + contested_document_resource_vote_poll.contract_id + )), + ))?, + VotePoll::YesNoVotePoll(_) => { + return Err(Error::Proof(ProofError::InvalidTransition( + "a resource vote cannot answer a yes/no vote poll".to_string(), + ))) + } + }; + // we expect to get a vote that matches the state transition + Drive::verify_masternode_vote( + proof, + pro_tx_hash.to_buffer(), + vote, + &contract, + false, + platform_version, )? - .ok_or(Error::Proof( - ProofError::UnknownContract(format!( - "unknown contract with id {} in resource vote verification", - contested_document_resource_vote_poll.contract_id - )), - ))?, - }, + } + Vote::YesNoVote(yes_no_vote) => { + let (root_hash, proved_vote) = Drive::verify_masternode_yes_no_vote( + proof, + pro_tx_hash.to_buffer(), + yes_no_vote, + false, + platform_version, + )?; + (root_hash, proved_vote.map(Vote::YesNoVote)) + } }; - - // we expect to get a vote that matches the state transition - let (root_hash, vote) = Drive::verify_masternode_vote( - proof, - pro_tx_hash.to_buffer(), - vote, - &contract, - false, - platform_version, - )?; let vote = vote.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain actual vote for masternode {} expected to exist because of state transition (masternode vote)", masternode_vote.pro_tx_hash()))))?; Ok((root_hash, VerifiedMasternodeVote(vote))) } @@ -4803,6 +4819,125 @@ mod tests { ); } + // --- MasternodeVote on a yes/no poll: the prover's path query and the verifier agree, + // so a registered vote is proved executed and an unregistered one is refused. + #[test] + fn verify_masternode_yes_no_vote_executed_round_trip() { + use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::platform_value::BinaryData; + use dpp::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; + use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; + use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; + use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; + use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; + use dpp::voting::votes::yes_no_vote::YesNoVote; + use dpp::voting::votes::Vote; + + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(None); + let vote_poll = YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![0xc1; 32])], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: 400, + }; + drive + .open_yes_no_vote_poll( + &vote_poll, + 1_000, + &BlockInfo::default(), + None, + platform_version, + ) + .expect("expected to open the poll"); + let voter = [1u8; 32]; + drive + .register_yes_no_identity_vote( + voter, + 4, + ResolvedYesNoVote { + vote_poll: vote_poll.clone(), + vote_choice: YesNoAbstainVoteChoice::Yes, + previous_vote_choice_to_remove: None, + }, + &BlockInfo::default(), + None, + platform_version, + ) + .expect("expected to register the vote"); + + let transition_for = |voter: [u8; 32], vote_choice: YesNoAbstainVoteChoice| { + StateTransition::MasternodeVote(MasternodeVoteTransition::V0( + MasternodeVoteTransitionV0 { + pro_tx_hash: dpp::prelude::Identifier::from(voter), + voter_identity_id: dpp::prelude::Identifier::from([2u8; 32]), + vote: Vote::YesNoVote(YesNoVote::V0(YesNoVoteV0 { + vote_poll: vote_poll.clone(), + vote_choice, + })), + nonce: 1, + signature_public_key_id: 0, + signature: Default::default(), + }, + )) + }; + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let executed = transition_for(voter, YesNoAbstainVoteChoice::Yes); + let proof = drive + .prove_state_transition(&executed, None, platform_version) + .expect("expected to prove the vote") + .into_data() + .expect("expected proof bytes"); + let (root_hash, verified) = Drive::verify_state_transition_was_executed_with_proof( + &executed, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ) + .expect("expected the executed vote to verify"); + assert_eq!( + root_hash, + drive + .grove + .root_hash(None, &platform_version.drive.grove_version) + .unwrap() + .expect("root hash") + ); + let StateTransition::MasternodeVote(masternode_vote) = &executed else { + unreachable!() + }; + assert_eq!( + verified.into_result(), + VerifiedMasternodeVote(masternode_vote.vote().clone()) + ); + + // A vote the state does not hold is not proved executed. + let not_executed = transition_for([9u8; 32], YesNoAbstainVoteChoice::No); + let proof = drive + .prove_state_transition(¬_executed, None, platform_version) + .expect("expected to prove the absence") + .into_data() + .expect("expected proof bytes"); + let result = Drive::verify_state_transition_was_executed_with_proof( + ¬_executed, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ); + assert!( + matches!( + result, + Err(crate::error::Error::Proof(ProofError::IncorrectProof(_))) + ), + "expected IncorrectProof, got {:?}", + result + ); + } + // --- MasternodeVote: unknown contract returns a specific // `ProofError::UnknownContract` because the provider returns None. #[test] diff --git a/packages/rs-drive/src/verify/voting/mod.rs b/packages/rs-drive/src/verify/voting/mod.rs index 28169ef1537..edfab9061b9 100644 --- a/packages/rs-drive/src/verify/voting/mod.rs +++ b/packages/rs-drive/src/verify/voting/mod.rs @@ -3,7 +3,9 @@ mod verify_contests_proof; mod verify_identity_votes_given_proof; mod verify_masternode_vote; +mod verify_masternode_yes_no_vote; mod verify_specialized_balance; mod verify_vote_poll_vote_state_proof; mod verify_vote_poll_votes_proof; mod verify_vote_polls_end_date_query; +mod verify_yes_no_vote_poll_state_proof; diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs new file mode 100644 index 00000000000..c0eb4008a27 --- /dev/null +++ b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs @@ -0,0 +1,154 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::verify::RootHash; +use dpp::version::PlatformVersion; +use dpp::voting::votes::yes_no_vote::YesNoVote; + +impl Drive { + /// Verifies a proof of a masternode's vote on a yes/no poll. + /// + /// Returns the root hash and the proved vote, `None` when the proof shows the masternode + /// has not voted on the poll. A proved vote whose answer differs from `vote` is an error. + pub fn verify_masternode_yes_no_vote( + proof: &[u8], + masternode_pro_tx_hash: [u8; 32], + vote: &YesNoVote, + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, + ) -> Result<(RootHash, Option), Error> { + match platform_version + .drive + .methods + .verify + .voting + .verify_masternode_yes_no_vote + { + 0 => Drive::verify_masternode_yes_no_vote_v0( + proof, + masternode_pro_tx_hash, + vote, + verify_subset_of_proof, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "verify_masternode_yes_no_vote".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} + +#[cfg(all(test, feature = "server"))] +mod tests { + use super::*; + use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; + use crate::error::proof::ProofError; + use crate::query::IdentityBasedVoteDriveQuery; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::platform_value::{BinaryData, Identifier}; + use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; + use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; + use dpp::voting::vote_polls::VotePoll; + use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; + + fn poll() -> YesNoVotePoll { + YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![0xc1; 32])], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: 400, + } + } + + fn vote(vote_choice: YesNoAbstainVoteChoice) -> YesNoVote { + YesNoVote::V0(YesNoVoteV0 { + vote_poll: poll(), + vote_choice, + }) + } + + fn proof_of_vote(drive: &crate::drive::Drive, voter: [u8; 32]) -> Vec { + let platform_version = PlatformVersion::latest(); + let mut path_query = IdentityBasedVoteDriveQuery { + identity_id: Identifier::new(voter), + vote_poll: VotePoll::YesNoVotePoll(poll()), + } + .construct_path_query() + .expect("path query"); + path_query.query.limit = None; + drive + .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) + .expect("proof") + } + + #[test] + fn should_prove_a_vote_and_its_absence() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + drive + .open_yes_no_vote_poll( + &poll(), + 1_000, + &BlockInfo::default(), + None, + platform_version, + ) + .expect("open"); + let voter = [7u8; 32]; + drive + .register_yes_no_identity_vote( + voter, + 1, + ResolvedYesNoVote { + vote_poll: poll(), + vote_choice: YesNoAbstainVoteChoice::No, + previous_vote_choice_to_remove: None, + }, + &BlockInfo::default(), + None, + platform_version, + ) + .expect("register"); + + let proof = proof_of_vote(&drive, voter); + let (_, proved) = Drive::verify_masternode_yes_no_vote( + &proof, + voter, + &vote(YesNoAbstainVoteChoice::No), + false, + platform_version, + ) + .expect("verify"); + assert_eq!(proved, Some(vote(YesNoAbstainVoteChoice::No))); + + // The proof shows the answer; claiming another one is caught. + assert!(matches!( + Drive::verify_masternode_yes_no_vote( + &proof, + voter, + &vote(YesNoAbstainVoteChoice::Yes), + false, + platform_version, + ), + Err(Error::Proof(ProofError::IncorrectProof(_))) + )); + + // A masternode that has not voted proves absent. + let other = [8u8; 32]; + let proof = proof_of_vote(&drive, other); + let (_, proved) = Drive::verify_masternode_yes_no_vote( + &proof, + other, + &vote(YesNoAbstainVoteChoice::No), + false, + platform_version, + ) + .expect("verify"); + assert_eq!(proved, None); + } +} diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs new file mode 100644 index 00000000000..28cd27ecdec --- /dev/null +++ b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs @@ -0,0 +1,74 @@ +use crate::drive::votes::paths::vote_decisions_identity_votes_tree_path_for_identity_vec; +use crate::drive::votes::storage_form::yes_no_vote_reference_storage_form::YesNoVoteReferenceStorageForm; +use crate::drive::Drive; +use crate::error::proof::ProofError; +use crate::error::Error; +use crate::query::Query; +use crate::verify::RootHash; +use dpp::voting::votes::yes_no_vote::accessors::v0::YesNoVoteGettersV0; +use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; +use dpp::voting::votes::yes_no_vote::YesNoVote; +use grovedb::{GroveDb, PathQuery, SizedQuery}; +use platform_version::version::PlatformVersion; + +impl Drive { + #[inline(always)] + pub(super) fn verify_masternode_yes_no_vote_v0( + proof: &[u8], + masternode_pro_tx_hash: [u8; 32], + vote: &YesNoVote, + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, + ) -> Result<(RootHash, Option), Error> { + let path = + vote_decisions_identity_votes_tree_path_for_identity_vec(&masternode_pro_tx_hash); + let vote_poll_id = vote.vote_poll().unique_id()?; + let mut query = Query::new(); + query.insert_key(vote_poll_id.to_vec()); + let path_query = PathQuery::new(path, SizedQuery::new(query, Some(1), None)); + let (root_hash, mut proved_key_values) = if verify_subset_of_proof { + GroveDb::verify_subset_query_with_absence_proof( + proof, + &path_query, + &platform_version.drive.grove_version, + )? + } else { + GroveDb::verify_query_with_absence_proof( + proof, + &path_query, + &platform_version.drive.grove_version, + )? + }; + if proved_key_values.len() != 1 { + return Err(Error::Proof(ProofError::TooManyElements( + "expected one masternode yes/no vote", + ))); + } + let (_, _, maybe_element) = proved_key_values.remove(0); + let maybe_vote = maybe_element + .map(|element| { + let bytes = element.into_item_bytes()?; + let storage_form = + YesNoVoteReferenceStorageForm::deserialize(&bytes).map_err(|e| { + Error::Proof(ProofError::CorruptedProof(format!( + "the proved yes/no vote could not be decoded: {}", + e + ))) + })?; + let proved_vote = YesNoVote::V0(YesNoVoteV0 { + vote_poll: vote.vote_poll().clone(), + vote_choice: storage_form.vote_choice, + }); + if &proved_vote != vote { + Err(Error::Proof(ProofError::IncorrectProof(format!( + "returned vote {:?} does not match the vote that was sent {:?}", + proved_vote, vote + )))) + } else { + Ok::(proved_vote) + } + }) + .transpose()?; + Ok((root_hash, maybe_vote)) + } +} diff --git a/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs b/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs new file mode 100644 index 00000000000..19cb04ef544 --- /dev/null +++ b/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs @@ -0,0 +1,33 @@ +mod v0; + +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::query::yes_no_vote_poll_state_query::{ + YesNoVotePollState, YesNoVotePollStateDriveQuery, +}; +use crate::verify::RootHash; +use dpp::version::PlatformVersion; + +impl YesNoVotePollStateDriveQuery { + /// Verifies a proof of the state of a yes/no vote poll: its stored info and its tallies. + pub fn verify_yes_no_vote_poll_state_proof( + &self, + proof: &[u8], + platform_version: &PlatformVersion, + ) -> Result<(RootHash, YesNoVotePollState), Error> { + match platform_version + .drive + .methods + .verify + .voting + .verify_yes_no_vote_poll_state_proof + { + 0 => self.verify_yes_no_vote_poll_state_proof_v0(proof, platform_version), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "verify_yes_no_vote_poll_state_proof".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/v0/mod.rs b/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/v0/mod.rs new file mode 100644 index 00000000000..3da2b6e040e --- /dev/null +++ b/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/v0/mod.rs @@ -0,0 +1,28 @@ +use crate::error::Error; +use crate::query::yes_no_vote_poll_state_query::{ + YesNoVotePollState, YesNoVotePollStateDriveQuery, +}; +use crate::verify::RootHash; +use grovedb::GroveDb; +use platform_version::version::PlatformVersion; + +impl YesNoVotePollStateDriveQuery { + #[inline(always)] + pub(super) fn verify_yes_no_vote_poll_state_proof_v0( + &self, + proof: &[u8], + platform_version: &PlatformVersion, + ) -> Result<(RootHash, YesNoVotePollState), Error> { + let path_query = self.construct_path_query()?; + let (root_hash, proved_key_values) = GroveDb::verify_query_with_absence_proof( + proof, + &path_query, + &platform_version.drive.grove_version, + )?; + let key_elements = proved_key_values + .into_iter() + .filter_map(|(_, key, maybe_element)| maybe_element.map(|element| (key, element))); + let state = Self::state_from_key_elements(key_elements)?; + Ok((root_hash, state)) + } +} diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs index 900fab5a43c..0318614f8e7 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs @@ -8,4 +8,6 @@ pub struct DPPVotingVersions { pub default_vote_poll_time_duration_mainnet_ms: u64, pub default_vote_poll_time_duration_test_network_ms: u64, pub contested_document_vote_poll_stored_info_version: FeatureVersion, + /// The stored info of a yes/no poll (protocol version 14). + pub yes_no_vote_poll_stored_info_version: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs index 944a295d1fe..4e19daaf563 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs @@ -4,4 +4,5 @@ pub const VOTING_VERSION_V1: DPPVotingVersions = DPPVotingVersions { default_vote_poll_time_duration_mainnet_ms: 1_209_600_000, //2 weeks default_vote_poll_time_duration_test_network_ms: 1_209_600_000, //2 weeks contested_document_vote_poll_stored_info_version: 0, + yes_no_vote_poll_stored_info_version: 0, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs index de072e87837..409b7bec462 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs @@ -4,4 +4,5 @@ pub const VOTING_VERSION_V2: DPPVotingVersions = DPPVotingVersions { default_vote_poll_time_duration_mainnet_ms: 1_209_600_000, //2 weeks default_vote_poll_time_duration_test_network_ms: 5_400_000, //90 minutes contested_document_vote_poll_stored_info_version: 0, + yes_no_vote_poll_stored_info_version: 0, }; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs index 3b1499e29cd..f9615f8af65 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs @@ -152,6 +152,12 @@ pub struct DriveAbciVotingMethodVersions { pub delay_vote_poll: FeatureVersion, pub run_dao_platform_events: FeatureVersion, pub remove_votes_for_removed_masternodes: FeatureVersion, + /// The yes/no poll kind (protocol version 14): its tally, its record, its clean-up and the + /// hook a feature acts on when one of its polls finishes. + pub tally_votes_for_yes_no_vote_poll: FeatureVersion, + pub keep_record_of_finished_yes_no_vote_poll: FeatureVersion, + pub clean_up_after_yes_no_vote_polls_end: FeatureVersion, + pub on_yes_no_vote_poll_finished: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v1.rs index 1356dbdaac1..4429368e9ba 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v1.rs @@ -102,6 +102,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V1: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, + tally_votes_for_yes_no_vote_poll: 0, + keep_record_of_finished_yes_no_vote_poll: 0, + clean_up_after_yes_no_vote_polls_end: 0, + on_yes_no_vote_poll_finished: 0, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs index 49de2a0083f..9853bd36095 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs @@ -102,14 +102,18 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V10: DriveAbciMethodVersions = DriveAbciMet }, voting: DriveAbciVotingMethodVersions { keep_record_of_finished_contested_resource_vote_poll: 0, - clean_up_after_vote_poll_end: 0, - clean_up_after_contested_resources_vote_poll_end: 1, - check_for_ended_vote_polls: 1, // changed in v14: a tie goes to the earliest contender + clean_up_after_vote_poll_end: 1, // changed in v14: also cleans up finished yes/no polls, and removes every finished poll from the end date index itself + clean_up_after_contested_resources_vote_poll_end: 2, // changed in v14: leaves the end date index to clean_up_after_vote_poll_end + check_for_ended_vote_polls: 1, // changed in v14: a tie goes to the earliest contender, and yes/no polls are closed too tally_votes_for_contested_document_resource_vote_poll: 0, award_document_to_winner: 0, delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, + tally_votes_for_yes_no_vote_poll: 0, // new in v14 + keep_record_of_finished_yes_no_vote_poll: 0, // new in v14 + clean_up_after_yes_no_vote_polls_end: 0, // new in v14 + on_yes_no_vote_poll_finished: 0, // new in v14 }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 1, // changed: deducts what a budgeted signing key spent from its remaining budget, and charges a document batch's gas sponsor instead of its signer diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v2.rs index a648c5553d5..0dd9de5dd0f 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v2.rs @@ -103,6 +103,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V2: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, + tally_votes_for_yes_no_vote_poll: 0, + keep_record_of_finished_yes_no_vote_poll: 0, + clean_up_after_yes_no_vote_polls_end: 0, + on_yes_no_vote_poll_finished: 0, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v3.rs index dc4873efd51..90b93945fd4 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v3.rs @@ -102,6 +102,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V3: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, + tally_votes_for_yes_no_vote_poll: 0, + keep_record_of_finished_yes_no_vote_poll: 0, + clean_up_after_yes_no_vote_polls_end: 0, + on_yes_no_vote_poll_finished: 0, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v4.rs index 6aa6c34ac4c..e06b75faf02 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v4.rs @@ -102,6 +102,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V4: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, + tally_votes_for_yes_no_vote_poll: 0, + keep_record_of_finished_yes_no_vote_poll: 0, + clean_up_after_yes_no_vote_polls_end: 0, + on_yes_no_vote_poll_finished: 0, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v5.rs index 8772809b224..b3ca7d4dd2a 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v5.rs @@ -106,6 +106,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V5: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, + tally_votes_for_yes_no_vote_poll: 0, + keep_record_of_finished_yes_no_vote_poll: 0, + clean_up_after_yes_no_vote_polls_end: 0, + on_yes_no_vote_poll_finished: 0, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v6.rs index 71f03c433f5..16c1a7bb0e5 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v6.rs @@ -104,6 +104,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V6: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, + tally_votes_for_yes_no_vote_poll: 0, + keep_record_of_finished_yes_no_vote_poll: 0, + clean_up_after_yes_no_vote_polls_end: 0, + on_yes_no_vote_poll_finished: 0, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v7.rs index a03a16bb9eb..a8283840fd5 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v7.rs @@ -111,6 +111,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V7: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, + tally_votes_for_yes_no_vote_poll: 0, + keep_record_of_finished_yes_no_vote_poll: 0, + clean_up_after_yes_no_vote_polls_end: 0, + on_yes_no_vote_poll_finished: 0, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v8.rs index a89543c3f84..5e7ed132811 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v8.rs @@ -113,6 +113,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V8: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, + tally_votes_for_yes_no_vote_poll: 0, + keep_record_of_finished_yes_no_vote_poll: 0, + clean_up_after_yes_no_vote_polls_end: 0, + on_yes_no_vote_poll_finished: 0, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v9.rs index 47e32f39da8..469197058c9 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v9.rs @@ -122,6 +122,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V9: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, + tally_votes_for_yes_no_vote_poll: 0, + keep_record_of_finished_yes_no_vote_poll: 0, + clean_up_after_yes_no_vote_polls_end: 0, + on_yes_no_vote_poll_finished: 0, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index e7802f8a616..5d4e5ec1f13 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -112,8 +112,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = advanced_structure: Some(0), identity_signatures: None, nonce: Some(1), - state: 1, // changed in v14: refuses a Lock vote on a contested index resolved without locking - transform_into_action: 0, + state: 1, // changed in v14: refuses a Lock vote on a contested index resolved without locking; a vote on a yes/no poll checks that poll's stored info + transform_into_action: 1, // changed in v14: a vote on a yes/no poll looks up the masternode's previous yes/no vote }, masternode_vote_state_transition_balance_pre_check: 0, contract_create_state_transition: DriveAbciStateTransitionValidationVersion { diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs index 9a6f1ba4a77..3e7714d6a68 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs @@ -182,6 +182,10 @@ pub struct DriveVerifyVoteMethodVersions { pub verify_contests_proof: FeatureVersion, pub verify_vote_polls_by_end_date_proof: FeatureVersion, pub verify_specialized_balance: FeatureVersion, + /// The stored info and tallies of a yes/no poll (protocol version 14). + pub verify_yes_no_vote_poll_state_proof: FeatureVersion, + /// A masternode's vote on a yes/no poll (protocol version 14). + pub verify_masternode_yes_no_vote: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs index 312f4c22275..eff0506447d 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs @@ -126,6 +126,8 @@ pub const DRIVE_VERIFY_METHOD_VERSIONS_V1: DriveVerifyMethodVersions = DriveVeri verify_contests_proof: 0, verify_vote_polls_by_end_date_proof: 0, verify_specialized_balance: 0, + verify_yes_no_vote_poll_state_proof: 0, + verify_masternode_yes_no_vote: 0, }, address_funds: DriveVerifyAddressFundsMethodVersions { verify_address_info: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs index 825b0629051..609f52b10ab 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs @@ -2,6 +2,7 @@ use versioned_feature_core::FeatureVersion; pub mod v1; pub mod v2; +pub mod v3; #[derive(Clone, Debug, Default)] pub struct DriveVoteMethodVersions { @@ -11,6 +12,22 @@ pub struct DriveVoteMethodVersions { pub setup: DriveVoteSetupMethodVersions, pub storage_form: DriveVoteStorageFormMethodVersions, pub fetch: DriveVoteFetchMethodVersions, + /// Yes/no polls: the decision polls under the votes tree's `d` branch (protocol version 14). + pub yes_no: DriveVoteYesNoMethodVersions, +} + +/// The methods of the yes/no poll kind. All of them are new in protocol version 14 and +/// unreachable before it: no yes/no poll can be opened or voted on earlier. +#[derive(Clone, Debug, Default)] +pub struct DriveVoteYesNoMethodVersions { + pub open_yes_no_vote_poll: FeatureVersion, + pub register_yes_no_identity_vote: FeatureVersion, + pub insert_stored_info_for_yes_no_vote_poll: FeatureVersion, + pub fetch_yes_no_vote_poll_stored_info: FeatureVersion, + pub fetch_identity_yes_no_vote: FeatureVersion, + pub fetch_identities_voting_in_yes_no_vote_poll: FeatureVersion, + pub remove_yes_no_vote_poll_votes_operations: FeatureVersion, + pub remove_yes_no_vote_references_given_by_identity: FeatureVersion, } #[derive(Clone, Debug, Default)] @@ -40,6 +57,11 @@ pub struct DriveVoteCleanupMethodVersions { pub remove_contested_resource_vote_poll_contenders_operations: FeatureVersion, pub remove_contested_resource_top_level_index_operations: FeatureVersion, pub remove_contested_resource_info_operations: FeatureVersion, + /// Removes every vote a removed masternode cast. Version 1 (protocol version 14) also + /// sweeps its yes/no votes. + pub remove_all_votes_given_by_identities: FeatureVersion, + /// Removes finished polls of every kind from the end date index (protocol version 14). + pub remove_vote_poll_end_date_query_operations: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs index 1e7a59dd95e..9d46bdfad07 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs @@ -1,7 +1,7 @@ use crate::version::drive_versions::drive_vote_method_versions::{ DriveVoteCleanupMethodVersions, DriveVoteContestedResourceInsertMethodVersions, DriveVoteFetchMethodVersions, DriveVoteInsertMethodVersions, DriveVoteMethodVersions, - DriveVoteSetupMethodVersions, DriveVoteStorageFormMethodVersions, + DriveVoteSetupMethodVersions, DriveVoteStorageFormMethodVersions, DriveVoteYesNoMethodVersions, }; pub const DRIVE_VOTE_METHOD_VERSIONS_V1: DriveVoteMethodVersions = DriveVoteMethodVersions { @@ -23,6 +23,8 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V1: DriveVoteMethodVersions = DriveVoteMeth remove_contested_resource_vote_poll_contenders_operations: 0, remove_contested_resource_top_level_index_operations: 0, remove_contested_resource_info_operations: 0, + remove_all_votes_given_by_identities: 0, + remove_vote_poll_end_date_query_operations: 0, }, setup: DriveVoteSetupMethodVersions { add_initial_vote_tree_main_structure_operations: 0, @@ -35,4 +37,14 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V1: DriveVoteMethodVersions = DriveVoteMeth fetch_contested_document_vote_poll_stored_info: 0, fetch_identity_contested_resource_vote: 0, }, + yes_no: DriveVoteYesNoMethodVersions { + open_yes_no_vote_poll: 0, + register_yes_no_identity_vote: 0, + insert_stored_info_for_yes_no_vote_poll: 0, + fetch_yes_no_vote_poll_stored_info: 0, + fetch_identity_yes_no_vote: 0, + fetch_identities_voting_in_yes_no_vote_poll: 0, + remove_yes_no_vote_poll_votes_operations: 0, + remove_yes_no_vote_references_given_by_identity: 0, + }, }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs index ef99612f58d..cbe2331c0ab 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs @@ -1,7 +1,7 @@ use crate::version::drive_versions::drive_vote_method_versions::{ DriveVoteCleanupMethodVersions, DriveVoteContestedResourceInsertMethodVersions, DriveVoteFetchMethodVersions, DriveVoteInsertMethodVersions, DriveVoteMethodVersions, - DriveVoteSetupMethodVersions, DriveVoteStorageFormMethodVersions, + DriveVoteSetupMethodVersions, DriveVoteStorageFormMethodVersions, DriveVoteYesNoMethodVersions, }; pub const DRIVE_VOTE_METHOD_VERSIONS_V2: DriveVoteMethodVersions = DriveVoteMethodVersions { @@ -23,6 +23,8 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V2: DriveVoteMethodVersions = DriveVoteMeth remove_contested_resource_vote_poll_contenders_operations: 1, remove_contested_resource_top_level_index_operations: 0, remove_contested_resource_info_operations: 0, + remove_all_votes_given_by_identities: 0, + remove_vote_poll_end_date_query_operations: 0, }, setup: DriveVoteSetupMethodVersions { add_initial_vote_tree_main_structure_operations: 0, @@ -35,4 +37,14 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V2: DriveVoteMethodVersions = DriveVoteMeth fetch_contested_document_vote_poll_stored_info: 0, fetch_identity_contested_resource_vote: 0, }, + yes_no: DriveVoteYesNoMethodVersions { + open_yes_no_vote_poll: 0, + register_yes_no_identity_vote: 0, + insert_stored_info_for_yes_no_vote_poll: 0, + fetch_yes_no_vote_poll_stored_info: 0, + fetch_identity_yes_no_vote: 0, + fetch_identities_voting_in_yes_no_vote_poll: 0, + remove_yes_no_vote_poll_votes_operations: 0, + remove_yes_no_vote_references_given_by_identity: 0, + }, }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs new file mode 100644 index 00000000000..149ba79d42a --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs @@ -0,0 +1,33 @@ +use crate::version::drive_versions::drive_vote_method_versions::v2::DRIVE_VOTE_METHOD_VERSIONS_V2; +use crate::version::drive_versions::drive_vote_method_versions::{ + DriveVoteCleanupMethodVersions, DriveVoteContestedResourceInsertMethodVersions, + DriveVoteMethodVersions, DriveVoteSetupMethodVersions, +}; + +/// Drive vote method versions 3. Introduced in protocol version 14 with the yes/no poll +/// kind: +/// +/// * `contested_resource_insert.register_identity_vote` 1 (the slot `register_identity_vote` +/// dispatches on; `insert.register_identity_vote` is read by nothing) dispatches a resolved +/// vote of either kind (v0 knows only contested resource votes). +/// * `add_initial_vote_tree_main_structure_operations` 1 creates the active polls and +/// identity votes trees under the decisions branch at genesis (a chain upgrading to +/// protocol version 14 creates them on its first block). +/// * `remove_all_votes_given_by_identities` 1 also removes the yes/no votes of a masternode +/// that left the list. +/// +/// Everything else matches `DRIVE_VOTE_METHOD_VERSIONS_V2`. +pub const DRIVE_VOTE_METHOD_VERSIONS_V3: DriveVoteMethodVersions = DriveVoteMethodVersions { + contested_resource_insert: DriveVoteContestedResourceInsertMethodVersions { + register_identity_vote: 1, + ..DRIVE_VOTE_METHOD_VERSIONS_V2.contested_resource_insert + }, + cleanup: DriveVoteCleanupMethodVersions { + remove_all_votes_given_by_identities: 1, + ..DRIVE_VOTE_METHOD_VERSIONS_V2.cleanup + }, + setup: DriveVoteSetupMethodVersions { + add_initial_vote_tree_main_structure_operations: 1, + }, + ..DRIVE_VOTE_METHOD_VERSIONS_V2 +}; diff --git a/packages/rs-platform-version/src/version/drive_versions/v9.rs b/packages/rs-platform-version/src/version/drive_versions/v9.rs index c6590a4f08e..b16b2455c2f 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v9.rs @@ -11,7 +11,7 @@ use crate::version::drive_versions::drive_state_transition_method_versions::v4:: use crate::version::drive_versions::drive_structure_version::v1::DRIVE_STRUCTURE_V1; use crate::version::drive_versions::drive_token_method_versions::v2::DRIVE_TOKEN_METHOD_VERSIONS_V2; use crate::version::drive_versions::drive_verify_method_versions::v3::DRIVE_VERIFY_METHOD_VERSIONS_V3; -use crate::version::drive_versions::drive_vote_method_versions::v2::DRIVE_VOTE_METHOD_VERSIONS_V2; +use crate::version::drive_versions::drive_vote_method_versions::v3::DRIVE_VOTE_METHOD_VERSIONS_V3; use crate::version::drive_versions::{ DriveAssetLockMethodVersions, DriveBalancesMethodVersions, DriveBatchOperationsMethodVersion, DriveEstimatedCostsMethodVersions, DriveFeesMethodVersions, DriveFetchMethodVersions, @@ -85,7 +85,7 @@ pub const DRIVE_VERSION_V9: DriveVersion = DriveVersion { calculate_total_credits_balance: 2, // ShieldedBalances root tree adds a fifth term to the equation }, document: DRIVE_DOCUMENT_METHOD_VERSIONS_V4, // changed in v9: v2 index walkers + v1 update walker (shared-prefix aggregate indexes become insertable) and the detect_ranked_mode slot - vote: DRIVE_VOTE_METHOD_VERSIONS_V2, + vote: DRIVE_VOTE_METHOD_VERSIONS_V3, // changed in v14: the yes/no poll kind (register_identity_vote 1, genesis vote trees 1, masternode removal sweep 1) contract: DRIVE_CONTRACT_METHOD_VERSIONS_V4, // changed in v9: add_contract_to_storage v1 writes the contract version item beside the contract; update_contract v2 creates the distribution storage and mints the base supply of tokens added by an update fees: DriveFeesMethodVersions { calculate_fee: 0 }, estimated_costs: DriveEstimatedCostsMethodVersions { diff --git a/packages/rs-platform-version/src/version/mocks/v2_test.rs b/packages/rs-platform-version/src/version/mocks/v2_test.rs index 1105d50cd62..ddb161e4e15 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -590,6 +590,8 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { max_contract_moderators: 16, max_contract_suspension_until: 9_007_199_254_740_991, max_contract_moderation_reason_length: 1024, + max_yes_no_vote_poll_resource_path_segments: 16, + max_yes_no_vote_poll_resource_path_bytes: 1024, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_contract_moderation_election_window_seconds: 86_400, diff --git a/packages/rs-platform-version/src/version/mocks/v3_test.rs b/packages/rs-platform-version/src/version/mocks/v3_test.rs index 12d62c79dc3..834c7baa37c 100644 --- a/packages/rs-platform-version/src/version/mocks/v3_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v3_test.rs @@ -137,6 +137,10 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, + tally_votes_for_yes_no_vote_poll: 0, + keep_record_of_finished_yes_no_vote_poll: 0, + clean_up_after_yes_no_vote_polls_end: 0, + on_yes_no_vote_poll_finished: 0, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index 3bd698ed411..876cad3a1ab 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -136,6 +136,12 @@ pub struct SystemLimits { /// warning or a moderator's document deletion carries (`ContractModerationReason::text`). Read by the `ContractUserModeration` basic /// structure validation v0 (protocol version 14) and never reached before. pub max_contract_moderation_reason_length: u16, + /// The most segments a yes/no vote poll's resource path may have, read by + /// `YesNoVotePoll::validate_parameters` (protocol version 14). + pub max_yes_no_vote_poll_resource_path_segments: u8, + /// The most bytes a yes/no vote poll's resource path may hold across its segments, read by + /// `YesNoVotePoll::validate_parameters` (protocol version 14). Every vote carries the poll. + pub max_yes_no_vote_poll_resource_path_bytes: u16, /// Maximum number of warnings one identity may carry on a contract's warning list at a /// time: a warn that would exceed it is refused until the warnings are cleared. Read by /// the `ContractUserModeration` state validation v0 (protocol version 14) and never diff --git a/packages/rs-platform-version/src/version/system_limits/v1.rs b/packages/rs-platform-version/src/version/system_limits/v1.rs index 09ae4b30517..1a63e357216 100644 --- a/packages/rs-platform-version/src/version/system_limits/v1.rs +++ b/packages/rs-platform-version/src/version/system_limits/v1.rs @@ -51,6 +51,8 @@ pub const SYSTEM_LIMITS_V1: SystemLimits = SystemLimits { max_contract_moderators: 16, max_contract_suspension_until: 9_007_199_254_740_991, max_contract_moderation_reason_length: 1024, + max_yes_no_vote_poll_resource_path_segments: 16, + max_yes_no_vote_poll_resource_path_bytes: 1024, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/system_limits/v2.rs b/packages/rs-platform-version/src/version/system_limits/v2.rs index a0a40ae65be..8a5a850433c 100644 --- a/packages/rs-platform-version/src/version/system_limits/v2.rs +++ b/packages/rs-platform-version/src/version/system_limits/v2.rs @@ -32,6 +32,8 @@ pub const SYSTEM_LIMITS_V2: SystemLimits = SystemLimits { max_contract_moderators: 16, max_contract_suspension_until: 9_007_199_254_740_991, max_contract_moderation_reason_length: 1024, + max_yes_no_vote_poll_resource_path_segments: 16, + max_yes_no_vote_poll_resource_path_bytes: 1024, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/system_limits/v3.rs b/packages/rs-platform-version/src/version/system_limits/v3.rs index b14cac1f1d1..88a6bfc3214 100644 --- a/packages/rs-platform-version/src/version/system_limits/v3.rs +++ b/packages/rs-platform-version/src/version/system_limits/v3.rs @@ -34,6 +34,8 @@ pub const SYSTEM_LIMITS_V3: SystemLimits = SystemLimits { max_contract_moderators: 16, max_contract_suspension_until: 9_007_199_254_740_991, max_contract_moderation_reason_length: 1024, + max_yes_no_vote_poll_resource_path_segments: 16, + max_yes_no_vote_poll_resource_path_bytes: 1024, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/system_limits/v4.rs b/packages/rs-platform-version/src/version/system_limits/v4.rs index d9269889647..11808387759 100644 --- a/packages/rs-platform-version/src/version/system_limits/v4.rs +++ b/packages/rs-platform-version/src/version/system_limits/v4.rs @@ -86,6 +86,8 @@ pub const SYSTEM_LIMITS_V4: SystemLimits = SystemLimits { max_contract_moderators: 16, max_contract_suspension_until: 9_007_199_254_740_991, max_contract_moderation_reason_length: 1024, + max_yes_no_vote_poll_resource_path_segments: 16, + max_yes_no_vote_poll_resource_path_bytes: 1024, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 26d0903bf78..4bc2b37f085 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -784,11 +784,11 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// its gates on; Drive identity methods v2 rewrite the key and raise the remaining budget). pub const PLATFORM_V14: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_14, - drive: DRIVE_VERSION_V9, // changed: drive document method versions v4 — v2 index walkers (shared-prefix aggregate indexes become insertable) + the detect_ranked_mode slot; contract method versions v4: the moderation list trees, the document removal record trees and the moderation method table; apply_drive_operations 1 (a moderator's document deletion refunds nobody); index uniqueness gains validate_restored_document_uniqueness (a moderator's document restore) + drive: DRIVE_VERSION_V9, // changed: drive document method versions v4 — v2 index walkers (shared-prefix aggregate indexes become insertable) + the detect_ranked_mode slot; contract method versions v4: the moderation list trees, the document removal record trees and the moderation method table; apply_drive_operations 1 (a moderator's document deletion refunds nobody); index uniqueness gains validate_restored_document_uniqueness (a moderator's document restore); vote method versions v3: the yes/no poll kind drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, // changed: saved platform state structure 1 keeps masternodes and validator sets as one aux entry each - methods: DRIVE_ABCI_METHOD_VERSIONS_V10, // changed: records the per-block total credits history for the daily withdrawal limit - validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V10, // changed: contested-index cross-check + refersTo document reference validation; the ContractUserModeration gates and the batch transformer's contract_moderation_gate + methods: DRIVE_ABCI_METHOD_VERSIONS_V10, // changed: records the per-block total credits history for the daily withdrawal limit; closes and cleans up yes/no polls + validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V10, // changed: contested-index cross-check + refersTo document reference validation; the ContractUserModeration gates and the batch transformer's contract_moderation_gate; masternode vote state + transform 1 admit votes on yes/no polls withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, // changed: prune bound for the total credits history query: DRIVE_ABCI_QUERY_VERSIONS_V3, // changed: ranked + boolean-HAVING routing gate; the v1 handler also resolves IN_TIME_RANGE from committed block time checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1, diff --git a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs index 2980294e6b1..52c04f01316 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs @@ -464,7 +464,12 @@ impl Sdk { for (timestamp, polls) in result.0 { let mut dpns_polls_count = 0; - for VotePoll::ContestedDocumentResourceVotePoll(contested_poll) in polls { + for contested_poll in polls.iter().filter_map(|poll| match poll { + VotePoll::ContestedDocumentResourceVotePoll(contested_poll) => { + Some(contested_poll) + } + VotePoll::YesNoVotePoll(_) => None, + }) { if contested_poll.contract_id == dpns_contract_id && contested_poll.document_type_name == "domain" { diff --git a/packages/rs-sdk/src/platform/transition/vote.rs b/packages/rs-sdk/src/platform/transition/vote.rs index 709310913c0..9008975670f 100644 --- a/packages/rs-sdk/src/platform/transition/vote.rs +++ b/packages/rs-sdk/src/platform/transition/vote.rs @@ -11,7 +11,6 @@ use dpp::identity::IdentityPublicKey; use dpp::prelude::Identifier; use dpp::state_transition::masternode_vote_transition::methods::MasternodeVoteTransitionMethodsV0; use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; -use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; use dpp::voting::votes::Vote; use rs_dapi_client::{DapiRequest, IntoInner}; @@ -116,8 +115,7 @@ impl> PutVote for Vote { let settings = settings.unwrap_or_default(); - let Vote::ResourceVote(resource_vote) = self; - let vote_poll_id = resource_vote.vote_poll().unique_id()?; + let vote_poll_id = self.vote_poll_unique_id()?; let masternode_vote_transition = MasternodeVoteTransition::try_from_vote_with_signer( self.clone(), diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index dc4a35252a8..32ca33dd1e8 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -168,6 +168,7 @@ use dpp::consensus::state::voting::masternode_voted_too_many_times::MasternodeVo use dpp::consensus::state::voting::vote_poll_not_available_for_voting_error::VotePollNotAvailableForVotingError; use dpp::consensus::state::voting::vote_choice_not_allowed_for_vote_poll_error::VoteChoiceNotAllowedForVotePollError; use dpp::consensus::state::voting::vote_poll_not_found_error::VotePollNotFoundError; +use dpp::consensus::state::voting::yes_no_vote_poll_not_available_for_voting_error::YesNoVotePollNotAvailableForVotingError; use crate::errors::consensus::basic::data_contract::{ DataContractErrorWasm, DataContractHaveNewUniqueIndexErrorWasm, @@ -691,6 +692,9 @@ pub fn from_state_error(state_error: &StateError) -> JsValue { StateError::ReferencedIdentityKeyRequirementNotMetError(e) => { generic_consensus_error!(ReferencedIdentityKeyRequirementNotMetError, e).into() } + StateError::YesNoVotePollNotAvailableForVotingError(e) => { + generic_consensus_error!(YesNoVotePollNotAvailableForVotingError, e).into() + } } } diff --git a/packages/wasm-dpp/src/voting/state_transition/masternode_vote_transition/mod.rs b/packages/wasm-dpp/src/voting/state_transition/masternode_vote_transition/mod.rs index 6e171a6bdd4..2dab1cf7c21 100644 --- a/packages/wasm-dpp/src/voting/state_transition/masternode_vote_transition/mod.rs +++ b/packages/wasm-dpp/src/voting/state_transition/masternode_vote_transition/mod.rs @@ -302,8 +302,10 @@ impl MasternodeVoteTransitionWasm { Some(js_object) } + VotePoll::YesNoVotePoll(_) => None, } } + Vote::YesNoVote(_) => None, } } diff --git a/packages/wasm-dpp2/src/voting/vote.rs b/packages/wasm-dpp2/src/voting/vote.rs index 5b409af1444..f255e4f52ad 100644 --- a/packages/wasm-dpp2/src/voting/vote.rs +++ b/packages/wasm-dpp2/src/voting/vote.rs @@ -1,12 +1,18 @@ +use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_try_from_js_value; use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::voting::resource_vote_choice::ResourceVoteChoiceWasm; use crate::voting::vote_poll::VotePollWasm; +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; +use dpp::voting::vote_polls::VotePoll; use dpp::voting::votes::Vote; use dpp::voting::votes::resource_vote::ResourceVote; use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; +use dpp::voting::votes::yes_no_vote::YesNoVote; +use dpp::voting::votes::yes_no_vote::accessors::v0::YesNoVoteGettersV0; +use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen(typescript_custom_section)] @@ -80,37 +86,76 @@ impl VoteWasm { pub fn poll(&self) -> VotePollWasm { match &self.0 { Vote::ResourceVote(vote) => vote.vote_poll().clone().into(), + Vote::YesNoVote(vote) => VotePoll::YesNoVotePoll(vote.vote_poll().clone()).into(), } } + /// The resource vote choice of a resource vote. A yes/no vote has `yesNoChoice` instead. #[wasm_bindgen(getter = choice)] - pub fn choice(&self) -> ResourceVoteChoiceWasm { + pub fn choice(&self) -> WasmDppResult { match &self.0 { - Vote::ResourceVote(vote) => vote.resource_vote_choice().into(), + Vote::ResourceVote(vote) => Ok(vote.resource_vote_choice().into()), + Vote::YesNoVote(_) => Err(not_a_resource_vote()), + } + } + + /// The answer of a yes/no vote: `yes`, `no` or `abstain`. + #[wasm_bindgen(getter = "yesNoChoice")] + pub fn yes_no_choice(&self) -> WasmDppResult { + match &self.0 { + Vote::ResourceVote(_) => Err(WasmDppError::invalid_argument( + "this vote is a resource vote, not a yes/no vote".to_string(), + )), + Vote::YesNoVote(vote) => Ok(match vote.vote_choice() { + YesNoAbstainVoteChoice::Yes => "yes".to_string(), + YesNoAbstainVoteChoice::No => "no".to_string(), + YesNoAbstainVoteChoice::Abstain => "abstain".to_string(), + }), } } #[wasm_bindgen(setter = poll)] - pub fn set_poll(&mut self, poll: &VotePollWasm) { + pub fn set_poll(&mut self, poll: &VotePollWasm) -> WasmDppResult<()> { self.0 = match self.0.clone() { Vote::ResourceVote(vote) => Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { vote_poll: poll.clone().into(), resource_vote_choice: vote.resource_vote_choice(), })), - } + Vote::YesNoVote(vote) => { + let VotePoll::YesNoVotePoll(vote_poll) = poll.clone().into() else { + return Err(WasmDppError::invalid_argument( + "a yes/no vote answers a yes/no vote poll".to_string(), + )); + }; + Vote::YesNoVote(YesNoVote::V0(YesNoVoteV0 { + vote_poll, + vote_choice: vote.vote_choice(), + })) + } + }; + + Ok(()) } #[wasm_bindgen(setter = choice)] - pub fn set_choice(&mut self, choice: &ResourceVoteChoiceWasm) { + pub fn set_choice(&mut self, choice: &ResourceVoteChoiceWasm) -> WasmDppResult<()> { self.0 = match self.0.clone() { Vote::ResourceVote(vote) => Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { vote_poll: vote.vote_poll().clone(), resource_vote_choice: choice.clone().into(), })), - } + Vote::YesNoVote(_) => return Err(not_a_resource_vote()), + }; + + Ok(()) } } +/// The resource vote choice exists on a resource vote only. +fn not_a_resource_vote() -> WasmDppError { + WasmDppError::invalid_argument("this vote is a yes/no vote, not a resource vote".to_string()) +} + impl_try_from_js_value!(VoteWasm, "Vote"); impl_wasm_conversions_inner!(VoteWasm, Vote, Vote, VoteObjectJs, VoteJSONJs); impl_wasm_type_info!(VoteWasm, Vote); diff --git a/packages/wasm-dpp2/src/voting/vote_poll.rs b/packages/wasm-dpp2/src/voting/vote_poll.rs index 45a433fa059..f4ed9731a99 100644 --- a/packages/wasm-dpp2/src/voting/vote_poll.rs +++ b/packages/wasm-dpp2/src/voting/vote_poll.rs @@ -118,26 +118,42 @@ impl VotePollWasm { self.0.to_string() } + /// The kind of poll: `contestedDocumentResourceVotePoll` or `yesNoVotePoll`. + #[wasm_bindgen(getter = "kind")] + pub fn kind(&self) -> String { + match &self.0 { + VotePoll::ContestedDocumentResourceVotePoll(_) => { + "contestedDocumentResourceVotePoll".to_string() + } + VotePoll::YesNoVotePoll(_) => "yesNoVotePoll".to_string(), + } + } + #[wasm_bindgen(getter = "contractId")] - pub fn contract_id(&self) -> IdentifierWasm { + pub fn contract_id(&self) -> WasmDppResult { match &self.0 { VotePoll::ContestedDocumentResourceVotePoll(poll) => { - IdentifierWasm::from(poll.contract_id) + Ok(IdentifierWasm::from(poll.contract_id)) } + VotePoll::YesNoVotePoll(_) => Err(not_a_contested_poll()), } } #[wasm_bindgen(getter = "documentTypeName")] - pub fn document_type_name(&self) -> String { + pub fn document_type_name(&self) -> WasmDppResult { match &self.0 { - VotePoll::ContestedDocumentResourceVotePoll(poll) => poll.document_type_name.clone(), + VotePoll::ContestedDocumentResourceVotePoll(poll) => { + Ok(poll.document_type_name.clone()) + } + VotePoll::YesNoVotePoll(_) => Err(not_a_contested_poll()), } } #[wasm_bindgen(getter = "indexName")] - pub fn index_name(&self) -> String { + pub fn index_name(&self) -> WasmDppResult { match &self.0 { - VotePoll::ContestedDocumentResourceVotePoll(poll) => poll.index_name.clone(), + VotePoll::ContestedDocumentResourceVotePoll(poll) => Ok(poll.index_name.clone()), + VotePoll::YesNoVotePoll(_) => Err(not_a_contested_poll()), } } @@ -166,6 +182,7 @@ impl VotePollWasm { Ok(js_array) } + VotePoll::YesNoVotePoll(_) => Err(not_a_contested_poll()), } } @@ -182,6 +199,7 @@ impl VotePollWasm { VotePoll::ContestedDocumentResourceVotePoll(poll) } + VotePoll::YesNoVotePoll(_) => return Err(not_a_contested_poll()), }; Ok(()) @@ -191,25 +209,34 @@ impl VotePollWasm { pub fn set_document_type_name( &mut self, #[wasm_bindgen(js_name = "documentTypeName")] document_type_name: String, - ) { + ) -> WasmDppResult<()> { self.0 = match self.0.clone() { VotePoll::ContestedDocumentResourceVotePoll(mut poll) => { poll.document_type_name = document_type_name; VotePoll::ContestedDocumentResourceVotePoll(poll) } - } + VotePoll::YesNoVotePoll(_) => return Err(not_a_contested_poll()), + }; + + Ok(()) } #[wasm_bindgen(setter = "indexName")] - pub fn set_index_name(&mut self, #[wasm_bindgen(js_name = "indexName")] index_name: String) { + pub fn set_index_name( + &mut self, + #[wasm_bindgen(js_name = "indexName")] index_name: String, + ) -> WasmDppResult<()> { self.0 = match self.0.clone() { VotePoll::ContestedDocumentResourceVotePoll(mut poll) => { poll.index_name = index_name; VotePoll::ContestedDocumentResourceVotePoll(poll) } + VotePoll::YesNoVotePoll(_) => return Err(not_a_contested_poll()), }; + + Ok(()) } #[wasm_bindgen(setter = "indexValues")] @@ -229,12 +256,21 @@ impl VotePollWasm { VotePoll::ContestedDocumentResourceVotePoll(poll) } + VotePoll::YesNoVotePoll(_) => return Err(not_a_contested_poll()), }; Ok(()) } } +/// The contested index fields exist on a contested document resource poll only. +fn not_a_contested_poll() -> WasmDppError { + WasmDppError::invalid_argument( + "this vote poll is a yes/no vote poll, not a contested document resource vote poll" + .to_string(), + ) +} + impl_try_from_js_value!(VotePollWasm, "VotePoll"); impl_try_from_options!(VotePollWasm); impl_wasm_conversions_inner!( From d6c4092d06f2fc4f22ec932859d41fdbcc7e5ddc Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 23 Sep 2026 09:07:06 +0700 Subject: [PATCH 02/18] fix(dpp): a resource vote naming a yes/no poll is inactive before protocol 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 --- packages/rs-dpp/src/state_transition/mod.rs | 94 +++++++++------ .../v0/mod.rs | 5 +- .../check_for_ended_vote_polls/v0/mod.rs | 10 ++ .../clean_up_after_vote_polls_end/v0/mod.rs | 3 + .../masternode_vote/state/v0/mod.rs | 4 + .../transform_into_action/v0/mod.rs | 4 + .../masternode_vote/yes_no_tests.rs | 113 ++++++++++++++++++ .../insert/register_identity_vote/v0/mod.rs | 8 ++ .../prove/prove_state_transition/v0/mod.rs | 3 + .../v0/mod.rs | 3 + 10 files changed, 211 insertions(+), 36 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 2539d453a0e..73f290fdd13 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -182,6 +182,8 @@ use crate::state_transition::state_transitions::document::batch_transition::meth use crate::state_transition::unshield_transition::{ UnshieldTransition, UnshieldTransitionSignable, }; +use crate::voting::vote_polls::VotePoll; +use crate::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; use crate::voting::votes::Vote; use state_transitions::document::batch_transition::batched_transition::token_transition::TokenTransition; pub use state_transitions::*; @@ -1049,11 +1051,15 @@ impl StateTransition { StateTransition::IdentityTopUp(_) | StateTransition::IdentityCreditWithdrawal(_) | StateTransition::IdentityCreditTransfer(_) => ALL_VERSIONS, - // A vote on a yes/no poll exists from protocol version 14. Binaries from before it - // cannot decode one, so an earlier version rejects it without charging, exactly as - // they do. + // A vote that names a yes/no poll, as a yes/no vote or as a resource vote, exists + // from protocol version 14. Binaries from before it cannot decode one, so an earlier + // version rejects it without charging, exactly as they do, and no masternode vote + // validator selected before 14 ever sees a yes/no poll. StateTransition::MasternodeVote(masternode_vote) => match masternode_vote.vote() { - Vote::ResourceVote(_) => ALL_VERSIONS, + Vote::ResourceVote(resource_vote) => match resource_vote.vote_poll() { + VotePoll::ContestedDocumentResourceVotePoll(_) => ALL_VERSIONS, + VotePoll::YesNoVotePoll(_) => 14..=LATEST_VERSION, + }, Vote::YesNoVote(_) => 14..=LATEST_VERSION, }, StateTransition::IdentityCreditTransferToAddresses(_) @@ -2551,49 +2557,67 @@ mod tests { assert_eq!(sample_withdrawal_st().active_version_range(), ALL_VERSIONS); } + /// Both vote kinds can name a yes/no poll: a yes/no vote, and a resource vote (which + /// protocol version 14 refuses as naming no contested poll). Before 14 neither decodes, so + /// the masternode vote validators selected there never meet a yes/no poll. #[test] fn test_active_version_range_masternode_vote_on_a_yes_no_poll_starts_at_14() { use crate::serialization::PlatformSerializable; + use crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use crate::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use crate::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; + use crate::voting::votes::resource_vote::v0::ResourceVoteV0; + use crate::voting::votes::resource_vote::ResourceVote; use crate::voting::votes::yes_no_vote::v0::YesNoVoteV0; use crate::voting::votes::yes_no_vote::YesNoVote; - let v0 = MasternodeVoteTransitionV0 { - pro_tx_hash: Identifier::from([3u8; 32]), - voter_identity_id: Identifier::from([4u8; 32]), - vote: Vote::YesNoVote(YesNoVote::V0(YesNoVoteV0 { - vote_poll: YesNoVotePoll { - resource_path: vec![BinaryData::new(vec![7u8; 32])], - supermajority_numerator: 2, - supermajority_denominator: 3, - minimum_voting_power: 400, - }, + let vote_poll = YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![7u8; 32])], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: 400, + }; + let votes = [ + Vote::YesNoVote(YesNoVote::V0(YesNoVoteV0 { + vote_poll: vote_poll.clone(), vote_choice: YesNoAbstainVoteChoice::Yes, })), - nonce: 2, - signature_public_key_id: 5, - signature: BinaryData::new(vec![9u8; 10]), - }; - let state_transition = StateTransition::MasternodeVote(MasternodeVoteTransition::V0(v0)); - assert_eq!(state_transition.active_version_range(), 14..=LATEST_VERSION); - - let bytes = state_transition - .serialize_to_bytes() - .expect("expected to serialize the vote"); + Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + vote_poll: VotePoll::YesNoVotePoll(vote_poll), + resource_vote_choice: ResourceVoteChoice::Abstain, + })), + ]; let version_13 = PlatformVersion::get(13).expect("protocol version 13"); - assert!(matches!( - StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_13), - Err(ProtocolError::StateTransitionError( - StateTransitionIsNotActiveError { .. } - )) - )); let version_14 = PlatformVersion::get(14).expect("protocol version 14"); - assert_eq!( - StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_14) - .expect("expected the vote to be active at 14"), - state_transition - ); + + for vote in votes { + let v0 = MasternodeVoteTransitionV0 { + pro_tx_hash: Identifier::from([3u8; 32]), + voter_identity_id: Identifier::from([4u8; 32]), + vote, + nonce: 2, + signature_public_key_id: 5, + signature: BinaryData::new(vec![9u8; 10]), + }; + let state_transition = + StateTransition::MasternodeVote(MasternodeVoteTransition::V0(v0)); + assert_eq!(state_transition.active_version_range(), 14..=LATEST_VERSION); + + let bytes = state_transition + .serialize_to_bytes() + .expect("expected to serialize the vote"); + assert!(matches!( + StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_13), + Err(ProtocolError::StateTransitionError( + StateTransitionIsNotActiveError { .. } + )) + )); + assert_eq!( + StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_14) + .expect("expected the vote to be active at 14"), + state_transition + ); + } } #[test] diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index c80b2b532a2..d7c95c4add7 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -313,6 +313,8 @@ impl Platform { VotePoll::ContestedDocumentResourceVotePoll(contested) => { contested.specialized_balance_id().map_err(Error::Protocol) } + // Unreachable: this runs once, on the block entering protocol version 8, + // and yes/no polls exist from 14. The arm only keeps the match exhaustive. VotePoll::YesNoVotePoll(yes_no) => { yes_no.specialized_balance_id().map_err(Error::Protocol) } @@ -817,7 +819,8 @@ impl Platform { // Vote decisions trees: the active yes/no polls and the identity votes index of the // decisions branch. Fresh chains create them at genesis - // (`add_initial_vote_tree_main_structure_operations` v1). + // (`add_initial_vote_tree_main_structure_operations` v1). Like everything in this + // function it runs only on the block entering protocol version 14. self.drive .insert_vote_decisions_trees(Some(transaction), platform_version)?; diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs index 378d44cc6b0..b7c82a4267e 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs @@ -176,6 +176,11 @@ where identifiers_voting_for_contenders, )) } + // Unreachable: this v0 is selected up to protocol version 13, and a + // yes/no poll enters the end date index only through + // `Drive::open_yes_no_vote_poll`, which writes under the `Votes/d/p` + // tree that exists from 14 (genesis vote setup v1, + // `transition_to_version_14`). ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Execution( ExecutionError::CorruptedCodeExecution( "yes/no vote polls need check_for_ended_vote_polls version 1", @@ -306,6 +311,11 @@ where )?; Ok(ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(resolved_contested_document_resource_vote_poll, identifiers_voting_for_contenders)) } + // Unreachable: this v0 is selected up to protocol version 13, and a + // yes/no poll enters the end date index only through + // `Drive::open_yes_no_vote_poll`, which writes under the `Votes/d/p` + // tree that exists from 14 (genesis vote setup v1, + // `transition_to_version_14`). ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Execution( ExecutionError::CorruptedCodeExecution( "yes/no vote polls need check_for_ended_vote_polls version 1", diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs index 1d7f911e996..2ced3c3c320 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs @@ -42,6 +42,9 @@ where ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(contested_poll, vote_info) => { contested_polls.push((contested_poll, end_date, vote_info)); } + // Unreachable: only `check_for_ended_vote_polls` v1 builds this variant, + // and every table that selects this v0 (protocol versions 1 to 13) selects + // check v0. ResolvedVotePollWithVotes::YesNoVotePollWithVotes(..) => { return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( "yes/no vote polls need clean_up_after_vote_polls_end version 1", diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs index 6aed88f4d06..129c7bc64b6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs @@ -92,6 +92,9 @@ impl MasternodeVoteStateTransitionStateValidationV0 for MasternodeVoteTransition } } } + // Unreachable: this v0 is selected up to protocol version 13, where a + // masternode vote that names a yes/no poll does not decode + // (`StateTransition::active_version_range`). ResolvedVotePoll::YesNoVotePoll(_) => { Err(Error::Execution(ExecutionError::CorruptedCodeExecution( "yes/no vote polls need validate_state version 1", @@ -99,6 +102,7 @@ impl MasternodeVoteStateTransitionStateValidationV0 for MasternodeVoteTransition } } } + // Unreachable for the same reason: no yes/no vote decodes before 14. ResolvedVote::YesNoVote(_) => { Err(Error::Execution(ExecutionError::CorruptedCodeExecution( "yes/no votes need validate_state version 1", diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs index 7ac0b241c20..07a483fcf42 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs @@ -104,6 +104,9 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV0 for Masternode } } } + // Unreachable: this v0 is selected up to protocol version 13, where a + // masternode vote that names a yes/no poll does not decode + // (`StateTransition::active_version_range`). VotePoll::YesNoVotePoll(_) => { return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( "yes/no vote polls need transform_into_action version 1", @@ -111,6 +114,7 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV0 for Masternode } } } + // Unreachable for the same reason: no yes/no vote decodes before 14. Vote::YesNoVote(_) => { return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( "yes/no votes need transform_into_action version 1", diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs index e6beb24dbfc..78a52d27322 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs @@ -1067,3 +1067,116 @@ async fn should_close_a_contested_poll_and_a_yes_no_poll_that_end_at_the_same_ti .passed ); } + +/// Protocol version 13 selects the shipped (v0) masternode vote transform and state +/// validation, vote registration, end of poll check and clean-up, which this change gave +/// yes/no arms. A contest there runs as it did before: the vote counts, the poll ends, the +/// contender is awarded and the end date index empties. A masternode vote naming a yes/no +/// poll, of either kind, does not decode there, so it is stripped before any of them runs and +/// consumes nothing. +#[tokio::test] +async fn should_run_a_contest_unchanged_and_strip_yes_no_polls_at_protocol_version_13() { + use crate::execution::validation::state_transition::state_transitions::tests::{ + create_dpns_identity_name_contest, dpns_name_vote_poll, get_vote_states, + }; + use dapi_grpc::platform::v0::get_contested_resource_vote_state_request::get_contested_resource_vote_state_request_v0::ResultType; + use dapi_grpc::platform::v0::get_contested_resource_vote_state_response::get_contested_resource_vote_state_response_v0::finished_vote_info::FinishedVoteOutcome; + use dpp::identity::accessors::IdentityGettersV0; + + let platform_version = PlatformVersion::get(13).expect("protocol version 13"); + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(13) + .build_with_mock_rpc() + .set_genesis_state(); + let platform_state = platform.state.load(); + let (contender_1, _contender_2, dpns_contract) = create_dpns_identity_name_contest( + &mut platform, + &platform_state, + 7, + "quantum", + platform_version, + ) + .await; + let (pro_tx_hash, signer, voting_key) = + setup_voter(&mut platform, 100, false, platform_version); + + let yes_no_poll = two_thirds_poll(b"before 14", 1); + let votes_naming_a_yes_no_poll = [ + Vote::YesNoVote(YesNoVote::V0(YesNoVoteV0 { + vote_poll: yes_no_poll.clone(), + vote_choice: YesNoAbstainVoteChoice::Yes, + })), + Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + vote_poll: VotePoll::YesNoVotePoll(yes_no_poll), + resource_vote_choice: ResourceVoteChoice::Abstain, + })), + ]; + for vote in votes_naming_a_yes_no_poll { + let result = perform_vote_transition( + &mut platform, + vote, + &signer, + pro_tx_hash, + &voting_key, + 1, + platform_version, + ) + .await + .expect_err("expected the vote to be stripped"); + assert_matches!(result, InternalError(message) if message.contains("is not active")); + } + + // Nonce 1 is still free: the stripped votes consumed nothing. + let contested_vote = Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + vote_poll: VotePoll::ContestedDocumentResourceVotePoll(dpns_name_vote_poll( + &dpns_contract, + "quantum", + )), + resource_vote_choice: ResourceVoteChoice::TowardsIdentity(contender_1.id()), + })); + perform_vote_transition( + &mut platform, + contested_vote, + &signer, + pro_tx_hash, + &voting_key, + 1, + platform_version, + ) + .await + .expect("expected the contested vote to be accepted"); + + close_ended_polls(&mut platform, platform_version); + + let platform_state = platform.state.load(); + let (_contenders, _abstaining, _locking, finished_vote_info) = get_vote_states( + &platform, + &platform_state, + &dpns_contract, + "quantum", + None, + true, + None, + ResultType::DocumentsAndVoteTally, + platform_version, + ); + let finished_vote_info = finished_vote_info.expect("expected the contest to be finished"); + assert_eq!( + finished_vote_info.finished_vote_outcome, + FinishedVoteOutcome::TowardsIdentity as i32 + ); + assert_eq!( + finished_vote_info.won_by_identity_id, + Some(contender_1.id().to_vec()) + ); + let end_dates = VotePollsByEndDateDriveQuery { + start_time: None, + end_time: None, + limit: None, + offset: None, + order_ascending: true, + } + .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) + .expect("end dates"); + assert!(end_dates.is_empty()); +} diff --git a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v0/mod.rs index 3ef1536caa9..659998b55f1 100644 --- a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v0/mod.rs @@ -40,11 +40,15 @@ impl Drive { transaction, platform_version, ), + // Unreachable: this v0 is selected up to protocol version 13, its vote + // comes from a masternode vote action, and a masternode vote that names a + // yes/no poll does not decode there (`StateTransition::active_version_range`). ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Drive( DriveError::CorruptedCodeExecution("yes/no vote polls need version 1"), )), } } + // Unreachable for the same reason: no yes/no vote decodes before 14. ResolvedVote::YesNoVote(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution( "yes/no votes need version 1", ))), @@ -75,11 +79,15 @@ impl Drive { transaction, platform_version, ), + // Unreachable: this v0 is selected up to protocol version 13, its vote + // comes from a masternode vote action, and a masternode vote that names a + // yes/no poll does not decode there (`StateTransition::active_version_range`). ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Drive( DriveError::CorruptedCodeExecution("yes/no vote polls need version 1"), )), } } + // Unreachable for the same reason: no yes/no vote decodes before 14. ResolvedVote::YesNoVote(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution( "yes/no votes need version 1", ))), diff --git a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs index 7ac15f98db1..3ad8bf6a908 100644 --- a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs +++ b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs @@ -432,6 +432,9 @@ impl Drive { StateTransition::MasternodeVote(st) => { let pro_tx_hash = st.pro_tx_hash(); + // Shared by versions 0 and 1, and off the block execution path (it answers + // proof queries). A resource vote on a contested poll gets the same path query + // as before; a vote naming a yes/no poll decodes only from protocol version 14. let vote_poll = match st.vote() { Vote::ResourceVote(resource_vote) => resource_vote.vote_poll().clone(), Vote::YesNoVote(yes_no_vote) => { diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 393014acf67..63b562d7812 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -1538,6 +1538,9 @@ impl Drive { StateTransition::MasternodeVote(masternode_vote) => { let pro_tx_hash = masternode_vote.pro_tx_hash(); let vote = masternode_vote.vote(); + // Shared by versions 0 and 1, and client side only. A resource vote on a + // contested poll verifies exactly as before; a vote naming a yes/no poll exists + // only from protocol version 14. let (root_hash, vote) = match vote { Vote::ResourceVote(resource_vote) => { let contract = match resource_vote.vote_poll() { From d7f783eb7b3507b0ca9abddd3f430a693e340355 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 23 Sep 2026 18:44:17 +0700 Subject: [PATCH 03/18] fix(platform): yes/no poll review fixes: end-date clean-up, proof routing, 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 --- .../voting/vote_polls/yes_no_vote_poll/mod.rs | 46 +++- .../v0/mod.rs | 6 +- .../check_for_ended_vote_polls/v0/mod.rs | 4 +- .../transform_into_action/v1/mod.rs | 4 +- .../masternode_vote/yes_no_tests.rs | 210 ++++++++++++------ .../src/drive/initialization/v4/mod.rs | 7 + .../v0/mod.rs | 45 ++-- .../v0/mod.rs | 20 +- .../v0/mod.rs | 6 +- .../setup/insert_vote_decisions_trees/mod.rs | 6 +- .../mod.rs | 4 +- .../v1/mod.rs | 41 ---- .../rs-drive/src/verify/bounded_decode.rs | 15 ++ .../voting/verify_masternode_vote/mod.rs | 26 +++ .../verify_masternode_yes_no_vote/mod.rs | 100 ++++++++- .../verify_masternode_yes_no_vote/v0/mod.rs | 29 +-- .../drive_vote_method_versions/v3.rs | 8 +- .../src/version/drive_versions/v9.rs | 2 +- .../dpns_usernames/contested_queries.rs | 33 ++- .../rs-sdk/src/platform/transition/vote.rs | 23 +- .../wasm-dpp2/src/voting/resource_vote.rs | 19 +- packages/wasm-dpp2/src/voting/vote.rs | 68 ++++-- packages/wasm-dpp2/src/voting/vote_poll.rs | 58 +++-- .../src/state_transitions/identity.rs | 7 + 24 files changed, 540 insertions(+), 247 deletions(-) delete mode 100644 packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/v1/mod.rs diff --git a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs index 0a0dd757c7b..582b0106dcf 100644 --- a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs @@ -22,6 +22,12 @@ use std::fmt; /// masternode list fits a `u32`. pub type VotingPower = u32; +/// Prefixed to the serialized poll before its id is hashed. A contested poll's id hashes its +/// bare encoding, which starts with a 32-byte contract id; matching this 33-byte tag would take +/// a contract id equal to its first 32 bytes, so the two kinds never share an id (and with it a +/// prefunded balance and an end date index entry). +const YES_NO_VOTE_POLL_ID_DOMAIN: &[u8] = b"dash platform yes/no vote poll id"; + /// A poll the masternodes answer with yes, no or abstain. /// /// The poll is keyed by a resource path the feature that opens it chooses (a contract id and a @@ -95,9 +101,9 @@ impl fmt::Display for YesNoVotePoll { impl YesNoVotePoll { /// Refuses parameters no poll can be opened with: an empty or oversized resource path (the - /// poll rides in every vote, whose wire size is bounded), a zero denominator, or a - /// numerator that is not below the denominator (yes could never exceed the whole of yes - /// plus no). + /// poll rides in every vote, whose wire size is bounded), a zero denominator, a zero + /// numerator (any single yes would pass against every no), or a numerator that is not below + /// the denominator (yes could never exceed the whole of yes plus no). pub fn validate_parameters( &self, platform_version: &PlatformVersion, @@ -132,6 +138,11 @@ impl YesNoVotePoll { "a yes/no vote poll supermajority denominator must not be zero".to_string(), )); } + if self.supermajority_numerator == 0 { + return Err(ProtocolError::VoteError( + "a yes/no vote poll supermajority numerator must not be zero".to_string(), + )); + } if self.supermajority_numerator >= self.supermajority_denominator { return Err(ProtocolError::VoteError(format!( "a yes/no vote poll supermajority numerator ({}) must be below its denominator ({})", @@ -151,10 +162,12 @@ impl YesNoVotePoll { yes * self.supermajority_denominator as u64 > cast * self.supermajority_numerator as u64 } - /// The double SHA-256 of the serialized `VotePoll` carrying this poll. + /// The double SHA-256 of the yes/no poll id domain tag followed by the serialized + /// `VotePoll` carrying this poll. pub fn sha256_2_hash(&self) -> Result<[u8; 32], ProtocolError> { - let encoded = VotePoll::YesNoVotePoll(self.clone()).serialize_to_bytes()?; - Ok(hash_double(encoded)) + let mut preimage = YES_NO_VOTE_POLL_ID_DOMAIN.to_vec(); + preimage.extend(VotePoll::YesNoVotePoll(self.clone()).serialize_to_bytes()?); + Ok(hash_double(preimage)) } /// The prefunded balance the votes on this poll are paid from: the poll's unique id. @@ -240,6 +253,13 @@ mod tests { } .validate_parameters(platform_version) .is_err()); + // A zero numerator would let one yes pass against any amount of no. + assert!(YesNoVotePoll { + supermajority_numerator: 0, + ..poll() + } + .validate_parameters(platform_version) + .is_err()); // A simple majority is a valid rule. assert!(YesNoVotePoll { supermajority_numerator: 1, @@ -311,6 +331,20 @@ mod tests { } assert_eq!(poll().specialized_balance_id().expect("balance id"), base); } + + #[test] + fn should_hash_the_id_under_the_yes_no_domain_tag() { + let encoded = VotePoll::YesNoVotePoll(poll()) + .serialize_to_bytes() + .expect("encoded poll"); + let untagged = Identifier::new(hash_double(&encoded)); + let tagged = Identifier::new(hash_double( + [YES_NO_VOTE_POLL_ID_DOMAIN, encoded.as_slice()].concat(), + )); + let id = poll().unique_id().expect("id"); + assert_eq!(id, tagged); + assert_ne!(id, untagged); + } } #[cfg(all( diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index d7c95c4add7..7d3a7d468f6 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -818,9 +818,9 @@ impl Platform { .insert_contract_fee_pot_trees(Some(transaction), platform_version)?; // Vote decisions trees: the active yes/no polls and the identity votes index of the - // decisions branch. Fresh chains create them at genesis - // (`add_initial_vote_tree_main_structure_operations` v1). Like everything in this - // function it runs only on the block entering protocol version 14. + // decisions branch. Fresh chains call the same helper from + // `create_initial_state_structure` v4, after the fee pot trees as here. Like everything + // in this function it runs only on the block entering protocol version 14. self.drive .insert_vote_decisions_trees(Some(transaction), platform_version)?; diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs index b7c82a4267e..44966c7c53b 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs @@ -179,7 +179,7 @@ where // Unreachable: this v0 is selected up to protocol version 13, and a // yes/no poll enters the end date index only through // `Drive::open_yes_no_vote_poll`, which writes under the `Votes/d/p` - // tree that exists from 14 (genesis vote setup v1, + // tree that exists from 14 (genesis structure v4, // `transition_to_version_14`). ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Execution( ExecutionError::CorruptedCodeExecution( @@ -314,7 +314,7 @@ where // Unreachable: this v0 is selected up to protocol version 13, and a // yes/no poll enters the end date index only through // `Drive::open_yes_no_vote_poll`, which writes under the `Votes/d/p` - // tree that exists from 14 (genesis vote setup v1, + // tree that exists from 14 (genesis structure v4, // `transition_to_version_14`). ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Execution( ExecutionError::CorruptedCodeExecution( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs index 1c3830c49da..4f5105423f2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs @@ -39,8 +39,8 @@ pub(in crate::execution::validation::state_transition::state_transitions::master impl MasternodeVoteStateTransitionTransformIntoActionValidationV1 for MasternodeVoteTransition { /// Version 1 (protocol version 14) also transforms a vote on a yes/no poll. The same rules - /// hold for both kinds: the same answer cannot be given twice, and a masternode may change - /// its answer only `votes_allowed_per_masternode` times. + /// hold for both kinds: the same answer cannot be given twice, and a masternode may vote on a + /// poll at most `votes_allowed_per_masternode` times, its first vote included. fn transform_into_action_v1( &self, platform: &PlatformRef, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs index 78a52d27322..23cbbcdc9bd 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs @@ -1,18 +1,22 @@ //! Votes on yes/no vote polls: the poll's life from opening to the record of its decision. +use crate::execution::check_tx::CheckTxLevel; use crate::execution::validation::state_transition::state_transitions::tests::{ - setup_masternode_voting_identity, take_down_masternode_identities, + process_test_state_transition, setup_masternode_voting_identity, + take_down_masternode_identities, }; +use crate::platform_types::platform::PlatformRef; use crate::platform_types::platform_state::PlatformStateV0Methods; use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult::{ InternalError, SuccessfulExecution, UnpaidConsensusError, }; use crate::rpc::core::MockCoreRPCLike; +use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; +use crate::test::helpers::state_mutation_guard::assert_check_tx_valid_at_all_levels; use assert_matches::assert_matches; use dpp::block::block_info::BlockInfo; -use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0; use dpp::consensus::state::state_error::StateError; use dpp::consensus::ConsensusError; use dpp::dashcore::hashes::Hash; @@ -148,7 +152,35 @@ async fn perform_yes_no_vote( .await } +/// Whether CheckTx admits the serialized transition on its first check. A transition CheckTx +/// cannot take at all (one not active at the current protocol version fails to decode) is not +/// admitted either. +fn check_tx_admits(platform: &TempPlatform, serialized_transition: &[u8]) -> bool { + let platform_state = platform.state.load(); + let platform_version = platform_state + .current_platform_version() + .expect("expected the current platform version"); + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &platform_state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + platform + .check_tx( + serialized_transition, + CheckTxLevel::FirstTimeCheck, + &platform_ref, + platform_version, + ) + .is_ok_and(|result| result.is_valid()) +} + /// Processes one masternode vote transition in a block and reports how it went. +/// +/// Every vote first goes through CheckTx, which asserts under cfg(test) that it leaves committed +/// state untouched (the devnet paloma height 788 class); a vote CheckTx admits must then pass at +/// both levels, as the shared masternode vote helper requires of its valid votes. async fn perform_vote_transition( platform: &mut TempPlatform, vote: Vote, @@ -172,26 +204,16 @@ async fn perform_vote_transition( let serialized_transition = masternode_vote_transition .serialize_to_bytes() .expect("expected to serialize the vote"); + if check_tx_admits(platform, &serialized_transition) { + assert_check_tx_valid_at_all_levels(platform, &serialized_transition, "yes/no vote"); + } let platform_state = platform.state.load(); - let transaction = platform.drive.grove.start_transaction(); - let processing_result = platform - .platform - .process_raw_state_transitions( - &[serialized_transition], - &platform_state, - &BlockInfo::default(), - &transaction, - platform_version, - false, - None, - ) - .expect("expected to process state transition"); - platform - .drive - .grove - .commit_transaction(transaction) - .unwrap() - .expect("expected to commit transaction"); + let processing_result = process_test_state_transition( + platform, + masternode_vote_transition, + &platform_state, + platform_version, + ); match processing_result.into_execution_results().remove(0) { SuccessfulExecution { .. } => Ok(()), other => Err(other), @@ -242,31 +264,13 @@ fn close_ended_polls_at( time_ms: u64, platform_version: &PlatformVersion, ) { - let mut platform_state = platform.state.load().clone().deref().clone(); + fast_forward_to_block(platform, time_ms, 10000, 42, 0, false); let block_info = BlockInfo { time_ms, height: 10000, core_height: 42, epoch: Default::default(), }; - platform_state.set_last_committed_block_info(Some( - ExtendedBlockInfoV0 { - basic_info: block_info, - app_hash: platform - .drive - .grove - .root_hash(None, &platform_version.drive.grove_version) - .unwrap() - .unwrap(), - quorum_hash: [0u8; 32], - block_id_hash: [0u8; 32], - proposer_pro_tx_hash: [0u8; 32], - signature: [0u8; 96], - round: 0, - } - .into(), - )); - platform.state.store(Arc::new(platform_state)); let platform_state = platform.state.load(); let transaction = platform.drive.grove.start_transaction(); platform @@ -634,18 +638,35 @@ async fn should_leave_only_the_record_behind_after_the_poll_ends() { .await .expect_err("expected the vote to be refused"); // Closing the poll emptied its fund, and the fund pre-check runs before state validation, - // so the missing fund is what refuses the vote; the finished status refuses it too. - assert!( - matches!( - &error, - UnpaidConsensusError(ConsensusError::StateError( - StateError::PrefundedSpecializedBalanceNotFoundError(_) - )) | UnpaidConsensusError(ConsensusError::StateError( - StateError::YesNoVotePollNotAvailableForVotingError(_) - )) - ), - "unexpected result {:?}", - error + // so the missing fund is what refuses the vote. + assert_matches!( + &error, + UnpaidConsensusError(ConsensusError::StateError( + StateError::PrefundedSpecializedBalanceNotFoundError(_) + )) + ); + // With a fund at its id again, the finished status refuses the vote. + platform + .drive + .add_prefunded_specialized_balance(vote_poll_id, FUND, None, platform_version) + .expect("expected to fund the finished poll"); + let error = perform_yes_no_vote( + &mut platform, + &vote_poll, + YesNoAbstainVoteChoice::Yes, + &signer, + pro_tx_hash, + &voting_key, + 1, + platform_version, + ) + .await + .expect_err("expected the vote to be refused"); + assert_matches!( + &error, + UnpaidConsensusError(ConsensusError::StateError( + StateError::YesNoVotePollNotAvailableForVotingError(_) + )) ); assert!(platform .drive @@ -848,10 +869,8 @@ async fn should_refuse_a_vote_when_the_poll_has_no_fund() { open_poll(&platform, &vote_poll, 0, platform_version); let (pro_tx_hash, signer, voting_key) = setup_voter(&mut platform, 100, false, platform_version); - // Block processing does not act on the prefunded balance pre-check today (its result is - // not read in the processor, for either poll kind), so the vote fails when its cost is - // deducted; #4904 turns that into an unpaid consensus error. Either way nothing of the - // vote is recorded. + // The prefunded balance pre-check runs before the vote is transformed, so the missing fund + // refuses it unpaid and nothing of the vote is recorded. let error = perform_yes_no_vote( &mut platform, &vote_poll, @@ -864,16 +883,11 @@ async fn should_refuse_a_vote_when_the_poll_has_no_fund() { ) .await .expect_err("expected the vote to be refused"); - assert!( - matches!(&error, InternalError(message) if message.contains("prefunded specialized balance")) - || matches!( - &error, - UnpaidConsensusError(ConsensusError::StateError( - StateError::PrefundedSpecializedBalanceNotFoundError(_) - )) - ), - "unexpected result {:?}", - error + assert_matches!( + &error, + UnpaidConsensusError(ConsensusError::StateError( + StateError::PrefundedSpecializedBalanceNotFoundError(_) + )) ); assert_eq!( platform @@ -1068,6 +1082,70 @@ async fn should_close_a_contested_poll_and_a_yes_no_poll_that_end_at_the_same_ti ); } +/// The block closes at most `maximum_vote_polls_to_process` polls, counted across every due end +/// date. When that cuts the last end date short, its tree must stay for the polls still under it: +/// deleting it in the same batch as the fetched poll's entry fails the whole block. +#[tokio::test] +async fn should_keep_an_end_date_the_block_only_partly_closed() { + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + let earlier = two_thirds_poll(b"earlier end date", 1); + let later_polls = [ + two_thirds_poll(b"later end date one", 1), + two_thirds_poll(b"later end date two", 1), + ]; + open_poll_ending_at(&platform, &earlier, END_DATE, FUND, platform_version); + for vote_poll in &later_polls { + open_poll_ending_at( + &platform, + vote_poll, + END_DATE + 1_000, + FUND, + platform_version, + ); + } + let end_dates = |platform: &TempPlatform| { + VotePollsByEndDateDriveQuery { + start_time: None, + end_time: None, + limit: None, + offset: None, + order_ascending: true, + } + .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) + .expect("end dates") + }; + let finished = |platform: &TempPlatform, vote_poll: &YesNoVotePoll| { + poll_state(platform, vote_poll, platform_version) + .stored_info + .expect("stored info") + .result() + .is_some() + }; + + // One block past both end dates closes the earlier poll and one of the later two. + close_ended_polls_at(&mut platform, END_DATE + 300_000, platform_version); + assert!(finished(&platform, &earlier)); + let finished_later = later_polls + .iter() + .filter(|vote_poll| finished(&platform, vote_poll)) + .count(); + assert_eq!(finished_later, 1); + let remaining = end_dates(&platform); + assert_eq!(remaining.len(), 1); + assert_eq!( + remaining.get(&(END_DATE + 1_000)).map(|polls| polls.len()), + Some(1) + ); + + // The next block closes the last one and the end date goes with it. + close_ended_polls_at(&mut platform, END_DATE + 301_000, platform_version); + assert!(later_polls + .iter() + .all(|vote_poll| finished(&platform, vote_poll))); + assert!(end_dates(&platform).is_empty()); +} + /// Protocol version 13 selects the shipped (v0) masternode vote transform and state /// validation, vote registration, end of poll check and clean-up, which this change gave /// yes/no arms. A contest there runs as it did before: the vote counts, the poll ends, the diff --git a/packages/rs-drive/src/drive/initialization/v4/mod.rs b/packages/rs-drive/src/drive/initialization/v4/mod.rs index c89df4de467..00eb17e945d 100644 --- a/packages/rs-drive/src/drive/initialization/v4/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v4/mod.rs @@ -104,6 +104,13 @@ impl Drive { // sequence of inserts on both node populations. self.insert_contract_fee_pot_trees(transaction, platform_version)?; + // Vote decisions trees (protocol version 14): the active yes/no polls and the identity + // votes index under the votes tree's decisions branch, which the batch above creates. + // The upgrade path (`Platform::transition_to_version_14`) calls the same helper after + // the fee pot trees, so both node populations build the decisions Merk from the same + // sequence of inserts. + self.insert_vote_decisions_trees(transaction, platform_version)?; + Ok(()) } } diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs index b71398773f2..5cec0e5a75e 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs @@ -3,6 +3,7 @@ use crate::drive::votes::paths::{ vote_end_date_queries_tree_path_vec, }; use crate::drive::Drive; +use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::query::VotePollsByEndDateDriveQuery; @@ -52,29 +53,27 @@ impl Drive { )?; } - // The block closes at most `maximum_vote_polls_to_process` polls. Fewer than that - // at one end date means every poll ending then was fetched, so the tree empties; - // otherwise look whether more are queued behind the ones processed. - let maximum_vote_polls_to_process = platform_version - .drive_abci - .validation_and_processing - .event_constants - .maximum_vote_polls_to_process; - let should_delete_parent_time_tree = if count < maximum_vote_polls_to_process as usize { - true - } else { - let total_count = - VotePollsByEndDateDriveQuery::execute_no_proof_for_specialized_end_time_query_only_check_end_time( - end_date, - maximum_vote_polls_to_process + 1, - self, - transaction, - &mut vec![], - platform_version, - )? - .len(); - total_count <= count - }; + // The tree goes only once nothing of either kind is left under it. The block's + // fetch shares one limit across every due end date, so the last end date it reached + // can be cut short: count what the tree holds instead of inferring it from how many + // polls were processed at that date. + let limit = u16::try_from(count) + .ok() + .and_then(|count| count.checked_add(1)) + .ok_or(Error::Drive(DriveError::CorruptedCodeExecution( + "more polls closed at one end date than an end date query can count", + )))?; + let total_count = + VotePollsByEndDateDriveQuery::execute_no_proof_for_specialized_end_time_query_only_check_end_time( + end_date, + limit, + self, + transaction, + &mut vec![], + platform_version, + )? + .len(); + let should_delete_parent_time_tree = total_count <= count; if should_delete_parent_time_tree { self.batch_delete( diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs index fe1e0fa7c2d..88059d6fdc3 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs @@ -8,7 +8,7 @@ use dpp::identifier::Identifier; use dpp::version::PlatformVersion; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; -use grovedb::{MaybeTree, TransactionArg, TreeType}; +use grovedb::{MaybeTree, TransactionArg}; use std::collections::BTreeMap; impl Drive { @@ -26,8 +26,13 @@ impl Drive { let poll_path = vote_poll.poll_path_vec()?; for vote_choice in YesNoAbstainVoteChoice::ALL { let votes_path = vote_poll.choice_votes_path_vec(vote_choice)?; + // Every delete here names its element type, so GroveDB never reads the pending + // batch for it. Each is built against an empty one: handing `batch_operations` + // to `batch_delete` would copy every operation already queued on each call and + // make the clean-up quadratic in the number of voters. if let Some(voters) = voters_by_choice.get(&vote_choice) { for voter in voters { + let mut delete_operations = vec![]; self.batch_delete( votes_path.as_slice().into(), voter.as_slice(), @@ -35,22 +40,27 @@ impl Drive { is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), }, transaction, - batch_operations, + &mut delete_operations, &platform_version.drive, )?; + batch_operations.append(&mut delete_operations); } } - // The sum tree itself, now empty. + // The sum tree itself, emptied by the deletes above. As for a contested poll's + // vote trees, it is not declared a tree: that would make GroveDB read every + // vote again to prove it empty. + let mut delete_operations = vec![]; self.batch_delete( poll_path.as_slice().into(), &[vote_choice.to_tree_key()], BatchDeleteApplyType::StatefulBatchDelete { - is_known_to_be_subtree_with_sum: Some(MaybeTree::Tree(TreeType::SumTree)), + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), }, transaction, - batch_operations, + &mut delete_operations, &platform_version.drive, )?; + batch_operations.append(&mut delete_operations); } } Ok(()) diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs index 00176cb6ee9..5a5ebe0a411 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs @@ -19,6 +19,9 @@ impl Drive { let path = vote_decisions_identity_votes_tree_path_for_identity(masternode_pro_tx_hash.as_bytes()); for vote_poll_id in vote_poll_ids { + // An item delete never reads the pending batch, so it is built against an empty + // one instead of copying everything a poll's clean-up has queued so far. + let mut delete_operations = vec![]; self.batch_delete( path.as_slice().into(), vote_poll_id.as_slice(), @@ -26,9 +29,10 @@ impl Drive { is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), }, transaction, - batch_operations, + &mut delete_operations, &platform_version.drive, )?; + batch_operations.append(&mut delete_operations); } Ok(()) } diff --git a/packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs b/packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs index a6d960c092c..c555686edb8 100644 --- a/packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs +++ b/packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs @@ -8,9 +8,9 @@ use grovedb::{Element, TransactionArg}; impl Drive { /// Creates the active polls and identity votes trees of the decisions branch when they are - /// missing. A chain upgrading to protocol version 14 calls this on its first block; fresh - /// chains create the same trees at genesis (`add_initial_vote_tree_main_structure_operations` - /// v1), so both populations build the same votes Merk. + /// missing. A chain upgrading to protocol version 14 calls this on its first block and a + /// chain born at 14 calls it from `create_initial_state_structure` v4, so both populations + /// build the same votes Merk. pub fn insert_vote_decisions_trees( &self, transaction: TransactionArg, diff --git a/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/mod.rs b/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/mod.rs index dfc9ac3fddf..25ece38374d 100644 --- a/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/mod.rs +++ b/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/mod.rs @@ -1,5 +1,4 @@ mod v0; -mod v1; use crate::drive::Drive; @@ -44,10 +43,9 @@ impl Drive { .add_initial_vote_tree_main_structure_operations { 0 => Drive::add_initial_vote_tree_main_structure_operations_v0(batch), - 1 => Drive::add_initial_vote_tree_main_structure_operations_v1(batch), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "add_initial_vote_tree_main_structure_operations".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } diff --git a/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/v1/mod.rs b/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/v1/mod.rs deleted file mode 100644 index 3e539311632..00000000000 --- a/packages/rs-drive/src/drive/votes/setup/setup_initial_vote_tree_main_structure/v1/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::drive::votes::paths::{ - vote_contested_resource_tree_path_vec, vote_decisions_tree_path_vec, vote_root_path_vec, - ACTIVE_POLLS_TREE_KEY, CONTESTED_RESOURCE_TREE_KEY, END_DATE_QUERIES_TREE_KEY, - IDENTITY_VOTES_TREE_KEY, VOTE_DECISIONS_TREE_KEY, -}; -use crate::drive::Drive; -use crate::error::Error; -use crate::util::batch::grovedb_op_batch::GroveDbOpBatchV0Methods; -use crate::util::batch::GroveDbOpBatch; - -impl Drive { - /// Version 1 (protocol version 14) also creates the active polls and identity votes trees - /// of the decisions branch, where yes/no vote polls live. - pub(super) fn add_initial_vote_tree_main_structure_operations_v1( - batch: &mut GroveDbOpBatch, - ) -> Result<(), Error> { - batch.add_insert_empty_tree(vote_root_path_vec(), vec![VOTE_DECISIONS_TREE_KEY as u8]); - batch.add_insert_empty_tree( - vote_root_path_vec(), - vec![CONTESTED_RESOURCE_TREE_KEY as u8], - ); - batch.add_insert_empty_tree(vote_root_path_vec(), vec![END_DATE_QUERIES_TREE_KEY as u8]); - batch.add_insert_empty_tree( - vote_contested_resource_tree_path_vec(), - vec![ACTIVE_POLLS_TREE_KEY as u8], - ); - batch.add_insert_empty_tree( - vote_contested_resource_tree_path_vec(), - vec![IDENTITY_VOTES_TREE_KEY as u8], - ); - batch.add_insert_empty_tree( - vote_decisions_tree_path_vec(), - vec![ACTIVE_POLLS_TREE_KEY as u8], - ); - batch.add_insert_empty_tree( - vote_decisions_tree_path_vec(), - vec![IDENTITY_VOTES_TREE_KEY as u8], - ); - Ok(()) - } -} diff --git a/packages/rs-drive/src/verify/bounded_decode.rs b/packages/rs-drive/src/verify/bounded_decode.rs index 7b324a89904..5bf76649c87 100644 --- a/packages/rs-drive/src/verify/bounded_decode.rs +++ b/packages/rs-drive/src/verify/bounded_decode.rs @@ -1,4 +1,5 @@ use crate::drive::votes::storage_form::contested_document_resource_reference_storage_form::ContestedDocumentResourceVoteReferenceStorageForm; +use crate::drive::votes::storage_form::yes_no_vote_reference_storage_form::YesNoVoteReferenceStorageForm; use crate::error::drive::DriveError; use crate::error::Error; use dpp::bincode; @@ -69,6 +70,20 @@ pub(super) fn decode_proof_data_contract( pub(super) fn decode_vote_reference( serialized_reference: &[u8], ) -> Result { + decode_bounded_vote_reference(serialized_reference) +} + +/// A masternode's proved yes/no vote, decoded under the same budget and exact-length rule as a +/// contested vote reference. +pub(super) fn decode_yes_no_vote_reference( + serialized_reference: &[u8], +) -> Result { + decode_bounded_vote_reference(serialized_reference) +} + +fn decode_bounded_vote_reference>( + serialized_reference: &[u8], +) -> Result { if serialized_reference.len() > MAX_VOTE_REFERENCE_DECODE_BYTES { return Err(Error::Drive(DriveError::CorruptedSerialization( "serialized vote reference exceeds the proof decoding limit".to_string(), diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs index 4c5669a67ec..52303ad7580 100644 --- a/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs @@ -4,10 +4,13 @@ mod v0; use crate::drive::Drive; use crate::error::drive::DriveError; +use crate::error::proof::ProofError; use crate::error::Error; use crate::verify::RootHash; use dpp::prelude::DataContract; use dpp::version::PlatformVersion; +use dpp::voting::vote_polls::VotePoll; +use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; use dpp::voting::votes::Vote; impl Drive { @@ -44,6 +47,29 @@ impl Drive { verify_subset_of_proof: bool, platform_version: &PlatformVersion, ) -> Result<(RootHash, Option), Error> { + // The versions below prove the contested resource votes index. A yes/no vote lives in the + // decisions branch, so it goes to its own verifier: querying the contested index for it + // would accept a genuine absence proof from the wrong branch as "not voted". A resource + // vote cannot name a yes/no poll at all. + match vote { + Vote::YesNoVote(yes_no_vote) => { + return Self::verify_masternode_yes_no_vote( + proof, + masternode_pro_tx_hash, + yes_no_vote, + verify_subset_of_proof, + platform_version, + ) + .map(|(root_hash, proved_vote)| (root_hash, proved_vote.map(Vote::YesNoVote))); + } + Vote::ResourceVote(resource_vote) => { + if let VotePoll::YesNoVotePoll(_) = resource_vote.vote_poll() { + return Err(Error::Proof(ProofError::InvalidTransition( + "a resource vote cannot answer a yes/no vote poll".to_string(), + ))); + } + } + } match platform_version .drive .methods diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs index c0eb4008a27..583c83465e1 100644 --- a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs @@ -72,7 +72,7 @@ mod tests { }) } - fn proof_of_vote(drive: &crate::drive::Drive, voter: [u8; 32]) -> Vec { + fn proof_of_vote(drive: &Drive, voter: [u8; 32]) -> Vec { let platform_version = PlatformVersion::latest(); let mut path_query = IdentityBasedVoteDriveQuery { identity_id: Identifier::new(voter), @@ -151,4 +151,102 @@ mod tests { .expect("verify"); assert_eq!(proved, None); } + + /// The generic masternode vote verifier routes a yes/no vote to this one, so a genuine + /// absence proof taken from the contested resource votes index (where a yes/no vote never + /// lives) cannot pass as "the masternode did not vote". + #[test] + fn should_route_a_yes_no_vote_through_the_generic_verifier() { + use crate::drive::votes::paths::vote_contested_resource_identity_votes_tree_path_for_identity_vec; + use crate::query::Query; + use dpp::tests::json_document::json_document_to_contract; + use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; + use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; + use dpp::voting::votes::resource_vote::ResourceVote; + use dpp::voting::votes::Vote; + use grovedb::{PathQuery, SizedQuery}; + + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let data_contract = json_document_to_contract( + "tests/supporting_files/contract/dpns/dpns-contract.json", + false, + platform_version, + ) + .expect("expected a data contract"); + drive + .open_yes_no_vote_poll( + &poll(), + 1_000, + &BlockInfo::default(), + None, + platform_version, + ) + .expect("open"); + let voter = [7u8; 32]; + drive + .register_yes_no_identity_vote( + voter, + 1, + ResolvedYesNoVote { + vote_poll: poll(), + vote_choice: YesNoAbstainVoteChoice::No, + previous_vote_choice_to_remove: None, + }, + &BlockInfo::default(), + None, + platform_version, + ) + .expect("register"); + let yes_no_vote = Vote::YesNoVote(vote(YesNoAbstainVoteChoice::No)); + + // The right proof verifies through the generic entry point. + let (_, proved) = Drive::verify_masternode_vote( + &proof_of_vote(&drive, voter), + voter, + &yes_no_vote, + &data_contract, + false, + platform_version, + ) + .expect("verify"); + assert_eq!(proved, Some(yes_no_vote.clone())); + + // An absence proof of the poll id under the contested index is refused. + let mut query = Query::new(); + query.insert_key(poll().unique_id().expect("id").to_vec()); + let wrong_branch = PathQuery::new( + vote_contested_resource_identity_votes_tree_path_for_identity_vec(&voter), + SizedQuery::new(query, None, None), + ); + let wrong_branch_proof = drive + .grove_get_proved_path_query(&wrong_branch, None, &mut vec![], &platform_version.drive) + .expect("proof"); + assert!(Drive::verify_masternode_vote( + &wrong_branch_proof, + voter, + &yes_no_vote, + &data_contract, + false, + platform_version, + ) + .is_err()); + + // A resource vote naming a yes/no poll is refused outright. + let resource_vote = Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + vote_poll: VotePoll::YesNoVotePoll(poll()), + resource_vote_choice: ResourceVoteChoice::Abstain, + })); + assert!(matches!( + Drive::verify_masternode_vote( + &wrong_branch_proof, + voter, + &resource_vote, + &data_contract, + false, + platform_version, + ), + Err(Error::Proof(ProofError::InvalidTransition(_))) + )); + } } diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs index 28cd27ecdec..6b2779f9b21 100644 --- a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs @@ -1,14 +1,15 @@ -use crate::drive::votes::paths::vote_decisions_identity_votes_tree_path_for_identity_vec; -use crate::drive::votes::storage_form::yes_no_vote_reference_storage_form::YesNoVoteReferenceStorageForm; use crate::drive::Drive; use crate::error::proof::ProofError; use crate::error::Error; -use crate::query::Query; +use crate::query::IdentityBasedVoteDriveQuery; +use crate::verify::bounded_decode::decode_yes_no_vote_reference; use crate::verify::RootHash; +use dpp::identifier::Identifier; +use dpp::voting::vote_polls::VotePoll; use dpp::voting::votes::yes_no_vote::accessors::v0::YesNoVoteGettersV0; use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; use dpp::voting::votes::yes_no_vote::YesNoVote; -use grovedb::{GroveDb, PathQuery, SizedQuery}; +use grovedb::GroveDb; use platform_version::version::PlatformVersion; impl Drive { @@ -20,12 +21,12 @@ impl Drive { verify_subset_of_proof: bool, platform_version: &PlatformVersion, ) -> Result<(RootHash, Option), Error> { - let path = - vote_decisions_identity_votes_tree_path_for_identity_vec(&masternode_pro_tx_hash); - let vote_poll_id = vote.vote_poll().unique_id()?; - let mut query = Query::new(); - query.insert_key(vote_poll_id.to_vec()); - let path_query = PathQuery::new(path, SizedQuery::new(query, Some(1), None)); + // The prover's own query, so where a masternode's yes/no vote lives is written once. + let path_query = IdentityBasedVoteDriveQuery { + identity_id: Identifier::new(masternode_pro_tx_hash), + vote_poll: VotePoll::YesNoVotePoll(vote.vote_poll().clone()), + } + .construct_path_query()?; let (root_hash, mut proved_key_values) = if verify_subset_of_proof { GroveDb::verify_subset_query_with_absence_proof( proof, @@ -48,13 +49,7 @@ impl Drive { let maybe_vote = maybe_element .map(|element| { let bytes = element.into_item_bytes()?; - let storage_form = - YesNoVoteReferenceStorageForm::deserialize(&bytes).map_err(|e| { - Error::Proof(ProofError::CorruptedProof(format!( - "the proved yes/no vote could not be decoded: {}", - e - ))) - })?; + let storage_form = decode_yes_no_vote_reference(&bytes)?; let proved_vote = YesNoVote::V0(YesNoVoteV0 { vote_poll: vote.vote_poll().clone(), vote_choice: storage_form.vote_choice, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs index 149ba79d42a..7bbdf5d0059 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs @@ -1,7 +1,7 @@ use crate::version::drive_versions::drive_vote_method_versions::v2::DRIVE_VOTE_METHOD_VERSIONS_V2; use crate::version::drive_versions::drive_vote_method_versions::{ DriveVoteCleanupMethodVersions, DriveVoteContestedResourceInsertMethodVersions, - DriveVoteMethodVersions, DriveVoteSetupMethodVersions, + DriveVoteMethodVersions, }; /// Drive vote method versions 3. Introduced in protocol version 14 with the yes/no poll @@ -10,9 +10,6 @@ use crate::version::drive_versions::drive_vote_method_versions::{ /// * `contested_resource_insert.register_identity_vote` 1 (the slot `register_identity_vote` /// dispatches on; `insert.register_identity_vote` is read by nothing) dispatches a resolved /// vote of either kind (v0 knows only contested resource votes). -/// * `add_initial_vote_tree_main_structure_operations` 1 creates the active polls and -/// identity votes trees under the decisions branch at genesis (a chain upgrading to -/// protocol version 14 creates them on its first block). /// * `remove_all_votes_given_by_identities` 1 also removes the yes/no votes of a masternode /// that left the list. /// @@ -26,8 +23,5 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V3: DriveVoteMethodVersions = DriveVoteMeth remove_all_votes_given_by_identities: 1, ..DRIVE_VOTE_METHOD_VERSIONS_V2.cleanup }, - setup: DriveVoteSetupMethodVersions { - add_initial_vote_tree_main_structure_operations: 1, - }, ..DRIVE_VOTE_METHOD_VERSIONS_V2 }; diff --git a/packages/rs-platform-version/src/version/drive_versions/v9.rs b/packages/rs-platform-version/src/version/drive_versions/v9.rs index b16b2455c2f..9ac79f1acbe 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v9.rs @@ -85,7 +85,7 @@ pub const DRIVE_VERSION_V9: DriveVersion = DriveVersion { calculate_total_credits_balance: 2, // ShieldedBalances root tree adds a fifth term to the equation }, document: DRIVE_DOCUMENT_METHOD_VERSIONS_V4, // changed in v9: v2 index walkers + v1 update walker (shared-prefix aggregate indexes become insertable) and the detect_ranked_mode slot - vote: DRIVE_VOTE_METHOD_VERSIONS_V3, // changed in v14: the yes/no poll kind (register_identity_vote 1, genesis vote trees 1, masternode removal sweep 1) + vote: DRIVE_VOTE_METHOD_VERSIONS_V3, // changed in v14: the yes/no poll kind (register_identity_vote 1, masternode removal sweep 1) contract: DRIVE_CONTRACT_METHOD_VERSIONS_V4, // changed in v9: add_contract_to_storage v1 writes the contract version item beside the contract; update_contract v2 creates the distribution storage and mints the base supply of tokens added by an update fees: DriveFeesMethodVersions { calculate_fee: 0 }, estimated_costs: DriveEstimatedCostsMethodVersions { diff --git a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs index 52c04f01316..b1a76acdb1c 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs @@ -458,11 +458,12 @@ impl Sdk { } let mut last_timestamp = None; - let mut polls_in_last_group = 0; + let mut polls_in_page = 0; // Process each timestamp group for (timestamp, polls) in result.0 { - let mut dpns_polls_count = 0; + polls_in_page += polls.len(); + last_timestamp = Some(timestamp); for contested_poll in polls.iter().filter_map(|poll| match poll { VotePoll::ContestedDocumentResourceVotePoll(contested_poll) => { @@ -477,31 +478,29 @@ impl Sdk { if contested_poll.index_values.len() >= 2 { if let Value::Text(label) = &contested_poll.index_values[1] { name_to_end_time.insert(label.clone(), timestamp); - dpns_polls_count += 1; } } } } - - if dpns_polls_count > 0 { - last_timestamp = Some(timestamp); - polls_in_last_group = dpns_polls_count; - } } - // Check if we should continue pagination - // If we got less than the limit, we've reached the end - if polls_in_last_group < query_limit as usize { + // The limit counts every poll of every kind across all of the page's end dates, so + // only a full page can have more behind it. + if polls_in_page < query_limit as usize { break; } - // Set up for next query - use the last timestamp as the new start - // with false (not included) to avoid duplicates - if let Some(last_ts) = last_timestamp { - current_start_time = Some((last_ts, false)); - } else { + // The limit may have cut the last end date short, so the next page starts at it + // again (a name seen twice is just written again). If that end date alone filled + // the page, starting at it again would return the same page, so move past it. + let Some(last_ts) = last_timestamp else { break; - } + }; + current_start_time = if current_start_time == Some((last_ts, true)) { + Some((last_ts, false)) + } else { + Some((last_ts, true)) + }; } Ok(name_to_end_time) diff --git a/packages/rs-sdk/src/platform/transition/vote.rs b/packages/rs-sdk/src/platform/transition/vote.rs index 9008975670f..078fdbe8d42 100644 --- a/packages/rs-sdk/src/platform/transition/vote.rs +++ b/packages/rs-sdk/src/platform/transition/vote.rs @@ -140,14 +140,25 @@ impl> PutVote for Vote { //todo make this more reliable Err(e) => { return if e.to_string().contains("already exists") { - let vote = - Vote::fetch(sdk, VoteQuery::new(voter_pro_tx_hash, vote_poll_id)).await?; - vote.ok_or(Error::Generic( - "vote was proved to not exist but was said to exist".to_string(), - )) + match self { + Vote::ResourceVote(_) => { + let vote = + Vote::fetch(sdk, VoteQuery::new(voter_pro_tx_hash, vote_poll_id)) + .await?; + vote.ok_or(Error::Generic( + "vote was proved to not exist but was said to exist".to_string(), + )) + } + // `VoteQuery` reads the contested resource votes index only; no query + // fetches a yes/no vote yet, so the existing vote cannot be returned. + Vote::YesNoVote(_) => Err(Error::Generic( + "the yes/no vote already exists; fetching yes/no votes is not supported yet" + .to_string(), + )), + } } else { Err(e.into()) - } + }; } } Self::wait_for_response(sdk, masternode_vote_transition, Some(settings)).await diff --git a/packages/wasm-dpp2/src/voting/resource_vote.rs b/packages/wasm-dpp2/src/voting/resource_vote.rs index c7a474fd97d..68f6ad47ae9 100644 --- a/packages/wasm-dpp2/src/voting/resource_vote.rs +++ b/packages/wasm-dpp2/src/voting/resource_vote.rs @@ -1,6 +1,8 @@ +use crate::error::WasmDppResult; use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::voting::resource_vote_choice::ResourceVoteChoiceWasm; +use crate::voting::vote::contested_poll_for_resource_vote; use crate::voting::vote_poll::VotePollWasm; use dpp::voting::votes::resource_vote::ResourceVote; use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; @@ -57,11 +59,14 @@ impl From for ResourceVote { #[wasm_bindgen(js_class = ResourceVote)] impl ResourceVoteWasm { #[wasm_bindgen(constructor)] - pub fn constructor(poll: &VotePollWasm, choice: &ResourceVoteChoiceWasm) -> Self { - ResourceVoteWasm(ResourceVote::V0(ResourceVoteV0 { - vote_poll: poll.clone().into(), + pub fn constructor( + poll: &VotePollWasm, + choice: &ResourceVoteChoiceWasm, + ) -> WasmDppResult { + Ok(ResourceVoteWasm(ResourceVote::V0(ResourceVoteV0 { + vote_poll: contested_poll_for_resource_vote(poll)?, resource_vote_choice: choice.clone().into(), - })) + }))) } #[wasm_bindgen(getter = poll)] @@ -75,16 +80,18 @@ impl ResourceVoteWasm { } #[wasm_bindgen(setter = poll)] - pub fn set_poll(&mut self, poll: &VotePollWasm) { + pub fn set_poll(&mut self, poll: &VotePollWasm) -> WasmDppResult<()> { let ResourceVote::V0(ResourceVoteV0 { resource_vote_choice, .. }) = self.0.clone(); self.0 = ResourceVote::V0(ResourceVoteV0 { - vote_poll: poll.clone().into(), + vote_poll: contested_poll_for_resource_vote(poll)?, resource_vote_choice, }); + + Ok(()) } #[wasm_bindgen(setter = choice)] diff --git a/packages/wasm-dpp2/src/voting/vote.rs b/packages/wasm-dpp2/src/voting/vote.rs index f255e4f52ad..179adfb4915 100644 --- a/packages/wasm-dpp2/src/voting/vote.rs +++ b/packages/wasm-dpp2/src/voting/vote.rs @@ -21,25 +21,39 @@ const TS_TYPES: &str = r#" * Vote serialized as a plain object. * * Internally tagged with `$type` ($-prefix because the level also carries - * the inner ResourceVote's `$formatVersion`). The single ResourceVote - * variant flattens its V0 body — no `data` wrapper. + * the inner vote's `$formatVersion`). Each variant flattens its V0 body — no + * `data` wrapper. A yes/no vote carries its poll's fields untagged. */ -export interface VoteObject { - $type: "resourceVote"; - $formatVersion: string; - votePoll: VotePollObject; - resourceVoteChoice: ResourceVoteChoiceObject; -} +export type VoteObject = + | { + $type: "resourceVote"; + $formatVersion: string; + votePoll: VotePollObject; + resourceVoteChoice: ResourceVoteChoiceObject; + } + | { + $type: "yesNoVote"; + $formatVersion: string; + votePoll: YesNoVotePollFieldsObject; + voteChoice: "yes" | "no" | "abstain"; + }; /** * Vote serialized as JSON. */ -export interface VoteJSON { - $type: "resourceVote"; - $formatVersion: string; - votePoll: VotePollJSON; - resourceVoteChoice: ResourceVoteChoiceJSON; -} +export type VoteJSON = + | { + $type: "resourceVote"; + $formatVersion: string; + votePoll: VotePollJSON; + resourceVoteChoice: ResourceVoteChoiceJSON; + } + | { + $type: "yesNoVote"; + $formatVersion: string; + votePoll: YesNoVotePollFieldsJSON; + voteChoice: "yes" | "no" | "abstain"; + }; "#; #[wasm_bindgen] @@ -75,11 +89,13 @@ impl VoteWasm { #[wasm_bindgen(js_name = "votePoll")] vote_poll: &VotePollWasm, #[wasm_bindgen(js_name = "resourceVoteChoice")] resource_vote_choice: &ResourceVoteChoiceWasm, - ) -> Self { - VoteWasm(Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { - vote_poll: vote_poll.clone().into(), - resource_vote_choice: resource_vote_choice.clone().into(), - }))) + ) -> WasmDppResult { + Ok(VoteWasm(Vote::ResourceVote(ResourceVote::V0( + ResourceVoteV0 { + vote_poll: contested_poll_for_resource_vote(vote_poll)?, + resource_vote_choice: resource_vote_choice.clone().into(), + }, + )))) } #[wasm_bindgen(getter = poll)] @@ -118,7 +134,7 @@ impl VoteWasm { pub fn set_poll(&mut self, poll: &VotePollWasm) -> WasmDppResult<()> { self.0 = match self.0.clone() { Vote::ResourceVote(vote) => Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { - vote_poll: poll.clone().into(), + vote_poll: contested_poll_for_resource_vote(poll)?, resource_vote_choice: vote.resource_vote_choice(), })), Vote::YesNoVote(vote) => { @@ -151,6 +167,18 @@ impl VoteWasm { } } +/// The poll of a resource vote: a contested document resource poll. Consensus refuses a resource +/// vote that names a yes/no poll, so it is refused here as a yes/no vote refuses a contested poll. +pub(crate) fn contested_poll_for_resource_vote(poll: &VotePollWasm) -> WasmDppResult { + match poll.clone().into() { + VotePoll::YesNoVotePoll(_) => Err(WasmDppError::invalid_argument( + "a resource vote answers a contested document resource vote poll, not a yes/no vote poll" + .to_string(), + )), + vote_poll => Ok(vote_poll), + } +} + /// The resource vote choice exists on a resource vote only. fn not_a_resource_vote() -> WasmDppError { WasmDppError::invalid_argument("this vote is a yes/no vote, not a resource vote".to_string()) diff --git a/packages/wasm-dpp2/src/voting/vote_poll.rs b/packages/wasm-dpp2/src/voting/vote_poll.rs index f4ed9731a99..30d9523e7e5 100644 --- a/packages/wasm-dpp2/src/voting/vote_poll.rs +++ b/packages/wasm-dpp2/src/voting/vote_poll.rs @@ -28,31 +28,55 @@ export interface VotePollOptions { indexValues: any[]; } +/** + * A yes/no poll's fields as a plain object. A yes/no vote carries its poll + * like this, without a `$type` tag. + */ +export interface YesNoVotePollFieldsObject { + resourcePath: Uint8Array[]; + supermajorityNumerator: number; + supermajorityDenominator: number; + minimumVotingPower: number; +} + +/** + * A yes/no poll's fields as JSON (resource path segments in base64). + */ +export interface YesNoVotePollFieldsJSON { + resourcePath: string[]; + supermajorityNumerator: number; + supermajorityDenominator: number; + minimumVotingPower: number; +} + /** * VotePoll serialized as a plain object. * - * Internally tagged with `type` (plain — no `$`-prefixed neighbors at - * this level). Inner ContestedDocumentResourceVotePoll fields flatten at - * the same level — no `data` wrapper. + * Internally tagged with `$type`. The inner poll's fields flatten at the + * same level — no `data` wrapper. */ -export interface VotePollObject { - $type: "contestedDocumentResourceVotePoll"; - contractId: Uint8Array; - documentTypeName: string; - indexName: string; - indexValues: any[]; -} +export type VotePollObject = + | { + $type: "contestedDocumentResourceVotePoll"; + contractId: Uint8Array; + documentTypeName: string; + indexName: string; + indexValues: any[]; + } + | ({ $type: "yesNoVotePoll" } & YesNoVotePollFieldsObject); /** * VotePoll serialized as JSON. */ -export interface VotePollJSON { - $type: "contestedDocumentResourceVotePoll"; - contractId: string; - documentTypeName: string; - indexName: string; - indexValues: any[]; -} +export type VotePollJSON = + | { + $type: "contestedDocumentResourceVotePoll"; + contractId: string; + documentTypeName: string; + indexName: string; + indexValues: any[]; + } + | ({ $type: "yesNoVotePoll" } & YesNoVotePollFieldsJSON); "#; #[wasm_bindgen] diff --git a/packages/wasm-sdk/src/state_transitions/identity.rs b/packages/wasm-sdk/src/state_transitions/identity.rs index d222c497e63..7901aa6cc22 100644 --- a/packages/wasm-sdk/src/state_transitions/identity.rs +++ b/packages/wasm-sdk/src/state_transitions/identity.rs @@ -795,6 +795,13 @@ impl WasmSdk { // Extract complex types first (borrows &options) let vote_poll: dash_sdk::dpp::voting::vote_polls::VotePoll = VotePollWasm::try_from_options(&options, "votePoll")?.into(); + // This casts a resource vote, which answers a contested document resource poll only; + // consensus refuses one naming a yes/no poll. + if let dash_sdk::dpp::voting::vote_polls::VotePoll::YesNoVotePoll(_) = vote_poll { + return Err(WasmSdkError::invalid_argument( + "masternodeVote casts a resource vote, which cannot answer a yes/no vote poll", + )); + } let resource_vote_choice: dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice = ResourceVoteChoiceWasm::try_from_options(&options, "voteChoice")?.into(); let voting_public_key: IdentityPublicKey = From b8fb0a10e2c33600d768b263a9f70d4b1fbbe900 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 23 Sep 2026 19:58:32 +0700 Subject: [PATCH 04/18] feat(platform)!: a yes/no poll's minimum is a fixed voting power or a 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 --- .../src/errors/consensus/state/state_error.rs | 4 +- .../src/serialization/json/safe_fields.rs | 1 + packages/rs-dpp/src/state_transition/mod.rs | 4 +- .../yes_no_vote_poll_stored_info/mod.rs | 15 +- .../yes_no_vote_poll_stored_info/v0/mod.rs | 2 + .../voting/vote_polls/yes_no_vote_poll/mod.rs | 285 ++++++++++++++++-- .../src/voting/votes/yes_no_vote/mod.rs | 8 +- .../check_for_ended_vote_polls/v1/mod.rs | 23 +- .../mod.rs | 9 +- .../v0/mod.rs | 11 +- .../masternode_vote/yes_no_tests.rs | 91 +++++- .../src/query/yes_no_vote_poll_state_query.rs | 3 +- packages/rs-drive/src/structure/tests.rs | 4 +- .../v0/mod.rs | 4 +- .../verify_masternode_yes_no_vote/mod.rs | 4 +- .../src/version/mocks/v2_test.rs | 2 + .../src/version/system_limits/mod.rs | 6 + .../src/version/system_limits/v1.rs | 2 + .../src/version/system_limits/v2.rs | 2 + .../src/version/system_limits/v3.rs | 2 + .../src/version/system_limits/v4.rs | 2 + packages/wasm-dpp2/src/voting/vote_poll.rs | 23 +- 22 files changed, 453 insertions(+), 54 deletions(-) diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index ab916c4d03b..2afb518be2d 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -630,7 +630,7 @@ mod tests { use crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use crate::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStatus; use crate::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; - use crate::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; + use crate::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; use crate::voting::vote_polls::VotePoll; use platform_value::{BinaryData, Identifier}; use std::collections::BTreeMap; @@ -1209,7 +1209,7 @@ mod tests { resource_path: vec![BinaryData::new(vec![1; 32])], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: 400, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), }, YesNoVotePollStatus::Started(BlockInfo::default()) ) diff --git a/packages/rs-dpp/src/serialization/json/safe_fields.rs b/packages/rs-dpp/src/serialization/json/safe_fields.rs index bd3eb833301..45af8462ba3 100644 --- a/packages/rs-dpp/src/serialization/json/safe_fields.rs +++ b/packages/rs-dpp/src/serialization/json/safe_fields.rs @@ -144,6 +144,7 @@ impl JsonSafeFields for crate::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice { } +impl JsonSafeFields for crate::voting::vote_polls::yes_no_vote_poll::YesNoMinimumVotingPower {} impl JsonSafeFields for crate::group::action_event::GroupActionEvent {} // TokenEvent contains u64 aliases (TokenAmount, Credits) in tuple variants that // `#[json_safe_fields]` can't auto-annotate. Developer takes responsibility for diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index c99fc26e673..17f6681bb6b 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -2622,7 +2622,7 @@ mod tests { use crate::serialization::PlatformSerializable; use crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use crate::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; - use crate::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; + use crate::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; use crate::voting::votes::resource_vote::v0::ResourceVoteV0; use crate::voting::votes::resource_vote::ResourceVote; use crate::voting::votes::yes_no_vote::v0::YesNoVoteV0; @@ -2632,7 +2632,7 @@ mod tests { resource_path: vec![BinaryData::new(vec![7u8; 32])], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: 400, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), }; let votes = [ Vote::YesNoVote(YesNoVote::V0(YesNoVoteV0 { diff --git a/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs index e929e6ecd58..4ec9a614071 100644 --- a/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs +++ b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs @@ -23,6 +23,9 @@ pub struct YesNoVotePollResult { pub no_voting_power: VotingPower, /// The abstaining voting power at the end. Recorded, but it plays no part in the result. pub abstain_voting_power: VotingPower, + /// The yes plus no voting power the poll needed, resolved in the block that closed it (a + /// share of the total is measured then). + pub required_voting_power: VotingPower, /// The block the poll opened in. pub start_block: BlockInfo, /// The block the poll was closed in. @@ -33,11 +36,12 @@ impl fmt::Display for YesNoVotePollResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "YesNoVotePollResult {{ passed: {}, yes: {}, no: {}, abstain: {}, start_block: {}, finalization_block: {} }}", + "YesNoVotePollResult {{ passed: {}, yes: {}, no: {}, abstain: {}, required: {}, start_block: {}, finalization_block: {} }}", self.passed, self.yes_voting_power, self.no_voting_power, self.abstain_voting_power, + self.required_voting_power, self.start_block, self.finalization_block ) @@ -138,6 +142,7 @@ impl YesNoVotePollStoredInfo { yes_voting_power: VotingPower, no_voting_power: VotingPower, abstain_voting_power: VotingPower, + required_voting_power: VotingPower, passed: bool, finalization_block: BlockInfo, ) -> Result { @@ -146,6 +151,7 @@ impl YesNoVotePollStoredInfo { yes_voting_power, no_voting_power, abstain_voting_power, + required_voting_power, passed, finalization_block, ), @@ -187,7 +193,7 @@ mod tests { let finalization_block = BlockInfo::default_with_time(20); let result = stored_info - .finalize_vote_poll(300, 100, 7, true, finalization_block) + .finalize_vote_poll(300, 100, 7, 400, true, finalization_block) .expect("finalize"); assert_eq!( result, @@ -196,6 +202,7 @@ mod tests { yes_voting_power: 300, no_voting_power: 100, abstain_voting_power: 7, + required_voting_power: 400, start_block, finalization_block, } @@ -204,7 +211,7 @@ mod tests { // A finished poll is not finalized twice. assert!(stored_info - .finalize_vote_poll(1, 1, 1, false, finalization_block) + .finalize_vote_poll(1, 1, 1, 400, false, finalization_block) .is_err()); } @@ -215,7 +222,7 @@ mod tests { YesNoVotePollStoredInfo::new(BlockInfo::default_with_time(10), platform_version) .expect("stored info"); stored_info - .finalize_vote_poll(5, 4, 3, false, BlockInfo::default_with_time(20)) + .finalize_vote_poll(5, 4, 3, 400, false, BlockInfo::default_with_time(20)) .expect("finalize"); let bytes = stored_info.serialize_to_bytes().expect("serialize"); let recovered = diff --git a/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/v0/mod.rs b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/v0/mod.rs index 98ec0770d2b..d89954e853d 100644 --- a/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/v0/mod.rs +++ b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/v0/mod.rs @@ -32,6 +32,7 @@ impl YesNoVotePollStoredInfoV0 { yes_voting_power: VotingPower, no_voting_power: VotingPower, abstain_voting_power: VotingPower, + required_voting_power: VotingPower, passed: bool, finalization_block: BlockInfo, ) -> Result { @@ -45,6 +46,7 @@ impl YesNoVotePollStoredInfoV0 { yes_voting_power, no_voting_power, abstain_voting_power, + required_voting_power, start_block, finalization_block, }; diff --git a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs index 582b0106dcf..f8a2700b9a7 100644 --- a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs @@ -28,6 +28,90 @@ pub type VotingPower = u32; /// prefunded balance and an end date index entry). const YES_NO_VOTE_POLL_ID_DOMAIN: &[u8] = b"dash platform yes/no vote poll id"; +/// Which way a fraction of the total voting power rounds to a whole voting power. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub enum VotingPowerRounding { + /// Toward zero: 1/3 of 10 is 3. + Down, + /// Away from zero: 1/3 of 10 is 4. + Up, +} + +/// The least yes plus no voting power a yes/no poll needs to pass. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub enum YesNoMinimumVotingPower { + /// A fixed voting power, at most `max_yes_no_vote_poll_minimum_voting_power`. + Absolute(VotingPower), + /// A share of the total voting power of the masternode list (masternode 1, evonode 4) in + /// the block that closes the poll, rounded as asked, at most + /// `max_yes_no_vote_poll_minimum_voting_power_percent_of_total` percent of it. + #[cfg_attr(feature = "serde-conversion", serde(rename_all = "camelCase"))] + FractionOfTotal { + numerator: u8, + denominator: u8, + rounding: VotingPowerRounding, + }, +} + +impl YesNoMinimumVotingPower { + /// The minimum as a voting power, given the total voting power of the masternode list when + /// the poll closes. A fraction with a zero denominator (refused when the poll opens) needs + /// more than any poll can gather. + pub fn resolve(&self, total_voting_power: VotingPower) -> VotingPower { + match *self { + YesNoMinimumVotingPower::Absolute(voting_power) => voting_power, + YesNoMinimumVotingPower::FractionOfTotal { + numerator, + denominator, + rounding, + } => { + if denominator == 0 { + return VotingPower::MAX; + } + let share = total_voting_power as u64 * numerator as u64; + let denominator = denominator as u64; + let resolved = match rounding { + VotingPowerRounding::Down => share / denominator, + VotingPowerRounding::Up => share.div_ceil(denominator), + }; + VotingPower::try_from(resolved).unwrap_or(VotingPower::MAX) + } + } + } +} + +impl fmt::Display for YesNoMinimumVotingPower { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + YesNoMinimumVotingPower::Absolute(voting_power) => write!(f, "{}", voting_power), + YesNoMinimumVotingPower::FractionOfTotal { + numerator, + denominator, + rounding, + } => write!( + f, + "{}/{} of the total, rounded {}", + numerator, + denominator, + match rounding { + VotingPowerRounding::Down => "down", + VotingPowerRounding::Up => "up", + } + ), + } + } +} + /// A poll the masternodes answer with yes, no or abstain. /// /// The poll is keyed by a resource path the feature that opens it chooses (a contract id and a @@ -38,9 +122,10 @@ const YES_NO_VOTE_POLL_ID_DOMAIN: &[u8] = b"dash platform yes/no vote poll id"; /// The poll passes when the yes voting power is strictly more than /// `supermajority_numerator / supermajority_denominator` of the yes plus no voting power, the /// abstaining power left out of both sides, and the yes plus no voting power is at least -/// `minimum_voting_power`. Otherwise it fails. Both numbers are carried by the poll so every -/// feature sets its own rule. Two thirds with a floor of 400 is numerator 2, denominator 3 and -/// minimum 400. +/// `minimum_voting_power`. Otherwise it fails. The minimum is a fixed voting power or a share of +/// the masternode list's total voting power when the poll closes. The whole rule is carried by +/// the poll so every feature sets its own. Two thirds with a floor of 400 is numerator 2, +/// denominator 3 and an absolute minimum of 400. /// /// The parameters are part of the poll's identity: [`Self::unique_id`] hashes the serialized /// [`VotePoll`] that carries the poll, so a vote names the exact poll it answers. The prefunded @@ -78,7 +163,7 @@ pub struct YesNoVotePoll { /// The denominator of the share of yes plus no voting power that yes must exceed. pub supermajority_denominator: u8, /// The least yes plus no voting power for the poll to pass. - pub minimum_voting_power: VotingPower, + pub minimum_voting_power: YesNoMinimumVotingPower, } impl fmt::Display for YesNoVotePoll { @@ -102,8 +187,10 @@ impl fmt::Display for YesNoVotePoll { impl YesNoVotePoll { /// Refuses parameters no poll can be opened with: an empty or oversized resource path (the /// poll rides in every vote, whose wire size is bounded), a zero denominator, a zero - /// numerator (any single yes would pass against every no), or a numerator that is not below - /// the denominator (yes could never exceed the whole of yes plus no). + /// numerator (any single yes would pass against every no), a numerator that is not below + /// the denominator (yes could never exceed the whole of yes plus no), and a minimum above + /// its cap (a voting power, or a share of the total with a zero denominator or above the + /// allowed percentage). pub fn validate_parameters( &self, platform_version: &PlatformVersion, @@ -149,14 +236,59 @@ impl YesNoVotePoll { self.supermajority_numerator, self.supermajority_denominator ))); } + match self.minimum_voting_power { + YesNoMinimumVotingPower::Absolute(voting_power) => { + let max_voting_power = platform_version + .system_limits + .max_yes_no_vote_poll_minimum_voting_power; + if voting_power > max_voting_power { + return Err(ProtocolError::VoteError(format!( + "a yes/no vote poll minimum voting power is at most {}, got {}", + max_voting_power, voting_power + ))); + } + } + YesNoMinimumVotingPower::FractionOfTotal { + numerator, + denominator, + .. + } => { + if denominator == 0 { + return Err(ProtocolError::VoteError( + "a yes/no vote poll minimum share denominator must not be zero".to_string(), + )); + } + let max_percent = platform_version + .system_limits + .max_yes_no_vote_poll_minimum_voting_power_percent_of_total; + if numerator as u32 * 100 > denominator as u32 * max_percent as u32 { + return Err(ProtocolError::VoteError(format!( + "a yes/no vote poll minimum share is at most {}% of the total voting power, got {}/{}", + max_percent, numerator, denominator + ))); + } + } + } Ok(()) } - /// Whether the poll passes with these final tallies. Abstaining power plays no part. - pub fn passes(&self, yes_voting_power: VotingPower, no_voting_power: VotingPower) -> bool { + /// The least yes plus no voting power the poll needs, given the total voting power of the + /// masternode list in the block that closes it. + pub fn required_voting_power(&self, total_voting_power: VotingPower) -> VotingPower { + self.minimum_voting_power.resolve(total_voting_power) + } + + /// Whether the poll passes with these final tallies, given the total voting power of the + /// masternode list in the block that closes it. Abstaining power plays no part. + pub fn passes( + &self, + yes_voting_power: VotingPower, + no_voting_power: VotingPower, + total_voting_power: VotingPower, + ) -> bool { let yes = yes_voting_power as u64; let cast = yes + no_voting_power as u64; - if cast < self.minimum_voting_power as u64 { + if cast < self.required_voting_power(total_voting_power) as u64 { return false; } yes * self.supermajority_denominator as u64 > cast * self.supermajority_numerator as u64 @@ -193,7 +325,7 @@ mod tests { ], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: 400, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), } } @@ -201,22 +333,109 @@ mod tests { fn should_pass_strictly_above_two_thirds_and_fail_at_or_below_it() { // The minimum is met by every tally below; only the share decides. let poll = YesNoVotePoll { - minimum_voting_power: 300, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(300), ..poll() }; // 300 cast: two thirds is 200, so 201 passes and 200 does not. - assert!(poll.passes(201, 99)); - assert!(!poll.passes(200, 100)); + assert!(poll.passes(201, 99, 0)); + assert!(!poll.passes(200, 100, 0)); // 301 cast: two thirds is 200.67, so 201 passes and 200 does not. - assert!(poll.passes(201, 100)); - assert!(!poll.passes(200, 101)); + assert!(poll.passes(201, 100, 0)); + assert!(!poll.passes(200, 101, 0)); } #[test] fn should_fail_below_the_minimum_voting_power_even_when_unanimous() { let poll = poll(); - assert!(!poll.passes(399, 0)); - assert!(poll.passes(400, 0)); + assert!(!poll.passes(399, 0, 0)); + assert!(poll.passes(400, 0, 0)); + } + + #[test] + fn should_resolve_a_share_of_the_total_rounded_as_asked() { + let third = |rounding| YesNoMinimumVotingPower::FractionOfTotal { + numerator: 1, + denominator: 3, + rounding, + }; + assert_eq!(third(VotingPowerRounding::Down).resolve(10), 3); + assert_eq!(third(VotingPowerRounding::Up).resolve(10), 4); + // An exact share is the same either way. + assert_eq!(third(VotingPowerRounding::Down).resolve(9), 3); + assert_eq!(third(VotingPowerRounding::Up).resolve(9), 3); + assert_eq!(third(VotingPowerRounding::Up).resolve(0), 0); + assert_eq!(YesNoMinimumVotingPower::Absolute(400).resolve(10), 400); + // No overflow on the largest total. + let half = YesNoMinimumVotingPower::FractionOfTotal { + numerator: 1, + denominator: 2, + rounding: VotingPowerRounding::Up, + }; + assert_eq!(half.resolve(u32::MAX), u32::MAX / 2 + 1); + } + + #[test] + fn should_judge_a_share_minimum_against_the_total_at_close() { + // Two thirds with at least 1/10 of the total cast, rounded up. + let poll = YesNoVotePoll { + minimum_voting_power: YesNoMinimumVotingPower::FractionOfTotal { + numerator: 1, + denominator: 10, + rounding: VotingPowerRounding::Up, + }, + ..poll() + }; + // A total of 3,801 needs 381 cast. + assert_eq!(poll.required_voting_power(3_801), 381); + assert!(!poll.passes(380, 0, 3_801)); + assert!(poll.passes(381, 0, 3_801)); + // The same tally fails once the list has grown. + assert!(!poll.passes(381, 0, 3_811)); + } + + #[test] + fn should_bound_the_minimum() { + let platform_version = PlatformVersion::latest(); + let max_voting_power = platform_version + .system_limits + .max_yes_no_vote_poll_minimum_voting_power; + let with_minimum = |minimum_voting_power| YesNoVotePoll { + minimum_voting_power, + ..poll() + }; + let share = |numerator, denominator| YesNoMinimumVotingPower::FractionOfTotal { + numerator, + denominator, + rounding: VotingPowerRounding::Up, + }; + for accepted in [ + YesNoMinimumVotingPower::Absolute(0), + YesNoMinimumVotingPower::Absolute(max_voting_power), + share(0, 1), + share(1, 3), + share(1, 2), + share(50, 100), + ] { + assert!( + with_minimum(accepted) + .validate_parameters(platform_version) + .is_ok(), + "{accepted} should be accepted" + ); + } + for refused in [ + YesNoMinimumVotingPower::Absolute(max_voting_power + 1), + share(1, 0), + share(51, 100), + share(2, 3), + ] { + assert!( + with_minimum(refused) + .validate_parameters(platform_version) + .is_err(), + "{refused} should be refused" + ); + } } #[test] @@ -226,8 +445,8 @@ mod tests { supermajority_denominator: 255, ..poll() }; - assert!(poll.passes(u32::MAX, 0)); - assert!(!poll.passes(u32::MAX, u32::MAX)); + assert!(poll.passes(u32::MAX, 0, 0)); + assert!(!poll.passes(u32::MAX, u32::MAX, 0)); } #[test] @@ -323,7 +542,7 @@ mod tests { ..poll() }; let other_minimum = YesNoVotePoll { - minimum_voting_power: 401, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(401), ..poll() }; for other in [other_path, other_rule, other_minimum] { @@ -366,7 +585,7 @@ mod json_convertible_tests { ], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: 400, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), } } @@ -382,13 +601,33 @@ mod json_convertible_tests { "resourcePath": ["wcHBwQ==", "Y2hhbGxlbmdl"], "supermajorityNumerator": 2, "supermajorityDenominator": 3, - "minimumVotingPower": 400, + "minimumVotingPower": { "absolute": 400 }, }) ); let recovered = YesNoVotePoll::from_json(json).expect("from_json"); assert_eq!(original, recovered); } + #[test] + fn should_round_trip_a_share_minimum_through_json() { + use crate::serialization::JsonConvertible; + let original = YesNoVotePoll { + minimum_voting_power: YesNoMinimumVotingPower::FractionOfTotal { + numerator: 1, + denominator: 3, + rounding: VotingPowerRounding::Up, + }, + ..fixture() + }; + let json = original.to_json().expect("to_json"); + assert_eq!( + json["minimumVotingPower"], + json!({ "fractionOfTotal": { "numerator": 1, "denominator": 3, "rounding": "up" } }) + ); + let recovered = YesNoVotePoll::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + #[test] fn value_round_trip_with_full_wire_shape() { use crate::serialization::ValueConvertible; @@ -403,7 +642,7 @@ mod json_convertible_tests { ], "supermajorityNumerator": 2u8, "supermajorityDenominator": 3u8, - "minimumVotingPower": 400u32, + "minimumVotingPower": { "absolute": 400u32 }, }) ); let recovered = YesNoVotePoll::from_object(value).expect("from_object"); diff --git a/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs b/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs index 79f80560d20..09b0bdf3ad4 100644 --- a/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs +++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs @@ -52,7 +52,7 @@ pub enum YesNoVote { mod json_convertible_tests_yes_no_vote { use super::*; use crate::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; - use crate::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; + use crate::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; use platform_value::{platform_value, BinaryData}; use serde_json::json; @@ -62,7 +62,7 @@ mod json_convertible_tests_yes_no_vote { resource_path: vec![BinaryData::new(vec![0xc1; 4])], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: 400, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), }, vote_choice: YesNoAbstainVoteChoice::Yes, }) @@ -81,7 +81,7 @@ mod json_convertible_tests_yes_no_vote { "resourcePath": ["wcHBwQ=="], "supermajorityNumerator": 2, "supermajorityDenominator": 3, - "minimumVotingPower": 400, + "minimumVotingPower": { "absolute": 400 }, }, "voteChoice": "yes", }) @@ -103,7 +103,7 @@ mod json_convertible_tests_yes_no_vote { "resourcePath": [platform_value::Value::Bytes(vec![0xc1; 4])], "supermajorityNumerator": 2u8, "supermajorityDenominator": 3u8, - "minimumVotingPower": 400u32, + "minimumVotingPower": { "absolute": 400u32 }, }, "voteChoice": "yes", }) diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs index 7bfe6d60a48..f91bccfe7dc 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs @@ -1,15 +1,17 @@ +use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; -use crate::platform_types::platform_state::PlatformState; +use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; +use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeType; use dpp::document::DocumentV0Getters; use dpp::prelude::TimestampMillis; use dpp::version::PlatformVersion; use dpp::voting::contender_structs::FinalizedContender; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice::TowardsIdentity; use dpp::voting::vote_info_storage::contested_document_vote_poll_winner_info::ContestedDocumentVotePollWinnerInfo; -use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use dpp::voting::vote_polls::yes_no_vote_poll::{VotingPower, YesNoVotePoll}; use drive::drive::votes::resolved::vote_polls::resolve::VotePollResolver; use drive::drive::votes::resolved::vote_polls::{ResolvedVotePoll, ResolvedVotePollWithVotes}; use drive::grovedb::TransactionArg; @@ -177,6 +179,7 @@ where } ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll) => self .finish_yes_no_vote_poll( + block_platform_state, block_info, yes_no_vote_poll, transaction, @@ -208,6 +211,7 @@ where /// voters so the clean-up can remove their votes. fn finish_yes_no_vote_poll( &self, + block_platform_state: &PlatformState, block_info: &BlockInfo, vote_poll: YesNoVotePoll, transaction: TransactionArg, @@ -215,10 +219,25 @@ where ) -> Result { let tally = self.tally_votes_for_yes_no_vote_poll(&vote_poll, transaction, platform_version)?; + // A minimum given as a share of the total is measured against the masternode list of + // the closing block, each masternode weighed as its vote is. + let total_voting_power = block_platform_state + .full_masternode_list() + .values() + .try_fold(0 as VotingPower, |total, masternode| { + total.checked_add(match masternode.node_type { + MasternodeType::Regular => 1, + MasternodeType::Evo => 4, + }) + }) + .ok_or(Error::Execution(ExecutionError::Overflow( + "the masternode list's total voting power overflows", + )))?; let result = self.keep_record_of_finished_yes_no_vote_poll( block_info, &vote_poll, &tally, + total_voting_power, transaction, platform_version, )?; diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs index a09380c7ac1..1d0665cb72e 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs @@ -5,7 +5,7 @@ use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; use dpp::version::PlatformVersion; use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollResult; -use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use dpp::voting::vote_polls::yes_no_vote_poll::{VotingPower, YesNoVotePoll}; use drive::grovedb::TransactionArg; use drive::query::yes_no_vote_poll_state_query::YesNoVotePollState; @@ -15,13 +15,15 @@ impl Platform where C: CoreRPCLike, { - /// Decides a finished yes/no poll from its tallies and writes the result into its stored - /// info, which stays as the record of the decision after the votes are cleaned up. + /// Decides a finished yes/no poll from its tallies and the total voting power of the + /// masternode list in the closing block, and writes the result into its stored info, which + /// stays as the record of the decision after the votes are cleaned up. pub(in crate::execution) fn keep_record_of_finished_yes_no_vote_poll( &self, block_info: &BlockInfo, vote_poll: &YesNoVotePoll, tally: &YesNoVotePollState, + total_voting_power: VotingPower, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result { @@ -35,6 +37,7 @@ where block_info, vote_poll, tally, + total_voting_power, transaction, platform_version, ), diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs index d9086142a8f..fb280c710b7 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs @@ -4,7 +4,7 @@ use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; use dpp::version::PlatformVersion; use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollResult; -use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use dpp::voting::vote_polls::yes_no_vote_poll::{VotingPower, YesNoVotePoll}; use drive::error::drive::DriveError; use drive::error::Error as DriveCrateError; use drive::grovedb::TransactionArg; @@ -20,6 +20,7 @@ where block_info: &BlockInfo, vote_poll: &YesNoVotePoll, tally: &YesNoVotePollState, + total_voting_power: VotingPower, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result { @@ -34,11 +35,17 @@ where )))?; // We perform an upgrade of the stored version just in case, most of the time this does nothing let mut stored_info = stored_info_from_disk.update_to_latest_version(platform_version)?; - let passed = vote_poll.passes(tally.yes_voting_power, tally.no_voting_power); + let required_voting_power = vote_poll.required_voting_power(total_voting_power); + let passed = vote_poll.passes( + tally.yes_voting_power, + tally.no_voting_power, + total_voting_power, + ); let result = stored_info.finalize_vote_poll( tally.yes_voting_power, tally.no_voting_power, tally.abstain_voting_power, + required_voting_power, passed, *block_info, )?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs index 23cbbcdc9bd..e70813e8c89 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs @@ -33,7 +33,9 @@ use dpp::version::PlatformVersion; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStatus; -use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use dpp::voting::vote_polls::yes_no_vote_poll::{ + VotingPowerRounding, YesNoMinimumVotingPower, YesNoVotePoll, +}; use dpp::voting::vote_polls::VotePoll; use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; use dpp::voting::votes::resource_vote::ResourceVote; @@ -59,7 +61,7 @@ fn two_thirds_poll(purpose: &[u8], minimum_voting_power: u32) -> YesNoVotePoll { ], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(minimum_voting_power), } } @@ -455,6 +457,91 @@ async fn should_fail_below_the_minimum_voting_power_even_when_every_vote_is_yes( assert_eq!(result.abstain_voting_power, 3); } +/// A minimum given as a share of the total is measured against the masternode list of the +/// closing block, evonodes weighing 4, and the bar it resolved to is kept in the result. +#[tokio::test] +async fn should_judge_a_share_minimum_against_the_masternode_list_at_close() { + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + let half_rounded_up = |purpose: &[u8]| YesNoVotePoll { + minimum_voting_power: YesNoMinimumVotingPower::FractionOfTotal { + numerator: 1, + denominator: 2, + rounding: VotingPowerRounding::Up, + }, + ..two_thirds_poll(purpose, 0) + }; + let evonode_alone = half_rounded_up(b"evonode alone"); + let evonode_and_one = half_rounded_up(b"evonode and one masternode"); + open_poll(&platform, &evonode_alone, FUND, platform_version); + open_poll(&platform, &evonode_and_one, FUND, platform_version); + + // One evonode and five masternodes: a total of 9, so half rounded up is 5. + let evonode = setup_voter(&mut platform, 100, true, platform_version); + let masternodes: Vec<_> = (101..106) + .map(|seed| setup_voter(&mut platform, seed, false, platform_version)) + .collect(); + let total: u32 = platform + .state + .load() + .full_masternode_list() + .values() + .map(|masternode| match masternode.node_type { + MasternodeType::Evo => 4, + MasternodeType::Regular => 1, + }) + .sum(); + assert_eq!(total, 9, "the list holds only the voters set up here"); + assert_eq!(masternodes.len(), 5); + + let (pro_tx_hash, signer, voting_key) = &evonode; + for (nonce, vote_poll) in [(1, &evonode_alone), (2, &evonode_and_one)] { + perform_yes_no_vote( + &mut platform, + vote_poll, + YesNoAbstainVoteChoice::Yes, + signer, + *pro_tx_hash, + voting_key, + nonce, + platform_version, + ) + .await + .expect("expected the evonode's vote to be accepted"); + } + let (pro_tx_hash, signer, voting_key) = &masternodes[0]; + perform_yes_no_vote( + &mut platform, + &evonode_and_one, + YesNoAbstainVoteChoice::Yes, + signer, + *pro_tx_hash, + voting_key, + 1, + platform_version, + ) + .await + .expect("expected the masternode's vote to be accepted"); + + close_ended_polls(&mut platform, platform_version); + + let result_of = |vote_poll: &YesNoVotePoll| { + *poll_state(&platform, vote_poll, platform_version) + .stored_info + .expect("stored info") + .result() + .expect("expected the poll to be finished") + }; + let alone = result_of(&evonode_alone); + assert_eq!(alone.yes_voting_power, 4); + assert_eq!(alone.required_voting_power, 5); + assert!(!alone.passed); + let with_one = result_of(&evonode_and_one); + assert_eq!(with_one.yes_voting_power, 5); + assert_eq!(with_one.required_voting_power, 5); + assert!(with_one.passed); +} + #[tokio::test] async fn should_count_an_evonode_vote_as_four() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs index 6ea1708b63c..b50fbb850f7 100644 --- a/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs +++ b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs @@ -188,13 +188,14 @@ mod tests { use dpp::block::block_info::BlockInfo; use dpp::platform_value::BinaryData; use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStatus; + use dpp::voting::vote_polls::yes_no_vote_poll::YesNoMinimumVotingPower; fn poll() -> YesNoVotePoll { YesNoVotePoll { resource_path: vec![BinaryData::new(vec![0xc1; 32])], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: 400, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), } } diff --git a/packages/rs-drive/src/structure/tests.rs b/packages/rs-drive/src/structure/tests.rs index 39a0093f79c..02de0e36579 100644 --- a/packages/rs-drive/src/structure/tests.rs +++ b/packages/rs-drive/src/structure/tests.rs @@ -355,7 +355,7 @@ mod fixtures { use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_info_storage::contested_document_vote_poll_stored_info::ContestedDocumentVotePollStoredInfo; - use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; + use dpp::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; use rand::rngs::StdRng; use rand::SeedableRng; use std::collections::{BTreeMap, BTreeSet}; @@ -1206,7 +1206,7 @@ mod fixtures { resource_path: vec![BinaryData::new(vec![0xc1; 32])], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: 400, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), }; drive .open_yes_no_vote_poll( diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 63b562d7812..86bc3853cec 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -4832,7 +4832,7 @@ mod tests { use dpp::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; - use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; + use dpp::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; use dpp::voting::votes::yes_no_vote::YesNoVote; use dpp::voting::votes::Vote; @@ -4843,7 +4843,7 @@ mod tests { resource_path: vec![BinaryData::new(vec![0xc1; 32])], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: 400, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), }; drive .open_yes_no_vote_poll( diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs index 583c83465e1..ab9e09ccf36 100644 --- a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs @@ -52,7 +52,7 @@ mod tests { use dpp::block::block_info::BlockInfo; use dpp::platform_value::{BinaryData, Identifier}; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; - use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; + use dpp::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; use dpp::voting::vote_polls::VotePoll; use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; @@ -61,7 +61,7 @@ mod tests { resource_path: vec![BinaryData::new(vec![0xc1; 32])], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: 400, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), } } diff --git a/packages/rs-platform-version/src/version/mocks/v2_test.rs b/packages/rs-platform-version/src/version/mocks/v2_test.rs index ddb161e4e15..1c97dde0eef 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -592,6 +592,8 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { max_contract_moderation_reason_length: 1024, max_yes_no_vote_poll_resource_path_segments: 16, max_yes_no_vote_poll_resource_path_bytes: 1024, + max_yes_no_vote_poll_minimum_voting_power: 2000, + max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_contract_moderation_election_window_seconds: 86_400, diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index 876cad3a1ab..68e8ffd261c 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -142,6 +142,12 @@ pub struct SystemLimits { /// The most bytes a yes/no vote poll's resource path may hold across its segments, read by /// `YesNoVotePoll::validate_parameters` (protocol version 14). Every vote carries the poll. pub max_yes_no_vote_poll_resource_path_bytes: u16, + /// The largest fixed minimum voting power a yes/no vote poll may require, read by + /// `YesNoVotePoll::validate_parameters` (protocol version 14). + pub max_yes_no_vote_poll_minimum_voting_power: u32, + /// The largest share of the total voting power, in percent, a yes/no vote poll's minimum may + /// require, read by `YesNoVotePoll::validate_parameters` (protocol version 14). + pub max_yes_no_vote_poll_minimum_voting_power_percent_of_total: u8, /// Maximum number of warnings one identity may carry on a contract's warning list at a /// time: a warn that would exceed it is refused until the warnings are cleared. Read by /// the `ContractUserModeration` state validation v0 (protocol version 14) and never diff --git a/packages/rs-platform-version/src/version/system_limits/v1.rs b/packages/rs-platform-version/src/version/system_limits/v1.rs index 1a63e357216..26aee941d9c 100644 --- a/packages/rs-platform-version/src/version/system_limits/v1.rs +++ b/packages/rs-platform-version/src/version/system_limits/v1.rs @@ -53,6 +53,8 @@ pub const SYSTEM_LIMITS_V1: SystemLimits = SystemLimits { max_contract_moderation_reason_length: 1024, max_yes_no_vote_poll_resource_path_segments: 16, max_yes_no_vote_poll_resource_path_bytes: 1024, + max_yes_no_vote_poll_minimum_voting_power: 2000, + max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/system_limits/v2.rs b/packages/rs-platform-version/src/version/system_limits/v2.rs index 8a5a850433c..43b1b31420a 100644 --- a/packages/rs-platform-version/src/version/system_limits/v2.rs +++ b/packages/rs-platform-version/src/version/system_limits/v2.rs @@ -34,6 +34,8 @@ pub const SYSTEM_LIMITS_V2: SystemLimits = SystemLimits { max_contract_moderation_reason_length: 1024, max_yes_no_vote_poll_resource_path_segments: 16, max_yes_no_vote_poll_resource_path_bytes: 1024, + max_yes_no_vote_poll_minimum_voting_power: 2000, + max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/system_limits/v3.rs b/packages/rs-platform-version/src/version/system_limits/v3.rs index 88a6bfc3214..387c03808f9 100644 --- a/packages/rs-platform-version/src/version/system_limits/v3.rs +++ b/packages/rs-platform-version/src/version/system_limits/v3.rs @@ -36,6 +36,8 @@ pub const SYSTEM_LIMITS_V3: SystemLimits = SystemLimits { max_contract_moderation_reason_length: 1024, max_yes_no_vote_poll_resource_path_segments: 16, max_yes_no_vote_poll_resource_path_bytes: 1024, + max_yes_no_vote_poll_minimum_voting_power: 2000, + max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/system_limits/v4.rs b/packages/rs-platform-version/src/version/system_limits/v4.rs index 11808387759..f0d22df3d5d 100644 --- a/packages/rs-platform-version/src/version/system_limits/v4.rs +++ b/packages/rs-platform-version/src/version/system_limits/v4.rs @@ -88,6 +88,8 @@ pub const SYSTEM_LIMITS_V4: SystemLimits = SystemLimits { max_contract_moderation_reason_length: 1024, max_yes_no_vote_poll_resource_path_segments: 16, max_yes_no_vote_poll_resource_path_bytes: 1024, + max_yes_no_vote_poll_minimum_voting_power: 2000, + max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/wasm-dpp2/src/voting/vote_poll.rs b/packages/wasm-dpp2/src/voting/vote_poll.rs index 30d9523e7e5..f51ec3d5e04 100644 --- a/packages/wasm-dpp2/src/voting/vote_poll.rs +++ b/packages/wasm-dpp2/src/voting/vote_poll.rs @@ -30,13 +30,22 @@ export interface VotePollOptions { /** * A yes/no poll's fields as a plain object. A yes/no vote carries its poll - * like this, without a `$type` tag. + * like this, without a `$type` tag. The minimum is a fixed voting power or a + * share of the masternode list's total voting power when the poll closes. */ export interface YesNoVotePollFieldsObject { resourcePath: Uint8Array[]; supermajorityNumerator: number; supermajorityDenominator: number; - minimumVotingPower: number; + minimumVotingPower: + | { absolute: number } + | { + fractionOfTotal: { + numerator: number; + denominator: number; + rounding: "up" | "down"; + }; + }; } /** @@ -46,7 +55,15 @@ export interface YesNoVotePollFieldsJSON { resourcePath: string[]; supermajorityNumerator: number; supermajorityDenominator: number; - minimumVotingPower: number; + minimumVotingPower: + | { absolute: number } + | { + fractionOfTotal: { + numerator: number; + denominator: number; + rounding: "up" | "down"; + }; + }; } /** From 49df27f9fca040a6e9d52aad38e28cdcb57a7496 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 23 Sep 2026 20:28:56 +0700 Subject: [PATCH 05/18] feat(dpp): round a yes/no poll's share minimum down plus one or up minus 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 --- .../voting/vote_polls/yes_no_vote_poll/mod.rs | 57 ++++++++++++++++++- packages/wasm-dpp2/src/voting/vote_poll.rs | 4 +- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs index f8a2700b9a7..452067b6d6b 100644 --- a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs @@ -36,10 +36,15 @@ const YES_NO_VOTE_POLL_ID_DOMAIN: &[u8] = b"dash platform yes/no vote poll id"; serde(rename_all = "camelCase") )] pub enum VotingPowerRounding { - /// Toward zero: 1/3 of 10 is 3. + /// Toward zero: 1/3 of 10 is 3, 1/3 of 9 is 3. Down, - /// Away from zero: 1/3 of 10 is 4. + /// Away from zero: 1/3 of 10 is 4, 1/3 of 9 is 3. Up, + /// Rounded down, plus one: 1/3 of 10 is 4, 1/3 of 9 is 4. Half the total this way is a + /// strict majority of it. + DownPlusOne, + /// Rounded up, minus one, never below zero: 1/3 of 10 is 3, 1/3 of 9 is 2. + UpMinusOne, } /// The least yes plus no voting power a yes/no poll needs to pass. @@ -83,6 +88,10 @@ impl YesNoMinimumVotingPower { let resolved = match rounding { VotingPowerRounding::Down => share / denominator, VotingPowerRounding::Up => share.div_ceil(denominator), + VotingPowerRounding::DownPlusOne => share / denominator + 1, + VotingPowerRounding::UpMinusOne => { + share.div_ceil(denominator).saturating_sub(1) + } }; VotingPower::try_from(resolved).unwrap_or(VotingPower::MAX) } @@ -106,6 +115,8 @@ impl fmt::Display for YesNoMinimumVotingPower { match rounding { VotingPowerRounding::Down => "down", VotingPowerRounding::Up => "up", + VotingPowerRounding::DownPlusOne => "down plus one", + VotingPowerRounding::UpMinusOne => "up minus one", } ), } @@ -364,6 +375,22 @@ mod tests { assert_eq!(third(VotingPowerRounding::Down).resolve(9), 3); assert_eq!(third(VotingPowerRounding::Up).resolve(9), 3); assert_eq!(third(VotingPowerRounding::Up).resolve(0), 0); + // Rounded down plus one, and rounded up minus one (never below zero). + assert_eq!(third(VotingPowerRounding::DownPlusOne).resolve(10), 4); + assert_eq!(third(VotingPowerRounding::DownPlusOne).resolve(9), 4); + assert_eq!(third(VotingPowerRounding::DownPlusOne).resolve(0), 1); + assert_eq!(third(VotingPowerRounding::UpMinusOne).resolve(10), 3); + assert_eq!(third(VotingPowerRounding::UpMinusOne).resolve(9), 2); + assert_eq!(third(VotingPowerRounding::UpMinusOne).resolve(0), 0); + // Half rounded down plus one is a strict majority. + let majority = YesNoMinimumVotingPower::FractionOfTotal { + numerator: 1, + denominator: 2, + rounding: VotingPowerRounding::DownPlusOne, + }; + assert_eq!(majority.resolve(10), 6); + assert_eq!(majority.resolve(9), 5); + assert_eq!(majority.resolve(u32::MAX), u32::MAX / 2 + 1); assert_eq!(YesNoMinimumVotingPower::Absolute(400).resolve(10), 400); // No overflow on the largest total. let half = YesNoMinimumVotingPower::FractionOfTotal { @@ -628,6 +655,32 @@ mod json_convertible_tests { assert_eq!(original, recovered); } + #[test] + fn should_name_every_rounding_in_camel_case() { + use crate::serialization::JsonConvertible; + for (rounding, name) in [ + (VotingPowerRounding::Down, "down"), + (VotingPowerRounding::Up, "up"), + (VotingPowerRounding::DownPlusOne, "downPlusOne"), + (VotingPowerRounding::UpMinusOne, "upMinusOne"), + ] { + let original = YesNoVotePoll { + minimum_voting_power: YesNoMinimumVotingPower::FractionOfTotal { + numerator: 1, + denominator: 2, + rounding, + }, + ..fixture() + }; + let json = original.to_json().expect("to_json"); + assert_eq!( + json["minimumVotingPower"]["fractionOfTotal"]["rounding"], + json!(name) + ); + assert_eq!(YesNoVotePoll::from_json(json).expect("from_json"), original); + } + } + #[test] fn value_round_trip_with_full_wire_shape() { use crate::serialization::ValueConvertible; diff --git a/packages/wasm-dpp2/src/voting/vote_poll.rs b/packages/wasm-dpp2/src/voting/vote_poll.rs index f51ec3d5e04..65e502cea58 100644 --- a/packages/wasm-dpp2/src/voting/vote_poll.rs +++ b/packages/wasm-dpp2/src/voting/vote_poll.rs @@ -43,7 +43,7 @@ export interface YesNoVotePollFieldsObject { fractionOfTotal: { numerator: number; denominator: number; - rounding: "up" | "down"; + rounding: "up" | "down" | "downPlusOne" | "upMinusOne"; }; }; } @@ -61,7 +61,7 @@ export interface YesNoVotePollFieldsJSON { fractionOfTotal: { numerator: number; denominator: number; - rounding: "up" | "down"; + rounding: "up" | "down" | "downPlusOne" | "upMinusOne"; }; }; } From d6f5d96dab6d46779805ed5e4a6744cadf22f618 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 21 Oct 2026 21:04:02 +0300 Subject: [PATCH 06/18] fix(platform): apply the yes/no vote poll review findings Closing polls: - contested clean-up v2 and the kind-agnostic end date pre-pass are gone: contested polls keep clean-up v1 at protocol version 15, and the yes/no clean-up removes its own polls from the shared end date index. The kind- agnostic removal reuses the body of the contested end date removal v2 (moved, unchanged, into remove_vote_poll_ids_from_end_dates_operations_v2), and the yes/no clean-up drains its funds through the contested clean-up v1 drain (moved, unchanged, into empty_vote_poll_prefunded_balances_operations_v1). - the three vote sum trees of a closed poll are dropped flat instead of deleting every vote first. - check_for_ended_vote_polls v1 closes a yes/no poll before resolving, so ResolvedVotePoll is contested-only again: the in-place arms in check_for_ended_vote_polls v0, register_identity_vote v0 and masternode vote state v0/v1 for a resolved yes/no poll are gone, and ResolvedVote converts to Vote infallibly again. - the closing total is folded only for a share minimum, with one masternode_vote_strength helper shared with the vote strength. Voting and opening: - transform_into_action v1, register_identity_vote v1 and remove_all_votes_given_by_identities v1 hand resource votes to their v0. - register_identity_vote dispatches on insert.register_identity_vote (0 up to protocol version 14), which DRIVE_VOTE_METHOD_VERSIONS_V4 sets to 1. - contested_vote_poll_for_document returns the contested poll type, which removes the unreachable yes/no arm from document create advanced structure v1. - opening a poll refuses one the same batch already opens, and creates the decisions trees on a chain that reached protocol version 15 without them. Clients and docs: - the minimum voting power serializes internally tagged: { "$type": "absolute", "votingPower": 400 }. - the wasm bindings no longer refuse a yes/no poll in a resource vote (validation stays in consensus); wasm-dpp2 specs cover the yes/no shapes. - get_current_dpns_contests pages until an empty page, never asks for a range starting past the end time, and refuses rather than drops an end date holding more polls than a page; a duplicate yes/no vote broadcast waits for its result. - the book's contested storage section covers yes/no polls in the index. Tests: the yes/no test helper asserts CheckTx's verdict both ways, end dates are read with vote_poll_end_dates, each close is its own block, the share minimum is judged against a closing block list that differs from the committed one, an unfunded poll closes, the leftover fund reaches the processing pool, the contested half of a shared end date is checked, and the end date clean-up tests also run at protocol version 14. Co-Authored-By: Claude Opus 5.5 --- book/src/data-model/contested-documents.md | 18 +- .../document_type/methods/mod.rs | 6 +- .../methods/versioned_methods.rs | 19 +- .../voting/vote_polls/yes_no_vote_poll/mod.rs | 117 +++++- .../src/voting/votes/yes_no_vote/mod.rs | 4 +- .../check_for_ended_vote_polls/v0/mod.rs | 21 - .../check_for_ended_vote_polls/v1/mod.rs | 59 +-- .../mod.rs | 10 +- .../v1/mod.rs | 52 ++- .../v2/mod.rs | 203 ---------- .../clean_up_after_vote_polls_end/v1/mod.rs | 31 +- .../mod.rs | 11 +- .../v0/mod.rs | 67 ++-- .../execution/platform_events/voting/mod.rs | 12 + .../advanced_structure_v1/mod.rs | 17 +- .../masternode_vote/end_date_cleanup_tests.rs | 38 +- .../no_locking_contest_tests.rs | 49 ++- .../masternode_vote/state/v0/mod.rs | 11 +- .../masternode_vote/state/v1/mod.rs | 12 - .../transform_into_action/v1/mod.rs | 247 ++++-------- .../masternode_vote/yes_no_tests.rs | 358 +++++++++++------- .../verify_state_transitions.rs | 9 +- .../v1/mod.rs | 159 ++------ .../v2/mod.rs | 37 +- .../mod.rs | 8 +- .../v0/mod.rs | 80 +--- .../mod.rs | 13 +- .../v0/mod.rs | 67 +--- .../insert/register_identity_vote/mod.rs | 4 +- .../insert/register_identity_vote/v0/mod.rs | 20 +- .../insert/register_identity_vote/v1/mod.rs | 67 +--- .../yes_no/open_yes_no_vote_poll/mod.rs | 97 +++++ .../yes_no/open_yes_no_vote_poll/v0/mod.rs | 67 +++- .../drive/votes/resolved/vote_polls/mod.rs | 11 - .../votes/resolved/vote_polls/resolve.rs | 19 +- .../src/drive/votes/resolved/votes/mod.rs | 21 +- .../drive_abci_method_versions/v11.rs | 8 +- .../drive_vote_method_versions/v4.rs | 11 +- .../src/version/drive_versions/v10.rs | 6 +- .../rs-platform-version/src/version/v15.rs | 10 +- .../src/platform/documents/contest_fund.rs | 3 +- .../dpns_usernames/contested_queries.rs | 194 +++++++--- .../rs-sdk/src/platform/transition/vote.rs | 35 +- .../wasm-dpp2/src/voting/resource_vote.rs | 19 +- packages/wasm-dpp2/src/voting/vote.rs | 26 +- packages/wasm-dpp2/src/voting/vote_poll.rs | 22 +- packages/wasm-dpp2/tests/unit/Vote.spec.ts | 61 +++ .../wasm-dpp2/tests/unit/VotePoll.spec.ts | 70 ++++ .../src/state_transitions/identity.rs | 7 - 49 files changed, 1271 insertions(+), 1242 deletions(-) delete mode 100644 packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v2/mod.rs diff --git a/book/src/data-model/contested-documents.md b/book/src/data-model/contested-documents.md index 11b29c27a69..d9f10347eba 100644 --- a/book/src/data-model/contested-documents.md +++ b/book/src/data-model/contested-documents.md @@ -116,13 +116,17 @@ which holds its contenders, stored result and tallies, is a count tree, so a joi contenders there are in one fetch; a contest started before 14 keeps its plain tree, and a join counts its contenders by reading their keys. The masternodes' vote references live under `votes / contested_resource / identity_votes`, and the end dates under `votes / end_date_queries`, -one tree per end date holding an entry for each contest ending then. +one tree per end date holding an entry for each vote poll ending then. From protocol version 15 +that index also lists yes/no vote polls (the masternode polls that ask "should this happen?", +stored under `votes / decisions`), so an end date can hold contests and yes/no polls side by side. Once the contest ends, the winning document is awarded, the losers are removed, and the stored result stays for the `getContestedResourceVoteState` query. -A block ends at most `maximum_vote_polls_to_process` contests (a drive-abci `event_constants` -value, two at protocol version 14), the earliest end date first, so contests due together may end -over several blocks. The cleanup removes each ended contest's -end-date entry, and removes an end date only once none of its contests remain under it; the rest -end in the next blocks. Before that read, a block removes any end date among the first due that -holds no contest, since each would take a place in the read and end nothing. +A block ends at most `maximum_vote_polls_to_process` vote polls (a drive-abci `event_constants` +value, two at protocol version 14), the earliest end date first, so polls due together may end +over several blocks; from protocol version 15 yes/no polls count toward the same limit, so a +yes/no poll due earlier can push a contest to the next block. The cleanup removes each ended +poll's end-date entry, and removes an end date only once none of its polls, of either kind, +remain under it; the rest end in the next blocks. Before that read, a block removes any end date +among the first due that holds no vote poll, since each would take a place in the read and end +nothing. diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs index 66f9ea9084b..470361285da 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs @@ -30,7 +30,7 @@ use crate::data_contract::document_type::DocumentPropertyType; #[cfg(feature = "validation")] use crate::data_contract::document_type::StringPropertySizes; use crate::fee::Credits; -use crate::voting::vote_polls::VotePoll; +use crate::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; use platform_value::btreemap_extensions::{ BTreeValueMapInsertionPathHelper, BTreeValueMapPathHelper, }; @@ -1046,7 +1046,7 @@ pub trait DocumentTypeV0Methods: DocumentTypeV0Getters + DocumentTypeV0MethodsVe &self, document: &Document, platform_version: &PlatformVersion, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { match platform_version .dpp .contract_versions @@ -1067,7 +1067,7 @@ pub trait DocumentTypeV0Methods: DocumentTypeV0Getters + DocumentTypeV0MethodsVe &self, document_properties: &BTreeMap, platform_version: &PlatformVersion, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { match platform_version .dpp .contract_versions diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs b/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs index 00d288ad68c..f694f8213af 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs @@ -29,7 +29,6 @@ use crate::validation::SimpleConsensusValidationResult; use crate::voting::vote_polls::contested_document_resource_vote_poll::{ required_vote_resolution_fund, ContestedDocumentResourceVotePoll, }; -use crate::voting::vote_polls::VotePoll; use crate::ProtocolError; use chrono::Utc; use itertools::Itertools; @@ -377,7 +376,7 @@ pub trait DocumentTypeV0MethodsVersioned: DocumentTypeV0Getters + DocumentTypeBa &self, document: &Document, platform_version: &PlatformVersion, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { self.contested_vote_poll_for_document_properties_v0(document.properties(), platform_version) } @@ -385,7 +384,7 @@ pub trait DocumentTypeV0MethodsVersioned: DocumentTypeV0Getters + DocumentTypeBa &self, document_properties: &BTreeMap, platform_version: &PlatformVersion, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { self.indexes() .values() .find(|index| { @@ -417,14 +416,12 @@ pub trait DocumentTypeV0MethodsVersioned: DocumentTypeV0Getters + DocumentTypeBa self.flattened_properties(), platform_version, )?; - Ok(VotePoll::ContestedDocumentResourceVotePoll( - ContestedDocumentResourceVotePoll { - contract_id: self.data_contract_id(), - document_type_name: self.name().clone(), - index_name: index.name.clone(), - index_values, - }, - )) + Ok(ContestedDocumentResourceVotePoll { + contract_id: self.data_contract_id(), + document_type_name: self.name().clone(), + index_name: index.name.clone(), + index_values, + }) }) .transpose() } diff --git a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs index 452067b6d6b..8d89e01b25c 100644 --- a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs @@ -48,19 +48,17 @@ pub enum VotingPowerRounding { } /// The least yes plus no voting power a yes/no poll needs to pass. +/// +/// Its JSON and Value shape is internally tagged, as every sum type's is: +/// `{ "$type": "absolute", "votingPower": 400 }` or +/// `{ "$type": "fractionOfTotal", "numerator": 1, "denominator": 3, "rounding": "up" }`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, DecodeUntrusted)] -#[cfg_attr( - feature = "serde-conversion", - derive(Serialize, Deserialize), - serde(rename_all = "camelCase") -)] pub enum YesNoMinimumVotingPower { /// A fixed voting power, at most `max_yes_no_vote_poll_minimum_voting_power`. Absolute(VotingPower), /// A share of the total voting power of the masternode list (masternode 1, evonode 4) in /// the block that closes the poll, rounded as asked, at most /// `max_yes_no_vote_poll_minimum_voting_power_percent_of_total` percent of it. - #[cfg_attr(feature = "serde-conversion", serde(rename_all = "camelCase"))] FractionOfTotal { numerator: u8, denominator: u8, @@ -68,6 +66,102 @@ pub enum YesNoMinimumVotingPower { }, } +// `Absolute` wraps a bare number, which serde's internal tagging cannot carry, so both +// directions are written out, as for `ResourceVoteChoice`. +#[cfg(feature = "serde-conversion")] +impl Serialize for YesNoMinimumVotingPower { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeMap; + match self { + YesNoMinimumVotingPower::Absolute(voting_power) => { + let mut map = serializer.serialize_map(Some(2))?; + map.serialize_entry("$type", "absolute")?; + map.serialize_entry("votingPower", voting_power)?; + map.end() + } + YesNoMinimumVotingPower::FractionOfTotal { + numerator, + denominator, + rounding, + } => { + let mut map = serializer.serialize_map(Some(4))?; + map.serialize_entry("$type", "fractionOfTotal")?; + map.serialize_entry("numerator", numerator)?; + map.serialize_entry("denominator", denominator)?; + map.serialize_entry("rounding", rounding)?; + map.end() + } + } + } +} + +#[cfg(feature = "serde-conversion")] +impl<'de> Deserialize<'de> for YesNoMinimumVotingPower { + fn deserialize>(deserializer: D) -> Result { + use serde::de::{self, MapAccess, Visitor}; + + struct MinimumVisitor; + + impl<'de> Visitor<'de> for MinimumVisitor { + type Value = YesNoMinimumVotingPower; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("YesNoMinimumVotingPower as a map with a `$type` discriminator") + } + + fn visit_map>(self, mut map: A) -> Result { + let mut variant: Option = None; + let mut voting_power: Option = None; + let mut numerator: Option = None; + let mut denominator: Option = None; + let mut rounding: Option = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "$type" if variant.is_none() => variant = Some(map.next_value()?), + "votingPower" if voting_power.is_none() => { + voting_power = Some(map.next_value()?) + } + "numerator" if numerator.is_none() => numerator = Some(map.next_value()?), + "denominator" if denominator.is_none() => { + denominator = Some(map.next_value()?) + } + "rounding" if rounding.is_none() => rounding = Some(map.next_value()?), + "$type" | "votingPower" | "numerator" | "denominator" | "rounding" => { + return Err(de::Error::custom(format!("duplicate field `{key}`"))); + } + _ => { + let _: de::IgnoredAny = map.next_value()?; + } + } + } + + match variant + .ok_or_else(|| de::Error::missing_field("$type"))? + .as_str() + { + "absolute" => Ok(YesNoMinimumVotingPower::Absolute( + voting_power.ok_or_else(|| de::Error::missing_field("votingPower"))?, + )), + "fractionOfTotal" => Ok(YesNoMinimumVotingPower::FractionOfTotal { + numerator: numerator + .ok_or_else(|| de::Error::missing_field("numerator"))?, + denominator: denominator + .ok_or_else(|| de::Error::missing_field("denominator"))?, + rounding: rounding.ok_or_else(|| de::Error::missing_field("rounding"))?, + }), + other => Err(de::Error::unknown_variant( + other, + &["absolute", "fractionOfTotal"], + )), + } + } + } + + deserializer.deserialize_map(MinimumVisitor) + } +} + impl YesNoMinimumVotingPower { /// The minimum as a voting power, given the total voting power of the masternode list when /// the poll closes. A fraction with a zero denominator (refused when the poll opens) needs @@ -628,7 +722,7 @@ mod json_convertible_tests { "resourcePath": ["wcHBwQ==", "Y2hhbGxlbmdl"], "supermajorityNumerator": 2, "supermajorityDenominator": 3, - "minimumVotingPower": { "absolute": 400 }, + "minimumVotingPower": { "$type": "absolute", "votingPower": 400 }, }) ); let recovered = YesNoVotePoll::from_json(json).expect("from_json"); @@ -649,7 +743,7 @@ mod json_convertible_tests { let json = original.to_json().expect("to_json"); assert_eq!( json["minimumVotingPower"], - json!({ "fractionOfTotal": { "numerator": 1, "denominator": 3, "rounding": "up" } }) + json!({ "$type": "fractionOfTotal", "numerator": 1, "denominator": 3, "rounding": "up" }) ); let recovered = YesNoVotePoll::from_json(json).expect("from_json"); assert_eq!(original, recovered); @@ -673,10 +767,7 @@ mod json_convertible_tests { ..fixture() }; let json = original.to_json().expect("to_json"); - assert_eq!( - json["minimumVotingPower"]["fractionOfTotal"]["rounding"], - json!(name) - ); + assert_eq!(json["minimumVotingPower"]["rounding"], json!(name)); assert_eq!(YesNoVotePoll::from_json(json).expect("from_json"), original); } } @@ -695,7 +786,7 @@ mod json_convertible_tests { ], "supermajorityNumerator": 2u8, "supermajorityDenominator": 3u8, - "minimumVotingPower": { "absolute": 400u32 }, + "minimumVotingPower": { "$type": "absolute", "votingPower": 400u32 }, }) ); let recovered = YesNoVotePoll::from_object(value).expect("from_object"); diff --git a/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs b/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs index 98de0d651cc..c2009ee1838 100644 --- a/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs +++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs @@ -81,7 +81,7 @@ mod json_convertible_tests_yes_no_vote { "resourcePath": ["wcHBwQ=="], "supermajorityNumerator": 2, "supermajorityDenominator": 3, - "minimumVotingPower": { "absolute": 400 }, + "minimumVotingPower": { "$type": "absolute", "votingPower": 400 }, }, "voteChoice": "yes", }) @@ -103,7 +103,7 @@ mod json_convertible_tests_yes_no_vote { "resourcePath": [platform_value::Value::Bytes(vec![0xc1; 4])], "supermajorityNumerator": 2u8, "supermajorityDenominator": 3u8, - "minimumVotingPower": { "absolute": 400u32 }, + "minimumVotingPower": { "$type": "absolute", "votingPower": 400u32 }, }, "voteChoice": "yes", }) diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs index 044d7b2a7e9..94d8ab0c3fb 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v0/mod.rs @@ -1,4 +1,3 @@ -use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::PlatformState; @@ -176,16 +175,6 @@ where identifiers_voting_for_contenders, )) } - // Unreachable: this v0 is selected up to protocol version 13, and a - // yes/no poll enters the end date index only through - // `Drive::open_yes_no_vote_poll`, which writes under the `Votes/d/p` - // tree that exists from 15 (genesis structure v4 behind `>= 15`, - // `transition_to_version_15`). - ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Execution( - ExecutionError::CorruptedCodeExecution( - "yes/no vote polls need check_for_ended_vote_polls version 1", - ), - )), } }).collect::, Error>>()?; Ok((end_date, vote_polls_with_votes)) @@ -311,16 +300,6 @@ where )?; Ok(ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(resolved_contested_document_resource_vote_poll, identifiers_voting_for_contenders)) } - // Unreachable: this v0 is selected up to protocol version 13, and a - // yes/no poll enters the end date index only through - // `Drive::open_yes_no_vote_poll`, which writes under the `Votes/d/p` - // tree that exists from 15 (genesis structure v4 behind `>= 15`, - // `transition_to_version_15`). - ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Execution( - ExecutionError::CorruptedCodeExecution( - "yes/no vote polls need check_for_ended_vote_polls version 1", - ), - )), } }).collect::, Error>>()?; Ok((end_date, vote_polls_with_votes)) diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs index 79da2fb7f55..71c36488002 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs @@ -1,10 +1,10 @@ use crate::error::execution::ExecutionError; use crate::error::Error; +use crate::execution::platform_events::voting::masternode_vote_strength; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; -use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeType; use dpp::data_contract::document_type::ContestedIndexResolution; use dpp::document::DocumentV0Getters; use dpp::prelude::TimestampMillis; @@ -12,7 +12,10 @@ use dpp::version::PlatformVersion; use dpp::voting::contender_structs::FinalizedContender; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice::TowardsIdentity; use dpp::voting::vote_info_storage::contested_document_vote_poll_winner_info::ContestedDocumentVotePollWinnerInfo; -use dpp::voting::vote_polls::yes_no_vote_poll::{VotingPower, YesNoVotePoll}; +use dpp::voting::vote_polls::yes_no_vote_poll::{ + VotingPower, YesNoMinimumVotingPower, YesNoVotePoll, +}; +use dpp::voting::vote_polls::VotePoll; use drive::drive::votes::paths::vote_end_date_queries_tree_path_vec; use drive::drive::votes::resolved::vote_polls::resolve::VotePollResolver; use drive::drive::votes::resolved::vote_polls::{ResolvedVotePoll, ResolvedVotePollWithVotes}; @@ -80,6 +83,20 @@ where // Check if awarding is disabled let vote_polls_with_info = vote_polls_by_timestamp.into_iter().map(|(end_date, vote_polls)| { let vote_polls_with_votes = vote_polls.into_iter().map(|vote_poll| { + // Reached from protocol version 15 only. At 14, which also selects this + // generation, no yes/no poll is in the end date index: it enters only + // through `Drive::open_yes_no_vote_poll`, which writes under the `Votes/d/p` + // tree that exists from 15 (genesis structure v4 behind `>= 15`, + // `transition_to_version_15`). A yes/no poll needs no resolution. + if let VotePoll::YesNoVotePoll(yes_no_vote_poll) = vote_poll { + return self.finish_yes_no_vote_poll( + block_platform_state, + block_info, + yes_no_vote_poll, + transaction, + platform_version, + ); + } let resolved_vote_poll = vote_poll.resolve(&self.drive, transaction, platform_version)?; match resolved_vote_poll { @@ -211,19 +228,6 @@ where )?; Ok(ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(resolved_contested_document_resource_vote_poll, identifiers_voting_for_contenders)) } - // Reached from protocol version 15 only. At 14, which also selects - // this generation, no yes/no poll is in the end date index: it enters - // only through `Drive::open_yes_no_vote_poll`, which writes under the - // `Votes/d/p` tree that exists from 15 (genesis structure v4 behind - // `>= 15`, `transition_to_version_15`). - ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll) => self - .finish_yes_no_vote_poll( - block_platform_state, - block_info, - yes_no_vote_poll, - transaction, - platform_version, - ), } }).collect::, Error>>()?; Ok((end_date, vote_polls_with_votes)) @@ -259,19 +263,20 @@ where let tally = self.tally_votes_for_yes_no_vote_poll(&vote_poll, transaction, platform_version)?; // A minimum given as a share of the total is measured against the masternode list of - // the closing block, each masternode weighed as its vote is. - let total_voting_power = block_platform_state - .full_masternode_list() - .values() - .try_fold(0 as VotingPower, |total, masternode| { - total.checked_add(match masternode.node_type { - MasternodeType::Regular => 1, - MasternodeType::Evo => 4, + // the closing block, each masternode weighed as its vote is. A fixed minimum reads no + // total. + let total_voting_power = match vote_poll.minimum_voting_power { + YesNoMinimumVotingPower::Absolute(_) => 0, + YesNoMinimumVotingPower::FractionOfTotal { .. } => block_platform_state + .full_masternode_list() + .values() + .try_fold(0 as VotingPower, |total, masternode| { + total.checked_add(masternode_vote_strength(&masternode.node_type).into()) }) - }) - .ok_or(Error::Execution(ExecutionError::Overflow( - "the masternode list's total voting power overflows", - )))?; + .ok_or(Error::Execution(ExecutionError::Overflow( + "the masternode list's total voting power overflows", + )))?, + }; let result = self.keep_record_of_finished_yes_no_vote_poll( block_info, &vote_poll, diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/mod.rs index e58ad533ccd..ad59a7b84ce 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/mod.rs @@ -15,7 +15,6 @@ use std::collections::BTreeMap; mod tests; mod v0; mod v1; -mod v2; impl Platform where @@ -55,16 +54,9 @@ where transaction, platform_version, ), - 2 => self.clean_up_after_contested_resources_vote_polls_end_v2( - block_info, - vote_polls, - clean_up_testnet_corrupted_reference_issue, - transaction, - platform_version, - ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "clean_up_after_contested_resources_vote_polls_end".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v1/mod.rs index f4c4112147c..714ed483da9 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v1/mod.rs @@ -8,7 +8,9 @@ use dpp::identifier::Identifier; use dpp::prelude::TimestampMillis; use dpp::version::PlatformVersion; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use dpp::ProtocolError; use drive::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfo; +use drive::fees::op::LowLevelDriveOperation; use drive::grovedb::TransactionArg; use std::collections::BTreeMap; @@ -40,13 +42,45 @@ where )?; // We also need to clean out the specialized balances + self.empty_vote_poll_prefunded_balances_operations_v1( + vote_polls + .iter() + .map(|(vote_poll, _, _)| vote_poll.specialized_balance_id()), + block_info, + &mut operations, + transaction, + platform_version, + )?; + + if !operations.is_empty() { + self.drive.apply_batch_low_level_drive_operations( + None, + transaction, + operations, + &mut vec![], + &platform_version.drive, + )?; + } + + Ok(()) + } + /// Empties the prefunded balances of ended vote polls, in the given order, and credits what + /// they held to the epoch's processing pool, appending the operations. Moved here unchanged + /// from the body above, which shipped with protocol version 14, so its operations must not + /// change; the yes/no clean-up v0 (protocol version 15) drains its polls' balances through it + /// too. + pub(in crate::execution::platform_events::voting) fn empty_vote_poll_prefunded_balances_operations_v1( + &self, + specialized_balance_ids: impl IntoIterator>, + block_info: &BlockInfo, + operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { let mut total_credits_to_add_to_processing: Credits = 0; - for specialized_balance_id in vote_polls - .iter() - .map(|(vote_poll, _, _)| vote_poll.specialized_balance_id()) - { + for specialized_balance_id in specialized_balance_ids { let (credits, mut empty_specialized_balance_operation) = self.drive.empty_prefunded_specialized_balance_operations( specialized_balance_id?, @@ -75,16 +109,6 @@ where ); } - if !operations.is_empty() { - self.drive.apply_batch_low_level_drive_operations( - None, - transaction, - operations, - &mut vec![], - &platform_version.drive, - )?; - } - Ok(()) } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v2/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v2/mod.rs deleted file mode 100644 index d6445e65692..00000000000 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v2/mod.rs +++ /dev/null @@ -1,203 +0,0 @@ -use crate::error::execution::ExecutionError; -use crate::error::Error; -use crate::platform_types::platform::Platform; -use crate::rpc::core::CoreRPCLike; -use dpp::block::block_info::BlockInfo; -use dpp::fee::Credits; -use dpp::identifier::Identifier; -use dpp::prelude::TimestampMillis; -use dpp::version::PlatformVersion; -use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; -use dpp::ProtocolError; -use drive::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfo; -use drive::fees::op::LowLevelDriveOperation; -use drive::grovedb::TransactionArg; -use std::collections::BTreeMap; - -impl Platform -where - C: CoreRPCLike, -{ - /// Cleans up after contested resource vote polls that ended. - /// Version 2 (protocol version 15) leaves the end date index alone: - /// `clean_up_after_vote_polls_end` removes every finished poll of every kind from it, so - /// that an end date's tree goes only once nothing is left under it. - #[inline(always)] - // TODO: Use type or struct - #[allow(clippy::type_complexity)] - pub(super) fn clean_up_after_contested_resources_vote_polls_end_v2( - &self, - block_info: &BlockInfo, - vote_polls: Vec<( - &ContestedDocumentResourceVotePollWithContractInfo, - &TimestampMillis, - &BTreeMap>, - )>, - clean_up_testnet_corrupted_reference_issue: bool, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { - let mut operations = self.clean_up_after_contested_resources_vote_polls_end_operations_v1( - vote_polls.as_slice(), - clean_up_testnet_corrupted_reference_issue, - transaction, - platform_version, - )?; - - // We also need to clean out the specialized balances - - let mut total_credits_to_add_to_processing: Credits = 0; - - for specialized_balance_id in vote_polls - .iter() - .map(|(vote_poll, _, _)| vote_poll.specialized_balance_id()) - { - let (credits, mut empty_specialized_balance_operation) = - self.drive.empty_prefunded_specialized_balance_operations( - specialized_balance_id?, - false, - &mut None, - transaction, - platform_version, - )?; - operations.append(&mut empty_specialized_balance_operation); - total_credits_to_add_to_processing = total_credits_to_add_to_processing - .checked_add(credits) - .ok_or(Error::Execution(ExecutionError::Overflow( - "Credits from specialized balances are overflowing", - )))?; - } - - if total_credits_to_add_to_processing > 0 { - operations.push( - self.drive - .add_epoch_processing_credits_for_distribution_operation( - &block_info.epoch, - total_credits_to_add_to_processing, - transaction, - platform_version, - )?, - ); - } - - if !operations.is_empty() { - self.drive.apply_batch_low_level_drive_operations( - None, - transaction, - operations, - &mut vec![], - &platform_version.drive, - )?; - } - - Ok(()) - } - - #[inline(always)] - // TODO: Use type or struct - #[allow(clippy::type_complexity)] - fn clean_up_after_contested_resources_vote_polls_end_operations_v1( - &self, - vote_polls: &[( - &ContestedDocumentResourceVotePollWithContractInfo, - &TimestampMillis, - &BTreeMap>, - )], - clean_up_testnet_corrupted_reference_issue: bool, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result, Error> { - let mut operations = vec![]; - - // We remove the votes from under the contenders votes received - self.drive - .remove_contested_resource_vote_poll_votes_operations( - vote_polls, - true, - &mut operations, - transaction, - platform_version, - )?; - - // We remove the documents that contenders have - self.drive - .remove_contested_resource_vote_poll_documents_operations( - vote_polls, - clean_up_testnet_corrupted_reference_issue, - &mut operations, - transaction, - platform_version, - )?; - - // We remove the contenders - self.drive - .remove_contested_resource_vote_poll_contenders_operations( - vote_polls, - &mut operations, - transaction, - platform_version, - )?; - - let vote_poll_ids = vote_polls - .iter() - .map(|(vote_poll, _, _)| Ok((*vote_poll, vote_poll.unique_id()?))) - .collect::, - ProtocolError, - >>()?; - - let mut identity_to_vote_ids_map: BTreeMap<&Identifier, Vec<&Identifier>> = BTreeMap::new(); - - for (vote_poll, _, voters_for_contender) in vote_polls { - let vote_id = vote_poll_ids - .iter() - .find_map(|(vp, vid)| if vp == vote_poll { Some(vid) } else { None }) - .ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( - "the vote poll must exist in this lookup table", - )))?; - - for identifiers in voters_for_contender.values() { - for identity_id in identifiers { - identity_to_vote_ids_map - .entry(identity_id) - .or_default() - .push(vote_id); - } - } - } - - for (identity, vote_ids) in identity_to_vote_ids_map { - // We remove the identity votes given - self.drive - .remove_specific_vote_references_given_by_identity( - identity, - vote_ids.as_slice(), - &mut operations, - transaction, - platform_version, - )?; - } - - if clean_up_testnet_corrupted_reference_issue { - self.drive.remove_contested_resource_info_operations( - vote_polls, - &mut operations, - transaction, - platform_version, - )?; - // We remove the last index - self.drive - .remove_contested_resource_top_level_index_operations( - vote_polls, - &mut operations, - transaction, - platform_version, - )?; - } - - Ok(operations) - } -} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v1/mod.rs index ffc343ff1cd..d28938684c4 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v1/mod.rs @@ -18,9 +18,10 @@ where C: CoreRPCLike, { /// Cleans up after the polls that ended in this block. Version 1 (protocol version 15) - /// handles both kinds. It removes every finished poll from the end date index itself, - /// so that an end date's tree is deleted only once nothing of either kind is left under - /// it, then cleans up the polls of each kind. + /// handles both kinds: the contested polls go to the contested clean-up and the yes/no + /// polls to the yes/no clean-up. Each removes its own polls from the shared end date index + /// and applies its own batch, and each deletes an end date's tree only once its read of + /// the tree finds nothing of either kind left, so the two can run one after the other. #[inline(always)] pub(super) fn clean_up_after_vote_polls_end_v1( &self, @@ -37,44 +38,26 @@ where &TimestampMillis, &BTreeMap>, )> = Vec::new(); + #[allow(clippy::type_complexity)] let mut yes_no_polls: Vec<( &YesNoVotePoll, + &TimestampMillis, &BTreeMap>, )> = Vec::new(); - let mut end_date_entries: Vec<(Identifier, TimestampMillis)> = Vec::new(); for (end_date, vote_polls_for_time) in vote_polls { for vote_poll in vote_polls_for_time { match vote_poll { ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(contested_poll, vote_info) => { - end_date_entries.push((contested_poll.unique_id()?, *end_date)); contested_polls.push((contested_poll, end_date, vote_info)); } ResolvedVotePollWithVotes::YesNoVotePollWithVotes(yes_no_poll, voters) => { - end_date_entries.push((yes_no_poll.unique_id()?, *end_date)); - yes_no_polls.push((yes_no_poll, voters)); + yes_no_polls.push((yes_no_poll, end_date, voters)); } } } } - if !end_date_entries.is_empty() { - let mut operations = vec![]; - self.drive.remove_vote_poll_end_date_query_operations( - end_date_entries.as_slice(), - &mut operations, - transaction, - platform_version, - )?; - self.drive.apply_batch_low_level_drive_operations( - None, - transaction, - operations, - &mut vec![], - &platform_version.drive, - )?; - } - if !contested_polls.is_empty() { self.clean_up_after_contested_resources_vote_polls_end( block_info, diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs index 3b14bb8be66..1a195cb3d97 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs @@ -4,6 +4,7 @@ use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; use dpp::identifier::Identifier; +use dpp::prelude::TimestampMillis; use dpp::version::PlatformVersion; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; @@ -16,15 +17,17 @@ impl Platform where C: CoreRPCLike, { - /// Cleans up after yes/no polls that ended: their votes and vote trees, the voters' - /// entries in the identity votes index, and what is left of the prefunded balance that - /// paid for the votes, which goes to the processing pool. The end date index was already - /// cleared by the caller. The stored info stays as the record of the decision. + /// Cleans up after yes/no polls that ended: their entries in the end date index (each + /// given with its end date), their votes and vote trees, the voters' entries in the + /// identity votes index, and what is left of the prefunded balance that paid for the votes, + /// which goes to the processing pool. The stored info stays as the record of the decision. + #[allow(clippy::type_complexity)] pub(in crate::execution) fn clean_up_after_yes_no_vote_polls_end( &self, block_info: &BlockInfo, vote_polls: Vec<( &YesNoVotePoll, + &TimestampMillis, &BTreeMap>, )>, transaction: TransactionArg, diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs index 278d528baf6..e1c6c73a8a1 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs @@ -1,13 +1,13 @@ -use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; -use dpp::fee::Credits; use dpp::identifier::Identifier; +use dpp::prelude::TimestampMillis; use dpp::version::PlatformVersion; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; +use dpp::ProtocolError; use drive::grovedb::TransactionArg; use std::collections::BTreeMap; @@ -16,11 +16,13 @@ where C: CoreRPCLike, { #[inline(always)] + #[allow(clippy::type_complexity)] pub(super) fn clean_up_after_yes_no_vote_polls_end_v0( &self, block_info: &BlockInfo, vote_polls: Vec<( &YesNoVotePoll, + &TimestampMillis, &BTreeMap>, )>, transaction: TransactionArg, @@ -28,17 +30,33 @@ where ) -> Result<(), Error> { let mut operations = vec![]; + // The polls' entries in the end date index, which contested polls share: an end date's + // tree goes only once nothing of either kind is left under it. + let end_date_entries = vote_polls + .iter() + .map(|(vote_poll, end_date, _)| Ok((vote_poll.unique_id()?, **end_date))) + .collect::, ProtocolError>>()?; + self.drive.remove_vote_poll_end_date_query_operations( + end_date_entries.as_slice(), + &mut operations, + transaction, + platform_version, + )?; + // The votes and the three vote trees of every poll. + let polls: Vec<&YesNoVotePoll> = vote_polls + .iter() + .map(|(vote_poll, _, _)| *vote_poll) + .collect(); self.drive.remove_yes_no_vote_poll_votes_operations( - vote_polls.as_slice(), + polls.as_slice(), &mut operations, - transaction, platform_version, )?; // Each voter's entries in the identity votes index. let mut poll_ids_by_voter: BTreeMap<&Identifier, Vec> = BTreeMap::new(); - for (vote_poll, voters_by_choice) in &vote_polls { + for (vote_poll, _, voters_by_choice) in &vote_polls { let vote_poll_id = vote_poll.unique_id()?; for voters in voters_by_choice.values() { for voter in voters { @@ -61,35 +79,16 @@ where )?; } - // What is left of the prefunded balances goes to processing. - let mut total_credits_to_add_to_processing: Credits = 0; - for (vote_poll, _) in &vote_polls { - let (credits, mut empty_specialized_balance_operations) = - self.drive.empty_prefunded_specialized_balance_operations( - vote_poll.specialized_balance_id()?, - false, - &mut None, - transaction, - platform_version, - )?; - operations.append(&mut empty_specialized_balance_operations); - total_credits_to_add_to_processing = total_credits_to_add_to_processing - .checked_add(credits) - .ok_or(Error::Execution(ExecutionError::Overflow( - "Credits from specialized balances are overflowing", - )))?; - } - if total_credits_to_add_to_processing > 0 { - operations.push( - self.drive - .add_epoch_processing_credits_for_distribution_operation( - &block_info.epoch, - total_credits_to_add_to_processing, - transaction, - platform_version, - )?, - ); - } + // What is left of the prefunded balances goes to processing, as for contested polls. + self.empty_vote_poll_prefunded_balances_operations_v1( + polls + .iter() + .map(|vote_poll| vote_poll.specialized_balance_id()), + block_info, + &mut operations, + transaction, + platform_version, + )?; if !operations.is_empty() { self.drive.apply_batch_low_level_drive_operations( diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs index 394da430ef1..b7f7eb5f52b 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs @@ -1,3 +1,5 @@ +use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeType; + mod award_document_to_winner; mod check_for_ended_vote_polls; mod clean_up_after_contested_resources_vote_polls_end; @@ -10,3 +12,13 @@ mod remove_votes_for_removed_masternodes; mod run_dao_platform_events; mod tally_votes_for_contested_document_resource_vote_poll; mod tally_votes_for_yes_no_vote_poll; + +/// The voting power a masternode's yes/no vote carries: 1 for a masternode, 4 for an evonode, +/// as for contested polls. The strength of each yes/no vote and the masternode list total that a +/// share minimum is measured against both come from here, so they always use the same weights. +pub(in crate::execution) fn masternode_vote_strength(node_type: &MasternodeType) -> u8 { + match node_type { + MasternodeType::Regular => 1, + MasternodeType::Evo => 4, + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs index 5ecd383d217..c59629f75f5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs @@ -16,11 +16,9 @@ use dpp::data_contract::validate_document::DataContractDocumentValidationMethods use dpp::identifier::Identifier; use dpp::validation::{SimpleConsensusValidationResult}; use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; -use dpp::voting::vote_polls::VotePoll; use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::{DocumentCreateTransitionAction, DocumentCreateTransitionActionAccessorsV0}; use dpp::version::PlatformVersion; -use crate::error::execution::ExecutionError; use crate::error::Error; pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentCreateTransitionActionStructureValidationV1 { @@ -62,10 +60,7 @@ impl DocumentCreateTransitionActionStructureValidationV1 for DocumentCreateTrans .contested_vote_poll_for_document_properties(self.data(), platform_version)?; match (expected_vote_poll, self.prefunded_voting_balance()) { - ( - Some(VotePoll::ContestedDocumentResourceVotePoll(expected)), - Some((provided, _)), - ) => { + (Some(expected), Some((provided, _))) => { // -->> Changed in V1 <<-- The amount is the most the contender pays, and // what it has to pay depends on how many contenders the contest holds, so // state validation, which counts them, judges it and refuses a contender @@ -92,7 +87,7 @@ impl DocumentCreateTransitionActionStructureValidationV1 for DocumentCreateTrans } // -->> End Introduced in V1 <<-- } - (Some(VotePoll::ContestedDocumentResourceVotePoll(expected)), None) => { + (Some(expected), None) => { // A contested document stating no fund at all is refused with the contest's // fund, the least a contest takes: this step does not count contenders let expected_amount = expected.required_vote_resolution_fund(platform_version); @@ -114,14 +109,6 @@ impl DocumentCreateTransitionActionStructureValidationV1 for DocumentCreateTrans } // -->> End Introduced in V1 <<-- (None, None) => {} - // A document type's contested index only ever resolves to a contested - // document resource poll, at every protocol version: the arm only keeps the - // match exhaustive. - (Some(VotePoll::YesNoVotePoll(_)), _) => { - return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( - "a contested index never resolves to a yes/no vote poll", - ))); - } } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs index 18f5fe91e04..49387461c93 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs @@ -80,9 +80,24 @@ fn end_due_vote_polls( #[tokio::test] async fn should_end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left() { - let platform_version = PlatformVersion::latest(); + end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left(PlatformVersion::latest()).await; +} + +/// Protocol version 14 ends contests through the vote poll clean-up v0, which version 15 replaced. +#[tokio::test] +async fn should_end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left_at_protocol_version_14( +) { + end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left( + PlatformVersion::get(14).expect("expected protocol version 14"), + ) + .await; +} + +async fn end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left( + platform_version: &'static PlatformVersion, +) { let mut platform = TestPlatformBuilder::new() - .with_latest_protocol_version() + .with_initial_protocol_version(platform_version.protocol_version) .build_with_mock_rpc() .set_genesis_state(); @@ -180,9 +195,24 @@ async fn should_end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one #[tokio::test] async fn should_end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block() { - let platform_version = PlatformVersion::latest(); + end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block(PlatformVersion::latest()).await; +} + +/// Protocol version 14 ends contests through the vote poll clean-up v0, which version 15 replaced. +#[tokio::test] +async fn should_end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block_at_protocol_version_14( +) { + end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block( + PlatformVersion::get(14).expect("expected protocol version 14"), + ) + .await; +} + +async fn end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block( + platform_version: &'static PlatformVersion, +) { let mut platform = TestPlatformBuilder::new() - .with_latest_protocol_version() + .with_initial_protocol_version(platform_version.protocol_version) .build_with_mock_rpc() .set_genesis_state(); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs index d9083e260b4..857d792d971 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs @@ -80,9 +80,20 @@ fn setup() -> ( DataContract, StdRng, ) { - let platform_version = PlatformVersion::latest(); + setup_at(PlatformVersion::latest()) +} + +/// [`setup`] on a chain born at `platform_version`. +fn setup_at( + platform_version: &'static PlatformVersion, +) -> ( + TempPlatform, + &'static PlatformVersion, + DataContract, + StdRng, +) { let mut platform = TestPlatformBuilder::new() - .with_latest_protocol_version() + .with_initial_protocol_version(platform_version.protocol_version) .build_with_mock_rpc() .set_genesis_state(); let mut rng = StdRng::seed_from_u64(0x9010_C41A); @@ -734,7 +745,24 @@ async fn should_open_the_vote_window_when_a_second_contender_joins() { /// Both then end in the one block at the end of their vote window. #[tokio::test] async fn should_keep_a_shared_join_window_end_date_until_its_last_contest_moves() { - let (mut platform, platform_version, contract, mut rng) = setup(); + keep_a_shared_join_window_end_date_until_its_last_contest_moves(PlatformVersion::latest()) + .await; +} + +/// Protocol version 14 ends contests through the vote poll clean-up v0, which version 15 replaced. +#[tokio::test] +async fn should_keep_a_shared_join_window_end_date_until_its_last_contest_moves_at_protocol_version_14( +) { + keep_a_shared_join_window_end_date_until_its_last_contest_moves( + PlatformVersion::get(14).expect("expected protocol version 14"), + ) + .await; +} + +async fn keep_a_shared_join_window_end_date_until_its_last_contest_moves( + platform_version: &'static PlatformVersion, +) { + let (mut platform, platform_version, contract, mut rng) = setup_at(platform_version); let (join_window, poll_duration) = windows(&platform, platform_version); let alice = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let bob = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); @@ -830,7 +858,20 @@ async fn should_keep_a_shared_join_window_end_date_until_its_last_contest_moves( /// block; the block removes them first and ends the contest behind them. #[tokio::test] async fn should_end_a_contest_behind_end_dates_left_empty() { - let (mut platform, platform_version, contract, mut rng) = setup(); + end_a_contest_behind_end_dates_left_empty(PlatformVersion::latest()).await; +} + +/// Protocol version 14 ends contests through the vote poll clean-up v0, which version 15 replaced. +#[tokio::test] +async fn should_end_a_contest_behind_end_dates_left_empty_at_protocol_version_14() { + end_a_contest_behind_end_dates_left_empty( + PlatformVersion::get(14).expect("expected protocol version 14"), + ) + .await; +} + +async fn end_a_contest_behind_end_dates_left_empty(platform_version: &'static PlatformVersion) { + let (mut platform, platform_version, contract, mut rng) = setup_at(platform_version); let (join_window, _) = windows(&platform, platform_version); let alice = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let start = join( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs index bff5af6419f..5be8ad8f026 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs @@ -92,17 +92,10 @@ impl MasternodeVoteStateTransitionStateValidationV0 for MasternodeVoteTransition } } } - // Unreachable: this v0 is selected up to protocol version 13, where a - // masternode vote that names a yes/no poll does not decode - // (`StateTransition::active_version_range`). - ResolvedVotePoll::YesNoVotePoll(_) => { - Err(Error::Execution(ExecutionError::CorruptedCodeExecution( - "yes/no vote polls need validate_state version 1", - ))) - } } } - // Unreachable for the same reason: no yes/no vote decodes before 15. + // Unreachable: this v0 is selected up to protocol version 13, where a yes/no vote + // does not decode (`StateTransition::active_version_range`). ResolvedVote::YesNoVote(_) => { Err(Error::Execution(ExecutionError::CorruptedCodeExecution( "yes/no votes need validate_state version 1", diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v1/mod.rs index d9e42d902c7..a13c4c2c9da 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v1/mod.rs @@ -146,18 +146,6 @@ impl MasternodeVoteStateTransitionStateValidationV1 for MasternodeVoteTransition } } } - ResolvedVotePoll::YesNoVotePoll(_) => { - // A resource vote choice answers no yes/no poll: no contested - // document resource poll named by this vote exists. Reached from - // protocol version 15 only: at 14, which also selects this generation, - // a resource vote naming a yes/no poll is not active - // (`StateTransition::active_version_range`) and never decodes. - Ok(ConsensusValidationResult::new_with_error( - ConsensusError::StateError(StateError::VotePollNotFoundError( - VotePollNotFoundError::new(vote_poll.into()), - )), - )) - } } } // Reached from protocol version 15 only: at 14, which also selects this generation, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs index edf3957ddd2..c7720069139 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs @@ -1,4 +1,6 @@ use crate::error::Error; +use crate::execution::platform_events::voting::masternode_vote_strength; +use crate::execution::validation::state_transition::state_transitions::masternode_vote::transform_into_action::v0::MasternodeVoteStateTransitionTransformIntoActionValidationV0; use crate::platform_types::platform::PlatformRef; use dpp::consensus::state::state_error::StateError; use dpp::consensus::state::voting::masternode_not_found_error::MasternodeNotFoundError; @@ -8,7 +10,6 @@ use dpp::consensus::state::voting::vote_poll_not_found_error::VotePollNotFoundEr use dpp::consensus::ConsensusError; use dpp::dashcore::hashes::Hash; use dpp::dashcore::ProTxHash; -use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeType; use dpp::prelude::ConsensusValidationResult; use dpp::state_transition::masternode_vote_transition::accessors::MasternodeVoteTransitionAccessorsV0; use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; @@ -38,9 +39,10 @@ pub(in crate::execution::validation::state_transition::state_transitions::master } impl MasternodeVoteStateTransitionTransformIntoActionValidationV1 for MasternodeVoteTransition { - /// Version 1 (protocol version 15) also transforms a vote on a yes/no poll. The same rules - /// hold for both kinds: the same answer cannot be given twice, and a masternode may vote on a - /// poll at most `votes_allowed_per_masternode` times, its first vote included. + /// Version 1 (protocol version 15) also transforms a vote on a yes/no poll, with the rules + /// of a resource vote: the same answer cannot be given twice, and a masternode may vote on a + /// poll at most `votes_allowed_per_masternode` times, its first vote included. A resource + /// vote on a contested poll transforms through version 0, unchanged. fn transform_into_action_v1( &self, platform: &PlatformRef, @@ -48,137 +50,73 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV1 for Masternode tx: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { - // A resource vote choice (towards an identity, abstain, lock) answers a contested - // document resource poll. No such poll is named by a resource vote that carries a - // yes/no poll, whatever validation mode we are in. - if let Vote::ResourceVote(resource_vote) = self.vote() { - if let VotePoll::YesNoVotePoll(_) = resource_vote.vote_poll() { - return Ok(ConsensusValidationResult::new_with_error( - ConsensusError::StateError(StateError::VotePollNotFoundError( - VotePollNotFoundError::new(resource_vote.vote_poll().clone()), - )), - )); + let yes_no_vote = match self.vote() { + Vote::YesNoVote(yes_no_vote) => yes_no_vote, + Vote::ResourceVote(resource_vote) => { + // A resource vote choice (towards an identity, abstain, lock) answers a + // contested document resource poll. No such poll is named by a resource vote + // that carries a yes/no poll, whatever validation mode we are in. + if let VotePoll::YesNoVotePoll(_) = resource_vote.vote_poll() { + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::VotePollNotFoundError( + VotePollNotFoundError::new(resource_vote.vote_poll().clone()), + )), + )); + } + return self.transform_into_action_v0( + platform, + validation_mode, + tx, + platform_version, + ); } - } + }; - let mut previous_resource_vote_choice_to_remove = None; - let mut previous_yes_no_vote_choice_to_remove = None; + let mut previous_vote_choice_to_remove = None; if validation_mode != ValidationMode::NoValidation { // Before we transform into action we want to make sure that we have not yet voted - match self.vote() { - Vote::ResourceVote(resource_vote) => { - match resource_vote.vote_poll() { - VotePoll::ContestedDocumentResourceVotePoll(vote_poll) => { - let vote_id = vote_poll.unique_id()?; - let maybe_existing_resource_vote_choice = - platform.drive.fetch_identity_contested_resource_vote( - self.pro_tx_hash(), - vote_id, - tx, - &mut vec![], - platform_version, - )?; - if let Some((existing_resource_vote_choice, previous_vote_count)) = - maybe_existing_resource_vote_choice - { - if existing_resource_vote_choice - == resource_vote.resource_vote_choice() - { - // We are submitting a vote for something we already have - return Ok(ConsensusValidationResult::new_with_error( - ConsensusError::StateError( - StateError::MasternodeVoteAlreadyPresentError( - MasternodeVoteAlreadyPresentError::new( - self.pro_tx_hash(), - resource_vote.vote_poll().clone(), - ), - ), - ), - )); - } else if previous_vote_count - >= platform_version - .dpp - .validation - .voting - .votes_allowed_per_masternode - { - // We are submitting a vote for something we already have - return Ok(ConsensusValidationResult::new_with_error( - ConsensusError::StateError( - StateError::MasternodeVotedTooManyTimesError( - MasternodeVotedTooManyTimesError::new( - self.pro_tx_hash(), - previous_vote_count, - platform_version - .dpp - .validation - .voting - .votes_allowed_per_masternode, - ), - ), - ), - )); - } else { - previous_resource_vote_choice_to_remove = - Some((existing_resource_vote_choice, previous_vote_count)); - } - } - } - // Refused above. - VotePoll::YesNoVotePoll(_) => {} - } - } - Vote::YesNoVote(yes_no_vote) => { - let vote_poll = yes_no_vote.vote_poll(); - let vote_id = vote_poll.unique_id()?; - let maybe_existing_vote_choice = platform.drive.fetch_identity_yes_no_vote( - self.pro_tx_hash(), - vote_id, - tx, - &mut vec![], - platform_version, - )?; - if let Some((existing_vote_choice, previous_vote_count)) = - maybe_existing_vote_choice - { - if existing_vote_choice == yes_no_vote.vote_choice() { - return Ok(ConsensusValidationResult::new_with_error( - ConsensusError::StateError( - StateError::MasternodeVoteAlreadyPresentError( - MasternodeVoteAlreadyPresentError::new( - self.pro_tx_hash(), - VotePoll::YesNoVotePoll(vote_poll.clone()), - ), - ), - ), - )); - } else if previous_vote_count - >= platform_version - .dpp - .validation - .voting - .votes_allowed_per_masternode - { - return Ok(ConsensusValidationResult::new_with_error( - ConsensusError::StateError( - StateError::MasternodeVotedTooManyTimesError( - MasternodeVotedTooManyTimesError::new( - self.pro_tx_hash(), - previous_vote_count, - platform_version - .dpp - .validation - .voting - .votes_allowed_per_masternode, - ), - ), - ), - )); - } else { - previous_yes_no_vote_choice_to_remove = - Some((existing_vote_choice, previous_vote_count)); - } - } + let vote_poll = yes_no_vote.vote_poll(); + let vote_id = vote_poll.unique_id()?; + let maybe_existing_vote_choice = platform.drive.fetch_identity_yes_no_vote( + self.pro_tx_hash(), + vote_id, + tx, + &mut vec![], + platform_version, + )?; + if let Some((existing_vote_choice, previous_vote_count)) = maybe_existing_vote_choice { + if existing_vote_choice == yes_no_vote.vote_choice() { + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::MasternodeVoteAlreadyPresentError( + MasternodeVoteAlreadyPresentError::new( + self.pro_tx_hash(), + VotePoll::YesNoVotePoll(vote_poll.clone()), + ), + )), + )); + } else if previous_vote_count + >= platform_version + .dpp + .validation + .voting + .votes_allowed_per_masternode + { + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::MasternodeVotedTooManyTimesError( + MasternodeVotedTooManyTimesError::new( + self.pro_tx_hash(), + previous_vote_count, + platform_version + .dpp + .validation + .voting + .votes_allowed_per_masternode, + ), + )), + )); + } else { + previous_vote_choice_to_remove = + Some((existing_vote_choice, previous_vote_count)); } } } @@ -193,39 +131,20 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV1 for Masternode )); }; - let strength = match masternode.node_type { - MasternodeType::Regular => 1, - MasternodeType::Evo => 4, - }; - - let action = match self.vote() { - Vote::ResourceVote(_) => MasternodeVoteTransitionAction::transform_from_transition( - self, - masternode.state.voting_address, - strength, - previous_resource_vote_choice_to_remove, - platform.drive, - tx, - platform_version, - )?, - Vote::YesNoVote(yes_no_vote) => { - // A yes/no vote needs nothing from the state to be resolved; the previous - // answer found above rides with it. - let mut resolved_vote = ResolvedYesNoVote::from_vote(yes_no_vote); - resolved_vote.previous_vote_choice_to_remove = - previous_yes_no_vote_choice_to_remove; - MasternodeVoteTransitionActionV0 { - pro_tx_hash: self.pro_tx_hash(), - voter_identity_id: self.voter_identity_id(), - voting_address: masternode.state.voting_address, - vote_strength: strength, - vote: ResolvedVote::YesNoVote(resolved_vote), - previous_resource_vote_choice_to_remove: None, - nonce: self.nonce(), - } - .into() - } - }; + // A yes/no vote needs nothing from the state to be resolved; the previous answer found + // above rides with it. + let mut resolved_vote = ResolvedYesNoVote::from_vote(yes_no_vote); + resolved_vote.previous_vote_choice_to_remove = previous_vote_choice_to_remove; + let action = MasternodeVoteTransitionActionV0 { + pro_tx_hash: self.pro_tx_hash(), + voter_identity_id: self.voter_identity_id(), + voting_address: masternode.state.voting_address, + vote_strength: masternode_vote_strength(&masternode.node_type), + vote: ResolvedVote::YesNoVote(resolved_vote), + previous_resource_vote_choice_to_remove: None, + nonce: self.nonce(), + } + .into(); Ok(ConsensusValidationResult::new_with_data(action)) } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs index 0a7b7b37230..22702b3704d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs @@ -6,7 +6,7 @@ use crate::execution::validation::state_transition::state_transitions::tests::{ take_down_masternode_identities, }; use crate::platform_types::platform::PlatformRef; -use crate::platform_types::platform_state::PlatformStateV0Methods; +use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult::{ InternalError, SuccessfulExecution, UnpaidConsensusError, @@ -14,15 +14,16 @@ use crate::platform_types::state_transitions_processing_result::StateTransitionE use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; -use crate::test::helpers::state_mutation_guard::assert_check_tx_valid_at_all_levels; use assert_matches::assert_matches; use dpp::block::block_info::BlockInfo; +use dpp::block::epoch::Epoch; use dpp::consensus::basic::BasicError; use dpp::consensus::state::state_error::StateError; use dpp::consensus::ConsensusError; use dpp::dashcore::hashes::Hash; use dpp::dashcore::ProTxHash; use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeType; +use dpp::fee::Credits; use dpp::identifier::Identifier; use dpp::identity::IdentityPublicKey; use dpp::platform_value::BinaryData; @@ -43,8 +44,10 @@ use dpp::voting::votes::resource_vote::ResourceVote; use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; use dpp::voting::votes::yes_no_vote::YesNoVote; use dpp::voting::votes::Vote; +use drive::error::Error as DriveError; +use drive::grovedb::Error as GroveError; use drive::query::yes_no_vote_poll_state_query::YesNoVotePollStateDriveQuery; -use drive::query::VotePollsByEndDateDriveQuery; +use drive::util::test_helpers::vote_poll_end_dates; use simple_signer::signer::SimpleSigner; use std::ops::Deref; use std::sync::Arc; @@ -155,10 +158,12 @@ async fn perform_yes_no_vote( .await } -/// Whether CheckTx admits the serialized transition on its first check. A transition CheckTx -/// cannot take at all (one not active at the current protocol version fails to decode) is not -/// admitted either. -fn check_tx_admits(platform: &TempPlatform, serialized_transition: &[u8]) -> bool { +/// CheckTx's verdicts on the serialized transition at both levels, taken before a block runs it: +/// whether it admits the transition, or the error it failed with. +fn check_tx_verdicts( + platform: &TempPlatform, + serialized_transition: &[u8], +) -> Vec<(CheckTxLevel, Result)> { let platform_state = platform.state.load(); let platform_version = platform_state .current_platform_version() @@ -169,21 +174,29 @@ fn check_tx_admits(platform: &TempPlatform, serialized_transiti config: &platform.config, core_rpc: &platform.core_rpc, }; - platform - .check_tx( - serialized_transition, - CheckTxLevel::FirstTimeCheck, - &platform_ref, - platform_version, - ) - .is_ok_and(|result| result.is_valid()) + [CheckTxLevel::FirstTimeCheck, CheckTxLevel::Recheck] + .into_iter() + .map(|level| { + let verdict = platform + .check_tx( + serialized_transition, + level, + &platform_ref, + platform_version, + ) + .map(|result| result.is_valid()) + .map_err(|error| error.to_string()); + (level, verdict) + }) + .collect() } /// Processes one masternode vote transition in a block and reports how it went. /// /// Every vote first goes through CheckTx, which asserts under cfg(test) that it leaves committed -/// state untouched (the devnet paloma height 788 class); a vote CheckTx admits must then pass at -/// both levels, as the shared masternode vote helper requires of its valid votes. +/// state untouched (the devnet paloma height 788 class). As the shared masternode vote helper +/// requires, 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, or its voter never learns why. async fn perform_vote_transition( platform: &mut TempPlatform, vote: Vote, @@ -207,9 +220,7 @@ async fn perform_vote_transition( let serialized_transition = masternode_vote_transition .serialize_to_bytes() .expect("expected to serialize the vote"); - if check_tx_admits(platform, &serialized_transition) { - assert_check_tx_valid_at_all_levels(platform, &serialized_transition, "yes/no vote"); - } + let check_tx_verdicts = check_tx_verdicts(platform, &serialized_transition); let platform_state = platform.state.load(); let processing_result = process_test_state_transition( platform, @@ -218,8 +229,34 @@ async fn perform_vote_transition( platform_version, ); match processing_result.into_execution_results().remove(0) { - SuccessfulExecution { .. } => Ok(()), - other => Err(other), + SuccessfulExecution { .. } => { + for (level, verdict) in check_tx_verdicts { + assert_eq!( + verdict, + Ok(true), + "check_tx {level:?} must admit a yes/no vote a block accepts" + ); + } + Ok(()) + } + other => { + // CheckTx does not run the prefunded balance pre-check, for votes on contested polls + // either, so a vote on a poll without a fund passes it. + let refused_for_its_fund = matches!( + &other, + UnpaidConsensusError(ConsensusError::StateError( + StateError::PrefundedSpecializedBalanceNotFoundError(_) + | StateError::PrefundedSpecializedBalanceInsufficientError(_) + )) + ); + let (level, verdict) = &check_tx_verdicts[0]; + assert!( + refused_for_its_fund || matches!(verdict, Ok(false) | Err(_)), + "check_tx {level:?} must refuse a yes/no vote a block refuses ({other:?}), got \ + {verdict:?}" + ); + Err(other) + } } } @@ -261,25 +298,40 @@ fn close_ended_polls( close_ended_polls_at(platform, END_DATE + 300_000, platform_version) } -/// Moves the chain to `time_ms` and closes the polls that ended by then. +/// Moves the chain to `time_ms`, one block after the last, and closes the polls that ended by +/// then. fn close_ended_polls_at( platform: &mut TempPlatform, time_ms: u64, platform_version: &PlatformVersion, ) { - fast_forward_to_block(platform, time_ms, 10000, 42, 0, false); + close_ended_polls_in_block(platform, time_ms, None, platform_version) +} + +/// Like [`close_ended_polls_at`], with the closing block's own platform state when given: the +/// last committed one stays the chain's. +fn close_ended_polls_in_block( + platform: &mut TempPlatform, + time_ms: u64, + block_platform_state: Option, + platform_version: &PlatformVersion, +) { + let height = platform.state.load().last_committed_block_height() + 1; + fast_forward_to_block(platform, time_ms, height, 42, 0, false); let block_info = BlockInfo { time_ms, - height: 10000, + height, core_height: 42, epoch: Default::default(), }; - let platform_state = platform.state.load(); + let last_committed_platform_state = platform.state.load(); + let block_platform_state = block_platform_state + .unwrap_or_else(|| PlatformState::clone(&last_committed_platform_state)); let transaction = platform.drive.grove.start_transaction(); platform .check_for_ended_vote_polls( - &platform_state, - &platform_state, + &last_committed_platform_state, + &block_platform_state, &block_info, Some(&transaction), platform_version, @@ -305,6 +357,25 @@ fn poll_state( .expect("expected the poll state") } +/// The processing credits epoch 0 will distribute; the item is written by the first credit, so +/// before any it is missing, which is none. +fn processing_credits( + platform: &TempPlatform, + platform_version: &PlatformVersion, +) -> Credits { + match platform + .drive + .get_epoch_processing_credits_for_distribution( + &Epoch::new(0).expect("epoch"), + None, + platform_version, + ) { + Ok(credits) => credits, + Err(DriveError::GroveDB(error)) if matches!(*error, GroveError::PathKeyNotFound(_)) => 0, + Err(error) => panic!("expected the epoch's processing credits: {error:?}"), + } +} + fn new_platform() -> TempPlatform { TestPlatformBuilder::new() .with_latest_protocol_version() @@ -473,30 +544,34 @@ async fn should_judge_a_share_minimum_against_the_masternode_list_at_close() { ..two_thirds_poll(purpose, 0) }; let evonode_alone = half_rounded_up(b"evonode alone"); - let evonode_and_one = half_rounded_up(b"evonode and one masternode"); + let evonode_and_three = half_rounded_up(b"evonode and three masternodes"); open_poll(&platform, &evonode_alone, FUND, platform_version); - open_poll(&platform, &evonode_and_one, FUND, platform_version); + open_poll(&platform, &evonode_and_three, FUND, platform_version); - // One evonode and five masternodes: a total of 9, so half rounded up is 5. + // One evonode and five masternodes: a total of 9 while the polls run. let evonode = setup_voter(&mut platform, 100, true, platform_version); let masternodes: Vec<_> = (101..106) .map(|seed| setup_voter(&mut platform, seed, false, platform_version)) .collect(); - let total: u32 = platform - .state - .load() - .full_masternode_list() - .values() - .map(|masternode| match masternode.node_type { - MasternodeType::Evo => 4, - MasternodeType::Regular => 1, - }) - .sum(); - assert_eq!(total, 9, "the list holds only the voters set up here"); + let total_voting_power = |platform_state: &PlatformState| -> u32 { + platform_state + .full_masternode_list() + .values() + .map(|masternode| match masternode.node_type { + MasternodeType::Evo => 4, + MasternodeType::Regular => 1, + }) + .sum() + }; + assert_eq!( + total_voting_power(&platform.state.load()), + 9, + "the list holds only the voters set up here" + ); assert_eq!(masternodes.len(), 5); let (pro_tx_hash, signer, voting_key) = &evonode; - for (nonce, vote_poll) in [(1, &evonode_alone), (2, &evonode_and_one)] { + for (nonce, vote_poll) in [(1, &evonode_alone), (2, &evonode_and_three)] { perform_yes_no_vote( &mut platform, vote_poll, @@ -510,21 +585,45 @@ async fn should_judge_a_share_minimum_against_the_masternode_list_at_close() { .await .expect("expected the evonode's vote to be accepted"); } - let (pro_tx_hash, signer, voting_key) = &masternodes[0]; - perform_yes_no_vote( + for (pro_tx_hash, signer, voting_key) in &masternodes[..3] { + perform_yes_no_vote( + &mut platform, + &evonode_and_three, + YesNoAbstainVoteChoice::Yes, + signer, + *pro_tx_hash, + voting_key, + 1, + platform_version, + ) + .await + .expect("expected the masternode's vote to be accepted"); + } + + // The closing block's list gains four masternodes the last committed one lacks: a total of + // 13, so half rounded up is 7. Judged against the last committed list (9), the bar would be + // 5 instead. + let mut closing_block_state = PlatformState::clone(&platform.state.load()); + let template = closing_block_state + .full_masternode_list() + .values() + .find(|masternode| masternode.node_type == MasternodeType::Regular) + .expect("expected a masternode") + .clone(); + for seed in 0..4u8 { + let mut masternode = template.clone(); + masternode.pro_tx_hash = ProTxHash::from_byte_array([0xa0 + seed; 32]); + closing_block_state + .full_masternode_list_mut() + .insert(masternode.pro_tx_hash, masternode); + } + assert_eq!(total_voting_power(&closing_block_state), 13); + close_ended_polls_in_block( &mut platform, - &evonode_and_one, - YesNoAbstainVoteChoice::Yes, - signer, - *pro_tx_hash, - voting_key, - 1, + END_DATE + 300_000, + Some(closing_block_state), platform_version, - ) - .await - .expect("expected the masternode's vote to be accepted"); - - close_ended_polls(&mut platform, platform_version); + ); let result_of = |vote_poll: &YesNoVotePoll| { *poll_state(&platform, vote_poll, platform_version) @@ -535,12 +634,12 @@ async fn should_judge_a_share_minimum_against_the_masternode_list_at_close() { }; let alone = result_of(&evonode_alone); assert_eq!(alone.yes_voting_power, 4); - assert_eq!(alone.required_voting_power, 5); + assert_eq!(alone.required_voting_power, 7); assert!(!alone.passed); - let with_one = result_of(&evonode_and_one); - assert_eq!(with_one.yes_voting_power, 5); - assert_eq!(with_one.required_voting_power, 5); - assert!(with_one.passed); + let with_three = result_of(&evonode_and_three); + assert_eq!(with_three.yes_voting_power, 7); + assert_eq!(with_three.required_voting_power, 7); + assert!(with_three.passed); } #[tokio::test] @@ -644,16 +743,9 @@ async fn should_leave_only_the_record_behind_after_the_poll_ends() { .fetch_prefunded_specialized_balance(vote_poll_id.to_buffer(), None, platform_version) .expect("balance"); assert_eq!(balance, Some(FUND - 4 * vote_cost)); - let end_dates = VotePollsByEndDateDriveQuery { - start_time: None, - end_time: None, - limit: None, - offset: None, - order_ascending: true, - } - .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) - .expect("end dates"); + let end_dates = vote_poll_end_dates(&platform.drive, platform_version); assert_eq!(end_dates.len(), 1); + let processing_credits_before = processing_credits(&platform, platform_version); close_ended_polls(&mut platform, platform_version); @@ -694,20 +786,17 @@ async fn should_leave_only_the_record_behind_after_the_poll_ends() { None ); } + // What the votes left of the fund goes to the processing pool, and the balance goes. let balance = platform .drive .fetch_prefunded_specialized_balance(vote_poll_id.to_buffer(), None, platform_version) .expect("balance"); - assert!(balance.is_none() || balance == Some(0)); - let end_dates = VotePollsByEndDateDriveQuery { - start_time: None, - end_time: None, - limit: None, - offset: None, - order_ascending: true, - } - .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) - .expect("end dates"); + assert_eq!(balance, None); + assert_eq!( + processing_credits(&platform, platform_version) - processing_credits_before, + FUND - 4 * vote_cost + ); + let end_dates = vote_poll_end_dates(&platform.drive, platform_version); assert!(end_dates.is_empty()); // A finished poll takes no more votes, and cannot be opened again under the same id. @@ -992,6 +1081,22 @@ async fn should_refuse_a_vote_when_the_poll_has_no_fund() { ); let state = poll_state(&platform, &vote_poll, platform_version); assert_eq!(state.yes_voting_power, 0); + + // The poll still closes when its time comes: the clean-up finds no fund to drain and moves + // on, rather than failing the block. + let processing_credits_before = processing_credits(&platform, platform_version); + close_ended_polls(&mut platform, platform_version); + let result = *poll_state(&platform, &vote_poll, platform_version) + .stored_info + .expect("stored info") + .result() + .expect("expected the poll to be finished"); + assert!(!result.passed); + assert_eq!( + processing_credits(&platform, platform_version), + processing_credits_before + ); + assert!(vote_poll_end_dates(&platform.drive, platform_version).is_empty()); } #[tokio::test] @@ -1091,13 +1196,16 @@ async fn should_remove_the_votes_of_masternodes_that_left_the_list() { #[tokio::test] async fn should_close_a_contested_poll_and_a_yes_no_poll_that_end_at_the_same_time() { - use crate::execution::validation::state_transition::state_transitions::tests::create_dpns_identity_name_contest; + use crate::execution::validation::state_transition::state_transitions::tests::{ + create_dpns_identity_name_contest, dpns_name_vote_poll, get_vote_states, + }; + use dapi_grpc::platform::v0::get_contested_resource_vote_state_request::get_contested_resource_vote_state_request_v0::ResultType; let platform_version = PlatformVersion::latest(); let mut platform = new_platform(); let platform_state = platform.state.load(); // The DPNS contest ends two weeks after its creation at the genesis block time. - let (_contender_1, _contender_2, _dpns_contract) = create_dpns_identity_name_contest( + let (_contender_1, _contender_2, dpns_contract) = create_dpns_identity_name_contest( &mut platform, &platform_state, 7, @@ -1106,18 +1214,10 @@ async fn should_close_a_contested_poll_and_a_yes_no_poll_that_end_at_the_same_ti ) .await; // The yes/no poll ends at the very millisecond the contest does. - let contest_end_date = *VotePollsByEndDateDriveQuery { - start_time: None, - end_time: None, - limit: None, - offset: None, - order_ascending: true, - } - .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) - .expect("end dates") - .keys() - .next() - .expect("expected the contest's end date"); + let contest_end_date = *vote_poll_end_dates(&platform.drive, platform_version) + .keys() + .next() + .expect("expected the contest's end date"); let vote_poll = two_thirds_poll(b"shared end date", 1); open_poll_ending_at( &platform, @@ -1136,29 +1236,13 @@ async fn should_close_a_contested_poll_and_a_yes_no_poll_that_end_at_the_same_ti ) .await; - let end_dates = VotePollsByEndDateDriveQuery { - start_time: None, - end_time: None, - limit: None, - offset: None, - order_ascending: true, - } - .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) - .expect("end dates"); + let end_dates = vote_poll_end_dates(&platform.drive, platform_version); assert_eq!(end_dates.len(), 1, "both polls end at the same time"); assert_eq!(end_dates.values().next().expect("polls").len(), 2); close_ended_polls_at(&mut platform, contest_end_date + 300_000, platform_version); - let end_dates = VotePollsByEndDateDriveQuery { - start_time: None, - end_time: None, - limit: None, - offset: None, - order_ascending: true, - } - .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) - .expect("end dates"); + let end_dates = vote_poll_end_dates(&platform.drive, platform_version); assert!(end_dates.is_empty()); assert!( poll_state(&platform, &vote_poll, platform_version) @@ -1168,6 +1252,36 @@ async fn should_close_a_contested_poll_and_a_yes_no_poll_that_end_at_the_same_ti .expect("finished") .passed ); + + // The contest closed in the same block: it has a result, and its clean-up drained its + // fund. + let platform_state = platform.state.load(); + let (_contenders, _abstaining, _locking, finished_vote_info) = get_vote_states( + &platform, + &platform_state, + &dpns_contract, + "quantum", + None, + true, + None, + ResultType::DocumentsAndVoteTally, + platform_version, + ); + assert!(finished_vote_info.is_some(), "the contest must be finished"); + let contest_fund_id = dpns_name_vote_poll(&dpns_contract, "quantum") + .specialized_balance_id() + .expect("expected the contest's fund id"); + assert_eq!( + platform + .drive + .fetch_prefunded_specialized_balance( + contest_fund_id.to_buffer(), + None, + platform_version + ) + .expect("balance"), + None + ); } /// The block closes at most `maximum_vote_polls_to_process` polls, counted across every due end @@ -1193,15 +1307,7 @@ async fn should_keep_an_end_date_the_block_only_partly_closed() { ); } let end_dates = |platform: &TempPlatform| { - VotePollsByEndDateDriveQuery { - start_time: None, - end_time: None, - limit: None, - offset: None, - order_ascending: true, - } - .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) - .expect("end dates") + vote_poll_end_dates(&platform.drive, platform_version) }; let finished = |platform: &TempPlatform, vote_poll: &YesNoVotePoll| { poll_state(platform, vote_poll, platform_version) @@ -1360,14 +1466,6 @@ async fn run_a_contest_and_strip_yes_no_polls(protocol_version: u32) { finished_vote_info.won_by_identity_id, Some(contender_1.id().to_vec()) ); - let end_dates = VotePollsByEndDateDriveQuery { - start_time: None, - end_time: None, - limit: None, - offset: None, - order_ascending: true, - } - .execute_no_proof(&platform.drive, None, &mut vec![], platform_version) - .expect("end dates"); + let end_dates = vote_poll_end_dates(&platform.drive, platform_version); assert!(end_dates.is_empty()); } diff --git a/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs index 220c7967443..6fdb9ebb1af 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs @@ -970,11 +970,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( } } StateTransitionAction::MasternodeVoteAction(masternode_vote_action) => { - let vote: Vote = masternode_vote_action - .vote_ref() - .clone() - .try_into() - .expect("expected a vote"); + let vote: Vote = masternode_vote_action.vote_ref().clone().into(); // we expect to get a vote that matches the state transition let (root_hash_vote, maybe_vote) = match masternode_vote_action.vote_ref() { @@ -983,9 +979,6 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( ResolvedVotePoll::ContestedDocumentResourceVotePollWithContractInfo( contested_document_resource_vote_poll, ) => contested_document_resource_vote_poll.contract.as_ref(), - ResolvedVotePoll::YesNoVotePoll(_) => { - panic!("a resource vote cannot answer a yes/no vote poll") - } }; Drive::verify_masternode_vote( &response_proof.grovedb_proof, diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs index 10a1ce09c59..714517d9fef 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs @@ -5,13 +5,9 @@ use crate::error::drive::DriveError; use crate::error::Error; use crate::drive::votes::paths::{ - vote_contested_resource_identity_votes_tree_path_for_identity, - vote_contested_resource_identity_votes_tree_path_vec, - vote_decisions_active_polls_poll_tree_path_vec, - vote_decisions_identity_votes_tree_path_for_identity, - vote_decisions_identity_votes_tree_path_vec, vote_decisions_tree_path, IDENTITY_VOTES_TREE_KEY, + vote_decisions_active_polls_poll_tree_path_vec, vote_decisions_identity_votes_tree_path_vec, + vote_decisions_tree_path, IDENTITY_VOTES_TREE_KEY, }; -use crate::drive::votes::storage_form::contested_document_resource_reference_storage_form::ContestedDocumentResourceVoteReferenceStorageForm; use crate::drive::votes::storage_form::yes_no_vote_reference_storage_form::YesNoVoteReferenceStorageForm; use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; use crate::query::QueryItem; @@ -24,9 +20,11 @@ use grovedb::{Element, MaybeTree, PathQuery, Query, SizedQuery, TransactionArg}; use std::collections::BTreeMap; impl Drive { - /// Version 1 (protocol version 15) removes the yes/no votes of the removed masternodes - /// too: the vote under the poll's choice sum tree and the entry in the decisions identity - /// votes index. + /// Version 1 (protocol version 15) removes the contested resource votes of the removed + /// masternodes through version 0, unchanged, then their yes/no votes in a batch of its own: + /// the vote under the poll's choice sum tree and the entry in the decisions identity votes + /// index. The two batches touch different subtrees, so applying them one after the other + /// leaves the same state as one batch would. pub(super) fn remove_all_votes_given_by_identities_v1( &self, identity_ids_as_byte_arrays: Vec>, @@ -36,81 +34,14 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { - let mut deletion_batch = vec![]; - - // Contested resource votes, exactly as version 0. - let vote_path = vote_contested_resource_identity_votes_tree_path_vec(); - let mut query = Query::new_with_direction(true); - query.insert_keys(identity_ids_as_byte_arrays.clone()); - let subquery = Query::new_single_query_item(QueryItem::RangeFull(RangeFull)); - query.set_subquery(subquery); - let path_query = PathQuery::new(vote_path.clone(), SizedQuery::new(query, None, None)); - let votes_to_remove_by_identity_id = self - .grove_get_raw_path_query( - &path_query, - transaction, - QueryPathKeyElementTrioResultType, - &mut vec![], - &platform_version.drive, - )? - .0 - .to_last_path_to_key_elements_btree_map(); - - for (identifier_bytes, votes_to_remove) in votes_to_remove_by_identity_id { - let identity_id = Identifier::from_vec(identifier_bytes.clone())?; - let vote_path_ref = vote_contested_resource_identity_votes_tree_path_for_identity( - identity_id.as_bytes(), - ); - for (vote_id, vote_to_remove) in votes_to_remove { - // We delete the vote item as reference - self.batch_delete( - vote_path_ref.as_slice().into(), - vote_id.as_slice(), - BatchDeleteApplyType::StatefulBatchDelete { - is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), - }, - transaction, - &mut deletion_batch, - &platform_version.drive, - )?; - - let serialized_reference = vote_to_remove.into_item_bytes()?; - let bincode_config = bincode::config::standard() - .with_big_endian() - .with_no_limit(); - let reference: ContestedDocumentResourceVoteReferenceStorageForm = - bincode::decode_from_slice(&serialized_reference, bincode_config) - .map_err(|e| { - Error::Drive(DriveError::CorruptedSerialization(format!( - "serialization of reference {} is corrupted: {}", - hex::encode(serialized_reference), - e - ))) - })? - .0; - - let mut absolute_path = reference - .reference_path_type - .absolute_path(vote_path_ref.as_slice(), Some(vote_id.as_slice()))?; - - // we then need to add to the batch the deletion - absolute_path.pop(); // we need to get rid of the key (which is the identifier bytes) - - let absolute_path_ref: Vec<_> = - absolute_path.iter().map(|a| a.as_slice()).collect(); - - self.batch_delete( - absolute_path_ref.as_slice().into(), - identifier_bytes.as_slice(), - BatchDeleteApplyType::StatefulBatchDelete { - is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), - }, - transaction, - &mut deletion_batch, - &platform_version.drive, - )?; - } - } + self.remove_all_votes_given_by_identities_v0( + identity_ids_as_byte_arrays.clone(), + block_height, + network, + chain_id, + transaction, + platform_version, + )?; // Yes/no votes: the index item names the poll and the answer, so the vote sits under // the poll's tree at the answer's sum tree. A chain that activated protocol version 15 @@ -123,32 +54,19 @@ impl Drive { &mut vec![], &platform_version.drive, )?; - let yes_no_votes_to_remove_by_identity_id = if has_decisions_index { - self.yes_no_votes_of_identities( - identity_ids_as_byte_arrays, - transaction, - platform_version, - )? - } else { - BTreeMap::new() - }; + if !has_decisions_index { + return Ok(()); + } - for (identifier_bytes, votes_to_remove) in yes_no_votes_to_remove_by_identity_id { + let mut deletion_batch = vec![]; + for (identifier_bytes, votes_to_remove) in self.yes_no_votes_of_identities( + identity_ids_as_byte_arrays, + transaction, + platform_version, + )? { let identity_id = Identifier::from_vec(identifier_bytes.clone())?; - let vote_path_ref = - vote_decisions_identity_votes_tree_path_for_identity(identity_id.as_bytes()); + let mut vote_poll_ids = Vec::with_capacity(votes_to_remove.len()); for (vote_poll_id, vote_to_remove) in votes_to_remove { - self.batch_delete( - vote_path_ref.as_slice().into(), - vote_poll_id.as_slice(), - BatchDeleteApplyType::StatefulBatchDelete { - is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), - }, - transaction, - &mut deletion_batch, - &platform_version.drive, - )?; - let serialized_form = vote_to_remove.into_item_bytes()?; let storage_form = YesNoVoteReferenceStorageForm::deserialize(&serialized_form) .map_err(|e| { @@ -172,26 +90,19 @@ impl Drive { &mut deletion_batch, &platform_version.drive, )?; + vote_poll_ids.push(vote_poll_id); } + let vote_poll_ids: Vec<&Identifier> = vote_poll_ids.iter().collect(); + self.remove_yes_no_vote_references_given_by_identity_operations( + &identity_id, + vote_poll_ids.as_slice(), + &mut deletion_batch, + transaction, + platform_version, + )?; } if !deletion_batch.is_empty() { - // We had a sequence of errors on the mainnet started since block 32326. - // We got RocksDB's "transaction is busy" error because of a bug (https://github.com/dashpay/platform/pull/2309). - // Due to another bug in Tenderdash (https://github.com/dashpay/tenderdash/pull/966), - // validators just proceeded to the next block partially committing the state - // and updating the cache (https://github.com/dashpay/platform/pull/2305). - // Full nodes are stuck and proceeded after re-sync. - // For the mainnet chain, we enable this fix at the block when we consider the state is consistent. - let transaction = - if network == Network::Mainnet && chain_id == "evo1" && block_height < 32329 { - // Old behaviour on mainnet - None - } else { - // We should use transaction - transaction - }; - self.apply_batch_low_level_drive_operations( None, transaction, @@ -199,7 +110,7 @@ impl Drive { &mut vec![], &platform_version.drive, ) - .inspect_err(|err| tracing::error!(?err, "vote deletion batch failed"))?; + .inspect_err(|err| tracing::error!(?err, "yes/no vote deletion batch failed"))?; } Ok(()) diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_contested_resource_vote_poll_end_date_query_operations/v2/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_contested_resource_vote_poll_end_date_query_operations/v2/mod.rs index dbbeb4ddcb0..629e0b1af78 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_contested_resource_vote_poll_end_date_query_operations/v2/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_contested_resource_vote_poll_end_date_query_operations/v2/mod.rs @@ -32,6 +32,34 @@ impl Drive { batch_operations: &mut Vec, transaction: TransactionArg, platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let mut by_end_date: BTreeMap> = BTreeMap::new(); + + for (vote_poll, end_date, _) in vote_polls { + by_end_date + .entry(**end_date) + .or_default() + .insert(vote_poll.unique_id()?); + } + + self.remove_vote_poll_ids_from_end_dates_operations_v2( + by_end_date, + batch_operations, + transaction, + platform_version, + ) + } + + /// The body of version 2 once the vote polls are grouped into their unique ids by end date, + /// whatever their kind. Shipped with protocol version 14, so its operations must not change; + /// `remove_vote_poll_end_date_query_operations` v0 (protocol version 15) removes yes/no polls + /// from the shared end date index through it too. + pub(in crate::drive::votes) fn remove_vote_poll_ids_from_end_dates_operations_v2( + &self, + by_end_date: BTreeMap>, + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, ) -> Result<(), Error> { // This is a GroveDB Tree (Not Sub Tree Merk representation) // End Date queries @@ -44,15 +72,6 @@ impl Drive { is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), }; - let mut by_end_date: BTreeMap> = BTreeMap::new(); - - for (vote_poll, end_date, _) in vote_polls { - by_end_date - .entry(**end_date) - .or_default() - .insert(vote_poll.unique_id()?); - } - for (end_date, unique_ids) in by_end_date { let time_path = vote_contested_resource_end_date_queries_at_time_tree_path_vec(end_date); diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs index 1ab0dda65a4..e1c1308f35e 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs @@ -10,10 +10,10 @@ use dpp::version::PlatformVersion; use grovedb::TransactionArg; impl Drive { - /// Removes finished polls of every kind from the end date index, and each end date's tree - /// once nothing is left under it. `vote_polls` holds every poll closed in this block with - /// its end date; whether an end date is left empty is read from the tree, since it can - /// still hold a poll of either kind the block did not reach. + /// Removes finished polls, given by unique id with their end date, from the end date index, + /// and each end date's tree once nothing is left under it. Whether an end date is left empty + /// is read from the tree, since it can still hold a poll of either kind that this batch does + /// not remove. The yes/no clean-up removes its polls with it. pub fn remove_vote_poll_end_date_query_operations( &self, vote_polls: &[(Identifier, TimestampMillis)], diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs index 8b89e7f3e67..c5b894acdd9 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs @@ -1,22 +1,16 @@ -use crate::drive::votes::paths::{ - vote_contested_resource_end_date_queries_at_time_tree_path_vec, - vote_end_date_queries_tree_path_vec, -}; use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use crate::query::VotePollsByEndDateDriveQuery; -use crate::util::common::encode::encode_u64; -use crate::util::grove_operations::BatchDeleteApplyType; use dpp::identifier::Identifier; use dpp::identity::TimestampMillis; -use grovedb::{MaybeTree, TransactionArg}; +use grovedb::TransactionArg; use platform_version::version::PlatformVersion; use std::collections::{BTreeMap, BTreeSet}; impl Drive { - /// The kind-agnostic counterpart of `remove_contested_resource_vote_poll_end_date_query_operations` - /// v2, with the same rule for an end date: it goes once none of its vote polls remain. + /// Removes the given vote polls, of any kind, from the end date index with the rule of + /// `remove_contested_resource_vote_poll_end_date_query_operations` v2, whose body it reuses: + /// an end date goes once none of its vote polls remain. pub(super) fn remove_vote_poll_end_date_query_operations_v0( &self, vote_polls: &[(Identifier, TimestampMillis)], @@ -24,70 +18,16 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { - // This is a GroveDB Tree (Not Sub Tree Merk representation) - // End Date queries - // / \ - // 15/08/2025 5PM 15/08/2025 6PM - // / \ | - // VotePoll Info 1 VotePoll Info 2 VotePoll Info 3 - let delete_apply_type = BatchDeleteApplyType::StatefulBatchDelete { - is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), - }; - let mut by_end_date: BTreeMap> = BTreeMap::new(); for (unique_id, end_date) in vote_polls { by_end_date.entry(*end_date).or_default().insert(*unique_id); } - for (end_date, unique_ids) in by_end_date { - let time_path = - vote_contested_resource_end_date_queries_at_time_tree_path_vec(end_date); - for unique_id in &unique_ids { - self.batch_delete( - time_path.as_slice().into(), - unique_id.as_bytes(), - delete_apply_type, - transaction, - batch_operations, - &platform_version.drive, - )?; - } - - // The block's fetch shares one limit across every due end date, so the last end date - // it reached can be cut short, and an end date can also hold a poll of the other - // kind. Read one entry more than this batch removes: the end date is left empty only - // when every entry read is removed here, and then the read found all of them. A - // count too large for a query limit reads without one. An end date that lists - // nothing (it has no tree) is not removed. - let limit = u16::try_from(unique_ids.len().saturating_add(1)).ok(); - let entries = VotePollsByEndDateDriveQuery::execute_no_proof_keys_for_single_end_time( - end_date, - limit, - self, - transaction, - &mut vec![], - platform_version, - )?; - let none_remain = !entries.is_empty() - && entries.iter().all(|key| { - Identifier::from_bytes(key).is_ok_and(|entry_id| unique_ids.contains(&entry_id)) - }); - - if none_remain { - // The end date holds nothing once this batch applies, so it goes in the same - // batch. The `NotTree` hint is deliberate although the end date is a tree: the - // read above is the emptiness check. - self.batch_delete( - vote_end_date_queries_tree_path_vec().as_slice().into(), - encode_u64(end_date).as_slice(), - delete_apply_type, - transaction, - batch_operations, - &platform_version.drive, - )?; - } - } - - Ok(()) + self.remove_vote_poll_ids_from_end_dates_operations_v2( + by_end_date, + batch_operations, + transaction, + platform_version, + ) } } diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs index 5c0f72696ab..35dcc0aafb8 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs @@ -4,24 +4,16 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use dpp::identifier::Identifier; use dpp::version::PlatformVersion; -use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; -use grovedb::TransactionArg; -use std::collections::BTreeMap; impl Drive { - /// Removes the votes of finished yes/no polls and the three sum trees that held them. The + /// Removes the votes of finished yes/no polls with the three sum trees that hold them. The /// polls' stored info stays as the record of the decision. pub fn remove_yes_no_vote_poll_votes_operations( &self, - vote_polls: &[( - &YesNoVotePoll, - &BTreeMap>, - )], + vote_polls: &[&YesNoVotePoll], batch_operations: &mut Vec, - transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { match platform_version @@ -34,7 +26,6 @@ impl Drive { 0 => self.remove_yes_no_vote_poll_votes_operations_v0( vote_polls, batch_operations, - transaction, platform_version, ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs index 88059d6fdc3..d4da38c954b 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs @@ -3,64 +3,35 @@ use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use crate::util::grove_operations::BatchDeleteApplyType; -use dpp::identifier::Identifier; use dpp::version::PlatformVersion; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; -use grovedb::{MaybeTree, TransactionArg}; -use std::collections::BTreeMap; +use grovedb::batch::key_info::KeyInfo; +use grovedb::batch::{GroveOp, KeyInfoPath, QualifiedGroveDbOp, SubelementsDeletionBehavior}; +use grovedb::TreeType; impl Drive { pub(super) fn remove_yes_no_vote_poll_votes_operations_v0( &self, - vote_polls: &[( - &YesNoVotePoll, - &BTreeMap>, - )], + vote_polls: &[&YesNoVotePoll], batch_operations: &mut Vec, - transaction: TransactionArg, - platform_version: &PlatformVersion, + _platform_version: &PlatformVersion, ) -> Result<(), Error> { - for (vote_poll, voters_by_choice) in vote_polls { - let poll_path = vote_poll.poll_path_vec()?; + for vote_poll in vote_polls { + let poll_path = KeyInfoPath::from_known_owned_path(vote_poll.poll_path_vec()?); for vote_choice in YesNoAbstainVoteChoice::ALL { - let votes_path = vote_poll.choice_votes_path_vec(vote_choice)?; - // Every delete here names its element type, so GroveDB never reads the pending - // batch for it. Each is built against an empty one: handing `batch_operations` - // to `batch_delete` would copy every operation already queued on each call and - // make the clean-up quadratic in the number of voters. - if let Some(voters) = voters_by_choice.get(&vote_choice) { - for voter in voters { - let mut delete_operations = vec![]; - self.batch_delete( - votes_path.as_slice().into(), - voter.as_slice(), - BatchDeleteApplyType::StatefulBatchDelete { - is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), - }, - transaction, - &mut delete_operations, - &platform_version.drive, - )?; - batch_operations.append(&mut delete_operations); - } - } - // The sum tree itself, emptied by the deletes above. As for a contested poll's - // vote trees, it is not declared a tree: that would make GroveDB read every - // vote again to prove it empty. - let mut delete_operations = vec![]; - self.batch_delete( - poll_path.as_slice().into(), - &[vote_choice.to_tree_key()], - BatchDeleteApplyType::StatefulBatchDelete { - is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), - }, - transaction, - &mut delete_operations, - &platform_version.drive, - )?; - batch_operations.append(&mut delete_operations); + // Each choice's sum tree holds only the votes, one sum item per voter, and a + // finished poll is never opened again, so the tree is dropped whole without + // reading or deleting its votes one by one (GroveDB's flat drop, which the + // grove version of protocol version 15 accepts in a batch). + batch_operations.push(LowLevelDriveOperation::GroveOperation(QualifiedGroveDbOp { + path: poll_path.clone(), + key: Some(KeyInfo::KnownKey(vec![vote_choice.to_tree_key()])), + op: GroveOp::DeleteTreeDontCheckForBackwardsReferences( + TreeType::SumTree, + SubelementsDeletionBehavior::DropFlat, + ), + })); } } Ok(()) diff --git a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/mod.rs b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/mod.rs index d1460122dc9..224de76a74b 100644 --- a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/mod.rs @@ -55,7 +55,7 @@ impl Drive { .drive .methods .vote - .contested_resource_insert + .insert .register_identity_vote { 0 => self.register_identity_vote_v0( @@ -122,7 +122,7 @@ impl Drive { .drive .methods .vote - .contested_resource_insert + .insert .register_identity_vote { 0 => self.register_identity_vote_operations_v0( diff --git a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v0/mod.rs index 3424420dfe0..36c3e9cff93 100644 --- a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v0/mod.rs @@ -40,15 +40,11 @@ impl Drive { transaction, platform_version, ), - // Unreachable: this v0 is selected up to protocol version 14, its vote - // comes from a masternode vote action, and a masternode vote that names a - // yes/no poll does not decode there (`StateTransition::active_version_range`). - ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Drive( - DriveError::CorruptedCodeExecution("yes/no vote polls need version 1"), - )), } } - // Unreachable for the same reason: no yes/no vote decodes before 15. + // Unreachable: this v0 is selected up to protocol version 14, its vote comes from a + // masternode vote action, and a yes/no vote does not decode there + // (`StateTransition::active_version_range`). ResolvedVote::YesNoVote(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution( "yes/no votes need version 1", ))), @@ -79,15 +75,11 @@ impl Drive { transaction, platform_version, ), - // Unreachable: this v0 is selected up to protocol version 14, its vote - // comes from a masternode vote action, and a masternode vote that names a - // yes/no poll does not decode there (`StateTransition::active_version_range`). - ResolvedVotePoll::YesNoVotePoll(_) => Err(Error::Drive( - DriveError::CorruptedCodeExecution("yes/no vote polls need version 1"), - )), } } - // Unreachable for the same reason: no yes/no vote decodes before 15. + // Unreachable: this v0 is selected up to protocol version 14, its vote comes from a + // masternode vote action, and a yes/no vote does not decode there + // (`StateTransition::active_version_range`). ResolvedVote::YesNoVote(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution( "yes/no votes need version 1", ))), diff --git a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v1/mod.rs b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v1/mod.rs index 09709bb6f9e..468706df0bc 100644 --- a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v1/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v1/mod.rs @@ -1,8 +1,5 @@ -use crate::drive::votes::resolved::vote_polls::ResolvedVotePoll; -use crate::drive::votes::resolved::votes::resolved_resource_vote::accessors::v0::ResolvedResourceVoteGettersV0; use crate::drive::votes::resolved::votes::ResolvedVote; use crate::drive::Drive; -use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::state_transition_action::identity::masternode_vote::v0::PreviousVoteCount; @@ -13,8 +10,8 @@ use grovedb::TransactionArg; use platform_version::version::PlatformVersion; impl Drive { - /// Version 1 (protocol version 15) registers a vote of either kind: a resource vote as - /// before, and a yes/no vote under its poll in the decisions branch. + /// Version 1 (protocol version 15) registers a vote of either kind: a yes/no vote under its + /// poll in the decisions branch, and a resource vote through version 0, unchanged. #[allow(clippy::too_many_arguments)] pub(super) fn register_identity_vote_v1( &self, @@ -27,28 +24,6 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result { match vote { - ResolvedVote::ResolvedResourceVote(resource_vote) => { - let vote_choice = resource_vote.resource_vote_choice(); - match resource_vote.vote_poll_owned() { - ResolvedVotePoll::ContestedDocumentResourceVotePollWithContractInfo( - contested_document_resource_vote_poll, - ) => self.register_contested_resource_identity_vote( - voter_pro_tx_hash, - strength, - contested_document_resource_vote_poll, - vote_choice, - previous_resource_vote_choice_to_remove, - block_info, - transaction, - platform_version, - ), - ResolvedVotePoll::YesNoVotePoll(_) => { - Err(Error::Drive(DriveError::CorruptedCodeExecution( - "a resource vote cannot answer a yes/no vote poll", - ))) - } - } - } ResolvedVote::YesNoVote(yes_no_vote) => self.register_yes_no_identity_vote( voter_pro_tx_hash, strength, @@ -57,6 +32,15 @@ impl Drive { transaction, platform_version, ), + resource_vote => self.register_identity_vote_v0( + voter_pro_tx_hash, + strength, + resource_vote, + previous_resource_vote_choice_to_remove, + block_info, + transaction, + platform_version, + ), } } @@ -70,27 +54,6 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result, Error> { match vote { - ResolvedVote::ResolvedResourceVote(resource_vote) => { - let vote_choice = resource_vote.resource_vote_choice(); - match resource_vote.vote_poll_owned() { - ResolvedVotePoll::ContestedDocumentResourceVotePollWithContractInfo( - contested_document_resource_vote_poll, - ) => self.register_contested_resource_identity_vote_operations( - voter_pro_tx_hash, - strength, - contested_document_resource_vote_poll, - vote_choice, - previous_resource_vote_choice_to_remove, - transaction, - platform_version, - ), - ResolvedVotePoll::YesNoVotePoll(_) => { - Err(Error::Drive(DriveError::CorruptedCodeExecution( - "a resource vote cannot answer a yes/no vote poll", - ))) - } - } - } ResolvedVote::YesNoVote(yes_no_vote) => self.register_yes_no_identity_vote_operations( voter_pro_tx_hash, strength, @@ -98,6 +61,14 @@ impl Drive { transaction, platform_version, ), + resource_vote => self.register_identity_vote_operations_v0( + voter_pro_tx_hash, + strength, + resource_vote, + previous_resource_vote_choice_to_remove, + transaction, + platform_version, + ), } } } diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs index ba99d118201..d0ab65a61a0 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs @@ -48,6 +48,10 @@ impl Drive { } /// The operations of [`Self::open_yes_no_vote_poll`], added to `batch_operations`. + /// + /// They are stateful and have no estimated-costs path: a poll opens as a system action, + /// outside any fee estimate. A caller that runs them inside a fee estimate must first give + /// them one. #[allow(clippy::too_many_arguments)] pub fn open_yes_no_vote_poll_operations( &self, @@ -83,3 +87,96 @@ impl Drive { } } } + +#[cfg(test)] +mod tests { + use crate::drive::votes::paths::{ + vote_decisions_tree_path, ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY, + }; + use crate::util::grove_operations::DirectQueryType; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::platform_value::BinaryData; + use dpp::version::PlatformVersion; + use dpp::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; + + fn poll() -> YesNoVotePoll { + YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![0xc1; 32])], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), + } + } + + #[test] + fn should_refuse_to_open_a_poll_a_batch_already_opens() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let vote_poll = poll(); + let mut batch_operations = vec![]; + drive + .open_yes_no_vote_poll_operations( + &vote_poll, + 1_000, + &BlockInfo::default(), + &mut None, + &mut batch_operations, + None, + platform_version, + ) + .expect("expected to open the poll"); + // The same poll with another end date: the end date is not part of its id. + drive + .open_yes_no_vote_poll_operations( + &vote_poll, + 2_000, + &BlockInfo::default(), + &mut None, + &mut batch_operations, + None, + platform_version, + ) + .expect_err("expected the second opening in the batch to be refused"); + } + + #[test] + fn should_create_the_decisions_trees_a_chain_lacks_when_opening_a_poll() { + // A chain born at protocol version 14 has no decisions trees until it upgrades. + let drive = setup_drive_with_initial_state_structure(Some( + PlatformVersion::get(14).expect("expected protocol version 14"), + )); + let platform_version = PlatformVersion::latest(); + let has_decisions_tree = |key: char| { + drive + .grove_has_raw( + (&vote_decisions_tree_path()).into(), + &[key as u8], + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to read the decisions tree") + }; + assert!(!has_decisions_tree(ACTIVE_POLLS_TREE_KEY)); + + let vote_poll = poll(); + drive + .open_yes_no_vote_poll( + &vote_poll, + 1_000, + &BlockInfo::default(), + None, + platform_version, + ) + .expect("expected to open the poll"); + + assert!(has_decisions_tree(ACTIVE_POLLS_TREE_KEY)); + assert!(has_decisions_tree(IDENTITY_VOTES_TREE_KEY)); + assert!(drive + .fetch_yes_no_vote_poll_stored_info(&vote_poll, None, platform_version) + .expect("expected to fetch the stored info") + .is_some()); + } +} diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs index 8e20244aa1a..be9c347cbd0 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs @@ -1,8 +1,12 @@ -use crate::drive::votes::paths::{vote_decisions_active_polls_tree_path_vec, YesNoVotePollPaths}; +use crate::drive::votes::paths::{ + vote_decisions_active_polls_tree_path_vec, vote_decisions_tree_path_vec, YesNoVotePollPaths, + ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY, +}; use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use crate::fees::op::LowLevelDriveOperation::{EphemeralGroveOperation, GroveOperation}; use crate::util::grove_operations::BatchInsertTreeApplyType; use crate::util::object_size_info::{DriveKeyInfo, PathKeyInfo}; use dpp::block::block_info::BlockInfo; @@ -13,6 +17,8 @@ use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollS use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use dpp::voting::vote_polls::VotePoll; use dpp::ProtocolError; +use grovedb::batch::key_info::KeyInfo; +use grovedb::batch::KeyInfoPath; use grovedb::{TransactionArg, TreeType}; impl Drive { @@ -57,20 +63,51 @@ impl Drive { vote_poll.validate_parameters(platform_version)?; let poll_id = vote_poll.unique_id()?; - // The poll's own tree. A poll that already has one was opened before. - let inserted = self.batch_insert_empty_tree_if_not_exists( - PathKeyInfo::PathKey::<0>(( - vote_decisions_active_polls_tree_path_vec(), - poll_id.to_vec(), - )), - TreeType::NormalTree, - None, - BatchInsertTreeApplyType::StatefulBatchInsertTree, - transaction, - previous_batch_operations, - batch_operations, - &platform_version.drive, - )?; + // The decisions trees, when the chain lacks them: genesis and the upgrade to protocol + // version 15 create them, but a chain that reached 15 before they existed did not. The + // same keys in the same order as `insert_vote_decisions_trees`. + for subtree_key in [ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY] { + self.batch_insert_empty_tree_if_not_exists( + PathKeyInfo::PathKey::<0>(( + vote_decisions_tree_path_vec(), + vec![subtree_key as u8], + )), + TreeType::NormalTree, + None, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + transaction, + previous_batch_operations, + batch_operations, + &platform_version.drive, + )?; + } + + // The poll's own tree. A poll that already has one was opened before, and so was one + // whose tree an earlier opening queued in this batch: opened twice, it would be listed + // under two end dates and the second would close a finished poll. + let active_polls_path = vote_decisions_active_polls_tree_path_vec(); + let queued_in_this_batch = { + let path = KeyInfoPath::from_known_owned_path(active_polls_path.clone()); + let key = KeyInfo::KnownKey(poll_id.to_vec()); + batch_operations.iter().any(|operation| { + matches!( + operation, + GroveOperation(queued) | EphemeralGroveOperation(queued, _) + if queued.path == path && queued.key.as_ref() == Some(&key) + ) + }) + }; + let inserted = !queued_in_this_batch + && self.batch_insert_empty_tree_if_not_exists( + PathKeyInfo::PathKey::<0>((active_polls_path, poll_id.to_vec())), + TreeType::NormalTree, + None, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + transaction, + previous_batch_operations, + batch_operations, + &platform_version.drive, + )?; if !inserted { return Err(Error::Protocol(Box::new(ProtocolError::VoteError( format!( diff --git a/packages/rs-drive/src/drive/votes/resolved/vote_polls/mod.rs b/packages/rs-drive/src/drive/votes/resolved/vote_polls/mod.rs index 31a3cc20617..6e1546d9726 100644 --- a/packages/rs-drive/src/drive/votes/resolved/vote_polls/mod.rs +++ b/packages/rs-drive/src/drive/votes/resolved/vote_polls/mod.rs @@ -16,16 +16,12 @@ pub mod contested_document_resource_vote_poll; pub mod resolve; /// Represents a resolved vote poll in the system. -// The contested variant carries the resolved contract; a yes/no poll is a few bytes. -#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, From)] pub enum ResolvedVotePoll { /// A resolved vote poll with contract information for a contested document resource. ContestedDocumentResourceVotePollWithContractInfo( ContestedDocumentResourceVotePollWithContractInfo, ), - /// A yes/no vote poll. It needs nothing from the state to be resolved. - YesNoVotePoll(YesNoVotePoll), } impl From<&ResolvedVotePoll> for VotePoll { @@ -36,9 +32,6 @@ impl From<&ResolvedVotePoll> for VotePoll { ) => VotePoll::ContestedDocumentResourceVotePoll( contested_document_resource_vote_poll.into(), ), - ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll) => { - VotePoll::YesNoVotePoll(yes_no_vote_poll.clone()) - } } } } @@ -79,9 +72,6 @@ impl ResolvedVotePoll { ) => Ok(Some( contested_document_resource_vote_poll.specialized_balance_id()?, )), - ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll) => { - Ok(Some(yes_no_vote_poll.specialized_balance_id()?)) - } } } @@ -100,7 +90,6 @@ impl ResolvedVotePoll { ResolvedVotePoll::ContestedDocumentResourceVotePollWithContractInfo( contested_document_resource_vote_poll, ) => contested_document_resource_vote_poll.unique_id(), - ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll) => yes_no_vote_poll.unique_id(), } } } diff --git a/packages/rs-drive/src/drive/votes/resolved/vote_polls/resolve.rs b/packages/rs-drive/src/drive/votes/resolved/vote_polls/resolve.rs index 40f4b474fa8..c8fc17d3358 100644 --- a/packages/rs-drive/src/drive/votes/resolved/vote_polls/resolve.rs +++ b/packages/rs-drive/src/drive/votes/resolved/vote_polls/resolve.rs @@ -1,6 +1,7 @@ use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::resolve::ContestedDocumentResourceVotePollResolver; use crate::drive::votes::resolved::vote_polls::ResolvedVotePoll; use crate::drive::Drive; +use crate::error::drive::DriveError; use crate::error::Error; use dpp::voting::vote_polls::VotePoll; use grovedb::TransactionArg; @@ -44,6 +45,12 @@ pub trait VotePollResolver { ) -> Result; } +/// Resolution looks up a contested poll's contract; a yes/no poll needs nothing from the state. +/// The end of block check closes a yes/no poll before resolving, and a resource vote naming one +/// is refused before resolution (and does not decode before protocol version 15), so a yes/no +/// poll never reaches here. +const YES_NO_VOTE_POLL_NOT_RESOLVED: &str = "a yes/no vote poll is never resolved"; + impl VotePollResolver for VotePoll { fn resolve( &self, @@ -63,9 +70,9 @@ impl VotePollResolver for VotePoll { ), ) } - VotePoll::YesNoVotePoll(yes_no_vote_poll) => { - Ok(ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll.clone())) - } + VotePoll::YesNoVotePoll(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution( + YES_NO_VOTE_POLL_NOT_RESOLVED, + ))), } } @@ -87,9 +94,9 @@ impl VotePollResolver for VotePoll { ), ) } - VotePoll::YesNoVotePoll(yes_no_vote_poll) => { - Ok(ResolvedVotePoll::YesNoVotePoll(yes_no_vote_poll)) - } + VotePoll::YesNoVotePoll(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution( + YES_NO_VOTE_POLL_NOT_RESOLVED, + ))), } } } diff --git a/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs b/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs index ca1fdfd0957..af290bc7117 100644 --- a/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs +++ b/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs @@ -51,13 +51,8 @@ impl ResolvedVote { } } -impl TryFrom for Vote { - type Error = ProtocolError; - - /// The vote as it was cast. A resource vote resolves to the contested document resource - /// poll it carried, so a resolved resource vote holding a yes/no poll is a programming - /// error, not a vote. - fn try_from(value: ResolvedVote) -> Result { +impl From for Vote { + fn from(value: ResolvedVote) -> Self { match value { ResolvedVote::ResolvedResourceVote(resolved_resource_vote) => { match resolved_resource_vote { @@ -66,23 +61,17 @@ impl TryFrom for Vote { match v0.resolved_vote_poll { ResolvedVotePoll::ContestedDocumentResourceVotePollWithContractInfo( contested_document_resource, - ) => Ok(Self::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + ) => Self::ResourceVote(ResourceVote::V0(ResourceVoteV0 { vote_poll: VotePoll::ContestedDocumentResourceVotePoll( contested_document_resource.into(), ), resource_vote_choice: vote_choice, - }))), - ResolvedVotePoll::YesNoVotePoll(_) => { - Err(ProtocolError::CorruptedCodeExecution( - "a resolved resource vote cannot hold a yes/no vote poll" - .to_string(), - )) - } + })), } } } } - ResolvedVote::YesNoVote(yes_no_vote) => Ok(Self::YesNoVote(yes_no_vote.into())), + ResolvedVote::YesNoVote(yes_no_vote) => Self::YesNoVote(yes_no_vote.into()), } } } diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs index 729eeb3fb8c..ad9cbf4df36 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs @@ -103,10 +103,9 @@ const DKG_MINING_SCHEDULES_V1: &[DkgMiningSchedule] = &[ /// only safe together with this check. /// /// The yes/no vote polls of protocol v15 share the end date index with contested polls: -/// - `voting.clean_up_after_vote_poll_end` is 1 rather than 0: it also cleans up finished yes/no -/// polls, and removes every finished poll of either kind from the end date index itself. -/// - `voting.clean_up_after_contested_resources_vote_poll_end` is 2 rather than 1: version 1 -/// without the end date index removal, which `clean_up_after_vote_poll_end` 1 does instead. +/// `voting.clean_up_after_vote_poll_end` is 1 rather than 0, so the yes/no polls a block ends are +/// cleaned up too. The contested ones still go to `clean_up_after_contested_resources_vote_poll_end` +/// 1, and each clean-up removes its own polls from the end date index. pub const DRIVE_ABCI_METHOD_VERSIONS_V11: DriveAbciMethodVersions = DriveAbciMethodVersions { core_chain_lock: DriveAbciCoreChainLockMethodVersionsAndConstants { verify_chain_lock_locally: 1, @@ -116,7 +115,6 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V11: DriveAbciMethodVersions = DriveAbciMet }, voting: DriveAbciVotingMethodVersions { clean_up_after_vote_poll_end: 1, - clean_up_after_contested_resources_vote_poll_end: 2, ..DRIVE_ABCI_METHOD_VERSIONS_V10.voting }, ..DRIVE_ABCI_METHOD_VERSIONS_V10 diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs index bf23138ac33..d26a38429f7 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs @@ -1,23 +1,20 @@ use crate::version::drive_versions::drive_vote_method_versions::v3::DRIVE_VOTE_METHOD_VERSIONS_V3; use crate::version::drive_versions::drive_vote_method_versions::{ - DriveVoteCleanupMethodVersions, DriveVoteContestedResourceInsertMethodVersions, - DriveVoteMethodVersions, + DriveVoteCleanupMethodVersions, DriveVoteInsertMethodVersions, DriveVoteMethodVersions, }; /// Drive vote method versions 4. Introduced in protocol version 15 with the yes/no poll /// kind: /// -/// * `contested_resource_insert.register_identity_vote` 1 (the slot `register_identity_vote` -/// dispatches on; `insert.register_identity_vote` is read by nothing) dispatches a resolved -/// vote of either kind (v0 knows only contested resource votes). +/// * `insert.register_identity_vote` 1 registers a resolved vote of either kind (v0 knows only +/// contested resource votes, which v1 still hands to it). /// * `remove_all_votes_given_by_identities` 1 also removes the yes/no votes of a masternode /// that left the list. /// /// Everything else matches `DRIVE_VOTE_METHOD_VERSIONS_V3`. pub const DRIVE_VOTE_METHOD_VERSIONS_V4: DriveVoteMethodVersions = DriveVoteMethodVersions { - contested_resource_insert: DriveVoteContestedResourceInsertMethodVersions { + insert: DriveVoteInsertMethodVersions { register_identity_vote: 1, - ..DRIVE_VOTE_METHOD_VERSIONS_V3.contested_resource_insert }, cleanup: DriveVoteCleanupMethodVersions { remove_all_votes_given_by_identities: 1, diff --git a/packages/rs-platform-version/src/version/drive_versions/v10.rs b/packages/rs-platform-version/src/version/drive_versions/v10.rs index 2b11e358ef7..3d6f0987353 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -6,9 +6,9 @@ use crate::version::drive_versions::{DriveMethodVersions, DriveVersion}; /// Introduced in protocol v15: /// /// * **Yes/no vote polls**: `DRIVE_VOTE_METHOD_VERSIONS_V4` bumps -/// `contested_resource_insert.register_identity_vote` to 1, which registers a vote of either -/// poll kind, and `cleanup.remove_all_votes_given_by_identities` to 1, which also removes the -/// yes/no votes of a masternode that left the list. +/// `insert.register_identity_vote` to 1, which registers a vote of either poll kind, and +/// `cleanup.remove_all_votes_given_by_identities` to 1, which also removes the yes/no votes of +/// a masternode that left the list. /// /// Everything else matches `DRIVE_VERSION_V9`. pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 079a0b77bc3..61036fe4837 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -80,18 +80,16 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// (`create_initial_state_structure` 4 behind `>= 15`) and /// `transition_to_version_15` create. Masternode vote `transform_into_action` /// 1 (`DRIVE_ABCI_VALIDATION_VERSIONS_V11`) looks up the previous answer, -/// `clean_up_after_vote_poll_end` 1 and -/// `clean_up_after_contested_resources_vote_poll_end` 2 -/// (`DRIVE_ABCI_METHOD_VERSIONS_V11`) clean finished polls of both kinds out -/// of the shared end date index, and `DRIVE_VERSION_V10` registers and -/// sweeps the votes. Nothing opens a yes/no poll yet, so at this version +/// `clean_up_after_vote_poll_end` 1 (`DRIVE_ABCI_METHOD_VERSIONS_V11`) cleans +/// finished yes/no polls up beside contested ones, and `DRIVE_VERSION_V10` +/// registers and sweeps the votes. Nothing opens a yes/no poll yet, so at this version /// every yes/no vote is refused for want of a poll. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, drive: DRIVE_VERSION_V10, // changed: vote method versions v4: register_identity_vote 1 registers a yes/no vote and remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, - methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock; clean_up_after_vote_poll_end 1 and clean_up_after_contested_resources_vote_poll_end 2 close yes/no polls beside contested ones + methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock; clean_up_after_vote_poll_end 1 cleans up the yes/no polls a block ends beside the contested ones validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: identity update advanced_structure 1 refuses unpaid an identity update whose added key's proof of possession does not verify; masternode vote transform_into_action 1 looks up a masternode's previous answer on a yes/no poll withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, query: DRIVE_ABCI_QUERY_VERSIONS_V2, diff --git a/packages/rs-sdk/src/platform/documents/contest_fund.rs b/packages/rs-sdk/src/platform/documents/contest_fund.rs index 740f43218ff..c3e4a6ca644 100644 --- a/packages/rs-sdk/src/platform/documents/contest_fund.rs +++ b/packages/rs-sdk/src/platform/documents/contest_fund.rs @@ -14,7 +14,6 @@ use dpp::document::DocumentV0Getters; use dpp::fee::Credits; use dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; use dpp::voting::contender_structs::ContenderWithSerializedDocument; -use dpp::voting::vote_polls::VotePoll; use drive::config::DEFAULT_QUERY_LIMIT; use drive::query::vote_poll_vote_state_query::{ ContestedDocumentVotePollDriveQuery, ContestedDocumentVotePollDriveQueryResultType, @@ -56,7 +55,7 @@ impl Sdk { // `generatedFrom` property generated from its params as the platform generates it let mut document = document.clone(); document_type.regenerate_generated_properties(document.properties_mut(), self.version())?; - let Some(VotePoll::ContestedDocumentResourceVotePoll(vote_poll)) = + let Some(vote_poll) = document_type.contested_vote_poll_for_document(&document, self.version())? else { return Ok(None); diff --git a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs index b1a76acdb1c..d9a9d397cce 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs @@ -436,73 +436,163 @@ impl Sdk { limit: Option, ) -> Result, Error> { let dpns_contract_id = self.get_dpns_contract_id()?; - let query_limit = limit.unwrap_or(100); + let query_limit = limit.unwrap_or(MAXIMUM_VOTE_POLLS_BY_END_DATE_LIMIT); let mut name_to_end_time: BTreeMap = BTreeMap::new(); let mut current_start_time = start_time.map(|t| (t, true)); + // A proved query takes no offset, so the pages follow the end dates. The limit counts + // the polls of every kind, and also each empty end date tree in range, which returns no + // row: a page can be cut short with fewer polls than the limit, so only an empty page + // ends the listing. loop { - let query = VotePollsByEndDateDriveQuery { - start_time: current_start_time, - end_time: end_time.map(|t| (t, true)), - limit: Some(query_limit), - offset: None, - order_ascending: true, + let result = self + .fetch_vote_polls_by_end_date(current_start_time, end_time, query_limit) + .await?; + let Some(&(last_ts, _)) = result.0.last() else { + break; }; + let polls_in_page: usize = result.0.iter().map(|(_, polls)| polls.len()).sum(); + record_dpns_contests(&mut name_to_end_time, dpns_contract_id, result); - // Execute the query - let result: VotePollsGroupedByTimestamp = VotePoll::fetch_many(self, query).await?; + if current_start_time == Some((last_ts, true)) && polls_in_page >= query_limit as usize + { + // This end date alone filled the page, so the limit may have cut it, and a + // proved query cannot continue within one end date: read it alone at the + // largest limit, or refuse rather than leave contests out. + if query_limit >= MAXIMUM_VOTE_POLLS_BY_END_DATE_LIMIT { + return Err(too_many_vote_polls_at_end_date(last_ts)); + } + let whole_end_date = self + .fetch_vote_polls_by_end_date( + Some((last_ts, true)), + Some(last_ts), + MAXIMUM_VOTE_POLLS_BY_END_DATE_LIMIT, + ) + .await?; + let polls_at_end_date: usize = + whole_end_date.0.iter().map(|(_, polls)| polls.len()).sum(); + if polls_at_end_date >= MAXIMUM_VOTE_POLLS_BY_END_DATE_LIMIT as usize { + return Err(too_many_vote_polls_at_end_date(last_ts)); + } + record_dpns_contests(&mut name_to_end_time, dpns_contract_id, whole_end_date); + } - // Check if we got any results - if result.0.is_empty() { - break; + match next_vote_polls_page_start(current_start_time, last_ts, end_time) { + Some(next_start_time) => current_start_time = Some(next_start_time), + None => break, } + } + + Ok(name_to_end_time) + } +} - let mut last_timestamp = None; - let mut polls_in_page = 0; +/// The largest page the vote polls by end date query serves (the platform's default query +/// limit), and the page size `get_current_dpns_contests` uses when given none. +const MAXIMUM_VOTE_POLLS_BY_END_DATE_LIMIT: u16 = 100; - // Process each timestamp group - for (timestamp, polls) in result.0 { - polls_in_page += polls.len(); - last_timestamp = Some(timestamp); +impl Sdk { + /// One proved page of vote polls by end date, from `start_time` up to `end_time` included. + async fn fetch_vote_polls_by_end_date( + &self, + start_time: Option<(TimestampMillis, bool)>, + end_time: Option, + limit: u16, + ) -> Result { + let query = VotePollsByEndDateDriveQuery { + start_time, + end_time: end_time.map(|t| (t, true)), + limit: Some(limit), + offset: None, + order_ascending: true, + }; + VotePoll::fetch_many(self, query).await + } +} - for contested_poll in polls.iter().filter_map(|poll| match poll { - VotePoll::ContestedDocumentResourceVotePoll(contested_poll) => { - Some(contested_poll) - } - VotePoll::YesNoVotePoll(_) => None, - }) { - if contested_poll.contract_id == dpns_contract_id - && contested_poll.document_type_name == "domain" - { - // Extract the contested name from index_values - if contested_poll.index_values.len() >= 2 { - if let Value::Text(label) = &contested_poll.index_values[1] { - name_to_end_time.insert(label.clone(), timestamp); - } - } - } +/// Adds the DPNS name contests of a page to `name_to_end_time`; other contests and yes/no polls, +/// which share the end date index, are skipped. A name seen twice is just written again. +fn record_dpns_contests( + name_to_end_time: &mut BTreeMap, + dpns_contract_id: Identifier, + page: VotePollsGroupedByTimestamp, +) { + for (timestamp, polls) in page.0 { + for poll in polls { + let VotePoll::ContestedDocumentResourceVotePoll(contested_poll) = poll else { + continue; + }; + if contested_poll.contract_id == dpns_contract_id + && contested_poll.document_type_name == "domain" + { + // Extract the contested name from index_values + if let Some(Value::Text(label)) = contested_poll.index_values.get(1) { + name_to_end_time.insert(label.clone(), timestamp); } } + } + } +} - // The limit counts every poll of every kind across all of the page's end dates, so - // only a full page can have more behind it. - if polls_in_page < query_limit as usize { - break; - } +/// The error for an end date that holds at least as many vote polls as one page can carry, so +/// that its contests cannot all be listed. +fn too_many_vote_polls_at_end_date(end_date: TimestampMillis) -> Error { + Error::Generic(format!( + "at least {MAXIMUM_VOTE_POLLS_BY_END_DATE_LIMIT} vote polls end at {end_date}, more than \ + one page of vote polls by end date can list" + )) +} - // The limit may have cut the last end date short, so the next page starts at it - // again (a name seen twice is just written again). If that end date alone filled - // the page, starting at it again would return the same page, so move past it. - let Some(last_ts) = last_timestamp else { - break; - }; - current_start_time = if current_start_time == Some((last_ts, true)) { - Some((last_ts, false)) - } else { - Some((last_ts, true)) - }; - } +/// Where the page after one that ended at `last_end_date` starts, or `None` when nothing is left. +/// +/// The limit may have cut the last end date short, so the next page starts at it again. A page +/// that started at that end date held only it, all of it, so the next one starts past it, unless +/// it is the caller's end time: a range starting past its own end is refused. +fn next_vote_polls_page_start( + page_start_time: Option<(TimestampMillis, bool)>, + last_end_date: TimestampMillis, + end_time: Option, +) -> Option<(TimestampMillis, bool)> { + if page_start_time != Some((last_end_date, true)) { + Some((last_end_date, true)) + } else if end_time == Some(last_end_date) { + None + } else { + Some((last_end_date, false)) + } +} - Ok(name_to_end_time) +#[cfg(test)] +mod tests { + use super::next_vote_polls_page_start; + + #[test] + fn should_start_the_next_page_at_the_last_end_date_again() { + assert_eq!(next_vote_polls_page_start(None, 10, None), Some((10, true))); + assert_eq!( + next_vote_polls_page_start(Some((5, true)), 10, Some(20)), + Some((10, true)) + ); + } + + #[test] + fn should_move_past_an_end_date_a_page_held_alone() { + assert_eq!( + next_vote_polls_page_start(Some((10, true)), 10, None), + Some((10, false)) + ); + assert_eq!( + next_vote_polls_page_start(Some((10, true)), 10, Some(20)), + Some((10, false)) + ); + } + + #[test] + fn should_stop_past_the_end_time_instead_of_asking_for_an_empty_range() { + // A start past the end time at the same instant is refused by the query. + assert_eq!( + next_vote_polls_page_start(Some((20, true)), 20, Some(20)), + None + ); } } diff --git a/packages/rs-sdk/src/platform/transition/vote.rs b/packages/rs-sdk/src/platform/transition/vote.rs index 078fdbe8d42..080111dbcc4 100644 --- a/packages/rs-sdk/src/platform/transition/vote.rs +++ b/packages/rs-sdk/src/platform/transition/vote.rs @@ -139,26 +139,23 @@ impl> PutVote for Vote { Ok(_) => {} //todo make this more reliable Err(e) => { - return if e.to_string().contains("already exists") { - match self { - Vote::ResourceVote(_) => { - let vote = - Vote::fetch(sdk, VoteQuery::new(voter_pro_tx_hash, vote_poll_id)) - .await?; - vote.ok_or(Error::Generic( - "vote was proved to not exist but was said to exist".to_string(), - )) - } - // `VoteQuery` reads the contested resource votes index only; no query - // fetches a yes/no vote yet, so the existing vote cannot be returned. - Vote::YesNoVote(_) => Err(Error::Generic( - "the yes/no vote already exists; fetching yes/no votes is not supported yet" - .to_string(), - )), + if !e.to_string().contains("already exists") { + return Err(e.into()); + } + match self { + Vote::ResourceVote(_) => { + let vote = + Vote::fetch(sdk, VoteQuery::new(voter_pro_tx_hash, vote_poll_id)) + .await?; + return vote.ok_or(Error::Generic( + "vote was proved to not exist but was said to exist".to_string(), + )); } - } else { - Err(e.into()) - }; + // `VoteQuery` reads the contested resource votes index only, so no query + // fetches a yes/no vote yet. The duplicate is this very transition, already + // delivered: wait for it as for one just broadcast, which proves it executed. + Vote::YesNoVote(_) => {} + } } } Self::wait_for_response(sdk, masternode_vote_transition, Some(settings)).await diff --git a/packages/wasm-dpp2/src/voting/resource_vote.rs b/packages/wasm-dpp2/src/voting/resource_vote.rs index 68f6ad47ae9..c7a474fd97d 100644 --- a/packages/wasm-dpp2/src/voting/resource_vote.rs +++ b/packages/wasm-dpp2/src/voting/resource_vote.rs @@ -1,8 +1,6 @@ -use crate::error::WasmDppResult; use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::voting::resource_vote_choice::ResourceVoteChoiceWasm; -use crate::voting::vote::contested_poll_for_resource_vote; use crate::voting::vote_poll::VotePollWasm; use dpp::voting::votes::resource_vote::ResourceVote; use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; @@ -59,14 +57,11 @@ impl From for ResourceVote { #[wasm_bindgen(js_class = ResourceVote)] impl ResourceVoteWasm { #[wasm_bindgen(constructor)] - pub fn constructor( - poll: &VotePollWasm, - choice: &ResourceVoteChoiceWasm, - ) -> WasmDppResult { - Ok(ResourceVoteWasm(ResourceVote::V0(ResourceVoteV0 { - vote_poll: contested_poll_for_resource_vote(poll)?, + pub fn constructor(poll: &VotePollWasm, choice: &ResourceVoteChoiceWasm) -> Self { + ResourceVoteWasm(ResourceVote::V0(ResourceVoteV0 { + vote_poll: poll.clone().into(), resource_vote_choice: choice.clone().into(), - }))) + })) } #[wasm_bindgen(getter = poll)] @@ -80,18 +75,16 @@ impl ResourceVoteWasm { } #[wasm_bindgen(setter = poll)] - pub fn set_poll(&mut self, poll: &VotePollWasm) -> WasmDppResult<()> { + pub fn set_poll(&mut self, poll: &VotePollWasm) { let ResourceVote::V0(ResourceVoteV0 { resource_vote_choice, .. }) = self.0.clone(); self.0 = ResourceVote::V0(ResourceVoteV0 { - vote_poll: contested_poll_for_resource_vote(poll)?, + vote_poll: poll.clone().into(), resource_vote_choice, }); - - Ok(()) } #[wasm_bindgen(setter = choice)] diff --git a/packages/wasm-dpp2/src/voting/vote.rs b/packages/wasm-dpp2/src/voting/vote.rs index 179adfb4915..ba1124182be 100644 --- a/packages/wasm-dpp2/src/voting/vote.rs +++ b/packages/wasm-dpp2/src/voting/vote.rs @@ -89,13 +89,11 @@ impl VoteWasm { #[wasm_bindgen(js_name = "votePoll")] vote_poll: &VotePollWasm, #[wasm_bindgen(js_name = "resourceVoteChoice")] resource_vote_choice: &ResourceVoteChoiceWasm, - ) -> WasmDppResult { - Ok(VoteWasm(Vote::ResourceVote(ResourceVote::V0( - ResourceVoteV0 { - vote_poll: contested_poll_for_resource_vote(vote_poll)?, - resource_vote_choice: resource_vote_choice.clone().into(), - }, - )))) + ) -> Self { + VoteWasm(Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + vote_poll: vote_poll.clone().into(), + resource_vote_choice: resource_vote_choice.clone().into(), + }))) } #[wasm_bindgen(getter = poll)] @@ -134,7 +132,7 @@ impl VoteWasm { pub fn set_poll(&mut self, poll: &VotePollWasm) -> WasmDppResult<()> { self.0 = match self.0.clone() { Vote::ResourceVote(vote) => Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { - vote_poll: contested_poll_for_resource_vote(poll)?, + vote_poll: poll.clone().into(), resource_vote_choice: vote.resource_vote_choice(), })), Vote::YesNoVote(vote) => { @@ -167,18 +165,6 @@ impl VoteWasm { } } -/// The poll of a resource vote: a contested document resource poll. Consensus refuses a resource -/// vote that names a yes/no poll, so it is refused here as a yes/no vote refuses a contested poll. -pub(crate) fn contested_poll_for_resource_vote(poll: &VotePollWasm) -> WasmDppResult { - match poll.clone().into() { - VotePoll::YesNoVotePoll(_) => Err(WasmDppError::invalid_argument( - "a resource vote answers a contested document resource vote poll, not a yes/no vote poll" - .to_string(), - )), - vote_poll => Ok(vote_poll), - } -} - /// The resource vote choice exists on a resource vote only. fn not_a_resource_vote() -> WasmDppError { WasmDppError::invalid_argument("this vote is a yes/no vote, not a resource vote".to_string()) diff --git a/packages/wasm-dpp2/src/voting/vote_poll.rs b/packages/wasm-dpp2/src/voting/vote_poll.rs index 65e502cea58..fb40179f3d4 100644 --- a/packages/wasm-dpp2/src/voting/vote_poll.rs +++ b/packages/wasm-dpp2/src/voting/vote_poll.rs @@ -38,13 +38,12 @@ export interface YesNoVotePollFieldsObject { supermajorityNumerator: number; supermajorityDenominator: number; minimumVotingPower: - | { absolute: number } + | { $type: "absolute"; votingPower: number } | { - fractionOfTotal: { - numerator: number; - denominator: number; - rounding: "up" | "down" | "downPlusOne" | "upMinusOne"; - }; + $type: "fractionOfTotal"; + numerator: number; + denominator: number; + rounding: "up" | "down" | "downPlusOne" | "upMinusOne"; }; } @@ -56,13 +55,12 @@ export interface YesNoVotePollFieldsJSON { supermajorityNumerator: number; supermajorityDenominator: number; minimumVotingPower: - | { absolute: number } + | { $type: "absolute"; votingPower: number } | { - fractionOfTotal: { - numerator: number; - denominator: number; - rounding: "up" | "down" | "downPlusOne" | "upMinusOne"; - }; + $type: "fractionOfTotal"; + numerator: number; + denominator: number; + rounding: "up" | "down" | "downPlusOne" | "upMinusOne"; }; } diff --git a/packages/wasm-dpp2/tests/unit/Vote.spec.ts b/packages/wasm-dpp2/tests/unit/Vote.spec.ts index ed5708195f8..8832f2b253f 100644 --- a/packages/wasm-dpp2/tests/unit/Vote.spec.ts +++ b/packages/wasm-dpp2/tests/unit/Vote.spec.ts @@ -87,4 +87,65 @@ describe('Vote', () => { vote.free(); }); }); + + describe('yes/no vote', () => { + // A yes/no vote carries its poll's fields untagged. + const yesNoVoteJSON = { + $type: 'yesNoVote', + $formatVersion: '0', + votePoll: { + resourcePath: ['wcHBwQ=='], + supermajorityNumerator: 2, + supermajorityDenominator: 3, + minimumVotingPower: { $type: 'absolute', votingPower: 400 }, + }, + voteChoice: 'yes', + }; + + it('should serialize toJSON() with the yesNoVote tag and the answer', () => { + const vote = wasm.Vote.fromJSON(yesNoVoteJSON); + const json = vote.toJSON(); + + expect(json.$type).to.equal('yesNoVote'); + expect(json.$formatVersion).to.equal('0'); + expect(json.votePoll).to.deep.equal(yesNoVoteJSON.votePoll); + expect(json.voteChoice).to.equal('yes'); + expect(json).to.deep.equal(yesNoVoteJSON); + + vote.free(); + }); + + it('should round-trip through toObject() and fromObject()', () => { + const vote = wasm.Vote.fromJSON(yesNoVoteJSON); + const obj = vote.toObject(); + + expect(obj.$type).to.equal('yesNoVote'); + expect(obj.votePoll.resourcePath[0]).to.be.instanceOf(Uint8Array); + expect(obj.voteChoice).to.equal('yes'); + + const restored = wasm.Vote.fromObject(obj); + expect(restored.toObject()).to.deep.equal(obj); + + vote.free(); + restored.free(); + }); + + it('should expose its answer and poll, and refuse the resource vote choice', () => { + const vote = wasm.Vote.fromJSON(yesNoVoteJSON); + + expect(vote.yesNoChoice).to.equal('yes'); + expect(vote.poll.kind).to.equal('yesNoVotePoll'); + expect(() => vote.choice).to.throw(); + + vote.free(); + }); + + it('should refuse the yes/no answer of a resource vote', () => { + const vote = new wasm.Vote(createPoll(), wasm.ResourceVoteChoice.Abstain()); + + expect(() => vote.yesNoChoice).to.throw(); + + vote.free(); + }); + }); }); diff --git a/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts b/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts index a5dd2e4f755..0f800581683 100644 --- a/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts +++ b/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts @@ -121,4 +121,74 @@ describe('VotePoll', () => { poll.free(); }); }); + + describe('yes/no vote poll', () => { + // A yes/no poll has no constructor of its own; it comes from its object or JSON form. + const yesNoPollJSON = { + $type: 'yesNoVotePoll', + resourcePath: ['wcHBwQ=='], + supermajorityNumerator: 2, + supermajorityDenominator: 3, + minimumVotingPower: { $type: 'absolute', votingPower: 400 }, + }; + + it('should serialize toJSON() with the yesNoVotePoll tag and its rule', () => { + const poll = wasm.VotePoll.fromJSON(yesNoPollJSON); + const json = poll.toJSON(); + + expect(json.$type).to.equal('yesNoVotePoll'); + expect(json.resourcePath).to.deep.equal(['wcHBwQ==']); + expect(json.supermajorityNumerator).to.equal(2); + expect(json.supermajorityDenominator).to.equal(3); + expect(json.minimumVotingPower).to.deep.equal({ $type: 'absolute', votingPower: 400 }); + + poll.free(); + }); + + it('should round-trip a share minimum through fromJSON() and toJSON()', () => { + const fixture = { + ...yesNoPollJSON, + minimumVotingPower: { + $type: 'fractionOfTotal', + numerator: 1, + denominator: 3, + rounding: 'up', + }, + }; + const poll = wasm.VotePoll.fromJSON(fixture); + + expect(poll.toJSON()).to.deep.equal(fixture); + + poll.free(); + }); + + it('should serialize toObject() with Uint8Array path segments and round-trip fromObject()', () => { + const poll = wasm.VotePoll.fromJSON(yesNoPollJSON); + const obj = poll.toObject(); + + expect(obj.$type).to.equal('yesNoVotePoll'); + expect(obj.resourcePath).to.have.lengthOf(1); + expect(obj.resourcePath[0]).to.be.instanceOf(Uint8Array); + expect(Array.from(obj.resourcePath[0])).to.deep.equal([0xc1, 0xc1, 0xc1, 0xc1]); + expect(obj.minimumVotingPower.$type).to.equal('absolute'); + + const restored = wasm.VotePoll.fromObject(obj); + expect(restored.toObject()).to.deep.equal(obj); + + poll.free(); + restored.free(); + }); + + it('should report its kind and refuse the contested poll getters', () => { + const poll = wasm.VotePoll.fromJSON(yesNoPollJSON); + + expect(poll.kind).to.equal('yesNoVotePoll'); + expect(() => poll.contractId).to.throw(); + expect(() => poll.documentTypeName).to.throw(); + expect(() => poll.indexName).to.throw(); + expect(() => poll.indexValues).to.throw(); + + poll.free(); + }); + }); }); diff --git a/packages/wasm-sdk/src/state_transitions/identity.rs b/packages/wasm-sdk/src/state_transitions/identity.rs index be440bf70a1..0fd35bb11ab 100644 --- a/packages/wasm-sdk/src/state_transitions/identity.rs +++ b/packages/wasm-sdk/src/state_transitions/identity.rs @@ -785,13 +785,6 @@ impl WasmSdk { let pro_tx_hash: Identifier = IdentifierWasm::try_from_options(&options, "masternodeProTxHash")?.into(); let vote_poll: VotePoll = VotePollWasm::try_from_options(&options, "votePoll")?.into(); - // This casts a resource vote, which answers a contested document resource poll only; - // consensus refuses one naming a yes/no poll. - if let VotePoll::YesNoVotePoll(_) = vote_poll { - return Err(WasmSdkError::invalid_argument( - "masternodeVote casts a resource vote, which cannot answer a yes/no vote poll", - )); - } let resource_vote_choice: ResourceVoteChoice = ResourceVoteChoiceWasm::try_from_options(&options, "voteChoice")?.into(); let voting_public_key: IdentityPublicKey = From 38e9563d4052d107e8bac06e939f40b6d7dfc339 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 22 Oct 2026 09:57:19 +0300 Subject: [PATCH 07/18] fix(platform): apply the second yes/no vote poll review round Opening polls: - opening a yes/no poll is refused before protocol version 15, so no yes/no poll can reach the end date index there (since the opener creates the decisions trees itself, that no longer followed from the trees' absence). - a poll whose end date is not after the opening block is refused. - a tree two openings in one batch share (the decisions trees, an end date's tree) is queued once; the decisions subtree keys are one constant. Closing polls: - check_for_ended_vote_polls v1 sends finished yes/no polls straight to the yes/no clean-up after the contested one, so clean_up_after_vote_polls_end keeps v0 at every version: its v1, its PV15 slot and the yes/no variant of ResolvedVotePollWithVotes are gone, and its v0 is back to base. - an end date entry of a poll an earlier entry already closed is only removed, instead of failing the block on a finished poll. - the voters fetch returns a flat list, and the clean-up computes each poll id once. Shared code: - transform_into_action v0 and v1 share the vote-change rule and the masternode lookup (moved unchanged out of v0); v0 uses masternode_vote_strength. - the masternode removal sweep v1 reuses v0's identities-index query and a stored-form decode helper; yes/no reference removal reuses the contested loop; an id-based choice path builds the vote tree path. - the minimum voting power's serde derives through a private repr, the same JSON and Value shape. - PreviousVoteCount lives in drive::votes; dead items removed. Docs and records: in-place comments on the type-only and dispatcher-slot edits and on the vote verifier's routing; the contested drain is frozen from protocol version 8, not 14; the yes/no poll id descriptions name the domain tag. Clients: get_current_dpns_contests re-reads an empty page at the full page size before ending, and uses DEFAULT_QUERY_LIMIT. Tests: a shared yes/no fixture in rs-drive and shared CheckTx and credit helpers in drive-abci; a CheckTx error counts as a refusal only where the block reports an internal error; frozen v0 tests pin protocol version 14; new tests for the opener refusals, shared trees in one batch, and a poll listed under two end dates. Co-Authored-By: Claude Opus 5.5 --- .../methods/versioned_methods.rs | 5 + packages/rs-dpp/src/state_transition/mod.rs | 2 +- .../voting/vote_polls/yes_no_vote_poll/mod.rs | 135 +++----- .../src/voting/votes/yes_no_vote/v0/mod.rs | 7 - .../v0/mod.rs | 2 +- .../check_for_ended_vote_polls/v1/mod.rs | 70 +++- .../v1/mod.rs | 6 +- .../clean_up_after_vote_polls_end/mod.rs | 10 +- .../clean_up_after_vote_polls_end/v0/mod.rs | 13 +- .../clean_up_after_vote_polls_end/v1/mod.rs | 82 ----- .../mod.rs | 18 +- .../v0/mod.rs | 49 ++- .../advanced_structure_v1/mod.rs | 3 + .../masternode_vote/charter_election_tests.rs | 26 +- .../transform_into_action/v0/mod.rs | 140 ++++---- .../transform_into_action/v1/mod.rs | 74 ++--- .../masternode_vote/yes_no_tests.rs | 305 ++++++++---------- .../state_transition/state_transitions/mod.rs | 23 ++ .../src/test/helpers/state_mutation_guard.rs | 63 ++-- packages/rs-drive/grovedb-structure.json | 6 +- .../mod.rs | 3 + .../v0/mod.rs | 73 +++-- .../v1/mod.rs | 59 +--- .../v0/mod.rs | 21 ++ .../v0/mod.rs | 27 +- .../mod.rs | 8 +- .../v0/mod.rs | 36 +-- .../fetch/fetch_identity_yes_no_vote/mod.rs | 4 +- .../fetch_identity_yes_no_vote/v0/mod.rs | 14 +- .../insert/register_identity_vote/mod.rs | 4 + .../yes_no/open_yes_no_vote_poll/mod.rs | 94 +++++- .../yes_no/open_yes_no_vote_poll/v0/mod.rs | 139 +++++--- packages/rs-drive/src/drive/votes/mod.rs | 3 + packages/rs-drive/src/drive/votes/paths.rs | 31 +- .../drive/votes/resolved/vote_polls/mod.rs | 9 - .../votes/resolved_yes_no_vote/mod.rs | 6 +- .../setup/insert_vote_decisions_trees/mod.rs | 10 +- .../yes_no_vote_reference_storage_form/mod.rs | 14 + .../rs-drive/src/drive/votes/structure.rs | 15 +- .../src/query/yes_no_vote_poll_state_query.rs | 51 +-- .../identity/masternode_vote/v0/mod.rs | 2 +- packages/rs-drive/src/structure/tests.rs | 48 +-- .../rs-drive/src/util/test_helpers/mod.rs | 55 ++++ .../v0/mod.rs | 42 +-- .../voting/verify_masternode_vote/mod.rs | 4 + .../verify_masternode_yes_no_vote/mod.rs | 72 +---- .../drive_abci_method_versions/v11.rs | 14 +- .../drive_vote_method_versions/mod.rs | 4 +- .../drive_vote_method_versions/v1.rs | 1 - .../drive_vote_method_versions/v2.rs | 1 - .../drive_vote_method_versions/v3.rs | 1 - .../rs-platform-version/src/version/v15.rs | 10 +- .../dpns_usernames/contested_queries.rs | 22 +- 53 files changed, 935 insertions(+), 1001 deletions(-) delete mode 100644 packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v1/mod.rs diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs b/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs index f694f8213af..72c518aafce 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs @@ -372,6 +372,9 @@ pub trait DocumentTypeV0MethodsVersioned: DocumentTypeV0Getters + DocumentTypeBa } /// Figures out the prefunded voting balance (v0) for a document in a document type + // In place, type only: this v0 (every protocol version) returned the same poll wrapped in + // `VotePoll::ContestedDocumentResourceVotePoll`, the only variant it ever built. The poll + // and every check made on it are unchanged. fn contested_vote_poll_for_document_v0( &self, document: &Document, @@ -380,6 +383,8 @@ pub trait DocumentTypeV0MethodsVersioned: DocumentTypeV0Getters + DocumentTypeBa self.contested_vote_poll_for_document_properties_v0(document.properties(), platform_version) } + // In place, type only, as for `contested_vote_poll_for_document_v0`: the poll built below is + // the one this v0 always built, no longer wrapped in `VotePoll`. fn contested_vote_poll_for_document_properties_v0( &self, document_properties: &BTreeMap, diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 96ae361b578..a17ad8178a1 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -2735,7 +2735,7 @@ mod tests { /// protocol version 15 refuses as naming no contested poll). Before 15 neither decodes, so /// the masternode vote validators selected there never meet a yes/no poll. #[test] - fn test_active_version_range_masternode_vote_on_a_yes_no_poll_starts_at_15() { + fn should_make_a_masternode_vote_on_a_yes_no_poll_active_from_protocol_version_15() { use crate::serialization::PlatformSerializable; use crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use crate::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; diff --git a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs index 8d89e01b25c..58ca3758207 100644 --- a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs @@ -53,6 +53,14 @@ pub enum VotingPowerRounding { /// `{ "$type": "absolute", "votingPower": 400 }` or /// `{ "$type": "fractionOfTotal", "numerator": 1, "denominator": 3, "rounding": "up" }`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde( + into = "YesNoMinimumVotingPowerRepr", + from = "YesNoMinimumVotingPowerRepr" + ) +)] pub enum YesNoMinimumVotingPower { /// A fixed voting power, at most `max_yes_no_vote_poll_minimum_voting_power`. Absolute(VotingPower), @@ -66,99 +74,55 @@ pub enum YesNoMinimumVotingPower { }, } -// `Absolute` wraps a bare number, which serde's internal tagging cannot carry, so both -// directions are written out, as for `ResourceVoteChoice`. +// Internal-`$type` serde shape: `Absolute` is a tuple variant, which internal tagging cannot +// carry, so this struct-variant helper names its field. Bincode, and with it the poll id, still +// encodes the outer enum directly. #[cfg(feature = "serde-conversion")] -impl Serialize for YesNoMinimumVotingPower { - fn serialize(&self, serializer: S) -> Result { - use serde::ser::SerializeMap; - match self { - YesNoMinimumVotingPower::Absolute(voting_power) => { - let mut map = serializer.serialize_map(Some(2))?; - map.serialize_entry("$type", "absolute")?; - map.serialize_entry("votingPower", voting_power)?; - map.end() - } +#[derive(Serialize, Deserialize)] +#[serde(tag = "$type", rename_all = "camelCase")] +enum YesNoMinimumVotingPowerRepr { + #[serde(rename_all = "camelCase")] + Absolute { voting_power: VotingPower }, + FractionOfTotal { + numerator: u8, + denominator: u8, + rounding: VotingPowerRounding, + }, +} + +#[cfg(feature = "serde-conversion")] +impl From for YesNoMinimumVotingPowerRepr { + fn from(minimum: YesNoMinimumVotingPower) -> Self { + match minimum { + YesNoMinimumVotingPower::Absolute(voting_power) => Self::Absolute { voting_power }, YesNoMinimumVotingPower::FractionOfTotal { numerator, denominator, rounding, - } => { - let mut map = serializer.serialize_map(Some(4))?; - map.serialize_entry("$type", "fractionOfTotal")?; - map.serialize_entry("numerator", numerator)?; - map.serialize_entry("denominator", denominator)?; - map.serialize_entry("rounding", rounding)?; - map.end() - } + } => Self::FractionOfTotal { + numerator, + denominator, + rounding, + }, } } } #[cfg(feature = "serde-conversion")] -impl<'de> Deserialize<'de> for YesNoMinimumVotingPower { - fn deserialize>(deserializer: D) -> Result { - use serde::de::{self, MapAccess, Visitor}; - - struct MinimumVisitor; - - impl<'de> Visitor<'de> for MinimumVisitor { - type Value = YesNoMinimumVotingPower; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("YesNoMinimumVotingPower as a map with a `$type` discriminator") - } - - fn visit_map>(self, mut map: A) -> Result { - let mut variant: Option = None; - let mut voting_power: Option = None; - let mut numerator: Option = None; - let mut denominator: Option = None; - let mut rounding: Option = None; - - while let Some(key) = map.next_key::()? { - match key.as_str() { - "$type" if variant.is_none() => variant = Some(map.next_value()?), - "votingPower" if voting_power.is_none() => { - voting_power = Some(map.next_value()?) - } - "numerator" if numerator.is_none() => numerator = Some(map.next_value()?), - "denominator" if denominator.is_none() => { - denominator = Some(map.next_value()?) - } - "rounding" if rounding.is_none() => rounding = Some(map.next_value()?), - "$type" | "votingPower" | "numerator" | "denominator" | "rounding" => { - return Err(de::Error::custom(format!("duplicate field `{key}`"))); - } - _ => { - let _: de::IgnoredAny = map.next_value()?; - } - } - } - - match variant - .ok_or_else(|| de::Error::missing_field("$type"))? - .as_str() - { - "absolute" => Ok(YesNoMinimumVotingPower::Absolute( - voting_power.ok_or_else(|| de::Error::missing_field("votingPower"))?, - )), - "fractionOfTotal" => Ok(YesNoMinimumVotingPower::FractionOfTotal { - numerator: numerator - .ok_or_else(|| de::Error::missing_field("numerator"))?, - denominator: denominator - .ok_or_else(|| de::Error::missing_field("denominator"))?, - rounding: rounding.ok_or_else(|| de::Error::missing_field("rounding"))?, - }), - other => Err(de::Error::unknown_variant( - other, - &["absolute", "fractionOfTotal"], - )), - } - } +impl From for YesNoMinimumVotingPower { + fn from(repr: YesNoMinimumVotingPowerRepr) -> Self { + match repr { + YesNoMinimumVotingPowerRepr::Absolute { voting_power } => Self::Absolute(voting_power), + YesNoMinimumVotingPowerRepr::FractionOfTotal { + numerator, + denominator, + rounding, + } => Self::FractionOfTotal { + numerator, + denominator, + rounding, + }, } - - deserializer.deserialize_map(MinimumVisitor) } } @@ -232,9 +196,10 @@ impl fmt::Display for YesNoMinimumVotingPower { /// the poll so every feature sets its own. Two thirds with a floor of 400 is numerator 2, /// denominator 3 and an absolute minimum of 400. /// -/// The parameters are part of the poll's identity: [`Self::unique_id`] hashes the serialized -/// [`VotePoll`] that carries the poll, so a vote names the exact poll it answers. The prefunded -/// balance that pays for the votes has the same id. +/// The parameters are part of the poll's identity: [`Self::unique_id`] is the double SHA-256 of +/// the domain tag `dash platform yes/no vote poll id` followed by the serialized [`VotePoll`] +/// that carries the poll, so a vote names the exact poll it answers. The prefunded balance that +/// pays for the votes has the same id. #[cfg_attr(feature = "json-conversion", json_safe_fields)] #[cfg_attr( all(feature = "json-conversion", feature = "serde-conversion"), diff --git a/packages/rs-dpp/src/voting/votes/yes_no_vote/v0/mod.rs b/packages/rs-dpp/src/voting/votes/yes_no_vote/v0/mod.rs index e7df7111966..bfc9d564b12 100644 --- a/packages/rs-dpp/src/voting/votes/yes_no_vote/v0/mod.rs +++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/v0/mod.rs @@ -7,7 +7,6 @@ use bincode::{Decode, DecodeUntrusted, Encode}; use platform_serialization_derive::{ PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, }; -use platform_value::Identifier; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; @@ -37,9 +36,3 @@ pub struct YesNoVoteV0 { /// The answer. pub vote_choice: YesNoAbstainVoteChoice, } - -impl YesNoVoteV0 { - pub fn vote_poll_unique_id(&self) -> Result { - self.vote_poll.unique_id() - } -} diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index d290d128d9e..af3e68c1cfd 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -2499,7 +2499,7 @@ mod tests { /// byte-identical to the one of a chain born at 15, so both node populations build the /// same Merk when the first yes/no poll opens. #[test] - fn test_transition_to_version_15_creates_vote_decisions_trees() { + fn should_create_the_vote_decisions_trees_on_transition_to_version_15() { use drive::drive::votes::paths::{ vote_decisions_tree_path, ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY, }; diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs index 71c36488002..7495101e1f4 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs @@ -7,6 +7,7 @@ use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; use dpp::data_contract::document_type::ContestedIndexResolution; use dpp::document::DocumentV0Getters; +use dpp::identifier::Identifier; use dpp::prelude::TimestampMillis; use dpp::version::PlatformVersion; use dpp::voting::contender_structs::FinalizedContender; @@ -80,22 +81,36 @@ where platform_version, )?; + // The yes/no polls this block closes, with their voters, and the end date entries of + // yes/no polls an earlier entry already closed. Both stay empty before protocol + // version 15. + let mut finished_yes_no_vote_polls: Vec<(YesNoVotePoll, TimestampMillis, Vec)> = + vec![]; + let mut leftover_yes_no_end_date_entries: Vec<(Identifier, TimestampMillis)> = vec![]; + // Check if awarding is disabled let vote_polls_with_info = vote_polls_by_timestamp.into_iter().map(|(end_date, vote_polls)| { let vote_polls_with_votes = vote_polls.into_iter().map(|vote_poll| { // Reached from protocol version 15 only. At 14, which also selects this // generation, no yes/no poll is in the end date index: it enters only - // through `Drive::open_yes_no_vote_poll`, which writes under the `Votes/d/p` - // tree that exists from 15 (genesis structure v4 behind `>= 15`, - // `transition_to_version_15`). A yes/no poll needs no resolution. + // through `Drive::open_yes_no_vote_poll`, which refuses to open one before + // protocol version 15. A yes/no poll needs no resolution, and is cleaned up + // apart from the contested polls below. if let VotePoll::YesNoVotePoll(yes_no_vote_poll) = vote_poll { - return self.finish_yes_no_vote_poll( + match self.finish_yes_no_vote_poll( block_platform_state, block_info, - yes_no_vote_poll, + &yes_no_vote_poll, transaction, platform_version, - ); + )? { + Some(voters) => { + finished_yes_no_vote_polls.push((yes_no_vote_poll, end_date, voters)) + } + None => leftover_yes_no_end_date_entries + .push((yes_no_vote_poll.unique_id()?, end_date)), + } + return Ok(None); } let resolved_vote_poll = vote_poll.resolve(&self.drive, transaction, platform_version)?; @@ -226,10 +241,10 @@ where transaction, platform_version, )?; - Ok(ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(resolved_contested_document_resource_vote_poll, identifiers_voting_for_contenders)) + Ok(Some(ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(resolved_contested_document_resource_vote_poll, identifiers_voting_for_contenders))) } } - }).collect::, Error>>()?; + }).collect::>, Error>>()?.into_iter().flatten().collect::>(); Ok((end_date, vote_polls_with_votes)) }).collect::>, Error>>() ?; @@ -246,22 +261,45 @@ where )?; } + // Then the yes/no polls, in a batch of their own: its read of a shared end date sees + // what the contested clean-up left there. + if !finished_yes_no_vote_polls.is_empty() || !leftover_yes_no_end_date_entries.is_empty() { + self.clean_up_after_yes_no_vote_polls_end( + block_info, + &finished_yes_no_vote_polls, + &leftover_yes_no_end_date_entries, + transaction, + platform_version, + )?; + } + Ok(()) } /// Closes one yes/no poll whose time has come: tallies it, records the result in its /// stored info, hands the result to the feature that opened the poll, and returns the /// voters so the clean-up can remove their votes. + /// + /// Returns `None` for a poll that is already finished: one listed under two end dates + /// (two openings that could not see each other) closes at the first, and its later entry + /// is only removed. fn finish_yes_no_vote_poll( &self, block_platform_state: &PlatformState, block_info: &BlockInfo, - vote_poll: YesNoVotePoll, + vote_poll: &YesNoVotePoll, transaction: TransactionArg, platform_version: &PlatformVersion, - ) -> Result { + ) -> Result>, Error> { + let already_finished = self + .drive + .fetch_yes_no_vote_poll_stored_info(vote_poll, transaction, platform_version)? + .is_some_and(|stored_info| stored_info.result().is_some()); + if already_finished { + return Ok(None); + } let tally = - self.tally_votes_for_yes_no_vote_poll(&vote_poll, transaction, platform_version)?; + self.tally_votes_for_yes_no_vote_poll(vote_poll, transaction, platform_version)?; // A minimum given as a share of the total is measured against the masternode list of // the closing block, each masternode weighed as its vote is. A fixed minimum reads no // total. @@ -279,7 +317,7 @@ where }; let result = self.keep_record_of_finished_yes_no_vote_poll( block_info, - &vote_poll, + vote_poll, &tally, total_voting_power, transaction, @@ -287,18 +325,16 @@ where )?; self.on_yes_no_vote_poll_finished( block_info, - &vote_poll, + vote_poll, &result, transaction, platform_version, )?; let voters = self.drive.fetch_identities_voting_in_yes_no_vote_poll( - &vote_poll, + vote_poll, transaction, platform_version, )?; - Ok(ResolvedVotePollWithVotes::YesNoVotePollWithVotes( - vote_poll, voters, - )) + Ok(Some(voters)) } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v1/mod.rs index 714ed483da9..9c38054b1c3 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_contested_resources_vote_polls_end/v1/mod.rs @@ -67,9 +67,9 @@ where /// Empties the prefunded balances of ended vote polls, in the given order, and credits what /// they held to the epoch's processing pool, appending the operations. Moved here unchanged - /// from the body above, which shipped with protocol version 14, so its operations must not - /// change; the yes/no clean-up v0 (protocol version 15) drains its polls' balances through it - /// too. + /// from the body above, which every protocol version from 8 selects (mainnet included), so + /// its operations must not change; the yes/no clean-up v0 (protocol version 15) drains its + /// polls' balances through it too. pub(in crate::execution::platform_events::voting) fn empty_vote_poll_prefunded_balances_operations_v1( &self, specialized_balance_ids: impl IntoIterator>, diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/mod.rs index b5186a05005..6580b030998 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/mod.rs @@ -10,7 +10,6 @@ use drive::grovedb::TransactionArg; use std::collections::BTreeMap; mod v0; -mod v1; impl Platform where @@ -38,16 +37,9 @@ where transaction, platform_version, ), - 1 => self.clean_up_after_vote_polls_end_v1( - block_info, - vote_polls, - clean_up_testnet_corrupted_reference_issue, - transaction, - platform_version, - ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "clean_up_after_vote_polls_end".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs index 8797f5decef..1d1b4186e9f 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs @@ -1,4 +1,3 @@ -use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; @@ -41,17 +40,7 @@ where match vote_poll { ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(contested_poll, vote_info) => { contested_polls.push((contested_poll, end_date, vote_info)); - } - // Unreachable: every table that selects this v0 (protocol versions 1 to 14) - // selects `check_for_ended_vote_polls` v0 or v1, which build this variant - // only for a yes/no poll in the end date index, and none can be there - // before 15: it enters only through `Drive::open_yes_no_vote_poll`, which - // writes under the `Votes/d/p` tree that exists from 15. - ResolvedVotePollWithVotes::YesNoVotePollWithVotes(..) => { - return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( - "yes/no vote polls need clean_up_after_vote_polls_end version 1", - ))) - } + } // Add more match arms here for other types of vote polls in the future } } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v1/mod.rs deleted file mode 100644 index d28938684c4..00000000000 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v1/mod.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::error::Error; -use crate::platform_types::platform::Platform; -use crate::rpc::core::CoreRPCLike; -use dpp::block::block_info::BlockInfo; -use dpp::identifier::Identifier; -use dpp::prelude::TimestampMillis; -use dpp::version::PlatformVersion; -use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; -use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; -use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; -use drive::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfo; -use drive::drive::votes::resolved::vote_polls::ResolvedVotePollWithVotes; -use drive::grovedb::TransactionArg; -use std::collections::BTreeMap; - -impl Platform -where - C: CoreRPCLike, -{ - /// Cleans up after the polls that ended in this block. Version 1 (protocol version 15) - /// handles both kinds: the contested polls go to the contested clean-up and the yes/no - /// polls to the yes/no clean-up. Each removes its own polls from the shared end date index - /// and applies its own batch, and each deletes an end date's tree only once its read of - /// the tree finds nothing of either kind left, so the two can run one after the other. - #[inline(always)] - pub(super) fn clean_up_after_vote_polls_end_v1( - &self, - block_info: &BlockInfo, - vote_polls: &BTreeMap>, - clean_up_testnet_corrupted_reference_issue: bool, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { - // TODO: Use type or struct - #[allow(clippy::type_complexity)] - let mut contested_polls: Vec<( - &ContestedDocumentResourceVotePollWithContractInfo, - &TimestampMillis, - &BTreeMap>, - )> = Vec::new(); - #[allow(clippy::type_complexity)] - let mut yes_no_polls: Vec<( - &YesNoVotePoll, - &TimestampMillis, - &BTreeMap>, - )> = Vec::new(); - - for (end_date, vote_polls_for_time) in vote_polls { - for vote_poll in vote_polls_for_time { - match vote_poll { - ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(contested_poll, vote_info) => { - contested_polls.push((contested_poll, end_date, vote_info)); - } - ResolvedVotePollWithVotes::YesNoVotePollWithVotes(yes_no_poll, voters) => { - yes_no_polls.push((yes_no_poll, end_date, voters)); - } - } - } - } - - if !contested_polls.is_empty() { - self.clean_up_after_contested_resources_vote_polls_end( - block_info, - contested_polls, - clean_up_testnet_corrupted_reference_issue, - transaction, - platform_version, - )?; - } - - if !yes_no_polls.is_empty() { - self.clean_up_after_yes_no_vote_polls_end( - block_info, - yes_no_polls, - transaction, - platform_version, - )?; - } - - Ok(()) - } -} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs index 1a195cb3d97..0e5b154881e 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs @@ -6,10 +6,8 @@ use dpp::block::block_info::BlockInfo; use dpp::identifier::Identifier; use dpp::prelude::TimestampMillis; use dpp::version::PlatformVersion; -use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use drive::grovedb::TransactionArg; -use std::collections::BTreeMap; mod v0; @@ -17,19 +15,18 @@ impl Platform where C: CoreRPCLike, { - /// Cleans up after yes/no polls that ended: their entries in the end date index (each - /// given with its end date), their votes and vote trees, the voters' entries in the + /// Cleans up after yes/no polls that ended, each given with its end date and its voters: + /// their entries in the end date index, their vote trees, the voters' entries in the /// identity votes index, and what is left of the prefunded balance that paid for the votes, /// which goes to the processing pool. The stored info stays as the record of the decision. - #[allow(clippy::type_complexity)] + /// + /// `leftover_end_date_entries` are the end date entries, by poll id, of polls an earlier + /// entry already closed: only the entries go. pub(in crate::execution) fn clean_up_after_yes_no_vote_polls_end( &self, block_info: &BlockInfo, - vote_polls: Vec<( - &YesNoVotePoll, - &TimestampMillis, - &BTreeMap>, - )>, + vote_polls: &[(YesNoVotePoll, TimestampMillis, Vec)], + leftover_end_date_entries: &[(Identifier, TimestampMillis)], transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { @@ -42,6 +39,7 @@ where 0 => self.clean_up_after_yes_no_vote_polls_end_v0( block_info, vote_polls, + leftover_end_date_entries, transaction, platform_version, ), diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs index e1c6c73a8a1..e99996b3368 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs @@ -5,7 +5,6 @@ use dpp::block::block_info::BlockInfo; use dpp::identifier::Identifier; use dpp::prelude::TimestampMillis; use dpp::version::PlatformVersion; -use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use dpp::ProtocolError; use drive::grovedb::TransactionArg; @@ -16,26 +15,30 @@ where C: CoreRPCLike, { #[inline(always)] - #[allow(clippy::type_complexity)] pub(super) fn clean_up_after_yes_no_vote_polls_end_v0( &self, block_info: &BlockInfo, - vote_polls: Vec<( - &YesNoVotePoll, - &TimestampMillis, - &BTreeMap>, - )>, + vote_polls: &[(YesNoVotePoll, TimestampMillis, Vec)], + leftover_end_date_entries: &[(Identifier, TimestampMillis)], transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { let mut operations = vec![]; + // Each poll's id, which its prefunded balance shares. + let vote_poll_ids = vote_polls + .iter() + .map(|(vote_poll, _, _)| vote_poll.unique_id()) + .collect::, ProtocolError>>()?; + // The polls' entries in the end date index, which contested polls share: an end date's // tree goes only once nothing of either kind is left under it. - let end_date_entries = vote_polls + let end_date_entries: Vec<(Identifier, TimestampMillis)> = vote_poll_ids .iter() - .map(|(vote_poll, end_date, _)| Ok((vote_poll.unique_id()?, **end_date))) - .collect::, ProtocolError>>()?; + .zip(vote_polls) + .map(|(vote_poll_id, (_, end_date, _))| (*vote_poll_id, *end_date)) + .chain(leftover_end_date_entries.iter().copied()) + .collect(); self.drive.remove_vote_poll_end_date_query_operations( end_date_entries.as_slice(), &mut operations, @@ -43,10 +46,10 @@ where platform_version, )?; - // The votes and the three vote trees of every poll. + // The three vote trees of every poll. let polls: Vec<&YesNoVotePoll> = vote_polls .iter() - .map(|(vote_poll, _, _)| *vote_poll) + .map(|(vote_poll, _, _)| vote_poll) .collect(); self.drive.remove_yes_no_vote_poll_votes_operations( polls.as_slice(), @@ -55,20 +58,16 @@ where )?; // Each voter's entries in the identity votes index. - let mut poll_ids_by_voter: BTreeMap<&Identifier, Vec> = BTreeMap::new(); - for (vote_poll, _, voters_by_choice) in &vote_polls { - let vote_poll_id = vote_poll.unique_id()?; - for voters in voters_by_choice.values() { - for voter in voters { - poll_ids_by_voter - .entry(voter) - .or_default() - .push(vote_poll_id); - } + let mut poll_ids_by_voter: BTreeMap<&Identifier, Vec<&Identifier>> = BTreeMap::new(); + for (vote_poll_id, (_, _, voters)) in vote_poll_ids.iter().zip(vote_polls) { + for voter in voters { + poll_ids_by_voter + .entry(voter) + .or_default() + .push(vote_poll_id); } } for (voter, vote_poll_ids) in poll_ids_by_voter { - let vote_poll_ids: Vec<&Identifier> = vote_poll_ids.iter().collect(); self.drive .remove_yes_no_vote_references_given_by_identity_operations( voter, @@ -81,9 +80,7 @@ where // What is left of the prefunded balances goes to processing, as for contested polls. self.empty_vote_poll_prefunded_balances_operations_v1( - polls - .iter() - .map(|vote_poll| vote_poll.specialized_balance_id()), + vote_poll_ids.iter().map(|vote_poll_id| Ok(*vote_poll_id)), block_info, &mut operations, transaction, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs index c59629f75f5..849f66cf2b3 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs @@ -59,6 +59,9 @@ impl DocumentCreateTransitionActionStructureValidationV1 for DocumentCreateTrans let expected_vote_poll = document_type .contested_vote_poll_for_document_properties(self.data(), platform_version)?; + // In place, type only (protocol versions 14 and 15): the expected poll was the same + // contested poll wrapped in `VotePoll`, the only variant the helper built, so these + // arms match the same cases as before. match (expected_vote_poll, self.prefunded_voting_balance()) { (Some(expected), Some((provided, _))) => { // -->> Changed in V1 <<-- The amount is the most the contender pays, and diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/charter_election_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/charter_election_tests.rs index 82ed2fb453b..94c6eb752ad 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/charter_election_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/charter_election_tests.rs @@ -5,15 +5,14 @@ //! included, keeps the generic windows and fund. use crate::execution::validation::state_transition::state_transitions::tests::{ - create_dpns_identity_name_contest, fill_contest_with_bare_contenders, setup_identity, - setup_masternode_voting_identity, + create_dpns_identity_name_contest, fill_contest_with_bare_contenders, processing_credits, + setup_identity, setup_masternode_voting_identity, }; use crate::platform_types::platform_state::PlatformStateV0Methods; use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; use dpp::block::block_info::BlockInfo; -use dpp::block::epoch::Epoch; use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0; use dpp::consensus::state::state_error::StateError; use dpp::consensus::ConsensusError; @@ -53,8 +52,6 @@ use dpp::voting::votes::Vote; use drive::drive::contract::paths::contract_root_path; use drive::drive::document::ContestWindows; use drive::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::resolve::ContestedDocumentResourceVotePollResolver; -use drive::error::Error as DriveError; -use drive::grovedb::Error as GroveError; use drive::query::VotePollsByEndDateDriveQuery; use drive::util::test_helpers::setup_contract; use platform_version::version::PlatformVersion; @@ -613,25 +610,6 @@ fn balance_of( .expect("expected the identity") } -/// The processing credits epoch 0 will distribute; the item is written by the first credit, so -/// before any it is missing, which is none. -fn processing_credits( - platform: &TempPlatform, - platform_version: &PlatformVersion, -) -> Credits { - match platform - .drive - .get_epoch_processing_credits_for_distribution( - &Epoch::new(0).expect("epoch"), - None, - platform_version, - ) { - Ok(credits) => credits, - Err(DriveError::GroveDB(error)) if matches!(*error, GroveError::PathKeyNotFound(_)) => 0, - Err(error) => panic!("expected the epoch's processing credits: {error:?}"), - } -} - #[tokio::test] async fn should_award_a_single_applicant_when_the_target_join_window_closes() { let (mut platform, platform_version, charters, mut rng) = setup(); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs index aac50c23176..eb76b428c25 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs @@ -1,5 +1,6 @@ use crate::error::execution::ExecutionError; use crate::error::Error; +use crate::execution::platform_events::voting::masternode_vote_strength; use crate::platform_types::platform::PlatformRef; use dpp::consensus::state::state_error::StateError; use dpp::consensus::state::voting::masternode_not_found_error::MasternodeNotFoundError; @@ -8,11 +9,13 @@ use dpp::consensus::state::voting::masternode_voted_too_many_times::MasternodeVo use dpp::consensus::ConsensusError; use dpp::dashcore::hashes::Hash; use dpp::dashcore::ProTxHash; -use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeType; +use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeListItem; +use dpp::identifier::Identifier; use dpp::prelude::ConsensusValidationResult; use dpp::state_transition::masternode_vote_transition::accessors::MasternodeVoteTransitionAccessorsV0; use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; +use drive::drive::votes::PreviousVoteCount; use drive::state_transition_action::identity::masternode_vote::MasternodeVoteTransitionAction; use crate::execution::validation::state_transition::ValidationMode; @@ -58,49 +61,18 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV0 for Masternode &mut vec![], platform_version, )?; - if let Some((existing_resource_vote_choice, previous_vote_count)) = - maybe_existing_resource_vote_choice - { - if existing_resource_vote_choice - == resource_vote.resource_vote_choice() - { - // We are submitting a vote for something we already have - return Ok(ConsensusValidationResult::new_with_error( - ConsensusError::StateError( - StateError::MasternodeVoteAlreadyPresentError( - MasternodeVoteAlreadyPresentError::new( - self.pro_tx_hash(), - resource_vote.vote_poll().clone(), - ), - ), - ), - )); - } else if previous_vote_count - >= platform_version - .dpp - .validation - .voting - .votes_allowed_per_masternode - { - // We are submitting a vote for something we already have - return Ok(ConsensusValidationResult::new_with_error( - ConsensusError::StateError( - StateError::MasternodeVotedTooManyTimesError( - MasternodeVotedTooManyTimesError::new( - self.pro_tx_hash(), - previous_vote_count, - platform_version - .dpp - .validation - .voting - .votes_allowed_per_masternode, - ), - ), - ), - )); - } else { - previous_resource_vote_choice_to_remove = - Some((existing_resource_vote_choice, previous_vote_count)); + match previous_vote_to_replace( + self.pro_tx_hash(), + maybe_existing_resource_vote_choice, + &resource_vote.resource_vote_choice(), + || resource_vote.vote_poll().clone(), + platform_version, + ) { + Ok(previous_vote) => { + previous_resource_vote_choice_to_remove = previous_vote + } + Err(error) => { + return Ok(ConsensusValidationResult::new_with_error(error)) } } } @@ -123,20 +95,12 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV0 for Masternode } } - let Some(masternode) = platform - .state - .full_masternode_list() - .get(&ProTxHash::from_byte_array(self.pro_tx_hash().to_buffer())) - else { - return Ok(ConsensusValidationResult::new_with_error( - MasternodeNotFoundError::new(self.pro_tx_hash()).into(), - )); + let masternode = match voting_masternode(platform, self.pro_tx_hash()) { + Ok(masternode) => masternode, + Err(error) => return Ok(ConsensusValidationResult::new_with_error(error)), }; - let strength = match masternode.node_type { - MasternodeType::Regular => 1, - MasternodeType::Evo => 4, - }; + let strength = masternode_vote_strength(&masternode.node_type); Ok(ConsensusValidationResult::new_with_data( MasternodeVoteTransitionAction::transform_from_transition( @@ -151,3 +115,67 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV0 for Masternode )) } } + +/// What a masternode's earlier vote on a poll means for its new one, the same for both poll +/// kinds: the same answer again is refused, a change once the masternode has voted +/// `votes_allowed_per_masternode` times is refused, and any other change carries the earlier +/// answer for registration to remove. `None` for a first vote. +/// +/// Moved here unchanged from `transform_into_action_v0` (in place: the same checks, in the same +/// order, with the same errors at every protocol version that selects it), so version 1 applies +/// the rule to yes/no votes too. +pub(super) fn previous_vote_to_replace( + pro_tx_hash: Identifier, + existing_vote: Option<(Choice, PreviousVoteCount)>, + new_vote_choice: &Choice, + vote_poll: impl FnOnce() -> VotePoll, + platform_version: &PlatformVersion, +) -> Result, ConsensusError> { + let Some((existing_vote_choice, previous_vote_count)) = existing_vote else { + return Ok(None); + }; + if &existing_vote_choice == new_vote_choice { + // We are submitting a vote for something we already have + Err(ConsensusError::StateError( + StateError::MasternodeVoteAlreadyPresentError(MasternodeVoteAlreadyPresentError::new( + pro_tx_hash, + vote_poll(), + )), + )) + } else if previous_vote_count + >= platform_version + .dpp + .validation + .voting + .votes_allowed_per_masternode + { + // We are submitting a vote for something we already have + Err(ConsensusError::StateError( + StateError::MasternodeVotedTooManyTimesError(MasternodeVotedTooManyTimesError::new( + pro_tx_hash, + previous_vote_count, + platform_version + .dpp + .validation + .voting + .votes_allowed_per_masternode, + )), + )) + } else { + Ok(Some((existing_vote_choice, previous_vote_count))) + } +} + +/// The voting masternode in the block's masternode list, or the error refusing a vote from one +/// that is not in it. Moved here unchanged from `transform_into_action_v0` (in place, as +/// [`previous_vote_to_replace`]) for version 1. +pub(super) fn voting_masternode<'a, C>( + platform: &PlatformRef<'a, C>, + pro_tx_hash: Identifier, +) -> Result<&'a MasternodeListItem, ConsensusError> { + platform + .state + .full_masternode_list() + .get(&ProTxHash::from_byte_array(pro_tx_hash.to_buffer())) + .ok_or_else(|| MasternodeNotFoundError::new(pro_tx_hash).into()) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs index c7720069139..c0cc1488774 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs @@ -1,15 +1,13 @@ use crate::error::Error; use crate::execution::platform_events::voting::masternode_vote_strength; -use crate::execution::validation::state_transition::state_transitions::masternode_vote::transform_into_action::v0::MasternodeVoteStateTransitionTransformIntoActionValidationV0; +use crate::execution::validation::state_transition::state_transitions::masternode_vote::transform_into_action::v0::{ + previous_vote_to_replace, voting_masternode, + MasternodeVoteStateTransitionTransformIntoActionValidationV0, +}; use crate::platform_types::platform::PlatformRef; use dpp::consensus::state::state_error::StateError; -use dpp::consensus::state::voting::masternode_not_found_error::MasternodeNotFoundError; -use dpp::consensus::state::voting::masternode_vote_already_present_error::MasternodeVoteAlreadyPresentError; -use dpp::consensus::state::voting::masternode_voted_too_many_times::MasternodeVotedTooManyTimesError; use dpp::consensus::state::voting::vote_poll_not_found_error::VotePollNotFoundError; use dpp::consensus::ConsensusError; -use dpp::dashcore::hashes::Hash; -use dpp::dashcore::ProTxHash; use dpp::prelude::ConsensusValidationResult; use dpp::state_transition::masternode_vote_transition::accessors::MasternodeVoteTransitionAccessorsV0; use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; @@ -19,7 +17,6 @@ use drive::state_transition_action::identity::masternode_vote::v0::MasternodeVot use drive::state_transition_action::identity::masternode_vote::MasternodeVoteTransitionAction; use crate::execution::validation::state_transition::ValidationMode; -use crate::platform_types::platform_state::PlatformStateV0Methods; use dpp::version::PlatformVersion; use dpp::voting::vote_polls::VotePoll; use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; @@ -39,10 +36,11 @@ pub(in crate::execution::validation::state_transition::state_transitions::master } impl MasternodeVoteStateTransitionTransformIntoActionValidationV1 for MasternodeVoteTransition { - /// Version 1 (protocol version 15) also transforms a vote on a yes/no poll, with the rules - /// of a resource vote: the same answer cannot be given twice, and a masternode may vote on a - /// poll at most `votes_allowed_per_masternode` times, its first vote included. A resource - /// vote on a contested poll transforms through version 0, unchanged. + /// Version 1 (protocol version 15) also transforms a vote on a yes/no poll, with the rule + /// of a resource vote ([`previous_vote_to_replace`]): the same answer cannot be given twice, + /// and a masternode may vote on a poll at most `votes_allowed_per_masternode` times, its + /// first vote included. A resource vote on a contested poll transforms through version 0, + /// unchanged. fn transform_into_action_v1( &self, platform: &PlatformRef, @@ -84,51 +82,21 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV1 for Masternode &mut vec![], platform_version, )?; - if let Some((existing_vote_choice, previous_vote_count)) = maybe_existing_vote_choice { - if existing_vote_choice == yes_no_vote.vote_choice() { - return Ok(ConsensusValidationResult::new_with_error( - ConsensusError::StateError(StateError::MasternodeVoteAlreadyPresentError( - MasternodeVoteAlreadyPresentError::new( - self.pro_tx_hash(), - VotePoll::YesNoVotePoll(vote_poll.clone()), - ), - )), - )); - } else if previous_vote_count - >= platform_version - .dpp - .validation - .voting - .votes_allowed_per_masternode - { - return Ok(ConsensusValidationResult::new_with_error( - ConsensusError::StateError(StateError::MasternodeVotedTooManyTimesError( - MasternodeVotedTooManyTimesError::new( - self.pro_tx_hash(), - previous_vote_count, - platform_version - .dpp - .validation - .voting - .votes_allowed_per_masternode, - ), - )), - )); - } else { - previous_vote_choice_to_remove = - Some((existing_vote_choice, previous_vote_count)); - } + match previous_vote_to_replace( + self.pro_tx_hash(), + maybe_existing_vote_choice, + &yes_no_vote.vote_choice(), + || VotePoll::YesNoVotePoll(vote_poll.clone()), + platform_version, + ) { + Ok(previous_vote) => previous_vote_choice_to_remove = previous_vote, + Err(error) => return Ok(ConsensusValidationResult::new_with_error(error)), } } - let Some(masternode) = platform - .state - .full_masternode_list() - .get(&ProTxHash::from_byte_array(self.pro_tx_hash().to_buffer())) - else { - return Ok(ConsensusValidationResult::new_with_error( - MasternodeNotFoundError::new(self.pro_tx_hash()).into(), - )); + let masternode = match voting_masternode(platform, self.pro_tx_hash()) { + Ok(masternode) => masternode, + Err(error) => return Ok(ConsensusValidationResult::new_with_error(error)), }; // A yes/no vote needs nothing from the state to be resolved; the previous answer found diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs index 22702b3704d..05afb42114d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs @@ -2,10 +2,9 @@ use crate::execution::check_tx::CheckTxLevel; use crate::execution::validation::state_transition::state_transitions::tests::{ - process_test_state_transition, setup_masternode_voting_identity, + process_test_state_transition, processing_credits, setup_masternode_voting_identity, take_down_masternode_identities, }; -use crate::platform_types::platform::PlatformRef; use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult::{ @@ -14,16 +13,15 @@ use crate::platform_types::state_transitions_processing_result::StateTransitionE use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; +use crate::test::helpers::state_mutation_guard::check_tx_at_all_levels; use assert_matches::assert_matches; use dpp::block::block_info::BlockInfo; -use dpp::block::epoch::Epoch; use dpp::consensus::basic::BasicError; use dpp::consensus::state::state_error::StateError; use dpp::consensus::ConsensusError; use dpp::dashcore::hashes::Hash; use dpp::dashcore::ProTxHash; use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeType; -use dpp::fee::Credits; use dpp::identifier::Identifier; use dpp::identity::IdentityPublicKey; use dpp::platform_value::BinaryData; @@ -34,7 +32,9 @@ use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; use dpp::version::PlatformVersion; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; -use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStatus; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::{ + YesNoVotePollResult, YesNoVotePollStatus, +}; use dpp::voting::vote_polls::yes_no_vote_poll::{ VotingPowerRounding, YesNoMinimumVotingPower, YesNoVotePoll, }; @@ -44,8 +44,6 @@ use dpp::voting::votes::resource_vote::ResourceVote; use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; use dpp::voting::votes::yes_no_vote::YesNoVote; use dpp::voting::votes::Vote; -use drive::error::Error as DriveError; -use drive::grovedb::Error as GroveError; use drive::query::yes_no_vote_poll_state_query::YesNoVotePollStateDriveQuery; use drive::util::test_helpers::vote_poll_end_dates; use simple_signer::signer::SimpleSigner; @@ -164,26 +162,10 @@ fn check_tx_verdicts( platform: &TempPlatform, serialized_transition: &[u8], ) -> Vec<(CheckTxLevel, Result)> { - let platform_state = platform.state.load(); - let platform_version = platform_state - .current_platform_version() - .expect("expected the current platform version"); - let platform_ref = PlatformRef { - drive: &platform.drive, - state: &platform_state, - config: &platform.config, - core_rpc: &platform.core_rpc, - }; - [CheckTxLevel::FirstTimeCheck, CheckTxLevel::Recheck] + check_tx_at_all_levels(platform, serialized_transition) .into_iter() - .map(|level| { - let verdict = platform - .check_tx( - serialized_transition, - level, - &platform_ref, - platform_version, - ) + .map(|(level, result)| { + let verdict = result .map(|result| result.is_valid()) .map_err(|error| error.to_string()); (level, verdict) @@ -249,9 +231,16 @@ async fn perform_vote_transition( | StateError::PrefundedSpecializedBalanceInsufficientError(_) )) ); + // CheckTx fails with an error only where the block reports an internal error too + // (a transition protocol version 13 cannot decode); any other refusal must be a + // consensus refusal. let (level, verdict) = &check_tx_verdicts[0]; + let refused = match verdict { + Ok(admitted) => !admitted, + Err(_) => matches!(other, InternalError(_)), + }; assert!( - refused_for_its_fund || matches!(verdict, Ok(false) | Err(_)), + refused_for_its_fund || refused, "check_tx {level:?} must refuse a yes/no vote a block refuses ({other:?}), got \ {verdict:?}" ); @@ -357,23 +346,31 @@ fn poll_state( .expect("expected the poll state") } -/// The processing credits epoch 0 will distribute; the item is written by the first credit, so -/// before any it is missing, which is none. -fn processing_credits( +/// The recorded result of a poll that ended. +fn finished_result( platform: &TempPlatform, + vote_poll: &YesNoVotePoll, platform_version: &PlatformVersion, -) -> Credits { - match platform +) -> YesNoVotePollResult { + *poll_state(platform, vote_poll, platform_version) + .stored_info + .expect("stored info") + .result() + .expect("expected the poll to be finished") +} + +/// A masternode's answer to a poll and how many times it voted on it, from the identity votes +/// index. +fn identity_vote( + platform: &TempPlatform, + voter: Identifier, + vote_poll_id: Identifier, + platform_version: &PlatformVersion, +) -> Option<(YesNoAbstainVoteChoice, u16)> { + platform .drive - .get_epoch_processing_credits_for_distribution( - &Epoch::new(0).expect("epoch"), - None, - platform_version, - ) { - Ok(credits) => credits, - Err(DriveError::GroveDB(error)) if matches!(*error, GroveError::PathKeyNotFound(_)) => 0, - Err(error) => panic!("expected the epoch's processing credits: {error:?}"), - } + .fetch_identity_yes_no_vote(voter, vote_poll_id, None, &mut vec![], platform_version) + .expect("vote") } fn new_platform() -> TempPlatform { @@ -465,11 +462,7 @@ async fn should_pass_at_exactly_the_threshold_and_fail_one_power_below_it_with_a close_ended_polls(&mut platform, platform_version); - let passing_result = *poll_state(&platform, &passing, platform_version) - .stored_info - .expect("stored info") - .result() - .expect("expected the poll to be finished"); + let passing_result = finished_result(&platform, &passing, platform_version); assert!(passing_result.passed); assert_eq!(passing_result.yes_voting_power, 5); assert_eq!(passing_result.no_voting_power, 1); @@ -479,11 +472,7 @@ async fn should_pass_at_exactly_the_threshold_and_fail_one_power_below_it_with_a END_DATE + 300_000 ); - let failing_result = *poll_state(&platform, &failing, platform_version) - .stored_info - .expect("stored info") - .result() - .expect("expected the poll to be finished"); + let failing_result = finished_result(&platform, &failing, platform_version); assert!(!failing_result.passed); assert_eq!(failing_result.yes_voting_power, 4); assert_eq!(failing_result.no_voting_power, 2); @@ -518,11 +507,7 @@ async fn should_fail_below_the_minimum_voting_power_even_when_every_vote_is_yes( close_ended_polls(&mut platform, platform_version); - let result = *poll_state(&platform, &vote_poll, platform_version) - .stored_info - .expect("stored info") - .result() - .expect("expected the poll to be finished"); + let result = finished_result(&platform, &vote_poll, platform_version); assert!(!result.passed); assert_eq!(result.yes_voting_power, 5); assert_eq!(result.no_voting_power, 0); @@ -625,18 +610,11 @@ async fn should_judge_a_share_minimum_against_the_masternode_list_at_close() { platform_version, ); - let result_of = |vote_poll: &YesNoVotePoll| { - *poll_state(&platform, vote_poll, platform_version) - .stored_info - .expect("stored info") - .result() - .expect("expected the poll to be finished") - }; - let alone = result_of(&evonode_alone); + let alone = finished_result(&platform, &evonode_alone, platform_version); assert_eq!(alone.yes_voting_power, 4); assert_eq!(alone.required_voting_power, 7); assert!(!alone.passed); - let with_three = result_of(&evonode_and_three); + let with_three = finished_result(&platform, &evonode_and_three, platform_version); assert_eq!(with_three.yes_voting_power, 7); assert_eq!(with_three.required_voting_power, 7); assert!(with_three.passed); @@ -678,11 +656,7 @@ async fn should_count_an_evonode_vote_as_four() { close_ended_polls(&mut platform, platform_version); // Five cast, minimum five: four is strictly more than two thirds of five. - let result = *poll_state(&platform, &vote_poll, platform_version) - .stored_info - .expect("stored info") - .result() - .expect("expected the poll to be finished"); + let result = finished_result(&platform, &vote_poll, platform_version); assert!(result.passed); assert_eq!(result.yes_voting_power, 4); } @@ -722,20 +696,14 @@ async fn should_leave_only_the_record_behind_after_the_poll_ends() { .drive .fetch_identities_voting_in_yes_no_vote_poll(&vote_poll, None, platform_version) .expect("voters"); - assert_eq!(voters[&YesNoAbstainVoteChoice::Yes].len(), 3); - assert_eq!(voters[&YesNoAbstainVoteChoice::No].len(), 1); - assert!(voters[&YesNoAbstainVoteChoice::Abstain].is_empty()); + let mut sorted_voters = voters.clone(); + sorted_voters.sort(); + let mut expected_voters: Vec = + yes_voters.iter().chain(no_voters.iter()).copied().collect(); + expected_voters.sort(); + assert_eq!(sorted_voters, expected_voters); assert_eq!( - platform - .drive - .fetch_identity_yes_no_vote( - yes_voters[0], - vote_poll_id, - None, - &mut vec![], - platform_version - ) - .expect("vote"), + identity_vote(&platform, yes_voters[0], vote_poll_id, platform_version), Some((YesNoAbstainVoteChoice::Yes, 1)) ); let balance = platform @@ -751,11 +719,7 @@ async fn should_leave_only_the_record_behind_after_the_poll_ends() { // After the end: the record with the result, and nothing else. let state = poll_state(&platform, &vote_poll, platform_version); - let result = *state - .stored_info - .expect("stored info") - .result() - .expect("expected the poll to be finished"); + let result = finished_result(&platform, &vote_poll, platform_version); assert!(result.passed); assert_eq!((result.yes_voting_power, result.no_voting_power), (3, 1)); assert_eq!( @@ -770,19 +734,10 @@ async fn should_leave_only_the_record_behind_after_the_poll_ends() { .drive .fetch_identities_voting_in_yes_no_vote_poll(&vote_poll, None, platform_version) .expect("voters"); - assert!(voters.values().all(|voters| voters.is_empty())); + assert!(voters.is_empty()); for voter in yes_voters.iter().chain(no_voters.iter()) { assert_eq!( - platform - .drive - .fetch_identity_yes_no_vote( - *voter, - vote_poll_id, - None, - &mut vec![], - platform_version - ) - .expect("vote"), + identity_vote(&platform, *voter, vote_poll_id, platform_version), None ); } @@ -1003,16 +958,7 @@ async fn should_move_a_changed_vote_and_cap_the_changes() { assert_eq!(state.voting_power(choice), expected, "vote {}", vote_number); } assert_eq!( - platform - .drive - .fetch_identity_yes_no_vote( - pro_tx_hash, - vote_poll_id, - None, - &mut vec![], - platform_version - ) - .expect("vote"), + identity_vote(&platform, pro_tx_hash, vote_poll_id, platform_version), Some((*vote_choice, vote_number as u16 + 1)) ); } @@ -1067,16 +1013,12 @@ async fn should_refuse_a_vote_when_the_poll_has_no_fund() { )) ); assert_eq!( - platform - .drive - .fetch_identity_yes_no_vote( - pro_tx_hash, - vote_poll.unique_id().expect("id"), - None, - &mut vec![], - platform_version - ) - .expect("vote"), + identity_vote( + &platform, + pro_tx_hash, + vote_poll.unique_id().expect("id"), + platform_version + ), None ); let state = poll_state(&platform, &vote_poll, platform_version); @@ -1086,11 +1028,7 @@ async fn should_refuse_a_vote_when_the_poll_has_no_fund() { // on, rather than failing the block. let processing_credits_before = processing_credits(&platform, platform_version); close_ended_polls(&mut platform, platform_version); - let result = *poll_state(&platform, &vote_poll, platform_version) - .stored_info - .expect("stored info") - .result() - .expect("expected the poll to be finished"); + let result = finished_result(&platform, &vote_poll, platform_version); assert!(!result.passed); assert_eq!( processing_credits(&platform, platform_version), @@ -1152,44 +1090,22 @@ async fn should_remove_the_votes_of_masternodes_that_left_the_list() { .drive .fetch_identities_voting_in_yes_no_vote_poll(&vote_poll, None, platform_version) .expect("voters"); - assert_eq!(voters[&YesNoAbstainVoteChoice::Yes], vec![yes_voters[2]]); - assert_eq!(voters[&YesNoAbstainVoteChoice::No], vec![no_voters[1]]); + // The yes voters, then the no voters. + assert_eq!(voters, vec![yes_voters[2], no_voters[1]]); for voter in &removed { assert_eq!( - platform - .drive - .fetch_identity_yes_no_vote( - *voter, - vote_poll_id, - None, - &mut vec![], - platform_version - ) - .expect("vote"), + identity_vote(&platform, *voter, vote_poll_id, platform_version), None ); } assert_eq!( - platform - .drive - .fetch_identity_yes_no_vote( - yes_voters[2], - vote_poll_id, - None, - &mut vec![], - platform_version - ) - .expect("vote"), + identity_vote(&platform, yes_voters[2], vote_poll_id, platform_version), Some((YesNoAbstainVoteChoice::Yes, 1)) ); // The poll still closes cleanly with what is left. close_ended_polls(&mut platform, platform_version); - let result = *poll_state(&platform, &vote_poll, platform_version) - .stored_info - .expect("stored info") - .result() - .expect("expected the poll to be finished"); + let result = finished_result(&platform, &vote_poll, platform_version); assert_eq!((result.yes_voting_power, result.no_voting_power), (1, 1)); assert!(!result.passed); } @@ -1244,14 +1160,7 @@ async fn should_close_a_contested_poll_and_a_yes_no_poll_that_end_at_the_same_ti let end_dates = vote_poll_end_dates(&platform.drive, platform_version); assert!(end_dates.is_empty()); - assert!( - poll_state(&platform, &vote_poll, platform_version) - .stored_info - .expect("stored info") - .result() - .expect("finished") - .passed - ); + assert!(finished_result(&platform, &vote_poll, platform_version).passed); // The contest closed in the same block: it has a result, and its clean-up drained its // fund. @@ -1340,9 +1249,79 @@ async fn should_keep_an_end_date_the_block_only_partly_closed() { assert!(end_dates(&platform).is_empty()); } +/// Two openings of one poll that cannot see each other (each built with no previous +/// operations, as drive operations are converted one at a time) list it under two end dates. +/// The poll closes at the first; when the second comes due, only its entry goes, rather than +/// the block failing on a poll that is already finished. +#[tokio::test] +async fn should_only_remove_the_later_end_date_entry_of_a_poll_listed_twice() { + let platform_version = PlatformVersion::latest(); + let mut platform = new_platform(); + let vote_poll = two_thirds_poll(b"listed twice", 1); + let later_end_date = END_DATE + 1_000_000; + let mut operations = vec![]; + for end_date in [END_DATE, later_end_date] { + let mut opening_operations = vec![]; + platform + .drive + .open_yes_no_vote_poll_operations( + &vote_poll, + end_date, + &BlockInfo::default(), + &mut None, + &mut opening_operations, + None, + platform_version, + ) + .expect("expected to open the poll"); + operations.append(&mut opening_operations); + } + platform + .drive + .apply_batch_low_level_drive_operations( + None, + None, + operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected the openings to apply"); + platform + .drive + .add_prefunded_specialized_balance( + vote_poll.specialized_balance_id().expect("id"), + FUND, + None, + platform_version, + ) + .expect("expected to fund the poll"); + assert_eq!( + vote_poll_end_dates(&platform.drive, platform_version).len(), + 2 + ); + + close_ended_polls(&mut platform, platform_version); + let result = finished_result(&platform, &vote_poll, platform_version); + assert!(!result.passed); + assert_eq!( + vote_poll_end_dates(&platform.drive, platform_version) + .keys() + .copied() + .collect::>(), + vec![later_end_date] + ); + + close_ended_polls_at(&mut platform, later_end_date + 300_000, platform_version); + assert!(vote_poll_end_dates(&platform.drive, platform_version).is_empty()); + assert_eq!( + finished_result(&platform, &vote_poll, platform_version), + result + ); +} + /// Protocol version 13 selects the shipped (v0) masternode vote transform and state -/// validation, vote registration, end of poll check and clean-up, which this change gave -/// yes/no arms. A contest there runs as it did before: the vote counts, the poll ends, the +/// validation and vote registration, which this change gave yes/no arms. A contest there runs +/// as it did before: the vote counts, the poll ends, the /// contender is awarded and the end date index empties. A masternode vote naming a yes/no /// poll, of either kind, does not decode there, so it is stripped before any of them runs and /// consumes nothing. @@ -1352,8 +1331,8 @@ async fn should_run_a_contest_unchanged_and_strip_yes_no_polls_at_protocol_versi } /// Protocol version 14 selects the masternode vote transform v0 and state validation v1, vote -/// registration v0, end of poll check v1 and clean-up v0, which this change gave yes/no arms -/// that only 15 reaches. A contest there runs as it did before, and a masternode vote naming a +/// registration v0 and end of poll check v1, which this change gave yes/no arms that only 15 +/// reaches. A contest there runs as it did before, and a masternode vote naming a /// yes/no poll is stripped as at 13. #[tokio::test] async fn should_run_a_contest_unchanged_and_strip_yes_no_polls_at_protocol_version_14() { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs index 4574d230cbf..49f98a81cc8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs @@ -899,6 +899,29 @@ pub(in crate::execution) mod tests { platform.state.store(Arc::new(platform_state)); } + /// The processing credits epoch 0 will distribute; the item is written by the first credit, + /// so before any it is missing, which is none. + pub(in crate::execution) fn processing_credits( + platform: &TempPlatform, + platform_version: &PlatformVersion, + ) -> dpp::fee::Credits { + match platform + .drive + .get_epoch_processing_credits_for_distribution( + &dpp::block::epoch::Epoch::new(0).expect("epoch"), + None, + platform_version, + ) { + Ok(credits) => credits, + Err(drive::error::Error::GroveDB(error)) + if matches!(*error, drive::grovedb::Error::PathKeyNotFound(_)) => + { + 0 + } + Err(error) => panic!("expected the epoch's processing credits: {error:?}"), + } + } + #[allow(dead_code)] pub(in crate::execution) enum IdentityTestInfo<'a> { Given { diff --git a/packages/rs-drive-abci/src/test/helpers/state_mutation_guard.rs b/packages/rs-drive-abci/src/test/helpers/state_mutation_guard.rs index a588390ef11..c017ffed93a 100644 --- a/packages/rs-drive-abci/src/test/helpers/state_mutation_guard.rs +++ b/packages/rs-drive-abci/src/test/helpers/state_mutation_guard.rs @@ -15,11 +15,14 @@ //! during CheckTx fee estimation). #3823 made the converter pure; the helpers here guard the //! CLASS by asserting the committed root hash is byte-identical around CheckTx-path entry points. -use crate::execution::check_tx::CheckTxLevel; +use crate::error::Error; +use crate::execution::check_tx::{CheckTxLevel, CheckTxResult}; use crate::platform_types::platform::PlatformRef; use crate::platform_types::platform_state::PlatformStateV0Methods; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::TempPlatform; +use dpp::consensus::ConsensusError; +use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; use drive::drive::Drive; @@ -56,22 +59,15 @@ pub fn assert_committed_root_hash_unchanged( result } -/// Runs `check_tx` at BOTH levels (`FirstTimeCheck`, then `Recheck`) on a serialized state -/// transition and asserts each returns a VALID result. -/// -/// Call this from every state-transition type's canonical valid-fixture test, BEFORE the fixture -/// is processed: it proves the type's full CheckTx pipeline (decode -> validation -> fee -/// estimation) runs against committed state without errors, and — because `Platform::check_tx` -/// itself asserts committed-root-hash invariance under `cfg(test)` — that nothing on the type's -/// CheckTx path mutates committed GroveDB state (the devnet paloma height-788 halt class). -/// -/// The validity assertions matter for the guard's strength: an early consensus rejection would -/// skip fee estimation, leaving the type's drive-op converters unexercised in estimation mode. -pub fn assert_check_tx_valid_at_all_levels( +/// Runs `check_tx` at both levels (`FirstTimeCheck`, then `Recheck`) on a serialized state +/// transition against committed state, and returns each level's result. +pub fn check_tx_at_all_levels( platform: &TempPlatform, serialized_transition: &[u8], - context: &str, -) { +) -> Vec<( + CheckTxLevel, + Result, Error>, +)> { let platform_state = platform.state.load(); let platform_version = platform_state .current_platform_version() @@ -83,17 +79,40 @@ pub fn assert_check_tx_valid_at_all_levels( core_rpc: &platform.core_rpc, }; - for level in [CheckTxLevel::FirstTimeCheck, CheckTxLevel::Recheck] { - let result = platform - .check_tx( + [CheckTxLevel::FirstTimeCheck, CheckTxLevel::Recheck] + .into_iter() + .map(|level| { + let result = platform.check_tx( serialized_transition, level, &platform_ref, platform_version, - ) - .unwrap_or_else(|error| { - panic!("{context}: check_tx {level:?} must not error, got {error}") - }); + ); + (level, result) + }) + .collect() +} + +/// Runs `check_tx` at BOTH levels (`FirstTimeCheck`, then `Recheck`) on a serialized state +/// transition and asserts each returns a VALID result. +/// +/// Call this from every state-transition type's canonical valid-fixture test, BEFORE the fixture +/// is processed: it proves the type's full CheckTx pipeline (decode -> validation -> fee +/// estimation) runs against committed state without errors, and — because `Platform::check_tx` +/// itself asserts committed-root-hash invariance under `cfg(test)` — that nothing on the type's +/// CheckTx path mutates committed GroveDB state (the devnet paloma height-788 halt class). +/// +/// The validity assertions matter for the guard's strength: an early consensus rejection would +/// skip fee estimation, leaving the type's drive-op converters unexercised in estimation mode. +pub fn assert_check_tx_valid_at_all_levels( + platform: &TempPlatform, + serialized_transition: &[u8], + context: &str, +) { + for (level, result) in check_tx_at_all_levels(platform, serialized_transition) { + let result = result.unwrap_or_else(|error| { + panic!("{context}: check_tx {level:?} must not error, got {error}") + }); assert!( result.is_valid(), "{context}: check_tx {level:?} must pass for the canonical valid fixture so fee \ diff --git a/packages/rs-drive/grovedb-structure.json b/packages/rs-drive/grovedb-structure.json index 7a559a383f7..48f8fbb8ce7 100644 --- a/packages/rs-drive/grovedb-structure.json +++ b/packages/rs-drive/grovedb-structure.json @@ -5165,7 +5165,7 @@ "len": 32 }, "encoding": "hash32", - "description": "The double sha256 of the serialized vote poll" + "description": "The double sha256 of the domain tag `dash platform yes/no vote poll id` followed by the serialized vote poll" }, "kinds": [ "Item" @@ -5208,7 +5208,7 @@ "len": 32 }, "encoding": "hash32", - "description": "The double sha256 of the serialized vote poll" + "description": "The double sha256 of the domain tag `dash platform yes/no vote poll id` followed by the serialized vote poll" }, "kinds": [ "Tree" @@ -5414,7 +5414,7 @@ "len": 32 }, "encoding": "hash32", - "description": "The double sha256 of the serialized vote poll" + "description": "The poll's unique id: for a contest the double sha256 of the serialized poll, for a yes/no poll that of the yes/no domain tag followed by the serialized vote poll" }, "kinds": [ "Item" diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/mod.rs index 2f0afe81d3d..f5a2ebe5127 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/mod.rs @@ -23,6 +23,9 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { + // In place: this dispatcher read `cleanup.remove_specific_vote_references_given_by_identity`, + // which, like its own slot, is 0 in every table up to protocol version 14, so v0 still + // runs there. match platform_version .drive .methods diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v0/mod.rs index c6d007d695a..660908d8ee3 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v0/mod.rs @@ -15,7 +15,8 @@ use dpp::dashcore::Network; use dpp::prelude::{BlockHeight, Identifier}; use dpp::version::PlatformVersion; use grovedb::query_result_type::QueryResultType::QueryPathKeyElementTrioResultType; -use grovedb::{MaybeTree, PathQuery, Query, SizedQuery, TransactionArg}; +use grovedb::{Element, MaybeTree, PathQuery, Query, SizedQuery, TransactionArg}; +use std::collections::BTreeMap; impl Drive { /// We remove votes for an identity when that identity is somehow disabled. Currently there is @@ -33,26 +34,12 @@ impl Drive { let vote_path = vote_contested_resource_identity_votes_tree_path_vec(); - let mut query = Query::new_with_direction(true); - - query.insert_keys(identity_ids_as_byte_arrays); - - let subquery = Query::new_single_query_item(QueryItem::RangeFull(RangeFull)); - - query.set_subquery(subquery); - - let path_query = PathQuery::new(vote_path.clone(), SizedQuery::new(query, None, None)); - - let votes_to_remove_by_identity_id = self - .grove_get_raw_path_query( - &path_query, - transaction, - QueryPathKeyElementTrioResultType, - &mut vec![], - &platform_version.drive, - )? - .0 - .to_last_path_to_key_elements_btree_map(); + let votes_to_remove_by_identity_id = self.identity_vote_entries_of_identities_v0( + vote_path, + identity_ids_as_byte_arrays, + transaction, + platform_version, + )?; // Then we take each votes and go looking for it (to remove it) @@ -144,6 +131,40 @@ impl Drive { Ok(()) } + + /// The entries of these identities in the identity votes index at `vote_path`, by identity + /// then vote poll id. The query of `remove_all_votes_given_by_identities_v0`, moved here + /// unchanged (in place: the same read at every protocol version that selects it) so v1 + /// reads the yes/no identity votes index, which differs only in its path, the same way. + #[allow(clippy::type_complexity)] + pub(super) fn identity_vote_entries_of_identities_v0( + &self, + vote_path: Vec>, + identity_ids_as_byte_arrays: Vec>, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, BTreeMap, Element>>, Error> { + let mut query = Query::new_with_direction(true); + + query.insert_keys(identity_ids_as_byte_arrays); + + let subquery = Query::new_single_query_item(QueryItem::RangeFull(RangeFull)); + + query.set_subquery(subquery); + + let path_query = PathQuery::new(vote_path, SizedQuery::new(query, None, None)); + + Ok(self + .grove_get_raw_path_query( + &path_query, + transaction, + QueryPathKeyElementTrioResultType, + &mut vec![], + &platform_version.drive, + )? + .0 + .to_last_path_to_key_elements_btree_map()) + } } #[cfg(test)] @@ -293,8 +314,9 @@ mod tests { #[test] fn mainnet_evo1_below_32329_commits_vote_deletion_outside_block_transaction() { - let drive = setup_drive_with_initial_state_structure(None); - let platform_version = PlatformVersion::latest(); + // v0 is the generation protocol version 14 and earlier select; 15 selects v1. + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); seed_vote(&drive, platform_version); let transaction = drive.grove.start_transaction(); @@ -319,8 +341,9 @@ mod tests { #[test] fn mainnet_evo1_at_32329_keeps_vote_deletion_inside_block_transaction() { - let drive = setup_drive_with_initial_state_structure(None); - let platform_version = PlatformVersion::latest(); + // v0 is the generation protocol version 14 and earlier select; 15 selects v1. + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); seed_vote(&drive, platform_version); let transaction = drive.grove.start_transaction(); diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs index 714517d9fef..693bc7479b4 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs @@ -1,23 +1,17 @@ use crate::drive::Drive; -use std::ops::RangeFull; -use crate::error::drive::DriveError; use crate::error::Error; use crate::drive::votes::paths::{ - vote_decisions_active_polls_poll_tree_path_vec, vote_decisions_identity_votes_tree_path_vec, + vote_decisions_identity_votes_tree_path_vec, vote_decisions_poll_choice_votes_tree_path_vec, vote_decisions_tree_path, IDENTITY_VOTES_TREE_KEY, }; use crate::drive::votes::storage_form::yes_no_vote_reference_storage_form::YesNoVoteReferenceStorageForm; -use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; -use crate::query::QueryItem; use crate::util::grove_operations::{BatchDeleteApplyType, DirectQueryType}; use dpp::dashcore::Network; use dpp::prelude::{BlockHeight, Identifier}; use dpp::version::PlatformVersion; -use grovedb::query_result_type::QueryResultType::QueryPathKeyElementTrioResultType; -use grovedb::{Element, MaybeTree, PathQuery, Query, SizedQuery, TransactionArg}; -use std::collections::BTreeMap; +use grovedb::{MaybeTree, TransactionArg}; impl Drive { /// Version 1 (protocol version 15) removes the contested resource votes of the removed @@ -59,7 +53,8 @@ impl Drive { } let mut deletion_batch = vec![]; - for (identifier_bytes, votes_to_remove) in self.yes_no_votes_of_identities( + for (identifier_bytes, votes_to_remove) in self.identity_vote_entries_of_identities_v0( + vote_decisions_identity_votes_tree_path_vec(), identity_ids_as_byte_arrays, transaction, platform_version, @@ -67,19 +62,14 @@ impl Drive { let identity_id = Identifier::from_vec(identifier_bytes.clone())?; let mut vote_poll_ids = Vec::with_capacity(votes_to_remove.len()); for (vote_poll_id, vote_to_remove) in votes_to_remove { - let serialized_form = vote_to_remove.into_item_bytes()?; - let storage_form = YesNoVoteReferenceStorageForm::deserialize(&serialized_form) - .map_err(|e| { - Error::Drive(DriveError::CorruptedSerialization(format!( - "serialization of yes/no vote reference {} is corrupted: {}", - hex::encode(serialized_form), - e - ))) - })?; + let storage_form = YesNoVoteReferenceStorageForm::deserialize_stored( + &vote_to_remove.into_item_bytes()?, + )?; let vote_poll_id = Identifier::from_vec(vote_poll_id)?; - let mut votes_path = - vote_decisions_active_polls_poll_tree_path_vec(vote_poll_id.as_bytes()); - votes_path.push(vec![storage_form.vote_choice.to_tree_key()]); + let votes_path = vote_decisions_poll_choice_votes_tree_path_vec( + vote_poll_id.as_bytes(), + storage_form.vote_choice, + ); self.batch_delete( votes_path.as_slice().into(), identifier_bytes.as_slice(), @@ -115,31 +105,4 @@ impl Drive { Ok(()) } - - /// The yes/no vote index entries of these masternodes, by masternode then poll id. - #[allow(clippy::type_complexity)] - fn yes_no_votes_of_identities( - &self, - identity_ids_as_byte_arrays: Vec>, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result, BTreeMap, Element>>, Error> { - let decisions_vote_path = vote_decisions_identity_votes_tree_path_vec(); - let mut query = Query::new_with_direction(true); - query.insert_keys(identity_ids_as_byte_arrays); - query.set_subquery(Query::new_single_query_item(QueryItem::RangeFull( - RangeFull, - ))); - let path_query = PathQuery::new(decisions_vote_path, SizedQuery::new(query, None, None)); - Ok(self - .grove_get_raw_path_query( - &path_query, - transaction, - QueryPathKeyElementTrioResultType, - &mut vec![], - &platform_version.drive, - )? - .0 - .to_last_path_to_key_elements_btree_map()) - } } diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_specific_votes_given_by_identity/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_specific_votes_given_by_identity/v0/mod.rs index 1802e9ab242..fc479ff1fbe 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_specific_votes_given_by_identity/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_specific_votes_given_by_identity/v0/mod.rs @@ -24,6 +24,27 @@ impl Drive { let vote_path_ref = vote_contested_resource_identity_votes_tree_path_for_identity(identity_id.as_bytes()); + self.remove_identity_vote_references_operations_v0( + &vote_path_ref, + votes, + batch_operations, + transaction, + platform_version, + ) + } + + /// The loop of `remove_specific_vote_references_given_by_identity_v0`, moved here unchanged + /// (in place: the same deletes in the same order at every protocol version that selects + /// it) so the yes/no identity votes index, which differs only in its path, removes its + /// entries the same way. + pub(in crate::drive::votes::cleanup) fn remove_identity_vote_references_operations_v0( + &self, + vote_path_ref: &[&[u8]; 4], + votes: &[&Identifier], + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { for vote_identifier_to_remove in votes { self.batch_delete( vote_path_ref.as_slice().into(), diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs index 5a5ebe0a411..a30bea1d24b 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs @@ -2,10 +2,9 @@ use crate::drive::votes::paths::vote_decisions_identity_votes_tree_path_for_iden use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use crate::util::grove_operations::BatchDeleteApplyType; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; -use grovedb::{MaybeTree, TransactionArg}; +use grovedb::TransactionArg; impl Drive { pub(super) fn remove_yes_no_vote_references_given_by_identity_operations_v0( @@ -18,22 +17,12 @@ impl Drive { ) -> Result<(), Error> { let path = vote_decisions_identity_votes_tree_path_for_identity(masternode_pro_tx_hash.as_bytes()); - for vote_poll_id in vote_poll_ids { - // An item delete never reads the pending batch, so it is built against an empty - // one instead of copying everything a poll's clean-up has queued so far. - let mut delete_operations = vec![]; - self.batch_delete( - path.as_slice().into(), - vote_poll_id.as_slice(), - BatchDeleteApplyType::StatefulBatchDelete { - is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), - }, - transaction, - &mut delete_operations, - &platform_version.drive, - )?; - batch_operations.append(&mut delete_operations); - } - Ok(()) + self.remove_identity_vote_references_operations_v0( + &path, + vote_poll_ids, + batch_operations, + transaction, + platform_version, + ) } } diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs index 4cf401ad926..c3b78f34da8 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs @@ -5,20 +5,18 @@ use crate::error::drive::DriveError; use crate::error::Error; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; -use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use grovedb::TransactionArg; -use std::collections::BTreeMap; impl Drive { - /// The masternodes that voted on a yes/no poll, by their answer. Every choice is present, - /// with an empty list when nobody chose it. + /// The masternodes that voted on a yes/no poll: the yes voters, then the no voters, then + /// the abstaining ones. pub fn fetch_identities_voting_in_yes_no_vote_poll( &self, vote_poll: &YesNoVotePoll, transaction: TransactionArg, platform_version: &PlatformVersion, - ) -> Result>, Error> { + ) -> Result, Error> { match platform_version .drive .methods diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs index 788e6e9a976..bc3da1232e7 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs @@ -1,7 +1,6 @@ use crate::drive::votes::paths::YesNoVotePollPaths; use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; use crate::drive::Drive; -use crate::error::drive::DriveError; use crate::error::Error; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; @@ -9,7 +8,6 @@ use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoic use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use grovedb::query_result_type::QueryResultType; use grovedb::{PathQuery, Query, QueryItem, SizedQuery, TransactionArg}; -use std::collections::BTreeMap; use std::ops::RangeFull; impl Drive { @@ -18,7 +16,7 @@ impl Drive { vote_poll: &YesNoVotePoll, transaction: TransactionArg, platform_version: &PlatformVersion, - ) -> Result>, Error> { + ) -> Result, Error> { let path = vote_poll.poll_path_vec()?; let mut query = Query::new_with_direction(true); query.insert_keys( @@ -38,7 +36,8 @@ impl Drive { offset: None, }, }; - let voters_by_choice_key = self + // The subquery's keys are the voters' pro tx hashes, under the three choice trees. + let voters = self .grove_get_path_query( &path_query, transaction, @@ -47,29 +46,10 @@ impl Drive { &platform_version.drive, )? .0 - .to_last_path_to_key_elements_btree_map(); - let mut voters_by_choice: BTreeMap> = - YesNoAbstainVoteChoice::ALL - .iter() - .map(|vote_choice| (*vote_choice, vec![])) - .collect(); - for (choice_key, voters) in voters_by_choice_key { - let vote_choice = choice_key - .first() - .filter(|_| choice_key.len() == 1) - .and_then(|key| YesNoAbstainVoteChoice::from_tree_key(*key)) - .ok_or_else(|| { - Error::Drive(DriveError::CorruptedDriveState(format!( - "unexpected key {} in yes/no vote poll tree", - hex::encode(&choice_key) - ))) - })?; - let voters = voters - .into_keys() - .map(|voter| voter.try_into()) - .collect::, dpp::platform_value::Error>>()?; - voters_by_choice.insert(vote_choice, voters); - } - Ok(voters_by_choice) + .to_keys() + .into_iter() + .map(|voter| voter.try_into()) + .collect::, dpp::platform_value::Error>>()?; + Ok(voters) } } diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs index 3c4324c266f..d09a1d4e7aa 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs @@ -1,6 +1,6 @@ mod v0; -use crate::drive::votes::resolved::votes::resolved_yes_no_vote::PreviousYesNoVoteCount; +use crate::drive::votes::PreviousVoteCount; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -20,7 +20,7 @@ impl Drive { transaction: TransactionArg, drive_operations: &mut Vec, platform_version: &PlatformVersion, - ) -> Result, Error> { + ) -> Result, Error> { match platform_version .drive .methods diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs index 7c9252f85cc..7e3ea7ce72b 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs @@ -1,8 +1,7 @@ use crate::drive::votes::paths::vote_decisions_identity_votes_tree_path_for_identity; -use crate::drive::votes::resolved::votes::resolved_yes_no_vote::PreviousYesNoVoteCount; use crate::drive::votes::storage_form::yes_no_vote_reference_storage_form::YesNoVoteReferenceStorageForm; +use crate::drive::votes::PreviousVoteCount; use crate::drive::Drive; -use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::util::grove_operations::DirectQueryType; @@ -19,7 +18,7 @@ impl Drive { transaction: TransactionArg, drive_operations: &mut Vec, platform_version: &PlatformVersion, - ) -> Result, Error> { + ) -> Result, Error> { let path = vote_decisions_identity_votes_tree_path_for_identity(masternode_pro_tx_hash.as_bytes()); let maybe_element = self.grove_get_raw_optional( @@ -33,14 +32,7 @@ impl Drive { maybe_element .map(|element| { let bytes = element.into_item_bytes()?; - let storage_form = - YesNoVoteReferenceStorageForm::deserialize(&bytes).map_err(|e| { - Error::Drive(DriveError::CorruptedSerialization(format!( - "serialization of yes/no vote reference {} is corrupted: {}", - hex::encode(bytes), - e - ))) - })?; + let storage_form = YesNoVoteReferenceStorageForm::deserialize_stored(&bytes)?; Ok((storage_form.vote_choice, storage_form.identity_vote_times)) }) .transpose() diff --git a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/mod.rs b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/mod.rs index 224de76a74b..12c897dcc49 100644 --- a/packages/rs-drive/src/drive/votes/insert/register_identity_vote/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/mod.rs @@ -51,6 +51,9 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result { + // In place: this dispatcher read `contested_resource_insert.register_identity_vote`, + // which, like `insert.register_identity_vote`, is 0 in every table up to protocol + // version 14, so v0 still runs there. match platform_version .drive .methods @@ -118,6 +121,7 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { + // In place, as in `register_identity_vote`: both slots are 0 up to protocol version 14. match platform_version .drive .methods diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs index d0ab65a61a0..5b61b2c4777 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs @@ -95,19 +95,11 @@ mod tests { }; use crate::util::grove_operations::DirectQueryType; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use crate::util::test_helpers::test_yes_no_vote_poll as poll; + use crate::util::test_helpers::vote_poll_end_dates; use dpp::block::block_info::BlockInfo; use dpp::platform_value::BinaryData; use dpp::version::PlatformVersion; - use dpp::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; - - fn poll() -> YesNoVotePoll { - YesNoVotePoll { - resource_path: vec![BinaryData::new(vec![0xc1; 32])], - supermajority_numerator: 2, - supermajority_denominator: 3, - minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), - } - } #[test] fn should_refuse_to_open_a_poll_a_batch_already_opens() { @@ -179,4 +171,86 @@ mod tests { .expect("expected to fetch the stored info") .is_some()); } + + #[test] + fn should_refuse_to_open_a_poll_before_protocol_version_15() { + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + drive + .open_yes_no_vote_poll( + &poll(), + 1_000, + &BlockInfo::default(), + None, + platform_version, + ) + .expect_err("expected protocol version 14 to refuse opening a yes/no poll"); + } + + #[test] + fn should_refuse_a_poll_that_ends_by_the_block_that_opens_it() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let block_info = BlockInfo { + time_ms: 1_000, + ..Default::default() + }; + for end_date in [999, 1_000] { + drive + .open_yes_no_vote_poll(&poll(), end_date, &block_info, None, platform_version) + .expect_err("expected a poll ending by the opening block to be refused"); + } + drive + .open_yes_no_vote_poll(&poll(), 1_001, &block_info, None, platform_version) + .expect("expected a poll ending after the opening block to open"); + } + + #[test] + fn should_queue_the_trees_two_openings_in_a_batch_share_once() { + // A chain born at 14 lacks the decisions trees, which both openings need, and both polls + // end at the same time. The test drive verifies batch consistency, which refuses a tree + // queued twice. + let drive = setup_drive_with_initial_state_structure(Some( + PlatformVersion::get(14).expect("expected protocol version 14"), + )); + let platform_version = PlatformVersion::latest(); + let mut other_poll = poll(); + other_poll + .resource_path + .push(BinaryData::new(b"other".to_vec())); + let mut batch_operations = vec![]; + for vote_poll in [poll(), other_poll.clone()] { + drive + .open_yes_no_vote_poll_operations( + &vote_poll, + 1_000, + &BlockInfo::default(), + &mut None, + &mut batch_operations, + None, + platform_version, + ) + .expect("expected to open the poll"); + } + drive + .apply_batch_low_level_drive_operations( + None, + None, + batch_operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected the batch to apply"); + let end_dates = vote_poll_end_dates(&drive, platform_version); + assert_eq!(end_dates.len(), 1); + assert_eq!( + end_dates[&1_000], + [ + poll().unique_id().expect("id"), + other_poll.unique_id().expect("id") + ] + .into_iter() + .collect() + ); + } } diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs index be9c347cbd0..e0ca5d167b2 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs @@ -1,12 +1,14 @@ use crate::drive::votes::paths::{ - vote_decisions_active_polls_tree_path_vec, vote_decisions_tree_path_vec, YesNoVotePollPaths, - ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY, + vote_decisions_active_polls_tree_path_vec, vote_decisions_tree_path_vec, + vote_end_date_queries_tree_path_vec, YesNoVotePollPaths, VOTE_DECISIONS_SUBTREE_KEYS, }; use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; use crate::drive::Drive; +use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::fees::op::LowLevelDriveOperation::{EphemeralGroveOperation, GroveOperation}; +use crate::util::common::encode::encode_u64; use crate::util::grove_operations::BatchInsertTreeApplyType; use crate::util::object_size_info::{DriveKeyInfo, PathKeyInfo}; use dpp::block::block_info::BlockInfo; @@ -60,43 +62,56 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { + // Yes/no polls exist from protocol version 15. The end-of-block generations that + // protocol version 14 also selects (`check_for_ended_vote_polls` v1) take their yes/no + // handling in place on the ground that no yes/no poll is in the end date index before + // 15, and this refusal is what keeps that true. + if platform_version.protocol_version < 15 { + return Err(Error::Drive(DriveError::NotSupported( + "yes/no vote polls can only be opened from protocol version 15", + ))); + } vote_poll.validate_parameters(platform_version)?; + // A poll that ends in the block that opens it, or earlier, would close at the end of + // this block with no vote cast. + if end_date <= block_info.time_ms { + return Err(Error::Protocol(Box::new(ProtocolError::VoteError( + format!( + "yes/no vote poll {} must end after the block that opens it (at {}), not at {}", + vote_poll, block_info.time_ms, end_date + ), + )))); + } let poll_id = vote_poll.unique_id()?; // The decisions trees, when the chain lacks them: genesis and the upgrade to protocol - // version 15 create them, but a chain that reached 15 before they existed did not. The - // same keys in the same order as `insert_vote_decisions_trees`. - for subtree_key in [ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY] { - self.batch_insert_empty_tree_if_not_exists( - PathKeyInfo::PathKey::<0>(( - vote_decisions_tree_path_vec(), - vec![subtree_key as u8], - )), - TreeType::NormalTree, - None, - BatchInsertTreeApplyType::StatefulBatchInsertTree, - transaction, - previous_batch_operations, - batch_operations, - &platform_version.drive, - )?; + // version 15 create them, but a chain that reached 15 before they existed did not. An + // earlier opening in this batch may already queue them. + for subtree_key in VOTE_DECISIONS_SUBTREE_KEYS { + let key = vec![subtree_key as u8]; + if !tree_queued_in(batch_operations, vote_decisions_tree_path_vec(), &key) { + self.batch_insert_empty_tree_if_not_exists( + PathKeyInfo::PathKey::<0>((vote_decisions_tree_path_vec(), key)), + TreeType::NormalTree, + None, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + transaction, + previous_batch_operations, + batch_operations, + &platform_version.drive, + )?; + } } // The poll's own tree. A poll that already has one was opened before, and so was one // whose tree an earlier opening queued in this batch: opened twice, it would be listed - // under two end dates and the second would close a finished poll. + // under two end dates. let active_polls_path = vote_decisions_active_polls_tree_path_vec(); - let queued_in_this_batch = { - let path = KeyInfoPath::from_known_owned_path(active_polls_path.clone()); - let key = KeyInfo::KnownKey(poll_id.to_vec()); - batch_operations.iter().any(|operation| { - matches!( - operation, - GroveOperation(queued) | EphemeralGroveOperation(queued, _) - if queued.path == path && queued.key.as_ref() == Some(&key) - ) - }) - }; + let queued_in_this_batch = tree_queued_in( + batch_operations, + active_polls_path.clone(), + poll_id.as_slice(), + ); let inserted = !queued_in_this_batch && self.batch_insert_empty_tree_if_not_exists( PathKeyInfo::PathKey::<0>((active_polls_path, poll_id.to_vec())), @@ -139,17 +154,59 @@ impl Drive { } // The end date index closes the poll when its time comes. Nobody is refunded for the - // entry: the poll is a system action, so it carries no storage flags. - self.add_vote_poll_end_date_query_operations( - None, - VotePoll::YesNoVotePoll(vote_poll.clone()), - end_date, - block_info, - &mut None, - previous_batch_operations, + // entry: the poll is a system action, so it carries no storage flags. When an earlier + // opening in this batch already queues the end date's tree, this batch serves as the + // operations to check, so the tree is queued once. + let end_date_tree_queued = tree_queued_in( batch_operations, - transaction, - platform_version, - ) + vote_end_date_queries_tree_path_vec(), + &encode_u64(end_date), + ); + if end_date_tree_queued { + let mut end_date_operations = vec![]; + self.add_vote_poll_end_date_query_operations( + None, + VotePoll::YesNoVotePoll(vote_poll.clone()), + end_date, + block_info, + &mut None, + &mut Some(batch_operations), + &mut end_date_operations, + transaction, + platform_version, + )?; + batch_operations.append(&mut end_date_operations); + Ok(()) + } else { + self.add_vote_poll_end_date_query_operations( + None, + VotePoll::YesNoVotePoll(vote_poll.clone()), + end_date, + block_info, + &mut None, + previous_batch_operations, + batch_operations, + transaction, + platform_version, + ) + } } } + +/// Whether `batch_operations` already queues a grove operation on `key` under `path`, which +/// `batch_insert_empty_tree_if_not_exists` only looks for in the previous operations. +fn tree_queued_in( + batch_operations: &[LowLevelDriveOperation], + path: Vec>, + key: &[u8], +) -> bool { + let path = KeyInfoPath::from_known_owned_path(path); + let key = KeyInfo::KnownKey(key.to_vec()); + batch_operations.iter().any(|operation| { + matches!( + operation, + GroveOperation(queued) | EphemeralGroveOperation(queued, _) + if queued.path == path && queued.key.as_ref() == Some(&key) + ) + }) +} diff --git a/packages/rs-drive/src/drive/votes/mod.rs b/packages/rs-drive/src/drive/votes/mod.rs index 7b1cf13217e..6d0991a576a 100644 --- a/packages/rs-drive/src/drive/votes/mod.rs +++ b/packages/rs-drive/src/drive/votes/mod.rs @@ -15,6 +15,9 @@ use dpp::voting::votes::resource_vote::ResourceVote; use dpp::voting::votes::Vote; use dpp::ProtocolError; +/// How many times a masternode already voted on a poll, before the vote being cast. +pub type PreviousVoteCount = u16; + #[cfg(feature = "server")] mod cleanup; #[cfg(all(feature = "server", any(test, feature = "structure")))] diff --git a/packages/rs-drive/src/drive/votes/paths.rs b/packages/rs-drive/src/drive/votes/paths.rs index e2cf2467830..ff39d13173a 100644 --- a/packages/rs-drive/src/drive/votes/paths.rs +++ b/packages/rs-drive/src/drive/votes/paths.rs @@ -64,6 +64,10 @@ pub const ACTIVE_POLLS_TREE_KEY: char = 'p'; /// A subtree made for being able to query votes that an identity has made pub const IDENTITY_VOTES_TREE_KEY: char = 'i'; +/// The subtrees of the decisions branch, in the order they are created: the active yes/no polls +/// and the identity votes index. +pub const VOTE_DECISIONS_SUBTREE_KEYS: [char; 2] = [ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY]; + /// The finished info pub const RESOURCE_STORED_INFO_KEY_U8_32: [u8; 32] = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -369,12 +373,23 @@ impl YesNoVotePollPaths for YesNoVotePoll { &self, vote_choice: YesNoAbstainVoteChoice, ) -> Result>, Error> { - let mut path = self.poll_path_vec()?; - path.push(vec![vote_choice.to_tree_key()]); - Ok(path) + Ok(vote_decisions_poll_choice_votes_tree_path_vec( + self.unique_id()?.as_bytes(), + vote_choice, + )) } } +/// the sum tree holding one choice's votes on a yes/no vote poll, by the poll's unique id +pub fn vote_decisions_poll_choice_votes_tree_path_vec( + poll_id: &[u8; 32], + vote_choice: YesNoAbstainVoteChoice, +) -> Vec> { + let mut path = vote_decisions_active_polls_poll_tree_path_vec(poll_id); + path.push(vec![vote_choice.to_tree_key()]); + path +} + /// the root path of the voting branch pub fn vote_root_path<'a>() -> [&'a [u8]; 1] { [Into::<&[u8; 1]>::into(RootTree::Votes)] @@ -635,16 +650,6 @@ pub fn vote_decisions_active_polls_tree_path_vec() -> Vec> { ] } -/// the tree of one yes/no vote poll, keyed by its unique id -pub fn vote_decisions_active_polls_poll_tree_path(poll_id: &[u8; 32]) -> [&[u8]; 4] { - [ - Into::<&[u8; 1]>::into(RootTree::Votes), - &[VOTE_DECISIONS_TREE_KEY as u8], - &[ACTIVE_POLLS_TREE_KEY as u8], - poll_id, - ] -} - /// the tree of one yes/no vote poll, keyed by its unique id, as a vec pub fn vote_decisions_active_polls_poll_tree_path_vec(poll_id: &[u8; 32]) -> Vec> { vec![ diff --git a/packages/rs-drive/src/drive/votes/resolved/vote_polls/mod.rs b/packages/rs-drive/src/drive/votes/resolved/vote_polls/mod.rs index 6e1546d9726..643af76e8a1 100644 --- a/packages/rs-drive/src/drive/votes/resolved/vote_polls/mod.rs +++ b/packages/rs-drive/src/drive/votes/resolved/vote_polls/mod.rs @@ -2,8 +2,6 @@ use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_ use derive_more::From; use dpp::identifier::Identifier; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; -use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; -use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use dpp::voting::vote_polls::VotePoll; use dpp::ProtocolError; use std::collections::BTreeMap; @@ -37,8 +35,6 @@ impl From<&ResolvedVotePoll> for VotePoll { } /// Represents a resolved vote poll in the system that also contains votes. -// The contested variant carries the resolved contract; a yes/no poll is a few bytes. -#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, From)] pub enum ResolvedVotePollWithVotes { /// A resolved vote poll with contract information for a contested document resource. @@ -46,11 +42,6 @@ pub enum ResolvedVotePollWithVotes { ContestedDocumentResourceVotePollWithContractInfo, BTreeMap>, ), - /// A yes/no vote poll and the masternodes that voted for each choice. - YesNoVotePollWithVotes( - YesNoVotePoll, - BTreeMap>, - ), } impl ResolvedVotePoll { diff --git a/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs b/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs index 425d51fd56c..15e3024c287 100644 --- a/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs +++ b/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs @@ -1,12 +1,10 @@ +use crate::drive::votes::PreviousVoteCount; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use dpp::voting::votes::yes_no_vote::accessors::v0::YesNoVoteGettersV0; use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; use dpp::voting::votes::yes_no_vote::YesNoVote; -/// How many times a masternode already voted on a poll, before the vote being cast. -pub type PreviousYesNoVoteCount = u16; - /// A yes/no vote as the execution layer handles it. A yes/no poll needs no contract to be /// resolved, so this is the vote itself plus what validation learned from the state: the /// masternode's previous answer to the same poll, which registering the vote removes. @@ -18,7 +16,7 @@ pub struct ResolvedYesNoVote { pub vote_choice: YesNoAbstainVoteChoice, /// The masternode's previous answer to this poll and how many times it had voted, when it /// changes its vote. `None` for a first vote, and whenever validation was skipped. - pub previous_vote_choice_to_remove: Option<(YesNoAbstainVoteChoice, PreviousYesNoVoteCount)>, + pub previous_vote_choice_to_remove: Option<(YesNoAbstainVoteChoice, PreviousVoteCount)>, } impl ResolvedYesNoVote { diff --git a/packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs b/packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs index 2c8c12bfd77..fee760af627 100644 --- a/packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs +++ b/packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs @@ -1,6 +1,4 @@ -use crate::drive::votes::paths::{ - vote_decisions_tree_path, ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY, -}; +use crate::drive::votes::paths::{vote_decisions_tree_path, VOTE_DECISIONS_SUBTREE_KEYS}; use crate::drive::Drive; use crate::error::Error; use dpp::version::PlatformVersion; @@ -16,7 +14,7 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { - for subtree_key in [ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY] { + for subtree_key in VOTE_DECISIONS_SUBTREE_KEYS { self.grove_insert_if_not_exists( vote_decisions_tree_path().as_slice().into(), &[subtree_key as u8], @@ -61,12 +59,10 @@ mod tests { vote_decisions_active_polls_tree_path(), vote_decisions_identity_votes_tree_path(), ] { - let (parent, key) = path.split_at(3); - let _ = key; let element = drive .grove .get( - &parent[..2], + &path[..2], path[2], None, &platform_version.drive.grove_version, diff --git a/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs b/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs index 82fba796a9d..30ad7f53051 100644 --- a/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs +++ b/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs @@ -1,3 +1,5 @@ +use crate::error::drive::DriveError; +use crate::error::Error; use bincode::{Decode, DecodeUntrusted, Encode}; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; @@ -22,6 +24,18 @@ impl YesNoVoteReferenceStorageForm { bincode::decode_from_slice(bytes, bincode::config::standard().with_big_endian()) .map(|(form, _)| form) } + + /// Decodes the form stored in the identity votes index, a stored form that does not + /// decode being corrupted state. + pub fn deserialize_stored(bytes: &[u8]) -> Result { + Self::deserialize(bytes).map_err(|e| { + Error::Drive(DriveError::CorruptedSerialization(format!( + "serialization of yes/no vote reference {} is corrupted: {}", + hex::encode(bytes), + e + ))) + }) + } } #[cfg(test)] diff --git a/packages/rs-drive/src/drive/votes/structure.rs b/packages/rs-drive/src/drive/votes/structure.rs index eb5267fd181..ca149407275 100644 --- a/packages/rs-drive/src/drive/votes/structure.rs +++ b/packages/rs-drive/src/drive/votes/structure.rs @@ -16,7 +16,6 @@ const CONTENDER_FLAGS: &str = refunded when the poll is cleaned up. Written without storage flags, it carries none."; const POLL_FLAGS: &str = "The owner is the identity whose contested document started the \ poll. A yes/no poll is opened by the system and carries none."; -const POLL: [FlagsKind; 2] = [FlagsKind::EpochOwned, FlagsKind::None]; const OWNED: [FlagsKind; 2] = [FlagsKind::EpochOwned, FlagsKind::None]; const CONTESTED_DOCUMENT: &str = "votes.contested_resource.active_polls.contract.document_type.storage.document"; @@ -121,7 +120,7 @@ pub(crate) fn structure() -> StructureNode { with the sign bit flipped", ) .kind(ElementKind::Tree) - .flags(&POLL, POLL_FLAGS) + .flags(&OWNED, POLL_FLAGS) .describe("The polls ending at this time.") .child( StructureNode::dynamic( @@ -129,10 +128,12 @@ pub(crate) fn structure() -> StructureNode { "vote_poll_id", KeyMatcher::Len(32), KeyEncoding::Hash32, - "The double sha256 of the serialized vote poll", + "The poll's unique id: for a contest the double sha256 of the \ + serialized poll, for a yes/no poll that of the yes/no \ + domain tag followed by the serialized vote poll", ) .kind(ElementKind::Item) - .flags(&POLL, POLL_FLAGS) + .flags(&OWNED, POLL_FLAGS) .value("serialized VotePoll") .describe("One poll ending at this time."), ), @@ -376,7 +377,8 @@ fn decision_active_polls() -> StructureNode { "vote_poll_id", KeyMatcher::Len(32), KeyEncoding::Hash32, - "The double sha256 of the serialized vote poll", + "The double sha256 of the domain tag `dash platform yes/no vote \ + poll id` followed by the serialized vote poll", ) .kind(ElementKind::Tree) .describe( @@ -452,7 +454,8 @@ fn decision_identity_votes() -> StructureNode { "vote_poll_id", KeyMatcher::Len(32), KeyEncoding::Hash32, - "The double sha256 of the serialized vote poll", + "The double sha256 of the domain tag `dash platform yes/no vote \ + poll id` followed by the serialized vote poll", ) .kind(ElementKind::Item) .value( diff --git a/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs index b50fbb850f7..d3dc7fca5cb 100644 --- a/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs +++ b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs @@ -183,20 +183,12 @@ impl YesNoVotePollStateDriveQuery { #[cfg(all(test, feature = "server", feature = "verify"))] mod tests { use super::*; - use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; - use dpp::block::block_info::BlockInfo; - use dpp::platform_value::BinaryData; + use crate::util::test_helpers::{open_yes_no_vote_poll_with_votes, test_yes_no_vote_poll}; use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStatus; - use dpp::voting::vote_polls::yes_no_vote_poll::YesNoMinimumVotingPower; fn poll() -> YesNoVotePoll { - YesNoVotePoll { - resource_path: vec![BinaryData::new(vec![0xc1; 32])], - supermajority_numerator: 2, - supermajority_denominator: 3, - minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), - } + test_yes_no_vote_poll() } #[test] @@ -204,35 +196,16 @@ mod tests { let drive = setup_drive_with_initial_state_structure(None); let platform_version = PlatformVersion::latest(); let vote_poll = poll(); - drive - .open_yes_no_vote_poll( - &vote_poll, - 1_000, - &BlockInfo::default(), - None, - platform_version, - ) - .expect("expected to open the poll"); - for (voter, strength, vote_choice) in [ - ([1u8; 32], 4, YesNoAbstainVoteChoice::Yes), - ([2u8; 32], 1, YesNoAbstainVoteChoice::Yes), - ([3u8; 32], 1, YesNoAbstainVoteChoice::No), - ] { - drive - .register_yes_no_identity_vote( - voter, - strength, - ResolvedYesNoVote { - vote_poll: vote_poll.clone(), - vote_choice, - previous_vote_choice_to_remove: None, - }, - &BlockInfo::default(), - None, - platform_version, - ) - .expect("expected to register the vote"); - } + open_yes_no_vote_poll_with_votes( + &drive, + &vote_poll, + &[ + ([1u8; 32], 4, YesNoAbstainVoteChoice::Yes), + ([2u8; 32], 1, YesNoAbstainVoteChoice::Yes), + ([3u8; 32], 1, YesNoAbstainVoteChoice::No), + ], + platform_version, + ); let query = YesNoVotePollStateDriveQuery { vote_poll }; let state = query .execute_no_proof(&drive, None, &mut vec![], platform_version) diff --git a/packages/rs-drive/src/state_transition_action/identity/masternode_vote/v0/mod.rs b/packages/rs-drive/src/state_transition_action/identity/masternode_vote/v0/mod.rs index 9461bb882cb..55ed2e1bf27 100644 --- a/packages/rs-drive/src/state_transition_action/identity/masternode_vote/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/identity/masternode_vote/v0/mod.rs @@ -6,7 +6,7 @@ use dpp::prelude::IdentityNonce; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; /// The previous vote count -pub type PreviousVoteCount = u16; +pub use crate::drive::votes::PreviousVoteCount; /// action v0 #[derive(Debug, Clone)] diff --git a/packages/rs-drive/src/structure/tests.rs b/packages/rs-drive/src/structure/tests.rs index a216ccd3e8e..978dd03afc1 100644 --- a/packages/rs-drive/src/structure/tests.rs +++ b/packages/rs-drive/src/structure/tests.rs @@ -280,7 +280,6 @@ mod fixtures { use crate::drive::credit_pools::epochs::operations_factory::EpochOperations; use crate::drive::credit_pools::operations::update_lifetime_storage_fee_pool_operation; use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfo; - use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; use crate::drive::Drive; use crate::fees::op::LowLevelDriveOperation; use crate::structure::export::{LayerShape, ShapeNode, StateShape}; @@ -300,6 +299,7 @@ mod fixtures { use crate::util::storage_flags::StorageFlags; use crate::util::test_helpers::setup::setup_document; use crate::util::test_helpers::setup_contract; + use crate::util::test_helpers::{open_yes_no_vote_poll_with_votes, test_yes_no_vote_poll}; use dpp::address_funds::PlatformAddress; use dpp::block::block_info::BlockInfo; use crate::drive::credit_pools::epochs::paths::EpochProposers; @@ -365,7 +365,6 @@ mod fixtures { use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_info_storage::contested_document_vote_poll_stored_info::ContestedDocumentVotePollStoredInfo; - use dpp::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; use rand::rngs::StdRng; use rand::SeedableRng; use std::borrow::Cow; @@ -1407,41 +1406,16 @@ mod fixtures { fn yes_no_vote_polls(run: &mut FixtureRun) { let platform_version = PlatformVersion::latest(); let drive = setup_drive_with_initial_state_structure(Some(platform_version)); - let vote_poll = YesNoVotePoll { - resource_path: vec![BinaryData::new(vec![0xc1; 32])], - supermajority_numerator: 2, - supermajority_denominator: 3, - minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), - }; - drive - .open_yes_no_vote_poll( - &vote_poll, - 1_000, - &BlockInfo::default(), - None, - platform_version, - ) - .expect("expected to open the yes/no vote poll"); - for (voter, strength, vote_choice) in [ - ([1; 32], 4, YesNoAbstainVoteChoice::Yes), - ([2; 32], 1, YesNoAbstainVoteChoice::No), - ([3; 32], 1, YesNoAbstainVoteChoice::Abstain), - ] { - drive - .register_yes_no_identity_vote( - voter, - strength, - ResolvedYesNoVote { - vote_poll: vote_poll.clone(), - vote_choice, - previous_vote_choice_to_remove: None, - }, - &BlockInfo::default(), - None, - platform_version, - ) - .expect("expected to register the vote"); - } + open_yes_no_vote_poll_with_votes( + &drive, + &test_yes_no_vote_poll(), + &[ + ([1; 32], 4, YesNoAbstainVoteChoice::Yes), + ([2; 32], 1, YesNoAbstainVoteChoice::No), + ([3; 32], 1, YesNoAbstainVoteChoice::Abstain), + ], + platform_version, + ); conformance_of(&drive, "yes_no_vote_polls", run); } diff --git a/packages/rs-drive/src/util/test_helpers/mod.rs b/packages/rs-drive/src/util/test_helpers/mod.rs index 201d7d76d45..8c67f8a8f01 100644 --- a/packages/rs-drive/src/util/test_helpers/mod.rs +++ b/packages/rs-drive/src/util/test_helpers/mod.rs @@ -242,6 +242,61 @@ pub(crate) fn add_dpns_name_contenders( vote_poll } +#[cfg(all(test, feature = "server"))] +/// The yes/no poll rs-drive's tests open: it passes on more than two thirds of yes over yes plus +/// no, with at least 400 voting power cast. +pub(crate) fn test_yes_no_vote_poll() -> dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll { + use dpp::platform_value::BinaryData; + use dpp::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; + YesNoVotePoll { + resource_path: vec![BinaryData::new(vec![0xc1; 32])], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), + } +} + +#[cfg(all(test, feature = "server"))] +/// Opens `vote_poll` to end at 1_000, written straight to Drive at the default block with no +/// validation, and registers each `(voter, strength, choice)` vote on it as a first vote. +pub(crate) fn open_yes_no_vote_poll_with_votes( + drive: &crate::drive::Drive, + vote_poll: &dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll, + votes: &[( + [u8; 32], + u8, + dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice, + )], + platform_version: &dpp::version::PlatformVersion, +) { + use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; + drive + .open_yes_no_vote_poll( + vote_poll, + 1_000, + &dpp::block::block_info::BlockInfo::default(), + None, + platform_version, + ) + .expect("expected to open the yes/no vote poll"); + for (voter, strength, vote_choice) in votes { + drive + .register_yes_no_identity_vote( + *voter, + *strength, + ResolvedYesNoVote { + vote_poll: vote_poll.clone(), + vote_choice: *vote_choice, + previous_vote_choice_to_remove: None, + }, + &dpp::block::block_info::BlockInfo::default(), + None, + platform_version, + ) + .expect("expected to register the vote"); + } +} + #[cfg(test)] /// Serializes a hex string to CBOR. pub fn cbor_from_hex(hex_string: String) -> Vec { diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index c6a3ef81bfb..42f07653974 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -5391,50 +5391,26 @@ mod tests { // --- MasternodeVote on a yes/no poll: the prover's path query and the verifier agree, // so a registered vote is proved executed and an unregistered one is refused. #[test] - fn verify_masternode_yes_no_vote_executed_round_trip() { - use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; + fn should_verify_a_yes_no_masternode_vote_executed_and_refuse_one_never_cast() { use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; - use dpp::platform_value::BinaryData; + use crate::util::test_helpers::{open_yes_no_vote_poll_with_votes, test_yes_no_vote_poll}; use dpp::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; - use dpp::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; use dpp::voting::votes::yes_no_vote::YesNoVote; use dpp::voting::votes::Vote; let platform_version = PlatformVersion::latest(); let drive = setup_drive_with_initial_state_structure(None); - let vote_poll = YesNoVotePoll { - resource_path: vec![BinaryData::new(vec![0xc1; 32])], - supermajority_numerator: 2, - supermajority_denominator: 3, - minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), - }; - drive - .open_yes_no_vote_poll( - &vote_poll, - 1_000, - &BlockInfo::default(), - None, - platform_version, - ) - .expect("expected to open the poll"); + let vote_poll = test_yes_no_vote_poll(); let voter = [1u8; 32]; - drive - .register_yes_no_identity_vote( - voter, - 4, - ResolvedYesNoVote { - vote_poll: vote_poll.clone(), - vote_choice: YesNoAbstainVoteChoice::Yes, - previous_vote_choice_to_remove: None, - }, - &BlockInfo::default(), - None, - platform_version, - ) - .expect("expected to register the vote"); + open_yes_no_vote_poll_with_votes( + &drive, + &vote_poll, + &[(voter, 4, YesNoAbstainVoteChoice::Yes)], + platform_version, + ); let transition_for = |voter: [u8; 32], vote_choice: YesNoAbstainVoteChoice| { StateTransition::MasternodeVote(MasternodeVoteTransition::V0( diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs index 52303ad7580..9d6d773d7ec 100644 --- a/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs @@ -51,6 +51,10 @@ impl Drive { // decisions branch, so it goes to its own verifier: querying the contested index for it // would accept a genuine absence proof from the wrong branch as "not voted". A resource // vote cannot name a yes/no poll at all. + // + // In place (client side, every version): these arms take only votes naming a yes/no + // poll, which no valid transition carries before protocol version 15 (it does not + // decode there). Every contested vote reaches the version match below unchanged. match vote { Vote::YesNoVote(yes_no_vote) => { return Self::verify_masternode_yes_no_vote( diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs index ab9e09ccf36..601a6f08db5 100644 --- a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs @@ -45,24 +45,18 @@ impl Drive { #[cfg(all(test, feature = "server"))] mod tests { use super::*; - use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; use crate::error::proof::ProofError; use crate::query::IdentityBasedVoteDriveQuery; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; - use dpp::block::block_info::BlockInfo; - use dpp::platform_value::{BinaryData, Identifier}; + use crate::util::test_helpers::{open_yes_no_vote_poll_with_votes, test_yes_no_vote_poll}; + use dpp::platform_value::Identifier; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; - use dpp::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; + use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use dpp::voting::vote_polls::VotePoll; use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; fn poll() -> YesNoVotePoll { - YesNoVotePoll { - resource_path: vec![BinaryData::new(vec![0xc1; 32])], - supermajority_numerator: 2, - supermajority_denominator: 3, - minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), - } + test_yes_no_vote_poll() } fn vote(vote_choice: YesNoAbstainVoteChoice) -> YesNoVote { @@ -90,30 +84,13 @@ mod tests { fn should_prove_a_vote_and_its_absence() { let drive = setup_drive_with_initial_state_structure(None); let platform_version = PlatformVersion::latest(); - drive - .open_yes_no_vote_poll( - &poll(), - 1_000, - &BlockInfo::default(), - None, - platform_version, - ) - .expect("open"); let voter = [7u8; 32]; - drive - .register_yes_no_identity_vote( - voter, - 1, - ResolvedYesNoVote { - vote_poll: poll(), - vote_choice: YesNoAbstainVoteChoice::No, - previous_vote_choice_to_remove: None, - }, - &BlockInfo::default(), - None, - platform_version, - ) - .expect("register"); + open_yes_no_vote_poll_with_votes( + &drive, + &poll(), + &[(voter, 1, YesNoAbstainVoteChoice::No)], + platform_version, + ); let proof = proof_of_vote(&drive, voter); let (_, proved) = Drive::verify_masternode_yes_no_vote( @@ -174,30 +151,13 @@ mod tests { platform_version, ) .expect("expected a data contract"); - drive - .open_yes_no_vote_poll( - &poll(), - 1_000, - &BlockInfo::default(), - None, - platform_version, - ) - .expect("open"); let voter = [7u8; 32]; - drive - .register_yes_no_identity_vote( - voter, - 1, - ResolvedYesNoVote { - vote_poll: poll(), - vote_choice: YesNoAbstainVoteChoice::No, - previous_vote_choice_to_remove: None, - }, - &BlockInfo::default(), - None, - platform_version, - ) - .expect("register"); + open_yes_no_vote_poll_with_votes( + &drive, + &poll(), + &[(voter, 1, YesNoAbstainVoteChoice::No)], + platform_version, + ); let yes_no_vote = Vote::YesNoVote(vote(YesNoAbstainVoteChoice::No)); // The right proof verifies through the generic entry point. diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs index ad9cbf4df36..c232a2170be 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs @@ -1,7 +1,6 @@ use super::v10::DRIVE_ABCI_METHOD_VERSIONS_V10; use super::{ DkgMiningSchedule, DriveAbciCoreChainLockMethodVersionsAndConstants, DriveAbciMethodVersions, - DriveAbciVotingMethodVersions, }; /// Dash Core's DKG schedule (`llmq/params.h`) of the non-rotating quorum types, the ones chain @@ -88,9 +87,7 @@ const DKG_MINING_SCHEDULES_V1: &[DkgMiningSchedule] = &[ ]; /// Drive ABCI method versions 11, introduced in protocol v15. Differs from v10 in the chain lock -/// verification and in the closing of yes/no vote polls. -/// -/// The chain lock verification only works as a whole: +/// verification, which only works as a whole: /// - `core_chain_lock.verify_chain_lock_locally` is 1 rather than 0: a chain lock is rejected /// locally only when Platform holds exactly the quorums active at its signing height, using /// Dash Core's DKG mining schedule in `core_chain_lock.dkg_mining_schedules`. When a quorum may @@ -101,11 +98,6 @@ const DKG_MINING_SCHEDULES_V1: &[DkgMiningSchedule] = &[ /// `submitchainlock`, which skips that check for a height Core already has a chain lock at or /// above. `verify_chain_lock_locally` 1 leaves to Core chain locks that 0 rejected, so it is /// only safe together with this check. -/// -/// The yes/no vote polls of protocol v15 share the end date index with contested polls: -/// `voting.clean_up_after_vote_poll_end` is 1 rather than 0, so the yes/no polls a block ends are -/// cleaned up too. The contested ones still go to `clean_up_after_contested_resources_vote_poll_end` -/// 1, and each clean-up removes its own polls from the end date index. pub const DRIVE_ABCI_METHOD_VERSIONS_V11: DriveAbciMethodVersions = DriveAbciMethodVersions { core_chain_lock: DriveAbciCoreChainLockMethodVersionsAndConstants { verify_chain_lock_locally: 1, @@ -113,9 +105,5 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V11: DriveAbciMethodVersions = DriveAbciMet dkg_mining_schedules: Some(DKG_MINING_SCHEDULES_V1), ..DRIVE_ABCI_METHOD_VERSIONS_V10.core_chain_lock }, - voting: DriveAbciVotingMethodVersions { - clean_up_after_vote_poll_end: 1, - ..DRIVE_ABCI_METHOD_VERSIONS_V10.voting - }, ..DRIVE_ABCI_METHOD_VERSIONS_V10 }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs index 4fa0553e6c3..e3d9000d143 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs @@ -18,7 +18,8 @@ pub struct DriveVoteMethodVersions { } /// The methods of the yes/no poll kind. All of them are new in protocol version 15 and -/// unreachable before it: no yes/no poll can be opened or voted on earlier. +/// unreachable before it: opening a poll is refused before 15, and a vote naming a yes/no poll +/// does not decode before 15. #[derive(Clone, Debug, Default)] pub struct DriveVoteYesNoMethodVersions { pub open_yes_no_vote_poll: FeatureVersion, @@ -76,6 +77,5 @@ pub struct DriveVoteInsertMethodVersions { pub struct DriveVoteContestedResourceInsertMethodVersions { pub register_contested_resource_identity_vote: FeatureVersion, pub insert_stored_info_for_contested_resource_vote_poll: FeatureVersion, - pub register_identity_vote: FeatureVersion, pub add_vote_poll_end_date_query_operations: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs index 511785661ae..8adc75d9b56 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs @@ -11,7 +11,6 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V1: DriveVoteMethodVersions = DriveVoteMeth contested_resource_insert: DriveVoteContestedResourceInsertMethodVersions { register_contested_resource_identity_vote: 0, insert_stored_info_for_contested_resource_vote_poll: 0, - register_identity_vote: 0, add_vote_poll_end_date_query_operations: 0, }, cleanup: DriveVoteCleanupMethodVersions { diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs index 7378617154a..dfe7164b973 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs @@ -11,7 +11,6 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V2: DriveVoteMethodVersions = DriveVoteMeth contested_resource_insert: DriveVoteContestedResourceInsertMethodVersions { register_contested_resource_identity_vote: 0, insert_stored_info_for_contested_resource_vote_poll: 0, - register_identity_vote: 0, add_vote_poll_end_date_query_operations: 0, }, cleanup: DriveVoteCleanupMethodVersions { diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs index 3f85e432762..cf56063a3d4 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs @@ -19,7 +19,6 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V3: DriveVoteMethodVersions = DriveVoteMeth contested_resource_insert: DriveVoteContestedResourceInsertMethodVersions { register_contested_resource_identity_vote: 0, insert_stored_info_for_contested_resource_vote_poll: 0, - register_identity_vote: 0, add_vote_poll_end_date_query_operations: 0, }, cleanup: DriveVoteCleanupMethodVersions { diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 61036fe4837..51f856219a1 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -79,17 +79,15 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// the polls live under the votes tree's decisions branch, which genesis /// (`create_initial_state_structure` 4 behind `>= 15`) and /// `transition_to_version_15` create. Masternode vote `transform_into_action` -/// 1 (`DRIVE_ABCI_VALIDATION_VERSIONS_V11`) looks up the previous answer, -/// `clean_up_after_vote_poll_end` 1 (`DRIVE_ABCI_METHOD_VERSIONS_V11`) cleans -/// finished yes/no polls up beside contested ones, and `DRIVE_VERSION_V10` -/// registers and sweeps the votes. Nothing opens a yes/no poll yet, so at this version -/// every yes/no vote is refused for want of a poll. +/// 1 (`DRIVE_ABCI_VALIDATION_VERSIONS_V11`) looks up the previous answer, and +/// `DRIVE_VERSION_V10` registers and sweeps the votes. Nothing opens a yes/no +/// poll yet, so at this version every yes/no vote is refused for want of a poll. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, drive: DRIVE_VERSION_V10, // changed: vote method versions v4: register_identity_vote 1 registers a yes/no vote and remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, - methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock; clean_up_after_vote_poll_end 1 cleans up the yes/no polls a block ends beside the contested ones + methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: identity update advanced_structure 1 refuses unpaid an identity update whose added key's proof of possession does not verify; masternode vote transform_into_action 1 looks up a masternode's previous answer on a yes/no poll withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, query: DRIVE_ABCI_QUERY_VERSIONS_V2, diff --git a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs index d9a9d397cce..87bbd3931c4 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs @@ -10,6 +10,7 @@ use dpp::platform_value::{Identifier, Value}; use dpp::prelude::TimestampMillis; use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; use dpp::voting::vote_polls::VotePoll; +use drive::config::DEFAULT_QUERY_LIMIT; use drive::query::contested_resource_votes_given_by_identity_query::ContestedResourceVotesGivenByIdentityQuery; use drive::query::vote_poll_contestant_votes_query::ContestedDocumentVotePollVotesDriveQuery; use drive::query::vote_poll_vote_state_query::{ @@ -436,7 +437,7 @@ impl Sdk { limit: Option, ) -> Result, Error> { let dpns_contract_id = self.get_dpns_contract_id()?; - let query_limit = limit.unwrap_or(MAXIMUM_VOTE_POLLS_BY_END_DATE_LIMIT); + let mut query_limit = limit.unwrap_or(DEFAULT_QUERY_LIMIT); let mut name_to_end_time: BTreeMap = BTreeMap::new(); let mut current_start_time = start_time.map(|t| (t, true)); @@ -449,6 +450,13 @@ impl Sdk { .fetch_vote_polls_by_end_date(current_start_time, end_time, query_limit) .await?; let Some(&(last_ts, _)) = result.0.last() else { + // Empty end date trees alone can fill a page and leave it empty. Before ending, + // read on at the largest page, which only that many empty end dates in a row + // would leave empty too (each block sweeps the first due ones away). + if query_limit < DEFAULT_QUERY_LIMIT { + query_limit = DEFAULT_QUERY_LIMIT; + continue; + } break; }; let polls_in_page: usize = result.0.iter().map(|(_, polls)| polls.len()).sum(); @@ -459,19 +467,19 @@ impl Sdk { // This end date alone filled the page, so the limit may have cut it, and a // proved query cannot continue within one end date: read it alone at the // largest limit, or refuse rather than leave contests out. - if query_limit >= MAXIMUM_VOTE_POLLS_BY_END_DATE_LIMIT { + if query_limit >= DEFAULT_QUERY_LIMIT { return Err(too_many_vote_polls_at_end_date(last_ts)); } let whole_end_date = self .fetch_vote_polls_by_end_date( Some((last_ts, true)), Some(last_ts), - MAXIMUM_VOTE_POLLS_BY_END_DATE_LIMIT, + DEFAULT_QUERY_LIMIT, ) .await?; let polls_at_end_date: usize = whole_end_date.0.iter().map(|(_, polls)| polls.len()).sum(); - if polls_at_end_date >= MAXIMUM_VOTE_POLLS_BY_END_DATE_LIMIT as usize { + if polls_at_end_date >= DEFAULT_QUERY_LIMIT as usize { return Err(too_many_vote_polls_at_end_date(last_ts)); } record_dpns_contests(&mut name_to_end_time, dpns_contract_id, whole_end_date); @@ -487,10 +495,6 @@ impl Sdk { } } -/// The largest page the vote polls by end date query serves (the platform's default query -/// limit), and the page size `get_current_dpns_contests` uses when given none. -const MAXIMUM_VOTE_POLLS_BY_END_DATE_LIMIT: u16 = 100; - impl Sdk { /// One proved page of vote polls by end date, from `start_time` up to `end_time` included. async fn fetch_vote_polls_by_end_date( @@ -538,7 +542,7 @@ fn record_dpns_contests( /// that its contests cannot all be listed. fn too_many_vote_polls_at_end_date(end_date: TimestampMillis) -> Error { Error::Generic(format!( - "at least {MAXIMUM_VOTE_POLLS_BY_END_DATE_LIMIT} vote polls end at {end_date}, more than \ + "at least {DEFAULT_QUERY_LIMIT} vote polls end at {end_date}, more than \ one page of vote polls by end date can list" )) } From 217cd1425234cb6abb1dea4f4bbe2c9292690dbf Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 25 Oct 2026 11:44:35 +0300 Subject: [PATCH 08/18] fix(platform): apply the third yes/no vote poll review round Opening polls: - opening is gated by the version tables: yes_no.open_yes_no_vote_poll is an OptionalFeatureVersion, None up to protocol version 14 and Some(0) in DRIVE_VOTE_METHOD_VERSIONS_V4, in place of a protocol_version literal (the test mocks no longer slip through). - an end date past i64::MAX is refused: the end date index flips its top bit, so such a poll would close in the block that opens it. - the opener follows the batch contract of document operations (earlier operations as previous operations, a fresh batch), which already dedupes shared trees and refuses a poll the batch opens twice; the in-batch scans are gone. It writes its end date entry itself and skips a tree a contest among the previous operations already queues, so that tree keeps the contender's storage flags. The queued-operation scan is shared with the document counters (grove_operation_queued). Closing polls: - check_for_ended_vote_polls v1 splits the yes/no polls off before resolution, so its contested loop is as on base again. - the tally comes first and also tells a leftover end date entry, with no separate stored info read; the voters fetch reads keys only; the vote trees are dropped by poll id with the GroveDB delete_tree_op builder. - the masternode removal sweep drops each removed masternode's tree in the yes/no identity votes index flat, emptied or not. - resolving a yes/no vote without its previous answer is refused. Other: - transform_into_action v0 keeps its own 1/4 vote weights. - YesNoMinimumVotingPower::Absolute is a struct variant; its tagged serde is derived on the enum itself (same JSON, Value and bincode bytes). - YesNoVotePoll's decode limit sits in its one platform_serialize attribute. - one decoder for the yes/no vote reference; the yes/no fetches use grove_get_raw_optional_item. - get_current_dpns_contests re-reads a full end date through the same retry. - wasm-dpp2: kind and yesNoChoice come from serde, and the minimum and choice TypeScript types are shared aliases. - test doc comments, imports and dispatcher # Parameters / # Returns docs. Co-Authored-By: Claude Opus 5.5 --- .../src/errors/consensus/state/state_error.rs | 4 +- packages/rs-dpp/src/state_transition/mod.rs | 2 +- .../voting/vote_polls/yes_no_vote_poll/mod.rs | 93 +++------- .../src/voting/votes/yes_no_vote/mod.rs | 2 +- .../check_for_ended_vote_polls/v1/mod.rs | 66 ++++--- .../mod.rs | 10 + .../v0/mod.rs | 6 +- .../mod.rs | 11 ++ .../execution/platform_events/voting/mod.rs | 3 + .../on_yes_no_vote_poll_finished/mod.rs | 10 + .../tally_votes_for_yes_no_vote_poll/mod.rs | 8 + .../masternode_vote/end_date_cleanup_tests.rs | 8 +- .../no_locking_contest_tests.rs | 8 +- .../transform_into_action/v0/mod.rs | 8 +- .../masternode_vote/yes_no_tests.rs | 10 +- .../state_transition/state_transitions/mod.rs | 12 +- .../document/summable_off_count_counter.rs | 5 +- .../v1/mod.rs | 51 +++-- .../mod.rs | 9 + .../mod.rs | 14 +- .../v0/mod.rs | 26 +-- .../mod.rs | 10 + .../mod.rs | 8 + .../v0/mod.rs | 2 +- .../fetch/fetch_identity_yes_no_vote/mod.rs | 10 + .../fetch_identity_yes_no_vote/v0/mod.rs | 16 +- .../fetch_yes_no_vote_poll_stored_info/mod.rs | 8 + .../v0/mod.rs | 14 +- .../mod.rs | 17 ++ .../yes_no/open_yes_no_vote_poll/mod.rs | 144 ++++++++++++-- .../yes_no/open_yes_no_vote_poll/v0/mod.rs | 175 ++++++++---------- .../register_yes_no_identity_vote/mod.rs | 21 +++ .../src/drive/votes/resolved/votes/resolve.rs | 20 +- .../votes/resolved_yes_no_vote/mod.rs | 10 - .../yes_no_vote_reference_storage_form/mod.rs | 24 +-- .../pending_grove_operations/mod.rs | 14 +- .../rs-drive/src/util/test_helpers/mod.rs | 31 ++-- .../verify_masternode_yes_no_vote/mod.rs | 13 +- .../mod.rs | 7 + .../drive_vote_method_versions/mod.rs | 9 +- .../drive_vote_method_versions/v1.rs | 2 +- .../drive_vote_method_versions/v2.rs | 2 +- .../drive_vote_method_versions/v3.rs | 2 +- .../drive_vote_method_versions/v4.rs | 7 + .../dpns_usernames/contested_queries.rs | 17 +- packages/wasm-dpp2/src/voting/vote.rs | 23 ++- packages/wasm-dpp2/src/voting/vote_poll.rs | 49 ++--- 47 files changed, 634 insertions(+), 387 deletions(-) diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index 888b00e3edf..e6db59653ca 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -1690,7 +1690,9 @@ mod tests { resource_path: vec![BinaryData::new(vec![1; 32])], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), + minimum_voting_power: YesNoMinimumVotingPower::Absolute { + voting_power: 400 + }, }, YesNoVotePollStatus::Started(BlockInfo::default()) ) diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index a17ad8178a1..d3e3177761a 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -2749,7 +2749,7 @@ mod tests { resource_path: vec![BinaryData::new(vec![7u8; 32])], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), + minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 400 }, }; let votes = [ Vote::YesNoVote(YesNoVote::V0(YesNoVoteV0 { diff --git a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs index 58ca3758207..ff06426a3de 100644 --- a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs @@ -56,14 +56,12 @@ pub enum VotingPowerRounding { #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde( - into = "YesNoMinimumVotingPowerRepr", - from = "YesNoMinimumVotingPowerRepr" - ) + serde(tag = "$type", rename_all = "camelCase") )] pub enum YesNoMinimumVotingPower { /// A fixed voting power, at most `max_yes_no_vote_poll_minimum_voting_power`. - Absolute(VotingPower), + #[cfg_attr(feature = "serde-conversion", serde(rename_all = "camelCase"))] + Absolute { voting_power: VotingPower }, /// A share of the total voting power of the masternode list (masternode 1, evonode 4) in /// the block that closes the poll, rounded as asked, at most /// `max_yes_no_vote_poll_minimum_voting_power_percent_of_total` percent of it. @@ -74,65 +72,13 @@ pub enum YesNoMinimumVotingPower { }, } -// Internal-`$type` serde shape: `Absolute` is a tuple variant, which internal tagging cannot -// carry, so this struct-variant helper names its field. Bincode, and with it the poll id, still -// encodes the outer enum directly. -#[cfg(feature = "serde-conversion")] -#[derive(Serialize, Deserialize)] -#[serde(tag = "$type", rename_all = "camelCase")] -enum YesNoMinimumVotingPowerRepr { - #[serde(rename_all = "camelCase")] - Absolute { voting_power: VotingPower }, - FractionOfTotal { - numerator: u8, - denominator: u8, - rounding: VotingPowerRounding, - }, -} - -#[cfg(feature = "serde-conversion")] -impl From for YesNoMinimumVotingPowerRepr { - fn from(minimum: YesNoMinimumVotingPower) -> Self { - match minimum { - YesNoMinimumVotingPower::Absolute(voting_power) => Self::Absolute { voting_power }, - YesNoMinimumVotingPower::FractionOfTotal { - numerator, - denominator, - rounding, - } => Self::FractionOfTotal { - numerator, - denominator, - rounding, - }, - } - } -} - -#[cfg(feature = "serde-conversion")] -impl From for YesNoMinimumVotingPower { - fn from(repr: YesNoMinimumVotingPowerRepr) -> Self { - match repr { - YesNoMinimumVotingPowerRepr::Absolute { voting_power } => Self::Absolute(voting_power), - YesNoMinimumVotingPowerRepr::FractionOfTotal { - numerator, - denominator, - rounding, - } => Self::FractionOfTotal { - numerator, - denominator, - rounding, - }, - } - } -} - impl YesNoMinimumVotingPower { /// The minimum as a voting power, given the total voting power of the masternode list when /// the poll closes. A fraction with a zero denominator (refused when the poll opens) needs /// more than any poll can gather. pub fn resolve(&self, total_voting_power: VotingPower) -> VotingPower { match *self { - YesNoMinimumVotingPower::Absolute(voting_power) => voting_power, + YesNoMinimumVotingPower::Absolute { voting_power } => voting_power, YesNoMinimumVotingPower::FractionOfTotal { numerator, denominator, @@ -160,7 +106,7 @@ impl YesNoMinimumVotingPower { impl fmt::Display for YesNoMinimumVotingPower { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - YesNoMinimumVotingPower::Absolute(voting_power) => write!(f, "{}", voting_power), + YesNoMinimumVotingPower::Absolute { voting_power } => write!(f, "{}", voting_power), YesNoMinimumVotingPower::FractionOfTotal { numerator, denominator, @@ -223,8 +169,8 @@ impl fmt::Display for YesNoMinimumVotingPower { serde(rename_all = "camelCase") )] #[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] -#[platform_serialize(unversioned)] -#[platform_serialize(limit = 100000)] +// One attribute: the derives read only the first `platform_serialize` attribute. +#[platform_serialize(limit = 100000, unversioned)] pub struct YesNoVotePoll { /// What the poll decides on, as opaque segments chosen by the feature that opened it. pub resource_path: Vec, @@ -307,7 +253,7 @@ impl YesNoVotePoll { ))); } match self.minimum_voting_power { - YesNoMinimumVotingPower::Absolute(voting_power) => { + YesNoMinimumVotingPower::Absolute { voting_power } => { let max_voting_power = platform_version .system_limits .max_yes_no_vote_poll_minimum_voting_power; @@ -395,7 +341,7 @@ mod tests { ], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), + minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 400 }, } } @@ -403,7 +349,7 @@ mod tests { fn should_pass_strictly_above_two_thirds_and_fail_at_or_below_it() { // The minimum is met by every tally below; only the share decides. let poll = YesNoVotePoll { - minimum_voting_power: YesNoMinimumVotingPower::Absolute(300), + minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 300 }, ..poll() }; // 300 cast: two thirds is 200, so 201 passes and 200 does not. @@ -450,7 +396,10 @@ mod tests { assert_eq!(majority.resolve(10), 6); assert_eq!(majority.resolve(9), 5); assert_eq!(majority.resolve(u32::MAX), u32::MAX / 2 + 1); - assert_eq!(YesNoMinimumVotingPower::Absolute(400).resolve(10), 400); + assert_eq!( + YesNoMinimumVotingPower::Absolute { voting_power: 400 }.resolve(10), + 400 + ); // No overflow on the largest total. let half = YesNoMinimumVotingPower::FractionOfTotal { numerator: 1, @@ -495,8 +444,10 @@ mod tests { rounding: VotingPowerRounding::Up, }; for accepted in [ - YesNoMinimumVotingPower::Absolute(0), - YesNoMinimumVotingPower::Absolute(max_voting_power), + YesNoMinimumVotingPower::Absolute { voting_power: 0 }, + YesNoMinimumVotingPower::Absolute { + voting_power: max_voting_power, + }, share(0, 1), share(1, 3), share(1, 2), @@ -510,7 +461,9 @@ mod tests { ); } for refused in [ - YesNoMinimumVotingPower::Absolute(max_voting_power + 1), + YesNoMinimumVotingPower::Absolute { + voting_power: max_voting_power + 1, + }, share(1, 0), share(51, 100), share(2, 3), @@ -628,7 +581,7 @@ mod tests { ..poll() }; let other_minimum = YesNoVotePoll { - minimum_voting_power: YesNoMinimumVotingPower::Absolute(401), + minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 401 }, ..poll() }; for other in [other_path, other_rule, other_minimum] { @@ -671,7 +624,7 @@ mod json_convertible_tests { ], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), + minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 400 }, } } diff --git a/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs b/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs index c2009ee1838..57eb1073a6d 100644 --- a/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs +++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/mod.rs @@ -62,7 +62,7 @@ mod json_convertible_tests_yes_no_vote { resource_path: vec![BinaryData::new(vec![0xc1; 4])], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), + minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 400 }, }, vote_choice: YesNoAbstainVoteChoice::Yes, }) diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs index 7495101e1f4..1cc8e87cd63 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs @@ -81,22 +81,22 @@ where platform_version, )?; - // The yes/no polls this block closes, with their voters, and the end date entries of - // yes/no polls an earlier entry already closed. Both stay empty before protocol - // version 15. + // Yes/no polls need no resolution: they close here, before the contested polls, which + // keep the loop below as it was. Reached from protocol version 15 only: at 14, which + // also selects this generation, no yes/no poll is in the end date index, since opening + // one (`Drive::open_yes_no_vote_poll`) is not active before 15, and the contested polls + // are the polls the index returned, in the same order. The yes/no polls this block + // closes keep their voters for the clean-up; the end date entries of yes/no polls an + // earlier entry already closed are only removed. let mut finished_yes_no_vote_polls: Vec<(YesNoVotePoll, TimestampMillis, Vec)> = vec![]; let mut leftover_yes_no_end_date_entries: Vec<(Identifier, TimestampMillis)> = vec![]; - - // Check if awarding is disabled - let vote_polls_with_info = vote_polls_by_timestamp.into_iter().map(|(end_date, vote_polls)| { - let vote_polls_with_votes = vote_polls.into_iter().map(|vote_poll| { - // Reached from protocol version 15 only. At 14, which also selects this - // generation, no yes/no poll is in the end date index: it enters only - // through `Drive::open_yes_no_vote_poll`, which refuses to open one before - // protocol version 15. A yes/no poll needs no resolution, and is cleaned up - // apart from the contested polls below. - if let VotePoll::YesNoVotePoll(yes_no_vote_poll) = vote_poll { + let mut contested_vote_polls_by_timestamp: BTreeMap> = + BTreeMap::new(); + for (end_date, vote_polls) in vote_polls_by_timestamp { + for vote_poll in vote_polls { + match vote_poll { + VotePoll::YesNoVotePoll(yes_no_vote_poll) => { match self.finish_yes_no_vote_poll( block_platform_state, block_info, @@ -104,14 +104,26 @@ where transaction, platform_version, )? { - Some(voters) => { - finished_yes_no_vote_polls.push((yes_no_vote_poll, end_date, voters)) - } + Some(voters) => finished_yes_no_vote_polls.push(( + yes_no_vote_poll, + end_date, + voters, + )), None => leftover_yes_no_end_date_entries .push((yes_no_vote_poll.unique_id()?, end_date)), } - return Ok(None); } + contested_vote_poll => contested_vote_polls_by_timestamp + .entry(end_date) + .or_default() + .push(contested_vote_poll), + } + } + } + + // Check if awarding is disabled + let vote_polls_with_info = contested_vote_polls_by_timestamp.into_iter().map(|(end_date, vote_polls)| { + let vote_polls_with_votes = vote_polls.into_iter().map(|vote_poll| { let resolved_vote_poll = vote_poll.resolve(&self.drive, transaction, platform_version)?; match resolved_vote_poll { @@ -241,10 +253,10 @@ where transaction, platform_version, )?; - Ok(Some(ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(resolved_contested_document_resource_vote_poll, identifiers_voting_for_contenders))) + Ok(ResolvedVotePollWithVotes::ContestedDocumentResourceVotePollWithContractInfoAndVotes(resolved_contested_document_resource_vote_poll, identifiers_voting_for_contenders)) } } - }).collect::>, Error>>()?.into_iter().flatten().collect::>(); + }).collect::, Error>>()?; Ok((end_date, vote_polls_with_votes)) }).collect::>, Error>>() ?; @@ -291,20 +303,20 @@ where transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result>, Error> { - let already_finished = self - .drive - .fetch_yes_no_vote_poll_stored_info(vote_poll, transaction, platform_version)? - .is_some_and(|stored_info| stored_info.result().is_some()); - if already_finished { - return Ok(None); - } let tally = self.tally_votes_for_yes_no_vote_poll(vote_poll, transaction, platform_version)?; + if tally + .stored_info + .as_ref() + .is_some_and(|stored_info| stored_info.result().is_some()) + { + return Ok(None); + } // A minimum given as a share of the total is measured against the masternode list of // the closing block, each masternode weighed as its vote is. A fixed minimum reads no // total. let total_voting_power = match vote_poll.minimum_voting_power { - YesNoMinimumVotingPower::Absolute(_) => 0, + YesNoMinimumVotingPower::Absolute { .. } => 0, YesNoMinimumVotingPower::FractionOfTotal { .. } => block_platform_state .full_masternode_list() .values() diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs index 0e5b154881e..260a7480071 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs @@ -22,6 +22,16 @@ where /// /// `leftover_end_date_entries` are the end date entries, by poll id, of polls an earlier /// entry already closed: only the entries go. + /// + /// # Parameters + /// - `block_info`: the block that closes the polls, whose epoch gets what is left of their funds. + /// - `vote_polls`: the polls that ended, each with its end date and its voters. + /// - `leftover_end_date_entries`: the end date entries, by poll id, of polls an earlier entry already closed. + /// - `transaction`: the transaction to read and write in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// `Ok(())` once the clean-up is applied. pub(in crate::execution) fn clean_up_after_yes_no_vote_polls_end( &self, block_info: &BlockInfo, diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs index e99996b3368..0aa40e14c66 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs @@ -47,12 +47,8 @@ where )?; // The three vote trees of every poll. - let polls: Vec<&YesNoVotePoll> = vote_polls - .iter() - .map(|(vote_poll, _, _)| vote_poll) - .collect(); self.drive.remove_yes_no_vote_poll_votes_operations( - polls.as_slice(), + vote_poll_ids.as_slice(), &mut operations, platform_version, )?; diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs index 1d0665cb72e..002b8ef0553 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs @@ -18,6 +18,17 @@ where /// Decides a finished yes/no poll from its tallies and the total voting power of the /// masternode list in the closing block, and writes the result into its stored info, which /// stays as the record of the decision after the votes are cleaned up. + /// + /// # Parameters + /// - `block_info`: the block that closes the poll. + /// - `vote_poll`: the poll that ended. + /// - `tally`: its tallies and stored info. + /// - `total_voting_power`: the masternode list's total voting power in the closing block, 0 for a fixed minimum. + /// - `transaction`: the transaction to read and write in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The poll's result, as recorded. pub(in crate::execution) fn keep_record_of_finished_yes_no_vote_poll( &self, block_info: &BlockInfo, diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs index b7f7eb5f52b..0eb2116990a 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs @@ -16,6 +16,9 @@ mod tally_votes_for_yes_no_vote_poll; /// The voting power a masternode's yes/no vote carries: 1 for a masternode, 4 for an evonode, /// as for contested polls. The strength of each yes/no vote and the masternode list total that a /// share minimum is measured against both come from here, so they always use the same weights. +/// The contested vote strength of masternode vote `transform_into_action` v0 (protocol versions +/// 1 to 14, and every contested vote at 15) keeps its own copy of these weights, so changing +/// them here cannot change a shipped contest. pub(in crate::execution) fn masternode_vote_strength(node_type: &MasternodeType) -> u8 { match node_type { MasternodeType::Regular => 1, diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs index 640ffc7edfc..1b7afdd99fe 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs @@ -17,6 +17,16 @@ where /// Acts on the result of a yes/no poll that just finished, in the same block, before its /// votes are cleaned up. The feature that opened the poll recognises it by its resource /// path. Nothing opens a yes/no poll yet, so version 0 acts on nothing. + /// + /// # Parameters + /// - `block_info`: the block that closes the poll. + /// - `vote_poll`: the poll that finished. + /// - `result`: its result. + /// - `transaction`: the transaction to read and write in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// `Ok(())` once the feature that opened the poll acted on its result. pub(in crate::execution) fn on_yes_no_vote_poll_finished( &self, block_info: &BlockInfo, diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs index ccbfb229f2b..e4a8f9b64f4 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs @@ -14,6 +14,14 @@ where C: CoreRPCLike, { /// The tallies of a yes/no poll, read from its vote sum trees, with its stored info. + /// + /// # Parameters + /// - `vote_poll`: the poll to tally. + /// - `transaction`: the transaction to read in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The poll's state: its stored info and the voting power cast for each answer. pub(in crate::execution) fn tally_votes_for_yes_no_vote_poll( &self, vote_poll: &YesNoVotePoll, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs index 49387461c93..d0518cfedbe 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs @@ -83,7 +83,9 @@ async fn should_end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left(PlatformVersion::latest()).await; } -/// Protocol version 14 ends contests through the vote poll clean-up v0, which version 15 replaced. +/// Protocol version 14 runs the same end of block generations on this path as 15 today +/// (`check_for_ended_vote_polls` v1, `clean_up_after_vote_polls_end` v0); this run pins them to +/// 14, so a later change to 15's tables cannot leave 14 untested. #[tokio::test] async fn should_end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left_at_protocol_version_14( ) { @@ -198,7 +200,9 @@ async fn should_end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block() { end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block(PlatformVersion::latest()).await; } -/// Protocol version 14 ends contests through the vote poll clean-up v0, which version 15 replaced. +/// Protocol version 14 runs the same end of block generations on this path as 15 today +/// (`check_for_ended_vote_polls` v1, `clean_up_after_vote_polls_end` v0); this run pins them to +/// 14, so a later change to 15's tables cannot leave 14 untested. #[tokio::test] async fn should_end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block_at_protocol_version_14( ) { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs index 857d792d971..4d3fec87b3c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs @@ -749,7 +749,9 @@ async fn should_keep_a_shared_join_window_end_date_until_its_last_contest_moves( .await; } -/// Protocol version 14 ends contests through the vote poll clean-up v0, which version 15 replaced. +/// Protocol version 14 runs the same end of block generations on this path as 15 today +/// (`check_for_ended_vote_polls` v1, `clean_up_after_vote_polls_end` v0); this run pins them to +/// 14, so a later change to 15's tables cannot leave 14 untested. #[tokio::test] async fn should_keep_a_shared_join_window_end_date_until_its_last_contest_moves_at_protocol_version_14( ) { @@ -861,7 +863,9 @@ async fn should_end_a_contest_behind_end_dates_left_empty() { end_a_contest_behind_end_dates_left_empty(PlatformVersion::latest()).await; } -/// Protocol version 14 ends contests through the vote poll clean-up v0, which version 15 replaced. +/// Protocol version 14 runs the same end of block generations on this path as 15 today +/// (`check_for_ended_vote_polls` v1, `clean_up_after_vote_polls_end` v0); this run pins them to +/// 14, so a later change to 15's tables cannot leave 14 untested. #[tokio::test] async fn should_end_a_contest_behind_end_dates_left_empty_at_protocol_version_14() { end_a_contest_behind_end_dates_left_empty( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs index eb76b428c25..4f84d62f1e3 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs @@ -1,6 +1,5 @@ use crate::error::execution::ExecutionError; use crate::error::Error; -use crate::execution::platform_events::voting::masternode_vote_strength; use crate::platform_types::platform::PlatformRef; use dpp::consensus::state::state_error::StateError; use dpp::consensus::state::voting::masternode_not_found_error::MasternodeNotFoundError; @@ -9,7 +8,7 @@ use dpp::consensus::state::voting::masternode_voted_too_many_times::MasternodeVo use dpp::consensus::ConsensusError; use dpp::dashcore::hashes::Hash; use dpp::dashcore::ProTxHash; -use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeListItem; +use dpp::dashcore_rpc::dashcore_rpc_json::{MasternodeListItem, MasternodeType}; use dpp::identifier::Identifier; use dpp::prelude::ConsensusValidationResult; @@ -100,7 +99,10 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV0 for Masternode Err(error) => return Ok(ConsensusValidationResult::new_with_error(error)), }; - let strength = masternode_vote_strength(&masternode.node_type); + let strength = match masternode.node_type { + MasternodeType::Regular => 1, + MasternodeType::Evo => 4, + }; Ok(ConsensusValidationResult::new_with_data( MasternodeVoteTransitionAction::transform_from_transition( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs index 05afb42114d..9b1be0924f6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs @@ -44,7 +44,9 @@ use dpp::voting::votes::resource_vote::ResourceVote; use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; use dpp::voting::votes::yes_no_vote::YesNoVote; use dpp::voting::votes::Vote; -use drive::query::yes_no_vote_poll_state_query::YesNoVotePollStateDriveQuery; +use drive::query::yes_no_vote_poll_state_query::{ + YesNoVotePollState, YesNoVotePollStateDriveQuery, +}; use drive::util::test_helpers::vote_poll_end_dates; use simple_signer::signer::SimpleSigner; use std::ops::Deref; @@ -63,7 +65,9 @@ fn two_thirds_poll(purpose: &[u8], minimum_voting_power: u32) -> YesNoVotePoll { ], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: YesNoMinimumVotingPower::Absolute(minimum_voting_power), + minimum_voting_power: YesNoMinimumVotingPower::Absolute { + voting_power: minimum_voting_power, + }, } } @@ -338,7 +342,7 @@ fn poll_state( platform: &TempPlatform, vote_poll: &YesNoVotePoll, platform_version: &PlatformVersion, -) -> drive::query::yes_no_vote_poll_state_query::YesNoVotePollState { +) -> YesNoVotePollState { YesNoVotePollStateDriveQuery { vote_poll: vote_poll.clone(), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs index 49f98a81cc8..bec769ce305 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs @@ -185,7 +185,7 @@ pub(in crate::execution) mod tests { use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; use dpp::state_transition::masternode_vote_transition::methods::MasternodeVoteTransitionMethodsV0; use dpp::state_transition::StateTransition; - use dpp::block::epoch::EpochIndex; + use dpp::block::epoch::{Epoch, EpochIndex}; use dpp::tokens::calculate_token_id; use dpp::util::hash::hash_double; use dpp::util::strings::convert_to_homograph_safe_chars; @@ -205,7 +205,9 @@ pub(in crate::execution) mod tests { use drive::drive::votes::paths::VotePollPaths; use drive::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::resolve::ContestedDocumentResourceVotePollResolver; use drive::fees::op::LowLevelDriveOperation; + use drive::error::Error as DriveError; use drive::grovedb::Element; + use drive::grovedb::Error as GroveError; use crate::execution::types::block_execution_context::BlockExecutionContext; use crate::execution::types::block_execution_context::v0::BlockExecutionContextV0; use crate::expect_match; @@ -904,18 +906,16 @@ pub(in crate::execution) mod tests { pub(in crate::execution) fn processing_credits( platform: &TempPlatform, platform_version: &PlatformVersion, - ) -> dpp::fee::Credits { + ) -> Credits { match platform .drive .get_epoch_processing_credits_for_distribution( - &dpp::block::epoch::Epoch::new(0).expect("epoch"), + &Epoch::new(0).expect("epoch"), None, platform_version, ) { Ok(credits) => credits, - Err(drive::error::Error::GroveDB(error)) - if matches!(*error, drive::grovedb::Error::PathKeyNotFound(_)) => - { + Err(DriveError::GroveDB(error)) if matches!(*error, GroveError::PathKeyNotFound(_)) => { 0 } Err(error) => panic!("expected the epoch's processing credits: {error:?}"), diff --git a/packages/rs-drive/src/drive/document/summable_off_count_counter.rs b/packages/rs-drive/src/drive/document/summable_off_count_counter.rs index ebfae286010..1af9797c60e 100644 --- a/packages/rs-drive/src/drive/document/summable_off_count_counter.rs +++ b/packages/rs-drive/src/drive/document/summable_off_count_counter.rs @@ -30,7 +30,7 @@ use crate::error::fee::FeeError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::fees::op::LowLevelDriveOperation::CalculatedCostOperation; -use crate::util::grove_operations::pending_grove_operations::pending_grove_operations; +use crate::util::grove_operations::pending_grove_operations::grove_operation_queued; use crate::util::grove_operations::QueryTarget::QueryTargetValue; use crate::util::grove_operations::{ BatchDeleteUpTreeApplyType, BatchInsertApplyType, DirectQueryType, @@ -504,6 +504,5 @@ fn counter_queued( path: &KeyInfoPath, key: &KeyInfo, ) -> bool { - pending_grove_operations(operations) - .any(|operation| operation.path == *path && operation.key.as_ref() == Some(key)) + grove_operation_queued(operations, path, key) } diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs index 693bc7479b4..d04b0e67801 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs @@ -7,18 +7,20 @@ use crate::drive::votes::paths::{ vote_decisions_tree_path, IDENTITY_VOTES_TREE_KEY, }; use crate::drive::votes::storage_form::yes_no_vote_reference_storage_form::YesNoVoteReferenceStorageForm; +use crate::fees::op::LowLevelDriveOperation; use crate::util::grove_operations::{BatchDeleteApplyType, DirectQueryType}; use dpp::dashcore::Network; use dpp::prelude::{BlockHeight, Identifier}; use dpp::version::PlatformVersion; -use grovedb::{MaybeTree, TransactionArg}; +use grovedb::batch::{QualifiedGroveDbOp, SubelementsDeletionBehavior}; +use grovedb::{MaybeTree, TransactionArg, TreeType}; impl Drive { /// Version 1 (protocol version 15) removes the contested resource votes of the removed /// masternodes through version 0, unchanged, then their yes/no votes in a batch of its own: - /// the vote under the poll's choice sum tree and the entry in the decisions identity votes - /// index. The two batches touch different subtrees, so applying them one after the other - /// leaves the same state as one batch would. + /// the vote under the poll's choice sum tree, and each removed masternode's tree in the + /// decisions identity votes index, entries included. The two batches touch different + /// subtrees, so applying them one after the other leaves the same state as one batch would. pub(super) fn remove_all_votes_given_by_identities_v1( &self, identity_ids_as_byte_arrays: Vec>, @@ -55,16 +57,13 @@ impl Drive { let mut deletion_batch = vec![]; for (identifier_bytes, votes_to_remove) in self.identity_vote_entries_of_identities_v0( vote_decisions_identity_votes_tree_path_vec(), - identity_ids_as_byte_arrays, + identity_ids_as_byte_arrays.clone(), transaction, platform_version, )? { - let identity_id = Identifier::from_vec(identifier_bytes.clone())?; - let mut vote_poll_ids = Vec::with_capacity(votes_to_remove.len()); for (vote_poll_id, vote_to_remove) in votes_to_remove { - let storage_form = YesNoVoteReferenceStorageForm::deserialize_stored( - &vote_to_remove.into_item_bytes()?, - )?; + let storage_form = + YesNoVoteReferenceStorageForm::deserialize(&vote_to_remove.into_item_bytes()?)?; let vote_poll_id = Identifier::from_vec(vote_poll_id)?; let votes_path = vote_decisions_poll_choice_votes_tree_path_vec( vote_poll_id.as_bytes(), @@ -80,16 +79,34 @@ impl Drive { &mut deletion_batch, &platform_version.drive, )?; - vote_poll_ids.push(vote_poll_id); } - let vote_poll_ids: Vec<&Identifier> = vote_poll_ids.iter().collect(); - self.remove_yes_no_vote_references_given_by_identity_operations( - &identity_id, - vote_poll_ids.as_slice(), - &mut deletion_batch, + } + + // Then each removed masternode's tree in the identity votes index, emptied or not: the + // close of a poll removes the poll's entries but keeps the tree, which no one else + // removes. It holds only items, so it is dropped flat with them (GroveDB's flat drop, + // which the grove version of protocol version 15 accepts in a batch). + let identity_votes_path = vote_decisions_identity_votes_tree_path_vec(); + for identifier_bytes in identity_ids_as_byte_arrays { + let has_identity_votes_tree = self.grove_has_raw( + identity_votes_path.as_slice().into(), + identifier_bytes.as_slice(), + DirectQueryType::StatefulDirectQuery, transaction, - platform_version, + &mut vec![], + &platform_version.drive, )?; + if has_identity_votes_tree { + deletion_batch.push(LowLevelDriveOperation::GroveOperation( + QualifiedGroveDbOp::delete_tree_op( + identity_votes_path.clone(), + identifier_bytes, + TreeType::NormalTree, + SubelementsDeletionBehavior::DropFlat, + ) + .dont_check_for_backwards_references(), + )); + } } if !deletion_batch.is_empty() { diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs index e1c1308f35e..af72c7da7e7 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs @@ -14,6 +14,15 @@ impl Drive { /// and each end date's tree once nothing is left under it. Whether an end date is left empty /// is read from the tree, since it can still hold a poll of either kind that this batch does /// not remove. The yes/no clean-up removes its polls with it. + /// + /// # Parameters + /// - `vote_polls`: the finished polls' unique ids, each with its end date. + /// - `batch_operations`: where the removals are added. + /// - `transaction`: the transaction to read the end dates in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// `Ok(())` once the removals are added. pub fn remove_vote_poll_end_date_query_operations( &self, vote_polls: &[(Identifier, TimestampMillis)], diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs index 35dcc0aafb8..77baa3b32ac 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs @@ -4,15 +4,23 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use dpp::identifier::Identifier; use dpp::version::PlatformVersion; -use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; impl Drive { /// Removes the votes of finished yes/no polls with the three sum trees that hold them. The /// polls' stored info stays as the record of the decision. + /// + /// # Parameters + /// - `vote_poll_ids`: the unique ids of the finished polls. + /// - `batch_operations`: where the removals are added. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// `Ok(())` once the removals are added. pub fn remove_yes_no_vote_poll_votes_operations( &self, - vote_polls: &[&YesNoVotePoll], + vote_poll_ids: &[Identifier], batch_operations: &mut Vec, platform_version: &PlatformVersion, ) -> Result<(), Error> { @@ -24,7 +32,7 @@ impl Drive { .remove_yes_no_vote_poll_votes_operations { 0 => self.remove_yes_no_vote_poll_votes_operations_v0( - vote_polls, + vote_poll_ids, batch_operations, platform_version, ), diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs index d4da38c954b..d4d4945b99c 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs @@ -1,37 +1,37 @@ -use crate::drive::votes::paths::YesNoVotePollPaths; +use crate::drive::votes::paths::vote_decisions_active_polls_poll_tree_path_vec; use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use dpp::identifier::Identifier; use dpp::version::PlatformVersion; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; -use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; -use grovedb::batch::key_info::KeyInfo; -use grovedb::batch::{GroveOp, KeyInfoPath, QualifiedGroveDbOp, SubelementsDeletionBehavior}; +use grovedb::batch::{QualifiedGroveDbOp, SubelementsDeletionBehavior}; use grovedb::TreeType; impl Drive { pub(super) fn remove_yes_no_vote_poll_votes_operations_v0( &self, - vote_polls: &[&YesNoVotePoll], + vote_poll_ids: &[Identifier], batch_operations: &mut Vec, _platform_version: &PlatformVersion, ) -> Result<(), Error> { - for vote_poll in vote_polls { - let poll_path = KeyInfoPath::from_known_owned_path(vote_poll.poll_path_vec()?); + for vote_poll_id in vote_poll_ids { + let poll_path = vote_decisions_active_polls_poll_tree_path_vec(vote_poll_id.as_bytes()); for vote_choice in YesNoAbstainVoteChoice::ALL { // Each choice's sum tree holds only the votes, one sum item per voter, and a // finished poll is never opened again, so the tree is dropped whole without // reading or deleting its votes one by one (GroveDB's flat drop, which the // grove version of protocol version 15 accepts in a batch). - batch_operations.push(LowLevelDriveOperation::GroveOperation(QualifiedGroveDbOp { - path: poll_path.clone(), - key: Some(KeyInfo::KnownKey(vec![vote_choice.to_tree_key()])), - op: GroveOp::DeleteTreeDontCheckForBackwardsReferences( + batch_operations.push(LowLevelDriveOperation::GroveOperation( + QualifiedGroveDbOp::delete_tree_op( + poll_path.clone(), + vec![vote_choice.to_tree_key()], TreeType::SumTree, SubelementsDeletionBehavior::DropFlat, - ), - })); + ) + .dont_check_for_backwards_references(), + )); } } Ok(()) diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs index 5e869ff6bb5..cf75cf085a3 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs @@ -11,6 +11,16 @@ use grovedb::TransactionArg; impl Drive { /// Removes a masternode's entries for these yes/no polls from the identity votes index of /// the decisions branch. + /// + /// # Parameters + /// - `masternode_pro_tx_hash`: the masternode. + /// - `vote_poll_ids`: the polls whose entries go. + /// - `batch_operations`: where the removals are added. + /// - `transaction`: the transaction to read in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// `Ok(())` once the removals are added. pub fn remove_yes_no_vote_references_given_by_identity_operations( &self, masternode_pro_tx_hash: &Identifier, diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs index c3b78f34da8..262044d4501 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs @@ -11,6 +11,14 @@ use grovedb::TransactionArg; impl Drive { /// The masternodes that voted on a yes/no poll: the yes voters, then the no voters, then /// the abstaining ones. + /// + /// # Parameters + /// - `vote_poll`: the poll. + /// - `transaction`: the transaction to read in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The voters' pro tx hashes. pub fn fetch_identities_voting_in_yes_no_vote_poll( &self, vote_poll: &YesNoVotePoll, diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs index bc3da1232e7..1c596bd17b5 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs @@ -41,7 +41,7 @@ impl Drive { .grove_get_path_query( &path_query, transaction, - QueryResultType::QueryPathKeyElementTrioResultType, + QueryResultType::QueryKeyElementPairResultType, &mut vec![], &platform_version.drive, )? diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs index d09a1d4e7aa..e87fa7b744b 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs @@ -13,6 +13,16 @@ use grovedb::TransactionArg; impl Drive { /// A masternode's current answer to a yes/no poll and how many times it voted on it, or /// `None` when it never did. + /// + /// # Parameters + /// - `masternode_pro_tx_hash`: the masternode. + /// - `vote_poll_id`: the poll's unique id. + /// - `transaction`: the transaction to read in. + /// - `drive_operations`: where the read's cost is added. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The answer and how many times the masternode voted on the poll, or `None`. pub fn fetch_identity_yes_no_vote( &self, masternode_pro_tx_hash: Identifier, diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs index 7e3ea7ce72b..9ac6fbea7ff 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs @@ -21,20 +21,18 @@ impl Drive { ) -> Result, Error> { let path = vote_decisions_identity_votes_tree_path_for_identity(masternode_pro_tx_hash.as_bytes()); - let maybe_element = self.grove_get_raw_optional( + self.grove_get_raw_optional_item( (&path).into(), vote_poll_id.as_slice(), DirectQueryType::StatefulDirectQuery, transaction, drive_operations, &platform_version.drive, - )?; - maybe_element - .map(|element| { - let bytes = element.into_item_bytes()?; - let storage_form = YesNoVoteReferenceStorageForm::deserialize_stored(&bytes)?; - Ok((storage_form.vote_choice, storage_form.identity_vote_times)) - }) - .transpose() + )? + .map(|bytes| { + let storage_form = YesNoVoteReferenceStorageForm::deserialize(&bytes)?; + Ok((storage_form.vote_choice, storage_form.identity_vote_times)) + }) + .transpose() } } diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs index 32f6aec9697..7351d64d855 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs @@ -10,6 +10,14 @@ use grovedb::TransactionArg; impl Drive { /// The stored info of a yes/no vote poll, `None` when the poll was never opened. + /// + /// # Parameters + /// - `vote_poll`: the poll. + /// - `transaction`: the transaction to read in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The stored info, or `None`. pub fn fetch_yes_no_vote_poll_stored_info( &self, vote_poll: &YesNoVotePoll, diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs index 654fe0d5386..a6e176eb41b 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs @@ -16,20 +16,16 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result, Error> { let path = vote_poll.poll_path_vec()?; - let maybe_element = self.grove_get_raw_optional( + self.grove_get_raw_optional_item( path.as_slice().into(), &[YES_NO_VOTE_POLL_STORED_INFO_KEY], DirectQueryType::StatefulDirectQuery, transaction, &mut vec![], &platform_version.drive, - )?; - let Some(element) = maybe_element else { - return Ok(None); - }; - let bytes = element.into_item_bytes()?; - Ok(Some( - YesNoVotePollStoredInfo::deserialize_from_bytes_trusted(&bytes)?, - )) + )? + .map(|bytes| YesNoVotePollStoredInfo::deserialize_from_bytes_trusted(&bytes)) + .transpose() + .map_err(Error::from) } } diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/mod.rs index 4ddddcd1ef2..4e666fcee34 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/mod.rs @@ -11,6 +11,15 @@ use grovedb::TransactionArg; impl Drive { /// Writes the stored info of a yes/no vote poll, replacing what was there. + /// + /// # Parameters + /// - `vote_poll`: the poll. + /// - `stored_info`: its stored info. + /// - `transaction`: the transaction to write in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// `Ok(())` once the stored info is written. pub fn insert_stored_info_for_yes_no_vote_poll( &self, vote_poll: &YesNoVotePoll, @@ -40,6 +49,14 @@ impl Drive { } /// The operations of [`Self::insert_stored_info_for_yes_no_vote_poll`]. + /// + /// # Parameters + /// - `vote_poll`: the poll. + /// - `stored_info`: its stored info. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The operations that write it. pub fn insert_stored_info_for_yes_no_vote_poll_operations( &self, vote_poll: &YesNoVotePoll, diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs index 5b61b2c4777..17e3db18eaf 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs @@ -16,7 +16,19 @@ impl Drive { /// balance the votes are paid from is the caller's to fund, under the poll's /// `specialized_balance_id`. /// - /// Refuses parameters no poll can run with, and a poll that already exists. + /// Refuses parameters no poll can run with, an end date that is not after the opening block + /// (or past `i64::MAX`), and a poll that already exists. Not active before protocol version + /// 15. + /// + /// # Parameters + /// - `vote_poll`: the poll to open. + /// - `end_date`: when the poll closes, in milliseconds. + /// - `block_info`: the block that opens it. + /// - `transaction`: the transaction to read and write in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// `Ok(())` once the poll is open, or the error refusing it. pub fn open_yes_no_vote_poll( &self, vote_poll: &YesNoVotePoll, @@ -32,26 +44,51 @@ impl Drive { .yes_no .open_yes_no_vote_poll { - 0 => self.open_yes_no_vote_poll_v0( + Some(0) => self.open_yes_no_vote_poll_v0( vote_poll, end_date, block_info, transaction, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "open_yes_no_vote_poll".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "open_yes_no_vote_poll".to_string(), + known_versions: vec![0], + })), } } /// The operations of [`Self::open_yes_no_vote_poll`], added to `batch_operations`. /// + /// Operations already queued in the same batch, earlier openings and contests included, + /// go in `previous_batch_operations`, with `batch_operations` fresh, as for document + /// operations: that is how an opening refuses a poll the batch already opens and queues a + /// tree the batch already holds once. A contest that shares the poll's end date must come + /// before the opening: the contested end date insert skips only an identical insert, so a + /// contest after it would queue the end date's tree again. Two openings converted with no + /// view of each other can list one poll under two end dates; it then closes at the first and + /// its later entry is only removed. + /// /// They are stateful and have no estimated-costs path: a poll opens as a system action, /// outside any fee estimate. A caller that runs them inside a fee estimate must first give /// them one. + /// + /// # Parameters + /// - `vote_poll`: the poll to open. + /// - `end_date`: when the poll closes, in milliseconds. + /// - `block_info`: the block that opens it. + /// - `previous_batch_operations`: the operations the batch already holds. + /// - `batch_operations`: where the opening's operations are added. + /// - `transaction`: the transaction to read in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// `Ok(())` once the operations are added, or the error refusing the poll. #[allow(clippy::too_many_arguments)] pub fn open_yes_no_vote_poll_operations( &self, @@ -70,7 +107,7 @@ impl Drive { .yes_no .open_yes_no_vote_poll { - 0 => self.open_yes_no_vote_poll_operations_v0( + Some(0) => self.open_yes_no_vote_poll_operations_v0( vote_poll, end_date, block_info, @@ -79,11 +116,15 @@ impl Drive { transaction, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "open_yes_no_vote_poll_operations".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "open_yes_no_vote_poll_operations".to_string(), + known_versions: vec![0], + })), } } } @@ -91,8 +132,10 @@ impl Drive { #[cfg(test)] mod tests { use crate::drive::votes::paths::{ - vote_decisions_tree_path, ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY, + vote_decisions_tree_path, vote_end_date_queries_tree_path, ACTIVE_POLLS_TREE_KEY, + IDENTITY_VOTES_TREE_KEY, }; + use crate::util::common::encode::encode_u64; use crate::util::grove_operations::DirectQueryType; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; use crate::util::test_helpers::test_yes_no_vote_poll as poll; @@ -100,6 +143,8 @@ mod tests { use dpp::block::block_info::BlockInfo; use dpp::platform_value::BinaryData; use dpp::version::PlatformVersion; + use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; + use dpp::voting::vote_polls::VotePoll; #[test] fn should_refuse_to_open_a_poll_a_batch_already_opens() { @@ -118,14 +163,15 @@ mod tests { platform_version, ) .expect("expected to open the poll"); - // The same poll with another end date: the end date is not part of its id. + // The same poll with another end date (the end date is not part of its id), with the + // batch so far as the previous operations. drive .open_yes_no_vote_poll_operations( &vote_poll, 2_000, &BlockInfo::default(), - &mut None, - &mut batch_operations, + &mut Some(&mut batch_operations), + &mut vec![], None, platform_version, ) @@ -195,10 +241,11 @@ mod tests { time_ms: 1_000, ..Default::default() }; - for end_date in [999, 1_000] { + // The last two would sort before every block time in the end date index. + for end_date in [999, 1_000, i64::MAX as u64 + 1, u64::MAX] { drive .open_yes_no_vote_poll(&poll(), end_date, &block_info, None, platform_version) - .expect_err("expected a poll ending by the opening block to be refused"); + .expect_err("expected a poll that ends at once to be refused"); } drive .open_yes_no_vote_poll(&poll(), 1_001, &block_info, None, platform_version) @@ -220,17 +267,19 @@ mod tests { .push(BinaryData::new(b"other".to_vec())); let mut batch_operations = vec![]; for vote_poll in [poll(), other_poll.clone()] { + let mut opening_operations = vec![]; drive .open_yes_no_vote_poll_operations( &vote_poll, 1_000, &BlockInfo::default(), - &mut None, - &mut batch_operations, + &mut Some(&mut batch_operations), + &mut opening_operations, None, platform_version, ) .expect("expected to open the poll"); + batch_operations.append(&mut opening_operations); } drive .apply_batch_low_level_drive_operations( @@ -253,4 +302,73 @@ mod tests { .collect() ); } + + #[test] + fn should_keep_the_flags_of_a_contest_end_date_tree_the_batch_already_queues() { + // A contest created earlier in the batch queues the end date's tree with its + // contender's storage flags; the opening adds only its entry, and the tree keeps them. + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let contested_poll = VotePoll::ContestedDocumentResourceVotePoll( + ContestedDocumentResourceVotePoll::default(), + ); + let mut batch_operations = vec![]; + drive + .add_vote_poll_end_date_query_operations( + Some([7; 32]), + contested_poll.clone(), + 1_000, + &BlockInfo::default(), + &mut None, + &mut None, + &mut batch_operations, + None, + platform_version, + ) + .expect("expected the contest's end date entry"); + let mut opening_operations = vec![]; + drive + .open_yes_no_vote_poll_operations( + &poll(), + 1_000, + &BlockInfo::default(), + &mut Some(&mut batch_operations), + &mut opening_operations, + None, + platform_version, + ) + .expect("expected to open the poll"); + batch_operations.append(&mut opening_operations); + drive + .apply_batch_low_level_drive_operations( + None, + None, + batch_operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected the batch to apply"); + + let end_dates = vote_poll_end_dates(&drive, platform_version); + assert_eq!( + end_dates[&1_000], + [ + contested_poll.unique_id().expect("id"), + poll().unique_id().expect("id") + ] + .into_iter() + .collect() + ); + let end_date_tree = drive + .grove + .get( + &vote_end_date_queries_tree_path(), + encode_u64(1_000).as_slice(), + None, + &platform_version.drive.grove_version, + ) + .unwrap() + .expect("expected the end date's tree"); + assert!(end_date_tree.get_flags().is_some()); + } } diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs index e0ca5d167b2..9989837ad59 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs @@ -1,18 +1,19 @@ use crate::drive::votes::paths::{ + vote_contested_resource_end_date_queries_at_time_tree_path_vec, vote_decisions_active_polls_tree_path_vec, vote_decisions_tree_path_vec, vote_end_date_queries_tree_path_vec, YesNoVotePollPaths, VOTE_DECISIONS_SUBTREE_KEYS, }; use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; use crate::drive::Drive; -use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use crate::fees::op::LowLevelDriveOperation::{EphemeralGroveOperation, GroveOperation}; use crate::util::common::encode::encode_u64; -use crate::util::grove_operations::BatchInsertTreeApplyType; -use crate::util::object_size_info::{DriveKeyInfo, PathKeyInfo}; +use crate::util::grove_operations::pending_grove_operations::grove_operation_queued; +use crate::util::grove_operations::{BatchInsertApplyType, BatchInsertTreeApplyType}; +use crate::util::object_size_info::{DriveKeyInfo, PathKeyElementInfo, PathKeyInfo}; use dpp::block::block_info::BlockInfo; use dpp::prelude::TimestampMillis; +use dpp::serialization::PlatformSerializable; use dpp::version::PlatformVersion; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStoredInfo; @@ -21,7 +22,7 @@ use dpp::voting::vote_polls::VotePoll; use dpp::ProtocolError; use grovedb::batch::key_info::KeyInfo; use grovedb::batch::KeyInfoPath; -use grovedb::{TransactionArg, TreeType}; +use grovedb::{Element, TransactionArg, TreeType}; impl Drive { pub(super) fn open_yes_no_vote_poll_v0( @@ -62,23 +63,20 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { - // Yes/no polls exist from protocol version 15. The end-of-block generations that - // protocol version 14 also selects (`check_for_ended_vote_polls` v1) take their yes/no - // handling in place on the ground that no yes/no poll is in the end date index before - // 15, and this refusal is what keeps that true. - if platform_version.protocol_version < 15 { - return Err(Error::Drive(DriveError::NotSupported( - "yes/no vote polls can only be opened from protocol version 15", - ))); - } vote_poll.validate_parameters(platform_version)?; // A poll that ends in the block that opens it, or earlier, would close at the end of - // this block with no vote cast. - if end_date <= block_info.time_ms { + // this block with no vote cast. So would one ending after `i64::MAX`: the end date index + // keys an end date with its top bit flipped (`encode_u64`), which sorts such an end date + // before every block time. + if end_date <= block_info.time_ms || end_date > i64::MAX as TimestampMillis { return Err(Error::Protocol(Box::new(ProtocolError::VoteError( format!( - "yes/no vote poll {} must end after the block that opens it (at {}), not at {}", - vote_poll, block_info.time_ms, end_date + "yes/no vote poll {} must end after the block that opens it (at {}) and at \ + most at {}, not at {}", + vote_poll, + block_info.time_ms, + i64::MAX, + end_date ), )))); } @@ -86,35 +84,13 @@ impl Drive { // The decisions trees, when the chain lacks them: genesis and the upgrade to protocol // version 15 create them, but a chain that reached 15 before they existed did not. An - // earlier opening in this batch may already queue them. + // earlier opening in the batch, among the previous operations, may already queue them. for subtree_key in VOTE_DECISIONS_SUBTREE_KEYS { - let key = vec![subtree_key as u8]; - if !tree_queued_in(batch_operations, vote_decisions_tree_path_vec(), &key) { - self.batch_insert_empty_tree_if_not_exists( - PathKeyInfo::PathKey::<0>((vote_decisions_tree_path_vec(), key)), - TreeType::NormalTree, - None, - BatchInsertTreeApplyType::StatefulBatchInsertTree, - transaction, - previous_batch_operations, - batch_operations, - &platform_version.drive, - )?; - } - } - - // The poll's own tree. A poll that already has one was opened before, and so was one - // whose tree an earlier opening queued in this batch: opened twice, it would be listed - // under two end dates. - let active_polls_path = vote_decisions_active_polls_tree_path_vec(); - let queued_in_this_batch = tree_queued_in( - batch_operations, - active_polls_path.clone(), - poll_id.as_slice(), - ); - let inserted = !queued_in_this_batch - && self.batch_insert_empty_tree_if_not_exists( - PathKeyInfo::PathKey::<0>((active_polls_path, poll_id.to_vec())), + self.batch_insert_empty_tree_if_not_exists( + PathKeyInfo::PathKey::<0>(( + vote_decisions_tree_path_vec(), + vec![subtree_key as u8], + )), TreeType::NormalTree, None, BatchInsertTreeApplyType::StatefulBatchInsertTree, @@ -123,6 +99,23 @@ impl Drive { batch_operations, &platform_version.drive, )?; + } + + // The poll's own tree. A poll that already has one, in state or among the previous + // operations, was opened before: opened twice, it would be listed under two end dates. + let inserted = self.batch_insert_empty_tree_if_not_exists( + PathKeyInfo::PathKey::<0>(( + vote_decisions_active_polls_tree_path_vec(), + poll_id.to_vec(), + )), + TreeType::NormalTree, + None, + BatchInsertTreeApplyType::StatefulBatchInsertTree, + transaction, + previous_batch_operations, + batch_operations, + &platform_version.drive, + )?; if !inserted { return Err(Error::Protocol(Box::new(ProtocolError::VoteError( format!( @@ -153,60 +146,50 @@ impl Drive { )?; } - // The end date index closes the poll when its time comes. Nobody is refunded for the - // entry: the poll is a system action, so it carries no storage flags. When an earlier - // opening in this batch already queues the end date's tree, this batch serves as the - // operations to check, so the tree is queued once. - let end_date_tree_queued = tree_queued_in( - batch_operations, - vote_end_date_queries_tree_path_vec(), - &encode_u64(end_date), - ); - if end_date_tree_queued { - let mut end_date_operations = vec![]; - self.add_vote_poll_end_date_query_operations( + // The end date index closes the poll when its time comes: the end date's tree, then the + // poll's entry under it, the entry `add_vote_poll_end_date_query_operations` writes for + // a contest. Nobody is refunded for what an opening adds: the poll is a system action, + // so it carries no storage flags. A contest among the previous operations may already + // queue the end date's tree with its contender's storage flags; the tree keeps them and + // this opening adds only its entry, where the empty-tree insert, which skips only an + // identical insert, would queue the tree a second time. + let end_date_tree_path = vote_end_date_queries_tree_path_vec(); + let end_date_key = encode_u64(end_date); + let end_date_tree_queued = previous_batch_operations + .as_deref() + .is_some_and(|previous| { + grove_operation_queued( + previous, + &KeyInfoPath::from_known_owned_path(end_date_tree_path.clone()), + &KeyInfo::KnownKey(end_date_key.clone()), + ) + }); + if !end_date_tree_queued { + self.batch_insert_empty_tree_if_not_exists( + PathKeyInfo::PathKey::<0>((end_date_tree_path, end_date_key)), + TreeType::NormalTree, None, - VotePoll::YesNoVotePoll(vote_poll.clone()), - end_date, - block_info, - &mut None, - &mut Some(batch_operations), - &mut end_date_operations, + BatchInsertTreeApplyType::StatefulBatchInsertTree, transaction, - platform_version, - )?; - batch_operations.append(&mut end_date_operations); - Ok(()) - } else { - self.add_vote_poll_end_date_query_operations( - None, - VotePoll::YesNoVotePoll(vote_poll.clone()), - end_date, - block_info, - &mut None, previous_batch_operations, batch_operations, - transaction, - platform_version, - ) + &platform_version.drive, + )?; } + self.batch_insert_if_not_exists( + PathKeyElementInfo::PathKeyRefElement::<0>(( + vote_contested_resource_end_date_queries_at_time_tree_path_vec(end_date), + poll_id.as_bytes(), + Element::Item( + VotePoll::YesNoVotePoll(vote_poll.clone()).serialize_to_bytes()?, + None, + ), + )), + BatchInsertApplyType::StatefulBatchInsert, + transaction, + batch_operations, + &platform_version.drive, + )?; + Ok(()) } } - -/// Whether `batch_operations` already queues a grove operation on `key` under `path`, which -/// `batch_insert_empty_tree_if_not_exists` only looks for in the previous operations. -fn tree_queued_in( - batch_operations: &[LowLevelDriveOperation], - path: Vec>, - key: &[u8], -) -> bool { - let path = KeyInfoPath::from_known_owned_path(path); - let key = KeyInfo::KnownKey(key.to_vec()); - batch_operations.iter().any(|operation| { - matches!( - operation, - GroveOperation(queued) | EphemeralGroveOperation(queued, _) - if queued.path == path && queued.key.as_ref() == Some(&key) - ) - }) -} diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs index 3b9d63ec89a..cdde82cbf9d 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs @@ -14,6 +14,17 @@ impl Drive { /// Registers a masternode's vote on a yes/no poll: its strength under the chosen answer's /// sum tree, the previous answer removed if the vote changed, and the identity votes index /// updated. + /// + /// # Parameters + /// - `voter_pro_tx_hash`: the voting masternode's pro tx hash. + /// - `strength`: its voting power: 1 for a masternode, 4 for an evonode. + /// - `vote`: the vote, with the previous answer to remove when it changed. + /// - `block_info`: the block that registers it. + /// - `transaction`: the transaction to read and write in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The fee of the operations applied. pub fn register_yes_no_identity_vote( &self, voter_pro_tx_hash: [u8; 32], @@ -47,6 +58,16 @@ impl Drive { } /// The operations of [`Self::register_yes_no_identity_vote`]. + /// + /// # Parameters + /// - `voter_pro_tx_hash`: the voting masternode's pro tx hash. + /// - `strength`: its voting power: 1 for a masternode, 4 for an evonode. + /// - `vote`: the vote, with the previous answer to remove when it changed. + /// - `transaction`: the transaction to read in. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The operations that register the vote. pub fn register_yes_no_identity_vote_operations( &self, voter_pro_tx_hash: [u8; 32], diff --git a/packages/rs-drive/src/drive/votes/resolved/votes/resolve.rs b/packages/rs-drive/src/drive/votes/resolved/votes/resolve.rs index 053a181a82e..9f36001257c 100644 --- a/packages/rs-drive/src/drive/votes/resolved/votes/resolve.rs +++ b/packages/rs-drive/src/drive/votes/resolved/votes/resolve.rs @@ -1,7 +1,7 @@ use crate::drive::votes::resolved::votes::resolved_resource_vote::resolve::ResourceVoteResolver; -use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; use crate::drive::votes::resolved::votes::ResolvedVote; use crate::drive::Drive; +use crate::error::drive::DriveError; use crate::error::Error; use dpp::voting::votes::Vote; use grovedb::TransactionArg; @@ -43,6 +43,12 @@ pub trait VoteResolver { ) -> Result; } +/// A yes/no vote is not resolved here: transform_into_action v1 builds its action itself, with +/// the masternode's previous answer, which a resolution from the vote alone would lose (and +/// with it the removal of the previous answer's vote and the count of vote changes). +const YES_NO_VOTE_NOT_RESOLVED: &str = + "a yes/no vote is resolved with its previous answer, by transform_into_action"; + impl VoteResolver for Vote { fn resolve( &self, @@ -54,9 +60,9 @@ impl VoteResolver for Vote { Vote::ResourceVote(resource_vote) => Ok(ResolvedVote::ResolvedResourceVote( resource_vote.resolve(drive, transaction, platform_version)?, )), - Vote::YesNoVote(yes_no_vote) => Ok(ResolvedVote::YesNoVote( - ResolvedYesNoVote::from_vote(yes_no_vote), - )), + Vote::YesNoVote(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution( + YES_NO_VOTE_NOT_RESOLVED, + ))), } } @@ -70,9 +76,9 @@ impl VoteResolver for Vote { Vote::ResourceVote(resource_vote) => Ok(ResolvedVote::ResolvedResourceVote( resource_vote.resolve_owned(drive, transaction, platform_version)?, )), - Vote::YesNoVote(yes_no_vote) => Ok(ResolvedVote::YesNoVote( - ResolvedYesNoVote::from_vote_owned(yes_no_vote), - )), + Vote::YesNoVote(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution( + YES_NO_VOTE_NOT_RESOLVED, + ))), } } } diff --git a/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs b/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs index 15e3024c287..8a86bca0181 100644 --- a/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs +++ b/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs @@ -28,16 +28,6 @@ impl ResolvedYesNoVote { previous_vote_choice_to_remove: None, } } - - /// The vote as it came in, owned, with no previous answer attached. - pub fn from_vote_owned(vote: YesNoVote) -> Self { - let vote_choice = vote.vote_choice(); - ResolvedYesNoVote { - vote_poll: vote.vote_poll_owned(), - vote_choice, - previous_vote_choice_to_remove: None, - } - } } impl From for YesNoVote { diff --git a/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs b/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs index 30ad7f53051..e4fd3665a7f 100644 --- a/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs +++ b/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs @@ -19,22 +19,18 @@ impl YesNoVoteReferenceStorageForm { bincode::encode_to_vec(self, bincode::config::standard().with_big_endian()) } - /// Decodes a stored form. - pub fn deserialize(bytes: &[u8]) -> Result { - bincode::decode_from_slice(bytes, bincode::config::standard().with_big_endian()) - .map(|(form, _)| form) - } - /// Decodes the form stored in the identity votes index, a stored form that does not /// decode being corrupted state. - pub fn deserialize_stored(bytes: &[u8]) -> Result { - Self::deserialize(bytes).map_err(|e| { - Error::Drive(DriveError::CorruptedSerialization(format!( - "serialization of yes/no vote reference {} is corrupted: {}", - hex::encode(bytes), - e - ))) - }) + pub fn deserialize(bytes: &[u8]) -> Result { + bincode::decode_from_slice(bytes, bincode::config::standard().with_big_endian()) + .map(|(form, _)| form) + .map_err(|e| { + Error::Drive(DriveError::CorruptedSerialization(format!( + "serialization of yes/no vote reference {} is corrupted: {}", + hex::encode(bytes), + e + ))) + }) } } diff --git a/packages/rs-drive/src/util/grove_operations/pending_grove_operations/mod.rs b/packages/rs-drive/src/util/grove_operations/pending_grove_operations/mod.rs index 025cd325856..935b55cc56d 100644 --- a/packages/rs-drive/src/util/grove_operations/pending_grove_operations/mod.rs +++ b/packages/rs-drive/src/util/grove_operations/pending_grove_operations/mod.rs @@ -9,7 +9,8 @@ //! batch for every delete. use crate::fees::op::LowLevelDriveOperation; -use grovedb::batch::QualifiedGroveDbOp; +use grovedb::batch::key_info::KeyInfo; +use grovedb::batch::{KeyInfoPath, QualifiedGroveDbOp}; #[cfg(test)] use std::cell::Cell; @@ -41,5 +42,16 @@ pub(crate) fn pending_grove_operations( }) } +/// Whether `drive_operations` already queue a grove operation, of any kind, on `key` under +/// `path`. +pub(crate) fn grove_operation_queued( + drive_operations: &[LowLevelDriveOperation], + path: &KeyInfoPath, + key: &KeyInfo, +) -> bool { + pending_grove_operations(drive_operations) + .any(|operation| operation.path == *path && operation.key.as_ref() == Some(key)) +} + #[cfg(test)] mod tests; diff --git a/packages/rs-drive/src/util/test_helpers/mod.rs b/packages/rs-drive/src/util/test_helpers/mod.rs index 8c67f8a8f01..8923cd5d52f 100644 --- a/packages/rs-drive/src/util/test_helpers/mod.rs +++ b/packages/rs-drive/src/util/test_helpers/mod.rs @@ -49,6 +49,8 @@ use ciborium::value::Value; #[cfg(test)] use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfo; +#[cfg(all(test, feature = "server"))] +use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; #[cfg(test)] use crate::util::object_size_info::DocumentInfo::DocumentRefInfo; #[cfg(test)] @@ -65,10 +67,16 @@ use dpp::data_contract::document_type::random_document::{ use dpp::document::DocumentV0Setters; #[cfg(test)] use dpp::platform_value; +#[cfg(all(test, feature = "server"))] +use dpp::platform_value::BinaryData; #[cfg(test)] use dpp::platform_value::Bytes32; +#[cfg(all(test, feature = "server"))] +use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; #[cfg(test)] use dpp::voting::vote_info_storage::contested_document_vote_poll_stored_info::ContestedDocumentVotePollStoredInfo; +#[cfg(all(test, feature = "server"))] +use dpp::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; #[cfg(test)] use rand::rngs::StdRng; #[cfg(test)] @@ -245,14 +253,12 @@ pub(crate) fn add_dpns_name_contenders( #[cfg(all(test, feature = "server"))] /// The yes/no poll rs-drive's tests open: it passes on more than two thirds of yes over yes plus /// no, with at least 400 voting power cast. -pub(crate) fn test_yes_no_vote_poll() -> dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll { - use dpp::platform_value::BinaryData; - use dpp::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; +pub(crate) fn test_yes_no_vote_poll() -> YesNoVotePoll { YesNoVotePoll { resource_path: vec![BinaryData::new(vec![0xc1; 32])], supermajority_numerator: 2, supermajority_denominator: 3, - minimum_voting_power: YesNoMinimumVotingPower::Absolute(400), + minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 400 }, } } @@ -260,21 +266,16 @@ pub(crate) fn test_yes_no_vote_poll() -> dpp::voting::vote_polls::yes_no_vote_po /// Opens `vote_poll` to end at 1_000, written straight to Drive at the default block with no /// validation, and registers each `(voter, strength, choice)` vote on it as a first vote. pub(crate) fn open_yes_no_vote_poll_with_votes( - drive: &crate::drive::Drive, - vote_poll: &dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll, - votes: &[( - [u8; 32], - u8, - dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice, - )], - platform_version: &dpp::version::PlatformVersion, + drive: &Drive, + vote_poll: &YesNoVotePoll, + votes: &[([u8; 32], u8, YesNoAbstainVoteChoice)], + platform_version: &PlatformVersion, ) { - use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; drive .open_yes_no_vote_poll( vote_poll, 1_000, - &dpp::block::block_info::BlockInfo::default(), + &BlockInfo::default(), None, platform_version, ) @@ -289,7 +290,7 @@ pub(crate) fn open_yes_no_vote_poll_with_votes( vote_choice: *vote_choice, previous_vote_choice_to_remove: None, }, - &dpp::block::block_info::BlockInfo::default(), + &BlockInfo::default(), None, platform_version, ) diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs index 601a6f08db5..893850118ec 100644 --- a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs @@ -10,8 +10,17 @@ use dpp::voting::votes::yes_no_vote::YesNoVote; impl Drive { /// Verifies a proof of a masternode's vote on a yes/no poll. /// - /// Returns the root hash and the proved vote, `None` when the proof shows the masternode - /// has not voted on the poll. A proved vote whose answer differs from `vote` is an error. + /// A proved vote whose answer differs from `vote` is an error. + /// + /// # Parameters + /// - `proof`: the proof. + /// - `masternode_pro_tx_hash`: the masternode. + /// - `vote`: the vote the proof is checked against. + /// - `verify_subset_of_proof`: whether the proof may hold more than this vote. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The root hash and the proved vote, `None` when the masternode has not voted on the poll. pub fn verify_masternode_yes_no_vote( proof: &[u8], masternode_pro_tx_hash: [u8; 32], diff --git a/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs b/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs index 19cb04ef544..12940737709 100644 --- a/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs @@ -10,6 +10,13 @@ use dpp::version::PlatformVersion; impl YesNoVotePollStateDriveQuery { /// Verifies a proof of the state of a yes/no vote poll: its stored info and its tallies. + /// + /// # Parameters + /// - `proof`: the proof. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The root hash and the poll's proved state. pub fn verify_yes_no_vote_poll_state_proof( &self, proof: &[u8], diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs index e3d9000d143..eba643f39bc 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs @@ -1,4 +1,4 @@ -use versioned_feature_core::FeatureVersion; +use versioned_feature_core::{FeatureVersion, OptionalFeatureVersion}; pub mod v1; pub mod v2; @@ -18,11 +18,12 @@ pub struct DriveVoteMethodVersions { } /// The methods of the yes/no poll kind. All of them are new in protocol version 15 and -/// unreachable before it: opening a poll is refused before 15, and a vote naming a yes/no poll -/// does not decode before 15. +/// unreachable before it: opening a poll is not active before 15, and a vote naming a yes/no +/// poll does not decode before 15. #[derive(Clone, Debug, Default)] pub struct DriveVoteYesNoMethodVersions { - pub open_yes_no_vote_poll: FeatureVersion, + /// `None` before protocol version 15, where no yes/no poll may be opened. + pub open_yes_no_vote_poll: OptionalFeatureVersion, pub register_yes_no_identity_vote: FeatureVersion, pub insert_stored_info_for_yes_no_vote_poll: FeatureVersion, pub fetch_yes_no_vote_poll_stored_info: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs index 8adc75d9b56..c8ee8ac8fda 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs @@ -38,7 +38,7 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V1: DriveVoteMethodVersions = DriveVoteMeth fetch_contested_document_vote_poll_contender_count: 0, }, yes_no: DriveVoteYesNoMethodVersions { - open_yes_no_vote_poll: 0, + open_yes_no_vote_poll: None, register_yes_no_identity_vote: 0, insert_stored_info_for_yes_no_vote_poll: 0, fetch_yes_no_vote_poll_stored_info: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs index dfe7164b973..03fe1027ddc 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs @@ -38,7 +38,7 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V2: DriveVoteMethodVersions = DriveVoteMeth fetch_contested_document_vote_poll_contender_count: 0, }, yes_no: DriveVoteYesNoMethodVersions { - open_yes_no_vote_poll: 0, + open_yes_no_vote_poll: None, register_yes_no_identity_vote: 0, insert_stored_info_for_yes_no_vote_poll: 0, fetch_yes_no_vote_poll_stored_info: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs index cf56063a3d4..2f5a19ea030 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs @@ -46,7 +46,7 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V3: DriveVoteMethodVersions = DriveVoteMeth fetch_contested_document_vote_poll_contender_count: 0, }, yes_no: DriveVoteYesNoMethodVersions { - open_yes_no_vote_poll: 0, + open_yes_no_vote_poll: None, register_yes_no_identity_vote: 0, insert_stored_info_for_yes_no_vote_poll: 0, fetch_yes_no_vote_poll_stored_info: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs index d26a38429f7..9539131ba0f 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs @@ -1,6 +1,7 @@ use crate::version::drive_versions::drive_vote_method_versions::v3::DRIVE_VOTE_METHOD_VERSIONS_V3; use crate::version::drive_versions::drive_vote_method_versions::{ DriveVoteCleanupMethodVersions, DriveVoteInsertMethodVersions, DriveVoteMethodVersions, + DriveVoteYesNoMethodVersions, }; /// Drive vote method versions 4. Introduced in protocol version 15 with the yes/no poll @@ -10,6 +11,8 @@ use crate::version::drive_versions::drive_vote_method_versions::{ /// contested resource votes, which v1 still hands to it). /// * `remove_all_votes_given_by_identities` 1 also removes the yes/no votes of a masternode /// that left the list. +/// * `yes_no.open_yes_no_vote_poll` is `Some(0)`: yes/no polls can be opened from this version +/// (`None` in every earlier table). /// /// Everything else matches `DRIVE_VOTE_METHOD_VERSIONS_V3`. pub const DRIVE_VOTE_METHOD_VERSIONS_V4: DriveVoteMethodVersions = DriveVoteMethodVersions { @@ -20,5 +23,9 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V4: DriveVoteMethodVersions = DriveVoteMeth remove_all_votes_given_by_identities: 1, ..DRIVE_VOTE_METHOD_VERSIONS_V3.cleanup }, + yes_no: DriveVoteYesNoMethodVersions { + open_yes_no_vote_poll: Some(0), + ..DRIVE_VOTE_METHOD_VERSIONS_V3.yes_no + }, ..DRIVE_VOTE_METHOD_VERSIONS_V3 }; diff --git a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs index 87bbd3931c4..c586f32e20b 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs @@ -465,24 +465,13 @@ impl Sdk { if current_start_time == Some((last_ts, true)) && polls_in_page >= query_limit as usize { // This end date alone filled the page, so the limit may have cut it, and a - // proved query cannot continue within one end date: read it alone at the + // proved query cannot continue within one end date: read it again at the // largest limit, or refuse rather than leave contests out. if query_limit >= DEFAULT_QUERY_LIMIT { return Err(too_many_vote_polls_at_end_date(last_ts)); } - let whole_end_date = self - .fetch_vote_polls_by_end_date( - Some((last_ts, true)), - Some(last_ts), - DEFAULT_QUERY_LIMIT, - ) - .await?; - let polls_at_end_date: usize = - whole_end_date.0.iter().map(|(_, polls)| polls.len()).sum(); - if polls_at_end_date >= DEFAULT_QUERY_LIMIT as usize { - return Err(too_many_vote_polls_at_end_date(last_ts)); - } - record_dpns_contests(&mut name_to_end_time, dpns_contract_id, whole_end_date); + query_limit = DEFAULT_QUERY_LIMIT; + continue; } match next_vote_polls_page_start(current_start_time, last_ts, end_time) { diff --git a/packages/wasm-dpp2/src/voting/vote.rs b/packages/wasm-dpp2/src/voting/vote.rs index ba1124182be..9f112273252 100644 --- a/packages/wasm-dpp2/src/voting/vote.rs +++ b/packages/wasm-dpp2/src/voting/vote.rs @@ -4,7 +4,6 @@ use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::voting::resource_vote_choice::ResourceVoteChoiceWasm; use crate::voting::vote_poll::VotePollWasm; -use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_polls::VotePoll; use dpp::voting::votes::Vote; use dpp::voting::votes::resource_vote::ResourceVote; @@ -17,6 +16,11 @@ use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen(typescript_custom_section)] const TS_TYPES: &str = r#" +/** + * A yes/no vote's answer, the same as a plain object and as JSON. + */ +export type YesNoAbstainVoteChoiceName = "yes" | "no" | "abstain"; + /** * Vote serialized as a plain object. * @@ -35,7 +39,7 @@ export type VoteObject = $type: "yesNoVote"; $formatVersion: string; votePoll: YesNoVotePollFieldsObject; - voteChoice: "yes" | "no" | "abstain"; + voteChoice: YesNoAbstainVoteChoiceName; }; /** @@ -52,7 +56,7 @@ export type VoteJSON = $type: "yesNoVote"; $formatVersion: string; votePoll: YesNoVotePollFieldsJSON; - voteChoice: "yes" | "no" | "abstain"; + voteChoice: YesNoAbstainVoteChoiceName; }; "#; @@ -120,11 +124,14 @@ impl VoteWasm { Vote::ResourceVote(_) => Err(WasmDppError::invalid_argument( "this vote is a resource vote, not a yes/no vote".to_string(), )), - Vote::YesNoVote(vote) => Ok(match vote.vote_choice() { - YesNoAbstainVoteChoice::Yes => "yes".to_string(), - YesNoAbstainVoteChoice::No => "no".to_string(), - YesNoAbstainVoteChoice::Abstain => "abstain".to_string(), - }), + // The name the choice's serde writes, so it always matches `toJSON()`. + Vote::YesNoVote(vote) => serde_json::to_value(vote.vote_choice()) + .map_err(|e| WasmDppError::serialization(e.to_string()))? + .as_str() + .map(str::to_owned) + .ok_or_else(|| { + WasmDppError::serialization("a yes/no vote choice without a name".to_string()) + }), } } diff --git a/packages/wasm-dpp2/src/voting/vote_poll.rs b/packages/wasm-dpp2/src/voting/vote_poll.rs index fb40179f3d4..418385aad1a 100644 --- a/packages/wasm-dpp2/src/voting/vote_poll.rs +++ b/packages/wasm-dpp2/src/voting/vote_poll.rs @@ -28,6 +28,20 @@ export interface VotePollOptions { indexValues: any[]; } +/** + * A yes/no poll's minimum yes plus no voting power, the same as a plain object + * and as JSON: a fixed voting power, or a share of the masternode list's total + * voting power when the poll closes. + */ +export type YesNoMinimumVotingPowerObject = + | { $type: "absolute"; votingPower: number } + | { + $type: "fractionOfTotal"; + numerator: number; + denominator: number; + rounding: "up" | "down" | "downPlusOne" | "upMinusOne"; + }; + /** * A yes/no poll's fields as a plain object. A yes/no vote carries its poll * like this, without a `$type` tag. The minimum is a fixed voting power or a @@ -37,14 +51,7 @@ export interface YesNoVotePollFieldsObject { resourcePath: Uint8Array[]; supermajorityNumerator: number; supermajorityDenominator: number; - minimumVotingPower: - | { $type: "absolute"; votingPower: number } - | { - $type: "fractionOfTotal"; - numerator: number; - denominator: number; - rounding: "up" | "down" | "downPlusOne" | "upMinusOne"; - }; + minimumVotingPower: YesNoMinimumVotingPowerObject; } /** @@ -54,14 +61,7 @@ export interface YesNoVotePollFieldsJSON { resourcePath: string[]; supermajorityNumerator: number; supermajorityDenominator: number; - minimumVotingPower: - | { $type: "absolute"; votingPower: number } - | { - $type: "fractionOfTotal"; - numerator: number; - denominator: number; - rounding: "up" | "down" | "downPlusOne" | "upMinusOne"; - }; + minimumVotingPower: YesNoMinimumVotingPowerObject; } /** @@ -157,15 +157,16 @@ impl VotePollWasm { self.0.to_string() } - /// The kind of poll: `contestedDocumentResourceVotePoll` or `yesNoVotePoll`. + /// The kind of poll: `contestedDocumentResourceVotePoll` or `yesNoVotePoll`, the `$type` + /// tag the poll's serde writes, so it always matches `toJSON()`. #[wasm_bindgen(getter = "kind")] - pub fn kind(&self) -> String { - match &self.0 { - VotePoll::ContestedDocumentResourceVotePoll(_) => { - "contestedDocumentResourceVotePoll".to_string() - } - VotePoll::YesNoVotePoll(_) => "yesNoVotePoll".to_string(), - } + pub fn kind(&self) -> WasmDppResult { + serde_json::to_value(&self.0) + .map_err(|e| WasmDppError::serialization(e.to_string()))? + .get("$type") + .and_then(serde_json::Value::as_str) + .map(str::to_owned) + .ok_or_else(|| WasmDppError::serialization("a vote poll without a $type".to_string())) } #[wasm_bindgen(getter = "contractId")] From 6ffb1e74e3531d7319582ab46240e045ad575720 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 27 Oct 2026 16:18:53 +0300 Subject: [PATCH 09/18] fix(platform): apply the fourth yes/no vote poll review round - The opener writes its end date entry through the shared writer again. add_vote_poll_end_date_query_operations 1 (protocol version 15) adds only the entry when the batch already queues the end date's tree with other storage flags, so a queued deletion of that tree (a contest leaving its join-window end date) is cancelled instead of failing the batch, and a contest after an opening no longer queues the tree twice. - The document counters' counter_queued is back to its shipped body; the shared grove_operation_queued helper is gone. - A yes/no poll may run at most max_yes_no_vote_poll_duration_ms (four weeks, a new SystemLimits field). - The vote-change and masternode helpers shared by transform v0 and v1 are previous_vote_to_replace_v0 and voting_masternode_v0, marked as shipped; the vote weight doc says PV15 freezes them. - The yes/no clean-up takes the due end date entries and the polls this block closed, each with its poll id, fund id (specialized_balance_id) and voters; it removes each voter's entry directly. The close decides finished, started or missing once and hands the stored info over. - Smaller clean-ups: ResolvedYesNoVote::from_vote, YesNoVotePollStoredInfo::update_to_latest_version, YesNoVoteGettersV0::vote_poll_owned and YesNoVotePollState::voting_power removed; the tally reads its poll directly; the removal sweep reads a masternode's tree only when it had no entries; one flat-drop builder; VotePoll.kind is an infallible match again; the SDK listing raises its limit on a full single end date page without refetching it; PV14 end of block tests loop over both versions; docs and changelog updated. Co-Authored-By: Claude Opus 5.5 --- .../yes_no_vote_poll_stored_info/mod.rs | 21 -- .../voting/votes/yes_no_vote/accessors/mod.rs | 6 - .../votes/yes_no_vote/accessors/v0/mod.rs | 3 - .../check_for_ended_vote_polls/v1/mod.rs | 56 ++--- .../mod.rs | 37 ++-- .../v0/mod.rs | 61 +++--- .../mod.rs | 10 +- .../v0/mod.rs | 19 +- .../execution/platform_events/voting/mod.rs | 8 +- .../masternode_vote/end_date_cleanup_tests.rs | 42 ++-- .../no_locking_contest_tests.rs | 33 +-- .../transform_into_action/v0/mod.rs | 15 +- .../transform_into_action/v1/mod.rs | 15 +- .../masternode_vote/yes_no_tests.rs | 13 +- packages/rs-drive/grovedb-structure.json | 2 +- .../document/summable_off_count_counter.rs | 5 +- .../v1/mod.rs | 43 ++-- .../v0/mod.rs | 16 +- .../v0/mod.rs | 2 +- .../mod.rs | 14 +- .../v1/mod.rs | 107 ++++++++++ .../yes_no/open_yes_no_vote_poll/mod.rs | 194 ++++++++++++++++-- .../yes_no/open_yes_no_vote_poll/v0/mod.rs | 91 +++----- .../votes/resolved_yes_no_vote/mod.rs | 12 -- .../rs-drive/src/drive/votes/structure.rs | 4 +- packages/rs-drive/src/fees/op.rs | 21 ++ .../src/query/yes_no_vote_poll_state_query.rs | 59 ++---- .../pending_grove_operations/mod.rs | 14 +- .../drive_vote_method_versions/mod.rs | 2 + .../drive_vote_method_versions/v4.rs | 11 +- .../src/version/drive_versions/v10.rs | 5 +- .../src/version/mocks/v2_test.rs | 1 + .../src/version/system_limits/mod.rs | 4 + .../src/version/system_limits/v1.rs | 1 + .../src/version/system_limits/v2.rs | 1 + .../src/version/system_limits/v3.rs | 1 + .../src/version/system_limits/v4.rs | 1 + .../rs-platform-version/src/version/v15.rs | 13 +- .../dpns_usernames/contested_queries.rs | 10 +- packages/wasm-dpp2/src/voting/vote_poll.rs | 16 +- .../wasm-dpp2/tests/unit/VotePoll.spec.ts | 2 + 41 files changed, 610 insertions(+), 381 deletions(-) create mode 100644 packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/v1/mod.rs diff --git a/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs index 4ec9a614071..217bc30d45a 100644 --- a/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs +++ b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs @@ -114,27 +114,6 @@ impl YesNoVotePollStoredInfo { } } - /// Rewrites the stored info in the version the platform version wants stored. - pub fn update_to_latest_version( - self, - platform_version: &PlatformVersion, - ) -> Result { - match platform_version - .dpp - .voting_versions - .yes_no_vote_poll_stored_info_version - { - 0 => match self { - YesNoVotePollStoredInfo::V0(_) => Ok(self), - }, - version => Err(ProtocolError::UnknownVersionMismatch { - method: "YesNoVotePollStoredInfo::update_to_latest_version".to_string(), - known_versions: vec![0], - received: version, - }), - } - } - /// Closes a started poll with its final tallies. `passed` is the poll's own verdict on /// them; the stored info records it without judging. pub fn finalize_vote_poll( diff --git a/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/mod.rs b/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/mod.rs index 4b6295a18ce..87d327779c1 100644 --- a/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/mod.rs +++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/mod.rs @@ -12,12 +12,6 @@ impl YesNoVoteGettersV0 for YesNoVote { } } - fn vote_poll_owned(self) -> YesNoVotePoll { - match self { - YesNoVote::V0(v0) => v0.vote_poll, - } - } - fn vote_choice(&self) -> YesNoAbstainVoteChoice { match self { YesNoVote::V0(v0) => v0.vote_choice, diff --git a/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/v0/mod.rs b/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/v0/mod.rs index 9ac03954776..74079ff46b4 100644 --- a/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/v0/mod.rs +++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/v0/mod.rs @@ -6,9 +6,6 @@ pub trait YesNoVoteGettersV0 { /// The poll answered. fn vote_poll(&self) -> &YesNoVotePoll; - /// The poll answered, owned. - fn vote_poll_owned(self) -> YesNoVotePoll; - /// The answer. fn vote_choice(&self) -> YesNoAbstainVoteChoice; } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs index 1cc8e87cd63..44e2142fc81 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs @@ -1,5 +1,6 @@ use crate::error::execution::ExecutionError; use crate::error::Error; +use crate::execution::platform_events::voting::clean_up_after_yes_no_vote_polls_end::FinishedYesNoVotePoll; use crate::execution::platform_events::voting::masternode_vote_strength; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; @@ -20,6 +21,8 @@ use dpp::voting::vote_polls::VotePoll; use drive::drive::votes::paths::vote_end_date_queries_tree_path_vec; use drive::drive::votes::resolved::vote_polls::resolve::VotePollResolver; use drive::drive::votes::resolved::vote_polls::{ResolvedVotePoll, ResolvedVotePollWithVotes}; +use drive::error::drive::DriveError; +use drive::error::Error as DriveCrateError; use drive::grovedb::TransactionArg; use drive::query::VotePollsByEndDateDriveQuery; use drive::util::common::encode::encode_u64; @@ -85,32 +88,31 @@ where // keep the loop below as it was. Reached from protocol version 15 only: at 14, which // also selects this generation, no yes/no poll is in the end date index, since opening // one (`Drive::open_yes_no_vote_poll`) is not active before 15, and the contested polls - // are the polls the index returned, in the same order. The yes/no polls this block - // closes keep their voters for the clean-up; the end date entries of yes/no polls an - // earlier entry already closed are only removed. - let mut finished_yes_no_vote_polls: Vec<(YesNoVotePoll, TimestampMillis, Vec)> = - vec![]; - let mut leftover_yes_no_end_date_entries: Vec<(Identifier, TimestampMillis)> = vec![]; + // are the polls the index returned, in the same order. Every due yes/no entry goes from + // the index; the polls this block closes also keep their voters for the clean-up, and a + // poll an earlier entry already closed has nothing more to clean. + let mut yes_no_end_date_entries: Vec<(Identifier, TimestampMillis)> = vec![]; + let mut finished_yes_no_vote_polls: Vec = vec![]; let mut contested_vote_polls_by_timestamp: BTreeMap> = BTreeMap::new(); for (end_date, vote_polls) in vote_polls_by_timestamp { for vote_poll in vote_polls { match vote_poll { VotePoll::YesNoVotePoll(yes_no_vote_poll) => { - match self.finish_yes_no_vote_poll( + let poll_id = yes_no_vote_poll.unique_id()?; + yes_no_end_date_entries.push((poll_id, end_date)); + if let Some(voters) = self.finish_yes_no_vote_poll( block_platform_state, block_info, &yes_no_vote_poll, transaction, platform_version, )? { - Some(voters) => finished_yes_no_vote_polls.push(( - yes_no_vote_poll, - end_date, + finished_yes_no_vote_polls.push(FinishedYesNoVotePoll { + poll_id, + prefunded_balance_id: yes_no_vote_poll.specialized_balance_id()?, voters, - )), - None => leftover_yes_no_end_date_entries - .push((yes_no_vote_poll.unique_id()?, end_date)), + }); } } contested_vote_poll => contested_vote_polls_by_timestamp @@ -275,11 +277,11 @@ where // Then the yes/no polls, in a batch of their own: its read of a shared end date sees // what the contested clean-up left there. - if !finished_yes_no_vote_polls.is_empty() || !leftover_yes_no_end_date_entries.is_empty() { + if !yes_no_end_date_entries.is_empty() { self.clean_up_after_yes_no_vote_polls_end( block_info, + &yes_no_end_date_entries, &finished_yes_no_vote_polls, - &leftover_yes_no_end_date_entries, transaction, platform_version, )?; @@ -294,7 +296,8 @@ where /// /// Returns `None` for a poll that is already finished: one listed under two end dates /// (two openings that could not see each other) closes at the first, and its later entry - /// is only removed. + /// is only removed. A due poll with no stored info is corrupted state: an opening writes it + /// with the end date entry. fn finish_yes_no_vote_poll( &self, block_platform_state: &PlatformState, @@ -303,15 +306,19 @@ where transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result>, Error> { - let tally = + let mut tally = self.tally_votes_for_yes_no_vote_poll(vote_poll, transaction, platform_version)?; - if tally - .stored_info - .as_ref() - .is_some_and(|stored_info| stored_info.result().is_some()) - { - return Ok(None); - } + let stored_info = match tally.stored_info.take() { + Some(stored_info) if stored_info.result().is_some() => return Ok(None), + Some(stored_info) => stored_info, + None => { + return Err(Error::Drive(DriveCrateError::Drive( + DriveError::CorruptedDriveState( + "an ended yes/no vote poll must have a record in the state".to_string(), + ), + ))) + } + }; // A minimum given as a share of the total is measured against the masternode list of // the closing block, each masternode weighed as its vote is. A fixed minimum reads no // total. @@ -330,6 +337,7 @@ where let result = self.keep_record_of_finished_yes_no_vote_poll( block_info, vote_poll, + stored_info, &tally, total_voting_power, transaction, diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs index 260a7480071..70f0446c930 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs @@ -6,27 +6,38 @@ use dpp::block::block_info::BlockInfo; use dpp::identifier::Identifier; use dpp::prelude::TimestampMillis; use dpp::version::PlatformVersion; -use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use drive::grovedb::TransactionArg; mod v0; +/// A yes/no poll the block closed, as its clean-up needs it. +pub(in crate::execution) struct FinishedYesNoVotePoll { + /// The poll's id, which keys its vote trees and its voters' entries in the identity votes + /// index. + pub poll_id: Identifier, + /// The id of the prefunded balance its votes were paid from + /// (`YesNoVotePoll::specialized_balance_id`). + pub prefunded_balance_id: Identifier, + /// The masternodes that voted in it. + pub voters: Vec, +} + impl Platform where C: CoreRPCLike, { - /// Cleans up after yes/no polls that ended, each given with its end date and its voters: - /// their entries in the end date index, their vote trees, the voters' entries in the - /// identity votes index, and what is left of the prefunded balance that paid for the votes, - /// which goes to the processing pool. The stored info stays as the record of the decision. + /// Cleans up after yes/no polls that ended: their entries in the end date index, the vote + /// trees of the polls this block closed, their voters' entries in the identity votes index, + /// and what is left of the prefunded balances that paid for the votes, which goes to the + /// processing pool. The stored info stays as the record of the decision. /// - /// `leftover_end_date_entries` are the end date entries, by poll id, of polls an earlier - /// entry already closed: only the entries go. + /// `end_date_entries` lists every due yes/no entry, including those of polls an earlier + /// entry already closed, of which only the entry goes. /// /// # Parameters /// - `block_info`: the block that closes the polls, whose epoch gets what is left of their funds. - /// - `vote_polls`: the polls that ended, each with its end date and its voters. - /// - `leftover_end_date_entries`: the end date entries, by poll id, of polls an earlier entry already closed. + /// - `end_date_entries`: the due yes/no entries of the end date index, by poll id. + /// - `finished_vote_polls`: the polls this block closed. /// - `transaction`: the transaction to read and write in. /// - `platform_version`: the platform version. /// @@ -35,8 +46,8 @@ where pub(in crate::execution) fn clean_up_after_yes_no_vote_polls_end( &self, block_info: &BlockInfo, - vote_polls: &[(YesNoVotePoll, TimestampMillis, Vec)], - leftover_end_date_entries: &[(Identifier, TimestampMillis)], + end_date_entries: &[(Identifier, TimestampMillis)], + finished_vote_polls: &[FinishedYesNoVotePoll], transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { @@ -48,8 +59,8 @@ where { 0 => self.clean_up_after_yes_no_vote_polls_end_v0( block_info, - vote_polls, - leftover_end_date_entries, + end_date_entries, + finished_vote_polls, transaction, platform_version, ), diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs index 0aa40e14c66..ae93c6d760f 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/v0/mod.rs @@ -1,3 +1,4 @@ +use super::FinishedYesNoVotePoll; use crate::error::Error; use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; @@ -5,10 +6,7 @@ use dpp::block::block_info::BlockInfo; use dpp::identifier::Identifier; use dpp::prelude::TimestampMillis; use dpp::version::PlatformVersion; -use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; -use dpp::ProtocolError; use drive::grovedb::TransactionArg; -use std::collections::BTreeMap; impl Platform where @@ -18,65 +16,52 @@ where pub(super) fn clean_up_after_yes_no_vote_polls_end_v0( &self, block_info: &BlockInfo, - vote_polls: &[(YesNoVotePoll, TimestampMillis, Vec)], - leftover_end_date_entries: &[(Identifier, TimestampMillis)], + end_date_entries: &[(Identifier, TimestampMillis)], + finished_vote_polls: &[FinishedYesNoVotePoll], transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { let mut operations = vec![]; - // Each poll's id, which its prefunded balance shares. - let vote_poll_ids = vote_polls - .iter() - .map(|(vote_poll, _, _)| vote_poll.unique_id()) - .collect::, ProtocolError>>()?; - // The polls' entries in the end date index, which contested polls share: an end date's // tree goes only once nothing of either kind is left under it. - let end_date_entries: Vec<(Identifier, TimestampMillis)> = vote_poll_ids - .iter() - .zip(vote_polls) - .map(|(vote_poll_id, (_, end_date, _))| (*vote_poll_id, *end_date)) - .chain(leftover_end_date_entries.iter().copied()) - .collect(); self.drive.remove_vote_poll_end_date_query_operations( - end_date_entries.as_slice(), + end_date_entries, &mut operations, transaction, platform_version, )?; - // The three vote trees of every poll. + // The three vote trees of every poll this block closed. + let vote_poll_ids: Vec = finished_vote_polls + .iter() + .map(|vote_poll| vote_poll.poll_id) + .collect(); self.drive.remove_yes_no_vote_poll_votes_operations( vote_poll_ids.as_slice(), &mut operations, platform_version, )?; - // Each voter's entries in the identity votes index. - let mut poll_ids_by_voter: BTreeMap<&Identifier, Vec<&Identifier>> = BTreeMap::new(); - for (vote_poll_id, (_, _, voters)) in vote_poll_ids.iter().zip(vote_polls) { - for voter in voters { - poll_ids_by_voter - .entry(voter) - .or_default() - .push(vote_poll_id); + // Each voter's entry in the identity votes index. + for vote_poll in finished_vote_polls { + for voter in &vote_poll.voters { + self.drive + .remove_yes_no_vote_references_given_by_identity_operations( + voter, + &[&vote_poll.poll_id], + &mut operations, + transaction, + platform_version, + )?; } } - for (voter, vote_poll_ids) in poll_ids_by_voter { - self.drive - .remove_yes_no_vote_references_given_by_identity_operations( - voter, - vote_poll_ids.as_slice(), - &mut operations, - transaction, - platform_version, - )?; - } // What is left of the prefunded balances goes to processing, as for contested polls. self.empty_vote_poll_prefunded_balances_operations_v1( - vote_poll_ids.iter().map(|vote_poll_id| Ok(*vote_poll_id)), + finished_vote_polls + .iter() + .map(|vote_poll| Ok(vote_poll.prefunded_balance_id)), block_info, &mut operations, transaction, diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs index 002b8ef0553..8f557756569 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs @@ -4,7 +4,9 @@ use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; use dpp::version::PlatformVersion; -use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollResult; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::{ + YesNoVotePollResult, YesNoVotePollStoredInfo, +}; use dpp::voting::vote_polls::yes_no_vote_poll::{VotingPower, YesNoVotePoll}; use drive::grovedb::TransactionArg; use drive::query::yes_no_vote_poll_state_query::YesNoVotePollState; @@ -22,17 +24,20 @@ where /// # Parameters /// - `block_info`: the block that closes the poll. /// - `vote_poll`: the poll that ended. - /// - `tally`: its tallies and stored info. + /// - `stored_info`: its stored info, started and not yet finished. + /// - `tally`: its tallies. /// - `total_voting_power`: the masternode list's total voting power in the closing block, 0 for a fixed minimum. /// - `transaction`: the transaction to read and write in. /// - `platform_version`: the platform version. /// /// # Returns /// The poll's result, as recorded. + #[allow(clippy::too_many_arguments)] pub(in crate::execution) fn keep_record_of_finished_yes_no_vote_poll( &self, block_info: &BlockInfo, vote_poll: &YesNoVotePoll, + stored_info: YesNoVotePollStoredInfo, tally: &YesNoVotePollState, total_voting_power: VotingPower, transaction: TransactionArg, @@ -47,6 +52,7 @@ where 0 => self.keep_record_of_finished_yes_no_vote_poll_v0( block_info, vote_poll, + stored_info, tally, total_voting_power, transaction, diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs index fb280c710b7..75334e34a16 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs @@ -3,10 +3,10 @@ use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; use dpp::version::PlatformVersion; -use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollResult; +use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::{ + YesNoVotePollResult, YesNoVotePollStoredInfo, +}; use dpp::voting::vote_polls::yes_no_vote_poll::{VotingPower, YesNoVotePoll}; -use drive::error::drive::DriveError; -use drive::error::Error as DriveCrateError; use drive::grovedb::TransactionArg; use drive::query::yes_no_vote_poll_state_query::YesNoVotePollState; @@ -14,27 +14,18 @@ impl Platform where C: CoreRPCLike, { + #[allow(clippy::too_many_arguments)] #[inline(always)] pub(super) fn keep_record_of_finished_yes_no_vote_poll_v0( &self, block_info: &BlockInfo, vote_poll: &YesNoVotePoll, + mut stored_info: YesNoVotePollStoredInfo, tally: &YesNoVotePollState, total_voting_power: VotingPower, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result { - let stored_info_from_disk = - tally - .stored_info - .clone() - .ok_or(Error::Drive(DriveCrateError::Drive( - DriveError::CorruptedDriveState( - "there must be a record of the yes/no vote poll in the state".to_string(), - ), - )))?; - // We perform an upgrade of the stored version just in case, most of the time this does nothing - let mut stored_info = stored_info_from_disk.update_to_latest_version(platform_version)?; let required_voting_power = vote_poll.required_voting_power(total_voting_power); let passed = vote_poll.passes( tally.yes_voting_power, diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs index 0eb2116990a..da01983e426 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs @@ -17,8 +17,12 @@ mod tally_votes_for_yes_no_vote_poll; /// as for contested polls. The strength of each yes/no vote and the masternode list total that a /// share minimum is measured against both come from here, so they always use the same weights. /// The contested vote strength of masternode vote `transform_into_action` v0 (protocol versions -/// 1 to 14, and every contested vote at 15) keeps its own copy of these weights, so changing -/// them here cannot change a shipped contest. +/// 1 to 14, and every contested vote at 15) keeps its own copy of these weights. +/// +/// Protocol version 15 reads these weights for every yes/no vote (`transform_into_action` v1) +/// and for the masternode list total a share minimum is judged against +/// (`check_for_ended_vote_polls` v1), so once it ships they must not change: other weights for a +/// later version go in a new helper that version selects. pub(in crate::execution) fn masternode_vote_strength(node_type: &MasternodeType) -> u8 { match node_type { MasternodeType::Regular => 1, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs index d0518cfedbe..b0b81b9611e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs @@ -78,21 +78,23 @@ fn end_due_vote_polls( .expect("expected to commit the block"); } -#[tokio::test] -async fn should_end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left() { - end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left(PlatformVersion::latest()).await; +/// The protocol versions the end of block tests run at, 14 first. Protocol version 14 runs the +/// same end of block generations on these paths as 15 today (`check_for_ended_vote_polls` v1, +/// `clean_up_after_vote_polls_end` v0); the run at 14 pins them there, so a later change to 15's +/// tables cannot leave 14 untested. +pub(super) fn end_of_block_test_versions() -> [&'static PlatformVersion; 2] { + [ + PlatformVersion::get(14).expect("expected protocol version 14"), + PlatformVersion::latest(), + ] } -/// Protocol version 14 runs the same end of block generations on this path as 15 today -/// (`check_for_ended_vote_polls` v1, `clean_up_after_vote_polls_end` v0); this run pins them to -/// 14, so a later change to 15's tables cannot leave 14 untested. #[tokio::test] -async fn should_end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left_at_protocol_version_14( -) { - end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left( - PlatformVersion::get(14).expect("expected protocol version 14"), - ) - .await; +async fn should_end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left() { + for platform_version in end_of_block_test_versions() { + end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left(platform_version) + .await; + } } async fn end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left( @@ -197,19 +199,9 @@ async fn end_vote_polls_across_end_dates_and_keep_the_end_date_of_the_one_left( #[tokio::test] async fn should_end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block() { - end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block(PlatformVersion::latest()).await; -} - -/// Protocol version 14 runs the same end of block generations on this path as 15 today -/// (`check_for_ended_vote_polls` v1, `clean_up_after_vote_polls_end` v0); this run pins them to -/// 14, so a later change to 15's tables cannot leave 14 untested. -#[tokio::test] -async fn should_end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block_at_protocol_version_14( -) { - end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block( - PlatformVersion::get(14).expect("expected protocol version 14"), - ) - .await; + for platform_version in end_of_block_test_versions() { + end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block(platform_version).await; + } } async fn end_every_vote_poll_of_an_end_date_at_the_limit_in_one_block( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs index 4d3fec87b3c..f625550a972 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs @@ -9,6 +9,7 @@ use crate::execution::validation::state_transition::state_transitions::tests::{ get_vote_states, perform_vote, perform_votes_multi, serialized_dpns_name_vote, setup_identity, setup_masternode_voting_identity, }; +use super::end_date_cleanup_tests::end_of_block_test_versions; use crate::platform_types::platform_state::PlatformStateV0Methods; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; @@ -745,20 +746,9 @@ async fn should_open_the_vote_window_when_a_second_contender_joins() { /// Both then end in the one block at the end of their vote window. #[tokio::test] async fn should_keep_a_shared_join_window_end_date_until_its_last_contest_moves() { - keep_a_shared_join_window_end_date_until_its_last_contest_moves(PlatformVersion::latest()) - .await; -} - -/// Protocol version 14 runs the same end of block generations on this path as 15 today -/// (`check_for_ended_vote_polls` v1, `clean_up_after_vote_polls_end` v0); this run pins them to -/// 14, so a later change to 15's tables cannot leave 14 untested. -#[tokio::test] -async fn should_keep_a_shared_join_window_end_date_until_its_last_contest_moves_at_protocol_version_14( -) { - keep_a_shared_join_window_end_date_until_its_last_contest_moves( - PlatformVersion::get(14).expect("expected protocol version 14"), - ) - .await; + for platform_version in end_of_block_test_versions() { + keep_a_shared_join_window_end_date_until_its_last_contest_moves(platform_version).await; + } } async fn keep_a_shared_join_window_end_date_until_its_last_contest_moves( @@ -860,18 +850,9 @@ async fn keep_a_shared_join_window_end_date_until_its_last_contest_moves( /// block; the block removes them first and ends the contest behind them. #[tokio::test] async fn should_end_a_contest_behind_end_dates_left_empty() { - end_a_contest_behind_end_dates_left_empty(PlatformVersion::latest()).await; -} - -/// Protocol version 14 runs the same end of block generations on this path as 15 today -/// (`check_for_ended_vote_polls` v1, `clean_up_after_vote_polls_end` v0); this run pins them to -/// 14, so a later change to 15's tables cannot leave 14 untested. -#[tokio::test] -async fn should_end_a_contest_behind_end_dates_left_empty_at_protocol_version_14() { - end_a_contest_behind_end_dates_left_empty( - PlatformVersion::get(14).expect("expected protocol version 14"), - ) - .await; + for platform_version in end_of_block_test_versions() { + end_a_contest_behind_end_dates_left_empty(platform_version).await; + } } async fn end_a_contest_behind_end_dates_left_empty(platform_version: &'static PlatformVersion) { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs index 4f84d62f1e3..81c4878e860 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v0/mod.rs @@ -60,7 +60,7 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV0 for Masternode &mut vec![], platform_version, )?; - match previous_vote_to_replace( + match previous_vote_to_replace_v0( self.pro_tx_hash(), maybe_existing_resource_vote_choice, &resource_vote.resource_vote_choice(), @@ -94,7 +94,7 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV0 for Masternode } } - let masternode = match voting_masternode(platform, self.pro_tx_hash()) { + let masternode = match voting_masternode_v0(platform, self.pro_tx_hash()) { Ok(masternode) => masternode, Err(error) => return Ok(ConsensusValidationResult::new_with_error(error)), }; @@ -125,8 +125,10 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV0 for Masternode /// /// Moved here unchanged from `transform_into_action_v0` (in place: the same checks, in the same /// order, with the same errors at every protocol version that selects it), so version 1 applies -/// the rule to yes/no votes too. -pub(super) fn previous_vote_to_replace( +/// the rule to yes/no votes too. Shipped with protocol version 1 (version 0 runs it for every +/// contested vote up to 14), so it must not change: a different rule for a later version is a +/// new helper. +pub(super) fn previous_vote_to_replace_v0( pro_tx_hash: Identifier, existing_vote: Option<(Choice, PreviousVoteCount)>, new_vote_choice: &Choice, @@ -170,8 +172,9 @@ pub(super) fn previous_vote_to_replace( /// The voting masternode in the block's masternode list, or the error refusing a vote from one /// that is not in it. Moved here unchanged from `transform_into_action_v0` (in place, as -/// [`previous_vote_to_replace`]) for version 1. -pub(super) fn voting_masternode<'a, C>( +/// [`previous_vote_to_replace_v0`]) for version 1. Shipped with protocol version 1, so it must +/// not change. +pub(super) fn voting_masternode_v0<'a, C>( platform: &PlatformRef<'a, C>, pro_tx_hash: Identifier, ) -> Result<&'a MasternodeListItem, ConsensusError> { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs index c0cc1488774..c99767032dd 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs @@ -1,7 +1,7 @@ use crate::error::Error; use crate::execution::platform_events::voting::masternode_vote_strength; use crate::execution::validation::state_transition::state_transitions::masternode_vote::transform_into_action::v0::{ - previous_vote_to_replace, voting_masternode, + previous_vote_to_replace_v0, voting_masternode_v0, MasternodeVoteStateTransitionTransformIntoActionValidationV0, }; use crate::platform_types::platform::PlatformRef; @@ -37,7 +37,7 @@ pub(in crate::execution::validation::state_transition::state_transitions::master impl MasternodeVoteStateTransitionTransformIntoActionValidationV1 for MasternodeVoteTransition { /// Version 1 (protocol version 15) also transforms a vote on a yes/no poll, with the rule - /// of a resource vote ([`previous_vote_to_replace`]): the same answer cannot be given twice, + /// of a resource vote ([`previous_vote_to_replace_v0`]): the same answer cannot be given twice, /// and a masternode may vote on a poll at most `votes_allowed_per_masternode` times, its /// first vote included. A resource vote on a contested poll transforms through version 0, /// unchanged. @@ -82,7 +82,7 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV1 for Masternode &mut vec![], platform_version, )?; - match previous_vote_to_replace( + match previous_vote_to_replace_v0( self.pro_tx_hash(), maybe_existing_vote_choice, &yes_no_vote.vote_choice(), @@ -94,15 +94,18 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV1 for Masternode } } - let masternode = match voting_masternode(platform, self.pro_tx_hash()) { + let masternode = match voting_masternode_v0(platform, self.pro_tx_hash()) { Ok(masternode) => masternode, Err(error) => return Ok(ConsensusValidationResult::new_with_error(error)), }; // A yes/no vote needs nothing from the state to be resolved; the previous answer found // above rides with it. - let mut resolved_vote = ResolvedYesNoVote::from_vote(yes_no_vote); - resolved_vote.previous_vote_choice_to_remove = previous_vote_choice_to_remove; + let resolved_vote = ResolvedYesNoVote { + vote_poll: yes_no_vote.vote_poll().clone(), + vote_choice: yes_no_vote.vote_choice(), + previous_vote_choice_to_remove, + }; let action = MasternodeVoteTransitionActionV0 { pro_tx_hash: self.pro_tx_hash(), voter_identity_id: self.voter_identity_id(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs index 9b1be0924f6..c0e1c6cc1ed 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs @@ -1,6 +1,7 @@ //! Votes on yes/no vote polls: the poll's life from opening to the record of its decision. use crate::execution::check_tx::CheckTxLevel; +use crate::execution::platform_events::voting::masternode_vote_strength; use crate::execution::validation::state_transition::state_transitions::tests::{ process_test_state_transition, processing_credits, setup_masternode_voting_identity, take_down_masternode_identities, @@ -546,10 +547,7 @@ async fn should_judge_a_share_minimum_against_the_masternode_list_at_close() { platform_state .full_masternode_list() .values() - .map(|masternode| match masternode.node_type { - MasternodeType::Evo => 4, - MasternodeType::Regular => 1, - }) + .map(|masternode| u32::from(masternode_vote_strength(&masternode.node_type))) .sum() }; assert_eq!( @@ -959,7 +957,12 @@ async fn should_move_a_changed_vote_and_cap_the_changes() { let state = poll_state(&platform, &vote_poll, platform_version); for choice in choices { let expected = if choice == *vote_choice { 4 } else { 0 }; - assert_eq!(state.voting_power(choice), expected, "vote {}", vote_number); + let voting_power = match choice { + YesNoAbstainVoteChoice::Yes => state.yes_voting_power, + YesNoAbstainVoteChoice::No => state.no_voting_power, + YesNoAbstainVoteChoice::Abstain => state.abstain_voting_power, + }; + assert_eq!(voting_power, expected, "vote {}", vote_number); } assert_eq!( identity_vote(&platform, pro_tx_hash, vote_poll_id, platform_version), diff --git a/packages/rs-drive/grovedb-structure.json b/packages/rs-drive/grovedb-structure.json index 48f8fbb8ce7..5615ae6ea1d 100644 --- a/packages/rs-drive/grovedb-structure.json +++ b/packages/rs-drive/grovedb-structure.json @@ -5153,7 +5153,7 @@ "since": 15, "presence": "always", "source": "packages/rs-drive/src/drive/votes/paths.rs", - "description": "One masternode's yes/no votes. Stays once its votes are gone, like its contested counterpart.", + "description": "One masternode's yes/no votes. Stays when its polls close, and is dropped when the masternode leaves the list.", "children": [ { "id": "votes.decisions.identity_votes.voter.vote", diff --git a/packages/rs-drive/src/drive/document/summable_off_count_counter.rs b/packages/rs-drive/src/drive/document/summable_off_count_counter.rs index 1af9797c60e..ebfae286010 100644 --- a/packages/rs-drive/src/drive/document/summable_off_count_counter.rs +++ b/packages/rs-drive/src/drive/document/summable_off_count_counter.rs @@ -30,7 +30,7 @@ use crate::error::fee::FeeError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::fees::op::LowLevelDriveOperation::CalculatedCostOperation; -use crate::util::grove_operations::pending_grove_operations::grove_operation_queued; +use crate::util::grove_operations::pending_grove_operations::pending_grove_operations; use crate::util::grove_operations::QueryTarget::QueryTargetValue; use crate::util::grove_operations::{ BatchDeleteUpTreeApplyType, BatchInsertApplyType, DirectQueryType, @@ -504,5 +504,6 @@ fn counter_queued( path: &KeyInfoPath, key: &KeyInfo, ) -> bool { - grove_operation_queued(operations, path, key) + pending_grove_operations(operations) + .any(|operation| operation.path == *path && operation.key.as_ref() == Some(key)) } diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs index d04b0e67801..831e07d98d7 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs @@ -12,8 +12,8 @@ use crate::util::grove_operations::{BatchDeleteApplyType, DirectQueryType}; use dpp::dashcore::Network; use dpp::prelude::{BlockHeight, Identifier}; use dpp::version::PlatformVersion; -use grovedb::batch::{QualifiedGroveDbOp, SubelementsDeletionBehavior}; use grovedb::{MaybeTree, TransactionArg, TreeType}; +use std::collections::BTreeSet; impl Drive { /// Version 1 (protocol version 15) removes the contested resource votes of the removed @@ -55,12 +55,15 @@ impl Drive { } let mut deletion_batch = vec![]; - for (identifier_bytes, votes_to_remove) in self.identity_vote_entries_of_identities_v0( + let identity_vote_entries = self.identity_vote_entries_of_identities_v0( vote_decisions_identity_votes_tree_path_vec(), identity_ids_as_byte_arrays.clone(), transaction, platform_version, - )? { + )?; + let identities_with_entries: BTreeSet> = + identity_vote_entries.keys().cloned().collect(); + for (identifier_bytes, votes_to_remove) in identity_vote_entries { for (vote_poll_id, vote_to_remove) in votes_to_remove { let storage_form = YesNoVoteReferenceStorageForm::deserialize(&vote_to_remove.into_item_bytes()?)?; @@ -84,27 +87,25 @@ impl Drive { // Then each removed masternode's tree in the identity votes index, emptied or not: the // close of a poll removes the poll's entries but keeps the tree, which no one else - // removes. It holds only items, so it is dropped flat with them (GroveDB's flat drop, - // which the grove version of protocol version 15 accepts in a batch). + // removes. It holds only items, so it is dropped flat with them. A masternode with + // entries has its tree; one without is read, since the close of its last poll leaves + // its tree empty. let identity_votes_path = vote_decisions_identity_votes_tree_path_vec(); for identifier_bytes in identity_ids_as_byte_arrays { - let has_identity_votes_tree = self.grove_has_raw( - identity_votes_path.as_slice().into(), - identifier_bytes.as_slice(), - DirectQueryType::StatefulDirectQuery, - transaction, - &mut vec![], - &platform_version.drive, - )?; + let has_identity_votes_tree = identities_with_entries.contains(&identifier_bytes) + || self.grove_has_raw( + identity_votes_path.as_slice().into(), + identifier_bytes.as_slice(), + DirectQueryType::StatefulDirectQuery, + transaction, + &mut vec![], + &platform_version.drive, + )?; if has_identity_votes_tree { - deletion_batch.push(LowLevelDriveOperation::GroveOperation( - QualifiedGroveDbOp::delete_tree_op( - identity_votes_path.clone(), - identifier_bytes, - TreeType::NormalTree, - SubelementsDeletionBehavior::DropFlat, - ) - .dont_check_for_backwards_references(), + deletion_batch.push(LowLevelDriveOperation::for_known_path_key_drop_flat_tree( + identity_votes_path.clone(), + identifier_bytes, + TreeType::NormalTree, )); } } diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs index d4d4945b99c..d636fa375fb 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs @@ -6,7 +6,6 @@ use crate::fees::op::LowLevelDriveOperation; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; -use grovedb::batch::{QualifiedGroveDbOp, SubelementsDeletionBehavior}; use grovedb::TreeType; impl Drive { @@ -21,16 +20,11 @@ impl Drive { for vote_choice in YesNoAbstainVoteChoice::ALL { // Each choice's sum tree holds only the votes, one sum item per voter, and a // finished poll is never opened again, so the tree is dropped whole without - // reading or deleting its votes one by one (GroveDB's flat drop, which the - // grove version of protocol version 15 accepts in a batch). - batch_operations.push(LowLevelDriveOperation::GroveOperation( - QualifiedGroveDbOp::delete_tree_op( - poll_path.clone(), - vec![vote_choice.to_tree_key()], - TreeType::SumTree, - SubelementsDeletionBehavior::DropFlat, - ) - .dont_check_for_backwards_references(), + // reading or deleting its votes one by one. + batch_operations.push(LowLevelDriveOperation::for_known_path_key_drop_flat_tree( + poll_path.clone(), + vec![vote_choice.to_tree_key()], + TreeType::SumTree, )); } } diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs index 1c596bd17b5..abc5167f2e6 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs @@ -49,7 +49,7 @@ impl Drive { .to_keys() .into_iter() .map(|voter| voter.try_into()) - .collect::, dpp::platform_value::Error>>()?; + .collect::, _>>()?; Ok(voters) } } diff --git a/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/mod.rs b/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/mod.rs index de9882409d2..bc487f0db04 100644 --- a/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use crate::drive::Drive; use grovedb::batch::KeyInfoPath; @@ -50,9 +51,20 @@ impl Drive { transaction, platform_version, ), + 1 => self.add_vote_poll_end_date_query_operations_v1( + creator_identity_id, + vote_poll, + end_date, + block_info, + estimated_costs_only_with_layer_info, + previous_batch_operations, + batch_operations, + transaction, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "add_vote_poll_end_date_query_operations".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/v1/mod.rs b/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/v1/mod.rs new file mode 100644 index 00000000000..4e1b020fc49 --- /dev/null +++ b/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/v1/mod.rs @@ -0,0 +1,107 @@ +use crate::drive::votes::paths::{ + vote_contested_resource_end_date_queries_at_time_tree_path_vec, + vote_end_date_queries_tree_path_vec, +}; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::common::encode::encode_u64; +use crate::util::grove_operations::pending_grove_operations::pending_grove_operations; +use crate::util::grove_operations::BatchInsertApplyType; +use crate::util::object_size_info::PathKeyElementInfo::PathKeyRefElement; +use crate::util::storage_flags::StorageFlags; +use dpp::block::block_info::BlockInfo; +use dpp::identity::TimestampMillis; +use dpp::serialization::PlatformSerializable; +use dpp::voting::vote_polls::VotePoll; +use grovedb::batch::key_info::KeyInfo; +use grovedb::batch::{GroveOp, KeyInfoPath}; +use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; +use platform_version::version::PlatformVersion; +use std::collections::HashMap; + +impl Drive { + /// Version 1 (protocol version 15): when the previous operations already queue an insert of + /// the end date's tree, whatever its storage flags, only the poll's entry is added and the + /// tree keeps the flags it was queued with. Otherwise it is version 0, whose tree insert skips + /// only an identical insert (flags included) and cancels a queued deletion of the tree. A + /// contest and a yes/no poll opening that share an end date in one batch queue the tree with + /// different flags, so version 0 queued it twice when the contest came second. + #[allow(clippy::too_many_arguments)] + pub(in crate::drive::votes::insert) fn add_vote_poll_end_date_query_operations_v1( + &self, + creator_identity_id: Option<[u8; 32]>, + vote_poll: VotePoll, + end_date: TimestampMillis, + block_info: &BlockInfo, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + previous_batch_operations: &mut Option<&mut Vec>, + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let end_date_tree_insert_queued = estimated_costs_only_with_layer_info.is_none() + && previous_batch_operations + .as_deref() + .is_some_and(|previous| end_date_tree_insert_queued(previous, end_date)); + if !end_date_tree_insert_queued { + return self.add_vote_poll_end_date_query_operations_v0( + creator_identity_id, + vote_poll, + end_date, + block_info, + estimated_costs_only_with_layer_info, + previous_batch_operations, + batch_operations, + transaction, + platform_version, + ); + } + + // The entry version 0 writes after the tree. + let storage_flags = creator_identity_id.map(|creator_identity_id| { + StorageFlags::new_single_epoch(block_info.epoch.index, Some(creator_identity_id)) + }); + let unique_id = vote_poll.unique_id()?; + let item = Element::Item( + vote_poll.serialize_to_bytes()?, + StorageFlags::map_to_some_element_flags(storage_flags.as_ref()), + ); + self.batch_insert_if_not_exists( + PathKeyRefElement::<0>(( + vote_contested_resource_end_date_queries_at_time_tree_path_vec(end_date), + unique_id.as_bytes(), + item, + )), + BatchInsertApplyType::StatefulBatchInsert, + transaction, + batch_operations, + &platform_version.drive, + )?; + Ok(()) + } +} + +/// Whether `previous_batch_operations` queue an insert of the tree of `end_date` in the end date +/// index. +fn end_date_tree_insert_queued( + previous_batch_operations: &[LowLevelDriveOperation], + end_date: TimestampMillis, +) -> bool { + let path = KeyInfoPath::from_known_owned_path(vote_end_date_queries_tree_path_vec()); + let key = KeyInfo::KnownKey(encode_u64(end_date)); + pending_grove_operations(previous_batch_operations).any(|operation| { + operation.path == path + && operation.key.as_ref() == Some(&key) + && matches!( + &operation.op, + GroveOp::InsertOrReplace { element } + | GroveOp::InsertOrReplaceDontCheckForBackwardsReferences { element } + | GroveOp::InsertWithKnownToNotAlreadyExist { element } + | GroveOp::InsertIfNotExists { element, .. } + if element.is_any_tree() + ) + }) +} diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs index 17e3db18eaf..b6d3b84f03d 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs @@ -17,8 +17,8 @@ impl Drive { /// `specialized_balance_id`. /// /// Refuses parameters no poll can run with, an end date that is not after the opening block - /// (or past `i64::MAX`), and a poll that already exists. Not active before protocol version - /// 15. + /// or is further from it than `max_yes_no_vote_poll_duration_ms`, and a poll that already + /// exists. Not active before protocol version 15. /// /// # Parameters /// - `vote_poll`: the poll to open. @@ -67,12 +67,13 @@ impl Drive { /// /// Operations already queued in the same batch, earlier openings and contests included, /// go in `previous_batch_operations`, with `batch_operations` fresh, as for document - /// operations: that is how an opening refuses a poll the batch already opens and queues a - /// tree the batch already holds once. A contest that shares the poll's end date must come - /// before the opening: the contested end date insert skips only an identical insert, so a - /// contest after it would queue the end date's tree again. Two openings converted with no - /// view of each other can list one poll under two end dates; it then closes at the first and - /// its later entry is only removed. + /// operations: that is how an opening refuses a poll the batch already opens, queues a tree + /// the batch already holds once (an end date's tree a contest queued keeps its contender's + /// storage flags) and cancels a queued deletion of its end date's tree (a contest leaving its + /// join-window end date). Converted without them, an opening sees none of this: a deletion + /// of its end date's tree queued earlier in the batch then fails the batch, as it would for + /// a contest, and two openings with no view of each other can list one poll under two end + /// dates; it then closes at the first and its later entry is only removed. /// /// They are stateful and have no estimated-costs path: a poll opens as a system action, /// outside any fee estimate. A caller that runs them inside a fee estimate must first give @@ -132,11 +133,12 @@ impl Drive { #[cfg(test)] mod tests { use crate::drive::votes::paths::{ - vote_decisions_tree_path, vote_end_date_queries_tree_path, ACTIVE_POLLS_TREE_KEY, + vote_contested_resource_end_date_queries_at_time_tree_path_vec, vote_decisions_tree_path, + vote_end_date_queries_tree_path, vote_root_path, ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY, }; use crate::util::common::encode::encode_u64; - use crate::util::grove_operations::DirectQueryType; + use crate::util::grove_operations::{BatchDeleteUpTreeApplyType, DirectQueryType}; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; use crate::util::test_helpers::test_yes_no_vote_poll as poll; use crate::util::test_helpers::vote_poll_end_dates; @@ -145,6 +147,8 @@ mod tests { use dpp::version::PlatformVersion; use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; use dpp::voting::vote_polls::VotePoll; + use grovedb::batch::KeyInfoPath; + use grovedb::MaybeTree; #[test] fn should_refuse_to_open_a_poll_a_batch_already_opens() { @@ -241,15 +245,38 @@ mod tests { time_ms: 1_000, ..Default::default() }; - // The last two would sort before every block time in the end date index. - for end_date in [999, 1_000, i64::MAX as u64 + 1, u64::MAX] { + let max_duration = platform_version + .system_limits + .max_yes_no_vote_poll_duration_ms; + // One that runs too long is refused too; the last two would also sort before every + // block time in the end date index. + for end_date in [ + 999, + 1_000, + 1_000 + max_duration + 1, + i64::MAX as u64 + 1, + u64::MAX, + ] { drive .open_yes_no_vote_poll(&poll(), end_date, &block_info, None, platform_version) - .expect_err("expected a poll that ends at once to be refused"); + .expect_err("expected a poll that ends at once or too late to be refused"); } drive .open_yes_no_vote_poll(&poll(), 1_001, &block_info, None, platform_version) .expect("expected a poll ending after the opening block to open"); + let mut longest_poll = poll(); + longest_poll + .resource_path + .push(BinaryData::new(b"longest".to_vec())); + drive + .open_yes_no_vote_poll( + &longest_poll, + 1_000 + max_duration, + &block_info, + None, + platform_version, + ) + .expect("expected a poll running exactly the longest duration to open"); } #[test] @@ -371,4 +398,145 @@ mod tests { .expect("expected the end date's tree"); assert!(end_date_tree.get_flags().is_some()); } + + #[test] + fn should_cancel_a_queued_deletion_of_the_end_date_tree_when_opening_a_poll() { + // A contest whose join-window entry is the only one at its end date, in state. A second + // contender moving the contest off that end date queues the entry's deletion and, with + // it, the deletion of the emptied end date tree; a poll opened later in the batch at that + // end date must keep the tree. + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let contested_poll = VotePoll::ContestedDocumentResourceVotePoll( + ContestedDocumentResourceVotePoll::default(), + ); + let mut contest_operations = vec![]; + drive + .add_vote_poll_end_date_query_operations( + Some([7; 32]), + contested_poll.clone(), + 1_000, + &BlockInfo::default(), + &mut None, + &mut None, + &mut contest_operations, + None, + platform_version, + ) + .expect("expected the contest's end date entry"); + drive + .apply_batch_low_level_drive_operations( + None, + None, + contest_operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected the contest's end date entry to apply"); + + let mut batch_operations = vec![]; + drive + .batch_delete_up_tree_while_empty( + KeyInfoPath::from_known_owned_path( + vote_contested_resource_end_date_queries_at_time_tree_path_vec(1_000), + ), + contested_poll.unique_id().expect("id").as_slice(), + Some(vote_root_path().len() as u16), + BatchDeleteUpTreeApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + }, + None, + &None, + &mut batch_operations, + &platform_version.drive, + ) + .expect("expected the contest's entry and end date tree to be queued for deletion"); + let mut opening_operations = vec![]; + drive + .open_yes_no_vote_poll_operations( + &poll(), + 1_000, + &BlockInfo::default(), + &mut Some(&mut batch_operations), + &mut opening_operations, + None, + platform_version, + ) + .expect("expected to open the poll"); + batch_operations.append(&mut opening_operations); + drive + .apply_batch_low_level_drive_operations( + None, + None, + batch_operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected the batch to apply"); + + let end_dates = vote_poll_end_dates(&drive, platform_version); + assert_eq!( + end_dates[&1_000], + [poll().unique_id().expect("id")].into_iter().collect() + ); + } + + #[test] + fn should_queue_the_end_date_tree_once_when_a_contest_follows_an_opening() { + // The opening queues the end date's tree without storage flags, the contest after it + // would queue it with its contender's: the contest adds only its entry. The test drive + // verifies batch consistency, which refuses a tree queued twice. + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let contested_poll = VotePoll::ContestedDocumentResourceVotePoll( + ContestedDocumentResourceVotePoll::default(), + ); + let mut batch_operations = vec![]; + drive + .open_yes_no_vote_poll_operations( + &poll(), + 1_000, + &BlockInfo::default(), + &mut None, + &mut batch_operations, + None, + platform_version, + ) + .expect("expected to open the poll"); + let mut contest_operations = vec![]; + drive + .add_vote_poll_end_date_query_operations( + Some([7; 32]), + contested_poll.clone(), + 1_000, + &BlockInfo::default(), + &mut None, + &mut Some(&mut batch_operations), + &mut contest_operations, + None, + platform_version, + ) + .expect("expected the contest's end date entry"); + batch_operations.append(&mut contest_operations); + drive + .apply_batch_low_level_drive_operations( + None, + None, + batch_operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected the batch to apply"); + + let end_dates = vote_poll_end_dates(&drive, platform_version); + assert_eq!( + end_dates[&1_000], + [ + contested_poll.unique_id().expect("id"), + poll().unique_id().expect("id") + ] + .into_iter() + .collect() + ); + } } diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs index 9989837ad59..f756d79b603 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs @@ -1,28 +1,22 @@ use crate::drive::votes::paths::{ - vote_contested_resource_end_date_queries_at_time_tree_path_vec, - vote_decisions_active_polls_tree_path_vec, vote_decisions_tree_path_vec, - vote_end_date_queries_tree_path_vec, YesNoVotePollPaths, VOTE_DECISIONS_SUBTREE_KEYS, + vote_decisions_active_polls_tree_path_vec, vote_decisions_tree_path_vec, YesNoVotePollPaths, + VOTE_DECISIONS_SUBTREE_KEYS, }; use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use crate::util::common::encode::encode_u64; -use crate::util::grove_operations::pending_grove_operations::grove_operation_queued; -use crate::util::grove_operations::{BatchInsertApplyType, BatchInsertTreeApplyType}; -use crate::util::object_size_info::{DriveKeyInfo, PathKeyElementInfo, PathKeyInfo}; +use crate::util::grove_operations::BatchInsertTreeApplyType; +use crate::util::object_size_info::{DriveKeyInfo, PathKeyInfo}; use dpp::block::block_info::BlockInfo; use dpp::prelude::TimestampMillis; -use dpp::serialization::PlatformSerializable; use dpp::version::PlatformVersion; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStoredInfo; use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; use dpp::voting::vote_polls::VotePoll; use dpp::ProtocolError; -use grovedb::batch::key_info::KeyInfo; -use grovedb::batch::KeyInfoPath; -use grovedb::{Element, TransactionArg, TreeType}; +use grovedb::{TransactionArg, TreeType}; impl Drive { pub(super) fn open_yes_no_vote_poll_v0( @@ -65,16 +59,24 @@ impl Drive { ) -> Result<(), Error> { vote_poll.validate_parameters(platform_version)?; // A poll that ends in the block that opens it, or earlier, would close at the end of - // this block with no vote cast. So would one ending after `i64::MAX`: the end date index - // keys an end date with its top bit flipped (`encode_u64`), which sorts such an end date + // this block with no vote cast. One that runs longer than the system limit would hold + // its fund and trees for as long. The limit also keeps the end date below `i64::MAX`, + // past which the end date index (which flips its top bit, `encode_u64`) would sort it // before every block time. - if end_date <= block_info.time_ms || end_date > i64::MAX as TimestampMillis { + let max_duration = platform_version + .system_limits + .max_yes_no_vote_poll_duration_ms; + if end_date <= block_info.time_ms + || end_date - block_info.time_ms > max_duration + || end_date > i64::MAX as TimestampMillis + { return Err(Error::Protocol(Box::new(ProtocolError::VoteError( format!( - "yes/no vote poll {} must end after the block that opens it (at {}) and at \ - most at {}, not at {}", + "yes/no vote poll {} must end after the block that opens it (at {}), at \ + most {} ms later and at most at {}, not at {}", vote_poll, block_info.time_ms, + max_duration, i64::MAX, end_date ), @@ -146,49 +148,22 @@ impl Drive { )?; } - // The end date index closes the poll when its time comes: the end date's tree, then the - // poll's entry under it, the entry `add_vote_poll_end_date_query_operations` writes for - // a contest. Nobody is refunded for what an opening adds: the poll is a system action, - // so it carries no storage flags. A contest among the previous operations may already - // queue the end date's tree with its contender's storage flags; the tree keeps them and - // this opening adds only its entry, where the empty-tree insert, which skips only an - // identical insert, would queue the tree a second time. - let end_date_tree_path = vote_end_date_queries_tree_path_vec(); - let end_date_key = encode_u64(end_date); - let end_date_tree_queued = previous_batch_operations - .as_deref() - .is_some_and(|previous| { - grove_operation_queued( - previous, - &KeyInfoPath::from_known_owned_path(end_date_tree_path.clone()), - &KeyInfo::KnownKey(end_date_key.clone()), - ) - }); - if !end_date_tree_queued { - self.batch_insert_empty_tree_if_not_exists( - PathKeyInfo::PathKey::<0>((end_date_tree_path, end_date_key)), - TreeType::NormalTree, - None, - BatchInsertTreeApplyType::StatefulBatchInsertTree, - transaction, - previous_batch_operations, - batch_operations, - &platform_version.drive, - )?; - } - self.batch_insert_if_not_exists( - PathKeyElementInfo::PathKeyRefElement::<0>(( - vote_contested_resource_end_date_queries_at_time_tree_path_vec(end_date), - poll_id.as_bytes(), - Element::Item( - VotePoll::YesNoVotePoll(vote_poll.clone()).serialize_to_bytes()?, - None, - ), - )), - BatchInsertApplyType::StatefulBatchInsert, - transaction, + // The end date index closes the poll when its time comes: the entry a contest gets, under + // the end date's tree. Nobody is refunded for what an opening adds: the poll is a system + // action, so it carries no storage flags. A contest among the previous operations may + // already queue the end date's tree with its contender's flags, which the tree keeps, or + // queue its deletion (a contest leaving its join-window end date), which the insert + // cancels. + self.add_vote_poll_end_date_query_operations( + None, + VotePoll::YesNoVotePoll(vote_poll.clone()), + end_date, + block_info, + &mut None, + previous_batch_operations, batch_operations, - &platform_version.drive, + transaction, + platform_version, )?; Ok(()) } diff --git a/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs b/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs index 8a86bca0181..48024bd78ae 100644 --- a/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs +++ b/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs @@ -1,7 +1,6 @@ use crate::drive::votes::PreviousVoteCount; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_polls::yes_no_vote_poll::YesNoVotePoll; -use dpp::voting::votes::yes_no_vote::accessors::v0::YesNoVoteGettersV0; use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; use dpp::voting::votes::yes_no_vote::YesNoVote; @@ -19,17 +18,6 @@ pub struct ResolvedYesNoVote { pub previous_vote_choice_to_remove: Option<(YesNoAbstainVoteChoice, PreviousVoteCount)>, } -impl ResolvedYesNoVote { - /// The vote as it came in, with no previous answer attached. - pub fn from_vote(vote: &YesNoVote) -> Self { - ResolvedYesNoVote { - vote_poll: vote.vote_poll().clone(), - vote_choice: vote.vote_choice(), - previous_vote_choice_to_remove: None, - } - } -} - impl From for YesNoVote { fn from(value: ResolvedYesNoVote) -> Self { YesNoVote::V0(YesNoVoteV0 { diff --git a/packages/rs-drive/src/drive/votes/structure.rs b/packages/rs-drive/src/drive/votes/structure.rs index ca149407275..3d517c22794 100644 --- a/packages/rs-drive/src/drive/votes/structure.rs +++ b/packages/rs-drive/src/drive/votes/structure.rs @@ -445,8 +445,8 @@ fn decision_identity_votes() -> StructureNode { ) .kind(ElementKind::Tree) .describe( - "One masternode's yes/no votes. Stays once its \ - votes are gone, like its contested counterpart.", + "One masternode's yes/no votes. Stays when its polls \ + close, and is dropped when the masternode leaves the list.", ) .child( StructureNode::dynamic( diff --git a/packages/rs-drive/src/fees/op.rs b/packages/rs-drive/src/fees/op.rs index 5550da343be..3793a5ede9b 100644 --- a/packages/rs-drive/src/fees/op.rs +++ b/packages/rs-drive/src/fees/op.rs @@ -9,6 +9,7 @@ use enum_map::Enum; use grovedb::batch::key_info::KeyInfo; use grovedb::batch::GroveOp; use grovedb::batch::KeyInfoPath; +use grovedb::batch::SubelementsDeletionBehavior; use grovedb::element::reference_path::ReferencePathType; use grovedb::element::IndexAxis; use grovedb::element::MaxReferenceHop; @@ -710,6 +711,26 @@ impl LowLevelDriveOperation { .collect() } + /// Sets `GroveOperation` for dropping the tree at the given path and key with everything in + /// it, without reading or deleting its elements one by one: GroveDB's flat drop + /// (`SubelementsDeletionBehavior::DropFlat`), which a batch accepts from `GROVE_V4`. Only for a + /// tree of `tree_type` that holds no subtree and whose elements no reference points at. + pub fn for_known_path_key_drop_flat_tree( + path: Vec>, + key: Vec, + tree_type: TreeType, + ) -> Self { + GroveOperation( + QualifiedGroveDbOp::delete_tree_op( + path, + key, + tree_type, + SubelementsDeletionBehavior::DropFlat, + ) + .dont_check_for_backwards_references(), + ) + } + /// Sets `GroveOperation` for inserting an empty tree at the given path and key pub fn for_known_path_key_empty_tree( path: Vec>, diff --git a/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs index d3dc7fca5cb..1d554a3c110 100644 --- a/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs +++ b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "server")] -use crate::drive::votes::paths::vote_decisions_active_polls_tree_path; use crate::drive::votes::paths::{YesNoVotePollPaths, YES_NO_VOTE_POLL_STORED_INFO_KEY}; use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait; #[cfg(feature = "server")] @@ -8,9 +6,9 @@ use crate::error::drive::DriveError; use crate::error::Error; #[cfg(feature = "server")] use crate::fees::op::LowLevelDriveOperation; -use crate::query::Query; #[cfg(feature = "server")] -use crate::util::grove_operations::DirectQueryType; +use crate::query::GroveError; +use crate::query::Query; use dpp::serialization::PlatformDeserializableUntrusted; use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStoredInfo; @@ -37,17 +35,6 @@ pub struct YesNoVotePollState { pub abstain_voting_power: VotingPower, } -impl YesNoVotePollState { - /// The voting power cast for a choice. - pub fn voting_power(&self, vote_choice: YesNoAbstainVoteChoice) -> VotingPower { - match vote_choice { - YesNoAbstainVoteChoice::Yes => self.yes_voting_power, - YesNoAbstainVoteChoice::No => self.no_voting_power, - YesNoAbstainVoteChoice::Abstain => self.abstain_voting_power, - } - } -} - /// The query for the state of a yes/no vote poll: its stored info item and its three vote sum /// trees, whose sums are the tallies. #[derive(Debug, PartialEq, Clone)] @@ -151,31 +138,29 @@ impl YesNoVotePollStateDriveQuery { drive_operations: &mut Vec, platform_version: &PlatformVersion, ) -> Result { - // A poll that was never opened has no tree; the proof path reports the same as an - // empty state, so does this one. - let poll_id = self.vote_poll.unique_id()?; - let exists = drive.grove_has_raw( - (&vote_decisions_active_polls_tree_path()).into(), - poll_id.as_bytes(), - DirectQueryType::StatefulDirectQuery, + // A poll that was never opened has no tree (a chain without the decisions trees has no + // parent either); the proof path reports the same as an empty state, so does this one. + let path_query = self.construct_path_query()?; + let key_elements = match drive.grove_get_raw_path_query( + &path_query, transaction, + QueryResultType::QueryKeyElementPairResultType, drive_operations, &platform_version.drive, - )?; - if !exists { - return Ok(YesNoVotePollState::default()); - } - let path_query = self.construct_path_query()?; - let key_elements = drive - .grove_get_raw_path_query( - &path_query, - transaction, - QueryResultType::QueryKeyElementPairResultType, - drive_operations, - &platform_version.drive, - )? - .0 - .to_key_elements(); + ) { + Ok((query_result_elements, _)) => query_result_elements.to_key_elements(), + Err(Error::GroveDB(error)) + if matches!( + error.as_ref(), + GroveError::PathKeyNotFound(_) + | GroveError::PathNotFound(_) + | GroveError::PathParentLayerNotFound(_) + ) => + { + return Ok(YesNoVotePollState::default()); + } + Err(error) => return Err(error), + }; Self::state_from_key_elements(key_elements) } } diff --git a/packages/rs-drive/src/util/grove_operations/pending_grove_operations/mod.rs b/packages/rs-drive/src/util/grove_operations/pending_grove_operations/mod.rs index 935b55cc56d..025cd325856 100644 --- a/packages/rs-drive/src/util/grove_operations/pending_grove_operations/mod.rs +++ b/packages/rs-drive/src/util/grove_operations/pending_grove_operations/mod.rs @@ -9,8 +9,7 @@ //! batch for every delete. use crate::fees::op::LowLevelDriveOperation; -use grovedb::batch::key_info::KeyInfo; -use grovedb::batch::{KeyInfoPath, QualifiedGroveDbOp}; +use grovedb::batch::QualifiedGroveDbOp; #[cfg(test)] use std::cell::Cell; @@ -42,16 +41,5 @@ pub(crate) fn pending_grove_operations( }) } -/// Whether `drive_operations` already queue a grove operation, of any kind, on `key` under -/// `path`. -pub(crate) fn grove_operation_queued( - drive_operations: &[LowLevelDriveOperation], - path: &KeyInfoPath, - key: &KeyInfo, -) -> bool { - pending_grove_operations(drive_operations) - .any(|operation| operation.path == *path && operation.key.as_ref() == Some(key)) -} - #[cfg(test)] mod tests; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs index eba643f39bc..f66ac0dcd88 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs @@ -78,5 +78,7 @@ pub struct DriveVoteInsertMethodVersions { pub struct DriveVoteContestedResourceInsertMethodVersions { pub register_contested_resource_identity_vote: FeatureVersion, pub insert_stored_info_for_contested_resource_vote_poll: FeatureVersion, + /// Adds a poll to the end date index. Version 1 (protocol version 15) adds only the entry + /// when the batch already queues the end date's tree with other storage flags. pub add_vote_poll_end_date_query_operations: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs index 9539131ba0f..6a493f3dd40 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs @@ -1,7 +1,7 @@ use crate::version::drive_versions::drive_vote_method_versions::v3::DRIVE_VOTE_METHOD_VERSIONS_V3; use crate::version::drive_versions::drive_vote_method_versions::{ - DriveVoteCleanupMethodVersions, DriveVoteInsertMethodVersions, DriveVoteMethodVersions, - DriveVoteYesNoMethodVersions, + DriveVoteCleanupMethodVersions, DriveVoteContestedResourceInsertMethodVersions, + DriveVoteInsertMethodVersions, DriveVoteMethodVersions, DriveVoteYesNoMethodVersions, }; /// Drive vote method versions 4. Introduced in protocol version 15 with the yes/no poll @@ -13,12 +13,19 @@ use crate::version::drive_versions::drive_vote_method_versions::{ /// that left the list. /// * `yes_no.open_yes_no_vote_poll` is `Some(0)`: yes/no polls can be opened from this version /// (`None` in every earlier table). +/// * `contested_resource_insert.add_vote_poll_end_date_query_operations` 1 adds only the entry +/// when the batch already queues an insert of the end date's tree with other storage flags (a +/// contest and a yes/no poll opening sharing an end date), where 0 queued the tree twice. /// /// Everything else matches `DRIVE_VOTE_METHOD_VERSIONS_V3`. pub const DRIVE_VOTE_METHOD_VERSIONS_V4: DriveVoteMethodVersions = DriveVoteMethodVersions { insert: DriveVoteInsertMethodVersions { register_identity_vote: 1, }, + contested_resource_insert: DriveVoteContestedResourceInsertMethodVersions { + add_vote_poll_end_date_query_operations: 1, + ..DRIVE_VOTE_METHOD_VERSIONS_V3.contested_resource_insert + }, cleanup: DriveVoteCleanupMethodVersions { remove_all_votes_given_by_identities: 1, ..DRIVE_VOTE_METHOD_VERSIONS_V3.cleanup diff --git a/packages/rs-platform-version/src/version/drive_versions/v10.rs b/packages/rs-platform-version/src/version/drive_versions/v10.rs index 3d6f0987353..84d171efc02 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -8,7 +8,10 @@ use crate::version::drive_versions::{DriveMethodVersions, DriveVersion}; /// * **Yes/no vote polls**: `DRIVE_VOTE_METHOD_VERSIONS_V4` bumps /// `insert.register_identity_vote` to 1, which registers a vote of either poll kind, and /// `cleanup.remove_all_votes_given_by_identities` to 1, which also removes the yes/no votes of -/// a masternode that left the list. +/// a masternode that left the list. It sets `yes_no.open_yes_no_vote_poll` to `Some(0)`, so +/// yes/no polls can be opened (`None`, not active, in every earlier table), and bumps +/// `contested_resource_insert.add_vote_poll_end_date_query_operations` to 1, which adds only +/// the entry when the batch already queues the end date's tree with other storage flags. /// /// Everything else matches `DRIVE_VERSION_V9`. pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { diff --git a/packages/rs-platform-version/src/version/mocks/v2_test.rs b/packages/rs-platform-version/src/version/mocks/v2_test.rs index 7136f55fa79..1a36978efa8 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -619,6 +619,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { max_yes_no_vote_poll_resource_path_bytes: 1024, max_yes_no_vote_poll_minimum_voting_power: 2000, max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, + max_yes_no_vote_poll_duration_ms: 2_419_200_000, // four weeks max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_mainnet_contract_moderation_election_window_seconds: 86_400, diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index b6cb4fa765f..7172898360d 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -251,6 +251,10 @@ pub struct SystemLimits { /// The largest share of the total voting power, in percent, a yes/no vote poll's minimum may /// require, read by `YesNoVotePoll::validate_parameters` (protocol version 15). pub max_yes_no_vote_poll_minimum_voting_power_percent_of_total: u8, + /// The longest a yes/no vote poll may run, in milliseconds from the block that opens it to + /// its end date: four weeks, the longest election window a contract may declare. Read by + /// `Drive::open_yes_no_vote_poll` (protocol version 15). + pub max_yes_no_vote_poll_duration_ms: u64, /// Maximum number of warnings one identity may carry on a contract's warning list at a /// time: a warn that would exceed it is refused until the warnings are cleared. Read by /// the `ContractUserModeration` state validation v0 (protocol version 14) and never diff --git a/packages/rs-platform-version/src/version/system_limits/v1.rs b/packages/rs-platform-version/src/version/system_limits/v1.rs index 77979c138db..b577e06817b 100644 --- a/packages/rs-platform-version/src/version/system_limits/v1.rs +++ b/packages/rs-platform-version/src/version/system_limits/v1.rs @@ -65,6 +65,7 @@ pub const SYSTEM_LIMITS_V1: SystemLimits = SystemLimits { max_yes_no_vote_poll_resource_path_bytes: 1024, max_yes_no_vote_poll_minimum_voting_power: 2000, max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, + max_yes_no_vote_poll_duration_ms: 2_419_200_000, // four weeks max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_mainnet_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/system_limits/v2.rs b/packages/rs-platform-version/src/version/system_limits/v2.rs index cf3220caa77..081496121e9 100644 --- a/packages/rs-platform-version/src/version/system_limits/v2.rs +++ b/packages/rs-platform-version/src/version/system_limits/v2.rs @@ -46,6 +46,7 @@ pub const SYSTEM_LIMITS_V2: SystemLimits = SystemLimits { max_yes_no_vote_poll_resource_path_bytes: 1024, max_yes_no_vote_poll_minimum_voting_power: 2000, max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, + max_yes_no_vote_poll_duration_ms: 2_419_200_000, // four weeks max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_mainnet_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/system_limits/v3.rs b/packages/rs-platform-version/src/version/system_limits/v3.rs index dc9e66ed2d1..e6778105f0d 100644 --- a/packages/rs-platform-version/src/version/system_limits/v3.rs +++ b/packages/rs-platform-version/src/version/system_limits/v3.rs @@ -48,6 +48,7 @@ pub const SYSTEM_LIMITS_V3: SystemLimits = SystemLimits { max_yes_no_vote_poll_resource_path_bytes: 1024, max_yes_no_vote_poll_minimum_voting_power: 2000, max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, + max_yes_no_vote_poll_duration_ms: 2_419_200_000, // four weeks max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_mainnet_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/system_limits/v4.rs b/packages/rs-platform-version/src/version/system_limits/v4.rs index b9abfbb8c7b..6114742dbe4 100644 --- a/packages/rs-platform-version/src/version/system_limits/v4.rs +++ b/packages/rs-platform-version/src/version/system_limits/v4.rs @@ -169,6 +169,7 @@ pub const SYSTEM_LIMITS_V4: SystemLimits = SystemLimits { max_yes_no_vote_poll_resource_path_bytes: 1024, max_yes_no_vote_poll_minimum_voting_power: 2000, max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, + max_yes_no_vote_poll_duration_ms: 2_419_200_000, // four weeks max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_mainnet_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 51f856219a1..9e88de15173 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -80,11 +80,18 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// (`create_initial_state_structure` 4 behind `>= 15`) and /// `transition_to_version_15` create. Masternode vote `transform_into_action` /// 1 (`DRIVE_ABCI_VALIDATION_VERSIONS_V11`) looks up the previous answer, and -/// `DRIVE_VERSION_V10` registers and sweeps the votes. Nothing opens a yes/no -/// poll yet, so at this version every yes/no vote is refused for want of a poll. +/// `DRIVE_VERSION_V10` registers and sweeps the votes. Opening a yes/no poll +/// (`Drive::open_yes_no_vote_poll`, `open_yes_no_vote_poll` `Some(0)`) is active +/// from this version, for an end date at most `max_yes_no_vote_poll_duration_ms` +/// (four weeks, a `SystemLimits` field read only by the opener) after the +/// opening block, and its end date entry goes through +/// `add_vote_poll_end_date_query_operations` 1, which adds only the entry when +/// the batch already queues the end date's tree with other storage flags. +/// Nothing calls the opener yet, so at this version every yes/no vote is +/// refused for want of a poll. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, - drive: DRIVE_VERSION_V10, // changed: vote method versions v4: register_identity_vote 1 registers a yes/no vote and remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list + drive: DRIVE_VERSION_V10, // changed: vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, open_yes_no_vote_poll Some(0) makes opening a yes/no poll active and add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock diff --git a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs index c586f32e20b..8a721a740d9 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs @@ -460,17 +460,19 @@ impl Sdk { break; }; let polls_in_page: usize = result.0.iter().map(|(_, polls)| polls.len()).sum(); + let single_end_date = result.0.len() == 1; record_dpns_contests(&mut name_to_end_time, dpns_contract_id, result); - if current_start_time == Some((last_ts, true)) && polls_in_page >= query_limit as usize - { + if single_end_date && polls_in_page >= query_limit as usize { // This end date alone filled the page, so the limit may have cut it, and a - // proved query cannot continue within one end date: read it again at the - // largest limit, or refuse rather than leave contests out. + // proved query cannot continue within one end date: read it again from that end + // date at the largest limit (nothing before it in range held a poll), or refuse + // rather than leave contests out. if query_limit >= DEFAULT_QUERY_LIMIT { return Err(too_many_vote_polls_at_end_date(last_ts)); } query_limit = DEFAULT_QUERY_LIMIT; + current_start_time = Some((last_ts, true)); continue; } diff --git a/packages/wasm-dpp2/src/voting/vote_poll.rs b/packages/wasm-dpp2/src/voting/vote_poll.rs index 418385aad1a..72e758a54e1 100644 --- a/packages/wasm-dpp2/src/voting/vote_poll.rs +++ b/packages/wasm-dpp2/src/voting/vote_poll.rs @@ -158,15 +158,15 @@ impl VotePollWasm { } /// The kind of poll: `contestedDocumentResourceVotePoll` or `yesNoVotePoll`, the `$type` - /// tag the poll's serde writes, so it always matches `toJSON()`. + /// tag `toJSON()` writes (the specs check they agree). #[wasm_bindgen(getter = "kind")] - pub fn kind(&self) -> WasmDppResult { - serde_json::to_value(&self.0) - .map_err(|e| WasmDppError::serialization(e.to_string()))? - .get("$type") - .and_then(serde_json::Value::as_str) - .map(str::to_owned) - .ok_or_else(|| WasmDppError::serialization("a vote poll without a $type".to_string())) + pub fn kind(&self) -> String { + match &self.0 { + VotePoll::ContestedDocumentResourceVotePoll(_) => { + "contestedDocumentResourceVotePoll".to_string() + } + VotePoll::YesNoVotePoll(_) => "yesNoVotePoll".to_string(), + } } #[wasm_bindgen(getter = "contractId")] diff --git a/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts b/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts index 0f800581683..3bdb2b504a1 100644 --- a/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts +++ b/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts @@ -23,6 +23,7 @@ describe('VotePoll', () => { const json = poll.toJSON(); expect(json.$type).to.equal('contestedDocumentResourceVotePoll'); + expect(poll.kind).to.equal(json.$type); expect(json.contractId).to.equal(testContractId); expect(json.documentTypeName).to.equal('domain'); expect(json.indexName).to.equal('parentNameAndLabel'); @@ -183,6 +184,7 @@ describe('VotePoll', () => { const poll = wasm.VotePoll.fromJSON(yesNoPollJSON); expect(poll.kind).to.equal('yesNoVotePoll'); + expect(poll.kind).to.equal(poll.toJSON().$type); expect(() => poll.contractId).to.throw(); expect(() => poll.documentTypeName).to.throw(); expect(() => poll.indexName).to.throw(); From e74343bb7341a4d6d95f7ccfaa3cb5d820b7c4bb Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 27 Oct 2026 22:11:42 +0300 Subject: [PATCH 10/18] fix(platform): version the yes/no poll rules and refuse resource votes on yes/no polls in rs-dpp - YesNoVotePoll::validate_parameters, required_voting_power and passes dispatch through two new dpp voting slots, yes_no_vote_poll_parameters and yes_no_vote_poll_decision (0 in both voting tables), and take the platform version; the current rules are the private _v0 bodies. - Every yes/no drive method slot is an OptionalFeatureVersion: None in vote method tables V1 to V3, Some(0) in V4 (protocol version 15), and each dispatcher answers VersionNotActive before it. - ResourceVoteV0::try_new refuses a yes/no poll; the wasm-dpp2 Vote and ResourceVote constructors and poll setters and wasm-sdk's masternodeVote build resource votes through it. - The PV13 stripped-vote test expects the unpaid not-active error that v5.1-dev's decode_raw_state_transitions v0 now reports. Co-Authored-By: Claude Opus 5.5 --- .../voting/vote_polls/yes_no_vote_poll/mod.rs | 171 +++++++++++++----- .../src/voting/votes/resource_vote/v0/mod.rs | 47 +++++ .../v0/mod.rs | 6 +- .../masternode_vote/yes_no_tests.rs | 22 +-- .../mod.rs | 8 +- .../mod.rs | 8 +- .../mod.rs | 8 +- .../fetch/fetch_identity_yes_no_vote/mod.rs | 8 +- .../fetch_yes_no_vote_poll_stored_info/mod.rs | 8 +- .../mod.rs | 16 +- .../yes_no/open_yes_no_vote_poll/mod.rs | 94 ++++++++++ .../register_yes_no_identity_vote/mod.rs | 16 +- .../dpp_versions/dpp_voting_versions/mod.rs | 6 + .../dpp_versions/dpp_voting_versions/v1.rs | 2 + .../dpp_versions/dpp_voting_versions/v2.rs | 2 + .../drive_vote_method_versions/mod.rs | 21 +-- .../drive_vote_method_versions/v1.rs | 14 +- .../drive_vote_method_versions/v2.rs | 14 +- .../drive_vote_method_versions/v3.rs | 14 +- .../drive_vote_method_versions/v4.rs | 12 +- .../src/version/drive_versions/v10.rs | 4 +- .../rs-platform-version/src/version/v15.rs | 4 +- .../wasm-dpp2/src/voting/resource_vote.rs | 23 ++- packages/wasm-dpp2/src/voting/vote.rs | 19 +- .../wasm-dpp2/tests/unit/ResourceVote.spec.ts | 21 +++ packages/wasm-dpp2/tests/unit/Vote.spec.ts | 17 ++ .../src/state_transitions/identity.rs | 4 +- 27 files changed, 455 insertions(+), 134 deletions(-) diff --git a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs index ff06426a3de..26c1501503f 100644 --- a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs @@ -74,9 +74,10 @@ pub enum YesNoMinimumVotingPower { impl YesNoMinimumVotingPower { /// The minimum as a voting power, given the total voting power of the masternode list when - /// the poll closes. A fraction with a zero denominator (refused when the poll opens) needs - /// more than any poll can gather. - pub fn resolve(&self, total_voting_power: VotingPower) -> VotingPower { + /// the poll closes, as `YesNoVotePoll::required_voting_power` version 0 resolves it. A + /// fraction with a zero denominator (refused when the poll opens) needs more than any poll + /// can gather. + fn resolve_v0(&self, total_voting_power: VotingPower) -> VotingPower { match *self { YesNoMinimumVotingPower::Absolute { voting_power } => voting_power, YesNoMinimumVotingPower::FractionOfTotal { @@ -201,15 +202,40 @@ impl fmt::Display for YesNoVotePoll { } impl YesNoVotePoll { - /// Refuses parameters no poll can be opened with: an empty or oversized resource path (the - /// poll rides in every vote, whose wire size is bounded), a zero denominator, a zero - /// numerator (any single yes would pass against every no), a numerator that is not below - /// the denominator (yes could never exceed the whole of yes plus no), and a minimum above - /// its cap (a voting power, or a share of the total with a zero denominator or above the - /// allowed percentage). + /// Refuses parameters no poll can be opened with, by the rules the platform version selects + /// (`dpp.voting_versions.yes_no_vote_poll_parameters`). + /// + /// # Parameters + /// - `platform_version`: the platform version, whose system limits bound the parameters. + /// + /// # Returns + /// `Ok(())` for parameters a poll can run with, or the error refusing them. pub fn validate_parameters( &self, platform_version: &PlatformVersion, + ) -> Result<(), ProtocolError> { + match platform_version + .dpp + .voting_versions + .yes_no_vote_poll_parameters + { + 0 => self.validate_parameters_v0(platform_version), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "YesNoVotePoll::validate_parameters".to_string(), + known_versions: vec![0], + received: version, + }), + } + } + + /// Version 0 refuses an empty or oversized resource path (the poll rides in every vote, + /// whose wire size is bounded), a zero denominator, a zero numerator (any single yes would + /// pass against every no), a numerator that is not below the denominator (yes could never + /// exceed the whole of yes plus no), and a minimum above its cap (a voting power, or a share + /// of the total with a zero denominator or above the allowed percentage). + fn validate_parameters_v0( + &self, + platform_version: &PlatformVersion, ) -> Result<(), ProtocolError> { if self.resource_path.is_empty() { return Err(ProtocolError::VoteError( @@ -289,22 +315,83 @@ impl YesNoVotePoll { } /// The least yes plus no voting power the poll needs, given the total voting power of the - /// masternode list in the block that closes it. - pub fn required_voting_power(&self, total_voting_power: VotingPower) -> VotingPower { - self.minimum_voting_power.resolve(total_voting_power) + /// masternode list in the block that closes it, by the rule the platform version selects + /// (`dpp.voting_versions.yes_no_vote_poll_decision`). + /// + /// # Parameters + /// - `total_voting_power`: the masternode list's total voting power in the closing block. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The required voting power. + pub fn required_voting_power( + &self, + total_voting_power: VotingPower, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .voting_versions + .yes_no_vote_poll_decision + { + 0 => Ok(self.required_voting_power_v0(total_voting_power)), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "YesNoVotePoll::required_voting_power".to_string(), + known_versions: vec![0], + received: version, + }), + } } /// Whether the poll passes with these final tallies, given the total voting power of the - /// masternode list in the block that closes it. Abstaining power plays no part. + /// masternode list in the block that closes it, by the rule the platform version selects + /// (`dpp.voting_versions.yes_no_vote_poll_decision`). Abstaining power plays no part. + /// + /// # Parameters + /// - `yes_voting_power`: the yes voting power cast. + /// - `no_voting_power`: the no voting power cast. + /// - `total_voting_power`: the masternode list's total voting power in the closing block. + /// - `platform_version`: the platform version. + /// + /// # Returns + /// Whether the poll passes. pub fn passes( &self, yes_voting_power: VotingPower, no_voting_power: VotingPower, total_voting_power: VotingPower, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .voting_versions + .yes_no_vote_poll_decision + { + 0 => Ok(self.passes_v0(yes_voting_power, no_voting_power, total_voting_power)), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "YesNoVotePoll::passes".to_string(), + known_versions: vec![0], + received: version, + }), + } + } + + /// Version 0: the minimum resolved against the total, rounded as the poll asks. + fn required_voting_power_v0(&self, total_voting_power: VotingPower) -> VotingPower { + self.minimum_voting_power.resolve_v0(total_voting_power) + } + + /// Version 0: yes plus no must reach the required voting power, and yes must be strictly + /// more than the supermajority share of yes plus no. + fn passes_v0( + &self, + yes_voting_power: VotingPower, + no_voting_power: VotingPower, + total_voting_power: VotingPower, ) -> bool { let yes = yes_voting_power as u64; let cast = yes + no_voting_power as u64; - if cast < self.required_voting_power(total_voting_power) as u64 { + if cast < self.required_voting_power_v0(total_voting_power) as u64 { return false; } yes * self.supermajority_denominator as u64 > cast * self.supermajority_numerator as u64 @@ -353,18 +440,18 @@ mod tests { ..poll() }; // 300 cast: two thirds is 200, so 201 passes and 200 does not. - assert!(poll.passes(201, 99, 0)); - assert!(!poll.passes(200, 100, 0)); + assert!(poll.passes_v0(201, 99, 0)); + assert!(!poll.passes_v0(200, 100, 0)); // 301 cast: two thirds is 200.67, so 201 passes and 200 does not. - assert!(poll.passes(201, 100, 0)); - assert!(!poll.passes(200, 101, 0)); + assert!(poll.passes_v0(201, 100, 0)); + assert!(!poll.passes_v0(200, 101, 0)); } #[test] fn should_fail_below_the_minimum_voting_power_even_when_unanimous() { let poll = poll(); - assert!(!poll.passes(399, 0, 0)); - assert!(poll.passes(400, 0, 0)); + assert!(!poll.passes_v0(399, 0, 0)); + assert!(poll.passes_v0(400, 0, 0)); } #[test] @@ -374,30 +461,30 @@ mod tests { denominator: 3, rounding, }; - assert_eq!(third(VotingPowerRounding::Down).resolve(10), 3); - assert_eq!(third(VotingPowerRounding::Up).resolve(10), 4); + assert_eq!(third(VotingPowerRounding::Down).resolve_v0(10), 3); + assert_eq!(third(VotingPowerRounding::Up).resolve_v0(10), 4); // An exact share is the same either way. - assert_eq!(third(VotingPowerRounding::Down).resolve(9), 3); - assert_eq!(third(VotingPowerRounding::Up).resolve(9), 3); - assert_eq!(third(VotingPowerRounding::Up).resolve(0), 0); + assert_eq!(third(VotingPowerRounding::Down).resolve_v0(9), 3); + assert_eq!(third(VotingPowerRounding::Up).resolve_v0(9), 3); + assert_eq!(third(VotingPowerRounding::Up).resolve_v0(0), 0); // Rounded down plus one, and rounded up minus one (never below zero). - assert_eq!(third(VotingPowerRounding::DownPlusOne).resolve(10), 4); - assert_eq!(third(VotingPowerRounding::DownPlusOne).resolve(9), 4); - assert_eq!(third(VotingPowerRounding::DownPlusOne).resolve(0), 1); - assert_eq!(third(VotingPowerRounding::UpMinusOne).resolve(10), 3); - assert_eq!(third(VotingPowerRounding::UpMinusOne).resolve(9), 2); - assert_eq!(third(VotingPowerRounding::UpMinusOne).resolve(0), 0); + assert_eq!(third(VotingPowerRounding::DownPlusOne).resolve_v0(10), 4); + assert_eq!(third(VotingPowerRounding::DownPlusOne).resolve_v0(9), 4); + assert_eq!(third(VotingPowerRounding::DownPlusOne).resolve_v0(0), 1); + assert_eq!(third(VotingPowerRounding::UpMinusOne).resolve_v0(10), 3); + assert_eq!(third(VotingPowerRounding::UpMinusOne).resolve_v0(9), 2); + assert_eq!(third(VotingPowerRounding::UpMinusOne).resolve_v0(0), 0); // Half rounded down plus one is a strict majority. let majority = YesNoMinimumVotingPower::FractionOfTotal { numerator: 1, denominator: 2, rounding: VotingPowerRounding::DownPlusOne, }; - assert_eq!(majority.resolve(10), 6); - assert_eq!(majority.resolve(9), 5); - assert_eq!(majority.resolve(u32::MAX), u32::MAX / 2 + 1); + assert_eq!(majority.resolve_v0(10), 6); + assert_eq!(majority.resolve_v0(9), 5); + assert_eq!(majority.resolve_v0(u32::MAX), u32::MAX / 2 + 1); assert_eq!( - YesNoMinimumVotingPower::Absolute { voting_power: 400 }.resolve(10), + YesNoMinimumVotingPower::Absolute { voting_power: 400 }.resolve_v0(10), 400 ); // No overflow on the largest total. @@ -406,7 +493,7 @@ mod tests { denominator: 2, rounding: VotingPowerRounding::Up, }; - assert_eq!(half.resolve(u32::MAX), u32::MAX / 2 + 1); + assert_eq!(half.resolve_v0(u32::MAX), u32::MAX / 2 + 1); } #[test] @@ -421,11 +508,11 @@ mod tests { ..poll() }; // A total of 3,801 needs 381 cast. - assert_eq!(poll.required_voting_power(3_801), 381); - assert!(!poll.passes(380, 0, 3_801)); - assert!(poll.passes(381, 0, 3_801)); + assert_eq!(poll.required_voting_power_v0(3_801), 381); + assert!(!poll.passes_v0(380, 0, 3_801)); + assert!(poll.passes_v0(381, 0, 3_801)); // The same tally fails once the list has grown. - assert!(!poll.passes(381, 0, 3_811)); + assert!(!poll.passes_v0(381, 0, 3_811)); } #[test] @@ -484,8 +571,8 @@ mod tests { supermajority_denominator: 255, ..poll() }; - assert!(poll.passes(u32::MAX, 0, 0)); - assert!(!poll.passes(u32::MAX, u32::MAX, 0)); + assert!(poll.passes_v0(u32::MAX, 0, 0)); + assert!(!poll.passes_v0(u32::MAX, u32::MAX, 0)); } #[test] diff --git a/packages/rs-dpp/src/voting/votes/resource_vote/v0/mod.rs b/packages/rs-dpp/src/voting/votes/resource_vote/v0/mod.rs index d1253100944..3cb27df7c40 100644 --- a/packages/rs-dpp/src/voting/votes/resource_vote/v0/mod.rs +++ b/packages/rs-dpp/src/voting/votes/resource_vote/v0/mod.rs @@ -44,7 +44,54 @@ impl Default for ResourceVoteV0 { } impl ResourceVoteV0 { + /// A resource vote on `vote_poll`, refused when the poll is a yes/no poll, which a yes/no + /// vote answers. The fields stay public for decoding; consensus refuses a resource vote + /// naming a yes/no poll however it was built. + pub fn try_new( + vote_poll: VotePoll, + resource_vote_choice: ResourceVoteChoice, + ) -> Result { + match vote_poll { + VotePoll::ContestedDocumentResourceVotePoll(_) => Ok(ResourceVoteV0 { + vote_poll, + resource_vote_choice, + }), + VotePoll::YesNoVotePoll(_) => Err(ProtocolError::VoteError( + "a resource vote answers a contested document resource vote poll; a yes/no vote \ + poll is answered by a yes/no vote" + .to_string(), + )), + } + } + pub fn vote_poll_unique_id(&self) -> Result { self.vote_poll.unique_id() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; + use crate::voting::vote_polls::yes_no_vote_poll::{YesNoMinimumVotingPower, YesNoVotePoll}; + use platform_value::BinaryData; + + #[test] + fn should_build_a_resource_vote_only_on_a_contested_poll() { + let contested_poll = VotePoll::ContestedDocumentResourceVotePoll( + ContestedDocumentResourceVotePoll::default(), + ); + let vote = ResourceVoteV0::try_new(contested_poll.clone(), ResourceVoteChoice::Abstain) + .expect("expected a resource vote on a contested poll"); + assert_eq!(vote.vote_poll, contested_poll); + + let yes_no_poll = VotePoll::YesNoVotePoll(YesNoVotePoll { + resource_path: vec![BinaryData::new(b"challenge".to_vec())], + supermajority_numerator: 2, + supermajority_denominator: 3, + minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 0 }, + }); + ResourceVoteV0::try_new(yes_no_poll, ResourceVoteChoice::Abstain) + .expect_err("expected a resource vote on a yes/no poll to be refused"); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs index 75334e34a16..4d7de36438b 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/v0/mod.rs @@ -26,12 +26,14 @@ where transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result { - let required_voting_power = vote_poll.required_voting_power(total_voting_power); + let required_voting_power = + vote_poll.required_voting_power(total_voting_power, platform_version)?; let passed = vote_poll.passes( tally.yes_voting_power, tally.no_voting_power, total_voting_power, - ); + platform_version, + )?; let result = stored_info.finalize_vote_poll( tally.yes_voting_power, tally.no_voting_power, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs index c0e1c6cc1ed..2f46ee55d32 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs @@ -1394,19 +1394,15 @@ async fn run_a_contest_and_strip_yes_no_polls(protocol_version: u32) { ) .await .expect_err("expected the vote to be stripped"); - // Protocol version 14 decodes with `decode_raw_state_transitions` 1, which reports a - // transition not active yet as an unpaid consensus error; 13 reports it as an internal - // error. Both strip the vote without charging. - if protocol_version >= 14 { - assert_matches!( - result, - UnpaidConsensusError(ConsensusError::BasicError( - BasicError::StateTransitionNotActiveError(_) - )) - ); - } else { - assert_matches!(result, InternalError(message) if message.contains("is not active")); - } + // Both decoding generations (`decode_raw_state_transitions` 0 at 13, 1 at 14) report a + // transition not active yet as an unpaid consensus error, which strips the vote without + // charging. + assert_matches!( + result, + UnpaidConsensusError(ConsensusError::BasicError( + BasicError::StateTransitionNotActiveError(_) + )) + ); } // Nonce 1 is still free: the stripped votes consumed nothing. diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs index 77baa3b32ac..458ec6c5a8f 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs @@ -31,16 +31,20 @@ impl Drive { .yes_no .remove_yes_no_vote_poll_votes_operations { - 0 => self.remove_yes_no_vote_poll_votes_operations_v0( + Some(0) => self.remove_yes_no_vote_poll_votes_operations_v0( vote_poll_ids, batch_operations, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "remove_yes_no_vote_poll_votes_operations".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "remove_yes_no_vote_poll_votes_operations".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs index cf75cf085a3..89d2f4419e8 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs @@ -36,18 +36,22 @@ impl Drive { .yes_no .remove_yes_no_vote_references_given_by_identity { - 0 => self.remove_yes_no_vote_references_given_by_identity_operations_v0( + Some(0) => self.remove_yes_no_vote_references_given_by_identity_operations_v0( masternode_pro_tx_hash, vote_poll_ids, batch_operations, transaction, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "remove_yes_no_vote_references_given_by_identity_operations".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "remove_yes_no_vote_references_given_by_identity_operations".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs index 262044d4501..48cd957db44 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs @@ -32,16 +32,20 @@ impl Drive { .yes_no .fetch_identities_voting_in_yes_no_vote_poll { - 0 => self.fetch_identities_voting_in_yes_no_vote_poll_v0( + Some(0) => self.fetch_identities_voting_in_yes_no_vote_poll_v0( vote_poll, transaction, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "fetch_identities_voting_in_yes_no_vote_poll".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "fetch_identities_voting_in_yes_no_vote_poll".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs index e87fa7b744b..7cc056f767d 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs @@ -38,18 +38,22 @@ impl Drive { .yes_no .fetch_identity_yes_no_vote { - 0 => self.fetch_identity_yes_no_vote_v0( + Some(0) => self.fetch_identity_yes_no_vote_v0( masternode_pro_tx_hash, vote_poll_id, transaction, drive_operations, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "fetch_identity_yes_no_vote".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "fetch_identity_yes_no_vote".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs index 7351d64d855..7a3bc753f24 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs @@ -31,14 +31,18 @@ impl Drive { .yes_no .fetch_yes_no_vote_poll_stored_info { - 0 => { + Some(0) => { self.fetch_yes_no_vote_poll_stored_info_v0(vote_poll, transaction, platform_version) } - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "fetch_yes_no_vote_poll_stored_info".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "fetch_yes_no_vote_poll_stored_info".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/mod.rs index 4e666fcee34..d9c4fb68ff5 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/mod.rs @@ -34,17 +34,21 @@ impl Drive { .yes_no .insert_stored_info_for_yes_no_vote_poll { - 0 => self.insert_stored_info_for_yes_no_vote_poll_v0( + Some(0) => self.insert_stored_info_for_yes_no_vote_poll_v0( vote_poll, stored_info, transaction, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "insert_stored_info_for_yes_no_vote_poll".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "insert_stored_info_for_yes_no_vote_poll".to_string(), + known_versions: vec![0], + })), } } @@ -70,16 +74,20 @@ impl Drive { .yes_no .insert_stored_info_for_yes_no_vote_poll { - 0 => self.insert_stored_info_for_yes_no_vote_poll_operations_v0( + Some(0) => self.insert_stored_info_for_yes_no_vote_poll_operations_v0( vote_poll, stored_info, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "insert_stored_info_for_yes_no_vote_poll_operations".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "insert_stored_info_for_yes_no_vote_poll_operations".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs index b6d3b84f03d..d4941dc0c41 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs @@ -137,14 +137,20 @@ mod tests { vote_end_date_queries_tree_path, vote_root_path, ACTIVE_POLLS_TREE_KEY, IDENTITY_VOTES_TREE_KEY, }; + use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; + use crate::error::drive::DriveError; + use crate::error::Error; use crate::util::common::encode::encode_u64; use crate::util::grove_operations::{BatchDeleteUpTreeApplyType, DirectQueryType}; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; use crate::util::test_helpers::test_yes_no_vote_poll as poll; use crate::util::test_helpers::vote_poll_end_dates; use dpp::block::block_info::BlockInfo; + use dpp::identifier::Identifier; use dpp::platform_value::BinaryData; use dpp::version::PlatformVersion; + use dpp::voting::vote_choices::yes_no_abstain_vote_choice::YesNoAbstainVoteChoice; + use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStoredInfo; use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; use dpp::voting::vote_polls::VotePoll; use grovedb::batch::KeyInfoPath; @@ -237,6 +243,94 @@ mod tests { .expect_err("expected protocol version 14 to refuse opening a yes/no poll"); } + #[test] + fn should_report_every_yes_no_method_not_active_before_protocol_version_15() { + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let vote_poll = poll(); + let poll_id = vote_poll.unique_id().expect("id"); + let not_active = |result: Result<(), Error>, method: &str| { + assert!( + matches!( + result, + Err(Error::Drive(DriveError::VersionNotActive { .. })) + ), + "expected {method} to be not active at protocol version 14, got {result:?}" + ); + }; + not_active( + drive + .register_yes_no_identity_vote_operations( + [1; 32], + 1, + ResolvedYesNoVote { + vote_poll: vote_poll.clone(), + vote_choice: YesNoAbstainVoteChoice::Yes, + previous_vote_choice_to_remove: None, + }, + None, + platform_version, + ) + .map(|_| ()), + "register_yes_no_identity_vote", + ); + not_active( + YesNoVotePollStoredInfo::new(BlockInfo::default(), PlatformVersion::latest()) + .map_err(|error| Error::Protocol(Box::new(error))) + .and_then(|stored_info| { + drive.insert_stored_info_for_yes_no_vote_poll_operations( + &vote_poll, + stored_info, + platform_version, + ) + }) + .map(|_| ()), + "insert_stored_info_for_yes_no_vote_poll", + ); + not_active( + drive + .fetch_yes_no_vote_poll_stored_info(&vote_poll, None, platform_version) + .map(|_| ()), + "fetch_yes_no_vote_poll_stored_info", + ); + not_active( + drive + .fetch_identity_yes_no_vote( + Identifier::new([1; 32]), + poll_id, + None, + &mut vec![], + platform_version, + ) + .map(|_| ()), + "fetch_identity_yes_no_vote", + ); + not_active( + drive + .fetch_identities_voting_in_yes_no_vote_poll(&vote_poll, None, platform_version) + .map(|_| ()), + "fetch_identities_voting_in_yes_no_vote_poll", + ); + not_active( + drive.remove_yes_no_vote_poll_votes_operations( + &[poll_id], + &mut vec![], + platform_version, + ), + "remove_yes_no_vote_poll_votes_operations", + ); + not_active( + drive.remove_yes_no_vote_references_given_by_identity_operations( + &Identifier::new([1; 32]), + &[&poll_id], + &mut vec![], + None, + platform_version, + ), + "remove_yes_no_vote_references_given_by_identity", + ); + } + #[test] fn should_refuse_a_poll_that_ends_by_the_block_that_opens_it() { let drive = setup_drive_with_initial_state_structure(None); diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs index cdde82cbf9d..d0b4fb5fa19 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs @@ -41,7 +41,7 @@ impl Drive { .yes_no .register_yes_no_identity_vote { - 0 => self.register_yes_no_identity_vote_v0( + Some(0) => self.register_yes_no_identity_vote_v0( voter_pro_tx_hash, strength, vote, @@ -49,11 +49,15 @@ impl Drive { transaction, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "register_yes_no_identity_vote".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "register_yes_no_identity_vote".to_string(), + known_versions: vec![0], + })), } } @@ -83,18 +87,22 @@ impl Drive { .yes_no .register_yes_no_identity_vote { - 0 => self.register_yes_no_identity_vote_operations_v0( + Some(0) => self.register_yes_no_identity_vote_operations_v0( voter_pro_tx_hash, strength, vote, transaction, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "register_yes_no_identity_vote_operations".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "register_yes_no_identity_vote_operations".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs index 1714fbfdf8d..35e99653d37 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs @@ -10,4 +10,10 @@ pub struct DPPVotingVersions { pub contested_document_vote_poll_stored_info_version: FeatureVersion, /// The stored info of a yes/no poll (protocol version 15). pub yes_no_vote_poll_stored_info_version: FeatureVersion, + /// The rules a yes/no poll's parameters must meet (`YesNoVotePoll::validate_parameters`, + /// protocol version 15). + pub yes_no_vote_poll_parameters: FeatureVersion, + /// How a yes/no poll is decided: its required voting power and whether it passes + /// (`YesNoVotePoll::required_voting_power` and `YesNoVotePoll::passes`, protocol version 15). + pub yes_no_vote_poll_decision: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs index 4e19daaf563..1ab94dde2af 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs @@ -5,4 +5,6 @@ pub const VOTING_VERSION_V1: DPPVotingVersions = DPPVotingVersions { default_vote_poll_time_duration_test_network_ms: 1_209_600_000, //2 weeks contested_document_vote_poll_stored_info_version: 0, yes_no_vote_poll_stored_info_version: 0, + yes_no_vote_poll_parameters: 0, + yes_no_vote_poll_decision: 0, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs index 409b7bec462..9d246f9ed0c 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs @@ -5,4 +5,6 @@ pub const VOTING_VERSION_V2: DPPVotingVersions = DPPVotingVersions { default_vote_poll_time_duration_test_network_ms: 5_400_000, //90 minutes contested_document_vote_poll_stored_info_version: 0, yes_no_vote_poll_stored_info_version: 0, + yes_no_vote_poll_parameters: 0, + yes_no_vote_poll_decision: 0, }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs index f66ac0dcd88..38dbfc832e6 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs @@ -17,20 +17,19 @@ pub struct DriveVoteMethodVersions { pub yes_no: DriveVoteYesNoMethodVersions, } -/// The methods of the yes/no poll kind. All of them are new in protocol version 15 and -/// unreachable before it: opening a poll is not active before 15, and a vote naming a yes/no -/// poll does not decode before 15. +/// The methods of the yes/no poll kind. All of them are new in protocol version 15 and `None` +/// (not active) in every earlier table: opening a poll is not active before 15, and a vote +/// naming a yes/no poll does not decode before 15. #[derive(Clone, Debug, Default)] pub struct DriveVoteYesNoMethodVersions { - /// `None` before protocol version 15, where no yes/no poll may be opened. pub open_yes_no_vote_poll: OptionalFeatureVersion, - pub register_yes_no_identity_vote: FeatureVersion, - pub insert_stored_info_for_yes_no_vote_poll: FeatureVersion, - pub fetch_yes_no_vote_poll_stored_info: FeatureVersion, - pub fetch_identity_yes_no_vote: FeatureVersion, - pub fetch_identities_voting_in_yes_no_vote_poll: FeatureVersion, - pub remove_yes_no_vote_poll_votes_operations: FeatureVersion, - pub remove_yes_no_vote_references_given_by_identity: FeatureVersion, + pub register_yes_no_identity_vote: OptionalFeatureVersion, + pub insert_stored_info_for_yes_no_vote_poll: OptionalFeatureVersion, + pub fetch_yes_no_vote_poll_stored_info: OptionalFeatureVersion, + pub fetch_identity_yes_no_vote: OptionalFeatureVersion, + pub fetch_identities_voting_in_yes_no_vote_poll: OptionalFeatureVersion, + pub remove_yes_no_vote_poll_votes_operations: OptionalFeatureVersion, + pub remove_yes_no_vote_references_given_by_identity: OptionalFeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs index c8ee8ac8fda..c30c7a6a3d7 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs @@ -39,12 +39,12 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V1: DriveVoteMethodVersions = DriveVoteMeth }, yes_no: DriveVoteYesNoMethodVersions { open_yes_no_vote_poll: None, - register_yes_no_identity_vote: 0, - insert_stored_info_for_yes_no_vote_poll: 0, - fetch_yes_no_vote_poll_stored_info: 0, - fetch_identity_yes_no_vote: 0, - fetch_identities_voting_in_yes_no_vote_poll: 0, - remove_yes_no_vote_poll_votes_operations: 0, - remove_yes_no_vote_references_given_by_identity: 0, + register_yes_no_identity_vote: None, + insert_stored_info_for_yes_no_vote_poll: None, + fetch_yes_no_vote_poll_stored_info: None, + fetch_identity_yes_no_vote: None, + fetch_identities_voting_in_yes_no_vote_poll: None, + remove_yes_no_vote_poll_votes_operations: None, + remove_yes_no_vote_references_given_by_identity: None, }, }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs index 03fe1027ddc..f95a0e43da7 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs @@ -39,12 +39,12 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V2: DriveVoteMethodVersions = DriveVoteMeth }, yes_no: DriveVoteYesNoMethodVersions { open_yes_no_vote_poll: None, - register_yes_no_identity_vote: 0, - insert_stored_info_for_yes_no_vote_poll: 0, - fetch_yes_no_vote_poll_stored_info: 0, - fetch_identity_yes_no_vote: 0, - fetch_identities_voting_in_yes_no_vote_poll: 0, - remove_yes_no_vote_poll_votes_operations: 0, - remove_yes_no_vote_references_given_by_identity: 0, + register_yes_no_identity_vote: None, + insert_stored_info_for_yes_no_vote_poll: None, + fetch_yes_no_vote_poll_stored_info: None, + fetch_identity_yes_no_vote: None, + fetch_identities_voting_in_yes_no_vote_poll: None, + remove_yes_no_vote_poll_votes_operations: None, + remove_yes_no_vote_references_given_by_identity: None, }, }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs index 2f5a19ea030..addf338b709 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs @@ -47,12 +47,12 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V3: DriveVoteMethodVersions = DriveVoteMeth }, yes_no: DriveVoteYesNoMethodVersions { open_yes_no_vote_poll: None, - register_yes_no_identity_vote: 0, - insert_stored_info_for_yes_no_vote_poll: 0, - fetch_yes_no_vote_poll_stored_info: 0, - fetch_identity_yes_no_vote: 0, - fetch_identities_voting_in_yes_no_vote_poll: 0, - remove_yes_no_vote_poll_votes_operations: 0, - remove_yes_no_vote_references_given_by_identity: 0, + register_yes_no_identity_vote: None, + insert_stored_info_for_yes_no_vote_poll: None, + fetch_yes_no_vote_poll_stored_info: None, + fetch_identity_yes_no_vote: None, + fetch_identities_voting_in_yes_no_vote_poll: None, + remove_yes_no_vote_poll_votes_operations: None, + remove_yes_no_vote_references_given_by_identity: None, }, }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs index 6a493f3dd40..0bdeafcd8f3 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs @@ -11,8 +11,8 @@ use crate::version::drive_versions::drive_vote_method_versions::{ /// contested resource votes, which v1 still hands to it). /// * `remove_all_votes_given_by_identities` 1 also removes the yes/no votes of a masternode /// that left the list. -/// * `yes_no.open_yes_no_vote_poll` is `Some(0)`: yes/no polls can be opened from this version -/// (`None` in every earlier table). +/// * every `yes_no` method is `Some(0)`: yes/no polls can be opened and voted on from this +/// version (`None`, not active, in every earlier table). /// * `contested_resource_insert.add_vote_poll_end_date_query_operations` 1 adds only the entry /// when the batch already queues an insert of the end date's tree with other storage flags (a /// contest and a yes/no poll opening sharing an end date), where 0 queued the tree twice. @@ -32,7 +32,13 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V4: DriveVoteMethodVersions = DriveVoteMeth }, yes_no: DriveVoteYesNoMethodVersions { open_yes_no_vote_poll: Some(0), - ..DRIVE_VOTE_METHOD_VERSIONS_V3.yes_no + register_yes_no_identity_vote: Some(0), + insert_stored_info_for_yes_no_vote_poll: Some(0), + fetch_yes_no_vote_poll_stored_info: Some(0), + fetch_identity_yes_no_vote: Some(0), + fetch_identities_voting_in_yes_no_vote_poll: Some(0), + remove_yes_no_vote_poll_votes_operations: Some(0), + remove_yes_no_vote_references_given_by_identity: Some(0), }, ..DRIVE_VOTE_METHOD_VERSIONS_V3 }; diff --git a/packages/rs-platform-version/src/version/drive_versions/v10.rs b/packages/rs-platform-version/src/version/drive_versions/v10.rs index 84d171efc02..1242bdde499 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -8,8 +8,8 @@ use crate::version::drive_versions::{DriveMethodVersions, DriveVersion}; /// * **Yes/no vote polls**: `DRIVE_VOTE_METHOD_VERSIONS_V4` bumps /// `insert.register_identity_vote` to 1, which registers a vote of either poll kind, and /// `cleanup.remove_all_votes_given_by_identities` to 1, which also removes the yes/no votes of -/// a masternode that left the list. It sets `yes_no.open_yes_no_vote_poll` to `Some(0)`, so -/// yes/no polls can be opened (`None`, not active, in every earlier table), and bumps +/// a masternode that left the list. It sets every `yes_no` method to `Some(0)`, so yes/no +/// polls can be opened and voted on (`None`, not active, in every earlier table), and bumps /// `contested_resource_insert.add_vote_poll_end_date_query_operations` to 1, which adds only /// the entry when the batch already queues the end date's tree with other storage flags. /// diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 027f8128729..f5d5b185a6d 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -170,7 +170,7 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// `transition_to_version_15` create. Masternode vote `transform_into_action` /// 1 (`DRIVE_ABCI_VALIDATION_VERSIONS_V11`) looks up the previous answer, and /// `DRIVE_VERSION_V10` registers and sweeps the votes. Opening a yes/no poll -/// (`Drive::open_yes_no_vote_poll`, `open_yes_no_vote_poll` `Some(0)`) is active +/// (`Drive::open_yes_no_vote_poll`; every `yes_no` drive method is `Some(0)`) is active /// from this version, for an end date at most `max_yes_no_vote_poll_duration_ms` /// (four weeks, a `SystemLimits` field read only by the opener) after the /// opening block, and its end date entry goes through @@ -180,7 +180,7 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// refused for want of a poll. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, - drive: DRIVE_VERSION_V10, // changed: vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, open_yes_no_vote_poll Some(0) makes opening a yes/no poll active and add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags (note 8) + drive: DRIVE_VERSION_V10, // changed: vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, every yes_no method Some(0) makes yes/no polls active and add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags (note 8) drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock diff --git a/packages/wasm-dpp2/src/voting/resource_vote.rs b/packages/wasm-dpp2/src/voting/resource_vote.rs index c7a474fd97d..e7f8bc1774e 100644 --- a/packages/wasm-dpp2/src/voting/resource_vote.rs +++ b/packages/wasm-dpp2/src/voting/resource_vote.rs @@ -1,3 +1,4 @@ +use crate::error::WasmDppResult; use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::voting::resource_vote_choice::ResourceVoteChoiceWasm; @@ -57,11 +58,14 @@ impl From for ResourceVote { #[wasm_bindgen(js_class = ResourceVote)] impl ResourceVoteWasm { #[wasm_bindgen(constructor)] - pub fn constructor(poll: &VotePollWasm, choice: &ResourceVoteChoiceWasm) -> Self { - ResourceVoteWasm(ResourceVote::V0(ResourceVoteV0 { - vote_poll: poll.clone().into(), - resource_vote_choice: choice.clone().into(), - })) + pub fn constructor( + poll: &VotePollWasm, + choice: &ResourceVoteChoiceWasm, + ) -> WasmDppResult { + Ok(ResourceVoteWasm(ResourceVote::V0(ResourceVoteV0::try_new( + poll.clone().into(), + choice.clone().into(), + )?))) } #[wasm_bindgen(getter = poll)] @@ -75,16 +79,17 @@ impl ResourceVoteWasm { } #[wasm_bindgen(setter = poll)] - pub fn set_poll(&mut self, poll: &VotePollWasm) { + pub fn set_poll(&mut self, poll: &VotePollWasm) -> WasmDppResult<()> { let ResourceVote::V0(ResourceVoteV0 { resource_vote_choice, .. }) = self.0.clone(); - self.0 = ResourceVote::V0(ResourceVoteV0 { - vote_poll: poll.clone().into(), + self.0 = ResourceVote::V0(ResourceVoteV0::try_new( + poll.clone().into(), resource_vote_choice, - }); + )?); + Ok(()) } #[wasm_bindgen(setter = choice)] diff --git a/packages/wasm-dpp2/src/voting/vote.rs b/packages/wasm-dpp2/src/voting/vote.rs index 9f112273252..ceda8c4a4e9 100644 --- a/packages/wasm-dpp2/src/voting/vote.rs +++ b/packages/wasm-dpp2/src/voting/vote.rs @@ -93,11 +93,13 @@ impl VoteWasm { #[wasm_bindgen(js_name = "votePoll")] vote_poll: &VotePollWasm, #[wasm_bindgen(js_name = "resourceVoteChoice")] resource_vote_choice: &ResourceVoteChoiceWasm, - ) -> Self { - VoteWasm(Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { - vote_poll: vote_poll.clone().into(), - resource_vote_choice: resource_vote_choice.clone().into(), - }))) + ) -> WasmDppResult { + Ok(VoteWasm(Vote::ResourceVote(ResourceVote::V0( + ResourceVoteV0::try_new( + vote_poll.clone().into(), + resource_vote_choice.clone().into(), + )?, + )))) } #[wasm_bindgen(getter = poll)] @@ -138,10 +140,9 @@ impl VoteWasm { #[wasm_bindgen(setter = poll)] pub fn set_poll(&mut self, poll: &VotePollWasm) -> WasmDppResult<()> { self.0 = match self.0.clone() { - Vote::ResourceVote(vote) => Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { - vote_poll: poll.clone().into(), - resource_vote_choice: vote.resource_vote_choice(), - })), + Vote::ResourceVote(vote) => Vote::ResourceVote(ResourceVote::V0( + ResourceVoteV0::try_new(poll.clone().into(), vote.resource_vote_choice())?, + )), Vote::YesNoVote(vote) => { let VotePoll::YesNoVotePoll(vote_poll) = poll.clone().into() else { return Err(WasmDppError::invalid_argument( diff --git a/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts b/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts index f1aeef73113..c6296de5114 100644 --- a/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts +++ b/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts @@ -107,4 +107,25 @@ describe('ResourceVote', () => { vote.free(); }); }); + + it('should refuse a yes/no poll', () => { + const yesNoPoll = wasm.VotePoll.fromJSON({ + $type: 'yesNoVotePoll', + resourcePath: ['wcHBwQ=='], + supermajorityNumerator: 2, + supermajorityDenominator: 3, + minimumVotingPower: { $type: 'absolute', votingPower: 400 }, + }); + const choice = wasm.ResourceVoteChoice.Abstain(); + + expect(() => new wasm.ResourceVote(yesNoPoll, choice)).to.throw(); + + const vote = new wasm.ResourceVote(createPoll(), choice); + expect(() => { + vote.poll = yesNoPoll; + }).to.throw(); + + vote.free(); + yesNoPoll.free(); + }); }); diff --git a/packages/wasm-dpp2/tests/unit/Vote.spec.ts b/packages/wasm-dpp2/tests/unit/Vote.spec.ts index 8832f2b253f..b2209ce3834 100644 --- a/packages/wasm-dpp2/tests/unit/Vote.spec.ts +++ b/packages/wasm-dpp2/tests/unit/Vote.spec.ts @@ -147,5 +147,22 @@ describe('Vote', () => { vote.free(); }); + + it('should refuse a resource vote on a yes/no poll', () => { + const yesNoPoll = wasm.VotePoll.fromJSON({ + $type: 'yesNoVotePoll', + ...yesNoVoteJSON.votePoll, + }); + + expect(() => new wasm.Vote(yesNoPoll, wasm.ResourceVoteChoice.Abstain())).to.throw(); + + const vote = new wasm.Vote(createPoll(), wasm.ResourceVoteChoice.Abstain()); + expect(() => { + vote.poll = yesNoPoll; + }).to.throw(); + + vote.free(); + yesNoPoll.free(); + }); }); }); diff --git a/packages/wasm-sdk/src/state_transitions/identity.rs b/packages/wasm-sdk/src/state_transitions/identity.rs index 0fd35bb11ab..6aacefde7e8 100644 --- a/packages/wasm-sdk/src/state_transitions/identity.rs +++ b/packages/wasm-sdk/src/state_transitions/identity.rs @@ -793,10 +793,10 @@ impl WasmSdk { let settings = try_from_options_optional::(&options, "settings")?.map(Into::into); - let vote = Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + let vote = Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0::try_new( vote_poll, resource_vote_choice, - })); + )?)); let recorded_vote = vote .put_to_platform_and_wait_for_response( From 245f526edaaaad40521cebe724de9985e92b2634 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 27 Oct 2026 23:33:27 +0300 Subject: [PATCH 11/18] fix(platform): refuse yes/no polls in resource votes read from JSON and trailing bytes in stored yes/no vote references A resource vote read from a plain object or JSON now goes through ResourceVoteV0::try_new, so WASM fromObject/fromJSON refuse a resource vote naming a yes/no poll as the constructors do. Bincode decoding is unchanged; consensus still refuses such a vote however it was built. A stored yes/no vote reference carrying bytes past the form now decodes as corrupted, matching the proof-side decoder's exact-length rule. Co-Authored-By: Claude Opus 5.5 --- .../src/voting/votes/resource_vote/mod.rs | 24 +++++++++++ .../src/voting/votes/resource_vote/v0/mod.rs | 22 +++++++++- .../yes_no_vote_reference_storage_form/mod.rs | 40 ++++++++++++++----- .../wasm-dpp2/tests/unit/ResourceVote.spec.ts | 23 +++++++++++ packages/wasm-dpp2/tests/unit/Vote.spec.ts | 18 +++++++++ 5 files changed, 116 insertions(+), 11 deletions(-) diff --git a/packages/rs-dpp/src/voting/votes/resource_vote/mod.rs b/packages/rs-dpp/src/voting/votes/resource_vote/mod.rs index 6084c04e5ba..a74e6dfa0b9 100644 --- a/packages/rs-dpp/src/voting/votes/resource_vote/mod.rs +++ b/packages/rs-dpp/src/voting/votes/resource_vote/mod.rs @@ -139,4 +139,28 @@ mod json_convertible_tests_resource_vote { let recovered = ResourceVote::from_object(value).expect("from_object"); assert_eq!(original, recovered); } + + #[test] + fn should_refuse_a_resource_vote_naming_a_yes_no_poll_from_json() { + use crate::serialization::JsonConvertible; + let yes_no_poll = json!({ + "$type": "yesNoVotePoll", + "resourcePath": ["Y2hhbGxlbmdl"], + "supermajorityNumerator": 2, + "supermajorityDenominator": 3, + "minimumVotingPower": { "$type": "absolute", "votingPower": 0 }, + }); + VotePoll::from_json(yes_no_poll.clone()).expect("expected the yes/no poll to decode"); + let json = json!({ + "$formatVersion": "0", + "votePoll": yes_no_poll, + "resourceVoteChoice": { "$type": "abstain" }, + }); + let error = ResourceVote::from_json(json) + .expect_err("expected a resource vote on a yes/no poll to be refused"); + assert!( + error.to_string().contains("a yes/no vote"), + "unexpected error: {error}" + ); + } } diff --git a/packages/rs-dpp/src/voting/votes/resource_vote/v0/mod.rs b/packages/rs-dpp/src/voting/votes/resource_vote/v0/mod.rs index 3cb27df7c40..cca969d1e3c 100644 --- a/packages/rs-dpp/src/voting/votes/resource_vote/v0/mod.rs +++ b/packages/rs-dpp/src/voting/votes/resource_vote/v0/mod.rs @@ -26,7 +26,8 @@ use serde::{Deserialize, Serialize}; #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde(rename_all = "camelCase") + serde(rename_all = "camelCase"), + serde(try_from = "ResourceVoteV0Inner") )] #[platform_serialize(unversioned)] pub struct ResourceVoteV0 { @@ -34,6 +35,25 @@ pub struct ResourceVoteV0 { pub resource_vote_choice: ResourceVoteChoice, } +/// The fields of a resource vote read from a plain object or JSON, built into one through +/// [`ResourceVoteV0::try_new`] so those conversions refuse a yes/no poll as `try_new` does. +#[cfg(feature = "serde-conversion")] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ResourceVoteV0Inner { + vote_poll: VotePoll, + resource_vote_choice: ResourceVoteChoice, +} + +#[cfg(feature = "serde-conversion")] +impl TryFrom for ResourceVoteV0 { + type Error = ProtocolError; + + fn try_from(value: ResourceVoteV0Inner) -> Result { + ResourceVoteV0::try_new(value.vote_poll, value.resource_vote_choice) + } +} + impl Default for ResourceVoteV0 { fn default() -> Self { ResourceVoteV0 { diff --git a/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs b/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs index e4fd3665a7f..c3f87bb8a81 100644 --- a/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs +++ b/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs @@ -20,17 +20,23 @@ impl YesNoVoteReferenceStorageForm { } /// Decodes the form stored in the identity votes index, a stored form that does not - /// decode being corrupted state. + /// decode, or carries bytes past the form, being corrupted state, as the proof decoder + /// treats it. pub fn deserialize(bytes: &[u8]) -> Result { - bincode::decode_from_slice(bytes, bincode::config::standard().with_big_endian()) - .map(|(form, _)| form) - .map_err(|e| { - Error::Drive(DriveError::CorruptedSerialization(format!( - "serialization of yes/no vote reference {} is corrupted: {}", - hex::encode(bytes), - e - ))) - }) + let corrupted = |reason: String| { + Error::Drive(DriveError::CorruptedSerialization(format!( + "serialization of yes/no vote reference {} is corrupted: {}", + hex::encode(bytes), + reason + ))) + }; + let (form, consumed) = + bincode::decode_from_slice(bytes, bincode::config::standard().with_big_endian()) + .map_err(|e| corrupted(e.to_string()))?; + if consumed != bytes.len() { + return Err(corrupted("it carries trailing bytes".to_string())); + } + Ok(form) } } @@ -50,4 +56,18 @@ mod tests { form ); } + + #[test] + fn should_refuse_trailing_bytes() { + let form = YesNoVoteReferenceStorageForm { + vote_choice: YesNoAbstainVoteChoice::Yes, + identity_vote_times: 1, + }; + let mut bytes = form.serialize().expect("serialize"); + bytes.push(0); + assert!(matches!( + YesNoVoteReferenceStorageForm::deserialize(&bytes), + Err(Error::Drive(DriveError::CorruptedSerialization(_))) + )); + } } diff --git a/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts b/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts index c6296de5114..dcaeb7fb480 100644 --- a/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts +++ b/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts @@ -128,4 +128,27 @@ describe('ResourceVote', () => { vote.free(); yesNoPoll.free(); }); + + it('should refuse a yes/no poll from JSON or an object', () => { + const yesNoPoll = wasm.VotePoll.fromJSON({ + $type: 'yesNoVotePoll', + resourcePath: ['wcHBwQ=='], + supermajorityNumerator: 2, + supermajorityDenominator: 3, + minimumVotingPower: { $type: 'absolute', votingPower: 400 }, + }); + const vote = new wasm.ResourceVote(createPoll(), wasm.ResourceVoteChoice.Abstain()); + + wasm.ResourceVote.fromJSON(vote.toJSON()).free(); + wasm.ResourceVote.fromObject(vote.toObject()).free(); + expect(() => wasm.ResourceVote.fromJSON({ ...vote.toJSON(), votePoll: yesNoPoll.toJSON() })) + .to.throw(); + expect(() => wasm.ResourceVote.fromObject({ + ...vote.toObject(), + votePoll: yesNoPoll.toObject(), + })).to.throw(); + + vote.free(); + yesNoPoll.free(); + }); }); diff --git a/packages/wasm-dpp2/tests/unit/Vote.spec.ts b/packages/wasm-dpp2/tests/unit/Vote.spec.ts index b2209ce3834..2a534ed7f8f 100644 --- a/packages/wasm-dpp2/tests/unit/Vote.spec.ts +++ b/packages/wasm-dpp2/tests/unit/Vote.spec.ts @@ -164,5 +164,23 @@ describe('Vote', () => { vote.free(); yesNoPoll.free(); }); + + it('should refuse a resource vote on a yes/no poll from JSON or an object', () => { + const yesNoPoll = wasm.VotePoll.fromJSON({ + $type: 'yesNoVotePoll', + ...yesNoVoteJSON.votePoll, + }); + const vote = new wasm.Vote(createPoll(), wasm.ResourceVoteChoice.Abstain()); + + wasm.Vote.fromJSON(vote.toJSON()).free(); + wasm.Vote.fromObject(vote.toObject()).free(); + expect(() => wasm.Vote.fromJSON({ ...vote.toJSON(), votePoll: yesNoPoll.toJSON() })) + .to.throw(); + expect(() => wasm.Vote.fromObject({ ...vote.toObject(), votePoll: yesNoPoll.toObject() })) + .to.throw(); + + vote.free(); + yesNoPoll.free(); + }); }); }); From 258d5406c1a18a6a095279f937f31fb3cf2890e6 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 28 Oct 2026 02:06:53 +0300 Subject: [PATCH 12/18] fix(platform): mark every yes/no poll method not active before protocol version 15 The yes/no poll rules (dpp voting), the end-of-block tally, record, clean-up and finish (drive-abci voting), removing finished polls from the end date index and the two yes/no proof verifiers were version 0 in every table, so they looked active at protocol versions that cannot hold a yes/no poll. They are now OptionalFeatureVersion slots, None in every table before protocol version 15 and Some(0) in the tables only 15 uses: VOTING_VERSION_V3, DRIVE_ABCI_METHOD_VERSIONS_V11, DRIVE_VERIFY_METHOD_VERSIONS_V4 (through DRIVE_VERSION_V10) and DRIVE_VOTE_METHOD_VERSIONS_V4. Each dispatcher reports the method not active on None, and should_activate_every_yes_no_vote_poll_method_at_protocol_version_15 checks every slot in every protocol version. Co-Authored-By: Claude Opus 5.5 --- .../yes_no_vote_poll_stored_info/mod.rs | 16 ++- .../voting/vote_polls/yes_no_vote_poll/mod.rs | 48 +++++++- .../mod.rs | 8 +- .../mod.rs | 8 +- .../on_yes_no_vote_poll_finished/mod.rs | 8 +- .../tally_votes_for_yes_no_vote_poll/mod.rs | 10 +- .../mod.rs | 8 +- .../yes_no/open_yes_no_vote_poll/mod.rs | 35 ++++++ .../voting/verify_masternode_vote/mod.rs | 3 +- .../verify_masternode_yes_no_vote/mod.rs | 8 +- .../mod.rs | 8 +- .../dpp_versions/dpp_voting_versions/mod.rs | 18 +-- .../dpp_versions/dpp_voting_versions/v1.rs | 6 +- .../dpp_versions/dpp_voting_versions/v2.rs | 6 +- .../dpp_versions/dpp_voting_versions/v3.rs | 12 ++ .../drive_abci_method_versions/mod.rs | 12 +- .../drive_abci_method_versions/v1.rs | 8 +- .../drive_abci_method_versions/v10.rs | 8 +- .../drive_abci_method_versions/v11.rs | 15 ++- .../drive_abci_method_versions/v2.rs | 8 +- .../drive_abci_method_versions/v3.rs | 8 +- .../drive_abci_method_versions/v4.rs | 8 +- .../drive_abci_method_versions/v5.rs | 8 +- .../drive_abci_method_versions/v6.rs | 8 +- .../drive_abci_method_versions/v7.rs | 8 +- .../drive_abci_method_versions/v8.rs | 8 +- .../drive_abci_method_versions/v9.rs | 8 +- .../drive_verify_method_versions/mod.rs | 12 +- .../drive_verify_method_versions/v1.rs | 4 +- .../drive_verify_method_versions/v4.rs | 17 +++ .../drive_vote_method_versions/mod.rs | 5 +- .../drive_vote_method_versions/v1.rs | 2 +- .../drive_vote_method_versions/v2.rs | 2 +- .../drive_vote_method_versions/v3.rs | 2 +- .../drive_vote_method_versions/v4.rs | 3 + .../src/version/drive_versions/v10.rs | 5 + .../src/version/mocks/v3_test.rs | 8 +- .../src/version/protocol_version.rs | 103 ++++++++++++++++++ .../rs-platform-version/src/version/v15.rs | 14 ++- 39 files changed, 381 insertions(+), 105 deletions(-) create mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v3.rs create mode 100644 packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v4.rs diff --git a/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs index 217bc30d45a..3c7fb1f8d94 100644 --- a/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs +++ b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs @@ -105,12 +105,15 @@ impl YesNoVotePollStoredInfo { .voting_versions .yes_no_vote_poll_stored_info_version { - 0 => Ok(YesNoVotePollStoredInfoV0::new(start_block).into()), - version => Err(ProtocolError::UnknownVersionMismatch { + Some(0) => Ok(YesNoVotePollStoredInfoV0::new(start_block).into()), + Some(version) => Err(ProtocolError::UnknownVersionMismatch { method: "YesNoVotePollStoredInfo::new".to_string(), known_versions: vec![0], received: version, }), + None => Err(ProtocolError::UnknownVersionError( + "YesNoVotePollStoredInfo::new is not active before protocol version 15".to_string(), + )), } } @@ -158,6 +161,15 @@ mod tests { use super::*; use crate::serialization::{PlatformDeserializableTrusted, PlatformSerializable}; + #[test] + fn should_report_the_stored_info_not_active_before_protocol_version_15() { + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); + assert!(matches!( + YesNoVotePollStoredInfo::new(BlockInfo::default(), platform_version), + Err(ProtocolError::UnknownVersionError(_)) + )); + } + #[test] fn should_finalize_a_started_poll_once() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs index 26c1501503f..84b2b0abb94 100644 --- a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs @@ -219,12 +219,16 @@ impl YesNoVotePoll { .voting_versions .yes_no_vote_poll_parameters { - 0 => self.validate_parameters_v0(platform_version), - version => Err(ProtocolError::UnknownVersionMismatch { + Some(0) => self.validate_parameters_v0(platform_version), + Some(version) => Err(ProtocolError::UnknownVersionMismatch { method: "YesNoVotePoll::validate_parameters".to_string(), known_versions: vec![0], received: version, }), + None => Err(ProtocolError::UnknownVersionError( + "YesNoVotePoll::validate_parameters is not active before protocol version 15" + .to_string(), + )), } } @@ -334,12 +338,16 @@ impl YesNoVotePoll { .voting_versions .yes_no_vote_poll_decision { - 0 => Ok(self.required_voting_power_v0(total_voting_power)), - version => Err(ProtocolError::UnknownVersionMismatch { + Some(0) => Ok(self.required_voting_power_v0(total_voting_power)), + Some(version) => Err(ProtocolError::UnknownVersionMismatch { method: "YesNoVotePoll::required_voting_power".to_string(), known_versions: vec![0], received: version, }), + None => Err(ProtocolError::UnknownVersionError( + "YesNoVotePoll::required_voting_power is not active before protocol version 15" + .to_string(), + )), } } @@ -367,12 +375,15 @@ impl YesNoVotePoll { .voting_versions .yes_no_vote_poll_decision { - 0 => Ok(self.passes_v0(yes_voting_power, no_voting_power, total_voting_power)), - version => Err(ProtocolError::UnknownVersionMismatch { + Some(0) => Ok(self.passes_v0(yes_voting_power, no_voting_power, total_voting_power)), + Some(version) => Err(ProtocolError::UnknownVersionMismatch { method: "YesNoVotePoll::passes".to_string(), known_versions: vec![0], received: version, }), + None => Err(ProtocolError::UnknownVersionError( + "YesNoVotePoll::passes is not active before protocol version 15".to_string(), + )), } } @@ -690,6 +701,31 @@ mod tests { assert_eq!(id, tagged); assert_ne!(id, untagged); } + + #[test] + fn should_report_the_poll_rules_not_active_before_protocol_version_15() { + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); + let not_active = |result: Result<(), ProtocolError>, method: &str| { + assert!( + matches!(result, Err(ProtocolError::UnknownVersionError(_))), + "expected {method} to be not active at protocol version 14, got {result:?}" + ); + }; + not_active( + poll().validate_parameters(platform_version), + "validate_parameters", + ); + not_active( + poll() + .required_voting_power(100, platform_version) + .map(|_| ()), + "required_voting_power", + ); + not_active( + poll().passes(10, 0, 100, platform_version).map(|_| ()), + "passes", + ); + } } #[cfg(all( diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs index 70f0446c930..ad26f40b78a 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_yes_no_vote_polls_end/mod.rs @@ -57,18 +57,22 @@ where .voting .clean_up_after_yes_no_vote_polls_end { - 0 => self.clean_up_after_yes_no_vote_polls_end_v0( + Some(0) => self.clean_up_after_yes_no_vote_polls_end_v0( block_info, end_date_entries, finished_vote_polls, transaction, platform_version, ), - version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "clean_up_after_yes_no_vote_polls_end".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Execution(ExecutionError::VersionNotActive { + method: "clean_up_after_yes_no_vote_polls_end".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs index 8f557756569..4e2e7514f20 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/keep_record_of_finished_yes_no_vote_poll/mod.rs @@ -49,7 +49,7 @@ where .voting .keep_record_of_finished_yes_no_vote_poll { - 0 => self.keep_record_of_finished_yes_no_vote_poll_v0( + Some(0) => self.keep_record_of_finished_yes_no_vote_poll_v0( block_info, vote_poll, stored_info, @@ -58,11 +58,15 @@ where transaction, platform_version, ), - version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "keep_record_of_finished_yes_no_vote_poll".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Execution(ExecutionError::VersionNotActive { + method: "keep_record_of_finished_yes_no_vote_poll".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs index 1b7afdd99fe..efcb9eec4c0 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs @@ -41,18 +41,22 @@ where .voting .on_yes_no_vote_poll_finished { - 0 => self.on_yes_no_vote_poll_finished_v0( + Some(0) => self.on_yes_no_vote_poll_finished_v0( block_info, vote_poll, result, transaction, platform_version, ), - version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "on_yes_no_vote_poll_finished".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Execution(ExecutionError::VersionNotActive { + method: "on_yes_no_vote_poll_finished".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs index e4a8f9b64f4..2be5e948cb2 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs @@ -34,12 +34,18 @@ where .voting .tally_votes_for_yes_no_vote_poll { - 0 => self.tally_votes_for_yes_no_vote_poll_v0(vote_poll, transaction, platform_version), - version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + Some(0) => { + self.tally_votes_for_yes_no_vote_poll_v0(vote_poll, transaction, platform_version) + } + Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "tally_votes_for_yes_no_vote_poll".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Execution(ExecutionError::VersionNotActive { + method: "tally_votes_for_yes_no_vote_poll".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs index af72c7da7e7..b32d4043ab3 100644 --- a/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs +++ b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs @@ -37,17 +37,21 @@ impl Drive { .cleanup .remove_vote_poll_end_date_query_operations { - 0 => self.remove_vote_poll_end_date_query_operations_v0( + Some(0) => self.remove_vote_poll_end_date_query_operations_v0( vote_polls, batch_operations, transaction, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "remove_vote_poll_end_date_query_operations".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "remove_vote_poll_end_date_query_operations".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs index d4941dc0c41..5dcfce4a33d 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs @@ -138,8 +138,10 @@ mod tests { IDENTITY_VOTES_TREE_KEY, }; use crate::drive::votes::resolved::votes::resolved_yes_no_vote::ResolvedYesNoVote; + use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; + use crate::query::yes_no_vote_poll_state_query::YesNoVotePollStateDriveQuery; use crate::util::common::encode::encode_u64; use crate::util::grove_operations::{BatchDeleteUpTreeApplyType, DirectQueryType}; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; @@ -153,6 +155,8 @@ mod tests { use dpp::voting::vote_info_storage::yes_no_vote_poll_stored_info::YesNoVotePollStoredInfo; use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; use dpp::voting::vote_polls::VotePoll; + use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0; + use dpp::voting::votes::yes_no_vote::YesNoVote; use grovedb::batch::KeyInfoPath; use grovedb::MaybeTree; @@ -329,6 +333,37 @@ mod tests { ), "remove_yes_no_vote_references_given_by_identity", ); + not_active( + drive.remove_vote_poll_end_date_query_operations( + &[(poll_id, 1_000)], + &mut vec![], + None, + platform_version, + ), + "remove_vote_poll_end_date_query_operations", + ); + not_active( + Drive::verify_masternode_yes_no_vote( + &[], + [1; 32], + &YesNoVote::V0(YesNoVoteV0 { + vote_poll: vote_poll.clone(), + vote_choice: YesNoAbstainVoteChoice::Yes, + }), + false, + platform_version, + ) + .map(|_| ()), + "verify_masternode_yes_no_vote", + ); + not_active( + YesNoVotePollStateDriveQuery { + vote_poll: vote_poll.clone(), + } + .verify_yes_no_vote_poll_state_proof(&[], platform_version) + .map(|_| ()), + "verify_yes_no_vote_poll_state_proof", + ); } #[test] diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs index 9d6d773d7ec..8a7afe7dc56 100644 --- a/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs @@ -54,7 +54,8 @@ impl Drive { // // In place (client side, every version): these arms take only votes naming a yes/no // poll, which no valid transition carries before protocol version 15 (it does not - // decode there). Every contested vote reaches the version match below unchanged. + // decode there), and the yes/no verifier is not active before 15, so the route refuses + // there. Every contested vote reaches the version match below unchanged. match vote { Vote::YesNoVote(yes_no_vote) => { return Self::verify_masternode_yes_no_vote( diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs index 893850118ec..0668f3d8d2d 100644 --- a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs @@ -35,18 +35,22 @@ impl Drive { .voting .verify_masternode_yes_no_vote { - 0 => Drive::verify_masternode_yes_no_vote_v0( + Some(0) => Drive::verify_masternode_yes_no_vote_v0( proof, masternode_pro_tx_hash, vote, verify_subset_of_proof, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "verify_masternode_yes_no_vote".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "verify_masternode_yes_no_vote".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs b/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs index 12940737709..d1167d84cb7 100644 --- a/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs @@ -29,12 +29,16 @@ impl YesNoVotePollStateDriveQuery { .voting .verify_yes_no_vote_poll_state_proof { - 0 => self.verify_yes_no_vote_poll_state_proof_v0(proof, platform_version), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(0) => self.verify_yes_no_vote_poll_state_proof_v0(proof, platform_version), + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "verify_yes_no_vote_poll_state_proof".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "verify_yes_no_vote_poll_state_proof".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs index 35e99653d37..9408dca929a 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs @@ -1,19 +1,21 @@ -use versioned_feature_core::FeatureVersion; +use versioned_feature_core::{FeatureVersion, OptionalFeatureVersion}; pub mod v1; pub mod v2; +pub mod v3; #[derive(Clone, Debug, Default)] pub struct DPPVotingVersions { pub default_vote_poll_time_duration_mainnet_ms: u64, pub default_vote_poll_time_duration_test_network_ms: u64, pub contested_document_vote_poll_stored_info_version: FeatureVersion, - /// The stored info of a yes/no poll (protocol version 15). - pub yes_no_vote_poll_stored_info_version: FeatureVersion, - /// The rules a yes/no poll's parameters must meet (`YesNoVotePoll::validate_parameters`, - /// protocol version 15). - pub yes_no_vote_poll_parameters: FeatureVersion, + /// The stored info of a yes/no poll. `None` (not active) before protocol version 15. + pub yes_no_vote_poll_stored_info_version: OptionalFeatureVersion, + /// The rules a yes/no poll's parameters must meet (`YesNoVotePoll::validate_parameters`). + /// `None` (not active) before protocol version 15. + pub yes_no_vote_poll_parameters: OptionalFeatureVersion, /// How a yes/no poll is decided: its required voting power and whether it passes - /// (`YesNoVotePoll::required_voting_power` and `YesNoVotePoll::passes`, protocol version 15). - pub yes_no_vote_poll_decision: FeatureVersion, + /// (`YesNoVotePoll::required_voting_power` and `YesNoVotePoll::passes`). `None` (not + /// active) before protocol version 15. + pub yes_no_vote_poll_decision: OptionalFeatureVersion, } diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs index 1ab94dde2af..66e25f8e111 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs @@ -4,7 +4,7 @@ pub const VOTING_VERSION_V1: DPPVotingVersions = DPPVotingVersions { default_vote_poll_time_duration_mainnet_ms: 1_209_600_000, //2 weeks default_vote_poll_time_duration_test_network_ms: 1_209_600_000, //2 weeks contested_document_vote_poll_stored_info_version: 0, - yes_no_vote_poll_stored_info_version: 0, - yes_no_vote_poll_parameters: 0, - yes_no_vote_poll_decision: 0, + yes_no_vote_poll_stored_info_version: None, + yes_no_vote_poll_parameters: None, + yes_no_vote_poll_decision: None, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs index 9d246f9ed0c..a064e9e33cc 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs @@ -4,7 +4,7 @@ pub const VOTING_VERSION_V2: DPPVotingVersions = DPPVotingVersions { default_vote_poll_time_duration_mainnet_ms: 1_209_600_000, //2 weeks default_vote_poll_time_duration_test_network_ms: 5_400_000, //90 minutes contested_document_vote_poll_stored_info_version: 0, - yes_no_vote_poll_stored_info_version: 0, - yes_no_vote_poll_parameters: 0, - yes_no_vote_poll_decision: 0, + yes_no_vote_poll_stored_info_version: None, + yes_no_vote_poll_parameters: None, + yes_no_vote_poll_decision: None, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v3.rs new file mode 100644 index 00000000000..3aef3feb762 --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v3.rs @@ -0,0 +1,12 @@ +use crate::version::dpp_versions::dpp_voting_versions::v2::VOTING_VERSION_V2; +use crate::version::dpp_versions::dpp_voting_versions::DPPVotingVersions; + +/// Voting versions 3. Introduced in protocol version 15 with the yes/no poll kind: its stored +/// info, parameter rules and decision rules are `Some(0)` (`None`, not active, in every earlier +/// table). Everything else matches `VOTING_VERSION_V2`. +pub const VOTING_VERSION_V3: DPPVotingVersions = DPPVotingVersions { + yes_no_vote_poll_stored_info_version: Some(0), + yes_no_vote_poll_parameters: Some(0), + yes_no_vote_poll_decision: Some(0), + ..VOTING_VERSION_V2 +}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs index a39498ffd93..8e3ad58748c 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs @@ -179,12 +179,12 @@ pub struct DriveAbciVotingMethodVersions { pub delay_vote_poll: FeatureVersion, pub run_dao_platform_events: FeatureVersion, pub remove_votes_for_removed_masternodes: FeatureVersion, - /// The yes/no poll kind (protocol version 15): its tally, its record, its clean-up and the - /// hook a feature acts on when one of its polls finishes. - pub tally_votes_for_yes_no_vote_poll: FeatureVersion, - pub keep_record_of_finished_yes_no_vote_poll: FeatureVersion, - pub clean_up_after_yes_no_vote_polls_end: FeatureVersion, - pub on_yes_no_vote_poll_finished: FeatureVersion, + /// The yes/no poll kind: its tally, its record, its clean-up and the hook a feature acts on + /// when one of its polls finishes. `None` (not active) before protocol version 15. + pub tally_votes_for_yes_no_vote_poll: OptionalFeatureVersion, + pub keep_record_of_finished_yes_no_vote_poll: OptionalFeatureVersion, + pub clean_up_after_yes_no_vote_polls_end: OptionalFeatureVersion, + pub on_yes_no_vote_poll_finished: OptionalFeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v1.rs index 146c22b2dc2..af852d395eb 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v1.rs @@ -105,10 +105,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V1: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, - tally_votes_for_yes_no_vote_poll: 0, - keep_record_of_finished_yes_no_vote_poll: 0, - clean_up_after_yes_no_vote_polls_end: 0, - on_yes_no_vote_poll_finished: 0, + tally_votes_for_yes_no_vote_poll: None, + keep_record_of_finished_yes_no_vote_poll: None, + clean_up_after_yes_no_vote_polls_end: None, + on_yes_no_vote_poll_finished: None, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs index 72ee21ea5c3..733e2490659 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs @@ -129,10 +129,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V10: DriveAbciMethodVersions = DriveAbciMet delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, - tally_votes_for_yes_no_vote_poll: 0, - keep_record_of_finished_yes_no_vote_poll: 0, - clean_up_after_yes_no_vote_polls_end: 0, - on_yes_no_vote_poll_finished: 0, + tally_votes_for_yes_no_vote_poll: None, + keep_record_of_finished_yes_no_vote_poll: None, + clean_up_after_yes_no_vote_polls_end: None, + on_yes_no_vote_poll_finished: None, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 1, // changed: deducts what a budgeted signing key spent from its remaining budget, and charges a document batch's gas sponsor instead of its signer diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs index c232a2170be..3961aec78ab 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs @@ -1,6 +1,7 @@ use super::v10::DRIVE_ABCI_METHOD_VERSIONS_V10; use super::{ DkgMiningSchedule, DriveAbciCoreChainLockMethodVersionsAndConstants, DriveAbciMethodVersions, + DriveAbciVotingMethodVersions, }; /// Dash Core's DKG schedule (`llmq/params.h`) of the non-rotating quorum types, the ones chain @@ -86,8 +87,11 @@ const DKG_MINING_SCHEDULES_V1: &[DkgMiningSchedule] = &[ }, ]; -/// Drive ABCI method versions 11, introduced in protocol v15. Differs from v10 in the chain lock -/// verification, which only works as a whole: +/// Drive ABCI method versions 11, introduced in protocol v15. Differs from v10 in the yes/no +/// poll kind's voting methods (`voting.tally_votes_for_yes_no_vote_poll`, +/// `keep_record_of_finished_yes_no_vote_poll`, `clean_up_after_yes_no_vote_polls_end` and +/// `on_yes_no_vote_poll_finished` are `Some(0)`, `None` in every earlier table) and in the chain +/// lock verification, which only works as a whole: /// - `core_chain_lock.verify_chain_lock_locally` is 1 rather than 0: a chain lock is rejected /// locally only when Platform holds exactly the quorums active at its signing height, using /// Dash Core's DKG mining schedule in `core_chain_lock.dkg_mining_schedules`. When a quorum may @@ -105,5 +109,12 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V11: DriveAbciMethodVersions = DriveAbciMet dkg_mining_schedules: Some(DKG_MINING_SCHEDULES_V1), ..DRIVE_ABCI_METHOD_VERSIONS_V10.core_chain_lock }, + voting: DriveAbciVotingMethodVersions { + tally_votes_for_yes_no_vote_poll: Some(0), + keep_record_of_finished_yes_no_vote_poll: Some(0), + clean_up_after_yes_no_vote_polls_end: Some(0), + on_yes_no_vote_poll_finished: Some(0), + ..DRIVE_ABCI_METHOD_VERSIONS_V10.voting + }, ..DRIVE_ABCI_METHOD_VERSIONS_V10 }; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v2.rs index 72813322869..02192421217 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v2.rs @@ -106,10 +106,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V2: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, - tally_votes_for_yes_no_vote_poll: 0, - keep_record_of_finished_yes_no_vote_poll: 0, - clean_up_after_yes_no_vote_polls_end: 0, - on_yes_no_vote_poll_finished: 0, + tally_votes_for_yes_no_vote_poll: None, + keep_record_of_finished_yes_no_vote_poll: None, + clean_up_after_yes_no_vote_polls_end: None, + on_yes_no_vote_poll_finished: None, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v3.rs index 66f4576231d..562ef11f09f 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v3.rs @@ -105,10 +105,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V3: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, - tally_votes_for_yes_no_vote_poll: 0, - keep_record_of_finished_yes_no_vote_poll: 0, - clean_up_after_yes_no_vote_polls_end: 0, - on_yes_no_vote_poll_finished: 0, + tally_votes_for_yes_no_vote_poll: None, + keep_record_of_finished_yes_no_vote_poll: None, + clean_up_after_yes_no_vote_polls_end: None, + on_yes_no_vote_poll_finished: None, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v4.rs index a589eb66e23..a80bf22c093 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v4.rs @@ -105,10 +105,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V4: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, - tally_votes_for_yes_no_vote_poll: 0, - keep_record_of_finished_yes_no_vote_poll: 0, - clean_up_after_yes_no_vote_polls_end: 0, - on_yes_no_vote_poll_finished: 0, + tally_votes_for_yes_no_vote_poll: None, + keep_record_of_finished_yes_no_vote_poll: None, + clean_up_after_yes_no_vote_polls_end: None, + on_yes_no_vote_poll_finished: None, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v5.rs index a6274b30c22..df85fd7f139 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v5.rs @@ -109,10 +109,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V5: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, - tally_votes_for_yes_no_vote_poll: 0, - keep_record_of_finished_yes_no_vote_poll: 0, - clean_up_after_yes_no_vote_polls_end: 0, - on_yes_no_vote_poll_finished: 0, + tally_votes_for_yes_no_vote_poll: None, + keep_record_of_finished_yes_no_vote_poll: None, + clean_up_after_yes_no_vote_polls_end: None, + on_yes_no_vote_poll_finished: None, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v6.rs index 1c84779b6ab..4be3341af89 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v6.rs @@ -107,10 +107,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V6: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, - tally_votes_for_yes_no_vote_poll: 0, - keep_record_of_finished_yes_no_vote_poll: 0, - clean_up_after_yes_no_vote_polls_end: 0, - on_yes_no_vote_poll_finished: 0, + tally_votes_for_yes_no_vote_poll: None, + keep_record_of_finished_yes_no_vote_poll: None, + clean_up_after_yes_no_vote_polls_end: None, + on_yes_no_vote_poll_finished: None, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v7.rs index e8edde8f024..78ce2f621e0 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v7.rs @@ -114,10 +114,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V7: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, - tally_votes_for_yes_no_vote_poll: 0, - keep_record_of_finished_yes_no_vote_poll: 0, - clean_up_after_yes_no_vote_polls_end: 0, - on_yes_no_vote_poll_finished: 0, + tally_votes_for_yes_no_vote_poll: None, + keep_record_of_finished_yes_no_vote_poll: None, + clean_up_after_yes_no_vote_polls_end: None, + on_yes_no_vote_poll_finished: None, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v8.rs index 4956752c99d..f176a345790 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v8.rs @@ -116,10 +116,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V8: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, - tally_votes_for_yes_no_vote_poll: 0, - keep_record_of_finished_yes_no_vote_poll: 0, - clean_up_after_yes_no_vote_polls_end: 0, - on_yes_no_vote_poll_finished: 0, + tally_votes_for_yes_no_vote_poll: None, + keep_record_of_finished_yes_no_vote_poll: None, + clean_up_after_yes_no_vote_polls_end: None, + on_yes_no_vote_poll_finished: None, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v9.rs index 17130423c97..7a7b56b48bc 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v9.rs @@ -125,10 +125,10 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V9: DriveAbciMethodVersions = DriveAbciMeth delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, - tally_votes_for_yes_no_vote_poll: 0, - keep_record_of_finished_yes_no_vote_poll: 0, - clean_up_after_yes_no_vote_polls_end: 0, - on_yes_no_vote_poll_finished: 0, + tally_votes_for_yes_no_vote_poll: None, + keep_record_of_finished_yes_no_vote_poll: None, + clean_up_after_yes_no_vote_polls_end: None, + on_yes_no_vote_poll_finished: None, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs index 3be80c1183a..34e55f8bf13 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/mod.rs @@ -1,8 +1,9 @@ -use versioned_feature_core::FeatureVersion; +use versioned_feature_core::{FeatureVersion, OptionalFeatureVersion}; pub mod v1; pub mod v2; pub mod v3; +pub mod v4; #[derive(Clone, Debug, Default)] pub struct DriveVerifyMethodVersions { @@ -187,10 +188,11 @@ pub struct DriveVerifyVoteMethodVersions { pub verify_contests_proof: FeatureVersion, pub verify_vote_polls_by_end_date_proof: FeatureVersion, pub verify_specialized_balance: FeatureVersion, - /// The stored info and tallies of a yes/no poll (protocol version 15). - pub verify_yes_no_vote_poll_state_proof: FeatureVersion, - /// A masternode's vote on a yes/no poll (protocol version 15). - pub verify_masternode_yes_no_vote: FeatureVersion, + /// The stored info and tallies of a yes/no poll. `None` (not active) before protocol + /// version 15. + pub verify_yes_no_vote_poll_state_proof: OptionalFeatureVersion, + /// A masternode's vote on a yes/no poll. `None` (not active) before protocol version 15. + pub verify_masternode_yes_no_vote: OptionalFeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs index 9f16166b4fb..a14c0478825 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v1.rs @@ -131,8 +131,8 @@ pub const DRIVE_VERIFY_METHOD_VERSIONS_V1: DriveVerifyMethodVersions = DriveVeri verify_contests_proof: 0, verify_vote_polls_by_end_date_proof: 0, verify_specialized_balance: 0, - verify_yes_no_vote_poll_state_proof: 0, - verify_masternode_yes_no_vote: 0, + verify_yes_no_vote_poll_state_proof: None, + verify_masternode_yes_no_vote: None, }, address_funds: DriveVerifyAddressFundsMethodVersions { verify_address_info: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v4.rs new file mode 100644 index 00000000000..2eab4d99795 --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v4.rs @@ -0,0 +1,17 @@ +use crate::version::drive_versions::drive_verify_method_versions::v3::DRIVE_VERIFY_METHOD_VERSIONS_V3; +use crate::version::drive_versions::drive_verify_method_versions::{ + DriveVerifyMethodVersions, DriveVerifyVoteMethodVersions, +}; + +/// Version 4 of the Drive verify method versions, introduced in protocol version 15 with the +/// yes/no poll kind: `voting.verify_yes_no_vote_poll_state_proof` and +/// `voting.verify_masternode_yes_no_vote` are `Some(0)` (`None`, not active, in every earlier +/// table). Everything else matches `DRIVE_VERIFY_METHOD_VERSIONS_V3`. +pub const DRIVE_VERIFY_METHOD_VERSIONS_V4: DriveVerifyMethodVersions = DriveVerifyMethodVersions { + voting: DriveVerifyVoteMethodVersions { + verify_yes_no_vote_poll_state_proof: Some(0), + verify_masternode_yes_no_vote: Some(0), + ..DRIVE_VERIFY_METHOD_VERSIONS_V3.voting + }, + ..DRIVE_VERIFY_METHOD_VERSIONS_V3 +}; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs index 38dbfc832e6..41ec3dae38d 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs @@ -64,8 +64,9 @@ pub struct DriveVoteCleanupMethodVersions { /// Removes every vote a removed masternode cast. Version 1 (protocol version 15) also /// sweeps its yes/no votes. pub remove_all_votes_given_by_identities: FeatureVersion, - /// Removes finished polls of every kind from the end date index (protocol version 15). - pub remove_vote_poll_end_date_query_operations: FeatureVersion, + /// Removes finished polls of every kind from the end date index. `None` (not active) before + /// protocol version 15. + pub remove_vote_poll_end_date_query_operations: OptionalFeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs index c30c7a6a3d7..be4d1742316 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs @@ -23,7 +23,7 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V1: DriveVoteMethodVersions = DriveVoteMeth remove_contested_resource_top_level_index_operations: 0, remove_contested_resource_info_operations: 0, remove_all_votes_given_by_identities: 0, - remove_vote_poll_end_date_query_operations: 0, + remove_vote_poll_end_date_query_operations: None, }, setup: DriveVoteSetupMethodVersions { add_initial_vote_tree_main_structure_operations: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs index f95a0e43da7..aa35e886477 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs @@ -23,7 +23,7 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V2: DriveVoteMethodVersions = DriveVoteMeth remove_contested_resource_top_level_index_operations: 0, remove_contested_resource_info_operations: 0, remove_all_votes_given_by_identities: 0, - remove_vote_poll_end_date_query_operations: 0, + remove_vote_poll_end_date_query_operations: None, }, setup: DriveVoteSetupMethodVersions { add_initial_vote_tree_main_structure_operations: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs index addf338b709..ddaef80652a 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs @@ -31,7 +31,7 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V3: DriveVoteMethodVersions = DriveVoteMeth remove_contested_resource_top_level_index_operations: 0, remove_contested_resource_info_operations: 0, remove_all_votes_given_by_identities: 0, - remove_vote_poll_end_date_query_operations: 0, + remove_vote_poll_end_date_query_operations: None, }, setup: DriveVoteSetupMethodVersions { add_initial_vote_tree_main_structure_operations: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs index 0bdeafcd8f3..4f6e3ae922f 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs @@ -11,6 +11,8 @@ use crate::version::drive_versions::drive_vote_method_versions::{ /// contested resource votes, which v1 still hands to it). /// * `remove_all_votes_given_by_identities` 1 also removes the yes/no votes of a masternode /// that left the list. +/// * `cleanup.remove_vote_poll_end_date_query_operations` is `Some(0)`: the yes/no clean-up +/// removes finished polls from the end date index with it (`None`, not active, before). /// * every `yes_no` method is `Some(0)`: yes/no polls can be opened and voted on from this /// version (`None`, not active, in every earlier table). /// * `contested_resource_insert.add_vote_poll_end_date_query_operations` 1 adds only the entry @@ -28,6 +30,7 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V4: DriveVoteMethodVersions = DriveVoteMeth }, cleanup: DriveVoteCleanupMethodVersions { remove_all_votes_given_by_identities: 1, + remove_vote_poll_end_date_query_operations: Some(0), ..DRIVE_VOTE_METHOD_VERSIONS_V3.cleanup }, yes_no: DriveVoteYesNoMethodVersions { diff --git a/packages/rs-platform-version/src/version/drive_versions/v10.rs b/packages/rs-platform-version/src/version/drive_versions/v10.rs index 1242bdde499..ad977d60f77 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -1,3 +1,4 @@ +use crate::version::drive_versions::drive_verify_method_versions::v4::DRIVE_VERIFY_METHOD_VERSIONS_V4; use crate::version::drive_versions::drive_vote_method_versions::v4::DRIVE_VOTE_METHOD_VERSIONS_V4; use crate::version::drive_versions::v9::DRIVE_VERSION_V9; use crate::version::drive_versions::{DriveMethodVersions, DriveVersion}; @@ -12,11 +13,15 @@ use crate::version::drive_versions::{DriveMethodVersions, DriveVersion}; /// polls can be opened and voted on (`None`, not active, in every earlier table), and bumps /// `contested_resource_insert.add_vote_poll_end_date_query_operations` to 1, which adds only /// the entry when the batch already queues the end date's tree with other storage flags. +/// `cleanup.remove_vote_poll_end_date_query_operations`, which removes finished polls from +/// the end date index, is `Some(0)` (`None` before). `DRIVE_VERIFY_METHOD_VERSIONS_V4` makes +/// the yes/no proof verifiers `Some(0)` (`None` before). /// /// Everything else matches `DRIVE_VERSION_V9`. pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { methods: DriveMethodVersions { vote: DRIVE_VOTE_METHOD_VERSIONS_V4, // changed in v10: the yes/no poll kind + verify: DRIVE_VERIFY_METHOD_VERSIONS_V4, // changed in v10: the yes/no proof verifiers ..DRIVE_VERSION_V9.methods }, ..DRIVE_VERSION_V9 diff --git a/packages/rs-platform-version/src/version/mocks/v3_test.rs b/packages/rs-platform-version/src/version/mocks/v3_test.rs index 0f892f250c4..9a502004273 100644 --- a/packages/rs-platform-version/src/version/mocks/v3_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v3_test.rs @@ -140,10 +140,10 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { delay_vote_poll: 0, run_dao_platform_events: 0, remove_votes_for_removed_masternodes: 0, - tally_votes_for_yes_no_vote_poll: 0, - keep_record_of_finished_yes_no_vote_poll: 0, - clean_up_after_yes_no_vote_polls_end: 0, - on_yes_no_vote_poll_finished: 0, + tally_votes_for_yes_no_vote_poll: None, + keep_record_of_finished_yes_no_vote_poll: None, + clean_up_after_yes_no_vote_polls_end: None, + on_yes_no_vote_poll_finished: None, }, state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { execute_event: 0, diff --git a/packages/rs-platform-version/src/version/protocol_version.rs b/packages/rs-platform-version/src/version/protocol_version.rs index d6273f0e766..9b4eff82afe 100644 --- a/packages/rs-platform-version/src/version/protocol_version.rs +++ b/packages/rs-platform-version/src/version/protocol_version.rs @@ -325,3 +325,106 @@ mod token_math_gating_tests { ); } } + +#[cfg(test)] +mod yes_no_vote_poll_gating_tests { + use super::*; + + // Every method of the yes/no poll kind is `None` (not active) before protocol version 15 + // and `Some(0)` from it, so a caller on an older version is refused at the method instead + // of running the protocol 15 implementation. + #[test] + fn should_activate_every_yes_no_vote_poll_method_at_protocol_version_15() { + for platform_version in PLATFORM_VERSIONS { + let expected = (platform_version.protocol_version >= 15).then_some(0); + let voting = &platform_version.dpp.voting_versions; + let abci_voting = &platform_version.drive_abci.methods.voting; + let drive_methods = &platform_version.drive.methods; + let yes_no = &drive_methods.vote.yes_no; + let slots = [ + ( + "yes_no_vote_poll_stored_info_version", + voting.yes_no_vote_poll_stored_info_version, + ), + ( + "yes_no_vote_poll_parameters", + voting.yes_no_vote_poll_parameters, + ), + ( + "yes_no_vote_poll_decision", + voting.yes_no_vote_poll_decision, + ), + ( + "tally_votes_for_yes_no_vote_poll", + abci_voting.tally_votes_for_yes_no_vote_poll, + ), + ( + "keep_record_of_finished_yes_no_vote_poll", + abci_voting.keep_record_of_finished_yes_no_vote_poll, + ), + ( + "clean_up_after_yes_no_vote_polls_end", + abci_voting.clean_up_after_yes_no_vote_polls_end, + ), + ( + "on_yes_no_vote_poll_finished", + abci_voting.on_yes_no_vote_poll_finished, + ), + ( + "verify_yes_no_vote_poll_state_proof", + drive_methods + .verify + .voting + .verify_yes_no_vote_poll_state_proof, + ), + ( + "verify_masternode_yes_no_vote", + drive_methods.verify.voting.verify_masternode_yes_no_vote, + ), + ( + "remove_vote_poll_end_date_query_operations", + drive_methods + .vote + .cleanup + .remove_vote_poll_end_date_query_operations, + ), + ("open_yes_no_vote_poll", yes_no.open_yes_no_vote_poll), + ( + "register_yes_no_identity_vote", + yes_no.register_yes_no_identity_vote, + ), + ( + "insert_stored_info_for_yes_no_vote_poll", + yes_no.insert_stored_info_for_yes_no_vote_poll, + ), + ( + "fetch_yes_no_vote_poll_stored_info", + yes_no.fetch_yes_no_vote_poll_stored_info, + ), + ( + "fetch_identity_yes_no_vote", + yes_no.fetch_identity_yes_no_vote, + ), + ( + "fetch_identities_voting_in_yes_no_vote_poll", + yes_no.fetch_identities_voting_in_yes_no_vote_poll, + ), + ( + "remove_yes_no_vote_poll_votes_operations", + yes_no.remove_yes_no_vote_poll_votes_operations, + ), + ( + "remove_yes_no_vote_references_given_by_identity", + yes_no.remove_yes_no_vote_references_given_by_identity, + ), + ]; + for (method, version) in slots { + assert_eq!( + version, expected, + "{method} at protocol version {}", + platform_version.protocol_version + ); + } + } + } +} diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index f5d5b185a6d..542c240bfd3 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -12,7 +12,7 @@ use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v use crate::version::dpp_versions::dpp_state_transition_versions::v4::STATE_TRANSITION_VERSIONS_V4; use crate::version::dpp_versions::dpp_token_versions::v3::TOKEN_VERSIONS_V3; use crate::version::dpp_versions::dpp_validation_versions::v5::DPP_VALIDATION_VERSIONS_V5; -use crate::version::dpp_versions::dpp_voting_versions::v2::VOTING_VERSION_V2; +use crate::version::dpp_versions::dpp_voting_versions::v3::VOTING_VERSION_V3; use crate::version::dpp_versions::DPPVersion; use crate::version::drive_abci_versions::drive_abci_checkpoint_parameters::v1::DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1; use crate::version::drive_abci_versions::drive_abci_method_versions::v11::DRIVE_ABCI_METHOD_VERSIONS_V11; @@ -176,14 +176,20 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// opening block, and its end date entry goes through /// `add_vote_poll_end_date_query_operations` 1, which adds only the entry when /// the batch already queues the end date's tree with other storage flags. +/// Every other yes/no method is active from this version too and `None` +/// (not active) in every earlier table: the poll rules (`VOTING_VERSION_V3`), +/// the end-of-block tally, record and clean-up (`DRIVE_ABCI_METHOD_VERSIONS_V11` +/// voting), removing finished polls from the end date index +/// (`remove_vote_poll_end_date_query_operations`) and the proof verifiers +/// (`DRIVE_VERIFY_METHOD_VERSIONS_V4`). /// Nothing calls the opener yet, so at this version every yes/no vote is /// refused for want of a poll. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, - drive: DRIVE_VERSION_V10, // changed: vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, every yes_no method Some(0) makes yes/no polls active and add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags (note 8) + drive: DRIVE_VERSION_V10, // changed: vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, every yes_no method and remove_vote_poll_end_date_query_operations Some(0) make yes/no polls active, add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags, and verify methods v4 make the yes/no proof verifiers active (note 8) drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, - methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock + methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: the yes/no poll's tally, record, clean-up and finish hook are Some(0) (note 8); verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: identity update advanced_structure 1 refuses unpaid an identity update whose added key's proof of possession does not verify (note 2); create_domain_data_trigger 2 (note 3); masternode vote transform_into_action 1 looks up a masternode's previous answer on a yes/no poll (note 8) withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, query: DRIVE_ABCI_QUERY_VERSIONS_V2, @@ -199,7 +205,7 @@ pub const PLATFORM_V15: PlatformVersion = PlatformVersion { contract_versions: CONTRACT_VERSIONS_V7, // changed: parse_property_constraints 1 lets countOf and sumOf match byte array keys (note 4); validate_document 2 checks documents natively (note 6); check_document_type_schema 1 checks schemas natively at registration (note 7) document_versions: DOCUMENT_VERSIONS_V4, identity_versions: IDENTITY_VERSIONS_V1, - voting_versions: VOTING_VERSION_V2, + voting_versions: VOTING_VERSION_V3, // changed: the yes/no poll's stored info, parameter rules and decision rules are active (note 8) token_versions: TOKEN_VERSIONS_V3, asset_lock_versions: DPP_ASSET_LOCK_VERSIONS_V1, methods: DPP_METHOD_VERSIONS_V3, From 4a7f57f6c68a709ea2c4259d1412bc2be2584e7b Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 28 Oct 2026 06:37:41 +0300 Subject: [PATCH 13/18] fix(platform): read the yes/no vote weights from SystemLimits and build resource votes only through try_new The yes/no vote strength (1 for a masternode, 4 for an evonode) and the masternode list total a share minimum is measured against now read yes_no_vote_poll_masternode_vote_strength and yes_no_vote_poll_evonode_vote_strength from the protocol version's SystemLimits, so a later version changes them in its own table instead of editing the helper protocol version 15 tallies with. Every table carries 1 and 4; nothing reads them before 15, and a test pins them at 15. The WASM Vote and ResourceVote choice setters rebuild the vote through ResourceVoteV0::try_new like the constructors and poll setters, so no binding path skips its checks. Co-Authored-By: Claude Opus 5.5 --- .../check_for_ended_vote_polls/v1/mod.rs | 4 ++- .../execution/platform_events/voting/mod.rs | 30 +++++++++++-------- .../transform_into_action/v1/mod.rs | 2 +- .../masternode_vote/yes_no_tests.rs | 7 ++++- .../src/version/mocks/v2_test.rs | 2 ++ .../src/version/protocol_version.rs | 12 ++++++++ .../src/version/system_limits/mod.rs | 8 +++++ .../src/version/system_limits/v1.rs | 2 ++ .../src/version/system_limits/v2.rs | 2 ++ .../src/version/system_limits/v3.rs | 2 ++ .../src/version/system_limits/v4.rs | 2 ++ .../wasm-dpp2/src/voting/resource_vote.rs | 8 ++--- packages/wasm-dpp2/src/voting/vote.rs | 7 ++--- 13 files changed, 63 insertions(+), 25 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs index 44e2142fc81..b9edb93d0cc 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs @@ -328,7 +328,9 @@ where .full_masternode_list() .values() .try_fold(0 as VotingPower, |total, masternode| { - total.checked_add(masternode_vote_strength(&masternode.node_type).into()) + total.checked_add( + masternode_vote_strength(&masternode.node_type, platform_version).into(), + ) }) .ok_or(Error::Execution(ExecutionError::Overflow( "the masternode list's total voting power overflows", diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs index da01983e426..4d559da2d40 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/mod.rs @@ -1,4 +1,5 @@ use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeType; +use dpp::version::PlatformVersion; mod award_document_to_winner; mod check_for_ended_vote_polls; @@ -13,19 +14,22 @@ mod run_dao_platform_events; mod tally_votes_for_contested_document_resource_vote_poll; mod tally_votes_for_yes_no_vote_poll; -/// The voting power a masternode's yes/no vote carries: 1 for a masternode, 4 for an evonode, -/// as for contested polls. The strength of each yes/no vote and the masternode list total that a -/// share minimum is measured against both come from here, so they always use the same weights. -/// The contested vote strength of masternode vote `transform_into_action` v0 (protocol versions -/// 1 to 14, and every contested vote at 15) keeps its own copy of these weights. -/// -/// Protocol version 15 reads these weights for every yes/no vote (`transform_into_action` v1) -/// and for the masternode list total a share minimum is judged against -/// (`check_for_ended_vote_polls` v1), so once it ships they must not change: other weights for a -/// later version go in a new helper that version selects. -pub(in crate::execution) fn masternode_vote_strength(node_type: &MasternodeType) -> u8 { +/// The voting power a masternode's yes/no vote carries, read from the protocol version's +/// `SystemLimits` (`yes_no_vote_poll_masternode_vote_strength` and +/// `yes_no_vote_poll_evonode_vote_strength`: 1 for a masternode, 4 for an evonode, as for +/// contested polls). The strength of each yes/no vote (`transform_into_action` v1) and the +/// masternode list total a share minimum is measured against (`check_for_ended_vote_polls` v1) +/// both come from here, so they always use the same weights; a later protocol version changes +/// them in its own `SystemLimits` table. The contested vote strength of masternode vote +/// `transform_into_action` v0 (protocol versions 1 to 14, and every contested vote at 15) keeps +/// its own copy of these weights. +pub(in crate::execution) fn masternode_vote_strength( + node_type: &MasternodeType, + platform_version: &PlatformVersion, +) -> u8 { + let limits = &platform_version.system_limits; match node_type { - MasternodeType::Regular => 1, - MasternodeType::Evo => 4, + MasternodeType::Regular => limits.yes_no_vote_poll_masternode_vote_strength, + MasternodeType::Evo => limits.yes_no_vote_poll_evonode_vote_strength, } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs index c99767032dd..bc9ee66048d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs @@ -110,7 +110,7 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV1 for Masternode pro_tx_hash: self.pro_tx_hash(), voter_identity_id: self.voter_identity_id(), voting_address: masternode.state.voting_address, - vote_strength: masternode_vote_strength(&masternode.node_type), + vote_strength: masternode_vote_strength(&masternode.node_type, platform_version), vote: ResolvedVote::YesNoVote(resolved_vote), previous_resource_vote_choice_to_remove: None, nonce: self.nonce(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs index 2f46ee55d32..1366261cc4f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs @@ -547,7 +547,12 @@ async fn should_judge_a_share_minimum_against_the_masternode_list_at_close() { platform_state .full_masternode_list() .values() - .map(|masternode| u32::from(masternode_vote_strength(&masternode.node_type))) + .map(|masternode| { + u32::from(masternode_vote_strength( + &masternode.node_type, + platform_version, + )) + }) .sum() }; assert_eq!( diff --git a/packages/rs-platform-version/src/version/mocks/v2_test.rs b/packages/rs-platform-version/src/version/mocks/v2_test.rs index a03c3db6158..79410eb3aea 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -621,6 +621,8 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { max_yes_no_vote_poll_minimum_voting_power: 2000, max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, max_yes_no_vote_poll_duration_ms: 2_419_200_000, // four weeks + yes_no_vote_poll_masternode_vote_strength: 1, + yes_no_vote_poll_evonode_vote_strength: 4, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_mainnet_contract_moderation_election_window_seconds: 86_400, diff --git a/packages/rs-platform-version/src/version/protocol_version.rs b/packages/rs-platform-version/src/version/protocol_version.rs index 9b4eff82afe..155b7a6ee58 100644 --- a/packages/rs-platform-version/src/version/protocol_version.rs +++ b/packages/rs-platform-version/src/version/protocol_version.rs @@ -427,4 +427,16 @@ mod yes_no_vote_poll_gating_tests { } } } + + // Protocol version 15 tallies yes/no votes and share minimums with these weights, so they + // cannot change in its table without changing how its blocks replay. + #[test] + fn should_weigh_yes_no_votes_one_for_a_masternode_and_four_for_an_evonode_at_protocol_version_15( + ) { + let limits = &PlatformVersion::get(15) + .expect("expected protocol version 15") + .system_limits; + assert_eq!(limits.yes_no_vote_poll_masternode_vote_strength, 1); + assert_eq!(limits.yes_no_vote_poll_evonode_vote_strength, 4); + } } diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index 8f4001454c5..2e802505b8a 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -260,6 +260,14 @@ pub struct SystemLimits { /// its end date: four weeks, the longest election window a contract may declare. Read by /// `Drive::open_yes_no_vote_poll` (protocol version 15). pub max_yes_no_vote_poll_duration_ms: u64, + /// The voting power a regular masternode's yes/no vote carries, and what it adds to the + /// masternode list total a share minimum is measured against. Read by masternode vote + /// `transform_into_action` v1 and `check_for_ended_vote_polls` v1 (protocol version 15). + pub yes_no_vote_poll_masternode_vote_strength: u8, + /// The voting power an evonode's yes/no vote carries, and what it adds to the masternode + /// list total a share minimum is measured against. Read by masternode vote + /// `transform_into_action` v1 and `check_for_ended_vote_polls` v1 (protocol version 15). + pub yes_no_vote_poll_evonode_vote_strength: u8, /// Maximum number of warnings one identity may carry on a contract's warning list at a /// time: a warn that would exceed it is refused until the warnings are cleared. Read by /// the `ContractUserModeration` state validation v0 (protocol version 14) and never diff --git a/packages/rs-platform-version/src/version/system_limits/v1.rs b/packages/rs-platform-version/src/version/system_limits/v1.rs index c4fcd9f6fbb..a963b2a986a 100644 --- a/packages/rs-platform-version/src/version/system_limits/v1.rs +++ b/packages/rs-platform-version/src/version/system_limits/v1.rs @@ -67,6 +67,8 @@ pub const SYSTEM_LIMITS_V1: SystemLimits = SystemLimits { max_yes_no_vote_poll_minimum_voting_power: 2000, max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, max_yes_no_vote_poll_duration_ms: 2_419_200_000, // four weeks + yes_no_vote_poll_masternode_vote_strength: 1, + yes_no_vote_poll_evonode_vote_strength: 4, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_mainnet_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/system_limits/v2.rs b/packages/rs-platform-version/src/version/system_limits/v2.rs index 54849cffd69..e5af72e9c02 100644 --- a/packages/rs-platform-version/src/version/system_limits/v2.rs +++ b/packages/rs-platform-version/src/version/system_limits/v2.rs @@ -48,6 +48,8 @@ pub const SYSTEM_LIMITS_V2: SystemLimits = SystemLimits { max_yes_no_vote_poll_minimum_voting_power: 2000, max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, max_yes_no_vote_poll_duration_ms: 2_419_200_000, // four weeks + yes_no_vote_poll_masternode_vote_strength: 1, + yes_no_vote_poll_evonode_vote_strength: 4, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_mainnet_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/system_limits/v3.rs b/packages/rs-platform-version/src/version/system_limits/v3.rs index 1348f44a556..7c303a128ce 100644 --- a/packages/rs-platform-version/src/version/system_limits/v3.rs +++ b/packages/rs-platform-version/src/version/system_limits/v3.rs @@ -50,6 +50,8 @@ pub const SYSTEM_LIMITS_V3: SystemLimits = SystemLimits { max_yes_no_vote_poll_minimum_voting_power: 2000, max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, max_yes_no_vote_poll_duration_ms: 2_419_200_000, // four weeks + yes_no_vote_poll_masternode_vote_strength: 1, + yes_no_vote_poll_evonode_vote_strength: 4, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_mainnet_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/rs-platform-version/src/version/system_limits/v4.rs b/packages/rs-platform-version/src/version/system_limits/v4.rs index 6d09ace5ac0..75c3b9cb177 100644 --- a/packages/rs-platform-version/src/version/system_limits/v4.rs +++ b/packages/rs-platform-version/src/version/system_limits/v4.rs @@ -171,6 +171,8 @@ pub const SYSTEM_LIMITS_V4: SystemLimits = SystemLimits { max_yes_no_vote_poll_minimum_voting_power: 2000, max_yes_no_vote_poll_minimum_voting_power_percent_of_total: 50, max_yes_no_vote_poll_duration_ms: 2_419_200_000, // four weeks + yes_no_vote_poll_masternode_vote_strength: 1, + yes_no_vote_poll_evonode_vote_strength: 4, max_contract_warnings_per_identity: 16, max_contract_moderation_reason_documents: 16, min_mainnet_contract_moderation_election_window_seconds: 86_400, // one day diff --git a/packages/wasm-dpp2/src/voting/resource_vote.rs b/packages/wasm-dpp2/src/voting/resource_vote.rs index e7f8bc1774e..2054f8a5dc5 100644 --- a/packages/wasm-dpp2/src/voting/resource_vote.rs +++ b/packages/wasm-dpp2/src/voting/resource_vote.rs @@ -93,13 +93,11 @@ impl ResourceVoteWasm { } #[wasm_bindgen(setter = choice)] - pub fn set_choice(&mut self, choice: &ResourceVoteChoiceWasm) { + pub fn set_choice(&mut self, choice: &ResourceVoteChoiceWasm) -> WasmDppResult<()> { let ResourceVote::V0(ResourceVoteV0 { vote_poll, .. }) = self.0.clone(); - self.0 = ResourceVote::V0(ResourceVoteV0 { - vote_poll, - resource_vote_choice: choice.clone().into(), - }); + self.0 = ResourceVote::V0(ResourceVoteV0::try_new(vote_poll, choice.clone().into())?); + Ok(()) } } diff --git a/packages/wasm-dpp2/src/voting/vote.rs b/packages/wasm-dpp2/src/voting/vote.rs index ceda8c4a4e9..229ab1c3853 100644 --- a/packages/wasm-dpp2/src/voting/vote.rs +++ b/packages/wasm-dpp2/src/voting/vote.rs @@ -162,10 +162,9 @@ impl VoteWasm { #[wasm_bindgen(setter = choice)] pub fn set_choice(&mut self, choice: &ResourceVoteChoiceWasm) -> WasmDppResult<()> { self.0 = match self.0.clone() { - Vote::ResourceVote(vote) => Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { - vote_poll: vote.vote_poll().clone(), - resource_vote_choice: choice.clone().into(), - })), + Vote::ResourceVote(vote) => Vote::ResourceVote(ResourceVote::V0( + ResourceVoteV0::try_new(vote.vote_poll().clone(), choice.clone().into())?, + )), Vote::YesNoVote(_) => return Err(not_a_resource_vote()), }; From 00eb4969719dd6a7a8241c5a9de4452002539aa0 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 28 Oct 2026 09:04:29 +0300 Subject: [PATCH 14/18] test(drive-abci): move the nullifier batch guard's latest-version root pins with the vote decisions trees The two single-entry characterization tests pin GroveDB roots at PlatformVersion::latest(). A chain born at protocol version 15 creates the vote decisions trees at genesis, so the root and the nine settled hashes change; the nine validation hashes (actions, errors, billed context and lowered operations) do not. With the genesis decisions trees left out, both tests pass on the previous values. Co-Authored-By: Claude Opus 5.5 --- .../token/shielded/nullifier_batch_guard.rs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs index a637f1d36c2..02cddece6e2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs @@ -734,7 +734,7 @@ async fn should_preserve_single_entry_full_processor_fees_and_result() { .value .unwrap() ), - "2b58577a1cdb38e9da1f3fe0b16556f15cd11f851364bf8e404dfb4f14b4410a" + "f4ca287e6d7378ead384eab764303eb345336099c8724fa9d8988bfe4181f0ab" ); assert_eq!( pool.platform @@ -2158,39 +2158,39 @@ async fn should_preserve_single_entry_actions_errors_operations_and_settled_stat let (expected_validation, expected_settled) = match case { "shield" => ( "7e3cf25f4080c94ef028ede8d3bdc982d0055feaef0d9e75dcae11287146d425", - "4ba6e38cdbdda59c48644b8e0df3182a805bd87a7b3584906df8a84008c7df36", + "f117201477ff1f2662fe3de6e01689d8a1859501fbbc987e4a4120ccf3cfe637", ), "valid-payment" => ( "da69c2e10a0fe88df56655907ae5c3acd6700f13823358159f6240b1fb33c6d1", - "1eb8b109ddb6a2d8896b113387132673e42f18f9abe560a607607593517ee3a3", + "d5d680a8cfbe135a6bfe5a9f8e7daaa710d63c21eab7527469ae3996b19fde45", ), "failed-payment" => ( "d9b5fe191841bea952494f2a45e3833f925f4d367a1937b2ef1235a1aa01f51e", - "9f22a694033eb8c899df1fbad5c34bc97710ebad14aa2aff2dc495024bb6685b", + "a7f05be975dbcc0c806e391eb2f93dad15832e46e5c24a342d6cb7f665985b84", ), "within-bundle-repeat" => ( "8023e03a697a20f6a78396a05503a4bc583ca8f69e19bd7ee07a6ae0ce7be478", - "4a7e0f9b38b87219f276b6e54a4c0ace51245c23201057a0846be726f6854089", + "9dbae7b0536d906c23d8a9f79cae8df5346912984914a0cf2e9480494a91bf8b", ), "stored-repeat" => ( "05652ac85c972902e8c09d20a7d8586e17e9434a478b335da136ad85f73def01", - "63be3fcbb363b244ab581de6cbaf38e50fd042bf8c276745ef62c2b5031abd53", + "bba2e610a23979fc0b4367dccb702378e467ebb23ea437e367f1df319e719056", ), "open-mint-group" => ( "7f694d1f3507aaca0c5b5b1254eef3f8cd595379c23c32dea1d0a9c21d302088", - "637c59bb64c308336fb7de597fb34bb3137a39f9b45996b2ebbf238e7d7372ec", + "c73f45063c6699ec92bfc767a2afddcc659497a4e89bc5731a89d9d66d0d7ce1", ), "closing-mint-group" => ( "1901253314ac7c543ffa24ae22b91f6790365b66bfa9b477dce90bacd3c97e75", - "0c0789e5dc329c11b67d202552957cd81cc4ca78feee6c019d1e6e519bcd7439", + "7b734f5f97b612d1e9c112debb86b7da09a49cc7c206928c8f4f00b0bfc5030c", ), "open-burn-group" => ( "eb4786ab90fb36ffc33d8167e199316929ef28cceb0dcff61e6612cd6fcc1d73", - "9485f54bfbbe8c73408d48186132b9f1fc8e5fc37685712bda30bc6478d29cdc", + "9e057f6fd9888843b98eefc9cd39696498197fef35fc1c5eda4d2de21ab4a078", ), "closing-burn-group" => ( "dbb52b37ff8c4b451cf36ee3a6a8b1d299d0f786f55cad6c700ef1b947bc1179", - "24fe37d9d5e2a81c90c7415007d6351f87aa253c051dcc75c473a4b69b33d71e", + "0bb87ba191917936fd0a141af283421c5cd6cfb7c0b309fbdcf0dea698b23a4d", ), _ => unreachable!("enumerated characterization case"), }; From 88df31ed5ff6877faa5d4b28e80760c68fa9f4c4 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 28 Oct 2026 09:04:29 +0300 Subject: [PATCH 15/18] refactor(platform): name the yes/no vote poll activation version YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION (15) joins the other feature activation versions. The masternode vote's active_version_range for a yes/no poll, the genesis guard that creates the vote decisions trees and the gating tests read it instead of a literal 15. Co-Authored-By: Claude Opus 5.5 --- packages/rs-dpp/src/state_transition/mod.rs | 12 +++++++++--- packages/rs-drive/src/drive/initialization/v4/mod.rs | 3 ++- .../src/version/feature_initial_protocol_versions.rs | 5 +++++ .../src/version/protocol_version.rs | 7 +++++-- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 1379729a6d5..392a3423311 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -19,6 +19,7 @@ use dashcore::signer::double_sha; use platform_serialization_derive::{ PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, PlatformSignable, }; +use platform_version::version::feature_initial_protocol_versions::YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION; use platform_version::version::{PlatformVersion, ProtocolVersion, ALL_VERSIONS, LATEST_VERSION}; #[cfg(any( @@ -1227,9 +1228,11 @@ impl StateTransition { StateTransition::MasternodeVote(masternode_vote) => match masternode_vote.vote() { Vote::ResourceVote(resource_vote) => match resource_vote.vote_poll() { VotePoll::ContestedDocumentResourceVotePoll(_) => ALL_VERSIONS, - VotePoll::YesNoVotePoll(_) => 15..=LATEST_VERSION, + VotePoll::YesNoVotePoll(_) => { + YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION..=LATEST_VERSION + } }, - Vote::YesNoVote(_) => 15..=LATEST_VERSION, + Vote::YesNoVote(_) => YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION..=LATEST_VERSION, }, StateTransition::IdentityCreditTransferToAddresses(_) | StateTransition::IdentityTopUpFromAddresses(_) @@ -2796,7 +2799,10 @@ mod tests { }; let state_transition = StateTransition::MasternodeVote(MasternodeVoteTransition::V0(v0)); - assert_eq!(state_transition.active_version_range(), 15..=LATEST_VERSION); + assert_eq!( + state_transition.active_version_range(), + YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION..=LATEST_VERSION + ); let bytes = state_transition .serialize_to_bytes() diff --git a/packages/rs-drive/src/drive/initialization/v4/mod.rs b/packages/rs-drive/src/drive/initialization/v4/mod.rs index 494ec4333f5..6f4998761b2 100644 --- a/packages/rs-drive/src/drive/initialization/v4/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v4/mod.rs @@ -4,6 +4,7 @@ use crate::drive::{Drive, RootTree}; use crate::error::Error; +use dpp::version::feature_initial_protocol_versions::YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION; use dpp::version::PlatformVersion; use grovedb::{TransactionArg, TreeType}; use grovedb_path::SubtreePath; @@ -132,7 +133,7 @@ impl Drive { // born at 14 replays its genesis without them and gets them from its upgrade rung. The // upgrade path (`Platform::transition_to_version_15`) calls the same helper, so both // node populations build the decisions Merk from the same sequence of inserts. - if platform_version.protocol_version >= 15 { + if platform_version.protocol_version >= YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION { self.insert_vote_decisions_trees(transaction, platform_version)?; } diff --git a/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs b/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs index 39632330672..6d2b45d5ea8 100644 --- a/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs +++ b/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs @@ -35,3 +35,8 @@ pub const MODERATION_CHARTERS_CONTRACT_INITIAL_PROTOCOL_VERSION: ProtocolVersion /// Below it a token configuration or a configuration update that makes a token tradeable is /// refused without a fee, as software that predates the marketplace cannot decode one. pub const TOKEN_MARKETPLACE_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 15; +/// Yes/no masternode vote polls activate with protocol version 15: a masternode vote naming one +/// (a `YesNoVote`, or a resource vote on a `YesNoVotePoll`) is not active before it, every +/// yes/no method slot is `None` in the tables before it, and the vote decisions trees are +/// created at genesis from it (and by the upgrade to it). +pub const YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 15; diff --git a/packages/rs-platform-version/src/version/protocol_version.rs b/packages/rs-platform-version/src/version/protocol_version.rs index 155b7a6ee58..dd7035f104b 100644 --- a/packages/rs-platform-version/src/version/protocol_version.rs +++ b/packages/rs-platform-version/src/version/protocol_version.rs @@ -329,6 +329,7 @@ mod token_math_gating_tests { #[cfg(test)] mod yes_no_vote_poll_gating_tests { use super::*; + use crate::version::feature_initial_protocol_versions::YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION; // Every method of the yes/no poll kind is `None` (not active) before protocol version 15 // and `Some(0)` from it, so a caller on an older version is refused at the method instead @@ -336,7 +337,9 @@ mod yes_no_vote_poll_gating_tests { #[test] fn should_activate_every_yes_no_vote_poll_method_at_protocol_version_15() { for platform_version in PLATFORM_VERSIONS { - let expected = (platform_version.protocol_version >= 15).then_some(0); + let expected = (platform_version.protocol_version + >= YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION) + .then_some(0); let voting = &platform_version.dpp.voting_versions; let abci_voting = &platform_version.drive_abci.methods.voting; let drive_methods = &platform_version.drive.methods; @@ -433,7 +436,7 @@ mod yes_no_vote_poll_gating_tests { #[test] fn should_weigh_yes_no_votes_one_for_a_masternode_and_four_for_an_evonode_at_protocol_version_15( ) { - let limits = &PlatformVersion::get(15) + let limits = &PlatformVersion::get(YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION) .expect("expected protocol version 15") .system_limits; assert_eq!(limits.yes_no_vote_poll_masternode_vote_strength, 1); From b30a5c0bce597c13d0af92df92b07be27bbe3aaf Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 28 Oct 2026 19:24:38 +0300 Subject: [PATCH 16/18] fix(drive): refuse the yes/no poll state query before protocol version 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 --- .../yes_no/open_yes_no_vote_poll/mod.rs | 15 +++++++++++ .../src/query/yes_no_vote_poll_state_query.rs | 26 +++++++++++++++++++ .../drive_vote_method_versions/mod.rs | 3 +++ .../drive_vote_method_versions/v1.rs | 1 + .../drive_vote_method_versions/v2.rs | 1 + .../drive_vote_method_versions/v3.rs | 1 + .../drive_vote_method_versions/v4.rs | 1 + .../src/version/protocol_version.rs | 4 +++ 8 files changed, 52 insertions(+) diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs index 5dcfce4a33d..a88a4a8947f 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs @@ -364,6 +364,21 @@ mod tests { .map(|_| ()), "verify_yes_no_vote_poll_state_proof", ); + let state_query = YesNoVotePollStateDriveQuery { + vote_poll: vote_poll.clone(), + }; + not_active( + state_query + .execute_no_proof(&drive, None, &mut vec![], platform_version) + .map(|_| ()), + "fetch_yes_no_vote_poll_state (no proof)", + ); + not_active( + state_query + .execute_with_proof(&drive, None, &mut vec![], platform_version) + .map(|_| ()), + "fetch_yes_no_vote_poll_state (proof)", + ); } #[test] diff --git a/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs index 1d554a3c110..aaba45608ac 100644 --- a/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs +++ b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs @@ -111,6 +111,30 @@ impl YesNoVotePollStateDriveQuery { Ok(state) } + /// Refuses the query where yes/no polls are not active (`fetch_yes_no_vote_poll_state` is + /// `None` before protocol version 15), as the proof verifier does there. + #[cfg(feature = "server")] + fn ensure_active(platform_version: &PlatformVersion) -> Result<(), Error> { + match platform_version + .drive + .methods + .vote + .yes_no + .fetch_yes_no_vote_poll_state + { + Some(0) => Ok(()), + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "fetch_yes_no_vote_poll_state".to_string(), + known_versions: vec![0], + received: version, + })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "fetch_yes_no_vote_poll_state".to_string(), + known_versions: vec![0], + })), + } + } + /// Executes the query and returns the proof. #[cfg(feature = "server")] pub fn execute_with_proof( @@ -120,6 +144,7 @@ impl YesNoVotePollStateDriveQuery { drive_operations: &mut Vec, platform_version: &PlatformVersion, ) -> Result, Error> { + Self::ensure_active(platform_version)?; let path_query = self.construct_path_query()?; drive.grove_get_proved_path_query( &path_query, @@ -138,6 +163,7 @@ impl YesNoVotePollStateDriveQuery { drive_operations: &mut Vec, platform_version: &PlatformVersion, ) -> Result { + Self::ensure_active(platform_version)?; // A poll that was never opened has no tree (a chain without the decisions trees has no // parent either); the proof path reports the same as an empty state, so does this one. let path_query = self.construct_path_query()?; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs index 41ec3dae38d..e639438cef8 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/mod.rs @@ -28,6 +28,9 @@ pub struct DriveVoteYesNoMethodVersions { pub fetch_yes_no_vote_poll_stored_info: OptionalFeatureVersion, pub fetch_identity_yes_no_vote: OptionalFeatureVersion, pub fetch_identities_voting_in_yes_no_vote_poll: OptionalFeatureVersion, + /// Reads a yes/no poll's state (`YesNoVotePollStateDriveQuery::execute_with_proof` and + /// `execute_no_proof`), as its proof verifier does from the same protocol version. + pub fetch_yes_no_vote_poll_state: OptionalFeatureVersion, pub remove_yes_no_vote_poll_votes_operations: OptionalFeatureVersion, pub remove_yes_no_vote_references_given_by_identity: OptionalFeatureVersion, } diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs index be4d1742316..c4a619848a7 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs @@ -44,6 +44,7 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V1: DriveVoteMethodVersions = DriveVoteMeth fetch_yes_no_vote_poll_stored_info: None, fetch_identity_yes_no_vote: None, fetch_identities_voting_in_yes_no_vote_poll: None, + fetch_yes_no_vote_poll_state: None, remove_yes_no_vote_poll_votes_operations: None, remove_yes_no_vote_references_given_by_identity: None, }, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs index aa35e886477..c49ee0bdfcd 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v2.rs @@ -44,6 +44,7 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V2: DriveVoteMethodVersions = DriveVoteMeth fetch_yes_no_vote_poll_stored_info: None, fetch_identity_yes_no_vote: None, fetch_identities_voting_in_yes_no_vote_poll: None, + fetch_yes_no_vote_poll_state: None, remove_yes_no_vote_poll_votes_operations: None, remove_yes_no_vote_references_given_by_identity: None, }, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs index ddaef80652a..fd1b4314a4b 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v3.rs @@ -52,6 +52,7 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V3: DriveVoteMethodVersions = DriveVoteMeth fetch_yes_no_vote_poll_stored_info: None, fetch_identity_yes_no_vote: None, fetch_identities_voting_in_yes_no_vote_poll: None, + fetch_yes_no_vote_poll_state: None, remove_yes_no_vote_poll_votes_operations: None, remove_yes_no_vote_references_given_by_identity: None, }, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs index 4f6e3ae922f..09d68cc9f70 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs @@ -40,6 +40,7 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V4: DriveVoteMethodVersions = DriveVoteMeth fetch_yes_no_vote_poll_stored_info: Some(0), fetch_identity_yes_no_vote: Some(0), fetch_identities_voting_in_yes_no_vote_poll: Some(0), + fetch_yes_no_vote_poll_state: Some(0), remove_yes_no_vote_poll_votes_operations: Some(0), remove_yes_no_vote_references_given_by_identity: Some(0), }, diff --git a/packages/rs-platform-version/src/version/protocol_version.rs b/packages/rs-platform-version/src/version/protocol_version.rs index dd7035f104b..0faa40ca87a 100644 --- a/packages/rs-platform-version/src/version/protocol_version.rs +++ b/packages/rs-platform-version/src/version/protocol_version.rs @@ -412,6 +412,10 @@ mod yes_no_vote_poll_gating_tests { "fetch_identities_voting_in_yes_no_vote_poll", yes_no.fetch_identities_voting_in_yes_no_vote_poll, ), + ( + "fetch_yes_no_vote_poll_state", + yes_no.fetch_yes_no_vote_poll_state, + ), ( "remove_yes_no_vote_poll_votes_operations", yes_no.remove_yes_no_vote_poll_votes_operations, From 0222808d56f338b17fa61357a8303895552712bd Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 28 Oct 2026 22:27:57 +0300 Subject: [PATCH 17/18] fix(sdk): answer no current DPNS contests for a zero limit 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 --- .../dpns_usernames/contested_queries.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs index 173b9c378e5..5476ad62314 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/contested_queries.rs @@ -479,6 +479,11 @@ impl Sdk { limit: Option, ) -> Result, Error> { let dpns_contract_id = self.get_dpns_contract_id()?; + // Zero asks for nothing. Checked here because an empty page read at a small limit is + // read again at the default one below. + if limit == Some(0) { + return Ok(BTreeMap::new()); + } let mut query_limit = limit.unwrap_or(DEFAULT_QUERY_LIMIT); let mut name_to_end_time: BTreeMap = BTreeMap::new(); let mut current_start_time = start_time.map(|t| (t, true)); @@ -759,6 +764,19 @@ mod tests { assert_eq!(usernames, names(42, 47)); } + #[tokio::test] + async fn should_list_no_current_contests_for_a_zero_limit_without_asking() { + // No expectation is registered, so any request would fail. + let sdk = mock_sdk(); + + let contests = sdk + .get_current_dpns_contests(None, None, Some(0)) + .await + .expect("expected a zero limit to answer without a request"); + + assert!(contests.is_empty()); + } + #[test] fn should_extract_a_label_answered_as_text() { let label = Sdk::extract_label_from_contested_resource(ContestedResource(Value::Text( From 1dace00274d89fc9913361167261581d1afb6635 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 28 Oct 2026 22:28:06 +0300 Subject: [PATCH 18/18] refactor(platform)!: select the yes/no vote poll id through the platform 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 --- packages/rs-dpp/src/voting/vote_polls/mod.rs | 25 ++++-- .../voting/vote_polls/yes_no_vote_poll/mod.rs | 79 ++++++++++++++----- packages/rs-dpp/src/voting/votes/mod.rs | 34 ++++++-- .../src/voting/votes/resource_vote/v0/mod.rs | 9 ++- .../v0/mod.rs | 6 +- .../check_for_ended_vote_polls/v1/mod.rs | 5 +- .../masternode_vote/balance/v0/mod.rs | 10 ++- .../masternode_vote/end_date_cleanup_tests.rs | 3 +- .../state_transitions/masternode_vote/mod.rs | 4 +- .../no_locking_contest_tests.rs | 6 +- .../transform_into_action/v1/mod.rs | 2 +- .../masternode_vote/yes_no_tests.rs | 18 +++-- .../query_request_errors/offset_controls.rs | 2 +- .../v1/mod.rs | 3 +- .../v0/mod.rs | 2 +- .../v0/mod.rs | 2 +- .../v0/mod.rs | 4 +- .../v1/mod.rs | 2 +- .../v0/mod.rs | 2 +- .../yes_no/open_yes_no_vote_poll/mod.rs | 24 +++--- .../yes_no/open_yes_no_vote_poll/v0/mod.rs | 4 +- .../register_yes_no_identity_vote/v0/mod.rs | 7 +- packages/rs-drive/src/drive/votes/paths.rs | 13 +-- .../src/drive/votes/resolved/votes/mod.rs | 18 ++++- .../v0/mod.rs | 2 +- .../prove/prove_state_transition/v0/mod.rs | 2 +- packages/rs-drive/src/query/vote_query.rs | 11 ++- .../src/query/yes_no_vote_poll_state_query.rs | 15 ++-- .../identity/masternode_vote_transition.rs | 6 +- .../voting/verify_masternode_vote/v0/mod.rs | 6 +- .../verify_masternode_yes_no_vote/mod.rs | 4 +- .../verify_masternode_yes_no_vote/v0/mod.rs | 2 +- .../v0/mod.rs | 2 +- .../dpp_versions/dpp_voting_versions/mod.rs | 4 + .../dpp_versions/dpp_voting_versions/v1.rs | 1 + .../dpp_versions/dpp_voting_versions/v2.rs | 1 + .../dpp_versions/dpp_voting_versions/v3.rs | 5 +- .../src/version/protocol_version.rs | 1 + .../rs-platform-version/src/version/v15.rs | 4 +- .../rs-sdk/src/platform/transition/vote.rs | 2 +- .../rs-sdk/tests/fetch/mock_fetch_many.rs | 3 +- .../fetch/prefunded_specialized_balance.rs | 2 +- .../tests/fetch/vote_poll_default_limit.rs | 2 +- .../fetch/vote_poll_default_limit_controls.rs | 2 +- 44 files changed, 245 insertions(+), 116 deletions(-) diff --git a/packages/rs-dpp/src/voting/vote_polls/mod.rs b/packages/rs-dpp/src/voting/vote_polls/mod.rs index 73edbd29e88..f6bc473cd36 100644 --- a/packages/rs-dpp/src/voting/vote_polls/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/mod.rs @@ -11,6 +11,7 @@ use platform_serialization_derive::{ PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, }; use platform_value::Identifier; +use platform_version::version::PlatformVersion; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; use std::fmt; @@ -70,25 +71,37 @@ impl Default for VotePoll { } impl VotePoll { - pub fn specialized_balance_id(&self) -> Result, ProtocolError> { + /// The prefunded balance the votes on this poll are paid from. A yes/no poll's id is + /// derived as the platform version selects; a contested poll's does not depend on it. + pub fn specialized_balance_id( + &self, + platform_version: &PlatformVersion, + ) -> Result, ProtocolError> { match self { VotePoll::ContestedDocumentResourceVotePoll(contested_document_resource_vote_poll) => { Ok(Some( contested_document_resource_vote_poll.specialized_balance_id()?, )) } - VotePoll::YesNoVotePoll(yes_no_vote_poll) => { - Ok(Some(yes_no_vote_poll.specialized_balance_id()?)) - } + VotePoll::YesNoVotePoll(yes_no_vote_poll) => Ok(Some( + yes_no_vote_poll.specialized_balance_id(platform_version)?, + )), } } - pub fn unique_id(&self) -> Result { + /// The id the poll is stored and voted on under. A yes/no poll's id is derived as the + /// platform version selects; a contested poll's does not depend on it. + pub fn unique_id( + &self, + platform_version: &PlatformVersion, + ) -> Result { match self { VotePoll::ContestedDocumentResourceVotePoll(contested_document_resource_vote_poll) => { contested_document_resource_vote_poll.unique_id() } - VotePoll::YesNoVotePoll(yes_no_vote_poll) => yes_no_vote_poll.unique_id(), + VotePoll::YesNoVotePoll(yes_no_vote_poll) => { + yes_no_vote_poll.unique_id(platform_version) + } } } } diff --git a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs index 84b2b0abb94..f18803f05f8 100644 --- a/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs @@ -143,10 +143,10 @@ impl fmt::Display for YesNoMinimumVotingPower { /// the poll so every feature sets its own. Two thirds with a floor of 400 is numerator 2, /// denominator 3 and an absolute minimum of 400. /// -/// The parameters are part of the poll's identity: [`Self::unique_id`] is the double SHA-256 of -/// the domain tag `dash platform yes/no vote poll id` followed by the serialized [`VotePoll`] -/// that carries the poll, so a vote names the exact poll it answers. The prefunded balance that -/// pays for the votes has the same id. +/// The parameters are part of the poll's identity: [`Self::unique_id`] version 0 is the double +/// SHA-256 of the domain tag `dash platform yes/no vote poll id` followed by the serialized +/// [`VotePoll`] that carries the poll, so a vote names the exact poll it answers. The prefunded +/// balance that pays for the votes has the same id. #[cfg_attr(feature = "json-conversion", json_safe_fields)] #[cfg_attr( all(feature = "json-conversion", feature = "serde-conversion"), @@ -408,22 +408,52 @@ impl YesNoVotePoll { yes * self.supermajority_denominator as u64 > cast * self.supermajority_numerator as u64 } - /// The double SHA-256 of the yes/no poll id domain tag followed by the serialized - /// `VotePoll` carrying this poll. - pub fn sha256_2_hash(&self) -> Result<[u8; 32], ProtocolError> { - let mut preimage = YES_NO_VOTE_POLL_ID_DOMAIN.to_vec(); - preimage.extend(VotePoll::YesNoVotePoll(self.clone()).serialize_to_bytes()?); - Ok(hash_double(preimage)) + /// The id the poll is stored and voted on under, which also keys its end date index entry + /// and names its prefunded balance, derived as the platform version selects + /// (`dpp.voting_versions.yes_no_vote_poll_id`). + /// + /// # Parameters + /// - `platform_version`: the platform version. + /// + /// # Returns + /// The poll's id. + pub fn unique_id( + &self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version.dpp.voting_versions.yes_no_vote_poll_id { + Some(0) => self.unique_id_v0(), + Some(version) => Err(ProtocolError::UnknownVersionMismatch { + method: "YesNoVotePoll::unique_id".to_string(), + known_versions: vec![0], + received: version, + }), + None => Err(ProtocolError::UnknownVersionError( + "YesNoVotePoll::unique_id is not active before protocol version 15".to_string(), + )), + } } /// The prefunded balance the votes on this poll are paid from: the poll's unique id. - pub fn specialized_balance_id(&self) -> Result { - self.unique_id() + /// + /// # Parameters + /// - `platform_version`: the platform version, which selects how the id is derived. + /// + /// # Returns + /// The balance's id. + pub fn specialized_balance_id( + &self, + platform_version: &PlatformVersion, + ) -> Result { + self.unique_id(platform_version) } - /// The id the poll is stored and voted on under. - pub fn unique_id(&self) -> Result { - self.sha256_2_hash().map(Identifier::new) + /// Version 0: the double SHA-256 of the yes/no poll id domain tag followed by the serialized + /// `VotePoll` carrying this poll. + fn unique_id_v0(&self) -> Result { + let mut preimage = YES_NO_VOTE_POLL_ID_DOMAIN.to_vec(); + preimage.extend(VotePoll::YesNoVotePoll(self.clone()).serialize_to_bytes()?); + Ok(Identifier::new(hash_double(preimage))) } } @@ -668,7 +698,8 @@ mod tests { #[test] fn should_change_the_id_with_every_parameter() { - let base = poll().unique_id().expect("id"); + let platform_version = PlatformVersion::latest(); + let base = poll().unique_id(platform_version).expect("id"); let other_path = YesNoVotePoll { resource_path: vec![BinaryData::new(vec![0xc1; 32])], ..poll() @@ -683,9 +714,14 @@ mod tests { ..poll() }; for other in [other_path, other_rule, other_minimum] { - assert_ne!(other.unique_id().expect("id"), base); + assert_ne!(other.unique_id(platform_version).expect("id"), base); } - assert_eq!(poll().specialized_balance_id().expect("balance id"), base); + assert_eq!( + poll() + .specialized_balance_id(platform_version) + .expect("balance id"), + base + ); } #[test] @@ -697,7 +733,7 @@ mod tests { let tagged = Identifier::new(hash_double( [YES_NO_VOTE_POLL_ID_DOMAIN, encoded.as_slice()].concat(), )); - let id = poll().unique_id().expect("id"); + let id = poll().unique_id(PlatformVersion::latest()).expect("id"); assert_eq!(id, tagged); assert_ne!(id, untagged); } @@ -725,6 +761,11 @@ mod tests { poll().passes(10, 0, 100, platform_version).map(|_| ()), "passes", ); + not_active(poll().unique_id(platform_version).map(|_| ()), "unique_id"); + not_active( + poll().specialized_balance_id(platform_version).map(|_| ()), + "specialized_balance_id", + ); } } diff --git a/packages/rs-dpp/src/voting/votes/mod.rs b/packages/rs-dpp/src/voting/votes/mod.rs index 6b4c90684cb..a59db1f5c65 100644 --- a/packages/rs-dpp/src/voting/votes/mod.rs +++ b/packages/rs-dpp/src/voting/votes/mod.rs @@ -16,6 +16,7 @@ use platform_serialization_derive::{ PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, }; use platform_value::Identifier; +use platform_version::version::PlatformVersion; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; @@ -59,18 +60,35 @@ impl Default for Vote { } impl Vote { - pub fn specialized_balance_id(&self) -> Result, ProtocolError> { + /// The prefunded balance the votes on this vote's poll are paid from. A yes/no poll's id is + /// derived as the platform version selects. + pub fn specialized_balance_id( + &self, + platform_version: &PlatformVersion, + ) -> Result, ProtocolError> { match self { - Vote::ResourceVote(resource_vote) => resource_vote.vote_poll().specialized_balance_id(), - Vote::YesNoVote(yes_no_vote) => { - Ok(Some(yes_no_vote.vote_poll().specialized_balance_id()?)) - } + Vote::ResourceVote(resource_vote) => resource_vote + .vote_poll() + .specialized_balance_id(platform_version), + Vote::YesNoVote(yes_no_vote) => Ok(Some( + yes_no_vote + .vote_poll() + .specialized_balance_id(platform_version)?, + )), } } - pub fn vote_poll_unique_id(&self) -> Result { + + /// The id of the poll this vote answers. A yes/no poll's id is derived as the platform + /// version selects. + pub fn vote_poll_unique_id( + &self, + platform_version: &PlatformVersion, + ) -> Result { match self { - Vote::ResourceVote(resource_vote) => resource_vote.vote_poll().unique_id(), - Vote::YesNoVote(yes_no_vote) => yes_no_vote.vote_poll().unique_id(), + Vote::ResourceVote(resource_vote) => { + resource_vote.vote_poll().unique_id(platform_version) + } + Vote::YesNoVote(yes_no_vote) => yes_no_vote.vote_poll().unique_id(platform_version), } } } diff --git a/packages/rs-dpp/src/voting/votes/resource_vote/v0/mod.rs b/packages/rs-dpp/src/voting/votes/resource_vote/v0/mod.rs index cca969d1e3c..f2258b7c74c 100644 --- a/packages/rs-dpp/src/voting/votes/resource_vote/v0/mod.rs +++ b/packages/rs-dpp/src/voting/votes/resource_vote/v0/mod.rs @@ -8,6 +8,7 @@ use platform_serialization_derive::{ PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, }; use platform_value::Identifier; +use platform_version::version::PlatformVersion; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; @@ -84,8 +85,12 @@ impl ResourceVoteV0 { } } - pub fn vote_poll_unique_id(&self) -> Result { - self.vote_poll.unique_id() + /// The id of the poll this vote answers. + pub fn vote_poll_unique_id( + &self, + platform_version: &PlatformVersion, + ) -> Result { + self.vote_poll.unique_id(platform_version) } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 9130ff56f32..4b8870fa3f1 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -329,9 +329,9 @@ impl Platform { } // Unreachable: this runs once, on the block entering protocol version 8, // and yes/no polls exist from 15. The arm only keeps the match exhaustive. - VotePoll::YesNoVotePoll(yes_no) => { - yes_no.specialized_balance_id().map_err(Error::Protocol) - } + VotePoll::YesNoVotePoll(yes_no) => yes_no + .specialized_balance_id(platform_version) + .map_err(Error::Protocol), } }) .collect::, Error>>()?; diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs index b9edb93d0cc..4968ccb3e05 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/check_for_ended_vote_polls/v1/mod.rs @@ -99,7 +99,7 @@ where for vote_poll in vote_polls { match vote_poll { VotePoll::YesNoVotePoll(yes_no_vote_poll) => { - let poll_id = yes_no_vote_poll.unique_id()?; + let poll_id = yes_no_vote_poll.unique_id(platform_version)?; yes_no_end_date_entries.push((poll_id, end_date)); if let Some(voters) = self.finish_yes_no_vote_poll( block_platform_state, @@ -110,7 +110,8 @@ where )? { finished_yes_no_vote_polls.push(FinishedYesNoVotePoll { poll_id, - prefunded_balance_id: yes_no_vote_poll.specialized_balance_id()?, + prefunded_balance_id: yes_no_vote_poll + .specialized_balance_id(platform_version)?, voters, }); } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/balance/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/balance/v0/mod.rs index bdabab16e3d..35151e3788f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/balance/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/balance/v0/mod.rs @@ -45,11 +45,13 @@ impl MasternodeVoteTransitionBalanceValidationV0 for MasternodeVoteTransition { let vote = self.vote(); - let balance_id = vote.specialized_balance_id()?.ok_or(Error::Execution( - ExecutionError::CorruptedCodeExecution( + // Only a yes/no poll's id reads the platform version, and no vote naming one decodes + // before protocol version 15. + let balance_id = vote + .specialized_balance_id(platform_version)? + .ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( "In this version there should always be a specialized balance id", - ), - ))?; + )))?; let maybe_balance = drive.fetch_prefunded_specialized_balance( balance_id.to_buffer(), tx, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs index b0b81b9611e..e2ed7bd3a0a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/end_date_cleanup_tests.rs @@ -14,13 +14,12 @@ use dpp::block::block_info::BlockInfo; use dpp::data_contract::DataContract; use dpp::prelude::{Identifier, TimestampMillis}; use dpp::version::PlatformVersion; -use dpp::voting::vote_polls::VotePoll; use drive::util::test_helpers::vote_poll_end_dates; use std::collections::{BTreeMap, BTreeSet}; /// The unique id of the DPNS name contest on `name` fn contest_id(dpns_contract: &DataContract, name: &str) -> Identifier { - VotePoll::ContestedDocumentResourceVotePoll(dpns_name_vote_poll(dpns_contract, name)) + dpns_name_vote_poll(dpns_contract, name) .unique_id() .expect("expected a vote poll id") } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs index 20dca6550dc..a897304e72d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs @@ -4327,7 +4327,9 @@ mod tests { }, ); ( - vote_poll.unique_id().expect("expected a vote poll id"), + vote_poll + .unique_id(platform_version) + .expect("expected a vote poll id"), ( ResourceVote::V0(ResourceVoteV0 { vote_poll, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs index f625550a972..04f5277151d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs @@ -688,7 +688,7 @@ async fn should_open_the_vote_window_when_a_second_contender_joins() { BTreeMap::from([( start + poll_duration, BTreeSet::from([vote_poll(&contract) - .unique_id() + .unique_id(platform_version) .expect("expected a vote poll id")]), )]), "no end date is left at the end of the join window" @@ -762,7 +762,7 @@ async fn keep_a_shared_join_window_end_date_until_its_last_contest_moves( let dave = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let unique_id = |name| { vote_poll_for(&contract, name) - .unique_id() + .unique_id(platform_version) .expect("expected a vote poll id") }; @@ -894,7 +894,7 @@ async fn end_a_contest_behind_end_dates_left_empty(platform_version: &'static Pl end_dates_before.insert( start + join_window, BTreeSet::from([vote_poll(&contract) - .unique_id() + .unique_id(platform_version) .expect("expected a vote poll id")]), ); assert_eq!( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs index bc9ee66048d..e21ce0320f1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/transform_into_action/v1/mod.rs @@ -74,7 +74,7 @@ impl MasternodeVoteStateTransitionTransformIntoActionValidationV1 for Masternode if validation_mode != ValidationMode::NoValidation { // Before we transform into action we want to make sure that we have not yet voted let vote_poll = yes_no_vote.vote_poll(); - let vote_id = vote_poll.unique_id()?; + let vote_id = vote_poll.unique_id(platform_version)?; let maybe_existing_vote_choice = platform.drive.fetch_identity_yes_no_vote( self.pro_tx_hash(), vote_id, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs index 1366261cc4f..b30c972fa3e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs @@ -103,7 +103,7 @@ fn open_poll_ending_at( .drive .add_prefunded_specialized_balance( vote_poll - .specialized_balance_id() + .specialized_balance_id(platform_version) .expect("expected a specialized balance id"), fund, None, @@ -692,7 +692,7 @@ async fn should_leave_only_the_record_behind_after_the_poll_ends() { platform_version, ) .await; - let vote_poll_id = vote_poll.unique_id().expect("id"); + let vote_poll_id = vote_poll.unique_id(platform_version).expect("id"); let vote_cost = platform_version .fee_version .vote_resolution_fund_fees @@ -830,7 +830,9 @@ async fn should_refuse_a_vote_on_an_unknown_poll_and_the_same_answer_twice() { platform .drive .add_prefunded_specialized_balance( - unknown_poll.specialized_balance_id().expect("id"), + unknown_poll + .specialized_balance_id(platform_version) + .expect("id"), FUND, None, platform_version, @@ -931,7 +933,7 @@ async fn should_move_a_changed_vote_and_cap_the_changes() { let vote_poll = two_thirds_poll(b"changes", 1); open_poll(&platform, &vote_poll, FUND, platform_version); let (pro_tx_hash, signer, voting_key) = setup_voter(&mut platform, 100, true, platform_version); - let vote_poll_id = vote_poll.unique_id().expect("id"); + let vote_poll_id = vote_poll.unique_id(platform_version).expect("id"); let allowed = platform_version .dpp .validation @@ -1028,7 +1030,7 @@ async fn should_refuse_a_vote_when_the_poll_has_no_fund() { identity_vote( &platform, pro_tx_hash, - vote_poll.unique_id().expect("id"), + vote_poll.unique_id(platform_version).expect("id"), platform_version ), None @@ -1073,7 +1075,7 @@ async fn should_remove_the_votes_of_masternodes_that_left_the_list() { platform_version, ) .await; - let vote_poll_id = vote_poll.unique_id().expect("id"); + let vote_poll_id = vote_poll.unique_id(platform_version).expect("id"); let platform_state_before_removals = platform.state.load().deref().clone(); let removed = vec![yes_voters[0], yes_voters[1], no_voters[0]]; @@ -1301,7 +1303,9 @@ async fn should_only_remove_the_later_end_date_entry_of_a_poll_listed_twice() { platform .drive .add_prefunded_specialized_balance( - vote_poll.specialized_balance_id().expect("id"), + vote_poll + .specialized_balance_id(platform_version) + .expect("id"), FUND, None, platform_version, diff --git a/packages/rs-drive-abci/tests/query_request_errors/offset_controls.rs b/packages/rs-drive-abci/tests/query_request_errors/offset_controls.rs index 0fb73b30783..a4c9a430652 100644 --- a/packages/rs-drive-abci/tests/query_request_errors/offset_controls.rs +++ b/packages/rs-drive-abci/tests/query_request_errors/offset_controls.rs @@ -207,7 +207,7 @@ async fn should_preserve_supported_offset_pages_and_original_proof_roots() { .expect("register identity vote"); let poll: dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll = poll.into(); let poll = VotePoll::ContestedDocumentResourceVotePoll(poll); - let id = poll.unique_id().expect("poll ID"); + let id = poll.unique_id(version).expect("poll ID"); expected_votes.insert( id, ResourceVote::V0(ResourceVoteV0 { diff --git a/packages/rs-drive/src/drive/document/insert_contested/add_contested_document_for_contract_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/insert_contested/add_contested_document_for_contract_operations/v1/mod.rs index 7a1bccb0a2d..6ffa3e973a6 100644 --- a/packages/rs-drive/src/drive/document/insert_contested/add_contested_document_for_contract_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/insert_contested/add_contested_document_for_contract_operations/v1/mod.rs @@ -200,7 +200,8 @@ impl Drive { contested_document_resource_vote_poll.into(), ); - let unique_id = vote_poll.unique_id()?; + // A contested poll's id does not read the platform version. + let unique_id = vote_poll.unique_id(platform_version)?; let join_end_path = vote_contested_resource_end_date_queries_at_time_tree_path_vec(join_end); // The windows are read again rather than remembered, so the entry the contest diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs index abc5167f2e6..441f5876ad4 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs @@ -17,7 +17,7 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { - let path = vote_poll.poll_path_vec()?; + let path = vote_poll.poll_path_vec(platform_version)?; let mut query = Query::new_with_direction(true); query.insert_keys( YesNoAbstainVoteChoice::ALL diff --git a/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs index a6e176eb41b..abe44bce794 100644 --- a/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs @@ -15,7 +15,7 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { - let path = vote_poll.poll_path_vec()?; + let path = vote_poll.poll_path_vec(platform_version)?; self.grove_get_raw_optional_item( path.as_slice().into(), &[YES_NO_VOTE_POLL_STORED_INFO_KEY], diff --git a/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/v0/mod.rs index 5df377420f5..37653d5b676 100644 --- a/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/v0/mod.rs @@ -142,7 +142,9 @@ impl Drive { } }; - let unique_id = vote_poll.unique_id()?; + // Only a yes/no poll's id reads the platform version, and no yes/no poll is opened + // before protocol version 15. + let unique_id = vote_poll.unique_id(platform_version)?; let path_key_element_info: PathKeyElementInfo<'_, 0> = if estimated_costs_only_with_layer_info.is_none() { diff --git a/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/v1/mod.rs b/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/v1/mod.rs index 4e1b020fc49..9992ca5a1ac 100644 --- a/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/vote_poll/add_vote_poll_end_date_query_operations/v1/mod.rs @@ -64,7 +64,7 @@ impl Drive { let storage_flags = creator_identity_id.map(|creator_identity_id| { StorageFlags::new_single_epoch(block_info.epoch.index, Some(creator_identity_id)) }); - let unique_id = vote_poll.unique_id()?; + let unique_id = vote_poll.unique_id(platform_version)?; let item = Element::Item( vote_poll.serialize_to_bytes()?, StorageFlags::map_to_some_element_flags(storage_flags.as_ref()), diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/v0/mod.rs index c976a961014..cf57cfc19f6 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/insert_stored_info_for_yes_no_vote_poll/v0/mod.rs @@ -41,7 +41,7 @@ impl Drive { let serialization = stored_info.serialize_consume_to_bytes()?; self.batch_insert::<0>( PathKeyElement(( - vote_poll.poll_path_vec()?, + vote_poll.poll_path_vec(platform_version)?, vec![YES_NO_VOTE_POLL_STORED_INFO_KEY], Element::new_item(serialization), )), diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs index a88a4a8947f..a23cb9b8445 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs @@ -252,7 +252,8 @@ mod tests { let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); let drive = setup_drive_with_initial_state_structure(Some(platform_version)); let vote_poll = poll(); - let poll_id = vote_poll.unique_id().expect("id"); + // The id as protocol version 15 derives it: version 14 derives none. + let poll_id = vote_poll.unique_id(PlatformVersion::latest()).expect("id"); let not_active = |result: Result<(), Error>, method: &str| { assert!( matches!( @@ -466,8 +467,8 @@ mod tests { assert_eq!( end_dates[&1_000], [ - poll().unique_id().expect("id"), - other_poll.unique_id().expect("id") + poll().unique_id(platform_version).expect("id"), + other_poll.unique_id(platform_version).expect("id") ] .into_iter() .collect() @@ -524,8 +525,8 @@ mod tests { assert_eq!( end_dates[&1_000], [ - contested_poll.unique_id().expect("id"), - poll().unique_id().expect("id") + contested_poll.unique_id(platform_version).expect("id"), + poll().unique_id(platform_version).expect("id") ] .into_iter() .collect() @@ -584,7 +585,10 @@ mod tests { KeyInfoPath::from_known_owned_path( vote_contested_resource_end_date_queries_at_time_tree_path_vec(1_000), ), - contested_poll.unique_id().expect("id").as_slice(), + contested_poll + .unique_id(platform_version) + .expect("id") + .as_slice(), Some(vote_root_path().len() as u16), BatchDeleteUpTreeApplyType::StatefulBatchDelete { is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), @@ -621,7 +625,9 @@ mod tests { let end_dates = vote_poll_end_dates(&drive, platform_version); assert_eq!( end_dates[&1_000], - [poll().unique_id().expect("id")].into_iter().collect() + [poll().unique_id(platform_version).expect("id")] + .into_iter() + .collect() ); } @@ -676,8 +682,8 @@ mod tests { assert_eq!( end_dates[&1_000], [ - contested_poll.unique_id().expect("id"), - poll().unique_id().expect("id") + contested_poll.unique_id(platform_version).expect("id"), + poll().unique_id(platform_version).expect("id") ] .into_iter() .collect() diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs index f756d79b603..6f5bc526656 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs @@ -82,7 +82,7 @@ impl Drive { ), )))); } - let poll_id = vote_poll.unique_id()?; + let poll_id = vote_poll.unique_id(platform_version)?; // The decisions trees, when the chain lacks them: genesis and the upgrade to protocol // version 15 create them, but a chain that reached 15 before they existed did not. An @@ -137,7 +137,7 @@ impl Drive { batch_operations.append(&mut stored_info_operations); // One sum tree per choice; the sums are the tallies. - let poll_path = vote_poll.poll_path_vec()?; + let poll_path = vote_poll.poll_path_vec(platform_version)?; for vote_choice in YesNoAbstainVoteChoice::ALL { self.batch_insert_empty_sum_tree( poll_path.iter().map(|segment| segment.as_slice()), diff --git a/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/v0/mod.rs b/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/v0/mod.rs index 4b7beedaf1b..6dfcac0432b 100644 --- a/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/v0/mod.rs +++ b/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/v0/mod.rs @@ -69,7 +69,7 @@ impl Drive { } = vote; // The vote itself: the masternode's strength under its answer's sum tree. - let voting_path = vote_poll.choice_votes_path_vec(vote_choice)?; + let voting_path = vote_poll.choice_votes_path_vec(vote_choice, platform_version)?; self.batch_insert::<0>( PathKeyElement(( voting_path, @@ -83,7 +83,8 @@ impl Drive { // A changed vote leaves its previous answer. let mut identity_vote_times = 1; if let Some((previous_vote_choice, previous_vote_count)) = previous_vote_choice_to_remove { - let previous_voting_path = vote_poll.choice_votes_path_vec(previous_vote_choice)?; + let previous_voting_path = + vote_poll.choice_votes_path_vec(previous_vote_choice, platform_version)?; self.batch_delete( previous_voting_path.as_slice().into(), voter_pro_tx_hash.as_slice(), @@ -124,7 +125,7 @@ impl Drive { self.batch_insert::<0>( PathKeyElement(( vote_decisions_identity_votes_tree_path_for_identity_vec(&voter_pro_tx_hash), - vote_poll.unique_id()?.to_vec(), + vote_poll.unique_id(platform_version)?.to_vec(), Element::new_item(encoded), )), &mut drive_operations, diff --git a/packages/rs-drive/src/drive/votes/paths.rs b/packages/rs-drive/src/drive/votes/paths.rs index ff39d13173a..c8b8ce046e1 100644 --- a/packages/rs-drive/src/drive/votes/paths.rs +++ b/packages/rs-drive/src/drive/votes/paths.rs @@ -350,31 +350,34 @@ impl VotePollPaths for ContestedDocumentResourceVotePollWithContractInfoAllowBor } } -/// The paths of a yes/no vote poll's storage, derived from its unique id. +/// The paths of a yes/no vote poll's storage, derived from its unique id, which the platform +/// version selects how to derive. pub trait YesNoVotePollPaths { /// The poll's tree, holding its stored info and its three vote sum trees. - fn poll_path_vec(&self) -> Result>, Error>; + fn poll_path_vec(&self, platform_version: &PlatformVersion) -> Result>, Error>; /// The sum tree holding the votes for one choice. fn choice_votes_path_vec( &self, vote_choice: YesNoAbstainVoteChoice, + platform_version: &PlatformVersion, ) -> Result>, Error>; } impl YesNoVotePollPaths for YesNoVotePoll { - fn poll_path_vec(&self) -> Result>, Error> { + fn poll_path_vec(&self, platform_version: &PlatformVersion) -> Result>, Error> { Ok(vote_decisions_active_polls_poll_tree_path_vec( - self.unique_id()?.as_bytes(), + self.unique_id(platform_version)?.as_bytes(), )) } fn choice_votes_path_vec( &self, vote_choice: YesNoAbstainVoteChoice, + platform_version: &PlatformVersion, ) -> Result>, Error> { Ok(vote_decisions_poll_choice_votes_tree_path_vec( - self.unique_id()?.as_bytes(), + self.unique_id(platform_version)?.as_bytes(), vote_choice, )) } diff --git a/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs b/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs index af290bc7117..a5f11c51064 100644 --- a/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs +++ b/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs @@ -15,6 +15,7 @@ use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; use dpp::voting::votes::resource_vote::ResourceVote; use dpp::voting::votes::Vote; use dpp::ProtocolError; +use platform_version::version::PlatformVersion; /// Represents the different types of resolved votes within the system. // A resource vote carries the resolved contract of its poll; a yes/no vote is a few bytes. @@ -30,6 +31,10 @@ pub enum ResolvedVote { impl ResolvedVote { /// Retrieves the specialized balance identifier associated with the resolved vote. /// + /// # Parameters + /// + /// * `platform_version`: selects how a yes/no poll's id is derived. + /// /// # Returns /// /// * `Ok(Some(identifier))` if a specialized balance ID is available. @@ -39,14 +44,19 @@ impl ResolvedVote { /// # Errors /// /// Returns a `ProtocolError` if there is an issue retrieving the specialized balance ID. - pub fn specialized_balance_id(&self) -> Result, ProtocolError> { + pub fn specialized_balance_id( + &self, + platform_version: &PlatformVersion, + ) -> Result, ProtocolError> { match self { ResolvedVote::ResolvedResourceVote(resource_vote) => { resource_vote.vote_poll().specialized_balance_id() } - ResolvedVote::YesNoVote(yes_no_vote) => { - Ok(Some(yes_no_vote.vote_poll.specialized_balance_id()?)) - } + ResolvedVote::YesNoVote(yes_no_vote) => Ok(Some( + yes_no_vote + .vote_poll + .specialized_balance_id(platform_version)?, + )), } } } diff --git a/packages/rs-drive/src/prove/prove_multiple_state_transition_results/v0/mod.rs b/packages/rs-drive/src/prove/prove_multiple_state_transition_results/v0/mod.rs index cc18405f0a6..26e90388295 100644 --- a/packages/rs-drive/src/prove/prove_multiple_state_transition_results/v0/mod.rs +++ b/packages/rs-drive/src/prove/prove_multiple_state_transition_results/v0/mod.rs @@ -115,7 +115,7 @@ impl Drive { path_queries.extend(vote_queries.iter().filter_map(|vote_query| { // The path query construction can only fail if the serialization fails. // Because the serialization will pretty much never fail, we can do this. - let mut path_query = vote_query.construct_path_query().ok()?; + let mut path_query = vote_query.construct_path_query(platform_version).ok()?; path_query.query.limit = None; Some(path_query) })); diff --git a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs index a2e8ae78832..9ac03177827 100644 --- a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs +++ b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs @@ -531,7 +531,7 @@ impl Drive { // The path query construction can only fail if the serialization fails. // Because the serialization will pretty much never fail, we can do this. - let mut path_query = query.construct_path_query()?; + let mut path_query = query.construct_path_query(platform_version)?; path_query.query.limit = None; path_query } diff --git a/packages/rs-drive/src/query/vote_query.rs b/packages/rs-drive/src/query/vote_query.rs index eab05afec88..6ccf484f345 100644 --- a/packages/rs-drive/src/query/vote_query.rs +++ b/packages/rs-drive/src/query/vote_query.rs @@ -8,6 +8,7 @@ use bincode::{Decode, Encode}; use dpp::identifier::Identifier; use dpp::voting::vote_polls::VotePoll; use grovedb::{PathQuery, SizedQuery}; +use platform_version::version::PlatformVersion; /// Vote Drive Query struct #[derive(Debug, PartialEq, Clone, Encode, Decode)] @@ -19,8 +20,12 @@ pub struct IdentityBasedVoteDriveQuery { } impl IdentityBasedVoteDriveQuery { - /// Operations to construct a path query. - pub fn construct_path_query(&self) -> Result { + /// Operations to construct a path query. The platform version selects how a yes/no poll's + /// id is derived. + pub fn construct_path_query( + &self, + platform_version: &PlatformVersion, + ) -> Result { // Each poll kind indexes the votes of a masternode under its own branch let path = match &self.vote_poll { VotePoll::ContestedDocumentResourceVotePoll(_) => { @@ -33,7 +38,7 @@ impl IdentityBasedVoteDriveQuery { ), }; - let vote_id = self.vote_poll.unique_id()?; + let vote_id = self.vote_poll.unique_id(platform_version)?; let mut query = Query::new(); query.insert_key(vote_id.to_vec()); diff --git a/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs index aaba45608ac..503a396641d 100644 --- a/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs +++ b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs @@ -18,7 +18,6 @@ use grovedb::query_result_type::QueryResultType; #[cfg(feature = "server")] use grovedb::TransactionArg; use grovedb::{Element, PathQuery, SizedQuery}; -#[cfg(feature = "server")] use platform_version::version::PlatformVersion; /// The state of one yes/no vote poll: its stored info and its tallies. @@ -44,9 +43,13 @@ pub struct YesNoVotePollStateDriveQuery { } impl YesNoVotePollStateDriveQuery { - /// The path query: the four keys of the poll's tree. - pub fn construct_path_query(&self) -> Result { - let path = self.vote_poll.poll_path_vec()?; + /// The path query: the four keys of the poll's tree, whose path the platform version + /// selects how to derive. + pub fn construct_path_query( + &self, + platform_version: &PlatformVersion, + ) -> Result { + let path = self.vote_poll.poll_path_vec(platform_version)?; let mut query = Query::new_with_direction(true); query.insert_key(vec![YES_NO_VOTE_POLL_STORED_INFO_KEY]); for vote_choice in YesNoAbstainVoteChoice::ALL { @@ -145,7 +148,7 @@ impl YesNoVotePollStateDriveQuery { platform_version: &PlatformVersion, ) -> Result, Error> { Self::ensure_active(platform_version)?; - let path_query = self.construct_path_query()?; + let path_query = self.construct_path_query(platform_version)?; drive.grove_get_proved_path_query( &path_query, transaction, @@ -166,7 +169,7 @@ impl YesNoVotePollStateDriveQuery { Self::ensure_active(platform_version)?; // A poll that was never opened has no tree (a chain without the decisions trees has no // parent either); the proof path reports the same as an empty state, so does this one. - let path_query = self.construct_path_query()?; + let path_query = self.construct_path_query(platform_version)?; let key_elements = match drive.grove_get_raw_path_query( &path_query, transaction, diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/identity/masternode_vote_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/identity/masternode_vote_transition.rs index 317215f624f..3c5ff479b36 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/identity/masternode_vote_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/identity/masternode_vote_transition.rs @@ -31,7 +31,9 @@ impl DriveHighLevelOperationConverter for MasternodeVoteTransitionAction { let previous_resource_vote_choice_to_remove = self.take_previous_resource_vote_choice_to_remove(); let vote = self.vote_owned(); - let prefunded_specialized_balance_id = vote.specialized_balance_id()?.ok_or(Error::Protocol(Box::new(ProtocolError::VoteError("vote does not have a specialized balance from where it can use to pay for processing (this should have been caught during validation)".to_string()))))?; + // Only a yes/no poll's id reads the platform version, and no vote naming one + // decodes before protocol version 15. + let prefunded_specialized_balance_id = vote.specialized_balance_id(platform_version)?.ok_or(Error::Protocol(Box::new(ProtocolError::VoteError("vote does not have a specialized balance from where it can use to pay for processing (this should have been caught during validation)".to_string()))))?; let drive_operations = vec![ IdentityOperation(IdentityOperationType::UpdateIdentityNonce { @@ -198,7 +200,7 @@ mod tests { // Build the vote and compute its expected balance ID before it is consumed let vote = make_resolved_vote(); let expected_balance_id = vote - .specialized_balance_id() + .specialized_balance_id(platform_version) .expect("expected specialized balance id") .expect("expected Some balance id"); diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_vote/v0/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_vote/v0/mod.rs index 76d8dfaf3bb..0ed67fec707 100644 --- a/packages/rs-drive/src/verify/voting/verify_masternode_vote/v0/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_masternode_vote/v0/mod.rs @@ -56,7 +56,9 @@ impl Drive { &masternode_pro_tx_hash, ); - let vote_id = vote.vote_poll_unique_id()?; + // The dispatcher sends a vote on a yes/no poll elsewhere, and a contested poll's id + // does not read the platform version. + let vote_id = vote.vote_poll_unique_id(platform_version)?; let mut query = Query::new(); query.insert_key(vote_id.to_vec()); @@ -156,7 +158,7 @@ mod tests { &masternode_pro_tx_hash, ); let vote_id = vote - .vote_poll_unique_id() + .vote_poll_unique_id(platform_version) .expect("expected vote poll unique id"); let mut query = Query::new(); query.insert_key(vote_id.to_vec()); diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs index 0668f3d8d2d..50955257c62 100644 --- a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs @@ -85,7 +85,7 @@ mod tests { identity_id: Identifier::new(voter), vote_poll: VotePoll::YesNoVotePoll(poll()), } - .construct_path_query() + .construct_path_query(platform_version) .expect("path query"); path_query.query.limit = None; drive @@ -187,7 +187,7 @@ mod tests { // An absence proof of the poll id under the contested index is refused. let mut query = Query::new(); - query.insert_key(poll().unique_id().expect("id").to_vec()); + query.insert_key(poll().unique_id(platform_version).expect("id").to_vec()); let wrong_branch = PathQuery::new( vote_contested_resource_identity_votes_tree_path_for_identity_vec(&voter), SizedQuery::new(query, None, None), diff --git a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs index 6b2779f9b21..a480d735a3d 100644 --- a/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs @@ -26,7 +26,7 @@ impl Drive { identity_id: Identifier::new(masternode_pro_tx_hash), vote_poll: VotePoll::YesNoVotePoll(vote.vote_poll().clone()), } - .construct_path_query()?; + .construct_path_query(platform_version)?; let (root_hash, mut proved_key_values) = if verify_subset_of_proof { GroveDb::verify_subset_query_with_absence_proof( proof, diff --git a/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/v0/mod.rs b/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/v0/mod.rs index 3da2b6e040e..be313dd81a1 100644 --- a/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/v0/mod.rs @@ -13,7 +13,7 @@ impl YesNoVotePollStateDriveQuery { proof: &[u8], platform_version: &PlatformVersion, ) -> Result<(RootHash, YesNoVotePollState), Error> { - let path_query = self.construct_path_query()?; + let path_query = self.construct_path_query(platform_version)?; let (root_hash, proved_key_values) = GroveDb::verify_query_with_absence_proof( proof, &path_query, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs index 9408dca929a..fef1a6aab79 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/mod.rs @@ -11,6 +11,10 @@ pub struct DPPVotingVersions { pub contested_document_vote_poll_stored_info_version: FeatureVersion, /// The stored info of a yes/no poll. `None` (not active) before protocol version 15. pub yes_no_vote_poll_stored_info_version: OptionalFeatureVersion, + /// How a yes/no poll's id is derived (`YesNoVotePoll::unique_id`). The id keys the poll's + /// stored trees and its end date index entry, and names its prefunded balance. `None` (not + /// active) before protocol version 15. + pub yes_no_vote_poll_id: OptionalFeatureVersion, /// The rules a yes/no poll's parameters must meet (`YesNoVotePoll::validate_parameters`). /// `None` (not active) before protocol version 15. pub yes_no_vote_poll_parameters: OptionalFeatureVersion, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs index 66e25f8e111..6daa84e921c 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v1.rs @@ -5,6 +5,7 @@ pub const VOTING_VERSION_V1: DPPVotingVersions = DPPVotingVersions { default_vote_poll_time_duration_test_network_ms: 1_209_600_000, //2 weeks contested_document_vote_poll_stored_info_version: 0, yes_no_vote_poll_stored_info_version: None, + yes_no_vote_poll_id: None, yes_no_vote_poll_parameters: None, yes_no_vote_poll_decision: None, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs index a064e9e33cc..86931cd3d8e 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v2.rs @@ -5,6 +5,7 @@ pub const VOTING_VERSION_V2: DPPVotingVersions = DPPVotingVersions { default_vote_poll_time_duration_test_network_ms: 5_400_000, //90 minutes contested_document_vote_poll_stored_info_version: 0, yes_no_vote_poll_stored_info_version: None, + yes_no_vote_poll_id: None, yes_no_vote_poll_parameters: None, yes_no_vote_poll_decision: None, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v3.rs index 3aef3feb762..9eaaf9c6353 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v3.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v3.rs @@ -2,10 +2,11 @@ use crate::version::dpp_versions::dpp_voting_versions::v2::VOTING_VERSION_V2; use crate::version::dpp_versions::dpp_voting_versions::DPPVotingVersions; /// Voting versions 3. Introduced in protocol version 15 with the yes/no poll kind: its stored -/// info, parameter rules and decision rules are `Some(0)` (`None`, not active, in every earlier -/// table). Everything else matches `VOTING_VERSION_V2`. +/// info, id, parameter rules and decision rules are `Some(0)` (`None`, not active, in every +/// earlier table). Everything else matches `VOTING_VERSION_V2`. pub const VOTING_VERSION_V3: DPPVotingVersions = DPPVotingVersions { yes_no_vote_poll_stored_info_version: Some(0), + yes_no_vote_poll_id: Some(0), yes_no_vote_poll_parameters: Some(0), yes_no_vote_poll_decision: Some(0), ..VOTING_VERSION_V2 diff --git a/packages/rs-platform-version/src/version/protocol_version.rs b/packages/rs-platform-version/src/version/protocol_version.rs index 0faa40ca87a..3049223ba2b 100644 --- a/packages/rs-platform-version/src/version/protocol_version.rs +++ b/packages/rs-platform-version/src/version/protocol_version.rs @@ -349,6 +349,7 @@ mod yes_no_vote_poll_gating_tests { "yes_no_vote_poll_stored_info_version", voting.yes_no_vote_poll_stored_info_version, ), + ("yes_no_vote_poll_id", voting.yes_no_vote_poll_id), ( "yes_no_vote_poll_parameters", voting.yes_no_vote_poll_parameters, diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 83878453ae1..d69f854a9f9 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -205,7 +205,7 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// `add_vote_poll_end_date_query_operations` 1, which adds only the entry when /// the batch already queues the end date's tree with other storage flags. /// Every other yes/no method is active from this version too and `None` -/// (not active) in every earlier table: the poll rules (`VOTING_VERSION_V3`), +/// (not active) in every earlier table: the poll's id and rules (`VOTING_VERSION_V3`), /// the end-of-block tally, record and clean-up (`DRIVE_ABCI_METHOD_VERSIONS_V11` /// voting), removing finished polls from the end date index /// (`remove_vote_poll_end_date_query_operations`) and the proof verifiers @@ -233,7 +233,7 @@ pub const PLATFORM_V15: PlatformVersion = PlatformVersion { contract_versions: CONTRACT_VERSIONS_V7, // changed: parse_property_constraints 1 lets countOf and sumOf match byte array keys (note 4); validate_document 2 checks documents natively (note 6); check_document_type_schema 1 checks schemas natively at registration (note 7); contract_storage_serialization_version 2 stores contracts compiled (note 8) document_versions: DOCUMENT_VERSIONS_V4, identity_versions: IDENTITY_VERSIONS_V1, - voting_versions: VOTING_VERSION_V3, // changed: the yes/no poll's stored info, parameter rules and decision rules are active (note 9) + voting_versions: VOTING_VERSION_V3, // changed: the yes/no poll's stored info, id, parameter rules and decision rules are active (note 9) token_versions: TOKEN_VERSIONS_V3, asset_lock_versions: DPP_ASSET_LOCK_VERSIONS_V1, methods: DPP_METHOD_VERSIONS_V3, diff --git a/packages/rs-sdk/src/platform/transition/vote.rs b/packages/rs-sdk/src/platform/transition/vote.rs index 080111dbcc4..8046e38e9f9 100644 --- a/packages/rs-sdk/src/platform/transition/vote.rs +++ b/packages/rs-sdk/src/platform/transition/vote.rs @@ -115,7 +115,7 @@ impl> PutVote for Vote { let settings = settings.unwrap_or_default(); - let vote_poll_id = self.vote_poll_unique_id()?; + let vote_poll_id = self.vote_poll_unique_id(sdk.version())?; let masternode_vote_transition = MasternodeVoteTransition::try_from_vote_with_signer( self.clone(), diff --git a/packages/rs-sdk/tests/fetch/mock_fetch_many.rs b/packages/rs-sdk/tests/fetch/mock_fetch_many.rs index 9f183f01594..e7f5760a2a9 100644 --- a/packages/rs-sdk/tests/fetch/mock_fetch_many.rs +++ b/packages/rs-sdk/tests/fetch/mock_fetch_many.rs @@ -20,6 +20,7 @@ use dpp::{ use dpp::{ platform_value::Value, prelude::Identifier, + version::PlatformVersion, voting::{ vote_choices::resource_vote_choice::ResourceVoteChoice, vote_polls::{ @@ -142,7 +143,7 @@ async fn should_match_public_mock_poll_expectations_with_effective_query_limits( async fn should_match_public_mock_identity_vote_expectations_with_effective_query_limits() { let poll = mock_vote_poll(); let expected: ResourceVotesByIdentity = [( - poll.unique_id().unwrap(), + poll.unique_id(PlatformVersion::latest()).unwrap(), Some(ResourceVoteWithCount { resource_vote: ResourceVote::V0(ResourceVoteV0 { vote_poll: poll, diff --git a/packages/rs-sdk/tests/fetch/prefunded_specialized_balance.rs b/packages/rs-sdk/tests/fetch/prefunded_specialized_balance.rs index d8b86b1d941..d624c2ff267 100644 --- a/packages/rs-sdk/tests/fetch/prefunded_specialized_balance.rs +++ b/packages/rs-sdk/tests/fetch/prefunded_specialized_balance.rs @@ -61,7 +61,7 @@ async fn test_prefunded_specialized_balance_ok() { // Vote poll specialized balance ID let balance_id = poll - .specialized_balance_id() + .specialized_balance_id(sdk.version()) .expect("vote poll specialized balance ID") .expect("must have specialized balance ID"); diff --git a/packages/rs-sdk/tests/fetch/vote_poll_default_limit.rs b/packages/rs-sdk/tests/fetch/vote_poll_default_limit.rs index f2763fe5bd5..0f371f69b4b 100644 --- a/packages/rs-sdk/tests/fetch/vote_poll_default_limit.rs +++ b/packages/rs-sdk/tests/fetch/vote_poll_default_limit.rs @@ -240,7 +240,7 @@ async fn should_verify_omitted_limit_identity_vote_page_when_more_than_default_v .map(|i| { let poll = fixture::poll(&contract, i); ( - poll.unique_id().expect("poll id"), + poll.unique_id(sdk.version()).expect("poll id"), Some(ResourceVoteWithCount { resource_vote: ResourceVote::V0(ResourceVoteV0 { vote_poll: poll, diff --git a/packages/rs-sdk/tests/fetch/vote_poll_default_limit_controls.rs b/packages/rs-sdk/tests/fetch/vote_poll_default_limit_controls.rs index e7e8a4cb41a..b83e8e8d676 100644 --- a/packages/rs-sdk/tests/fetch/vote_poll_default_limit_controls.rs +++ b/packages/rs-sdk/tests/fetch/vote_poll_default_limit_controls.rs @@ -74,7 +74,7 @@ async fn should_preserve_exact_voting_pages_for_short_full_descending_and_explic .map(|i| { let poll = fixture::poll(&contract, i); ( - poll.unique_id().unwrap(), + poll.unique_id(sdk.version()).unwrap(), Some(ResourceVoteWithCount { resource_vote: ResourceVote::V0(ResourceVoteV0 { vote_poll: poll,