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 alloc::{
boxed::Box,
collections::{btree_map::Entry, btree_set::BTreeSet},
vec,
vec::Vec,
};
use polkadot_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};
const LOG_TARGET: &str = "runtime::parachains::slashing";
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;
pub trait BenchmarkingConfiguration {
const MAX_VALIDATORS: u32;
}
pub struct BenchConfig<const M: u32>;
impl<const M: u32> BenchmarkingConfiguration for BenchConfig<M> {
const MAX_VALIDATORS: u32 = M;
}
#[derive(TypeInfo)]
#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))]
pub struct SlashingOffence<KeyOwnerIdentification> {
pub validator_set_count: ValidatorSetCount,
pub time_slot: DisputesTimeSlot,
pub offenders: Vec<KeyOwnerIdentification>,
pub slash_fraction: Perbill,
pub kind: SlashingOffenceKind,
}
impl<Offender> Offence<Offender> for SlashingOffence<Offender>
where
Offender: Clone,
{
const ID: Kind = *b"disputes:slashin";
type TimeSlot = DisputesTimeSlot;
fn offenders(&self) -> Vec<Offender> {
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<KeyOwnerIdentification> SlashingOffence<KeyOwnerIdentification> {
fn new(
session_index: SessionIndex,
candidate_hash: CandidateHash,
validator_set_count: ValidatorSetCount,
offenders: Vec<KeyOwnerIdentification>,
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 }
}
}
pub struct SlashValidatorsForDisputes<C> {
_phantom: core::marker::PhantomData<C>,
}
impl<C> Default for SlashValidatorsForDisputes<C> {
fn default() -> Self {
Self { _phantom: Default::default() }
}
}
impl<T> SlashValidatorsForDisputes<Pallet<T>>
where
T: Config<KeyOwnerIdentification = IdentificationTuple<T>>,
{
fn maybe_identify_validators(
session_index: SessionIndex,
validators: impl IntoIterator<Item = ValidatorIndex>,
) -> Option<Vec<IdentificationTuple<T>>> {
let current_session = T::ValidatorSet::session_index();
if session_index == current_session {
let account_keys = crate::session_info::AccountKeys::<T>::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| {
<T::ValidatorSet as ValidatorSetWithIdentification<T::AccountId>>::IdentificationOf::convert(
id.clone()
).map(|full_id| (id, full_id))
})
.collect::<Vec<IdentificationTuple<T>>>();
return Some(fully_identified)
}
None
}
fn do_punish(
session_index: SessionIndex,
candidate_hash: CandidateHash,
kind: SlashingOffenceKind,
losers: impl IntoIterator<Item = ValidatorIndex>,
backers: impl IntoIterator<Item = ValidatorIndex>,
) {
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<ValidatorIndex> = losers.intersection(&backers).cloned().collect();
if to_punish.is_empty() {
return
}
let session_info = crate::session_info::Sessions::<T>::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,
);
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<PendingSlashes>| {
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)
};
<UnappliedSlashes<T>>::mutate(session_index, candidate_hash, append);
}
}
impl<T> disputes::SlashingHandler<BlockNumberFor<T>> for SlashValidatorsForDisputes<Pallet<T>>
where
T: Config<KeyOwnerIdentification = IdentificationTuple<T>>,
{
fn punish_for_invalid(
session_index: SessionIndex,
candidate_hash: CandidateHash,
losers: impl IntoIterator<Item = ValidatorIndex>,
backers: impl IntoIterator<Item = ValidatorIndex>,
) {
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<Item = ValidatorIndex>,
_backers: impl IntoIterator<Item = ValidatorIndex>,
) {
}
fn initializer_initialize(now: BlockNumberFor<T>) -> Weight {
Pallet::<T>::initializer_initialize(now)
}
fn initializer_finalize() {
Pallet::<T>::initializer_finalize()
}
fn initializer_on_new_session(session_index: SessionIndex) {
Pallet::<T>::initializer_on_new_session(session_index)
}
}
pub trait HandleReports<T: Config> {
type ReportLongevity: Get<u64>;
fn report_offence(
offence: SlashingOffence<T::KeyOwnerIdentification>,
) -> Result<(), OffenceError>;
fn is_known_offence(
offenders: &[T::KeyOwnerIdentification],
time_slot: &DisputesTimeSlot,
) -> bool;
fn submit_unsigned_slashing_report(
dispute_proof: DisputeProof,
key_owner_proof: T::KeyOwnerProof,
) -> Result<(), sp_runtime::TryRuntimeError>;
}
impl<T: Config> HandleReports<T> for () {
type ReportLongevity = ();
fn report_offence(
_offence: SlashingOffence<T::KeyOwnerIdentification>,
) -> 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_unsigned(validator_count: ValidatorSetCount) -> Weight;
}
pub struct TestWeightInfo;
impl WeightInfo for TestWeightInfo {
fn report_dispute_lost_unsigned(_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 {
type KeyOwnerProof: Parameter + GetSessionNumber + GetValidatorCount;
type KeyOwnerIdentification: Parameter;
type KeyOwnerProofSystem: KeyOwnerProofSystem<
(KeyTypeId, ValidatorId),
Proof = Self::KeyOwnerProof,
IdentificationTuple = Self::KeyOwnerIdentification,
>;
type HandleReports: HandleReports<Self>;
type WeightInfo: WeightInfo;
type BenchmarkingConfig: BenchmarkingConfiguration;
}
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::storage]
pub(super) type UnappliedSlashes<T> = StorageDoubleMap<
_,
Twox64Concat,
SessionIndex,
Blake2_128Concat,
CandidateHash,
PendingSlashes,
>;
#[pallet::storage]
pub(super) type ValidatorSetCounts<T> =
StorageMap<_, Twox64Concat, SessionIndex, ValidatorSetCount>;
#[pallet::error]
pub enum Error<T> {
InvalidKeyOwnershipProof,
InvalidSessionIndex,
InvalidCandidateHash,
InvalidValidatorIndex,
ValidatorIndexIdMismatch,
DuplicateSlashingReport,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(<T as Config>::WeightInfo::report_dispute_lost_unsigned(
key_owner_proof.validator_count()
))]
pub fn report_dispute_lost_unsigned(
origin: OriginFor<T>,
dispute_proof: Box<DisputeProof>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResultWithPostInfo {
ensure_none(origin)?;
let validator_set_count = key_owner_proof.validator_count() as ValidatorSetCount;
let key =
(polkadot_primitives::PARACHAIN_KEY_TYPE_ID, dispute_proof.validator_id.clone());
let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof)
.ok_or(Error::<T>::InvalidKeyOwnershipProof)?;
let session_index = dispute_proof.time_slot.session_index;
let candidate_hash = dispute_proof.time_slot.candidate_hash;
let try_remove = |v: &mut Option<PendingSlashes>| -> Result<(), DispatchError> {
let pending = v.as_mut().ok_or(Error::<T>::InvalidCandidateHash)?;
if pending.kind != dispute_proof.kind {
return Err(Error::<T>::InvalidCandidateHash.into())
}
match pending.keys.entry(dispute_proof.validator_index) {
Entry::Vacant(_) => return Err(Error::<T>::InvalidValidatorIndex.into()),
Entry::Occupied(e) if e.get() != &dispute_proof.validator_id =>
return Err(Error::<T>::ValidatorIndexIdMismatch.into()),
Entry::Occupied(e) => {
e.remove(); },
}
if pending.keys.is_empty() {
*v = None;
}
Ok(())
};
<UnappliedSlashes<T>>::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,
);
<T::HandleReports as HandleReports<T>>::report_offence(offence)
.map_err(|_| Error::<T>::DuplicateSlashingReport)?;
Ok(Pays::No.into())
}
}
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
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<T: Config> Pallet<T> {
fn initializer_initialize(_now: BlockNumberFor<T>) -> Weight {
Weight::zero()
}
fn initializer_finalize() {}
fn initializer_on_new_session(session_index: SessionIndex) {
const REMOVE_LIMIT: u32 = u32::MAX;
let config = crate::configuration::ActiveConfig::<T>::get();
if session_index <= config.dispute_period + 1 {
return
}
let old_session = session_index - config.dispute_period - 1;
let _ = <UnappliedSlashes<T>>::clear_prefix(old_session, REMOVE_LIMIT, None);
}
pub(crate) fn unapplied_slashes() -> Vec<(SessionIndex, CandidateHash, PendingSlashes)> {
<UnappliedSlashes<T>>::iter().collect()
}
pub(crate) fn submit_unsigned_slashing_report(
dispute_proof: DisputeProof,
key_ownership_proof: <T as Config>::KeyOwnerProof,
) -> Option<()> {
T::HandleReports::submit_unsigned_slashing_report(dispute_proof, key_ownership_proof).ok()
}
}
impl<T: Config> Pallet<T> {
pub fn validate_unsigned(source: TransactionSource, call: &Call<T>) -> TransactionValidity {
if let Call::report_dispute_lost_unsigned { dispute_proof, key_owner_proof } = call {
match source {
TransactionSource::Local | TransactionSource::InBlock => { },
_ => {
log::warn!(
target: LOG_TARGET,
"rejecting unsigned transaction because it is not local/in-block."
);
return InvalidTransaction::Call.into()
},
}
is_known_offence::<T>(dispute_proof, key_owner_proof)?;
let longevity = <T::HandleReports as HandleReports<T>>::ReportLongevity::get();
let tag_prefix = match dispute_proof.kind {
SlashingOffenceKind::ForInvalid => "DisputeForInvalid",
SlashingOffenceKind::AgainstValid => "DisputeAgainstValid",
};
ValidTransaction::with_tag_prefix(tag_prefix)
.priority(TransactionPriority::max_value())
.and_provides((dispute_proof.time_slot.clone(), dispute_proof.validator_id.clone()))
.longevity(longevity)
.propagate(false)
.build()
} else {
InvalidTransaction::Call.into()
}
}
pub fn pre_dispatch(call: &Call<T>) -> Result<(), TransactionValidityError> {
if let Call::report_dispute_lost_unsigned { dispute_proof, key_owner_proof } = call {
is_known_offence::<T>(dispute_proof, key_owner_proof)
} else {
Err(InvalidTransaction::Call.into())
}
}
}
fn is_known_offence<T: Config>(
dispute_proof: &DisputeProof,
key_owner_proof: &T::KeyOwnerProof,
) -> Result<(), TransactionValidityError> {
let key = (polkadot_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)?;
let is_known_offence = <T::HandleReports as HandleReports<T>>::is_known_offence(
&[offender],
&dispute_proof.time_slot,
);
if is_known_offence {
Err(InvalidTransaction::Stale.into())
} else {
Ok(())
}
}
pub struct SlashingReportHandler<I, R, L> {
_phantom: core::marker::PhantomData<(I, R, L)>,
}
impl<I, R, L> Default for SlashingReportHandler<I, R, L> {
fn default() -> Self {
Self { _phantom: Default::default() }
}
}
impl<T, R, L> HandleReports<T> for SlashingReportHandler<T::KeyOwnerIdentification, R, L>
where
T: Config + frame_system::offchain::CreateInherent<Call<T>>,
R: ReportOffence<
T::AccountId,
T::KeyOwnerIdentification,
SlashingOffence<T::KeyOwnerIdentification>,
>,
L: Get<u64>,
{
type ReportLongevity = L;
fn report_offence(
offence: SlashingOffence<T::KeyOwnerIdentification>,
) -> Result<(), OffenceError> {
let reporters = Vec::new();
R::report_offence(reporters, offence)
}
fn is_known_offence(
offenders: &[T::KeyOwnerIdentification],
time_slot: &DisputesTimeSlot,
) -> bool {
<R as ReportOffence<
T::AccountId,
T::KeyOwnerIdentification,
SlashingOffence<T::KeyOwnerIdentification>,
>>::is_known_offence(offenders, time_slot)
}
fn submit_unsigned_slashing_report(
dispute_proof: DisputeProof,
key_owner_proof: <T as Config>::KeyOwnerProof,
) -> Result<(), sp_runtime::TryRuntimeError> {
use frame_system::offchain::{CreateInherent, 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,
};
let xt = <T as CreateInherent<Call<T>>>::create_inherent(call.into());
match SubmitTransaction::<T, Call<T>>::submit_transaction(xt) {
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(""))
},
}
}
}