Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Commit

Permalink
properly bound staking validators
Browse files Browse the repository at this point in the history
  • Loading branch information
kianenigma committed Sep 5, 2022
1 parent 4c83ee0 commit 4682f3f
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 33 deletions.
17 changes: 10 additions & 7 deletions frame/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,12 +299,12 @@ pub mod weights;

mod pallet;

use codec::{Decode, Encode, HasCompact};
use codec::{Decode, Encode, HasCompact, MaxEncodedLen};
use frame_support::{
parameter_types,
traits::{Currency, Defensive, Get},
weights::Weight,
BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
BoundedBTreeMap, BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
};
use scale_info::TypeInfo;
use sp_runtime::{
Expand Down Expand Up @@ -368,17 +368,20 @@ pub struct ActiveEraInfo {
/// Reward points of an era. Used to split era total payout between validators.
///
/// This points will be used to reward validators and their respective nominators.
#[derive(PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)]
pub struct EraRewardPoints<AccountId: Ord> {
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebugNoBound, CloneNoBound)]
#[cfg_attr(feature = "std", derive(frame_support::PartialEqNoBound))]
#[codec(mel_bound(T: Config))]
#[scale_info(skip_type_params(T))]
pub struct EraRewardPoints<T: Config> {
/// Total number of points. Equals the sum of reward points for each validator.
pub total: RewardPoint,
/// The reward points earned by a given validator.
pub individual: BTreeMap<AccountId, RewardPoint>,
pub individual: BoundedBTreeMap<T::AccountId, RewardPoint, T::MaxActiveValidators>,
}

impl<AccountId: Ord> Default for EraRewardPoints<AccountId> {
impl<T: Config> Default for EraRewardPoints<T> {
fn default() -> Self {
EraRewardPoints { total: Default::default(), individual: BTreeMap::new() }
EraRewardPoints { total: Default::default(), individual: BoundedBTreeMap::new() }
}
}

Expand Down
54 changes: 38 additions & 16 deletions frame/staking/src/pallet/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
use frame_election_provider_support::{
data_provider, ElectionDataProvider, ElectionProvider, ScoreProvider, SortedListProvider,
Supports, VoteWeight, VoterOf,
Support, VoteWeight, VoterOf,
};
use frame_support::{
pallet_prelude::*,
Expand All @@ -42,9 +42,9 @@ use sp_staking::{
use sp_std::{collections::btree_map::BTreeMap, prelude::*};

use crate::{
log, slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, EraPayout, Exposure, ExposureOf,
Forcing, IndividualExposure, Nominations, PositiveImbalanceOf, RewardDestination,
SessionInterface, StakingLedger, ValidatorPrefs,
log, slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, EraPayout, EraRewardPoints,
Exposure, ExposureOf, Forcing, IndividualExposure, Nominations, PositiveImbalanceOf,
RewardDestination, SessionInterface, StakingLedger, ValidatorPrefs,
};

use super::{pallet::*, STAKING_ID};
Expand Down Expand Up @@ -406,7 +406,10 @@ 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>>)>,
exposures: BoundedVec<
(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>),
T::MaxActiveValidators,
>,
) -> Vec<T::AccountId> {
// Increment or set current era.
let new_planned_era = CurrentEra::<T>::mutate(|s| {
Expand Down Expand Up @@ -434,7 +437,7 @@ impl<T: Config> Pallet<T> {
start_session_index: SessionIndex,
is_genesis: bool,
) -> Option<Vec<T::AccountId>> {
let election_result = if is_genesis {
let election_result: BoundedVec<_, T::MaxActiveValidators> = if is_genesis {
T::GenesisElectionProvider::elect().map_err(|e| {
log!(warn, "genesis election provider failed due to {:?}", e);
Self::deposit_event(Event::StakingElectionFailed);
Expand All @@ -445,6 +448,7 @@ impl<T: Config> Pallet<T> {
Self::deposit_event(Event::StakingElectionFailed);
})
}
.and_then(|er| er.try_into().defensive())
.ok()?;

let exposures = Self::collect_exposures(election_result);
Expand Down Expand Up @@ -482,7 +486,10 @@ 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>>),
T::MaxActiveValidators,
>,
new_planned_era: EraIndex,
) -> Vec<T::AccountId> {
let elected_stashes = exposures.iter().cloned().map(|(x, _)| x).collect::<Vec<_>>();
Expand Down Expand Up @@ -526,12 +533,13 @@ impl<T: Config> Pallet<T> {
/// Consume a set of [`Supports`] 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: BoundedVec<(T::AccountId, Support<T::AccountId>), T::MaxActiveValidators>,
) -> BoundedVec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>), T::MaxActiveValidators> {
let total_issuance = T::Currency::total_issuance();
let to_currency = |e: frame_election_provider_support::ExtendedBalance| {
T::CurrencyToVote::to_currency(e, total_issuance)
};
use sp_runtime::traits::TryCollect;

supports
.into_iter()
Expand All @@ -556,7 +564,8 @@ impl<T: Config> Pallet<T> {
let exposure = Exposure { own, others, total };
(validator, exposure)
})
.collect::<Vec<(T::AccountId, Exposure<_, _>)>>()
.try_collect()
.expect("`supports` is bounded, `map` does not change the length of iterator, outcome is bounded; qed.")
}

/// Remove all associated data of a stash account from the staking system.
Expand Down Expand Up @@ -625,12 +634,25 @@ impl<T: Config> Pallet<T> {
/// COMPLEXITY: Complexity is `number_of_validator_to_reward x current_elected_len`.
pub fn reward_by_ids(validators_points: impl IntoIterator<Item = (T::AccountId, u32)>) {
if let Some(active_era) = Self::active_era() {
<ErasRewardPoints<T>>::mutate(active_era.index, |era_rewards| {
for (validator, points) in validators_points.into_iter() {
*era_rewards.individual.entry(validator).or_default() += points;
era_rewards.total += points;
}
});
<ErasRewardPoints<T>>::mutate(
active_era.index,
|era_rewards: &mut EraRewardPoints<T>| {
for (validator, points) in validators_points.into_iter() {
if let Some(i) = era_rewards.individual.get_mut(&validator) {
*i += points;
era_rewards.total += points;
} else {
let _ = era_rewards
.individual
.try_insert(validator, points)
.map(|_| {
era_rewards.total += points;
})
.defensive();
}
}
},
);
}
}

Expand Down
56 changes: 46 additions & 10 deletions frame/staking/src/pallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ pub mod pallet {
/// other pallets exist that are affected by slashing per-staker.
type OnStakerSlash: sp_staking::OnStakerSlash<Self::AccountId, BalanceOf<Self>>;

/// The maximum number of validators that this pallet can have.
///
/// If more validators are provided by the election provider, they are silently dropped.
// TODO: once we have something like https://github.com/paritytech/substrate/pull/11499, we can have better guarantees here.
type MaxActiveValidators: Get<u32>;

/// Some parameters of the benchmarking.
type BenchmarkingConfig: BenchmarkingConfig;

Expand All @@ -219,6 +225,8 @@ pub mod pallet {
pub(crate) type HistoryDepth<T> = StorageValue<_, u32, ValueQuery, HistoryDepthOnEmpty>;

/// The ideal number of staking participants.
///
/// Invariant: This number can never be more than [`Config::MaxActiveValidators`].
#[pallet::storage]
#[pallet::getter(fn validator_count)]
pub type ValidatorCount<T> = StorageValue<_, u32, ValueQuery>;
Expand All @@ -233,7 +241,8 @@ pub mod pallet {
/// invulnerables) and restricted to testnets.
#[pallet::storage]
#[pallet::getter(fn invulnerables)]
pub type Invulnerables<T: Config> = StorageValue<_, Vec<T::AccountId>, ValueQuery>;
pub type Invulnerables<T: Config> =
StorageValue<_, BoundedVec<T::AccountId, T::MaxActiveValidators>, ValueQuery>;

/// Map from all locked "stash" accounts to the controller account.
#[pallet::storage]
Expand Down Expand Up @@ -334,6 +343,9 @@ pub mod pallet {
///
/// Is it removed after `HISTORY_DEPTH` eras.
/// If stakers hasn't been set or has been removed then empty exposure is returned.
///
/// Invariant: the number of keys in this map is always less than
/// [`Config::MaxActiveValidators`].
#[pallet::storage]
#[pallet::getter(fn eras_stakers)]
pub type ErasStakers<T: Config> = StorageDoubleMap<
Expand All @@ -357,6 +369,9 @@ pub mod pallet {
///
/// Is it removed after `HISTORY_DEPTH` eras.
/// If stakers hasn't been set or has been removed then empty exposure is returned.
///
/// Invariant: the number of keys in this map is always less than
/// [`Config::MaxActiveValidators`].
#[pallet::storage]
#[pallet::getter(fn eras_stakers_clipped)]
pub type ErasStakersClipped<T: Config> = StorageDoubleMap<
Expand All @@ -374,7 +389,11 @@ pub mod pallet {
/// This is keyed first by the era index to allow bulk deletion and then the stash account.
///
/// Is it removed after `HISTORY_DEPTH` eras.
// If prefs hasn't been set or has been removed then 0 commission is returned.
///
/// If prefs hasn't been set or has been removed then 0 commission is returned.
///
/// Invariant: the number of keys in this map is always less than
/// [`Config::MaxActiveValidators`].
#[pallet::storage]
#[pallet::getter(fn eras_validator_prefs)]
pub type ErasValidatorPrefs<T: Config> = StorageDoubleMap<
Expand All @@ -390,6 +409,9 @@ pub mod pallet {
/// The total validator era payout for the last `HISTORY_DEPTH` eras.
///
/// Eras that haven't finished yet or has been removed doesn't have reward.
///
/// Invariant: the number of keys in this map is always less than
/// [`Config::MaxActiveValidators`].
#[pallet::storage]
#[pallet::getter(fn eras_validator_reward)]
pub type ErasValidatorReward<T: Config> = StorageMap<_, Twox64Concat, EraIndex, BalanceOf<T>>;
Expand All @@ -399,7 +421,7 @@ pub mod pallet {
#[pallet::storage]
#[pallet::getter(fn eras_reward_points)]
pub type ErasRewardPoints<T: Config> =
StorageMap<_, Twox64Concat, EraIndex, EraRewardPoints<T::AccountId>, ValueQuery>;
StorageMap<_, Twox64Concat, EraIndex, EraRewardPoints<T>, ValueQuery>;

/// The total amount staked for the last `HISTORY_DEPTH` eras.
/// If total hasn't been set or has been removed then 0 stake is returned.
Expand Down Expand Up @@ -493,14 +515,14 @@ pub mod pallet {
/// `OffendingValidatorsThreshold` is reached. The vec is always kept sorted so that we can find
/// whether a given validator has previously offended using binary search. It gets cleared when
/// the era ends.
// TODO: this should also be bounded, but since it is in the slashing code, we might want to do
// it later.
#[pallet::storage]
#[pallet::getter(fn offending_validators)]
pub type OffendingValidators<T: Config> = StorageValue<_, Vec<(u32, bool)>, ValueQuery>;

/// True if network has been upgraded to this version.
/// Storage version of the pallet.
///
/// This is set to v7.0.0 for new networks.
#[pallet::storage]
pub(crate) type StorageVersion<T: Config> = StorageValue<_, Releases, ValueQuery>;

Expand All @@ -515,7 +537,7 @@ pub mod pallet {
pub history_depth: u32,
pub validator_count: u32,
pub minimum_validator_count: u32,
pub invulnerables: Vec<T::AccountId>,
pub invulnerables: BoundedVec<T::AccountId, T::MaxActiveValidators>,
pub force_era: Forcing,
pub slash_reward_fraction: Perbill,
pub canceled_payout: BalanceOf<T>,
Expand Down Expand Up @@ -551,7 +573,10 @@ pub mod pallet {
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
fn build(&self) {
HistoryDepth::<T>::put(self.history_depth);

assert!(self.validator_count <= T::MaxActiveValidators::get());
ValidatorCount::<T>::put(self.validator_count);

MinimumValidatorCount::<T>::put(self.minimum_validator_count);
Invulnerables::<T>::put(&self.invulnerables);
ForceEra::<T>::put(self.force_era);
Expand Down Expand Up @@ -701,6 +726,8 @@ pub mod pallet {
TooManyValidators,
/// Commission is too low. Must be at least `MinCommission`.
CommissionTooLow,
/// Some bound is not met.
BoundNotMet,
}

#[pallet::hooks]
Expand Down Expand Up @@ -1206,6 +1233,7 @@ pub mod pallet {
#[pallet::compact] new: u32,
) -> DispatchResult {
ensure_root(origin)?;
ensure!(new <= T::MaxActiveValidators::get(), Error::<T>::BoundNotMet);
ValidatorCount::<T>::put(new);
Ok(())
}
Expand All @@ -1223,8 +1251,11 @@ pub mod pallet {
#[pallet::compact] additional: u32,
) -> DispatchResult {
ensure_root(origin)?;
ValidatorCount::<T>::mutate(|n| *n += additional);
Ok(())
ValidatorCount::<T>::try_mutate(|n| {
*n += additional;
ensure!(*n <= T::MaxActiveValidators::get(), Error::<T>::BoundNotMet);
Ok(())
})
}

/// Scale up the ideal number of validators by a factor.
Expand All @@ -1237,8 +1268,11 @@ 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);
Ok(())
ValidatorCount::<T>::mutate(|n| {
*n += factor * *n;
ensure!(*n <= T::MaxActiveValidators::get(), Error::<T>::BoundNotMet);
Ok(())
})
}

/// Force there to be no new eras indefinitely.
Expand Down Expand Up @@ -1295,6 +1329,8 @@ pub mod pallet {
invulnerables: Vec<T::AccountId>,
) -> DispatchResult {
ensure_root(origin)?;
let invulnerables: BoundedVec<T::AccountId, T::MaxActiveValidators> =
invulnerables.try_into().map_err(|_| Error::<T>::BoundNotMet)?;
<Invulnerables<T>>::put(invulnerables);
Ok(())
}
Expand Down

0 comments on commit 4682f3f

Please sign in to comment.