From 8717ccb1c8612e3795d0bc400a2bb228dac5d67d Mon Sep 17 00:00:00 2001 From: kianenigma Date: Mon, 12 Aug 2019 13:23:49 +0200 Subject: [PATCH 01/41] phragmen election module. --- Cargo.lock | 15 ++ Cargo.toml | 1 + core/sr-primitives/src/lib.rs | 4 +- srml/staking/src/lib.rs | 27 +-- srml/staking/src/phragmen.rs | 393 ---------------------------------- srml/staking/src/tests.rs | 11 +- 6 files changed, 39 insertions(+), 412 deletions(-) delete mode 100644 srml/staking/src/phragmen.rs diff --git a/Cargo.lock b/Cargo.lock index 524e710f88fc6..7e6b1f257da4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3845,6 +3845,21 @@ dependencies = [ "substrate-primitives 2.0.0", ] +[[package]] +name = "srml-elections-phragmen" +version = "2.0.0" +dependencies = [ + "hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", + "sr-io 2.0.0", + "sr-primitives 2.0.0", + "srml-balances 2.0.0", + "srml-support 2.0.0", + "srml-system 2.0.0", + "substrate-primitives 2.0.0", +] + [[package]] name = "srml-example" version = "2.0.0" diff --git a/Cargo.toml b/Cargo.toml index be6c89c2b7b10..fdb29978fde3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ members = [ "srml/collective", "srml/democracy", "srml/elections", + "srml/elections-phragmen", "srml/example", "srml/executive", "srml/finality-tracker", diff --git a/core/sr-primitives/src/lib.rs b/core/sr-primitives/src/lib.rs index 9246cd3ff6c3c..f661257f20575 100644 --- a/core/sr-primitives/src/lib.rs +++ b/core/sr-primitives/src/lib.rs @@ -45,8 +45,8 @@ use codec::{Encode, Decode, CompactAs}; pub mod testing; pub mod weights; +pub mod phragmen; pub mod traits; -use traits::{SaturatedConversion, UniqueSaturatedInto, Saturating, Bounded, CheckedSub, CheckedAdd}; pub mod generic; pub mod transaction_validity; @@ -58,6 +58,8 @@ pub use generic::{DigestItem, Digest}; pub use primitives::crypto::{key_types, KeyTypeId, CryptoType}; pub use app_crypto::AppKey; +use traits::{SaturatedConversion, UniqueSaturatedInto, Saturating, Bounded, CheckedSub, CheckedAdd}; + /// A message indicating an invalid signature in extrinsic. pub const BAD_SIGNATURE: &str = "bad signature in extrinsic"; diff --git a/srml/staking/src/lib.rs b/srml/staking/src/lib.rs index 0d2ba102d1f27..9d94428242d04 100644 --- a/srml/staking/src/lib.rs +++ b/srml/staking/src/lib.rs @@ -276,7 +276,7 @@ mod mock; #[cfg(test)] mod tests; -mod phragmen; +mod equalize; mod inflation; #[cfg(all(feature = "bench", test))] @@ -304,7 +304,7 @@ use sr_primitives::traits::{ use sr_primitives::{Serialize, Deserialize}; use system::{ensure_signed, ensure_root}; -use phragmen::{elect, ACCURACY, ExtendedBalance, equalize}; +use sr_primitives::phragmen::{elect, ACCURACY, ExtendedBalance}; const RECENT_OFFLINE_COUNT: usize = 32; const DEFAULT_MINIMUM_VALIDATOR_COUNT: u32 = 4; @@ -473,7 +473,6 @@ type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; type MomentOf= <::Time as Time>::Moment; -type RawAssignment = (::AccountId, ExtendedBalance); type Assignment = (::AccountId, ExtendedBalance, BalanceOf); type ExpoMap = BTreeMap< ::AccountId, @@ -1221,16 +1220,17 @@ impl Module { /// /// Returns the new `SlotStake` value and a set of newly selected _stash_ IDs. fn select_validators() -> (BalanceOf, Option>) { - let maybe_elected_set = elect::( - Self::validator_count() as usize, - Self::minimum_validator_count().max(1) as usize, - >::enumerate(), - >::enumerate(), - Self::slashable_balance_of, - ); + let maybe_elected_set = elect::, _, T::CurrencyToVote>( + Self::validator_count() as usize, + true, + Self::minimum_validator_count().max(1) as usize, + >::enumerate().map(|(who, _)| who).collect::>(), + >::enumerate().collect(), + Self::slashable_balance_of + ); if let Some(elected_set) = maybe_elected_set { - let elected_stashes = elected_set.0; + let elected_stashes = elected_set.0.into_iter().map(|(s, _)| s).collect::>(); let assignments = elected_set.1; // helper closure. @@ -1281,7 +1281,8 @@ impl Module { } } - if cfg!(feature = "equalize") { + #[cfg(feature = "equalize")] + { let tolerance = 0_u128; let iterations = 2_usize; let mut assignments_with_votes = assignments_with_stakes.iter() @@ -1296,7 +1297,7 @@ impl Module { BalanceOf, Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)> )>>(); - equalize::(&mut assignments_with_votes, &mut exposures, tolerance, iterations); + equalize::equalize::(&mut assignments_with_votes, &mut exposures, tolerance, iterations); } // Clear Stakers and reduce their slash_count. diff --git a/srml/staking/src/phragmen.rs b/srml/staking/src/phragmen.rs deleted file mode 100644 index 14b8a3845f2c4..0000000000000 --- a/srml/staking/src/phragmen.rs +++ /dev/null @@ -1,393 +0,0 @@ -// Copyright 2019 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 . - -//! Rust implementation of the Phragmén election algorithm. - -use rstd::{prelude::*, collections::btree_map::BTreeMap}; -use sr_primitives::{PerU128}; -use sr_primitives::traits::{Zero, Convert, Saturating}; -use crate::{BalanceOf, RawAssignment, ExpoMap, Trait, ValidatorPrefs, IndividualExposure}; - -type Fraction = PerU128; -/// Wrapper around the type used as the _safe_ wrapper around a `balance`. -pub type ExtendedBalance = u128; - -// this is only used while creating the candidate score. Due to reasons explained below -// The more accurate this is, the less likely we choose a wrong candidate. -const SCALE_FACTOR: ExtendedBalance = u32::max_value() as ExtendedBalance + 1; -/// These are used to expose a fixed accuracy to the caller function. The bigger they are, -/// the more accurate we get, but the more likely it is for us to overflow. The case of overflow -/// is handled but accuracy will be lost. 32 or 16 are reasonable values. -pub const ACCURACY: ExtendedBalance = u32::max_value() as ExtendedBalance + 1; - -/// Wrapper around validation candidates some metadata. -#[derive(Clone, Default)] -#[cfg_attr(feature = "std", derive(Debug))] -pub struct Candidate { - /// The validator's account - pub who: AccountId, - /// Intermediary value used to sort candidates. - pub score: Fraction, - /// Accumulator of the stake of this candidate based on received votes. - approval_stake: ExtendedBalance, - /// Flag for being elected. - elected: bool, -} - -/// Wrapper around the nomination info of a single nominator for a group of validators. -#[derive(Clone, Default)] -#[cfg_attr(feature = "std", derive(Debug))] -pub struct Nominator { - /// The nominator's account. - who: AccountId, - /// List of validators proposed by this nominator. - edges: Vec>, - /// the stake amount proposed by the nominator as a part of the vote. - budget: ExtendedBalance, - /// Incremented each time a nominee that this nominator voted for has been elected. - load: Fraction, -} - -/// Wrapper around a nominator vote and the load of that vote. -#[derive(Clone, Default)] -#[cfg_attr(feature = "std", derive(Debug))] -pub struct Edge { - /// Account being voted for - who: AccountId, - /// Load of this vote. - load: Fraction, - /// Equal to `edge.load / nom.load`. Stored only to be used with post-processing. - ratio: ExtendedBalance, - /// Index of the candidate stored in the 'candidates' vector. - candidate_index: usize, -} - -/// Perform election based on Phragmén algorithm. -/// -/// Reference implementation: https://github.com/w3f/consensus -/// -/// Returns an Option of elected candidates, if election is performed. -/// Returns None if not enough candidates exist. -/// -/// The returned Option is a tuple consisting of: -/// - The list of elected candidates. -/// - The list of nominators and their associated vote weights. -pub fn elect( - validator_count: usize, - minimum_validator_count: usize, - validator_iter: FV, - nominator_iter: FN, - slashable_balance_of: FS, -) -> Option<(Vec, Vec<(T::AccountId, Vec>)>)> where - FV: Iterator>)>, - FN: Iterator)>, - for <'r> FS: Fn(&'r T::AccountId) -> BalanceOf, -{ - let to_votes = |b: BalanceOf| , u64>>::convert(b) as ExtendedBalance; - - // return structures - let mut elected_candidates: Vec; - let mut assigned: Vec<(T::AccountId, Vec>)>; - let mut c_idx_cache = BTreeMap::::new(); - - // 1- Pre-process candidates and place them in a container, optimisation and add phantom votes. - // Candidates who have 0 stake => have no votes or all null-votes. Kick them out not. - let mut nominators: Vec> = - Vec::with_capacity(validator_iter.size_hint().0 + nominator_iter.size_hint().0); - let mut candidates = validator_iter.map(|(who, _)| { - let stash_balance = slashable_balance_of(&who); - (Candidate { who, ..Default::default() }, stash_balance) - }) - .filter_map(|(mut c, s)| { - c.approval_stake += to_votes(s); - if c.approval_stake.is_zero() { - None - } else { - Some((c, s)) - } - }) - .enumerate() - .map(|(idx, (c, s))| { - nominators.push(Nominator { - who: c.who.clone(), - edges: vec![ Edge { who: c.who.clone(), candidate_index: idx, ..Default::default() }], - budget: to_votes(s), - load: Fraction::zero(), - }); - c_idx_cache.insert(c.who.clone(), idx); - c - }) - .collect::>>(); - - // 2- Collect the nominators with the associated votes. - // Also collect approval stake along the way. - nominators.extend(nominator_iter.map(|(who, nominees)| { - let nominator_stake = slashable_balance_of(&who); - let mut edges: Vec> = Vec::with_capacity(nominees.len()); - for n in &nominees { - if let Some(idx) = c_idx_cache.get(n) { - // This candidate is valid + already cached. - candidates[*idx].approval_stake = candidates[*idx].approval_stake - .saturating_add(to_votes(nominator_stake)); - edges.push(Edge { who: n.clone(), candidate_index: *idx, ..Default::default() }); - } // else {} would be wrong votes. We don't really care about it. - } - Nominator { - who, - edges: edges, - budget: to_votes(nominator_stake), - load: Fraction::zero(), - } - })); - - // 4- If we have more candidates then needed, run Phragmén. - if candidates.len() >= minimum_validator_count { - let validator_count = validator_count.min(candidates.len()); - - elected_candidates = Vec::with_capacity(validator_count); - assigned = Vec::with_capacity(validator_count); - // Main election loop - for _round in 0..validator_count { - // Loop 1: initialize score - for c in &mut candidates { - if !c.elected { - c.score = Fraction::from_xth(c.approval_stake); - } - } - // Loop 2: increment score. - for n in &nominators { - for e in &n.edges { - let c = &mut candidates[e.candidate_index]; - if !c.elected && !c.approval_stake.is_zero() { - // Basic fixed-point shifting by 32. - // `n.budget.saturating_mul(SCALE_FACTOR)` will never saturate - // since n.budget cannot exceed u64,despite being stored in u128. yet, - // `*n.load / SCALE_FACTOR` might collapse to zero. Hence, 32 or 16 bits are better scale factors. - // Note that left-associativity in operators precedence is crucially important here. - let temp = - n.budget.saturating_mul(SCALE_FACTOR) / c.approval_stake - * (*n.load / SCALE_FACTOR); - c.score = Fraction::from_parts((*c.score).saturating_add(temp)); - } - } - } - - // Find the best - if let Some(winner) = candidates - .iter_mut() - .filter(|c| !c.elected) - .min_by_key(|c| *c.score) - { - // loop 3: update nominator and edge load - winner.elected = true; - for n in &mut nominators { - for e in &mut n.edges { - if e.who == winner.who { - e.load = Fraction::from_parts(*winner.score - *n.load); - n.load = winner.score; - } - } - } - - elected_candidates.push(winner.who.clone()); - } else { - break - } - } // end of all rounds - - // 4.1- Update backing stake of candidates and nominators - for n in &mut nominators { - let mut assignment = (n.who.clone(), vec![]); - for e in &mut n.edges { - if let Some(c) = elected_candidates.iter().find(|c| **c == e.who) { - if *c != n.who { - let ratio = { - // Full support. No need to calculate. - if *n.load == *e.load { ACCURACY } - else { - // This should not saturate. Safest is to just check - if let Some(r) = ACCURACY.checked_mul(*e.load) { - r / n.load.max(1) - } else { - // Just a simple trick. - *e.load / (n.load.max(1) / ACCURACY) - } - } - }; - e.ratio = ratio; - assignment.1.push((e.who.clone(), ratio)); - } - } - } - - if assignment.1.len() > 0 { - // To ensure an assertion indicating: no stake from the nominator going to waste, - // we add a minimal post-processing to equally assign all of the leftover stake ratios. - let vote_count = assignment.1.len() as ExtendedBalance; - let l = assignment.1.len(); - let sum = assignment.1.iter().map(|a| a.1).sum::(); - let diff = ACCURACY.checked_sub(sum).unwrap_or(0); - let diff_per_vote= diff / vote_count; - - if diff_per_vote > 0 { - for i in 0..l { - assignment.1[i%l].1 = - assignment.1[i%l].1 - .saturating_add(diff_per_vote); - } - } - - // `remainder` is set to be less than maximum votes of a nominator (currently 16). - // safe to cast it to usize. - let remainder = diff - diff_per_vote * vote_count; - for i in 0..remainder as usize { - assignment.1[i%l].1 = - assignment.1[i%l].1 - .saturating_add(1); - } - assigned.push(assignment); - } - } - - } else { - // if we have less than minimum, use the previous validator set. - return None - } - Some((elected_candidates, assigned)) -} - -/// Performs equalize post-processing to the output of the election algorithm -/// This function mutates the input parameters, most noticeably it updates the exposure of -/// the elected candidates. -/// -/// No value is returned from the function and the `expo_map` parameter is updated. -pub fn equalize( - assignments: &mut Vec<(T::AccountId, BalanceOf, Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)>)>, - expo_map: &mut ExpoMap, - tolerance: ExtendedBalance, - iterations: usize, -) { - for _i in 0..iterations { - let mut max_diff = 0; - assignments.iter_mut().for_each(|(n, budget, assignment)| { - let diff = do_equalize::(&n, *budget, assignment, expo_map, tolerance); - if diff > max_diff { - max_diff = diff; - } - }); - if max_diff < tolerance { - break; - } - } -} - -fn do_equalize( - nominator: &T::AccountId, - budget_balance: BalanceOf, - elected_edges: &mut Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)>, - expo_map: &mut ExpoMap, - tolerance: ExtendedBalance -) -> ExtendedBalance { - let to_votes = |b: BalanceOf| - , u64>>::convert(b) as ExtendedBalance; - let to_balance = |v: ExtendedBalance| - >>::convert(v); - let budget = to_votes(budget_balance); - - // Nothing to do. This nominator had nothing useful. - // Defensive only. Assignment list should always be populated. - if elected_edges.is_empty() { return 0; } - - let stake_used = elected_edges - .iter() - .fold(0 as ExtendedBalance, |s, e| s.saturating_add(e.2)); - - let backed_stakes_iter = elected_edges - .iter() - .filter_map(|e| expo_map.get(&e.0)) - .map(|e| to_votes(e.total)); - - let backing_backed_stake = elected_edges - .iter() - .filter(|e| e.2 > 0) - .filter_map(|e| expo_map.get(&e.0)) - .map(|e| to_votes(e.total)) - .collect::>(); - - let mut difference; - if backing_backed_stake.len() > 0 { - let max_stake = backing_backed_stake - .iter() - .max() - .expect("vector with positive length will have a max; qed"); - let min_stake = backed_stakes_iter - .min() - .expect("iterator with positive length will have a min; qed"); - - difference = max_stake.saturating_sub(min_stake); - difference = difference.saturating_add(budget.saturating_sub(stake_used)); - if difference < tolerance { - return difference; - } - } else { - difference = budget; - } - - // Undo updates to exposure - elected_edges.iter_mut().for_each(|e| { - if let Some(expo) = expo_map.get_mut(&e.0) { - expo.total = expo.total.saturating_sub(to_balance(e.2)); - expo.others.retain(|i_expo| i_expo.who != *nominator); - } - e.2 = 0; - }); - - elected_edges.sort_unstable_by_key(|e| - if let Some(e) = expo_map.get(&e.0) { e.total } else { Zero::zero() } - ); - - let mut cumulative_stake: ExtendedBalance = 0; - let mut last_index = elected_edges.len() - 1; - elected_edges.iter_mut().enumerate().for_each(|(idx, e)| { - if let Some(expo) = expo_map.get_mut(&e.0) { - let stake: ExtendedBalance = to_votes(expo.total); - let stake_mul = stake.saturating_mul(idx as ExtendedBalance); - let stake_sub = stake_mul.saturating_sub(cumulative_stake); - if stake_sub > budget { - last_index = idx.checked_sub(1).unwrap_or(0); - return - } - cumulative_stake = cumulative_stake.saturating_add(stake); - } - }); - - let last_stake = elected_edges[last_index].2; - let split_ways = last_index + 1; - let excess = budget - .saturating_add(cumulative_stake) - .saturating_sub(last_stake.saturating_mul(split_ways as ExtendedBalance)); - elected_edges.iter_mut().take(split_ways).for_each(|e| { - if let Some(expo) = expo_map.get_mut(&e.0) { - e.2 = (excess / split_ways as ExtendedBalance) - .saturating_add(last_stake) - .saturating_sub(to_votes(expo.total)); - expo.total = expo.total.saturating_add(to_balance(e.2)); - expo.others.push(IndividualExposure { who: nominator.clone(), value: to_balance(e.2)}); - } - }); - - difference -} diff --git a/srml/staking/src/tests.rs b/srml/staking/src/tests.rs index cf0ef3152600d..0250bf12e9590 100644 --- a/srml/staking/src/tests.rs +++ b/srml/staking/src/tests.rs @@ -18,7 +18,7 @@ use super::*; use runtime_io::with_externalities; -use phragmen; +use sr_primitives::phragmen; use sr_primitives::traits::OnInitialize; use srml_support::{assert_ok, assert_noop, assert_eq_uvec, EnumerableStorageMap}; use mock::*; @@ -1589,18 +1589,19 @@ fn phragmen_poc_2_works() { assert_ok!(Staking::bond(Origin::signed(3), 4, 1000, RewardDestination::default())); assert_ok!(Staking::nominate(Origin::signed(4), vec![11, 31])); - let winners = phragmen::elect::( + let winners = phragmen::elect::::CurrencyToVote>( 2, + true, Staking::minimum_validator_count() as usize, - >::enumerate(), - >::enumerate(), + >::enumerate().map(|(v, _)| v).collect::>(), + >::enumerate().collect(), Staking::slashable_balance_of, ); let (winners, assignment) = winners.unwrap(); // 10 and 30 must be the winners - assert_eq!(winners, vec![11, 31]); + assert_eq!(winners, vec![(11, 2050), (31, 2000)]); assert_eq!(assignment, vec![ (3, vec![(11, 2816371998), (31, 1478595298)]), (1, vec![(11, 4294967296)]), From 8e5d5ee5888659db78818f211a6e9a005eede2f4 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Mon, 12 Aug 2019 13:24:15 +0200 Subject: [PATCH 02/41] Add new files. --- core/sr-primitives/src/phragmen.rs | 284 +++++++++ srml/elections-phragmen/Cargo.toml | 30 + srml/elections-phragmen/src/lib.rs | 947 +++++++++++++++++++++++++++++ srml/staking/src/equalize.rs | 148 +++++ 4 files changed, 1409 insertions(+) create mode 100644 core/sr-primitives/src/phragmen.rs create mode 100644 srml/elections-phragmen/Cargo.toml create mode 100644 srml/elections-phragmen/src/lib.rs create mode 100644 srml/staking/src/equalize.rs diff --git a/core/sr-primitives/src/phragmen.rs b/core/sr-primitives/src/phragmen.rs new file mode 100644 index 0000000000000..f0384019084a6 --- /dev/null +++ b/core/sr-primitives/src/phragmen.rs @@ -0,0 +1,284 @@ +// Copyright 2019 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 . + +//! Rust implementation of the Phragmén election algorithm. + +use rstd::{prelude::*, collections::btree_map::BTreeMap}; +use crate::PerU128; +use crate::traits::{Zero, Convert, Member, SimpleArithmetic, MaybeDebug}; + +/// Type used as the fraction. +type Fraction = PerU128; +/// Wrapper around the type used as the _safe_ wrapper around a `balance`. +pub type ExtendedBalance = u128; + +// this is only used while creating the candidate score. Due to reasons explained below +// The more accurate this is, the less likely we choose a wrong candidate. +const SCALE_FACTOR: ExtendedBalance = u32::max_value() as ExtendedBalance + 1; +/// These are used to expose a fixed accuracy to the caller function. The bigger they are, +/// the more accurate we get, but the more likely it is for us to overflow. The case of overflow +/// is handled but accuracy will be lost. 32 or 16 are reasonable values. +pub const ACCURACY: ExtendedBalance = u32::max_value() as ExtendedBalance + 1; + +/// Wrapper around validation candidates some metadata. +#[derive(Clone, Default)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct Candidate { + /// The validator's account + pub who: AccountId, + /// Intermediary value used to sort candidates. + pub score: Fraction, + /// Accumulator of the stake of this candidate based on received votes. + approval_stake: ExtendedBalance, + /// Flag for being elected. + elected: bool, +} + +/// Wrapper around the nomination info of a single nominator for a group of validators. +#[derive(Clone, Default)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct Nominator { + /// The nominator's account. + who: AccountId, + /// List of validators proposed by this nominator. + edges: Vec>, + /// the stake amount proposed by the nominator as a part of the vote. + budget: ExtendedBalance, + /// Incremented each time a nominee that this nominator voted for has been elected. + load: Fraction, +} + +/// Wrapper around a nominator vote and the load of that vote. +#[derive(Clone, Default)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct Edge { + /// Account being voted for + who: AccountId, + /// Load of this vote. + load: Fraction, + /// Equal to `edge.load / nom.load`. Stored only to be used with post-processing. + ratio: ExtendedBalance, + /// Index of the candidate stored in the 'candidates' vector. + candidate_index: usize, +} + +/// A ratio of a nominator's (`A`) vote, indicated by `ExtendedBalance` / `ACCURACY`. +pub type PhragmenAssignment = (A, ExtendedBalance); + +/// Perform election based on Phragmén algorithm. +/// +/// Reference implementation: https://github.com/w3f/consensus +/// +/// Returns an Option of elected candidates, if election is performed. +/// Returns None if not enough candidates exist. +/// +/// The returned Option is a tuple consisting of: +/// - The list of elected candidates. +/// - The list of nominators and their associated vote weights. +pub fn elect( + validator_count: usize, + self_vote: bool, + minimum_validator_count: usize, + initial_candidates: Vec, + initial_voters: Vec<(AccountId, Vec)>, + slashable_balance_of: FS, +) -> Option<(Vec<(AccountId, ExtendedBalance)>, Vec<(AccountId, Vec>)>)> where + AccountId: Default + Ord + Member, + Balance: Default + Copy + SimpleArithmetic + MaybeDebug, + for <'r> FS: Fn(&'r AccountId) -> Balance, + C: Convert + Convert, +{ + let to_votes = |b: Balance| + >::convert(b) as ExtendedBalance; + + // return structures + let mut elected_candidates: Vec<(AccountId, ExtendedBalance)>; + let mut assigned: Vec<(AccountId, Vec>)>; + let mut c_idx_cache = BTreeMap::::new(); + + // 1- Pre-process candidates and place them in a container, optimisation and add phantom votes. + // Candidates who have 0 stake => have no votes or all null-votes. Kick them out not. + let mut nominators: Vec> = Vec::with_capacity(initial_candidates.len() + initial_voters.len()); + let mut candidates = if self_vote { + initial_candidates.into_iter().map(|who| { + let stake = slashable_balance_of(&who); + Candidate { who, approval_stake: to_votes(stake), ..Default::default() } + }) + .filter_map(|c| { + if c.approval_stake.is_zero() { + None + } else { + Some(c) + } + }) + .enumerate() + .map(|(idx, c)| { + nominators.push(Nominator { + who: c.who.clone(), + edges: vec![ Edge { who: c.who.clone(), candidate_index: idx, ..Default::default() }], + budget: c.approval_stake, + load: Fraction::zero(), + }); + c_idx_cache.insert(c.who.clone(), idx); + c + }) + .collect::>>() + } else { + initial_candidates.into_iter() + .enumerate() + .map(|(idx, who)| { + c_idx_cache.insert(who.clone(), idx); + Candidate { who, ..Default::default() } + }) + .collect::>>() + }; + + // 2- Collect the nominators with the associated votes. + // Also collect approval stake along the way. + nominators.extend(initial_voters.into_iter().map(|(who, nominees)| { + let nominator_stake = slashable_balance_of(&who); + let mut edges: Vec> = Vec::with_capacity(nominees.len()); + for n in nominees { + if let Some(idx) = c_idx_cache.get(&n) { + // This candidate is valid + already cached. + candidates[*idx].approval_stake = candidates[*idx].approval_stake + .saturating_add(to_votes(nominator_stake)); + edges.push(Edge { who: n.clone(), candidate_index: *idx, ..Default::default() }); + } // else {} would be wrong votes. We don't really care about it. + } + Nominator { + who, + edges: edges, + budget: to_votes(nominator_stake), + load: Fraction::zero(), + } + })); + + // 4- If we have more candidates then needed, run Phragmén. + if candidates.len() >= minimum_validator_count { + let validator_count = validator_count.min(candidates.len()); + + elected_candidates = Vec::with_capacity(validator_count); + assigned = Vec::with_capacity(validator_count); + // Main election loop + for _round in 0..validator_count { + // Loop 1: initialize score + for c in &mut candidates { + if !c.elected { + c.score = Fraction::from_xth(c.approval_stake); + } + } + // Loop 2: increment score. + for n in &nominators { + for e in &n.edges { + let c = &mut candidates[e.candidate_index]; + if !c.elected && !c.approval_stake.is_zero() { + // Basic fixed-point shifting by 32. + // `n.budget.saturating_mul(SCALE_FACTOR)` will never saturate + // since n.budget cannot exceed u64,despite being stored in u128. yet, + // `*n.load / SCALE_FACTOR` might collapse to zero. Hence, 32 or 16 bits are better scale factors. + // Note that left-associativity in operators precedence is crucially important here. + let temp = + n.budget.saturating_mul(SCALE_FACTOR) / c.approval_stake + * (*n.load / SCALE_FACTOR); + c.score = Fraction::from_parts((*c.score).saturating_add(temp)); + } + } + } + + // Find the best + if let Some(winner) = candidates + .iter_mut() + .filter(|c| !c.elected) + .min_by_key(|c| *c.score) + { + // loop 3: update nominator and edge load + winner.elected = true; + for n in &mut nominators { + for e in &mut n.edges { + if e.who == winner.who { + e.load = Fraction::from_parts(*winner.score - *n.load); + n.load = winner.score; + } + } + } + + elected_candidates.push((winner.who.clone(), winner.approval_stake)); + } else { + break + } + } // end of all rounds + + // 4.1- Update backing stake of candidates and nominators + for n in &mut nominators { + let mut assignment = (n.who.clone(), vec![]); + for e in &mut n.edges { + if let Some(c) = elected_candidates.iter().find(|(c, _)| *c == e.who) { + if c.0 != n.who { + let ratio = { + // Full support. No need to calculate. + if *n.load == *e.load { ACCURACY } + else { + // This should not saturate. Safest is to just check + if let Some(r) = ACCURACY.checked_mul(*e.load) { + r / n.load.max(1) + } else { + // Just a simple trick. + *e.load / (n.load.max(1) / ACCURACY) + } + } + }; + e.ratio = ratio; + assignment.1.push((e.who.clone(), ratio)); + } + } + } + + if assignment.1.len() > 0 { + // To ensure an assertion indicating: no stake from the nominator going to waste, + // we add a minimal post-processing to equally assign all of the leftover stake ratios. + let vote_count = assignment.1.len() as ExtendedBalance; + let l = assignment.1.len(); + let sum = assignment.1.iter().map(|a| a.1).sum::(); + let diff = ACCURACY.checked_sub(sum).unwrap_or(0); + let diff_per_vote= diff / vote_count; + + if diff_per_vote > 0 { + for i in 0..l { + assignment.1[i%l].1 = + assignment.1[i%l].1 + .saturating_add(diff_per_vote); + } + } + + // `remainder` is set to be less than maximum votes of a nominator (currently 16). + // safe to cast it to usize. + let remainder = diff - diff_per_vote * vote_count; + for i in 0..remainder as usize { + assignment.1[i%l].1 = + assignment.1[i%l].1 + .saturating_add(1); + } + assigned.push(assignment); + } + } + + } else { + // if we have less than minimum, use the previous validator set. + return None + } + Some((elected_candidates, assigned)) +} diff --git a/srml/elections-phragmen/Cargo.toml b/srml/elections-phragmen/Cargo.toml new file mode 100644 index 0000000000000..eb48b8bc2c009 --- /dev/null +++ b/srml/elections-phragmen/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "srml-elections-phragmen" +version = "2.0.0" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +serde = { version = "1.0", optional = true } +codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] } +primitives = { package = "substrate-primitives", path = "../../core/primitives", default-features = false } +runtime_io = { package = "sr-io", path = "../../core/sr-io", default-features = false } +sr-primitives = { path = "../../core/sr-primitives", default-features = false } +srml-support = { path = "../support", default-features = false } +system = { package = "srml-system", path = "../system", default-features = false } + +[dev-dependencies] +hex-literal = "0.2.0" +balances = { package = "srml-balances", path = "../balances" } + +[features] +default = ["std"] +std = [ + "codec/std", + "primitives/std", + "serde", + "runtime_io/std", + "srml-support/std", + "sr-primitives/std", + "system/std", +] diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs new file mode 100644 index 0000000000000..29b1e808d1e46 --- /dev/null +++ b/srml/elections-phragmen/src/lib.rs @@ -0,0 +1,947 @@ +// Copyright 2017-2019 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 . + +//! An election module based on sequential phragmen. +//! +//! The election happens in _rounds_: every `N` block, the entire previous members are retired and +//! a new set is elected (which may or may not have some of the same members). Each round lasts for +//! some number of blocks defined by `TermDuration` storage. Each cycle of is called a _term_. +//! +//! `TermDuration` might change during a round. This can shorten or extend the length of the round. +//! The next election round's block number is never stored but rather always checked on the fly. +//! Based on the current block number and `TermDuration`, `BlockNumber % TermDuration == 0` will +//! always trigger a new election round. +//! +//! Voters can vote for as many of the candidates by providing a list of account ids. Invalid votes +//! (voting for non-candidates) are ignored during election. Voters reserve a bond as they vote. +//! The entire free balance of a voter is locked upon voting and can only be used to pay for fees. +//! Voters can update their votes by calling `vote()` again during the same round. This does not +//! effect the reserved bond or lock. After a term, voters _must_ call `remove_voter` to get their +//! bond back and remove the lock. Furthermore, voters of an old round cannot vote for the current +//! round until they remove their previous data via `remove_voter`. +//! +//! Candidates also reserve a bond as they submit candidacy. A candidate cannot take their candidacy +//! back. Winner candidates will be moved to the members list and will get their bond back at end of +//! their term. Loser candidates will immediately get their bond back. Note that unlike phragmen in +//! staking, candidates do NOT automatically vote for themselves (though they could via a separate +//! transaction). Furthermore, the amount of tokens (stake) help by the candidate does not matter. + +#![cfg_attr(not(feature = "std"), no_std)] +#![recursion_limit="128"] + +use sr_primitives::traits::{Zero, StaticLookup, Bounded, Convert}; +use sr_primitives::weights::SimpleDispatchInfo; +use srml_support::{StorageValue, StorageMap, EnumerableStorageMap, decl_storage, decl_event, ensure, + decl_module, dispatch::Result, + traits::{Currency, Get, LockableCurrency, LockIdentifier, ReservableCurrency, WithdrawReason, + WithdrawReasons, ChangeMembers + } +}; +use system::{self, ensure_signed, ensure_root}; + +const MODULE_ID: LockIdentifier = *b"phrelect"; + +type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; + +pub const DEFAULT_CANDIDACY_BOND: u32 = 9; +pub const DEFAULT_VOTING_BOND: u32 = 2; + +pub trait Trait: system::Trait { + type Event: From> + Into<::Event>; + + /// The currency that people are electing with. + type Currency: + LockableCurrency + + ReservableCurrency; + + /// What to do when the members change. + type ChangeMembers: ChangeMembers; + + /// Convert a balance into a number used for election calculation. + /// This must fit into a `u64` but is allowed to be sensibly lossy. + type CurrencyToVote: Convert, u64> + Convert>; + + /// How much should be locked up in order to submit one's candidacy. + type CandidacyBond: Get>; + + /// How much should be locked up in order to be able to submit votes. + type VotingBond: Get>; +} + +decl_storage! { + trait Store for Module as PhragmenElection { + // ---- parameters + /// Number of seats to elect. + pub DesiredSeats get(desired_seats) config(): u32; + /// The total number of vote rounds that have happened, exclusive of the upcoming one. + pub ElectionRounds get(election_rounds): u32 = Zero::zero(); + /// How long each seat is kept. This defined the next block number at which an election + /// round will happen. + pub TermDuration get(term_duration) config(): T::BlockNumber; + + /// The current elected membership. + pub Members get(members) config(): Vec; + + /// Votes of a particular voter, with the round index of the votes. + pub VotesOf get(votes_of): linked_map T::AccountId => (Vec, u32); + /// Locked stake of a voter. + pub StakeOf get(stake_of): map T::AccountId => BalanceOf; + + /// The present candidate list. + pub Candidates get(candidates): Vec; + /// Current number of active candidates. + pub CandidateCount get(candidate_count): u32; + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + fn deposit_event() = default; + + const CandidacyBond: BalanceOf = T::CandidacyBond::get(); + const VotingBond: BalanceOf = T::VotingBond::get(); + + /// Vote for a set of candidates for the upcoming round of election. + /// + /// The `votes` should: + /// - not be empty. + /// - not be less than the number of candidates. + /// + /// Upon voting, the entire balance of `who` is locked and a bond amount is reserved. + #[weight = SimpleDispatchInfo::FixedNormal(2_500_000)] + fn vote(origin, votes: Vec) -> Result { + let who = ensure_signed(origin)?; + + // TODO: use decode_len #3071. + let candidates = Self::candidates(); + ensure!(!candidates.is_empty(), "number of candidates cannot be zero"); + ensure!(candidates.len() >= votes.len(), "cannot vote more than candidates"); + + ensure!(!votes.is_empty(), "must vote for at least one candidate."); + ensure!(!Self::is_old_voter(&who), "must first remove self as voter and re-submit"); + + if !Self::is_current_voter(&who) { + // Amount to be locked up. + let locked_balance = T::Currency::total_balance(&who); + + // lock and reserve + T::Currency::reserve(&who, T::VotingBond::get()) + .map_err(|_| "voter can not pay voting bond")?; + T::Currency::set_lock( + MODULE_ID, + &who, + locked_balance, + T::BlockNumber::max_value(), + WithdrawReasons::except(WithdrawReason::TransactionPayment), + ); + >::insert(&who, locked_balance); + } + + let now = Self::election_rounds(); + >::insert(&who, (votes, now)); + Ok(()) + } + + /// Remove `origin` as a voter. One can use this to _undo_ their votes before the election + /// has started. + /// + /// This can only be used for a vote which is submitted in the ongoing round. + #[weight = SimpleDispatchInfo::FixedNormal(1_250_000)] + fn remove_voter(origin) { + let who = ensure_signed(origin)?; + + ensure!( + Self::is_current_voter(&who) || >::exists(&who), + "should be a current/old voter" + ); + + // remove storage. + >::remove(&who); + >::remove(&who); + + // unreserve the bond and remove. + T::Currency::unreserve(&who, T::VotingBond::get()); + T::Currency::remove_lock(MODULE_ID, &who); + } + + /// Submit oneself for candidacy. + /// + /// A candidate will either: + /// - Lose at the end of the term and gets their bond unreserved (TODO: maybe burn?) + /// - Win and become a member. Members will get their bond back at the end ot their term. + #[weight = SimpleDispatchInfo::FixedNormal(2_500_000)] + fn submit_candidacy(origin) { + let who = ensure_signed(origin)?; + + ensure!(!Self::is_candidate(&who), "duplicate candidate submission"); + + T::Currency::reserve(&who, T::CandidacyBond::get()) + .map_err(|_| "candidate has not enough funds")?; + + >::append(&[who.clone()]) + .unwrap_or_else(|_| >::mutate(|c| c.push(who.clone()))); + CandidateCount::mutate(|c| *c += 1); + } + + /// 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. + #[weight = SimpleDispatchInfo::FixedOperational(10_000)] + fn set_desired_seats(origin, #[compact] count: u32) { + ensure_root(origin)?; + DesiredSeats::put(count); + } + + /// Remove a particular member from the set. This is effective immediately. + /// + /// Note: A tally should happen instantly (if not already in a presentation + /// period) to fill the seat if removal means that the desired members are not met. + #[weight = SimpleDispatchInfo::FixedOperational(10_000)] + fn remove_member(origin, who: ::Source) { + ensure_root(origin)?; + let who = T::Lookup::lookup(who)?; + + if Self::is_candidate(&who) { + let members = Self::members(); + let new_members: Vec = members + .into_iter() + .filter(|i| *i != who) + .collect(); + >::put(new_members.clone()); + T::Currency::unreserve(&who, T::CandidacyBond::get()); + T::ChangeMembers::change_members(&[], &[who], new_members); + } + } + + /// Set the presentation duration. If there is current a vote being presented for, will + /// invoke `finalize_vote`. + #[weight = SimpleDispatchInfo::FixedOperational(10_000)] + fn set_term_duration(origin, #[compact] count: T::BlockNumber) { + ensure_root(origin)?; + >::put(count); + } + + fn on_initialize(n: T::BlockNumber) { + if let Err(e) = Self::end_block(n) { + runtime_io::print("Guru meditation"); + runtime_io::print(e); + } + } + } +} + +decl_event!( + pub enum Event where ::AccountId { + /// A new term with new members. + NewTerm(Vec), + /// No candidates were elected for this round. + EmptyCouncil(), + } +); + +impl Module { + // exposed immutables. + + fn is_candidate(who: &T::AccountId) -> bool { + Self::candidates().iter().find(|c| *c == who).is_some() + } + + fn is_current_voter(who: &T::AccountId) -> bool { + let now = Self::election_rounds(); + Self::votes_of(who).1 == now && >::exists(who) + } + + fn is_old_voter(who: &T::AccountId) -> bool { + let now = Self::election_rounds(); + Self::votes_of(who).1 < now && >::exists(who) + } + + fn locked_stake_of(who: &T::AccountId) -> BalanceOf { + if Self::is_current_voter(who) { + Self::stake_of(who) + } else { + Zero::zero() + } + } + + // Private + /// Check there's nothing to do this block. + /// + /// Runs phragmen election and cleans all the previous state. + fn end_block(block_number: T::BlockNumber) -> Result { + if (block_number % Self::term_duration()).is_zero() { + use sr_primitives::phragmen; + let seats = Self::desired_seats() as usize; + let candidates = Self::candidates(); + let current_round = Self::election_rounds(); + let voters_and_votes = >::enumerate() + .filter_map(|(v, i)| if i.1 == current_round { Some((v, i.0)) } else { None } ) + .collect::)>>(); + + let maybe_new_members = phragmen::elect::<_, _, _, T::CurrencyToVote>( + seats, + false, + 0, + candidates.clone(), + voters_and_votes, + Self::locked_stake_of + ); + + let old_members = >::take(); + if let Some((new_members_with_approval, _)) = maybe_new_members { + let new_members = new_members_with_approval + .into_iter() + .filter_map(|(m, a)| if !a.is_zero() { Some(m) } else { None } ) + .collect::>(); + >::put(new_members.clone()); + + // return bond to losers. + let losers = candidates.into_iter() + .filter(|c| new_members.iter().find(|v| *v == c).is_none()) + .collect::>(); + losers.iter().for_each(|c| { T::Currency::unreserve(&c, T::CandidacyBond::get()); }); + + T::ChangeMembers::change_members( + &new_members, + &old_members, + new_members.clone(), + ); + Self::deposit_event(RawEvent::NewTerm(new_members)); + } else { + Self::deposit_event(RawEvent::EmptyCouncil()); + } + + // clean candidates + // unreserve the bond of all the outgoings. + old_members.iter().for_each(|m| { + T::Currency::unreserve(&m, T::CandidacyBond::get()); + }); + >::kill(); + CandidateCount::put(0); + + ElectionRounds::mutate(|v| *v += 1); + } + Ok(()) + } + +} + +#[cfg(test)] +mod tests { + use super::*; + use std::cell::RefCell; + use srml_support::{assert_ok, assert_noop, parameter_types, assert_eq_uvec}; + use runtime_io::with_externalities; + use primitives::{H256, Blake2Hasher}; + use sr_primitives::{Perbill, testing::Header, BuildStorage, + traits::{BlakeTwo256, IdentityLookup, Block as BlockT} + }; + use crate as elections; + + parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: u32 = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + } + impl system::Trait for Test { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type WeightMultiplierUpdate = (); + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + } + parameter_types! { + pub const ExistentialDeposit: u64 = 0; + pub const TransferFee: u64 = 0; + pub const CreationFee: u64 = 0; + pub const TransactionBaseFee: u64 = 0; + pub const TransactionByteFee: u64 = 0; + } + impl balances::Trait for Test { + type Balance = u64; + type OnNewAccount = (); + type OnFreeBalanceZero = (); + type Event = Event; + type TransactionPayment = (); + type TransferPayment = (); + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type TransferFee = TransferFee; + type CreationFee = CreationFee; + type TransactionBaseFee = TransactionBaseFee; + type TransactionByteFee = TransactionByteFee; + type WeightToFee = (); + } + parameter_types! { + pub const CandidacyBond: u64 = 3; + } + + thread_local! { + static VOTER_BOND: RefCell = RefCell::new(2); + static MEMBERS: RefCell> = RefCell::new(vec![]); + } + + pub struct VotingBond; + impl Get for VotingBond { + fn get() -> u64 { VOTER_BOND.with(|v| *v.borrow()) } + } + + pub struct TestChangeMembers; + impl ChangeMembers for TestChangeMembers { + fn change_members_sorted(_: &[u64], _: &[u64], _: &[u64]) {} + } + + /// Simple structure that exposes how u64 currency can be represented as... u64. + pub struct CurrencyToVoteHandler; + impl Convert for CurrencyToVoteHandler { + fn convert(x: u64) -> u64 { x } + } + impl Convert for CurrencyToVoteHandler { + fn convert(x: u128) -> u64 { + x as u64 + } + } + + impl Trait for Test { + type Event = Event; + type Currency = Balances; + type CurrencyToVote = CurrencyToVoteHandler; + type ChangeMembers = TestChangeMembers; + type CandidacyBond = CandidacyBond; + type VotingBond = VotingBond; + } + + pub type Block = sr_primitives::generic::Block; + pub type UncheckedExtrinsic = sr_primitives::generic::UncheckedExtrinsic; + + srml_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic + { + System: system::{Module, Call, Event}, + Balances: balances::{Module, Call, Event, Config}, + Elections: elections::{Module, Call, Event, Config}, + } + ); + + pub struct ExtBuilder { + balance_factor: u64, + voter_bond: u64, + } + + impl Default for ExtBuilder { + fn default() -> Self { + Self { + balance_factor: 1, + voter_bond: 2, + } + } + } + + impl ExtBuilder { + pub fn voter_bond(mut self, fee: u64) -> Self { + self.voter_bond = fee; + self + } + pub fn build(self) -> runtime_io::TestExternalities { + VOTER_BOND.with(|v| *v.borrow_mut() = self.voter_bond); + GenesisConfig { + balances: Some(balances::GenesisConfig::{ + balances: vec![ + (1, 10 * self.balance_factor), + (2, 20 * self.balance_factor), + (3, 30 * self.balance_factor), + (4, 40 * self.balance_factor), + (5, 50 * self.balance_factor), + (6, 60 * self.balance_factor) + ], + vesting: vec![], + }), + elections: Some(elections::GenesisConfig::{ + members: vec![], + desired_seats: 2, + term_duration: 5, + }), + }.build_storage().unwrap().into() + } + } + + fn current_voters() -> Vec { + >::enumerate() + .filter(|(v, _)| Elections::is_current_voter(v)) + .map(|(v, _)| v).collect::>() + } + + fn all_voters() -> Vec { + >::enumerate().map(|(v, _)| v).collect::>() + } + + #[test] + fn params_should_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + System::set_block_number(1); + assert_eq!(Elections::election_rounds(), 0); + assert_eq!(Elections::term_duration(), 5); + assert_eq!(Elections::desired_seats(), 2); + + assert_eq!(Elections::members(), vec![]); + + assert_eq!(Elections::candidates(), vec![]); + assert_eq!(Elections::candidate_count(), 0); + assert_eq!(Elections::is_candidate(&1), false); + + assert_eq!(current_voters(), vec![]); + assert_eq!(all_voters(), vec![]); + assert_eq!(Elections::votes_of(&1).0, vec![]); + }); + } + + #[test] + fn simple_candidate_submission_should_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(Elections::candidates(), Vec::::new()); + assert_eq!(Elections::is_candidate(&1), false); + assert_eq!(Elections::is_candidate(&2), false); + + assert_eq!(Balances::free_balance(&1), 10); + assert_ok!(Elections::submit_candidacy(Origin::signed(1))); + assert_eq!(Balances::free_balance(&1), 10 - 3); + assert_eq!(Balances::reserved_balance(&1), 3); + assert_eq!(Elections::candidates(), vec![1]); + assert_eq!(Elections::is_candidate(&1), true); + assert_eq!(Elections::is_candidate(&2), false); + + assert_eq!(Balances::free_balance(&2), 20); + assert_ok!(Elections::submit_candidacy(Origin::signed(2))); + assert_eq!(Balances::free_balance(&2), 20 - 3); + assert_eq!(Elections::candidates(), vec![1, 2]); + assert_eq!(Elections::is_candidate(&1), true); + assert_eq!(Elections::is_candidate(&2), true); + }); + } + + #[test] + fn dupe_candidate_submission_should_not_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(Elections::candidates(), Vec::::new()); + assert_ok!(Elections::submit_candidacy(Origin::signed(1))); + assert_eq!(Elections::candidates(), vec![1]); + assert_noop!( + Elections::submit_candidacy(Origin::signed(1)), + "duplicate candidate submission" + ); + }); + } + + #[test] + fn poor_candidate_submission_should_not_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(Elections::candidates(), Vec::::new()); + assert_noop!( + Elections::submit_candidacy(Origin::signed(7)), + "candidate has not enough funds" + ); + }); + } + + #[test] + fn vote_locks_entire_balance() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(Elections::candidates(), Vec::::new()); + assert_eq!(Balances::free_balance(&2), 20); + + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::vote(Origin::signed(2), vec![5])); + + assert_eq!(Balances::free_balance(&2), 20 - 2); + assert_noop!( + Balances::reserve(&2, 1), + "account liquidity restrictions prevent withdrawal" + ); // locked. + }); + } + + #[test] + fn can_update_votes_in_current_round() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(Balances::free_balance(&2), 20); + assert_eq!(Balances::reserved_balance(&2), 0); + + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::vote(Origin::signed(2), vec![5])); + + assert_eq!(Balances::free_balance(&2), 20 - 2); + assert_eq!(Balances::reserved_balance(&2), 2); + assert_eq!(Elections::stake_of(2), 20); + + // can update; same stake; same lock and reserve. + assert_ok!(Elections::vote(Origin::signed(2), vec![5, 4])); + assert_eq!(Balances::reserved_balance(&2), 2); + assert_eq!(Elections::stake_of(2), 20); + }); + } + + #[test] + fn cannot_vote_for_no_candidate() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_noop!( + Elections::vote(Origin::signed(2), vec![]), + "number of candidates cannot be zero" + ); + }); + } + + #[test] + fn cannot_vote_for_more_than_candidates() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + + assert_noop!( + Elections::vote(Origin::signed(2), vec![10, 20, 30]), + "cannot vote more than candidates" + ); + }); + } + + #[test] + fn remove_voter_should_work() { + with_externalities(&mut ExtBuilder::default().voter_bond(8).build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + + assert_ok!(Elections::vote(Origin::signed(2), vec![5])); + assert_ok!(Elections::vote(Origin::signed(3), vec![5])); + + assert_eq_uvec!(all_voters(), vec![2, 3]); + assert_eq_uvec!(all_voters(), current_voters()); + assert_eq!(Elections::stake_of(2), 20); + assert_eq!(Elections::stake_of(3), 30); + assert_eq!(Elections::votes_of(2), (vec![5], 0)); + assert_eq!(Elections::votes_of(3), (vec![5], 0)); + + assert_ok!(Elections::remove_voter(Origin::signed(2))); + + assert_eq!(all_voters(), vec![3]); + assert_eq!(current_voters(), vec![3]); + assert_eq!(Elections::votes_of(2), (vec![], 0)); + assert_eq!(Elections::stake_of(2), 0); + + assert_eq!(Balances::free_balance(&2), 20); + assert_eq!(Balances::reserved_balance(&2), 0); + assert_ok!(Balances::reserve(&2, 10)); // no locks + }); + } + + #[test] + fn non_voter_remove_should_not_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_noop!( + Elections::remove_voter(Origin::signed(3)), + "should be a current/old voter" + ); + }); + } + + #[test] + fn dupe_remove_should_fail() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::vote(Origin::signed(2), vec![5])); + + assert_ok!(Elections::remove_voter(Origin::signed(2))); + assert_eq!(all_voters(), vec![]); + assert_eq!(current_voters(), vec![]); + + assert_noop!( + Elections::remove_voter(Origin::signed(2)), + "should be a current/old voter" + ); + }); + } + + #[test] + fn removed_voter_should_not_be_counted() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + + assert_ok!(Elections::vote(Origin::signed(5), vec![5])); + assert_ok!(Elections::vote(Origin::signed(4), vec![4])); + assert_ok!(Elections::vote(Origin::signed(3), vec![3])); + + assert_ok!(Elections::remove_voter(Origin::signed(4))); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq_uvec!(Elections::members(), vec![3, 5]); + }); + } + + + #[test] + fn old_voter_should_not_be_counted() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + + assert_ok!(Elections::vote(Origin::signed(3), vec![3])); + assert_ok!(Elections::vote(Origin::signed(4), vec![4])); + assert_ok!(Elections::vote(Origin::signed(5), vec![5])); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members(), vec![5, 4]); + assert_eq!(Elections::election_rounds(), 1); + + // meanwhile, no one cares to become a candidate again. + System::set_block_number(10); + assert_ok!(Elections::end_block(System::block_number())); + // none of the votes matter anymore + assert_eq!(Elections::members(), vec![]); + assert_eq!(Elections::election_rounds(), 2); + assert_eq!(current_voters(), vec![]); + assert_eq_uvec!(all_voters(), vec![5, 4, 3]); + }); + } + + #[test] + fn voting_rounds_should_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + + assert_ok!(Elections::vote(Origin::signed(2), vec![5])); + assert_ok!(Elections::vote(Origin::signed(3), vec![5])); + + assert_eq_uvec!(current_voters(), vec![2, 3]); + assert_eq!(Elections::votes_of(2).0, vec![5]); + assert_eq!(Elections::votes_of(3).0, vec![5]); + + assert_eq!(Elections::candidates(), vec![5]); + assert_eq!(Elections::candidate_count(), 1); + + assert_eq!(Elections::election_rounds(), 0); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(current_voters(), vec![]); + assert_eq_uvec!(all_voters(), vec![2, 3]); + + assert_eq!(Elections::candidates(), vec![]); + assert_eq!(Elections::candidate_count(), 0); + + assert_eq!(Elections::election_rounds(), 1); + + assert_eq!(Elections::members(), vec![5]); + }); + } + + #[test] + fn only_desired_seats_are_chosen() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_ok!(Elections::submit_candidacy(Origin::signed(2))); + + assert_ok!(Elections::vote(Origin::signed(2), vec![2])); + assert_ok!(Elections::vote(Origin::signed(3), vec![3])); + assert_ok!(Elections::vote(Origin::signed(4), vec![4])); + assert_ok!(Elections::vote(Origin::signed(5), vec![5])); +; + assert_eq!(Elections::candidate_count(), 4); + + assert_eq!(Elections::election_rounds(), 0); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::candidates(), vec![]); + assert_eq!(Elections::candidate_count(), 0); + + assert_eq!(Elections::election_rounds(), 1); + + assert_eq!(Elections::members(), vec![5, 4]); + }); + } + + #[test] + fn seats_should_be_released() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + + assert_ok!(Elections::vote(Origin::signed(2), vec![3])); + assert_ok!(Elections::vote(Origin::signed(3), vec![3])); + assert_ok!(Elections::vote(Origin::signed(4), vec![4])); + assert_ok!(Elections::vote(Origin::signed(5), vec![5])); +; + assert_eq!(Elections::candidate_count(), 3); + + assert_eq!(Elections::election_rounds(), 0); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![5, 3]); + assert_eq!(Elections::election_rounds(), 1); + + // meanwhile, no one cares to become a candidate again. + System::set_block_number(10); + assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![]); + assert_eq!(Elections::election_rounds(), 2); + }); + } + + #[test] + fn outgoing_will_get_the_bond_back() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + + assert_ok!(Elections::vote(Origin::signed(3), vec![3])); + assert_ok!(Elections::vote(Origin::signed(4), vec![4])); + assert_ok!(Elections::vote(Origin::signed(5), vec![5])); + + assert_eq!(Balances::reserved_balance(&5), 3 + 2); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members(), vec![5, 4]); + assert_eq!(Elections::election_rounds(), 1); + assert_eq!(Balances::reserved_balance(&5), 3 + 2); + + // meanwhile, no one cares to become a candidate again. + System::set_block_number(10); + assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![]); + assert_eq!(Elections::election_rounds(), 2); + assert_eq!(Balances::reserved_balance(&5), 2); + assert_eq!(Balances::reserved_balance(&4), 2); + }); + } + + #[test] + fn losers_will_get_the_bond_back_after_remove() { + with_externalities(&mut ExtBuilder::default().voter_bond(2).build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + + assert_ok!(Elections::vote(Origin::signed(4), vec![5])); + + assert_eq!(Balances::reserved_balance(&5), 3); + assert_eq!(Balances::reserved_balance(&3), 3); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members(), vec![5]); + + assert_eq!(Balances::reserved_balance(&5), 3); // winner + assert_eq!(Balances::reserved_balance(&3), 0); // loser + }); + } + + #[test] + fn invalid_votes_are_moot() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + + assert_ok!(Elections::vote(Origin::signed(3), vec![3])); + assert_ok!(Elections::vote(Origin::signed(4), vec![4])); + assert_ok!(Elections::vote(Origin::signed(5), vec![10])); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq_uvec!(Elections::members(), vec![3, 4]); + assert_eq!(Elections::election_rounds(), 1); + }); + } + + #[test] + fn consequent_vote_without_remove_should_fail() { + with_externalities(&mut ExtBuilder::default().voter_bond(2).build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::vote(Origin::signed(3), vec![5])); + + assert_eq!(Balances::reserved_balance(&5), 3); + assert_eq!(Balances::reserved_balance(&3), 2); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members(), vec![5]); + assert_eq!(Elections::election_rounds(), 1); + assert_ok!(Elections::submit_candidacy(Origin::signed(2))); + + assert_eq!(Balances::reserved_balance(&5), 3); + assert_eq!(Balances::reserved_balance(&2), 3); + assert_eq!(Balances::reserved_balance(&3), 2); + + assert_noop!( + Elections::vote(Origin::signed(3), vec![2]), + "must first remove self as voter and re-submit" + ); + assert_ok!(Elections::remove_voter(Origin::signed(3))); + assert_eq!(Balances::reserved_balance(&3), 0); + + assert_ok!(Elections::vote(Origin::signed(3), vec![2])); + + System::set_block_number(10); + assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![2]); + assert_eq!(Balances::reserved_balance(&5), 0); + }) + } + + #[test] + fn candidate_stake_does_not_matter() { + with_externalities(&mut ExtBuilder::default().voter_bond(2).build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members(), vec![]); + }) + } +} diff --git a/srml/staking/src/equalize.rs b/srml/staking/src/equalize.rs new file mode 100644 index 0000000000000..cccf9335c3825 --- /dev/null +++ b/srml/staking/src/equalize.rs @@ -0,0 +1,148 @@ +// Copyright 2019 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 . + +//! Phragmen post-processing fot staking module. + +// TODO: deprecate this file, decouple from staking primitives and move to sr_primitives. +#![cfg(feature = "equalize")] + +use crate::{ExpoMap, Trait, BalanceOf, IndividualExposure}; + +use sr_primitives::traits::{Zero, Convert, Saturating}; +use sr_primitives::phragmen::{ExtendedBalance}; + +/// Performs equalize post-processing to the output of the election algorithm +/// This function mutates the input parameters, most noticeably it updates the exposure of +/// the elected candidates. +/// +/// No value is returned from the function and the `expo_map` parameter is updated. +pub fn equalize( + assignments: &mut Vec<(T::AccountId, BalanceOf, Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)>)>, + expo_map: &mut ExpoMap, + tolerance: ExtendedBalance, + iterations: usize, +) { + for _i in 0..iterations { + let mut max_diff = 0; + assignments.iter_mut().for_each(|(n, budget, assignment)| { + let diff = do_equalize::(&n, *budget, assignment, expo_map, tolerance); + if diff > max_diff { + max_diff = diff; + } + }); + if max_diff < tolerance { + break; + } + } +} + +fn do_equalize( + nominator: &T::AccountId, + budget_balance: BalanceOf, + elected_edges: &mut Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)>, + expo_map: &mut ExpoMap, + tolerance: ExtendedBalance +) -> ExtendedBalance { + let to_votes = |b: BalanceOf| + , u64>>::convert(b) as ExtendedBalance; + let to_balance = |v: ExtendedBalance| + >>::convert(v); + let budget = to_votes(budget_balance); + + // Nothing to do. This nominator had nothing useful. + // Defensive only. Assignment list should always be populated. + if elected_edges.is_empty() { return 0; } + + let stake_used = elected_edges + .iter() + .fold(0 as ExtendedBalance, |s, e| s.saturating_add(e.2)); + + let backed_stakes_iter = elected_edges + .iter() + .filter_map(|e| expo_map.get(&e.0)) + .map(|e| to_votes(e.total)); + + let backing_backed_stake = elected_edges + .iter() + .filter(|e| e.2 > 0) + .filter_map(|e| expo_map.get(&e.0)) + .map(|e| to_votes(e.total)) + .collect::>(); + + let mut difference; + if backing_backed_stake.len() > 0 { + let max_stake = backing_backed_stake + .iter() + .max() + .expect("vector with positive length will have a max; qed"); + let min_stake = backed_stakes_iter + .min() + .expect("iterator with positive length will have a min; qed"); + + difference = max_stake.saturating_sub(min_stake); + difference = difference.saturating_add(budget.saturating_sub(stake_used)); + if difference < tolerance { + return difference; + } + } else { + difference = budget; + } + + // Undo updates to exposure + elected_edges.iter_mut().for_each(|e| { + if let Some(expo) = expo_map.get_mut(&e.0) { + expo.total = expo.total.saturating_sub(to_balance(e.2)); + expo.others.retain(|i_expo| i_expo.who != *nominator); + } + e.2 = 0; + }); + + elected_edges.sort_unstable_by_key(|e| + if let Some(e) = expo_map.get(&e.0) { e.total } else { Zero::zero() } + ); + + let mut cumulative_stake: ExtendedBalance = 0; + let mut last_index = elected_edges.len() - 1; + elected_edges.iter_mut().enumerate().for_each(|(idx, e)| { + if let Some(expo) = expo_map.get_mut(&e.0) { + let stake: ExtendedBalance = to_votes(expo.total); + let stake_mul = stake.saturating_mul(idx as ExtendedBalance); + let stake_sub = stake_mul.saturating_sub(cumulative_stake); + if stake_sub > budget { + last_index = idx.checked_sub(1).unwrap_or(0); + return + } + cumulative_stake = cumulative_stake.saturating_add(stake); + } + }); + + let last_stake = elected_edges[last_index].2; + let split_ways = last_index + 1; + let excess = budget + .saturating_add(cumulative_stake) + .saturating_sub(last_stake.saturating_mul(split_ways as ExtendedBalance)); + elected_edges.iter_mut().take(split_ways).for_each(|e| { + if let Some(expo) = expo_map.get_mut(&e.0) { + e.2 = (excess / split_ways as ExtendedBalance) + .saturating_add(last_stake) + .saturating_sub(to_votes(expo.total)); + expo.total = expo.total.saturating_add(to_balance(e.2)); + expo.others.push(IndividualExposure { who: nominator.clone(), value: to_balance(e.2)}); + } + }); + + difference +} From f7d8c138f461829698d05ecbfc5050e058789f67 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Mon, 12 Aug 2019 14:48:15 +0200 Subject: [PATCH 03/41] Some doc update --- srml/elections-phragmen/src/lib.rs | 39 +++++++++++++++++------------- srml/staking/src/equalize.rs | 2 +- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 29b1e808d1e46..c2d8267608835 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -16,9 +16,9 @@ //! An election module based on sequential phragmen. //! -//! The election happens in _rounds_: every `N` block, the entire previous members are retired and -//! a new set is elected (which may or may not have some of the same members). Each round lasts for -//! some number of blocks defined by `TermDuration` storage. Each cycle of is called a _term_. +//! The election happens in _rounds_: every `N` blocks, the all previous members are retired and a +//! new set is elected (which may or may not have some of the same members). Each round lasts for +//! some number of blocks defined by `TermDuration` storage item. Each cycle of election is a term. //! //! `TermDuration` might change during a round. This can shorten or extend the length of the round. //! The next election round's block number is never stored but rather always checked on the fly. @@ -30,17 +30,17 @@ //! The entire free balance of a voter is locked upon voting and can only be used to pay for fees. //! Voters can update their votes by calling `vote()` again during the same round. This does not //! effect the reserved bond or lock. After a term, voters _must_ call `remove_voter` to get their -//! bond back and remove the lock. Furthermore, voters of an old round cannot vote for the current -//! round until they remove their previous data via `remove_voter`. +//! bond back and remove the lock. Otherwise the bond amount will stay reserved and the lock will +//! persist. Furthermore, voters of an old round cannot vote for the current round until they remove +//! their previous data via `remove_voter`. //! //! Candidates also reserve a bond as they submit candidacy. A candidate cannot take their candidacy //! back. Winner candidates will be moved to the members list and will get their bond back at end of //! their term. Loser candidates will immediately get their bond back. Note that unlike phragmen in -//! staking, candidates do NOT automatically vote for themselves (though they could via a separate -//! transaction). Furthermore, the amount of tokens (stake) help by the candidate does not matter. +//! staking, candidates do NOT automatically vote for themselves (though they _could_ via a separate +//! transaction). Furthermore, the amount of tokens (stake) held by the candidate does not matter. #![cfg_attr(not(feature = "std"), no_std)] -#![recursion_limit="128"] use sr_primitives::traits::{Zero, StaticLookup, Bounded, Convert}; use sr_primitives::weights::SimpleDispatchInfo; @@ -92,6 +92,7 @@ decl_storage! { /// round will happen. pub TermDuration get(term_duration) config(): T::BlockNumber; + // ---- State /// The current elected membership. pub Members get(members) config(): Vec; @@ -120,6 +121,8 @@ decl_module! { /// - not be empty. /// - not be less than the number of candidates. /// + /// `who` cannot be an old voter who has not yet called [`remove_voter`]. + /// /// Upon voting, the entire balance of `who` is locked and a bond amount is reserved. #[weight = SimpleDispatchInfo::FixedNormal(2_500_000)] fn vote(origin, votes: Vec) -> Result { @@ -155,10 +158,9 @@ decl_module! { Ok(()) } - /// Remove `origin` as a voter. One can use this to _undo_ their votes before the election - /// has started. - /// - /// This can only be used for a vote which is submitted in the ongoing round. + /// Remove `origin` as a voter. One can use this to + /// 1. _undo_ their votes before the election has started. + /// 2. Remove their lock and unreserve the bond after an election round. #[weight = SimpleDispatchInfo::FixedNormal(1_250_000)] fn remove_voter(origin) { let who = ensure_signed(origin)?; @@ -180,7 +182,7 @@ decl_module! { /// Submit oneself for candidacy. /// /// A candidate will either: - /// - Lose at the end of the term and gets their bond unreserved (TODO: maybe burn?) + /// - Lose at the end of the term and gets their bond unreserved /// - Win and become a member. Members will get their bond back at the end ot their term. #[weight = SimpleDispatchInfo::FixedNormal(2_500_000)] fn submit_candidacy(origin) { @@ -253,22 +255,25 @@ decl_event!( ); impl Module { - // exposed immutables. - + /// Check if `who` is a candidate. fn is_candidate(who: &T::AccountId) -> bool { Self::candidates().iter().find(|c| *c == who).is_some() } + + /// Check if `who` is a voter in the ongoing round. fn is_current_voter(who: &T::AccountId) -> bool { let now = Self::election_rounds(); Self::votes_of(who).1 == now && >::exists(who) } + /// Check if `who` was a voter who has not yet called `remove_voter`. fn is_old_voter(who: &T::AccountId) -> bool { let now = Self::election_rounds(); Self::votes_of(who).1 < now && >::exists(who) } + /// The locked stake of a voter. fn locked_stake_of(who: &T::AccountId) -> BalanceOf { if Self::is_current_voter(who) { Self::stake_of(who) @@ -277,10 +282,10 @@ impl Module { } } - // Private /// Check there's nothing to do this block. /// - /// Runs phragmen election and cleans all the previous state. + /// Runs phragmen election and cleans all the previous candidate state. The voter state is NOT + /// cleaned and voters must themselves submit a transaction to clean it. fn end_block(block_number: T::BlockNumber) -> Result { if (block_number % Self::term_duration()).is_zero() { use sr_primitives::phragmen; diff --git a/srml/staking/src/equalize.rs b/srml/staking/src/equalize.rs index cccf9335c3825..2e40a00e070eb 100644 --- a/srml/staking/src/equalize.rs +++ b/srml/staking/src/equalize.rs @@ -16,7 +16,7 @@ //! Phragmen post-processing fot staking module. -// TODO: deprecate this file, decouple from staking primitives and move to sr_primitives. +// TODO #3365: deprecate this file, decouple from staking and move to sr_primitives. #![cfg(feature = "equalize")] use crate::{ExpoMap, Trait, BalanceOf, IndividualExposure}; From ac3078c34c9be981661ce3455377a5713b44115e Mon Sep 17 00:00:00 2001 From: kianenigma Date: Mon, 12 Aug 2019 16:20:08 +0200 Subject: [PATCH 04/41] Update weights. --- srml/elections-phragmen/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index c2d8267608835..c02e1d1970f1c 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -124,7 +124,7 @@ decl_module! { /// `who` cannot be an old voter who has not yet called [`remove_voter`]. /// /// Upon voting, the entire balance of `who` is locked and a bond amount is reserved. - #[weight = SimpleDispatchInfo::FixedNormal(2_500_000)] + #[weight = SimpleDispatchInfo::FixedNormal(750_000)] fn vote(origin, votes: Vec) -> Result { let who = ensure_signed(origin)?; @@ -161,7 +161,7 @@ decl_module! { /// Remove `origin` as a voter. One can use this to /// 1. _undo_ their votes before the election has started. /// 2. Remove their lock and unreserve the bond after an election round. - #[weight = SimpleDispatchInfo::FixedNormal(1_250_000)] + #[weight = SimpleDispatchInfo::FixedNormal(500_000)] fn remove_voter(origin) { let who = ensure_signed(origin)?; @@ -184,7 +184,7 @@ decl_module! { /// A candidate will either: /// - Lose at the end of the term and gets their bond unreserved /// - Win and become a member. Members will get their bond back at the end ot their term. - #[weight = SimpleDispatchInfo::FixedNormal(2_500_000)] + #[weight = SimpleDispatchInfo::FixedNormal(1_000_000)] fn submit_candidacy(origin) { let who = ensure_signed(origin)?; From edcdee13abe50c1f9245e12f36757c5e5ca82de0 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Mon, 12 Aug 2019 18:22:57 +0200 Subject: [PATCH 05/41] bump and a few nits. --- node/runtime/src/lib.rs | 4 ++-- srml/elections-phragmen/src/lib.rs | 2 +- srml/staking/src/lib.rs | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/node/runtime/src/lib.rs b/node/runtime/src/lib.rs index 086818c51fbef..c4c4ca3f8f62a 100644 --- a/node/runtime/src/lib.rs +++ b/node/runtime/src/lib.rs @@ -80,8 +80,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // and set impl_version to equal spec_version. If only runtime // implementation changes and behavior does not, then leave spec_version as // is and increment impl_version. - spec_version: 138, - impl_version: 138, + spec_version: 139, + impl_version: 139, apis: RUNTIME_API_VERSIONS, }; diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index c02e1d1970f1c..4d1981aba359c 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2017-2019 Parity Technologies (UK) Ltd. +// Copyright 2019 Parity Technologies (UK) Ltd. // This file is part of Substrate. // Substrate is free software: you can redistribute it and/or modify diff --git a/srml/staking/src/lib.rs b/srml/staking/src/lib.rs index 9d94428242d04..4d40c610cf3f7 100644 --- a/srml/staking/src/lib.rs +++ b/srml/staking/src/lib.rs @@ -1221,13 +1221,13 @@ impl Module { /// Returns the new `SlotStake` value and a set of newly selected _stash_ IDs. fn select_validators() -> (BalanceOf, Option>) { let maybe_elected_set = elect::, _, T::CurrencyToVote>( - Self::validator_count() as usize, - true, - Self::minimum_validator_count().max(1) as usize, - >::enumerate().map(|(who, _)| who).collect::>(), - >::enumerate().collect(), - Self::slashable_balance_of - ); + Self::validator_count() as usize, + true, + Self::minimum_validator_count().max(1) as usize, + >::enumerate().map(|(who, _)| who).collect::>(), + >::enumerate().collect(), + Self::slashable_balance_of + ); if let Some(elected_set) = maybe_elected_set { let elected_stashes = elected_set.0.into_iter().map(|(s, _)| s).collect::>(); From 33b94094040a4a54237bebc54d343e340ed38421 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Tue, 13 Aug 2019 15:25:37 +0200 Subject: [PATCH 06/41] Performance improvement. --- Cargo.lock | 1 + srml/elections-phragmen/Cargo.toml | 2 + srml/elections-phragmen/src/lib.rs | 69 ++++++++++++++++-------------- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ac2e2128f459..1cbd152ea1496 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3854,6 +3854,7 @@ dependencies = [ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", + "sr-std 2.0.0", "srml-balances 2.0.0", "srml-support 2.0.0", "srml-system 2.0.0", diff --git a/srml/elections-phragmen/Cargo.toml b/srml/elections-phragmen/Cargo.toml index eb48b8bc2c009..fc54d18d45c4b 100644 --- a/srml/elections-phragmen/Cargo.toml +++ b/srml/elections-phragmen/Cargo.toml @@ -12,6 +12,7 @@ runtime_io = { package = "sr-io", path = "../../core/sr-io", default-features = sr-primitives = { path = "../../core/sr-primitives", default-features = false } srml-support = { path = "../support", default-features = false } system = { package = "srml-system", path = "../system", default-features = false } +rstd = { package = "sr-std", path = "../../core/sr-std", default-features = false } [dev-dependencies] hex-literal = "0.2.0" @@ -27,4 +28,5 @@ std = [ "srml-support/std", "sr-primitives/std", "system/std", + "rstd/std", ] diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index c02e1d1970f1c..7b0d61cb9cc44 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -45,7 +45,7 @@ use sr_primitives::traits::{Zero, StaticLookup, Bounded, Convert}; use sr_primitives::weights::SimpleDispatchInfo; use srml_support::{StorageValue, StorageMap, EnumerableStorageMap, decl_storage, decl_event, ensure, - decl_module, dispatch::Result, + decl_module, dispatch, traits::{Currency, Get, LockableCurrency, LockIdentifier, ReservableCurrency, WithdrawReason, WithdrawReasons, ChangeMembers } @@ -93,7 +93,7 @@ decl_storage! { pub TermDuration get(term_duration) config(): T::BlockNumber; // ---- State - /// The current elected membership. + /// The current elected membership. Sorted. pub Members get(members) config(): Vec; /// Votes of a particular voter, with the round index of the votes. @@ -101,7 +101,7 @@ decl_storage! { /// Locked stake of a voter. pub StakeOf get(stake_of): map T::AccountId => BalanceOf; - /// The present candidate list. + /// The present candidate list. Always sorted. pub Candidates get(candidates): Vec; /// Current number of active candidates. pub CandidateCount get(candidate_count): u32; @@ -125,7 +125,7 @@ decl_module! { /// /// Upon voting, the entire balance of `who` is locked and a bond amount is reserved. #[weight = SimpleDispatchInfo::FixedNormal(750_000)] - fn vote(origin, votes: Vec) -> Result { + fn vote(origin, votes: Vec) { let who = ensure_signed(origin)?; // TODO: use decode_len #3071. @@ -155,7 +155,6 @@ decl_module! { let now = Self::election_rounds(); >::insert(&who, (votes, now)); - Ok(()) } /// Remove `origin` as a voter. One can use this to @@ -188,13 +187,15 @@ decl_module! { fn submit_candidacy(origin) { let who = ensure_signed(origin)?; - ensure!(!Self::is_candidate(&who), "duplicate candidate submission"); + let is_candidate = Self::is_candidate(&who); + ensure!(!is_candidate.is_ok(), "duplicate candidate submission"); + // assured to be an error, error always contains the index. + let index = is_candidate.unwrap_err(); T::Currency::reserve(&who, T::CandidacyBond::get()) .map_err(|_| "candidate has not enough funds")?; - >::append(&[who.clone()]) - .unwrap_or_else(|_| >::mutate(|c| c.push(who.clone()))); + >::mutate(|c| c.insert(index, who)); CandidateCount::mutate(|c| *c += 1); } @@ -216,7 +217,7 @@ decl_module! { ensure_root(origin)?; let who = T::Lookup::lookup(who)?; - if Self::is_candidate(&who) { + if Self::is_candidate(&who).is_ok() { let members = Self::members(); let new_members: Vec = members .into_iter() @@ -255,12 +256,12 @@ decl_event!( ); impl Module { - /// Check if `who` is a candidate. - fn is_candidate(who: &T::AccountId) -> bool { - Self::candidates().iter().find(|c| *c == who).is_some() + /// Check if `who` is a candidate. It returns the insert index if the element does not exists as + /// an error. + fn is_candidate(who: &T::AccountId) -> Result<(), usize> { + Self::candidates().binary_search(who).map(|_| ()) } - /// Check if `who` is a voter in the ongoing round. fn is_current_voter(who: &T::AccountId) -> bool { let now = Self::election_rounds(); @@ -286,7 +287,7 @@ impl Module { /// /// Runs phragmen election and cleans all the previous candidate state. The voter state is NOT /// cleaned and voters must themselves submit a transaction to clean it. - fn end_block(block_number: T::BlockNumber) -> Result { + fn end_block(block_number: T::BlockNumber) -> dispatch::Result { if (block_number % Self::term_duration()).is_zero() { use sr_primitives::phragmen; let seats = Self::desired_seats() as usize; @@ -306,7 +307,9 @@ impl Module { ); let old_members = >::take(); - if let Some((new_members_with_approval, _)) = maybe_new_members { + if let Some((mut new_members_with_approval, _)) = maybe_new_members { + new_members_with_approval.sort_by_key(|x| x.0.clone()); + let new_members = new_members_with_approval .into_iter() .filter_map(|(m, a)| if !a.is_zero() { Some(m) } else { None } ) @@ -314,10 +317,12 @@ impl Module { >::put(new_members.clone()); // return bond to losers. - let losers = candidates.into_iter() - .filter(|c| new_members.iter().find(|v| *v == c).is_none()) - .collect::>(); - losers.iter().for_each(|c| { T::Currency::unreserve(&c, T::CandidacyBond::get()); }); + candidates.into_iter().for_each(|c| { + // if this candidate is a loser return the bond + if new_members.binary_search(&c).is_err() { + T::Currency::unreserve(&c, T::CandidacyBond::get()); + } + }); T::ChangeMembers::change_members( &new_members, @@ -519,7 +524,7 @@ mod tests { assert_eq!(Elections::candidates(), vec![]); assert_eq!(Elections::candidate_count(), 0); - assert_eq!(Elections::is_candidate(&1), false); + assert!(Elections::is_candidate(&1).is_err()); assert_eq!(current_voters(), vec![]); assert_eq!(all_voters(), vec![]); @@ -531,23 +536,23 @@ mod tests { fn simple_candidate_submission_should_work() { with_externalities(&mut ExtBuilder::default().build(), || { assert_eq!(Elections::candidates(), Vec::::new()); - assert_eq!(Elections::is_candidate(&1), false); - assert_eq!(Elections::is_candidate(&2), false); + assert!(Elections::is_candidate(&1).is_err()); + assert!(Elections::is_candidate(&2).is_err()); assert_eq!(Balances::free_balance(&1), 10); assert_ok!(Elections::submit_candidacy(Origin::signed(1))); assert_eq!(Balances::free_balance(&1), 10 - 3); assert_eq!(Balances::reserved_balance(&1), 3); assert_eq!(Elections::candidates(), vec![1]); - assert_eq!(Elections::is_candidate(&1), true); - assert_eq!(Elections::is_candidate(&2), false); + assert!(Elections::is_candidate(&1).is_ok()); + assert!(Elections::is_candidate(&2).is_err()); assert_eq!(Balances::free_balance(&2), 20); assert_ok!(Elections::submit_candidacy(Origin::signed(2))); assert_eq!(Balances::free_balance(&2), 20 - 3); assert_eq!(Elections::candidates(), vec![1, 2]); - assert_eq!(Elections::is_candidate(&1), true); - assert_eq!(Elections::is_candidate(&2), true); + assert!(Elections::is_candidate(&1).is_ok()); + assert!(Elections::is_candidate(&2).is_ok()); }); } @@ -707,7 +712,7 @@ mod tests { System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); - assert_eq_uvec!(Elections::members(), vec![3, 5]); + assert_eq!(Elections::members(), vec![3, 5]); }); } @@ -726,7 +731,7 @@ mod tests { System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::members(), vec![5, 4]); + assert_eq!(Elections::members(), vec![4, 5]); assert_eq!(Elections::election_rounds(), 1); // meanwhile, no one cares to become a candidate again. @@ -736,7 +741,7 @@ mod tests { assert_eq!(Elections::members(), vec![]); assert_eq!(Elections::election_rounds(), 2); assert_eq!(current_voters(), vec![]); - assert_eq_uvec!(all_voters(), vec![5, 4, 3]); + assert_eq_uvec!(all_voters(), vec![3, 4, 5]); }); } @@ -797,7 +802,7 @@ mod tests { assert_eq!(Elections::election_rounds(), 1); - assert_eq!(Elections::members(), vec![5, 4]); + assert_eq!(Elections::members(), vec![4, 5]); }); } @@ -819,7 +824,7 @@ mod tests { System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::members(), vec![5, 3]); + assert_eq!(Elections::members(), vec![3, 5]); assert_eq!(Elections::election_rounds(), 1); // meanwhile, no one cares to become a candidate again. @@ -846,7 +851,7 @@ mod tests { System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::members(), vec![5, 4]); + assert_eq!(Elections::members(), vec![4, 5]); assert_eq!(Elections::election_rounds(), 1); assert_eq!(Balances::reserved_balance(&5), 3 + 2); From 9cc03ce6f70395f3562f80aa6d11cc4a0e5b4b29 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Tue, 13 Aug 2019 15:26:11 +0200 Subject: [PATCH 07/41] Master.into() --- node/runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/runtime/src/lib.rs b/node/runtime/src/lib.rs index e47a6d8df70d2..055a78c7b80b8 100644 --- a/node/runtime/src/lib.rs +++ b/node/runtime/src/lib.rs @@ -81,7 +81,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // implementation changes and behavior does not, then leave spec_version as // is and increment impl_version. spec_version: 140, - impl_version: 140, + impl_version: 141, apis: RUNTIME_API_VERSIONS, }; From bb81b9f3623f693187d83d43bd0a047b65e4fdfe Mon Sep 17 00:00:00 2001 From: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Date: Wed, 14 Aug 2019 15:01:56 +0200 Subject: [PATCH 08/41] Update srml/elections-phragmen/src/lib.rs Co-Authored-By: Gavin Wood --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 9a4dd7583d686..e36dd227a83dd 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -251,7 +251,7 @@ decl_event!( /// A new term with new members. NewTerm(Vec), /// No candidates were elected for this round. - EmptyCouncil(), + EmptyCouncil, } ); From b456c9bfb55ad97351fd0a26b8206e8db0463d78 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Thu, 15 Aug 2019 11:02:22 +0200 Subject: [PATCH 09/41] Fix build --- srml/elections-phragmen/src/lib.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index e36dd227a83dd..00536416edef1 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -39,6 +39,10 @@ //! their term. Loser candidates will immediately get their bond back. Note that unlike phragmen in //! staking, candidates do NOT automatically vote for themselves (though they _could_ via a separate //! transaction). Furthermore, the amount of tokens (stake) held by the candidate does not matter. +//! +//! - [`election_phragmen::Trait`](./trait.Trait.html) +//! - [`Call`](./enum.Call.html) +//! - [`Module`](./struct.Module.html) #![cfg_attr(not(feature = "std"), no_std)] @@ -60,6 +64,7 @@ pub const DEFAULT_CANDIDACY_BOND: u32 = 9; pub const DEFAULT_VOTING_BOND: u32 = 2; pub trait Trait: system::Trait { + /// The overarching event type. type Event: From> + Into<::Event>; /// The currency that people are electing with. @@ -86,8 +91,6 @@ decl_storage! { // ---- parameters /// Number of seats to elect. pub DesiredSeats get(desired_seats) config(): u32; - /// The total number of vote rounds that have happened, exclusive of the upcoming one. - pub ElectionRounds get(election_rounds): u32 = Zero::zero(); /// How long each seat is kept. This defined the next block number at which an election /// round will happen. pub TermDuration get(term_duration) config(): T::BlockNumber; @@ -95,6 +98,8 @@ decl_storage! { // ---- State /// The current elected membership. Sorted. pub Members get(members) config(): Vec; + /// The total number of vote rounds that have happened, exclusive of the upcoming one. + pub ElectionRounds get(election_rounds): u32 = Zero::zero(); /// Votes of a particular voter, with the round index of the votes. pub VotesOf get(votes_of): linked_map T::AccountId => (Vec, u32); @@ -331,7 +336,7 @@ impl Module { ); Self::deposit_event(RawEvent::NewTerm(new_members)); } else { - Self::deposit_event(RawEvent::EmptyCouncil()); + Self::deposit_event(RawEvent::EmptyCouncil); } // clean candidates From 6433a8192739671c3b4dcf92a4de66cd54c9394e Mon Sep 17 00:00:00 2001 From: kianenigma Date: Thu, 15 Aug 2019 12:55:15 +0200 Subject: [PATCH 10/41] Some fixes. --- srml/elections-phragmen/src/lib.rs | 264 +++++++++++++++-------------- srml/elections/src/lib.rs | 13 +- 2 files changed, 135 insertions(+), 142 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 00536416edef1..1c000899f8de0 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -48,10 +48,11 @@ use sr_primitives::traits::{Zero, StaticLookup, Bounded, Convert}; use sr_primitives::weights::SimpleDispatchInfo; +use sr_primitives::phragmen; use srml_support::{StorageValue, StorageMap, EnumerableStorageMap, decl_storage, decl_event, ensure, decl_module, dispatch, - traits::{Currency, Get, LockableCurrency, LockIdentifier, ReservableCurrency, WithdrawReason, - WithdrawReasons, ChangeMembers + traits::{Currency, Get, LockableCurrency, LockIdentifier, ReservableCurrency, WithdrawReasons, + ChangeMembers } }; use system::{self, ensure_signed, ensure_root}; @@ -60,11 +61,8 @@ const MODULE_ID: LockIdentifier = *b"phrelect"; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; -pub const DEFAULT_CANDIDACY_BOND: u32 = 9; -pub const DEFAULT_VOTING_BOND: u32 = 2; - pub trait Trait: system::Trait { - /// The overarching event type. + /// The overarching event type.c type Event: From> + Into<::Event>; /// The currency that people are electing with. @@ -89,8 +87,8 @@ pub trait Trait: system::Trait { decl_storage! { trait Store for Module as PhragmenElection { // ---- parameters - /// Number of seats to elect. - pub DesiredSeats get(desired_seats) config(): u32; + /// Number of members to elect. + pub DesiredMembers get(desired_members) config(): u32; /// How long each seat is kept. This defined the next block number at which an election /// round will happen. pub TermDuration get(term_duration) config(): T::BlockNumber; @@ -124,13 +122,15 @@ decl_module! { /// /// The `votes` should: /// - not be empty. - /// - not be less than the number of candidates. + /// - be less than the number of candidates. /// /// `who` cannot be an old voter who has not yet called [`remove_voter`]. /// - /// Upon voting, the entire balance of `who` is locked and a bond amount is reserved. + /// Upon voting, `value` units of `who`'s balance is locked and a bond amount is reserved. + /// It is the responsibility of the caller to not place all of their balance into the lock + /// and keep some for further transactions. #[weight = SimpleDispatchInfo::FixedNormal(750_000)] - fn vote(origin, votes: Vec) { + fn vote(origin, votes: Vec, value: BalanceOf) { let who = ensure_signed(origin)?; // TODO: use decode_len #3071. @@ -143,7 +143,7 @@ decl_module! { if !Self::is_current_voter(&who) { // Amount to be locked up. - let locked_balance = T::Currency::total_balance(&who); + let locked_balance = value.min(T::Currency::total_balance(&who)); // lock and reserve T::Currency::reserve(&who, T::VotingBond::get()) @@ -153,7 +153,7 @@ decl_module! { &who, locked_balance, T::BlockNumber::max_value(), - WithdrawReasons::except(WithdrawReason::TransactionPayment), + WithdrawReasons::all(), ); >::insert(&who, locked_balance); } @@ -204,33 +204,29 @@ decl_module! { CandidateCount::mutate(|c| *c += 1); } - /// 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. + /// Set the desired member count. Changes will be effective at the beginning of next round. #[weight = SimpleDispatchInfo::FixedOperational(10_000)] - fn set_desired_seats(origin, #[compact] count: u32) { + fn set_desired_member_count(origin, #[compact] count: u32) { ensure_root(origin)?; - DesiredSeats::put(count); + DesiredMembers::put(count); } - /// Remove a particular member from the set. This is effective immediately. - /// - /// Note: A tally should happen instantly (if not already in a presentation - /// period) to fill the seat if removal means that the desired members are not met. + /// Remove a particular member from the set. This is effective immediately. No further + /// election is enforced. #[weight = SimpleDispatchInfo::FixedOperational(10_000)] fn remove_member(origin, who: ::Source) { ensure_root(origin)?; let who = T::Lookup::lookup(who)?; - if Self::is_candidate(&who).is_ok() { - let members = Self::members(); - let new_members: Vec = members - .into_iter() - .filter(|i| *i != who) - .collect(); - >::put(new_members.clone()); + + let mut members = Self::members(); + if let Ok(index) = members.binary_search(&who) { + members.remove(index); + T::Currency::unreserve(&who, T::CandidacyBond::get()); - T::ChangeMembers::change_members(&[], &[who], new_members); + >::put(members.clone()); + Self::deposit_event(RawEvent::MemberKicked(who.clone())); + T::ChangeMembers::change_members(&[], &[who], members); } } @@ -256,7 +252,10 @@ decl_event!( /// A new term with new members. NewTerm(Vec), /// No candidates were elected for this round. - EmptyCouncil, + EmptyTerm, + /// A member has been removed. This should always be followed by either `NewTerm` ot + /// `EmptyTerm`. + MemberKicked(AccountId), } ); @@ -281,11 +280,7 @@ impl Module { /// The locked stake of a voter. fn locked_stake_of(who: &T::AccountId) -> BalanceOf { - if Self::is_current_voter(who) { - Self::stake_of(who) - } else { - Zero::zero() - } + Self::stake_of(who) } /// Check there's nothing to do this block. @@ -294,64 +289,65 @@ impl Module { /// cleaned and voters must themselves submit a transaction to clean it. fn end_block(block_number: T::BlockNumber) -> dispatch::Result { if (block_number % Self::term_duration()).is_zero() { - use sr_primitives::phragmen; - let seats = Self::desired_seats() as usize; - let candidates = Self::candidates(); - let current_round = Self::election_rounds(); - let voters_and_votes = >::enumerate() - .filter_map(|(v, i)| if i.1 == current_round { Some((v, i.0)) } else { None } ) - .collect::)>>(); - - let maybe_new_members = phragmen::elect::<_, _, _, T::CurrencyToVote>( - seats, - false, - 0, - candidates.clone(), - voters_and_votes, - Self::locked_stake_of - ); - - let old_members = >::take(); - if let Some((mut new_members_with_approval, _)) = maybe_new_members { - new_members_with_approval.sort_by_key(|x| x.0.clone()); - - let new_members = new_members_with_approval - .into_iter() - .filter_map(|(m, a)| if !a.is_zero() { Some(m) } else { None } ) - .collect::>(); - >::put(new_members.clone()); - - // return bond to losers. - candidates.into_iter().for_each(|c| { - // if this candidate is a loser return the bond - if new_members.binary_search(&c).is_err() { - T::Currency::unreserve(&c, T::CandidacyBond::get()); - } - }); - - T::ChangeMembers::change_members( - &new_members, - &old_members, - new_members.clone(), - ); - Self::deposit_event(RawEvent::NewTerm(new_members)); - } else { - Self::deposit_event(RawEvent::EmptyCouncil); - } - - // clean candidates - // unreserve the bond of all the outgoings. - old_members.iter().for_each(|m| { - T::Currency::unreserve(&m, T::CandidacyBond::get()); - }); - >::kill(); - CandidateCount::put(0); - + Self::do_phragmen(); ElectionRounds::mutate(|v| *v += 1); } Ok(()) } + /// Run the phragmen election with all required side processes. + fn do_phragmen() { + let num_to_elect = Self::desired_members() as usize; + let candidates = Self::candidates(); + let current_round = Self::election_rounds(); + let voters_and_votes = >::enumerate() + .filter_map(|(v, i)| if i.1 == current_round { Some((v, i.0)) } else { None } ) + .collect::)>>(); + let maybe_new_members = phragmen::elect::<_, _, _, T::CurrencyToVote>( + num_to_elect, + false, + 0, + candidates.clone(), + voters_and_votes, + Self::locked_stake_of + ); + + let old_members = >::take(); + if let Some((mut new_members_with_approval, _)) = maybe_new_members { + new_members_with_approval.sort_by_key(|x| x.0.clone()); + + let new_members = new_members_with_approval + .into_iter() + .filter_map(|(m, a)| if !a.is_zero() { Some(m) } else { None } ) + .collect::>(); + >::put(new_members.clone()); + + // return bond to losers. + candidates.into_iter().for_each(|c| { + // if this candidate is a loser return the bond + if new_members.binary_search(&c).is_err() { + T::Currency::unreserve(&c, T::CandidacyBond::get()); + } + }); + + T::ChangeMembers::change_members_sorted( + &new_members, + &old_members, + new_members.clone(), + ); + Self::deposit_event(RawEvent::NewTerm(new_members)); + } else { + Self::deposit_event(RawEvent::EmptyTerm); + } + + // clean candidates + // unreserve the bond of all the outgoings. + old_members.iter().for_each(|m| { + T::Currency::unreserve(&m, T::CandidacyBond::get()); + }); + >::kill(); + CandidateCount::put(0); + } } #[cfg(test)] @@ -500,7 +496,7 @@ mod tests { }), elections: Some(elections::GenesisConfig::{ members: vec![], - desired_seats: 2, + desired_members: 2, term_duration: 5, }), }.build_storage().unwrap().into() @@ -523,7 +519,7 @@ mod tests { System::set_block_number(1); assert_eq!(Elections::election_rounds(), 0); assert_eq!(Elections::term_duration(), 5); - assert_eq!(Elections::desired_seats(), 2); + assert_eq!(Elections::desired_members(), 2); assert_eq!(Elections::members(), vec![]); @@ -592,7 +588,7 @@ mod tests { assert_eq!(Balances::free_balance(&2), 20); assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::vote(Origin::signed(2), vec![5])); + assert_ok!(Elections::vote(Origin::signed(2), vec![5], 20)); assert_eq!(Balances::free_balance(&2), 20 - 2); assert_noop!( @@ -610,14 +606,14 @@ mod tests { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); - assert_ok!(Elections::vote(Origin::signed(2), vec![5])); + assert_ok!(Elections::vote(Origin::signed(2), vec![5], 20)); assert_eq!(Balances::free_balance(&2), 20 - 2); assert_eq!(Balances::reserved_balance(&2), 2); assert_eq!(Elections::stake_of(2), 20); // can update; same stake; same lock and reserve. - assert_ok!(Elections::vote(Origin::signed(2), vec![5, 4])); + assert_ok!(Elections::vote(Origin::signed(2), vec![5, 4], 20)); assert_eq!(Balances::reserved_balance(&2), 2); assert_eq!(Elections::stake_of(2), 20); }); @@ -627,7 +623,7 @@ mod tests { fn cannot_vote_for_no_candidate() { with_externalities(&mut ExtBuilder::default().build(), || { assert_noop!( - Elections::vote(Origin::signed(2), vec![]), + Elections::vote(Origin::signed(2), vec![], 20), "number of candidates cannot be zero" ); }); @@ -640,19 +636,31 @@ mod tests { assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_noop!( - Elections::vote(Origin::signed(2), vec![10, 20, 30]), + Elections::vote(Origin::signed(2), vec![10, 20, 30], 20), "cannot vote more than candidates" ); }); } + #[test] + fn cannot_vote_for_more_than_total_balance() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + + assert_ok!(Elections::vote(Origin::signed(2), vec![4, 5], 30)); + // you can lie but won't get away with it. + assert_eq!(Elections::stake_of(2), 20); + }); + } + #[test] fn remove_voter_should_work() { with_externalities(&mut ExtBuilder::default().voter_bond(8).build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::vote(Origin::signed(2), vec![5])); - assert_ok!(Elections::vote(Origin::signed(3), vec![5])); + assert_ok!(Elections::vote(Origin::signed(2), vec![5], 20)); + assert_ok!(Elections::vote(Origin::signed(3), vec![5], 30)); assert_eq_uvec!(all_voters(), vec![2, 3]); assert_eq_uvec!(all_voters(), current_voters()); @@ -688,7 +696,7 @@ mod tests { fn dupe_remove_should_fail() { with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::vote(Origin::signed(2), vec![5])); + assert_ok!(Elections::vote(Origin::signed(2), vec![5], 20)); assert_ok!(Elections::remove_voter(Origin::signed(2))); assert_eq!(all_voters(), vec![]); @@ -708,9 +716,9 @@ mod tests { assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); - assert_ok!(Elections::vote(Origin::signed(5), vec![5])); - assert_ok!(Elections::vote(Origin::signed(4), vec![4])); - assert_ok!(Elections::vote(Origin::signed(3), vec![3])); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); assert_ok!(Elections::remove_voter(Origin::signed(4))); @@ -729,9 +737,9 @@ mod tests { assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); - assert_ok!(Elections::vote(Origin::signed(3), vec![3])); - assert_ok!(Elections::vote(Origin::signed(4), vec![4])); - assert_ok!(Elections::vote(Origin::signed(5), vec![5])); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); @@ -755,8 +763,8 @@ mod tests { with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::vote(Origin::signed(2), vec![5])); - assert_ok!(Elections::vote(Origin::signed(3), vec![5])); + assert_ok!(Elections::vote(Origin::signed(2), vec![5], 20)); + assert_ok!(Elections::vote(Origin::signed(3), vec![5], 30)); assert_eq_uvec!(current_voters(), vec![2, 3]); assert_eq!(Elections::votes_of(2).0, vec![5]); @@ -790,21 +798,15 @@ mod tests { assert_ok!(Elections::submit_candidacy(Origin::signed(3))); assert_ok!(Elections::submit_candidacy(Origin::signed(2))); - assert_ok!(Elections::vote(Origin::signed(2), vec![2])); - assert_ok!(Elections::vote(Origin::signed(3), vec![3])); - assert_ok!(Elections::vote(Origin::signed(4), vec![4])); - assert_ok!(Elections::vote(Origin::signed(5), vec![5])); -; - assert_eq!(Elections::candidate_count(), 4); - - assert_eq!(Elections::election_rounds(), 0); + assert_ok!(Elections::vote(Origin::signed(2), vec![2], 20)); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::candidates(), vec![]); - assert_eq!(Elections::candidate_count(), 0); - assert_eq!(Elections::election_rounds(), 1); assert_eq!(Elections::members(), vec![4, 5]); @@ -818,10 +820,10 @@ mod tests { assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); - assert_ok!(Elections::vote(Origin::signed(2), vec![3])); - assert_ok!(Elections::vote(Origin::signed(3), vec![3])); - assert_ok!(Elections::vote(Origin::signed(4), vec![4])); - assert_ok!(Elections::vote(Origin::signed(5), vec![5])); + assert_ok!(Elections::vote(Origin::signed(2), vec![3], 20)); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); ; assert_eq!(Elections::candidate_count(), 3); @@ -847,9 +849,9 @@ mod tests { assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); - assert_ok!(Elections::vote(Origin::signed(3), vec![3])); - assert_ok!(Elections::vote(Origin::signed(4), vec![4])); - assert_ok!(Elections::vote(Origin::signed(5), vec![5])); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); assert_eq!(Balances::reserved_balance(&5), 3 + 2); @@ -877,7 +879,7 @@ mod tests { assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); - assert_ok!(Elections::vote(Origin::signed(4), vec![5])); + assert_ok!(Elections::vote(Origin::signed(4), vec![5], 40)); assert_eq!(Balances::reserved_balance(&5), 3); assert_eq!(Balances::reserved_balance(&3), 3); @@ -898,9 +900,9 @@ mod tests { assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); - assert_ok!(Elections::vote(Origin::signed(3), vec![3])); - assert_ok!(Elections::vote(Origin::signed(4), vec![4])); - assert_ok!(Elections::vote(Origin::signed(5), vec![10])); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![10], 50)); System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); @@ -914,7 +916,7 @@ mod tests { fn consequent_vote_without_remove_should_fail() { with_externalities(&mut ExtBuilder::default().voter_bond(2).build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::vote(Origin::signed(3), vec![5])); + assert_ok!(Elections::vote(Origin::signed(3), vec![5], 30)); assert_eq!(Balances::reserved_balance(&5), 3); assert_eq!(Balances::reserved_balance(&3), 2); @@ -931,13 +933,13 @@ mod tests { assert_eq!(Balances::reserved_balance(&3), 2); assert_noop!( - Elections::vote(Origin::signed(3), vec![2]), + Elections::vote(Origin::signed(3), vec![2], 30), "must first remove self as voter and re-submit" ); assert_ok!(Elections::remove_voter(Origin::signed(3))); assert_eq!(Balances::reserved_balance(&3), 0); - assert_ok!(Elections::vote(Origin::signed(3), vec![2])); + assert_ok!(Elections::vote(Origin::signed(3), vec![2], 30)); System::set_block_number(10); assert_ok!(Elections::end_block(System::block_number())); diff --git a/srml/elections/src/lib.rs b/srml/elections/src/lib.rs index a96588e84a3c6..b6357afe48e53 100644 --- a/srml/elections/src/lib.rs +++ b/srml/elections/src/lib.rs @@ -138,17 +138,8 @@ pub type VoteIndex = u32; // all three must be in sync. type ApprovalFlag = u32; -pub const APPROVAL_FLAG_MASK: ApprovalFlag = 0x8000_0000; -pub const APPROVAL_FLAG_LEN: usize = 32; - -pub const DEFAULT_CANDIDACY_BOND: u32 = 9; -pub const DEFAULT_VOTING_BOND: u32 = 0; -pub const DEFAULT_VOTING_FEE: u32 = 0; -pub const DEFAULT_PRESENT_SLASH_PER_VOTER: u32 = 1; -pub const DEFAULT_CARRY_COUNT: u32 = 2; -pub const DEFAULT_INACTIVE_GRACE_PERIOD: u32 = 1; -pub const DEFAULT_VOTING_PERIOD: u32 = 1000; -pub const DEFAULT_DECAY_RATIO: u32 = 24; +const APPROVAL_FLAG_MASK: ApprovalFlag = 0x8000_0000; +const APPROVAL_FLAG_LEN: usize = 32; pub trait Trait: system::Trait { type Event: From> + Into<::Event>; From 3d90cd3f4204a9c7d78c472b9b492aad8fdb38fe Mon Sep 17 00:00:00 2001 From: kianenigma Date: Thu, 15 Aug 2019 13:12:05 +0200 Subject: [PATCH 11/41] Fix build. --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 1c000899f8de0..ad6752ede265d 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -333,7 +333,7 @@ impl Module { T::ChangeMembers::change_members_sorted( &new_members, &old_members, - new_members.clone(), + &new_members, ); Self::deposit_event(RawEvent::NewTerm(new_members)); } else { From 88e81084528d7417bd5110f3d8473a324183cd9b Mon Sep 17 00:00:00 2001 From: kianenigma Date: Sat, 17 Aug 2019 16:53:07 +0200 Subject: [PATCH 12/41] Proper outgoing and runner-up managment. --- Cargo.lock | 230 +++++----- srml/elections-phragmen/src/lib.rs | 693 ++++++++++++++++++++--------- srml/support/src/traits.rs | 15 +- 3 files changed, 607 insertions(+), 331 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c55c2ff5a3ff..2f4fab56df08f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,11 +212,6 @@ name = "bitmask" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "bitvec" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "bitvec" version = "0.14.0" @@ -853,7 +848,7 @@ dependencies = [ "hashmap_core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -905,7 +900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "fork-tree" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1316,7 +1311,7 @@ name = "impl-codec" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1648,7 +1643,7 @@ dependencies = [ "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "zeroize 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1802,7 +1797,7 @@ dependencies = [ "snow 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "x25519-dalek 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "zeroize 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2109,7 +2104,7 @@ dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "keccak 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2270,7 +2265,7 @@ dependencies = [ "node-executor 2.0.0", "node-primitives 2.0.0", "node-runtime 2.0.0", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -2311,7 +2306,7 @@ version = "2.0.0" dependencies = [ "node-primitives 2.0.0", "node-runtime 2.0.0", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", "srml-balances 2.0.0", @@ -2338,7 +2333,7 @@ dependencies = [ name = "node-primitives" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", @@ -2366,7 +2361,7 @@ version = "2.0.0" dependencies = [ "integer-sqrt 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "node-primitives 2.0.0", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hex 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2413,7 +2408,7 @@ dependencies = [ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "node-template-runtime 2.0.0", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "substrate-basic-authorship 2.0.0", @@ -2437,7 +2432,7 @@ dependencies = [ name = "node-template-runtime" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -2605,16 +2600,6 @@ name = "parity-bytes" version = "0.1.0" source = "git+https://github.com/paritytech/parity-common?rev=b0317f649ab2c665b7987b8475878fc4d2e1f81d#b0317f649ab2c665b7987b8475878fc4d2e1f81d" -[[package]] -name = "parity-codec" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "bitvec 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "parity-multiaddr" version = "0.5.0" @@ -2648,7 +2633,7 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2656,7 +2641,6 @@ dependencies = [ "byte-slice-cast 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "parity-scale-codec-derive 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", - "vecarray 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2939,7 +2923,7 @@ dependencies = [ "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2991,7 +2975,7 @@ dependencies = [ "autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3036,12 +3020,12 @@ name = "rand_core" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "rand_core" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -3082,7 +3066,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -3094,7 +3078,7 @@ dependencies = [ "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -3105,7 +3089,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -3209,7 +3193,7 @@ dependencies = [ "error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -3322,17 +3306,18 @@ dependencies = [ [[package]] name = "schnorrkel" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "curve25519-dalek 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "ed25519-dalek 1.0.0-pre.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "merlin 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "subtle 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "zeroize 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -3558,7 +3543,7 @@ dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3601,7 +3586,7 @@ version = "2.0.0" dependencies = [ "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", "criterion 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro-crate 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3623,7 +3608,7 @@ dependencies = [ "environmental 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "hash-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "libsecp256k1 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "sr-std 2.0.0", "substrate-offchain 2.0.0", @@ -3640,7 +3625,7 @@ dependencies = [ "integer-sqrt 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "paste 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "primitive-types 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3657,7 +3642,7 @@ name = "sr-sandbox" version = "2.0.0" dependencies = [ "assert_matches 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "sr-std 2.0.0", "substrate-primitives 2.0.0", @@ -3677,7 +3662,7 @@ name = "sr-version" version = "2.0.0" dependencies = [ "impl-serde 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "sr-std 2.0.0", @@ -3687,7 +3672,7 @@ dependencies = [ name = "srml-assets" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -3702,7 +3687,7 @@ name = "srml-aura" version = "2.0.0" dependencies = [ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -3723,7 +3708,7 @@ dependencies = [ name = "srml-authorship" version = "0.1.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", "sr-std 2.0.0", @@ -3739,7 +3724,7 @@ version = "2.0.0" dependencies = [ "hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -3759,7 +3744,7 @@ dependencies = [ name = "srml-balances" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -3776,7 +3761,7 @@ name = "srml-collective" version = "2.0.0" dependencies = [ "hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -3795,7 +3780,7 @@ dependencies = [ "assert_matches 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parity-wasm 0.31.3 (registry+https://github.com/rust-lang/crates.io-index)", "pwasm-utils 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3816,7 +3801,7 @@ dependencies = [ name = "srml-democracy" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -3833,7 +3818,7 @@ name = "srml-elections" version = "2.0.0" dependencies = [ "hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -3850,7 +3835,7 @@ name = "srml-elections-phragmen" version = "2.0.0" dependencies = [ "hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -3865,7 +3850,7 @@ dependencies = [ name = "srml-example" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -3880,7 +3865,7 @@ name = "srml-executive" version = "2.0.0" dependencies = [ "hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -3896,7 +3881,7 @@ dependencies = [ name = "srml-finality-tracker" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -3911,7 +3896,7 @@ dependencies = [ name = "srml-generic-asset" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -3925,7 +3910,7 @@ dependencies = [ name = "srml-grandpa" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -3942,7 +3927,7 @@ dependencies = [ name = "srml-im-online" version = "0.1.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -3958,7 +3943,7 @@ dependencies = [ name = "srml-indices" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "ref_thread_local 0.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3975,7 +3960,7 @@ dependencies = [ name = "srml-membership" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -3989,7 +3974,7 @@ dependencies = [ name = "srml-metadata" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-std 2.0.0", "substrate-primitives 2.0.0", @@ -4000,7 +3985,7 @@ name = "srml-session" version = "2.0.0" dependencies = [ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -4018,7 +4003,7 @@ dependencies = [ name = "srml-staking" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4039,7 +4024,7 @@ dependencies = [ name = "srml-sudo" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -4056,7 +4041,7 @@ version = "2.0.0" dependencies = [ "bitmask 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "once_cell 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "paste 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4105,7 +4090,7 @@ dependencies = [ name = "srml-support-test" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -4120,7 +4105,7 @@ name = "srml-system" version = "2.0.0" dependencies = [ "criterion 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -4134,7 +4119,7 @@ dependencies = [ name = "srml-timestamp" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -4149,7 +4134,7 @@ dependencies = [ name = "srml-treasury" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -4241,7 +4226,7 @@ dependencies = [ "hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "node-primitives 2.0.0", "node-runtime 2.0.0", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hex 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", @@ -4266,7 +4251,7 @@ dependencies = [ name = "substrate-application-crypto" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -4281,7 +4266,7 @@ version = "2.0.0" dependencies = [ "futures-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "substrate-client 2.0.0", "substrate-consensus-common 2.0.0", @@ -4299,7 +4284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "hmac 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "pbkdf2 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "schnorrkel 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", + "schnorrkel 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)", "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -4351,7 +4336,7 @@ dependencies = [ "kvdb 0.1.0 (git+https://github.com/paritytech/parity-common?rev=b0317f649ab2c665b7987b8475878fc4d2e1f81d)", "kvdb-memorydb 0.1.0 (git+https://github.com/paritytech/parity-common?rev=b0317f649ab2c665b7987b8475878fc4d2e1f81d)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-api-macros 2.0.0", "sr-primitives 2.0.0", @@ -4379,7 +4364,7 @@ dependencies = [ "kvdb-rocksdb 0.1.4 (git+https://github.com/paritytech/parity-common?rev=b0317f649ab2c665b7987b8475878fc4d2e1f81d)", "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "substrate-client 2.0.0", @@ -4402,7 +4387,7 @@ dependencies = [ "futures-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", "futures-timer 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -4431,7 +4416,7 @@ dependencies = [ name = "substrate-consensus-aura-primitives" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "sr-std 2.0.0", "substrate-application-crypto 2.0.0", @@ -4452,10 +4437,10 @@ dependencies = [ "num-bigint 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "num-rational 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", - "schnorrkel 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", + "schnorrkel 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", "sr-version 2.0.0", @@ -4484,8 +4469,8 @@ dependencies = [ name = "substrate-consensus-babe-primitives" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "schnorrkel 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "schnorrkel 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "sr-std 2.0.0", "substrate-application-crypto 2.0.0", @@ -4502,7 +4487,7 @@ dependencies = [ "futures-timer 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "libp2p 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "sr-std 2.0.0", @@ -4516,7 +4501,7 @@ dependencies = [ name = "substrate-consensus-common-primitives" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "sr-std 2.0.0", "substrate-client 2.0.0", @@ -4530,7 +4515,7 @@ dependencies = [ "exit-future 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "rhododendron 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -4553,7 +4538,7 @@ dependencies = [ "futures-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", "futures-timer 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "substrate-client 2.0.0", @@ -4588,7 +4573,7 @@ dependencies = [ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "libsecp256k1 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parity-wasm 0.31.3 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -4616,7 +4601,7 @@ dependencies = [ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", "futures-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4644,7 +4629,7 @@ dependencies = [ name = "substrate-finality-grandpa-primitives" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "sr-std 2.0.0", @@ -4656,7 +4641,7 @@ dependencies = [ name = "substrate-inherents" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "sr-std 2.0.0", @@ -4708,7 +4693,7 @@ dependencies = [ "linked_hash_set 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "quickcheck 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4733,7 +4718,7 @@ dependencies = [ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "unsigned-varint 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "zeroize 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -4743,7 +4728,7 @@ dependencies = [ "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "futures-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "substrate-client 2.0.0", @@ -4801,14 +4786,14 @@ dependencies = [ "impl-serde 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "primitive-types 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hex 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "schnorrkel 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", + "schnorrkel 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-std 2.0.0", @@ -4817,7 +4802,7 @@ dependencies = [ "tiny-bip39 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "twox-hash 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "wasmi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "zeroize 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -4833,7 +4818,7 @@ dependencies = [ "jsonrpc-derive 12.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-pubsub 12.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hex 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4899,7 +4884,7 @@ dependencies = [ "node-primitives 2.0.0", "node-runtime 2.0.0", "parity-multiaddr 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4963,7 +4948,7 @@ version = "2.0.0" dependencies = [ "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "substrate-primitives 2.0.0", ] @@ -4976,7 +4961,7 @@ dependencies = [ "hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "substrate-panic-handler 2.0.0", "substrate-primitives 2.0.0", @@ -5012,7 +4997,7 @@ version = "2.0.0" dependencies = [ "futures-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", "hash-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "substrate-client 2.0.0", "substrate-client-db 2.0.0", @@ -5030,7 +5015,7 @@ dependencies = [ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "memory-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -5062,7 +5047,7 @@ dependencies = [ name = "substrate-test-runtime-client" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "substrate-primitives 2.0.0", "substrate-test-client 2.0.0", @@ -5078,7 +5063,7 @@ dependencies = [ "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "futures-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", @@ -5092,7 +5077,7 @@ version = "2.0.0" dependencies = [ "derive_more 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "substrate-client 2.0.0", @@ -5111,7 +5096,7 @@ dependencies = [ "hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "keccak-hasher 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "memory-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-std 2.0.0", "substrate-primitives 2.0.0", "trie-bench 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -5523,7 +5508,7 @@ name = "transaction-factory" version = "0.0.1" dependencies = [ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "substrate-cli 2.0.0", "substrate-client 2.0.0", @@ -5541,7 +5526,7 @@ dependencies = [ "hash-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "keccak-hasher 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "memory-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "trie-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "trie-root 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "trie-standardmap 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -5723,16 +5708,6 @@ name = "vec_map" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "vecarray" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "parity-codec 4.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", - "typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "vergen" version = "3.0.4" @@ -6082,7 +6057,7 @@ dependencies = [ [[package]] name = "zeroize" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "zeroize_derive 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -6125,7 +6100,6 @@ dependencies = [ "checksum bindgen 0.47.3 (registry+https://github.com/rust-lang/crates.io-index)" = "df683a55b54b41d5ea8ebfaebb5aa7e6b84e3f3006a78f010dadc9ca88469260" "checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" "checksum bitmask 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5da9b3d9f6f585199287a473f4f8dfab6566cf827d15c00c219f53c645687ead" -"checksum bitvec 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b67491e1cc6f37da6c4415cd743cb8d2e2c65388acc91ca3094a054cbf3cbd0c" "checksum bitvec 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9633b74910e1870f50f5af189b08487195cdb83c0e27a71d6f64d5e09dd0538b" "checksum blake2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "91721a6330935673395a0607df4d49a9cb90ae12d259f1b3e0a3f6e1d486872e" "checksum blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" @@ -6349,10 +6323,9 @@ dependencies = [ "checksum owning_ref 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cdf84f41639e037b484f93433aa3897863b561ed65c6e59c7073d7c561710f37" "checksum owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13" "checksum parity-bytes 0.1.0 (git+https://github.com/paritytech/parity-common?rev=b0317f649ab2c665b7987b8475878fc4d2e1f81d)" = "" -"checksum parity-codec 4.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2900f06356edf90de66a2922db622b36178dca71e85625eae58d0d9cc6cff2ac" "checksum parity-multiaddr 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "045b3c7af871285146300da35b1932bb6e4639b66c7c98e85d06a32cbc4e8fa7" "checksum parity-multihash 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "df3a17dc27848fd99e4f87eb0f8c9baba6ede0a6d555400c850ca45254ef4ce3" -"checksum parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "00fd14ff806ad82cea9a8f909bb116443d92efda7c9acd4502690af64741ad81" +"checksum parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "65582b5c02128a4b0fa60fb3e070216e9c84be3e4a8f1b74bc37e15a25e58daf" "checksum parity-scale-codec-derive 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a81f3cd93ed368a8e41c4e79538e99ca6e8f536096de23e3a0bc3e782093ce28" "checksum parity-send-wrapper 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aa9777aa91b8ad9dd5aaa04a9b6bcb02c7f1deb952fca5a66034d5e63afc5c6f" "checksum parity-util-mem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2005637ccf93dbb60c85081ccaaf3f945f573da48dcc79f27f9646caa3ec1dc" @@ -6394,7 +6367,7 @@ dependencies = [ "checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" "checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -"checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0" +"checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" "checksum rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "615e683324e75af5d43d8f7a39ffe3ee4a9dc42c5c701167a71dc59c3a493aca" "checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" "checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" @@ -6427,7 +6400,7 @@ dependencies = [ "checksum safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dca453248a96cb0749e36ccdfe2b0b4e54a61bfef89fb97ec621eb8e0a93dd9" "checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421" "checksum schannel 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "f2f6abf258d99c3c1c5c2131d99d064e94b7b3dd5f416483057f308fea253339" -"checksum schnorrkel 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "77e8d6a92f49a53f21b71c090a5559bf45c469071ebe556aebaf2dca3abc5cb5" +"checksum schnorrkel 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)" = "eacd8381b3c37840c9c9f40472af529e49975bdcbc24f83c31059fd6539023d3" "checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" "checksum sct 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f5adf8fbd58e1b1b52699dc8bed2630faecb6d8c7bee77d009d6bbe4af569b9" @@ -6530,7 +6503,6 @@ dependencies = [ "checksum utf8-ranges 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9d50aa7650df78abf942826607c62468ce18d9019673d4a2ebe1865dbb96ffde" "checksum vcpkg 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "33dd455d0f96e90a75803cfeb7f948768c08d70a6de9a8d2362461935698bf95" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" -"checksum vecarray 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d4d68a73b7d7d950c6558b6009e9fba229fb67562bda9fd02198f614f4ecf83f" "checksum vergen 3.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6aba5e34f93dc7051dfad05b98a18e9156f27e7b431fe1d2398cb6061c0a1dba" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" @@ -6567,5 +6539,5 @@ dependencies = [ "checksum xdg 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" "checksum yaml-rust 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992" "checksum yamux 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "01bd67889938c48f0049fc60a77341039e6c3eaf16cb7693e6ead7c0ba701295" -"checksum zeroize 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4177936c03b5a349c1b8e4509c46add268e66bc66fe92663729fa0570fe4f213" +"checksum zeroize 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "45af6a010d13e4cf5b54c94ba5a2b2eba5596b9e46bf5875612d332a1f2b3f86" "checksum zeroize_derive 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "afd1469e4bbca3b96606d26ba6e9bd6d3aed3b1299c82b92ec94377d22d78dbc" diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index ad6752ede265d..5855b867bc818 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -14,31 +14,55 @@ // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . +//! # Phragmen Election Module. +//! //! An election module based on sequential phragmen. //! -//! The election happens in _rounds_: every `N` blocks, the all previous members are retired and a -//! new set is elected (which may or may not have some of the same members). Each round lasts for -//! some number of blocks defined by `TermDuration` storage item. Each cycle of election is a term. +//! ### Term and Round +//! +//! The election happens in _rounds_: every `N` blocks, all previous members are retired and a new +//! set is elected (which may or may not have an intersection with the previous set). Each round +//! lasts for some number of blocks defined by `TermDuration` storage item. The works _term_ and +//! _round_ can be used interchangeably in this context. //! //! `TermDuration` might change during a round. This can shorten or extend the length of the round. //! The next election round's block number is never stored but rather always checked on the fly. -//! Based on the current block number and `TermDuration`, `BlockNumber % TermDuration == 0` will +//! Based on the current block height and `TermDuration`, `BlockNumber % TermDuration == 0` will //! always trigger a new election round. //! +//! ### Voting +//! //! Voters can vote for as many of the candidates by providing a list of account ids. Invalid votes -//! (voting for non-candidates) are ignored during election. Voters reserve a bond as they vote. -//! The entire free balance of a voter is locked upon voting and can only be used to pay for fees. -//! Voters can update their votes by calling `vote()` again during the same round. This does not -//! effect the reserved bond or lock. After a term, voters _must_ call `remove_voter` to get their -//! bond back and remove the lock. Otherwise the bond amount will stay reserved and the lock will -//! persist. Furthermore, voters of an old round cannot vote for the current round until they remove -//! their previous data via `remove_voter`. +//! (voting for non-candidates) are ignored during election. Yet, a voter _might_ vote for a +//! future candidate. Voters reserve a bond as they vote. Each vote defines a `value`. This amount +//! is locked from the account of the voter and indicated the weight of the vote. Voters can update +//! their votes at any time by calling `vote()` again. This keeps the bond untouched but can +//! optionally change the locked `value`. After a round, votes are kept and might still be valid +//! for further rounds. A voter is responsible to call `remove_voter` upon once they are done to +//! have their bond back and remove the lock. +//! +//! ### Candidacy and Members //! //! Candidates also reserve a bond as they submit candidacy. A candidate cannot take their candidacy -//! back. Winner candidates will be moved to the members list and will get their bond back at end of -//! their term. Loser candidates will immediately get their bond back. Note that unlike phragmen in -//! staking, candidates do NOT automatically vote for themselves (though they _could_ via a separate -//! transaction). Furthermore, the amount of tokens (stake) held by the candidate does not matter. +//! back. A candidate can end up in one of the below situations: +//! - **Winner**: A winner is kept as a _member_. They must still have a bond in reserve +//! and they are automatically counted as a candidate for the next election. +//! - **Runner-up**: Runner ups are the best candidates immediately after the winners. The number +//! of runner-ups to keep is configurable. Runner-ups serve one major role: if a member is +//! forcefully removed, the next best runner-up is chosen as a replacement. If not, a new +//! election is triggered. +//! - **Loser**: Any of the candidate who are not a winner are left as losers. A loser might be an +//! _outgoing member_, meaning that they are an active member who failed to keep their spot. In +//! this case, the outgoing member will get their bond back. Otherwise, the bond is slashed from +//! the loser candidate. +//! +//! Note that with the members being the default candidates for the next round and votes +//! persisting in storage, the election system is entirely stable given no further input. This means +//! that if the system has a particular set of candidates `C` and voters `V` that lead to a +//! set of members `M` being elected, as long as `V` and `C` don't remove their candidacy and votes, +//! `M` will keep being re-elected at the end of each round. +//! +//! ### Module Information //! //! - [`election_phragmen::Trait`](./trait.Trait.html) //! - [`Call`](./enum.Call.html) @@ -52,14 +76,19 @@ use sr_primitives::phragmen; use srml_support::{StorageValue, StorageMap, EnumerableStorageMap, decl_storage, decl_event, ensure, decl_module, dispatch, traits::{Currency, Get, LockableCurrency, LockIdentifier, ReservableCurrency, WithdrawReasons, - ChangeMembers + ChangeMembers, OnUnbalanced, } }; use system::{self, ensure_signed, ensure_root}; const MODULE_ID: LockIdentifier = *b"phrelect"; +/// The maximum votes allowed per voter. +pub const MAXIMUM_VOTE: usize = 16; + type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; +type NegativeImbalanceOf = + <::Currency as Currency<::AccountId>>::NegativeImbalance; pub trait Trait: system::Trait { /// The overarching event type.c @@ -82,6 +111,12 @@ pub trait Trait: system::Trait { /// How much should be locked up in order to be able to submit votes. type VotingBond: Get>; + + /// Handler for the unbalanced reduction when a candidate has lost (and is not a runner-up) + type LoserCandidate: OnUnbalanced>; + + /// Handler for the unbalanced reduction when a member has been kicked. + type KickedMember: OnUnbalanced>; } decl_storage! { @@ -89,13 +124,17 @@ decl_storage! { // ---- parameters /// Number of members to elect. pub DesiredMembers get(desired_members) config(): u32; + /// Number of runner-ups to store. + pub DesiredRunnerUps get(desired_runner_ups) config(): u32; /// How long each seat is kept. This defined the next block number at which an election /// round will happen. pub TermDuration get(term_duration) config(): T::BlockNumber; // ---- State - /// The current elected membership. Sorted. + /// The current elected membership. Sorted based on account id (low to high). pub Members get(members) config(): Vec; + /// The current runner-ups. Sorted based on **stake** (low to high). + pub RunnerUps get(runner_ups): Vec; /// The total number of vote rounds that have happened, exclusive of the upcoming one. pub ElectionRounds get(election_rounds): u32 = Zero::zero(); @@ -104,7 +143,7 @@ decl_storage! { /// Locked stake of a voter. pub StakeOf get(stake_of): map T::AccountId => BalanceOf; - /// The present candidate list. Always sorted. + /// The present candidate list. Sorted based on account id. pub Candidates get(candidates): Vec; /// Current number of active candidates. pub CandidateCount get(candidate_count): u32; @@ -124,8 +163,6 @@ decl_module! { /// - not be empty. /// - be less than the number of candidates. /// - /// `who` cannot be an old voter who has not yet called [`remove_voter`]. - /// /// Upon voting, `value` units of `who`'s balance is locked and a bond amount is reserved. /// It is the responsibility of the caller to not place all of their balance into the lock /// and keep some for further transactions. @@ -133,46 +170,44 @@ decl_module! { fn vote(origin, votes: Vec, value: BalanceOf) { let who = ensure_signed(origin)?; - // TODO: use decode_len #3071. - let candidates = Self::candidates(); - ensure!(!candidates.is_empty(), "number of candidates cannot be zero"); - ensure!(candidates.len() >= votes.len(), "cannot vote more than candidates"); + // TODO: use decode_len #3071 and remove candidate_count. + let candidates_count = Self::candidate_count(); + let allowed_votes = candidates_count as usize + Self::members().len(); + ensure!(!allowed_votes.is_zero(), "number of candidates cannot be zero"); + ensure!(votes.len() <= allowed_votes, "cannot vote more than candidates"); + ensure!(votes.len() <= MAXIMUM_VOTE, "cannot vote more than maximum allowed"); ensure!(!votes.is_empty(), "must vote for at least one candidate."); - ensure!(!Self::is_old_voter(&who), "must first remove self as voter and re-submit"); + ensure!(value > T::Currency::minimum_balance(), "cannot vote with stake less than min balance"); - if !Self::is_current_voter(&who) { - // Amount to be locked up. - let locked_balance = value.min(T::Currency::total_balance(&who)); - - // lock and reserve + if !Self::is_voter(&who) { + // first time voter. Reserve bond. T::Currency::reserve(&who, T::VotingBond::get()) .map_err(|_| "voter can not pay voting bond")?; - T::Currency::set_lock( - MODULE_ID, - &who, - locked_balance, - T::BlockNumber::max_value(), - WithdrawReasons::all(), - ); - >::insert(&who, locked_balance); } + // Amount to be locked up. + let locked_balance = value.min(T::Currency::total_balance(&who)); + + // lock + T::Currency::set_lock( + MODULE_ID, + &who, + locked_balance, + T::BlockNumber::max_value(), + WithdrawReasons::all(), + ); + >::insert(&who, locked_balance); let now = Self::election_rounds(); >::insert(&who, (votes, now)); } - /// Remove `origin` as a voter. One can use this to - /// 1. _undo_ their votes before the election has started. - /// 2. Remove their lock and unreserve the bond after an election round. + /// Remove `origin` as a voter. This removes the lock and returns the bond. #[weight = SimpleDispatchInfo::FixedNormal(500_000)] fn remove_voter(origin) { let who = ensure_signed(origin)?; - ensure!( - Self::is_current_voter(&who) || >::exists(&who), - "should be a current/old voter" - ); + ensure!(Self::is_voter(&who), "must be a voter"); // remove storage. >::remove(&who); @@ -186,14 +221,20 @@ decl_module! { /// Submit oneself for candidacy. /// /// A candidate will either: - /// - Lose at the end of the term and gets their bond unreserved - /// - Win and become a member. Members will get their bond back at the end ot their term. + /// - Lose at the end of the term and get slashed. + /// - Win and become a member. Members will eventually get their stash back. + /// - Become a runner-up. Runner-ups are reserved members in case one gets forcefully + /// removed. #[weight = SimpleDispatchInfo::FixedNormal(1_000_000)] fn submit_candidacy(origin) { let who = ensure_signed(origin)?; let is_candidate = Self::is_candidate(&who); ensure!(!is_candidate.is_ok(), "duplicate candidate submission"); + ensure!( + Self::members().binary_search(&who).is_err(), + "member cannot re-submit candidacy" + ); // assured to be an error, error always contains the index. let index = is_candidate.unwrap_err(); @@ -211,33 +252,54 @@ decl_module! { DesiredMembers::put(count); } - /// Remove a particular member from the set. This is effective immediately. No further - /// election is enforced. + /// Remove a particular member from the set. This is effective immediately. + /// + /// If a runner-up si available, then the best runner-up will be removed and replaces the + /// outgoing member. Otherwise, a new phragmen round is started. + /// + /// Note that this does not affect the designated block number of the next election. #[weight = SimpleDispatchInfo::FixedOperational(10_000)] fn remove_member(origin, who: ::Source) { ensure_root(origin)?; let who = T::Lookup::lookup(who)?; - let mut members = Self::members(); if let Ok(index) = members.binary_search(&who) { + // remove, slash, emit event. members.remove(index); - - T::Currency::unreserve(&who, T::CandidacyBond::get()); - >::put(members.clone()); + let (imbalance, _) = T::Currency::slash_reserved(&who, T::CandidacyBond::get()); + T::KickedMember::on_unbalanced(imbalance); Self::deposit_event(RawEvent::MemberKicked(who.clone())); - T::ChangeMembers::change_members(&[], &[who], members); + + let mut runner_ups = Self::runner_ups(); + if let Some(replacement) = runner_ups.pop() { + // replace the outgoing with the best runner up. + if let Err(index) = members.binary_search(&replacement) { + members.insert(index, replacement.clone()); + ElectionRounds::mutate(|v| *v += 1); + T::ChangeMembers::change_members_sorted(&[replacement], &[who], &members); + } + // else it would mean that the runner up was already a candidate. This cannot + // happen. If it does, not much that we can do about it. + + >::put(members); + >::put(runner_ups); + } else { + // trigger a new phragmen. grab a cup of coffee. This might take a while. + >::put(members); + Self::do_phragmen(); + } } } - /// Set the presentation duration. If there is current a vote being presented for, will - /// invoke `finalize_vote`. + /// Set the duration of each term. This will affect the next election's block number. #[weight = SimpleDispatchInfo::FixedOperational(10_000)] fn set_term_duration(origin, #[compact] count: T::BlockNumber) { ensure_root(origin)?; >::put(count); } + /// What to do at the end of each block. Checks if an election needs to happen or not. fn on_initialize(n: T::BlockNumber) { if let Err(e) = Self::end_block(n) { runtime_io::print("Guru meditation"); @@ -249,9 +311,10 @@ decl_module! { decl_event!( pub enum Event where ::AccountId { - /// A new term with new members. + /// A new term with new members. This indicates that enough candidates existed, not that + /// enough have has been elected. The inner value must be examined for this purpose. NewTerm(Vec), - /// No candidates were elected for this round. + /// No (or not enough) candidates existed for this round. EmptyTerm, /// A member has been removed. This should always be followed by either `NewTerm` ot /// `EmptyTerm`. @@ -266,16 +329,9 @@ impl Module { Self::candidates().binary_search(who).map(|_| ()) } - /// Check if `who` is a voter in the ongoing round. - fn is_current_voter(who: &T::AccountId) -> bool { - let now = Self::election_rounds(); - Self::votes_of(who).1 == now && >::exists(who) - } - - /// Check if `who` was a voter who has not yet called `remove_voter`. - fn is_old_voter(who: &T::AccountId) -> bool { - let now = Self::election_rounds(); - Self::votes_of(who).1 < now && >::exists(who) + /// Check if `who` is a voter. It may or may not be a _current_ one. + fn is_voter(who: &T::AccountId) -> bool { + >::exists(who) } /// The locked stake of a voter. @@ -286,22 +342,28 @@ impl Module { /// Check there's nothing to do this block. /// /// Runs phragmen election and cleans all the previous candidate state. The voter state is NOT - /// cleaned and voters must themselves submit a transaction to clean it. + /// cleaned and voters must themselves submit a transaction to retract. fn end_block(block_number: T::BlockNumber) -> dispatch::Result { if (block_number % Self::term_duration()).is_zero() { Self::do_phragmen(); - ElectionRounds::mutate(|v| *v += 1); } Ok(()) } - /// Run the phragmen election with all required side processes. + /// Run the phragmen election with all required side processes and state updates. + /// + /// Calls the appropriate `ChangeMembers` function variant internally. fn do_phragmen() { - let num_to_elect = Self::desired_members() as usize; - let candidates = Self::candidates(); - let current_round = Self::election_rounds(); + let desired_seats = Self::desired_members() as usize; + let desired_runner_ups = Self::desired_runner_ups() as usize; + let num_to_elect = desired_runner_ups + desired_seats; + + // current members are always a candidate for the next round as well. + // this is guaranteed to not create any duplicates. + let mut candidates = Self::candidates(); + candidates.append(&mut Self::members()); let voters_and_votes = >::enumerate() - .filter_map(|(v, i)| if i.1 == current_round { Some((v, i.0)) } else { None } ) + .map(|(v, i)| (v, i.0)) .collect::)>>(); let maybe_new_members = phragmen::elect::<_, _, _, T::CurrencyToVote>( num_to_elect, @@ -312,41 +374,76 @@ impl Module { Self::locked_stake_of ); + let mut to_release_bond: Vec = Vec::with_capacity(desired_seats); let old_members = >::take(); if let Some((mut new_members_with_approval, _)) = maybe_new_members { - new_members_with_approval.sort_by_key(|x| x.0.clone()); + // sort based on stake to to split winner and runner ups. + new_members_with_approval.sort_by_key(|x| x.1.clone()); - let new_members = new_members_with_approval + // filter out those who had literally no votes at all. + let new_members_filtered = new_members_with_approval .into_iter() .filter_map(|(m, a)| if !a.is_zero() { Some(m) } else { None } ) .collect::>(); + + let mut new_members = new_members_filtered.iter() + .rev() + .take(desired_seats) + .map(|m| m.clone()) + .collect::>(); + new_members.sort(); >::put(new_members.clone()); - // return bond to losers. - candidates.into_iter().for_each(|c| { - // if this candidate is a loser return the bond - if new_members.binary_search(&c).is_err() { - T::Currency::unreserve(&c, T::CandidacyBond::get()); - } - }); + let mut allowed_runner_ups = new_members_filtered.len() - new_members.len(); + // defensive only. We should never have more leftover than desired runner ups. + allowed_runner_ups = allowed_runner_ups.min(desired_runner_ups); + let runner_ups = new_members_filtered.into_iter() + .take(allowed_runner_ups) + .collect::>(); + // should still be sorted by stake. Low to high. + >::put(runner_ups.clone()); + // report. We don't know/compute the diff here, therefore we call `set_member`. + let (incoming, outgoing) = T::ChangeMembers::compute_members_diff(&new_members, &old_members); T::ChangeMembers::change_members_sorted( - &new_members, - &old_members, - &new_members, + &incoming, + &outgoing.clone(), + &new_members ); + + to_release_bond = outgoing.to_vec(); + + // NOTE: it is unfortunate that we have to re-sort new_members and runner ups + // above, but their length is relatively small (few dozens at most) while it helps the + // below search of `O(candidates)` quite faster. + + // Burn loser bond. All three lists are sorted. O(3LogN) ~ O(LogN) + candidates.into_iter().for_each(|c| { + // a candidate who's + if new_members.binary_search(&c).is_err() // now a member + && outgoing.binary_search(&c).is_err() // or was a member + && runner_ups.binary_search(&c).is_err() // or a runner-up + { + // should ge their bond slashed. + let (imbalance, _) = T::Currency::slash_reserved(&c, T::CandidacyBond::get()); + T::LoserCandidate::on_unbalanced(imbalance); + } + }); Self::deposit_event(RawEvent::NewTerm(new_members)); } else { Self::deposit_event(RawEvent::EmptyTerm); } - // clean candidates // unreserve the bond of all the outgoings. - old_members.iter().for_each(|m| { + to_release_bond.iter().for_each(|m| { T::Currency::unreserve(&m, T::CandidacyBond::get()); }); + + // clean candidates. >::kill(); CandidateCount::put(0); + + ElectionRounds::mutate(|v| *v += 1); } } @@ -368,6 +465,7 @@ mod tests { pub const MaximumBlockLength: u32 = 2 * 1024; pub const AvailableBlockRatio: Perbill = Perbill::one(); } + impl system::Trait for Test { type Origin = Origin; type Index = u64; @@ -385,13 +483,15 @@ mod tests { type MaximumBlockLength = MaximumBlockLength; type AvailableBlockRatio = AvailableBlockRatio; } + parameter_types! { - pub const ExistentialDeposit: u64 = 0; + pub const ExistentialDeposit: u64 = 1; pub const TransferFee: u64 = 0; pub const CreationFee: u64 = 0; pub const TransactionBaseFee: u64 = 0; pub const TransactionByteFee: u64 = 0; } + impl balances::Trait for Test { type Balance = u64; type OnNewAccount = (); @@ -407,18 +507,19 @@ mod tests { type TransactionByteFee = TransactionByteFee; type WeightToFee = (); } + parameter_types! { pub const CandidacyBond: u64 = 3; } thread_local! { - static VOTER_BOND: RefCell = RefCell::new(2); + static VOTING_BOND: RefCell = RefCell::new(2); static MEMBERS: RefCell> = RefCell::new(vec![]); } pub struct VotingBond; impl Get for VotingBond { - fn get() -> u64 { VOTER_BOND.with(|v| *v.borrow()) } + fn get() -> u64 { VOTING_BOND.with(|v| *v.borrow()) } } pub struct TestChangeMembers; @@ -444,6 +545,8 @@ mod tests { type ChangeMembers = TestChangeMembers; type CandidacyBond = CandidacyBond; type VotingBond = VotingBond; + type LoserCandidate = (); + type KickedMember = (); } pub type Block = sr_primitives::generic::Block; @@ -464,6 +567,7 @@ mod tests { pub struct ExtBuilder { balance_factor: u64, voter_bond: u64, + desired_runner_ups: u32, } impl Default for ExtBuilder { @@ -471,6 +575,7 @@ mod tests { Self { balance_factor: 1, voter_bond: 2, + desired_runner_ups: 0, } } } @@ -480,8 +585,12 @@ mod tests { self.voter_bond = fee; self } + pub fn desired_runner_ups(mut self, count: u32) -> Self { + self.desired_runner_ups = count; + self + } pub fn build(self) -> runtime_io::TestExternalities { - VOTER_BOND.with(|v| *v.borrow_mut() = self.voter_bond); + VOTING_BOND.with(|v| *v.borrow_mut() = self.voter_bond); GenesisConfig { balances: Some(balances::GenesisConfig::{ balances: vec![ @@ -497,22 +606,21 @@ mod tests { elections: Some(elections::GenesisConfig::{ members: vec![], desired_members: 2, + desired_runner_ups: self.desired_runner_ups, term_duration: 5, }), }.build_storage().unwrap().into() } } - fn current_voters() -> Vec { - >::enumerate() - .filter(|(v, _)| Elections::is_current_voter(v)) - .map(|(v, _)| v).collect::>() - } - fn all_voters() -> Vec { >::enumerate().map(|(v, _)| v).collect::>() } + fn balances(who: &u64) -> (u64, u64) { + (Balances::free_balance(who), Balances::reserved_balance(who)) + } + #[test] fn params_should_work() { with_externalities(&mut ExtBuilder::default().build(), || { @@ -522,12 +630,12 @@ mod tests { assert_eq!(Elections::desired_members(), 2); assert_eq!(Elections::members(), vec![]); + assert_eq!(Elections::runner_ups(), vec![]); assert_eq!(Elections::candidates(), vec![]); assert_eq!(Elections::candidate_count(), 0); assert!(Elections::is_candidate(&1).is_err()); - assert_eq!(current_voters(), vec![]); assert_eq!(all_voters(), vec![]); assert_eq!(Elections::votes_of(&1).0, vec![]); }); @@ -570,6 +678,26 @@ mod tests { }); } + #[test] + fn member_candidacy_submission_should_not_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::vote(Origin::signed(2), vec![5], 20)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members(), vec![5]); + assert_eq!(Elections::runner_ups(), vec![]); + assert_eq!(Elections::candidates(), vec![]); + + assert_noop!( + Elections::submit_candidacy(Origin::signed(5)), + "member cannot re-submit candidacy" + ); + }); + } + #[test] fn poor_candidate_submission_should_not_work() { with_externalities(&mut ExtBuilder::default().build(), || { @@ -582,7 +710,7 @@ mod tests { } #[test] - fn vote_locks_entire_balance() { + fn vote_locks_balance() { with_externalities(&mut ExtBuilder::default().build(), || { assert_eq!(Elections::candidates(), Vec::::new()); assert_eq!(Balances::free_balance(&2), 20); @@ -599,7 +727,7 @@ mod tests { } #[test] - fn can_update_votes_in_current_round() { + fn can_update_votes() { with_externalities(&mut ExtBuilder::default().build(), || { assert_eq!(Balances::free_balance(&2), 20); assert_eq!(Balances::reserved_balance(&2), 0); @@ -612,10 +740,10 @@ mod tests { assert_eq!(Balances::reserved_balance(&2), 2); assert_eq!(Elections::stake_of(2), 20); - // can update; same stake; same lock and reserve. - assert_ok!(Elections::vote(Origin::signed(2), vec![5, 4], 20)); + // can update; different stake; different lock and reserve. + assert_ok!(Elections::vote(Origin::signed(2), vec![5, 4], 15)); assert_eq!(Balances::reserved_balance(&2), 2); - assert_eq!(Elections::stake_of(2), 20); + assert_eq!(Elections::stake_of(2), 15); }); } @@ -643,7 +771,20 @@ mod tests { } #[test] - fn cannot_vote_for_more_than_total_balance() { + fn cannot_vote_for_less_than_ed() { + with_externalities(&mut ExtBuilder::default().voter_bond(8).build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + + assert_noop!( + Elections::vote(Origin::signed(2), vec![4], 1), + "cannot vote with stake less than min balance" + ); + }) + } + + #[test] + fn can_vote_for_more_than_total_balance_but_moot() { with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); @@ -663,7 +804,6 @@ mod tests { assert_ok!(Elections::vote(Origin::signed(3), vec![5], 30)); assert_eq_uvec!(all_voters(), vec![2, 3]); - assert_eq_uvec!(all_voters(), current_voters()); assert_eq!(Elections::stake_of(2), 20); assert_eq!(Elections::stake_of(3), 30); assert_eq!(Elections::votes_of(2), (vec![5], 0)); @@ -672,7 +812,6 @@ mod tests { assert_ok!(Elections::remove_voter(Origin::signed(2))); assert_eq!(all_voters(), vec![3]); - assert_eq!(current_voters(), vec![3]); assert_eq!(Elections::votes_of(2), (vec![], 0)); assert_eq!(Elections::stake_of(2), 0); @@ -685,10 +824,7 @@ mod tests { #[test] fn non_voter_remove_should_not_work() { with_externalities(&mut ExtBuilder::default().build(), || { - assert_noop!( - Elections::remove_voter(Origin::signed(3)), - "should be a current/old voter" - ); + assert_noop!(Elections::remove_voter(Origin::signed(3)), "must be a voter"); }); } @@ -700,12 +836,8 @@ mod tests { assert_ok!(Elections::remove_voter(Origin::signed(2))); assert_eq!(all_voters(), vec![]); - assert_eq!(current_voters(), vec![]); - assert_noop!( - Elections::remove_voter(Origin::signed(2)), - "should be a current/old voter" - ); + assert_noop!(Elections::remove_voter(Origin::signed(2)), "must be a voter"); }); } @@ -729,70 +861,126 @@ mod tests { }); } + #[test] + fn simple_voting_rounds_should_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + + assert_ok!(Elections::vote(Origin::signed(2), vec![5], 20)); + assert_ok!(Elections::vote(Origin::signed(3), vec![5], 30)); + + assert_eq_uvec!(all_voters(), vec![2, 3]); + + assert_eq!(Elections::votes_of(2).0, vec![5]); + assert_eq!(Elections::votes_of(3).0, vec![5]); + + assert_eq!(Elections::candidates(), vec![5]); + assert_eq!(Elections::candidate_count(), 1); + + assert_eq!(Elections::election_rounds(), 0); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members(), vec![5]); + assert_eq!(Elections::runner_ups(), vec![]); + assert_eq_uvec!(all_voters(), vec![2, 3]); + assert_eq!(Elections::candidates(), vec![]); + assert_eq!(Elections::candidate_count(), 0); + assert_eq!(Elections::election_rounds(), 1); + }); + } #[test] - fn old_voter_should_not_be_counted() { + fn old_voter_will_be_counted() { with_externalities(&mut ExtBuilder::default().build(), || { + // TODO: add to mopdule doc the tip that: one can always submit a candidacy and vote but not the other way around. assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::submit_candidacy(Origin::signed(4))); - assert_ok!(Elections::submit_candidacy(Origin::signed(3))); - assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); - assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + // This guys vote is pointless for this round. + assert_ok!(Elections::vote(Origin::signed(3), vec![4], 30)); assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::members(), vec![4, 5]); + assert_eq!(Elections::members(), vec![5]); assert_eq!(Elections::election_rounds(), 1); + // but now it has a valid target. + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + // meanwhile, no one cares to become a candidate again. System::set_block_number(10); assert_ok!(Elections::end_block(System::block_number())); // none of the votes matter anymore - assert_eq!(Elections::members(), vec![]); + assert_eq!(Elections::members(), vec![4, 5]); assert_eq!(Elections::election_rounds(), 2); - assert_eq!(current_voters(), vec![]); - assert_eq_uvec!(all_voters(), vec![3, 4, 5]); + assert_eq_uvec!(all_voters(), vec![3, 5]); }); } #[test] - fn voting_rounds_should_work() { + fn only_desired_seats_are_chosen() { with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_ok!(Elections::submit_candidacy(Origin::signed(2))); - assert_ok!(Elections::vote(Origin::signed(2), vec![5], 20)); - assert_ok!(Elections::vote(Origin::signed(3), vec![5], 30)); + assert_ok!(Elections::vote(Origin::signed(2), vec![2], 20)); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); - assert_eq_uvec!(current_voters(), vec![2, 3]); - assert_eq!(Elections::votes_of(2).0, vec![5]); - assert_eq!(Elections::votes_of(3).0, vec![5]); + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::candidates(), vec![5]); - assert_eq!(Elections::candidate_count(), 1); + assert_eq!(Elections::election_rounds(), 1); + assert_eq!(Elections::members(), vec![4, 5]); + }); + } - assert_eq!(Elections::election_rounds(), 0); + #[test] + fn phragmen_should_not_self_vote() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(current_voters(), vec![]); - assert_eq_uvec!(all_voters(), vec![2, 3]); - assert_eq!(Elections::candidates(), vec![]); - assert_eq!(Elections::candidate_count(), 0); - assert_eq!(Elections::election_rounds(), 1); + assert_eq!(Elections::members(), vec![]); + assert_eq!(Elections::runner_ups(), vec![]); + }); + } - assert_eq!(Elections::members(), vec![5]); + #[test] + fn runner_ups_should_be_kept() { + with_externalities(&mut ExtBuilder::default().desired_runner_ups(1).build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + + assert_ok!(Elections::vote(Origin::signed(2), vec![3], 20)); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![3, 5]); + assert_eq!(Elections::runner_ups(), vec![4]); + assert_eq!(Balances::reserved_balance(&4), 3 + 2); + assert_eq!(Balances::free_balance(&4), 35); }); } #[test] - fn only_desired_seats_are_chosen() { - with_externalities(&mut ExtBuilder::default().build(), || { + fn current_members_are_always_implicitly_next_candidate() { + with_externalities(&mut ExtBuilder::default().desired_runner_ups(1).build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); @@ -806,15 +994,123 @@ mod tests { System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![4, 5]); + assert_eq!(Elections::runner_ups(), vec![3]); + assert_eq!(Elections::election_rounds(), 1); + + // 4, 5 will persist as candidates despite not being in the list. assert_eq!(Elections::candidates(), vec![]); + System::set_block_number(10); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members(), vec![4, 5]); + assert_eq!(Elections::runner_ups(), vec![]); + }); + } + + #[test] + fn election_state_is_uninterrupted() { + with_externalities(&mut ExtBuilder::default().desired_runner_ups(1).build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + + let check_at_block = |b: u32| { + System::set_block_number(b.into()); + assert_ok!(Elections::end_block(System::block_number())); + // we keep re-electing the same folks. + assert_eq!(Elections::members(), vec![4, 5]); + assert_eq!(Elections::election_rounds(), b / 5); + assert_eq_uvec!(all_voters(), vec![5, 4]); + }; + + // this state will always persist when no further input is given. + check_at_block(5); + check_at_block(10); + check_at_block(15); + check_at_block(20); + }); + } + + #[test] + fn remove_members_triggers_election_if_no_runner_up() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![4, 5]); assert_eq!(Elections::election_rounds(), 1); + // a new candidate is also there waiting. + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + + assert_ok!(Elections::remove_member(Origin::ROOT, 4)); + + assert_eq!(balances(&4), (35, 2)); // slashed + assert_eq!(Elections::election_rounds(), 2); // new election round + assert_eq!(Elections::members(), vec![3, 5]); // new members + }); + } + + #[test] + fn unexpected_election_will_use_runner_ups_if_exists() { + with_externalities(&mut ExtBuilder::default().desired_runner_ups(1).build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![4, 5]); + assert_eq!(Elections::runner_ups(), vec![3]); + assert_eq!(Elections::election_rounds(), 1); + + assert_ok!(Elections::remove_member(Origin::ROOT, 4)); + + assert_eq!(balances(&4), (35, 2)); // slashed + assert_eq!(Elections::election_rounds(), 2); // new election round + assert_eq!(Elections::members(), vec![3, 5]); // new members + assert_eq!(Elections::runner_ups(), vec![]); }); } #[test] - fn seats_should_be_released() { + fn runner_ups_are_sorted_by_stake() { + with_externalities(&mut ExtBuilder::default().desired_runner_ups(3).build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_ok!(Elections::submit_candidacy(Origin::signed(2))); + assert_ok!(Elections::submit_candidacy(Origin::signed(1))); + + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 9)); + assert_ok!(Elections::vote(Origin::signed(2), vec![2], 8)); + assert_ok!(Elections::vote(Origin::signed(1), vec![1], 10)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![4, 5]); + assert_eq!(Elections::runner_ups(), vec![2, 3, 1]); + assert_eq!(Elections::election_rounds(), 1); + }); + } + + #[test] + fn seats_should_be_released_when_no_vote() { with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); @@ -824,7 +1120,7 @@ mod tests { assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); -; + assert_eq!(Elections::candidate_count(), 3); assert_eq!(Elections::election_rounds(), 0); @@ -834,6 +1130,11 @@ mod tests { assert_eq!(Elections::members(), vec![3, 5]); assert_eq!(Elections::election_rounds(), 1); + assert_ok!(Elections::remove_voter(Origin::signed(2))); + assert_ok!(Elections::remove_voter(Origin::signed(3))); + assert_ok!(Elections::remove_voter(Origin::signed(4))); + assert_ok!(Elections::remove_voter(Origin::signed(5))); + // meanwhile, no one cares to become a candidate again. System::set_block_number(10); assert_ok!(Elections::end_block(System::block_number())); @@ -845,36 +1146,33 @@ mod tests { #[test] fn outgoing_will_get_the_bond_back() { with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(Balances::total_balance(&5), 50); + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::submit_candidacy(Origin::signed(4))); - assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_eq!(Balances::reserved_balance(&5), 3); - assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); - assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); - assert_eq!(Balances::reserved_balance(&5), 3 + 2); System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![5]); - assert_eq!(Elections::members(), vec![4, 5]); - assert_eq!(Elections::election_rounds(), 1); - assert_eq!(Balances::reserved_balance(&5), 3 + 2); - + assert_ok!(Elections::remove_voter(Origin::signed(5))); + assert_eq!(Balances::reserved_balance(&5), 3); // meanwhile, no one cares to become a candidate again. System::set_block_number(10); assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![]); - assert_eq!(Elections::election_rounds(), 2); - assert_eq!(Balances::reserved_balance(&5), 2); - assert_eq!(Balances::reserved_balance(&4), 2); + + assert_eq!(Balances::reserved_balance(&5), 0); + assert_eq!(Balances::total_balance(&5), 50); }); } #[test] - fn losers_will_get_the_bond_back_after_remove() { - with_externalities(&mut ExtBuilder::default().voter_bond(2).build(), || { + fn losers_will_lose_the_bond() { + with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); @@ -890,75 +1188,72 @@ mod tests { assert_eq!(Elections::members(), vec![5]); assert_eq!(Balances::reserved_balance(&5), 3); // winner + assert_eq!(Balances::reserved_balance(&3), 0); // loser + assert_eq!(Balances::free_balance(&3), 27); // loser }); } #[test] - fn invalid_votes_are_moot() { - with_externalities(&mut ExtBuilder::default().build(), || { + fn incoming_outgoing_are_reported() { + with_externalities(&mut ExtBuilder::default().desired_runner_ups(1).build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(4))); - assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); - assert_ok!(Elections::vote(Origin::signed(5), vec![10], 50)); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); - assert_eq_uvec!(Elections::members(), vec![3, 4]); - assert_eq!(Elections::election_rounds(), 1); - }); - } - - #[test] - fn consequent_vote_without_remove_should_fail() { - with_externalities(&mut ExtBuilder::default().voter_bond(2).build(), || { - assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::vote(Origin::signed(3), vec![5], 30)); + assert_ok!(Elections::submit_candidacy(Origin::signed(1))); + assert_ok!(Elections::submit_candidacy(Origin::signed(2))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); - assert_eq!(Balances::reserved_balance(&5), 3); - assert_eq!(Balances::reserved_balance(&3), 2); + assert_ok!(Elections::vote(Origin::signed(5), vec![4], 8)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + assert_ok!(Elections::vote(Origin::signed(2), vec![2], 20)); + assert_ok!(Elections::vote(Origin::signed(1), vec![1], 10)); - System::set_block_number(5); + System::set_block_number(10); assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::members(), vec![5]); - assert_eq!(Elections::election_rounds(), 1); - assert_ok!(Elections::submit_candidacy(Origin::signed(2))); + // 3, 4 are new members, must still be bonded + assert_eq!(Elections::members(), vec![3, 4]); + assert_eq!(balances(&3), (25, 5)); + assert_eq!(balances(&4), (35, 5)); - assert_eq!(Balances::reserved_balance(&5), 3); - assert_eq!(Balances::reserved_balance(&2), 3); - assert_eq!(Balances::reserved_balance(&3), 2); + // 2 is the runner up, must still be bonded + assert_eq!(Elections::runner_ups(), vec![2]); + assert_eq!(balances(&2), (15, 5)); - assert_noop!( - Elections::vote(Origin::signed(3), vec![2], 30), - "must first remove self as voter and re-submit" - ); - assert_ok!(Elections::remove_voter(Origin::signed(3))); - assert_eq!(Balances::reserved_balance(&3), 0); + // 1 is a loser, will get slashed. + assert_eq!(Balances::reserved_balance(&1), 2); + assert_eq!(balances(&1), (5, 2)); - assert_ok!(Elections::vote(Origin::signed(3), vec![2], 30)); + // 5 is an outgoing loser, it will get their bond back. + assert_eq!(balances(&5), (48, 2)); - System::set_block_number(10); - assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::members(), vec![2]); - assert_eq!(Balances::reserved_balance(&5), 0); + assert_eq!(System::events()[0].event, Event::elections(RawEvent::NewTerm(vec![4, 5]))); }) } #[test] - fn candidate_stake_does_not_matter() { - with_externalities(&mut ExtBuilder::default().voter_bond(2).build(), || { - assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + fn invalid_votes_are_moot() { + with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![10], 50)); + System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::members(), vec![]); - }) + assert_eq_uvec!(Elections::members(), vec![3, 4]); + assert_eq!(Elections::election_rounds(), 1); + }); } } diff --git a/srml/support/src/traits.rs b/srml/support/src/traits.rs index a7d8c9e6ba0fa..50e15a0452983 100644 --- a/srml/support/src/traits.rs +++ b/srml/support/src/traits.rs @@ -652,6 +652,16 @@ pub trait ChangeMembers { /// Set the new members; they **must already be sorted**. This will compute the diff and use it to /// call `change_members_sorted`. fn set_members_sorted(new_members: &[AccountId], old_members: &[AccountId]) { + let (incoming, outgoing) = Self::compute_members_diff(new_members, old_members); + Self::change_members_sorted(&incoming[..], &outgoing[..], &new_members); + } + + /// Set the new members; they **must already be sorted**. This will compute the diff and use it to + /// call `change_members_sorted`. + fn compute_members_diff( + new_members: &[AccountId], + old_members: &[AccountId] + ) -> (Vec, Vec) { let mut old_iter = old_members.iter(); let mut new_iter = new_members.iter(); let mut incoming = Vec::new(); @@ -679,8 +689,7 @@ pub trait ChangeMembers { } } } - - Self::change_members_sorted(&incoming[..], &outgoing[..], &new_members); + (incoming, outgoing) } } @@ -698,4 +707,4 @@ pub trait InitializeMembers { impl InitializeMembers for () { fn initialize_members(_: &[T]) {} -} \ No newline at end of file +} From cf15877a64d69ae0fa1df208385c73023a58ce0a Mon Sep 17 00:00:00 2001 From: kianenigma Date: Sat, 17 Aug 2019 16:57:54 +0200 Subject: [PATCH 13/41] Bit more sensical weight values. --- srml/elections-phragmen/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 5855b867bc818..8acc4c8838d43 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -166,7 +166,7 @@ decl_module! { /// Upon voting, `value` units of `who`'s balance is locked and a bond amount is reserved. /// It is the responsibility of the caller to not place all of their balance into the lock /// and keep some for further transactions. - #[weight = SimpleDispatchInfo::FixedNormal(750_000)] + #[weight = SimpleDispatchInfo::FixedNormal(500_000)] fn vote(origin, votes: Vec, value: BalanceOf) { let who = ensure_signed(origin)?; @@ -225,7 +225,7 @@ decl_module! { /// - Win and become a member. Members will eventually get their stash back. /// - Become a runner-up. Runner-ups are reserved members in case one gets forcefully /// removed. - #[weight = SimpleDispatchInfo::FixedNormal(1_000_000)] + #[weight = SimpleDispatchInfo::FixedNormal(500_000)] fn submit_candidacy(origin) { let who = ensure_signed(origin)?; @@ -258,7 +258,7 @@ decl_module! { /// outgoing member. Otherwise, a new phragmen round is started. /// /// Note that this does not affect the designated block number of the next election. - #[weight = SimpleDispatchInfo::FixedOperational(10_000)] + #[weight = SimpleDispatchInfo::FixedOperational(2_000_000)] fn remove_member(origin, who: ::Source) { ensure_root(origin)?; let who = T::Lookup::lookup(who)?; From 0e8af21d872021253bd8d065d18b346bb245a71e Mon Sep 17 00:00:00 2001 From: Gavin Wood Date: Thu, 22 Aug 2019 11:11:09 +0200 Subject: [PATCH 14/41] Update srml/elections-phragmen/src/lib.rs --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 8acc4c8838d43..71bcefa81fab7 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -22,7 +22,7 @@ //! //! The election happens in _rounds_: every `N` blocks, all previous members are retired and a new //! set is elected (which may or may not have an intersection with the previous set). Each round -//! lasts for some number of blocks defined by `TermDuration` storage item. The works _term_ and +//! lasts for some number of blocks defined by `TermDuration` storage item. The words _term_ and //! _round_ can be used interchangeably in this context. //! //! `TermDuration` might change during a round. This can shorten or extend the length of the round. From c4cf8bc79197d9b1be5a0fcef0b99302d8885ab6 Mon Sep 17 00:00:00 2001 From: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Date: Thu, 22 Aug 2019 11:41:08 +0200 Subject: [PATCH 15/41] Update srml/elections-phragmen/src/lib.rs Co-Authored-By: Gavin Wood --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 71bcefa81fab7..dc2e197358cc9 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -32,7 +32,7 @@ //! //! ### Voting //! -//! Voters can vote for as many of the candidates by providing a list of account ids. Invalid votes +//! Voters can vote for any set of the candidates by providing a list of account ids. Invalid votes //! (voting for non-candidates) are ignored during election. Yet, a voter _might_ vote for a //! future candidate. Voters reserve a bond as they vote. Each vote defines a `value`. This amount //! is locked from the account of the voter and indicated the weight of the vote. Voters can update From 20415cab08be706a18c7e83463e0e0f51273d1ba Mon Sep 17 00:00:00 2001 From: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Date: Thu, 22 Aug 2019 11:41:30 +0200 Subject: [PATCH 16/41] Update srml/elections-phragmen/src/lib.rs Co-Authored-By: Gavin Wood --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index dc2e197358cc9..b655b76be8438 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -38,7 +38,7 @@ //! is locked from the account of the voter and indicated the weight of the vote. Voters can update //! their votes at any time by calling `vote()` again. This keeps the bond untouched but can //! optionally change the locked `value`. After a round, votes are kept and might still be valid -//! for further rounds. A voter is responsible to call `remove_voter` upon once they are done to +//! for further rounds. A voter is responsible for calling `remove_voter` once they are done to //! have their bond back and remove the lock. //! //! ### Candidacy and Members From 6e5c78062b0362d9a4279fb7a45194be16dc16f4 Mon Sep 17 00:00:00 2001 From: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Date: Thu, 22 Aug 2019 11:41:47 +0200 Subject: [PATCH 17/41] Update srml/elections-phragmen/src/lib.rs Co-Authored-By: Gavin Wood --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index b655b76be8438..5e0b326cc4414 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -35,7 +35,7 @@ //! Voters can vote for any set of the candidates by providing a list of account ids. Invalid votes //! (voting for non-candidates) are ignored during election. Yet, a voter _might_ vote for a //! future candidate. Voters reserve a bond as they vote. Each vote defines a `value`. This amount -//! is locked from the account of the voter and indicated the weight of the vote. Voters can update +//! is locked from the account of the voter and indicates the weight of the vote. Voters can update //! their votes at any time by calling `vote()` again. This keeps the bond untouched but can //! optionally change the locked `value`. After a round, votes are kept and might still be valid //! for further rounds. A voter is responsible for calling `remove_voter` once they are done to From dbfaec3af69e4129f4a3a3e38f5ddd0bd9c2c912 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Thu, 22 Aug 2019 11:47:46 +0200 Subject: [PATCH 18/41] fix lock file --- Cargo.lock | 67 ++++++++++-------------------------------------------- 1 file changed, 12 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77067622c03fc..e6a25e5356eb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2330,12 +2330,8 @@ version = "2.0.0" dependencies = [ "node-primitives 2.0.0", "node-runtime 2.0.0", -<<<<<<< HEAD - "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", -======= "node-testing 2.0.0", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ->>>>>>> e59282b561b9fb775424f336ecab5d16d464a3f0 + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", "srml-balances 2.0.0", @@ -2383,7 +2379,7 @@ dependencies = [ "node-primitives 2.0.0", "node-runtime 2.0.0", "node-testing 2.0.0", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "substrate-client 2.0.0", @@ -2517,7 +2513,7 @@ dependencies = [ "node-executor 2.0.0", "node-primitives 2.0.0", "node-runtime 2.0.0", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", "srml-balances 2.0.0", @@ -3737,7 +3733,7 @@ dependencies = [ name = "sr-staking-primitives" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "sr-std 2.0.0", ] @@ -3799,7 +3795,7 @@ dependencies = [ name = "srml-authority-discovery" version = "0.1.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -4094,7 +4090,7 @@ dependencies = [ name = "srml-offences" version = "1.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -4110,7 +4106,7 @@ dependencies = [ name = "srml-scored-pool" version = "1.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -4408,7 +4404,7 @@ dependencies = [ name = "substrate-authority-discovery-primitives" version = "2.0.0" dependencies = [ - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "sr-std 2.0.0", "substrate-client 2.0.0", @@ -4653,19 +4649,6 @@ dependencies = [ ] [[package]] -<<<<<<< HEAD -name = "substrate-consensus-common-primitives" -version = "2.0.0" -dependencies = [ - "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "sr-primitives 2.0.0", - "sr-std 2.0.0", - "substrate-client 2.0.0", -] - -[[package]] -======= ->>>>>>> e59282b561b9fb775424f336ecab5d16d464a3f0 name = "substrate-consensus-rhd" version = "2.0.0" dependencies = [ @@ -5155,13 +5138,8 @@ name = "substrate-test-client" version = "2.0.0" dependencies = [ "futures-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", -<<<<<<< HEAD - "hash-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", -======= "hash-db 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ->>>>>>> e59282b561b9fb775424f336ecab5d16d464a3f0 + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", "substrate-client 2.0.0", "substrate-client-db 2.0.0", @@ -5178,13 +5156,8 @@ version = "2.0.0" dependencies = [ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", -<<<<<<< HEAD - "memory-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", -======= "memory-db 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ->>>>>>> e59282b561b9fb775424f336ecab5d16d464a3f0 + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", @@ -5263,15 +5236,9 @@ dependencies = [ "criterion 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "hash-db 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", "hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", -<<<<<<< HEAD - "keccak-hasher 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memory-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", -======= "keccak-hasher 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", "memory-db 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ->>>>>>> e59282b561b9fb775424f336ecab5d16d464a3f0 + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sr-std 2.0.0", "substrate-primitives 2.0.0", "trie-bench 0.16.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -5687,23 +5654,13 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "criterion 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", -<<<<<<< HEAD - "hash-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "keccak-hasher 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memory-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "trie-db 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "trie-root 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "trie-standardmap 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", -======= "hash-db 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", "keccak-hasher 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", "memory-db 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "trie-db 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", "trie-root 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", "trie-standardmap 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", ->>>>>>> e59282b561b9fb775424f336ecab5d16d464a3f0 ] [[package]] From 12bd3c3960df57e6636e6a8e9d649459ef1d21e4 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Thu, 22 Aug 2019 12:54:53 +0200 Subject: [PATCH 19/41] Fix build. --- srml/elections-phragmen/src/lib.rs | 1 + srml/elections/src/lib.rs | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 5e0b326cc4414..fb1b017c8315b 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -482,6 +482,7 @@ mod tests { type MaximumBlockWeight = MaximumBlockWeight; type MaximumBlockLength = MaximumBlockLength; type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); } parameter_types! { diff --git a/srml/elections/src/lib.rs b/srml/elections/src/lib.rs index ede96dde25845..467d202460c9c 100644 --- a/srml/elections/src/lib.rs +++ b/srml/elections/src/lib.rs @@ -138,7 +138,6 @@ pub type VoteIndex = u32; // all three must be in sync. type ApprovalFlag = u32; -const APPROVAL_FLAG_MASK: ApprovalFlag = 0x8000_0000; const APPROVAL_FLAG_LEN: usize = 32; pub trait Trait: system::Trait { @@ -1011,7 +1010,6 @@ impl Module { /// Return true of the bit `n` of scalar `x` is set to `1` and false otherwise. fn bit_at(x: ApprovalFlag, n: usize) -> bool { if n < APPROVAL_FLAG_LEN { - // x & ( APPROVAL_FLAG_MASK >> n ) != 0 x & ( 1 << n ) != 0 } else { false From aefea2718ef5f40c0593f76bf74f7483b8ba6057 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Thu, 22 Aug 2019 15:15:44 +0200 Subject: [PATCH 20/41] Remove runner-ups --- srml/elections-phragmen/src/lib.rs | 192 +++++------------------------ 1 file changed, 31 insertions(+), 161 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index fb1b017c8315b..f049196226d8c 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -45,12 +45,8 @@ //! //! Candidates also reserve a bond as they submit candidacy. A candidate cannot take their candidacy //! back. A candidate can end up in one of the below situations: -//! - **Winner**: A winner is kept as a _member_. They must still have a bond in reserve -//! and they are automatically counted as a candidate for the next election. -//! - **Runner-up**: Runner ups are the best candidates immediately after the winners. The number -//! of runner-ups to keep is configurable. Runner-ups serve one major role: if a member is -//! forcefully removed, the next best runner-up is chosen as a replacement. If not, a new -//! election is triggered. +//! - **Winner**: A winner is kept as a _member_. They must still have a bond in reserve and they +//! are automatically counted as a candidate for the next election. //! - **Loser**: Any of the candidate who are not a winner are left as losers. A loser might be an //! _outgoing member_, meaning that they are an active member who failed to keep their spot. In //! this case, the outgoing member will get their bond back. Otherwise, the bond is slashed from @@ -112,7 +108,7 @@ pub trait Trait: system::Trait { /// How much should be locked up in order to be able to submit votes. type VotingBond: Get>; - /// Handler for the unbalanced reduction when a candidate has lost (and is not a runner-up) + /// Handler for the unbalanced reduction when a candidate has lost type LoserCandidate: OnUnbalanced>; /// Handler for the unbalanced reduction when a member has been kicked. @@ -124,8 +120,6 @@ decl_storage! { // ---- parameters /// Number of members to elect. pub DesiredMembers get(desired_members) config(): u32; - /// Number of runner-ups to store. - pub DesiredRunnerUps get(desired_runner_ups) config(): u32; /// How long each seat is kept. This defined the next block number at which an election /// round will happen. pub TermDuration get(term_duration) config(): T::BlockNumber; @@ -133,8 +127,6 @@ decl_storage! { // ---- State /// The current elected membership. Sorted based on account id (low to high). pub Members get(members) config(): Vec; - /// The current runner-ups. Sorted based on **stake** (low to high). - pub RunnerUps get(runner_ups): Vec; /// The total number of vote rounds that have happened, exclusive of the upcoming one. pub ElectionRounds get(election_rounds): u32 = Zero::zero(); @@ -223,8 +215,6 @@ decl_module! { /// A candidate will either: /// - Lose at the end of the term and get slashed. /// - Win and become a member. Members will eventually get their stash back. - /// - Become a runner-up. Runner-ups are reserved members in case one gets forcefully - /// removed. #[weight = SimpleDispatchInfo::FixedNormal(500_000)] fn submit_candidacy(origin) { let who = ensure_signed(origin)?; @@ -254,8 +244,8 @@ decl_module! { /// Remove a particular member from the set. This is effective immediately. /// - /// If a runner-up si available, then the best runner-up will be removed and replaces the - /// outgoing member. Otherwise, a new phragmen round is started. + /// A new phragmen round is started immediately afterwards to determine the new members. + /// Current members (except for the removed one) are treated like active candidate. /// /// Note that this does not affect the designated block number of the next election. #[weight = SimpleDispatchInfo::FixedOperational(2_000_000)] @@ -271,24 +261,9 @@ decl_module! { T::KickedMember::on_unbalanced(imbalance); Self::deposit_event(RawEvent::MemberKicked(who.clone())); - let mut runner_ups = Self::runner_ups(); - if let Some(replacement) = runner_ups.pop() { - // replace the outgoing with the best runner up. - if let Err(index) = members.binary_search(&replacement) { - members.insert(index, replacement.clone()); - ElectionRounds::mutate(|v| *v += 1); - T::ChangeMembers::change_members_sorted(&[replacement], &[who], &members); - } - // else it would mean that the runner up was already a candidate. This cannot - // happen. If it does, not much that we can do about it. - - >::put(members); - >::put(runner_ups); - } else { - // trigger a new phragmen. grab a cup of coffee. This might take a while. - >::put(members); - Self::do_phragmen(); - } + // update `Members` storage -- `do_phragmen` adds this to the candidate list. + >::put(members); + Self::do_phragmen(); } } @@ -355,12 +330,14 @@ impl Module { /// Calls the appropriate `ChangeMembers` function variant internally. fn do_phragmen() { let desired_seats = Self::desired_members() as usize; - let desired_runner_ups = Self::desired_runner_ups() as usize; - let num_to_elect = desired_runner_ups + desired_seats; + let num_to_elect = desired_seats; // current members are always a candidate for the next round as well. // this is guaranteed to not create any duplicates. let mut candidates = Self::candidates(); + // candidates who exactly called `submit_candidacy`. Only these folks are at the risk of + // losing their bond. + let exposed_candidates = candidates.clone(); candidates.append(&mut Self::members()); let voters_and_votes = >::enumerate() .map(|(v, i)| (v, i.0)) @@ -376,34 +353,16 @@ impl Module { let mut to_release_bond: Vec = Vec::with_capacity(desired_seats); let old_members = >::take(); - if let Some((mut new_members_with_approval, _)) = maybe_new_members { - // sort based on stake to to split winner and runner ups. - new_members_with_approval.sort_by_key(|x| x.1.clone()); - + if let Some((new_members_with_approval, _)) = maybe_new_members { // filter out those who had literally no votes at all. - let new_members_filtered = new_members_with_approval + let mut new_members = new_members_with_approval .into_iter() - .filter_map(|(m, a)| if !a.is_zero() { Some(m) } else { None } ) - .collect::>(); - - let mut new_members = new_members_filtered.iter() - .rev() - .take(desired_seats) - .map(|m| m.clone()) + .filter_map(|(m, a)| if a.is_zero() { None } else { Some(m) } ) .collect::>(); new_members.sort(); >::put(new_members.clone()); - let mut allowed_runner_ups = new_members_filtered.len() - new_members.len(); - // defensive only. We should never have more leftover than desired runner ups. - allowed_runner_ups = allowed_runner_ups.min(desired_runner_ups); - let runner_ups = new_members_filtered.into_iter() - .take(allowed_runner_ups) - .collect::>(); - // should still be sorted by stake. Low to high. - >::put(runner_ups.clone()); - - // report. We don't know/compute the diff here, therefore we call `set_member`. + // report member changes. We compute diff because we need the outgoing list. let (incoming, outgoing) = T::ChangeMembers::compute_members_diff(&new_members, &old_members); T::ChangeMembers::change_members_sorted( &incoming, @@ -411,20 +370,13 @@ impl Module { &new_members ); + // unlike exposed_candidates, these are members who were in the list and no longer + // exist. They must get their bond back. to_release_bond = outgoing.to_vec(); - // NOTE: it is unfortunate that we have to re-sort new_members and runner ups - // above, but their length is relatively small (few dozens at most) while it helps the - // below search of `O(candidates)` quite faster. - - // Burn loser bond. All three lists are sorted. O(3LogN) ~ O(LogN) - candidates.into_iter().for_each(|c| { - // a candidate who's - if new_members.binary_search(&c).is_err() // now a member - && outgoing.binary_search(&c).is_err() // or was a member - && runner_ups.binary_search(&c).is_err() // or a runner-up - { - // should ge their bond slashed. + // Burn loser bond. members list is sorted. O(NLogM) (N candidates, M members) + exposed_candidates.into_iter().for_each(|c| { + if new_members.binary_search(&c).is_err() { // TODO make sure candidate cannot fuck themselves up here and get slashed by re-submitting. let (imbalance, _) = T::Currency::slash_reserved(&c, T::CandidacyBond::get()); T::LoserCandidate::on_unbalanced(imbalance); } @@ -568,7 +520,6 @@ mod tests { pub struct ExtBuilder { balance_factor: u64, voter_bond: u64, - desired_runner_ups: u32, } impl Default for ExtBuilder { @@ -576,7 +527,6 @@ mod tests { Self { balance_factor: 1, voter_bond: 2, - desired_runner_ups: 0, } } } @@ -586,10 +536,6 @@ mod tests { self.voter_bond = fee; self } - pub fn desired_runner_ups(mut self, count: u32) -> Self { - self.desired_runner_ups = count; - self - } pub fn build(self) -> runtime_io::TestExternalities { VOTING_BOND.with(|v| *v.borrow_mut() = self.voter_bond); GenesisConfig { @@ -607,7 +553,6 @@ mod tests { elections: Some(elections::GenesisConfig::{ members: vec![], desired_members: 2, - desired_runner_ups: self.desired_runner_ups, term_duration: 5, }), }.build_storage().unwrap().into() @@ -631,7 +576,6 @@ mod tests { assert_eq!(Elections::desired_members(), 2); assert_eq!(Elections::members(), vec![]); - assert_eq!(Elections::runner_ups(), vec![]); assert_eq!(Elections::candidates(), vec![]); assert_eq!(Elections::candidate_count(), 0); @@ -689,7 +633,6 @@ mod tests { assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![5]); - assert_eq!(Elections::runner_ups(), vec![]); assert_eq!(Elections::candidates(), vec![]); assert_noop!( @@ -884,7 +827,6 @@ mod tests { assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![5]); - assert_eq!(Elections::runner_ups(), vec![]); assert_eq_uvec!(all_voters(), vec![2, 3]); assert_eq!(Elections::candidates(), vec![]); assert_eq!(Elections::candidate_count(), 0); @@ -954,34 +896,12 @@ mod tests { assert_eq!(Elections::candidates(), vec![]); assert_eq!(Elections::election_rounds(), 1); assert_eq!(Elections::members(), vec![]); - assert_eq!(Elections::runner_ups(), vec![]); - }); - } - - #[test] - fn runner_ups_should_be_kept() { - with_externalities(&mut ExtBuilder::default().desired_runner_ups(1).build(), || { - assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::submit_candidacy(Origin::signed(4))); - assert_ok!(Elections::submit_candidacy(Origin::signed(3))); - - assert_ok!(Elections::vote(Origin::signed(2), vec![3], 20)); - assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); - assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); - assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); - - System::set_block_number(5); - assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::members(), vec![3, 5]); - assert_eq!(Elections::runner_ups(), vec![4]); - assert_eq!(Balances::reserved_balance(&4), 3 + 2); - assert_eq!(Balances::free_balance(&4), 35); }); } #[test] fn current_members_are_always_implicitly_next_candidate() { - with_externalities(&mut ExtBuilder::default().desired_runner_ups(1).build(), || { + with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); @@ -996,22 +916,25 @@ mod tests { assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![4, 5]); - assert_eq!(Elections::runner_ups(), vec![3]); assert_eq!(Elections::election_rounds(), 1); + // 2 will become a candidate again, with the same vote. + assert_ok!(Elections::remove_voter(Origin::signed(2))); + assert_ok!(Elections::submit_candidacy(Origin::signed(2))); + assert_ok!(Elections::vote(Origin::signed(2), vec![2], 18)); + // 4, 5 will persist as candidates despite not being in the list. - assert_eq!(Elections::candidates(), vec![]); + assert_eq!(Elections::candidates(), vec![2]); System::set_block_number(10); assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![4, 5]); - assert_eq!(Elections::runner_ups(), vec![]); }); } #[test] fn election_state_is_uninterrupted() { - with_externalities(&mut ExtBuilder::default().desired_runner_ups(1).build(), || { + with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); @@ -1036,7 +959,7 @@ mod tests { } #[test] - fn remove_members_triggers_election_if_no_runner_up() { + fn remove_members_triggers_election() { with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); @@ -1061,55 +984,6 @@ mod tests { }); } - #[test] - fn unexpected_election_will_use_runner_ups_if_exists() { - with_externalities(&mut ExtBuilder::default().desired_runner_ups(1).build(), || { - assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::submit_candidacy(Origin::signed(4))); - assert_ok!(Elections::submit_candidacy(Origin::signed(3))); - - assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); - assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); - assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); - - System::set_block_number(5); - assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::members(), vec![4, 5]); - assert_eq!(Elections::runner_ups(), vec![3]); - assert_eq!(Elections::election_rounds(), 1); - - assert_ok!(Elections::remove_member(Origin::ROOT, 4)); - - assert_eq!(balances(&4), (35, 2)); // slashed - assert_eq!(Elections::election_rounds(), 2); // new election round - assert_eq!(Elections::members(), vec![3, 5]); // new members - assert_eq!(Elections::runner_ups(), vec![]); - }); - } - - #[test] - fn runner_ups_are_sorted_by_stake() { - with_externalities(&mut ExtBuilder::default().desired_runner_ups(3).build(), || { - assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::submit_candidacy(Origin::signed(4))); - assert_ok!(Elections::submit_candidacy(Origin::signed(3))); - assert_ok!(Elections::submit_candidacy(Origin::signed(2))); - assert_ok!(Elections::submit_candidacy(Origin::signed(1))); - - assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); - assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); - assert_ok!(Elections::vote(Origin::signed(3), vec![3], 9)); - assert_ok!(Elections::vote(Origin::signed(2), vec![2], 8)); - assert_ok!(Elections::vote(Origin::signed(1), vec![1], 10)); - - System::set_block_number(5); - assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::members(), vec![4, 5]); - assert_eq!(Elections::runner_ups(), vec![2, 3, 1]); - assert_eq!(Elections::election_rounds(), 1); - }); - } - #[test] fn seats_should_be_released_when_no_vote() { with_externalities(&mut ExtBuilder::default().build(), || { @@ -1197,7 +1071,7 @@ mod tests { #[test] fn incoming_outgoing_are_reported() { - with_externalities(&mut ExtBuilder::default().desired_runner_ups(1).build(), || { + with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(5))); @@ -1225,10 +1099,6 @@ mod tests { assert_eq!(balances(&3), (25, 5)); assert_eq!(balances(&4), (35, 5)); - // 2 is the runner up, must still be bonded - assert_eq!(Elections::runner_ups(), vec![2]); - assert_eq!(balances(&2), (15, 5)); - // 1 is a loser, will get slashed. assert_eq!(Balances::reserved_balance(&1), 2); assert_eq!(balances(&1), (5, 2)); From 19af37b6cd1c3907888600fe02767f234f6d5867 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Fri, 23 Aug 2019 14:09:20 +0200 Subject: [PATCH 21/41] Some refactors. --- srml/elections-phragmen/src/lib.rs | 214 ++++++++++++++++++----------- 1 file changed, 134 insertions(+), 80 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index f049196226d8c..0612709bca9c8 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -131,11 +131,12 @@ decl_storage! { pub ElectionRounds get(election_rounds): u32 = Zero::zero(); /// Votes of a particular voter, with the round index of the votes. - pub VotesOf get(votes_of): linked_map T::AccountId => (Vec, u32); + pub VotesOf get(votes_of): linked_map T::AccountId => Vec; /// Locked stake of a voter. pub StakeOf get(stake_of): map T::AccountId => BalanceOf; - /// The present candidate list. Sorted based on account id. + /// The present candidate list. Sorted based on account id. A current member can never enter + /// this vector and is always implicitly assumed to be a candidate. pub Candidates get(candidates): Vec; /// Current number of active candidates. pub CandidateCount get(candidate_count): u32; @@ -164,13 +165,18 @@ decl_module! { // TODO: use decode_len #3071 and remove candidate_count. let candidates_count = Self::candidate_count(); + // addition is valid: candidates adn members never overlap. let allowed_votes = candidates_count as usize + Self::members().len(); - ensure!(!allowed_votes.is_zero(), "number of candidates cannot be zero"); + + ensure!(!allowed_votes.is_zero(), "cannot vote when no candidates or members exist"); ensure!(votes.len() <= allowed_votes, "cannot vote more than candidates"); ensure!(votes.len() <= MAXIMUM_VOTE, "cannot vote more than maximum allowed"); ensure!(!votes.is_empty(), "must vote for at least one candidate."); - ensure!(value > T::Currency::minimum_balance(), "cannot vote with stake less than min balance"); + ensure!( + value > T::Currency::minimum_balance(), + "cannot vote with stake less than minimum balance" + ); if !Self::is_voter(&who) { // first time voter. Reserve bond. @@ -190,8 +196,7 @@ decl_module! { ); >::insert(&who, locked_balance); - let now = Self::election_rounds(); - >::insert(&who, (votes, now)); + >::insert(&who, votes); } /// Remove `origin` as a voter. This removes the lock and returns the bond. @@ -229,7 +234,7 @@ decl_module! { let index = is_candidate.unwrap_err(); T::Currency::reserve(&who, T::CandidacyBond::get()) - .map_err(|_| "candidate has not enough funds")?; + .map_err(|_| "candidate does not have enough funds")?; >::mutate(|c| c.insert(index, who)); CandidateCount::mutate(|c| *c += 1); @@ -340,7 +345,7 @@ impl Module { let exposed_candidates = candidates.clone(); candidates.append(&mut Self::members()); let voters_and_votes = >::enumerate() - .map(|(v, i)| (v, i.0)) + .map(|(v, i)| (v, i)) .collect::)>>(); let maybe_new_members = phragmen::elect::<_, _, _, T::CurrencyToVote>( num_to_elect, @@ -376,7 +381,7 @@ impl Module { // Burn loser bond. members list is sorted. O(NLogM) (N candidates, M members) exposed_candidates.into_iter().for_each(|c| { - if new_members.binary_search(&c).is_err() { // TODO make sure candidate cannot fuck themselves up here and get slashed by re-submitting. + if new_members.binary_search(&c).is_err() { let (imbalance, _) = T::Currency::slash_reserved(&c, T::CandidacyBond::get()); T::LoserCandidate::on_unbalanced(imbalance); } @@ -567,13 +572,19 @@ mod tests { (Balances::free_balance(who), Balances::reserved_balance(who)) } + fn has_lock(who: &u64) -> u64 { + let lock = Balances::locks(who)[0].clone(); + assert_eq!(lock.id, MODULE_ID); + lock.amount + } + #[test] fn params_should_work() { with_externalities(&mut ExtBuilder::default().build(), || { System::set_block_number(1); - assert_eq!(Elections::election_rounds(), 0); - assert_eq!(Elections::term_duration(), 5); assert_eq!(Elections::desired_members(), 2); + assert_eq!(Elections::term_duration(), 5); + assert_eq!(Elections::election_rounds(), 0); assert_eq!(Elections::members(), vec![]); @@ -582,7 +593,7 @@ mod tests { assert!(Elections::is_candidate(&1).is_err()); assert_eq!(all_voters(), vec![]); - assert_eq!(Elections::votes_of(&1).0, vec![]); + assert_eq!(Elections::votes_of(&1), vec![]); }); } @@ -593,18 +604,21 @@ mod tests { assert!(Elections::is_candidate(&1).is_err()); assert!(Elections::is_candidate(&2).is_err()); - assert_eq!(Balances::free_balance(&1), 10); + assert_eq!(balances(&1), (10, 0)); assert_ok!(Elections::submit_candidacy(Origin::signed(1))); - assert_eq!(Balances::free_balance(&1), 10 - 3); - assert_eq!(Balances::reserved_balance(&1), 3); + assert_eq!(balances(&1), (7, 3)); + assert_eq!(Elections::candidates(), vec![1]); + assert!(Elections::is_candidate(&1).is_ok()); assert!(Elections::is_candidate(&2).is_err()); - assert_eq!(Balances::free_balance(&2), 20); + assert_eq!(balances(&2), (20, 0)); assert_ok!(Elections::submit_candidacy(Origin::signed(2))); - assert_eq!(Balances::free_balance(&2), 20 - 3); + assert_eq!(balances(&2), (17, 3)); + assert_eq!(Elections::candidates(), vec![1, 2]); + assert!(Elections::is_candidate(&1).is_ok()); assert!(Elections::is_candidate(&2).is_ok()); }); @@ -625,6 +639,7 @@ mod tests { #[test] fn member_candidacy_submission_should_not_work() { + // critically important to make sure that outgoing candidates and losers are not mixed up. with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::vote(Origin::signed(2), vec![5], 20)); @@ -648,45 +663,56 @@ mod tests { assert_eq!(Elections::candidates(), Vec::::new()); assert_noop!( Elections::submit_candidacy(Origin::signed(7)), - "candidate has not enough funds" + "candidate does not have enough funds" ); }); } #[test] - fn vote_locks_balance() { + fn simple_voting_should_work() { with_externalities(&mut ExtBuilder::default().build(), || { assert_eq!(Elections::candidates(), Vec::::new()); - assert_eq!(Balances::free_balance(&2), 20); + assert_eq!(balances(&2), (20, 0)); assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::vote(Origin::signed(2), vec![5], 20)); - assert_eq!(Balances::free_balance(&2), 20 - 2); - assert_noop!( - Balances::reserve(&2, 1), - "account liquidity restrictions prevent withdrawal" - ); // locked. + assert_eq!(balances(&2), (18, 2)); + assert_eq!(has_lock(&2), 20); }); } #[test] - fn can_update_votes() { + fn can_vote_with_custom_stake() { with_externalities(&mut ExtBuilder::default().build(), || { - assert_eq!(Balances::free_balance(&2), 20); - assert_eq!(Balances::reserved_balance(&2), 0); + assert_eq!(Elections::candidates(), Vec::::new()); + assert_eq!(balances(&2), (20, 0)); + + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::vote(Origin::signed(2), vec![5], 12)); + + assert_eq!(balances(&2), (18, 2)); + assert_eq!(has_lock(&2), 12); + }); + } + + #[test] + fn can_update_votes_and_stake() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(balances(&2), (20, 0)); assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::vote(Origin::signed(2), vec![5], 20)); - assert_eq!(Balances::free_balance(&2), 20 - 2); - assert_eq!(Balances::reserved_balance(&2), 2); + assert_eq!(balances(&2), (18, 2)); + assert_eq!(has_lock(&2), 20); assert_eq!(Elections::stake_of(2), 20); // can update; different stake; different lock and reserve. assert_ok!(Elections::vote(Origin::signed(2), vec![5, 4], 15)); - assert_eq!(Balances::reserved_balance(&2), 2); + assert_eq!(balances(&2), (18, 2)); + assert_eq!(has_lock(&2), 15); assert_eq!(Elections::stake_of(2), 15); }); } @@ -696,11 +722,30 @@ mod tests { with_externalities(&mut ExtBuilder::default().build(), || { assert_noop!( Elections::vote(Origin::signed(2), vec![], 20), - "number of candidates cannot be zero" + "cannot vote when no candidates or members exist" ); }); } + #[test] + fn can_vote_for_old_members_even_when_no_new_candidates() { + // let allowed_votes = candidates_count as usize + Self::members().len() + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + + assert_ok!(Elections::vote(Origin::signed(2), vec![4, 5], 20)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members(), vec![4, 5]); + assert_eq!(Elections::candidates(), vec![]); + + assert_ok!(Elections::vote(Origin::signed(3), vec![4, 5], 10)); + }); + } + #[test] fn cannot_vote_for_more_than_candidates() { with_externalities(&mut ExtBuilder::default().build(), || { @@ -722,7 +767,7 @@ mod tests { assert_noop!( Elections::vote(Origin::signed(2), vec![4], 1), - "cannot vote with stake less than min balance" + "cannot vote with stake less than minimum balance" ); }) } @@ -736,6 +781,7 @@ mod tests { assert_ok!(Elections::vote(Origin::signed(2), vec![4, 5], 30)); // you can lie but won't get away with it. assert_eq!(Elections::stake_of(2), 20); + assert_eq!(has_lock(&2), 20); }); } @@ -750,18 +796,17 @@ mod tests { assert_eq_uvec!(all_voters(), vec![2, 3]); assert_eq!(Elections::stake_of(2), 20); assert_eq!(Elections::stake_of(3), 30); - assert_eq!(Elections::votes_of(2), (vec![5], 0)); - assert_eq!(Elections::votes_of(3), (vec![5], 0)); + assert_eq!(Elections::votes_of(2), vec![5]); + assert_eq!(Elections::votes_of(3), vec![5]); assert_ok!(Elections::remove_voter(Origin::signed(2))); - assert_eq!(all_voters(), vec![3]); - assert_eq!(Elections::votes_of(2), (vec![], 0)); + assert_eq_uvec!(all_voters(), vec![3]); + assert_eq!(Elections::votes_of(2), vec![]); assert_eq!(Elections::stake_of(2), 0); - assert_eq!(Balances::free_balance(&2), 20); - assert_eq!(Balances::reserved_balance(&2), 0); - assert_ok!(Balances::reserve(&2, 10)); // no locks + assert_eq!(balances(&2), (20, 0)); + assert_eq!(Balances::locks(&2).len(), 0); }); } @@ -809,27 +854,32 @@ mod tests { fn simple_voting_rounds_should_work() { with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); assert_ok!(Elections::vote(Origin::signed(2), vec![5], 20)); - assert_ok!(Elections::vote(Origin::signed(3), vec![5], 30)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 15)); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); - assert_eq_uvec!(all_voters(), vec![2, 3]); + assert_eq_uvec!(all_voters(), vec![2, 3, 4]); - assert_eq!(Elections::votes_of(2).0, vec![5]); - assert_eq!(Elections::votes_of(3).0, vec![5]); + assert_eq!(Elections::votes_of(2), vec![5]); + assert_eq!(Elections::votes_of(3), vec![3]); + assert_eq!(Elections::votes_of(4), vec![4]); - assert_eq!(Elections::candidates(), vec![5]); - assert_eq!(Elections::candidate_count(), 1); + assert_eq!(Elections::candidates(), vec![3, 4, 5]); + assert_eq!(Elections::candidate_count(), 3); assert_eq!(Elections::election_rounds(), 0); System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::members(), vec![5]); - assert_eq_uvec!(all_voters(), vec![2, 3]); + assert_eq!(Elections::members(), vec![3, 5]); + assert_eq_uvec!(all_voters(), vec![2, 3, 4]); assert_eq!(Elections::candidates(), vec![]); assert_eq!(Elections::candidate_count(), 0); + assert_eq!(Elections::election_rounds(), 1); }); } @@ -837,10 +887,9 @@ mod tests { #[test] fn old_voter_will_be_counted() { with_externalities(&mut ExtBuilder::default().build(), || { - // TODO: add to mopdule doc the tip that: one can always submit a candidacy and vote but not the other way around. assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - // This guys vote is pointless for this round. + // This guy's vote is pointless for this round. assert_ok!(Elections::vote(Origin::signed(3), vec![4], 30)); assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); @@ -853,10 +902,10 @@ mod tests { // but now it has a valid target. assert_ok!(Elections::submit_candidacy(Origin::signed(4))); - // meanwhile, no one cares to become a candidate again. System::set_block_number(10); assert_ok!(Elections::end_block(System::block_number())); - // none of the votes matter anymore + + // candidate 4 is affected by an old vote. assert_eq!(Elections::members(), vec![4, 5]); assert_eq!(Elections::election_rounds(), 2); assert_eq_uvec!(all_voters(), vec![3, 5]); @@ -904,11 +953,7 @@ mod tests { with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); - assert_ok!(Elections::submit_candidacy(Origin::signed(3))); - assert_ok!(Elections::submit_candidacy(Origin::signed(2))); - assert_ok!(Elections::vote(Origin::signed(2), vec![2], 20)); - assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); @@ -918,22 +963,29 @@ mod tests { assert_eq!(Elections::members(), vec![4, 5]); assert_eq!(Elections::election_rounds(), 1); - // 2 will become a candidate again, with the same vote. - assert_ok!(Elections::remove_voter(Origin::signed(2))); assert_ok!(Elections::submit_candidacy(Origin::signed(2))); - assert_ok!(Elections::vote(Origin::signed(2), vec![2], 18)); + assert_ok!(Elections::vote(Origin::signed(2), vec![2], 20)); + + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + + assert_ok!(Elections::remove_voter(Origin::signed(4))); + + // 5 will persist as candidates despite not being in the list. + assert_eq!(Elections::candidates(), vec![2, 3]); - // 4, 5 will persist as candidates despite not being in the list. - assert_eq!(Elections::candidates(), vec![2]); System::set_block_number(10); assert_ok!(Elections::end_block(System::block_number())); - assert_eq!(Elections::members(), vec![4, 5]); + // 4 removed; 5 and 3 are the new best. + assert_eq!(Elections::members(), vec![3, 5]); }); } #[test] fn election_state_is_uninterrupted() { + // what I mean by uninterrupted: + // given no input or stimulants the same members are re-elected. with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); @@ -972,7 +1024,7 @@ mod tests { assert_eq!(Elections::members(), vec![4, 5]); assert_eq!(Elections::election_rounds(), 1); - // a new candidate is also there waiting. + // a new candidate assert_ok!(Elections::submit_candidacy(Origin::signed(3))); assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); @@ -1021,27 +1073,26 @@ mod tests { #[test] fn outgoing_will_get_the_bond_back() { with_externalities(&mut ExtBuilder::default().build(), || { - assert_eq!(Balances::total_balance(&5), 50); + assert_eq!(balances(&5), (50, 0)); assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_eq!(Balances::reserved_balance(&5), 3); + assert_eq!(balances(&5), (47, 3)); assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); - assert_eq!(Balances::reserved_balance(&5), 3 + 2); + assert_eq!(balances(&5), (45, 5)); System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![5]); assert_ok!(Elections::remove_voter(Origin::signed(5))); - assert_eq!(Balances::reserved_balance(&5), 3); - // meanwhile, no one cares to become a candidate again. + assert_eq!(balances(&5), (47, 3)); + System::set_block_number(10); assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![]); - assert_eq!(Balances::reserved_balance(&5), 0); - assert_eq!(Balances::total_balance(&5), 50); + assert_eq!(balances(&5), (50, 0)); }); } @@ -1049,23 +1100,22 @@ mod tests { fn losers_will_lose_the_bond() { with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); - assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); assert_ok!(Elections::vote(Origin::signed(4), vec![5], 40)); - assert_eq!(Balances::reserved_balance(&5), 3); - assert_eq!(Balances::reserved_balance(&3), 3); + assert_eq!(balances(&5), (47, 3)); + assert_eq!(balances(&3), (27, 3)); System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![5]); - assert_eq!(Balances::reserved_balance(&5), 3); // winner - - assert_eq!(Balances::reserved_balance(&3), 0); // loser - assert_eq!(Balances::free_balance(&3), 27); // loser + // winner + assert_eq!(balances(&5), (47, 3)); + // loser + assert_eq!(balances(&3), (27, 0)); }); } @@ -1080,27 +1130,31 @@ mod tests { System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![4, 5]); assert_ok!(Elections::submit_candidacy(Origin::signed(1))); assert_ok!(Elections::submit_candidacy(Origin::signed(2))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + // 5 will change their vote and becomes an `outgoing` assert_ok!(Elections::vote(Origin::signed(5), vec![4], 8)); + // 4 will stay in the set assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + // 3 will become a winner assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + // these two are losers. assert_ok!(Elections::vote(Origin::signed(2), vec![2], 20)); assert_ok!(Elections::vote(Origin::signed(1), vec![1], 10)); System::set_block_number(10); assert_ok!(Elections::end_block(System::block_number())); - // 3, 4 are new members, must still be bonded + // 3, 4 are new members, must still be bonded, nothing slashed. assert_eq!(Elections::members(), vec![3, 4]); assert_eq!(balances(&3), (25, 5)); assert_eq!(balances(&4), (35, 5)); - // 1 is a loser, will get slashed. - assert_eq!(Balances::reserved_balance(&1), 2); + // 1 is a loser, slashed by 3. assert_eq!(balances(&1), (5, 2)); // 5 is an outgoing loser, it will get their bond back. From 840737c824f3b099966313be748804d493e7c7fc Mon Sep 17 00:00:00 2001 From: kianenigma Date: Fri, 30 Aug 2019 13:09:35 +0200 Subject: [PATCH 22/41] Add support for reporting voters. --- srml/elections-phragmen/src/lib.rs | 285 +++++++++++++++++++++++++---- srml/support/src/lib.rs | 6 +- 2 files changed, 256 insertions(+), 35 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index ae433e14d8d25..1f07bfe6a8b96 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -27,19 +27,26 @@ //! //! `TermDuration` might change during a round. This can shorten or extend the length of the round. //! The next election round's block number is never stored but rather always checked on the fly. -//! Based on the current block height and `TermDuration`, `BlockNumber % TermDuration == 0` will -//! always trigger a new election round. +//! Based on the current block number and `TermDuration`, the condition `BlockNumber % TermDuration +//! == 0` being satisfied will always trigger a new election round. //! //! ### Voting //! //! Voters can vote for any set of the candidates by providing a list of account ids. Invalid votes -//! (voting for non-candidates) are ignored during election. Yet, a voter _might_ vote for a -//! future candidate. Voters reserve a bond as they vote. Each vote defines a `value`. This amount -//! is locked from the account of the voter and indicates the weight of the vote. Voters can update +//! (voting for non-candidates) are ignored during election. Yet, a voter _might_ vote for a future +//! candidate. Voters reserve a bond as they vote. Each vote defines a `value`. This amount is +//! locked from the account of the voter and indicates the weight of the vote. Voters can update //! their votes at any time by calling `vote()` again. This keeps the bond untouched but can -//! optionally change the locked `value`. After a round, votes are kept and might still be valid -//! for further rounds. A voter is responsible for calling `remove_voter` once they are done to -//! have their bond back and remove the lock. +//! optionally change the locked `value`. After a round, votes are kept and might still be valid for +//! further rounds. A voter is responsible for calling `remove_voter` once they are done to have +//! their bond back and remove the lock. +//! +//! Voters also report other voters as being defunct to earn their bond. A voter is defunct once all +//! of the candidates that they have voted for are neither a valid candidate anymore nor a member. +//! Upon reporting, if the target voter is actually defunct, the reporter will be rewarded by the +//! voting bond of the target. The target will lose their bond and get removed. If the target is not +//! defunct, the reporter is slashed and removed. To prevent being reported, voters should manually +//! submit a `remove_voter()` as soon as they are in the defunct state. //! //! ### Candidacy and Members //! @@ -48,15 +55,15 @@ //! - **Winner**: A winner is kept as a _member_. They must still have a bond in reserve and they //! are automatically counted as a candidate for the next election. //! - **Loser**: Any of the candidate who are not a winner are left as losers. A loser might be an -//! _outgoing member_, meaning that they are an active member who failed to keep their spot. In -//! this case, the outgoing member will get their bond back. Otherwise, the bond is slashed from -//! the loser candidate. +//! _outgoing member_, meaning that they are an active member who failed to keep their spot. In +//! this case, the outgoing member will get their bond back. Otherwise, the bond is slashed from +//! the loser candidate. //! -//! Note that with the members being the default candidates for the next round and votes -//! persisting in storage, the election system is entirely stable given no further input. This means -//! that if the system has a particular set of candidates `C` and voters `V` that lead to a -//! set of members `M` being elected, as long as `V` and `C` don't remove their candidacy and votes, -//! `M` will keep being re-elected at the end of each round. +//! Note that with the members being the default candidates for the next round and votes persisting +//! in storage, the election system is entirely stable given no further input. This means that if +//! the system has a particular set of candidates `C` and voters `V` that lead to a set of members +//! `M` being elected, as long as `V` and `C` don't remove their candidacy and votes, `M` will keep +//! being re-elected at the end of each round. //! //! ### Module Information //! @@ -64,13 +71,21 @@ //! - [`Call`](./enum.Call.html) //! - [`Module`](./struct.Module.html) +// TODO: we don't hold the voters accountable in any sense; a voter can remove themselves (unbond) +// right after an election (even if their voted for some winners) and they can be reported right +// after an election. We should probably add some accountability here. Simplest would be that voters +// cannot remove/report as long as they have a voted target who is in the member list. + +// TODO: update the weights with O values. + #![cfg_attr(not(feature = "std"), no_std)] use sr_primitives::traits::{Zero, StaticLookup, Bounded, Convert}; use sr_primitives::weights::SimpleDispatchInfo; use srml_support::{StorageValue, StorageMap, EnumerableStorageMap, decl_storage, decl_event, ensure, decl_module, dispatch, - traits::{Currency, Get, LockableCurrency, LockIdentifier, ReservableCurrency, WithdrawReasons, + traits::{ + Currency, Get, LockableCurrency, LockIdentifier, ReservableCurrency, WithdrawReasons, ChangeMembers, OnUnbalanced, } }; @@ -107,9 +122,12 @@ pub trait Trait: system::Trait { /// How much should be locked up in order to be able to submit votes. type VotingBond: Get>; - /// Handler for the unbalanced reduction when a candidate has lost + /// Handler for the unbalanced reduction when a candidate has lost. type LoserCandidate: OnUnbalanced>; + /// Handler for the unbalanced reduction when a reporter has submitted a bad defunct report. + type BadReport: OnUnbalanced>; + /// Handler for the unbalanced reduction when a member has been kicked. type KickedMember: OnUnbalanced>; } @@ -124,7 +142,7 @@ decl_storage! { pub TermDuration get(term_duration) config(): T::BlockNumber; // ---- State - /// The current elected membership. Sorted based on account id (low to high). + /// The current elected membership. Sorted based on account id. pub Members get(members) config(): Vec; /// The total number of vote rounds that have happened, exclusive of the upcoming one. pub ElectionRounds get(election_rounds): u32 = Zero::zero(); @@ -158,6 +176,10 @@ decl_module! { /// Upon voting, `value` units of `who`'s balance is locked and a bond amount is reserved. /// It is the responsibility of the caller to not place all of their balance into the lock /// and keep some for further transactions. + /// + /// # + /// O(1) + /// # #[weight = SimpleDispatchInfo::FixedNormal(500_000)] fn vote(origin, votes: Vec, value: BalanceOf) { let who = ensure_signed(origin)?; @@ -194,7 +216,6 @@ decl_module! { WithdrawReasons::all(), ); >::insert(&who, locked_balance); - >::insert(&who, votes); } @@ -205,30 +226,67 @@ decl_module! { ensure!(Self::is_voter(&who), "must be a voter"); - // remove storage. - >::remove(&who); - >::remove(&who); + Self::do_remove_voter(&who, true); + } - // unreserve the bond and remove. - T::Currency::unreserve(&who, T::VotingBond::get()); - T::Currency::remove_lock(MODULE_ID, &who); + /// Report `target` for being an defunct voter. In case of a valid report, the reporter is + /// rewarded by the bond amount of `target`. Otherwise, the reporter itself is removed and + /// their bond is slashed. + /// + /// A defunct voter is defined to be: + /// - a voter who's current submitted votes are all invalid. i.e. all of them are no + /// longer a candidate nor an active member. + /// + /// # + /// O(NLogM) given M current candidates and N votes for `target`. + /// # + #[weight = SimpleDispatchInfo::FixedNormal(500_000)] + fn report_defunct_voter(origin, target: ::Source) { + let reporter = ensure_signed(origin)?; + let target = T::Lookup::lookup(target)?; + + ensure!(reporter != target, "cannot report self"); + ensure!(Self::is_voter(&reporter), "reporter must be a voter"); + + // Checking if someone is a candidate and a member here is O(LogN), making the whole + // function O(MLonN) with N candidates in total and M of them being voted by `target`. + // We could easily add another mapping to be able to check if someone is a candidate in + // `O(1)` but that would make the process of removing candidates at the end of each + // round slightly harder. + // + // Note that for now we have a bound of number of votes (`N`). + let valid = Self::is_defunct_voter(&target); + if valid { + // reporter will get the voting bond of the target + T::Currency::repatriate_reserved(&target, &reporter, T::VotingBond::get())?; + // remove the target. They are defunct. + Self::do_remove_voter(&target, false); + } else { + // slash the bond of the reporter. + let imbalance = T::Currency::slash_reserved(&reporter, T::VotingBond::get()).0; + T::BadReport::on_unbalanced(imbalance); + // remove the reporter. + Self::do_remove_voter(&reporter, false); + } + Self::deposit_event(RawEvent::VoterReported(target, reporter, valid)); } + /// Submit oneself for candidacy. /// /// A candidate will either: /// - Lose at the end of the term and get slashed. /// - Win and become a member. Members will eventually get their stash back. + /// # + /// O(LogN) Given N candidates. + /// # #[weight = SimpleDispatchInfo::FixedNormal(500_000)] fn submit_candidacy(origin) { let who = ensure_signed(origin)?; let is_candidate = Self::is_candidate(&who); ensure!(!is_candidate.is_ok(), "duplicate candidate submission"); - ensure!( - Self::members().binary_search(&who).is_err(), - "member cannot re-submit candidacy" - ); + ensure!(Self::is_member(&who), "member cannot re-submit candidacy"); // assured to be an error, error always contains the index. let index = is_candidate.unwrap_err(); @@ -240,6 +298,9 @@ decl_module! { } /// Set the desired member count. Changes will be effective at the beginning of next round. + /// # + /// O(1) + /// # #[weight = SimpleDispatchInfo::FixedOperational(10_000)] fn set_desired_member_count(origin, #[compact] count: u32) { ensure_root(origin)?; @@ -252,6 +313,9 @@ decl_module! { /// Current members (except for the removed one) are treated like active candidate. /// /// Note that this does not affect the designated block number of the next election. + /// # + /// O(LogN) given N candidates + O(phragmen) + /// # #[weight = SimpleDispatchInfo::FixedOperational(2_000_000)] fn remove_member(origin, who: ::Source) { ensure_root(origin)?; @@ -298,21 +362,65 @@ decl_event!( /// A member has been removed. This should always be followed by either `NewTerm` ot /// `EmptyTerm`. MemberKicked(AccountId), + /// A voter (first element) was reported (byt the second element) with the the report being + /// successful or not (third element). + VoterReported(AccountId, AccountId, bool), } ); impl Module { /// Check if `who` is a candidate. It returns the insert index if the element does not exists as /// an error. + /// + /// O(LogN) given N candidates. fn is_candidate(who: &T::AccountId) -> Result<(), usize> { Self::candidates().binary_search(who).map(|_| ()) } /// Check if `who` is a voter. It may or may not be a _current_ one. + /// + /// O(1). fn is_voter(who: &T::AccountId) -> bool { >::exists(who) } + /// Check if `who` is currently an active member. + /// + /// Limited number of members. Binary search. Constant time factor. O(1) + fn is_member(who: &T::AccountId) -> bool { + Self::members().binary_search(who).is_ok() + } + + /// Check if `who` is a defunct voter. + /// + /// Note that false is returned if `who` is not a voter at all. + /// + /// O(NLogM) with M candidates and `who` having voted for `N` of them. + fn is_defunct_voter(who: &T::AccountId) -> bool { + if Self::is_voter(who) { + Self::votes_of(who) + .iter() + .all(|v| !Self::is_member(v) && !Self::is_candidate(v).is_ok()) + } else { + false + } + } + + /// Remove a certain someone as a voter. + /// + /// This will clean always clean the storage associated with the voter, and remove the balance + /// lock. Optionally, it would also return the reserved voting bond if indicated by `unreserve`. + fn do_remove_voter(who: &T::AccountId, unreserve: bool) { + // remove storage and lock. + >::remove(who); + >::remove(who); + T::Currency::remove_lock(MODULE_ID, who); + + if unreserve { + T::Currency::unreserve(who, T::VotingBond::get()); + } + } + /// The locked stake of a voter. fn locked_stake_of(who: &T::AccountId) -> BalanceOf { Self::stake_of(who) @@ -368,7 +476,10 @@ impl Module { >::put(new_members.clone()); // report member changes. We compute diff because we need the outgoing list. - let (incoming, outgoing) = T::ChangeMembers::compute_members_diff(&new_members, &old_members); + let (incoming, outgoing) = T::ChangeMembers::compute_members_diff( + &new_members, + &old_members + ); T::ChangeMembers::change_members_sorted( &incoming, &outgoing.clone(), @@ -505,6 +616,7 @@ mod tests { type VotingBond = VotingBond; type LoserCandidate = (); type KickedMember = (); + type BadReport = (); } pub type Block = sr_primitives::generic::Block; @@ -850,6 +962,115 @@ mod tests { }); } + #[test] + fn reporter_must_be_voter() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_noop!( + Elections::report_defunct_voter(Origin::signed(1), 2), + "reporter must be a voter", + ); + }); + } + + #[test] + fn can_detect_defunct_voter() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(2), vec![4, 5], 20)); + // will be soon a defunct voter. + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members(), vec![4, 5]); + assert_eq!(Elections::candidates(), vec![]); + + // all of them have a member that they voted for. + assert_eq!(Elections::is_defunct_voter(&5), false); + assert_eq!(Elections::is_defunct_voter(&4), false); + assert_eq!(Elections::is_defunct_voter(&2), false); + + // defunct + assert_eq!(Elections::is_defunct_voter(&3), true); + + assert_ok!(Elections::submit_candidacy(Origin::signed(1))); + assert_ok!(Elections::vote(Origin::signed(1), vec![1], 10)); + + // has a candidate voted for. + assert_eq!(Elections::is_defunct_voter(&1), false); + + }); + } + + #[test] + fn report_voter_should_work_and_earn_reward() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(2), vec![4, 5], 20)); + // will be soon a defunct voter. + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members(), vec![4, 5]); + assert_eq!(Elections::candidates(), vec![]); + + assert_eq!(balances(&3), (28, 2)); + assert_eq!(balances(&5), (45, 5)); + + assert_ok!(Elections::report_defunct_voter(Origin::signed(5), 3)); + assert_eq!( + System::events()[1].event, + Event::elections(RawEvent::VoterReported(3, 5, true)) + ); + + assert_eq!(balances(&3), (28, 0)); + assert_eq!(balances(&5), (47, 5)); + }); + } + + #[test] + fn report_voter_should_slash_when_bad_report() { + with_externalities(&mut ExtBuilder::default().build(), || { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert_eq!(Elections::members(), vec![4, 5]); + assert_eq!(Elections::candidates(), vec![]); + + assert_eq!(balances(&4), (35, 5)); + assert_eq!(balances(&5), (45, 5)); + + assert_ok!(Elections::report_defunct_voter(Origin::signed(5), 4)); + assert_eq!( + System::events()[1].event, + Event::elections(RawEvent::VoterReported(4, 5, false)) + ); + + assert_eq!(balances(&4), (35, 5)); + assert_eq!(balances(&5), (45, 3)); + }); + }); + } + + #[test] fn simple_voting_rounds_should_work() { with_externalities(&mut ExtBuilder::default().build(), || { @@ -885,7 +1106,7 @@ mod tests { } #[test] - fn old_voter_will_be_counted() { + fn defunct_voter_will_be_counted() { with_externalities(&mut ExtBuilder::default().build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); diff --git a/srml/support/src/lib.rs b/srml/support/src/lib.rs index 9ebd848fca4c4..58b44eb805983 100644 --- a/srml/support/src/lib.rs +++ b/srml/support/src/lib.rs @@ -131,7 +131,7 @@ macro_rules! fail { /// Used as `ensure!(expression_to_ensure, expression_to_return_on_false)`. #[macro_export] macro_rules! ensure { - ( $x:expr, $y:expr ) => {{ + ( $x:expr, $y:expr $(,)?) => {{ if !$x { $crate::fail!($y); } @@ -145,7 +145,7 @@ macro_rules! ensure { #[macro_export] #[cfg(feature = "std")] macro_rules! assert_noop { - ( $x:expr , $y:expr ) => { + ( $x:expr , $y:expr $(,)?) => { let h = $crate::storage_root(); $crate::assert_err!($x, $y); assert_eq!(h, $crate::storage_root()); @@ -162,7 +162,7 @@ macro_rules! assert_noop { #[macro_export] #[cfg(feature = "std")] macro_rules! assert_err { - ( $x:expr , $y:expr ) => { + ( $x:expr , $y:expr $(,)?) => { assert_eq!($x, Err($y)); } } From e9c8e349f008f58dabd51b9c7d3b3dbe01945b08 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Fri, 30 Aug 2019 14:42:55 +0200 Subject: [PATCH 23/41] Fix member check. --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 1f07bfe6a8b96..82774f2d3275b 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -286,7 +286,7 @@ decl_module! { let is_candidate = Self::is_candidate(&who); ensure!(!is_candidate.is_ok(), "duplicate candidate submission"); - ensure!(Self::is_member(&who), "member cannot re-submit candidacy"); + ensure!(!Self::is_member(&who), "member cannot re-submit candidacy"); // assured to be an error, error always contains the index. let index = is_candidate.unwrap_err(); From 308feafa421f6304bf6b1f6e480a77cfec92c9a8 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Fri, 30 Aug 2019 16:17:17 +0200 Subject: [PATCH 24/41] Remove equlize.rs --- srml/staking/src/equalize.rs | 148 ----------------------------------- 1 file changed, 148 deletions(-) delete mode 100644 srml/staking/src/equalize.rs diff --git a/srml/staking/src/equalize.rs b/srml/staking/src/equalize.rs deleted file mode 100644 index 2e40a00e070eb..0000000000000 --- a/srml/staking/src/equalize.rs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2019 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 . - -//! Phragmen post-processing fot staking module. - -// TODO #3365: deprecate this file, decouple from staking and move to sr_primitives. -#![cfg(feature = "equalize")] - -use crate::{ExpoMap, Trait, BalanceOf, IndividualExposure}; - -use sr_primitives::traits::{Zero, Convert, Saturating}; -use sr_primitives::phragmen::{ExtendedBalance}; - -/// Performs equalize post-processing to the output of the election algorithm -/// This function mutates the input parameters, most noticeably it updates the exposure of -/// the elected candidates. -/// -/// No value is returned from the function and the `expo_map` parameter is updated. -pub fn equalize( - assignments: &mut Vec<(T::AccountId, BalanceOf, Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)>)>, - expo_map: &mut ExpoMap, - tolerance: ExtendedBalance, - iterations: usize, -) { - for _i in 0..iterations { - let mut max_diff = 0; - assignments.iter_mut().for_each(|(n, budget, assignment)| { - let diff = do_equalize::(&n, *budget, assignment, expo_map, tolerance); - if diff > max_diff { - max_diff = diff; - } - }); - if max_diff < tolerance { - break; - } - } -} - -fn do_equalize( - nominator: &T::AccountId, - budget_balance: BalanceOf, - elected_edges: &mut Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)>, - expo_map: &mut ExpoMap, - tolerance: ExtendedBalance -) -> ExtendedBalance { - let to_votes = |b: BalanceOf| - , u64>>::convert(b) as ExtendedBalance; - let to_balance = |v: ExtendedBalance| - >>::convert(v); - let budget = to_votes(budget_balance); - - // Nothing to do. This nominator had nothing useful. - // Defensive only. Assignment list should always be populated. - if elected_edges.is_empty() { return 0; } - - let stake_used = elected_edges - .iter() - .fold(0 as ExtendedBalance, |s, e| s.saturating_add(e.2)); - - let backed_stakes_iter = elected_edges - .iter() - .filter_map(|e| expo_map.get(&e.0)) - .map(|e| to_votes(e.total)); - - let backing_backed_stake = elected_edges - .iter() - .filter(|e| e.2 > 0) - .filter_map(|e| expo_map.get(&e.0)) - .map(|e| to_votes(e.total)) - .collect::>(); - - let mut difference; - if backing_backed_stake.len() > 0 { - let max_stake = backing_backed_stake - .iter() - .max() - .expect("vector with positive length will have a max; qed"); - let min_stake = backed_stakes_iter - .min() - .expect("iterator with positive length will have a min; qed"); - - difference = max_stake.saturating_sub(min_stake); - difference = difference.saturating_add(budget.saturating_sub(stake_used)); - if difference < tolerance { - return difference; - } - } else { - difference = budget; - } - - // Undo updates to exposure - elected_edges.iter_mut().for_each(|e| { - if let Some(expo) = expo_map.get_mut(&e.0) { - expo.total = expo.total.saturating_sub(to_balance(e.2)); - expo.others.retain(|i_expo| i_expo.who != *nominator); - } - e.2 = 0; - }); - - elected_edges.sort_unstable_by_key(|e| - if let Some(e) = expo_map.get(&e.0) { e.total } else { Zero::zero() } - ); - - let mut cumulative_stake: ExtendedBalance = 0; - let mut last_index = elected_edges.len() - 1; - elected_edges.iter_mut().enumerate().for_each(|(idx, e)| { - if let Some(expo) = expo_map.get_mut(&e.0) { - let stake: ExtendedBalance = to_votes(expo.total); - let stake_mul = stake.saturating_mul(idx as ExtendedBalance); - let stake_sub = stake_mul.saturating_sub(cumulative_stake); - if stake_sub > budget { - last_index = idx.checked_sub(1).unwrap_or(0); - return - } - cumulative_stake = cumulative_stake.saturating_add(stake); - } - }); - - let last_stake = elected_edges[last_index].2; - let split_ways = last_index + 1; - let excess = budget - .saturating_add(cumulative_stake) - .saturating_sub(last_stake.saturating_mul(split_ways as ExtendedBalance)); - elected_edges.iter_mut().take(split_ways).for_each(|e| { - if let Some(expo) = expo_map.get_mut(&e.0) { - e.2 = (excess / split_ways as ExtendedBalance) - .saturating_add(last_stake) - .saturating_sub(to_votes(expo.total)); - expo.total = expo.total.saturating_add(to_balance(e.2)); - expo.others.push(IndividualExposure { who: nominator.clone(), value: to_balance(e.2)}); - } - }); - - difference -} From 75d8f197610acd6d254852d0a21b2ef647de3c24 Mon Sep 17 00:00:00 2001 From: Gavin Wood Date: Mon, 2 Sep 2019 05:33:19 -0700 Subject: [PATCH 25/41] Update srml/elections-phragmen/src/lib.rs --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 82774f2d3275b..6635f7c359eb2 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -144,7 +144,7 @@ decl_storage! { // ---- State /// The current elected membership. Sorted based on account id. pub Members get(members) config(): Vec; - /// The total number of vote rounds that have happened, exclusive of the upcoming one. + /// The total number of vote rounds that have happened, excluding the upcoming one. pub ElectionRounds get(election_rounds): u32 = Zero::zero(); /// Votes of a particular voter, with the round index of the votes. From 44cb54d937270114add48a503c7d6f6d1f4528be Mon Sep 17 00:00:00 2001 From: Gavin Wood Date: Mon, 2 Sep 2019 05:33:36 -0700 Subject: [PATCH 26/41] Update srml/elections-phragmen/src/lib.rs --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 6635f7c359eb2..df88df5902b78 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -137,7 +137,7 @@ decl_storage! { // ---- parameters /// Number of members to elect. pub DesiredMembers get(desired_members) config(): u32; - /// How long each seat is kept. This defined the next block number at which an election + /// How long each seat is kept. This defines the next block number at which an election /// round will happen. pub TermDuration get(term_duration) config(): T::BlockNumber; From 8ef41eb9ba3299c4ade4eef607586a9f13abbda1 Mon Sep 17 00:00:00 2001 From: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Date: Thu, 5 Sep 2019 09:53:00 +0200 Subject: [PATCH 27/41] Update srml/elections-phragmen/src/lib.rs Co-Authored-By: Gavin Wood --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index df88df5902b78..9a9f6f5ea340a 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -186,7 +186,7 @@ decl_module! { // TODO: use decode_len #3071 and remove candidate_count. let candidates_count = Self::candidate_count(); - // addition is valid: candidates adn members never overlap. + // addition is valid: candidates and members never overlap. let allowed_votes = candidates_count as usize + Self::members().len(); ensure!(!allowed_votes.is_zero(), "cannot vote when no candidates or members exist"); From b7a1dc871b5a6af1c87badc208ed423b60f76682 Mon Sep 17 00:00:00 2001 From: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Date: Thu, 5 Sep 2019 09:53:09 +0200 Subject: [PATCH 28/41] Update srml/elections-phragmen/src/lib.rs Co-Authored-By: Gavin Wood --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 9a9f6f5ea340a..0ed09cbd8d6ba 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -234,7 +234,7 @@ decl_module! { /// their bond is slashed. /// /// A defunct voter is defined to be: - /// - a voter who's current submitted votes are all invalid. i.e. all of them are no + /// - a voter whose current submitted votes are all invalid. i.e. all of them are no /// longer a candidate nor an active member. /// /// # From fd1c7728c7c1910160004e108c8f50dc6bad92a4 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Thu, 5 Sep 2019 12:11:25 +0200 Subject: [PATCH 29/41] Bring back runner ups. --- core/phragmen/src/lib.rs | 1 + srml/elections-phragmen/src/lib.rs | 196 +++++++++++++++++++++++++---- 2 files changed, 170 insertions(+), 27 deletions(-) diff --git a/core/phragmen/src/lib.rs b/core/phragmen/src/lib.rs index 845e1d8f56695..60d7257789bd1 100644 --- a/core/phragmen/src/lib.rs +++ b/core/phragmen/src/lib.rs @@ -103,6 +103,7 @@ pub struct Edge { pub type PhragmenAssignment = (AccountId, ExtendedBalance); /// Final result of the phragmen election. +#[cfg_attr(feature = "std", derive(Debug))] pub struct PhragmenResult { /// Just winners zipped with their approval stake. Note that the approval stake is merely the /// sub of their received stake and could be used for very basic sorting and approval voting. diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 0ed09cbd8d6ba..a28c282b39ba0 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -58,6 +58,10 @@ //! _outgoing member_, meaning that they are an active member who failed to keep their spot. In //! this case, the outgoing member will get their bond back. Otherwise, the bond is slashed from //! the loser candidate. +//! - **Runner-up**: Runner ups are the best candidates immediately after the winners. The number +//! of runner-ups to keep is configurable. Runner-ups serve one major role: if a member is +//! forcefully removed, the next best runner-up is chosen as a replacement. If not, a new +//! election is triggered. //! //! Note that with the members being the default candidates for the next round and votes persisting //! in storage, the election system is entirely stable given no further input. This means that if @@ -122,7 +126,7 @@ pub trait Trait: system::Trait { /// How much should be locked up in order to be able to submit votes. type VotingBond: Get>; - /// Handler for the unbalanced reduction when a candidate has lost. + /// Handler for the unbalanced reduction when a candidate has lost (and is not a runner-up) type LoserCandidate: OnUnbalanced>; /// Handler for the unbalanced reduction when a reporter has submitted a bad defunct report. @@ -137,6 +141,8 @@ decl_storage! { // ---- parameters /// Number of members to elect. pub DesiredMembers get(desired_members) config(): u32; + /// Number of runner-ups to keep. + pub DesiredRunnerUps get(desired_runner_ups) config(): u32; /// How long each seat is kept. This defines the next block number at which an election /// round will happen. pub TermDuration get(term_duration) config(): T::BlockNumber; @@ -144,6 +150,8 @@ decl_storage! { // ---- State /// The current elected membership. Sorted based on account id. pub Members get(members) config(): Vec; + /// The current runner-ups. Sorted based on low to high merit (worse to best runner). + pub RunnerUps get(runner_ups): Vec; /// The total number of vote rounds that have happened, excluding the upcoming one. pub ElectionRounds get(election_rounds): u32 = Zero::zero(); @@ -155,8 +163,6 @@ decl_storage! { /// The present candidate list. Sorted based on account id. A current member can never enter /// this vector and is always implicitly assumed to be a candidate. pub Candidates get(candidates): Vec; - /// Current number of active candidates. - pub CandidateCount get(candidate_count): u32; } } @@ -184,8 +190,7 @@ decl_module! { fn vote(origin, votes: Vec, value: BalanceOf) { let who = ensure_signed(origin)?; - // TODO: use decode_len #3071 and remove candidate_count. - let candidates_count = Self::candidate_count(); + let candidates_count = >::decode_length(); // addition is valid: candidates and members never overlap. let allowed_votes = candidates_count as usize + Self::members().len(); @@ -275,8 +280,10 @@ decl_module! { /// Submit oneself for candidacy. /// /// A candidate will either: - /// - Lose at the end of the term and get slashed. + /// - Lose at the end of the term and forfeit their deposit. /// - Win and become a member. Members will eventually get their stash back. + /// - Become a runner-up. Runner-ups are reserved members in case one gets forcefully + /// removed. /// # /// O(LogN) Given N candidates. /// # @@ -294,7 +301,6 @@ decl_module! { .map_err(|_| "candidate does not have enough funds")?; >::mutate(|c| c.insert(index, who)); - CandidateCount::mutate(|c| *c += 1); } /// Set the desired member count. Changes will be effective at the beginning of next round. @@ -309,10 +315,11 @@ decl_module! { /// Remove a particular member from the set. This is effective immediately. /// - /// A new phragmen round is started immediately afterwards to determine the new members. - /// Current members (except for the removed one) are treated like active candidate. + /// If a runner-up is available, then the best runner-up will be removed and replaces the + /// outgoing member. Otherwise, a new phragmen round is started. /// /// Note that this does not affect the designated block number of the next election. + /// /// # /// O(LogN) given N candidates + O(phragmen) /// # @@ -329,9 +336,25 @@ decl_module! { T::KickedMember::on_unbalanced(imbalance); Self::deposit_event(RawEvent::MemberKicked(who.clone())); - // update `Members` storage -- `do_phragmen` adds this to the candidate list. - >::put(members); - Self::do_phragmen(); + let mut runner_ups = Self::runner_ups(); + if let Some(replacement) = runner_ups.pop() { + // replace the outgoing with the best runner up. + if let Err(index) = members.binary_search(&replacement) { + members.insert(index, replacement.clone()); + ElectionRounds::mutate(|v| *v += 1); + T::ChangeMembers::change_members_sorted(&[replacement], &[who], &members); + } + // else it would mean that the runner up was already a member. This cannot + // happen. If it does, not much that we can do about it. + + >::put(members); + >::put(runner_ups); + } else { + // update `Members` storage -- `do_phragmen` adds this to the candidate list. + >::put(members); + // trigger a new phragmen. grab a cup of coffee. This might take a while. + Self::do_phragmen(); + } } } @@ -442,19 +465,23 @@ impl Module { /// Calls the appropriate `ChangeMembers` function variant internally. fn do_phragmen() { let desired_seats = Self::desired_members() as usize; - let num_to_elect = desired_seats; + let desired_runner_ups = Self::desired_runner_ups() as usize; + let num_to_elect = desired_runner_ups + desired_seats; - // current members are always a candidate for the next round as well. - // this is guaranteed to not create any duplicates. let mut candidates = Self::candidates(); // candidates who exactly called `submit_candidacy`. Only these folks are at the risk of // losing their bond. - let exposed_candidates = candidates.clone(); + let mut exposed_candidates = candidates.clone(); + // current members are always a candidate for the next round as well. + // this is guaranteed to not create any duplicates. candidates.append(&mut Self::members()); + // previous runner-ups are also at the risk of losing their bond. + exposed_candidates.append(&mut Self::runner_ups()); + let voters_and_votes = >::enumerate() .map(|(v, i)| (v, i)) .collect::)>>(); - let maybe_new_members = phragmen::elect::<_, _, _, T::CurrencyToVote>( + let maybe_phragmen_result = phragmen::elect::<_, _, _, T::CurrencyToVote>( num_to_elect, 0, candidates.clone(), @@ -465,16 +492,34 @@ impl Module { let mut to_release_bond: Vec = Vec::with_capacity(desired_seats); let old_members = >::take(); - if let Some(phragmen_results) = maybe_new_members { + if let Some(phragmen_result) = maybe_phragmen_result { // filter out those who had literally no votes at all. - let new_members_with_approval = phragmen_results.winners; - let mut new_members = new_members_with_approval + // AUDIT/NOTE: the need to do this is because all candidates, even those who have no + // vote are still considered by phragmen and when good candidates are scarce, then these + // cheap ones might get elected. We might actually want to remove the filter and allow + // zero-voted candidates to also make it to the membership set. + let new_set_with_approval = phragmen_result.winners; + let new_set = new_set_with_approval .into_iter() .filter_map(|(m, a)| if a.is_zero() { None } else { Some(m) } ) .collect::>(); + + // split new set into winners and runner ups. + let split_point = desired_seats.min(new_set.len()); + let mut new_members = (&new_set[..split_point]).to_vec(); + let runner_ups = &new_set[split_point..] + .into_iter() + .cloned() + .rev() + .collect::>(); + + // sort and save the members. new_members.sort(); >::put(new_members.clone()); + // save the runners as-is + >::put(runner_ups); + // report member changes. We compute diff because we need the outgoing list. let (incoming, outgoing) = T::ChangeMembers::compute_members_diff( &new_members, @@ -491,13 +536,16 @@ impl Module { to_release_bond = outgoing.to_vec(); // Burn loser bond. members list is sorted. O(NLogM) (N candidates, M members) + // runner up list is not sorted. O(K*N) given K runner ups. Overall: O(NLogM + N*K) exposed_candidates.into_iter().for_each(|c| { - if new_members.binary_search(&c).is_err() { + // any candidate who is not a member and not a runner up. + if new_members.binary_search(&c).is_err() && !runner_ups.contains(&c) + { let (imbalance, _) = T::Currency::slash_reserved(&c, T::CandidacyBond::get()); T::LoserCandidate::on_unbalanced(imbalance); } }); - Self::deposit_event(RawEvent::NewTerm(new_members)); + Self::deposit_event(RawEvent::NewTerm(new_members.to_vec())); } else { Self::deposit_event(RawEvent::EmptyTerm); } @@ -509,7 +557,6 @@ impl Module { // clean candidates. >::kill(); - CandidateCount::put(0); ElectionRounds::mutate(|v| *v += 1); } @@ -637,6 +684,7 @@ mod tests { pub struct ExtBuilder { balance_factor: u64, voter_bond: u64, + desired_runner_ups: u32, } impl Default for ExtBuilder { @@ -644,6 +692,7 @@ mod tests { Self { balance_factor: 1, voter_bond: 2, + desired_runner_ups: 0, } } } @@ -653,6 +702,10 @@ mod tests { self.voter_bond = fee; self } + pub fn desired_runner_ups(mut self, count: u32) -> Self { + self.desired_runner_ups = count; + self + } pub fn build(self) -> runtime_io::TestExternalities { VOTING_BOND.with(|v| *v.borrow_mut() = self.voter_bond); GenesisConfig { @@ -670,6 +723,7 @@ mod tests { elections: Some(elections::GenesisConfig::{ members: vec![], desired_members: 2, + desired_runner_ups: self.desired_runner_ups, term_duration: 5, }), }.build_storage().unwrap().into() @@ -699,9 +753,10 @@ mod tests { assert_eq!(Elections::election_rounds(), 0); assert_eq!(Elections::members(), vec![]); + assert_eq!(Elections::runner_ups(), vec![]); assert_eq!(Elections::candidates(), vec![]); - assert_eq!(Elections::candidate_count(), 0); + assert_eq!(>::decode_length(), 0); assert!(Elections::is_candidate(&1).is_err()); assert_eq!(all_voters(), vec![]); @@ -736,6 +791,33 @@ mod tests { }); } + #[test] + fn simple_candidate_submission_with_no_votes_should_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(Elections::candidates(), Vec::::new()); + + assert_ok!(Elections::submit_candidacy(Origin::signed(1))); + assert_ok!(Elections::submit_candidacy(Origin::signed(2))); + + assert!(Elections::is_candidate(&1).is_ok()); + assert!(Elections::is_candidate(&2).is_ok()); + assert_eq!(Elections::candidates(), vec![1, 2]); + + assert_eq!(Elections::members(), vec![]); + assert_eq!(Elections::runner_ups(), vec![]); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + + assert!(Elections::is_candidate(&1).is_err()); + assert!(Elections::is_candidate(&2).is_err()); + assert_eq!(Elections::candidates(), vec![]); + + assert_eq!(Elections::members(), vec![]); + assert_eq!(Elections::runner_ups(), vec![]); + }); + } + #[test] fn dupe_candidate_submission_should_not_work() { with_externalities(&mut ExtBuilder::default().build(), || { @@ -760,6 +842,7 @@ mod tests { assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![5]); + assert_eq!(Elections::runner_ups(), vec![]); assert_eq!(Elections::candidates(), vec![]); assert_noop!( @@ -1089,7 +1172,7 @@ mod tests { assert_eq!(Elections::votes_of(4), vec![4]); assert_eq!(Elections::candidates(), vec![3, 4, 5]); - assert_eq!(Elections::candidate_count(), 3); + assert_eq!(>::decode_length(), 3); assert_eq!(Elections::election_rounds(), 0); @@ -1097,9 +1180,10 @@ mod tests { assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![3, 5]); + assert_eq!(Elections::runner_ups(), vec![]); assert_eq_uvec!(all_voters(), vec![2, 3, 4]); assert_eq!(Elections::candidates(), vec![]); - assert_eq!(Elections::candidate_count(), 0); + assert_eq!(>::decode_length(), 0); assert_eq!(Elections::election_rounds(), 1); }); @@ -1169,6 +1253,64 @@ mod tests { }); } + #[test] + fn runner_ups_should_be_kept() { + with_externalities(&mut ExtBuilder::default().desired_runner_ups(2).build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_ok!(Elections::submit_candidacy(Origin::signed(2))); + + assert_ok!(Elections::vote(Origin::signed(2), vec![3], 20)); + assert_ok!(Elections::vote(Origin::signed(3), vec![2], 30)); + assert_ok!(Elections::vote(Origin::signed(4), vec![5], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![4], 50)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + // sorted based on account id. + assert_eq!(Elections::members(), vec![4, 5]); + // sorted based on merit (least -> most) + assert_eq!(Elections::runner_ups(), vec![3, 2]); + + // runner ups are still locked. + assert_eq!(balances(&4), (35, 5)); + assert_eq!(balances(&5), (45, 5)); + assert_eq!(balances(&3), (25, 5)); + }); + } + + #[test] + fn runner_ups_lose_bond_once_outgoing() { + with_externalities(&mut ExtBuilder::default().desired_runner_ups(1).build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(2))); + + assert_ok!(Elections::vote(Origin::signed(2), vec![2], 20)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![4, 5]); + + assert_eq!(Elections::runner_ups(), vec![2]); + assert_eq!(balances(&2), (15, 5)); + + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + + System::set_block_number(10); + assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![4, 5]); + + assert_eq!(Elections::runner_ups(), vec![3]); + assert_eq!(balances(&3), (25, 5)); + assert_eq!(balances(&2), (15, 2)); + }); + } + #[test] fn current_members_are_always_implicitly_next_candidate() { with_externalities(&mut ExtBuilder::default().build(), || { @@ -1269,7 +1411,7 @@ mod tests { assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); - assert_eq!(Elections::candidate_count(), 3); + assert_eq!(>::decode_length(), 3); assert_eq!(Elections::election_rounds(), 0); From a8590fafd81f157118b8066df41438f1fcebf23b Mon Sep 17 00:00:00 2001 From: kianenigma Date: Thu, 5 Sep 2019 12:18:34 +0200 Subject: [PATCH 30/41] use decode_len --- srml/elections-phragmen/src/lib.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index a28c282b39ba0..17415235aa051 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -86,8 +86,9 @@ use sr_primitives::traits::{Zero, StaticLookup, Bounded, Convert}; use sr_primitives::weights::SimpleDispatchInfo; -use srml_support::{StorageValue, StorageMap, EnumerableStorageMap, decl_storage, decl_event, ensure, - decl_module, dispatch, +use srml_support::{ + StorageValue, StorageMap, StorageLinkedMap, + decl_storage, decl_event, ensure, decl_module, dispatch, traits::{ Currency, Get, LockableCurrency, LockIdentifier, ReservableCurrency, WithdrawReasons, ChangeMembers, OnUnbalanced, @@ -190,7 +191,7 @@ decl_module! { fn vote(origin, votes: Vec, value: BalanceOf) { let who = ensure_signed(origin)?; - let candidates_count = >::decode_length(); + let candidates_count = >::decode_len().unwrap_or(0); // addition is valid: candidates and members never overlap. let allowed_votes = candidates_count as usize + Self::members().len(); @@ -756,7 +757,7 @@ mod tests { assert_eq!(Elections::runner_ups(), vec![]); assert_eq!(Elections::candidates(), vec![]); - assert_eq!(>::decode_length(), 0); + assert_eq!(>::decode_len().unwrap(), 0); assert!(Elections::is_candidate(&1).is_err()); assert_eq!(all_voters(), vec![]); @@ -1172,7 +1173,7 @@ mod tests { assert_eq!(Elections::votes_of(4), vec![4]); assert_eq!(Elections::candidates(), vec![3, 4, 5]); - assert_eq!(>::decode_length(), 3); + assert_eq!(>::decode_len().unwrap(), 3); assert_eq!(Elections::election_rounds(), 0); @@ -1183,7 +1184,7 @@ mod tests { assert_eq!(Elections::runner_ups(), vec![]); assert_eq_uvec!(all_voters(), vec![2, 3, 4]); assert_eq!(Elections::candidates(), vec![]); - assert_eq!(>::decode_length(), 0); + assert_eq!(>::decode_len().unwrap(), 0); assert_eq!(Elections::election_rounds(), 1); }); @@ -1411,7 +1412,7 @@ mod tests { assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); - assert_eq!(>::decode_length(), 3); + assert_eq!(>::decode_len().unwrap(), 3); assert_eq!(Elections::election_rounds(), 0); From 6debacb01a9519a49730e8f0910612cbb2b6c384 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Thu, 5 Sep 2019 15:15:41 +0200 Subject: [PATCH 31/41] Better weight values. --- srml/elections-phragmen/src/lib.rs | 71 ++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 17415235aa051..e7777b6cbae3f 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -75,13 +75,6 @@ //! - [`Call`](./enum.Call.html) //! - [`Module`](./struct.Module.html) -// TODO: we don't hold the voters accountable in any sense; a voter can remove themselves (unbond) -// right after an election (even if their voted for some winners) and they can be reported right -// after an election. We should probably add some accountability here. Simplest would be that voters -// cannot remove/report as long as they have a voted target who is in the member list. - -// TODO: update the weights with O values. - #![cfg_attr(not(feature = "std"), no_std)] use sr_primitives::traits::{Zero, StaticLookup, Bounded, Convert}; @@ -185,15 +178,18 @@ decl_module! { /// and keep some for further transactions. /// /// # - /// O(1) + /// #### State + /// Reads: O(1) + /// Writes: O(V) given `V` votes. V is bounded by 16. /// # - #[weight = SimpleDispatchInfo::FixedNormal(500_000)] + #[weight = SimpleDispatchInfo::FixedNormal(100_000)] fn vote(origin, votes: Vec, value: BalanceOf) { let who = ensure_signed(origin)?; - let candidates_count = >::decode_len().unwrap_or(0); + let candidates_count = >::decode_len().unwrap_or(0) as usize; + let members_count = >::decode_len().unwrap_or(0) as usize; // addition is valid: candidates and members never overlap. - let allowed_votes = candidates_count as usize + Self::members().len(); + let allowed_votes = candidates_count + members_count; ensure!(!allowed_votes.is_zero(), "cannot vote when no candidates or members exist"); ensure!(votes.len() <= allowed_votes, "cannot vote more than candidates"); @@ -226,7 +222,13 @@ decl_module! { } /// Remove `origin` as a voter. This removes the lock and returns the bond. - #[weight = SimpleDispatchInfo::FixedNormal(500_000)] + /// + /// # + /// #### State + /// Reads: O(1) + /// Writes: O(1) + /// # + #[weight = SimpleDispatchInfo::FixedNormal(10_000)] fn remove_voter(origin) { let who = ensure_signed(origin)?; @@ -244,9 +246,11 @@ decl_module! { /// longer a candidate nor an active member. /// /// # - /// O(NLogM) given M current candidates and N votes for `target`. + /// #### State + /// Reads: O(NLogM) given M current candidates and N votes for `target`. + /// Writes: O(1) /// # - #[weight = SimpleDispatchInfo::FixedNormal(500_000)] + #[weight = SimpleDispatchInfo::FixedNormal(1_000_000)] fn report_defunct_voter(origin, target: ::Source) { let reporter = ensure_signed(origin)?; let target = T::Lookup::lookup(target)?; @@ -258,9 +262,7 @@ decl_module! { // function O(MLonN) with N candidates in total and M of them being voted by `target`. // We could easily add another mapping to be able to check if someone is a candidate in // `O(1)` but that would make the process of removing candidates at the end of each - // round slightly harder. - // - // Note that for now we have a bound of number of votes (`N`). + // round slightly harder. Note that for now we have a bound of number of votes (`N`). let valid = Self::is_defunct_voter(&target); if valid { // reporter will get the voting bond of the target @@ -285,19 +287,23 @@ decl_module! { /// - Win and become a member. Members will eventually get their stash back. /// - Become a runner-up. Runner-ups are reserved members in case one gets forcefully /// removed. + /// /// # - /// O(LogN) Given N candidates. + /// #### State + /// Reads: O(LogN) Given N candidates. + /// Writes: O(1) /// # #[weight = SimpleDispatchInfo::FixedNormal(500_000)] fn submit_candidacy(origin) { let who = ensure_signed(origin)?; let is_candidate = Self::is_candidate(&who); - ensure!(!is_candidate.is_ok(), "duplicate candidate submission"); - ensure!(!Self::is_member(&who), "member cannot re-submit candidacy"); + ensure!(is_candidate.is_err(), "duplicate candidate submission"); // assured to be an error, error always contains the index. let index = is_candidate.unwrap_err(); + ensure!(!Self::is_member(&who), "member cannot re-submit candidacy"); + T::Currency::reserve(&who, T::CandidacyBond::get()) .map_err(|_| "candidate does not have enough funds")?; @@ -305,8 +311,11 @@ decl_module! { } /// Set the desired member count. Changes will be effective at the beginning of next round. + /// /// # - /// O(1) + /// #### State + /// Reads: O(1) + /// Writes: O(1) /// # #[weight = SimpleDispatchInfo::FixedOperational(10_000)] fn set_desired_member_count(origin, #[compact] count: u32) { @@ -322,7 +331,9 @@ decl_module! { /// Note that this does not affect the designated block number of the next election. /// /// # - /// O(LogN) given N candidates + O(phragmen) + /// #### State + /// Reads: O(do_phragmen) + /// Writes: O(do_phragmen) /// # #[weight = SimpleDispatchInfo::FixedOperational(2_000_000)] fn remove_member(origin, who: ::Source) { @@ -360,6 +371,12 @@ decl_module! { } /// Set the duration of each term. This will affect the next election's block number. + /// + /// # + /// #### State + /// Reads: O(1) + /// Writes: O(1) + /// # #[weight = SimpleDispatchInfo::FixedOperational(10_000)] fn set_term_duration(origin, #[compact] count: T::BlockNumber) { ensure_root(origin)?; @@ -396,14 +413,14 @@ impl Module { /// Check if `who` is a candidate. It returns the insert index if the element does not exists as /// an error. /// - /// O(LogN) given N candidates. + /// State: O(LogN) given N candidates. fn is_candidate(who: &T::AccountId) -> Result<(), usize> { Self::candidates().binary_search(who).map(|_| ()) } /// Check if `who` is a voter. It may or may not be a _current_ one. /// - /// O(1). + /// State: O(1). fn is_voter(who: &T::AccountId) -> bool { >::exists(who) } @@ -464,6 +481,12 @@ impl Module { /// Run the phragmen election with all required side processes and state updates. /// /// Calls the appropriate `ChangeMembers` function variant internally. + /// + /// # + /// #### State + /// Reads: O(C + V*E) where C = candidates, V voters and E votes per voter exits. + /// Writes: O(M + R) with M desired members and R runner-ups. + /// # fn do_phragmen() { let desired_seats = Self::desired_members() as usize; let desired_runner_ups = Self::desired_runner_ups() as usize; From 28e93724b863450782cd8c1dec9d9375041c65a2 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Mon, 9 Sep 2019 23:24:34 +0200 Subject: [PATCH 32/41] Update bogus doc --- srml/elections-phragmen/src/lib.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index e7777b6cbae3f..94747bc26a07b 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -52,16 +52,15 @@ //! //! Candidates also reserve a bond as they submit candidacy. A candidate cannot take their candidacy //! back. A candidate can end up in one of the below situations: -//! - **Winner**: A winner is kept as a _member_. They must still have a bond in reserve and they -//! are automatically counted as a candidate for the next election. +//! - **Winner**: A winner is kept as a _member_. They must still have a bond in reserve and they +//! are automatically counted as a candidate for the next election. //! - **Loser**: Any of the candidate who are not a winner are left as losers. A loser might be an -//! _outgoing member_, meaning that they are an active member who failed to keep their spot. In -//! this case, the outgoing member will get their bond back. Otherwise, the bond is slashed from -//! the loser candidate. +//! _outgoing member_, meaning that they are an active member who failed to keep their spot. In +//! this case, the outgoing member will get their bond back. Otherwise, the bond is slashed from +//! the loser candidate. //! - **Runner-up**: Runner ups are the best candidates immediately after the winners. The number -//! of runner-ups to keep is configurable. Runner-ups serve one major role: if a member is -//! forcefully removed, the next best runner-up is chosen as a replacement. If not, a new -//! election is triggered. +//! of runner-ups to keep is configurable. Runner-ups are used, in order that they are elected, +//! as replacements when a candidate is kicked by `remove_member()`. //! //! Note that with the members being the default candidates for the next round and votes persisting //! in storage, the election system is entirely stable given no further input. This means that if From 016e5ff291e01526ffc8d77471949c16faeaea4a Mon Sep 17 00:00:00 2001 From: kianenigma Date: Mon, 9 Sep 2019 23:24:56 +0200 Subject: [PATCH 33/41] Bump. --- node/runtime/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/runtime/src/lib.rs b/node/runtime/src/lib.rs index 9d7ae5d9df85d..4a1df077c4f65 100644 --- a/node/runtime/src/lib.rs +++ b/node/runtime/src/lib.rs @@ -82,8 +82,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // and set impl_version to equal spec_version. If only runtime // implementation changes and behavior does not, then leave spec_version as // is and increment impl_version. - spec_version: 155, - impl_version: 155, + spec_version: 156, + impl_version: 156, apis: RUNTIME_API_VERSIONS, }; From 7e4a4ed684294c567f7038bd7df92f41fac67886 Mon Sep 17 00:00:00 2001 From: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Date: Mon, 9 Sep 2019 23:29:40 +0200 Subject: [PATCH 34/41] Update srml/elections-phragmen/src/lib.rs Co-Authored-By: Gavin Wood --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 94747bc26a07b..ceea46c8a9995 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -58,7 +58,7 @@ //! _outgoing member_, meaning that they are an active member who failed to keep their spot. In //! this case, the outgoing member will get their bond back. Otherwise, the bond is slashed from //! the loser candidate. -//! - **Runner-up**: Runner ups are the best candidates immediately after the winners. The number +//! - **Runner-up**: Runners-up are the best candidates immediately after the winners. The number //! of runner-ups to keep is configurable. Runner-ups are used, in order that they are elected, //! as replacements when a candidate is kicked by `remove_member()`. //! From b76d95f5978499c3743556282656f349d836c6ff Mon Sep 17 00:00:00 2001 From: kianenigma Date: Thu, 12 Sep 2019 21:48:30 +0200 Subject: [PATCH 35/41] Review comments. --- srml/elections-phragmen/src/lib.rs | 99 +++++++++++++++++++----------- 1 file changed, 64 insertions(+), 35 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index ceea46c8a9995..345bca7bc9ab5 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -59,7 +59,7 @@ //! this case, the outgoing member will get their bond back. Otherwise, the bond is slashed from //! the loser candidate. //! - **Runner-up**: Runners-up are the best candidates immediately after the winners. The number -//! of runner-ups to keep is configurable. Runner-ups are used, in order that they are elected, +//! of runners_up to keep is configurable. Runners-up are used, in order that they are elected, //! as replacements when a candidate is kicked by `remove_member()`. //! //! Note that with the members being the default candidates for the next round and votes persisting @@ -134,8 +134,8 @@ decl_storage! { // ---- parameters /// Number of members to elect. pub DesiredMembers get(desired_members) config(): u32; - /// Number of runner-ups to keep. - pub DesiredRunnerUps get(desired_runner_ups) config(): u32; + /// Number of runners_up to keep. + pub DesiredRunnersUp get(desired_runners_up) config(): u32; /// How long each seat is kept. This defines the next block number at which an election /// round will happen. pub TermDuration get(term_duration) config(): T::BlockNumber; @@ -143,8 +143,8 @@ decl_storage! { // ---- State /// The current elected membership. Sorted based on account id. pub Members get(members) config(): Vec; - /// The current runner-ups. Sorted based on low to high merit (worse to best runner). - pub RunnerUps get(runner_ups): Vec; + /// The current runners_up. Sorted based on low to high merit (worse to best runner). + pub RunnersUp get(runners_up): Vec; /// The total number of vote rounds that have happened, excluding the upcoming one. pub ElectionRounds get(election_rounds): u32 = Zero::zero(); @@ -284,7 +284,7 @@ decl_module! { /// A candidate will either: /// - Lose at the end of the term and forfeit their deposit. /// - Win and become a member. Members will eventually get their stash back. - /// - Become a runner-up. Runner-ups are reserved members in case one gets forcefully + /// - Become a runner-up. Runners-ups are reserved members in case one gets forcefully /// removed. /// /// # @@ -347,8 +347,8 @@ decl_module! { T::KickedMember::on_unbalanced(imbalance); Self::deposit_event(RawEvent::MemberKicked(who.clone())); - let mut runner_ups = Self::runner_ups(); - if let Some(replacement) = runner_ups.pop() { + let mut runners_up = Self::runners_up(); + if let Some(replacement) = runners_up.pop() { // replace the outgoing with the best runner up. if let Err(index) = members.binary_search(&replacement) { members.insert(index, replacement.clone()); @@ -359,7 +359,7 @@ decl_module! { // happen. If it does, not much that we can do about it. >::put(members); - >::put(runner_ups); + >::put(runners_up); } else { // update `Members` storage -- `do_phragmen` adds this to the candidate list. >::put(members); @@ -484,12 +484,12 @@ impl Module { /// # /// #### State /// Reads: O(C + V*E) where C = candidates, V voters and E votes per voter exits. - /// Writes: O(M + R) with M desired members and R runner-ups. + /// Writes: O(M + R) with M desired members and R runners_up. /// # fn do_phragmen() { let desired_seats = Self::desired_members() as usize; - let desired_runner_ups = Self::desired_runner_ups() as usize; - let num_to_elect = desired_runner_ups + desired_seats; + let desired_runners_up = Self::desired_runners_up() as usize; + let num_to_elect = desired_runners_up + desired_seats; let mut candidates = Self::candidates(); // candidates who exactly called `submit_candidacy`. Only these folks are at the risk of @@ -498,8 +498,10 @@ impl Module { // current members are always a candidate for the next round as well. // this is guaranteed to not create any duplicates. candidates.append(&mut Self::members()); - // previous runner-ups are also at the risk of losing their bond. - exposed_candidates.append(&mut Self::runner_ups()); + // previous runners_up are also always candidates for the next round. + candidates.append(&mut Self::runners_up()); + // and exposed to being an outgoing in case they are no longer elected. + exposed_candidates.append(&mut Self::runners_up()); let voters_and_votes = >::enumerate() .map(|(v, i)| (v, i)) @@ -507,7 +509,7 @@ impl Module { let maybe_phragmen_result = phragmen::elect::<_, _, _, T::CurrencyToVote>( num_to_elect, 0, - candidates.clone(), + candidates, voters_and_votes, Self::locked_stake_of, false, @@ -530,7 +532,7 @@ impl Module { // split new set into winners and runner ups. let split_point = desired_seats.min(new_set.len()); let mut new_members = (&new_set[..split_point]).to_vec(); - let runner_ups = &new_set[split_point..] + let runners_up = &new_set[split_point..] .into_iter() .cloned() .rev() @@ -541,7 +543,7 @@ impl Module { >::put(new_members.clone()); // save the runners as-is - >::put(runner_ups); + >::put(runners_up); // report member changes. We compute diff because we need the outgoing list. let (incoming, outgoing) = T::ChangeMembers::compute_members_diff( @@ -562,7 +564,7 @@ impl Module { // runner up list is not sorted. O(K*N) given K runner ups. Overall: O(NLogM + N*K) exposed_candidates.into_iter().for_each(|c| { // any candidate who is not a member and not a runner up. - if new_members.binary_search(&c).is_err() && !runner_ups.contains(&c) + if new_members.binary_search(&c).is_err() && !runners_up.contains(&c) { let (imbalance, _) = T::Currency::slash_reserved(&c, T::CandidacyBond::get()); T::LoserCandidate::on_unbalanced(imbalance); @@ -707,7 +709,7 @@ mod tests { pub struct ExtBuilder { balance_factor: u64, voter_bond: u64, - desired_runner_ups: u32, + desired_runners_up: u32, } impl Default for ExtBuilder { @@ -715,7 +717,7 @@ mod tests { Self { balance_factor: 1, voter_bond: 2, - desired_runner_ups: 0, + desired_runners_up: 0, } } } @@ -725,8 +727,8 @@ mod tests { self.voter_bond = fee; self } - pub fn desired_runner_ups(mut self, count: u32) -> Self { - self.desired_runner_ups = count; + pub fn desired_runners_up(mut self, count: u32) -> Self { + self.desired_runners_up = count; self } pub fn build(self) -> runtime_io::TestExternalities { @@ -746,7 +748,7 @@ mod tests { elections: Some(elections::GenesisConfig::{ members: vec![], desired_members: 2, - desired_runner_ups: self.desired_runner_ups, + desired_runners_up: self.desired_runners_up, term_duration: 5, }), }.build_storage().unwrap().into() @@ -776,7 +778,7 @@ mod tests { assert_eq!(Elections::election_rounds(), 0); assert_eq!(Elections::members(), vec![]); - assert_eq!(Elections::runner_ups(), vec![]); + assert_eq!(Elections::runners_up(), vec![]); assert_eq!(Elections::candidates(), vec![]); assert_eq!(>::decode_len().unwrap(), 0); @@ -827,7 +829,7 @@ mod tests { assert_eq!(Elections::candidates(), vec![1, 2]); assert_eq!(Elections::members(), vec![]); - assert_eq!(Elections::runner_ups(), vec![]); + assert_eq!(Elections::runners_up(), vec![]); System::set_block_number(5); assert_ok!(Elections::end_block(System::block_number())); @@ -837,7 +839,7 @@ mod tests { assert_eq!(Elections::candidates(), vec![]); assert_eq!(Elections::members(), vec![]); - assert_eq!(Elections::runner_ups(), vec![]); + assert_eq!(Elections::runners_up(), vec![]); }); } @@ -865,7 +867,7 @@ mod tests { assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![5]); - assert_eq!(Elections::runner_ups(), vec![]); + assert_eq!(Elections::runners_up(), vec![]); assert_eq!(Elections::candidates(), vec![]); assert_noop!( @@ -1203,7 +1205,7 @@ mod tests { assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![3, 5]); - assert_eq!(Elections::runner_ups(), vec![]); + assert_eq!(Elections::runners_up(), vec![]); assert_eq_uvec!(all_voters(), vec![2, 3, 4]); assert_eq!(Elections::candidates(), vec![]); assert_eq!(>::decode_len().unwrap(), 0); @@ -1277,8 +1279,8 @@ mod tests { } #[test] - fn runner_ups_should_be_kept() { - with_externalities(&mut ExtBuilder::default().desired_runner_ups(2).build(), || { + fn runners_up_should_be_kept() { + with_externalities(&mut ExtBuilder::default().desired_runners_up(2).build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); @@ -1294,7 +1296,7 @@ mod tests { // sorted based on account id. assert_eq!(Elections::members(), vec![4, 5]); // sorted based on merit (least -> most) - assert_eq!(Elections::runner_ups(), vec![3, 2]); + assert_eq!(Elections::runners_up(), vec![3, 2]); // runner ups are still locked. assert_eq!(balances(&4), (35, 5)); @@ -1304,8 +1306,35 @@ mod tests { } #[test] - fn runner_ups_lose_bond_once_outgoing() { - with_externalities(&mut ExtBuilder::default().desired_runner_ups(1).build(), || { + fn runners_up_should_be_next_candidates() { + with_externalities(&mut ExtBuilder::default().desired_runners_up(2).build(), || { + assert_ok!(Elections::submit_candidacy(Origin::signed(5))); + assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_ok!(Elections::submit_candidacy(Origin::signed(2))); + + assert_ok!(Elections::vote(Origin::signed(2), vec![2], 20)); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + + System::set_block_number(5); + assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![4, 5]); + assert_eq!(Elections::runners_up(), vec![2, 3]); + + assert_ok!(Elections::vote(Origin::signed(5), vec![5], 15)); + + System::set_block_number(10); + assert_ok!(Elections::end_block(System::block_number())); + assert_eq!(Elections::members(), vec![3, 4]); + assert_eq!(Elections::runners_up(), vec![5, 2]); + }); + } + + #[test] + fn runners_up_lose_bond_once_outgoing() { + with_externalities(&mut ExtBuilder::default().desired_runners_up(1).build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); assert_ok!(Elections::submit_candidacy(Origin::signed(2))); @@ -1318,7 +1347,7 @@ mod tests { assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![4, 5]); - assert_eq!(Elections::runner_ups(), vec![2]); + assert_eq!(Elections::runners_up(), vec![2]); assert_eq!(balances(&2), (15, 5)); assert_ok!(Elections::submit_candidacy(Origin::signed(3))); @@ -1328,7 +1357,7 @@ mod tests { assert_ok!(Elections::end_block(System::block_number())); assert_eq!(Elections::members(), vec![4, 5]); - assert_eq!(Elections::runner_ups(), vec![3]); + assert_eq!(Elections::runners_up(), vec![3]); assert_eq!(balances(&3), (25, 5)); assert_eq!(balances(&2), (15, 2)); }); From 8f7f589919d3310f765c3ef6d8936b0cb2d3bb58 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Fri, 13 Sep 2019 08:22:32 +0200 Subject: [PATCH 36/41] One more test --- srml/elections-phragmen/src/lib.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 345bca7bc9ab5..d6a13faa40eca 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -1401,20 +1401,27 @@ mod tests { fn election_state_is_uninterrupted() { // what I mean by uninterrupted: // given no input or stimulants the same members are re-elected. - with_externalities(&mut ExtBuilder::default().build(), || { + with_externalities(&mut ExtBuilder::default().desired_runners_up(2).build(), || { assert_ok!(Elections::submit_candidacy(Origin::signed(5))); assert_ok!(Elections::submit_candidacy(Origin::signed(4))); + assert_ok!(Elections::submit_candidacy(Origin::signed(3))); + assert_ok!(Elections::submit_candidacy(Origin::signed(2))); - assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50)); + assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40)); + assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30)); + assert_ok!(Elections::vote(Origin::signed(2), vec![2], 20)); let check_at_block = |b: u32| { System::set_block_number(b.into()); assert_ok!(Elections::end_block(System::block_number())); // we keep re-electing the same folks. assert_eq!(Elections::members(), vec![4, 5]); + assert_eq!(Elections::runners_up(), vec![2, 3]); + // no new candidates but old members and runners-up are always added. + assert_eq!(Elections::candidates(), vec![]); assert_eq!(Elections::election_rounds(), b / 5); - assert_eq_uvec!(all_voters(), vec![5, 4]); + assert_eq_uvec!(all_voters(), vec![2, 3, 4, 5]); }; // this state will always persist when no further input is given. From 509387ec2eb0b17eb3a245487e0ac5d33ecccff8 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Sun, 15 Sep 2019 09:24:24 +0200 Subject: [PATCH 37/41] Fix tests --- core/phragmen/src/mock.rs | 10 +++++----- core/phragmen/src/tests.rs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/phragmen/src/mock.rs b/core/phragmen/src/mock.rs index 659ff9dacb4a2..e4f220affcdc2 100644 --- a/core/phragmen/src/mock.rs +++ b/core/phragmen/src/mock.rs @@ -69,7 +69,7 @@ pub(crate) type AccountId = u64; #[derive(Debug, Clone)] pub(crate) struct _PhragmenResult { - pub winners: Vec, + pub winners: Vec<(A, Balance)>, pub assignments: Vec<(A, Vec<_PhragmenAssignment>)> } @@ -84,7 +84,7 @@ pub(crate) fn elect_float( A: Default + Ord + Member + Copy, for<'r> FS: Fn(&'r A) -> Balance, { - let mut elected_candidates: Vec; + let mut elected_candidates: Vec<(A, Balance)>; let mut assigned: Vec<(A, Vec<_PhragmenAssignment>)>; let mut c_idx_cache = BTreeMap::::new(); let num_voters = initial_candidates.len() + initial_voters.len(); @@ -178,7 +178,7 @@ pub(crate) fn elect_float( } } - elected_candidates.push(winner.who.clone()); + elected_candidates.push((winner.who.clone(), winner.approval_stake as Balance)); } else { break } @@ -187,7 +187,7 @@ pub(crate) fn elect_float( for n in &mut voters { let mut assignment = (n.who.clone(), vec![]); for e in &mut n.edges { - if let Some(c) = elected_candidates.iter().cloned().find(|c| *c == e.who) { + if let Some(c) = elected_candidates.iter().cloned().map(|(c, _)| c).find(|c| *c == e.who) { if c != n.who { let ratio = e.load / n.load; assignment.1.push((e.who.clone(), ratio)); @@ -397,7 +397,7 @@ pub(crate) fn build_support_map( let mut supports = <_SupportMap>::new(); result.winners .iter() - .map(|e| (e, stake_of(e) as f64)) + .map(|(e, _)| (e, stake_of(e) as f64)) .for_each(|(e, s)| { let item = _Support { own: s, total: s, ..Default::default() }; supports.insert(e.clone(), item); diff --git a/core/phragmen/src/tests.rs b/core/phragmen/src/tests.rs index 5a8cddb984aa6..750adb49cec61 100644 --- a/core/phragmen/src/tests.rs +++ b/core/phragmen/src/tests.rs @@ -35,7 +35,7 @@ fn float_phragmen_poc_works() { let winners = phragmen_result.clone().winners; let assignments = phragmen_result.clone().assignments; - assert_eq_uvec!(winners, vec![2, 3]); + assert_eq_uvec!(winners, vec![(2, 40), (3, 50)]); assert_eq_uvec!( assignments, vec![ @@ -86,7 +86,7 @@ fn phragmen_poc_works() { false, ).unwrap(); - assert_eq_uvec!(winners, vec![2, 3]); + assert_eq_uvec!(winners, vec![(2, 40), (3, 50)]); assert_eq_uvec!( assignments, vec![ From a6e1a3e20b3bad0a5ce57096e1eb4387154e2c5a Mon Sep 17 00:00:00 2001 From: kianenigma Date: Sun, 15 Sep 2019 11:59:30 +0200 Subject: [PATCH 38/41] Fix build --- srml/elections-phragmen/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index d6a13faa40eca..1c0434ac1ca48 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -76,7 +76,7 @@ #![cfg_attr(not(feature = "std"), no_std)] -use sr_primitives::traits::{Zero, StaticLookup, Bounded, Convert}; +use sr_primitives::{print, traits::{Zero, StaticLookup, Bounded, Convert}}; use sr_primitives::weights::SimpleDispatchInfo; use srml_support::{ StorageValue, StorageMap, StorageLinkedMap, @@ -385,8 +385,8 @@ decl_module! { /// What to do at the end of each block. Checks if an election needs to happen or not. fn on_initialize(n: T::BlockNumber) { if let Err(e) = Self::end_block(n) { - runtime_io::print("Guru meditation"); - runtime_io::print(e); + print("Guru meditation"); + print(e); } } } From 1adc36bcbe351cf6dc6e4f76bf4e5a6540bb80b5 Mon Sep 17 00:00:00 2001 From: kianenigma Date: Sun, 15 Sep 2019 12:53:48 +0200 Subject: [PATCH 39/41] .. and fix benchmarks. --- core/phragmen/benches/phragmen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/phragmen/benches/phragmen.rs b/core/phragmen/benches/phragmen.rs index dfe56aafd8958..ecbf235bab287 100644 --- a/core/phragmen/benches/phragmen.rs +++ b/core/phragmen/benches/phragmen.rs @@ -110,7 +110,7 @@ fn do_phragmen( let mut supports = >::new(); elected_stashes .iter() - .map(|e| (e, to_votes(slashable_balance(e)))) + .map(|(e, _)| (e, to_votes(slashable_balance(e)))) .for_each(|(e, s)| { let item = Support { own: s, total: s, ..Default::default() }; supports.insert(e.clone(), item); From 3ae31218794414393dc2b3d200dbaab2049b5074 Mon Sep 17 00:00:00 2001 From: Gavin Wood Date: Thu, 19 Sep 2019 16:26:47 +0800 Subject: [PATCH 40/41] Update srml/elections-phragmen/src/lib.rs --- srml/elections-phragmen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srml/elections-phragmen/src/lib.rs b/srml/elections-phragmen/src/lib.rs index 1c0434ac1ca48..6b0c773ec9c66 100644 --- a/srml/elections-phragmen/src/lib.rs +++ b/srml/elections-phragmen/src/lib.rs @@ -492,7 +492,7 @@ impl Module { let num_to_elect = desired_runners_up + desired_seats; let mut candidates = Self::candidates(); - // candidates who exactly called `submit_candidacy`. Only these folks are at the risk of + // candidates who explicitly called `submit_candidacy`. Only these folks are at the risk of // losing their bond. let mut exposed_candidates = candidates.clone(); // current members are always a candidate for the next round as well. From 91827db0acb0d5f677e6e84ee4de09a9bad4196c Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 19 Sep 2019 16:28:35 +0800 Subject: [PATCH 41/41] Version bump --- node/runtime/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/runtime/src/lib.rs b/node/runtime/src/lib.rs index 94497de0ac81f..0c1a229ab2567 100644 --- a/node/runtime/src/lib.rs +++ b/node/runtime/src/lib.rs @@ -82,8 +82,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // and set impl_version to equal spec_version. If only runtime // implementation changes and behavior does not, then leave spec_version as // is and increment impl_version. - spec_version: 158, - impl_version: 160, + spec_version: 159, + impl_version: 159, apis: RUNTIME_API_VERSIONS, };