Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3c8ec97
config to set slash cancellation duration
Ank4n Dec 7, 2024
6de531d
Merge branch 'master' into ankan/mb-slash
Ank4n Dec 7, 2024
41e1762
Merge branch 'master' into ankan/mb-slash
Ank4n Dec 9, 2024
94fef56
[CI/CD]Revert the token changes in backport flow (#6794)
EgorPopelyaev Dec 9, 2024
1d21c6c
Mak cmd swap omnibench (#6769)
mordamax Dec 9, 2024
ed17e70
Fix `Possible bug: Vote import failed` after aggression is enabled (#…
alexggh Dec 9, 2024
53d47cd
pallet-revive: Remove unused dependencies (#6796)
athei Dec 9, 2024
a783e82
xcm-executor: take transport fee from transferred assets if necessary…
acatangiu Dec 9, 2024
8cfc536
Add fallback_max_weight to snowbridge Transact (#6792)
franciscoaguirre Dec 10, 2024
9a2f756
Add fallback_weight to the log (#6782)
bkontur Dec 10, 2024
ff60aa6
Bridges - revert-back congestion mechanism (#6781)
bkontur Dec 10, 2024
a4dde05
Remove AccountKeyring everywhere (#5899)
programskillforverification Dec 10, 2024
3c987b0
Let cmd bot to trigger ci on commit (#6813)
mordamax Dec 10, 2024
6ae8ddd
polkadot-sdk-docs: Use command_macro! (#6624)
ndkazu Dec 10, 2024
b503d9c
Fix order of resending messages after restart (#6729)
alexggh Dec 10, 2024
f6b593b
move test under externalities
Ank4n Dec 10, 2024
3db6539
add deferred slashes storage and some consts
Ank4n Dec 15, 2024
b022f11
rewrite on_offence
Ank4n Dec 15, 2024
6b4a4f8
prune session to extra slashCancellationDuration
Ank4n Dec 15, 2024
2cf7cbb
start moving disabling code to session
Ank4n Dec 15, 2024
d0c4316
add new function offending_validator to session interface
Ank4n Dec 15, 2024
d44edcc
Merge branch 'master' into ankan/mb-slash
Ank4n Jan 13, 2025
267bf0d
Merge branch 'master' into ankan/mb-slash
Ank4n Jan 15, 2025
ebcb901
Merge branch 'master' into ankan/mb-slash
Ank4n Jan 16, 2025
b8c313f
clean prdocs
Ank4n Jan 16, 2025
7aaeedd
Merge branch 'master' into ankan/mb-slash
Ank4n Jan 16, 2025
45209a7
hitting the slashing span wall :(
Ank4n Jan 20, 2025
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
190 changes: 190 additions & 0 deletions substrate/frame/session/src/disabling.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::*;
use frame_support::defensive;
/// Controls validator disabling
pub trait DisablingStrategy<T: Config> {
/// Make a disabling decision. Returning a [`DisablingDecision`]
fn decision(
offender_stash: &T::ValidatorId,
offender_slash_severity: OffenceSeverity,
currently_disabled: &Vec<(u32, OffenceSeverity)>,
) -> DisablingDecision;
}

/// Helper struct representing a decision coming from a given [`DisablingStrategy`] implementing
/// `decision`
///
/// `disable` is the index of the validator to disable,
/// `reenable` is the index of the validator to re-enable.
#[derive(Debug)]
pub struct DisablingDecision {
pub disable: Option<u32>,
pub reenable: Option<u32>,
}

/// Calculate the disabling limit based on the number of validators and the disabling limit factor.
///
/// This is a sensible default implementation for the disabling limit factor for most disabling
/// strategies.
///
/// Disabling limit factor n=2 -> 1/n = 1/2 = 50% of validators can be disabled
fn factor_based_disable_limit(validators_len: usize, disabling_limit_factor: usize) -> usize {
validators_len
.saturating_sub(1)
.checked_div(disabling_limit_factor)
.unwrap_or_else(|| {
defensive!("DISABLING_LIMIT_FACTOR should not be 0");
0
})
}

/// Implementation of [`DisablingStrategy`] using factor_based_disable_limit 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 {
factor_based_disable_limit(validators_len, DISABLING_LIMIT_FACTOR)
}
}

impl<T: Config, const DISABLING_LIMIT_FACTOR: usize> DisablingStrategy<T>
for UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR>
{
fn decision(
offender_stash: &T::ValidatorId,
_offender_slash_severity: OffenceSeverity,
currently_disabled: &Vec<(u32, OffenceSeverity)>,
) -> DisablingDecision {
let active_set = Validators::<T>::get();

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

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

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

/// Implementation of [`DisablingStrategy`] which disables validators from the active set up to a
/// limit (factor_based_disable_limit) and if the limit is reached and the new offender is higher
/// (bigger punishment/severity) then it re-enables the lowest offender to free up space for the new
/// offender.
///
/// This strategy is not based on cumulative severity of offences but only on the severity of the
/// highest offence. Offender first committing a 25% offence and then a 50% offence will be treated
/// the same as an offender committing 50% offence.
///
/// 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 {
factor_based_disable_limit(validators_len, DISABLING_LIMIT_FACTOR)
}
}

impl<T: Config, const DISABLING_LIMIT_FACTOR: usize> DisablingStrategy<T>
for UpToLimitWithReEnablingDisablingStrategy<DISABLING_LIMIT_FACTOR>
{
fn decision(
offender_stash: &T::ValidatorId,
offender_slash_severity: OffenceSeverity,
currently_disabled: &Vec<(u32, OffenceSeverity)>,
) -> DisablingDecision {
let active_set = Validators::<T>::get();

// 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 }
};

// Check if offender is already disabled
if let Some((_, old_severity)) =
currently_disabled.iter().find(|(idx, _)| *idx == offender_idx)
{
if offender_slash_severity > *old_severity {
log!(debug, "Offender already disabled but with lower severity, will disable again to refresh severity of {:?}", offender_idx);
return DisablingDecision { disable: Some(offender_idx), reenable: None };
} else {
log!(debug, "Offender already disabled with higher or equal severity");
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(|(_, severity)| *severity <= offender_slash_severity)
.min_by_key(|(_, severity)| *severity)
{
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 }
}
} else {
// If we are not at the limit, just disable the new offender and dont re-enable anyone
log!(debug, "Will disable {:?}", offender_idx);
return DisablingDecision { disable: Some(offender_idx), reenable: None }
}
}
}
22 changes: 20 additions & 2 deletions substrate/frame/session/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@

#![cfg_attr(not(feature = "std"), no_std)]

mod disabling;
#[cfg(feature = "historical")]
pub mod historical;
pub mod migrations;
Expand Down Expand Up @@ -138,11 +139,24 @@ use sp_runtime::{
traits::{AtLeast32BitUnsigned, Convert, Member, One, OpaqueKeys, Zero},
ConsensusEngineId, DispatchError, KeyTypeId, Permill, RuntimeAppPublic,
};
use sp_staking::SessionIndex;
use sp_staking::{offence::OffenceSeverity, SessionIndex};

pub use pallet::*;
pub use weights::WeightInfo;

pub(crate) const LOG_TARGET: &str = "runtime::session";

// syntactic sugar for logging.
#[macro_export]
macro_rules! log {
($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
log::$level!(
target: crate::LOG_TARGET,
concat!("[{:?}] 💸 ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
)
};
}

/// Decides whether the session should be ended.
pub trait ShouldEndSession<BlockNumber> {
/// Return `true` if the session should be ended.
Expand Down Expand Up @@ -639,7 +653,7 @@ impl<T: Config> Pallet<T> {
/// punishment after a fork.
pub fn rotate_session() {
let session_index = CurrentIndex::<T>::get();
log::trace!(target: "runtime::session", "rotating session {:?}", session_index);
log!(trace, "rotating session {:?}", session_index);

let changed = QueuedChanged::<T>::get();

Expand Down Expand Up @@ -718,6 +732,10 @@ impl<T: Config> Pallet<T> {
T::SessionHandler::on_new_session::<T::Keys>(changed, &session_keys, &queued_amalgamated);
}

pub fn offending_validator(validator: T::ValidatorId, severity: OffenceSeverity) {
// todo(ank4n)
}

/// Disable the validator of index `i`, returns `false` if the validator was already disabled.
pub fn disable_index(i: u32) -> bool {
if i >= Validators::<T>::decode_len().unwrap_or(0) as u32 {
Expand Down
49 changes: 45 additions & 4 deletions substrate/frame/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,29 @@ impl<AccountId, Balance: HasCompact + Zero> UnappliedSlash<AccountId, Balance> {
}
}

/// Represents a deferred slash to be applied in a future era.
///
/// This struct holds minimal metadata required for recording and processing a slash. Slash amounts
/// for nominators and the validator are computed dynamically when the slash is applied, reducing
/// upfront computation and storage overhead.
#[derive(Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct DeferredSlash<AccountId> {
/// The stash account ID of the offending validator.
validator: AccountId,
/// The fraction of the validator's total exposure to be slashed.
slash_fraction: Perbill,
/// Reporter of the offence; bounty payout recipient.
reporter: Option<AccountId>,
/// The era in which the offence occurred.
offence_era: EraIndex,
/// The current page being processed in the slash application. Each page represents a portion
/// of nominators being slashed.
current_page: u32,
/// The total number of pages to process for this slash. This is derived based on the number of
/// nominators.
total_pages: u32,
}

/// Something that defines the maximum number of nominations per nominator based on a curve.
///
/// The method `curve` implements the nomination quota curve and should not be used directly.
Expand Down Expand Up @@ -844,14 +867,21 @@ impl<Balance, const MAX: u32> NominationsQuota<Balance> for FixedNominationsQuot
///
/// This is needed because `Staking` sets the `ValidatorIdOf` of the `pallet_session::Config`
pub trait SessionInterface<AccountId> {
/// Report an offending validator.
fn offending_validator(validator: AccountId, severity: OffenceSeverity);

/// Disable the validator at the given index, returns `false` if the validator was already
/// disabled or the index is out of bounds.
// todo(ank4n): remove the next two methods.
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.
fn prune_historical_up_to(up_to: SessionIndex);
}
Expand All @@ -870,6 +900,13 @@ where
Option<<T as frame_system::Config>::AccountId>,
>,
{
fn offending_validator(
validator: <T as frame_system::Config>::AccountId,
severity: OffenceSeverity,
) {
<pallet_session::Pallet<T>>::offending_validator(validator, severity)
}

fn disable_validator(validator_index: u32) -> bool {
<pallet_session::Pallet<T>>::disable_index(validator_index)
}
Expand All @@ -888,6 +925,10 @@ where
}

impl<AccountId> SessionInterface<AccountId> for () {
fn offending_validator(validator: AccountId, severity: OffenceSeverity) {
()
}

fn disable_validator(_: u32) -> bool {
true
}
Expand Down Expand Up @@ -1237,10 +1278,10 @@ impl<T: Config> EraInfo<T> {
let page_size = T::MaxExposurePageSize::get().defensive_max(1);

let nominator_count = exposure.others.len();
// expected page count is the number of nominators divided by the page size, rounded up.
let expected_page_count = nominator_count
.defensive_saturating_add((page_size as usize).defensive_saturating_sub(1))
.saturating_div(page_size as usize);
// expected page count is the number of nominators divided by the page size, rounded up.
let expected_page_count = nominator_count
.defensive_saturating_add((page_size as usize).defensive_saturating_sub(1))
.saturating_div(page_size as usize);

let (exposure_metadata, exposure_pages) = exposure.into_pages(page_size);
defensive_assert!(exposure_pages.len() == expected_page_count, "unexpected page count");
Expand Down
7 changes: 7 additions & 0 deletions substrate/frame/staking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ parameter_types! {
pub static SessionsPerEra: SessionIndex = 3;
pub static ExistentialDeposit: Balance = 1;
pub static SlashDeferDuration: EraIndex = 0;
pub static SlashCancellationDuration: EraIndex = 0;
pub static Period: BlockNumber = 5;
pub static Offset: BlockNumber = 0;
pub static MaxControllersInDeprecationBatch: u32 = 5900;
Expand Down Expand Up @@ -270,6 +271,7 @@ impl crate::pallet::pallet::Config for Test {
type Reward = MockReward;
type SessionsPerEra = SessionsPerEra;
type SlashDeferDuration = SlashDeferDuration;
type SlashCancellationDuration = SlashCancellationDuration;
type AdminOrigin = EnsureOneOrRoot;
type SessionInterface = Self;
type EraPayout = ConvertCurve<RewardCurve>;
Expand Down Expand Up @@ -371,6 +373,11 @@ impl ExtBuilder {
SLASH_DEFER_DURATION.with(|v| *v.borrow_mut() = eras);
self
}

pub fn slash_cancellation_duration(self, eras: EraIndex) -> Self {
SLASH_CANCELLATION_DURATION.with(|v| *v.borrow_mut() = eras);
self
}
pub fn invulnerables(mut self, invulnerables: Vec<AccountId>) -> Self {
self.invulnerables = invulnerables;
self
Expand Down
Loading