diff --git a/Cargo.lock b/Cargo.lock index 23f8d035fe533..1fa175f2400e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3665,6 +3665,7 @@ dependencies = [ "pallet-session-benchmarking", "pallet-society", "pallet-stake-tracker", + "pallet-stake-tracker-initializer", "pallet-staking", "pallet-staking-reward-curve", "pallet-staking-runtime-api", @@ -6756,6 +6757,23 @@ dependencies = [ "substrate-test-utils", ] +[[package]] +name = "pallet-stake-tracker-initializer" +version = "1.0.0-dev" +dependencies = [ + "frame-election-provider-support", + "frame-support", + "frame-system", + "log", + "pallet-stake-tracker", + "pallet-staking", + "parity-scale-codec", + "scale-info", + "sp-runtime", + "sp-staking", + "sp-std", +] + [[package]] name = "pallet-staking" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index a4e2de3fafdf9..18bb0f16a893d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,6 +143,7 @@ members = [ "frame/society", "frame/staking", "frame/stake-tracker", + "frame/stake-tracker/initializer", "frame/staking/reward-curve", "frame/staking/reward-fn", "frame/staking/runtime-api", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 3a839d2df204d..a5899e6e8dbb0 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -102,6 +102,7 @@ pallet-session = { version = "4.0.0-dev", features = [ "historical" ], path = ". pallet-session-benchmarking = { version = "4.0.0-dev", path = "../../../frame/session/benchmarking", default-features = false, optional = true } pallet-staking = { version = "4.0.0-dev", default-features = false, path = "../../../frame/staking" } pallet-stake-tracker = { version = "4.0.0-dev", default-features = false, path = "../../../frame/stake-tracker" } +pallet-stake-tracker-initializer = { version = "1.0.0-dev", default-features = false, path = "../../../frame/stake-tracker/initializer" } pallet-staking-reward-curve = { version = "4.0.0-dev", default-features = false, path = "../../../frame/staking/reward-curve" } pallet-staking-runtime-api = { version = "4.0.0-dev", default-features = false, path = "../../../frame/staking/runtime-api" } pallet-state-trie-migration = { version = "4.0.0-dev", default-features = false, path = "../../../frame/state-trie-migration" } @@ -186,6 +187,7 @@ std = [ "sp-staking/std", "pallet-staking/std", "pallet-stake-tracker/std", + "pallet-stake-tracker-initializer/std", "pallet-staking-runtime-api/std", "pallet-state-trie-migration/std", "pallet-salary/std", @@ -329,6 +331,7 @@ try-runtime = [ "pallet-session/try-runtime", "pallet-staking/try-runtime", "pallet-stake-tracker/try-runtime", + "pallet-stake-tracker-initializer/try-runtime", "pallet-state-trie-migration/try-runtime", "pallet-scheduler/try-runtime", "pallet-society/try-runtime", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 5288c8d017a4b..6b11490e488cf 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -601,8 +601,11 @@ impl pallet_stake_tracker::Config for Runtime { type Currency = Balances; type Staking = Staking; type VoterList = VoterList; + type TargetList = TargetList; } +impl pallet_stake_tracker_initializer::Config for Runtime {} + impl pallet_fast_unstake::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ControlOrigin = frame_system::EnsureRoot; @@ -775,12 +778,25 @@ parameter_types! { type VoterBagsListInstance = pallet_bags_list::Instance1; impl pallet_bags_list::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_bags_list::weights::SubstrateWeight; /// The voter bags-list is loosely kept up to date, and the real source of truth for the score /// of each node is the staking pallet. type ScoreProvider = Staking; type BagThresholds = BagThresholds; type Score = VoteWeight; +} + +parameter_types! { + pub const BagThresholdsBalances: &'static [Balance] = &voter_bags::THRESHOLDS_BALANCES; +} + +type TargetBagsListInstance = pallet_bags_list::Instance2; +impl pallet_bags_list::Config for Runtime { + type RuntimeEvent = RuntimeEvent; type WeightInfo = pallet_bags_list::weights::SubstrateWeight; + type ScoreProvider = StakeTracker; + type BagThresholds = BagThresholdsBalances; + type Score = Balance; } parameter_types! { @@ -1771,6 +1787,7 @@ construct_runtime!( ElectionProviderMultiPhase: pallet_election_provider_multi_phase, Staking: pallet_staking, StakeTracker: pallet_stake_tracker, + StakeTrackerInitializer: pallet_stake_tracker_initializer, Session: pallet_session, Democracy: pallet_democracy, Council: pallet_collective::, @@ -1807,6 +1824,7 @@ construct_runtime!( CoreFellowship: pallet_core_fellowship, TransactionStorage: pallet_transaction_storage, VoterList: pallet_bags_list::, + TargetList: pallet_bags_list::, StateTrieMigration: pallet_state_trie_migration, ChildBounties: pallet_child_bounties, Referenda: pallet_referenda, diff --git a/bin/node/runtime/src/voter_bags.rs b/bin/node/runtime/src/voter_bags.rs index bf18097ddf53b..f87e735886825 100644 --- a/bin/node/runtime/src/voter_bags.rs +++ b/bin/node/runtime/src/voter_bags.rs @@ -17,7 +17,7 @@ //! Autogenerated bag thresholds. //! -//! Generated on 2022-08-15T19:26:59.939787+00:00 +//! Generated on 2023-01-20T15:05:52.717430+00:00 //! Arguments //! Total issuance: 100000000000000 //! Minimum balance: 100000000000000 @@ -236,3 +236,207 @@ pub const THRESHOLDS: [u64; 200] = [ 17_356_326_621_502_140_416, 18_446_744_073_709_551_615, ]; + +/// Upper thresholds delimiting the bag list. +pub const THRESHOLDS_BALANCES: [u128; 200] = [ + 100_000_000_000_000, + 106_282_535_907_434, + 112_959_774_389_150, + 120_056_512_776_105, + 127_599_106_300_477, + 135_615_565_971_369, + 144_135_662_599_590, + 153_191_037_357_827, + 162_815_319_286_803, + 173_044_250_183_800, + 183_915_817_337_347, + 195_470_394_601_017, + 207_750_892_330_229, + 220_802_916_738_890, + 234_674_939_267_673, + 249_418_476_592_914, + 265_088_281_944_639, + 281_742_548_444_211, + 299_443_125_216_738, + 318_255_747_080_822, + 338_250_278_668_647, + 359_500_973_883_001, + 382_086_751_654_776, + 406_091_489_025_036, + 431_604_332_640_068, + 458_720_029_816_222, + 487_539_280_404_019, + 518_169_110_758_247, + 550_723_271_202_866, + 585_322_658_466_782, + 622_095_764_659_305, + 661_179_154_452_653, + 702_717_972_243_610, + 746_866_481_177_808, + 793_788_636_038_393, + 843_658_692_126_636, + 896_661_852_395_681, + 952_994_955_240_703, + 1_012_867_205_499_736, + 1_076_500_951_379_881, + 1_144_132_510_194_192, + 1_216_013_045_975_769, + 1_292_409_502_228_280, + 1_373_605_593_276_862, + 1_459_902_857_901_004, + 1_551_621_779_162_291, + 1_649_102_974_585_730, + 1_752_708_461_114_642, + 1_862_822_999_536_805, + 1_979_855_523_374_646, + 2_104_240_657_545_975, + 2_236_440_332_435_128, + 2_376_945_499_368_703, + 2_526_277_953_866_680, + 2_684_992_273_439_945, + 2_853_677_877_130_641, + 3_032_961_214_443_876, + 3_223_508_091_799_862, + 3_426_026_145_146_232, + 3_641_267_467_913_124, + 3_870_031_404_070_482, + 4_113_167_516_660_186, + 4_371_578_742_827_277, + 4_646_224_747_067_156, + 4_938_125_485_141_739, + 5_248_364_991_899_922, + 5_578_095_407_069_235, + 5_928_541_253_969_291, + 6_301_003_987_036_955, + 6_696_866_825_051_405, + 7_117_599_888_008_300, + 7_564_765_656_719_910, + 8_040_024_775_416_580, + 8_545_142_218_898_723, + 9_081_993_847_142_344, + 9_652_573_371_700_016, + 10_258_999_759_768_490, + 10_903_525_103_419_522, + 11_588_542_983_217_942, + 12_316_597_357_287_042, + 13_090_392_008_832_678, + 13_912_800_587_211_472, + 14_786_877_279_832_732, + 15_715_868_154_526_436, + 16_703_223_214_499_558, + 17_752_609_210_649_358, + 18_867_923_258_814_856, + 20_053_307_312_537_008, + 21_313_163_545_075_252, + 22_652_170_697_804_756, + 24_075_301_455_707_600, + 25_587_840_914_485_432, + 27_195_406_207_875_088, + 28_903_967_368_057_400, + 30_719_869_496_628_636, + 32_649_856_328_471_220, + 34_701_095_276_033_064, + 36_881_204_047_022_752, + 39_198_278_934_370_992, + 41_660_924_883_519_016, + 44_278_287_448_695_240, + 47_060_086_756_856_400, + 50_016_653_605_425_536, + 53_158_967_827_883_320, + 56_498_699_069_691_424, + 60_048_250_125_977_912, + 63_820_803_001_928_304, + 67_830_367_866_937_216, + 72_091_835_084_322_176, + 76_621_030_509_822_880, + 81_434_774_264_248_528, + 86_550_943_198_537_824, + 91_988_537_283_208_848, + 97_767_750_168_749_840, + 103_910_044_178_992_000, + 110_438_230_015_967_792, + 117_376_551_472_255_616, + 124_750_775_465_407_920, + 132_588_287_728_824_640, + 140_918_194_514_440_064, + 149_771_430_684_917_568, + 159_180_874_596_775_264, + 169_181_470_201_085_280, + 179_810_356_815_193_344, + 191_107_007_047_393_216, + 203_113_373_386_768_288, + 215_874_044_002_592_672, + 229_436_408_331_885_600, + 243_850_833_070_063_392, + 259_170_849_218_267_264, + 275_453_350_882_006_752, + 292_758_806_559_399_232, + 311_151_483_703_668_992, + 330_699_687_393_865_920, + 351_476_014_000_157_824, + 373_557_620_785_735_808, + 397_026_512_446_556_096, + 421_969_845_653_044_224, + 448_480_252_724_740_928, + 476_656_185_639_923_904, + 506_602_281_657_757_760, + 538_429_751_910_786_752, + 572_256_794_410_890_176, + 608_209_033_002_485_632, + 646_419_983_893_124_352, + 687_031_551_494_039_552, + 730_194_555_412_054_016, + 776_069_290_549_944_960, + 824_826_122_395_314_176, + 876_646_119_708_695_936, + 931_721_726_960_522_368, + 990_257_479_014_182_144, + 1_052_470_760_709_299_712, + 1_118_592_614_166_106_112, + 1_188_868_596_808_997_376, + 1_263_559_693_295_730_432, + 1_342_943_284_738_898_688, + 1_427_314_178_819_094_784, + 1_516_985_704_615_302_400, + 1_612_290_876_218_400_768, + 1_713_583_629_449_105_408, + 1_821_240_136_273_157_632, + 1_935_660_201_795_120_128, + 2_057_268_749_018_809_600, + 2_186_517_396_888_336_384, + 2_323_886_137_470_138_880, + 2_469_885_118_504_583_168, + 2_625_056_537_947_004_416, + 2_789_976_657_533_970_944, + 2_965_257_942_852_572_160, + 3_151_551_337_860_326_400, + 3_349_548_682_302_620_672, + 3_559_985_281_005_267_968, + 3_783_642_634_583_792_128, + 4_021_351_341_710_503_936, + 4_273_994_183_717_548_544, + 4_542_509_402_991_247_872, + 4_827_894_187_332_742_144, + 5_131_208_373_224_844_288, + 5_453_578_381_757_959_168, + 5_796_201_401_831_965_696, + 6_160_349_836_169_256_960, + 6_547_376_026_650_146_816, + 6_958_717_276_519_173_120, + 7_395_901_188_113_309_696, + 7_860_551_335_934_872_576, + 8_354_393_296_137_270_272, + 8_879_261_054_815_360_000, + 9_437_103_818_898_946_048, + 10_029_993_254_943_105_024, + 10_660_131_182_698_121_216, + 11_329_857_752_030_707_712, + 12_041_660_133_563_240_448, + 12_798_181_755_305_525_248, + 13_602_232_119_581_272_064, + 14_456_797_236_706_498_560, + 15_365_050_714_167_523_328, + 16_330_365_542_480_556_032, + 17_356_326_621_502_140_416, + 18_446_744_073_709_551_615, +]; diff --git a/frame/nomination-pools/benchmarking/src/mock.rs b/frame/nomination-pools/benchmarking/src/mock.rs index ebb3eea151f6b..bc8768fc363a7 100644 --- a/frame/nomination-pools/benchmarking/src/mock.rs +++ b/frame/nomination-pools/benchmarking/src/mock.rs @@ -130,6 +130,7 @@ impl pallet_stake_tracker::Config for Runtime { type Currency = Balances; type Staking = Staking; type VoterList = VoterList; + type TargetList = pallet_staking::UseValidatorsMap; } parameter_types! { diff --git a/frame/stake-tracker/initializer/Cargo.toml b/frame/stake-tracker/initializer/Cargo.toml new file mode 100644 index 0000000000000..be5745206622b --- /dev/null +++ b/frame/stake-tracker/initializer/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "pallet-stake-tracker-initializer" +version = "1.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Unlicense" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME stake tracker initializer submodule" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +pallet-staking = { path = "../../staking", default-features = false } +pallet-stake-tracker = { path = "..", default-features = false } +sp-std = { version = "5.0.0", default-features = false, path = "../../../primitives/std" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../../primitives/runtime" } +sp-staking = { default-features = false, path = "../../../primitives/staking" } +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" } +frame-election-provider-support = { version = "4.0.0-dev", default-features = false, path = "../../election-provider-support" } +log = { version = "0.4.17", default-features = false } +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ + "derive", +] } + + +[features] +default = ["std"] + +std = [ + "pallet-staking/std", + "pallet-stake-tracker/std", + "sp-std/std", + "sp-runtime/std", + "sp-staking/std", + "scale-info/std", + "frame-support/std", + "frame-system/std", + "frame-election-provider-support/std", + "log/std", + "codec/std" +] + +runtime-benchmarks = [ + "pallet-stake-tracker/runtime-benchmarks", + "frame-election-provider-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-staking/runtime-benchmarks", + "pallet-staking/runtime-benchmarks" +] + +try-runtime = [ + "frame-support/try-runtime", + "pallet-staking/try-runtime", + "pallet-stake-tracker/try-runtime", + "frame-election-provider-support/try-runtime", + "frame-system/try-runtime" +] diff --git a/frame/stake-tracker/initializer/README.md b/frame/stake-tracker/initializer/README.md new file mode 100644 index 0000000000000..28a375aee2ebd --- /dev/null +++ b/frame/stake-tracker/initializer/README.md @@ -0,0 +1,8 @@ +# Initializer submodule + +The Initializer submodule implements storage initialization for pallet-stake-tracker. + +## Overview + +The Initializer submodule depends on pallet-stake-tracker and pallet-staking as it needs access to +storage of both of these pallets, while avoiding direct dependency from one to another. diff --git a/frame/stake-tracker/initializer/src/lib.rs b/frame/stake-tracker/initializer/src/lib.rs new file mode 100644 index 0000000000000..568a35f251fc2 --- /dev/null +++ b/frame/stake-tracker/initializer/src/lib.rs @@ -0,0 +1,294 @@ +// This file is part of Substrate. + +// Copyright (C) 2023 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::Decode; +use frame_election_provider_support::SortedListProvider; +use frame_support::{ + pallet_prelude::{Get, Weight}, + sp_io, storage, +}; +pub use pallet::*; + +use pallet_stake_tracker::{ApprovalStake, BalanceOf}; +use pallet_staking::{Nominations, TemporaryMigrationLock, Validators}; +use sp_runtime::Saturating; +use sp_staking::StakingInterface; +use sp_std::collections::btree_map::BTreeMap; + +#[cfg(feature = "try-runtime")] +use sp_std::vec::Vec; + +pub(crate) const LOG_TARGET: &str = "runtime::stake-tracker-initializer"; + +#[macro_export] +macro_rules! log { + ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { + log::$level!( + target: crate::LOG_TARGET, + concat!("πŸ’ΈπŸ§", $patter), $(, $values)* + ) + }; +} + +#[frame_support::pallet] +pub mod pallet { + use crate::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::BlockNumberFor; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: + frame_system::Config + pallet_staking::Config + pallet_stake_tracker::Config + { + } + + #[derive( + Encode, Decode, CloneNoBound, PartialEqNoBound, EqNoBound, TypeInfo, MaxEncodedLen, Default, + )] + pub struct MigrationState { + pub last_key: BoundedVec>, + pub prefix: BoundedVec>, + pub done: bool, + } + + #[pallet::storage] + pub(crate) type MigrationV1StateNominators = + StorageValue<_, MigrationState, OptionQuery>; + + #[pallet::storage] + pub(crate) type MigrationV1StateValidators = + StorageValue<_, MigrationState, OptionQuery>; + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_runtime_upgrade() -> Weight { + // We have to set this manually, because we need this migration to happen in order for + // the pallet to get all the data from staking-pallet. + let current = StorageVersion::new(1); + let onchain = pallet_stake_tracker::Pallet::::on_chain_storage_version(); + + if current == 1 && onchain == 0 { + // This lets on_initialize code know to proceed with the migration. And pauses + // Staking extrinsics. + TemporaryMigrationLock::::put(true); + T::DbWeight::get().writes(1) + } else { + log!(warn, "This migration is being executed on a wrong storage version, probably needs to be removed."); + T::DbWeight::get().reads(1) + } + } + + fn on_initialize(_n: BlockNumberFor) -> Weight { + // Abort if the lock does not exist, it means we're done migrating. + if !TemporaryMigrationLock::::exists() { + return T::DbWeight::get().reads(1) + } + let max_weight = T::BlockWeights::get().max_block; + + let (approval_stakes, leftover_weight, is_finished) = Self::build_approval_stakes(); + + for (who, approval_stake) in approval_stakes { + if let Some(stake) = ApprovalStake::::get(&who) { + ApprovalStake::::set(&who, Some(stake.saturating_add(approval_stake))); + } else { + ApprovalStake::::insert(&who, approval_stake) + } + } + + // If there is enough weight - do this in one go. If there's max_weight, meaning + // that we are finished with approval_stake aggregation - do it in one go as well. + if is_finished && + leftover_weight + .all_gte(Weight::from_parts((Validators::::count() * 2) as u64, 0)) || + leftover_weight == max_weight + { + for (key, value) in ApprovalStake::::iter() { + if Validators::::contains_key(&key) { + ::TargetList::on_insert(key, value) + .unwrap(); + } + } + MigrationV1StateValidators::::kill(); + MigrationV1StateNominators::::kill(); + } + + TemporaryMigrationLock::::kill(); + let current = StorageVersion::new(1); + current.put::>(); + max_weight + } + + #[cfg(feature = "try-runtime")] + // This isn't very useful as we aren't doing the actual migration OnRuntimeUpgrade, only + // setting the flag for on_initialize to continue. + fn pre_upgrade() -> Result, &'static str> { + ensure!(Pallet::::current_storage_version() == 0, "must upgrade linearly"); + ensure!( + ::TargetList::count() == 0, + "must be run on an empty TargetList instance" + ); + Ok(Vec::new()) + } + + #[cfg(feature = "try-runtime")] + // Same here, we can't really assume anything here as we aren't sure how many blocks it + // takes for the migration to happen, assuming it's more than one already breaks most of the + // promises we could verify here. + fn post_upgrade(_state: Vec) -> Result<(), &'static str> { + Ok(()) + } + } +} + +impl Pallet { + // Reimplemented to avoid leaking this from stake-tracker. + pub(crate) fn active_stake_of(who: &T::AccountId) -> BalanceOf { + ::Staking::stake(&who) + .map(|s| s.active) + .unwrap_or_default() + } + + fn nominator_state() -> MigrationState { + MigrationV1StateNominators::::get().unwrap_or(MigrationState { + last_key: >::map_storage_final_prefix() + .try_into() + .unwrap(), + prefix: >::map_storage_final_prefix().try_into().unwrap(), + done: false, + }) + } + + fn set_nominator_state(state: MigrationState) { + MigrationV1StateNominators::::set(Some(state)) + } + + fn validator_state() -> MigrationState { + MigrationV1StateValidators::::get().unwrap_or(MigrationState { + last_key: >::map_storage_final_prefix() + .try_into() + .unwrap(), + prefix: >::map_storage_final_prefix().try_into().unwrap(), + done: false, + }) + } + + fn set_validator_state(state: MigrationState) { + MigrationV1StateValidators::::set(Some(state)) + } + + // Build approval stakes based on available weight. + pub(crate) fn build_approval_stakes() -> (BTreeMap>, Weight, bool) { + let mut approval_stakes = BTreeMap::>::new(); + let mut leftover_weight = T::BlockWeights::get().max_block; + + let mut nominator_state = Self::nominator_state(); + + if !nominator_state.done { + // each iteration = 2 reads + 2 reads = 4 reads + while let Some(next_key) = sp_io::storage::next_key(nominator_state.last_key.as_ref()) { + if !next_key.starts_with(&nominator_state.prefix) { + // Nothing else to iterate. Save the state and bail. + nominator_state.done = true; + Self::set_nominator_state(nominator_state.clone()); + break + } + + // Should be safe due to the check above. + let mut account_raw = + next_key.strip_prefix(nominator_state.prefix.as_slice()).unwrap(); + let who = T::AccountId::decode(&mut account_raw).unwrap(); + + match storage::unhashed::get::>(&next_key) { + Some(nominations) => { + let stake = Self::active_stake_of(&who); + + for target in nominations.targets { + let current = approval_stakes.entry(target).or_default(); + *current = current.saturating_add(stake); + } + + nominator_state.last_key = next_key.try_into().unwrap(); + let approval_stake_count = approval_stakes.len() as u64; + leftover_weight = leftover_weight + .saturating_sub(T::DbWeight::get().reads(4)) + .saturating_sub( + T::DbWeight::get() + .reads_writes(approval_stake_count, approval_stake_count), + ); + + if leftover_weight.all_lte(Weight::default()) { + // We ran out of weight, also taking into account writing + // approval_stakes here. Save the state and bail. + Self::set_nominator_state(nominator_state.clone()); + + return (approval_stakes, leftover_weight, false) + } + }, + None => { + log!(warn, "an un-decodable nomination detected."); + break + }, + }; + } + } + + let mut validator_state = Self::validator_state(); + + if !validator_state.done { + // each iteration = 1 read + 2 reads = 3 reads + while let Some(next_key) = sp_io::storage::next_key(validator_state.last_key.as_ref()) { + if !next_key.starts_with(&validator_state.prefix) { + // Nothing else to iterate. Save the state and bail. + validator_state.done = true; + Self::set_validator_state(validator_state.clone()); + break + } + + // Should be safe due to the check above. + let mut account_raw = + next_key.strip_prefix(validator_state.prefix.as_slice()).unwrap(); + + let who = T::AccountId::decode(&mut account_raw).unwrap(); + let stake = Self::active_stake_of(&who); + let current = approval_stakes.entry(who).or_default(); + *current = current.saturating_add(stake); + validator_state.last_key = next_key.try_into().unwrap(); + + let approval_stake_count = approval_stakes.len() as u64; + leftover_weight = + leftover_weight.saturating_sub(T::DbWeight::get().reads(3)).saturating_sub( + T::DbWeight::get().reads_writes(approval_stake_count, approval_stake_count), + ); + + if leftover_weight.all_lte(Weight::default()) { + // We ran out of weight, also taking into account writing + // approval_stakes here. Save the state and bail. + Self::set_validator_state(validator_state.clone()); + + return (approval_stakes, leftover_weight, false) + } + } + } + + (approval_stakes, leftover_weight, true) + } +} diff --git a/frame/stake-tracker/src/lib.rs b/frame/stake-tracker/src/lib.rs index a12102dcd2b3f..d264a80e3cb9c 100644 --- a/frame/stake-tracker/src/lib.rs +++ b/frame/stake-tracker/src/lib.rs @@ -31,6 +31,10 @@ //! lists accordingly. It also exposes [`TrackedList`] that adds defensive checks to a subset of //! [`SortedListProvider`] methods in order to spot unexpected list updates on the consumer side. //! This wrapper should be used to pass the tracked entity to the consumer. +//! +//! ## Terminology +//! +//! - Approval Stake: a sum total of self-stake and all of the backing nominator stakes. #![cfg_attr(not(feature = "std"), no_std)] @@ -39,12 +43,13 @@ pub(crate) mod mock; #[cfg(test)] mod tests; -use frame_election_provider_support::{SortedListProvider, VoteWeight}; +use frame_election_provider_support::{ScoreProvider, SortedListProvider, VoteWeight}; use frame_support::{ defensive, traits::{Currency, CurrencyToVote, Defensive}, }; pub use pallet::*; +use sp_runtime::Saturating; use sp_staking::{OnStakingUpdate, Stake, StakingInterface}; use sp_std::{boxed::Box, vec::Vec}; @@ -57,6 +62,7 @@ pub mod pallet { use crate::*; use frame_election_provider_support::{SortedListProvider, VoteWeight}; use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::BlockNumberFor; use sp_staking::StakingInterface; @@ -77,6 +83,33 @@ pub mod pallet { /// A sorted list of nominators and validators, by their stake and self-stake respectively. type VoterList: SortedListProvider; + + /// A sorted list of validators, by their approval stake. + type TargetList: SortedListProvider>; + } + + /// The map from validator stash key to their approval stake. Note that this map is kept up to + /// date even if a validator chilled or turned into nominator. Entries from this map are only + /// ever removed if the stash is reaped. + /// + /// NOTE: This is currently a [`CountedStorageMap`] for debugging purposes. We might actually + /// want to revisit this once this pallet starts populating the actual [`Config::TargetList`] + /// used by [`Config::Staking`]. + #[pallet::storage] + pub type ApprovalStake = + CountedStorageMap<_, Twox64Concat, T::AccountId, BalanceOf, OptionQuery>; + + #[pallet::hooks] + impl Hooks> for Pallet { + #[cfg(feature = "try-runtime")] + fn try_state(_n: BlockNumberFor) -> Result<(), &'static str> { + ensure!( + ApprovalStake::::count() >= T::TargetList::count(), + "ApprovalStake map missing entries" + ); + T::TargetList::try_state()?; + T::VoterList::try_state() + } } } @@ -89,15 +122,49 @@ impl Pallet { let total_issuance = T::Currency::total_issuance(); ::CurrencyToVote::to_vote(balance, total_issuance) } + + pub(crate) fn approval_stake(who: &T::AccountId) -> Option> { + ApprovalStake::::get(who) + } } impl OnStakingUpdate> for Pallet { - fn on_stake_update(who: &T::AccountId, _: Option>>) { + fn on_stake_update(who: &T::AccountId, prev_stake: Option>>) { if let Ok(current_stake) = T::Staking::stake(who) { let current_active = current_stake.active; + let prev_active = prev_stake.map(|s| s.active).unwrap_or_default(); + + let update_approval_stake = |who: &T::AccountId| { + let mut approval_stake = Self::approval_stake(who).unwrap_or_default(); + + use sp_std::cmp::Ordering; + match current_active.cmp(&prev_active) { + Ordering::Greater => { + approval_stake = + approval_stake.saturating_add(current_active - prev_active); + }, + Ordering::Less => { + approval_stake = + approval_stake.saturating_sub(prev_active - current_active); + }, + Ordering::Equal => return, + }; + + if T::TargetList::contains(who) { + let _ = T::TargetList::on_update(who, approval_stake) + .defensive_proof("Unable to update a TargetList entry."); + } + + ApprovalStake::::set(who, Some(approval_stake)); + }; // If this is a nominator, update their position in the `VoterList`. - if let Some(_) = T::Staking::nominations(¤t_stake.stash) { + if let Some(targets) = T::Staking::nominations(¤t_stake.stash) { + // update the target list. + for target in targets { + update_approval_stake(&target); + } + let _ = T::VoterList::on_update(¤t_stake.stash, Self::to_vote(current_active)) .defensive_proof("Nominator's position in VoterList updated; qed"); @@ -105,48 +172,142 @@ impl OnStakingUpdate> for Pallet { // If this is a validator, update their position in the `VoterList`. if T::Staking::is_validator(¤t_stake.stash) { + if !T::TargetList::contains(¤t_stake.stash) { + defensive!("Validator exists in TargetList; qed."); + } + + update_approval_stake(¤t_stake.stash); let _ = T::VoterList::on_update(¤t_stake.stash, Self::to_vote(current_active)) .defensive_proof("Validator's position in VoterList updated; qed"); } + } else { + defensive!("on_stake_update is called on a bonded account; qed"); } } fn on_nominator_add(who: &T::AccountId) { - let _ = T::VoterList::on_insert(who.clone(), Self::to_vote(Self::active_stake_of(who))) - .defensive_proof("Nominator inserted into VoterList; qed"); + let score = Self::active_stake_of(who); + if let Some(nominations) = T::Staking::nominations(who) { + for nomination in nominations { + // Create a new entry if it does not exist + let new_stake = + Self::approval_stake(&nomination).unwrap_or_default().saturating_add(score); + + ApprovalStake::::set(&nomination, Some(new_stake)); + + if T::TargetList::contains(&nomination) { + let _ = T::TargetList::on_update(&nomination, new_stake) + .defensive_proof("Unable to update a TargetList entry."); + } + } + let _ = T::VoterList::on_insert(who.clone(), Self::to_vote(score)) + .defensive_proof("Nominator inserted into VoterList; qed"); + } else { + defensive!("on_nominator_add is called for a nominator; qed"); + } } - fn on_nominator_update(who: &T::AccountId, _prev_nominations: Vec) { - if !T::VoterList::contains(who) { - defensive!("Active nominator is in the VoterList; qed"); + fn on_nominator_update(who: &T::AccountId, prev_nominations: Vec) { + if let Some(nominations) = T::Staking::nominations(who) { + if !T::VoterList::contains(who) { + defensive!("Active nominator is in the VoterList; qed"); + } + let new = nominations.iter().filter(|n| !prev_nominations.contains(&n)); + let obsolete = prev_nominations.iter().filter(|n| !nominations.contains(&n)); + + let update_approval_stake = |nomination: &T::AccountId, new_stake: BalanceOf| { + ApprovalStake::::set(&nomination, Some(new_stake)); + + if T::TargetList::contains(&nomination) { + let _ = T::TargetList::on_update(&nomination, new_stake); + } + }; + + for nomination in new { + // Create a new entry if it does not exist + let new_stake = Self::approval_stake(&nomination) + .unwrap_or_default() + .saturating_add(Self::active_stake_of(who)); + + update_approval_stake(&nomination, new_stake); + } + + for nomination in obsolete { + if let Some(new_stake) = Self::approval_stake(&nomination) { + let new_stake = new_stake.saturating_sub(Self::active_stake_of(who)); + update_approval_stake(&nomination, new_stake); + } + } } } fn on_validator_add(who: &T::AccountId) { - let _ = T::VoterList::on_insert(who.clone(), Self::to_vote(Self::active_stake_of(who))) + let self_stake = Self::active_stake_of(who); + let new_stake = Self::approval_stake(who).unwrap_or_default().saturating_add(self_stake); + let _ = T::TargetList::on_insert(who.clone(), new_stake) + .defensive_proof("Validator inserted into TargetList; qed"); + let _ = T::VoterList::on_insert(who.clone(), Self::to_vote(self_stake)) .defensive_proof("Validator inserted into VoterList; qed"); + ApprovalStake::::set(who, Some(new_stake)); } fn on_validator_update(who: &T::AccountId) { if !T::VoterList::contains(who) { defensive!("Active validator is in the VoterList; qed"); } + if !T::TargetList::contains(who) { + defensive!("Active validator is in the TargetList; qed"); + } } fn on_validator_remove(who: &T::AccountId) { + let _ = + T::TargetList::on_remove(who).defensive_proof("Validator removed from TargetList; qed"); let _ = T::VoterList::on_remove(who).defensive_proof("Validator removed from VoterList; qed"); + + let self_stake = Self::active_stake_of(who); + let new_stake = Self::approval_stake(who).unwrap_or_default().saturating_sub(self_stake); + ApprovalStake::::set(who, Some(new_stake)); } - fn on_nominator_remove(who: &T::AccountId, _nominations: Vec) { + fn on_nominator_remove(who: &T::AccountId, nominations: Vec) { let _ = T::VoterList::on_remove(who).defensive_proof("Nominator removed from VoterList; qed"); + let score = Self::active_stake_of(who); + + for validator in nominations { + let _ = ApprovalStake::::mutate(&validator, |x: &mut Option>| { + *x = x.map(|b| b.saturating_sub(score)) + }); + let _ = T::TargetList::on_update( + &validator, + Self::approval_stake(&validator).unwrap_or_default(), + ); + } } fn on_unstake(who: &T::AccountId) { if T::VoterList::contains(who) { - defensive!("The staker has already been removed; qed"); + defensive!("The staker has already been removed from VoterList; qed"); + } + if T::TargetList::contains(who) { + defensive!("The validator has already been removed from TargetList; qed"); } + ApprovalStake::::remove(who); + } +} + +impl ScoreProvider for Pallet { + type Score = BalanceOf; + + fn score(who: &T::AccountId) -> Self::Score { + Self::approval_stake(who).unwrap_or_default() + } + + #[cfg(feature = "runtime-benchmarks")] + fn set_score_of(who: &T::AccountId, score: Self::Score) { + ApprovalStake::::set(who, Some(score)); } } diff --git a/frame/stake-tracker/src/mock.rs b/frame/stake-tracker/src/mock.rs index 7a6473bc2596e..8e7a6eab67266 100644 --- a/frame/stake-tracker/src/mock.rs +++ b/frame/stake-tracker/src/mock.rs @@ -24,6 +24,7 @@ use sp_runtime::{ DispatchError, DispatchResult, }; use sp_staking::{EraIndex, Stake, StakingInterface}; +use std::collections::HashMap; use Currency; pub(crate) type AccountId = u64; @@ -31,6 +32,7 @@ pub(crate) type AccountIndex = u64; pub(crate) type BlockNumber = u64; pub(crate) type Balance = u128; +pub(crate) type Staking = ::Staking; type Block = frame_system::mocking::MockBlock; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -43,7 +45,10 @@ frame_support::construct_runtime!( System: frame_system::{Pallet, Call, Config, Storage, Event}, Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, VoterBagsList: pallet_bags_list::::{Pallet, Call, Storage, Event}, + TargetBagsList: pallet_bags_list::::{Pallet, Call, Storage, Event}, StakeTracker: pallet_stake_tracker::{Pallet, Storage}, + StakeTrackerInitializer: pallet_stake_tracker_initializer::{Pallet, Storage}, + } ); @@ -98,6 +103,7 @@ impl pallet_stake_tracker::Config for Runtime { type Currency = Balances; type Staking = StakingMock; type VoterList = VoterBagsList; + type TargetList = TargetBagsList; } const THRESHOLDS: [sp_npos_elections::VoteWeight; 9] = [10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]; @@ -116,6 +122,23 @@ impl pallet_bags_list::Config for Runtime { type Score = VoteWeight; } +const THRESHOLDS_BALANCES: [Balance; 9] = [10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]; + +parameter_types! { + pub static BagThresholdsBalances: &'static [Balance] = &THRESHOLDS_BALANCES; +} + +type TargetBagsListInstance = pallet_bags_list::Instance2; +impl pallet_bags_list::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + // StakeTracker is the source of truth for target bags list, because chilled validators are + // removed from it. + type ScoreProvider = StakeTracker; + type BagThresholds = BagThresholdsBalances; + type Score = Balance; +} + pub struct StakingMock {} // We don't really care about this yet in the context of testing stake-tracker logic. @@ -144,6 +167,18 @@ pub(crate) fn stakers() -> Vec { stakers } +pub(crate) fn nominations() -> HashMap> { + let mut nominations = HashMap::new(); + for id in Nominators::get() { + for nomination in Staking::nominations(&id).unwrap_or_default() { + let total_stake: BalanceOf = + nominations.get(&nomination).unwrap_or(&0) + StakeTracker::active_stake_of(&id); + nominations.insert(nomination, total_stake); + } + } + nominations +} + impl StakingInterface for StakingMock { type Balance = Balance; type AccountId = AccountId; @@ -228,11 +263,16 @@ impl StakingInterface for StakingMock { } fn nominations(who: &Self::AccountId) -> Option> { + // set up two nominators with different nominations. if Nominators::get().contains(who) { - Some(Vec::new()) - } else { - None + if *who == 20 { + return Some(vec![10, 11]) + } else if *who == 21 { + return Some(vec![12, 13]) + } + return Some(Vec::new()) } + None } #[cfg(feature = "runtime-benchmarks")] diff --git a/frame/stake-tracker/src/tests.rs b/frame/stake-tracker/src/tests.rs index efd2229a61d41..6ef607226b03d 100644 --- a/frame/stake-tracker/src/tests.rs +++ b/frame/stake-tracker/src/tests.rs @@ -18,66 +18,155 @@ use super::mock::*; use crate as pallet_stake_tracker; use frame_election_provider_support::SortedListProvider; -use frame_support::{assert_storage_noop, traits::fungible::Mutate}; -use sp_staking::OnStakingUpdate; -type VoterList = ::VoterList; +use crate::ApprovalStake; +use frame_support::{ + assert_err, assert_ok, assert_storage_noop, + traits::fungible::{Inspect, Mutate, Unbalanced}, +}; +use sp_staking::{OnStakingUpdate, StakingInterface}; -// It is the caller's problem to make sure each of events is emitted in the right context, therefore -// we test each event for all the stakers (validators + nominators). +pub(crate) type VoterList = ::VoterList; +pub(crate) type TargetList = ::TargetList; +// Note that the total stake is ignored in the implementation, so we just keep it at 0 for +// convenience. mod on_stake_update { use super::*; + use frame_support::traits::tokens::Precision::Exact; #[test] - fn does_nothing_when_not_bonded() { + #[should_panic(expected = "on_stake_update is called on a bonded account; qed")] + fn defensive_when_not_bonded() { ExtBuilder::default().build_and_execute(|| { - assert_eq!(VoterList::count(), 0); // user without stake assert_storage_noop!(StakeTracker::on_stake_update(&30, None)); }); } #[test] - fn works() { + fn validator_works() { ExtBuilder::default().build_and_execute(|| { - let balance_before: Balance = 1000; - let balance_after: Balance = 10; let validator_id = 10; - let nominator_id = 20; - assert_eq!(VoterList::count(), 0); - // validator - Balances::set_balance(&validator_id, balance_before); StakeTracker::on_validator_add(&validator_id); assert_eq!( VoterList::get_score(&validator_id).unwrap(), StakeTracker::to_vote(StakeTracker::active_stake_of(&validator_id)) ); - - Balances::set_balance(&validator_id, balance_after); - StakeTracker::on_stake_update(&validator_id, None); assert_eq!( - VoterList::get_score(&validator_id).unwrap(), - StakeTracker::to_vote(StakeTracker::active_stake_of(&validator_id)) + TargetList::get_score(&validator_id).unwrap(), + StakeTracker::active_stake_of(&validator_id) ); - - // nominator - Balances::set_balance(&nominator_id, balance_before); - StakeTracker::on_nominator_add(&nominator_id); assert_eq!( - VoterList::get_score(&nominator_id).unwrap(), - StakeTracker::to_vote(StakeTracker::active_stake_of(&nominator_id)) + StakeTracker::approval_stake(&validator_id).unwrap(), + StakeTracker::active_stake_of(&validator_id) ); + let mut prev_stake = Staking::stake(&validator_id).unwrap(); + + // test stake update logic + for balance in vec![ + // same + Balances::balance(&validator_id), + // lower + Balances::balance(&validator_id) - 2, + // higher + Balances::balance(&validator_id) + 2, + ] { + Balances::set_balance(&validator_id, balance); + + StakeTracker::on_stake_update(&validator_id, Some(prev_stake.clone())); + assert_eq!( + VoterList::get_score(&validator_id).unwrap(), + StakeTracker::to_vote(StakeTracker::active_stake_of(&validator_id)) + ); + assert_eq!( + TargetList::get_score(&validator_id).unwrap(), + StakeTracker::active_stake_of(&validator_id) + ); + assert_eq!( + StakeTracker::approval_stake(&validator_id).unwrap(), + StakeTracker::active_stake_of(&validator_id) + ); + prev_stake = Staking::stake(&validator_id).unwrap(); + } + }); + } + + #[test] + fn nominator_works() { + ExtBuilder::default().build_and_execute(|| { + let nominator_id = 20; + let nominations = Staking::nominations(&nominator_id).unwrap(); + + StakeTracker::on_nominator_add(&nominator_id); + + let mut prev_stake = Staking::stake(&nominator_id).unwrap(); + + for validator in &nominations { + StakeTracker::on_validator_add(validator); + } + + // test stake update logic + for balance in vec![ + // same + Balances::balance(&nominator_id), + // lower + Balances::balance(&nominator_id) - 2, + // higher + Balances::balance(&nominator_id) + 2, + ] { + Balances::set_balance(&nominator_id, balance); + + let nominator_stake = StakeTracker::active_stake_of(&nominator_id); + + StakeTracker::on_stake_update(&nominator_id, Some(prev_stake.clone())); + assert_eq!( + VoterList::get_score(&nominator_id).unwrap(), + StakeTracker::to_vote(nominator_stake) + ); + + for validator in &nominations { + let validator_stake = StakeTracker::active_stake_of(validator); + assert_eq!( + TargetList::get_score(validator).unwrap(), + validator_stake + nominator_stake + ); + assert_eq!( + StakeTracker::approval_stake(validator).unwrap(), + validator_stake + nominator_stake + ); + } + + assert_eq!(TargetList::count(), nominations.len() as u32); + + prev_stake = Staking::stake(&nominator_id).unwrap(); + } + + // test that this works even if validators are not in the TargetList + + for validator in &nominations { + StakeTracker::on_validator_remove(validator); + } + + assert_eq!(TargetList::count(), 0); - Balances::set_balance(&nominator_id, balance_after); - StakeTracker::on_stake_update(&nominator_id, None); + assert_ok!(Balances::increase_balance(&nominator_id, 10, Exact)); + + StakeTracker::on_stake_update(&nominator_id, Some(prev_stake.clone())); assert_eq!( VoterList::get_score(&nominator_id).unwrap(), StakeTracker::to_vote(StakeTracker::active_stake_of(&nominator_id)) ); - assert_eq!(VoterList::count(), 2); + for validator in &nominations { + assert_eq!( + StakeTracker::approval_stake(validator).unwrap(), + StakeTracker::active_stake_of(&nominator_id) + ); + } + + assert_eq!(TargetList::count(), 0); }); } @@ -92,10 +181,19 @@ mod on_stake_update { #[test] #[should_panic(expected = "Validator's position in VoterList updated; qed")] - fn defensive_when_not_in_list_validator() { + fn defensive_when_not_in_voter_list_validator() { ExtBuilder::default().build_and_execute(|| { assert_eq!(VoterList::count(), 0); + assert_ok!(TargetList::on_insert(10, 1)); + StakeTracker::on_stake_update(&10, None); + }); + } + #[test] + #[should_panic(expected = "Validator exists in TargetList; qed.")] + fn defensive_when_not_in_target_list_validator() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); StakeTracker::on_stake_update(&10, None); }); } @@ -105,11 +203,13 @@ mod on_nominator_add { use super::*; #[test] - fn works() { + fn works_with_empty_lists() { ExtBuilder::default().build_and_execute(|| { assert_eq!(VoterList::count(), 0); + assert_eq!(TargetList::count(), 0); + + let nominations = nominations(); - // nominators for id in Nominators::get() { StakeTracker::on_nominator_add(&id); assert_eq!( @@ -118,7 +218,52 @@ mod on_nominator_add { ); } + for (nomination, score) in nominations.clone() { + assert_eq!(StakeTracker::approval_stake(&nomination).unwrap_or_default(), score); + } + assert_eq!(VoterList::count(), Nominators::get().len() as u32); + // does not update the TargetList as the validators were not added to it + assert_eq!(TargetList::count(), 0); + }); + } + + #[test] + fn works_with_prepopulated_target_list() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + assert_eq!(TargetList::count(), 0); + + let nominations = nominations(); + + // prepopulate the TargetList with nominated validators + for (nomination, _) in &nominations { + StakeTracker::on_validator_add(&nomination); + } + + // nominators + for id in Nominators::get() { + StakeTracker::on_nominator_add(&id); + assert_eq!( + VoterList::get_score(&id).unwrap(), + StakeTracker::to_vote(StakeTracker::active_stake_of(&id)) + ); + } + + for (nomination, score) in &nominations { + let approval_stake = *score + StakeTracker::active_stake_of(nomination); + assert_eq!( + StakeTracker::approval_stake(nomination).unwrap_or_default(), + approval_stake + ); + assert_eq!( + StakeTracker::approval_stake(nomination).unwrap_or_default(), + TargetList::get_score(nomination).unwrap() + ); + } + + assert_eq!(VoterList::count(), (Nominators::get().len() + nominations.len()) as u32); + assert_eq!(TargetList::count(), nominations.len() as u32); }); } @@ -131,11 +276,34 @@ mod on_nominator_add { StakeTracker::on_nominator_add(&20); }); } + + #[test] + #[should_panic(expected = "on_nominator_add is called for a nominator; qed")] + fn defensive_works_only_for_nominators() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + StakeTracker::on_nominator_add(&10); + }); + } } mod on_nominator_update { use super::*; + #[test] + fn noop_for_empty_nominations() { + ExtBuilder::default().build_and_execute(|| { + // nominators with no nominations + for id in Nominators::get() + .iter() + .filter(|id| Staking::nominations(&id).unwrap().len() == 0) + { + StakeTracker::on_nominator_add(&id); + assert_storage_noop!(StakeTracker::on_nominator_update(&id, Vec::new())); + } + }); + } + #[test] #[should_panic(expected = "Active nominator is in the VoterList; qed")] fn defensive_not_in_list() { @@ -146,14 +314,93 @@ mod on_nominator_update { } #[test] - fn noop() { + fn nomination_scenarios() { ExtBuilder::default().build_and_execute(|| { - assert_eq!(VoterList::count(), 0); - let id = 20; + let nominator_id = 20; + StakeTracker::on_nominator_add(&nominator_id); - StakeTracker::on_nominator_add(&id); - assert_storage_noop!(StakeTracker::on_nominator_update(&id, Vec::new())); - assert_eq!(VoterList::count(), 1); + // nominations are not in TargetList + StakeTracker::on_nominator_update( + &nominator_id, + Staking::nominations(&nominator_id).unwrap(), + ); + + for nomination in Staking::nominations(&nominator_id).unwrap() { + assert_err!( + TargetList::get_score(&nomination), + pallet_bags_list::ListError::NodeNotFound + ); + assert_eq!( + StakeTracker::approval_stake(&nomination).unwrap(), + StakeTracker::active_stake_of(&nominator_id) + ); + } + + assert_eq!(ApprovalStake::::count(), 2); + let _ = ApprovalStake::::clear(100, None); + + // no prev nominations and nominations are in TargetList + for nomination in Staking::nominations(&nominator_id).unwrap() { + let _ = StakeTracker::on_validator_add(&nomination); + } + + StakeTracker::on_nominator_update(&nominator_id, Vec::new()); + + for nomination in Staking::nominations(&nominator_id).unwrap() { + assert_eq!( + TargetList::get_score(&nomination).unwrap(), + StakeTracker::active_stake_of(&nominator_id) + .saturating_add(StakeTracker::active_stake_of(&nomination)) + ); + assert_eq!( + StakeTracker::approval_stake(&nomination).unwrap(), + StakeTracker::active_stake_of(&nominator_id) + .saturating_add(StakeTracker::active_stake_of(&nomination)) + ); + } + + // some previous nominations + + // reset two validators to have something new to nominate and something that won't + // be touched + for nomination in vec![10, 11] { + StakeTracker::on_validator_remove(&nomination); + StakeTracker::on_unstake(&nomination); + StakeTracker::on_validator_add(&nomination); + } + + // add a validator to have something to de-nominate + StakeTracker::on_validator_add(&12); + StakeTracker::on_nominator_update(&nominator_id, vec![11, 12]); + + assert_eq!( + TargetList::get_score(&12).unwrap(), + StakeTracker::active_stake_of(&12) + .saturating_sub(StakeTracker::active_stake_of(&nominator_id)) + ); + assert_eq!( + StakeTracker::approval_stake(&12).unwrap(), + StakeTracker::active_stake_of(&12) + .saturating_sub(StakeTracker::active_stake_of(&nominator_id)) + ); + + assert_eq!( + TargetList::get_score(&10).unwrap(), + StakeTracker::active_stake_of(&nominator_id) + .saturating_add(StakeTracker::active_stake_of(&10)) + ); + assert_eq!( + StakeTracker::approval_stake(&10).unwrap(), + StakeTracker::active_stake_of(&nominator_id) + .saturating_add(StakeTracker::active_stake_of(&10)) + ); + + // this is untouched as it was present in both current and prev nominations + assert_eq!(TargetList::get_score(&11).unwrap(), StakeTracker::active_stake_of(&11)); + assert_eq!( + StakeTracker::approval_stake(&11).unwrap(), + StakeTracker::active_stake_of(&11) + ); }); } } @@ -165,17 +412,64 @@ mod on_validator_add { fn works() { ExtBuilder::default().build_and_execute(|| { assert_eq!(VoterList::count(), 0); + assert_eq!(TargetList::count(), 0); - // validators for id in Validators::get() { StakeTracker::on_validator_add(&id); assert_eq!( VoterList::get_score(&id).unwrap(), StakeTracker::to_vote(StakeTracker::active_stake_of(&id)) ); + assert_eq!( + TargetList::get_score(&id).unwrap(), + StakeTracker::approval_stake(&id).unwrap() + ); } assert_eq!(VoterList::count(), Validators::get().len() as u32); + assert_eq!(TargetList::count(), Validators::get().len() as u32); + }); + } + + #[test] + fn works_with_existing_approval_stake() { + ExtBuilder::default().build_and_execute(|| { + let nominator_id = 20; + + assert_eq!(VoterList::count(), 0); + assert_eq!(TargetList::count(), 0); + + StakeTracker::on_nominator_add(&nominator_id); + + // validators + for id in Staking::nominations(&nominator_id).unwrap() { + StakeTracker::on_validator_add(&id); + assert_eq!( + VoterList::get_score(&id).unwrap(), + StakeTracker::to_vote(StakeTracker::active_stake_of(&id)) + ); + assert_eq!( + TargetList::get_score(&id).unwrap(), + StakeTracker::approval_stake(&id).unwrap() + ); + assert_eq!( + StakeTracker::approval_stake(&id).unwrap(), + StakeTracker::active_stake_of(&id) + + StakeTracker::active_stake_of(&nominator_id), + ); + } + + // nominations + nominator + assert_eq!( + VoterList::count(), + (Staking::nominations(&nominator_id).unwrap().len() + 1) as u32 + ); + + // validators + assert_eq!( + TargetList::count(), + Staking::nominations(&nominator_id).unwrap().len() as u32 + ); }); } @@ -183,9 +477,20 @@ mod on_validator_add { #[should_panic(expected = "Validator inserted into VoterList; qed")] fn defensive_when_in_list() { ExtBuilder::default().build_and_execute(|| { - assert_eq!(VoterList::count(), 0); let id = 10; + assert_eq!(VoterList::count(), 0); + let _ = VoterList::on_insert(10, 100); StakeTracker::on_validator_add(&id); + }); + } + + #[test] + #[should_panic(expected = "Validator inserted into TargetList; qed")] + fn defensive_when_in_target_list() { + ExtBuilder::default().build_and_execute(|| { + let id = 10; + assert_eq!(TargetList::count(), 0); + let _ = TargetList::on_insert(id, 100); StakeTracker::on_validator_add(&id); }); } @@ -209,34 +514,97 @@ mod on_validator_update { #[test] #[should_panic(expected = "Active validator is in the VoterList; qed")] - fn defensive_not_in_list() { + fn defensive_not_in_voter_list() { ExtBuilder::default().build_and_execute(|| { assert_eq!(VoterList::count(), 0); StakeTracker::on_validator_update(&10) }); } + + #[test] + #[should_panic(expected = "Active validator is in the TargetList; qed")] + fn defensive_not_in_target_list() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(VoterList::on_insert(10, 10)); + assert_eq!(TargetList::count(), 0); + StakeTracker::on_validator_update(&10) + }); + } } mod on_validator_remove { use super::*; #[test] - fn works_for_validator_and_nominator() { + fn works() { ExtBuilder::default().build_and_execute(|| { assert_eq!(VoterList::count(), 0); + assert_eq!(TargetList::count(), 0); let validator_id = 10; let nominator_id = 20; StakeTracker::on_validator_add(&validator_id); + assert_eq!(VoterList::count(), 1); + assert_eq!(TargetList::count(), 1); + assert_eq!( + StakeTracker::approval_stake(&validator_id).unwrap(), + TargetList::get_score(&validator_id).unwrap() + ); + assert_eq!( + StakeTracker::approval_stake(&validator_id).unwrap(), + StakeTracker::active_stake_of(&validator_id) + ); StakeTracker::on_validator_remove(&validator_id); - + assert_eq!(StakeTracker::approval_stake(&validator_id).unwrap(), 0); assert_eq!(VoterList::count(), 0); + assert_eq!(TargetList::count(), 0); + + // with a nomination StakeTracker::on_nominator_add(&nominator_id); - StakeTracker::on_validator_remove(&nominator_id); - assert_eq!(VoterList::count(), 0); + for validator_id in Staking::nominations(&nominator_id).unwrap() { + StakeTracker::on_validator_add(&validator_id); + assert_eq!(VoterList::count(), 2); + assert_eq!(TargetList::count(), 1); + assert_eq!( + StakeTracker::approval_stake(&validator_id).unwrap(), + TargetList::get_score(&validator_id).unwrap() + ); + assert_eq!( + StakeTracker::approval_stake(&validator_id).unwrap(), + StakeTracker::active_stake_of(&validator_id) + + StakeTracker::active_stake_of(&nominator_id) + ); + StakeTracker::on_validator_remove(&validator_id); + assert_eq!( + StakeTracker::approval_stake(&validator_id).unwrap(), + StakeTracker::active_stake_of(&nominator_id) + ); + assert_eq!(VoterList::count(), 1); + assert_eq!(TargetList::count(), 0); + } + }); + } + + #[test] + #[should_panic(expected = "Validator removed from TargetList; qed")] + fn defensive_when_not_in_target_list() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(TargetList::count(), 0); + StakeTracker::on_validator_remove(&10); + }); + } + + #[test] + #[should_panic(expected = "Validator removed from TargetList; qed")] + fn does_not_work_for_a_nominator() { + ExtBuilder::default().build_and_execute(|| { + let id = 20; + assert_eq!(TargetList::count(), 0); + StakeTracker::on_nominator_add(&id); + StakeTracker::on_validator_remove(&id); }); } @@ -245,6 +613,7 @@ mod on_validator_remove { fn defensive_when_not_in_list() { ExtBuilder::default().build_and_execute(|| { assert_eq!(VoterList::count(), 0); + assert_ok!(TargetList::on_insert(10, 10)); StakeTracker::on_validator_remove(&10); }); } @@ -253,6 +622,60 @@ mod on_validator_remove { mod on_nominator_remove { use super::*; + #[test] + #[should_panic(expected = "Nominator removed from VoterList; qed")] + fn defensive_when_not_in_list() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + StakeTracker::on_nominator_remove(&20, Vec::new()); + }); + } + + #[test] + fn works_with_and_without_nominations() { + ExtBuilder::default().build_and_execute(|| { + let nominator_id = 20; + + let nominations = Staking::nominations(&nominator_id).unwrap(); + + StakeTracker::on_nominator_add(&20); + StakeTracker::on_validator_add(&10); + StakeTracker::on_validator_add(&11); + + let check_nominations = || { + for id in &nominations { + assert_eq!( + TargetList::get_score(id).unwrap(), + StakeTracker::active_stake_of(id) + .saturating_add(StakeTracker::active_stake_of(&nominator_id)) + ); + assert_eq!( + StakeTracker::approval_stake(id).unwrap(), + StakeTracker::active_stake_of(id) + .saturating_add(StakeTracker::active_stake_of(&nominator_id)) + ); + } + }; + + check_nominations(); + + StakeTracker::on_nominator_remove(&20, nominations.clone()); + + for id in &nominations { + assert_eq!(TargetList::get_score(id).unwrap(), StakeTracker::active_stake_of(id)); + assert_eq!( + StakeTracker::approval_stake(id).unwrap(), + StakeTracker::active_stake_of(id) + ); + } + + StakeTracker::on_nominator_add(&20); + StakeTracker::on_nominator_remove(&20, Vec::new()); + + check_nominations(); + }); + } + #[test] fn works_for_nominator_and_validator() { ExtBuilder::default().build_and_execute(|| { @@ -272,24 +695,13 @@ mod on_nominator_remove { assert_eq!(VoterList::count(), 0); }); } - - #[test] - #[should_panic(expected = "Nominator removed from VoterList; qed")] - fn defensive_when_not_in_list() { - ExtBuilder::default().build_and_execute(|| { - assert_eq!(VoterList::count(), 0); - StakeTracker::on_nominator_remove(&20, vec![]); - }); - } } mod on_unstake { use super::*; #[test] - // By the time this is called - staker has to already be removed from the list. Otherwise we hit - // the defensive path. - fn noop_when_not_in_list() { + fn noop_when_no_approval_stake() { ExtBuilder::default().build_and_execute(|| { assert_eq!(VoterList::count(), 0); @@ -301,7 +713,17 @@ mod on_unstake { } #[test] - #[should_panic(expected = "The staker has already been removed; qed")] + fn removes_approval_stake() { + ExtBuilder::default().build_and_execute(|| { + let validator_id = 10; + ApprovalStake::::set(&validator_id, Some(1)); + StakeTracker::on_unstake(&validator_id); + assert_eq!(StakeTracker::approval_stake(&validator_id), None); + }); + } + + #[test] + #[should_panic(expected = "The staker has already been removed from VoterList; qed")] fn defensive_when_in_list() { ExtBuilder::default().build_and_execute(|| { assert_eq!(VoterList::count(), 0); @@ -309,4 +731,16 @@ mod on_unstake { StakeTracker::on_unstake(&10); }); } + + #[test] + #[should_panic(expected = "The validator has already been removed from TargetList; qed")] + fn defensive_when_in_target_list() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(TargetList::count(), 0); + for id in stakers() { + let _ = TargetList::on_insert(id, 100); + StakeTracker::on_unstake(&id); + } + }); + } } diff --git a/frame/staking/src/benchmarking.rs b/frame/staking/src/benchmarking.rs index ad7aab984bdca..cc16161510a82 100644 --- a/frame/staking/src/benchmarking.rs +++ b/frame/staking/src/benchmarking.rs @@ -945,7 +945,7 @@ mod tests { #[test] fn create_validators_with_nominators_for_era_works() { - ExtBuilder::default().build_and_execute(|| { + ExtBuilder::default().clear_tracker_target_list().build_and_execute(|| { let v = 10; let n = 100; @@ -971,7 +971,7 @@ mod tests { #[test] fn create_validator_with_nominators_works() { - ExtBuilder::default().build_and_execute(|| { + ExtBuilder::default().clear_tracker_target_list().build_and_execute(|| { let n = 10; let (validator_stash, nominators) = create_validator_with_nominators::( @@ -1000,7 +1000,7 @@ mod tests { #[test] fn add_slashing_spans_works() { - ExtBuilder::default().build_and_execute(|| { + ExtBuilder::default().clear_tracker_target_list().build_and_execute(|| { let n = 10; let (validator_stash, _nominators) = create_validator_with_nominators::( @@ -1032,7 +1032,7 @@ mod tests { #[test] fn test_payout_all() { - ExtBuilder::default().build_and_execute(|| { + ExtBuilder::default().clear_tracker_target_list().build_and_execute(|| { let v = 10; let n = 100; diff --git a/frame/staking/src/mock.rs b/frame/staking/src/mock.rs index 23f00749ae905..c60ce698c5ef2 100644 --- a/frame/staking/src/mock.rs +++ b/frame/staking/src/mock.rs @@ -25,7 +25,9 @@ use crate::{ }, *, }; -use frame_election_provider_support::{onchain, SequentialPhragmen, VoteWeight}; +use frame_election_provider_support::{ + onchain, SequentialPhragmen, SortedListProvider, VoteWeight, +}; use frame_support::{ assert_ok, ord_parameter_types, parameter_types, traits::{ @@ -109,6 +111,7 @@ frame_support::construct_runtime!( Session: pallet_session, Historical: pallet_session::historical, VoterBagsList: pallet_bags_list::, + TargetBagsList: pallet_bags_list::, StakeTracker: pallet_stake_tracker, } ); @@ -240,9 +243,10 @@ impl OnUnbalanced> for RewardRemainderMock { const THRESHOLDS: [sp_npos_elections::VoteWeight; 9] = [10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]; - +const THRESHOLDS_BALANCES: [Balance; 9] = [10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]; parameter_types! { pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS; + pub static BagThresholdsBalances: &'static [Balance] = &THRESHOLDS_BALANCES; pub static MaxNominations: u32 = 16; pub static HistoryDepth: u32 = 80; pub static MaxUnlockingChunks: u32 = 32; @@ -261,6 +265,16 @@ impl pallet_bags_list::Config for Test { type Score = VoteWeight; } +type TargetBagsListInstance = pallet_bags_list::Instance2; +impl pallet_bags_list::Config for Test { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + // Staking is the source of truth for voter bags list, since they are not kept up to date. + type ScoreProvider = StakeTracker; + type BagThresholds = BagThresholdsBalances; + type Score = Balance; +} + pub struct OnChainSeqPhragmen; impl onchain::Config for OnChainSeqPhragmen { type System = Test; @@ -376,6 +390,7 @@ impl pallet_stake_tracker::Config for Test { type Currency = Balances; type Staking = Staking; type VoterList = VoterBagsList; + type TargetList = TargetBagsList; } pub(crate) type StakingCall = crate::Call; @@ -395,6 +410,7 @@ pub struct ExtBuilder { stakes: BTreeMap, stakers: Vec<(AccountId, AccountId, Balance, StakerStatus)>, check_events: bool, + tracker_target_list_cleanup: bool, } impl Default for ExtBuilder { @@ -413,6 +429,7 @@ impl Default for ExtBuilder { stakes: Default::default(), stakers: Default::default(), check_events: false, + tracker_target_list_cleanup: false, } } } @@ -496,10 +513,16 @@ impl ExtBuilder { self.check_events = check; self } + // NOTE: can be removed when pallet_staking is allowed to use BagsList as TargetList. + frame_election_provider_support::runtime_benchmarks_or_test_enabled! { + pub fn clear_tracker_target_list(mut self) -> Self { + self.tracker_target_list_cleanup = true; + self + } + } fn build(self) -> sp_io::TestExternalities { sp_tracing::try_init_simple(); let mut storage = frame_system::GenesisConfig::default().build_storage::().unwrap(); - let _ = pallet_balances::GenesisConfig:: { balances: vec![ (1, 10 * self.balance_factor), @@ -622,7 +645,16 @@ impl ExtBuilder { pub fn build_and_execute(self, test: impl FnOnce() -> ()) { sp_tracing::try_init_simple(); let check_events = self.check_events; + #[allow(unused)] + let tracker_cleanup = self.tracker_target_list_cleanup; let mut ext = self.build(); + frame_election_provider_support::runtime_benchmarks_or_test_enabled! { + ext.execute_with(|| { + if tracker_cleanup { + ::TargetList::unsafe_clear(); + } + }); + } ext.execute_with(|| { // Clean up all the events produced on init. EmittedEvents::take(); @@ -635,6 +667,11 @@ impl ExtBuilder { }); } ext.execute_with(|| { + assert_eq!( + Validators::::count(), + ::TargetList::count(), + "The number of validators does not much StakeTracker::TargetList" + ); Staking::do_try_state(System::block_number()).unwrap(); }); } diff --git a/frame/staking/src/pallet/impls.rs b/frame/staking/src/pallet/impls.rs index d772bb04c733d..4345f10d61f90 100644 --- a/frame/staking/src/pallet/impls.rs +++ b/frame/staking/src/pallet/impls.rs @@ -1709,6 +1709,16 @@ impl Pallet { "VoterList contains non-staker" ); + ensure!( + T::TargetList::iter().all(|x| >::contains_key(&x)), + "TargetList contains non-validator" + ); + + ensure!( + T::TargetList::count() == >::count(), + "TargetList and Validator counts don't match" + ); + Self::check_nominators()?; Self::check_exposures()?; Self::check_ledgers()?; diff --git a/frame/staking/src/pallet/mod.rs b/frame/staking/src/pallet/mod.rs index cf878274a448f..969275f6b9429 100644 --- a/frame/staking/src/pallet/mod.rs +++ b/frame/staking/src/pallet/mod.rs @@ -580,6 +580,12 @@ pub mod pallet { #[pallet::storage] pub(crate) type ChillThreshold = StorageValue<_, Percent, OptionQuery>; + /// This is a lock introduced to initialize StakeTracker's TargetList. Once this has been done - + /// safe to remove. All the checks are going to be `!TemporaryMigrationLock::exists()`, + /// therefore we don't really care about the value. + #[pallet::storage] + pub type TemporaryMigrationLock = StorageValue<_, bool, OptionQuery>; + #[pallet::genesis_config] pub struct GenesisConfig { pub validator_count: u32, @@ -778,6 +784,9 @@ pub mod pallet { /// Nominations are not decodable. A nominator is then stuck until it's fixed, because we /// can't forgo the bookkeeping. NotDecodableNominator, + /// Operations affecting validator's approval stake are currently locked. Only for the + /// duration of StakeTracker init. + TemporarilyLocked, } #[pallet::hooks] @@ -860,6 +869,7 @@ pub mod pallet { payee: RewardDestination, ) -> DispatchResult { let stash = ensure_signed(origin)?; + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); if >::contains_key(&stash) { return Err(Error::::AlreadyBonded.into()) @@ -927,6 +937,7 @@ pub mod pallet { #[pallet::compact] max_additional: BalanceOf, ) -> DispatchResult { let stash = ensure_signed(origin)?; + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); let controller = Self::bonded(&stash).ok_or(Error::::NotStash)?; let mut ledger = Self::ledger(&controller).ok_or(Error::::NotController)?; @@ -978,6 +989,8 @@ pub mod pallet { #[pallet::compact] value: BalanceOf, ) -> DispatchResultWithPostInfo { let controller = ensure_signed(origin)?; + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); + let unlocking = Self::ledger(&controller) .map(|l| l.unlocking.len()) .ok_or(Error::::NotController)?; @@ -1076,6 +1089,7 @@ pub mod pallet { num_slashing_spans: u32, ) -> DispatchResultWithPostInfo { let controller = ensure_signed(origin)?; + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); let actual_weight = Self::do_withdraw_unbonded(&controller, num_slashing_spans)?; Ok(Some(actual_weight).into()) @@ -1090,7 +1104,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::validate())] pub fn validate(origin: OriginFor, prefs: ValidatorPrefs) -> DispatchResult { let controller = ensure_signed(origin)?; - + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); let ledger = Self::ledger(&controller).ok_or(Error::::NotController)?; ensure!(ledger.active >= MinValidatorBond::::get(), Error::::InsufficientBond); @@ -1136,6 +1150,7 @@ pub mod pallet { targets: Vec>, ) -> DispatchResult { let controller = ensure_signed(origin)?; + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); let ledger = Self::ledger(&controller).ok_or(Error::::NotController)?; ensure!(ledger.active >= MinNominatorBond::::get(), Error::::InsufficientBond); @@ -1211,6 +1226,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::chill())] pub fn chill(origin: OriginFor) -> DispatchResult { let controller = ensure_signed(origin)?; + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); let ledger = Self::ledger(&controller).ok_or(Error::::NotController)?; Self::chill_stash(&ledger.stash); Ok(()) @@ -1259,6 +1275,8 @@ pub mod pallet { controller: AccountIdLookupOf, ) -> DispatchResult { let stash = ensure_signed(origin)?; + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); + let old_controller = Self::bonded(&stash).ok_or(Error::::NotStash)?; let controller = T::Lookup::lookup(controller)?; if >::contains_key(&controller) { @@ -1489,6 +1507,7 @@ pub mod pallet { era: EraIndex, ) -> DispatchResultWithPostInfo { ensure_signed(origin)?; + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); Self::do_payout_stakers(validator_stash, era) } @@ -1506,6 +1525,7 @@ pub mod pallet { #[pallet::compact] value: BalanceOf, ) -> DispatchResultWithPostInfo { let controller = ensure_signed(origin)?; + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); let ledger = Self::ledger(&controller).ok_or(Error::::NotController)?; ensure!(!ledger.unlocking.is_empty(), Error::::NoUnlockChunk); @@ -1548,6 +1568,7 @@ pub mod pallet { num_slashing_spans: u32, ) -> DispatchResultWithPostInfo { let _ = ensure_signed(origin)?; + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); let ed = T::Currency::minimum_balance(); let reapable = T::Currency::total_balance(&stash) < ed || @@ -1577,6 +1598,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::kick(who.len() as u32))] pub fn kick(origin: OriginFor, who: Vec>) -> DispatchResult { let controller = ensure_signed(origin)?; + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); let ledger = Self::ledger(&controller).ok_or(Error::::NotController)?; let stash = &ledger.stash; @@ -1686,6 +1708,7 @@ pub mod pallet { pub fn chill_other(origin: OriginFor, controller: T::AccountId) -> DispatchResult { // Anyone can call this function. let caller = ensure_signed(origin)?; + ensure!(!TemporaryMigrationLock::::exists(), Error::::TemporarilyLocked); let ledger = Self::ledger(&controller).ok_or(Error::::NotController)?; let stash = ledger.stash; diff --git a/frame/staking/src/testing_utils.rs b/frame/staking/src/testing_utils.rs index 88525341229e8..452a6ed243801 100644 --- a/frame/staking/src/testing_utils.rs +++ b/frame/staking/src/testing_utils.rs @@ -46,6 +46,8 @@ pub fn clear_validators_and_nominators() { use frame_election_provider_support::SortedListProvider; // NOTE: safe to call outside block production T::VoterList::unsafe_clear(); + // NOTE: This is currently a noop due to the fact that we're using UseValidatorsMap. + T::TargetList::unsafe_clear(); } }