diff --git a/Cargo.lock b/Cargo.lock index d277db3f519b9..83467f1593bee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -480,7 +480,7 @@ dependencies = [ "futures 0.3.16", "log 0.4.14", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sc-client-api", "sc-keystore", "sc-network", @@ -1871,7 +1871,7 @@ dependencies = [ "log 0.4.14", "num-traits", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "rand 0.8.4", "scale-info", ] @@ -3095,7 +3095,7 @@ dependencies = [ "jsonrpc-server-utils", "log 0.4.14", "net2", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "unicase 2.6.0", ] @@ -3110,7 +3110,7 @@ dependencies = [ "jsonrpc-server-utils", "log 0.4.14", "parity-tokio-ipc", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "tower-service", ] @@ -3124,7 +3124,7 @@ dependencies = [ "jsonrpc-core", "lazy_static", "log 0.4.14", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "rand 0.7.3", "serde", ] @@ -3158,7 +3158,7 @@ dependencies = [ "jsonrpc-server-utils", "log 0.4.14", "parity-ws", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "slab", ] @@ -3295,7 +3295,7 @@ checksum = "c3b6b85fc643f5acd0bffb2cc8a6d150209379267af0d41db72170021841f9f5" dependencies = [ "kvdb", "parity-util-mem", - "parking_lot 0.11.1", + "parking_lot 0.11.2", ] [[package]] @@ -3310,7 +3310,7 @@ dependencies = [ "num_cpus", "owning_ref", "parity-util-mem", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "regex", "rocksdb", "smallvec 1.7.0", @@ -3429,7 +3429,7 @@ dependencies = [ "libp2p-websocket", "libp2p-yamux", "multiaddr", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "pin-project 1.0.8", "smallvec 1.7.0", "wasm-timer", @@ -3454,7 +3454,7 @@ dependencies = [ "multiaddr", "multihash 0.14.0", "multistream-select", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "pin-project 1.0.8", "prost", "prost-build", @@ -3628,7 +3628,7 @@ dependencies = [ "libp2p-core", "log 0.4.14", "nohash-hasher", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "rand 0.7.3", "smallvec 1.7.0", "unsigned-varint 0.7.0", @@ -3861,7 +3861,7 @@ checksum = "4e7362abb8867d7187e7e93df17f460d554c997fc5c8ac57dc1259057f6889af" dependencies = [ "futures 0.3.16", "libp2p-core", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "thiserror", "yamux", ] @@ -3998,9 +3998,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.2" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] @@ -5502,6 +5502,31 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-election-playground" +version = "4.0.0-dev" +dependencies = [ + "frame-election-provider-support", + "frame-support", + "frame-system", + "pallet-bags-list", + "pallet-balances", + "pallet-election-provider-multi-phase", + "pallet-session", + "pallet-staking", + "pallet-staking-reward-curve", + "pallet-timestamp", + "parity-scale-codec", + "parking_lot 0.11.2", + "scale-info", + "sp-core", + "sp-io", + "sp-npos-elections", + "sp-runtime", + "sp-staking", + "sp-tracing", +] + [[package]] name = "pallet-election-provider-multi-phase" version = "4.0.0-dev" @@ -5513,7 +5538,7 @@ dependencies = [ "log 0.4.14", "pallet-balances", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "rand 0.7.3", "scale-info", "sp-arithmetic", @@ -6286,7 +6311,7 @@ dependencies = [ "log 0.4.14", "lz4", "memmap2 0.2.1", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "rand 0.8.4", "snap", ] @@ -6347,7 +6372,7 @@ dependencies = [ "hashbrown 0.11.2", "impl-trait-for-tuples", "parity-util-mem-derive", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "primitive-types", "smallvec 1.7.0", "winapi 0.3.9", @@ -6416,13 +6441,13 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", - "lock_api 0.4.2", - "parking_lot_core 0.8.3", + "lock_api 0.4.5", + "parking_lot_core 0.8.5", ] [[package]] @@ -6442,14 +6467,14 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.2.5", + "redox_syscall 0.2.10", "smallvec 1.7.0", "winapi 0.3.9", ] @@ -6834,7 +6859,7 @@ dependencies = [ "fnv", "lazy_static", "memchr", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "thiserror", ] @@ -7230,9 +7255,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.5" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] @@ -7244,7 +7269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ "getrandom 0.2.3", - "redox_syscall 0.2.5", + "redox_syscall 0.2.10", ] [[package]] @@ -7597,7 +7622,7 @@ dependencies = [ "futures-timer 3.0.2", "log 0.4.14", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sc-block-builder", "sc-client-api", "sc-proposer-metrics", @@ -7703,7 +7728,7 @@ dependencies = [ "hash-db", "log 0.4.14", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sc-executor", "sc-transaction-pool-api", "sc-utils", @@ -7736,7 +7761,7 @@ dependencies = [ "log 0.4.14", "parity-db", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "quickcheck", "sc-client-api", "sc-state-db", @@ -7761,7 +7786,7 @@ dependencies = [ "futures-timer 3.0.2", "libp2p", "log 0.4.14", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sc-client-api", "sc-utils", "serde", @@ -7786,7 +7811,7 @@ dependencies = [ "getrandom 0.2.3", "log 0.4.14", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sc-block-builder", "sc-client-api", "sc-consensus", @@ -7828,7 +7853,7 @@ dependencies = [ "num-rational 0.2.4", "num-traits", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "rand 0.7.3", "rand_chacha 0.2.2", "retain_mut", @@ -7952,7 +7977,7 @@ dependencies = [ "futures-timer 3.0.2", "log 0.4.14", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sc-client-api", "sc-consensus", "sp-api", @@ -8011,7 +8036,7 @@ dependencies = [ "libsecp256k1", "log 0.4.14", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "paste 1.0.6", "regex", "sc-executor-common", @@ -8108,7 +8133,7 @@ dependencies = [ "futures-timer 3.0.2", "log 0.4.14", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "rand 0.8.4", "sc-block-builder", "sc-client-api", @@ -8187,7 +8212,7 @@ dependencies = [ "async-trait", "derive_more", "hex", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "serde_json", "sp-application-crypto", "sp-core", @@ -8220,7 +8245,7 @@ dependencies = [ "log 0.4.14", "lru 0.7.0", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "pin-project 1.0.8", "prost", "prost-build", @@ -8280,7 +8305,7 @@ dependencies = [ "futures-timer 3.0.2", "libp2p", "log 0.4.14", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "rand 0.7.3", "sc-block-builder", "sc-client-api", @@ -8312,7 +8337,7 @@ dependencies = [ "num_cpus", "once_cell", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "rand 0.7.3", "sc-block-builder", "sc-client-api", @@ -8366,7 +8391,7 @@ dependencies = [ "lazy_static", "log 0.4.14", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sc-block-builder", "sc-chain-spec", "sc-client-api", @@ -8402,7 +8427,7 @@ dependencies = [ "jsonrpc-pubsub", "log 0.4.14", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sc-chain-spec", "sc-transaction-pool-api", "serde", @@ -8461,7 +8486,7 @@ dependencies = [ "log 0.4.14", "parity-scale-codec", "parity-util-mem", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "pin-project 1.0.8", "rand 0.7.3", "sc-block-builder", @@ -8521,7 +8546,7 @@ dependencies = [ "hex-literal", "log 0.4.14", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sc-block-builder", "sc-client-api", "sc-client-db", @@ -8555,7 +8580,7 @@ dependencies = [ "parity-scale-codec", "parity-util-mem", "parity-util-mem-derive", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sc-client-api", "sp-core", ] @@ -8589,7 +8614,7 @@ dependencies = [ "futures 0.3.16", "libp2p", "log 0.4.14", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "pin-project 1.0.8", "rand 0.7.3", "serde", @@ -8610,7 +8635,7 @@ dependencies = [ "libc", "log 0.4.14", "once_cell", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "regex", "rustc-hash", "sc-client-api", @@ -8652,7 +8677,7 @@ dependencies = [ "log 0.4.14", "parity-scale-codec", "parity-util-mem", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "retain_mut", "sc-block-builder", "sc-client-api", @@ -9264,7 +9289,7 @@ dependencies = [ "log 0.4.14", "lru 0.7.0", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sp-api", "sp-consensus", "sp-database", @@ -9388,7 +9413,7 @@ dependencies = [ "num-traits", "parity-scale-codec", "parity-util-mem", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "primitive-types", "rand 0.7.3", "regex", @@ -9443,7 +9468,7 @@ name = "sp-database" version = "4.0.0-dev" dependencies = [ "kvdb", - "parking_lot 0.11.1", + "parking_lot 0.11.2", ] [[package]] @@ -9505,7 +9530,7 @@ dependencies = [ "libsecp256k1", "log 0.4.14", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sp-core", "sp-externalities", "sp-keystore", @@ -9538,7 +9563,7 @@ dependencies = [ "futures 0.3.16", "merlin", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "rand 0.7.3", "rand_chacha 0.2.2", "schnorrkel", @@ -9779,7 +9804,7 @@ dependencies = [ "log 0.4.14", "num-traits", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "pretty_assertions", "rand 0.7.3", "smallvec 1.7.0", @@ -10231,7 +10256,7 @@ dependencies = [ "derive_more", "futures 0.3.16", "parity-scale-codec", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "sc-transaction-pool", "sc-transaction-pool-api", "sp-blockchain", @@ -10339,7 +10364,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "rand 0.8.4", - "redox_syscall 0.2.5", + "redox_syscall 0.2.10", "remove_dir_all", "winapi 0.3.9", ] @@ -10544,7 +10569,7 @@ dependencies = [ "mio 0.7.13", "num_cpus", "once_cell", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "pin-project-lite 0.2.6", "signal-hook-registry", "tokio-macros", @@ -10783,7 +10808,7 @@ dependencies = [ "chrono", "lazy_static", "matchers", - "parking_lot 0.11.1", + "parking_lot 0.9.0", "regex", "serde", "serde_json", @@ -10892,7 +10917,7 @@ dependencies = [ "lazy_static", "log 0.4.14", "lru-cache", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "resolv-conf", "smallvec 1.7.0", "thiserror", @@ -11292,7 +11317,7 @@ checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" dependencies = [ "futures 0.3.16", "js-sys", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "pin-utils", "wasm-bindgen", "wasm-bindgen-futures", @@ -11888,7 +11913,7 @@ dependencies = [ "futures 0.3.16", "log 0.4.14", "nohash-hasher", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "rand 0.8.4", "static_assertions", ] diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 2cf6548b79211..82f9f50843c8f 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -502,14 +502,14 @@ pallet_staking_reward_curve::build! { parameter_types! { pub const SessionsPerEra: sp_staking::SessionIndex = 6; pub const BondingDuration: pallet_staking::EraIndex = 24 * 28; - pub const SlashDeferDuration: pallet_staking::EraIndex = 24 * 7; // 1/4 the bonding duration. + pub const SlashDeferDuration: pallet_staking::EraIndex = 24 * 7; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const MaxNominatorRewardedPerValidator: u32 = 256; pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17); pub OffchainRepeat: BlockNumber = 5; } -use frame_election_provider_support::onchain; +use frame_election_provider_support::{onchain, SnapshotBoundsBuilder}; impl onchain::Config for Runtime { type Accuracy = Perbill; type DataProvider = Staking; @@ -522,7 +522,6 @@ impl pallet_staking::BenchmarkingConfig for StakingBenchmarkingConfig { } impl pallet_staking::Config for Runtime { - const MAX_NOMINATIONS: u32 = MAX_NOMINATIONS; type Currency = Balances; type UnixTime = Timestamp; type CurrencyToVote = U128CurrencyToVote; @@ -548,10 +547,13 @@ impl pallet_staking::Config for Runtime { // Alternatively, use pallet_staking::UseNominatorsMap to just use the nominators map. // Note that the aforementioned does not scale to a very large number of nominators. type SortedListProvider = BagsList; + // each nominator is allowed a fix number of nomination targets. + type NominationQuota = pallet_staking::FixedNominationQuota; type WeightInfo = pallet_staking::weights::SubstrateWeight; type BenchmarkingConfig = StakingBenchmarkingConfig; } +use frame_election_provider_support::SnapshotBounds; parameter_types! { // phase durations. 1/4 of the last session for each. pub const SignedPhase: u32 = EPOCH_DURATION_IN_BLOCKS / 4; @@ -577,10 +579,11 @@ parameter_types! { .max .get(DispatchClass::Normal); - // BagsList allows a practically unbounded count of nominators to participate in NPoS elections. - // To ensure we respect memory limits when using the BagsList this must be set to a number of - // voters we know can fit into a single vec allocation. - pub const VoterSnapshotPerBlock: u32 = 10_000; + /// maximum of 25k nominators, or 4MB. + pub VoterSnapshotBounds: SnapshotBounds = SnapshotBoundsBuilder::default().size(4 * 1024 * 1024).count(25_000).build(); + /// maximum of 1k validator candidates, with no size limit. + pub TargetSnapshotBounds: SnapshotBounds = SnapshotBounds::new_count(1000); + } sp_npos_elections::generate_solution_type!( @@ -661,10 +664,11 @@ impl pallet_election_provider_multi_phase::Config for Runtime { pallet_election_provider_multi_phase::SolutionAccuracyOf, OffchainRandomBalancing, >; - type WeightInfo = pallet_election_provider_multi_phase::weights::SubstrateWeight; type ForceOrigin = EnsureRootOrHalfCouncil; + type VoterSnapshotBounds = VoterSnapshotBounds; + type TargetSnapshotBounds = TargetSnapshotBounds; + type WeightInfo = pallet_election_provider_multi_phase::weights::SubstrateWeight; type BenchmarkingConfig = ElectionProviderBenchmarkConfig; - type VoterSnapshotPerBlock = VoterSnapshotPerBlock; } parameter_types! { @@ -1788,4 +1792,16 @@ mod tests { If the limit is too strong, maybe consider increase the limit to 300.", ); } + + #[test] + fn snapshot_details() { + let (_, _, max) = + pallet_staking::display_bounds_limits::(VoterSnapshotBounds::get()); + // example of an assertion that a runtime could have to ensure no mis-configuration happens. + assert!( + max <= 25_000, + "under any configuration, the maximum number of voters should be less than 25_000" + ); + let _ = pallet_staking::display_bounds_limits::(TargetSnapshotBounds::get()); + } } diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index f62e73aa43917..90373987ecf28 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -196,7 +196,6 @@ impl onchain::Config for Test { } impl pallet_staking::Config for Test { - const MAX_NOMINATIONS: u32 = 16; type RewardRemainder = (); type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote; type Event = Event; @@ -215,6 +214,7 @@ impl pallet_staking::Config for Test { type NextNewSession = Session; type ElectionProvider = onchain::OnChainSequentialPhragmen; type GenesisElectionProvider = Self::ElectionProvider; + type NominationQuota = pallet_staking::FixedNominationQuota<16>; type SortedListProvider = pallet_staking::UseNominatorsMap; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); diff --git a/frame/bags-list/remote-tests/src/snapshot.rs b/frame/bags-list/remote-tests/src/snapshot.rs index 0e68a4495edfc..f7b62e4fcada3 100644 --- a/frame/bags-list/remote-tests/src/snapshot.rs +++ b/frame/bags-list/remote-tests/src/snapshot.rs @@ -16,13 +16,14 @@ //! Test to execute the snapshot using the voter bag. +use frame_election_provider_support::SnapshotBounds; use frame_support::traits::PalletInfoAccess; use remote_externalities::{Builder, Mode, OnlineConfig}; use sp_runtime::{traits::Block as BlockT, DeserializeOwned}; /// Execute create a snapshot from pallet-staking. pub async fn execute( - voter_limit: Option, + voter_bounds: SnapshotBounds, currency_unit: u64, ws_url: String, ) { @@ -57,7 +58,7 @@ pub async fn execute let voters = as ElectionDataProvider< Runtime::AccountId, Runtime::BlockNumber, - >>::voters(voter_limit) + >>::voters(voter_bounds) .unwrap(); let mut voters_nominator_only = voters @@ -77,7 +78,7 @@ pub async fn execute log::info!( target: crate::LOG_TARGET, "a snapshot with limit {:?} has been created, {} voters are taken. min nominator: {:?}, max: {:?}", - voter_limit, + voter_bounds, voters.len(), min_voter, max_voter diff --git a/frame/election-provider-multi-phase/src/benchmarking.rs b/frame/election-provider-multi-phase/src/benchmarking.rs index d9db6c3090994..9ca0bd345cad8 100644 --- a/frame/election-provider-multi-phase/src/benchmarking.rs +++ b/frame/election-provider-multi-phase/src/benchmarking.rs @@ -246,8 +246,8 @@ frame_benchmarking::benchmarks! { // we don't directly need the data-provider to be populated, but it is just easy to use it. set_up_data_provider::(v, t); - let targets = T::DataProvider::targets(None)?; - let voters = T::DataProvider::voters(None)?; + let targets = T::DataProvider::targets(SnapshotBounds::new_unbounded())?; + let voters = T::DataProvider::voters(SnapshotBounds::new_unbounded())?; let desired_targets = T::DataProvider::desired_targets()?; assert!(>::snapshot().is_none()); }: { diff --git a/frame/election-provider-multi-phase/src/lib.rs b/frame/election-provider-multi-phase/src/lib.rs index 70bbed95fe973..385ba208dfb4a 100644 --- a/frame/election-provider-multi-phase/src/lib.rs +++ b/frame/election-provider-multi-phase/src/lib.rs @@ -230,7 +230,7 @@ #![cfg_attr(not(feature = "std"), no_std)] use codec::{Decode, Encode}; -use frame_election_provider_support::{ElectionDataProvider, ElectionProvider}; +use frame_election_provider_support::{ElectionDataProvider, ElectionProvider, SnapshotBounds}; use frame_support::{ dispatch::DispatchResultWithPostInfo, ensure, @@ -248,7 +248,6 @@ use sp_npos_elections::{ VoteWeight, }; use sp_runtime::{ - traits::Bounded, transaction_validity::{ InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, TransactionValidityError, ValidTransaction, @@ -309,6 +308,23 @@ pub trait BenchmarkingConfig { const MAXIMUM_TARGETS: u32; } +/// A benchmarking config, to be used only for testing. +#[cfg(feature = "std")] +pub struct TestBenchmarkingConfig; + +#[cfg(feature = "std")] +impl BenchmarkingConfig for TestBenchmarkingConfig { + const VOTERS: [u32; 2] = [400, 600]; + const ACTIVE_VOTERS: [u32; 2] = [100, 300]; + const TARGETS: [u32; 2] = [200, 400]; + const DESIRED_TARGETS: [u32; 2] = [100, 180]; + + const SNAPSHOT_MAXIMUM_VOTERS: u32 = 1000; + const MINER_MAXIMUM_VOTERS: u32 = 1000; + + const MAXIMUM_TARGETS: u32 = 200; +} + /// A fallback implementation that transitions the pallet to the emergency phase. pub struct NoFallback(sp_std::marker::PhantomData); @@ -631,14 +647,16 @@ pub mod pallet { #[pallet::constant] type SignedDepositWeight: Get>; - /// The maximum number of voters to put in the snapshot. At the moment, snapshots are only - /// over a single block, but once multi-block elections are introduced they will take place - /// over multiple blocks. + /// The bound on the amount of voters to put in the snapshot, per block. + #[pallet::constant] + type VoterSnapshotBounds: Get; + + /// The bound on the amount of targets to put in the snapshot, per block. /// - /// 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. + /// Note that the target snapshot happens next to voter snapshot. In essence, in a single + /// block, `TargetSnapshotSize + VoterSnapshotBounds` must not exhaust. #[pallet::constant] - type VoterSnapshotPerBlock: Get>; + type TargetSnapshotBounds: Get; /// Handler for the slashed deposits. type SlashHandler: OnUnbalanced>; @@ -821,6 +839,14 @@ pub mod pallet { >::MAXIMUM_VOTES_PER_VOTER, as NposSolution>::LIMIT as u32, ); + + // ---------------------------- + // maximum size of a snapshot should not exceed half the size of our allocator limit. + assert!( + T::VoterSnapshotBounds::get().size_bound().unwrap_or_default().saturating_add( + T::TargetSnapshotBounds::get().size_bound().unwrap_or_default() + ) < sp_core::MAX_POSSIBLE_ALLOCATION as usize / 2 - 1 + ); } } @@ -1286,22 +1312,22 @@ impl Pallet { /// Extracted for easier weight calculation. fn create_snapshot_external( ) -> Result<(Vec, Vec>, u32), ElectionError> { - let target_limit = >::max_value().saturated_into::(); - // for now we have just a single block snapshot. - let voter_limit = T::VoterSnapshotPerBlock::get().saturated_into::(); + let target_bound = T::TargetSnapshotBounds::get(); + let voter_bound = T::VoterSnapshotBounds::get(); let targets = - T::DataProvider::targets(Some(target_limit)).map_err(ElectionError::DataProvider)?; - let voters = - T::DataProvider::voters(Some(voter_limit)).map_err(ElectionError::DataProvider)?; + T::DataProvider::targets(target_bound).map_err(ElectionError::DataProvider)?; + let voters = T::DataProvider::voters(voter_bound).map_err(ElectionError::DataProvider)?; let desired_targets = T::DataProvider::desired_targets().map_err(ElectionError::DataProvider)?; // Defensive-only. - if targets.len() > target_limit || voters.len() > voter_limit { - debug_assert!(false, "Snapshot limit has not been respected."); - return Err(ElectionError::DataProvider("Snapshot too big for submission.")) - } + debug_assert!(!voter_bound + .exhausts_size_count_non_zero(|| voters.encoded_size() as u32, || voters.len() as u32)); + debug_assert!(!target_bound.exhausts_size_count_non_zero( + || targets.encoded_size() as u32, + || targets.len() as u32 + )); Ok((targets, voters, desired_targets)) } @@ -1703,8 +1729,8 @@ mod tests { use super::*; use crate::{ mock::{ - multi_phase_events, roll_to, AccountId, ExtBuilder, MockWeightInfo, MultiPhase, - Runtime, SignedMaxSubmissions, System, TargetIndex, Targets, + multi_phase_events, roll_to, ExtBuilder, MockWeightInfo, MultiPhase, Runtime, + SignedMaxSubmissions, System, }, Phase, }; @@ -1951,70 +1977,6 @@ mod tests { }) } - #[test] - 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::>()); - - // 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); - - // On-chain backup works though. - 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().onchain_fallback(false).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::Fallback("NoFallback.")); - 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 2. - 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 } - ); - }) - } - #[test] fn untrusted_score_verification_is_respected() { ExtBuilder::default().build_and_execute(|| { diff --git a/frame/election-provider-multi-phase/src/mock.rs b/frame/election-provider-multi-phase/src/mock.rs index f90c64b75ccb0..b09ef1bc80c50 100644 --- a/frame/election-provider-multi-phase/src/mock.rs +++ b/frame/election-provider-multi-phase/src/mock.rs @@ -18,7 +18,7 @@ use super::*; use crate as multi_phase; use frame_election_provider_support::{ - data_provider, onchain, ElectionDataProvider, SequentialPhragmen, + data_provider, onchain, ElectionDataProvider, SequentialPhragmen, SnapshotBounds, }; pub use frame_support::{assert_noop, assert_ok}; use frame_support::{parameter_types, traits::Hooks, weights::Weight}; @@ -37,7 +37,7 @@ use sp_npos_elections::{ }; use sp_runtime::{ testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + traits::{BlakeTwo256, Bounded, IdentityLookup}, PerU16, }; use std::sync::Arc; @@ -268,7 +268,8 @@ 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 VoterSnapshotBounds: SnapshotBounds = SnapshotBounds::new_unbounded(); + pub static TargetSnapshotBounds: SnapshotBounds = SnapshotBounds::new_unbounded(); pub static EpochLength: u64 = 30; pub static OnChianFallback: bool = true; @@ -377,19 +378,6 @@ parameter_types! { pub static Balancing: Option<(usize, ExtendedBalance)> = Some((0, 0)); } -pub struct TestBenchmarkingConfig; -impl BenchmarkingConfig for TestBenchmarkingConfig { - const VOTERS: [u32; 2] = [400, 600]; - const ACTIVE_VOTERS: [u32; 2] = [100, 300]; - const TARGETS: [u32; 2] = [200, 400]; - const DESIRED_TARGETS: [u32; 2] = [100, 180]; - - const SNAPSHOT_MAXIMUM_VOTERS: u32 = 1000; - const MINER_MAXIMUM_VOTERS: u32 = 1000; - - const MAXIMUM_TARGETS: u32 = 200; -} - impl crate::Config for Runtime { type Event = Event; type Currency = Balances; @@ -415,7 +403,8 @@ impl crate::Config for Runtime { type Fallback = MockFallback; type ForceOrigin = frame_system::EnsureRoot; type Solution = TestNposSolution; - type VoterSnapshotPerBlock = VoterSnapshotPerBlock; + type VoterSnapshotBounds = VoterSnapshotBounds; + type TargetSnapshotBounds = TargetSnapshotBounds; type Solver = SequentialPhragmen, Balancing>; } @@ -435,10 +424,12 @@ pub struct ExtBuilder {} pub struct StakingMock; impl ElectionDataProvider for StakingMock { const MAXIMUM_VOTES_PER_VOTER: u32 = ::LIMIT as u32; - fn targets(maybe_max_len: Option) -> data_provider::Result> { + fn targets(bounds: SnapshotBounds) -> data_provider::Result> { let targets = Targets::get(); - if maybe_max_len.map_or(false, |max_len| targets.len() > max_len) { + if bounds.size_exhausted(|| targets.encoded_size() as u32) || + bounds.count_exhausted(|| targets.len() as u32) + { return Err("Targets too big") } @@ -446,11 +437,17 @@ impl ElectionDataProvider for StakingMock { } fn voters( - maybe_max_len: Option, + bounds: SnapshotBounds, ) -> data_provider::Result)>> { let mut voters = Voters::get(); - if let Some(max_len) = maybe_max_len { - voters.truncate(max_len) + + while bounds.size_exhausted(|| voters.encoded_size() as u32) && !voters.is_empty() { + let _ = voters.pop(); + } + if bounds.count_exhausted(|| voters.len() as u32) { + voters.truncate( + bounds.count_bound().map(|b| b as usize).unwrap_or_else(Bounded::max_value), + ); } Ok(voters) @@ -501,6 +498,82 @@ impl ElectionDataProvider for StakingMock { } } +#[cfg(test)] +mod staking_mock { + use super::*; + + #[test] + fn snapshot_too_big_failure_onchain_fallback() { + // the `StakingMock` is designed such that if it has too many targets, it simply fails. + ExtBuilder::default().build_and_execute(|| { + // 1 byte won't allow for anything. + TargetSnapshotBounds::set(SnapshotBounds::new_count(1)); + + // 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); + + // On-chain backup works though. + 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().onchain_fallback(false).build_and_execute(|| { + TargetSnapshotBounds::set(SnapshotBounds::new_count(1)); + + // 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); + + // emergency phase has started. + roll_to(29); + let err = MultiPhase::elect().unwrap_err(); + assert_eq!(err, ElectionError::Fallback("NoFallback.")); + assert_eq!(MultiPhase::current_phase(), Phase::Emergency); + }); + } + + #[test] + fn snapshot_voter_too_big_truncate() { + // 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); + + // the byte-size of taking the first two voters. + let size_limit_2 = crate::mock::Voters::get() + .into_iter() + .take(2) + .collect::>() + .encoded_size() as u32; + VoterSnapshotBounds::set(SnapshotBounds::new_size(size_limit_2)); + + // Signed phase opens just fine. + roll_to(15); + assert_eq!(MultiPhase::current_phase(), Phase::Signed); + + // snapshot has only two voters. + assert_eq!( + MultiPhase::snapshot_metadata().unwrap(), + SolutionOrSnapshotSize { voters: 2, targets: 4 } + ); + }) + } +} + impl ExtBuilder { pub fn miner_tx_priority(self, p: u64) -> Self { ::set(p); diff --git a/frame/election-provider-support/src/lib.rs b/frame/election-provider-support/src/lib.rs index ac3bfccbbdb54..d87e883bbec56 100644 --- a/frame/election-provider-support/src/lib.rs +++ b/frame/election-provider-support/src/lib.rs @@ -103,12 +103,12 @@ //! fn desired_targets() -> data_provider::Result { //! Ok(1) //! } -//! fn voters(maybe_max_len: Option) +//! fn voters(_bounds: SnapshotBounds) //! -> data_provider::Result)>> //! { //! Ok(Default::default()) //! } -//! fn targets(maybe_max_len: Option) -> data_provider::Result> { +//! fn targets(_bounds: SnapshotBounds) -> data_provider::Result> { //! Ok(vec![10, 20, 30]) //! } //! fn next_election_prediction(now: BlockNumber) -> BlockNumber { @@ -132,7 +132,7 @@ //! type DataProvider = T::DataProvider; //! //! fn elect() -> Result, Self::Error> { -//! Self::DataProvider::targets(None) +//! Self::DataProvider::targets(SnapshotBounds::new_unbounded()) //! .map_err(|_| "failed to elect") //! .map(|t| vec![(t[0], Support::default())]) //! } @@ -161,7 +161,9 @@ #![cfg_attr(not(feature = "std"), no_std)] pub mod onchain; -use frame_support::traits::Get; + +use codec::{Decode, Encode}; +use frame_support::{traits::Get, RuntimeDebug}; use sp_std::{fmt::Debug, prelude::*}; /// Re-export some type as they are used in the interface. @@ -180,28 +182,30 @@ pub mod data_provider { /// Something that can provide the data to an [`ElectionProvider`]. pub trait ElectionDataProvider { /// Maximum number of votes per voter that this data provider is providing. + /// + /// Note that this is the absolute maximum, less votes is also possible. const MAXIMUM_VOTES_PER_VOTER: u32; /// All possible targets for the election, i.e. the candidates. /// - /// If `maybe_max_len` is `Some(v)` then the resulting vector MUST NOT be longer than `v` items - /// long. + /// If `maybe_max_size` is `Some(v)` then the size of the resulting vector MUST NOT be more than + /// `v` bytes. /// /// This should be implemented as a self-weighing function. The implementor should register its /// appropriate weight at the end of execution with the system pallet directly. - fn targets(maybe_max_len: Option) -> data_provider::Result>; + fn targets(bounds: SnapshotBounds) -> data_provider::Result>; /// All possible voters for the election. /// /// Note that if a notion of self-vote exists, it should be represented here. /// - /// If `maybe_max_len` is `Some(v)` then the resulting vector MUST NOT be longer than `v` items - /// long. + /// If `maybe_max_size` is `Some(v)` then the size of the resulting vector MUST NOT be more than + /// `v` bytes. /// /// This should be implemented as a self-weighing function. The implementor should register its /// appropriate weight at the end of execution with the system pallet directly. fn voters( - maybe_max_len: Option, + bounds: SnapshotBounds, ) -> data_provider::Result)>>; /// The number of targets to elect. @@ -250,11 +254,11 @@ pub trait ElectionDataProvider { #[cfg(feature = "std")] impl ElectionDataProvider for () { const MAXIMUM_VOTES_PER_VOTER: u32 = 0; - fn targets(_maybe_max_len: Option) -> data_provider::Result> { + fn targets(_: SnapshotBounds) -> data_provider::Result> { Ok(Default::default()) } fn voters( - _maybe_max_len: Option, + _: SnapshotBounds, ) -> data_provider::Result)>> { Ok(Default::default()) } @@ -439,3 +443,173 @@ impl< sp_npos_elections::phragmms(winners, targets, voters, Balancing::get()) } } + +/// The limits imposed on a snapshot, either voters or targets. +#[derive(Clone, Copy, RuntimeDebug, scale_info::TypeInfo, Encode, Decode)] +pub struct SnapshotBounds { + /// The bound on size, in bytes. `None` means unbounded. + size: Option, + /// The bound on count. `None` means unbounded. + count: Option, +} + +/// Utility builder for [`SnapshotBounds`]. +/// +/// The main purpose of this is to prevent mixing the order of similarly typed arguments (e.g. u32 +/// size and count). +#[derive(Default)] +pub struct SnapshotBoundsBuilder { + size: Option, + count: Option, +} + +impl SnapshotBoundsBuilder { + /// Set the given size. + pub fn size(mut self, size: u32) -> Self { + self.size = Some(size); + self + } + + /// Set the given count. + pub fn count(mut self, count: u32) -> Self { + self.count = Some(count); + self + } + + /// Build the [`SnapshotBounds`] instance. + pub fn build(self) -> SnapshotBounds { + SnapshotBounds { size: self.size, count: self.count } + } +} + +impl SnapshotBounds { + /// Create a new instance of self, with size limit. + pub const fn new_size(size: u32) -> Self { + Self { size: Some(size), count: None } + } + + /// Create a new instance of self, with count limit. + pub const fn new_count(count: u32) -> Self { + Self { count: Some(count), size: None } + } + + /// Create a new unbounded instance of self. + pub const fn new_unbounded() -> Self { + Self { size: None, count: None } + } + + /// returns true if `given_size` exhausts `self.size`. + pub fn size_exhausted(&self, given_size: impl FnOnce() -> u32) -> bool { + self.size.map_or(false, |size| given_size() > size) + } + + /// returns true if `given_count` exhausts `self.count`. + pub fn count_exhausted(&self, given_count: impl FnOnce() -> u32) -> bool { + self.count.map_or(false, |count| given_count() > count) + } + + /// Returns true if `self` is exhausted by either of `given_size` and `given_count`. + /// + /// Note that this will return `false` against an empty collection (size = 1, count = 0). + /// Calling [`self.size_exhausted`] alone cannot handle this edge case, since no information of + /// the count is available. + /// + /// # Warning + /// + /// The function name is hinting at the correct order of `given_size` and `given_count`. Be + /// aware that they have the same type, and mixing them can be catastrophic. + pub fn exhausts_size_count_non_zero( + &self, + given_size: impl FnOnce() -> u32, + given_count: impl FnOnce() -> u32, + ) -> bool { + // take care of this pesky edge case: empty vector (size = 1, count = 0) should not exhaust + // anything. + let given_size = given_size(); + let given_count = given_count(); + if given_size == 1 || given_count == 0 { + return false + } + self.size_exhausted(|| given_size) || self.count_exhausted(|| given_count) + } + + /// Return the size bound, if one exists. + pub fn size_bound(&self) -> Option { + self.size.map(|b| b as usize) + } + + /// Return the count bound, if one exists. + pub fn count_bound(&self) -> Option { + self.count.map(|b| b as usize) + } + + /// Return `true` if self is fully unbounded. + pub fn is_unbounded(&self) -> bool { + self.size.is_none() && self.count.is_none() + } + + /// Return `true` if either of the bounds exists. + pub fn is_bounded(&self) -> bool { + !self.is_unbounded() + } + + /// Predict the `::with_capacity` of a collection that has `self` as bounds (size and count), + /// when each item is `item_size` bytes on average. + /// + /// Returns `None` if no capacity could be made (e.g. if `self` is unbounded). + pub fn predict_capacity(&self, item_size: usize) -> Option { + match (self.size_bound(), self.count_bound()) { + (Some(max_size), Some(max_count)) => Some(max_count.min(max_size / item_size.max(1))), + (Some(max_size), None) => Some(max_size / item_size.max(1)), + (None, Some(max_count)) => Some(max_count), + (None, None) => None, + } + } +} + +#[cfg(test)] +mod snapshot_bounds { + use super::*; + + #[test] + fn predict_capacity_works_single_byte() { + // count is smaller + assert_eq!( + SnapshotBounds { count: Some(100), size: Some(200) }.predict_capacity(1), + Some(100) + ); + + // size is smaller + assert_eq!( + SnapshotBounds { count: Some(200), size: Some(100) }.predict_capacity(1), + Some(100) + ); + + // only one is present + assert_eq!(SnapshotBounds { count: Some(100), size: None }.predict_capacity(1), Some(100)); + assert_eq!(SnapshotBounds { count: None, size: Some(100) }.predict_capacity(1), Some(100)); + + assert_eq!(SnapshotBounds::new_unbounded().predict_capacity(1), None); + } + + #[test] + fn predict_capacity_works_multi_byte() { + // count is smaller + assert_eq!( + SnapshotBounds { count: Some(20), size: Some(200) }.predict_capacity(4), + Some(20) + ); + + // size is smaller + assert_eq!( + SnapshotBounds { count: Some(200), size: Some(100) }.predict_capacity(4), + Some(25) + ); + + // only one is present + assert_eq!(SnapshotBounds { count: Some(100), size: None }.predict_capacity(4), Some(100)); + assert_eq!(SnapshotBounds { count: None, size: Some(100) }.predict_capacity(4), Some(25)); + + assert_eq!(SnapshotBounds::new_unbounded().predict_capacity(4), None); + } +} diff --git a/frame/election-provider-support/src/onchain.rs b/frame/election-provider-support/src/onchain.rs index 6379adae4206b..8df22ca382187 100644 --- a/frame/election-provider-support/src/onchain.rs +++ b/frame/election-provider-support/src/onchain.rs @@ -17,7 +17,7 @@ //! An implementation of [`ElectionProvider`] that does an on-chain sequential phragmen. -use crate::{ElectionDataProvider, ElectionProvider}; +use crate::{ElectionDataProvider, ElectionProvider, SnapshotBounds}; use frame_support::{traits::Get, weights::DispatchClass}; use sp_npos_elections::*; use sp_std::{collections::btree_map::BTreeMap, marker::PhantomData, prelude::*}; @@ -48,7 +48,7 @@ impl From for Error { /// how much weight was consumed. /// /// Finally, this implementation does not impose any limits on the number of voters and targets that -/// are provided. +/// are provided. Only use with a strictly bounded number of nominator/validator candidates. pub struct OnChainSequentialPhragmen(PhantomData); /// Configuration trait of [`OnChainSequentialPhragmen`]. @@ -70,8 +70,10 @@ impl ElectionProvider for OnChainSequen type DataProvider = T::DataProvider; fn elect() -> Result, Self::Error> { - let voters = Self::DataProvider::voters(None).map_err(Error::DataProvider)?; - let targets = Self::DataProvider::targets(None).map_err(Error::DataProvider)?; + let voters = Self::DataProvider::voters(SnapshotBounds::new_unbounded()) + .map_err(Error::DataProvider)?; + let targets = Self::DataProvider::targets(SnapshotBounds::new_unbounded()) + .map_err(Error::DataProvider)?; let desired_targets = Self::DataProvider::desired_targets().map_err(Error::DataProvider)?; let stake_map: BTreeMap = voters @@ -156,18 +158,18 @@ mod tests { mod mock_data_provider { use super::*; - use crate::data_provider; + use crate::{data_provider, SnapshotBounds}; pub struct DataProvider; impl ElectionDataProvider for DataProvider { const MAXIMUM_VOTES_PER_VOTER: u32 = 2; fn voters( - _: Option, + _: SnapshotBounds, ) -> data_provider::Result)>> { Ok(vec![(1, 10, vec![10, 20]), (2, 20, vec![30, 20]), (3, 30, vec![10, 30])]) } - fn targets(_: Option) -> data_provider::Result> { + fn targets(_: SnapshotBounds) -> data_provider::Result> { Ok(vec![10, 20, 30]) } diff --git a/frame/grandpa/src/mock.rs b/frame/grandpa/src/mock.rs index fe7a530ffe0c8..f8cc98f326a19 100644 --- a/frame/grandpa/src/mock.rs +++ b/frame/grandpa/src/mock.rs @@ -198,7 +198,6 @@ impl onchain::Config for Test { } impl pallet_staking::Config for Test { - const MAX_NOMINATIONS: u32 = 16; type RewardRemainder = (); type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote; type Event = Event; @@ -218,6 +217,7 @@ impl pallet_staking::Config for Test { type ElectionProvider = onchain::OnChainSequentialPhragmen; type GenesisElectionProvider = Self::ElectionProvider; type SortedListProvider = pallet_staking::UseNominatorsMap; + type NominationQuota = pallet_staking::FixedNominationQuota<16>; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); } diff --git a/frame/offences/benchmarking/src/lib.rs b/frame/offences/benchmarking/src/lib.rs index 4d042cfd9997f..4623f3e666559 100644 --- a/frame/offences/benchmarking/src/lib.rs +++ b/frame/offences/benchmarking/src/lib.rs @@ -43,7 +43,7 @@ use pallet_session::{ Config as SessionConfig, SessionManager, }; use pallet_staking::{ - Config as StakingConfig, Event as StakingEvent, Exposure, IndividualExposure, + Config as StakingConfig, Event as StakingEvent, Exposure, IndividualExposure, NominationQuota, Pallet as Staking, RewardDestination, ValidatorPrefs, }; @@ -275,7 +275,7 @@ benchmarks! { let r in 1 .. MAX_REPORTERS; // we skip 1 offender, because in such case there is no slashing let o in 2 .. MAX_OFFENDERS; - let n in 0 .. MAX_NOMINATORS.min(::MAX_NOMINATIONS); + let n in 0 .. MAX_NOMINATORS.min(::NominationQuota::ABSOLUTE_MAXIMUM); // Make r reporters let mut reporters = vec![]; @@ -381,7 +381,7 @@ benchmarks! { } report_offence_grandpa { - let n in 0 .. MAX_NOMINATORS.min(::MAX_NOMINATIONS); + let n in 0 .. MAX_NOMINATORS.min(::NominationQuota::ABSOLUTE_MAXIMUM); // for grandpa equivocation reports the number of reporters // and offenders is always 1 @@ -416,7 +416,7 @@ benchmarks! { } report_offence_babe { - let n in 0 .. MAX_NOMINATORS.min(::MAX_NOMINATIONS); + let n in 0 .. MAX_NOMINATORS.min(::NominationQuota::ABSOLUTE_MAXIMUM); // for babe equivocation reports the number of reporters // and offenders is always 1 diff --git a/frame/offences/benchmarking/src/mock.rs b/frame/offences/benchmarking/src/mock.rs index 26a53c7f8a048..46dacddfe46f0 100644 --- a/frame/offences/benchmarking/src/mock.rs +++ b/frame/offences/benchmarking/src/mock.rs @@ -158,7 +158,6 @@ impl onchain::Config for Test { } impl pallet_staking::Config for Test { - const MAX_NOMINATIONS: u32 = 16; type Currency = Balances; type UnixTime = pallet_timestamp::Pallet; type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote; @@ -178,6 +177,7 @@ impl pallet_staking::Config for Test { type ElectionProvider = onchain::OnChainSequentialPhragmen; type GenesisElectionProvider = Self::ElectionProvider; type SortedListProvider = pallet_staking::UseNominatorsMap; + type NominationQuota = pallet_staking::FixedNominationQuota<16>; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); } diff --git a/frame/session/benchmarking/src/lib.rs b/frame/session/benchmarking/src/lib.rs index 6d9d81f385176..befd68037bc6a 100644 --- a/frame/session/benchmarking/src/lib.rs +++ b/frame/session/benchmarking/src/lib.rs @@ -34,7 +34,7 @@ use frame_system::RawOrigin; use pallet_session::{historical::Pallet as Historical, Pallet as Session, *}; use pallet_staking::{ benchmarking::create_validator_with_nominators, testing_utils::create_validators, - RewardDestination, + NominationQuota, RewardDestination, }; const MAX_VALIDATORS: u32 = 1000; @@ -53,10 +53,10 @@ impl OnInitialize for Pallet { benchmarks! { set_keys { - let n = ::MAX_NOMINATIONS; + let n = ::NominationQuota::ABSOLUTE_MAXIMUM; let (v_stash, _) = create_validator_with_nominators::( n, - ::MAX_NOMINATIONS, + ::NominationQuota::ABSOLUTE_MAXIMUM, false, RewardDestination::Staked, )?; @@ -69,10 +69,10 @@ benchmarks! { }: _(RawOrigin::Signed(v_controller), keys, proof) purge_keys { - let n = ::MAX_NOMINATIONS; + let n = ::NominationQuota::ABSOLUTE_MAXIMUM; let (v_stash, _) = create_validator_with_nominators::( n, - ::MAX_NOMINATIONS, + ::NominationQuota::ABSOLUTE_MAXIMUM, false, RewardDestination::Staked )?; diff --git a/frame/session/benchmarking/src/mock.rs b/frame/session/benchmarking/src/mock.rs index e000255b84b87..ca9e5405a5387 100644 --- a/frame/session/benchmarking/src/mock.rs +++ b/frame/session/benchmarking/src/mock.rs @@ -163,7 +163,6 @@ impl onchain::Config for Test { } impl pallet_staking::Config for Test { - const MAX_NOMINATIONS: u32 = 16; type Currency = Balances; type UnixTime = pallet_timestamp::Pallet; type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote; @@ -183,6 +182,7 @@ impl pallet_staking::Config for Test { type ElectionProvider = onchain::OnChainSequentialPhragmen; type GenesisElectionProvider = Self::ElectionProvider; type SortedListProvider = pallet_staking::UseNominatorsMap; + type NominationQuota = pallet_staking::FixedNominationQuota<16>; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); } diff --git a/frame/staking/Cargo.toml b/frame/staking/Cargo.toml index 81f5b181850c1..78c6d197726e8 100644 --- a/frame/staking/Cargo.toml +++ b/frame/staking/Cargo.toml @@ -20,6 +20,7 @@ codec = { package = "parity-scale-codec", version = "2.0.0", default-features = scale-info = { version = "1.0", default-features = false, features = ["derive"] } sp-std = { version = "4.0.0-dev", default-features = false, path = "../../primitives/std" } sp-io = { version = "4.0.0-dev", default-features = false, path = "../../primitives/io" } +sp-core = { version = "4.0.0-dev", default-features = false, path = "../../primitives/core" } sp-runtime = { version = "4.0.0-dev", default-features = false, path = "../../primitives/runtime" } sp-staking = { version = "4.0.0-dev", default-features = false, path = "../../primitives/staking" } frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } @@ -57,6 +58,7 @@ std = [ "scale-info/std", "sp-std/std", "sp-io/std", + "sp-core/std", "frame-support/std", "sp-runtime/std", "sp-staking/std", diff --git a/frame/staking/src/benchmarking.rs b/frame/staking/src/benchmarking.rs index 69a73c51fdc7e..ce8e8034e5715 100644 --- a/frame/staking/src/benchmarking.rs +++ b/frame/staking/src/benchmarking.rs @@ -21,7 +21,7 @@ use super::*; use crate::Pallet as Staking; use testing_utils::*; -use frame_election_provider_support::SortedListProvider; +use frame_election_provider_support::{SnapshotBounds, SortedListProvider}; use frame_support::{ dispatch::UnfilteredDispatchable, pallet_prelude::*, @@ -354,17 +354,17 @@ benchmarks! { kick { // scenario: we want to kick `k` nominators from nominating us (we are a validator). // we'll assume that `k` is under 128 for the purposes of determining the slope. - // each nominator should have `T::MAX_NOMINATIONS` validators nominated, and our validator + // each nominator should have `T::NominationQuota::ABSOLUTE_MAXIMUM` validators nominated, and our validator // should be somewhere in there. let k in 1 .. 128; - // these are the other validators; there are `T::MAX_NOMINATIONS - 1` of them, so - // there are a total of `T::MAX_NOMINATIONS` validators in the system. - let rest_of_validators = create_validators_with_seed::(T::MAX_NOMINATIONS - 1, 100, 415)?; + // these are the other validators; there are `T::NominationQuota::ABSOLUTE_MAXIMUM - 1` of them, so + // there are a total of `T::NominationQuota::ABSOLUTE_MAXIMUM` validators in the system. + let rest_of_validators = create_validators_with_seed::(T::NominationQuota::ABSOLUTE_MAXIMUM - 1, 100, 415)?; // this is the validator that will be kicking. let (stash, controller) = create_stash_controller::( - T::MAX_NOMINATIONS - 1, + T::NominationQuota::ABSOLUTE_MAXIMUM - 1, 100, Default::default(), )?; @@ -379,7 +379,7 @@ benchmarks! { for i in 0 .. k { // create a nominator stash. let (n_stash, n_controller) = create_stash_controller::( - T::MAX_NOMINATIONS + i, + T::NominationQuota::ABSOLUTE_MAXIMUM + i, 100, Default::default(), )?; @@ -414,9 +414,9 @@ benchmarks! { } } - // Worst case scenario, T::MAX_NOMINATIONS + // Worst case scenario, T::NominationQuota::ABSOLUTE_MAXIMUM nominate { - let n in 1 .. T::MAX_NOMINATIONS; + let n in 1 .. T::NominationQuota::ABSOLUTE_MAXIMUM; // clean up any existing state. clear_validators_and_nominators::(); @@ -427,7 +427,7 @@ benchmarks! { // we are just doing an insert into the origin position. let scenario = ListScenario::::new(origin_weight, true)?; let (stash, controller) = create_stash_controller_with_balance::( - SEED + T::MAX_NOMINATIONS + 1, // make sure the account does not conflict with others + SEED + T::NominationQuota::ABSOLUTE_MAXIMUM + 1, // make sure the account does not conflict with others origin_weight, Default::default(), ).unwrap(); @@ -714,7 +714,7 @@ benchmarks! { create_validators_with_nominators_for_era::( v, n, - ::MAX_NOMINATIONS as usize, + ::NominationQuota::ABSOLUTE_MAXIMUM as usize, false, None, )?; @@ -732,7 +732,7 @@ benchmarks! { create_validators_with_nominators_for_era::( v, n, - ::MAX_NOMINATIONS as usize, + ::NominationQuota::ABSOLUTE_MAXIMUM as usize, false, None, )?; @@ -803,7 +803,7 @@ benchmarks! { assert!(balance_before > balance_after); } - get_npos_voters { + get_npos_voters_unbounded { // number of validator intention. let v in (MaxValidators::::get() / 2) .. MaxValidators::::get(); // number of nominator intention. @@ -812,7 +812,7 @@ benchmarks! { let s in 1 .. 20; let validators = create_validators_with_nominators_for_era::( - v, n, T::MAX_NOMINATIONS as usize, false, None + v, n, T::NominationQuota::ABSOLUTE_MAXIMUM as usize, false, None )? .into_iter() .map(|v| T::Lookup::lookup(v).unwrap()) @@ -824,21 +824,56 @@ benchmarks! { let num_voters = (v + n) as usize; }: { - let voters = >::get_npos_voters(None); + let voters = >::get_npos_voters_unbounded(); assert_eq!(voters.len(), num_voters); } - get_npos_targets { + get_npos_voters_bounded { // number of validator intention. let v in (MaxValidators::::get() / 2) .. MaxValidators::::get(); // number of nominator intention. - let n = MaxNominators::::get(); + let n in (MaxNominators::::get() / 2) .. MaxNominators::::get(); + // total number of slashing spans. Assigned to validators randomly. + let s in 1 .. 20; + + let validators = create_validators_with_nominators_for_era::( + v, n, T::NominationQuota::ABSOLUTE_MAXIMUM as usize, false, None + )? + .into_iter() + .map(|v| T::Lookup::lookup(v).unwrap()) + .collect::>(); + + (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_bounded(SnapshotBounds::new_unbounded()); + assert_eq!(voters.len(), num_voters); + } + + get_npos_targets_unbounded { + // number of validator intention. + let v in (MaxValidators::::get() / 2) .. MaxValidators::::get(); + + let _ = create_validators_with_nominators_for_era::( + v, 0, 0, false, None + )?; + }: { + let targets = Validators::::iter().map(|(v, _)| v).collect::>(); + assert_eq!(targets.len() as u32, v); + } + + get_npos_targets_bounded { + // number of validator intention. + let v in (MaxValidators::::get() / 2) .. MaxValidators::::get(); let _ = create_validators_with_nominators_for_era::( - v, n, T::MAX_NOMINATIONS as usize, false, None + v, 0, 0, false, None )?; }: { - let targets = >::get_npos_targets(); + let targets = >::get_npos_targets_bounded(SnapshotBounds::new_unbounded()); assert_eq!(targets.len() as u32, v); } @@ -913,7 +948,8 @@ mod tests { create_validators_with_nominators_for_era::( v, n, - ::MAX_NOMINATIONS as usize, + <::NominationQuota as NominationQuota>>::ABSOLUTE_MAXIMUM + as usize, false, None, ) diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs index f2e5f0f783895..0a0e3571c57d0 100644 --- a/frame/staking/src/lib.rs +++ b/frame/staking/src/lib.rs @@ -307,8 +307,8 @@ use frame_support::{ use scale_info::TypeInfo; use sp_runtime::{ curve::PiecewiseLinear, - traits::{AtLeast32BitUnsigned, Convert, Saturating, Zero}, - Perbill, RuntimeDebug, + traits::{AtLeast32BitUnsigned, Convert, One, Saturating, UniqueSaturatedInto, Zero}, + Perbill, RuntimeDebug, SaturatedConversion, }; use sp_staking::{ offence::{Offence, OffenceError, ReportOffence}, @@ -808,6 +808,73 @@ where } } +/// Something that can dictate the maximum number of nominations per nominator. +pub trait NominationQuota { + /// The absolute maximum number that this trait can return. + // NOTE: this is useful for generating the compact solution type, i.e. + // `sp_npos_elections::generate_solution_type`. + const ABSOLUTE_MAXIMUM: u32; + + /// Determine the number of nominations that an account with `balance` at stake is allowed to + /// have. + fn nomination_quota(balance: Balance) -> u32; +} + +/// A nomination quota descriptor that allows `MAX` for all nominators. +pub struct FixedNominationQuota; +impl NominationQuota for FixedNominationQuota { + const ABSOLUTE_MAXIMUM: u32 = MAX; + + fn nomination_quota(_: Balance) -> u32 { + MAX + } +} + +/// An implementation of a linear nomination distribution. +/// +/// TODO: ideally we want this to be a const expression all the way through, no point in +/// re-computing this on the fly. For now we stick to a runtime placeholder code. +/// +/// Essentially, draws a line between `(MinBalance, MinQuota)` and `(MaxBalance, MaxQuota)`, Also, +/// values less than `MinBalance` have `MinQuota` and values higher than `MaxBalance` have +/// `MaxQuota`. +pub struct LinearNominationQuota( + sp_std::marker::PhantomData<(MinBalance, MaxBalance)>, +); +impl + NominationQuota for LinearNominationQuota +where + MinBalance: Get, + MaxBalance: Get, + Balance: From + + Copy + + UniqueSaturatedInto + + One + + Ord + + sp_std::ops::Sub + + sp_std::ops::Div, +{ + const ABSOLUTE_MAXIMUM: u32 = MAX_QUOTA; + + fn nomination_quota(balance: Balance) -> u32 { + if balance < MinBalance::get() { + MIN_QUOTA + } else if balance >= MinBalance::get() && balance < MaxBalance::get() { + let min_quota: Balance = MIN_QUOTA.into(); + let max_quota: Balance = MAX_QUOTA.into(); + // per this much balance, the quota increases per one (reverse of slope). + let balance_per_quota_inc = + (balance - MinBalance::get()) / (min_quota - max_quota).max(One::one()); + // this many increments happens to the quota. + let inc_steps = (balance - MinBalance::get()) / balance_per_quota_inc.max(One::one()); + + MIN_QUOTA + inc_steps.saturated_into::() + } else { + MAX_QUOTA + } + } +} + /// Configurations of the benchmarking of the pallet. pub trait BenchmarkingConfig { /// The maximum number of validators to use. diff --git a/frame/staking/src/mock.rs b/frame/staking/src/mock.rs index 0b1d6a06d9c7f..67007c808c43a 100644 --- a/frame/staking/src/mock.rs +++ b/frame/staking/src/mock.rs @@ -253,7 +253,6 @@ impl onchain::Config for Test { } impl crate::pallet::pallet::Config for Test { - const MAX_NOMINATIONS: u32 = 16; type Currency = Balances; type UnixTime = Timestamp; type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote; @@ -272,6 +271,7 @@ impl crate::pallet::pallet::Config for Test { type OffendingValidatorsThreshold = OffendingValidatorsThreshold; type ElectionProvider = onchain::OnChainSequentialPhragmen; type GenesisElectionProvider = Self::ElectionProvider; + type NominationQuota = FixedNominationQuota<16>; // NOTE: consider a macro and use `UseNominatorsMap` as well. type SortedListProvider = BagsList; type BenchmarkingConfig = TestBenchmarkingConfig; @@ -857,3 +857,11 @@ pub(crate) fn staking_events() -> Vec> { pub(crate) fn balances(who: &AccountId) -> (Balance, Balance) { (Balances::free_balance(who), Balances::reserved_balance(who)) } + +pub(crate) fn validator_ids() -> Vec { + Validators::::iter().map(|(v, _)| v).collect() +} + +pub(crate) fn nominator_ids() -> Vec { + Nominators::::iter().map(|(n, _)| n).collect() +} diff --git a/frame/staking/src/pallet/impls.rs b/frame/staking/src/pallet/impls.rs index cd26ff3b729c5..6dca74f819890 100644 --- a/frame/staking/src/pallet/impls.rs +++ b/frame/staking/src/pallet/impls.rs @@ -18,8 +18,8 @@ //! Implementations for the Staking FRAME Pallet. use frame_election_provider_support::{ - data_provider, ElectionDataProvider, ElectionProvider, SortedListProvider, Supports, - VoteWeight, VoteWeightProvider, + data_provider, ElectionDataProvider, ElectionProvider, SnapshotBounds, SortedListProvider, + Supports, VoteWeight, VoteWeightProvider, }; use frame_support::{ pallet_prelude::*, @@ -43,8 +43,8 @@ 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, + ExposureOf, Forcing, IndividualExposure, NominationQuota, Nominations, PositiveImbalanceOf, + RewardDestination, SessionInterface, StakingLedger, ValidatorPrefs, }; use super::{pallet::*, STAKING_ID}; @@ -647,120 +647,280 @@ impl Pallet { SlashRewardFraction::::put(fraction); } - /// Get all of the voters that are eligible for the npos election. + /// Get all of the voters that are eligible for the next npos election. /// - /// `maybe_max_len` can imposes a cap on the number of voters returned; First all the validator - /// are included in no particular order, then remainder is taken from the nominators, as - /// returned by [`Config::SortedListProvider`]. + /// This function is self-weighing as [`DispatchClass::Mandatory`]. + /// + /// ### Slashing + /// + /// All nominations that have been submitted before the last non-zero slash of the validator are + /// auto-chilled. /// - /// This will use nominators, and all the validators will inject a self vote. + /// # Warning + /// + /// This is the unbounded variant. Being called might cause a large number of storage reads. Use + /// [`get_npos_targets_bounded`] otherwise. + pub fn get_npos_voters_unbounded() -> Vec<(T::AccountId, VoteWeight, Vec)> { + let slashing_spans = >::iter().collect::>(); + let weight_of = Self::weight_of_fn(); + + let mut nominator_votes = T::SortedListProvider::iter() + .filter_map(|nominator| { + if let Some(Nominations { submitted_in, mut targets, suppressed: _ }) = + Self::nominators(nominator.clone()) + { + targets.retain(|validator_stash| { + slashing_spans + .get(validator_stash) + .map_or(true, |spans| submitted_in >= spans.last_nonzero_slash()) + }); + if !targets.is_empty() { + let weight = weight_of(&nominator); + return Some((nominator, weight, targets)) + } + } + None + }) + .collect::>(); + + let validator_votes = >::iter() + .map(|(v, _)| (v.clone(), Self::weight_of(&v), vec![v.clone()])) + .collect::>(); + + Self::register_weight(T::WeightInfo::get_npos_voters_unbounded( + validator_votes.len() as u32, + nominator_votes.len() as u32, + slashing_spans.len() as u32, + )); + log!( + info, + "generated {} npos voters, {} from validators and {} nominators, without any bounds", + validator_votes.len() + nominator_votes.len(), + validator_votes.len(), + nominator_votes.len(), + ); + + // NOTE: we chain the one we expect to have the smaller size (`validators_votes`) to the + // larger one, to minimize copying. Ideally we would collect only once, but sadly then we + // wouldn't have access to a cheap `.len()`, which we need for weighing. Only other option + // would have been using the counters, but since we entirely avoid reading them, we better + // stick to that. + nominator_votes.extend(validator_votes); + nominator_votes + } + + /// Get all of the voters that are eligible for the next npos election. + /// + /// `bounds` imposes a cap on the count and byte-size of the entire vector returned. + /// + /// As of now, first all the validator are included in no particular order, then remainder is + /// taken from the nominators, as returned by [`Config::SortedListProvider`]. /// /// This function is self-weighing as [`DispatchClass::Mandatory`]. /// /// ### Slashing /// /// All nominations that have been submitted before the last non-zero slash of the validator are - /// auto-chilled, but still count towards the limit imposed by `maybe_max_len`. - pub fn get_npos_voters( - maybe_max_len: Option, + /// auto-chilled, and they **DO** count towards the limit imposed by `bounds`. To prevent this + /// from getting in the way, [`update_slashed_nominator`] can be used to clean these stale + /// nominations. + pub fn get_npos_voters_bounded( + bounds: SnapshotBounds, ) -> Vec<(T::AccountId, VoteWeight, Vec)> { - let max_allowed_len = { - let nominator_count = CounterForNominators::::get() as usize; - let validator_count = CounterForValidators::::get() as usize; - let all_voter_count = validator_count.saturating_add(nominator_count); - maybe_max_len.unwrap_or(all_voter_count).min(all_voter_count) + let mut tracker = StaticSizeTracker::::new(); + let mut voters = { + // we take the maximum voter size, to do the minimum allocation. Note that we prefer not + // over-allocating. + let maximum_voter_size = StaticSizeTracker::::voter_size( + T::NominationQuota::ABSOLUTE_MAXIMUM as usize, + ); + if let Some(capacity) = bounds.predict_capacity(maximum_voter_size) { + Vec::with_capacity(capacity.min(sp_core::MAX_POSSIBLE_ALLOCATION as usize / 2)) + } else { + Vec::new() + } }; - let mut all_voters = Vec::<_>::with_capacity(max_allowed_len); + // we create two closures to make us agnostic of the type of `bounds` that we are dealing + // with. This still not the optimum. The most performant option would have been having a + // dedicated function for each variant. For example, in the current code, if `bounds` + // count-bounded, the static size tracker is allocated for no reason. Nonetheless, it is + // not actually tracking anything if it is not needed. This is as good as it gets without + // creating too much duplicate code. + + // register a voter with `votes` with regards to bounds. + let add_voter = |tracker_ref: &mut StaticSizeTracker, votes: usize| { + if let Some(_) = bounds.size_bound() { + tracker_ref.register_voter(votes) + } else if let Some(_) = bounds.count_bound() { + // nothing needed, voters.len() is itself a representation of the count. + } + }; - // first, grab all validators in no particular order, capped by the maximum allowed length. - let mut validators_taken = 0u32; - for (validator, _) in >::iter().take(max_allowed_len) { - // Append self vote. + // check if adding one more voter will exhaust any of the bounds. + let next_will_exhaust = |tracker_ref: &StaticSizeTracker, + voters_ref: &Vec<_>| match ( + bounds.size_bound(), + bounds.count_bound(), + ) { + (Some(max_size), Some(max_count)) => + tracker_ref.final_byte_size_of(voters_ref.len().saturating_add(1)) > max_size || + voters_ref.len().saturating_add(1) > max_count, + (Some(max_size), None) => + tracker_ref.final_byte_size_of(voters_ref.len().saturating_add(1)) > max_size, + (None, Some(max_count)) => voters_ref.len().saturating_add(1) > max_count, + (None, None) => { + debug_assert!(false, "unreachable code"); + false + }, + }; + + // first, grab all validators in no particular order. In most cases, all of them should fit + // anyway. + for (validator, _) in >::iter() { let self_vote = (validator.clone(), Self::weight_of(&validator), vec![validator.clone()]); - all_voters.push(self_vote); - validators_taken.saturating_inc(); + add_voter(&mut tracker, 1); + if next_will_exhaust(&tracker, &voters) { + log!( + warn, + "stopped iterating over validators' self-vote at {} due to bound {:?}", + voters.len(), + bounds, + ); + break + } + voters.push(self_vote); } + let validators_taken = voters.len(); - // .. and grab whatever we have left from nominators. - let nominators_quota = (max_allowed_len as u32).saturating_sub(validators_taken); - let slashing_spans = >::iter().collect::>(); - - // track the count of nominators added to `all_voters - let mut nominators_taken = 0u32; - // track every nominator iterated over, but not necessarily added to `all_voters` - let mut nominators_seen = 0u32; + // only bother with reading the slashing spans et.al. if we are not exhausted. + let slashing_spans_read = if !next_will_exhaust(&tracker, &voters) { + let slashing_spans = >::iter().collect::>(); + let weight_of = Self::weight_of_fn(); - // cache the total-issuance once in this function - let weight_of = Self::weight_of_fn(); - - let mut nominators_iter = T::SortedListProvider::iter(); - while nominators_taken < nominators_quota && nominators_seen < nominators_quota * 2 { - let nominator = match nominators_iter.next() { - Some(nominator) => { - nominators_seen.saturating_inc(); - nominator - }, - None => break, - }; + for nominator in T::SortedListProvider::iter() { + if let Some(Nominations { submitted_in, mut targets, suppressed: _ }) = + >::get(&nominator) + { + // IMPORTANT: we track the size and potentially break out right here. This + // ensures that votes that are invalid will also affect the snapshot bounds. + // Chain operators should ensure `update_slashed_nominator` is used to eliminate + // the need for this. + add_voter(&mut tracker, targets.len()); + if next_will_exhaust(&tracker, &voters) { + break + } - if let Some(Nominations { submitted_in, mut targets, suppressed: _ }) = - >::get(&nominator) - { - log!( - trace, - "fetched nominator {:?} with weight {:?}", - nominator, - weight_of(&nominator) - ); - targets.retain(|stash| { - slashing_spans - .get(stash) - .map_or(true, |spans| submitted_in >= spans.last_nonzero_slash()) - }); - if !targets.len().is_zero() { - all_voters.push((nominator.clone(), weight_of(&nominator), targets)); - nominators_taken.saturating_inc(); + targets.retain(|stash| { + slashing_spans + .get(stash) + .map_or(true, |spans| submitted_in >= spans.last_nonzero_slash()) + }); + if !targets.is_empty() { + voters.push((nominator.clone(), weight_of(&nominator), targets)); + } + } else { + log!(error, "DEFENSIVE: invalid item in `SortedListProvider`: {:?}", nominator) } - } else { - log!(error, "DEFENSIVE: invalid item in `SortedListProvider`: {:?}", nominator) } - } - - // all_voters should have not re-allocated. - debug_assert!(all_voters.capacity() == max_allowed_len); + slashing_spans.len() as u32 + } else { + Zero::zero() + }; - Self::register_weight(T::WeightInfo::get_npos_voters( - validators_taken, - nominators_taken, - slashing_spans.len() as u32, + let nominators_taken = voters.len().saturating_sub(validators_taken); + Self::register_weight(T::WeightInfo::get_npos_voters_bounded( + validators_taken as u32, + nominators_taken as u32, + slashing_spans_read, )); + debug_assert!( + !bounds.exhausts_size_count_non_zero( + || voters.encoded_size() as u32, + || voters.len() as u32 + ), + "{} voters, size {}, exhausted {:?}", + voters.len(), + voters.encoded_size(), + bounds, + ); + log!( info, - "generated {} npos voters, {} from validators and {} nominators", - all_voters.len(), + "generated {} npos voters, {} from validators and {} nominators, with bound {:?}", + voters.len(), validators_taken, - nominators_taken + nominators_taken, + bounds, ); - all_voters + voters } - /// Get the targets for an upcoming npos election. + /// Get the list of targets (validators) that are eligible for the next npos election. /// /// This function is self-weighing as [`DispatchClass::Mandatory`]. - pub fn get_npos_targets() -> Vec { - let mut validator_count = 0u32; - let targets = Validators::::iter() - .map(|(v, _)| { - validator_count.saturating_inc(); - v - }) - .collect::>(); + /// + /// # Warning + /// + /// This is the unbounded variant. Being called might cause a large number of storage reads. Use + /// [`get_npos_targets_bounded`] otherwise. + pub fn get_npos_targets_unbounded() -> Vec { + let targets = Validators::::iter().map(|(v, _)| v).collect::>(); + Self::register_weight(T::WeightInfo::get_npos_targets_unbounded(targets.len() as u32)); + log!(info, "generated {} npos targets, without bounds.", targets.len()); + targets + } + + /// Get the list of targets (validators) that are eligible for the next npos election. + /// + /// `bounds` imposes a cap on the count and byte-size of the entire targets returned. + /// + /// This function is self-weighing as [`DispatchClass::Mandatory`]. + pub fn get_npos_targets_bounded(bounds: SnapshotBounds) -> Vec { + let mut internal_size: usize = Zero::zero(); + let mut targets: Vec = if let Some(capacity) = + bounds.predict_capacity(sp_std::mem::size_of::()) + { + Vec::with_capacity(capacity.min(sp_core::MAX_POSSIBLE_ALLOCATION as usize / 2)) + } else { + Vec::new() + }; - Self::register_weight(T::WeightInfo::get_npos_targets(validator_count)); + let next_will_exhaust = + |new_final_size, new_count| match (bounds.size_bound(), bounds.count_bound()) { + (Some(size_bound), Some(count_bound)) => + new_final_size > size_bound || new_count > count_bound, + (Some(size_bound), None) => new_final_size > size_bound, + (None, Some(count_bound)) => new_count > count_bound, + (None, None) => false, + }; + + for (next, _) in Validators::::iter() { + // NOTE: rather sub-optimal, we should not need to track size if it is not bounded, but + // in this case we prefer not cluttering the code. + let new_internal_size = internal_size + sp_std::mem::size_of::(); + let new_final_size = new_internal_size + + StaticSizeTracker::::length_prefix(targets.len().saturating_add(1)); + let new_count = targets.len().saturating_add(1); + if next_will_exhaust(new_final_size, new_count) { + // we've had enough + break + } + targets.push(next); + internal_size = new_internal_size; + + debug_assert_eq!(targets.encoded_size(), new_final_size); + } + Self::register_weight(T::WeightInfo::get_npos_targets_bounded(targets.len() as u32)); + debug_assert!(!bounds.exhausts_size_count_non_zero( + || targets.encoded_size() as u32, + || targets.len() as u32 + )); + + log!(info, "generated {} npos targets, with bounds {:?}", targets.len(), bounds); targets } @@ -855,7 +1015,7 @@ impl Pallet { } impl ElectionDataProvider> for Pallet { - const MAXIMUM_VOTES_PER_VOTER: u32 = T::MAX_NOMINATIONS; + const MAXIMUM_VOTES_PER_VOTER: u32 = T::NominationQuota::ABSOLUTE_MAXIMUM; fn desired_targets() -> data_provider::Result { Self::register_weight(T::DbWeight::get().reads(1)); @@ -863,32 +1023,33 @@ impl ElectionDataProvider> for Pallet } fn voters( - maybe_max_len: Option, + bounds: SnapshotBounds, ) -> data_provider::Result)>> { - debug_assert!(>::iter().count() as u32 == CounterForNominators::::get()); - debug_assert!(>::iter().count() as u32 == CounterForValidators::::get()); - debug_assert_eq!( - CounterForNominators::::get(), - T::SortedListProvider::count(), - "voter_count must be accurate", - ); - - // This can never fail -- if `maybe_max_len` is `Some(_)` we handle it. - let voters = Self::get_npos_voters(maybe_max_len); - debug_assert!(maybe_max_len.map_or(true, |max| voters.len() <= max)); - - Ok(voters) + Ok(if bounds.is_unbounded() { + log!( + warn, + "iterating over an unbounded number of npos voters, this might exhaust the \ + memory limits of the chain. Ensure proper limits are set via \ + `MaxNominatorsCount` or `ElectionProvider`" + ); + Self::get_npos_voters_unbounded() + } else { + Self::get_npos_voters_bounded(bounds) + }) } - fn targets(maybe_max_len: Option) -> data_provider::Result> { - let target_count = CounterForValidators::::get(); - - // We can't handle this case yet -- return an error. - if maybe_max_len.map_or(false, |max_len| target_count > max_len as u32) { - return Err("Target snapshot too big") - } - - Ok(Self::get_npos_targets()) + fn targets(bounds: SnapshotBounds) -> data_provider::Result> { + Ok(if bounds.is_unbounded() { + log!( + warn, + "iterating over an unbounded number of npos targets, this might exhaust the \ + memory limits of the chain. Ensure proper limits are set via \ + `MaxValidatorsCount` or `ElectionProvider`" + ); + Self::get_npos_targets_unbounded() + } else { + Self::get_npos_targets_bounded(bounds) + }) } fn next_election_prediction(now: T::BlockNumber) -> T::BlockNumber { @@ -1316,3 +1477,183 @@ impl SortedListProvider for UseNominatorsMap { CounterForNominators::::take(); } } + +/// A static tracker for the snapshot of all voters. +/// +/// Computes the (SCALE) encoded byte length of a snapshot based on static rules, without any actual +/// encoding. +/// +/// ## Details +/// +/// The snapshot has a the form `Vec` where `Voter = (Account, u64, Vec)`. For each +/// voter added to the snapshot, [`register_voter`] should be called, with the number of votes +/// (length of the internal `Vec`). +/// +/// Whilst doing this, [`size`] will track the entire size of the `Vec`, except for the +/// length prefix of the outer `Vec`. To get the final size at any point, use +/// [`final_byte_size_of`]. +pub(crate) struct StaticSizeTracker { + size: usize, + _marker: sp_std::marker::PhantomData, +} + +impl StaticSizeTracker { + fn new() -> Self { + Self { size: 0, _marker: Default::default() } + } + + /// The length prefix of a vector with the given length. + #[inline] + pub(crate) fn length_prefix(length: usize) -> usize { + use codec::{Compact, CompactLen}; + let length = length as u32; + Compact::::compact_len(&length) + } + + /// Register a voter in `self` who has casted `votes`. + pub(crate) fn register_voter(&mut self, votes: usize) { + self.size = self.size.saturating_add(Self::voter_size(votes)) + } + + /// The byte size of a voter who casted `votes`. + pub(crate) fn voter_size(votes: usize) -> usize { + Self::length_prefix(votes) + // and each element + .saturating_add(votes * sp_std::mem::size_of::()) + // 1 vote-weight + .saturating_add(sp_std::mem::size_of::()) + // 1 voter account + .saturating_add(sp_std::mem::size_of::()) + } + + // Final size: size of all internal elements, plus the length prefix. + pub(crate) fn final_byte_size_of(&self, length: usize) -> usize { + self.size + Self::length_prefix(length) + } +} + +#[cfg(test)] +mod static_tracker { + use codec::Encode; + + use super::StaticSizeTracker; + + #[test] + fn len_prefix_works() { + let length_samples = + vec![0usize, 1, 62, 63, 64, 16383, 16384, 16385, 1073741822, 1073741823, 1073741824]; + + for s in length_samples { + // the encoded size of a vector of n bytes should be n + the length prefix + assert_eq!(vec![1u8; s].encoded_size(), StaticSizeTracker::::length_prefix(s) + s); + } + } + + #[test] + fn length_tracking_works() { + let mut voters: Vec<(u64, u64, Vec)> = vec![]; + let mut tracker = StaticSizeTracker::::new(); + + // initial state. + assert_eq!(voters.encoded_size(), tracker.final_byte_size_of(voters.len())); + + // add a bunch of stuff. + voters.push((1, 10, vec![1, 2, 3])); + tracker.register_voter(3); + assert_eq!(voters.encoded_size(), tracker.final_byte_size_of(voters.len())); + + voters.push((2, 20, vec![1, 3])); + tracker.register_voter(2); + assert_eq!(voters.encoded_size(), tracker.final_byte_size_of(voters.len())); + + voters.push((3, 30, vec![1])); + tracker.register_voter(1); + assert_eq!(voters.encoded_size(), tracker.final_byte_size_of(voters.len())); + + // unlikely to happen in reality, but anyways. + voters.push((4, 40, vec![])); + tracker.register_voter(0); + assert_eq!(voters.encoded_size(), tracker.final_byte_size_of(voters.len())); + } +} + +/// A helper function that does nothing other than return some information about the range at which +/// the given `bounds` works. +/// +/// Will print and return as a tuple as `(low, mid, high)`, where: +/// +/// - `low` is the minimum number of voters that `bounds` supports. This will be realized when all +/// voters use [`T::NominationQuota::ABSOLUTE_MAXIMUM`] votes. +/// - `how` is the maximum number of voters that `bounds` supports. This will be realized when all +/// voters use `1` votes. +/// - `mid` is the the average of the above two. This will be realized when all voters use +/// `[`T::NominationQuota::ABSOLUTE_MAXIMUM`] / 2` votes. +#[cfg(feature = "std")] +pub fn display_bounds_limits(bounds: SnapshotBounds) -> (usize, usize, usize) { + match (bounds.size_bound(), bounds.count_bound()) { + (None, None) => { + println!("{:?} is unbounded", bounds); + (Bounded::max_value(), Bounded::max_value(), Bounded::max_value()) + }, + (None, Some(count)) => { + println!("{:?} can have exactly maximum {} voters", bounds, count); + (count, count, count) + }, + (Some(size), Some(count)) => { + // maximum number of voters, it means that they all voted 1. + let max_voters = { + let voter_size = StaticSizeTracker::::voter_size(1); + // assuming that the length prefix is 4 bytes. + (size.saturating_sub(4) / voter_size).min(count) + }; + // minimum number of voters, it means that they all voted maximum. + let min_voters = { + let voter_size = StaticSizeTracker::::voter_size( + T::NominationQuota::ABSOLUTE_MAXIMUM as usize, + ); + (size.saturating_sub(4) / voter_size).min(count) + }; + // average of the above two. + let average_voters = { + let voter_size = StaticSizeTracker::::voter_size( + T::NominationQuota::ABSOLUTE_MAXIMUM as usize / 2, + ); + // assuming that the length prefix is 4 bytes. + (size.saturating_sub(4) / voter_size).min(count) + }; + println!( + "{:?} will be in [low, mid, high]: [{}, {}, {}]", + bounds, min_voters, average_voters, max_voters + ); + (min_voters, average_voters, max_voters) + }, + (Some(size), None) => { + // maximum number of voters, it means that they all voted 1. + let max_voters = { + let voter_size = StaticSizeTracker::::voter_size(1); + // assuming that the length prefix is 4 bytes. + size.saturating_sub(4) / voter_size + }; + // minimum number of voters, it means that they all voted maximum. + let min_voters = { + let voter_size = StaticSizeTracker::::voter_size( + T::NominationQuota::ABSOLUTE_MAXIMUM as usize, + ); + size.saturating_sub(4) / voter_size + }; + // average of the above two. + let average_voters = { + let voter_size = StaticSizeTracker::::voter_size( + T::NominationQuota::ABSOLUTE_MAXIMUM as usize / 2, + ); + // assuming that the length prefix is 4 bytes. + size.saturating_sub(4) / voter_size + }; + println!( + "{:?} will be in [low, mid, high]: [{}, {}, {}]", + bounds, min_voters, average_voters, max_voters + ); + (min_voters, average_voters, max_voters) + }, + } +} diff --git a/frame/staking/src/pallet/mod.rs b/frame/staking/src/pallet/mod.rs index f7e96ce0cf765..4c74a49396d8e 100644 --- a/frame/staking/src/pallet/mod.rs +++ b/frame/staking/src/pallet/mod.rs @@ -40,9 +40,9 @@ pub use impls::*; use crate::{ log, migrations, slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, EraIndex, EraPayout, - EraRewardPoints, Exposure, Forcing, NegativeImbalanceOf, Nominations, PositiveImbalanceOf, - Releases, RewardDestination, SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk, - ValidatorPrefs, + EraRewardPoints, Exposure, Forcing, NegativeImbalanceOf, NominationQuota, Nominations, + PositiveImbalanceOf, Releases, RewardDestination, SessionInterface, StakingLedger, + UnappliedSlash, UnlockChunk, ValidatorPrefs, }; pub const MAX_UNLOCKING_CHUNKS: usize = 32; @@ -92,9 +92,6 @@ pub mod pallet { DataProvider = Pallet, >; - /// Maximum number of nominations per nominator. - const MAX_NOMINATIONS: u32; - /// Tokens have been minted and are unused for validator-reward. /// See [Era payout](./index.html#era-payout). type RewardRemainder: OnUnbalanced>; @@ -153,6 +150,9 @@ pub mod pallet { /// the bags-list is not desired, [`impls::UseNominatorsMap`] is likely the desired option. type SortedListProvider: SortedListProvider; + /// Something that can dictate the number of nominations allowed per nominator. + type NominationQuota: NominationQuota>; + /// Some parameters of the benchmarking. type BenchmarkingConfig: BenchmarkingConfig; @@ -162,10 +162,9 @@ pub mod pallet { #[pallet::extra_constants] impl Pallet { - // TODO: rename to snake case after https://github.com/paritytech/substrate/issues/8826 fixed. - #[allow(non_snake_case)] - fn MaxNominations() -> u32 { - T::MAX_NOMINATIONS + #[pallet::constant_name(MaxNominations)] + fn max_nominations() -> u32 { + T::NominationQuota::ABSOLUTE_MAXIMUM } } @@ -1003,12 +1002,6 @@ pub mod pallet { /// Effects will be felt at the beginning of the next era. /// /// The dispatch origin for this call must be _Signed_ by the controller, not the stash. - /// - /// # - /// - The transaction's complexity is proportional to the size of `targets` (N) - /// which is capped at CompactAssignments::LIMIT (MAX_NOMINATIONS). - /// - Both the reads and writes follow a similar pattern. - /// # #[pallet::weight(T::WeightInfo::nominate(targets.len() as u32))] pub fn nominate( origin: OriginFor, @@ -1034,7 +1027,14 @@ pub mod pallet { } ensure!(!targets.is_empty(), Error::::EmptyTargets); - ensure!(targets.len() <= T::MAX_NOMINATIONS as usize, Error::::TooManyTargets); + + ensure!( + targets.len() as u32 <= + T::NominationQuota::nomination_quota(Self::slashable_balance_of( + &ledger.stash + )), + Error::::TooManyTargets + ); let old = Nominators::::get(stash).map_or_else(Vec::new, |x| x.targets); @@ -1635,6 +1635,34 @@ pub mod pallet { Self::chill_stash(&stash); Ok(()) } + + /// Update a nominator's nomination, if some of their nomination targets have been slashed. + /// + /// These votes are effectively being stripped in the election process. Removing them via + /// this extrinsic will be a pre-emptive cleanup that benefits the chain. + /// + /// Returns the transaction fee upon successful execution. + #[pallet::weight(0)] + pub fn update_slashed_nominator( + origin: OriginFor, + stash: T::AccountId, + ) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin); + Nominators::::try_mutate(stash, |maybe_nomination| { + if let Some(Nominations { targets, submitted_in, suppressed: _ }) = maybe_nomination + { + let initial_len = targets.len(); + targets.retain(|v| { + SlashingSpans::::get(v) + .map_or(true, |spans| *submitted_in >= spans.last_nonzero_slash()) + }); + if initial_len != targets.len() { + return Ok(Pays::No.into()) + } + } + Err("not slashed".into()) + }) + } } } diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs index 4604351b52305..6911b96bd46e8 100644 --- a/frame/staking/src/tests.rs +++ b/frame/staking/src/tests.rs @@ -1900,8 +1900,8 @@ fn bond_with_duplicate_vote_should_be_ignored_by_election_provider() { assert_eq!( supports, vec![ - (21, Support { total: 1800, voters: vec![(21, 1000), (1, 400), (3, 400)] }), - (31, Support { total: 2200, voters: vec![(31, 1000), (1, 600), (3, 600)] }) + (21, Support { total: 1800, voters: vec![(1, 400), (3, 400), (21, 1000)] }), + (31, Support { total: 2200, voters: vec![(1, 600), (3, 600), (31, 1000)] }) ], ); }); @@ -1944,8 +1944,8 @@ fn bond_with_duplicate_vote_should_be_ignored_by_election_provider_elected() { assert_eq!( supports, vec![ - (11, Support { total: 1500, voters: vec![(11, 1000), (1, 500)] }), - (21, Support { total: 2500, voters: vec![(21, 1000), (1, 500), (3, 1000)] }) + (11, Support { total: 1500, voters: vec![(1, 500), (11, 1000)] }), + (21, Support { total: 2500, voters: vec![(1, 500), (3, 1000), (21, 1000)] }) ], ); }); @@ -3954,18 +3954,71 @@ fn on_finalize_weight_is_nonzero() { mod election_data_provider { use super::*; - use frame_election_provider_support::ElectionDataProvider; + use frame_election_provider_support::{ + ElectionDataProvider, SnapshotBounds, SnapshotBoundsBuilder, + }; - #[test] - fn targets_2sec_block() { - let mut validators = 1000; - while ::WeightInfo::get_npos_targets(validators) < - 2 * frame_support::weights::constants::WEIGHT_PER_SECOND - { - validators += 1; + fn all_voters_count() -> u32 { + CounterForValidators::::get() + CounterForNominators::::get() + } + + fn limit_for_voters(maybe_size: Option, maybe_count: Option) -> SnapshotBounds { + let mut builder = SnapshotBoundsBuilder::default(); + if let Some(size) = maybe_size { + let all_voters = Staking::voters(SnapshotBounds::new_unbounded()).unwrap(); + let size_bound = if size <= all_voters.len() { + all_voters.into_iter().take(size).collect::>().encoded_size() as u32 + } else { + let excess = size - all_voters.len(); + let base = all_voters.encoded_size(); + let per_item = base / all_voters.len(); + (base + per_item * excess) as u32 + }; + builder = builder.size(size_bound) + } + + if let Some(count) = maybe_count { + builder = builder.count(count as u32) } - println!("Can create a snapshot of {} validators in 2sec block", validators); + builder.build() + } + + fn limit_for_targets(maybe_size: Option, maybe_count: Option) -> SnapshotBounds { + let mut builder = SnapshotBoundsBuilder::default(); + if let Some(size) = maybe_size { + let all_targets = Staking::targets(SnapshotBounds::new_unbounded()).unwrap(); + let size_bound = if size <= all_targets.len() { + dbg!(all_targets.iter().take(size).collect::>().encoded_size() as u32) + } else { + let excess = size - all_targets.len(); + let base = all_targets.encoded_size(); + let per_item = base / all_targets.len(); + (base + per_item * excess) as u32 + }; + builder = builder.size(size_bound) + } + if let Some(count) = maybe_count { + builder = builder.count(count as u32); + } + + builder.build() + } + + fn count_limit_for_voters(count: usize) -> SnapshotBounds { + limit_for_voters(None, Some(count)) + } + + fn size_limit_for_voters(size: usize) -> SnapshotBounds { + limit_for_voters(Some(size), None) + } + + fn count_limit_for_targets(count: usize) -> SnapshotBounds { + limit_for_targets(None, Some(count)) + } + + fn size_limit_for_targets(size: usize) -> SnapshotBounds { + limit_for_targets(Some(size), None) } #[test] @@ -3977,8 +4030,11 @@ mod election_data_provider { let slashing_spans = validators; let mut nominators = 1000; - while ::WeightInfo::get_npos_voters(validators, nominators, slashing_spans) < - 2 * frame_support::weights::constants::WEIGHT_PER_SECOND + while ::WeightInfo::get_npos_voters_bounded( + validators, + nominators, + slashing_spans, + ) < 2 * frame_support::weights::constants::WEIGHT_PER_SECOND { nominators += 1; } @@ -3990,168 +4046,512 @@ mod election_data_provider { } #[test] - fn voters_include_self_vote() { - ExtBuilder::default().nominate(false).build_and_execute(|| { - assert!(>::iter().map(|(x, _)| x).all(|v| Staking::voters(None) + fn estimate_next_election_works() { + ExtBuilder::default().session_per_era(5).period(5).build_and_execute(|| { + // first session is always length 0. + for b in 1..20 { + run_to_block(b); + assert_eq!(Staking::next_election_prediction(System::block_number()), 20); + } + + // election + run_to_block(20); + assert_eq!(Staking::next_election_prediction(System::block_number()), 45); + assert_eq!(staking_events().len(), 1); + assert_eq!(*staking_events().last().unwrap(), Event::StakersElected); + + for b in 21..45 { + run_to_block(b); + assert_eq!(Staking::next_election_prediction(System::block_number()), 45); + } + + // election + run_to_block(45); + assert_eq!(Staking::next_election_prediction(System::block_number()), 70); + assert_eq!(staking_events().len(), 3); + assert_eq!(*staking_events().last().unwrap(), Event::StakersElected); + + Staking::force_no_eras(Origin::root()).unwrap(); + assert_eq!(Staking::next_election_prediction(System::block_number()), u64::MAX); + + Staking::force_new_era_always(Origin::root()).unwrap(); + assert_eq!(Staking::next_election_prediction(System::block_number()), 45 + 5); + + Staking::force_new_era(Origin::root()).unwrap(); + assert_eq!(Staking::next_election_prediction(System::block_number()), 45 + 5); + + // Do a fail election + MinimumValidatorCount::::put(1000); + run_to_block(50); + // Election: failed, next session is a new election + assert_eq!(Staking::next_election_prediction(System::block_number()), 50 + 5); + // The new era is still forced until a new era is planned. + assert_eq!(ForceEra::::get(), Forcing::ForceNew); + + MinimumValidatorCount::::put(2); + run_to_block(55); + assert_eq!(Staking::next_election_prediction(System::block_number()), 55 + 25); + assert_eq!(staking_events().len(), 6); + assert_eq!(*staking_events().last().unwrap(), Event::StakersElected); + // The new era has been planned, forcing is changed from `ForceNew` to `NotForcing`. + assert_eq!(ForceEra::::get(), Forcing::NotForcing); + }) + } + + mod bounded { + use super::*; + + #[test] + fn voters_include_self_vote() { + ExtBuilder::default().nominate(false).build_and_execute(|| { + assert!(>::iter().map(|(x, _)| x).all(|v| Staking::voters( + SnapshotBounds::new_count(all_voters_count()) + ) .unwrap() .into_iter() .find(|(w, _, t)| { v == *w && t[0] == *w }) .is_some())) - }) - } + }) + } - #[test] - fn voters_exclude_slashed() { - ExtBuilder::default().build_and_execute(|| { - assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); - assert_eq!( - >::voters(None) + #[test] + fn voters_exclude_slashed() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); + assert_eq!( + >::voters( + SnapshotBounds::new_count(all_voters_count()) + ) .unwrap() .iter() .find(|x| x.0 == 101) .unwrap() .2, - vec![11, 21] - ); + vec![11, 21] + ); - start_active_era(1); - add_slash(&11); + start_active_era(1); + add_slash(&11); - // 11 is gone. - start_active_era(2); - assert_eq!( - >::voters(None) + // 11 is gone. + start_active_era(2); + assert_eq!( + >::voters( + SnapshotBounds::new_count(all_voters_count()) + ) .unwrap() .iter() .find(|x| x.0 == 101) .unwrap() .2, - vec![21] - ); + vec![21] + ); - // resubmit and it is back - assert_ok!(Staking::nominate(Origin::signed(100), vec![11, 21])); - assert_eq!( - >::voters(None) + // resubmit and it is back + assert_ok!(Staking::nominate(Origin::signed(100), vec![11, 21])); + assert_eq!( + >::voters( + SnapshotBounds::new_count(all_voters_count()) + ) .unwrap() .iter() .find(|x| x.0 == 101) .unwrap() .2, - vec![11, 21] - ); - }) - } - - #[test] - fn respects_snapshot_len_limits() { - ExtBuilder::default() - .set_status(41, StakerStatus::Validator) - .build_and_execute(|| { - // sum of all nominators who'd be voters (1), plus the self-votes (4). - assert_eq!( - ::SortedListProvider::count() + - >::iter().count() as u32, - 5 + vec![11, 21] ); + }) + } - // if limits is less.. - assert_eq!(Staking::voters(Some(1)).unwrap().len(), 1); + #[test] + fn get_npos_voters_works_size_limit() { + ExtBuilder::default() + .set_status(41, StakerStatus::Validator) + .build_and_execute(|| { + // unbounded: + assert_eq!( + Staking::voters(SnapshotBounds::new_unbounded()).unwrap(), + vec![ + (101, 500, vec![11, 21]), // 8 + 8 + (8 * 2) + 1 = 33 + (31, 500, vec![31]), // 8 + 8 + 8 + 1 = 25 + (41, 1000, vec![41]), + (21, 1000, vec![21]), + (11, 1000, vec![11]), + ] + ); + + // if limits is less.. + assert_eq!(Staking::voters(SnapshotBounds::new_size(0)).unwrap().len(), 0); + + // let's check one of the manually for some mental practice + assert_eq!(size_limit_for_voters(2).size_bound().unwrap(), 33 + 25 + 1); + assert_eq!(Staking::voters(size_limit_for_voters(2)).unwrap().len(), 2); + + // edge-case: we have enough size only for all validators, and none of the + // nominators. + let limit_validators = + size_limit_for_voters(>::iter().count()); + let voters = Staking::voters(limit_validators).unwrap(); + assert_eq!(voters.len(), 4); + assert!(Validators::::iter() + .all(|(v, _)| voters.iter().filter(|x| x.0 == v).count() == 1)); + + // if limit is equal.. + assert_eq!(Staking::voters(size_limit_for_voters(5)).unwrap().len(), 5); + + // if limit is more. + assert_eq!(Staking::voters(size_limit_for_voters(10)).unwrap().len(), 5); + }); + } - // if limit is equal.. - assert_eq!(Staking::voters(Some(5)).unwrap().len(), 5); + #[test] + fn get_npos_voters_works_count_limit() { + ExtBuilder::default() + .set_status(41, StakerStatus::Validator) + .build_and_execute(|| { + // sum of all nominators who'd be voters (1), plus the self-votes (4). + assert_eq!( + ::SortedListProvider::count() + + >::iter().count() as u32, + 5 + ); + + // unbounded: + assert_eq!(Staking::voters(SnapshotBounds::new_unbounded()).unwrap().len(), 5); + + // if less.. + assert_eq!(Staking::voters(count_limit_for_voters(0)).unwrap().len(), 0); + assert_eq!(Staking::voters(count_limit_for_voters(2)).unwrap().len(), 2); + + // only validators edge case.. + let voters = Staking::voters(count_limit_for_voters(4)).unwrap(); + assert_eq!(voters.len(), 4); + assert!(Validators::::iter() + .all(|(v, _)| voters.iter().filter(|x| x.0 == v).count() == 1)); + + // equal.. + assert_eq!(Staking::voters(count_limit_for_voters(5)).unwrap().len(), 5); + + // and finally more. + assert_eq!(Staking::voters(count_limit_for_voters(7)).unwrap().len(), 5); + }); + } - // if limit is more. - assert_eq!(Staking::voters(Some(55)).unwrap().len(), 5); + #[test] + fn get_npos_voters_works_hybrid_limit() { + ExtBuilder::default() + .set_status(41, StakerStatus::Validator) + .build_and_execute(|| { + // sum of all nominators who'd be voters (1), plus the self-votes (4). + assert_eq!( + ::SortedListProvider::count() + + >::iter().count() as u32, + 5 + ); + + // if less.. + assert_eq!( + Staking::voters(limit_for_voters(Some(0), Some(0))).unwrap().len(), + 0 + ); + assert_eq!( + Staking::voters(limit_for_voters(Some(0), Some(2))).unwrap().len(), + 0 + ); + assert_eq!( + Staking::voters(limit_for_voters(Some(2), Some(0))).unwrap().len(), + 0 + ); + + assert_eq!( + Staking::voters(limit_for_voters(Some(2), Some(1))).unwrap().len(), + 1 + ); + assert_eq!( + Staking::voters(limit_for_voters(Some(1), Some(1))).unwrap().len(), + 1 + ); + assert_eq!( + Staking::voters(limit_for_voters(Some(1), Some(2))).unwrap().len(), + 1 + ); + + assert_eq!( + Staking::voters(limit_for_voters(Some(2), Some(2))).unwrap().len(), + 2 + ); + + // only validators edge case.. + let v1 = Staking::voters(limit_for_voters(Some(4), Some(4))).unwrap(); + let v2 = Staking::voters(limit_for_voters(Some(4), Some(5))).unwrap(); + let v3 = Staking::voters(limit_for_voters(Some(5), Some(4))).unwrap(); + assert_eq!(v1, v2); + assert_eq!(v1, v3); + assert_eq!(v1.len(), 4); + assert!(Validators::::iter() + .all(|(v, _)| v1.iter().filter(|x| x.0 == v).count() == 1)); + + // equal.. + assert_eq!( + Staking::voters(limit_for_voters(Some(5), Some(5))).unwrap().len(), + 5 + ); + assert_eq!( + Staking::voters(limit_for_voters(Some(5), Some(6))).unwrap().len(), + 5 + ); + assert_eq!( + Staking::voters(limit_for_voters(Some(6), Some(5))).unwrap().len(), + 5 + ); + + // and finally more. + assert_eq!( + Staking::voters(limit_for_voters(Some(7), Some(7))).unwrap().len(), + 5 + ); + assert_eq!( + Staking::voters(limit_for_voters(Some(10), Some(7))).unwrap().len(), + 5 + ); + }); + } - // if target limit is more.. - assert_eq!(Staking::targets(Some(6)).unwrap().len(), 4); - assert_eq!(Staking::targets(Some(4)).unwrap().len(), 4); + #[test] + fn get_npos_targets_works_size_limit() { + ExtBuilder::default() + .set_status(41, StakerStatus::Validator) + .build_and_execute(|| { + // all targets: + assert_eq!(>::iter().count() as u32, 4); + + // if target limit is less.. + assert_eq!(Staking::targets(size_limit_for_targets(0)).unwrap().len(), 0); + assert_eq!(Staking::targets(size_limit_for_targets(1)).unwrap().len(), 1); + assert_eq!(Staking::targets(size_limit_for_targets(3)).unwrap().len(), 3); + + // if target limit is more.. + assert_eq!(Staking::targets(size_limit_for_targets(4)).unwrap().len(), 4); + assert_eq!(Staking::targets(size_limit_for_targets(8)).unwrap().len(), 4); + }) + } - // if target limit is less, then we return an error. - assert_eq!(Staking::targets(Some(1)).unwrap_err(), "Target snapshot too big"); - }); + #[test] + fn get_npos_targets_works_count_limit() { + ExtBuilder::default() + .set_status(41, StakerStatus::Validator) + .build_and_execute(|| { + // all targets: + assert_eq!(>::iter().count() as u32, 4); + + // if target limit is less.. + assert_eq!(Staking::targets(count_limit_for_targets(0)).unwrap().len(), 0); + assert_eq!(Staking::targets(count_limit_for_targets(1)).unwrap().len(), 1); + assert_eq!(Staking::targets(count_limit_for_targets(3)).unwrap().len(), 3); + + // if target limit is more.. + assert_eq!(Staking::targets(count_limit_for_targets(4)).unwrap().len(), 4); + assert_eq!(Staking::targets(count_limit_for_targets(8)).unwrap().len(), 4); + }) + } + + #[test] + fn get_npos_targets_works_hybrid_limit() { + ExtBuilder::default() + .set_status(41, StakerStatus::Validator) + .build_and_execute(|| { + // sum of all validators/targets. + assert_eq!(CounterForValidators::::get(), 4); + + // if less.. + assert_eq!( + Staking::targets(limit_for_targets(Some(0), Some(0))).unwrap().len(), + 0 + ); + assert_eq!( + Staking::targets(limit_for_targets(Some(0), Some(2))).unwrap().len(), + 0 + ); + assert_eq!( + Staking::targets(limit_for_targets(Some(2), Some(0))).unwrap().len(), + 0 + ); + + assert_eq!( + Staking::targets(dbg!(limit_for_targets(Some(2), Some(1)))).unwrap().len(), + 1 + ); + assert_eq!( + Staking::targets(limit_for_targets(Some(1), Some(1))).unwrap().len(), + 1 + ); + assert_eq!( + Staking::targets(limit_for_targets(Some(1), Some(2))).unwrap().len(), + 1 + ); + assert_eq!( + Staking::targets(limit_for_targets(Some(2), Some(2))).unwrap().len(), + 2 + ); + + // almost equal.. + assert_eq!( + Staking::targets(limit_for_targets(Some(3), Some(4))).unwrap().len(), + 3 + ); + // just equal.. + assert_eq!( + Staking::targets(limit_for_targets(Some(4), Some(4))).unwrap().len(), + 4 + ); + + // if more + assert_eq!( + Staking::targets(limit_for_targets(Some(5), Some(6))).unwrap().len(), + 4 + ); + assert_eq!( + Staking::targets(limit_for_targets(Some(6), Some(5))).unwrap().len(), + 4 + ); + }); + } } - #[test] - fn only_iterates_max_2_times_nominators_quota() { - ExtBuilder::default() - .nominate(true) // add nominator 101, who nominates [11, 21] - // the other nominators only nominate 21 - .add_staker(61, 60, 2_000, StakerStatus::::Nominator(vec![21])) - .add_staker(71, 70, 2_000, StakerStatus::::Nominator(vec![21])) - .add_staker(81, 80, 2_000, StakerStatus::::Nominator(vec![21])) - .build_and_execute(|| { - // given our nominators ordered by stake, - assert_eq!( - ::SortedListProvider::iter().collect::>(), - vec![61, 71, 81, 101] - ); + mod unbounded { + use super::*; - // and total voters + #[test] + fn voters_include_self_vote() { + ExtBuilder::default().nominate(false).build_and_execute(|| { + assert!(>::iter().map(|(x, _)| x).all(|v| Staking::voters( + SnapshotBounds::new_count(all_voters_count()) + ) + .unwrap() + .into_iter() + .find(|(w, _, t)| { v == *w && t[0] == *w }) + .is_some())) + }) + } + + #[test] + fn voters_exclude_slashed() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); assert_eq!( - ::SortedListProvider::count() + - >::iter().count() as u32, - 7 + >::voters( + SnapshotBounds::new_unbounded() + ) + .unwrap() + .iter() + .find(|x| x.0 == 101) + .unwrap() + .2, + vec![11, 21] ); - // roll to session 5 - run_to_block(25); + start_active_era(1); + add_slash(&11); - // slash 21, the only validator nominated by our first 3 nominators - add_slash(&21); + // 11 is gone. + start_active_era(2); + assert_eq!( + >::voters( + SnapshotBounds::new_unbounded() + ) + .unwrap() + .iter() + .find(|x| x.0 == 101) + .unwrap() + .2, + vec![21] + ); - // we take 4 voters: 2 validators and 2 nominators (so nominators quota = 2) + // resubmit and it is back + assert_ok!(Staking::nominate(Origin::signed(100), vec![11, 21])); assert_eq!( - Staking::voters(Some(3)) - .unwrap() - .iter() - .map(|(stash, _, _)| stash) - .copied() - .collect::>(), - vec![31, 11], // 2 validators, but no nominators because we hit the quota + >::voters( + SnapshotBounds::new_unbounded() + ) + .unwrap() + .iter() + .find(|x| x.0 == 101) + .unwrap() + .2, + vec![11, 21] ); - }); + }) + } + + #[test] + fn get_npos_voters_works() { + ExtBuilder::default() + .set_status(41, StakerStatus::Validator) + .build_and_execute(|| { + assert_eq!( + CounterForNominators::::get() + CounterForValidators::::get(), + 5 + ); + assert_eq!( + Staking::voters(SnapshotBounds::new_unbounded()).unwrap(), + vec![ + (101, 500, vec![11, 21]), + (31, 500, vec![31]), + (41, 1000, vec![41]), + (21, 1000, vec![21]), + (11, 1000, vec![11]), + ] + ); + }) + } + + #[test] + fn get_npos_targets_works() { + ExtBuilder::default() + .set_status(41, StakerStatus::Validator) + .build_and_execute(|| { + // all targets: + assert_eq!(>::iter().count() as u32, 4); + + // unbounded: + assert_eq!(Staking::targets(SnapshotBounds::new_unbounded()).unwrap().len(), 4); + }) + } } - // Even if some of the higher staked nominators are slashed, we still get up to max len voters - // by adding more lower staked nominators. In other words, we assert that we keep on adding - // valid nominators until we reach max len voters; which is opposed to simply stopping after we - // have iterated max len voters, but not adding all of them to voters due to some nominators not - // having valid targets. + // Even if some of the higher staked nominators are slashed, we don't get up to max len voters + // by adding more lower staked nominators. #[test] fn get_max_len_voters_even_if_some_nominators_are_slashed() { ExtBuilder::default() .nominate(true) // add nominator 101, who nominates [11, 21] .add_staker(61, 60, 20, StakerStatus::::Nominator(vec![21])) - // 61 only nominates validator 21 ^^ .add_staker(71, 70, 10, StakerStatus::::Nominator(vec![11, 21])) .build_and_execute(|| { - // given our nominators ordered by stake, - assert_eq!( - ::SortedListProvider::iter().collect::>(), - vec![101, 61, 71] - ); - - // and total voters - assert_eq!( - ::SortedListProvider::count() + - >::iter().count() as u32, - 6 - ); + // given + assert_eq!(validator_ids(), vec![31, 21, 11]); + assert_eq!(nominator_ids(), vec![101, 71, 61]); // we take 5 voters + let limit_5 = SnapshotBounds::new_size( + Staking::voters(SnapshotBounds::new_unbounded()) + .unwrap() + .into_iter() + .take(5) + .collect::>() + .encoded_size() as u32, + ); assert_eq!( - Staking::voters(Some(5)) + Staking::voters(limit_5) .unwrap() .iter() .map(|(stash, _, _)| stash) .copied() .collect::>(), - // then vec![ - 31, 21, 11, // 3 nominators - 101, 61 // 2 validators, and 71 is excluded + 31, 21, 11, // all validators are included + // and 2 nominator, and 71 is excluded, because it has less stake. + 101, 61, ], ); @@ -4162,8 +4562,16 @@ mod election_data_provider { add_slash(&21); // we take 4 voters + let limit_4 = SnapshotBounds::new_size( + Staking::voters(SnapshotBounds::new_unbounded()) + .unwrap() + .into_iter() + .take(4) + .collect::>() + .encoded_size() as u32, + ); assert_eq!( - Staking::voters(Some(4)) + Staking::voters(limit_4) .unwrap() .iter() .map(|(stash, _, _)| stash) @@ -4171,64 +4579,13 @@ mod election_data_provider { .collect::>(), vec![ 31, 11, // 2 validators (21 was slashed) - 101, 71 // 2 nominators, excluding 61 + 101, + /* 61 is skipped, since it has a slash now. 71 is not included, because + * we still count that as part of the byte `size_limit`. */ ], ); }); } - - #[test] - fn estimate_next_election_works() { - ExtBuilder::default().session_per_era(5).period(5).build_and_execute(|| { - // first session is always length 0. - for b in 1..20 { - run_to_block(b); - assert_eq!(Staking::next_election_prediction(System::block_number()), 20); - } - - // election - run_to_block(20); - assert_eq!(Staking::next_election_prediction(System::block_number()), 45); - assert_eq!(staking_events().len(), 1); - assert_eq!(*staking_events().last().unwrap(), Event::StakersElected); - - for b in 21..45 { - run_to_block(b); - assert_eq!(Staking::next_election_prediction(System::block_number()), 45); - } - - // election - run_to_block(45); - assert_eq!(Staking::next_election_prediction(System::block_number()), 70); - assert_eq!(staking_events().len(), 3); - assert_eq!(*staking_events().last().unwrap(), Event::StakersElected); - - Staking::force_no_eras(Origin::root()).unwrap(); - assert_eq!(Staking::next_election_prediction(System::block_number()), u64::MAX); - - Staking::force_new_era_always(Origin::root()).unwrap(); - assert_eq!(Staking::next_election_prediction(System::block_number()), 45 + 5); - - Staking::force_new_era(Origin::root()).unwrap(); - assert_eq!(Staking::next_election_prediction(System::block_number()), 45 + 5); - - // Do a fail election - MinimumValidatorCount::::put(1000); - run_to_block(50); - // Election: failed, next session is a new election - assert_eq!(Staking::next_election_prediction(System::block_number()), 50 + 5); - // The new era is still forced until a new era is planned. - assert_eq!(ForceEra::::get(), Forcing::ForceNew); - - MinimumValidatorCount::::put(2); - run_to_block(55); - assert_eq!(Staking::next_election_prediction(System::block_number()), 55 + 25); - assert_eq!(staking_events().len(), 6); - assert_eq!(*staking_events().last().unwrap(), Event::StakersElected); - // The new era has been planned, forcing is changed from `ForceNew` to `NotForcing`. - assert_eq!(ForceEra::::get(), Forcing::NotForcing); - }) - } } #[test] @@ -4543,6 +4900,29 @@ fn capped_stakers_works() { } #[test] +fn update_slashed_nominator_works() { + ExtBuilder::default().nominate(true).build_and_execute(|| { + start_active_era(1); + + // 101 nominates 11 by default. + assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); + + // can't update them at all. + assert_noop!(Staking::update_slashed_nominator(Origin::signed(31), 101), "not slashed"); + + // but it gets slashed. + add_slash(&11); + start_active_era(2); + + // 11 is cleaned now. + assert_ok!(Staking::update_slashed_nominator(Origin::signed(31), 101)); + assert_eq!(Staking::nominators(101).unwrap().targets, vec![21]); + + // can't repeat either + assert_noop!(Staking::update_slashed_nominator(Origin::signed(31), 101), "not slashed"); + }); +} + fn min_commission_works() { ExtBuilder::default().build_and_execute(|| { assert_ok!(Staking::validate( diff --git a/frame/staking/src/weights.rs b/frame/staking/src/weights.rs index 0ca819898fbb0..0fa56c556c119 100644 --- a/frame/staking/src/weights.rs +++ b/frame/staking/src/weights.rs @@ -68,8 +68,10 @@ pub trait WeightInfo { fn set_history_depth(e: u32, ) -> Weight; fn reap_stash(s: u32, ) -> Weight; fn new_era(v: u32, n: u32, ) -> Weight; - fn get_npos_voters(v: u32, n: u32, s: u32, ) -> Weight; - fn get_npos_targets(v: u32, ) -> Weight; + fn get_npos_voters_bounded(v: u32, n: u32, s: u32, ) -> Weight; + fn get_npos_voters_unbounded(v: u32, n: u32, s: u32, ) -> Weight; + fn get_npos_targets_bounded(v: u32, ) -> Weight; + fn get_npos_targets_unbounded(v: u32, ) -> Weight; fn set_staking_configs() -> Weight; fn chill_other() -> Weight; } @@ -397,7 +399,7 @@ impl WeightInfo for SubstrateWeight { // Storage: BagsList ListBags (r:200 w:0) // Storage: BagsList ListNodes (r:1000 w:0) // Storage: Staking Nominators (r:1000 w:0) - fn get_npos_voters(v: u32, n: u32, s: u32, ) -> Weight { + fn get_npos_voters_bounded(v: u32, n: u32, s: u32, ) -> Weight { (0 as Weight) // Standard Error: 87_000 .saturating_add((24_049_000 as Weight).saturating_mul(v as Weight)) @@ -408,8 +410,38 @@ impl WeightInfo for SubstrateWeight { .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 CounterForNominators (r:1 w:0) + // Storage: Staking CounterForValidators (r:1 w:0) + // Storage: Staking Validators (r:501 w:0) + // Storage: Staking Bonded (r:1500 w:0) + // Storage: Staking Ledger (r:1500 w:0) + // Storage: Staking SlashingSpans (r:21 w:0) + // Storage: BagsList ListBags (r:200 w:0) + // Storage: BagsList ListNodes (r:1000 w:0) + // Storage: Staking Nominators (r:1000 w:0) + fn get_npos_voters_unbounded(v: u32, n: u32, s: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 91_000 + .saturating_add((26_605_000 as Weight).saturating_mul(v as Weight)) + // Standard Error: 91_000 + .saturating_add((31_481_000 as Weight).saturating_mul(n as Weight)) + // Standard Error: 3_122_000 + .saturating_add((16_672_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(204 as Weight)) + .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(v 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) + fn get_npos_targets_bounded(v: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 34_000 + .saturating_add((10_558_000 as Weight).saturating_mul(v as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(v as Weight))) + } // Storage: Staking Validators (r:501 w:0) - fn get_npos_targets(v: u32, ) -> Weight { + fn get_npos_targets_unbounded(v: u32, ) -> Weight { (0 as Weight) // Standard Error: 32_000 .saturating_add((10_128_000 as Weight).saturating_mul(v as Weight)) @@ -765,7 +797,7 @@ impl WeightInfo for () { // Storage: BagsList ListBags (r:200 w:0) // Storage: BagsList ListNodes (r:1000 w:0) // Storage: Staking Nominators (r:1000 w:0) - fn get_npos_voters(v: u32, n: u32, s: u32, ) -> Weight { + fn get_npos_voters_bounded(v: u32, n: u32, s: u32, ) -> Weight { (0 as Weight) // Standard Error: 87_000 .saturating_add((24_049_000 as Weight).saturating_mul(v as Weight)) @@ -776,8 +808,38 @@ impl WeightInfo for () { .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 CounterForNominators (r:1 w:0) + // Storage: Staking CounterForValidators (r:1 w:0) + // Storage: Staking Validators (r:501 w:0) + // Storage: Staking Bonded (r:1500 w:0) + // Storage: Staking Ledger (r:1500 w:0) + // Storage: Staking SlashingSpans (r:21 w:0) + // Storage: BagsList ListBags (r:200 w:0) + // Storage: BagsList ListNodes (r:1000 w:0) + // Storage: Staking Nominators (r:1000 w:0) + fn get_npos_voters_unbounded(v: u32, n: u32, s: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 91_000 + .saturating_add((26_605_000 as Weight).saturating_mul(v as Weight)) + // Standard Error: 91_000 + .saturating_add((31_481_000 as Weight).saturating_mul(n as Weight)) + // Standard Error: 3_122_000 + .saturating_add((16_672_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(204 as Weight)) + .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(v 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) + fn get_npos_targets_bounded(v: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 34_000 + .saturating_add((10_558_000 as Weight).saturating_mul(v as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(v as Weight))) + } // Storage: Staking Validators (r:501 w:0) - fn get_npos_targets(v: u32, ) -> Weight { + fn get_npos_targets_unbounded(v: u32, ) -> Weight { (0 as Weight) // Standard Error: 32_000 .saturating_add((10_128_000 as Weight).saturating_mul(v as Weight))