mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-09 20:11:09 +00:00
safe multi-era slashing for NPoS (#3846)
* define slashing spans * tests and pruning for slashing-spans record * validators get slashed before nominators * apply slash to nominators as well * chill and end slashing spans * actually perform slashes * integration (tests failing) * prune metadata * fix compilation * some tests for slashing and metadata garbage collection * correctly pass session index to slash handler * test span-max property for nominators and validators * test that slashes are summed correctly * reward value computation * implement rewarding * add comment about rewards * do not adjust slash fraction in offences module * fix offences tests * remove unused new_offenders field * update runtime version * fix up some docs * fix some CI failures * remove no-std incompatible vec! invocation * try to fix span-max rounding error * Update srml/staking/src/slashing.rs Fix type: winow -> window Co-Authored-By: Tomasz Drwięga <tomusdrw@users.noreply.github.com> * slashes from prior spans don't kick validator again * more information for nominators, suppression * ensure ledger is consistent with itself post-slash * implement slash out of unlocking funds also * slashing: create records to be applied after-the-fact * queue slashes for a few eras later * method for canceling deferred slashes * attempt to fix test in CI * storage migration for `Nominators` * update node-runtime to use SlashDeferDuration * adjust migration entry-points somewhat * fix migration compilation * add manual Vec import to migration * enable migrations feature in node-runtime * bump runtime version * update to latest master crate renames * update to use ensure-origin * Apply suggestions from code review use `ensure!` Co-Authored-By: Gavin Wood <gavin@parity.io> * fix multi-slash removal * initialize storage version to current in genesis * add test for version initialization
This commit is contained in:
committed by
Gavin Wood
parent
de5686509c
commit
4598e13015
@@ -50,7 +50,7 @@ nicks = { package = "pallet-nicks", path = "../../../frame/nicks", default-featu
|
||||
offences = { package = "pallet-offences", path = "../../../frame/offences", default-features = false }
|
||||
randomness-collective-flip = { package = "pallet-randomness-collective-flip", path = "../../../frame/randomness-collective-flip", default-features = false }
|
||||
session = { package = "pallet-session", path = "../../../frame/session", default-features = false, features = ["historical"] }
|
||||
staking = { package = "pallet-staking", path = "../../../frame/staking", default-features = false }
|
||||
staking = { package = "pallet-staking", path = "../../../frame/staking", default-features = false, features = ["migrate"] }
|
||||
pallet-staking-reward-curve = { path = "../../../frame/staking/reward-curve"}
|
||||
sudo = { package = "pallet-sudo", path = "../../../frame/sudo", default-features = false }
|
||||
support = { package = "frame-support", path = "../../../frame/support", default-features = false }
|
||||
|
||||
@@ -250,6 +250,7 @@ pallet_staking_reward_curve::build! {
|
||||
parameter_types! {
|
||||
pub const SessionsPerEra: sr_staking_primitives::SessionIndex = 6;
|
||||
pub const BondingDuration: staking::EraIndex = 24 * 28;
|
||||
pub const SlashDeferDuration: staking::EraIndex = 24 * 7; // 1/4 the bonding duration.
|
||||
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
|
||||
}
|
||||
|
||||
@@ -263,6 +264,9 @@ impl staking::Trait for Runtime {
|
||||
type Reward = (); // rewards are minted from the void
|
||||
type SessionsPerEra = SessionsPerEra;
|
||||
type BondingDuration = BondingDuration;
|
||||
type SlashDeferDuration = SlashDeferDuration;
|
||||
/// A super-majority of the council can cancel the slash.
|
||||
type SlashCancelOrigin = collective::EnsureProportionAtLeast<_3, _4, AccountId, CouncilCollective>;
|
||||
type SessionInterface = Self;
|
||||
type RewardCurve = RewardCurve;
|
||||
}
|
||||
|
||||
@@ -986,6 +986,7 @@ where
|
||||
) -> (Self::NegativeImbalance, Self::Balance) {
|
||||
let free_balance = Self::free_balance(who);
|
||||
let free_slash = cmp::min(free_balance, value);
|
||||
|
||||
Self::set_free_balance(who, free_balance - free_slash);
|
||||
let remaining_slash = value - free_slash;
|
||||
// NOTE: `slash()` prefers free balance, but assumes that reserve balance can be drawn
|
||||
|
||||
@@ -24,17 +24,11 @@
|
||||
mod mock;
|
||||
mod tests;
|
||||
|
||||
use rstd::{
|
||||
vec::Vec,
|
||||
collections::btree_set::BTreeSet,
|
||||
};
|
||||
use rstd::vec::Vec;
|
||||
use support::{
|
||||
decl_module, decl_event, decl_storage, Parameter,
|
||||
};
|
||||
use sr_primitives::{
|
||||
Perbill,
|
||||
traits::{Hash, Saturating},
|
||||
};
|
||||
use sr_primitives::traits::Hash;
|
||||
use sr_staking_primitives::{
|
||||
offence::{Offence, ReportOffence, Kind, OnOffenceHandler, OffenceDetails},
|
||||
};
|
||||
@@ -100,10 +94,11 @@ where
|
||||
|
||||
// Go through all offenders in the offence report and find all offenders that was spotted
|
||||
// in unique reports.
|
||||
let TriageOutcome {
|
||||
new_offenders,
|
||||
concurrent_offenders,
|
||||
} = match Self::triage_offence_report::<O>(reporters, &time_slot, offenders) {
|
||||
let TriageOutcome { concurrent_offenders } = match Self::triage_offence_report::<O>(
|
||||
reporters,
|
||||
&time_slot,
|
||||
offenders,
|
||||
) {
|
||||
Some(triage) => triage,
|
||||
// The report contained only duplicates, so there is no need to slash again.
|
||||
None => return,
|
||||
@@ -113,44 +108,18 @@ where
|
||||
Self::deposit_event(Event::Offence(O::ID, time_slot.encode()));
|
||||
|
||||
let offenders_count = concurrent_offenders.len() as u32;
|
||||
let previous_offenders_count = offenders_count - new_offenders.len() as u32;
|
||||
|
||||
// The amount new offenders are slashed
|
||||
let new_fraction = O::slash_fraction(offenders_count, validator_set_count);
|
||||
|
||||
// The amount previous offenders are slashed additionally.
|
||||
//
|
||||
// Since they were slashed in the past, we slash by:
|
||||
// x = (new - prev) / (1 - prev)
|
||||
// because:
|
||||
// Y = X * (1 - prev)
|
||||
// Z = Y * (1 - x)
|
||||
// Z = X * (1 - new)
|
||||
let old_fraction = if previous_offenders_count > 0 {
|
||||
let previous_fraction = O::slash_fraction(
|
||||
offenders_count.saturating_sub(previous_offenders_count),
|
||||
validator_set_count,
|
||||
);
|
||||
let numerator = new_fraction.saturating_sub(previous_fraction);
|
||||
let denominator = Perbill::one().saturating_sub(previous_fraction);
|
||||
denominator.saturating_mul(numerator)
|
||||
} else {
|
||||
new_fraction.clone()
|
||||
};
|
||||
let slash_perbill: Vec<_> = (0..concurrent_offenders.len())
|
||||
.map(|_| new_fraction.clone()).collect();
|
||||
|
||||
// calculate how much to slash
|
||||
let slash_perbill = concurrent_offenders
|
||||
.iter()
|
||||
.map(|details| {
|
||||
if previous_offenders_count > 0 && new_offenders.contains(&details.offender) {
|
||||
new_fraction.clone()
|
||||
} else {
|
||||
old_fraction.clone()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
T::OnOffenceHandler::on_offence(&concurrent_offenders, &slash_perbill);
|
||||
T::OnOffenceHandler::on_offence(
|
||||
&concurrent_offenders,
|
||||
&slash_perbill,
|
||||
offence.session_index(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,13 +142,13 @@ impl<T: Trait> Module<T> {
|
||||
offenders: Vec<T::IdentificationTuple>,
|
||||
) -> Option<TriageOutcome<T>> {
|
||||
let mut storage = ReportIndexStorage::<T, O>::load(time_slot);
|
||||
let mut new_offenders = BTreeSet::new();
|
||||
|
||||
let mut any_new = false;
|
||||
for offender in offenders {
|
||||
let report_id = Self::report_id::<O>(time_slot, &offender);
|
||||
|
||||
if !<Reports<T>>::exists(&report_id) {
|
||||
new_offenders.insert(offender.clone());
|
||||
any_new = true;
|
||||
<Reports<T>>::insert(
|
||||
&report_id,
|
||||
OffenceDetails {
|
||||
@@ -192,7 +161,7 @@ impl<T: Trait> Module<T> {
|
||||
}
|
||||
}
|
||||
|
||||
if !new_offenders.is_empty() {
|
||||
if any_new {
|
||||
// Load report details for the all reports happened at the same time.
|
||||
let concurrent_offenders = storage.concurrent_reports
|
||||
.iter()
|
||||
@@ -202,7 +171,6 @@ impl<T: Trait> Module<T> {
|
||||
storage.save();
|
||||
|
||||
Some(TriageOutcome {
|
||||
new_offenders,
|
||||
concurrent_offenders,
|
||||
})
|
||||
} else {
|
||||
@@ -212,8 +180,6 @@ impl<T: Trait> Module<T> {
|
||||
}
|
||||
|
||||
struct TriageOutcome<T: Trait> {
|
||||
/// Offenders that was spotted in the unique reports.
|
||||
new_offenders: BTreeSet<T::IdentificationTuple>,
|
||||
/// Other reports for the same report kinds.
|
||||
concurrent_offenders: Vec<OffenceDetails<T::AccountId, T::IdentificationTuple>>,
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ impl<Reporter, Offender> offence::OnOffenceHandler<Reporter, Offender> for OnOff
|
||||
fn on_offence(
|
||||
_offenders: &[OffenceDetails<Reporter, Offender>],
|
||||
slash_fraction: &[Perbill],
|
||||
_offence_session: SessionIndex,
|
||||
) {
|
||||
ON_OFFENCE_PERBILL.with(|f| {
|
||||
*f.borrow_mut() = slash_fraction.to_vec();
|
||||
@@ -148,9 +149,7 @@ impl<T: Clone> offence::Offence<T> for Offence<T> {
|
||||
}
|
||||
|
||||
fn session_index(&self) -> SessionIndex {
|
||||
// session index is not used by the pallet-offences directly, but rather it exists only for
|
||||
// filtering historical reports.
|
||||
unimplemented!()
|
||||
1
|
||||
}
|
||||
|
||||
fn slash_fraction(
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::mock::{
|
||||
Offences, System, Offence, TestEvent, KIND, new_test_ext, with_on_offence_fractions,
|
||||
offence_reports,
|
||||
};
|
||||
use sr_primitives::Perbill;
|
||||
use system::{EventRecord, Phase};
|
||||
|
||||
#[test]
|
||||
@@ -48,38 +49,6 @@ fn should_report_an_authority_and_trigger_on_offence() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_calculate_the_fraction_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// given
|
||||
let time_slot = 42;
|
||||
assert_eq!(offence_reports(KIND, time_slot), vec![]);
|
||||
let offence1 = Offence {
|
||||
validator_set_count: 5,
|
||||
time_slot,
|
||||
offenders: vec![5],
|
||||
};
|
||||
let offence2 = Offence {
|
||||
validator_set_count: 5,
|
||||
time_slot,
|
||||
offenders: vec![4],
|
||||
};
|
||||
|
||||
// when
|
||||
Offences::report_offence(vec![], offence1);
|
||||
with_on_offence_fractions(|f| {
|
||||
assert_eq!(f.clone(), vec![Perbill::from_percent(25)]);
|
||||
});
|
||||
|
||||
Offences::report_offence(vec![], offence2);
|
||||
|
||||
// then
|
||||
with_on_offence_fractions(|f| {
|
||||
assert_eq!(f.clone(), vec![Perbill::from_percent(15), Perbill::from_percent(45)]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_report_the_same_authority_twice_in_the_same_slot() {
|
||||
new_test_ext().execute_with(|| {
|
||||
|
||||
@@ -28,6 +28,7 @@ substrate-test-utils = { path = "../../test/utils" }
|
||||
|
||||
[features]
|
||||
equalize = []
|
||||
migrate = []
|
||||
default = ["std", "equalize"]
|
||||
std = [
|
||||
"serde",
|
||||
|
||||
+302
-140
@@ -108,6 +108,8 @@
|
||||
//! determined, a value is deducted from the balance of the validator and all the nominators who
|
||||
//! voted for this validator (values are deducted from the _stash_ account of the slashed entity).
|
||||
//!
|
||||
//! Slashing logic is further described in the documentation of the `slashing` module.
|
||||
//!
|
||||
//! Similar to slashing, rewards are also shared among a validator and its associated nominators.
|
||||
//! Yet, the reward funds are not always transferred to the stash account and can be configured.
|
||||
//! See [Reward Calculation](#reward-calculation) for more details.
|
||||
@@ -248,6 +250,8 @@
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod migration;
|
||||
mod slashing;
|
||||
|
||||
pub mod inflation;
|
||||
|
||||
@@ -268,6 +272,7 @@ use sr_primitives::{
|
||||
curve::PiecewiseLinear,
|
||||
traits::{
|
||||
Convert, Zero, One, StaticLookup, CheckedSub, Saturating, Bounded, SaturatedConversion,
|
||||
SimpleArithmetic, EnsureOrigin,
|
||||
}
|
||||
};
|
||||
use sr_staking_primitives::{
|
||||
@@ -278,7 +283,7 @@ use sr_staking_primitives::{
|
||||
use sr_primitives::{Serialize, Deserialize};
|
||||
use system::{ensure_signed, ensure_root};
|
||||
|
||||
use phragmen::{elect, equalize, build_support_map, ExtendedBalance, PhragmenStakedAssignment};
|
||||
use phragmen::{ExtendedBalance, PhragmenStakedAssignment};
|
||||
|
||||
const DEFAULT_MINIMUM_VALIDATOR_COUNT: u32 = 4;
|
||||
const MAX_NOMINATIONS: usize = 16;
|
||||
@@ -406,6 +411,74 @@ impl<
|
||||
.collect();
|
||||
Self { total, active: self.active, stash: self.stash, unlocking }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<AccountId, Balance> StakingLedger<AccountId, Balance> where
|
||||
Balance: SimpleArithmetic + Saturating + Copy,
|
||||
{
|
||||
/// Slash the validator for a given amount of balance. This can grow the value
|
||||
/// of the slash in the case that the validator has less than `minimum_balance`
|
||||
/// active funds. Returns the amount of funds actually slashed.
|
||||
///
|
||||
/// Slashes from `active` funds first, and then `unlocking`, starting with the
|
||||
/// chunks that are closest to unlocking.
|
||||
fn slash(
|
||||
&mut self,
|
||||
mut value: Balance,
|
||||
minimum_balance: Balance,
|
||||
) -> Balance {
|
||||
let pre_total = self.total;
|
||||
let total = &mut self.total;
|
||||
let active = &mut self.active;
|
||||
|
||||
let slash_out_of = |
|
||||
total_remaining: &mut Balance,
|
||||
target: &mut Balance,
|
||||
value: &mut Balance,
|
||||
| {
|
||||
let mut slash_from_target = (*value).min(*target);
|
||||
|
||||
if !slash_from_target.is_zero() {
|
||||
*target -= slash_from_target;
|
||||
|
||||
// don't leave a dust balance in the staking system.
|
||||
if *target <= minimum_balance {
|
||||
slash_from_target += *target;
|
||||
*value += rstd::mem::replace(target, Zero::zero());
|
||||
}
|
||||
|
||||
*total_remaining = total_remaining.saturating_sub(slash_from_target);
|
||||
*value -= slash_from_target;
|
||||
}
|
||||
};
|
||||
|
||||
slash_out_of(total, active, &mut value);
|
||||
|
||||
let i = self.unlocking.iter_mut()
|
||||
.map(|chunk| {
|
||||
slash_out_of(total, &mut chunk.value, &mut value);
|
||||
chunk.value
|
||||
})
|
||||
.take_while(|value| value.is_zero()) // take all fully-consumed chunks out.
|
||||
.count();
|
||||
|
||||
// kill all drained chunks.
|
||||
let _ = self.unlocking.drain(..i);
|
||||
|
||||
pre_total.saturating_sub(*total)
|
||||
}
|
||||
}
|
||||
|
||||
/// A record of the nominations made by a specific account.
|
||||
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)]
|
||||
pub struct Nominations<AccountId> {
|
||||
/// The targets of nomination.
|
||||
pub targets: Vec<AccountId>,
|
||||
/// The era the nominations were submitted.
|
||||
pub submitted_in: EraIndex,
|
||||
/// Whether the nominations have been suppressed.
|
||||
pub suppressed: bool,
|
||||
}
|
||||
|
||||
/// The amount of exposure (to slashing) than an individual nominator has.
|
||||
@@ -431,12 +504,20 @@ pub struct Exposure<AccountId, Balance: HasCompact> {
|
||||
pub others: Vec<IndividualExposure<AccountId, Balance>>,
|
||||
}
|
||||
|
||||
/// A slashing event occurred, slashing a validator for a given amount of balance.
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, Default, RuntimeDebug)]
|
||||
pub struct SlashJournalEntry<AccountId, Balance: HasCompact> {
|
||||
who: AccountId,
|
||||
amount: Balance,
|
||||
own_slash: Balance, // the amount of `who`'s own exposure that was slashed
|
||||
/// A pending slash record. The value of the slash has been computed but not applied yet,
|
||||
/// rather deferred for several eras.
|
||||
#[derive(Encode, Decode, Default, RuntimeDebug)]
|
||||
pub struct UnappliedSlash<AccountId, Balance: HasCompact> {
|
||||
/// The stash ID of the offending validator.
|
||||
validator: AccountId,
|
||||
/// The validator's own slash.
|
||||
own: Balance,
|
||||
/// All other slashed stakers and amounts.
|
||||
others: Vec<(AccountId, Balance)>,
|
||||
/// Reporters of the offence; bounty payout recipients.
|
||||
reporters: Vec<AccountId>,
|
||||
/// The amount of payout.
|
||||
payout: Balance,
|
||||
}
|
||||
|
||||
pub type BalanceOf<T> =
|
||||
@@ -519,6 +600,14 @@ pub trait Trait: system::Trait {
|
||||
/// Number of eras that staked funds must remain bonded for.
|
||||
type BondingDuration: Get<EraIndex>;
|
||||
|
||||
/// Number of eras that slashes are deferred by, after computation. This
|
||||
/// should be less than the bonding duration. Set to 0 if slashes should be
|
||||
/// applied immediately, without opportunity for intervention.
|
||||
type SlashDeferDuration: Get<EraIndex>;
|
||||
|
||||
/// The origin which can cancel a deferred slash. Root can always do this.
|
||||
type SlashCancelOrigin: EnsureOrigin<Self::Origin>;
|
||||
|
||||
/// Interface for interacting with a session module.
|
||||
type SessionInterface: self::SessionInterface<Self::AccountId>;
|
||||
|
||||
@@ -571,7 +660,10 @@ decl_storage! {
|
||||
pub Validators get(fn validators): linked_map T::AccountId => ValidatorPrefs<BalanceOf<T>>;
|
||||
|
||||
/// The map from nominator stash key to the set of stash keys of all validators to nominate.
|
||||
pub Nominators get(fn nominators): linked_map T::AccountId => Vec<T::AccountId>;
|
||||
///
|
||||
/// NOTE: is private so that we can ensure upgraded before all typical accesses.
|
||||
/// Direct storage APIs can still bypass this protection.
|
||||
Nominators get(fn nominators): linked_map T::AccountId => Option<Nominations<T::AccountId>>;
|
||||
|
||||
/// Nominators for a particular account that is in action right now. You can't iterate
|
||||
/// through validators here, but you can find them in the Session module.
|
||||
@@ -609,12 +701,38 @@ decl_storage! {
|
||||
/// The rest of the slashed value is handled by the `Slash`.
|
||||
pub SlashRewardFraction get(fn slash_reward_fraction) config(): Perbill;
|
||||
|
||||
/// The amount of currency given to reporters of a slash event which was
|
||||
/// canceled by extraordinary circumstances (e.g. governance).
|
||||
pub CanceledSlashPayout get(fn canceled_payout) config(): BalanceOf<T>;
|
||||
|
||||
/// All unapplied slashes that are queued for later.
|
||||
pub UnappliedSlashes: map EraIndex => Vec<UnappliedSlash<T::AccountId, BalanceOf<T>>>;
|
||||
|
||||
/// A mapping from still-bonded eras to the first session index of that era.
|
||||
BondedEras: Vec<(EraIndex, SessionIndex)>;
|
||||
|
||||
/// All slashes that have occurred in a given era.
|
||||
EraSlashJournal get(fn era_slash_journal):
|
||||
map EraIndex => Vec<SlashJournalEntry<T::AccountId, BalanceOf<T>>>;
|
||||
/// All slashing events on validators, mapped by era to the highest slash proportion
|
||||
/// and slash value of the era.
|
||||
ValidatorSlashInEra:
|
||||
double_map EraIndex, twox_128(T::AccountId) => Option<(Perbill, BalanceOf<T>)>;
|
||||
|
||||
/// All slashing events on nominators, mapped by era to the highest slash value of the era.
|
||||
NominatorSlashInEra:
|
||||
double_map EraIndex, twox_128(T::AccountId) => Option<BalanceOf<T>>;
|
||||
|
||||
/// Slashing spans for stash accounts.
|
||||
SlashingSpans: map T::AccountId => Option<slashing::SlashingSpans>;
|
||||
|
||||
/// Records information about the maximum slash of a stash within a slashing span,
|
||||
/// as well as how much reward has been paid out.
|
||||
SpanSlash:
|
||||
map (T::AccountId, slashing::SpanIndex) => slashing::SpanRecord<BalanceOf<T>>;
|
||||
|
||||
/// The earliest era for which we have a pending, unapplied slash.
|
||||
EarliestUnappliedSlash: Option<EraIndex>;
|
||||
|
||||
/// The version of storage for upgrade.
|
||||
StorageVersion: u32;
|
||||
}
|
||||
add_extra_genesis {
|
||||
config(stakers):
|
||||
@@ -646,6 +764,8 @@ decl_storage! {
|
||||
}, _ => Ok(())
|
||||
};
|
||||
}
|
||||
|
||||
StorageVersion::put(migration::CURRENT_VERSION);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -673,6 +793,10 @@ decl_module! {
|
||||
|
||||
fn deposit_event() = default;
|
||||
|
||||
fn on_initialize() {
|
||||
Self::ensure_storage_upgraded();
|
||||
}
|
||||
|
||||
fn on_finalize() {
|
||||
// Set the start of the first era.
|
||||
if !<CurrentEraStart<T>>::exists() {
|
||||
@@ -859,6 +983,8 @@ decl_module! {
|
||||
/// # </weight>
|
||||
#[weight = SimpleDispatchInfo::FixedNormal(750_000)]
|
||||
fn validate(origin, prefs: ValidatorPrefs<BalanceOf<T>>) {
|
||||
Self::ensure_storage_upgraded();
|
||||
|
||||
let controller = ensure_signed(origin)?;
|
||||
let ledger = Self::ledger(&controller).ok_or("not a controller")?;
|
||||
let stash = &ledger.stash;
|
||||
@@ -879,6 +1005,8 @@ decl_module! {
|
||||
/// # </weight>
|
||||
#[weight = SimpleDispatchInfo::FixedNormal(750_000)]
|
||||
fn nominate(origin, targets: Vec<<T::Lookup as StaticLookup>::Source>) {
|
||||
Self::ensure_storage_upgraded();
|
||||
|
||||
let controller = ensure_signed(origin)?;
|
||||
let ledger = Self::ledger(&controller).ok_or("not a controller")?;
|
||||
let stash = &ledger.stash;
|
||||
@@ -888,8 +1016,14 @@ decl_module! {
|
||||
.map(|t| T::Lookup::lookup(t))
|
||||
.collect::<result::Result<Vec<T::AccountId>, _>>()?;
|
||||
|
||||
let nominations = Nominations {
|
||||
targets,
|
||||
submitted_in: Self::current_era(),
|
||||
suppressed: false,
|
||||
};
|
||||
|
||||
<Validators<T>>::remove(stash);
|
||||
<Nominators<T>>::insert(stash, targets);
|
||||
<Nominators<T>>::insert(stash, &nominations);
|
||||
}
|
||||
|
||||
/// Declare no desire to either validate or nominate.
|
||||
@@ -907,9 +1041,7 @@ decl_module! {
|
||||
fn chill(origin) {
|
||||
let controller = ensure_signed(origin)?;
|
||||
let ledger = Self::ledger(&controller).ok_or("not a controller")?;
|
||||
let stash = &ledger.stash;
|
||||
<Validators<T>>::remove(stash);
|
||||
<Nominators<T>>::remove(stash);
|
||||
Self::chill_stash(&ledger.stash);
|
||||
}
|
||||
|
||||
/// (Re-)set the payment target for a controller.
|
||||
@@ -1018,14 +1150,48 @@ decl_module! {
|
||||
ensure_root(origin)?;
|
||||
ForceEra::put(Forcing::ForceAlways);
|
||||
}
|
||||
|
||||
/// Cancel enactment of a deferred slash. Can be called by either the root origin or
|
||||
/// the `T::SlashCancelOrigin`.
|
||||
/// passing the era and indices of the slashes for that era to kill.
|
||||
///
|
||||
/// # <weight>
|
||||
/// - One storage write.
|
||||
/// # </weight>
|
||||
#[weight = SimpleDispatchInfo::FreeOperational]
|
||||
fn cancel_deferred_slash(origin, era: EraIndex, slash_indices: Vec<u32>) {
|
||||
T::SlashCancelOrigin::try_origin(origin)
|
||||
.map(|_| ())
|
||||
.or_else(ensure_root)
|
||||
.map_err(|_| "bad origin")?;
|
||||
|
||||
let mut slash_indices = slash_indices;
|
||||
slash_indices.sort_unstable();
|
||||
let mut unapplied = <Self as Store>::UnappliedSlashes::get(&era);
|
||||
|
||||
for (removed, index) in slash_indices.into_iter().enumerate() {
|
||||
let index = index as usize;
|
||||
|
||||
// if `index` is not duplicate, `removed` must be <= index.
|
||||
ensure!(removed <= index, "duplicate index");
|
||||
|
||||
// all prior removals were from before this index, since the
|
||||
// list is sorted.
|
||||
let index = index - removed;
|
||||
ensure!(index < unapplied.len(), "slash record index out of bounds");
|
||||
|
||||
unapplied.remove(index);
|
||||
}
|
||||
|
||||
<Self as Store>::UnappliedSlashes::insert(&era, &unapplied);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> Module<T> {
|
||||
// PUBLIC IMMUTABLES
|
||||
|
||||
/// The total balance that can be slashed from a validator controller 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> {
|
||||
Self::bonded(stash).and_then(Self::ledger).map(|l| l.active).unwrap_or_default()
|
||||
}
|
||||
@@ -1048,67 +1214,15 @@ impl<T: Trait> Module<T> {
|
||||
<Ledger<T>>::insert(controller, ledger);
|
||||
}
|
||||
|
||||
/// Slash a given validator by a specific amount with given (historical) exposure.
|
||||
///
|
||||
/// Removes the slash from the validator's balance by preference,
|
||||
/// and reduces the nominators' balance if needed.
|
||||
///
|
||||
/// Returns the resulting `NegativeImbalance` to allow distributing the slashed amount and
|
||||
/// pushes an entry onto the slash journal.
|
||||
fn slash_validator(
|
||||
stash: &T::AccountId,
|
||||
slash: BalanceOf<T>,
|
||||
exposure: &Exposure<T::AccountId, BalanceOf<T>>,
|
||||
journal: &mut Vec<SlashJournalEntry<T::AccountId, BalanceOf<T>>>,
|
||||
) -> NegativeImbalanceOf<T> {
|
||||
// The amount we are actually going to slash (can't be bigger than the validator's total
|
||||
// exposure)
|
||||
let slash = slash.min(exposure.total);
|
||||
/// Chill a stash account.
|
||||
fn chill_stash(stash: &T::AccountId) {
|
||||
<Validators<T>>::remove(stash);
|
||||
<Nominators<T>>::remove(stash);
|
||||
}
|
||||
|
||||
// limit what we'll slash of the stash's own to only what's in
|
||||
// the exposure.
|
||||
//
|
||||
// note: this is fine only because we limit reports of the current era.
|
||||
// otherwise, these funds may have already been slashed due to something
|
||||
// reported from a prior era.
|
||||
let already_slashed_own = journal.iter()
|
||||
.filter(|entry| &entry.who == stash)
|
||||
.map(|entry| entry.own_slash)
|
||||
.fold(<BalanceOf<T>>::zero(), |a, c| a.saturating_add(c));
|
||||
|
||||
let own_remaining = exposure.own.saturating_sub(already_slashed_own);
|
||||
|
||||
// The amount we'll slash from the validator's stash directly.
|
||||
let own_slash = own_remaining.min(slash);
|
||||
let (mut imbalance, missing) = T::Currency::slash(stash, own_slash);
|
||||
let own_slash = own_slash - missing;
|
||||
// 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;
|
||||
if !total.is_zero() {
|
||||
for i in exposure.others.iter() {
|
||||
let per_u64 = Perbill::from_rational_approximation(i.value, total);
|
||||
// best effort - not much that can be done on fail.
|
||||
imbalance.subsume(T::Currency::slash(&i.who, per_u64 * rest_slash).0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
journal.push(SlashJournalEntry {
|
||||
who: stash.clone(),
|
||||
own_slash: own_slash.clone(),
|
||||
amount: slash,
|
||||
});
|
||||
|
||||
// trigger the event
|
||||
Self::deposit_event(
|
||||
RawEvent::Slash(stash.clone(), slash)
|
||||
);
|
||||
|
||||
imbalance
|
||||
/// Ensures storage is upgraded to most recent necessary state.
|
||||
fn ensure_storage_upgraded() {
|
||||
migration::perform_migrations::<T>();
|
||||
}
|
||||
|
||||
/// Actually make a payment to a staker. This uses the currency's reward function
|
||||
@@ -1229,41 +1343,61 @@ impl<T: Trait> Module<T> {
|
||||
// Increment current era.
|
||||
let current_era = CurrentEra::mutate(|s| { *s += 1; *s });
|
||||
|
||||
// prune journal for last era.
|
||||
<EraSlashJournal<T>>::remove(current_era - 1);
|
||||
|
||||
CurrentEraStartSessionIndex::mutate(|v| {
|
||||
*v = start_session_index;
|
||||
});
|
||||
let bonding_duration = T::BondingDuration::get();
|
||||
|
||||
if current_era > bonding_duration {
|
||||
let first_kept = current_era - bonding_duration;
|
||||
BondedEras::mutate(|bonded| {
|
||||
bonded.push((current_era, start_session_index));
|
||||
BondedEras::mutate(|bonded| {
|
||||
bonded.push((current_era, start_session_index));
|
||||
|
||||
if current_era > bonding_duration {
|
||||
let first_kept = current_era - bonding_duration;
|
||||
|
||||
// prune out everything that's from before the first-kept index.
|
||||
let n_to_prune = bonded.iter()
|
||||
.take_while(|&&(era_idx, _)| era_idx < first_kept)
|
||||
.count();
|
||||
|
||||
bonded.drain(..n_to_prune);
|
||||
// kill slashing metadata.
|
||||
for (pruned_era, _) in bonded.drain(..n_to_prune) {
|
||||
slashing::clear_era_metadata::<T>(pruned_era);
|
||||
}
|
||||
|
||||
if let Some(&(_, first_session)) = bonded.first() {
|
||||
T::SessionInterface::prune_historical_up_to(first_session);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Reassign all Stakers.
|
||||
let (_slot_stake, maybe_new_validators) = Self::select_validators();
|
||||
Self::apply_unapplied_slashes(current_era);
|
||||
|
||||
maybe_new_validators
|
||||
}
|
||||
|
||||
/// Apply previously-unapplied slashes on the beginning of a new era, after a delay.
|
||||
fn apply_unapplied_slashes(current_era: EraIndex) {
|
||||
let slash_defer_duration = T::SlashDeferDuration::get();
|
||||
<Self as Store>::EarliestUnappliedSlash::mutate(|earliest| if let Some(ref mut earliest) = earliest {
|
||||
let keep_from = current_era.saturating_sub(slash_defer_duration);
|
||||
for era in (*earliest)..keep_from {
|
||||
let era_slashes = <Self as Store>::UnappliedSlashes::take(&era);
|
||||
for slash in era_slashes {
|
||||
slashing::apply_slash::<T>(slash);
|
||||
}
|
||||
}
|
||||
|
||||
*earliest = (*earliest).max(keep_from)
|
||||
})
|
||||
}
|
||||
|
||||
/// Select a new validator set from the assembled stakers and their role preferences.
|
||||
///
|
||||
/// Returns the new `SlotStake` value and a set of newly selected _stash_ IDs.
|
||||
///
|
||||
/// Assumes storage is coherent with the declaration.
|
||||
fn select_validators() -> (BalanceOf<T>, Option<Vec<T::AccountId>>) {
|
||||
let mut all_nominators: Vec<(T::AccountId, Vec<T::AccountId>)> = Vec::new();
|
||||
let all_validator_candidates_iter = <Validators<T>>::enumerate();
|
||||
@@ -1272,9 +1406,24 @@ impl<T: Trait> Module<T> {
|
||||
all_nominators.push(self_vote);
|
||||
who
|
||||
}).collect::<Vec<T::AccountId>>();
|
||||
all_nominators.extend(<Nominators<T>>::enumerate());
|
||||
|
||||
let maybe_phragmen_result = elect::<_, _, _, T::CurrencyToVote>(
|
||||
let nominator_votes = <Nominators<T>>::enumerate().map(|(nominator, nominations)| {
|
||||
let Nominations { submitted_in, mut targets, suppressed: _ } = nominations;
|
||||
|
||||
// Filter out nomination targets which were nominated before the most recent
|
||||
// slashing span.
|
||||
targets.retain(|stash| {
|
||||
<Self as Store>::SlashingSpans::get(&stash).map_or(
|
||||
true,
|
||||
|spans| submitted_in >= spans.last_start(),
|
||||
)
|
||||
});
|
||||
|
||||
(nominator, targets)
|
||||
});
|
||||
all_nominators.extend(nominator_votes);
|
||||
|
||||
let maybe_phragmen_result = phragmen::elect::<_, _, _, T::CurrencyToVote>(
|
||||
Self::validator_count() as usize,
|
||||
Self::minimum_validator_count().max(1) as usize,
|
||||
all_validators,
|
||||
@@ -1293,7 +1442,7 @@ impl<T: Trait> Module<T> {
|
||||
let to_balance = |e: ExtendedBalance|
|
||||
<T::CurrencyToVote as Convert<ExtendedBalance, BalanceOf<T>>>::convert(e);
|
||||
|
||||
let mut supports = build_support_map::<_, _, _, T::CurrencyToVote>(
|
||||
let mut supports = phragmen::build_support_map::<_, _, _, T::CurrencyToVote>(
|
||||
&elected_stashes,
|
||||
&assignments,
|
||||
Self::slashable_balance_of,
|
||||
@@ -1324,7 +1473,7 @@ impl<T: Trait> Module<T> {
|
||||
|
||||
let tolerance = 0_u128;
|
||||
let iterations = 2_usize;
|
||||
equalize::<_, _, T::CurrencyToVote, _>(
|
||||
phragmen::equalize::<_, _, T::CurrencyToVote, _>(
|
||||
staked_assignments,
|
||||
&mut supports,
|
||||
tolerance,
|
||||
@@ -1384,6 +1533,8 @@ impl<T: Trait> Module<T> {
|
||||
|
||||
/// Remove all associated data of a stash account from the staking system.
|
||||
///
|
||||
/// Assumes storage is upgraded before calling.
|
||||
///
|
||||
/// This is called :
|
||||
/// - Immediately when an account's balance falls below existential deposit.
|
||||
/// - after a `withdraw_unbond()` call that frees all of a stash's bonded balance.
|
||||
@@ -1394,6 +1545,8 @@ impl<T: Trait> Module<T> {
|
||||
<Payee<T>>::remove(stash);
|
||||
<Validators<T>>::remove(stash);
|
||||
<Nominators<T>>::remove(stash);
|
||||
|
||||
slashing::clear_stash_metadata::<T>(stash);
|
||||
}
|
||||
|
||||
/// Add reward points to validators using their stash account ID.
|
||||
@@ -1449,6 +1602,7 @@ impl<T: Trait> Module<T> {
|
||||
|
||||
impl<T: Trait> session::OnSessionEnding<T::AccountId> for Module<T> {
|
||||
fn on_session_ending(_ending: SessionIndex, start_session: SessionIndex) -> Option<Vec<T::AccountId>> {
|
||||
Self::ensure_storage_upgraded();
|
||||
Self::new_session(start_session - 1).map(|(new, _old)| new)
|
||||
}
|
||||
}
|
||||
@@ -1457,12 +1611,14 @@ impl<T: Trait> OnSessionEnding<T::AccountId, Exposure<T::AccountId, BalanceOf<T>
|
||||
fn on_session_ending(_ending: SessionIndex, start_session: SessionIndex)
|
||||
-> Option<(Vec<T::AccountId>, Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)>)>
|
||||
{
|
||||
Self::ensure_storage_upgraded();
|
||||
Self::new_session(start_session - 1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> OnFreeBalanceZero<T::AccountId> for Module<T> {
|
||||
fn on_free_balance_zero(stash: &T::AccountId) {
|
||||
Self::ensure_storage_upgraded();
|
||||
Self::kill_stash(stash);
|
||||
}
|
||||
}
|
||||
@@ -1526,12 +1682,37 @@ impl <T: Trait> OnOffenceHandler<T::AccountId, session::historical::Identificati
|
||||
fn on_offence(
|
||||
offenders: &[OffenceDetails<T::AccountId, session::historical::IdentificationTuple<T>>],
|
||||
slash_fraction: &[Perbill],
|
||||
slash_session: SessionIndex,
|
||||
) {
|
||||
let mut remaining_imbalance = <NegativeImbalanceOf<T>>::zero();
|
||||
let slash_reward_fraction = SlashRewardFraction::get();
|
||||
<Module<T>>::ensure_storage_upgraded();
|
||||
|
||||
let reward_proportion = SlashRewardFraction::get();
|
||||
|
||||
let era_now = Self::current_era();
|
||||
let mut journal = Self::era_slash_journal(era_now);
|
||||
let window_start = era_now.saturating_sub(T::BondingDuration::get());
|
||||
let current_era_start_session = CurrentEraStartSessionIndex::get();
|
||||
|
||||
// fast path for current-era report - most likely.
|
||||
let slash_era = if slash_session >= current_era_start_session {
|
||||
era_now
|
||||
} else {
|
||||
let eras = BondedEras::get();
|
||||
|
||||
// reverse because it's more likely to find reports from recent eras.
|
||||
match eras.iter().rev().filter(|&&(_, ref sesh)| sesh <= &slash_session).next() {
|
||||
None => return, // before bonding period. defensive - should be filtered out.
|
||||
Some(&(ref slash_era, _)) => *slash_era,
|
||||
}
|
||||
};
|
||||
|
||||
<Self as Store>::EarliestUnappliedSlash::mutate(|earliest| {
|
||||
if earliest.is_none() {
|
||||
*earliest = Some(era_now)
|
||||
}
|
||||
});
|
||||
|
||||
let slash_defer_duration = T::SlashDeferDuration::get();
|
||||
|
||||
for (details, slash_fraction) in offenders.iter().zip(slash_fraction) {
|
||||
let stash = &details.offender.0;
|
||||
let exposure = &details.offender.1;
|
||||
@@ -1541,57 +1722,34 @@ impl <T: Trait> OnOffenceHandler<T::AccountId, session::historical::Identificati
|
||||
continue
|
||||
}
|
||||
|
||||
// Auto deselect validator on any offence and force a new era if they haven't previously
|
||||
// been deselected.
|
||||
if <Validators<T>>::exists(stash) {
|
||||
<Validators<T>>::remove(stash);
|
||||
Self::ensure_new_era();
|
||||
}
|
||||
let unapplied = slashing::compute_slash::<T>(slashing::SlashParams {
|
||||
stash,
|
||||
slash: *slash_fraction,
|
||||
exposure,
|
||||
slash_era,
|
||||
window_start,
|
||||
now: era_now,
|
||||
reward_proportion,
|
||||
});
|
||||
|
||||
// calculate the amount to slash
|
||||
let slash_exposure = exposure.total;
|
||||
let amount = *slash_fraction * slash_exposure;
|
||||
// in some cases `slash_fraction` can be just `0`,
|
||||
// which means we are not slashing this time.
|
||||
if amount.is_zero() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// make sure to disable validator till the end of this session
|
||||
if T::SessionInterface::disable_validator(stash).unwrap_or(false) {
|
||||
// force a new era, to select a new validator set
|
||||
Self::ensure_new_era();
|
||||
}
|
||||
// actually slash the validator
|
||||
let slashed_amount = Self::slash_validator(stash, amount, exposure, &mut journal);
|
||||
|
||||
// distribute the rewards according to the slash
|
||||
let slash_reward = slash_reward_fraction * slashed_amount.peek();
|
||||
if !slash_reward.is_zero() && !details.reporters.is_empty() {
|
||||
let (mut reward, rest) = slashed_amount.split(slash_reward);
|
||||
// split the reward between reporters equally. Division cannot fail because
|
||||
// we guarded against it in the enclosing if.
|
||||
let per_reporter = reward.peek() / (details.reporters.len() as u32).into();
|
||||
for reporter in &details.reporters {
|
||||
let (reporter_reward, rest) = reward.split(per_reporter);
|
||||
reward = rest;
|
||||
T::Currency::resolve_creating(reporter, reporter_reward);
|
||||
if let Some(mut unapplied) = unapplied {
|
||||
unapplied.reporters = details.reporters.clone();
|
||||
if slash_defer_duration == 0 {
|
||||
// apply right away.
|
||||
slashing::apply_slash::<T>(unapplied);
|
||||
} else {
|
||||
// defer to end of some `slash_defer_duration` from now.
|
||||
<Self as Store>::UnappliedSlashes::mutate(
|
||||
era_now,
|
||||
move |for_later| for_later.push(unapplied),
|
||||
);
|
||||
}
|
||||
// The rest goes to the treasury.
|
||||
remaining_imbalance.subsume(reward);
|
||||
remaining_imbalance.subsume(rest);
|
||||
} else {
|
||||
remaining_imbalance.subsume(slashed_amount);
|
||||
}
|
||||
}
|
||||
<EraSlashJournal<T>>::insert(era_now, journal);
|
||||
|
||||
// Handle the rest of imbalances
|
||||
T::Slash::on_unbalanced(remaining_imbalance);
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter historical offences out and only allow those from the current era.
|
||||
/// Filter historical offences out and only allow those from the bonding period.
|
||||
pub struct FilterHistoricalOffences<T, R> {
|
||||
_inner: rstd::marker::PhantomData<(T, R)>,
|
||||
}
|
||||
@@ -1603,9 +1761,13 @@ impl<T, Reporter, Offender, R, O> ReportOffence<Reporter, Offender, O>
|
||||
O: Offence<Offender>,
|
||||
{
|
||||
fn report_offence(reporters: Vec<Reporter>, offence: O) {
|
||||
// disallow any slashing from before the current era.
|
||||
<Module<T>>::ensure_storage_upgraded();
|
||||
|
||||
// disallow any slashing from before the current bonding period.
|
||||
let offence_session = offence.session_index();
|
||||
if offence_session >= <Module<T>>::current_era_start_session_index() {
|
||||
let bonded_eras = BondedEras::get();
|
||||
|
||||
if bonded_eras.first().filter(|(_, start)| offence_session >= *start).is_some() {
|
||||
R::report_offence(reporters, offence)
|
||||
} else {
|
||||
<Module<T>>::deposit_event(
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2019 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Substrate is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Substrate is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Storage migrations for srml-staking.
|
||||
|
||||
/// Indicator of a version of a storage layout.
|
||||
pub type VersionNumber = u32;
|
||||
|
||||
// the current expected version of the storage
|
||||
pub const CURRENT_VERSION: VersionNumber = 1;
|
||||
|
||||
#[cfg(any(test, feature = "migrate"))]
|
||||
mod inner {
|
||||
use crate::{Store, Module, Trait};
|
||||
use support::{StorageLinkedMap, StorageValue};
|
||||
use rstd::vec::Vec;
|
||||
use super::{CURRENT_VERSION, VersionNumber};
|
||||
|
||||
// the minimum supported version of the migration logic.
|
||||
const MIN_SUPPORTED_VERSION: VersionNumber = 0;
|
||||
|
||||
// migrate storage from v0 to v1.
|
||||
//
|
||||
// this upgrades the `Nominators` linked_map value type from `Vec<T::AccountId>` to
|
||||
// `Option<Nominations<T::AccountId>>`
|
||||
pub fn to_v1<T: Trait>(version: &mut VersionNumber) {
|
||||
if *version != 0 { return }
|
||||
*version += 1;
|
||||
|
||||
let now = <Module<T>>::current_era();
|
||||
let res = <Module<T> as Store>::Nominators::translate::<T::AccountId, Vec<T::AccountId>, _, _>(
|
||||
|key| key,
|
||||
|targets| crate::Nominations {
|
||||
targets,
|
||||
submitted_in: now,
|
||||
suppressed: false,
|
||||
},
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
support::print("Encountered error in migration of Staking::Nominators map.");
|
||||
if e.is_none() {
|
||||
support::print("Staking::Nominators map reinitialized");
|
||||
}
|
||||
}
|
||||
|
||||
support::print("Finished migrating Staking storage to v1.");
|
||||
}
|
||||
|
||||
pub(super) fn perform_migrations<T: Trait>() {
|
||||
<Module<T> as Store>::StorageVersion::mutate(|version| {
|
||||
if *version < MIN_SUPPORTED_VERSION {
|
||||
support::print("Cannot migrate staking storage because version is less than\
|
||||
minimum.");
|
||||
support::print(*version);
|
||||
return
|
||||
}
|
||||
|
||||
if *version == CURRENT_VERSION { return }
|
||||
|
||||
to_v1::<T>(version);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "migrate")))]
|
||||
mod inner {
|
||||
pub(super) fn perform_migrations<T>() { }
|
||||
}
|
||||
|
||||
/// Perform all necessary storage migrations to get storage into the expected stsate for current
|
||||
/// logic. No-op if fully upgraded.
|
||||
pub(crate) fn perform_migrations<T: crate::Trait>() {
|
||||
inner::perform_migrations::<T>();
|
||||
}
|
||||
@@ -21,10 +21,10 @@ use sr_primitives::{Perbill, KeyTypeId};
|
||||
use sr_primitives::curve::PiecewiseLinear;
|
||||
use sr_primitives::traits::{IdentityLookup, Convert, OpaqueKeys, OnInitialize, SaturatedConversion};
|
||||
use sr_primitives::testing::{Header, UintAuthorityId};
|
||||
use sr_staking_primitives::SessionIndex;
|
||||
use sr_staking_primitives::{SessionIndex, offence::{OffenceDetails, OnOffenceHandler}};
|
||||
use primitives::{H256, crypto::key_types};
|
||||
use runtime_io;
|
||||
use support::{assert_ok, impl_outer_origin, parameter_types, StorageLinkedMap};
|
||||
use support::{assert_ok, impl_outer_origin, parameter_types, StorageLinkedMap, StorageValue};
|
||||
use support::traits::{Currency, Get, FindAuthor};
|
||||
use crate::{
|
||||
EraIndex, GenesisConfig, Module, Trait, StakerStatus, ValidatorPrefs, RewardDestination,
|
||||
@@ -48,6 +48,7 @@ impl Convert<u128, u64> for CurrencyToVoteHandler {
|
||||
thread_local! {
|
||||
static SESSION: RefCell<(Vec<AccountId>, HashSet<AccountId>)> = RefCell::new(Default::default());
|
||||
static EXISTENTIAL_DEPOSIT: RefCell<u64> = RefCell::new(0);
|
||||
static SLASH_DEFER_DURATION: RefCell<EraIndex> = RefCell::new(0);
|
||||
}
|
||||
|
||||
pub struct TestSessionHandler;
|
||||
@@ -87,6 +88,13 @@ impl Get<u64> for ExistentialDeposit {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SlashDeferDuration;
|
||||
impl Get<EraIndex> for SlashDeferDuration {
|
||||
fn get() -> EraIndex {
|
||||
SLASH_DEFER_DURATION.with(|v| *v.borrow())
|
||||
}
|
||||
}
|
||||
|
||||
impl_outer_origin!{
|
||||
pub enum Origin for Test {}
|
||||
}
|
||||
@@ -202,6 +210,8 @@ impl Trait for Test {
|
||||
type Slash = ();
|
||||
type Reward = ();
|
||||
type SessionsPerEra = SessionsPerEra;
|
||||
type SlashDeferDuration = SlashDeferDuration;
|
||||
type SlashCancelOrigin = system::EnsureRoot<Self::AccountId>;
|
||||
type BondingDuration = BondingDuration;
|
||||
type SessionInterface = Self;
|
||||
type RewardCurve = RewardCurve;
|
||||
@@ -213,6 +223,7 @@ pub struct ExtBuilder {
|
||||
nominate: bool,
|
||||
validator_count: u32,
|
||||
minimum_validator_count: u32,
|
||||
slash_defer_duration: EraIndex,
|
||||
fair: bool,
|
||||
num_validators: Option<u32>,
|
||||
invulnerables: Vec<u64>,
|
||||
@@ -226,6 +237,7 @@ impl Default for ExtBuilder {
|
||||
nominate: true,
|
||||
validator_count: 2,
|
||||
minimum_validator_count: 0,
|
||||
slash_defer_duration: 0,
|
||||
fair: true,
|
||||
num_validators: None,
|
||||
invulnerables: vec![],
|
||||
@@ -254,6 +266,10 @@ impl ExtBuilder {
|
||||
self.minimum_validator_count = count;
|
||||
self
|
||||
}
|
||||
pub fn slash_defer_duration(mut self, eras: EraIndex) -> Self {
|
||||
self.slash_defer_duration = eras;
|
||||
self
|
||||
}
|
||||
pub fn fair(mut self, is_fair: bool) -> Self {
|
||||
self.fair = is_fair;
|
||||
self
|
||||
@@ -268,6 +284,7 @@ impl ExtBuilder {
|
||||
}
|
||||
pub fn set_associated_consts(&self) {
|
||||
EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit);
|
||||
SLASH_DEFER_DURATION.with(|v| *v.borrow_mut() = self.slash_defer_duration);
|
||||
}
|
||||
pub fn build(self) -> runtime_io::TestExternalities {
|
||||
self.set_associated_consts();
|
||||
@@ -393,6 +410,14 @@ pub fn assert_is_stash(acc: u64) {
|
||||
assert!(Staking::bonded(&acc).is_some(), "Not a stash.");
|
||||
}
|
||||
|
||||
pub fn assert_ledger_consistent(stash: u64) {
|
||||
assert_is_stash(stash);
|
||||
let ledger = Staking::ledger(stash - 1).unwrap();
|
||||
|
||||
let real_total: Balance = ledger.unlocking.iter().fold(ledger.active, |a, c| a + c.value);
|
||||
assert_eq!(real_total, ledger.total);
|
||||
}
|
||||
|
||||
pub fn bond_validator(acc: u64, val: u64) {
|
||||
// a = controller
|
||||
// a + 1 = stash
|
||||
@@ -451,3 +476,33 @@ pub fn reward_all_elected() {
|
||||
pub fn validator_controllers() -> Vec<AccountId> {
|
||||
Session::validators().into_iter().map(|s| Staking::bonded(&s).expect("no controller for validator")).collect()
|
||||
}
|
||||
|
||||
pub fn on_offence_in_era(
|
||||
offenders: &[OffenceDetails<AccountId, session::historical::IdentificationTuple<Test>>],
|
||||
slash_fraction: &[Perbill],
|
||||
era: EraIndex,
|
||||
) {
|
||||
let bonded_eras = crate::BondedEras::get();
|
||||
for &(bonded_era, start_session) in bonded_eras.iter() {
|
||||
if bonded_era == era {
|
||||
Staking::on_offence(offenders, slash_fraction, start_session);
|
||||
return
|
||||
} else if bonded_era > era {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if Staking::current_era() == era {
|
||||
Staking::on_offence(offenders, slash_fraction, Staking::current_era_start_session_index());
|
||||
} else {
|
||||
panic!("cannot slash in era {}", era);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_offence_now(
|
||||
offenders: &[OffenceDetails<AccountId, session::historical::IdentificationTuple<Test>>],
|
||||
slash_fraction: &[Perbill],
|
||||
) {
|
||||
let now = Staking::current_era();
|
||||
on_offence_in_era(offenders, slash_fraction, now)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,824 @@
|
||||
// Copyright 2019 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Substrate is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Substrate is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A slashing implementation for NPoS systems.
|
||||
//!
|
||||
//! For the purposes of the economic model, it is easiest to think of each validator
|
||||
//! of a nominator which nominates only its own identity.
|
||||
//!
|
||||
//! The act of nomination signals intent to unify economic identity with the validator - to take part in the
|
||||
//! rewards of a job well done, and to take part in the punishment of a job done badly.
|
||||
//!
|
||||
//! There are 3 main difficulties to account for with slashing in NPoS:
|
||||
//! - A nominator can nominate multiple validators and be slashed via any of them.
|
||||
//! - Until slashed, stake is reused from era to era. Nominating with N coins for E eras in a row
|
||||
//! does not mean you have N*E coins to be slashed - you've only ever had N.
|
||||
//! - Slashable offences can be found after the fact and out of order.
|
||||
//!
|
||||
//! The algorithm implemented in this module tries to balance these 3 difficulties.
|
||||
//!
|
||||
//! First, we only slash participants for the _maximum_ slash they receive in some time period,
|
||||
//! rather than the sum. This ensures a protection from overslashing.
|
||||
//!
|
||||
//! Second, we do not want the time period (or "span") that the maximum is computed
|
||||
//! over to last indefinitely. That would allow participants to begin acting with
|
||||
//! impunity after some point, fearing no further repercussions. For that reason, we
|
||||
//! automatically "chill" validators and withdraw a nominator's nomination after a slashing event,
|
||||
//! requiring them to re-enlist voluntarily (acknowledging the slash) and begin a new
|
||||
//! slashing span.
|
||||
//!
|
||||
//! Typically, you will have a single slashing event per slashing span. Only in the case
|
||||
//! where a validator releases many misbehaviors at once, or goes "back in time" to misbehave in
|
||||
//! eras that have already passed, would you encounter situations where a slashing span
|
||||
//! has multiple misbehaviors. However, accounting for such cases is necessary
|
||||
//! to deter a class of "rage-quit" attacks.
|
||||
//!
|
||||
//! Based on research at https://research.web3.foundation/en/latest/polkadot/slashing/npos/
|
||||
|
||||
use super::{
|
||||
EraIndex, Trait, Module, Store, BalanceOf, Exposure, Perbill, SessionInterface,
|
||||
NegativeImbalanceOf, UnappliedSlash,
|
||||
};
|
||||
use sr_primitives::traits::{Zero, Saturating};
|
||||
use support::{
|
||||
StorageMap, StorageDoubleMap,
|
||||
traits::{Currency, OnUnbalanced, Imbalance},
|
||||
};
|
||||
use rstd::vec::Vec;
|
||||
use codec::{Encode, Decode};
|
||||
|
||||
/// The proportion of the slashing reward to be paid out on the first slashing detection.
|
||||
/// This is f_1 in the paper.
|
||||
const REWARD_F1: Perbill = Perbill::from_percent(50);
|
||||
|
||||
/// The index of a slashing span - unique to each stash.
|
||||
pub(crate) type SpanIndex = u32;
|
||||
|
||||
// A range of start..end eras for a slashing span.
|
||||
#[derive(Encode, Decode)]
|
||||
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||
pub(crate) struct SlashingSpan {
|
||||
pub(crate) index: SpanIndex,
|
||||
pub(crate) start: EraIndex,
|
||||
pub(crate) length: Option<EraIndex>, // the ongoing slashing span has indeterminate length.
|
||||
}
|
||||
|
||||
impl SlashingSpan {
|
||||
fn contains_era(&self, era: EraIndex) -> bool {
|
||||
self.start <= era && self.length.map_or(true, |l| self.start + l > era)
|
||||
}
|
||||
}
|
||||
|
||||
/// An encoding of all of a nominator's slashing spans.
|
||||
#[derive(Encode, Decode)]
|
||||
pub struct SlashingSpans {
|
||||
// the index of the current slashing span of the nominator. different for
|
||||
// every stash, resets when the account hits free balance 0.
|
||||
span_index: SpanIndex,
|
||||
// the start era of the most recent (ongoing) slashing span.
|
||||
last_start: EraIndex,
|
||||
// all prior slashing spans start indices, in reverse order (most recent first)
|
||||
// encoded as offsets relative to the slashing span after it.
|
||||
prior: Vec<EraIndex>,
|
||||
}
|
||||
|
||||
impl SlashingSpans {
|
||||
// creates a new record of slashing spans for a stash, starting at the beginning
|
||||
// of the bonding period, relative to now.
|
||||
fn new(window_start: EraIndex) -> Self {
|
||||
SlashingSpans {
|
||||
span_index: 0,
|
||||
last_start: window_start,
|
||||
prior: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// update the slashing spans to reflect the start of a new span at the era after `now`
|
||||
// returns `true` if a new span was started, `false` otherwise. `false` indicates
|
||||
// that internal state is unchanged.
|
||||
fn end_span(&mut self, now: EraIndex) -> bool {
|
||||
let next_start = now + 1;
|
||||
if next_start <= self.last_start { return false }
|
||||
|
||||
let last_length = next_start - self.last_start;
|
||||
self.prior.insert(0, last_length);
|
||||
self.last_start = next_start;
|
||||
self.span_index += 1;
|
||||
true
|
||||
}
|
||||
|
||||
// an iterator over all slashing spans in _reverse_ order - most recent first.
|
||||
pub(crate) fn iter(&'_ self) -> impl Iterator<Item = SlashingSpan> + '_ {
|
||||
let mut last_start = self.last_start;
|
||||
let mut index = self.span_index;
|
||||
let last = SlashingSpan { index, start: last_start, length: None };
|
||||
let prior = self.prior.iter().cloned().map(move |length| {
|
||||
let start = last_start - length;
|
||||
last_start = start;
|
||||
index -= 1;
|
||||
|
||||
SlashingSpan { index, start, length: Some(length) }
|
||||
});
|
||||
|
||||
rstd::iter::once(last).chain(prior)
|
||||
}
|
||||
|
||||
/// Yields the era index where the last (current) slashing span started.
|
||||
pub(crate) fn last_start(&self) -> EraIndex {
|
||||
self.last_start
|
||||
}
|
||||
|
||||
// prune the slashing spans against a window, whose start era index is given.
|
||||
//
|
||||
// If this returns `Some`, then it includes a range start..end of all the span
|
||||
// indices which were pruned.
|
||||
fn prune(&mut self, window_start: EraIndex) -> Option<(SpanIndex, SpanIndex)> {
|
||||
let old_idx = self.iter()
|
||||
.skip(1) // skip ongoing span.
|
||||
.position(|span| span.length.map_or(false, |len| span.start + len <= window_start));
|
||||
|
||||
let earliest_span_index = self.span_index - self.prior.len() as SpanIndex;
|
||||
let pruned = match old_idx {
|
||||
Some(o) => {
|
||||
self.prior.truncate(o);
|
||||
let new_earliest = self.span_index - self.prior.len() as SpanIndex;
|
||||
Some((earliest_span_index, new_earliest))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
// readjust the ongoing span, if it started before the beginning of the window.
|
||||
self.last_start = rstd::cmp::max(self.last_start, window_start);
|
||||
pruned
|
||||
}
|
||||
}
|
||||
|
||||
/// A slashing-span record for a particular stash.
|
||||
#[derive(Encode, Decode, Default)]
|
||||
pub(crate) struct SpanRecord<Balance> {
|
||||
slashed: Balance,
|
||||
paid_out: Balance,
|
||||
}
|
||||
|
||||
impl<Balance> SpanRecord<Balance> {
|
||||
/// The value of stash balance slashed in this span.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn amount_slashed(&self) -> &Balance {
|
||||
&self.slashed
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters for performing a slash.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct SlashParams<'a, T: 'a + Trait> {
|
||||
/// The stash account being slashed.
|
||||
pub(crate) stash: &'a T::AccountId,
|
||||
/// The proportion of the slash.
|
||||
pub(crate) slash: Perbill,
|
||||
/// The exposure of the stash and all nominators.
|
||||
pub(crate) exposure: &'a Exposure<T::AccountId, BalanceOf<T>>,
|
||||
/// The era where the offence occurred.
|
||||
pub(crate) slash_era: EraIndex,
|
||||
/// The first era in the current bonding period.
|
||||
pub(crate) window_start: EraIndex,
|
||||
/// The current era.
|
||||
pub(crate) now: EraIndex,
|
||||
/// The maximum percentage of a slash that ever gets paid out.
|
||||
/// This is f_inf in the paper.
|
||||
pub(crate) reward_proportion: Perbill,
|
||||
}
|
||||
|
||||
/// Computes a slash of a validator and nominators. It returns an unapplied
|
||||
/// record to be applied at some later point. Slashing metadata is updated in storage,
|
||||
/// since unapplied records are only rarely intended to be dropped.
|
||||
///
|
||||
/// The pending slash record returned does not have initialized reporters. Those have
|
||||
/// to be set at a higher level, if any.
|
||||
pub(crate) fn compute_slash<T: Trait>(params: SlashParams<T>)
|
||||
-> Option<UnappliedSlash<T::AccountId, BalanceOf<T>>>
|
||||
{
|
||||
let SlashParams {
|
||||
stash,
|
||||
slash,
|
||||
exposure,
|
||||
slash_era,
|
||||
window_start,
|
||||
now,
|
||||
reward_proportion,
|
||||
} = params.clone();
|
||||
|
||||
let mut reward_payout = Zero::zero();
|
||||
let mut val_slashed = Zero::zero();
|
||||
|
||||
// is the slash amount here a maximum for the era?
|
||||
let own_slash = slash * exposure.own;
|
||||
if slash * exposure.total == Zero::zero() {
|
||||
// kick out the validator even if they won't be slashed,
|
||||
// as long as the misbehavior is from their most recent slashing span.
|
||||
kick_out_if_recent::<T>(params);
|
||||
return None;
|
||||
}
|
||||
|
||||
let (prior_slash_p, _era_slash) = <Module<T> as Store>::ValidatorSlashInEra::get(
|
||||
&slash_era,
|
||||
stash,
|
||||
).unwrap_or((Perbill::zero(), Zero::zero()));
|
||||
|
||||
// compare slash proportions rather than slash values to avoid issues due to rounding
|
||||
// error.
|
||||
if slash.deconstruct() > prior_slash_p.deconstruct() {
|
||||
<Module<T> as Store>::ValidatorSlashInEra::insert(
|
||||
&slash_era,
|
||||
stash,
|
||||
&(slash, own_slash),
|
||||
);
|
||||
} else {
|
||||
// we slash based on the max in era - this new event is not the max,
|
||||
// so neither the validator or any nominators will need an update.
|
||||
//
|
||||
// this does lead to a divergence of our system from the paper, which
|
||||
// pays out some reward even if the latest report is not max-in-era.
|
||||
// we opt to avoid the nominator lookups and edits and leave more rewards
|
||||
// for more drastic misbehavior.
|
||||
return None;
|
||||
}
|
||||
|
||||
// apply slash to validator.
|
||||
{
|
||||
let mut spans = fetch_spans::<T>(
|
||||
stash,
|
||||
window_start,
|
||||
&mut reward_payout,
|
||||
&mut val_slashed,
|
||||
reward_proportion,
|
||||
);
|
||||
|
||||
let target_span = spans.compare_and_update_span_slash(
|
||||
slash_era,
|
||||
own_slash,
|
||||
);
|
||||
|
||||
if target_span == Some(spans.span_index()) {
|
||||
// misbehavior occurred within the current slashing span - take appropriate
|
||||
// actions.
|
||||
|
||||
// chill the validator - it misbehaved in the current span and should
|
||||
// not continue in the next election. also end the slashing span.
|
||||
spans.end_span(now);
|
||||
<Module<T>>::chill_stash(stash);
|
||||
|
||||
// make sure to disable validator till the end of this session
|
||||
if T::SessionInterface::disable_validator(stash).unwrap_or(false) {
|
||||
// force a new era, to select a new validator set
|
||||
<Module<T>>::ensure_new_era()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut nominators_slashed = Vec::new();
|
||||
reward_payout += slash_nominators::<T>(params, prior_slash_p, &mut nominators_slashed);
|
||||
|
||||
Some(UnappliedSlash {
|
||||
validator: stash.clone(),
|
||||
own: val_slashed,
|
||||
others: nominators_slashed,
|
||||
reporters: Vec::new(),
|
||||
payout: reward_payout,
|
||||
})
|
||||
}
|
||||
|
||||
// doesn't apply any slash, but kicks out the validator if the misbehavior is from
|
||||
// the most recent slashing span.
|
||||
fn kick_out_if_recent<T: Trait>(
|
||||
params: SlashParams<T>,
|
||||
) {
|
||||
// these are not updated by era-span or end-span.
|
||||
let mut reward_payout = Zero::zero();
|
||||
let mut val_slashed = Zero::zero();
|
||||
let mut spans = fetch_spans::<T>(
|
||||
params.stash,
|
||||
params.window_start,
|
||||
&mut reward_payout,
|
||||
&mut val_slashed,
|
||||
params.reward_proportion,
|
||||
);
|
||||
|
||||
if spans.era_span(params.slash_era).map(|s| s.index) == Some(spans.span_index()) {
|
||||
spans.end_span(params.now);
|
||||
<Module<T>>::chill_stash(params.stash);
|
||||
|
||||
// make sure to disable validator till the end of this session
|
||||
if T::SessionInterface::disable_validator(params.stash).unwrap_or(false) {
|
||||
// force a new era, to select a new validator set
|
||||
<Module<T>>::ensure_new_era()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Slash nominators. Accepts general parameters and the prior slash percentage of the validator.
|
||||
///
|
||||
/// Returns the amount of reward to pay out.
|
||||
fn slash_nominators<T: Trait>(
|
||||
params: SlashParams<T>,
|
||||
prior_slash_p: Perbill,
|
||||
nominators_slashed: &mut Vec<(T::AccountId, BalanceOf<T>)>,
|
||||
) -> BalanceOf<T> {
|
||||
let SlashParams {
|
||||
stash: _,
|
||||
slash,
|
||||
exposure,
|
||||
slash_era,
|
||||
window_start,
|
||||
now,
|
||||
reward_proportion,
|
||||
} = params;
|
||||
|
||||
let mut reward_payout = Zero::zero();
|
||||
|
||||
nominators_slashed.reserve(exposure.others.len());
|
||||
for nominator in &exposure.others {
|
||||
let stash = &nominator.who;
|
||||
let mut nom_slashed = Zero::zero();
|
||||
|
||||
// the era slash of a nominator always grows, if the validator
|
||||
// had a new max slash for the era.
|
||||
let era_slash = {
|
||||
let own_slash_prior = prior_slash_p * nominator.value;
|
||||
let own_slash_by_validator = slash * nominator.value;
|
||||
let own_slash_difference = own_slash_by_validator.saturating_sub(own_slash_prior);
|
||||
|
||||
let mut era_slash = <Module<T> as Store>::NominatorSlashInEra::get(
|
||||
&slash_era,
|
||||
stash,
|
||||
).unwrap_or(Zero::zero());
|
||||
|
||||
era_slash += own_slash_difference;
|
||||
|
||||
<Module<T> as Store>::NominatorSlashInEra::insert(
|
||||
&slash_era,
|
||||
stash,
|
||||
&era_slash,
|
||||
);
|
||||
|
||||
era_slash
|
||||
};
|
||||
|
||||
// compare the era slash against other eras in the same span.
|
||||
{
|
||||
let mut spans = fetch_spans::<T>(
|
||||
stash,
|
||||
window_start,
|
||||
&mut reward_payout,
|
||||
&mut nom_slashed,
|
||||
reward_proportion,
|
||||
);
|
||||
|
||||
let target_span = spans.compare_and_update_span_slash(
|
||||
slash_era,
|
||||
era_slash,
|
||||
);
|
||||
|
||||
if target_span == Some(spans.span_index()) {
|
||||
// Chill the nominator outright, ending the slashing span.
|
||||
spans.end_span(now);
|
||||
<Module<T>>::chill_stash(stash);
|
||||
}
|
||||
}
|
||||
|
||||
nominators_slashed.push((stash.clone(), nom_slashed));
|
||||
}
|
||||
|
||||
reward_payout
|
||||
}
|
||||
|
||||
// helper struct for managing a set of spans we are currently inspecting.
|
||||
// writes alterations to disk on drop, but only if a slash has been carried out.
|
||||
//
|
||||
// NOTE: alterations to slashing metadata should not be done after this is dropped.
|
||||
// dropping this struct applies any necessary slashes, which can lead to free balance
|
||||
// being 0, and the account being garbage-collected -- a dead account should get no new
|
||||
// metadata.
|
||||
struct InspectingSpans<'a, T: Trait + 'a> {
|
||||
dirty: bool,
|
||||
window_start: EraIndex,
|
||||
stash: &'a T::AccountId,
|
||||
spans: SlashingSpans,
|
||||
paid_out: &'a mut BalanceOf<T>,
|
||||
slash_of: &'a mut BalanceOf<T>,
|
||||
reward_proportion: Perbill,
|
||||
_marker: rstd::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
// fetches the slashing spans record for a stash account, initializing it if necessary.
|
||||
fn fetch_spans<'a, T: Trait + 'a>(
|
||||
stash: &'a T::AccountId,
|
||||
window_start: EraIndex,
|
||||
paid_out: &'a mut BalanceOf<T>,
|
||||
slash_of: &'a mut BalanceOf<T>,
|
||||
reward_proportion: Perbill,
|
||||
) -> InspectingSpans<'a, T> {
|
||||
let spans = <Module<T> as Store>::SlashingSpans::get(stash).unwrap_or_else(|| {
|
||||
let spans = SlashingSpans::new(window_start);
|
||||
<Module<T> as Store>::SlashingSpans::insert(stash, &spans);
|
||||
spans
|
||||
});
|
||||
|
||||
InspectingSpans {
|
||||
dirty: false,
|
||||
window_start,
|
||||
stash,
|
||||
spans,
|
||||
slash_of,
|
||||
paid_out,
|
||||
reward_proportion,
|
||||
_marker: rstd::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'a + Trait> InspectingSpans<'a, T> {
|
||||
fn span_index(&self) -> SpanIndex {
|
||||
self.spans.span_index
|
||||
}
|
||||
|
||||
fn end_span(&mut self, now: EraIndex) {
|
||||
self.dirty = self.spans.end_span(now) || self.dirty;
|
||||
}
|
||||
|
||||
fn add_slash(&mut self, amount: BalanceOf<T>) {
|
||||
*self.slash_of += amount;
|
||||
}
|
||||
|
||||
// find the span index of the given era, if covered.
|
||||
fn era_span(&self, era: EraIndex) -> Option<SlashingSpan> {
|
||||
self.spans.iter().find(|span| span.contains_era(era))
|
||||
}
|
||||
|
||||
// compares the slash in an era to the overall current span slash.
|
||||
// if it's higher, applies the difference of the slashes and then updates the span on disk.
|
||||
//
|
||||
// returns the span index of the era where the slash occurred, if any.
|
||||
fn compare_and_update_span_slash(
|
||||
&mut self,
|
||||
slash_era: EraIndex,
|
||||
slash: BalanceOf<T>,
|
||||
) -> Option<SpanIndex> {
|
||||
let target_span = self.era_span(slash_era)?;
|
||||
let span_slash_key = (self.stash.clone(), target_span.index);
|
||||
let mut span_record = <Module<T> as Store>::SpanSlash::get(&span_slash_key);
|
||||
let mut changed = false;
|
||||
|
||||
let reward = if span_record.slashed < slash {
|
||||
// new maximum span slash. apply the difference.
|
||||
let difference = slash - span_record.slashed;
|
||||
span_record.slashed = slash;
|
||||
|
||||
// compute reward.
|
||||
let reward = REWARD_F1
|
||||
* (self.reward_proportion * slash).saturating_sub(span_record.paid_out);
|
||||
|
||||
self.add_slash(difference);
|
||||
changed = true;
|
||||
|
||||
reward
|
||||
} else if span_record.slashed == slash {
|
||||
// compute reward. no slash difference to apply.
|
||||
REWARD_F1 * (self.reward_proportion * slash).saturating_sub(span_record.paid_out)
|
||||
} else {
|
||||
Zero::zero()
|
||||
};
|
||||
|
||||
if !reward.is_zero() {
|
||||
changed = true;
|
||||
span_record.paid_out += reward;
|
||||
*self.paid_out += reward;
|
||||
}
|
||||
|
||||
if changed {
|
||||
self.dirty = true;
|
||||
<Module<T> as Store>::SpanSlash::insert(&span_slash_key, &span_record);
|
||||
}
|
||||
|
||||
Some(target_span.index)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'a + Trait> Drop for InspectingSpans<'a, T> {
|
||||
fn drop(&mut self) {
|
||||
// only update on disk if we slashed this account.
|
||||
if !self.dirty { return }
|
||||
|
||||
if let Some((start, end)) = self.spans.prune(self.window_start) {
|
||||
for span_index in start..end {
|
||||
<Module<T> as Store>::SpanSlash::remove(&(self.stash.clone(), span_index));
|
||||
}
|
||||
}
|
||||
|
||||
<Module<T> as Store>::SlashingSpans::insert(self.stash, &self.spans);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear slashing metadata for an obsolete era.
|
||||
pub(crate) fn clear_era_metadata<T: Trait>(obsolete_era: EraIndex) {
|
||||
<Module<T> as Store>::ValidatorSlashInEra::remove_prefix(&obsolete_era);
|
||||
<Module<T> as Store>::NominatorSlashInEra::remove_prefix(&obsolete_era);
|
||||
}
|
||||
|
||||
/// Clear slashing metadata for a dead account.
|
||||
pub(crate) fn clear_stash_metadata<T: Trait>(stash: &T::AccountId) {
|
||||
let spans = match <Module<T> as Store>::SlashingSpans::take(stash) {
|
||||
None => return,
|
||||
Some(s) => s,
|
||||
};
|
||||
|
||||
// kill slashing-span metadata for account.
|
||||
//
|
||||
// this can only happen while the account is staked _if_ they are completely slashed.
|
||||
// in that case, they may re-bond, but it would count again as span 0. Further ancient
|
||||
// slashes would slash into this new bond, since metadata has now been cleared.
|
||||
for span in spans.iter() {
|
||||
<Module<T> as Store>::SpanSlash::remove(&(stash.clone(), span.index));
|
||||
}
|
||||
}
|
||||
|
||||
// apply the slash to a stash account, deducting any missing funds from the reward
|
||||
// payout, saturating at 0. this is mildly unfair but also an edge-case that
|
||||
// can only occur when overlapping locked funds have been slashed.
|
||||
fn do_slash<T: Trait>(
|
||||
stash: &T::AccountId,
|
||||
value: BalanceOf<T>,
|
||||
reward_payout: &mut BalanceOf<T>,
|
||||
slashed_imbalance: &mut NegativeImbalanceOf<T>,
|
||||
) {
|
||||
let controller = match <Module<T>>::bonded(stash) {
|
||||
None => return, // defensive: should always exist.
|
||||
Some(c) => c,
|
||||
};
|
||||
|
||||
let mut ledger = match <Module<T>>::ledger(&controller) {
|
||||
Some(ledger) => ledger,
|
||||
None => return, // nothing to do.
|
||||
};
|
||||
|
||||
let value = ledger.slash(value, T::Currency::minimum_balance());
|
||||
|
||||
if !value.is_zero() {
|
||||
let (imbalance, missing) = T::Currency::slash(stash, value);
|
||||
slashed_imbalance.subsume(imbalance);
|
||||
|
||||
if !missing.is_zero() {
|
||||
// deduct overslash from the reward payout
|
||||
*reward_payout = reward_payout.saturating_sub(missing);
|
||||
}
|
||||
|
||||
<Module<T>>::update_ledger(&controller, &ledger);
|
||||
|
||||
// trigger the event
|
||||
<Module<T>>::deposit_event(
|
||||
super::RawEvent::Slash(stash.clone(), value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a previously-unapplied slash.
|
||||
pub(crate) fn apply_slash<T: Trait>(unapplied_slash: UnappliedSlash<T::AccountId, BalanceOf<T>>) {
|
||||
let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero();
|
||||
let mut reward_payout = unapplied_slash.payout;
|
||||
|
||||
do_slash::<T>(
|
||||
&unapplied_slash.validator,
|
||||
unapplied_slash.own,
|
||||
&mut reward_payout,
|
||||
&mut slashed_imbalance,
|
||||
);
|
||||
|
||||
for &(ref nominator, nominator_slash) in &unapplied_slash.others {
|
||||
do_slash::<T>(
|
||||
&nominator,
|
||||
nominator_slash,
|
||||
&mut reward_payout,
|
||||
&mut slashed_imbalance,
|
||||
);
|
||||
}
|
||||
|
||||
pay_reporters::<T>(reward_payout, slashed_imbalance, &unapplied_slash.reporters);
|
||||
}
|
||||
|
||||
|
||||
/// Apply a reward payout to some reporters, paying the rewards out of the slashed imbalance.
|
||||
fn pay_reporters<T: Trait>(
|
||||
reward_payout: BalanceOf<T>,
|
||||
slashed_imbalance: NegativeImbalanceOf<T>,
|
||||
reporters: &[T::AccountId],
|
||||
) {
|
||||
if reward_payout.is_zero() || reporters.is_empty() {
|
||||
// nobody to pay out to or nothing to pay;
|
||||
// just treat the whole value as slashed.
|
||||
T::Slash::on_unbalanced(slashed_imbalance);
|
||||
return
|
||||
}
|
||||
|
||||
// take rewards out of the slashed imbalance.
|
||||
let reward_payout = reward_payout.min(slashed_imbalance.peek());
|
||||
let (mut reward_payout, mut value_slashed) = slashed_imbalance.split(reward_payout);
|
||||
|
||||
let per_reporter = reward_payout.peek() / (reporters.len() as u32).into();
|
||||
for reporter in reporters {
|
||||
let (reporter_reward, rest) = reward_payout.split(per_reporter);
|
||||
reward_payout = rest;
|
||||
|
||||
// this cancels out the reporter reward imbalance internally, leading
|
||||
// to no change in total issuance.
|
||||
T::Currency::resolve_creating(reporter, reporter_reward);
|
||||
}
|
||||
|
||||
// the rest goes to the on-slash imbalance handler (e.g. treasury)
|
||||
value_slashed.subsume(reward_payout); // remainder of reward division remains.
|
||||
T::Slash::on_unbalanced(value_slashed);
|
||||
}
|
||||
|
||||
// TODO: function for undoing a slash.
|
||||
//
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn span_contains_era() {
|
||||
// unbounded end
|
||||
let span = SlashingSpan { index: 0, start: 1000, length: None };
|
||||
assert!(!span.contains_era(0));
|
||||
assert!(!span.contains_era(999));
|
||||
|
||||
assert!(span.contains_era(1000));
|
||||
assert!(span.contains_era(1001));
|
||||
assert!(span.contains_era(10000));
|
||||
|
||||
// bounded end - non-inclusive range.
|
||||
let span = SlashingSpan { index: 0, start: 1000, length: Some(10) };
|
||||
assert!(!span.contains_era(0));
|
||||
assert!(!span.contains_era(999));
|
||||
|
||||
assert!(span.contains_era(1000));
|
||||
assert!(span.contains_era(1001));
|
||||
assert!(span.contains_era(1009));
|
||||
assert!(!span.contains_era(1010));
|
||||
assert!(!span.contains_era(1011));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_slashing_span() {
|
||||
let spans = SlashingSpans {
|
||||
span_index: 0,
|
||||
last_start: 1000,
|
||||
prior: Vec::new(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
spans.iter().collect::<Vec<_>>(),
|
||||
vec![SlashingSpan { index: 0, start: 1000, length: None }],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_prior_spans() {
|
||||
let spans = SlashingSpans {
|
||||
span_index: 10,
|
||||
last_start: 1000,
|
||||
prior: vec![10, 9, 8, 10],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
spans.iter().collect::<Vec<_>>(),
|
||||
vec![
|
||||
SlashingSpan { index: 10, start: 1000, length: None },
|
||||
SlashingSpan { index: 9, start: 990, length: Some(10) },
|
||||
SlashingSpan { index: 8, start: 981, length: Some(9) },
|
||||
SlashingSpan { index: 7, start: 973, length: Some(8) },
|
||||
SlashingSpan { index: 6, start: 963, length: Some(10) },
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pruning_spans() {
|
||||
let mut spans = SlashingSpans {
|
||||
span_index: 10,
|
||||
last_start: 1000,
|
||||
prior: vec![10, 9, 8, 10],
|
||||
};
|
||||
|
||||
assert_eq!(spans.prune(981), Some((6, 8)));
|
||||
assert_eq!(
|
||||
spans.iter().collect::<Vec<_>>(),
|
||||
vec![
|
||||
SlashingSpan { index: 10, start: 1000, length: None },
|
||||
SlashingSpan { index: 9, start: 990, length: Some(10) },
|
||||
SlashingSpan { index: 8, start: 981, length: Some(9) },
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(spans.prune(982), None);
|
||||
assert_eq!(
|
||||
spans.iter().collect::<Vec<_>>(),
|
||||
vec![
|
||||
SlashingSpan { index: 10, start: 1000, length: None },
|
||||
SlashingSpan { index: 9, start: 990, length: Some(10) },
|
||||
SlashingSpan { index: 8, start: 981, length: Some(9) },
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(spans.prune(989), None);
|
||||
assert_eq!(
|
||||
spans.iter().collect::<Vec<_>>(),
|
||||
vec![
|
||||
SlashingSpan { index: 10, start: 1000, length: None },
|
||||
SlashingSpan { index: 9, start: 990, length: Some(10) },
|
||||
SlashingSpan { index: 8, start: 981, length: Some(9) },
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(spans.prune(1000), Some((8, 10)));
|
||||
assert_eq!(
|
||||
spans.iter().collect::<Vec<_>>(),
|
||||
vec![
|
||||
SlashingSpan { index: 10, start: 1000, length: None },
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(spans.prune(2000), None);
|
||||
assert_eq!(
|
||||
spans.iter().collect::<Vec<_>>(),
|
||||
vec![
|
||||
SlashingSpan { index: 10, start: 2000, length: None },
|
||||
],
|
||||
);
|
||||
|
||||
// now all in one shot.
|
||||
let mut spans = SlashingSpans {
|
||||
span_index: 10,
|
||||
last_start: 1000,
|
||||
prior: vec![10, 9, 8, 10],
|
||||
};
|
||||
assert_eq!(spans.prune(2000), Some((6, 10)));
|
||||
assert_eq!(
|
||||
spans.iter().collect::<Vec<_>>(),
|
||||
vec![
|
||||
SlashingSpan { index: 10, start: 2000, length: None },
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ending_span() {
|
||||
let mut spans = SlashingSpans {
|
||||
span_index: 1,
|
||||
last_start: 10,
|
||||
prior: Vec::new(),
|
||||
};
|
||||
|
||||
assert!(spans.end_span(10));
|
||||
|
||||
assert_eq!(
|
||||
spans.iter().collect::<Vec<_>>(),
|
||||
vec![
|
||||
SlashingSpan { index: 2, start: 11, length: None },
|
||||
SlashingSpan { index: 1, start: 10, length: Some(1) },
|
||||
],
|
||||
);
|
||||
|
||||
assert!(spans.end_span(15));
|
||||
assert_eq!(
|
||||
spans.iter().collect::<Vec<_>>(),
|
||||
vec![
|
||||
SlashingSpan { index: 3, start: 16, length: None },
|
||||
SlashingSpan { index: 2, start: 11, length: Some(5) },
|
||||
SlashingSpan { index: 1, start: 10, length: Some(1) },
|
||||
],
|
||||
);
|
||||
|
||||
// does nothing if not a valid end.
|
||||
assert!(!spans.end_span(15));
|
||||
assert_eq!(
|
||||
spans.iter().collect::<Vec<_>>(),
|
||||
vec![
|
||||
SlashingSpan { index: 3, start: 16, length: None },
|
||||
SlashingSpan { index: 2, start: 11, length: Some(5) },
|
||||
SlashingSpan { index: 1, start: 10, length: Some(1) },
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
use super::*;
|
||||
use mock::*;
|
||||
use sr_primitives::{assert_eq_error_rate, traits::OnInitialize};
|
||||
use sr_staking_primitives::offence::{OffenceDetails, OnOffenceHandler};
|
||||
use sr_staking_primitives::offence::OffenceDetails;
|
||||
use support::{assert_ok, assert_noop, traits::{Currency, ReservableCurrency}};
|
||||
use substrate_test_utils::assert_eq_uvec;
|
||||
|
||||
@@ -80,7 +80,7 @@ fn basic_setup_works() {
|
||||
Staking::ledger(100),
|
||||
Some(StakingLedger { stash: 101, total: 500, active: 500, unlocking: vec![] })
|
||||
);
|
||||
assert_eq!(Staking::nominators(101), vec![11, 21]);
|
||||
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
|
||||
|
||||
if cfg!(feature = "equalize") {
|
||||
assert_eq!(
|
||||
@@ -638,7 +638,7 @@ fn nominators_also_get_slashed() {
|
||||
assert_eq!(Balances::total_balance(&2), initial_balance);
|
||||
|
||||
// 10 goes offline
|
||||
Staking::on_offence(
|
||||
on_offence_now(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
@@ -1661,12 +1661,26 @@ fn reward_validator_slashing_validator_doesnt_overflow() {
|
||||
// Set staker
|
||||
let _ = Balances::make_free_balance_be(&11, stake);
|
||||
let _ = Balances::make_free_balance_be(&2, stake);
|
||||
|
||||
// only slashes out of bonded stake are applied. without this line,
|
||||
// it is 0.
|
||||
Staking::bond(Origin::signed(2), 20000, stake - 1, RewardDestination::default()).unwrap();
|
||||
<Stakers<Test>>::insert(&11, Exposure { total: stake, own: 1, others: vec![
|
||||
IndividualExposure { who: 2, value: stake - 1 }
|
||||
]});
|
||||
|
||||
|
||||
// Check slashing
|
||||
let _ = Staking::slash_validator(&11, reward_slash, &Staking::stakers(&11), &mut Vec::new());
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, Staking::stakers(&11)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(100)],
|
||||
);
|
||||
|
||||
assert_eq!(Balances::total_balance(&11), stake - 1);
|
||||
assert_eq!(Balances::total_balance(&2), 1);
|
||||
})
|
||||
@@ -1761,7 +1775,7 @@ fn era_is_always_same_length() {
|
||||
#[test]
|
||||
fn offence_forces_new_era() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
Staking::on_offence(
|
||||
on_offence_now(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
@@ -1781,7 +1795,7 @@ fn offence_ensures_new_era_without_clobbering() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
assert_ok!(Staking::force_new_era_always(Origin::ROOT));
|
||||
|
||||
Staking::on_offence(
|
||||
on_offence_now(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
@@ -1800,7 +1814,7 @@ fn offence_ensures_new_era_without_clobbering() {
|
||||
fn offence_deselects_validator_when_slash_is_zero() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
assert!(<Validators<Test>>::exists(11));
|
||||
Staking::on_offence(
|
||||
on_offence_now(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
@@ -1823,7 +1837,7 @@ fn slashing_performed_according_exposure() {
|
||||
assert_eq!(Staking::stakers(&11).own, 1000);
|
||||
|
||||
// Handle an offence with a historical exposure.
|
||||
Staking::on_offence(
|
||||
on_offence_now(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
@@ -1843,6 +1857,71 @@ fn slashing_performed_according_exposure() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_in_old_span_does_not_deselect() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
assert!(<Validators<Test>>::exists(11));
|
||||
on_offence_now(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
Staking::stakers(&11),
|
||||
),
|
||||
reporters: vec![],
|
||||
}],
|
||||
&[Perbill::from_percent(0)],
|
||||
);
|
||||
assert_eq!(Staking::force_era(), Forcing::ForceNew);
|
||||
assert!(!<Validators<Test>>::exists(11));
|
||||
|
||||
start_era(2);
|
||||
|
||||
Staking::validate(Origin::signed(10), Default::default()).unwrap();
|
||||
assert_eq!(Staking::force_era(), Forcing::NotForcing);
|
||||
assert!(<Validators<Test>>::exists(11));
|
||||
|
||||
start_era(3);
|
||||
|
||||
// this staker is in a new slashing span now, having re-registered after
|
||||
// their prior slash.
|
||||
|
||||
on_offence_in_era(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
Staking::stakers(&11),
|
||||
),
|
||||
reporters: vec![],
|
||||
}],
|
||||
&[Perbill::from_percent(0)],
|
||||
1,
|
||||
);
|
||||
|
||||
// not for zero-slash.
|
||||
assert_eq!(Staking::force_era(), Forcing::NotForcing);
|
||||
assert!(<Validators<Test>>::exists(11));
|
||||
|
||||
on_offence_in_era(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
Staking::stakers(&11),
|
||||
),
|
||||
reporters: vec![],
|
||||
}],
|
||||
&[Perbill::from_percent(100)],
|
||||
1,
|
||||
);
|
||||
|
||||
// or non-zero.
|
||||
assert_eq!(Staking::force_era(), Forcing::NotForcing);
|
||||
assert!(<Validators<Test>>::exists(11));
|
||||
assert_ledger_consistent(11);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reporters_receive_their_slice() {
|
||||
// This test verifies that the reporters of the offence receive their slice from the slashed
|
||||
@@ -1856,7 +1935,7 @@ fn reporters_receive_their_slice() {
|
||||
|
||||
assert_eq!(Staking::stakers(&11).total, initial_balance);
|
||||
|
||||
Staking::on_offence(
|
||||
on_offence_now(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
@@ -1867,10 +1946,63 @@ fn reporters_receive_their_slice() {
|
||||
&[Perbill::from_percent(50)],
|
||||
);
|
||||
|
||||
// initial_balance x 50% (slash fraction) x 10% (rewards slice)
|
||||
let reward = initial_balance / 20 / 2;
|
||||
// F1 * (reward_proportion * slash - 0)
|
||||
// 50% * (10% * initial_balance / 2)
|
||||
let reward = (initial_balance / 20) / 2;
|
||||
let reward_each = reward / 2; // split into two pieces.
|
||||
assert_eq!(Balances::free_balance(&1), 10 + reward_each);
|
||||
assert_eq!(Balances::free_balance(&2), 20 + reward_each);
|
||||
assert_ledger_consistent(11);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subsequent_reports_in_same_span_pay_out_less() {
|
||||
// This test verifies that the reporters of the offence receive their slice from the slashed
|
||||
// amount.
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
// The reporters' reward is calculated from the total exposure.
|
||||
#[cfg(feature = "equalize")]
|
||||
let initial_balance = 1250;
|
||||
#[cfg(not(feature = "equalize"))]
|
||||
let initial_balance = 1125;
|
||||
|
||||
assert_eq!(Staking::stakers(&11).total, initial_balance);
|
||||
|
||||
on_offence_now(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
Staking::stakers(&11),
|
||||
),
|
||||
reporters: vec![1],
|
||||
}],
|
||||
&[Perbill::from_percent(20)],
|
||||
);
|
||||
|
||||
// F1 * (reward_proportion * slash - 0)
|
||||
// 50% * (10% * initial_balance * 20%)
|
||||
let reward = (initial_balance / 5) / 20;
|
||||
assert_eq!(Balances::free_balance(&1), 10 + reward);
|
||||
assert_eq!(Balances::free_balance(&2), 20 + reward);
|
||||
|
||||
on_offence_now(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
Staking::stakers(&11),
|
||||
),
|
||||
reporters: vec![1],
|
||||
}],
|
||||
&[Perbill::from_percent(50)],
|
||||
);
|
||||
|
||||
let prior_payout = reward;
|
||||
|
||||
// F1 * (reward_proportion * slash - prior_payout)
|
||||
// 50% * (10% * (initial_balance / 2) - prior_payout)
|
||||
let reward = ((initial_balance / 20) - prior_payout) / 2;
|
||||
assert_eq!(Balances::free_balance(&1), 10 + prior_payout + reward);
|
||||
assert_ledger_consistent(11);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1878,16 +2010,16 @@ fn reporters_receive_their_slice() {
|
||||
fn invulnerables_are_not_slashed() {
|
||||
// For invulnerable validators no slashing is performed.
|
||||
ExtBuilder::default().invulnerables(vec![11]).build().execute_with(|| {
|
||||
#[cfg(feature = "equalize")]
|
||||
let initial_balance = 1250;
|
||||
#[cfg(not(feature = "equalize"))]
|
||||
let initial_balance = 1375;
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Balances::free_balance(&21), 2000);
|
||||
assert_eq!(Staking::stakers(&21).total, initial_balance);
|
||||
|
||||
Staking::on_offence(
|
||||
let exposure = Staking::stakers(&21);
|
||||
let initial_balance = Staking::slashable_balance_of(&21);
|
||||
|
||||
let nominator_balances: Vec<_> = exposure.others
|
||||
.iter().map(|o| Balances::free_balance(&o.who)).collect();
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, Staking::stakers(&11)),
|
||||
@@ -1905,6 +2037,16 @@ fn invulnerables_are_not_slashed() {
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
// 2000 - (0.2 * initial_balance)
|
||||
assert_eq!(Balances::free_balance(&21), 2000 - (2 * initial_balance / 10));
|
||||
|
||||
// ensure that nominators were slashed as well.
|
||||
for (initial_balance, other) in nominator_balances.into_iter().zip(exposure.others) {
|
||||
assert_eq!(
|
||||
Balances::free_balance(&other.who),
|
||||
initial_balance - (2 * other.value / 10),
|
||||
);
|
||||
}
|
||||
assert_ledger_consistent(11);
|
||||
assert_ledger_consistent(21);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1914,7 +2056,7 @@ fn dont_slash_if_fraction_is_zero() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
|
||||
Staking::on_offence(
|
||||
on_offence_now(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
@@ -1927,5 +2069,462 @@ fn dont_slash_if_fraction_is_zero() {
|
||||
|
||||
// The validator hasn't been slashed. The new era is not forced.
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_ledger_consistent(11);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_slash_for_max_in_era() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, Staking::stakers(&11)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(50)],
|
||||
);
|
||||
|
||||
// The validator has been slashed and has been force-chilled.
|
||||
assert_eq!(Balances::free_balance(&11), 500);
|
||||
assert_eq!(Staking::force_era(), Forcing::ForceNew);
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, Staking::stakers(&11)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(25)],
|
||||
);
|
||||
|
||||
// The validator has not been slashed additionally.
|
||||
assert_eq!(Balances::free_balance(&11), 500);
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, Staking::stakers(&11)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(60)],
|
||||
);
|
||||
|
||||
// The validator got slashed 10% more.
|
||||
assert_eq!(Balances::free_balance(&11), 400);
|
||||
assert_ledger_consistent(11);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garbage_collection_after_slashing() {
|
||||
ExtBuilder::default().existential_deposit(1).build().execute_with(|| {
|
||||
assert_eq!(Balances::free_balance(&11), 256_000);
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, Staking::stakers(&11)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(10)],
|
||||
);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 256_000 - 25_600);
|
||||
assert!(<Staking as crate::Store>::SlashingSpans::get(&11).is_some());
|
||||
assert_eq!(<Staking as crate::Store>::SpanSlash::get(&(11, 0)).amount_slashed(), &25_600);
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, Staking::stakers(&11)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(100)],
|
||||
);
|
||||
|
||||
// validator and nominator slash in era are garbage-collected by era change,
|
||||
// so we don't test those here.
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 0);
|
||||
assert!(<Staking as crate::Store>::SlashingSpans::get(&11).is_none());
|
||||
assert_eq!(<Staking as crate::Store>::SpanSlash::get(&(11, 0)).amount_slashed(), &0);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garbage_collection_on_window_pruning() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
|
||||
let exposure = Staking::stakers(&11);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value;
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, Staking::stakers(&11)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(10)],
|
||||
);
|
||||
|
||||
let now = Staking::current_era();
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 900);
|
||||
assert_eq!(Balances::free_balance(&101), 2000 - (nominated_value / 10));
|
||||
|
||||
assert!(<Staking as crate::Store>::ValidatorSlashInEra::get(&now, &11).is_some());
|
||||
assert!(<Staking as crate::Store>::NominatorSlashInEra::get(&now, &101).is_some());
|
||||
|
||||
// + 1 because we have to exit the bonding window.
|
||||
for era in (0..(BondingDuration::get() + 1)).map(|offset| offset + now + 1) {
|
||||
assert!(<Staking as crate::Store>::ValidatorSlashInEra::get(&now, &11).is_some());
|
||||
assert!(<Staking as crate::Store>::NominatorSlashInEra::get(&now, &101).is_some());
|
||||
|
||||
start_era(era);
|
||||
}
|
||||
|
||||
assert!(<Staking as crate::Store>::ValidatorSlashInEra::get(&now, &11).is_none());
|
||||
assert!(<Staking as crate::Store>::NominatorSlashInEra::get(&now, &101).is_none());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slashing_nominators_by_span_max() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
start_era(1);
|
||||
start_era(2);
|
||||
start_era(3);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Balances::free_balance(&21), 2000);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
assert_eq!(Staking::slashable_balance_of(&21), 1000);
|
||||
|
||||
|
||||
let exposure_11 = Staking::stakers(&11);
|
||||
let exposure_21 = Staking::stakers(&21);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
let nominated_value_11 = exposure_11.others.iter().find(|o| o.who == 101).unwrap().value;
|
||||
let nominated_value_21 = exposure_21.others.iter().find(|o| o.who == 101).unwrap().value;
|
||||
|
||||
on_offence_in_era(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, Staking::stakers(&11)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(10)],
|
||||
2,
|
||||
);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 900);
|
||||
|
||||
let slash_1_amount = Perbill::from_percent(10) * nominated_value_11;
|
||||
assert_eq!(Balances::free_balance(&101), 2000 - slash_1_amount);
|
||||
|
||||
let expected_spans = vec![
|
||||
slashing::SlashingSpan { index: 1, start: 4, length: None },
|
||||
slashing::SlashingSpan { index: 0, start: 0, length: Some(4) },
|
||||
];
|
||||
|
||||
let get_span = |account| <Staking as crate::Store>::SlashingSpans::get(&account).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
get_span(11).iter().collect::<Vec<_>>(),
|
||||
expected_spans,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_span(101).iter().collect::<Vec<_>>(),
|
||||
expected_spans,
|
||||
);
|
||||
|
||||
// second slash: higher era, higher value, same span.
|
||||
on_offence_in_era(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (21, Staking::stakers(&21)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(30)],
|
||||
3,
|
||||
);
|
||||
|
||||
// 11 was not further slashed, but 21 and 101 were.
|
||||
assert_eq!(Balances::free_balance(&11), 900);
|
||||
assert_eq!(Balances::free_balance(&21), 1700);
|
||||
|
||||
let slash_2_amount = Perbill::from_percent(30) * nominated_value_21;
|
||||
assert!(slash_2_amount > slash_1_amount);
|
||||
|
||||
// only the maximum slash in a single span is taken.
|
||||
assert_eq!(Balances::free_balance(&101), 2000 - slash_2_amount);
|
||||
|
||||
// third slash: in same era and on same validator as first, higher
|
||||
// in-era value, but lower slash value than slash 2.
|
||||
on_offence_in_era(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, Staking::stakers(&11)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(20)],
|
||||
2,
|
||||
);
|
||||
|
||||
// 11 was further slashed, but 21 and 101 were not.
|
||||
assert_eq!(Balances::free_balance(&11), 800);
|
||||
assert_eq!(Balances::free_balance(&21), 1700);
|
||||
|
||||
let slash_3_amount = Perbill::from_percent(20) * nominated_value_21;
|
||||
assert!(slash_3_amount < slash_2_amount);
|
||||
assert!(slash_3_amount > slash_1_amount);
|
||||
|
||||
// only the maximum slash in a single span is taken.
|
||||
assert_eq!(Balances::free_balance(&101), 2000 - slash_2_amount);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slashes_are_summed_across_spans() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
start_era(1);
|
||||
start_era(2);
|
||||
start_era(3);
|
||||
|
||||
assert_eq!(Balances::free_balance(&21), 2000);
|
||||
assert_eq!(Staking::slashable_balance_of(&21), 1000);
|
||||
|
||||
let get_span = |account| <Staking as crate::Store>::SlashingSpans::get(&account).unwrap();
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (21, Staking::stakers(&21)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(10)],
|
||||
);
|
||||
|
||||
let expected_spans = vec![
|
||||
slashing::SlashingSpan { index: 1, start: 4, length: None },
|
||||
slashing::SlashingSpan { index: 0, start: 0, length: Some(4) },
|
||||
];
|
||||
|
||||
assert_eq!(get_span(21).iter().collect::<Vec<_>>(), expected_spans);
|
||||
assert_eq!(Balances::free_balance(&21), 1900);
|
||||
|
||||
// 21 has been force-chilled. re-signal intent to validate.
|
||||
Staking::validate(Origin::signed(20), Default::default()).unwrap();
|
||||
|
||||
start_era(4);
|
||||
|
||||
assert_eq!(Staking::slashable_balance_of(&21), 900);
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (21, Staking::stakers(&21)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(10)],
|
||||
);
|
||||
|
||||
let expected_spans = vec![
|
||||
slashing::SlashingSpan { index: 2, start: 5, length: None },
|
||||
slashing::SlashingSpan { index: 1, start: 4, length: Some(1) },
|
||||
slashing::SlashingSpan { index: 0, start: 0, length: Some(4) },
|
||||
];
|
||||
|
||||
assert_eq!(get_span(21).iter().collect::<Vec<_>>(), expected_spans);
|
||||
assert_eq!(Balances::free_balance(&21), 1810);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_slashes_are_deferred() {
|
||||
ExtBuilder::default().slash_defer_duration(2).build().execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
|
||||
let exposure = Staking::stakers(&11);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value;
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, Staking::stakers(&11)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(10)],
|
||||
);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
|
||||
start_era(2);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
|
||||
start_era(3);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
|
||||
// at the start of era 4, slashes from era 1 are processed,
|
||||
// after being deferred for at least 2 full eras.
|
||||
start_era(4);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 900);
|
||||
assert_eq!(Balances::free_balance(&101), 2000 - (nominated_value / 10));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_deferred() {
|
||||
ExtBuilder::default().slash_defer_duration(2).build().execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
|
||||
let exposure = Staking::stakers(&11);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value;
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, exposure.clone()),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(10)],
|
||||
);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
|
||||
start_era(2);
|
||||
|
||||
on_offence_in_era(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, exposure.clone()),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(15)],
|
||||
1,
|
||||
);
|
||||
|
||||
Staking::cancel_deferred_slash(Origin::ROOT, 1, vec![0]).unwrap();
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
|
||||
start_era(3);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
|
||||
// at the start of era 4, slashes from era 1 are processed,
|
||||
// after being deferred for at least 2 full eras.
|
||||
start_era(4);
|
||||
|
||||
// the first slash for 10% was cancelled, so no effect.
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
|
||||
start_era(5);
|
||||
|
||||
let slash_10 = Perbill::from_percent(10);
|
||||
let slash_15 = Perbill::from_percent(15);
|
||||
let initial_slash = slash_10 * nominated_value;
|
||||
|
||||
let total_slash = slash_15 * nominated_value;
|
||||
let actual_slash = total_slash - initial_slash;
|
||||
|
||||
// 5% slash (15 - 10) processed now.
|
||||
assert_eq!(Balances::free_balance(&11), 950);
|
||||
assert_eq!(Balances::free_balance(&101), 2000 - actual_slash);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_multi_deferred() {
|
||||
ExtBuilder::default().slash_defer_duration(2).build().execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
|
||||
let exposure = Staking::stakers(&11);
|
||||
assert_eq!(Balances::free_balance(&101), 2000);
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, exposure.clone()),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(10)],
|
||||
);
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (21, Staking::stakers(&21)),
|
||||
reporters: vec![],
|
||||
}
|
||||
],
|
||||
&[Perbill::from_percent(10)],
|
||||
);
|
||||
|
||||
|
||||
on_offence_now(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, exposure.clone()),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(25)],
|
||||
);
|
||||
|
||||
assert_eq!(<Staking as Store>::UnappliedSlashes::get(&1).len(), 3);
|
||||
Staking::cancel_deferred_slash(Origin::ROOT, 1, vec![0, 2]).unwrap();
|
||||
|
||||
let slashes = <Staking as Store>::UnappliedSlashes::get(&1);
|
||||
assert_eq!(slashes.len(), 1);
|
||||
assert_eq!(slashes[0].validator, 21);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_initialized() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
assert_eq!(<Staking as Store>::StorageVersion::get(), crate::migration::CURRENT_VERSION);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,9 +117,12 @@ pub trait OnOffenceHandler<Reporter, Offender> {
|
||||
/// the authorities should be slashed and is computed
|
||||
/// according to the `OffenceCount` already. This is of the same length as `offenders.`
|
||||
/// Zero is a valid value for a fraction.
|
||||
///
|
||||
/// The `session` parameter is the session index of the offence.
|
||||
fn on_offence(
|
||||
offenders: &[OffenceDetails<Reporter, Offender>],
|
||||
slash_fraction: &[Perbill],
|
||||
session: SessionIndex,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -127,6 +130,7 @@ impl<Reporter, Offender> OnOffenceHandler<Reporter, Offender> for () {
|
||||
fn on_offence(
|
||||
_offenders: &[OffenceDetails<Reporter, Offender>],
|
||||
_slash_fraction: &[Perbill],
|
||||
_session: SessionIndex,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user