diff --git a/Cargo.lock b/Cargo.lock index 11359c078140b..17a754a78dc0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2288,6 +2288,19 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "git2" +version = "0.13.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9831e983241f8c5591ed53f17d874833e2fa82cac2625f3888c50cbfe136cba" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url 2.2.1", +] + [[package]] name = "glob" version = "0.3.0" @@ -3159,6 +3172,18 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" +[[package]] +name = "libgit2-sys" +version = "0.12.21+1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86271bacd72b2b9e854c3dcfb82efd538f15f870e4c11af66900effb462f6825" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + [[package]] name = "libloading" version = "0.5.2" @@ -3654,6 +3679,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" dependencies = [ "cc", + "libc", "pkg-config", "vcpkg", ] @@ -4442,6 +4468,16 @@ dependencies = [ "substrate-wasm-builder", ] +[[package]] +name = "node-runtime-voter-bags" +version = "3.0.0" +dependencies = [ + "node-runtime", + "pallet-staking", + "sp-io", + "structopt", +] + [[package]] name = "node-template" version = "3.0.0" @@ -4602,6 +4638,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-format" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" +dependencies = [ + "arrayvec 0.4.12", + "itoa", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -5498,12 +5544,15 @@ dependencies = [ name = "pallet-staking" version = "4.0.0-dev" dependencies = [ + "chrono", "frame-benchmarking", "frame-election-provider-support", "frame-support", "frame-system", + "git2", "hex", "log", + "num-format", "pallet-authorship", "pallet-balances", "pallet-session", @@ -5512,11 +5561,13 @@ dependencies = [ "parity-scale-codec", "parking_lot 0.11.1", "paste 1.0.4", + "rand 0.8.4", "rand_chacha 0.2.2", "serde", "sp-application-crypto", "sp-core", "sp-io", + "sp-npos-elections", "sp-runtime", "sp-staking", "sp-std", @@ -6768,14 +6819,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.4.3" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] @@ -6790,9 +6840,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.22" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "region" @@ -10043,18 +10093,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 2834344153a8c..dd4c5bf14647b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "bin/node/rpc", "bin/node/rpc-client", "bin/node/runtime", + "bin/node/runtime/voter-bags", "bin/node/testing", "bin/utils/chain-spec-builder", "bin/utils/subkey", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 37b4b24fa6a2a..a90e55848a979 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -93,6 +93,9 @@ pub mod constants; use constants::{currency::*, time::*}; use sp_runtime::generic::Era; +/// Generated voter bag information. +mod voter_bags; + // Make the WASM binary available. #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); @@ -483,6 +486,7 @@ parameter_types! { pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const MaxNominatorRewardedPerValidator: u32 = 256; pub OffchainRepeat: BlockNumber = 5; + pub const VoterBagThresholds: &'static [u64] = &voter_bags::THRESHOLDS; } use frame_election_provider_support::onchain; @@ -513,6 +517,7 @@ impl pallet_staking::Config for Runtime { pallet_election_provider_multi_phase::OnChainConfig, >; type WeightInfo = pallet_staking::weights::SubstrateWeight; + type VoterBagThresholds = VoterBagThresholds; } parameter_types! { @@ -543,6 +548,8 @@ parameter_types! { *RuntimeBlockLength::get() .max .get(DispatchClass::Normal); + + pub const VoterSnapshotPerBlock: u32 = u32::max_value(); } sp_npos_elections::generate_solution_type!( @@ -597,6 +604,7 @@ impl pallet_election_provider_multi_phase::Config for Runtime { type WeightInfo = pallet_election_provider_multi_phase::weights::SubstrateWeight; type ForceOrigin = EnsureRootOrHalfCouncil; type BenchmarkingConfig = BenchmarkConfig; + type VoterSnapshotPerBlock = VoterSnapshotPerBlock; } parameter_types! { diff --git a/bin/node/runtime/src/voter_bags.rs b/bin/node/runtime/src/voter_bags.rs new file mode 100644 index 0000000000000..c4c731a58badc --- /dev/null +++ b/bin/node/runtime/src/voter_bags.rs @@ -0,0 +1,235 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 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 voter bag thresholds. +//! +//! Generated on 2021-07-05T09:17:40.469754927+00:00 +//! for the node runtime. + +/// Existential weight for this runtime. +#[cfg(any(test, feature = "std"))] +#[allow(unused)] +pub const EXISTENTIAL_WEIGHT: u64 = 100_000_000_000_000; + +/// Constant ratio between bags for this runtime. +#[cfg(any(test, feature = "std"))] +#[allow(unused)] +pub const CONSTANT_RATIO: f64 = 1.0628253590743408; + +/// Upper thresholds delimiting the bag list. +pub const THRESHOLDS: [u64; 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/bin/node/runtime/voter-bags/Cargo.toml b/bin/node/runtime/voter-bags/Cargo.toml new file mode 100644 index 0000000000000..55b9dca859db8 --- /dev/null +++ b/bin/node/runtime/voter-bags/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "node-runtime-voter-bags" +version = "3.0.0" +authors = ["Parity Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://substrate.dev" +repository = "https://github.com/paritytech/substrate/" +description = "Voter Bag generation script for pallet-staking and node-runtime." +readme = "README.md" + +[dependencies] +node-runtime = { version = "3.0.0-dev", path = ".." } +pallet-staking = { version = "4.0.0-dev", path = "../../../../frame/staking", features = ["make-bags"] } +sp-io = { version = "4.0.0-dev", path = "../../../../primitives/io" } +structopt = "0.3.21" diff --git a/bin/node/runtime/voter-bags/src/main.rs b/bin/node/runtime/voter-bags/src/main.rs new file mode 100644 index 0000000000000..a92af37fb5bf8 --- /dev/null +++ b/bin/node/runtime/voter-bags/src/main.rs @@ -0,0 +1,38 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 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. + +//! Make the set of voting bag thresholds to be used in `voter_bags.rs`. + +use pallet_staking::voter_bags::make_bags::generate_thresholds_module; +use std::path::PathBuf; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +struct Opt { + /// How many bags to generate. + #[structopt(long, default_value = "200")] + n_bags: usize, + + /// Where to write the output. + output: PathBuf, +} + +fn main() -> Result<(), std::io::Error> { + let Opt { n_bags, output } = Opt::from_args(); + let mut ext = sp_io::TestExternalities::new_empty(); + ext.execute_with(|| generate_thresholds_module::(n_bags, &output)) +} diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index 795d51e5876f8..9e1c67a959389 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -219,6 +219,7 @@ impl pallet_staking::Config for Test { type ElectionProvider = onchain::OnChainSequentialPhragmen; type GenesisElectionProvider = Self::ElectionProvider; type WeightInfo = (); + type VoterBagThresholds = (); } impl pallet_offences::Config for Test { diff --git a/frame/election-provider-multi-phase/src/lib.rs b/frame/election-provider-multi-phase/src/lib.rs index 48504b6073958..01d1efff4e2bc 100644 --- a/frame/election-provider-multi-phase/src/lib.rs +++ b/frame/election-provider-multi-phase/src/lib.rs @@ -635,6 +635,15 @@ pub mod pallet { #[pallet::constant] type SignedDepositWeight: Get>; + /// The number of snapshot voters to fetch per block. + /// + /// In the future, this value will make more sense with multi-block snapshot. + /// + /// Also, note the data type: If the voters are represented by a `u32` in `type + /// CompactSolution`, the same `u32` is used here to ensure bounds are respected. + #[pallet::constant] + type VoterSnapshotPerBlock: Get>; + /// Handler for the slashed deposits. type SlashHandler: OnUnbalanced>; @@ -1294,8 +1303,11 @@ impl Pallet { /// /// Returns `Ok(consumed_weight)` if operation is okay. pub fn create_snapshot() -> Result { + // we don't impose any limits on the targets for now, the assumption is that + // `T::DataProvider` will sensibly return small values to use. let target_limit = >::max_value().saturated_into::(); - let voter_limit = >::max_value().saturated_into::(); + // for now we have just a single block snapshot. + let voter_limit = T::VoterSnapshotPerBlock::get().saturated_into::(); let (targets, w1) = T::DataProvider::targets(Some(target_limit)).map_err(ElectionError::DataProvider)?; @@ -1979,8 +1991,8 @@ mod tests { }) } - #[test] - fn snapshot_creation_fails_if_too_big() { + fn snapshot_too_big_failure_onchain_fallback() { + // the `MockStaking` is designed such that if it has too many targets, it simply fails. ExtBuilder::default().build_and_execute(|| { Targets::set((0..(TargetIndex::max_value() as AccountId) + 1).collect::>()); @@ -1996,6 +2008,49 @@ mod tests { roll_to(29); let (supports, _) = MultiPhase::elect().unwrap(); assert!(supports.len() > 0); + }); + } + + #[test] + fn snapshot_too_big_failure_no_fallback() { + // and if the backup mode is nothing, we go into the emergency mode.. + ExtBuilder::default().fallback(FallbackStrategy::Nothing).build_and_execute(|| { + crate::mock::Targets::set( + (0..(TargetIndex::max_value() as AccountId) + 1).collect::>(), + ); + + // Signed phase failed to open. + roll_to(15); + assert_eq!(MultiPhase::current_phase(), Phase::Off); + + // Unsigned phase failed to open. + roll_to(25); + assert_eq!(MultiPhase::current_phase(), Phase::Off); + + roll_to(29); + let err = MultiPhase::elect().unwrap_err(); + assert_eq!(err, ElectionError::NoFallbackConfigured); + assert_eq!(MultiPhase::current_phase(), Phase::Emergency); + }); + } + + #[test] + fn snapshot_too_big_truncate() { + // but if there are too many voters, we simply truncate them. + ExtBuilder::default().build_and_execute(|| { + // we have 8 voters in total. + assert_eq!(crate::mock::Voters::get().len(), 8); + // but we want to take 4. + crate::mock::VoterSnapshotPerBlock::set(2); + + // Signed phase opens just fine. + roll_to(15); + assert_eq!(MultiPhase::current_phase(), Phase::Signed); + + assert_eq!( + MultiPhase::snapshot_metadata().unwrap(), + SolutionOrSnapshotSize { voters: 2, targets: 4 } + ); }) } diff --git a/frame/election-provider-multi-phase/src/mock.rs b/frame/election-provider-multi-phase/src/mock.rs index 56007f15f84af..fc0ba4e5712bf 100644 --- a/frame/election-provider-multi-phase/src/mock.rs +++ b/frame/election-provider-multi-phase/src/mock.rs @@ -269,6 +269,7 @@ parameter_types! { pub static MinerMaxWeight: Weight = BlockWeights::get().max_block; pub static MinerMaxLength: u32 = 256; pub static MockWeightInfo: bool = false; + pub static VoterSnapshotPerBlock: VoterIndex = u32::max_value(); pub static EpochLength: u64 = 30; } @@ -379,6 +380,7 @@ impl crate::Config for Runtime { type Fallback = Fallback; type ForceOrigin = frame_system::EnsureRoot; type CompactSolution = TestCompact; + type VoterSnapshotPerBlock = VoterSnapshotPerBlock; } impl frame_system::offchain::SendTransactionTypes for Runtime @@ -410,9 +412,9 @@ impl ElectionDataProvider for StakingMock { fn voters( maybe_max_len: Option, ) -> data_provider::Result<(Vec<(AccountId, VoteWeight, Vec)>, Weight)> { - let voters = Voters::get(); - if maybe_max_len.map_or(false, |max_len| voters.len() > max_len) { - return Err("Voters too big") + let mut voters = Voters::get(); + if let Some(max_len) = maybe_max_len { + voters.truncate(max_len) } Ok((voters, 0)) diff --git a/frame/election-provider-support/src/onchain.rs b/frame/election-provider-support/src/onchain.rs index 2e2c286dc6422..6709fe33d9b95 100644 --- a/frame/election-provider-support/src/onchain.rs +++ b/frame/election-provider-support/src/onchain.rs @@ -77,11 +77,10 @@ impl ElectionProvider for OnChainSequen let (desired_targets, _) = Self::DataProvider::desired_targets().map_err(Error::DataProvider)?; - let mut stake_map: BTreeMap = BTreeMap::new(); - - voters.iter().for_each(|(v, s, _)| { - stake_map.insert(v.clone(), *s); - }); + let stake_map: BTreeMap = voters + .iter() + .map(|(validator, vote_weight, _)| (validator.clone(), *vote_weight)) + .collect(); let stake_of = |w: &T::AccountId| -> VoteWeight { stake_map.get(w).cloned().unwrap_or_default() }; diff --git a/frame/grandpa/src/mock.rs b/frame/grandpa/src/mock.rs index 882acdb4bcc12..d94e09d76d89b 100644 --- a/frame/grandpa/src/mock.rs +++ b/frame/grandpa/src/mock.rs @@ -221,6 +221,7 @@ impl pallet_staking::Config for Test { type ElectionProvider = onchain::OnChainSequentialPhragmen; type GenesisElectionProvider = Self::ElectionProvider; type WeightInfo = (); + type VoterBagThresholds = (); } impl pallet_offences::Config for Test { diff --git a/frame/offences/benchmarking/src/mock.rs b/frame/offences/benchmarking/src/mock.rs index 6fc5ee8b66eb0..af6191d4cb8e5 100644 --- a/frame/offences/benchmarking/src/mock.rs +++ b/frame/offences/benchmarking/src/mock.rs @@ -178,6 +178,7 @@ impl pallet_staking::Config for Test { type ElectionProvider = onchain::OnChainSequentialPhragmen; type GenesisElectionProvider = Self::ElectionProvider; type WeightInfo = (); + type VoterBagThresholds = (); } impl pallet_im_online::Config for Test { diff --git a/frame/session/benchmarking/src/mock.rs b/frame/session/benchmarking/src/mock.rs index bd61acb9de180..0bfe033dddce6 100644 --- a/frame/session/benchmarking/src/mock.rs +++ b/frame/session/benchmarking/src/mock.rs @@ -186,6 +186,7 @@ impl pallet_staking::Config for Test { type ElectionProvider = onchain::OnChainSequentialPhragmen; type GenesisElectionProvider = Self::ElectionProvider; type WeightInfo = (); + type VoterBagThresholds = (); } impl crate::Config for Test {} diff --git a/frame/staking/Cargo.toml b/frame/staking/Cargo.toml index 285fb11cc52c0..bfe2e04475452 100644 --- a/frame/staking/Cargo.toml +++ b/frame/staking/Cargo.toml @@ -26,6 +26,11 @@ pallet-session = { version = "4.0.0-dev", default-features = false, features = [ pallet-authorship = { version = "4.0.0-dev", default-features = false, path = "../authorship" } sp-application-crypto = { version = "4.0.0-dev", default-features = false, path = "../../primitives/application-crypto" } frame-election-provider-support = { version = "4.0.0-dev", default-features = false, path = "../election-provider-support" } +sp-core = { version = "4.0.0-dev", path = "../../primitives/core", optional = true } +pallet-timestamp = { version = "4.0.0-dev", path = "../timestamp", optional = true } +pallet-staking-reward-curve = { version = "4.0.0-dev", path = "../staking/reward-curve", optional = true } +pallet-balances = { version = "4.0.0-dev", path = "../balances", optional = true } +sp-tracing = { version = "4.0.0-dev", path = "../../primitives/tracing", optional = true } log = { version = "0.4.14", default-features = false } paste = "1.0" @@ -33,16 +38,23 @@ paste = "1.0" frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true } rand_chacha = { version = "0.2", default-features = false, optional = true } +# Optional imports for making voter bags lists +chrono = { version = "0.4.19", optional = true } +git2 = { version = "0.13.20", default-features = false, optional = true } +num-format = { version = "0.4.0", optional = true } + [dev-dependencies] sp-storage = { version = "4.0.0-dev", path = "../../primitives/storage" } sp-tracing = { version = "4.0.0-dev", path = "../../primitives/tracing" } sp-core = { version = "4.0.0-dev", path = "../../primitives/core" } +sp-npos-elections = { version = "4.0.0-dev", path = "../../primitives/npos-elections", features = ["mocks"] } pallet-balances = { version = "4.0.0-dev", path = "../balances" } pallet-timestamp = { version = "4.0.0-dev", path = "../timestamp" } pallet-staking-reward-curve = { version = "4.0.0-dev", path = "../staking/reward-curve" } substrate-test-utils = { version = "4.0.0-dev", path = "../../test-utils" } frame-benchmarking = { version = "4.0.0-dev", path = "../benchmarking" } frame-election-provider-support = { version = "4.0.0-dev", features = ["runtime-benchmarks"], path = "../election-provider-support" } +rand = "0.8.4" rand_chacha = { version = "0.2" } parking_lot = "0.11.1" hex = "0.4" @@ -70,3 +82,14 @@ runtime-benchmarks = [ "rand_chacha", ] try-runtime = ["frame-support/try-runtime"] +make-bags = [ + "chrono", + "git2", + "num-format", + "pallet-staking-reward-curve", + "pallet-balances", + "pallet-timestamp", + "sp-core", + "sp-tracing", + "std", +] diff --git a/frame/staking/src/benchmarking.rs b/frame/staking/src/benchmarking.rs index bdc3d81f3c29b..ff4a8ba986b00 100644 --- a/frame/staking/src/benchmarking.rs +++ b/frame/staking/src/benchmarking.rs @@ -26,12 +26,13 @@ use frame_support::{ traits::{Currency, Get, Imbalance}, }; use sp_runtime::{ - traits::{StaticLookup, Zero}, + traits::{CheckedSub, StaticLookup, Zero}, Perbill, Percent, }; use sp_staking::SessionIndex; use sp_std::prelude::*; +use crate::voter_bags::VoterList; pub use frame_benchmarking::{ account, benchmarks, impl_benchmark_test_suite, whitelist_account, whitelisted_caller, }; @@ -110,6 +111,13 @@ pub fn create_validator_with_nominators( assert_eq!(new_validators.len(), 1); assert_eq!(new_validators[0], v_stash, "Our validator was not selected!"); + assert_eq!( + VoterList::::decode_len().unwrap_or_default() as u32, + CounterForNominators::::get() + CounterForValidators::::get(), + "ensure storage has been mutated coherently", + ); + assert_ne!(CounterForValidators::::get(), 0); + assert_ne!(CounterForNominators::::get(), 0); // Give Era Points let reward = EraRewardPoints:: { @@ -598,9 +606,11 @@ benchmarks! { (0..s).for_each(|index| { add_slashing_spans::(&validators[index as usize], 10); }); + + let num_voters = (v + n) as usize; }: { - let voters = >::get_npos_voters(); - assert_eq!(voters.len() as u32, v + n); + let voters = >::get_npos_voters(None, num_voters); + assert_eq!(voters.len(), num_voters); } get_npos_targets { @@ -648,6 +658,115 @@ benchmarks! { verify { assert!(!Validators::::contains_key(controller)); } + + rebag { + // The most expensive case for this call: + // + // - It doesn't matter where in the origin bag the stash lies; the number of reads and + // writes is constant. We can use the case that the stash is the only one in the origin + // bag, for simplicity. + // - The destination bag is not empty, because then we need to update the `next` pointer + // of the previous node in addition to the work we do otherwise. + + use crate::voter_bags::{Bag, Node}; + + let make_validator = |n: u32, balance_factor: u32| -> Result<(T::AccountId, T::AccountId), &'static str> { + let (stash, controller) = create_stash_controller::(n, balance_factor, Default::default())?; + whitelist_account!(controller); + + let prefs = ValidatorPrefs::default(); + // bond the full value of the stash + let free_balance = T::Currency::free_balance(&stash); + Staking::::bond_extra(RawOrigin::Signed(stash.clone()).into(), free_balance)?; + Staking::::validate(RawOrigin::Signed(controller.clone()).into(), prefs)?; + + Ok((stash, controller)) + }; + + // Clean up any existing state. + clear_validators_and_nominators::(); + + let thresholds = T::VoterBagThresholds::get(); + + // stash controls the node account + let bag0_thresh = thresholds[0]; + let (stash, controller) = make_validator(USER_SEED, bag0_thresh as u32)?; + + // create another validator with more stake + let bag2_thresh = thresholds[2]; + let (other_stash, _) = make_validator(USER_SEED + 1, bag2_thresh as u32)?; + + // update the stash account's value/weight + // + // note that we have to manually update the ledger; if we were to just call + // `Staking::::bond_extra`, then it would implicitly rebag. We want to separate that step + // so we can measure it in isolation. + let other_free_balance = T::Currency::free_balance(&other_stash); + T::Currency::make_free_balance_be(&stash, other_free_balance); + let controller = Staking::::bonded(&stash).ok_or("stash had no controller")?; + let mut ledger = Staking::::ledger(&controller).ok_or("controller had no ledger")?; + let extra = other_free_balance.checked_sub(&ledger.total).ok_or("balance did not increase")?; + ledger.total += extra; + ledger.active += extra; + Staking::::update_ledger(&controller, &ledger); + + // verify preconditions + let weight_of = Staking::::weight_of_fn(); + let node = Node::::from_id(&stash).ok_or("node not found for stash")?; + ensure!( + node.is_misplaced(&weight_of), + "rebagging only makes sense when a node is misplaced", + ); + ensure!( + { + let origin_bag = Bag::::get(node.bag_upper).ok_or("origin bag not found")?; + origin_bag.iter().count() == 1 + }, + "stash should be the only node in origin bag", + ); + let other_node = Node::::from_id(&other_stash).ok_or("node not found for other_stash")?; + ensure!(!other_node.is_misplaced(&weight_of), "other stash balance never changed"); + ensure!( + { + let destination_bag = Bag::::get(node.proper_bag_for()).ok_or("destination bag not found")?; + destination_bag.iter().count() != 0 + }, + "destination bag should not be empty", + ); + drop(node); + + // caller will call rebag + let caller = whitelisted_caller(); + // ensure it's distinct from the other accounts + ensure!(caller != stash, "caller must not be the same as the stash"); + ensure!(caller != controller, "caller must not be the same as the controller"); + }: _(RawOrigin::Signed(caller), stash.clone()) + verify { + let node = Node::::from_id(&stash).ok_or("node not found for stash")?; + ensure!(!node.is_misplaced(&weight_of), "node must be in proper place after rebag"); + } + + regenerate { + // number of validator intention. + let v in (MAX_VALIDATORS / 2) .. MAX_VALIDATORS; + // number of nominator intention. + let n in (MAX_NOMINATORS / 2) .. MAX_NOMINATORS; + + clear_validators_and_nominators::(); + ensure!( + create_validators_with_nominators_for_era::( + v, + n, + T::MAX_NOMINATIONS as usize, + true, + None, + ).is_ok(), + "creating validators and nominators failed", + ); + }: { + let migrated = VoterList::::regenerate(); + ensure!(v + n == migrated, "didn't migrate right amount of voters"); + } } #[cfg(test)] diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs index 7f8774b94efb0..993fe349a1045 100644 --- a/frame/staking/src/lib.rs +++ b/frame/staking/src/lib.rs @@ -100,6 +100,13 @@ //! //! An account can become a nominator via the [`nominate`](Call::nominate) call. //! +//! #### Voting +//! +//! Staking is closely related to elections; actual validators are chosen from among all potential +//! validators by election by the potential validators and nominators. To reduce use of the phrase +//! "potential validators and nominators", we often use the term **voters**, who are simply +//! the union of potential validators and nominators. +//! //! #### Rewards and Slash //! //! The **reward and slashing** procedure is the core of the Staking pallet, attempting to _embrace @@ -264,26 +271,28 @@ //! - [Session](../pallet_session/index.html): Used to manage sessions. Also, a list of new //! validators is stored in the Session pallet's `Validators` at the end of each era. -#![recursion_limit = "128"] #![cfg_attr(not(feature = "std"), no_std)] #[cfg(any(feature = "runtime-benchmarks", test))] pub mod benchmarking; -#[cfg(test)] -mod mock; #[cfg(any(feature = "runtime-benchmarks", test))] pub mod testing_utils; + +#[cfg(test)] +pub(crate) mod mock; #[cfg(test)] mod tests; pub mod inflation; pub mod migrations; pub mod slashing; +pub mod voter_bags; pub mod weights; mod pallet; use codec::{Decode, Encode, HasCompact}; +use frame_election_provider_support::VoteWeight; use frame_support::{ traits::{Currency, Get}, weights::Weight, @@ -332,6 +341,9 @@ type NegativeImbalanceOf = <::Currency as Currency< ::AccountId, >>::NegativeImbalance; +type AccountIdOf = ::AccountId; +type VotingDataOf = (AccountIdOf, VoteWeight, Vec>); + /// Information regarding the active era (era in used in session). #[derive(Encode, Decode, RuntimeDebug)] pub struct ActiveEraInfo { @@ -724,11 +736,12 @@ enum Releases { V5_0_0, // blockable validators. V6_0_0, // removal of all storage associated with offchain phragmen. V7_0_0, // keep track of number of nominators / validators in map + V8_0_0, // VoterList and efficient semi-sorted iteration } impl Default for Releases { fn default() -> Self { - Releases::V7_0_0 + Releases::V8_0_0 } } diff --git a/frame/staking/src/migrations.rs b/frame/staking/src/migrations.rs index d7fa2afc63082..83f3ae9b4d83e 100644 --- a/frame/staking/src/migrations.rs +++ b/frame/staking/src/migrations.rs @@ -18,6 +18,37 @@ use super::*; +pub mod v8 { + use super::{voter_bags::VoterList, *}; + use frame_support::ensure; + + pub fn pre_migrate() -> Result<(), &'static str> { + ensure!(StorageVersion::::get() == Releases::V7_0_0, "must upgrade linearly"); + ensure!(VoterList::::iter().count() == 0, "voter list already exists"); + Ok(()) + } + + pub fn migrate() -> Weight { + log!(info, "Migrating staking to Releases::V8_0_0"); + + let migrated = VoterList::::regenerate(); + debug_assert_eq!(VoterList::::sanity_check(), Ok(())); + + StorageVersion::::put(Releases::V8_0_0); + log!( + info, + "Completed staking migration to Releases::V8_0_0 with {} voters migrated", + migrated, + ); + + T::WeightInfo::regenerate( + CounterForValidators::::get(), + CounterForNominators::::get(), + ) + .saturating_add(T::DbWeight::get().reads(2)) + } +} + pub mod v7 { use super::*; diff --git a/frame/staking/src/mock.rs b/frame/staking/src/mock.rs index 9d50a43754e76..970b8a0f6e981 100644 --- a/frame/staking/src/mock.rs +++ b/frame/staking/src/mock.rs @@ -18,7 +18,7 @@ //! Test utilities use crate as staking; -use crate::*; +use crate::{voter_bags::VoterList, *}; use frame_election_provider_support::onchain; use frame_support::{ assert_ok, parameter_types, @@ -250,6 +250,13 @@ impl onchain::Config for Test { type DataProvider = Staking; } +/// Thresholds used for bags. +const THRESHOLDS: [VoteWeight; 9] = [10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]; + +parameter_types! { + pub const VoterBagThresholds: &'static [VoteWeight] = &THRESHOLDS; +} + impl Config for Test { const MAX_NOMINATIONS: u32 = 16; type Currency = Balances; @@ -270,6 +277,7 @@ impl Config for Test { type ElectionProvider = onchain::OnChainSequentialPhragmen; type GenesisElectionProvider = Self::ElectionProvider; type WeightInfo = (); + type VoterBagThresholds = VoterBagThresholds; } impl frame_system::offchain::SendTransactionTypes for Test @@ -383,6 +391,7 @@ impl ExtBuilder { } fn build(self) -> sp_io::TestExternalities { sp_tracing::try_init_simple(); + let mut storage = frame_system::GenesisConfig::default().build_storage::().unwrap(); let balance_factor = if ExistentialDeposit::get() > 1 { 256 } else { 1 }; @@ -487,13 +496,23 @@ impl ExtBuilder { ext.execute_with(test); ext.execute_with(post_conditions); } + /// WARNING: This should only be use for testing `VoterList` api or lower. + pub fn build_and_execute_without_check_count(self, test: impl FnOnce() -> ()) { + let mut ext = self.build(); + ext.execute_with(test); + ext.execute_with(post_conditions_without_check_count); + } } fn post_conditions() { + post_conditions_without_check_count(); + check_count(); +} + +fn post_conditions_without_check_count() { check_nominators(); check_exposures(); check_ledgers(); - check_count(); } fn check_count() { @@ -501,6 +520,9 @@ fn check_count() { let validator_count = Validators::::iter().count() as u32; assert_eq!(nominator_count, CounterForNominators::::get()); assert_eq!(validator_count, CounterForValidators::::get()); + + let voters_count = CounterForVoters::::get(); + assert_eq!(voters_count, nominator_count + validator_count); } fn check_ledgers() { @@ -593,10 +615,14 @@ pub(crate) fn current_era() -> EraIndex { Staking::current_era().unwrap() } -pub(crate) fn bond_validator(stash: AccountId, ctrl: AccountId, val: Balance) { +pub(crate) fn bond(stash: AccountId, ctrl: AccountId, val: Balance) { let _ = Balances::make_free_balance_be(&stash, val); let _ = Balances::make_free_balance_be(&ctrl, val); assert_ok!(Staking::bond(Origin::signed(stash), ctrl, val, RewardDestination::Controller)); +} + +pub(crate) fn bond_validator(stash: AccountId, ctrl: AccountId, val: Balance) { + bond(stash, ctrl, val); assert_ok!(Staking::validate(Origin::signed(ctrl), ValidatorPrefs::default())); } @@ -606,9 +632,7 @@ pub(crate) fn bond_nominator( val: Balance, target: Vec, ) { - let _ = Balances::make_free_balance_be(&stash, val); - let _ = Balances::make_free_balance_be(&ctrl, val); - assert_ok!(Staking::bond(Origin::signed(stash), ctrl, val, RewardDestination::Controller)); + bond(stash, ctrl, val); assert_ok!(Staking::nominate(Origin::signed(ctrl), target)); } @@ -812,3 +836,38 @@ pub(crate) fn staking_events() -> Vec> { pub(crate) fn balances(who: &AccountId) -> (Balance, Balance) { (Balances::free_balance(who), Balances::reserved_balance(who)) } + +use crate::voter_bags::Bag; +/// Returns the nodes of all non-empty bags. +pub(crate) fn get_bags() -> Vec<(VoteWeight, Vec)> { + VoterBagThresholds::get() + .into_iter() + .filter_map(|t| { + Bag::::get(*t) + .map(|bag| (*t, bag.iter().map(|n| n.voter().id).collect::>())) + }) + .collect::>() +} + +pub(crate) fn bag_as_ids(bag: &Bag) -> Vec { + bag.iter().map(|n| n.voter().id).collect::>() +} + +pub(crate) fn get_voter_list_as_ids() -> Vec { + VoterList::::iter().map(|n| n.voter().id).collect::>() +} + +pub(crate) fn get_voter_list_as_voters() -> Vec> { + VoterList::::iter().map(|node| node.voter().clone()).collect::>() +} + +// Useful for when you want to change the effectively bonded value but you don't want to use +// the bond extrinsics because they implicitly rebag. +pub(crate) fn set_ledger_and_free_balance(account: &AccountId, value: Balance) { + Balances::make_free_balance_be(account, value); + let controller = Staking::bonded(account).unwrap(); + let mut ledger = Staking::ledger(&controller).unwrap(); + ledger.total = value; + ledger.active = value; + Staking::update_ledger(&controller, &ledger); +} diff --git a/frame/staking/src/pallet/impls.rs b/frame/staking/src/pallet/impls.rs index accd7a0cf02eb..474411895b4ca 100644 --- a/frame/staking/src/pallet/impls.rs +++ b/frame/staking/src/pallet/impls.rs @@ -26,6 +26,7 @@ use frame_support::{ }, weights::{Weight, WithPostDispatchInfo}, }; +use frame_system::pallet_prelude::BlockNumberFor; use pallet_session::historical; use sp_runtime::{ traits::{Bounded, Convert, SaturatedConversion, Saturating, Zero}, @@ -38,9 +39,12 @@ use sp_staking::{ use sp_std::{collections::btree_map::BTreeMap, prelude::*}; use crate::{ - log, slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, EraIndex, EraPayout, Exposure, - ExposureOf, Forcing, IndividualExposure, Nominations, PositiveImbalanceOf, RewardDestination, - SessionInterface, StakingLedger, ValidatorPrefs, + log, slashing, + voter_bags::{self, VoterList}, + weights::WeightInfo, + ActiveEraInfo, BalanceOf, EraIndex, EraPayout, Exposure, ExposureOf, Forcing, + IndividualExposure, Nominations, PositiveImbalanceOf, RewardDestination, SessionInterface, + StakingLedger, ValidatorPrefs, VotingDataOf, }; use super::{pallet::*, STAKING_ID}; @@ -64,7 +68,7 @@ impl Pallet { /// /// This prevents call sites from repeatedly requesting `total_issuance` from backend. But it is /// important to be only used while the total issuance is not changing. - pub fn slashable_balance_of_fn() -> Box VoteWeight> { + pub fn weight_of_fn() -> Box VoteWeight> { // NOTE: changing this to unboxed `impl Fn(..)` return type and the pallet will still // compile, while some types in mock fail to resolve. let issuance = T::Currency::total_issuance(); @@ -78,10 +82,10 @@ impl Pallet { era: EraIndex, ) -> DispatchResultWithPostInfo { // Validate input data - let current_era = CurrentEra::::get().ok_or( + let current_era = CurrentEra::::get().ok_or_else(|| { Error::::InvalidEraToReward - .with_weight(T::WeightInfo::payout_stakers_alive_staked(0)), - )?; + .with_weight(T::WeightInfo::payout_stakers_alive_staked(0)) + })?; let history_depth = Self::history_depth(); ensure!( era <= current_era && era >= current_era.saturating_sub(history_depth), @@ -96,10 +100,10 @@ impl Pallet { .with_weight(T::WeightInfo::payout_stakers_alive_staked(0)) })?; - let controller = Self::bonded(&validator_stash).ok_or( - Error::::NotStash.with_weight(T::WeightInfo::payout_stakers_alive_staked(0)), - )?; - let mut ledger = >::get(&controller).ok_or_else(|| Error::::NotController)?; + let controller = Self::bonded(&validator_stash).ok_or_else(|| { + Error::::NotStash.with_weight(T::WeightInfo::payout_stakers_alive_staked(0)) + })?; + let mut ledger = >::get(&controller).ok_or(Error::::NotController)?; ledger .claimed_rewards @@ -633,6 +637,9 @@ impl Pallet { /// Get all of the voters that are eligible for the npos election. /// + /// `voter_count` imposes an implicit cap on the number of voters returned; care should be taken + /// to ensure that it is accurate. + /// /// This will use all on-chain nominators, and all the validators will inject a self vote. /// /// ### Slashing @@ -640,38 +647,22 @@ impl Pallet { /// All nominations that have been submitted before the last non-zero slash of the validator are /// auto-chilled. /// - /// Note that this is VERY expensive. Use with care. - pub fn get_npos_voters() -> Vec<(T::AccountId, VoteWeight, Vec)> { - let weight_of = Self::slashable_balance_of_fn(); - let mut all_voters = Vec::new(); - - for (validator, _) in >::iter() { - // Append self vote. - let self_vote = (validator.clone(), weight_of(&validator), vec![validator.clone()]); - all_voters.push(self_vote); - } + /// Note that this is fairly expensive: it must iterate over the min of `maybe_max_len` and + /// `voter_count` voters. Use with care. + pub fn get_npos_voters( + maybe_max_len: Option, + voter_count: usize, + ) -> Vec> { + let wanted_voters = maybe_max_len.unwrap_or(voter_count).min(voter_count); - // Collect all slashing spans into a BTreeMap for further queries. + let weight_of = Self::weight_of_fn(); + // collect all slashing spans into a BTreeMap for further queries. let slashing_spans = >::iter().collect::>(); - for (nominator, nominations) in Nominators::::iter() { - let Nominations { submitted_in, mut targets, suppressed: _ } = nominations; - - // Filter out nomination targets which were nominated before the most recent - // slashing span. - targets.retain(|stash| { - slashing_spans - .get(stash) - .map_or(true, |spans| submitted_in >= spans.last_nonzero_slash()) - }); - - if !targets.is_empty() { - let vote_weight = weight_of(&nominator); - all_voters.push((nominator, vote_weight, targets)) - } - } - - all_voters + VoterList::::iter() + .filter_map(|node| node.voting_data(&weight_of, &slashing_spans)) + .take(wanted_voters) + .collect() } /// This is a very expensive function and result should be cached versus being called multiple times. @@ -683,54 +674,95 @@ impl Pallet { /// and keep track of the `CounterForNominators`. /// /// If the nominator already exists, their nominations will be updated. + /// + /// NOTE: you must ALWAYS use this function to add a nominator to the system. Any access to + /// `Nominators`, its counter, or `VoterList` outside of this function is almost certainly + /// wrong. pub fn do_add_nominator(who: &T::AccountId, nominations: Nominations) { if !Nominators::::contains_key(who) { CounterForNominators::::mutate(|x| x.saturating_inc()) } Nominators::::insert(who, nominations); + VoterList::::insert_as(who, voter_bags::VoterType::Nominator); + debug_assert_eq!(VoterList::::sanity_check(), Ok(())); } /// This function will remove a nominator from the `Nominators` storage map, /// and keep track of the `CounterForNominators`. /// /// Returns true if `who` was removed from `Nominators`, otherwise false. + /// + /// NOTE: you must ALWAYS use this function to remove a nominator from the system. Any access to + /// `Nominators`, its counter, or `VoterList` outside of this function is almost certainly + /// wrong. pub fn do_remove_nominator(who: &T::AccountId) -> bool { if Nominators::::contains_key(who) { Nominators::::remove(who); CounterForNominators::::mutate(|x| x.saturating_dec()); + VoterList::::remove(who); + debug_assert_eq!(VoterList::::sanity_check(), Ok(())); true } else { false } } - /// This function will add a validator to the `Validators` storage map, - /// and keep track of the `CounterForValidators`. + /// This function will add a validator to the `Validators` storage map, and keep track of the + /// `CounterForValidators`. /// /// If the validator already exists, their preferences will be updated. + /// + /// NOTE: you must ALWAYS use this function to add a validator to the system. Any access to + /// `Validators`, its counter, or `VoterList` outside of this function is almost certainly + /// wrong. pub fn do_add_validator(who: &T::AccountId, prefs: ValidatorPrefs) { if !Validators::::contains_key(who) { CounterForValidators::::mutate(|x| x.saturating_inc()) } Validators::::insert(who, prefs); + VoterList::::insert_as(who, voter_bags::VoterType::Validator); + debug_assert_eq!(VoterList::::sanity_check(), Ok(())); } /// This function will remove a validator from the `Validators` storage map, /// and keep track of the `CounterForValidators`. /// /// Returns true if `who` was removed from `Validators`, otherwise false. + /// + /// NOTE: you must ALWAYS use this function to remove a validator from the system. Any access to + /// `Validators`, its counter, or `VoterList` outside of this function is almost certainly + /// wrong. pub fn do_remove_validator(who: &T::AccountId) -> bool { if Validators::::contains_key(who) { Validators::::remove(who); CounterForValidators::::mutate(|x| x.saturating_dec()); + VoterList::::remove(who); + debug_assert_eq!(VoterList::::sanity_check(), Ok(())); true } else { false } } + + /// Move a stash account from one bag to another, depositing an event on success. + /// + /// If the stash changed bags, returns `Some((from, to))`. + pub fn do_rebag(stash: &T::AccountId) -> Option<(VoteWeight, VoteWeight)> { + // if no voter at that node, don't do anything. + // the caller just wasted the fee to call this. + let maybe_movement = voter_bags::Node::::from_id(&stash).and_then(|node| { + let weight_of = Self::weight_of_fn(); + VoterList::update_position_for(node, weight_of) + }); + if let Some((from, to)) = maybe_movement { + Self::deposit_event(Event::::Rebagged(stash.clone(), from, to)); + }; + maybe_movement + } } -impl frame_election_provider_support::ElectionDataProvider +impl + frame_election_provider_support::ElectionDataProvider> for Pallet { const MAXIMUM_VOTES_PER_VOTER: u32 = T::MAX_NOMINATIONS; @@ -744,12 +776,15 @@ impl frame_election_provider_support::ElectionDataProvider::get(); let validator_count = CounterForValidators::::get(); let voter_count = nominator_count.saturating_add(validator_count) as usize; + + // check a few counters one last time... debug_assert!(>::iter().count() as u32 == CounterForNominators::::get()); debug_assert!(>::iter().count() as u32 == CounterForValidators::::get()); - - if maybe_max_len.map_or(false, |max_len| voter_count > max_len) { - return Err("Voter snapshot too big") - } + debug_assert_eq!( + voter_count, + VoterList::::decode_len().unwrap_or_default(), + "voter_count must be accurate", + ); let slashing_span_count = >::iter().count(); let weight = T::WeightInfo::get_npos_voters( @@ -757,7 +792,8 @@ impl frame_election_provider_support::ElectionDataProvider) -> data_provider::Result<(Vec, Weight)> { @@ -862,7 +898,7 @@ impl frame_election_provider_support::ElectionDataProvider = target_stake .and_then(|w| >::try_from(w).ok()) - .unwrap_or(MinNominatorBond::::get() * 100u32.into()); + .unwrap_or_else(|| MinNominatorBond::::get() * 100u32.into()); >::insert(v.clone(), v.clone()); >::insert( v.clone(), diff --git a/frame/staking/src/pallet/mod.rs b/frame/staking/src/pallet/mod.rs index 444768dbdccfa..ed518b0a2894b 100644 --- a/frame/staking/src/pallet/mod.rs +++ b/frame/staking/src/pallet/mod.rs @@ -17,6 +17,7 @@ //! Staking FRAME Pallet. +use frame_election_provider_support::VoteWeight; use frame_support::{ pallet_prelude::*, traits::{ @@ -41,10 +42,10 @@ mod impls; pub use impls::*; use crate::{ - migrations, slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, EraIndex, EraPayout, - EraRewardPoints, Exposure, Forcing, NegativeImbalanceOf, Nominations, PositiveImbalanceOf, - Releases, RewardDestination, SessionInterface, StakerStatus, StakingLedger, UnappliedSlash, - UnlockChunk, ValidatorPrefs, + log, migrations, slashing, voter_bags, weights::WeightInfo, AccountIdOf, ActiveEraInfo, + BalanceOf, EraIndex, EraPayout, EraRewardPoints, Exposure, Forcing, NegativeImbalanceOf, + Nominations, PositiveImbalanceOf, Releases, RewardDestination, SessionInterface, StakerStatus, + StakingLedger, UnappliedSlash, UnlockChunk, ValidatorPrefs, }; pub const MAX_UNLOCKING_CHUNKS: usize = 32; @@ -144,6 +145,56 @@ pub mod pallet { /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; + + /// The list of thresholds separating the various voter bags. + /// + /// Voters are separated into unsorted bags according to their vote weight. This specifies + /// the thresholds separating the bags. A voter's bag is the largest bag for which the + /// voter's weight is less than or equal to its upper threshold. + /// + /// When voters are iterated, higher bags are iterated completely before lower bags. This + /// means that iteration is _semi-sorted_: voters of higher weight tend to come before + /// voters of lower weight, but peer voters within a particular bag are sorted in insertion + /// order. + /// + /// # Expressing the constant + /// + /// This constant must be sorted in strictly increasing order. Duplicate items are not + /// permitted. + /// + /// There is an implied upper limit of `VoteWeight::MAX`; that value does not need to be + /// specified within the bag. For any two threshold lists, if one ends with + /// `VoteWeight::MAX`, the other one does not, and they are otherwise equal, the two lists + /// will behave identically. + /// + /// # Calculation + /// + /// It is recommended to generate the set of thresholds in a geometric series, such that + /// there exists some constant ratio such that `threshold[k + 1] == (threshold[k] * + /// constant_ratio).max(threshold[k] + 1)` for all `k`. + /// + /// The helpers in the `voter_bags::make_bags` module can simplify this calculation. To use + /// them, the `make-bags` feature must be enabled. + /// + /// # Examples + /// + /// - If `VoterBagThresholds::get().is_empty()`, then all voters are put into the same bag, + /// and iteration is strictly in insertion order. + /// - If `VoterBagThresholds::get().len() == 64`, and the thresholds are determined + /// according to the procedure given above, then the constant ratio is equal to 2. + /// - If `VoterBagThresholds::get().len() == 200`, and the thresholds are determined + /// according to the procedure given above, then the constant ratio is approximately equal + /// to 1.248. + /// - If the threshold list begins `[1, 2, 3, ...]`, then a voter with weight 0 or 1 will + /// fall into bag 0, a voter with weight 2 will fall into bag 1, etc. + /// + /// # Migration + /// + /// In the event that this list ever changes, a copy of the old bags list must be retained. + /// With that `VoterList::migrate` can be called, which will perform the appropriate + /// migration. + #[pallet::constant] + type VoterBagThresholds: Get<&'static [VoteWeight]>; } #[pallet::extra_constants] @@ -440,6 +491,41 @@ pub mod pallet { #[pallet::storage] pub(crate) type StorageVersion = StorageValue<_, Releases, ValueQuery>; + // The next storage items collectively comprise the voter bags: a composite data structure + // designed to allow efficient iteration of the top N voters by stake, mostly. See + // `mod voter_bags` for details. + // + // In each of these items, voter bags are indexed by their upper weight threshold. + + /// How many voters are registered. + #[pallet::storage] + pub(crate) type CounterForVoters = StorageValue<_, u32, ValueQuery>; + + /// Which bag currently contains a particular voter. + /// + /// This may not be the appropriate bag for the voter's weight if they have been rewarded or + /// slashed. + #[pallet::storage] + pub(crate) type VoterBagFor = + StorageMap<_, Twox64Concat, AccountIdOf, VoteWeight>; + + /// This storage item maps a bag (identified by its upper threshold) to the `Bag` struct, which + /// mainly exists to store head and tail pointers to the appropriate nodes. + #[pallet::storage] + pub(crate) type VoterBags = + StorageMap<_, Twox64Concat, VoteWeight, voter_bags::Bag>; + + /// Voter nodes store links forward and back within their respective bags, the stash id, and + /// whether the voter is a validator or nominator. + /// + /// There is nothing in this map directly identifying to which bag a particular node belongs. + /// However, the `Node` data structure has helpers which can provide that information. + #[pallet::storage] + pub(crate) type VoterNodes = + StorageMap<_, Twox64Concat, AccountIdOf, voter_bags::Node>; + + // End of voter bags data. + /// The threshold for when users can start calling `chill_other` for other validators / nominators. /// The threshold is compared to the actual number of validators / nominators (`CountFor*`) in /// the system compared to the configured max (`Max*Count`). @@ -492,29 +578,62 @@ pub mod pallet { MinNominatorBond::::put(self.min_nominator_bond); MinValidatorBond::::put(self.min_validator_bond); + let mut num_voters: u32 = 0; for &(ref stash, ref controller, balance, ref status) in &self.stakers { + log!( + trace, + "inserting genesis staker: {:?} => {:?} => {:?}", + stash, + balance, + status + ); assert!( T::Currency::free_balance(&stash) >= balance, "Stash does not have enough balance to bond." ); - let _ = >::bond( + + if let Err(why) = >::bond( T::Origin::from(Some(stash.clone()).into()), T::Lookup::unlookup(controller.clone()), balance, RewardDestination::Staked, - ); - let _ = match status { - StakerStatus::Validator => >::validate( - T::Origin::from(Some(controller.clone()).into()), - Default::default(), - ), - StakerStatus::Nominator(votes) => >::nominate( - T::Origin::from(Some(controller.clone()).into()), - votes.iter().map(|l| T::Lookup::unlookup(l.clone())).collect(), - ), - _ => Ok(()), + ) { + // TODO: later on, fix all the tests that trigger these warnings, and + // make these assertions. Genesis stakers should all be correct! + log!(warn, "failed to bond staker at genesis: {:?}.", why); + continue + } + match status { + StakerStatus::Validator => { + if let Err(why) = >::validate( + T::Origin::from(Some(controller.clone()).into()), + Default::default(), + ) { + log!(warn, "failed to validate staker at genesis: {:?}.", why); + } else { + num_voters += 1; + } + }, + StakerStatus::Nominator(votes) => { + if let Err(why) = >::nominate( + T::Origin::from(Some(controller.clone()).into()), + votes.iter().map(|l| T::Lookup::unlookup(l.clone())).collect(), + ) { + log!(warn, "failed to nominate staker at genesis: {:?}.", why); + } else { + num_voters += 1; + } + }, + _ => (), }; } + + // all voters are inserted sanely. + assert_eq!( + CounterForVoters::::get(), + num_voters, + "not all genesis stakers were inserted into bags, something is wrong." + ); } } @@ -555,6 +674,8 @@ pub mod pallet { Chilled(T::AccountId), /// The stakers' rewards are getting paid. \[era_index, validator_stash\] PayoutStarted(EraIndex, T::AccountId), + /// Moved an account from one bag to another. \[who, from, to\]. + Rebagged(T::AccountId, VoteWeight, VoteWeight), } #[pallet::error] @@ -648,14 +769,30 @@ pub mod pallet { fn integrity_test() { sp_std::if_std! { - sp_io::TestExternalities::new_empty().execute_with(|| + sp_io::TestExternalities::new_empty().execute_with(|| { assert!( T::SlashDeferDuration::get() < T::BondingDuration::get() || T::BondingDuration::get() == 0, "As per documentation, slash defer duration ({}) should be less than bonding duration ({}).", T::SlashDeferDuration::get(), T::BondingDuration::get(), - ) - ); + ); + + assert!( + T::VoterBagThresholds::get().windows(2).all(|window| window[1] > window[0]), + "Voter bag thresholds must strictly increase", + ); + + assert!( + { + let existential_weight = voter_bags::existential_weight::(); + T::VoterBagThresholds::get() + .first() + .map(|&lowest_threshold| lowest_threshold >= existential_weight) + .unwrap_or(true) + }, + "Smallest bag should not be smaller than existential weight", + ); + }); } } } @@ -764,8 +901,9 @@ pub mod pallet { Error::::InsufficientBond ); - Self::deposit_event(Event::::Bonded(stash, extra)); + Self::deposit_event(Event::::Bonded(stash.clone(), extra)); Self::update_ledger(&controller, &ledger); + Self::do_rebag(&stash); } Ok(()) } @@ -825,6 +963,7 @@ pub mod pallet { let era = Self::current_era().unwrap_or(0) + T::BondingDuration::get(); ledger.unlocking.push(UnlockChunk { value, era }); Self::update_ledger(&controller, &ledger); + Self::do_rebag(&ledger.stash); Self::deposit_event(Event::::Unbonded(ledger.stash, value)); } Ok(()) @@ -1317,6 +1456,7 @@ pub mod pallet { Self::deposit_event(Event::::Bonded(ledger.stash.clone(), value)); Self::update_ledger(&controller, &ledger); + Self::do_rebag(&ledger.stash); Ok(Some( 35 * WEIGHT_PER_MICROS + 50 * WEIGHT_PER_NANOS * (ledger.unlocking.len() as Weight) + @@ -1537,6 +1677,20 @@ pub mod pallet { Self::chill_stash(&stash); Ok(()) } + + /// Declare that some `stash` has, through rewards or penalties, sufficiently changed its + /// stake that it should properly fall into a different bag than its current position. + /// + /// This will adjust its position into the appropriate bag. This will affect its position + /// among the nominator/validator set once the snapshot is prepared for the election. + /// + /// Anyone can call this function about any stash. + #[pallet::weight(T::WeightInfo::rebag())] + pub fn rebag(origin: OriginFor, stash: AccountIdOf) -> DispatchResult { + ensure_signed(origin)?; + Pallet::::do_rebag(&stash); + Ok(()) + } } } diff --git a/frame/staking/src/testing_utils.rs b/frame/staking/src/testing_utils.rs index 795c066d09bb3..44bd84b9a167f 100644 --- a/frame/staking/src/testing_utils.rs +++ b/frame/staking/src/testing_utils.rs @@ -27,6 +27,7 @@ use rand_chacha::{ }; use sp_io::hashing::blake2_256; +use crate::voter_bags::VoterList; use frame_support::{pallet_prelude::*, traits::Currency}; use sp_runtime::{traits::StaticLookup, Perbill}; use sp_std::prelude::*; @@ -39,6 +40,7 @@ pub fn clear_validators_and_nominators() { CounterForValidators::::kill(); Nominators::::remove_all(None); CounterForNominators::::kill(); + VoterList::::clear(); } /// Grab a funded user. diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs index 3cb7a74e8982b..9c96caadd7026 100644 --- a/frame/staking/src/tests.rs +++ b/frame/staking/src/tests.rs @@ -18,6 +18,7 @@ //! Tests for the module. use super::{Event, *}; +use crate::voter_bags::VoterList; use frame_election_provider_support::{ElectionProvider, Support}; use frame_support::{ assert_noop, assert_ok, @@ -28,6 +29,7 @@ use frame_support::{ }; use mock::*; use pallet_balances::Error as BalancesError; +use sp_npos_elections::supports_eq_unordered; use sp_runtime::{ assert_eq_error_rate, traits::{BadOrigin, Dispatchable}, @@ -176,6 +178,11 @@ fn basic_setup_works() { // New era is not being forced assert_eq!(Staking::force_era(), Forcing::NotForcing); + + // check the bags + assert_eq!(CounterForVoters::::get(), 4); + + assert_eq!(get_bags(), vec![(10, vec![31]), (1000, vec![11, 21, 101])],); }); } @@ -549,8 +556,8 @@ fn nominating_and_rewards_should_work() { total: 1000 + 800, own: 1000, others: vec![ - IndividualExposure { who: 3, value: 400 }, IndividualExposure { who: 1, value: 400 }, + IndividualExposure { who: 3, value: 400 }, ] }, ); @@ -560,8 +567,8 @@ fn nominating_and_rewards_should_work() { total: 1000 + 1200, own: 1000, others: vec![ - IndividualExposure { who: 3, value: 600 }, IndividualExposure { who: 1, value: 600 }, + IndividualExposure { who: 3, value: 600 }, ] }, ); @@ -1921,13 +1928,13 @@ fn bond_with_duplicate_vote_should_be_ignored_by_election_provider() { // winners should be 21 and 31. Otherwise this election is taking duplicates into // account. let supports = ::ElectionProvider::elect().unwrap().0; - assert_eq!( - supports, - vec![ + assert!(supports_eq_unordered( + &supports, + &vec![ (21, Support { total: 1800, voters: vec![(21, 1000), (3, 400), (1, 400)] }), (31, Support { total: 2200, voters: vec![(31, 1000), (3, 600), (1, 600)] }) ], - ); + )); }); } @@ -1967,13 +1974,13 @@ fn bond_with_duplicate_vote_should_be_ignored_by_election_provider_elected() { // winners should be 21 and 11. let supports = ::ElectionProvider::elect().unwrap().0; - assert_eq!( - supports, - vec![ + assert!(supports_eq_unordered( + &supports, + &vec![ (11, Support { total: 1500, voters: vec![(11, 1000), (1, 500)] }), (21, Support { total: 2500, voters: vec![(21, 1000), (3, 1000), (1, 500)] }) ], - ); + )); }); } @@ -3826,6 +3833,95 @@ fn on_finalize_weight_is_nonzero() { }) } +// end-to-end nodes of the voter bags operation. +mod voter_bags { + use super::Origin; + use crate::{mock::*, ValidatorPrefs}; + use frame_support::{assert_ok, traits::Currency}; + + #[test] + fn insert_and_remove_works() { + // we test insert/remove indirectly via `validate`, `nominate`, and chill + ExtBuilder::default().build_and_execute(|| { + // given + assert_eq!(get_bags(), vec![(10, vec![31]), (1000, vec![11, 21, 101])]); + + // `bond` + bond(42, 43, 2_000); + // does not insert the voter + assert_eq!(get_bags(), vec![(10, vec![31]), (1000, vec![11, 21, 101])]); + + // `validate` + assert_ok!(Staking::validate(Origin::signed(43).into(), ValidatorPrefs::default())); + // moves the voter into a bag + assert_eq!( + get_bags(), + vec![(10, vec![31]), (1000, vec![11, 21, 101]), (2000, vec![42])] + ); + + // `nominate`-ing, but not changing active stake (which implicitly calls remove) + assert_ok!(Staking::nominate(Origin::signed(43), vec![11])); + // does not change the voters position + assert_eq!( + get_bags(), + vec![(10, vec![31]), (1000, vec![11, 21, 101]), (2000, vec![42])] + ); + + // `chill` + assert_ok!(Staking::chill(Origin::signed(43))); + // removes the voter + assert_eq!(get_bags(), vec![(10, vec![31]), (1000, vec![11, 21, 101])]); + }); + } + + #[test] + fn rebag_works() { + ExtBuilder::default().build_and_execute(|| { + // add a nominator to genesis state + bond_nominator(42, 43, 20, vec![11]); + Balances::make_free_balance_be(&42, 2_000); + + // given + assert_eq!(get_bags(), vec![(10, vec![31]), (20, vec![42]), (1000, vec![11, 21, 101])]); + + // increase stake and implicitly rebag with `bond_extra` to the level of non-existent bag + assert_ok!(Staking::bond_extra(Origin::signed(42), 1_980)); // 20 + 1_980 = 2_000 + assert_eq!( + get_bags(), + vec![(10, vec![31]), (1000, vec![11, 21, 101]), (2000, vec![42])] + ); + + // decrease stake within the range of the current bag + assert_ok!(Staking::unbond(Origin::signed(43), 999)); // 2000 - 999 = 1001 + // does not change bags + assert_eq!( + get_bags(), + vec![(10, vec![31]), (1000, vec![11, 21, 101]), (2000, vec![42])] + ); + + // reduce stake to the level of a non-existent bag + assert_ok!(Staking::unbond(Origin::signed(43), 971)); // 1001 - 971 = 30 + // creates the bag and moves the voter into it + assert_eq!( + get_bags(), + vec![(10, vec![31]), (30, vec![42]), (1000, vec![11, 21, 101]),] + ); + + // increase stake by `rebond`-ing to the level of a pre-existing bag + assert_ok!(Staking::rebond(Origin::signed(43), 31)); // 30 + 41 = 61 + // moves the voter to that bag + assert_eq!(get_bags(), vec![(10, vec![31]), (1000, vec![11, 21, 101, 42]),]); + + // TODO test rebag directly + }); + } + + // #[test] TODO + // fn rebag_head_works() { + // // rebagging the head of a bag results in the old bag having a new head and an overall correct state. + // } +} + mod election_data_provider { use super::*; use frame_election_provider_support::ElectionDataProvider; @@ -3922,9 +4018,21 @@ mod election_data_provider { } #[test] - fn respects_len_limits() { - ExtBuilder::default().build_and_execute(|| { - assert_eq!(Staking::voters(Some(1)).unwrap_err(), "Voter snapshot too big"); + fn respects_snapshot_len_limits() { + ExtBuilder::default().validator_pool(true).build_and_execute(|| { + // sum of all validators and nominators who'd be voters. + assert_eq!(VoterList::::decode_len().unwrap(), 5); + + // if limits is less.. + assert_eq!(Staking::voters(Some(1)).unwrap().0.len(), 1); + + // if limit is equal.. + assert_eq!(Staking::voters(Some(5)).unwrap().0.len(), 5); + + // if limit is more. + assert_eq!(Staking::voters(Some(55)).unwrap().0.len(), 5); + + // if target limit is less, then we return an error. assert_eq!(Staking::targets(Some(1)).unwrap_err(), "Target snapshot too big"); }); } @@ -3984,12 +4092,22 @@ mod election_data_provider { #[test] #[should_panic] - fn count_check_works() { + fn count_check_prevents_validator_insert() { ExtBuilder::default().build_and_execute(|| { // We should never insert into the validators or nominators map directly as this will // not keep track of the count. This test should panic as we verify the count is accurate - // after every test using the `post_checks` in `mock`. + // after every test using the `post_conditions` in `mock`. Validators::::insert(987654321, ValidatorPrefs::default()); + }) + } + + #[test] + #[should_panic] + fn count_check_prevents_nominator_insert() { + ExtBuilder::default().build_and_execute(|| { + // We should never insert into the validators or nominators map directly as this will + // not keep track of the count. This test should panic as we verify the count is accurate + // after every test using the `post_conditions` in `mock`. Nominators::::insert( 987654321, Nominations { diff --git a/frame/staking/src/voter_bags.rs b/frame/staking/src/voter_bags.rs new file mode 100644 index 0000000000000..45e94db1be83b --- /dev/null +++ b/frame/staking/src/voter_bags.rs @@ -0,0 +1,1728 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 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. + +//! Implement a data structure designed for the properties that: +//! +//! - It's efficient to insert or remove a voter +//! - It's efficient to iterate over the top* N voters by stake, where the precise ordering of +//! voters doesn't particularly matter. + +use codec::{Decode, Encode}; +use frame_support::{ensure, traits::Get, DefaultNoBound}; +use sp_runtime::SaturatedConversion; +use sp_std::{ + boxed::Box, + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + iter, + marker::PhantomData, +}; + +use crate::{ + slashing::SlashingSpans, AccountIdOf, Config, Nominations, Nominators, Pallet, Validators, + VoteWeight, VoterBagFor, VotingDataOf, +}; + +/// [`Voter`] parametrized by [`Config`] instead of by `AccountId`. +pub type VoterOf = Voter>; + +/// Given a certain vote weight, which bag should contain this voter? +/// +/// Bags are identified by their upper threshold; the value returned by this function is guaranteed +/// to be a member of `T::VoterBagThresholds`. +/// +/// This is used instead of a simpler scheme, such as the index within `T::VoterBagThresholds`, +/// because in the event that bags are inserted or deleted, the number of affected voters which need +/// to be migrated is smaller. +/// +/// Note that even if the thresholds list does not have `VoteWeight::MAX` as its final member, this +/// function behaves as if it does. +fn notional_bag_for(weight: VoteWeight) -> VoteWeight { + let thresholds = T::VoterBagThresholds::get(); + let idx = thresholds.partition_point(|&threshold| weight > threshold); + thresholds.get(idx).copied().unwrap_or(VoteWeight::MAX) +} + +/// Find the upper threshold of the actual bag containing the current voter. +fn current_bag_for(id: &AccountIdOf) -> Option { + VoterBagFor::::try_get(id).ok() +} + +/// Data structure providing efficient mostly-accurate selection of the top N voters by stake. +/// +/// It's implemented as a set of linked lists. Each linked list comprises a bag of voters of +/// arbitrary and unbounded length, all having a vote weight within a particular constant range. +/// This structure means that voters can be added and removed in `O(1)` time. +/// +/// Iteration is accomplished by chaining the iteration of each bag, from greatest to least. +/// While the users within any particular bag are sorted in an entirely arbitrary order, the overall +/// stake decreases as successive bags are reached. This means that it is valid to truncate +/// iteration at any desired point; only those voters in the lowest bag (who are known to have +/// relatively little power to affect the outcome) can be excluded. This satisfies both the desire +/// for fairness and the requirement for efficiency. +pub struct VoterList(PhantomData); + +impl VoterList { + /// Remove all data associated with the voter list from storage. + pub fn clear() { + crate::CounterForVoters::::kill(); + crate::VoterBagFor::::remove_all(None); + crate::VoterBags::::remove_all(None); + crate::VoterNodes::::remove_all(None); + } + + /// Regenerate voter data from the `Nominators` and `Validators` storage items. + /// + /// This is expensive and should only ever be performed during a migration, never during + /// consensus. + /// + /// Returns the number of voters migrated. + pub fn regenerate() -> u32 { + Self::clear(); + + let nominators_iter = Nominators::::iter().map(|(id, _)| Voter::nominator(id)); + let validators_iter = Validators::::iter().map(|(id, _)| Voter::validator(id)); + let weight_of = Pallet::::weight_of_fn(); + + Self::insert_many(nominators_iter.chain(validators_iter), weight_of) + } + + /// Decode the length of the voter list. + pub fn decode_len() -> Option { + let maybe_len = crate::CounterForVoters::::try_get().ok().map(|n| n.saturated_into()); + debug_assert_eq!( + maybe_len.unwrap_or_default(), + crate::VoterNodes::::iter().count(), + "stored length must match count of nodes", + ); + debug_assert_eq!( + // TODO: this case will fail in migration pre check + maybe_len.unwrap_or_default() as u32, + crate::CounterForNominators::::get() + crate::CounterForValidators::::get(), + "voter count must be sum of validator and nominator count", + ); + maybe_len + } + + /// Iterate over all nodes in all bags in the voter list. + /// + /// Full iteration can be expensive; it's recommended to limit the number of items with + /// `.take(n)`. + pub fn iter() -> impl Iterator> { + // We need a touch of special handling here: because we permit `T::VoterBagThresholds` to + // omit the final bound, we need to ensure that we explicitly include that threshold in the + // list. + // + // It's important to retain the ability to omit the final bound because it makes tests much + // easier; they can just configure `type VoterBagThresholds = ()`. + let thresholds = T::VoterBagThresholds::get(); + let iter = thresholds.iter().copied(); + let iter: Box> = if thresholds.last() == Some(&VoteWeight::MAX) { + // in the event that they included it, we can just pass the iterator through unchanged. + Box::new(iter.rev()) + } else { + // otherwise, insert it here. + Box::new(iter.chain(iter::once(VoteWeight::MAX)).rev()) + }; + iter.filter_map(Bag::get).flat_map(|bag| bag.iter()) + } + + /// Insert a new voter into the appropriate bag in the voter list. + /// + /// If the voter is already present in the list, their type will be updated. + /// That case is cheaper than inserting a new voter. + pub fn insert_as(account_id: &AccountIdOf, voter_type: VoterType) { + // if this is an update operation we can complete this easily and cheaply + if !Node::::update_voter_type_for(account_id, voter_type) { + // otherwise, we need to insert from scratch + let weight_of = Pallet::::weight_of_fn(); + let voter = Voter { id: account_id.clone(), voter_type }; + Self::insert(voter, weight_of); + } + } + + /// Insert a new voter into the appropriate bag in the voter list. + fn insert(voter: VoterOf, weight_of: impl Fn(&T::AccountId) -> VoteWeight) { + Self::insert_many(sp_std::iter::once(voter), weight_of); + } + + /// Insert several voters into the appropriate bags in the voter list. + /// + /// This is more efficient than repeated calls to `Self::insert`. + fn insert_many( + voters: impl IntoIterator>, + weight_of: impl Fn(&T::AccountId) -> VoteWeight, + ) -> u32 { + let mut bags = BTreeMap::new(); + let mut count = 0; + + for voter in voters.into_iter() { + let weight = weight_of(&voter.id); + let bag = notional_bag_for::(weight); + crate::log!(debug, "inserting {:?} with weight {} into bag {:?}", voter, weight, bag); + bags.entry(bag).or_insert_with(|| Bag::::get_or_make(bag)).insert(voter); + count += 1; + } + + for (_, bag) in bags { + bag.put(); + } + + crate::CounterForVoters::::mutate(|prev_count| { + *prev_count = prev_count.saturating_add(count) + }); + count + } + + /// Remove a voter (by id) from the voter list. + pub fn remove(voter: &AccountIdOf) { + Self::remove_many(sp_std::iter::once(voter)); + } + + /// Remove many voters (by id) from the voter list. + /// + /// This is more efficient than repeated calls to `Self::remove`. + pub fn remove_many<'a>(voters: impl IntoIterator>) { + let mut bags = BTreeMap::new(); + let mut count = 0; + + for voter_id in voters.into_iter() { + let node = match Node::::from_id(voter_id) { + Some(node) => node, + None => continue, + }; + count += 1; + + // clear the bag head/tail pointers as necessary + let bag = bags + .entry(node.bag_upper) + .or_insert_with(|| Bag::::get_or_make(node.bag_upper)); + bag.remove_node(&node); + + // now get rid of the node itself + crate::VoterNodes::::remove(voter_id); + crate::VoterBagFor::::remove(voter_id); + } + + for (_, bag) in bags { + bag.put(); + } + + crate::CounterForVoters::::mutate(|prev_count| { + *prev_count = prev_count.saturating_sub(count) + }); + } + + /// Update a voter's position in the voter list. + /// + /// If the voter was in the correct bag, no effect. If the voter was in the incorrect bag, they + /// are moved into the correct bag. + /// + /// Returns `Some((old_idx, new_idx))` if the voter moved, otherwise `None`. + /// + /// This operation is somewhat more efficient than simply calling [`self.remove`] followed by + /// [`self.insert`]. However, given large quantities of voters to move, it may be more efficient + /// to call [`self.remove_many`] followed by [`self.insert_many`]. + pub fn update_position_for( + mut node: Node, + weight_of: impl Fn(&AccountIdOf) -> VoteWeight, + ) -> Option<(VoteWeight, VoteWeight)> { + node.is_misplaced(&weight_of).then(move || { + let old_idx = node.bag_upper; + + // TODO: there should be a way to move a non-head-tail node to another bag + // with just 1 bag read of the destination bag and zero writes + // https://github.com/paritytech/substrate/pull/9468/files/83289aa4a15d61e6cb334f9d7e7f6804cb7e3537..44875c511ebdc79270100720320c8e3d2d56eb4a#r680559166 + + // clear the old bag head/tail pointers as necessary + if let Some(mut bag) = Bag::::get(node.bag_upper) { + bag.remove_node(&node); + bag.put(); + } else { + debug_assert!(false, "every node must have an extant bag associated with it"); + crate::log!( + error, + "Node for staker {:?} did not have a bag; VoterBags is in an inconsistent state", + node.voter.id, + ); + } + + // put the voter into the appropriate new bag + let new_idx = notional_bag_for::(weight_of(&node.voter.id)); + node.bag_upper = new_idx; + let mut bag = Bag::::get_or_make(node.bag_upper); + bag.insert_node(node); + bag.put(); + + (old_idx, new_idx) + }) + } + + /// Migrate the voter list from one set of thresholds to another. + /// + /// This should only be called as part of an intentional migration; it's fairly expensive. + /// + /// Returns the number of accounts affected. + /// + /// Preconditions: + /// + /// - `old_thresholds` is the previous list of thresholds. + /// - All `bag_upper` currently in storage are members of `old_thresholds`. + /// - `T::VoterBagThresholds` has already been updated. + /// + /// Postconditions: + /// + /// - All `bag_upper` currently in storage are members of `T::VoterBagThresholds`. + /// - No voter is changed unless required to by the difference between the old threshold list + /// and the new. + /// - Voters whose bags change at all are implicitly rebagged into the appropriate bag in the + /// new threshold set. + pub fn migrate(old_thresholds: &[VoteWeight]) -> u32 { + // we can't check all preconditions, but we can check one + debug_assert!( + crate::VoterBags::::iter().all(|(threshold, _)| old_thresholds.contains(&threshold)), + "not all `bag_upper` currently in storage are members of `old_thresholds`", + ); + + let old_set: BTreeSet<_> = old_thresholds.iter().copied().collect(); + let new_set: BTreeSet<_> = T::VoterBagThresholds::get().iter().copied().collect(); + + let mut affected_accounts = BTreeSet::new(); + let mut affected_old_bags = BTreeSet::new(); + + // a new bag means that all accounts previously using the old bag's threshold must now + // be rebagged + for inserted_bag in new_set.difference(&old_set).copied() { + let affected_bag = notional_bag_for::(inserted_bag); + if !affected_old_bags.insert(affected_bag) { + // If the previous threshold list was [10, 20], and we insert [3, 5], then there's + // no point iterating through bag 10 twice. + continue + } + + if let Some(bag) = Bag::::get(affected_bag) { + affected_accounts.extend(bag.iter().map(|node| node.voter)); + } + } + + // a removed bag means that all members of that bag must be rebagged + for removed_bag in old_set.difference(&new_set).copied() { + if !affected_old_bags.insert(removed_bag) { + continue + } + + if let Some(bag) = Bag::::get(removed_bag) { + affected_accounts.extend(bag.iter().map(|node| node.voter)); + } + } + + // migrate the + let weight_of = Pallet::::weight_of_fn(); + Self::remove_many(affected_accounts.iter().map(|voter| &voter.id)); + let num_affected = Self::insert_many(affected_accounts.into_iter(), weight_of); + + // we couldn't previously remove the old bags because both insertion and removal assume that + // it's always safe to add a bag if it's not present. Now that that's sorted, we can get rid + // of them. + // + // it's pretty cheap to iterate this again, because both sets are in-memory and require no + // lookups. + for removed_bag in old_set.difference(&new_set).copied() { + debug_assert!( + !VoterBagFor::::iter().any(|(_voter, bag)| bag == removed_bag), + "no voter should be present in a removed bag", + ); + crate::VoterBags::::remove(removed_bag); + } + + debug_assert!( + { + let thresholds = T::VoterBagThresholds::get(); + crate::VoterBags::::iter().all(|(threshold, _)| thresholds.contains(&threshold)) + }, + "all `bag_upper` in storage must be members of the new thresholds", + ); + + num_affected + } + + /// Sanity check the voter list. + /// + /// This should be called from the call-site, whenever one of the mutating apis (e.g. `insert`) + /// is being used, after all other staking data (such as counter) has been updated. It checks + /// that: + /// + /// * Iterate all voters in list and make sure there are no duplicates. + /// * Iterate all voters and ensure their count is in sync with `CounterForVoters`. + /// * Ensure `CounterForVoters` is `CounterForValidators + CounterForNominators`. + /// * Sanity-checks all bags. This will cascade down all the checks and makes sure all bags are + /// checked per *any* update to `VoterList`. + pub(super) fn sanity_check() -> Result<(), &'static str> { + let mut seen_in_list = BTreeSet::new(); + ensure!( + Self::iter().map(|node| node.voter.id).all(|voter| seen_in_list.insert(voter)), + "duplicate identified", + ); + + let iter_count = Self::iter().collect::>().len() as u32; + let stored_count = crate::CounterForVoters::::get(); + ensure!(iter_count == stored_count, "iter_count != voter_count"); + + let validators = crate::CounterForValidators::::get(); + let nominators = crate::CounterForNominators::::get(); + ensure!(validators + nominators == stored_count, "validators + nominators != voters"); + + let _ = T::VoterBagThresholds::get() + .into_iter() + .map(|t| Bag::::get(*t).unwrap_or_default()) + .map(|b| b.sanity_check()) + .collect::>()?; + + Ok(()) + } +} + +/// A Bag is a doubly-linked list of voters. +/// +/// Note that we maintain both head and tail pointers. While it would be possible to get away +/// with maintaining only a head pointer and cons-ing elements onto the front of the list, it's +/// more desirable to ensure that there is some element of first-come, first-serve to the list's +/// iteration so that there's no incentive to churn voter positioning to improve the chances of +/// appearing within the voter set. +#[derive(DefaultNoBound, Encode, Decode)] +#[cfg_attr(feature = "std", derive(frame_support::DebugNoBound))] +#[cfg_attr(test, derive(PartialEq))] +pub struct Bag { + head: Option>, + tail: Option>, + + #[codec(skip)] + bag_upper: VoteWeight, +} + +impl Bag { + /// Get a bag by its upper vote weight. + pub fn get(bag_upper: VoteWeight) -> Option> { + debug_assert!( + T::VoterBagThresholds::get().contains(&bag_upper) || bag_upper == VoteWeight::MAX, + "it is a logic error to attempt to get a bag which is not in the thresholds list" + ); + crate::VoterBags::::try_get(bag_upper).ok().map(|mut bag| { + bag.bag_upper = bag_upper; + bag + }) + } + + /// Get a bag by its upper vote weight or make it, appropriately initialized. + pub fn get_or_make(bag_upper: VoteWeight) -> Bag { + debug_assert!( + T::VoterBagThresholds::get().contains(&bag_upper) || bag_upper == VoteWeight::MAX, + "it is a logic error to attempt to get a bag which is not in the thresholds list" + ); + Self::get(bag_upper).unwrap_or(Bag { bag_upper, ..Default::default() }) + } + + /// `True` if self is empty. + pub fn is_empty(&self) -> bool { + self.head.is_none() && self.tail.is_none() + } + + /// Put the bag back into storage. + pub fn put(self) { + if self.is_empty() { + crate::VoterBags::::remove(self.bag_upper); + } else { + crate::VoterBags::::insert(self.bag_upper, self); + } + } + + /// Get the head node in this bag. + pub fn head(&self) -> Option> { + self.head.as_ref().and_then(|id| Node::get(self.bag_upper, id)) + } + + /// Get the tail node in this bag. + pub fn tail(&self) -> Option> { + self.tail.as_ref().and_then(|id| Node::get(self.bag_upper, id)) + } + + /// Iterate over the nodes in this bag. + pub fn iter(&self) -> impl Iterator> { + sp_std::iter::successors(self.head(), |prev| prev.next()) + } + + /// Insert a new voter into this bag. + /// + /// This is private on purpose because it's naive: it doesn't check whether this is the + /// appropriate bag for this voter at all. Generally, use [`VoterList::insert`] instead. + /// + /// Storage note: this modifies storage, but only for the nodes. You still need to call + /// `self.put()` after use. + fn insert(&mut self, voter: VoterOf) { + self.insert_node(Node:: { voter, prev: None, next: None, bag_upper: self.bag_upper }); + } + + /// Insert a voter node into this bag. + /// + /// This is private on purpose because it's naive; it doesn't check whether this is the + /// appropriate bag for this voter at all. Generally, use [`VoterList::insert`] instead. + /// + /// Storage note: this modifies storage, but only for the node. You still need to call + /// `self.put()` after use. + fn insert_node(&mut self, mut node: Node) { + if let Some(tail) = &self.tail { + if *tail == node.voter.id { + // this should never happen, but this check prevents a worst case infinite loop + debug_assert!(false, "system logic error: inserting a node who has the id of tail"); + crate::log!(warn, "system logic error: inserting a node who has the id of tail"); + return + }; + } + + let id = node.voter.id.clone(); + + node.prev = self.tail.clone(); + node.next = None; + node.put(); + + // update the previous tail + if let Some(mut old_tail) = self.tail() { + old_tail.next = Some(id.clone()); + old_tail.put(); + } + + // update the internal bag links + if self.head.is_none() { + self.head = Some(id.clone()); + } + self.tail = Some(id.clone()); + + crate::VoterBagFor::::insert(id, self.bag_upper); + } + + /// Remove a voter node from this bag. + /// + /// This is private on purpose because it doesn't check whether this bag contains the voter in + /// the first place. Generally, use [`VoterList::remove`] instead. + /// + /// Storage note: this modifies storage, but only for adjacent nodes. You still need to call + /// `self.put()`, `VoterNodes::remove(voter_id)` and `VoterBagFor::remove(voter_id)` + /// to update storage for the bag and `node`. + fn remove_node(&mut self, node: &Node) { + // Update previous node. + if let Some(mut prev) = node.prev() { + prev.next = node.next.clone(); + prev.put(); + } + // Update next node. + if let Some(mut next) = node.next() { + next.prev = node.prev.clone(); + next.put(); + } + + // clear the bag head/tail pointers as necessary + if self.head.as_ref() == Some(&node.voter.id) { + self.head = node.next.clone(); + } + if self.tail.as_ref() == Some(&node.voter.id) { + self.tail = node.prev.clone(); + } + } + + /// Sanity check this bag. + /// + /// Should be called by the call-site, after each mutating operation on a bag. The call site of + /// this struct is always `VoterList`. + /// + /// * Ensures head has no prev. + /// * Ensures tail has no next. + /// * Ensures there are no loops, traversal from head to tail is correct. + fn sanity_check(&self) -> Result<(), &'static str> { + ensure!( + self.head() + .map(|head| head.prev().is_none()) + // if there is no head, then there must not be a tail, meaning that the bag is + // empty. + .unwrap_or_else(|| self.tail.is_none()), + "head has a prev" + ); + + ensure!( + self.tail() + .map(|tail| tail.next().is_none()) + // if there is no tail, then there must not be a head, meaning that the bag is + // empty. + .unwrap_or_else(|| self.head.is_none()), + "tail has a next" + ); + + let mut seen_in_bag = BTreeSet::new(); + ensure!( + self.iter() + .map(|node| node.voter.id) + // each voter is only seen once, thus there is no cycle within a bag + .all(|voter| seen_in_bag.insert(voter)), + "Duplicate found in bag" + ); + + Ok(()) + } +} + +/// A Node is the fundamental element comprising the doubly-linked lists which for each bag. +#[derive(Encode, Decode)] +#[cfg_attr(feature = "std", derive(frame_support::DebugNoBound))] +#[cfg_attr(test, derive(PartialEq, Clone))] +pub struct Node { + voter: Voter>, + prev: Option>, + next: Option>, + + /// The bag index is not stored in storage, but injected during all fetch operations. + #[codec(skip)] + pub(crate) bag_upper: VoteWeight, +} + +impl Node { + /// Get a node by bag idx and account id. + pub fn get(bag_upper: VoteWeight, account_id: &AccountIdOf) -> Option> { + debug_assert!( + T::VoterBagThresholds::get().contains(&bag_upper) || bag_upper == VoteWeight::MAX, + "it is a logic error to attempt to get a bag which is not in the thresholds list" + ); + crate::VoterNodes::::try_get(account_id).ok().map(|mut node| { + node.bag_upper = bag_upper; + node + }) + } + + /// Get a node by account id. + /// + /// Note that this must perform two storage lookups: one to identify which bag is appropriate, + /// and another to actually fetch the node. + pub fn from_id(account_id: &AccountIdOf) -> Option> { + let bag = current_bag_for::(account_id)?; + Self::get(bag, account_id) + } + + /// Get a node by account id, assuming it's in the same bag as this node. + pub fn in_bag(&self, account_id: &AccountIdOf) -> Option> { + Self::get(self.bag_upper, account_id) + } + + /// Put the node back into storage. + pub fn put(self) { + crate::VoterNodes::::insert(self.voter.id.clone(), self); + } + + /// Get the previous node in the bag. + pub fn prev(&self) -> Option> { + self.prev.as_ref().and_then(|id| self.in_bag(id)) + } + + /// Get the next node in the bag. + pub fn next(&self) -> Option> { + self.next.as_ref().and_then(|id| self.in_bag(id)) + } + + /// Get this voter's voting data. + pub fn voting_data( + &self, + weight_of: impl Fn(&T::AccountId) -> VoteWeight, + slashing_spans: &BTreeMap, SlashingSpans>, + ) -> Option> { + let voter_weight = weight_of(&self.voter.id); + match self.voter.voter_type { + VoterType::Validator => + Some((self.voter.id.clone(), voter_weight, sp_std::vec![self.voter.id.clone()])), + VoterType::Nominator => { + let Nominations { submitted_in, mut targets, .. } = + Nominators::::get(&self.voter.id)?; + // Filter out nomination targets which were nominated before the most recent + // slashing span. + targets.retain(|stash| { + slashing_spans + .get(stash) + .map_or(true, |spans| submitted_in >= spans.last_nonzero_slash()) + }); + + (!targets.is_empty()).then(move || (self.voter.id.clone(), voter_weight, targets)) + }, + } + } + + /// `true` when this voter is in the wrong bag. + pub fn is_misplaced(&self, weight_of: impl Fn(&T::AccountId) -> VoteWeight) -> bool { + notional_bag_for::(weight_of(&self.voter.id)) != self.bag_upper + } + + /// Update the voter type associated with a particular node by id. + /// + /// This updates storage immediately. + /// + /// Returns whether the voter existed and was successfully updated. + pub fn update_voter_type_for(account_id: &AccountIdOf, voter_type: VoterType) -> bool { + let node = Self::from_id(account_id); + let existed = node.is_some(); + if let Some(mut node) = node { + node.voter.voter_type = voter_type; + node.put(); + } + existed + } + + /// Get the upper threshold of the bag that this node _should_ be in, given its vote weight. + /// + /// This is a helper intended only for benchmarking and should not be used in production. + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub fn proper_bag_for(&self) -> VoteWeight { + let weight_of = crate::Pallet::::weight_of_fn(); + let current_weight = weight_of(&self.voter.id); + notional_bag_for::(current_weight) + } + + /// Get the underlying voter. + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub fn voter(&self) -> &Voter { + &self.voter + } +} + +/// Fundamental information about a voter. +#[derive(Clone, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, sp_runtime::RuntimeDebug)] +pub struct Voter { + /// Account Id of this voter + pub id: AccountId, + /// Whether the voter is a validator or nominator + pub voter_type: VoterType, +} + +impl Voter { + pub fn nominator(id: AccountId) -> Self { + Self { id, voter_type: VoterType::Nominator } + } + + pub fn validator(id: AccountId) -> Self { + Self { id, voter_type: VoterType::Validator } + } +} + +/// Type of voter. +/// +/// Similar to [`crate::StakerStatus`], but somewhat more limited. +#[derive(Clone, Copy, Encode, Decode, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "std", derive(Debug))] +pub enum VoterType { + Validator, + Nominator, +} + +/// Compute the existential weight for the specified configuration. +/// +/// Note that this value depends on the current issuance, a quantity known to change over time. +/// This makes the project of computing a static value suitable for inclusion in a static, +/// generated file _excitingly unstable_. +#[cfg(any(feature = "std", feature = "make-bags"))] +pub fn existential_weight() -> VoteWeight { + use frame_support::traits::{Currency, CurrencyToVote}; + + let existential_deposit = >>::minimum_balance(); + let issuance = >>::total_issuance(); + T::CurrencyToVote::to_vote(existential_deposit, issuance) +} + +/// Support code to ease the process of generating voter bags. +/// +/// The process of adding voter bags to a runtime requires only four steps. +/// +/// 1. Update the runtime definition. +/// +/// ```ignore +/// parameter_types!{ +/// pub const VoterBagThresholds: &'static [u64] = &[]; +/// } +/// +/// impl pallet_staking::Config for Runtime { +/// // +/// type VoterBagThresholds = VoterBagThresholds; +/// } +/// ``` +/// +/// 2. Write a little program to generate the definitions. This can be a near-identical copy of +/// `substrate/node/runtime/voter-bags`. This program exists only to hook together the runtime +/// definitions with the various calculations here. +/// +/// 3. Run that program: +/// +/// ```sh,notrust +/// $ cargo run -p node-runtime-voter-bags -- bin/node/runtime/src/voter_bags.rs +/// ``` +/// +/// 4. Update the runtime definition. +/// +/// ```diff,notrust +/// + mod voter_bags; +/// - pub const VoterBagThresholds: &'static [u64] = &[]; +/// + pub const VoterBagThresholds: &'static [u64] = &voter_bags::THRESHOLDS; +/// ``` +#[cfg(feature = "make-bags")] +pub mod make_bags { + use crate::{voter_bags::existential_weight, Config}; + use frame_election_provider_support::VoteWeight; + use frame_support::traits::Get; + use std::{ + io::Write, + path::{Path, PathBuf}, + }; + + /// Return the path to a header file used in this repository if is exists. + /// + /// Just searches the git working directory root for files matching certain patterns; it's + /// pretty naive. + fn path_to_header_file() -> Option { + let repo = git2::Repository::open_from_env().ok()?; + let workdir = repo.workdir()?; + for file_name in &["HEADER-APACHE2", "HEADER-GPL3", "HEADER", "file_header.txt"] { + let path = workdir.join(file_name); + if path.exists() { + return Some(path) + } + } + None + } + + /// Create an underscore formatter: a formatter which inserts `_` every 3 digits of a number. + fn underscore_formatter() -> num_format::CustomFormat { + num_format::CustomFormat::builder() + .grouping(num_format::Grouping::Standard) + .separator("_") + .build() + .expect("format described here meets all constraints") + } + + /// Compute the constant ratio for the thresholds. + /// + /// This ratio ensures that each bag, with the possible exceptions of certain small ones and the + /// final one, is a constant multiple of the previous, while fully occupying the `VoteWeight` + /// space. + pub fn constant_ratio(existential_weight: VoteWeight, n_bags: usize) -> f64 { + ((VoteWeight::MAX as f64 / existential_weight as f64).ln() / ((n_bags - 1) as f64)).exp() + } + + /// Compute the list of bag thresholds. + /// + /// Returns a list of exactly `n_bags` elements, except in the case of overflow. + /// The first element is always `existential_weight`. + /// The last element is always `VoteWeight::MAX`. + /// + /// All other elements are computed from the previous according to the formula + /// `threshold[k + 1] = (threshold[k] * ratio).max(threshold[k] + 1); + pub fn thresholds( + existential_weight: VoteWeight, + constant_ratio: f64, + n_bags: usize, + ) -> Vec { + const WEIGHT_LIMIT: f64 = VoteWeight::MAX as f64; + + let mut thresholds = Vec::with_capacity(n_bags); + + if n_bags > 1 { + thresholds.push(existential_weight); + } + + while n_bags > 0 && thresholds.len() < n_bags - 1 { + let last = thresholds.last().copied().unwrap_or(existential_weight); + let successor = (last as f64 * constant_ratio).round().max(last as f64 + 1.0); + if successor < WEIGHT_LIMIT { + thresholds.push(successor as VoteWeight); + } else { + eprintln!("unexpectedly exceeded weight limit; breaking threshold generation loop"); + break + } + } + + thresholds.push(VoteWeight::MAX); + + debug_assert_eq!(thresholds.len(), n_bags); + debug_assert!(n_bags == 0 || thresholds[0] == existential_weight); + debug_assert!(n_bags == 0 || thresholds[thresholds.len() - 1] == VoteWeight::MAX); + + thresholds + } + + /// Write a thresholds module to the path specified. + /// + /// The `output` path should terminate with a Rust module name, i.e. `foo/bar/thresholds.rs`. + /// + /// This generated module contains, in order: + /// + /// - The contents of the header file in this repository's root, if found. + /// - Module documentation noting that this is autogenerated and when. + /// - Some associated constants. + /// - The constant array of thresholds. + pub fn generate_thresholds_module( + n_bags: usize, + output: &Path, + ) -> Result<(), std::io::Error> { + // ensure the file is accessable + if let Some(parent) = output.parent() { + if !parent.exists() { + std::fs::create_dir_all(parent)?; + } + } + + // copy the header file + if let Some(header_path) = path_to_header_file() { + std::fs::copy(header_path, output)?; + } + + // open an append buffer + let file = std::fs::OpenOptions::new().create(true).append(true).open(output)?; + let mut buf = std::io::BufWriter::new(file); + + // create underscore formatter and format buffer + let mut num_buf = num_format::Buffer::new(); + let format = underscore_formatter(); + + // module docs + let now = chrono::Utc::now(); + writeln!(buf)?; + writeln!(buf, "//! Autogenerated voter bag thresholds.")?; + writeln!(buf, "//!")?; + writeln!(buf, "//! Generated on {}", now.to_rfc3339())?; + writeln!( + buf, + "//! for the {} runtime.", + ::Version::get().spec_name, + )?; + + // existential weight + let existential_weight = existential_weight::(); + num_buf.write_formatted(&existential_weight, &format); + writeln!(buf)?; + writeln!(buf, "/// Existential weight for this runtime.")?; + writeln!(buf, "#[cfg(any(test, feature = \"std\"))]")?; + writeln!(buf, "#[allow(unused)]")?; + writeln!(buf, "pub const EXISTENTIAL_WEIGHT: u64 = {};", num_buf.as_str())?; + + // constant ratio + let constant_ratio = constant_ratio(existential_weight, n_bags); + writeln!(buf)?; + writeln!(buf, "/// Constant ratio between bags for this runtime.")?; + writeln!(buf, "#[cfg(any(test, feature = \"std\"))]")?; + writeln!(buf, "#[allow(unused)]")?; + writeln!(buf, "pub const CONSTANT_RATIO: f64 = {:.16};", constant_ratio)?; + + // thresholds + let thresholds = thresholds(existential_weight, constant_ratio, n_bags); + writeln!(buf)?; + writeln!(buf, "/// Upper thresholds delimiting the bag list.")?; + writeln!(buf, "pub const THRESHOLDS: [u64; {}] = [", thresholds.len())?; + for threshold in thresholds { + num_buf.write_formatted(&threshold, &format); + // u64::MAX, with spacers every 3 digits, is 26 characters wide + writeln!(buf, " {:>26},", num_buf.as_str())?; + } + writeln!(buf, "];")?; + + Ok(()) + } +} + +// This is the highest level of abstraction provided by this module. More generic tests are here, +// among those related to `VoterList` struct. +#[cfg(test)] +mod voter_list { + use super::*; + use crate::mock::*; + use frame_support::{assert_ok, assert_storage_noop, traits::Currency}; + + #[test] + fn basic_setup_works() { + use crate::{ + CounterForNominators, CounterForValidators, CounterForVoters, VoterBags, VoterNodes, + }; + let node = |voter, prev, next| Node:: { voter, prev, next, bag_upper: 0 }; + + // make sure ALL relevant data structures are setup correctly. + ExtBuilder::default().build_and_execute(|| { + assert_eq!(CounterForVoters::::get(), 4); + assert_eq!(VoterBagFor::::iter().count(), 4); + assert_eq!(VoterNodes::::iter().count(), 4); + assert_eq!(VoterBags::::iter().count(), 2); + assert_eq!(CounterForValidators::::get(), 3); + assert_eq!(CounterForNominators::::get(), 1); + + assert_eq!( + VoterBags::::get(10).unwrap(), + Bag:: { head: Some(31), tail: Some(31), bag_upper: 0 } + ); + assert_eq!( + VoterBags::::get(1_000).unwrap(), + Bag:: { head: Some(11), tail: Some(101), bag_upper: 0 } + ); + + let weight_of = Staking::weight_of_fn(); + + assert_eq!(weight_of(&11), 1000); + assert_eq!(VoterBagFor::::get(11).unwrap(), 1000); + assert_eq!( + VoterNodes::::get(11).unwrap(), + node(Voter::validator(11), None, Some(21)) + ); + + assert_eq!(weight_of(&21), 1000); + assert_eq!(VoterBagFor::::get(21).unwrap(), 1000); + assert_eq!( + VoterNodes::::get(21).unwrap(), + node(Voter::validator(21), Some(11), Some(101)) + ); + + assert_eq!(weight_of(&31), 1); + assert_eq!(VoterBagFor::::get(31).unwrap(), 10); + assert_eq!( + VoterNodes::::get(31).unwrap(), + node(Voter::validator(31), None, None) + ); + + assert_eq!(weight_of(&41), 1000); + assert_eq!(VoterBagFor::::get(41), None); // this staker is chilled! + assert_eq!(VoterNodes::::get(41), None); + + assert_eq!(weight_of(&101), 500); + assert_eq!(VoterBagFor::::get(101).unwrap(), 1000); + assert_eq!( + VoterNodes::::get(101).unwrap(), + node(Voter::nominator(101), Some(21), None) + ); + + // iteration of the bags would yield: + assert_eq!( + VoterList::::iter().map(|n| n.voter().id).collect::>(), + vec![11, 21, 101, 31], + // ^^ note the order of insertion in genesis! + ); + + assert_eq!(get_bags(), vec![(10, vec![31]), (1000, vec![11, 21, 101])]); + }) + } + + #[test] + fn notional_bag_for_works() { + // under a threshold gives the next threshold. + assert_eq!(notional_bag_for::(0), 10); + assert_eq!(notional_bag_for::(9), 10); + assert_eq!(notional_bag_for::(11), 20); + + // at a threshold gives that threshold. + assert_eq!(notional_bag_for::(10), 10); + + let max_explicit_threshold = *::VoterBagThresholds::get().last().unwrap(); + assert_eq!(max_explicit_threshold, 10_000); + // if the max explicit threshold is less than VoteWeight::MAX, + assert!(VoteWeight::MAX > max_explicit_threshold); + // anything above it will belong to the VoteWeight::MAX bag. + assert_eq!(notional_bag_for::(max_explicit_threshold + 1), VoteWeight::MAX); + } + + #[test] + fn remove_last_voter_in_bags_cleans_bag() { + ExtBuilder::default().build_and_execute(|| { + // given + assert_eq!(get_bags(), vec![(10, vec![31]), (1000, vec![11, 21, 101])]); + + // give 31 more stake to bump it to a new bag. + Balances::make_free_balance_be(&31, 10000); + assert_ok!(Staking::bond_extra(Origin::signed(31), 10000 - 10)); + + // then the bag with bound 10 is wiped from storage. + assert_eq!(get_bags(), vec![(1000, vec![11, 21, 101]), (10_000, vec![31])]); + + // and can be recreated again as needed + bond_validator(77, 777, 10); + assert_eq!( + get_bags(), + vec![(10, vec![77]), (1000, vec![11, 21, 101]), (10_000, vec![31])] + ); + }); + } + + #[test] + fn iteration_is_semi_sorted() { + ExtBuilder::default().build_and_execute(|| { + // add some new validators to the genesis state. + bond_validator(51, 50, 2000); + bond_validator(61, 60, 2000); + + // given + assert_eq!( + get_bags(), + vec![(10, vec![31]), (1000, vec![11, 21, 101]), (2000, vec![51, 61])], + ); + + // then + assert_eq!( + get_voter_list_as_ids(), + vec![ + 51, 61, // best bag + 11, 21, 101, // middle bag + 31, // last bag. + ] + ); + + // when adding a voter that has a higher weight than pre-existing voters in the bag + bond_validator(71, 70, 10); + + // then + assert_eq!( + get_voter_list_as_ids(), + vec![ + 51, 61, // best bag + 11, 21, 101, // middle bag + 31, + 71, // last bag; the new voter is last, because it is order of insertion + ] + ); + }) + } + + /// This tests that we can `take` x voters, even if that quantity ends midway through a list. + #[test] + fn take_works() { + ExtBuilder::default().build_and_execute(|| { + // add some new validators to the genesis state. + bond_validator(51, 50, 2000); + bond_validator(61, 60, 2000); + + // given + assert_eq!( + get_bags(), + vec![(10, vec![31]), (1000, vec![11, 21, 101]), (2000, vec![51, 61])], + ); + + // when + let iteration = + VoterList::::iter().map(|node| node.voter.id).take(4).collect::>(); + + // then + assert_eq!( + iteration, + vec![ + 51, 61, // best bag, fully iterated + 11, 21, // middle bag, partially iterated + ] + ); + }) + } + + #[test] + fn insert_works() { + ExtBuilder::default().build_and_execute_without_check_count(|| { + // when inserting into an existing bag + bond(42, 43, 1_000); + VoterList::::insert(Voter::<_>::nominator(42), Pallet::::weight_of_fn()); + + // then + assert_eq!(get_voter_list_as_ids(), vec![11, 21, 101, 42, 31]); + assert_eq!(get_bags(), vec![(10, vec![31]), (1_000, vec![11, 21, 101, 42])]); + + // when inserting into a non-existent bag + bond(422, 433, 1_001); + VoterList::::insert(Voter::<_>::nominator(422), Pallet::::weight_of_fn()); + + // then + assert_eq!(get_voter_list_as_ids(), vec![422, 11, 21, 101, 42, 31]); + assert_eq!( + get_bags(), + vec![(10, vec![31]), (1_000, vec![11, 21, 101, 42]), (2_000, vec![422])] + ); + }); + } + + #[test] + fn insert_as_works() { + ExtBuilder::default().build_and_execute_without_check_count(|| { + // given + let actual = get_voter_list_as_voters(); + let mut expected: Vec> = vec![ + Voter::<_>::validator(11), + Voter::<_>::validator(21), + Voter::<_>::nominator(101), + Voter::<_>::validator(31), + ]; + assert_eq!(actual, expected); + + // when inserting a new voter + VoterList::::insert_as(&42, VoterType::Nominator); + + // then + let actual = get_voter_list_as_voters(); + expected.push(Voter::<_>::nominator(42)); + assert_eq!(actual, expected); + + // when updating the voter type of an already existing voter + VoterList::::insert_as(&42, VoterType::Validator); + + // then + let actual = get_voter_list_as_voters(); + expected[4] = Voter::<_>::validator(42); + assert_eq!(actual, expected); + }); + } + + #[test] + fn remove_works() { + use crate::{CounterForVoters, VoterBags, VoterNodes}; + + let check_storage = |id, counter, voters, bags| { + assert!(!VoterBagFor::::contains_key(id)); + assert!(!VoterNodes::::contains_key(id)); + assert_eq!(CounterForVoters::::get(), counter); + assert_eq!(VoterBagFor::::iter().count() as u32, counter); + assert_eq!(VoterNodes::::iter().count() as u32, counter); + assert_eq!(get_voter_list_as_ids(), voters); + assert_eq!(get_bags(), bags); + }; + + ExtBuilder::default().build_and_execute_without_check_count(|| { + // when removing a non-existent voter + VoterList::::remove(&42); + assert!(!VoterBagFor::::contains_key(42)); + assert!(!VoterNodes::::contains_key(42)); + + // then nothing changes + assert_eq!(get_voter_list_as_ids(), vec![11, 21, 101, 31]); + assert_eq!(get_bags(), vec![(10, vec![31]), (1_000, vec![11, 21, 101])]); + assert_eq!(CounterForVoters::::get(), 4); + + // when removing a node from a bag with multiple nodes + VoterList::::remove(&11); + + // then + assert_eq!(get_voter_list_as_ids(), vec![21, 101, 31]); + check_storage( + 11, + 3, + vec![21, 101, 31], // voter list + vec![(10, vec![31]), (1_000, vec![21, 101])], // bags + ); + + // when removing a node from a bag with only one node: + VoterList::::remove(&31); + + // then + assert_eq!(get_voter_list_as_ids(), vec![21, 101]); + check_storage( + 31, + 2, + vec![21, 101], // voter list + vec![(1_000, vec![21, 101])], // bags + ); + assert!(!VoterBags::::contains_key(10)); // bag 10 is removed + + // remove remaining voters to make sure storage cleans up as expected + VoterList::::remove(&21); + check_storage( + 21, + 1, + vec![101], // voter list + vec![(1_000, vec![101])], // bags + ); + + VoterList::::remove(&101); + check_storage( + 101, + 0, + Vec::::new(), // voter list + vec![], // bags + ); + assert!(!VoterBags::::contains_key(1_000)); // bag 1_000 is removed + + // bags are deleted via removals + assert_eq!(VoterBags::::iter().count(), 0); + // nominator and validator counters are not updated at this level of the api + assert_eq!(crate::CounterForValidators::::get(), 3); + assert_eq!(crate::CounterForNominators::::get(), 1); + }); + } + + #[test] + fn update_position_for_works() { + ExtBuilder::default().build_and_execute_without_check_count(|| { + let weight_of = Staking::weight_of_fn(); + + // given a correctly placed account 31 + let node_31 = Node::::from_id(&31).unwrap(); + assert!(!node_31.is_misplaced(&weight_of)); + + // when account 31 bonds extra and needs to be moved to a non-existing higher bag + // (we can't call bond_extra, because that implicitly calls update_position_for) + set_ledger_and_free_balance(&31, 11); + + assert!(node_31.is_misplaced(&weight_of)); + assert_eq!(weight_of(&31), 11); + + // then updating position moves it to the correct bag + assert_eq!(VoterList::::update_position_for(node_31, &weight_of), Some((10, 20))); + assert_eq!(get_bags(), vec![(20, vec![31]), (1_000, vec![11, 21, 101])]); + assert_eq!(get_voter_list_as_ids(), vec![11, 21, 101, 31]); + + // and if you try and update the position with no change in active stake nothing changes + let node_31 = Node::::from_id(&31).unwrap(); + assert_storage_noop!(assert_eq!( + VoterList::::update_position_for(node_31, &weight_of), + None, + )); + + // when account 31 bonds extra and needs to be moved to an existing higher bag + set_ledger_and_free_balance(&31, 61); + + // then updating positions moves it to the correct bag + let node_31 = Node::::from_id(&31).unwrap(); + assert_eq!( + VoterList::::update_position_for(node_31, &weight_of), + Some((20, 1_000)) + ); + assert_eq!(get_bags(), vec![(1_000, vec![11, 21, 101, 31])]); + assert_eq!(get_voter_list_as_ids(), vec![11, 21, 101, 31]); + + // when account 31 bonds extra but should not change bags + set_ledger_and_free_balance(&31, 1_000); + + // then nothing changes + let node_31 = Node::::from_id(&31).unwrap(); + assert_storage_noop!(assert_eq!( + VoterList::::update_position_for(node_31, &weight_of), + None, + )); + }); + } +} + +#[cfg(test)] +mod bags { + use super::*; + use crate::mock::*; + use frame_support::{assert_ok, assert_storage_noop}; + + #[test] + fn get_works() { + use crate::VoterBags; + ExtBuilder::default().build_and_execute_without_check_count(|| { + let check_bag = |bag_upper, head, tail, ids| { + assert_storage_noop!(Bag::::get(bag_upper)); + + let bag = Bag::::get(bag_upper).unwrap(); + let bag_ids = bag.iter().map(|n| n.voter().id).collect::>(); + + assert_eq!(bag, Bag:: { head, tail, bag_upper }); + assert_eq!(bag_ids, ids); + }; + + // given uppers of bags that exist. + let existing_bag_uppers = vec![10, 1_000]; + + // we can fetch them + check_bag(existing_bag_uppers[0], Some(31), Some(31), vec![31]); + // (getting the same bag twice has the same results) + check_bag(existing_bag_uppers[0], Some(31), Some(31), vec![31]); + check_bag(existing_bag_uppers[1], Some(11), Some(101), vec![11, 21, 101]); + + // and all other uppers don't get bags. + ::VoterBagThresholds::get() + .iter() + .chain(iter::once(&VoteWeight::MAX)) + .filter(|bag_upper| !existing_bag_uppers.contains(bag_upper)) + .for_each(|bag_upper| { + assert_storage_noop!(assert_eq!(Bag::::get(*bag_upper), None)); + assert!(!VoterBags::::contains_key(*bag_upper)); + }); + + // when we make a pre-existing bag empty + VoterList::::remove(&31); + + // then + assert_eq!(Bag::::get(existing_bag_uppers[0]), None) + }); + } + + #[test] + #[should_panic] + fn get_panics_with_a_bad_threshold() { + // NOTE: panic is only expected with debug compilation + ExtBuilder::default().build_and_execute_without_check_count(|| { + Bag::::get(11); + }); + } + + #[test] + fn insert_node_happy_paths_works() { + ExtBuilder::default().build_and_execute_without_check_count(|| { + let node = |voter, bag_upper| Node:: { voter, prev: None, next: None, bag_upper }; + + // when inserting into a bag with 1 node + let mut bag_10 = Bag::::get(10).unwrap(); + // (note: bags api does not care about balance or ledger) + bag_10.insert_node(node(Voter::nominator(42), bag_10.bag_upper)); + // then + assert_eq!(bag_as_ids(&bag_10), vec![31, 42]); + + // when inserting into a bag with 3 nodes + let mut bag_1000 = Bag::::get(1_000).unwrap(); + bag_1000.insert_node(node(Voter::nominator(52), bag_1000.bag_upper)); + // then + assert_eq!(bag_as_ids(&bag_1000), vec![11, 21, 101, 52]); + + // when inserting into a new bag + let mut bag_20 = Bag::::get_or_make(20); + bag_20.insert_node(node(Voter::nominator(71), bag_20.bag_upper)); + // then + assert_eq!(bag_as_ids(&bag_20), vec![71]); + + // when inserting a node pointing to the accounts not in the bag + let voter_61 = Voter::validator(61); + let node_61 = Node:: { + voter: voter_61.clone(), + prev: Some(21), + next: Some(101), + bag_upper: 20, + }; + bag_20.insert_node(node_61); + // then ids are in order + assert_eq!(bag_as_ids(&bag_20), vec![71, 61]); + // and when the node is re-fetched all the info is correct + assert_eq!( + Node::::get(20, &61).unwrap(), + Node:: { voter: voter_61, prev: Some(71), next: None, bag_upper: 20 } + ); + + // state of all bags is as expected + bag_20.put(); // need to put this bag so its in the storage map + assert_eq!( + get_bags(), + vec![(10, vec![31, 42]), (20, vec![71, 61]), (1_000, vec![11, 21, 101, 52])] + ); + }); + } + + // Document improper ways `insert_node` may be getting used. + #[test] + fn insert_node_bad_paths_documented() { + let node = |voter, prev, next, bag_upper| Node:: { voter, prev, next, bag_upper }; + ExtBuilder::default().build_and_execute_without_check_count(|| { + // when inserting a node with both prev & next pointing at an account in the bag + // and an incorrect bag_upper + let mut bag_1000 = Bag::::get(1_000).unwrap(); + let voter_42 = Voter::nominator(42); + bag_1000.insert_node(node(voter_42.clone(), Some(11), Some(11), 0)); + + // then the ids are in the correct order + assert_eq!(bag_as_ids(&bag_1000), vec![11, 21, 101, 42]); + // and when the node is re-fetched all the info is correct + assert_eq!( + Node::::get(1_000, &42).unwrap(), + node(voter_42, Some(101), None, bag_1000.bag_upper) + ); + + // given 21 is a validator in bag_1000 (and not a tail node) + let bag_1000_voter = + bag_1000.iter().map(|node| node.voter().clone()).collect::>(); + assert_eq!(bag_1000_voter[1], Voter::validator(21)); + + // when inserting a node with duplicate id 21 but as a nominator + let voter_21_nom = Voter::nominator(21); + bag_1000.insert_node(node(voter_21_nom.clone(), None, None, bag_1000.bag_upper)); + + // then all the nodes after the duplicate are lost (because it is set as the tail) + assert_eq!(bag_as_ids(&bag_1000), vec![11, 21]); + // and the re-fetched node is a nominator with an **incorrect** prev pointer. + assert_eq!( + Node::::get(1_000, &21).unwrap(), + node(voter_21_nom, Some(42), None, bag_1000.bag_upper) + ); + }); + + ExtBuilder::default().build_and_execute_without_check_count(|| { + // when inserting a duplicate id of the head + let mut bag_1000 = Bag::::get(1_000).unwrap(); + let voter_11 = Voter::validator(11); + bag_1000.insert_node(node(voter_11.clone(), None, None, 0)); + // then all nodes after the head are lost + assert_eq!(bag_as_ids(&bag_1000), vec![11]); + // and the re-fetched node + assert_eq!( + Node::::get(1_000, &11).unwrap(), + node(voter_11, Some(101), None, bag_1000.bag_upper) + ); + + assert_eq!(bag_1000, Bag { head: Some(11), tail: Some(11), bag_upper: 1_000 }) + }); + } + + #[test] + #[should_panic = "system logic error: inserting a node who has the id of tail"] + fn insert_node_duplicate_tail_panics_with_debug_assert() { + ExtBuilder::default().build_and_execute_without_check_count(|| { + let node = |voter, prev, next, bag_upper| Node:: { voter, prev, next, bag_upper }; + + // given + assert_eq!(get_bags(), vec![(10, vec![31]), (1000, vec![11, 21, 101])],); + let mut bag_1000 = Bag::::get(1_000).unwrap(); + + // when inserting a duplicate id that is already the tail + assert_eq!(bag_1000.tail, Some(101)); + let voter_101 = Voter::validator(101); + bag_1000.insert_node(node(voter_101, None, None, bag_1000.bag_upper)); // panics + }); + } + + #[test] + fn remove_node_happy_paths_works() { + ExtBuilder::default().build_and_execute_without_check_count(|| { + // add some validators to genesis state + bond_validator(51, 50, 1_000); + bond_validator(61, 60, 1_000); + bond_validator(71, 70, 10); + bond_validator(81, 80, 10); + bond_validator(91, 90, 2_000); + bond_validator(161, 160, 2_000); + bond_validator(171, 170, 2_000); + bond_validator(181, 180, 2_000); + bond_validator(191, 190, 2_000); + + let mut bag_10 = Bag::::get(10).unwrap(); + let mut bag_1000 = Bag::::get(1_000).unwrap(); + let mut bag_2000 = Bag::::get(2_000).unwrap(); + + // given + assert_eq!(bag_as_ids(&bag_10), vec![31, 71, 81]); + assert_eq!(bag_as_ids(&bag_1000), vec![11, 21, 101, 51, 61]); + assert_eq!(bag_as_ids(&bag_2000), vec![91, 161, 171, 181, 191]); + + // remove node that is not pointing at head or tail + let node_101 = Node::::get(bag_1000.bag_upper, &101).unwrap(); + let node_101_pre_remove = node_101.clone(); + bag_1000.remove_node(&node_101); + assert_eq!(bag_as_ids(&bag_1000), vec![11, 21, 51, 61]); + assert_ok!(bag_1000.sanity_check()); + // node isn't mutated when its removed + assert_eq!(node_101, node_101_pre_remove); + + // remove head when its not pointing at tail + let node_11 = Node::::get(bag_1000.bag_upper, &11).unwrap(); + bag_1000.remove_node(&node_11); + assert_eq!(bag_as_ids(&bag_1000), vec![21, 51, 61]); + assert_ok!(bag_1000.sanity_check()); + + // remove tail when its not pointing at head + let node_61 = Node::::get(bag_1000.bag_upper, &61).unwrap(); + bag_1000.remove_node(&node_61); + assert_eq!(bag_as_ids(&bag_1000), vec![21, 51]); + assert_ok!(bag_1000.sanity_check()); + + // remove tail when its pointing at head + let node_51 = Node::::get(bag_1000.bag_upper, &51).unwrap(); + bag_1000.remove_node(&node_51); + assert_eq!(bag_as_ids(&bag_1000), vec![21]); + assert_ok!(bag_1000.sanity_check()); + + // remove node that is head & tail + let node_21 = Node::::get(bag_1000.bag_upper, &21).unwrap(); + bag_1000.remove_node(&node_21); + bag_1000.put(); // put into storage so get returns the updated bag + assert_eq!(Bag::::get(1_000), None); + + // remove node that is pointing at head and tail + let node_71 = Node::::get(bag_10.bag_upper, &71).unwrap(); + bag_10.remove_node(&node_71); + assert_eq!(bag_as_ids(&bag_10), vec![31, 81]); + assert_ok!(bag_10.sanity_check()); + + // remove head when pointing at tail + let node_31 = Node::::get(bag_10.bag_upper, &31).unwrap(); + bag_10.remove_node(&node_31); + assert_eq!(bag_as_ids(&bag_10), vec![81]); + assert_ok!(bag_10.sanity_check()); + bag_10.put(); // since we updated the bag's head/tail, we need to write this storage + + // remove node that is pointing at head, but not tail + let node_161 = Node::::get(bag_2000.bag_upper, &161).unwrap(); + bag_2000.remove_node(&node_161); + assert_eq!(bag_as_ids(&bag_2000), vec![91, 171, 181, 191]); + assert_ok!(bag_2000.sanity_check()); + + // remove node that is pointing at tail, but not head + let node_181 = Node::::get(bag_2000.bag_upper, &181).unwrap(); + bag_2000.remove_node(&node_181); + assert_eq!(bag_as_ids(&bag_2000), vec![91, 171, 191]); + assert_ok!(bag_2000.sanity_check()); + + // state of all bags is as expected + assert_eq!(get_bags(), vec![(10, vec![81]), (2_000, vec![91, 171, 191])]); + }); + } + + #[test] + fn remove_node_bad_paths_documented() { + ExtBuilder::default().build_and_execute_without_check_count(|| { + // removing a node that is in the bag but has the wrong upper works. + + let bad_upper_node_11 = Node:: { + voter: Voter::<_>::validator(11), + prev: None, + next: Some(21), + bag_upper: 10, // should be 1_000 + }; + let mut bag_1000 = Bag::::get(1_000).unwrap(); + bag_1000.remove_node(&bad_upper_node_11); + bag_1000.put(); + + assert_eq!(get_bags(), vec![(10, vec![31]), (1_000, vec![21, 101])]); + let bag_1000 = Bag::::get(1_000).unwrap(); + assert_ok!(bag_1000.sanity_check()); + assert_eq!(bag_1000.head, Some(21)); + assert_eq!(bag_1000.tail, Some(101)); + }); + + ExtBuilder::default().build_and_execute_without_check_count(|| { + // removing a node that is in another bag, will mess up the + // other bag. + + let node_101 = Node::::get(1_000, &101).unwrap(); + let mut bag_10 = Bag::::get(10).unwrap(); + bag_10.remove_node(&node_101); // node_101 is in bag 1_000 + bag_10.put(); + + // the node was removed from its actual bag, bag_1000. + assert_eq!(get_bags(), vec![(10, vec![31]), (1_000, vec![11, 21])]); + + // the bag removed was called on is ok. + let bag_10 = Bag::::get(10).unwrap(); + assert_eq!(bag_10.tail, Some(31)); + assert_eq!(bag_10.head, Some(31)); + + // but the bag that the node belonged to is in an invalid state + let bag_1000 = Bag::::get(1_000).unwrap(); + // because it still has the removed node as its tail. + assert_eq!(bag_1000.tail, Some(101)); + assert_eq!(bag_1000.head, Some(11)); + assert_ok!(bag_1000.sanity_check()); + }); + } +} + +#[cfg(test)] +mod voter_node { + use super::*; + use crate::mock::*; + + #[test] + fn voting_data_works() { + ExtBuilder::default().build_and_execute_without_check_count(|| { + let weight_of = Staking::weight_of_fn(); + + // add nominator with no targets + bond_nominator(42, 43, 1_000, vec![11]); + + // given + assert_eq!( + get_voter_list_as_voters(), + vec![ + Voter::validator(11), + Voter::validator(21), + Voter::nominator(101), + Voter::nominator(42), + Voter::validator(31), + ] + ); + assert_eq!(active_era(), 0); + + let slashing_spans = + ::SlashingSpans::iter().collect::>(); + assert_eq!(slashing_spans.keys().len(), 0); // no pre-existing slashing spans + + let node_11 = Node::::get(10, &11).unwrap(); + assert_eq!( + node_11.voting_data(&weight_of, &slashing_spans).unwrap(), + (11, 1_000, vec![11]) + ); + + // getting data for a nominators with 0 slashed targets + let node_101 = Node::::get(1_000, &101).unwrap(); + assert_eq!( + node_101.voting_data(&weight_of, &slashing_spans).unwrap(), + (101, 500, vec![11, 21]) + ); + let node_42 = Node::::get(10, &42).unwrap(); + assert_eq!( + node_42.voting_data(&weight_of, &slashing_spans).unwrap(), + (42, 1_000, vec![11]) + ); + + // roll ahead an era so any slashes will be after the previous nominations + start_active_era(1); + + // when a validator gets a slash, + add_slash(&11); + let slashing_spans = + ::SlashingSpans::iter().collect::>(); + + assert_eq!(slashing_spans.keys().cloned().collect::>(), vec![11, 42, 101]); + // then its node no longer exists + assert_eq!( + get_voter_list_as_voters(), + vec![ + Voter::validator(21), + Voter::nominator(101), + Voter::nominator(42), + Voter::validator(31), + ] + ); + // and its nominators no longer have it as a target + let node_101 = Node::::get(10, &101).unwrap(); + assert_eq!( + node_101.voting_data(&weight_of, &slashing_spans), + Some((101, 475, vec![21])), + ); + + let node_42 = Node::::get(10, &42).unwrap(); + assert_eq!( + node_42.voting_data(&weight_of, &slashing_spans), + None, // no voting data since its 1 target has been slashed since nominating + ); + }); + } + + #[test] + fn is_misplaced_works() { + ExtBuilder::default().build_and_execute_without_check_count(|| { + let weight_of = Staking::weight_of_fn(); + let node_31 = Node::::get(10, &31).unwrap(); + + // a node is properly placed if its slashable balance is in range + // of the threshold of the bag its in. + assert_eq!(Staking::slashable_balance_of(&31), 1); + assert!(!node_31.is_misplaced(&weight_of)); + + // and will become misplaced if its slashable balance does not + // correspond to the bag it is in. + set_ledger_and_free_balance(&31, 11); + + assert_eq!(Staking::slashable_balance_of(&31), 11); + assert!(node_31.is_misplaced(&weight_of)); + }); + } +} diff --git a/frame/staking/src/weights.rs b/frame/staking/src/weights.rs index fb4ed160d8325..c1c32c5da6b31 100644 --- a/frame/staking/src/weights.rs +++ b/frame/staking/src/weights.rs @@ -73,6 +73,8 @@ pub trait WeightInfo { fn get_npos_targets(v: u32, ) -> Weight; fn set_staking_limits() -> Weight; fn chill_other() -> Weight; + fn rebag() -> Weight; + fn regenerate(v: u32, n: u32, ) -> Weight; } /// Weights for pallet_staking using the Substrate node and recommended hardware. @@ -171,7 +173,7 @@ impl WeightInfo for SubstrateWeight { .saturating_add((5_527_000 as Weight).saturating_mul(n as Weight)) .saturating_add(T::DbWeight::get().reads(7 as Weight)) .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(n as Weight))) - .saturating_add(T::DbWeight::get().writes(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(7 as Weight)) } // Storage: Staking Validators (r:1 w:0) // Storage: Staking Ledger (r:1 w:0) @@ -374,7 +376,7 @@ impl WeightInfo for SubstrateWeight { .saturating_add((91_111_000 as Weight).saturating_mul(s as Weight)) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(v as Weight))) - .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(n as Weight))) + .saturating_add(T::DbWeight::get().reads((4 as Weight).saturating_mul(n as Weight))) .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) } // Storage: Staking Validators (r:501 w:0) @@ -406,6 +408,24 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(7 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } + fn rebag() -> Weight { + (84_501_000 as Weight) + .saturating_add(T::DbWeight::get().reads(7 as Weight)) + .saturating_add(T::DbWeight::get().writes(5 as Weight)) + } + fn regenerate(v: u32, n: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 128_000 + .saturating_add((40_328_000 as Weight).saturating_mul(v as Weight)) + // Standard Error: 128_000 + .saturating_add((42_763_000 as Weight).saturating_mul(n as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(v as Weight))) + .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(n as Weight))) + .saturating_add(T::DbWeight::get().writes(14 as Weight)) + .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(v as Weight))) + .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(n as Weight))) + } } // For backwards compatibility and tests @@ -503,7 +523,7 @@ impl WeightInfo for () { .saturating_add((5_527_000 as Weight).saturating_mul(n as Weight)) .saturating_add(RocksDbWeight::get().reads(7 as Weight)) .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(n as Weight))) - .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(7 as Weight)) } // Storage: Staking Validators (r:1 w:0) // Storage: Staking Ledger (r:1 w:0) @@ -706,7 +726,7 @@ impl WeightInfo for () { .saturating_add((91_111_000 as Weight).saturating_mul(s as Weight)) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(v as Weight))) - .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(n as Weight))) + .saturating_add(RocksDbWeight::get().reads((4 as Weight).saturating_mul(n as Weight))) .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) } // Storage: Staking Validators (r:501 w:0) @@ -738,4 +758,22 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(7 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } + fn rebag() -> Weight { + (84_501_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(7 as Weight)) + .saturating_add(RocksDbWeight::get().writes(5 as Weight)) + } + fn regenerate(v: u32, n: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 128_000 + .saturating_add((40_328_000 as Weight).saturating_mul(v as Weight)) + // Standard Error: 128_000 + .saturating_add((42_763_000 as Weight).saturating_mul(n as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(v as Weight))) + .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(n as Weight))) + .saturating_add(RocksDbWeight::get().writes(14 as Weight)) + .saturating_add(RocksDbWeight::get().writes((2 as Weight).saturating_mul(v as Weight))) + .saturating_add(RocksDbWeight::get().writes((2 as Weight).saturating_mul(n as Weight))) + } } diff --git a/primitives/npos-elections/src/lib.rs b/primitives/npos-elections/src/lib.rs index ece5be33b114a..ff4919876aff1 100644 --- a/primitives/npos-elections/src/lib.rs +++ b/primitives/npos-elections/src/lib.rs @@ -155,7 +155,8 @@ where + Debug + Copy + Clone - + Bounded; + + Bounded + + Encode; /// The target type. Needs to be an index (convert to usize). type Target: UniqueSaturatedInto @@ -164,7 +165,8 @@ where + Debug + Copy + Clone - + Bounded; + + Bounded + + Encode; /// The weight/accuracy type of each vote. type Accuracy: PerThing128; @@ -479,6 +481,18 @@ pub struct Support { pub voters: Vec<(AccountId, ExtendedBalance)>, } +#[cfg(feature = "mocks")] +impl Support { + /// `true` when the support is identical except for the ordering of the voters. + pub fn eq_unordered(&self, other: &Self) -> bool { + self.total == other.total && { + let my_voters: BTreeMap<_, _> = self.voters.iter().cloned().collect(); + let other_voters: BTreeMap<_, _> = other.voters.iter().cloned().collect(); + my_voters == other_voters + } + } +} + /// A target-major representation of the the election outcome. /// /// Essentially a flat variant of [`SupportMap`]. @@ -486,6 +500,21 @@ pub struct Support { /// The main advantage of this is that it is encodable. pub type Supports = Vec<(A, Support)>; +#[cfg(feature = "mocks")] +pub fn supports_eq_unordered( + a: &Supports, + b: &Supports, +) -> bool { + let map: BTreeMap<_, _> = a.iter().cloned().collect(); + b.iter().all(|(id, b_support)| { + let a_support = match map.get(id) { + Some(support) => support, + None => return false, + }; + a_support.eq_unordered(b_support) + }) +} + /// Linkage from a winner to their [`Support`]. /// /// This is more helpful than a normal [`Supports`] as it allows faster error checking. @@ -507,12 +536,12 @@ impl FlattenSupportMap for SupportMap { /// /// The list of winners is basically a redundancy for error checking only; It ensures that all the /// targets pointed to by the [`Assignment`] are present in the `winners`. -pub fn to_support_map( - winners: &[A], - assignments: &[StakedAssignment], -) -> Result, Error> { +pub fn to_support_map( + winners: &[AccountId], + assignments: &[StakedAssignment], +) -> Result, Error> { // Initialize the support of each candidate. - let mut supports = >::new(); + let mut supports = >::new(); winners.iter().for_each(|e| { supports.insert(e.clone(), Default::default()); }); @@ -535,10 +564,10 @@ pub fn to_support_map( /// flat vector. /// /// Similar to [`to_support_map`], `winners` is used for error checking. -pub fn to_supports( - winners: &[A], - assignments: &[StakedAssignment], -) -> Result, Error> { +pub fn to_supports( + winners: &[AccountId], + assignments: &[StakedAssignment], +) -> Result, Error> { to_support_map(winners, assignments).map(FlattenSupportMap::flatten) }