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