mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 19:51:05 +00:00
Decouple Phragmen from Staking. (#3498)
* Move phragmen to primitives * Improved docs * New crate. * Update lock. * Fix dependency. * Fix build. * Add basic testing and truth-value implementation with float types * Update srml/staking/src/lib.rs * Nits. * Bump. * Fix benchmarks.
This commit is contained in:
@@ -27,6 +27,7 @@ use test::Bencher;
|
||||
use runtime_io::with_externalities;
|
||||
use mock::*;
|
||||
use super::*;
|
||||
use phragmen;
|
||||
use rand::{self, Rng};
|
||||
|
||||
const VALIDATORS: u64 = 1000;
|
||||
@@ -35,6 +36,8 @@ const EDGES: u64 = 2;
|
||||
const TO_ELECT: usize = 100;
|
||||
const STAKE: u64 = 1000;
|
||||
|
||||
type C<T> = <T as Trait>::CurrencyToVote;
|
||||
|
||||
fn do_phragmen(
|
||||
b: &mut Bencher,
|
||||
num_vals: u64,
|
||||
@@ -42,7 +45,7 @@ fn do_phragmen(
|
||||
count: usize,
|
||||
votes_per: u64,
|
||||
eq_iters: usize,
|
||||
eq_tolerance: u128,
|
||||
_eq_tolerance: u128,
|
||||
) {
|
||||
with_externalities(&mut ExtBuilder::default().nominate(false).build(), || {
|
||||
assert!(num_vals > votes_per);
|
||||
@@ -71,67 +74,55 @@ fn do_phragmen(
|
||||
});
|
||||
|
||||
b.iter(|| {
|
||||
let r = phragmen::elect::<Test, _, _, _>(
|
||||
let r = phragmen::elect::<_, _, _, <Test as Trait>::CurrencyToVote>(
|
||||
count,
|
||||
1_usize,
|
||||
<Validators<Test>>::enumerate(),
|
||||
<Nominators<Test>>::enumerate(),
|
||||
Staking::slashable_balance_of
|
||||
<Validators<Test>>::enumerate().map(|(who, _)| who).collect::<Vec<u64>>(),
|
||||
<Nominators<Test>>::enumerate().collect(),
|
||||
Staking::slashable_balance_of,
|
||||
true,
|
||||
).unwrap();
|
||||
|
||||
// Do the benchmarking with equalize.
|
||||
if eq_iters > 0 {
|
||||
let elected_stashes = r.0;
|
||||
let assignments = r.1;
|
||||
let elected_stashes = r.winners;
|
||||
let mut assignments = r.assignments;
|
||||
|
||||
let to_balance = |b: ExtendedBalance|
|
||||
<<mock::Test as Trait>::CurrencyToVote as Convert<ExtendedBalance, Balance>>::convert(b);
|
||||
let to_votes = |b: Balance|
|
||||
<<mock::Test as Trait>::CurrencyToVote as Convert<Balance, u64>>::convert(b) as ExtendedBalance;
|
||||
let ratio_of = |b, p| (p as ExtendedBalance).saturating_mul(to_votes(b)) / ACCURACY;
|
||||
<C<Test> as Convert<Balance, AccountId>>::convert(b) as ExtendedBalance;
|
||||
let ratio_of = |b, r: ExtendedBalance| r.saturating_mul(to_votes(b)) / ACCURACY;
|
||||
|
||||
let assignments_with_stakes = assignments.into_iter().map(|(n, a)|(
|
||||
n,
|
||||
Staking::slashable_balance_of(&n),
|
||||
a.into_iter().map(|(acc, r)| (
|
||||
acc.clone(),
|
||||
r,
|
||||
to_balance(ratio_of(Staking::slashable_balance_of(&n), r)),
|
||||
))
|
||||
.collect::<Vec<Assignment<Test>>>()
|
||||
)).collect::<Vec<(AccountId, Balance, Vec<Assignment<Test>>)>>();
|
||||
|
||||
let mut exposures = <ExpoMap<Test>>::new();
|
||||
// Initialize the support of each candidate.
|
||||
let mut supports = <SupportMap<u64>>::new();
|
||||
elected_stashes
|
||||
.into_iter()
|
||||
.map(|e| (e, Staking::slashable_balance_of(&e)))
|
||||
.iter()
|
||||
.map(|e| (e, to_votes(Staking::slashable_balance_of(e))))
|
||||
.for_each(|(e, s)| {
|
||||
let item = Exposure { own: s, total: s, ..Default::default() };
|
||||
exposures.insert(e, item);
|
||||
let item = Support { own: s, total: s, ..Default::default() };
|
||||
supports.insert(e.clone(), item);
|
||||
});
|
||||
|
||||
for (n, _, assignment) in &assignments_with_stakes {
|
||||
for (c, _, s) in assignment {
|
||||
if let Some(expo) = exposures.get_mut(c) {
|
||||
expo.total = expo.total.saturating_add(*s);
|
||||
expo.others.push( IndividualExposure { who: n.clone(), value: *s } );
|
||||
for (n, assignment) in assignments.iter_mut() {
|
||||
for (c, r) in assignment.iter_mut() {
|
||||
let nominator_stake = Staking::slashable_balance_of(n);
|
||||
let other_stake = ratio_of(nominator_stake, *r);
|
||||
if let Some(support) = supports.get_mut(c) {
|
||||
support.total = support.total.saturating_add(other_stake);
|
||||
support.others.push((n.clone(), other_stake));
|
||||
}
|
||||
*r = other_stake;
|
||||
}
|
||||
}
|
||||
|
||||
let mut assignments_with_votes = assignments_with_stakes.into_iter()
|
||||
.map(|a| (
|
||||
a.0, a.1,
|
||||
a.2.into_iter()
|
||||
.map(|e| (e.0, e.1, to_votes(e.2)))
|
||||
.collect::<Vec<(AccountId, ExtendedBalance, ExtendedBalance)>>()
|
||||
))
|
||||
.collect::<Vec<(
|
||||
AccountId,
|
||||
Balance,
|
||||
Vec<(AccountId, ExtendedBalance, ExtendedBalance)>
|
||||
)>>();
|
||||
equalize::<Test>(&mut assignments_with_votes, &mut exposures, eq_tolerance, eq_iters);
|
||||
let tolerance = 0_u128;
|
||||
let iterations = 2_usize;
|
||||
phragmen::equalize::<_, _, <Test as Trait>::CurrencyToVote, _>(
|
||||
assignments,
|
||||
&mut supports,
|
||||
tolerance,
|
||||
iterations,
|
||||
Staking::slashable_balance_of,
|
||||
);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -254,7 +254,6 @@ mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
mod phragmen;
|
||||
pub mod inflation;
|
||||
|
||||
#[cfg(all(feature = "bench", test))]
|
||||
@@ -262,7 +261,7 @@ mod benches;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use runtime_io::with_storage;
|
||||
use rstd::{prelude::*, result, collections::btree_map::BTreeMap};
|
||||
use rstd::{prelude::*, result};
|
||||
use codec::{HasCompact, Encode, Decode};
|
||||
use srml_support::{
|
||||
StorageValue, StorageMap, EnumerableStorageMap, decl_module, decl_event,
|
||||
@@ -275,9 +274,10 @@ use session::{historical::OnSessionEnding, SelectInitialValidators};
|
||||
use sr_primitives::Perbill;
|
||||
use sr_primitives::weights::SimpleDispatchInfo;
|
||||
use sr_primitives::traits::{
|
||||
Convert, Zero, One, StaticLookup, CheckedSub, Saturating, Bounded,
|
||||
SimpleArithmetic, SaturatedConversion,
|
||||
Convert, Zero, One, StaticLookup, CheckedSub, Saturating, Bounded, SimpleArithmetic,
|
||||
SaturatedConversion,
|
||||
};
|
||||
use phragmen::{elect, equalize, Support, SupportMap, ExtendedBalance, ACCURACY};
|
||||
use sr_staking_primitives::{
|
||||
SessionIndex, CurrentElectedSet,
|
||||
offence::{OnOffenceHandler, OffenceDetails, Offence, ReportOffence},
|
||||
@@ -286,8 +286,6 @@ use sr_staking_primitives::{
|
||||
use sr_primitives::{Serialize, Deserialize};
|
||||
use system::{ensure_signed, ensure_root};
|
||||
|
||||
use phragmen::{elect, ACCURACY, ExtendedBalance, equalize};
|
||||
|
||||
const DEFAULT_MINIMUM_VALIDATOR_COUNT: u32 = 4;
|
||||
const MAX_NOMINATIONS: usize = 16;
|
||||
const MAX_UNLOCKING_CHUNKS: usize = 32;
|
||||
@@ -458,13 +456,6 @@ type NegativeImbalanceOf<T> =
|
||||
<<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::NegativeImbalance;
|
||||
type MomentOf<T>= <<T as Trait>::Time as Time>::Moment;
|
||||
|
||||
type RawAssignment<T> = (<T as system::Trait>::AccountId, ExtendedBalance);
|
||||
type Assignment<T> = (<T as system::Trait>::AccountId, ExtendedBalance, BalanceOf<T>);
|
||||
type ExpoMap<T> = BTreeMap<
|
||||
<T as system::Trait>::AccountId,
|
||||
Exposure<<T as system::Trait>::AccountId, BalanceOf<T>>
|
||||
>;
|
||||
|
||||
/// Means for interacting with a specialized version of the `session` trait.
|
||||
///
|
||||
/// This is needed because `Staking` sets the `ValidatorIdOf` of the `session::Trait`
|
||||
@@ -512,7 +503,7 @@ pub trait Trait: system::Trait {
|
||||
/// This must fit into a `u64` but is allowed to be sensibly lossy.
|
||||
/// TODO: #1377
|
||||
/// The backward convert should be removed as the new Phragmen API returns ratio.
|
||||
/// The post-processing needs it but will be moved to off-chain.
|
||||
/// The post-processing needs it but will be moved to off-chain. TODO: #2908
|
||||
type CurrencyToVote: Convert<BalanceOf<Self>, u64> + Convert<u128, BalanceOf<Self>>;
|
||||
|
||||
/// Some tokens minted.
|
||||
@@ -1254,17 +1245,18 @@ impl<T: Trait> Module<T> {
|
||||
///
|
||||
/// Returns the new `SlotStake` value and a set of newly selected _stash_ IDs.
|
||||
fn select_validators() -> (BalanceOf<T>, Option<Vec<T::AccountId>>) {
|
||||
let maybe_elected_set = elect::<T, _, _, _>(
|
||||
let maybe_phragmen_result = elect::<_, _, _, T::CurrencyToVote>(
|
||||
Self::validator_count() as usize,
|
||||
Self::minimum_validator_count().max(1) as usize,
|
||||
<Validators<T>>::enumerate(),
|
||||
<Nominators<T>>::enumerate(),
|
||||
<Validators<T>>::enumerate().map(|(who, _)| who).collect::<Vec<T::AccountId>>(),
|
||||
<Nominators<T>>::enumerate().collect(),
|
||||
Self::slashable_balance_of,
|
||||
true,
|
||||
);
|
||||
|
||||
if let Some(elected_set) = maybe_elected_set {
|
||||
let elected_stashes = elected_set.0;
|
||||
let assignments = elected_set.1;
|
||||
if let Some(phragmen_result) = maybe_phragmen_result {
|
||||
let elected_stashes = phragmen_result.winners;
|
||||
let mut assignments = phragmen_result.assignments;
|
||||
|
||||
// helper closure.
|
||||
let to_balance = |b: ExtendedBalance|
|
||||
@@ -1277,59 +1269,45 @@ impl<T: Trait> Module<T> {
|
||||
// to be properly multiplied by a ratio, which will lead to another value
|
||||
// less than u64 for sure. The result can then be safely passed to `to_balance`.
|
||||
// For now the backward convert is used. A simple `TryFrom<u64>` is also safe.
|
||||
let ratio_of = |b, p| (p as ExtendedBalance).saturating_mul(to_votes(b)) / ACCURACY;
|
||||
let ratio_of = |b, r: ExtendedBalance| r.saturating_mul(to_votes(b)) / ACCURACY;
|
||||
|
||||
// Compute the actual stake from nominator's ratio.
|
||||
let assignments_with_stakes = assignments.iter().map(|(n, a)|(
|
||||
n.clone(),
|
||||
Self::slashable_balance_of(n),
|
||||
a.iter().map(|(acc, r)| (
|
||||
acc.clone(),
|
||||
*r,
|
||||
to_balance(ratio_of(Self::slashable_balance_of(n), *r)),
|
||||
))
|
||||
.collect::<Vec<Assignment<T>>>()
|
||||
)).collect::<Vec<(T::AccountId, BalanceOf<T>, Vec<Assignment<T>>)>>();
|
||||
|
||||
// update elected candidate exposures.
|
||||
let mut exposures = <ExpoMap<T>>::new();
|
||||
// Initialize the support of each candidate.
|
||||
let mut supports = <SupportMap<T::AccountId>>::new();
|
||||
elected_stashes
|
||||
.iter()
|
||||
.map(|e| (e, Self::slashable_balance_of(e)))
|
||||
.map(|e| (e, to_votes(Self::slashable_balance_of(e))))
|
||||
.for_each(|(e, s)| {
|
||||
let item = Exposure { own: s, total: s, ..Default::default() };
|
||||
exposures.insert(e.clone(), item);
|
||||
let item = Support { own: s, total: s, ..Default::default() };
|
||||
supports.insert(e.clone(), item);
|
||||
});
|
||||
|
||||
for (n, _, assignment) in &assignments_with_stakes {
|
||||
for (c, _, s) in assignment {
|
||||
if let Some(expo) = exposures.get_mut(c) {
|
||||
// NOTE: simple example where this saturates:
|
||||
// candidate with max_value stake. 1 nominator with max_value stake.
|
||||
// Nuked. Sadly there is not much that we can do about this.
|
||||
// See this test: phragmen_should_not_overflow_xxx()
|
||||
expo.total = expo.total.saturating_add(*s);
|
||||
expo.others.push( IndividualExposure { who: n.clone(), value: *s } );
|
||||
// convert the ratio in-place (and replace) to the balance but still in the extended
|
||||
// balance type.
|
||||
for (n, assignment) in assignments.iter_mut() {
|
||||
for (c, r) in assignment.iter_mut() {
|
||||
let nominator_stake = Self::slashable_balance_of(n);
|
||||
let other_stake = ratio_of(nominator_stake, *r);
|
||||
if let Some(support) = supports.get_mut(c) {
|
||||
// This 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));
|
||||
}
|
||||
// convert the ratio to extended balance
|
||||
*r = other_stake;
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(feature = "equalize") {
|
||||
let tolerance = 0_u128;
|
||||
let iterations = 2_usize;
|
||||
let mut assignments_with_votes = assignments_with_stakes.iter()
|
||||
.map(|a| (
|
||||
a.0.clone(), a.1,
|
||||
a.2.iter()
|
||||
.map(|e| (e.0.clone(), e.1, to_votes(e.2)))
|
||||
.collect::<Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)>>()
|
||||
))
|
||||
.collect::<Vec<(
|
||||
T::AccountId,
|
||||
BalanceOf<T>,
|
||||
Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)>
|
||||
)>>();
|
||||
equalize::<T>(&mut assignments_with_votes, &mut exposures, tolerance, iterations);
|
||||
equalize::<_, _, T::CurrencyToVote, _>(
|
||||
assignments,
|
||||
&mut supports,
|
||||
tolerance,
|
||||
iterations,
|
||||
Self::slashable_balance_of,
|
||||
);
|
||||
}
|
||||
|
||||
// Clear Stakers.
|
||||
@@ -1339,11 +1317,24 @@ impl<T: Trait> Module<T> {
|
||||
|
||||
// Populate Stakers and figure out the minimum stake behind a slot.
|
||||
let mut slot_stake = BalanceOf::<T>::max_value();
|
||||
for (c, e) in exposures.iter() {
|
||||
if e.total < slot_stake {
|
||||
slot_stake = e.total;
|
||||
for (c, s) in supports.into_iter() {
|
||||
// build `struct exposure` from `support`
|
||||
let exposure = Exposure {
|
||||
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: to_balance(s.total),
|
||||
others: s.others
|
||||
.into_iter()
|
||||
.map(|(who, value)| IndividualExposure { who, value: to_balance(value) })
|
||||
.collect::<Vec<IndividualExposure<_, _>>>(),
|
||||
};
|
||||
if exposure.total < slot_stake {
|
||||
slot_stake = exposure.total;
|
||||
}
|
||||
<Stakers<T>>::insert(c.clone(), e.clone());
|
||||
<Stakers<T>>::insert(c.clone(), exposure.clone());
|
||||
}
|
||||
|
||||
// Update slot stake.
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
use std::{collections::HashSet, cell::RefCell};
|
||||
use sr_primitives::Perbill;
|
||||
use sr_primitives::traits::{IdentityLookup, Convert, OpaqueKeys, OnInitialize};
|
||||
use sr_primitives::traits::{IdentityLookup, Convert, OpaqueKeys, OnInitialize, SaturatedConversion};
|
||||
use sr_primitives::testing::{Header, UintAuthorityId};
|
||||
use sr_staking_primitives::SessionIndex;
|
||||
use primitives::{H256, Blake2Hasher};
|
||||
@@ -41,9 +41,7 @@ impl Convert<u64, u64> for CurrencyToVoteHandler {
|
||||
fn convert(x: u64) -> u64 { x }
|
||||
}
|
||||
impl Convert<u128, u64> for CurrencyToVoteHandler {
|
||||
fn convert(x: u128) -> u64 {
|
||||
x as u64
|
||||
}
|
||||
fn convert(x: u128) -> u64 { x.saturated_into() }
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! 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<AccountId> {
|
||||
/// 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<AccountId> {
|
||||
/// The nominator's account.
|
||||
who: AccountId,
|
||||
/// List of validators proposed by this nominator.
|
||||
edges: Vec<Edge<AccountId>>,
|
||||
/// 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<AccountId> {
|
||||
/// 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<T: Trait + 'static, FV, FN, FS>(
|
||||
validator_count: usize,
|
||||
minimum_validator_count: usize,
|
||||
validator_iter: FV,
|
||||
nominator_iter: FN,
|
||||
slashable_balance_of: FS,
|
||||
) -> Option<(Vec<T::AccountId>, Vec<(T::AccountId, Vec<RawAssignment<T>>)>)> where
|
||||
FV: Iterator<Item=(T::AccountId, ValidatorPrefs<BalanceOf<T>>)>,
|
||||
FN: Iterator<Item=(T::AccountId, Vec<T::AccountId>)>,
|
||||
for <'r> FS: Fn(&'r T::AccountId) -> BalanceOf<T>,
|
||||
{
|
||||
let to_votes = |b: BalanceOf<T>| <T::CurrencyToVote as Convert<BalanceOf<T>, u64>>::convert(b) as ExtendedBalance;
|
||||
|
||||
// return structures
|
||||
let mut elected_candidates: Vec<T::AccountId>;
|
||||
let mut assigned: Vec<(T::AccountId, Vec<RawAssignment<T>>)>;
|
||||
let mut c_idx_cache = BTreeMap::<T::AccountId, usize>::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<Nominator<T::AccountId>> =
|
||||
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::<Vec<Candidate<T::AccountId>>>();
|
||||
|
||||
// 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<Edge<T::AccountId>> = 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::<ExtendedBalance>();
|
||||
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<T: Trait + 'static>(
|
||||
assignments: &mut Vec<(T::AccountId, BalanceOf<T>, Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)>)>,
|
||||
expo_map: &mut ExpoMap<T>,
|
||||
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::<T>(&n, *budget, assignment, expo_map, tolerance);
|
||||
if diff > max_diff {
|
||||
max_diff = diff;
|
||||
}
|
||||
});
|
||||
if max_diff < tolerance {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn do_equalize<T: Trait + 'static>(
|
||||
nominator: &T::AccountId,
|
||||
budget_balance: BalanceOf<T>,
|
||||
elected_edges: &mut Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)>,
|
||||
expo_map: &mut ExpoMap<T>,
|
||||
tolerance: ExtendedBalance
|
||||
) -> ExtendedBalance {
|
||||
let to_votes = |b: BalanceOf<T>|
|
||||
<T::CurrencyToVote as Convert<BalanceOf<T>, u64>>::convert(b) as ExtendedBalance;
|
||||
let to_balance = |v: ExtendedBalance|
|
||||
<T::CurrencyToVote as Convert<ExtendedBalance, BalanceOf<T>>>::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::<Vec<ExtendedBalance>>();
|
||||
|
||||
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
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
use super::*;
|
||||
use runtime_io::with_externalities;
|
||||
use phragmen;
|
||||
use sr_primitives::traits::OnInitialize;
|
||||
use sr_staking_primitives::offence::{OffenceDetails, OnOffenceHandler};
|
||||
use srml_support::{assert_ok, assert_noop, assert_eq_uvec, EnumerableStorageMap};
|
||||
@@ -1429,19 +1428,20 @@ 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::<Test, _, _, _>(
|
||||
let results = phragmen::elect::<_, _, _, <Test as Trait>::CurrencyToVote>(
|
||||
2,
|
||||
Staking::minimum_validator_count() as usize,
|
||||
<Validators<Test>>::enumerate(),
|
||||
<Nominators<Test>>::enumerate(),
|
||||
<Validators<Test>>::enumerate().map(|(who, _)| who).collect::<Vec<u64>>(),
|
||||
<Nominators<Test>>::enumerate().collect(),
|
||||
Staking::slashable_balance_of,
|
||||
true,
|
||||
);
|
||||
|
||||
let (winners, assignment) = winners.unwrap();
|
||||
let phragmen::PhragmenResult { winners, assignments } = results.unwrap();
|
||||
|
||||
// 10 and 30 must be the winners
|
||||
assert_eq!(winners, vec![11, 31]);
|
||||
assert_eq!(assignment, vec![
|
||||
assert_eq!(assignments, vec![
|
||||
(3, vec![(11, 2816371998), (31, 1478595298)]),
|
||||
(1, vec![(11, 4294967296)]),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user