referrerpolicy=no-referrer-when-downgrade

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