Simple Staking Payouts (#5406)

* Simple Payouts

* explicit test for out of order claiming

* Add `payout_all` benchmark

* Fix merge

* add docs

* change event to controller

* Fix timestamp test warnings

* Revert "change event to controller"

This reverts commit 5d4a97832d47fe1273602d5410774d5421940c4e.

* Update Reward event doc

* Add "non-production" test

* add unlock chunk to test

* fix merge

* End payout early if no reward points

* payout_validator -> payout_stakers

* bring back payout nominator/validator, but limit their use to before migration era

* Add test for before migration

* New payout works for the era that we migrate

* Fix logic, check that migration era works

* Migrate Era tests (copypasta)

* Move comment

* Add mock back to external functions

* Fixes based on review from gui

* Update Cargo.lock

* Update Cargo.lock

* small docs update

Co-authored-by: joepetrowski <joe@parity.io>
This commit is contained in:
Shawn Tabrizi
2020-04-04 14:50:08 +02:00
committed by GitHub
parent 7c0fa83720
commit 6847f8452e
6 changed files with 1034 additions and 254 deletions
+1
View File
@@ -4437,6 +4437,7 @@ dependencies = [
"sp-runtime", "sp-runtime",
"sp-staking", "sp-staking",
"sp-std", "sp-std",
"sp-storage",
"static_assertions", "static_assertions",
"substrate-test-utils", "substrate-test-utils",
] ]
+1
View File
@@ -34,6 +34,7 @@ rand_chacha = { version = "0.2", default-features = false, optional = true }
[dev-dependencies] [dev-dependencies]
sp-core = { version = "2.0.0-alpha.5", path = "../../primitives/core" } sp-core = { version = "2.0.0-alpha.5", path = "../../primitives/core" }
sp-storage = { version = "2.0.0-alpha.5", path = "../../primitives/storage" }
pallet-balances = { version = "2.0.0-alpha.5", path = "../balances" } pallet-balances = { version = "2.0.0-alpha.5", path = "../balances" }
pallet-timestamp = { version = "2.0.0-alpha.5", path = "../timestamp" } pallet-timestamp = { version = "2.0.0-alpha.5", path = "../timestamp" }
pallet-staking-reward-curve = { version = "2.0.0-alpha.5", path = "../staking/reward-curve" } pallet-staking-reward-curve = { version = "2.0.0-alpha.5", path = "../staking/reward-curve" }
+59 -86
View File
@@ -20,7 +20,7 @@ use super::*;
use rand_chacha::{rand_core::{RngCore, SeedableRng}, ChaChaRng}; use rand_chacha::{rand_core::{RngCore, SeedableRng}, ChaChaRng};
use sp_runtime::traits::One; use sp_runtime::traits::{Dispatchable, One};
use sp_io::hashing::blake2_256; use sp_io::hashing::blake2_256;
use frame_system::RawOrigin; use frame_system::RawOrigin;
@@ -147,61 +147,6 @@ pub fn create_validator_with_nominators<T: Trait>(n: u32, upper_bound: u32) -> R
Ok(v_controller) Ok(v_controller)
} }
// This function generates one nominator nominating v validators.
// It starts an era and creates pending payouts.
pub fn create_nominator_with_validators<T: Trait>(v: u32) -> Result<(T::AccountId, Vec<T::AccountId>), &'static str> {
let mut validators = Vec::new();
let mut points_total = 0;
let mut points_individual = Vec::new();
MinimumValidatorCount::put(0);
// Create v validators
let mut validator_lookups = Vec::new();
for i in 0 .. v {
let (v_stash, v_controller) = create_stash_controller::<T>(i)?;
let validator_prefs = ValidatorPrefs {
commission: Perbill::from_percent(50),
};
Staking::<T>::validate(RawOrigin::Signed(v_controller.clone()).into(), validator_prefs)?;
let stash_lookup: <T::Lookup as StaticLookup>::Source = T::Lookup::unlookup(v_stash.clone());
points_total += 10;
points_individual.push((v_stash.clone(), 10));
validator_lookups.push(stash_lookup);
// Add to the list if it is less than the number we want the nominator to have
if validators.len() < v as usize {
validators.push(v_stash.clone())
}
}
// Create a nominator
let (_n_stash, n_controller) = create_stash_controller::<T>(u32::max_value())?;
Staking::<T>::nominate(RawOrigin::Signed(n_controller.clone()).into(), validator_lookups)?;
ValidatorCount::put(v);
// Start a new Era
let new_validators = Staking::<T>::new_era(SessionIndex::one()).unwrap();
assert!(new_validators.len() == v as usize);
// Give Era Points
let reward = EraRewardPoints::<T::AccountId> {
total: points_total,
individual: points_individual.into_iter().collect(),
};
let current_era = CurrentEra::get().unwrap();
ErasRewardPoints::<T>::insert(current_era, reward);
// Create reward pool
let total_payout = T::Currency::minimum_balance() * 1000.into();
<ErasValidatorReward<T>>::insert(current_era, total_payout);
Ok((n_controller, validators))
}
benchmarks! { benchmarks! {
_{ _{
// User account seed // User account seed
@@ -306,18 +251,12 @@ benchmarks! {
let slash_indices: Vec<u32> = (0 .. s).collect(); let slash_indices: Vec<u32> = (0 .. s).collect();
}: _(RawOrigin::Root, era, slash_indices) }: _(RawOrigin::Root, era, slash_indices)
payout_validator { payout_stakers {
let n in 1 .. MAX_NOMINATIONS as u32; let n in 1 .. MAX_NOMINATIONS as u32;
let validator = create_validator_with_nominators::<T>(n, MAX_NOMINATIONS as u32)?; let validator = create_validator_with_nominators::<T>(n, MAX_NOMINATIONS as u32)?;
let current_era = CurrentEra::get().unwrap(); let current_era = CurrentEra::get().unwrap();
}: _(RawOrigin::Signed(validator), current_era) let caller = account("caller", n, SEED);
}: _(RawOrigin::Signed(caller), validator, current_era)
payout_nominator {
let v in 1 .. MAX_NOMINATIONS as u32;
let (nominator, validators) = create_nominator_with_validators::<T>(v)?;
let current_era = CurrentEra::get().unwrap();
let find_nominator = validators.into_iter().map(|x| (x, 0)).collect();
}: _(RawOrigin::Signed(nominator), current_era, find_nominator)
rebond { rebond {
let l in 1 .. 1000; let l in 1 .. 1000;
@@ -386,6 +325,45 @@ benchmarks! {
&mut NegativeImbalanceOf::<T>::zero() &mut NegativeImbalanceOf::<T>::zero()
); );
} }
payout_all {
let v in 1 .. 10;
let n in 1 .. 100;
MinimumValidatorCount::put(0);
create_validators_with_nominators_for_era::<T>(v, n)?;
// Start a new Era
let new_validators = Staking::<T>::new_era(SessionIndex::one()).unwrap();
assert!(new_validators.len() == v as usize);
let current_era = CurrentEra::get().unwrap();
let mut points_total = 0;
let mut points_individual = Vec::new();
let mut payout_calls = Vec::new();
for validator in new_validators.iter() {
points_total += 10;
points_individual.push((validator.clone(), 10));
payout_calls.push(Call::<T>::payout_stakers(validator.clone(), current_era))
}
// Give Era Points
let reward = EraRewardPoints::<T::AccountId> {
total: points_total,
individual: points_individual.into_iter().collect(),
};
ErasRewardPoints::<T>::insert(current_era, reward);
// Create reward pool
let total_payout = T::Currency::minimum_balance() * 1000.into();
<ErasValidatorReward<T>>::insert(current_era, total_payout);
let caller: T::AccountId = account("caller", 0, SEED);
}: {
for call in payout_calls {
call.dispatch(RawOrigin::Signed(caller.clone()).into())?;
}
}
} }
#[cfg(test)] #[cfg(test)]
@@ -397,7 +375,7 @@ mod tests {
use crate::benchmarking::{ use crate::benchmarking::{
create_validators_with_nominators_for_era, create_validators_with_nominators_for_era,
create_validator_with_nominators, create_validator_with_nominators,
create_nominator_with_validators, SelectedBenchmark,
}; };
#[test] #[test]
@@ -429,36 +407,31 @@ mod tests {
let current_era = CurrentEra::get().unwrap(); let current_era = CurrentEra::get().unwrap();
let controller = validator; let controller = validator;
let ledger = Staking::ledger(&controller).unwrap(); let ledger = Staking::ledger(&controller).unwrap();
let stash = &ledger.stash; let stash = ledger.stash;
let original_free_balance = Balances::free_balance(stash); let original_free_balance = Balances::free_balance(&stash);
assert_ok!(Staking::payout_validator(Origin::signed(controller), current_era)); assert_ok!(Staking::payout_stakers(Origin::signed(1337), stash, current_era));
let new_free_balance = Balances::free_balance(stash); let new_free_balance = Balances::free_balance(&stash);
assert!(original_free_balance < new_free_balance); assert!(original_free_balance < new_free_balance);
}); });
} }
#[test] #[test]
fn create_nominator_with_validators_works() { fn test_payout_all() {
ExtBuilder::default().has_stakers(false).build().execute_with(|| { ExtBuilder::default().has_stakers(false).build().execute_with(|| {
let v = 5; let v = 10;
let n = 100;
let (nominator, validators) = create_nominator_with_validators::<Test>(v).unwrap(); let selected_benchmark = SelectedBenchmark::payout_all;
let c = vec![(frame_benchmarking::BenchmarkParameter::v, v), (frame_benchmarking::BenchmarkParameter::n, n)];
let closure_to_benchmark =
<SelectedBenchmark as frame_benchmarking::BenchmarkingSetup<Test>>::instance(
&selected_benchmark,
&c
).unwrap();
let current_era = CurrentEra::get().unwrap(); assert_ok!(closure_to_benchmark());
let controller = nominator;
let ledger = Staking::ledger(&controller).unwrap();
let stash = &ledger.stash;
let find_nominator = validators.into_iter().map(|x| (x, 0)).collect();
let original_free_balance = Balances::free_balance(stash);
assert_ok!(Staking::payout_nominator(Origin::signed(controller), current_era, find_nominator));
let new_free_balance = Balances::free_balance(stash);
assert!(original_free_balance < new_free_balance);
}); });
} }
} }
+224 -24
View File
@@ -104,10 +104,11 @@
//! The **reward and slashing** procedure is the core of the Staking module, attempting to _embrace //! The **reward and slashing** procedure is the core of the Staking module, attempting to _embrace
//! valid behavior_ while _punishing any misbehavior or lack of availability_. //! valid behavior_ while _punishing any misbehavior or lack of availability_.
//! //!
//! Reward must be claimed by stakers for each era before it gets too old by $HISTORY_DEPTH using //! Rewards must be claimed for each era before it gets too old by `$HISTORY_DEPTH` using the
//! `payout_nominator` and `payout_validator` calls. //! `payout_stakers` call. Any account can call `payout_stakers`, which pays the reward to
//! the validator as well as its nominators.
//! Only the [`T::MaxNominatorRewardedPerValidator`] biggest stakers can claim their reward. This //! Only the [`T::MaxNominatorRewardedPerValidator`] biggest stakers can claim their reward. This
//! limit the i/o cost to compute nominators payout. //! is to limit the i/o cost to mutate storage for each nominator's account.
//! //!
//! Slashing can occur at any point in time, once misbehavior is reported. Once slashing is //! Slashing can occur at any point in time, once misbehavior is reported. Once slashing is
//! determined, a value is deducted from the balance of the validator and all the nominators who //! determined, a value is deducted from the balance of the validator and all the nominators who
@@ -452,8 +453,9 @@ pub struct StakingLedger<AccountId, Balance: HasCompact> {
/// Any balance that is becoming free, which may eventually be transferred out /// Any balance that is becoming free, which may eventually be transferred out
/// of the stash (assuming it doesn't get slashed first). /// of the stash (assuming it doesn't get slashed first).
pub unlocking: Vec<UnlockChunk<Balance>>, pub unlocking: Vec<UnlockChunk<Balance>>,
/// The latest and highest era which the staker has claimed reward for. /// List of eras for which the stakers behind a validator have claimed rewards. Only updated
pub last_reward: Option<EraIndex>, /// for validators.
pub claimed_rewards: Vec<EraIndex>,
} }
impl< impl<
@@ -478,7 +480,7 @@ impl<
total, total,
active: self.active, active: self.active,
unlocking, unlocking,
last_reward: self.last_reward claimed_rewards: self.claimed_rewards
} }
} }
@@ -775,7 +777,7 @@ pub trait Trait: frame_system::Trait {
/// A transaction submitter. /// A transaction submitter.
type SubmitTransaction: SubmitUnsignedTransaction<Self, <Self as Trait>::Call>; type SubmitTransaction: SubmitUnsignedTransaction<Self, <Self as Trait>::Call>;
/// The maximum number of nominator rewarded for each validator. /// The maximum number of nominators rewarded for each validator.
/// ///
/// For each validator only the `$MaxNominatorRewardedPerValidator` biggest stakers can claim /// For each validator only the `$MaxNominatorRewardedPerValidator` biggest stakers can claim
/// their reward. This used to limit the i/o cost for the nominator payout. /// their reward. This used to limit the i/o cost for the nominator payout.
@@ -818,13 +820,13 @@ impl Default for Releases {
decl_storage! { decl_storage! {
trait Store for Module<T: Trait> as Staking { trait Store for Module<T: Trait> as Staking {
/// Number of era to keep in history. /// Number of eras to keep in history.
/// ///
/// Information is kept for eras in `[current_era - history_depth; current_era] /// Information is kept for eras in `[current_era - history_depth; current_era]`.
/// ///
/// Must be more than the number of era delayed by session otherwise. /// Must be more than the number of eras delayed by session otherwise.
/// i.e. active era must always be in history. /// I.e. active era must always be in history.
/// i.e. `active_era > current_era - history_depth` must be guaranteed. /// I.e. `active_era > current_era - history_depth` must be guaranteed.
HistoryDepth get(fn history_depth) config(): u32 = 84; HistoryDepth get(fn history_depth) config(): u32 = 84;
/// The ideal number of staking participants. /// The ideal number of staking participants.
@@ -860,7 +862,7 @@ decl_storage! {
/// The current era index. /// The current era index.
/// ///
/// This is the latest planned era, depending on how session module queues the validator /// This is the latest planned era, depending on how the Session pallet queues the validator
/// set, it might be active or not. /// set, it might be active or not.
pub CurrentEra get(fn current_era): Option<EraIndex>; pub CurrentEra get(fn current_era): Option<EraIndex>;
@@ -870,7 +872,7 @@ decl_storage! {
/// Validator set of this era must be equal to `SessionInterface::validators`. /// Validator set of this era must be equal to `SessionInterface::validators`.
pub ActiveEra get(fn active_era): Option<ActiveEraInfo>; pub ActiveEra get(fn active_era): Option<ActiveEraInfo>;
/// The session index at which the era start for the last `HISTORY_DEPTH` eras /// The session index at which the era start for the last `HISTORY_DEPTH` eras.
pub ErasStartSessionIndex get(fn eras_start_session_index): pub ErasStartSessionIndex get(fn eras_start_session_index):
map hasher(twox_64_concat) EraIndex => Option<SessionIndex>; map hasher(twox_64_concat) EraIndex => Option<SessionIndex>;
@@ -886,7 +888,7 @@ decl_storage! {
/// Clipped Exposure of validator at era. /// Clipped Exposure of validator at era.
/// ///
/// This is similar to [`ErasStakers`] but number of nominators exposed is reduce to the /// This is similar to [`ErasStakers`] but number of nominators exposed is reduced to the
/// `T::MaxNominatorRewardedPerValidator` biggest stakers. /// `T::MaxNominatorRewardedPerValidator` biggest stakers.
/// (Note: the field `total` and `own` of the exposure remains unchanged). /// (Note: the field `total` and `own` of the exposure remains unchanged).
/// This is used to limit the i/o cost for the nominator payout. /// This is used to limit the i/o cost for the nominator payout.
@@ -899,7 +901,7 @@ decl_storage! {
double_map hasher(twox_64_concat) EraIndex, hasher(twox_64_concat) T::AccountId double_map hasher(twox_64_concat) EraIndex, hasher(twox_64_concat) T::AccountId
=> Exposure<T::AccountId, BalanceOf<T>>; => Exposure<T::AccountId, BalanceOf<T>>;
/// Similarly to `ErasStakers` this holds the preferences of validators. /// Similar to `ErasStakers`, this holds the preferences of validators.
/// ///
/// This is keyed first by the era index to allow bulk deletion and then the stash account. /// This is keyed first by the era index to allow bulk deletion and then the stash account.
/// ///
@@ -998,6 +1000,9 @@ decl_storage! {
/// ///
/// This is set to v3.0.0 for new networks. /// This is set to v3.0.0 for new networks.
StorageVersion build(|_: &GenesisConfig<T>| Releases::V3_0_0): Releases; StorageVersion build(|_: &GenesisConfig<T>| Releases::V3_0_0): Releases;
/// The era where we migrated from Lazy Payouts to Simple Payouts
MigrateEra: Option<EraIndex>;
} }
add_extra_genesis { add_extra_genesis {
config(stakers): config(stakers):
@@ -1035,7 +1040,7 @@ decl_storage! {
decl_event!( decl_event!(
pub enum Event<T> where Balance = BalanceOf<T>, <T as frame_system::Trait>::AccountId { pub enum Event<T> where Balance = BalanceOf<T>, <T as frame_system::Trait>::AccountId {
/// The staker has been rewarded by this amount. AccountId is controller account. /// The staker has been rewarded by this amount. `AccountId` is the stash account.
Reward(AccountId, Balance), Reward(AccountId, Balance),
/// One validator (and its nominators) has been slashed by the given amount. /// One validator (and its nominators) has been slashed by the given amount.
Slash(AccountId, Balance), Slash(AccountId, Balance),
@@ -1088,6 +1093,8 @@ decl_error! {
InvalidNumberOfNominations, InvalidNumberOfNominations,
/// Items are not sorted and unique. /// Items are not sorted and unique.
NotSortedAndUnique, NotSortedAndUnique,
/// Rewards for this era have already been claimed for this validator.
AlreadyClaimed,
/// The submitted result is received out of the open window. /// The submitted result is received out of the open window.
PhragmenEarlySubmission, PhragmenEarlySubmission,
/// The submitted result is not as good as the one stored on chain. /// The submitted result is not as good as the one stored on chain.
@@ -1223,6 +1230,7 @@ decl_module! {
// For Kusama the type hasn't actually changed as Moment was u64 and was the number of // For Kusama the type hasn't actually changed as Moment was u64 and was the number of
// millisecond since unix epoch. // millisecond since unix epoch.
StorageVersion::put(Releases::V3_0_0); StorageVersion::put(Releases::V3_0_0);
Self::migrate_last_reward_to_claimed_rewards();
0 0
} }
@@ -1273,6 +1281,10 @@ decl_module! {
system::Module::<T>::inc_ref(&stash); system::Module::<T>::inc_ref(&stash);
let current_era = CurrentEra::get().unwrap_or(0);
let history_depth = Self::history_depth();
let last_reward_era = current_era.saturating_sub(history_depth);
let stash_balance = T::Currency::free_balance(&stash); let stash_balance = T::Currency::free_balance(&stash);
let value = value.min(stash_balance); let value = value.min(stash_balance);
Self::deposit_event(RawEvent::Bonded(stash.clone(), value)); Self::deposit_event(RawEvent::Bonded(stash.clone(), value));
@@ -1281,7 +1293,7 @@ decl_module! {
total: value, total: value,
active: value, active: value,
unlocking: vec![], unlocking: vec![],
last_reward: Self::current_era(), claimed_rewards: (last_reward_era..current_era).collect(),
}; };
Self::update_ledger(&controller, &item); Self::update_ledger(&controller, &item);
} }
@@ -1629,6 +1641,10 @@ decl_module! {
<Self as Store>::UnappliedSlashes::insert(&era, &unapplied); <Self as Store>::UnappliedSlashes::insert(&era, &unapplied);
} }
/// **This extrinsic will be removed after `MigrationEra + HistoryDepth` has passed, giving
/// opportunity for users to claim all rewards before moving to Simple Payouts. After this
/// time, you should use `payout_stakers` instead.**
///
/// Make one nominator's payout for one era. /// Make one nominator's payout for one era.
/// ///
/// - `who` is the controller account of the nominator to pay out. /// - `who` is the controller account of the nominator to pay out.
@@ -1663,6 +1679,10 @@ decl_module! {
Self::do_payout_nominator(who, era, validators) Self::do_payout_nominator(who, era, validators)
} }
/// **This extrinsic will be removed after `MigrationEra + HistoryDepth` has passed, giving
/// opportunity for users to claim all rewards before moving to Simple Payouts. After this
/// time, you should use `payout_stakers` instead.**
///
/// Make one validator's payout for one era. /// Make one validator's payout for one era.
/// ///
/// - `who` is the controller account of the validator to pay out. /// - `who` is the controller account of the validator to pay out.
@@ -1684,6 +1704,25 @@ decl_module! {
Self::do_payout_validator(who, era) Self::do_payout_validator(who, era)
} }
/// Pay out all the stakers behind a single validator for a single era.
///
/// - `validator_stash` is the stash account of the validator. Their nominators, up to
/// `T::MaxNominatorRewardedPerValidator`, will also receive their rewards.
/// - `era` may be any era between `[current_era - history_depth; current_era]`.
///
/// The origin of this call must be _Signed_. Any account can call this function, even if
/// it is not one of the stakers.
///
/// # <weight>
/// - Time complexity: at most O(MaxNominatorRewardedPerValidator).
/// - Contains a limited number of reads and writes.
/// # </weight>
#[weight = SimpleDispatchInfo::FixedNormal(500_000)]
fn payout_stakers(origin, validator_stash: T::AccountId, era: EraIndex) -> DispatchResult {
ensure_signed(origin)?;
Self::do_payout_stakers(validator_stash, era)
}
/// Rebond a portion of the stash scheduled to be unlocked. /// Rebond a portion of the stash scheduled to be unlocked.
/// ///
/// # <weight> /// # <weight>
@@ -1857,6 +1896,43 @@ decl_module! {
} }
impl<T: Trait> Module<T> { impl<T: Trait> Module<T> {
/// Migrate `last_reward` to `claimed_rewards`
pub fn migrate_last_reward_to_claimed_rewards() {
use frame_support::migration::{StorageIterator, put_storage_value};
// Migrate from `last_reward` to `claimed_rewards`.
// We will construct a vector from `current_era - history_depth` to `last_reward`
// for each validator and nominator.
//
// Old Staking Ledger
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)]
struct OldStakingLedger<AccountId, Balance: HasCompact> {
pub stash: AccountId,
#[codec(compact)]
pub total: Balance,
#[codec(compact)]
pub active: Balance,
pub unlocking: Vec<UnlockChunk<Balance>>,
pub last_reward: Option<EraIndex>,
}
// Current era and history depth
let current_era = Self::current_era().unwrap_or(0);
let history_depth = Self::history_depth();
let last_payout_era = current_era.saturating_sub(history_depth);
// Convert all ledgers to the new format.
for (hash, old_ledger) in StorageIterator::<OldStakingLedger<T::AccountId, BalanceOf<T>>>::new(b"Staking", b"Ledger").drain() {
let last_reward = old_ledger.last_reward.unwrap_or(0);
let new_ledger = StakingLedger {
stash: old_ledger.stash,
total: old_ledger.total,
active: old_ledger.active,
unlocking: old_ledger.unlocking,
claimed_rewards: (last_payout_era..=last_reward).collect(),
};
put_storage_value(b"Staking", b"Ledger", &hash, new_ledger);
}
MigrateEra::put(current_era);
}
/// The total balance that can be slashed from a stash account as of right now. /// The total balance that can be slashed from a stash account as of right now.
pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf<T> { pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf<T> {
Self::bonded(stash).and_then(Self::ledger).map(|l| l.active).unwrap_or_default() Self::bonded(stash).and_then(Self::ledger).map(|l| l.active).unwrap_or_default()
@@ -1916,6 +1992,15 @@ impl<T: Trait> Module<T> {
if validators.len() > MAX_NOMINATIONS { if validators.len() > MAX_NOMINATIONS {
return Err(Error::<T>::InvalidNumberOfNominations.into()); return Err(Error::<T>::InvalidNumberOfNominations.into());
} }
// If migrate_era is not populated, then you should use `payout_stakers`
let migrate_era = MigrateEra::get().ok_or(Error::<T>::InvalidEraToReward)?;
// This payout mechanism will only work for eras before the migration.
// Subsequent payouts should use `payout_stakers`.
ensure!(era < migrate_era, Error::<T>::InvalidEraToReward);
let current_era = CurrentEra::get().ok_or(Error::<T>::InvalidEraToReward)?;
ensure!(era <= current_era, Error::<T>::InvalidEraToReward);
let history_depth = Self::history_depth();
ensure!(era >= current_era.saturating_sub(history_depth), Error::<T>::InvalidEraToReward);
// Note: if era has no reward to be claimed, era may be future. better not to update // Note: if era has no reward to be claimed, era may be future. better not to update
// `nominator_ledger.last_reward` in this case. // `nominator_ledger.last_reward` in this case.
@@ -1924,11 +2009,12 @@ impl<T: Trait> Module<T> {
let mut nominator_ledger = <Ledger<T>>::get(&who).ok_or_else(|| Error::<T>::NotController)?; let mut nominator_ledger = <Ledger<T>>::get(&who).ok_or_else(|| Error::<T>::NotController)?;
if nominator_ledger.last_reward.map(|last_reward| last_reward >= era).unwrap_or(false) { nominator_ledger.claimed_rewards.retain(|&x| x >= current_era.saturating_sub(history_depth));
return Err(Error::<T>::InvalidEraToReward.into()); match nominator_ledger.claimed_rewards.binary_search(&era) {
Ok(_) => Err(Error::<T>::AlreadyClaimed)?,
Err(pos) => nominator_ledger.claimed_rewards.insert(pos, era),
} }
nominator_ledger.last_reward = Some(era);
<Ledger<T>>::insert(&who, &nominator_ledger); <Ledger<T>>::insert(&who, &nominator_ledger);
let mut reward = Perbill::zero(); let mut reward = Perbill::zero();
@@ -1972,17 +2058,29 @@ impl<T: Trait> Module<T> {
} }
fn do_payout_validator(who: T::AccountId, era: EraIndex) -> DispatchResult { fn do_payout_validator(who: T::AccountId, era: EraIndex) -> DispatchResult {
// If migrate_era is not populated, then you should use `payout_stakers`
let migrate_era = MigrateEra::get().ok_or(Error::<T>::InvalidEraToReward)?;
// This payout mechanism will only work for eras before the migration.
// Subsequent payouts should use `payout_stakers`.
ensure!(era < migrate_era, Error::<T>::InvalidEraToReward);
let current_era = CurrentEra::get().ok_or(Error::<T>::InvalidEraToReward)?;
ensure!(era <= current_era, Error::<T>::InvalidEraToReward);
let history_depth = Self::history_depth();
ensure!(era >= current_era.saturating_sub(history_depth), Error::<T>::InvalidEraToReward);
// Note: if era has no reward to be claimed, era may be future. better not to update // Note: if era has no reward to be claimed, era may be future. better not to update
// `ledger.last_reward` in this case. // `ledger.last_reward` in this case.
let era_payout = <ErasValidatorReward<T>>::get(&era) let era_payout = <ErasValidatorReward<T>>::get(&era)
.ok_or_else(|| Error::<T>::InvalidEraToReward)?; .ok_or_else(|| Error::<T>::InvalidEraToReward)?;
let mut ledger = <Ledger<T>>::get(&who).ok_or_else(|| Error::<T>::NotController)?; let mut ledger = <Ledger<T>>::get(&who).ok_or_else(|| Error::<T>::NotController)?;
if ledger.last_reward.map(|last_reward| last_reward >= era).unwrap_or(false) {
return Err(Error::<T>::InvalidEraToReward.into()); ledger.claimed_rewards.retain(|&x| x >= current_era.saturating_sub(history_depth));
match ledger.claimed_rewards.binary_search(&era) {
Ok(_) => Err(Error::<T>::AlreadyClaimed)?,
Err(pos) => ledger.claimed_rewards.insert(pos, era),
} }
ledger.last_reward = Some(era);
<Ledger<T>>::insert(&who, &ledger); <Ledger<T>>::insert(&who, &ledger);
let era_reward_points = <ErasRewardPoints<T>>::get(&era); let era_reward_points = <ErasRewardPoints<T>>::get(&era);
@@ -2013,6 +2111,108 @@ impl<T: Trait> Module<T> {
Ok(()) Ok(())
} }
fn do_payout_stakers(
validator_stash: T::AccountId,
era: EraIndex,
) -> DispatchResult {
/* Validate input data */
let current_era = CurrentEra::get().ok_or(Error::<T>::InvalidEraToReward)?;
ensure!(era <= current_era, Error::<T>::InvalidEraToReward);
let history_depth = Self::history_depth();
ensure!(era >= current_era.saturating_sub(history_depth), Error::<T>::InvalidEraToReward);
// If there was no migration, then this function is always valid.
if let Some(migrate_era) = MigrateEra::get() {
// This payout mechanism will only work for eras on and after the migration.
// Payouts before then should use `payout_nominator`/`payout_validator`.
ensure!(migrate_era <= era, Error::<T>::InvalidEraToReward);
}
// Note: if era has no reward to be claimed, era may be future. better not to update
// `ledger.claimed_rewards` in this case.
let era_payout = <ErasValidatorReward<T>>::get(&era)
.ok_or_else(|| Error::<T>::InvalidEraToReward)?;
let controller = Self::bonded(&validator_stash).ok_or(Error::<T>::NotStash)?;
let mut ledger = <Ledger<T>>::get(&controller).ok_or_else(|| Error::<T>::NotController)?;
ledger.claimed_rewards.retain(|&x| x >= current_era.saturating_sub(history_depth));
match ledger.claimed_rewards.binary_search(&era) {
Ok(_) => Err(Error::<T>::AlreadyClaimed)?,
Err(pos) => ledger.claimed_rewards.insert(pos, era),
}
let exposure = <ErasStakersClipped<T>>::get(&era, &ledger.stash);
/* Input data seems good, no errors allowed after this point */
<Ledger<T>>::insert(&controller, &ledger);
// Get Era reward points. It has TOTAL and INDIVIDUAL
// Find the fraction of the era reward that belongs to the validator
// Take that fraction of the eras rewards to split to nominator and validator
//
// Then look at the validator, figure out the proportion of their reward
// which goes to them and each of their nominators.
let era_reward_points = <ErasRewardPoints<T>>::get(&era);
let total_reward_points = era_reward_points.total;
let validator_reward_points = era_reward_points.individual.get(&ledger.stash)
.map(|points| *points)
.unwrap_or_else(|| Zero::zero());
// Nothing to do if they have no reward points.
if validator_reward_points.is_zero() { return Ok(())}
// This is the fraction of the total reward that the validator and the
// nominators will get.
let validator_total_reward_part = Perbill::from_rational_approximation(
validator_reward_points,
total_reward_points,
);
// This is how much validator + nominators are entitled to.
let validator_total_payout = validator_total_reward_part * era_payout;
let validator_prefs = Self::eras_validator_prefs(&era, &validator_stash);
// Validator first gets a cut off the top.
let validator_commission = validator_prefs.commission;
let validator_commission_payout = validator_commission * validator_total_payout;
let validator_leftover_payout = validator_total_payout - validator_commission_payout;
// Now let's calculate how this is split to the validator.
let validator_exposure_part = Perbill::from_rational_approximation(
exposure.own,
exposure.total,
);
let validator_staking_payout = validator_exposure_part * validator_leftover_payout;
// We can now make total validator payout:
if let Some(imbalance) = Self::make_payout(
&ledger.stash,
validator_staking_payout + validator_commission_payout
) {
Self::deposit_event(RawEvent::Reward(ledger.stash, imbalance.peek()));
}
// Lets now calculate how this is split to the nominators.
// Sort nominators by highest to lowest exposure, but only keep `max_nominator_payouts` of them.
for nominator in exposure.others.iter() {
let nominator_exposure_part = Perbill::from_rational_approximation(
nominator.value,
exposure.total,
);
let nominator_reward: BalanceOf<T> = nominator_exposure_part * validator_leftover_payout;
// We can now make nominator payout:
if let Some(imbalance) = Self::make_payout(&nominator.who, nominator_reward) {
Self::deposit_event(RawEvent::Reward(nominator.who.clone(), imbalance.peek()));
}
}
Ok(())
}
/// Update the ledger for a controller. This will also update the stash lock. The lock will /// Update the ledger for a controller. This will also update the stash lock. The lock will
/// will lock the entire funds except paying for further transactions. /// will lock the entire funds except paying for further transactions.
fn update_ledger( fn update_ledger(
+18 -8
View File
@@ -34,11 +34,7 @@ use sp_io;
use sp_phragmen::{ use sp_phragmen::{
build_support_map, evaluate_support, reduce, ExtendedBalance, StakedAssignment, PhragmenScore, build_support_map, evaluate_support, reduce, ExtendedBalance, StakedAssignment, PhragmenScore,
}; };
use crate::{ use crate::*;
EraIndex, GenesisConfig, Module, Trait, StakerStatus, ValidatorPrefs, RewardDestination,
Nominators, inflation, SessionInterface, Exposure, ErasStakers, ErasRewardPoints,
CompactAssignments, ValidatorIndex, NominatorIndex, Validators, OffchainAccuracy,
};
const INIT_TIMESTAMP: u64 = 30_000; const INIT_TIMESTAMP: u64 = 30_000;
@@ -559,6 +555,7 @@ pub fn assert_ledger_consistent(stash: AccountId) {
pub fn bond_validator(stash: u64, ctrl: u64, val: u64) { pub fn bond_validator(stash: u64, ctrl: u64, val: u64) {
let _ = Balances::make_free_balance_be(&stash, val); let _ = Balances::make_free_balance_be(&stash, val);
let _ = Balances::make_free_balance_be(&ctrl, val);
assert_ok!(Staking::bond( assert_ok!(Staking::bond(
Origin::signed(stash), Origin::signed(stash),
ctrl, ctrl,
@@ -573,6 +570,7 @@ pub fn bond_validator(stash: u64, ctrl: u64, val: u64) {
pub fn bond_nominator(stash: u64, ctrl: u64, val: u64, target: Vec<u64>) { pub fn bond_nominator(stash: u64, ctrl: u64, val: u64, target: Vec<u64>) {
let _ = Balances::make_free_balance_be(&stash, val); let _ = Balances::make_free_balance_be(&stash, val);
let _ = Balances::make_free_balance_be(&ctrl, val);
assert_ok!(Staking::bond( assert_ok!(Staking::bond(
Origin::signed(stash), Origin::signed(stash),
ctrl, ctrl,
@@ -684,8 +682,6 @@ pub fn on_offence_now(
pub fn horrible_phragmen_with_post_processing( pub fn horrible_phragmen_with_post_processing(
do_reduce: bool, do_reduce: bool,
) -> (CompactAssignments, Vec<ValidatorIndex>, PhragmenScore) { ) -> (CompactAssignments, Vec<ValidatorIndex>, PhragmenScore) {
use std::collections::BTreeMap;
let mut backing_stake_of: BTreeMap<AccountId, Balance> = BTreeMap::new(); let mut backing_stake_of: BTreeMap<AccountId, Balance> = BTreeMap::new();
// self stake // self stake
@@ -867,7 +863,7 @@ pub fn prepare_submission_with(
} }
/// Make all validator and nominator request their payment /// Make all validator and nominator request their payment
pub fn make_all_reward_payment(era: EraIndex) { pub fn make_all_reward_payment_before_migration(era: EraIndex) {
let validators_with_reward = ErasRewardPoints::<Test>::get(era).individual.keys() let validators_with_reward = ErasRewardPoints::<Test>::get(era).individual.keys()
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -898,6 +894,20 @@ pub fn make_all_reward_payment(era: EraIndex) {
} }
} }
/// Make all validator and nominator request their payment
pub fn make_all_reward_payment(era: EraIndex) {
let validators_with_reward = ErasRewardPoints::<Test>::get(era).individual.keys()
.cloned()
.collect::<Vec<_>>();
// reward validators
for validator_controller in validators_with_reward.iter().filter_map(Staking::bonded) {
let ledger = <Ledger<Test>>::get(&validator_controller).unwrap();
assert_ok!(Staking::payout_stakers(Origin::signed(1337), ledger.stash, era));
}
}
#[macro_export] #[macro_export]
macro_rules! assert_session_era { macro_rules! assert_session_era {
($session:expr, $era:expr) => { ($session:expr, $era:expr) => {
File diff suppressed because it is too large Load Diff