[NPoS] Implements dynamic number of nominators (#12970)

* Implements dynamic nominations per nominator

* Adds SnapshotBounds and ElectionSizeTracker

* Changes the ElectionDataProvider interface to receive ElectionBounds as input

* Implements get_npos_voters with ElectionBounds

* Implements get_npos_targets with ElectionBounds

* Adds comments

* tests

* Truncates nomninations that exceed nominations quota; Old tests passing

* Uses DataProviderBounds and ElectionBounds (to continue)

* Finishes conversions - tests passing

* Refactor staking in babe mocks

* Replaces MaxElectableTargets and MaxElectingVoters with ElectionBounds; Adds more tests

* Fixes nits; node compiling

* bechmarks

* removes nomination_quota extrinsic to request the nomination quota

* Lazy quota check, ie. at nominate time only

* remove non-working test (for now)

* tests lazy nominations quota when quota is lower than current number of nominated targets

* Adds runtime API and custom RPC call for clients to query the nominations quota for a given balance

* removes old rpc

* Cosmetic touches

* All mocks working

* Fixes benchmarking mocks

* nits

* more tests

* renames trait methods

* nit

* ".git/.scripts/commands/fmt/fmt.sh"

* Fix V2 PoV benchmarking (#13485)

* Bump default 'additional_trie_layers' to two

The default here only works for extremely small runtimes, which have
no more than 16 storage prefices. This is changed to a "sane" default
of 2, which is save for runtimes with up to 4096 storage prefices (eg StorageValue).

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Update tests and test weights

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Fix PoV weights

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_balances

* ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_message_queue

* ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_glutton

* ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_glutton

* Fix sanity check

>0 would also do as a check, but let's try this.

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

---------

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: command-bot <>

* Move BEEFY code to consensus (#13484)

* Move beefy primitives to consensus dir
* Move beefy gadget to client consensus folder
* Rename beefy crates

* chore: move genesis block builder to chain-spec crate. (#13427)

* chore: move genesis block builder to block builder crate.

* add missing file

* chore: move genesis block builder to sc-chain-spec

* Update client/chain-spec/src/genesis.rs

Co-authored-by: Bastian Köcher <git@kchr.de>

* Update test-utils/runtime/src/genesismap.rs

Co-authored-by: Bastian Köcher <git@kchr.de>

* Update test-utils/runtime/client/src/lib.rs

* fix warnings

* fix warnings

---------

Co-authored-by: Bastian Köcher <git@kchr.de>

* Speed up storage iteration from within the runtime (#13479)

* Speed up storage iteration from within the runtime

* Move the cached iterator into an `Option`

* Use `RefCell` in no_std

* Simplify the code slightly

* Use `Option::replace`

* Update doc comment for `next_storage_key_slow`

* Make unbounded channels size warning exact (part 1) (#13490)

* Replace `futures-channel` with `async-channel` in `out_events`

* Apply suggestions from code review

Co-authored-by: Koute <koute@users.noreply.github.com>

* Also print the backtrace of `send()` call

* Switch from `backtrace` crate to `std::backtrace`

* Remove outdated `backtrace` dependency

* Remove `backtrace` from `Cargo.lock`

---------

Co-authored-by: Koute <koute@users.noreply.github.com>

* Removal of Prometheus alerting rules deployment in cloud-infra (#13499)

* sp-consensus: remove unused error variants (#13495)

* Expose `ChargedAmount` (#13488)

* Expose `ChargedAmount`

* Fix imports

* sc-consensus-beefy: fix metrics: use correct names (#13494)


Signed-off-by: acatangiu <adrian@parity.io>

* clippy fix

* removes NominationsQuotaExceeded event

* Update frame/staking/src/lib.rs

Co-authored-by: Ross Bulat <ross@parity.io>

* adds back the npos_max_iter

* remove duplicate imports added after merge

* fmt

* Adds comment in public struct; Refactors CountBound and SizeCount to struct

* addresses various pr comments

* PR comment reviews

* Fixes on-chain election bounds and related code

* EPM checks the size of the voter list returned by the data provider

* cosmetic changes

* updates e2e tests mock

* Adds more tests for size tracker and refactors code

* Adds back only_iterates_max_2_times_max_allowed_len test

* Refactor

* removes unecessary dependency

* empty commit -- restart all stuck CI jobs

* restarts ci jobs

* Renames ElectionBounds -> Bounds in benchmarking mocks et al

* updates mocks

* Update frame/election-provider-support/src/lib.rs

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* Update frame/staking/src/pallet/impls.rs

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* Update frame/election-provider-support/src/lib.rs

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* Update frame/staking/src/tests.rs

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* more checks in api_nominations_quota in tests

* Improves docs

* fixes e2e tests

* Uses size_hint rather than mem::size_of in size tracker; Refactor size tracker to own module

* nits from reviews

* Refactors bounds to own module; improves docs

* More tests and docs

* fixes docs

* Fixes benchmarks

* Fixes rust docs

* fixes bags-list remote-ext-tests

* Simplify bound checks in create_snapshot_external

* Adds target size check in get_npos_targets

* ".git/.scripts/commands/fmt/fmt.sh"

* restart ci

* rust doc fixes and cosmetic nits

* rollback upgrade on parity-scale-codec version (unecessary)

* reset cargo lock, no need to update it

---------

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Signed-off-by: acatangiu <adrian@parity.io>
Co-authored-by: command-bot <>
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: Davide Galassi <davxy@datawok.net>
Co-authored-by: yjh <yjh465402634@gmail.com>
Co-authored-by: Bastian Köcher <git@kchr.de>
Co-authored-by: Koute <koute@users.noreply.github.com>
Co-authored-by: Dmitry Markin <dmitry@markin.tech>
Co-authored-by: Anthony Lazam <lazam@users.noreply.github.com>
Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>
Co-authored-by: Piotr Mikołajczyk <piomiko41@gmail.com>
Co-authored-by: Adrian Catangiu <adrian@parity.io>
Co-authored-by: Ross Bulat <ross@parity.io>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
This commit is contained in:
Gonçalo Pestana
2023-08-10 09:45:55 +02:00
committed by GitHub
parent 314109d87b
commit 93754780b1
30 changed files with 1415 additions and 307 deletions
+16 -14
View File
@@ -22,7 +22,7 @@ use crate::{ConfigOp, Pallet as Staking};
use testing_utils::*;
use codec::Decode;
use frame_election_provider_support::SortedListProvider;
use frame_election_provider_support::{bounds::DataProviderBounds, SortedListProvider};
use frame_support::{
dispatch::UnfilteredDispatchable,
pallet_prelude::*,
@@ -338,7 +338,7 @@ benchmarks! {
validate {
let (stash, controller) = create_stash_controller::<T>(
T::MaxNominations::get() - 1,
MaxNominationsOf::<T>::get() - 1,
100,
Default::default(),
)?;
@@ -362,11 +362,11 @@ benchmarks! {
// these are the other validators; there are `T::MaxNominations::get() - 1` of them, so
// there are a total of `T::MaxNominations::get()` validators in the system.
let rest_of_validators = create_validators_with_seed::<T>(T::MaxNominations::get() - 1, 100, 415)?;
let rest_of_validators = create_validators_with_seed::<T>(MaxNominationsOf::<T>::get() - 1, 100, 415)?;
// this is the validator that will be kicking.
let (stash, controller) = create_stash_controller::<T>(
T::MaxNominations::get() - 1,
MaxNominationsOf::<T>::get() - 1,
100,
Default::default(),
)?;
@@ -381,7 +381,7 @@ benchmarks! {
for i in 0 .. k {
// create a nominator stash.
let (n_stash, n_controller) = create_stash_controller::<T>(
T::MaxNominations::get() + i,
MaxNominationsOf::<T>::get() + i,
100,
Default::default(),
)?;
@@ -418,7 +418,7 @@ benchmarks! {
// Worst case scenario, T::MaxNominations::get()
nominate {
let n in 1 .. T::MaxNominations::get();
let n in 1 .. MaxNominationsOf::<T>::get();
// clean up any existing state.
clear_validators_and_nominators::<T>();
@@ -429,7 +429,7 @@ benchmarks! {
// we are just doing an insert into the origin position.
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let (stash, controller) = create_stash_controller_with_balance::<T>(
SEED + T::MaxNominations::get() + 1, // make sure the account does not conflict with others
SEED + MaxNominationsOf::<T>::get() + 1, // make sure the account does not conflict with others
origin_weight,
Default::default(),
).unwrap();
@@ -711,7 +711,7 @@ benchmarks! {
create_validators_with_nominators_for_era::<T>(
v,
n,
<T as Config>::MaxNominations::get() as usize,
MaxNominationsOf::<T>::get() as usize,
false,
None,
)?;
@@ -729,7 +729,7 @@ benchmarks! {
create_validators_with_nominators_for_era::<T>(
v,
n,
<T as Config>::MaxNominations::get() as usize,
MaxNominationsOf::<T>::get() as usize,
false,
None,
)?;
@@ -808,7 +808,7 @@ benchmarks! {
let n in (MaxNominators::<T>::get() / 2) .. MaxNominators::<T>::get();
let validators = create_validators_with_nominators_for_era::<T>(
v, n, T::MaxNominations::get() as usize, false, None
v, n, MaxNominationsOf::<T>::get() as usize, false, None
)?
.into_iter()
.map(|v| T::Lookup::lookup(v).unwrap())
@@ -819,7 +819,8 @@ benchmarks! {
let num_voters = (v + n) as usize;
}: {
let voters = <Staking<T>>::get_npos_voters(None);
// default bounds are unbounded.
let voters = <Staking<T>>::get_npos_voters(DataProviderBounds::default());
assert_eq!(voters.len(), num_voters);
}
@@ -830,10 +831,11 @@ benchmarks! {
let n = MaxNominators::<T>::get();
let _ = create_validators_with_nominators_for_era::<T>(
v, n, T::MaxNominations::get() as usize, false, None
v, n, MaxNominationsOf::<T>::get() as usize, false, None
)?;
}: {
let targets = <Staking<T>>::get_npos_targets(None);
// default bounds are unbounded.
let targets = <Staking<T>>::get_npos_targets(DataProviderBounds::default());
assert_eq!(targets.len() as u32, v);
}
@@ -961,7 +963,7 @@ mod tests {
create_validators_with_nominators_for_era::<Test>(
v,
n,
<Test as Config>::MaxNominations::get() as usize,
MaxNominationsOf::<Test>::get() as usize,
false,
None,
)
@@ -0,0 +1,259 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! ## A static size tracker for the election snapshot data.
//!
//! ### Overview
//!
//! The goal of the size tracker is to provide a static, no-allocation byte tracker to be
//! used by the election data provider when preparing the results of
//! [`ElectionDataProvider::electing_voters`]. The [`StaticTracker`] implementation uses
//! [`codec::Encode::size_hint`] to estimate the SCALE encoded size of the snapshot voters struct
//! as it is being constructed without requiring extra stack allocations.
//!
//! The [`StaticTracker::try_register_voter`] is called to update the static tracker internal
//! state, if It will return an error if the resulting SCALE encoded size (in bytes) is larger than
//! the provided `DataProviderBounds`.
//!
//! ### Example
//!
//! ```ignore
//! use pallet_staking::election_size_tracker::*;
//!
//! // instantiates a new tracker.
//! let mut size_tracker = StaticTracker::<Staking>::default();
//!
//! let voter_bounds = ElectionBoundsBuilder::default().voter_size(1_00.into()).build().voters;
//!
//! let mut sorted_voters = T::VoterList.iter();
//! let mut selected_voters = vec![];
//!
//! // fit as many voters in the vec as the bounds permit.
//! for v in sorted_voters {
//! let voter = (v, weight_of(&v), targets_of(&v));
//! if size_tracker.try_register_voter(&voter, &voter_bounds).is_err() {
//! // voter bounds size exhausted
//! break;
//! }
//! selected_voters.push(voter);
//! }
//!
//! // The SCALE encoded size in bytes of `selected_voters` is guaranteed to be below
//! // `voter_bounds`.
//! debug_assert!(
//! selected_voters.encoded_size() <=
//! SizeTracker::<Staking>::final_byte_size_of(size_tracker.num_voters, size_tracker.size)
//! );
//! ```
//!
//! ### Implementation Details
//!
//! The current implementation of the static tracker is tightly coupled with the staking pallet
//! implementation, namely the representation of a voter ([`VoterOf`]). The SCALE encoded byte size
//! is calculated using [`Encode::size_hint`] of each type in the voter tuple. Each voter's byte
//! size is the sum of:
//! - 1 * [`Encode::size_hint`] of the `AccountId` type;
//! - 1 * [`Encode::size_hint`] of the `VoteWeight` type;
//! - `num_votes` * [`Encode::size_hint`] of the `AccountId` type.
use codec::Encode;
use frame_election_provider_support::{
bounds::{DataProviderBounds, SizeBound},
ElectionDataProvider, VoterOf,
};
/// Keeps track of the SCALE encoded byte length of the snapshot's voters or targets.
///
/// The tracker calculates the bytes used based on static rules, without requiring any actual
/// encoding or extra allocations.
#[derive(Clone, Copy, Debug)]
pub struct StaticTracker<DataProvider> {
pub size: usize,
pub counter: usize,
_marker: sp_std::marker::PhantomData<DataProvider>,
}
impl<DataProvider> Default for StaticTracker<DataProvider> {
fn default() -> Self {
Self { size: 0, counter: 0, _marker: Default::default() }
}
}
impl<DataProvider> StaticTracker<DataProvider>
where
DataProvider: ElectionDataProvider,
{
/// Tries to register a new voter.
///
/// If the new voter exhausts the provided bounds, return an error. Otherwise, the internal
/// state of the tracker is updated with the new registered voter.
pub fn try_register_voter(
&mut self,
voter: &VoterOf<DataProvider>,
bounds: &DataProviderBounds,
) -> Result<(), ()> {
let tracker_size_after = {
let voter_hint = Self::voter_size_hint(voter);
Self::final_byte_size_of(self.counter + 1, self.size.saturating_add(voter_hint))
};
match bounds.size_exhausted(SizeBound(tracker_size_after as u32)) {
true => Err(()),
false => {
self.size = tracker_size_after;
self.counter += 1;
Ok(())
},
}
}
/// Calculates the size of the voter to register based on [`Encode::size_hint`].
fn voter_size_hint(voter: &VoterOf<DataProvider>) -> usize {
let (voter_account, vote_weight, targets) = voter;
voter_account
.size_hint()
.saturating_add(vote_weight.size_hint())
.saturating_add(voter_account.size_hint().saturating_mul(targets.len()))
}
/// Tries to register a new target.
///
/// If the new target exhausts the provided bounds, return an error. Otherwise, the internal
/// state of the tracker is updated with the new registered target.
pub fn try_register_target(
&mut self,
target: DataProvider::AccountId,
bounds: &DataProviderBounds,
) -> Result<(), ()> {
let tracker_size_after = Self::final_byte_size_of(
self.counter + 1,
self.size.saturating_add(target.size_hint()),
);
match bounds.size_exhausted(SizeBound(tracker_size_after as u32)) {
true => Err(()),
false => {
self.size = tracker_size_after;
self.counter += 1;
Ok(())
},
}
}
/// Size of the SCALE encoded prefix with a given length.
#[inline]
fn length_prefix(len: usize) -> usize {
use codec::{Compact, CompactLen};
Compact::<u32>::compact_len(&(len as u32))
}
/// Calculates the final size in bytes of the SCALE encoded snapshot voter struct.
fn final_byte_size_of(num_voters: usize, size: usize) -> usize {
Self::length_prefix(num_voters).saturating_add(size)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
mock::{AccountId, Staking, Test},
BoundedVec, MaxNominationsOf,
};
use frame_election_provider_support::bounds::ElectionBoundsBuilder;
use sp_core::bounded_vec;
type Voters = BoundedVec<AccountId, MaxNominationsOf<Test>>;
#[test]
pub fn election_size_tracker_works() {
let mut voters: Vec<(u64, u64, Voters)> = vec![];
let mut size_tracker = StaticTracker::<Staking>::default();
let voter_bounds = ElectionBoundsBuilder::default().voters_size(1_50.into()).build().voters;
// register 1 voter with 1 vote.
let voter = (1, 10, bounded_vec![2]);
assert!(size_tracker.try_register_voter(&voter, &voter_bounds).is_ok());
voters.push(voter);
assert_eq!(
StaticTracker::<Staking>::final_byte_size_of(size_tracker.counter, size_tracker.size),
voters.encoded_size()
);
// register another voter, now with 3 votes.
let voter = (2, 20, bounded_vec![3, 4, 5]);
assert!(size_tracker.try_register_voter(&voter, &voter_bounds).is_ok());
voters.push(voter);
assert_eq!(
StaticTracker::<Staking>::final_byte_size_of(size_tracker.counter, size_tracker.size),
voters.encoded_size()
);
// register noop vote (unlikely to happen).
let voter = (3, 30, bounded_vec![]);
assert!(size_tracker.try_register_voter(&voter, &voter_bounds).is_ok());
voters.push(voter);
assert_eq!(
StaticTracker::<Staking>::final_byte_size_of(size_tracker.counter, size_tracker.size),
voters.encoded_size()
);
}
#[test]
pub fn election_size_tracker_bounds_works() {
let mut voters: Vec<(u64, u64, Voters)> = vec![];
let mut size_tracker = StaticTracker::<Staking>::default();
let voter_bounds = ElectionBoundsBuilder::default().voters_size(1_00.into()).build().voters;
let voter = (1, 10, bounded_vec![2]);
assert!(size_tracker.try_register_voter(&voter, &voter_bounds).is_ok());
voters.push(voter);
assert_eq!(
StaticTracker::<Staking>::final_byte_size_of(size_tracker.counter, size_tracker.size),
voters.encoded_size()
);
assert!(size_tracker.size > 0 && size_tracker.size < 1_00);
let size_before_overflow = size_tracker.size;
// try many voters that will overflow the tracker's buffer.
let voter = (2, 10, bounded_vec![2, 3, 4, 5, 6, 7, 8, 9]);
voters.push(voter.clone());
assert!(size_tracker.try_register_voter(&voter, &voter_bounds).is_err());
assert!(size_tracker.size > 0 && size_tracker.size < 1_00);
// size of the tracker did not update when trying to register votes failed.
assert_eq!(size_tracker.size, size_before_overflow);
}
#[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(), StaticTracker::<Staking>::length_prefix(s) + s);
}
}
}
+37 -2
View File
@@ -292,6 +292,7 @@ pub(crate) mod mock;
#[cfg(test)]
mod tests;
pub mod election_size_tracker;
pub mod inflation;
pub mod migrations;
pub mod slashing;
@@ -301,7 +302,7 @@ mod pallet;
use codec::{Decode, Encode, HasCompact, MaxEncodedLen};
use frame_support::{
traits::{Currency, Defensive, Get},
traits::{ConstU32, Currency, Defensive, Get},
weights::Weight,
BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
};
@@ -338,6 +339,10 @@ macro_rules! log {
/// pallet.
pub type MaxWinnersOf<T> = <<T as Config>::ElectionProvider as frame_election_provider_support::ElectionProviderBase>::MaxWinners;
/// Maximum number of nominations per nominator.
pub type MaxNominationsOf<T> =
<<T as Config>::NominationsQuota as NominationsQuota<BalanceOf<T>>>::MaxNominations;
/// Counter for the number of "reward" points earned by a given validator.
pub type RewardPoint = u32;
@@ -679,7 +684,7 @@ impl<T: Config> StakingLedger<T> {
#[scale_info(skip_type_params(T))]
pub struct Nominations<T: Config> {
/// The targets of nomination.
pub targets: BoundedVec<T::AccountId, T::MaxNominations>,
pub targets: BoundedVec<T::AccountId, MaxNominationsOf<T>>,
/// The era the nominations were submitted.
///
/// Except for initial nominations which are considered submitted at era 0.
@@ -749,6 +754,36 @@ impl<AccountId, Balance: HasCompact + Zero> UnappliedSlash<AccountId, Balance> {
}
}
/// Something that defines the maximum number of nominations per nominator based on a curve.
///
/// The method `curve` implements the nomination quota curve and should not be used directly.
/// However, `get_quota` returns the bounded maximum number of nominations based on `fn curve` and
/// the nominator's balance.
pub trait NominationsQuota<Balance> {
/// Strict maximum number of nominations that caps the nominations curve. This value can be
/// used as the upper bound of the number of votes per nominator.
type MaxNominations: Get<u32>;
/// Returns the voter's nomination quota within reasonable bounds [`min`, `max`], where `min`
/// is 1 and `max` is `Self::MaxNominations`.
fn get_quota(balance: Balance) -> u32 {
Self::curve(balance).clamp(1, Self::MaxNominations::get())
}
/// Returns the voter's nomination quota based on its balance and a curve.
fn curve(balance: Balance) -> u32;
}
/// A nomination quota that allows up to MAX nominations for all validators.
pub struct FixedNominationsQuota<const MAX: u32>;
impl<Balance, const MAX: u32> NominationsQuota<Balance> for FixedNominationsQuota<MAX> {
type MaxNominations = ConstU32<MAX>;
fn curve(_: Balance) -> u32 {
MAX
}
}
/// Means for interacting with a specialized version of the `session` trait.
///
/// This is needed because `Staking` sets the `ValidatorIdOf` of the `pallet_session::Config`
+27 -5
View File
@@ -18,7 +18,10 @@
//! Test utilities
use crate::{self as pallet_staking, *};
use frame_election_provider_support::{onchain, SequentialPhragmen, VoteWeight};
use frame_election_provider_support::{
bounds::{ElectionBounds, ElectionBoundsBuilder},
onchain, SequentialPhragmen, VoteWeight,
};
use frame_support::{
assert_ok, ord_parameter_types, parameter_types,
traits::{
@@ -228,11 +231,12 @@ const THRESHOLDS: [sp_npos_elections::VoteWeight; 9] =
parameter_types! {
pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS;
pub static MaxNominations: u32 = 16;
pub static HistoryDepth: u32 = 80;
pub static MaxUnlockingChunks: u32 = 32;
pub static RewardOnUnbalanceWasCalled: bool = false;
pub static MaxWinners: u32 = 100;
pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build();
pub static AbsoluteMaxNominations: u32 = 16;
}
type VoterBagsListInstance = pallet_bags_list::Instance1;
@@ -252,8 +256,7 @@ impl onchain::Config for OnChainSeqPhragmen {
type DataProvider = Staking;
type WeightInfo = ();
type MaxWinners = MaxWinners;
type VotersBound = ConstU32<{ u32::MAX }>;
type TargetsBound = ConstU32<{ u32::MAX }>;
type Bounds = ElectionsBounds;
}
pub struct MockReward {}
@@ -281,7 +284,6 @@ impl OnStakingUpdate<AccountId, Balance> for EventListenerMock {
}
impl crate::pallet::pallet::Config for Test {
type MaxNominations = MaxNominations;
type Currency = Balances;
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
type UnixTime = Timestamp;
@@ -304,6 +306,7 @@ impl crate::pallet::pallet::Config for Test {
// NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well.
type VoterList = VoterBagsList;
type TargetList = UseValidatorsMap<Self>;
type NominationsQuota = WeightedNominationsQuota<16>;
type MaxUnlockingChunks = MaxUnlockingChunks;
type HistoryDepth = HistoryDepth;
type EventListeners = EventListenerMock;
@@ -311,6 +314,25 @@ impl crate::pallet::pallet::Config for Test {
type WeightInfo = ();
}
pub struct WeightedNominationsQuota<const MAX: u32>;
impl<Balance, const MAX: u32> NominationsQuota<Balance> for WeightedNominationsQuota<MAX>
where
u128: From<Balance>,
{
type MaxNominations = AbsoluteMaxNominations;
fn curve(balance: Balance) -> u32 {
match balance.into() {
// random curve for testing.
0..=110 => MAX,
111 => 0,
222 => 2,
333 => MAX + 10,
_ => MAX,
}
}
}
pub(crate) type StakingCall = crate::Call<Test>;
pub(crate) type TestCall = <Test as frame_system::Config>::RuntimeCall;
+85 -42
View File
@@ -18,8 +18,9 @@
//! Implementations for the Staking FRAME Pallet.
use frame_election_provider_support::{
data_provider, BoundedSupportsOf, ElectionDataProvider, ElectionProvider, ScoreProvider,
SortedListProvider, VoteWeight, VoterOf,
bounds::{CountBound, SizeBound},
data_provider, BoundedSupportsOf, DataProviderBounds, ElectionDataProvider, ElectionProvider,
ScoreProvider, SortedListProvider, VoteWeight, VoterOf,
};
use frame_support::{
defensive,
@@ -45,8 +46,9 @@ use sp_staking::{
use sp_std::prelude::*;
use crate::{
log, slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, EraPayout, Exposure, ExposureOf,
Forcing, IndividualExposure, MaxWinnersOf, Nominations, PositiveImbalanceOf, RewardDestination,
election_size_tracker::StaticTracker, log, slashing, weights::WeightInfo, ActiveEraInfo,
BalanceOf, EraPayout, Exposure, ExposureOf, Forcing, IndividualExposure, MaxNominationsOf,
MaxWinnersOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination,
SessionInterface, StakingLedger, ValidatorPrefs,
};
@@ -761,13 +763,15 @@ impl<T: Config> Pallet<T> {
/// nominators.
///
/// This function is self-weighing as [`DispatchClass::Mandatory`].
pub fn get_npos_voters(maybe_max_len: Option<usize>) -> Vec<VoterOf<Self>> {
let max_allowed_len = {
let all_voter_count = T::VoterList::count() as usize;
maybe_max_len.unwrap_or(all_voter_count).min(all_voter_count)
pub fn get_npos_voters(bounds: DataProviderBounds) -> Vec<VoterOf<Self>> {
let mut voters_size_tracker: StaticTracker<Self> = StaticTracker::default();
let final_predicted_len = {
let all_voter_count = T::VoterList::count();
bounds.count.unwrap_or(all_voter_count.into()).min(all_voter_count.into()).0
};
let mut all_voters = Vec::<_>::with_capacity(max_allowed_len);
let mut all_voters = Vec::<_>::with_capacity(final_predicted_len as usize);
// cache a few things.
let weight_of = Self::weight_of_fn();
@@ -778,8 +782,8 @@ impl<T: Config> Pallet<T> {
let mut min_active_stake = u64::MAX;
let mut sorted_voters = T::VoterList::iter();
while all_voters.len() < max_allowed_len &&
voters_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * max_allowed_len as u32)
while all_voters.len() < final_predicted_len as usize &&
voters_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32)
{
let voter = match sorted_voters.next() {
Some(voter) => {
@@ -798,10 +802,23 @@ impl<T: Config> Pallet<T> {
if let Some(Nominations { targets, .. }) = <Nominators<T>>::get(&voter) {
if !targets.is_empty() {
all_voters.push((voter.clone(), voter_weight, targets));
// Note on lazy nomination quota: we do not check the nomination quota of the
// voter at this point and accept all the current nominations. The nomination
// quota is only enforced at `nominate` time.
let voter = (voter, voter_weight, targets);
if voters_size_tracker.try_register_voter(&voter, &bounds).is_err() {
// no more space left for the election result, stop iterating.
Self::deposit_event(Event::<T>::SnapshotVotersSizeExceeded {
size: voters_size_tracker.size as u32,
});
break
}
all_voters.push(voter);
nominators_taken.saturating_inc();
} else {
// Technically should never happen, but not much we can do about it.
// technically should never happen, but not much we can do about it.
}
min_active_stake =
if voter_weight < min_active_stake { voter_weight } else { min_active_stake };
@@ -814,24 +831,31 @@ impl<T: Config> Pallet<T> {
.try_into()
.expect("`MaxVotesPerVoter` must be greater than or equal to 1"),
);
if voters_size_tracker.try_register_voter(&self_vote, &bounds).is_err() {
// no more space left for the election snapshot, stop iterating.
Self::deposit_event(Event::<T>::SnapshotVotersSizeExceeded {
size: voters_size_tracker.size as u32,
});
break
}
all_voters.push(self_vote);
validators_taken.saturating_inc();
} else {
// this can only happen if: 1. there a bug in the bags-list (or whatever is the
// sorted list) logic and the state of the two pallets is no longer compatible, or
// because the nominators is not decodable since they have more nomination than
// `T::MaxNominations`. The latter can rarely happen, and is not really an emergency
// or bug if it does.
log!(
warn,
"DEFENSIVE: invalid item in `VoterList`: {:?}, this nominator probably has too many nominations now",
voter
);
// `T::NominationsQuota::get_quota`. The latter can rarely happen, and is not
// really an emergency or bug if it does.
defensive!(
"DEFENSIVE: invalid item in `VoterList`: {:?}, this nominator probably has too many nominations now",
voter,
);
}
}
// all_voters should have not re-allocated.
debug_assert!(all_voters.capacity() == max_allowed_len);
debug_assert!(all_voters.capacity() == final_predicted_len as usize);
Self::register_weight(T::WeightInfo::get_npos_voters(validators_taken, nominators_taken));
@@ -854,14 +878,20 @@ impl<T: Config> Pallet<T> {
/// Get the targets for an upcoming npos election.
///
/// This function is self-weighing as [`DispatchClass::Mandatory`].
pub fn get_npos_targets(maybe_max_len: Option<usize>) -> Vec<T::AccountId> {
let max_allowed_len = maybe_max_len.unwrap_or_else(|| T::TargetList::count() as usize);
let mut all_targets = Vec::<T::AccountId>::with_capacity(max_allowed_len);
pub fn get_npos_targets(bounds: DataProviderBounds) -> Vec<T::AccountId> {
let mut targets_size_tracker: StaticTracker<Self> = StaticTracker::default();
let final_predicted_len = {
let all_target_count = T::TargetList::count();
bounds.count.unwrap_or(all_target_count.into()).min(all_target_count.into()).0
};
let mut all_targets = Vec::<T::AccountId>::with_capacity(final_predicted_len as usize);
let mut targets_seen = 0;
let mut targets_iter = T::TargetList::iter();
while all_targets.len() < max_allowed_len &&
targets_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * max_allowed_len as u32)
while all_targets.len() < final_predicted_len as usize &&
targets_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32)
{
let target = match targets_iter.next() {
Some(target) => {
@@ -871,6 +901,14 @@ impl<T: Config> Pallet<T> {
None => break,
};
if targets_size_tracker.try_register_target(target.clone(), &bounds).is_err() {
// no more space left for the election snapshot, stop iterating.
Self::deposit_event(Event::<T>::SnapshotTargetsSizeExceeded {
size: targets_size_tracker.size as u32,
});
break
}
if Validators::<T>::contains_key(&target) {
all_targets.push(target);
}
@@ -989,43 +1027,48 @@ impl<T: Config> Pallet<T> {
/// Returns the current nominations quota for nominators.
///
/// Used by the runtime API.
/// Note: for now, this api runtime will always return value of `T::MaxNominations` and thus it
/// is redundant. However, with the upcoming changes in
/// <https://github.com/paritytech/substrate/pull/12970>, the nominations quota will change
/// depending on the nominators balance. We're introducing this runtime API now to prepare the
/// community to use it before rolling out PR#12970.
pub fn api_nominations_quota(_balance: BalanceOf<T>) -> u32 {
T::MaxNominations::get()
pub fn api_nominations_quota(balance: BalanceOf<T>) -> u32 {
T::NominationsQuota::get_quota(balance)
}
}
impl<T: Config> ElectionDataProvider for Pallet<T> {
type AccountId = T::AccountId;
type BlockNumber = BlockNumberFor<T>;
type MaxVotesPerVoter = T::MaxNominations;
type MaxVotesPerVoter = MaxNominationsOf<T>;
fn desired_targets() -> data_provider::Result<u32> {
Self::register_weight(T::DbWeight::get().reads(1));
Ok(Self::validator_count())
}
fn electing_voters(maybe_max_len: Option<usize>) -> data_provider::Result<Vec<VoterOf<Self>>> {
fn electing_voters(bounds: DataProviderBounds) -> data_provider::Result<Vec<VoterOf<Self>>> {
// 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));
let voters = Self::get_npos_voters(bounds);
debug_assert!(!bounds.exhausted(
SizeBound(voters.encoded_size() as u32).into(),
CountBound(voters.len() as u32).into()
));
Ok(voters)
}
fn electable_targets(maybe_max_len: Option<usize>) -> data_provider::Result<Vec<T::AccountId>> {
let target_count = T::TargetList::count();
fn electable_targets(bounds: DataProviderBounds) -> data_provider::Result<Vec<T::AccountId>> {
let targets = Self::get_npos_targets(bounds);
// 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) {
// We can't handle this case yet -- return an error. WIP to improve handling this case in
// <https://github.com/paritytech/substrate/pull/13195>.
if bounds.exhausted(None, CountBound(T::TargetList::count() as u32).into()) {
return Err("Target snapshot too big")
}
Ok(Self::get_npos_targets(None))
debug_assert!(!bounds.exhausted(
SizeBound(targets.encoded_size() as u32).into(),
CountBound(targets.len() as u32).into()
));
Ok(targets)
}
fn next_election_prediction(now: BlockNumberFor<T>) -> BlockNumberFor<T> {
+17 -10
View File
@@ -45,9 +45,9 @@ pub use impls::*;
use crate::{
slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout,
EraRewardPoints, Exposure, Forcing, NegativeImbalanceOf, Nominations, PositiveImbalanceOf,
RewardDestination, SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk,
ValidatorPrefs,
EraRewardPoints, Exposure, Forcing, MaxNominationsOf, NegativeImbalanceOf, Nominations,
NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, StakingLedger,
UnappliedSlash, UnlockChunk, ValidatorPrefs,
};
const STAKING_ID: LockIdentifier = *b"staking ";
@@ -129,9 +129,8 @@ pub mod pallet {
DataProvider = Pallet<Self>,
>;
/// Maximum number of nominations per nominator.
#[pallet::constant]
type MaxNominations: Get<u32>;
/// Something that defines the maximum number of nominations per nominator.
type NominationsQuota: NominationsQuota<BalanceOf<Self>>;
/// Number of eras to keep in history.
///
@@ -348,7 +347,8 @@ pub mod pallet {
/// they wish to support.
///
/// Note that the keys of this storage map might become non-decodable in case the
/// [`Config::MaxNominations`] configuration is decreased. In this rare case, these nominators
/// account's [`NominationsQuota::MaxNominations`] configuration is decreased.
/// In this rare case, these nominators
/// are still existent in storage, their key is correct and retrievable (i.e. `contains_key`
/// indicates that they exist), but their value cannot be decoded. Therefore, the non-decodable
/// nominators will effectively not-exist, until they re-submit their preferences such that it
@@ -696,6 +696,10 @@ pub mod pallet {
PayoutStarted { era_index: EraIndex, validator_stash: T::AccountId },
/// A validator has set their preferences.
ValidatorPrefsSet { stash: T::AccountId, prefs: ValidatorPrefs },
/// Voters size limit reached.
SnapshotVotersSizeExceeded { size: u32 },
/// Targets size limit reached.
SnapshotTargetsSizeExceeded { size: u32 },
/// A new force era mode was set.
ForceEra { mode: Forcing },
}
@@ -782,11 +786,11 @@ pub mod pallet {
fn integrity_test() {
// ensure that we funnel the correct value to the `DataProvider::MaxVotesPerVoter`;
assert_eq!(
T::MaxNominations::get(),
MaxNominationsOf::<T>::get(),
<Self as ElectionDataProvider>::MaxVotesPerVoter::get()
);
// and that MaxNominations is always greater than 1, since we count on this.
assert!(!T::MaxNominations::get().is_zero());
assert!(!MaxNominationsOf::<T>::get().is_zero());
// ensure election results are always bounded with the same value
assert!(
@@ -1145,7 +1149,10 @@ pub mod pallet {
}
ensure!(!targets.is_empty(), Error::<T>::EmptyTargets);
ensure!(targets.len() <= T::MaxNominations::get() as usize, Error::<T>::TooManyTargets);
ensure!(
targets.len() <= T::NominationsQuota::get_quota(ledger.active) as usize,
Error::<T>::TooManyTargets
);
let old = Nominators::<T>::get(stash).map_or_else(Vec::new, |x| x.targets.into_inner());
+269 -51
View File
@@ -18,7 +18,10 @@
//! Tests for the module.
use super::{ConfigOp, Event, *};
use frame_election_provider_support::{ElectionProvider, SortedListProvider, Support};
use frame_election_provider_support::{
bounds::{DataProviderBounds, ElectionBoundsBuilder},
ElectionProvider, SortedListProvider, Support,
};
use frame_support::{
assert_noop, assert_ok, assert_storage_noop, bounded_vec,
dispatch::{extract_actual_weight, GetDispatchInfo, WithPostDispatchInfo},
@@ -4508,12 +4511,16 @@ mod election_data_provider {
.add_staker(71, 71, 10, StakerStatus::<AccountId>::Nominator(vec![21]))
.add_staker(81, 81, 50, StakerStatus::<AccountId>::Nominator(vec![21]))
.build_and_execute(|| {
assert_ok!(<Staking as ElectionDataProvider>::electing_voters(None));
// default bounds are unbounded.
assert_ok!(<Staking as ElectionDataProvider>::electing_voters(
DataProviderBounds::default()
));
assert_eq!(MinimumActiveStake::<Test>::get(), 10);
// remove staker with lower bond by limiting the number of voters and check
// `MinimumActiveStake` again after electing voters.
assert_ok!(<Staking as ElectionDataProvider>::electing_voters(Some(5)));
let bounds = ElectionBoundsBuilder::default().voters_count(5.into()).build();
assert_ok!(<Staking as ElectionDataProvider>::electing_voters(bounds.voters));
assert_eq!(MinimumActiveStake::<Test>::get(), 50);
});
}
@@ -4522,8 +4529,11 @@ mod election_data_provider {
fn set_minimum_active_stake_lower_bond_works() {
// if there are no voters, minimum active stake is zero (should not happen).
ExtBuilder::default().has_stakers(false).build_and_execute(|| {
// default bounds are unbounded.
assert_ok!(<Staking as ElectionDataProvider>::electing_voters(
DataProviderBounds::default()
));
assert_eq!(<Test as Config>::VoterList::count(), 0);
assert_ok!(<Staking as ElectionDataProvider>::electing_voters(None));
assert_eq!(MinimumActiveStake::<Test>::get(), 0);
});
@@ -4537,7 +4547,9 @@ mod election_data_provider {
assert_ok!(Staking::nominate(RuntimeOrigin::signed(4), vec![1]));
assert_eq!(<Test as Config>::VoterList::count(), 5);
let voters_before = <Staking as ElectionDataProvider>::electing_voters(None).unwrap();
let voters_before =
<Staking as ElectionDataProvider>::electing_voters(DataProviderBounds::default())
.unwrap();
assert_eq!(MinimumActiveStake::<Test>::get(), 5);
// update minimum nominator bond.
@@ -4547,7 +4559,9 @@ mod election_data_provider {
// lower than `MinNominatorBond`.
assert_eq!(<Test as Config>::VoterList::count(), 5);
let voters = <Staking as ElectionDataProvider>::electing_voters(None).unwrap();
let voters =
<Staking as ElectionDataProvider>::electing_voters(DataProviderBounds::default())
.unwrap();
assert_eq!(voters_before, voters);
// minimum active stake is lower than `MinNominatorBond`.
@@ -4563,7 +4577,10 @@ mod election_data_provider {
.add_staker(61, 61, 2_000, StakerStatus::<AccountId>::Nominator(vec![21]))
.build_and_execute(|| {
assert_eq!(Staking::weight_of(&101), 500);
let voters = <Staking as ElectionDataProvider>::electing_voters(None).unwrap();
let voters = <Staking as ElectionDataProvider>::electing_voters(
DataProviderBounds::default(),
)
.unwrap();
assert_eq!(voters.len(), 5);
assert_eq!(MinimumActiveStake::<Test>::get(), 500);
@@ -4575,7 +4592,10 @@ mod election_data_provider {
// corrupt ledger state by lowering max unlocking chunks bounds.
MaxUnlockingChunks::set(1);
let voters = <Staking as ElectionDataProvider>::electing_voters(None).unwrap();
let voters = <Staking as ElectionDataProvider>::electing_voters(
DataProviderBounds::default(),
)
.unwrap();
// number of returned voters decreases since ledger entry of stash 101 is now
// corrupt.
assert_eq!(voters.len(), 4);
@@ -4593,8 +4613,9 @@ mod election_data_provider {
#[test]
fn voters_include_self_vote() {
ExtBuilder::default().nominate(false).build_and_execute(|| {
// default bounds are unbounded.
assert!(<Validators<Test>>::iter().map(|(x, _)| x).all(|v| Staking::electing_voters(
None
DataProviderBounds::default()
)
.unwrap()
.into_iter()
@@ -4602,39 +4623,11 @@ mod election_data_provider {
})
}
#[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!(<Test as Config>::VoterList::count(), 5);
// if limits is less..
assert_eq!(Staking::electing_voters(Some(1)).unwrap().len(), 1);
// if limit is equal..
assert_eq!(Staking::electing_voters(Some(5)).unwrap().len(), 5);
// if limit is more.
assert_eq!(Staking::electing_voters(Some(55)).unwrap().len(), 5);
// if target limit is more..
assert_eq!(Staking::electable_targets(Some(6)).unwrap().len(), 4);
assert_eq!(Staking::electable_targets(Some(4)).unwrap().len(), 4);
// if target limit is less, then we return an error.
assert_eq!(
Staking::electable_targets(Some(1)).unwrap_err(),
"Target snapshot too big"
);
});
}
// Tests the criteria that in `ElectionDataProvider::voters` function, we try to get at most
// `maybe_max_len` voters, and if some of them end up being skipped, we iterate at most `2 *
// maybe_max_len`.
#[test]
#[should_panic]
fn only_iterates_max_2_times_max_allowed_len() {
ExtBuilder::default()
.nominate(false)
@@ -4659,13 +4652,14 @@ mod election_data_provider {
StakerStatus::<AccountId>::Nominator(vec![21, 22, 23, 24, 25]),
)
.build_and_execute(|| {
let bounds_builder = ElectionBoundsBuilder::default();
// all voters ordered by stake,
assert_eq!(
<Test as Config>::VoterList::iter().collect::<Vec<_>>(),
vec![61, 71, 81, 11, 21, 31]
);
MaxNominations::set(2);
AbsoluteMaxNominations::set(2);
// we want 2 voters now, and in maximum we allow 4 iterations. This is what happens:
// 61 is pruned;
@@ -4674,7 +4668,7 @@ mod election_data_provider {
// 11 is taken;
// we finish since the 2x limit is reached.
assert_eq!(
Staking::electing_voters(Some(2))
Staking::electing_voters(bounds_builder.voters_count(2.into()).build().voters)
.unwrap()
.iter()
.map(|(stash, _, _)| stash)
@@ -4685,6 +4679,189 @@ mod election_data_provider {
});
}
#[test]
fn respects_snapshot_count_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!(<Test as Config>::VoterList::count(), 5);
let bounds_builder = ElectionBoundsBuilder::default();
// if voter count limit is less..
assert_eq!(
Staking::electing_voters(bounds_builder.voters_count(1.into()).build().voters)
.unwrap()
.len(),
1
);
// if voter count limit is equal..
assert_eq!(
Staking::electing_voters(bounds_builder.voters_count(5.into()).build().voters)
.unwrap()
.len(),
5
);
// if voter count limit is more.
assert_eq!(
Staking::electing_voters(bounds_builder.voters_count(55.into()).build().voters)
.unwrap()
.len(),
5
);
// if target count limit is more..
assert_eq!(
Staking::electable_targets(
bounds_builder.targets_count(6.into()).build().targets
)
.unwrap()
.len(),
4
);
// if target count limit is equal..
assert_eq!(
Staking::electable_targets(
bounds_builder.targets_count(4.into()).build().targets
)
.unwrap()
.len(),
4
);
// if target limit count is less, then we return an error.
assert_eq!(
Staking::electable_targets(
bounds_builder.targets_count(1.into()).build().targets
)
.unwrap_err(),
"Target snapshot too big"
);
});
}
#[test]
fn respects_snapshot_size_limits() {
ExtBuilder::default().build_and_execute(|| {
// voters: set size bounds that allows only for 1 voter.
let bounds = ElectionBoundsBuilder::default().voters_size(26.into()).build();
let elected = Staking::electing_voters(bounds.voters).unwrap();
assert!(elected.encoded_size() == 26 as usize);
let prev_len = elected.len();
// larger size bounds means more quota for voters.
let bounds = ElectionBoundsBuilder::default().voters_size(100.into()).build();
let elected = Staking::electing_voters(bounds.voters).unwrap();
assert!(elected.encoded_size() <= 100 as usize);
assert!(elected.len() > 1 && elected.len() > prev_len);
// targets: set size bounds that allows for only one target to fit in the snapshot.
let bounds = ElectionBoundsBuilder::default().targets_size(10.into()).build();
let elected = Staking::electable_targets(bounds.targets).unwrap();
assert!(elected.encoded_size() == 9 as usize);
let prev_len = elected.len();
// larger size bounds means more space for targets.
let bounds = ElectionBoundsBuilder::default().targets_size(100.into()).build();
let elected = Staking::electable_targets(bounds.targets).unwrap();
assert!(elected.encoded_size() <= 100 as usize);
assert!(elected.len() > 1 && elected.len() > prev_len);
});
}
#[test]
fn nomination_quota_checks_at_nominate_works() {
ExtBuilder::default().nominate(false).build_and_execute(|| {
// stash bond of 222 has a nomination quota of 2 targets.
bond(61, 222);
assert_eq!(Staking::api_nominations_quota(222), 2);
// nominating with targets below the nomination quota works.
assert_ok!(Staking::nominate(RuntimeOrigin::signed(61), vec![11]));
assert_ok!(Staking::nominate(RuntimeOrigin::signed(61), vec![11, 12]));
// nominating with targets above the nomination quota returns error.
assert_noop!(
Staking::nominate(RuntimeOrigin::signed(61), vec![11, 12, 13]),
Error::<Test>::TooManyTargets
);
});
}
#[test]
fn lazy_quota_npos_voters_works_above_quota() {
ExtBuilder::default()
.nominate(false)
.add_staker(
61,
60,
300, // 300 bond has 16 nomination quota.
StakerStatus::<AccountId>::Nominator(vec![21, 22, 23, 24, 25]),
)
.build_and_execute(|| {
// unbond 78 from stash 60 so that it's bonded balance is 222, which has a lower
// nomination quota than at nomination time (max 2 targets).
assert_ok!(Staking::unbond(RuntimeOrigin::signed(61), 78));
assert_eq!(Staking::api_nominations_quota(300 - 78), 2);
// even through 61 has nomination quota of 2 at the time of the election, all the
// nominations (5) will be used.
assert_eq!(
Staking::electing_voters(DataProviderBounds::default())
.unwrap()
.iter()
.map(|(stash, _, targets)| (*stash, targets.len()))
.collect::<Vec<_>>(),
vec![(11, 1), (21, 1), (31, 1), (61, 5)],
);
});
}
#[test]
fn nominations_quota_limits_size_work() {
ExtBuilder::default()
.nominate(false)
.add_staker(
71,
70,
333,
StakerStatus::<AccountId>::Nominator(vec![16, 15, 14, 13, 12, 11, 10]),
)
.build_and_execute(|| {
// nominations of controller 70 won't be added due to voter size limit exceeded.
let bounds = ElectionBoundsBuilder::default().voters_size(100.into()).build();
assert_eq!(
Staking::electing_voters(bounds.voters)
.unwrap()
.iter()
.map(|(stash, _, targets)| (*stash, targets.len()))
.collect::<Vec<_>>(),
vec![(11, 1), (21, 1), (31, 1)],
);
assert_eq!(
*staking_events().last().unwrap(),
Event::SnapshotVotersSizeExceeded { size: 75 }
);
// however, if the election voter size bounds were largers, the snapshot would
// include the electing voters of 70.
let bounds = ElectionBoundsBuilder::default().voters_size(1_000.into()).build();
assert_eq!(
Staking::electing_voters(bounds.voters)
.unwrap()
.iter()
.map(|(stash, _, targets)| (*stash, targets.len()))
.collect::<Vec<_>>(),
vec![(11, 1), (21, 1), (31, 1), (71, 7)],
);
});
}
#[test]
fn estimate_next_election_works() {
ExtBuilder::default().session_per_era(5).period(5).build_and_execute(|| {
@@ -5120,7 +5297,8 @@ fn min_commission_works() {
}
#[test]
fn change_of_max_nominations() {
#[should_panic]
fn change_of_absolute_max_nominations() {
use frame_election_provider_support::ElectionDataProvider;
ExtBuilder::default()
.add_staker(61, 61, 10, StakerStatus::Nominator(vec![1]))
@@ -5128,7 +5306,7 @@ fn change_of_max_nominations() {
.balance_factor(10)
.build_and_execute(|| {
// pre-condition
assert_eq!(MaxNominations::get(), 16);
assert_eq!(AbsoluteMaxNominations::get(), 16);
assert_eq!(
Nominators::<Test>::iter()
@@ -5136,11 +5314,15 @@ fn change_of_max_nominations() {
.collect::<Vec<_>>(),
vec![(101, 2), (71, 3), (61, 1)]
);
// default bounds are unbounded.
let bounds = DataProviderBounds::default();
// 3 validators and 3 nominators
assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 3);
assert_eq!(Staking::electing_voters(bounds).unwrap().len(), 3 + 3);
// abrupt change from 16 to 4, everyone should be fine.
MaxNominations::set(4);
AbsoluteMaxNominations::set(4);
assert_eq!(
Nominators::<Test>::iter()
@@ -5148,10 +5330,10 @@ fn change_of_max_nominations() {
.collect::<Vec<_>>(),
vec![(101, 2), (71, 3), (61, 1)]
);
assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 3);
assert_eq!(Staking::electing_voters(bounds).unwrap().len(), 3 + 3);
// abrupt change from 4 to 3, everyone should be fine.
MaxNominations::set(3);
AbsoluteMaxNominations::set(3);
assert_eq!(
Nominators::<Test>::iter()
@@ -5159,11 +5341,11 @@ fn change_of_max_nominations() {
.collect::<Vec<_>>(),
vec![(101, 2), (71, 3), (61, 1)]
);
assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 3);
assert_eq!(Staking::electing_voters(bounds).unwrap().len(), 3 + 3);
// abrupt change from 3 to 2, this should cause some nominators to be non-decodable, and
// thus non-existent unless if they update.
MaxNominations::set(2);
AbsoluteMaxNominations::set(2);
assert_eq!(
Nominators::<Test>::iter()
@@ -5176,12 +5358,12 @@ fn change_of_max_nominations() {
// but its value cannot be decoded and default is returned.
assert!(Nominators::<Test>::get(71).is_none());
assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 2);
assert_eq!(Staking::electing_voters(bounds).unwrap().len(), 3 + 2);
assert!(Nominators::<Test>::contains_key(101));
// abrupt change from 2 to 1, this should cause some nominators to be non-decodable, and
// thus non-existent unless if they update.
MaxNominations::set(1);
AbsoluteMaxNominations::set(1);
assert_eq!(
Nominators::<Test>::iter()
@@ -5193,7 +5375,7 @@ fn change_of_max_nominations() {
assert!(Nominators::<Test>::contains_key(61));
assert!(Nominators::<Test>::get(71).is_none());
assert!(Nominators::<Test>::get(61).is_some());
assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 1);
assert_eq!(Staking::electing_voters(bounds).unwrap().len(), 3 + 1);
// now one of them can revive themselves by re-nominating to a proper value.
assert_ok!(Staking::nominate(RuntimeOrigin::signed(71), vec![1]));
@@ -5213,6 +5395,42 @@ fn change_of_max_nominations() {
})
}
#[test]
fn nomination_quota_max_changes_decoding() {
use frame_election_provider_support::ElectionDataProvider;
ExtBuilder::default()
.add_staker(60, 61, 10, StakerStatus::Nominator(vec![1]))
.add_staker(70, 71, 10, StakerStatus::Nominator(vec![1, 2, 3]))
.add_staker(30, 330, 10, StakerStatus::Nominator(vec![1, 2, 3, 4]))
.add_staker(50, 550, 10, StakerStatus::Nominator(vec![1, 2, 3, 4]))
.balance_factor(10)
.build_and_execute(|| {
// pre-condition.
assert_eq!(MaxNominationsOf::<Test>::get(), 16);
let unbonded_election = DataProviderBounds::default();
assert_eq!(
Nominators::<Test>::iter()
.map(|(k, n)| (k, n.targets.len()))
.collect::<Vec<_>>(),
vec![(70, 3), (101, 2), (50, 4), (30, 4), (60, 1)]
);
// 4 validators and 4 nominators
assert_eq!(Staking::electing_voters(unbonded_election).unwrap().len(), 4 + 4);
});
}
#[test]
fn api_nominations_quota_works() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(Staking::api_nominations_quota(10), MaxNominationsOf::<Test>::get());
assert_eq!(Staking::api_nominations_quota(333), MaxNominationsOf::<Test>::get());
assert_eq!(Staking::api_nominations_quota(222), 2);
assert_eq!(Staking::api_nominations_quota(111), 1);
})
}
mod sorted_list_provider {
use super::*;
use frame_election_provider_support::SortedListProvider;