diff --git a/substrate/node/cli/src/chain_spec.rs b/substrate/node/cli/src/chain_spec.rs index bc24de9ddf..242c5078f5 100644 --- a/substrate/node/cli/src/chain_spec.rs +++ b/substrate/node/cli/src/chain_spec.rs @@ -124,12 +124,14 @@ fn staging_testnet_config_genesis() -> GenesisConfig { active_council: vec![], candidacy_bond: 10 * DOLLARS, voter_bond: 1 * DOLLARS, + voting_fee: 2 * DOLLARS, present_slash_per_voter: 1 * CENTS, carry_count: 6, presentation_duration: 1 * DAYS, approval_voting_period: 2 * DAYS, term_duration: 28 * DAYS, desired_seats: 0, + decay_ratio: 0, inactive_grace_period: 1, // one additional vote should go by before an inactive voter can be reaped. }), timestamp: Some(TimestampConfig { @@ -234,6 +236,7 @@ pub fn testnet_genesis( const STASH: u128 = 1 << 20; const ENDOWMENT: u128 = 1 << 20; + let council_desired_seats = (endowed_accounts.len() / 2 - initial_authorities.len()) as u32; let mut contract_config = ContractConfig { signed_claim_handicap: 2, rent_byte_price: 4, @@ -299,12 +302,14 @@ pub fn testnet_genesis( .map(|a| (a.clone(), 1000000)).collect(), candidacy_bond: 10, voter_bond: 2, + voting_fee: 5, present_slash_per_voter: 1, carry_count: 4, presentation_duration: 10, approval_voting_period: 20, term_duration: 1000000, - desired_seats: (endowed_accounts.len() / 2 - initial_authorities.len()) as u32, + desired_seats: council_desired_seats, + decay_ratio: council_desired_seats / 3, inactive_grace_period: 1, }), timestamp: Some(TimestampConfig { diff --git a/substrate/node/runtime/src/lib.rs b/substrate/node/runtime/src/lib.rs index c5104f331e..427ee8d1fc 100644 --- a/substrate/node/runtime/src/lib.rs +++ b/substrate/node/runtime/src/lib.rs @@ -182,9 +182,12 @@ impl council::Trait for Runtime { type Event = Event; type BadPresentation = (); type BadReaper = (); + type BadVoterIndex = (); + type LoserCandidate = (); type OnMembersChanged = CouncilMotions; } + impl council::motions::Trait for Runtime { type Origin = Origin; type Proposal = Call; diff --git a/substrate/srml/council/src/lib.rs b/substrate/srml/council/src/lib.rs index a72e93c9eb..fd69805732 100644 --- a/substrate/srml/council/src/lib.rs +++ b/substrate/srml/council/src/lib.rs @@ -116,6 +116,8 @@ mod tests { type Event = Event; type BadPresentation = (); type BadReaper = (); + type BadVoterIndex = (); + type LoserCandidate = (); type OnMembersChanged = CouncilMotions; } impl motions::Trait for Test { @@ -124,35 +126,91 @@ mod tests { type Event = Event; } - pub fn new_test_ext(with_council: bool) -> runtime_io::TestExternalities { - let mut t = system::GenesisConfig::::default().build_storage().unwrap().0; - t.extend(balances::GenesisConfig::{ - transaction_base_fee: 0, - transaction_byte_fee: 0, - balances: vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)], - existential_deposit: 0, - transfer_fee: 0, - creation_fee: 0, - vesting: vec![], - }.build_storage().unwrap().0); - t.extend(democracy::GenesisConfig::::default().build_storage().unwrap().0); - t.extend(seats::GenesisConfig:: { - candidacy_bond: 9, - voter_bond: 3, - present_slash_per_voter: 1, - carry_count: 2, - inactive_grace_period: 1, - active_council: if with_council { vec![ - (1, 10), - (2, 10), - (3, 10) - ] } else { vec![] }, - approval_voting_period: 4, - presentation_duration: 2, - desired_seats: 2, - term_duration: 5, - }.build_storage().unwrap().0); - runtime_io::TestExternalities::new(t) + pub struct ExtBuilder { + balance_factor: u64, + decay_ratio: u32, + voting_fee: u64, + voter_bond: u64, + bad_presentation_punishment: u64, + with_council: bool, + } + + impl Default for ExtBuilder { + fn default() -> Self { + Self { + balance_factor: 1, + decay_ratio: 24, + voting_fee: 0, + voter_bond: 0, + bad_presentation_punishment: 1, + with_council: false, + } + } + } + + impl ExtBuilder { + pub fn with_council(mut self, council: bool) -> Self { + self.with_council = council; + self + } + pub fn balance_factor(mut self, factor: u64) -> Self { + self.balance_factor = factor; + self + } + pub fn decay_ratio(mut self, ratio: u32) -> Self { + self.decay_ratio = ratio; + self + } + pub fn voting_fee(mut self, fee: u64) -> Self { + self.voting_fee = fee; + self + } + pub fn bad_presentation_punishment(mut self, fee: u64) -> Self { + self.bad_presentation_punishment = fee; + self + } + pub fn voter_bond(mut self, fee: u64) -> Self { + self.voter_bond = fee; + self + } + pub fn build(self) -> runtime_io::TestExternalities { + let mut t = system::GenesisConfig::::default().build_storage().unwrap().0; + t.extend(balances::GenesisConfig::{ + transaction_base_fee: 0, + transaction_byte_fee: 0, + balances: vec![ + (1, 10 * self.balance_factor), + (2, 20 * self.balance_factor), + (3, 30 * self.balance_factor), + (4, 40 * self.balance_factor), + (5, 50 * self.balance_factor), + (6, 60 * self.balance_factor) + ], + existential_deposit: 0, + transfer_fee: 0, + creation_fee: 0, + vesting: vec![], + }.build_storage().unwrap().0); + t.extend(seats::GenesisConfig:: { + candidacy_bond: 3, + voter_bond: self.voter_bond, + present_slash_per_voter: self.bad_presentation_punishment, + carry_count: 2, + inactive_grace_period: 1, + active_council: if self.with_council { vec![ + (1, 10), + (2, 10), + (3, 10) + ] } else { vec![] }, + approval_voting_period: 4, + presentation_duration: 2, + desired_seats: 2, + decay_ratio: self.decay_ratio, + voting_fee: self.voting_fee, + term_duration: 5, + }.build_storage().unwrap().0); + runtime_io::TestExternalities::new(t) + } } pub type System = system::Module; diff --git a/substrate/srml/council/src/motions.rs b/substrate/srml/council/src/motions.rs index 4ab4e84c12..df357ac8c8 100644 --- a/substrate/srml/council/src/motions.rs +++ b/substrate/srml/council/src/motions.rs @@ -326,8 +326,8 @@ mod tests { use hex_literal::hex; #[test] - fn basic_environment_works() { - with_externalities(&mut new_test_ext(true), || { + fn motions_basic_environment_works() { + with_externalities(&mut ExtBuilder::default().with_council(true).build(), || { System::set_block_number(1); assert_eq!(Balances::free_balance(&42), 0); assert_eq!(CouncilMotions::proposals(), Vec::::new()); @@ -340,7 +340,7 @@ mod tests { #[test] fn removal_of_old_voters_votes_works() { - with_externalities(&mut new_test_ext(true), || { + with_externalities(&mut ExtBuilder::default().with_council(true).build(), || { System::set_block_number(1); let proposal = set_balance_proposal(42); let hash = BlakeTwo256::hash_of(&proposal); @@ -374,7 +374,7 @@ mod tests { #[test] fn propose_works() { - with_externalities(&mut new_test_ext(true), || { + with_externalities(&mut ExtBuilder::default().with_council(true).build(), || { System::set_block_number(1); let proposal = set_balance_proposal(42); let hash = proposal.blake2_256().into(); @@ -397,8 +397,8 @@ mod tests { } #[test] - fn ignoring_non_council_proposals_works() { - with_externalities(&mut new_test_ext(true), || { + fn motions_ignoring_non_council_proposals_works() { + with_externalities(&mut ExtBuilder::default().with_council(true).build(), || { System::set_block_number(1); let proposal = set_balance_proposal(42); assert_noop!(CouncilMotions::propose(Origin::signed(42), 3, Box::new(proposal.clone())), "proposer not on council"); @@ -406,8 +406,8 @@ mod tests { } #[test] - fn ignoring_non_council_votes_works() { - with_externalities(&mut new_test_ext(true), || { + fn motions_ignoring_non_council_votes_works() { + with_externalities(&mut ExtBuilder::default().with_council(true).build(), || { System::set_block_number(1); let proposal = set_balance_proposal(42); let hash: H256 = proposal.blake2_256().into(); @@ -417,8 +417,8 @@ mod tests { } #[test] - fn ignoring_bad_index_council_vote_works() { - with_externalities(&mut new_test_ext(true), || { + fn motions_ignoring_bad_index_council_vote_works() { + with_externalities(&mut ExtBuilder::default().with_council(true).build(), || { System::set_block_number(3); let proposal = set_balance_proposal(42); let hash: H256 = proposal.blake2_256().into(); @@ -428,8 +428,8 @@ mod tests { } #[test] - fn revoting_works() { - with_externalities(&mut new_test_ext(true), || { + fn motions_revoting_works() { + with_externalities(&mut ExtBuilder::default().with_council(true).build(), || { System::set_block_number(1); let proposal = set_balance_proposal(42); let hash: H256 = proposal.blake2_256().into(); @@ -462,8 +462,8 @@ mod tests { } #[test] - fn disapproval_works() { - with_externalities(&mut new_test_ext(true), || { + fn motions_disapproval_works() { + with_externalities(&mut ExtBuilder::default().with_council(true).build(), || { System::set_block_number(1); let proposal = set_balance_proposal(42); let hash: H256 = proposal.blake2_256().into(); @@ -491,8 +491,8 @@ mod tests { } #[test] - fn approval_works() { - with_externalities(&mut new_test_ext(true), || { + fn motions_approval_works() { + with_externalities(&mut ExtBuilder::default().with_council(true).build(), || { System::set_block_number(1); let proposal = set_balance_proposal(42); let hash: H256 = proposal.blake2_256().into(); diff --git a/substrate/srml/council/src/seats.rs b/substrate/srml/council/src/seats.rs index 15817df70b..3e80584900 100644 --- a/substrate/srml/council/src/seats.rs +++ b/substrate/srml/council/src/seats.rs @@ -17,13 +17,18 @@ //! Council system: Handles the voting in and maintenance of council members. use rstd::prelude::*; -use primitives::traits::{Zero, One, StaticLookup}; +use primitives::traits::{Zero, One, StaticLookup, Bounded, Saturating}; use runtime_io::print; use srml_support::{ - StorageValue, StorageMap, dispatch::Result, decl_storage, decl_event, ensure, - traits::{Currency, ReservableCurrency, OnUnbalanced} + StorageValue, StorageMap, + dispatch::Result, decl_storage, decl_event, ensure, decl_module, + traits::{ + Currency, ReservableCurrency, OnUnbalanced, LockIdentifier, + LockableCurrency, WithdrawReasons, WithdrawReason, ExistenceRequirement + } }; use democracy; +use parity_codec::{Encode, Decode}; use system::{self, ensure_signed}; use super::OnMembersChanged; @@ -81,13 +86,51 @@ use super::OnMembersChanged; // after each vote as all but K entries are cleared. newly registering candidates must use cleared // entries before they increase the capacity. -use srml_support::decl_module; -pub type VoteIndex = u32; +/// The activity status of a voter. +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Default)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct VoterInfo { + /// Last VoteIndex in which this voter assigned (or initialized) approvals. + last_active: VoteIndex, + /// Last VoteIndex in which one of this voter's approvals won. + /// Note that `last_win = N` indicates a last win at index `N-1`, hence `last_win = 0` means no win ever. + last_win: VoteIndex, + /// The amount of stored weight as a result of not winning but changing approvals. + pot: Balance, + /// Current staked amount. A lock equal to this value always exists. + stake: Balance, +} + +/// Used to demonstrate the status of a particular index in the global voter list. +#[derive(PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +pub enum CellStatus { + /// Any out of bound index. Means a push a must happen to the chunk pointed by `NextVoterSet`. + /// Voting fee is applied in case a new chunk is created. + Head, + /// Already occupied by another voter. Voting fee is applied. + Occupied, + /// Empty hole which should be filled. No fee will be applied. + Hole, +} + +const COUNCIL_SEATS_ID: LockIdentifier = *b"councils"; + +pub const VOTER_SET_SIZE: usize = 64; +pub const APPROVAL_SET_SIZE: usize = 8; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; +type SetIndex = u32; +pub type VoteIndex = u32; + +// all three must be in sync. +type ApprovalFlag = u32; +pub const APPROVAL_FLAG_MASK: ApprovalFlag = 0x8000_0000; +pub const APPROVAL_FLAG_LEN: usize = 32; + pub trait Trait: democracy::Trait { type Event: From> + Into<::Event>; @@ -97,6 +140,11 @@ pub trait Trait: democracy::Trait { /// Handler for the unbalanced reduction when slashing an invalid reaping attempt. type BadReaper: OnUnbalanced>; + /// Handler for the unbalanced reduction when submitting a bad `voter_index`. + type BadVoterIndex: OnUnbalanced>; + + /// Handler for the unbalanced reduction when a candidate has lost (and is not a runner up) + type LoserCandidate: OnUnbalanced>; /// What to do when the members change. type OnMembersChanged: OnMembersChanged; } @@ -107,22 +155,40 @@ decl_module! { /// Set candidate approvals. Approval slots stay valid as long as candidates in those slots /// are registered. - fn set_approvals(origin, votes: Vec, #[compact] index: VoteIndex) -> Result { + /// + /// Locks the total balance of caller indefinitely. + /// Only [`retract_voter`] or [`reap_inactive_voter`] can unlock the balance. + /// + /// `hint` argument is interpreted differently based on: + /// - if `origin` is setting approvals for the first time: The index will be checked + /// for being a valid _hole_ in the voter list. + /// - if the hint is correctly pointing to a hole, no fee is deducted from `origin`. + /// - Otherwise, the call will succeed but the index is ignored and simply a push to the last chunk + /// with free space happens. If the new push causes a new chunk to be created, a fee indicated by + /// [`VotingFee`] is deducted. + /// - if `origin` is already a voter: the index __must__ be valid and point to the correct + /// position of the `origin` in the current voters list. + /// + /// Note that any trailing `false` votes in `votes` is ignored; In approval voting, not voting for a candidate + /// and voting false, are equal. + fn set_approvals(origin, votes: Vec, #[compact] index: VoteIndex, hint: SetIndex) -> Result { let who = ensure_signed(origin)?; - Self::do_set_approvals(who, votes, index) + Self::do_set_approvals(who, votes, index, hint) } /// Set candidate approvals from a proxy. Approval slots stay valid as long as candidates in those slots /// are registered. - fn proxy_set_approvals(origin, votes: Vec, #[compact] index: VoteIndex) -> Result { + fn proxy_set_approvals(origin, votes: Vec, #[compact] index: VoteIndex, hint: SetIndex) -> Result { let who = >::proxy(ensure_signed(origin)?).ok_or("not a proxy")?; - Self::do_set_approvals(who, votes, index) + Self::do_set_approvals(who, votes, index, hint) } /// Remove a voter. For it not to be a bond-consuming no-op, all approved candidate indices /// must now be either unregistered or registered to a candidate that registered the slot after /// the voter gave their last approval set. /// + /// Both indices must be provided as explained in [`voter_at`] function. + /// /// May be called by anyone. Returns the voter deposit to `signed`. fn reap_inactive_voter( origin, @@ -132,34 +198,49 @@ decl_module! { #[compact] assumed_vote_index: VoteIndex ) { let reporter = ensure_signed(origin)?; - let who = T::Lookup::lookup(who)?; + ensure!(!Self::presentation_active(), "cannot reap during presentation period"); - ensure!(Self::voter_last_active(&reporter).is_some(), "reporter must be a voter"); - let last_active = Self::voter_last_active(&who).ok_or("target for inactivity cleanup must be active")?; + ensure!(Self::voter_info(&reporter).is_some(), "reporter must be a voter"); + + let info = Self::voter_info(&who).ok_or("target for inactivity cleanup must be active")?; + let last_active = info.last_active; + ensure!(assumed_vote_index == Self::vote_index(), "vote index not current"); - ensure!(assumed_vote_index > last_active + Self::inactivity_grace_period(), "cannot reap during grace period"); - let voters = Self::voters(); + ensure!( + assumed_vote_index > last_active+ Self::inactivity_grace_period(), + "cannot reap during grace period" + ); + let reporter_index = reporter_index as usize; let who_index = who_index as usize; - ensure!(reporter_index < voters.len() && voters[reporter_index] == reporter, "bad reporter index"); - ensure!(who_index < voters.len() && voters[who_index] == who, "bad target index"); + let assumed_reporter = Self::voter_at(reporter_index).ok_or("invalid reporter index")?; + let assumed_who = Self::voter_at(who_index).ok_or("invalid target index")?; - // will definitely kill one of signed or who now. + ensure!(assumed_reporter == reporter, "bad reporter index"); + ensure!(assumed_who == who, "bad target index"); - let valid = !Self::approvals_of(&who).iter() + // will definitely kill one of reporter or who now. + + let valid = !Self::all_approvals_of(&who).iter() .zip(Self::candidates().iter()) .any(|(&appr, addr)| appr && *addr != T::AccountId::default() && - Self::candidate_reg_info(addr).map_or(false, |x| x.0 <= last_active)/*defensive only: all items in candidates list are registered*/ + // defensive only: all items in candidates list are registered + Self::candidate_reg_info(addr).map_or(false, |x| x.0 <= last_active) ); Self::remove_voter( if valid { &who } else { &reporter }, - if valid { who_index } else { reporter_index }, - voters + if valid { who_index } else { reporter_index } ); + + T::Currency::remove_lock( + COUNCIL_SEATS_ID, + if valid { &who } else { &reporter } + ); + if valid { // This only fails if `reporter` doesn't exist, which it clearly must do since its the origin. // Still, it's no more harmful to propagate any error at this point. @@ -173,23 +254,32 @@ decl_module! { } /// Remove a voter. All votes are cancelled and the voter deposit is returned. + /// + /// The index must be provided as explained in [`voter_at`] function. + /// + /// Also removes the lock on the balance of the voter. See [`do_set_approvals()`]. fn retract_voter(origin, #[compact] index: u32) { let who = ensure_signed(origin)?; ensure!(!Self::presentation_active(), "cannot retract when presenting"); - ensure!(>::exists(&who), "cannot retract non-voter"); - let voters = Self::voters(); + ensure!(>::exists(&who), "cannot retract non-voter"); let index = index as usize; - ensure!(index < voters.len(), "retraction index invalid"); - ensure!(voters[index] == who, "retraction index mismatch"); + let voter = Self::voter_at(index).ok_or("retraction index invalid")?; + ensure!(voter == who, "retraction index mismatch"); - Self::remove_voter(&who, index, voters); + Self::remove_voter(&who, index); T::Currency::unreserve(&who, Self::voting_bond()); + T::Currency::remove_lock(COUNCIL_SEATS_ID, &who); } /// Submit oneself for candidacy. /// /// Account must have enough transferrable funds in it to pay the bond. + /// + /// NOTE: if `origin` has already assigned approvals via [`set_approvals`], + /// it will NOT have any usable funds to pass candidacy bond and must first retract. + /// Note that setting approvals will lock the entire balance of the voter until + /// retraction or being reported. fn submit_candidacy(origin, #[compact] slot: u32) { let who = ensure_signed(origin)?; @@ -218,7 +308,7 @@ decl_module! { } /// Claim that `signed` is one of the top Self::carry_count() + current_vote().1 candidates. - /// Only works if the `block_number >= current_vote().0` and `< current_vote().0 + presentation_duration()`` + /// Only works if the `block_number >= current_vote().0` and `< current_vote().0 + presentation_duration()` /// `signed` should have at least fn present_winner( origin, @@ -227,15 +317,21 @@ decl_module! { #[compact] index: VoteIndex ) -> Result { let who = ensure_signed(origin)?; - ensure!(!total.is_zero(), "stake deposited to present winner and be added to leaderboard should be non-zero"); + ensure!( + !total.is_zero(), + "stake deposited to present winner and be added to leaderboard should be non-zero" + ); let candidate = T::Lookup::lookup(candidate)?; ensure!(index == Self::vote_index(), "index not current"); let (_, _, expiring) = Self::next_finalize().ok_or("cannot present outside of presentation period")?; - let stakes = Self::snapshoted_stakes(); - let voters = Self::voters(); - let bad_presentation_punishment = Self::present_slash_per_voter() * BalanceOf::::from(voters.len() as u32); - ensure!(T::Currency::can_slash(&who, bad_presentation_punishment), "presenter must have sufficient slashable funds"); + let bad_presentation_punishment = + Self::present_slash_per_voter() + * BalanceOf::::from(Self::voter_count() as u32); + ensure!( + T::Currency::can_slash(&who, bad_presentation_punishment), + "presenter must have sufficient slashable funds" + ); let mut leaderboard = Self::leaderboard().ok_or("leaderboard must exist while present phase active")?; ensure!(total > leaderboard[0].0, "candidate not worthy of leaderboard"); @@ -244,17 +340,24 @@ decl_module! { ensure!(p < expiring.len(), "candidate must not form a duplicated member if elected"); } + let voters = Self::all_voters(); let (registered_since, candidate_index): (VoteIndex, u32) = Self::candidate_reg_info(&candidate).ok_or("presented candidate must be current")?; let actual_total = voters.iter() - .zip(stakes.iter()) - .filter_map(|(voter, stake)| - match Self::voter_last_active(voter) { - Some(b) if b >= registered_since => - Self::approvals_of(voter).get(candidate_index as usize) - .and_then(|approved| if *approved { Some(*stake) } else { None }), - _ => None, - }) + .filter_map(|maybe_voter| maybe_voter.as_ref()) + .filter_map(|voter| match Self::voter_info(voter) { + Some(b) if b.last_active >= registered_since => { + let last_win = b.last_win; + let now = Self::vote_index(); + let stake = b.stake; + let offset = Self::get_offset(stake, now - last_win); + let weight = stake + offset + b.pot; + if Self::approvals_of_at(voter, candidate_index as usize) { + Some(weight) + } else { None } + }, + _ => None, + }) .fold(Zero::zero(), |acc, n| acc + n); let dupe = leaderboard.iter().find(|&&(_, ref c)| c == &candidate).is_some(); if total == actual_total && !dupe { @@ -317,18 +420,20 @@ decl_module! { decl_storage! { trait Store for Module as Council { - // parameters + // ---- parameters /// How much should be locked up in order to submit one's candidacy. pub CandidacyBond get(candidacy_bond) config(): BalanceOf = 9.into(); /// How much should be locked up in order to be able to submit votes. pub VotingBond get(voting_bond) config(voter_bond): BalanceOf; + /// The amount of fee paid upon each vote submission, unless if they submit a _hole_ index and replace it. + pub VotingFee get(voting_fee) config(voting_fee): BalanceOf; /// The punishment, per voter, if you provide an invalid presentation. pub PresentSlashPerVoter get(present_slash_per_voter) config(): BalanceOf = 1.into(); /// How many runners-up should have their approvals persist until the next vote. pub CarryCount get(carry_count) config(): u32 = 2; /// How long to give each top candidate to present themselves after the vote ends. pub PresentationDuration get(presentation_duration) config(): T::BlockNumber = 1000.into(); - /// How many vote indexes need to go by after a target voter's last vote before they can be reaped if their + /// How many vote indices need to go by after a target voter's last vote before they can be reaped if their /// approvals are moot. pub InactiveGracePeriod get(inactivity_grace_period) config(inactive_grace_period): VoteIndex = 1; /// How often (in blocks) to check for new votes. @@ -337,37 +442,49 @@ decl_storage! { pub TermDuration get(term_duration) config(): T::BlockNumber = 5.into(); /// Number of accounts that should be sitting on the council. pub DesiredSeats get(desired_seats) config(): u32; + /// Decay factor of weight when being accumulated. It should typically be set to + /// __at least__ `council_size -1` to keep the council secure. + /// When set to `N`, it indicates `(1/N)^t` of staked is decayed at weight increment step `t`. + /// 0 will result in no weight being added at all (normal approval voting). + pub DecayRatio get(decay_ratio) config(decay_ratio): u32 = 24; - // permanent state (always relevant, changes only at the finalization of voting) + // ---- permanent state (always relevant, changes only at the finalization of voting) /// The current council. When there's a vote going on, this should still be used for executive /// matters. The block number (second element in the tuple) is the block that their position is /// active until (calculated by the sum of the block number when the council member was elected /// and their term duration). pub ActiveCouncil get(active_council) config(): Vec<(T::AccountId, T::BlockNumber)>; - /// The total number of votes that have happened or are in progress. + /// The total number of vote rounds that have happened or are in progress. pub VoteCount get(vote_index): VoteIndex; - // persistent state (always relevant, changes constantly) - /// A list of votes for each voter, respecting the last cleared vote index that this voter was - /// last active at. - pub ApprovalsOf get(approvals_of): map T::AccountId => Vec; + // ---- persistent state (always relevant, changes constantly) + /// A list of votes for each voter. The votes are stored as numeric values and parsed in a bit-wise manner. + /// + /// In order to get a human-readable representation (`Vec`), use [`all_approvals_of`]. + /// + /// Furthermore, each vector of scalars is chunked with the cap of `APPROVAL_SET_SIZE`. + pub ApprovalsOf get(approvals_of): map (T::AccountId, SetIndex) => Vec; /// The vote index and list slot that the candidate `who` was registered or `None` if they are not /// currently registered. pub RegisterInfoOf get(candidate_reg_info): map T::AccountId => Option<(VoteIndex, u32)>; - /// The last cleared vote index that this voter was last active at. - pub LastActiveOf get(voter_last_active): map T::AccountId => Option; - /// The present voter list. - pub Voters get(voters): Vec; + /// Basic information about a voter. + pub VoterInfoOf get(voter_info): map T::AccountId => Option>>; + /// The present voter list (chunked and capped at [`VOTER_SET_SIZE`]). + pub Voters get(voters): map SetIndex => Vec>; + /// the next free set to store a voter in. This will keep growing. + pub NextVoterSet get(next_nonfull_voter_set): SetIndex = 0; + /// Current number of Voters. + pub VoterCount get(voter_count): SetIndex = 0; /// The present candidate list. pub Candidates get(candidates): Vec; // has holes + /// Current number of active candidates pub CandidateCount get(candidate_count): u32; - // temporary state (only relevant during finalization/presentation) + // ---- temporary state (only relevant during finalization/presentation) /// The accounts holding the seats that will become free on the next tally. pub NextFinalize get(next_finalize): Option<(T::BlockNumber, u32, Vec)>; - /// The stakes as they were at the point that the vote ended. - pub SnapshotedStakes get(snapshoted_stakes): Vec>; - /// Get the leaderboard if we;re in the presentation phase. + /// Get the leaderboard if we're in the presentation phase. The first element is the weight of each entry; + /// It may be the direct summed approval stakes, or a weighted version of it. pub Leaderboard get(leaderboard): Option, T::AccountId)> >; // ORDERED low -> high } } @@ -460,15 +577,21 @@ impl Module { Ok(()) } - /// Remove a voter from the system. Trusts that Self::voters()[index] != voter. - fn remove_voter(voter: &T::AccountId, index: usize, mut voters: Vec) { - >::put({ voters.swap_remove(index); voters }); - >::remove(voter); - >::remove(voter); + /// Remove a voter at a specified index from the system. + fn remove_voter(voter: &T::AccountId, index: usize) { + let (set_index, vec_index) = Self::split_index(index, VOTER_SET_SIZE); + let mut set = Self::voters(set_index); + set[vec_index] = None; + >::insert(set_index, set); + >::mutate(|c| *c = *c - 1); + Self::remove_all_approvals_of(voter); + >::remove(voter); } - // Actually do the voting. - fn do_set_approvals(who: T::AccountId, votes: Vec, index: VoteIndex) -> Result { + /// Actually do the voting. + /// + /// The voter index must be provided as explained in [`voter_at`] function. + fn do_set_approvals(who: T::AccountId, votes: Vec, index: VoteIndex, hint: SetIndex) -> Result { let candidates = Self::candidates(); ensure!(!Self::presentation_active(), "no approval changes during presentation period"); @@ -477,39 +600,98 @@ impl Module { // Prevent a vote from voters that provide a list of votes that exceeds the candidates length // since otherwise an attacker may be able to submit a very long list of `votes` that far exceeds // the amount of candidates and waste more computation than a reasonable voting bond would cover. - ensure!(candidates.len() >= votes.len(), "amount of candidate approval votes cannot exceed amount of candidates"); + ensure!(candidates.len() >= votes.len(), "amount of candidate votes cannot exceed amount of candidates"); + + // Amount to be locked up. + let mut locked_balance = T::Currency::total_balance(&who); + let mut pot_to_set = Zero::zero(); + let hint = hint as usize; + + if let Some(info) = Self::voter_info(&who) { + // already a voter. Index must be valid. No fee. update pot. O(1) + let voter = Self::voter_at(hint).ok_or("invalid voter index")?; + ensure!(voter == who, "wrong voter index"); + + // write new accumulated offset. + let last_win = info.last_win; + let now = index; + let offset = Self::get_offset(info.stake, now - last_win); + pot_to_set = info.pot + offset; + } else { + // not yet a voter. Index _could be valid_. Fee might apply. Bond will be reserved O(1). + ensure!( + T::Currency::free_balance(&who) > Self::voting_bond(), + "new voter must have sufficient funds to pay the bond" + ); + + let (set_index, vec_index) = Self::split_index(hint, VOTER_SET_SIZE); + match Self::cell_status(set_index, vec_index) { + CellStatus::Hole => { + // requested cell was a valid hole. + >::mutate(set_index, |set| set[vec_index] = Some(who.clone())); + }, + CellStatus::Head | CellStatus::Occupied => { + // Either occupied or out-of-range. + let next = Self::next_nonfull_voter_set(); + let mut set = Self::voters(next); + // Caused a new set to be created. Pay for it. + // This is the last potential error. Writes will begin afterwards. + if set.is_empty() { + let imbalance = T::Currency::withdraw( + &who, + Self::voting_fee(), + WithdrawReason::Fee, + ExistenceRequirement::KeepAlive, + )?; + T::BadVoterIndex::on_unbalanced(imbalance); + // NOTE: this is safe since the `withdraw()` will check this. + locked_balance -= Self::voting_fee(); + } + Self::checked_push_voter(&mut set, who.clone(), next); + >::insert(next, set); + } + } - if !>::exists(&who) { - // not yet a voter - deduct bond. - // NOTE: this must be the last potential bailer, since it changes state. T::Currency::reserve(&who, Self::voting_bond())?; - - >::mutate(|v| v.push(who.clone())); + >::mutate(|c| *c = *c + 1); } - >::insert(&who, index); - >::insert(&who, votes); + + T::Currency::set_lock( + COUNCIL_SEATS_ID, + &who, + locked_balance, + T::BlockNumber::max_value(), + WithdrawReasons::all() + ); + + >::insert( + &who, + VoterInfo::> { + last_active: index, + last_win: index, + stake: locked_balance, + pot: pot_to_set, + } + ); + Self::set_approvals_chunked(&who, votes); Ok(()) } - /// Close the voting, snapshot the staking and the number of seats that are actually up for grabs. + /// Close the voting, record the number of seats that are actually up for grabs. fn start_tally() { let active_council = Self::active_council(); let desired_seats = Self::desired_seats() as usize; let number = >::block_number(); - let expiring = active_council.iter().take_while(|i| i.1 == number).map(|i| i.0.clone()).collect::>(); + let expiring = active_council.iter().take_while(|i| i.1 <= number).map(|i| i.0.clone()).collect::>(); let retaining_seats = active_council.len() - expiring.len(); if retaining_seats < desired_seats { let empty_seats = desired_seats - retaining_seats; >::put((number + Self::presentation_duration(), empty_seats as u32, expiring)); - let voters = Self::voters(); - let votes = voters.iter().map(T::Currency::total_balance).collect::>(); - >::put(votes); - // initialize leaderboard. let leaderboard_size = empty_seats + Self::carry_count() as usize; - >::put(vec![(BalanceOf::::zero(), T::AccountId::default()); leaderboard_size]); + >::put(vec![(Zero::zero(), T::AccountId::default()); leaderboard_size]); Self::deposit_event(RawEvent::TallyStarted(empty_seats as u32)); } @@ -520,7 +702,6 @@ impl Module { /// a new vote is started. /// Clears all presented candidates, returning the bond of the elected ones. fn finalize_tally() -> Result { - >::kill(); let (_, coming, expiring): (T::BlockNumber, u32, Vec) = >::take().ok_or("finalize can only be called after a tally is started.")?; let leaderboard: Vec<(BalanceOf, T::AccountId)> = >::take().unwrap_or_default(); @@ -534,8 +715,20 @@ impl Module { .take(coming as usize) .map(|(_, a)| a) .cloned() - .inspect(|a| {T::Currency::unreserve(a, candidacy_bond);}) + .inspect(|a| { T::Currency::unreserve(a, candidacy_bond); }) .collect(); + + // Update last win index for anyone voted for any of the incomings. + incoming.iter().filter_map(|i| Self::candidate_reg_info(i)).for_each(|r| { + let index = r.1 as usize; + Self::all_voters() + .iter() + .filter_map(|mv| mv.as_ref()) + .filter(|v| Self::approvals_of_at(*v, index)) + .for_each(|v| >::mutate(v, |a| { + if let Some(activity) = a { activity.last_win = Self::vote_index() + 1; } + })); + }); let active_council = Self::active_council(); let outgoing: Vec<_> = active_council.iter() .take(expiring.len()) @@ -583,6 +776,205 @@ impl Module { >::put(Self::vote_index() + 1); Ok(()) } + + fn checked_push_voter(set: &mut Vec>, who: T::AccountId, index: u32) { + let len = set.len(); + + // Defensive only: this should never happen. Don't push since it will break more things. + if len == VOTER_SET_SIZE { return; } + + set.push(Some(who)); + if len + 1 == VOTER_SET_SIZE { + >::put(index + 1); + } + } + + /// Get the set and vector index of a global voter index. + /// + /// Note that this function does not take holes into account. + /// See [`voter_at`]. + fn split_index(index: usize, scale: usize) -> (SetIndex, usize) { + let set_index = (index / scale) as u32; + let vec_index = index % scale; + (set_index, vec_index) + } + + /// Return a concatenated vector over all voter sets. + fn all_voters() -> Vec> { + let mut all = >::get(0); + let mut index = 1; + // NOTE: we could also use `Self::next_nonfull_voter_set()` here but that might change based + // on how we do chunking. This is more generic. + loop { + let next_set = >::get(index); + if next_set.is_empty() { + break; + } else { + index += 1; + all.extend(next_set); + } + } + all + } + + /// Shorthand for fetching a voter at a specific (global) index. + /// + /// NOTE: this function is used for checking indices. Yet, it does not take holes into account. + /// This means that any account submitting an index at any point in time should submit: + /// `VOTER_SET_SIZE * set_index + local_index`, meaning that you are ignoring all holes in the + /// first `set_index` sets. + fn voter_at(index: usize) -> Option { + let (set_index, vec_index) = Self::split_index(index, VOTER_SET_SIZE); + let set = Self::voters(set_index); + if vec_index < set.len() { + set[vec_index].clone() + } else { + None + } + } + + /// A more sophisticated version of `voter_at`. Will be kept separate as most often it is an overdue + /// compared to `voter_at`. Only used when setting approvals. + fn cell_status(set_index: SetIndex, vec_index: usize) -> CellStatus { + let set = Self::voters(set_index); + if vec_index < set.len() { + if let Some(_) = set[vec_index] { + CellStatus::Occupied + } else { + CellStatus::Hole + } + } else { + CellStatus::Head + } + } + + /// Sets the approval of a voter in a chunked manner. + fn set_approvals_chunked(who: &T::AccountId, approvals: Vec) { + let approvals_flag_vec = Self::bool_to_flag(approvals); + approvals_flag_vec + .chunks(APPROVAL_SET_SIZE) + .enumerate() + .for_each(|(index, slice)| >::insert((who.clone(), index as SetIndex), slice.to_vec())); + } + + /// shorthand for fetching a specific approval of a voter at a specific (global) index. + /// + /// Using this function to read a vote is preferred as it reads `APPROVAL_SET_SIZE` items of type + /// `ApprovalFlag` from storage at most; not all of them. + /// + /// Note that false is returned in case of no-vote or an explicit `false`. + fn approvals_of_at(who: &T::AccountId, index: usize) -> bool { + let (flag_index, bit) = Self::split_index(index, APPROVAL_FLAG_LEN); + let (set_index, vec_index) = Self::split_index(flag_index as usize, APPROVAL_SET_SIZE); + let set = Self::approvals_of((who.clone(), set_index)); + if vec_index < set.len() { + // This is because bit_at treats numbers in lsb -> msb order. + let reversed_index = set.len() - 1 - vec_index; + Self::bit_at(set[reversed_index], bit) + } else { + false + } + } + + /// Return true of the bit `n` of scalar `x` is set to `1` and false otherwise. + fn bit_at(x: ApprovalFlag, n: usize) -> bool { + if n < APPROVAL_FLAG_LEN { + // x & ( APPROVAL_FLAG_MASK >> n ) != 0 + x & ( 1 << n ) != 0 + } else { + false + } + } + + /// Convert a vec of boolean approval flags to a vec of integers, as denoted by + /// the type `ApprovalFlag`. see `bool_to_flag_should_work` test for examples. + pub fn bool_to_flag(x: Vec) -> Vec { + let mut result: Vec = Vec::with_capacity(x.len() / APPROVAL_FLAG_LEN); + if x.is_empty() { + return result; + } + result.push(0); + let mut index = 0; + let mut counter = 0; + loop { + let shl_index = counter % APPROVAL_FLAG_LEN; + result[index] += (if x[counter] { 1 } else { 0 }) << shl_index; + counter += 1; + if counter > x.len() - 1 { break; } + if counter % APPROVAL_FLAG_LEN == 0 { + result.push(0); + index += 1; + } + } + result + } + + /// Convert a vec of flags (u32) to boolean. + pub fn flag_to_bool(chunk: Vec) -> Vec { + let mut result = Vec::with_capacity(chunk.len()); + if chunk.is_empty() { return vec![] } + chunk.into_iter() + .map(|num| (0..APPROVAL_FLAG_LEN).map(|bit| Self::bit_at(num, bit)).collect::>()) + .for_each(|c| { + let last_approve = match c.iter().rposition(|n| *n) { + Some(index) => index + 1, + None => 0 + }; + result.extend(c.into_iter().take(last_approve)); + }); + result + } + + /// Return a concatenated vector over all approvals of a voter as boolean. + /// The trailing zeros are removed. + fn all_approvals_of(who: &T::AccountId) -> Vec { + let mut all: Vec = vec![]; + let mut index = 0_u32; + loop { + let chunk = Self::approvals_of((who.clone(), index)); + if chunk.is_empty() { break; } + all.extend(Self::flag_to_bool(chunk)); + index += 1; + } + all + } + + /// Remove all approvals associated with one account. + fn remove_all_approvals_of(who: &T::AccountId) { + let mut index = 0; + loop { + let set = Self::approvals_of((who.clone(), index)); + if set.len() > 0 { + >::remove((who.clone(), index)); + index += 1; + } else { + break + } + } + } + + /// Calculates the offset value (stored pot) of a stake, based on the distance + /// to the last win_index, `t`. Regardless of the internal implementation, + /// it should always be used with the following structure: + /// + /// Given Stake of voter `V` being `x` and distance to last_win index `t`, the new weight + /// of `V` is `x + get_offset(x, t)`. + /// + /// In other words, this function returns everything extra that should be added + /// to a voter's stake value to get the correct weight. Indeed, zero is + /// returned if `t` is zero. + fn get_offset(stake: BalanceOf, t: VoteIndex) -> BalanceOf { + let decay_ratio: BalanceOf = Self::decay_ratio().into(); + if t > 150 { return stake * decay_ratio } + let mut offset = stake; + let mut r = Zero::zero(); + let decay = decay_ratio + One::one(); + for _ in 0..t { + offset = offset.saturating_sub(offset / decay); + r += offset + } + r + } } #[cfg(test)] @@ -591,16 +983,86 @@ mod tests { use crate::tests::*; use srml_support::{assert_ok, assert_noop, assert_err}; + fn voter_ids() -> Vec { + Council::all_voters().iter().map(|v| v.unwrap_or(0) ).collect::>() + } + + fn vote(i: u64, l: usize) { + let _ = Balances::make_free_balance_be(&i, 20); + assert_ok!(Council::set_approvals(Origin::signed(i), (0..l).map(|_| true).collect::>(), 0, 0)); + } + + fn vote_at(i: u64, l: usize, index: VoteIndex) { + let _ = Balances::make_free_balance_be(&i, 20); + assert_ok!(Council::set_approvals(Origin::signed(i), (0..l).map(|_| true).collect::>(), 0, index)); + } + + fn create_candidate(i: u64, index: u32) { + let _ = Balances::make_free_balance_be(&i, 20); + assert_ok!(Council::submit_candidacy(Origin::signed(i), index)); + } + + fn bond() -> u64 { + Council::voting_bond() + } + + + #[test] + fn bool_to_flag_should_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(Council::bool_to_flag(vec![]), vec![]); + assert_eq!(Council::bool_to_flag(vec![false]), vec![0]); + assert_eq!(Council::bool_to_flag(vec![true]), vec![1]); + assert_eq!(Council::bool_to_flag(vec![true, true, true, true]), vec![15]); + assert_eq!(Council::bool_to_flag(vec![true, true, true, true, true]), vec![15 + 16]); + + let set_1 = vec![ + true, false, false, false, // 0x1 + false, true, true, true, // 0xE + ]; + assert_eq!( + Council::bool_to_flag(set_1.clone()), + vec![0x00_00_00_E1_u32] + ); + assert_eq!( + Council::flag_to_bool(vec![0x00_00_00_E1_u32]), + set_1 + ); + + let set_2 = vec![ + false, false, false, true, // 0x8 + false, true, false, true, // 0xA + ]; + assert_eq!( + Council::bool_to_flag(set_2.clone()), + vec![0x00_00_00_A8_u32] + ); + assert_eq!( + Council::flag_to_bool(vec![0x00_00_00_A8_u32]), + set_2 + ); + + let mut rhs = (0..100/APPROVAL_FLAG_LEN).map(|_| 0xFFFFFFFF_u32).collect::>(); + // NOTE: this might be need change based on `APPROVAL_FLAG_LEN`. + rhs.extend(vec![0x00_00_00_0F]); + assert_eq!( + Council::bool_to_flag((0..100).map(|_| true).collect()), + rhs + ) + }) + } + #[test] fn params_should_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_eq!(Council::next_vote_from(1), 4); assert_eq!(Council::next_vote_from(4), 4); assert_eq!(Council::next_vote_from(5), 8); assert_eq!(Council::vote_index(), 0); - assert_eq!(Council::candidacy_bond(), 9); - assert_eq!(Council::voting_bond(), 3); + assert_eq!(Council::candidacy_bond(), 3); + assert_eq!(Council::voting_bond(), 0); + assert_eq!(Council::voting_fee(), 0); assert_eq!(Council::present_slash_per_voter(), 1); assert_eq!(Council::presentation_duration(), 2); assert_eq!(Council::inactivity_grace_period(), 1); @@ -618,15 +1080,225 @@ mod tests { assert_eq!(Council::is_a_candidate(&1), false); assert_eq!(Council::candidate_reg_info(1), None); - assert_eq!(Council::voters(), Vec::::new()); - assert_eq!(Council::voter_last_active(1), None); - assert_eq!(Council::approvals_of(1), vec![]); + assert_eq!(Council::voters(0), Vec::>::new()); + assert_eq!(Council::voter_info(1), None); + assert_eq!(Council::all_approvals_of(&1), vec![]); }); } + #[test] + fn voter_set_growth_should_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(3), 1)); + + // create 65. 64 (set0) + 1 (set1) + (1..=63).for_each(|i| vote(i, 2)); + assert_eq!(Council::next_nonfull_voter_set(), 0); + vote(64, 2); + assert_eq!(Council::next_nonfull_voter_set(), 1); + vote(65, 2); + + let set1 = Council::voters(0); + let set2 = Council::voters(1); + + assert_eq!(set1.len(), 64); + assert_eq!(set2.len(), 1); + + assert_eq!(set1[0], Some(1)); + assert_eq!(set1[10], Some(11)); + assert_eq!(set2[0], Some(65)); + }) + } + + #[test] + fn voter_set_reclaim_should_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(3), 1)); + + (1..=129).for_each(|i| vote(i, 2)); + assert_eq!(Council::next_nonfull_voter_set(), 2); + + assert_ok!(Council::retract_voter(Origin::signed(11), 10)); + + assert_ok!(Council::retract_voter(Origin::signed(66), 65)); + assert_ok!(Council::retract_voter(Origin::signed(67), 66)); + + // length does not show it but holes do exist. + assert_eq!(Council::voters(0).len(), 64); + assert_eq!(Council::voters(1).len(), 64); + assert_eq!(Council::voters(2).len(), 1); + + assert_eq!(Council::voters(0)[10], None); + assert_eq!(Council::voters(1)[1], None); + assert_eq!(Council::voters(1)[2], None); + // Next set with capacity is 2. + assert_eq!(Council::next_nonfull_voter_set(), 2); + + // But we can fill a hole. + vote_at(130, 2, 10); + + // Nothing added to set 2. A hole was filled. + assert_eq!(Council::voters(0).len(), 64); + assert_eq!(Council::voters(1).len(), 64); + assert_eq!(Council::voters(2).len(), 1); + + // and the next two (scheduled) to the second set. + assert_eq!(Council::next_nonfull_voter_set(), 2); + }) + } + + #[test] + fn approvals_set_growth_should_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + // create candidates and voters. + (1..=250).for_each(|i| create_candidate(i, (i-1) as u32)); + (1..=250).for_each(|i| vote(i, i as usize)); + + // all approvals of should return the exact expected vector. + assert_eq!(Council::all_approvals_of(&180), (0..180).map(|_| true).collect::>()); + + assert_eq!(Council::all_approvals_of(&32), (0..32).map(|_| true).collect::>()); + assert_eq!(Council::all_approvals_of(&8), (0..8).map(|_| true).collect::>()); + assert_eq!(Council::all_approvals_of(&64), (0..64).map(|_| true).collect::>()); + assert_eq!(Council::all_approvals_of(&65), (0..65).map(|_| true).collect::>()); + assert_eq!(Council::all_approvals_of(&63), (0..63).map(|_| true).collect::>()); + + // NOTE: assuming that APPROVAL_SET_SIZE is more or less small-ish. Might fail otherwise. + let full_sets = (180 / APPROVAL_FLAG_LEN) / APPROVAL_SET_SIZE; + let left_over = (180 / APPROVAL_FLAG_LEN) / APPROVAL_SET_SIZE; + let rem = 180 % APPROVAL_FLAG_LEN; + + // grab and check the last full set, if it exists. + if full_sets > 0 { + assert_eq!( + Council::approvals_of((180, (full_sets-1) as SetIndex )), + Council::bool_to_flag((0..APPROVAL_SET_SIZE * APPROVAL_FLAG_LEN).map(|_| true).collect::>()) + ); + } + + // grab and check the last, half-empty, set. + if left_over > 0 { + assert_eq!( + Council::approvals_of((180, full_sets as SetIndex)), + Council::bool_to_flag((0..left_over * APPROVAL_FLAG_LEN + rem).map(|_| true).collect::>()) + ); + } + }) + } + + + #[test] + fn cell_status_works() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(3), 1)); + + (1..=63).for_each(|i| vote(i, 2)); + + assert_ok!(Council::retract_voter(Origin::signed(11), 10)); + assert_ok!(Council::retract_voter(Origin::signed(21), 20)); + + assert_eq!(Council::cell_status(0, 10), CellStatus::Hole); + assert_eq!(Council::cell_status(0, 0), CellStatus::Occupied); + assert_eq!(Council::cell_status(0, 20), CellStatus::Hole); + assert_eq!(Council::cell_status(0, 63), CellStatus::Head); + assert_eq!(Council::cell_status(1, 0), CellStatus::Head); + assert_eq!(Council::cell_status(1, 10), CellStatus::Head); + }) + } + + #[test] + fn initial_set_approvals_ignores_voter_index() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); + + // Last argument is essentially irrelevant. You might get or miss a tip. + assert_ok!(Council::set_approvals(Origin::signed(3), vec![true], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![true], 0, 5)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![true], 0, 100)); + + // indices are more or less ignored. all is pushed. + assert_eq!(voter_ids(), vec![3, 4, 5]); + }) + } + + #[test] + fn bad_approval_index_slashes_voters_and_bond_reduces_stake() { + with_externalities(&mut ExtBuilder::default().voting_fee(5).voter_bond(2).build(), || { + assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(3), 1)); + + (1..=63).for_each(|i| vote(i, 2)); + assert_eq!(Balances::free_balance(&1), 20 - 5 - 2); // -5 fee -2 bond + assert_eq!(Balances::free_balance(&10), 20 - 2); + assert_eq!(Balances::free_balance(&60), 20 - 2); + + // still no fee + vote(64, 2); + assert_eq!(Balances::free_balance(&64), 20 - 2); // -2 bond + assert_eq!( + Council::voter_info(&64).unwrap(), + VoterInfo { last_win: 0, last_active: 0, stake: 20, pot:0 } + ); + + assert_eq!(Council::next_nonfull_voter_set(), 1); + + // now we charge the next voter. + vote(65, 2); + assert_eq!(Balances::free_balance(&65), 20 - 5 - 2); + assert_eq!( + Council::voter_info(&65).unwrap(), + VoterInfo { last_win: 0, last_active: 0, stake: 15, pot:0 } + ); + }); + } + + #[test] + fn subsequent_set_approvals_checks_voter_index() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); + + assert_ok!(Council::set_approvals(Origin::signed(3), vec![true], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![true], 0, 5)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![true], 0, 100)); + + // invalid index + assert_noop!(Council::set_approvals(Origin::signed(4), vec![true], 0, 5), "invalid voter index"); + // wrong index + assert_noop!(Council::set_approvals(Origin::signed(4), vec![true], 0, 0), "wrong voter index"); + // correct + assert_ok!(Council::set_approvals(Origin::signed(4), vec![true], 0, 1)); + }) + } + + #[test] + fn voter_index_does_not_take_holes_into_account() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(3), 1)); + + // create 65. 64 (set0) + 1 (set1) + (1..=65).for_each(|i| vote(i, 2)); + + // account 65 has global index 65. + assert_eq!(Council::voter_at(64).unwrap(), 65); + + assert_ok!(Council::retract_voter(Origin::signed(1), 0)); + assert_ok!(Council::retract_voter(Origin::signed(2), 1)); + + // still the same. These holes are in some other set. + assert_eq!(Council::voter_at(64).unwrap(), 65); + // proof: can submit a new approval with the old index. + assert_noop!(Council::set_approvals(Origin::signed(65), vec![false, true], 0, 64 - 2), "wrong voter index"); + assert_ok!(Council::set_approvals(Origin::signed(65), vec![false, true], 0, 64)); + }) + } + #[test] fn simple_candidate_submission_should_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_eq!(Council::candidates(), Vec::::new()); assert_eq!(Council::candidate_reg_info(1), None); @@ -651,7 +1323,7 @@ mod tests { } fn new_test_ext_with_candidate_holes() -> runtime_io::TestExternalities { - let mut t = new_test_ext(false); + let mut t = ExtBuilder::default().build(); with_externalities(&mut t, || { >::put(vec![0, 0, 1]); >::put(1); @@ -694,7 +1366,9 @@ mod tests { #[test] fn candidate_submission_not_using_free_slot_should_not_work() { - with_externalities(&mut new_test_ext_with_candidate_holes(), || { + let mut t = new_test_ext_with_candidate_holes(); + + with_externalities(&mut t, || { System::set_block_number(1); assert_noop!(Council::submit_candidacy(Origin::signed(4), 3), "invalid candidate slot"); }); @@ -702,7 +1376,7 @@ mod tests { #[test] fn bad_candidate_slot_submission_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_eq!(Council::candidates(), Vec::::new()); assert_noop!(Council::submit_candidacy(Origin::signed(1), 1), "invalid candidate slot"); @@ -711,7 +1385,7 @@ mod tests { #[test] fn non_free_candidate_slot_submission_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_eq!(Council::candidates(), Vec::::new()); assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); @@ -722,7 +1396,7 @@ mod tests { #[test] fn dupe_candidate_submission_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_eq!(Council::candidates(), Vec::::new()); assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); @@ -733,45 +1407,367 @@ mod tests { #[test] fn poor_candidate_submission_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_eq!(Council::candidates(), Vec::::new()); assert_noop!(Council::submit_candidacy(Origin::signed(7), 0), "candidate has not enough funds"); }); } + #[test] + fn balance_should_lock_to_the_maximum() { + with_externalities(&mut ExtBuilder::default().build(), || { + System::set_block_number(1); + assert_eq!(Council::candidates(), Vec::::new()); + assert_eq!(Balances::free_balance(&2), 20); + + assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0, 0)); + + assert_eq!(Balances::free_balance(&2), 20 - bond() ); + assert_noop!(Balances::reserve(&2, 1), "account liquidity restrictions prevent withdrawal"); // locked. + + // deposit a bit more. + let _ = Balances::deposit_creating(&2, 100); + assert_ok!(Balances::reserve(&2, 1)); // locked but now has enough. + + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0, 0)); + assert_noop!(Balances::reserve(&2, 1), "account liquidity restrictions prevent withdrawal"); // locked. + assert_eq!(Balances::locks(&2).len(), 1); + assert_eq!(Balances::locks(&2)[0].amount, 100 + 20); + + assert_ok!(Council::retract_voter(Origin::signed(2), 0)); + + assert_eq!(Balances::locks(&2).len(), 0); + assert_eq!(Balances::free_balance(&2), 120 - 1); // 1 ok call to .reserve() happened. + assert_ok!(Balances::reserve(&2, 1)); // unlocked. + }); + } + + #[test] + fn balance_should_lock_on_submit_approvals_unlock_on_retract() { + with_externalities(&mut ExtBuilder::default().voter_bond(8).voting_fee(0).build(), || { + System::set_block_number(1); + assert_eq!(Council::candidates(), Vec::::new()); + assert_eq!(Balances::free_balance(&2), 20); + + assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0, 0)); + + assert_eq!(Balances::free_balance(&2), 12); // 20 - 8 (bond) + assert_noop!(Balances::reserve(&2, 10), "account liquidity restrictions prevent withdrawal"); // locked. + + assert_ok!(Council::retract_voter(Origin::signed(2), 0)); + + assert_eq!(Balances::free_balance(&2), 20); + assert_ok!(Balances::reserve(&2, 10)); // unlocked. + }); + } + + #[test] + fn accumulating_weight_and_decaying_should_work() { + with_externalities(&mut ExtBuilder::default().balance_factor(10).build(), || { + System::set_block_number(4); + assert!(!Council::presentation_active()); + + assert_ok!(Council::submit_candidacy(Origin::signed(6), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(5), 1)); + assert_ok!(Council::submit_candidacy(Origin::signed(1), 2)); + + assert_ok!(Council::set_approvals(Origin::signed(6), vec![true, false, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(1), vec![false, false, true], 0, 0)); + + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(6); + assert!(Council::presentation_active()); + + assert_eq!(Council::present_winner(Origin::signed(6), 6, 600, 0), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(5), 5, 500, 0), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(1), 1, 100, 0), Ok(())); + assert_eq!(Council::leaderboard(), Some(vec![(0, 0), (100, 1), (500, 5), (600, 6)])); + assert_ok!(Council::end_block(System::block_number())); + + assert_eq!(Council::active_council(), vec![(6, 11), (5, 11)]); + assert_eq!(Council::voter_info(6).unwrap(), VoterInfo { last_win: 1, last_active: 0, stake: 600, pot: 0}); + assert_eq!(Council::voter_info(5).unwrap(), VoterInfo { last_win: 1, last_active: 0, stake: 500, pot: 0}); + assert_eq!(Council::voter_info(1).unwrap(), VoterInfo { last_win: 0, last_active: 0, stake: 100, pot: 0}); + + System::set_block_number(12); + // retract needed to unlock approval funds => submit candidacy again. + assert_ok!(Council::retract_voter(Origin::signed(6), 0)); + assert_ok!(Council::retract_voter(Origin::signed(5), 1)); + assert_ok!(Council::submit_candidacy(Origin::signed(6), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(5), 1)); + assert_ok!(Council::set_approvals(Origin::signed(6), vec![true, false, false], 1, 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true, false], 1, 1)); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(14); + assert!(Council::presentation_active()); + assert_eq!(Council::present_winner(Origin::signed(6), 6, 600, 1), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(5), 5, 500, 1), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(1), 1, 100 + Council::get_offset(100, 1), 1), Ok(())); + assert_eq!(Council::leaderboard(), Some(vec![(0, 0), (100 + 96, 1), (500, 5), (600, 6)])); + assert_ok!(Council::end_block(System::block_number())); + + assert_eq!(Council::active_council(), vec![(6, 19), (5, 19)]); + assert_eq!( + Council::voter_info(6).unwrap(), + VoterInfo { last_win: 2, last_active: 1, stake: 600, pot:0 } + ); + assert_eq!(Council::voter_info(5).unwrap(), VoterInfo { last_win: 2, last_active: 1, stake: 500, pot:0 }); + assert_eq!(Council::voter_info(1).unwrap(), VoterInfo { last_win: 0, last_active: 0, stake: 100, pot:0 }); + + System::set_block_number(20); + assert_ok!(Council::retract_voter(Origin::signed(6), 0)); + assert_ok!(Council::retract_voter(Origin::signed(5), 1)); + assert_ok!(Council::submit_candidacy(Origin::signed(6), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(5), 1)); + assert_ok!(Council::set_approvals(Origin::signed(6), vec![true, false, false], 2, 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true, false], 2, 1)); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(22); + assert!(Council::presentation_active()); + assert_eq!(Council::present_winner(Origin::signed(6), 6, 600, 2), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(5), 5, 500, 2), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(1), 1, 100 + Council::get_offset(100, 2), 2), Ok(())); + assert_eq!(Council::leaderboard(), Some(vec![(0, 0), (100 + 96 + 93, 1), (500, 5), (600, 6)])); + assert_ok!(Council::end_block(System::block_number())); + + assert_eq!(Council::active_council(), vec![(6, 27), (5, 27)]); + assert_eq!( + Council::voter_info(6).unwrap(), + VoterInfo { last_win: 3, last_active: 2, stake: 600, pot: 0} + ); + assert_eq!(Council::voter_info(5).unwrap(), VoterInfo { last_win: 3, last_active: 2, stake: 500, pot: 0}); + assert_eq!(Council::voter_info(1).unwrap(), VoterInfo { last_win: 0, last_active: 0, stake: 100, pot: 0}); + + + System::set_block_number(28); + assert_ok!(Council::retract_voter(Origin::signed(6), 0)); + assert_ok!(Council::retract_voter(Origin::signed(5), 1)); + assert_ok!(Council::submit_candidacy(Origin::signed(6), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(5), 1)); + assert_ok!(Council::set_approvals(Origin::signed(6), vec![true, false, false], 3, 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true, false], 3, 1)); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(30); + assert!(Council::presentation_active()); + assert_eq!(Council::present_winner(Origin::signed(6), 6, 600, 3), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(5), 5, 500, 3), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(1), 1, 100 + Council::get_offset(100, 3), 3), Ok(())); + assert_eq!(Council::leaderboard(), Some(vec![(0, 0), (100 + 96 + 93 + 90, 1), (500, 5), (600, 6)])); + assert_ok!(Council::end_block(System::block_number())); + + assert_eq!(Council::active_council(), vec![(6, 35), (5, 35)]); + assert_eq!( + Council::voter_info(6).unwrap(), + VoterInfo { last_win: 4, last_active: 3, stake: 600, pot: 0} + ); + assert_eq!(Council::voter_info(5).unwrap(), VoterInfo { last_win: 4, last_active: 3, stake: 500, pot: 0}); + assert_eq!(Council::voter_info(1).unwrap(), VoterInfo { last_win: 0, last_active: 0, stake: 100, pot: 0}); + }) + } + + #[test] + fn winning_resets_accumulated_pot() { + with_externalities(&mut ExtBuilder::default().balance_factor(10).build(), || { + System::set_block_number(4); + assert!(!Council::presentation_active()); + + assert_ok!(Council::submit_candidacy(Origin::signed(6), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(4), 1)); + assert_ok!(Council::submit_candidacy(Origin::signed(3), 2)); + assert_ok!(Council::submit_candidacy(Origin::signed(2), 3)); + + assert_ok!(Council::set_approvals(Origin::signed(6), vec![true, false, false, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, true, false, false], 0, 1)); + assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, false, true, true], 0, 2)); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(6); + assert!(Council::presentation_active()); + assert_eq!(Council::present_winner(Origin::signed(6), 6, 600, 0), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(4), 4, 400, 0), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(3), 3, 300, 0), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(2), 2, 300, 0), Ok(())); + assert_eq!(Council::leaderboard(), Some(vec![(300, 2), (300, 3), (400, 4), (600, 6)])); + assert_ok!(Council::end_block(System::block_number())); + + assert_eq!(Council::active_council(), vec![(6, 11), (4, 11)]); + + System::set_block_number(12); + assert_ok!(Council::retract_voter(Origin::signed(6), 0)); + assert_ok!(Council::retract_voter(Origin::signed(4), 1)); + assert_ok!(Council::submit_candidacy(Origin::signed(6), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(4), 1)); + assert_ok!(Council::set_approvals(Origin::signed(6), vec![true, false, false, false], 1, 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, true, false, false], 1, 1)); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(14); + assert!(Council::presentation_active()); + assert_eq!(Council::present_winner(Origin::signed(6), 6, 600, 1), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(4), 4, 400, 1), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(3), 3, 300 + Council::get_offset(300, 1), 1), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(2), 2, 300 + Council::get_offset(300, 1), 1), Ok(())); + assert_eq!(Council::leaderboard(), Some(vec![(400, 4), (588, 2), (588, 3), (600, 6)])); + assert_ok!(Council::end_block(System::block_number())); + + assert_eq!(Council::active_council(), vec![(6, 19), (3, 19)]); + + System::set_block_number(20); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(22); + // 2 will not get re-elected with 300 + 288, instead just 300. + // because one of 3's candidates (3) won in previous round + // 4 on the other hand will get extra weight since it was unlucky. + assert_eq!(Council::present_winner(Origin::signed(3), 2, 300, 2), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(4), 4, 400 + Council::get_offset(400, 1), 2), Ok(())); + assert_ok!(Council::end_block(System::block_number())); + + assert_eq!(Council::active_council(), vec![(4, 27), (2, 27)]); + }) + } + + #[test] + fn resubmitting_approvals_stores_pot() { + with_externalities(&mut ExtBuilder::default() + .voter_bond(0) + .voting_fee(0) + .balance_factor(10) + .build(), + || { System::set_block_number(4); + assert!(!Council::presentation_active()); + + assert_ok!(Council::submit_candidacy(Origin::signed(6), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(5), 1)); + assert_ok!(Council::submit_candidacy(Origin::signed(1), 2)); + + assert_ok!(Council::set_approvals(Origin::signed(6), vec![true, false, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true, false], 0, 1)); + assert_ok!(Council::set_approvals(Origin::signed(1), vec![false, false, true], 0, 2)); + + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(6); + assert!(Council::presentation_active()); + + assert_eq!(Council::present_winner(Origin::signed(6), 6, 600, 0), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(5), 5, 500, 0), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(1), 1, 100, 0), Ok(())); + assert_eq!(Council::leaderboard(), Some(vec![(0, 0), (100, 1), (500, 5), (600, 6)])); + assert_ok!(Council::end_block(System::block_number())); + + assert_eq!(Council::active_council(), vec![(6, 11), (5, 11)]); + + System::set_block_number(12); + assert_ok!(Council::retract_voter(Origin::signed(6), 0)); + assert_ok!(Council::retract_voter(Origin::signed(5), 1)); + assert_ok!(Council::submit_candidacy(Origin::signed(6), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(5), 1)); + assert_ok!(Council::set_approvals(Origin::signed(6), vec![true, false, false], 1, 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true, false], 1, 1)); + // give 1 some new high balance + let _ = Balances::make_free_balance_be(&1, 997); + assert_ok!(Council::set_approvals(Origin::signed(1), vec![false, false, true], 1, 2)); + assert_eq!(Council::voter_info(1).unwrap(), + VoterInfo { + stake: 1000, // 997 + 3 which is candidacy bond. + pot: Council::get_offset(100, 1), + last_active: 1, + last_win: 1, + } + ); + assert_ok!(Council::end_block(System::block_number())); + + assert_eq!(Council::active_council(), vec![(6, 11), (5, 11)]); + + System::set_block_number(14); + assert!(Council::presentation_active()); + assert_eq!(Council::present_winner(Origin::signed(6), 6, 600, 1), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(5), 5, 500, 1), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(1), 1, 1000 + 96 /* pot */, 1), Ok(())); + assert_eq!(Council::leaderboard(), Some(vec![(0, 0), (500, 5), (600, 6), (1096, 1)])); + assert_ok!(Council::end_block(System::block_number())); + + assert_eq!(Council::active_council(), vec![(1, 19), (6, 19)]); + }) + } + + #[test] + fn get_offset_should_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(Council::get_offset(100, 0), 0); + assert_eq!(Council::get_offset(100, 1), 96); + assert_eq!(Council::get_offset(100, 2), 96 + 93); + assert_eq!(Council::get_offset(100, 3), 96 + 93 + 90); + assert_eq!(Council::get_offset(100, 4), 96 + 93 + 90 + 87); + // limit + assert_eq!(Council::get_offset(100, 1000), 100 * 24); + + assert_eq!(Council::get_offset(50_000_000_000, 0), 0); + assert_eq!(Council::get_offset(50_000_000_000, 1), 48_000_000_000); + assert_eq!(Council::get_offset(50_000_000_000, 2), 48_000_000_000 + 46_080_000_000); + assert_eq!(Council::get_offset(50_000_000_000, 3), 48_000_000_000 + 46_080_000_000 + 44_236_800_000); + assert_eq!( + Council::get_offset(50_000_000_000, 4), + 48_000_000_000 + 46_080_000_000 + 44_236_800_000 + 42_467_328_000 + ); + // limit + assert_eq!(Council::get_offset(50_000_000_000, 1000), 50_000_000_000 * 24); + }) + } + + #[test] + fn get_offset_with_zero_decay() { + with_externalities(&mut ExtBuilder::default().decay_ratio(0).build(), || { + assert_eq!(Council::get_offset(100, 0), 0); + assert_eq!(Council::get_offset(100, 1), 0); + assert_eq!(Council::get_offset(100, 2), 0); + assert_eq!(Council::get_offset(100, 3), 0); + // limit + assert_eq!(Council::get_offset(100, 1000), 0); + }) + } + #[test] fn voting_should_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); - assert_ok!(Council::set_approvals(Origin::signed(1), vec![true], 0)); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(1), vec![true], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![true], 0, 1)); - assert_eq!(Council::approvals_of(1), vec![true]); - assert_eq!(Council::approvals_of(4), vec![true]); - assert_eq!(Council::voters(), vec![1, 4]); + assert_eq!(Council::all_approvals_of(&1), vec![true]); + assert_eq!(Council::all_approvals_of(&4), vec![true]); + assert_eq!(voter_ids(), vec![1, 4]); assert_ok!(Council::submit_candidacy(Origin::signed(2), 1)); assert_ok!(Council::submit_candidacy(Origin::signed(3), 2)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true, true], 0)); - assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, true, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true, true], 0, 2)); + assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, true, true], 0, 3)); - assert_eq!(Council::approvals_of(1), vec![true]); - assert_eq!(Council::approvals_of(4), vec![true]); - assert_eq!(Council::approvals_of(2), vec![false, true, true]); - assert_eq!(Council::approvals_of(3), vec![false, true, true]); + assert_eq!(Council::all_approvals_of(&1), vec![true]); + assert_eq!(Council::all_approvals_of(&4), vec![true]); + assert_eq!(Council::all_approvals_of(&2), vec![false, true, true]); + assert_eq!(Council::all_approvals_of(&3), vec![false, true, true]); - assert_eq!(Council::voters(), vec![1, 4, 2, 3]); + assert_eq!(voter_ids(), vec![1, 4, 2, 3]); }); } #[test] fn proxy_voting_should_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); @@ -780,74 +1776,79 @@ mod tests { Democracy::force_proxy(2, 12); Democracy::force_proxy(3, 13); Democracy::force_proxy(4, 14); + assert_ok!(Council::proxy_set_approvals(Origin::signed(11), vec![true], 0, 0)); + assert_ok!(Council::proxy_set_approvals(Origin::signed(14), vec![true], 0, 1)); - assert_ok!(Council::proxy_set_approvals(Origin::signed(11), vec![true], 0)); - assert_ok!(Council::proxy_set_approvals(Origin::signed(14), vec![true], 0)); - - assert_eq!(Council::approvals_of(1), vec![true]); - assert_eq!(Council::approvals_of(4), vec![true]); - assert_eq!(Council::voters(), vec![1, 4]); + assert_eq!(Council::all_approvals_of(&1), vec![true]); + assert_eq!(Council::all_approvals_of(&4), vec![true]); + assert_eq!(voter_ids(), vec![1, 4]); assert_ok!(Council::submit_candidacy(Origin::signed(2), 1)); assert_ok!(Council::submit_candidacy(Origin::signed(3), 2)); - assert_ok!(Council::proxy_set_approvals(Origin::signed(12), vec![false, true, true], 0)); - assert_ok!(Council::proxy_set_approvals(Origin::signed(13), vec![false, true, true], 0)); + assert_ok!(Council::proxy_set_approvals(Origin::signed(12), vec![false, true, true], 0, 2)); + assert_ok!(Council::proxy_set_approvals(Origin::signed(13), vec![false, true, true], 0, 3)); - assert_eq!(Council::approvals_of(1), vec![true]); - assert_eq!(Council::approvals_of(4), vec![true]); - assert_eq!(Council::approvals_of(2), vec![false, true, true]); - assert_eq!(Council::approvals_of(3), vec![false, true, true]); + assert_eq!(Council::all_approvals_of(&1), vec![true]); + assert_eq!(Council::all_approvals_of(&4), vec![true]); + assert_eq!(Council::all_approvals_of(&2), vec![false, true, true]); + assert_eq!(Council::all_approvals_of(&3), vec![false, true, true]); - assert_eq!(Council::voters(), vec![1, 4, 2, 3]); + assert_eq!(voter_ids(), vec![1, 4, 2, 3]); }); } #[test] fn setting_any_approval_vote_count_without_any_candidate_count_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_eq!(Council::candidates().len(), 0); - assert_noop!(Council::set_approvals(Origin::signed(4), vec![], 0), "amount of candidates to receive approval votes should be non-zero"); + assert_noop!( + Council::set_approvals(Origin::signed(4), vec![], 0, 0), + "amount of candidates to receive approval votes should be non-zero" + ); }); } #[test] fn setting_an_approval_vote_count_more_than_candidate_count_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); assert_eq!(Council::candidates().len(), 1); - assert_noop!(Council::set_approvals(Origin::signed(4), vec![true, true], 0), "amount of candidate approval votes cannot exceed amount of candidates"); + assert_noop!( + Council::set_approvals(Origin::signed(4),vec![true, true], 0, 0), + "amount of candidate votes cannot exceed amount of candidates" + ); }); } #[test] fn resubmitting_voting_should_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![true], 0, 0)); - assert_eq!(Council::approvals_of(4), vec![true]); + assert_eq!(Council::all_approvals_of(&4), vec![true]); assert_ok!(Council::submit_candidacy(Origin::signed(2), 1)); assert_ok!(Council::submit_candidacy(Origin::signed(3), 2)); assert_eq!(Council::candidates().len(), 3); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![true, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![true, false, true], 0, 0)); - assert_eq!(Council::approvals_of(4), vec![true, false, true]); + assert_eq!(Council::all_approvals_of(&4), vec![true, false, true]); }); } #[test] fn retracting_voter_should_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); @@ -855,88 +1856,108 @@ mod tests { assert_ok!(Council::submit_candidacy(Origin::signed(3), 2)); assert_eq!(Council::candidates().len(), 3); - assert_ok!(Council::set_approvals(Origin::signed(1), vec![true], 0)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true, true], 0)); - assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, true, true], 0)); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![true, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(1), vec![true], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true, true], 0, 1)); + assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, true, true], 0, 2)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![true, false, true], 0, 3)); - assert_eq!(Council::voters(), vec![1, 2, 3, 4]); - assert_eq!(Council::approvals_of(1), vec![true]); - assert_eq!(Council::approvals_of(2), vec![false, true, true]); - assert_eq!(Council::approvals_of(3), vec![false, true, true]); - assert_eq!(Council::approvals_of(4), vec![true, false, true]); + assert_eq!(voter_ids(), vec![1, 2, 3, 4]); + assert_eq!(Council::all_approvals_of(&1), vec![true]); + assert_eq!(Council::all_approvals_of(&2), vec![false, true, true]); + assert_eq!(Council::all_approvals_of(&3), vec![false, true, true]); + assert_eq!(Council::all_approvals_of(&4), vec![true, false, true]); assert_ok!(Council::retract_voter(Origin::signed(1), 0)); - assert_eq!(Council::voters(), vec![4, 2, 3]); - assert_eq!(Council::approvals_of(1), Vec::::new()); - assert_eq!(Council::approvals_of(2), vec![false, true, true]); - assert_eq!(Council::approvals_of(3), vec![false, true, true]); - assert_eq!(Council::approvals_of(4), vec![true, false, true]); + assert_eq!(voter_ids(), vec![0, 2, 3, 4]); + assert_eq!(Council::all_approvals_of(&1), Vec::::new()); + assert_eq!(Council::all_approvals_of(&2), vec![false, true, true]); + assert_eq!(Council::all_approvals_of(&3), vec![false, true, true]); + assert_eq!(Council::all_approvals_of(&4), vec![true, false, true]); assert_ok!(Council::retract_voter(Origin::signed(2), 1)); - assert_eq!(Council::voters(), vec![4, 3]); - assert_eq!(Council::approvals_of(1), Vec::::new()); - assert_eq!(Council::approvals_of(2), Vec::::new()); - assert_eq!(Council::approvals_of(3), vec![false, true, true]); - assert_eq!(Council::approvals_of(4), vec![true, false, true]); + assert_eq!(voter_ids(), vec![0, 0, 3, 4]); + assert_eq!(Council::all_approvals_of(&1), Vec::::new()); + assert_eq!(Council::all_approvals_of(&2), Vec::::new()); + assert_eq!(Council::all_approvals_of(&3), vec![false, true, true]); + assert_eq!(Council::all_approvals_of(&4), vec![true, false, true]); - assert_ok!(Council::retract_voter(Origin::signed(3), 1)); + assert_ok!(Council::retract_voter(Origin::signed(3), 2)); - assert_eq!(Council::voters(), vec![4]); - assert_eq!(Council::approvals_of(1), Vec::::new()); - assert_eq!(Council::approvals_of(2), Vec::::new()); - assert_eq!(Council::approvals_of(3), Vec::::new()); - assert_eq!(Council::approvals_of(4), vec![true, false, true]); + assert_eq!(voter_ids(), vec![0, 0, 0, 4]); + assert_eq!(Council::all_approvals_of(&1), Vec::::new()); + assert_eq!(Council::all_approvals_of(&2), Vec::::new()); + assert_eq!(Council::all_approvals_of(&3), Vec::::new()); + assert_eq!(Council::all_approvals_of(&4), vec![true, false, true]); }); } #[test] fn invalid_retraction_index_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_ok!(Council::submit_candidacy(Origin::signed(3), 0)); - assert_ok!(Council::set_approvals(Origin::signed(1), vec![true], 0)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0)); - assert_eq!(Council::voters(), vec![1, 2]); + assert_ok!(Council::set_approvals(Origin::signed(1), vec![true], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0, 0)); + assert_eq!(voter_ids(), vec![1, 2]); assert_noop!(Council::retract_voter(Origin::signed(1), 1), "retraction index mismatch"); }); } #[test] fn overflow_retraction_index_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_ok!(Council::submit_candidacy(Origin::signed(3), 0)); - assert_ok!(Council::set_approvals(Origin::signed(1), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(1), vec![true], 0, 0)); assert_noop!(Council::retract_voter(Origin::signed(1), 1), "retraction index invalid"); }); } #[test] fn non_voter_retraction_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); assert_ok!(Council::submit_candidacy(Origin::signed(3), 0)); - assert_ok!(Council::set_approvals(Origin::signed(1), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(1), vec![true], 0, 0)); assert_noop!(Council::retract_voter(Origin::signed(2), 0), "cannot retract non-voter"); }); } + #[test] + fn approval_storage_should_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(3), 1)); + + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![], 0, 0)); + + assert_eq!(Council::all_approvals_of(&2), vec![true]); + // NOTE: these two are stored in mem differently though. + assert_eq!(Council::all_approvals_of(&3), vec![]); + assert_eq!(Council::all_approvals_of(&4), vec![]); + + assert_eq!(Council::approvals_of((3, 0)), vec![0]); + assert_eq!(Council::approvals_of((4, 0)), vec![]); + }); + } + #[test] fn simple_tally_should_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert!(!Council::presentation_active()); assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); assert_ok!(Council::submit_candidacy(Origin::signed(5), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true, false], 0)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true], 0)); - assert_eq!(Council::voters(), vec![2, 5]); - assert_eq!(Council::approvals_of(2), vec![true, false]); - assert_eq!(Council::approvals_of(5), vec![false, true]); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true], 0, 0)); + assert_eq!(voter_ids(), vec![2, 5]); + assert_eq!(Council::all_approvals_of(&2), vec![true]); + assert_eq!(Council::all_approvals_of(&5), vec![false, true]); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -953,34 +1974,71 @@ mod tests { assert!(!Council::is_a_candidate(&2)); assert!(!Council::is_a_candidate(&5)); assert_eq!(Council::vote_index(), 1); - assert_eq!(Council::voter_last_active(2), Some(0)); - assert_eq!(Council::voter_last_active(5), Some(0)); + assert_eq!(Council::voter_info(2), Some(VoterInfo { last_win: 1, last_active: 0, stake: 20, pot: 0 })); + assert_eq!(Council::voter_info(5), Some(VoterInfo { last_win: 1, last_active: 0, stake: 50, pot: 0 })); + }); + } + + #[test] + fn seats_should_be_released() { + with_externalities(&mut ExtBuilder::default().build(), || { + System::set_block_number(4); + assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); + assert_ok!(Council::submit_candidacy(Origin::signed(5), 1)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true], 0, 0)); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(6); + assert!(Council::presentation_active()); + assert_eq!(Council::present_winner(Origin::signed(4), 2, 20, 0), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(4), 5, 50, 0), Ok(())); + assert_eq!(Council::leaderboard(), Some(vec![(0, 0), (0, 0), (20, 2), (50, 5)])); + assert_ok!(Council::end_block(System::block_number())); + + assert_eq!(Council::active_council(), vec![(5, 11), (2, 11)]); + let mut current = System::block_number(); + let free_block; + loop { + current += 1; + System::set_block_number(current); + assert_ok!(Council::end_block(System::block_number())); + if Council::active_council().len() == 0 { + free_block = current; + break; + } + } + // 11 + 2 which is the next voting period. + assert_eq!(free_block, 14); }); } #[test] fn presentations_with_zero_staked_deposit_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); - assert_noop!(Council::present_winner(Origin::signed(4), 2, 0, 0), "stake deposited to present winner and be added to leaderboard should be non-zero"); + assert_noop!( + Council::present_winner(Origin::signed(4), 2, 0, 0), + "stake deposited to present winner and be added to leaderboard should be non-zero" + ); }); } #[test] fn double_presentations_should_be_punished() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { assert!(Balances::can_slash(&4, 10)); System::set_block_number(4); assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); assert_ok!(Council::submit_candidacy(Origin::signed(5), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true, false], 0)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -996,10 +2054,10 @@ mod tests { #[test] fn retracting_inactive_voter_should_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -1008,7 +2066,7 @@ mod tests { System::set_block_number(8); assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![true], 1)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![true], 1, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(10); @@ -1016,24 +2074,24 @@ mod tests { assert_ok!(Council::end_block(System::block_number())); assert_ok!(Council::reap_inactive_voter(Origin::signed(5), - (Council::voters().iter().position(|&i| i == 5).unwrap() as u32).into(), - 2, (Council::voters().iter().position(|&i| i == 2).unwrap() as u32).into(), + (voter_ids().iter().position(|&i| i == 5).unwrap() as u32).into(), + 2, (voter_ids().iter().position(|&i| i == 2).unwrap() as u32).into(), 2 )); - assert_eq!(Council::voters(), vec![5]); - assert_eq!(Council::approvals_of(2).len(), 0); - assert_eq!(Balances::total_balance(&2), 17); - assert_eq!(Balances::total_balance(&5), 53); + assert_eq!(voter_ids(), vec![0, 5]); + assert_eq!(Council::all_approvals_of(&2).len(), 0); + assert_eq!(Balances::total_balance(&2), 20); + assert_eq!(Balances::total_balance(&5), 50); }); } #[test] fn presenting_for_double_election_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert_eq!(Council::submit_candidacy(Origin::signed(2), 0), Ok(())); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -1041,21 +2099,26 @@ mod tests { assert_ok!(Council::end_block(System::block_number())); System::set_block_number(8); + // NOTE: This is now mandatory to disable the lock + assert_ok!(Council::retract_voter(Origin::signed(2), 0)); assert_eq!(Council::submit_candidacy(Origin::signed(2), 0), Ok(())); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 1)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 1, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(10); - assert_noop!(Council::present_winner(Origin::signed(4), 2, 20, 1), "candidate must not form a duplicated member if elected"); + assert_noop!( + Council::present_winner(Origin::signed(4), 2, 20, 1), + "candidate must not form a duplicated member if elected" + ); }); } #[test] fn retracting_inactive_voter_with_other_candidates_in_slots_should_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().voter_bond(2).build(), || { System::set_block_number(4); assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -1064,7 +2127,7 @@ mod tests { System::set_block_number(8); assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![true], 1)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![true], 1, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(10); @@ -1075,24 +2138,24 @@ mod tests { assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); assert_ok!(Council::reap_inactive_voter(Origin::signed(5), - (Council::voters().iter().position(|&i| i == 5).unwrap() as u32).into(), - 2, (Council::voters().iter().position(|&i| i == 2).unwrap() as u32).into(), + (voter_ids().iter().position(|&i| i == 5).unwrap() as u32).into(), + 2, (voter_ids().iter().position(|&i| i == 2).unwrap() as u32).into(), 2 )); - assert_eq!(Council::voters(), vec![5]); - assert_eq!(Council::approvals_of(2).len(), 0); - assert_eq!(Balances::total_balance(&2), 17); - assert_eq!(Balances::total_balance(&5), 53); + assert_eq!(voter_ids(), vec![0, 5]); + assert_eq!(Council::all_approvals_of(&2).len(), 0); + assert_eq!(Balances::total_balance(&2), 18); + assert_eq!(Balances::total_balance(&5), 52); }); } #[test] fn retracting_inactive_voter_with_bad_reporter_index_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -1101,7 +2164,7 @@ mod tests { System::set_block_number(8); assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![true], 1)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![true], 1, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(10); @@ -1110,18 +2173,18 @@ mod tests { assert_noop!(Council::reap_inactive_voter(Origin::signed(2), 42, - 2, (Council::voters().iter().position(|&i| i == 2).unwrap() as u32).into(), + 2, (voter_ids().iter().position(|&i| i == 2).unwrap() as u32).into(), 2 - ), "bad reporter index"); + ), "invalid reporter index"); }); } #[test] fn retracting_inactive_voter_with_bad_target_index_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -1130,7 +2193,7 @@ mod tests { System::set_block_number(8); assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![true], 1)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![true], 1, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(10); @@ -1138,25 +2201,25 @@ mod tests { assert_ok!(Council::end_block(System::block_number())); assert_noop!(Council::reap_inactive_voter(Origin::signed(2), - (Council::voters().iter().position(|&i| i == 2).unwrap() as u32).into(), + (voter_ids().iter().position(|&i| i == 2).unwrap() as u32).into(), 2, 42, 2 - ), "bad target index"); + ), "invalid target index"); }); } #[test] fn attempting_to_retract_active_voter_should_slash_reporter() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); assert_ok!(Council::submit_candidacy(Origin::signed(3), 1)); assert_ok!(Council::submit_candidacy(Origin::signed(4), 2)); assert_ok!(Council::submit_candidacy(Origin::signed(5), 3)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true, false, false, false], 0)); - assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, true, false, false], 0)); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, true, false], 0)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true, false, false, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, true, false, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, true, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, false, false, true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -1171,34 +2234,34 @@ mod tests { assert_ok!(Council::end_block(System::block_number())); System::set_block_number(10); - assert_ok!(Council::present_winner(Origin::signed(4), 2, 20, 1)); - assert_ok!(Council::present_winner(Origin::signed(4), 3, 30, 1)); + assert_ok!(Council::present_winner(Origin::signed(4), 2, 20 + Council::get_offset(20, 1), 1)); + assert_ok!(Council::present_winner(Origin::signed(4), 3, 30 + Council::get_offset(30, 1), 1)); assert_ok!(Council::end_block(System::block_number())); assert_eq!(Council::vote_index(), 2); assert_eq!(Council::inactivity_grace_period(), 1); assert_eq!(Council::voting_period(), 4); - assert_eq!(Council::voter_last_active(4), Some(0)); + assert_eq!(Council::voter_info(4), Some(VoterInfo { last_win: 1, last_active: 0, stake: 40, pot: 0 })); assert_ok!(Council::reap_inactive_voter(Origin::signed(4), - (Council::voters().iter().position(|&i| i == 4).unwrap() as u32).into(), + (voter_ids().iter().position(|&i| i == 4).unwrap() as u32).into(), 2, - (Council::voters().iter().position(|&i| i == 2).unwrap() as u32).into(), + (voter_ids().iter().position(|&i| i == 2).unwrap() as u32).into(), 2 )); - assert_eq!(Council::voters(), vec![2, 3, 5]); - assert_eq!(Council::approvals_of(4).len(), 0); - assert_eq!(Balances::total_balance(&4), 37); + assert_eq!(voter_ids(), vec![2, 3, 0, 5]); + assert_eq!(Council::all_approvals_of(&4).len(), 0); + assert_eq!(Balances::total_balance(&4), 40); }); } #[test] fn attempting_to_retract_inactive_voter_by_nonvoter_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -1207,7 +2270,7 @@ mod tests { System::set_block_number(8); assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![true], 1)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![true], 1, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(10); @@ -1216,7 +2279,7 @@ mod tests { assert_noop!(Council::reap_inactive_voter(Origin::signed(4), 0, - 2, (Council::voters().iter().position(|&i| i == 2).unwrap() as u32).into(), + 2, (voter_ids().iter().position(|&i| i == 2).unwrap() as u32).into(), 2 ), "reporter must be a voter"); }); @@ -1224,18 +2287,18 @@ mod tests { #[test] fn presenting_loser_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); - assert_ok!(Council::set_approvals(Origin::signed(6), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(6), vec![true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(2), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(3), 2)); - assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, false, true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(4), 3)); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(5), 4)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, false, false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, false, false, false, true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -1257,18 +2320,18 @@ mod tests { #[test] fn presenting_loser_first_should_not_matter() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); - assert_ok!(Council::set_approvals(Origin::signed(6), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(6), vec![true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(2), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(3), 2)); - assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, false, true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(4), 3)); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(5), 4)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, false, false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, false, false, false, true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -1289,21 +2352,24 @@ mod tests { #[test] fn present_outside_of_presentation_period_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert!(!Council::presentation_active()); - assert_noop!(Council::present_winner(Origin::signed(5), 5, 1, 0), "cannot present outside of presentation period"); + assert_noop!( + Council::present_winner(Origin::signed(5), 5, 1, 0), + "cannot present outside of presentation period" + ); }); } #[test] fn present_with_invalid_vote_index_should_not_work() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); assert_ok!(Council::submit_candidacy(Origin::signed(5), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true, false], 0)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -1313,34 +2379,50 @@ mod tests { #[test] fn present_when_presenter_is_poor_should_not_work() { - with_externalities(&mut new_test_ext(false), || { - System::set_block_number(4); - assert!(!Council::presentation_active()); + let test_present = |p| { + with_externalities(&mut ExtBuilder::default() + .voting_fee(5) + .voter_bond(2) + .bad_presentation_punishment(p) + .build(), + || { + System::set_block_number(4); + let _ = Balances::make_free_balance_be(&1, 15); + assert!(!Council::presentation_active()); - assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); - assert_ok!(Council::submit_candidacy(Origin::signed(5), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true, false], 0)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true], 0)); - assert_ok!(Council::end_block(System::block_number())); + assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); // -3 + assert_eq!(Balances::free_balance(&1), 12); + assert_ok!(Council::set_approvals(Origin::signed(1), vec![true], 0, 0)); // -2 -5 + assert_ok!(Council::end_block(System::block_number())); - System::set_block_number(6); - assert_eq!(Balances::free_balance(&1), 1); - assert_eq!(Balances::reserved_balance(&1), 9); - assert_noop!(Council::present_winner(Origin::signed(1), 1, 20, 0), "presenter must have sufficient slashable funds"); - }); + System::set_block_number(6); + assert_eq!(Balances::free_balance(&1), 5); + assert_eq!(Balances::reserved_balance(&1), 5); + if p > 5 { + assert_noop!(Council::present_winner( + Origin::signed(1), 1, 10, 0), + "presenter must have sufficient slashable funds" + ); + } else { + assert_ok!(Council::present_winner(Origin::signed(1), 1, 10, 0)); + } + }); + }; + test_present(4); + test_present(6); } #[test] fn invalid_present_tally_should_slash() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert!(!Council::presentation_active()); assert_eq!(Balances::total_balance(&4), 40); assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); assert_ok!(Council::submit_candidacy(Origin::signed(5), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true, false], 0)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true, false], 0, 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -1352,20 +2434,20 @@ mod tests { #[test] fn runners_up_should_be_kept() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert!(!Council::presentation_active()); assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); - assert_ok!(Council::set_approvals(Origin::signed(6), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(6), vec![true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(2), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(3), 2)); - assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, false, true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(4), 3)); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(5), 4)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, false, false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, false, false, false, true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); @@ -1373,7 +2455,7 @@ mod tests { assert!(Council::presentation_active()); assert_ok!(Council::present_winner(Origin::signed(4), 1, 60, 0)); // leaderboard length is the empty seats plus the carry count (i.e. 5 + 2), where those - // to be carried are the lowest and stored in lowest indexes + // to be carried are the lowest and stored in lowest indices assert_eq!(Council::leaderboard(), Some(vec![ (0, 0), (0, 0), @@ -1401,11 +2483,11 @@ mod tests { assert!(Council::is_a_candidate(&3)); assert!(Council::is_a_candidate(&4)); assert_eq!(Council::vote_index(), 1); - assert_eq!(Council::voter_last_active(2), Some(0)); - assert_eq!(Council::voter_last_active(3), Some(0)); - assert_eq!(Council::voter_last_active(4), Some(0)); - assert_eq!(Council::voter_last_active(5), Some(0)); - assert_eq!(Council::voter_last_active(6), Some(0)); + assert_eq!(Council::voter_info(2), Some(VoterInfo { last_win: 0, last_active: 0, stake: 20, pot: 0 })); + assert_eq!(Council::voter_info(3), Some(VoterInfo { last_win: 0, last_active: 0, stake: 30, pot: 0 })); + assert_eq!(Council::voter_info(4), Some(VoterInfo { last_win: 0, last_active: 0, stake: 40, pot: 0 })); + assert_eq!(Council::voter_info(5), Some(VoterInfo { last_win: 1, last_active: 0, stake: 50, pot: 0 })); + assert_eq!(Council::voter_info(6), Some(VoterInfo { last_win: 1, last_active: 0, stake: 60, pot: 0 })); assert_eq!(Council::candidate_reg_info(3), Some((0, 2))); assert_eq!(Council::candidate_reg_info(4), Some((0, 3))); }); @@ -1413,18 +2495,18 @@ mod tests { #[test] fn second_tally_should_use_runners_up() { - with_externalities(&mut new_test_ext(false), || { + with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(4); assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); - assert_ok!(Council::set_approvals(Origin::signed(6), vec![true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(6), vec![true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(2), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(3), 2)); - assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(3), vec![false, false, true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(4), 3)); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 0, 0)); assert_ok!(Council::submit_candidacy(Origin::signed(5), 4)); - assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, false, false, false, true], 0)); + assert_ok!(Council::set_approvals(Origin::signed(5), vec![false, false, false, false, true], 0, 0)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(6); @@ -1435,13 +2517,13 @@ mod tests { assert_ok!(Council::end_block(System::block_number())); System::set_block_number(8); - assert_ok!(Council::set_approvals(Origin::signed(6), vec![false, false, true, false], 1)); + assert_ok!(Council::set_approvals(Origin::signed(6), vec![false, false, true, false], 1, 0)); assert_ok!(Council::set_desired_seats(3)); assert_ok!(Council::end_block(System::block_number())); System::set_block_number(10); - assert_ok!(Council::present_winner(Origin::signed(4), 3, 90, 1)); - assert_ok!(Council::present_winner(Origin::signed(4), 4, 40, 1)); + assert_ok!(Council::present_winner(Origin::signed(4), 3, 30 + Council::get_offset(30, 1) + 60, 1)); + assert_ok!(Council::present_winner(Origin::signed(4), 4, 40 + Council::get_offset(40, 1), 1)); assert_ok!(Council::end_block(System::block_number())); assert!(!Council::presentation_active()); @@ -1453,11 +2535,14 @@ mod tests { assert!(!Council::is_a_candidate(&5)); assert!(Council::is_a_candidate(&4)); assert_eq!(Council::vote_index(), 2); - assert_eq!(Council::voter_last_active(2), Some(0)); - assert_eq!(Council::voter_last_active(3), Some(0)); - assert_eq!(Council::voter_last_active(4), Some(0)); - assert_eq!(Council::voter_last_active(5), Some(0)); - assert_eq!(Council::voter_last_active(6), Some(1)); + assert_eq!(Council::voter_info(2), Some( VoterInfo { last_win: 0, last_active: 0, stake: 20, pot: 0})); + assert_eq!(Council::voter_info(3), Some( VoterInfo { last_win: 2, last_active: 0, stake: 30, pot: 0})); + assert_eq!(Council::voter_info(4), Some( VoterInfo { last_win: 0, last_active: 0, stake: 40, pot: 0})); + assert_eq!(Council::voter_info(5), Some( VoterInfo { last_win: 1, last_active: 0, stake: 50, pot: 0})); + assert_eq!( + Council::voter_info(6), + Some(VoterInfo { last_win: 2, last_active: 1, stake: 60, pot: 0}) + ); assert_eq!(Council::candidate_reg_info(4), Some((0, 3))); }); diff --git a/substrate/srml/indices/src/lib.rs b/substrate/srml/indices/src/lib.rs index 509e5c1133..38f7ee668d 100644 --- a/substrate/srml/indices/src/lib.rs +++ b/substrate/srml/indices/src/lib.rs @@ -45,7 +45,7 @@ pub trait ResolveHint { fn resolve_hint(who: &AccountId) -> Option; } -/// Simple encode-based resolve hint implemenntation. +/// Simple encode-based resolve hint implementation. pub struct SimpleResolveHint(PhantomData<(AccountId, AccountIndex)>); impl> ResolveHint for SimpleResolveHint