From 7cde43a29e2ee089de8fea5e77d36109e3f73467 Mon Sep 17 00:00:00 2001 From: "Alisher A. Khassanov" Date: Tue, 14 Feb 2023 15:53:12 +0600 Subject: [PATCH 1/2] Backport sp-staking from polkadot-0.9.36 --- Cargo.lock | 11 ++ Cargo.toml | 1 + primitives/staking-backport/Cargo.toml | 29 +++ primitives/staking-backport/README.md | 4 + primitives/staking-backport/src/lib.rs | 194 +++++++++++++++++++ primitives/staking-backport/src/offence.rs | 211 +++++++++++++++++++++ 6 files changed, 450 insertions(+) create mode 100644 primitives/staking-backport/Cargo.toml create mode 100644 primitives/staking-backport/README.md create mode 100644 primitives/staking-backport/src/lib.rs create mode 100644 primitives/staking-backport/src/offence.rs diff --git a/Cargo.lock b/Cargo.lock index 892ea26340a3f..826a4cf74e511 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10301,6 +10301,17 @@ dependencies = [ "sp-std", ] +[[package]] +name = "sp-staking-backport" +version = "4.0.0-dev" +dependencies = [ + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-runtime", + "sp-std", +] + [[package]] name = "sp-state-machine" version = "0.12.0" diff --git a/Cargo.toml b/Cargo.toml index 1fb8f474d9a5f..a67431f57cdd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -181,6 +181,7 @@ members = [ "primitives/serializer", "primitives/session", "primitives/staking", + "primitives/staking-backport", "primitives/state-machine", "primitives/std", "primitives/storage", diff --git a/primitives/staking-backport/Cargo.toml b/primitives/staking-backport/Cargo.toml new file mode 100644 index 0000000000000..527481b7d3009 --- /dev/null +++ b/primitives/staking-backport/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "sp-staking-backport" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "A crate which contains primitives that are useful for implementation that uses staking approaches in general. Definitions related to sessions, slashing, etc go here." +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } +sp-core = { version = "6.0.0", default-features = false, path = "../core" } +sp-runtime = { version = "6.0.0", default-features = false, path = "../runtime" } +sp-std = { version = "4.0.0", default-features = false, path = "../std" } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", +] diff --git a/primitives/staking-backport/README.md b/primitives/staking-backport/README.md new file mode 100644 index 0000000000000..892e1379d9a53 --- /dev/null +++ b/primitives/staking-backport/README.md @@ -0,0 +1,4 @@ +A crate which contains primitives that are useful for implementation that uses staking +approaches in general. Definitions related to sessions, slashing, etc go here. + +License: Apache-2.0 \ No newline at end of file diff --git a/primitives/staking-backport/src/lib.rs b/primitives/staking-backport/src/lib.rs new file mode 100644 index 0000000000000..0fbf1b16e7c98 --- /dev/null +++ b/primitives/staking-backport/src/lib.rs @@ -0,0 +1,194 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +//! A crate which contains primitives that are useful for implementation that uses staking +//! approaches in general. Definitions related to sessions, slashing, etc go here. + +use sp_runtime::{DispatchError, DispatchResult}; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; + +pub mod offence; + +/// Simple index type with which we can count sessions. +pub type SessionIndex = u32; + +/// Counter for the number of eras that have passed. +pub type EraIndex = u32; + +/// Trait describing something2 that implements a hook for any operations to perform when a staker is +/// slashed. +pub trait OnStakerSlash { + /// A hook for any operations to perform when a staker is slashed. + /// + /// # Arguments + /// + /// * `stash` - The stash of the staker whom the slash was applied to. + /// * `slashed_active` - The new bonded balance of the staker after the slash was applied. + /// * `slashed_unlocking` - A map of slashed eras, and the balance of that unlocking chunk after + /// the slash is applied. Any era not present in the map is not affected at all. + fn on_slash( + stash: &AccountId, + slashed_active: Balance, + slashed_unlocking: &BTreeMap, + ); +} + +impl OnStakerSlash for () { + fn on_slash(_: &AccountId, _: Balance, _: &BTreeMap) { + // Nothing to do here + } +} + +/// A struct that reflects stake that an account has in the staking system. Provides a set of +/// methods to operate on it's properties. Aimed at making `StakingInterface` more concise. +pub struct Stake { + /// The stash account whose balance is actually locked and at stake. + pub stash: T::AccountId, + /// The total stake that `stash` has in the staking system. This includes the + /// `active` stake, and any funds currently in the process of unbonding via + /// [`StakingInterface::unbond`]. + /// + /// # Note + /// + /// This is only guaranteed to reflect the amount locked by the staking system. If there are + /// non-staking locks on the bonded pair's balance this amount is going to be larger in + /// reality. + pub total: T::Balance, + /// The total amount of the stash's balance that will be at stake in any forthcoming + /// rounds. + pub active: T::Balance, +} + +/// A generic representation of a staking implementation. +/// +/// This interface uses the terminology of NPoS, but it is aims to be generic enough to cover other +/// implementations as well. +pub trait StakingInterface { + /// Balance type used by the staking system. + type Balance: PartialEq; + + /// AccountId type used by the staking system + type AccountId; + + /// The minimum amount required to bond in order to set nomination intentions. This does not + /// necessarily mean the nomination will be counted in an election, but instead just enough to + /// be stored as a nominator. In other words, this is the minimum amount to register the + /// intention to nominate. + fn minimum_nominator_bond() -> Self::Balance; + + /// The minimum amount required to bond in order to set validation intentions. + fn minimum_validator_bond() -> Self::Balance; + + /// Return a stash account that is controlled by a `controller`. + /// + /// ## Note + /// + /// The controller abstraction is not permanent and might go away. Avoid using this as much as + /// possible. + fn stash_by_ctrl(controller: &Self::AccountId) -> Result; + + /// Number of eras that staked funds must remain bonded for. + fn bonding_duration() -> EraIndex; + + /// The current era index. + /// + /// This should be the latest planned era that the staking system knows about. + fn current_era() -> EraIndex; + + /// Returns the stake of `who`. + fn stake(who: &Self::AccountId) -> Result, DispatchError>; + + fn total_stake(who: &Self::AccountId) -> Result { + Self::stake(who).map(|s| s.total) + } + + fn active_stake(who: &Self::AccountId) -> Result { + Self::stake(who).map(|s| s.active) + } + + fn is_unbonding(who: &Self::AccountId) -> Result { + Self::stake(who).map(|s| s.active != s.total) + } + + fn fully_unbond(who: &Self::AccountId) -> DispatchResult { + Self::unbond(who, Self::stake(who)?.active) + } + + /// Bond (lock) `value` of `who`'s balance, while forwarding any rewards to `payee`. + fn bond(who: &Self::AccountId, value: Self::Balance, payee: &Self::AccountId) + -> DispatchResult; + + /// Have `who` nominate `validators`. + fn nominate(who: &Self::AccountId, validators: Vec) -> DispatchResult; + + /// Chill `who`. + fn chill(who: &Self::AccountId) -> DispatchResult; + + /// Bond some extra amount in `who`'s free balance against the active bonded balance of + /// the account. The amount extra actually bonded will never be more than `who`'s free + /// balance. + fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult; + + /// Schedule a portion of the active bonded balance to be unlocked at era + /// [Self::current_era] + [`Self::bonding_duration`]. + /// + /// Once the unlock era has been reached, [`Self::withdraw_unbonded`] can be called to unlock + /// the funds. + /// + /// The amount of times this can be successfully called is limited based on how many distinct + /// eras funds are schedule to unlock in. Calling [`Self::withdraw_unbonded`] after some unlock + /// schedules have reached their unlocking era should allow more calls to this function. + fn unbond(stash: &Self::AccountId, value: Self::Balance) -> DispatchResult; + + /// Unlock any funds schedule to unlock before or at the current era. + /// + /// Returns whether the stash was killed because of this withdraw or not. + fn withdraw_unbonded( + stash: Self::AccountId, + num_slashing_spans: u32, + ) -> Result; + + /// The ideal number of active validators. + fn desired_validator_count() -> u32; + + /// Whether or not there is an ongoing election. + fn election_ongoing() -> bool; + + /// Force a current staker to become completely unstaked, immediately. + fn force_unstake(who: Self::AccountId) -> DispatchResult; + + /// Checks whether an account `staker` has been exposed in an era. + fn is_exposed_in_era(who: &Self::AccountId, era: &EraIndex) -> bool; + + /// Get the nominations of a stash, if they are a nominator, `None` otherwise. + #[cfg(feature = "runtime-benchmarks")] + fn nominations(who: Self::AccountId) -> Option>; + + #[cfg(feature = "runtime-benchmarks")] + fn add_era_stakers( + current_era: &EraIndex, + stash: &Self::AccountId, + exposures: Vec<(Self::AccountId, Self::Balance)>, + ); + + #[cfg(feature = "runtime-benchmarks")] + fn set_current_era(era: EraIndex); +} + +// sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); diff --git a/primitives/staking-backport/src/offence.rs b/primitives/staking-backport/src/offence.rs new file mode 100644 index 0000000000000..f6517b9e9028b --- /dev/null +++ b/primitives/staking-backport/src/offence.rs @@ -0,0 +1,211 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Common traits and types that are useful for describing offences for usage in environments +//! that use staking. + +use sp_std::vec::Vec; + +use codec::{Decode, Encode}; +use sp_runtime::Perbill; + +use crate::SessionIndex; + +/// The kind of an offence, is a byte string representing some kind identifier +/// e.g. `b"im-online:offlin"`, `b"babe:equivocatio"` +pub type Kind = [u8; 16]; + +/// Number of times the offence of this authority was already reported in the past. +/// +/// Note that we don't buffer offence reporting, so every time we see a new offence +/// of the same kind, we will report past authorities again. +/// This counter keeps track of how many times the authority was already reported in the past, +/// so that we can slash it accordingly. +pub type OffenceCount = u32; + +/// In case of an offence, which conditions get an offending validator disabled. +#[derive( + Clone, + Copy, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + Encode, + Decode, + sp_runtime::RuntimeDebug, + scale_info::TypeInfo, +)] +pub enum DisableStrategy { + /// Independently of slashing, this offence will not disable the offender. + Never, + /// Only disable the offender if it is also slashed. + WhenSlashed, + /// Independently of slashing, this offence will always disable the offender. + Always, +} + +/// A trait implemented by an offence report. +/// +/// This trait assumes that the offence is legitimate and was validated already. +/// +/// Examples of offences include: a BABE equivocation or a GRANDPA unjustified vote. +pub trait Offence { + /// Identifier which is unique for this kind of an offence. + const ID: Kind; + + /// A type that represents a point in time on an abstract timescale. + /// + /// See `Offence::time_slot` for details. The only requirement is that such timescale could be + /// represented by a single `u128` value. + type TimeSlot: Clone + codec::Codec + Ord; + + /// The list of all offenders involved in this incident. + /// + /// The list has no duplicates, so it is rather a set. + fn offenders(&self) -> Vec; + + /// The session index that is used for querying the validator set for the `slash_fraction` + /// function. + /// + /// This is used for filtering historical sessions. + fn session_index(&self) -> SessionIndex; + + /// Return a validator set count at the time when the offence took place. + fn validator_set_count(&self) -> u32; + + /// A point in time when this offence happened. + /// + /// This is used for looking up offences that happened at the "same time". + /// + /// The timescale is abstract and doesn't have to be the same across different implementations + /// of this trait. The value doesn't represent absolute timescale though since it is interpreted + /// along with the `session_index`. Two offences are considered to happen at the same time iff + /// both `session_index` and `time_slot` are equal. + /// + /// As an example, for GRANDPA timescale could be a round number and for BABE it could be a slot + /// number. Note that for GRANDPA the round number is reset each epoch. + fn time_slot(&self) -> Self::TimeSlot; + + /// In which cases this offence needs to disable offenders until the next era starts. + fn disable_strategy(&self) -> DisableStrategy { + DisableStrategy::WhenSlashed + } + + /// A slash fraction of the total exposure that should be slashed for this + /// particular offence for the `offenders_count` that happened at a singular `TimeSlot`. + /// + /// `offenders_count` - the count of unique offending authorities for this `TimeSlot`. It is >0. + fn slash_fraction(&self, offenders_count: u32) -> Perbill; +} + +/// Errors that may happen on offence reports. +#[derive(PartialEq, sp_runtime::RuntimeDebug)] +pub enum OffenceError { + /// The report has already been sumbmitted. + DuplicateReport, + + /// Other error has happened. + Other(u8), +} + +impl sp_runtime::traits::Printable for OffenceError { + fn print(&self) { + "OffenceError".print(); + match self { + Self::DuplicateReport => "DuplicateReport".print(), + Self::Other(e) => { + "Other".print(); + e.print(); + }, + } + } +} + +/// A trait for decoupling offence reporters from the actual handling of offence reports. +pub trait ReportOffence> { + /// Report an `offence` and reward given `reporters`. + fn report_offence(reporters: Vec, offence: O) -> Result<(), OffenceError>; + + /// Returns true iff all of the given offenders have been previously reported + /// at the given time slot. This function is useful to prevent the sending of + /// duplicate offence reports. + fn is_known_offence(offenders: &[Offender], time_slot: &O::TimeSlot) -> bool; +} + +impl> ReportOffence for () { + fn report_offence(_reporters: Vec, _offence: O) -> Result<(), OffenceError> { + Ok(()) + } + + fn is_known_offence(_offenders: &[Offender], _time_slot: &O::TimeSlot) -> bool { + true + } +} + +/// A trait to take action on an offence. +/// +/// Used to decouple the module that handles offences and +/// the one that should punish for those offences. +pub trait OnOffenceHandler { + /// A handler for an offence of a particular kind. + /// + /// Note that this contains a list of all previous offenders + /// as well. The implementer should cater for a case, where + /// the same authorities were reported for the same offence + /// in the past (see `OffenceCount`). + /// + /// The vector of `slash_fraction` contains `Perbill`s + /// the authorities should be slashed and is computed + /// according to the `OffenceCount` already. This is of the same length as `offenders.` + /// Zero is a valid value for a fraction. + /// + /// The `session` parameter is the session index of the offence. + /// + /// The `disable_strategy` parameter decides if the offenders need to be disabled immediately. + /// + /// The receiver might decide to not accept this offence. In this case, the call site is + /// responsible for queuing the report and re-submitting again. + fn on_offence( + offenders: &[OffenceDetails], + slash_fraction: &[Perbill], + session: SessionIndex, + disable_strategy: DisableStrategy, + ) -> Res; +} + +impl OnOffenceHandler for () { + fn on_offence( + _offenders: &[OffenceDetails], + _slash_fraction: &[Perbill], + _session: SessionIndex, + _disable_strategy: DisableStrategy, + ) -> Res { + Default::default() + } +} + +/// A details about an offending authority for a particular kind of offence. +#[derive(Clone, PartialEq, Eq, Encode, Decode, sp_runtime::RuntimeDebug, scale_info::TypeInfo)] +pub struct OffenceDetails { + /// The offending authority id + pub offender: Offender, + /// A list of reporters of offences of this authority ID. Possibly empty where there are no + /// particular reporters. + pub reporters: Vec, +} From 26271f70bf10a09bb7a1d404113c650db576e855 Mon Sep 17 00:00:00 2001 From: "Alisher A. Khassanov" Date: Fri, 24 Feb 2023 12:37:50 +0600 Subject: [PATCH 2/2] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f6bcba4029e3..a0f950083373a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [vNext] +### Added +- `sp-staking-backport` crate with `sp-staking` from `polkadot-0.9.36` substrate branch + ## [vNext] ### Added - DDC staking initial impl in `ddc-staking` pallet