use crate::{
asset, log, BalanceOf, Config, EraInfo, Error, NegativeImbalanceOf, NominatorSlashInEra,
OffenceQueue, OffenceQueueEras, PagedExposure, Pallet, Perbill, ProcessingOffence,
SlashRewardFraction, SpanSlash, UnappliedSlash, UnappliedSlashes, ValidatorSlashInEra,
};
use alloc::vec::Vec;
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::{
ensure,
traits::{Defensive, DefensiveSaturating, Get, Imbalance, OnUnbalanced},
};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{Saturating, Zero},
DispatchResult, RuntimeDebug, WeakBoundedVec, Weight,
};
use sp_staking::{EraIndex, StakingInterface};
const REWARD_F1: Perbill = Perbill::from_percent(50);
pub type SpanIndex = u32;
#[derive(Encode, Decode, TypeInfo)]
#[cfg_attr(test, derive(Debug, PartialEq))]
pub(crate) struct SlashingSpan {
pub(crate) index: SpanIndex,
pub(crate) start: EraIndex,
pub(crate) length: Option<EraIndex>, }
impl SlashingSpan {
fn contains_era(&self, era: EraIndex) -> bool {
self.start <= era && self.length.map_or(true, |l| self.start.saturating_add(l) > era)
}
}
#[derive(Encode, Decode, RuntimeDebug, TypeInfo)]
pub struct SlashingSpans {
span_index: SpanIndex,
last_start: EraIndex,
last_nonzero_slash: EraIndex,
prior: Vec<EraIndex>,
}
impl SlashingSpans {
pub(crate) fn new(window_start: EraIndex) -> Self {
SlashingSpans {
span_index: 0,
last_start: window_start,
last_nonzero_slash: 0,
prior: Vec::new(),
}
}
pub(crate) fn end_span(&mut self, now: EraIndex) -> bool {
let next_start = now.defensive_saturating_add(1);
if next_start <= self.last_start {
return false
}
let last_length = next_start.defensive_saturating_sub(self.last_start);
self.prior.insert(0, last_length);
self.last_start = next_start;
self.span_index.defensive_saturating_accrue(1);
true
}
pub(crate) fn iter(&'_ self) -> impl Iterator<Item = SlashingSpan> + '_ {
let mut last_start = self.last_start;
let mut index = self.span_index;
let last = SlashingSpan { index, start: last_start, length: None };
let prior = self.prior.iter().cloned().map(move |length| {
let start = last_start.defensive_saturating_sub(length);
last_start = start;
index.defensive_saturating_reduce(1);
SlashingSpan { index, start, length: Some(length) }
});
core::iter::once(last).chain(prior)
}
pub fn last_nonzero_slash(&self) -> EraIndex {
self.last_nonzero_slash
}
fn prune(&mut self, window_start: EraIndex) -> Option<(SpanIndex, SpanIndex)> {
let old_idx = self
.iter()
.skip(1) .position(|span| {
span.length
.map_or(false, |len| span.start.defensive_saturating_add(len) <= window_start)
});
let earliest_span_index =
self.span_index.defensive_saturating_sub(self.prior.len() as SpanIndex);
let pruned = match old_idx {
Some(o) => {
self.prior.truncate(o);
let new_earliest =
self.span_index.defensive_saturating_sub(self.prior.len() as SpanIndex);
Some((earliest_span_index, new_earliest))
},
None => None,
};
self.last_start = core::cmp::max(self.last_start, window_start);
pruned
}
}
#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]
pub(crate) struct SpanRecord<Balance> {
slashed: Balance,
paid_out: Balance,
}
impl<Balance> SpanRecord<Balance> {
#[cfg(test)]
pub(crate) fn amount(&self) -> &Balance {
&self.slashed
}
}
#[derive(Clone)]
pub(crate) struct SlashParams<'a, T: 'a + Config> {
pub(crate) stash: &'a T::AccountId,
pub(crate) slash: Perbill,
pub(crate) prior_slash: Perbill,
pub(crate) exposure: &'a PagedExposure<T::AccountId, BalanceOf<T>>,
pub(crate) slash_era: EraIndex,
pub(crate) window_start: EraIndex,
pub(crate) now: EraIndex,
pub(crate) reward_proportion: Perbill,
}
#[derive(Clone, Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, RuntimeDebug)]
pub struct OffenceRecord<AccountId> {
pub reporter: Option<AccountId>,
pub reported_era: EraIndex,
pub exposure_page: u32,
pub slash_fraction: Perbill,
pub prior_slash_fraction: Perbill,
}
fn next_offence<T: Config>() -> Option<(EraIndex, T::AccountId, OffenceRecord<T::AccountId>)> {
let processing_offence = ProcessingOffence::<T>::get();
if let Some((offence_era, offender, offence_record)) = processing_offence {
if offence_record.exposure_page == 0 {
ProcessingOffence::<T>::kill();
return Some((offence_era, offender, offence_record))
}
ProcessingOffence::<T>::put((
offence_era,
&offender,
OffenceRecord {
exposure_page: offence_record.exposure_page.defensive_saturating_sub(1),
..offence_record.clone()
},
));
return Some((offence_era, offender, offence_record))
}
let Some(mut eras) = OffenceQueueEras::<T>::get() else { return None };
let Some(&oldest_era) = eras.first() else { return None };
let mut offence_iter = OffenceQueue::<T>::iter_prefix(oldest_era);
let next_offence = offence_iter.next();
if let Some((ref validator, ref offence_record)) = next_offence {
if offence_record.exposure_page > 0 {
ProcessingOffence::<T>::put((
oldest_era,
validator.clone(),
OffenceRecord {
exposure_page: offence_record.exposure_page.defensive_saturating_sub(1),
..offence_record.clone()
},
));
}
OffenceQueue::<T>::remove(oldest_era, &validator);
}
if offence_iter.next().is_none() {
if eras.len() == 1 {
OffenceQueueEras::<T>::kill();
} else {
eras.remove(0);
OffenceQueueEras::<T>::put(eras);
}
}
next_offence.map(|(v, o)| (oldest_era, v, o))
}
pub(crate) fn process_offence<T: Config>() -> Weight {
let mut consumed_weight = Weight::from_parts(0, 0);
let mut add_db_reads_writes = |reads, writes| {
consumed_weight += T::DbWeight::get().reads_writes(reads, writes);
};
add_db_reads_writes(1, 1);
let Some((offence_era, offender, offence_record)) = next_offence::<T>() else {
return consumed_weight
};
log!(
debug,
"🦹 Processing offence for {:?} in era {:?} with slash fraction {:?}",
offender,
offence_era,
offence_record.slash_fraction,
);
add_db_reads_writes(1, 0);
let reward_proportion = SlashRewardFraction::<T>::get();
add_db_reads_writes(2, 0);
let Some(exposure) =
EraInfo::<T>::get_paged_exposure(offence_era, &offender, offence_record.exposure_page)
else {
return consumed_weight
};
let slash_page = offence_record.exposure_page;
let slash_defer_duration = T::SlashDeferDuration::get();
let slash_era = offence_era.saturating_add(slash_defer_duration);
let window_start = offence_record.reported_era.saturating_sub(T::BondingDuration::get());
add_db_reads_writes(3, 3);
let Some(mut unapplied) = compute_slash::<T>(SlashParams {
stash: &offender,
slash: offence_record.slash_fraction,
prior_slash: offence_record.prior_slash_fraction,
exposure: &exposure,
slash_era: offence_era,
window_start,
now: offence_record.reported_era,
reward_proportion,
}) else {
log!(
debug,
"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is discarded, as could not compute slash",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
);
return consumed_weight
};
<Pallet<T>>::deposit_event(super::Event::<T>::SlashComputed {
offence_era,
slash_era,
offender: offender.clone(),
page: slash_page,
});
log!(
debug,
"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is computed",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
);
unapplied.reporter = offence_record.reporter;
if slash_defer_duration == 0 {
log!(
debug,
"🦹 applying slash instantly of {:?}% happened in {:?} (reported in {:?}) to {:?}",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
offender,
);
let accounts_slashed = unapplied.others.len() as u64 + 1;
add_db_reads_writes(3 * accounts_slashed, 3 * accounts_slashed);
apply_slash::<T>(unapplied, offence_era);
} else {
log!(
debug,
"🦹 deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
slash_era,
);
add_db_reads_writes(0, 1);
UnappliedSlashes::<T>::insert(
slash_era,
(offender, offence_record.slash_fraction, slash_page),
unapplied,
);
}
consumed_weight
}
pub(crate) fn compute_slash<T: Config>(params: SlashParams<T>) -> Option<UnappliedSlash<T>> {
let (val_slashed, mut reward_payout) = slash_validator::<T>(params.clone());
let mut nominators_slashed = Vec::new();
let (nom_slashed, nom_reward_payout) =
slash_nominators::<T>(params.clone(), &mut nominators_slashed);
reward_payout += nom_reward_payout;
(nom_slashed + val_slashed > Zero::zero()).then_some(UnappliedSlash {
validator: params.stash.clone(),
own: val_slashed,
others: WeakBoundedVec::force_from(
nominators_slashed,
Some("slashed nominators not expected to be larger than the bounds"),
),
reporter: None,
payout: reward_payout,
})
}
fn kick_out_if_recent<T: Config>(params: SlashParams<T>) {
let mut reward_payout = Zero::zero();
let mut val_slashed = Zero::zero();
let mut spans = fetch_spans::<T>(
params.stash,
params.window_start,
&mut reward_payout,
&mut val_slashed,
params.reward_proportion,
);
if spans.era_span(params.slash_era).map(|s| s.index) == Some(spans.span_index()) {
spans.end_span(params.now);
}
}
fn slash_validator<T: Config>(params: SlashParams<T>) -> (BalanceOf<T>, BalanceOf<T>) {
let own_slash = params.slash * params.exposure.exposure_metadata.own;
log!(
warn,
"🦹 slashing validator {:?} of stake: {:?} with {:?}% for {:?} in era {:?}",
params.stash,
params.exposure.exposure_metadata.own,
params.slash,
own_slash,
params.slash_era,
);
if own_slash == Zero::zero() {
kick_out_if_recent::<T>(params);
return (Zero::zero(), Zero::zero())
}
let mut reward_payout = Zero::zero();
let mut val_slashed = Zero::zero();
{
let mut spans = fetch_spans::<T>(
params.stash,
params.window_start,
&mut reward_payout,
&mut val_slashed,
params.reward_proportion,
);
let target_span = spans.compare_and_update_span_slash(params.slash_era, own_slash);
if target_span == Some(spans.span_index()) {
spans.end_span(params.now);
}
}
(val_slashed, reward_payout)
}
fn slash_nominators<T: Config>(
params: SlashParams<T>,
nominators_slashed: &mut Vec<(T::AccountId, BalanceOf<T>)>,
) -> (BalanceOf<T>, BalanceOf<T>) {
let mut reward_payout = BalanceOf::<T>::zero();
let mut total_slashed = BalanceOf::<T>::zero();
nominators_slashed.reserve(params.exposure.exposure_page.others.len());
for nominator in ¶ms.exposure.exposure_page.others {
let stash = &nominator.who;
let mut nom_slashed = Zero::zero();
let era_slash = {
let own_slash_prior = params.prior_slash * nominator.value;
let own_slash_by_validator = params.slash * nominator.value;
let own_slash_difference = own_slash_by_validator.saturating_sub(own_slash_prior);
let mut era_slash =
NominatorSlashInEra::<T>::get(¶ms.slash_era, stash).unwrap_or_else(Zero::zero);
era_slash += own_slash_difference;
NominatorSlashInEra::<T>::insert(¶ms.slash_era, stash, &era_slash);
era_slash
};
{
let mut spans = fetch_spans::<T>(
stash,
params.window_start,
&mut reward_payout,
&mut nom_slashed,
params.reward_proportion,
);
let target_span = spans.compare_and_update_span_slash(params.slash_era, era_slash);
if target_span == Some(spans.span_index()) {
spans.end_span(params.now);
}
}
nominators_slashed.push((stash.clone(), nom_slashed));
total_slashed.saturating_accrue(nom_slashed);
}
(total_slashed, reward_payout)
}
struct InspectingSpans<'a, T: Config + 'a> {
dirty: bool,
window_start: EraIndex,
stash: &'a T::AccountId,
spans: SlashingSpans,
paid_out: &'a mut BalanceOf<T>,
slash_of: &'a mut BalanceOf<T>,
reward_proportion: Perbill,
_marker: core::marker::PhantomData<T>,
}
fn fetch_spans<'a, T: Config + 'a>(
stash: &'a T::AccountId,
window_start: EraIndex,
paid_out: &'a mut BalanceOf<T>,
slash_of: &'a mut BalanceOf<T>,
reward_proportion: Perbill,
) -> InspectingSpans<'a, T> {
let spans = crate::SlashingSpans::<T>::get(stash).unwrap_or_else(|| {
let spans = SlashingSpans::new(window_start);
crate::SlashingSpans::<T>::insert(stash, &spans);
spans
});
InspectingSpans {
dirty: false,
window_start,
stash,
spans,
slash_of,
paid_out,
reward_proportion,
_marker: core::marker::PhantomData,
}
}
impl<'a, T: 'a + Config> InspectingSpans<'a, T> {
fn span_index(&self) -> SpanIndex {
self.spans.span_index
}
fn end_span(&mut self, now: EraIndex) {
self.dirty = self.spans.end_span(now) || self.dirty;
}
fn add_slash(&mut self, amount: BalanceOf<T>, slash_era: EraIndex) {
*self.slash_of += amount;
self.spans.last_nonzero_slash = core::cmp::max(self.spans.last_nonzero_slash, slash_era);
}
fn era_span(&self, era: EraIndex) -> Option<SlashingSpan> {
self.spans.iter().find(|span| span.contains_era(era))
}
fn compare_and_update_span_slash(
&mut self,
slash_era: EraIndex,
slash: BalanceOf<T>,
) -> Option<SpanIndex> {
let target_span = self.era_span(slash_era)?;
let span_slash_key = (self.stash.clone(), target_span.index);
let mut span_record = SpanSlash::<T>::get(&span_slash_key);
let mut changed = false;
let reward = if span_record.slashed < slash {
let difference = slash.defensive_saturating_sub(span_record.slashed);
span_record.slashed = slash;
let reward =
REWARD_F1 * (self.reward_proportion * slash).saturating_sub(span_record.paid_out);
self.add_slash(difference, slash_era);
changed = true;
reward
} else if span_record.slashed == slash {
REWARD_F1 * (self.reward_proportion * slash).saturating_sub(span_record.paid_out)
} else {
Zero::zero()
};
if !reward.is_zero() {
changed = true;
span_record.paid_out += reward;
*self.paid_out += reward;
}
if changed {
self.dirty = true;
SpanSlash::<T>::insert(&span_slash_key, &span_record);
}
Some(target_span.index)
}
}
impl<'a, T: 'a + Config> Drop for InspectingSpans<'a, T> {
fn drop(&mut self) {
if !self.dirty {
return
}
if let Some((start, end)) = self.spans.prune(self.window_start) {
for span_index in start..end {
SpanSlash::<T>::remove(&(self.stash.clone(), span_index));
}
}
crate::SlashingSpans::<T>::insert(self.stash, &self.spans);
}
}
pub(crate) fn clear_era_metadata<T: Config>(obsolete_era: EraIndex) {
#[allow(deprecated)]
ValidatorSlashInEra::<T>::remove_prefix(&obsolete_era, None);
#[allow(deprecated)]
NominatorSlashInEra::<T>::remove_prefix(&obsolete_era, None);
}
pub(crate) fn clear_stash_metadata<T: Config>(
stash: &T::AccountId,
num_slashing_spans: u32,
) -> DispatchResult {
let spans = match crate::SlashingSpans::<T>::get(stash) {
None => return Ok(()),
Some(s) => s,
};
ensure!(
num_slashing_spans as usize >= spans.iter().count(),
Error::<T>::IncorrectSlashingSpans
);
crate::SlashingSpans::<T>::remove(stash);
for span in spans.iter() {
SpanSlash::<T>::remove(&(stash.clone(), span.index));
}
Ok(())
}
pub fn do_slash<T: Config>(
stash: &T::AccountId,
value: BalanceOf<T>,
reward_payout: &mut BalanceOf<T>,
slashed_imbalance: &mut NegativeImbalanceOf<T>,
slash_era: EraIndex,
) {
let mut ledger =
match Pallet::<T>::ledger(sp_staking::StakingAccount::Stash(stash.clone())).defensive() {
Ok(ledger) => ledger,
Err(_) => return, };
let value = ledger.slash(value, asset::existential_deposit::<T>(), slash_era);
if value.is_zero() {
return
}
if !Pallet::<T>::is_virtual_staker(stash) {
let (imbalance, missing) = asset::slash::<T>(stash, value);
slashed_imbalance.subsume(imbalance);
if !missing.is_zero() {
*reward_payout = reward_payout.saturating_sub(missing);
}
}
let _ = ledger
.update()
.defensive_proof("ledger fetched from storage so it exists in storage; qed.");
<Pallet<T>>::deposit_event(super::Event::<T>::Slashed { staker: stash.clone(), amount: value });
}
pub(crate) fn apply_slash<T: Config>(unapplied_slash: UnappliedSlash<T>, slash_era: EraIndex) {
let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero();
let mut reward_payout = unapplied_slash.payout;
if unapplied_slash.own > Zero::zero() {
do_slash::<T>(
&unapplied_slash.validator,
unapplied_slash.own,
&mut reward_payout,
&mut slashed_imbalance,
slash_era,
);
}
for &(ref nominator, nominator_slash) in &unapplied_slash.others {
if nominator_slash.is_zero() {
continue
}
do_slash::<T>(
nominator,
nominator_slash,
&mut reward_payout,
&mut slashed_imbalance,
slash_era,
);
}
pay_reporters::<T>(
reward_payout,
slashed_imbalance,
&unapplied_slash.reporter.map(|v| crate::vec![v]).unwrap_or_default(),
);
}
fn pay_reporters<T: Config>(
reward_payout: BalanceOf<T>,
slashed_imbalance: NegativeImbalanceOf<T>,
reporters: &[T::AccountId],
) {
if reward_payout.is_zero() || reporters.is_empty() {
T::Slash::on_unbalanced(slashed_imbalance);
return
}
let reward_payout = reward_payout.min(slashed_imbalance.peek());
let (mut reward_payout, mut value_slashed) = slashed_imbalance.split(reward_payout);
let per_reporter = reward_payout.peek() / (reporters.len() as u32).into();
for reporter in reporters {
let (reporter_reward, rest) = reward_payout.split(per_reporter);
reward_payout = rest;
asset::deposit_slashed::<T>(reporter, reporter_reward);
}
value_slashed.subsume(reward_payout); T::Slash::on_unbalanced(value_slashed);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn span_contains_era() {
let span = SlashingSpan { index: 0, start: 1000, length: None };
assert!(!span.contains_era(0));
assert!(!span.contains_era(999));
assert!(span.contains_era(1000));
assert!(span.contains_era(1001));
assert!(span.contains_era(10000));
let span = SlashingSpan { index: 0, start: 1000, length: Some(10) };
assert!(!span.contains_era(0));
assert!(!span.contains_era(999));
assert!(span.contains_era(1000));
assert!(span.contains_era(1001));
assert!(span.contains_era(1009));
assert!(!span.contains_era(1010));
assert!(!span.contains_era(1011));
}
#[test]
fn single_slashing_span() {
let spans = SlashingSpans {
span_index: 0,
last_start: 1000,
last_nonzero_slash: 0,
prior: Vec::new(),
};
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![SlashingSpan { index: 0, start: 1000, length: None }],
);
}
#[test]
fn many_prior_spans() {
let spans = SlashingSpans {
span_index: 10,
last_start: 1000,
last_nonzero_slash: 0,
prior: vec![10, 9, 8, 10],
};
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 10, start: 1000, length: None },
SlashingSpan { index: 9, start: 990, length: Some(10) },
SlashingSpan { index: 8, start: 981, length: Some(9) },
SlashingSpan { index: 7, start: 973, length: Some(8) },
SlashingSpan { index: 6, start: 963, length: Some(10) },
],
)
}
#[test]
fn pruning_spans() {
let mut spans = SlashingSpans {
span_index: 10,
last_start: 1000,
last_nonzero_slash: 0,
prior: vec![10, 9, 8, 10],
};
assert_eq!(spans.prune(981), Some((6, 8)));
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 10, start: 1000, length: None },
SlashingSpan { index: 9, start: 990, length: Some(10) },
SlashingSpan { index: 8, start: 981, length: Some(9) },
],
);
assert_eq!(spans.prune(982), None);
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 10, start: 1000, length: None },
SlashingSpan { index: 9, start: 990, length: Some(10) },
SlashingSpan { index: 8, start: 981, length: Some(9) },
],
);
assert_eq!(spans.prune(989), None);
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 10, start: 1000, length: None },
SlashingSpan { index: 9, start: 990, length: Some(10) },
SlashingSpan { index: 8, start: 981, length: Some(9) },
],
);
assert_eq!(spans.prune(1000), Some((8, 10)));
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![SlashingSpan { index: 10, start: 1000, length: None },],
);
assert_eq!(spans.prune(2000), None);
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![SlashingSpan { index: 10, start: 2000, length: None },],
);
let mut spans = SlashingSpans {
span_index: 10,
last_start: 1000,
last_nonzero_slash: 0,
prior: vec![10, 9, 8, 10],
};
assert_eq!(spans.prune(2000), Some((6, 10)));
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![SlashingSpan { index: 10, start: 2000, length: None },],
);
}
#[test]
fn ending_span() {
let mut spans = SlashingSpans {
span_index: 1,
last_start: 10,
last_nonzero_slash: 0,
prior: Vec::new(),
};
assert!(spans.end_span(10));
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 2, start: 11, length: None },
SlashingSpan { index: 1, start: 10, length: Some(1) },
],
);
assert!(spans.end_span(15));
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 3, start: 16, length: None },
SlashingSpan { index: 2, start: 11, length: Some(5) },
SlashingSpan { index: 1, start: 10, length: Some(1) },
],
);
assert!(!spans.end_span(15));
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 3, start: 16, length: None },
SlashingSpan { index: 2, start: 11, length: Some(5) },
SlashingSpan { index: 1, start: 10, length: Some(1) },
],
);
}
}