mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 17: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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user