referrerpolicy=no-referrer-when-downgrade

polkadot_runtime_parachains/disputes/
slashing.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Dispute slashing pallet.
18//!
19//! Once a dispute is concluded, we want to slash validators who were on the
20//! wrong side of the dispute.
21//!
22//! A dispute should always result in an offence. There are 3 possible
23//! offence types:
24//! - `ForInvalidBacked`: A major offence when a validator backed an
25//! invalid block. Main source of economic security.
26//! - `ForInvalidApproved`: A medium offence when a validator approved (NOT backed) an
27//! invalid block. Protects from lazy validators.
28//! - `AgainstValid`: A minor offence when a validator disputed a valid block.
29//! Protects from spam attacks.
30//!
31//! Past session slashing edgecase:
32//!
33//! The `offences` pallet from Substrate provides us with a way to do both.
34//! Currently, the interface expects us to provide staking information including
35//! nominator exposure in order to submit an offence.
36//!
37//! Normally, we'd able to fetch this information from the runtime as soon as
38//! the dispute is concluded. However, since a dispute can conclude several
39//! sessions after the candidate was backed (see `dispute_period` in
40//! `HostConfiguration`), we can't rely on this information being available
41//! in the context of the current block. The `babe` and `grandpa` equivocation
42//! handlers also have to deal with this problem.
43//!
44//! Our implementation looks simillar to the `grandpa
45//! equivocation` handler. Meaning, we submit an `offence` for the concluded
46//! disputes about the current session candidate directly from the runtime. If,
47//! however, the dispute is about a past session, we record unapplied slashes on
48//! chain, without `FullIdentification` of the offenders. Later on, a block
49//! producer can submit an unsigned transaction with `KeyOwnershipProof` of an
50//! offender and submit it to the runtime to produce an offence.
51
52use crate::{disputes, initializer::ValidatorSetCount, session_info::IdentificationTuple};
53use frame_support::{
54	dispatch::Pays,
55	traits::{Defensive, Get, KeyOwnerProofSystem, ValidatorSet, ValidatorSetWithIdentification},
56	weights::Weight,
57};
58use frame_system::pallet_prelude::BlockNumberFor;
59
60use alloc::{
61	boxed::Box,
62	collections::{btree_map::Entry, btree_set::BTreeSet},
63	vec,
64	vec::Vec,
65};
66use polkadot_primitives::{
67	slashing::{DisputeProof, DisputesTimeSlot, PendingSlashes},
68	CandidateHash, DisputeOffenceKind, SessionIndex, ValidatorId, ValidatorIndex,
69};
70use scale_info::TypeInfo;
71use sp_runtime::{
72	traits::Convert,
73	transaction_validity::{
74		InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
75		TransactionValidityError, ValidTransaction,
76	},
77	KeyTypeId, Perbill,
78};
79use sp_session::{GetSessionNumber, GetValidatorCount};
80use sp_staking::offence::{Kind, Offence, OffenceError, ReportOffence};
81
82const LOG_TARGET: &str = "runtime::parachains::slashing";
83
84// These are constants, but we want to make them configurable
85// via `HostConfiguration` in the future.
86const SLASH_FOR_INVALID_BACKED: Perbill = Perbill::from_percent(100);
87const SLASH_FOR_INVALID_APPROVED: Perbill = Perbill::from_percent(2);
88const SLASH_AGAINST_VALID: Perbill = Perbill::zero();
89const DEFENSIVE_PROOF: &'static str = "disputes module should bail on old session";
90
91#[cfg(feature = "runtime-benchmarks")]
92pub mod benchmarking;
93
94#[cfg(test)]
95mod tests;
96
97/// The benchmarking configuration.
98pub trait BenchmarkingConfiguration {
99	const MAX_VALIDATORS: u32;
100}
101
102pub struct BenchConfig<const M: u32>;
103
104impl<const M: u32> BenchmarkingConfiguration for BenchConfig<M> {
105	const MAX_VALIDATORS: u32 = M;
106}
107
108/// An offence that is filed against the validators that lost a dispute.
109#[derive(TypeInfo)]
110#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))]
111pub struct SlashingOffence<KeyOwnerIdentification> {
112	/// The size of the validator set in that session.
113	pub validator_set_count: ValidatorSetCount,
114	/// Should be unique per dispute.
115	pub time_slot: DisputesTimeSlot,
116	/// Staking information about the validators that lost the dispute
117	/// needed for slashing.
118	pub offenders: Vec<KeyOwnerIdentification>,
119	/// What fraction of the total exposure that should be slashed for
120	/// this offence.
121	pub slash_fraction: Perbill,
122	/// The type of slashing offence.
123	pub kind: DisputeOffenceKind,
124}
125
126impl<Offender> Offence<Offender> for SlashingOffence<Offender>
127where
128	Offender: Clone,
129{
130	const ID: Kind = *b"disputes:slashin";
131
132	type TimeSlot = DisputesTimeSlot;
133
134	fn offenders(&self) -> Vec<Offender> {
135		self.offenders.clone()
136	}
137
138	fn session_index(&self) -> SessionIndex {
139		self.time_slot.session_index
140	}
141
142	fn validator_set_count(&self) -> ValidatorSetCount {
143		self.validator_set_count
144	}
145
146	fn time_slot(&self) -> Self::TimeSlot {
147		self.time_slot.clone()
148	}
149
150	fn slash_fraction(&self, _offenders: u32) -> Perbill {
151		self.slash_fraction
152	}
153}
154
155impl<KeyOwnerIdentification> SlashingOffence<KeyOwnerIdentification> {
156	fn new(
157		session_index: SessionIndex,
158		candidate_hash: CandidateHash,
159		validator_set_count: ValidatorSetCount,
160		offenders: Vec<KeyOwnerIdentification>,
161		kind: DisputeOffenceKind,
162	) -> Self {
163		let time_slot = DisputesTimeSlot::new(session_index, candidate_hash);
164		let slash_fraction = match kind {
165			DisputeOffenceKind::ForInvalidBacked => SLASH_FOR_INVALID_BACKED,
166			DisputeOffenceKind::ForInvalidApproved => SLASH_FOR_INVALID_APPROVED,
167			DisputeOffenceKind::AgainstValid => SLASH_AGAINST_VALID,
168		};
169		Self { time_slot, validator_set_count, offenders, slash_fraction, kind }
170	}
171}
172
173/// This type implements `SlashingHandler`.
174pub struct SlashValidatorsForDisputes<C> {
175	_phantom: core::marker::PhantomData<C>,
176}
177
178impl<C> Default for SlashValidatorsForDisputes<C> {
179	fn default() -> Self {
180		Self { _phantom: Default::default() }
181	}
182}
183
184impl<T> SlashValidatorsForDisputes<Pallet<T>>
185where
186	T: Config<KeyOwnerIdentification = IdentificationTuple<T>>,
187{
188	/// If in the current session, returns the identified validators. `None`
189	/// otherwise.
190	fn maybe_identify_validators(
191		session_index: SessionIndex,
192		validators: impl IntoIterator<Item = ValidatorIndex>,
193	) -> Option<Vec<IdentificationTuple<T>>> {
194		// We use `ValidatorSet::session_index` and not
195		// `shared::CurrentSessionIndex::<T>::get()` because at the first block of a new era,
196		// the `IdentificationOf` of a validator in the previous session might be
197		// missing, while `shared` pallet would return the same session index as being
198		// updated at the end of the block.
199		let current_session = T::ValidatorSet::session_index();
200		if session_index == current_session {
201			let account_keys = crate::session_info::AccountKeys::<T>::get(session_index);
202			let account_ids = account_keys.defensive_unwrap_or_default();
203
204			let fully_identified = validators
205				.into_iter()
206				.flat_map(|i| account_ids.get(i.0 as usize).cloned())
207				.filter_map(|id| {
208					<T::ValidatorSet as ValidatorSetWithIdentification<T::AccountId>>::IdentificationOf::convert(
209						id.clone()
210					).map(|full_id| (id, full_id))
211				})
212				.collect::<Vec<IdentificationTuple<T>>>();
213			return Some(fully_identified);
214		}
215		None
216	}
217
218	fn do_punish(
219		session_index: SessionIndex,
220		candidate_hash: CandidateHash,
221		kind: DisputeOffenceKind,
222		losers: impl IntoIterator<Item = ValidatorIndex>,
223	) {
224		let losers: BTreeSet<_> = losers.into_iter().collect();
225		if losers.is_empty() {
226			return;
227		}
228		let session_info = crate::session_info::Sessions::<T>::get(session_index);
229		let session_info = match session_info.defensive_proof(DEFENSIVE_PROOF) {
230			Some(info) => info,
231			None => return,
232		};
233
234		let maybe_offenders =
235			Self::maybe_identify_validators(session_index, losers.iter().cloned());
236		if let Some(offenders) = maybe_offenders {
237			let validator_set_count = session_info.discovery_keys.len() as ValidatorSetCount;
238			let offence = SlashingOffence::new(
239				session_index,
240				candidate_hash,
241				validator_set_count,
242				offenders,
243				kind,
244			);
245			// This is the first time we report an offence for this dispute,
246			// so it is not a duplicate.
247			let _ = T::HandleReports::report_offence(offence);
248			return;
249		}
250
251		let keys = losers
252			.into_iter()
253			.filter_map(|i| session_info.validators.get(i).cloned().map(|id| (i, id)))
254			.collect();
255		let unapplied = PendingSlashes { keys, kind };
256
257		let append = |old: &mut Option<PendingSlashes>| {
258			let old = old
259				.get_or_insert(PendingSlashes { keys: Default::default(), kind: unapplied.kind });
260			debug_assert_eq!(old.kind, unapplied.kind);
261
262			old.keys.extend(unapplied.keys)
263		};
264		<UnappliedSlashes<T>>::mutate(session_index, candidate_hash, append);
265	}
266}
267
268impl<T> disputes::SlashingHandler<BlockNumberFor<T>> for SlashValidatorsForDisputes<Pallet<T>>
269where
270	T: Config<KeyOwnerIdentification = IdentificationTuple<T>>,
271{
272	fn punish_for_invalid(
273		session_index: SessionIndex,
274		candidate_hash: CandidateHash,
275		losers: impl IntoIterator<Item = ValidatorIndex>,
276		backers: impl IntoIterator<Item = ValidatorIndex>,
277	) {
278		let losers: Vec<_> = losers.into_iter().collect();
279		let backers: BTreeSet<_> = backers.into_iter().collect();
280
281		if losers.is_empty() || backers.is_empty() {
282			return;
283		}
284
285		let (loosing_backers, loosing_approvers): (Vec<_>, Vec<_>) =
286			losers.into_iter().partition(|v| backers.contains(v));
287
288		if !loosing_backers.is_empty() {
289			Self::do_punish(
290				session_index,
291				candidate_hash,
292				DisputeOffenceKind::ForInvalidBacked,
293				loosing_backers,
294			);
295		}
296		if !loosing_approvers.is_empty() {
297			Self::do_punish(
298				session_index,
299				candidate_hash,
300				DisputeOffenceKind::ForInvalidApproved,
301				loosing_approvers,
302			);
303		}
304	}
305
306	fn punish_against_valid(
307		session_index: SessionIndex,
308		candidate_hash: CandidateHash,
309		losers: impl IntoIterator<Item = ValidatorIndex>,
310		_backers: impl IntoIterator<Item = ValidatorIndex>,
311	) {
312		let kind = DisputeOffenceKind::AgainstValid;
313		Self::do_punish(session_index, candidate_hash, kind, losers);
314	}
315
316	fn initializer_initialize(now: BlockNumberFor<T>) -> Weight {
317		Pallet::<T>::initializer_initialize(now)
318	}
319
320	fn initializer_finalize() {
321		Pallet::<T>::initializer_finalize()
322	}
323
324	fn initializer_on_new_session(session_index: SessionIndex) {
325		Pallet::<T>::initializer_on_new_session(session_index)
326	}
327}
328
329/// A trait that defines methods to report an offence (after the slashing report
330/// has been validated) and for submitting a transaction to report a slash (from
331/// an offchain context).
332pub trait HandleReports<T: Config> {
333	/// The longevity, in blocks, that the offence report is valid for. When
334	/// using the staking pallet this should be equal to the bonding duration
335	/// (in blocks, not eras).
336	type ReportLongevity: Get<u64>;
337
338	/// Report an offence.
339	fn report_offence(
340		offence: SlashingOffence<T::KeyOwnerIdentification>,
341	) -> Result<(), OffenceError>;
342
343	/// Returns true if the offenders at the given time slot has already been
344	/// reported.
345	fn is_known_offence(
346		offenders: &[T::KeyOwnerIdentification],
347		time_slot: &DisputesTimeSlot,
348	) -> bool;
349
350	/// Create and dispatch a slashing report extrinsic.
351	/// This should be called offchain.
352	fn submit_unsigned_slashing_report(
353		dispute_proof: DisputeProof,
354		key_owner_proof: T::KeyOwnerProof,
355	) -> Result<(), sp_runtime::TryRuntimeError>;
356}
357
358impl<T: Config> HandleReports<T> for () {
359	type ReportLongevity = ();
360
361	fn report_offence(
362		_offence: SlashingOffence<T::KeyOwnerIdentification>,
363	) -> Result<(), OffenceError> {
364		Ok(())
365	}
366
367	fn is_known_offence(
368		_offenders: &[T::KeyOwnerIdentification],
369		_time_slot: &DisputesTimeSlot,
370	) -> bool {
371		true
372	}
373
374	fn submit_unsigned_slashing_report(
375		_dispute_proof: DisputeProof,
376		_key_owner_proof: T::KeyOwnerProof,
377	) -> Result<(), sp_runtime::TryRuntimeError> {
378		Ok(())
379	}
380}
381
382pub trait WeightInfo {
383	fn report_dispute_lost_unsigned(validator_count: ValidatorSetCount) -> Weight;
384}
385
386pub struct TestWeightInfo;
387impl WeightInfo for TestWeightInfo {
388	fn report_dispute_lost_unsigned(_validator_count: ValidatorSetCount) -> Weight {
389		Weight::zero()
390	}
391}
392
393pub use pallet::*;
394#[frame_support::pallet]
395pub mod pallet {
396	use super::*;
397	use frame_support::pallet_prelude::*;
398	use frame_system::pallet_prelude::*;
399
400	#[pallet::config]
401	pub trait Config: frame_system::Config + crate::disputes::Config {
402		/// The proof of key ownership, used for validating slashing reports.
403		/// The proof must include the session index and validator count of the
404		/// session at which the offence occurred.
405		type KeyOwnerProof: Parameter + GetSessionNumber + GetValidatorCount;
406
407		/// The identification of a key owner, used when reporting slashes.
408		type KeyOwnerIdentification: Parameter;
409
410		/// A system for proving ownership of keys, i.e. that a given key was
411		/// part of a validator set, needed for validating slashing reports.
412		type KeyOwnerProofSystem: KeyOwnerProofSystem<
413			(KeyTypeId, ValidatorId),
414			Proof = Self::KeyOwnerProof,
415			IdentificationTuple = Self::KeyOwnerIdentification,
416		>;
417
418		/// The slashing report handling subsystem, defines methods to report an
419		/// offence (after the slashing report has been validated) and for
420		/// submitting a transaction to report a slash (from an offchain
421		/// context). NOTE: when enabling slashing report handling (i.e. this
422		/// type isn't set to `()`) you must use this pallet's
423		/// `ValidateUnsigned` in the runtime definition.
424		type HandleReports: HandleReports<Self>;
425
426		/// Weight information for extrinsics in this pallet.
427		type WeightInfo: WeightInfo;
428
429		/// Benchmarking configuration.
430		type BenchmarkingConfig: BenchmarkingConfiguration;
431	}
432
433	#[pallet::pallet]
434	#[pallet::without_storage_info]
435	pub struct Pallet<T>(_);
436
437	/// Validators pending dispute slashes.
438	#[pallet::storage]
439	pub(crate) type UnappliedSlashes<T> = StorageDoubleMap<
440		_,
441		Twox64Concat,
442		SessionIndex,
443		Blake2_128Concat,
444		CandidateHash,
445		PendingSlashes,
446	>;
447
448	/// `ValidatorSetCount` per session.
449	#[pallet::storage]
450	pub(super) type ValidatorSetCounts<T> =
451		StorageMap<_, Twox64Concat, SessionIndex, ValidatorSetCount>;
452
453	#[pallet::error]
454	pub enum Error<T> {
455		/// The key ownership proof is invalid.
456		InvalidKeyOwnershipProof,
457		/// The session index is too old or invalid.
458		InvalidSessionIndex,
459		/// The candidate hash is invalid.
460		InvalidCandidateHash,
461		/// There is no pending slash for the given validator index and time
462		/// slot.
463		InvalidValidatorIndex,
464		/// The validator index does not match the validator id.
465		ValidatorIndexIdMismatch,
466		/// The given slashing report is valid but already previously reported.
467		DuplicateSlashingReport,
468	}
469
470	#[pallet::call]
471	impl<T: Config> Pallet<T> {
472		#[pallet::call_index(0)]
473		#[pallet::weight(<T as Config>::WeightInfo::report_dispute_lost_unsigned(
474			key_owner_proof.validator_count()
475		))]
476		pub fn report_dispute_lost_unsigned(
477			origin: OriginFor<T>,
478			// box to decrease the size of the call
479			dispute_proof: Box<DisputeProof>,
480			key_owner_proof: T::KeyOwnerProof,
481		) -> DispatchResultWithPostInfo {
482			ensure_none(origin)?;
483			let validator_set_count = key_owner_proof.validator_count() as ValidatorSetCount;
484			let session_index = dispute_proof.time_slot.session_index;
485
486			// The membership proof must be for the same session as the dispute.
487			ensure!(
488				key_owner_proof.session() == session_index,
489				Error::<T>::InvalidKeyOwnershipProof,
490			);
491
492			let key =
493				(polkadot_primitives::PARACHAIN_KEY_TYPE_ID, dispute_proof.validator_id.clone());
494			let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof)
495				.ok_or(Error::<T>::InvalidKeyOwnershipProof)?;
496
497			// check that there is a pending slash for the given
498			// validator index and candidate hash
499			let candidate_hash = dispute_proof.time_slot.candidate_hash;
500			let try_remove = |v: &mut Option<PendingSlashes>| -> Result<(), DispatchError> {
501				let pending = v.as_mut().ok_or(Error::<T>::InvalidCandidateHash)?;
502				if pending.kind != dispute_proof.kind {
503					return Err(Error::<T>::InvalidCandidateHash.into());
504				}
505
506				match pending.keys.entry(dispute_proof.validator_index) {
507					Entry::Vacant(_) => return Err(Error::<T>::InvalidValidatorIndex.into()),
508					// check that `validator_index` matches `validator_id`
509					Entry::Occupied(e) if e.get() != &dispute_proof.validator_id => {
510						return Err(Error::<T>::ValidatorIndexIdMismatch.into())
511					},
512					Entry::Occupied(e) => {
513						e.remove(); // the report is correct
514					},
515				}
516
517				// if the last validator is slashed for this dispute, clean up the storage
518				if pending.keys.is_empty() {
519					*v = None;
520				}
521
522				Ok(())
523			};
524
525			<UnappliedSlashes<T>>::try_mutate_exists(&session_index, &candidate_hash, try_remove)?;
526
527			let offence = SlashingOffence::new(
528				session_index,
529				candidate_hash,
530				validator_set_count,
531				vec![offender],
532				dispute_proof.kind,
533			);
534
535			<T::HandleReports as HandleReports<T>>::report_offence(offence)
536				.map_err(|_| Error::<T>::DuplicateSlashingReport)?;
537
538			Ok(Pays::No.into())
539		}
540	}
541
542	#[allow(deprecated)]
543	#[pallet::validate_unsigned]
544	impl<T: Config> ValidateUnsigned for Pallet<T> {
545		type Call = Call<T>;
546		fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
547			Self::validate_unsigned(source, call)
548		}
549
550		fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
551			Self::pre_dispatch(call)
552		}
553	}
554}
555
556impl<T: Config> Pallet<T> {
557	/// Called by the initializer to initialize the disputes slashing module.
558	fn initializer_initialize(_now: BlockNumberFor<T>) -> Weight {
559		Weight::zero()
560	}
561
562	/// Called by the initializer to finalize the disputes slashing pallet.
563	fn initializer_finalize() {}
564
565	/// Called by the initializer to note a new session in the disputes slashing
566	/// pallet.
567	fn initializer_on_new_session(session_index: SessionIndex) {
568		// This should be small, as disputes are limited by spam slots, so no limit is
569		// fine.
570		const REMOVE_LIMIT: u32 = u32::MAX;
571
572		let config = crate::configuration::ActiveConfig::<T>::get();
573		if session_index <= config.dispute_period + 1 {
574			return;
575		}
576
577		let old_session = session_index - config.dispute_period - 1;
578		let _ = <UnappliedSlashes<T>>::clear_prefix(old_session, REMOVE_LIMIT, None);
579	}
580
581	pub(crate) fn unapplied_slashes() -> Vec<(SessionIndex, CandidateHash, PendingSlashes)> {
582		<UnappliedSlashes<T>>::iter().collect()
583	}
584
585	pub(crate) fn submit_unsigned_slashing_report(
586		dispute_proof: DisputeProof,
587		key_ownership_proof: <T as Config>::KeyOwnerProof,
588	) -> Option<()> {
589		T::HandleReports::submit_unsigned_slashing_report(dispute_proof, key_ownership_proof).ok()
590	}
591}
592
593/// Methods for the `ValidateUnsigned` implementation:
594///
595/// It restricts calls to `report_dispute_lost_unsigned` to local calls (i.e.
596/// extrinsics generated on this node) or that already in a block. This
597/// guarantees that only block authors can include unsigned slashing reports.
598impl<T: Config> Pallet<T> {
599	pub fn validate_unsigned(source: TransactionSource, call: &Call<T>) -> TransactionValidity {
600		if let Call::report_dispute_lost_unsigned { dispute_proof, key_owner_proof } = call {
601			// discard slashing report not coming from the local node
602			match source {
603				TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ },
604				_ => {
605					log::warn!(
606						target: LOG_TARGET,
607						"rejecting unsigned transaction because it is not local/in-block."
608					);
609
610					return InvalidTransaction::Call.into();
611				},
612			}
613
614			// check report staleness
615			is_known_offence::<T>(dispute_proof, key_owner_proof)?;
616
617			let longevity = <T::HandleReports as HandleReports<T>>::ReportLongevity::get();
618
619			let tag_prefix = match dispute_proof.kind {
620				DisputeOffenceKind::ForInvalidBacked => "DisputeForInvalidBacked",
621				DisputeOffenceKind::ForInvalidApproved => "DisputeForInvalidApproved",
622				DisputeOffenceKind::AgainstValid => "DisputeAgainstValid",
623			};
624
625			ValidTransaction::with_tag_prefix(tag_prefix)
626				// We assign the maximum priority for any report.
627				.priority(TransactionPriority::max_value())
628				// Only one report for the same offender at the same slot.
629				.and_provides((dispute_proof.time_slot.clone(), dispute_proof.validator_id.clone()))
630				.longevity(longevity)
631				// We don't propagate this. This can never be included on a remote node.
632				.propagate(false)
633				.build()
634		} else {
635			InvalidTransaction::Call.into()
636		}
637	}
638
639	pub fn pre_dispatch(call: &Call<T>) -> Result<(), TransactionValidityError> {
640		if let Call::report_dispute_lost_unsigned { dispute_proof, key_owner_proof } = call {
641			is_known_offence::<T>(dispute_proof, key_owner_proof)
642		} else {
643			Err(InvalidTransaction::Call.into())
644		}
645	}
646}
647
648fn is_known_offence<T: Config>(
649	dispute_proof: &DisputeProof,
650	key_owner_proof: &T::KeyOwnerProof,
651) -> Result<(), TransactionValidityError> {
652	// The membership proof must be for the same session as the dispute.
653	if key_owner_proof.session() != dispute_proof.time_slot.session_index {
654		return Err(InvalidTransaction::BadProof.into());
655	}
656
657	let key = (polkadot_primitives::PARACHAIN_KEY_TYPE_ID, dispute_proof.validator_id.clone());
658
659	let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof.clone())
660		.ok_or(InvalidTransaction::BadProof)?;
661
662	// check if the offence has already been reported,
663	// and if so then we can discard the report.
664	let is_known_offence = <T::HandleReports as HandleReports<T>>::is_known_offence(
665		&[offender],
666		&dispute_proof.time_slot,
667	);
668
669	if is_known_offence {
670		Err(InvalidTransaction::Stale.into())
671	} else {
672		Ok(())
673	}
674}
675
676/// Actual `HandleReports` implementation.
677///
678/// When configured properly, should be instantiated with
679/// `T::KeyOwnerIdentification, Offences, ReportLongevity` parameters.
680pub struct SlashingReportHandler<I, R, L> {
681	_phantom: core::marker::PhantomData<(I, R, L)>,
682}
683
684impl<I, R, L> Default for SlashingReportHandler<I, R, L> {
685	fn default() -> Self {
686		Self { _phantom: Default::default() }
687	}
688}
689
690impl<T, R, L> HandleReports<T> for SlashingReportHandler<T::KeyOwnerIdentification, R, L>
691where
692	T: Config + frame_system::offchain::CreateBare<Call<T>>,
693	R: ReportOffence<
694		T::AccountId,
695		T::KeyOwnerIdentification,
696		SlashingOffence<T::KeyOwnerIdentification>,
697	>,
698	L: Get<u64>,
699{
700	type ReportLongevity = L;
701
702	fn report_offence(
703		offence: SlashingOffence<T::KeyOwnerIdentification>,
704	) -> Result<(), OffenceError> {
705		let reporters = Vec::new();
706		R::report_offence(reporters, offence)
707	}
708
709	fn is_known_offence(
710		offenders: &[T::KeyOwnerIdentification],
711		time_slot: &DisputesTimeSlot,
712	) -> bool {
713		<R as ReportOffence<
714			T::AccountId,
715			T::KeyOwnerIdentification,
716			SlashingOffence<T::KeyOwnerIdentification>,
717		>>::is_known_offence(offenders, time_slot)
718	}
719
720	fn submit_unsigned_slashing_report(
721		dispute_proof: DisputeProof,
722		key_owner_proof: <T as Config>::KeyOwnerProof,
723	) -> Result<(), sp_runtime::TryRuntimeError> {
724		use frame_system::offchain::{CreateBare, SubmitTransaction};
725
726		let session_index = dispute_proof.time_slot.session_index;
727		let validator_index = dispute_proof.validator_index.0;
728		let kind = dispute_proof.kind;
729
730		let call = Call::report_dispute_lost_unsigned {
731			dispute_proof: Box::new(dispute_proof),
732			key_owner_proof,
733		};
734
735		let xt = <T as CreateBare<Call<T>>>::create_bare(call.into());
736		match SubmitTransaction::<T, Call<T>>::submit_transaction(xt) {
737			Ok(()) => {
738				log::info!(
739					target: LOG_TARGET,
740					"Submitted dispute slashing report, session({}), index({}), kind({:?})",
741					session_index,
742					validator_index,
743					kind,
744				);
745				Ok(())
746			},
747			Err(()) => {
748				log::error!(
749					target: LOG_TARGET,
750					"Error submitting dispute slashing report, session({}), index({}), kind({:?})",
751					session_index,
752					validator_index,
753					kind,
754				);
755				Err(sp_runtime::DispatchError::Other(""))
756			},
757		}
758	}
759}