diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 29aaaf5fdad5a..0462a7afde0bd 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -24,6 +24,7 @@ use codec::{Decode, Encode, MaxEncodedLen}; use frame_election_provider_support::{ + bounds::{ElectionBounds, ElectionBoundsBuilder}, onchain, BalancingConfig, ElectionDataProvider, SequentialPhragmen, VoteWeight, }; use frame_support::{ @@ -562,6 +563,9 @@ parameter_types! { pub HistoryDepth: u32 = 84; } +/// Upper limit on the number of NPOS nominations. +const MAX_QUOTA_NOMINATIONS: u32 = 16; + pub struct StakingBenchmarkingConfig; impl pallet_staking::BenchmarkingConfig for StakingBenchmarkingConfig { type MaxNominators = ConstU32<1000>; @@ -569,7 +573,6 @@ impl pallet_staking::BenchmarkingConfig for StakingBenchmarkingConfig { } impl pallet_staking::Config for Runtime { - type MaxNominations = MaxNominations; type Currency = Balances; type CurrencyBalance = Balance; type UnixTime = Timestamp; @@ -594,6 +597,7 @@ impl pallet_staking::Config for Runtime { type ElectionProvider = ElectionProviderMultiPhase; type GenesisElectionProvider = onchain::OnChainExecution; type VoterList = VoterList; + type NominationsQuota = pallet_staking::FixedNominationsQuota; // This a placeholder, to be introduced in the next PR as an instance of bags-list type TargetList = pallet_staking::UseValidatorsMap; type MaxUnlockingChunks = ConstU32<32>; @@ -647,17 +651,20 @@ frame_election_provider_support::generate_solution_type!( VoterIndex = u32, TargetIndex = u16, Accuracy = sp_runtime::PerU16, - MaxVoters = MaxElectingVoters, + MaxVoters = MaxElectingVotersSolution, >(16) ); 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 = ::LIMIT as u32; - pub MaxElectingVoters: u32 = 40_000; - pub MaxElectableTargets: u16 = 10_000; - // OnChain values are lower. - pub MaxOnChainElectingVoters: u32 = 5000; - pub MaxOnChainElectableTargets: u16 = 1250; + pub MaxElectingVotersSolution: u32 = 40_000; // The maximum winners that can be elected by the Election pallet which is equivalent to the // maximum active validators the staking pallet can have. pub MaxActiveValidators: u32 = 1000; @@ -712,8 +719,7 @@ impl onchain::Config for OnChainSeqPhragmen { type DataProvider = ::DataProvider; type WeightInfo = frame_election_provider_support::weights::SubstrateWeight; type MaxWinners = ::MaxWinners; - type VotersBound = MaxOnChainElectingVoters; - type TargetsBound = MaxOnChainElectableTargets; + type Bounds = ElectionBoundsOnChain; } 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; type Solver = SequentialPhragmen, OffchainRandomBalancing>; type ForceOrigin = EnsureRootOrHalfCouncil; - type MaxElectableTargets = MaxElectableTargets; type MaxWinners = MaxActiveValidators; - type MaxElectingVoters = MaxElectingVoters; + type ElectionBounds = ElectionBoundsMultiPhase; type BenchmarkingConfig = ElectionProviderBenchmarkConfig; type WeightInfo = pallet_election_provider_multi_phase::weights::SubstrateWeight; } diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index dc8e8e3499fad..dbffe9f312e60 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -19,12 +19,16 @@ use crate::{self as pallet_babe, Config, CurrentSlot}; use codec::Encode; -use frame_election_provider_support::{onchain, SequentialPhragmen}; +use frame_election_provider_support::{ + bounds::{ElectionBounds, ElectionBoundsBuilder}, + onchain, SequentialPhragmen, +}; use frame_support::{ parameter_types, traits::{ConstU128, ConstU32, ConstU64, KeyOwnerProofSystem, OnInitialize}, }; use pallet_session::historical as pallet_session_historical; +use pallet_staking::FixedNominationsQuota; use sp_consensus_babe::{AuthorityId, AuthorityPair, Randomness, Slot, VrfSignature}; use sp_core::{ crypto::{KeyTypeId, Pair, VrfSecret}, @@ -161,6 +165,7 @@ parameter_types! { pub const SlashDeferDuration: EraIndex = 0; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(16); + pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build(); } pub struct OnChainSeqPhragmen; @@ -170,12 +175,10 @@ impl onchain::Config for OnChainSeqPhragmen { type DataProvider = Staking; type WeightInfo = (); type MaxWinners = ConstU32<100>; - type VotersBound = ConstU32<{ u32::MAX }>; - type TargetsBound = ConstU32<{ u32::MAX }>; + type Bounds = ElectionsBounds; } impl pallet_staking::Config for Test { - type MaxNominations = ConstU32<16>; type RewardRemainder = (); type CurrencyToVote = (); type RuntimeEvent = RuntimeEvent; @@ -197,6 +200,7 @@ impl pallet_staking::Config for Test { type GenesisElectionProvider = Self::ElectionProvider; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; type TargetList = pallet_staking::UseValidatorsMap; + type NominationsQuota = FixedNominationsQuota<16>; type MaxUnlockingChunks = ConstU32<32>; type HistoryDepth = ConstU32<84>; type EventListeners = (); diff --git a/frame/bags-list/remote-tests/src/snapshot.rs b/frame/bags-list/remote-tests/src/snapshot.rs index 0c6e194d32478..78c5b4e1c7b6d 100644 --- a/frame/bags-list/remote-tests/src/snapshot.rs +++ b/frame/bags-list/remote-tests/src/snapshot.rs @@ -16,7 +16,10 @@ //! 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 remote_externalities::{Builder, Mode, OnlineConfig}; use sp_runtime::{traits::Block as BlockT, DeserializeOwned}; @@ -62,8 +65,13 @@ where ::VoterList::count(), ); + let bounds = match voter_limit { + None => DataProviderBounds::default(), + Some(v) => DataProviderBounds { count: Some(CountBound(v as u32)), size: None }, + }; + let voters = - as ElectionDataProvider>::electing_voters(voter_limit) + as ElectionDataProvider>::electing_voters(bounds) .unwrap(); let mut voters_nominator_only = voters diff --git a/frame/beefy/src/mock.rs b/frame/beefy/src/mock.rs index af770c0b49448..f2d8415bc01f7 100644 --- a/frame/beefy/src/mock.rs +++ b/frame/beefy/src/mock.rs @@ -17,7 +17,10 @@ use std::vec; -use frame_election_provider_support::{onchain, SequentialPhragmen}; +use frame_election_provider_support::{ + bounds::{ElectionBounds, ElectionBoundsBuilder}, + onchain, SequentialPhragmen, +}; use frame_support::{ construct_runtime, parameter_types, sp_io::TestExternalities, @@ -184,6 +187,7 @@ parameter_types! { pub const BondingDuration: EraIndex = 3; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17); + pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build(); } pub struct OnChainSeqPhragmen; @@ -193,12 +197,10 @@ impl onchain::Config for OnChainSeqPhragmen { type DataProvider = Staking; type WeightInfo = (); type MaxWinners = ConstU32<100>; - type VotersBound = ConstU32<{ u32::MAX }>; - type TargetsBound = ConstU32<{ u32::MAX }>; + type Bounds = ElectionsBoundsOnChain; } impl pallet_staking::Config for Test { - type MaxNominations = ConstU32<16>; type RewardRemainder = (); type CurrencyToVote = (); type RuntimeEvent = RuntimeEvent; @@ -220,6 +222,7 @@ impl pallet_staking::Config for Test { type GenesisElectionProvider = Self::ElectionProvider; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; type TargetList = pallet_staking::UseValidatorsMap; + type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; type MaxUnlockingChunks = ConstU32<32>; type HistoryDepth = ConstU32<84>; type EventListeners = (); diff --git a/frame/election-provider-multi-phase/src/benchmarking.rs b/frame/election-provider-multi-phase/src/benchmarking.rs index e9a4583b576ad..4a2855f1361f2 100644 --- a/frame/election-provider-multi-phase/src/benchmarking.rs +++ b/frame/election-provider-multi-phase/src/benchmarking.rs @@ -20,6 +20,7 @@ use super::*; use crate::{unsigned::IndexAssignmentOf, Pallet as MultiPhase}; use frame_benchmarking::account; +use frame_election_provider_support::bounds::DataProviderBounds; use frame_support::{ assert_ok, 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. set_up_data_provider::(v, t); - let targets = T::DataProvider::electable_targets(None)?; - let voters = T::DataProvider::electing_voters(None)?; + // default bounds are unbounded. + let targets = T::DataProvider::electable_targets(DataProviderBounds::default())?; + let voters = T::DataProvider::electing_voters(DataProviderBounds::default())?; let desired_targets = T::DataProvider::desired_targets()?; assert!(>::snapshot().is_none()); }: { diff --git a/frame/election-provider-multi-phase/src/lib.rs b/frame/election-provider-multi-phase/src/lib.rs index 03ab5574a2180..f26a6f40d4267 100644 --- a/frame/election-provider-multi-phase/src/lib.rs +++ b/frame/election-provider-multi-phase/src/lib.rs @@ -231,8 +231,9 @@ use codec::{Decode, Encode}; use frame_election_provider_support::{ - BoundedSupportsOf, ElectionDataProvider, ElectionProvider, ElectionProviderBase, - InstantElectionProvider, NposSolution, + bounds::{CountBound, ElectionBounds, ElectionBoundsBuilder, SizeBound}, + BoundedSupportsOf, DataProviderBounds, ElectionDataProvider, ElectionProvider, + ElectionProviderBase, InstantElectionProvider, NposSolution, }; use frame_support::{ dispatch::DispatchClass, @@ -659,16 +660,6 @@ pub mod pallet { #[pallet::constant] type SignedDepositWeight: Get>; - /// 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>; - - /// The maximum number of electable targets to put in the snapshot. - #[pallet::constant] - type MaxElectableTargets: Get>; - /// The maximum number of winners that can be elected by this `ElectionProvider` /// implementation. /// @@ -676,6 +667,11 @@ pub mod pallet { #[pallet::constant] type MaxWinners: Get; + /// 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; + /// Handler for the slashed deposits. type SlashHandler: OnUnbalanced>; @@ -1097,13 +1093,19 @@ pub mod pallet { T::ForceOrigin::ensure_origin(origin)?; ensure!(Self::current_phase().is_emergency(), >::CallNotAllowed); - let supports = - T::GovernanceFallback::instant_elect(maybe_max_voters, maybe_max_targets).map_err( - |e| { - log!(error, "GovernanceFallback failed: {:?}", e); - Error::::FallbackFailed - }, - )?; + let election_bounds = ElectionBoundsBuilder::default() + .voters_count(maybe_max_voters.unwrap_or(u32::MAX).into()) + .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); + Error::::FallbackFailed + })?; // transform BoundedVec<_, T::GovernanceFallback::MaxWinners> into // `BoundedVec<_, T::MaxWinners>` @@ -1426,19 +1428,28 @@ impl Pallet { /// Extracted for easier weight calculation. fn create_snapshot_external( ) -> Result<(Vec, Vec>, u32), ElectionError> { - let target_limit = T::MaxElectableTargets::get().saturated_into::(); - let voter_limit = T::MaxElectingVoters::get().saturated_into::(); + let election_bounds = T::ElectionBounds::get(); - 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)?; - 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)?; - if targets.len() > target_limit || voters.len() > voter_limit { - return Err(ElectionError::DataProvider("Snapshot too big for submission.")) - } - let mut desired_targets = as ElectionProviderBase>::desired_targets_checked() .map_err(|e| ElectionError::DataProvider(e))?; @@ -1544,18 +1555,25 @@ impl Pallet { // - signed phase was complete or not started, in which case finalization is idempotent and // inexpensive (1 read of an empty vector). let _ = Self::finalize_signed_phase(); + >::take() .ok_or(ElectionError::::NothingQueued) .or_else(|_| { - T::Fallback::instant_elect(None, None) - .map_err(|fe| ElectionError::Fallback(fe)) - .and_then(|supports| { - Ok(ReadySolution { - supports, - score: Default::default(), - compute: ElectionCompute::Fallback, - }) + // 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)) + .and_then(|supports| { + Ok(ReadySolution { + supports, + score: Default::default(), + compute: ElectionCompute::Fallback, }) + }) }) .map(|ReadySolution { compute, score, supports }| { Self::deposit_event(Event::ElectionFinalized { compute, score }); @@ -1925,8 +1943,8 @@ mod tests { use crate::{ mock::{ multi_phase_events, raw_solution, roll_to, roll_to_signed, roll_to_unsigned, AccountId, - ExtBuilder, MockWeightInfo, MockedWeightInfo, MultiPhase, Runtime, RuntimeOrigin, - SignedMaxSubmissions, System, TargetIndex, Targets, + ElectionsBounds, ExtBuilder, MockWeightInfo, MockedWeightInfo, MultiPhase, Runtime, + RuntimeOrigin, SignedMaxSubmissions, System, TargetIndex, Targets, Voters, }, Phase, }; @@ -2529,7 +2547,11 @@ mod tests { fn snapshot_too_big_failure_onchain_fallback() { // the `MockStaking` is designed such that if it has too many targets, it simply fails. ExtBuilder::default().build_and_execute(|| { - Targets::set((0..(TargetIndex::max_value() as AccountId) + 1).collect::>()); + // 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::>()); // Signed phase failed to open. roll_to(15); @@ -2564,9 +2586,11 @@ mod tests { fn snapshot_too_big_failure_no_fallback() { // and if the backup mode is nothing, we go into the emergency mode.. ExtBuilder::default().onchain_fallback(false).build_and_execute(|| { - crate::mock::Targets::set( - (0..(TargetIndex::max_value() as AccountId) + 1).collect::>(), - ); + // sets bounds on number of targets. + 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::>()); // Signed phase failed to open. roll_to(15); @@ -2596,9 +2620,10 @@ mod tests { // but if there are too many voters, we simply truncate them. ExtBuilder::default().build_and_execute(|| { // we have 8 voters in total. - assert_eq!(crate::mock::Voters::get().len(), 8); + assert_eq!(Voters::get().len(), 8); // 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. roll_to_signed(); diff --git a/frame/election-provider-multi-phase/src/mock.rs b/frame/election-provider-multi-phase/src/mock.rs index c5133d89e4c82..82c7279879feb 100644 --- a/frame/election-provider-multi-phase/src/mock.rs +++ b/frame/election-provider-multi-phase/src/mock.rs @@ -18,9 +18,8 @@ use super::*; use crate::{self as multi_phase, unsigned::MinerConfig}; use frame_election_provider_support::{ - data_provider, - onchain::{self}, - ElectionDataProvider, NposSolution, SequentialPhragmen, + bounds::{DataProviderBounds, ElectionBounds}, + data_provider, onchain, ElectionDataProvider, NposSolution, SequentialPhragmen, }; pub use frame_support::{assert_noop, assert_ok, pallet_prelude::GetDefault}; use frame_support::{ @@ -300,7 +299,9 @@ parameter_types! { #[derive(Debug)] 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 OnChainFallback: bool = true; } @@ -312,8 +313,7 @@ impl onchain::Config for OnChainSeqPhragmen { type DataProvider = StakingMock; type WeightInfo = (); type MaxWinners = MaxWinners; - type VotersBound = ConstU32<{ u32::MAX }>; - type TargetsBound = ConstU32<{ u32::MAX }>; + type Bounds = OnChainElectionsBounds; } pub struct MockFallback; @@ -327,12 +327,15 @@ impl ElectionProviderBase for MockFallback { impl InstantElectionProvider for MockFallback { fn instant_elect( - max_voters: Option, - max_targets: Option, + voters_bounds: DataProviderBounds, + targets_bounds: DataProviderBounds, ) -> Result, Self::Error> { if OnChainFallback::get() { - onchain::OnChainExecution::::instant_elect(max_voters, max_targets) - .map_err(|_| "onchain::OnChainExecution failed.") + onchain::OnChainExecution::::instant_elect( + voters_bounds, + targets_bounds, + ) + .map_err(|_| "onchain::OnChainExecution failed.") } else { Err("NoFallback.") } @@ -404,11 +407,10 @@ impl crate::Config for Runtime { type GovernanceFallback = frame_election_provider_support::onchain::OnChainExecution; type ForceOrigin = frame_system::EnsureRoot; - type MaxElectingVoters = MaxElectingVoters; - type MaxElectableTargets = MaxElectableTargets; type MaxWinners = MaxWinners; type MinerConfig = Self; type Solver = SequentialPhragmen, Balancing>; + type ElectionBounds = ElectionsBounds; } impl frame_system::offchain::SendTransactionTypes for Runtime @@ -436,11 +438,11 @@ impl ElectionDataProvider for StakingMock { type AccountId = AccountId; type MaxVotesPerVoter = MaxNominations; - fn electable_targets(maybe_max_len: Option) -> data_provider::Result> { + fn electable_targets(bounds: DataProviderBounds) -> data_provider::Result> { let targets = Targets::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") } @@ -448,13 +450,12 @@ impl ElectionDataProvider for StakingMock { Ok(targets) } - fn electing_voters( - maybe_max_len: Option, - ) -> data_provider::Result>> { + fn electing_voters(bounds: DataProviderBounds) -> data_provider::Result>> { let mut voters = Voters::get(); + if !DataProviderAllowBadData::get() { - if let Some(max_len) = maybe_max_len { - voters.truncate(max_len) + if let Some(max_len) = bounds.count { + voters.truncate(max_len.0 as usize) } } diff --git a/frame/election-provider-multi-phase/src/signed.rs b/frame/election-provider-multi-phase/src/signed.rs index 226404e4afc1a..76068ba99d36c 100644 --- a/frame/election-provider-multi-phase/src/signed.rs +++ b/frame/election-provider-multi-phase/src/signed.rs @@ -536,7 +536,10 @@ impl Pallet { #[cfg(test)] mod tests { 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}; #[test] @@ -565,13 +568,14 @@ mod tests { fn data_provider_should_respect_target_limits() { ExtBuilder::default().build_and_execute(|| { // 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 DataProviderAllowBadData::set(true); assert_noop!( 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() { ExtBuilder::default().build_and_execute(|| { // 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 DataProviderAllowBadData::set(true); assert_noop!( MultiPhase::create_snapshot(), - ElectionError::DataProvider("Snapshot too big for submission."), + ElectionError::DataProvider("Ensure voters bounds: bounds exceeded."), ); }) } diff --git a/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index 0d5f8872193ad..9c3511ae35751 100644 --- a/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -17,7 +17,6 @@ #![allow(dead_code)] -use _feps::ExtendedBalance; use frame_support::{ assert_ok, dispatch::UnfilteredDispatchable, parameter_types, traits, traits::Hooks, weights::constants, @@ -42,7 +41,10 @@ use sp_std::prelude::*; use std::collections::BTreeMap; 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::{ 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 // enetering in emergency phase after the election failed. pub static MinBlocksBeforeEmergency: BlockNumber = 3; - pub static MaxElectingVoters: VoterIndex = 1000; - pub static MaxElectableTargets: TargetIndex = 1000; pub static MaxActiveValidators: u32 = 1000; pub static OffchainRepeat: u32 = 5; pub static MinerMaxLength: u32 = 256; @@ -181,8 +181,8 @@ parameter_types! { pub static TransactionPriority: transaction_validity::TransactionPriority = 1; #[derive(Debug)] pub static MaxWinners: u32 = 100; - pub static MaxVotesPerVoter: u32 = 16; - pub static MaxNominations: u32 = 16; + pub static ElectionBounds: frame_election_provider_support::bounds::ElectionBounds = ElectionBoundsBuilder::default() + .voters_count(1_000.into()).targets_count(1_000.into()).build(); } 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; type Solver = SequentialPhragmen, ()>; type ForceOrigin = EnsureRoot; - type MaxElectableTargets = MaxElectableTargets; - type MaxElectingVoters = MaxElectingVoters; type MaxWinners = MaxWinners; + type ElectionBounds = ElectionBounds; type BenchmarkingConfig = NoopElectionProviderBenchmarkConfig; type WeightInfo = (); } @@ -252,8 +251,10 @@ impl pallet_bags_list::Config for Runtime { type Score = VoteWeight; } +/// Upper limit on the number of NPOS nominations. +const MAX_QUOTA_NOMINATIONS: u32 = 16; + impl pallet_staking::Config for Runtime { - type MaxNominations = MaxNominations; type Currency = Balances; type CurrencyBalance = Balance; type UnixTime = Timestamp; @@ -274,6 +275,7 @@ impl pallet_staking::Config for Runtime { type ElectionProvider = ElectionProviderMultiPhase; type GenesisElectionProvider = onchain::OnChainExecution; type VoterList = BagsList; + type NominationsQuota = pallet_staking::FixedNominationsQuota; type TargetList = pallet_staking::UseValidatorsMap; type MaxUnlockingChunks = ConstU32<32>; type HistoryDepth = HistoryDepth; @@ -306,8 +308,7 @@ impl onchain::Config for OnChainSeqPhragmen { type DataProvider = Staking; type WeightInfo = (); type MaxWinners = MaxWinners; - type VotersBound = VotersBound; - type TargetsBound = TargetsBound; + type Bounds = ElectionBounds; } pub struct NoopElectionProviderBenchmarkConfig; diff --git a/frame/election-provider-support/src/bounds.rs b/frame/election-provider-support/src/bounds.rs new file mode 100644 index 0000000000000..b9ae21e49ca70 --- /dev/null +++ b/frame/election-provider-support/src/bounds.rs @@ -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 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 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, + pub size: Option, +} + +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, given_count: Option) -> 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, + targets: Option, +} + +impl From 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) -> Self { + self.voters = bounds; + self + } + + /// Set the targets bounds. + pub fn targets(mut self, bounds: Option) -> 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)); + } +} diff --git a/frame/election-provider-support/src/lib.rs b/frame/election-provider-support/src/lib.rs index 978a5df427244..577ac9c0b65a9 100644 --- a/frame/election-provider-support/src/lib.rs +++ b/frame/election-provider-support/src/lib.rs @@ -109,12 +109,12 @@ //! fn desired_targets() -> data_provider::Result { //! Ok(1) //! } -//! fn electing_voters(maybe_max_len: Option) +//! fn electing_voters(bounds: DataProviderBounds) //! -> data_provider::Result>> //! { //! Ok(Default::default()) //! } -//! fn electable_targets(maybe_max_len: Option) -> data_provider::Result> { +//! fn electable_targets(bounds: DataProviderBounds) -> data_provider::Result> { //! Ok(vec![10, 20, 30]) //! } //! fn next_election_prediction(now: BlockNumber) -> BlockNumber { @@ -145,7 +145,7 @@ //! impl ElectionProvider for GenericElectionProvider { //! fn ongoing() -> bool { false } //! fn elect() -> Result, Self::Error> { -//! Self::DataProvider::electable_targets(None) +//! Self::DataProvider::electable_targets(DataProviderBounds::default()) //! .map_err(|_| "failed to elect") //! .map(|t| bounded_vec![(t[0], Support::default())]) //! } @@ -173,11 +173,15 @@ #![cfg_attr(not(feature = "std"), no_std)] +pub mod bounds; pub mod onchain; pub mod traits; + use sp_runtime::traits::{Bounded, Saturating, Zero}; use sp_std::{fmt::Debug, prelude::*}; +pub use bounds::DataProviderBounds; +pub use codec::{Decode, Encode}; /// Re-export the solution generation macro. pub use frame_election_provider_solution_type::generate_solution_type; 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`. This property turns out /// to be important when trimming for solution length. #[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 { /// Index of the voter among the voters list. pub who: VoterIndex, @@ -275,7 +279,7 @@ pub mod data_provider { /// Something that can provide the data to an [`ElectionProvider`]. pub trait ElectionDataProvider { /// The account identifier type. - type AccountId; + type AccountId: Encode; /// The block number type. type BlockNumber; @@ -286,25 +290,18 @@ pub trait ElectionDataProvider { /// All possible targets for the election, i.e. the targets that could become elected, thus /// "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 /// appropriate weight at the end of execution with the system pallet directly. - fn electable_targets( - maybe_max_len: Option, - ) -> data_provider::Result>; + fn electable_targets(bounds: DataProviderBounds) + -> data_provider::Result>; /// All the voters that participate in the election, thus "electing". /// /// 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 /// appropriate weight at the end of execution with the system pallet directly. - fn electing_voters(maybe_max_len: Option) -> data_provider::Result>>; + fn electing_voters(bounds: DataProviderBounds) -> data_provider::Result>>; /// 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`. pub trait InstantElectionProvider: ElectionProviderBase { fn instant_elect( - forced_input_voters_bound: Option, - forced_input_target_bound: Option, + forced_input_voters_bound: DataProviderBounds, + forced_input_target_bound: DataProviderBounds, ) -> Result, Self::Error>; } @@ -468,8 +465,8 @@ where MaxWinners: Get, { fn instant_elect( - _: Option, - _: Option, + _: DataProviderBounds, + _: DataProviderBounds, ) -> Result, Self::Error> { Err("`NoElection` cannot do anything.") } diff --git a/frame/election-provider-support/src/onchain.rs b/frame/election-provider-support/src/onchain.rs index bfdc21a8fe2d6..21ee710ea304d 100644 --- a/frame/election-provider-support/src/onchain.rs +++ b/frame/election-provider-support/src/onchain.rs @@ -20,6 +20,7 @@ //! careful when using it onchain. use crate::{ + bounds::{DataProviderBounds, ElectionBounds, ElectionBoundsBuilder}, BoundedSupportsOf, Debug, ElectionDataProvider, ElectionProvider, ElectionProviderBase, InstantElectionProvider, NposSolver, WeightInfo, }; @@ -52,8 +53,7 @@ impl From for Error { /// This implements both `ElectionProvider` and `InstantElectionProvider`. /// /// 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 -/// the size of the input is sensible. +/// thoughtful value must be set as [`Config::Bounds`] to ensure the size of the input is sensible. pub struct OnChainExecution(PhantomData); #[deprecated(note = "use OnChainExecution, which is bounded by default")] @@ -85,13 +85,9 @@ pub trait Config { /// always be more than `DataProvider::desired_target`. type MaxWinners: Get; - /// Bounds the number of voters, when calling into [`Config::DataProvider`]. It might be - /// overwritten in the `InstantElectionProvider` impl. - type VotersBound: Get; - - /// Bounds the number of targets, when calling into [`Config::DataProvider`]. It might be - /// overwritten in the `InstantElectionProvider` impl. - type TargetsBound: Get; + /// Elections bounds, to use when calling into [`Config::DataProvider`]. It might be overwritten + /// in the `InstantElectionProvider` impl. + type Bounds: Get; } /// Same as `BoundedSupportsOf` but for `onchain::Config`. @@ -101,12 +97,12 @@ pub type OnChainBoundedSupportsOf = BoundedSupports< >; fn elect_with_input_bounds( - maybe_max_voters: Option, - maybe_max_targets: Option, + bounds: ElectionBounds, ) -> Result, Error> { - let voters = T::DataProvider::electing_voters(maybe_max_voters).map_err(Error::DataProvider)?; - let targets = - T::DataProvider::electable_targets(maybe_max_targets).map_err(Error::DataProvider)?; + let (voters, targets) = T::DataProvider::electing_voters(bounds.voters) + .and_then(|voters| Ok((voters, T::DataProvider::electable_targets(bounds.targets)?))) + .map_err(Error::DataProvider)?; + let desired_targets = T::DataProvider::desired_targets().map_err(Error::DataProvider)?; if desired_targets > T::MaxWinners::get() { @@ -159,13 +155,15 @@ impl ElectionProviderBase for OnChainExecution { impl InstantElectionProvider for OnChainExecution { fn instant_elect( - forced_input_voters_bound: Option, - forced_input_target_bound: Option, + forced_input_voters_bounds: DataProviderBounds, + forced_input_targets_bounds: DataProviderBounds, ) -> Result, Self::Error> { - elect_with_input_bounds::( - Some(T::VotersBound::get().min(forced_input_voters_bound.unwrap_or(u32::MAX)) as usize), - Some(T::TargetsBound::get().min(forced_input_target_bound.unwrap_or(u32::MAX)) as usize), - ) + let elections_bounds = ElectionBoundsBuilder::from(T::Bounds::get()) + .voters_or_lower(forced_input_voters_bounds) + .targets_or_lower(forced_input_targets_bounds) + .build(); + + elect_with_input_bounds::(elections_bounds) } } @@ -175,10 +173,8 @@ impl ElectionProvider for OnChainExecution { } fn elect() -> Result, Self::Error> { - elect_with_input_bounds::( - Some(T::VotersBound::get() as usize), - Some(T::TargetsBound::get() as usize), - ) + let election_bounds = ElectionBoundsBuilder::from(T::Bounds::get()).build(); + elect_with_input_bounds::(election_bounds) } } @@ -186,7 +182,7 @@ impl ElectionProvider for OnChainExecution { mod tests { use super::*; 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_runtime::Perbill; type AccountId = u64; @@ -236,6 +232,7 @@ mod tests { parameter_types! { pub static MaxWinners: u32 = 10; pub static DesiredTargets: u32 = 2; + pub static Bounds: ElectionBounds = ElectionBoundsBuilder::default().voters_count(600.into()).targets_count(400.into()).build(); } impl Config for PhragmenParams { @@ -244,8 +241,7 @@ mod tests { type DataProvider = mock_data_provider::DataProvider; type WeightInfo = (); type MaxWinners = MaxWinners; - type VotersBound = ConstU32<600>; - type TargetsBound = ConstU32<400>; + type Bounds = Bounds; } impl Config for PhragMMSParams { @@ -254,8 +250,7 @@ mod tests { type DataProvider = mock_data_provider::DataProvider; type WeightInfo = (); type MaxWinners = MaxWinners; - type VotersBound = ConstU32<600>; - type TargetsBound = ConstU32<400>; + type Bounds = Bounds; } mod mock_data_provider { @@ -269,7 +264,7 @@ mod tests { type AccountId = AccountId; type BlockNumber = BlockNumber; type MaxVotesPerVoter = ConstU32<2>; - fn electing_voters(_: Option) -> data_provider::Result>> { + fn electing_voters(_: DataProviderBounds) -> data_provider::Result>> { Ok(vec![ (1, 10, bounded_vec![10, 20]), (2, 20, bounded_vec![30, 20]), @@ -277,7 +272,7 @@ mod tests { ]) } - fn electable_targets(_: Option) -> data_provider::Result> { + fn electable_targets(_: DataProviderBounds) -> data_provider::Result> { Ok(vec![10, 20, 30]) } diff --git a/frame/fast-unstake/src/mock.rs b/frame/fast-unstake/src/mock.rs index 192d525e278d7..3a3d9e2d4b133 100644 --- a/frame/fast-unstake/src/mock.rs +++ b/frame/fast-unstake/src/mock.rs @@ -135,7 +135,6 @@ impl frame_election_provider_support::ElectionProvider for MockElection { } impl pallet_staking::Config for Runtime { - type MaxNominations = ConstU32<16>; type Currency = Balances; type CurrencyBalance = Balance; type UnixTime = pallet_timestamp::Pallet; @@ -158,6 +157,7 @@ impl pallet_staking::Config for Runtime { type GenesisElectionProvider = Self::ElectionProvider; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; type TargetList = pallet_staking::UseValidatorsMap; + type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; type MaxUnlockingChunks = ConstU32<32>; type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; diff --git a/frame/grandpa/src/mock.rs b/frame/grandpa/src/mock.rs index 86f91ba803230..fd4d737dc493f 100644 --- a/frame/grandpa/src/mock.rs +++ b/frame/grandpa/src/mock.rs @@ -22,7 +22,10 @@ use crate::{self as pallet_grandpa, AuthorityId, AuthorityList, Config, ConsensusLog}; use ::grandpa as finality_grandpa; use codec::Encode; -use frame_election_provider_support::{onchain, SequentialPhragmen}; +use frame_election_provider_support::{ + bounds::{ElectionBounds, ElectionBoundsBuilder}, + onchain, SequentialPhragmen, +}; use frame_support::{ parameter_types, traits::{ConstU128, ConstU32, ConstU64, KeyOwnerProofSystem, OnFinalize, OnInitialize}, @@ -164,6 +167,7 @@ parameter_types! { pub const BondingDuration: EraIndex = 3; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17); + pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build(); } pub struct OnChainSeqPhragmen; @@ -173,12 +177,10 @@ impl onchain::Config for OnChainSeqPhragmen { type DataProvider = Staking; type WeightInfo = (); type MaxWinners = ConstU32<100>; - type VotersBound = ConstU32<{ u32::MAX }>; - type TargetsBound = ConstU32<{ u32::MAX }>; + type Bounds = ElectionsBoundsOnChain; } impl pallet_staking::Config for Test { - type MaxNominations = ConstU32<16>; type RewardRemainder = (); type CurrencyToVote = (); type RuntimeEvent = RuntimeEvent; @@ -200,6 +202,7 @@ impl pallet_staking::Config for Test { type GenesisElectionProvider = Self::ElectionProvider; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; type TargetList = pallet_staking::UseValidatorsMap; + type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; type MaxUnlockingChunks = ConstU32<32>; type HistoryDepth = ConstU32<84>; type EventListeners = (); diff --git a/frame/nomination-pools/benchmarking/src/lib.rs b/frame/nomination-pools/benchmarking/src/lib.rs index 30bef6221c968..e757f66e725a0 100644 --- a/frame/nomination-pools/benchmarking/src/lib.rs +++ b/frame/nomination-pools/benchmarking/src/lib.rs @@ -35,6 +35,7 @@ use pallet_nomination_pools::{ MaxPoolMembersPerPool, MaxPools, Metadata, MinCreateBond, MinJoinBond, Pallet as Pools, PoolMembers, PoolRoles, PoolState, RewardPools, SubPoolsStorage, }; +use pallet_staking::MaxNominationsOf; use sp_runtime::{ traits::{Bounded, StaticLookup, Zero}, Perbill, @@ -564,7 +565,7 @@ frame_benchmarking::benchmarks! { } nominate { - let n in 1 .. T::MaxNominations::get(); + let n in 1 .. MaxNominationsOf::::get(); // Create a pool let min_create_bond = Pools::::depositor_min_bond() * 2u32.into(); @@ -679,7 +680,7 @@ frame_benchmarking::benchmarks! { let (depositor, pool_account) = create_pool_account::(0, Pools::::depositor_min_bond() * 2u32.into(), None); // Nominate with the pool. - let validators: Vec<_> = (0..T::MaxNominations::get()) + let validators: Vec<_> = (0..MaxNominationsOf::::get()) .map(|i| account("stash", USER_SEED, i)) .collect(); diff --git a/frame/nomination-pools/benchmarking/src/mock.rs b/frame/nomination-pools/benchmarking/src/mock.rs index 2298f611d7fff..2d75df63b518a 100644 --- a/frame/nomination-pools/benchmarking/src/mock.rs +++ b/frame/nomination-pools/benchmarking/src/mock.rs @@ -94,7 +94,6 @@ parameter_types! { pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS; } impl pallet_staking::Config for Runtime { - type MaxNominations = ConstU32<16>; type Currency = Balances; type CurrencyBalance = Balance; type UnixTime = pallet_timestamp::Pallet; @@ -117,6 +116,7 @@ impl pallet_staking::Config for Runtime { type GenesisElectionProvider = Self::ElectionProvider; type VoterList = VoterList; type TargetList = pallet_staking::UseValidatorsMap; + type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; type MaxUnlockingChunks = ConstU32<32>; type HistoryDepth = ConstU32<84>; type EventListeners = Pools; diff --git a/frame/nomination-pools/test-staking/src/mock.rs b/frame/nomination-pools/test-staking/src/mock.rs index ffc1ed56d08c4..02c253e62c018 100644 --- a/frame/nomination-pools/test-staking/src/mock.rs +++ b/frame/nomination-pools/test-staking/src/mock.rs @@ -108,7 +108,6 @@ parameter_types! { } impl pallet_staking::Config for Runtime { - type MaxNominations = ConstU32<16>; type Currency = Balances; type CurrencyBalance = Balance; type UnixTime = pallet_timestamp::Pallet; @@ -131,6 +130,7 @@ impl pallet_staking::Config for Runtime { type GenesisElectionProvider = Self::ElectionProvider; type VoterList = VoterList; type TargetList = pallet_staking::UseValidatorsMap; + type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; type MaxUnlockingChunks = ConstU32<32>; type HistoryDepth = ConstU32<84>; type EventListeners = Pools; diff --git a/frame/offences/benchmarking/src/lib.rs b/frame/offences/benchmarking/src/lib.rs index 92a87fb58b8c5..c190927b84bf1 100644 --- a/frame/offences/benchmarking/src/lib.rs +++ b/frame/offences/benchmarking/src/lib.rs @@ -50,8 +50,8 @@ use pallet_session::{ #[cfg(test)] use pallet_staking::Event as StakingEvent; use pallet_staking::{ - Config as StakingConfig, Exposure, IndividualExposure, Pallet as Staking, RewardDestination, - ValidatorPrefs, + Config as StakingConfig, Exposure, IndividualExposure, MaxNominationsOf, Pallet as Staking, + RewardDestination, ValidatorPrefs, }; const SEED: u32 = 0; @@ -283,7 +283,7 @@ benchmarks! { let r in 1 .. MAX_REPORTERS; // we skip 1 offender, because in such case there is no slashing let o in 2 .. MAX_OFFENDERS; - let n in 0 .. MAX_NOMINATORS.min(::MaxNominations::get()); + let n in 0 .. MAX_NOMINATORS.min(MaxNominationsOf::::get()); // Make r reporters let mut reporters = vec![]; @@ -399,7 +399,7 @@ benchmarks! { } report_offence_grandpa { - let n in 0 .. MAX_NOMINATORS.min(::MaxNominations::get()); + let n in 0 .. MAX_NOMINATORS.min(MaxNominationsOf::::get()); // for grandpa equivocation reports the number of reporters // and offenders is always 1 @@ -436,7 +436,7 @@ benchmarks! { } report_offence_babe { - let n in 0 .. MAX_NOMINATORS.min(::MaxNominations::get()); + let n in 0 .. MAX_NOMINATORS.min(MaxNominationsOf::::get()); // for babe equivocation reports the number of reporters // and offenders is always 1 diff --git a/frame/offences/benchmarking/src/mock.rs b/frame/offences/benchmarking/src/mock.rs index 0e3dfd09fe7ab..88f418dd3e2e8 100644 --- a/frame/offences/benchmarking/src/mock.rs +++ b/frame/offences/benchmarking/src/mock.rs @@ -20,7 +20,10 @@ #![cfg(test)] use super::*; -use frame_election_provider_support::{onchain, SequentialPhragmen}; +use frame_election_provider_support::{ + bounds::{ElectionBounds, ElectionBoundsBuilder}, + onchain, SequentialPhragmen, +}; use frame_support::{ parameter_types, traits::{ConstU32, ConstU64}, @@ -141,6 +144,7 @@ pallet_staking_reward_curve::build! { } parameter_types! { 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; @@ -152,12 +156,10 @@ impl onchain::Config for OnChainSeqPhragmen { type DataProvider = Staking; type WeightInfo = (); type MaxWinners = ConstU32<100>; - type VotersBound = ConstU32<{ u32::MAX }>; - type TargetsBound = ConstU32<{ u32::MAX }>; + type Bounds = ElectionsBounds; } impl pallet_staking::Config for Test { - type MaxNominations = ConstU32<16>; type Currency = Balances; type CurrencyBalance = ::Balance; type UnixTime = pallet_timestamp::Pallet; @@ -179,6 +181,7 @@ impl pallet_staking::Config for Test { type GenesisElectionProvider = Self::ElectionProvider; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; type TargetList = pallet_staking::UseValidatorsMap; + type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; type MaxUnlockingChunks = ConstU32<32>; type HistoryDepth = ConstU32<84>; type EventListeners = (); diff --git a/frame/root-offences/src/mock.rs b/frame/root-offences/src/mock.rs index 92d188f49fb92..2d2a5476149f6 100644 --- a/frame/root-offences/src/mock.rs +++ b/frame/root-offences/src/mock.rs @@ -18,7 +18,10 @@ use super::*; 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::{ parameter_types, 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; impl onchain::Config for OnChainSeqPhragmen { type System = Test; @@ -141,8 +148,7 @@ impl onchain::Config for OnChainSeqPhragmen { type DataProvider = Staking; type WeightInfo = (); type MaxWinners = ConstU32<100>; - type VotersBound = ConstU32<{ u32::MAX }>; - type TargetsBound = ConstU32<{ u32::MAX }>; + type Bounds = ElectionsBounds; } parameter_types! { @@ -157,7 +163,6 @@ parameter_types! { } impl pallet_staking::Config for Test { - type MaxNominations = ConstU32<16>; type Currency = Balances; type CurrencyBalance = ::Balance; type UnixTime = Timestamp; @@ -178,6 +183,7 @@ impl pallet_staking::Config for Test { type ElectionProvider = onchain::OnChainExecution; type GenesisElectionProvider = Self::ElectionProvider; type TargetList = pallet_staking::UseValidatorsMap; + type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; type MaxUnlockingChunks = ConstU32<32>; type HistoryDepth = ConstU32<84>; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; diff --git a/frame/session/benchmarking/src/lib.rs b/frame/session/benchmarking/src/lib.rs index f27136e820d41..cbf5d67ba567c 100644 --- a/frame/session/benchmarking/src/lib.rs +++ b/frame/session/benchmarking/src/lib.rs @@ -35,7 +35,7 @@ use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin}; use pallet_session::{historical::Pallet as Historical, Pallet as Session, *}; use pallet_staking::{ benchmarking::create_validator_with_nominators, testing_utils::create_validators, - RewardDestination, + MaxNominationsOf, RewardDestination, }; const MAX_VALIDATORS: u32 = 1000; @@ -54,10 +54,10 @@ impl OnInitialize> for Pallet { benchmarks! { set_keys { - let n = ::MaxNominations::get(); + let n = MaxNominationsOf::::get(); let (v_stash, _) = create_validator_with_nominators::( n, - ::MaxNominations::get(), + MaxNominationsOf::::get(), false, true, RewardDestination::Staked, @@ -72,10 +72,10 @@ benchmarks! { }: _(RawOrigin::Signed(v_controller), keys, proof) purge_keys { - let n = ::MaxNominations::get(); + let n = MaxNominationsOf::::get(); let (v_stash, _) = create_validator_with_nominators::( n, - ::MaxNominations::get(), + MaxNominationsOf::::get(), false, true, RewardDestination::Staked, diff --git a/frame/session/benchmarking/src/mock.rs b/frame/session/benchmarking/src/mock.rs index dbbd437bbd938..24a821ac87af5 100644 --- a/frame/session/benchmarking/src/mock.rs +++ b/frame/session/benchmarking/src/mock.rs @@ -19,7 +19,10 @@ #![cfg(test)] -use frame_election_provider_support::{onchain, SequentialPhragmen}; +use frame_election_provider_support::{ + bounds::{ElectionBounds, ElectionBoundsBuilder}, + onchain, SequentialPhragmen, +}; use frame_support::{ parameter_types, traits::{ConstU32, ConstU64}, @@ -140,6 +143,7 @@ pallet_staking_reward_curve::build! { } parameter_types! { pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS; + pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build(); } pub struct OnChainSeqPhragmen; @@ -149,12 +153,10 @@ impl onchain::Config for OnChainSeqPhragmen { type DataProvider = Staking; type WeightInfo = (); type MaxWinners = ConstU32<100>; - type VotersBound = ConstU32<{ u32::MAX }>; - type TargetsBound = ConstU32<{ u32::MAX }>; + type Bounds = ElectionsBounds; } impl pallet_staking::Config for Test { - type MaxNominations = ConstU32<16>; type Currency = Balances; type CurrencyBalance = ::Balance; type UnixTime = pallet_timestamp::Pallet; @@ -178,6 +180,7 @@ impl pallet_staking::Config for Test { type HistoryDepth = ConstU32<84>; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; type TargetList = pallet_staking::UseValidatorsMap; + type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); diff --git a/frame/staking/src/benchmarking.rs b/frame/staking/src/benchmarking.rs index 8d6eb7459074c..e72a9baf044fe 100644 --- a/frame/staking/src/benchmarking.rs +++ b/frame/staking/src/benchmarking.rs @@ -22,7 +22,7 @@ use crate::{ConfigOp, Pallet as Staking}; use testing_utils::*; use codec::Decode; -use frame_election_provider_support::SortedListProvider; +use frame_election_provider_support::{bounds::DataProviderBounds, SortedListProvider}; use frame_support::{ dispatch::UnfilteredDispatchable, pallet_prelude::*, @@ -338,7 +338,7 @@ benchmarks! { validate { let (stash, controller) = create_stash_controller::( - T::MaxNominations::get() - 1, + MaxNominationsOf::::get() - 1, 100, Default::default(), )?; @@ -362,11 +362,11 @@ benchmarks! { // these are the other validators; there are `T::MaxNominations::get() - 1` of them, so // there are a total of `T::MaxNominations::get()` validators in the system. - let rest_of_validators = create_validators_with_seed::(T::MaxNominations::get() - 1, 100, 415)?; + let rest_of_validators = create_validators_with_seed::(MaxNominationsOf::::get() - 1, 100, 415)?; // this is the validator that will be kicking. let (stash, controller) = create_stash_controller::( - T::MaxNominations::get() - 1, + MaxNominationsOf::::get() - 1, 100, Default::default(), )?; @@ -381,7 +381,7 @@ benchmarks! { for i in 0 .. k { // create a nominator stash. let (n_stash, n_controller) = create_stash_controller::( - T::MaxNominations::get() + i, + MaxNominationsOf::::get() + i, 100, Default::default(), )?; @@ -418,7 +418,7 @@ benchmarks! { // Worst case scenario, T::MaxNominations::get() nominate { - let n in 1 .. T::MaxNominations::get(); + let n in 1 .. MaxNominationsOf::::get(); // clean up any existing state. clear_validators_and_nominators::(); @@ -429,7 +429,7 @@ benchmarks! { // we are just doing an insert into the origin position. let scenario = ListScenario::::new(origin_weight, true)?; let (stash, controller) = create_stash_controller_with_balance::( - SEED + T::MaxNominations::get() + 1, // make sure the account does not conflict with others + SEED + MaxNominationsOf::::get() + 1, // make sure the account does not conflict with others origin_weight, Default::default(), ).unwrap(); @@ -711,7 +711,7 @@ benchmarks! { create_validators_with_nominators_for_era::( v, n, - ::MaxNominations::get() as usize, + MaxNominationsOf::::get() as usize, false, None, )?; @@ -729,7 +729,7 @@ benchmarks! { create_validators_with_nominators_for_era::( v, n, - ::MaxNominations::get() as usize, + MaxNominationsOf::::get() as usize, false, None, )?; @@ -808,7 +808,7 @@ benchmarks! { let n in (MaxNominators::::get() / 2) .. MaxNominators::::get(); let validators = create_validators_with_nominators_for_era::( - v, n, T::MaxNominations::get() as usize, false, None + v, n, MaxNominationsOf::::get() as usize, false, None )? .into_iter() .map(|v| T::Lookup::lookup(v).unwrap()) @@ -819,7 +819,8 @@ benchmarks! { let num_voters = (v + n) as usize; }: { - let voters = >::get_npos_voters(None); + // default bounds are unbounded. + let voters = >::get_npos_voters(DataProviderBounds::default()); assert_eq!(voters.len(), num_voters); } @@ -830,10 +831,11 @@ benchmarks! { let n = MaxNominators::::get(); let _ = create_validators_with_nominators_for_era::( - v, n, T::MaxNominations::get() as usize, false, None + v, n, MaxNominationsOf::::get() as usize, false, None )?; }: { - let targets = >::get_npos_targets(None); + // default bounds are unbounded. + let targets = >::get_npos_targets(DataProviderBounds::default()); assert_eq!(targets.len() as u32, v); } @@ -961,7 +963,7 @@ mod tests { create_validators_with_nominators_for_era::( v, n, - ::MaxNominations::get() as usize, + MaxNominationsOf::::get() as usize, false, None, ) diff --git a/frame/staking/src/election_size_tracker.rs b/frame/staking/src/election_size_tracker.rs new file mode 100644 index 0000000000000..283ae0140ee68 --- /dev/null +++ b/frame/staking/src/election_size_tracker.rs @@ -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::::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::::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 { + pub size: usize, + pub counter: usize, + _marker: sp_std::marker::PhantomData, +} + +impl Default for StaticTracker { + fn default() -> Self { + Self { size: 0, counter: 0, _marker: Default::default() } + } +} + +impl StaticTracker +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, + 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) -> 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::::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>; + + #[test] + pub fn election_size_tracker_works() { + let mut voters: Vec<(u64, u64, Voters)> = vec![]; + let mut size_tracker = StaticTracker::::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::::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::::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::::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::::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::::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::::length_prefix(s) + s); + } + } +} diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs index 8f1513f5065aa..e59b2a3324a62 100644 --- a/frame/staking/src/lib.rs +++ b/frame/staking/src/lib.rs @@ -292,6 +292,7 @@ pub(crate) mod mock; #[cfg(test)] mod tests; +pub mod election_size_tracker; pub mod inflation; pub mod migrations; pub mod slashing; @@ -301,7 +302,7 @@ mod pallet; use codec::{Decode, Encode, HasCompact, MaxEncodedLen}; use frame_support::{ - traits::{Currency, Defensive, Get}, + traits::{ConstU32, Currency, Defensive, Get}, weights::Weight, BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; @@ -338,6 +339,10 @@ macro_rules! log { /// pallet. pub type MaxWinnersOf = <::ElectionProvider as frame_election_provider_support::ElectionProviderBase>::MaxWinners; +/// Maximum number of nominations per nominator. +pub type MaxNominationsOf = + <::NominationsQuota as NominationsQuota>>::MaxNominations; + /// Counter for the number of "reward" points earned by a given validator. pub type RewardPoint = u32; @@ -679,7 +684,7 @@ impl StakingLedger { #[scale_info(skip_type_params(T))] pub struct Nominations { /// The targets of nomination. - pub targets: BoundedVec, + pub targets: BoundedVec>, /// The era the nominations were submitted. /// /// Except for initial nominations which are considered submitted at era 0. @@ -749,6 +754,36 @@ impl UnappliedSlash { } } +/// 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 { + /// 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; + + /// 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; +impl NominationsQuota for FixedNominationsQuota { + type MaxNominations = ConstU32; + + fn curve(_: Balance) -> u32 { + MAX + } +} + /// Means for interacting with a specialized version of the `session` trait. /// /// This is needed because `Staking` sets the `ValidatorIdOf` of the `pallet_session::Config` diff --git a/frame/staking/src/mock.rs b/frame/staking/src/mock.rs index 232b37de7c1b8..cf08f8be1f27d 100644 --- a/frame/staking/src/mock.rs +++ b/frame/staking/src/mock.rs @@ -18,7 +18,10 @@ //! Test utilities use crate::{self as pallet_staking, *}; -use frame_election_provider_support::{onchain, SequentialPhragmen, VoteWeight}; +use frame_election_provider_support::{ + bounds::{ElectionBounds, ElectionBoundsBuilder}, + onchain, SequentialPhragmen, VoteWeight, +}; use frame_support::{ assert_ok, ord_parameter_types, parameter_types, traits::{ @@ -228,11 +231,12 @@ const THRESHOLDS: [sp_npos_elections::VoteWeight; 9] = parameter_types! { pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS; - pub static MaxNominations: u32 = 16; pub static HistoryDepth: u32 = 80; pub static MaxUnlockingChunks: u32 = 32; pub static RewardOnUnbalanceWasCalled: bool = false; pub static MaxWinners: u32 = 100; + pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build(); + pub static AbsoluteMaxNominations: u32 = 16; } type VoterBagsListInstance = pallet_bags_list::Instance1; @@ -252,8 +256,7 @@ impl onchain::Config for OnChainSeqPhragmen { type DataProvider = Staking; type WeightInfo = (); type MaxWinners = MaxWinners; - type VotersBound = ConstU32<{ u32::MAX }>; - type TargetsBound = ConstU32<{ u32::MAX }>; + type Bounds = ElectionsBounds; } pub struct MockReward {} @@ -281,7 +284,6 @@ impl OnStakingUpdate for EventListenerMock { } impl crate::pallet::pallet::Config for Test { - type MaxNominations = MaxNominations; type Currency = Balances; type CurrencyBalance = ::Balance; type UnixTime = Timestamp; @@ -304,6 +306,7 @@ impl crate::pallet::pallet::Config for Test { // NOTE: consider a macro and use `UseNominatorsAndValidatorsMap` as well. type VoterList = VoterBagsList; type TargetList = UseValidatorsMap; + type NominationsQuota = WeightedNominationsQuota<16>; type MaxUnlockingChunks = MaxUnlockingChunks; type HistoryDepth = HistoryDepth; type EventListeners = EventListenerMock; @@ -311,6 +314,25 @@ impl crate::pallet::pallet::Config for Test { type WeightInfo = (); } +pub struct WeightedNominationsQuota; +impl NominationsQuota for WeightedNominationsQuota +where + u128: From, +{ + 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; pub(crate) type TestCall = ::RuntimeCall; diff --git a/frame/staking/src/pallet/impls.rs b/frame/staking/src/pallet/impls.rs index 052ba801618c2..e0f5c95587818 100644 --- a/frame/staking/src/pallet/impls.rs +++ b/frame/staking/src/pallet/impls.rs @@ -18,8 +18,9 @@ //! Implementations for the Staking FRAME Pallet. use frame_election_provider_support::{ - data_provider, BoundedSupportsOf, ElectionDataProvider, ElectionProvider, ScoreProvider, - SortedListProvider, VoteWeight, VoterOf, + bounds::{CountBound, SizeBound}, + data_provider, BoundedSupportsOf, DataProviderBounds, ElectionDataProvider, ElectionProvider, + ScoreProvider, SortedListProvider, VoteWeight, VoterOf, }; use frame_support::{ defensive, @@ -45,8 +46,9 @@ use sp_staking::{ use sp_std::prelude::*; use crate::{ - log, slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, EraPayout, Exposure, ExposureOf, - Forcing, IndividualExposure, MaxWinnersOf, Nominations, PositiveImbalanceOf, RewardDestination, + election_size_tracker::StaticTracker, log, slashing, weights::WeightInfo, ActiveEraInfo, + BalanceOf, EraPayout, Exposure, ExposureOf, Forcing, IndividualExposure, MaxNominationsOf, + MaxWinnersOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, StakingLedger, ValidatorPrefs, }; @@ -761,13 +763,15 @@ impl Pallet { /// nominators. /// /// This function is self-weighing as [`DispatchClass::Mandatory`]. - pub fn get_npos_voters(maybe_max_len: Option) -> Vec> { - let max_allowed_len = { - let all_voter_count = T::VoterList::count() as usize; - maybe_max_len.unwrap_or(all_voter_count).min(all_voter_count) + pub fn get_npos_voters(bounds: DataProviderBounds) -> Vec> { + let mut voters_size_tracker: StaticTracker = StaticTracker::default(); + + let final_predicted_len = { + let all_voter_count = T::VoterList::count(); + bounds.count.unwrap_or(all_voter_count.into()).min(all_voter_count.into()).0 }; - let mut all_voters = Vec::<_>::with_capacity(max_allowed_len); + let mut all_voters = Vec::<_>::with_capacity(final_predicted_len as usize); // cache a few things. let weight_of = Self::weight_of_fn(); @@ -778,8 +782,8 @@ impl Pallet { let mut min_active_stake = u64::MAX; let mut sorted_voters = T::VoterList::iter(); - while all_voters.len() < max_allowed_len && - voters_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * max_allowed_len as u32) + while all_voters.len() < final_predicted_len as usize && + voters_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32) { let voter = match sorted_voters.next() { Some(voter) => { @@ -798,10 +802,23 @@ impl Pallet { if let Some(Nominations { targets, .. }) = >::get(&voter) { if !targets.is_empty() { - all_voters.push((voter.clone(), voter_weight, targets)); + // Note on lazy nomination quota: we do not check the nomination quota of the + // voter at this point and accept all the current nominations. The nomination + // quota is only enforced at `nominate` time. + + let voter = (voter, voter_weight, targets); + if voters_size_tracker.try_register_voter(&voter, &bounds).is_err() { + // no more space left for the election result, stop iterating. + Self::deposit_event(Event::::SnapshotVotersSizeExceeded { + size: voters_size_tracker.size as u32, + }); + break + } + + all_voters.push(voter); nominators_taken.saturating_inc(); } else { - // Technically should never happen, but not much we can do about it. + // technically should never happen, but not much we can do about it. } min_active_stake = if voter_weight < min_active_stake { voter_weight } else { min_active_stake }; @@ -814,24 +831,31 @@ impl Pallet { .try_into() .expect("`MaxVotesPerVoter` must be greater than or equal to 1"), ); + + if voters_size_tracker.try_register_voter(&self_vote, &bounds).is_err() { + // no more space left for the election snapshot, stop iterating. + Self::deposit_event(Event::::SnapshotVotersSizeExceeded { + size: voters_size_tracker.size as u32, + }); + break + } all_voters.push(self_vote); validators_taken.saturating_inc(); } else { // this can only happen if: 1. there a bug in the bags-list (or whatever is the // sorted list) logic and the state of the two pallets is no longer compatible, or // because the nominators is not decodable since they have more nomination than - // `T::MaxNominations`. The latter can rarely happen, and is not really an emergency - // or bug if it does. - log!( - warn, - "DEFENSIVE: invalid item in `VoterList`: {:?}, this nominator probably has too many nominations now", - voter - ); + // `T::NominationsQuota::get_quota`. The latter can rarely happen, and is not + // really an emergency or bug if it does. + defensive!( + "DEFENSIVE: invalid item in `VoterList`: {:?}, this nominator probably has too many nominations now", + voter, + ); } } // all_voters should have not re-allocated. - debug_assert!(all_voters.capacity() == max_allowed_len); + debug_assert!(all_voters.capacity() == final_predicted_len as usize); Self::register_weight(T::WeightInfo::get_npos_voters(validators_taken, nominators_taken)); @@ -854,14 +878,20 @@ impl Pallet { /// Get the targets for an upcoming npos election. /// /// This function is self-weighing as [`DispatchClass::Mandatory`]. - pub fn get_npos_targets(maybe_max_len: Option) -> Vec { - let max_allowed_len = maybe_max_len.unwrap_or_else(|| T::TargetList::count() as usize); - let mut all_targets = Vec::::with_capacity(max_allowed_len); + pub fn get_npos_targets(bounds: DataProviderBounds) -> Vec { + let mut targets_size_tracker: StaticTracker = StaticTracker::default(); + + let final_predicted_len = { + let all_target_count = T::TargetList::count(); + bounds.count.unwrap_or(all_target_count.into()).min(all_target_count.into()).0 + }; + + let mut all_targets = Vec::::with_capacity(final_predicted_len as usize); let mut targets_seen = 0; let mut targets_iter = T::TargetList::iter(); - while all_targets.len() < max_allowed_len && - targets_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * max_allowed_len as u32) + while all_targets.len() < final_predicted_len as usize && + targets_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32) { let target = match targets_iter.next() { Some(target) => { @@ -871,6 +901,14 @@ impl Pallet { 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::::SnapshotTargetsSizeExceeded { + size: targets_size_tracker.size as u32, + }); + break + } + if Validators::::contains_key(&target) { all_targets.push(target); } @@ -989,43 +1027,48 @@ impl Pallet { /// Returns the current nominations quota for nominators. /// /// Used by the runtime API. - /// Note: for now, this api runtime will always return value of `T::MaxNominations` and thus it - /// is redundant. However, with the upcoming changes in - /// , 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) -> u32 { - T::MaxNominations::get() + pub fn api_nominations_quota(balance: BalanceOf) -> u32 { + T::NominationsQuota::get_quota(balance) } } impl ElectionDataProvider for Pallet { type AccountId = T::AccountId; type BlockNumber = BlockNumberFor; - type MaxVotesPerVoter = T::MaxNominations; + type MaxVotesPerVoter = MaxNominationsOf; fn desired_targets() -> data_provider::Result { Self::register_weight(T::DbWeight::get().reads(1)); Ok(Self::validator_count()) } - fn electing_voters(maybe_max_len: Option) -> data_provider::Result>> { + fn electing_voters(bounds: DataProviderBounds) -> data_provider::Result>> { // This can never fail -- if `maybe_max_len` is `Some(_)` we handle it. - let voters = Self::get_npos_voters(maybe_max_len); - debug_assert!(maybe_max_len.map_or(true, |max| voters.len() <= max)); + let voters = Self::get_npos_voters(bounds); + + debug_assert!(!bounds.exhausted( + SizeBound(voters.encoded_size() as u32).into(), + CountBound(voters.len() as u32).into() + )); Ok(voters) } - fn electable_targets(maybe_max_len: Option) -> data_provider::Result> { - let target_count = T::TargetList::count(); + fn electable_targets(bounds: DataProviderBounds) -> data_provider::Result> { + let targets = Self::get_npos_targets(bounds); - // We can't handle this case yet -- return an error. - if maybe_max_len.map_or(false, |max_len| target_count > max_len as u32) { + // We can't handle this case yet -- return an error. WIP to improve handling this case in + // . + if bounds.exhausted(None, CountBound(T::TargetList::count() as u32).into()) { return Err("Target snapshot too big") } - Ok(Self::get_npos_targets(None)) + debug_assert!(!bounds.exhausted( + SizeBound(targets.encoded_size() as u32).into(), + CountBound(targets.len() as u32).into() + )); + + Ok(targets) } fn next_election_prediction(now: BlockNumberFor) -> BlockNumberFor { diff --git a/frame/staking/src/pallet/mod.rs b/frame/staking/src/pallet/mod.rs index ec9805b333ecf..40a2f5cf73eb1 100644 --- a/frame/staking/src/pallet/mod.rs +++ b/frame/staking/src/pallet/mod.rs @@ -45,9 +45,9 @@ pub use impls::*; use crate::{ slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout, - EraRewardPoints, Exposure, Forcing, NegativeImbalanceOf, Nominations, PositiveImbalanceOf, - RewardDestination, SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk, - ValidatorPrefs, + EraRewardPoints, Exposure, Forcing, MaxNominationsOf, NegativeImbalanceOf, Nominations, + NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, StakingLedger, + UnappliedSlash, UnlockChunk, ValidatorPrefs, }; const STAKING_ID: LockIdentifier = *b"staking "; @@ -129,9 +129,8 @@ pub mod pallet { DataProvider = Pallet, >; - /// Maximum number of nominations per nominator. - #[pallet::constant] - type MaxNominations: Get; + /// Something that defines the maximum number of nominations per nominator. + type NominationsQuota: NominationsQuota>; /// Number of eras to keep in history. /// @@ -348,7 +347,8 @@ pub mod pallet { /// they wish to support. /// /// Note that the keys of this storage map might become non-decodable in case the - /// [`Config::MaxNominations`] configuration is decreased. In this rare case, these nominators + /// account's [`NominationsQuota::MaxNominations`] configuration is decreased. + /// In this rare case, these nominators /// are still existent in storage, their key is correct and retrievable (i.e. `contains_key` /// indicates that they exist), but their value cannot be decoded. Therefore, the non-decodable /// nominators will effectively not-exist, until they re-submit their preferences such that it @@ -696,6 +696,10 @@ pub mod pallet { PayoutStarted { era_index: EraIndex, validator_stash: T::AccountId }, /// A validator has set their preferences. ValidatorPrefsSet { stash: T::AccountId, prefs: ValidatorPrefs }, + /// Voters size limit reached. + SnapshotVotersSizeExceeded { size: u32 }, + /// Targets size limit reached. + SnapshotTargetsSizeExceeded { size: u32 }, /// A new force era mode was set. ForceEra { mode: Forcing }, } @@ -782,11 +786,11 @@ pub mod pallet { fn integrity_test() { // ensure that we funnel the correct value to the `DataProvider::MaxVotesPerVoter`; assert_eq!( - T::MaxNominations::get(), + MaxNominationsOf::::get(), ::MaxVotesPerVoter::get() ); // and that MaxNominations is always greater than 1, since we count on this. - assert!(!T::MaxNominations::get().is_zero()); + assert!(!MaxNominationsOf::::get().is_zero()); // ensure election results are always bounded with the same value assert!( @@ -1145,7 +1149,10 @@ pub mod pallet { } ensure!(!targets.is_empty(), Error::::EmptyTargets); - ensure!(targets.len() <= T::MaxNominations::get() as usize, Error::::TooManyTargets); + ensure!( + targets.len() <= T::NominationsQuota::get_quota(ledger.active) as usize, + Error::::TooManyTargets + ); let old = Nominators::::get(stash).map_or_else(Vec::new, |x| x.targets.into_inner()); diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs index 124e1c3fda910..29539cbb84cf7 100644 --- a/frame/staking/src/tests.rs +++ b/frame/staking/src/tests.rs @@ -18,7 +18,10 @@ //! Tests for the module. use super::{ConfigOp, Event, *}; -use frame_election_provider_support::{ElectionProvider, SortedListProvider, Support}; +use frame_election_provider_support::{ + bounds::{DataProviderBounds, ElectionBoundsBuilder}, + ElectionProvider, SortedListProvider, Support, +}; use frame_support::{ assert_noop, assert_ok, assert_storage_noop, bounded_vec, dispatch::{extract_actual_weight, GetDispatchInfo, WithPostDispatchInfo}, @@ -4508,12 +4511,16 @@ mod election_data_provider { .add_staker(71, 71, 10, StakerStatus::::Nominator(vec![21])) .add_staker(81, 81, 50, StakerStatus::::Nominator(vec![21])) .build_and_execute(|| { - assert_ok!(::electing_voters(None)); + // default bounds are unbounded. + assert_ok!(::electing_voters( + DataProviderBounds::default() + )); assert_eq!(MinimumActiveStake::::get(), 10); // remove staker with lower bond by limiting the number of voters and check // `MinimumActiveStake` again after electing voters. - assert_ok!(::electing_voters(Some(5))); + let bounds = ElectionBoundsBuilder::default().voters_count(5.into()).build(); + assert_ok!(::electing_voters(bounds.voters)); assert_eq!(MinimumActiveStake::::get(), 50); }); } @@ -4522,8 +4529,11 @@ mod election_data_provider { fn set_minimum_active_stake_lower_bond_works() { // if there are no voters, minimum active stake is zero (should not happen). ExtBuilder::default().has_stakers(false).build_and_execute(|| { + // default bounds are unbounded. + assert_ok!(::electing_voters( + DataProviderBounds::default() + )); assert_eq!(::VoterList::count(), 0); - assert_ok!(::electing_voters(None)); assert_eq!(MinimumActiveStake::::get(), 0); }); @@ -4537,7 +4547,9 @@ mod election_data_provider { assert_ok!(Staking::nominate(RuntimeOrigin::signed(4), vec![1])); assert_eq!(::VoterList::count(), 5); - let voters_before = ::electing_voters(None).unwrap(); + let voters_before = + ::electing_voters(DataProviderBounds::default()) + .unwrap(); assert_eq!(MinimumActiveStake::::get(), 5); // update minimum nominator bond. @@ -4547,7 +4559,9 @@ mod election_data_provider { // lower than `MinNominatorBond`. assert_eq!(::VoterList::count(), 5); - let voters = ::electing_voters(None).unwrap(); + let voters = + ::electing_voters(DataProviderBounds::default()) + .unwrap(); assert_eq!(voters_before, voters); // minimum active stake is lower than `MinNominatorBond`. @@ -4563,7 +4577,10 @@ mod election_data_provider { .add_staker(61, 61, 2_000, StakerStatus::::Nominator(vec![21])) .build_and_execute(|| { assert_eq!(Staking::weight_of(&101), 500); - let voters = ::electing_voters(None).unwrap(); + let voters = ::electing_voters( + DataProviderBounds::default(), + ) + .unwrap(); assert_eq!(voters.len(), 5); assert_eq!(MinimumActiveStake::::get(), 500); @@ -4575,7 +4592,10 @@ mod election_data_provider { // corrupt ledger state by lowering max unlocking chunks bounds. MaxUnlockingChunks::set(1); - let voters = ::electing_voters(None).unwrap(); + let voters = ::electing_voters( + DataProviderBounds::default(), + ) + .unwrap(); // number of returned voters decreases since ledger entry of stash 101 is now // corrupt. assert_eq!(voters.len(), 4); @@ -4593,8 +4613,9 @@ mod election_data_provider { #[test] fn voters_include_self_vote() { ExtBuilder::default().nominate(false).build_and_execute(|| { + // default bounds are unbounded. assert!(>::iter().map(|(x, _)| x).all(|v| Staking::electing_voters( - None + DataProviderBounds::default() ) .unwrap() .into_iter() @@ -4602,39 +4623,11 @@ mod election_data_provider { }) } - #[test] - fn respects_snapshot_len_limits() { - ExtBuilder::default() - .set_status(41, StakerStatus::Validator) - .build_and_execute(|| { - // sum of all nominators who'd be voters (1), plus the self-votes (4). - assert_eq!(::VoterList::count(), 5); - - // if limits is less.. - assert_eq!(Staking::electing_voters(Some(1)).unwrap().len(), 1); - - // if limit is equal.. - assert_eq!(Staking::electing_voters(Some(5)).unwrap().len(), 5); - - // if limit is more. - assert_eq!(Staking::electing_voters(Some(55)).unwrap().len(), 5); - - // if target limit is more.. - assert_eq!(Staking::electable_targets(Some(6)).unwrap().len(), 4); - assert_eq!(Staking::electable_targets(Some(4)).unwrap().len(), 4); - - // if target limit is less, then we return an error. - assert_eq!( - Staking::electable_targets(Some(1)).unwrap_err(), - "Target snapshot too big" - ); - }); - } - // Tests the criteria that in `ElectionDataProvider::voters` function, we try to get at most // `maybe_max_len` voters, and if some of them end up being skipped, we iterate at most `2 * // maybe_max_len`. #[test] + #[should_panic] fn only_iterates_max_2_times_max_allowed_len() { ExtBuilder::default() .nominate(false) @@ -4659,13 +4652,14 @@ mod election_data_provider { StakerStatus::::Nominator(vec![21, 22, 23, 24, 25]), ) .build_and_execute(|| { + let bounds_builder = ElectionBoundsBuilder::default(); // all voters ordered by stake, assert_eq!( ::VoterList::iter().collect::>(), vec![61, 71, 81, 11, 21, 31] ); - MaxNominations::set(2); + AbsoluteMaxNominations::set(2); // we want 2 voters now, and in maximum we allow 4 iterations. This is what happens: // 61 is pruned; @@ -4674,7 +4668,7 @@ mod election_data_provider { // 11 is taken; // we finish since the 2x limit is reached. assert_eq!( - Staking::electing_voters(Some(2)) + Staking::electing_voters(bounds_builder.voters_count(2.into()).build().voters) .unwrap() .iter() .map(|(stash, _, _)| stash) @@ -4685,6 +4679,189 @@ mod election_data_provider { }); } + #[test] + fn respects_snapshot_count_limits() { + ExtBuilder::default() + .set_status(41, StakerStatus::Validator) + .build_and_execute(|| { + // sum of all nominators who'd be voters (1), plus the self-votes (4). + assert_eq!(::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::::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::::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![(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::::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![(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![(11, 1), (21, 1), (31, 1), (71, 7)], + ); + }); + } + #[test] fn estimate_next_election_works() { ExtBuilder::default().session_per_era(5).period(5).build_and_execute(|| { @@ -5120,7 +5297,8 @@ fn min_commission_works() { } #[test] -fn change_of_max_nominations() { +#[should_panic] +fn change_of_absolute_max_nominations() { use frame_election_provider_support::ElectionDataProvider; ExtBuilder::default() .add_staker(61, 61, 10, StakerStatus::Nominator(vec![1])) @@ -5128,7 +5306,7 @@ fn change_of_max_nominations() { .balance_factor(10) .build_and_execute(|| { // pre-condition - assert_eq!(MaxNominations::get(), 16); + assert_eq!(AbsoluteMaxNominations::get(), 16); assert_eq!( Nominators::::iter() @@ -5136,11 +5314,15 @@ fn change_of_max_nominations() { .collect::>(), vec![(101, 2), (71, 3), (61, 1)] ); + + // default bounds are unbounded. + let bounds = DataProviderBounds::default(); + // 3 validators and 3 nominators - assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 3); + assert_eq!(Staking::electing_voters(bounds).unwrap().len(), 3 + 3); // abrupt change from 16 to 4, everyone should be fine. - MaxNominations::set(4); + AbsoluteMaxNominations::set(4); assert_eq!( Nominators::::iter() @@ -5148,10 +5330,10 @@ fn change_of_max_nominations() { .collect::>(), vec![(101, 2), (71, 3), (61, 1)] ); - assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 3); + assert_eq!(Staking::electing_voters(bounds).unwrap().len(), 3 + 3); // abrupt change from 4 to 3, everyone should be fine. - MaxNominations::set(3); + AbsoluteMaxNominations::set(3); assert_eq!( Nominators::::iter() @@ -5159,11 +5341,11 @@ fn change_of_max_nominations() { .collect::>(), vec![(101, 2), (71, 3), (61, 1)] ); - assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 3); + assert_eq!(Staking::electing_voters(bounds).unwrap().len(), 3 + 3); // abrupt change from 3 to 2, this should cause some nominators to be non-decodable, and // thus non-existent unless if they update. - MaxNominations::set(2); + AbsoluteMaxNominations::set(2); assert_eq!( Nominators::::iter() @@ -5176,12 +5358,12 @@ fn change_of_max_nominations() { // but its value cannot be decoded and default is returned. assert!(Nominators::::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::::contains_key(101)); // abrupt change from 2 to 1, this should cause some nominators to be non-decodable, and // thus non-existent unless if they update. - MaxNominations::set(1); + AbsoluteMaxNominations::set(1); assert_eq!( Nominators::::iter() @@ -5193,7 +5375,7 @@ fn change_of_max_nominations() { assert!(Nominators::::contains_key(61)); assert!(Nominators::::get(71).is_none()); assert!(Nominators::::get(61).is_some()); - assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 1); + assert_eq!(Staking::electing_voters(bounds).unwrap().len(), 3 + 1); // now one of them can revive themselves by re-nominating to a proper value. assert_ok!(Staking::nominate(RuntimeOrigin::signed(71), vec![1])); @@ -5213,6 +5395,42 @@ fn change_of_max_nominations() { }) } +#[test] +fn nomination_quota_max_changes_decoding() { + use frame_election_provider_support::ElectionDataProvider; + ExtBuilder::default() + .add_staker(60, 61, 10, StakerStatus::Nominator(vec![1])) + .add_staker(70, 71, 10, StakerStatus::Nominator(vec![1, 2, 3])) + .add_staker(30, 330, 10, StakerStatus::Nominator(vec![1, 2, 3, 4])) + .add_staker(50, 550, 10, StakerStatus::Nominator(vec![1, 2, 3, 4])) + .balance_factor(10) + .build_and_execute(|| { + // pre-condition. + assert_eq!(MaxNominationsOf::::get(), 16); + + let unbonded_election = DataProviderBounds::default(); + + assert_eq!( + Nominators::::iter() + .map(|(k, n)| (k, n.targets.len())) + .collect::>(), + 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::::get()); + assert_eq!(Staking::api_nominations_quota(333), MaxNominationsOf::::get()); + assert_eq!(Staking::api_nominations_quota(222), 2); + assert_eq!(Staking::api_nominations_quota(111), 1); + }) +} + mod sorted_list_provider { use super::*; use frame_election_provider_support::SortedListProvider; diff --git a/primitives/npos-elections/src/lib.rs b/primitives/npos-elections/src/lib.rs index 9239a2d90d309..0afe1ec5bb692 100644 --- a/primitives/npos-elections/src/lib.rs +++ b/primitives/npos-elections/src/lib.rs @@ -404,7 +404,7 @@ impl Voter { }) } - /// This voter's budget + /// This voter's budget. #[inline] pub fn budget(&self) -> ExtendedBalance { self.budget