From 1b482dc8561d9077ef5ecf9117e043a0cb32857a Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 25 Apr 2024 20:57:06 +0200 Subject: [PATCH 1/7] pending rewards api for staking --- substrate/frame/staking/src/lib.rs | 28 ++++++- substrate/frame/staking/src/tests.rs | 107 +++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index f5b7e3eca3de..b671ffc63856 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1035,11 +1035,37 @@ where /// can and add more functions to it as needed. pub struct EraInfo(sp_std::marker::PhantomData); impl EraInfo { + /// 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) = >::get(&era, validator) { + overview.page_count + } else { + if >::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 >::get(validator) + .map(|l| l.legacy_claimed_rewards.contains(&era)) + .unwrap_or_default() + { + return false + } + + ClaimedRewards::::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, diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 87f6fd424bd7..6a737589c99e 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -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 = ::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:: { + total: 1, + individual: vec![(validator_one, 1), (validator_two, 1), (validator_three, 1)] + .into_iter() + .collect(), + }; + ErasRewardPoints::::insert(0, reward); + + // build exposure + let mut individual_exposures: Vec> = vec![]; + for i in 0..=MaxExposurePageSize::get() { + individual_exposures.push(IndividualExposure { who: i.into(), value: stake }); + } + let exposure = Exposure:: { + total: stake * (MaxExposurePageSize::get() as Balance + 2), + own: stake, + others: individual_exposures, + }; + + // add non-paged exposure for one and two. + >::insert(0, validator_one, exposure.clone()); + >::insert(0, validator_two, exposure.clone()); + // add paged exposure for third validator + EraInfo::::set_exposure(0, &validator_three, exposure); + + // add some reward to be distributed + ErasValidatorReward::::insert(0, 1000); + + // mark rewards claimed for validator_one in legacy claimed rewards + >::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::::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::::AlreadyClaimed.with_weight(err_weight) + ); + + // SCENARIO TWO: non-paged exposure + // validator two has not claimed rewards, so pending rewards is true. + assert!(EraInfo::::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::::pending_rewards(0, &validator_two)); + // and payout fails + assert_noop!( + Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_two, 0), + Error::::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::::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::::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::::pending_rewards(0, &validator_three)); + // and payout fails + assert_noop!( + Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_three, 0), + Error::::AlreadyClaimed.with_weight(err_weight) + ); + + // for eras with no exposure, pending rewards is false. + assert!(!EraInfo::::pending_rewards(0, &validator_one)); + assert!(!EraInfo::::pending_rewards(0, &validator_two)); + assert!(!EraInfo::::pending_rewards(0, &validator_three)); + }); +} + mod staking_interface { use frame_support::storage::with_storage_layer; use sp_staking::StakingInterface; From d149c23d11ef7c3e6ee114da6acfaeba40741f0d Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 25 Apr 2024 21:09:50 +0200 Subject: [PATCH 2/7] add pending reward as runtime api --- polkadot/runtime/westend/src/lib.rs | 4 ++++ substrate/bin/node/runtime/src/lib.rs | 4 ++++ substrate/frame/staking/runtime-api/src/lib.rs | 5 ++++- substrate/frame/staking/src/pallet/impls.rs | 4 ++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 7924939c79bd..243e5351cac6 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -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")] diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 43c617023bcb..fe47f0c36776 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -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 for Runtime { diff --git a/substrate/frame/staking/runtime-api/src/lib.rs b/substrate/frame/staking/runtime-api/src/lib.rs index b04c383a077d..7955f4184a43 100644 --- a/substrate/frame/staking/runtime-api/src/lib.rs +++ b/substrate/frame/staking/runtime-api/src/lib.rs @@ -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; } } diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 0c0ef0dbf463..e975cec1274a 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1193,6 +1193,10 @@ impl Pallet { pub fn api_eras_stakers_page_count(era: EraIndex, account: T::AccountId) -> Page { EraInfo::::get_page_count(era, &account) } + + pub fn api_pending_rewards(era: EraIndex, account: T::AccountId) -> bool { + EraInfo::::pending_rewards(era, &account) + } } impl ElectionDataProvider for Pallet { From 6754b9a275a8b4ac023e71178412335aef6b8738 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 25 Apr 2024 21:52:20 +0200 Subject: [PATCH 3/7] prdoc --- prdoc/pr_4301.prdoc | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 prdoc/pr_4301.prdoc diff --git a/prdoc/pr_4301.prdoc b/prdoc/pr_4301.prdoc new file mode 100644 index 000000000000..f524dae398a9 --- /dev/null +++ b/prdoc/pr_4301.prdoc @@ -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 + description: | + Creates a new runtime api to check if era reward 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 reward not claimed. + +crates: +- name: pallet-staking +- name: pallet-staking-runtime-api From dd978463f24d77b3db6b9249875f46c0b0abc476 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 25 Apr 2024 21:54:18 +0200 Subject: [PATCH 4/7] update prdoc --- prdoc/pr_4301.prdoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prdoc/pr_4301.prdoc b/prdoc/pr_4301.prdoc index f524dae398a9..f9f8e764ca48 100644 --- a/prdoc/pr_4301.prdoc +++ b/prdoc/pr_4301.prdoc @@ -3,8 +3,8 @@ title: New runtime api to check if a validator has pending pages of rewards for doc: - audience: Node Dev description: | - Creates a new runtime api to check if era reward 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 reward not claimed. + 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 From 76aa998bb3c35a1a402855651307504d03077f42 Mon Sep 17 00:00:00 2001 From: Ankan <10196091+Ank4n@users.noreply.github.com> Date: Fri, 26 Apr 2024 22:09:14 +0200 Subject: [PATCH 5/7] Update pr_4301.prdoc --- prdoc/pr_4301.prdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prdoc/pr_4301.prdoc b/prdoc/pr_4301.prdoc index f9f8e764ca48..260fd47ff999 100644 --- a/prdoc/pr_4301.prdoc +++ b/prdoc/pr_4301.prdoc @@ -1,7 +1,7 @@ title: New runtime api to check if a validator has pending pages of rewards for an era. doc: - - audience: Node Dev + - audience: App Builder 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. From 457b0e03cc4033a6cf1fbabe7495c84ad05d9da8 Mon Sep 17 00:00:00 2001 From: Ankan <10196091+Ank4n@users.noreply.github.com> Date: Fri, 26 Apr 2024 22:15:40 +0200 Subject: [PATCH 6/7] Update pr_4301.prdoc --- prdoc/pr_4301.prdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prdoc/pr_4301.prdoc b/prdoc/pr_4301.prdoc index 260fd47ff999..538fb0b4d4d0 100644 --- a/prdoc/pr_4301.prdoc +++ b/prdoc/pr_4301.prdoc @@ -1,7 +1,7 @@ title: New runtime api to check if a validator has pending pages of rewards for an era. doc: - - audience: App Builder + - 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. From 53db85222389e921a525b6ad0dc4d23deae76137 Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 26 Apr 2024 22:18:13 +0200 Subject: [PATCH 7/7] fix prdoc --- prdoc/pr_4301.prdoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/prdoc/pr_4301.prdoc b/prdoc/pr_4301.prdoc index 538fb0b4d4d0..2ca2534243a8 100644 --- a/prdoc/pr_4301.prdoc +++ b/prdoc/pr_4301.prdoc @@ -1,7 +1,9 @@ title: New runtime api to check if a validator has pending pages of rewards for an era. doc: - - audience: Node Dev | Runtime User + - 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.