Bound Election and Staking by MaxActiveValidators (#12436)

* bounding election provider with kian

* multi phase implement bounded election provider

* election provider blanket implementation

* staking compiles

* fix test for election provider support

* fmt

* fixing epmp tests, does not compile yet

* fix epmp tests

* fix staking tests

* fmt

* fix runtime tests

* fmt

* remove outdated wip tags

* add enum error

* sort and truncate supports

* comment

* error when unsupported number of election winners

* compiling wip after kian's suggestions

* fix TODOs

* remove,fix tags

* ensure validator count does not exceed maxwinners

* clean up

* some more clean up and todos

* handle too many winners

* rename parameter for mock

* todo

* add sort and truncate rule if there are too many winners

* fmt

* fail, not swallow emergency result bound not met

* remove too many winners resolution as it can be guaranteed to be bounded

* fix benchmark

* give MaxWinners more contextual name

* make ready solution generic over T

* kian feedback

* fix stuff

* Kian's way of solvign this

* comment fix

* fix compile

* remove use of BoundedExecution

* fmt

* comment out failing integrity test

* cap validator count increment to max winners

* dont panic

* add test for bad data provider

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

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

* fix namespace conflict and add test for onchain max winners less than desired targets

* defensive unwrap

* early convert to bounded vec

* fix syntax

* fmt

* fix doc

* fix rustdoc

* fmt

* fix maxwinner count for benchmarking

* add instant election for noelection

* fmt

* fix compile

* pr feedbacks

* always error at validator count exceeding max winners

* add useful error message

* pr comments

* import fix

* add checked_desired_targets

* fmt

* fmt

* fix rust doc

Co-authored-by: parity-processbot <>
Co-authored-by: kianenigma <kian@parity.io>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
This commit is contained in:
Ankan
2022-11-09 10:11:51 +01:00
committed by GitHub
parent 535c6f2e94
commit 657d99202c
21 changed files with 544 additions and 318 deletions
+4
View File
@@ -333,6 +333,10 @@ macro_rules! log {
};
}
/// Maximum number of winners (aka. active validators), as defined in the election provider of this
/// pallet.
pub type MaxWinnersOf<T> = <<T as Config>::ElectionProvider as frame_election_provider_support::ElectionProviderBase>::MaxWinners;
/// Counter for the number of "reward" points earned by a given validator.
pub type RewardPoint = u32;
+5 -1
View File
@@ -238,6 +238,7 @@ parameter_types! {
pub static MaxUnlockingChunks: u32 = 32;
pub static RewardOnUnbalanceWasCalled: bool = false;
pub static LedgerSlashPerEra: (BalanceOf<Test>, BTreeMap<EraIndex, BalanceOf<Test>>) = (Zero::zero(), BTreeMap::new());
pub static MaxWinners: u32 = 100;
}
type VoterBagsListInstance = pallet_bags_list::Instance1;
@@ -256,6 +257,9 @@ impl onchain::Config for OnChainSeqPhragmen {
type Solver = SequentialPhragmen<AccountId, Perbill>;
type DataProvider = Staking;
type WeightInfo = ();
type MaxWinners = MaxWinners;
type VotersBound = ConstU32<{ u32::MAX }>;
type TargetsBound = ConstU32<{ u32::MAX }>;
}
pub struct MockReward {}
@@ -295,7 +299,7 @@ impl crate::pallet::pallet::Config for Test {
type NextNewSession = Session;
type MaxNominatorRewardedPerValidator = ConstU32<64>;
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
type ElectionProvider = onchain::UnboundedExecution<OnChainSeqPhragmen>;
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type GenesisElectionProvider = Self::ElectionProvider;
// NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well.
type VoterList = VoterBagsList;
+54 -25
View File
@@ -18,15 +18,15 @@
//! Implementations for the Staking FRAME Pallet.
use frame_election_provider_support::{
data_provider, ElectionDataProvider, ElectionProvider, ElectionProviderBase, ScoreProvider,
SortedListProvider, Supports, VoteWeight, VoterOf,
data_provider, BoundedSupportsOf, ElectionDataProvider, ElectionProvider, ScoreProvider,
SortedListProvider, VoteWeight, VoterOf,
};
use frame_support::{
dispatch::WithPostDispatchInfo,
pallet_prelude::*,
traits::{
Currency, CurrencyToVote, Defensive, DefensiveResult, EstimateNextNewSession, Get,
Imbalance, LockableCurrency, OnUnbalanced, UnixTime, WithdrawReasons,
Imbalance, LockableCurrency, OnUnbalanced, TryCollect, UnixTime, WithdrawReasons,
},
weights::Weight,
};
@@ -44,7 +44,7 @@ use sp_std::{collections::btree_map::BTreeMap, prelude::*};
use crate::{
log, slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, EraPayout, Exposure, ExposureOf,
Forcing, IndividualExposure, Nominations, PositiveImbalanceOf, RewardDestination,
Forcing, IndividualExposure, MaxWinnersOf, Nominations, PositiveImbalanceOf, RewardDestination,
SessionInterface, StakingLedger, ValidatorPrefs,
};
@@ -267,7 +267,10 @@ impl<T: Config> Pallet<T> {
}
/// Plan a new session potentially trigger a new era.
fn new_session(session_index: SessionIndex, is_genesis: bool) -> Option<Vec<T::AccountId>> {
fn new_session(
session_index: SessionIndex,
is_genesis: bool,
) -> Option<BoundedVec<T::AccountId, MaxWinnersOf<T>>> {
if let Some(current_era) = Self::current_era() {
// Initial era has been set.
let current_era_start_session_index = Self::eras_start_session_index(current_era)
@@ -426,8 +429,11 @@ impl<T: Config> Pallet<T> {
/// Returns the new validator set.
pub fn trigger_new_era(
start_session_index: SessionIndex,
exposures: Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)>,
) -> Vec<T::AccountId> {
exposures: BoundedVec<
(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>),
MaxWinnersOf<T>,
>,
) -> BoundedVec<T::AccountId, MaxWinnersOf<T>> {
// Increment or set current era.
let new_planned_era = CurrentEra::<T>::mutate(|s| {
*s = Some(s.map(|s| s + 1).unwrap_or(0));
@@ -453,19 +459,26 @@ impl<T: Config> Pallet<T> {
pub(crate) fn try_trigger_new_era(
start_session_index: SessionIndex,
is_genesis: bool,
) -> Option<Vec<T::AccountId>> {
let election_result = if is_genesis {
T::GenesisElectionProvider::elect().map_err(|e| {
) -> Option<BoundedVec<T::AccountId, MaxWinnersOf<T>>> {
let election_result: BoundedVec<_, MaxWinnersOf<T>> = if is_genesis {
let result = <T::GenesisElectionProvider>::elect().map_err(|e| {
log!(warn, "genesis election provider failed due to {:?}", e);
Self::deposit_event(Event::StakingElectionFailed);
})
});
result
.ok()?
.into_inner()
.try_into()
// both bounds checked in integrity test to be equal
.defensive_unwrap_or_default()
} else {
T::ElectionProvider::elect().map_err(|e| {
let result = <T::ElectionProvider>::elect().map_err(|e| {
log!(warn, "election provider failed due to {:?}", e);
Self::deposit_event(Event::StakingElectionFailed);
})
}
.ok()?;
});
result.ok()?
};
let exposures = Self::collect_exposures(election_result);
if (exposures.len() as u32) < Self::minimum_validator_count().max(1) {
@@ -502,10 +515,19 @@ impl<T: Config> Pallet<T> {
///
/// Store staking information for the new planned era
pub fn store_stakers_info(
exposures: Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)>,
exposures: BoundedVec<
(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>),
MaxWinnersOf<T>,
>,
new_planned_era: EraIndex,
) -> Vec<T::AccountId> {
let elected_stashes = exposures.iter().cloned().map(|(x, _)| x).collect::<Vec<_>>();
) -> BoundedVec<T::AccountId, MaxWinnersOf<T>> {
let elected_stashes: BoundedVec<_, MaxWinnersOf<T>> = exposures
.iter()
.cloned()
.map(|(x, _)| x)
.collect::<Vec<_>>()
.try_into()
.expect("since we only map through exposures, size of elected_stashes is always same as exposures; qed");
// Populate stakers, exposures, and the snapshot of validator prefs.
let mut total_stake: BalanceOf<T> = Zero::zero();
@@ -543,11 +565,11 @@ impl<T: Config> Pallet<T> {
elected_stashes
}
/// Consume a set of [`Supports`] from [`sp_npos_elections`] and collect them into a
/// Consume a set of [`BoundedSupports`] from [`sp_npos_elections`] and collect them into a
/// [`Exposure`].
fn collect_exposures(
supports: Supports<T::AccountId>,
) -> Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)> {
supports: BoundedSupportsOf<T::ElectionProvider>,
) -> BoundedVec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>), MaxWinnersOf<T>> {
let total_issuance = T::Currency::total_issuance();
let to_currency = |e: frame_election_provider_support::ExtendedBalance| {
T::CurrencyToVote::to_currency(e, total_issuance)
@@ -576,7 +598,8 @@ impl<T: Config> Pallet<T> {
let exposure = Exposure { own, others, total };
(validator, exposure)
})
.collect::<Vec<(T::AccountId, Exposure<_, _>)>>()
.try_collect()
.expect("we only map through support vector which cannot change the size; qed")
}
/// Remove all associated data of a stash account from the staking system.
@@ -1085,12 +1108,12 @@ impl<T: Config> pallet_session::SessionManager<T::AccountId> for Pallet<T> {
fn new_session(new_index: SessionIndex) -> Option<Vec<T::AccountId>> {
log!(trace, "planning new session {}", new_index);
CurrentPlannedSession::<T>::put(new_index);
Self::new_session(new_index, false)
Self::new_session(new_index, false).map(|v| v.into_inner())
}
fn new_session_genesis(new_index: SessionIndex) -> Option<Vec<T::AccountId>> {
log!(trace, "planning new session {} at genesis", new_index);
CurrentPlannedSession::<T>::put(new_index);
Self::new_session(new_index, true)
Self::new_session(new_index, true).map(|v| v.into_inner())
}
fn start_session(start_index: SessionIndex) {
log!(trace, "starting session {}", start_index);
@@ -1500,7 +1523,7 @@ impl<T: Config> StakingInterface for Pallet<T> {
}
fn election_ongoing() -> bool {
<T::ElectionProvider as ElectionProviderBase>::ongoing()
T::ElectionProvider::ongoing()
}
fn force_unstake(who: Self::AccountId) -> sp_runtime::DispatchResult {
@@ -1626,6 +1649,12 @@ impl<T: Config> Pallet<T> {
Nominators::<T>::count() + Validators::<T>::count(),
"wrong external count"
);
ensure!(
ValidatorCount::<T>::get() <=
<T::ElectionProvider as frame_election_provider_support::ElectionProviderBase>::MaxWinners::get(),
"validator count exceeded election max winners"
);
Ok(())
}
+45 -10
View File
@@ -17,7 +17,9 @@
//! Staking FRAME Pallet.
use frame_election_provider_support::{SortedListProvider, VoteWeight};
use frame_election_provider_support::{
ElectionProvider, ElectionProviderBase, SortedListProvider, VoteWeight,
};
use frame_support::{
dispatch::Codec,
pallet_prelude::*,
@@ -32,7 +34,7 @@ use frame_support::{
use frame_system::{ensure_root, ensure_signed, pallet_prelude::*};
use sp_runtime::{
traits::{CheckedSub, SaturatedConversion, StaticLookup, Zero},
Perbill, Percent,
ArithmeticError, Perbill, Percent,
};
use sp_staking::{EraIndex, SessionIndex};
use sp_std::prelude::*;
@@ -107,7 +109,7 @@ pub mod pallet {
type CurrencyToVote: CurrencyToVote<BalanceOf<Self>>;
/// Something that provides the election functionality.
type ElectionProvider: frame_election_provider_support::ElectionProvider<
type ElectionProvider: ElectionProvider<
AccountId = Self::AccountId,
BlockNumber = Self::BlockNumber,
// we only accept an election provider that has staking as data provider.
@@ -115,7 +117,7 @@ pub mod pallet {
>;
/// Something that provides the election functionality at genesis.
type GenesisElectionProvider: frame_election_provider_support::ElectionProvider<
type GenesisElectionProvider: ElectionProvider<
AccountId = Self::AccountId,
BlockNumber = Self::BlockNumber,
DataProvider = Pallet<Self>,
@@ -646,6 +648,10 @@ pub mod pallet {
),
_ => Ok(()),
});
assert!(
ValidatorCount::<T>::get() <=
<T::ElectionProvider as ElectionProviderBase>::MaxWinners::get()
);
}
// all voters are reported to the `VoterList`.
@@ -743,8 +749,8 @@ pub mod pallet {
/// There are too many nominators in the system. Governance needs to adjust the staking
/// settings to keep things safe for the runtime.
TooManyNominators,
/// There are too many validators in the system. Governance needs to adjust the staking
/// settings to keep things safe for the runtime.
/// There are too many validator candidates in the system. Governance needs to adjust the
/// staking settings to keep things safe for the runtime.
TooManyValidators,
/// Commission is too low. Must be at least `MinCommission`.
CommissionTooLow,
@@ -782,6 +788,12 @@ pub mod pallet {
// and that MaxNominations is always greater than 1, since we count on this.
assert!(!T::MaxNominations::get().is_zero());
// ensure election results are always bounded with the same value
assert!(
<T::ElectionProvider as ElectionProviderBase>::MaxWinners::get() ==
<T::GenesisElectionProvider as ElectionProviderBase>::MaxWinners::get()
);
sp_std::if_std! {
sp_io::TestExternalities::new_empty().execute_with(||
assert!(
@@ -1264,11 +1276,18 @@ pub mod pallet {
#[pallet::compact] new: u32,
) -> DispatchResult {
ensure_root(origin)?;
// ensure new validator count does not exceed maximum winners
// support by election provider.
ensure!(
new <= <T::ElectionProvider as ElectionProviderBase>::MaxWinners::get(),
Error::<T>::TooManyValidators
);
ValidatorCount::<T>::put(new);
Ok(())
}
/// Increments the ideal number of validators.
/// Increments the ideal number of validators upto maximum of
/// `ElectionProviderBase::MaxWinners`.
///
/// The dispatch origin must be Root.
///
@@ -1281,11 +1300,19 @@ pub mod pallet {
#[pallet::compact] additional: u32,
) -> DispatchResult {
ensure_root(origin)?;
ValidatorCount::<T>::mutate(|n| *n += additional);
let old = ValidatorCount::<T>::get();
let new = old.checked_add(additional).ok_or(ArithmeticError::Overflow)?;
ensure!(
new <= <T::ElectionProvider as ElectionProviderBase>::MaxWinners::get(),
Error::<T>::TooManyValidators
);
ValidatorCount::<T>::put(new);
Ok(())
}
/// Scale up the ideal number of validators by a factor.
/// Scale up the ideal number of validators by a factor upto maximum of
/// `ElectionProviderBase::MaxWinners`.
///
/// The dispatch origin must be Root.
///
@@ -1295,7 +1322,15 @@ pub mod pallet {
#[pallet::weight(T::WeightInfo::set_validator_count())]
pub fn scale_validator_count(origin: OriginFor<T>, factor: Percent) -> DispatchResult {
ensure_root(origin)?;
ValidatorCount::<T>::mutate(|n| *n += factor * *n);
let old = ValidatorCount::<T>::get();
let new = old.checked_add(factor.mul_floor(old)).ok_or(ArithmeticError::Overflow)?;
ensure!(
new <= <T::ElectionProvider as ElectionProviderBase>::MaxWinners::get(),
Error::<T>::TooManyValidators
);
ValidatorCount::<T>::put(new);
Ok(())
}
+54
View File
@@ -5624,3 +5624,57 @@ fn reducing_max_unlocking_chunks_abrupt() {
MaxUnlockingChunks::set(2);
})
}
#[test]
fn cannot_set_unsupported_validator_count() {
ExtBuilder::default().build_and_execute(|| {
MaxWinners::set(50);
// set validator count works
assert_ok!(Staking::set_validator_count(RuntimeOrigin::root(), 30));
assert_ok!(Staking::set_validator_count(RuntimeOrigin::root(), 50));
// setting validator count above 100 does not work
assert_noop!(
Staking::set_validator_count(RuntimeOrigin::root(), 51),
Error::<Test>::TooManyValidators,
);
})
}
#[test]
fn increase_validator_count_errors() {
ExtBuilder::default().build_and_execute(|| {
MaxWinners::set(50);
assert_ok!(Staking::set_validator_count(RuntimeOrigin::root(), 40));
// increase works
assert_ok!(Staking::increase_validator_count(RuntimeOrigin::root(), 6));
assert_eq!(ValidatorCount::<Test>::get(), 46);
// errors
assert_noop!(
Staking::increase_validator_count(RuntimeOrigin::root(), 5),
Error::<Test>::TooManyValidators,
);
})
}
#[test]
fn scale_validator_count_errors() {
ExtBuilder::default().build_and_execute(|| {
MaxWinners::set(50);
assert_ok!(Staking::set_validator_count(RuntimeOrigin::root(), 20));
// scale value works
assert_ok!(Staking::scale_validator_count(
RuntimeOrigin::root(),
Percent::from_percent(200)
));
assert_eq!(ValidatorCount::<Test>::get(), 40);
// errors
assert_noop!(
Staking::scale_validator_count(RuntimeOrigin::root(), Percent::from_percent(126)),
Error::<Test>::TooManyValidators,
);
})
}