diff --git a/Cargo.lock b/Cargo.lock index 5172c3d02861d..d1349ce488fa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3376,6 +3376,7 @@ dependencies = [ "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", "pallet-elections-phragmen", + "pallet-fast-unstake", "pallet-gilt", "pallet-grandpa", "pallet-identity", @@ -5718,6 +5719,31 @@ dependencies = [ "sp-tasks", ] +[[package]] +name = "pallet-fast-unstake" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-election-provider-support", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-nomination-pools", + "pallet-staking", + "pallet-staking-reward-curve", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-staking", + "sp-std", + "sp-tracing", + "substrate-test-utils", +] + [[package]] name = "pallet-gilt" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 48a31940fd3bf..018355df6c9fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ members = [ "frame/contracts/rpc/runtime-api", "frame/conviction-voting", "frame/democracy", + "frame/fast-unstake", "frame/try-runtime", "frame/election-provider-multi-phase", "frame/election-provider-support", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index d52b1aaccfcfa..e722024231651 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -68,6 +68,7 @@ pallet-democracy = { version = "4.0.0-dev", default-features = false, path = ".. pallet-election-provider-multi-phase = { version = "4.0.0-dev", default-features = false, path = "../../../frame/election-provider-multi-phase" } pallet-election-provider-support-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../../../frame/election-provider-support/benchmarking", optional = true } pallet-elections-phragmen = { version = "5.0.0-dev", default-features = false, path = "../../../frame/elections-phragmen" } +pallet-fast-unstake = { version = "4.0.0-dev", default-features = false, path = "../../../frame/fast-unstake" } pallet-gilt = { version = "4.0.0-dev", default-features = false, path = "../../../frame/gilt" } pallet-grandpa = { version = "4.0.0-dev", default-features = false, path = "../../../frame/grandpa" } pallet-im-online = { version = "4.0.0-dev", default-features = false, path = "../../../frame/im-online" } @@ -142,6 +143,7 @@ std = [ "pallet-conviction-voting/std", "pallet-democracy/std", "pallet-elections-phragmen/std", + "pallet-fast-unstake/std", "frame-executive/std", "pallet-gilt/std", "pallet-grandpa/std", @@ -220,6 +222,7 @@ runtime-benchmarks = [ "pallet-election-provider-multi-phase/runtime-benchmarks", "pallet-election-provider-support-benchmarking/runtime-benchmarks", "pallet-elections-phragmen/runtime-benchmarks", + "pallet-fast-unstake/runtime-benchmarks", "pallet-gilt/runtime-benchmarks", "pallet-grandpa/runtime-benchmarks", "pallet-identity/runtime-benchmarks", @@ -272,6 +275,7 @@ try-runtime = [ "pallet-democracy/try-runtime", "pallet-election-provider-multi-phase/try-runtime", "pallet-elections-phragmen/try-runtime", + "pallet-fast-unstake/try-runtime", "pallet-gilt/try-runtime", "pallet-grandpa/try-runtime", "pallet-im-online/try-runtime", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index d6a9798b4d9ca..8ed5f1c847f5e 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -579,6 +579,13 @@ impl pallet_staking::Config for Runtime { type BenchmarkingConfig = StakingBenchmarkingConfig; } +impl pallet_fast_unstake::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type SlashPerEra = ConstU128<{ DOLLARS }>; + type ControlOrigin = frame_system::EnsureRoot; + type WeightInfo = (); +} + parameter_types! { // phase durations. 1/4 of the last session for each. pub const SignedPhase: u32 = EPOCH_DURATION_IN_BLOCKS / 4; @@ -1655,6 +1662,7 @@ construct_runtime!( NominationPools: pallet_nomination_pools, RankedPolls: pallet_referenda::, RankedCollective: pallet_ranked_collective, + FastUnstake: pallet_fast_unstake, } ); @@ -1741,6 +1749,7 @@ mod benches { [pallet_election_provider_multi_phase, ElectionProviderMultiPhase] [pallet_election_provider_support_benchmarking, EPSBench::] [pallet_elections_phragmen, Elections] + [pallet_fast_unstake, FastUnstake] [pallet_gilt, Gilt] [pallet_grandpa, Grandpa] [pallet_identity, Identity] diff --git a/frame/election-provider-multi-phase/src/lib.rs b/frame/election-provider-multi-phase/src/lib.rs index 3c9b15dabb053..05353e5a3ac61 100644 --- a/frame/election-provider-multi-phase/src/lib.rs +++ b/frame/election-provider-multi-phase/src/lib.rs @@ -318,6 +318,10 @@ impl ElectionProvider for NoFallback { type DataProvider = T::DataProvider; type Error = &'static str; + fn ongoing() -> bool { + false + } + fn elect() -> Result, Self::Error> { // Do nothing, this will enable the emergency phase. Err("NoFallback.") @@ -1598,6 +1602,13 @@ impl ElectionProvider for Pallet { type Error = ElectionError; type DataProvider = T::DataProvider; + fn ongoing() -> bool { + match Self::current_phase() { + Phase::Off => false, + _ => true, + } + } + fn elect() -> Result, Self::Error> { match Self::do_elect() { Ok(supports) => { diff --git a/frame/election-provider-multi-phase/src/mock.rs b/frame/election-provider-multi-phase/src/mock.rs index d7affc14564f5..34aa2e1bbfc58 100644 --- a/frame/election-provider-multi-phase/src/mock.rs +++ b/frame/election-provider-multi-phase/src/mock.rs @@ -303,6 +303,10 @@ impl ElectionProvider for MockFallback { type Error = &'static str; type DataProvider = StakingMock; + fn ongoing() -> bool { + false + } + fn elect() -> Result, Self::Error> { Self::elect_with_bounds(Bounded::max_value(), Bounded::max_value()) } diff --git a/frame/election-provider-support/src/lib.rs b/frame/election-provider-support/src/lib.rs index 9f60411435da0..0bf62bd8c35cd 100644 --- a/frame/election-provider-support/src/lib.rs +++ b/frame/election-provider-support/src/lib.rs @@ -136,7 +136,7 @@ //! type BlockNumber = BlockNumber; //! type Error = &'static str; //! type DataProvider = T::DataProvider; -//! +//! fn ongoing() -> bool { false } //! fn elect() -> Result, Self::Error> { //! Self::DataProvider::electable_targets(None) //! .map_err(|_| "failed to elect") @@ -370,6 +370,9 @@ pub trait ElectionProvider { BlockNumber = Self::BlockNumber, >; + /// Indicate if this election provider is currently ongoing an asynchronous election or not. + fn ongoing() -> bool; + /// Elect a new set of winners, without specifying any bounds on the amount of data fetched from /// [`Self::DataProvider`]. An implementation could nonetheless impose its own custom limits. /// @@ -420,6 +423,10 @@ where fn elect() -> Result, Self::Error> { Err(" cannot do anything.") } + + fn ongoing() -> bool { + false + } } /// A utility trait for something to implement `ElectionDataProvider` in a sensible way. diff --git a/frame/election-provider-support/src/onchain.rs b/frame/election-provider-support/src/onchain.rs index e477499f3c927..10c3519d03df6 100644 --- a/frame/election-provider-support/src/onchain.rs +++ b/frame/election-provider-support/src/onchain.rs @@ -138,6 +138,10 @@ impl ElectionProvider for UnboundedExecution { type Error = Error; type DataProvider = T::DataProvider; + fn ongoing() -> bool { + false + } + fn elect() -> Result, Self::Error> { // This should not be called if not in `std` mode (and therefore neither in genesis nor in // testing) @@ -167,6 +171,10 @@ impl ElectionProvider for BoundedExecution { type Error = Error; type DataProvider = T::DataProvider; + fn ongoing() -> bool { + false + } + fn elect() -> Result, Self::Error> { elect_with::(Some(T::VotersBound::get() as usize), Some(T::TargetsBound::get() as usize)) } diff --git a/frame/fast-unstake/Cargo.toml b/frame/fast-unstake/Cargo.toml new file mode 100644 index 0000000000000..1fa118dba4a8d --- /dev/null +++ b/frame/fast-unstake/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "pallet-fast-unstake" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Unlicense" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME fast unstake pallet" +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 } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } + +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } + +sp-io = { version = "6.0.0", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "6.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "4.0.0", default-features = false, path = "../../primitives/std" } +sp-staking = { default-features = false, path = "../../primitives/staking" } + +pallet-balances = { default-features = false, path = "../balances" } +pallet-timestamp = { default-features = false, path = "../timestamp" } +pallet-staking = { default-features = false, path = "../staking" } +pallet-nomination-pools = { default-features = false, path = "../nomination-pools" } +frame-election-provider-support = { default-features = false, path = "../election-provider-support" } + +frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } + +[dev-dependencies] +pallet-staking-reward-curve = { version = "4.0.0-dev", path = "../staking/reward-curve" } +sp-core = { version = "6.0.0", default-features = false, path = "../../primitives/core" } +substrate-test-utils = { version = "4.0.0-dev", path = "../../test-utils" } +sp-tracing = { version = "5.0.0", path = "../../primitives/tracing" } + +[features] +default = ["std"] +std = [ + "codec/std", + "log/std", + "scale-info/std", + + "frame-support/std", + "frame-system/std", + + "sp-io/std", + "sp-staking/std", + "sp-runtime/std", + "sp-std/std", + + "pallet-staking/std", + "pallet-nomination-pools/std", + "pallet-balances/std", + "pallet-timestamp/std", + "frame-election-provider-support/std", + + "frame-benchmarking/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-staking/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/fast-unstake/src/benchmarking.rs b/frame/fast-unstake/src/benchmarking.rs new file mode 100644 index 0000000000000..68a3da0d40af3 --- /dev/null +++ b/frame/fast-unstake/src/benchmarking.rs @@ -0,0 +1,228 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Benchmarking for pallet-fast-unstake. + +#![cfg(feature = "runtime-benchmarks")] + +use crate::{types::*, Pallet as FastUnstake, *}; +use frame_benchmarking::{benchmarks, whitelist_account}; +use frame_support::{ + assert_ok, + traits::{Currency, EnsureOrigin, Get, Hooks}, +}; +use frame_system::RawOrigin; +use pallet_nomination_pools::{Pallet as Pools, PoolId}; +use pallet_staking::Pallet as Staking; +use sp_runtime::traits::{StaticLookup, Zero}; +use sp_staking::EraIndex; +use sp_std::prelude::*; + +const USER_SEED: u32 = 0; +const DEFAULT_BACKER_PER_VALIDATOR: u32 = 128; +const MAX_VALIDATORS: u32 = 128; + +type CurrencyOf = ::Currency; + +fn l( + who: T::AccountId, +) -> <::Lookup as StaticLookup>::Source { + T::Lookup::unlookup(who) +} + +fn create_unexposed_nominator() -> T::AccountId { + let account = frame_benchmarking::account::("nominator_42", 0, USER_SEED); + fund_and_bond_account::(&account); + account +} + +fn fund_and_bond_account(account: &T::AccountId) { + let stake = CurrencyOf::::minimum_balance() * 100u32.into(); + CurrencyOf::::make_free_balance_be(&account, stake * 10u32.into()); + + let account_lookup = l::(account.clone()); + // bond and nominate ourselves, this will guarantee that we are not backing anyone. + assert_ok!(Staking::::bond( + RawOrigin::Signed(account.clone()).into(), + account_lookup.clone(), + stake, + pallet_staking::RewardDestination::Controller, + )); + assert_ok!(Staking::::nominate( + RawOrigin::Signed(account.clone()).into(), + vec![account_lookup] + )); +} + +pub(crate) fn fast_unstake_events() -> Vec> { + frame_system::Pallet::::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| ::RuntimeEvent::from(e).try_into().ok()) + .collect::>() +} + +fn setup_pool() -> PoolId { + let depositor = frame_benchmarking::account::("depositor_42", 0, USER_SEED); + let depositor_lookup = l::(depositor.clone()); + + let stake = Pools::::depositor_min_bond(); + CurrencyOf::::make_free_balance_be(&depositor, stake * 10u32.into()); + + Pools::::create( + RawOrigin::Signed(depositor.clone()).into(), + stake, + depositor_lookup.clone(), + depositor_lookup.clone(), + depositor_lookup, + ) + .unwrap(); + + pallet_nomination_pools::LastPoolId::::get() +} + +fn setup_staking(v: u32, until: EraIndex) { + let ed = CurrencyOf::::minimum_balance(); + + log!(debug, "registering {} validators and {} eras.", v, until); + + // our validators don't actually need to registered in staking -- just generate `v` random + // accounts. + let validators = (0..v) + .map(|x| frame_benchmarking::account::("validator", x, USER_SEED)) + .collect::>(); + + for era in 0..=until { + let others = (0..DEFAULT_BACKER_PER_VALIDATOR) + .map(|s| { + let who = frame_benchmarking::account::("nominator", era, s); + let value = ed; + pallet_staking::IndividualExposure { who, value } + }) + .collect::>(); + let exposure = + pallet_staking::Exposure { total: Default::default(), own: Default::default(), others }; + validators.iter().for_each(|v| { + Staking::::add_era_stakers(era, v.clone(), exposure.clone()); + }); + } +} + +fn on_idle_full_block() { + let remaining_weight = ::BlockWeights::get().max_block; + FastUnstake::::on_idle(Zero::zero(), remaining_weight); +} + +benchmarks! { + // on_idle, we we don't check anyone, but fully unbond and move them to another pool. + on_idle_unstake { + let who = create_unexposed_nominator::(); + let pool_id = setup_pool::(); + assert_ok!(FastUnstake::::register_fast_unstake( + RawOrigin::Signed(who.clone()).into(), + Some(pool_id) + )); + ErasToCheckPerBlock::::put(1); + + // run on_idle once. This will check era 0. + assert_eq!(Head::::get(), None); + on_idle_full_block::(); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { stash: who.clone(), checked: vec![0].try_into().unwrap(), maybe_pool_id: Some(pool_id) }) + ); + } + : { + on_idle_full_block::(); + } + verify { + assert!(matches!( + fast_unstake_events::().last(), + Some(Event::Unstaked { .. }) + )); + } + + // on_idle, when we check some number of eras, + on_idle_check { + // number of eras multiplied by validators in that era. + let x in (::BondingDuration::get() * 1) .. (::BondingDuration::get() * MAX_VALIDATORS); + + let v = x / ::BondingDuration::get(); + let u = ::BondingDuration::get(); + + ErasToCheckPerBlock::::put(u); + pallet_staking::CurrentEra::::put(u); + + // setup staking with v validators and u eras of data (0..=u) + setup_staking::(v, u); + let who = create_unexposed_nominator::(); + assert_ok!(FastUnstake::::register_fast_unstake( + RawOrigin::Signed(who.clone()).into(), + None, + )); + + // no one is queued thus far. + assert_eq!(Head::::get(), None); + } + : { + on_idle_full_block::(); + } + verify { + let checked: frame_support::BoundedVec<_, _> = (1..=u).rev().collect::>().try_into().unwrap(); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { stash: who.clone(), checked, maybe_pool_id: None }) + ); + assert!(matches!( + fast_unstake_events::().last(), + Some(Event::Checking { .. }) + )); + } + + register_fast_unstake { + let who = create_unexposed_nominator::(); + whitelist_account!(who); + assert_eq!(Queue::::count(), 0); + + } + :_(RawOrigin::Signed(who.clone()), None) + verify { + assert_eq!(Queue::::count(), 1); + } + + deregister { + let who = create_unexposed_nominator::(); + assert_ok!(FastUnstake::::register_fast_unstake( + RawOrigin::Signed(who.clone()).into(), + None + )); + assert_eq!(Queue::::count(), 1); + whitelist_account!(who); + } + :_(RawOrigin::Signed(who.clone())) + verify { + assert_eq!(Queue::::count(), 0); + } + + control { + let origin = ::ControlOrigin::successful_origin(); + } + : _(origin, 128) + verify {} + + impl_benchmark_test_suite!(Pallet, crate::mock::ExtBuilder::default().build(), crate::mock::Runtime) +} diff --git a/frame/fast-unstake/src/lib.rs b/frame/fast-unstake/src/lib.rs new file mode 100644 index 0000000000000..51416808f48c8 --- /dev/null +++ b/frame/fast-unstake/src/lib.rs @@ -0,0 +1,505 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! A pallet that's designed to JUST do the following: +//! +//! If a nominator is not exposed in any `ErasStakers` (i.e. "has not actively backed any +//! validators in the last `BondingDuration` days"), then they can register themselves in this +//! pallet, unstake faster than having to wait an entire bonding duration, and potentially move +//! into a nomination pool. +//! +//! Appearing in the exposure of a validator means being exposed equal to that validator from the +//! point of view of the staking system. This usually means earning rewards with the validator, and +//! also being at the risk of slashing with the validator. This is equivalent to the "Active +//! Nominator" role explained in the +//! [February Staking Update](https://polkadot.network/blog/staking-update-february-2022/). +//! +//! This pallet works off the basis of `on_idle`, meaning that it provides no guarantee about when +//! it will succeed, if at all. Moreover, the queue implementation is unordered. In case of +//! congestion, no FIFO ordering is provided. +//! +//! Stakers who are certain about NOT being exposed can register themselves with +//! [`Call::register_fast_unstake`]. This will chill, and fully unbond the staker, and place them in +//! the queue to be checked. +//! +//! Once queued, but not being actively processed, stakers can withdraw their request via +//! [`Call::deregister`]. +//! +//! Once queued, a staker wishing to unbond can perform no further action in pallet-staking. This is +//! to prevent them from accidentally exposing themselves behind a validator etc. +//! +//! Once processed, if successful, no additional fee for the checking process is taken, and the +//! staker is instantly unbonded. Optionally, if they have asked to join a pool, their *entire* +//! stake is joined into their pool of choice. +//! +//! If unsuccessful, meaning that the staker was exposed sometime in the last `BondingDuration` eras +//! they will end up being slashed for the amount of wasted work they have inflicted on the chian. + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +// NOTE: enable benchmarking in tests as well. +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +mod types; +pub mod weights; + +pub const LOG_TARGET: &'static str = "runtime::fast-unstake"; + +// syntactic sugar for logging. +#[macro_export] +macro_rules! log { + ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { + log::$level!( + target: crate::LOG_TARGET, + concat!("[{:?}] 💨 ", $patter), >::block_number() $(, $values)* + ) + }; +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use crate::types::*; + use frame_election_provider_support::ElectionProvider; + use frame_support::pallet_prelude::*; + use frame_system::{pallet_prelude::*, RawOrigin}; + use pallet_nomination_pools::PoolId; + use pallet_staking::Pallet as Staking; + use sp_runtime::{ + traits::{Saturating, Zero}, + DispatchResult, + }; + use sp_staking::EraIndex; + use sp_std::{prelude::*, vec::Vec}; + use weights::WeightInfo; + + #[derive(scale_info::TypeInfo, codec::Encode, codec::Decode, codec::MaxEncodedLen)] + #[codec(mel_bound(T: Config))] + #[scale_info(skip_type_params(T))] + pub struct MaxChecking(sp_std::marker::PhantomData); + impl frame_support::traits::Get for MaxChecking { + fn get() -> u32 { + ::BondingDuration::get() + 1 + } + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: + frame_system::Config + + pallet_staking::Config< + CurrencyBalance = ::CurrencyBalance, + > + pallet_nomination_pools::Config + { + /// The overarching event type. + type RuntimeEvent: From> + + IsType<::RuntimeEvent> + + TryInto>; + + /// The amount of balance slashed per each era that was wastefully checked. + /// + /// A reasonable value could be `runtime_weight_to_fee(weight_per_era_check)`. + type SlashPerEra: Get>; + + /// The origin that can control this pallet. + type ControlOrigin: frame_support::traits::EnsureOrigin; + + /// The weight information of this pallet. + type WeightInfo: WeightInfo; + } + + /// The current "head of the queue" being unstaked. + #[pallet::storage] + pub type Head = + StorageValue<_, UnstakeRequest>, OptionQuery>; + + /// The map of all accounts wishing to be unstaked. + /// + /// Points the `AccountId` wishing to unstake to the optional `PoolId` they wish to join + /// thereafter. + #[pallet::storage] + pub type Queue = CountedStorageMap<_, Twox64Concat, T::AccountId, Option>; + + /// Number of eras to check per block. + /// + /// If set to 0, this pallet does absolutely nothing. + /// + /// Based on the amount of weight available at `on_idle`, up to this many eras of a single + /// nominator might be checked. + #[pallet::storage] + pub type ErasToCheckPerBlock = StorageValue<_, u32, ValueQuery>; + + /// The events of this pallet. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A staker was unstaked. + Unstaked { stash: T::AccountId, maybe_pool_id: Option, result: DispatchResult }, + /// A staker was slashed for requesting fast-unstake whilst being exposed. + Slashed { stash: T::AccountId, amount: BalanceOf }, + /// A staker was partially checked for the given eras, but the process did not finish. + Checking { stash: T::AccountId, eras: Vec }, + /// Some internal error happened while migrating stash. They are removed as head as a + /// consequence. + Errored { stash: T::AccountId }, + /// An internal error happened. Operations will be paused now. + InternalError, + } + + #[pallet::error] + #[cfg_attr(test, derive(PartialEq))] + pub enum Error { + /// The provided Controller account was not found. + /// + /// This means that the given account is not bonded. + NotController, + /// The bonded account has already been queued. + AlreadyQueued, + /// The bonded account has active unlocking chunks. + NotFullyBonded, + /// The provided un-staker is not in the `Queue`. + NotQueued, + /// The provided un-staker is already in Head, and cannot deregister. + AlreadyHead, + } + + #[pallet::hooks] + impl Hooks for Pallet { + fn on_idle(_: T::BlockNumber, remaining_weight: Weight) -> Weight { + if remaining_weight.any_lt(T::DbWeight::get().reads(2)) { + return Weight::from_ref_time(0) + } + + Self::do_on_idle(remaining_weight) + } + } + + #[pallet::call] + impl Pallet { + /// Register oneself for fast-unstake. + /// + /// The dispatch origin of this call must be signed by the controller account, similar to + /// `staking::unbond`. + /// + /// The stash associated with the origin must have no ongoing unlocking chunks. If + /// successful, this will fully unbond and chill the stash. Then, it will enqueue the stash + /// to be checked in further blocks. + /// + /// If by the time this is called, the stash is actually eligible for fast-unstake, then + /// they are guaranteed to remain eligible, because the call will chill them as well. + /// + /// If the check works, the entire staking data is removed, i.e. the stash is fully + /// unstaked, and they potentially join a pool with their entire bonded stake. + /// + /// If the check fails, the stash remains chilled and waiting for being unbonded as in with + /// the normal staking system, but they lose part of their unbonding chunks due to consuming + /// the chain's resources. + #[pallet::weight(::WeightInfo::register_fast_unstake())] + pub fn register_fast_unstake( + origin: OriginFor, + maybe_pool_id: Option, + ) -> DispatchResult { + let ctrl = ensure_signed(origin)?; + + let ledger = + pallet_staking::Ledger::::get(&ctrl).ok_or(Error::::NotController)?; + ensure!(!Queue::::contains_key(&ledger.stash), Error::::AlreadyQueued); + ensure!( + Head::::get().map_or(true, |UnstakeRequest { stash, .. }| stash != ledger.stash), + Error::::AlreadyHead + ); + // second part of the && is defensive. + ensure!( + ledger.active == ledger.total && ledger.unlocking.is_empty(), + Error::::NotFullyBonded + ); + + // chill and fully unstake. + Staking::::chill(RawOrigin::Signed(ctrl.clone()).into())?; + Staking::::unbond(RawOrigin::Signed(ctrl).into(), ledger.total)?; + + // enqueue them. + Queue::::insert(ledger.stash, maybe_pool_id); + Ok(()) + } + + /// Deregister oneself from the fast-unstake (also cancels joining the pool if that was + /// supplied on `register_fast_unstake` . + /// + /// This is useful if one is registered, they are still waiting, and they change their mind. + /// + /// Note that the associated stash is still fully unbonded and chilled as a consequence of + /// calling `register_fast_unstake`. This should probably be followed by a call to + /// `Staking::rebond`. + #[pallet::weight(::WeightInfo::deregister())] + pub fn deregister(origin: OriginFor) -> DispatchResult { + let ctrl = ensure_signed(origin)?; + let stash = pallet_staking::Ledger::::get(&ctrl) + .map(|l| l.stash) + .ok_or(Error::::NotController)?; + ensure!(Queue::::contains_key(&stash), Error::::NotQueued); + ensure!( + Head::::get().map_or(true, |UnstakeRequest { stash, .. }| stash != stash), + Error::::AlreadyHead + ); + Queue::::remove(stash); + Ok(()) + } + + /// Control the operation of this pallet. + /// + /// Dispatch origin must be signed by the [`Config::ControlOrigin`]. + #[pallet::weight(::WeightInfo::control())] + pub fn control(origin: OriginFor, unchecked_eras_to_check: EraIndex) -> DispatchResult { + let _ = T::ControlOrigin::ensure_origin(origin)?; + ErasToCheckPerBlock::::put(unchecked_eras_to_check); + Ok(()) + } + } + + impl Pallet { + /// process up to `remaining_weight`. + /// + /// Returns the actual weight consumed. + /// + /// Written for readability in mind, not efficiency. For example: + /// + /// 1. We assume this is only ever called once per `on_idle`. This is because we know that + /// in all use cases, even a single nominator cannot be unbonded in a single call. Multiple + /// calls to this function are thus not needed. + /// + /// 2. We will only mark a staker as unstaked if at the beginning of a check cycle, they are + /// found out to have no eras to check. At the end of a check cycle, even if they are fully + /// checked, we don't finish the process. + pub(crate) fn do_on_idle(remaining_weight: Weight) -> Weight { + let mut eras_to_check_per_block = ErasToCheckPerBlock::::get(); + if eras_to_check_per_block.is_zero() { + return T::DbWeight::get().reads(1) + } + + // NOTE: here we're assuming that the number of validators has only ever increased, + // meaning that the number of exposures to check is either this per era, or less. + let validator_count = pallet_staking::ValidatorCount::::get(); + + // determine the number of eras to check. This is based on both `ErasToCheckPerBlock` + // and `remaining_weight` passed on to us from the runtime executive. + let max_weight = |v, u| { + ::WeightInfo::on_idle_check(v * u) + .max(::WeightInfo::on_idle_unstake()) + }; + while max_weight(validator_count, eras_to_check_per_block).any_gt(remaining_weight) { + eras_to_check_per_block.saturating_dec(); + if eras_to_check_per_block.is_zero() { + log!(debug, "early existing because eras_to_check_per_block is zero"); + return T::DbWeight::get().reads(2) + } + } + + if ::ElectionProvider::ongoing() { + // NOTE: we assume `ongoing` does not consume any weight. + // there is an ongoing election -- we better not do anything. Imagine someone is not + // exposed anywhere in the last era, and the snapshot for the election is already + // taken. In this time period, we don't want to accidentally unstake them. + return T::DbWeight::get().reads(2) + } + + let UnstakeRequest { stash, mut checked, maybe_pool_id } = match Head::::take() + .or_else(|| { + // NOTE: there is no order guarantees in `Queue`. + Queue::::drain() + .map(|(stash, maybe_pool_id)| UnstakeRequest { + stash, + maybe_pool_id, + checked: Default::default(), + }) + .next() + }) { + None => { + // There's no `Head` and nothing in the `Queue`, nothing to do here. + return T::DbWeight::get().reads(4) + }, + Some(head) => head, + }; + + log!( + debug, + "checking {:?}, eras_to_check_per_block = {:?}, remaining_weight = {:?}", + stash, + eras_to_check_per_block, + remaining_weight + ); + + // the range that we're allowed to check in this round. + let current_era = pallet_staking::CurrentEra::::get().unwrap_or_default(); + let bonding_duration = ::BondingDuration::get(); + // prune all the old eras that we don't care about. This will help us keep the bound + // of `checked`. + checked.retain(|e| *e >= current_era.saturating_sub(bonding_duration)); + let unchecked_eras_to_check = { + // get the last available `bonding_duration` eras up to current era in reverse + // order. + let total_check_range = (current_era.saturating_sub(bonding_duration)..= + current_era) + .rev() + .collect::>(); + debug_assert!( + total_check_range.len() <= (bonding_duration + 1) as usize, + "{:?}", + total_check_range + ); + + // remove eras that have already been checked, take a maximum of + // eras_to_check_per_block. + total_check_range + .into_iter() + .filter(|e| !checked.contains(e)) + .take(eras_to_check_per_block as usize) + .collect::>() + }; + + log!( + debug, + "{} eras to check: {:?}", + unchecked_eras_to_check.len(), + unchecked_eras_to_check + ); + + if unchecked_eras_to_check.is_empty() { + // `stash` is not exposed in any era now -- we can let go of them now. + let num_slashing_spans = Staking::::slashing_spans(&stash).iter().count() as u32; + + let ctrl = match pallet_staking::Bonded::::get(&stash) { + Some(ctrl) => ctrl, + None => { + Self::deposit_event(Event::::Errored { stash }); + return ::WeightInfo::on_idle_unstake() + }, + }; + + let ledger = match pallet_staking::Ledger::::get(ctrl) { + Some(ledger) => ledger, + None => { + Self::deposit_event(Event::::Errored { stash }); + return ::WeightInfo::on_idle_unstake() + }, + }; + + let unstake_result = pallet_staking::Pallet::::force_unstake( + RawOrigin::Root.into(), + stash.clone(), + num_slashing_spans, + ); + + let pool_stake_result = if let Some(pool_id) = maybe_pool_id { + pallet_nomination_pools::Pallet::::join( + RawOrigin::Signed(stash.clone()).into(), + ledger.total, + pool_id, + ) + } else { + Ok(()) + }; + + let result = unstake_result.and(pool_stake_result); + log!( + info, + "unstaked {:?}, maybe_pool {:?}, outcome: {:?}", + stash, + maybe_pool_id, + result + ); + + Self::deposit_event(Event::::Unstaked { stash, maybe_pool_id, result }); + ::WeightInfo::on_idle_unstake() + } else { + // eras remaining to be checked. + let mut eras_checked = 0u32; + let is_exposed = unchecked_eras_to_check.iter().any(|e| { + eras_checked.saturating_inc(); + Self::is_exposed_in_era(&stash, e) + }); + + log!( + debug, + "checked {:?} eras, exposed? {}, (v: {:?}, u: {:?})", + eras_checked, + is_exposed, + validator_count, + unchecked_eras_to_check.len() + ); + + // NOTE: you can be extremely unlucky and get slashed here: You are not exposed in + // the last 28 eras, have registered yourself to be unstaked, midway being checked, + // you are exposed. + if is_exposed { + let amount = T::SlashPerEra::get() + .saturating_mul(eras_checked.saturating_add(checked.len() as u32).into()); + pallet_staking::slashing::do_slash::( + &stash, + amount, + &mut Default::default(), + &mut Default::default(), + current_era, + ); + log!(info, "slashed {:?} by {:?}", stash, amount); + Self::deposit_event(Event::::Slashed { stash, amount }); + } else { + // Not exposed in these eras. + match checked.try_extend(unchecked_eras_to_check.clone().into_iter()) { + Ok(_) => { + Head::::put(UnstakeRequest { + stash: stash.clone(), + checked, + maybe_pool_id, + }); + Self::deposit_event(Event::::Checking { + stash, + eras: unchecked_eras_to_check, + }); + }, + Err(_) => { + // don't put the head back in -- there is an internal error in the + // pallet. + frame_support::defensive!("`checked is pruned via retain above`"); + ErasToCheckPerBlock::::put(0); + Self::deposit_event(Event::::InternalError); + }, + } + } + + ::WeightInfo::on_idle_check(validator_count * eras_checked) + } + } + + /// Checks whether an account `staker` has been exposed in an era. + fn is_exposed_in_era(staker: &T::AccountId, era: &EraIndex) -> bool { + pallet_staking::ErasStakers::::iter_prefix(era).any(|(validator, exposures)| { + validator == *staker || exposures.others.iter().any(|i| i.who == *staker) + }) + } + } +} diff --git a/frame/fast-unstake/src/mock.rs b/frame/fast-unstake/src/mock.rs new file mode 100644 index 0000000000000..b9cf16e18e8d1 --- /dev/null +++ b/frame/fast-unstake/src/mock.rs @@ -0,0 +1,387 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +use crate::{self as fast_unstake}; +use frame_support::{ + assert_ok, + pallet_prelude::*, + parameter_types, + traits::{ConstU64, ConstU8, Currency}, + weights::constants::WEIGHT_PER_SECOND, + PalletId, +}; +use sp_runtime::{ + traits::{Convert, IdentityLookup}, + FixedU128, +}; + +use frame_system::RawOrigin; +use pallet_staking::{Exposure, IndividualExposure, StakerStatus}; +use sp_std::prelude::*; + +pub type AccountId = u128; +pub type AccountIndex = u32; +pub type BlockNumber = u64; +pub type Balance = u128; +pub type T = Runtime; + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(2u64 * WEIGHT_PER_SECOND); +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = BlockWeights; + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = AccountIndex; + type BlockNumber = BlockNumber; + type RuntimeCall = RuntimeCall; + type Hash = sp_core::H256; + type Hashing = sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = sp_runtime::testing::Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<5>; + type WeightInfo = (); +} + +parameter_types! { + pub static ExistentialDeposit: Balance = 1; +} + +impl pallet_balances::Config for Runtime { + type MaxLocks = ConstU32<128>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); +} + +pallet_staking_reward_curve::build! { + const I_NPOS: sp_runtime::curve::PiecewiseLinear<'static> = curve!( + min_inflation: 0_025_000, + max_inflation: 0_100_000, + ideal_stake: 0_500_000, + falloff: 0_050_000, + max_piece_count: 40, + test_precision: 0_005_000, + ); +} + +parameter_types! { + pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS; + pub static BondingDuration: u32 = 3; + pub static CurrentEra: u32 = 0; + pub static Ongoing: bool = false; +} + +pub struct MockElection; +impl frame_election_provider_support::ElectionProvider for MockElection { + type AccountId = AccountId; + type BlockNumber = BlockNumber; + type DataProvider = Staking; + type Error = (); + + fn ongoing() -> bool { + Ongoing::get() + } + + fn elect() -> Result, Self::Error> { + Err(()) + } +} + +impl pallet_staking::Config for Runtime { + type MaxNominations = ConstU32<16>; + type Currency = Balances; + type CurrencyBalance = Balance; + type UnixTime = pallet_timestamp::Pallet; + type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote; + type RewardRemainder = (); + type RuntimeEvent = RuntimeEvent; + type Slash = (); + type Reward = (); + type SessionsPerEra = (); + type SlashDeferDuration = (); + type SlashCancelOrigin = frame_system::EnsureRoot; + type BondingDuration = BondingDuration; + type SessionInterface = (); + type EraPayout = pallet_staking::ConvertCurve; + type NextNewSession = (); + type HistoryDepth = ConstU32<84>; + type MaxNominatorRewardedPerValidator = ConstU32<64>; + type OffendingValidatorsThreshold = (); + type ElectionProvider = MockElection; + type GenesisElectionProvider = Self::ElectionProvider; + type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; + type TargetList = pallet_staking::UseValidatorsMap; + type MaxUnlockingChunks = ConstU32<32>; + type OnStakerSlash = Pools; + type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; + type WeightInfo = (); +} + +pub struct BalanceToU256; +impl Convert for BalanceToU256 { + fn convert(n: Balance) -> sp_core::U256 { + n.into() + } +} + +pub struct U256ToBalance; +impl Convert for U256ToBalance { + fn convert(n: sp_core::U256) -> Balance { + n.try_into().unwrap() + } +} + +parameter_types! { + pub const PostUnbondingPoolsWindow: u32 = 10; + pub const PoolsPalletId: PalletId = PalletId(*b"py/nopls"); + pub static MaxMetadataLen: u32 = 10; + pub static CheckLevel: u8 = 255; +} + +impl pallet_nomination_pools::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type Currency = Balances; + type CurrencyBalance = Balance; + type RewardCounter = FixedU128; + type BalanceToU256 = BalanceToU256; + type U256ToBalance = U256ToBalance; + type StakingInterface = Staking; + type PostUnbondingPoolsWindow = PostUnbondingPoolsWindow; + type MaxMetadataLen = MaxMetadataLen; + type MaxUnbonding = ConstU32<8>; + type MaxPointsToBalance = ConstU8<10>; + type PalletId = PoolsPalletId; +} + +parameter_types! { + pub static SlashPerEra: u32 = 100; +} + +impl fast_unstake::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type SlashPerEra = SlashPerEra; + type ControlOrigin = frame_system::EnsureRoot; + type WeightInfo = (); +} + +type Block = frame_system::mocking::MockBlock; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +frame_support::construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic + { + System: frame_system, + Timestamp: pallet_timestamp, + Balances: pallet_balances, + Staking: pallet_staking, + Pools: pallet_nomination_pools, + FastUnstake: fast_unstake, + } +); + +parameter_types! { + static FastUnstakeEvents: u32 = 0; +} + +pub(crate) fn fast_unstake_events_since_last_call() -> Vec> { + let events = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let RuntimeEvent::FastUnstake(inner) = e { Some(inner) } else { None }) + .collect::>(); + let already_seen = FastUnstakeEvents::get(); + FastUnstakeEvents::set(events.len() as u32); + events.into_iter().skip(already_seen as usize).collect() +} + +pub struct ExtBuilder { + exposed_nominators: Vec<(AccountId, AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + exposed_nominators: vec![ + (1, 2, 100), + (3, 4, 100), + (5, 6, 100), + (7, 8, 100), + (9, 10, 100), + ], + } + } +} + +pub(crate) const VALIDATORS_PER_ERA: AccountId = 32; +pub(crate) const VALIDATOR_PREFIX: AccountId = 100; +pub(crate) const NOMINATORS_PER_VALIDATOR_PER_ERA: AccountId = 4; +pub(crate) const NOMINATOR_PREFIX: AccountId = 1000; + +impl ExtBuilder { + pub(crate) fn register_stakers_for_era(era: u32) { + // validators are prefixed with 100 and nominators with 1000 to prevent conflict. Make sure + // all the other accounts used in tests are below 100. Also ensure here that we don't + // overlap. + assert!(VALIDATOR_PREFIX + VALIDATORS_PER_ERA < NOMINATOR_PREFIX); + + (VALIDATOR_PREFIX..VALIDATOR_PREFIX + VALIDATORS_PER_ERA) + .map(|v| { + // for the sake of sanity, let's register this taker as an actual validator. + let others = (NOMINATOR_PREFIX.. + (NOMINATOR_PREFIX + NOMINATORS_PER_VALIDATOR_PER_ERA)) + .map(|n| IndividualExposure { who: n, value: 0 as Balance }) + .collect::>(); + (v, Exposure { total: 0, own: 0, others }) + }) + .for_each(|(validator, exposure)| { + pallet_staking::ErasStakers::::insert(era, validator, exposure); + }); + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + let mut storage = + frame_system::GenesisConfig::default().build_storage::().unwrap(); + + // create one default pool. + let _ = pallet_nomination_pools::GenesisConfig:: { ..Default::default() } + .assimilate_storage(&mut storage); + + let validators_range = VALIDATOR_PREFIX..VALIDATOR_PREFIX + VALIDATORS_PER_ERA; + let nominators_range = + NOMINATOR_PREFIX..NOMINATOR_PREFIX + NOMINATORS_PER_VALIDATOR_PER_ERA; + + let _ = pallet_balances::GenesisConfig:: { + balances: self + .exposed_nominators + .clone() + .into_iter() + .map(|(stash, _, balance)| (stash, balance * 2)) + .chain( + self.exposed_nominators + .clone() + .into_iter() + .map(|(_, ctrl, balance)| (ctrl, balance * 2)), + ) + .chain(validators_range.clone().map(|x| (x, 100))) + .chain(nominators_range.clone().map(|x| (x, 100))) + .collect::>(), + } + .assimilate_storage(&mut storage); + + let _ = pallet_staking::GenesisConfig:: { + stakers: self + .exposed_nominators + .into_iter() + .map(|(x, y, z)| (x, y, z, pallet_staking::StakerStatus::Nominator(vec![42]))) + .chain(validators_range.map(|x| (x, x, 100, StakerStatus::Validator))) + .chain(nominators_range.map(|x| (x, x, 100, StakerStatus::Nominator(vec![x])))) + .collect::>(), + ..Default::default() + } + .assimilate_storage(&mut storage); + + let mut ext = sp_io::TestExternalities::from(storage); + + ext.execute_with(|| { + // for events to be deposited. + frame_system::Pallet::::set_block_number(1); + + for era in 0..=(BondingDuration::get()) { + Self::register_stakers_for_era(era); + } + + // because we read this value as a measure of how many validators we have. + pallet_staking::ValidatorCount::::put(VALIDATORS_PER_ERA as u32); + + // make a pool + let amount_to_bond = Pools::depositor_min_bond(); + Balances::make_free_balance_be(&10, amount_to_bond * 5); + assert_ok!(Pools::create(RawOrigin::Signed(10).into(), amount_to_bond, 900, 901, 902)); + }); + ext + } + + pub fn build_and_execute(self, test: impl FnOnce() -> ()) { + self.build().execute_with(|| { + test(); + }) + } +} + +pub(crate) fn run_to_block(n: u64, on_idle: bool) { + let current_block = System::block_number(); + assert!(n > current_block); + while System::block_number() < n { + Balances::on_finalize(System::block_number()); + Staking::on_finalize(System::block_number()); + Pools::on_finalize(System::block_number()); + FastUnstake::on_finalize(System::block_number()); + + System::set_block_number(System::block_number() + 1); + + Balances::on_initialize(System::block_number()); + Staking::on_initialize(System::block_number()); + Pools::on_initialize(System::block_number()); + FastUnstake::on_initialize(System::block_number()); + if on_idle { + FastUnstake::on_idle(System::block_number(), BlockWeights::get().max_block); + } + } +} + +pub(crate) fn next_block(on_idle: bool) { + let current = System::block_number(); + run_to_block(current + 1, on_idle); +} + +pub fn assert_unstaked(stash: &AccountId) { + assert!(!pallet_staking::Bonded::::contains_key(stash)); + assert!(!pallet_staking::Payee::::contains_key(stash)); + assert!(!pallet_staking::Validators::::contains_key(stash)); + assert!(!pallet_staking::Nominators::::contains_key(stash)); +} diff --git a/frame/fast-unstake/src/tests.rs b/frame/fast-unstake/src/tests.rs new file mode 100644 index 0000000000000..a51c1acdf06eb --- /dev/null +++ b/frame/fast-unstake/src/tests.rs @@ -0,0 +1,1036 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Tests for pallet-fast-unstake. + +use super::*; +use crate::{mock::*, types::*, weights::WeightInfo, Event}; +use frame_support::{assert_noop, assert_ok, bounded_vec, pallet_prelude::*, traits::Currency}; +use pallet_nomination_pools::{BondedPools, LastPoolId, RewardPools}; +use pallet_staking::{CurrentEra, IndividualExposure, RewardDestination}; + +use sp_runtime::{traits::BadOrigin, DispatchError, ModuleError}; +use sp_staking::StakingInterface; + +#[test] +fn test_setup_works() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(BondedPools::::count(), 1); + assert_eq!(RewardPools::::count(), 1); + assert_eq!(Staking::bonding_duration(), 3); + let last_pool = LastPoolId::::get(); + assert_eq!(last_pool, 1); + }); +} + +#[test] +fn register_works() { + ExtBuilder::default().build_and_execute(|| { + // Controller account registers for fast unstake. + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32))); + // Ensure stash is in the queue. + assert_ne!(Queue::::get(1), None); + }); +} + +#[test] +fn cannot_register_if_not_bonded() { + ExtBuilder::default().build_and_execute(|| { + // Mint accounts 1 and 2 with 200 tokens. + for _ in 1..2 { + let _ = Balances::make_free_balance_be(&1, 200); + } + // Attempt to fast unstake. + assert_noop!( + FastUnstake::register_fast_unstake(RuntimeOrigin::signed(1), Some(1_u32)), + Error::::NotController + ); + }); +} + +#[test] +fn cannot_register_if_in_queue() { + ExtBuilder::default().build_and_execute(|| { + // Insert some Queue item + Queue::::insert(1, Some(1_u32)); + // Cannot re-register, already in queue + assert_noop!( + FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)), + Error::::AlreadyQueued + ); + }); +} + +#[test] +fn cannot_register_if_head() { + ExtBuilder::default().build_and_execute(|| { + // Insert some Head item for stash + Head::::put(UnstakeRequest { stash: 1, checked: bounded_vec![], maybe_pool_id: None }); + // Controller attempts to regsiter + assert_noop!( + FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)), + Error::::AlreadyHead + ); + }); +} + +#[test] +fn cannot_register_if_has_unlocking_chunks() { + ExtBuilder::default().build_and_execute(|| { + // Start unbonding half of staked tokens + assert_ok!(Staking::unbond(RuntimeOrigin::signed(2), 50_u128)); + // Cannot register for fast unstake with unlock chunks active + assert_noop!( + FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)), + Error::::NotFullyBonded + ); + }); +} + +#[test] +fn deregister_works() { + ExtBuilder::default().build_and_execute(|| { + // Controller account registers for fast unstake. + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32))); + // Controller then changes mind and deregisters. + assert_ok!(FastUnstake::deregister(RuntimeOrigin::signed(2))); + // Ensure stash no longer exists in the queue. + assert_eq!(Queue::::get(1), None); + }); +} + +#[test] +fn cannot_deregister_if_not_controller() { + ExtBuilder::default().build_and_execute(|| { + // Controller account registers for fast unstake. + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32))); + // Stash tries to deregister. + assert_noop!(FastUnstake::deregister(RuntimeOrigin::signed(1)), Error::::NotController); + }); +} + +#[test] +fn cannot_deregister_if_not_queued() { + ExtBuilder::default().build_and_execute(|| { + // Controller tries to deregister without first registering + assert_noop!(FastUnstake::deregister(RuntimeOrigin::signed(2)), Error::::NotQueued); + }); +} + +#[test] +fn cannot_deregister_already_head() { + ExtBuilder::default().build_and_execute(|| { + // Controller attempts to register, should fail + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32))); + // Insert some Head item for stash. + Head::::put(UnstakeRequest { stash: 1, checked: bounded_vec![], maybe_pool_id: None }); + // Controller attempts to deregister + assert_noop!(FastUnstake::deregister(RuntimeOrigin::signed(2)), Error::::AlreadyHead); + }); +} + +#[test] +fn control_works() { + ExtBuilder::default().build_and_execute(|| { + // account with control (root) origin wants to only check 1 era per block. + assert_ok!(FastUnstake::control(RuntimeOrigin::root(), 1_u32)); + }); +} + +#[test] +fn control_must_be_control_origin() { + ExtBuilder::default().build_and_execute(|| { + // account without control (root) origin wants to only check 1 era per block. + assert_noop!(FastUnstake::control(RuntimeOrigin::signed(1), 1_u32), BadOrigin); + }); +} + +mod on_idle { + use super::*; + + #[test] + fn early_exit() { + ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(BondingDuration::get() + 1); + CurrentEra::::put(BondingDuration::get()); + + // set up Queue item + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1))); + assert_eq!(Queue::::get(1), Some(Some(1))); + + // call on_idle with no remaining weight + FastUnstake::on_idle(System::block_number(), Weight::from_ref_time(0)); + + // assert nothing changed in Queue and Head + assert_eq!(Head::::get(), None); + assert_eq!(Queue::::get(1), Some(Some(1))); + }); + } + + #[test] + fn respects_weight() { + ExtBuilder::default().build_and_execute(|| { + // we want to check all eras in one block... + ErasToCheckPerBlock::::put(BondingDuration::get() + 1); + CurrentEra::::put(BondingDuration::get()); + + // given + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1))); + assert_eq!(Queue::::get(1), Some(Some(1))); + + assert_eq!(Queue::::count(), 1); + assert_eq!(Head::::get(), None); + + // when: call fast unstake with not enough weight to process the whole thing, just one + // era. + let remaining_weight = ::WeightInfo::on_idle_check( + pallet_staking::ValidatorCount::::get() * 1, + ); + assert_eq!(FastUnstake::on_idle(0, remaining_weight), remaining_weight); + + // then + assert_eq!( + fast_unstake_events_since_last_call(), + vec![Event::Checking { stash: 1, eras: vec![3] }] + ); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { stash: 1, checked: bounded_vec![3], maybe_pool_id: Some(1) }) + ); + + // when: another 1 era. + let remaining_weight = ::WeightInfo::on_idle_check( + pallet_staking::ValidatorCount::::get() * 1, + ); + assert_eq!(FastUnstake::on_idle(0, remaining_weight), remaining_weight); + + // then: + assert_eq!( + fast_unstake_events_since_last_call(), + vec![Event::Checking { stash: 1, eras: bounded_vec![2] }] + ); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2], + maybe_pool_id: Some(1) + }) + ); + + // when: then 5 eras, we only need 2 more. + let remaining_weight = ::WeightInfo::on_idle_check( + pallet_staking::ValidatorCount::::get() * 5, + ); + assert_eq!( + FastUnstake::on_idle(0, remaining_weight), + // note the amount of weight consumed: 2 eras worth of weight. + ::WeightInfo::on_idle_check( + pallet_staking::ValidatorCount::::get() * 2, + ) + ); + + // then: + assert_eq!( + fast_unstake_events_since_last_call(), + vec![Event::Checking { stash: 1, eras: vec![1, 0] }] + ); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2, 1, 0], + maybe_pool_id: Some(1) + }) + ); + + // when: not enough weight to unstake: + let remaining_weight = + ::WeightInfo::on_idle_unstake() - Weight::from_ref_time(1); + assert_eq!(FastUnstake::on_idle(0, remaining_weight), Weight::from_ref_time(0)); + + // then nothing happens: + assert_eq!(fast_unstake_events_since_last_call(), vec![]); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2, 1, 0], + maybe_pool_id: Some(1) + }) + ); + + // when: enough weight to get over at least one iteration: then we are unblocked and can + // unstake. + let remaining_weight = ::WeightInfo::on_idle_check( + pallet_staking::ValidatorCount::::get() * 1, + ); + assert_eq!( + FastUnstake::on_idle(0, remaining_weight), + ::WeightInfo::on_idle_unstake() + ); + + // then we finish the unbonding: + assert_eq!( + fast_unstake_events_since_last_call(), + vec![Event::Unstaked { stash: 1, maybe_pool_id: Some(1), result: Ok(()) }] + ); + assert_eq!(Head::::get(), None,); + + assert_unstaked(&1); + }); + } + + #[test] + fn if_head_not_set_one_random_fetched_from_queue() { + ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(BondingDuration::get() + 1); + CurrentEra::::put(BondingDuration::get()); + + // given + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), None)); + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(4), None)); + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(6), None)); + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(8), None)); + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(10), None)); + + assert_eq!(Queue::::count(), 5); + assert_eq!(Head::::get(), None); + + // when + next_block(true); + + // then + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2, 1, 0], + maybe_pool_id: None + }) + ); + assert_eq!(Queue::::count(), 4); + + // when + next_block(true); + + // then + assert_eq!(Head::::get(), None,); + assert_eq!(Queue::::count(), 4); + + // when + next_block(true); + + // then + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 5, + checked: bounded_vec![3, 2, 1, 0], + maybe_pool_id: None + }), + ); + assert_eq!(Queue::::count(), 3); + + assert_eq!( + fast_unstake_events_since_last_call(), + vec![ + Event::Checking { stash: 1, eras: vec![3, 2, 1, 0] }, + Event::Unstaked { stash: 1, maybe_pool_id: None, result: Ok(()) }, + Event::Checking { stash: 5, eras: vec![3, 2, 1, 0] } + ] + ); + }); + } + + #[test] + fn successful_multi_queue() { + ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(BondingDuration::get() + 1); + CurrentEra::::put(BondingDuration::get()); + + // register multi accounts for fast unstake + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1))); + assert_eq!(Queue::::get(1), Some(Some(1))); + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(4), Some(1))); + assert_eq!(Queue::::get(3), Some(Some(1))); + + // assert 2 queue items are in Queue & None in Head to start with + assert_eq!(Queue::::count(), 2); + assert_eq!(Head::::get(), None); + + // process on idle and check eras for next Queue item + next_block(true); + + // process on idle & let go of current Head + next_block(true); + + // confirm Head / Queue items remaining + assert_eq!(Queue::::count(), 1); + assert_eq!(Head::::get(), None); + + // process on idle and check eras for next Queue item + next_block(true); + + // process on idle & let go of current Head + next_block(true); + + // Head & Queue should now be empty + assert_eq!(Head::::get(), None); + assert_eq!(Queue::::count(), 0); + + assert_eq!( + fast_unstake_events_since_last_call(), + vec![ + Event::Checking { stash: 1, eras: vec![3, 2, 1, 0] }, + Event::Unstaked { stash: 1, maybe_pool_id: Some(1), result: Ok(()) }, + Event::Checking { stash: 3, eras: vec![3, 2, 1, 0] }, + Event::Unstaked { stash: 3, maybe_pool_id: Some(1), result: Ok(()) }, + ] + ); + + assert_unstaked(&1); + assert_unstaked(&3); + }); + } + + #[test] + fn successful_unstake_without_pool_join() { + ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(BondingDuration::get() + 1); + CurrentEra::::put(BondingDuration::get()); + + // register for fast unstake + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), None)); + assert_eq!(Queue::::get(1), Some(None)); + + // process on idle + next_block(true); + + // assert queue item has been moved to head + assert_eq!(Queue::::get(1), None); + + // assert head item present + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2, 1, 0], + maybe_pool_id: None + }) + ); + + next_block(true); + assert_eq!(Head::::get(), None,); + + assert_eq!( + fast_unstake_events_since_last_call(), + vec![ + Event::Checking { stash: 1, eras: vec![3, 2, 1, 0] }, + Event::Unstaked { stash: 1, maybe_pool_id: None, result: Ok(()) } + ] + ); + assert_unstaked(&1); + }); + } + + #[test] + fn successful_unstake_joining_bad_pool() { + ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(BondingDuration::get() + 1); + CurrentEra::::put(BondingDuration::get()); + + // register for fast unstake + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(0))); + assert_eq!(Queue::::get(1), Some(Some(0))); + + // process on idle + next_block(true); + + // assert queue item has been moved to head + assert_eq!(Queue::::get(1), None); + + // assert head item present + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2, 1, 0], + maybe_pool_id: Some(0) + }) + ); + + next_block(true); + assert_eq!(Head::::get(), None,); + + assert_eq!( + fast_unstake_events_since_last_call(), + vec![ + Event::Checking { stash: 1, eras: vec![3, 2, 1, 0] }, + Event::Unstaked { + stash: 1, + maybe_pool_id: Some(0), + result: Err(DispatchError::Module(ModuleError { + index: 4, + error: [0, 0, 0, 0], + message: None + })) + } + ] + ); + assert_unstaked(&1); + }); + } + + #[test] + fn successful_unstake_all_eras_per_block() { + ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(BondingDuration::get() + 1); + CurrentEra::::put(BondingDuration::get()); + + // register for fast unstake + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32))); + assert_eq!(Queue::::get(1), Some(Some(1))); + + // process on idle + next_block(true); + + // assert queue item has been moved to head + assert_eq!(Queue::::get(1), None); + + // assert head item present + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2, 1, 0], + maybe_pool_id: Some(1) + }) + ); + + next_block(true); + assert_eq!(Head::::get(), None,); + + assert_eq!( + fast_unstake_events_since_last_call(), + vec![ + Event::Checking { stash: 1, eras: vec![3, 2, 1, 0] }, + Event::Unstaked { stash: 1, maybe_pool_id: Some(1), result: Ok(()) } + ] + ); + assert_unstaked(&1); + assert!(pallet_nomination_pools::PoolMembers::::contains_key(&1)); + }); + } + + #[test] + fn successful_unstake_one_era_per_block() { + ExtBuilder::default().build_and_execute(|| { + // put 1 era per block + ErasToCheckPerBlock::::put(1); + CurrentEra::::put(BondingDuration::get()); + + // register for fast unstake + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32))); + assert_eq!(Queue::::get(1), Some(Some(1))); + + // process on idle + next_block(true); + + // assert queue item has been moved to head + assert_eq!(Queue::::get(1), None); + + // assert head item present + assert_eq!( + Head::::get(), + Some(UnstakeRequest { stash: 1, checked: bounded_vec![3], maybe_pool_id: Some(1) }) + ); + + next_block(true); + + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2], + maybe_pool_id: Some(1) + }) + ); + + next_block(true); + + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2, 1], + maybe_pool_id: Some(1) + }) + ); + + next_block(true); + + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2, 1, 0], + maybe_pool_id: Some(1) + }) + ); + + next_block(true); + + assert_eq!(Head::::get(), None,); + + assert_eq!( + fast_unstake_events_since_last_call(), + vec![ + Event::Checking { stash: 1, eras: vec![3] }, + Event::Checking { stash: 1, eras: vec![2] }, + Event::Checking { stash: 1, eras: vec![1] }, + Event::Checking { stash: 1, eras: vec![0] }, + Event::Unstaked { stash: 1, maybe_pool_id: Some(1), result: Ok(()) } + ] + ); + assert_unstaked(&1); + assert!(pallet_nomination_pools::PoolMembers::::contains_key(&1)); + }); + } + + #[test] + fn old_checked_era_pruned() { + // the only scenario where checked era pruning (checked.retain) comes handy is a follows: + // the whole vector is full and at capacity and in the next call we are ready to unstake, + // but then a new era happens. + ExtBuilder::default().build_and_execute(|| { + // given + ErasToCheckPerBlock::::put(1); + CurrentEra::::put(BondingDuration::get()); + + // register for fast unstake + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), None)); + assert_eq!(Queue::::get(1), Some(None)); + + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { stash: 1, checked: bounded_vec![3], maybe_pool_id: None }) + ); + + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2], maybe_pool_id: None }) + ); + + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2, 1], + maybe_pool_id: None + }) + ); + + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2, 1, 0], + maybe_pool_id: None + }) + ); + + // when: a new era happens right before one is free. + CurrentEra::::put(CurrentEra::::get().unwrap() + 1); + ExtBuilder::register_stakers_for_era(CurrentEra::::get().unwrap()); + + // then + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + // note era 0 is pruned to keep the vector length sane. + checked: bounded_vec![3, 2, 1, 4], + maybe_pool_id: None + }) + ); + + next_block(true); + assert_eq!(Head::::get(), None); + + assert_eq!( + fast_unstake_events_since_last_call(), + vec![ + Event::Checking { stash: 1, eras: vec![3] }, + Event::Checking { stash: 1, eras: vec![2] }, + Event::Checking { stash: 1, eras: vec![1] }, + Event::Checking { stash: 1, eras: vec![0] }, + Event::Checking { stash: 1, eras: vec![4] }, + Event::Unstaked { stash: 1, maybe_pool_id: None, result: Ok(()) } + ] + ); + assert_unstaked(&1); + }); + } + + #[test] + fn unstake_paused_mid_election() { + ExtBuilder::default().build_and_execute(|| { + // give: put 1 era per block + ErasToCheckPerBlock::::put(1); + CurrentEra::::put(BondingDuration::get()); + + // register for fast unstake + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32))); + + // process 2 blocks + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { stash: 1, checked: bounded_vec![3], maybe_pool_id: Some(1) }) + ); + + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2], + maybe_pool_id: Some(1) + }) + ); + + // when + Ongoing::set(true); + + // then nothing changes + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2], + maybe_pool_id: Some(1) + }) + ); + + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2], + maybe_pool_id: Some(1) + }) + ); + + // then we register a new era. + Ongoing::set(false); + CurrentEra::::put(CurrentEra::::get().unwrap() + 1); + ExtBuilder::register_stakers_for_era(CurrentEra::::get().unwrap()); + + // then we can progress again, but notice that the new era that had to be checked. + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2, 4], + maybe_pool_id: Some(1) + }) + ); + + // progress to end + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 1, + checked: bounded_vec![3, 2, 4, 1], + maybe_pool_id: Some(1) + }) + ); + + // but notice that we don't care about era 0 instead anymore! we're done. + next_block(true); + assert_eq!(Head::::get(), None); + + assert_eq!( + fast_unstake_events_since_last_call(), + vec![ + Event::Checking { stash: 1, eras: vec![3] }, + Event::Checking { stash: 1, eras: vec![2] }, + Event::Checking { stash: 1, eras: vec![4] }, + Event::Checking { stash: 1, eras: vec![1] }, + Event::Unstaked { stash: 1, maybe_pool_id: Some(1), result: Ok(()) } + ] + ); + + assert_unstaked(&1); + assert!(pallet_nomination_pools::PoolMembers::::contains_key(&1)); + }); + } + + #[test] + fn exposed_nominator_cannot_unstake() { + ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(1); + SlashPerEra::set(7); + CurrentEra::::put(BondingDuration::get()); + + // create an exposed nominator in era 1 + let exposed = 666 as AccountId; + pallet_staking::ErasStakers::::mutate(1, VALIDATORS_PER_ERA, |expo| { + expo.others.push(IndividualExposure { who: exposed, value: 0 as Balance }); + }); + Balances::make_free_balance_be(&exposed, 100); + assert_ok!(Staking::bond( + RuntimeOrigin::signed(exposed), + exposed, + 10, + RewardDestination::Staked + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(exposed), vec![exposed])); + + // register the exposed one. + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(exposed), None)); + + // a few blocks later, we realize they are slashed + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: exposed, + checked: bounded_vec![3], + maybe_pool_id: None + }) + ); + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: exposed, + checked: bounded_vec![3, 2], + maybe_pool_id: None + }) + ); + next_block(true); + assert_eq!(Head::::get(), None); + + assert_eq!( + fast_unstake_events_since_last_call(), + // we slash them by 21, since we checked 3 eras in total (3, 2, 1). + vec![ + Event::Checking { stash: exposed, eras: vec![3] }, + Event::Checking { stash: exposed, eras: vec![2] }, + Event::Slashed { stash: exposed, amount: 3 * 7 } + ] + ); + }); + } + + #[test] + fn exposed_nominator_cannot_unstake_multi_check() { + ExtBuilder::default().build_and_execute(|| { + // same as the previous check, but we check 2 eras per block, and we make the exposed be + // exposed in era 0, so that it is detected halfway in a check era. + ErasToCheckPerBlock::::put(2); + SlashPerEra::set(7); + CurrentEra::::put(BondingDuration::get()); + + // create an exposed nominator in era 1 + let exposed = 666 as AccountId; + pallet_staking::ErasStakers::::mutate(0, VALIDATORS_PER_ERA, |expo| { + expo.others.push(IndividualExposure { who: exposed, value: 0 as Balance }); + }); + Balances::make_free_balance_be(&exposed, 100); + assert_ok!(Staking::bond( + RuntimeOrigin::signed(exposed), + exposed, + 10, + RewardDestination::Staked + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(exposed), vec![exposed])); + + // register the exposed one. + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(exposed), None)); + + // a few blocks later, we realize they are slashed + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: exposed, + checked: bounded_vec![3, 2], + maybe_pool_id: None + }) + ); + next_block(true); + assert_eq!(Head::::get(), None); + + assert_eq!( + fast_unstake_events_since_last_call(), + // we slash them by 28, since we checked 4 eras in total. + vec![ + Event::Checking { stash: exposed, eras: vec![3, 2] }, + Event::Slashed { stash: exposed, amount: 4 * 7 } + ] + ); + }); + } + + #[test] + fn validators_cannot_bail() { + ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(BondingDuration::get() + 1); + CurrentEra::::put(BondingDuration::get()); + + // a validator switches role and register... + assert_ok!(Staking::nominate( + RuntimeOrigin::signed(VALIDATOR_PREFIX), + vec![VALIDATOR_PREFIX] + )); + assert_ok!(FastUnstake::register_fast_unstake( + RuntimeOrigin::signed(VALIDATOR_PREFIX), + None + )); + + // but they indeed are exposed! + assert!(pallet_staking::ErasStakers::::contains_key( + BondingDuration::get() - 1, + VALIDATOR_PREFIX + )); + + // process a block, this validator is exposed and has been slashed. + next_block(true); + assert_eq!(Head::::get(), None); + + assert_eq!( + fast_unstake_events_since_last_call(), + vec![Event::Slashed { stash: 100, amount: 100 }] + ); + }); + } + + #[test] + fn unexposed_validator_can_fast_unstake() { + ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(BondingDuration::get() + 1); + CurrentEra::::put(BondingDuration::get()); + + // create a new validator that 100% not exposed. + Balances::make_free_balance_be(&42, 100); + assert_ok!(Staking::bond(RuntimeOrigin::signed(42), 42, 10, RewardDestination::Staked)); + assert_ok!(Staking::validate(RuntimeOrigin::signed(42), Default::default())); + + // let them register: + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(42), None)); + + // 2 block's enough to unstake them. + next_block(true); + assert_eq!( + Head::::get(), + Some(UnstakeRequest { + stash: 42, + checked: bounded_vec![3, 2, 1, 0], + maybe_pool_id: None + }) + ); + next_block(true); + assert_eq!(Head::::get(), None); + + assert_eq!( + fast_unstake_events_since_last_call(), + vec![ + Event::Checking { stash: 42, eras: vec![3, 2, 1, 0] }, + Event::Unstaked { stash: 42, maybe_pool_id: None, result: Ok(()) } + ] + ); + }); + } +} + +mod signed_extension { + use super::*; + use sp_runtime::traits::SignedExtension; + + const STAKING_CALL: crate::mock::RuntimeCall = + crate::mock::RuntimeCall::Staking(pallet_staking::Call::::chill {}); + + #[test] + fn does_nothing_if_not_queued() { + ExtBuilder::default().build_and_execute(|| { + assert!(PreventStakingOpsIfUnbonding::::new() + .pre_dispatch(&1, &STAKING_CALL, &Default::default(), Default::default()) + .is_ok()); + }) + } + + #[test] + fn prevents_queued() { + ExtBuilder::default().build_and_execute(|| { + // given: stash for 2 is 1. + // when + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), None)); + + // then + // stash can't. + assert!(PreventStakingOpsIfUnbonding::::new() + .pre_dispatch(&1, &STAKING_CALL, &Default::default(), Default::default()) + .is_err()); + + // controller can't. + assert!(PreventStakingOpsIfUnbonding::::new() + .pre_dispatch(&2, &STAKING_CALL, &Default::default(), Default::default()) + .is_err()); + }) + } + + #[test] + fn prevents_head_stash() { + ExtBuilder::default().build_and_execute(|| { + // given: stash for 2 is 1. + // when + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), None)); + + ErasToCheckPerBlock::::put(1); + CurrentEra::::put(BondingDuration::get()); + next_block(true); + + assert_eq!( + Head::::get(), + Some(UnstakeRequest { stash: 1, checked: bounded_vec![3], maybe_pool_id: None }) + ); + + // then + // stash can't + assert!(PreventStakingOpsIfUnbonding::::new() + .pre_dispatch(&2, &STAKING_CALL, &Default::default(), Default::default()) + .is_err()); + + // controller can't + assert!(PreventStakingOpsIfUnbonding::::new() + .pre_dispatch(&1, &STAKING_CALL, &Default::default(), Default::default()) + .is_err()); + }) + } +} diff --git a/frame/fast-unstake/src/types.rs b/frame/fast-unstake/src/types.rs new file mode 100644 index 0000000000000..ae8702e56a842 --- /dev/null +++ b/frame/fast-unstake/src/types.rs @@ -0,0 +1,119 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Types used in the Fast Unstake pallet. + +use crate::*; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + traits::{Currency, Get, IsSubType}, + BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, +}; +use pallet_nomination_pools::PoolId; +use scale_info::TypeInfo; +use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError}; +use sp_staking::EraIndex; +use sp_std::{fmt::Debug, prelude::*}; + +pub type BalanceOf = <::Currency as Currency< + ::AccountId, +>>::Balance; + +/// An unstake request. +#[derive( + Encode, Decode, EqNoBound, PartialEqNoBound, Clone, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen, +)] +pub struct UnstakeRequest> { + /// Their stash account. + pub(crate) stash: AccountId, + /// The list of eras for which they have been checked. + pub(crate) checked: BoundedVec, + /// The pool they wish to join, if any. + pub(crate) maybe_pool_id: Option, +} + +#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo, RuntimeDebugNoBound)] +#[scale_info(skip_type_params(T))] +pub struct PreventStakingOpsIfUnbonding(sp_std::marker::PhantomData); + +#[cfg(test)] +impl PreventStakingOpsIfUnbonding { + pub fn new() -> Self { + Self(Default::default()) + } +} + +impl sp_runtime::traits::SignedExtension + for PreventStakingOpsIfUnbonding +where + ::RuntimeCall: IsSubType>, +{ + type AccountId = T::AccountId; + type Call = ::RuntimeCall; + type AdditionalSigned = (); + type Pre = (); + const IDENTIFIER: &'static str = "PreventStakingOpsIfUnbonding"; + + fn additional_signed(&self) -> Result { + Ok(()) + } + + fn pre_dispatch( + self, + // NOTE: we want to prevent this stash-controller pair from doing anything in the + // staking system as long as they are registered here. + stash_or_controller: &Self::AccountId, + call: &Self::Call, + _info: &sp_runtime::traits::DispatchInfoOf, + _len: usize, + ) -> Result { + // we don't check this in the tx-pool as it requires a storage read. + if >>::is_sub_type(call).is_some() { + let check_stash = |stash: &T::AccountId| { + if Queue::::contains_key(&stash) || + Head::::get().map_or(false, |u| &u.stash == stash) + { + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + } else { + Ok(()) + } + }; + match ( + // mapped from controller. + pallet_staking::Ledger::::get(&stash_or_controller), + // mapped from stash. + pallet_staking::Bonded::::get(&stash_or_controller), + ) { + (Some(ledger), None) => { + // it is a controller. + check_stash(&ledger.stash) + }, + (_, Some(_)) => { + // it's a stash. + let stash = stash_or_controller; + check_stash(stash) + }, + (None, None) => { + // They are not a staker -- let them execute. + Ok(()) + }, + } + } else { + Ok(()) + } + } +} diff --git a/frame/fast-unstake/src/weights.rs b/frame/fast-unstake/src/weights.rs new file mode 100644 index 0000000000000..04857d0dcc865 --- /dev/null +++ b/frame/fast-unstake/src/weights.rs @@ -0,0 +1,210 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Autogenerated weights for pallet_fast_unstake +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-09-07, STEPS: `10`, REPEAT: 1, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! HOSTNAME: `Kians-MacBook-Pro-2.local`, CPU: `` +//! EXECUTION: Some(Native), WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024 + +// Executed Command: +// target/release/substrate +// benchmark +// pallet +// --steps=10 +// --repeat=1 +// --pallet=pallet_fast_unstake +// --extrinsic=* +// --execution=native +// --output +// weight.rs +// --template +// ./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_fast_unstake. +pub trait WeightInfo { + fn on_idle_unstake() -> Weight; + fn on_idle_check(x: u32, ) -> Weight; + fn register_fast_unstake() -> Weight; + fn deregister() -> Weight; + fn control() -> Weight; +} + +/// Weights for pallet_fast_unstake using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: FastUnstake ErasToCheckPerBlock (r:1 w:0) + // Storage: Staking ValidatorCount (r:1 w:0) + // Storage: ElectionProviderMultiPhase CurrentPhase (r:1 w:0) + // Storage: FastUnstake Head (r:1 w:1) + // Storage: Staking CurrentEra (r:1 w:0) + // Storage: Staking SlashingSpans (r:1 w:0) + // Storage: Staking Bonded (r:2 w:1) + // Storage: Staking Ledger (r:2 w:2) + // Storage: Staking Validators (r:1 w:0) + // Storage: Staking Nominators (r:1 w:0) + // Storage: System Account (r:3 w:2) + // Storage: Balances Locks (r:2 w:2) + // Storage: NominationPools MinJoinBond (r:1 w:0) + // Storage: NominationPools PoolMembers (r:1 w:1) + // Storage: NominationPools BondedPools (r:1 w:1) + // Storage: NominationPools RewardPools (r:1 w:1) + // Storage: NominationPools MaxPoolMembersPerPool (r:1 w:0) + // Storage: NominationPools MaxPoolMembers (r:1 w:0) + // Storage: NominationPools CounterForPoolMembers (r:1 w:1) + // Storage: BagsList ListNodes (r:1 w:0) + // Storage: Staking Payee (r:0 w:1) + fn on_idle_unstake() -> Weight { + Weight::from_ref_time(102_000_000 as u64) + .saturating_add(T::DbWeight::get().reads(25 as u64)) + .saturating_add(T::DbWeight::get().writes(13 as u64)) + } + // Storage: FastUnstake ErasToCheckPerBlock (r:1 w:0) + // Storage: Staking ValidatorCount (r:1 w:0) + // Storage: ElectionProviderMultiPhase CurrentPhase (r:1 w:0) + // Storage: FastUnstake Head (r:1 w:1) + // Storage: FastUnstake Queue (r:2 w:1) + // Storage: FastUnstake CounterForQueue (r:1 w:1) + // Storage: Staking CurrentEra (r:1 w:0) + // Storage: Staking ErasStakers (r:1344 w:0) + /// The range of component `x` is `[672, 86016]`. + fn on_idle_check(x: u32, ) -> Weight { + Weight::from_ref_time(0 as u64) + // Standard Error: 244_000 + .saturating_add(Weight::from_ref_time(13_913_000 as u64).saturating_mul(x as u64)) + .saturating_add(T::DbWeight::get().reads(585 as u64)) + .saturating_add(T::DbWeight::get().reads((1 as u64).saturating_mul(x as u64))) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + } + // Storage: Staking Ledger (r:1 w:1) + // Storage: Staking Nominators (r:1 w:1) + // Storage: FastUnstake Queue (r:1 w:1) + // Storage: FastUnstake Head (r:1 w:0) + // Storage: Staking Validators (r:1 w:0) + // Storage: Staking CounterForNominators (r:1 w:1) + // Storage: BagsList ListNodes (r:1 w:1) + // Storage: BagsList ListBags (r:1 w:1) + // Storage: BagsList CounterForListNodes (r:1 w:1) + // Storage: Staking CurrentEra (r:1 w:0) + // Storage: Balances Locks (r:1 w:1) + // Storage: FastUnstake CounterForQueue (r:1 w:1) + fn register_fast_unstake() -> Weight { + Weight::from_ref_time(57_000_000 as u64) + .saturating_add(T::DbWeight::get().reads(12 as u64)) + .saturating_add(T::DbWeight::get().writes(9 as u64)) + } + // Storage: Staking Ledger (r:1 w:0) + // Storage: FastUnstake Queue (r:1 w:1) + // Storage: FastUnstake Head (r:1 w:0) + // Storage: FastUnstake CounterForQueue (r:1 w:1) + fn deregister() -> Weight { + Weight::from_ref_time(15_000_000 as u64) + .saturating_add(T::DbWeight::get().reads(4 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: FastUnstake ErasToCheckPerBlock (r:0 w:1) + fn control() -> Weight { + Weight::from_ref_time(3_000_000 as u64) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: FastUnstake ErasToCheckPerBlock (r:1 w:0) + // Storage: Staking ValidatorCount (r:1 w:0) + // Storage: ElectionProviderMultiPhase CurrentPhase (r:1 w:0) + // Storage: FastUnstake Head (r:1 w:1) + // Storage: Staking CurrentEra (r:1 w:0) + // Storage: Staking SlashingSpans (r:1 w:0) + // Storage: Staking Bonded (r:2 w:1) + // Storage: Staking Ledger (r:2 w:2) + // Storage: Staking Validators (r:1 w:0) + // Storage: Staking Nominators (r:1 w:0) + // Storage: System Account (r:3 w:2) + // Storage: Balances Locks (r:2 w:2) + // Storage: NominationPools MinJoinBond (r:1 w:0) + // Storage: NominationPools PoolMembers (r:1 w:1) + // Storage: NominationPools BondedPools (r:1 w:1) + // Storage: NominationPools RewardPools (r:1 w:1) + // Storage: NominationPools MaxPoolMembersPerPool (r:1 w:0) + // Storage: NominationPools MaxPoolMembers (r:1 w:0) + // Storage: NominationPools CounterForPoolMembers (r:1 w:1) + // Storage: BagsList ListNodes (r:1 w:0) + // Storage: Staking Payee (r:0 w:1) + fn on_idle_unstake() -> Weight { + Weight::from_ref_time(102_000_000 as u64) + .saturating_add(RocksDbWeight::get().reads(25 as u64)) + .saturating_add(RocksDbWeight::get().writes(13 as u64)) + } + // Storage: FastUnstake ErasToCheckPerBlock (r:1 w:0) + // Storage: Staking ValidatorCount (r:1 w:0) + // Storage: ElectionProviderMultiPhase CurrentPhase (r:1 w:0) + // Storage: FastUnstake Head (r:1 w:1) + // Storage: FastUnstake Queue (r:2 w:1) + // Storage: FastUnstake CounterForQueue (r:1 w:1) + // Storage: Staking CurrentEra (r:1 w:0) + // Storage: Staking ErasStakers (r:1344 w:0) + /// The range of component `x` is `[672, 86016]`. + fn on_idle_check(x: u32, ) -> Weight { + Weight::from_ref_time(0 as u64) + // Standard Error: 244_000 + .saturating_add(Weight::from_ref_time(13_913_000 as u64).saturating_mul(x as u64)) + .saturating_add(RocksDbWeight::get().reads(585 as u64)) + .saturating_add(RocksDbWeight::get().reads((1 as u64).saturating_mul(x as u64))) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } + // Storage: Staking Ledger (r:1 w:1) + // Storage: Staking Nominators (r:1 w:1) + // Storage: FastUnstake Queue (r:1 w:1) + // Storage: FastUnstake Head (r:1 w:0) + // Storage: Staking Validators (r:1 w:0) + // Storage: Staking CounterForNominators (r:1 w:1) + // Storage: BagsList ListNodes (r:1 w:1) + // Storage: BagsList ListBags (r:1 w:1) + // Storage: BagsList CounterForListNodes (r:1 w:1) + // Storage: Staking CurrentEra (r:1 w:0) + // Storage: Balances Locks (r:1 w:1) + // Storage: FastUnstake CounterForQueue (r:1 w:1) + fn register_fast_unstake() -> Weight { + Weight::from_ref_time(57_000_000 as u64) + .saturating_add(RocksDbWeight::get().reads(12 as u64)) + .saturating_add(RocksDbWeight::get().writes(9 as u64)) + } + // Storage: Staking Ledger (r:1 w:0) + // Storage: FastUnstake Queue (r:1 w:1) + // Storage: FastUnstake Head (r:1 w:0) + // Storage: FastUnstake CounterForQueue (r:1 w:1) + fn deregister() -> Weight { + Weight::from_ref_time(15_000_000 as u64) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: FastUnstake ErasToCheckPerBlock (r:0 w:1) + fn control() -> Weight { + Weight::from_ref_time(3_000_000 as u64) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } +} diff --git a/frame/nomination-pools/benchmarking/src/lib.rs b/frame/nomination-pools/benchmarking/src/lib.rs index 040a9fa8e828e..c31bcb1546ecd 100644 --- a/frame/nomination-pools/benchmarking/src/lib.rs +++ b/frame/nomination-pools/benchmarking/src/lib.rs @@ -52,12 +52,6 @@ pub trait Config: pub struct Pallet(Pools); -fn min_create_bond() -> BalanceOf { - MinCreateBond::::get() - .max(T::StakingInterface::minimum_bond()) - .max(CurrencyOf::::minimum_balance()) -} - fn create_funded_user_with_balance( string: &'static str, n: u32, @@ -220,7 +214,7 @@ impl ListScenario { frame_benchmarking::benchmarks! { join { - let origin_weight = min_create_bond::() * 2u32.into(); + let origin_weight = Pools::::depositor_min_bond() * 2u32.into(); // setup the worst case list scenario. let scenario = ListScenario::::new(origin_weight, true)?; @@ -246,7 +240,7 @@ frame_benchmarking::benchmarks! { } bond_extra_transfer { - let origin_weight = min_create_bond::() * 2u32.into(); + let origin_weight = Pools::::depositor_min_bond() * 2u32.into(); let scenario = ListScenario::::new(origin_weight, true)?; let extra = scenario.dest_weight - origin_weight; @@ -261,7 +255,7 @@ frame_benchmarking::benchmarks! { } bond_extra_reward { - let origin_weight = min_create_bond::() * 2u32.into(); + let origin_weight = Pools::::depositor_min_bond() * 2u32.into(); let scenario = ListScenario::::new(origin_weight, true)?; let extra = (scenario.dest_weight - origin_weight).max(CurrencyOf::::minimum_balance()); @@ -279,7 +273,7 @@ frame_benchmarking::benchmarks! { } claim_payout { - let origin_weight = min_create_bond::() * 2u32.into(); + let origin_weight = Pools::::depositor_min_bond() * 2u32.into(); let ed = CurrencyOf::::minimum_balance(); let (depositor, pool_account) = create_pool_account::(0, origin_weight); let reward_account = Pools::::create_reward_account(1); @@ -309,7 +303,7 @@ frame_benchmarking::benchmarks! { unbond { // The weight the nominator will start at. The value used here is expected to be // significantly higher than the first position in a list (e.g. the first bag threshold). - let origin_weight = min_create_bond::() * 200u32.into(); + let origin_weight = Pools::::depositor_min_bond() * 200u32.into(); let scenario = ListScenario::::new(origin_weight, false)?; let amount = origin_weight - scenario.dest_weight; @@ -340,7 +334,7 @@ frame_benchmarking::benchmarks! { pool_withdraw_unbonded { let s in 0 .. MAX_SPANS; - let min_create_bond = min_create_bond::(); + let min_create_bond = Pools::::depositor_min_bond(); let (depositor, pool_account) = create_pool_account::(0, min_create_bond); // Add a new member @@ -382,7 +376,7 @@ frame_benchmarking::benchmarks! { withdraw_unbonded_update { let s in 0 .. MAX_SPANS; - let min_create_bond = min_create_bond::(); + let min_create_bond = Pools::::depositor_min_bond(); let (depositor, pool_account) = create_pool_account::(0, min_create_bond); // Add a new member @@ -428,7 +422,7 @@ frame_benchmarking::benchmarks! { withdraw_unbonded_kill { let s in 0 .. MAX_SPANS; - let min_create_bond = min_create_bond::(); + let min_create_bond = Pools::::depositor_min_bond(); let (depositor, pool_account) = create_pool_account::(0, min_create_bond); let depositor_lookup = T::Lookup::unlookup(depositor.clone()); @@ -493,14 +487,14 @@ frame_benchmarking::benchmarks! { } create { - let min_create_bond = min_create_bond::(); + let min_create_bond = Pools::::depositor_min_bond(); let depositor: T::AccountId = account("depositor", USER_SEED, 0); let depositor_lookup = T::Lookup::unlookup(depositor.clone()); // Give the depositor some balance to bond CurrencyOf::::make_free_balance_be(&depositor, min_create_bond * 2u32.into()); - // Make sure no pools exist as a pre-condition for our verify checks + // Make sure no Pools exist at a pre-condition for our verify checks assert_eq!(RewardPools::::count(), 0); assert_eq!(BondedPools::::count(), 0); @@ -540,7 +534,7 @@ frame_benchmarking::benchmarks! { let n in 1 .. T::MaxNominations::get(); // Create a pool - let min_create_bond = min_create_bond::() * 2u32.into(); + let min_create_bond = Pools::::depositor_min_bond() * 2u32.into(); let (depositor, pool_account) = create_pool_account::(0, min_create_bond); // Create some accounts to nominate. For the sake of benchmarking they don't need to be @@ -577,7 +571,7 @@ frame_benchmarking::benchmarks! { set_state { // Create a pool - let min_create_bond = min_create_bond::(); + let min_create_bond = Pools::::depositor_min_bond(); let (depositor, pool_account) = create_pool_account::(0, min_create_bond); BondedPools::::mutate(&1, |maybe_pool| { // Force the pool into an invalid state @@ -595,7 +589,7 @@ frame_benchmarking::benchmarks! { let n in 1 .. ::MaxMetadataLen::get(); // Create a pool - let (depositor, pool_account) = create_pool_account::(0, min_create_bond::() * 2u32.into()); + let (depositor, pool_account) = create_pool_account::(0, Pools::::depositor_min_bond() * 2u32.into()); // Create metadata of the max possible size let metadata: Vec = (0..n).map(|_| 42).collect(); @@ -624,7 +618,7 @@ frame_benchmarking::benchmarks! { update_roles { let first_id = pallet_nomination_pools::LastPoolId::::get() + 1; - let (root, _) = create_pool_account::(0, min_create_bond::() * 2u32.into()); + let (root, _) = create_pool_account::(0, Pools::::depositor_min_bond() * 2u32.into()); let random: T::AccountId = account("but is anything really random in computers..?", 0, USER_SEED); }:_( RuntimeOrigin::Signed(root.clone()), @@ -646,7 +640,7 @@ frame_benchmarking::benchmarks! { chill { // Create a pool - let (depositor, pool_account) = create_pool_account::(0, min_create_bond::() * 2u32.into()); + let (depositor, pool_account) = create_pool_account::(0, Pools::::depositor_min_bond() * 2u32.into()); // Nominate with the pool. let validators: Vec<_> = (0..T::MaxNominations::get()) diff --git a/frame/nomination-pools/src/lib.rs b/frame/nomination-pools/src/lib.rs index dfe4bf1931068..28d10ce573401 100644 --- a/frame/nomination-pools/src/lib.rs +++ b/frame/nomination-pools/src/lib.rs @@ -2184,10 +2184,11 @@ impl Pallet { /// /// It is essentially `max { MinNominatorBond, MinCreateBond, MinJoinBond }`, where the former /// is coming from the staking pallet and the latter two are configured in this pallet. - fn depositor_min_bond() -> BalanceOf { + pub fn depositor_min_bond() -> BalanceOf { T::StakingInterface::minimum_bond() .max(MinCreateBond::::get()) .max(MinJoinBond::::get()) + .max(T::Currency::minimum_balance()) } /// Remove everything related to the given bonded pool. /// diff --git a/frame/nomination-pools/src/tests.rs b/frame/nomination-pools/src/tests.rs index 7b8d14164d63f..5074a7ffa695a 100644 --- a/frame/nomination-pools/src/tests.rs +++ b/frame/nomination-pools/src/tests.rs @@ -2771,7 +2771,7 @@ mod unbond { #[test] fn partial_unbond_era_tracking() { - ExtBuilder::default().build_and_execute(|| { + ExtBuilder::default().ed(1).build_and_execute(|| { // to make the depositor capable of withdrawing. StakingMinBond::set(1); MinCreateBond::::set(1); diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs index 56fcb1965bc13..df568d6b596ba 100644 --- a/frame/staking/src/lib.rs +++ b/frame/staking/src/lib.rs @@ -563,7 +563,7 @@ impl StakingLedger { /// /// This calls `Config::OnStakerSlash::on_slash` with information as to how the slash was /// applied. - fn slash( + pub fn slash( &mut self, slash_amount: BalanceOf, minimum_balance: BalanceOf, diff --git a/frame/staking/src/pallet/impls.rs b/frame/staking/src/pallet/impls.rs index abb8649169fdd..399f50aaed865 100644 --- a/frame/staking/src/pallet/impls.rs +++ b/frame/staking/src/pallet/impls.rs @@ -653,10 +653,10 @@ impl Pallet { #[cfg(feature = "runtime-benchmarks")] pub fn add_era_stakers( current_era: EraIndex, - controller: T::AccountId, + stash: T::AccountId, exposure: Exposure>, ) { - >::insert(¤t_era, &controller, &exposure); + >::insert(¤t_era, &stash, &exposure); } #[cfg(feature = "runtime-benchmarks")] diff --git a/frame/staking/src/pallet/mod.rs b/frame/staking/src/pallet/mod.rs index d113c0bb2243c..4db3870c62d8b 100644 --- a/frame/staking/src/pallet/mod.rs +++ b/frame/staking/src/pallet/mod.rs @@ -504,6 +504,7 @@ pub mod pallet { /// Slashing spans for stash accounts. #[pallet::storage] + #[pallet::getter(fn slashing_spans)] #[pallet::unbounded] pub(crate) type SlashingSpans = StorageMap<_, Twox64Concat, T::AccountId, slashing::SlashingSpans>; @@ -656,8 +657,8 @@ pub mod pallet { EraPaid(EraIndex, BalanceOf, BalanceOf), /// The nominator has been rewarded by this amount. \[stash, amount\] Rewarded(T::AccountId, BalanceOf), - /// One validator (and its nominators) has been slashed by the given amount. - /// \[validator, amount\] + /// One staker (and potentially its nominators) has been slashed by the given amount. + /// \[staker, amount\] Slashed(T::AccountId, BalanceOf), /// An old slashing report from a prior era was discarded because it could /// not be processed. \[session_index\]