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
+81 -1
View File
@@ -85,7 +85,7 @@ macro_rules! create_runtime_str {
#[cfg(feature = "std")]
pub use serde::{Serialize, de::DeserializeOwned};
#[cfg(feature = "std")]
use serde_derive::{Serialize, Deserialize};
pub use serde_derive::{Serialize, Deserialize};
/// Complex storage builder stuff.
#[cfg(feature = "std")]
@@ -251,6 +251,86 @@ impl From<codec::Compact<Perbill>> for Perbill {
}
}
/// Perquintill is parts-per-quintillion. It stores a value between 0 and 1 in fixed point and
/// provides a means to multiply some other value by that.
#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
#[derive(Encode, Decode, Default, Copy, Clone, PartialEq, Eq)]
pub struct Perquintill(u64);
const QUINTILLION: u64 = 1_000_000_000_000_000_000;
impl Perquintill {
/// Nothing.
pub fn zero() -> Self { Self(0) }
/// Everything.
pub fn one() -> Self { Self(QUINTILLION) }
/// Construct new instance where `x` is in quintillionths. Value equivalent to `x / 1,000,000,000,000,000,000`.
pub fn from_quintillionths(x: u64) -> Self { Self(x.min(QUINTILLION)) }
/// Construct new instance where `x` is in billionths. Value equivalent to `x / 1,000,000,000`.
pub fn from_billionths(x: u64) -> Self { Self(x.min(1_000_000_000) * 1_000_000_000 ) }
/// Construct new instance where `x` is in millionths. Value equivalent to `x / 1,000,000`.
pub fn from_millionths(x: u64) -> Self { Self(x.min(1_000_000) * 1000_000_000_000) }
/// Construct new instance where `x` is denominator and the nominator is 1.
pub fn from_xth(x: u64) -> Self { Self(QUINTILLION / x.min(QUINTILLION)) }
#[cfg(feature = "std")]
/// Construct new instance whose value is equal to `x` (between 0 and 1).
pub fn from_fraction(x: f64) -> Self { Self((x.max(0.0).min(1.0) * QUINTILLION as f64) as u64) }
}
impl ::rstd::ops::Deref for Perquintill {
type Target = u64;
fn deref(&self) -> &u64 {
&self.0
}
}
impl<N> ::rstd::ops::Mul<N> for Perquintill
where
N: traits::As<u64>
{
type Output = N;
fn mul(self, b: N) -> Self::Output {
<N as traits::As<u64>>::sa(b.as_().saturating_mul(self.0) / QUINTILLION)
}
}
#[cfg(feature = "std")]
impl From<f64> for Perquintill {
fn from(x: f64) -> Perquintill {
Perquintill::from_fraction(x)
}
}
#[cfg(feature = "std")]
impl From<f32> for Perquintill {
fn from(x: f32) -> Perquintill {
Perquintill::from_fraction(x as f64)
}
}
impl codec::CompactAs for Perquintill {
type As = u64;
fn encode_as(&self) -> &u64 {
&self.0
}
fn decode_from(x: u64) -> Perquintill {
Perquintill(x)
}
}
impl From<codec::Compact<Perquintill>> for Perquintill {
fn from(x: codec::Compact<Perquintill>) -> Perquintill {
x.0
}
}
/// Signature verify that can work with any known signature types..
#[derive(Eq, PartialEq, Clone, Encode, Decode)]
#[cfg_attr(feature = "std", derive(Debug))]
+3 -3
View File
@@ -19,7 +19,7 @@
use primitives::{ed25519::Public as AuthorityId, ed25519, sr25519, Pair, crypto::UncheckedInto};
use node_primitives::AccountId;
use node_runtime::{ConsensusConfig, CouncilSeatsConfig, CouncilVotingConfig, DemocracyConfig,
SessionConfig, StakingConfig, TimestampConfig, BalancesConfig, TreasuryConfig,
SessionConfig, StakingConfig, StakerStatus, TimestampConfig, BalancesConfig, TreasuryConfig,
SudoConfig, ContractConfig, GrandpaConfig, IndicesConfig, FeesConfig, Permill, Perbill};
pub use node_runtime::GenesisConfig;
use substrate_service;
@@ -113,7 +113,7 @@ fn staging_testnet_config_genesis() -> GenesisConfig {
bonding_duration: 60 * MINUTES,
offline_slash_grace: 4,
minimum_validator_count: 4,
stakers: initial_authorities.iter().map(|x| (x.0.clone(), x.1.clone(), STASH)).collect(),
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(),
}),
democracy: Some(DemocracyConfig {
@@ -267,7 +267,7 @@ pub fn testnet_genesis(
current_offline_slash: 0,
current_session_reward: 0,
offline_slash_grace: 0,
stakers: initial_authorities.iter().map(|x| (x.0.clone(), x.1.clone(), STASH)).collect(),
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(),
}),
democracy: Some(DemocracyConfig {
+21 -3
View File
@@ -39,7 +39,7 @@ mod tests {
use node_primitives::{Hash, BlockNumber, AccountId};
use runtime_primitives::traits::{Header as HeaderT, Hash as HashT};
use runtime_primitives::{generic, generic::Era, ApplyOutcome, ApplyError, ApplyResult, Perbill};
use {balances, indices, session, system, consensus, timestamp, treasury, contract};
use {balances, indices, session, system, staking, consensus, timestamp, treasury, contract};
use contract::ContractAddressFor;
use system::{EventRecord, Phase};
use node_runtime::{Header, Block, UncheckedExtrinsic, CheckedExtrinsic, Call, Runtime, Balances,
@@ -296,7 +296,11 @@ mod tests {
staking: Some(StakingConfig {
sessions_per_era: 2,
current_era: 0,
stakers: vec![(dave(), alice(), 111), (eve(), bob(), 101), (ferdie(), charlie(), 100)],
stakers: vec![
(dave(), alice(), 111, staking::StakerStatus::Validator),
(eve(), bob(), 100, staking::StakerStatus::Validator),
(ferdie(), charlie(), 100, staking::StakerStatus::Validator)
],
validator_count: 3,
minimum_validator_count: 0,
bonding_duration: 0,
@@ -441,7 +445,13 @@ mod tests {
]
);
let digest = generic::Digest::<Log>::default();
// let mut digest = generic::Digest::<Log>::default();
// digest.push(Log::from(::grandpa::RawLog::AuthoritiesChangeSignal(0, vec![
// (Keyring::Charlie.to_raw_public().into(), 1),
// (Keyring::Bob.to_raw_public().into(), 1),
// (Keyring::Alice.to_raw_public().into(), 1),
// ])));
let digest = generic::Digest::<Log>::default(); // TODO test this
assert_eq!(Header::decode(&mut &block2.0[..]).unwrap().digest, digest);
(block1, block2)
@@ -574,6 +584,14 @@ mod tests {
phase: Phase::Finalization,
event: Event::session(session::RawEvent::NewSession(1))
},
// EventRecord { // TODO: this might be wrong.
// phase: Phase::Finalization,
// event: Event::grandpa(::grandpa::RawEvent::NewAuthorities(vec![
// (Keyring::Charlie.to_raw_public().into(), 1),
// (Keyring::Bob.to_raw_public().into(), 1),
// (Keyring::Alice.to_raw_public().into(), 1),
// ])),
// },
EventRecord {
phase: Phase::Finalization,
event: Event::treasury(treasury::RawEvent::Spending(0))
+2 -1
View File
@@ -54,6 +54,7 @@ pub use timestamp::Call as TimestampCall;
pub use balances::Call as BalancesCall;
pub use runtime_primitives::{Permill, Perbill};
pub use support::StorageValue;
pub use staking::StakerStatus;
/// Runtime version.
pub const VERSION: RuntimeVersion = RuntimeVersion {
@@ -61,7 +62,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
impl_name: create_runtime_str!("substrate-node"),
authoring_version: 10,
spec_version: 35,
impl_version: 35,
impl_version: 36,
apis: RUNTIME_API_VERSIONS,
};
-62
View File
@@ -1,62 +0,0 @@
# Module Summary, Description and Specification
## Staking
The staking module is the means by which a set of network maintainers (known as "authorities" in some contexts and "validators" in others) are chosen based upon those who voluntarily place funds under deposit. Under deposit, those funds are rewarded under normal operation but are held at pain of "slash" (expropriation) should they be found not to bee discharhing their duties properly.
### Vocabulary
- Staking: The process of locking up funds for some time, placing them at risk of slashing (loss) in order to become a rewarded maintainer of the network.
- Validating: The process of running a node to actively maintain the network, either by producing blocks or guaranteeing finality of the chain.
- Nominating: The process of placing staked funds behind one or more validators in order to share in any reward, and punishment, they take.
- Stash account: The account holding an owner's funds used for staking.
- Controller account: The account which controls am owner's funds for staking.
- Era: A (whole) number of sessions which is the period that the validator set (and each validator's active nominator set) is recalculated and where rewards are paid out.
- Slash: The punishment of a staker by reducing their funds.
### Goals
The staking system in Substrate NPoS is designed to achieve three goals:
- It should be possible to stake funds that are controlled by a cold wallet.
- It should be possible to withdraw some, or deposit more, funds without interrupting the role of t.
- It should be possible to switch between roles (nominator, validator, idle) with minimal overhead.
### Stash account
To achieve these goals, Substrate NPoS distinguishes the act of staking from the act of declaring the role (nominating or validating) desired. An owner of funds wanting to validate or nominate must first deposit some or all of an account's balance to be managed by the staking system. When they do this, we call it *staking* and we say the funds are *under management* and *bonded*. A transaction-dispatchable call `bond` is provided for this. Once an account has funds bonded, those funds may no longer be transfered out or deducted in any way, including for transaction fees payment. If all funds of the account are thus used, then the account is effectively locked since it is unable to pay for any transactions.
Since the funds under management may be entirely frozen, and quite possibly controlled only by an offline cold wallet device, another account is used to control the staking activity of these funds. At the point of staking an account, this account is declared. Whereas the account holding the funds under management is known as the *stash*, the second account that controls the staking activity is called the *controller* account. Once staked, the stash account has no further transactional interaction with the staking module; all transactions regarding the staking activity of the stash are signed with the controller account. If there are unmanaged funds, then non-staking transactions may still be issued from the stash account, such as transfering funds out with the balances module.
### Controller account
Once the stash account's funds are committed under management of the staking system, then the controller account takes over. Three operations allow the owner to control their role in the staking system, switching between idle (no role at all) with the `chill` call; switching to a validator role with the `validate` call; and finally switching to the nominator role with `nominate`. In the case of the latter, the set of validators they are happy to entrust their stake to is also provided. The effect of these operations is felt at the next point that the nominator/validator set is recalculated, which will usually be at the end of the current era.
Three further operations are provided for the fund management: two for withdrawing funds that are under management of the staking system `unbond` and `withdraw_unbonded`, and another for introducing additional funds under management, `bond_extra`. Regarding the withdrawal of funds, the funds become inactive in the staking system from the era following the `unbond` call, however they may not be transfered out of the account (by a normal transfer operation using the stash key) until the bonding period has ended. At that point, the `withdraw_unbonded` must be called before the funds are free to bee used.
Funds deposited into the stash account will not automatically be introduced under management of the staking system: They may be retransfered out of the stash account normally until they enter under management. If there is a desire to bring such funds not yet under managment into the staking system, a separate transaction calling `bond_extra` must be issued to do this.
### Validating
A `validate` transaction takes a parameter of type `ValidatorPrefs`; this encodes a set of options available to validators. There are two options here: the `unstake_threshold` and `validator_payment`. The former allows a validator to control how long they acrue punishment for being offline before they are finally removed from the validator list and have the slash deducted. There is a tradeoff between being removed from the validator set early and thus missing out on an era's rewards and risking a much more substantial punishment as the slash amount increases exponentially with each offline infraction.
The latter option, `validator_payment`, allows a validator to reserve some amount of funds for themselves before the rest is shared out, *pro rata* amongst itself and the nominators. By "default", this is zero which means the validator and nominators partake in the rewards equally. However, by raising this, the validator may reserve some of the funds for themselves, making them a less attractive financial proposal compared to other less "greedy" validators. This allows over-subscribed validators to monetise their reputation and provides a macroeconomic mechanism of redistributing nominations between different validators.
### Nonminating
A `nominate` transaction take a single parameter which is the set of validator identities the nominator approves of their stake backing. Nomination does not allow control of *how much* of the stake backs any individual validator. If a staker wishes to have such fine-grained control, they could split their funds between several accounts and stake each individually to effect such a arrangement.
At the beginning of each era, each staker's funds is automatically allocated between some or all of each of their nominated validators, possibly with some (or, in extremis all) left unallocated. Only the portion of their stake that is allocated generates rewards and is at risk of slashing.
When an era begins, a basic usage of the Phragmén method gives an initial allocation. Over some initial period (perhaps one session) in the era, third-parties may submit their own solutions (typically determined by running Phragmén more extensively) in order to further optimise the allocation between nominators and validators. At the end of the initial period, the allocation is fixed for the rest of the era. During the initial period, any slashing uses the initial, suboptimal allocations.
### Rewards & Payouts
At the end of each successful session, a reward is accrued according to the overall timeliness of blocks. If the session's aveerage block period was optimal, then the maximum reward is accrued; the fewer blocks producted, the lower the reward. At the end of each era, each validator is paid this overall reward into an account of their choosing. Nominator portions are distributed *pro rata* for the amount of stake backing that validator and according to the validator's preferences.
There are three possible payment destinations or `Payee`s and this is set during the call to `bond` and may be updated by dispatching a `set_payee` transaction. The `Controller` payee places rewards into the controller account. The `Stash` and `Staked` targets both place rewards into the stash account, but the latter also places the rewards immediately under management.
### Slashing
Slashing happens when a validator has misbehaved in some way. Funds may be slashed up until the point they are withdrawn from management (using the `withdraw_unbonded` call). Digests of validator and nominator arrangements are recorded in order to ensure that historical misbehaviour can be properly attributed to stakes and punished.
For a slash on some validator balance and associated nominator balances, the validator balance is reduced at preference. If the slash amount is greater than that which the validator has at stake, then the nominators balances are reduced pro rata for the remainder.
+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