Skip to content

Commit

Permalink
Feat/reward pool refactor #1976 (#2005)
Browse files Browse the repository at this point in the history
The goal of this PR is to implement a "chunk" version of the overall
reward pool history to reduce read/write load and hence weight for
transactions and `on_initialize` when a new `RewardEra` needs to start.

Part of #1976

Co-authored-by: Wil Wade <[email protected]>
  • Loading branch information
shannonwells and wilwade committed Jul 23, 2024
1 parent 869cb1a commit b9466a8
Show file tree
Hide file tree
Showing 15 changed files with 639 additions and 277 deletions.
28 changes: 15 additions & 13 deletions pallets/capacity/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,8 @@ pub fn set_era_and_reward_pool_at_block<T: Config>(
) {
let era_info: RewardEraInfo<T::RewardEra, BlockNumberFor<T>> =
RewardEraInfo { era_index, started_at };
let total_reward_pool: BalanceOf<T> =
T::MinimumStakingAmount::get().saturating_add(1_100u32.into());
CurrentEraInfo::<T>::set(era_info);
let pool_info: RewardPoolInfo<BalanceOf<T>> = RewardPoolInfo {
total_staked_token,
total_reward_pool,
unclaimed_balance: total_reward_pool,
};
ProviderBoostRewardPool::<T>::insert(era_index, pool_info);
CurrentEraProviderBoostTotal::<T>::set(total_staked_token)
}

// caller stakes the given amount to the given target
Expand Down Expand Up @@ -93,6 +86,19 @@ fn fill_unlock_chunks<T: Config>(caller: &T::AccountId, count: u32) {
UnstakeUnlocks::<T>::set(caller, Some(unlocking));
}

fn fill_reward_pool_chunks<T: Config>() {
let chunk_len = T::RewardPoolChunkLength::get();
let chunks = T::ProviderBoostHistoryLimit::get() / (chunk_len);
for i in 0..chunks {
let mut new_chunk = RewardPoolHistoryChunk::<T>::new();
for j in 0..chunk_len {
let era = (i + 1) * (j + 1);
assert_ok!(new_chunk.try_insert(era.into(), (1000u32 * era).into()));
}
ProviderBoostRewardPools::<T>::set(i, Some(new_chunk));
}
}

benchmarks! {
stake {
let caller: T::AccountId = create_funded_account::<T>("account", SEED, 105u32);
Expand Down Expand Up @@ -145,11 +151,7 @@ benchmarks! {

let current_era: T::RewardEra = (history_limit + 1u32).into();
CurrentEraInfo::<T>::set(RewardEraInfo{ era_index: current_era, started_at });

for i in 0..history_limit {
let era: T::RewardEra = i.into();
ProviderBoostRewardPool::<T>::insert(era, RewardPoolInfo { total_staked_token, total_reward_pool, unclaimed_balance});
}
fill_reward_pool_chunks::<T>();
}: {
Capacity::<T>::start_new_reward_era_if_needed(current_block);
} verify {
Expand Down
203 changes: 136 additions & 67 deletions pallets/capacity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
rustdoc::invalid_codeblock_attributes,
missing_docs
)]
use sp_std::ops::{Add, Mul};

use sp_std::ops::Mul;

use frame_support::{
ensure,
Expand Down Expand Up @@ -166,6 +167,8 @@ pub mod pallet {
+ EncodeLike
+ Into<BalanceOf<Self>>
+ Into<BlockNumberFor<Self>>
+ Into<u32>
+ EncodeLike<u32>
+ TypeInfo;

/// The number of blocks in a RewardEra
Expand All @@ -175,6 +178,7 @@ pub mod pallet {
/// The maximum number of eras over which one can claim rewards
/// Note that you can claim rewards even if you no longer are boosting, because you
/// may claim rewards for past eras up to the history limit.
/// MUST be a multiple of [`Self::RewardPoolChunkLength`]
#[pallet::constant]
type ProviderBoostHistoryLimit: Get<u32>;

Expand All @@ -192,6 +196,11 @@ pub mod pallet {
/// the percentage cap per era of an individual Provider Boost reward
#[pallet::constant]
type RewardPercentCap: Get<Permill>;

/// The number of chunks of Reward Pool history we expect to store
/// MUST be a divisor of [`Self::ProviderBoostHistoryLimit`]
#[pallet::constant]
type RewardPoolChunkLength: Get<u32>;
}

/// Storage for keeping a ledger of staked token amounts for accounts.
Expand Down Expand Up @@ -253,29 +262,36 @@ pub mod pallet {
pub type UnstakeUnlocks<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, UnlockChunkList<T>>;

/// stores how many times an account has retargeted, and when it last retargeted.
#[pallet::storage]
#[pallet::getter(fn get_retargets_for)]
pub type Retargets<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, RetargetInfo<T>>;

/// Information about the current reward era. Checked every block.
#[pallet::storage]
#[pallet::whitelist_storage]
#[pallet::getter(fn get_current_era)]
pub type CurrentEraInfo<T: Config> =
StorageValue<_, RewardEraInfo<T::RewardEra, BlockNumberFor<T>>, ValueQuery>;

/// Reward Pool history
/// Reward Pool history is divided into chunks of size RewardPoolChunkLength.
/// ProviderBoostHistoryLimit is the total number of items, the key is the
/// chunk number.
#[pallet::storage]
#[pallet::getter(fn get_reward_pool_for_era)]
pub type ProviderBoostRewardPool<T: Config> =
CountedStorageMap<_, Twox64Concat, T::RewardEra, RewardPoolInfo<BalanceOf<T>>>;
#[pallet::getter(fn get_reward_pool_chunk)]
pub type ProviderBoostRewardPools<T: Config> =
StorageMap<_, Twox64Concat, u32, RewardPoolHistoryChunk<T>>;

/// How much is staked this era
#[pallet::storage]
pub type CurrentEraProviderBoostTotal<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;

/// Individual history for each account that has Provider-Boosted.
#[pallet::storage]
#[pallet::getter(fn get_staking_history_for)]
pub type ProviderBoostHistories<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, ProviderBoostHistory<T>>;

/// stores how many times an account has retargeted, and when it last retargeted.
#[pallet::storage]
#[pallet::getter(fn get_retargets_for)]
pub type Retargets<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, RetargetInfo<T>>;

// Simple declaration of the `Pallet` type. It is placeholder we use to implement traits and
// method.
#[pallet::pallet]
Expand Down Expand Up @@ -583,7 +599,7 @@ pub mod pallet {
let (mut boosting_details, actual_amount) =
Self::ensure_can_boost(&staker, &target, &amount)?;

let capacity = Self::increase_stake_and_issue_boost(
let capacity = Self::increase_stake_and_issue_boost_capacity(
&staker,
&mut boosting_details,
&target,
Expand Down Expand Up @@ -681,34 +697,33 @@ impl<T: Config> Pallet<T> {
Ok(capacity)
}

fn increase_stake_and_issue_boost(
fn increase_stake_and_issue_boost_capacity(
staker: &T::AccountId,
staking_details: &mut StakingDetails<T>,
target: &MessageSourceId,
amount: &BalanceOf<T>,
) -> Result<BalanceOf<T>, DispatchError> {
staking_details.deposit(*amount).ok_or(ArithmeticError::Overflow)?;
Self::set_staking_account_and_lock(staker, staking_details)?;

// get the capacity generated by a Provider Boost
let capacity = Self::capacity_generated(T::RewardsProvider::capacity_boost(*amount));

let mut target_details = Self::get_target_for(staker, target).unwrap_or_default();

target_details.deposit(*amount, capacity).ok_or(ArithmeticError::Overflow)?;
Self::set_target_details_for(staker, *target, target_details);

let mut capacity_details = Self::get_capacity_for(target).unwrap_or_default();
capacity_details.deposit(amount, &capacity).ok_or(ArithmeticError::Overflow)?;
Self::set_capacity_for(*target, capacity_details);

let era = Self::get_current_era().era_index;
let mut reward_pool =
Self::get_reward_pool_for_era(era).ok_or(Error::<T>::EraOutOfRange)?;
reward_pool.total_staked_token = reward_pool.total_staked_token.saturating_add(*amount);

Self::set_staking_account_and_lock(staker, staking_details)?;
Self::set_target_details_for(staker, *target, target_details);
Self::set_capacity_for(*target, capacity_details);
Self::set_reward_pool(era, &reward_pool);
Self::upsert_boost_history(staker, era, *amount, true)?;

let reward_pool_total = CurrentEraProviderBoostTotal::<T>::get();
CurrentEraProviderBoostTotal::<T>::set(reward_pool_total.saturating_add(*amount));

Ok(capacity)
}

Expand Down Expand Up @@ -764,10 +779,6 @@ impl<T: Config> Pallet<T> {
CapacityLedger::<T>::insert(target, capacity_details);
}

fn set_reward_pool(era: <T>::RewardEra, new_reward_pool: &RewardPoolInfo<BalanceOf<T>>) {
ProviderBoostRewardPool::<T>::set(era, Some(new_reward_pool.clone()));
}

/// Decrease a staking account's active token and reap if it goes below the minimum.
/// Returns: actual amount unstaked, plus the staking type + StakingDetails,
/// since StakingDetails may be reaped and staking type must be used to calculate the
Expand All @@ -786,11 +797,11 @@ impl<T: Config> Pallet<T> {
let staking_type = staking_account.staking_type;
if staking_type == ProviderBoost {
let era = Self::get_current_era().era_index;
let mut reward_pool =
Self::get_reward_pool_for_era(era).ok_or(Error::<T>::EraOutOfRange)?;
reward_pool.total_staked_token = reward_pool.total_staked_token.saturating_sub(amount);
Self::set_reward_pool(era, &reward_pool.clone());
Self::upsert_boost_history(&unstaker, era, actual_unstaked_amount, false)?;
let reward_pool_total = CurrentEraProviderBoostTotal::<T>::get();
CurrentEraProviderBoostTotal::<T>::set(
reward_pool_total.saturating_sub(actual_unstaked_amount),
);
}
Ok((actual_unstaked_amount, staking_type))
}
Expand Down Expand Up @@ -949,26 +960,12 @@ impl<T: Config> Pallet<T> {
};
CurrentEraInfo::<T>::set(new_era_info); // 1w

let current_reward_pool =
Self::get_reward_pool_for_era(current_era_info.era_index).unwrap_or_default(); // 1r

let past_eras_max = T::ProviderBoostHistoryLimit::get();
let entries: u32 = ProviderBoostRewardPool::<T>::count(); // 1r

if past_eras_max.eq(&entries) {
let earliest_era =
current_era_info.era_index.saturating_sub(past_eras_max.into()).add(One::one());
ProviderBoostRewardPool::<T>::remove(earliest_era); // 1w
}

let total_reward_pool =
T::RewardsProvider::reward_pool_size(current_reward_pool.total_staked_token);
let new_reward_pool = RewardPoolInfo {
total_staked_token: current_reward_pool.total_staked_token,
total_reward_pool,
unclaimed_balance: total_reward_pool,
};
ProviderBoostRewardPool::<T>::insert(new_era_info.era_index, new_reward_pool); // 1w
// carry over the current reward pool total
let current_reward_pool_total: BalanceOf<T> = CurrentEraProviderBoostTotal::<T>::get(); // 1
Self::update_provider_boost_reward_pool(
current_era_info.era_index,
current_reward_pool_total,
);
T::WeightInfo::start_new_reward_era_if_needed()
} else {
T::DbWeight::get().reads(1)
Expand Down Expand Up @@ -1077,39 +1074,34 @@ impl<T: Config> Pallet<T> {
let staking_history =
Self::get_staking_history_for(account).ok_or(Error::<T>::NotAStakingAccount)?; // cached read from has_unclaimed_rewards

let era_info = Self::get_current_era(); // cached read, ditto
let current_era_info = Self::get_current_era(); // cached read, ditto
let max_history: u32 = T::ProviderBoostHistoryLimit::get(); // 1r
let era_length: u32 = T::EraLength::get(); // 1r length in blocks
let chunk_length: u32 = T::RewardPoolChunkLength::get();

let mut reward_era = current_era_info.era_index.saturating_sub((max_history).into());
let end_era = current_era_info.era_index.saturating_sub(One::one());

let max_history: u32 = T::ProviderBoostHistoryLimit::get() - 1; // 1r
let era_length: u32 = T::EraLength::get(); // 1r
let mut reward_era = era_info.era_index.saturating_sub((max_history).into());
let end_era = era_info.era_index.saturating_sub(One::one());
// start with how much was staked in the era before the earliest for which there are eligible rewards.
let mut previous_amount: BalanceOf<T> =
staking_history.get_amount_staked_for_era(&(reward_era.saturating_sub(1u32.into())));

while reward_era.le(&end_era) {
let staked_amount = staking_history.get_amount_staked_for_era(&reward_era);
if !staked_amount.is_zero() {
let expires_at_era = reward_era.saturating_add(max_history.into());
let reward_pool =
Self::get_reward_pool_for_era(reward_era).ok_or(Error::<T>::EraOutOfRange)?; // 1r
let expires_at_block = if expires_at_era.eq(&era_info.era_index) {
era_info.started_at + era_length.into() // expires at end of this era
} else {
let eras_to_expiration =
expires_at_era.saturating_sub(era_info.era_index).add(1u32.into());
let blocks_to_expiration = eras_to_expiration * era_length.into();
let started_at = era_info.started_at;
started_at + blocks_to_expiration.into()
};
let expires_at_block = Self::block_at_end_of_era(expires_at_era);
let eligible_amount = if staked_amount.lt(&previous_amount) {
staked_amount
} else {
previous_amount
};
let total_for_era =
Self::get_total_stake_for_past_era(reward_era, current_era_info.era_index)?;
let earned_amount = <T>::RewardsProvider::era_staking_reward(
eligible_amount,
reward_pool.total_staked_token,
reward_pool.total_reward_pool,
total_for_era,
T::RewardPoolEachEra::get(),
);
unclaimed_rewards
.try_push(UnclaimedRewardInfo {
Expand All @@ -1119,13 +1111,90 @@ impl<T: Config> Pallet<T> {
earned_amount,
})
.map_err(|_e| Error::<T>::CollectionBoundExceeded)?;
// ^^ there's no good reason for this ever to fail in production but it should be handled.
// ^^ there's no good reason for this ever to fail in production but it must be handled.
previous_amount = staked_amount;
}
reward_era = reward_era.saturating_add(One::one());
} // 1r * up to ProviderBoostHistoryLimit-1, if they staked every RewardEra.
Ok(unclaimed_rewards)
}

// Returns the block number for the end of the provided era. Assumes `era` is at least this
// era or in the future
pub(crate) fn block_at_end_of_era(era: T::RewardEra) -> BlockNumberFor<T> {
let current_era_info = Self::get_current_era();
let era_length: BlockNumberFor<T> = T::EraLength::get().into();

let era_diff = if current_era_info.era_index.eq(&era) {
1u32.into()
} else {
era.saturating_sub(current_era_info.era_index).saturating_add(1u32.into())
};
current_era_info.started_at + era_length.mul(era_diff.into()) - 1u32.into()
}

// Figure out the history chunk that a given era is in and pull out the total stake for that era.
pub(crate) fn get_total_stake_for_past_era(
reward_era: T::RewardEra,
current_era: T::RewardEra,
) -> Result<BalanceOf<T>, DispatchError> {
// Make sure that the past era is not too old
let era_range = current_era.saturating_sub(reward_era);
ensure!(
current_era.gt(&reward_era) &&
era_range.le(&T::ProviderBoostHistoryLimit::get().into()),
Error::<T>::EraOutOfRange
);

let chunk_idx: u32 = Self::get_chunk_index_for_era(reward_era);
let reward_pool_chunk = Self::get_reward_pool_chunk(chunk_idx).unwrap_or_default(); // 1r
let total_for_era =
reward_pool_chunk.total_for_era(&reward_era).ok_or(Error::<T>::EraOutOfRange)?;
Ok(*total_for_era)
}

/// Get the index of the chunk for a given era, hustory limit, and chunk length
/// Example with history limit of 6 and chunk length 3:
/// - Arrange the chuncks such that we overwrite a complete chunk only when it is not needed
/// - The cycle is thus era modulo (history limit + chunk length)
/// - `[0,1,2],[3,4,5],[6,7,8]`
/// - The second step is which chunk to add to:
/// - Divide the cycle by the chunk length and take the floor
/// - Floor(5 / 3) = 1
pub(crate) fn get_chunk_index_for_era(era: T::RewardEra) -> u32 {
let history_limit: u32 = T::ProviderBoostHistoryLimit::get();
let chunk_len = T::RewardPoolChunkLength::get();
// Remove one because eras are 1 indexed
let era_u32: u32 = era.saturating_sub(One::one()).into();

// Add one chunk so that we always have the full history limit in our chunks
let cycle: u32 = era_u32 % history_limit.saturating_add(chunk_len);
cycle.saturating_div(chunk_len)
}

// This is where the reward pool gets updated.
// Example with Limit 6, Chunk 2:
// - [0,1], [2,3], [4,5]
// - [6], [2,3], [4,5]
// - [6,7], [2,3], [4,5]
// - [6,7], [8], [4,5]
pub(crate) fn update_provider_boost_reward_pool(era: T::RewardEra, boost_total: BalanceOf<T>) {
// Current era is this era
let chunk_idx: u32 = Self::get_chunk_index_for_era(era);
let mut new_chunk =
ProviderBoostRewardPools::<T>::get(chunk_idx).unwrap_or(RewardPoolHistoryChunk::new()); // 1r

// If it is full we are resetting.
// This assumes that the chunk length is a divisor of the history limit
if new_chunk.is_full() {
new_chunk = RewardPoolHistoryChunk::new();
};

if new_chunk.try_insert(era, boost_total).is_err() {
// Handle the error case that should never happen
}
ProviderBoostRewardPools::<T>::set(chunk_idx, Some(new_chunk)); // 1w
}
}

/// Nontransferable functions are intended for capacity spend and recharge.
Expand Down
Loading

0 comments on commit b9466a8

Please sign in to comment.