Skip to content

Commit

Permalink
[Staking] Runtime api if era rewards are pending to be claimed (#4301)
Browse files Browse the repository at this point in the history
closes #426.
related to #1189.

Would help offchain programs to query if there are unclaimed pages of
rewards for a given era.

The logic could look like below

```js
// loop as long as all era pages are claimed.
while (api.call.stakingApi.pendingRewards(era, validator_stash)) {
  api.tx.staking.payout_stakers(validator_stash, era)
}
```
  • Loading branch information
Ank4n committed May 20, 2024
1 parent 177fa0e commit 3c46576
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 2 deletions.
4 changes: 4 additions & 0 deletions polkadot/runtime/westend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2218,6 +2218,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
13 changes: 13 additions & 0 deletions prdoc/pr_4301.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
title: New runtime api to check if a validator has pending pages of rewards for an era.

doc:
- audience:
- Node Dev
- Runtime User
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 @@ -2458,6 +2458,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 @@ -1027,11 +1027,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 @@ -1140,6 +1140,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 @@ -6695,6 +6695,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

0 comments on commit 3c46576

Please sign in to comment.