From 6f66bc5b4687465d14ecfd3348056791520fc776 Mon Sep 17 00:00:00 2001 From: Gav Date: Mon, 10 Sep 2018 11:09:14 +0200 Subject: [PATCH 1/7] Initial bits --- substrate/runtime/council/src/voting.rs | 24 +++++++++++++++++++++++- substrate/runtime/session/src/lib.rs | 11 ++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/substrate/runtime/council/src/voting.rs b/substrate/runtime/council/src/voting.rs index 5d14f68c9d5e1..f0a675a62358b 100644 --- a/substrate/runtime/council/src/voting.rs +++ b/substrate/runtime/council/src/voting.rs @@ -23,9 +23,13 @@ use runtime_io::print; use substrate_runtime_support::dispatch::Result; use substrate_runtime_support::{StorageValue, StorageMap, IsSubType}; use {system, democracy}; -use super::{Trait, Module as Council}; +use super::{Trait as CouncilTrait, Module as Council}; use system::{ensure_signed, ensure_root}; +pub trait Trait: CouncilTrait { + type Event: From> + Into<::Event>; +} + decl_module! { pub struct Module for enum Call where origin: T::Origin { fn propose(origin, proposal: Box) -> Result; @@ -49,7 +53,25 @@ decl_storage! { } } +pub type Event = RawEvent<::BlockNumber>; + +/// An event in this module. +#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] +#[derive(Encode, Decode, PartialEq, Eq, Clone)] +pub enum RawEvent { +} + +impl From> for () { + fn from(_: RawEvent) -> () { () } +} + impl Module { + + /// Deposit one of this module's events. + fn deposit_event(event: Event) { + >::deposit_event(::Event::from(event).into()); + } + pub fn is_vetoed>(proposal: B) -> bool { Self::veto_of(proposal.borrow()) .map(|(expiry, _): (T::BlockNumber, Vec)| >::block_number() < expiry) diff --git a/substrate/runtime/session/src/lib.rs b/substrate/runtime/session/src/lib.rs index deb138f7e3cbd..5338b852728fc 100644 --- a/substrate/runtime/session/src/lib.rs +++ b/substrate/runtime/session/src/lib.rs @@ -122,6 +122,12 @@ decl_storage! { } impl Module { + + /// Deposit one of this module's events. + fn deposit_event(event: Event) { + >::deposit_event(::Event::from(event).into()); + } + /// The number of validators currently. pub fn validator_count() -> u32 { >::get().len() as u32 // TODO: can probably optimised @@ -186,11 +192,6 @@ impl Module { } } - /// Deposit one of this module's events. - fn deposit_event(event: Event) { - >::deposit_event(::Event::from(event).into()); - } - /// Move onto next session: register the new authority set. pub fn rotate_session(is_final_block: bool, apply_rewards: bool) { let now = >::get(); From 6005169103b9eeb7ca56183ca44e8215f09f98c1 Mon Sep 17 00:00:00 2001 From: Gav Date: Mon, 10 Sep 2018 11:20:54 +0200 Subject: [PATCH 2/7] More stuff --- substrate/runtime/council/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/substrate/runtime/council/src/lib.rs b/substrate/runtime/council/src/lib.rs index 0ff41c2743f9f..fb88113160c40 100644 --- a/substrate/runtime/council/src/lib.rs +++ b/substrate/runtime/council/src/lib.rs @@ -666,7 +666,12 @@ mod tests { impl democracy::Trait for Test { type Proposal = Call; } - impl Trait for Test {} + impl voting::Trait for Test { + type Event = (); + } + impl Trait for Test { + type Event = (); + } pub fn new_test_ext(with_council: bool) -> runtime_io::TestExternalities { let mut t = system::GenesisConfig::::default().build_storage().unwrap(); From aef688fb824c2025d641ba03d4768c189347fb03 Mon Sep 17 00:00:00 2001 From: Gav Date: Mon, 10 Sep 2018 12:10:11 +0200 Subject: [PATCH 3/7] Cleave voting from council --- Cargo.lock | 1 + substrate/runtime/council/Cargo.toml | 2 ++ substrate/runtime/council/src/lib.rs | 47 +++++++++++++++++++++---- substrate/runtime/council/src/voting.rs | 23 ++++++------ 4 files changed, 55 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 406c57a4fd374..6f7940818e260 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2708,6 +2708,7 @@ dependencies = [ "serde 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)", "substrate-codec 0.1.0", + "substrate-codec-derive 0.1.0", "substrate-keyring 0.1.0", "substrate-primitives 0.1.0", "substrate-runtime-balances 0.1.0", diff --git a/substrate/runtime/council/Cargo.toml b/substrate/runtime/council/Cargo.toml index 7b420a7b2949d..aa1bbd0bdf079 100644 --- a/substrate/runtime/council/Cargo.toml +++ b/substrate/runtime/council/Cargo.toml @@ -11,6 +11,7 @@ serde_derive = { version = "1.0", optional = true } safe-mix = { version = "1.0", default_features = false} substrate-keyring = { path = "../../keyring", optional = true } substrate-codec = { path = "../../codec", default_features = false } +substrate-codec-derive = { path = "../../codec/derive", default_features = false } substrate-primitives = { path = "../../primitives", default_features = false } substrate-runtime-std = { path = "../../runtime-std", default_features = false } substrate-runtime-io = { path = "../../runtime-io", default_features = false } @@ -29,6 +30,7 @@ std = [ "safe-mix/std", "substrate-keyring", "substrate-codec/std", + "substrate-codec-derive/std", "substrate-primitives/std", "substrate-runtime-std/std", "substrate-runtime-io/std", diff --git a/substrate/runtime/council/src/lib.rs b/substrate/runtime/council/src/lib.rs index fb88113160c40..d5bfe666b702c 100644 --- a/substrate/runtime/council/src/lib.rs +++ b/substrate/runtime/council/src/lib.rs @@ -27,6 +27,7 @@ extern crate serde_derive; extern crate integer_sqrt; extern crate substrate_codec as codec; +#[macro_use] extern crate substrate_codec_derive; extern crate substrate_primitives; #[cfg(feature = "std")] extern crate substrate_keyring as keyring; #[macro_use] extern crate substrate_runtime_std as rstd; @@ -40,9 +41,9 @@ extern crate substrate_runtime_system as system; use rstd::prelude::*; #[cfg(feature = "std")] use std::collections::HashMap; -use primitives::traits::{Zero, One, As, Lookup}; -use substrate_runtime_support::{StorageValue, StorageMap}; -use substrate_runtime_support::dispatch::Result; +use primitives::traits::{Zero, One, As, Lookup, OnFinalise}; +use runtime_io::print; +use substrate_runtime_support::{StorageValue, StorageMap, dispatch::Result}; use balances::address::Address; use system::{ensure_signed, ensure_root}; @@ -104,7 +105,9 @@ pub mod voting; pub type VoteIndex = u32; -pub trait Trait: democracy::Trait {} +pub trait Trait: democracy::Trait { + type Event: From> + Into<::Event>; +} decl_module! { pub struct Module for enum Call where origin: T::Origin { @@ -176,8 +179,29 @@ decl_storage! { } } +pub type Event = RawEvent<::Hash>; + +/// An event in this module. +#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] +#[derive(Encode, Decode, PartialEq, Eq, Clone)] +pub enum RawEvent { + /// A voting tally has happened for a referendum cancelation vote. Last three are yes, no, abstain counts. + TallyCancelation(Hash, u32, u32, u32), + /// A voting tally has happened for a referendum vote. Last three are yes, no, abstain counts. + TallyReferendum(Hash, u32, u32, u32), +} + +impl From> for () { + fn from(_: RawEvent) -> () { () } +} + impl Module { + /// Deposit one of this module's events. + fn deposit_event(event: Event) { + >::deposit_event(::Event::from(event).into()); + } + // exposed immutables. /// True if we're currently in a presentation period. @@ -552,6 +576,15 @@ impl Module { } } +impl OnFinalise for Module { + fn on_finalise(n: T::BlockNumber) { + if let Err(e) = Self::end_block(n) { + print("Guru meditation"); + print(e); + } + } +} + #[cfg(feature = "std")] #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -569,14 +602,13 @@ pub struct GenesisConfig { pub term_duration: T::BlockNumber, pub inactive_grace_period: T::BlockNumber, - // for the council's votes. pub cooloff_period: T::BlockNumber, pub voting_period: T::BlockNumber, } #[cfg(feature = "std")] -impl Default for GenesisConfig { +impl Default for GenesisConfig { fn default() -> Self { GenesisConfig { candidacy_bond: T::Balance::sa(9), @@ -596,7 +628,7 @@ impl Default for GenesisConfig { } #[cfg(feature = "std")] -impl primitives::BuildStorage for GenesisConfig +impl primitives::BuildStorage for GenesisConfig { fn build_storage(self) -> ::std::result::Result, Vec>, String> { use codec::Encode; @@ -714,6 +746,7 @@ mod tests { pub type Balances = balances::Module; pub type Democracy = democracy::Module; pub type Council = Module; + pub type CouncilVoting = voting::Module; #[test] fn params_should_work() { diff --git a/substrate/runtime/council/src/voting.rs b/substrate/runtime/council/src/voting.rs index f0a675a62358b..6239f31683806 100644 --- a/substrate/runtime/council/src/voting.rs +++ b/substrate/runtime/council/src/voting.rs @@ -53,12 +53,16 @@ decl_storage! { } } -pub type Event = RawEvent<::BlockNumber>; +pub type Event = RawEvent<::Hash>; /// An event in this module. #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] #[derive(Encode, Decode, PartialEq, Eq, Clone)] -pub enum RawEvent { +pub enum RawEvent { + /// A voting tally has happened for a referendum cancelation vote. Last three are yes, no, abstain counts. + TallyCancelation(Hash, u32, u32, u32), + /// A voting tally has happened for a referendum vote. Last three are yes, no, abstain counts. + TallyReferendum(Hash, u32, u32, u32), } impl From> for () { @@ -113,7 +117,8 @@ impl Module { >::insert(proposal_hash, *proposal); >::insert(proposal_hash, vec![who.clone()]); - >::insert((proposal_hash, who), true); + >::insert((proposal_hash, who.clone()), true); + Ok(()) } @@ -121,9 +126,7 @@ impl Module { let who = ensure_signed(origin)?; if Self::vote_of((proposal, who.clone())).is_none() { - let mut voters = Self::proposal_voters(&proposal); - voters.push(who.clone()); - >::insert(proposal, voters); + >::mutate(proposal, |voters| voters.push(who.clone())); } >::insert((proposal, who), approve); Ok(()) @@ -208,10 +211,12 @@ impl Module { while let Some((proposal, proposal_hash)) = Self::take_proposal_if_expiring_at(now) { let tally = Self::take_tally(&proposal_hash); if let Some(&democracy::Call::cancel_referendum(ref_index)) = IsSubType::>::is_aux_sub_type(&proposal) { + Self::deposit_event(RawEvent::TallyCancelation(proposal_hash, tally.0, tally.1, tally.2)); if let (_, 0, 0) = tally { >::internal_cancel_referendum(ref_index); } } else { + Self::deposit_event(RawEvent::TallyReferendum(proposal_hash.clone(), tally.0, tally.1, tally.2)); if tally.0 > tally.1 + tally.2 { Self::kill_veto_of(&proposal_hash); match tally { @@ -225,16 +230,12 @@ impl Module { } } -impl OnFinalise for Council { +impl OnFinalise for Module { fn on_finalise(n: T::BlockNumber) { if let Err(e) = Self::end_block(n) { print("Guru meditation"); print(e); } - if let Err(e) = >::end_block(n) { - print("Guru meditation"); - print(e); - } } } From b45a8614edf083e9bb7e0c79782511f26c29ec5e Mon Sep 17 00:00:00 2001 From: Gav Date: Mon, 10 Sep 2018 13:47:21 +0200 Subject: [PATCH 4/7] More events --- substrate/runtime/council/src/lib.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/substrate/runtime/council/src/lib.rs b/substrate/runtime/council/src/lib.rs index d5bfe666b702c..50be4b72cb716 100644 --- a/substrate/runtime/council/src/lib.rs +++ b/substrate/runtime/council/src/lib.rs @@ -179,16 +179,20 @@ decl_storage! { } } -pub type Event = RawEvent<::Hash>; +pub type Event = RawEvent<::AccountId>; /// An event in this module. #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] #[derive(Encode, Decode, PartialEq, Eq, Clone)] -pub enum RawEvent { - /// A voting tally has happened for a referendum cancelation vote. Last three are yes, no, abstain counts. - TallyCancelation(Hash, u32, u32, u32), - /// A voting tally has happened for a referendum vote. Last three are yes, no, abstain counts. - TallyReferendum(Hash, u32, u32, u32), +pub enum RawEvent { + /// reaped voter, reaper + VoterReaped(AccountId, AccountId), + /// slashed reaper + BadReaperSlashed(AccountId), + /// A tally (for approval votes of council seat(s)) has started. + TallyStarted, + /// A tally (for approval votes of council seat(s)) has ended (with one or more new members). + TallyFinalised, } impl From> for () { @@ -320,8 +324,10 @@ impl Module { // This only fails if `who` 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. >::repatriate_reserved(&who, &reporter, Self::voting_bond())?; + Self::deposit_event(RawEvent::VoterReaped(who, reporter)); } else { >::slash_reserved(&reporter, Self::voting_bond()); + Self::deposit_event(RawEvent::BadReaperSlashed(reporter)); } Ok(()) } @@ -449,7 +455,7 @@ impl Module { Ok(()) } - /// Set the presentation duration. If there is current a vote being presented for, will + /// Set the presentation duration. If there is currently a vote being presented for, will /// invoke `finalise_vote`. fn set_presentation_duration(origin: T::Origin, count: T::BlockNumber) -> Result { ensure_root(origin)?; @@ -508,6 +514,8 @@ impl Module { // initialise leaderboard. let leaderboard_size = empty_seats + Self::carry_count() as usize; >::put(vec![(T::Balance::zero(), T::AccountId::default()); leaderboard_size]); + + Self::deposit_event(RawEvent::TallyStarted); } } @@ -569,6 +577,9 @@ impl Module { if let Some(last_index) = new_candidates.iter().rposition(|c| *c != T::AccountId::default()) { new_candidates.truncate(last_index + 1); } + + Self::deposit_event(RawEvent::TallyFinalised); + >::put(new_candidates); >::put(count); >::put(Self::vote_index() + 1); From 211936297c70f6dc2d218d8d2c8496dcb90f41a0 Mon Sep 17 00:00:00 2001 From: Gav Date: Mon, 10 Sep 2018 17:01:16 +0200 Subject: [PATCH 5/7] Rearrange council a little --- substrate/runtime/council/src/lib.rs | 1387 +--------------------- substrate/runtime/council/src/motions.rs | 23 +- substrate/runtime/council/src/seats.rs | 1362 +++++++++++++++++++++ substrate/runtime/council/src/voting.rs | 2 - 4 files changed, 1402 insertions(+), 1372 deletions(-) create mode 100644 substrate/runtime/council/src/seats.rs diff --git a/substrate/runtime/council/src/lib.rs b/substrate/runtime/council/src/lib.rs index 785b4f879005d..6878466568c60 100644 --- a/substrate/runtime/council/src/lib.rs +++ b/substrate/runtime/council/src/lib.rs @@ -45,567 +45,20 @@ extern crate substrate_runtime_system as system; use rstd::prelude::*; #[cfg(feature = "std")] use std::collections::HashMap; -use primitives::traits::{Zero, One, As, Lookup, OnFinalise}; -use runtime_io::print; -use substrate_runtime_support::{StorageValue, StorageMap, dispatch::Result}; -use balances::address::Address; -use system::{ensure_signed, ensure_root}; +use primitives::traits::As; +use substrate_runtime_support::StorageValue; pub mod voting; pub mod motions; +pub mod seats; -// no polynomial attacks: -// -// all unbonded public operations should be constant time. -// all other public operations must be linear time in terms of prior public operations and: -// - those "valid" ones that cost nothing be limited to a constant number per single protected operation -// - the rest costing the same order as the computational complexity -// all protected operations must complete in at most O(public operations) -// -// we assume "beneficial" transactions will have the same access as attack transactions. -// -// any storage requirements should be bonded by the same order as the volume. - -// public operations: -// - express approvals (you pay in a "voter" bond the first time you do this; O(1); one extra DB entry, one DB change) -// - remove active voter (you get your "voter" bond back; O(1); one fewer DB entry, one DB change) -// - remove inactive voter (either you or the target is removed; if the target, you get their "voter" bond back; O(1); one fewer DB entry, one DB change) -// - submit candidacy (you pay a "candidate" bond; O(1); one extra DB entry, two DB changes) -// - present winner/runner-up (you may pay a "presentation" bond of O(voters) if the presentation is invalid; O(voters) compute; ) -// protected operations: -// - remove candidacy (remove all votes for a candidate) (one fewer DB entry, two DB changes) - -// to avoid a potentially problematic case of not-enough approvals prior to voting causing a -// back-to-back votes that have no way of ending, then there's a forced grace period between votes. -// to keep the system as stateless as possible (making it a bit easier to reason about), we just -// restrict when votes can begin to blocks that lie on boundaries (`voting_period`). - -// for an approval vote of C councilers: - -// top K runners-up are maintained between votes. all others are discarded. -// - candidate removed & bond returned when elected. -// - candidate removed & bond burned when discarded. - -// at the point that the vote ends (), all voters' balances are snapshotted. - -// for B blocks following, there's a counting period whereby each of the candidates that believe -// they fall in the top K+C voted can present themselves. they get the total stake -// recorded (based on the snapshot); an ordered list is maintained (the leaderboard). Noone may -// present themselves that, if elected, would result in being included twice on the council -// (important since existing councilers will will have their approval votes as it may be that they -// don't get removed), nor if existing presenters would mean they're not in the top K+C. - -// following B blocks, the top C candidates are elected and have their bond returned. the top C -// candidates and all other candidates beyond the top C+K are cleared. - -// vote-clearing happens lazily; for an approval to count, the most recent vote at the time of the -// voter's most recent vote must be no later than the most recent vote at the time that the -// candidate in the approval position was registered there. as candidates are removed from the -// register and others join in their place, this prevent an approval meant for an earlier candidate -// being used to elect a new candidate. - -// the candidate list increases as needed, but the contents (though not really the capacity) reduce -// after each vote as all but K entries are cleared. newly registering candidates must use cleared -// entries before they increase the capacity. - -pub type VoteIndex = u32; - -pub trait Trait: democracy::Trait { - type Event: From> + Into<::Event>; -} - -decl_module! { - pub struct Module for enum Call where origin: T::Origin { - fn set_approvals(origin, votes: Vec, index: VoteIndex) -> Result; - fn reap_inactive_voter(origin, reporter_index: u32, who: Address, who_index: u32, assumed_vote_index: VoteIndex) -> Result; - fn retract_voter(origin, index: u32) -> Result; - fn submit_candidacy(origin, slot: u32) -> Result; - fn present_winner(origin, candidate: Address, total: T::Balance, index: VoteIndex) -> Result; - - fn set_desired_seats(origin, count: u32) -> Result; - fn remove_member(origin, who: Address) -> Result; - fn set_presentation_duration(origin, count: T::BlockNumber) -> Result; - fn set_term_duration(origin, count: T::BlockNumber) -> Result; - } -} - -decl_storage! { - trait Store for Module as Council { - - // parameters - /// How much should be locked up in order to submit one's candidacy. - pub CandidacyBond get(candidacy_bond): required T::Balance; - /// How much should be locked up in order to be able to submit votes. - pub VotingBond get(voting_bond): required T::Balance; - /// The punishment, per voter, if you provide an invalid presentation. - pub PresentSlashPerVoter get(present_slash_per_voter): required T::Balance; - /// How many runners-up should have their approvals persist until the next vote. - pub CarryCount get(carry_count): required u32; - /// How long to give each top candidate to present themselves after the vote ends. - pub PresentationDuration get(presentation_duration): required T::BlockNumber; - /// How many votes need to go by after a voter's last vote before they can be reaped if their - /// approvals are moot. - pub InactiveGracePeriod get(inactivity_grace_period): required VoteIndex; - /// How often (in blocks) to check for new votes. - pub VotingPeriod get(voting_period): required T::BlockNumber; - /// How long each position is active for. - pub TermDuration get(term_duration): required T::BlockNumber; - /// Number of accounts that should be sitting on the council. - pub DesiredSeats get(desired_seats): required u32; - - // permanent state (always relevant, changes only at the finalisation of voting) - /// The current council. When there's a vote going on, this should still be used for executive - /// matters. - pub ActiveCouncil get(active_council): default Vec<(T::AccountId, T::BlockNumber)>; - /// The total number of votes that have happened or are in progress. - pub VoteCount get(vote_index): default VoteIndex; - - // persistent state (always relevant, changes constantly) - /// The last cleared vote index that this voter was last active at. - pub ApprovalsOf get(approvals_of): default map [ T::AccountId => 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 => (VoteIndex, u32) ]; - /// The last cleared vote index that this voter was last active at. - pub LastActiveOf get(voter_last_active): map [ T::AccountId => VoteIndex ]; - /// The present voter list. - pub Voters get(voters): default Vec; - /// The present candidate list. - pub Candidates get(candidates): default Vec; // has holes - pub CandidateCount get(candidate_count): default u32; - - // temporary state (only relevant during finalisation/presentation) - /// The accounts holding the seats that will become free on the next tally. - pub NextFinalise get(next_finalise): (T::BlockNumber, u32, Vec); - /// The stakes as they were at the point that the vote ended. - pub SnapshotedStakes get(snapshoted_stakes): required Vec; - /// Get the leaderboard if we;re in the presentation phase. - pub Leaderboard get(leaderboard): Vec<(T::Balance, T::AccountId)>; // ORDERED low -> high - } -} - -pub type Event = RawEvent<::AccountId>; - -/// An event in this module. -#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] -#[derive(Encode, Decode, PartialEq, Eq, Clone)] -pub enum RawEvent { - /// reaped voter, reaper - VoterReaped(AccountId, AccountId), - /// slashed reaper - BadReaperSlashed(AccountId), - /// A tally (for approval votes of council seat(s)) has started. - TallyStarted, - /// A tally (for approval votes of council seat(s)) has ended (with one or more new members). - TallyFinalised, -} - -impl From> for () { - fn from(_: RawEvent) -> () { () } -} - -impl Module { - - /// Deposit one of this module's events. - fn deposit_event(event: Event) { - >::deposit_event(::Event::from(event).into()); - } - - // exposed immutables. - - /// True if we're currently in a presentation period. - pub fn presentation_active() -> bool { - >::exists() - } - - /// If `who` a candidate at the moment? - pub fn is_a_candidate(who: &T::AccountId) -> bool { - >::exists(who) - } - - /// Determine the block that a vote can happen on which is no less than `n`. - pub fn next_vote_from(n: T::BlockNumber) -> T::BlockNumber { - let voting_period = Self::voting_period(); - (n + voting_period - One::one()) / voting_period * voting_period - } - - /// The block number on which the tally for the next election will happen. `None` only if the - /// desired seats of the council is zero. - pub fn next_tally() -> Option { - let desired_seats = Self::desired_seats(); - if desired_seats == 0 { - None - } else { - let c = Self::active_council(); - let (next_possible, count, coming) = - if let Some((tally_end, comers, leavers)) = Self::next_finalise() { - // if there's a tally in progress, then next tally can begin immediately afterwards - (tally_end, c.len() - leavers.len() + comers as usize, comers) - } else { - (>::block_number(), c.len(), 0) - }; - if count < desired_seats as usize { - Some(next_possible) - } else { - // next tally begins once enough council members expire to bring members below desired. - if desired_seats <= coming { - // the entire amount of desired seats is less than those new members - we'll have - // to wait until they expire. - Some(next_possible + Self::term_duration()) - } else { - Some(c[c.len() - (desired_seats - coming) as usize].1) - } - }.map(Self::next_vote_from) - } - } - - // dispatch - - /// Set candidate approvals. Approval slots stay valid as long as candidates in those slots - /// are registered. - fn set_approvals(origin: T::Origin, votes: Vec, index: VoteIndex) -> Result { - let who = ensure_signed(origin)?; - - ensure!(!Self::presentation_active(), "no approval changes during presentation period"); - ensure!(index == Self::vote_index(), "incorrect vote index"); - if !>::exists(&who) { - // not yet a voter - deduct bond. - // NOTE: this must be the last potential bailer, since it changes state. - >::reserve(&who, Self::voting_bond())?; - - >::put({ - let mut v = Self::voters(); - v.push(who.clone()); - v - }); - } - >::insert(&who, index); - >::insert(&who, votes); - Ok(()) - } - - /// 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. - /// - /// May be called by anyone. Returns the voter deposit to `signed`. - fn reap_inactive_voter( - origin: T::Origin, - reporter_index: u32, - who: Address, - who_index: u32, - assumed_vote_index: VoteIndex - ) -> Result { - let reporter = ensure_signed(origin)?; - - let who = >::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!(assumed_vote_index == Self::vote_index(), "vote index not current"); - ensure!(last_active < assumed_vote_index - Self::inactivity_grace_period(), "cannot reap during grace perid"); - let voters = Self::voters(); - 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"); - - // will definitely kill one of signed or who now. - - let valid = !Self::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*/ - ); - - Self::remove_voter( - if valid { &who } else { &reporter }, - if valid { who_index } else { reporter_index }, - voters - ); - if valid { - // This only fails if `who` 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. - >::repatriate_reserved(&who, &reporter, Self::voting_bond())?; - Self::deposit_event(RawEvent::VoterReaped(who, reporter)); - } else { - >::slash_reserved(&reporter, Self::voting_bond()); - Self::deposit_event(RawEvent::BadReaperSlashed(reporter)); - } - Ok(()) - } - - /// Remove a voter. All votes are cancelled and the voter deposit is returned. - fn retract_voter(origin: T::Origin, index: u32) -> Result { - let who = ensure_signed(origin)?; - - ensure!(!Self::presentation_active(), "cannot retract when presenting"); - ensure!(>::exists(&who), "cannot retract non-voter"); - let voters = Self::voters(); - let index = index as usize; - ensure!(index < voters.len(), "retraction index invalid"); - ensure!(voters[index] == who, "retraction index mismatch"); - - Self::remove_voter(&who, index, voters); - >::unreserve(&who, Self::voting_bond()); - Ok(()) - } - - /// Submit oneself for candidacy. - /// - /// Account must have enough transferrable funds in it to pay the bond. - fn submit_candidacy(origin: T::Origin, slot: u32) -> Result { - let who = ensure_signed(origin)?; - - ensure!(!Self::is_a_candidate(&who), "duplicate candidate submission"); - let slot = slot as usize; - let count = Self::candidate_count() as usize; - let candidates = Self::candidates(); - ensure!( - (slot == count && count == candidates.len()) || - (slot < candidates.len() && candidates[slot] == T::AccountId::default()), - "invalid candidate slot" - ); - // NOTE: This must be last as it has side-effects. - >::reserve(&who, Self::candidacy_bond()) - .map_err(|_| "candidate has not enough funds")?; - - >::insert(&who, (Self::vote_index(), slot as u32)); - let mut candidates = candidates; - if slot == candidates.len() { - candidates.push(who); - } else { - candidates[slot] = who; - } - >::put(candidates); - >::put(count as u32 + 1); - Ok(()) - } - - /// 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()`` - /// `signed` should have at least - fn present_winner( - origin: T::Origin, - candidate: Address, - total: T::Balance, - index: VoteIndex - ) -> Result { - let who = ensure_signed(origin)?; - - let candidate = >::lookup(candidate)?; - ensure!(index == Self::vote_index(), "index not current"); - let (_, _, expiring) = Self::next_finalise().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() * T::Balance::sa(voters.len() as u64); - ensure!(>::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"); - - if let Some(p) = Self::active_council().iter().position(|&(ref c, _)| c == &candidate) { - ensure!(p < expiring.len(), "candidate must not form a duplicated member if elected"); - } - - 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, - }) - .fold(Zero::zero(), |acc, n| acc + n); - let dupe = leaderboard.iter().find(|&&(_, ref c)| c == &candidate).is_some(); - if total == actual_total && !dupe { - // insert into leaderboard - leaderboard[0] = (total, candidate); - leaderboard.sort_by_key(|&(t, _)| t); - >::put(leaderboard); - Ok(()) - } else { - // we can rest assured it will be Ok since we checked `can_slash` earlier; still - // better safe than sorry. - let _ = >::slash(&who, bad_presentation_punishment); - Err(if dupe { "duplicate presentation" } else { "incorrect total" }) - } - } - - /// Set the desired member count; if lower than the current count, then seats will not be up - /// election when they expire. If more, then a new vote will be started if one is not already - /// in progress. - fn set_desired_seats(origin: T::Origin, count: u32) -> Result { - ensure_root(origin)?; - >::put(count); - Ok(()) - } - - /// Remove a particular member. A tally will happen instantly (if not already in a presentation - /// period) to fill the seat if removal means that the desired members are not met. - /// This is effective immediately. - fn remove_member(origin: T::Origin, who: Address) -> Result { - ensure_root(origin)?; - let who = >::lookup(who)?; - let new_council: Vec<(T::AccountId, T::BlockNumber)> = Self::active_council() - .into_iter() - .filter(|i| i.0 != who) - .collect(); - >::put(new_council); - Ok(()) - } - - /// Set the presentation duration. If there is currently a vote being presented for, will - /// invoke `finalise_vote`. - fn set_presentation_duration(origin: T::Origin, count: T::BlockNumber) -> Result { - ensure_root(origin)?; - >::put(count); - Ok(()) - } - - /// Set the presentation duration. If there is current a vote being presented for, will - /// invoke `finalise_vote`. - fn set_term_duration(origin: T::Origin, count: T::BlockNumber) -> Result { - ensure_root(origin)?; - >::put(count); - Ok(()) - } - - // private - - /// Check there's nothing to do this block - fn end_block(block_number: T::BlockNumber) -> Result { - if (block_number % Self::voting_period()).is_zero() { - if let Some(number) = Self::next_tally() { - if block_number == number { - Self::start_tally(); - } - } - } - if let Some((number, _, _)) = Self::next_finalise() { - if block_number == number { - Self::finalise_tally()? - } - } - 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); - } - - /// Close the voting, snapshot the staking and 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::>(); - if active_council.len() - expiring.len() < desired_seats { - let empty_seats = desired_seats - (active_council.len() - expiring.len()); - >::put((number + Self::presentation_duration(), empty_seats as u32, expiring)); - - let voters = Self::voters(); - let votes = voters.iter().map(>::total_balance).collect::>(); - >::put(votes); - - // initialise leaderboard. - let leaderboard_size = empty_seats + Self::carry_count() as usize; - >::put(vec![(T::Balance::zero(), T::AccountId::default()); leaderboard_size]); - - Self::deposit_event(RawEvent::TallyStarted); - } - } - - /// Finalise the vote, removing each of the `removals` and inserting `seats` of the most approved - /// candidates in their place. If the total council members is less than the desired membership - /// a new vote is started. - /// Clears all presented candidates, returning the bond of the elected ones. - fn finalise_tally() -> Result { - >::kill(); - let (_, coming, expiring): (T::BlockNumber, u32, Vec) = - >::take().ok_or("finalise can only be called after a tally is started.")?; - let leaderboard: Vec<(T::Balance, T::AccountId)> = >::take().unwrap_or_default(); - let new_expiry = >::block_number() + Self::term_duration(); - - // return bond to winners. - let candidacy_bond = Self::candidacy_bond(); - for &(_, ref w) in leaderboard.iter() - .rev() - .take_while(|&&(b, _)| !b.is_zero()) - .take(coming as usize) - { - >::unreserve(w, candidacy_bond); - } - - // set the new council. - let mut new_council: Vec<_> = Self::active_council() - .into_iter() - .skip(expiring.len()) - .chain(leaderboard.iter() - .rev() - .take_while(|&&(b, _)| !b.is_zero()) - .take(coming as usize) - .cloned() - .map(|(_, a)| (a, new_expiry))) - .collect(); - new_council.sort_by_key(|&(_, expiry)| expiry); - >::put(new_council); - - // clear all except runners-up from candidate list. - let candidates = Self::candidates(); - let mut new_candidates = vec![T::AccountId::default(); candidates.len()]; // shrink later. - let runners_up = leaderboard.into_iter() - .rev() - .take_while(|&(b, _)| !b.is_zero()) - .skip(coming as usize) - .filter_map(|(_, a)| Self::candidate_reg_info(&a).map(|i| (a, i.1))); - let mut count = 0u32; - for (address, slot) in runners_up { - new_candidates[slot as usize] = address; - count += 1; - } - for (old, new) in candidates.iter().zip(new_candidates.iter()) { - if old != new { - // removed - kill it - >::remove(old); - } - } - // discard any superfluous slots. - if let Some(last_index) = new_candidates.iter().rposition(|c| *c != T::AccountId::default()) { - new_candidates.truncate(last_index + 1); - } - - Self::deposit_event(RawEvent::TallyFinalised); - - >::put(new_candidates); - >::put(count); - >::put(Self::vote_index() + 1); - Ok(()) - } -} - -impl OnFinalise for Module { - fn on_finalise(n: T::BlockNumber) { - if let Err(e) = Self::end_block(n) { - print("Guru meditation"); - print(e); - } - } -} +pub use seats::{Trait, Module, RawEvent, Event, VoteIndex}; #[cfg(feature = "std")] #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] -pub struct GenesisConfig { +pub struct GenesisConfig { // for the voting onto the council pub candidacy_bond: T::Balance, pub voter_bond: T::Balance, @@ -624,7 +77,7 @@ pub struct GenesisConfig { } #[cfg(feature = "std")] -impl Default for GenesisConfig { +impl Default for GenesisConfig { fn default() -> Self { GenesisConfig { candidacy_bond: T::Balance::sa(9), @@ -644,22 +97,22 @@ impl Default for GenesisConfig { } #[cfg(feature = "std")] -impl primitives::BuildStorage for GenesisConfig +impl primitives::BuildStorage for GenesisConfig { fn build_storage(self) -> ::std::result::Result, Vec>, String> { use codec::Encode; Ok(map![ - Self::hash(>::key()).to_vec() => self.candidacy_bond.encode(), - Self::hash(>::key()).to_vec() => self.voter_bond.encode(), - Self::hash(>::key()).to_vec() => self.present_slash_per_voter.encode(), - Self::hash(>::key()).to_vec() => self.carry_count.encode(), - Self::hash(>::key()).to_vec() => self.presentation_duration.encode(), - Self::hash(>::key()).to_vec() => self.approval_voting_period.encode(), - Self::hash(>::key()).to_vec() => self.term_duration.encode(), - Self::hash(>::key()).to_vec() => self.desired_seats.encode(), - Self::hash(>::key()).to_vec() => self.inactive_grace_period.encode(), - Self::hash(>::key()).to_vec() => self.active_council.encode(), + Self::hash(>::key()).to_vec() => self.candidacy_bond.encode(), + Self::hash(>::key()).to_vec() => self.voter_bond.encode(), + Self::hash(>::key()).to_vec() => self.present_slash_per_voter.encode(), + Self::hash(>::key()).to_vec() => self.carry_count.encode(), + Self::hash(>::key()).to_vec() => self.presentation_duration.encode(), + Self::hash(>::key()).to_vec() => self.approval_voting_period.encode(), + Self::hash(>::key()).to_vec() => self.term_duration.encode(), + Self::hash(>::key()).to_vec() => self.desired_seats.encode(), + Self::hash(>::key()).to_vec() => self.inactive_grace_period.encode(), + Self::hash(>::key()).to_vec() => self.active_council.encode(), Self::hash(>::key()).to_vec() => self.cooloff_period.encode(), Self::hash(>::key()).to_vec() => self.voting_period.encode(), @@ -674,11 +127,11 @@ mod tests { pub use super::*; pub use runtime_io::with_externalities; pub use substrate_primitives::H256; - use primitives::BuildStorage; - use primitives::traits::{BlakeTwo256}; - use primitives::testing::{Digest, Header}; - use substrate_primitives::KeccakHasher; - use motions; + pub use primitives::BuildStorage; + pub use primitives::traits::{BlakeTwo256}; + pub use primitives::testing::{Digest, Header}; + pub use substrate_primitives::KeccakHasher; + pub use {seats, motions, voting}; impl_outer_origin! { pub enum Origin for Test { @@ -688,7 +141,7 @@ mod tests { impl_outer_event! { pub enum Event for Test { - balances, democracy, motions + balances, democracy, seats, voting, motions } } @@ -724,8 +177,8 @@ mod tests { type Proposal = Call; type Event = Event; } - impl Trait for Test { - type Event = (); + impl seats::Trait for Test { + type Event = Event; } impl motions::Trait for Test { type Origin = Origin; @@ -733,7 +186,7 @@ mod tests { type Event = Event; } impl voting::Trait for Test { - type Event = (); + type Event = Event; } pub fn new_test_ext(with_council: bool) -> runtime_io::TestExternalities { @@ -776,789 +229,7 @@ mod tests { pub type System = system::Module; pub type Balances = balances::Module; pub type Democracy = democracy::Module; - pub type Council = Module; + pub type Council = seats::Module; pub type CouncilVoting = voting::Module; - - #[test] - fn params_should_work() { - with_externalities(&mut new_test_ext(false), || { - 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::present_slash_per_voter(), 1); - assert_eq!(Council::presentation_duration(), 2); - assert_eq!(Council::voting_period(), 4); - assert_eq!(Council::term_duration(), 5); - assert_eq!(Council::desired_seats(), 2); - assert_eq!(Council::carry_count(), 2); - - assert_eq!(Council::active_council(), vec![]); - assert_eq!(Council::next_tally(), Some(4)); - assert_eq!(Council::presentation_active(), false); - assert_eq!(Council::next_finalise(), None); - - assert_eq!(Council::candidates(), Vec::::new()); - 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![]); - }); - } - - #[test] - fn simple_candidate_submission_should_work() { - with_externalities(&mut new_test_ext(false), || { - System::set_block_number(1); - assert_eq!(Council::candidates(), Vec::::new()); - assert_eq!(Council::candidate_reg_info(1), None); - assert_eq!(Council::candidate_reg_info(2), None); - assert_eq!(Council::is_a_candidate(&1), false); - assert_eq!(Council::is_a_candidate(&2), false); - - assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); - assert_eq!(Council::candidates(), vec![1]); - assert_eq!(Council::candidate_reg_info(1), Some((0, 0))); - assert_eq!(Council::candidate_reg_info(2), None); - assert_eq!(Council::is_a_candidate(&1), true); - assert_eq!(Council::is_a_candidate(&2), false); - - assert_ok!(Council::submit_candidacy(Origin::signed(2), 1)); - assert_eq!(Council::candidates(), vec![1, 2]); - assert_eq!(Council::candidate_reg_info(1), Some((0, 0))); - assert_eq!(Council::candidate_reg_info(2), Some((0, 1))); - assert_eq!(Council::is_a_candidate(&1), true); - assert_eq!(Council::is_a_candidate(&2), true); - }); - } - - fn new_test_ext_with_candidate_holes() -> runtime_io::TestExternalities { - let mut t = new_test_ext(false); - with_externalities(&mut t, || { - >::put(vec![0, 0, 1]); - >::put(1); - >::insert(1, (0, 2)); - }); - t - } - - #[test] - fn candidate_submission_using_free_slot_should_work() { - let mut t = new_test_ext_with_candidate_holes(); - - with_externalities(&mut t, || { - System::set_block_number(1); - assert_eq!(Council::candidates(), vec![0, 0, 1]); - - assert_ok!(Council::submit_candidacy(Origin::signed(2), 1)); - assert_eq!(Council::candidates(), vec![0, 2, 1]); - - assert_ok!(Council::submit_candidacy(Origin::signed(3), 0)); - assert_eq!(Council::candidates(), vec![3, 2, 1]); - }); - } - - #[test] - fn candidate_submission_using_alternative_free_slot_should_work() { - let mut t = new_test_ext_with_candidate_holes(); - - with_externalities(&mut t, || { - System::set_block_number(1); - assert_eq!(Council::candidates(), vec![0, 0, 1]); - - assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); - assert_eq!(Council::candidates(), vec![2, 0, 1]); - - assert_ok!(Council::submit_candidacy(Origin::signed(3), 1)); - assert_eq!(Council::candidates(), vec![2, 3, 1]); - }); - } - - #[test] - fn candidate_submission_not_using_free_slot_should_not_work() { - with_externalities(&mut new_test_ext_with_candidate_holes(), || { - System::set_block_number(1); - assert_noop!(Council::submit_candidacy(Origin::signed(4), 3), "invalid candidate slot"); - }); - } - - #[test] - fn bad_candidate_slot_submission_should_not_work() { - with_externalities(&mut new_test_ext(false), || { - System::set_block_number(1); - assert_eq!(Council::candidates(), Vec::::new()); - assert_noop!(Council::submit_candidacy(Origin::signed(1), 1), "invalid candidate slot"); - }); - } - - #[test] - fn non_free_candidate_slot_submission_should_not_work() { - with_externalities(&mut new_test_ext(false), || { - System::set_block_number(1); - assert_eq!(Council::candidates(), Vec::::new()); - assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); - assert_eq!(Council::candidates(), vec![1]); - assert_noop!(Council::submit_candidacy(Origin::signed(2), 0), "invalid candidate slot"); - }); - } - - #[test] - fn dupe_candidate_submission_should_not_work() { - with_externalities(&mut new_test_ext(false), || { - System::set_block_number(1); - assert_eq!(Council::candidates(), Vec::::new()); - assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); - assert_eq!(Council::candidates(), vec![1]); - assert_noop!(Council::submit_candidacy(Origin::signed(1), 1), "duplicate candidate submission"); - }); - } - - #[test] - fn poor_candidate_submission_should_not_work() { - with_externalities(&mut new_test_ext(false), || { - 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 voting_should_work() { - with_externalities(&mut new_test_ext(false), || { - 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_eq!(Council::approvals_of(1), vec![true]); - assert_eq!(Council::approvals_of(4), vec![true]); - assert_eq!(Council::voters(), 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_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::voters(), vec![1, 4, 2, 3]); - }); - } - - #[test] - fn resubmitting_voting_should_work() { - with_externalities(&mut new_test_ext(false), || { - 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_eq!(Council::approvals_of(4), vec![true]); - - 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(4), vec![true, false, true], 0)); - - assert_eq!(Council::approvals_of(4), vec![true, false, true]); - }); - } - - #[test] - fn retracting_voter_should_work() { - with_externalities(&mut new_test_ext(false), || { - System::set_block_number(1); - - assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); - 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(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_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_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_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_ok!(Council::retract_voter(Origin::signed(3), 1)); - - 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]); - }); - } - - #[test] - fn invalid_retraction_index_should_not_work() { - with_externalities(&mut new_test_ext(false), || { - 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_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), || { - 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_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), || { - 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_noop!(Council::retract_voter(Origin::signed(2), 0), "cannot retract non-voter"); - }); - } - - #[test] - fn simple_tally_should_work() { - with_externalities(&mut new_test_ext(false), || { - 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::end_block(System::block_number())); - - System::set_block_number(6); - assert!(Council::presentation_active()); - assert_eq!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0), Ok(())); - assert_eq!(Council::present_winner(Origin::signed(4), 5.into(), 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!(!Council::presentation_active()); - assert_eq!(Council::active_council(), vec![(5, 11), (2, 11)]); - - 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)); - }); - } - - #[test] - fn double_presentations_should_be_punished() { - with_externalities(&mut new_test_ext(false), || { - 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0)); - assert_eq!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0), Err("duplicate presentation")); - assert_ok!(Council::end_block(System::block_number())); - - assert_eq!(Council::active_council(), vec![(5, 11), (2, 11)]); - assert_eq!(Balances::total_balance(&4), 38); - }); - } - - #[test] - fn retracting_inactive_voter_should_work() { - with_externalities(&mut new_test_ext(false), || { - 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); - assert_ok!(Council::end_block(System::block_number())); - - 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::end_block(System::block_number())); - - System::set_block_number(10); - assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 1)); - 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, - 2.into(), Council::voters().iter().position(|&i| i == 2).unwrap() as u32, - 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); - }); - } - - #[test] - fn presenting_for_double_election_should_not_work() { - with_externalities(&mut new_test_ext(false), || { - 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); - assert_ok!(Council::end_block(System::block_number())); - - System::set_block_number(8); - assert_eq!(Council::submit_candidacy(Origin::signed(2), 0), Ok(())); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 1)); - assert_ok!(Council::end_block(System::block_number())); - - System::set_block_number(10); - assert_noop!(Council::present_winner(Origin::signed(4), 2.into(), 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), || { - 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); - assert_ok!(Council::end_block(System::block_number())); - - 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::end_block(System::block_number())); - - System::set_block_number(10); - assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 1)); - assert_ok!(Council::end_block(System::block_number())); - - System::set_block_number(11); - 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, - 2.into(), Council::voters().iter().position(|&i| i == 2).unwrap() as u32, - 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); - }); - } - - #[test] - fn retracting_inactive_voter_with_bad_reporter_index_should_not_work() { - with_externalities(&mut new_test_ext(false), || { - 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); - assert_ok!(Council::end_block(System::block_number())); - - 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::end_block(System::block_number())); - - System::set_block_number(10); - assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 1)); - assert_ok!(Council::end_block(System::block_number())); - - assert_noop!(Council::reap_inactive_voter(Origin::signed(2), - 42, - 2.into(), Council::voters().iter().position(|&i| i == 2).unwrap() as u32, - 2 - ), "bad reporter index"); - }); - } - - #[test] - fn retracting_inactive_voter_with_bad_target_index_should_not_work() { - with_externalities(&mut new_test_ext(false), || { - 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); - assert_ok!(Council::end_block(System::block_number())); - - 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::end_block(System::block_number())); - - System::set_block_number(10); - assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 1)); - 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, - 2.into(), 42, - 2 - ), "bad target index"); - }); - } - - #[test] - fn attempting_to_retract_active_voter_should_slash_reporter() { - with_externalities(&mut new_test_ext(false), || { - 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0)); - assert_ok!(Council::end_block(System::block_number())); - - System::set_block_number(8); - assert_ok!(Council::set_desired_seats(Origin::ROOT, 3)); - assert_ok!(Council::end_block(System::block_number())); - - System::set_block_number(10); - assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 1)); - assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30, 1)); - assert_ok!(Council::end_block(System::block_number())); - - assert_ok!(Council::reap_inactive_voter(Origin::signed(4), - Council::voters().iter().position(|&i| i == 4).unwrap() as u32, - 2.into(), Council::voters().iter().position(|&i| i == 2).unwrap() as u32, - 2 - )); - - assert_eq!(Council::voters(), vec![2, 3, 5]); - assert_eq!(Council::approvals_of(4).len(), 0); - assert_eq!(Balances::total_balance(&4), 37); - }); - } - - #[test] - fn attempting_to_retract_inactive_voter_by_nonvoter_should_not_work() { - with_externalities(&mut new_test_ext(false), || { - 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); - assert_ok!(Council::end_block(System::block_number())); - - 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::end_block(System::block_number())); - - System::set_block_number(10); - assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 1)); - assert_ok!(Council::end_block(System::block_number())); - - assert_noop!(Council::reap_inactive_voter(Origin::signed(4), - 0, - 2.into(), Council::voters().iter().position(|&i| i == 2).unwrap() as u32, - 2 - ), "reporter must be a voter"); - }); - } - - #[test] - fn presenting_loser_should_not_work() { - with_externalities(&mut new_test_ext(false), || { - 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::submit_candidacy(Origin::signed(2), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 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::submit_candidacy(Origin::signed(4), 3)); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_ok!(Council::present_winner(Origin::signed(4), 1.into(), 60, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0)); - assert_noop!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0), "candidate not worthy of leaderboard"); - }); - } - - #[test] - fn presenting_loser_first_should_not_matter() { - with_externalities(&mut new_test_ext(false), || { - 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::submit_candidacy(Origin::signed(2), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 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::submit_candidacy(Origin::signed(4), 3)); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 1.into(), 60, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0)); - - assert_eq!(Council::leaderboard(), Some(vec![ - (30, 3), - (40, 4), - (50, 5), - (60, 1) - ])); - }); - } - - #[test] - fn present_outside_of_presentation_period_should_not_work() { - with_externalities(&mut new_test_ext(false), || { - System::set_block_number(4); - assert!(!Council::presentation_active()); - assert_noop!(Council::present_winner(Origin::signed(5), 5.into(), 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), || { - 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_noop!(Council::present_winner(Origin::signed(4), 2.into(), 20, 1), "index not current"); - }); - } - - #[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()); - - 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())); - - 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.into(), 20, 0), "presenter must have sufficient slashable funds"); - }); - } - - #[test] - fn invalid_present_tally_should_slash() { - with_externalities(&mut new_test_ext(false), || { - 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_err!(Council::present_winner(Origin::signed(4), 2.into(), 80, 0), "incorrect total"); - - assert_eq!(Balances::total_balance(&4), 38); - }); - } - - #[test] - fn runners_up_should_be_kept() { - with_externalities(&mut new_test_ext(false), || { - 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::submit_candidacy(Origin::signed(2), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 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::submit_candidacy(Origin::signed(4), 3)); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 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::end_block(System::block_number())); - - System::set_block_number(6); - assert!(Council::presentation_active()); - assert_ok!(Council::present_winner(Origin::signed(4), 1.into(), 60, 0)); - assert_eq!(Council::leaderboard(), Some(vec![ - (0, 0), - (0, 0), - (0, 0), - (60, 1) - ])); - assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0)); - assert_eq!(Council::leaderboard(), Some(vec![ - (30, 3), - (40, 4), - (50, 5), - (60, 1) - ])); - - assert_ok!(Council::end_block(System::block_number())); - - assert!(!Council::presentation_active()); - assert_eq!(Council::active_council(), vec![(1, 11), (5, 11)]); - - assert!(!Council::is_a_candidate(&1)); - assert!(!Council::is_a_candidate(&5)); - assert!(!Council::is_a_candidate(&2)); - 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::candidate_reg_info(3), Some((0, 2))); - assert_eq!(Council::candidate_reg_info(4), Some((0, 3))); - }); - } - - #[test] - fn second_tally_should_use_runners_up() { - with_externalities(&mut new_test_ext(false), || { - 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::submit_candidacy(Origin::signed(2), 1)); - assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 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::submit_candidacy(Origin::signed(4), 3)); - assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 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::end_block(System::block_number())); - - System::set_block_number(6); - assert_ok!(Council::present_winner(Origin::signed(4), 1.into(), 60, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40, 0)); - assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0)); - 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_desired_seats(Origin::ROOT, 3)); - assert_ok!(Council::end_block(System::block_number())); - - System::set_block_number(10); - assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 90, 1)); - assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40, 1)); - assert_ok!(Council::end_block(System::block_number())); - - assert!(!Council::presentation_active()); - assert_eq!(Council::active_council(), vec![(1, 11), (5, 11), (3, 15)]); - - assert!(!Council::is_a_candidate(&1)); - assert!(!Council::is_a_candidate(&2)); - assert!(!Council::is_a_candidate(&3)); - 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::candidate_reg_info(4), Some((0, 3))); - }); - } -} + pub type CouncilMotions = motions::Module; +} \ No newline at end of file diff --git a/substrate/runtime/council/src/motions.rs b/substrate/runtime/council/src/motions.rs index 0993d13949c98..c4d3443ef7227 100644 --- a/substrate/runtime/council/src/motions.rs +++ b/substrate/runtime/council/src/motions.rs @@ -218,13 +218,12 @@ impl EnsureOrigin for EnsureMembers #[cfg(test)] mod tests { use super::*; + use super::RawEvent; use ::tests::*; use ::tests::{Call, Origin, Event as OuterEvent}; use substrate_runtime_support::Hashable; use system::{EventRecord, Phase}; - type CouncilMotions = super::Module; - #[test] fn motions_basic_environment_works() { with_externalities(&mut new_test_ext(true), || { @@ -252,7 +251,7 @@ mod tests { assert_eq!(System::events(), vec![ EventRecord { phase: Phase::ApplyExtrinsic(0), - event: OuterEvent::council_motions(RawEvent::Proposed(1, 0, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), 3)) + event: OuterEvent::motions(RawEvent::Proposed(1, 0, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), 3)) } ]); }); @@ -305,11 +304,11 @@ mod tests { assert_eq!(System::events(), vec![ EventRecord { phase: Phase::ApplyExtrinsic(0), - event: OuterEvent::council_motions(RawEvent::Proposed(1, 0, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), 2)) + event: OuterEvent::motions(RawEvent::Proposed(1, 0, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), 2)) }, EventRecord { phase: Phase::ApplyExtrinsic(0), - event: OuterEvent::council_motions(RawEvent::Voted(1, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), false, 0, 1)) + event: OuterEvent::motions(RawEvent::Voted(1, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), false, 0, 1)) } ]); }); @@ -327,15 +326,15 @@ mod tests { assert_eq!(System::events(), vec![ EventRecord { phase: Phase::ApplyExtrinsic(0), - event: OuterEvent::council_motions(RawEvent::Proposed(1, 0, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), 3)) + event: OuterEvent::motions(RawEvent::Proposed(1, 0, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), 3)) }, EventRecord { phase: Phase::ApplyExtrinsic(0), - event: OuterEvent::council_motions(RawEvent::Voted(2, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), false, 1, 1)) + event: OuterEvent::motions(RawEvent::Voted(2, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), false, 1, 1)) }, EventRecord { phase: Phase::ApplyExtrinsic(0), - event: OuterEvent::council_motions(RawEvent::Disapproved(hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into())) + event: OuterEvent::motions(RawEvent::Disapproved(hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into())) } ]); }); @@ -353,19 +352,19 @@ mod tests { assert_eq!(System::events(), vec![ EventRecord { phase: Phase::ApplyExtrinsic(0), - event: OuterEvent::council_motions(RawEvent::Proposed(1, 0, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), 2)) + event: OuterEvent::motions(RawEvent::Proposed(1, 0, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), 2)) }, EventRecord { phase: Phase::ApplyExtrinsic(0), - event: OuterEvent::council_motions(RawEvent::Voted(2, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), true, 2, 0)) + event: OuterEvent::motions(RawEvent::Voted(2, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), true, 2, 0)) }, EventRecord { phase: Phase::ApplyExtrinsic(0), - event: OuterEvent::council_motions(RawEvent::Approved(hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into())) + event: OuterEvent::motions(RawEvent::Approved(hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into())) }, EventRecord { phase: Phase::ApplyExtrinsic(0), - event: OuterEvent::council_motions(RawEvent::Executed(hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), false)) + event: OuterEvent::motions(RawEvent::Executed(hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), false)) } ]); }); diff --git a/substrate/runtime/council/src/seats.rs b/substrate/runtime/council/src/seats.rs new file mode 100644 index 0000000000000..a5f8422de49ad --- /dev/null +++ b/substrate/runtime/council/src/seats.rs @@ -0,0 +1,1362 @@ +// Copyright 2017 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Council system: Handles the voting in and maintenance of council members. + +use rstd::prelude::*; +#[cfg(feature = "std")] +use primitives::traits::{Zero, One, As, Lookup, OnFinalise}; +use runtime_io::print; +use substrate_runtime_support::{StorageValue, StorageMap, dispatch::Result}; +use democracy; +use balances::{self, address::Address}; +use system::{self, ensure_signed, ensure_root}; + +// no polynomial attacks: +// +// all unbonded public operations should be constant time. +// all other public operations must be linear time in terms of prior public operations and: +// - those "valid" ones that cost nothing be limited to a constant number per single protected operation +// - the rest costing the same order as the computational complexity +// all protected operations must complete in at most O(public operations) +// +// we assume "beneficial" transactions will have the same access as attack transactions. +// +// any storage requirements should be bonded by the same order as the volume. + +// public operations: +// - express approvals (you pay in a "voter" bond the first time you do this; O(1); one extra DB entry, one DB change) +// - remove active voter (you get your "voter" bond back; O(1); one fewer DB entry, one DB change) +// - remove inactive voter (either you or the target is removed; if the target, you get their "voter" bond back; O(1); one fewer DB entry, one DB change) +// - submit candidacy (you pay a "candidate" bond; O(1); one extra DB entry, two DB changes) +// - present winner/runner-up (you may pay a "presentation" bond of O(voters) if the presentation is invalid; O(voters) compute; ) +// protected operations: +// - remove candidacy (remove all votes for a candidate) (one fewer DB entry, two DB changes) + +// to avoid a potentially problematic case of not-enough approvals prior to voting causing a +// back-to-back votes that have no way of ending, then there's a forced grace period between votes. +// to keep the system as stateless as possible (making it a bit easier to reason about), we just +// restrict when votes can begin to blocks that lie on boundaries (`voting_period`). + +// for an approval vote of C councilers: + +// top K runners-up are maintained between votes. all others are discarded. +// - candidate removed & bond returned when elected. +// - candidate removed & bond burned when discarded. + +// at the point that the vote ends (), all voters' balances are snapshotted. + +// for B blocks following, there's a counting period whereby each of the candidates that believe +// they fall in the top K+C voted can present themselves. they get the total stake +// recorded (based on the snapshot); an ordered list is maintained (the leaderboard). Noone may +// present themselves that, if elected, would result in being included twice on the council +// (important since existing councilers will will have their approval votes as it may be that they +// don't get removed), nor if existing presenters would mean they're not in the top K+C. + +// following B blocks, the top C candidates are elected and have their bond returned. the top C +// candidates and all other candidates beyond the top C+K are cleared. + +// vote-clearing happens lazily; for an approval to count, the most recent vote at the time of the +// voter's most recent vote must be no later than the most recent vote at the time that the +// candidate in the approval position was registered there. as candidates are removed from the +// register and others join in their place, this prevent an approval meant for an earlier candidate +// being used to elect a new candidate. + +// the candidate list increases as needed, but the contents (though not really the capacity) reduce +// after each vote as all but K entries are cleared. newly registering candidates must use cleared +// entries before they increase the capacity. + +pub type VoteIndex = u32; + +pub trait Trait: democracy::Trait { + type Event: From> + Into<::Event>; +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + fn set_approvals(origin, votes: Vec, index: VoteIndex) -> Result; + fn reap_inactive_voter(origin, reporter_index: u32, who: Address, who_index: u32, assumed_vote_index: VoteIndex) -> Result; + fn retract_voter(origin, index: u32) -> Result; + fn submit_candidacy(origin, slot: u32) -> Result; + fn present_winner(origin, candidate: Address, total: T::Balance, index: VoteIndex) -> Result; + + fn set_desired_seats(origin, count: u32) -> Result; + fn remove_member(origin, who: Address) -> Result; + fn set_presentation_duration(origin, count: T::BlockNumber) -> Result; + fn set_term_duration(origin, count: T::BlockNumber) -> Result; + } +} + +decl_storage! { + trait Store for Module as Council { + + // parameters + /// How much should be locked up in order to submit one's candidacy. + pub CandidacyBond get(candidacy_bond): required T::Balance; + /// How much should be locked up in order to be able to submit votes. + pub VotingBond get(voting_bond): required T::Balance; + /// The punishment, per voter, if you provide an invalid presentation. + pub PresentSlashPerVoter get(present_slash_per_voter): required T::Balance; + /// How many runners-up should have their approvals persist until the next vote. + pub CarryCount get(carry_count): required u32; + /// How long to give each top candidate to present themselves after the vote ends. + pub PresentationDuration get(presentation_duration): required T::BlockNumber; + /// How many votes need to go by after a voter's last vote before they can be reaped if their + /// approvals are moot. + pub InactiveGracePeriod get(inactivity_grace_period): required VoteIndex; + /// How often (in blocks) to check for new votes. + pub VotingPeriod get(voting_period): required T::BlockNumber; + /// How long each position is active for. + pub TermDuration get(term_duration): required T::BlockNumber; + /// Number of accounts that should be sitting on the council. + pub DesiredSeats get(desired_seats): required u32; + + // permanent state (always relevant, changes only at the finalisation of voting) + /// The current council. When there's a vote going on, this should still be used for executive + /// matters. + pub ActiveCouncil get(active_council): default Vec<(T::AccountId, T::BlockNumber)>; + /// The total number of votes that have happened or are in progress. + pub VoteCount get(vote_index): default VoteIndex; + + // persistent state (always relevant, changes constantly) + /// The last cleared vote index that this voter was last active at. + pub ApprovalsOf get(approvals_of): default map [ T::AccountId => 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 => (VoteIndex, u32) ]; + /// The last cleared vote index that this voter was last active at. + pub LastActiveOf get(voter_last_active): map [ T::AccountId => VoteIndex ]; + /// The present voter list. + pub Voters get(voters): default Vec; + /// The present candidate list. + pub Candidates get(candidates): default Vec; // has holes + pub CandidateCount get(candidate_count): default u32; + + // temporary state (only relevant during finalisation/presentation) + /// The accounts holding the seats that will become free on the next tally. + pub NextFinalise get(next_finalise): (T::BlockNumber, u32, Vec); + /// The stakes as they were at the point that the vote ended. + pub SnapshotedStakes get(snapshoted_stakes): required Vec; + /// Get the leaderboard if we;re in the presentation phase. + pub Leaderboard get(leaderboard): Vec<(T::Balance, T::AccountId)>; // ORDERED low -> high + } +} + +pub type Event = RawEvent<::AccountId>; + +/// An event in this module. +#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] +#[derive(Encode, Decode, PartialEq, Eq, Clone)] +pub enum RawEvent { + /// reaped voter, reaper + VoterReaped(AccountId, AccountId), + /// slashed reaper + BadReaperSlashed(AccountId), + /// A tally (for approval votes of council seat(s)) has started. + TallyStarted, + /// A tally (for approval votes of council seat(s)) has ended (with one or more new members). + TallyFinalised, +} + +impl From> for () { + fn from(_: RawEvent) -> () { () } +} + +impl Module { + + /// Deposit one of this module's events. + fn deposit_event(event: Event) { + >::deposit_event(::Event::from(event).into()); + } + + // exposed immutables. + + /// True if we're currently in a presentation period. + pub fn presentation_active() -> bool { + >::exists() + } + + /// If `who` a candidate at the moment? + pub fn is_a_candidate(who: &T::AccountId) -> bool { + >::exists(who) + } + + /// Determine the block that a vote can happen on which is no less than `n`. + pub fn next_vote_from(n: T::BlockNumber) -> T::BlockNumber { + let voting_period = Self::voting_period(); + (n + voting_period - One::one()) / voting_period * voting_period + } + + /// The block number on which the tally for the next election will happen. `None` only if the + /// desired seats of the council is zero. + pub fn next_tally() -> Option { + let desired_seats = Self::desired_seats(); + if desired_seats == 0 { + None + } else { + let c = Self::active_council(); + let (next_possible, count, coming) = + if let Some((tally_end, comers, leavers)) = Self::next_finalise() { + // if there's a tally in progress, then next tally can begin immediately afterwards + (tally_end, c.len() - leavers.len() + comers as usize, comers) + } else { + (>::block_number(), c.len(), 0) + }; + if count < desired_seats as usize { + Some(next_possible) + } else { + // next tally begins once enough council members expire to bring members below desired. + if desired_seats <= coming { + // the entire amount of desired seats is less than those new members - we'll have + // to wait until they expire. + Some(next_possible + Self::term_duration()) + } else { + Some(c[c.len() - (desired_seats - coming) as usize].1) + } + }.map(Self::next_vote_from) + } + } + + // dispatch + + /// Set candidate approvals. Approval slots stay valid as long as candidates in those slots + /// are registered. + fn set_approvals(origin: T::Origin, votes: Vec, index: VoteIndex) -> Result { + let who = ensure_signed(origin)?; + + ensure!(!Self::presentation_active(), "no approval changes during presentation period"); + ensure!(index == Self::vote_index(), "incorrect vote index"); + if !>::exists(&who) { + // not yet a voter - deduct bond. + // NOTE: this must be the last potential bailer, since it changes state. + >::reserve(&who, Self::voting_bond())?; + + >::put({ + let mut v = Self::voters(); + v.push(who.clone()); + v + }); + } + >::insert(&who, index); + >::insert(&who, votes); + Ok(()) + } + + /// 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. + /// + /// May be called by anyone. Returns the voter deposit to `signed`. + fn reap_inactive_voter( + origin: T::Origin, + reporter_index: u32, + who: Address, + who_index: u32, + assumed_vote_index: VoteIndex + ) -> Result { + let reporter = ensure_signed(origin)?; + + let who = >::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!(assumed_vote_index == Self::vote_index(), "vote index not current"); + ensure!(last_active < assumed_vote_index - Self::inactivity_grace_period(), "cannot reap during grace perid"); + let voters = Self::voters(); + 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"); + + // will definitely kill one of signed or who now. + + let valid = !Self::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*/ + ); + + Self::remove_voter( + if valid { &who } else { &reporter }, + if valid { who_index } else { reporter_index }, + voters + ); + if valid { + // This only fails if `who` 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. + >::repatriate_reserved(&who, &reporter, Self::voting_bond())?; + Self::deposit_event(RawEvent::VoterReaped(who, reporter)); + } else { + >::slash_reserved(&reporter, Self::voting_bond()); + Self::deposit_event(RawEvent::BadReaperSlashed(reporter)); + } + Ok(()) + } + + /// Remove a voter. All votes are cancelled and the voter deposit is returned. + fn retract_voter(origin: T::Origin, index: u32) -> Result { + let who = ensure_signed(origin)?; + + ensure!(!Self::presentation_active(), "cannot retract when presenting"); + ensure!(>::exists(&who), "cannot retract non-voter"); + let voters = Self::voters(); + let index = index as usize; + ensure!(index < voters.len(), "retraction index invalid"); + ensure!(voters[index] == who, "retraction index mismatch"); + + Self::remove_voter(&who, index, voters); + >::unreserve(&who, Self::voting_bond()); + Ok(()) + } + + /// Submit oneself for candidacy. + /// + /// Account must have enough transferrable funds in it to pay the bond. + fn submit_candidacy(origin: T::Origin, slot: u32) -> Result { + let who = ensure_signed(origin)?; + + ensure!(!Self::is_a_candidate(&who), "duplicate candidate submission"); + let slot = slot as usize; + let count = Self::candidate_count() as usize; + let candidates = Self::candidates(); + ensure!( + (slot == count && count == candidates.len()) || + (slot < candidates.len() && candidates[slot] == T::AccountId::default()), + "invalid candidate slot" + ); + // NOTE: This must be last as it has side-effects. + >::reserve(&who, Self::candidacy_bond()) + .map_err(|_| "candidate has not enough funds")?; + + >::insert(&who, (Self::vote_index(), slot as u32)); + let mut candidates = candidates; + if slot == candidates.len() { + candidates.push(who); + } else { + candidates[slot] = who; + } + >::put(candidates); + >::put(count as u32 + 1); + Ok(()) + } + + /// 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()`` + /// `signed` should have at least + fn present_winner( + origin: T::Origin, + candidate: Address, + total: T::Balance, + index: VoteIndex + ) -> Result { + let who = ensure_signed(origin)?; + + let candidate = >::lookup(candidate)?; + ensure!(index == Self::vote_index(), "index not current"); + let (_, _, expiring) = Self::next_finalise().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() * T::Balance::sa(voters.len() as u64); + ensure!(>::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"); + + if let Some(p) = Self::active_council().iter().position(|&(ref c, _)| c == &candidate) { + ensure!(p < expiring.len(), "candidate must not form a duplicated member if elected"); + } + + 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, + }) + .fold(Zero::zero(), |acc, n| acc + n); + let dupe = leaderboard.iter().find(|&&(_, ref c)| c == &candidate).is_some(); + if total == actual_total && !dupe { + // insert into leaderboard + leaderboard[0] = (total, candidate); + leaderboard.sort_by_key(|&(t, _)| t); + >::put(leaderboard); + Ok(()) + } else { + // we can rest assured it will be Ok since we checked `can_slash` earlier; still + // better safe than sorry. + let _ = >::slash(&who, bad_presentation_punishment); + Err(if dupe { "duplicate presentation" } else { "incorrect total" }) + } + } + + /// Set the desired member count; if lower than the current count, then seats will not be up + /// election when they expire. If more, then a new vote will be started if one is not already + /// in progress. + fn set_desired_seats(origin: T::Origin, count: u32) -> Result { + ensure_root(origin)?; + >::put(count); + Ok(()) + } + + /// Remove a particular member. A tally will happen instantly (if not already in a presentation + /// period) to fill the seat if removal means that the desired members are not met. + /// This is effective immediately. + fn remove_member(origin: T::Origin, who: Address) -> Result { + ensure_root(origin)?; + let who = >::lookup(who)?; + let new_council: Vec<(T::AccountId, T::BlockNumber)> = Self::active_council() + .into_iter() + .filter(|i| i.0 != who) + .collect(); + >::put(new_council); + Ok(()) + } + + /// Set the presentation duration. If there is currently a vote being presented for, will + /// invoke `finalise_vote`. + fn set_presentation_duration(origin: T::Origin, count: T::BlockNumber) -> Result { + ensure_root(origin)?; + >::put(count); + Ok(()) + } + + /// Set the presentation duration. If there is current a vote being presented for, will + /// invoke `finalise_vote`. + fn set_term_duration(origin: T::Origin, count: T::BlockNumber) -> Result { + ensure_root(origin)?; + >::put(count); + Ok(()) + } + + // private + + /// Check there's nothing to do this block + fn end_block(block_number: T::BlockNumber) -> Result { + if (block_number % Self::voting_period()).is_zero() { + if let Some(number) = Self::next_tally() { + if block_number == number { + Self::start_tally(); + } + } + } + if let Some((number, _, _)) = Self::next_finalise() { + if block_number == number { + Self::finalise_tally()? + } + } + 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); + } + + /// Close the voting, snapshot the staking and 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::>(); + if active_council.len() - expiring.len() < desired_seats { + let empty_seats = desired_seats - (active_council.len() - expiring.len()); + >::put((number + Self::presentation_duration(), empty_seats as u32, expiring)); + + let voters = Self::voters(); + let votes = voters.iter().map(>::total_balance).collect::>(); + >::put(votes); + + // initialise leaderboard. + let leaderboard_size = empty_seats + Self::carry_count() as usize; + >::put(vec![(T::Balance::zero(), T::AccountId::default()); leaderboard_size]); + + Self::deposit_event(RawEvent::TallyStarted); + } + } + + /// Finalise the vote, removing each of the `removals` and inserting `seats` of the most approved + /// candidates in their place. If the total council members is less than the desired membership + /// a new vote is started. + /// Clears all presented candidates, returning the bond of the elected ones. + fn finalise_tally() -> Result { + >::kill(); + let (_, coming, expiring): (T::BlockNumber, u32, Vec) = + >::take().ok_or("finalise can only be called after a tally is started.")?; + let leaderboard: Vec<(T::Balance, T::AccountId)> = >::take().unwrap_or_default(); + let new_expiry = >::block_number() + Self::term_duration(); + + // return bond to winners. + let candidacy_bond = Self::candidacy_bond(); + for &(_, ref w) in leaderboard.iter() + .rev() + .take_while(|&&(b, _)| !b.is_zero()) + .take(coming as usize) + { + >::unreserve(w, candidacy_bond); + } + + // set the new council. + let mut new_council: Vec<_> = Self::active_council() + .into_iter() + .skip(expiring.len()) + .chain(leaderboard.iter() + .rev() + .take_while(|&&(b, _)| !b.is_zero()) + .take(coming as usize) + .cloned() + .map(|(_, a)| (a, new_expiry))) + .collect(); + new_council.sort_by_key(|&(_, expiry)| expiry); + >::put(new_council); + + // clear all except runners-up from candidate list. + let candidates = Self::candidates(); + let mut new_candidates = vec![T::AccountId::default(); candidates.len()]; // shrink later. + let runners_up = leaderboard.into_iter() + .rev() + .take_while(|&(b, _)| !b.is_zero()) + .skip(coming as usize) + .filter_map(|(_, a)| Self::candidate_reg_info(&a).map(|i| (a, i.1))); + let mut count = 0u32; + for (address, slot) in runners_up { + new_candidates[slot as usize] = address; + count += 1; + } + for (old, new) in candidates.iter().zip(new_candidates.iter()) { + if old != new { + // removed - kill it + >::remove(old); + } + } + // discard any superfluous slots. + if let Some(last_index) = new_candidates.iter().rposition(|c| *c != T::AccountId::default()) { + new_candidates.truncate(last_index + 1); + } + + Self::deposit_event(RawEvent::TallyFinalised); + + >::put(new_candidates); + >::put(count); + >::put(Self::vote_index() + 1); + Ok(()) + } +} + +impl OnFinalise for Module { + fn on_finalise(n: T::BlockNumber) { + if let Err(e) = Self::end_block(n) { + print("Guru meditation"); + print(e); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ::tests::*; + + #[test] + fn params_should_work() { + with_externalities(&mut new_test_ext(false), || { + 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::present_slash_per_voter(), 1); + assert_eq!(Council::presentation_duration(), 2); + assert_eq!(Council::voting_period(), 4); + assert_eq!(Council::term_duration(), 5); + assert_eq!(Council::desired_seats(), 2); + assert_eq!(Council::carry_count(), 2); + + assert_eq!(Council::active_council(), vec![]); + assert_eq!(Council::next_tally(), Some(4)); + assert_eq!(Council::presentation_active(), false); + assert_eq!(Council::next_finalise(), None); + + assert_eq!(Council::candidates(), Vec::::new()); + 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![]); + }); + } + + #[test] + fn simple_candidate_submission_should_work() { + with_externalities(&mut new_test_ext(false), || { + System::set_block_number(1); + assert_eq!(Council::candidates(), Vec::::new()); + assert_eq!(Council::candidate_reg_info(1), None); + assert_eq!(Council::candidate_reg_info(2), None); + assert_eq!(Council::is_a_candidate(&1), false); + assert_eq!(Council::is_a_candidate(&2), false); + + assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); + assert_eq!(Council::candidates(), vec![1]); + assert_eq!(Council::candidate_reg_info(1), Some((0, 0))); + assert_eq!(Council::candidate_reg_info(2), None); + assert_eq!(Council::is_a_candidate(&1), true); + assert_eq!(Council::is_a_candidate(&2), false); + + assert_ok!(Council::submit_candidacy(Origin::signed(2), 1)); + assert_eq!(Council::candidates(), vec![1, 2]); + assert_eq!(Council::candidate_reg_info(1), Some((0, 0))); + assert_eq!(Council::candidate_reg_info(2), Some((0, 1))); + assert_eq!(Council::is_a_candidate(&1), true); + assert_eq!(Council::is_a_candidate(&2), true); + }); + } + + fn new_test_ext_with_candidate_holes() -> runtime_io::TestExternalities { + let mut t = new_test_ext(false); + with_externalities(&mut t, || { + >::put(vec![0, 0, 1]); + >::put(1); + >::insert(1, (0, 2)); + }); + t + } + + #[test] + fn candidate_submission_using_free_slot_should_work() { + let mut t = new_test_ext_with_candidate_holes(); + + with_externalities(&mut t, || { + System::set_block_number(1); + assert_eq!(Council::candidates(), vec![0, 0, 1]); + + assert_ok!(Council::submit_candidacy(Origin::signed(2), 1)); + assert_eq!(Council::candidates(), vec![0, 2, 1]); + + assert_ok!(Council::submit_candidacy(Origin::signed(3), 0)); + assert_eq!(Council::candidates(), vec![3, 2, 1]); + }); + } + + #[test] + fn candidate_submission_using_alternative_free_slot_should_work() { + let mut t = new_test_ext_with_candidate_holes(); + + with_externalities(&mut t, || { + System::set_block_number(1); + assert_eq!(Council::candidates(), vec![0, 0, 1]); + + assert_ok!(Council::submit_candidacy(Origin::signed(2), 0)); + assert_eq!(Council::candidates(), vec![2, 0, 1]); + + assert_ok!(Council::submit_candidacy(Origin::signed(3), 1)); + assert_eq!(Council::candidates(), vec![2, 3, 1]); + }); + } + + #[test] + fn candidate_submission_not_using_free_slot_should_not_work() { + with_externalities(&mut new_test_ext_with_candidate_holes(), || { + System::set_block_number(1); + assert_noop!(Council::submit_candidacy(Origin::signed(4), 3), "invalid candidate slot"); + }); + } + + #[test] + fn bad_candidate_slot_submission_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + System::set_block_number(1); + assert_eq!(Council::candidates(), Vec::::new()); + assert_noop!(Council::submit_candidacy(Origin::signed(1), 1), "invalid candidate slot"); + }); + } + + #[test] + fn non_free_candidate_slot_submission_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + System::set_block_number(1); + assert_eq!(Council::candidates(), Vec::::new()); + assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); + assert_eq!(Council::candidates(), vec![1]); + assert_noop!(Council::submit_candidacy(Origin::signed(2), 0), "invalid candidate slot"); + }); + } + + #[test] + fn dupe_candidate_submission_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + System::set_block_number(1); + assert_eq!(Council::candidates(), Vec::::new()); + assert_ok!(Council::submit_candidacy(Origin::signed(1), 0)); + assert_eq!(Council::candidates(), vec![1]); + assert_noop!(Council::submit_candidacy(Origin::signed(1), 1), "duplicate candidate submission"); + }); + } + + #[test] + fn poor_candidate_submission_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + 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 voting_should_work() { + with_externalities(&mut new_test_ext(false), || { + 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_eq!(Council::approvals_of(1), vec![true]); + assert_eq!(Council::approvals_of(4), vec![true]); + assert_eq!(Council::voters(), 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_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::voters(), vec![1, 4, 2, 3]); + }); + } + + #[test] + fn resubmitting_voting_should_work() { + with_externalities(&mut new_test_ext(false), || { + 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_eq!(Council::approvals_of(4), vec![true]); + + 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(4), vec![true, false, true], 0)); + + assert_eq!(Council::approvals_of(4), vec![true, false, true]); + }); + } + + #[test] + fn retracting_voter_should_work() { + with_externalities(&mut new_test_ext(false), || { + System::set_block_number(1); + + assert_ok!(Council::submit_candidacy(Origin::signed(5), 0)); + 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(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_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_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_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_ok!(Council::retract_voter(Origin::signed(3), 1)); + + 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]); + }); + } + + #[test] + fn invalid_retraction_index_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + 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_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), || { + 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_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), || { + 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_noop!(Council::retract_voter(Origin::signed(2), 0), "cannot retract non-voter"); + }); + } + + #[test] + fn simple_tally_should_work() { + with_externalities(&mut new_test_ext(false), || { + 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::end_block(System::block_number())); + + System::set_block_number(6); + assert!(Council::presentation_active()); + assert_eq!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0), Ok(())); + assert_eq!(Council::present_winner(Origin::signed(4), 5.into(), 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!(!Council::presentation_active()); + assert_eq!(Council::active_council(), vec![(5, 11), (2, 11)]); + + 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)); + }); + } + + #[test] + fn double_presentations_should_be_punished() { + with_externalities(&mut new_test_ext(false), || { + 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0)); + assert_eq!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0), Err("duplicate presentation")); + assert_ok!(Council::end_block(System::block_number())); + + assert_eq!(Council::active_council(), vec![(5, 11), (2, 11)]); + assert_eq!(Balances::total_balance(&4), 38); + }); + } + + #[test] + fn retracting_inactive_voter_should_work() { + with_externalities(&mut new_test_ext(false), || { + 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); + assert_ok!(Council::end_block(System::block_number())); + + 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::end_block(System::block_number())); + + System::set_block_number(10); + assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 1)); + 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, + 2.into(), Council::voters().iter().position(|&i| i == 2).unwrap() as u32, + 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); + }); + } + + #[test] + fn presenting_for_double_election_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(8); + assert_eq!(Council::submit_candidacy(Origin::signed(2), 0), Ok(())); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 1)); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(10); + assert_noop!(Council::present_winner(Origin::signed(4), 2.into(), 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), || { + 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); + assert_ok!(Council::end_block(System::block_number())); + + 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::end_block(System::block_number())); + + System::set_block_number(10); + assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 1)); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(11); + 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, + 2.into(), Council::voters().iter().position(|&i| i == 2).unwrap() as u32, + 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); + }); + } + + #[test] + fn retracting_inactive_voter_with_bad_reporter_index_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); + assert_ok!(Council::end_block(System::block_number())); + + 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::end_block(System::block_number())); + + System::set_block_number(10); + assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 1)); + assert_ok!(Council::end_block(System::block_number())); + + assert_noop!(Council::reap_inactive_voter(Origin::signed(2), + 42, + 2.into(), Council::voters().iter().position(|&i| i == 2).unwrap() as u32, + 2 + ), "bad reporter index"); + }); + } + + #[test] + fn retracting_inactive_voter_with_bad_target_index_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); + assert_ok!(Council::end_block(System::block_number())); + + 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::end_block(System::block_number())); + + System::set_block_number(10); + assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 1)); + 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, + 2.into(), 42, + 2 + ), "bad target index"); + }); + } + + #[test] + fn attempting_to_retract_active_voter_should_slash_reporter() { + with_externalities(&mut new_test_ext(false), || { + 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0)); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(8); + assert_ok!(Council::set_desired_seats(Origin::ROOT, 3)); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(10); + assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 1)); + assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30, 1)); + assert_ok!(Council::end_block(System::block_number())); + + assert_ok!(Council::reap_inactive_voter(Origin::signed(4), + Council::voters().iter().position(|&i| i == 4).unwrap() as u32, + 2.into(), Council::voters().iter().position(|&i| i == 2).unwrap() as u32, + 2 + )); + + assert_eq!(Council::voters(), vec![2, 3, 5]); + assert_eq!(Council::approvals_of(4).len(), 0); + assert_eq!(Balances::total_balance(&4), 37); + }); + } + + #[test] + fn attempting_to_retract_inactive_voter_by_nonvoter_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); + assert_ok!(Council::end_block(System::block_number())); + + 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::end_block(System::block_number())); + + System::set_block_number(10); + assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 1)); + assert_ok!(Council::end_block(System::block_number())); + + assert_noop!(Council::reap_inactive_voter(Origin::signed(4), + 0, + 2.into(), Council::voters().iter().position(|&i| i == 2).unwrap() as u32, + 2 + ), "reporter must be a voter"); + }); + } + + #[test] + fn presenting_loser_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + 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::submit_candidacy(Origin::signed(2), 1)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 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::submit_candidacy(Origin::signed(4), 3)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_ok!(Council::present_winner(Origin::signed(4), 1.into(), 60, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0)); + assert_noop!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0), "candidate not worthy of leaderboard"); + }); + } + + #[test] + fn presenting_loser_first_should_not_matter() { + with_externalities(&mut new_test_ext(false), || { + 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::submit_candidacy(Origin::signed(2), 1)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 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::submit_candidacy(Origin::signed(4), 3)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_ok!(Council::present_winner(Origin::signed(4), 2.into(), 20, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 1.into(), 60, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0)); + + assert_eq!(Council::leaderboard(), Some(vec![ + (30, 3), + (40, 4), + (50, 5), + (60, 1) + ])); + }); + } + + #[test] + fn present_outside_of_presentation_period_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + System::set_block_number(4); + assert!(!Council::presentation_active()); + assert_noop!(Council::present_winner(Origin::signed(5), 5.into(), 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), || { + 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_noop!(Council::present_winner(Origin::signed(4), 2.into(), 20, 1), "index not current"); + }); + } + + #[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()); + + 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())); + + 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.into(), 20, 0), "presenter must have sufficient slashable funds"); + }); + } + + #[test] + fn invalid_present_tally_should_slash() { + with_externalities(&mut new_test_ext(false), || { + 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_err!(Council::present_winner(Origin::signed(4), 2.into(), 80, 0), "incorrect total"); + + assert_eq!(Balances::total_balance(&4), 38); + }); + } + + #[test] + fn runners_up_should_be_kept() { + with_externalities(&mut new_test_ext(false), || { + 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::submit_candidacy(Origin::signed(2), 1)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 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::submit_candidacy(Origin::signed(4), 3)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 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::end_block(System::block_number())); + + System::set_block_number(6); + assert!(Council::presentation_active()); + assert_ok!(Council::present_winner(Origin::signed(4), 1.into(), 60, 0)); + assert_eq!(Council::leaderboard(), Some(vec![ + (0, 0), + (0, 0), + (0, 0), + (60, 1) + ])); + assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0)); + assert_eq!(Council::leaderboard(), Some(vec![ + (30, 3), + (40, 4), + (50, 5), + (60, 1) + ])); + + assert_ok!(Council::end_block(System::block_number())); + + assert!(!Council::presentation_active()); + assert_eq!(Council::active_council(), vec![(1, 11), (5, 11)]); + + assert!(!Council::is_a_candidate(&1)); + assert!(!Council::is_a_candidate(&5)); + assert!(!Council::is_a_candidate(&2)); + 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::candidate_reg_info(3), Some((0, 2))); + assert_eq!(Council::candidate_reg_info(4), Some((0, 3))); + }); + } + + #[test] + fn second_tally_should_use_runners_up() { + with_externalities(&mut new_test_ext(false), || { + 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::submit_candidacy(Origin::signed(2), 1)); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true], 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::submit_candidacy(Origin::signed(4), 3)); + assert_ok!(Council::set_approvals(Origin::signed(4), vec![false, false, false, true], 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::end_block(System::block_number())); + + System::set_block_number(6); + assert_ok!(Council::present_winner(Origin::signed(4), 1.into(), 60, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40, 0)); + assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50, 0)); + 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_desired_seats(Origin::ROOT, 3)); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(10); + assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 90, 1)); + assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40, 1)); + assert_ok!(Council::end_block(System::block_number())); + + assert!(!Council::presentation_active()); + assert_eq!(Council::active_council(), vec![(1, 11), (5, 11), (3, 15)]); + + assert!(!Council::is_a_candidate(&1)); + assert!(!Council::is_a_candidate(&2)); + assert!(!Council::is_a_candidate(&3)); + 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::candidate_reg_info(4), Some((0, 3))); + }); + } +} \ No newline at end of file diff --git a/substrate/runtime/council/src/voting.rs b/substrate/runtime/council/src/voting.rs index 8faa680c289d0..a024413654fd9 100644 --- a/substrate/runtime/council/src/voting.rs +++ b/substrate/runtime/council/src/voting.rs @@ -249,8 +249,6 @@ mod tests { use substrate_runtime_support::Hashable; use democracy::VoteThreshold; - type CouncilVoting = super::Module; - #[test] fn basic_environment_works() { with_externalities(&mut new_test_ext(true), || { From a783f5f52a4a42a8043d1b6b0d4c903243121c59 Mon Sep 17 00:00:00 2001 From: Gav Date: Mon, 10 Sep 2018 17:12:11 +0200 Subject: [PATCH 6/7] Fix demo --- demo/runtime/src/lib.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/demo/runtime/src/lib.rs b/demo/runtime/src/lib.rs index c07e5104764b6..599180839ead6 100644 --- a/demo/runtime/src/lib.rs +++ b/demo/runtime/src/lib.rs @@ -55,12 +55,12 @@ extern crate substrate_runtime_treasury as treasury; extern crate substrate_runtime_version as version; extern crate demo_primitives; +use substrate_primitives::u32_trait::{_2, _4}; use demo_primitives::{AccountId, AccountIndex, Balance, BlockNumber, Hash, Index, SessionKey, Signature}; use runtime_primitives::generic; use runtime_primitives::traits::{Convert, BlakeTwo256, DigestItem}; use version::RuntimeVersion; -use council::motions as council_motions; -use substrate_primitives::u32_trait::{_2, _4}; +use council::{motions as council_motions, voting as council_voting}; #[cfg(any(feature = "std", test))] pub use runtime_primitives::{BuildStorage, Permill}; @@ -158,10 +158,17 @@ impl democracy::Trait for Runtime { /// Democracy module for this concrete runtime. pub type Democracy = democracy::Module; -impl council::Trait for Runtime {} +impl council::Trait for Runtime { + type Event = Event; +} /// Council module for this concrete runtime. pub type Council = council::Module; + +impl council::voting::Trait for Runtime { + type Event = Event; +} + /// Council voting module for this concrete runtime. pub type CouncilVoting = council::voting::Module; @@ -185,7 +192,7 @@ pub type Treasury = treasury::Module; impl_outer_event! { pub enum Event for Runtime { - balances, session, staking, democracy, treasury, council_motions + balances, session, staking, democracy, treasury, council, council_voting, council_motions } } From 19fce489dda9cc050853c6df405423e0ef9900ac Mon Sep 17 00:00:00 2001 From: Gav Date: Mon, 10 Sep 2018 17:56:18 +0200 Subject: [PATCH 7/7] More info in events --- substrate/runtime/council/src/seats.rs | 28 ++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/substrate/runtime/council/src/seats.rs b/substrate/runtime/council/src/seats.rs index a5f8422de49ad..0335e90bb383f 100644 --- a/substrate/runtime/council/src/seats.rs +++ b/substrate/runtime/council/src/seats.rs @@ -166,9 +166,9 @@ pub enum RawEvent { /// slashed reaper BadReaperSlashed(AccountId), /// A tally (for approval votes of council seat(s)) has started. - TallyStarted, + TallyStarted(u32), /// A tally (for approval votes of council seat(s)) has ended (with one or more new members). - TallyFinalised, + TallyFinalised(Vec, Vec), } impl From> for () { @@ -491,7 +491,7 @@ impl Module { let leaderboard_size = empty_seats + Self::carry_count() as usize; >::put(vec![(T::Balance::zero(), T::AccountId::default()); leaderboard_size]); - Self::deposit_event(RawEvent::TallyStarted); + Self::deposit_event(RawEvent::TallyStarted(empty_seats as u32)); } } @@ -508,24 +508,22 @@ impl Module { // return bond to winners. let candidacy_bond = Self::candidacy_bond(); - for &(_, ref w) in leaderboard.iter() + let incoming: Vec = leaderboard.iter() .rev() .take_while(|&&(b, _)| !b.is_zero()) .take(coming as usize) - { - >::unreserve(w, candidacy_bond); - } + .map(|(_, a)| a) + .cloned() + .inspect(|a| {>::unreserve(a, candidacy_bond);}) + .collect(); + let active_council = Self::active_council(); + let outgoing = active_council.iter().take(expiring.len()).map(|a| a.0.clone()).collect(); // set the new council. - let mut new_council: Vec<_> = Self::active_council() + let mut new_council: Vec<_> = active_council .into_iter() .skip(expiring.len()) - .chain(leaderboard.iter() - .rev() - .take_while(|&&(b, _)| !b.is_zero()) - .take(coming as usize) - .cloned() - .map(|(_, a)| (a, new_expiry))) + .chain(incoming.iter().cloned().map(|a| (a, new_expiry))) .collect(); new_council.sort_by_key(|&(_, expiry)| expiry); >::put(new_council); @@ -554,7 +552,7 @@ impl Module { new_candidates.truncate(last_index + 1); } - Self::deposit_event(RawEvent::TallyFinalised); + Self::deposit_event(RawEvent::TallyFinalised(incoming, outgoing)); >::put(new_candidates); >::put(count);