Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pallet-staking] Clean up stale non paged exposure storages in the pallet #5986

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion polkadot/runtime/common/src/try_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ where

let all_stakers = Ledger::<T>::iter().map(|(ctrl, l)| (ctrl, l.stash)).collect::<BTreeSet<_>>();
let mut all_exposed = BTreeSet::new();
ErasStakers::<T>::iter().for_each(|(_, val, expo)| {
ErasStakersPaged::<T>::iter().for_each(|((_era, val, _page), expo)| {
all_exposed.insert(val);
all_exposed.extend(expo.others.iter().map(|ie| ie.who.clone()))
});
Expand Down
87 changes: 11 additions & 76 deletions substrate/frame/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1039,52 +1039,16 @@ where
pub struct EraInfo<T>(core::marker::PhantomData<T>);
impl<T: Config> EraInfo<T> {
/// Returns true if validator has one or more page of era rewards not claimed yet.
// Also looks at legacy storage that can be cleaned up after #433.
pub fn pending_rewards(era: EraIndex, validator: &T::AccountId) -> bool {
let page_count = if let Some(overview) = <ErasStakersOverview<T>>::get(&era, validator) {
overview.page_count
} else {
if <ErasStakers<T>>::contains_key(era, validator) {
// this means non paged exposure, and we treat them as single paged.
1
} else {
// if no exposure, then no rewards to claim.
return false
}
};

// check if era is marked claimed in legacy storage.
if <Ledger<T>>::get(validator)
.map(|l| l.legacy_claimed_rewards.contains(&era))
.unwrap_or_default()
{
return false
}

ClaimedRewards::<T>::get(era, validator).len() < page_count as usize
}

/// Temporary function which looks at both (1) passed param `T::StakingLedger` for legacy
/// non-paged rewards, and (2) `T::ClaimedRewards` for paged rewards. This function can be
/// removed once `T::HistoryDepth` eras have passed and none of the older non-paged rewards
/// are relevant/claimable.
// Refer tracker issue for cleanup: https://github.com/paritytech/polkadot-sdk/issues/433
pub(crate) fn is_rewards_claimed_with_legacy_fallback(
era: EraIndex,
ledger: &StakingLedger<T>,
validator: &T::AccountId,
page: Page,
) -> bool {
ledger.legacy_claimed_rewards.binary_search(&era).is_ok() ||
Self::is_rewards_claimed(era, validator, page)
<ErasStakersOverview<T>>::get(&era, validator)
.map(|overview| {
ClaimedRewards::<T>::get(era, validator).len() < overview.page_count as usize
})
.unwrap_or(false)
}

/// Check if the rewards for the given era and page index have been claimed.
///
/// This is only used for paged rewards. Once older non-paged rewards are no longer
/// relevant, `is_rewards_claimed_with_legacy_fallback` can be removed and this function can
/// be made public.
fn is_rewards_claimed(era: EraIndex, validator: &T::AccountId, page: Page) -> bool {
pub(crate) fn is_rewards_claimed(era: EraIndex, validator: &T::AccountId, page: Page) -> bool {
ClaimedRewards::<T>::get(era, validator).contains(&page)
}

Expand All @@ -1097,20 +1061,7 @@ impl<T: Config> EraInfo<T> {
validator: &T::AccountId,
page: Page,
) -> Option<PagedExposure<T::AccountId, BalanceOf<T>>> {
let overview = <ErasStakersOverview<T>>::get(&era, validator);

// return clipped exposure if page zero and paged exposure does not exist
// exists for backward compatibility and can be removed as part of #13034
if overview.is_none() && page == 0 {
return Some(PagedExposure::from_clipped(<ErasStakersClipped<T>>::get(era, validator)))
}

// no exposure for this validator
if overview.is_none() {
return None
}

let overview = overview.expect("checked above; qed");
let overview = <ErasStakersOverview<T>>::get(&era, validator)?;

// validator stake is added only in page zero
let validator_stake = if page == 0 { overview.own } else { Zero::zero() };
Expand All @@ -1127,14 +1078,16 @@ impl<T: Config> EraInfo<T> {
}

/// Get full exposure of the validator at a given era.
///
/// Builds up the full exposure of the validator by combining all the pages of exposure.
pub fn get_full_exposure(
era: EraIndex,
validator: &T::AccountId,
) -> Exposure<T::AccountId, BalanceOf<T>> {
let overview = <ErasStakersOverview<T>>::get(&era, validator);

if overview.is_none() {
return ErasStakers::<T>::get(era, validator)
return Exposure::default()
}

let overview = overview.expect("checked above; qed");
Expand Down Expand Up @@ -1168,20 +1121,7 @@ impl<T: Config> EraInfo<T> {
}

/// Returns the next page that can be claimed or `None` if nothing to claim.
pub(crate) fn get_next_claimable_page(
era: EraIndex,
validator: &T::AccountId,
ledger: &StakingLedger<T>,
) -> Option<Page> {
if Self::is_non_paged_exposure(era, validator) {
return match ledger.legacy_claimed_rewards.binary_search(&era) {
// already claimed
Ok(_) => None,
// Non-paged exposure is considered as a single page
Err(_) => Some(0),
}
}

pub(crate) fn get_next_claimable_page(era: EraIndex, validator: &T::AccountId) -> Option<Page> {
// Find next claimable page of paged exposure.
let page_count = Self::get_page_count(era, validator);
let all_claimable_pages: Vec<Page> = (0..page_count).collect();
Expand All @@ -1190,11 +1130,6 @@ impl<T: Config> EraInfo<T> {
all_claimable_pages.into_iter().find(|p| !claimed_pages.contains(p))
}

/// Checks if exposure is paged or not.
fn is_non_paged_exposure(era: EraIndex, validator: &T::AccountId) -> bool {
<ErasStakersClipped<T>>::contains_key(&era, validator)
}

/// Returns validator commission for this era and page.
pub(crate) fn get_validator_commission(
era: EraIndex,
Expand Down
55 changes: 8 additions & 47 deletions substrate/frame/staking/src/pallet/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,8 @@ impl<T: Config> Pallet<T> {
validator_stash: T::AccountId,
era: EraIndex,
) -> DispatchResultWithPostInfo {
let controller = Self::bonded(&validator_stash).ok_or_else(|| {
Error::<T>::NotStash.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
})?;

let ledger = Self::ledger(StakingAccount::Controller(controller))?;
let page = EraInfo::<T>::get_next_claimable_page(era, &validator_stash, &ledger)
.ok_or_else(|| {
let page =
EraInfo::<T>::get_next_claimable_page(era, &validator_stash).ok_or_else(|| {
Error::<T>::AlreadyClaimed
.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
})?;
Expand Down Expand Up @@ -294,13 +289,12 @@ impl<T: Config> Pallet<T> {

let stash = ledger.stash.clone();

if EraInfo::<T>::is_rewards_claimed_with_legacy_fallback(era, &ledger, &stash, page) {
if EraInfo::<T>::is_rewards_claimed(era, &stash, page) {
return Err(Error::<T>::AlreadyClaimed
.with_weight(T::WeightInfo::payout_stakers_alive_staked(0)))
} else {
EraInfo::<T>::set_rewards_as_claimed(era, &stash, page);
}

EraInfo::<T>::set_rewards_as_claimed(era, &stash, page);
let exposure = EraInfo::<T>::get_paged_exposure(era, &stash, page).ok_or_else(|| {
Error::<T>::InvalidEraToReward
.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
Expand Down Expand Up @@ -806,11 +800,7 @@ impl<T: Config> Pallet<T> {
pub(crate) fn clear_era_information(era_index: EraIndex) {
// FIXME: We can possibly set a reasonable limit since we do this only once per era and
// clean up state across multiple blocks.
let mut cursor = <ErasStakers<T>>::clear_prefix(era_index, u32::MAX, None);
debug_assert!(cursor.maybe_cursor.is_none());
cursor = <ErasStakersClipped<T>>::clear_prefix(era_index, u32::MAX, None);
debug_assert!(cursor.maybe_cursor.is_none());
cursor = <ErasValidatorPrefs<T>>::clear_prefix(era_index, u32::MAX, None);
let mut cursor = <ErasValidatorPrefs<T>>::clear_prefix(era_index, u32::MAX, None);
debug_assert!(cursor.maybe_cursor.is_none());
cursor = <ClaimedRewards<T>>::clear_prefix(era_index, u32::MAX, None);
debug_assert!(cursor.maybe_cursor.is_none());
Expand Down Expand Up @@ -1151,10 +1141,9 @@ impl<T: Config> Pallet<T> {
}

/// Returns full exposure of a validator for a given era.
///
/// History note: This used to be a getter for old storage item `ErasStakers` deprecated in v14.
/// Since this function is used in the codebase at various places, we kept it as a custom getter
/// that takes care of getting the full exposure of the validator in a backward compatible way.
// Implementation note: This used to be a getter for the now removed storage item `ErasStakers`
// that is succeeded by `ErasStakersPaged`. Since this function is used in the codebase at
// various places, we kept it as a custom getter that builds the full exposure from the pages.
pub fn eras_stakers(
era: EraIndex,
account: &T::AccountId,
Expand Down Expand Up @@ -1845,12 +1834,6 @@ impl<T: Config> StakingInterface for Pallet<T> {
}

fn is_exposed_in_era(who: &Self::AccountId, era: &EraIndex) -> bool {
// look in the non paged exposures
// FIXME: Can be cleaned up once non paged exposures are cleared (https://github.com/paritytech/polkadot-sdk/issues/433)
ErasStakers::<T>::iter_prefix(era).any(|(validator, exposures)| {
validator == *who || exposures.others.iter().any(|i| i.who == *who)
})
||
// look in the paged exposures
ErasStakersPaged::<T>::iter_prefix((era,)).any(|((validator, _), exposure_page)| {
validator == *who || exposure_page.others.iter().any(|i| i.who == *who)
Expand Down Expand Up @@ -1974,7 +1957,6 @@ impl<T: Config> Pallet<T> {
Self::check_bonded_consistency()?;
Self::check_payees()?;
Self::check_nominators()?;
Self::check_exposures()?;
Self::check_paged_exposures()?;
Self::check_count()?;
Self::ensure_disabled_validators_sorted()
Expand Down Expand Up @@ -2128,27 +2110,6 @@ impl<T: Config> Pallet<T> {
Ok(())
}

/// Invariants:
/// * For each era exposed validator, check if the exposure total is sane (exposure.total =
/// exposure.own + exposure.own).
fn check_exposures() -> Result<(), TryRuntimeError> {
let era = Self::active_era().unwrap().index;
ErasStakers::<T>::iter_prefix_values(era)
.map(|expo| {
ensure!(
expo.total ==
expo.own +
expo.others
.iter()
.map(|e| e.value)
.fold(Zero::zero(), |acc, x| acc + x),
"wrong total exposure.",
);
Ok(())
})
.collect::<Result<(), TryRuntimeError>>()
}

/// Invariants:
/// * For each paged era exposed validator, check if the exposure total is sane (exposure.total
/// = exposure.own + exposure.own).
Expand Down
66 changes: 8 additions & 58 deletions substrate/frame/staking/src/pallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ pub use impls::*;

use crate::{
asset, slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf,
DisablingStrategy, EraPayout, EraRewardPoints, Exposure, ExposurePage, Forcing,
LedgerIntegrityState, MaxNominationsOf, NegativeImbalanceOf, Nominations, NominationsQuota,
PositiveImbalanceOf, RewardDestination, SessionInterface, StakingLedger, UnappliedSlash,
UnlockChunk, ValidatorPrefs,
DisablingStrategy, EraPayout, EraRewardPoints, ExposurePage, Forcing, LedgerIntegrityState,
MaxNominationsOf, NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf,
RewardDestination, SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk,
ValidatorPrefs,
};

// The speculative number of spans are used as an input of the weight annotation of
Expand Down Expand Up @@ -145,10 +145,9 @@ pub mod pallet {
/// Number of eras to keep in history.
///
/// Following information is kept for eras in `[current_era -
/// HistoryDepth, current_era]`: `ErasStakers`, `ErasStakersClipped`,
/// `ErasValidatorPrefs`, `ErasValidatorReward`, `ErasRewardPoints`,
/// `ErasTotalStake`, `ErasStartSessionIndex`, `ClaimedRewards`, `ErasStakersPaged`,
/// `ErasStakersOverview`.
/// HistoryDepth, current_era]`: `ErasValidatorPrefs`, `ErasValidatorReward`,
/// `ErasRewardPoints`, `ErasTotalStake`, `ErasStartSessionIndex`, `ClaimedRewards`,
/// `ErasStakersPaged`, `ErasStakersOverview`.
///
/// Must be more than the number of eras delayed by session.
/// I.e. active era must always be in history. I.e. `active_era >
Expand Down Expand Up @@ -482,26 +481,6 @@ pub mod pallet {
#[pallet::getter(fn eras_start_session_index)]
pub type ErasStartSessionIndex<T> = StorageMap<_, Twox64Concat, EraIndex, SessionIndex>;

/// Exposure of validator at era.
///
/// This is keyed first by the era index to allow bulk deletion and then the stash account.
///
/// Is it removed after [`Config::HistoryDepth`] eras.
/// If stakers hasn't been set or has been removed then empty exposure is returned.
///
/// Note: Deprecated since v14. Use `EraInfo` instead to work with exposures.
#[pallet::storage]
#[pallet::unbounded]
pub type ErasStakers<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
EraIndex,
Twox64Concat,
T::AccountId,
Exposure<T::AccountId, BalanceOf<T>>,
ValueQuery,
>;

/// Summary of validator exposure at a given era.
///
/// This contains the total stake in support of the validator and their own stake. In addition,
Expand All @@ -525,35 +504,6 @@ pub mod pallet {
OptionQuery,
>;

/// Clipped Exposure of validator at era.
///
/// Note: This is deprecated, should be used as read-only and will be removed in the future.
/// New `Exposure`s are stored in a paged manner in `ErasStakersPaged` instead.
///
/// This is similar to [`ErasStakers`] but number of nominators exposed is reduced to the
/// `T::MaxExposurePageSize` biggest stakers.
/// (Note: the field `total` and `own` of the exposure remains unchanged).
/// This is used to limit the i/o cost for the nominator payout.
///
/// This is keyed fist by the era index to allow bulk deletion and then the stash account.
///
/// It is removed after [`Config::HistoryDepth`] eras.
/// If stakers hasn't been set or has been removed then empty exposure is returned.
///
/// Note: Deprecated since v14. Use `EraInfo` instead to work with exposures.
#[pallet::storage]
#[pallet::unbounded]
#[pallet::getter(fn eras_stakers_clipped)]
pub type ErasStakersClipped<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
EraIndex,
Twox64Concat,
T::AccountId,
Exposure<T::AccountId, BalanceOf<T>>,
ValueQuery,
>;

/// Paginated exposure of a validator at given era.
///
/// This is keyed first by the era index to allow bulk deletion, then stash account and finally
Expand Down Expand Up @@ -592,7 +542,7 @@ pub mod pallet {
ValueQuery,
>;

/// Similar to `ErasStakers`, this holds the preferences of validators.
/// Similar to `ErasStakersOverview`, this holds the preferences of validators.
///
/// This is keyed first by the era index to allow bulk deletion and then the stash account.
///
Expand Down
Loading
Loading