Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validator Re-Enabling #5724

Open
wants to merge 50 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
3d438de
expand DisabledValidators with severity
Overkillus Aug 27, 2024
b2d21dd
comment
Overkillus Aug 26, 2024
4c01a56
migration to disabling with severity
Overkillus Aug 28, 2024
996dfab
initial func tests
Overkillus Aug 28, 2024
d908f65
session val index enabling
Overkillus Aug 28, 2024
ec350ff
Disabling Decision Abstraction
Overkillus Aug 28, 2024
927d686
DisablingDecision struct refactor
Overkillus Aug 30, 2024
533025b
prdoc
Overkillus Sep 17, 2024
29111f0
extra staking func tests
Overkillus Sep 17, 2024
a9d2450
decision docs
Overkillus Sep 17, 2024
dbaa856
migration desc in prdoc
Overkillus Sep 17, 2024
7bcfd3e
severity update todo
Overkillus Sep 17, 2024
9fe1cac
migration cleanup
Overkillus Sep 17, 2024
0d994ad
repeated offences can update severity
Overkillus Sep 23, 2024
50dbecf
extra severity update tests
Overkillus Sep 23, 2024
1db6a26
refactor match in add_offending_validator
Overkillus Sep 23, 2024
29c59a6
reenabling test update
Overkillus Sep 23, 2024
2b78972
reenabling test updates p2
Overkillus Sep 23, 2024
32941ab
changing config and default to reenabling strategy
Overkillus Sep 23, 2024
c153fc0
Mock tests and disabling events
Overkillus Sep 24, 2024
63cca97
fmt
Overkillus Sep 24, 2024
ca99191
prdoc format fix
Overkillus Sep 24, 2024
27c698f
storage version bump
Overkillus Oct 1, 2024
0baf1ed
import clean
Overkillus Oct 1, 2024
14f5545
westend bump
Overkillus Oct 2, 2024
1880ee5
try runtime checks
Overkillus Oct 2, 2024
02c5d51
try runtime tests
Overkillus Oct 2, 2024
e0970c7
fmt
Overkillus Oct 2, 2024
cb75b92
changelog
Overkillus Oct 2, 2024
e4dfd4a
Update prdoc/pr_5724.prdoc
Overkillus Oct 15, 2024
12d88d0
decision doc fix
Overkillus Oct 15, 2024
9ebd8a7
changelog fixes
Overkillus Oct 15, 2024
a3aa92f
#64 fix
Overkillus Oct 30, 2024
ece9025
reverting #64 fix in paras runtime
Overkillus Oct 31, 2024
1e1bc48
perbill -> OffenceSeverity wrapper
Overkillus Oct 31, 2024
3382397
OffenceSeverity adjusted tests
Overkillus Oct 31, 2024
bd7cd72
deduplicate disable_limit
Overkillus Nov 4, 2024
a07a7e8
typo
Overkillus Nov 4, 2024
675b2be
non-cumulative assumption for re-enabling strategy impl
Overkillus Nov 4, 2024
f07d895
Merge branch 'master' into mkz-re-enabling
Overkillus Nov 4, 2024
273342a
md fmt
Overkillus Nov 4, 2024
cac5a61
fmt
Overkillus Nov 4, 2024
b8f6e04
clean
Overkillus Nov 4, 2024
2fe9b6d
typo
Overkillus Nov 5, 2024
beaf472
Merge branch 'master' into mkz-re-enabling
Overkillus Nov 5, 2024
dcd312e
Merge branch 'master' into mkz-re-enabling
Overkillus Nov 11, 2024
f7f9096
Merge branch 'master' into mkz-re-enabling
Ank4n Nov 12, 2024
3802d74
defensive
Overkillus Nov 13, 2024
73bd23b
Merge branch 'master' into mkz-re-enabling
Overkillus Nov 13, 2024
c4a19f5
fmt
Overkillus Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions prdoc/pr_5724.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json

title: Validator Re-Enabling (master PR)

doc:
- audience: Runtime Dev
description: |
Implementation of the Stage 3 for the New Disabling Strategy: https://github.com/paritytech/polkadot-sdk/issues/4359

This PR changes when an active validator node gets disabled for comitting offences.
When Byzantine Threshold Validators (1/3) are already disabled instead of no longer
disabling the highest offenders will be disabled potentially re-enabling low offenders.

- audience: Node Operator
Implementation of the Stage 3 for the New Disabling Strategy: https://github.com/paritytech/polkadot-sdk/issues/4359

This PR changes when an active validator node gets disabled within parachain consensus (reduced responsibilities and
Overkillus marked this conversation as resolved.
Show resolved Hide resolved
reduced rewards) for comitting offences. This should not affect active validators on a dato-day basis and will only
Overkillus marked this conversation as resolved.
Show resolved Hide resolved
be relevant when the network is under attack or there is a wide spread malfunction causing slashes. In that case
lowest offenders might get eventually re-enabled (back to normal responsibilities and normal rewards).

migrations:
db: []
runtime:
- reference: pallet-staking
description: |
Migrating `DisabledValidators` from `Vec<u32>` to `Vec<(u32, PerBill)>` where the PerBill represents the severity
of the offence in terms of the % slash.

crates:
- name: pallet-staking
bump: minor

- name: pallet-session
bump: minor
17 changes: 17 additions & 0 deletions substrate/frame/session/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,23 @@ impl<T: Config> Pallet<T> {
})
}

/// Re-enable the validator of index `i`, returns `false` if the validator was already enabled.
pub fn enable_index(i: u32) -> bool {
if i >= Validators::<T>::decode_len().unwrap_or(0) as u32 {
Overkillus marked this conversation as resolved.
Show resolved Hide resolved
return false
}
Overkillus marked this conversation as resolved.
Show resolved Hide resolved

// If the validator is not disabled, return false.
DisabledValidators::<T>::mutate(|disabled| {
if let Ok(index) = disabled.binary_search(&i) {
disabled.remove(index);
true
} else {
false
}
})
}

/// Disable the validator identified by `c`. (If using with the staking pallet,
/// this would be their *stash* account.)
///
Expand Down
126 changes: 116 additions & 10 deletions substrate/frame/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,9 @@ pub trait SessionInterface<AccountId> {
/// Disable the validator at the given index, returns `false` if the validator was already
/// disabled or the index is out of bounds.
fn disable_validator(validator_index: u32) -> bool;
/// Re-enable a validator that was previously disabled. Returns `false` if the validator was
/// already enabled or the index is out of bounds.
fn enable_validator(validator_index: u32) -> bool;
/// Get the validators from session.
fn validators() -> Vec<AccountId>;
/// Prune historical session tries up to but not including the given index.
Expand All @@ -872,6 +875,10 @@ where
<pallet_session::Pallet<T>>::disable_index(validator_index)
}

fn enable_validator(validator_index: u32) -> bool {
<pallet_session::Pallet<T>>::enable_index(validator_index)
}

fn validators() -> Vec<<T as frame_system::Config>::AccountId> {
<pallet_session::Pallet<T>>::validators()
}
Expand All @@ -885,6 +892,9 @@ impl<AccountId> SessionInterface<AccountId> for () {
fn disable_validator(_: u32) -> bool {
true
}
fn enable_validator(_: u32) -> bool {
true
}
fn validators() -> Vec<AccountId> {
Vec::new()
}
Expand Down Expand Up @@ -1270,13 +1280,29 @@ impl BenchmarkingConfig for TestBenchmarkingConfig {

/// 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.
/// Make a disabling decision. Potentially returns 2 validators indices.
/// First index is to be disabled, second is to be re-enabled.
///
/// Options:
/// (None, None) no changes needed
/// (Some(x), None) just disable x
/// (Some(x), Some(y)) disable x instead of y
/// (None, Some(y)) unreachable
fn decision(
offender_stash: &T::AccountId,
offender_slash_severity: Perbill,
slash_era: EraIndex,
currently_disabled: &Vec<u32>,
) -> Option<u32>;
currently_disabled: &Vec<(u32, Perbill)>,
) -> DisablingDecision;
}

/// Helper struct representing a decision coming from a given [`DisablingStrategy`] implementing `decision`
///
/// Currently supports at most 1 disable + 1 re-enable per decision
#[derive(Debug)]
pub struct DisablingDecision {
pub disable: Option<u32>,
pub reenable: Option<u32>,
}

/// Implementation of [`DisablingStrategy`] which disables validators from the active set up to a
Expand Down Expand Up @@ -1305,9 +1331,10 @@ impl<T: Config, const DISABLING_LIMIT_FACTOR: usize> DisablingStrategy<T>
{
fn decision(
offender_stash: &T::AccountId,
_offender_slash_severity: Perbill,
slash_era: EraIndex,
currently_disabled: &Vec<u32>,
) -> Option<u32> {
currently_disabled: &Vec<(u32, Perbill)>,
) -> DisablingDecision {
let active_set = T::SessionInterface::validators();

// We don't disable more than the limit
Expand All @@ -1317,7 +1344,7 @@ impl<T: Config, const DISABLING_LIMIT_FACTOR: usize> DisablingStrategy<T>
"Won't disable: reached disabling limit {:?}",
Self::disable_limit(active_set.len())
);
return None
return DisablingDecision { disable: None, reenable: None }
}

// We don't disable for offences in previous eras
Expand All @@ -1328,18 +1355,97 @@ impl<T: Config, const DISABLING_LIMIT_FACTOR: usize> DisablingStrategy<T>
Pallet::<T>::current_era().unwrap_or_default(),
slash_era
);
return None
return DisablingDecision { disable: None, reenable: 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
return DisablingDecision { disable: None, reenable: None }
};

log!(debug, "Will disable {:?}", offender_idx);

Some(offender_idx)
DisablingDecision { disable: Some(offender_idx), reenable: None }
}
}

/// Implementation of [`DisablingStrategy`] which disables validators from the active set up to a
/// limit and if the limit is reached and the new offender is higher (bigger punishment/severity)
/// then it re-enables to lowest offender to free up space for the new offender.
///
/// An extension of [`UpToLimitDisablingStrategy`].
pub struct UpToLimitWithReEnablingDisablingStrategy<const DISABLING_LIMIT_FACTOR: usize = 3>;

impl<const DISABLING_LIMIT_FACTOR: usize> UpToLimitWithReEnablingDisablingStrategy<DISABLING_LIMIT_FACTOR> {
/// Disabling limit calculated from the total number of validators in the active set. When
/// reached re-enabling logic might kick in.
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
})
Overkillus marked this conversation as resolved.
Show resolved Hide resolved
}
}

impl<T: Config, const DISABLING_LIMIT_FACTOR: usize> DisablingStrategy<T>
for UpToLimitWithReEnablingDisablingStrategy<DISABLING_LIMIT_FACTOR>
{
fn decision(
offender_stash: &T::AccountId,
offender_slash_severity: Perbill,
slash_era: EraIndex,
currently_disabled: &Vec<(u32, Perbill)>,
) -> DisablingDecision {
let active_set = T::SessionInterface::validators();

// 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 DisablingDecision { disable: None, reenable: None }
}

// We don't disable validators that are not in the active set
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 DisablingDecision { disable: None, reenable: None }
};

// We don't disable more than the limit (but we can re-enable a smaller offender to make space)
if currently_disabled.len() >= Self::disable_limit(active_set.len()) {
log!(
debug,
"Reached disabling limit {:?}, checking for re-enabling",
Self::disable_limit(active_set.len())
);

// Find the smallest offender to re-enable that is not higher than offender_slash_severity
if let Some((smallest_idx, _)) = currently_disabled
.iter()
.filter(|(_, perbill)| *perbill <= offender_slash_severity)
Overkillus marked this conversation as resolved.
Show resolved Hide resolved
.min_by_key(|(_, perbill)| *perbill)
{
log!(debug, "Will disable {:?} and re-enable {:?}", offender_idx, smallest_idx);
return DisablingDecision { disable: Some(offender_idx), reenable: Some(*smallest_idx) }
} else {
log!(debug, "No smaller offender found to re-enable");
return DisablingDecision { disable: None, reenable: None }
}
}

// If we are not at the limit, just disable the new offender and dont re-enable anyone
log!(debug, "Will disable {:?}", offender_idx);
DisablingDecision { disable: Some(offender_idx), reenable: None }
}
}
32 changes: 32 additions & 0 deletions substrate/frame/staking/src/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,45 @@ impl Default for ObsoleteReleases {
#[storage_alias]
type StorageVersion<T: Config> = StorageValue<Pallet<T>, ObsoleteReleases, ValueQuery>;

/// Migrating `DisabledValidators` from `Vec<u32>` to `Vec<(u32, PerBill)>` to track offense severity for re-enabling purposes.
pub mod v16 {
use super::*;

pub struct VersionUncheckedMigrateV15ToV16<T>(core::marker::PhantomData<T>);
impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV15ToV16<T> {
fn on_runtime_upgrade() -> Weight {
// Migrating `DisabledValidators` from `Vec<u32>` to `Vec<(u32, PerBill)>`.
// Using max severity (PerBill) for the migration which effectively makes it so offenders before the migration will never be re-enabled.
let max_perbill = Perbill::from_percent(100);
// Inject severity
let migrated = v15::DisabledValidators::<T>::take().into_iter().map(|v| (v, max_perbill)).collect::<Vec<_>>();

DisabledValidators::<T>::set(migrated);

log!(info, "v16 applied successfully.");
T::DbWeight::get().reads_writes(1, 1)
}
}

pub type MigrateV14ToV15<T> = VersionedMigration<
15,
16,
VersionUncheckedMigrateV15ToV16<T>,
Pallet<T>,
<T as frame_system::Config>::DbWeight,
>;
}

/// Migrating `OffendingValidators` from `Vec<(u32, bool)>` to `Vec<u32>`
pub mod v15 {
use super::*;

// The disabling strategy used by staking pallet
type DefaultDisablingStrategy = UpToLimitDisablingStrategy;

#[storage_alias]
pub(crate) type DisabledValidators<T: Config> = StorageValue<Pallet<T>, Vec<u32>, ValueQuery>;

pub struct VersionUncheckedMigrateV14ToV15<T>(core::marker::PhantomData<T>);
impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV14ToV15<T> {
fn on_runtime_upgrade() -> Weight {
Expand Down
5 changes: 3 additions & 2 deletions substrate/frame/staking/src/pallet/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ impl<T: Config> Pallet<T> {
}

// disable all offending validators that have been disabled for the whole era
for index in <DisabledValidators<T>>::get() {
for (index, _) in <DisabledValidators<T>>::get() {
T::SessionInterface::disable_validator(index);
}
}
Expand Down Expand Up @@ -2297,9 +2297,10 @@ impl<T: Config> Pallet<T> {
Ok(())
}

// Sorted by index
fn ensure_disabled_validators_sorted() -> Result<(), TryRuntimeError> {
ensure!(
DisabledValidators::<T>::get().windows(2).all(|pair| pair[0] <= pair[1]),
DisabledValidators::<T>::get().windows(2).all(|pair| pair[0].0 <= pair[1].0),
"DisabledValidators is not sorted"
);
Ok(())
Expand Down
2 changes: 1 addition & 1 deletion substrate/frame/staking/src/pallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ pub mod pallet {
/// offended using binary search.
#[pallet::storage]
#[pallet::unbounded]
pub type DisabledValidators<T: Config> = StorageValue<_, Vec<u32>, ValueQuery>;
pub type DisabledValidators<T: Config> = StorageValue<_, Vec<(u32, Perbill)>, ValueQuery>;
ordian marked this conversation as resolved.
Show resolved Hide resolved

/// 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
Expand Down
Loading
Loading