Phragmén Validator Election (#1915)

* adds first draft phragmen with tuned tests and sr-primitives

* fix a few docs and code styles

* clean, organize and finish remaining test cases

* more and more tests

* update edge cases behavior and relavent tests

* fix global test issues

* updated wasm files

* all tests fixed

* cleanup

* fix some global issues

* fix global tests

* bump versions

* fix typo

* first step toward extracting phragmen

* Fix most of the grumbles.

* fix rest of the grumbles

* spaces to tabs

* update wasm

* Removed nightly feature.

* More tests

* Fix broken tests.

* Bump and update wasm.
This commit is contained in:
Kian Peymani
2019-03-14 12:23:55 +01:00
committed by Gav Wood
parent d7fcf5dc9d
commit b86c96ea31
11 changed files with 1147 additions and 383 deletions
+60 -69
View File
@@ -31,18 +31,25 @@ use srml_support::traits::{
LockIdentifier, LockableCurrency, WithdrawReasons
};
use session::OnSessionChange;
use primitives::Perbill;
use primitives::{Perbill};
use primitives::traits::{Zero, One, As, StaticLookup, Saturating, Bounded};
#[cfg(feature = "std")]
use primitives::{Serialize, Deserialize};
use system::ensure_signed;
mod mock;
mod mock;
mod tests;
mod phragmen;
const RECENT_OFFLINE_COUNT: usize = 32;
const DEFAULT_MINIMUM_VALIDATOR_COUNT: u32 = 4;
const MAX_NOMINATIONS: usize = 16;
const MAX_UNSTAKE_THRESHOLD: u32 = 10;
// Indicates the initial status of the staker
#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))]
pub enum StakerStatus<AccountId> { Idle, Validator, Nominator(Vec<AccountId>), }
/// A destination account for payment.
#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode)]
#[cfg_attr(feature = "std", derive(Debug))]
@@ -159,7 +166,7 @@ pub struct Exposure<AccountId, Balance: HasCompact> {
pub others: Vec<IndividualExposure<AccountId, Balance>>,
}
type BalanceOf<T> = <<T as Trait>::Currency as ArithmeticType>::Type;
type BalanceOf<T> = <<T as Trait>::Currency as ArithmeticType>::Type;
pub trait Trait: system::Trait + session::Trait {
/// The staking balance.
@@ -230,7 +237,7 @@ decl_storage! {
//
// Every era change, this will be appended with the trie root of the contents of `Stakers`, and the oldest
// entry removed down to a specific number of entries (probably around 90 for a 3 month history).
// pub HistoricalStakers get(historical_stakers): map T::BlockNumber => Option<H256>;
// pub HistoricalStakers get(historical_stakers): map T::BlockNumber => Option<H256>;
/// The current era index.
pub CurrentEra get(current_era) config(): T::BlockNumber;
@@ -253,7 +260,7 @@ decl_storage! {
///
/// This is used to derive rewards and punishments.
pub SlotStake get(slot_stake) build(|config: &GenesisConfig<T>| {
config.stakers.iter().map(|&(_, _, value)| value).min().unwrap_or_default()
config.stakers.iter().map(|&(_, _, value, _)| value).min().unwrap_or_default()
}): BalanceOf<T>;
/// The number of times a given validator has been reported offline. This gets decremented by one each era that passes.
@@ -266,14 +273,31 @@ decl_storage! {
pub RecentlyOffline get(recently_offline): Vec<(T::AccountId, T::BlockNumber, u32)>;
}
add_extra_genesis {
config(stakers): Vec<(T::AccountId, T::AccountId, BalanceOf<T>)>;
config(stakers): Vec<(T::AccountId, T::AccountId, BalanceOf<T>, StakerStatus<T::AccountId>)>;
build(|storage: &mut primitives::StorageOverlay, _: &mut primitives::ChildrenStorageOverlay, config: &GenesisConfig<T>| {
with_storage(storage, || {
for &(ref stash, ref controller, balance) in &config.stakers {
assert!(T::Currency::free_balance(&stash) >= balance);
let _ = <Module<T>>::bond(T::Origin::from(Some(stash.clone()).into()), T::Lookup::unlookup(controller.clone()), balance, RewardDestination::Staked);
let _ = <Module<T>>::validate(T::Origin::from(Some(controller.clone()).into()), Default::default());
for &(ref stash, ref controller, balance, ref status) in &config.stakers {
let _ = <Module<T>>::bond(
T::Origin::from(Some(stash.clone()).into()),
T::Lookup::unlookup(controller.clone()),
balance,
RewardDestination::Staked
);
let _ = match status {
StakerStatus::Validator => {
<Module<T>>::validate(
T::Origin::from(Some(controller.clone()).into()),
Default::default()
)
}, StakerStatus::Nominator(votes) => {
<Module<T>>::nominate(
T::Origin::from(Some(controller.clone()).into()),
votes.iter().map(|l| {T::Lookup::unlookup(l.clone())}).collect()
)
}, _ => Ok(())
};
}
<Module<T>>::select_validators();
});
});
@@ -512,14 +536,13 @@ impl<T: Trait> Module<T> {
fn slash_validator(v: &T::AccountId, slash: BalanceOf<T>) {
// The exposure (backing stake) information of the validator to be slashed.
let exposure = Self::stakers(v);
// The amount we are actually going to slash (can't be bigger than thair total exposure)
// The amount we are actually going to slash (can't be bigger than their total exposure)
let slash = slash.min(exposure.total);
// The amount we'll slash from the validator's stash directly.
let own_slash = exposure.own.min(slash);
let own_slash = own_slash - T::Currency::slash(v, own_slash).unwrap_or_default();
// The amount remaining that we can't slash from the validator, that must be taken from the nominators.
let rest_slash = slash - own_slash;
if !rest_slash.is_zero() {
// The total to be slashed from the nominators.
let total = exposure.total - exposure.own;
@@ -640,56 +663,27 @@ impl<T: Trait> Module<T> {
/// @returns the new SlotStake value.
fn select_validators() -> BalanceOf<T> {
// Map of (would-be) validator account to amount of stake backing it.
let rounds = || <ValidatorCount<T>>::get() as usize;
let validators = || <Validators<T>>::enumerate();
let nominators = || <Nominators<T>>::enumerate();
let stash_of = |w| Self::stash_balance(&w);
let min_validator_count = Self::minimum_validator_count() as usize;
let elected_candidates = phragmen::elect::<T, _, _, _, _>(
rounds,
validators,
nominators,
stash_of,
min_validator_count
);
// First, we pull all validators, together with their stash balance into a Vec (cpu=O(V), mem=O(V))
let mut candidates = <Validators<T>>::enumerate()
.map(|(who, _)| {
let stash_balance = Self::stash_balance(&who);
(who, Exposure { total: stash_balance, own: stash_balance, others: vec![] })
})
.collect::<Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)>>();
// Second, we sort by accountid (cpu=O(V.log(V)))
candidates.sort_unstable_by_key(|i| i.0.clone());
// Third, iterate through nominators and add their balance to the first validator in their approval
// list. cpu=O(N.log(V))
for (who, nominees) in <Nominators<T>>::enumerate() {
// For this trivial nominator mapping, we just assume that nominators always
// have themselves assigned to the first validator in their list.
if nominees.is_empty() {
// Not possible, but we protect against it anyway.
continue;
}
if let Ok(index) = candidates.binary_search_by(|i| i.0.cmp(&nominees[0])) {
let stash_balance = Self::stash_balance(&who);
candidates[index].1.total += stash_balance;
candidates[index].1.others.push(IndividualExposure { who, value: stash_balance });
}
}
// Get the new staker set by sorting by total backing stake and truncating.
// cpu=O(V.log(s)) average, O(V.s) worst.
let count = Self::validator_count() as usize;
let candidates = if candidates.len() <= count {
candidates.sort_unstable_by(|a, b| b.1.total.cmp(&a.1.total));
candidates
} else {
candidates.into_iter().fold(vec![], |mut winners: Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)>, entry| {
if let Err(insert_point) = winners.binary_search_by_key(&entry.1.total, |i| i.1.total) {
if winners.len() < count {
winners.insert(insert_point, entry)
} else {
if insert_point > 0 {
// Big enough to be considered: insert at beginning and swap up to relevant point.
winners[0] = entry;
for i in 0..(insert_point - 1) {
winners.swap(i, i + 1)
}
}
}
}
winners
})
};
// Figure out the minimum stake behind a slot.
let slot_stake = elected_candidates
.iter()
.min_by_key(|c| c.exposure.total)
.map(|c| c.exposure.total)
.unwrap_or_default();
<SlotStake<T>>::put(&slot_stake);
// Clear Stakers and reduce their slash_count.
for v in <session::Module<T>>::validators().iter() {
@@ -700,19 +694,16 @@ impl<T: Trait> Module<T> {
}
}
// Figure out the minimum stake behind a slot.
let slot_stake = candidates.last().map(|i| i.1.total).unwrap_or_default();
<SlotStake<T>>::put(&slot_stake);
// Populate Stakers.
for (who, exposure) in &candidates {
<Stakers<T>>::insert(who, exposure);
for candidate in &elected_candidates {
<Stakers<T>>::insert(candidate.who.clone(), candidate.exposure.clone());
}
// Set the new validator set.
<session::Module<T>>::set_validators(
&candidates.into_iter().map(|i| i.0).collect::<Vec<_>>()
&elected_candidates.into_iter().map(|i| i.who).collect::<Vec<_>>()
);
slot_stake
}
+67 -11
View File
@@ -23,7 +23,10 @@ use primitives::testing::{Digest, DigestItem, Header, UintAuthorityId, ConvertUi
use substrate_primitives::{H256, Blake2Hasher};
use runtime_io;
use srml_support::impl_outer_origin;
use crate::{GenesisConfig, Module, Trait};
use crate::{GenesisConfig, Module, Trait, StakerStatus};
// The AccountId alias in this test module.
pub type AccountIdType = u64;
impl_outer_origin!{
pub enum Origin for Test {}
@@ -44,7 +47,7 @@ impl system::Trait for Test {
type Hash = H256;
type Hashing = ::primitives::traits::BlakeTwo256;
type Digest = Digest;
type AccountId = u64;
type AccountId = AccountIdType;
type Lookup = IdentityLookup<u64>;
type Header = Header;
type Event = ();
@@ -78,17 +81,25 @@ pub struct ExtBuilder {
current_era: u64,
monied: bool,
reward: u64,
validator_pool: bool,
nominate: bool,
validator_count: u32,
minimum_validator_count: u32,
}
impl Default for ExtBuilder {
fn default() -> Self {
Self {
existential_deposit: 0,
session_length: 3,
sessions_per_era: 3,
session_length: 1,
sessions_per_era: 1,
current_era: 0,
monied: true,
reward: 10,
validator_pool: false,
nominate: true,
validator_count: 2,
minimum_validator_count: 0,
}
}
}
@@ -118,6 +129,24 @@ impl ExtBuilder {
self.reward = reward;
self
}
pub fn validator_pool(mut self, validator_pool: bool) -> Self {
// NOTE: this should only be set to true with monied = false.
self.validator_pool = validator_pool;
self
}
pub fn nominate(mut self, nominate: bool) -> Self {
// NOTE: this only sets a dummy nominator for tests that want 10 and 20 (default validators) to be chosen by default.
self.nominate = nominate;
self
}
pub fn validator_count(mut self, count: u32) -> Self {
self.validator_count = count;
self
}
pub fn minimum_validator_count(mut self, count: u32) -> Self {
self.minimum_validator_count = count;
self
}
pub fn build(self) -> runtime_io::TestExternalities<Blake2Hasher> {
let (mut t, mut c) = system::GenesisConfig::<Test>::default().build_storage().unwrap();
let balance_factor = if self.existential_deposit > 0 {
@@ -131,7 +160,8 @@ impl ExtBuilder {
}.assimilate_storage(&mut t, &mut c);
let _ = session::GenesisConfig::<Test>{
session_length: self.session_length,
validators: vec![10, 20],
// NOTE: if config.nominate == false then 100 is also selected in the initial round.
validators: if self.validator_pool { vec![10, 20, 30, 40] } else { vec![10, 20] },
keys: vec![],
}.assimilate_storage(&mut t, &mut c);
let _ = balances::GenesisConfig::<Test>{
@@ -145,13 +175,23 @@ impl ExtBuilder {
(10, balance_factor),
(11, balance_factor * 1000),
(20, balance_factor),
(21, balance_factor * 2000)
(21, balance_factor * 2000),
(100, 2000 * balance_factor),
(101, 2000 * balance_factor),
]
} else {
vec![(1, 10 * balance_factor), (2, 20 * balance_factor), (3, 300 * balance_factor), (4, 400 * balance_factor)]
vec![
(1, 10 * balance_factor), (2, 20 * balance_factor),
(3, 300 * balance_factor), (4, 400 * balance_factor)
]
}
} else {
vec![(10, balance_factor), (11, balance_factor * 1000), (20, balance_factor), (21, balance_factor * 2000)]
vec![
(10, balance_factor), (11, balance_factor * 10),
(20, balance_factor), (21, balance_factor * 20),
(30, balance_factor), (31, balance_factor * 30),
(40, balance_factor), (41, balance_factor * 40)
]
},
existential_deposit: self.existential_deposit,
transfer_fee: 0,
@@ -161,9 +201,25 @@ impl ExtBuilder {
let _ = GenesisConfig::<Test>{
sessions_per_era: self.sessions_per_era,
current_era: self.current_era,
stakers: vec![(11, 10, balance_factor * 1000), (21, 20, balance_factor * 2000)],
validator_count: 2,
minimum_validator_count: 0,
stakers: if self.validator_pool {
vec![
(11, 10, balance_factor * 1000, StakerStatus::<AccountIdType>::Validator),
(21, 20, balance_factor * 2000, StakerStatus::<AccountIdType>::Validator),
(31, 30, balance_factor * 3000, if self.validator_pool { StakerStatus::<AccountIdType>::Validator } else { StakerStatus::<AccountIdType>::Idle }),
(41, 40, balance_factor * 4000, if self.validator_pool { StakerStatus::<AccountIdType>::Validator } else { StakerStatus::<AccountIdType>::Idle }),
// nominator
(101, 100, balance_factor * 500, if self.nominate { StakerStatus::<AccountIdType>::Nominator(vec![10, 20]) } else { StakerStatus::<AccountIdType>::Nominator(vec![]) })
]
} else {
vec![
(11, 10, balance_factor * 1000, StakerStatus::<AccountIdType>::Validator),
(21, 20, balance_factor * 2000, StakerStatus::<AccountIdType>::Validator),
// nominator
(101, 100, balance_factor * 500, if self.nominate { StakerStatus::<AccountIdType>::Nominator(vec![10, 20]) } else { StakerStatus::<AccountIdType>::Nominator(vec![]) })
]
},
validator_count: self.validator_count,
minimum_validator_count: self.minimum_validator_count,
bonding_duration: self.sessions_per_era * self.session_length * 3,
session_reward: Perbill::from_millionths((1000000 * self.reward / balance_factor) as u32),
offline_slash: if self.monied { Perbill::from_percent(40) } else { Perbill::zero() },
+223
View File
@@ -0,0 +1,223 @@
// 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 <http://www.gnu.org/licenses/>.
//! Rust implementation of the Phragmén election algorithm.
use rstd::{prelude::*};
use primitives::Perquintill;
use primitives::traits::{Zero, As};
use parity_codec::{HasCompact, Encode, Decode};
use crate::{Exposure, BalanceOf, Trait, ValidatorPrefs, IndividualExposure};
// Wrapper around validation candidates some metadata.
#[derive(Clone, Encode, Decode)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Candidate<AccountId, Balance: HasCompact> {
// The validator's account
pub who: AccountId,
// Exposure struct, holding info about the value that the validator has in stake.
pub exposure: Exposure<AccountId, Balance>,
// Accumulator of the stake of this candidate based on received votes.
approval_stake: Balance,
// Intermediary value used to sort candidates.
// See Phragmén reference implementation.
pub score: Perquintill,
}
// Wrapper around the nomination info of a single nominator for a group of validators.
#[derive(Clone, Encode, Decode)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Nominations<AccountId, Balance: HasCompact> {
// The nominator's account.
who: AccountId,
// List of validators proposed by this nominator.
nominees: Vec<Vote<AccountId, Balance>>,
// the stake amount proposed by the nominator as a part of the vote.
// Same as `nom.budget` in Phragmén reference.
stake: Balance,
// Incremented each time a nominee that this nominator voted for has been elected.
load: Perquintill,
}
// Wrapper around a nominator vote and the load of that vote.
// Referred to as 'edge' in the Phragmén reference implementation.
#[derive(Clone, Encode, Decode)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Vote<AccountId, Balance: HasCompact> {
// Account being voted for
who: AccountId,
// Load of this vote.
load: Perquintill,
// Final backing stake of this vote.
backing_stake: Balance
}
/// Perform election based on Phragmén algorithm.
///
/// Reference implementation: https://github.com/w3f/consensus
///
/// @returns a vector of elected candidates
pub fn elect<T: Trait + 'static, FR, FN, FV, FS>(
get_rounds: FR,
get_validators: FV,
get_nominators: FN,
stash_of: FS,
minimum_validator_count: usize,
) -> Vec<Candidate<T::AccountId, BalanceOf<T>>> where
FR: Fn() -> usize,
FV: Fn() -> Box<dyn Iterator<
Item =(T::AccountId, ValidatorPrefs<BalanceOf<T>>)
>>,
FN: Fn() -> Box<dyn Iterator<
Item =(T::AccountId, Vec<T::AccountId>)
>>,
FS: Fn(T::AccountId) -> BalanceOf<T>,
{
let rounds = get_rounds();
let mut elected_candidates = vec![];
// 1- Pre-process candidates and place them in a container
let mut candidates = get_validators().map(|(who, _)| {
let stash_balance = stash_of(who.clone());
Candidate {
who,
approval_stake: BalanceOf::<T>::zero(),
score: Perquintill::zero(),
exposure: Exposure { total: stash_balance, own: stash_balance, others: vec![] },
}
}).collect::<Vec<Candidate<T::AccountId, BalanceOf<T>>>>();
// Just to be used when we are below minimum validator count
let original_candidates = candidates.clone();
// 2- Collect the nominators with the associated votes.
// Also collect approval stake along the way.
let mut nominations = get_nominators().map(|(who, nominees)| {
let nominator_stake = stash_of(who.clone());
for n in &nominees {
candidates.iter_mut().filter(|i| i.who == *n).for_each(|c| {
c.approval_stake += nominator_stake;
});
}
Nominations {
who,
nominees: nominees.into_iter()
.map(|n| Vote {who: n, load: Perquintill::zero(), backing_stake: BalanceOf::<T>::zero()})
.collect::<Vec<Vote<T::AccountId, BalanceOf<T>>>>(),
stake: nominator_stake,
load : Perquintill::zero(),
}
}).collect::<Vec<Nominations<T::AccountId, BalanceOf<T>>>>();
// 3- optimization:
// Candidates who have 0 stake => have no votes or all null-votes. Kick them out not.
let mut candidates = candidates.into_iter().filter(|c| c.approval_stake > BalanceOf::<T>::zero())
.collect::<Vec<Candidate<T::AccountId, BalanceOf<T>>>>();
// 4- If we have more candidates then needed, run Phragmén.
if candidates.len() > rounds {
// Main election loop
for _round in 0..rounds {
// Loop 1: initialize score
for nominaotion in &nominations {
for vote in &nominaotion.nominees {
let candidate = &vote.who;
if let Some(c) = candidates.iter_mut().find(|i| i.who == *candidate) {
let approval_stake = c.approval_stake;
c.score = Perquintill::from_xth(approval_stake.as_());
}
}
}
// Loop 2: increment score.
for nominaotion in &nominations {
for vote in &nominaotion.nominees {
let candidate = &vote.who;
if let Some(c) = candidates.iter_mut().find(|i| i.who == *candidate) {
let approval_stake = c.approval_stake;
let temp =
nominaotion.stake.as_()
* *nominaotion.load
/ approval_stake.as_();
c.score = Perquintill::from_quintillionths(*c.score + temp);
}
}
}
// Find the best
let (winner_index, _) = candidates.iter().enumerate().min_by_key(|&(_i, c)| *c.score)
.expect("candidates length is checked to be >0; qed");
// loop 3: update nominator and vote load
let winner = candidates.remove(winner_index);
for n in &mut nominations {
for v in &mut n.nominees {
if v.who == winner.who {
v.load =
Perquintill::from_quintillionths(
*winner.score
- *n.load
);
n.load = winner.score;
}
}
}
elected_candidates.push(winner);
} // end of all rounds
// 4.1- Update backing stake of candidates and nominators
for n in &mut nominations {
for v in &mut n.nominees {
// if the target of this vote is among the winners, otherwise let go.
if let Some(c) = elected_candidates.iter_mut().find(|c| c.who == v.who) {
v.backing_stake = <BalanceOf<T> as As<u64>>::sa(
n.stake.as_()
* *v.load
/ *n.load
);
c.exposure.total += v.backing_stake;
// Update IndividualExposure of those who nominated and their vote won
c.exposure.others.push(
IndividualExposure {who: n.who.clone(), value: v.backing_stake }
);
}
}
}
} else {
if candidates.len() > minimum_validator_count {
// if we don't have enough candidates, just choose all that have some vote.
elected_candidates = candidates;
// `Exposure.others` still needs an update
for n in &mut nominations {
for v in &mut n.nominees {
if let Some(c) = elected_candidates.iter_mut().find(|c| c.who == v.who) {
c.exposure.total += n.stake;
c.exposure.others.push(
IndividualExposure {who: n.who.clone(), value: n.stake }
);
}
}
}
} else {
// if we have less than minimum, use the previous validator set.
elected_candidates = original_candidates;
}
}
elected_candidates
}
File diff suppressed because it is too large Load Diff