diff --git a/Cargo.lock b/Cargo.lock index cc8557daad2f4..0a7e200183428 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5521,6 +5521,7 @@ dependencies = [ "sp-tracing", "static_assertions", "substrate-test-utils", + "thiserror", ] [[package]] @@ -10037,18 +10038,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" dependencies = [ "proc-macro2", "quote", diff --git a/frame/staking/Cargo.toml b/frame/staking/Cargo.toml index 908e361e667e3..dad14b4a882a4 100644 --- a/frame/staking/Cargo.toml +++ b/frame/staking/Cargo.toml @@ -28,6 +28,7 @@ sp-application-crypto = { version = "3.0.0", default-features = false, path = ". frame-election-provider-support = { version = "3.0.0", default-features = false, path = "../election-provider-support" } log = { version = "0.4.14", default-features = false } paste = "1.0" +thiserror = "1.0.25" # Optional imports for benchmarking frame-benchmarking = { version = "3.1.0", default-features = false, path = "../benchmarking", optional = true } diff --git a/frame/staking/src/benchmarking.rs b/frame/staking/src/benchmarking.rs index 1d8a5c1fd6451..62df7ad33d4c7 100644 --- a/frame/staking/src/benchmarking.rs +++ b/frame/staking/src/benchmarking.rs @@ -561,14 +561,14 @@ benchmarks! { } get_npos_voters { - // number of validator intention. - let v in 200 .. 400; - // number of nominator intention. - let n in 200 .. 400; + // total number of voters (validators + nominators) + let v in 400 .. 800; // 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::MAX_NOMINATIONS as usize, false, None)? + // this isn't ideal, but as we don't store the numbers of nominators and validators + // distinctly, we can't really parametrize this function with different quantities of each + let validators = create_validators_with_nominators_for_era::(v / 2, n / 2, T::MAX_NOMINATIONS as usize, false, None)? .into_iter() .map(|v| T::Lookup::lookup(v).unwrap()) .collect::>(); diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs index 888601e307f35..fe194033075ef 100644 --- a/frame/staking/src/lib.rs +++ b/frame/staking/src/lib.rs @@ -276,6 +276,7 @@ pub mod testing_utils; #[cfg(any(feature = "runtime-benchmarks", test))] pub mod benchmarking; +pub mod voter_list; pub mod slashing; pub mod inflation; pub mod weights; @@ -353,6 +354,9 @@ type NegativeImbalanceOf = <::Currency as Currency< ::AccountId, >>::NegativeImbalance; +type AccountIdOf = ::AccountId; +type VotingDataOf = (AccountIdOf, VoteWeight, Vec>); + /// Information regarding the active era (era in used in session). #[derive(Encode, Decode, RuntimeDebug)] pub struct ActiveEraInfo { @@ -994,6 +998,19 @@ decl_storage! { /// /// This is set to v6.0.0 for new networks. StorageVersion build(|_: &GenesisConfig| Releases::V6_0_0): Releases; + + // The next three storage items collectively comprise the `voter_list::VoterList` + // data structure. They should not be accessed individually, but only through the interface + // provided by that type. + + /// Count of nominators. Size of `NominatorList`. + NominatorCount: u32; + + /// Linked list of nominators, sorted in decreasing order of stake. + NominatorList: voter_list::VoterList; + + /// Map of nodes in the nominator list. + NominatorNodes: map hasher(twox_64_concat) T::AccountId => Option>; } add_extra_genesis { config(stakers): @@ -2495,37 +2512,22 @@ impl Module { /// auto-chilled. /// /// Note that this is VERY expensive. Use with care. - pub fn get_npos_voters() -> Vec<(T::AccountId, VoteWeight, Vec)> { - let weight_of = Self::slashable_balance_of_fn(); - let mut all_voters = Vec::new(); - - for (validator, _) in >::iter() { - // append self vote - let self_vote = (validator.clone(), weight_of(&validator), vec![validator.clone()]); - all_voters.push(self_vote); - } + pub fn get_npos_voters( + maybe_max_len: Option, + ) -> Vec<(T::AccountId, VoteWeight, Vec)> { + // nominator list contains all nominators and validators + let voter_count = voter_list::VoterList::::decode_len().unwrap_or_default(); + let wanted_voter_count = maybe_max_len.unwrap_or(voter_count).min(voter_count); + let weight_of = Self::slashable_balance_of_fn(); // collect all slashing spans into a BTreeMap for further queries. - let slashing_spans = >::iter().collect::>(); + let slashing_spans = >::iter().collect(); - for (nominator, nominations) in >::iter() { - let Nominations { submitted_in, mut targets, suppressed: _ } = nominations; - - // Filter out nomination targets which were nominated before the most recent - // slashing span. - targets.retain(|stash| { - slashing_spans - .get(stash) - .map_or(true, |spans| submitted_in >= spans.last_nonzero_slash()) - }); - - if !targets.is_empty() { - let vote_weight = weight_of(&nominator); - all_voters.push((nominator, vote_weight, targets)) - } - } - - all_voters + Self::voter_list() + .iter() + .filter_map(|node| node.voting_data(&weight_of, &slashing_spans)) + .take(wanted_voter_count) + .collect() } pub fn get_npos_targets() -> Vec { @@ -2543,25 +2545,11 @@ impl frame_election_provider_support::ElectionDataProvider, - ) -> data_provider::Result<(Vec<(T::AccountId, VoteWeight, Vec)>, Weight)> { - // NOTE: reading these counts already needs to iterate a lot of storage keys, but they get - // cached. This is okay for the case of `Ok(_)`, but bad for `Err(_)`, as the trait does not - // report weight in failures. - let nominator_count = >::iter().count(); - let validator_count = >::iter().count(); - let voter_count = nominator_count.saturating_add(validator_count); - - if maybe_max_len.map_or(false, |max_len| voter_count > max_len) { - return Err("Voter snapshot too big"); - } - + ) -> data_provider::Result<(Vec>, Weight)> { + let voter_count = voter_list::VoterList::::decode_len().unwrap_or_default(); let slashing_span_count = >::iter().count(); - let weight = T::WeightInfo::get_npos_voters( - nominator_count as u32, - validator_count as u32, - slashing_span_count as u32, - ); - Ok((Self::get_npos_voters(), weight)) + let weight = T::WeightInfo::get_npos_voters(voter_count as u32, slashing_span_count as u32); + Ok((Self::get_npos_voters(maybe_max_len), weight)) } fn targets(maybe_max_len: Option) -> data_provider::Result<(Vec, Weight)> { diff --git a/frame/staking/src/voter_list.rs b/frame/staking/src/voter_list.rs new file mode 100644 index 0000000000000..870b2f900637e --- /dev/null +++ b/frame/staking/src/voter_list.rs @@ -0,0 +1,295 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Provide a linked list of nominators, sorted by stake. + +use crate::{ + slashing::SlashingSpans, AccountIdOf, Config, Nominations, Nominators, Pallet, VotingDataOf, + VoteWeight, +}; +use codec::{Encode, Decode}; +use frame_support::{DefaultNoBound, StorageMap, StorageValue}; +use sp_runtime::SaturatedConversion; +use sp_std::{collections::btree_map::BTreeMap, marker::PhantomData}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("attempted to insert into the wrong position in the list")] + WrongInsertPosition, +} + +/// Type of voter. +/// +/// Similar to [`crate::StakerStatus`], but somewhat more limited. +#[derive(Clone, Copy, Encode, Decode, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +pub enum VoterType { + Validator, + Nominator, +} + +/// Fundamental information about a voter. +#[derive(Clone, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct Voter { + /// Account Id of this voter + pub id: AccountId, + /// Whether the voter is a validator or nominator + pub voter_type: VoterType, + /// The current slashable balance this voter brings. + /// + /// This value is cached because the actual vote weight is likely to change frequently due to + /// various rewards and slashes, without substantially affecting the overall position in the + /// list. It's cheaper and easier to operate on a cache than to look up the actual value for + /// each voter each time. + pub cache_weight: VoteWeight, +} + +pub type VoterOf = Voter>; + +impl Pallet { + /// `Self` accessor for `NominatorList` + pub fn voter_list() -> VoterList { + VoterList::::get() + } +} + +/// Linked list of nominstors, sorted by stake. +#[derive(DefaultNoBound, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct VoterList { + head: Option>, + tail: Option>, +} + +impl VoterList { + /// Decode the length of this list without actually iterating over it. + pub fn decode_len() -> Option { + crate::NominatorCount::try_get().ok().map(|n| n.saturated_into()) + } + + /// Get this list from storage. + pub fn get() -> Self { + crate::NominatorList::::get() + } + + /// Update this list in storage. + pub fn put(self) { + crate::NominatorList::::put(self); + } + + /// Get the first member of the list. + pub fn head(&self) -> Option> { + self.head.as_ref().and_then(|head| crate::NominatorNodes::try_get(head).ok()) + } + + /// Get the last member of the list. + pub fn tail(&self) -> Option> { + self.tail.as_ref().and_then(|tail| crate::NominatorNodes::try_get(tail).ok()) + } + + /// Create an iterator over this list. + pub fn iter(&self) -> Iter { + Iter { _lifetime: PhantomData, upcoming: self.head() } + } + + /// Insert a new voter into the list. + /// + /// It is always necessary to specify the position in the list into which the voter's node + /// should be inserted. This ensures that, while _someone_ has to iterate the list to discover + /// the proper insertion position, that iteration happens off the blockchain. + /// + /// If `after.is_none()`, then the voter should be inserted at the list head. + /// + /// This function does _not_ check that `voter.cache_weight` is accurate. That value should be + /// computed in a higher-level function calling this one. + /// + /// This is an immediate operation which modifies storage directly. + pub fn insert(mut self, voter: VoterOf, after: Option>) -> Result<(), Error> { + let predecessor = after.as_ref().and_then(Node::::from_id); + let successor = after.map_or_else(|| self.head(), |id| Node::::from_id(&id)); + + // an insert position is legal if it is not less than its predecessor and not greater than + // its successor. + if let Some(predecessor) = predecessor.as_ref() { + if predecessor.voter.cache_weight < voter.cache_weight { + return Err(Error::WrongInsertPosition); + } + } + if let Some(successor) = successor.as_ref() { + if successor.voter.cache_weight > voter.cache_weight { + return Err(Error::WrongInsertPosition); + } + } + + let id = voter.id.clone(); + + // insert the actual voter + let voter_node = Node:: { + voter, + prev: predecessor.as_ref().map(|prev| prev.voter.id.clone()), + next: successor.as_ref().map(|next| next.voter.id.clone()), + }; + voter_node.put(); + + // update the list links + if predecessor.is_none() { + self.head = Some(id.clone()); + } + if successor.is_none() { + self.tail = Some(id.clone()); + } + self.put(); + + // update the node links + if let Some(mut predecessor) = predecessor { + predecessor.next = Some(id.clone()); + predecessor.put(); + + } + if let Some(mut successor) = successor { + successor.prev = Some(id.clone()); + successor.put(); + } + + Ok(()) + } + + /// Remove a voter from the list. + /// + /// Returns `true` when the voter had previously existed in the list. + /// + /// This is an immediate operation which modifies storage directly. + pub fn remove(mut self, id: AccountIdOf) -> Result { + let maybe_node = Node::::from_id(&id); + let existed = maybe_node.is_some(); + if let Some(node) = maybe_node { + let predecessor = node.prev(); + let successor = node.next(); + + let predecessor_id = predecessor.as_ref().map(|prev| prev.voter.id.clone()); + let successor_id = successor.as_ref().map(|next| next.voter.id.clone()); + + // update list + if predecessor.is_none() { + self.head = successor_id.clone(); + } + if successor.is_none() { + self.tail = predecessor_id.clone(); + } + self.put(); + + // update adjacent nodes + if let Some(mut prev) = predecessor { + prev.next = successor_id; + prev.put(); + } + if let Some(mut next) = successor { + next.prev = predecessor_id; + next.put(); + } + + // remove the node itself + node.remove(); + } + Ok(existed) + } +} + +#[derive(Encode, Decode)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct Node { + prev: Option>, + next: Option>, + voter: Voter>, +} + +impl Node { + /// Get a node by account id. + pub fn from_id(id: &AccountIdOf) -> Option> { + crate::NominatorNodes::::try_get(id).ok() + } + + /// Update the node in storage. + pub fn put(self) { + crate::NominatorNodes::::insert(self.voter.id.clone(), self); + } + + /// Remove the node from storage. + /// + /// This function is intentionally private, because it's naive. + /// [`VoterList::::remove`] is the better option for general use. + fn remove(self) { + crate::NominatorNodes::::remove(self.voter.id); + } + + /// Get the previous node. + pub fn prev(&self) -> Option> { + self.prev.as_ref().and_then(Self::from_id) + } + + /// Get the next node. + pub fn next(&self) -> Option> { + self.next.as_ref().and_then(Self::from_id) + } + + /// Get this voter's voting data. + pub fn voting_data( + &self, + weight_of: impl Fn(&T::AccountId) -> VoteWeight, + slashing_spans: &BTreeMap, SlashingSpans>, + ) -> Option> { + match self.voter.voter_type { + VoterType::Validator => Some(( + self.voter.id.clone(), + weight_of(&self.voter.id), + vec![self.voter.id.clone()], + )), + VoterType::Nominator => { + let Nominations { submitted_in, mut targets, .. } = + Nominators::::get(self.voter.id.clone())?; + // Filter out nomination targets which were nominated before the most recent + // slashing span. + targets.retain(|stash| { + slashing_spans + .get(stash) + .map_or(true, |spans| submitted_in >= spans.last_nonzero_slash()) + }); + + (!targets.is_empty()) + .then(move || (self.voter.id.clone(), weight_of(&self.voter.id), targets)) + } + } + } +} + +pub struct Iter<'a, T: Config> { + _lifetime: sp_std::marker::PhantomData<&'a ()>, + upcoming: Option>, +} + +impl<'a, T: Config> Iterator for Iter<'a, T> { + type Item = Node; + + fn next(&mut self) -> Option { + let next = self.upcoming.take(); + if let Some(next) = next.as_ref() { + self.upcoming = next.next(); + } + next + } +} diff --git a/frame/staking/src/weights.rs b/frame/staking/src/weights.rs index 5960d6612566e..f273607f97274 100644 --- a/frame/staking/src/weights.rs +++ b/frame/staking/src/weights.rs @@ -35,7 +35,6 @@ // --output=./frame/staking/src/weights.rs // --template=./.maintain/frame-weight-template.hbs - #![allow(unused_parens)] #![allow(unused_imports)] @@ -66,10 +65,10 @@ pub trait WeightInfo { fn payout_stakers_alive_staked(n: u32, ) -> Weight; fn rebond(l: u32, ) -> Weight; 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 reap_stash(s: u32) -> Weight; + fn new_era(v: u32, n: u32) -> Weight; + fn get_npos_voters(v: u32, s: u32) -> Weight; + fn get_npos_targets(v: u32) -> Weight; } /// Weights for pallet_staking using the Substrate node and recommended hardware. @@ -219,7 +218,7 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().writes(8 as Weight)) .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(s as Weight))) } - fn new_era(v: u32, n: u32, ) -> Weight { + fn new_era(v: u32, n: u32) -> Weight { (0 as Weight) // Standard Error: 1_462_000 .saturating_add((393_007_000 as Weight).saturating_mul(v as Weight)) @@ -231,20 +230,17 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().writes(9 as Weight)) .saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(v as Weight))) } - fn get_npos_voters(v: u32, n: u32, s: u32, ) -> Weight { + fn get_npos_voters(v: u32, s: u32) -> Weight { (0 as Weight) // Standard Error: 235_000 .saturating_add((35_212_000 as Weight).saturating_mul(v as Weight)) - // Standard Error: 235_000 - .saturating_add((38_391_000 as Weight).saturating_mul(n as Weight)) // Standard Error: 3_200_000 .saturating_add((31_130_000 as Weight).saturating_mul(s as Weight)) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(v as Weight))) - .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(n as Weight))) .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) } - fn get_npos_targets(v: u32, ) -> Weight { + fn get_npos_targets(v: u32) -> Weight { (52_314_000 as Weight) // Standard Error: 71_000 .saturating_add((15_195_000 as Weight).saturating_mul(v as Weight)) @@ -399,7 +395,7 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().writes(8 as Weight)) .saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(s as Weight))) } - fn new_era(v: u32, n: u32, ) -> Weight { + fn new_era(v: u32, n: u32) -> Weight { (0 as Weight) // Standard Error: 1_462_000 .saturating_add((393_007_000 as Weight).saturating_mul(v as Weight)) @@ -411,20 +407,17 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().writes(9 as Weight)) .saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(v as Weight))) } - fn get_npos_voters(v: u32, n: u32, s: u32, ) -> Weight { + fn get_npos_voters(v: u32, s: u32) -> Weight { (0 as Weight) // Standard Error: 235_000 .saturating_add((35_212_000 as Weight).saturating_mul(v as Weight)) - // Standard Error: 235_000 - .saturating_add((38_391_000 as Weight).saturating_mul(n as Weight)) // Standard Error: 3_200_000 .saturating_add((31_130_000 as Weight).saturating_mul(s as Weight)) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(v as Weight))) - .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(n as Weight))) .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) } - fn get_npos_targets(v: u32, ) -> Weight { + fn get_npos_targets(v: u32) -> Weight { (52_314_000 as Weight) // Standard Error: 71_000 .saturating_add((15_195_000 as Weight).saturating_mul(v as Weight))