diff --git a/Cargo.lock b/Cargo.lock index b468d02278633..a438e7273ae4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4911,7 +4911,7 @@ dependencies = [ [[package]] name = "pallet-elections-phragmen" -version = "3.0.0" +version = "4.0.0" dependencies = [ "frame-benchmarking", "frame-support", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 9d72186966547..512f32d66a66c 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -57,7 +57,7 @@ pallet-contracts-primitives = { version = "3.0.0", default-features = false, pat pallet-contracts-rpc-runtime-api = { version = "3.0.0", default-features = false, path = "../../../frame/contracts/rpc/runtime-api/" } pallet-democracy = { version = "3.0.0", default-features = false, path = "../../../frame/democracy" } pallet-election-provider-multi-phase = { version = "3.0.0", default-features = false, path = "../../../frame/election-provider-multi-phase" } -pallet-elections-phragmen = { version = "3.0.0", default-features = false, path = "../../../frame/elections-phragmen" } +pallet-elections-phragmen = { version = "4.0.0", default-features = false, path = "../../../frame/elections-phragmen" } pallet-gilt = { version = "3.0.0", default-features = false, path = "../../../frame/gilt" } pallet-grandpa = { version = "3.0.0", default-features = false, path = "../../../frame/grandpa" } pallet-im-online = { version = "3.0.0", default-features = false, path = "../../../frame/im-online" } diff --git a/frame/elections-phragmen/CHANGELOG.md b/frame/elections-phragmen/CHANGELOG.md index 3d48448fa55ec..231de1d2e475e 100644 --- a/frame/elections-phragmen/CHANGELOG.md +++ b/frame/elections-phragmen/CHANGELOG.md @@ -4,7 +4,18 @@ All notable changes to this crate will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this crate adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [3.0.0] - UNRELEASED +## [4.0.0] - UNRELEASED + +### Added + +### Changed +\[**Needs Migration**\] [migrate pallet-elections-phragmen to attribute macros](https://github.com/paritytech/substrate/pull/8044) + +### Fixed + +### Security + +## [3.0.0] ### Added [Add slashing events to elections-phragmen](https://github.com/paritytech/substrate/pull/7543) diff --git a/frame/elections-phragmen/Cargo.toml b/frame/elections-phragmen/Cargo.toml index 32ae9968c7bf1..aa2b564f73f24 100644 --- a/frame/elections-phragmen/Cargo.toml +++ b/frame/elections-phragmen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-elections-phragmen" -version = "3.0.0" +version = "4.0.0" authors = ["Parity Technologies "] edition = "2018" license = "Apache-2.0" @@ -16,14 +16,15 @@ targets = ["x86_64-unknown-linux-gnu"] codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } sp-runtime = { version = "3.0.0", default-features = false, path = "../../primitives/runtime" } sp-npos-elections = { version = "3.0.0", default-features = false, path = "../../primitives/npos-elections" } +sp-io = { version = "3.0.0", default-features = false, path = "../../primitives/io" } frame-support = { version = "3.0.0", default-features = false, path = "../support" } frame-system = { version = "3.0.0", default-features = false, path = "../system" } sp-std = { version = "3.0.0", default-features = false, path = "../../primitives/std" } +sp-core = { version = "3.0.0", default-features = false, path = "../../primitives/core" } frame-benchmarking = { version = "3.1.0", default-features = false, path = "../benchmarking", optional = true } log = { version = "0.4.14", default-features = false } [dev-dependencies] -sp-io = { version = "3.0.0", path = "../../primitives/io" } hex-literal = "0.3.1" pallet-balances = { version = "3.0.0", path = "../balances" } sp-core = { version = "3.0.0", path = "../../primitives/core" } @@ -38,6 +39,8 @@ std = [ "sp-npos-elections/std", "frame-system/std", "sp-std/std", + "sp-io/std", + "sp-core/std", "log/std", ] runtime-benchmarks = [ diff --git a/frame/elections-phragmen/src/benchmarking.rs b/frame/elections-phragmen/src/benchmarking.rs index 39e04dcc2dab7..3534a62ac3ce0 100644 --- a/frame/elections-phragmen/src/benchmarking.rs +++ b/frame/elections-phragmen/src/benchmarking.rs @@ -23,9 +23,9 @@ use super::*; use frame_system::RawOrigin; use frame_benchmarking::{benchmarks, account, whitelist, impl_benchmark_test_suite}; -use frame_support::traits::OnInitialize; +use frame_support::{traits::OnInitialize, dispatch::DispatchResultWithPostInfo}; -use crate::Module as Elections; +use crate::Pallet as Elections; const BALANCE_FACTOR: u32 = 250; const MAX_VOTERS: u32 = 500; @@ -87,11 +87,12 @@ fn submit_candidates_with_self_vote(c: u32, prefix: &'static str) Ok(candidates) } - /// Submit one voter. -fn submit_voter(caller: T::AccountId, votes: Vec, stake: BalanceOf) - -> frame_support::dispatch::DispatchResult -{ +fn submit_voter( + caller: T::AccountId, + votes: Vec, + stake: BalanceOf, +) -> DispatchResultWithPostInfo { >::vote(RawOrigin::Signed(caller).into(), votes, stake) } diff --git a/frame/elections-phragmen/src/lib.rs b/frame/elections-phragmen/src/lib.rs index c7fcb4cec8308..dafcc3dd5910e 100644 --- a/frame/elections-phragmen/src/lib.rs +++ b/frame/elections-phragmen/src/lib.rs @@ -100,10 +100,7 @@ use codec::{Decode, Encode}; use frame_support::{ - decl_error, decl_event, decl_module, decl_storage, - dispatch::{DispatchResultWithPostInfo, WithPostDispatchInfo}, - ensure, - storage::{IterableStorageMap, StorageMap}, + dispatch::{WithPostDispatchInfo}, traits::{ ChangeMembers, Contains, ContainsLengthBound, Currency, CurrencyToVote, Get, InitializeMembers, LockIdentifier, LockableCurrency, OnUnbalanced, ReservableCurrency, @@ -111,7 +108,6 @@ use frame_support::{ }, weights::Weight, }; -use frame_system::{ensure_root, ensure_signed}; use sp_npos_elections::{ElectionResult, ExtendedBalance}; use sp_runtime::{ traits::{Saturating, StaticLookup, Zero}, @@ -123,7 +119,8 @@ mod benchmarking; pub mod weights; pub use weights::WeightInfo; -pub mod migrations_3_0_0; +/// All migrations. +pub mod migrations; /// The maximum votes allowed per voter. pub const MAXIMUM_VOTE: usize = 16; @@ -170,213 +167,97 @@ pub struct SeatHolder { pub deposit: Balance, } -pub trait Config: frame_system::Config { - /// The overarching event type.c - type Event: From> + Into<::Event>; +pub use pallet::*; - /// Identifier for the elections-phragmen pallet's lock - type PalletId: Get; - - /// The currency that people are electing with. - type Currency: - LockableCurrency + - ReservableCurrency; - - /// What to do when the members change. - type ChangeMembers: ChangeMembers; - - /// What to do with genesis members - type InitializeMembers: InitializeMembers; - - /// Convert a balance into a number used for election calculation. - /// This must fit into a `u64` but is allowed to be sensibly lossy. - type CurrencyToVote: CurrencyToVote>; - - /// How much should be locked up in order to submit one's candidacy. - type CandidacyBond: Get>; - - /// Base deposit associated with voting. - /// - /// This should be sensibly high to economically ensure the pallet cannot be attacked by - /// creating a gigantic number of votes. - type VotingBondBase: Get>; - - /// The amount of bond that need to be locked for each vote (32 bytes). - type VotingBondFactor: Get>; +#[frame_support::pallet] +pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use super::*; - /// Handler for the unbalanced reduction when a candidate has lost (and is not a runner-up) - type LoserCandidate: OnUnbalanced>; + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + + IsType<::Event>; - /// Handler for the unbalanced reduction when a member has been kicked. - type KickedMember: OnUnbalanced>; + /// Identifier for the elections-phragmen pallet's lock + #[pallet::constant] + type PalletId: Get; - /// Number of members to elect. - type DesiredMembers: Get; + /// The currency that people are electing with. + type Currency: LockableCurrency + + ReservableCurrency; - /// Number of runners_up to keep. - type DesiredRunnersUp: Get; + /// What to do when the members change. + type ChangeMembers: ChangeMembers; - /// How long each seat is kept. This defines the next block number at which an election - /// round will happen. If set to zero, no elections are ever triggered and the module will - /// be in passive mode. - type TermDuration: Get; + /// What to do with genesis members + type InitializeMembers: InitializeMembers; - /// Weight information for extrinsics in this pallet. - type WeightInfo: WeightInfo; -} + /// Convert a balance into a number used for election calculation. + /// This must fit into a `u64` but is allowed to be sensibly lossy. + type CurrencyToVote: CurrencyToVote>; -decl_storage! { - trait Store for Module as PhragmenElection { - /// The current elected members. - /// - /// Invariant: Always sorted based on account id. - pub Members get(fn members): Vec>>; + /// How much should be locked up in order to submit one's candidacy. + #[pallet::constant] + type CandidacyBond: Get>; - /// The current reserved runners-up. + /// Base deposit associated with voting. /// - /// Invariant: Always sorted based on rank (worse to best). Upon removal of a member, the - /// last (i.e. _best_) runner-up will be replaced. - pub RunnersUp get(fn runners_up): Vec>>; + /// This should be sensibly high to economically ensure the pallet cannot be attacked by + /// creating a gigantic number of votes. + #[pallet::constant] + type VotingBondBase: Get>; - /// The present candidate list. A current member or runner-up can never enter this vector - /// and is always implicitly assumed to be a candidate. - /// - /// Second element is the deposit. - /// - /// Invariant: Always sorted based on account id. - pub Candidates get(fn candidates): Vec<(T::AccountId, BalanceOf)>; + /// The amount of bond that need to be locked for each vote (32 bytes). + #[pallet::constant] + type VotingBondFactor: Get>; - /// The total number of vote rounds that have happened, excluding the upcoming one. - pub ElectionRounds get(fn election_rounds): u32 = Zero::zero(); + /// Handler for the unbalanced reduction when a candidate has lost (and is not a runner-up) + type LoserCandidate: OnUnbalanced>; - /// Votes and locked stake of a particular voter. - /// - /// TWOX-NOTE: SAFE as `AccountId` is a crypto hash. - pub Voting get(fn voting): map hasher(twox_64_concat) T::AccountId => Voter>; - } add_extra_genesis { - config(members): Vec<(T::AccountId, BalanceOf)>; - build(|config: &GenesisConfig| { - assert!( - config.members.len() as u32 <= T::DesiredMembers::get(), - "Cannot accept more than DesiredMembers genesis member", - ); - let members = config.members.iter().map(|(ref member, ref stake)| { - // make sure they have enough stake. - assert!( - T::Currency::free_balance(member) >= *stake, - "Genesis member does not have enough stake.", - ); + /// Handler for the unbalanced reduction when a member has been kicked. + type KickedMember: OnUnbalanced>; - // Note: all members will only vote for themselves, hence they must be given exactly - // their own stake as total backing. Any sane election should behave as such. - // Nonetheless, stakes will be updated for term 1 onwards according to the election. - Members::::mutate(|members| { - match members.binary_search_by(|m| m.who.cmp(member)) { - Ok(_) => panic!("Duplicate member in elections-phragmen genesis: {}", member), - Err(pos) => members.insert( - pos, - SeatHolder { who: member.clone(), stake: *stake, deposit: Zero::zero() }, - ), - } - }); + /// Number of members to elect. + #[pallet::constant] + type DesiredMembers: Get; - // set self-votes to make persistent. Genesis voters don't have any bond, nor do - // they have any lock. NOTE: this means that we will still try to remove a lock once - // this genesis voter is removed, and for now it is okay because remove_lock is noop - // if lock is not there. - >::insert( - &member, - Voter { votes: vec![member.clone()], stake: *stake, deposit: Zero::zero() }, - ); + /// Number of runners_up to keep. + #[pallet::constant] + type DesiredRunnersUp: Get; - member.clone() - }).collect::>(); + /// How long each seat is kept. This defines the next block number at which an election + /// round will happen. If set to zero, no elections are ever triggered and the module will + /// be in passive mode. + #[pallet::constant] + type TermDuration: Get; - // report genesis members to upstream, if any. - T::InitializeMembers::initialize_members(&members); - }) + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; } -} -decl_error! { - pub enum Error for Module { - /// Cannot vote when no candidates or members exist. - UnableToVote, - /// Must vote for at least one candidate. - NoVotes, - /// Cannot vote more than candidates. - TooManyVotes, - /// Cannot vote more than maximum allowed. - MaximumVotesExceeded, - /// Cannot vote with stake less than minimum balance. - LowBalance, - /// Voter can not pay voting bond. - UnableToPayBond, - /// Must be a voter. - MustBeVoter, - /// Cannot report self. - ReportSelf, - /// Duplicated candidate submission. - DuplicatedCandidate, - /// Member cannot re-submit candidacy. - MemberSubmit, - /// Runner cannot re-submit candidacy. - RunnerUpSubmit, - /// Candidate does not have enough funds. - InsufficientCandidateFunds, - /// Not a member. - NotMember, - /// The provided count of number of candidates is incorrect. - InvalidWitnessData, - /// The provided count of number of votes is incorrect. - InvalidVoteCount, - /// The renouncing origin presented a wrong `Renouncing` parameter. - InvalidRenouncing, - /// Prediction regarding replacement after member removal is wrong. - InvalidReplacement, - } -} + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(PhantomData); -decl_event!( - pub enum Event where Balance = BalanceOf, ::AccountId { - /// A new term with \[new_members\]. This indicates that enough candidates existed to run the - /// election, not that enough have has been elected. The inner value must be examined for - /// this purpose. A `NewTerm(\[\])` indicates that some candidates got their bond slashed and - /// none were elected, whilst `EmptyTerm` means that no candidates existed to begin with. - NewTerm(Vec<(AccountId, Balance)>), - /// No (or not enough) candidates existed for this round. This is different from - /// `NewTerm(\[\])`. See the description of `NewTerm`. - EmptyTerm, - /// Internal error happened while trying to perform election. - ElectionError, - /// A \[member\] has been removed. This should always be followed by either `NewTerm` or - /// `EmptyTerm`. - MemberKicked(AccountId), - /// Someone has renounced their candidacy. - Renounced(AccountId), - /// A \[candidate\] was slashed by \[amount\] due to failing to obtain a seat as member or - /// runner-up. + #[pallet::hooks] + impl Hooks> for Pallet { + /// What to do at the end of each block. /// - /// Note that old members and runners-up are also candidates. - CandidateSlashed(AccountId, Balance), - /// A \[seat holder\] was slashed by \[amount\] by being forcefully removed from the set. - SeatHolderSlashed(AccountId, Balance), + /// Checks if an election needs to happen or not. + fn on_initialize(n: T::BlockNumber) -> Weight { + let term_duration = T::TermDuration::get(); + if !term_duration.is_zero() && (n % term_duration).is_zero() { + Self::do_phragmen() + } else { + 0 + } + } } -); - -decl_module! { - pub struct Module for enum Call where origin: T::Origin { - type Error = Error; - fn deposit_event() = default; - - const CandidacyBond: BalanceOf = T::CandidacyBond::get(); - const VotingBondBase: BalanceOf = T::VotingBondBase::get(); - const VotingBondFactor: BalanceOf = T::VotingBondFactor::get(); - const DesiredMembers: u32 = T::DesiredMembers::get(); - const DesiredRunnersUp: u32 = T::DesiredRunnersUp::get(); - const TermDuration: T::BlockNumber = T::TermDuration::get(); - const PalletId: LockIdentifier = T::PalletId::get(); + #[pallet::call] + impl Pallet { /// Vote for a set of candidates for the upcoming round of election. This can be called to /// set the initial votes, or update already existing votes. /// @@ -400,16 +281,16 @@ decl_module! { /// # /// We assume the maximum weight among all 3 cases: vote_equal, vote_more and vote_less. /// # - #[weight = + #[pallet::weight( T::WeightInfo::vote_more(votes.len() as u32) .max(T::WeightInfo::vote_less(votes.len() as u32)) .max(T::WeightInfo::vote_equal(votes.len() as u32)) - ] - fn vote( - origin, + )] + pub(crate) fn vote( + origin: OriginFor, votes: Vec, - #[compact] value: BalanceOf, - ) { + #[pallet::compact] value: BalanceOf, + ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; // votes should not be empty and more than `MAXIMUM_VOTE` in any case. @@ -423,9 +304,8 @@ decl_module! { // can never submit a vote of there are no members, and cannot submit more votes than // all potential vote targets. // addition is valid: candidates, members and runners-up will never overlap. - let allowed_votes = candidates_count - .saturating_add(members_count) - .saturating_add(runners_up_count); + let allowed_votes = + candidates_count.saturating_add(members_count).saturating_add(runners_up_count); ensure!(!allowed_votes.is_zero(), Error::::UnableToVote); ensure!(votes.len() <= allowed_votes, Error::::TooManyVotes); @@ -438,15 +318,16 @@ decl_module! { Ordering::Greater => { // Must reserve a bit more. let to_reserve = new_deposit - old_deposit; - T::Currency::reserve(&who, to_reserve).map_err(|_| Error::::UnableToPayBond)?; - }, - Ordering::Equal => {}, + T::Currency::reserve(&who, to_reserve) + .map_err(|_| Error::::UnableToPayBond)?; + } + Ordering::Equal => {} Ordering::Less => { // Must unreserve a bit. let to_unreserve = old_deposit - new_deposit; let _remainder = T::Currency::unreserve(&who, to_unreserve); debug_assert!(_remainder.is_zero()); - }, + } }; // Amount to be locked up. @@ -459,6 +340,7 @@ decl_module! { ); Voting::::insert(&who, Voter { votes, deposit: new_deposit, stake: locked_stake }); + Ok(None.into()) } /// Remove `origin` as a voter. @@ -466,11 +348,12 @@ decl_module! { /// This removes the lock and returns the deposit. /// /// The dispatch origin of this call must be signed and be a voter. - #[weight = T::WeightInfo::remove_voter()] - fn remove_voter(origin) { + #[pallet::weight(T::WeightInfo::remove_voter())] + pub(crate) fn remove_voter(origin: OriginFor) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; ensure!(Self::is_voter(&who), Error::::MustBeVoter); Self::do_remove_voter(&who); + Ok(None.into()) } /// Submit oneself for candidacy. A fixed amount of deposit is recorded. @@ -488,15 +371,15 @@ decl_module! { /// # /// The number of current candidates must be provided as witness data. /// # - #[weight = T::WeightInfo::submit_candidacy(*candidate_count)] - fn submit_candidacy(origin, #[compact] candidate_count: u32) { + #[pallet::weight(T::WeightInfo::submit_candidacy(*candidate_count))] + pub(crate) fn submit_candidacy( + origin: OriginFor, + #[pallet::compact] candidate_count: u32, + ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; let actual_count = >::decode_len().unwrap_or(0); - ensure!( - actual_count as u32 <= candidate_count, - Error::::InvalidWitnessData, - ); + ensure!(actual_count as u32 <= candidate_count, Error::::InvalidWitnessData,); let index = Self::is_candidate(&who).err().ok_or(Error::::DuplicatedCandidate)?; @@ -507,6 +390,7 @@ decl_module! { .map_err(|_| Error::::InsufficientCandidateFunds)?; >::mutate(|c| c.insert(index, (who, T::CandidacyBond::get()))); + Ok(None.into()) } /// Renounce one's intention to be a candidate for the next election round. 3 potential @@ -518,27 +402,30 @@ decl_module! { /// origin is removed as a runner-up. /// - `origin` is a current member. In this case, the deposit is unreserved and origin is /// removed as a member, consequently not being a candidate for the next round anymore. - /// Similar to [`remove_members`], if replacement runners exists, they are immediately used. - /// If the prime is renouncing, then no prime will exist until the next round. + /// Similar to [`remove_members`], if replacement runners exists, they are immediately + /// used. If the prime is renouncing, then no prime will exist until the next round. /// /// The dispatch origin of this call must be signed, and have one of the above roles. /// /// # /// The type of renouncing must be provided as witness data. /// # - #[weight = match *renouncing { + #[pallet::weight(match *renouncing { Renouncing::Candidate(count) => T::WeightInfo::renounce_candidacy_candidate(count), Renouncing::Member => T::WeightInfo::renounce_candidacy_members(), Renouncing::RunnerUp => T::WeightInfo::renounce_candidacy_runners_up(), - }] - fn renounce_candidacy(origin, renouncing: Renouncing) { + })] + pub(crate) fn renounce_candidacy( + origin: OriginFor, + renouncing: Renouncing, + ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; match renouncing { Renouncing::Member => { let _ = Self::remove_and_replace_member(&who, false) .map_err(|_| Error::::InvalidRenouncing)?; - Self::deposit_event(RawEvent::Renounced(who)); - }, + Self::deposit_event(Event::Renounced(who)); + } Renouncing::RunnerUp => { >::try_mutate::<_, Error, _>(|runners_up| { let index = runners_up @@ -549,7 +436,7 @@ decl_module! { let SeatHolder { deposit, .. } = runners_up.remove(index); let _remainder = T::Currency::unreserve(&who, deposit); debug_assert!(_remainder.is_zero()); - Self::deposit_event(RawEvent::Renounced(who)); + Self::deposit_event(Event::Renounced(who)); Ok(()) })?; } @@ -562,11 +449,12 @@ decl_module! { let (_removed, deposit) = candidates.remove(index); let _remainder = T::Currency::unreserve(&who, deposit); debug_assert!(_remainder.is_zero()); - Self::deposit_event(RawEvent::Renounced(who)); + Self::deposit_event(Event::Renounced(who)); Ok(()) })?; } }; + Ok(None.into()) } /// Remove a particular member from the set. This is effective immediately and the bond of @@ -583,13 +471,13 @@ decl_module! { /// If we have a replacement, we use a small weight. Else, since this is a root call and /// will go into phragmen, we assume full block for now. /// # - #[weight = if *has_replacement { + #[pallet::weight(if *has_replacement { T::WeightInfo::remove_member_with_replacement() } else { T::BlockWeights::get().max_block - }] - fn remove_member( - origin, + })] + pub(crate) fn remove_member( + origin: OriginFor, who: ::Source, has_replacement: bool, ) -> DispatchResultWithPostInfo { @@ -601,13 +489,13 @@ decl_module! { // In both cases, we will change more weight than need. Refund and abort. return Err(Error::::InvalidReplacement.with_weight( // refund. The weight value comes from a benchmark which is special to this. - T::WeightInfo::remove_member_wrong_refund() + T::WeightInfo::remove_member_wrong_refund(), )); } let had_replacement = Self::remove_and_replace_member(&who, true)?; debug_assert_eq!(has_replacement, had_replacement); - Self::deposit_event(RawEvent::MemberKicked(who.clone())); + Self::deposit_event(Event::MemberKicked(who.clone())); if !had_replacement { Self::do_phragmen(); @@ -627,36 +515,197 @@ decl_module! { /// # /// The total number of voters and those that are defunct must be provided as witness data. /// # - #[weight = T::WeightInfo::clean_defunct_voters(*_num_voters, *_num_defunct)] - fn clean_defunct_voters(origin, _num_voters: u32, _num_defunct: u32) { + #[pallet::weight(T::WeightInfo::clean_defunct_voters(*_num_voters, *_num_defunct))] + pub(crate) fn clean_defunct_voters( + origin: OriginFor, + _num_voters: u32, + _num_defunct: u32, + ) -> DispatchResultWithPostInfo { let _ = ensure_root(origin)?; >::iter() .filter(|(_, x)| Self::is_defunct_voter(&x.votes)) - .for_each(|(dv, _)| { - Self::do_remove_voter(&dv) - }) + .for_each(|(dv, _)| Self::do_remove_voter(&dv)); + + Ok(None.into()) } + } - /// What to do at the end of each block. + #[pallet::event] + #[pallet::metadata( + ::AccountId = "AccountId", + BalanceOf = "Balance", + Vec<(::AccountId, BalanceOf)> = "Vec<(AccountId, Balance)>", + )] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new term with \[new_members\]. This indicates that enough candidates existed to run + /// the election, not that enough have has been elected. The inner value must be examined + /// for this purpose. A `NewTerm(\[\])` indicates that some candidates got their bond + /// slashed and none were elected, whilst `EmptyTerm` means that no candidates existed to + /// begin with. + NewTerm(Vec<(::AccountId, BalanceOf)>), + /// No (or not enough) candidates existed for this round. This is different from + /// `NewTerm(\[\])`. See the description of `NewTerm`. + EmptyTerm, + /// Internal error happened while trying to perform election. + ElectionError, + /// A \[member\] has been removed. This should always be followed by either `NewTerm` or + /// `EmptyTerm`. + MemberKicked(::AccountId), + /// Someone has renounced their candidacy. + Renounced(::AccountId), + /// A \[candidate\] was slashed by \[amount\] due to failing to obtain a seat as member or + /// runner-up. /// - /// Checks if an election needs to happen or not. - fn on_initialize(n: T::BlockNumber) -> Weight { - let term_duration = T::TermDuration::get(); - if !term_duration.is_zero() && (n % term_duration).is_zero() { - Self::do_phragmen() - } else { - 0 - } + /// Note that old members and runners-up are also candidates. + CandidateSlashed(::AccountId, BalanceOf), + /// A \[seat holder\] was slashed by \[amount\] by being forcefully removed from the set. + SeatHolderSlashed(::AccountId, BalanceOf), + } + + #[deprecated(note = "use `Event` instead")] + pub type RawEvent = Event; + + #[pallet::error] + pub enum Error { + /// Cannot vote when no candidates or members exist. + UnableToVote, + /// Must vote for at least one candidate. + NoVotes, + /// Cannot vote more than candidates. + TooManyVotes, + /// Cannot vote more than maximum allowed. + MaximumVotesExceeded, + /// Cannot vote with stake less than minimum balance. + LowBalance, + /// Voter can not pay voting bond. + UnableToPayBond, + /// Must be a voter. + MustBeVoter, + /// Cannot report self. + ReportSelf, + /// Duplicated candidate submission. + DuplicatedCandidate, + /// Member cannot re-submit candidacy. + MemberSubmit, + /// Runner cannot re-submit candidacy. + RunnerUpSubmit, + /// Candidate does not have enough funds. + InsufficientCandidateFunds, + /// Not a member. + NotMember, + /// The provided count of number of candidates is incorrect. + InvalidWitnessData, + /// The provided count of number of votes is incorrect. + InvalidVoteCount, + /// The renouncing origin presented a wrong `Renouncing` parameter. + InvalidRenouncing, + /// Prediction regarding replacement after member removal is wrong. + InvalidReplacement, + } + + /// The current elected members. + /// + /// Invariant: Always sorted based on account id. + #[pallet::storage] + #[pallet::getter(fn members)] + pub type Members = + StorageValue<_, Vec>>, ValueQuery>; + + /// The current reserved runners-up. + /// + /// Invariant: Always sorted based on rank (worse to best). Upon removal of a member, the + /// last (i.e. _best_) runner-up will be replaced. + #[pallet::storage] + #[pallet::getter(fn runners_up)] + pub type RunnersUp = + StorageValue<_, Vec>>, ValueQuery>; + + /// The present candidate list. A current member or runner-up can never enter this vector + /// and is always implicitly assumed to be a candidate. + /// + /// Second element is the deposit. + /// + /// Invariant: Always sorted based on account id. + #[pallet::storage] + #[pallet::getter(fn candidates)] + pub type Candidates = StorageValue<_, Vec<(T::AccountId, BalanceOf)>, ValueQuery>; + + /// The total number of vote rounds that have happened, excluding the upcoming one. + #[pallet::storage] + #[pallet::getter(fn election_rounds)] + pub type ElectionRounds = StorageValue<_, u32, ValueQuery>; + + /// Votes and locked stake of a particular voter. + /// + /// TWOX-NOTE: SAFE as `AccountId` is a crypto hash. + #[pallet::storage] + #[pallet::getter(fn voting)] + pub type Voting = + StorageMap<_, Twox64Concat, T::AccountId, Voter>, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub members: Vec<(T::AccountId, BalanceOf)>, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + Self { members: Default::default() } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + assert!( + self.members.len() as u32 <= T::DesiredMembers::get(), + "Cannot accept more than DesiredMembers genesis member", + ); + let members = self.members.iter().map(|(ref member, ref stake)| { + // make sure they have enough stake. + assert!( + T::Currency::free_balance(member) >= *stake, + "Genesis member does not have enough stake.", + ); + + // Note: all members will only vote for themselves, hence they must be given exactly + // their own stake as total backing. Any sane election should behave as such. + // Nonetheless, stakes will be updated for term 1 onwards according to the election. + Members::::mutate(|members| { + match members.binary_search_by(|m| m.who.cmp(member)) { + Ok(_) => panic!("Duplicate member in elections-phragmen genesis: {}", member), + Err(pos) => members.insert( + pos, + SeatHolder { who: member.clone(), stake: *stake, deposit: Zero::zero() }, + ), + } + }); + + // set self-votes to make persistent. Genesis voters don't have any bond, nor do + // they have any lock. NOTE: this means that we will still try to remove a lock once + // this genesis voter is removed, and for now it is okay because remove_lock is noop + // if lock is not there. + >::insert( + &member, + Voter { votes: vec![member.clone()], stake: *stake, deposit: Zero::zero() }, + ); + + member.clone() + }).collect::>(); + + // report genesis members to upstream, if any. + T::InitializeMembers::initialize_members(&members); } } } -impl Module { +impl Pallet { /// The deposit value of `count` votes. fn deposit_of(count: usize) -> BalanceOf { - T::VotingBondBase::get().saturating_add( - T::VotingBondFactor::get().saturating_mul((count as u32).into()) - ) + T::VotingBondBase::get() + .saturating_add(T::VotingBondFactor::get().saturating_mul((count as u32).into())) } /// Attempts to remove a member `who`. If a runner-up exists, it is used as the replacement. @@ -691,7 +740,7 @@ impl Module { let (imbalance, _remainder) = T::Currency::slash_reserved(who, removed.deposit); debug_assert!(_remainder.is_zero()); T::LoserCandidate::on_unbalanced(imbalance); - Self::deposit_event(RawEvent::SeatHolderSlashed(who.clone(), removed.deposit)); + Self::deposit_event(Event::SeatHolderSlashed(who.clone(), removed.deposit)); } else { T::Currency::unreserve(who, removed.deposit); } @@ -829,7 +878,7 @@ impl Module { candidates_and_deposit.append(&mut Self::implicit_candidates_with_deposit()); if candidates_and_deposit.len().is_zero() { - Self::deposit_event(RawEvent::EmptyTerm); + Self::deposit_event(Event::EmptyTerm); return T::DbWeight::get().reads(5); } @@ -956,7 +1005,7 @@ impl Module { { let (imbalance, _) = T::Currency::slash_reserved(c, *d); T::LoserCandidate::on_unbalanced(imbalance); - Self::deposit_event(RawEvent::CandidateSlashed(c.clone(), *d)); + Self::deposit_event(Event::CandidateSlashed(c.clone(), *d)); } }); @@ -996,28 +1045,28 @@ impl Module { // clean candidates. >::kill(); - Self::deposit_event(RawEvent::NewTerm(new_members_sorted_by_id)); - ElectionRounds::mutate(|v| *v += 1); + Self::deposit_event(Event::NewTerm(new_members_sorted_by_id)); + >::mutate(|v| *v += 1); }).map_err(|e| { log::error!( target: "runtime::elections-phragmen", "Failed to run election [{:?}].", e, ); - Self::deposit_event(RawEvent::ElectionError); + Self::deposit_event(Event::ElectionError); }); T::WeightInfo::election_phragmen(weight_candidates, weight_voters, weight_edges) } } -impl Contains for Module { +impl Contains for Pallet { fn contains(who: &T::AccountId) -> bool { Self::is_member(who) } } -impl SortedMembers for Module { +impl SortedMembers for Pallet { fn contains(who: &T::AccountId) -> bool { Self::is_member(who) } @@ -1039,8 +1088,10 @@ impl SortedMembers for Module { } } -impl ContainsLengthBound for Module { - fn min_len() -> usize { 0 } +impl ContainsLengthBound for Pallet { + fn min_len() -> usize { + 0 + } /// Implementation uses a parameter type so calling is cost-free. fn max_len() -> usize { @@ -1051,15 +1102,18 @@ impl ContainsLengthBound for Module { #[cfg(test)] mod tests { use super::*; - use frame_support::{assert_ok, assert_noop, parameter_types, - traits::OnInitialize, + use frame_support::{ + assert_ok, assert_noop, parameter_types, traits::OnInitialize, + dispatch::DispatchResultWithPostInfo, }; use substrate_test_utils::assert_eq_uvec; use sp_core::H256; use sp_runtime::{ - testing::Header, BuildStorage, DispatchResult, + BuildStorage, + testing::Header, traits::{BlakeTwo256, IdentityLookup}, }; + use frame_system::ensure_signed; use crate as elections_phragmen; parameter_types! { @@ -1375,11 +1429,11 @@ mod tests { ensure_members_has_approval_stake(); } - fn submit_candidacy(origin: Origin) -> DispatchResult { + fn submit_candidacy(origin: Origin) -> DispatchResultWithPostInfo { Elections::submit_candidacy(origin, Elections::candidates().len() as u32) } - fn vote(origin: Origin, votes: Vec, stake: u64) -> DispatchResult { + fn vote(origin: Origin, votes: Vec, stake: u64) -> DispatchResultWithPostInfo { // historical note: helper function was created in a period of time in which the API of vote // call was changing. Currently it is a wrapper for the original call and does not do much. // Nonetheless, totally harmless. @@ -2080,7 +2134,7 @@ mod tests { assert_eq!( System::events().iter().last().unwrap().event, - Event::elections_phragmen(RawEvent::EmptyTerm), + Event::elections_phragmen(super::Event::EmptyTerm), ) }) } @@ -2099,7 +2153,7 @@ mod tests { assert_eq!( System::events().iter().last().unwrap().event, - Event::elections_phragmen(RawEvent::NewTerm(vec![(4, 40), (5, 50)])), + Event::elections_phragmen(super::Event::NewTerm(vec![(4, 40), (5, 50)])), ); assert_eq!(members_and_stake(), vec![(4, 40), (5, 50)]); @@ -2113,7 +2167,7 @@ mod tests { assert_eq!( System::events().iter().last().unwrap().event, - Event::elections_phragmen(RawEvent::NewTerm(vec![])), + Event::elections_phragmen(super::Event::NewTerm(vec![])), ); // outgoing have lost their bond. @@ -2186,7 +2240,7 @@ mod tests { assert_eq!( System::events().iter().last().unwrap().event, - Event::elections_phragmen(RawEvent::NewTerm(vec![])), + Event::elections_phragmen(super::Event::NewTerm(vec![])), ) }); } @@ -2546,7 +2600,7 @@ mod tests { assert_eq!(balances(&5), (45, 2)); assert!(System::events().iter().any(|event| { - event.event == Event::elections_phragmen(RawEvent::NewTerm(vec![(4, 40), (5, 50)])) + event.event == Event::elections_phragmen(super::Event::NewTerm(vec![(4, 40), (5, 50)])) })); }) } diff --git a/frame/elections-phragmen/src/migrations/mod.rs b/frame/elections-phragmen/src/migrations/mod.rs new file mode 100644 index 0000000000000..9a1f86a1ad7ce --- /dev/null +++ b/frame/elections-phragmen/src/migrations/mod.rs @@ -0,0 +1,23 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! All migrations of this pallet. + +/// Version 3. +pub mod v3; +/// Version 4. +pub mod v4; diff --git a/frame/elections-phragmen/src/migrations_3_0_0.rs b/frame/elections-phragmen/src/migrations/v3.rs similarity index 100% rename from frame/elections-phragmen/src/migrations_3_0_0.rs rename to frame/elections-phragmen/src/migrations/v3.rs diff --git a/frame/elections-phragmen/src/migrations/v4.rs b/frame/elections-phragmen/src/migrations/v4.rs new file mode 100644 index 0000000000000..f704b203d34cf --- /dev/null +++ b/frame/elections-phragmen/src/migrations/v4.rs @@ -0,0 +1,110 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Migrations to version [`4.0.0`], as denoted by the changelog. + +use frame_support::{ + weights::Weight, + traits::{GetPalletVersion, PalletVersion, Get}, +}; + +/// The old prefix. +pub const OLD_PREFIX: &[u8] = b"PhragmenElection"; + +/// Migrate the entire storage of this pallet to a new prefix. +/// +/// This new prefix must be the same as the one set in construct_runtime. For safety, use +/// `PalletInfo` to get it, as: +/// `::PalletInfo::name::`. +/// +/// The old storage prefix, `PhragmenElection` is hardcoded in the migration code. +pub fn migrate< + T: frame_system::Config, + P: GetPalletVersion, + N: AsRef, +>(new_pallet_name: N) -> Weight { + if new_pallet_name.as_ref().as_bytes() == OLD_PREFIX { + log::info!( + target: "runtime::elections-phragmen", + "New pallet name is equal to the old prefix. No migration needs to be done.", + ); + return 0; + } + let maybe_storage_version =

::storage_version(); + log::info!( + target: "runtime::elections-phragmen", + "Running migration to v4 for elections-phragmen with storage version {:?}", + maybe_storage_version, + ); + + match maybe_storage_version { + Some(storage_version) if storage_version <= PalletVersion::new(3, 0, 0) => { + log::info!("new prefix: {}", new_pallet_name.as_ref()); + frame_support::storage::migration::move_pallet( + OLD_PREFIX, + new_pallet_name.as_ref().as_bytes(), + ); + ::BlockWeights::get().max_block + } + _ => { + log::warn!( + target: "runtime::elections-phragmen", + "Attempted to apply migration to v4 but failed because storage version is {:?}", + maybe_storage_version, + ); + 0 + }, + } +} + +/// Some checks prior to migration. This can be linked to +/// [`frame_support::traits::OnRuntimeUpgrade::pre_upgrade`] for further testing. +/// +/// Panics if anything goes wrong. +pub fn pre_migration>(new: N) { + let new = new.as_ref(); + log::info!("pre-migration elections-phragmen test with new = {}", new); + + // the next key must exist, and start with the hash of `OLD_PREFIX`. + let next_key = sp_io::storage::next_key(OLD_PREFIX).unwrap(); + assert!(next_key.starts_with(&sp_io::hashing::twox_128(OLD_PREFIX))); + + // ensure nothing is stored in the new prefix. + assert!( + sp_io::storage::next_key(new.as_bytes()).map_or( + // either nothing is there + true, + // or we ensure that it has no common prefix with twox_128(new). + |next_key| !next_key.starts_with(&sp_io::hashing::twox_128(new.as_bytes())) + ), + "unexpected next_key({}) = {:?}", + new, + sp_core::hexdisplay::HexDisplay::from(&sp_io::storage::next_key(new.as_bytes()).unwrap()) + ); + // ensure storage version is 3. + assert!(

::storage_version().unwrap().major == 3); +} + +/// Some checks for after migration. This can be linked to +/// [`frame_support::traits::OnRuntimeUpgrade::post_upgrade`] for further testing. +/// +/// Panics if anything goes wrong. +pub fn post_migration

() { + log::info!("post-migration elections-phragmen"); + // ensure we've been updated to v4 by the automatic write of crate version -> storage version. + assert!(

::storage_version().unwrap().major == 4); +}