[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 -11
View File
@@ -24,6 +24,7 @@
use codec::{Decode, Encode, MaxEncodedLen}; use codec::{Decode, Encode, MaxEncodedLen};
use frame_election_provider_support::{ use frame_election_provider_support::{
bounds::{ElectionBounds, ElectionBoundsBuilder},
onchain, BalancingConfig, ElectionDataProvider, SequentialPhragmen, VoteWeight, onchain, BalancingConfig, ElectionDataProvider, SequentialPhragmen, VoteWeight,
}; };
use frame_support::{ use frame_support::{
@@ -562,6 +563,9 @@ parameter_types! {
pub HistoryDepth: u32 = 84; pub HistoryDepth: u32 = 84;
} }
/// Upper limit on the number of NPOS nominations.
const MAX_QUOTA_NOMINATIONS: u32 = 16;
pub struct StakingBenchmarkingConfig; pub struct StakingBenchmarkingConfig;
impl pallet_staking::BenchmarkingConfig for StakingBenchmarkingConfig { impl pallet_staking::BenchmarkingConfig for StakingBenchmarkingConfig {
type MaxNominators = ConstU32<1000>; type MaxNominators = ConstU32<1000>;
@@ -569,7 +573,6 @@ impl pallet_staking::BenchmarkingConfig for StakingBenchmarkingConfig {
} }
impl pallet_staking::Config for Runtime { impl pallet_staking::Config for Runtime {
type MaxNominations = MaxNominations;
type Currency = Balances; type Currency = Balances;
type CurrencyBalance = Balance; type CurrencyBalance = Balance;
type UnixTime = Timestamp; type UnixTime = Timestamp;
@@ -594,6 +597,7 @@ impl pallet_staking::Config for Runtime {
type ElectionProvider = ElectionProviderMultiPhase; type ElectionProvider = ElectionProviderMultiPhase;
type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>; type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type VoterList = VoterList; type VoterList = VoterList;
type NominationsQuota = pallet_staking::FixedNominationsQuota<MAX_QUOTA_NOMINATIONS>;
// This a placeholder, to be introduced in the next PR as an instance of bags-list // This a placeholder, to be introduced in the next PR as an instance of bags-list
type TargetList = pallet_staking::UseValidatorsMap<Self>; type TargetList = pallet_staking::UseValidatorsMap<Self>;
type MaxUnlockingChunks = ConstU32<32>; type MaxUnlockingChunks = ConstU32<32>;
@@ -647,17 +651,20 @@ frame_election_provider_support::generate_solution_type!(
VoterIndex = u32, VoterIndex = u32,
TargetIndex = u16, TargetIndex = u16,
Accuracy = sp_runtime::PerU16, Accuracy = sp_runtime::PerU16,
MaxVoters = MaxElectingVoters, MaxVoters = MaxElectingVotersSolution,
>(16) >(16)
); );
parameter_types! { parameter_types! {
// Note: the EPM in this runtime runs the election on-chain. The election bounds must be
// carefully set so that an election round fits in one block.
pub ElectionBoundsMultiPhase: ElectionBounds = ElectionBoundsBuilder::default()
.voters_count(10_000.into()).targets_count(1_500.into()).build();
pub ElectionBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default()
.voters_count(5_000.into()).targets_count(1_250.into()).build();
pub MaxNominations: u32 = <NposSolution16 as frame_election_provider_support::NposSolution>::LIMIT as u32; pub MaxNominations: u32 = <NposSolution16 as frame_election_provider_support::NposSolution>::LIMIT as u32;
pub MaxElectingVoters: u32 = 40_000; pub MaxElectingVotersSolution: u32 = 40_000;
pub MaxElectableTargets: u16 = 10_000;
// OnChain values are lower.
pub MaxOnChainElectingVoters: u32 = 5000;
pub MaxOnChainElectableTargets: u16 = 1250;
// The maximum winners that can be elected by the Election pallet which is equivalent to the // The maximum winners that can be elected by the Election pallet which is equivalent to the
// maximum active validators the staking pallet can have. // maximum active validators the staking pallet can have.
pub MaxActiveValidators: u32 = 1000; pub MaxActiveValidators: u32 = 1000;
@@ -712,8 +719,7 @@ impl onchain::Config for OnChainSeqPhragmen {
type DataProvider = <Runtime as pallet_election_provider_multi_phase::Config>::DataProvider; type DataProvider = <Runtime as pallet_election_provider_multi_phase::Config>::DataProvider;
type WeightInfo = frame_election_provider_support::weights::SubstrateWeight<Runtime>; type WeightInfo = frame_election_provider_support::weights::SubstrateWeight<Runtime>;
type MaxWinners = <Runtime as pallet_election_provider_multi_phase::Config>::MaxWinners; type MaxWinners = <Runtime as pallet_election_provider_multi_phase::Config>::MaxWinners;
type VotersBound = MaxOnChainElectingVoters; type Bounds = ElectionBoundsOnChain;
type TargetsBound = MaxOnChainElectableTargets;
} }
impl pallet_election_provider_multi_phase::MinerConfig for Runtime { impl pallet_election_provider_multi_phase::MinerConfig for Runtime {
@@ -761,9 +767,8 @@ impl pallet_election_provider_multi_phase::Config for Runtime {
type GovernanceFallback = onchain::OnChainExecution<OnChainSeqPhragmen>; type GovernanceFallback = onchain::OnChainExecution<OnChainSeqPhragmen>;
type Solver = SequentialPhragmen<AccountId, SolutionAccuracyOf<Self>, OffchainRandomBalancing>; type Solver = SequentialPhragmen<AccountId, SolutionAccuracyOf<Self>, OffchainRandomBalancing>;
type ForceOrigin = EnsureRootOrHalfCouncil; type ForceOrigin = EnsureRootOrHalfCouncil;
type MaxElectableTargets = MaxElectableTargets;
type MaxWinners = MaxActiveValidators; type MaxWinners = MaxActiveValidators;
type MaxElectingVoters = MaxElectingVoters; type ElectionBounds = ElectionBoundsMultiPhase;
type BenchmarkingConfig = ElectionProviderBenchmarkConfig; type BenchmarkingConfig = ElectionProviderBenchmarkConfig;
type WeightInfo = pallet_election_provider_multi_phase::weights::SubstrateWeight<Self>; type WeightInfo = pallet_election_provider_multi_phase::weights::SubstrateWeight<Self>;
} }
+8 -4
View File
@@ -19,12 +19,16 @@
use crate::{self as pallet_babe, Config, CurrentSlot}; use crate::{self as pallet_babe, Config, CurrentSlot};
use codec::Encode; use codec::Encode;
use frame_election_provider_support::{onchain, SequentialPhragmen}; use frame_election_provider_support::{
bounds::{ElectionBounds, ElectionBoundsBuilder},
onchain, SequentialPhragmen,
};
use frame_support::{ use frame_support::{
parameter_types, parameter_types,
traits::{ConstU128, ConstU32, ConstU64, KeyOwnerProofSystem, OnInitialize}, traits::{ConstU128, ConstU32, ConstU64, KeyOwnerProofSystem, OnInitialize},
}; };
use pallet_session::historical as pallet_session_historical; use pallet_session::historical as pallet_session_historical;
use pallet_staking::FixedNominationsQuota;
use sp_consensus_babe::{AuthorityId, AuthorityPair, Randomness, Slot, VrfSignature}; use sp_consensus_babe::{AuthorityId, AuthorityPair, Randomness, Slot, VrfSignature};
use sp_core::{ use sp_core::{
crypto::{KeyTypeId, Pair, VrfSecret}, crypto::{KeyTypeId, Pair, VrfSecret},
@@ -161,6 +165,7 @@ parameter_types! {
pub const SlashDeferDuration: EraIndex = 0; pub const SlashDeferDuration: EraIndex = 0;
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(16); pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(16);
pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build();
} }
pub struct OnChainSeqPhragmen; pub struct OnChainSeqPhragmen;
@@ -170,12 +175,10 @@ impl onchain::Config for OnChainSeqPhragmen {
type DataProvider = Staking; type DataProvider = Staking;
type WeightInfo = (); type WeightInfo = ();
type MaxWinners = ConstU32<100>; type MaxWinners = ConstU32<100>;
type VotersBound = ConstU32<{ u32::MAX }>; type Bounds = ElectionsBounds;
type TargetsBound = ConstU32<{ u32::MAX }>;
} }
impl pallet_staking::Config for Test { impl pallet_staking::Config for Test {
type MaxNominations = ConstU32<16>;
type RewardRemainder = (); type RewardRemainder = ();
type CurrencyToVote = (); type CurrencyToVote = ();
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
@@ -197,6 +200,7 @@ impl pallet_staking::Config for Test {
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type TargetList = pallet_staking::UseValidatorsMap<Self>; type TargetList = pallet_staking::UseValidatorsMap<Self>;
type NominationsQuota = FixedNominationsQuota<16>;
type MaxUnlockingChunks = ConstU32<32>; type MaxUnlockingChunks = ConstU32<32>;
type HistoryDepth = ConstU32<84>; type HistoryDepth = ConstU32<84>;
type EventListeners = (); type EventListeners = ();
@@ -16,7 +16,10 @@
//! Test to execute the snapshot using the voter bag. //! Test to execute the snapshot using the voter bag.
use frame_election_provider_support::SortedListProvider; use frame_election_provider_support::{
bounds::{CountBound, DataProviderBounds},
SortedListProvider,
};
use frame_support::traits::PalletInfoAccess; use frame_support::traits::PalletInfoAccess;
use remote_externalities::{Builder, Mode, OnlineConfig}; use remote_externalities::{Builder, Mode, OnlineConfig};
use sp_runtime::{traits::Block as BlockT, DeserializeOwned}; use sp_runtime::{traits::Block as BlockT, DeserializeOwned};
@@ -62,8 +65,13 @@ where
<Runtime as pallet_staking::Config>::VoterList::count(), <Runtime as pallet_staking::Config>::VoterList::count(),
); );
let bounds = match voter_limit {
None => DataProviderBounds::default(),
Some(v) => DataProviderBounds { count: Some(CountBound(v as u32)), size: None },
};
let voters = let voters =
<pallet_staking::Pallet<Runtime> as ElectionDataProvider>::electing_voters(voter_limit) <pallet_staking::Pallet<Runtime> as ElectionDataProvider>::electing_voters(bounds)
.unwrap(); .unwrap();
let mut voters_nominator_only = voters let mut voters_nominator_only = voters
+7 -4
View File
@@ -17,7 +17,10 @@
use std::vec; use std::vec;
use frame_election_provider_support::{onchain, SequentialPhragmen}; use frame_election_provider_support::{
bounds::{ElectionBounds, ElectionBoundsBuilder},
onchain, SequentialPhragmen,
};
use frame_support::{ use frame_support::{
construct_runtime, parameter_types, construct_runtime, parameter_types,
sp_io::TestExternalities, sp_io::TestExternalities,
@@ -184,6 +187,7 @@ parameter_types! {
pub const BondingDuration: EraIndex = 3; pub const BondingDuration: EraIndex = 3;
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17); pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build();
} }
pub struct OnChainSeqPhragmen; pub struct OnChainSeqPhragmen;
@@ -193,12 +197,10 @@ impl onchain::Config for OnChainSeqPhragmen {
type DataProvider = Staking; type DataProvider = Staking;
type WeightInfo = (); type WeightInfo = ();
type MaxWinners = ConstU32<100>; type MaxWinners = ConstU32<100>;
type VotersBound = ConstU32<{ u32::MAX }>; type Bounds = ElectionsBoundsOnChain;
type TargetsBound = ConstU32<{ u32::MAX }>;
} }
impl pallet_staking::Config for Test { impl pallet_staking::Config for Test {
type MaxNominations = ConstU32<16>;
type RewardRemainder = (); type RewardRemainder = ();
type CurrencyToVote = (); type CurrencyToVote = ();
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
@@ -220,6 +222,7 @@ impl pallet_staking::Config for Test {
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type TargetList = pallet_staking::UseValidatorsMap<Self>; type TargetList = pallet_staking::UseValidatorsMap<Self>;
type NominationsQuota = pallet_staking::FixedNominationsQuota<16>;
type MaxUnlockingChunks = ConstU32<32>; type MaxUnlockingChunks = ConstU32<32>;
type HistoryDepth = ConstU32<84>; type HistoryDepth = ConstU32<84>;
type EventListeners = (); type EventListeners = ();
@@ -20,6 +20,7 @@
use super::*; use super::*;
use crate::{unsigned::IndexAssignmentOf, Pallet as MultiPhase}; use crate::{unsigned::IndexAssignmentOf, Pallet as MultiPhase};
use frame_benchmarking::account; use frame_benchmarking::account;
use frame_election_provider_support::bounds::DataProviderBounds;
use frame_support::{ use frame_support::{
assert_ok, assert_ok,
traits::{Hooks, TryCollect}, traits::{Hooks, TryCollect},
@@ -270,8 +271,9 @@ frame_benchmarking::benchmarks! {
// we don't directly need the data-provider to be populated, but it is just easy to use it. // we don't directly need the data-provider to be populated, but it is just easy to use it.
set_up_data_provider::<T>(v, t); set_up_data_provider::<T>(v, t);
let targets = T::DataProvider::electable_targets(None)?; // default bounds are unbounded.
let voters = T::DataProvider::electing_voters(None)?; let targets = T::DataProvider::electable_targets(DataProviderBounds::default())?;
let voters = T::DataProvider::electing_voters(DataProviderBounds::default())?;
let desired_targets = T::DataProvider::desired_targets()?; let desired_targets = T::DataProvider::desired_targets()?;
assert!(<MultiPhase<T>>::snapshot().is_none()); assert!(<MultiPhase<T>>::snapshot().is_none());
}: { }: {
@@ -231,8 +231,9 @@
use codec::{Decode, Encode}; use codec::{Decode, Encode};
use frame_election_provider_support::{ use frame_election_provider_support::{
BoundedSupportsOf, ElectionDataProvider, ElectionProvider, ElectionProviderBase, bounds::{CountBound, ElectionBounds, ElectionBoundsBuilder, SizeBound},
InstantElectionProvider, NposSolution, BoundedSupportsOf, DataProviderBounds, ElectionDataProvider, ElectionProvider,
ElectionProviderBase, InstantElectionProvider, NposSolution,
}; };
use frame_support::{ use frame_support::{
dispatch::DispatchClass, dispatch::DispatchClass,
@@ -659,16 +660,6 @@ pub mod pallet {
#[pallet::constant] #[pallet::constant]
type SignedDepositWeight: Get<BalanceOf<Self>>; type SignedDepositWeight: Get<BalanceOf<Self>>;
/// The maximum number of electing 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.
#[pallet::constant]
type MaxElectingVoters: Get<SolutionVoterIndexOf<Self::MinerConfig>>;
/// The maximum number of electable targets to put in the snapshot.
#[pallet::constant]
type MaxElectableTargets: Get<SolutionTargetIndexOf<Self::MinerConfig>>;
/// The maximum number of winners that can be elected by this `ElectionProvider` /// The maximum number of winners that can be elected by this `ElectionProvider`
/// implementation. /// implementation.
/// ///
@@ -676,6 +667,11 @@ pub mod pallet {
#[pallet::constant] #[pallet::constant]
type MaxWinners: Get<u32>; type MaxWinners: Get<u32>;
/// The maximum number of electing voters and electable targets 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.
type ElectionBounds: Get<ElectionBounds>;
/// Handler for the slashed deposits. /// Handler for the slashed deposits.
type SlashHandler: OnUnbalanced<NegativeImbalanceOf<Self>>; type SlashHandler: OnUnbalanced<NegativeImbalanceOf<Self>>;
@@ -1097,13 +1093,19 @@ pub mod pallet {
T::ForceOrigin::ensure_origin(origin)?; T::ForceOrigin::ensure_origin(origin)?;
ensure!(Self::current_phase().is_emergency(), <Error<T>>::CallNotAllowed); ensure!(Self::current_phase().is_emergency(), <Error<T>>::CallNotAllowed);
let supports = let election_bounds = ElectionBoundsBuilder::default()
T::GovernanceFallback::instant_elect(maybe_max_voters, maybe_max_targets).map_err( .voters_count(maybe_max_voters.unwrap_or(u32::MAX).into())
|e| { .targets_count(maybe_max_targets.unwrap_or(u32::MAX).into())
.build();
let supports = T::GovernanceFallback::instant_elect(
election_bounds.voters,
election_bounds.targets,
)
.map_err(|e| {
log!(error, "GovernanceFallback failed: {:?}", e); log!(error, "GovernanceFallback failed: {:?}", e);
Error::<T>::FallbackFailed Error::<T>::FallbackFailed
}, })?;
)?;
// transform BoundedVec<_, T::GovernanceFallback::MaxWinners> into // transform BoundedVec<_, T::GovernanceFallback::MaxWinners> into
// `BoundedVec<_, T::MaxWinners>` // `BoundedVec<_, T::MaxWinners>`
@@ -1426,19 +1428,28 @@ impl<T: Config> Pallet<T> {
/// Extracted for easier weight calculation. /// Extracted for easier weight calculation.
fn create_snapshot_external( fn create_snapshot_external(
) -> Result<(Vec<T::AccountId>, Vec<VoterOf<T>>, u32), ElectionError<T>> { ) -> Result<(Vec<T::AccountId>, Vec<VoterOf<T>>, u32), ElectionError<T>> {
let target_limit = T::MaxElectableTargets::get().saturated_into::<usize>(); let election_bounds = T::ElectionBounds::get();
let voter_limit = T::MaxElectingVoters::get().saturated_into::<usize>();
let targets = T::DataProvider::electable_targets(Some(target_limit)) let targets = T::DataProvider::electable_targets(election_bounds.targets)
.and_then(|t| {
election_bounds.ensure_targets_limits(
CountBound(t.len() as u32),
SizeBound(t.encoded_size() as u32),
)?;
Ok(t)
})
.map_err(ElectionError::DataProvider)?; .map_err(ElectionError::DataProvider)?;
let voters = T::DataProvider::electing_voters(Some(voter_limit)) let voters = T::DataProvider::electing_voters(election_bounds.voters)
.and_then(|v| {
election_bounds.ensure_voters_limits(
CountBound(v.len() as u32),
SizeBound(v.encoded_size() as u32),
)?;
Ok(v)
})
.map_err(ElectionError::DataProvider)?; .map_err(ElectionError::DataProvider)?;
if targets.len() > target_limit || voters.len() > voter_limit {
return Err(ElectionError::DataProvider("Snapshot too big for submission."))
}
let mut desired_targets = <Pallet<T> as ElectionProviderBase>::desired_targets_checked() let mut desired_targets = <Pallet<T> as ElectionProviderBase>::desired_targets_checked()
.map_err(|e| ElectionError::DataProvider(e))?; .map_err(|e| ElectionError::DataProvider(e))?;
@@ -1544,10 +1555,17 @@ impl<T: Config> Pallet<T> {
// - signed phase was complete or not started, in which case finalization is idempotent and // - signed phase was complete or not started, in which case finalization is idempotent and
// inexpensive (1 read of an empty vector). // inexpensive (1 read of an empty vector).
let _ = Self::finalize_signed_phase(); let _ = Self::finalize_signed_phase();
<QueuedSolution<T>>::take() <QueuedSolution<T>>::take()
.ok_or(ElectionError::<T>::NothingQueued) .ok_or(ElectionError::<T>::NothingQueued)
.or_else(|_| { .or_else(|_| {
T::Fallback::instant_elect(None, None) // default data provider bounds are unbounded. calling `instant_elect` with
// unbounded data provider bounds means that the on-chain `T:Bounds` configs will
// *not* be overwritten.
T::Fallback::instant_elect(
DataProviderBounds::default(),
DataProviderBounds::default(),
)
.map_err(|fe| ElectionError::Fallback(fe)) .map_err(|fe| ElectionError::Fallback(fe))
.and_then(|supports| { .and_then(|supports| {
Ok(ReadySolution { Ok(ReadySolution {
@@ -1925,8 +1943,8 @@ mod tests {
use crate::{ use crate::{
mock::{ mock::{
multi_phase_events, raw_solution, roll_to, roll_to_signed, roll_to_unsigned, AccountId, multi_phase_events, raw_solution, roll_to, roll_to_signed, roll_to_unsigned, AccountId,
ExtBuilder, MockWeightInfo, MockedWeightInfo, MultiPhase, Runtime, RuntimeOrigin, ElectionsBounds, ExtBuilder, MockWeightInfo, MockedWeightInfo, MultiPhase, Runtime,
SignedMaxSubmissions, System, TargetIndex, Targets, RuntimeOrigin, SignedMaxSubmissions, System, TargetIndex, Targets, Voters,
}, },
Phase, Phase,
}; };
@@ -2529,7 +2547,11 @@ mod tests {
fn snapshot_too_big_failure_onchain_fallback() { fn snapshot_too_big_failure_onchain_fallback() {
// the `MockStaking` is designed such that if it has too many targets, it simply fails. // the `MockStaking` is designed such that if it has too many targets, it simply fails.
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default().build_and_execute(|| {
Targets::set((0..(TargetIndex::max_value() as AccountId) + 1).collect::<Vec<_>>()); // sets bounds on number of targets.
let new_bounds = ElectionBoundsBuilder::default().targets_count(1_000.into()).build();
ElectionsBounds::set(new_bounds);
Targets::set((0..(1_000 as AccountId) + 1).collect::<Vec<_>>());
// Signed phase failed to open. // Signed phase failed to open.
roll_to(15); roll_to(15);
@@ -2564,9 +2586,11 @@ mod tests {
fn snapshot_too_big_failure_no_fallback() { fn snapshot_too_big_failure_no_fallback() {
// and if the backup mode is nothing, we go into the emergency mode.. // and if the backup mode is nothing, we go into the emergency mode..
ExtBuilder::default().onchain_fallback(false).build_and_execute(|| { ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
crate::mock::Targets::set( // sets bounds on number of targets.
(0..(TargetIndex::max_value() as AccountId) + 1).collect::<Vec<_>>(), let new_bounds = ElectionBoundsBuilder::default().targets_count(1_000.into()).build();
); ElectionsBounds::set(new_bounds);
Targets::set((0..(TargetIndex::max_value() as AccountId) + 1).collect::<Vec<_>>());
// Signed phase failed to open. // Signed phase failed to open.
roll_to(15); roll_to(15);
@@ -2596,9 +2620,10 @@ mod tests {
// but if there are too many voters, we simply truncate them. // but if there are too many voters, we simply truncate them.
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default().build_and_execute(|| {
// we have 8 voters in total. // we have 8 voters in total.
assert_eq!(crate::mock::Voters::get().len(), 8); assert_eq!(Voters::get().len(), 8);
// but we want to take 2. // but we want to take 2.
crate::mock::MaxElectingVoters::set(2); let new_bounds = ElectionBoundsBuilder::default().voters_count(2.into()).build();
ElectionsBounds::set(new_bounds);
// Signed phase opens just fine. // Signed phase opens just fine.
roll_to_signed(); roll_to_signed();
@@ -18,9 +18,8 @@
use super::*; use super::*;
use crate::{self as multi_phase, unsigned::MinerConfig}; use crate::{self as multi_phase, unsigned::MinerConfig};
use frame_election_provider_support::{ use frame_election_provider_support::{
data_provider, bounds::{DataProviderBounds, ElectionBounds},
onchain::{self}, data_provider, onchain, ElectionDataProvider, NposSolution, SequentialPhragmen,
ElectionDataProvider, NposSolution, SequentialPhragmen,
}; };
pub use frame_support::{assert_noop, assert_ok, pallet_prelude::GetDefault}; pub use frame_support::{assert_noop, assert_ok, pallet_prelude::GetDefault};
use frame_support::{ use frame_support::{
@@ -300,7 +299,9 @@ parameter_types! {
#[derive(Debug)] #[derive(Debug)]
pub static MaxWinners: u32 = 200; pub static MaxWinners: u32 = 200;
// `ElectionBounds` and `OnChainElectionsBounds` are defined separately to set them independently in the tests.
pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build();
pub static OnChainElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build();
pub static EpochLength: u64 = 30; pub static EpochLength: u64 = 30;
pub static OnChainFallback: bool = true; pub static OnChainFallback: bool = true;
} }
@@ -312,8 +313,7 @@ impl onchain::Config for OnChainSeqPhragmen {
type DataProvider = StakingMock; type DataProvider = StakingMock;
type WeightInfo = (); type WeightInfo = ();
type MaxWinners = MaxWinners; type MaxWinners = MaxWinners;
type VotersBound = ConstU32<{ u32::MAX }>; type Bounds = OnChainElectionsBounds;
type TargetsBound = ConstU32<{ u32::MAX }>;
} }
pub struct MockFallback; pub struct MockFallback;
@@ -327,11 +327,14 @@ impl ElectionProviderBase for MockFallback {
impl InstantElectionProvider for MockFallback { impl InstantElectionProvider for MockFallback {
fn instant_elect( fn instant_elect(
max_voters: Option<u32>, voters_bounds: DataProviderBounds,
max_targets: Option<u32>, targets_bounds: DataProviderBounds,
) -> Result<BoundedSupportsOf<Self>, Self::Error> { ) -> Result<BoundedSupportsOf<Self>, Self::Error> {
if OnChainFallback::get() { if OnChainFallback::get() {
onchain::OnChainExecution::<OnChainSeqPhragmen>::instant_elect(max_voters, max_targets) onchain::OnChainExecution::<OnChainSeqPhragmen>::instant_elect(
voters_bounds,
targets_bounds,
)
.map_err(|_| "onchain::OnChainExecution failed.") .map_err(|_| "onchain::OnChainExecution failed.")
} else { } else {
Err("NoFallback.") Err("NoFallback.")
@@ -404,11 +407,10 @@ impl crate::Config for Runtime {
type GovernanceFallback = type GovernanceFallback =
frame_election_provider_support::onchain::OnChainExecution<OnChainSeqPhragmen>; frame_election_provider_support::onchain::OnChainExecution<OnChainSeqPhragmen>;
type ForceOrigin = frame_system::EnsureRoot<AccountId>; type ForceOrigin = frame_system::EnsureRoot<AccountId>;
type MaxElectingVoters = MaxElectingVoters;
type MaxElectableTargets = MaxElectableTargets;
type MaxWinners = MaxWinners; type MaxWinners = MaxWinners;
type MinerConfig = Self; type MinerConfig = Self;
type Solver = SequentialPhragmen<AccountId, SolutionAccuracyOf<Runtime>, Balancing>; type Solver = SequentialPhragmen<AccountId, SolutionAccuracyOf<Runtime>, Balancing>;
type ElectionBounds = ElectionsBounds;
} }
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime
@@ -436,11 +438,11 @@ impl ElectionDataProvider for StakingMock {
type AccountId = AccountId; type AccountId = AccountId;
type MaxVotesPerVoter = MaxNominations; type MaxVotesPerVoter = MaxNominations;
fn electable_targets(maybe_max_len: Option<usize>) -> data_provider::Result<Vec<AccountId>> { fn electable_targets(bounds: DataProviderBounds) -> data_provider::Result<Vec<AccountId>> {
let targets = Targets::get(); let targets = Targets::get();
if !DataProviderAllowBadData::get() && if !DataProviderAllowBadData::get() &&
maybe_max_len.map_or(false, |max_len| targets.len() > max_len) bounds.count.map_or(false, |max_len| targets.len() > max_len.0 as usize)
{ {
return Err("Targets too big") return Err("Targets too big")
} }
@@ -448,13 +450,12 @@ impl ElectionDataProvider for StakingMock {
Ok(targets) Ok(targets)
} }
fn electing_voters( fn electing_voters(bounds: DataProviderBounds) -> data_provider::Result<Vec<VoterOf<Runtime>>> {
maybe_max_len: Option<usize>,
) -> data_provider::Result<Vec<VoterOf<Runtime>>> {
let mut voters = Voters::get(); let mut voters = Voters::get();
if !DataProviderAllowBadData::get() { if !DataProviderAllowBadData::get() {
if let Some(max_len) = maybe_max_len { if let Some(max_len) = bounds.count {
voters.truncate(max_len) voters.truncate(max_len.0 as usize)
} }
} }
@@ -536,7 +536,10 @@ impl<T: Config> Pallet<T> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{mock::*, ElectionCompute, ElectionError, Error, Event, Perbill, Phase}; use crate::{
mock::*, ElectionBoundsBuilder, ElectionCompute, ElectionError, Error, Event, Perbill,
Phase,
};
use frame_support::{assert_noop, assert_ok, assert_storage_noop}; use frame_support::{assert_noop, assert_ok, assert_storage_noop};
#[test] #[test]
@@ -565,13 +568,14 @@ mod tests {
fn data_provider_should_respect_target_limits() { fn data_provider_should_respect_target_limits() {
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default().build_and_execute(|| {
// given a reduced expectation of maximum electable targets // given a reduced expectation of maximum electable targets
MaxElectableTargets::set(2); let new_bounds = ElectionBoundsBuilder::default().targets_count(2.into()).build();
ElectionsBounds::set(new_bounds);
// and a data provider that does not respect limits // and a data provider that does not respect limits
DataProviderAllowBadData::set(true); DataProviderAllowBadData::set(true);
assert_noop!( assert_noop!(
MultiPhase::create_snapshot(), MultiPhase::create_snapshot(),
ElectionError::DataProvider("Snapshot too big for submission."), ElectionError::DataProvider("Ensure targets bounds: bounds exceeded."),
); );
}) })
} }
@@ -580,13 +584,14 @@ mod tests {
fn data_provider_should_respect_voter_limits() { fn data_provider_should_respect_voter_limits() {
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default().build_and_execute(|| {
// given a reduced expectation of maximum electing voters // given a reduced expectation of maximum electing voters
MaxElectingVoters::set(2); let new_bounds = ElectionBoundsBuilder::default().voters_count(2.into()).build();
ElectionsBounds::set(new_bounds);
// and a data provider that does not respect limits // and a data provider that does not respect limits
DataProviderAllowBadData::set(true); DataProviderAllowBadData::set(true);
assert_noop!( assert_noop!(
MultiPhase::create_snapshot(), MultiPhase::create_snapshot(),
ElectionError::DataProvider("Snapshot too big for submission."), ElectionError::DataProvider("Ensure voters bounds: bounds exceeded."),
); );
}) })
} }
@@ -17,7 +17,6 @@
#![allow(dead_code)] #![allow(dead_code)]
use _feps::ExtendedBalance;
use frame_support::{ use frame_support::{
assert_ok, dispatch::UnfilteredDispatchable, parameter_types, traits, traits::Hooks, assert_ok, dispatch::UnfilteredDispatchable, parameter_types, traits, traits::Hooks,
weights::constants, weights::constants,
@@ -42,7 +41,10 @@ use sp_std::prelude::*;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use codec::Decode; use codec::Decode;
use frame_election_provider_support::{onchain, ElectionDataProvider, SequentialPhragmen, Weight}; use frame_election_provider_support::{
bounds::ElectionBoundsBuilder, onchain, ElectionDataProvider, ExtendedBalance,
SequentialPhragmen, Weight,
};
use pallet_election_provider_multi_phase::{ use pallet_election_provider_multi_phase::{
unsigned::MinerConfig, Call, ElectionCompute, QueuedSolution, SolutionAccuracyOf, unsigned::MinerConfig, Call, ElectionCompute, QueuedSolution, SolutionAccuracyOf,
}; };
@@ -172,8 +174,6 @@ parameter_types! {
// we expect a minimum of 3 blocks in signed phase and unsigned phases before trying // we expect a minimum of 3 blocks in signed phase and unsigned phases before trying
// enetering in emergency phase after the election failed. // enetering in emergency phase after the election failed.
pub static MinBlocksBeforeEmergency: BlockNumber = 3; pub static MinBlocksBeforeEmergency: BlockNumber = 3;
pub static MaxElectingVoters: VoterIndex = 1000;
pub static MaxElectableTargets: TargetIndex = 1000;
pub static MaxActiveValidators: u32 = 1000; pub static MaxActiveValidators: u32 = 1000;
pub static OffchainRepeat: u32 = 5; pub static OffchainRepeat: u32 = 5;
pub static MinerMaxLength: u32 = 256; pub static MinerMaxLength: u32 = 256;
@@ -181,8 +181,8 @@ parameter_types! {
pub static TransactionPriority: transaction_validity::TransactionPriority = 1; pub static TransactionPriority: transaction_validity::TransactionPriority = 1;
#[derive(Debug)] #[derive(Debug)]
pub static MaxWinners: u32 = 100; pub static MaxWinners: u32 = 100;
pub static MaxVotesPerVoter: u32 = 16; pub static ElectionBounds: frame_election_provider_support::bounds::ElectionBounds = ElectionBoundsBuilder::default()
pub static MaxNominations: u32 = 16; .voters_count(1_000.into()).targets_count(1_000.into()).build();
} }
impl pallet_election_provider_multi_phase::Config for Runtime { impl pallet_election_provider_multi_phase::Config for Runtime {
@@ -211,9 +211,8 @@ impl pallet_election_provider_multi_phase::Config for Runtime {
type GovernanceFallback = onchain::OnChainExecution<OnChainSeqPhragmen>; type GovernanceFallback = onchain::OnChainExecution<OnChainSeqPhragmen>;
type Solver = SequentialPhragmen<AccountId, SolutionAccuracyOf<Runtime>, ()>; type Solver = SequentialPhragmen<AccountId, SolutionAccuracyOf<Runtime>, ()>;
type ForceOrigin = EnsureRoot<AccountId>; type ForceOrigin = EnsureRoot<AccountId>;
type MaxElectableTargets = MaxElectableTargets;
type MaxElectingVoters = MaxElectingVoters;
type MaxWinners = MaxWinners; type MaxWinners = MaxWinners;
type ElectionBounds = ElectionBounds;
type BenchmarkingConfig = NoopElectionProviderBenchmarkConfig; type BenchmarkingConfig = NoopElectionProviderBenchmarkConfig;
type WeightInfo = (); type WeightInfo = ();
} }
@@ -252,8 +251,10 @@ impl pallet_bags_list::Config for Runtime {
type Score = VoteWeight; type Score = VoteWeight;
} }
/// Upper limit on the number of NPOS nominations.
const MAX_QUOTA_NOMINATIONS: u32 = 16;
impl pallet_staking::Config for Runtime { impl pallet_staking::Config for Runtime {
type MaxNominations = MaxNominations;
type Currency = Balances; type Currency = Balances;
type CurrencyBalance = Balance; type CurrencyBalance = Balance;
type UnixTime = Timestamp; type UnixTime = Timestamp;
@@ -274,6 +275,7 @@ impl pallet_staking::Config for Runtime {
type ElectionProvider = ElectionProviderMultiPhase; type ElectionProvider = ElectionProviderMultiPhase;
type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>; type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type VoterList = BagsList; type VoterList = BagsList;
type NominationsQuota = pallet_staking::FixedNominationsQuota<MAX_QUOTA_NOMINATIONS>;
type TargetList = pallet_staking::UseValidatorsMap<Self>; type TargetList = pallet_staking::UseValidatorsMap<Self>;
type MaxUnlockingChunks = ConstU32<32>; type MaxUnlockingChunks = ConstU32<32>;
type HistoryDepth = HistoryDepth; type HistoryDepth = HistoryDepth;
@@ -306,8 +308,7 @@ impl onchain::Config for OnChainSeqPhragmen {
type DataProvider = Staking; type DataProvider = Staking;
type WeightInfo = (); type WeightInfo = ();
type MaxWinners = MaxWinners; type MaxWinners = MaxWinners;
type VotersBound = VotersBound; type Bounds = ElectionBounds;
type TargetsBound = TargetsBound;
} }
pub struct NoopElectionProviderBenchmarkConfig; pub struct NoopElectionProviderBenchmarkConfig;
@@ -0,0 +1,460 @@
// 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.
//! Types and helpers to define and handle election bounds.
//!
//! ### Overview
//!
//! This module defines and implements types that help creating and handling election bounds.
//! [`DataProviderBounds`] encapsulates the upper limits for the results provided by `DataProvider`
//! implementors. Those limits can be defined over two axis: number of elements returned (`count`)
//! and/or the size of the returned SCALE encoded structure (`size`).
//!
//! [`ElectionBoundsBuilder`] is a helper to construct data election bounds and it aims at
//! preventing the caller from mistake the order of size and count limits.
//!
//! ### Examples
//!
//! [`ElectionBoundsBuilder`] helps defining the size and count bounds for both voters and targets.
//!
//! ```
//! use frame_election_provider_support::bounds::*;
//!
//! // unbounded limits are never exhausted.
//! let unbounded = ElectionBoundsBuilder::default().build();
//! assert!(!unbounded.targets.exhausted(SizeBound(1_000_000_000).into(), None));
//!
//! let bounds = ElectionBoundsBuilder::default()
//! .voters_count(100.into())
//! .voters_size(1_000.into())
//! .targets_count(200.into())
//! .targets_size(2_000.into())
//! .build();
//!
//! assert!(!bounds.targets.exhausted(SizeBound(1).into(), CountBound(1).into()));
//! assert!(bounds.targets.exhausted(SizeBound(1).into(), CountBound(100_000).into()));
//! ```
//!
//! ### Implementation details
//!
//! A default or `None` bound means that no bounds are enforced (i.e. unlimited result size). In
//! general, be careful when using unbounded election bounds in production.
use core::ops::Add;
use sp_runtime::traits::Zero;
/// Count type for data provider bounds.
///
/// Encapsulates the counting of things that can be bounded in an election, such as voters,
/// targets or anything else.
///
/// This struct is defined mostly to prevent callers from mistankingly using `CountBound` instead of
/// `SizeBound` and vice-versa.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct CountBound(pub u32);
impl From<u32> for CountBound {
fn from(value: u32) -> Self {
CountBound(value)
}
}
impl Add for CountBound {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
CountBound(self.0.saturating_add(rhs.0))
}
}
impl Zero for CountBound {
fn is_zero(&self) -> bool {
self.0 == 0u32
}
fn zero() -> Self {
CountBound(0)
}
}
/// Size type for data provider bounds.
///
/// Encapsulates the size limit of things that can be bounded in an election, such as voters,
/// targets or anything else. The size unit can represent anything depending on the election
/// logic and implementation, but it most likely will represent bytes in SCALE encoding in this
/// context.
///
/// This struct is defined mostly to prevent callers from mistankingly using `CountBound` instead of
/// `SizeBound` and vice-versa.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct SizeBound(pub u32);
impl From<u32> for SizeBound {
fn from(value: u32) -> Self {
SizeBound(value)
}
}
impl Zero for SizeBound {
fn is_zero(&self) -> bool {
self.0 == 0u32
}
fn zero() -> Self {
SizeBound(0)
}
}
impl Add for SizeBound {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
SizeBound(self.0.saturating_add(rhs.0))
}
}
/// Data bounds for election data.
///
/// Limits the data returned by `DataProvider` implementors, defined over two axis: `count`,
/// defining the maximum number of elements returned, and `size`, defining the limit in size
/// (bytes) of the SCALE encoded result.
///
/// `None` represents unlimited bounds in both `count` and `size` axis.
#[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
pub struct DataProviderBounds {
pub count: Option<CountBound>,
pub size: Option<SizeBound>,
}
impl DataProviderBounds {
/// Returns true if `given_count` exhausts `self.count`.
pub fn count_exhausted(self, given_count: CountBound) -> bool {
self.count.map_or(false, |count| given_count > count)
}
/// Returns true if `given_size` exhausts `self.size`.
pub fn size_exhausted(self, given_size: SizeBound) -> bool {
self.size.map_or(false, |size| given_size > size)
}
/// Returns true if `given_size` or `given_count` exhausts `self.size` or `self_count`,
/// respectively.
pub fn exhausted(self, given_size: Option<SizeBound>, given_count: Option<CountBound>) -> bool {
self.count_exhausted(given_count.unwrap_or(CountBound::zero())) ||
self.size_exhausted(given_size.unwrap_or(SizeBound::zero()))
}
/// Returns an instance of `Self` that is constructed by capping both the `count` and `size`
/// fields. If `self` is None, overwrite it with the provided bounds.
pub fn max(self, bounds: DataProviderBounds) -> Self {
DataProviderBounds {
count: self
.count
.map(|c| {
c.clamp(CountBound::zero(), bounds.count.unwrap_or(CountBound(u32::MAX))).into()
})
.or(bounds.count),
size: self
.size
.map(|c| {
c.clamp(SizeBound::zero(), bounds.size.unwrap_or(SizeBound(u32::MAX))).into()
})
.or(bounds.size),
}
}
}
/// The voter and target bounds of an election.
///
/// The bounds are defined over two axis: `count` of element of the election (voters or targets) and
/// the `size` of the SCALE encoded result snapshot.
#[derive(Clone, Debug, Copy)]
pub struct ElectionBounds {
pub voters: DataProviderBounds,
pub targets: DataProviderBounds,
}
impl ElectionBounds {
/// Returns an error if the provided `count` and `size` do not fit in the voter's election
/// bounds.
pub fn ensure_voters_limits(
self,
count: CountBound,
size: SizeBound,
) -> Result<(), &'static str> {
match self.voters.exhausted(Some(size), Some(count)) {
true => Err("Ensure voters bounds: bounds exceeded."),
false => Ok(()),
}
}
/// Returns an error if the provided `count` and `size` do not fit in the target's election
/// bounds.
pub fn ensure_targets_limits(
self,
count: CountBound,
size: SizeBound,
) -> Result<(), &'static str> {
match self.targets.exhausted(Some(size), Some(count).into()) {
true => Err("Ensure targets bounds: bounds exceeded."),
false => Ok(()),
}
}
}
/// Utility builder for [`ElectionBounds`].
#[derive(Copy, Clone, Default)]
pub struct ElectionBoundsBuilder {
voters: Option<DataProviderBounds>,
targets: Option<DataProviderBounds>,
}
impl From<ElectionBounds> for ElectionBoundsBuilder {
fn from(bounds: ElectionBounds) -> Self {
ElectionBoundsBuilder { voters: Some(bounds.voters), targets: Some(bounds.targets) }
}
}
impl ElectionBoundsBuilder {
/// Sets the voters count bounds.
pub fn voters_count(mut self, count: CountBound) -> Self {
self.voters = self.voters.map_or(
Some(DataProviderBounds { count: Some(count), size: None }),
|mut bounds| {
bounds.count = Some(count);
Some(bounds)
},
);
self
}
/// Sets the voters size bounds.
pub fn voters_size(mut self, size: SizeBound) -> Self {
self.voters = self.voters.map_or(
Some(DataProviderBounds { count: None, size: Some(size) }),
|mut bounds| {
bounds.size = Some(size);
Some(bounds)
},
);
self
}
/// Sets the targets count bounds.
pub fn targets_count(mut self, count: CountBound) -> Self {
self.targets = self.targets.map_or(
Some(DataProviderBounds { count: Some(count), size: None }),
|mut bounds| {
bounds.count = Some(count);
Some(bounds)
},
);
self
}
/// Sets the targets size bounds.
pub fn targets_size(mut self, size: SizeBound) -> Self {
self.targets = self.targets.map_or(
Some(DataProviderBounds { count: None, size: Some(size) }),
|mut bounds| {
bounds.size = Some(size);
Some(bounds)
},
);
self
}
/// Set the voters bounds.
pub fn voters(mut self, bounds: Option<DataProviderBounds>) -> Self {
self.voters = bounds;
self
}
/// Set the targets bounds.
pub fn targets(mut self, bounds: Option<DataProviderBounds>) -> Self {
self.targets = bounds;
self
}
/// Caps the number of the voters bounds in self to `voters` bounds. If `voters` bounds are
/// higher than the self bounds, keeps it. Note that `None` bounds are equivalent to maximum
/// and should be treated as such.
pub fn voters_or_lower(mut self, voters: DataProviderBounds) -> Self {
self.voters = match self.voters {
None => Some(voters),
Some(v) => Some(v.max(voters)),
};
self
}
/// Caps the number of the target bounds in self to `voters` bounds. If `voters` bounds are
/// higher than the self bounds, keeps it. Note that `None` bounds are equivalent to maximum
/// and should be treated as such.
pub fn targets_or_lower(mut self, targets: DataProviderBounds) -> Self {
self.targets = match self.targets {
None => Some(targets),
Some(t) => Some(t.max(targets)),
};
self
}
/// Returns an instance of `ElectionBounds` from the current state.
pub fn build(self) -> ElectionBounds {
ElectionBounds {
voters: self.voters.unwrap_or_default(),
targets: self.targets.unwrap_or_default(),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use frame_support::{assert_err, assert_ok};
#[test]
fn data_provider_bounds_unbounded_works() {
let bounds = DataProviderBounds::default();
assert!(!bounds.exhausted(None, None));
assert!(!bounds.exhausted(SizeBound(u32::MAX).into(), CountBound(u32::MAX).into()));
}
#[test]
fn election_bounds_builder_and_exhausted_bounds_work() {
// voter bounds exhausts if count > 100 or size > 1_000; target bounds exhausts if count >
// 200 or size > 2_000.
let bounds = ElectionBoundsBuilder::default()
.voters_count(100.into())
.voters_size(1_000.into())
.targets_count(200.into())
.targets_size(2_000.into())
.build();
assert!(!bounds.voters.exhausted(None, None));
assert!(!bounds.voters.exhausted(SizeBound(10).into(), CountBound(10).into()));
assert!(!bounds.voters.exhausted(None, CountBound(100).into()));
assert!(!bounds.voters.exhausted(SizeBound(1_000).into(), None));
// exhausts bounds.
assert!(bounds.voters.exhausted(None, CountBound(101).into()));
assert!(bounds.voters.exhausted(SizeBound(1_001).into(), None));
assert!(!bounds.targets.exhausted(None, None));
assert!(!bounds.targets.exhausted(SizeBound(20).into(), CountBound(20).into()));
assert!(!bounds.targets.exhausted(None, CountBound(200).into()));
assert!(!bounds.targets.exhausted(SizeBound(2_000).into(), None));
// exhausts bounds.
assert!(bounds.targets.exhausted(None, CountBound(201).into()));
assert!(bounds.targets.exhausted(SizeBound(2_001).into(), None));
}
#[test]
fn election_bounds_ensure_limits_works() {
let bounds = ElectionBounds {
voters: DataProviderBounds { count: Some(CountBound(10)), size: Some(SizeBound(10)) },
targets: DataProviderBounds { count: Some(CountBound(10)), size: Some(SizeBound(10)) },
};
assert_ok!(bounds.ensure_voters_limits(CountBound(1), SizeBound(1)));
assert_ok!(bounds.ensure_voters_limits(CountBound(1), SizeBound(1)));
assert_ok!(bounds.ensure_voters_limits(CountBound(10), SizeBound(10)));
assert_err!(
bounds.ensure_voters_limits(CountBound(1), SizeBound(11)),
"Ensure voters bounds: bounds exceeded."
);
assert_err!(
bounds.ensure_voters_limits(CountBound(11), SizeBound(10)),
"Ensure voters bounds: bounds exceeded."
);
assert_ok!(bounds.ensure_targets_limits(CountBound(1), SizeBound(1)));
assert_ok!(bounds.ensure_targets_limits(CountBound(1), SizeBound(1)));
assert_ok!(bounds.ensure_targets_limits(CountBound(10), SizeBound(10)));
assert_err!(
bounds.ensure_targets_limits(CountBound(1), SizeBound(11)),
"Ensure targets bounds: bounds exceeded."
);
assert_err!(
bounds.ensure_targets_limits(CountBound(11), SizeBound(10)),
"Ensure targets bounds: bounds exceeded."
);
}
#[test]
fn data_provider_max_unbounded_works() {
let unbounded = DataProviderBounds::default();
// max of some bounds with unbounded data provider bounds will always return the defined
// bounds.
let bounds = DataProviderBounds { count: CountBound(5).into(), size: SizeBound(10).into() };
assert_eq!(unbounded.max(bounds), bounds);
let bounds = DataProviderBounds { count: None, size: SizeBound(10).into() };
assert_eq!(unbounded.max(bounds), bounds);
let bounds = DataProviderBounds { count: CountBound(5).into(), size: None };
assert_eq!(unbounded.max(bounds), bounds);
}
#[test]
fn data_provider_max_bounded_works() {
let bounds_one =
DataProviderBounds { count: CountBound(10).into(), size: SizeBound(100).into() };
let bounds_two =
DataProviderBounds { count: CountBound(100).into(), size: SizeBound(10).into() };
let max_bounds_expected =
DataProviderBounds { count: CountBound(10).into(), size: SizeBound(10).into() };
assert_eq!(bounds_one.max(bounds_two), max_bounds_expected);
assert_eq!(bounds_two.max(bounds_one), max_bounds_expected);
}
#[test]
fn election_bounds_clamp_works() {
let bounds = ElectionBoundsBuilder::default()
.voters_count(10.into())
.voters_size(10.into())
.voters_or_lower(DataProviderBounds {
count: CountBound(5).into(),
size: SizeBound(20).into(),
})
.targets_count(20.into())
.targets_or_lower(DataProviderBounds {
count: CountBound(30).into(),
size: SizeBound(30).into(),
})
.build();
assert_eq!(bounds.voters.count.unwrap(), CountBound(5));
assert_eq!(bounds.voters.size.unwrap(), SizeBound(10));
assert_eq!(bounds.targets.count.unwrap(), CountBound(20));
assert_eq!(bounds.targets.size.unwrap(), SizeBound(30));
// note that unbounded bounds (None) are equivalent to maximum value.
let bounds = ElectionBoundsBuilder::default()
.voters_or_lower(DataProviderBounds {
count: CountBound(5).into(),
size: SizeBound(20).into(),
})
.targets_or_lower(DataProviderBounds {
count: CountBound(10).into(),
size: SizeBound(10).into(),
})
.build();
assert_eq!(bounds.voters.count.unwrap(), CountBound(5));
assert_eq!(bounds.voters.size.unwrap(), SizeBound(20));
assert_eq!(bounds.targets.count.unwrap(), CountBound(10));
assert_eq!(bounds.targets.size.unwrap(), SizeBound(10));
}
}
@@ -109,12 +109,12 @@
//! fn desired_targets() -> data_provider::Result<u32> { //! fn desired_targets() -> data_provider::Result<u32> {
//! Ok(1) //! Ok(1)
//! } //! }
//! fn electing_voters(maybe_max_len: Option<usize>) //! fn electing_voters(bounds: DataProviderBounds)
//! -> data_provider::Result<Vec<VoterOf<Self>>> //! -> data_provider::Result<Vec<VoterOf<Self>>>
//! { //! {
//! Ok(Default::default()) //! Ok(Default::default())
//! } //! }
//! fn electable_targets(maybe_max_len: Option<usize>) -> data_provider::Result<Vec<AccountId>> { //! fn electable_targets(bounds: DataProviderBounds) -> data_provider::Result<Vec<AccountId>> {
//! Ok(vec![10, 20, 30]) //! Ok(vec![10, 20, 30])
//! } //! }
//! fn next_election_prediction(now: BlockNumber) -> BlockNumber { //! fn next_election_prediction(now: BlockNumber) -> BlockNumber {
@@ -145,7 +145,7 @@
//! impl<T: Config> ElectionProvider for GenericElectionProvider<T> { //! impl<T: Config> ElectionProvider for GenericElectionProvider<T> {
//! fn ongoing() -> bool { false } //! fn ongoing() -> bool { false }
//! fn elect() -> Result<BoundedSupportsOf<Self>, Self::Error> { //! fn elect() -> Result<BoundedSupportsOf<Self>, Self::Error> {
//! Self::DataProvider::electable_targets(None) //! Self::DataProvider::electable_targets(DataProviderBounds::default())
//! .map_err(|_| "failed to elect") //! .map_err(|_| "failed to elect")
//! .map(|t| bounded_vec![(t[0], Support::default())]) //! .map(|t| bounded_vec![(t[0], Support::default())])
//! } //! }
@@ -173,11 +173,15 @@
#![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), no_std)]
pub mod bounds;
pub mod onchain; pub mod onchain;
pub mod traits; pub mod traits;
use sp_runtime::traits::{Bounded, Saturating, Zero}; use sp_runtime::traits::{Bounded, Saturating, Zero};
use sp_std::{fmt::Debug, prelude::*}; use sp_std::{fmt::Debug, prelude::*};
pub use bounds::DataProviderBounds;
pub use codec::{Decode, Encode};
/// Re-export the solution generation macro. /// Re-export the solution generation macro.
pub use frame_election_provider_solution_type::generate_solution_type; pub use frame_election_provider_solution_type::generate_solution_type;
pub use frame_support::{traits::Get, weights::Weight, BoundedVec, RuntimeDebug}; pub use frame_support::{traits::Get, weights::Weight, BoundedVec, RuntimeDebug};
@@ -231,7 +235,7 @@ mod tests;
/// making it fast to repeatedly encode into a `SolutionOf<T>`. This property turns out /// making it fast to repeatedly encode into a `SolutionOf<T>`. This property turns out
/// to be important when trimming for solution length. /// to be important when trimming for solution length.
#[derive(RuntimeDebug, Clone, Default)] #[derive(RuntimeDebug, Clone, Default)]
#[cfg_attr(feature = "std", derive(PartialEq, Eq, codec::Encode, codec::Decode))] #[cfg_attr(feature = "std", derive(PartialEq, Eq, Encode, Decode))]
pub struct IndexAssignment<VoterIndex, TargetIndex, P: PerThing> { pub struct IndexAssignment<VoterIndex, TargetIndex, P: PerThing> {
/// Index of the voter among the voters list. /// Index of the voter among the voters list.
pub who: VoterIndex, pub who: VoterIndex,
@@ -275,7 +279,7 @@ pub mod data_provider {
/// Something that can provide the data to an [`ElectionProvider`]. /// Something that can provide the data to an [`ElectionProvider`].
pub trait ElectionDataProvider { pub trait ElectionDataProvider {
/// The account identifier type. /// The account identifier type.
type AccountId; type AccountId: Encode;
/// The block number type. /// The block number type.
type BlockNumber; type BlockNumber;
@@ -286,25 +290,18 @@ pub trait ElectionDataProvider {
/// All possible targets for the election, i.e. the targets that could become elected, thus /// All possible targets for the election, i.e. the targets that could become elected, thus
/// "electable". /// "electable".
/// ///
/// If `maybe_max_len` is `Some(v)` then the resulting vector MUST NOT be longer than `v` items
/// long.
///
/// This should be implemented as a self-weighing function. The implementor should register its /// 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. /// appropriate weight at the end of execution with the system pallet directly.
fn electable_targets( fn electable_targets(bounds: DataProviderBounds)
maybe_max_len: Option<usize>, -> data_provider::Result<Vec<Self::AccountId>>;
) -> data_provider::Result<Vec<Self::AccountId>>;
/// All the voters that participate in the election, thus "electing". /// All the voters that participate in the election, thus "electing".
/// ///
/// Note that if a notion of self-vote exists, it should be represented here. /// 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.
///
/// This should be implemented as a self-weighing function. The implementor should register its /// 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. /// appropriate weight at the end of execution with the system pallet directly.
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>>>;
/// The number of targets to elect. /// The number of targets to elect.
/// ///
@@ -425,8 +422,8 @@ pub trait ElectionProvider: ElectionProviderBase {
/// data provider at runtime via `forced_input_voters_bound` and `forced_input_target_bound`. /// data provider at runtime via `forced_input_voters_bound` and `forced_input_target_bound`.
pub trait InstantElectionProvider: ElectionProviderBase { pub trait InstantElectionProvider: ElectionProviderBase {
fn instant_elect( fn instant_elect(
forced_input_voters_bound: Option<u32>, forced_input_voters_bound: DataProviderBounds,
forced_input_target_bound: Option<u32>, forced_input_target_bound: DataProviderBounds,
) -> Result<BoundedSupportsOf<Self>, Self::Error>; ) -> Result<BoundedSupportsOf<Self>, Self::Error>;
} }
@@ -468,8 +465,8 @@ where
MaxWinners: Get<u32>, MaxWinners: Get<u32>,
{ {
fn instant_elect( fn instant_elect(
_: Option<u32>, _: DataProviderBounds,
_: Option<u32>, _: DataProviderBounds,
) -> Result<BoundedSupportsOf<Self>, Self::Error> { ) -> Result<BoundedSupportsOf<Self>, Self::Error> {
Err("`NoElection` cannot do anything.") Err("`NoElection` cannot do anything.")
} }
@@ -20,6 +20,7 @@
//! careful when using it onchain. //! careful when using it onchain.
use crate::{ use crate::{
bounds::{DataProviderBounds, ElectionBounds, ElectionBoundsBuilder},
BoundedSupportsOf, Debug, ElectionDataProvider, ElectionProvider, ElectionProviderBase, BoundedSupportsOf, Debug, ElectionDataProvider, ElectionProvider, ElectionProviderBase,
InstantElectionProvider, NposSolver, WeightInfo, InstantElectionProvider, NposSolver, WeightInfo,
}; };
@@ -52,8 +53,7 @@ impl From<sp_npos_elections::Error> for Error {
/// This implements both `ElectionProvider` and `InstantElectionProvider`. /// This implements both `ElectionProvider` and `InstantElectionProvider`.
/// ///
/// This type has some utilities to make it safe. Nonetheless, it should be used with utmost care. A /// This type has some utilities to make it safe. Nonetheless, it should be used with utmost care. A
/// thoughtful value must be set as [`Config::VotersBound`] and [`Config::TargetsBound`] to ensure /// thoughtful value must be set as [`Config::Bounds`] to ensure the size of the input is sensible.
/// the size of the input is sensible.
pub struct OnChainExecution<T: Config>(PhantomData<T>); pub struct OnChainExecution<T: Config>(PhantomData<T>);
#[deprecated(note = "use OnChainExecution, which is bounded by default")] #[deprecated(note = "use OnChainExecution, which is bounded by default")]
@@ -85,13 +85,9 @@ pub trait Config {
/// always be more than `DataProvider::desired_target`. /// always be more than `DataProvider::desired_target`.
type MaxWinners: Get<u32>; type MaxWinners: Get<u32>;
/// Bounds the number of voters, when calling into [`Config::DataProvider`]. It might be /// Elections bounds, to use when calling into [`Config::DataProvider`]. It might be overwritten
/// overwritten in the `InstantElectionProvider` impl. /// in the `InstantElectionProvider` impl.
type VotersBound: Get<u32>; type Bounds: Get<ElectionBounds>;
/// Bounds the number of targets, when calling into [`Config::DataProvider`]. It might be
/// overwritten in the `InstantElectionProvider` impl.
type TargetsBound: Get<u32>;
} }
/// Same as `BoundedSupportsOf` but for `onchain::Config`. /// Same as `BoundedSupportsOf` but for `onchain::Config`.
@@ -101,12 +97,12 @@ pub type OnChainBoundedSupportsOf<E> = BoundedSupports<
>; >;
fn elect_with_input_bounds<T: Config>( fn elect_with_input_bounds<T: Config>(
maybe_max_voters: Option<usize>, bounds: ElectionBounds,
maybe_max_targets: Option<usize>,
) -> Result<OnChainBoundedSupportsOf<T>, Error> { ) -> Result<OnChainBoundedSupportsOf<T>, Error> {
let voters = T::DataProvider::electing_voters(maybe_max_voters).map_err(Error::DataProvider)?; let (voters, targets) = T::DataProvider::electing_voters(bounds.voters)
let targets = .and_then(|voters| Ok((voters, T::DataProvider::electable_targets(bounds.targets)?)))
T::DataProvider::electable_targets(maybe_max_targets).map_err(Error::DataProvider)?; .map_err(Error::DataProvider)?;
let desired_targets = T::DataProvider::desired_targets().map_err(Error::DataProvider)?; let desired_targets = T::DataProvider::desired_targets().map_err(Error::DataProvider)?;
if desired_targets > T::MaxWinners::get() { if desired_targets > T::MaxWinners::get() {
@@ -159,13 +155,15 @@ impl<T: Config> ElectionProviderBase for OnChainExecution<T> {
impl<T: Config> InstantElectionProvider for OnChainExecution<T> { impl<T: Config> InstantElectionProvider for OnChainExecution<T> {
fn instant_elect( fn instant_elect(
forced_input_voters_bound: Option<u32>, forced_input_voters_bounds: DataProviderBounds,
forced_input_target_bound: Option<u32>, forced_input_targets_bounds: DataProviderBounds,
) -> Result<BoundedSupportsOf<Self>, Self::Error> { ) -> Result<BoundedSupportsOf<Self>, Self::Error> {
elect_with_input_bounds::<T>( let elections_bounds = ElectionBoundsBuilder::from(T::Bounds::get())
Some(T::VotersBound::get().min(forced_input_voters_bound.unwrap_or(u32::MAX)) as usize), .voters_or_lower(forced_input_voters_bounds)
Some(T::TargetsBound::get().min(forced_input_target_bound.unwrap_or(u32::MAX)) as usize), .targets_or_lower(forced_input_targets_bounds)
) .build();
elect_with_input_bounds::<T>(elections_bounds)
} }
} }
@@ -175,10 +173,8 @@ impl<T: Config> ElectionProvider for OnChainExecution<T> {
} }
fn elect() -> Result<BoundedSupportsOf<Self>, Self::Error> { fn elect() -> Result<BoundedSupportsOf<Self>, Self::Error> {
elect_with_input_bounds::<T>( let election_bounds = ElectionBoundsBuilder::from(T::Bounds::get()).build();
Some(T::VotersBound::get() as usize), elect_with_input_bounds::<T>(election_bounds)
Some(T::TargetsBound::get() as usize),
)
} }
} }
@@ -186,7 +182,7 @@ impl<T: Config> ElectionProvider for OnChainExecution<T> {
mod tests { mod tests {
use super::*; use super::*;
use crate::{ElectionProvider, PhragMMS, SequentialPhragmen}; use crate::{ElectionProvider, PhragMMS, SequentialPhragmen};
use frame_support::{assert_noop, parameter_types, traits::ConstU32}; use frame_support::{assert_noop, parameter_types};
use sp_npos_elections::Support; use sp_npos_elections::Support;
use sp_runtime::Perbill; use sp_runtime::Perbill;
type AccountId = u64; type AccountId = u64;
@@ -236,6 +232,7 @@ mod tests {
parameter_types! { parameter_types! {
pub static MaxWinners: u32 = 10; pub static MaxWinners: u32 = 10;
pub static DesiredTargets: u32 = 2; pub static DesiredTargets: u32 = 2;
pub static Bounds: ElectionBounds = ElectionBoundsBuilder::default().voters_count(600.into()).targets_count(400.into()).build();
} }
impl Config for PhragmenParams { impl Config for PhragmenParams {
@@ -244,8 +241,7 @@ mod tests {
type DataProvider = mock_data_provider::DataProvider; type DataProvider = mock_data_provider::DataProvider;
type WeightInfo = (); type WeightInfo = ();
type MaxWinners = MaxWinners; type MaxWinners = MaxWinners;
type VotersBound = ConstU32<600>; type Bounds = Bounds;
type TargetsBound = ConstU32<400>;
} }
impl Config for PhragMMSParams { impl Config for PhragMMSParams {
@@ -254,8 +250,7 @@ mod tests {
type DataProvider = mock_data_provider::DataProvider; type DataProvider = mock_data_provider::DataProvider;
type WeightInfo = (); type WeightInfo = ();
type MaxWinners = MaxWinners; type MaxWinners = MaxWinners;
type VotersBound = ConstU32<600>; type Bounds = Bounds;
type TargetsBound = ConstU32<400>;
} }
mod mock_data_provider { mod mock_data_provider {
@@ -269,7 +264,7 @@ mod tests {
type AccountId = AccountId; type AccountId = AccountId;
type BlockNumber = BlockNumber; type BlockNumber = BlockNumber;
type MaxVotesPerVoter = ConstU32<2>; type MaxVotesPerVoter = ConstU32<2>;
fn electing_voters(_: Option<usize>) -> data_provider::Result<Vec<VoterOf<Self>>> { fn electing_voters(_: DataProviderBounds) -> data_provider::Result<Vec<VoterOf<Self>>> {
Ok(vec![ Ok(vec![
(1, 10, bounded_vec![10, 20]), (1, 10, bounded_vec![10, 20]),
(2, 20, bounded_vec![30, 20]), (2, 20, bounded_vec![30, 20]),
@@ -277,7 +272,7 @@ mod tests {
]) ])
} }
fn electable_targets(_: Option<usize>) -> data_provider::Result<Vec<AccountId>> { fn electable_targets(_: DataProviderBounds) -> data_provider::Result<Vec<AccountId>> {
Ok(vec![10, 20, 30]) Ok(vec![10, 20, 30])
} }
+1 -1
View File
@@ -135,7 +135,6 @@ impl frame_election_provider_support::ElectionProvider for MockElection {
} }
impl pallet_staking::Config for Runtime { impl pallet_staking::Config for Runtime {
type MaxNominations = ConstU32<16>;
type Currency = Balances; type Currency = Balances;
type CurrencyBalance = Balance; type CurrencyBalance = Balance;
type UnixTime = pallet_timestamp::Pallet<Self>; type UnixTime = pallet_timestamp::Pallet<Self>;
@@ -158,6 +157,7 @@ impl pallet_staking::Config for Runtime {
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type TargetList = pallet_staking::UseValidatorsMap<Self>; type TargetList = pallet_staking::UseValidatorsMap<Self>;
type NominationsQuota = pallet_staking::FixedNominationsQuota<16>;
type MaxUnlockingChunks = ConstU32<32>; type MaxUnlockingChunks = ConstU32<32>;
type EventListeners = (); type EventListeners = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
+7 -4
View File
@@ -22,7 +22,10 @@
use crate::{self as pallet_grandpa, AuthorityId, AuthorityList, Config, ConsensusLog}; use crate::{self as pallet_grandpa, AuthorityId, AuthorityList, Config, ConsensusLog};
use ::grandpa as finality_grandpa; use ::grandpa as finality_grandpa;
use codec::Encode; use codec::Encode;
use frame_election_provider_support::{onchain, SequentialPhragmen}; use frame_election_provider_support::{
bounds::{ElectionBounds, ElectionBoundsBuilder},
onchain, SequentialPhragmen,
};
use frame_support::{ use frame_support::{
parameter_types, parameter_types,
traits::{ConstU128, ConstU32, ConstU64, KeyOwnerProofSystem, OnFinalize, OnInitialize}, traits::{ConstU128, ConstU32, ConstU64, KeyOwnerProofSystem, OnFinalize, OnInitialize},
@@ -164,6 +167,7 @@ parameter_types! {
pub const BondingDuration: EraIndex = 3; pub const BondingDuration: EraIndex = 3;
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17); pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build();
} }
pub struct OnChainSeqPhragmen; pub struct OnChainSeqPhragmen;
@@ -173,12 +177,10 @@ impl onchain::Config for OnChainSeqPhragmen {
type DataProvider = Staking; type DataProvider = Staking;
type WeightInfo = (); type WeightInfo = ();
type MaxWinners = ConstU32<100>; type MaxWinners = ConstU32<100>;
type VotersBound = ConstU32<{ u32::MAX }>; type Bounds = ElectionsBoundsOnChain;
type TargetsBound = ConstU32<{ u32::MAX }>;
} }
impl pallet_staking::Config for Test { impl pallet_staking::Config for Test {
type MaxNominations = ConstU32<16>;
type RewardRemainder = (); type RewardRemainder = ();
type CurrencyToVote = (); type CurrencyToVote = ();
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
@@ -200,6 +202,7 @@ impl pallet_staking::Config for Test {
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type TargetList = pallet_staking::UseValidatorsMap<Self>; type TargetList = pallet_staking::UseValidatorsMap<Self>;
type NominationsQuota = pallet_staking::FixedNominationsQuota<16>;
type MaxUnlockingChunks = ConstU32<32>; type MaxUnlockingChunks = ConstU32<32>;
type HistoryDepth = ConstU32<84>; type HistoryDepth = ConstU32<84>;
type EventListeners = (); type EventListeners = ();
@@ -35,6 +35,7 @@ use pallet_nomination_pools::{
MaxPoolMembersPerPool, MaxPools, Metadata, MinCreateBond, MinJoinBond, Pallet as Pools, MaxPoolMembersPerPool, MaxPools, Metadata, MinCreateBond, MinJoinBond, Pallet as Pools,
PoolMembers, PoolRoles, PoolState, RewardPools, SubPoolsStorage, PoolMembers, PoolRoles, PoolState, RewardPools, SubPoolsStorage,
}; };
use pallet_staking::MaxNominationsOf;
use sp_runtime::{ use sp_runtime::{
traits::{Bounded, StaticLookup, Zero}, traits::{Bounded, StaticLookup, Zero},
Perbill, Perbill,
@@ -564,7 +565,7 @@ frame_benchmarking::benchmarks! {
} }
nominate { nominate {
let n in 1 .. T::MaxNominations::get(); let n in 1 .. MaxNominationsOf::<T>::get();
// Create a pool // Create a pool
let min_create_bond = Pools::<T>::depositor_min_bond() * 2u32.into(); let min_create_bond = Pools::<T>::depositor_min_bond() * 2u32.into();
@@ -679,7 +680,7 @@ frame_benchmarking::benchmarks! {
let (depositor, pool_account) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into(), None); let (depositor, pool_account) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into(), None);
// Nominate with the pool. // Nominate with the pool.
let validators: Vec<_> = (0..T::MaxNominations::get()) let validators: Vec<_> = (0..MaxNominationsOf::<T>::get())
.map(|i| account("stash", USER_SEED, i)) .map(|i| account("stash", USER_SEED, i))
.collect(); .collect();
@@ -94,7 +94,6 @@ parameter_types! {
pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS; pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS;
} }
impl pallet_staking::Config for Runtime { impl pallet_staking::Config for Runtime {
type MaxNominations = ConstU32<16>;
type Currency = Balances; type Currency = Balances;
type CurrencyBalance = Balance; type CurrencyBalance = Balance;
type UnixTime = pallet_timestamp::Pallet<Self>; type UnixTime = pallet_timestamp::Pallet<Self>;
@@ -117,6 +116,7 @@ impl pallet_staking::Config for Runtime {
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = VoterList; type VoterList = VoterList;
type TargetList = pallet_staking::UseValidatorsMap<Self>; type TargetList = pallet_staking::UseValidatorsMap<Self>;
type NominationsQuota = pallet_staking::FixedNominationsQuota<16>;
type MaxUnlockingChunks = ConstU32<32>; type MaxUnlockingChunks = ConstU32<32>;
type HistoryDepth = ConstU32<84>; type HistoryDepth = ConstU32<84>;
type EventListeners = Pools; type EventListeners = Pools;
@@ -108,7 +108,6 @@ parameter_types! {
} }
impl pallet_staking::Config for Runtime { impl pallet_staking::Config for Runtime {
type MaxNominations = ConstU32<16>;
type Currency = Balances; type Currency = Balances;
type CurrencyBalance = Balance; type CurrencyBalance = Balance;
type UnixTime = pallet_timestamp::Pallet<Self>; type UnixTime = pallet_timestamp::Pallet<Self>;
@@ -131,6 +130,7 @@ impl pallet_staking::Config for Runtime {
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = VoterList; type VoterList = VoterList;
type TargetList = pallet_staking::UseValidatorsMap<Self>; type TargetList = pallet_staking::UseValidatorsMap<Self>;
type NominationsQuota = pallet_staking::FixedNominationsQuota<16>;
type MaxUnlockingChunks = ConstU32<32>; type MaxUnlockingChunks = ConstU32<32>;
type HistoryDepth = ConstU32<84>; type HistoryDepth = ConstU32<84>;
type EventListeners = Pools; type EventListeners = Pools;
@@ -50,8 +50,8 @@ use pallet_session::{
#[cfg(test)] #[cfg(test)]
use pallet_staking::Event as StakingEvent; use pallet_staking::Event as StakingEvent;
use pallet_staking::{ use pallet_staking::{
Config as StakingConfig, Exposure, IndividualExposure, Pallet as Staking, RewardDestination, Config as StakingConfig, Exposure, IndividualExposure, MaxNominationsOf, Pallet as Staking,
ValidatorPrefs, RewardDestination, ValidatorPrefs,
}; };
const SEED: u32 = 0; const SEED: u32 = 0;
@@ -283,7 +283,7 @@ benchmarks! {
let r in 1 .. MAX_REPORTERS; let r in 1 .. MAX_REPORTERS;
// we skip 1 offender, because in such case there is no slashing // we skip 1 offender, because in such case there is no slashing
let o in 2 .. MAX_OFFENDERS; let o in 2 .. MAX_OFFENDERS;
let n in 0 .. MAX_NOMINATORS.min(<T as pallet_staking::Config>::MaxNominations::get()); let n in 0 .. MAX_NOMINATORS.min(MaxNominationsOf::<T>::get());
// Make r reporters // Make r reporters
let mut reporters = vec![]; let mut reporters = vec![];
@@ -399,7 +399,7 @@ benchmarks! {
} }
report_offence_grandpa { report_offence_grandpa {
let n in 0 .. MAX_NOMINATORS.min(<T as pallet_staking::Config>::MaxNominations::get()); let n in 0 .. MAX_NOMINATORS.min(MaxNominationsOf::<T>::get());
// for grandpa equivocation reports the number of reporters // for grandpa equivocation reports the number of reporters
// and offenders is always 1 // and offenders is always 1
@@ -436,7 +436,7 @@ benchmarks! {
} }
report_offence_babe { report_offence_babe {
let n in 0 .. MAX_NOMINATORS.min(<T as pallet_staking::Config>::MaxNominations::get()); let n in 0 .. MAX_NOMINATORS.min(MaxNominationsOf::<T>::get());
// for babe equivocation reports the number of reporters // for babe equivocation reports the number of reporters
// and offenders is always 1 // and offenders is always 1
@@ -20,7 +20,10 @@
#![cfg(test)] #![cfg(test)]
use super::*; use super::*;
use frame_election_provider_support::{onchain, SequentialPhragmen}; use frame_election_provider_support::{
bounds::{ElectionBounds, ElectionBoundsBuilder},
onchain, SequentialPhragmen,
};
use frame_support::{ use frame_support::{
parameter_types, parameter_types,
traits::{ConstU32, ConstU64}, traits::{ConstU32, ConstU64},
@@ -141,6 +144,7 @@ pallet_staking_reward_curve::build! {
} }
parameter_types! { parameter_types! {
pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS; pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS;
pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build();
} }
pub type Extrinsic = sp_runtime::testing::TestXt<RuntimeCall, ()>; pub type Extrinsic = sp_runtime::testing::TestXt<RuntimeCall, ()>;
@@ -152,12 +156,10 @@ impl onchain::Config for OnChainSeqPhragmen {
type DataProvider = Staking; type DataProvider = Staking;
type WeightInfo = (); type WeightInfo = ();
type MaxWinners = ConstU32<100>; type MaxWinners = ConstU32<100>;
type VotersBound = ConstU32<{ u32::MAX }>; type Bounds = ElectionsBounds;
type TargetsBound = ConstU32<{ u32::MAX }>;
} }
impl pallet_staking::Config for Test { impl pallet_staking::Config for Test {
type MaxNominations = ConstU32<16>;
type Currency = Balances; type Currency = Balances;
type CurrencyBalance = <Self as pallet_balances::Config>::Balance; type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
type UnixTime = pallet_timestamp::Pallet<Self>; type UnixTime = pallet_timestamp::Pallet<Self>;
@@ -179,6 +181,7 @@ impl pallet_staking::Config for Test {
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type TargetList = pallet_staking::UseValidatorsMap<Self>; type TargetList = pallet_staking::UseValidatorsMap<Self>;
type NominationsQuota = pallet_staking::FixedNominationsQuota<16>;
type MaxUnlockingChunks = ConstU32<32>; type MaxUnlockingChunks = ConstU32<32>;
type HistoryDepth = ConstU32<84>; type HistoryDepth = ConstU32<84>;
type EventListeners = (); type EventListeners = ();
+10 -4
View File
@@ -18,7 +18,10 @@
use super::*; use super::*;
use crate as root_offences; use crate as root_offences;
use frame_election_provider_support::{onchain, SequentialPhragmen}; use frame_election_provider_support::{
bounds::{ElectionBounds, ElectionBoundsBuilder},
onchain, SequentialPhragmen,
};
use frame_support::{ use frame_support::{
parameter_types, parameter_types,
traits::{ConstU32, ConstU64, Hooks, OneSessionHandler}, traits::{ConstU32, ConstU64, Hooks, OneSessionHandler},
@@ -134,6 +137,10 @@ pallet_staking_reward_curve::build! {
); );
} }
parameter_types! {
pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build();
}
pub struct OnChainSeqPhragmen; pub struct OnChainSeqPhragmen;
impl onchain::Config for OnChainSeqPhragmen { impl onchain::Config for OnChainSeqPhragmen {
type System = Test; type System = Test;
@@ -141,8 +148,7 @@ impl onchain::Config for OnChainSeqPhragmen {
type DataProvider = Staking; type DataProvider = Staking;
type WeightInfo = (); type WeightInfo = ();
type MaxWinners = ConstU32<100>; type MaxWinners = ConstU32<100>;
type VotersBound = ConstU32<{ u32::MAX }>; type Bounds = ElectionsBounds;
type TargetsBound = ConstU32<{ u32::MAX }>;
} }
parameter_types! { parameter_types! {
@@ -157,7 +163,6 @@ parameter_types! {
} }
impl pallet_staking::Config for Test { impl pallet_staking::Config for Test {
type MaxNominations = ConstU32<16>;
type Currency = Balances; type Currency = Balances;
type CurrencyBalance = <Self as pallet_balances::Config>::Balance; type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
type UnixTime = Timestamp; type UnixTime = Timestamp;
@@ -178,6 +183,7 @@ impl pallet_staking::Config for Test {
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>; type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
type TargetList = pallet_staking::UseValidatorsMap<Self>; type TargetList = pallet_staking::UseValidatorsMap<Self>;
type NominationsQuota = pallet_staking::FixedNominationsQuota<16>;
type MaxUnlockingChunks = ConstU32<32>; type MaxUnlockingChunks = ConstU32<32>;
type HistoryDepth = ConstU32<84>; type HistoryDepth = ConstU32<84>;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
@@ -35,7 +35,7 @@ use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin};
use pallet_session::{historical::Pallet as Historical, Pallet as Session, *}; use pallet_session::{historical::Pallet as Historical, Pallet as Session, *};
use pallet_staking::{ use pallet_staking::{
benchmarking::create_validator_with_nominators, testing_utils::create_validators, benchmarking::create_validator_with_nominators, testing_utils::create_validators,
RewardDestination, MaxNominationsOf, RewardDestination,
}; };
const MAX_VALIDATORS: u32 = 1000; const MAX_VALIDATORS: u32 = 1000;
@@ -54,10 +54,10 @@ impl<T: Config> OnInitialize<BlockNumberFor<T>> for Pallet<T> {
benchmarks! { benchmarks! {
set_keys { set_keys {
let n = <T as pallet_staking::Config>::MaxNominations::get(); let n = MaxNominationsOf::<T>::get();
let (v_stash, _) = create_validator_with_nominators::<T>( let (v_stash, _) = create_validator_with_nominators::<T>(
n, n,
<T as pallet_staking::Config>::MaxNominations::get(), MaxNominationsOf::<T>::get(),
false, false,
true, true,
RewardDestination::Staked, RewardDestination::Staked,
@@ -72,10 +72,10 @@ benchmarks! {
}: _(RawOrigin::Signed(v_controller), keys, proof) }: _(RawOrigin::Signed(v_controller), keys, proof)
purge_keys { purge_keys {
let n = <T as pallet_staking::Config>::MaxNominations::get(); let n = MaxNominationsOf::<T>::get();
let (v_stash, _) = create_validator_with_nominators::<T>( let (v_stash, _) = create_validator_with_nominators::<T>(
n, n,
<T as pallet_staking::Config>::MaxNominations::get(), MaxNominationsOf::<T>::get(),
false, false,
true, true,
RewardDestination::Staked, RewardDestination::Staked,
@@ -19,7 +19,10 @@
#![cfg(test)] #![cfg(test)]
use frame_election_provider_support::{onchain, SequentialPhragmen}; use frame_election_provider_support::{
bounds::{ElectionBounds, ElectionBoundsBuilder},
onchain, SequentialPhragmen,
};
use frame_support::{ use frame_support::{
parameter_types, parameter_types,
traits::{ConstU32, ConstU64}, traits::{ConstU32, ConstU64},
@@ -140,6 +143,7 @@ pallet_staking_reward_curve::build! {
} }
parameter_types! { parameter_types! {
pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS; pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS;
pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build();
} }
pub struct OnChainSeqPhragmen; pub struct OnChainSeqPhragmen;
@@ -149,12 +153,10 @@ impl onchain::Config for OnChainSeqPhragmen {
type DataProvider = Staking; type DataProvider = Staking;
type WeightInfo = (); type WeightInfo = ();
type MaxWinners = ConstU32<100>; type MaxWinners = ConstU32<100>;
type VotersBound = ConstU32<{ u32::MAX }>; type Bounds = ElectionsBounds;
type TargetsBound = ConstU32<{ u32::MAX }>;
} }
impl pallet_staking::Config for Test { impl pallet_staking::Config for Test {
type MaxNominations = ConstU32<16>;
type Currency = Balances; type Currency = Balances;
type CurrencyBalance = <Self as pallet_balances::Config>::Balance; type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
type UnixTime = pallet_timestamp::Pallet<Self>; type UnixTime = pallet_timestamp::Pallet<Self>;
@@ -178,6 +180,7 @@ impl pallet_staking::Config for Test {
type HistoryDepth = ConstU32<84>; type HistoryDepth = ConstU32<84>;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type TargetList = pallet_staking::UseValidatorsMap<Self>; type TargetList = pallet_staking::UseValidatorsMap<Self>;
type NominationsQuota = pallet_staking::FixedNominationsQuota<16>;
type EventListeners = (); type EventListeners = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = (); type WeightInfo = ();
+16 -14
View File
@@ -22,7 +22,7 @@ use crate::{ConfigOp, Pallet as Staking};
use testing_utils::*; use testing_utils::*;
use codec::Decode; use codec::Decode;
use frame_election_provider_support::SortedListProvider; use frame_election_provider_support::{bounds::DataProviderBounds, SortedListProvider};
use frame_support::{ use frame_support::{
dispatch::UnfilteredDispatchable, dispatch::UnfilteredDispatchable,
pallet_prelude::*, pallet_prelude::*,
@@ -338,7 +338,7 @@ benchmarks! {
validate { validate {
let (stash, controller) = create_stash_controller::<T>( let (stash, controller) = create_stash_controller::<T>(
T::MaxNominations::get() - 1, MaxNominationsOf::<T>::get() - 1,
100, 100,
Default::default(), Default::default(),
)?; )?;
@@ -362,11 +362,11 @@ benchmarks! {
// these are the other validators; there are `T::MaxNominations::get() - 1` of them, so // 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. // 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. // this is the validator that will be kicking.
let (stash, controller) = create_stash_controller::<T>( let (stash, controller) = create_stash_controller::<T>(
T::MaxNominations::get() - 1, MaxNominationsOf::<T>::get() - 1,
100, 100,
Default::default(), Default::default(),
)?; )?;
@@ -381,7 +381,7 @@ benchmarks! {
for i in 0 .. k { for i in 0 .. k {
// create a nominator stash. // create a nominator stash.
let (n_stash, n_controller) = create_stash_controller::<T>( let (n_stash, n_controller) = create_stash_controller::<T>(
T::MaxNominations::get() + i, MaxNominationsOf::<T>::get() + i,
100, 100,
Default::default(), Default::default(),
)?; )?;
@@ -418,7 +418,7 @@ benchmarks! {
// Worst case scenario, T::MaxNominations::get() // Worst case scenario, T::MaxNominations::get()
nominate { nominate {
let n in 1 .. T::MaxNominations::get(); let n in 1 .. MaxNominationsOf::<T>::get();
// clean up any existing state. // clean up any existing state.
clear_validators_and_nominators::<T>(); clear_validators_and_nominators::<T>();
@@ -429,7 +429,7 @@ benchmarks! {
// we are just doing an insert into the origin position. // we are just doing an insert into the origin position.
let scenario = ListScenario::<T>::new(origin_weight, true)?; let scenario = ListScenario::<T>::new(origin_weight, true)?;
let (stash, controller) = create_stash_controller_with_balance::<T>( 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, origin_weight,
Default::default(), Default::default(),
).unwrap(); ).unwrap();
@@ -711,7 +711,7 @@ benchmarks! {
create_validators_with_nominators_for_era::<T>( create_validators_with_nominators_for_era::<T>(
v, v,
n, n,
<T as Config>::MaxNominations::get() as usize, MaxNominationsOf::<T>::get() as usize,
false, false,
None, None,
)?; )?;
@@ -729,7 +729,7 @@ benchmarks! {
create_validators_with_nominators_for_era::<T>( create_validators_with_nominators_for_era::<T>(
v, v,
n, n,
<T as Config>::MaxNominations::get() as usize, MaxNominationsOf::<T>::get() as usize,
false, false,
None, None,
)?; )?;
@@ -808,7 +808,7 @@ benchmarks! {
let n in (MaxNominators::<T>::get() / 2) .. MaxNominators::<T>::get(); let n in (MaxNominators::<T>::get() / 2) .. MaxNominators::<T>::get();
let validators = create_validators_with_nominators_for_era::<T>( 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() .into_iter()
.map(|v| T::Lookup::lookup(v).unwrap()) .map(|v| T::Lookup::lookup(v).unwrap())
@@ -819,7 +819,8 @@ benchmarks! {
let num_voters = (v + n) as usize; 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); assert_eq!(voters.len(), num_voters);
} }
@@ -830,10 +831,11 @@ benchmarks! {
let n = MaxNominators::<T>::get(); let n = MaxNominators::<T>::get();
let _ = create_validators_with_nominators_for_era::<T>( 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); assert_eq!(targets.len() as u32, v);
} }
@@ -961,7 +963,7 @@ mod tests {
create_validators_with_nominators_for_era::<Test>( create_validators_with_nominators_for_era::<Test>(
v, v,
n, n,
<Test as Config>::MaxNominations::get() as usize, MaxNominationsOf::<Test>::get() as usize,
false, false,
None, 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)] #[cfg(test)]
mod tests; mod tests;
pub mod election_size_tracker;
pub mod inflation; pub mod inflation;
pub mod migrations; pub mod migrations;
pub mod slashing; pub mod slashing;
@@ -301,7 +302,7 @@ mod pallet;
use codec::{Decode, Encode, HasCompact, MaxEncodedLen}; use codec::{Decode, Encode, HasCompact, MaxEncodedLen};
use frame_support::{ use frame_support::{
traits::{Currency, Defensive, Get}, traits::{ConstU32, Currency, Defensive, Get},
weights::Weight, weights::Weight,
BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
}; };
@@ -338,6 +339,10 @@ macro_rules! log {
/// pallet. /// pallet.
pub type MaxWinnersOf<T> = <<T as Config>::ElectionProvider as frame_election_provider_support::ElectionProviderBase>::MaxWinners; 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. /// Counter for the number of "reward" points earned by a given validator.
pub type RewardPoint = u32; pub type RewardPoint = u32;
@@ -679,7 +684,7 @@ impl<T: Config> StakingLedger<T> {
#[scale_info(skip_type_params(T))] #[scale_info(skip_type_params(T))]
pub struct Nominations<T: Config> { pub struct Nominations<T: Config> {
/// The targets of nomination. /// The targets of nomination.
pub targets: BoundedVec<T::AccountId, T::MaxNominations>, pub targets: BoundedVec<T::AccountId, MaxNominationsOf<T>>,
/// The era the nominations were submitted. /// The era the nominations were submitted.
/// ///
/// Except for initial nominations which are considered submitted at era 0. /// 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. /// Means for interacting with a specialized version of the `session` trait.
/// ///
/// This is needed because `Staking` sets the `ValidatorIdOf` of the `pallet_session::Config` /// This is needed because `Staking` sets the `ValidatorIdOf` of the `pallet_session::Config`
+27 -5
View File
@@ -18,7 +18,10 @@
//! Test utilities //! Test utilities
use crate::{self as pallet_staking, *}; 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::{ use frame_support::{
assert_ok, ord_parameter_types, parameter_types, assert_ok, ord_parameter_types, parameter_types,
traits::{ traits::{
@@ -228,11 +231,12 @@ const THRESHOLDS: [sp_npos_elections::VoteWeight; 9] =
parameter_types! { parameter_types! {
pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS; pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS;
pub static MaxNominations: u32 = 16;
pub static HistoryDepth: u32 = 80; pub static HistoryDepth: u32 = 80;
pub static MaxUnlockingChunks: u32 = 32; pub static MaxUnlockingChunks: u32 = 32;
pub static RewardOnUnbalanceWasCalled: bool = false; pub static RewardOnUnbalanceWasCalled: bool = false;
pub static MaxWinners: u32 = 100; pub static MaxWinners: u32 = 100;
pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build();
pub static AbsoluteMaxNominations: u32 = 16;
} }
type VoterBagsListInstance = pallet_bags_list::Instance1; type VoterBagsListInstance = pallet_bags_list::Instance1;
@@ -252,8 +256,7 @@ impl onchain::Config for OnChainSeqPhragmen {
type DataProvider = Staking; type DataProvider = Staking;
type WeightInfo = (); type WeightInfo = ();
type MaxWinners = MaxWinners; type MaxWinners = MaxWinners;
type VotersBound = ConstU32<{ u32::MAX }>; type Bounds = ElectionsBounds;
type TargetsBound = ConstU32<{ u32::MAX }>;
} }
pub struct MockReward {} pub struct MockReward {}
@@ -281,7 +284,6 @@ impl OnStakingUpdate<AccountId, Balance> for EventListenerMock {
} }
impl crate::pallet::pallet::Config for Test { impl crate::pallet::pallet::Config for Test {
type MaxNominations = MaxNominations;
type Currency = Balances; type Currency = Balances;
type CurrencyBalance = <Self as pallet_balances::Config>::Balance; type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
type UnixTime = Timestamp; type UnixTime = Timestamp;
@@ -304,6 +306,7 @@ impl crate::pallet::pallet::Config for Test {
// NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well. // NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well.
type VoterList = VoterBagsList; type VoterList = VoterBagsList;
type TargetList = UseValidatorsMap<Self>; type TargetList = UseValidatorsMap<Self>;
type NominationsQuota = WeightedNominationsQuota<16>;
type MaxUnlockingChunks = MaxUnlockingChunks; type MaxUnlockingChunks = MaxUnlockingChunks;
type HistoryDepth = HistoryDepth; type HistoryDepth = HistoryDepth;
type EventListeners = EventListenerMock; type EventListeners = EventListenerMock;
@@ -311,6 +314,25 @@ impl crate::pallet::pallet::Config for Test {
type WeightInfo = (); 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 StakingCall = crate::Call<Test>;
pub(crate) type TestCall = <Test as frame_system::Config>::RuntimeCall; pub(crate) type TestCall = <Test as frame_system::Config>::RuntimeCall;
+83 -40
View File
@@ -18,8 +18,9 @@
//! Implementations for the Staking FRAME Pallet. //! Implementations for the Staking FRAME Pallet.
use frame_election_provider_support::{ use frame_election_provider_support::{
data_provider, BoundedSupportsOf, ElectionDataProvider, ElectionProvider, ScoreProvider, bounds::{CountBound, SizeBound},
SortedListProvider, VoteWeight, VoterOf, data_provider, BoundedSupportsOf, DataProviderBounds, ElectionDataProvider, ElectionProvider,
ScoreProvider, SortedListProvider, VoteWeight, VoterOf,
}; };
use frame_support::{ use frame_support::{
defensive, defensive,
@@ -45,8 +46,9 @@ use sp_staking::{
use sp_std::prelude::*; use sp_std::prelude::*;
use crate::{ use crate::{
log, slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, EraPayout, Exposure, ExposureOf, election_size_tracker::StaticTracker, log, slashing, weights::WeightInfo, ActiveEraInfo,
Forcing, IndividualExposure, MaxWinnersOf, Nominations, PositiveImbalanceOf, RewardDestination, BalanceOf, EraPayout, Exposure, ExposureOf, Forcing, IndividualExposure, MaxNominationsOf,
MaxWinnersOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination,
SessionInterface, StakingLedger, ValidatorPrefs, SessionInterface, StakingLedger, ValidatorPrefs,
}; };
@@ -761,13 +763,15 @@ impl<T: Config> Pallet<T> {
/// nominators. /// nominators.
/// ///
/// This function is self-weighing as [`DispatchClass::Mandatory`]. /// This function is self-weighing as [`DispatchClass::Mandatory`].
pub fn get_npos_voters(maybe_max_len: Option<usize>) -> Vec<VoterOf<Self>> { pub fn get_npos_voters(bounds: DataProviderBounds) -> Vec<VoterOf<Self>> {
let max_allowed_len = { let mut voters_size_tracker: StaticTracker<Self> = StaticTracker::default();
let all_voter_count = T::VoterList::count() as usize;
maybe_max_len.unwrap_or(all_voter_count).min(all_voter_count) 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. // cache a few things.
let weight_of = Self::weight_of_fn(); 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 min_active_stake = u64::MAX;
let mut sorted_voters = T::VoterList::iter(); let mut sorted_voters = T::VoterList::iter();
while all_voters.len() < max_allowed_len && while all_voters.len() < final_predicted_len as usize &&
voters_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * max_allowed_len as u32) voters_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32)
{ {
let voter = match sorted_voters.next() { let voter = match sorted_voters.next() {
Some(voter) => { Some(voter) => {
@@ -798,10 +802,23 @@ impl<T: Config> Pallet<T> {
if let Some(Nominations { targets, .. }) = <Nominators<T>>::get(&voter) { if let Some(Nominations { targets, .. }) = <Nominators<T>>::get(&voter) {
if !targets.is_empty() { 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(); nominators_taken.saturating_inc();
} else { } 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 = min_active_stake =
if voter_weight < min_active_stake { voter_weight } else { 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() .try_into()
.expect("`MaxVotesPerVoter` must be greater than or equal to 1"), .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); all_voters.push(self_vote);
validators_taken.saturating_inc(); validators_taken.saturating_inc();
} else { } else {
// this can only happen if: 1. there a bug in the bags-list (or whatever is the // 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 // 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 // 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 // `T::NominationsQuota::get_quota`. The latter can rarely happen, and is not
// or bug if it does. // really an emergency or bug if it does.
log!( defensive!(
warn,
"DEFENSIVE: invalid item in `VoterList`: {:?}, this nominator probably has too many nominations now", "DEFENSIVE: invalid item in `VoterList`: {:?}, this nominator probably has too many nominations now",
voter voter,
); );
} }
} }
// all_voters should have not re-allocated. // 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)); 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. /// Get the targets for an upcoming npos election.
/// ///
/// This function is self-weighing as [`DispatchClass::Mandatory`]. /// This function is self-weighing as [`DispatchClass::Mandatory`].
pub fn get_npos_targets(maybe_max_len: Option<usize>) -> Vec<T::AccountId> { pub fn get_npos_targets(bounds: DataProviderBounds) -> Vec<T::AccountId> {
let max_allowed_len = maybe_max_len.unwrap_or_else(|| T::TargetList::count() as usize); let mut targets_size_tracker: StaticTracker<Self> = StaticTracker::default();
let mut all_targets = Vec::<T::AccountId>::with_capacity(max_allowed_len);
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_seen = 0;
let mut targets_iter = T::TargetList::iter(); let mut targets_iter = T::TargetList::iter();
while all_targets.len() < max_allowed_len && while all_targets.len() < final_predicted_len as usize &&
targets_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * max_allowed_len as u32) targets_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32)
{ {
let target = match targets_iter.next() { let target = match targets_iter.next() {
Some(target) => { Some(target) => {
@@ -871,6 +901,14 @@ impl<T: Config> Pallet<T> {
None => break, 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) { if Validators::<T>::contains_key(&target) {
all_targets.push(target); all_targets.push(target);
} }
@@ -989,43 +1027,48 @@ impl<T: Config> Pallet<T> {
/// Returns the current nominations quota for nominators. /// Returns the current nominations quota for nominators.
/// ///
/// Used by the runtime API. /// Used by the runtime API.
/// Note: for now, this api runtime will always return value of `T::MaxNominations` and thus it pub fn api_nominations_quota(balance: BalanceOf<T>) -> u32 {
/// is redundant. However, with the upcoming changes in T::NominationsQuota::get_quota(balance)
/// <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()
} }
} }
impl<T: Config> ElectionDataProvider for Pallet<T> { impl<T: Config> ElectionDataProvider for Pallet<T> {
type AccountId = T::AccountId; type AccountId = T::AccountId;
type BlockNumber = BlockNumberFor<T>; type BlockNumber = BlockNumberFor<T>;
type MaxVotesPerVoter = T::MaxNominations; type MaxVotesPerVoter = MaxNominationsOf<T>;
fn desired_targets() -> data_provider::Result<u32> { fn desired_targets() -> data_provider::Result<u32> {
Self::register_weight(T::DbWeight::get().reads(1)); Self::register_weight(T::DbWeight::get().reads(1));
Ok(Self::validator_count()) 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. // This can never fail -- if `maybe_max_len` is `Some(_)` we handle it.
let voters = Self::get_npos_voters(maybe_max_len); let voters = Self::get_npos_voters(bounds);
debug_assert!(maybe_max_len.map_or(true, |max| voters.len() <= max));
debug_assert!(!bounds.exhausted(
SizeBound(voters.encoded_size() as u32).into(),
CountBound(voters.len() as u32).into()
));
Ok(voters) Ok(voters)
} }
fn electable_targets(maybe_max_len: Option<usize>) -> data_provider::Result<Vec<T::AccountId>> { fn electable_targets(bounds: DataProviderBounds) -> data_provider::Result<Vec<T::AccountId>> {
let target_count = T::TargetList::count(); let targets = Self::get_npos_targets(bounds);
// We can't handle this case yet -- return an error. // We can't handle this case yet -- return an error. WIP to improve handling this case in
if maybe_max_len.map_or(false, |max_len| target_count > max_len as u32) { // <https://github.com/paritytech/substrate/pull/13195>.
if bounds.exhausted(None, CountBound(T::TargetList::count() as u32).into()) {
return Err("Target snapshot too big") 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> { fn next_election_prediction(now: BlockNumberFor<T>) -> BlockNumberFor<T> {
+17 -10
View File
@@ -45,9 +45,9 @@ pub use impls::*;
use crate::{ use crate::{
slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout, slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout,
EraRewardPoints, Exposure, Forcing, NegativeImbalanceOf, Nominations, PositiveImbalanceOf, EraRewardPoints, Exposure, Forcing, MaxNominationsOf, NegativeImbalanceOf, Nominations,
RewardDestination, SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk, NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, StakingLedger,
ValidatorPrefs, UnappliedSlash, UnlockChunk, ValidatorPrefs,
}; };
const STAKING_ID: LockIdentifier = *b"staking "; const STAKING_ID: LockIdentifier = *b"staking ";
@@ -129,9 +129,8 @@ pub mod pallet {
DataProvider = Pallet<Self>, DataProvider = Pallet<Self>,
>; >;
/// Maximum number of nominations per nominator. /// Something that defines the maximum number of nominations per nominator.
#[pallet::constant] type NominationsQuota: NominationsQuota<BalanceOf<Self>>;
type MaxNominations: Get<u32>;
/// Number of eras to keep in history. /// Number of eras to keep in history.
/// ///
@@ -348,7 +347,8 @@ pub mod pallet {
/// they wish to support. /// they wish to support.
/// ///
/// Note that the keys of this storage map might become non-decodable in case the /// 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` /// 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 /// 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 /// 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 }, PayoutStarted { era_index: EraIndex, validator_stash: T::AccountId },
/// A validator has set their preferences. /// A validator has set their preferences.
ValidatorPrefsSet { stash: T::AccountId, prefs: ValidatorPrefs }, 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. /// A new force era mode was set.
ForceEra { mode: Forcing }, ForceEra { mode: Forcing },
} }
@@ -782,11 +786,11 @@ pub mod pallet {
fn integrity_test() { fn integrity_test() {
// ensure that we funnel the correct value to the `DataProvider::MaxVotesPerVoter`; // ensure that we funnel the correct value to the `DataProvider::MaxVotesPerVoter`;
assert_eq!( assert_eq!(
T::MaxNominations::get(), MaxNominationsOf::<T>::get(),
<Self as ElectionDataProvider>::MaxVotesPerVoter::get() <Self as ElectionDataProvider>::MaxVotesPerVoter::get()
); );
// and that MaxNominations is always greater than 1, since we count on this. // 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 // ensure election results are always bounded with the same value
assert!( assert!(
@@ -1145,7 +1149,10 @@ pub mod pallet {
} }
ensure!(!targets.is_empty(), Error::<T>::EmptyTargets); 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()); 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. //! Tests for the module.
use super::{ConfigOp, Event, *}; 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::{ use frame_support::{
assert_noop, assert_ok, assert_storage_noop, bounded_vec, assert_noop, assert_ok, assert_storage_noop, bounded_vec,
dispatch::{extract_actual_weight, GetDispatchInfo, WithPostDispatchInfo}, 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(71, 71, 10, StakerStatus::<AccountId>::Nominator(vec![21]))
.add_staker(81, 81, 50, StakerStatus::<AccountId>::Nominator(vec![21])) .add_staker(81, 81, 50, StakerStatus::<AccountId>::Nominator(vec![21]))
.build_and_execute(|| { .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); assert_eq!(MinimumActiveStake::<Test>::get(), 10);
// remove staker with lower bond by limiting the number of voters and check // remove staker with lower bond by limiting the number of voters and check
// `MinimumActiveStake` again after electing voters. // `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); assert_eq!(MinimumActiveStake::<Test>::get(), 50);
}); });
} }
@@ -4522,8 +4529,11 @@ mod election_data_provider {
fn set_minimum_active_stake_lower_bond_works() { fn set_minimum_active_stake_lower_bond_works() {
// if there are no voters, minimum active stake is zero (should not happen). // if there are no voters, minimum active stake is zero (should not happen).
ExtBuilder::default().has_stakers(false).build_and_execute(|| { 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_eq!(<Test as Config>::VoterList::count(), 0);
assert_ok!(<Staking as ElectionDataProvider>::electing_voters(None));
assert_eq!(MinimumActiveStake::<Test>::get(), 0); assert_eq!(MinimumActiveStake::<Test>::get(), 0);
}); });
@@ -4537,7 +4547,9 @@ mod election_data_provider {
assert_ok!(Staking::nominate(RuntimeOrigin::signed(4), vec![1])); assert_ok!(Staking::nominate(RuntimeOrigin::signed(4), vec![1]));
assert_eq!(<Test as Config>::VoterList::count(), 5); 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); assert_eq!(MinimumActiveStake::<Test>::get(), 5);
// update minimum nominator bond. // update minimum nominator bond.
@@ -4547,7 +4559,9 @@ mod election_data_provider {
// lower than `MinNominatorBond`. // lower than `MinNominatorBond`.
assert_eq!(<Test as Config>::VoterList::count(), 5); 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); assert_eq!(voters_before, voters);
// minimum active stake is lower than `MinNominatorBond`. // 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])) .add_staker(61, 61, 2_000, StakerStatus::<AccountId>::Nominator(vec![21]))
.build_and_execute(|| { .build_and_execute(|| {
assert_eq!(Staking::weight_of(&101), 500); 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!(voters.len(), 5);
assert_eq!(MinimumActiveStake::<Test>::get(), 500); assert_eq!(MinimumActiveStake::<Test>::get(), 500);
@@ -4575,7 +4592,10 @@ mod election_data_provider {
// corrupt ledger state by lowering max unlocking chunks bounds. // corrupt ledger state by lowering max unlocking chunks bounds.
MaxUnlockingChunks::set(1); 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 // number of returned voters decreases since ledger entry of stash 101 is now
// corrupt. // corrupt.
assert_eq!(voters.len(), 4); assert_eq!(voters.len(), 4);
@@ -4593,8 +4613,9 @@ mod election_data_provider {
#[test] #[test]
fn voters_include_self_vote() { fn voters_include_self_vote() {
ExtBuilder::default().nominate(false).build_and_execute(|| { ExtBuilder::default().nominate(false).build_and_execute(|| {
// default bounds are unbounded.
assert!(<Validators<Test>>::iter().map(|(x, _)| x).all(|v| Staking::electing_voters( assert!(<Validators<Test>>::iter().map(|(x, _)| x).all(|v| Staking::electing_voters(
None DataProviderBounds::default()
) )
.unwrap() .unwrap()
.into_iter() .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 // 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` voters, and if some of them end up being skipped, we iterate at most `2 *
// maybe_max_len`. // maybe_max_len`.
#[test] #[test]
#[should_panic]
fn only_iterates_max_2_times_max_allowed_len() { fn only_iterates_max_2_times_max_allowed_len() {
ExtBuilder::default() ExtBuilder::default()
.nominate(false) .nominate(false)
@@ -4659,13 +4652,14 @@ mod election_data_provider {
StakerStatus::<AccountId>::Nominator(vec![21, 22, 23, 24, 25]), StakerStatus::<AccountId>::Nominator(vec![21, 22, 23, 24, 25]),
) )
.build_and_execute(|| { .build_and_execute(|| {
let bounds_builder = ElectionBoundsBuilder::default();
// all voters ordered by stake, // all voters ordered by stake,
assert_eq!( assert_eq!(
<Test as Config>::VoterList::iter().collect::<Vec<_>>(), <Test as Config>::VoterList::iter().collect::<Vec<_>>(),
vec![61, 71, 81, 11, 21, 31] 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: // we want 2 voters now, and in maximum we allow 4 iterations. This is what happens:
// 61 is pruned; // 61 is pruned;
@@ -4674,7 +4668,7 @@ mod election_data_provider {
// 11 is taken; // 11 is taken;
// we finish since the 2x limit is reached. // we finish since the 2x limit is reached.
assert_eq!( assert_eq!(
Staking::electing_voters(Some(2)) Staking::electing_voters(bounds_builder.voters_count(2.into()).build().voters)
.unwrap() .unwrap()
.iter() .iter()
.map(|(stash, _, _)| stash) .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] #[test]
fn estimate_next_election_works() { fn estimate_next_election_works() {
ExtBuilder::default().session_per_era(5).period(5).build_and_execute(|| { ExtBuilder::default().session_per_era(5).period(5).build_and_execute(|| {
@@ -5120,7 +5297,8 @@ fn min_commission_works() {
} }
#[test] #[test]
fn change_of_max_nominations() { #[should_panic]
fn change_of_absolute_max_nominations() {
use frame_election_provider_support::ElectionDataProvider; use frame_election_provider_support::ElectionDataProvider;
ExtBuilder::default() ExtBuilder::default()
.add_staker(61, 61, 10, StakerStatus::Nominator(vec![1])) .add_staker(61, 61, 10, StakerStatus::Nominator(vec![1]))
@@ -5128,7 +5306,7 @@ fn change_of_max_nominations() {
.balance_factor(10) .balance_factor(10)
.build_and_execute(|| { .build_and_execute(|| {
// pre-condition // pre-condition
assert_eq!(MaxNominations::get(), 16); assert_eq!(AbsoluteMaxNominations::get(), 16);
assert_eq!( assert_eq!(
Nominators::<Test>::iter() Nominators::<Test>::iter()
@@ -5136,11 +5314,15 @@ fn change_of_max_nominations() {
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
vec![(101, 2), (71, 3), (61, 1)] vec![(101, 2), (71, 3), (61, 1)]
); );
// default bounds are unbounded.
let bounds = DataProviderBounds::default();
// 3 validators and 3 nominators // 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. // abrupt change from 16 to 4, everyone should be fine.
MaxNominations::set(4); AbsoluteMaxNominations::set(4);
assert_eq!( assert_eq!(
Nominators::<Test>::iter() Nominators::<Test>::iter()
@@ -5148,10 +5330,10 @@ fn change_of_max_nominations() {
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
vec![(101, 2), (71, 3), (61, 1)] 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. // abrupt change from 4 to 3, everyone should be fine.
MaxNominations::set(3); AbsoluteMaxNominations::set(3);
assert_eq!( assert_eq!(
Nominators::<Test>::iter() Nominators::<Test>::iter()
@@ -5159,11 +5341,11 @@ fn change_of_max_nominations() {
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
vec![(101, 2), (71, 3), (61, 1)] 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 // abrupt change from 3 to 2, this should cause some nominators to be non-decodable, and
// thus non-existent unless if they update. // thus non-existent unless if they update.
MaxNominations::set(2); AbsoluteMaxNominations::set(2);
assert_eq!( assert_eq!(
Nominators::<Test>::iter() Nominators::<Test>::iter()
@@ -5176,12 +5358,12 @@ fn change_of_max_nominations() {
// but its value cannot be decoded and default is returned. // but its value cannot be decoded and default is returned.
assert!(Nominators::<Test>::get(71).is_none()); 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)); assert!(Nominators::<Test>::contains_key(101));
// abrupt change from 2 to 1, this should cause some nominators to be non-decodable, and // abrupt change from 2 to 1, this should cause some nominators to be non-decodable, and
// thus non-existent unless if they update. // thus non-existent unless if they update.
MaxNominations::set(1); AbsoluteMaxNominations::set(1);
assert_eq!( assert_eq!(
Nominators::<Test>::iter() Nominators::<Test>::iter()
@@ -5193,7 +5375,7 @@ fn change_of_max_nominations() {
assert!(Nominators::<Test>::contains_key(61)); assert!(Nominators::<Test>::contains_key(61));
assert!(Nominators::<Test>::get(71).is_none()); assert!(Nominators::<Test>::get(71).is_none());
assert!(Nominators::<Test>::get(61).is_some()); 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. // now one of them can revive themselves by re-nominating to a proper value.
assert_ok!(Staking::nominate(RuntimeOrigin::signed(71), vec![1])); 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 { mod sorted_list_provider {
use super::*; use super::*;
use frame_election_provider_support::SortedListProvider; use frame_election_provider_support::SortedListProvider;
@@ -404,7 +404,7 @@ impl<AccountId: IdentifierT> Voter<AccountId> {
}) })
} }
/// This voter's budget /// This voter's budget.
#[inline] #[inline]
pub fn budget(&self) -> ExtendedBalance { pub fn budget(&self) -> ExtendedBalance {
self.budget self.budget