Skip to content
Merged
29 changes: 29 additions & 0 deletions polkadot/runtime/westend/src/weights/pallet_staking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,4 +875,33 @@ impl<T: frame_system::Config> pallet_staking::WeightInfo for WeightInfo<T> {
.saturating_add(T::DbWeight::get().reads(457))
.saturating_add(T::DbWeight::get().writes(261))
}
/// Storage: `Staking::CurrentEra` (r:1 w:0)
/// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `Staking::ErasStartSessionIndex` (r:1 w:0)
/// Proof: `Staking::ErasStartSessionIndex` (`max_values`: None, `max_size`: Some(16), added: 2491, mode: `MaxEncodedLen`)
/// Storage: `Staking::ActiveEra` (r:1 w:0)
/// Proof: `Staking::ActiveEra` (`max_values`: Some(1), `max_size`: Some(13), added: 508, mode: `MaxEncodedLen`)
/// Storage: `Staking::Invulnerables` (r:1 w:0)
/// Proof: `Staking::Invulnerables` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`)
/// Storage: `Staking::ErasStakersOverview` (r:1 w:0)
/// Proof: `Staking::ErasStakersOverview` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`)
/// Storage: `Session::DisabledValidators` (r:1 w:1)
/// Proof: `Session::DisabledValidators` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `Session::Validators` (r:1 w:0)
/// Proof: `Session::Validators` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `Staking::ValidatorSlashInEra` (r:1 w:1)
/// Proof: `Staking::ValidatorSlashInEra` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
/// Storage: `Staking::OffenceQueue` (r:1 w:1)
/// Proof: `Staking::OffenceQueue` (`max_values`: None, `max_size`: Some(101), added: 2576, mode: `MaxEncodedLen`)
/// Storage: `Staking::OffenceQueueEras` (r:1 w:1)
/// Proof: `Staking::OffenceQueueEras` (`max_values`: Some(1), `max_size`: Some(2690), added: 3185, mode: `MaxEncodedLen`)
fn manual_slash() -> Weight {
// Proof Size summary in bytes:
// Measured: `514`
// Estimated: `4175`
// Minimum execution time: 30_000_000 picoseconds.
Weight::from_parts(33_000_000, 4175)
.saturating_add(T::DbWeight::get().reads(10_u64))
.saturating_add(T::DbWeight::get().writes(4_u64))
}
}
9 changes: 9 additions & 0 deletions prdoc/pr_7805.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: New `staking::manual_slash` extrinsic

doc:
- audience: Runtime Dev
description: A new `manual_slash` extrinsic that allows slashing a validator's stake manually by governance.

crates:
- name: pallet-staking
bump: minor
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,7 @@ pub(crate) fn on_offence_now(
slash_fraction: &[Perbill],
) {
let now = ActiveEra::<Runtime>::get().unwrap().index;
let _ = Staking::on_offence(
let _ = <Staking as OnOffenceHandler<_, _, _>>::on_offence(
offenders,
slash_fraction,
ErasStartSessionIndex::<Runtime>::get(now).unwrap(),
Expand Down
27 changes: 27 additions & 0 deletions substrate/frame/staking/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,33 @@ mod benchmarks {
Ok(())
}

#[benchmark]
fn manual_slash() -> Result<(), BenchmarkError> {
let era = EraIndex::zero();
CurrentEra::<T>::put(era);
ErasStartSessionIndex::<T>::insert(era, 0);
ActiveEra::<T>::put(ActiveEraInfo { index: era, start: None });

// Create a validator with nominators
let (validator_stash, _nominators) = create_validator_with_nominators::<T>(
T::MaxExposurePageSize::get() as u32,
T::MaxExposurePageSize::get() as u32,
false,
true,
RewardDestination::Staked,
era,
)?;

let slash_fraction = Perbill::from_percent(10);

#[extrinsic_call]
_(RawOrigin::Root, validator_stash.clone(), era, slash_fraction);

assert!(ValidatorSlashInEra::<T>::get(era, &validator_stash).is_some());

Ok(())
}

impl_benchmark_test_suite!(
Staking,
crate::mock::ExtBuilder::default().has_stakers(true),
Expand Down
8 changes: 6 additions & 2 deletions substrate/frame/staking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,11 @@ pub(crate) fn on_offence_in_era(
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);
let _ = <Staking as OnOffenceHandler<_, _, _>>::on_offence(
offenders,
slash_fraction,
start_session,
);
if advance_processing_blocks {
advance_blocks(process_blocks as u64);
}
Expand All @@ -857,7 +861,7 @@ pub(crate) fn on_offence_in_era(
}

if pallet_staking::ActiveEra::<Test>::get().unwrap().index == era {
let _ = Staking::on_offence(
let _ = <Staking as OnOffenceHandler<_, _, _>>::on_offence(
offenders,
slash_fraction,
pallet_staking::ErasStartSessionIndex::<Test>::get(era).unwrap(),
Expand Down
22 changes: 20 additions & 2 deletions substrate/frame/staking/src/pallet/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1817,6 +1817,24 @@ where
slash_session,
);

// the exposure is not actually being used in this implementation
let offenders = offenders.iter().map(|details| {
let (ref offender, _) = details.offender;
OffenceDetails { offender: offender.clone(), reporters: details.reporters.clone() }
});
Self::on_offence(offenders, slash_fractions, slash_session)
}
}

impl<T: Config> Pallet<T> {
/// When an offence is reported, it is split into pages and put in the offence queue.
/// As offence queue is processed, computed slashes are queued to be applied after the
/// `SlashDeferDuration`.
pub fn on_offence(
offenders: impl Iterator<Item = OffenceDetails<T::AccountId, T::AccountId>>,
slash_fractions: &[Perbill],
slash_session: SessionIndex,
) -> Weight {
// todo(ank4n): Needs to be properly benched.
let mut consumed_weight = Weight::zero();
let mut add_db_reads_writes = |reads, writes| {
Expand Down Expand Up @@ -1860,8 +1878,8 @@ where
add_db_reads_writes(1, 0);
let invulnerables = Invulnerables::<T>::get();

for (details, slash_fraction) in offenders.iter().zip(slash_fractions) {
let (validator, _) = &details.offender;
for (details, slash_fraction) in offenders.zip(slash_fractions) {
let validator = &details.offender;
// Skip if the validator is invulnerable.
if invulnerables.contains(&validator) {
log!(debug, "🦹 on_offence: {:?} is invulnerable; ignoring offence", validator);
Expand Down
58 changes: 58 additions & 0 deletions substrate/frame/staking/src/pallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2639,5 +2639,63 @@ pub mod pallet {

Ok(())
}

/// This function allows governance to manually slash a validator and is a
/// **fallback mechanism**.
///
/// The dispatch origin must be `T::AdminOrigin`.
///
/// ## Parameters
/// - `validator_stash` - The stash account of the validator to slash.
/// - `era` - The era in which the validator was in the active set.
/// - `slash_fraction` - The percentage of the stake to slash, expressed as a Perbill.
///
/// ## Behavior
///
/// The slash will be applied using the standard slashing mechanics, respecting the
/// configured `SlashDeferDuration`.
///
/// This means:
/// - If the validator was already slashed by a higher percentage for the same era, this
/// slash will have no additional effect.
/// - If the validator was previously slashed by a lower percentage, only the difference
/// will be applied.
/// - The slash will be deferred by `SlashDeferDuration` eras before being enacted.
#[pallet::call_index(33)]
#[pallet::weight(T::WeightInfo::manual_slash())]
pub fn manual_slash(
origin: OriginFor<T>,
validator_stash: T::AccountId,
era: EraIndex,
slash_fraction: Perbill,
) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;

// Check era is valid
let current_era = CurrentEra::<T>::get().ok_or(Error::<T>::InvalidEraToReward)?;
let history_depth = T::HistoryDepth::get();
ensure!(
era <= current_era && era >= current_era.saturating_sub(history_depth),
Error::<T>::InvalidEraToReward
);

let offence_details = sp_staking::offence::OffenceDetails {
offender: validator_stash.clone(),
reporters: Vec::new(),
};

// Get the session index for the era
let session_index =
ErasStartSessionIndex::<T>::get(era).ok_or(Error::<T>::InvalidEraToReward)?;

// Create the offence and report it through on_offence system
let _ = Self::on_offence(
core::iter::once(offence_details),
&[slash_fraction],
session_index,
);

Ok(())
}
}
}
Loading
Loading