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 disabling in session enhancements #7663

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d02401f
macroed traces
Overkillus Feb 21, 2025
26b5a0b
disabling for era unit test
Overkillus Feb 21, 2025
dc9fc1b
extra session disabling traces
Overkillus Feb 21, 2025
030afd0
exposing severities
Overkillus Feb 21, 2025
cc884c9
tweating migration, stronger default severity
Overkillus Feb 21, 2025
46e5d55
higher migration limit
Overkillus Feb 21, 2025
b605342
past session disabling test
Overkillus Feb 25, 2025
8a5772e
porting disabling tests from staking to session
Overkillus Feb 25, 2025
8623571
additional test for custom disabling limits
Overkillus Feb 25, 2025
c533c10
fmt
Overkillus Feb 25, 2025
c1030ae
try-runtime disabling validators ordering invariant added to session
Overkillus Feb 25, 2025
c8fdd20
zero slash disables
Overkillus Feb 25, 2025
1324eff
clean up disabling tests in staking
Overkillus Feb 25, 2025
e4551ac
default offence severity
Overkillus Feb 25, 2025
7ed635a
session disabling interface refactor
Overkillus Feb 25, 2025
1e1bab5
test cleanup
Overkillus Feb 25, 2025
ca8757a
try state checks
Overkillus Feb 25, 2025
c30127d
add default ref in migratotion
Overkillus Feb 25, 2025
8223665
severity min max default
Overkillus Mar 3, 2025
0ac3abc
default -> max
Overkillus Mar 3, 2025
0f914a1
prdoc
Overkillus Mar 3, 2025
19435bd
migration bounded vec bump
Overkillus Mar 3, 2025
d14134b
fmt
Overkillus Mar 3, 2025
894b3a8
Merge branch 'master' into mkz-session-disabling-plus
Overkillus Mar 3, 2025
d7ca394
live indexing safeguard
Overkillus Mar 3, 2025
4a099a2
import clean
Overkillus Mar 3, 2025
8c7909e
Merge branch 'master' into mkz-session-disabling-plus
Overkillus Mar 10, 2025
830d875
Merge branch 'master' into mkz-session-disabling-plus
Overkillus Mar 17, 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
19 changes: 19 additions & 0 deletions prdoc/pr_7663.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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 disabling in session enhancements

doc:
- audience: Runtime Dev
description: |
This PR introduces changes to the pallet-session interface. Disabled validators can
still be disabled with just the index but it will default to highest possible severity.
pallet-session also additionally exposes DisabledValidators with their severities.
The staking primitive OffenceSeverity received min, max and default implementations
for ease of use.

crates:
- name: pallet-staking
bump: major
- name: pallet-session
bump: major
Comment on lines +16 to +19
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe it doesn't have to be major if you keep disable pub fn

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer removing old disable. It simplifies and unifies the interface while still allowing for old-scholl interface with the provided translation func (pub fn validator_id_to_index).

182 changes: 112 additions & 70 deletions substrate/frame/session/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,16 @@
use frame_system::pallet_prelude::BlockNumberFor;
use sp_runtime::{
traits::{AtLeast32BitUnsigned, Convert, Member, One, OpaqueKeys, Zero},
ConsensusEngineId, DispatchError, KeyTypeId, Perbill, Permill, RuntimeAppPublic,

Check failure on line 141 in substrate/frame/session/src/lib.rs

View workflow job for this annotation

GitHub Actions / cargo-check-all-crate-macos

unused import: `Perbill`
};
use sp_staking::{offence::OffenceSeverity, SessionIndex};

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

#[cfg(any(test, feature = "try-runtime"))]
use sp_runtime::TryRuntimeError;

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

// syntactic sugar for logging.
Expand Down Expand Up @@ -590,6 +593,11 @@
Weight::zero()
}
}

#[cfg(feature = "try-runtime")]
fn try_state(_n: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
Self::do_try_state()
}
}

#[pallet::call]
Expand Down Expand Up @@ -656,6 +664,11 @@
DisabledValidators::<T>::get().iter().map(|(i, _)| *i).collect()
}

/// Public function to access the disabled validators with their severities.
pub fn disabled_validators_with_severities() -> Vec<(u32, OffenceSeverity)> {
DisabledValidators::<T>::get()
}
Comment on lines +667 to +670
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the plan to expose this in a runtime API or how is it planned to be used?


/// Move on to next session. Register new validator set and session keys. Changes to the
/// validator set have a session of delay to take effect. This allows for equivocation
/// punishment after a fork.
Expand All @@ -675,6 +688,7 @@
Validators::<T>::put(&validators);

if changed {
log!(trace, "resetting disabled validators");
// reset disabled validators if active set was changed
DisabledValidators::<T>::take();
}
Expand All @@ -683,12 +697,12 @@
let session_index = session_index + 1;
CurrentIndex::<T>::put(session_index);
T::SessionManager::start_session(session_index);
log::trace!(target: "runtime::session", "starting_session {:?}", session_index);
log!(trace, "starting_session {:?}", session_index);

// Get next validator set.
let maybe_next_validators = T::SessionManager::new_session(session_index + 1);
log::trace!(
target: "runtime::session",
log!(
trace,
"planning_session {:?} with {:?} validators",
session_index + 1,
maybe_next_validators.as_ref().map(|v| v.len())
Expand Down Expand Up @@ -745,38 +759,6 @@
T::SessionHandler::on_new_session::<T::Keys>(changed, &session_keys, &queued_amalgamated);
}

/// Disable the validator of index `i`, returns `false` if the validator was already disabled.
///
/// Note: This sets the OffenceSeverity to the lowest value.
pub fn disable_index(i: u32) -> bool {
if i >= Validators::<T>::decode_len().defensive_unwrap_or(0) as u32 {
return false
}

DisabledValidators::<T>::mutate(|disabled| {
if let Err(index) = disabled.binary_search_by_key(&i, |(index, _)| *index) {
disabled.insert(index, (i, OffenceSeverity(Perbill::zero())));
T::SessionHandler::on_disabled(i);
return true
}

false
})
}

/// Disable the validator identified by `c`. (If using with the staking pallet,
/// this would be their *stash* account.)
///
/// Returns `false` either if the validator could not be found or it was already
/// disabled.
pub fn disable(c: &T::ValidatorId) -> bool {
Validators::<T>::get()
.iter()
.position(|i| i == c)
.map(|i| Self::disable_index(i as u32))
.unwrap_or(false)
}

Comment on lines -767 to -779
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to keep disable_index, but not disable? i thought the former was done for backwards compatibility, but without the latter this is a breaking change

Copy link
Contributor Author

@Overkillus Overkillus Mar 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can disable, disable with severity and re-enable. All those could be called with either index or validator id. Instead of having 6+ different functions we provide index ones and supply pub fn validator_id_to_index(id: &T::ValidatorId) -> Option<u32> so users can do the translation themselves.

Do you think it would be sensible to duplicate all those calls with _validator_id where we just bake in the call to validator_id_to_index?

/// Upgrade the key type from some old type to a new type. Supports adding
/// and removing key types.
///
Expand Down Expand Up @@ -928,45 +910,105 @@
KeyOwner::<T>::remove((id, key_data));
}

pub fn report_offence(validator: T::ValidatorId, severity: OffenceSeverity) {
/// Disable the validator of index `i` with a specified severity,
/// returns `false` if the validator is not found.
///
/// Note: If validator is already disabled, the severity will
/// be updated if the new one is higher.
pub fn disable_index_with_severity(i: u32, severity: OffenceSeverity) -> bool {
if i >= Validators::<T>::decode_len().defensive_unwrap_or(0) as u32 {
return false;
}

DisabledValidators::<T>::mutate(|disabled| {
let decision = T::DisablingStrategy::decision(&validator, severity, &disabled);

if let Some(offender_idx) = decision.disable {
// Check if the offender is already disabled
match disabled.binary_search_by_key(&offender_idx, |(index, _)| *index) {
// Offender is already disabled, update severity if the new one is higher
Ok(index) => {
let (_, old_severity) = &mut disabled[index];
if severity > *old_severity {
*old_severity = severity;
}
},
Err(index) => {
// Offender is not disabled, add to `DisabledValidators` and disable it
disabled.insert(index, (offender_idx, severity));
// let the session handlers know that a validator got disabled
T::SessionHandler::on_disabled(offender_idx);

// Emit event that a validator got disabled
Self::deposit_event(Event::ValidatorDisabled {
validator: validator.clone(),
});
},
}
match disabled.binary_search_by_key(&i, |(index, _)| *index) {
// Validator is already disabled, update severity if the new one is higher
Ok(index) => {
let current_severity = &mut disabled[index].1;
if severity > *current_severity {
log!(
trace,
"updating disablement severity of validator {:?} from {:?} to {:?}",
i,
*current_severity,
severity
);
*current_severity = severity;
}
true
},
// Validator is not disabled, add to `DisabledValidators` and disable it
Err(index) => {
log!(trace, "disabling validator {:?}", i);
Self::deposit_event(Event::ValidatorDisabled {
validator: Validators::<T>::get()[index as usize].clone(),
});
disabled.insert(index, (i, severity));
T::SessionHandler::on_disabled(i);
true
},
}
})
}

if let Some(reenable_idx) = decision.reenable {
// Remove the validator from `DisabledValidators` and re-enable it.
if let Ok(index) = disabled.binary_search_by_key(&reenable_idx, |(index, _)| *index)
{
disabled.remove(index);
// Emit event that a validator got re-enabled
let reenabled_stash = Validators::<T>::get()[reenable_idx as usize].clone();
Self::deposit_event(Event::ValidatorReenabled { validator: reenabled_stash });
}
/// Disable the validator of index `i` with a default severity (defaults to most severe),
/// returns `false` if the validator is not found.
pub fn disable_index(i: u32) -> bool {
let default_severity = OffenceSeverity::default();
Self::disable_index_with_severity(i, default_severity)
}

/// Re-enable the validator of index `i`, returns `false` if the validator was not disabled.
pub fn reenable_index(i: u32) -> bool {
if i >= Validators::<T>::decode_len().defensive_unwrap_or(0) as u32 {
return false;
}

DisabledValidators::<T>::mutate(|disabled| {
if let Ok(index) = disabled.binary_search_by_key(&i, |(index, _)| *index) {
log!(trace, "reenabling validator {:?}", i);
Self::deposit_event(Event::ValidatorReenabled {
validator: Validators::<T>::get()[index as usize].clone(),
});
disabled.remove(index);
return true;
}
});
false
})
}

/// Convert a validator ID to an index.
/// (If using with the staking pallet, this would be their *stash* account.)
pub fn validator_id_to_index(id: &T::ValidatorId) -> Option<u32> {
Validators::<T>::get().iter().position(|i| i == id).map(|i| i as u32)
}

/// Report an offence for the given validator and let disabling strategy decide
/// what changes to disabled validators should be made.
pub fn report_offence(validator: T::ValidatorId, severity: OffenceSeverity) {
log!(trace, "reporting offence for {:?} with {:?}", validator, severity);
let decision =
T::DisablingStrategy::decision(&validator, severity, &DisabledValidators::<T>::get());

// Disable
if let Some(offender_idx) = decision.disable {
Self::disable_index_with_severity(offender_idx, severity);
}

// Re-enable
if let Some(reenable_idx) = decision.reenable {
Self::reenable_index(reenable_idx);
}
}

#[cfg(any(test, feature = "try-runtime"))]
pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
// Ensure that the validators are sorted
ensure!(
DisabledValidators::<T>::get().windows(2).all(|pair| pair[0].0 <= pair[1].0),
"DisabledValidators is not sorted"
);
Ok(())
}
}

Expand Down
6 changes: 3 additions & 3 deletions substrate/frame/session/src/migrations/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::{Config, DisabledValidators as NewDisabledValidators, Pallet, Perbill, Vec};
use crate::{Config, DisabledValidators as NewDisabledValidators, Pallet, Vec};
use frame_support::{
pallet_prelude::{Get, ValueQuery, Weight},
traits::UncheckedOnRuntimeUpgrade,
Expand Down Expand Up @@ -49,14 +49,14 @@ impl<T: Config> MigrateDisabledValidators for InitOffenceSeverity<T> {
fn peek_disabled() -> Vec<(u32, OffenceSeverity)> {
DisabledValidators::<T>::get()
.iter()
.map(|v| (*v, OffenceSeverity(Perbill::zero())))
.map(|v| (*v, OffenceSeverity::max_severity()))
.collect::<Vec<_>>()
}

fn take_disabled() -> Vec<(u32, OffenceSeverity)> {
DisabledValidators::<T>::take()
.iter()
.map(|v| (*v, OffenceSeverity(Perbill::zero())))
.map(|v| (*v, OffenceSeverity::max_severity()))
.collect::<Vec<_>>()
}
}
Expand Down
Loading
Loading