......@@ -43,7 +43,7 @@ use sp_runtime::{
};
use sp_staking::{
currency_to_vote::CurrencyToVote,
offence::{DisableStrategy, OffenceDetails, OnOffenceHandler},
offence::{OffenceDetails, OnOffenceHandler},
EraIndex, OnStakingUpdate, Page, SessionIndex, Stake,
StakingAccount::{self, Controller, Stash},
StakingInterface,
......@@ -505,10 +505,8 @@ impl<T: Config> Pallet<T> {
}
// disable all offending validators that have been disabled for the whole era
for (index, disabled) in <OffendingValidators<T>>::get() {
if disabled {
T::SessionInterface::disable_validator(index);
}
for index in <DisabledValidators<T>>::get() {
T::SessionInterface::disable_validator(index);
}
}
......@@ -598,8 +596,8 @@ impl<T: Config> Pallet<T> {
<ErasValidatorReward<T>>::insert(&active_era.index, validator_payout);
T::RewardRemainder::on_unbalanced(T::Currency::issue(remainder));
// Clear offending validators.
<OffendingValidators<T>>::kill();
// Clear disabled validators.
<DisabledValidators<T>>::kill();
}
}
......@@ -868,14 +866,6 @@ impl<T: Config> Pallet<T> {
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")]
pub fn add_era_stakers(
current_era: EraIndex,
......@@ -1447,7 +1437,6 @@ where
>],
slash_fraction: &[Perbill],
slash_session: SessionIndex,
disable_strategy: DisableStrategy,
) -> Weight {
let reward_proportion = SlashRewardFraction::<T>::get();
let mut consumed_weight = Weight::from_parts(0, 0);
......@@ -1512,7 +1501,6 @@ where
window_start,
now: active_era,
reward_proportion,
disable_strategy,
});
Self::deposit_event(Event::<T>::SlashReported {
......@@ -1986,7 +1974,8 @@ impl<T: Config> Pallet<T> {
Self::check_nominators()?;
Self::check_exposures()?;
Self::check_paged_exposures()?;
Self::check_count()
Self::check_count()?;
Self::ensure_disabled_validators_sorted()
}
/// Invariants:
......@@ -2300,4 +2289,12 @@ impl<T: Config> Pallet<T> {
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;
pub use impls::*;
use crate::{
slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout,
EraRewardPoints, Exposure, ExposurePage, Forcing, LedgerIntegrityState, MaxNominationsOf,
NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination,
SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk, ValidatorPrefs,
slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, DisablingStrategy,
EraPayout, EraRewardPoints, Exposure, ExposurePage, Forcing, LedgerIntegrityState,
MaxNominationsOf, NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf,
RewardDestination, SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk,
ValidatorPrefs,
};
// The speculative number of spans are used as an input of the weight annotation of
......@@ -67,7 +68,7 @@ pub mod pallet {
use super::*;
/// The in-code storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(14);
const STORAGE_VERSION: StorageVersion = StorageVersion::new(15);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
......@@ -217,10 +218,6 @@ pub mod pallet {
#[pallet::constant]
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,
/// used for NPoS election.
///
......@@ -278,6 +275,9 @@ pub mod pallet {
/// WARNING: this only reports slashing and withdraw events for the time being.
type EventListeners: sp_staking::OnStakingUpdate<Self::AccountId, BalanceOf<Self>>;
// `DisablingStragegy` controls how validators are disabled
type DisablingStrategy: DisablingStrategy<Self>;
/// Some parameters of the benchmarking.
type BenchmarkingConfig: BenchmarkingConfig;
......@@ -654,19 +654,16 @@ pub mod pallet {
#[pallet::getter(fn current_planned_session)]
pub type CurrentPlannedSession<T> = StorageValue<_, SessionIndex, ValueQuery>;
/// Indices of validators that have offended in the active era and whether they are currently
/// disabled.
/// Indices of validators that have offended in the active era. The offenders are disabled for a
/// 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
/// validator being disabled (if there was no slash). This is needed to track the percentage of
/// 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.
/// The vec is always kept sorted so that we can find whether a given validator has previously
/// offended using binary search.
#[pallet::storage]
#[pallet::unbounded]
#[pallet::getter(fn offending_validators)]
pub type OffendingValidators<T: Config> = StorageValue<_, Vec<(u32, bool)>, ValueQuery>;
pub type DisabledValidators<T: Config> = StorageValue<_, Vec<u32>, ValueQuery>;
/// 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
......
......@@ -50,21 +50,21 @@
//! Based on research at <https://research.web3.foundation/en/latest/polkadot/slashing/npos.html>
use crate::{
BalanceOf, Config, Error, Exposure, NegativeImbalanceOf, NominatorSlashInEra,
OffendingValidators, Pallet, Perbill, SessionInterface, SpanSlash, UnappliedSlash,
BalanceOf, Config, DisabledValidators, DisablingStrategy, Error, Exposure, NegativeImbalanceOf,
NominatorSlashInEra, Pallet, Perbill, SessionInterface, SpanSlash, UnappliedSlash,
ValidatorSlashInEra,
};
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::{
ensure,
traits::{Currency, Defensive, DefensiveSaturating, Get, Imbalance, OnUnbalanced},
traits::{Currency, Defensive, DefensiveSaturating, Imbalance, OnUnbalanced},
};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{Saturating, Zero},
DispatchResult, RuntimeDebug,
};
use sp_staking::{offence::DisableStrategy, EraIndex};
use sp_staking::EraIndex;
use sp_std::vec::Vec;
/// 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> {
/// The maximum percentage of a slash that ever gets paid out.
/// This is f_inf in the paper.
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
......@@ -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);
if target_span == Some(spans.span_index()) {
// misbehavior occurred within the current slashing span - take appropriate
// actions.
// chill the validator - it misbehaved in the current span and should
// not continue in the next election. also end the slashing span.
// misbehavior occurred within the current slashing span - end current span.
// Check <https://github.com/paritytech/polkadot-sdk/issues/2650> for details.
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.stash, disable_when_slashed);
add_offending_validator::<T>(&params);
let mut nominators_slashed = Vec::new();
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>) {
);
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);
<Pallet<T>>::chill_stash(params.stash);
}
let disable_without_slash = params.disable_strategy == DisableStrategy::Always;
add_offending_validator::<T>(params.stash, disable_without_slash);
add_offending_validator::<T>(&params);
}
/// Add the given validator to the offenders list and optionally disable it.
/// If after adding the validator `OffendingValidatorsThreshold` is reached
/// a new era will be forced.
fn add_offending_validator<T: Config>(stash: &T::AccountId, disable: bool) {
OffendingValidators::<T>::mutate(|offending| {
let validators = T::SessionInterface::validators();
let validator_index = match validators.iter().position(|i| i == stash) {
Some(index) => index,
None => return,
};
let validator_index_u32 = validator_index as u32;
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);
}
},
/// Inform the [`DisablingStrategy`] implementation about the new offender and disable the list of
/// validators provided by [`make_disabling_decision`].
fn add_offending_validator<T: Config>(params: &SlashParams<T>) {
DisabledValidators::<T>::mutate(|disabled| {
if let Some(offender) =
T::DisablingStrategy::decision(params.stash, params.slash_era, &disabled)
{
// Add the validator to `DisabledValidators` and disable it. Do nothing if it is
// already disabled.
if let Err(index) = disabled.binary_search_by_key(&offender, |index| *index) {
disabled.insert(index, offender);
T::SessionInterface::disable_validator(offender);
}
}
});
// `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.
......
......@@ -38,7 +38,7 @@ use sp_runtime::{
Perbill, Percent, Perquintill, Rounding, TokenError,
};
use sp_staking::{
offence::{DisableStrategy, OffenceDetails, OnOffenceHandler},
offence::{OffenceDetails, OnOffenceHandler},
SessionIndex,
};
use sp_std::prelude::*;
......@@ -716,56 +716,65 @@ fn nominating_and_rewards_should_work() {
#[test]
fn nominators_also_get_slashed_pro_rata() {
ExtBuilder::default().build_and_execute(|| {
mock::start_active_era(1);
let slash_percent = Perbill::from_percent(5);
let initial_exposure = Staking::eras_stakers(active_era(), &11);
// 101 is a nominator for 11
assert_eq!(initial_exposure.others.first().unwrap().who, 101);
// staked values;
let nominator_stake = Staking::ledger(101.into()).unwrap().active;
let nominator_balance = balances(&101).0;
let validator_stake = Staking::ledger(11.into()).unwrap().active;
let validator_balance = balances(&11).0;
let exposed_stake = initial_exposure.total;
let exposed_validator = initial_exposure.own;
let exposed_nominator = initial_exposure.others.first().unwrap().value;
// 11 goes offline
on_offence_now(
&[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }],
&[slash_percent],
);
ExtBuilder::default()
.validator_count(4)
.set_status(41, StakerStatus::Validator)
.build_and_execute(|| {
mock::start_active_era(1);
let slash_percent = Perbill::from_percent(5);
let initial_exposure = Staking::eras_stakers(active_era(), &11);
// 101 is a nominator for 11
assert_eq!(initial_exposure.others.first().unwrap().who, 101);
// staked values;
let nominator_stake = Staking::ledger(101.into()).unwrap().active;
let nominator_balance = balances(&101).0;
let validator_stake = Staking::ledger(11.into()).unwrap().active;
let validator_balance = balances(&11).0;
let exposed_stake = initial_exposure.total;
let exposed_validator = initial_exposure.own;
let exposed_nominator = initial_exposure.others.first().unwrap().value;
// 11 goes offline
on_offence_now(
&[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }],
&[slash_percent],
);
// both stakes must have been decreased.
assert!(Staking::ledger(101.into()).unwrap().active < nominator_stake);
assert!(Staking::ledger(11.into()).unwrap().active < validator_stake);
// both stakes must have been decreased.
assert!(Staking::ledger(101.into()).unwrap().active < nominator_stake);
assert!(Staking::ledger(11.into()).unwrap().active < validator_stake);
let slash_amount = slash_percent * exposed_stake;
let validator_share =
Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount;
let nominator_share =
Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount;
let slash_amount = slash_percent * exposed_stake;
let validator_share =
Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount;
let nominator_share =
Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount;
// both slash amounts need to be positive for the test to make sense.
assert!(validator_share > 0);
assert!(nominator_share > 0);
// both slash amounts need to be positive for the test to make sense.
assert!(validator_share > 0);
assert!(nominator_share > 0);
// both stakes must have been decreased pro-rata.
assert_eq!(Staking::ledger(101.into()).unwrap().active, nominator_stake - nominator_share);
assert_eq!(Staking::ledger(11.into()).unwrap().active, validator_stake - validator_share);
assert_eq!(
balances(&101).0, // free balance
nominator_balance - nominator_share,
);
assert_eq!(
balances(&11).0, // free balance
validator_balance - validator_share,
);
// Because slashing happened.
assert!(is_disabled(11));
});
// both stakes must have been decreased pro-rata.
assert_eq!(
Staking::ledger(101.into()).unwrap().active,
nominator_stake - nominator_share
);
assert_eq!(
Staking::ledger(11.into()).unwrap().active,
validator_stake - validator_share
);
assert_eq!(
balances(&101).0, // free balance
nominator_balance - nominator_share,
);
assert_eq!(
balances(&11).0, // free balance
validator_balance - validator_share,
);
// Because slashing happened.
assert!(is_disabled(11));
});
}
#[test]
......@@ -2401,7 +2410,7 @@ fn era_is_always_same_length() {
}
#[test]
fn offence_forces_new_era() {
fn offence_doesnt_force_new_era() {
ExtBuilder::default().build_and_execute(|| {
on_offence_now(
&[OffenceDetails {
......@@ -2411,7 +2420,7 @@ fn offence_forces_new_era() {
&[Perbill::from_percent(5)],
);
assert_eq!(Staking::force_era(), Forcing::ForceNew);
assert_eq!(Staking::force_era(), Forcing::NotForcing);
});
}
......@@ -2435,26 +2444,32 @@ fn offence_ensures_new_era_without_clobbering() {
#[test]
fn offence_deselects_validator_even_when_slash_is_zero() {
ExtBuilder::default().build_and_execute(|| {
assert!(Session::validators().contains(&11));
assert!(<Validators<Test>>::contains_key(11));
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!(<Validators<Test>>::contains_key(11));
on_offence_now(
&[OffenceDetails {
offender: (11, Staking::eras_stakers(active_era(), &11)),
reporters: vec![],
}],
&[Perbill::from_percent(0)],
);
on_offence_now(
&[OffenceDetails {
offender: (11, Staking::eras_stakers(active_era(), &11)),
reporters: vec![],
}],
&[Perbill::from_percent(0)],
);
assert_eq!(Staking::force_era(), Forcing::ForceNew);
assert!(!<Validators<Test>>::contains_key(11));
assert_eq!(Staking::force_era(), Forcing::NotForcing);
assert!(is_disabled(11));
mock::start_active_era(1);
mock::start_active_era(1);
assert!(!Session::validators().contains(&11));
assert!(!<Validators<Test>>::contains_key(11));
});
// The validator should be reenabled in the new era
assert!(!is_disabled(11));
});
}
#[test]
......@@ -2479,71 +2494,70 @@ fn slashing_performed_according_exposure() {
}
#[test]
fn slash_in_old_span_does_not_deselect() {
ExtBuilder::default().build_and_execute(|| {
mock::start_active_era(1);
assert!(<Validators<Test>>::contains_key(11));
assert!(Session::validators().contains(&11));
on_offence_now(
&[OffenceDetails {
offender: (11, Staking::eras_stakers(active_era(), &11)),
reporters: vec![],
}],
&[Perbill::from_percent(0)],
);
fn validator_is_not_disabled_for_an_offence_in_previous_era() {
ExtBuilder::default()
.validator_count(4)
.set_status(41, StakerStatus::Validator)
.build_and_execute(|| {
mock::start_active_era(1);
assert_eq!(Staking::force_era(), Forcing::ForceNew);
assert!(!<Validators<Test>>::contains_key(11));
assert!(<Validators<Test>>::contains_key(11));
assert!(Session::validators().contains(&11));
mock::start_active_era(2);
on_offence_now(
&[OffenceDetails {
offender: (11, Staking::eras_stakers(active_era(), &11)),
reporters: vec![],
}],
&[Perbill::from_percent(0)],
);
Staking::validate(RuntimeOrigin::signed(11), Default::default()).unwrap();
assert_eq!(Staking::force_era(), Forcing::NotForcing);
assert!(<Validators<Test>>::contains_key(11));
assert!(!Session::validators().contains(&11));
assert_eq!(Staking::force_era(), Forcing::NotForcing);
assert!(is_disabled(11));
mock::start_active_era(3);
mock::start_active_era(2);
// this staker is in a new slashing span now, having re-registered after
// their prior slash.
// the validator is not disabled in the new era
Staking::validate(RuntimeOrigin::signed(11), Default::default()).unwrap();
assert_eq!(Staking::force_era(), Forcing::NotForcing);
assert!(<Validators<Test>>::contains_key(11));
assert!(Session::validators().contains(&11));
on_offence_in_era(
&[OffenceDetails {
offender: (11, Staking::eras_stakers(active_era(), &11)),
reporters: vec![],
}],
&[Perbill::from_percent(0)],
1,
DisableStrategy::WhenSlashed,
);
mock::start_active_era(3);
// the validator doesn't get chilled again
assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
// an offence committed in era 1 is reported in era 3
on_offence_in_era(
&[OffenceDetails {
offender: (11, Staking::eras_stakers(active_era(), &11)),
reporters: vec![],
}],
&[Perbill::from_percent(0)],
1,
);
// but we are still forcing a new era
assert_eq!(Staking::force_era(), Forcing::ForceNew);
// the validator doesn't get disabled for an old offence
assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
assert!(!is_disabled(11));
on_offence_in_era(
&[OffenceDetails {
offender: (11, Staking::eras_stakers(active_era(), &11)),
reporters: vec![],
}],
// NOTE: A 100% slash here would clean up the account, causing de-registration.
&[Perbill::from_percent(95)],
1,
DisableStrategy::WhenSlashed,
);
// and we are not forcing a new era
assert_eq!(Staking::force_era(), Forcing::NotForcing);
// the validator doesn't get chilled again
assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
on_offence_in_era(
&[OffenceDetails {
offender: (11, Staking::eras_stakers(active_era(), &11)),
reporters: vec![],
}],
// NOTE: A 100% slash here would clean up the account, causing de-registration.
&[Perbill::from_percent(95)],
1,
);
// but it's disabled
assert!(is_disabled(11));
// and we are still forcing a new era
assert_eq!(Staking::force_era(), Forcing::ForceNew);
});
// the validator doesn't get disabled again
assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
assert!(!is_disabled(11));
// and we are still not forcing a new era
assert_eq!(Staking::force_era(), Forcing::NotForcing);
});
}
#[test]
......@@ -2671,7 +2685,7 @@ fn dont_slash_if_fraction_is_zero() {
// The validator hasn't been slashed. The new era is not forced.
assert_eq!(Balances::free_balance(11), 1000);
assert_eq!(Staking::force_era(), Forcing::ForceNew);
assert_eq!(Staking::force_era(), Forcing::NotForcing);
});
}
......@@ -2692,7 +2706,7 @@ fn only_slash_for_max_in_era() {
// The validator has been slashed and has been force-chilled.
assert_eq!(Balances::free_balance(11), 500);
assert_eq!(Staking::force_era(), Forcing::ForceNew);
assert_eq!(Staking::force_era(), Forcing::NotForcing);
on_offence_now(
&[OffenceDetails {
......@@ -2833,7 +2847,6 @@ fn slashing_nominators_by_span_max() {
}],
&[Perbill::from_percent(10)],
2,
DisableStrategy::WhenSlashed,
);
assert_eq!(Balances::free_balance(11), 900);
......@@ -2860,7 +2873,6 @@ fn slashing_nominators_by_span_max() {
}],
&[Perbill::from_percent(30)],
3,
DisableStrategy::WhenSlashed,
);
// 11 was not further slashed, but 21 and 101 were.
......@@ -2882,7 +2894,6 @@ fn slashing_nominators_by_span_max() {
}],
&[Perbill::from_percent(20)],
2,
DisableStrategy::WhenSlashed,
);
// 11 was further slashed, but 21 and 101 were not.
......@@ -2999,11 +3010,8 @@ fn deferred_slashes_are_deferred() {
assert!(matches!(
staking_events_since_last_call().as_slice(),
&[
Event::Chilled { stash: 11 },
Event::ForceEra { mode: Forcing::ForceNew },
Event::SlashReported { validator: 11, slash_era: 1, .. },
Event::StakersElected,
Event::ForceEra { mode: Forcing::NotForcing },
..,
Event::Slashed { staker: 11, amount: 100 },
Event::Slashed { staker: 101, amount: 12 }
......@@ -3029,7 +3037,6 @@ fn retroactive_deferred_slashes_two_eras_before() {
&[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }],
&[Perbill::from_percent(10)],
1, // should be deferred for two full eras, and applied at the beginning of era 4.
DisableStrategy::Never,
);
mock::start_active_era(4);
......@@ -3037,8 +3044,6 @@ fn retroactive_deferred_slashes_two_eras_before() {
assert!(matches!(
staking_events_since_last_call().as_slice(),
&[
Event::Chilled { stash: 11 },
Event::ForceEra { mode: Forcing::ForceNew },
Event::SlashReported { validator: 11, slash_era: 1, .. },
..,
Event::Slashed { staker: 11, amount: 100 },
......@@ -3067,7 +3072,6 @@ fn retroactive_deferred_slashes_one_before() {
&[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }],
&[Perbill::from_percent(10)],
2, // should be deferred for two full eras, and applied at the beginning of era 5.
DisableStrategy::Never,
);
mock::start_active_era(4);
......@@ -3197,7 +3201,6 @@ fn remove_deferred() {
&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
&[Perbill::from_percent(15)],
1,
DisableStrategy::WhenSlashed,
);
// fails if empty
......@@ -3312,192 +3315,198 @@ fn remove_multi_deferred() {
#[test]
fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_validator() {
ExtBuilder::default().build_and_execute(|| {
mock::start_active_era(1);
assert_eq_uvec!(Session::validators(), vec![11, 21]);
// pre-slash balance
assert_eq!(Balances::free_balance(11), 1000);
assert_eq!(Balances::free_balance(101), 2000);
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);
assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]);
// 100 has approval for 11 as of now
assert!(Staking::nominators(101).unwrap().targets.contains(&11));
// pre-slash balance
assert_eq!(Balances::free_balance(11), 1000);
assert_eq!(Balances::free_balance(101), 2000);
// 11 and 21 both have the support of 100
let exposure_11 = Staking::eras_stakers(active_era(), &11);
let exposure_21 = Staking::eras_stakers(active_era(), &21);
// 100 has approval for 11 as of now
assert!(Staking::nominators(101).unwrap().targets.contains(&11));
assert_eq!(exposure_11.total, 1000 + 125);
assert_eq!(exposure_21.total, 1000 + 375);
// 11 and 21 both have the support of 100
let exposure_11 = Staking::eras_stakers(active_era(), &11);
let exposure_21 = Staking::eras_stakers(active_era(), &21);
on_offence_now(
&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
&[Perbill::from_percent(10)],
);
assert_eq!(exposure_11.total, 1000 + 125);
assert_eq!(exposure_21.total, 1000 + 375);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::StakersElected,
Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
Event::Chilled { stash: 11 },
Event::ForceEra { mode: Forcing::ForceNew },
Event::SlashReported {
validator: 11,
fraction: Perbill::from_percent(10),
slash_era: 1
},
Event::Slashed { staker: 11, amount: 100 },
Event::Slashed { staker: 101, amount: 12 },
]
);
on_offence_now(
&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
&[Perbill::from_percent(10)],
);
// post-slash balance
let nominator_slash_amount_11 = 125 / 10;
assert_eq!(Balances::free_balance(11), 900);
assert_eq!(Balances::free_balance(101), 2000 - nominator_slash_amount_11);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::StakersElected,
Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
Event::SlashReported {
validator: 11,
fraction: Perbill::from_percent(10),
slash_era: 1
},
Event::Slashed { staker: 11, amount: 100 },
Event::Slashed { staker: 101, amount: 12 },
]
);
// check that validator was chilled.
assert!(Validators::<Test>::iter().all(|(stash, _)| stash != 11));
// post-slash balance
let nominator_slash_amount_11 = 125 / 10;
assert_eq!(Balances::free_balance(11), 900);
assert_eq!(Balances::free_balance(101), 2000 - nominator_slash_amount_11);
// actually re-bond the slashed validator
assert_ok!(Staking::validate(RuntimeOrigin::signed(11), Default::default()));
// check that validator was disabled.
assert!(is_disabled(11));
mock::start_active_era(2);
let exposure_11 = Staking::eras_stakers(active_era(), &11);
let exposure_21 = Staking::eras_stakers(active_era(), &21);
// actually re-bond the slashed validator
assert_ok!(Staking::validate(RuntimeOrigin::signed(11), Default::default()));
// 11's own expo is reduced. sum of support from 11 is less (448), which is 500
// 900 + 146
assert!(matches!(exposure_11, Exposure { own: 900, total: 1046, .. }));
// 1000 + 342
assert!(matches!(exposure_21, Exposure { own: 1000, total: 1342, .. }));
assert_eq!(500 - 146 - 342, nominator_slash_amount_11);
});
mock::start_active_era(2);
let exposure_11 = Staking::eras_stakers(active_era(), &11);
let exposure_21 = Staking::eras_stakers(active_era(), &21);
// 11's own expo is reduced. sum of support from 11 is less (448), which is 500
// 900 + 146
assert!(matches!(exposure_11, Exposure { own: 900, total: 1046, .. }));
// 1000 + 342
assert!(matches!(exposure_21, Exposure { own: 1000, total: 1342, .. }));
assert_eq!(500 - 146 - 342, nominator_slash_amount_11);
});
}
#[test]
fn non_slashable_offence_doesnt_disable_validator() {
ExtBuilder::default().build_and_execute(|| {
mock::start_active_era(1);
assert_eq_uvec!(Session::validators(), vec![11, 21]);
fn non_slashable_offence_disables_validator() {
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);
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_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11);
let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
// offence with no slash associated
on_offence_now(
&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
&[Perbill::zero()],
);
// offence with no slash associated
on_offence_now(
&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
&[Perbill::zero()],
);
// it does NOT affect the nominator.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
// it does NOT affect the nominator.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
// offence that slashes 25% of the bond
on_offence_now(
&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
&[Perbill::from_percent(25)],
);
// offence that slashes 25% of the bond
on_offence_now(
&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
&[Perbill::from_percent(25)],
);
// it DOES NOT affect the nominator.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
// it DOES NOT affect the nominator.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::StakersElected,
Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
Event::Chilled { stash: 11 },
Event::ForceEra { mode: Forcing::ForceNew },
Event::SlashReported {
validator: 11,
fraction: Perbill::from_percent(0),
slash_era: 1
},
Event::Chilled { stash: 21 },
Event::SlashReported {
validator: 21,
fraction: Perbill::from_percent(25),
slash_era: 1
},
Event::Slashed { staker: 21, amount: 250 },
Event::Slashed { staker: 101, amount: 94 }
]
);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::StakersElected,
Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
Event::SlashReported {
validator: 11,
fraction: Perbill::from_percent(0),
slash_era: 1
},
Event::SlashReported {
validator: 21,
fraction: Perbill::from_percent(25),
slash_era: 1
},
Event::Slashed { staker: 21, amount: 250 },
Event::Slashed { staker: 101, amount: 94 }
]
);
// the offence for validator 10 wasn't slashable so it wasn't disabled
assert!(!is_disabled(11));
// whereas validator 20 gets disabled
assert!(is_disabled(21));
});
// the offence for validator 11 wasn't slashable but it is disabled
assert!(is_disabled(11));
// validator 21 gets disabled too
assert!(is_disabled(21));
});
}
#[test]
fn slashing_independent_of_disabling_validator() {
ExtBuilder::default().build_and_execute(|| {
mock::start_active_era(1);
assert_eq_uvec!(Session::validators(), vec![11, 21]);
ExtBuilder::default()
.validator_count(5)
.set_status(41, StakerStatus::Validator)
.set_status(51, StakerStatus::Validator)
.build_and_execute(|| {
mock::start_active_era(1);
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_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
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 now = Staking::active_era().unwrap().index;
let now = Staking::active_era().unwrap().index;
// offence with no slash associated, BUT disabling
on_offence_in_era(
&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
&[Perbill::zero()],
now,
DisableStrategy::Always,
);
// offence with no slash associated
on_offence_in_era(
&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
&[Perbill::zero()],
now,
);
// nomination remains untouched.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
// nomination remains untouched.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
// offence that slashes 25% of the bond, BUT not disabling
on_offence_in_era(
&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
&[Perbill::from_percent(25)],
now,
DisableStrategy::Never,
);
// offence that slashes 25% of the bond
on_offence_in_era(
&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
&[Perbill::from_percent(25)],
now,
);
// nomination remains untouched.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
// nomination remains untouched.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::StakersElected,
Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
Event::Chilled { stash: 11 },
Event::ForceEra { mode: Forcing::ForceNew },
Event::SlashReported {
validator: 11,
fraction: Perbill::from_percent(0),
slash_era: 1
},
Event::Chilled { stash: 21 },
Event::SlashReported {
validator: 21,
fraction: Perbill::from_percent(25),
slash_era: 1
},
Event::Slashed { staker: 21, amount: 250 },
Event::Slashed { staker: 101, amount: 94 }
]
);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::StakersElected,
Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
Event::SlashReported {
validator: 11,
fraction: Perbill::from_percent(0),
slash_era: 1
},
Event::SlashReported {
validator: 21,
fraction: Perbill::from_percent(25),
slash_era: 1
},
Event::Slashed { staker: 21, amount: 250 },
Event::Slashed { staker: 101, amount: 94 }
]
);
// the offence for validator 10 was explicitly disabled
assert!(is_disabled(11));
// whereas validator 21 is explicitly not disabled
assert!(!is_disabled(21));
});
// first validator is disabled but not slashed
assert!(is_disabled(11));
// second validator is slashed but not disabled
assert!(!is_disabled(21));
});
}
#[test]
fn offence_threshold_triggers_new_era() {
fn offence_threshold_doesnt_trigger_new_era() {
ExtBuilder::default()
.validator_count(4)
.set_status(41, StakerStatus::Validator)
......@@ -3506,12 +3515,14 @@ fn offence_threshold_triggers_new_era() {
assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]);
assert_eq!(
<Test as Config>::OffendingValidatorsThreshold::get(),
Perbill::from_percent(75),
UpToLimitDisablingStrategy::<DISABLING_LIMIT_FACTOR>::disable_limit(
Session::validators().len()
),
1
);
// we have 4 validators and an offending validator threshold of 75%,
// once the third validator commits an offence a new era should be forced
// we have 4 validators and an offending validator threshold of 1/3,
// 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_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
......@@ -3522,6 +3533,9 @@ fn offence_threshold_triggers_new_era() {
&[Perbill::zero()],
);
// 11 should be disabled because the byzantine threshold is 1
assert!(is_disabled(11));
assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
on_offence_now(
......@@ -3529,6 +3543,10 @@ fn offence_threshold_triggers_new_era() {
&[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);
on_offence_now(
......@@ -3536,28 +3554,29 @@ fn offence_threshold_triggers_new_era() {
&[Perbill::zero()],
);
assert_eq!(ForceEra::<Test>::get(), Forcing::ForceNew);
// same for 31
assert!(!is_disabled(31));
assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
});
}
#[test]
fn disabled_validators_are_kept_disabled_for_whole_era() {
ExtBuilder::default()
.validator_count(4)
.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);
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);
let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11);
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(
&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
&[Perbill::from_percent(25)],
......@@ -3566,18 +3585,15 @@ fn disabled_validators_are_kept_disabled_for_whole_era() {
// nominations are not updated.
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
assert!(is_disabled(21));
advance_session();
// disabled validators should carry-on through all sessions in the era
assert!(!is_disabled(11));
assert!(is_disabled(21));
// validator 11 should now get disabled
// validator 11 commits an offence
on_offence_now(
&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
&[Perbill::from_percent(25)],
......@@ -3687,27 +3703,34 @@ fn claim_reward_at_the_last_era_and_no_double_claim_and_invalid_claim() {
#[test]
fn zero_slash_keeps_nominators() {
ExtBuilder::default().build_and_execute(|| {
mock::start_active_era(1);
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);
assert_eq!(Balances::free_balance(11), 1000);
assert_eq!(Balances::free_balance(11), 1000);
let exposure = Staking::eras_stakers(active_era(), &11);
assert_eq!(Balances::free_balance(101), 2000);
let exposure = Staking::eras_stakers(active_era(), &11);
assert_eq!(Balances::free_balance(101), 2000);
on_offence_now(
&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
&[Perbill::from_percent(0)],
);
on_offence_now(
&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
&[Perbill::from_percent(0)],
);
assert_eq!(Balances::free_balance(11), 1000);
assert_eq!(Balances::free_balance(101), 2000);
assert_eq!(Balances::free_balance(11), 1000);
assert_eq!(Balances::free_balance(101), 2000);
// 11 is still removed..
assert!(Validators::<Test>::iter().all(|(stash, _)| stash != 11));
// but their nominations are kept.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
});
// 11 is not removed but disabled
assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
assert!(is_disabled(11));
// and their nominations are kept.
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
});
}
#[test]
......@@ -4710,7 +4733,7 @@ fn offences_weight_calculated_correctly() {
let zero_offence_weight =
<Test as frame_system::Config>::DbWeight::get().reads_writes(4, 1);
assert_eq!(
Staking::on_offence(&[], &[Perbill::from_percent(50)], 0, DisableStrategy::WhenSlashed),
Staking::on_offence(&[], &[Perbill::from_percent(50)], 0),
zero_offence_weight
);
......@@ -4735,7 +4758,6 @@ fn offences_weight_calculated_correctly() {
&offenders,
&[Perbill::from_percent(50)],
0,
DisableStrategy::WhenSlashed
),
n_offence_unapplied_weight
);
......@@ -4765,7 +4787,6 @@ fn offences_weight_calculated_correctly() {
&one_offender,
&[Perbill::from_percent(50)],
0,
DisableStrategy::WhenSlashed{}
),
one_offence_unapplied_weight
);
......@@ -7011,62 +7032,71 @@ mod staking_unchecked {
#[test]
fn virtual_nominators_are_lazily_slashed() {
ExtBuilder::default().build_and_execute(|| {
mock::start_active_era(1);
let slash_percent = Perbill::from_percent(5);
let initial_exposure = Staking::eras_stakers(active_era(), &11);
// 101 is a nominator for 11
assert_eq!(initial_exposure.others.first().unwrap().who, 101);
// make 101 a virtual nominator
<Staking as StakingUnchecked>::migrate_to_virtual_staker(&101);
// set payee different to self.
assert_ok!(<Staking as StakingInterface>::update_payee(&101, &102));
// cache values
let nominator_stake = Staking::ledger(101.into()).unwrap().active;
let nominator_balance = balances(&101).0;
let validator_stake = Staking::ledger(11.into()).unwrap().active;
let validator_balance = balances(&11).0;
let exposed_stake = initial_exposure.total;
let exposed_validator = initial_exposure.own;
let exposed_nominator = initial_exposure.others.first().unwrap().value;
// 11 goes offline
on_offence_now(
&[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }],
&[slash_percent],
);
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);
let slash_percent = Perbill::from_percent(5);
let initial_exposure = Staking::eras_stakers(active_era(), &11);
// 101 is a nominator for 11
assert_eq!(initial_exposure.others.first().unwrap().who, 101);
// make 101 a virtual nominator
<Staking as StakingUnchecked>::migrate_to_virtual_staker(&101);
// set payee different to self.
assert_ok!(<Staking as StakingInterface>::update_payee(&101, &102));
// cache values
let nominator_stake = Staking::ledger(101.into()).unwrap().active;
let nominator_balance = balances(&101).0;
let validator_stake = Staking::ledger(11.into()).unwrap().active;
let validator_balance = balances(&11).0;
let exposed_stake = initial_exposure.total;
let exposed_validator = initial_exposure.own;
let exposed_nominator = initial_exposure.others.first().unwrap().value;
// 11 goes offline
on_offence_now(
&[OffenceDetails {
offender: (11, initial_exposure.clone()),
reporters: vec![],
}],
&[slash_percent],
);
let slash_amount = slash_percent * exposed_stake;
let validator_share =
Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount;
let nominator_share =
Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount;
let slash_amount = slash_percent * exposed_stake;
let validator_share =
Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount;
let nominator_share =
Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount;
// both slash amounts need to be positive for the test to make sense.
assert!(validator_share > 0);
assert!(nominator_share > 0);
// both slash amounts need to be positive for the test to make sense.
assert!(validator_share > 0);
assert!(nominator_share > 0);
// both stakes must have been decreased pro-rata.
assert_eq!(
Staking::ledger(101.into()).unwrap().active,
nominator_stake - nominator_share
);
assert_eq!(
Staking::ledger(11.into()).unwrap().active,
validator_stake - validator_share
);
// both stakes must have been decreased pro-rata.
assert_eq!(
Staking::ledger(101.into()).unwrap().active,
nominator_stake - nominator_share
);
assert_eq!(
Staking::ledger(11.into()).unwrap().active,
validator_stake - validator_share
);
// validator balance is slashed as usual
assert_eq!(balances(&11).0, validator_balance - validator_share);
// Because slashing happened.
assert!(is_disabled(11));
// validator balance is slashed as usual
assert_eq!(balances(&11).0, validator_balance - validator_share);
// Because slashing happened.
assert!(is_disabled(11));
// but virtual nominator's balance is not slashed.
assert_eq!(Balances::free_balance(&101), nominator_balance);
// but slash is broadcasted to slash observers.
assert_eq!(SlashObserver::get().get(&101).unwrap(), &nominator_share);
})
// but virtual nominator's balance is not slashed.
assert_eq!(Balances::free_balance(&101), nominator_balance);
// but slash is broadcasted to slash observers.
assert_eq!(SlashObserver::get().get(&101).unwrap(), &nominator_share);
})
}
}
mod ledger {
......@@ -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(
// Prevent UncheckedExtrinsic to print unused warning.
const _: () = {
#[allow(unused)]
type __hidden_use_of_unchecked_extrinsic = #unchecked_extrinsic;
type __HiddenUseOfUncheckedExtrinsic = #unchecked_extrinsic;
};
#[derive(
......
......@@ -2465,6 +2465,9 @@ pub mod pallet_macros {
/// 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`.
///
/// When submitted as unsigned transactions (for example via an off-chain workder), note
/// that the tasks will be executed in a random order.
///
/// ## Example
#[doc = docify::embed!("src/tests/tasks.rs", tasks_example)]
/// Now, this can be executed as follows:
......
......@@ -46,6 +46,8 @@ pub enum ProcessMessageError {
/// 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.
Yield,
/// The message could not be processed for reaching the stack depth limit.
StackLimitReached,
}
/// Can process messages from a specific origin.
......@@ -96,6 +98,8 @@ pub trait ServiceQueues {
/// - `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`.
/// 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;
/// 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 {
fn iter() -> Self::Enumeration;
/// 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;
/// Performs the work for this particular `Task` variant.
......
......@@ -161,22 +161,31 @@ impl<BlockNumber: Clone + sp_std::fmt::Debug + AtLeast32BitUnsigned> TryState<Bl
match targets {
Select::None => Ok(()),
Select::All => {
let mut error_count = 0;
let mut errors = Vec::<TryRuntimeError>::new();
for_tuples!(#(
if let Err(_) = Tuple::try_state(n.clone(), targets.clone()) {
error_count += 1;
if let Err(err) = Tuple::try_state(n.clone(), targets.clone()) {
errors.push(err);
}
)*);
if error_count > 0 {
if !errors.is_empty() {
log::error!(
target: "try-runtime",
"{} pallets exited with errors while executing try_state checks.",
error_count
"Detected errors while executing `try_state`:",
);
errors.iter().for_each(|err| {
log::error!(
target: "try-runtime",
"{:?}",
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(),
)
}
......
......@@ -741,9 +741,7 @@ pub mod pallet {
#[cfg(feature = "experimental")]
#[pallet::call_index(8)]
#[pallet::weight(task.weight())]
pub fn do_task(origin: OriginFor<T>, task: T::RuntimeTask) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;
pub fn do_task(_origin: OriginFor<T>, task: T::RuntimeTask) -> DispatchResultWithPostInfo {
if !task.is_valid() {
return Err(Error::<T>::InvalidTask.into())
}
......@@ -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())
}
}
......
......@@ -19,8 +19,30 @@ use alloc::{vec, vec::Vec};
use codec::{Decode, Encode, Error, Input};
use core::cmp;
use scale_info::TypeInfo;
use sp_application_crypto::RuntimeAppPublic;
use sp_runtime::traits::Hash;
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,
}
use crate::{Payload, ValidatorSetId};
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.
///
......@@ -113,9 +135,49 @@ impl<TBlockNumber: core::fmt::Debug, TSignature> core::fmt::Display
impl<TBlockNumber, TSignature> SignedCommitment<TBlockNumber, TSignature> {
/// 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()
}
/// 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
......@@ -439,13 +501,13 @@ mod tests {
commitment,
signatures: vec![None, None, Some(sigs.0), Some(sigs.1)],
};
assert_eq!(signed.no_of_signatures(), 2);
assert_eq!(signed.signature_count(), 2);
// when
signed.signatures[2] = None;
// then
assert_eq!(signed.no_of_signatures(), 1);
assert_eq!(signed.signature_count(), 1);
}
#[test]
......
......@@ -43,7 +43,7 @@ pub mod witness;
#[cfg(feature = "std")]
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};
use alloc::vec::Vec;
......
......@@ -182,7 +182,7 @@ impl From<MultiRemovalResults> for KillStorageResult {
pub trait Storage {
/// 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> {
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
......
......@@ -37,29 +37,6 @@ pub type Kind = [u8; 16];
/// so that we can slash it accordingly.
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.
///
/// This trait assumes that the offence is legitimate and was validated already.
......@@ -102,11 +79,6 @@ pub trait Offence<Offender> {
/// number. Note that for GRANDPA the round number is reset each epoch.
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
/// particular offence for the `offenders_count` that happened at a singular `TimeSlot`.
///
......@@ -177,15 +149,12 @@ pub trait OnOffenceHandler<Reporter, Offender, Res> {
///
/// 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
/// responsible for queuing the report and re-submitting again.
fn on_offence(
offenders: &[OffenceDetails<Reporter, Offender>],
slash_fraction: &[Perbill],
session: SessionIndex,
disable_strategy: DisableStrategy,
) -> Res;
}
......@@ -194,7 +163,6 @@ impl<Reporter, Offender, Res: Default> OnOffenceHandler<Reporter, Offender, Res>
_offenders: &[OffenceDetails<Reporter, Offender>],
_slash_fraction: &[Perbill],
_session: SessionIndex,
_disable_strategy: DisableStrategy,
) -> Res {
Default::default()
}
......
......@@ -830,19 +830,22 @@ where
child_prefix: StorageKey,
at: B::Hash,
) -> Result<Vec<StorageKey>, &'static str> {
// This is deprecated and will generate a warning which causes the CI to fail.
#[allow(warnings)]
let child_keys = substrate_rpc_client::ChildStateApi::storage_keys(
client,
PrefixedStorageKey::new(prefixed_top_key.as_ref().to_vec()),
child_prefix,
Some(at),
)
.await
.map_err(|e| {
error!(target: LOG_TARGET, "Error = {:?}", e);
"rpc child_get_keys failed."
})?;
let retry_strategy =
FixedInterval::new(Self::KEYS_PAGE_RETRY_INTERVAL).take(Self::MAX_RETRIES);
let get_child_keys_closure = || {
#[allow(deprecated)]
substrate_rpc_client::ChildStateApi::storage_keys(
client,
PrefixedStorageKey::new(prefixed_top_key.as_ref().to_vec()),
child_prefix.clone(),
Some(at),
)
};
let child_keys =
Retry::spawn(retry_strategy, get_child_keys_closure).await.map_err(|e| {
error!(target: LOG_TARGET, "Error = {:?}", e);
"rpc child_get_keys failed."
})?;
debug!(
target: LOG_TARGET,
......
......@@ -116,6 +116,39 @@ impl WasmBuilder {
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.
///
/// This adds `-Clink-arg=--export=__heap_base` to `RUST_FLAGS`.
......
......@@ -33,15 +33,9 @@
//! use substrate_wasm_builder::WasmBuilder;
//!
//! fn main() {
//! WasmBuilder::new()
//! // Tell the builder to build the project (crate) this `build.rs` is part of.
//! .with_current_project()
//! // 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()
//! // Builds the WASM binary using the recommended defaults.
//! // If you need more control, you can call `new` or `init_with_defaults`.
//! WasmBuilder::build_using_defaults();
//! }
//! ```
//!
......
......@@ -18,10 +18,6 @@
fn main() {
#[cfg(feature = "std")]
{
substrate_wasm_builder::WasmBuilder::new()
.with_current_project()
.export_heap_base()
.import_memory()
.build();
substrate_wasm_builder::WasmBuilder::build_using_defaults();
}
}
#[cfg(feature = "std")]
fn main() {
substrate_wasm_builder::WasmBuilder::new()
.with_current_project()
.export_heap_base()
.import_memory()
.build()
substrate_wasm_builder::WasmBuilder::build_using_defaults();
}
/// The wasm builder is deactivated when compiling
......
......@@ -26,6 +26,8 @@ parameter_types! {
pub const RelayLocation: Location = Location::parent();
pub const RelayNetwork: Option<NetworkId> = None;
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();
}
......