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
94#[cfg(test)]
95mod tests;
96
97pub 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#[derive(TypeInfo)]
110#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))]
111pub struct SlashingOffence<KeyOwnerIdentification> {
112 pub validator_set_count: ValidatorSetCount,
114 pub time_slot: DisputesTimeSlot,
116 pub offenders: Vec<KeyOwnerIdentification>,
119 pub slash_fraction: Perbill,
122 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
173pub 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 fn maybe_identify_validators(
191 session_index: SessionIndex,
192 validators: impl IntoIterator<Item = ValidatorIndex>,
193 ) -> Option<Vec<IdentificationTuple<T>>> {
194 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 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
329pub trait HandleReports<T: Config> {
333 type ReportLongevity: Get<u64>;
337
338 fn report_offence(
340 offence: SlashingOffence<T::KeyOwnerIdentification>,
341 ) -> Result<(), OffenceError>;
342
343 fn is_known_offence(
346 offenders: &[T::KeyOwnerIdentification],
347 time_slot: &DisputesTimeSlot,
348 ) -> bool;
349
350 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 type KeyOwnerProof: Parameter + GetSessionNumber + GetValidatorCount;
406
407 type KeyOwnerIdentification: Parameter;
409
410 type KeyOwnerProofSystem: KeyOwnerProofSystem<
413 (KeyTypeId, ValidatorId),
414 Proof = Self::KeyOwnerProof,
415 IdentificationTuple = Self::KeyOwnerIdentification,
416 >;
417
418 type HandleReports: HandleReports<Self>;
425
426 type WeightInfo: WeightInfo;
428
429 type BenchmarkingConfig: BenchmarkingConfiguration;
431 }
432
433 #[pallet::pallet]
434 #[pallet::without_storage_info]
435 pub struct Pallet<T>(_);
436
437 #[pallet::storage]
439 pub(crate) type UnappliedSlashes<T> = StorageDoubleMap<
440 _,
441 Twox64Concat,
442 SessionIndex,
443 Blake2_128Concat,
444 CandidateHash,
445 PendingSlashes,
446 >;
447
448 #[pallet::storage]
450 pub(super) type ValidatorSetCounts<T> =
451 StorageMap<_, Twox64Concat, SessionIndex, ValidatorSetCount>;
452
453 #[pallet::error]
454 pub enum Error<T> {
455 InvalidKeyOwnershipProof,
457 InvalidSessionIndex,
459 InvalidCandidateHash,
461 InvalidValidatorIndex,
464 ValidatorIndexIdMismatch,
466 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 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 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 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 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(); },
515 }
516
517 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 fn initializer_initialize(_now: BlockNumberFor<T>) -> Weight {
559 Weight::zero()
560 }
561
562 fn initializer_finalize() {}
564
565 fn initializer_on_new_session(session_index: SessionIndex) {
568 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
593impl<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 match source {
603 TransactionSource::Local | TransactionSource::InBlock => { },
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 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 .priority(TransactionPriority::max_value())
628 .and_provides((dispute_proof.time_slot.clone(), dispute_proof.validator_id.clone()))
630 .longevity(longevity)
631 .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 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 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
676pub 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}