diff --git a/core/sr-primitives/src/lib.rs b/core/sr-primitives/src/lib.rs index 953568e57bc7a..d0e9dbc8c0f92 100644 --- a/core/sr-primitives/src/lib.rs +++ b/core/sr-primitives/src/lib.rs @@ -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> 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 ::rstd::ops::Mul for Perquintill +where + N: traits::As +{ + type Output = N; + fn mul(self, b: N) -> Self::Output { + >::sa(b.as_().saturating_mul(self.0) / QUINTILLION) + } +} + +#[cfg(feature = "std")] +impl From for Perquintill { + fn from(x: f64) -> Perquintill { + Perquintill::from_fraction(x) + } +} + +#[cfg(feature = "std")] +impl From 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> for Perquintill { + fn from(x: codec::Compact) -> 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))] diff --git a/core/test-runtime/wasm/target/wasm32-unknown-unknown/release/substrate_test_runtime.compact.wasm b/core/test-runtime/wasm/target/wasm32-unknown-unknown/release/substrate_test_runtime.compact.wasm index ff8a7a70c368b..e844c0124e12a 100644 Binary files a/core/test-runtime/wasm/target/wasm32-unknown-unknown/release/substrate_test_runtime.compact.wasm and b/core/test-runtime/wasm/target/wasm32-unknown-unknown/release/substrate_test_runtime.compact.wasm differ diff --git a/node/cli/src/chain_spec.rs b/node/cli/src/chain_spec.rs index 784dcf5a75d59..db36ac92232c9 100644 --- a/node/cli/src/chain_spec.rs +++ b/node/cli/src/chain_spec.rs @@ -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 { diff --git a/node/executor/src/lib.rs b/node/executor/src/lib.rs index 8e1a2162da982..84b2de336ed58 100644 --- a/node/executor/src/lib.rs +++ b/node/executor/src/lib.rs @@ -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::::default(); + // let mut digest = generic::Digest::::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::::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)) diff --git a/node/runtime/src/lib.rs b/node/runtime/src/lib.rs index eab07326a0649..55293d3e46ed2 100644 --- a/node/runtime/src/lib.rs +++ b/node/runtime/src/lib.rs @@ -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, }; diff --git a/node/runtime/wasm/target/wasm32-unknown-unknown/release/node_runtime.compact.wasm b/node/runtime/wasm/target/wasm32-unknown-unknown/release/node_runtime.compact.wasm index 29139d149965f..f18a1fd01dd5b 100644 Binary files a/node/runtime/wasm/target/wasm32-unknown-unknown/release/node_runtime.compact.wasm and b/node/runtime/wasm/target/wasm32-unknown-unknown/release/node_runtime.compact.wasm differ diff --git a/srml/staking/Staking.md b/srml/staking/Staking.md deleted file mode 100644 index 160349656bd9e..0000000000000 --- a/srml/staking/Staking.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/srml/staking/src/lib.rs b/srml/staking/src/lib.rs index 221ba8966c5d8..4650bff981d77 100644 --- a/srml/staking/src/lib.rs +++ b/srml/staking/src/lib.rs @@ -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 { Idle, Validator, Nominator(Vec), } + /// 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 { pub others: Vec>, } -type BalanceOf = <::Currency as ArithmeticType>::Type; +type BalanceOf = <::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; + // pub HistoricalStakers get(historical_stakers): map T::BlockNumber => Option; /// 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| { - config.stakers.iter().map(|&(_, _, value)| value).min().unwrap_or_default() + config.stakers.iter().map(|&(_, _, value, _)| value).min().unwrap_or_default() }): BalanceOf; /// 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)>; + config(stakers): Vec<(T::AccountId, T::AccountId, BalanceOf, StakerStatus)>; build(|storage: &mut primitives::StorageOverlay, _: &mut primitives::ChildrenStorageOverlay, config: &GenesisConfig| { with_storage(storage, || { - for &(ref stash, ref controller, balance) in &config.stakers { - assert!(T::Currency::free_balance(&stash) >= balance); - let _ = >::bond(T::Origin::from(Some(stash.clone()).into()), T::Lookup::unlookup(controller.clone()), balance, RewardDestination::Staked); - let _ = >::validate(T::Origin::from(Some(controller.clone()).into()), Default::default()); + for &(ref stash, ref controller, balance, ref status) in &config.stakers { + let _ = >::bond( + T::Origin::from(Some(stash.clone()).into()), + T::Lookup::unlookup(controller.clone()), + balance, + RewardDestination::Staked + ); + let _ = match status { + StakerStatus::Validator => { + >::validate( + T::Origin::from(Some(controller.clone()).into()), + Default::default() + ) + }, StakerStatus::Nominator(votes) => { + >::nominate( + T::Origin::from(Some(controller.clone()).into()), + votes.iter().map(|l| {T::Lookup::unlookup(l.clone())}).collect() + ) + }, _ => Ok(()) + }; } + >::select_validators(); }); }); @@ -512,14 +536,13 @@ impl Module { fn slash_validator(v: &T::AccountId, slash: BalanceOf) { // 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 Module { /// @returns the new SlotStake value. fn select_validators() -> BalanceOf { // Map of (would-be) validator account to amount of stake backing it. + + let rounds = || >::get() as usize; + let validators = || >::enumerate(); + let nominators = || >::enumerate(); + let stash_of = |w| Self::stash_balance(&w); + let min_validator_count = Self::minimum_validator_count() as usize; + let elected_candidates = phragmen::elect::( + 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 = >::enumerate() - .map(|(who, _)| { - let stash_balance = Self::stash_balance(&who); - (who, Exposure { total: stash_balance, own: stash_balance, others: vec![] }) - }) - .collect::>)>>(); - // 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 >::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>)>, 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(); + >::put(&slot_stake); // Clear Stakers and reduce their slash_count. for v in >::validators().iter() { @@ -700,19 +694,16 @@ impl Module { } } - // Figure out the minimum stake behind a slot. - let slot_stake = candidates.last().map(|i| i.1.total).unwrap_or_default(); - >::put(&slot_stake); - // Populate Stakers. - for (who, exposure) in &candidates { - >::insert(who, exposure); + for candidate in &elected_candidates { + >::insert(candidate.who.clone(), candidate.exposure.clone()); } + // Set the new validator set. >::set_validators( - &candidates.into_iter().map(|i| i.0).collect::>() + &elected_candidates.into_iter().map(|i| i.who).collect::>() ); - + slot_stake } diff --git a/srml/staking/src/mock.rs b/srml/staking/src/mock.rs index 2934022a079a1..6a76f350efbea 100644 --- a/srml/staking/src/mock.rs +++ b/srml/staking/src/mock.rs @@ -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; 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 { let (mut t, mut c) = system::GenesisConfig::::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::{ 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::{ @@ -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::{ 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::::Validator), + (21, 20, balance_factor * 2000, StakerStatus::::Validator), + (31, 30, balance_factor * 3000, if self.validator_pool { StakerStatus::::Validator } else { StakerStatus::::Idle }), + (41, 40, balance_factor * 4000, if self.validator_pool { StakerStatus::::Validator } else { StakerStatus::::Idle }), + // nominator + (101, 100, balance_factor * 500, if self.nominate { StakerStatus::::Nominator(vec![10, 20]) } else { StakerStatus::::Nominator(vec![]) }) + ] + } else { + vec![ + (11, 10, balance_factor * 1000, StakerStatus::::Validator), + (21, 20, balance_factor * 2000, StakerStatus::::Validator), + // nominator + (101, 100, balance_factor * 500, if self.nominate { StakerStatus::::Nominator(vec![10, 20]) } else { StakerStatus::::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() }, diff --git a/srml/staking/src/phragmen.rs b/srml/staking/src/phragmen.rs new file mode 100644 index 0000000000000..bdaed1fee9760 --- /dev/null +++ b/srml/staking/src/phragmen.rs @@ -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 . + +//! 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 { + // The validator's account + pub who: AccountId, + // Exposure struct, holding info about the value that the validator has in stake. + pub exposure: Exposure, + // 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 { + // The nominator's account. + who: AccountId, + // List of validators proposed by this nominator. + nominees: Vec>, + // 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 { + // 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( + get_rounds: FR, + get_validators: FV, + get_nominators: FN, + stash_of: FS, + minimum_validator_count: usize, + ) -> Vec>> where + FR: Fn() -> usize, + FV: Fn() -> Box>) + >>, + FN: Fn() -> Box) + >>, + FS: Fn(T::AccountId) -> BalanceOf, +{ + 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::::zero(), + score: Perquintill::zero(), + exposure: Exposure { total: stash_balance, own: stash_balance, others: vec![] }, + } + }).collect::>>>(); + + // 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::::zero()}) + .collect::>>>(), + stake: nominator_stake, + load : Perquintill::zero(), + } + }).collect::>>>(); + + // 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::::zero()) + .collect::>>>(); + + // 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 = as As>::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 +} \ No newline at end of file diff --git a/srml/staking/src/tests.rs b/srml/staking/src/tests.rs index c3be7609b0d7a..7921d7f313027 100644 --- a/srml/staking/src/tests.rs +++ b/srml/staking/src/tests.rs @@ -20,6 +20,8 @@ use super::*; use runtime_io::with_externalities; +use phragmen; +use primitives::Perquintill; use srml_support::{assert_ok, assert_noop, EnumerableStorageMap}; use mock::{Balances, Session, Staking, System, Timestamp, Test, ExtBuilder, Origin}; use srml_support::traits::Currency; @@ -27,9 +29,8 @@ use srml_support::traits::Currency; #[test] fn basic_setup_works() { // Verifies initial conditions of mock - // TODO: Verify this check is comprehensive - // - Session Per Era, Session Reward - with_externalities(&mut ExtBuilder::default().build(), + with_externalities(&mut ExtBuilder::default() + .build(), || { assert_eq!(Staking::bonded(&11), Some(10)); // Account 11 is stashed and locked, and account 10 is the controller assert_eq!(Staking::bonded(&21), Some(20)); // Account 21 is stashed and locked, and account 20 is the controller @@ -48,13 +49,33 @@ fn basic_setup_works() { (10, ValidatorPrefs { unstake_threshold: 3, validator_payment: 0 }) ]); + // Account 100 is the default nominator + assert_eq!(Staking::ledger(100), Some(StakingLedger { stash: 101, total: 500, active: 500, unlocking: vec![] })); + assert_eq!(Staking::nominators(100), vec![10, 20]); + // Account 10 is exposed by 100 * balance_factor from their own stash in account 11 - assert_eq!(Staking::stakers(10), Exposure { total: 1000, own: 1000, others: vec![] }); - assert_eq!(Staking::stakers(20), Exposure { total: 2000, own: 2000, others: vec![] }); + assert_eq!(Staking::stakers(10), Exposure { total: 1500, own: 1000, others: vec![ IndividualExposure { who: 100, value: 500 }] }); + assert_eq!(Staking::stakers(20), Exposure { total: 2500, own: 2000, others: vec![ IndividualExposure { who: 100, value: 500 }] }); + + // The number of validators required. + assert_eq!(Staking::validator_count(), 2); + + // Initial Era and session + assert_eq!(Staking::current_era(), 0); + assert_eq!(Session::current_index(), 0); + + // initial rewards + assert_eq!(Staking::current_session_reward(), 10); + + // initial slot_stake + assert_eq!(Staking::slot_stake(), 1500); + + // initial slash_count of validators + assert_eq!(Staking::slash_count(&10), 0); + assert_eq!(Staking::slash_count(&20), 0); }); } - #[test] fn no_offline_should_work() { // Test the staking module works when no validators are offline @@ -83,7 +104,7 @@ fn invulnerability_should_work() { assert_ok!(Staking::set_invulnerables(vec![10])); // Give account 10 some funds Balances::set_free_balance(&10, 70); - // There is no slash grade period + // There is no slash grace -- slash immediately. assert_eq!(Staking::offline_slash_grace(), 0); // Account 10 has not been slashed assert_eq!(Staking::slash_count(&10), 0); @@ -101,6 +122,7 @@ fn invulnerability_should_work() { assert_eq!(Balances::free_balance(&10), 70); assert!(>::exists(&10)); // New era not being forced + // NOTE: new era is always forced once slashing happens -> new validators need to be chosen. assert!(Staking::forcing_new_era().is_none()); }); } @@ -176,16 +198,98 @@ fn offline_grace_should_delay_slashing() { } +#[test] +fn max_unstake_threshold_works() { + // Tests that max_unstake_threshold gets used when prefs.unstake_threshold is large + with_externalities(&mut ExtBuilder::default().build(), || { + const MAX_UNSTAKE_THRESHOLD: u32 = 10; + // Two users with maximum possible balance + Balances::set_free_balance(&10, u64::max_value()); + Balances::set_free_balance(&20, u64::max_value()); + + // Give them full exposer as a staker + >::insert(&10, Exposure { total: u64::max_value(), own: u64::max_value(), others: vec![]}); + >::insert(&20, Exposure { total: u64::max_value(), own: u64::max_value(), others: vec![]}); + + // Check things are initialized correctly + assert_eq!(Balances::free_balance(&10), u64::max_value()); + assert_eq!(Balances::free_balance(&20), u64::max_value()); + assert_eq!(Balances::free_balance(&10), Balances::free_balance(&20)); + assert_eq!(Staking::offline_slash_grace(), 0); + assert_eq!(Staking::current_offline_slash(), 20); + // Account 10 will have max unstake_threshold + assert_ok!(Staking::validate(Origin::signed(10), ValidatorPrefs { + unstake_threshold: MAX_UNSTAKE_THRESHOLD, + validator_payment: 0, + })); + // Account 20 could not set their unstake_threshold past 10 + assert_noop!(Staking::validate(Origin::signed(20), ValidatorPrefs { + unstake_threshold: 11, + validator_payment: 0}), + "unstake threshold too large" + ); + // Give Account 20 unstake_threshold 11 anyway, should still be limited to 10 + >::insert(20, ValidatorPrefs { + unstake_threshold: 11, + validator_payment: 0, + }); + + // Make slot_stake really large, as to not affect punishment curve + >::put(u64::max_value()); + // Confirm `slot_stake` is greater than exponential punishment, else math below will be different + assert!(Staking::slot_stake() > 2_u64.pow(MAX_UNSTAKE_THRESHOLD) * 20); + + // Report each user 1 more than the max_unstake_threshold + Staking::on_offline_validator(10, MAX_UNSTAKE_THRESHOLD as usize + 1); + Staking::on_offline_validator(20, MAX_UNSTAKE_THRESHOLD as usize + 1); + + // Show that each balance only gets reduced by 2^max_unstake_threshold + assert_eq!(Balances::free_balance(&10), u64::max_value() - 2_u64.pow(MAX_UNSTAKE_THRESHOLD) * 20); + assert_eq!(Balances::free_balance(&20), u64::max_value() - 2_u64.pow(MAX_UNSTAKE_THRESHOLD) * 20); + }); +} + +#[test] +fn slashing_does_not_cause_underflow() { + // Tests that slashing more than a user has does not underflow + with_externalities(&mut ExtBuilder::default().build(), || { + // One user with less than `max_value` will test underflow does not occur + Balances::set_free_balance(&10, 1); + + // Verify initial conditions + assert_eq!(Balances::free_balance(&10), 1); + assert_eq!(Staking::offline_slash_grace(), 0); + + // Set validator preference so that 2^unstake_threshold would cause overflow (greater than 64) + >::insert(10, ValidatorPrefs { + unstake_threshold: 10, + validator_payment: 0, + }); + + System::set_block_number(1); + Session::check_rotate_session(System::block_number()); + + // Should not panic + Staking::on_offline_validator(10, 100); + // Confirm that underflow has not occurred, and account balance is set to zero + assert_eq!(Balances::free_balance(&10), 0); + }); +} + + #[test] fn rewards_should_work() { - // should check that: - // 1) rewards get recorded per session - // 2) rewards get paid per Era - // 3) (bonus) Check that nominators are also rewarded - with_externalities(&mut ExtBuilder::default().build(), + // should check that: + // * rewards get recorded per session + // * rewards get paid per Era + // * Check that nominators are also rewarded + with_externalities(&mut ExtBuilder::default() + .session_length(3) + .sessions_per_era(3) + .build(), || { let delay = 2; - // this test is only in the scope of one era. Since this variable changes + // this test is only in the scope of one era. Since this variable changes // at the last block/new era, we'll save it. let session_reward = 10; @@ -202,17 +306,17 @@ fn rewards_should_work() { assert_eq!(Staking::current_session_reward(), 10); // check the balance of a validator accounts. - assert_eq!(Balances::total_balance(&10), 1); + assert_eq!(Balances::total_balance(&10), 1); // and the nominator (to-be) - assert_eq!(Balances::total_balance(&2), 20); + assert_eq!(Balances::total_balance(&2), 20); // add a dummy nominator. // NOTE: this nominator is being added 'manually'. a Further test (nomination_and_reward..) will add it via '.nominate()' >::insert(&10, Exposure { own: 500, // equal division indicates that the reward will be equally divided among validator and nominator. total: 1000, - others: vec![IndividualExposure {who: 2, value: 500 }] - }); + others: vec![IndividualExposure {who: 2, value: 500 }] + }); >::insert(&2, RewardDestination::Controller); @@ -220,7 +324,7 @@ fn rewards_should_work() { // Block 3 => Session 1 => Era 0 System::set_block_number(block); Timestamp::set_timestamp(block*5); // on time. - Session::check_rotate_session(System::block_number()); // QUESTIONS: why this matters ? + Session::check_rotate_session(System::block_number()); assert_eq!(Staking::current_era(), 0); assert_eq!(Session::current_index(), 1); @@ -228,7 +332,7 @@ fn rewards_should_work() { assert_eq!(Staking::current_session_reward(), session_reward); assert_eq!(Staking::current_era_reward(), session_reward); - block = 6; // Block 6 => Session 2 => Era 0 + block = 6; // Block 6 => Session 2 => Era 0 System::set_block_number(block); Timestamp::set_timestamp(block*5 + delay); // a little late. Session::check_rotate_session(System::block_number()); @@ -255,9 +359,13 @@ fn rewards_should_work() { #[test] fn multi_era_reward_should_work() { // should check that: - // The value of current_session_reward is set at the end of each era, based on - // slot_stake and session_reward. Check and verify this. - with_externalities(&mut ExtBuilder::default().build(), + // The value of current_session_reward is set at the end of each era, based on + // slot_stake and session_reward. Check and verify this. + with_externalities(&mut ExtBuilder::default() + .session_length(3) + .sessions_per_era(3) + .nominate(false) + .build(), || { let delay = 0; let session_reward = 10; @@ -275,7 +383,7 @@ fn multi_era_reward_should_work() { // Block 3 => Session 1 => Era 0 System::set_block_number(block); Timestamp::set_timestamp(block*5); // on time. - Session::check_rotate_session(System::block_number()); // QUESTIONS: why this matters ? + Session::check_rotate_session(System::block_number()); assert_eq!(Staking::current_era(), 0); assert_eq!(Session::current_index(), 1); @@ -283,7 +391,7 @@ fn multi_era_reward_should_work() { assert_eq!(Staking::current_session_reward(), session_reward); assert_eq!(Staking::current_era_reward(), session_reward); - block = 6; // Block 6 => Session 2 => Era 0 + block = 6; // Block 6 => Session 2 => Era 0 System::set_block_number(block); Timestamp::set_timestamp(block*5 + delay); // a little late. Session::check_rotate_session(System::block_number()); @@ -315,6 +423,7 @@ fn multi_era_reward_should_work() { // intermediate test. assert_eq!(Staking::current_era_reward(), 2*new_session_reward); + // new era is triggered here. block=18;System::set_block_number(block);Timestamp::set_timestamp(block*5);Session::check_rotate_session(System::block_number()); // pay time @@ -324,24 +433,33 @@ fn multi_era_reward_should_work() { #[test] fn staking_should_work() { - // should test: + // should test: // * new validators can be added to the default set - // * new ones will be chosen per era (+ based on phragmen) - // * either one can unlock the stash and back-down from being a validator. - with_externalities(&mut ExtBuilder::default().session_length(1).build(), || { + // * new ones will be chosen per era + // * either one can unlock the stash and back-down from being a validator via `chill`ing. + with_externalities(&mut ExtBuilder::default() + .sessions_per_era(3) + .nominate(false) + .build(), + || { assert_eq!(Staking::era_length(), 3); - assert_eq!(Staking::validator_count(), 2); // remember + compare this along with the test. assert_eq!(Session::validators(), vec![20, 10]); assert_ok!(Staking::set_bonding_duration(2)); assert_eq!(Staking::bonding_duration(), 2); - // --- Block 1: + // put some money in account that we'll use. + for i in 1..5 { Balances::set_free_balance(&i, 1000); } + + // bond one account pair and state interest in nomination. + // this is needed to keep 10 and 20 in the validator list with phragmen + assert_ok!(Staking::bond(Origin::signed(1), 2, 500, RewardDestination::default())); + assert_ok!(Staking::nominate(Origin::signed(2), vec![20, 4])); + + // --- Block 1: System::set_block_number(1); - // give the man some coins - Balances::set_free_balance(&3, 3000); - // initial stakers: vec![(11, 10, balance_factor * 100), (21, 20, balance_factor * 200)], - // account 3 controlled by 4. + + // add a new candidate for being a validator. account 3 controlled by 4. assert_ok!(Staking::bond(Origin::signed(3), 4, 1500, RewardDestination::Controller)); // balance of 3 = 3000, stashed = 1500 Session::check_rotate_session(System::block_number()); @@ -350,11 +468,11 @@ fn staking_should_work() { assert_eq!(Session::validators(), vec![20, 10]); - // --- Block 2: + // --- Block 2: System::set_block_number(2); - // Explicitly state the desire to validate for all of them. + // Explicitly state the desire to validate // note that the controller account will state interest as representative of the stash-controller pair. - assert_ok!(Staking::validate(Origin::signed(4), ValidatorPrefs { unstake_threshold: 3, validator_payment: 0 })); + assert_ok!(Staking::validate(Origin::signed(4), ValidatorPrefs::default())); Session::check_rotate_session(System::block_number()); assert_eq!(Staking::current_era(), 0); @@ -362,12 +480,11 @@ fn staking_should_work() { assert_eq!(Session::validators(), vec![20, 10]); - // --- Block 3: the validators will now change. + // --- Block 3: the validators will now change. System::set_block_number(3); Session::check_rotate_session(System::block_number()); - // TODO: the assertion in the section should be changed to something in sync with how phragmen works. - // for now just check that some arbitrary "two validators" have been chosen. + // 2 only voted for 4 and 20 assert_eq!(Session::validators().len(), 2); assert_eq!(Session::validators(), vec![4, 20]); assert_eq!(Staking::current_era(), 1); @@ -377,7 +494,13 @@ fn staking_should_work() { System::set_block_number(4); // unlock the entire stashed value. + // Note that this will ne be enough to remove 4 as a validator candidate! Staking::unbond(Origin::signed(4), Staking::ledger(&4).unwrap().active).unwrap(); + // explicit chill indicated that 4 no longer wants to be a validator. + Staking::chill(Origin::signed(4)).unwrap(); + + // nominator votes for 10 + assert_ok!(Staking::nominate(Origin::signed(2), vec![20, 10])); Session::check_rotate_session(System::block_number()); // nothing should be changed so far. @@ -385,101 +508,189 @@ fn staking_should_work() { assert_eq!(Staking::current_era(), 1); - // --- Block 5: nothing. 4 is still there. + // --- Block 5: nothing. 4 is still there. System::set_block_number(5); Session::check_rotate_session(System::block_number()); - assert_eq!(Session::validators(), vec![4, 20]); + assert_eq!(Session::validators(), vec![4, 20]); assert_eq!(Staking::current_era(), 1); - // --- Block 6: 4 will be not be a validator as it has nothing in stash. + // --- Block 6: 4 will not be a validator. System::set_block_number(6); Session::check_rotate_session(System::block_number()); - assert_eq!(Session::validators().contains(&4), false); + assert_eq!(Staking::current_era(), 2); + assert_eq!(Session::validators().contains(&4), false); + assert_eq!(Session::validators(), vec![20, 10]); }); } +#[test] +fn less_than_needed_candidates_works() { + // Test the situation where the number of validators are less than `ValidatorCount` but more than + // The expected behavior is to choose all the candidates that have some vote. + with_externalities(&mut ExtBuilder::default() + .minimum_validator_count(1) + .validator_count(3) + .nominate(false) + .validator_pool(true) + .build(), + || { + assert_eq!(Staking::era_length(), 1); + assert_eq!(Staking::validator_count(), 3); + + assert_eq!(Staking::minimum_validator_count(), 1); + assert_eq!(Staking::validator_count(), 3); + + // initial validators + assert_eq!(Session::validators(), vec![40, 30, 20, 10]); + + // only one nominator will exist and it will + assert_ok!(Staking::bond(Origin::signed(1), 2, 500, RewardDestination::default())); + assert_ok!(Staking::nominate(Origin::signed(2), vec![10, 20])); + + // 10 and 20 are now valid candidates. + // trigger era + System::set_block_number(1); + Session::check_rotate_session(System::block_number()); + assert_eq!(Staking::current_era(), 1); + + // both validators will be chosen again. NO election algorithm is even executed. + assert_eq!(Session::validators(), vec![20, 10]); + + // But the exposure is updated in a simple way. Each nominators vote is applied + assert_eq!(Staking::stakers(10).others.iter().map(|e| e.who).collect::>>(), vec![2]); + assert_eq!(Staking::stakers(20).others.iter().map(|e| e.who).collect::>>(), vec![2]); + }); +} + +#[test] +fn no_candidate_emergency_condition() { + // Test the situation where the number of validators are less than `ValidatorCount` and less than + // The expected behavior is to choose all candidates from the previous era. + with_externalities(&mut ExtBuilder::default() + .minimum_validator_count(1) + .validator_count(3) + .nominate(false) + .validator_pool(true) + .build(), + || { + assert_eq!(Staking::era_length(), 1); + assert_eq!(Staking::validator_count(), 3); + + assert_eq!(Staking::minimum_validator_count(), 1); + assert_eq!(Staking::validator_count(), 3); + + // initial validators + assert_eq!(Session::validators(), vec![40, 30, 20, 10]); + + // trigger era + System::set_block_number(1); + Session::check_rotate_session(System::block_number()); + assert_eq!(Staking::current_era(), 1); + + // No one nominates => no one has a proper vote => no change + assert_eq!(Session::validators(), vec![40, 30, 20, 10]); + }); +} #[test] fn nominating_and_rewards_should_work() { - // TODO: This should be rewritten and tested with the Phragmen algorithm // For now it tests a functionality which somehow overlaps with other tests: // the fact that the nominator is rewarded properly. + // + // PHRAGMEN OUTPUT: running this test with the reference impl gives: + // + // Votes [('2', 500, ['10', '20', '30']), ('4', 500, ['10', '20', '40'])] + // Sequential Phragmén gives + // 10 is elected with stake 500.0 and score 0.001 + // 20 is elected with stake 500.0 and score 0.002 + // + // 2 has load 0.002 and supported + // 10 with stake 250.0 20 with stake 250.0 30 with stake 0.0 + // 4 has load 0.002 and supported + // 10 with stake 250.0 20 with stake 250.0 40 with stake 0.0 + with_externalities(&mut ExtBuilder::default() - .session_length(1).sessions_per_era(1).build(), + .nominate(false) + .validator_pool(true) + .build(), || { - let session_reward = 10; - let initial_balance = 1000; - assert_eq!(Staking::era_length(), 1); - assert_eq!(Staking::validator_count(), 2); - assert_eq!(Staking::bonding_duration(), 3); - assert_eq!(Session::validators(), vec![20, 10]); + // initial validators + assert_eq!(Session::validators(), vec![40, 30, 20, 10]); // Set payee to controller assert_ok!(Staking::set_payee(Origin::signed(10), RewardDestination::Controller)); assert_ok!(Staking::set_payee(Origin::signed(20), RewardDestination::Controller)); // default reward for the first session. + let session_reward = 10; assert_eq!(Staking::current_session_reward(), session_reward); // give the man some money + let initial_balance = 1000; for i in 1..5 { Balances::set_free_balance(&i, initial_balance); } Balances::set_free_balance(&10, initial_balance); Balances::set_free_balance(&20, initial_balance); - - System::set_block_number(1); // record their balances. for i in 1..5 { assert_eq!(Balances::total_balance(&i), initial_balance); } - // bond two account pairs and state interest in nomination. - // NOTE: in the current naive version only the first vote matters and will be chosen anyhow. - - // 2 will nominate for 10, 10 has 1000 in stash, 500 will be 1/3 of the total 1500 + // 2 will nominate for 10, 20, 30 assert_ok!(Staking::bond(Origin::signed(1), 2, 500, RewardDestination::Controller)); - assert_ok!(Staking::nominate(Origin::signed(2), vec![10, 20])); - // 4 will nominate for 20, 20 has 2000 in stash, 500 will be 1/5 of the total 2500 + assert_ok!(Staking::nominate(Origin::signed(2), vec![10, 20, 30])); + // 4 will nominate for 10, 20, 40 assert_ok!(Staking::bond(Origin::signed(3), 4, 500, RewardDestination::Stash)); - assert_ok!(Staking::nominate(Origin::signed(4), vec![20, 10])); + assert_ok!(Staking::nominate(Origin::signed(4), vec![10, 20, 40])); - + System::set_block_number(1); Session::check_rotate_session(System::block_number()); assert_eq!(Staking::current_era(), 1); - // validators will not change, since selection currently is actually not dependent on nomination and votes, only stake. + // 10 and 20 have more votes, they will be chosen by phragmen. assert_eq!(Session::validators(), vec![20, 10]); - // avalidators must have already received some rewards. + // validators must have already received some rewards. assert_eq!(Balances::total_balance(&10), initial_balance + session_reward); assert_eq!(Balances::total_balance(&20), initial_balance + session_reward); + + // ------ check the staked value of all parties. + // total expo of 10, with 500 coming from nominators (externals), according to phragmen. + assert_eq!(Staking::stakers(10).own, 1000); + assert_eq!(Staking::stakers(10).total, 1000 + 500); + // 2 and 4 supported 10, each with stake 250, according to phragmen. + assert_eq!(Staking::stakers(10).others.iter().map(|e| e.value).collect::>>(), vec![250, 250]); + assert_eq!(Staking::stakers(10).others.iter().map(|e| e.who).collect::>>(), vec![4, 2]); + // total expo of 20, with 500 coming from nominators (externals), according to phragmen. + assert_eq!(Staking::stakers(20).own, 2000); + assert_eq!(Staking::stakers(20).total, 2000 + 500); + // 2 and 4 supported 20, each with stake 250, according to phragmen. + assert_eq!(Staking::stakers(20).others.iter().map(|e| e.value).collect::>>(), vec![250, 250]); + assert_eq!(Staking::stakers(20).others.iter().map(|e| e.who).collect::>>(), vec![4, 2]); System::set_block_number(2); // next session reward. let new_session_reward = Staking::session_reward() * Staking::slot_stake(); // nothing else will happen, era ends and rewards are paid again, - // it is expected that nominators will also be paid. + // it is expected that nominators will also be paid. See below Session::check_rotate_session(System::block_number()); + // Nominator 2: has [250/1500 ~ 1/6 from 10] + [250/2500 ~ 1/10 from 20]'s reward. ==> 1/6 + 1/10 + assert_eq!(Balances::total_balance(&2), initial_balance + (new_session_reward/6 + new_session_reward/10)); + // The Associated validator will get the other 4/6 --> 1500(total) minus 1/6(250) by each nominator -> 6/6 - 1/6 - 1/6 + assert_eq!(Balances::total_balance(&10), initial_balance + session_reward + 4*new_session_reward/6) ; - // Nominator 2: staked 1/3 of the total, gets 1/3 of the reward, chose controller as destination - assert_eq!(Balances::total_balance(&2), initial_balance + new_session_reward/3); - // The Associated validator will get the other 2/3 - assert_eq!(Balances::total_balance(&10), initial_balance + session_reward + 2*new_session_reward/3); - - // Nominator 4: staked 1/5 of the total, gets 1/5 of the reward, chose stash as destination - // This means that the reward will go to 3, which is bonded as the stash of 4. - assert_eq!(Balances::total_balance(&3), initial_balance + new_session_reward/5); - // The Associated validator will get the other 4/5 - assert_eq!(Balances::total_balance(&20), initial_balance + session_reward + 4*new_session_reward/5); + // Nominator 4: has [250/1500 ~ 1/6 from 10] + [250/2500 ~ 1/10 from 20]'s reward. ==> 1/6 + 1/10 + // This nominator chose stash as the reward destination. This means that the reward will go to 3, which is bonded as the stash of 4. + assert_eq!(Balances::total_balance(&3), initial_balance + (new_session_reward/6 + new_session_reward/10)); + // The Associated validator will get the other 8/10 --> 2500(total) minus 1/10(250) by each nominator -> 10/10 - 1/10 - 1/10 + assert_eq!(Balances::total_balance(&20), initial_balance + session_reward + 8*new_session_reward/10); }); } #[test] fn nominators_also_get_slashed() { // A nominator should be slashed if the validator they nominated is slashed - with_externalities(&mut ExtBuilder::default() - .session_length(1).sessions_per_era(1).build(), - || { + with_externalities(&mut ExtBuilder::default().nominate(false).build(), || { assert_eq!(Staking::era_length(), 1); assert_eq!(Staking::validator_count(), 2); // slash happens immediately. @@ -508,13 +719,14 @@ fn nominators_also_get_slashed() { // 10 goes offline Staking::on_offline_validator(10, 4); - let slash_value = Staking::current_offline_slash()*8; + let slash_value = 2_u64.pow(3) * Staking::current_offline_slash(); let expo = Staking::stakers(10); let actual_slash = expo.own.min(slash_value); let nominator_actual_slash = nominator_stake.min(expo.total - actual_slash); // initial + first era reward + slash assert_eq!(Balances::total_balance(&10), initial_balance + 10 - actual_slash); assert_eq!(Balances::total_balance(&2), initial_balance - nominator_actual_slash); + // Because slashing happened. assert!(Staking::forcing_new_era().is_some()); }); } @@ -527,7 +739,8 @@ fn double_staking_should_fail() { // * an account already bonded as stash cannot nominate. // * an account already bonded as controller can nominate. with_externalities(&mut ExtBuilder::default() - .session_length(1).sessions_per_era(2).build(), + .sessions_per_era(2) + .build(), || { let arbitrary_value = 5; System::set_block_number(1); @@ -547,10 +760,9 @@ fn double_staking_should_fail() { #[test] fn session_and_eras_work() { with_externalities(&mut ExtBuilder::default() - .session_length(1) .sessions_per_era(2) .reward(10) - .build(), + .build(), || { assert_eq!(Staking::era_length(), 2); assert_eq!(Staking::sessions_per_era(), 2); @@ -626,7 +838,7 @@ fn cannot_transfer_staked_balance() { // Confirm account 11 has some free balance assert_eq!(Balances::free_balance(&11), 1000); // Confirm account 11 (via controller 10) is totally staked - assert_eq!(Staking::stakers(&10).total, 1000); + assert_eq!(Staking::stakers(&10).total, 1000 + 500); // Confirm account 11 cannot transfer as a result assert_noop!(Balances::transfer(Origin::signed(11), 20, 1), "account liquidity restrictions prevent withdrawal"); @@ -646,7 +858,7 @@ fn cannot_reserve_staked_balance() { // Confirm account 11 has some free balance assert_eq!(Balances::free_balance(&11), 1000); // Confirm account 11 (via controller 10) is totally staked - assert_eq!(Staking::stakers(&10).total, 1000); + assert_eq!(Staking::stakers(&10).total, 1000 + 500); // Confirm account 11 cannot transfer as a result assert_noop!(Balances::reserve(&11, 1), "account liquidity restrictions prevent withdrawal"); @@ -657,96 +869,16 @@ fn cannot_reserve_staked_balance() { }); } -#[test] -fn max_unstake_threshold_works() { - // Tests that max_unstake_threshold gets used when prefs.unstake_threshold is large - with_externalities(&mut ExtBuilder::default().build(), || { - const MAX_UNSTAKE_THRESHOLD: u32 = 10; - // Two users with maximum possible balance - Balances::set_free_balance(&10, u64::max_value()); - Balances::set_free_balance(&20, u64::max_value()); - - // Give them full exposer as a staker - >::insert(&10, Exposure { total: u64::max_value(), own: u64::max_value(), others: vec![]}); - >::insert(&20, Exposure { total: u64::max_value(), own: u64::max_value(), others: vec![]}); - - // Check things are initialized correctly - assert_eq!(Balances::free_balance(&10), u64::max_value()); - assert_eq!(Balances::free_balance(&20), u64::max_value()); - assert_eq!(Balances::free_balance(&10), Balances::free_balance(&20)); - assert_eq!(Staking::offline_slash_grace(), 0); - assert_eq!(Staking::current_offline_slash(), 20); - // Account 10 will have max unstake_threshold - assert_ok!(Staking::validate(Origin::signed(10), ValidatorPrefs { - unstake_threshold: MAX_UNSTAKE_THRESHOLD, - validator_payment: 0, - })); - // Account 20 could not set their unstake_threshold past 10 - assert_noop!(Staking::validate(Origin::signed(20), ValidatorPrefs { - unstake_threshold: 11, - validator_payment: 0}), - "unstake threshold too large" - ); - // Give Account 20 unstake_threshold 11 anyway, should still be limited to 10 - >::insert(20, ValidatorPrefs { - unstake_threshold: 11, - validator_payment: 0, - }); - - // Make slot_stake really large, as to not affect punishment curve - >::put(u64::max_value()); - // Confirm `slot_stake` is greater than exponential punishment, else math below will be different - assert!(Staking::slot_stake() > 2_u64.pow(MAX_UNSTAKE_THRESHOLD) * 20); - - // Report each user 1 more than the max_unstake_threshold - Staking::on_offline_validator(10, MAX_UNSTAKE_THRESHOLD as usize + 1); - Staking::on_offline_validator(20, MAX_UNSTAKE_THRESHOLD as usize + 1); - - // Show that each balance only gets reduced by 2^max_unstake_threshold - assert_eq!(Balances::free_balance(&10), u64::max_value() - 2_u64.pow(MAX_UNSTAKE_THRESHOLD) * 20); - assert_eq!(Balances::free_balance(&20), u64::max_value() - 2_u64.pow(MAX_UNSTAKE_THRESHOLD) * 20); - }); -} - -#[test] -fn slashing_does_not_cause_underflow() { - // Tests that slashing more than a user has does not underflow - with_externalities(&mut ExtBuilder::default().build(), || { - // One user with less than `max_value` will test underflow does not occur - Balances::set_free_balance(&10, 1); - - // Verify initial conditions - assert_eq!(Balances::free_balance(&10), 1); - assert_eq!(Staking::offline_slash_grace(), 0); - - // Set validator preference so that 2^unstake_threshold would cause overflow (greater than 64) - >::insert(10, ValidatorPrefs { - unstake_threshold: 10, - validator_payment: 0, - }); - - // Should not panic - Staking::on_offline_validator(10, 100); - // Confirm that underflow has not occurred, and account balance is set to zero - assert_eq!(Balances::free_balance(&10), 0); - }); -} - - #[test] fn reward_destination_works() { // Rewards go to the correct destination as determined in Payee - with_externalities(&mut ExtBuilder::default() - .sessions_per_era(1) - .session_length(1) - .build(), - || { + with_externalities(&mut ExtBuilder::default().build(), || { // Check that account 10 is a validator assert!(>::exists(10)); // Check the balance of the validator account - assert_eq!(Balances::free_balance(&10), 1); + assert_eq!(Balances::free_balance(&10), 1); // Check the balance of the stash account - assert_eq!(Balances::free_balance(&11), 1000); + assert_eq!(Balances::free_balance(&11), 1000); // Check these two accounts are bonded assert_eq!(Staking::bonded(&11), Some(10)); // Check how much is at stake @@ -763,10 +895,12 @@ fn reward_destination_works() { assert_eq!(Staking::payee(&10), RewardDestination::Staked); // Check current session reward is 10 assert_eq!(current_session_reward, 10); - // Check that reward went to the stash account - assert_eq!(Balances::free_balance(&11), 1000 + 10); + // Check that reward went to the stash account of validator + // 1/3 of the reward is for the nominator. + let validator_reward = (10. * (2./3.)) as u64; // = 6 + assert_eq!(Balances::free_balance(&11), 1000 + validator_reward); // Check that amount at stake increased accordingly - assert_eq!(Staking::ledger(&10), Some(StakingLedger { stash: 11, total: 1000 + 10, active: 1000 + 10, unlocking: vec![] })); + assert_eq!(Staking::ledger(&10), Some(StakingLedger { stash: 11, total: 1000 + 6, active: 1000 + 6, unlocking: vec![] })); // Update current session reward current_session_reward = Staking::current_session_reward(); @@ -781,15 +915,18 @@ fn reward_destination_works() { // Check that RewardDestination is Stash assert_eq!(Staking::payee(&10), RewardDestination::Stash); // Check that reward went to the stash account - assert_eq!(Balances::free_balance(&11), 1010 + current_session_reward); + let new_validator_reward = ((1000 + 6) as f64 / ( (1000 + 6) + (500 + 4) ) as f64) * current_session_reward as f64; + assert_eq!(Balances::free_balance(&11), 1000 + validator_reward + new_validator_reward as u64); // Check that amount at stake is not increased - assert_eq!(Staking::ledger(&10), Some(StakingLedger { stash: 11, total: 1010, active: 1010, unlocking: vec![] })); - // Update current session reward - current_session_reward = Staking::current_session_reward(); + assert_eq!(Staking::ledger(&10), Some(StakingLedger { stash: 11, total: 1006, active: 1006, unlocking: vec![] })); //Change RewardDestination to Controller >::insert(&10, RewardDestination::Controller); + // Check controller balance + assert_eq!(Balances::free_balance(&10), 1); + + // Move forward the system for payment System::set_block_number(3); Timestamp::set_timestamp(15); @@ -798,20 +935,22 @@ fn reward_destination_works() { // Check that RewardDestination is Controller assert_eq!(Staking::payee(&10), RewardDestination::Controller); // Check that reward went to the controller account - assert_eq!(Balances::free_balance(&10), 1 + current_session_reward); + let reward_of = |w| Staking::stakers(w).own * Staking::current_session_reward() / Staking::stakers(w).total; + assert_eq!(Balances::free_balance(&10), 1 + reward_of(&10)); // Check that amount at stake is not increased - assert_eq!(Staking::ledger(&10), Some(StakingLedger { stash: 11, total: 1010, active: 1010, unlocking: vec![] })); - + assert_eq!(Staking::ledger(&10), Some(StakingLedger { stash: 11, total: 1006, active: 1006, unlocking: vec![] })); }); - } #[test] fn validator_payment_prefs_work() { // Test that validator preferences are correctly honored - // Note: unstake threshold is being directly tested in slashing tests. + // Note: unstake threshold is being directly tested in slashing tests. // This test will focus on validator payment. - with_externalities(&mut ExtBuilder::default().build(), + with_externalities(&mut ExtBuilder::default() + .session_length(3) + .sessions_per_era(3) + .build(), || { let session_reward = 10; let validator_cut = 5; @@ -826,31 +965,31 @@ fn validator_payment_prefs_work() { assert_eq!(Staking::current_session_reward(), session_reward); // check the balance of a validator accounts. - assert_eq!(Balances::total_balance(&10), 1); + assert_eq!(Balances::total_balance(&10), 1); // check the balance of a validator's stash accounts. - assert_eq!(Balances::total_balance(&11), validator_initial_balance); + assert_eq!(Balances::total_balance(&11), validator_initial_balance); // and the nominator (to-be) - assert_eq!(Balances::total_balance(&2), 20); + assert_eq!(Balances::total_balance(&2), 20); // add a dummy nominator. // NOTE: this nominator is being added 'manually', use '.nominate()' to do it realistically. >::insert(&10, Exposure { own: 500, // equal division indicates that the reward will be equally divided among validator and nominator. total: 1000, - others: vec![IndividualExposure {who: 2, value: 500 }] + others: vec![IndividualExposure {who: 2, value: 500 }] }); >::insert(&2, RewardDestination::Controller); - >::insert(&10, ValidatorPrefs { - unstake_threshold: 3, - validator_payment: validator_cut + >::insert(&10, ValidatorPrefs { + unstake_threshold: 3, + validator_payment: validator_cut }); - // ------------ Fast forward + // ------------ Fast forward let mut block = 3; // Block 3 => Session 1 => Era 0 System::set_block_number(block); Timestamp::set_timestamp(block*5); // on time. - Session::check_rotate_session(System::block_number()); + Session::check_rotate_session(System::block_number()); assert_eq!(Staking::current_era(), 0); assert_eq!(Session::current_index(), 1); @@ -858,7 +997,7 @@ fn validator_payment_prefs_work() { assert_eq!(Staking::current_session_reward(), session_reward); assert_eq!(Staking::current_era_reward(), session_reward); - block = 6; // Block 6 => Session 2 => Era 0 + block = 6; // Block 6 => Session 2 => Era 0 System::set_block_number(block); Timestamp::set_timestamp(block*5); // a little late. Session::check_rotate_session(System::block_number()); @@ -870,7 +1009,7 @@ fn validator_payment_prefs_work() { block = 9; // Block 9 => Session 3 => Era 1 System::set_block_number(block); - Timestamp::set_timestamp(block*5); + Timestamp::set_timestamp(block*5); Session::check_rotate_session(System::block_number()); assert_eq!(Staking::current_era(), 1); assert_eq!(Session::current_index(), 3); @@ -884,22 +1023,14 @@ fn validator_payment_prefs_work() { // Rest of the reward will be shared and paid to the nominator in stake. assert_eq!(Balances::total_balance(&2), 20 + shared_cut/2); }); -} -#[test] -fn staking_ledger_grows_and_shrinks() { - // TODO: Show that staking ledger grows with new events - // TODO: Show that staking ledger shrinks when user is removed -} - -#[test] -fn consolidate_unlocked_works() { - // TODO: Figure out what it does and then test it } #[test] fn bond_extra_works() { // Tests that extra `free_balance` in the stash can be added to stake + // NOTE: this tests only verifies `StakingLedger` for correct updates. + // See `bond_extra_and_withdraw_unbonded_works` for more details and updates on `Exposure`. with_externalities(&mut ExtBuilder::default().build(), || { // Check that account 10 is a validator @@ -928,34 +1059,112 @@ fn bond_extra_works() { } #[test] -fn withdraw_unbonded_works() { - // TODO: Learn what it is and test it -} +fn bond_extra_and_withdraw_unbonded_works() { + // * Should test + // * Given an account being bonded [and chosen as a validator](not mandatory) + // * It can add extra funds to the bonded account. + // * it can unbond a portion of its funds from the stash account. + // * Once the unbonding period is done, it can actually take the funds out of the stash. + with_externalities(&mut ExtBuilder::default() + .reward(10) // it is the default, just for verbosity + .nominate(false) + .build(), + || { + // Set payee to controller. avoids confusion + assert_ok!(Staking::set_payee(Origin::signed(10), RewardDestination::Controller)); -#[test] -fn reporting_misbehaviors_work() { - // TODO: Does this code exist? -} + // Set unbonding era (bonding_duration) to 2 + assert_ok!(Staking::set_bonding_duration(2)); -#[test] -fn correct_number_of_validators_are_chosen() { - // TODO: Check that number is at least minimum, and at most what is set - // TODO: Test emergency conditions? -} + // Give account 11 some large free balance greater than total + Balances::set_free_balance(&11, 1000000); + // Check the balance of the stash account + assert_eq!(Balances::free_balance(&11), 1000000); + // Initial config should be correct + assert_eq!(Staking::sessions_per_era(), 1); + assert_eq!(Staking::current_era(), 0); + assert_eq!(Session::current_index(), 0); + + assert_eq!(Staking::current_session_reward(), 10); + + // check the balance of a validator accounts. + assert_eq!(Balances::total_balance(&10), 1); + + // confirm that 10 is a normal validator and gets paid at the end of the era. + System::set_block_number(1); + Timestamp::set_timestamp(5); + Session::check_rotate_session(System::block_number()); + assert_eq!(Staking::current_era(), 1); + assert_eq!(Session::current_index(), 1); + + // NOTE: despite having .nominate() in extBuilder, 20 doesn't have a share since + // rewards are paid before election in new_era() + assert_eq!(Balances::total_balance(&10), 1 + 10); + + // Initial state of 10 + assert_eq!(Staking::ledger(&10), Some(StakingLedger { stash: 11, total: 1000, active: 1000, unlocking: vec![] })); + assert_eq!(Staking::stakers(&10), Exposure { total: 1000, own: 1000, others: vec![] }); + + + // deposit the extra 100 units + Staking::bond_extra(Origin::signed(10), 100).unwrap(); + + assert_eq!(Staking::ledger(&10), Some(StakingLedger { stash: 11, total: 1000 + 100, active: 1000 + 100, unlocking: vec![] })); + // Exposure is a snapshot! only updated after the next era update. + assert_ne!(Staking::stakers(&10), Exposure { total: 1000 + 100, own: 1000 + 100, others: vec![] }); + + // trigger next era. + System::set_block_number(2);Timestamp::set_timestamp(10);Session::check_rotate_session(System::block_number()); + assert_eq!(Staking::current_era(), 2); + assert_eq!(Session::current_index(), 2); + + // ledger should be the same. + assert_eq!(Staking::ledger(&10), Some(StakingLedger { stash: 11, total: 1000 + 100, active: 1000 + 100, unlocking: vec![] })); + // Exposure is now updated. + assert_eq!(Staking::stakers(&10), Exposure { total: 1000 + 100, own: 1000 + 100, others: vec![] }); + // Note that by this point 10 also have received more rewards, but we don't care now. + // assert_eq!(Balances::total_balance(&10), 1 + 10 + MORE_REWARD); + + // Unbond almost all of the funds in stash. + Staking::unbond(Origin::signed(10), 1000).unwrap(); + assert_eq!(Staking::ledger(&10), Some(StakingLedger { + stash: 11, total: 1000 + 100, active: 100, unlocking: vec![UnlockChunk{ value: 1000, era: 2 + 2}] })); + + // Attempting to free the balances now will fail. 2 eras need to pass. + Staking::withdraw_unbonded(Origin::signed(10)).unwrap(); + assert_eq!(Staking::ledger(&10), Some(StakingLedger { + stash: 11, total: 1000 + 100, active: 100, unlocking: vec![UnlockChunk{ value: 1000, era: 2 + 2}] })); + + // trigger next era. + System::set_block_number(3);Timestamp::set_timestamp(15);Session::check_rotate_session(System::block_number()); + assert_eq!(Staking::current_era(), 3); + assert_eq!(Session::current_index(), 3); + + // nothing yet + Staking::withdraw_unbonded(Origin::signed(10)).unwrap(); + assert_eq!(Staking::ledger(&10), Some(StakingLedger { + stash: 11, total: 1000 + 100, active: 100, unlocking: vec![UnlockChunk{ value: 1000, era: 2 + 2}] })); + + // trigger next era. + System::set_block_number(4);Timestamp::set_timestamp(20);Session::check_rotate_session(System::block_number()); + assert_eq!(Staking::current_era(), 4); + assert_eq!(Session::current_index(), 4); + + Staking::withdraw_unbonded(Origin::signed(10)).unwrap(); + // Now the value is free and the staking ledger is updated. + assert_eq!(Staking::ledger(&10), Some(StakingLedger { + stash: 11, total: 100, active: 100, unlocking: vec![] })); + }) +} #[test] fn slot_stake_is_least_staked_validator_and_limits_maximum_punishment() { - // TODO: Complete this test! // Test that slot_stake is determined by the least staked validator // Test that slot_stake is the maximum punishment that can happen to a validator // Note that rewardDestination is the stash account by default // Note that unlike reward slash will affect free_balance, not the stash account. - with_externalities(&mut ExtBuilder::default() - .session_length(1) - .sessions_per_era(1) - .build(), - || { + with_externalities(&mut ExtBuilder::default().nominate(false).build(), || { // Confirm validator count is 2 assert_eq!(Staking::validator_count(), 2); // Confirm account 10 and 20 are validators @@ -981,7 +1190,7 @@ fn slot_stake_is_least_staked_validator_and_limits_maximum_punishment() { assert_eq!(Staking::stakers(&20).total, 69); >::insert(&20, StakingLedger { stash: 22, total: 69, active: 69, unlocking: vec![] }); - // New era --> rewards are paid --> stakes are changed + // New era --> rewards are paid --> stakes are changed System::set_block_number(1); Timestamp::set_timestamp(5); Session::check_rotate_session(System::block_number()); @@ -991,7 +1200,7 @@ fn slot_stake_is_least_staked_validator_and_limits_maximum_punishment() { assert_eq!(Staking::stakers(&10).total, 1000 + 10); assert_eq!(Staking::stakers(&20).total, 69 + 10); - // -- Note that rewards are going drectly to stash, not as free balance. + // -- Note that rewards are going directly to stash, not as free balance. assert_eq!(Balances::free_balance(&10), 1000); assert_eq!(Balances::free_balance(&20), 1000); @@ -1008,7 +1217,6 @@ fn slot_stake_is_least_staked_validator_and_limits_maximum_punishment() { }); } - #[test] fn on_free_balance_zero_stash_removes_validator() { // Tests that validator storage items are cleaned up when stash is empty @@ -1020,9 +1228,9 @@ fn on_free_balance_zero_stash_removes_validator() { // Check that account 10 is a validator assert!(>::exists(10)); // Check the balance of the validator account - assert_eq!(Balances::free_balance(&10), 256); + assert_eq!(Balances::free_balance(&10), 256); // Check the balance of the stash account - assert_eq!(Balances::free_balance(&11), 256000); + assert_eq!(Balances::free_balance(&11), 256000); // Check these two accounts are bonded assert_eq!(Staking::bonded(&11), Some(10)); @@ -1041,10 +1249,10 @@ fn on_free_balance_zero_stash_removes_validator() { // Reduce free_balance of controller to 0 Balances::set_free_balance(&10, 0); // Check total balance of account 10 - assert_eq!(Balances::total_balance(&10), 0); + assert_eq!(Balances::total_balance(&10), 0); // Check the balance of the stash account has not been touched - assert_eq!(Balances::free_balance(&11), 256000); + assert_eq!(Balances::free_balance(&11), 256000); // Check these two accounts are still bonded assert_eq!(Staking::bonded(&11), Some(10)); @@ -1057,7 +1265,7 @@ fn on_free_balance_zero_stash_removes_validator() { // Reduce free_balance of stash to 0 Balances::set_free_balance(&11, 0); // Check total balance of stash - assert_eq!(Balances::total_balance(&11), 0); + assert_eq!(Balances::total_balance(&11), 0); // Check storage items do not exist assert!(!>::exists(&10)); @@ -1082,9 +1290,9 @@ fn on_free_balance_zero_stash_removes_nominator() { // Check that account 10 is a nominator assert!(>::exists(10)); // Check the balance of the nominator account - assert_eq!(Balances::free_balance(&10), 256); + assert_eq!(Balances::free_balance(&10), 256); // Check the balance of the stash account - assert_eq!(Balances::free_balance(&11), 256000); + assert_eq!(Balances::free_balance(&11), 256000); // Check these two accounts are bonded assert_eq!(Staking::bonded(&11), Some(10)); @@ -1100,10 +1308,10 @@ fn on_free_balance_zero_stash_removes_nominator() { // Reduce free_balance of controller to 0 Balances::set_free_balance(&10, 0); // Check total balance of account 10 - assert_eq!(Balances::total_balance(&10), 0); + assert_eq!(Balances::total_balance(&10), 0); // Check the balance of the stash account has not been touched - assert_eq!(Balances::free_balance(&11), 256000); + assert_eq!(Balances::free_balance(&11), 256000); // Check these two accounts are still bonded assert_eq!(Staking::bonded(&11), Some(10)); @@ -1115,7 +1323,7 @@ fn on_free_balance_zero_stash_removes_nominator() { // Reduce free_balance of stash to 0 Balances::set_free_balance(&11, 0); // Check total balance of stash - assert_eq!(Balances::total_balance(&11), 0); + assert_eq!(Balances::total_balance(&11), 0); // Check storage items do not exist assert!(!>::exists(&10)); @@ -1125,4 +1333,253 @@ fn on_free_balance_zero_stash_removes_nominator() { assert!(!>::exists(&10)); assert!(!>::exists(&11)); }); -} \ No newline at end of file +} + +#[test] +fn phragmen_poc_works() { + // Tests the POC test of the phragmen, mentioned in the paper and reference implementation. + // Initial votes: + // vote_list = [ + // ("A", 10.0, ["X", "Y"]), + // ("B", 20.0, ["X", "Z"]), + // ("C", 30.0, ["Y", "Z"]) + // ] + // + // Sequential Phragmén gives + // Z is elected with stake 35.0 and score 0.02 + // Y is elected with stake 25.0 and score 0.04 + // + // A has load 0.04 and supported + // X with stake 0.0 Y with stake 10.0 + // B has load 0.02 and supported + // X with stake 0.0 Z with stake 20.0 + // C has load 0.04 and supported + // Y with stake 15.0 Z with stake 15.0 + // + // NOTE: doesn't X/Y/Z's stash value make a difference here in phragmen? + with_externalities(&mut ExtBuilder::default() + .nominate(false) + .build(), + || { + // initial setup of 10 and 20, both validators. + assert_eq!(Session::validators(), vec![20, 10]); + + assert_eq!(Staking::ledger(&10), Some(StakingLedger { stash: 11, total: 1000, active: 1000, unlocking: vec![] })); + assert_eq!(Staking::ledger(&20), Some(StakingLedger { stash: 21, total: 2000, active: 2000, unlocking: vec![] })); + + assert_eq!(Staking::validators(10), ValidatorPrefs::default()); + assert_eq!(Staking::validators(20), ValidatorPrefs::default()); + + assert_eq!(Balances::free_balance(10), 1); + assert_eq!(Balances::free_balance(20), 1); + + // no one is a nominator + assert_eq!(>::enumerate().count(), 0 as usize); + + // Bond [30, 31] as the third validator + assert_ok!(Staking::bond(Origin::signed(31), 30, 1000, RewardDestination::default())); + assert_ok!(Staking::validate(Origin::signed(30), ValidatorPrefs::default())); + + // bond [2,1](A), [4,3](B), [6,5](C) as the 3 nominators + // Give all of them some balance to be able to bond properly. + for i in &[1, 3, 5] { Balances::set_free_balance(i, 50); } + // Linking names to the above test: + // 10 => X + // 20 => Y + // 30 => Z + assert_ok!(Staking::bond(Origin::signed(1), 2, 10, RewardDestination::default())); + assert_ok!(Staking::nominate(Origin::signed(2), vec![10, 20])); + + assert_ok!(Staking::bond(Origin::signed(3), 4, 20, RewardDestination::default())); + assert_ok!(Staking::nominate(Origin::signed(4), vec![10, 30])); + + assert_ok!(Staking::bond(Origin::signed(5), 6, 30, RewardDestination::default())); + assert_ok!(Staking::nominate(Origin::signed(6), vec![20, 30])); + + // New era => election algorithm will trigger + System::set_block_number(1); + Session::check_rotate_session(System::block_number()); + + // Z and Y are chosen + assert_eq!(Session::validators(), vec![30, 20]); + + // with stake 35 and 25 respectively + + // This is only because 30 has been bonded on the fly, exposures are stored at the very end of the era. + // 35 is the point, not 'own' Exposure. + assert_eq!(Staking::stakers(30).own, 0); + assert_eq!(Staking::stakers(30).total, 0 + 35); + // same as above. +25 is the point + assert_eq!(Staking::stakers(20).own, 2010); + assert_eq!(Staking::stakers(20).total, 2010 + 25); + + // 30(Z) was supported by B-4 and C-6 with stake 20 and 15 respectively. + assert_eq!(Staking::stakers(30).others.iter().map(|e| e.value).collect::>>(), vec![15, 20]); + assert_eq!(Staking::stakers(30).others.iter().map(|e| e.who).collect::>>(), vec![6, 4]); + + // 20(Y) was supported by A-2 and C-6 with stake 10 and 15 respectively. + assert_eq!(Staking::stakers(20).others.iter().map(|e| e.value).collect::>>(), vec![15, 10]); + assert_eq!(Staking::stakers(20).others.iter().map(|e| e.who).collect::>>(), vec![6, 2]); + }); +} + +#[test] +fn phragmen_election_works() { + // tests the encapsulated phragmen::elect function. + with_externalities(&mut ExtBuilder::default().nominate(false).build(), || { + // initial setup of 10 and 20, both validators + assert_eq!(Session::validators(), vec![20, 10]); + + // no one is a nominator + assert_eq!(>::enumerate().count(), 0 as usize); + + // Bond [30, 31] as the third validator + assert_ok!(Staking::bond(Origin::signed(31), 30, 1000, RewardDestination::default())); + assert_ok!(Staking::validate(Origin::signed(30), ValidatorPrefs::default())); + + // bond [2,1](A), [4,3](B), as 2 nominators + // Give all of them some balance to be able to bond properly. + for i in &[1, 3] { Balances::set_free_balance(i, 50); } + assert_ok!(Staking::bond(Origin::signed(1), 2, 5, RewardDestination::default())); + assert_ok!(Staking::nominate(Origin::signed(2), vec![10, 20])); + + assert_ok!(Staking::bond(Origin::signed(3), 4, 45, RewardDestination::default())); + assert_ok!(Staking::nominate(Origin::signed(4), vec![10, 30])); + + let rounds = || 2 as usize; + let validators = || >::enumerate(); + let nominators = || >::enumerate(); + let stash_of = |w| Staking::stash_balance(&w); + let min_validator_count = Staking::minimum_validator_count() as usize; + + let winners = phragmen::elect::( + rounds, + validators, + nominators, + stash_of, + min_validator_count + ); + + // 10 and 30 must be the winners + assert_eq!(winners.iter().map(|w| w.who).collect::>>(), vec![10, 30]); + + let winner_10 = winners.iter().filter(|w| w.who == 10).nth(0).unwrap(); + let winner_30 = winners.iter().filter(|w| w.who == 30).nth(0).unwrap(); + + // python implementation output: + /* + 10 is elected with stake 26.31578947368421 and score 0.02 + 30 is elected with stake 23.684210526315788 and score 0.042222222222222223 + + 2 has load 0.02 and supported + 10 with stake 5.0 20 with stake 0.0 + 4 has load 0.042222222222222223 and supported + 10 with stake 21.31578947368421 30 with stake 23.684210526315788 + */ + + assert_eq!(winner_10.exposure.total, 1000 + 26); + assert_eq!(winner_10.score, Perquintill::from_fraction(0.02)); + assert_eq!(winner_10.exposure.others[0].value, 21); + assert_eq!(winner_10.exposure.others[1].value, 5); + + assert_eq!(winner_30.exposure.total, 23); + assert_eq!(winner_30.score, Perquintill::from_quintillionths(42222222222222222)); + assert_eq!(winner_30.exposure.others[0].value, 23); + }) +} + +#[test] +fn switching_roles() { + // Show: It should be possible to switch between roles (nominator, validator, idle) with minimal overhead. + with_externalities(&mut ExtBuilder::default() + .nominate(false) + .sessions_per_era(3) + .build(), + || { + assert_eq!(Session::validators(), vec![20, 10]); + + // put some money in account that we'll use. + for i in 1..7 { Balances::set_free_balance(&i, 5000); } + + // add 2 nominators + assert_ok!(Staking::bond(Origin::signed(1), 2, 2000, RewardDestination::default())); + assert_ok!(Staking::nominate(Origin::signed(2), vec![10, 6])); + + assert_ok!(Staking::bond(Origin::signed(3), 4, 500, RewardDestination::default())); + assert_ok!(Staking::nominate(Origin::signed(4), vec![20, 2])); + + // add a new validator candidate + assert_ok!(Staking::bond(Origin::signed(5), 6, 1500, RewardDestination::Controller)); + assert_ok!(Staking::validate(Origin::signed(6), ValidatorPrefs::default())); + + // new block + System::set_block_number(1); + Session::check_rotate_session(System::block_number()); + + // no change + assert_eq!(Session::validators(), vec![20, 10]); + + // new block + System::set_block_number(2); + Session::check_rotate_session(System::block_number()); + + // no change + assert_eq!(Session::validators(), vec![20, 10]); + + // new block --> ne era --> new validators + System::set_block_number(3); + Session::check_rotate_session(System::block_number()); + + // with current nominators 10 and 4 have the most stake + assert_eq!(Session::validators(), vec![6, 10]); + + // 2 decides to be a validator. Consequences: + // 6 will not be chosen in the next round (no votes) + // 2 itself will be chosen + 20 who now has the higher votes + // 10 wil have no votes. + assert_ok!(Staking::validate(Origin::signed(2), ValidatorPrefs::default())); + + System::set_block_number(4); + Session::check_rotate_session(System::block_number()); + assert_eq!(Session::validators(), vec![6, 10]); + + System::set_block_number(5); + Session::check_rotate_session(System::block_number()); + assert_eq!(Session::validators(), vec![6, 10]); + + // ne era + System::set_block_number(6); + Session::check_rotate_session(System::block_number()); + assert_eq!(Session::validators(), vec![2, 20]); + }); +} + +#[test] +fn wrong_vote_is_null() { + with_externalities(&mut ExtBuilder::default() + .session_length(1) + .sessions_per_era(1) + .nominate(false) + .validator_pool(true) + .build(), + || { + // from the first era onward, only two will be chosen + assert_eq!(Session::validators(), vec![40, 30, 20, 10]); + + // put some money in account that we'll use. + for i in 1..3 { Balances::set_free_balance(&i, 5000); } + + // add 1 nominators + assert_ok!(Staking::bond(Origin::signed(1), 2, 2000, RewardDestination::default())); + assert_ok!(Staking::nominate(Origin::signed(2), vec![ + 10, 20, // good votes + 1, 2, 15, 1000, 25 // crap votes. No effect. + ])); + + // new block + System::set_block_number(1); + Session::check_rotate_session(System::block_number()); + + assert_eq!(Session::validators(), vec![20, 10]); + }); +}