mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 10:31:03 +00:00
Implementation of the new validator disabling strategy (#2226)
Closes https://github.com/paritytech/polkadot-sdk/issues/1966, https://github.com/paritytech/polkadot-sdk/issues/1963 and https://github.com/paritytech/polkadot-sdk/issues/1962. Disabling strategy specification [here](https://github.com/paritytech/polkadot-sdk/pull/2955). (Updated 13/02/2024) Implements: * validator disabling for a whole era instead of just a session * no more than 1/3 of the validators in the active set are disabled Removes: * `DisableStrategy` enum - now each validator committing an offence is disabled. * New era is not forced if too many validators are disabled. Before this PR not all offenders were disabled. A decision was made based on [`enum DisableStrategy`](https://github.com/paritytech/polkadot-sdk/blob/bbb6631641f9adba30c0ee6f4d11023a424dd362/substrate/primitives/staking/src/offence.rs#L54). Some offenders were disabled for a whole era, some just for a session, some were not disabled at all. This PR changes the disabling behaviour. Now a validator committing an offense is disabled immediately till the end of the current era. Some implementation notes: * `OffendingValidators` in pallet session keeps all offenders (this is not changed). However its type is changed from `Vec<(u32, bool)>` to `Vec<u32>`. The reason is simple - each offender is getting disabled so the bool doesn't make sense anymore. * When a validator is disabled it is first added to `OffendingValidators` and then to `DisabledValidators`. This is done in [`add_offending_validator`](https://github.com/paritytech/polkadot-sdk/blob/bbb6631641f9adba30c0ee6f4d11023a424dd362/substrate/frame/staking/src/slashing.rs#L325) from staking pallet. * In [`rotate_session`](https://github.com/paritytech/polkadot-sdk/blob/bdbe98297032e21a553bf191c530690b1d591405/substrate/frame/session/src/lib.rs#L623) the `end_session` also calls [`end_era`](https://github.com/paritytech/polkadot-sdk/blob/bbb6631641f9adba30c0ee6f4d11023a424dd362/substrate/frame/staking/src/pallet/impls.rs#L490) when an era ends. In this case `OffendingValidators` are cleared **(1)**. * Then in [`rotate_session`](https://github.com/paritytech/polkadot-sdk/blob/bdbe98297032e21a553bf191c530690b1d591405/substrate/frame/session/src/lib.rs#L623) `DisabledValidators` are cleared **(2)** * And finally (still in `rotate_session`) a call to [`start_session`](https://github.com/paritytech/polkadot-sdk/blob/bbb6631641f9adba30c0ee6f4d11023a424dd362/substrate/frame/staking/src/pallet/impls.rs#L430) repopulates the disabled validators **(3)**. * The reason for this complication is that session pallet knows nothing abut eras. To overcome this on each new session the disabled list is repopulated (points 2 and 3). Staking pallet knows when a new era starts so with point 1 it ensures that the offenders list is cleared. --------- Co-authored-by: ordian <noreply@reusable.software> Co-authored-by: ordian <write@reusable.software> Co-authored-by: Maciej <maciej.zyszkiewicz@parity.io> Co-authored-by: Gonçalo Pestana <g6pestana@gmail.com> Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Co-authored-by: command-bot <> Co-authored-by: Ankan <10196091+Ank4n@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
97f7425338
commit
988e30f102
@@ -144,7 +144,6 @@ parameter_types! {
|
||||
pub const BondingDuration: EraIndex = 3;
|
||||
pub const SlashDeferDuration: EraIndex = 0;
|
||||
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
|
||||
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(16);
|
||||
pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build();
|
||||
}
|
||||
|
||||
@@ -174,7 +173,6 @@ impl pallet_staking::Config for Test {
|
||||
type UnixTime = pallet_timestamp::Pallet<Test>;
|
||||
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
|
||||
type MaxExposurePageSize = ConstU32<64>;
|
||||
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
|
||||
type NextNewSession = Session;
|
||||
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
|
||||
type GenesisElectionProvider = Self::ElectionProvider;
|
||||
@@ -187,6 +185,7 @@ impl pallet_staking::Config for Test {
|
||||
type EventListeners = ();
|
||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||
type WeightInfo = ();
|
||||
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
|
||||
}
|
||||
|
||||
impl pallet_offences::Config for Test {
|
||||
|
||||
@@ -158,7 +158,6 @@ parameter_types! {
|
||||
pub const SessionsPerEra: SessionIndex = 3;
|
||||
pub const BondingDuration: EraIndex = 3;
|
||||
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
|
||||
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
|
||||
pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build();
|
||||
}
|
||||
|
||||
@@ -188,7 +187,6 @@ impl pallet_staking::Config for Test {
|
||||
type UnixTime = pallet_timestamp::Pallet<Test>;
|
||||
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
|
||||
type MaxExposurePageSize = ConstU32<64>;
|
||||
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
|
||||
type NextNewSession = Session;
|
||||
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
|
||||
type GenesisElectionProvider = Self::ElectionProvider;
|
||||
@@ -201,6 +199,7 @@ impl pallet_staking::Config for Test {
|
||||
type EventListeners = ();
|
||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||
type WeightInfo = ();
|
||||
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
|
||||
}
|
||||
|
||||
impl pallet_offences::Config for Test {
|
||||
|
||||
@@ -23,7 +23,6 @@ pub(crate) const LOG_TARGET: &str = "tests::e2e-epm";
|
||||
use frame_support::{assert_err, assert_noop, assert_ok};
|
||||
use mock::*;
|
||||
use sp_core::Get;
|
||||
use sp_npos_elections::{to_supports, StakedAssignment};
|
||||
use sp_runtime::Perbill;
|
||||
|
||||
use crate::mock::RuntimeOrigin;
|
||||
@@ -127,75 +126,48 @@ fn offchainify_works() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Replicates the Kusama incident of 8th Dec 2022 and its resolution through the governance
|
||||
/// Inspired by the Kusama incident of 8th Dec 2022 and its resolution through the governance
|
||||
/// fallback.
|
||||
///
|
||||
/// After enough slashes exceeded the `Staking::OffendingValidatorsThreshold`, the staking pallet
|
||||
/// set `Forcing::ForceNew`. When a new session starts, staking will start to force a new era and
|
||||
/// calls <EPM as election_provider>::elect(). If at this point EPM and the staking miners did not
|
||||
/// have enough time to queue a new solution (snapshot + solution submission), the election request
|
||||
/// fails. If there is no election fallback mechanism in place, EPM enters in emergency mode.
|
||||
/// Recovery: Once EPM is in emergency mode, subsequent calls to `elect()` will fail until a new
|
||||
/// solution is added to EPM's `QueuedSolution` queue. This can be achieved through
|
||||
/// `Call::set_emergency_election_result` or `Call::governance_fallback` dispatchables. Once a new
|
||||
/// solution is added to the queue, EPM phase transitions to `Phase::Off` and the election flow
|
||||
/// restarts. Note that in this test case, the emergency throttling is disabled.
|
||||
fn enters_emergency_phase_after_forcing_before_elect() {
|
||||
/// Mass slash of validators shouldn't disable more than 1/3 of them (the byzantine threshold). Also
|
||||
/// no new era should be forced which could lead to EPM entering emergency mode.
|
||||
fn mass_slash_doesnt_enter_emergency_phase() {
|
||||
let epm_builder = EpmExtBuilder::default().disable_emergency_throttling();
|
||||
let (ext, pool_state, _) = ExtBuilder::default().epm(epm_builder).build_offchainify();
|
||||
let staking_builder = StakingExtBuilder::default().validator_count(7);
|
||||
let (mut ext, _, _) = ExtBuilder::default()
|
||||
.epm(epm_builder)
|
||||
.staking(staking_builder)
|
||||
.build_offchainify();
|
||||
|
||||
execute_with(ext, || {
|
||||
log!(
|
||||
trace,
|
||||
"current validators (staking): {:?}",
|
||||
<Runtime as pallet_staking::SessionInterface<AccountId>>::validators()
|
||||
ext.execute_with(|| {
|
||||
assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::NotForcing);
|
||||
|
||||
let active_set_size_before_slash = Session::validators().len();
|
||||
|
||||
// Slash more than 1/3 of the active validators
|
||||
let mut slashed = slash_half_the_active_set();
|
||||
|
||||
let active_set_size_after_slash = Session::validators().len();
|
||||
|
||||
// active set should stay the same before and after the slash
|
||||
assert_eq!(active_set_size_before_slash, active_set_size_after_slash);
|
||||
|
||||
// Slashed validators are disabled up to a limit
|
||||
slashed.truncate(
|
||||
pallet_staking::UpToLimitDisablingStrategy::<SLASHING_DISABLING_FACTOR>::disable_limit(
|
||||
active_set_size_after_slash,
|
||||
),
|
||||
);
|
||||
let session_validators_before = Session::validators();
|
||||
|
||||
roll_to_epm_off();
|
||||
assert!(ElectionProviderMultiPhase::current_phase().is_off());
|
||||
// Find the indices of the disabled validators
|
||||
let active_set = Session::validators();
|
||||
let expected_disabled = slashed
|
||||
.into_iter()
|
||||
.map(|d| active_set.iter().position(|a| *a == d).unwrap() as u32)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::NotForcing);
|
||||
// slashes so that staking goes into `Forcing::ForceNew`.
|
||||
slash_through_offending_threshold();
|
||||
|
||||
assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::ForceNew);
|
||||
|
||||
advance_session_delayed_solution(pool_state.clone());
|
||||
assert!(ElectionProviderMultiPhase::current_phase().is_emergency());
|
||||
log_current_time();
|
||||
|
||||
let era_before_delayed_next = Staking::current_era();
|
||||
// try to advance 2 eras.
|
||||
assert!(start_next_active_era_delayed_solution(pool_state.clone()).is_ok());
|
||||
assert_eq!(Staking::current_era(), era_before_delayed_next);
|
||||
assert!(start_next_active_era(pool_state).is_err());
|
||||
assert_eq!(Staking::current_era(), era_before_delayed_next);
|
||||
|
||||
// EPM is still in emergency phase.
|
||||
assert!(ElectionProviderMultiPhase::current_phase().is_emergency());
|
||||
|
||||
// session validator set remains the same.
|
||||
assert_eq!(Session::validators(), session_validators_before);
|
||||
|
||||
// performs recovery through the set emergency result.
|
||||
let supports = to_supports(&vec![
|
||||
StakedAssignment { who: 21, distribution: vec![(21, 10)] },
|
||||
StakedAssignment { who: 31, distribution: vec![(21, 10), (31, 10)] },
|
||||
StakedAssignment { who: 41, distribution: vec![(41, 10)] },
|
||||
]);
|
||||
assert!(ElectionProviderMultiPhase::set_emergency_election_result(
|
||||
RuntimeOrigin::root(),
|
||||
supports
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
// EPM can now roll to signed phase to proceed with elections. The validator set is the
|
||||
// expected (ie. set through `set_emergency_election_result`).
|
||||
roll_to_epm_signed();
|
||||
//assert!(ElectionProviderMultiPhase::current_phase().is_signed());
|
||||
assert_eq!(Session::validators(), vec![21, 31, 41]);
|
||||
assert_eq!(Staking::current_era(), era_before_delayed_next.map(|e| e + 1));
|
||||
assert_eq!(Session::disabled_validators(), expected_disabled);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -253,77 +225,7 @@ fn continuous_slashes_below_offending_threshold() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Slashed validator sets intentions in the same era of slashing.
|
||||
///
|
||||
/// When validators are slashed, they are chilled and removed from the current `VoterList`. Thus,
|
||||
/// the slashed validator should not be considered in the next validator set. However, if the
|
||||
/// slashed validator sets its intention to validate again in the same era when it was slashed and
|
||||
/// chilled, the validator may not be removed from the active validator set across eras, provided
|
||||
/// it would selected in the subsequent era if there was no slash. Nominators of the slashed
|
||||
/// validator will also be slashed and chilled, as expected, but the nomination intentions will
|
||||
/// remain after the validator re-set the intention to be validating again.
|
||||
///
|
||||
/// This behaviour is due to removing implicit chill upon slash
|
||||
/// <https://github.com/paritytech/substrate/pull/12420>.
|
||||
///
|
||||
/// Related to <https://github.com/paritytech/substrate/issues/13714>.
|
||||
fn set_validation_intention_after_chilled() {
|
||||
use frame_election_provider_support::SortedListProvider;
|
||||
use pallet_staking::{Event, Forcing, Nominators};
|
||||
|
||||
let (ext, pool_state, _) = ExtBuilder::default()
|
||||
.epm(EpmExtBuilder::default())
|
||||
.staking(StakingExtBuilder::default())
|
||||
.build_offchainify();
|
||||
|
||||
execute_with(ext, || {
|
||||
assert_eq!(active_era(), 0);
|
||||
// validator is part of the validator set.
|
||||
assert!(Session::validators().contains(&41));
|
||||
assert!(<Runtime as pallet_staking::Config>::VoterList::contains(&41));
|
||||
|
||||
// nominate validator 81.
|
||||
assert_ok!(Staking::nominate(RuntimeOrigin::signed(21), vec![41]));
|
||||
assert_eq!(Nominators::<Runtime>::get(21).unwrap().targets, vec![41]);
|
||||
|
||||
// validator is slashed. it is removed from the `VoterList` through chilling but in the
|
||||
// current era, the validator is still part of the active validator set.
|
||||
add_slash(&41);
|
||||
assert!(Session::validators().contains(&41));
|
||||
assert!(!<Runtime as pallet_staking::Config>::VoterList::contains(&41));
|
||||
assert_eq!(
|
||||
staking_events(),
|
||||
[
|
||||
Event::Chilled { stash: 41 },
|
||||
Event::ForceEra { mode: Forcing::ForceNew },
|
||||
Event::SlashReported {
|
||||
validator: 41,
|
||||
slash_era: 0,
|
||||
fraction: Perbill::from_percent(10)
|
||||
}
|
||||
],
|
||||
);
|
||||
|
||||
// after the nominator is slashed and chilled, the nominations remain.
|
||||
assert_eq!(Nominators::<Runtime>::get(21).unwrap().targets, vec![41]);
|
||||
|
||||
// validator sets intention to stake again in the same era it was chilled.
|
||||
assert_ok!(Staking::validate(RuntimeOrigin::signed(41), Default::default()));
|
||||
|
||||
// progress era and check that the slashed validator is still part of the validator
|
||||
// set.
|
||||
assert!(start_next_active_era(pool_state).is_ok());
|
||||
assert_eq!(active_era(), 1);
|
||||
assert!(Session::validators().contains(&41));
|
||||
assert!(<Runtime as pallet_staking::Config>::VoterList::contains(&41));
|
||||
|
||||
// nominations are still active as before the slash.
|
||||
assert_eq!(Nominators::<Runtime>::get(21).unwrap().targets, vec![41]);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Active ledger balance may fall below ED if account chills before unbonding.
|
||||
/// Active ledger balance may fall below ED if account chills before unbounding.
|
||||
///
|
||||
/// Unbonding call fails if the remaining ledger's stash balance falls below the existential
|
||||
/// deposit. However, if the stash is chilled before unbonding, the ledger's active balance may
|
||||
|
||||
@@ -35,7 +35,7 @@ use sp_runtime::{
|
||||
transaction_validity, BuildStorage, PerU16, Perbill, Percent,
|
||||
};
|
||||
use sp_staking::{
|
||||
offence::{DisableStrategy, OffenceDetails, OnOffenceHandler},
|
||||
offence::{OffenceDetails, OnOffenceHandler},
|
||||
EraIndex, SessionIndex,
|
||||
};
|
||||
use sp_std::prelude::*;
|
||||
@@ -236,7 +236,6 @@ parameter_types! {
|
||||
pub const SessionsPerEra: sp_staking::SessionIndex = 2;
|
||||
pub static BondingDuration: sp_staking::EraIndex = 28;
|
||||
pub const SlashDeferDuration: sp_staking::EraIndex = 7; // 1/4 the bonding duration.
|
||||
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(40);
|
||||
pub HistoryDepth: u32 = 84;
|
||||
}
|
||||
|
||||
@@ -290,6 +289,8 @@ parameter_types! {
|
||||
|
||||
/// Upper limit on the number of NPOS nominations.
|
||||
const MAX_QUOTA_NOMINATIONS: u32 = 16;
|
||||
/// Disabling factor set explicitly to byzantine threshold
|
||||
pub(crate) const SLASHING_DISABLING_FACTOR: usize = 3;
|
||||
|
||||
impl pallet_staking::Config for Runtime {
|
||||
type Currency = Balances;
|
||||
@@ -308,7 +309,6 @@ impl pallet_staking::Config for Runtime {
|
||||
type EraPayout = ();
|
||||
type NextNewSession = Session;
|
||||
type MaxExposurePageSize = ConstU32<256>;
|
||||
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
|
||||
type ElectionProvider = ElectionProviderMultiPhase;
|
||||
type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
|
||||
type VoterList = BagsList;
|
||||
@@ -320,6 +320,7 @@ impl pallet_staking::Config for Runtime {
|
||||
type EventListeners = Pools;
|
||||
type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
|
||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy<SLASHING_DISABLING_FACTOR>;
|
||||
}
|
||||
|
||||
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime
|
||||
@@ -871,7 +872,6 @@ pub(crate) fn on_offence_now(
|
||||
offenders,
|
||||
slash_fraction,
|
||||
Staking::eras_start_session_index(now).unwrap(),
|
||||
DisableStrategy::WhenSlashed,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -886,19 +886,16 @@ pub(crate) fn add_slash(who: &AccountId) {
|
||||
);
|
||||
}
|
||||
|
||||
// Slashes enough validators to cross the `Staking::OffendingValidatorsThreshold`.
|
||||
pub(crate) fn slash_through_offending_threshold() {
|
||||
let validators = Session::validators();
|
||||
let mut remaining_slashes =
|
||||
<Runtime as pallet_staking::Config>::OffendingValidatorsThreshold::get() *
|
||||
validators.len() as u32;
|
||||
// Slashes 1/2 of the active set. Returns the `AccountId`s of the slashed validators.
|
||||
pub(crate) fn slash_half_the_active_set() -> Vec<AccountId> {
|
||||
let mut slashed = Session::validators();
|
||||
slashed.truncate(slashed.len() / 2);
|
||||
|
||||
for v in validators.into_iter() {
|
||||
if remaining_slashes != 0 {
|
||||
add_slash(&v);
|
||||
remaining_slashes -= 1;
|
||||
}
|
||||
for v in slashed.iter() {
|
||||
add_slash(v);
|
||||
}
|
||||
|
||||
slashed
|
||||
}
|
||||
|
||||
// Slashes a percentage of the active nominators that haven't been slashed yet, with
|
||||
|
||||
@@ -134,7 +134,6 @@ impl pallet_staking::Config for Runtime {
|
||||
type NextNewSession = ();
|
||||
type HistoryDepth = ConstU32<84>;
|
||||
type MaxExposurePageSize = ConstU32<64>;
|
||||
type OffendingValidatorsThreshold = ();
|
||||
type ElectionProvider = MockElection;
|
||||
type GenesisElectionProvider = Self::ElectionProvider;
|
||||
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
|
||||
@@ -145,6 +144,7 @@ impl pallet_staking::Config for Runtime {
|
||||
type EventListeners = ();
|
||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||
type WeightInfo = ();
|
||||
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
|
||||
}
|
||||
|
||||
pub struct BalanceToU256;
|
||||
|
||||
@@ -146,7 +146,6 @@ parameter_types! {
|
||||
pub const SessionsPerEra: SessionIndex = 3;
|
||||
pub const BondingDuration: EraIndex = 3;
|
||||
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
|
||||
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
|
||||
pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build();
|
||||
}
|
||||
|
||||
@@ -176,7 +175,6 @@ impl pallet_staking::Config for Test {
|
||||
type UnixTime = pallet_timestamp::Pallet<Test>;
|
||||
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
|
||||
type MaxExposurePageSize = ConstU32<64>;
|
||||
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
|
||||
type NextNewSession = Session;
|
||||
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
|
||||
type GenesisElectionProvider = Self::ElectionProvider;
|
||||
@@ -189,6 +187,7 @@ impl pallet_staking::Config for Test {
|
||||
type EventListeners = ();
|
||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||
type WeightInfo = ();
|
||||
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
|
||||
}
|
||||
|
||||
impl pallet_offences::Config for Test {
|
||||
|
||||
@@ -104,7 +104,7 @@ use sp_runtime::{
|
||||
PerThing, Perbill, Permill, RuntimeDebug, SaturatedConversion,
|
||||
};
|
||||
use sp_staking::{
|
||||
offence::{DisableStrategy, Kind, Offence, ReportOffence},
|
||||
offence::{Kind, Offence, ReportOffence},
|
||||
SessionIndex,
|
||||
};
|
||||
use sp_std::prelude::*;
|
||||
@@ -847,10 +847,6 @@ impl<Offender: Clone> Offence<Offender> for UnresponsivenessOffence<Offender> {
|
||||
self.session_index
|
||||
}
|
||||
|
||||
fn disable_strategy(&self) -> DisableStrategy {
|
||||
DisableStrategy::Never
|
||||
}
|
||||
|
||||
fn slash_fraction(&self, offenders: u32) -> Perbill {
|
||||
// the formula is min((3 * (k - (n / 10 + 1))) / n, 1) * 0.07
|
||||
// basically, 10% can be offline with no slash, but after that, it linearly climbs up to 7%
|
||||
|
||||
@@ -50,9 +50,6 @@ fn test_unresponsiveness_slash_fraction() {
|
||||
dummy_offence.slash_fraction(17),
|
||||
Perbill::from_parts(46200000), // 4.62%
|
||||
);
|
||||
|
||||
// Offline offences should never lead to being disabled.
|
||||
assert_eq!(dummy_offence.disable_strategy(), DisableStrategy::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -111,7 +111,6 @@ impl pallet_staking::Config for Runtime {
|
||||
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
|
||||
type NextNewSession = ();
|
||||
type MaxExposurePageSize = ConstU32<64>;
|
||||
type OffendingValidatorsThreshold = ();
|
||||
type ElectionProvider =
|
||||
frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>;
|
||||
type GenesisElectionProvider = Self::ElectionProvider;
|
||||
@@ -124,6 +123,7 @@ impl pallet_staking::Config for Runtime {
|
||||
type EventListeners = Pools;
|
||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||
type WeightInfo = ();
|
||||
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
|
||||
@@ -125,7 +125,6 @@ impl pallet_staking::Config for Runtime {
|
||||
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
|
||||
type NextNewSession = ();
|
||||
type MaxExposurePageSize = ConstU32<64>;
|
||||
type OffendingValidatorsThreshold = ();
|
||||
type ElectionProvider =
|
||||
frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>;
|
||||
type GenesisElectionProvider = Self::ElectionProvider;
|
||||
@@ -138,6 +137,7 @@ impl pallet_staking::Config for Runtime {
|
||||
type EventListeners = Pools;
|
||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||
type WeightInfo = ();
|
||||
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
|
||||
@@ -174,7 +174,6 @@ impl pallet_staking::Config for Test {
|
||||
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
|
||||
type NextNewSession = Session;
|
||||
type MaxExposurePageSize = ConstU32<64>;
|
||||
type OffendingValidatorsThreshold = ();
|
||||
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
|
||||
type GenesisElectionProvider = Self::ElectionProvider;
|
||||
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
|
||||
@@ -186,6 +185,7 @@ impl pallet_staking::Config for Test {
|
||||
type EventListeners = ();
|
||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||
type WeightInfo = ();
|
||||
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
|
||||
}
|
||||
|
||||
impl pallet_im_online::Config for Test {
|
||||
|
||||
@@ -132,7 +132,6 @@ where
|
||||
&concurrent_offenders,
|
||||
&slash_perbill,
|
||||
offence.session_index(),
|
||||
offence.disable_strategy(),
|
||||
);
|
||||
|
||||
// Deposit the event.
|
||||
|
||||
@@ -23,7 +23,7 @@ use frame_support::{
|
||||
weights::Weight,
|
||||
Twox64Concat,
|
||||
};
|
||||
use sp_staking::offence::{DisableStrategy, OnOffenceHandler};
|
||||
use sp_staking::offence::OnOffenceHandler;
|
||||
use sp_std::vec::Vec;
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
@@ -106,12 +106,7 @@ pub fn remove_deferred_storage<T: Config>() -> Weight {
|
||||
let deferred = <DeferredOffences<T>>::take();
|
||||
log::info!(target: LOG_TARGET, "have {} deferred offences, applying.", deferred.len());
|
||||
for (offences, perbill, session) in deferred.iter() {
|
||||
let consumed = T::OnOffenceHandler::on_offence(
|
||||
offences,
|
||||
perbill,
|
||||
*session,
|
||||
DisableStrategy::WhenSlashed,
|
||||
);
|
||||
let consumed = T::OnOffenceHandler::on_offence(offences, perbill, *session);
|
||||
weight = weight.saturating_add(consumed);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ use sp_runtime::{
|
||||
BuildStorage, Perbill,
|
||||
};
|
||||
use sp_staking::{
|
||||
offence::{self, DisableStrategy, Kind, OffenceDetails},
|
||||
offence::{self, Kind, OffenceDetails},
|
||||
SessionIndex,
|
||||
};
|
||||
|
||||
@@ -51,7 +51,6 @@ impl<Reporter, Offender> offence::OnOffenceHandler<Reporter, Offender, Weight>
|
||||
_offenders: &[OffenceDetails<Reporter, Offender>],
|
||||
slash_fraction: &[Perbill],
|
||||
_offence_session: SessionIndex,
|
||||
_disable_strategy: DisableStrategy,
|
||||
) -> Weight {
|
||||
OnOffencePerbill::mutate(|f| {
|
||||
*f = slash_fraction.to_vec();
|
||||
|
||||
@@ -33,7 +33,7 @@ use alloc::vec::Vec;
|
||||
use pallet_session::historical::IdentificationTuple;
|
||||
use pallet_staking::{BalanceOf, Exposure, ExposureOf, Pallet as Staking};
|
||||
use sp_runtime::Perbill;
|
||||
use sp_staking::offence::{DisableStrategy, OnOffenceHandler};
|
||||
use sp_staking::offence::OnOffenceHandler;
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
@@ -128,7 +128,7 @@ pub mod pallet {
|
||||
T::AccountId,
|
||||
IdentificationTuple<T>,
|
||||
Weight,
|
||||
>>::on_offence(&offenders, &slash_fraction, session_index, DisableStrategy::WhenSlashed);
|
||||
>>::on_offence(&offenders, &slash_fraction, session_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,6 @@ parameter_types! {
|
||||
pub static SlashDeferDuration: EraIndex = 0;
|
||||
pub const BondingDuration: EraIndex = 3;
|
||||
pub static LedgerSlashPerEra: (BalanceOf<Test>, BTreeMap<EraIndex, BalanceOf<Test>>) = (Zero::zero(), BTreeMap::new());
|
||||
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(75);
|
||||
}
|
||||
|
||||
impl pallet_staking::Config for Test {
|
||||
@@ -153,7 +152,6 @@ impl pallet_staking::Config for Test {
|
||||
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
|
||||
type NextNewSession = Session;
|
||||
type MaxExposurePageSize = ConstU32<64>;
|
||||
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
|
||||
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
|
||||
type GenesisElectionProvider = Self::ElectionProvider;
|
||||
type TargetList = pallet_staking::UseValidatorsMap<Self>;
|
||||
@@ -165,6 +163,7 @@ impl pallet_staking::Config for Test {
|
||||
type EventListeners = ();
|
||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||
type WeightInfo = ();
|
||||
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
|
||||
}
|
||||
|
||||
impl pallet_session::historical::Config for Test {
|
||||
|
||||
@@ -174,7 +174,6 @@ impl pallet_staking::Config for Test {
|
||||
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
|
||||
type NextNewSession = Session;
|
||||
type MaxExposurePageSize = ConstU32<64>;
|
||||
type OffendingValidatorsThreshold = ();
|
||||
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
|
||||
type GenesisElectionProvider = Self::ElectionProvider;
|
||||
type MaxUnlockingChunks = ConstU32<32>;
|
||||
@@ -186,6 +185,7 @@ impl pallet_staking::Config for Test {
|
||||
type EventListeners = ();
|
||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||
type WeightInfo = ();
|
||||
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
|
||||
}
|
||||
|
||||
impl crate::Config for Test {}
|
||||
|
||||
@@ -627,7 +627,7 @@ impl<T: Config> Pallet<T> {
|
||||
Validators::<T>::put(&validators);
|
||||
|
||||
if changed {
|
||||
// reset disabled validators
|
||||
// reset disabled validators if active set was changed
|
||||
<DisabledValidators<T>>::take();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,25 @@ on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). We maintain a
|
||||
single integer version number for staking pallet to keep track of all storage
|
||||
migrations.
|
||||
|
||||
## [v15]
|
||||
|
||||
### Added
|
||||
|
||||
- New trait `DisablingStrategy` which is responsible for making a decision which offenders should be
|
||||
disabled on new offence.
|
||||
- Default implementation of `DisablingStrategy` - `UpToLimitDisablingStrategy`. It
|
||||
disables each new offender up to a threshold (1/3 by default). Offenders are not runtime disabled for
|
||||
offences in previous era(s). But they will be low-priority node-side disabled for dispute initiation.
|
||||
- `OffendingValidators` storage item is replaced with `DisabledValidators`. The former keeps all
|
||||
offenders and if they are disabled or not. The latter just keeps a list of all offenders as they
|
||||
are disabled by default.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- `enum DisableStrategy` is no longer needed because disabling is not related to the type of the
|
||||
offence anymore. A decision if a offender is disabled or not is made by a `DisablingStrategy`
|
||||
implementation.
|
||||
|
||||
## [v14]
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1239,3 +1239,79 @@ impl BenchmarkingConfig for TestBenchmarkingConfig {
|
||||
type MaxValidators = frame_support::traits::ConstU32<100>;
|
||||
type MaxNominators = frame_support::traits::ConstU32<100>;
|
||||
}
|
||||
|
||||
/// Controls validator disabling
|
||||
pub trait DisablingStrategy<T: Config> {
|
||||
/// Make a disabling decision. Returns the index of the validator to disable or `None` if no new
|
||||
/// validator should be disabled.
|
||||
fn decision(
|
||||
offender_stash: &T::AccountId,
|
||||
slash_era: EraIndex,
|
||||
currently_disabled: &Vec<u32>,
|
||||
) -> Option<u32>;
|
||||
}
|
||||
|
||||
/// Implementation of [`DisablingStrategy`] which disables validators from the active set up to a
|
||||
/// threshold. `DISABLING_LIMIT_FACTOR` is the factor of the maximum disabled validators in the
|
||||
/// active set. E.g. setting this value to `3` means no more than 1/3 of the validators in the
|
||||
/// active set can be disabled in an era.
|
||||
/// By default a factor of 3 is used which is the byzantine threshold.
|
||||
pub struct UpToLimitDisablingStrategy<const DISABLING_LIMIT_FACTOR: usize = 3>;
|
||||
|
||||
impl<const DISABLING_LIMIT_FACTOR: usize> UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR> {
|
||||
/// Disabling limit calculated from the total number of validators in the active set. When
|
||||
/// reached no more validators will be disabled.
|
||||
pub fn disable_limit(validators_len: usize) -> usize {
|
||||
validators_len
|
||||
.saturating_sub(1)
|
||||
.checked_div(DISABLING_LIMIT_FACTOR)
|
||||
.unwrap_or_else(|| {
|
||||
defensive!("DISABLING_LIMIT_FACTOR should not be 0");
|
||||
0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, const DISABLING_LIMIT_FACTOR: usize> DisablingStrategy<T>
|
||||
for UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR>
|
||||
{
|
||||
fn decision(
|
||||
offender_stash: &T::AccountId,
|
||||
slash_era: EraIndex,
|
||||
currently_disabled: &Vec<u32>,
|
||||
) -> Option<u32> {
|
||||
let active_set = T::SessionInterface::validators();
|
||||
|
||||
// We don't disable more than the limit
|
||||
if currently_disabled.len() >= Self::disable_limit(active_set.len()) {
|
||||
log!(
|
||||
debug,
|
||||
"Won't disable: reached disabling limit {:?}",
|
||||
Self::disable_limit(active_set.len())
|
||||
);
|
||||
return None
|
||||
}
|
||||
|
||||
// We don't disable for offences in previous eras
|
||||
if ActiveEra::<T>::get().map(|e| e.index).unwrap_or_default() > slash_era {
|
||||
log!(
|
||||
debug,
|
||||
"Won't disable: current_era {:?} > slash_era {:?}",
|
||||
Pallet::<T>::current_era().unwrap_or_default(),
|
||||
slash_era
|
||||
);
|
||||
return None
|
||||
}
|
||||
|
||||
let offender_idx = if let Some(idx) = active_set.iter().position(|i| i == offender_stash) {
|
||||
idx as u32
|
||||
} else {
|
||||
log!(debug, "Won't disable: offender not in active set",);
|
||||
return None
|
||||
};
|
||||
|
||||
log!(debug, "Will disable {:?}", offender_idx);
|
||||
|
||||
Some(offender_idx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
use super::*;
|
||||
use frame_election_provider_support::SortedListProvider;
|
||||
use frame_support::{
|
||||
migrations::VersionedMigration,
|
||||
pallet_prelude::ValueQuery,
|
||||
storage_alias,
|
||||
traits::{GetStorageVersion, OnRuntimeUpgrade},
|
||||
traits::{GetStorageVersion, OnRuntimeUpgrade, UncheckedOnRuntimeUpgrade},
|
||||
};
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
@@ -59,11 +60,61 @@ impl Default for ObsoleteReleases {
|
||||
#[storage_alias]
|
||||
type StorageVersion<T: Config> = StorageValue<Pallet<T>, ObsoleteReleases, ValueQuery>;
|
||||
|
||||
/// Migrating `OffendingValidators` from `Vec<(u32, bool)>` to `Vec<u32>`
|
||||
pub mod v15 {
|
||||
use super::*;
|
||||
|
||||
// The disabling strategy used by staking pallet
|
||||
type DefaultDisablingStrategy = UpToLimitDisablingStrategy;
|
||||
|
||||
pub struct VersionUncheckedMigrateV14ToV15<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV14ToV15<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let mut migrated = v14::OffendingValidators::<T>::take()
|
||||
.into_iter()
|
||||
.filter(|p| p.1) // take only disabled validators
|
||||
.map(|p| p.0)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Respect disabling limit
|
||||
migrated.truncate(DefaultDisablingStrategy::disable_limit(
|
||||
T::SessionInterface::validators().len(),
|
||||
));
|
||||
|
||||
DisabledValidators::<T>::set(migrated);
|
||||
|
||||
log!(info, "v15 applied successfully.");
|
||||
T::DbWeight::get().reads_writes(1, 1)
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
frame_support::ensure!(
|
||||
v14::OffendingValidators::<T>::decode_len().is_none(),
|
||||
"OffendingValidators is not empty after the migration"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub type MigrateV14ToV15<T> = VersionedMigration<
|
||||
14,
|
||||
15,
|
||||
VersionUncheckedMigrateV14ToV15<T>,
|
||||
Pallet<T>,
|
||||
<T as frame_system::Config>::DbWeight,
|
||||
>;
|
||||
}
|
||||
|
||||
/// Migration of era exposure storage items to paged exposures.
|
||||
/// Changelog: [v14.](https://github.com/paritytech/substrate/blob/ankan/paged-rewards-rebased2/frame/staking/CHANGELOG.md#14)
|
||||
pub mod v14 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type OffendingValidators<T: Config> =
|
||||
StorageValue<Pallet<T>, Vec<(u32, bool)>, ValueQuery>;
|
||||
|
||||
pub struct MigrateToV14<T>(core::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV14<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
@@ -73,10 +124,10 @@ pub mod v14 {
|
||||
if in_code == 14 && on_chain == 13 {
|
||||
in_code.put::<Pallet<T>>();
|
||||
|
||||
log!(info, "v14 applied successfully.");
|
||||
log!(info, "staking v14 applied successfully.");
|
||||
T::DbWeight::get().reads_writes(1, 1)
|
||||
} else {
|
||||
log!(warn, "v14 not applied.");
|
||||
log!(warn, "staking v14 not applied.");
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ use frame_system::{EnsureRoot, EnsureSignedBy};
|
||||
use sp_io;
|
||||
use sp_runtime::{curve::PiecewiseLinear, testing::UintAuthorityId, traits::Zero, BuildStorage};
|
||||
use sp_staking::{
|
||||
offence::{DisableStrategy, OffenceDetails, OnOffenceHandler},
|
||||
offence::{OffenceDetails, OnOffenceHandler},
|
||||
OnStakingUpdate,
|
||||
};
|
||||
|
||||
@@ -186,7 +186,6 @@ pallet_staking_reward_curve::build! {
|
||||
parameter_types! {
|
||||
pub const BondingDuration: EraIndex = 3;
|
||||
pub const RewardCurve: &'static PiecewiseLinear<'static> = &I_NPOS;
|
||||
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(75);
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
@@ -267,6 +266,9 @@ impl OnStakingUpdate<AccountId, Balance> for EventListenerMock {
|
||||
}
|
||||
}
|
||||
|
||||
// Disabling threshold for `UpToLimitDisablingStrategy`
|
||||
pub(crate) const DISABLING_LIMIT_FACTOR: usize = 3;
|
||||
|
||||
impl crate::pallet::pallet::Config for Test {
|
||||
type Currency = Balances;
|
||||
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
|
||||
@@ -284,7 +286,6 @@ impl crate::pallet::pallet::Config for Test {
|
||||
type EraPayout = ConvertCurve<RewardCurve>;
|
||||
type NextNewSession = Session;
|
||||
type MaxExposurePageSize = MaxExposurePageSize;
|
||||
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
|
||||
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
|
||||
type GenesisElectionProvider = Self::ElectionProvider;
|
||||
// NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well.
|
||||
@@ -297,6 +298,7 @@ impl crate::pallet::pallet::Config for Test {
|
||||
type EventListeners = EventListenerMock;
|
||||
type BenchmarkingConfig = TestBenchmarkingConfig;
|
||||
type WeightInfo = ();
|
||||
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR>;
|
||||
}
|
||||
|
||||
pub struct WeightedNominationsQuota<const MAX: u32>;
|
||||
@@ -461,6 +463,8 @@ impl ExtBuilder {
|
||||
(31, self.balance_factor * 2000),
|
||||
(41, self.balance_factor * 2000),
|
||||
(51, self.balance_factor * 2000),
|
||||
(201, self.balance_factor * 2000),
|
||||
(202, self.balance_factor * 2000),
|
||||
// optional nominator
|
||||
(100, self.balance_factor * 2000),
|
||||
(101, self.balance_factor * 2000),
|
||||
@@ -488,8 +492,10 @@ impl ExtBuilder {
|
||||
(31, 31, self.balance_factor * 500, StakerStatus::<AccountId>::Validator),
|
||||
// an idle validator
|
||||
(41, 41, self.balance_factor * 1000, StakerStatus::<AccountId>::Idle),
|
||||
];
|
||||
// optionally add a nominator
|
||||
(51, 51, self.balance_factor * 1000, StakerStatus::<AccountId>::Idle),
|
||||
(201, 201, self.balance_factor * 1000, StakerStatus::<AccountId>::Idle),
|
||||
(202, 202, self.balance_factor * 1000, StakerStatus::<AccountId>::Idle),
|
||||
]; // optionally add a nominator
|
||||
if self.nominate {
|
||||
stakers.push((
|
||||
101,
|
||||
@@ -728,12 +734,11 @@ pub(crate) fn on_offence_in_era(
|
||||
>],
|
||||
slash_fraction: &[Perbill],
|
||||
era: EraIndex,
|
||||
disable_strategy: DisableStrategy,
|
||||
) {
|
||||
let bonded_eras = crate::BondedEras::<Test>::get();
|
||||
for &(bonded_era, start_session) in bonded_eras.iter() {
|
||||
if bonded_era == era {
|
||||
let _ = Staking::on_offence(offenders, slash_fraction, start_session, disable_strategy);
|
||||
let _ = Staking::on_offence(offenders, slash_fraction, start_session);
|
||||
return
|
||||
} else if bonded_era > era {
|
||||
break
|
||||
@@ -745,7 +750,6 @@ pub(crate) fn on_offence_in_era(
|
||||
offenders,
|
||||
slash_fraction,
|
||||
Staking::eras_start_session_index(era).unwrap(),
|
||||
disable_strategy,
|
||||
);
|
||||
} else {
|
||||
panic!("cannot slash in era {}", era);
|
||||
@@ -760,7 +764,7 @@ pub(crate) fn on_offence_now(
|
||||
slash_fraction: &[Perbill],
|
||||
) {
|
||||
let now = Staking::active_era().unwrap().index;
|
||||
on_offence_in_era(offenders, slash_fraction, now, DisableStrategy::WhenSlashed)
|
||||
on_offence_in_era(offenders, slash_fraction, now)
|
||||
}
|
||||
|
||||
pub(crate) fn add_slash(who: &AccountId) {
|
||||
|
||||
@@ -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>(¶ms);
|
||||
|
||||
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>(¶ms);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user