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

[Staking] Runtime api if era rewards are pending to be claimed #4301

Merged
merged 8 commits into from
Apr 28, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 4 additions & 0 deletions polkadot/runtime/westend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2251,6 +2251,10 @@ sp_api::impl_runtime_apis! {
fn eras_stakers_page_count(era: sp_staking::EraIndex, account: AccountId) -> sp_staking::Page {
Staking::api_eras_stakers_page_count(era, account)
}

fn pending_rewards(era: sp_staking::EraIndex, account: AccountId) -> bool {
Staking::api_pending_rewards(era, account)
}
}

#[cfg(feature = "try-runtime")]
Expand Down
11 changes: 11 additions & 0 deletions prdoc/pr_4301.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
title: New runtime api to check if a validator has pending pages of rewards for an era.

doc:
- audience: Node Dev
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- audience: Node Dev
- audience: Node Dev | Runtime Dev

I think it is worth pursuing a new audience type here, wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added Runtime user which is defined in schema as Anyone using the runtime. This can be a token holder or a dev writing a front end for a chain.

description: |
Creates a new runtime api to check if reward for an era is pending for a validator. Era rewards are paged and this
api will return true as long as there is one or more pages of era reward which are not claimed.

crates:
- name: pallet-staking
- name: pallet-staking-runtime-api
4 changes: 4 additions & 0 deletions substrate/bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2792,6 +2792,10 @@ impl_runtime_apis! {
fn eras_stakers_page_count(era: sp_staking::EraIndex, account: AccountId) -> sp_staking::Page {
Staking::api_eras_stakers_page_count(era, account)
}

fn pending_rewards(era: sp_staking::EraIndex, account: AccountId) -> bool {
Staking::api_pending_rewards(era, account)
}
}

impl sp_consensus_babe::BabeApi<Block> for Runtime {
Expand Down
5 changes: 4 additions & 1 deletion substrate/frame/staking/runtime-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ sp_api::decl_runtime_apis! {
/// Returns the nominations quota for a nominator with a given balance.
fn nominations_quota(balance: Balance) -> u32;

/// Returns the page count of exposures for a validator in a given era.
/// Returns the page count of exposures for a validator `account` in a given era.
fn eras_stakers_page_count(era: sp_staking::EraIndex, account: AccountId) -> sp_staking::Page;

/// Returns true if validator `account` has pages to be claimed for the given era.
fn pending_rewards(era: sp_staking::EraIndex, account: AccountId) -> bool;
}
}
28 changes: 27 additions & 1 deletion substrate/frame/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1035,11 +1035,37 @@ where
/// can and add more functions to it as needed.
pub struct EraInfo<T>(sp_std::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: #13034
// 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>,
Expand Down
4 changes: 4 additions & 0 deletions substrate/frame/staking/src/pallet/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,10 @@ impl<T: Config> Pallet<T> {
pub fn api_eras_stakers_page_count(era: EraIndex, account: T::AccountId) -> Page {
EraInfo::<T>::get_page_count(era, &account)
}

pub fn api_pending_rewards(era: EraIndex, account: T::AccountId) -> bool {
EraInfo::<T>::pending_rewards(era, &account)
}
}

impl<T: Config> ElectionDataProvider for Pallet<T> {
Expand Down
107 changes: 107 additions & 0 deletions substrate/frame/staking/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6775,6 +6775,113 @@ fn test_validator_exposure_is_backward_compatible_with_non_paged_rewards_payout(
});
}

#[test]
fn test_runtime_api_pending_rewards() {
ExtBuilder::default().build_and_execute(|| {
// GIVEN
let err_weight = <Test as Config>::WeightInfo::payout_stakers_alive_staked(0);
let stake = 100;

// validator with non-paged exposure, rewards marked in legacy claimed rewards.
let validator_one = 301;
// validator with non-paged exposure, rewards marked in paged claimed rewards.
let validator_two = 302;
// validator with paged exposure.
let validator_three = 303;

// Set staker
for v in validator_one..=validator_three {
let _ = Balances::make_free_balance_be(&v, stake);
assert_ok!(Staking::bond(RuntimeOrigin::signed(v), stake, RewardDestination::Staked));
}

// Add reward points
let reward = EraRewardPoints::<AccountId> {
total: 1,
individual: vec![(validator_one, 1), (validator_two, 1), (validator_three, 1)]
.into_iter()
.collect(),
};
ErasRewardPoints::<Test>::insert(0, reward);

// build exposure
let mut individual_exposures: Vec<IndividualExposure<AccountId, Balance>> = vec![];
for i in 0..=MaxExposurePageSize::get() {
individual_exposures.push(IndividualExposure { who: i.into(), value: stake });
}
let exposure = Exposure::<AccountId, Balance> {
total: stake * (MaxExposurePageSize::get() as Balance + 2),
own: stake,
others: individual_exposures,
};

// add non-paged exposure for one and two.
<ErasStakers<Test>>::insert(0, validator_one, exposure.clone());
<ErasStakers<Test>>::insert(0, validator_two, exposure.clone());
// add paged exposure for third validator
EraInfo::<Test>::set_exposure(0, &validator_three, exposure);

// add some reward to be distributed
ErasValidatorReward::<Test>::insert(0, 1000);

// mark rewards claimed for validator_one in legacy claimed rewards
<Ledger<Test>>::insert(
validator_one,
StakingLedgerInspect {
stash: validator_one,
total: stake,
active: stake,
unlocking: Default::default(),
legacy_claimed_rewards: bounded_vec![0],
},
);

// SCENARIO ONE: rewards already marked claimed in legacy storage.
// runtime api should return false for pending rewards for validator_one.
assert!(!EraInfo::<Test>::pending_rewards(0, &validator_one));
// and if we try to pay, we get an error.
assert_noop!(
Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_one, 0),
Error::<Test>::AlreadyClaimed.with_weight(err_weight)
);

// SCENARIO TWO: non-paged exposure
// validator two has not claimed rewards, so pending rewards is true.
assert!(EraInfo::<Test>::pending_rewards(0, &validator_two));
// and payout works
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_two, 0));
// now pending rewards is false.
assert!(!EraInfo::<Test>::pending_rewards(0, &validator_two));
// and payout fails
assert_noop!(
Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_two, 0),
Error::<Test>::AlreadyClaimed.with_weight(err_weight)
);

// SCENARIO THREE: validator with paged exposure (two pages).
// validator three has not claimed rewards, so pending rewards is true.
assert!(EraInfo::<Test>::pending_rewards(0, &validator_three));
// and payout works
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_three, 0));
// validator three has two pages of exposure, so pending rewards is still true.
assert!(EraInfo::<Test>::pending_rewards(0, &validator_three));
// payout again
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_three, 0));
// now pending rewards is false.
assert!(!EraInfo::<Test>::pending_rewards(0, &validator_three));
// and payout fails
assert_noop!(
Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_three, 0),
Error::<Test>::AlreadyClaimed.with_weight(err_weight)
);

// for eras with no exposure, pending rewards is false.
assert!(!EraInfo::<Test>::pending_rewards(0, &validator_one));
assert!(!EraInfo::<Test>::pending_rewards(0, &validator_two));
assert!(!EraInfo::<Test>::pending_rewards(0, &validator_three));
});
}

mod staking_interface {
use frame_support::storage::with_storage_layer;
use sp_staking::StakingInterface;
Expand Down
Loading