// Copyright (C) Parity Technologies (UK) Ltd. // This file is part of Polkadot. // Polkadot is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Polkadot is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . //! Dispute slashing pallet. //! //! Once a dispute is concluded, we want to slash validators who were on the //! wrong side of the dispute. The slashing amount depends on whether the //! candidate was valid (none at the moment) or invalid (big). In addition to //! that, we might want to kick out the validators from the active set. //! Currently, we limit slashing to the backing group for invalid disputes. //! //! The `offences` pallet from Substrate provides us with a way to do both. //! Currently, the interface expects us to provide staking information including //! nominator exposure in order to submit an offence. //! //! Normally, we'd able to fetch this information from the runtime as soon as //! the dispute is concluded. This is also what `im-online` pallet does. //! However, since a dispute can conclude several sessions after the candidate //! was backed (see `dispute_period` in `HostConfiguration`), we can't rely on //! this information being available in the context of the current block. The //! `babe` and `grandpa` equivocation handlers also have to deal with this //! problem. //! //! Our implementation looks like a hybrid of `im-online` and `grandpa` //! equivocation handlers. Meaning, we submit an `offence` for the concluded //! disputes about the current session candidate directly from the runtime. If, //! however, the dispute is about a past session, we record unapplied slashes on //! chain, without `FullIdentification` of the offenders. Later on, a block //! producer can submit an unsigned transaction with `KeyOwnershipProof` of an //! offender and submit it to the runtime to produce an offence. use crate::{disputes, initializer::ValidatorSetCount, session_info::IdentificationTuple}; use frame_support::{ dispatch::Pays, traits::{Defensive, Get, KeyOwnerProofSystem, ValidatorSet, ValidatorSetWithIdentification}, weights::Weight, }; use frame_system::pallet_prelude::BlockNumberFor; use primitives::{ slashing::{DisputeProof, DisputesTimeSlot, PendingSlashes, SlashingOffenceKind}, CandidateHash, SessionIndex, ValidatorId, ValidatorIndex, }; use scale_info::TypeInfo; use sp_runtime::{ traits::Convert, transaction_validity::{ InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, TransactionValidityError, ValidTransaction, }, KeyTypeId, Perbill, }; use sp_session::{GetSessionNumber, GetValidatorCount}; use sp_staking::offence::{Kind, Offence, OffenceError, ReportOffence}; use sp_std::{ collections::{btree_map::Entry, btree_set::BTreeSet}, prelude::*, }; const LOG_TARGET: &str = "runtime::parachains::slashing"; // These are constants, but we want to make them configurable // via `HostConfiguration` in the future. const SLASH_FOR_INVALID: Perbill = Perbill::from_percent(100); const SLASH_AGAINST_VALID: Perbill = Perbill::zero(); const DEFENSIVE_PROOF: &'static str = "disputes module should bail on old session"; #[cfg(feature = "runtime-benchmarks")] pub mod benchmarking; /// The benchmarking configuration. pub trait BenchmarkingConfiguration { const MAX_VALIDATORS: u32; } pub struct BenchConfig; impl BenchmarkingConfiguration for BenchConfig { const MAX_VALIDATORS: u32 = M; } /// An offence that is filed when a series of validators lost a dispute. #[derive(TypeInfo)] #[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))] pub struct SlashingOffence { /// The size of the validator set in that session. pub validator_set_count: ValidatorSetCount, /// Should be unique per dispute. pub time_slot: DisputesTimeSlot, /// Staking information about the validators that lost the dispute /// needed for slashing. pub offenders: Vec, /// What fraction of the total exposure that should be slashed for /// this offence. pub slash_fraction: Perbill, /// Whether the candidate was valid or invalid. pub kind: SlashingOffenceKind, } impl Offence for SlashingOffence where Offender: Clone, { const ID: Kind = *b"disputes:slashin"; type TimeSlot = DisputesTimeSlot; fn offenders(&self) -> Vec { self.offenders.clone() } fn session_index(&self) -> SessionIndex { self.time_slot.session_index } fn validator_set_count(&self) -> ValidatorSetCount { self.validator_set_count } fn time_slot(&self) -> Self::TimeSlot { self.time_slot.clone() } fn slash_fraction(&self, _offenders: u32) -> Perbill { self.slash_fraction } } impl SlashingOffence { fn new( session_index: SessionIndex, candidate_hash: CandidateHash, validator_set_count: ValidatorSetCount, offenders: Vec, kind: SlashingOffenceKind, ) -> Self { let time_slot = DisputesTimeSlot::new(session_index, candidate_hash); let slash_fraction = match kind { SlashingOffenceKind::ForInvalid => SLASH_FOR_INVALID, SlashingOffenceKind::AgainstValid => SLASH_AGAINST_VALID, }; Self { time_slot, validator_set_count, offenders, slash_fraction, kind } } } /// This type implements `SlashingHandler`. pub struct SlashValidatorsForDisputes { _phantom: sp_std::marker::PhantomData, } impl Default for SlashValidatorsForDisputes { fn default() -> Self { Self { _phantom: Default::default() } } } impl SlashValidatorsForDisputes> where T: Config>, { /// If in the current session, returns the identified validators. `None` /// otherwise. fn maybe_identify_validators( session_index: SessionIndex, validators: impl IntoIterator, ) -> Option>> { // We use `ValidatorSet::session_index` and not // `shared::CurrentSessionIndex::::get()` because at the first block of a new era, // the `IdentificationOf` of a validator in the previous session might be // missing, while `shared` pallet would return the same session index as being // updated at the end of the block. let current_session = T::ValidatorSet::session_index(); if session_index == current_session { let account_keys = crate::session_info::AccountKeys::::get(session_index); let account_ids = account_keys.defensive_unwrap_or_default(); let fully_identified = validators .into_iter() .flat_map(|i| account_ids.get(i.0 as usize).cloned()) .filter_map(|id| { >::IdentificationOf::convert( id.clone() ).map(|full_id| (id, full_id)) }) .collect::>>(); return Some(fully_identified) } None } fn do_punish( session_index: SessionIndex, candidate_hash: CandidateHash, kind: SlashingOffenceKind, losers: impl IntoIterator, backers: impl IntoIterator, ) { // sanity check for the current implementation if kind == SlashingOffenceKind::AgainstValid { debug_assert!(false, "should only slash ForInvalid disputes"); return } let losers: BTreeSet<_> = losers.into_iter().collect(); if losers.is_empty() { return } let backers: BTreeSet<_> = backers.into_iter().collect(); let to_punish: Vec = losers.intersection(&backers).cloned().collect(); if to_punish.is_empty() { return } let session_info = crate::session_info::Sessions::::get(session_index); let session_info = match session_info.defensive_proof(DEFENSIVE_PROOF) { Some(info) => info, None => return, }; let maybe = Self::maybe_identify_validators(session_index, to_punish.iter().cloned()); if let Some(offenders) = maybe { let validator_set_count = session_info.discovery_keys.len() as ValidatorSetCount; let offence = SlashingOffence::new( session_index, candidate_hash, validator_set_count, offenders, kind, ); // This is the first time we report an offence for this dispute, // so it is not a duplicate. let _ = T::HandleReports::report_offence(offence); return } let keys = to_punish .into_iter() .filter_map(|i| session_info.validators.get(i).cloned().map(|id| (i, id))) .collect(); let unapplied = PendingSlashes { keys, kind }; let append = |old: &mut Option| { let old = old .get_or_insert(PendingSlashes { keys: Default::default(), kind: unapplied.kind }); debug_assert_eq!(old.kind, unapplied.kind); old.keys.extend(unapplied.keys) }; >::mutate(session_index, candidate_hash, append); } } impl disputes::SlashingHandler> for SlashValidatorsForDisputes> where T: Config>, { fn punish_for_invalid( session_index: SessionIndex, candidate_hash: CandidateHash, losers: impl IntoIterator, backers: impl IntoIterator, ) { let kind = SlashingOffenceKind::ForInvalid; Self::do_punish(session_index, candidate_hash, kind, losers, backers); } fn punish_against_valid( _session_index: SessionIndex, _candidate_hash: CandidateHash, _losers: impl IntoIterator, _backers: impl IntoIterator, ) { // do nothing for now // NOTE: changing that requires modifying `do_punish` implementation } fn initializer_initialize(now: BlockNumberFor) -> Weight { Pallet::::initializer_initialize(now) } fn initializer_finalize() { Pallet::::initializer_finalize() } fn initializer_on_new_session(session_index: SessionIndex) { Pallet::::initializer_on_new_session(session_index) } } /// A trait that defines methods to report an offence (after the slashing report /// has been validated) and for submitting a transaction to report a slash (from /// an offchain context). pub trait HandleReports { /// The longevity, in blocks, that the offence report is valid for. When /// using the staking pallet this should be equal to the bonding duration /// (in blocks, not eras). type ReportLongevity: Get; /// Report an offence. fn report_offence( offence: SlashingOffence, ) -> Result<(), OffenceError>; /// Returns true if the offenders at the given time slot has already been /// reported. fn is_known_offence( offenders: &[T::KeyOwnerIdentification], time_slot: &DisputesTimeSlot, ) -> bool; /// Create and dispatch a slashing report extrinsic. /// This should be called offchain. fn submit_unsigned_slashing_report( dispute_proof: DisputeProof, key_owner_proof: T::KeyOwnerProof, ) -> Result<(), sp_runtime::TryRuntimeError>; } impl HandleReports for () { type ReportLongevity = (); fn report_offence( _offence: SlashingOffence, ) -> Result<(), OffenceError> { Ok(()) } fn is_known_offence( _offenders: &[T::KeyOwnerIdentification], _time_slot: &DisputesTimeSlot, ) -> bool { true } fn submit_unsigned_slashing_report( _dispute_proof: DisputeProof, _key_owner_proof: T::KeyOwnerProof, ) -> Result<(), sp_runtime::TryRuntimeError> { Ok(()) } } pub trait WeightInfo { fn report_dispute_lost(validator_count: ValidatorSetCount) -> Weight; } pub struct TestWeightInfo; impl WeightInfo for TestWeightInfo { fn report_dispute_lost(_validator_count: ValidatorSetCount) -> Weight { Weight::zero() } } pub use pallet::*; #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; #[pallet::config] pub trait Config: frame_system::Config + crate::disputes::Config { /// The proof of key ownership, used for validating slashing reports. /// The proof must include the session index and validator count of the /// session at which the offence occurred. type KeyOwnerProof: Parameter + GetSessionNumber + GetValidatorCount; /// The identification of a key owner, used when reporting slashes. type KeyOwnerIdentification: Parameter; /// A system for proving ownership of keys, i.e. that a given key was /// part of a validator set, needed for validating slashing reports. type KeyOwnerProofSystem: KeyOwnerProofSystem< (KeyTypeId, ValidatorId), Proof = Self::KeyOwnerProof, IdentificationTuple = Self::KeyOwnerIdentification, >; /// The slashing report handling subsystem, defines methods to report an /// offence (after the slashing report has been validated) and for /// submitting a transaction to report a slash (from an offchain /// context). NOTE: when enabling slashing report handling (i.e. this /// type isn't set to `()`) you must use this pallet's /// `ValidateUnsigned` in the runtime definition. type HandleReports: HandleReports; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; /// Benchmarking configuration. type BenchmarkingConfig: BenchmarkingConfiguration; } #[pallet::pallet] #[pallet::without_storage_info] pub struct Pallet(_); /// Validators pending dispute slashes. #[pallet::storage] pub(super) type UnappliedSlashes = StorageDoubleMap< _, Twox64Concat, SessionIndex, Blake2_128Concat, CandidateHash, PendingSlashes, >; /// `ValidatorSetCount` per session. #[pallet::storage] pub(super) type ValidatorSetCounts = StorageMap<_, Twox64Concat, SessionIndex, ValidatorSetCount>; #[pallet::error] pub enum Error { /// The key ownership proof is invalid. InvalidKeyOwnershipProof, /// The session index is too old or invalid. InvalidSessionIndex, /// The candidate hash is invalid. InvalidCandidateHash, /// There is no pending slash for the given validator index and time /// slot. InvalidValidatorIndex, /// The validator index does not match the validator id. ValidatorIndexIdMismatch, /// The given slashing report is valid but already previously reported. DuplicateSlashingReport, } #[pallet::call] impl Pallet { #[pallet::call_index(0)] #[pallet::weight(::WeightInfo::report_dispute_lost( key_owner_proof.validator_count() ))] pub fn report_dispute_lost_unsigned( origin: OriginFor, // box to decrease the size of the call dispute_proof: Box, key_owner_proof: T::KeyOwnerProof, ) -> DispatchResultWithPostInfo { ensure_none(origin)?; let validator_set_count = key_owner_proof.validator_count() as ValidatorSetCount; // check the membership proof to extract the offender's id let key = (primitives::PARACHAIN_KEY_TYPE_ID, dispute_proof.validator_id.clone()); let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof) .ok_or(Error::::InvalidKeyOwnershipProof)?; let session_index = dispute_proof.time_slot.session_index; // check that there is a pending slash for the given // validator index and candidate hash let candidate_hash = dispute_proof.time_slot.candidate_hash; let try_remove = |v: &mut Option| -> Result<(), DispatchError> { let pending = v.as_mut().ok_or(Error::::InvalidCandidateHash)?; if pending.kind != dispute_proof.kind { return Err(Error::::InvalidCandidateHash.into()) } match pending.keys.entry(dispute_proof.validator_index) { Entry::Vacant(_) => return Err(Error::::InvalidValidatorIndex.into()), // check that `validator_index` matches `validator_id` Entry::Occupied(e) if e.get() != &dispute_proof.validator_id => return Err(Error::::ValidatorIndexIdMismatch.into()), Entry::Occupied(e) => { e.remove(); // the report is correct }, } // if the last validator is slashed for this dispute, clean up the storage if pending.keys.is_empty() { *v = None; } Ok(()) }; >::try_mutate_exists(&session_index, &candidate_hash, try_remove)?; let offence = SlashingOffence::new( session_index, candidate_hash, validator_set_count, vec![offender], dispute_proof.kind, ); >::report_offence(offence) .map_err(|_| Error::::DuplicateSlashingReport)?; Ok(Pays::No.into()) } } #[pallet::validate_unsigned] impl ValidateUnsigned for Pallet { type Call = Call; fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity { Self::validate_unsigned(source, call) } fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> { Self::pre_dispatch(call) } } } impl Pallet { /// Called by the initializer to initialize the disputes slashing module. fn initializer_initialize(_now: BlockNumberFor) -> Weight { Weight::zero() } /// Called by the initializer to finalize the disputes slashing pallet. fn initializer_finalize() {} /// Called by the initializer to note a new session in the disputes slashing /// pallet. fn initializer_on_new_session(session_index: SessionIndex) { // This should be small, as disputes are limited by spam slots, so no limit is // fine. const REMOVE_LIMIT: u32 = u32::MAX; let config = crate::configuration::ActiveConfig::::get(); if session_index <= config.dispute_period + 1 { return } let old_session = session_index - config.dispute_period - 1; let _ = >::clear_prefix(old_session, REMOVE_LIMIT, None); } pub(crate) fn unapplied_slashes() -> Vec<(SessionIndex, CandidateHash, PendingSlashes)> { >::iter().collect() } pub(crate) fn submit_unsigned_slashing_report( dispute_proof: DisputeProof, key_ownership_proof: ::KeyOwnerProof, ) -> Option<()> { T::HandleReports::submit_unsigned_slashing_report(dispute_proof, key_ownership_proof).ok() } } /// Methods for the `ValidateUnsigned` implementation: /// /// It restricts calls to `report_dispute_lost_unsigned` to local calls (i.e. /// extrinsics generated on this node) or that already in a block. This /// guarantees that only block authors can include unsigned slashing reports. impl Pallet { pub fn validate_unsigned(source: TransactionSource, call: &Call) -> TransactionValidity { if let Call::report_dispute_lost_unsigned { dispute_proof, key_owner_proof } = call { // discard slashing report not coming from the local node match source { TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ }, _ => { log::warn!( target: LOG_TARGET, "rejecting unsigned transaction because it is not local/in-block." ); return InvalidTransaction::Call.into() }, } // check report staleness is_known_offence::(dispute_proof, key_owner_proof)?; let longevity = >::ReportLongevity::get(); let tag_prefix = match dispute_proof.kind { SlashingOffenceKind::ForInvalid => "DisputeForInvalid", SlashingOffenceKind::AgainstValid => "DisputeAgainstValid", }; ValidTransaction::with_tag_prefix(tag_prefix) // We assign the maximum priority for any report. .priority(TransactionPriority::max_value()) // Only one report for the same offender at the same slot. .and_provides((dispute_proof.time_slot.clone(), dispute_proof.validator_id.clone())) .longevity(longevity) // We don't propagate this. This can never be included on a remote node. .propagate(false) .build() } else { InvalidTransaction::Call.into() } } pub fn pre_dispatch(call: &Call) -> Result<(), TransactionValidityError> { if let Call::report_dispute_lost_unsigned { dispute_proof, key_owner_proof } = call { is_known_offence::(dispute_proof, key_owner_proof) } else { Err(InvalidTransaction::Call.into()) } } } fn is_known_offence( dispute_proof: &DisputeProof, key_owner_proof: &T::KeyOwnerProof, ) -> Result<(), TransactionValidityError> { // check the membership proof to extract the offender's id let key = (primitives::PARACHAIN_KEY_TYPE_ID, dispute_proof.validator_id.clone()); let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof.clone()) .ok_or(InvalidTransaction::BadProof)?; // check if the offence has already been reported, // and if so then we can discard the report. let is_known_offence = >::is_known_offence( &[offender], &dispute_proof.time_slot, ); if is_known_offence { Err(InvalidTransaction::Stale.into()) } else { Ok(()) } } /// Actual `HandleReports` implementation. /// /// When configured properly, should be instantiated with /// `T::KeyOwnerIdentification, Offences, ReportLongevity` parameters. pub struct SlashingReportHandler { _phantom: sp_std::marker::PhantomData<(I, R, L)>, } impl Default for SlashingReportHandler { fn default() -> Self { Self { _phantom: Default::default() } } } impl HandleReports for SlashingReportHandler where T: Config + frame_system::offchain::SendTransactionTypes>, R: ReportOffence< T::AccountId, T::KeyOwnerIdentification, SlashingOffence, >, L: Get, { type ReportLongevity = L; fn report_offence( offence: SlashingOffence, ) -> Result<(), OffenceError> { let reporters = Vec::new(); R::report_offence(reporters, offence) } fn is_known_offence( offenders: &[T::KeyOwnerIdentification], time_slot: &DisputesTimeSlot, ) -> bool { , >>::is_known_offence(offenders, time_slot) } fn submit_unsigned_slashing_report( dispute_proof: DisputeProof, key_owner_proof: ::KeyOwnerProof, ) -> Result<(), sp_runtime::TryRuntimeError> { use frame_system::offchain::SubmitTransaction; let session_index = dispute_proof.time_slot.session_index; let validator_index = dispute_proof.validator_index.0; let kind = dispute_proof.kind; let call = Call::report_dispute_lost_unsigned { dispute_proof: Box::new(dispute_proof), key_owner_proof, }; match SubmitTransaction::>::submit_unsigned_transaction(call.into()) { Ok(()) => { log::info!( target: LOG_TARGET, "Submitted dispute slashing report, session({}), index({}), kind({:?})", session_index, validator_index, kind, ); Ok(()) }, Err(()) => { log::error!( target: LOG_TARGET, "Error submitting dispute slashing report, session({}), index({}), kind({:?})", session_index, validator_index, kind, ); Err(sp_runtime::DispatchError::Other("")) }, } } }