mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-26 19:17:58 +00:00
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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user