From e26e2ba1d68ad90bbd6700a50268b80894e13531 Mon Sep 17 00:00:00 2001 From: HackFisher Date: Fri, 15 Nov 2019 20:33:18 +0800 Subject: [PATCH] fixing Currency to Vote and new session related --- Cargo.lock | 7 +- node/cli/src/chain_spec.rs | 12 +- node/executor/src/lib.rs | 6 +- node/runtime/src/impls.rs | 17 +- node/runtime/src/lib.rs | 16 +- srml/staking/Cargo.toml | 6 +- srml/staking/src/lib.rs | 153 +++++------ srml/staking/src/mock.rs | 6 +- srml/staking/src/phragmen.rs | 507 ----------------------------------- 9 files changed, 116 insertions(+), 614 deletions(-) delete mode 100644 srml/staking/src/phragmen.rs diff --git a/Cargo.lock b/Cargo.lock index cb4c2f567..b35b55e45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,7 +724,7 @@ dependencies = [ [[package]] name = "darwinia-staking" -version = "0.1.0" +version = "0.2.0" dependencies = [ "darwinia-kton 0.1.0", "darwinia-support 0.1.0", @@ -745,6 +745,7 @@ dependencies = [ "srml-system 2.0.0 (git+https://github.com/darwinia-network/substrate.git)", "srml-timestamp 2.0.0 (git+https://github.com/darwinia-network/substrate.git)", "substrate-keyring 2.0.0 (git+https://github.com/darwinia-network/substrate.git)", + "substrate-phragmen 2.0.0 (git+https://github.com/darwinia-network/substrate.git)", "substrate-primitives 2.0.0 (git+https://github.com/darwinia-network/substrate.git)", ] @@ -2577,7 +2578,7 @@ dependencies = [ name = "node-executor" version = "2.0.0" dependencies = [ - "darwinia-staking 0.1.0", + "darwinia-staking 0.2.0", "node-primitives 2.0.0", "node-runtime 0.1.0", "parity-scale-codec 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2650,7 +2651,7 @@ dependencies = [ "darwinia-eos-bridge 0.1.0", "darwinia-ethereum-bridge 0.1.0", "darwinia-kton 0.1.0", - "darwinia-staking 0.1.0", + "darwinia-staking 0.2.0", "integer-sqrt 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "merkle-mountain-range 0.1.0", "node-primitives 2.0.0", diff --git a/node/cli/src/chain_spec.rs b/node/cli/src/chain_spec.rs index 5b21cd1c1..58bc5b3c0 100644 --- a/node/cli/src/chain_spec.rs +++ b/node/cli/src/chain_spec.rs @@ -250,16 +250,18 @@ pub fn testnet_genesis( staking: Some(StakingConfig { current_era: 0, current_era_total_reward: 80_000_000 * COIN / 63720, - offline_slash: Perbill::from_parts(1_000_000), + // offline_slash: Perbill::from_parts(1_000_000), session_reward: Perbill::from_percent(90), validator_count: 7, - offline_slash_grace: 4, + // offline_slash_grace: 4, minimum_validator_count: 4, stakers: initial_authorities .iter() .map(|x| (x.0.clone(), x.1.clone(), STASH, StakerStatus::Validator)) .collect(), invulnerables: initial_authorities.iter().map(|x| x.1.clone()).collect(), + slash_reward_fraction: Perbill::from_percent(10), + ..Default::default() }), } } @@ -403,16 +405,18 @@ pub fn darwinia_genesis_verbose( staking: Some(StakingConfig { current_era: 0, current_era_total_reward: 80_000_000 * COIN / 63720, - offline_slash: Perbill::from_parts(1_000_000), + // offline_slash: Perbill::from_parts(1_000_000), session_reward: Perbill::from_percent(90), validator_count: 7, - offline_slash_grace: 4, + // offline_slash_grace: 4, minimum_validator_count: 4, stakers: initial_authorities .iter() .map(|x| (x.0.clone(), x.1.clone(), STASH, StakerStatus::Validator)) .collect(), invulnerables: initial_authorities.iter().map(|x| x.1.clone()).collect(), + slash_reward_fraction: Perbill::from_percent(10), + ..Default::default() }), } } diff --git a/node/executor/src/lib.rs b/node/executor/src/lib.rs index 86fbff178..221def451 100644 --- a/node/executor/src/lib.rs +++ b/node/executor/src/lib.rs @@ -388,10 +388,12 @@ mod tests { ], validator_count: 3, minimum_validator_count: 0, - offline_slash: Perbill::zero(), + // offline_slash: Perbill::zero(), session_reward: Perbill::zero(), - offline_slash_grace: 0, + // offline_slash_grace: 0, invulnerables: vec![alice(), bob(), charlie()], + slash_reward_fraction: Perbill::from_percent(10), + ..Default::default() }), timestamp: Some(Default::default()), contracts: Some(Default::default()), diff --git a/node/runtime/src/impls.rs b/node/runtime/src/impls.rs index fb367c294..cc9b2d851 100644 --- a/node/runtime/src/impls.rs +++ b/node/runtime/src/impls.rs @@ -22,7 +22,7 @@ use sr_primitives::traits::{Convert, Saturating}; use sr_primitives::weights::Weight; use sr_primitives::{Fixed64, Perbill}; -use support::traits::{OnUnbalanced, Currency, Get}; +use support::traits::{Currency, Get, OnUnbalanced}; pub struct Author; impl OnUnbalanced for Author { @@ -37,7 +37,8 @@ pub struct CurrencyToVoteHandler; impl CurrencyToVoteHandler { fn factor() -> Balance { - (Balances::total_issuance() / u64::max_value() as Balance).max(1) + //(Balances::total_issuance() / u64::max_value() as Balance).max(1) + 1 } } @@ -53,6 +54,18 @@ impl Convert for CurrencyToVoteHandler { } } +//impl Convert for CurrencyToVoteHandler { +// fn convert(x: u128) -> u64 { +// x as u64 +// } +//} +// +//impl Convert for CurrencyToVoteHandler { +// fn convert(x: u128) -> u128 { +// x +// } +//} + /// Convert from weight to balance via a simple coefficient multiplication /// The associated type C encapsulates a constant in units of balance per weight pub struct LinearWeightToFee(rstd::marker::PhantomData); diff --git a/node/runtime/src/lib.rs b/node/runtime/src/lib.rs index 8ea07a288..7527abc0e 100644 --- a/node/runtime/src/lib.rs +++ b/node/runtime/src/lib.rs @@ -22,9 +22,9 @@ use authority_discovery_primitives::{AuthorityId as EncodedAuthorityId, Signature as EncodedSignature}; use babe_primitives::{AuthorityId as BabeId, AuthoritySignature as BabeSignature}; pub use balances::Call as BalancesCall; -use sr_api::impl_runtime_apis; use codec::{Decode, Encode}; pub use contracts::Gas; +use sr_api::impl_runtime_apis; //use grandpa::fg_primitives; //use grandpa::{AuthorityId as GrandpaId, AuthorityWeight as GrandpaWeight}; @@ -49,24 +49,23 @@ use support::{ }; pub use timestamp::Call as TimestampCall; -use version::RuntimeVersion; #[cfg(any(feature = "std", test))] - use version::NativeVersion; +use version::RuntimeVersion; -use substrate_primitives::OpaqueMetadata; -use grandpa::AuthorityList as GrandpaAuthorityList; use grandpa::fg_primitives; -use im_online::sr25519::{AuthorityId as ImOnlineId}; -use transaction_payment_rpc_runtime_api::RuntimeDispatchInfo; +use grandpa::AuthorityList as GrandpaAuthorityList; +use im_online::sr25519::AuthorityId as ImOnlineId; +use substrate_primitives::OpaqueMetadata; use system::offchain::TransactionSubmitter; +use transaction_payment_rpc_runtime_api::RuntimeDispatchInfo; use staking::EraIndex; pub use staking::StakerStatus; /// Implementations of some helper traits passed into runtime modules as associated types. pub mod impls; -use impls::{CurrencyToVoteHandler, Author, LinearWeightToFee, TargetedFeeAdjustment}; +use impls::{Author, CurrencyToVoteHandler, LinearWeightToFee, TargetedFeeAdjustment}; /// Constant values used within the runtime. pub mod constants; @@ -417,6 +416,7 @@ parameter_types! { impl staking::Trait for Runtime { type Ring = Balances; type Kton = Kton; + type Time = Timestamp; type CurrencyToVote = CurrencyToVoteHandler; type Event = Event; type RingSlash = (); diff --git a/srml/staking/Cargo.toml b/srml/staking/Cargo.toml index 97657bf25..f1c424bcc 100644 --- a/srml/staking/Cargo.toml +++ b/srml/staking/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "darwinia-staking" -version = "0.1.0" -authors = ["hammeWang "] +version = "0.2.0" +authors = ["darwinia "] edition = "2018" [dependencies] @@ -10,6 +10,7 @@ safe-mix = { version = "1.0.0", default-features = false } codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] } substrate-keyring = { git = 'https://github.com/darwinia-network/substrate.git', optional = true } rstd = { package = "sr-std", git = 'https://github.com/darwinia-network/substrate.git', default-features = false } +phragmen = { package = "substrate-phragmen", git = 'https://github.com/darwinia-network/substrate.git', default-features = false } runtime_io = { package = "sr-io", git = 'https://github.com/darwinia-network/substrate.git', default-features = false } sr-primitives = { git = 'https://github.com/darwinia-network/substrate.git', default-features = false } srml-support = { git = 'https://github.com/darwinia-network/substrate.git', default-features = false } @@ -21,7 +22,6 @@ substrate-primitives = { git = 'https://github.com/darwinia-network/substrate.gi sr-arithmetic = { git = 'https://github.com/darwinia-network/substrate.git', default-features = false } sr-staking-primitives = { git = 'https://github.com/darwinia-network/substrate.git', default-features = false } authorship = { package = "srml-authorship", git = 'https://github.com/darwinia-network/substrate.git', default-features = false } -#phragmen = { package = "substrate-phragmen", git = 'https://github.com/darwinia-network/substrate.git', default-features = false } [dev-dependencies] substrate-primitives = { git = 'https://github.com/darwinia-network/substrate.git' } diff --git a/srml/staking/src/lib.rs b/srml/staking/src/lib.rs index 0bb33b9aa..768d7f501 100644 --- a/srml/staking/src/lib.rs +++ b/srml/staking/src/lib.rs @@ -44,7 +44,7 @@ use srml_support::{ }; use system::{ensure_root, ensure_signed}; -use phragmen::{elect, equalize, ExtendedBalance, PhragmenStakedAssignment, Support, SupportMap}; +use phragmen::{build_support_map, elect, equalize, ExtendedBalance, PhragmenStakedAssignment, Support, SupportMap}; mod utils; @@ -55,12 +55,9 @@ mod mock; #[cfg(test)] mod tests; -mod phragmen; - //#[cfg(all(feature = "bench", test))] //mod benches; -const RECENT_OFFLINE_COUNT: usize = 32; const DEFAULT_MINIMUM_VALIDATOR_COUNT: u32 = 4; const MAX_NOMINATIONS: usize = 16; const MAX_UNSTAKE_THRESHOLD: u32 = 10; @@ -298,7 +295,7 @@ pub trait Trait: timestamp::Trait + session::Trait { /// Time used for computing era duration. type Time: Time; - type CurrencyToVote: Convert, u64> + Convert>; + type CurrencyToVote: Convert + Convert; /// The overarching event type. type Event: From> + Into<::Event>; @@ -389,7 +386,7 @@ decl_storage! { /// Rewards for the current era. Using indices of current elected set. CurrentEraPointsEarned get(fn current_era_reward): EraPoints; - pub SlotStake get(slot_stake): ExtendedBalance; + pub SlotStake get(fn slot_stake): ExtendedBalance; /// True if the next session change will be a new era regardless of index. pub ForceEra get(fn force_era) config(): Forcing; @@ -976,7 +973,7 @@ impl Module { // The amount we'll slash from the validator's stash directly. let own_slash = own_remaining.min(slash); - let (mut ring_imbalance, kton_imblance, missing) = + let (mut ring_imbalance, mut kton_imblance, missing) = Self::slash_individual(stash, Perbill::from_rational_approximation(own_slash, exposure.own)); // T::Currency::slash(stash, own_slash); let own_slash = own_slash - missing; // The amount remaining that we can't slash from the validator, @@ -1013,24 +1010,6 @@ impl Module { (ring_imbalance, kton_imblance) } - // fn slash_validator(stash: &T::AccountId, slash_ratio_in_u32: u32) { - // // construct Perbill here to make sure slash_ratio lt 0. - // let slash_ratio = Perbill::from_parts(slash_ratio_in_u32); - // // The exposures (backing stake) information of the validator to be slashed. - // let exposures = Self::stakers(stash); - // - // let (mut ring_imbalance, mut kton_imbalance) = Self::slash_individual(stash, slash_ratio); - // - // for i in exposures.others.iter() { - // let (rn, kn) = Self::slash_individual(&i.who, slash_ratio); - // ring_imbalance.subsume(rn); - // kton_imbalance.subsume(kn); - // } - // - // T::RingSlash::on_unbalanced(ring_imbalance); - // T::KtonSlash::on_unbalanced(kton_imbalance); - // } - // TODO: there is reserve balance in Balance.Slash, we assuming it is zero for now. fn slash_individual( stash: &T::AccountId, @@ -1146,27 +1125,32 @@ impl Module { Vec, Vec<(T::AccountId, Exposure)>, )> { - if ForceNewEra::take() || session_index % T::SessionsPerEra::get() == 0 { - let validators = T::SessionInterface::validators(); - let prior = validators - .into_iter() - .map(|v| { - let e = Self::stakers(&v); - (v, e) - }) - .collect(); - - Self::new_era().map(move |new| (new, prior)) - } else { - None + let era_length = session_index + .checked_sub(Self::current_era_start_session_index()) + .unwrap_or(0); + match ForceEra::get() { + Forcing::ForceNew => ForceEra::kill(), + Forcing::ForceAlways => (), + Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => (), + _ => return None, } + let validators = T::SessionInterface::validators(); + let prior = validators + .into_iter() + .map(|v| { + let e = Self::stakers(&v); + (v, e) + }) + .collect(); + + Self::new_era(session_index).map(move |new| (new, prior)) } /// The era has changed - enact new staking set. /// /// NOTE: This always happens immediately before a session change to ensure that new validators /// get a chance to set their session keys. - fn new_era() -> Option> { + fn new_era(start_session_index: SessionIndex) -> Option> { let reward = Self::session_reward() * Self::current_era_total_reward(); if !reward.is_zero() { let validators = Self::current_elected(); @@ -1259,63 +1243,57 @@ impl Module { /// /// Returns the new `SlotStake` value. fn select_validators() -> (ExtendedBalance, Option>) { - let maybe_elected_set = elect::<_, _>( + let mut all_nominators: Vec<(T::AccountId, Vec)> = Vec::new(); + let all_validator_candidates_iter = >::enumerate(); + let all_validators = all_validator_candidates_iter + .map(|(who, _pref)| { + let self_vote = (who.clone(), vec![who.clone()]); + all_nominators.push(self_vote); + who + }) + .collect::>(); + all_nominators.extend(>::enumerate()); + + let maybe_phragmen_result = elect::<_, _, _, T::CurrencyToVote>( Self::validator_count() as usize, Self::minimum_validator_count().max(1) as usize, - >::enumerate() - .map(|(who, _)| who) - .collect::>(), - >::enumerate().collect(), + all_validators, + all_nominators, Self::power_of, - true, ); - if let Some(elected_set) = maybe_elected_set { - let elected_stashes = elected_set + if let Some(phragmen_result) = maybe_phragmen_result { + let elected_stashes = phragmen_result .winners .iter() .map(|(s, _)| s.clone()) .collect::>(); - let assignments = elected_set.assignments; + let assignments = phragmen_result.assignments; - // The return value of this is safe to be converted to u64. - // Initialize the support of each candidate. - let mut supports = >::new(); - elected_stashes - .iter() - .map(|e| (e, Self::power_of(e))) - .for_each(|(e, s)| { - let item = Support { - own: s, - total: s, - ..Default::default() - }; - supports.insert(e.clone(), item); - }); + let to_votes = |b: ExtendedBalance| { + >::convert(b) as ExtendedBalance + }; + let to_balance = + |e: ExtendedBalance| >::convert(e); + + let mut supports = + build_support_map::<_, _, _, T::CurrencyToVote>(&elected_stashes, &assignments, Self::power_of); - // build support struct. - for (n, assignment) in assignments.iter() { - for (c, per_thing) in assignment.iter() { - let nominator_stake = Self::power_of(n); - // AUDIT: it is crucially important for the `Mul` implementation of all - // per-things to be sound. - let other_stake = *per_thing * nominator_stake; - if let Some(support) = supports.get_mut(c) { - // For an astronomically rich validator with more astronomically rich - // set of nominators, this might saturate. - support.total = support.total.saturating_add(other_stake); - support.others.push((n.clone(), other_stake)); - } - } - } if cfg!(feature = "equalize") { let mut staked_assignments: Vec<(T::AccountId, Vec>)> = Vec::with_capacity(assignments.len()); for (n, assignment) in assignments.iter() { let mut staked_assignment: Vec> = Vec::with_capacity(assignment.len()); + + // If this is a self vote, then we don't need to equalise it at all. While the + // staking system does not allow nomination and validation at the same time, + // this must always be 100% support. + if assignment.len() == 1 && assignment[0].0 == *n { + continue; + } for (c, per_thing) in assignment.iter() { - let nominator_stake = Self::power_of(n); + let nominator_stake = to_votes(Self::power_of(n)); let other_stake = *per_thing * nominator_stake; staked_assignment.push((c.clone(), other_stake)); } @@ -1324,7 +1302,13 @@ impl Module { let tolerance = 0_u128; let iterations = 2_usize; - equalize::<_, _>(staked_assignments, &mut supports, tolerance, iterations, Self::power_of); + equalize::<_, _, T::CurrencyToVote, _>( + staked_assignments, + &mut supports, + tolerance, + iterations, + Self::power_of, + ); } // Clear Stakers. @@ -1337,16 +1321,19 @@ impl Module { for (c, s) in supports.into_iter() { // build `struct exposure` from `support` let exposure = Exposure { - own: s.own, + own: to_balance(s.own), // This might reasonably saturate and we cannot do much about it. The sum of // someone's stake might exceed the balance type if they have the maximum amount // of balance and receive some support. This is super unlikely to happen, yet // we simulate it in some tests. - total: s.total, + total: to_balance(s.total), others: s .others .into_iter() - .map(|(who, value)| IndividualExposure { who, value: value }) + .map(|(who, value)| IndividualExposure { + who, + value: to_balance(value), + }) .collect::>>(), }; if exposure.total < slot_stake { @@ -1356,7 +1343,7 @@ impl Module { } // Update slot stake. - SlotStake::put(&slot_stake); + ::put(&slot_stake); // Set the new validator set in sessions. >::put(&elected_stashes); diff --git a/srml/staking/src/mock.rs b/srml/staking/src/mock.rs index 72c84d169..23924e958 100644 --- a/srml/staking/src/mock.rs +++ b/srml/staking/src/mock.rs @@ -317,9 +317,11 @@ impl ExtBuilder { validator_count: self.validator_count, minimum_validator_count: self.minimum_validator_count, session_reward: Perbill::from_rational_approximation(1_000_000 * self.reward / balance_factor, 1_000_000), - offline_slash: Perbill::from_percent(5), - offline_slash_grace: 0, + // offline_slash: Perbill::from_percent(5), + // offline_slash_grace: 0, invulnerables: vec![], + slash_reward_fraction: Perbill::from_percent(10), + ..Default::default() } .assimilate_storage(&mut storage); diff --git a/srml/staking/src/phragmen.rs b/srml/staking/src/phragmen.rs deleted file mode 100644 index 5ec9dcccf..000000000 --- a/srml/staking/src/phragmen.rs +++ /dev/null @@ -1,507 +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. This is used in several SRML modules to -//! optimally distribute the weight of a set of voters among an elected set of candidates. In the -//! context of staking this is mapped to validators and nominators. -//! -//! The algorithm has two phases: -//! - Sequential phragmen: performed in [`elect`] function which is first pass of the distribution -//! The results are not optimal but the execution time is less. -//! - Equalize post-processing: tries to further distribute the weight fairly among candidates. -//! Incurs more execution time. -//! -//! The main objective of the assignments done by phragmen is to maximize the minimum backed -//! candidate in the elected set. -//! -//! Reference implementation: https://github.com/w3f/consensus -//! Further details: -//! https://research.web3.foundation/en/latest/polkadot/NPoS/4.%20Sequential%20Phragm%C3%A9n%E2%80%99s%20method/ - -#![cfg_attr(not(feature = "std"), no_std)] - -use rstd::{collections::btree_map::BTreeMap, prelude::*}; -use sr_primitives::traits::{Bounded, Member, Saturating, Zero}; -use sr_primitives::{helpers_128bit::multiply_by_rational, Perbill, Rational128}; - -/// A type in which performing operations on balances and stakes of candidates and voters are safe. -/// -/// This module's functions expect a `Convert` type to convert all balances to u64. Hence, u128 is -/// a safe type for arithmetic operations over them. -/// -/// Balance types converted to `ExtendedBalance` are referred to as `Votes`. -pub type ExtendedBalance = u128; - -/// The denominator used for loads. Since votes are collected as u64, the smallest ratio that we -/// might collect is `1/approval_stake` where approval stake is the sum of votes. Hence, some number -/// bigger than u64::max_value() is needed. For maximum accuracy we simply use u128; -const DEN: u128 = u128::max_value(); - -/// A candidate entity for phragmen election. -#[derive(Clone, Default)] -#[cfg_attr(feature = "std", derive(Debug))] -pub struct Candidate { - /// Identifier. - pub who: AccountId, - /// Intermediary value used to sort candidates. - pub score: Rational128, - /// Sum of the stake of this candidate based on received votes. - approval_stake: ExtendedBalance, - /// Flag for being elected. - elected: bool, -} - -/// A voter entity. -#[derive(Clone, Default)] -#[cfg_attr(feature = "std", derive(Debug))] -pub struct Voter { - /// Identifier. - who: AccountId, - /// List of candidates proposed by this voter. - edges: Vec>, - /// The stake of this voter. - budget: ExtendedBalance, - /// Incremented each time a candidate that this voter voted for has been elected. - load: Rational128, -} - -/// A candidate being backed by a voter. -#[derive(Clone, Default)] -#[cfg_attr(feature = "std", derive(Debug))] -pub struct Edge { - /// Identifier. - who: AccountId, - /// Load of this vote. - load: Rational128, - /// Index of the candidate stored in the 'candidates' vector. - candidate_index: usize, -} - -/// Means a particular `AccountId` was backed by `Perbill`th of a nominator's stake. -pub type PhragmenAssignment = (AccountId, Perbill); - -/// Means a particular `AccountId` was backed by `ExtendedBalance` of a nominator's stake. -pub type PhragmenStakedAssignment = (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. - pub winners: Vec<(AccountId, ExtendedBalance)>, - /// Individual assignments. for each tuple, the first elements is a voter and the second - /// is the list of candidates that it supports. - pub assignments: Vec<(AccountId, Vec>)>, -} - -/// A structure to demonstrate the phragmen result from the perspective of the candidate, i.e. how -/// much support each candidate is receiving. -/// -/// This complements the [`PhragmenResult`] and is needed to run the equalize post-processing. -/// -/// This, at the current version, resembles the `Exposure` defined in the staking SRML module, yet -/// they do not necessarily have to be the same. -#[derive(Default)] -#[cfg_attr(feature = "std", derive(Debug))] -pub struct Support { - /// The amount of support as the effect of self-vote. - pub own: ExtendedBalance, - /// Total support. - pub total: ExtendedBalance, - /// Support from voters. - pub others: Vec>, -} - -/// A linkage from a candidate and its [`Support`]. -pub type SupportMap = BTreeMap>; - -/// Perform election based on Phragmén algorithm. -/// -/// Returns an `Option` the set of winners and their detailed support ratio from each voter if -/// enough candidates are provided. Returns `None` otherwise. -/// -/// * `candidate_count`: number of candidates to elect. -/// * `minimum_candidate_count`: minimum number of candidates to elect. If less candidates exist, -/// `None` is returned. -/// * `initial_candidates`: candidates list to be elected from. -/// * `initial_voters`: voters list. -/// * `stake_of`: something that can return the stake stake of a particular candidate or voter. -/// * `self_vote`. If true, then each candidate will automatically vote for themselves with the a -/// weight indicated by their stake. Note that when this is `true` candidates are filtered by -/// having at least some backed stake from themselves. -pub fn elect( - candidate_count: usize, - minimum_candidate_count: usize, - initial_candidates: Vec, - initial_voters: Vec<(AccountId, Vec)>, - stake_of: FS, - self_vote: bool, -) -> Option> -where - AccountId: Default + Ord + Member, - for<'r> FS: Fn(&'r AccountId) -> ExtendedBalance, -{ - // return structures - let mut elected_candidates: Vec<(AccountId, ExtendedBalance)>; - let mut assigned: Vec<(AccountId, Vec>)>; - - // used to cache and access candidates index. - let mut c_idx_cache = BTreeMap::::new(); - - // voters list. - let num_voters = initial_candidates.len() + initial_voters.len(); - let mut voters: Vec> = Vec::with_capacity(num_voters); - - // collect candidates. self vote or filter might apply - let mut candidates = if self_vote { - // self vote. filter. - initial_candidates - .into_iter() - .map(|who| { - let stake = stake_of(&who); - Candidate { - who, - approval_stake: stake, - ..Default::default() - } - }) - .filter(|c| !c.approval_stake.is_zero()) - .enumerate() - .map(|(i, c)| { - voters.push(Voter { - who: c.who.clone(), - edges: vec![Edge { - who: c.who.clone(), - candidate_index: i, - ..Default::default() - }], - budget: c.approval_stake, - load: Rational128::zero(), - }); - c_idx_cache.insert(c.who.clone(), i); - c - }) - .collect::>>() - } else { - // no self vote. just collect. - initial_candidates - .into_iter() - .enumerate() - .map(|(idx, who)| { - c_idx_cache.insert(who.clone(), idx); - Candidate { - who, - ..Default::default() - } - }) - .collect::>>() - }; - - // early return if we don't have enough candidates - if candidates.len() < minimum_candidate_count { - return None; - } - - // collect voters. use `c_idx_cache` for fast access and aggregate `approval_stake` of - // candidates. - voters.extend(initial_voters.into_iter().map(|(who, votes)| { - let voter_stake = stake_of(&who); - let mut edges: Vec> = Vec::with_capacity(votes.len()); - for v in votes { - if let Some(idx) = c_idx_cache.get(&v) { - // This candidate is valid + already cached. - candidates[*idx].approval_stake = candidates[*idx].approval_stake.saturating_add(voter_stake); - edges.push(Edge { - who: v.clone(), - candidate_index: *idx, - ..Default::default() - }); - } // else {} would be wrong votes. We don't really care about it. - } - Voter { - who, - edges: edges, - budget: voter_stake, - load: Rational128::zero(), - } - })); - - // we have already checked that we have more candidates than minimum_candidate_count. - // run phragmen. - let to_elect = candidate_count.min(candidates.len()); - elected_candidates = Vec::with_capacity(candidate_count); - assigned = Vec::with_capacity(candidate_count); - - // main election loop - for _round in 0..to_elect { - // loop 1: initialize score - for c in &mut candidates { - if !c.elected { - // 1 / approval_stake == (DEN / approval_stake) / DEN. If approval_stake is zero, - // then the ratio should be as large as possible, essentially `infinity`. - if c.approval_stake.is_zero() { - c.score = Rational128::from_unchecked(DEN, 0); - } else { - c.score = Rational128::from(DEN / c.approval_stake, DEN); - } - } - } - - // loop 2: increment score - for n in &voters { - for e in &n.edges { - let c = &mut candidates[e.candidate_index]; - if !c.elected && !c.approval_stake.is_zero() { - let temp_n = - multiply_by_rational(n.load.n(), n.budget, c.approval_stake).unwrap_or(Bounded::max_value()); - let temp_d = n.load.d(); - let temp = Rational128::from(temp_n, temp_d); - c.score = c.score.lazy_saturating_add(temp); - } - } - } - - // loop 3: find the best - if let Some(winner) = candidates.iter_mut().filter(|c| !c.elected).min_by_key(|c| c.score) { - // loop 3: update voter and edge load - winner.elected = true; - for n in &mut voters { - for e in &mut n.edges { - if e.who == winner.who { - e.load = winner.score.lazy_saturating_sub(n.load); - n.load = winner.score; - } - } - } - - elected_candidates.push((winner.who.clone(), winner.approval_stake)); - } else { - break; - } - } // end of all rounds - - // update backing stake of candidates and voters - 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 c.0 != n.who { - let per_bill_parts = { - if n.load == e.load { - // Full support. No need to calculate. - Perbill::accuracy().into() - } else { - if e.load.d() == n.load.d() { - // return e.load / n.load. - let desired_scale: u128 = Perbill::accuracy().into(); - multiply_by_rational(desired_scale, e.load.n(), n.load.n()) - .unwrap_or(Bounded::max_value()) - } else { - // defensive only. Both edge and nominator loads are built from - // scores, hence MUST have the same denominator. - Zero::zero() - } - } - }; - // safer to .min() inside as well to argue as u32 is safe. - let per_thing = Perbill::from_parts(per_bill_parts.min(Perbill::accuracy().into()) as u32); - assignment.1.push((e.who.clone(), per_thing)); - } - } - } - - 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 u32; - let len = assignment.1.len(); - let sum = assignment.1.iter().map(|a| a.1.deconstruct()).sum::(); - let accuracy = Perbill::accuracy(); - let diff = accuracy.checked_sub(sum).unwrap_or(0); - let diff_per_vote = (diff / vote_count).min(accuracy); - - if diff_per_vote > 0 { - for i in 0..len { - let current_ratio = assignment.1[i % len].1; - let next_ratio = current_ratio.saturating_add(Perbill::from_parts(diff_per_vote)); - assignment.1[i % len].1 = next_ratio; - } - } - - // `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 { - let current_ratio = assignment.1[i % len].1; - let next_ratio = current_ratio.saturating_add(Perbill::from_parts(1)); - assignment.1[i % len].1 = next_ratio; - } - assigned.push(assignment); - } - } - - Some(PhragmenResult { - winners: elected_candidates, - assignments: assigned, - }) -} - -/// Performs equalize post-processing to the output of the election algorithm. This happens in -/// rounds. The number of rounds and the maximum diff-per-round tolerance can be tuned through input -/// parameters. -/// -/// No value is returned from the function and the `supports` parameter is updated. -/// -/// * `assignments`: exactly the same is the output of phragmen. -/// * `supports`: mutable reference to s `SupportMap`. This parameter is updated. -/// * `tolerance`: maximum difference that can occur before an early quite happens. -/// * `iterations`: maximum number of iterations that will be processed. -/// * `stake_of`: something that can return the stake stake of a particular candidate or voter. -pub fn equalize( - mut assignments: Vec<(AccountId, Vec>)>, - supports: &mut SupportMap, - tolerance: ExtendedBalance, - iterations: usize, - stake_of: FS, -) where - for<'r> FS: Fn(&'r AccountId) -> ExtendedBalance, - AccountId: Ord + Clone, -{ - // prepare the data for equalise - for _i in 0..iterations { - let mut max_diff = 0; - - for (voter, assignment) in assignments.iter_mut() { - let voter_budget = stake_of(&voter); - - let diff = do_equalize::<_>(voter, voter_budget, assignment, supports, tolerance); - if diff > max_diff { - max_diff = diff; - } - } - - if max_diff < tolerance { - break; - } - } -} - -/// actually perform equalize. same interface is `equalize`. Just called in loops with a check for -/// maximum difference. -fn do_equalize( - voter: &AccountId, - budget_balance: ExtendedBalance, - elected_edges: &mut Vec>, - support_map: &mut SupportMap, - tolerance: ExtendedBalance, -) -> ExtendedBalance -where - AccountId: Ord + Clone, -{ - let budget = budget_balance; - - // Nothing to do. This voter 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.1)); - - let backed_stakes_iter = elected_edges - .iter() - .filter_map(|e| support_map.get(&e.0)) - .map(|e| e.total); - - let backing_backed_stake = elected_edges - .iter() - .filter(|e| e.1 > 0) - .filter_map(|e| support_map.get(&e.0)) - .map(|e| e.total) - .collect::>(); - - let mut difference: u128; - 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 support - elected_edges.iter_mut().for_each(|e| { - if let Some(support) = support_map.get_mut(&e.0) { - support.total = support.total.saturating_sub(e.1); - support.others.retain(|i_support| i_support.0 != *voter); - } - e.1 = 0; - }); - - elected_edges.sort_unstable_by_key(|e| { - if let Some(e) = support_map.get(&e.0) { - e.total - } else { - Zero::zero() - } - }); - - let mut cumulative_stake: ExtendedBalance = 0; - let mut last_index = elected_edges.len() - 1; - let mut idx = 0usize; - for e in &mut elected_edges[..] { - if let Some(support) = support_map.get_mut(&e.0) { - let stake = support.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); - break; - } - cumulative_stake = cumulative_stake.saturating_add(stake); - } - idx += 1; - } - - let last_stake = elected_edges[last_index].1; - 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(support) = support_map.get_mut(&e.0) { - e.1 = (excess / split_ways as ExtendedBalance) - .saturating_add(last_stake) - .saturating_sub(support.total); - support.total = support.total.saturating_add(e.1); - support.others.push((voter.clone(), e.1)); - } - }); - - difference -}