mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-18 10:41:01 +00:00
Offences reporting and slashing (#3322)
* Remove offline slashing logic from staking. * Initial version of reworked offence module, can report offences * Clean up staking example. * Commit SlashingOffence * Force new era on slash. * Add offenders in the SlashingOffence trait. * Introduce the ReportOffence trait. * Rename `Offence`. * Add on_before_session_ending handler. * Move offence related stuff under sr-primitives. * Fix cargo check. * Import new im-online implementation. * Adding validator count to historical session storage as it's needed for slash calculations * Add a comment about offence. * Add BabeEquivocationOffence * GrandpaEquivocationOffence * slash_fraction and fix * current_era_start_session_index * UnresponsivnessOffence * Finalise OnOffenceHandler traits, and stub impl for staking. * slash_fraction doesn't really need &self * Note that offenders count is greater than 0 * Add a test to ensure that I got the math right * Use FullIdentification in offences. * Use FullIndentification. * Hook up the offences module. * Report unresponsive validators * Make sure eras have the same length. * Slashing and rewards. * Fix compilation. * Distribute rewards. * Supply validators_count * Use identificationTuple in Unresponsivness report * Fix merge. * Make sure we don't slash if amount is zero. * We don't return an error from report_offence anymo * We actually can use vec! * Prevent division by zero if the reporters is empty * offence_forces_new_era/nominators_also_get_slashed * advance_session * Fix tests. * Update srml/staking/src/lib.rs Co-Authored-By: Robert Habermeier <rphmeier@gmail.com> * slashing_performed_according_exposure * Check that reporters receive their slice. * Small clean-up. * invulnerables_are_not_slashed * Minor clean ups. * Improve docs. * dont_slash_if_fraction_is_zero * Remove session dependency from offences. * Introduce sr-staking-primitives * Move offence under sr_staking_primitives * rename session_index * Resolves todos re using SessionIndex * Fix staking tests. * Properly scale denominator. * Fix UnresponsivnessOffence * Fix compilation. * Tests for offences. * Clean offences tests. * Fix staking doc test. * Bump spec version * Fix aura tests. * Fix node_executor * Deposit an event on offence. * Fix compilation of node-runtime * Remove aura slashing logic. * Remove HandleReport * Update docs for timeslot. * rename with_on_offence_fractions * Add should_properly_count_offences * Replace ValidatorIdByIndex with CurrentElectedSet ValidatorIdByIndex was querying the current_elected set in each call, doing loading (even though its from cache), deserializing and cloning of element. Instead of this it is more efficient to use `CurrentElectedSet`. As a small bonus, the invariant became a little bit easier: now we just rely on the fact that `keys` and `current_elected` set are of the same length rather than relying on the fact that `validator_id_by_index` would work similar to `<[T]>::get`. * Clarify babe equivocation * Fix offences. * Rename validators_count to validator_set_count * Fix squaring. * Update core/sr-staking-primitives/src/offence.rs Co-Authored-By: Gavin Wood <gavin@parity.io> * Docs for CurrentElectedSet. * Don't punish only invulnerables * Use `get/insert` instead of `mutate`. * Fix compilation * Update core/sr-staking-primitives/src/offence.rs Co-Authored-By: Gavin Wood <gavin@parity.io> * Update srml/offences/src/lib.rs Co-Authored-By: Robert Habermeier <rphmeier@gmail.com> * Update srml/im-online/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/im-online/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/im-online/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/babe/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update core/sr-staking-primitives/src/offence.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update core/sr-staking-primitives/src/offence.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update core/sr-staking-primitives/src/offence.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update core/sr-staking-primitives/src/offence.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update core/sr-staking-primitives/src/offence.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Add aura todo. * Allow multiple reports for single offence report. * Fix slash_fraction calculation. * Fix typos. * Fix compilation and tests. * Fix staking tests. * Update srml/im-online/src/lib.rs Co-Authored-By: Logan Saether <x@logansaether.com> * Fix doc on time_slot * Allow slashing only on current era (#3411) * only slash in current era * prune journal for last era * comment own_slash * emit an event when old slashing events are discarded * Pave the way for pruning * Address issues. * Try to refactor collect_offence_reports * Other fixes. * More fixes.
This commit is contained in:
committed by
Gavin Wood
parent
99f3f07690
commit
6cc4495700
+190
-137
@@ -133,7 +133,7 @@
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ### Example: Reporting Misbehavior
|
||||
//! ### Example: Rewarding a validator by id.
|
||||
//!
|
||||
//! ```
|
||||
//! use srml_support::{decl_module, dispatch::Result};
|
||||
@@ -144,10 +144,10 @@
|
||||
//!
|
||||
//! decl_module! {
|
||||
//! pub struct Module<T: Trait> for enum Call where origin: T::Origin {
|
||||
//! /// Report whoever calls this function as offline once.
|
||||
//! pub fn report_sender(origin) -> Result {
|
||||
//! /// Reward a validator.
|
||||
//! pub fn reward_myself(origin) -> Result {
|
||||
//! let reported = ensure_signed(origin)?;
|
||||
//! <staking::Module<T>>::on_offline_validator(reported, 1);
|
||||
//! <staking::Module<T>>::reward_by_ids(vec![(reported, 10)]);
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! }
|
||||
@@ -203,28 +203,6 @@
|
||||
//! - Stash account, not increasing the staked value.
|
||||
//! - Stash account, also increasing the staked value.
|
||||
//!
|
||||
//! ### Slashing details
|
||||
//!
|
||||
//! A validator can be _reported_ to be offline at any point via the public function
|
||||
//! [`on_offline_validator`](enum.Call.html#variant.on_offline_validator). Each validator declares
|
||||
//! how many times it can be _reported_ before it actually gets slashed via its
|
||||
//! [`ValidatorPrefs::unstake_threshold`](./struct.ValidatorPrefs.html#structfield.unstake_threshold).
|
||||
//!
|
||||
//! On top of this, the Staking module also introduces an
|
||||
//! [`OfflineSlashGrace`](./struct.Module.html#method.offline_slash_grace), which applies
|
||||
//! to all validators and prevents them from getting immediately slashed.
|
||||
//!
|
||||
//! Essentially, a validator gets slashed once they have been reported more than
|
||||
//! [`OfflineSlashGrace`] + [`ValidatorPrefs::unstake_threshold`] times. Getting slashed due to
|
||||
//! offline report always leads to being _unstaked_ (_i.e._ removed as a validator candidate) as
|
||||
//! the consequence.
|
||||
//!
|
||||
//! The base slash value is computed _per slash-event_ by multiplying
|
||||
//! [`OfflineSlash`](./struct.Module.html#method.offline_slash) and the `total` `Exposure`. This
|
||||
//! value is then multiplied by `2.pow(unstake_threshold)` to obtain the final slash value. All
|
||||
//! individual accounts' punishments are capped at their total stake (NOTE: This cap should never
|
||||
//! come into force in a correctly implemented, non-corrupted, well-configured system).
|
||||
//!
|
||||
//! ### Additional Fund Management Operations
|
||||
//!
|
||||
//! Any funds already placed into stash can be the target of the following operations:
|
||||
@@ -293,12 +271,16 @@ use srml_support::{
|
||||
WithdrawReasons, WithdrawReason, OnUnbalanced, Imbalance, Get, Time
|
||||
}
|
||||
};
|
||||
use session::{historical::OnSessionEnding, SelectInitialValidators, SessionIndex};
|
||||
use session::{historical::OnSessionEnding, SelectInitialValidators};
|
||||
use sr_primitives::Perbill;
|
||||
use sr_primitives::weights::SimpleDispatchInfo;
|
||||
use sr_primitives::traits::{
|
||||
Convert, Zero, One, StaticLookup, CheckedSub, CheckedShl, Saturating, Bounded,
|
||||
SaturatedConversion, SimpleArithmetic
|
||||
Convert, Zero, One, StaticLookup, CheckedSub, Saturating, Bounded,
|
||||
SimpleArithmetic, SaturatedConversion,
|
||||
};
|
||||
use sr_staking_primitives::{
|
||||
SessionIndex, CurrentElectedSet,
|
||||
offence::{OnOffenceHandler, OffenceDetails, Offence, ReportOffence},
|
||||
};
|
||||
#[cfg(feature = "std")]
|
||||
use sr_primitives::{Serialize, Deserialize};
|
||||
@@ -306,10 +288,8 @@ use system::{ensure_signed, ensure_root};
|
||||
|
||||
use phragmen::{elect, ACCURACY, ExtendedBalance, equalize};
|
||||
|
||||
const RECENT_OFFLINE_COUNT: usize = 32;
|
||||
const DEFAULT_MINIMUM_VALIDATOR_COUNT: u32 = 4;
|
||||
const MAX_NOMINATIONS: usize = 16;
|
||||
const MAX_UNSTAKE_THRESHOLD: u32 = 10;
|
||||
const MAX_UNLOCKING_CHUNKS: usize = 32;
|
||||
const STAKING_ID: LockIdentifier = *b"staking ";
|
||||
|
||||
@@ -371,9 +351,6 @@ impl Default for RewardDestination {
|
||||
#[derive(PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[cfg_attr(feature = "std", derive(Debug))]
|
||||
pub struct ValidatorPrefs<Balance: HasCompact> {
|
||||
/// Validator should ensure this many more slashes than is necessary before being unstaked.
|
||||
#[codec(compact)]
|
||||
pub unstake_threshold: u32,
|
||||
/// Reward that validator takes up-front; only the rest is split between themselves and
|
||||
/// nominators.
|
||||
#[codec(compact)]
|
||||
@@ -383,7 +360,6 @@ pub struct ValidatorPrefs<Balance: HasCompact> {
|
||||
impl<B: Default + HasCompact + Copy> Default for ValidatorPrefs<B> {
|
||||
fn default() -> Self {
|
||||
ValidatorPrefs {
|
||||
unstake_threshold: 3,
|
||||
validator_payment: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -465,6 +441,15 @@ 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)]
|
||||
#[cfg_attr(feature = "std", derive(Debug))]
|
||||
pub struct SlashJournalEntry<AccountId, Balance: HasCompact> {
|
||||
who: AccountId,
|
||||
amount: Balance,
|
||||
own_slash: Balance, // the amount of `who`'s own exposure that was slashed
|
||||
}
|
||||
|
||||
pub type BalanceOf<T> =
|
||||
<<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
|
||||
type PositiveImbalanceOf<T> =
|
||||
@@ -492,7 +477,7 @@ pub trait SessionInterface<AccountId>: system::Trait {
|
||||
/// Get the validators from session.
|
||||
fn validators() -> Vec<AccountId>;
|
||||
/// Prune historical session tries up to but not including the given index.
|
||||
fn prune_historical_up_to(up_to: session::SessionIndex);
|
||||
fn prune_historical_up_to(up_to: SessionIndex);
|
||||
}
|
||||
|
||||
impl<T: Trait> SessionInterface<<T as system::Trait>::AccountId> for T where
|
||||
@@ -514,7 +499,7 @@ impl<T: Trait> SessionInterface<<T as system::Trait>::AccountId> for T where
|
||||
<session::Module<T>>::validators()
|
||||
}
|
||||
|
||||
fn prune_historical_up_to(up_to: session::SessionIndex) {
|
||||
fn prune_historical_up_to(up_to: SessionIndex) {
|
||||
<session::historical::Module<T>>::prune_up_to(up_to);
|
||||
}
|
||||
}
|
||||
@@ -579,10 +564,6 @@ decl_storage! {
|
||||
/// Minimum number of staking participants before emergency conditions are imposed.
|
||||
pub MinimumValidatorCount get(minimum_validator_count) config():
|
||||
u32 = DEFAULT_MINIMUM_VALIDATOR_COUNT;
|
||||
/// Slash, per validator that is taken for the first time they are found to be offline.
|
||||
pub OfflineSlash get(offline_slash) config(): Perbill = Perbill::from_millionths(1000);
|
||||
/// Number of instances of offline reports before slashing begins for validators.
|
||||
pub OfflineSlashGrace get(offline_slash_grace) config(): u32;
|
||||
|
||||
/// Any validators that may never be slashed or forcibly kicked. It's a Vec since they're
|
||||
/// easy to initialize and the performance hit is minimal (we expect no more than four
|
||||
@@ -632,19 +613,20 @@ decl_storage! {
|
||||
config.stakers.iter().map(|&(_, _, value, _)| value).min().unwrap_or_default()
|
||||
}): BalanceOf<T>;
|
||||
|
||||
/// The number of times a given validator has been reported offline. This gets decremented
|
||||
/// by one each era that passes.
|
||||
pub SlashCount get(slash_count): map T::AccountId => u32;
|
||||
|
||||
/// Most recent `RECENT_OFFLINE_COUNT` instances. (Who it was, when it was reported, how
|
||||
/// many instances they were offline for).
|
||||
pub RecentlyOffline get(recently_offline): Vec<(T::AccountId, T::BlockNumber, u32)>;
|
||||
|
||||
/// True if the next session change will be a new era regardless of index.
|
||||
pub ForceEra get(force_era) config(): Forcing;
|
||||
|
||||
/// The percentage of the slash that is distributed to reporters.
|
||||
///
|
||||
/// The rest of the slashed value is handled by the `Slash`.
|
||||
pub SlashRewardFraction get(slash_reward_fraction) config(): Perbill;
|
||||
|
||||
/// 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(era_slash_journal):
|
||||
map EraIndex => Vec<SlashJournalEntry<T::AccountId, BalanceOf<T>>>;
|
||||
}
|
||||
add_extra_genesis {
|
||||
config(stakers):
|
||||
@@ -688,11 +670,11 @@ decl_event!(
|
||||
pub enum Event<T> where Balance = BalanceOf<T>, <T as system::Trait>::AccountId {
|
||||
/// All validators have been rewarded by the given balance.
|
||||
Reward(Balance),
|
||||
/// One validator (and its nominators) has been given an offline-warning (it is still
|
||||
/// within its grace). The accrued number of slashes is recorded, too.
|
||||
OfflineWarning(AccountId, u32),
|
||||
/// One validator (and its nominators) has been slashed by the given amount.
|
||||
OfflineSlash(AccountId, Balance),
|
||||
Slash(AccountId, Balance),
|
||||
/// An old slashing report from a prior era was discarded because it could
|
||||
/// not be processed.
|
||||
OldSlashingReportDiscarded(SessionIndex),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -895,10 +877,6 @@ decl_module! {
|
||||
let controller = ensure_signed(origin)?;
|
||||
let ledger = Self::ledger(&controller).ok_or("not a controller")?;
|
||||
let stash = &ledger.stash;
|
||||
ensure!(
|
||||
prefs.unstake_threshold <= MAX_UNSTAKE_THRESHOLD,
|
||||
"unstake threshold too large"
|
||||
);
|
||||
<Nominators<T>>::remove(stash);
|
||||
<Validators<T>>::insert(stash, prefs);
|
||||
}
|
||||
@@ -1027,13 +1005,6 @@ decl_module! {
|
||||
ForceEra::put(Forcing::ForceNew);
|
||||
}
|
||||
|
||||
/// Set the offline slash grace period.
|
||||
#[weight = SimpleDispatchInfo::FixedOperational(10_000)]
|
||||
fn set_offline_slash_grace(origin, #[compact] new: u32) {
|
||||
ensure_root(origin)?;
|
||||
OfflineSlashGrace::put(new);
|
||||
}
|
||||
|
||||
/// Set the validators who cannot be slashed (if any).
|
||||
#[weight = SimpleDispatchInfo::FixedOperational(10_000)]
|
||||
fn set_invulnerables(origin, validators: Vec<T::AccountId>) {
|
||||
@@ -1070,20 +1041,42 @@ impl<T: Trait> Module<T> {
|
||||
<Ledger<T>>::insert(controller, ledger);
|
||||
}
|
||||
|
||||
/// Slash a given validator by a specific amount. Removes the slash from the validator's
|
||||
/// balance by preference, and reduces the nominators' balance if needed.
|
||||
fn slash_validator(stash: &T::AccountId, slash: BalanceOf<T>) {
|
||||
// The exposure (backing stake) information of the validator to be slashed.
|
||||
let exposure = Self::stakers(stash);
|
||||
/// 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);
|
||||
|
||||
// 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 = exposure.own.min(slash);
|
||||
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.
|
||||
// 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.
|
||||
@@ -1096,7 +1089,19 @@ impl<T: Trait> Module<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
T::Slash::on_unbalanced(imbalance);
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// Actually make a payment to a staker. This uses the currency's reward function
|
||||
@@ -1154,9 +1159,10 @@ impl<T: Trait> Module<T> {
|
||||
fn new_session(session_index: SessionIndex)
|
||||
-> Option<(Vec<T::AccountId>, Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)>)>
|
||||
{
|
||||
let era_length = session_index.checked_sub(Self::current_era_start_session_index()).unwrap_or(0);
|
||||
match ForceEra::get() {
|
||||
Forcing::ForceNew => ForceEra::kill(),
|
||||
Forcing::NotForcing if session_index % T::SessionsPerEra::get() == 0 => (),
|
||||
Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => (),
|
||||
_ => return None,
|
||||
}
|
||||
let validators = T::SessionInterface::validators();
|
||||
@@ -1210,6 +1216,10 @@ 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;
|
||||
});
|
||||
@@ -1325,13 +1335,9 @@ impl<T: Trait> Module<T> {
|
||||
equalize::<T>(&mut assignments_with_votes, &mut exposures, tolerance, iterations);
|
||||
}
|
||||
|
||||
// Clear Stakers and reduce their slash_count.
|
||||
// Clear Stakers.
|
||||
for v in Self::current_elected().iter() {
|
||||
<Stakers<T>>::remove(v);
|
||||
let slash_count = <SlashCount<T>>::take(v);
|
||||
if slash_count > 1 {
|
||||
<SlashCount<T>>::insert(v, slash_count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate Stakers and figure out the minimum stake behind a slot.
|
||||
@@ -1371,68 +1377,10 @@ impl<T: Trait> Module<T> {
|
||||
<Ledger<T>>::remove(&controller);
|
||||
}
|
||||
<Payee<T>>::remove(stash);
|
||||
<SlashCount<T>>::remove(stash);
|
||||
<Validators<T>>::remove(stash);
|
||||
<Nominators<T>>::remove(stash);
|
||||
}
|
||||
|
||||
/// Call when a validator is determined to be offline. `count` is the
|
||||
/// number of offenses the validator has committed.
|
||||
///
|
||||
/// NOTE: This is called with the controller (not the stash) account id.
|
||||
pub fn on_offline_validator(controller: T::AccountId, count: usize) {
|
||||
if let Some(l) = Self::ledger(&controller) {
|
||||
let stash = l.stash;
|
||||
|
||||
// Early exit if validator is invulnerable.
|
||||
if Self::invulnerables().contains(&stash) {
|
||||
return
|
||||
}
|
||||
|
||||
let slash_count = Self::slash_count(&stash);
|
||||
let new_slash_count = slash_count + count as u32;
|
||||
<SlashCount<T>>::insert(&stash, new_slash_count);
|
||||
let grace = Self::offline_slash_grace();
|
||||
|
||||
if RECENT_OFFLINE_COUNT > 0 {
|
||||
let item = (stash.clone(), <system::Module<T>>::block_number(), count as u32);
|
||||
<RecentlyOffline<T>>::mutate(|v| if v.len() >= RECENT_OFFLINE_COUNT {
|
||||
let index = v.iter()
|
||||
.enumerate()
|
||||
.min_by_key(|(_, (_, block, _))| block)
|
||||
.expect("v is non-empty; qed")
|
||||
.0;
|
||||
v[index] = item;
|
||||
} else {
|
||||
v.push(item);
|
||||
});
|
||||
}
|
||||
|
||||
let prefs = Self::validators(&stash);
|
||||
let unstake_threshold = prefs.unstake_threshold.min(MAX_UNSTAKE_THRESHOLD);
|
||||
let max_slashes = grace + unstake_threshold;
|
||||
|
||||
let event = if new_slash_count > max_slashes {
|
||||
let slash_exposure = Self::stakers(&stash).total;
|
||||
let offline_slash_base = Self::offline_slash() * slash_exposure;
|
||||
// They're bailing.
|
||||
let slash = offline_slash_base
|
||||
// Multiply slash_mantissa by 2^(unstake_threshold with upper bound)
|
||||
.checked_shl(unstake_threshold)
|
||||
.map(|x| x.min(slash_exposure))
|
||||
.unwrap_or(slash_exposure);
|
||||
let _ = Self::slash_validator(&stash, slash);
|
||||
let _ = T::SessionInterface::disable_validator(&stash);
|
||||
|
||||
RawEvent::OfflineSlash(stash.clone(), slash)
|
||||
} else {
|
||||
RawEvent::OfflineWarning(stash.clone(), slash_count)
|
||||
};
|
||||
|
||||
Self::deposit_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add reward points to validators using their stash account ID.
|
||||
///
|
||||
/// Validators are keyed by stash account ID and must be in the current elected set.
|
||||
@@ -1564,3 +1512,108 @@ impl<T: Trait> SelectInitialValidators<T::AccountId> for Module<T> {
|
||||
<Module<T>>::select_validators().1
|
||||
}
|
||||
}
|
||||
|
||||
/// This is intended to be used with `FilterHistoricalOffences`.
|
||||
impl <T: Trait> OnOffenceHandler<T::AccountId, session::historical::IdentificationTuple<T>> for Module<T> where
|
||||
T: session::Trait<ValidatorId = <T as system::Trait>::AccountId>,
|
||||
T: session::historical::Trait<
|
||||
FullIdentification = Exposure<<T as system::Trait>::AccountId, BalanceOf<T>>,
|
||||
FullIdentificationOf = ExposureOf<T>,
|
||||
>,
|
||||
T::SessionHandler: session::SessionHandler<<T as system::Trait>::AccountId>,
|
||||
T::OnSessionEnding: session::OnSessionEnding<<T as system::Trait>::AccountId>,
|
||||
T::SelectInitialValidators: session::SelectInitialValidators<<T as system::Trait>::AccountId>,
|
||||
T::ValidatorIdOf: Convert<<T as system::Trait>::AccountId, Option<<T as system::Trait>::AccountId>>
|
||||
{
|
||||
fn on_offence(
|
||||
offenders: &[OffenceDetails<T::AccountId, session::historical::IdentificationTuple<T>>],
|
||||
slash_fraction: &[Perbill],
|
||||
) {
|
||||
let mut remaining_imbalance = <NegativeImbalanceOf<T>>::zero();
|
||||
let slash_reward_fraction = SlashRewardFraction::get();
|
||||
|
||||
let era_now = Self::current_era();
|
||||
let mut journal = Self::era_slash_journal(era_now);
|
||||
for (details, slash_fraction) in offenders.iter().zip(slash_fraction) {
|
||||
let stash = &details.offender.0;
|
||||
let exposure = &details.offender.1;
|
||||
|
||||
// Skip if the validator is invulnerable.
|
||||
if Self::invulnerables().contains(stash) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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 in next sessions
|
||||
let _ = T::SessionInterface::disable_validator(stash);
|
||||
// force a new era, to select a new validator set
|
||||
ForceEra::put(Forcing::ForceNew);
|
||||
// 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);
|
||||
}
|
||||
// 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.
|
||||
pub struct FilterHistoricalOffences<T, R> {
|
||||
_inner: rstd::marker::PhantomData<(T, R)>,
|
||||
}
|
||||
|
||||
impl<T, Reporter, Offender, R, O> ReportOffence<Reporter, Offender, O>
|
||||
for FilterHistoricalOffences<Module<T>, R> where
|
||||
T: Trait,
|
||||
R: ReportOffence<Reporter, Offender, O>,
|
||||
O: Offence<Offender>,
|
||||
{
|
||||
fn report_offence(reporters: Vec<Reporter>, offence: O) {
|
||||
// disallow any slashing from before the current era.
|
||||
let offence_session = offence.session_index();
|
||||
if offence_session >= <Module<T>>::current_era_start_session_index() {
|
||||
R::report_offence(reporters, offence)
|
||||
} else {
|
||||
<Module<T>>::deposit_event(
|
||||
RawEvent::OldSlashingReportDiscarded(offence_session).into()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the currently elected validator set represented by their stash accounts.
|
||||
pub struct CurrentElectedStashAccounts<T>(rstd::marker::PhantomData<T>);
|
||||
|
||||
impl<T: Trait> CurrentElectedSet<T::AccountId> for CurrentElectedStashAccounts<T> {
|
||||
fn current_elected_set() -> Vec<T::AccountId> {
|
||||
<Module<T>>::current_elected()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ use std::{collections::HashSet, cell::RefCell};
|
||||
use sr_primitives::Perbill;
|
||||
use sr_primitives::traits::{IdentityLookup, Convert, OpaqueKeys, OnInitialize};
|
||||
use sr_primitives::testing::{Header, UintAuthorityId};
|
||||
use sr_staking_primitives::SessionIndex;
|
||||
use primitives::{H256, Blake2Hasher};
|
||||
use runtime_io;
|
||||
use srml_support::{assert_ok, impl_outer_origin, parameter_types, EnumerableStorageMap};
|
||||
@@ -73,8 +74,8 @@ impl session::SessionHandler<AccountId> for TestSessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_disabled(validator: AccountId) -> bool {
|
||||
let stash = Staking::ledger(&validator).unwrap().stash;
|
||||
pub fn is_disabled(controller: AccountId) -> bool {
|
||||
let stash = Staking::ledger(&controller).unwrap().stash;
|
||||
SESSION.with(|d| d.borrow().1.contains(&stash))
|
||||
}
|
||||
|
||||
@@ -181,7 +182,7 @@ impl timestamp::Trait for Test {
|
||||
type MinimumPeriod = MinimumPeriod;
|
||||
}
|
||||
parameter_types! {
|
||||
pub const SessionsPerEra: session::SessionIndex = 3;
|
||||
pub const SessionsPerEra: SessionIndex = 3;
|
||||
pub const BondingDuration: EraIndex = 3;
|
||||
}
|
||||
impl Trait for Test {
|
||||
@@ -205,6 +206,7 @@ pub struct ExtBuilder {
|
||||
minimum_validator_count: u32,
|
||||
fair: bool,
|
||||
num_validators: Option<u32>,
|
||||
invulnerables: Vec<u64>,
|
||||
}
|
||||
|
||||
impl Default for ExtBuilder {
|
||||
@@ -217,6 +219,7 @@ impl Default for ExtBuilder {
|
||||
minimum_validator_count: 0,
|
||||
fair: true,
|
||||
num_validators: None,
|
||||
invulnerables: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,6 +253,10 @@ impl ExtBuilder {
|
||||
self.num_validators = Some(num_validators);
|
||||
self
|
||||
}
|
||||
pub fn invulnerables(mut self, invulnerables: Vec<u64>) -> Self {
|
||||
self.invulnerables = invulnerables;
|
||||
self
|
||||
}
|
||||
pub fn set_associated_consts(&self) {
|
||||
EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit);
|
||||
}
|
||||
@@ -300,6 +307,7 @@ impl ExtBuilder {
|
||||
let _ = GenesisConfig::<Test>{
|
||||
current_era: 0,
|
||||
stakers: vec![
|
||||
// (stash, controller, staked_amount, status)
|
||||
(11, 10, balance_factor * 1000, StakerStatus::<AccountId>::Validator),
|
||||
(21, 20, stake_21, StakerStatus::<AccountId>::Validator),
|
||||
(31, 30, stake_31, StakerStatus::<AccountId>::Validator),
|
||||
@@ -309,10 +317,9 @@ impl ExtBuilder {
|
||||
],
|
||||
validator_count: self.validator_count,
|
||||
minimum_validator_count: self.minimum_validator_count,
|
||||
offline_slash: Perbill::from_percent(5),
|
||||
offline_slash_grace: 0,
|
||||
invulnerables: vec![],
|
||||
.. Default::default()
|
||||
invulnerables: self.invulnerables,
|
||||
slash_reward_fraction: Perbill::from_percent(10),
|
||||
..Default::default()
|
||||
}.assimilate_storage(&mut storage);
|
||||
|
||||
let _ = session::GenesisConfig::<Test> {
|
||||
@@ -398,7 +405,12 @@ pub fn bond_nominator(acc: u64, val: u64, target: Vec<u64>) {
|
||||
assert_ok!(Staking::nominate(Origin::signed(acc), target));
|
||||
}
|
||||
|
||||
pub fn start_session(session_index: session::SessionIndex) {
|
||||
pub fn advance_session() {
|
||||
let current_index = Session::current_index();
|
||||
start_session(current_index + 1);
|
||||
}
|
||||
|
||||
pub fn start_session(session_index: SessionIndex) {
|
||||
// Compensate for session delay
|
||||
let session_index = session_index + 1;
|
||||
for i in Session::current_index()..session_index {
|
||||
|
||||
+180
-230
@@ -20,6 +20,7 @@ use super::*;
|
||||
use runtime_io::with_externalities;
|
||||
use phragmen;
|
||||
use sr_primitives::traits::OnInitialize;
|
||||
use sr_staking_primitives::offence::{OffenceDetails, OnOffenceHandler};
|
||||
use srml_support::{assert_ok, assert_noop, assert_eq_uvec, EnumerableStorageMap};
|
||||
use mock::*;
|
||||
use srml_support::traits::{Currency, ReservableCurrency};
|
||||
@@ -41,11 +42,11 @@ fn basic_setup_works() {
|
||||
// Account 1 does not control any stash
|
||||
assert_eq!(Staking::ledger(&1), None);
|
||||
|
||||
// ValidatorPrefs are default, thus unstake_threshold is 3, other values are default for their type
|
||||
// ValidatorPrefs are default
|
||||
assert_eq!(<Validators<Test>>::enumerate().collect::<Vec<_>>(), vec![
|
||||
(31, ValidatorPrefs { unstake_threshold: 3, validator_payment: 0 }),
|
||||
(21, ValidatorPrefs { unstake_threshold: 3, validator_payment: 0 }),
|
||||
(11, ValidatorPrefs { unstake_threshold: 3, validator_payment: 0 })
|
||||
(31, ValidatorPrefs::default()),
|
||||
(21, ValidatorPrefs::default()),
|
||||
(11, ValidatorPrefs::default())
|
||||
]);
|
||||
|
||||
// Account 100 is the default nominator
|
||||
@@ -83,9 +84,12 @@ fn basic_setup_works() {
|
||||
// Initial Era and session
|
||||
assert_eq!(Staking::current_era(), 0);
|
||||
|
||||
// initial slash_count of validators
|
||||
assert_eq!(Staking::slash_count(&11), 0);
|
||||
assert_eq!(Staking::slash_count(&21), 0);
|
||||
// Account 10 has `balance_factor` free balance
|
||||
assert_eq!(Balances::free_balance(&10), 1);
|
||||
assert_eq!(Balances::free_balance(&10), 1);
|
||||
|
||||
// New era is not being forced
|
||||
assert_eq!(Staking::force_era(), Forcing::NotForcing);
|
||||
|
||||
// All exposures must be correct.
|
||||
check_exposure_all();
|
||||
@@ -93,25 +97,6 @@ fn basic_setup_works() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_offline_should_work() {
|
||||
// Test the staking module works when no validators are offline
|
||||
with_externalities(&mut ExtBuilder::default().build(),
|
||||
|| {
|
||||
// Slashing begins for validators immediately if found offline
|
||||
assert_eq!(Staking::offline_slash_grace(), 0);
|
||||
// Account 10 has not been reported offline
|
||||
assert_eq!(Staking::slash_count(&10), 0);
|
||||
// Account 10 has `balance_factor` free balance
|
||||
assert_eq!(Balances::free_balance(&10), 1);
|
||||
// Nothing happens to Account 10, as expected
|
||||
assert_eq!(Staking::slash_count(&10), 0);
|
||||
assert_eq!(Balances::free_balance(&10), 1);
|
||||
// New era is not being forced
|
||||
assert_eq!(Staking::force_era(), Forcing::NotForcing);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_controller_works() {
|
||||
with_externalities(&mut ExtBuilder::default().build(),
|
||||
@@ -135,183 +120,6 @@ fn change_controller_works() {
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invulnerability_should_work() {
|
||||
// Test that users can be invulnerable from slashing and being kicked
|
||||
with_externalities(&mut ExtBuilder::default().build(),
|
||||
|| {
|
||||
// Make account 11 invulnerable
|
||||
assert_ok!(Staking::set_invulnerables(Origin::ROOT, vec![11]));
|
||||
// Give account 11 some funds
|
||||
let _ = Balances::make_free_balance_be(&11, 70);
|
||||
// There is no slash grace -- slash immediately.
|
||||
assert_eq!(Staking::offline_slash_grace(), 0);
|
||||
// Account 11 has not been slashed
|
||||
assert_eq!(Staking::slash_count(&11), 0);
|
||||
// Account 11 has the 70 funds we gave it above
|
||||
assert_eq!(Balances::free_balance(&11), 70);
|
||||
// Account 11 should be a validator
|
||||
assert!(<Validators<Test>>::exists(&11));
|
||||
|
||||
// Set account 11 as an offline validator with a large number of reports
|
||||
// Should exit early if invulnerable
|
||||
Staking::on_offline_validator(10, 100);
|
||||
|
||||
// Show that account 11 has not been touched
|
||||
assert_eq!(Staking::slash_count(&11), 0);
|
||||
assert_eq!(Balances::free_balance(&11), 70);
|
||||
assert!(<Validators<Test>>::exists(&11));
|
||||
// New era not being forced
|
||||
// NOTE: new era is always forced once slashing happens -> new validators need to be chosen.
|
||||
assert_eq!(Staking::force_era(), Forcing::NotForcing);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offline_should_slash_and_disable() {
|
||||
// Test that an offline validator gets slashed and kicked
|
||||
with_externalities(&mut ExtBuilder::default().build(), || {
|
||||
// Give account 10 some balance
|
||||
let _ = Balances::make_free_balance_be(&11, 1000);
|
||||
// Confirm account 10 is a validator
|
||||
assert!(<Validators<Test>>::exists(&11));
|
||||
// Validators get slashed immediately
|
||||
assert_eq!(Staking::offline_slash_grace(), 0);
|
||||
// Unstake threshold is 3
|
||||
assert_eq!(Staking::validators(&11).unstake_threshold, 3);
|
||||
// Account 10 has not been slashed before
|
||||
assert_eq!(Staking::slash_count(&11), 0);
|
||||
// Account 10 has the funds we just gave it
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
// Account 10 is not yet disabled.
|
||||
assert!(!is_disabled(10));
|
||||
// Report account 10 as offline, one greater than unstake threshold
|
||||
Staking::on_offline_validator(10, 4);
|
||||
// Confirm user has been reported
|
||||
assert_eq!(Staking::slash_count(&11), 4);
|
||||
// Confirm balance has been reduced by 2^unstake_threshold * offline_slash() * amount_at_stake.
|
||||
let slash_base = Staking::offline_slash() * Staking::stakers(11).total;
|
||||
assert_eq!(Balances::free_balance(&11), 1000 - 2_u64.pow(3) * slash_base);
|
||||
// Confirm account 10 has been disabled.
|
||||
assert!(is_disabled(10));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offline_grace_should_delay_slashing() {
|
||||
// Tests that with grace, slashing is delayed
|
||||
with_externalities(&mut ExtBuilder::default().build(), || {
|
||||
// Initialize account 10 with balance
|
||||
let _ = Balances::make_free_balance_be(&11, 70);
|
||||
// Verify account 11 has balance
|
||||
assert_eq!(Balances::free_balance(&11), 70);
|
||||
|
||||
// Set offline slash grace
|
||||
let offline_slash_grace = 1;
|
||||
assert_ok!(Staking::set_offline_slash_grace(Origin::ROOT, offline_slash_grace));
|
||||
assert_eq!(Staking::offline_slash_grace(), 1);
|
||||
|
||||
// Check unstake_threshold is 3 (default)
|
||||
let default_unstake_threshold = 3;
|
||||
assert_eq!(
|
||||
Staking::validators(&11),
|
||||
ValidatorPrefs { unstake_threshold: default_unstake_threshold, validator_payment: 0 }
|
||||
);
|
||||
|
||||
// Check slash count is zero
|
||||
assert_eq!(Staking::slash_count(&11), 0);
|
||||
|
||||
// Report account 10 up to the threshold
|
||||
Staking::on_offline_validator(10, default_unstake_threshold as usize + offline_slash_grace as usize);
|
||||
// Confirm slash count
|
||||
assert_eq!(Staking::slash_count(&11), 4);
|
||||
|
||||
// Nothing should happen
|
||||
assert_eq!(Balances::free_balance(&11), 70);
|
||||
|
||||
// Report account 10 one more time
|
||||
Staking::on_offline_validator(10, 1);
|
||||
assert_eq!(Staking::slash_count(&11), 5);
|
||||
// User gets slashed
|
||||
assert!(Balances::free_balance(&11) < 70);
|
||||
// New era is forced
|
||||
assert!(is_disabled(10));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn max_unstake_threshold_works() {
|
||||
// Tests that max_unstake_threshold gets used when prefs.unstake_threshold is large
|
||||
with_externalities(&mut ExtBuilder::default().build(), || {
|
||||
const MAX_UNSTAKE_THRESHOLD: u32 = 10;
|
||||
// Two users with maximum possible balance
|
||||
let _ = Balances::make_free_balance_be(&11, u64::max_value());
|
||||
let _ = Balances::make_free_balance_be(&21, u64::max_value());
|
||||
|
||||
// Give them full exposure as a staker
|
||||
<Stakers<Test>>::insert(&11, Exposure { total: 1000000, own: 1000000, others: vec![]});
|
||||
<Stakers<Test>>::insert(&21, Exposure { total: 2000000, own: 2000000, others: vec![]});
|
||||
|
||||
// Check things are initialized correctly
|
||||
assert_eq!(Balances::free_balance(&11), u64::max_value());
|
||||
assert_eq!(Balances::free_balance(&21), u64::max_value());
|
||||
assert_eq!(Staking::offline_slash_grace(), 0);
|
||||
// Account 10 will have max unstake_threshold
|
||||
assert_ok!(Staking::validate(Origin::signed(10), ValidatorPrefs {
|
||||
unstake_threshold: MAX_UNSTAKE_THRESHOLD,
|
||||
validator_payment: 0,
|
||||
}));
|
||||
// Account 20 could not set their unstake_threshold past 10
|
||||
assert_noop!(Staking::validate(Origin::signed(20), ValidatorPrefs {
|
||||
unstake_threshold: MAX_UNSTAKE_THRESHOLD + 1,
|
||||
validator_payment: 0}),
|
||||
"unstake threshold too large"
|
||||
);
|
||||
// Give Account 20 unstake_threshold 11 anyway, should still be limited to 10
|
||||
<Validators<Test>>::insert(21, ValidatorPrefs {
|
||||
unstake_threshold: MAX_UNSTAKE_THRESHOLD + 1,
|
||||
validator_payment: 0,
|
||||
});
|
||||
|
||||
OfflineSlash::put(Perbill::from_fraction(0.0001));
|
||||
|
||||
// Report each user 1 more than the max_unstake_threshold
|
||||
Staking::on_offline_validator(10, MAX_UNSTAKE_THRESHOLD as usize + 1);
|
||||
Staking::on_offline_validator(20, MAX_UNSTAKE_THRESHOLD as usize + 1);
|
||||
|
||||
// Show that each balance only gets reduced by 2^max_unstake_threshold times 10%
|
||||
// of their total stake.
|
||||
assert_eq!(Balances::free_balance(&11), u64::max_value() - 2_u64.pow(MAX_UNSTAKE_THRESHOLD) * 100);
|
||||
assert_eq!(Balances::free_balance(&21), u64::max_value() - 2_u64.pow(MAX_UNSTAKE_THRESHOLD) * 200);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slashing_does_not_cause_underflow() {
|
||||
// Tests that slashing more than a user has does not underflow
|
||||
with_externalities(&mut ExtBuilder::default().build(), || {
|
||||
// Verify initial conditions
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Staking::offline_slash_grace(), 0);
|
||||
|
||||
// Set validator preference so that 2^unstake_threshold would cause overflow (greater than 64)
|
||||
// FIXME: that doesn't overflow.
|
||||
<Validators<Test>>::insert(11, ValidatorPrefs {
|
||||
unstake_threshold: 10,
|
||||
validator_payment: 0,
|
||||
});
|
||||
|
||||
System::set_block_number(1);
|
||||
Session::on_initialize(System::block_number());
|
||||
|
||||
// Should not panic
|
||||
Staking::on_offline_validator(10, 100);
|
||||
// Confirm that underflow has not occurred, and account balance is set to zero
|
||||
assert_eq!(Balances::free_balance(&11), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewards_should_work() {
|
||||
// should check that:
|
||||
@@ -748,13 +556,12 @@ fn nominating_and_rewards_should_work() {
|
||||
#[test]
|
||||
fn nominators_also_get_slashed() {
|
||||
// A nominator should be slashed if the validator they nominated is slashed
|
||||
// Here is the breakdown of roles:
|
||||
// 10 - is the controller of 11
|
||||
// 11 - is the stash.
|
||||
// 2 - is the nominator of 20, 10
|
||||
with_externalities(&mut ExtBuilder::default().nominate(false).build(), || {
|
||||
assert_eq!(Staking::validator_count(), 2);
|
||||
// slash happens immediately.
|
||||
assert_eq!(Staking::offline_slash_grace(), 0);
|
||||
// Account 10 has not been reported offline
|
||||
assert_eq!(Staking::slash_count(&10), 0);
|
||||
OfflineSlash::put(Perbill::from_percent(12));
|
||||
|
||||
// Set payee to controller
|
||||
assert_ok!(Staking::set_payee(Origin::signed(10), RewardDestination::Controller));
|
||||
@@ -765,7 +572,7 @@ fn nominators_also_get_slashed() {
|
||||
let _ = Balances::make_free_balance_be(i, initial_balance);
|
||||
}
|
||||
|
||||
// 2 will nominate for 10
|
||||
// 2 will nominate for 10, 20
|
||||
let nominator_stake = 500;
|
||||
assert_ok!(Staking::bond(Origin::signed(1), 2, nominator_stake, RewardDestination::default()));
|
||||
assert_ok!(Staking::nominate(Origin::signed(2), vec![20, 10]));
|
||||
@@ -781,15 +588,24 @@ fn nominators_also_get_slashed() {
|
||||
assert_eq!(Balances::total_balance(&2), initial_balance);
|
||||
|
||||
// 10 goes offline
|
||||
Staking::on_offline_validator(10, 4);
|
||||
let expo = Staking::stakers(10);
|
||||
let slash_value = Staking::offline_slash() * expo.total * 2_u64.pow(3);
|
||||
Staking::on_offence(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
Staking::stakers(&11),
|
||||
),
|
||||
reporters: vec![],
|
||||
}],
|
||||
&[Perbill::from_percent(5)],
|
||||
);
|
||||
let expo = Staking::stakers(11);
|
||||
let slash_value = 50;
|
||||
let total_slash = expo.total.min(slash_value);
|
||||
let validator_slash = expo.own.min(total_slash);
|
||||
let nominator_slash = nominator_stake.min(total_slash - validator_slash);
|
||||
|
||||
// initial + first era reward + slash
|
||||
assert_eq!(Balances::total_balance(&10), initial_balance + total_payout - validator_slash);
|
||||
assert_eq!(Balances::total_balance(&11), initial_balance - validator_slash);
|
||||
assert_eq!(Balances::total_balance(&2), initial_balance - nominator_slash);
|
||||
check_exposure_all();
|
||||
check_nominator_all();
|
||||
@@ -907,10 +723,11 @@ fn forcing_new_era_works() {
|
||||
start_session(6);
|
||||
assert_eq!(Staking::current_era(), 1);
|
||||
|
||||
// back to normal
|
||||
// back to normal.
|
||||
// this immediatelly starts a new session.
|
||||
ForceEra::put(Forcing::NotForcing);
|
||||
start_session(7);
|
||||
assert_eq!(Staking::current_era(), 1);
|
||||
assert_eq!(Staking::current_era(), 2);
|
||||
start_session(8);
|
||||
assert_eq!(Staking::current_era(), 2);
|
||||
|
||||
@@ -1100,7 +917,6 @@ fn validator_payment_prefs_work() {
|
||||
});
|
||||
<Payee<Test>>::insert(&2, RewardDestination::Stash);
|
||||
<Validators<Test>>::insert(&11, ValidatorPrefs {
|
||||
unstake_threshold: 3,
|
||||
validator_payment: validator_cut
|
||||
});
|
||||
|
||||
@@ -1337,13 +1153,6 @@ fn slot_stake_is_least_staked_validator_and_exposure_defines_maximum_punishment(
|
||||
// -- slot stake should also be updated.
|
||||
assert_eq!(Staking::slot_stake(), 69 + total_payout_0/2);
|
||||
|
||||
// If 10 gets slashed now, it will be slashed by 5% of exposure.total * 2.pow(unstake_thresh)
|
||||
Staking::on_offline_validator(10, 4);
|
||||
// Confirm user has been reported
|
||||
assert_eq!(Staking::slash_count(&11), 4);
|
||||
// check the balance of 10 (slash will be deducted from free balance.)
|
||||
assert_eq!(Balances::free_balance(&11), _11_balance - _11_balance*5/100 * 2u64.pow(3));
|
||||
|
||||
check_exposure_all();
|
||||
check_nominator_all();
|
||||
});
|
||||
@@ -1365,8 +1174,6 @@ fn on_free_balance_zero_stash_removes_validator() {
|
||||
assert_eq!(Staking::bonded(&11), Some(10));
|
||||
|
||||
// Set some storage items which we expect to be cleaned up
|
||||
// Initiate slash count storage item
|
||||
Staking::on_offline_validator(10, 1);
|
||||
// Set payee information
|
||||
assert_ok!(Staking::set_payee(Origin::signed(10), RewardDestination::Stash));
|
||||
|
||||
@@ -1374,7 +1181,6 @@ fn on_free_balance_zero_stash_removes_validator() {
|
||||
assert!(<Ledger<Test>>::exists(&10));
|
||||
assert!(<Bonded<Test>>::exists(&11));
|
||||
assert!(<Validators<Test>>::exists(&11));
|
||||
assert!(<SlashCount<Test>>::exists(&11));
|
||||
assert!(<Payee<Test>>::exists(&11));
|
||||
|
||||
// Reduce free_balance of controller to 0
|
||||
@@ -1389,7 +1195,6 @@ fn on_free_balance_zero_stash_removes_validator() {
|
||||
assert!(<Ledger<Test>>::exists(&10));
|
||||
assert!(<Bonded<Test>>::exists(&11));
|
||||
assert!(<Validators<Test>>::exists(&11));
|
||||
assert!(<SlashCount<Test>>::exists(&11));
|
||||
assert!(<Payee<Test>>::exists(&11));
|
||||
|
||||
// Reduce free_balance of stash to 0
|
||||
@@ -1402,7 +1207,6 @@ fn on_free_balance_zero_stash_removes_validator() {
|
||||
assert!(!<Bonded<Test>>::exists(&11));
|
||||
assert!(!<Validators<Test>>::exists(&11));
|
||||
assert!(!<Nominators<Test>>::exists(&11));
|
||||
assert!(!<SlashCount<Test>>::exists(&11));
|
||||
assert!(!<Payee<Test>>::exists(&11));
|
||||
});
|
||||
}
|
||||
@@ -1459,7 +1263,6 @@ fn on_free_balance_zero_stash_removes_nominator() {
|
||||
assert!(!<Bonded<Test>>::exists(&11));
|
||||
assert!(!<Validators<Test>>::exists(&11));
|
||||
assert!(!<Nominators<Test>>::exists(&11));
|
||||
assert!(!<SlashCount<Test>>::exists(&11));
|
||||
assert!(!<Payee<Test>>::exists(&11));
|
||||
});
|
||||
}
|
||||
@@ -2107,7 +1910,7 @@ fn reward_validator_slashing_validator_doesnt_overflow() {
|
||||
]});
|
||||
|
||||
// Check slashing
|
||||
Staking::slash_validator(&11, reward_slash);
|
||||
let _ = Staking::slash_validator(&11, reward_slash, &Staking::stakers(&11), &mut Vec::new());
|
||||
assert_eq!(Balances::total_balance(&11), stake - 1);
|
||||
assert_eq!(Balances::total_balance(&2), 1);
|
||||
})
|
||||
@@ -2180,3 +1983,150 @@ fn unbonded_balance_is_not_slashable() {
|
||||
assert_eq!(Staking::slashable_balance_of(&11), 200);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn era_is_always_same_length() {
|
||||
// This ensures that the sessions is always of the same length if there is no forcing no
|
||||
// session changes.
|
||||
with_externalities(&mut ExtBuilder::default().build(), || {
|
||||
start_era(1);
|
||||
assert_eq!(Staking::current_era_start_session_index(), SessionsPerEra::get());
|
||||
|
||||
start_era(2);
|
||||
assert_eq!(Staking::current_era_start_session_index(), SessionsPerEra::get() * 2);
|
||||
|
||||
let session = Session::current_index();
|
||||
ForceEra::put(Forcing::ForceNew);
|
||||
advance_session();
|
||||
assert_eq!(Staking::current_era(), 3);
|
||||
assert_eq!(Staking::current_era_start_session_index(), session + 1);
|
||||
|
||||
start_era(4);
|
||||
assert_eq!(Staking::current_era_start_session_index(), session + SessionsPerEra::get() + 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offence_forces_new_era() {
|
||||
with_externalities(&mut ExtBuilder::default().build(), || {
|
||||
Staking::on_offence(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
Staking::stakers(&11),
|
||||
),
|
||||
reporters: vec![],
|
||||
}],
|
||||
&[Perbill::from_percent(5)],
|
||||
);
|
||||
|
||||
assert_eq!(Staking::force_era(), Forcing::ForceNew);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slashing_performed_according_exposure() {
|
||||
// This test checks that slashing is performed according the exposure (or more precisely,
|
||||
// historical exposure), not the current balance.
|
||||
with_externalities(&mut ExtBuilder::default().build(), || {
|
||||
assert_eq!(Staking::stakers(&11).own, 1000);
|
||||
|
||||
// Handle an offence with a historical exposure.
|
||||
Staking::on_offence(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
Exposure {
|
||||
total: 500,
|
||||
own: 500,
|
||||
others: vec![],
|
||||
},
|
||||
),
|
||||
reporters: vec![],
|
||||
}],
|
||||
&[Perbill::from_percent(50)],
|
||||
);
|
||||
|
||||
// The stash account should be slashed for 250 (50% of 500).
|
||||
assert_eq!(Balances::free_balance(&11), 1000 - 250);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reporters_receive_their_slice() {
|
||||
// This test verifies that the reporters of the offence receive their slice from the slashed
|
||||
// amount.
|
||||
with_externalities(&mut ExtBuilder::default().build(), || {
|
||||
// The reporters' reward is calculated from the total exposure.
|
||||
assert_eq!(Staking::stakers(&11).total, 1250);
|
||||
|
||||
Staking::on_offence(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
Staking::stakers(&11),
|
||||
),
|
||||
reporters: vec![1, 2],
|
||||
}],
|
||||
&[Perbill::from_percent(50)],
|
||||
);
|
||||
|
||||
// 1250 x 50% (slash fraction) x 10% (rewards slice)
|
||||
assert_eq!(Balances::free_balance(&1), 10 + 31);
|
||||
assert_eq!(Balances::free_balance(&2), 20 + 31);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invulnerables_are_not_slashed() {
|
||||
// For invulnerable validators no slashing is performed.
|
||||
with_externalities(
|
||||
&mut ExtBuilder::default().invulnerables(vec![11]).build(),
|
||||
|| {
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Balances::free_balance(&21), 2000);
|
||||
assert_eq!(Staking::stakers(&21).total, 1250);
|
||||
|
||||
Staking::on_offence(
|
||||
&[
|
||||
OffenceDetails {
|
||||
offender: (11, Staking::stakers(&11)),
|
||||
reporters: vec![],
|
||||
},
|
||||
OffenceDetails {
|
||||
offender: (21, Staking::stakers(&21)),
|
||||
reporters: vec![],
|
||||
},
|
||||
],
|
||||
&[Perbill::from_percent(50), Perbill::from_percent(20)],
|
||||
);
|
||||
|
||||
// The validator 11 hasn't been slashed, but 21 has been.
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Balances::free_balance(&21), 1750); // 2000 - (0.2 * 1250)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dont_slash_if_fraction_is_zero() {
|
||||
// Don't slash if the fraction is zero.
|
||||
with_externalities(&mut ExtBuilder::default().build(), || {
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
|
||||
Staking::on_offence(
|
||||
&[OffenceDetails {
|
||||
offender: (
|
||||
11,
|
||||
Staking::stakers(&11),
|
||||
),
|
||||
reporters: vec![],
|
||||
}],
|
||||
&[Perbill::from_percent(0)],
|
||||
);
|
||||
|
||||
// The validator hasn't been slashed. The new era is not forced.
|
||||
assert_eq!(Balances::free_balance(&11), 1000);
|
||||
assert_eq!(Staking::force_era(), Forcing::NotForcing);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user