Staking rate targeting and specific rewards. (#2882)

* PNPoS implementation

* wip: change staking api

* code readibility

* fix overflow

* comment

* license

* doc

* reorganize a bit

* rename to proper english + doc

* address comments

* refactor unused mock

* fix add_point

* update tests

* add not equalize to ci

* Revert "add not equalize to ci"

This reverts commit 1c61c25874b04b1cdfadd7d353b75a189a59ad35.

* bring test back

* update locks

* fix genesis config

* add authorship event handler + test

* uncouple timestamp from staking

* use on finalize instead

* remove todo

* simplify mock

* address comment

* doc

* merge test

* fmt

* remove todo todo

* move add_reward_point to regular function

* doc

* doc

* increase version

* doc and fmt

* Update srml/staking/src/inflation.rs

Co-Authored-By: Bastian Köcher <bkchr@users.noreply.github.com>

* Fix some doc typos
This commit is contained in:
thiolliere
2019-07-24 19:25:43 +02:00
committed by Gavin Wood
parent 1c346d2190
commit 2b6f664caf
11 changed files with 711 additions and 206 deletions
+140 -37
View File
@@ -167,20 +167,30 @@
//!
//! ### Reward Calculation
//!
//! Rewards are recorded **per-session** and paid **per-era**. The value of the reward for each
//! session is calculated at the end of the session based on the timeliness of the session, then
//! accumulated to be paid later. The value of the new _per-session-reward_ is calculated at the end
//! of each era by multiplying `SlotStake` and `SessionReward` (`SessionReward` is the
//! multiplication factor, represented by a number between 0 and 1). Once a new era is triggered,
//! rewards are paid to the validators and their associated nominators.
//! Validators and nominators are rewarded at the end of each era. The total reward of an era is
//! calculated using the era duration and the staking rate (the total amount of tokens staked by
//! nominators and validators, divided by the total token supply). It aims to incentivise toward a
//! defined staking rate. The full specification can be found
//! [here](https://research.web3.foundation/en/latest/polkadot/Token%20Economics/#inflation-model).
//!
//! Total reward is split among validators and their nominators depending on the number of points
//! they received during the era. Points are added to a validator using
//! [`add_reward_points_to_validator`](./enum.Call.html#variant.add_reward_points_to_validator).
//!
//! [`Module`](./struct.Module.html) implements
//! [`authorship::EventHandler`](../srml_authorship/trait.EventHandler.html) to add reward points
//! to block producer and block producer of referenced uncles.
//!
//! The validator and its nominator split their reward as following:
//!
//! The validator can declare an amount, named
//! [`validator_payment`](./struct.ValidatorPrefs.html#structfield.validator_payment), that does not
//! get shared with the nominators at each reward payout through its
//! [`ValidatorPrefs`](./struct.ValidatorPrefs.html). This value gets deducted from the total reward
//! that can be paid. The remaining portion is split among the validator and all of the nominators
//! that nominated the validator, proportional to the value staked behind this validator (_i.e._
//! dividing the [`own`](./struct.Exposure.html#structfield.own) or
//! that is paid to the validator and its nominators. The remaining portion is split among the
//! validator and all of the nominators that nominated the validator, proportional to the value
//! staked behind this validator (_i.e._ dividing the
//! [`own`](./struct.Exposure.html#structfield.own) or
//! [`others`](./struct.Exposure.html#structfield.others) by
//! [`total`](./struct.Exposure.html#structfield.total) in [`Exposure`](./struct.Exposure.html)).
//!
@@ -266,6 +276,7 @@ mod mock;
mod tests;
mod phragmen;
mod inflation;
#[cfg(all(feature = "bench", test))]
mod benches;
@@ -278,13 +289,14 @@ use srml_support::{
StorageValue, StorageMap, EnumerableStorageMap, decl_module, decl_event,
decl_storage, ensure, traits::{
Currency, OnFreeBalanceZero, OnDilution, LockIdentifier, LockableCurrency,
WithdrawReasons, WithdrawReason, OnUnbalanced, Imbalance, Get
WithdrawReasons, WithdrawReason, OnUnbalanced, Imbalance, Get, Time
}
};
use session::{historical::OnSessionEnding, SelectInitialValidators, SessionIndex};
use primitives::Perbill;
use primitives::traits::{
Convert, Zero, One, StaticLookup, CheckedSub, CheckedShl, Saturating, Bounded,
SaturatedConversion, SimpleArithmetic
};
#[cfg(feature = "std")]
use primitives::{Serialize, Deserialize};
@@ -302,6 +314,16 @@ const STAKING_ID: LockIdentifier = *b"staking ";
/// Counter for the number of eras that have passed.
pub type EraIndex = u32;
/// Reward points of an era. Used to split era total payout between validators.
#[derive(Encode, Decode, Default)]
pub struct EraRewards {
/// Total number of points. Equals the sum of reward points for each validator.
total: u32,
/// Reward at one index correspond to reward for validator in current_elected of this index.
/// Thus this reward vec is only valid for one elected set.
rewards: Vec<u32>,
}
/// Indicates the initial status of the staker.
#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))]
pub enum StakerStatus<AccountId> {
@@ -434,6 +456,7 @@ type PositiveImbalanceOf<T> =
<<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::PositiveImbalance;
type NegativeImbalanceOf<T> =
<<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::NegativeImbalance;
type MomentOf<T>= <<T as Trait>::Time as Time>::Moment;
type RawAssignment<T> = (<T as system::Trait>::AccountId, ExtendedBalance);
type Assignment<T> = (<T as system::Trait>::AccountId, ExtendedBalance, BalanceOf<T>);
@@ -485,6 +508,9 @@ pub trait Trait: system::Trait {
/// The staking balance.
type Currency: LockableCurrency<Self::AccountId, Moment=Self::BlockNumber>;
/// Time used for computing era duration.
type Time: Time;
/// Convert a balance into a number used for election calculation.
/// This must fit into a `u64` but is allowed to be sensibly lossy.
/// TODO: #1377
@@ -522,8 +548,6 @@ decl_storage! {
/// Minimum number of staking participants before emergency conditions are imposed.
pub MinimumValidatorCount get(minimum_validator_count) config():
u32 = DEFAULT_MINIMUM_VALIDATOR_COUNT;
/// Maximum reward, per validator, that is provided per acceptable session.
pub SessionReward get(session_reward) config(): Perbill = Perbill::from_parts(60);
/// Slash, per validator that is taken for the first time they are found to be offline.
pub OfflineSlash get(offline_slash) config(): Perbill = Perbill::from_millionths(1000);
/// Number of instances of offline reports before slashing begins for validators.
@@ -561,12 +585,11 @@ decl_storage! {
/// The current era index.
pub CurrentEra get(current_era) config(): EraIndex;
/// Maximum reward, per validator, that is provided per acceptable session.
pub CurrentSessionReward get(current_session_reward) config(): BalanceOf<T>;
/// The start of the current era.
pub CurrentEraStart get(current_era_start): MomentOf<T>;
/// The accumulated reward for the current era. Reset to zero at the beginning of the era
/// and increased for every successfully finished session.
pub CurrentEraReward get(current_era_reward): BalanceOf<T>;
/// Rewards for the current era. Using indices of current elected set.
pub CurrentEraRewards: EraRewards;
/// The amount of balance actively at stake for each validator slot, currently.
///
@@ -650,6 +673,13 @@ decl_module! {
fn deposit_event<T>() = default;
fn on_finalize() {
// Set the start of the first era.
if !<CurrentEraStart<T>>::exists() {
<CurrentEraStart<T>>::put(T::Time::now());
}
}
/// Take the origin account as a stash and lock up `value` of its balance. `controller` will
/// be the account that controls it.
///
@@ -1040,7 +1070,7 @@ impl<T: Trait> Module<T> {
/// Reward a given validator by a specific amount. Add the reward to the validator's, and its
/// nominators' balance, pro-rata based on their exposure, after having removed the validator's
/// pre-payout cut.
fn reward_validator(stash: &T::AccountId, reward: BalanceOf<T>) {
fn reward_validator(stash: &T::AccountId, reward: BalanceOf<T>) -> PositiveImbalanceOf<T> {
let off_the_table = reward.min(Self::validators(stash).validator_payment);
let reward = reward - off_the_table;
let mut imbalance = <PositiveImbalanceOf<T>>::zero();
@@ -1058,8 +1088,10 @@ impl<T: Trait> Module<T> {
let per_u64 = Perbill::from_rational_approximation(exposure.own, total);
per_u64 * reward
};
imbalance.maybe_subsume(Self::make_payout(stash, validator_cut + off_the_table));
T::Reward::on_unbalanced(imbalance);
imbalance
}
/// Session has just ended. Provide the validator set for the next session if it's an era-end, along
@@ -1067,10 +1099,6 @@ impl<T: Trait> Module<T> {
fn new_session(session_index: SessionIndex)
-> Option<(Vec<T::AccountId>, Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)>)>
{
// accumulate good session reward
let reward = Self::current_session_reward();
<CurrentEraReward<T>>::mutate(|r| *r += reward);
if ForceNewEra::take() || session_index % T::SessionsPerEra::get() == 0 {
let validators = T::SessionInterface::validators();
let prior = validators.into_iter()
@@ -1089,18 +1117,39 @@ impl<T: Trait> Module<T> {
/// get a chance to set their session keys.
fn new_era(start_session_index: SessionIndex) -> Option<Vec<T::AccountId>> {
// Payout
let reward = <CurrentEraReward<T>>::take();
if !reward.is_zero() {
let rewards = CurrentEraRewards::take();
let now = T::Time::now();
let previous_era_start = <CurrentEraStart<T>>::mutate(|v| {
rstd::mem::replace(v, now.clone())
});
let era_duration = now - previous_era_start;
if !era_duration.is_zero() {
let validators = Self::current_elected();
for v in validators.iter() {
Self::reward_validator(v, reward);
let validator_len: BalanceOf<T> = (validators.len() as u32).into();
let total_rewarded_stake = Self::slot_stake() * validator_len;
let total_payout = inflation::compute_total_payout(
total_rewarded_stake.clone(),
T::Currency::total_issuance(),
// Era of duration more than u32::MAX is rewarded as u32::MAX.
<BalanceOf<T>>::from(era_duration.saturated_into::<u32>()),
);
let mut total_imbalance = <PositiveImbalanceOf<T>>::zero();
let total_points = rewards.total;
for (v, points) in validators.iter().zip(rewards.rewards.into_iter()) {
if points != 0 {
let reward = multiply_by_rational(total_payout, points, total_points);
total_imbalance.subsume(Self::reward_validator(v, reward));
}
}
Self::deposit_event(RawEvent::Reward(reward));
let len = validators.len() as u32; // validators length can never overflow u64
let len: BalanceOf<T> = len.into();
let total_minted = reward * len;
let total_rewarded_stake = Self::slot_stake() * len;
T::OnRewardMinted::on_dilution(total_minted, total_rewarded_stake);
let total_reward = total_imbalance.peek();
Self::deposit_event(RawEvent::Reward(total_reward));
T::Reward::on_unbalanced(total_imbalance);
T::OnRewardMinted::on_dilution(total_reward, total_rewarded_stake);
}
// Increment current era.
@@ -1126,10 +1175,7 @@ impl<T: Trait> Module<T> {
}
// Reassign all Stakers.
let (slot_stake, maybe_new_validators) = Self::select_validators();
// Update the balances for rewarding according to the stakes.
<CurrentSessionReward<T>>::put(Self::session_reward() * slot_stake);
let (_slot_stake, maybe_new_validators) = Self::select_validators();
maybe_new_validators
}
@@ -1331,6 +1377,24 @@ impl<T: Trait> Module<T> {
Self::deposit_event(event);
}
}
/// Add reward points to validator.
///
/// At the end of the era each the total payout will be distributed among validator
/// relatively to their points.
fn add_reward_points_to_validator(validator: T::AccountId, points: u32) {
<Module<T>>::current_elected().iter()
.position(|elected| *elected == validator)
.map(|index| {
CurrentEraRewards::mutate(|rewards| {
if let Some(new_total) = rewards.total.checked_add(points) {
rewards.total = new_total;
rewards.rewards.resize((index + 1).max(rewards.rewards.len()), 0);
rewards.rewards[index] += points; // Addition is less than total
}
});
});
}
}
impl<T: Trait> session::OnSessionEnding<T::AccountId> for Module<T> {
@@ -1353,6 +1417,45 @@ impl<T: Trait> OnFreeBalanceZero<T::AccountId> for Module<T> {
}
}
/// Add reward points to block authors:
/// * 20 points to the block producer for producing a (non-uncle) block in the relay chain,
/// * 2 points to the block producer for each reference to a previously unreferenced uncle, and
/// * 1 point to the producer of each referenced uncle block.
impl<T: Trait + authorship::Trait> authorship::EventHandler<T::AccountId, T::BlockNumber> for Module<T> {
fn note_author(author: T::AccountId) {
Self::add_reward_points_to_validator(author, 20);
}
fn note_uncle(author: T::AccountId, _age: T::BlockNumber) {
Self::add_reward_points_to_validator(<authorship::Module<T>>::author(), 2);
Self::add_reward_points_to_validator(author, 1);
}
}
// This is guarantee not to overflow on whatever values.
// `num` must be inferior to `den` otherwise it will be reduce to `den`.
fn multiply_by_rational<N>(value: N, num: u32, den: u32) -> N
where N: SimpleArithmetic + Clone
{
let num = num.min(den);
let result_divisor_part = value.clone() / den.into() * num.into();
let result_remainder_part = {
let rem = value % den.into();
// Fits into u32 because den is u32 and remainder < den
let rem_u32 = rem.saturated_into::<u32>();
// Multiplication fits into u64 as both term are u32
let rem_part = rem_u32 as u64 * num as u64 / den as u64;
// Result fits into u32 as num < total_points
(rem_part as u32).into()
};
result_divisor_part + result_remainder_part
}
/// A `Convert` implementation that finds the stash of the given controller account,
/// if any.
pub struct StashOf<T>(rstd::marker::PhantomData<T>);