Skip to content

Commit 7b371e7

Browse files
author
José Molina
committed
Cleanup the counters storage item
1 parent 617cb00 commit 7b371e7

File tree

4 files changed

+115
-30
lines changed

4 files changed

+115
-30
lines changed

src/benchmarking.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,9 @@ fn prepare_rewards<T: Config + pallet_session::Config>(
167167
)
168168
.unwrap_or_else(|e| panic!("Could not stake: {:?}", e));
169169
Counters::<T>::mutate(candidate, |counter| {
170-
counter.saturating_accrue(FixedU128::saturating_from_rational(amount, amount))
170+
counter
171+
.value
172+
.saturating_accrue(FixedU128::saturating_from_rational(amount, amount));
171173
})
172174
}
173175
}

src/lib.rs

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ pub mod weights;
7878

7979
const LOG_TARGET: &str = "runtime::collator-staking";
8080

81+
/// # Pallet Documentation
82+
///
8183
#[frame_support::pallet]
8284
pub mod pallet {
8385
use frame_support::{
@@ -412,7 +414,7 @@ pub mod pallet {
412414
/// Tracks the different types of locks that can be applied to an account's balance.
413415
///
414416
/// This struct keeps track of the different amounts that are locked for staking,
415-
/// releasing (in process of being unlocked), and held as candidacy bond.
417+
/// releasing (in the process of being unlocked), and held as candidacy bond.
416418
#[derive(
417419
PartialEq,
418420
Eq,
@@ -431,6 +433,30 @@ pub mod pallet {
431433
pub candidacy_bond: Balance,
432434
}
433435

436+
/// Track reward distribution state for a candidate.
437+
///
438+
/// It maintains the state needed to calculate and distribute rewards for stakers
439+
/// who have delegated to a specific collator.
440+
#[derive(
441+
PartialEq,
442+
Eq,
443+
Clone,
444+
Encode,
445+
Decode,
446+
RuntimeDebug,
447+
scale_info::TypeInfo,
448+
MaxEncodedLen,
449+
Default,
450+
)]
451+
pub struct Counter {
452+
/// Accumulated reward ratio per token staked, represented as a fixed-point number.
453+
/// Updated when new rewards are added for distribution.
454+
pub value: FixedU128,
455+
/// Number of stakers that have not yet claimed their rewards.
456+
/// Decremented when stakers claim their rewards.
457+
pub pending_claims: u32,
458+
}
459+
434460
impl<Balance> LockedBalance<Balance>
435461
where
436462
Balance: Saturating + Copy,
@@ -564,7 +590,7 @@ pub mod pallet {
564590
/// Represents accumulated rewards per token staked on a given collator over time.
565591
#[pallet::storage]
566592
pub type Counters<T: Config> =
567-
StorageMap<_, Blake2_128Concat, T::AccountId, FixedU128, ValueQuery>;
593+
StorageMap<_, Blake2_128Concat, T::AccountId, Counter, ValueQuery>;
568594

569595
/// The storage value `LastRewardedKey` is used to track the last key that was rewarded
570596
/// automatically by the system.
@@ -1495,13 +1521,26 @@ pub mod pallet {
14951521
let current_session = CurrentSession::<T>::get();
14961522
UserStake::<T>::try_mutate(who, |user_stake_info| {
14971523
for candidate in &user_stake_info.candidates {
1498-
let counter = Counters::<T>::get(candidate);
1499-
CandidateStake::<T>::mutate(candidate, who, |info| {
1500-
let reward =
1501-
counter.saturating_sub(info.checkpoint).saturating_mul_int(info.stake);
1502-
candidate_rewards.push((candidate.clone(), reward));
1503-
total_rewards.saturating_accrue(reward);
1504-
info.checkpoint = counter;
1524+
Counters::<T>::mutate_exists(candidate, |maybe_counter| {
1525+
CandidateStake::<T>::mutate(candidate, who, |info| {
1526+
let value = maybe_counter.clone().unwrap_or_default().value;
1527+
let reward = value
1528+
.saturating_sub(info.checkpoint)
1529+
.saturating_mul_int(info.stake);
1530+
candidate_rewards.push((candidate.clone(), reward));
1531+
total_rewards.saturating_accrue(reward);
1532+
info.checkpoint = value;
1533+
});
1534+
// Remove the counter if everything has been claimed and the candidate left.
1535+
if let Some(counter) = maybe_counter {
1536+
if counter.pending_claims <= 1
1537+
&& Self::get_candidate(candidate).is_err()
1538+
{
1539+
*maybe_counter = None;
1540+
} else {
1541+
counter.pending_claims.saturating_dec();
1542+
}
1543+
}
15051544
});
15061545
}
15071546
user_stake_info.maybe_last_reward_session = Some(current_session);
@@ -1532,7 +1571,8 @@ pub mod pallet {
15321571
for candidate in &user_stake_info.candidates {
15331572
let counter = Counters::<T>::get(candidate);
15341573
let info = CandidateStake::<T>::get(candidate, who);
1535-
let reward = counter.saturating_sub(info.checkpoint).saturating_mul_int(info.stake);
1574+
let reward =
1575+
counter.value.saturating_sub(info.checkpoint).saturating_mul_int(info.stake);
15361576
total_rewards.saturating_accrue(reward);
15371577
}
15381578
total_rewards
@@ -1700,7 +1740,7 @@ pub mod pallet {
17001740
candidate_info.stakers.saturating_inc();
17011741
}
17021742
candidate_stake_info.stake = final_staker_stake;
1703-
candidate_stake_info.checkpoint = Counters::<T>::get(candidate);
1743+
candidate_stake_info.checkpoint = Counters::<T>::get(candidate).value;
17041744
candidate_info.stake.saturating_accrue(amount);
17051745
UserStake::<T>::try_mutate(staker, |user_stake_info| -> DispatchResult {
17061746
// In case the user recently unstaked we cannot allow those funds to be quickly
@@ -1897,7 +1937,13 @@ pub mod pallet {
18971937
}
18981938
Self::release_candidacy_bond(who, reason)?;
18991939

1900-
// Store removed candidate in SessionRemovedCandidates to properly reward
1940+
// Remove the counter if all rewards have been claimed.
1941+
let counter = Counters::<T>::get(who);
1942+
if counter.pending_claims.is_zero() {
1943+
Counters::<T>::remove(who);
1944+
}
1945+
1946+
// Store the removed candidate in SessionRemovedCandidates to properly reward
19011947
// the candidate and its stakers at the end of the session.
19021948
SessionRemovedCandidates::<T>::insert(who, candidate.clone());
19031949

@@ -2100,7 +2146,8 @@ pub mod pallet {
21002146
collator_info.stake,
21012147
);
21022148
Counters::<T>::mutate(&collator, |counter| {
2103-
counter.saturating_accrue(session_ratio)
2149+
counter.value.saturating_accrue(session_ratio);
2150+
counter.pending_claims = collator_info.stakers;
21042151
});
21052152
} else {
21062153
log::warn!("Collator {:?} is no longer a candidate", collator);

src/migrations/v2.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919

2020
use crate::migrations::PALLET_MIGRATIONS_ID;
2121
use crate::{
22-
AutoCompoundSettings, BalanceOf, CandidacyBondReleaseOf, CandidacyBondReleases, CandidateStake,
23-
CandidateStakeInfo, Candidates, ClaimableRewards, Config, FreezeReason, Layer, LockedBalances,
24-
Pallet, ReleaseQueues, ReleaseRequestOf, WeightInfo,
22+
AutoCompoundSettings, BalanceOf, CandidacyBondReleases, CandidateStake, CandidateStakeInfo,
23+
Candidates, ClaimableRewards, Config, FreezeReason, Layer, LockedBalances, Pallet,
24+
ReleaseQueues, WeightInfo,
2525
};
2626
use core::fmt::Debug;
2727
use frame_support::migrations::{MigrationId, SteppedMigration, SteppedMigrationError};
@@ -31,6 +31,8 @@ use frame_support::weights::WeightMeter;
3131
use sp_runtime::{FixedU128, Percent, Saturating};
3232
use sp_std::vec::Vec;
3333

34+
#[cfg(feature = "try-runtime")]
35+
use crate::{CandidacyBondReleaseOf, ReleaseRequestOf};
3436
#[cfg(feature = "try-runtime")]
3537
use sp_runtime::TryRuntimeError;
3638
#[cfg(feature = "try-runtime")]

src/tests.rs

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate as collator_staking;
1717
use crate::{
1818
mock::*, AutoCompoundSettings, CandidacyBondRelease, CandidacyBondReleaseReason,
1919
CandidacyBondReleases, CandidateInfo, CandidateStake, CandidateStakeInfo, Candidates,
20-
ClaimableRewards, CollatorRewardPercentage, Config, Counters, CurrentSession,
20+
ClaimableRewards, CollatorRewardPercentage, Config, Counter, Counters, CurrentSession,
2121
DesiredCandidates, Error, Event, ExtraReward, IdentityCollator, Invulnerables,
2222
LastAuthoredBlock, Layer, MinCandidacyBond, MinStake, NextSystemOperation, Operation,
2323
ProducedBlocks, ReleaseQueues, ReleaseRequest, SessionRemovedCandidates, StakeTarget,
@@ -3453,8 +3453,14 @@ mod collator_rewards {
34533453
)
34543454
}));
34553455
assert_eq!(CollatorStaking::calculate_unclaimed_rewards(&4), 15);
3456+
assert_eq!(Counters::<Test>::get(4).pending_claims, 1);
34563457
assert_ok!(CollatorStaking::do_claim_rewards(&4));
3457-
assert_eq!(CandidateStake::<Test>::get(4, 4).checkpoint, Counters::<Test>::get(4));
3458+
// The candidate 4 left the candidacy, so the counter got erased.
3459+
assert_eq!(
3460+
Counters::<Test>::get(4),
3461+
Counter { value: FixedU128::zero(), pending_claims: 0 }
3462+
);
3463+
assert!(!Counters::<Test>::contains_key(4));
34583464
assert_eq!(ClaimableRewards::<Test>::get(), 0);
34593465
// Now we can see the reward.
34603466
System::assert_has_event(RuntimeEvent::CollatorStaking(Event::StakingRewardReceived {
@@ -3565,7 +3571,10 @@ mod collator_rewards {
35653571
// Reward for staker when claiming.
35663572
assert_eq!(CollatorStaking::calculate_unclaimed_rewards(&4), 28);
35673573
assert_ok!(CollatorStaking::do_claim_rewards(&4));
3568-
assert_eq!(CandidateStake::<Test>::get(4, 4).checkpoint, Counters::<Test>::get(4));
3574+
assert_eq!(
3575+
Counters::<Test>::get(4),
3576+
Counter { value: CandidateStake::<Test>::get(4, 4).checkpoint, pending_claims: 0 }
3577+
);
35693578
assert_eq!(ClaimableRewards::<Test>::get(), 0);
35703579
System::assert_has_event(RuntimeEvent::CollatorStaking(Event::StakingRewardReceived {
35713580
account: 4,
@@ -3677,7 +3686,10 @@ mod collator_rewards {
36773686
// Reward for staker.
36783687
assert_eq!(CollatorStaking::calculate_unclaimed_rewards(&4), 15);
36793688
assert_ok!(CollatorStaking::do_claim_rewards(&4));
3680-
assert_eq!(CandidateStake::<Test>::get(4, 4).checkpoint, Counters::<Test>::get(4));
3689+
assert_eq!(
3690+
Counters::<Test>::get(4),
3691+
Counter { value: CandidateStake::<Test>::get(4, 4).checkpoint, pending_claims: 0 }
3692+
);
36813693
assert_eq!(ClaimableRewards::<Test>::get(), 0);
36823694
System::assert_has_event(RuntimeEvent::CollatorStaking(Event::StakingRewardReceived {
36833695
account: 4,
@@ -3814,15 +3826,21 @@ mod collator_rewards {
38143826
}));
38153827
assert_eq!(CollatorStaking::calculate_unclaimed_rewards(&2), 12);
38163828
assert_ok!(CollatorStaking::do_claim_rewards(&2));
3817-
assert_eq!(CandidateStake::<Test>::get(4, 2).checkpoint, Counters::<Test>::get(4));
3829+
assert_eq!(
3830+
Counters::<Test>::get(4),
3831+
Counter { value: CandidateStake::<Test>::get(4, 2).checkpoint, pending_claims: 1 }
3832+
);
38183833
assert_eq!(ClaimableRewards::<Test>::get(), 16); // this remains to staker 3.
38193834
System::assert_has_event(RuntimeEvent::CollatorStaking(Event::StakingRewardReceived {
38203835
account: 2,
38213836
amount: 12,
38223837
}));
38233838
assert_eq!(CollatorStaking::calculate_unclaimed_rewards(&3), 15);
38243839
assert_ok!(CollatorStaking::do_claim_rewards(&3));
3825-
assert_eq!(CandidateStake::<Test>::get(4, 3).checkpoint, Counters::<Test>::get(4));
3840+
assert_eq!(
3841+
Counters::<Test>::get(4),
3842+
Counter { value: CandidateStake::<Test>::get(4, 3).checkpoint, pending_claims: 0 }
3843+
);
38263844
assert_eq!(ClaimableRewards::<Test>::get(), 1); // rounding issue
38273845
System::assert_has_event(RuntimeEvent::CollatorStaking(Event::StakingRewardReceived {
38283846
account: 3,
@@ -3916,7 +3934,10 @@ mod collator_rewards {
39163934

39173935
// Check the collator's counter and staker's checkpoint. Both should be zero, as no
39183936
// rewards were distributed.
3919-
assert_eq!(Counters::<Test>::get(3), FixedU128::zero());
3937+
assert_eq!(
3938+
Counters::<Test>::get(3),
3939+
Counter { value: FixedU128::zero(), pending_claims: 0 }
3940+
);
39203941
assert_eq!(CandidateStake::<Test>::get(3, 4).checkpoint, FixedU128::zero());
39213942

39223943
// Skip session 0, as there are no rewards for this session.
@@ -3934,7 +3955,10 @@ mod collator_rewards {
39343955
// Now we have 10 units of rewards being distributed. 20% goes to the collator, and 80%
39353956
// goes to stakers, so total 8 for stakers. The collator's counter should be the ratio
39363957
// between the rewards obtained and the total stake deposited in it.
3937-
assert_eq!(Counters::<Test>::get(4), FixedU128::from_rational(8, 40));
3958+
assert_eq!(
3959+
Counters::<Test>::get(4),
3960+
Counter { value: FixedU128::from_rational(8, 40), pending_claims: 1 }
3961+
);
39383962

39393963
// The current checkpoint does not vary, as the staker did not claim the rewards yet.
39403964
assert_eq!(CandidateStake::<Test>::get(4, 3).checkpoint, FixedU128::zero());
@@ -3962,7 +3986,10 @@ mod collator_rewards {
39623986
));
39633987

39643988
// The checkpoint should be equal to the candidate's current counter.
3965-
assert_eq!(Counters::<Test>::get(4), FixedU128::from_rational(8, 40));
3989+
assert_eq!(
3990+
Counters::<Test>::get(4),
3991+
Counter { value: FixedU128::from_rational(8, 40), pending_claims: 0 }
3992+
);
39663993
assert_eq!(
39673994
CandidateStake::<Test>::get(4, 5).checkpoint,
39683995
FixedU128::from_rational(8, 40)
@@ -4369,7 +4396,12 @@ mod claim_rewards_other {
43694396

43704397
// Check the collator's counter and staker's checkpoint. Both should be zero, as no
43714398
// rewards were distributed.
4372-
assert_eq!(Counters::<Test>::get(4), FixedU128::zero());
4399+
assert_eq!(
4400+
Counters::<Test>::get(4),
4401+
Counter { value: FixedU128::zero(), pending_claims: 0 }
4402+
);
4403+
// Which implies the counter should not exist yet.
4404+
assert!(!Counters::<Test>::contains_key(4));
43734405
assert_eq!(CandidateStake::<Test>::get(4, 3).checkpoint, FixedU128::zero());
43744406

43754407
// Skip session 0, as there are no rewards for this session
@@ -4392,7 +4424,10 @@ mod claim_rewards_other {
43924424
// At this point, rewards from session 1 should have been calculated
43934425
// Check counter is updated as expected with 20% to collator, 80% to stakers
43944426
let expected_checkpoint = FixedU128::from_rational(80, 40);
4395-
assert_eq!(Counters::<Test>::get(4), expected_checkpoint);
4427+
assert_eq!(
4428+
Counters::<Test>::get(4),
4429+
Counter { value: expected_checkpoint, pending_claims: 1 }
4430+
);
43964431

43974432
// The checkpoint should still be zero, as rewards aren't claimed
43984433
// or distributed yet
@@ -4402,8 +4437,7 @@ mod claim_rewards_other {
44024437
let weight = Weight::from_parts(u64::MAX, u64::MAX);
44034438
CollatorStaking::on_idle(21, weight);
44044439

4405-
// The checkpoint should now be updated to match the counter as
4406-
// rewards have been distributed
4440+
// The checkpoint should now be zero, since all stakers claimed
44074441
assert_eq!(CandidateStake::<Test>::get(4, 3).checkpoint, expected_checkpoint);
44084442

44094443
// The stake should have increased by the reward amount (40 * 80/40 = 80)

0 commit comments

Comments
 (0)