Add Control to Growth of the Staking Pallet (#8920)

* start count

* track count

* add max limit

* min bonds for participating

* respect min bond when unbonding

* revert a bit of u32

* fix merge

* more merge fixes

* update to `Current*`

* add helper functions

* Update frame/staking/src/lib.rs

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

* fix

* minbond as storage

* checkpoint

* chill_other

* better bond tracking

* MinBond to MinNominatorBond

* better doc

* use helper function

* oops

* simple hard limits to validators / nominators.

* better doc

* update storage version

* fix tests

* enable migrations

* min bond tests

* chill other tests

* tests for max cap

* check `None` on cap too

* benchmarks

* Update frame/staking/src/lib.rs

* Update frame/staking/src/lib.rs

Co-authored-by: Zeke Mostov <32168567+emostov@users.noreply.github.com>

* Update frame/staking/src/lib.rs

Co-authored-by: Zeke Mostov <32168567+emostov@users.noreply.github.com>

* Update frame/staking/src/tests.rs

Co-authored-by: Zeke Mostov <32168567+emostov@users.noreply.github.com>

* fix benchmark

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_staking --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/staking/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* nits

* fix reap_stash benchmark

* remove lower bound to min bond

Co-authored-by: kianenigma <kian@parity.io>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Co-authored-by: Parity Bot <admin@parity.io>
Co-authored-by: Zeke Mostov <32168567+emostov@users.noreply.github.com>
This commit is contained in:
Shawn Tabrizi
2021-06-16 05:57:14 +01:00
committed by GitHub
parent 58e837fcd3
commit 36ac9111dd
6 changed files with 734 additions and 290 deletions
+263 -32
View File
@@ -745,17 +745,46 @@ enum Releases {
V4_0_0,
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
}
impl Default for Releases {
fn default() -> Self {
Releases::V6_0_0
Releases::V7_0_0
}
}
pub mod migrations {
use super::*;
pub mod v7 {
use super::*;
pub fn pre_migrate<T: Config>() -> Result<(), &'static str> {
assert!(CurrentValidatorsCount::<T>::get().is_zero(), "CurrentValidatorsCount already set.");
assert!(CurrentNominatorsCount::<T>::get().is_zero(), "CurrentNominatorsCount already set.");
assert!(StorageVersion::<T>::get() == Releases::V6_0_0);
Ok(())
}
pub fn migrate<T: Config>() -> Weight {
log!(info, "Migrating staking to Releases::V7_0_0");
let validator_count = Validators::<T>::iter().count() as u32;
let nominator_count = Nominators::<T>::iter().count() as u32;
CurrentValidatorsCount::<T>::put(validator_count);
CurrentNominatorsCount::<T>::put(nominator_count);
StorageVersion::<T>::put(Releases::V7_0_0);
log!(info, "Completed staking migration to Releases::V7_0_0");
T::DbWeight::get().reads_writes(
validator_count.saturating_add(nominator_count).into(),
2,
)
}
}
pub mod v6 {
use super::*;
use frame_support::{traits::Get, weights::Weight, generate_storage_alias};
@@ -940,6 +969,14 @@ pub mod pallet {
#[pallet::getter(fn bonded)]
pub type Bonded<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, T::AccountId>;
/// The minimum active bond to become and maintain the role of a nominator.
#[pallet::storage]
pub type MinNominatorBond<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
/// The minimum active bond to become and maintain the role of a validator.
#[pallet::storage]
pub type MinValidatorBond<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
/// Map from all (unlocked) "controller" accounts to the info regarding the staking.
#[pallet::storage]
#[pallet::getter(fn ledger)]
@@ -960,15 +997,39 @@ pub mod pallet {
>;
/// The map from (wannabe) validator stash key to the preferences of that validator.
///
/// When updating this storage item, you must also update the `CurrentValidatorsCount`.
#[pallet::storage]
#[pallet::getter(fn validators)]
pub type Validators<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, ValidatorPrefs, ValueQuery>;
/// A tracker to keep count of the number of items in the `Validators` map.
#[pallet::storage]
pub type CurrentValidatorsCount<T> = StorageValue<_, u32, ValueQuery>;
/// The maximum validator count before we stop allowing new validators to join.
///
/// When this value is not set, no limits are enforced.
#[pallet::storage]
pub type MaxValidatorsCount<T> = StorageValue<_, u32, OptionQuery>;
/// The map from nominator stash key to the set of stash keys of all validators to nominate.
///
/// When updating this storage item, you must also update the `CurrentNominatorsCount`.
#[pallet::storage]
#[pallet::getter(fn nominators)]
pub type Nominators<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, Nominations<T::AccountId>>;
/// A tracker to keep count of the number of items in the `Nominators` map.
#[pallet::storage]
pub type CurrentNominatorsCount<T> = StorageValue<_, u32, ValueQuery>;
/// The maximum nominator count before we stop allowing new validators to join.
///
/// When this value is not set, no limits are enforced.
#[pallet::storage]
pub type MaxNominatorsCount<T> = StorageValue<_, u32, OptionQuery>;
/// The current era index.
///
/// This is the latest planned era, depending on how the Session pallet queues the validator
@@ -1165,6 +1226,8 @@ pub mod pallet {
pub slash_reward_fraction: Perbill,
pub canceled_payout: BalanceOf<T>,
pub stakers: Vec<(T::AccountId, T::AccountId, BalanceOf<T>, StakerStatus<T::AccountId>)>,
pub min_nominator_bond: BalanceOf<T>,
pub min_validator_bond: BalanceOf<T>,
}
#[cfg(feature = "std")]
@@ -1179,6 +1242,8 @@ pub mod pallet {
slash_reward_fraction: Default::default(),
canceled_payout: Default::default(),
stakers: Default::default(),
min_nominator_bond: Default::default(),
min_validator_bond: Default::default(),
}
}
}
@@ -1194,6 +1259,8 @@ pub mod pallet {
CanceledSlashPayout::<T>::put(self.canceled_payout);
SlashRewardFraction::<T>::put(self.slash_reward_fraction);
StorageVersion::<T>::put(Releases::V6_0_0);
MinNominatorBond::<T>::put(self.min_nominator_bond);
MinValidatorBond::<T>::put(self.min_validator_bond);
for &(ref stash, ref controller, balance, ref status) in &self.stakers {
assert!(
@@ -1274,8 +1341,8 @@ pub mod pallet {
DuplicateIndex,
/// Slash record index out of bounds.
InvalidSlashIndex,
/// Can not bond with value less than minimum balance.
InsufficientValue,
/// Can not bond with value less than minimum required.
InsufficientBond,
/// Can not schedule more unlock chunks.
NoMoreChunks,
/// Can not rebond without unlocking chunks.
@@ -1300,18 +1367,35 @@ pub mod pallet {
TooManyTargets,
/// A nomination target was supplied that was blocked or otherwise not a validator.
BadTarget,
/// The user has enough bond and thus cannot be chilled forcefully by an external person.
CannotChillOther,
/// 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.
TooManyValidators,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_runtime_upgrade() -> Weight {
if StorageVersion::<T>::get() == Releases::V5_0_0 {
migrations::v6::migrate::<T>()
if StorageVersion::<T>::get() == Releases::V6_0_0 {
migrations::v7::migrate::<T>()
} else {
T::DbWeight::get().reads(1)
}
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<(), &'static str> {
if StorageVersion::<T>::get() == Releases::V6_0_0 {
migrations::v7::pre_migrate::<T>()
} else {
Ok(())
}
}
fn on_initialize(_now: BlockNumberFor<T>) -> Weight {
// just return the weight of the on_finalize.
T::DbWeight::get().reads(1)
@@ -1389,7 +1473,7 @@ pub mod pallet {
// Reject a bond which is considered to be _dust_.
if value < T::Currency::minimum_balance() {
Err(Error::<T>::InsufficientValue)?
Err(Error::<T>::InsufficientBond)?
}
frame_system::Pallet::<T>::inc_consumers(&stash).map_err(|_| Error::<T>::BadState)?;
@@ -1454,7 +1538,7 @@ pub mod pallet {
ledger.total += extra;
ledger.active += extra;
// Last check: the new active amount of ledger must be more than ED.
ensure!(ledger.active >= T::Currency::minimum_balance(), Error::<T>::InsufficientValue);
ensure!(ledger.active >= T::Currency::minimum_balance(), Error::<T>::InsufficientBond);
Self::deposit_event(Event::<T>::Bonded(stash, extra));
Self::update_ledger(&controller, &ledger);
@@ -1473,6 +1557,9 @@ pub mod pallet {
/// can co-exists at the same time. In that case, [`Call::withdraw_unbonded`] need
/// to be called first to remove some of the chunks (if possible).
///
/// If a user encounters the `InsufficientBond` error when calling this extrinsic,
/// they should call `chill` first in order to free up their bonded funds.
///
/// The dispatch origin for this call must be _Signed_ by the controller, not the stash.
/// And, it can be only called when [`EraElectionStatus`] is `Closed`.
///
@@ -1514,6 +1601,18 @@ pub mod pallet {
ledger.active = Zero::zero();
}
let min_active_bond = if Nominators::<T>::contains_key(&ledger.stash) {
MinNominatorBond::<T>::get()
} else if Validators::<T>::contains_key(&ledger.stash) {
MinValidatorBond::<T>::get()
} else {
Zero::zero()
};
// Make sure that the user maintains enough active bond for their role.
// If a user runs into this error, they should chill first.
ensure!(ledger.active >= min_active_bond, Error::<T>::InsufficientBond);
// Note: in case there is no current era it is fine to bond one era more.
let era = Self::current_era().unwrap_or(0) + T::BondingDuration::get();
ledger.unlocking.push(UnlockChunk { value, era });
@@ -1614,10 +1713,19 @@ pub mod pallet {
#[pallet::weight(T::WeightInfo::validate())]
pub fn validate(origin: OriginFor<T>, prefs: ValidatorPrefs) -> DispatchResult {
let controller = ensure_signed(origin)?;
// If this error is reached, we need to adjust the `MinValidatorBond` and start calling `chill_other`.
// Until then, we explicitly block new validators to protect the runtime.
if let Some(max_validators) = MaxValidatorsCount::<T>::get() {
ensure!(CurrentValidatorsCount::<T>::get() < max_validators, Error::<T>::TooManyValidators);
}
let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?;
ensure!(ledger.active >= MinValidatorBond::<T>::get(), Error::<T>::InsufficientBond);
let stash = &ledger.stash;
<Nominators<T>>::remove(stash);
<Validators<T>>::insert(stash, prefs);
Self::do_remove_nominator(stash);
Self::do_add_validator(stash, prefs);
Ok(())
}
@@ -1646,7 +1754,16 @@ pub mod pallet {
targets: Vec<<T::Lookup as StaticLookup>::Source>,
) -> DispatchResult {
let controller = ensure_signed(origin)?;
// If this error is reached, we need to adjust the `MinNominatorBond` and start calling `chill_other`.
// Until then, we explicitly block new nominators to protect the runtime.
if let Some(max_nominators) = MaxNominatorsCount::<T>::get() {
ensure!(CurrentNominatorsCount::<T>::get() < max_nominators, Error::<T>::TooManyNominators);
}
let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?;
ensure!(ledger.active >= MinNominatorBond::<T>::get(), Error::<T>::InsufficientBond);
let stash = &ledger.stash;
ensure!(!targets.is_empty(), Error::<T>::EmptyTargets);
ensure!(targets.len() <= T::MAX_NOMINATIONS as usize, Error::<T>::TooManyTargets);
@@ -1669,8 +1786,8 @@ pub mod pallet {
suppressed: false,
};
<Validators<T>>::remove(stash);
<Nominators<T>>::insert(stash, &nominations);
Self::do_remove_validator(stash);
Self::do_add_nominator(stash, nominations);
Ok(())
}
@@ -2022,7 +2139,7 @@ pub mod pallet {
let ledger = ledger.rebond(value);
// Last check: the new active amount of ledger must be more than ED.
ensure!(ledger.active >= T::Currency::minimum_balance(), Error::<T>::InsufficientValue);
ensure!(ledger.active >= T::Currency::minimum_balance(), Error::<T>::InsufficientBond);
Self::deposit_event(Event::<T>::Bonded(ledger.stash.clone(), value));
Self::update_ledger(&controller, &ledger);
@@ -2135,6 +2252,80 @@ pub mod pallet {
Ok(())
}
/// Update the various staking limits this pallet.
///
/// * `min_nominator_bond`: The minimum active bond needed to be a nominator.
/// * `min_validator_bond`: The minimum active bond needed to be a validator.
/// * `max_nominator_count`: The max number of users who can be a nominator at once.
/// When set to `None`, no limit is enforced.
/// * `max_validator_count`: The max number of users who can be a validator at once.
/// When set to `None`, no limit is enforced.
///
/// Origin must be Root to call this function.
///
/// NOTE: Existing nominators and validators will not be affected by this update.
/// to kick people under the new limits, `chill_other` should be called.
#[pallet::weight(T::WeightInfo::update_staking_limits())]
pub fn update_staking_limits(
origin: OriginFor<T>,
min_nominator_bond: BalanceOf<T>,
min_validator_bond: BalanceOf<T>,
max_nominator_count: Option<u32>,
max_validator_count: Option<u32>,
) -> DispatchResult {
ensure_root(origin)?;
MinNominatorBond::<T>::set(min_nominator_bond);
MinValidatorBond::<T>::set(min_validator_bond);
MaxNominatorsCount::<T>::set(max_nominator_count);
MaxValidatorsCount::<T>::set(max_validator_count);
Ok(())
}
/// Declare a `controller` as having no desire to either validator or nominate.
///
/// Effects will be felt at the beginning of the next era.
///
/// The dispatch origin for this call must be _Signed_, but can be called by anyone.
///
/// If the caller is the same as the controller being targeted, then no further checks
/// are enforced. However, this call can also be made by an third party user who witnesses
/// that this controller does not satisfy the minimum bond requirements to be in their role.
///
/// This can be helpful if bond requirements are updated, and we need to remove old users
/// who do not satisfy these requirements.
///
// TODO: Maybe we can deprecate `chill` in the future.
// https://github.com/paritytech/substrate/issues/9111
#[pallet::weight(T::WeightInfo::chill_other())]
pub fn chill_other(
origin: OriginFor<T>,
controller: T::AccountId,
) -> DispatchResult {
// Anyone can call this function.
let caller = ensure_signed(origin)?;
let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?;
let stash = ledger.stash;
// If the caller is not the controller, we want to check that the minimum bond
// requirements are not satisfied, and thus we have reason to chill this user.
//
// Otherwise, if caller is the same as the controller, this is just like `chill`.
if caller != controller {
let min_active_bond = if Nominators::<T>::contains_key(&stash) {
MinNominatorBond::<T>::get()
} else if Validators::<T>::contains_key(&stash) {
MinValidatorBond::<T>::get()
} else {
Zero::zero()
};
ensure!(ledger.active < min_active_bond, Error::<T>::CannotChillOther);
}
Self::chill_stash(&stash);
Ok(())
}
}
}
@@ -2296,8 +2487,8 @@ impl<T: Config> Pallet<T> {
/// Chill a stash account.
fn chill_stash(stash: &T::AccountId) {
<Validators<T>>::remove(stash);
<Nominators<T>>::remove(stash);
Self::do_remove_validator(stash);
Self::do_remove_nominator(stash);
}
/// Actually make a payment to a staker. This uses the currency's reward function
@@ -2645,8 +2836,8 @@ impl<T: Config> Pallet<T> {
<Ledger<T>>::remove(&controller);
<Payee<T>>::remove(stash);
<Validators<T>>::remove(stash);
<Nominators<T>>::remove(stash);
Self::do_remove_validator(stash);
Self::do_remove_nominator(stash);
frame_system::Pallet::<T>::dec_consumers(stash);
@@ -2749,7 +2940,7 @@ impl<T: Config> Pallet<T> {
// Collect all slashing spans into a BTreeMap for further queries.
let slashing_spans = <SlashingSpans<T>>::iter().collect::<BTreeMap<_, _>>();
for (nominator, nominations) in <Nominators<T>>::iter() {
for (nominator, nominations) in Nominators::<T>::iter() {
let Nominations { submitted_in, mut targets, suppressed: _ } = nominations;
// Filter out nomination targets which were nominated before the most recent
@@ -2769,8 +2960,49 @@ impl<T: Config> Pallet<T> {
all_voters
}
/// This is a very expensive function and result should be cached versus being called multiple times.
pub fn get_npos_targets() -> Vec<T::AccountId> {
<Validators<T>>::iter().map(|(v, _)| v).collect::<Vec<_>>()
Validators::<T>::iter().map(|(v, _)| v).collect::<Vec<_>>()
}
/// This function will add a nominator to the `Nominators` storage map,
/// and keep track of the `CurrentNominatorsCount`.
///
/// If the nominator already exists, their nominations will be updated.
pub fn do_add_nominator(who: &T::AccountId, nominations: Nominations<T::AccountId>) {
if !Nominators::<T>::contains_key(who) {
CurrentNominatorsCount::<T>::mutate(|x| x.saturating_inc())
}
Nominators::<T>::insert(who, nominations);
}
/// This function will remove a nominator from the `Nominators` storage map,
/// and keep track of the `CurrentNominatorsCount`.
pub fn do_remove_nominator(who: &T::AccountId) {
if Nominators::<T>::contains_key(who) {
Nominators::<T>::remove(who);
CurrentNominatorsCount::<T>::mutate(|x| x.saturating_dec());
}
}
/// This function will add a validator to the `Validators` storage map,
/// and keep track of the `CurrentValidatorsCount`.
///
/// If the validator already exists, their preferences will be updated.
pub fn do_add_validator(who: &T::AccountId, prefs: ValidatorPrefs) {
if !Validators::<T>::contains_key(who) {
CurrentValidatorsCount::<T>::mutate(|x| x.saturating_inc())
}
Validators::<T>::insert(who, prefs);
}
/// This function will remove a validator from the `Validators` storage map,
/// and keep track of the `CurrentValidatorsCount`.
pub fn do_remove_validator(who: &T::AccountId) {
if Validators::<T>::contains_key(who) {
Validators::<T>::remove(who);
CurrentValidatorsCount::<T>::mutate(|x| x.saturating_dec());
}
}
}
@@ -2785,12 +3017,11 @@ impl<T: Config> frame_election_provider_support::ElectionDataProvider<T::Account
fn voters(
maybe_max_len: Option<usize>,
) -> data_provider::Result<(Vec<(T::AccountId, VoteWeight, Vec<T::AccountId>)>, Weight)> {
// NOTE: reading these counts already needs to iterate a lot of storage keys, but they get
// cached. This is okay for the case of `Ok(_)`, but bad for `Err(_)`, as the trait does not
// report weight in failures.
let nominator_count = <Nominators<T>>::iter().count();
let validator_count = <Validators<T>>::iter().count();
let voter_count = nominator_count.saturating_add(validator_count);
let nominator_count = CurrentNominatorsCount::<T>::get();
let validator_count = CurrentValidatorsCount::<T>::get();
let voter_count = nominator_count.saturating_add(validator_count) as usize;
debug_assert!(<Nominators<T>>::iter().count() as u32 == CurrentNominatorsCount::<T>::get());
debug_assert!(<Validators<T>>::iter().count() as u32 == CurrentValidatorsCount::<T>::get());
if maybe_max_len.map_or(false, |max_len| voter_count > max_len) {
return Err("Voter snapshot too big");
@@ -2798,15 +3029,15 @@ impl<T: Config> frame_election_provider_support::ElectionDataProvider<T::Account
let slashing_span_count = <SlashingSpans<T>>::iter().count();
let weight = T::WeightInfo::get_npos_voters(
validator_count as u32,
nominator_count as u32,
nominator_count,
validator_count,
slashing_span_count as u32,
);
Ok((Self::get_npos_voters(), weight))
}
fn targets(maybe_max_len: Option<usize>) -> data_provider::Result<(Vec<T::AccountId>, Weight)> {
let target_count = <Validators<T>>::iter().count();
let target_count = CurrentValidatorsCount::<T>::get() as usize;
if maybe_max_len.map_or(false, |max_len| target_count > max_len) {
return Err("Target snapshot too big");
@@ -2859,7 +3090,7 @@ impl<T: Config> frame_election_provider_support::ElectionDataProvider<T::Account
targets.into_iter().for_each(|v| {
let stake: BalanceOf<T> = target_stake
.and_then(|w| <BalanceOf<T>>::try_from(w).ok())
.unwrap_or(T::Currency::minimum_balance() * 100u32.into());
.unwrap_or(MinNominatorBond::<T>::get() * 100u32.into());
<Bonded<T>>::insert(v.clone(), v.clone());
<Ledger<T>>::insert(
v.clone(),
@@ -2871,8 +3102,8 @@ impl<T: Config> frame_election_provider_support::ElectionDataProvider<T::Account
claimed_rewards: vec![],
},
);
<Validators<T>>::insert(
v,
Self::do_add_validator(
&v,
ValidatorPrefs { commission: Perbill::zero(), blocked: false },
);
});
@@ -2892,8 +3123,8 @@ impl<T: Config> frame_election_provider_support::ElectionDataProvider<T::Account
claimed_rewards: vec![],
},
);
<Nominators<T>>::insert(
v,
Self::do_add_nominator(
&v,
Nominations { targets: t, submitted_in: 0, suppressed: false },
);
});