Store validator self-vote in bags-list, and allow them to be trimmed for election (#10821)

* Implement the new validator-in-bags-list scenario + migration

* Apply suggestions from code review

Co-authored-by: Zeke Mostov <z.mostov@gmail.com>

* some review comments

* guard the migration

* some review comments

* Fix tests 🤦‍♂️

* Fix build

* fix weight_of_fn

* reformat line width

* make const

* use weight of fn cached

* SortedListProvider -> VoterList

* Fix all build and docs

* check post migration

Co-authored-by: Zeke Mostov <z.mostov@gmail.com>
This commit is contained in:
Kian Paimani
2022-03-23 14:17:26 +00:00
committed by GitHub
parent e0cef34921
commit 661d0ea5bb
17 changed files with 317 additions and 216 deletions
+1 -3
View File
@@ -558,9 +558,7 @@ impl pallet_staking::Config for Runtime {
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
type ElectionProvider = ElectionProviderMultiPhase;
type GenesisElectionProvider = onchain::OnChainSequentialPhragmen<Self>;
// Alternatively, use pallet_staking::UseNominatorsMap<Runtime> to just use the nominators map.
// Note that the aforementioned does not scale to a very large number of nominators.
type SortedListProvider = BagsList;
type VoterList = BagsList;
type MaxUnlockingChunks = ConstU32<32>;
type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
type BenchmarkingConfig = StakingBenchmarkingConfig;
+1 -1
View File
@@ -197,7 +197,7 @@ impl pallet_staking::Config for Test {
type NextNewSession = Session;
type ElectionProvider = onchain::OnChainSequentialPhragmen<Self>;
type GenesisElectionProvider = Self::ElectionProvider;
type SortedListProvider = pallet_staking::UseNominatorsMap<Self>;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type MaxUnlockingChunks = ConstU32<32>;
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
@@ -48,7 +48,7 @@ pub fn display_and_check_bags<Runtime: RuntimeT>(currency_unit: u64, currency_na
let min_nominator_bond = <pallet_staking::MinNominatorBond<Runtime>>::get();
log::info!(target: LOG_TARGET, "min nominator bond is {:?}", min_nominator_bond);
let voter_list_count = <Runtime as pallet_staking::Config>::SortedListProvider::count();
let voter_list_count = <Runtime as pallet_staking::Config>::VoterList::count();
// go through every bag to track the total number of voters within bags and log some info about
// how voters are distributed within the bags.
@@ -17,7 +17,6 @@
//! Test to check the migration of the voter bag.
use crate::{RuntimeT, LOG_TARGET};
use frame_election_provider_support::SortedListProvider;
use frame_support::traits::PalletInfoAccess;
use pallet_staking::Nominators;
use remote_externalities::{Builder, Mode, OnlineConfig};
@@ -45,16 +44,16 @@ pub async fn execute<Runtime: RuntimeT, Block: BlockT + DeserializeOwned>(
let pre_migrate_nominator_count = <Nominators<Runtime>>::iter().count() as u32;
log::info!(target: LOG_TARGET, "Nominator count: {}", pre_migrate_nominator_count);
// run the actual migration,
let moved = <Runtime as pallet_staking::Config>::SortedListProvider::unsafe_regenerate(
use frame_election_provider_support::SortedListProvider;
// run the actual migration
let moved = <Runtime as pallet_staking::Config>::VoterList::unsafe_regenerate(
pallet_staking::Nominators::<Runtime>::iter().map(|(n, _)| n),
pallet_staking::Pallet::<Runtime>::weight_of_fn(),
);
log::info!(target: LOG_TARGET, "Moved {} nominators", moved);
let voter_list_len =
<Runtime as pallet_staking::Config>::SortedListProvider::iter().count() as u32;
let voter_list_count = <Runtime as pallet_staking::Config>::SortedListProvider::count();
let voter_list_len = <Runtime as pallet_staking::Config>::VoterList::iter().count() as u32;
let voter_list_count = <Runtime as pallet_staking::Config>::VoterList::count();
// and confirm it is equal to the length of the `VoterList`.
assert_eq!(pre_migrate_nominator_count, voter_list_len);
assert_eq!(pre_migrate_nominator_count, voter_list_count);
@@ -16,6 +16,7 @@
//! Test to execute the snapshot using the voter bag.
use frame_election_provider_support::SortedListProvider;
use frame_support::traits::PalletInfoAccess;
use remote_externalities::{Builder, Mode, OnlineConfig};
use sp_runtime::{traits::Block as BlockT, DeserializeOwned};
@@ -48,11 +49,11 @@ pub async fn execute<Runtime: crate::RuntimeT, Block: BlockT + DeserializeOwned>
.unwrap();
ext.execute_with(|| {
use frame_election_provider_support::{ElectionDataProvider, SortedListProvider};
use frame_election_provider_support::ElectionDataProvider;
log::info!(
target: crate::LOG_TARGET,
"{} nodes in bags list.",
<Runtime as pallet_staking::Config>::SortedListProvider::count(),
<Runtime as pallet_staking::Config>::VoterList::count(),
);
let voters =
+1 -1
View File
@@ -205,7 +205,7 @@ impl pallet_staking::Config for Test {
type NextNewSession = Session;
type ElectionProvider = onchain::OnChainSequentialPhragmen<Self>;
type GenesisElectionProvider = Self::ElectionProvider;
type SortedListProvider = pallet_staking::UseNominatorsMap<Self>;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type MaxUnlockingChunks = ConstU32<32>;
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
@@ -175,7 +175,7 @@ impl pallet_staking::Config for Test {
type OffendingValidatorsThreshold = ();
type ElectionProvider = onchain::OnChainSequentialPhragmen<Self>;
type GenesisElectionProvider = Self::ElectionProvider;
type SortedListProvider = pallet_staking::UseNominatorsMap<Self>;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type MaxUnlockingChunks = ConstU32<32>;
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
@@ -182,7 +182,7 @@ impl pallet_staking::Config for Test {
type ElectionProvider = onchain::OnChainSequentialPhragmen<Self>;
type GenesisElectionProvider = Self::ElectionProvider;
type MaxUnlockingChunks = ConstU32<32>;
type SortedListProvider = pallet_staking::UseNominatorsMap<Self>;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
}
+2 -2
View File
@@ -87,7 +87,7 @@ pub fn migrate<T: pallet_session_historical::Config, P: GetStorageVersion + Pall
}
/// Some checks prior to migration. This can be linked to
/// [`frame_support::traits::OnRuntimeUpgrade::pre_upgrade`] for further testing.
/// `frame_support::traits::OnRuntimeUpgrade::pre_upgrade` for further testing.
///
/// Panics if anything goes wrong.
pub fn pre_migrate<
@@ -123,7 +123,7 @@ pub fn pre_migrate<
}
/// Some checks for after migration. This can be linked to
/// [`frame_support::traits::OnRuntimeUpgrade::post_upgrade`] for further testing.
/// `frame_support::traits::OnRuntimeUpgrade::post_upgrade` for further testing.
///
/// Panics if anything goes wrong.
pub fn post_migrate<
+23 -27
View File
@@ -155,8 +155,8 @@ impl<T: Config> ListScenario<T> {
/// - the destination bag has at least one node, which will need its next pointer updated.
///
/// NOTE: while this scenario specifically targets a worst case for the bags-list, it should
/// also elicit a worst case for other known `SortedListProvider` implementations; although
/// this may not be true against unknown `SortedListProvider` implementations.
/// also elicit a worst case for other known `VoterList` implementations; although
/// this may not be true against unknown `VoterList` implementations.
fn new(origin_weight: BalanceOf<T>, is_increase: bool) -> Result<Self, &'static str> {
ensure!(!origin_weight.is_zero(), "origin weight must be greater than 0");
@@ -189,7 +189,7 @@ impl<T: Config> ListScenario<T> {
// find a destination weight that will trigger the worst case scenario
let dest_weight_as_vote =
T::SortedListProvider::score_update_worst_case(&origin_stash1, is_increase);
T::VoterList::score_update_worst_case(&origin_stash1, is_increase);
let total_issuance = T::Currency::total_issuance();
@@ -316,7 +316,7 @@ benchmarks! {
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let controller = scenario.origin_controller1.clone();
let stash = scenario.origin_stash1.clone();
assert!(T::SortedListProvider::contains(&stash));
assert!(T::VoterList::contains(&stash));
let ed = T::Currency::minimum_balance();
let mut ledger = Ledger::<T>::get(&controller).unwrap();
@@ -328,28 +328,24 @@ benchmarks! {
}: withdraw_unbonded(RawOrigin::Signed(controller.clone()), s)
verify {
assert!(!Ledger::<T>::contains_key(controller));
assert!(!T::SortedListProvider::contains(&stash));
assert!(!T::VoterList::contains(&stash));
}
validate {
// clean up any existing state.
clear_validators_and_nominators::<T>();
let origin_weight = MinNominatorBond::<T>::get().max(T::Currency::minimum_balance());
// setup a worst case scenario where the user calling validate was formerly a nominator so
// they must be removed from the list.
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let controller = scenario.origin_controller1.clone();
let stash = scenario.origin_stash1.clone();
assert!(T::SortedListProvider::contains(&stash));
let (stash, controller) = create_stash_controller::<T>(
T::MaxNominations::get() - 1,
100,
Default::default(),
)?;
// because it is chilled.
assert!(!T::VoterList::contains(&stash));
let prefs = ValidatorPrefs::default();
whitelist_account!(controller);
}: _(RawOrigin::Signed(controller), prefs)
verify {
assert!(Validators::<T>::contains_key(&stash));
assert!(!T::SortedListProvider::contains(&stash));
assert!(T::VoterList::contains(&stash));
}
kick {
@@ -434,14 +430,14 @@ benchmarks! {
).unwrap();
assert!(!Nominators::<T>::contains_key(&stash));
assert!(!T::SortedListProvider::contains(&stash));
assert!(!T::VoterList::contains(&stash));
let validators = create_validators::<T>(n, 100).unwrap();
whitelist_account!(controller);
}: _(RawOrigin::Signed(controller), validators)
verify {
assert!(Nominators::<T>::contains_key(&stash));
assert!(T::SortedListProvider::contains(&stash))
assert!(T::VoterList::contains(&stash))
}
chill {
@@ -455,12 +451,12 @@ benchmarks! {
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let controller = scenario.origin_controller1.clone();
let stash = scenario.origin_stash1.clone();
assert!(T::SortedListProvider::contains(&stash));
assert!(T::VoterList::contains(&stash));
whitelist_account!(controller);
}: _(RawOrigin::Signed(controller))
verify {
assert!(!T::SortedListProvider::contains(&stash));
assert!(!T::VoterList::contains(&stash));
}
set_payee {
@@ -523,13 +519,13 @@ benchmarks! {
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let controller = scenario.origin_controller1.clone();
let stash = scenario.origin_stash1.clone();
assert!(T::SortedListProvider::contains(&stash));
assert!(T::VoterList::contains(&stash));
add_slashing_spans::<T>(&stash, s);
}: _(RawOrigin::Root, stash.clone(), s)
verify {
assert!(!Ledger::<T>::contains_key(&controller));
assert!(!T::SortedListProvider::contains(&stash));
assert!(!T::VoterList::contains(&stash));
}
cancel_deferred_slash {
@@ -708,13 +704,13 @@ benchmarks! {
Ledger::<T>::insert(&controller, l);
assert!(Bonded::<T>::contains_key(&stash));
assert!(T::SortedListProvider::contains(&stash));
assert!(T::VoterList::contains(&stash));
whitelist_account!(controller);
}: _(RawOrigin::Signed(controller), stash.clone(), s)
verify {
assert!(!Bonded::<T>::contains_key(&stash));
assert!(!T::SortedListProvider::contains(&stash));
assert!(!T::VoterList::contains(&stash));
}
new_era {
@@ -899,7 +895,7 @@ benchmarks! {
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let controller = scenario.origin_controller1.clone();
let stash = scenario.origin_stash1.clone();
assert!(T::SortedListProvider::contains(&stash));
assert!(T::VoterList::contains(&stash));
Staking::<T>::set_staking_configs(
RawOrigin::Root.into(),
@@ -914,7 +910,7 @@ benchmarks! {
let caller = whitelisted_caller();
}: _(RawOrigin::Signed(caller), controller.clone())
verify {
assert!(!T::SortedListProvider::contains(&stash));
assert!(!T::VoterList::contains(&stash));
}
force_apply_min_commission {
+2 -1
View File
@@ -780,7 +780,8 @@ enum Releases {
V5_0_0, // blockable validators.
V6_0_0, // removal of all storage associated with offchain phragmen.
V7_0_0, // keep track of number of nominators / validators in map
V8_0_0, // populate `SortedListProvider`.
V8_0_0, // populate `VoterList`.
V9_0_0, // inject validators into `VoterList` as well.
}
impl Default for Releases {
+76 -7
View File
@@ -17,13 +17,83 @@
//! Storage migrations for the Staking pallet.
use super::*;
use frame_election_provider_support::SortedListProvider;
use frame_support::traits::OnRuntimeUpgrade;
pub mod v9 {
use super::*;
/// Migration implementation that injects all validators into sorted list.
///
/// This is only useful for chains that started their `VoterList` just based on nominators.
pub struct InjectValidatorsIntoVoterList<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> OnRuntimeUpgrade for InjectValidatorsIntoVoterList<T> {
fn on_runtime_upgrade() -> Weight {
if StorageVersion::<T>::get() == Releases::V8_0_0 {
let prev_count = T::VoterList::count();
let weight_of_cached = Pallet::<T>::weight_of_fn();
for (v, _) in Validators::<T>::iter() {
let weight = weight_of_cached(&v);
let _ = T::VoterList::on_insert(v.clone(), weight).map_err(|err| {
log!(warn, "failed to insert {:?} into VoterList: {:?}", v, err)
});
}
log!(
info,
"injected a total of {} new voters, prev count: {} next count: {}, updating to version 9",
Validators::<T>::count(),
prev_count,
T::VoterList::count(),
);
StorageVersion::<T>::put(crate::Releases::V9_0_0);
T::BlockWeights::get().max_block
} else {
log!(
warn,
"InjectValidatorsIntoVoterList being executed on the wrong storage \
version, expected Releases::V8_0_0"
);
T::DbWeight::get().reads(1)
}
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<(), &'static str> {
use frame_support::traits::OnRuntimeUpgradeHelpersExt;
frame_support::ensure!(
StorageVersion::<T>::get() == crate::Releases::V8_0_0,
"must upgrade linearly"
);
let prev_count = T::VoterList::count();
Self::set_temp_storage(prev_count, "prev");
Ok(())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade() -> Result<(), &'static str> {
use frame_support::traits::OnRuntimeUpgradeHelpersExt;
let post_count = T::VoterList::count();
let prev_count = Self::get_temp_storage::<u32>("prev").unwrap();
let validators = Validators::<T>::count();
assert!(post_count == prev_count + validators);
frame_support::ensure!(
StorageVersion::<T>::get() == crate::Releases::V9_0_0,
"must upgrade "
);
Ok(())
}
}
}
pub mod v8 {
use crate::{Config, Nominators, Pallet, StorageVersion, Weight};
use frame_election_provider_support::SortedListProvider;
use frame_support::traits::Get;
use crate::{Config, Nominators, Pallet, StorageVersion, Weight};
#[cfg(feature = "try-runtime")]
pub fn pre_migrate<T: Config>() -> Result<(), &'static str> {
frame_support::ensure!(
@@ -35,16 +105,16 @@ pub mod v8 {
Ok(())
}
/// Migration to sorted [`SortedListProvider`].
/// Migration to sorted `VoterList`.
pub fn migrate<T: Config>() -> Weight {
if StorageVersion::<T>::get() == crate::Releases::V7_0_0 {
crate::log!(info, "migrating staking to Releases::V8_0_0");
let migrated = T::SortedListProvider::unsafe_regenerate(
let migrated = T::VoterList::unsafe_regenerate(
Nominators::<T>::iter().map(|(id, _)| id),
Pallet::<T>::weight_of_fn(),
);
debug_assert_eq!(T::SortedListProvider::sanity_check(), Ok(()));
debug_assert_eq!(T::VoterList::sanity_check(), Ok(()));
StorageVersion::<T>::put(crate::Releases::V8_0_0);
crate::log!(
@@ -61,8 +131,7 @@ pub mod v8 {
#[cfg(feature = "try-runtime")]
pub fn post_migrate<T: Config>() -> Result<(), &'static str> {
T::SortedListProvider::sanity_check()
.map_err(|_| "SortedListProvider is not in a sane state.")?;
T::VoterList::sanity_check().map_err(|_| "VoterList is not in a sane state.")?;
crate::log!(info, "👜 staking bags-list migration passes POST migrate checks ✅",);
Ok(())
}
+5 -5
View File
@@ -270,8 +270,8 @@ impl crate::pallet::pallet::Config for Test {
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
type ElectionProvider = onchain::OnChainSequentialPhragmen<Self>;
type GenesisElectionProvider = Self::ElectionProvider;
// NOTE: consider a macro and use `UseNominatorsMap<Self>` as well.
type SortedListProvider = BagsList;
// NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well.
type VoterList = BagsList;
type MaxUnlockingChunks = ConstU32<32>;
type BenchmarkingConfig = TestBenchmarkingConfig;
type WeightInfo = ();
@@ -541,9 +541,9 @@ fn check_count() {
assert_eq!(nominator_count, Nominators::<Test>::count());
assert_eq!(validator_count, Validators::<Test>::count());
// the voters that the `SortedListProvider` list is storing for us.
let external_voters = <Test as Config>::SortedListProvider::count();
assert_eq!(external_voters, nominator_count);
// the voters that the `VoterList` list is storing for us.
let external_voters = <Test as Config>::VoterList::count();
assert_eq!(external_voters, nominator_count + validator_count);
}
fn check_ledgers() {
+103 -73
View File
@@ -49,6 +49,14 @@ use crate::{
use super::{pallet::*, STAKING_ID};
/// The maximum number of iterations that we do whilst iterating over `T::VoterList` in
/// `get_npos_voters`.
///
/// In most cases, if we want n items, we iterate exactly n times. In rare cases, if a voter is
/// invalid (for any reason) the iteration continues. With this constant, we iterate at most 2 * n
/// times and then give up.
const NPOS_MAX_ITERATIONS_COEFFICIENT: u32 = 2;
impl<T: Config> Pallet<T> {
/// The total balance that can be slashed from a stash account as of right now.
pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf<T> {
@@ -649,90 +657,77 @@ impl<T: Config> Pallet<T> {
/// Get all of the voters that are eligible for the npos election.
///
/// `maybe_max_len` can imposes a cap on the number of voters returned; First all the validator
/// are included in no particular order, then remainder is taken from the nominators, as
/// returned by [`Config::SortedListProvider`].
///
/// This will use nominators, and all the validators will inject a self vote.
/// `maybe_max_len` can imposes a cap on the number of voters returned;
///
/// This function is self-weighing as [`DispatchClass::Mandatory`].
///
/// ### Slashing
///
/// All nominations that have been submitted before the last non-zero slash of the validator are
/// auto-chilled, but still count towards the limit imposed by `maybe_max_len`.
/// All votes that have been submitted before the last non-zero slash of the corresponding
/// target are *auto-chilled*, but still count towards the limit imposed by `maybe_max_len`.
pub fn get_npos_voters(maybe_max_len: Option<usize>) -> Vec<VoterOf<Self>> {
let max_allowed_len = {
let nominator_count = Nominators::<T>::count() as usize;
let validator_count = Validators::<T>::count() as usize;
let all_voter_count = validator_count.saturating_add(nominator_count);
let all_voter_count = T::VoterList::count() as usize;
maybe_max_len.unwrap_or(all_voter_count).min(all_voter_count)
};
let mut all_voters = Vec::<_>::with_capacity(max_allowed_len);
// first, grab all validators in no particular order, capped by the maximum allowed length.
let mut validators_taken = 0u32;
for (validator, _) in <Validators<T>>::iter().take(max_allowed_len) {
// Append self vote.
let self_vote = (
validator.clone(),
Self::weight_of(&validator),
vec![validator.clone()]
.try_into()
.expect("`MaxVotesPerVoter` must be greater than or equal to 1"),
);
all_voters.push(self_vote);
validators_taken.saturating_inc();
}
// .. and grab whatever we have left from nominators.
let nominators_quota = (max_allowed_len as u32).saturating_sub(validators_taken);
// cache a few things.
let weight_of = Self::weight_of_fn();
let slashing_spans = <SlashingSpans<T>>::iter().collect::<BTreeMap<_, _>>();
// track the count of nominators added to `all_voters
let mut voters_seen = 0u32;
let mut validators_taken = 0u32;
let mut nominators_taken = 0u32;
// track every nominator iterated over, but not necessarily added to `all_voters`
let mut nominators_seen = 0u32;
// cache the total-issuance once in this function
let weight_of = Self::weight_of_fn();
let mut nominators_iter = T::SortedListProvider::iter();
while nominators_taken < nominators_quota && nominators_seen < nominators_quota * 2 {
let nominator = match nominators_iter.next() {
Some(nominator) => {
nominators_seen.saturating_inc();
nominator
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)
{
let voter = match sorted_voters.next() {
Some(voter) => {
voters_seen.saturating_inc();
voter
},
None => break,
};
if let Some(Nominations { submitted_in, mut targets, suppressed: _ }) =
<Nominators<T>>::get(&nominator)
<Nominators<T>>::get(&voter)
{
log!(
trace,
"fetched nominator {:?} with weight {:?}",
nominator,
weight_of(&nominator)
);
// if this voter is a nominator:
targets.retain(|stash| {
slashing_spans
.get(stash)
.map_or(true, |spans| submitted_in >= spans.last_nonzero_slash())
});
if !targets.len().is_zero() {
all_voters.push((nominator.clone(), weight_of(&nominator), targets));
all_voters.push((voter.clone(), weight_of(&voter), targets));
nominators_taken.saturating_inc();
}
} else if Validators::<T>::contains_key(&voter) {
// if this voter is a validator:
let self_vote = (
voter.clone(),
weight_of(&voter),
vec![voter.clone()]
.try_into()
.expect("`MaxVotesPerVoter` must be greater than or equal to 1"),
);
all_voters.push(self_vote);
validators_taken.saturating_inc();
} else {
// this can only happen if: 1. there a pretty bad 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`. This can rarely happen, and is not really an
// emergency or bug if it does.
log!(warn, "DEFENSIVE: invalid item in `SortedListProvider`: {:?}, this nominator probably has too many nominations now", nominator)
// 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
)
}
}
@@ -752,6 +747,7 @@ impl<T: Config> Pallet<T> {
validators_taken,
nominators_taken
);
all_voters
}
@@ -773,7 +769,7 @@ impl<T: Config> Pallet<T> {
}
/// This function will add a nominator to the `Nominators` storage map,
/// and [`SortedListProvider`].
/// and `VoterList`.
///
/// If the nominator already exists, their nominations will be updated.
///
@@ -782,18 +778,21 @@ impl<T: Config> Pallet<T> {
/// wrong.
pub fn do_add_nominator(who: &T::AccountId, nominations: Nominations<T>) {
if !Nominators::<T>::contains_key(who) {
// maybe update sorted list. Error checking is defensive-only - this should never fail.
let _ = T::SortedListProvider::on_insert(who.clone(), Self::weight_of(who))
// maybe update sorted list.
let _ = T::VoterList::on_insert(who.clone(), Self::weight_of(who))
.defensive_unwrap_or_default();
debug_assert_eq!(T::SortedListProvider::sanity_check(), Ok(()));
}
Nominators::<T>::insert(who, nominations);
debug_assert_eq!(
Nominators::<T>::count() + Validators::<T>::count(),
T::VoterList::count()
);
debug_assert_eq!(T::VoterList::sanity_check(), Ok(()));
}
/// This function will remove a nominator from the `Nominators` storage map,
/// and [`SortedListProvider`].
/// and `VoterList`.
///
/// Returns true if `who` was removed from `Nominators`, otherwise false.
///
@@ -801,15 +800,21 @@ impl<T: Config> Pallet<T> {
/// `Nominators` or `VoterList` outside of this function is almost certainly
/// wrong.
pub fn do_remove_nominator(who: &T::AccountId) -> bool {
if Nominators::<T>::contains_key(who) {
let outcome = if Nominators::<T>::contains_key(who) {
Nominators::<T>::remove(who);
T::SortedListProvider::on_remove(who);
debug_assert_eq!(T::SortedListProvider::sanity_check(), Ok(()));
debug_assert_eq!(Nominators::<T>::count(), T::SortedListProvider::count());
T::VoterList::on_remove(who);
true
} else {
false
}
};
debug_assert_eq!(T::VoterList::sanity_check(), Ok(()));
debug_assert_eq!(
Nominators::<T>::count() + Validators::<T>::count(),
T::VoterList::count()
);
outcome
}
/// This function will add a validator to the `Validators` storage map.
@@ -820,7 +825,18 @@ impl<T: Config> Pallet<T> {
/// `Validators` or `VoterList` outside of this function is almost certainly
/// wrong.
pub fn do_add_validator(who: &T::AccountId, prefs: ValidatorPrefs) {
if !Validators::<T>::contains_key(who) {
// maybe update sorted list.
let _ = T::VoterList::on_insert(who.clone(), Self::weight_of(who))
.defensive_unwrap_or_default();
}
Validators::<T>::insert(who, prefs);
debug_assert_eq!(
Nominators::<T>::count() + Validators::<T>::count(),
T::VoterList::count()
);
debug_assert_eq!(T::VoterList::sanity_check(), Ok(()));
}
/// This function will remove a validator from the `Validators` storage map.
@@ -831,12 +847,21 @@ impl<T: Config> Pallet<T> {
/// `Validators` or `VoterList` outside of this function is almost certainly
/// wrong.
pub fn do_remove_validator(who: &T::AccountId) -> bool {
if Validators::<T>::contains_key(who) {
let outcome = if Validators::<T>::contains_key(who) {
Validators::<T>::remove(who);
T::VoterList::on_remove(who);
true
} else {
false
}
};
debug_assert_eq!(T::VoterList::sanity_check(), Ok(()));
debug_assert_eq!(
Nominators::<T>::count() + Validators::<T>::count(),
T::VoterList::count()
);
outcome
}
/// Register some amount of weight directly with the system pallet.
@@ -963,7 +988,7 @@ impl<T: Config> ElectionDataProvider for Pallet<T> {
<Validators<T>>::remove_all();
<Nominators<T>>::remove_all();
T::SortedListProvider::unsafe_clear();
T::VoterList::unsafe_clear();
}
#[cfg(feature = "runtime-benchmarks")]
@@ -1278,20 +1303,24 @@ impl<T: Config> ScoreProvider<T::AccountId> for Pallet<T> {
/// A simple voter list implementation that does not require any additional pallets. Note, this
/// does not provided nominators in sorted ordered. If you desire nominators in a sorted order take
/// a look at [`pallet-bags-list].
pub struct UseNominatorsMap<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> SortedListProvider<T::AccountId> for UseNominatorsMap<T> {
pub struct UseNominatorsAndValidatorsMap<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> SortedListProvider<T::AccountId> for UseNominatorsAndValidatorsMap<T> {
type Error = ();
type Score = VoteWeight;
/// Returns iterator over voter list, which can have `take` called on it.
fn iter() -> Box<dyn Iterator<Item = T::AccountId>> {
Box::new(Nominators::<T>::iter().map(|(n, _)| n))
Box::new(
Validators::<T>::iter()
.map(|(v, _)| v)
.chain(Nominators::<T>::iter().map(|(n, _)| n)),
)
}
fn count() -> u32 {
Nominators::<T>::count()
Nominators::<T>::count().saturating_add(Validators::<T>::count())
}
fn contains(id: &T::AccountId) -> bool {
Nominators::<T>::contains_key(id)
Nominators::<T>::contains_key(id) || Validators::<T>::contains_key(id)
}
fn on_insert(_: T::AccountId, _weight: Self::Score) -> Result<(), Self::Error> {
// nothing to do on insert.
@@ -1318,5 +1347,6 @@ impl<T: Config> SortedListProvider<T::AccountId> for UseNominatorsMap<T> {
// NOTE: Caller must ensure this doesn't lead to too many storage accesses. This is a
// condition of SortedListProvider::unsafe_clear.
Nominators::<T>::remove_all();
Validators::<T>::remove_all();
}
}
+17 -18
View File
@@ -17,7 +17,7 @@
//! Staking FRAME Pallet.
use frame_election_provider_support::SortedListProvider;
use frame_election_provider_support::{SortedListProvider, VoteWeight};
use frame_support::{
dispatch::Codec,
pallet_prelude::*,
@@ -163,13 +163,12 @@ pub mod pallet {
/// After the threshold is reached a new era will be forced.
type OffendingValidatorsThreshold: Get<Perbill>;
/// Something that can provide a sorted list of voters in a somewhat sorted way. The
/// original use case for this was designed with `pallet_bags_list::Pallet` in mind. If
/// the bags-list is not desired, [`impls::UseNominatorsMap`] is likely the desired option.
type SortedListProvider: SortedListProvider<
Self::AccountId,
Score = frame_election_provider_support::VoteWeight,
>;
/// Something that provides a best-effort sorted list of voters aka electing nominators,
/// used for NPoS election.
///
/// The changes to nominators are reported to this. Moreover, each validator's self-vote is
/// also reported as one independent vote.
type VoterList: SortedListProvider<Self::AccountId, Score = VoteWeight>;
/// The maximum number of `unlocking` chunks a [`StakingLedger`] can have. Effectively
/// determines how many unique eras a staker may be unbonding in.
@@ -584,10 +583,10 @@ pub mod pallet {
});
}
// all voters are reported to the `SortedListProvider`.
// all voters are reported to the `VoterList`.
assert_eq!(
T::SortedListProvider::count(),
Nominators::<T>::count(),
T::VoterList::count(),
Nominators::<T>::count() + Validators::<T>::count(),
"not all genesis stakers were inserted into sorted list provider, something is wrong."
);
}
@@ -837,9 +836,9 @@ pub mod pallet {
// NOTE: ledger must be updated prior to calling `Self::weight_of`.
Self::update_ledger(&controller, &ledger);
// update this staker in the sorted list, if they exist in it.
if T::SortedListProvider::contains(&stash) {
T::SortedListProvider::on_update(&stash, Self::weight_of(&ledger.stash));
debug_assert_eq!(T::SortedListProvider::sanity_check(), Ok(()));
if T::VoterList::contains(&stash) {
T::VoterList::on_update(&stash, Self::weight_of(&ledger.stash));
debug_assert_eq!(T::VoterList::sanity_check(), Ok(()));
}
Self::deposit_event(Event::<T>::Bonded(stash.clone(), extra));
@@ -920,8 +919,8 @@ pub mod pallet {
Self::update_ledger(&controller, &ledger);
// update this staker in the sorted list, if they exist in it.
if T::SortedListProvider::contains(&ledger.stash) {
T::SortedListProvider::on_update(&ledger.stash, Self::weight_of(&ledger.stash));
if T::VoterList::contains(&ledger.stash) {
T::VoterList::on_update(&ledger.stash, Self::weight_of(&ledger.stash));
}
Self::deposit_event(Event::<T>::Unbonded(ledger.stash, value));
@@ -1403,8 +1402,8 @@ pub mod pallet {
// NOTE: ledger must be updated prior to calling `Self::weight_of`.
Self::update_ledger(&controller, &ledger);
if T::SortedListProvider::contains(&ledger.stash) {
T::SortedListProvider::on_update(&ledger.stash, Self::weight_of(&ledger.stash));
if T::VoterList::contains(&ledger.stash) {
T::VoterList::on_update(&ledger.stash, Self::weight_of(&ledger.stash));
}
let removed_chunks = 1u32 // for the case where the last iterated chunk is not removed
+2 -2
View File
@@ -38,11 +38,11 @@ const SEED: u32 = 0;
pub fn clear_validators_and_nominators<T: Config>() {
Validators::<T>::remove_all();
// whenever we touch nominators counter we should update `T::SortedListProvider` as well.
// whenever we touch nominators counter we should update `T::VoterList` as well.
Nominators::<T>::remove_all();
// NOTE: safe to call outside block production
T::SortedListProvider::unsafe_clear();
T::VoterList::unsafe_clear();
}
/// Grab a funded user.
+73 -65
View File
@@ -4113,11 +4113,7 @@ mod election_data_provider {
.set_status(41, StakerStatus::Validator)
.build_and_execute(|| {
// sum of all nominators who'd be voters (1), plus the self-votes (4).
assert_eq!(
<Test as Config>::SortedListProvider::count() +
<Validators<Test>>::iter().count() as u32,
5
);
assert_eq!(<Test as Config>::VoterList::count(), 5);
// if limits is less..
assert_eq!(Staking::electing_voters(Some(1)).unwrap().len(), 1);
@@ -4140,43 +4136,43 @@ mod election_data_provider {
});
}
// 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]
fn only_iterates_max_2_times_nominators_quota() {
fn only_iterates_max_2_times_max_allowed_len() {
ExtBuilder::default()
.nominate(true) // add nominator 101, who nominates [11, 21]
.nominate(false)
// the other nominators only nominate 21
.add_staker(61, 60, 2_000, StakerStatus::<AccountId>::Nominator(vec![21]))
.add_staker(71, 70, 2_000, StakerStatus::<AccountId>::Nominator(vec![21]))
.add_staker(81, 80, 2_000, StakerStatus::<AccountId>::Nominator(vec![21]))
.build_and_execute(|| {
// given our nominators ordered by stake,
// all voters ordered by stake,
assert_eq!(
<Test as Config>::SortedListProvider::iter().collect::<Vec<_>>(),
vec![61, 71, 81, 101]
<Test as Config>::VoterList::iter().collect::<Vec<_>>(),
vec![61, 71, 81, 11, 21, 31]
);
// and total voters
assert_eq!(
<Test as Config>::SortedListProvider::count() +
<Validators<Test>>::iter().count() as u32,
7
);
// roll to session 5
run_to_block(25);
// slash 21, the only validator nominated by our first 3 nominators
add_slash(&21);
// we take 4 voters: 2 validators and 2 nominators (so nominators quota = 2)
// we want 2 voters now, and in maximum we allow 4 iterations. This is what happens:
// 61 is pruned;
// 71 is pruned;
// 81 is pruned;
// 11 is taken;
// we finish since the 2x limit is reached.
assert_eq!(
Staking::electing_voters(Some(3))
Staking::electing_voters(Some(2))
.unwrap()
.iter()
.map(|(stash, _, _)| stash)
.copied()
.collect::<Vec<_>>(),
vec![31, 11], // 2 validators, but no nominators because we hit the quota
vec![11],
);
});
}
@@ -4189,45 +4185,17 @@ mod election_data_provider {
#[test]
fn get_max_len_voters_even_if_some_nominators_are_slashed() {
ExtBuilder::default()
.nominate(true) // add nominator 101, who nominates [11, 21]
.nominate(false)
.add_staker(61, 60, 20, StakerStatus::<AccountId>::Nominator(vec![21]))
// 61 only nominates validator 21 ^^
.add_staker(71, 70, 10, StakerStatus::<AccountId>::Nominator(vec![11, 21]))
.add_staker(81, 80, 10, StakerStatus::<AccountId>::Nominator(vec![11, 21]))
.build_and_execute(|| {
// given our nominators ordered by stake,
// given our voters ordered by stake,
assert_eq!(
<Test as Config>::SortedListProvider::iter().collect::<Vec<_>>(),
vec![101, 61, 71]
<Test as Config>::VoterList::iter().collect::<Vec<_>>(),
vec![11, 21, 31, 61, 71, 81]
);
// and total voters
assert_eq!(
<Test as Config>::SortedListProvider::count() +
<Validators<Test>>::iter().count() as u32,
6
);
// we take 5 voters
assert_eq!(
Staking::electing_voters(Some(5))
.unwrap()
.iter()
.map(|(stash, _, _)| stash)
.copied()
.collect::<Vec<_>>(),
// then
vec![
31, 21, 11, // 3 nominators
101, 61 // 2 validators, and 71 is excluded
],
);
// roll to session 5
run_to_block(25);
// slash 21, the only validator nominated by 61
add_slash(&21);
// we take 4 voters
assert_eq!(
Staking::electing_voters(Some(4))
@@ -4236,10 +4204,24 @@ mod election_data_provider {
.map(|(stash, _, _)| stash)
.copied()
.collect::<Vec<_>>(),
vec![
31, 11, // 2 validators (21 was slashed)
101, 71 // 2 nominators, excluding 61
],
vec![11, 21, 31, 61],
);
// roll to session 5
run_to_block(25);
// slash 21, the only validator nominated by 61.
add_slash(&21);
// we take 4 voters; 71 and 81 are replacing the ejected ones.
assert_eq!(
Staking::electing_voters(Some(4))
.unwrap()
.iter()
.map(|(stash, _, _)| stash)
.copied()
.collect::<Vec<_>>(),
vec![11, 31, 71, 81],
);
});
}
@@ -4755,19 +4737,45 @@ mod sorted_list_provider {
fn re_nominate_does_not_change_counters_or_list() {
ExtBuilder::default().nominate(true).build_and_execute(|| {
// given
let pre_insert_nominator_count = Nominators::<Test>::iter().count() as u32;
assert_eq!(<Test as Config>::SortedListProvider::count(), pre_insert_nominator_count);
assert!(Nominators::<Test>::contains_key(101));
assert_eq!(<Test as Config>::SortedListProvider::iter().collect::<Vec<_>>(), vec![101]);
let pre_insert_voter_count =
(Nominators::<Test>::count() + Validators::<Test>::count()) as u32;
assert_eq!(<Test as Config>::VoterList::count(), pre_insert_voter_count);
assert_eq!(
<Test as Config>::VoterList::iter().collect::<Vec<_>>(),
vec![11, 21, 31, 101]
);
// when account 101 renominates
assert_ok!(Staking::nominate(Origin::signed(100), vec![41]));
// then counts don't change
assert_eq!(<Test as Config>::SortedListProvider::count(), pre_insert_nominator_count);
assert_eq!(Nominators::<Test>::iter().count() as u32, pre_insert_nominator_count);
assert_eq!(<Test as Config>::VoterList::count(), pre_insert_voter_count);
// and the list is the same
assert_eq!(<Test as Config>::SortedListProvider::iter().collect::<Vec<_>>(), vec![101]);
assert_eq!(
<Test as Config>::VoterList::iter().collect::<Vec<_>>(),
vec![11, 21, 31, 101]
);
});
}
#[test]
fn re_validate_does_not_change_counters_or_list() {
ExtBuilder::default().nominate(false).build_and_execute(|| {
// given
let pre_insert_voter_count =
(Nominators::<Test>::count() + Validators::<Test>::count()) as u32;
assert_eq!(<Test as Config>::VoterList::count(), pre_insert_voter_count);
assert_eq!(<Test as Config>::VoterList::iter().collect::<Vec<_>>(), vec![11, 21, 31]);
// when account 11 re-validates
assert_ok!(Staking::validate(Origin::signed(10), Default::default()));
// then counts don't change
assert_eq!(<Test as Config>::VoterList::count(), pre_insert_voter_count);
// and the list is the same
assert_eq!(<Test as Config>::VoterList::iter().collect::<Vec<_>>(), vec![11, 21, 31]);
});
}
}