feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use super::*;
|
||||
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_system::RawOrigin;
|
||||
use sp_runtime::traits::One;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn force_unfreeze() {
|
||||
Frozen::<T>::set(Some(One::one()));
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root);
|
||||
|
||||
assert!(Frozen::<T>::get().is_none())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
Pallet,
|
||||
crate::mock::new_test_ext(Default::default()),
|
||||
crate::mock::Test
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Storage migration(s) related to disputes pallet
|
||||
|
||||
use frame_support::traits::StorageVersion;
|
||||
|
||||
pub mod v1 {
|
||||
use super::*;
|
||||
use crate::disputes::{Config, Pallet};
|
||||
use alloc::vec::Vec;
|
||||
use frame_support::{
|
||||
pallet_prelude::*, storage_alias, traits::OnRuntimeUpgrade, weights::Weight,
|
||||
};
|
||||
use pezkuwi_primitives::SessionIndex;
|
||||
|
||||
#[storage_alias]
|
||||
type SpamSlots<T: Config> = StorageMap<Pallet<T>, Twox64Concat, SessionIndex, Vec<u32>>;
|
||||
|
||||
pub struct MigrateToV1<T>(core::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV1<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let mut weight: Weight = Weight::zero();
|
||||
|
||||
if StorageVersion::get::<Pallet<T>>() < 1 {
|
||||
log::info!(target: crate::disputes::LOG_TARGET, "Migrating disputes storage to v1");
|
||||
weight += migrate_to_v1::<T>();
|
||||
StorageVersion::new(1).put::<Pallet<T>>();
|
||||
weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 1));
|
||||
} else {
|
||||
log::info!(
|
||||
target: crate::disputes::LOG_TARGET,
|
||||
"Disputes storage up to date - no need for migration"
|
||||
);
|
||||
}
|
||||
|
||||
weight
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
|
||||
log::trace!(
|
||||
target: crate::disputes::LOG_TARGET,
|
||||
"SpamSlots before migration: {}",
|
||||
SpamSlots::<T>::iter().count()
|
||||
);
|
||||
ensure!(
|
||||
StorageVersion::get::<Pallet<T>>() == 0,
|
||||
"Storage version should be less than `1` before the migration",
|
||||
);
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::disputes::LOG_TARGET, "Running post_upgrade()");
|
||||
ensure!(
|
||||
StorageVersion::get::<Pallet<T>>() >= 1,
|
||||
"Storage version should be `1` after the migration"
|
||||
);
|
||||
ensure!(
|
||||
SpamSlots::<T>::iter().count() == 0,
|
||||
"SpamSlots should be empty after the migration"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates the pallet storage to the most recent version, checking and setting the
|
||||
/// `StorageVersion`.
|
||||
pub fn migrate_to_v1<T: Config>() -> Weight {
|
||||
let mut weight: Weight = Weight::zero();
|
||||
|
||||
// SpamSlots should not contain too many keys so removing everything at once should be safe
|
||||
let res = SpamSlots::<T>::clear(u32::MAX, None);
|
||||
// `loops` is the number of iterations => used to calculate read weights
|
||||
// `backend` is the number of keys removed from the backend => used to calculate write
|
||||
// weights
|
||||
weight = weight
|
||||
.saturating_add(T::DbWeight::get().reads_writes(res.loops as u64, res.backend as u64));
|
||||
|
||||
weight
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,745 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Dispute slashing pallet.
|
||||
//!
|
||||
//! Once a dispute is concluded, we want to slash validators who were on the
|
||||
//! wrong side of the dispute.
|
||||
//!
|
||||
//! A dispute should always result in an offence. There are 3 possible
|
||||
//! offence types:
|
||||
//! - `ForInvalidBacked`: A major offence when a validator backed an
|
||||
//! invalid block. Main source of economic security.
|
||||
//! - `ForInvalidApproved`: A medium offence when a validator approved (NOT backed) an
|
||||
//! invalid block. Protects from lazy validators.
|
||||
//! - `AgainstValid`: A minor offence when a validator disputed a valid block.
|
||||
//! Protects from spam attacks.
|
||||
//!
|
||||
//! Past session slashing edgecase:
|
||||
//!
|
||||
//! 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. 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 simillar to the `grandpa
|
||||
//! equivocation` handler. 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 alloc::{
|
||||
boxed::Box,
|
||||
collections::{btree_map::Entry, btree_set::BTreeSet},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
use pezkuwi_primitives::{
|
||||
slashing::{DisputeProof, DisputesTimeSlot, PendingSlashes},
|
||||
CandidateHash, DisputeOffenceKind, 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::teyrchains::slashing";
|
||||
|
||||
// These are constants, but we want to make them configurable
|
||||
// via `HostConfiguration` in the future.
|
||||
const SLASH_FOR_INVALID_BACKED: Perbill = Perbill::from_percent(100);
|
||||
const SLASH_FOR_INVALID_APPROVED: Perbill = Perbill::from_percent(2);
|
||||
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<const M: u32>;
|
||||
|
||||
impl<const M: u32> BenchmarkingConfiguration for BenchConfig<M> {
|
||||
const MAX_VALIDATORS: u32 = M;
|
||||
}
|
||||
|
||||
/// An offence that is filed against the validators that lost a dispute.
|
||||
#[derive(TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))]
|
||||
pub struct SlashingOffence<KeyOwnerIdentification> {
|
||||
/// 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<KeyOwnerIdentification>,
|
||||
/// What fraction of the total exposure that should be slashed for
|
||||
/// this offence.
|
||||
pub slash_fraction: Perbill,
|
||||
/// The type of slashing offence.
|
||||
pub kind: DisputeOffenceKind,
|
||||
}
|
||||
|
||||
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: DisputeOffenceKind,
|
||||
) -> Self {
|
||||
let time_slot = DisputesTimeSlot::new(session_index, candidate_hash);
|
||||
let slash_fraction = match kind {
|
||||
DisputeOffenceKind::ForInvalidBacked => SLASH_FOR_INVALID_BACKED,
|
||||
DisputeOffenceKind::ForInvalidApproved => SLASH_FOR_INVALID_APPROVED,
|
||||
DisputeOffenceKind::AgainstValid => SLASH_AGAINST_VALID,
|
||||
};
|
||||
Self { time_slot, validator_set_count, offenders, slash_fraction, kind }
|
||||
}
|
||||
}
|
||||
|
||||
/// This type implements `SlashingHandler`.
|
||||
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>>,
|
||||
{
|
||||
/// If in the current session, returns the identified validators. `None`
|
||||
/// otherwise.
|
||||
fn maybe_identify_validators(
|
||||
session_index: SessionIndex,
|
||||
validators: impl IntoIterator<Item = ValidatorIndex>,
|
||||
) -> Option<Vec<IdentificationTuple<T>>> {
|
||||
// We use `ValidatorSet::session_index` and not
|
||||
// `shared::CurrentSessionIndex::<T>::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::<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: DisputeOffenceKind,
|
||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||
) {
|
||||
let losers: BTreeSet<_> = losers.into_iter().collect();
|
||||
if losers.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_offenders =
|
||||
Self::maybe_identify_validators(session_index, losers.iter().cloned());
|
||||
if let Some(offenders) = maybe_offenders {
|
||||
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 = losers
|
||||
.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 losers: Vec<_> = losers.into_iter().collect();
|
||||
let backers: BTreeSet<_> = backers.into_iter().collect();
|
||||
|
||||
if losers.is_empty() || backers.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (loosing_backers, loosing_approvers): (Vec<_>, Vec<_>) =
|
||||
losers.into_iter().partition(|v| backers.contains(v));
|
||||
|
||||
if !loosing_backers.is_empty() {
|
||||
Self::do_punish(
|
||||
session_index,
|
||||
candidate_hash,
|
||||
DisputeOffenceKind::ForInvalidBacked,
|
||||
loosing_backers,
|
||||
);
|
||||
}
|
||||
if !loosing_approvers.is_empty() {
|
||||
Self::do_punish(
|
||||
session_index,
|
||||
candidate_hash,
|
||||
DisputeOffenceKind::ForInvalidApproved,
|
||||
loosing_approvers,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn punish_against_valid(
|
||||
session_index: SessionIndex,
|
||||
candidate_hash: CandidateHash,
|
||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||
_backers: impl IntoIterator<Item = ValidatorIndex>,
|
||||
) {
|
||||
let kind = DisputeOffenceKind::AgainstValid;
|
||||
Self::do_punish(session_index, candidate_hash, kind, losers);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<T: Config> {
|
||||
/// 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<u64>;
|
||||
|
||||
/// Report an offence.
|
||||
fn report_offence(
|
||||
offence: SlashingOffence<T::KeyOwnerIdentification>,
|
||||
) -> 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<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 {
|
||||
/// 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<Self>;
|
||||
|
||||
/// Weight information for extrinsics in this pallet.
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
/// Benchmarking configuration.
|
||||
type BenchmarkingConfig: BenchmarkingConfiguration;
|
||||
}
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::without_storage_info]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
/// Validators pending dispute slashes.
|
||||
#[pallet::storage]
|
||||
pub(crate) type UnappliedSlashes<T> = StorageDoubleMap<
|
||||
_,
|
||||
Twox64Concat,
|
||||
SessionIndex,
|
||||
Blake2_128Concat,
|
||||
CandidateHash,
|
||||
PendingSlashes,
|
||||
>;
|
||||
|
||||
/// `ValidatorSetCount` per session.
|
||||
#[pallet::storage]
|
||||
pub(super) type ValidatorSetCounts<T> =
|
||||
StorageMap<_, Twox64Concat, SessionIndex, ValidatorSetCount>;
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// 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<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>,
|
||||
// box to decrease the size of the call
|
||||
dispute_proof: Box<DisputeProof>,
|
||||
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 =
|
||||
(pezkuwi_primitives::TEYRCHAIN_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;
|
||||
|
||||
// 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<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()),
|
||||
// check that `validator_index` matches `validator_id`
|
||||
Entry::Occupied(e) if e.get() != &dispute_proof.validator_id =>
|
||||
return Err(Error::<T>::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(())
|
||||
};
|
||||
|
||||
<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> {
|
||||
/// Called by the initializer to initialize the disputes slashing module.
|
||||
fn initializer_initialize(_now: BlockNumberFor<T>) -> 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::<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()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<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 {
|
||||
// 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::<T>(dispute_proof, key_owner_proof)?;
|
||||
|
||||
let longevity = <T::HandleReports as HandleReports<T>>::ReportLongevity::get();
|
||||
|
||||
let tag_prefix = match dispute_proof.kind {
|
||||
DisputeOffenceKind::ForInvalidBacked => "DisputeForInvalidBacked",
|
||||
DisputeOffenceKind::ForInvalidApproved => "DisputeForInvalidApproved",
|
||||
DisputeOffenceKind::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<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> {
|
||||
// check the membership proof to extract the offender's id
|
||||
let key = (pezkuwi_primitives::TEYRCHAIN_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 = <T::HandleReports as HandleReports<T>>::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<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::CreateBare<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::{CreateBare, 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 CreateBare<Call<T>>>::create_bare(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(""))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::{disputes::SlashingHandler, initializer, shared};
|
||||
use codec::Decode;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_support::traits::{OnFinalize, OnInitialize};
|
||||
use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin};
|
||||
use pallet_staking::testing_utils::create_validators;
|
||||
use pezkuwi_primitives::{Hash, TEYRCHAIN_KEY_TYPE_ID};
|
||||
use sp_runtime::traits::{One, OpaqueKeys, StaticLookup};
|
||||
use sp_session::MembershipProof;
|
||||
|
||||
// Candidate hash of the disputed candidate.
|
||||
const CANDIDATE_HASH: CandidateHash = CandidateHash(Hash::zero());
|
||||
|
||||
// Simplify getting the value in the benchmark
|
||||
pub const fn max_validators_for<T: super::Config>() -> u32 {
|
||||
<<T>::BenchmarkingConfig as BenchmarkingConfiguration>::MAX_VALIDATORS
|
||||
}
|
||||
|
||||
pub trait Config:
|
||||
pallet_session::Config
|
||||
+ pallet_session::historical::Config
|
||||
+ pallet_staking::Config
|
||||
+ super::Config
|
||||
+ shared::Config
|
||||
+ initializer::Config
|
||||
{
|
||||
}
|
||||
|
||||
fn setup_validator_set<T>(n: u32) -> (SessionIndex, MembershipProof, ValidatorId)
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
pallet_staking::ValidatorCount::<T>::put(n);
|
||||
|
||||
let balance_factor = 1000;
|
||||
// create validators and set random session keys
|
||||
for (n, who) in create_validators::<T>(n, balance_factor).unwrap().into_iter().enumerate() {
|
||||
use rand::{RngCore, SeedableRng};
|
||||
|
||||
let validator = T::Lookup::lookup(who).unwrap();
|
||||
let controller = pallet_staking::Pallet::<T>::bonded(&validator).unwrap();
|
||||
|
||||
let keys = {
|
||||
const SESSION_KEY_LEN: usize = 32;
|
||||
let key_ids = T::Keys::key_ids();
|
||||
let mut keys_len = key_ids.len() * SESSION_KEY_LEN;
|
||||
if key_ids.contains(&sp_core::crypto::key_types::BEEFY) {
|
||||
// BEEFY key is 33 bytes long, not 32.
|
||||
keys_len += 1;
|
||||
}
|
||||
let mut keys = vec![0u8; keys_len];
|
||||
let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(n as u64);
|
||||
rng.fill_bytes(&mut keys);
|
||||
keys
|
||||
};
|
||||
|
||||
let keys: T::Keys = Decode::decode(&mut &keys[..]).expect("wrong number of session keys?");
|
||||
let proof: Vec<u8> = vec![];
|
||||
|
||||
whitelist_account!(controller);
|
||||
pallet_session::Pallet::<T>::ensure_can_pay_key_deposit(&controller).unwrap();
|
||||
pallet_session::Pallet::<T>::set_keys(RawOrigin::Signed(controller).into(), keys, proof)
|
||||
.expect("session::set_keys should work");
|
||||
}
|
||||
|
||||
pallet_session::Pallet::<T>::on_initialize(BlockNumberFor::<T>::one());
|
||||
initializer::Pallet::<T>::on_initialize(BlockNumberFor::<T>::one());
|
||||
|
||||
// signal to `pallet-staking`'s `ElectionProvider` to be ready asap.
|
||||
use frame_election_provider_support::ElectionProvider;
|
||||
<<T as pallet_staking::Config>::ElectionProvider as ElectionProvider>::asap();
|
||||
|
||||
// skip sessions until the new validator set is enacted
|
||||
while pallet_session::Pallet::<T>::validators().len() < n as usize {
|
||||
pallet_session::Pallet::<T>::rotate_session();
|
||||
}
|
||||
initializer::Pallet::<T>::on_finalize(BlockNumberFor::<T>::one());
|
||||
|
||||
let session_index = crate::shared::CurrentSessionIndex::<T>::get();
|
||||
let session_info = crate::session_info::Sessions::<T>::get(session_index);
|
||||
let session_info = session_info.unwrap();
|
||||
let validator_id = session_info.validators.get(ValidatorIndex::from(0)).unwrap().clone();
|
||||
let key = (TEYRCHAIN_KEY_TYPE_ID, validator_id.clone());
|
||||
let key_owner_proof = pallet_session::historical::Pallet::<T>::prove(key).unwrap();
|
||||
|
||||
// rotate a session to make sure `key_owner_proof` is historical
|
||||
initializer::Pallet::<T>::on_initialize(BlockNumberFor::<T>::one());
|
||||
pallet_session::Pallet::<T>::rotate_session();
|
||||
initializer::Pallet::<T>::on_finalize(BlockNumberFor::<T>::one());
|
||||
|
||||
let idx = crate::shared::CurrentSessionIndex::<T>::get();
|
||||
assert!(
|
||||
idx > session_index,
|
||||
"session rotation should work for teyrchain pallets: {} <= {}",
|
||||
idx,
|
||||
session_index,
|
||||
);
|
||||
|
||||
(session_index, key_owner_proof, validator_id)
|
||||
}
|
||||
|
||||
/// Submits a single `ForInvalid` dispute.
|
||||
fn setup_dispute<T>(session_index: SessionIndex, validator_id: ValidatorId) -> DisputeProof
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
let current_session = T::ValidatorSet::session_index();
|
||||
assert_ne!(session_index, current_session);
|
||||
|
||||
let validator_index = ValidatorIndex(0);
|
||||
let losers = [validator_index].into_iter();
|
||||
let backers = losers.clone();
|
||||
|
||||
T::SlashingHandler::punish_for_invalid(session_index, CANDIDATE_HASH, losers, backers);
|
||||
|
||||
let unapplied = <UnappliedSlashes<T>>::get(session_index, CANDIDATE_HASH);
|
||||
assert_eq!(unapplied.unwrap().keys.len(), 1);
|
||||
|
||||
dispute_proof(session_index, validator_id, validator_index)
|
||||
}
|
||||
|
||||
/// Creates a `ForInvalid` dispute proof.
|
||||
fn dispute_proof(
|
||||
session_index: SessionIndex,
|
||||
validator_id: ValidatorId,
|
||||
validator_index: ValidatorIndex,
|
||||
) -> DisputeProof {
|
||||
let kind = DisputeOffenceKind::ForInvalidBacked;
|
||||
let time_slot = DisputesTimeSlot::new(session_index, CANDIDATE_HASH);
|
||||
|
||||
DisputeProof { time_slot, kind, validator_index, validator_id }
|
||||
}
|
||||
|
||||
#[benchmarks(where T: Config<KeyOwnerProof = MembershipProof>)]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn report_dispute_lost_unsigned(n: Linear<4, { max_validators_for::<T>() }>) {
|
||||
let (session_index, key_owner_proof, validator_id) = setup_validator_set::<T>(n);
|
||||
|
||||
// submit a single `ForInvalid` dispute for a past session.
|
||||
let dispute_proof = setup_dispute::<T>(session_index, validator_id);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::None, Box::new(dispute_proof), key_owner_proof);
|
||||
|
||||
let unapplied = <UnappliedSlashes<T>>::get(session_index, CANDIDATE_HASH);
|
||||
assert!(unapplied.is_none());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user