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:
Tomasz Drwięga
2019-08-16 19:54:50 +02:00
committed by Gavin Wood
parent 99f3f07690
commit 6cc4495700
37 changed files with 1775 additions and 597 deletions
+190 -137
View File
@@ -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 -8
View File
@@ -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
View File
@@ -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);
});
}