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/book/src/error-handling/error-codes.md b/book/src/error-handling/error-codes.md
index 124a8df7c28..39bbeb647d0 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-40149 | Documents | `DocumentAlreadyPresentError` (40100), `DocumentNotFoundError` (40101), `DuplicateUniqueIndexError` (40105), `DocumentActionFeeAgreementNotSetError` (40132), `DocumentActionFeeAgreementMismatchError` (40133), `DocumentActionFeeMultiplierNotToleratedError` (40134), `DocumentActionFeeModeratorsShareMismatchError` (40139), `DocumentExpiredError` (40140), `DocumentContestMaximumContendersReachedError` (40141), `ReferencedDocumentRequirementNotMetError` (40142), `ReferencedDocumentTypeNotModeratedError` (40143), `ReferencedDocumentTypeModeratedError` (40144), `ReferencedDocumentRemovedError` (40145), `ReferencedDocumentTypeIndexOnlyError` (40146), `DocumentDeleteConstraintViolatedError` (40147), `ReferencedDocumentRuleReadInvalidError` (40148), `ReferencedDocumentPrecededError` (40149) |
| 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/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..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
@@ -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;
@@ -373,19 +372,24 @@ 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,
platform_version: &PlatformVersion,
- ) -> Result , ProtocolError> {
+ ) -> Result , ProtocolError> {
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,
platform_version: &PlatformVersion,
- ) -> Result, ProtocolError> {
+ ) -> Result , ProtocolError> {
self.indexes()
.values()
.find(|index| {
@@ -417,14 +421,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/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs
index 51a88b762d2..cdae5e712b1 100644
--- a/packages/rs-dpp/src/errors/consensus/codes.rs
+++ b/packages/rs-dpp/src/errors/consensus/codes.rs
@@ -423,6 +423,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 7b60b11c177..e6db59653ca 100644
--- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs
+++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs
@@ -123,6 +123,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;
@@ -745,6 +746,10 @@ pub enum StateError {
// `chooseFirst` groups by (protocol version 14).
#[error(transparent)]
ReferencedDocumentPrecededError(ReferencedDocumentPrecededError),
+
+ // Yes/no vote polls (protocol version 15).
+ #[error(transparent)]
+ YesNoVotePollNotAvailableForVotingError(YesNoVotePollNotAvailableForVotingError),
}
impl From for ConsensusError {
@@ -756,6 +761,7 @@ impl From for ConsensusError {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::block::block_info::BlockInfo;
use crate::consensus::basic::document::PropertyConstraintViolation;
use crate::consensus::state::contract_moderation::ContractModerationCounterpartyRole;
use crate::consensus::state::identity::identity_public_key_limit_not_set_error::KeyLimit;
@@ -772,9 +778,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::{YesNoMinimumVotingPower, YesNoVotePoll};
use crate::voting::vote_polls::VotePoll;
- use platform_value::Identifier;
+ use platform_value::{BinaryData, Identifier};
use std::collections::BTreeMap;
/// `StateError` is encoded by variant position, so inserting a variant
@@ -1674,5 +1682,22 @@ mod tests {
)),
175
);
+ // Yes/no vote polls (protocol version 15): 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: YesNoMinimumVotingPower::Absolute {
+ voting_power: 400
+ },
+ },
+ YesNoVotePollStatus::Started(BlockInfo::default())
+ )
+ )),
+ 176
+ );
}
}
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..be53422911d
--- /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 15).
+#[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 50d14da7120..76045bd4df6 100644
--- a/packages/rs-dpp/src/serialization/json/safe_fields.rs
+++ b/packages/rs-dpp/src/serialization/json/safe_fields.rs
@@ -150,6 +150,11 @@ impl JsonSafeFields for TokenTransition {}
#[cfg(feature = "state-transitions")]
impl JsonSafeFields for BatchedTransition {}
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::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 a47c0d07eb8..78ad1a003f4 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(
@@ -158,6 +159,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;
@@ -189,6 +191,9 @@ use crate::state_transition::token_unshield_with_shielded_fee_transition::{
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::*;
@@ -1215,8 +1220,20 @@ impl StateTransition {
}
StateTransition::IdentityTopUp(_)
| StateTransition::IdentityCreditWithdrawal(_)
- | StateTransition::IdentityCreditTransfer(_)
- | StateTransition::MasternodeVote(_) => ALL_VERSIONS,
+ | StateTransition::IdentityCreditTransfer(_) => ALL_VERSIONS,
+ // A vote that names a yes/no poll, as a yes/no vote or as a resource vote, exists
+ // from protocol version 15. 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 15 ever sees a yes/no poll.
+ StateTransition::MasternodeVote(masternode_vote) => match masternode_vote.vote() {
+ Vote::ResourceVote(resource_vote) => match resource_vote.vote_poll() {
+ VotePoll::ContestedDocumentResourceVotePoll(_) => ALL_VERSIONS,
+ VotePoll::YesNoVotePoll(_) => {
+ YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION..=LATEST_VERSION
+ }
+ },
+ Vote::YesNoVote(_) => YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION..=LATEST_VERSION,
+ },
StateTransition::IdentityCreditTransferToAddresses(_)
| StateTransition::IdentityTopUpFromAddresses(_)
| StateTransition::AddressFundsTransfer(_)
@@ -2738,6 +2755,72 @@ 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 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 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;
+ 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;
+ use crate::voting::votes::yes_no_vote::YesNoVote;
+
+ let vote_poll = YesNoVotePoll {
+ resource_path: vec![BinaryData::new(vec![7u8; 32])],
+ supermajority_numerator: 2,
+ supermajority_denominator: 3,
+ minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 400 },
+ };
+ let votes = [
+ Vote::YesNoVote(YesNoVote::V0(YesNoVoteV0 {
+ vote_poll: vote_poll.clone(),
+ vote_choice: YesNoAbstainVoteChoice::Yes,
+ })),
+ Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 {
+ vote_poll: VotePoll::YesNoVotePoll(vote_poll),
+ resource_vote_choice: ResourceVoteChoice::Abstain,
+ })),
+ ];
+ let version_14 = PlatformVersion::get(14).expect("protocol version 14");
+ let version_15 = PlatformVersion::get(15).expect("protocol version 15");
+
+ 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(),
+ YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION..=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_14),
+ Err(ProtocolError::StateTransitionError(
+ StateTransitionIsNotActiveError { .. }
+ ))
+ ));
+ assert_eq!(
+ StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_15)
+ .expect("expected the vote to be active at 15"),
+ 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 a3e16b456a5..b129e3c5b50 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
@@ -5,18 +5,53 @@ use crate::serialization::ValueConvertible;
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) ---
@@ -37,23 +72,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);
}
@@ -61,9 +88,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);
}
@@ -71,9 +98,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);
}
@@ -81,9 +108,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);
}
@@ -91,9 +118,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);
}
@@ -101,9 +128,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..3c7fb1f8d94
--- /dev/null
+++ b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/mod.rs
@@ -0,0 +1,223 @@
+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 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.
+ 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: {}, 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
+ )
+ }
+}
+
+/// 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
+ {
+ 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(),
+ )),
+ }
+ }
+
+ /// 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,
+ required_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,
+ required_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_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();
+ 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, 400, true, finalization_block)
+ .expect("finalize");
+ assert_eq!(
+ result,
+ YesNoVotePollResult {
+ passed: true,
+ yes_voting_power: 300,
+ no_voting_power: 100,
+ abstain_voting_power: 7,
+ required_voting_power: 400,
+ 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, 400, 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, 400, 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..d89954e853d
--- /dev/null
+++ b/packages/rs-dpp/src/voting/vote_info_storage/yes_no_vote_poll_stored_info/v0/mod.rs
@@ -0,0 +1,56 @@
+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,
+ required_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,
+ required_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..f6bc473cd36 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;
@@ -10,11 +11,13 @@ 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;
pub mod contested_document_resource_vote_poll;
+pub mod yes_no_vote_poll;
#[cfg_attr(
all(feature = "json-conversion", feature = "serde-conversion"),
@@ -44,6 +47,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 15). Appended: the bincode discriminant is the
+ /// variant position.
+ YesNoVotePoll(YesNoVotePoll),
}
impl fmt::Display for VotePoll {
@@ -52,6 +59,7 @@ impl fmt::Display for VotePoll {
VotePoll::ContestedDocumentResourceVotePoll(poll) => {
write!(f, "ContestedDocumentResourceVotePoll({})", poll)
}
+ VotePoll::YesNoVotePoll(poll) => write!(f, "YesNoVotePoll({})", poll),
}
}
}
@@ -63,21 +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(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(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
new file mode 100644
index 00000000000..f18803f05f8
--- /dev/null
+++ b/packages/rs-dpp/src/voting/vote_polls/yes_no_vote_poll/mod.rs
@@ -0,0 +1,877 @@
+#[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;
+
+/// 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";
+
+/// 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, 1/3 of 9 is 3.
+ Down,
+ /// 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.
+///
+/// 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(tag = "$type", rename_all = "camelCase")
+)]
+pub enum YesNoMinimumVotingPower {
+ /// A fixed voting power, at most `max_yes_no_vote_poll_minimum_voting_power`.
+ #[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.
+ 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, 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 {
+ 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),
+ VotingPowerRounding::DownPlusOne => share / denominator + 1,
+ VotingPowerRounding::UpMinusOne => {
+ share.div_ceil(denominator).saturating_sub(1)
+ }
+ };
+ 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",
+ VotingPowerRounding::DownPlusOne => "down plus one",
+ VotingPowerRounding::UpMinusOne => "up minus one",
+ }
+ ),
+ }
+ }
+}
+
+/// 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. 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`] 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"),
+ 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))]
+// 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,
+ /// 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: YesNoMinimumVotingPower,
+}
+
+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, 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
+ {
+ 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(),
+ )),
+ }
+ }
+
+ /// 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(
+ "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 == 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 ({})",
+ 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(())
+ }
+
+ /// 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, 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
+ {
+ 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(),
+ )),
+ }
+ }
+
+ /// Whether the poll passes with these final tallies, given the total voting power of the
+ /// 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
+ {
+ 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(),
+ )),
+ }
+ }
+
+ /// 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_v0(total_voting_power) as u64 {
+ return false;
+ }
+ yes * self.supermajority_denominator as u64 > cast * self.supermajority_numerator as u64
+ }
+
+ /// 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.
+ ///
+ /// # 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)
+ }
+
+ /// 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)))
+ }
+}
+
+#[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: YesNoMinimumVotingPower::Absolute { 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: YesNoMinimumVotingPower::Absolute { voting_power: 300 },
+ ..poll()
+ };
+ // 300 cast: two thirds is 200, so 201 passes and 200 does not.
+ 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_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_v0(399, 0, 0));
+ assert!(poll.passes_v0(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_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_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_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_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_v0(10),
+ 400
+ );
+ // No overflow on the largest total.
+ let half = YesNoMinimumVotingPower::FractionOfTotal {
+ numerator: 1,
+ denominator: 2,
+ rounding: VotingPowerRounding::Up,
+ };
+ assert_eq!(half.resolve_v0(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_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_v0(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 { voting_power: 0 },
+ YesNoMinimumVotingPower::Absolute {
+ voting_power: 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 {
+ voting_power: 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]
+ fn should_not_overflow_on_the_largest_tallies() {
+ let poll = YesNoVotePoll {
+ supermajority_numerator: 254,
+ supermajority_denominator: 255,
+ ..poll()
+ };
+ assert!(poll.passes_v0(u32::MAX, 0, 0));
+ assert!(!poll.passes_v0(u32::MAX, u32::MAX, 0));
+ }
+
+ #[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 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,
+ 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 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()
+ };
+ let other_rule = YesNoVotePoll {
+ supermajority_numerator: 1,
+ supermajority_denominator: 2,
+ ..poll()
+ };
+ let other_minimum = YesNoVotePoll {
+ minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 401 },
+ ..poll()
+ };
+ for other in [other_path, other_rule, other_minimum] {
+ assert_ne!(other.unique_id(platform_version).expect("id"), base);
+ }
+ assert_eq!(
+ poll()
+ .specialized_balance_id(platform_version)
+ .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(PlatformVersion::latest()).expect("id");
+ 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",
+ );
+ not_active(poll().unique_id(platform_version).map(|_| ()), "unique_id");
+ not_active(
+ poll().specialized_balance_id(platform_version).map(|_| ()),
+ "specialized_balance_id",
+ );
+ }
+}
+
+#[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: YesNoMinimumVotingPower::Absolute { 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": { "$type": "absolute", "votingPower": 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!({ "$type": "fractionOfTotal", "numerator": 1, "denominator": 3, "rounding": "up" })
+ );
+ let recovered = YesNoVotePoll::from_json(json).expect("from_json");
+ 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"]["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;
+ 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": { "$type": "absolute", "votingPower": 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..a59db1f5c65 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;
@@ -13,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};
@@ -40,6 +44,9 @@ use serde::{Deserialize, Serialize};
#[platform_serialize(limit = 15000, unversioned)]
pub enum Vote {
ResourceVote(ResourceVote),
+ /// A vote on a yes/no poll (protocol version 15). Appended: the bincode discriminant is
+ /// the variant position.
+ YesNoVote(YesNoVote),
}
// Manual impl because Vote is a flat enum (not versioned V0/V1).
@@ -53,14 +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::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::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/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 d1253100944..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};
@@ -26,7 +27,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 +36,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 {
@@ -44,7 +65,58 @@ impl Default for ResourceVoteV0 {
}
impl ResourceVoteV0 {
- pub fn vote_poll_unique_id(&self) -> Result {
- self.vote_poll.unique_id()
+ /// 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(),
+ )),
+ }
+ }
+
+ /// 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)
+ }
+}
+
+#[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-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..87d327779c1
--- /dev/null
+++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/mod.rs
@@ -0,0 +1,20 @@
+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_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..74079ff46b4
--- /dev/null
+++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/accessors/v0/mod.rs
@@ -0,0 +1,11 @@
+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 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..57eb1073a6d
--- /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 15).
+#[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::{YesNoMinimumVotingPower, 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: YesNoMinimumVotingPower::Absolute { 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": { "$type": "absolute", "votingPower": 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": { "$type": "absolute", "votingPower": 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..bfc9d564b12
--- /dev/null
+++ b/packages/rs-dpp/src/voting/votes/yes_no_vote/v0/mod.rs
@@ -0,0 +1,38 @@
+#[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,
+};
+#[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,
+}
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 3820f30b922..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
@@ -327,6 +327,11 @@ 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 15. The arm only keeps the match exhaustive.
+ VotePoll::YesNoVotePoll(yes_no) => yes_no
+ .specialized_balance_id(platform_version)
+ .map_err(Error::Protocol),
}
})
.collect::, Error>>()?;
@@ -910,7 +915,7 @@ impl Platform {
/// When transitioning to version 15 we re-store the DPNS contract whose v3
/// schema checks a domain create with keywords instead of most of its data
- /// trigger.
+ /// trigger, and create the vote decisions trees that yes/no polls live in.
fn transition_to_version_15(
&self,
block_info: &BlockInfo,
@@ -930,6 +935,14 @@ impl Platform {
platform_version,
)?;
+ // Vote decisions trees: the active yes/no polls and the identity votes index of the
+ // decisions branch. Fresh chains call the same helper from
+ // `create_initial_state_structure` v4, behind a `>= 15` check on the chain's initial
+ // protocol version, so both node populations build the decisions Merk from the same
+ // sequence of inserts.
+ self.drive
+ .insert_vote_decisions_trees(Some(transaction), platform_version)?;
+
Ok(())
}
}
@@ -2511,6 +2524,74 @@ mod tests {
);
}
+ /// The only way a running network gets the vote decisions trees is this upgrade hook.
+ /// They are absent at 14 and present after the transition, and the votes subtree is then
+ /// 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 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,
+ };
+ use drive::util::grove_operations::DirectQueryType;
+
+ let platform_version = PlatformVersion::latest();
+ let born_at_15 = TestPlatformBuilder::new()
+ .with_initial_protocol_version(15)
+ .build_with_mock_rpc()
+ .set_genesis_state();
+ let upgraded = TestPlatformBuilder::new()
+ .with_initial_protocol_version(14)
+ .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 14 has no decisions {key} tree"
+ );
+ }
+
+ upgraded
+ .transition_to_version_15(&BlockInfo::default(), &transaction, platform_version)
+ .expect("expected version 15 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_15,
+ &upgraded,
+ &transaction,
+ vec![vec![RootTree::Votes as u8]],
+ );
+ assert!(
+ diffs.is_empty(),
+ "the votes subtree differs between a chain born at version 15 and one upgraded to \
+ it:\n{}",
+ diffs.join("\n")
+ );
+ }
+
#[test]
fn should_create_the_document_ttl_trees_on_transition_to_version_14() {
let platform_version = PlatformVersion::latest();
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 0eb6ba217aa..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
@@ -1,18 +1,28 @@
+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;
+use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods};
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;
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, 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};
+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;
@@ -24,7 +34,9 @@ where
C: CoreRPCLike,
{
/// Checks for ended vote polls, awarding a tie to the earliest contender, after removing
- /// the end dates left with no vote poll among the first due
+ /// the end dates left with no vote poll among the first due. From protocol version 15 it
+ /// 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,
@@ -72,8 +84,48 @@ where
platform_version,
)?;
+ // 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. 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) => {
+ 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,
+ block_info,
+ &yes_no_vote_poll,
+ transaction,
+ platform_version,
+ )? {
+ finished_yes_no_vote_polls.push(FinishedYesNoVotePoll {
+ poll_id,
+ prefunded_balance_id: yes_no_vote_poll
+ .specialized_balance_id(platform_version)?,
+ voters,
+ });
+ }
+ }
+ 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 = vote_polls_by_timestamp.into_iter().map(|(end_date, vote_polls)| {
+ 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)?;
@@ -224,6 +276,88 @@ 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 !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,
+ 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. 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,
+ block_info: &BlockInfo,
+ vote_poll: &YesNoVotePoll,
+ transaction: TransactionArg,
+ platform_version: &PlatformVersion,
+ ) -> Result>, Error> {
+ let mut tally =
+ self.tally_votes_for_yes_no_vote_poll(vote_poll, transaction, platform_version)?;
+ 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.
+ 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, platform_version).into(),
+ )
+ })
+ .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,
+ stored_info,
+ &tally,
+ total_voting_power,
+ 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(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 f4c4112147c..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
@@ -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 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- >,
+ 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_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..ad26f40b78a
--- /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,78 @@
+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::prelude::TimestampMillis;
+use dpp::version::PlatformVersion;
+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: 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.
+ ///
+ /// `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.
+ /// - `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.
+ ///
+ /// # 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,
+ end_date_entries: &[(Identifier, TimestampMillis)],
+ finished_vote_polls: &[FinishedYesNoVotePoll],
+ transaction: TransactionArg,
+ platform_version: &PlatformVersion,
+ ) -> Result<(), Error> {
+ match platform_version
+ .drive_abci
+ .methods
+ .voting
+ .clean_up_after_yes_no_vote_polls_end
+ {
+ Some(0) => self.clean_up_after_yes_no_vote_polls_end_v0(
+ block_info,
+ end_date_entries,
+ finished_vote_polls,
+ transaction,
+ platform_version,
+ ),
+ 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/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..ae93c6d760f
--- /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,82 @@
+use super::FinishedYesNoVotePoll;
+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 drive::grovedb::TransactionArg;
+
+impl Platform
+where
+ C: CoreRPCLike,
+{
+ #[inline(always)]
+ pub(super) fn clean_up_after_yes_no_vote_polls_end_v0(
+ &self,
+ block_info: &BlockInfo,
+ end_date_entries: &[(Identifier, TimestampMillis)],
+ finished_vote_polls: &[FinishedYesNoVotePoll],
+ transaction: TransactionArg,
+ platform_version: &PlatformVersion,
+ ) -> 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.
+ self.drive.remove_vote_poll_end_date_query_operations(
+ end_date_entries,
+ &mut operations,
+ transaction,
+ platform_version,
+ )?;
+
+ // 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 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,
+ )?;
+ }
+ }
+
+ // What is left of the prefunded balances goes to processing, as for contested polls.
+ self.empty_vote_poll_prefunded_balances_operations_v1(
+ finished_vote_polls
+ .iter()
+ .map(|vote_poll| Ok(vote_poll.prefunded_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(())
+ }
+}
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..4e2e7514f20
--- /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,72 @@
+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, 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;
+
+mod v0;
+
+impl Platform
+where
+ C: CoreRPCLike,
+{
+ /// 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.
+ /// - `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,
+ platform_version: &PlatformVersion,
+ ) -> Result {
+ match platform_version
+ .drive_abci
+ .methods
+ .voting
+ .keep_record_of_finished_yes_no_vote_poll
+ {
+ Some(0) => self.keep_record_of_finished_yes_no_vote_poll_v0(
+ block_info,
+ vote_poll,
+ stored_info,
+ tally,
+ total_voting_power,
+ transaction,
+ platform_version,
+ ),
+ 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/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..4d7de36438b
--- /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, 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;
+
+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 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,
+ tally.abstain_voting_power,
+ required_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..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,8 +1,35 @@
+use dpp::dashcore_rpc::dashcore_rpc_json::MasternodeType;
+use dpp::version::PlatformVersion;
+
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;
+
+/// 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 => 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/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..efcb9eec4c0
--- /dev/null
+++ b/packages/rs-drive-abci/src/execution/platform_events/voting/on_yes_no_vote_poll_finished/mod.rs
@@ -0,0 +1,62 @@
+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.
+ ///
+ /// # 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,
+ 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
+ {
+ Some(0) => self.on_yes_no_vote_poll_finished_v0(
+ block_info,
+ vote_poll,
+ result,
+ transaction,
+ platform_version,
+ ),
+ 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/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..2be5e948cb2
--- /dev/null
+++ b/packages/rs-drive-abci/src/execution/platform_events/voting/tally_votes_for_yes_no_vote_poll/mod.rs
@@ -0,0 +1,51 @@
+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.
+ ///
+ /// # 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,
+ transaction: TransactionArg,
+ platform_version: &PlatformVersion,
+ ) -> Result {
+ match platform_version
+ .drive_abci
+ .methods
+ .voting
+ .tally_votes_for_yes_no_vote_poll
+ {
+ 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-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 e60341912b8..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
@@ -16,7 +16,6 @@ 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;
@@ -60,11 +59,11 @@ 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(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
@@ -91,7 +90,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);
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 c3f0d25aa7e..5afe657671d 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()
),
- "0ed48425e10d0aa37b68f4f1e52c773a667304f9b510a4151b1dbc323f350076"
+ "24e77c57edbf5294e7b43df5af21fcd055697120124f3912021c2e41c954f4e0"
);
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" => (
"6a823c2db94612652926e6b9c1ae7feec9c2547dfd4cfc4e474318da1c16d59c",
- "5ec2378c4d9015a2b5ad8552797497e7c53f3a80b31b6491db88e43e27972060",
+ "a80509bb285ede04d5c921e432a6d38021bfa2b38e2855cf8f0e735fd20285d9",
),
"valid-payment" => (
"a1308c6fe05b0a60cfee4fa0d43f7bda5856b02f7d11f3499fd1acd866328aa3",
- "90439b1b17e8d5a003958e2cb256f4bc47c7d08afaf3d18f0e36b668c00ac9df",
+ "f6694678f1b835c476af2012fd217c67524f4f8d3ca5e4f9facbcec0b205081b",
),
"failed-payment" => (
"d9b5fe191841bea952494f2a45e3833f925f4d367a1937b2ef1235a1aa01f51e",
- "cbccba78463e46aaab22351f696d704651d76095d8d09cb820d7d3d901d0b0da",
+ "4a1e0a2664ef5d529920510c003340cf5ecdc443d7478b08974a821076a7c8cc",
),
"within-bundle-repeat" => (
"8023e03a697a20f6a78396a05503a4bc583ca8f69e19bd7ee07a6ae0ce7be478",
- "fa77338960e55ad5ce67a338127a0ae77fcae262f1a23bc237b1d9a15dcc61fa",
+ "a3cbb2d69941b402cb5fc9c564da81d4716a168355fe4ce4d3ae530d99e40247",
),
"stored-repeat" => (
"05652ac85c972902e8c09d20a7d8586e17e9434a478b335da136ad85f73def01",
- "575b104c33a9a4425b3fbb58a6a6d5b3d5790ba04c84ac11f4bb432e8c58f3fd",
+ "20b47800939b417e433c695208924af0582df67c33edbf085f1fba297c9e6a06",
),
"open-mint-group" => (
"5c68381297c9bf68c5e85ebe6bb9e0f1289e2c07718eecfb5e6773d6009455bb",
- "8f312783271abade3cd47d38981225a72477cb7b1cd3812028d514119eca1517",
+ "5d22e1accc47d79db4b31a93e35a88b70e04412884239c4e04a05a0e2dde7f95",
),
"closing-mint-group" => (
"2cedc16732c76e0605b5f46085db3d69c12936a5af55a2a82f52ec9e9ca67e38",
- "5dac51fdee8bb00560d035496286d8ca5703bdffbf444d3fb82af5f2d050803c",
+ "bc346f8d036f51e82f5f7af69a9ece71f34501e21c848f1d9df7e17c76dd847f",
),
"open-burn-group" => (
"e664a045f020ff84de1ca98f364fbabccfc141ad46aa82c79491a340bb0db5f8",
- "45d313dd0b30e5875b4c207c272d08b23241cd8d22df976ef3abe5fab00a04d5",
+ "2b6f1d4e5b67d252c1aa76c466e716106107a98ec96d64eda488c37216611cd9",
),
"closing-burn-group" => (
"93e1732286f168c4a91d2765931e181c5721bfca1bfd5f93b8baacca03e00b2b",
- "a89f568a0068341804b030000219994a544a48ef5f2591085ee98820c75923cb",
+ "764422c7f8a87ad8165a7d38c795dcb57ea67d092fa751e7f0c50c06d52fbba9",
),
_ => unreachable!("enumerated characterization case"),
};
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/charter_election_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/charter_election_tests.rs
index b9f0bbdfa03..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();
@@ -1148,7 +1126,10 @@ async fn should_keep_the_generic_windows_and_fund_for_a_dpns_contest() {
let [(end_date, VotePoll::ContestedDocumentResourceVotePoll(poll))]: [_; 1] =
end_dates(&platform, platform_version)
.try_into()
- .expect("expected one contest");
+ .expect("expected one contest")
+ else {
+ panic!("expected a contested document resource vote poll");
+ };
assert_eq!(poll.contract_id, dpns_contract.id());
let ContestedDocumentVotePollStatus::Started(start_block) =
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..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")
}
@@ -78,11 +77,30 @@ fn end_due_vote_polls(
.expect("expected to commit the block");
}
+/// 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(),
+ ]
+}
+
#[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();
+ 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(
+ 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 +198,16 @@ 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();
+ 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(
+ 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/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs
index 6150b629f0e..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
@@ -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,
})),
}
@@ -4321,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 d9083e260b4..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
@@ -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};
@@ -80,9 +81,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);
@@ -676,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"
@@ -734,7 +746,15 @@ 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();
+ 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(
+ 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));
@@ -742,7 +762,7 @@ async fn should_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")
};
@@ -830,7 +850,13 @@ 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();
+ 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) {
+ 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(
@@ -868,7 +894,7 @@ async fn should_end_a_contest_behind_end_dates_left_empty() {
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/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/state/v0/mod.rs
index cde77903a56..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
@@ -94,6 +94,13 @@ impl MasternodeVoteStateTransitionStateValidationV0 for MasternodeVoteTransition
}
}
}
+ // 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 284f7e00f93..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
@@ -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;
@@ -17,6 +18,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::paths::{
RESOURCE_ABSTAIN_VOTE_TREE_KEY_U8_32, RESOURCE_LOCK_VOTE_TREE_KEY_U8_32,
RESOURCE_STORED_INFO_KEY_U8_32,
@@ -145,6 +148,39 @@ impl MasternodeVoteStateTransitionStateValidationV1 for MasternodeVoteTransition
}
}
}
+ // Reached from protocol version 15 only: at 14, which also selects this generation,
+ // a yes/no vote is not active (`StateTransition::active_version_range`) and never
+ // decodes.
+ 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..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
@@ -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;
@@ -7,11 +8,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, MasternodeType};
+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;
@@ -57,65 +60,43 @@ 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_v0(
+ 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))
}
}
}
+ // Unreachable: this v0 is selected up to protocol version 14, 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",
+ )))
+ }
}
}
+ // Unreachable for the same reason: no yes/no vote decodes before 15.
+ Vote::YesNoVote(_) => {
+ return Err(Error::Execution(ExecutionError::CorruptedCodeExecution(
+ "yes/no votes need transform_into_action version 1",
+ )))
+ }
}
}
- 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_v0(platform, self.pro_tx_hash()) {
+ Ok(masternode) => masternode,
+ Err(error) => return Ok(ConsensusValidationResult::new_with_error(error)),
};
let strength = match masternode.node_type {
@@ -136,3 +117,70 @@ 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. 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,
+ 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_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> {
+ 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
new file mode 100644
index 00000000000..e21ce0320f1
--- /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,122 @@
+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_v0, voting_masternode_v0,
+ MasternodeVoteStateTransitionTransformIntoActionValidationV0,
+};
+use crate::platform_types::platform::PlatformRef;
+use dpp::consensus::state::state_error::StateError;
+use dpp::consensus::state::voting::vote_poll_not_found_error::VotePollNotFoundError;
+use dpp::consensus::ConsensusError;
+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 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 15) also transforms a vote on a yes/no poll, with the rule
+ /// 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.
+ fn transform_into_action_v1(
+ &self,
+ platform: &PlatformRef,
+ validation_mode: ValidationMode,
+ tx: TransactionArg,
+ platform_version: &PlatformVersion,
+ ) -> Result, Error> {
+ 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_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
+ let vote_poll = yes_no_vote.vote_poll();
+ 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,
+ tx,
+ &mut vec![],
+ platform_version,
+ )?;
+ match previous_vote_to_replace_v0(
+ 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 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 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(),
+ voting_address: masternode.state.voting_address,
+ 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(),
+ }
+ .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..b30c972fa3e
--- /dev/null
+++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/yes_no_tests.rs
@@ -0,0 +1,1462 @@
+//! 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,
+};
+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,
+};
+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::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::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::{
+ YesNoVotePollResult, YesNoVotePollStatus,
+};
+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;
+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::{
+ YesNoVotePollState, YesNoVotePollStateDriveQuery,
+};
+use drive::util::test_helpers::vote_poll_end_dates;
+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: YesNoMinimumVotingPower::Absolute {
+ voting_power: 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(platform_version)
+ .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
+}
+
+/// 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)> {
+ check_tx_at_all_levels(platform, serialized_transition)
+ .into_iter()
+ .map(|(level, result)| {
+ let verdict = result
+ .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). 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,
+ 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 check_tx_verdicts = check_tx_verdicts(platform, &serialized_transition);
+ let platform_state = platform.state.load();
+ let processing_result = process_test_state_transition(
+ platform,
+ masternode_vote_transition,
+ &platform_state,
+ platform_version,
+ );
+ match processing_result.into_execution_results().remove(0) {
+ 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(_)
+ ))
+ );
+ // 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 || refused,
+ "check_tx {level:?} must refuse a yes/no vote a block refuses ({other:?}), got \
+ {verdict:?}"
+ );
+ 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`, 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,
+) {
+ 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,
+ core_height: 42,
+ epoch: Default::default(),
+ };
+ 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(
+ &last_committed_platform_state,
+ &block_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,
+) -> YesNoVotePollState {
+ YesNoVotePollStateDriveQuery {
+ vote_poll: vote_poll.clone(),
+ }
+ .execute_no_proof(&platform.drive, None, &mut vec![], platform_version)
+ .expect("expected the poll state")
+}
+
+/// The recorded result of a poll that ended.
+fn finished_result(
+ platform: &TempPlatform,
+ vote_poll: &YesNoVotePoll,
+ platform_version: &PlatformVersion,
+) -> 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
+ .fetch_identity_yes_no_vote(voter, vote_poll_id, None, &mut vec![], platform_version)
+ .expect("vote")
+}
+
+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 = 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);
+ assert_eq!(passing_result.abstain_voting_power, 2);
+ assert_eq!(
+ passing_result.finalization_block.time_ms,
+ END_DATE + 300_000
+ );
+
+ 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);
+ 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 = finished_result(&platform, &vote_poll, platform_version);
+ assert!(!result.passed);
+ assert_eq!(result.yes_voting_power, 5);
+ assert_eq!(result.no_voting_power, 0);
+ 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_three = half_rounded_up(b"evonode and three masternodes");
+ open_poll(&platform, &evonode_alone, FUND, platform_version);
+ open_poll(&platform, &evonode_and_three, FUND, platform_version);
+
+ // 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_voting_power = |platform_state: &PlatformState| -> u32 {
+ platform_state
+ .full_masternode_list()
+ .values()
+ .map(|masternode| {
+ u32::from(masternode_vote_strength(
+ &masternode.node_type,
+ platform_version,
+ ))
+ })
+ .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_three)] {
+ 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");
+ }
+ 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,
+ END_DATE + 300_000,
+ Some(closing_block_state),
+ platform_version,
+ );
+
+ 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 = 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);
+}
+
+#[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 = finished_result(&platform, &vote_poll, platform_version);
+ 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(platform_version).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");
+ 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!(
+ identity_vote(&platform, yes_voters[0], vote_poll_id, platform_version),
+ 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 = 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);
+
+ // After the end: the record with the result, and nothing else.
+ let state = poll_state(&platform, &vote_poll, platform_version);
+ 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!(
+ (
+ 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.is_empty());
+ for voter in yes_voters.iter().chain(no_voters.iter()) {
+ assert_eq!(
+ identity_vote(&platform, *voter, vote_poll_id, platform_version),
+ 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_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.
+ 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.
+ 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
+ .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(platform_version)
+ .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(platform_version).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 };
+ 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),
+ 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);
+ // 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,
+ 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::PrefundedSpecializedBalanceNotFoundError(_)
+ ))
+ );
+ assert_eq!(
+ identity_vote(
+ &platform,
+ pro_tx_hash,
+ vote_poll.unique_id(platform_version).expect("id"),
+ platform_version
+ ),
+ None
+ );
+ 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 = finished_result(&platform, &vote_poll, platform_version);
+ 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]
+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(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]];
+ 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");
+ // The yes voters, then the no voters.
+ assert_eq!(voters, vec![yes_voters[2], no_voters[1]]);
+ for voter in &removed {
+ assert_eq!(
+ identity_vote(&platform, *voter, vote_poll_id, platform_version),
+ None
+ );
+ }
+ assert_eq!(
+ 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 = finished_result(&platform, &vote_poll, platform_version);
+ 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, 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(
+ &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 = *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,
+ &vote_poll,
+ contest_end_date,
+ FUND,
+ platform_version,
+ );
+ cast_votes(
+ &mut platform,
+ &vote_poll,
+ YesNoAbstainVoteChoice::Yes,
+ 1,
+ 100,
+ platform_version,
+ )
+ .await;
+
+ 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 = vote_poll_end_dates(&platform.drive, platform_version);
+ assert!(end_dates.is_empty());
+ 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.
+ 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
+/// 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| {
+ vote_poll_end_dates(&platform.drive, platform_version)
+ };
+ 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());
+}
+
+/// 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(platform_version)
+ .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 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.
+#[tokio::test]
+async fn should_run_a_contest_unchanged_and_strip_yes_no_polls_at_protocol_version_13() {
+ run_a_contest_and_strip_yes_no_polls(13).await;
+}
+
+/// Protocol version 14 selects the masternode vote transform v0 and state validation v1, vote
+/// 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() {
+ run_a_contest_and_strip_yes_no_polls(14).await;
+}
+
+async fn run_a_contest_and_strip_yes_no_polls(protocol_version: u32) {
+ 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(protocol_version).expect("protocol version");
+ let mut platform = TestPlatformBuilder::new()
+ .with_initial_protocol_version(protocol_version)
+ .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 15", 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");
+ // 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.
+ 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 = vote_poll_end_dates(&platform.drive, platform_version);
+ assert!(end_dates.is_empty());
+}
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 61e02898e96..0d8c5b3d6eb 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
@@ -186,7 +186,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;
@@ -206,7 +206,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;
@@ -901,6 +903,27 @@ 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,
+ ) -> 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:?}"),
+ }
+ }
+
#[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-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-abci/tests/strategy_tests/verify_state_transitions.rs b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs
index ab37b75ee34..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,28 +970,38 @@ 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();
// 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(),
+ };
+ 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 6d99f41c026..5615ae6ea1d 100644
--- a/packages/rs-drive/grovedb-structure.json
+++ b/packages/rs-drive/grovedb-structure.json
@@ -4587,7 +4587,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",
@@ -5116,8 +5116,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": 15,
+ "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": 15,
+ "presence": "always",
+ "source": "packages/rs-drive/src/drive/votes/paths.rs",
+ "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",
+ "key": {
+ "type": "dynamic",
+ "name": "vote_poll_id",
+ "matcher": {
+ "type": "len",
+ "len": 32
+ },
+ "encoding": "hash32",
+ "description": "The double sha256 of the domain tag `dash platform yes/no vote poll id` followed by the serialized vote poll"
+ },
+ "kinds": [
+ "Item"
+ ],
+ "value": "bincode YesNoVoteReferenceStorageForm: the choice and how many times the masternode voted on the poll",
+ "since": 15,
+ "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": 15,
+ "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 domain tag `dash platform yes/no vote poll id` followed by the serialized vote poll"
+ },
+ "kinds": [
+ "Tree"
+ ],
+ "since": 15,
+ "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": 15,
+ "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": 15,
+ "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": 15,
+ "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": 15,
+ "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": 15,
+ "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": 15,
+ "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": 15,
+ "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",
@@ -5152,9 +5395,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",
@@ -5170,15 +5414,16 @@
"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"
],
"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",
@@ -6266,6 +6511,30 @@
}
}
},
+ "votes.decisions": {
+ "origin": "genesis@15",
+ "tree": {
+ "hex": "70",
+ "left": {
+ "hex": "69"
+ }
+ }
+ },
+ "votes.decisions.active_polls.poll": {
+ "origin": "fixture yes_no_vote_polls@15",
+ "tree": {
+ "hex": "02",
+ "left": {
+ "hex": "01",
+ "left": {
+ "hex": "00"
+ }
+ },
+ "right": {
+ "hex": "03"
+ }
+ }
+ },
"withdrawals": {
"origin": "genesis@15",
"tree": {
diff --git a/packages/rs-drive/src/drive/contract/migration/rekey_unsigned_integer_index_values.rs b/packages/rs-drive/src/drive/contract/migration/rekey_unsigned_integer_index_values.rs
index c9cb93e10aa..17e38d50150 100644
--- a/packages/rs-drive/src/drive/contract/migration/rekey_unsigned_integer_index_values.rs
+++ b/packages/rs-drive/src/drive/contract/migration/rekey_unsigned_integer_index_values.rs
@@ -1637,7 +1637,9 @@ mod tests {
panic!("expected the one vote, got {votes:?}");
};
assert_eq!(*vote_count, 1);
- let VotePoll::ContestedDocumentResourceVotePoll(poll) = vote.vote_poll();
+ let VotePoll::ContestedDocumentResourceVotePoll(poll) = vote.vote_poll() else {
+ panic!("expected a contested document resource vote poll");
+ };
assert_eq!(poll.index_values, vec![Value::U8(200)]);
assert_eq!(vote.resource_vote_choice(), towards_first);
}
@@ -1709,7 +1711,10 @@ mod tests {
.try_into()
.expect("expected one end date");
let [VotePoll::ContestedDocumentResourceVotePoll(poll)]: [VotePoll; 1] =
- polls.try_into().expect("expected one poll");
+ polls.try_into().expect("expected one poll")
+ else {
+ panic!("expected a contested document resource vote poll");
+ };
assert_eq!(poll.index_values, vec![Value::U8(200)]);
let tally: FinalizedContestedDocumentVotePollDriveQueryExecutionResult =
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/initialization/v4/mod.rs b/packages/rs-drive/src/drive/initialization/v4/mod.rs
index fb4cf1b4f39..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;
@@ -126,6 +127,16 @@ impl Drive {
// withdrawals Merk is built by the same sequence of inserts on both node populations.
self.insert_withdrawal_limit_trees(transaction, platform_version)?;
+ // Vote decisions trees (protocol version 15): the active yes/no polls and the identity
+ // votes index under the votes tree's decisions branch, which the batch above creates.
+ // Chain-creation content, so it compares the chain's initial protocol version: a chain
+ // 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 >= YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION {
+ self.insert_vote_decisions_trees(transaction, platform_version)?;
+ }
+
Ok(())
}
}
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..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
@@ -1,4 +1,5 @@
mod v0;
+mod v1;
use crate::drive::Drive;
@@ -22,12 +23,15 @@ 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
.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 +41,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/v0/mod.rs b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v0/mod.rs
index 11d83100c88..3af376a7604 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
@@ -16,7 +16,8 @@ use dpp::platform_serialization::PLATFORM_BINCODE_CONFIG;
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
@@ -34,26 +35,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)
@@ -143,6 +130,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)]
@@ -291,8 +312,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();
@@ -317,8 +339,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
new file mode 100644
index 00000000000..831e07d98d7
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/cleanup/remove_all_votes_given_by_identities/v1/mod.rs
@@ -0,0 +1,126 @@
+use crate::drive::Drive;
+
+use crate::error::Error;
+
+use crate::drive::votes::paths::{
+ 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::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, TreeType};
+use std::collections::BTreeSet;
+
+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 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>,
+ block_height: BlockHeight,
+ network: Network,
+ chain_id: &str,
+ transaction: TransactionArg,
+ platform_version: &PlatformVersion,
+ ) -> Result<(), Error> {
+ 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
+ // 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,
+ )?;
+ if !has_decisions_index {
+ return Ok(());
+ }
+
+ let mut deletion_batch = vec![];
+ 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()?)?;
+ 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(),
+ storage_form.vote_choice,
+ );
+ 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,
+ )?;
+ }
+ }
+
+ // 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. 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 = 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::for_known_path_key_drop_flat_tree(
+ identity_votes_path.clone(),
+ identifier_bytes,
+ TreeType::NormalTree,
+ ));
+ }
+ }
+
+ if !deletion_batch.is_empty() {
+ self.apply_batch_low_level_drive_operations(
+ None,
+ transaction,
+ deletion_batch,
+ &mut vec![],
+ &platform_version.drive,
+ )
+ .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_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_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..b32d4043ab3
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/mod.rs
@@ -0,0 +1,57 @@
+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, 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.
+ ///
+ /// # 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)],
+ 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
+ {
+ Some(0) => self.remove_vote_poll_end_date_query_operations_v0(
+ vote_polls,
+ batch_operations,
+ transaction,
+ platform_version,
+ ),
+ 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/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..c5b894acdd9
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/cleanup/remove_vote_poll_end_date_query_operations/v0/mod.rs
@@ -0,0 +1,33 @@
+use crate::drive::Drive;
+use crate::error::Error;
+use crate::fees::op::LowLevelDriveOperation;
+use dpp::identifier::Identifier;
+use dpp::identity::TimestampMillis;
+use grovedb::TransactionArg;
+use platform_version::version::PlatformVersion;
+use std::collections::{BTreeMap, BTreeSet};
+
+impl Drive {
+ /// 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)],
+ batch_operations: &mut Vec,
+ transaction: TransactionArg,
+ platform_version: &PlatformVersion,
+ ) -> Result<(), Error> {
+ 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);
+ }
+
+ 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
new file mode 100644
index 00000000000..458ec6c5a8f
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/mod.rs
@@ -0,0 +1,50 @@
+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;
+
+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_poll_ids: &[Identifier],
+ batch_operations: &mut Vec,
+ platform_version: &PlatformVersion,
+ ) -> Result<(), Error> {
+ match platform_version
+ .drive
+ .methods
+ .vote
+ .yes_no
+ .remove_yes_no_vote_poll_votes_operations
+ {
+ Some(0) => self.remove_yes_no_vote_poll_votes_operations_v0(
+ vote_poll_ids,
+ batch_operations,
+ platform_version,
+ ),
+ 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_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..d636fa375fb
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_poll_votes_operations/v0/mod.rs
@@ -0,0 +1,33 @@
+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 grovedb::TreeType;
+
+impl Drive {
+ pub(super) fn remove_yes_no_vote_poll_votes_operations_v0(
+ &self,
+ vote_poll_ids: &[Identifier],
+ batch_operations: &mut Vec,
+ _platform_version: &PlatformVersion,
+ ) -> Result<(), Error> {
+ 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.
+ batch_operations.push(LowLevelDriveOperation::for_known_path_key_drop_flat_tree(
+ poll_path.clone(),
+ vec![vote_choice.to_tree_key()],
+ TreeType::SumTree,
+ ));
+ }
+ }
+ 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..89d2f4419e8
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/mod.rs
@@ -0,0 +1,57 @@
+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.
+ ///
+ /// # 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,
+ 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
+ {
+ 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,
+ ),
+ 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/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..a30bea1d24b
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/cleanup/remove_yes_no_vote_references_given_by_identity/v0/mod.rs
@@ -0,0 +1,28 @@
+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 dpp::identifier::Identifier;
+use dpp::version::PlatformVersion;
+use grovedb::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());
+ 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
new file mode 100644
index 00000000000..48cd957db44
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/mod.rs
@@ -0,0 +1,51 @@
+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_polls::yes_no_vote_poll::YesNoVotePoll;
+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,
+ transaction: TransactionArg,
+ platform_version: &PlatformVersion,
+ ) -> Result, Error> {
+ match platform_version
+ .drive
+ .methods
+ .vote
+ .yes_no
+ .fetch_identities_voting_in_yes_no_vote_poll
+ {
+ Some(0) => self.fetch_identities_voting_in_yes_no_vote_poll_v0(
+ vote_poll,
+ transaction,
+ platform_version,
+ ),
+ 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_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..441f5876ad4
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identities_voting_in_yes_no_vote_poll/v0/mod.rs
@@ -0,0 +1,55 @@
+use crate::drive::votes::paths::YesNoVotePollPaths;
+use crate::drive::votes::YesNoAbstainVoteChoiceToKeyTrait;
+use crate::drive::Drive;
+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::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(platform_version)?;
+ 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,
+ },
+ };
+ // 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,
+ QueryResultType::QueryKeyElementPairResultType,
+ &mut vec![],
+ &platform_version.drive,
+ )?
+ .0
+ .to_keys()
+ .into_iter()
+ .map(|voter| voter.try_into())
+ .collect::, _>>()?;
+ 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
new file mode 100644
index 00000000000..7cc056f767d
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/mod.rs
@@ -0,0 +1,59 @@
+mod v0;
+
+use crate::drive::votes::PreviousVoteCount;
+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.
+ ///
+ /// # 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,
+ 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
+ {
+ Some(0) => self.fetch_identity_yes_no_vote_v0(
+ masternode_pro_tx_hash,
+ vote_poll_id,
+ transaction,
+ drive_operations,
+ platform_version,
+ ),
+ 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_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..9ac6fbea7ff
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/fetch/fetch_identity_yes_no_vote/v0/mod.rs
@@ -0,0 +1,38 @@
+use crate::drive::votes::paths::vote_decisions_identity_votes_tree_path_for_identity;
+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::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());
+ self.grove_get_raw_optional_item(
+ (&path).into(),
+ vote_poll_id.as_slice(),
+ DirectQueryType::StatefulDirectQuery,
+ transaction,
+ drive_operations,
+ &platform_version.drive,
+ )?
+ .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
new file mode 100644
index 00000000000..7a3bc753f24
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/mod.rs
@@ -0,0 +1,48 @@
+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.
+ ///
+ /// # 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,
+ transaction: TransactionArg,
+ platform_version: &PlatformVersion,
+ ) -> Result , Error> {
+ match platform_version
+ .drive
+ .methods
+ .vote
+ .yes_no
+ .fetch_yes_no_vote_poll_stored_info
+ {
+ Some(0) => {
+ self.fetch_yes_no_vote_poll_stored_info_v0(vote_poll, transaction, platform_version)
+ }
+ 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/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..abe44bce794
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/fetch/fetch_yes_no_vote_poll_stored_info/v0/mod.rs
@@ -0,0 +1,31 @@
+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(platform_version)?;
+ 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,
+ )?
+ .map(|bytes| YesNoVotePollStoredInfo::deserialize_from_bytes_trusted(&bytes))
+ .transpose()
+ .map_err(Error::from)
+ }
+}
diff --git a/packages/rs-drive/src/drive/votes/fetch/mod.rs b/packages/rs-drive/src/drive/votes/fetch/mod.rs
index d4393ec6b95..8288d67c834 100644
--- a/packages/rs-drive/src/drive/votes/fetch/mod.rs
+++ b/packages/rs-drive/src/drive/votes/fetch/mod.rs
@@ -1,4 +1,7 @@
mod fetch_contested_document_vote_poll_contender_count;
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..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
@@ -1,4 +1,5 @@
mod v0;
+mod v1;
use crate::drive::Drive;
@@ -50,11 +51,14 @@ 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
.vote
- .contested_resource_insert
+ .insert
.register_identity_vote
{
0 => self.register_identity_vote_v0(
@@ -66,9 +70,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,
})),
}
@@ -108,11 +121,12 @@ 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
.vote
- .contested_resource_insert
+ .insert
.register_identity_vote
{
0 => self.register_identity_vote_operations_v0(
@@ -123,9 +137,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..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
@@ -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;
@@ -41,6 +42,12 @@ impl Drive {
),
}
}
+ // 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",
+ ))),
}
}
@@ -70,6 +77,12 @@ impl Drive {
),
}
}
+ // 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
new file mode 100644
index 00000000000..468706df0bc
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/insert/register_identity_vote/v1/mod.rs
@@ -0,0 +1,74 @@
+use crate::drive::votes::resolved::votes::ResolvedVote;
+use crate::drive::Drive;
+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 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,
+ 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::YesNoVote(yes_no_vote) => self.register_yes_no_identity_vote(
+ voter_pro_tx_hash,
+ strength,
+ yes_no_vote,
+ block_info,
+ 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,
+ ),
+ }
+ }
+
+ 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::YesNoVote(yes_no_vote) => self.register_yes_no_identity_vote_operations(
+ voter_pro_tx_hash,
+ strength,
+ yes_no_vote,
+ 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/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/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
new file mode 100644
index 00000000000..9992ca5a1ac
--- /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(platform_version)?;
+ 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/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..d9c4fb68ff5
--- /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,93 @@
+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.
+ ///
+ /// # 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,
+ 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
+ {
+ Some(0) => self.insert_stored_info_for_yes_no_vote_poll_v0(
+ vote_poll,
+ stored_info,
+ transaction,
+ platform_version,
+ ),
+ 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],
+ })),
+ }
+ }
+
+ /// 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,
+ stored_info: YesNoVotePollStoredInfo,
+ platform_version: &PlatformVersion,
+ ) -> Result, Error> {
+ match platform_version
+ .drive
+ .methods
+ .vote
+ .yes_no
+ .insert_stored_info_for_yes_no_vote_poll
+ {
+ Some(0) => self.insert_stored_info_for_yes_no_vote_poll_operations_v0(
+ vote_poll,
+ stored_info,
+ platform_version,
+ ),
+ 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/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..cf57cfc19f6
--- /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(platform_version)?,
+ 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..a23cb9b8445
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/mod.rs
@@ -0,0 +1,692 @@
+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, an end date that is not after the opening block
+ /// 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.
+ /// - `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,
+ 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
+ {
+ Some(0) => self.open_yes_no_vote_poll_v0(
+ vote_poll,
+ end_date,
+ block_info,
+ transaction,
+ platform_version,
+ ),
+ 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, 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
+ /// 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,
+ 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
+ {
+ Some(0) => self.open_yes_no_vote_poll_operations_v0(
+ vote_poll,
+ end_date,
+ block_info,
+ previous_batch_operations,
+ batch_operations,
+ transaction,
+ platform_version,
+ ),
+ 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],
+ })),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::drive::votes::paths::{
+ 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::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;
+ 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 dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0;
+ use dpp::voting::votes::yes_no_vote::YesNoVote;
+ use grovedb::batch::KeyInfoPath;
+ use grovedb::MaybeTree;
+
+ #[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), with the
+ // batch so far as the previous operations.
+ drive
+ .open_yes_no_vote_poll_operations(
+ &vote_poll,
+ 2_000,
+ &BlockInfo::default(),
+ &mut Some(&mut batch_operations),
+ &mut vec![],
+ 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());
+ }
+
+ #[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_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();
+ // 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!(
+ 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",
+ );
+ 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",
+ );
+ 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]
+ 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()
+ };
+ 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 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]
+ 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()] {
+ let mut opening_operations = vec![];
+ drive
+ .open_yes_no_vote_poll_operations(
+ &vote_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.len(), 1);
+ assert_eq!(
+ end_dates[&1_000],
+ [
+ poll().unique_id(platform_version).expect("id"),
+ other_poll.unique_id(platform_version).expect("id")
+ ]
+ .into_iter()
+ .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(platform_version).expect("id"),
+ poll().unique_id(platform_version).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());
+ }
+
+ #[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(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),
+ },
+ 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(platform_version).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(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
new file mode 100644
index 00000000000..6f5bc526656
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/insert/yes_no/open_yes_no_vote_poll/v0/mod.rs
@@ -0,0 +1,170 @@
+use crate::drive::votes::paths::{
+ 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::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)?;
+ // A poll that ends in the block that opens it, or earlier, would close at the end of
+ // 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.
+ 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 {}), at \
+ most {} ms later and at most at {}, not at {}",
+ vote_poll,
+ block_info.time_ms,
+ max_duration,
+ i64::MAX,
+ end_date
+ ),
+ ))));
+ }
+ 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
+ // earlier opening in the batch, among the previous operations, may already queue them.
+ for subtree_key in VOTE_DECISIONS_SUBTREE_KEYS {
+ 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, 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!(
+ "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(platform_version)?;
+ 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: 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,
+ transaction,
+ platform_version,
+ )?;
+ Ok(())
+ }
+}
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..d0b4fb5fa19
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/mod.rs
@@ -0,0 +1,108 @@
+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.
+ ///
+ /// # 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],
+ 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
+ {
+ Some(0) => self.register_yes_no_identity_vote_v0(
+ voter_pro_tx_hash,
+ strength,
+ vote,
+ block_info,
+ transaction,
+ platform_version,
+ ),
+ 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],
+ })),
+ }
+ }
+
+ /// 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],
+ strength: u8,
+ vote: ResolvedYesNoVote,
+ transaction: TransactionArg,
+ platform_version: &PlatformVersion,
+ ) -> Result, Error> {
+ match platform_version
+ .drive
+ .methods
+ .vote
+ .yes_no
+ .register_yes_no_identity_vote
+ {
+ Some(0) => self.register_yes_no_identity_vote_operations_v0(
+ voter_pro_tx_hash,
+ strength,
+ vote,
+ transaction,
+ platform_version,
+ ),
+ 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-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..6dfcac0432b
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/insert/yes_no/register_yes_no_identity_vote/v0/mod.rs
@@ -0,0 +1,136 @@
+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, platform_version)?;
+ 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, platform_version)?;
+ 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(platform_version)?.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..6d0991a576a 100644
--- a/packages/rs-drive/src/drive/votes/mod.rs
+++ b/packages/rs-drive/src/drive/votes/mod.rs
@@ -1,17 +1,23 @@
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;
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")))]
@@ -50,6 +56,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 +105,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 +128,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 dd12fb445a7..c8b8ce046e1 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 15). 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.
@@ -40,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,
@@ -64,6 +92,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
@@ -310,6 +350,49 @@ impl VotePollPaths for ContestedDocumentResourceVotePollWithContractInfoAllowBor
}
}
+/// 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, 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, platform_version: &PlatformVersion) -> Result>, Error> {
+ Ok(vote_decisions_active_polls_poll_tree_path_vec(
+ 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(platform_version)?.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)]
@@ -552,6 +635,74 @@ 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, 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/resolve.rs b/packages/rs-drive/src/drive/votes/resolved/vote_polls/resolve.rs
index eecb852f1b1..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,6 +70,9 @@ impl VotePollResolver for VotePoll {
),
)
}
+ VotePoll::YesNoVotePoll(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution(
+ YES_NO_VOTE_POLL_NOT_RESOLVED,
+ ))),
}
}
@@ -84,6 +94,9 @@ impl VotePollResolver for VotePoll {
),
)
}
+ 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 99a31f50a69..a5f11c51064 100644
--- a/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs
+++ b/packages/rs-drive/src/drive/votes/resolved/votes/mod.rs
@@ -2,27 +2,39 @@
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;
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.
+#[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 {
/// 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.
@@ -32,11 +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(platform_version)?,
+ )),
}
}
}
@@ -61,6 +81,7 @@ impl From for Vote {
}
}
}
+ ResolvedVote::YesNoVote(yes_no_vote) => 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..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,6 +1,7 @@
use crate::drive::votes::resolved::votes::resolved_resource_vote::resolve::ResourceVoteResolver;
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;
@@ -42,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,
@@ -53,6 +60,9 @@ impl VoteResolver for Vote {
Vote::ResourceVote(resource_vote) => Ok(ResolvedVote::ResolvedResourceVote(
resource_vote.resolve(drive, transaction, platform_version)?,
)),
+ Vote::YesNoVote(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution(
+ YES_NO_VOTE_NOT_RESOLVED,
+ ))),
}
}
@@ -66,6 +76,9 @@ impl VoteResolver for Vote {
Vote::ResourceVote(resource_vote) => Ok(ResolvedVote::ResolvedResourceVote(
resource_vote.resolve_owned(drive, transaction, platform_version)?,
)),
+ 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
new file mode 100644
index 00000000000..48024bd78ae
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/resolved/votes/resolved_yes_no_vote/mod.rs
@@ -0,0 +1,28 @@
+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::v0::YesNoVoteV0;
+use dpp::voting::votes::yes_no_vote::YesNoVote;
+
+/// 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, PreviousVoteCount)>,
+}
+
+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..fee760af627
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/setup/insert_vote_decisions_trees/mod.rs
@@ -0,0 +1,75 @@
+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;
+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 15 calls this on its first block and a
+ /// chain born at 15 or later 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,
+ platform_version: &PlatformVersion,
+ ) -> Result<(), Error> {
+ 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],
+ 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 element = drive
+ .grove
+ .get(
+ &path[..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/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..c3f87bb8a81
--- /dev/null
+++ b/packages/rs-drive/src/drive/votes/storage_form/yes_no_vote_reference_storage_form/mod.rs
@@ -0,0 +1,73 @@
+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;
+
+/// 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 the form stored in the identity votes index, a stored form that does not
+ /// decode, or carries bytes past the form, being corrupted state, as the proof decoder
+ /// treats it.
+ pub fn deserialize(bytes: &[u8]) -> Result {
+ 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)
+ }
+}
+
+#[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
+ );
+ }
+
+ #[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/rs-drive/src/drive/votes/structure.rs b/packages/rs-drive/src/drive/votes/structure.rs
index 9163214b1bb..3d517c22794 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,8 @@ 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 OWNED: [FlagsKind; 2] = [FlagsKind::EpochOwned, FlagsKind::None];
const CONTESTED_DOCUMENT: &str =
"votes.contested_resource.active_polls.contract.document_type.storage.document";
@@ -61,8 +64,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(
@@ -89,9 +92,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],
@@ -115,7 +120,7 @@ pub(crate) fn structure() -> StructureNode {
with the sign bit flipped",
)
.kind(ElementKind::Tree)
- .flags(&[FlagsKind::EpochOwned], POLL_FLAGS)
+ .flags(&OWNED, POLL_FLAGS)
.describe("The polls ending at this time.")
.child(
StructureNode::dynamic(
@@ -123,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(&[FlagsKind::EpochOwned], POLL_FLAGS)
+ .flags(&OWNED, POLL_FLAGS)
.value("serialized VotePoll")
.describe("One poll ending at this time."),
),
@@ -328,3 +335,135 @@ 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(15)
+ .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 domain tag `dash platform yes/no vote \
+ poll id` followed by 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(15)
+ .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 when its polls \
+ close, and is dropped when the masternode leaves the list.",
+ )
+ .child(
+ StructureNode::dynamic(
+ "vote",
+ "vote_poll_id",
+ KeyMatcher::Len(32),
+ KeyEncoding::Hash32,
+ "The double sha256 of the domain tag `dash platform yes/no vote \
+ poll id` followed by 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/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/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 08e77057ecc..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
@@ -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;
@@ -513,20 +515,25 @@ 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
+ // 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 15.
+ 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(platform_version)?;
+ 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 7352a30a02a..fcfd8c7f9c8 100644
--- a/packages/rs-drive/src/query/mod.rs
+++ b/packages/rs-drive/src/query/mod.rs
@@ -215,6 +215,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..6ccf484f345 100644
--- a/packages/rs-drive/src/query/vote_query.rs
+++ b/packages/rs-drive/src/query/vote_query.rs
@@ -1,10 +1,14 @@
-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};
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)]
@@ -16,14 +20,25 @@ 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(),
- );
+ /// 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(_) => {
+ 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()?;
+ 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
new file mode 100644
index 00000000000..503a396641d
--- /dev/null
+++ b/packages/rs-drive/src/query/yes_no_vote_poll_state_query.rs
@@ -0,0 +1,274 @@
+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;
+#[cfg(feature = "server")]
+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;
+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};
+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,
+}
+
+/// 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, 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 {
+ 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)
+ }
+
+ /// 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(
+ &self,
+ drive: &Drive,
+ transaction: TransactionArg,
+ drive_operations: &mut Vec,
+ platform_version: &PlatformVersion,
+ ) -> Result, Error> {
+ Self::ensure_active(platform_version)?;
+ let path_query = self.construct_path_query(platform_version)?;
+ 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 {
+ 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(platform_version)?;
+ let key_elements = match drive.grove_get_raw_path_query(
+ &path_query,
+ transaction,
+ QueryResultType::QueryKeyElementPairResultType,
+ drive_operations,
+ &platform_version.drive,
+ ) {
+ 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)
+ }
+}
+
+#[cfg(all(test, feature = "server", feature = "verify"))]
+mod tests {
+ use super::*;
+ use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure;
+ 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;
+
+ fn poll() -> YesNoVotePoll {
+ test_yes_no_vote_poll()
+ }
+
+ #[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();
+ 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)
+ .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..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 {
@@ -183,6 +185,7 @@ mod tests {
assert_eq!(v0.resource_vote_choice, ResourceVoteChoice::Abstain);
}
},
+ ResolvedVote::YesNoVote(_) => panic!("expected a resource vote"),
}
}
other => panic!("expected MasternodeCastVote, got {:?}", other),
@@ -197,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");
@@ -291,6 +294,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/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 46cc279415e..978dd03afc1 100644
--- a/packages/rs-drive/src/structure/tests.rs
+++ b/packages/rs-drive/src/structure/tests.rs
@@ -299,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;
@@ -362,6 +363,7 @@ 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 rand::rngs::StdRng;
use rand::SeedableRng;
@@ -1399,6 +1401,24 @@ 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));
+ 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);
+ }
+
fn apply_operations(drive: &Drive, operations: Vec) {
drive
.apply_batch_low_level_drive_operations(
@@ -1859,6 +1879,7 @@ mod fixtures {
core_anchored_withdrawal_accounting(&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/util/test_helpers/mod.rs b/packages/rs-drive/src/util/test_helpers/mod.rs
index 201d7d76d45..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)]
@@ -242,6 +250,54 @@ 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() -> YesNoVotePoll {
+ YesNoVotePoll {
+ resource_path: vec![BinaryData::new(vec![0xc1; 32])],
+ supermajority_numerator: 2,
+ supermajority_denominator: 3,
+ minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 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: &Drive,
+ vote_poll: &YesNoVotePoll,
+ votes: &[([u8; 32], u8, YesNoAbstainVoteChoice)],
+ platform_version: &PlatformVersion,
+) {
+ 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 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,
+ },
+ &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/bounded_decode.rs b/packages/rs-drive/src/verify/bounded_decode.rs
index d86f1f99c5c..e3b63f488ea 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;
@@ -89,6 +90,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/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 1ee41e54f6f..3937f173b5f 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
@@ -1779,31 +1779,50 @@ 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,
+ // 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 15.
+ 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)))
}
@@ -5358,6 +5377,101 @@ 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 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 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::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 = test_yes_no_vote_poll();
+ let voter = [1u8; 32];
+ 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(
+ 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_vote/mod.rs b/packages/rs-drive/src/verify/voting/verify_masternode_vote/mod.rs
index 4c5669a67ec..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
@@ -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,34 @@ 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.
+ //
+ // 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), 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(
+ 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_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
new file mode 100644
index 00000000000..50955257c62
--- /dev/null
+++ b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/mod.rs
@@ -0,0 +1,225 @@
+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.
+ ///
+ /// 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],
+ 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
+ {
+ Some(0) => Drive::verify_masternode_yes_no_vote_v0(
+ proof,
+ masternode_pro_tx_hash,
+ vote,
+ verify_subset_of_proof,
+ platform_version,
+ ),
+ 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],
+ })),
+ }
+ }
+}
+
+#[cfg(all(test, feature = "server"))]
+mod tests {
+ use super::*;
+ use crate::error::proof::ProofError;
+ use crate::query::IdentityBasedVoteDriveQuery;
+ use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure;
+ 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::YesNoVotePoll;
+ use dpp::voting::vote_polls::VotePoll;
+ use dpp::voting::votes::yes_no_vote::v0::YesNoVoteV0;
+
+ fn poll() -> YesNoVotePoll {
+ test_yes_no_vote_poll()
+ }
+
+ fn vote(vote_choice: YesNoAbstainVoteChoice) -> YesNoVote {
+ YesNoVote::V0(YesNoVoteV0 {
+ vote_poll: poll(),
+ vote_choice,
+ })
+ }
+
+ 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),
+ vote_poll: VotePoll::YesNoVotePoll(poll()),
+ }
+ .construct_path_query(platform_version)
+ .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();
+ let voter = [7u8; 32];
+ 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(
+ &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);
+ }
+
+ /// 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");
+ let voter = [7u8; 32];
+ 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.
+ 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(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),
+ );
+ 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
new file mode 100644
index 00000000000..a480d735a3d
--- /dev/null
+++ b/packages/rs-drive/src/verify/voting/verify_masternode_yes_no_vote/v0/mod.rs
@@ -0,0 +1,69 @@
+use crate::drive::Drive;
+use crate::error::proof::ProofError;
+use crate::error::Error;
+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;
+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> {
+ // 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(platform_version)?;
+ 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 = decode_yes_no_vote_reference(&bytes)?;
+ 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..d1167d84cb7
--- /dev/null
+++ b/packages/rs-drive/src/verify/voting/verify_yes_no_vote_poll_state_proof/mod.rs
@@ -0,0 +1,44 @@
+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.
+ ///
+ /// # 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],
+ platform_version: &PlatformVersion,
+ ) -> Result<(RootHash, YesNoVotePollState), Error> {
+ match platform_version
+ .drive
+ .methods
+ .verify
+ .voting
+ .verify_yes_no_vote_poll_state_proof
+ {
+ 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-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..be313dd81a1
--- /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(platform_version)?;
+ 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..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
@@ -1,11 +1,25 @@
-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. `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,
+ /// How a yes/no poll is decided: its required voting power and whether it passes
+ /// (`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 944a295d1fe..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
@@ -4,4 +4,8 @@ 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: 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 de072e87837..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
@@ -4,4 +4,8 @@ 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: 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
new file mode 100644
index 00000000000..9eaaf9c6353
--- /dev/null
+++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_voting_versions/v3.rs
@@ -0,0 +1,13 @@
+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, 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/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 0b4a236b28c..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,6 +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: 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 410eb8ba8c8..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,6 +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: 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 d2e1249af35..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,6 +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: 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 741535ee010..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,6 +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: 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 1c559a0b1c5..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,6 +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: 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 fcd85d382b8..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,6 +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: 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 34b722d7e0f..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,6 +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: 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 9bf22b9afa7..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,6 +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: 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 2105c268ec5..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,6 +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: 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 cbe35a4ea23..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,6 +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: 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 d4f38b73665..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,6 +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: 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_validation_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs
index 402cb215efb..6557dfc7039 100644
--- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs
+++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs
@@ -18,6 +18,8 @@ use super::{
/// - the batch `advanced_structure` 2: v1, then the structure checks of token actions protocol
/// v15 introduces, which read only the contract the action carries and so run in check_tx too.
/// A `TokenConfigUpdate` can make a token tradeable only if it is transferable.
+/// - the masternode vote `transform_into_action`, 1 rather than 0: a vote on a yes/no poll looks up
+/// the masternode's previous answer on that poll.
/// - the contract update `transform_into_action` 1: v0, then in check_tx and recheck, which run no
/// state validation, the updated contract is held to the compiled size cap with its time fields
/// at their widest, a bound on what a block measures once it restores the stored contract's
@@ -34,6 +36,12 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V11: DriveAbciValidationVersions =
.state_transitions
.identity_update_state_transition
},
+ masternode_vote_state_transition: DriveAbciStateTransitionValidationVersion {
+ transform_into_action: 1, // a vote on a yes/no poll looks up the masternode's previous answer
+ ..DRIVE_ABCI_VALIDATION_VERSIONS_V10
+ .state_transitions
+ .masternode_vote_state_transition
+ },
contract_update_state_transition: DriveAbciStateTransitionValidationVersion {
transform_into_action: 1, // check_tx holds the update to the compiled size cap
..DRIVE_ABCI_VALIDATION_VERSIONS_V10
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 3ba13c9f529..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,6 +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. `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 d00538192b0..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,6 +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: 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 3b268472ea2..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
@@ -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 DriveVoteMethodVersions {
@@ -12,6 +13,26 @@ 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 15).
+ pub yes_no: DriveVoteYesNoMethodVersions,
+}
+
+/// 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 {
+ pub open_yes_no_vote_poll: OptionalFeatureVersion,
+ 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,
+ /// 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,
}
#[derive(Clone, Debug, Default)]
@@ -43,6 +64,12 @@ 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 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. `None` (not active) before
+ /// protocol version 15.
+ pub remove_vote_poll_end_date_query_operations: OptionalFeatureVersion,
}
#[derive(Clone, Debug, Default)]
@@ -54,6 +81,7 @@ 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,
+ /// 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/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v1.rs
index 17c779d2b9d..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
@@ -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 {
@@ -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 {
@@ -23,6 +22,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: None,
},
setup: DriveVoteSetupMethodVersions {
add_initial_vote_tree_main_structure_operations: 0,
@@ -36,4 +37,15 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V1: DriveVoteMethodVersions = DriveVoteMeth
fetch_identity_contested_resource_vote: 0,
fetch_contested_document_vote_poll_contender_count: 0,
},
+ yes_no: DriveVoteYesNoMethodVersions {
+ open_yes_no_vote_poll: None,
+ 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,
+ 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 4a7c24f091a..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
@@ -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 {
@@ -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 {
@@ -23,6 +22,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: None,
},
setup: DriveVoteSetupMethodVersions {
add_initial_vote_tree_main_structure_operations: 0,
@@ -36,4 +37,15 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V2: DriveVoteMethodVersions = DriveVoteMeth
fetch_identity_contested_resource_vote: 0,
fetch_contested_document_vote_poll_contender_count: 0,
},
+ yes_no: DriveVoteYesNoMethodVersions {
+ open_yes_no_vote_poll: None,
+ 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,
+ 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 f25e7a74859..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
@@ -1,7 +1,7 @@
use crate::version::drive_versions::drive_vote_method_versions::{
DriveVoteCleanupMethodVersions, DriveVoteContestedResourceInsertMethodVersions,
DriveVoteFetchMethodVersions, DriveVoteInsertMethodVersions, DriveVoteMethodVersions,
- DriveVoteSetupMethodVersions, DriveVoteStorageFormMethodVersions,
+ DriveVoteSetupMethodVersions, DriveVoteStorageFormMethodVersions, DriveVoteYesNoMethodVersions,
};
/// Drive vote methods for protocol v14+.
@@ -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 {
@@ -31,6 +30,8 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V3: 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: None,
},
setup: DriveVoteSetupMethodVersions {
add_initial_vote_tree_main_structure_operations: 0,
@@ -44,4 +45,15 @@ pub const DRIVE_VOTE_METHOD_VERSIONS_V3: DriveVoteMethodVersions = DriveVoteMeth
fetch_identity_contested_resource_vote: 0,
fetch_contested_document_vote_poll_contender_count: 0,
},
+ yes_no: DriveVoteYesNoMethodVersions {
+ open_yes_no_vote_poll: None,
+ 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,
+ 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
new file mode 100644
index 00000000000..09d68cc9f70
--- /dev/null
+++ b/packages/rs-platform-version/src/version/drive_versions/drive_vote_method_versions/v4.rs
@@ -0,0 +1,48 @@
+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,
+ DriveVoteInsertMethodVersions, DriveVoteMethodVersions, DriveVoteYesNoMethodVersions,
+};
+
+/// Drive vote method versions 4. Introduced in protocol version 15 with the yes/no poll
+/// kind:
+///
+/// * `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.
+/// * `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
+/// 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,
+ remove_vote_poll_end_date_query_operations: Some(0),
+ ..DRIVE_VOTE_METHOD_VERSIONS_V3.cleanup
+ },
+ yes_no: DriveVoteYesNoMethodVersions {
+ open_yes_no_vote_poll: Some(0),
+ 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),
+ 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),
+ },
+ ..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 31b7dd100a0..ff1ce116d60 100644
--- a/packages/rs-platform-version/src/version/drive_versions/v10.rs
+++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs
@@ -1,15 +1,33 @@
use crate::version::drive_versions::drive_contract_method_versions::v5::DRIVE_CONTRACT_METHOD_VERSIONS_V5;
+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};
-/// Drive version 10, introduced in protocol v15. Differs from v9 only in
-/// `DRIVE_CONTRACT_METHOD_VERSIONS_V5`: applying a contract writes nothing when its format
-/// version 1 bytes match the stored contract's, whatever format the stored one is in, as
-/// contracts are stored compiled from this version (`contract_storage_serialization_version`
-/// 2).
+/// Drive version 10.
+/// Introduced in protocol v15:
+///
+/// * **Compiled contract storage**: `DRIVE_CONTRACT_METHOD_VERSIONS_V5`: applying a contract
+/// writes nothing when its format version 1 bytes match the stored contract's, whatever
+/// format the stored one is in, as contracts are stored compiled from this version
+/// (`contract_storage_serialization_version` 2).
+/// * **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 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.
+/// `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 {
- contract: DRIVE_CONTRACT_METHOD_VERSIONS_V5, // changed
+ contract: DRIVE_CONTRACT_METHOD_VERSIONS_V5, // changed in v10: compiled contract storage
+ 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/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/mocks/v2_test.rs b/packages/rs-platform-version/src/version/mocks/v2_test.rs
index fb30e0ecc34..469ff4894bb 100644
--- a/packages/rs-platform-version/src/version/mocks/v2_test.rs
+++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs
@@ -617,6 +617,13 @@ 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_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/mocks/v3_test.rs b/packages/rs-platform-version/src/version/mocks/v3_test.rs
index f56f03c806c..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,6 +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: 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..3049223ba2b 100644
--- a/packages/rs-platform-version/src/version/protocol_version.rs
+++ b/packages/rs-platform-version/src/version/protocol_version.rs
@@ -325,3 +325,126 @@ 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
+ // 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
+ >= 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;
+ 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_id", voting.yes_no_vote_poll_id),
+ (
+ "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,
+ ),
+ (
+ "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,
+ ),
+ (
+ "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
+ );
+ }
+ }
+ }
+
+ // 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(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);
+ 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 af6da317d5a..b20fd65d2bb 100644
--- a/packages/rs-platform-version/src/version/system_limits/mod.rs
+++ b/packages/rs-platform-version/src/version/system_limits/mod.rs
@@ -254,6 +254,30 @@ 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 15).
+ 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 15). 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 15).
+ 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 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,
+ /// 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 dc3f9679f2a..38ef587a2e6 100644
--- a/packages/rs-platform-version/src/version/system_limits/v1.rs
+++ b/packages/rs-platform-version/src/version/system_limits/v1.rs
@@ -63,6 +63,13 @@ 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_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 332d6052f85..8c9198f9a44 100644
--- a/packages/rs-platform-version/src/version/system_limits/v2.rs
+++ b/packages/rs-platform-version/src/version/system_limits/v2.rs
@@ -44,6 +44,13 @@ 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_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 2774b855cda..950f730fe29 100644
--- a/packages/rs-platform-version/src/version/system_limits/v3.rs
+++ b/packages/rs-platform-version/src/version/system_limits/v3.rs
@@ -46,6 +46,13 @@ 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_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 c629e4c3249..ffce24773b5 100644
--- a/packages/rs-platform-version/src/version/system_limits/v4.rs
+++ b/packages/rs-platform-version/src/version/system_limits/v4.rs
@@ -167,6 +167,13 @@ 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_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/v15.rs b/packages/rs-platform-version/src/version/v15.rs
index 67f82921101..855e4a41567 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;
@@ -198,13 +198,38 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15;
/// contract, performed or a group member's vote, with `DataContractIsReadonlyError` (40001),
/// charged, so check_tx refuses it too. Protocol v14 and earlier keep the batch
/// `advanced_structure` 1.
+///
+/// 10. **Yes/no vote polls**: a second kind of masternode vote poll, which asks
+/// "should this happen?" and passes on a supermajority of yes over yes plus
+/// no with a minimum of voting power cast. A masternode vote naming a yes/no
+/// poll is active from this version (`StateTransition::active_version_range`);
+/// the polls live under the votes tree's decisions branch, which genesis
+/// (`create_initial_state_structure` 4 behind
+/// `>= YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION`, 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. Opening a yes/no poll
+/// (`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
+/// `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'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
+/// (`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: apply_contract_with_serialization 1 writes nothing for a contract whose format version 1 bytes match the stored one's (note 8)
+ drive: DRIVE_VERSION_V10, // changed: apply_contract_with_serialization 1 writes nothing for a contract whose format version 1 bytes match the stored one's (note 8); 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 10)
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
- 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); contract update transform_into_action 1 holds an update to the compiled size cap in check_tx (note 8); batch advanced_structure 2 refuses a token config update on a read-only contract (note 9)
+ methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: the yes/no poll's tally, record, clean-up and finish hook are Some(0) (note 10); 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); contract update transform_into_action 1 holds an update to the compiled size cap in check_tx (note 8); batch advanced_structure 2 refuses a token config update on a read-only contract (note 9); masternode vote transform_into_action 1 looks up a masternode's previous answer on a yes/no poll (note 10)
withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3,
query: DRIVE_ABCI_QUERY_VERSIONS_V2,
checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1,
@@ -219,7 +244,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_V2,
+ voting_versions: VOTING_VERSION_V3, // changed: the yes/no poll's stored info, id, parameter rules and decision rules are active (note 10)
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/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 3805c10c7ff..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,70 +479,163 @@ impl Sdk {
limit: Option,
) -> Result, Error> {
let dpns_contract_id = self.get_dpns_contract_id()?;
- let query_limit = limit.unwrap_or(100);
+ // 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));
+ // 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 {
+ // 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();
+ let single_end_date = result.0.len() == 1;
+ record_dpns_contests(&mut name_to_end_time, dpns_contract_id, result);
+
+ 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 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;
+ }
- // Execute the query
- let result: VotePollsGroupedByTimestamp = VotePoll::fetch_many(self, query).await?;
-
- // 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,
}
+ }
- let mut last_timestamp = None;
- let mut polls_in_last_group = 0;
-
- // Process each timestamp group
- for (timestamp, polls) in result.0 {
- let mut dpns_polls_count = 0;
-
- for VotePoll::ContestedDocumentResourceVotePoll(contested_poll) in polls {
- 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);
- dpns_polls_count += 1;
- }
- }
- }
- }
+ Ok(name_to_end_time)
+ }
+}
- if dpns_polls_count > 0 {
- last_timestamp = Some(timestamp);
- polls_in_last_group = dpns_polls_count;
+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
+ }
+}
+
+/// 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);
}
}
+ }
+ }
+}
- // 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 {
- 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 {DEFAULT_QUERY_LIMIT} vote polls end at {end_date}, more than \
+ one page of vote polls by end date can list"
+ ))
+}
- // 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 {
- break;
- }
- }
+/// 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 vote_polls_by_end_date_paging_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
+ );
}
}
@@ -671,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(
diff --git a/packages/rs-sdk/src/platform/transition/vote.rs b/packages/rs-sdk/src/platform/transition/vote.rs
index 709310913c0..8046e38e9f9 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(sdk.version())?;
let masternode_vote_transition = MasternodeVoteTransition::try_from_vote_with_signer(
self.clone(),
@@ -141,14 +139,22 @@ impl> PutVote for Vote {
Ok(_) => {}
//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(),
- ))
- } else {
- Err(e.into())
+ 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(),
+ ));
+ }
+ // `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(_) => {}
}
}
}
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,
diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs
index 7167f3294ad..ebeda1b906c 100644
--- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs
+++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs
@@ -192,6 +192,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,
@@ -808,6 +809,9 @@ pub fn from_state_error(state_error: &StateError) -> JsValue {
StateError::ReferencedDocumentPrecededError(e) => {
generic_consensus_error!(ReferencedDocumentPrecededError, 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 78b481c1ee5..5b0e72847d9 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
@@ -301,8 +301,10 @@ impl MasternodeVoteTransitionWasm {
Some(js_object)
}
+ VotePoll::YesNoVotePoll(_) => None,
}
}
+ Vote::YesNoVote(_) => None,
}
}
diff --git a/packages/wasm-dpp2/src/voting/resource_vote.rs b/packages/wasm-dpp2/src/voting/resource_vote.rs
index c7a474fd97d..2054f8a5dc5 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,26 +79,25 @@ 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)]
- 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 5b409af1444..229ab1c3853 100644
--- a/packages/wasm-dpp2/src/voting/vote.rs
+++ b/packages/wasm-dpp2/src/voting/vote.rs
@@ -1,39 +1,63 @@
+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_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)]
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.
*
* 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: YesNoAbstainVoteChoiceName;
+ };
/**
* 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: YesNoAbstainVoteChoiceName;
+ };
"#;
#[wasm_bindgen]
@@ -69,48 +93,90 @@ 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)]
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(),
+ )),
+ // 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())
+ }),
}
}
#[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::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(
+ "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::ResourceVote(vote) => Vote::ResourceVote(ResourceVote::V0(
+ ResourceVoteV0::try_new(vote.vote_poll().clone(), 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 d1be2052a43..a31fdf70960 100644
--- a/packages/wasm-dpp2/src/voting/vote_poll.rs
+++ b/packages/wasm-dpp2/src/voting/vote_poll.rs
@@ -29,31 +29,70 @@ 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
+ * share of the masternode list's total voting power when the poll closes.
+ */
+export interface YesNoVotePollFieldsObject {
+ resourcePath: Uint8Array[];
+ supermajorityNumerator: number;
+ supermajorityDenominator: number;
+ minimumVotingPower: YesNoMinimumVotingPowerObject;
+}
+
+/**
+ * A yes/no poll's fields as JSON (resource path segments in base64).
+ */
+export interface YesNoVotePollFieldsJSON {
+ resourcePath: string[];
+ supermajorityNumerator: number;
+ supermajorityDenominator: number;
+ minimumVotingPower: YesNoMinimumVotingPowerObject;
+}
+
/**
* 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]
@@ -119,26 +158,43 @@ impl VotePollWasm {
self.0.to_string()
}
+ /// The kind of poll: `contestedDocumentResourceVotePoll` or `yesNoVotePoll`, the `$type`
+ /// tag `toJSON()` writes (the specs check they agree).
+ #[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()),
}
}
@@ -165,6 +221,7 @@ impl VotePollWasm {
Ok(js_array)
}
+ VotePoll::YesNoVotePoll(_) => Err(not_a_contested_poll()),
}
}
@@ -181,6 +238,7 @@ impl VotePollWasm {
VotePoll::ContestedDocumentResourceVotePoll(poll)
}
+ VotePoll::YesNoVotePoll(_) => return Err(not_a_contested_poll()),
};
Ok(())
@@ -190,25 +248,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")]
@@ -228,12 +295,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!(
diff --git a/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts b/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts
index f1aeef73113..dcaeb7fb480 100644
--- a/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts
+++ b/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts
@@ -107,4 +107,48 @@ 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();
+ });
+
+ 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 ed5708195f8..2a534ed7f8f 100644
--- a/packages/wasm-dpp2/tests/unit/Vote.spec.ts
+++ b/packages/wasm-dpp2/tests/unit/Vote.spec.ts
@@ -87,4 +87,100 @@ 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();
+ });
+
+ 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();
+ });
+
+ 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();
+ });
+ });
});
diff --git a/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts b/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts
index a5dd2e4f755..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');
@@ -121,4 +122,75 @@ 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.kind).to.equal(poll.toJSON().$type);
+ 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 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(