1use 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
84const 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
94pub 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#[derive(TypeInfo)]
107#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))]
108pub struct SlashingOffence<KeyOwnerIdentification> {
109 pub validator_set_count: ValidatorSetCount,
111 pub time_slot: DisputesTimeSlot,
113 pub offenders: Vec<KeyOwnerIdentification>,
116 pub slash_fraction: Perbill,
119 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
170pub 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 fn maybe_identify_validators(
188 session_index: SessionIndex,
189 validators: impl IntoIterator<Item = ValidatorIndex>,
190 ) -> Option<Vec<IdentificationTuple<T>>> {
191 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 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
326pub trait HandleReports<T: Config> {
330 type ReportLongevity: Get<u64>;
334
335 fn report_offence(
337 offence: SlashingOffence<T::KeyOwnerIdentification>,
338 ) -> Result<(), OffenceError>;
339
340 fn is_known_offence(
343 offenders: &[T::KeyOwnerIdentification],
344 time_slot: &DisputesTimeSlot,
345 ) -> bool;
346
347 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 type KeyOwnerProof: Parameter + GetSessionNumber + GetValidatorCount;
403
404 type KeyOwnerIdentification: Parameter;
406
407 type KeyOwnerProofSystem: KeyOwnerProofSystem<
410 (KeyTypeId, ValidatorId),
411 Proof = Self::KeyOwnerProof,
412 IdentificationTuple = Self::KeyOwnerIdentification,
413 >;
414
415 type HandleReports: HandleReports<Self>;
422
423 type WeightInfo: WeightInfo;
425
426 type BenchmarkingConfig: BenchmarkingConfiguration;
428 }
429
430 #[pallet::pallet]
431 #[pallet::without_storage_info]
432 pub struct Pallet<T>(_);
433
434 #[pallet::storage]
436 pub(crate) type UnappliedSlashes<T> = StorageDoubleMap<
437 _,
438 Twox64Concat,
439 SessionIndex,
440 Blake2_128Concat,
441 CandidateHash,
442 PendingSlashes,
443 >;
444
445 #[pallet::storage]
447 pub(super) type ValidatorSetCounts<T> =
448 StorageMap<_, Twox64Concat, SessionIndex, ValidatorSetCount>;
449
450 #[pallet::error]
451 pub enum Error<T> {
452 InvalidKeyOwnershipProof,
454 InvalidSessionIndex,
456 InvalidCandidateHash,
458 InvalidValidatorIndex,
461 ValidatorIndexIdMismatch,
463 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 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 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 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 Entry::Occupied(e) if e.get() != &dispute_proof.validator_id =>
502 return Err(Error::<T>::ValidatorIndexIdMismatch.into()),
503 Entry::Occupied(e) => {
504 e.remove(); },
506 }
507
508 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 fn initializer_initialize(_now: BlockNumberFor<T>) -> Weight {
549 Weight::zero()
550 }
551
552 fn initializer_finalize() {}
554
555 fn initializer_on_new_session(session_index: SessionIndex) {
558 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
583impl<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 match source {
593 TransactionSource::Local | TransactionSource::InBlock => { },
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 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 .priority(TransactionPriority::max_value())
618 .and_provides((dispute_proof.time_slot.clone(), dispute_proof.validator_id.clone()))
620 .longevity(longevity)
621 .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 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 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
662pub 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}