...@@ -43,7 +43,7 @@ use sp_runtime::{ ...@@ -43,7 +43,7 @@ use sp_runtime::{
}; };
use sp_staking::{ use sp_staking::{
currency_to_vote::CurrencyToVote, currency_to_vote::CurrencyToVote,
offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, offence::{OffenceDetails, OnOffenceHandler},
EraIndex, OnStakingUpdate, Page, SessionIndex, Stake, EraIndex, OnStakingUpdate, Page, SessionIndex, Stake,
StakingAccount::{self, Controller, Stash}, StakingAccount::{self, Controller, Stash},
StakingInterface, StakingInterface,
...@@ -505,12 +505,10 @@ impl<T: Config> Pallet<T> { ...@@ -505,12 +505,10 @@ impl<T: Config> Pallet<T> {
} }
// disable all offending validators that have been disabled for the whole era // disable all offending validators that have been disabled for the whole era
for (index, disabled) in <OffendingValidators<T>>::get() { for index in <DisabledValidators<T>>::get() {
if disabled {
T::SessionInterface::disable_validator(index); T::SessionInterface::disable_validator(index);
} }
} }
}
/// End a session potentially ending an era. /// End a session potentially ending an era.
fn end_session(session_index: SessionIndex) { fn end_session(session_index: SessionIndex) {
...@@ -598,8 +596,8 @@ impl<T: Config> Pallet<T> { ...@@ -598,8 +596,8 @@ impl<T: Config> Pallet<T> {
<ErasValidatorReward<T>>::insert(&active_era.index, validator_payout); <ErasValidatorReward<T>>::insert(&active_era.index, validator_payout);
T::RewardRemainder::on_unbalanced(T::Currency::issue(remainder)); T::RewardRemainder::on_unbalanced(T::Currency::issue(remainder));
// Clear offending validators. // Clear disabled validators.
<OffendingValidators<T>>::kill(); <DisabledValidators<T>>::kill();
} }
} }
...@@ -868,14 +866,6 @@ impl<T: Config> Pallet<T> { ...@@ -868,14 +866,6 @@ impl<T: Config> Pallet<T> {
Self::deposit_event(Event::<T>::ForceEra { mode }); Self::deposit_event(Event::<T>::ForceEra { mode });
} }
/// Ensures that at the end of the current session there will be a new era.
pub(crate) fn ensure_new_era() {
match ForceEra::<T>::get() {
Forcing::ForceAlways | Forcing::ForceNew => (),
_ => Self::set_force_era(Forcing::ForceNew),
}
}
#[cfg(feature = "runtime-benchmarks")] #[cfg(feature = "runtime-benchmarks")]
pub fn add_era_stakers( pub fn add_era_stakers(
current_era: EraIndex, current_era: EraIndex,
...@@ -1447,7 +1437,6 @@ where ...@@ -1447,7 +1437,6 @@ where
>], >],
slash_fraction: &[Perbill], slash_fraction: &[Perbill],
slash_session: SessionIndex, slash_session: SessionIndex,
disable_strategy: DisableStrategy,
) -> Weight { ) -> Weight {
let reward_proportion = SlashRewardFraction::<T>::get(); let reward_proportion = SlashRewardFraction::<T>::get();
let mut consumed_weight = Weight::from_parts(0, 0); let mut consumed_weight = Weight::from_parts(0, 0);
...@@ -1512,7 +1501,6 @@ where ...@@ -1512,7 +1501,6 @@ where
window_start, window_start,
now: active_era, now: active_era,
reward_proportion, reward_proportion,
disable_strategy,
}); });
Self::deposit_event(Event::<T>::SlashReported { Self::deposit_event(Event::<T>::SlashReported {
...@@ -1986,7 +1974,8 @@ impl<T: Config> Pallet<T> { ...@@ -1986,7 +1974,8 @@ impl<T: Config> Pallet<T> {
Self::check_nominators()?; Self::check_nominators()?;
Self::check_exposures()?; Self::check_exposures()?;
Self::check_paged_exposures()?; Self::check_paged_exposures()?;
Self::check_count() Self::check_count()?;
Self::ensure_disabled_validators_sorted()
} }
/// Invariants: /// Invariants:
...@@ -2300,4 +2289,12 @@ impl<T: Config> Pallet<T> { ...@@ -2300,4 +2289,12 @@ impl<T: Config> Pallet<T> {
Ok(()) Ok(())
} }
fn ensure_disabled_validators_sorted() -> Result<(), TryRuntimeError> {
ensure!(
DisabledValidators::<T>::get().windows(2).all(|pair| pair[0] <= pair[1]),
"DisabledValidators is not sorted"
);
Ok(())
}
} }
...@@ -47,10 +47,11 @@ mod impls; ...@@ -47,10 +47,11 @@ mod impls;
pub use impls::*; pub use impls::*;
use crate::{ use crate::{
slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout, slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, DisablingStrategy,
EraRewardPoints, Exposure, ExposurePage, Forcing, LedgerIntegrityState, MaxNominationsOf, EraPayout, EraRewardPoints, Exposure, ExposurePage, Forcing, LedgerIntegrityState,
NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, MaxNominationsOf, NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf,
SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk, ValidatorPrefs, RewardDestination, SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk,
ValidatorPrefs,
}; };
// The speculative number of spans are used as an input of the weight annotation of // The speculative number of spans are used as an input of the weight annotation of
...@@ -67,7 +68,7 @@ pub mod pallet { ...@@ -67,7 +68,7 @@ pub mod pallet {
use super::*; use super::*;
/// The in-code storage version. /// The in-code storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(14); const STORAGE_VERSION: StorageVersion = StorageVersion::new(15);
#[pallet::pallet] #[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)] #[pallet::storage_version(STORAGE_VERSION)]
...@@ -217,10 +218,6 @@ pub mod pallet { ...@@ -217,10 +218,6 @@ pub mod pallet {
#[pallet::constant] #[pallet::constant]
type MaxExposurePageSize: Get<u32>; type MaxExposurePageSize: Get<u32>;
/// The fraction of the validator set that is safe to be offending.
/// After the threshold is reached a new era will be forced.
type OffendingValidatorsThreshold: Get<Perbill>;
/// Something that provides a best-effort sorted list of voters aka electing nominators, /// Something that provides a best-effort sorted list of voters aka electing nominators,
/// used for NPoS election. /// used for NPoS election.
/// ///
...@@ -278,6 +275,9 @@ pub mod pallet { ...@@ -278,6 +275,9 @@ pub mod pallet {
/// WARNING: this only reports slashing and withdraw events for the time being. /// WARNING: this only reports slashing and withdraw events for the time being.
type EventListeners: sp_staking::OnStakingUpdate<Self::AccountId, BalanceOf<Self>>; type EventListeners: sp_staking::OnStakingUpdate<Self::AccountId, BalanceOf<Self>>;
// `DisablingStragegy` controls how validators are disabled
type DisablingStrategy: DisablingStrategy<Self>;
/// Some parameters of the benchmarking. /// Some parameters of the benchmarking.
type BenchmarkingConfig: BenchmarkingConfig; type BenchmarkingConfig: BenchmarkingConfig;
...@@ -654,19 +654,16 @@ pub mod pallet { ...@@ -654,19 +654,16 @@ pub mod pallet {
#[pallet::getter(fn current_planned_session)] #[pallet::getter(fn current_planned_session)]
pub type CurrentPlannedSession<T> = StorageValue<_, SessionIndex, ValueQuery>; pub type CurrentPlannedSession<T> = StorageValue<_, SessionIndex, ValueQuery>;
/// Indices of validators that have offended in the active era and whether they are currently /// Indices of validators that have offended in the active era. The offenders are disabled for a
/// disabled. /// whole era. For this reason they are kept here - only staking pallet knows about eras. The
/// implementor of [`DisablingStrategy`] defines if a validator should be disabled which
/// implicitly means that the implementor also controls the max number of disabled validators.
/// ///
/// This value should be a superset of disabled validators since not all offences lead to the /// The vec is always kept sorted so that we can find whether a given validator has previously
/// validator being disabled (if there was no slash). This is needed to track the percentage of /// offended using binary search.
/// validators that have offended in the current era, ensuring a new era is forced if
/// `OffendingValidatorsThreshold` is reached. The vec is always kept sorted so that we can find
/// whether a given validator has previously offended using binary search. It gets cleared when
/// the era ends.
#[pallet::storage] #[pallet::storage]
#[pallet::unbounded] #[pallet::unbounded]
#[pallet::getter(fn offending_validators)] pub type DisabledValidators<T: Config> = StorageValue<_, Vec<u32>, ValueQuery>;
pub type OffendingValidators<T: Config> = StorageValue<_, Vec<(u32, bool)>, ValueQuery>;
/// The threshold for when users can start calling `chill_other` for other validators / /// The threshold for when users can start calling `chill_other` for other validators /
/// nominators. The threshold is compared to the actual number of validators / nominators /// nominators. The threshold is compared to the actual number of validators / nominators
......
...@@ -50,21 +50,21 @@ ...@@ -50,21 +50,21 @@
//! Based on research at <https://research.web3.foundation/en/latest/polkadot/slashing/npos.html> //! Based on research at <https://research.web3.foundation/en/latest/polkadot/slashing/npos.html>
use crate::{ use crate::{
BalanceOf, Config, Error, Exposure, NegativeImbalanceOf, NominatorSlashInEra, BalanceOf, Config, DisabledValidators, DisablingStrategy, Error, Exposure, NegativeImbalanceOf,
OffendingValidators, Pallet, Perbill, SessionInterface, SpanSlash, UnappliedSlash, NominatorSlashInEra, Pallet, Perbill, SessionInterface, SpanSlash, UnappliedSlash,
ValidatorSlashInEra, ValidatorSlashInEra,
}; };
use codec::{Decode, Encode, MaxEncodedLen}; use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::{ use frame_support::{
ensure, ensure,
traits::{Currency, Defensive, DefensiveSaturating, Get, Imbalance, OnUnbalanced}, traits::{Currency, Defensive, DefensiveSaturating, Imbalance, OnUnbalanced},
}; };
use scale_info::TypeInfo; use scale_info::TypeInfo;
use sp_runtime::{ use sp_runtime::{
traits::{Saturating, Zero}, traits::{Saturating, Zero},
DispatchResult, RuntimeDebug, DispatchResult, RuntimeDebug,
}; };
use sp_staking::{offence::DisableStrategy, EraIndex}; use sp_staking::EraIndex;
use sp_std::vec::Vec; use sp_std::vec::Vec;
/// The proportion of the slashing reward to be paid out on the first slashing detection. /// The proportion of the slashing reward to be paid out on the first slashing detection.
...@@ -220,8 +220,6 @@ pub(crate) struct SlashParams<'a, T: 'a + Config> { ...@@ -220,8 +220,6 @@ pub(crate) struct SlashParams<'a, T: 'a + Config> {
/// The maximum percentage of a slash that ever gets paid out. /// The maximum percentage of a slash that ever gets paid out.
/// This is f_inf in the paper. /// This is f_inf in the paper.
pub(crate) reward_proportion: Perbill, pub(crate) reward_proportion: Perbill,
/// When to disable offenders.
pub(crate) disable_strategy: DisableStrategy,
} }
/// Computes a slash of a validator and nominators. It returns an unapplied /// Computes a slash of a validator and nominators. It returns an unapplied
...@@ -280,18 +278,13 @@ pub(crate) fn compute_slash<T: Config>( ...@@ -280,18 +278,13 @@ pub(crate) fn compute_slash<T: Config>(
let target_span = spans.compare_and_update_span_slash(params.slash_era, own_slash); let target_span = spans.compare_and_update_span_slash(params.slash_era, own_slash);
if target_span == Some(spans.span_index()) { if target_span == Some(spans.span_index()) {
// misbehavior occurred within the current slashing span - take appropriate // misbehavior occurred within the current slashing span - end current span.
// actions. // Check <https://github.com/paritytech/polkadot-sdk/issues/2650> for details.
// chill the validator - it misbehaved in the current span and should
// not continue in the next election. also end the slashing span.
spans.end_span(params.now); spans.end_span(params.now);
<Pallet<T>>::chill_stash(params.stash);
} }
} }
let disable_when_slashed = params.disable_strategy != DisableStrategy::Never; add_offending_validator::<T>(&params);
add_offending_validator::<T>(params.stash, disable_when_slashed);
let mut nominators_slashed = Vec::new(); let mut nominators_slashed = Vec::new();
reward_payout += slash_nominators::<T>(params.clone(), prior_slash_p, &mut nominators_slashed); reward_payout += slash_nominators::<T>(params.clone(), prior_slash_p, &mut nominators_slashed);
...@@ -320,54 +313,31 @@ fn kick_out_if_recent<T: Config>(params: SlashParams<T>) { ...@@ -320,54 +313,31 @@ fn kick_out_if_recent<T: Config>(params: SlashParams<T>) {
); );
if spans.era_span(params.slash_era).map(|s| s.index) == Some(spans.span_index()) { if spans.era_span(params.slash_era).map(|s| s.index) == Some(spans.span_index()) {
// Check https://github.com/paritytech/polkadot-sdk/issues/2650 for details
spans.end_span(params.now); spans.end_span(params.now);
<Pallet<T>>::chill_stash(params.stash);
} }
let disable_without_slash = params.disable_strategy == DisableStrategy::Always; add_offending_validator::<T>(&params);
add_offending_validator::<T>(params.stash, disable_without_slash);
} }
/// Add the given validator to the offenders list and optionally disable it. /// Inform the [`DisablingStrategy`] implementation about the new offender and disable the list of
/// If after adding the validator `OffendingValidatorsThreshold` is reached /// validators provided by [`make_disabling_decision`].
/// a new era will be forced. fn add_offending_validator<T: Config>(params: &SlashParams<T>) {
fn add_offending_validator<T: Config>(stash: &T::AccountId, disable: bool) { DisabledValidators::<T>::mutate(|disabled| {
OffendingValidators::<T>::mutate(|offending| { if let Some(offender) =
let validators = T::SessionInterface::validators(); T::DisablingStrategy::decision(params.stash, params.slash_era, &disabled)
let validator_index = match validators.iter().position(|i| i == stash) { {
Some(index) => index, // Add the validator to `DisabledValidators` and disable it. Do nothing if it is
None => return, // already disabled.
}; if let Err(index) = disabled.binary_search_by_key(&offender, |index| *index) {
disabled.insert(index, offender);
let validator_index_u32 = validator_index as u32; T::SessionInterface::disable_validator(offender);
match offending.binary_search_by_key(&validator_index_u32, |(index, _)| *index) {
// this is a new offending validator
Err(index) => {
offending.insert(index, (validator_index_u32, disable));
let offending_threshold =
T::OffendingValidatorsThreshold::get() * validators.len() as u32;
if offending.len() >= offending_threshold as usize {
// force a new era, to select a new validator set
<Pallet<T>>::ensure_new_era()
}
if disable {
T::SessionInterface::disable_validator(validator_index_u32);
}
},
Ok(index) => {
if disable && !offending[index].1 {
// the validator had previously offended without being disabled,
// let's make sure we disable it now
offending[index].1 = true;
T::SessionInterface::disable_validator(validator_index_u32);
} }
},
} }
}); });
// `DisabledValidators` should be kept sorted
debug_assert!(DisabledValidators::<T>::get().windows(2).all(|pair| pair[0] < pair[1]));
} }
/// Slash nominators. Accepts general parameters and the prior slash percentage of the validator. /// Slash nominators. Accepts general parameters and the prior slash percentage of the validator.
......
...@@ -38,7 +38,7 @@ use sp_runtime::{ ...@@ -38,7 +38,7 @@ use sp_runtime::{
Perbill, Percent, Perquintill, Rounding, TokenError, Perbill, Percent, Perquintill, Rounding, TokenError,
}; };
use sp_staking::{ use sp_staking::{
offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, offence::{OffenceDetails, OnOffenceHandler},
SessionIndex, SessionIndex,
}; };
use sp_std::prelude::*; use sp_std::prelude::*;
...@@ -716,7 +716,10 @@ fn nominating_and_rewards_should_work() { ...@@ -716,7 +716,10 @@ fn nominating_and_rewards_should_work() {
#[test] #[test]
fn nominators_also_get_slashed_pro_rata() { fn nominators_also_get_slashed_pro_rata() {
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default()
.validator_count(4)
.set_status(41, StakerStatus::Validator)
.build_and_execute(|| {
mock::start_active_era(1); mock::start_active_era(1);
let slash_percent = Perbill::from_percent(5); let slash_percent = Perbill::from_percent(5);
let initial_exposure = Staking::eras_stakers(active_era(), &11); let initial_exposure = Staking::eras_stakers(active_era(), &11);
...@@ -753,8 +756,14 @@ fn nominators_also_get_slashed_pro_rata() { ...@@ -753,8 +756,14 @@ fn nominators_also_get_slashed_pro_rata() {
assert!(nominator_share > 0); assert!(nominator_share > 0);
// both stakes must have been decreased pro-rata. // both stakes must have been decreased pro-rata.
assert_eq!(Staking::ledger(101.into()).unwrap().active, nominator_stake - nominator_share); assert_eq!(
assert_eq!(Staking::ledger(11.into()).unwrap().active, validator_stake - validator_share); Staking::ledger(101.into()).unwrap().active,
nominator_stake - nominator_share
);
assert_eq!(
Staking::ledger(11.into()).unwrap().active,
validator_stake - validator_share
);
assert_eq!( assert_eq!(
balances(&101).0, // free balance balances(&101).0, // free balance
nominator_balance - nominator_share, nominator_balance - nominator_share,
...@@ -2401,7 +2410,7 @@ fn era_is_always_same_length() { ...@@ -2401,7 +2410,7 @@ fn era_is_always_same_length() {
} }
#[test] #[test]
fn offence_forces_new_era() { fn offence_doesnt_force_new_era() {
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default().build_and_execute(|| {
on_offence_now( on_offence_now(
&[OffenceDetails { &[OffenceDetails {
...@@ -2411,7 +2420,7 @@ fn offence_forces_new_era() { ...@@ -2411,7 +2420,7 @@ fn offence_forces_new_era() {
&[Perbill::from_percent(5)], &[Perbill::from_percent(5)],
); );
assert_eq!(Staking::force_era(), Forcing::ForceNew); assert_eq!(Staking::force_era(), Forcing::NotForcing);
}); });
} }
...@@ -2435,7 +2444,13 @@ fn offence_ensures_new_era_without_clobbering() { ...@@ -2435,7 +2444,13 @@ fn offence_ensures_new_era_without_clobbering() {
#[test] #[test]
fn offence_deselects_validator_even_when_slash_is_zero() { fn offence_deselects_validator_even_when_slash_is_zero() {
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default()
.validator_count(7)
.set_status(41, StakerStatus::Validator)
.set_status(51, StakerStatus::Validator)
.set_status(201, StakerStatus::Validator)
.set_status(202, StakerStatus::Validator)
.build_and_execute(|| {
assert!(Session::validators().contains(&11)); assert!(Session::validators().contains(&11));
assert!(<Validators<Test>>::contains_key(11)); assert!(<Validators<Test>>::contains_key(11));
...@@ -2447,13 +2462,13 @@ fn offence_deselects_validator_even_when_slash_is_zero() { ...@@ -2447,13 +2462,13 @@ fn offence_deselects_validator_even_when_slash_is_zero() {
&[Perbill::from_percent(0)], &[Perbill::from_percent(0)],
); );
assert_eq!(Staking::force_era(), Forcing::ForceNew); assert_eq!(Staking::force_era(), Forcing::NotForcing);
assert!(!<Validators<Test>>::contains_key(11)); assert!(is_disabled(11));
mock::start_active_era(1); mock::start_active_era(1);
assert!(!Session::validators().contains(&11)); // The validator should be reenabled in the new era
assert!(!<Validators<Test>>::contains_key(11)); assert!(!is_disabled(11));
}); });
} }
...@@ -2479,8 +2494,11 @@ fn slashing_performed_according_exposure() { ...@@ -2479,8 +2494,11 @@ fn slashing_performed_according_exposure() {
} }
#[test] #[test]
fn slash_in_old_span_does_not_deselect() { fn validator_is_not_disabled_for_an_offence_in_previous_era() {
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default()
.validator_count(4)
.set_status(41, StakerStatus::Validator)
.build_and_execute(|| {
mock::start_active_era(1); mock::start_active_era(1);
assert!(<Validators<Test>>::contains_key(11)); assert!(<Validators<Test>>::contains_key(11));
...@@ -2494,21 +2512,20 @@ fn slash_in_old_span_does_not_deselect() { ...@@ -2494,21 +2512,20 @@ fn slash_in_old_span_does_not_deselect() {
&[Perbill::from_percent(0)], &[Perbill::from_percent(0)],
); );
assert_eq!(Staking::force_era(), Forcing::ForceNew); assert_eq!(Staking::force_era(), Forcing::NotForcing);
assert!(!<Validators<Test>>::contains_key(11)); assert!(is_disabled(11));
mock::start_active_era(2); mock::start_active_era(2);
// the validator is not disabled in the new era
Staking::validate(RuntimeOrigin::signed(11), Default::default()).unwrap(); Staking::validate(RuntimeOrigin::signed(11), Default::default()).unwrap();
assert_eq!(Staking::force_era(), Forcing::NotForcing); assert_eq!(Staking::force_era(), Forcing::NotForcing);
assert!(<Validators<Test>>::contains_key(11)); assert!(<Validators<Test>>::contains_key(11));
assert!(!Session::validators().contains(&11)); assert!(Session::validators().contains(&11));
mock::start_active_era(3); mock::start_active_era(3);
// this staker is in a new slashing span now, having re-registered after // an offence committed in era 1 is reported in era 3
// their prior slash.
on_offence_in_era( on_offence_in_era(
&[OffenceDetails { &[OffenceDetails {
offender: (11, Staking::eras_stakers(active_era(), &11)), offender: (11, Staking::eras_stakers(active_era(), &11)),
...@@ -2516,14 +2533,14 @@ fn slash_in_old_span_does_not_deselect() { ...@@ -2516,14 +2533,14 @@ fn slash_in_old_span_does_not_deselect() {
}], }],
&[Perbill::from_percent(0)], &[Perbill::from_percent(0)],
1, 1,
DisableStrategy::WhenSlashed,
); );
// the validator doesn't get chilled again // the validator doesn't get disabled for an old offence
assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11)); assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
assert!(!is_disabled(11));
// but we are still forcing a new era // and we are not forcing a new era
assert_eq!(Staking::force_era(), Forcing::ForceNew); assert_eq!(Staking::force_era(), Forcing::NotForcing);
on_offence_in_era( on_offence_in_era(
&[OffenceDetails { &[OffenceDetails {
...@@ -2533,16 +2550,13 @@ fn slash_in_old_span_does_not_deselect() { ...@@ -2533,16 +2550,13 @@ fn slash_in_old_span_does_not_deselect() {
// NOTE: A 100% slash here would clean up the account, causing de-registration. // NOTE: A 100% slash here would clean up the account, causing de-registration.
&[Perbill::from_percent(95)], &[Perbill::from_percent(95)],
1, 1,
DisableStrategy::WhenSlashed,
); );
// the validator doesn't get chilled again // the validator doesn't get disabled again
assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11)); assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
assert!(!is_disabled(11));
// but it's disabled // and we are still not forcing a new era
assert!(is_disabled(11)); assert_eq!(Staking::force_era(), Forcing::NotForcing);
// and we are still forcing a new era
assert_eq!(Staking::force_era(), Forcing::ForceNew);
}); });
} }
...@@ -2671,7 +2685,7 @@ fn dont_slash_if_fraction_is_zero() { ...@@ -2671,7 +2685,7 @@ fn dont_slash_if_fraction_is_zero() {
// The validator hasn't been slashed. The new era is not forced. // The validator hasn't been slashed. The new era is not forced.
assert_eq!(Balances::free_balance(11), 1000); assert_eq!(Balances::free_balance(11), 1000);
assert_eq!(Staking::force_era(), Forcing::ForceNew); assert_eq!(Staking::force_era(), Forcing::NotForcing);
}); });
} }
...@@ -2692,7 +2706,7 @@ fn only_slash_for_max_in_era() { ...@@ -2692,7 +2706,7 @@ fn only_slash_for_max_in_era() {
// The validator has been slashed and has been force-chilled. // The validator has been slashed and has been force-chilled.
assert_eq!(Balances::free_balance(11), 500); assert_eq!(Balances::free_balance(11), 500);
assert_eq!(Staking::force_era(), Forcing::ForceNew); assert_eq!(Staking::force_era(), Forcing::NotForcing);
on_offence_now( on_offence_now(
&[OffenceDetails { &[OffenceDetails {
...@@ -2833,7 +2847,6 @@ fn slashing_nominators_by_span_max() { ...@@ -2833,7 +2847,6 @@ fn slashing_nominators_by_span_max() {
}], }],
&[Perbill::from_percent(10)], &[Perbill::from_percent(10)],
2, 2,
DisableStrategy::WhenSlashed,
); );
assert_eq!(Balances::free_balance(11), 900); assert_eq!(Balances::free_balance(11), 900);
...@@ -2860,7 +2873,6 @@ fn slashing_nominators_by_span_max() { ...@@ -2860,7 +2873,6 @@ fn slashing_nominators_by_span_max() {
}], }],
&[Perbill::from_percent(30)], &[Perbill::from_percent(30)],
3, 3,
DisableStrategy::WhenSlashed,
); );
// 11 was not further slashed, but 21 and 101 were. // 11 was not further slashed, but 21 and 101 were.
...@@ -2882,7 +2894,6 @@ fn slashing_nominators_by_span_max() { ...@@ -2882,7 +2894,6 @@ fn slashing_nominators_by_span_max() {
}], }],
&[Perbill::from_percent(20)], &[Perbill::from_percent(20)],
2, 2,
DisableStrategy::WhenSlashed,
); );
// 11 was further slashed, but 21 and 101 were not. // 11 was further slashed, but 21 and 101 were not.
...@@ -2999,11 +3010,8 @@ fn deferred_slashes_are_deferred() { ...@@ -2999,11 +3010,8 @@ fn deferred_slashes_are_deferred() {
assert!(matches!( assert!(matches!(
staking_events_since_last_call().as_slice(), staking_events_since_last_call().as_slice(),
&[ &[
Event::Chilled { stash: 11 },
Event::ForceEra { mode: Forcing::ForceNew },
Event::SlashReported { validator: 11, slash_era: 1, .. }, Event::SlashReported { validator: 11, slash_era: 1, .. },
Event::StakersElected, Event::StakersElected,
Event::ForceEra { mode: Forcing::NotForcing },
.., ..,
Event::Slashed { staker: 11, amount: 100 }, Event::Slashed { staker: 11, amount: 100 },
Event::Slashed { staker: 101, amount: 12 } Event::Slashed { staker: 101, amount: 12 }
...@@ -3029,7 +3037,6 @@ fn retroactive_deferred_slashes_two_eras_before() { ...@@ -3029,7 +3037,6 @@ fn retroactive_deferred_slashes_two_eras_before() {
&[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }], &[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }],
&[Perbill::from_percent(10)], &[Perbill::from_percent(10)],
1, // should be deferred for two full eras, and applied at the beginning of era 4. 1, // should be deferred for two full eras, and applied at the beginning of era 4.
DisableStrategy::Never,
); );
mock::start_active_era(4); mock::start_active_era(4);
...@@ -3037,8 +3044,6 @@ fn retroactive_deferred_slashes_two_eras_before() { ...@@ -3037,8 +3044,6 @@ fn retroactive_deferred_slashes_two_eras_before() {
assert!(matches!( assert!(matches!(
staking_events_since_last_call().as_slice(), staking_events_since_last_call().as_slice(),
&[ &[
Event::Chilled { stash: 11 },
Event::ForceEra { mode: Forcing::ForceNew },
Event::SlashReported { validator: 11, slash_era: 1, .. }, Event::SlashReported { validator: 11, slash_era: 1, .. },
.., ..,
Event::Slashed { staker: 11, amount: 100 }, Event::Slashed { staker: 11, amount: 100 },
...@@ -3067,7 +3072,6 @@ fn retroactive_deferred_slashes_one_before() { ...@@ -3067,7 +3072,6 @@ fn retroactive_deferred_slashes_one_before() {
&[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }], &[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }],
&[Perbill::from_percent(10)], &[Perbill::from_percent(10)],
2, // should be deferred for two full eras, and applied at the beginning of era 5. 2, // should be deferred for two full eras, and applied at the beginning of era 5.
DisableStrategy::Never,
); );
mock::start_active_era(4); mock::start_active_era(4);
...@@ -3197,7 +3201,6 @@ fn remove_deferred() { ...@@ -3197,7 +3201,6 @@ fn remove_deferred() {
&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }], &[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
&[Perbill::from_percent(15)], &[Perbill::from_percent(15)],
1, 1,
DisableStrategy::WhenSlashed,
); );
// fails if empty // fails if empty
...@@ -3312,9 +3315,15 @@ fn remove_multi_deferred() { ...@@ -3312,9 +3315,15 @@ fn remove_multi_deferred() {
#[test] #[test]
fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_validator() { fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_validator() {
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default()
.validator_count(7)
.set_status(41, StakerStatus::Validator)
.set_status(51, StakerStatus::Validator)
.set_status(201, StakerStatus::Validator)
.set_status(202, StakerStatus::Validator)
.build_and_execute(|| {
mock::start_active_era(1); mock::start_active_era(1);
assert_eq_uvec!(Session::validators(), vec![11, 21]); assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]);
// pre-slash balance // pre-slash balance
assert_eq!(Balances::free_balance(11), 1000); assert_eq!(Balances::free_balance(11), 1000);
...@@ -3340,8 +3349,6 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid ...@@ -3340,8 +3349,6 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid
vec![ vec![
Event::StakersElected, Event::StakersElected,
Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
Event::Chilled { stash: 11 },
Event::ForceEra { mode: Forcing::ForceNew },
Event::SlashReported { Event::SlashReported {
validator: 11, validator: 11,
fraction: Perbill::from_percent(10), fraction: Perbill::from_percent(10),
...@@ -3357,8 +3364,8 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid ...@@ -3357,8 +3364,8 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid
assert_eq!(Balances::free_balance(11), 900); assert_eq!(Balances::free_balance(11), 900);
assert_eq!(Balances::free_balance(101), 2000 - nominator_slash_amount_11); assert_eq!(Balances::free_balance(101), 2000 - nominator_slash_amount_11);
// check that validator was chilled. // check that validator was disabled.
assert!(Validators::<Test>::iter().all(|(stash, _)| stash != 11)); assert!(is_disabled(11));
// actually re-bond the slashed validator // actually re-bond the slashed validator
assert_ok!(Staking::validate(RuntimeOrigin::signed(11), Default::default())); assert_ok!(Staking::validate(RuntimeOrigin::signed(11), Default::default()));
...@@ -3377,10 +3384,16 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid ...@@ -3377,10 +3384,16 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid
} }
#[test] #[test]
fn non_slashable_offence_doesnt_disable_validator() { fn non_slashable_offence_disables_validator() {
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default()
.validator_count(7)
.set_status(41, StakerStatus::Validator)
.set_status(51, StakerStatus::Validator)
.set_status(201, StakerStatus::Validator)
.set_status(202, StakerStatus::Validator)
.build_and_execute(|| {
mock::start_active_era(1); mock::start_active_era(1);
assert_eq_uvec!(Session::validators(), vec![11, 21]); assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]);
let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11);
let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
...@@ -3408,14 +3421,11 @@ fn non_slashable_offence_doesnt_disable_validator() { ...@@ -3408,14 +3421,11 @@ fn non_slashable_offence_doesnt_disable_validator() {
vec![ vec![
Event::StakersElected, Event::StakersElected,
Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
Event::Chilled { stash: 11 },
Event::ForceEra { mode: Forcing::ForceNew },
Event::SlashReported { Event::SlashReported {
validator: 11, validator: 11,
fraction: Perbill::from_percent(0), fraction: Perbill::from_percent(0),
slash_era: 1 slash_era: 1
}, },
Event::Chilled { stash: 21 },
Event::SlashReported { Event::SlashReported {
validator: 21, validator: 21,
fraction: Perbill::from_percent(25), fraction: Perbill::from_percent(25),
...@@ -3426,41 +3436,43 @@ fn non_slashable_offence_doesnt_disable_validator() { ...@@ -3426,41 +3436,43 @@ fn non_slashable_offence_doesnt_disable_validator() {
] ]
); );
// the offence for validator 10 wasn't slashable so it wasn't disabled // the offence for validator 11 wasn't slashable but it is disabled
assert!(!is_disabled(11)); assert!(is_disabled(11));
// whereas validator 20 gets disabled // validator 21 gets disabled too
assert!(is_disabled(21)); assert!(is_disabled(21));
}); });
} }
#[test] #[test]
fn slashing_independent_of_disabling_validator() { fn slashing_independent_of_disabling_validator() {
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default()
.validator_count(5)
.set_status(41, StakerStatus::Validator)
.set_status(51, StakerStatus::Validator)
.build_and_execute(|| {
mock::start_active_era(1); mock::start_active_era(1);
assert_eq_uvec!(Session::validators(), vec![11, 21]); assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51]);
let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11);
let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
let now = Staking::active_era().unwrap().index; let now = Staking::active_era().unwrap().index;
// offence with no slash associated, BUT disabling // offence with no slash associated
on_offence_in_era( on_offence_in_era(
&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
&[Perbill::zero()], &[Perbill::zero()],
now, now,
DisableStrategy::Always,
); );
// nomination remains untouched. // nomination remains untouched.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
// offence that slashes 25% of the bond, BUT not disabling // offence that slashes 25% of the bond
on_offence_in_era( on_offence_in_era(
&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
&[Perbill::from_percent(25)], &[Perbill::from_percent(25)],
now, now,
DisableStrategy::Never,
); );
// nomination remains untouched. // nomination remains untouched.
...@@ -3471,14 +3483,11 @@ fn slashing_independent_of_disabling_validator() { ...@@ -3471,14 +3483,11 @@ fn slashing_independent_of_disabling_validator() {
vec![ vec![
Event::StakersElected, Event::StakersElected,
Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
Event::Chilled { stash: 11 },
Event::ForceEra { mode: Forcing::ForceNew },
Event::SlashReported { Event::SlashReported {
validator: 11, validator: 11,
fraction: Perbill::from_percent(0), fraction: Perbill::from_percent(0),
slash_era: 1 slash_era: 1
}, },
Event::Chilled { stash: 21 },
Event::SlashReported { Event::SlashReported {
validator: 21, validator: 21,
fraction: Perbill::from_percent(25), fraction: Perbill::from_percent(25),
...@@ -3489,15 +3498,15 @@ fn slashing_independent_of_disabling_validator() { ...@@ -3489,15 +3498,15 @@ fn slashing_independent_of_disabling_validator() {
] ]
); );
// the offence for validator 10 was explicitly disabled // first validator is disabled but not slashed
assert!(is_disabled(11)); assert!(is_disabled(11));
// whereas validator 21 is explicitly not disabled // second validator is slashed but not disabled
assert!(!is_disabled(21)); assert!(!is_disabled(21));
}); });
} }
#[test] #[test]
fn offence_threshold_triggers_new_era() { fn offence_threshold_doesnt_trigger_new_era() {
ExtBuilder::default() ExtBuilder::default()
.validator_count(4) .validator_count(4)
.set_status(41, StakerStatus::Validator) .set_status(41, StakerStatus::Validator)
...@@ -3506,12 +3515,14 @@ fn offence_threshold_triggers_new_era() { ...@@ -3506,12 +3515,14 @@ fn offence_threshold_triggers_new_era() {
assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]); assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]);
assert_eq!( assert_eq!(
<Test as Config>::OffendingValidatorsThreshold::get(), UpToLimitDisablingStrategy::<DISABLING_LIMIT_FACTOR>::disable_limit(
Perbill::from_percent(75), Session::validators().len()
),
1
); );
// we have 4 validators and an offending validator threshold of 75%, // we have 4 validators and an offending validator threshold of 1/3,
// once the third validator commits an offence a new era should be forced // even if the third validator commits an offence a new era should not be forced
let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11);
let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
...@@ -3522,6 +3533,9 @@ fn offence_threshold_triggers_new_era() { ...@@ -3522,6 +3533,9 @@ fn offence_threshold_triggers_new_era() {
&[Perbill::zero()], &[Perbill::zero()],
); );
// 11 should be disabled because the byzantine threshold is 1
assert!(is_disabled(11));
assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing); assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
on_offence_now( on_offence_now(
...@@ -3529,6 +3543,10 @@ fn offence_threshold_triggers_new_era() { ...@@ -3529,6 +3543,10 @@ fn offence_threshold_triggers_new_era() {
&[Perbill::zero()], &[Perbill::zero()],
); );
// 21 should not be disabled because the number of disabled validators will be above the
// byzantine threshold
assert!(!is_disabled(21));
assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing); assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
on_offence_now( on_offence_now(
...@@ -3536,28 +3554,29 @@ fn offence_threshold_triggers_new_era() { ...@@ -3536,28 +3554,29 @@ fn offence_threshold_triggers_new_era() {
&[Perbill::zero()], &[Perbill::zero()],
); );
assert_eq!(ForceEra::<Test>::get(), Forcing::ForceNew); // same for 31
assert!(!is_disabled(31));
assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
}); });
} }
#[test] #[test]
fn disabled_validators_are_kept_disabled_for_whole_era() { fn disabled_validators_are_kept_disabled_for_whole_era() {
ExtBuilder::default() ExtBuilder::default()
.validator_count(4) .validator_count(7)
.set_status(41, StakerStatus::Validator) .set_status(41, StakerStatus::Validator)
.set_status(51, StakerStatus::Validator)
.set_status(201, StakerStatus::Validator)
.set_status(202, StakerStatus::Validator)
.build_and_execute(|| { .build_and_execute(|| {
mock::start_active_era(1); mock::start_active_era(1);
assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]); assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]);
assert_eq!(<Test as Config>::SessionsPerEra::get(), 3); assert_eq!(<Test as Config>::SessionsPerEra::get(), 3);
let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11);
let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
on_offence_now(
&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
&[Perbill::zero()],
);
on_offence_now( on_offence_now(
&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
&[Perbill::from_percent(25)], &[Perbill::from_percent(25)],
...@@ -3566,18 +3585,15 @@ fn disabled_validators_are_kept_disabled_for_whole_era() { ...@@ -3566,18 +3585,15 @@ fn disabled_validators_are_kept_disabled_for_whole_era() {
// nominations are not updated. // nominations are not updated.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
// validator 11 should not be disabled since the offence wasn't slashable
assert!(!is_disabled(11));
// validator 21 gets disabled since it got slashed // validator 21 gets disabled since it got slashed
assert!(is_disabled(21)); assert!(is_disabled(21));
advance_session(); advance_session();
// disabled validators should carry-on through all sessions in the era // disabled validators should carry-on through all sessions in the era
assert!(!is_disabled(11));
assert!(is_disabled(21)); assert!(is_disabled(21));
// validator 11 should now get disabled // validator 11 commits an offence
on_offence_now( on_offence_now(
&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
&[Perbill::from_percent(25)], &[Perbill::from_percent(25)],
...@@ -3687,7 +3703,13 @@ fn claim_reward_at_the_last_era_and_no_double_claim_and_invalid_claim() { ...@@ -3687,7 +3703,13 @@ fn claim_reward_at_the_last_era_and_no_double_claim_and_invalid_claim() {
#[test] #[test]
fn zero_slash_keeps_nominators() { fn zero_slash_keeps_nominators() {
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default()
.validator_count(7)
.set_status(41, StakerStatus::Validator)
.set_status(51, StakerStatus::Validator)
.set_status(201, StakerStatus::Validator)
.set_status(202, StakerStatus::Validator)
.build_and_execute(|| {
mock::start_active_era(1); mock::start_active_era(1);
assert_eq!(Balances::free_balance(11), 1000); assert_eq!(Balances::free_balance(11), 1000);
...@@ -3703,9 +3725,10 @@ fn zero_slash_keeps_nominators() { ...@@ -3703,9 +3725,10 @@ fn zero_slash_keeps_nominators() {
assert_eq!(Balances::free_balance(11), 1000); assert_eq!(Balances::free_balance(11), 1000);
assert_eq!(Balances::free_balance(101), 2000); assert_eq!(Balances::free_balance(101), 2000);
// 11 is still removed.. // 11 is not removed but disabled
assert!(Validators::<Test>::iter().all(|(stash, _)| stash != 11)); assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
// but their nominations are kept. assert!(is_disabled(11));
// and their nominations are kept.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
}); });
} }
...@@ -4710,7 +4733,7 @@ fn offences_weight_calculated_correctly() { ...@@ -4710,7 +4733,7 @@ fn offences_weight_calculated_correctly() {
let zero_offence_weight = let zero_offence_weight =
<Test as frame_system::Config>::DbWeight::get().reads_writes(4, 1); <Test as frame_system::Config>::DbWeight::get().reads_writes(4, 1);
assert_eq!( assert_eq!(
Staking::on_offence(&[], &[Perbill::from_percent(50)], 0, DisableStrategy::WhenSlashed), Staking::on_offence(&[], &[Perbill::from_percent(50)], 0),
zero_offence_weight zero_offence_weight
); );
...@@ -4735,7 +4758,6 @@ fn offences_weight_calculated_correctly() { ...@@ -4735,7 +4758,6 @@ fn offences_weight_calculated_correctly() {
&offenders, &offenders,
&[Perbill::from_percent(50)], &[Perbill::from_percent(50)],
0, 0,
DisableStrategy::WhenSlashed
), ),
n_offence_unapplied_weight n_offence_unapplied_weight
); );
...@@ -4765,7 +4787,6 @@ fn offences_weight_calculated_correctly() { ...@@ -4765,7 +4787,6 @@ fn offences_weight_calculated_correctly() {
&one_offender, &one_offender,
&[Perbill::from_percent(50)], &[Perbill::from_percent(50)],
0, 0,
DisableStrategy::WhenSlashed{}
), ),
one_offence_unapplied_weight one_offence_unapplied_weight
); );
...@@ -7011,7 +7032,13 @@ mod staking_unchecked { ...@@ -7011,7 +7032,13 @@ mod staking_unchecked {
#[test] #[test]
fn virtual_nominators_are_lazily_slashed() { fn virtual_nominators_are_lazily_slashed() {
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default()
.validator_count(7)
.set_status(41, StakerStatus::Validator)
.set_status(51, StakerStatus::Validator)
.set_status(201, StakerStatus::Validator)
.set_status(202, StakerStatus::Validator)
.build_and_execute(|| {
mock::start_active_era(1); mock::start_active_era(1);
let slash_percent = Perbill::from_percent(5); let slash_percent = Perbill::from_percent(5);
let initial_exposure = Staking::eras_stakers(active_era(), &11); let initial_exposure = Staking::eras_stakers(active_era(), &11);
...@@ -7033,7 +7060,10 @@ mod staking_unchecked { ...@@ -7033,7 +7060,10 @@ mod staking_unchecked {
// 11 goes offline // 11 goes offline
on_offence_now( on_offence_now(
&[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }], &[OffenceDetails {
offender: (11, initial_exposure.clone()),
reporters: vec![],
}],
&[slash_percent], &[slash_percent],
); );
...@@ -7926,3 +7956,69 @@ mod ledger_recovery { ...@@ -7926,3 +7956,69 @@ mod ledger_recovery {
}) })
} }
} }
mod byzantine_threshold_disabling_strategy {
use crate::{
tests::Test, ActiveEra, ActiveEraInfo, DisablingStrategy, UpToLimitDisablingStrategy,
};
use sp_staking::EraIndex;
// Common test data - the stash of the offending validator, the era of the offence and the
// active set
const OFFENDER_ID: <Test as frame_system::Config>::AccountId = 7;
const SLASH_ERA: EraIndex = 1;
const ACTIVE_SET: [<Test as pallet_session::Config>::ValidatorId; 7] = [1, 2, 3, 4, 5, 6, 7];
const OFFENDER_VALIDATOR_IDX: u32 = 6; // the offender is with index 6 in the active set
#[test]
fn dont_disable_for_ancient_offence() {
sp_io::TestExternalities::default().execute_with(|| {
let initially_disabled = vec![];
pallet_session::Validators::<Test>::put(ACTIVE_SET.to_vec());
ActiveEra::<Test>::put(ActiveEraInfo { index: 2, start: None });
let disable_offender =
<UpToLimitDisablingStrategy as DisablingStrategy<Test>>::decision(
&OFFENDER_ID,
SLASH_ERA,
&initially_disabled,
);
assert!(disable_offender.is_none());
});
}
#[test]
fn dont_disable_beyond_byzantine_threshold() {
sp_io::TestExternalities::default().execute_with(|| {
let initially_disabled = vec![1, 2];
pallet_session::Validators::<Test>::put(ACTIVE_SET.to_vec());
let disable_offender =
<UpToLimitDisablingStrategy as DisablingStrategy<Test>>::decision(
&OFFENDER_ID,
SLASH_ERA,
&initially_disabled,
);
assert!(disable_offender.is_none());
});
}
#[test]
fn disable_when_below_byzantine_threshold() {
sp_io::TestExternalities::default().execute_with(|| {
let initially_disabled = vec![1];
pallet_session::Validators::<Test>::put(ACTIVE_SET.to_vec());
let disable_offender =
<UpToLimitDisablingStrategy as DisablingStrategy<Test>>::decision(
&OFFENDER_ID,
SLASH_ERA,
&initially_disabled,
);
assert_eq!(disable_offender, Some(OFFENDER_VALIDATOR_IDX));
});
}
}
...@@ -244,7 +244,7 @@ fn construct_runtime_final_expansion( ...@@ -244,7 +244,7 @@ fn construct_runtime_final_expansion(
// Prevent UncheckedExtrinsic to print unused warning. // Prevent UncheckedExtrinsic to print unused warning.
const _: () = { const _: () = {
#[allow(unused)] #[allow(unused)]
type __hidden_use_of_unchecked_extrinsic = #unchecked_extrinsic; type __HiddenUseOfUncheckedExtrinsic = #unchecked_extrinsic;
}; };
#[derive( #[derive(
......
...@@ -2465,6 +2465,9 @@ pub mod pallet_macros { ...@@ -2465,6 +2465,9 @@ pub mod pallet_macros {
/// Finally, the `RuntimeTask` can then used by a script or off-chain worker to create and /// Finally, the `RuntimeTask` can then used by a script or off-chain worker to create and
/// submit such tasks via an extrinsic defined in `frame_system` called `do_task`. /// submit such tasks via an extrinsic defined in `frame_system` called `do_task`.
/// ///
/// When submitted as unsigned transactions (for example via an off-chain workder), note
/// that the tasks will be executed in a random order.
///
/// ## Example /// ## Example
#[doc = docify::embed!("src/tests/tasks.rs", tasks_example)] #[doc = docify::embed!("src/tests/tasks.rs", tasks_example)]
/// Now, this can be executed as follows: /// Now, this can be executed as follows:
......
...@@ -46,6 +46,8 @@ pub enum ProcessMessageError { ...@@ -46,6 +46,8 @@ pub enum ProcessMessageError {
/// the case that a queue is re-serviced within the same block after *yielding*. A queue is /// the case that a queue is re-serviced within the same block after *yielding*. A queue is
/// not required to *yield* again when it is being re-serviced withing the same block. /// not required to *yield* again when it is being re-serviced withing the same block.
Yield, Yield,
/// The message could not be processed for reaching the stack depth limit.
StackLimitReached,
} }
/// Can process messages from a specific origin. /// Can process messages from a specific origin.
...@@ -96,6 +98,8 @@ pub trait ServiceQueues { ...@@ -96,6 +98,8 @@ pub trait ServiceQueues {
/// - `weight_limit`: The maximum amount of dynamic weight that this call can use. /// - `weight_limit`: The maximum amount of dynamic weight that this call can use.
/// ///
/// Returns the dynamic weight used by this call; is never greater than `weight_limit`. /// Returns the dynamic weight used by this call; is never greater than `weight_limit`.
/// Should only be called in top-level runtime entry points like `on_initialize` or `on_idle`.
/// Otherwise, stack depth limit errors may be miss-handled.
fn service_queues(weight_limit: Weight) -> Weight; fn service_queues(weight_limit: Weight) -> Weight;
/// Executes a message that could not be executed by [`Self::service_queues()`] because it was /// Executes a message that could not be executed by [`Self::service_queues()`] because it was
......
...@@ -46,6 +46,10 @@ pub trait Task: Sized + FullCodec + TypeInfo + Clone + Debug + PartialEq + Eq { ...@@ -46,6 +46,10 @@ pub trait Task: Sized + FullCodec + TypeInfo + Clone + Debug + PartialEq + Eq {
fn iter() -> Self::Enumeration; fn iter() -> Self::Enumeration;
/// Checks if a particular instance of this `Task` variant is a valid piece of work. /// Checks if a particular instance of this `Task` variant is a valid piece of work.
///
/// This is used to validate tasks for unsigned execution. Hence, it MUST be cheap
/// with minimal to no storage reads. Else, it can make the blockchain vulnerable
/// to DoS attacks.
fn is_valid(&self) -> bool; fn is_valid(&self) -> bool;
/// Performs the work for this particular `Task` variant. /// Performs the work for this particular `Task` variant.
......
...@@ -161,22 +161,31 @@ impl<BlockNumber: Clone + sp_std::fmt::Debug + AtLeast32BitUnsigned> TryState<Bl ...@@ -161,22 +161,31 @@ impl<BlockNumber: Clone + sp_std::fmt::Debug + AtLeast32BitUnsigned> TryState<Bl
match targets { match targets {
Select::None => Ok(()), Select::None => Ok(()),
Select::All => { Select::All => {
let mut error_count = 0; let mut errors = Vec::<TryRuntimeError>::new();
for_tuples!(#( for_tuples!(#(
if let Err(_) = Tuple::try_state(n.clone(), targets.clone()) { if let Err(err) = Tuple::try_state(n.clone(), targets.clone()) {
error_count += 1; errors.push(err);
} }
)*); )*);
if error_count > 0 { if !errors.is_empty() {
log::error!(
target: "try-runtime",
"Detected errors while executing `try_state`:",
);
errors.iter().for_each(|err| {
log::error!( log::error!(
target: "try-runtime", target: "try-runtime",
"{} pallets exited with errors while executing try_state checks.", "{:?}",
error_count err
); );
});
return Err( return Err(
"Detected errors while executing try_state checks. See logs for more info." "Detected errors while executing `try_state` checks. See logs for more \
info."
.into(), .into(),
) )
} }
......
...@@ -741,9 +741,7 @@ pub mod pallet { ...@@ -741,9 +741,7 @@ pub mod pallet {
#[cfg(feature = "experimental")] #[cfg(feature = "experimental")]
#[pallet::call_index(8)] #[pallet::call_index(8)]
#[pallet::weight(task.weight())] #[pallet::weight(task.weight())]
pub fn do_task(origin: OriginFor<T>, task: T::RuntimeTask) -> DispatchResultWithPostInfo { pub fn do_task(_origin: OriginFor<T>, task: T::RuntimeTask) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;
if !task.is_valid() { if !task.is_valid() {
return Err(Error::<T>::InvalidTask.into()) return Err(Error::<T>::InvalidTask.into())
} }
...@@ -1032,6 +1030,18 @@ pub mod pallet { ...@@ -1032,6 +1030,18 @@ pub mod pallet {
}) })
} }
} }
#[cfg(feature = "experimental")]
if let Call::do_task { ref task } = call {
if task.is_valid() {
return Ok(ValidTransaction {
priority: u64::max_value(),
requires: Vec::new(),
provides: vec![T::Hashing::hash_of(&task.encode()).as_ref().to_vec()],
longevity: TransactionLongevity::max_value(),
propagate: true,
})
}
}
Err(InvalidTransaction::Call.into()) Err(InvalidTransaction::Call.into())
} }
} }
......
...@@ -19,8 +19,30 @@ use alloc::{vec, vec::Vec}; ...@@ -19,8 +19,30 @@ use alloc::{vec, vec::Vec};
use codec::{Decode, Encode, Error, Input}; use codec::{Decode, Encode, Error, Input};
use core::cmp; use core::cmp;
use scale_info::TypeInfo; use scale_info::TypeInfo;
use sp_application_crypto::RuntimeAppPublic;
use sp_runtime::traits::Hash;
use crate::{Payload, ValidatorSetId}; use crate::{BeefyAuthorityId, Payload, ValidatorSet, ValidatorSetId};
/// A commitment signature, accompanied by the id of the validator that it belongs to.
#[derive(Debug)]
pub struct KnownSignature<TAuthorityId, TSignature> {
/// The signing validator.
pub validator_id: TAuthorityId,
/// The signature.
pub signature: TSignature,
}
impl<TAuthorityId: Clone, TSignature: Clone> KnownSignature<&TAuthorityId, &TSignature> {
/// Creates a `KnownSignature<TAuthorityId, TSignature>` from an
/// `KnownSignature<&TAuthorityId, &TSignature>`.
pub fn to_owned(&self) -> KnownSignature<TAuthorityId, TSignature> {
KnownSignature {
validator_id: self.validator_id.clone(),
signature: self.signature.clone(),
}
}
}
/// A commitment signed by GRANDPA validators as part of BEEFY protocol. /// A commitment signed by GRANDPA validators as part of BEEFY protocol.
/// ///
...@@ -113,9 +135,49 @@ impl<TBlockNumber: core::fmt::Debug, TSignature> core::fmt::Display ...@@ -113,9 +135,49 @@ impl<TBlockNumber: core::fmt::Debug, TSignature> core::fmt::Display
impl<TBlockNumber, TSignature> SignedCommitment<TBlockNumber, TSignature> { impl<TBlockNumber, TSignature> SignedCommitment<TBlockNumber, TSignature> {
/// Return the number of collected signatures. /// Return the number of collected signatures.
pub fn no_of_signatures(&self) -> usize { pub fn signature_count(&self) -> usize {
self.signatures.iter().filter(|x| x.is_some()).count() self.signatures.iter().filter(|x| x.is_some()).count()
} }
/// Verify all the commitment signatures against the validator set that was active
/// at the block where the commitment was generated.
///
/// Returns the valid validator-signature pairs if the commitment can be verified.
pub fn verify_signatures<'a, TAuthorityId, MsgHash>(
&'a self,
target_number: TBlockNumber,
validator_set: &'a ValidatorSet<TAuthorityId>,
) -> Result<Vec<KnownSignature<&'a TAuthorityId, &'a TSignature>>, u32>
where
TBlockNumber: Clone + Encode + PartialEq,
TAuthorityId: RuntimeAppPublic<Signature = TSignature> + BeefyAuthorityId<MsgHash>,
MsgHash: Hash,
{
if self.signatures.len() != validator_set.len() ||
self.commitment.validator_set_id != validator_set.id() ||
self.commitment.block_number != target_number
{
return Err(0)
}
// Arrangement of signatures in the commitment should be in the same order
// as validators for that set.
let encoded_commitment = self.commitment.encode();
let signatories: Vec<_> = validator_set
.validators()
.into_iter()
.zip(self.signatures.iter())
.filter_map(|(id, maybe_signature)| {
let signature = maybe_signature.as_ref()?;
match BeefyAuthorityId::verify(id, signature, &encoded_commitment) {
true => Some(KnownSignature { validator_id: id, signature }),
false => None,
}
})
.collect();
Ok(signatories)
}
} }
/// Type to be used to denote placement of signatures /// Type to be used to denote placement of signatures
...@@ -439,13 +501,13 @@ mod tests { ...@@ -439,13 +501,13 @@ mod tests {
commitment, commitment,
signatures: vec![None, None, Some(sigs.0), Some(sigs.1)], signatures: vec![None, None, Some(sigs.0), Some(sigs.1)],
}; };
assert_eq!(signed.no_of_signatures(), 2); assert_eq!(signed.signature_count(), 2);
// when // when
signed.signatures[2] = None; signed.signatures[2] = None;
// then // then
assert_eq!(signed.no_of_signatures(), 1); assert_eq!(signed.signature_count(), 1);
} }
#[test] #[test]
......
...@@ -43,7 +43,7 @@ pub mod witness; ...@@ -43,7 +43,7 @@ pub mod witness;
#[cfg(feature = "std")] #[cfg(feature = "std")]
pub mod test_utils; pub mod test_utils;
pub use commitment::{Commitment, SignedCommitment, VersionedFinalityProof}; pub use commitment::{Commitment, KnownSignature, SignedCommitment, VersionedFinalityProof};
pub use payload::{known_payloads, BeefyPayloadId, Payload, PayloadProvider}; pub use payload::{known_payloads, BeefyPayloadId, Payload, PayloadProvider};
use alloc::vec::Vec; use alloc::vec::Vec;
......
...@@ -182,7 +182,7 @@ impl From<MultiRemovalResults> for KillStorageResult { ...@@ -182,7 +182,7 @@ impl From<MultiRemovalResults> for KillStorageResult {
pub trait Storage { pub trait Storage {
/// Returns the data for `key` in the storage or `None` if the key can not be found. /// Returns the data for `key` in the storage or `None` if the key can not be found.
fn get(&self, key: &[u8]) -> Option<bytes::Bytes> { fn get(&self, key: &[u8]) -> Option<bytes::Bytes> {
self.storage(key).map(|s| bytes::Bytes::from(s.to_vec())) self.storage(key).map(bytes::Bytes::from)
} }
/// Get `key` from storage, placing the value into `value_out` and return the number of /// Get `key` from storage, placing the value into `value_out` and return the number of
......
...@@ -37,29 +37,6 @@ pub type Kind = [u8; 16]; ...@@ -37,29 +37,6 @@ pub type Kind = [u8; 16];
/// so that we can slash it accordingly. /// so that we can slash it accordingly.
pub type OffenceCount = u32; pub type OffenceCount = u32;
/// In case of an offence, which conditions get an offending validator disabled.
#[derive(
Clone,
Copy,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
Encode,
Decode,
sp_runtime::RuntimeDebug,
scale_info::TypeInfo,
)]
pub enum DisableStrategy {
/// Independently of slashing, this offence will not disable the offender.
Never,
/// Only disable the offender if it is also slashed.
WhenSlashed,
/// Independently of slashing, this offence will always disable the offender.
Always,
}
/// A trait implemented by an offence report. /// A trait implemented by an offence report.
/// ///
/// This trait assumes that the offence is legitimate and was validated already. /// This trait assumes that the offence is legitimate and was validated already.
...@@ -102,11 +79,6 @@ pub trait Offence<Offender> { ...@@ -102,11 +79,6 @@ pub trait Offence<Offender> {
/// number. Note that for GRANDPA the round number is reset each epoch. /// number. Note that for GRANDPA the round number is reset each epoch.
fn time_slot(&self) -> Self::TimeSlot; fn time_slot(&self) -> Self::TimeSlot;
/// In which cases this offence needs to disable offenders until the next era starts.
fn disable_strategy(&self) -> DisableStrategy {
DisableStrategy::WhenSlashed
}
/// A slash fraction of the total exposure that should be slashed for this /// A slash fraction of the total exposure that should be slashed for this
/// particular offence for the `offenders_count` that happened at a singular `TimeSlot`. /// particular offence for the `offenders_count` that happened at a singular `TimeSlot`.
/// ///
...@@ -177,15 +149,12 @@ pub trait OnOffenceHandler<Reporter, Offender, Res> { ...@@ -177,15 +149,12 @@ pub trait OnOffenceHandler<Reporter, Offender, Res> {
/// ///
/// The `session` parameter is the session index of the offence. /// The `session` parameter is the session index of the offence.
/// ///
/// The `disable_strategy` parameter decides if the offenders need to be disabled immediately.
///
/// The receiver might decide to not accept this offence. In this case, the call site is /// The receiver might decide to not accept this offence. In this case, the call site is
/// responsible for queuing the report and re-submitting again. /// responsible for queuing the report and re-submitting again.
fn on_offence( fn on_offence(
offenders: &[OffenceDetails<Reporter, Offender>], offenders: &[OffenceDetails<Reporter, Offender>],
slash_fraction: &[Perbill], slash_fraction: &[Perbill],
session: SessionIndex, session: SessionIndex,
disable_strategy: DisableStrategy,
) -> Res; ) -> Res;
} }
...@@ -194,7 +163,6 @@ impl<Reporter, Offender, Res: Default> OnOffenceHandler<Reporter, Offender, Res> ...@@ -194,7 +163,6 @@ impl<Reporter, Offender, Res: Default> OnOffenceHandler<Reporter, Offender, Res>
_offenders: &[OffenceDetails<Reporter, Offender>], _offenders: &[OffenceDetails<Reporter, Offender>],
_slash_fraction: &[Perbill], _slash_fraction: &[Perbill],
_session: SessionIndex, _session: SessionIndex,
_disable_strategy: DisableStrategy,
) -> Res { ) -> Res {
Default::default() Default::default()
} }
......
...@@ -830,16 +830,19 @@ where ...@@ -830,16 +830,19 @@ where
child_prefix: StorageKey, child_prefix: StorageKey,
at: B::Hash, at: B::Hash,
) -> Result<Vec<StorageKey>, &'static str> { ) -> Result<Vec<StorageKey>, &'static str> {
// This is deprecated and will generate a warning which causes the CI to fail. let retry_strategy =
#[allow(warnings)] FixedInterval::new(Self::KEYS_PAGE_RETRY_INTERVAL).take(Self::MAX_RETRIES);
let child_keys = substrate_rpc_client::ChildStateApi::storage_keys( let get_child_keys_closure = || {
#[allow(deprecated)]
substrate_rpc_client::ChildStateApi::storage_keys(
client, client,
PrefixedStorageKey::new(prefixed_top_key.as_ref().to_vec()), PrefixedStorageKey::new(prefixed_top_key.as_ref().to_vec()),
child_prefix, child_prefix.clone(),
Some(at), Some(at),
) )
.await };
.map_err(|e| { let child_keys =
Retry::spawn(retry_strategy, get_child_keys_closure).await.map_err(|e| {
error!(target: LOG_TARGET, "Error = {:?}", e); error!(target: LOG_TARGET, "Error = {:?}", e);
"rpc child_get_keys failed." "rpc child_get_keys failed."
})?; })?;
......
...@@ -116,6 +116,39 @@ impl WasmBuilder { ...@@ -116,6 +116,39 @@ impl WasmBuilder {
WasmBuilderSelectProject { _ignore: () } WasmBuilderSelectProject { _ignore: () }
} }
/// Build the WASM binary using the recommended default values.
///
/// This is the same as calling:
/// ```no_run
/// substrate_wasm_builder::WasmBuilder::new()
/// .with_current_project()
/// .import_memory()
/// .export_heap_base()
/// .build();
/// ```
pub fn build_using_defaults() {
WasmBuilder::new()
.with_current_project()
.import_memory()
.export_heap_base()
.build();
}
/// Init the wasm builder with the recommended default values.
///
/// In contrast to [`Self::build_using_defaults`] it does not build the WASM binary directly.
///
/// This is the same as calling:
/// ```no_run
/// substrate_wasm_builder::WasmBuilder::new()
/// .with_current_project()
/// .import_memory()
/// .export_heap_base();
/// ```
pub fn init_with_defaults() -> Self {
WasmBuilder::new().with_current_project().import_memory().export_heap_base()
}
/// Enable exporting `__heap_base` as global variable in the WASM binary. /// Enable exporting `__heap_base` as global variable in the WASM binary.
/// ///
/// This adds `-Clink-arg=--export=__heap_base` to `RUST_FLAGS`. /// This adds `-Clink-arg=--export=__heap_base` to `RUST_FLAGS`.
......
...@@ -33,15 +33,9 @@ ...@@ -33,15 +33,9 @@
//! use substrate_wasm_builder::WasmBuilder; //! use substrate_wasm_builder::WasmBuilder;
//! //!
//! fn main() { //! fn main() {
//! WasmBuilder::new() //! // Builds the WASM binary using the recommended defaults.
//! // Tell the builder to build the project (crate) this `build.rs` is part of. //! // If you need more control, you can call `new` or `init_with_defaults`.
//! .with_current_project() //! WasmBuilder::build_using_defaults();
//! // Make sure to export the `heap_base` global, this is required by Substrate
//! .export_heap_base()
//! // Build the Wasm file so that it imports the memory (need to be provided by at instantiation)
//! .import_memory()
//! // Build it.
//! .build()
//! } //! }
//! ``` //! ```
//! //!
......
...@@ -18,10 +18,6 @@ ...@@ -18,10 +18,6 @@
fn main() { fn main() {
#[cfg(feature = "std")] #[cfg(feature = "std")]
{ {
substrate_wasm_builder::WasmBuilder::new() substrate_wasm_builder::WasmBuilder::build_using_defaults();
.with_current_project()
.export_heap_base()
.import_memory()
.build();
} }
} }
#[cfg(feature = "std")] #[cfg(feature = "std")]
fn main() { fn main() {
substrate_wasm_builder::WasmBuilder::new() substrate_wasm_builder::WasmBuilder::build_using_defaults();
.with_current_project()
.export_heap_base()
.import_memory()
.build()
} }
/// The wasm builder is deactivated when compiling /// The wasm builder is deactivated when compiling
......
...@@ -26,6 +26,8 @@ parameter_types! { ...@@ -26,6 +26,8 @@ parameter_types! {
pub const RelayLocation: Location = Location::parent(); pub const RelayLocation: Location = Location::parent();
pub const RelayNetwork: Option<NetworkId> = None; pub const RelayNetwork: Option<NetworkId> = None;
pub RelayChainOrigin: RuntimeOrigin = cumulus_pallet_xcm::Origin::Relay.into(); pub RelayChainOrigin: RuntimeOrigin = cumulus_pallet_xcm::Origin::Relay.into();
// For the real deployment, it is recommended to set `RelayNetwork` according to the relay chain
// and prepend `UniversalLocation` with `GlobalConsensus(RelayNetwork::get())`.
pub UniversalLocation: InteriorLocation = Parachain(ParachainInfo::parachain_id().into()).into(); pub UniversalLocation: InteriorLocation = Parachain(ParachainInfo::parachain_id().into()).into();
} }
......