pallet_staking/
slashing.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! A slashing implementation for NPoS systems.
19//!
20//! For the purposes of the economic model, it is easiest to think of each validator as a nominator
21//! which nominates only its own identity.
22//!
23//! The act of nomination signals intent to unify economic identity with the validator - to take
24//! part in the rewards of a job well done, and to take part in the punishment of a job done badly.
25//!
26//! There are 3 main difficulties to account for with slashing in NPoS:
27//!   - A nominator can nominate multiple validators and be slashed via any of them.
28//!   - Until slashed, stake is reused from era to era. Nominating with N coins for E eras in a row
29//!     does not mean you have N*E coins to be slashed - you've only ever had N.
30//!   - Slashable offences can be found after the fact and out of order.
31//!
32//! The algorithm implemented in this module tries to balance these 3 difficulties.
33//!
34//! First, we only slash participants for the _maximum_ slash they receive in some time period,
35//! rather than the sum. This ensures a protection from overslashing.
36//!
37//! Second, we do not want the time period (or "span") that the maximum is computed
38//! over to last indefinitely. That would allow participants to begin acting with
39//! impunity after some point, fearing no further repercussions. For that reason, we
40//! automatically "chill" validators and withdraw a nominator's nomination after a slashing event,
41//! requiring them to re-enlist voluntarily (acknowledging the slash) and begin a new
42//! slashing span.
43//!
44//! Typically, you will have a single slashing event per slashing span. Only in the case
45//! where a validator releases many misbehaviors at once, or goes "back in time" to misbehave in
46//! eras that have already passed, would you encounter situations where a slashing span
47//! has multiple misbehaviors. However, accounting for such cases is necessary
48//! to deter a class of "rage-quit" attacks.
49//!
50//! Based on research at <https://research.web3.foundation/en/latest/polkadot/slashing/npos.html>
51
52use crate::{
53	asset, BalanceOf, Config, DisabledValidators, DisablingStrategy, Error, Exposure,
54	NegativeImbalanceOf, NominatorSlashInEra, Pallet, Perbill, SessionInterface, SpanSlash,
55	UnappliedSlash, ValidatorSlashInEra,
56};
57use alloc::vec::Vec;
58use codec::{Decode, Encode, MaxEncodedLen};
59use frame_support::{
60	ensure,
61	traits::{Defensive, DefensiveSaturating, Imbalance, OnUnbalanced},
62};
63use scale_info::TypeInfo;
64use sp_runtime::{
65	traits::{Saturating, Zero},
66	DispatchResult, RuntimeDebug,
67};
68use sp_staking::{EraIndex, StakingInterface};
69
70/// The proportion of the slashing reward to be paid out on the first slashing detection.
71/// This is f_1 in the paper.
72const REWARD_F1: Perbill = Perbill::from_percent(50);
73
74/// The index of a slashing span - unique to each stash.
75pub type SpanIndex = u32;
76
77// A range of start..end eras for a slashing span.
78#[derive(Encode, Decode, TypeInfo)]
79#[cfg_attr(test, derive(Debug, PartialEq))]
80pub(crate) struct SlashingSpan {
81	pub(crate) index: SpanIndex,
82	pub(crate) start: EraIndex,
83	pub(crate) length: Option<EraIndex>, // the ongoing slashing span has indeterminate length.
84}
85
86impl SlashingSpan {
87	fn contains_era(&self, era: EraIndex) -> bool {
88		self.start <= era && self.length.map_or(true, |l| self.start.saturating_add(l) > era)
89	}
90}
91
92/// An encoding of all of a nominator's slashing spans.
93#[derive(Encode, Decode, RuntimeDebug, TypeInfo)]
94pub struct SlashingSpans {
95	// the index of the current slashing span of the nominator. different for
96	// every stash, resets when the account hits free balance 0.
97	span_index: SpanIndex,
98	// the start era of the most recent (ongoing) slashing span.
99	last_start: EraIndex,
100	// the last era at which a non-zero slash occurred.
101	last_nonzero_slash: EraIndex,
102	// all prior slashing spans' start indices, in reverse order (most recent first)
103	// encoded as offsets relative to the slashing span after it.
104	prior: Vec<EraIndex>,
105}
106
107impl SlashingSpans {
108	// creates a new record of slashing spans for a stash, starting at the beginning
109	// of the bonding period, relative to now.
110	pub(crate) fn new(window_start: EraIndex) -> Self {
111		SlashingSpans {
112			span_index: 0,
113			last_start: window_start,
114			// initialize to zero, as this structure is lazily created until
115			// the first slash is applied. setting equal to `window_start` would
116			// put a time limit on nominations.
117			last_nonzero_slash: 0,
118			prior: Vec::new(),
119		}
120	}
121
122	// update the slashing spans to reflect the start of a new span at the era after `now`
123	// returns `true` if a new span was started, `false` otherwise. `false` indicates
124	// that internal state is unchanged.
125	pub(crate) fn end_span(&mut self, now: EraIndex) -> bool {
126		let next_start = now.defensive_saturating_add(1);
127		if next_start <= self.last_start {
128			return false
129		}
130
131		let last_length = next_start.defensive_saturating_sub(self.last_start);
132		self.prior.insert(0, last_length);
133		self.last_start = next_start;
134		self.span_index.defensive_saturating_accrue(1);
135		true
136	}
137
138	// an iterator over all slashing spans in _reverse_ order - most recent first.
139	pub(crate) fn iter(&'_ self) -> impl Iterator<Item = SlashingSpan> + '_ {
140		let mut last_start = self.last_start;
141		let mut index = self.span_index;
142		let last = SlashingSpan { index, start: last_start, length: None };
143		let prior = self.prior.iter().cloned().map(move |length| {
144			let start = last_start.defensive_saturating_sub(length);
145			last_start = start;
146			index.defensive_saturating_reduce(1);
147
148			SlashingSpan { index, start, length: Some(length) }
149		});
150
151		core::iter::once(last).chain(prior)
152	}
153
154	/// Yields the era index where the most recent non-zero slash occurred.
155	pub fn last_nonzero_slash(&self) -> EraIndex {
156		self.last_nonzero_slash
157	}
158
159	// prune the slashing spans against a window, whose start era index is given.
160	//
161	// If this returns `Some`, then it includes a range start..end of all the span
162	// indices which were pruned.
163	fn prune(&mut self, window_start: EraIndex) -> Option<(SpanIndex, SpanIndex)> {
164		let old_idx = self
165			.iter()
166			.skip(1) // skip ongoing span.
167			.position(|span| {
168				span.length
169					.map_or(false, |len| span.start.defensive_saturating_add(len) <= window_start)
170			});
171
172		let earliest_span_index =
173			self.span_index.defensive_saturating_sub(self.prior.len() as SpanIndex);
174		let pruned = match old_idx {
175			Some(o) => {
176				self.prior.truncate(o);
177				let new_earliest =
178					self.span_index.defensive_saturating_sub(self.prior.len() as SpanIndex);
179				Some((earliest_span_index, new_earliest))
180			},
181			None => None,
182		};
183
184		// readjust the ongoing span, if it started before the beginning of the window.
185		self.last_start = core::cmp::max(self.last_start, window_start);
186		pruned
187	}
188}
189
190/// A slashing-span record for a particular stash.
191#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]
192pub(crate) struct SpanRecord<Balance> {
193	slashed: Balance,
194	paid_out: Balance,
195}
196
197impl<Balance> SpanRecord<Balance> {
198	/// The value of stash balance slashed in this span.
199	#[cfg(test)]
200	pub(crate) fn amount(&self) -> &Balance {
201		&self.slashed
202	}
203}
204
205/// Parameters for performing a slash.
206#[derive(Clone)]
207pub(crate) struct SlashParams<'a, T: 'a + Config> {
208	/// The stash account being slashed.
209	pub(crate) stash: &'a T::AccountId,
210	/// The proportion of the slash.
211	pub(crate) slash: Perbill,
212	/// The exposure of the stash and all nominators.
213	pub(crate) exposure: &'a Exposure<T::AccountId, BalanceOf<T>>,
214	/// The era where the offence occurred.
215	pub(crate) slash_era: EraIndex,
216	/// The first era in the current bonding period.
217	pub(crate) window_start: EraIndex,
218	/// The current era.
219	pub(crate) now: EraIndex,
220	/// The maximum percentage of a slash that ever gets paid out.
221	/// This is f_inf in the paper.
222	pub(crate) reward_proportion: Perbill,
223}
224
225/// Computes a slash of a validator and nominators. It returns an unapplied
226/// record to be applied at some later point. Slashing metadata is updated in storage,
227/// since unapplied records are only rarely intended to be dropped.
228///
229/// The pending slash record returned does not have initialized reporters. Those have
230/// to be set at a higher level, if any.
231pub(crate) fn compute_slash<T: Config>(
232	params: SlashParams<T>,
233) -> Option<UnappliedSlash<T::AccountId, BalanceOf<T>>> {
234	let mut reward_payout = Zero::zero();
235	let mut val_slashed = Zero::zero();
236
237	// is the slash amount here a maximum for the era?
238	let own_slash = params.slash * params.exposure.own;
239	if params.slash * params.exposure.total == Zero::zero() {
240		// kick out the validator even if they won't be slashed,
241		// as long as the misbehavior is from their most recent slashing span.
242		kick_out_if_recent::<T>(params);
243		return None
244	}
245
246	let prior_slash_p = ValidatorSlashInEra::<T>::get(&params.slash_era, params.stash)
247		.map_or(Zero::zero(), |(prior_slash_proportion, _)| prior_slash_proportion);
248
249	// compare slash proportions rather than slash values to avoid issues due to rounding
250	// error.
251	if params.slash.deconstruct() > prior_slash_p.deconstruct() {
252		ValidatorSlashInEra::<T>::insert(
253			&params.slash_era,
254			params.stash,
255			&(params.slash, own_slash),
256		);
257	} else {
258		// we slash based on the max in era - this new event is not the max,
259		// so neither the validator or any nominators will need an update.
260		//
261		// this does lead to a divergence of our system from the paper, which
262		// pays out some reward even if the latest report is not max-in-era.
263		// we opt to avoid the nominator lookups and edits and leave more rewards
264		// for more drastic misbehavior.
265		return None
266	}
267
268	// apply slash to validator.
269	{
270		let mut spans = fetch_spans::<T>(
271			params.stash,
272			params.window_start,
273			&mut reward_payout,
274			&mut val_slashed,
275			params.reward_proportion,
276		);
277
278		let target_span = spans.compare_and_update_span_slash(params.slash_era, own_slash);
279
280		if target_span == Some(spans.span_index()) {
281			// misbehavior occurred within the current slashing span - end current span.
282			// Check <https://github.com/paritytech/polkadot-sdk/issues/2650> for details.
283			spans.end_span(params.now);
284		}
285	}
286
287	add_offending_validator::<T>(&params);
288
289	let mut nominators_slashed = Vec::new();
290	reward_payout += slash_nominators::<T>(params.clone(), prior_slash_p, &mut nominators_slashed);
291
292	Some(UnappliedSlash {
293		validator: params.stash.clone(),
294		own: val_slashed,
295		others: nominators_slashed,
296		reporters: Vec::new(),
297		payout: reward_payout,
298	})
299}
300
301// doesn't apply any slash, but kicks out the validator if the misbehavior is from
302// the most recent slashing span.
303fn kick_out_if_recent<T: Config>(params: SlashParams<T>) {
304	// these are not updated by era-span or end-span.
305	let mut reward_payout = Zero::zero();
306	let mut val_slashed = Zero::zero();
307	let mut spans = fetch_spans::<T>(
308		params.stash,
309		params.window_start,
310		&mut reward_payout,
311		&mut val_slashed,
312		params.reward_proportion,
313	);
314
315	if spans.era_span(params.slash_era).map(|s| s.index) == Some(spans.span_index()) {
316		// Check https://github.com/paritytech/polkadot-sdk/issues/2650 for details
317		spans.end_span(params.now);
318	}
319
320	add_offending_validator::<T>(&params);
321}
322
323/// Inform the [`DisablingStrategy`] implementation about the new offender and disable the list of
324/// validators provided by [`make_disabling_decision`].
325fn add_offending_validator<T: Config>(params: &SlashParams<T>) {
326	DisabledValidators::<T>::mutate(|disabled| {
327		if let Some(offender) =
328			T::DisablingStrategy::decision(params.stash, params.slash_era, &disabled)
329		{
330			// Add the validator to `DisabledValidators` and disable it. Do nothing if it is
331			// already disabled.
332			if let Err(index) = disabled.binary_search_by_key(&offender, |index| *index) {
333				disabled.insert(index, offender);
334				T::SessionInterface::disable_validator(offender);
335			}
336		}
337	});
338
339	// `DisabledValidators` should be kept sorted
340	debug_assert!(DisabledValidators::<T>::get().windows(2).all(|pair| pair[0] < pair[1]));
341}
342
343/// Slash nominators. Accepts general parameters and the prior slash percentage of the validator.
344///
345/// Returns the amount of reward to pay out.
346fn slash_nominators<T: Config>(
347	params: SlashParams<T>,
348	prior_slash_p: Perbill,
349	nominators_slashed: &mut Vec<(T::AccountId, BalanceOf<T>)>,
350) -> BalanceOf<T> {
351	let mut reward_payout = Zero::zero();
352
353	nominators_slashed.reserve(params.exposure.others.len());
354	for nominator in &params.exposure.others {
355		let stash = &nominator.who;
356		let mut nom_slashed = Zero::zero();
357
358		// the era slash of a nominator always grows, if the validator
359		// had a new max slash for the era.
360		let era_slash = {
361			let own_slash_prior = prior_slash_p * nominator.value;
362			let own_slash_by_validator = params.slash * nominator.value;
363			let own_slash_difference = own_slash_by_validator.saturating_sub(own_slash_prior);
364
365			let mut era_slash =
366				NominatorSlashInEra::<T>::get(&params.slash_era, stash).unwrap_or_else(Zero::zero);
367			era_slash += own_slash_difference;
368			NominatorSlashInEra::<T>::insert(&params.slash_era, stash, &era_slash);
369
370			era_slash
371		};
372
373		// compare the era slash against other eras in the same span.
374		{
375			let mut spans = fetch_spans::<T>(
376				stash,
377				params.window_start,
378				&mut reward_payout,
379				&mut nom_slashed,
380				params.reward_proportion,
381			);
382
383			let target_span = spans.compare_and_update_span_slash(params.slash_era, era_slash);
384
385			if target_span == Some(spans.span_index()) {
386				// end the span, but don't chill the nominator.
387				spans.end_span(params.now);
388			}
389		}
390		nominators_slashed.push((stash.clone(), nom_slashed));
391	}
392
393	reward_payout
394}
395
396// helper struct for managing a set of spans we are currently inspecting.
397// writes alterations to disk on drop, but only if a slash has been carried out.
398//
399// NOTE: alterations to slashing metadata should not be done after this is dropped.
400// dropping this struct applies any necessary slashes, which can lead to free balance
401// being 0, and the account being garbage-collected -- a dead account should get no new
402// metadata.
403struct InspectingSpans<'a, T: Config + 'a> {
404	dirty: bool,
405	window_start: EraIndex,
406	stash: &'a T::AccountId,
407	spans: SlashingSpans,
408	paid_out: &'a mut BalanceOf<T>,
409	slash_of: &'a mut BalanceOf<T>,
410	reward_proportion: Perbill,
411	_marker: core::marker::PhantomData<T>,
412}
413
414// fetches the slashing spans record for a stash account, initializing it if necessary.
415fn fetch_spans<'a, T: Config + 'a>(
416	stash: &'a T::AccountId,
417	window_start: EraIndex,
418	paid_out: &'a mut BalanceOf<T>,
419	slash_of: &'a mut BalanceOf<T>,
420	reward_proportion: Perbill,
421) -> InspectingSpans<'a, T> {
422	let spans = crate::SlashingSpans::<T>::get(stash).unwrap_or_else(|| {
423		let spans = SlashingSpans::new(window_start);
424		crate::SlashingSpans::<T>::insert(stash, &spans);
425		spans
426	});
427
428	InspectingSpans {
429		dirty: false,
430		window_start,
431		stash,
432		spans,
433		slash_of,
434		paid_out,
435		reward_proportion,
436		_marker: core::marker::PhantomData,
437	}
438}
439
440impl<'a, T: 'a + Config> InspectingSpans<'a, T> {
441	fn span_index(&self) -> SpanIndex {
442		self.spans.span_index
443	}
444
445	fn end_span(&mut self, now: EraIndex) {
446		self.dirty = self.spans.end_span(now) || self.dirty;
447	}
448
449	// add some value to the slash of the staker.
450	// invariant: the staker is being slashed for non-zero value here
451	// although `amount` may be zero, as it is only a difference.
452	fn add_slash(&mut self, amount: BalanceOf<T>, slash_era: EraIndex) {
453		*self.slash_of += amount;
454		self.spans.last_nonzero_slash = core::cmp::max(self.spans.last_nonzero_slash, slash_era);
455	}
456
457	// find the span index of the given era, if covered.
458	fn era_span(&self, era: EraIndex) -> Option<SlashingSpan> {
459		self.spans.iter().find(|span| span.contains_era(era))
460	}
461
462	// compares the slash in an era to the overall current span slash.
463	// if it's higher, applies the difference of the slashes and then updates the span on disk.
464	//
465	// returns the span index of the era where the slash occurred, if any.
466	fn compare_and_update_span_slash(
467		&mut self,
468		slash_era: EraIndex,
469		slash: BalanceOf<T>,
470	) -> Option<SpanIndex> {
471		let target_span = self.era_span(slash_era)?;
472		let span_slash_key = (self.stash.clone(), target_span.index);
473		let mut span_record = SpanSlash::<T>::get(&span_slash_key);
474		let mut changed = false;
475
476		let reward = if span_record.slashed < slash {
477			// new maximum span slash. apply the difference.
478			let difference = slash.defensive_saturating_sub(span_record.slashed);
479			span_record.slashed = slash;
480
481			// compute reward.
482			let reward =
483				REWARD_F1 * (self.reward_proportion * slash).saturating_sub(span_record.paid_out);
484
485			self.add_slash(difference, slash_era);
486			changed = true;
487
488			reward
489		} else if span_record.slashed == slash {
490			// compute reward. no slash difference to apply.
491			REWARD_F1 * (self.reward_proportion * slash).saturating_sub(span_record.paid_out)
492		} else {
493			Zero::zero()
494		};
495
496		if !reward.is_zero() {
497			changed = true;
498			span_record.paid_out += reward;
499			*self.paid_out += reward;
500		}
501
502		if changed {
503			self.dirty = true;
504			SpanSlash::<T>::insert(&span_slash_key, &span_record);
505		}
506
507		Some(target_span.index)
508	}
509}
510
511impl<'a, T: 'a + Config> Drop for InspectingSpans<'a, T> {
512	fn drop(&mut self) {
513		// only update on disk if we slashed this account.
514		if !self.dirty {
515			return
516		}
517
518		if let Some((start, end)) = self.spans.prune(self.window_start) {
519			for span_index in start..end {
520				SpanSlash::<T>::remove(&(self.stash.clone(), span_index));
521			}
522		}
523
524		crate::SlashingSpans::<T>::insert(self.stash, &self.spans);
525	}
526}
527
528/// Clear slashing metadata for an obsolete era.
529pub(crate) fn clear_era_metadata<T: Config>(obsolete_era: EraIndex) {
530	#[allow(deprecated)]
531	ValidatorSlashInEra::<T>::remove_prefix(&obsolete_era, None);
532	#[allow(deprecated)]
533	NominatorSlashInEra::<T>::remove_prefix(&obsolete_era, None);
534}
535
536/// Clear slashing metadata for a dead account.
537pub(crate) fn clear_stash_metadata<T: Config>(
538	stash: &T::AccountId,
539	num_slashing_spans: u32,
540) -> DispatchResult {
541	let spans = match crate::SlashingSpans::<T>::get(stash) {
542		None => return Ok(()),
543		Some(s) => s,
544	};
545
546	ensure!(
547		num_slashing_spans as usize >= spans.iter().count(),
548		Error::<T>::IncorrectSlashingSpans
549	);
550
551	crate::SlashingSpans::<T>::remove(stash);
552
553	// kill slashing-span metadata for account.
554	//
555	// this can only happen while the account is staked _if_ they are completely slashed.
556	// in that case, they may re-bond, but it would count again as span 0. Further ancient
557	// slashes would slash into this new bond, since metadata has now been cleared.
558	for span in spans.iter() {
559		SpanSlash::<T>::remove(&(stash.clone(), span.index));
560	}
561
562	Ok(())
563}
564
565// apply the slash to a stash account, deducting any missing funds from the reward
566// payout, saturating at 0. this is mildly unfair but also an edge-case that
567// can only occur when overlapping locked funds have been slashed.
568pub fn do_slash<T: Config>(
569	stash: &T::AccountId,
570	value: BalanceOf<T>,
571	reward_payout: &mut BalanceOf<T>,
572	slashed_imbalance: &mut NegativeImbalanceOf<T>,
573	slash_era: EraIndex,
574) {
575	let mut ledger =
576		match Pallet::<T>::ledger(sp_staking::StakingAccount::Stash(stash.clone())).defensive() {
577			Ok(ledger) => ledger,
578			Err(_) => return, // nothing to do.
579		};
580
581	let value = ledger.slash(value, asset::existential_deposit::<T>(), slash_era);
582	if value.is_zero() {
583		// nothing to do
584		return
585	}
586
587	// Skip slashing for virtual stakers. The pallets managing them should handle the slashing.
588	if !Pallet::<T>::is_virtual_staker(stash) {
589		let (imbalance, missing) = asset::slash::<T>(stash, value);
590		slashed_imbalance.subsume(imbalance);
591
592		if !missing.is_zero() {
593			// deduct overslash from the reward payout
594			*reward_payout = reward_payout.saturating_sub(missing);
595		}
596	}
597
598	let _ = ledger
599		.update()
600		.defensive_proof("ledger fetched from storage so it exists in storage; qed.");
601
602	// trigger the event
603	<Pallet<T>>::deposit_event(super::Event::<T>::Slashed { staker: stash.clone(), amount: value });
604}
605
606/// Apply a previously-unapplied slash.
607pub(crate) fn apply_slash<T: Config>(
608	unapplied_slash: UnappliedSlash<T::AccountId, BalanceOf<T>>,
609	slash_era: EraIndex,
610) {
611	let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero();
612	let mut reward_payout = unapplied_slash.payout;
613
614	do_slash::<T>(
615		&unapplied_slash.validator,
616		unapplied_slash.own,
617		&mut reward_payout,
618		&mut slashed_imbalance,
619		slash_era,
620	);
621
622	for &(ref nominator, nominator_slash) in &unapplied_slash.others {
623		do_slash::<T>(
624			nominator,
625			nominator_slash,
626			&mut reward_payout,
627			&mut slashed_imbalance,
628			slash_era,
629		);
630	}
631
632	pay_reporters::<T>(reward_payout, slashed_imbalance, &unapplied_slash.reporters);
633}
634
635/// Apply a reward payout to some reporters, paying the rewards out of the slashed imbalance.
636fn pay_reporters<T: Config>(
637	reward_payout: BalanceOf<T>,
638	slashed_imbalance: NegativeImbalanceOf<T>,
639	reporters: &[T::AccountId],
640) {
641	if reward_payout.is_zero() || reporters.is_empty() {
642		// nobody to pay out to or nothing to pay;
643		// just treat the whole value as slashed.
644		T::Slash::on_unbalanced(slashed_imbalance);
645		return
646	}
647
648	// take rewards out of the slashed imbalance.
649	let reward_payout = reward_payout.min(slashed_imbalance.peek());
650	let (mut reward_payout, mut value_slashed) = slashed_imbalance.split(reward_payout);
651
652	let per_reporter = reward_payout.peek() / (reporters.len() as u32).into();
653	for reporter in reporters {
654		let (reporter_reward, rest) = reward_payout.split(per_reporter);
655		reward_payout = rest;
656
657		// this cancels out the reporter reward imbalance internally, leading
658		// to no change in total issuance.
659		asset::deposit_slashed::<T>(reporter, reporter_reward);
660	}
661
662	// the rest goes to the on-slash imbalance handler (e.g. treasury)
663	value_slashed.subsume(reward_payout); // remainder of reward division remains.
664	T::Slash::on_unbalanced(value_slashed);
665}
666
667#[cfg(test)]
668mod tests {
669	use super::*;
670
671	#[test]
672	fn span_contains_era() {
673		// unbounded end
674		let span = SlashingSpan { index: 0, start: 1000, length: None };
675		assert!(!span.contains_era(0));
676		assert!(!span.contains_era(999));
677
678		assert!(span.contains_era(1000));
679		assert!(span.contains_era(1001));
680		assert!(span.contains_era(10000));
681
682		// bounded end - non-inclusive range.
683		let span = SlashingSpan { index: 0, start: 1000, length: Some(10) };
684		assert!(!span.contains_era(0));
685		assert!(!span.contains_era(999));
686
687		assert!(span.contains_era(1000));
688		assert!(span.contains_era(1001));
689		assert!(span.contains_era(1009));
690		assert!(!span.contains_era(1010));
691		assert!(!span.contains_era(1011));
692	}
693
694	#[test]
695	fn single_slashing_span() {
696		let spans = SlashingSpans {
697			span_index: 0,
698			last_start: 1000,
699			last_nonzero_slash: 0,
700			prior: Vec::new(),
701		};
702
703		assert_eq!(
704			spans.iter().collect::<Vec<_>>(),
705			vec![SlashingSpan { index: 0, start: 1000, length: None }],
706		);
707	}
708
709	#[test]
710	fn many_prior_spans() {
711		let spans = SlashingSpans {
712			span_index: 10,
713			last_start: 1000,
714			last_nonzero_slash: 0,
715			prior: vec![10, 9, 8, 10],
716		};
717
718		assert_eq!(
719			spans.iter().collect::<Vec<_>>(),
720			vec![
721				SlashingSpan { index: 10, start: 1000, length: None },
722				SlashingSpan { index: 9, start: 990, length: Some(10) },
723				SlashingSpan { index: 8, start: 981, length: Some(9) },
724				SlashingSpan { index: 7, start: 973, length: Some(8) },
725				SlashingSpan { index: 6, start: 963, length: Some(10) },
726			],
727		)
728	}
729
730	#[test]
731	fn pruning_spans() {
732		let mut spans = SlashingSpans {
733			span_index: 10,
734			last_start: 1000,
735			last_nonzero_slash: 0,
736			prior: vec![10, 9, 8, 10],
737		};
738
739		assert_eq!(spans.prune(981), Some((6, 8)));
740		assert_eq!(
741			spans.iter().collect::<Vec<_>>(),
742			vec![
743				SlashingSpan { index: 10, start: 1000, length: None },
744				SlashingSpan { index: 9, start: 990, length: Some(10) },
745				SlashingSpan { index: 8, start: 981, length: Some(9) },
746			],
747		);
748
749		assert_eq!(spans.prune(982), None);
750		assert_eq!(
751			spans.iter().collect::<Vec<_>>(),
752			vec![
753				SlashingSpan { index: 10, start: 1000, length: None },
754				SlashingSpan { index: 9, start: 990, length: Some(10) },
755				SlashingSpan { index: 8, start: 981, length: Some(9) },
756			],
757		);
758
759		assert_eq!(spans.prune(989), None);
760		assert_eq!(
761			spans.iter().collect::<Vec<_>>(),
762			vec![
763				SlashingSpan { index: 10, start: 1000, length: None },
764				SlashingSpan { index: 9, start: 990, length: Some(10) },
765				SlashingSpan { index: 8, start: 981, length: Some(9) },
766			],
767		);
768
769		assert_eq!(spans.prune(1000), Some((8, 10)));
770		assert_eq!(
771			spans.iter().collect::<Vec<_>>(),
772			vec![SlashingSpan { index: 10, start: 1000, length: None },],
773		);
774
775		assert_eq!(spans.prune(2000), None);
776		assert_eq!(
777			spans.iter().collect::<Vec<_>>(),
778			vec![SlashingSpan { index: 10, start: 2000, length: None },],
779		);
780
781		// now all in one shot.
782		let mut spans = SlashingSpans {
783			span_index: 10,
784			last_start: 1000,
785			last_nonzero_slash: 0,
786			prior: vec![10, 9, 8, 10],
787		};
788		assert_eq!(spans.prune(2000), Some((6, 10)));
789		assert_eq!(
790			spans.iter().collect::<Vec<_>>(),
791			vec![SlashingSpan { index: 10, start: 2000, length: None },],
792		);
793	}
794
795	#[test]
796	fn ending_span() {
797		let mut spans = SlashingSpans {
798			span_index: 1,
799			last_start: 10,
800			last_nonzero_slash: 0,
801			prior: Vec::new(),
802		};
803
804		assert!(spans.end_span(10));
805
806		assert_eq!(
807			spans.iter().collect::<Vec<_>>(),
808			vec![
809				SlashingSpan { index: 2, start: 11, length: None },
810				SlashingSpan { index: 1, start: 10, length: Some(1) },
811			],
812		);
813
814		assert!(spans.end_span(15));
815		assert_eq!(
816			spans.iter().collect::<Vec<_>>(),
817			vec![
818				SlashingSpan { index: 3, start: 16, length: None },
819				SlashingSpan { index: 2, start: 11, length: Some(5) },
820				SlashingSpan { index: 1, start: 10, length: Some(1) },
821			],
822		);
823
824		// does nothing if not a valid end.
825		assert!(!spans.end_span(15));
826		assert_eq!(
827			spans.iter().collect::<Vec<_>>(),
828			vec![
829				SlashingSpan { index: 3, start: 16, length: None },
830				SlashingSpan { index: 2, start: 11, length: Some(5) },
831				SlashingSpan { index: 1, start: 10, length: Some(1) },
832			],
833		);
834	}
835}