From 7ed73458445968a47888a9f33753c664636cdd80 Mon Sep 17 00:00:00 2001 From: Andronik Date: Tue, 20 Sep 2022 12:56:55 +0200 Subject: [PATCH] runtime/disputes: slashing (#5535) * disputes: runtime part of slashing * disputes: reward winners * disputes/slashing: validate_unsigned impl * fmt * disputes/slashing: report_dispute_lost_unsigned * disputes/slashing: separate winners from losers and report winners * disputes/slashing: refactoring * impl HandleReports * enable on Wenstend * fmt * add slashing pallet to the mock and test runtimes * fix a bug in report_dispute_lost_unsigned * fmt * disputes: remove new_participants from summary * disputes: remove punish_inconclusive * impl SlashingHandler for Pallet for type-safety * do not impl slashing::Config on mainnets yet * teach spellcheck deduplication * simplify interfaces and resolve some TODOs * resolve some more TODOs * minor typos * move slashing into a folder * remove unnecessary clone * fix validator_set_count calculation * introduce ValidatorSetCount * store ValidatorSetCount * fmt * add the benchmark * fmt * unflatten slashing * post-rebase fixes * remove winners eagerly * use real slashing weights for westend * remove bench test suite * zombinet: modify disputes test to check for an offence report * zombinet: add a timeout * add slashing pallet to Rococo * zombienet: revert back to rococo-local * fmt * remove TODOs * revert some accidental changes * slashing is submodule of disputes * Change the log target Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * wrap comments with rustfmt, more docs, constants * use Defensive trait * cargo update -p sp-io * merge offence types, remove rewards for now * cargo update -p sp-io * benchmark fixes * fmt * unused var * fix block_author impl * ressurect RewardValidators trait * remove outdated comment * more module docs * introduce BenchmarkingConfig * typo fix * teach spellcheck unapplied * use Weight::new() * fix mocking rewards * use RefTimeWeight * ".git/.scripts/bench-bot.sh" runtime westend-dev runtime_parachains::disputes::slashing * refactor maybe_identify_validators * no more ticket in disguise * remove outdated comments * lower against valid to 0.1% * bump zombienet version for debug * use from_perthousand * post-merge fixes * another day, another Weight changes * Revert "bump zombienet version for debug" This reverts commit 0d9978711f8ec9a746a5e1c45e8ffbe7c75e7b5c. * do not reward block authors * fix outdated comment * use Pays from frame_support::dispatch::Pays * add timeout to is up Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Co-authored-by: command-bot <> Co-authored-by: Javier Viola Co-authored-by: Javier Viola --- runtime/kusama/src/lib.rs | 2 +- runtime/parachains/Cargo.toml | 1 + runtime/parachains/src/disputes.rs | 78 +- runtime/parachains/src/disputes/slashing.rs | 739 ++++++++++++++++++ .../src/disputes/slashing/benchmarking.rs | 156 ++++ runtime/parachains/src/disputes/tests.rs | 11 +- runtime/parachains/src/initializer.rs | 9 +- runtime/parachains/src/mock.rs | 32 +- runtime/polkadot/src/lib.rs | 2 +- runtime/rococo/src/lib.rs | 25 +- runtime/test-runtime/src/lib.rs | 2 +- runtime/westend/src/lib.rs | 31 +- runtime/westend/src/weights/mod.rs | 1 + .../runtime_parachains_disputes_slashing.rs | 70 ++ scripts/ci/gitlab/lingua.dic | 2 + .../0002-parachains-disputes.feature | 3 + .../functional/0002-parachains-disputes.toml | 6 +- ...eregister-register-validator-smoke.feature | 8 +- 18 files changed, 1105 insertions(+), 73 deletions(-) create mode 100644 runtime/parachains/src/disputes/slashing.rs create mode 100644 runtime/parachains/src/disputes/slashing/benchmarking.rs create mode 100644 runtime/westend/src/weights/runtime_parachains_disputes_slashing.rs diff --git a/runtime/kusama/src/lib.rs b/runtime/kusama/src/lib.rs index d889408504f9..af55eba4203a 100644 --- a/runtime/kusama/src/lib.rs +++ b/runtime/kusama/src/lib.rs @@ -1173,7 +1173,7 @@ impl parachains_initializer::Config for Runtime { impl parachains_disputes::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RewardValidators = parachains_reward_points::RewardValidatorsWithEraPoints; - type PunishValidators = (); + type SlashingHandler = (); type WeightInfo = weights::runtime_parachains_disputes::WeightInfo; } diff --git a/runtime/parachains/Cargo.toml b/runtime/parachains/Cargo.toml index 40f05ddda6a8..3f34566013e6 100644 --- a/runtime/parachains/Cargo.toml +++ b/runtime/parachains/Cargo.toml @@ -95,6 +95,7 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-staking/runtime-benchmarks", "primitives/runtime-benchmarks", "static_assertions", "sp-application-crypto", diff --git a/runtime/parachains/src/disputes.rs b/runtime/parachains/src/disputes.rs index a6fc7e75aa22..a26eccad0daf 100644 --- a/runtime/parachains/src/disputes.rs +++ b/runtime/parachains/src/disputes.rs @@ -39,6 +39,7 @@ use sp_std::{cmp::Ordering, prelude::*}; #[allow(unused_imports)] pub(crate) use self::tests::run_to_block; +pub mod slashing; #[cfg(test)] mod tests; @@ -73,35 +74,55 @@ impl RewardValidators for () { } /// Punishment hooks for disputes. -pub trait PunishValidators { - /// Punish a series of validators who were for an invalid parablock. This is expected to be a major - /// punishment. +pub trait SlashingHandler { + /// Punish a series of validators who were for an invalid parablock. This is + /// expected to be a major punishment. fn punish_for_invalid( session: SessionIndex, - validators: impl IntoIterator, + candidate_hash: CandidateHash, + losers: impl IntoIterator, ); - /// Punish a series of validators who were against a valid parablock. This is expected to be a minor - /// punishment. + /// Punish a series of validators who were against a valid parablock. This + /// is expected to be a minor punishment. fn punish_against_valid( session: SessionIndex, - validators: impl IntoIterator, + candidate_hash: CandidateHash, + losers: impl IntoIterator, ); - /// Punish a series of validators who were part of a dispute which never concluded. This is expected - /// to be a minor punishment. - fn punish_inconclusive( - session: SessionIndex, - validators: impl IntoIterator, - ); + /// Called by the initializer to initialize the slashing pallet. + fn initializer_initialize(now: BlockNumber) -> Weight; + + /// Called by the initializer to finalize the slashing pallet. + fn initializer_finalize(); + + /// Called by the initializer to note that a new session has started. + fn initializer_on_new_session(session_index: SessionIndex); } -impl PunishValidators for () { - fn punish_for_invalid(_: SessionIndex, _: impl IntoIterator) {} +impl SlashingHandler for () { + fn punish_for_invalid( + _: SessionIndex, + _: CandidateHash, + _: impl IntoIterator, + ) { + } + + fn punish_against_valid( + _: SessionIndex, + _: CandidateHash, + _: impl IntoIterator, + ) { + } - fn punish_against_valid(_: SessionIndex, _: impl IntoIterator) {} + fn initializer_initialize(_now: BlockNumber) -> Weight { + Weight::zero() + } + + fn initializer_finalize() {} - fn punish_inconclusive(_: SessionIndex, _: impl IntoIterator) {} + fn initializer_on_new_session(_: SessionIndex) {} } /// Binary discriminator to determine if the expensive signature @@ -412,7 +433,7 @@ pub mod pallet { pub trait Config: frame_system::Config + configuration::Config + session_info::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; type RewardValidators: RewardValidators; - type PunishValidators: PunishValidators; + type SlashingHandler: SlashingHandler; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; @@ -831,14 +852,7 @@ impl Pallet { // it would be unexpected for any change here to occur when the dispute has not concluded // in time, as a dispute guaranteed to have at least one honest participant should // conclude quickly. - let participating = decrement_spam(spam_slots, &dispute); - - // Slight punishment as these validators have failed to make data available to - // others in a timely manner. - T::PunishValidators::punish_inconclusive( - session_index, - participating.iter_ones().map(|i| ValidatorIndex(i as _)), - ); + let _participating = decrement_spam(spam_slots, &dispute); }); weight += T::DbWeight::get().reads_writes(2, 2); @@ -1187,7 +1201,9 @@ impl Pallet { Error::::SingleSidedDispute, ); - let DisputeStatementSet { session, candidate_hash, .. } = set.clone(); + let DisputeStatementSet { ref session, ref candidate_hash, .. } = set; + let session = *session; + let candidate_hash = *candidate_hash; // we can omit spam slot checks, `fn filter_disputes_data` is // always called before calling this `fn`. @@ -1227,10 +1243,14 @@ impl Pallet { // Slash participants on a losing side. { // a valid candidate, according to 2/3. Punish those on the 'against' side. - T::PunishValidators::punish_against_valid(session, summary.slash_against); + T::SlashingHandler::punish_against_valid( + session, + candidate_hash, + summary.slash_against, + ); // an invalid candidate, according to 2/3. Punish those on the 'for' side. - T::PunishValidators::punish_for_invalid(session, summary.slash_for); + T::SlashingHandler::punish_for_invalid(session, candidate_hash, summary.slash_for); } >::insert(&session, &candidate_hash, &summary.state); diff --git a/runtime/parachains/src/disputes/slashing.rs b/runtime/parachains/src/disputes/slashing.rs new file mode 100644 index 000000000000..fd6708de6ba0 --- /dev/null +++ b/runtime/parachains/src/disputes/slashing.rs @@ -0,0 +1,739 @@ +// Copyright 2022 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 (small) or invalid (big). +//! In addition to that, we might want to kick out the validators from the +//! active set. +//! +//! 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 be 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 parity_scale_codec::{Decode, Encode}; +use primitives::v2::{CandidateHash, SessionIndex, ValidatorId, ValidatorIndex}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::Convert, + transaction_validity::{ + InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, + TransactionValidityError, ValidTransaction, + }, + DispatchResult, KeyTypeId, Perbill, RuntimeDebug, +}; +use sp_session::{GetSessionNumber, GetValidatorCount}; +use sp_staking::offence::{DisableStrategy, Kind, Offence, OffenceError, ReportOffence}; +use sp_std::{ + collections::btree_map::{BTreeMap, Entry}, + 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::from_perthousand(1); +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; +} + +/// Timeslots should uniquely identify offences and are used for the offence +/// deduplication. +#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Encode, Decode, TypeInfo, RuntimeDebug)] +pub struct DisputesTimeSlot { + // The order of these matters for `derive(Ord)`. + session_index: SessionIndex, + candidate_hash: CandidateHash, +} + +impl DisputesTimeSlot { + pub fn new(session_index: SessionIndex, candidate_hash: CandidateHash) -> Self { + Self { session_index, candidate_hash } + } +} + +/// An offence that is filed when a series of validators lost a dispute. +#[derive(RuntimeDebug, 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 disable_strategy(&self) -> DisableStrategy { + match self.kind { + SlashingOffenceKind::ForInvalid => DisableStrategy::Always, + // in the future we might change it based on number of disputes initiated: + // + SlashingOffenceKind::AgainstValid => DisableStrategy::Never, + } + } + + 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::Pallet::session_index()` 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::Pallet::::account_keys(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, + ) { + let losers: Vec = losers.into_iter().collect(); + if losers.is_empty() { + // Nothing to do + return + } + let session_info = crate::session_info::Pallet::::session_info(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, losers.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 = losers + .into_iter() + .filter_map(|i| session_info.validators.get(i.0 as usize).cloned().map(|id| (i, id))) + .collect(); + let unapplied = PendingSlashes { keys, kind }; + >::insert(session_index, candidate_hash, unapplied); + } +} + +impl disputes::SlashingHandler for SlashValidatorsForDisputes> +where + T: Config>, +{ + fn punish_for_invalid( + session_index: SessionIndex, + candidate_hash: CandidateHash, + losers: impl IntoIterator, + ) { + let kind = SlashingOffenceKind::ForInvalid; + Self::do_punish(session_index, candidate_hash, kind, losers); + } + + fn punish_against_valid( + session_index: SessionIndex, + candidate_hash: CandidateHash, + losers: impl IntoIterator, + ) { + let kind = SlashingOffenceKind::AgainstValid; + Self::do_punish(session_index, candidate_hash, kind, losers); + } + + fn initializer_initialize(now: T::BlockNumber) -> 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) + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum SlashingOffenceKind { + #[codec(index = 0)] + ForInvalid, + #[codec(index = 1)] + AgainstValid, +} + +/// We store most of the information about a lost dispute on chain. This struct +/// is required to identify and verify it. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct DisputeProof { + /// Time slot when the dispute occured. + pub time_slot: DisputesTimeSlot, + /// The dispute outcome. + pub kind: SlashingOffenceKind, + /// The index of the validator who lost a dispute. + pub validator_index: ValidatorIndex, + /// The parachain session key of the validator. + pub validator_id: ValidatorId, +} + +/// Slashes that are waiting to be applied once we have validator key +/// identification. +#[derive(Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct PendingSlashes { + /// Indices and keys of the validators who lost a dispute and are pending + /// slashes. + pub keys: BTreeMap, + /// The dispute outcome. + pub kind: SlashingOffenceKind, +} + +/// 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, + ) -> DispatchResult; +} + +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, + ) -> DispatchResult { + 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::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)?; + + // check the membership proof to extract the offender's id + let key = (primitives::v2::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; + let validator_set_count = crate::session_info::Pallet::::session_info(session_index) + .ok_or(Error::::InvalidSessionIndex)? + .discovery_keys + .len() as ValidatorSetCount; + + // 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: T::BlockNumber) -> 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 = >::config(); + 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); + } +} + +/// 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::v2::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` implemention. +/// +/// 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, + ) -> DispatchResult { + 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, + ), + Err(()) => log::error!( + target: LOG_TARGET, + "Error submitting dispute slashing report, session({}), index({}), kind({:?})", + session_index, + validator_index, + kind, + ), + } + + Ok(()) + } +} diff --git a/runtime/parachains/src/disputes/slashing/benchmarking.rs b/runtime/parachains/src/disputes/slashing/benchmarking.rs new file mode 100644 index 000000000000..2a21c3a0f62b --- /dev/null +++ b/runtime/parachains/src/disputes/slashing/benchmarking.rs @@ -0,0 +1,156 @@ +// Copyright 2021 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 . + +use super::*; + +use crate::{disputes::SlashingHandler, initializer, shared}; +use frame_benchmarking::{benchmarks, whitelist_account}; +use frame_support::traits::{OnFinalize, OnInitialize}; +use frame_system::RawOrigin; +use pallet_staking::testing_utils::create_validators; +use primitives::v2::{Hash, PARACHAIN_KEY_TYPE_ID}; +use sp_runtime::traits::{One, StaticLookup}; +use sp_session::MembershipProof; + +// Candidate hash of the disputed candidate. +const CANDIDATE_HASH: CandidateHash = CandidateHash(Hash::zero()); + +pub trait Config: + pallet_session::Config + + pallet_session::historical::Config + + pallet_staking::Config + + super::Config + + shared::Config + + initializer::Config +{ +} + +fn setup_validator_set(n: u32) -> (SessionIndex, MembershipProof, ValidatorId) +where + T: Config, +{ + pallet_staking::ValidatorCount::::put(n); + + let balance_factor = 1000; + // create validators and set random session keys + for (n, who) in create_validators::(n, balance_factor).unwrap().into_iter().enumerate() { + use rand::{RngCore, SeedableRng}; + + let validator = T::Lookup::lookup(who).unwrap(); + let controller = pallet_staking::Pallet::::bonded(validator).unwrap(); + + let keys = { + const NUM_SESSION_KEYS: usize = 6; + const SESSION_KEY_LEN: usize = 32; + let mut keys = [0u8; NUM_SESSION_KEYS * SESSION_KEY_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 = vec![]; + + whitelist_account!(controller); + pallet_session::Pallet::::set_keys(RawOrigin::Signed(controller).into(), keys, proof) + .expect("session::set_keys should work"); + } + + pallet_session::Pallet::::on_initialize(T::BlockNumber::one()); + initializer::Pallet::::on_initialize(T::BlockNumber::one()); + // skip sessions until the new validator set is enacted + while pallet_session::Pallet::::validators().len() < n as usize { + pallet_session::Pallet::::rotate_session(); + } + initializer::Pallet::::on_finalize(T::BlockNumber::one()); + + let session_index = crate::shared::Pallet::::session_index(); + let session_info = crate::session_info::Pallet::::session_info(session_index); + let session_info = session_info.unwrap(); + let validator_id = session_info.validators[0].clone(); + let key = (PARACHAIN_KEY_TYPE_ID, validator_id.clone()); + let key_owner_proof = pallet_session::historical::Pallet::::prove(key).unwrap(); + + // rotate a session to make sure `key_owner_proof` is historical + initializer::Pallet::::on_initialize(T::BlockNumber::one()); + pallet_session::Pallet::::rotate_session(); + initializer::Pallet::::on_finalize(T::BlockNumber::one()); + + let idx = crate::shared::Pallet::::session_index(); + assert!( + idx > session_index, + "session rotation should work for parachain pallets: {} <= {}", + idx, + session_index, + ); + + (session_index, key_owner_proof, validator_id) +} + +fn setup_dispute(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(); + + T::SlashingHandler::punish_against_valid(session_index, CANDIDATE_HASH, losers); + + let unapplied = >::get(session_index, CANDIDATE_HASH); + assert_eq!(unapplied.unwrap().keys.len(), 1); + + dispute_proof(session_index, validator_id, validator_index) +} + +fn dispute_proof( + session_index: SessionIndex, + validator_id: ValidatorId, + validator_index: ValidatorIndex, +) -> DisputeProof { + let kind = SlashingOffenceKind::AgainstValid; + let time_slot = DisputesTimeSlot::new(session_index, CANDIDATE_HASH); + + DisputeProof { time_slot, kind, validator_index, validator_id } +} + +benchmarks! { + where_clause { + where T: Config, + } + + // in this setup we have a single `AgainstValid` dispute + // submitted for a past session + report_dispute_lost { + let n in 4..<::BenchmarkingConfig as BenchmarkingConfiguration>::MAX_VALIDATORS; + + let origin = RawOrigin::None.into(); + let (session_index, key_owner_proof, validator_id) = setup_validator_set::(n); + let dispute_proof = setup_dispute::(session_index, validator_id); + }: { + let result = Pallet::::report_dispute_lost_unsigned( + origin, + Box::new(dispute_proof), + key_owner_proof, + ); + assert!(result.is_ok()); + } verify { + let unapplied = >::get(session_index, CANDIDATE_HASH); + assert!(unapplied.is_none()); + } +} diff --git a/runtime/parachains/src/disputes/tests.rs b/runtime/parachains/src/disputes/tests.rs index 2897ced22ed0..4d8ac714cb7b 100644 --- a/runtime/parachains/src/disputes/tests.rs +++ b/runtime/parachains/src/disputes/tests.rs @@ -20,8 +20,7 @@ use crate::{ disputes::DisputesHandler, mock::{ new_test_ext, AccountId, AllPalletsWithSystem, Initializer, MockGenesisConfig, System, - Test, PUNISH_VALIDATORS_AGAINST, PUNISH_VALIDATORS_FOR, PUNISH_VALIDATORS_INCONCLUSIVE, - REWARD_VALIDATORS, + Test, PUNISH_VALIDATORS_AGAINST, PUNISH_VALIDATORS_FOR, REWARD_VALIDATORS, }, }; use assert_matches::assert_matches; @@ -500,9 +499,9 @@ fn dispute_statement_becoming_onesided_due_to_spamslots_is_accepted() { }); } -// Test that punish_inconclusive is correctly called. +// Test that dispute timeout is handled correctly. #[test] -fn test_initializer_initialize() { +fn test_dispute_timeout() { let dispute_conclusion_by_time_out_period = 3; let start = 10; @@ -602,10 +601,6 @@ fn test_initializer_initialize() { // Run to timeout + 1 in order to executive on_finalize(timeout) run_to_block(start + dispute_conclusion_by_time_out_period + 1, |_| None); assert_eq!(SpamSlots::::get(start - 1), Some(vec![0, 0, 0, 0, 0, 0, 0])); - assert_eq!( - PUNISH_VALIDATORS_INCONCLUSIVE.with(|r| r.borrow()[0].clone()), - (9, vec![ValidatorIndex(0), ValidatorIndex(6)]), - ); }); } diff --git a/runtime/parachains/src/initializer.rs b/runtime/parachains/src/initializer.rs index 027dd677bbba..a5634bf48419 100644 --- a/runtime/parachains/src/initializer.rs +++ b/runtime/parachains/src/initializer.rs @@ -21,7 +21,7 @@ use crate::{ configuration::{self, HostConfiguration}, - disputes::DisputesHandler, + disputes::{self, DisputesHandler as _, SlashingHandler as _}, dmp, hrmp, inclusion, paras, scheduler, session_info, shared, ump, }; use frame_support::{ @@ -59,6 +59,9 @@ pub struct SessionChangeNotification { pub session_index: SessionIndex, } +/// Number of validators (not only parachain) in a session. +pub type ValidatorSetCount = u32; + impl> Default for SessionChangeNotification { fn default() -> Self { Self { @@ -109,6 +112,7 @@ pub mod pallet { + scheduler::Config + inclusion::Config + session_info::Config + + disputes::Config + dmp::Config + ump::Config + hrmp::Config @@ -163,6 +167,7 @@ pub mod pallet { inclusion::Pallet::::initializer_initialize(now) + session_info::Pallet::::initializer_initialize(now) + T::DisputesHandler::initializer_initialize(now) + + T::SlashingHandler::initializer_initialize(now) + dmp::Pallet::::initializer_initialize(now) + ump::Pallet::::initializer_initialize(now) + hrmp::Pallet::::initializer_initialize(now); @@ -177,6 +182,7 @@ pub mod pallet { hrmp::Pallet::::initializer_finalize(); ump::Pallet::::initializer_finalize(); dmp::Pallet::::initializer_finalize(); + T::SlashingHandler::initializer_finalize(); T::DisputesHandler::initializer_finalize(); session_info::Pallet::::initializer_finalize(); inclusion::Pallet::::initializer_finalize(); @@ -260,6 +266,7 @@ impl Pallet { inclusion::Pallet::::initializer_on_new_session(¬ification); session_info::Pallet::::initializer_on_new_session(¬ification); T::DisputesHandler::initializer_on_new_session(¬ification); + T::SlashingHandler::initializer_on_new_session(session_index); dmp::Pallet::::initializer_on_new_session(¬ification, &outgoing_paras); ump::Pallet::::initializer_on_new_session(¬ification, &outgoing_paras); hrmp::Pallet::::initializer_on_new_session(¬ification, &outgoing_paras); diff --git a/runtime/parachains/src/mock.rs b/runtime/parachains/src/mock.rs index 8a1714070540..dc0eeb97e049 100644 --- a/runtime/parachains/src/mock.rs +++ b/runtime/parachains/src/mock.rs @@ -31,8 +31,8 @@ use frame_support::{ use frame_support_test::TestRandomness; use parity_scale_codec::Decode; use primitives::v2::{ - AuthorityDiscoveryId, Balance, BlockNumber, Header, Moment, SessionIndex, UpwardMessage, - ValidatorIndex, + AuthorityDiscoveryId, Balance, BlockNumber, CandidateHash, Header, Moment, SessionIndex, + UpwardMessage, ValidatorIndex, }; use sp_core::H256; use sp_io::TestExternalities; @@ -243,7 +243,7 @@ impl crate::hrmp::Config for Test { impl crate::disputes::Config for Test { type RuntimeEvent = RuntimeEvent; type RewardValidators = Self; - type PunishValidators = Self; + type SlashingHandler = Self; type WeightInfo = crate::disputes::TestWeightInfo; } @@ -251,7 +251,6 @@ thread_local! { pub static REWARD_VALIDATORS: RefCell)>> = RefCell::new(Vec::new()); pub static PUNISH_VALIDATORS_FOR: RefCell)>> = RefCell::new(Vec::new()); pub static PUNISH_VALIDATORS_AGAINST: RefCell)>> = RefCell::new(Vec::new()); - pub static PUNISH_VALIDATORS_INCONCLUSIVE: RefCell)>> = RefCell::new(Vec::new()); } impl crate::disputes::RewardValidators for Test { @@ -263,30 +262,31 @@ impl crate::disputes::RewardValidators for Test { } } -impl crate::disputes::PunishValidators for Test { +impl crate::disputes::SlashingHandler for Test { fn punish_for_invalid( session: SessionIndex, - validators: impl IntoIterator, + _: CandidateHash, + losers: impl IntoIterator, ) { - PUNISH_VALIDATORS_FOR - .with(|r| r.borrow_mut().push((session, validators.into_iter().collect()))) + PUNISH_VALIDATORS_FOR.with(|r| r.borrow_mut().push((session, losers.into_iter().collect()))) } fn punish_against_valid( session: SessionIndex, - validators: impl IntoIterator, + _: CandidateHash, + losers: impl IntoIterator, ) { PUNISH_VALIDATORS_AGAINST - .with(|r| r.borrow_mut().push((session, validators.into_iter().collect()))) + .with(|r| r.borrow_mut().push((session, losers.into_iter().collect()))) } - fn punish_inconclusive( - session: SessionIndex, - validators: impl IntoIterator, - ) { - PUNISH_VALIDATORS_INCONCLUSIVE - .with(|r| r.borrow_mut().push((session, validators.into_iter().collect()))) + fn initializer_initialize(_now: BlockNumber) -> Weight { + Weight::zero() } + + fn initializer_finalize() {} + + fn initializer_on_new_session(_: SessionIndex) {} } impl crate::scheduler::Config for Test {} diff --git a/runtime/polkadot/src/lib.rs b/runtime/polkadot/src/lib.rs index 11330b51df21..b906d7d6bada 100644 --- a/runtime/polkadot/src/lib.rs +++ b/runtime/polkadot/src/lib.rs @@ -1260,7 +1260,7 @@ impl parachains_initializer::Config for Runtime { impl parachains_disputes::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RewardValidators = (); - type PunishValidators = (); + type SlashingHandler = (); type WeightInfo = weights::runtime_parachains_disputes::WeightInfo; } diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index 992d0ce05688..f861594ae61e 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -37,8 +37,9 @@ use sp_std::{cmp::Ordering, collections::btree_map::BTreeMap, prelude::*}; use runtime_parachains::{ configuration as parachains_configuration, disputes as parachains_disputes, - dmp as parachains_dmp, hrmp as parachains_hrmp, inclusion as parachains_inclusion, - initializer as parachains_initializer, origin as parachains_origin, paras as parachains_paras, + disputes::slashing as parachains_slashing, dmp as parachains_dmp, hrmp as parachains_hrmp, + inclusion as parachains_inclusion, initializer as parachains_initializer, + origin as parachains_origin, paras as parachains_paras, paras_inherent as parachains_paras_inherent, runtime_api_impl::v2 as parachains_runtime_api_impl, scheduler as parachains_scheduler, session_info as parachains_session_info, shared as parachains_shared, ump as parachains_ump, @@ -1091,10 +1092,27 @@ impl parachains_initializer::Config for Runtime { impl parachains_disputes::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RewardValidators = (); - type PunishValidators = (); + type SlashingHandler = parachains_slashing::SlashValidatorsForDisputes; type WeightInfo = weights::runtime_parachains_disputes::WeightInfo; } +impl parachains_slashing::Config for Runtime { + type KeyOwnerProofSystem = Historical; + type KeyOwnerProof = + >::Proof; + type KeyOwnerIdentification = >::IdentificationTuple; + type HandleReports = parachains_slashing::SlashingReportHandler< + Self::KeyOwnerIdentification, + Offences, + ReportLongevity, + >; + type WeightInfo = parachains_slashing::TestWeightInfo; + type BenchmarkingConfig = parachains_slashing::BenchConfig<200>; +} + parameter_types! { pub const ParaDeposit: Balance = 40 * UNITS; } @@ -1374,6 +1392,7 @@ construct_runtime! { Hrmp: parachains_hrmp::{Pallet, Call, Storage, Event, Config} = 60, ParaSessionInfo: parachains_session_info::{Pallet, Storage} = 61, ParasDisputes: parachains_disputes::{Pallet, Call, Storage, Event} = 62, + ParasSlashing: parachains_slashing::{Pallet, Call, Storage, ValidateUnsigned} = 63, // Parachain Onboarding Pallets. Start indices at 70 to leave room. Registrar: paras_registrar::{Pallet, Call, Storage, Event, Config} = 70, diff --git a/runtime/test-runtime/src/lib.rs b/runtime/test-runtime/src/lib.rs index 00057014a9f3..ffaad22c5dbe 100644 --- a/runtime/test-runtime/src/lib.rs +++ b/runtime/test-runtime/src/lib.rs @@ -485,7 +485,7 @@ impl parachains_inclusion::Config for Runtime { impl parachains_disputes::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RewardValidators = (); - type PunishValidators = (); + type SlashingHandler = (); type WeightInfo = parachains_disputes::TestWeightInfo; } diff --git a/runtime/westend/src/lib.rs b/runtime/westend/src/lib.rs index a5d79e89bf9f..834972f1f431 100644 --- a/runtime/westend/src/lib.rs +++ b/runtime/westend/src/lib.rs @@ -49,8 +49,9 @@ use runtime_common::{ }; use runtime_parachains::{ configuration as parachains_configuration, disputes as parachains_disputes, - dmp as parachains_dmp, hrmp as parachains_hrmp, inclusion as parachains_inclusion, - initializer as parachains_initializer, origin as parachains_origin, paras as parachains_paras, + disputes::slashing as parachains_slashing, dmp as parachains_dmp, hrmp as parachains_hrmp, + inclusion as parachains_inclusion, initializer as parachains_initializer, + origin as parachains_origin, paras as parachains_paras, paras_inherent as parachains_paras_inherent, reward_points as parachains_reward_points, runtime_api_impl::v2 as parachains_runtime_api_impl, scheduler as parachains_scheduler, session_info as parachains_session_info, shared as parachains_shared, ump as parachains_ump, @@ -431,9 +432,9 @@ impl pallet_election_provider_multi_phase::Config for Runtime { type OffchainRepeat = OffchainRepeat; type MinerTxPriority = NposSolutionPriority; type DataProvider = Staking; - #[cfg(feature = "fast-runtime")] + #[cfg(any(feature = "fast-runtime", feature = "runtime-benchmarks"))] type Fallback = onchain::UnboundedExecution; - #[cfg(not(feature = "fast-runtime"))] + #[cfg(not(any(feature = "fast-runtime", feature = "runtime-benchmarks")))] type Fallback = pallet_election_provider_multi_phase::NoFallback; type GovernanceFallback = onchain::UnboundedExecution; type Solver = SequentialPhragmen< @@ -947,10 +948,27 @@ impl assigned_slots::Config for Runtime { impl parachains_disputes::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RewardValidators = parachains_reward_points::RewardValidatorsWithEraPoints; - type PunishValidators = (); + type SlashingHandler = parachains_slashing::SlashValidatorsForDisputes; type WeightInfo = weights::runtime_parachains_disputes::WeightInfo; } +impl parachains_slashing::Config for Runtime { + type KeyOwnerProofSystem = Historical; + type KeyOwnerProof = + >::Proof; + type KeyOwnerIdentification = >::IdentificationTuple; + type HandleReports = parachains_slashing::SlashingReportHandler< + Self::KeyOwnerIdentification, + Offences, + ReportLongevity, + >; + type WeightInfo = weights::runtime_parachains_disputes_slashing::WeightInfo; + type BenchmarkingConfig = parachains_slashing::BenchConfig<300>; +} + parameter_types! { pub const ParaDeposit: Balance = 2000 * CENTS; pub const DataDepositPerByte: Balance = deposit(0, 1); @@ -1121,6 +1139,7 @@ construct_runtime! { Hrmp: parachains_hrmp::{Pallet, Call, Storage, Event, Config} = 51, ParaSessionInfo: parachains_session_info::{Pallet, Storage} = 52, ParasDisputes: parachains_disputes::{Pallet, Call, Storage, Event} = 53, + ParasSlashing: parachains_slashing::{Pallet, Call, Storage, ValidateUnsigned} = 54, // Parachain Onboarding Pallets. Start indices at 60 to leave room. Registrar: paras_registrar::{Pallet, Call, Storage, Event, Config} = 60, @@ -1202,6 +1221,7 @@ mod benches { [runtime_common::slots, Slots] [runtime_parachains::configuration, Configuration] [runtime_parachains::disputes, ParasDisputes] + [runtime_parachains::disputes::slashing, ParasSlashing] [runtime_parachains::hrmp, Hrmp] [runtime_parachains::initializer, Initializer] [runtime_parachains::paras, Paras] @@ -1665,6 +1685,7 @@ sp_api::impl_runtime_apis! { impl pallet_election_provider_support_benchmarking::Config for Runtime {} impl frame_system_benchmarking::Config for Runtime {} impl pallet_nomination_pools_benchmarking::Config for Runtime {} + impl runtime_parachains::disputes::slashing::benchmarking::Config for Runtime {} use xcm::latest::{ AssetId::*, Fungibility::*, Junctions::*, MultiAsset, MultiAssets, MultiLocation, diff --git a/runtime/westend/src/weights/mod.rs b/runtime/westend/src/weights/mod.rs index 90394ea0fd16..df8b55067efc 100644 --- a/runtime/westend/src/weights/mod.rs +++ b/runtime/westend/src/weights/mod.rs @@ -39,6 +39,7 @@ pub mod runtime_common_paras_registrar; pub mod runtime_common_slots; pub mod runtime_parachains_configuration; pub mod runtime_parachains_disputes; +pub mod runtime_parachains_disputes_slashing; pub mod runtime_parachains_hrmp; pub mod runtime_parachains_initializer; pub mod runtime_parachains_paras; diff --git a/runtime/westend/src/weights/runtime_parachains_disputes_slashing.rs b/runtime/westend/src/weights/runtime_parachains_disputes_slashing.rs new file mode 100644 index 000000000000..868be3969728 --- /dev/null +++ b/runtime/westend/src/weights/runtime_parachains_disputes_slashing.rs @@ -0,0 +1,70 @@ +// Copyright 2017-2022 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 . +//! Autogenerated weights for `runtime_parachains::disputes::slashing` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-08-31, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! HOSTNAME: `bm3`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("westend-dev"), DB CACHE: 1024 + +// Executed Command: +// /home/benchbot/cargo_target_dir/production/polkadot +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --pallet=runtime_parachains::disputes::slashing +// --chain=westend-dev +// --header=./file_header.txt +// --output=./runtime/westend/src/weights/runtime_parachains_disputes_slashing.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for `runtime_parachains::disputes::slashing`. +pub struct WeightInfo(PhantomData); +impl runtime_parachains::disputes::slashing::WeightInfo for WeightInfo { + // Storage: Session CurrentIndex (r:1 w:0) + // Storage: Historical HistoricalSessions (r:1 w:0) + // Storage: ParaSessionInfo Sessions (r:1 w:0) + // Storage: ParasSlashing UnappliedSlashes (r:1 w:1) + // Storage: Authorship Author (r:1 w:0) + // Storage: System Digest (r:1 w:0) + // Storage: Offences ReportsByKindIndex (r:1 w:1) + // Storage: Offences ConcurrentReportsIndex (r:1 w:1) + // Storage: Offences Reports (r:1 w:1) + // Storage: Staking SlashRewardFraction (r:1 w:0) + // Storage: Staking ActiveEra (r:1 w:0) + // Storage: Staking ErasStartSessionIndex (r:1 w:0) + // Storage: Staking Invulnerables (r:1 w:0) + // Storage: Staking ValidatorSlashInEra (r:1 w:0) + /// The range of component `n` is `[4, 300]`. + fn report_dispute_lost(n: u32, ) -> Weight { + Weight::from_ref_time(97_366_000 as u64) + // Standard Error: 2_000 + .saturating_add(Weight::from_ref_time(467_000 as u64).saturating_mul(n as u64)) + .saturating_add(T::DbWeight::get().reads(14 as u64)) + .saturating_add(T::DbWeight::get().writes(4 as u64)) + } +} diff --git a/scripts/ci/gitlab/lingua.dic b/scripts/ci/gitlab/lingua.dic index 6e6d0acea6ad..ea2da595908e 100644 --- a/scripts/ci/gitlab/lingua.dic +++ b/scripts/ci/gitlab/lingua.dic @@ -51,6 +51,7 @@ Debian/M decodable/MS decrement deduplicated +deduplication deinitializing dequeue/SD dequeuing @@ -283,6 +284,7 @@ typesystem ubuntu/M UDP UI +unapplied unassign unconcluded unfinalize/B diff --git a/zombienet_tests/functional/0002-parachains-disputes.feature b/zombienet_tests/functional/0002-parachains-disputes.feature index b56cd9b06c89..9386e07e209a 100644 --- a/zombienet_tests/functional/0002-parachains-disputes.feature +++ b/zombienet_tests/functional/0002-parachains-disputes.feature @@ -48,6 +48,9 @@ eve: reports parachain_candidate_disputes_total is at least 10 within 15 seconds eve: reports parachain_candidate_dispute_concluded{validity="valid"} is at least 10 within 15 seconds eve: reports parachain_candidate_dispute_concluded{validity="invalid"} is 0 within 15 seconds +# Check there is an offence report +alice: system event contains "There is an offence reported" within 60 seconds + # Check lag - approval alice: reports polkadot_parachain_approval_checking_finality_lag is 0 bob: reports polkadot_parachain_approval_checking_finality_lag is 0 diff --git a/zombienet_tests/functional/0002-parachains-disputes.toml b/zombienet_tests/functional/0002-parachains-disputes.toml index dc909726bdae..a0a87d60d4e3 100644 --- a/zombienet_tests/functional/0002-parachains-disputes.toml +++ b/zombienet_tests/functional/0002-parachains-disputes.toml @@ -26,12 +26,10 @@ requests = { memory = "2G", cpu = "1" } name = "bob" command = "malus dispute-ancestor --fake-validation approval-invalid" args = [ "--bob", "-lparachain=debug,MALUS=trace"] - + [[relaychain.nodes]] - image = "{{MALUS_IMAGE}}" name = "charlie" - command = "malus dispute-ancestor --fake-validation approval-invalid" - args = [ "--charlie", "-lparachain=debug,MALUS=trace" ] + args = [ "--charlie", "-lparachain=debug" ] [[relaychain.nodes]] name = "dave" diff --git a/zombienet_tests/smoke/0003-deregister-register-validator-smoke.feature b/zombienet_tests/smoke/0003-deregister-register-validator-smoke.feature index 90fa4ef9d711..6a79c4cb071d 100644 --- a/zombienet_tests/smoke/0003-deregister-register-validator-smoke.feature +++ b/zombienet_tests/smoke/0003-deregister-register-validator-smoke.feature @@ -2,10 +2,10 @@ Description: Deregister / Register Validator Smoke Network: ./0003-deregister-register-validator-smoke.toml Creds: config -alice: is up -bob: is up -charlie: is up -dave: is up +alice: is up within 30 seconds +bob: is up within 30 seconds +charlie: is up within 30 seconds +dave: is up within 30 seconds # ensure is in the validator set dave: reports polkadot_node_is_parachain_validator is 1 within 240 secs