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

closes https://github.com/paritytech/polkadot-sdk/issues/426.
related to https://github.com/paritytech/polkadot-sdk/pull/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)
}
```
This commit is contained in:
Ankan
2024-04-28 14:35:51 +02:00
committed by GitHub
parent 2a497d2975
commit 73b9a8391f
7 changed files with 163 additions and 2 deletions
@@ -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;
}
}
+27 -1
View File
@@ -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>,
@@ -1183,6 +1183,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> {
+107
View File
@@ -6796,6 +6796,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;