Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/1207513338125664 delayed staking rewards #280

Merged
merged 32 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
65157e0
taking staking snapshot at start_session
talhadaar Aug 28, 2024
5392a56
implemented delayed payouts
talhadaar Aug 28, 2024
5f19c09
Merge branch 'dev' into feat/1207513338125664_delated-staking-rewards
talhadaar Aug 28, 2024
b808709
use take instead of get for AtStake
talhadaar Aug 28, 2024
68ff1e3
prepare to refresh staking snapshot even if selected candidates dont …
talhadaar Sep 3, 2024
7228c77
staking snapshot refreshes if collator set doesn't change
talhadaar Sep 4, 2024
102753f
setup storage migration for moving to delayed rewards distribution
talhadaar Sep 4, 2024
4abf945
fix build issue with parachain staking unit tests
talhadaar Sep 11, 2024
6b5427b
(bugfix) payout_collator at on_finalize and skip delayed reward calc …
talhadaar Sep 11, 2024
6471650
updated some unit tests, improved distribution mechanism to consider …
talhadaar Sep 11, 2024
c251d02
(chore): cargo fmt
talhadaar Sep 11, 2024
fb478bc
Fixued up most unit tests on parachain staking, ensure correct round …
talhadaar Sep 12, 2024
eedc4e2
(chore) cargo fmt
talhadaar Sep 12, 2024
ae7c784
fix coinbase_rewards_many_blocks_simple_check unit test in parachain …
talhadaar Sep 12, 2024
fce5808
fixed up further unit tests in parachain staking
talhadaar Sep 12, 2024
ca5c952
Some enw and some updated unit tests on parachain staking pallet, mit…
talhadaar Sep 13, 2024
81d5d4f
fixed unit tests on parachain staking, incorporate delayed staking re…
talhadaar Sep 13, 2024
1ed0367
Merge branch 'dev' into feat/1207513338125664_delated-staking-rewards
talhadaar Sep 16, 2024
7f301e3
use BlockNumberFor<T> instead of T::BlockNumber for parachain staking
talhadaar Sep 16, 2024
7e0b410
(chore): cargo fmt and clippy
talhadaar Sep 16, 2024
c30e3f5
(chore) minor cargo clippy fix
talhadaar Sep 16, 2024
72c9145
use end_session and new_session hooks in parachain staking
talhadaar Sep 25, 2024
39fddaa
minor fix to parachain-staking tests
talhadaar Sep 25, 2024
b09b3db
fixed prepare_delayed_rewards to use updated RoundInfo
talhadaar Sep 26, 2024
32b56d7
(chore) cargo clippy
talhadaar Sep 26, 2024
69710ce
Merge branch 'dev' into feat/1207513338125664_delated-staking-rewards
talhadaar Sep 26, 2024
fdb0fda
restrict sudo from forcing new round before payouts are finished
talhadaar Oct 10, 2024
1f580f3
(chore) cargo fmt and clippy
talhadaar Oct 10, 2024
ec1c859
remove unused code
talhadaar Oct 10, 2024
f98a2f6
sync with dev
talhadaar Nov 20, 2024
abb8596
added weights to parachain staking hooks
talhadaar Nov 20, 2024
faa5f66
account for ED when taking staking pot issuance
talhadaar Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 109 additions & 59 deletions pallets/parachain-staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,19 +189,22 @@ pub mod pallet {
use crate::{
set::OrderedSet,
types::{
BalanceOf, Candidate, CandidateOf, CandidateStatus, DelegationCounter, Delegator,
ReplacedDelegator, Reward, RoundInfo, Stake, StakeOf, TotalStake,
BalanceOf, Candidate, CandidateOf, CandidateStatus, DelayedPayoutInfoT,
DelegationCounter, Delegator, ReplacedDelegator, Reward, RoundInfo, Stake, StakeOf,
TotalStake,
},
weightinfo::WeightInfo,
};
use sp_runtime::Perquintill;

/// Kilt-specific lock for staking rewards.
pub(crate) const OLD_STAKING_ID: LockIdentifier = *b"kiltpstk";
/// Peaq-specific lock for staking rewards.
pub(crate) const STAKING_ID: LockIdentifier = *b"peaqstak";

/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(10);
const STORAGE_VERSION: StorageVersion =
StorageVersion::new(crate::migrations::Versions::V11 as u16);

/// Pallet for parachain staking.
#[pallet::pallet]
Expand Down Expand Up @@ -629,8 +632,15 @@ pub mod pallet {
/// We use this storage to store collator's block generation
#[pallet::storage]
#[pallet::getter(fn collator_blocks)]
pub(crate) type CollatorBlock<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, u32, ValueQuery>;
pub(crate) type CollatorBlocks<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
SessionIndex,
Twox64Concat,
T::AccountId,
u32,
ValueQuery,
>;

/// The maximum amount a collator candidate can stake.
#[pallet::storage]
Expand All @@ -648,6 +658,24 @@ pub mod pallet {
#[pallet::getter(fn new_round_forced)]
pub(crate) type ForceNewRound<T: Config> = StorageValue<_, bool, ValueQuery>;

#[pallet::storage]
#[pallet::getter(fn at_stake)]
/// Snapshot of collator delegation stake at the start of the round
pub(crate) type AtStake<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
SessionIndex,
Twox64Concat,
T::AccountId,
Candidate<T::AccountId, BalanceOf<T>, T::MaxDelegatorsPerCollator>,
OptionQuery,
>;

#[pallet::storage]
#[pallet::getter(fn delayed_payout_info)]
pub(crate) type DelayedPayoutInfo<T: Config> =
StorageValue<_, DelayedPayoutInfoT<SessionIndex, BalanceOf<T>>, ValueQuery>;

#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub stakers: GenesisStaker<T>,
Expand Down Expand Up @@ -2650,23 +2678,15 @@ pub mod pallet {
Ok(DelegationCounter { round: round.current, counter: counter.saturating_add(1) })
}

// [Post-launch TODO] Think about Collator stake or total stake?
// /// Attempts to add a collator candidate to the set of collator
// /// candidates which already reached its maximum size. On success,
// /// another collator with the minimum total stake is removed from the
// /// set. On failure, an error is returned. removing an already existing
// fn check_collator_candidate_inclusion(
// stake: Stake<T::AccountId, BalanceOf<T>>,
// mut candidates: OrderedSet<Stake<T::AccountId, BalanceOf<T>>,
// T::MaxTopCandidates>, ) -> Result<(), DispatchError> {
// todo!()
// }

// Public only for testing purpose
pub fn get_total_collator_staking_num() -> (Weight, BalanceOf<T>) {
/// [Post-launch TODO] Think about Collator stake or total stake?
/// Gives us the total stake of block authors and their delegators in a session
/// Public only for testing purpose
pub fn get_total_collator_staking_num(
session_index: SessionIndex,
) -> (Weight, BalanceOf<T>) {
let mut total_staking_in_session = BalanceOf::<T>::zero();
let mut read: u64 = 0;
CollatorBlock::<T>::iter().for_each(|(collator, num)| {
CollatorBlocks::<T>::iter_prefix(session_index).for_each(|(collator, num)| {
if let Some(state) = CandidatePool::<T>::get(collator) {
let collator_total = T::CurrencyBalance::from(num)
.checked_mul(&state.total)
Expand Down Expand Up @@ -2768,59 +2788,77 @@ pub mod pallet {
inner.try_into().expect("Did not extend vec q.e.d.")
}

fn peaq_reward_mechanism_impl() {
let mut reads = Weight::from_parts(0, 1);
let mut writes = Weight::from_parts(0, 1);
/// Get a unique, inaccessible account id from the `PotId`.
pub fn account_id() -> T::AccountId {
T::PotId::get().into_account_truncating()
}

/// Handles staking reward payout for previous session for one collator and their delegators
fn payout_collator() {
let pot = Self::account_id();
let issue_number = T::Currency::free_balance(&pot)
.checked_sub(&T::Currency::minimum_balance())
.unwrap_or_else(Zero::zero);
// get payout info for the last round
let payout_info = DelayedPayoutInfo::<T>::get();
sfffaaa marked this conversation as resolved.
Show resolved Hide resolved

let (in_reads, total_staking_in_session) = Self::get_total_collator_staking_num();
reads.saturating_add(in_reads);

// Here we also remove the all collator block after the iteration
CollatorBlock::<T>::iter().drain().for_each(|(collator, block_num)| {
// Get the delegator's staking number
if let Some(state) = CandidatePool::<T>::get(collator.clone()) {
// get one author
if let Some((author, block_num)) =
CollatorBlocks::<T>::iter_prefix(payout_info.round).drain().next()
{
// get collator's staking info
if let Some(state) = AtStake::<T>::take(payout_info.round, author) {
// calculate reward for collator from previous round
let now_reward = Self::get_collator_reward_per_session(
&state,
block_num,
total_staking_in_session,
issue_number,
payout_info.total_stake,
payout_info.total_issuance,
sfffaaa marked this conversation as resolved.
Show resolved Hide resolved
);

Self::do_reward(&pot, &now_reward.owner, now_reward.amount);
reads = reads.saturating_add(Weight::from_parts(1_u64, 0));
writes = writes.saturating_add(Weight::from_parts(1_u64, 0));

// calculate reward for collator's delegates from previous round
let now_rewards = Self::get_delgators_reward_per_session(
&state,
block_num,
total_staking_in_session,
issue_number,
payout_info.total_stake,
payout_info.total_issuance,
);

let len = now_rewards.len().saturated_into::<u64>();
now_rewards.into_iter().for_each(|x| {
Self::do_reward(&pot, &x.owner, x.amount);
});
reads = reads.saturating_add(Weight::from_parts(len, 0));
writes = writes.saturating_add(Weight::from_parts(len, 0));

// [TODO] add weights
}
reads = reads.saturating_add(Weight::from_parts(1_u64, 0));
});
}
}

frame_system::Pallet::<T>::register_extra_weight_unchecked(
T::DbWeight::get().reads_writes(reads.ref_time(), writes.ref_time()),
DispatchClass::Mandatory,
);
pub(crate) fn pot_issuance() -> BalanceOf<T> {
let pot = Self::account_id();
let total_issuance = T::Currency::free_balance(&pot)
.checked_sub(&T::Currency::minimum_balance())
.unwrap_or_else(Zero::zero);
total_issuance
}

/// Get a unique, inaccessible account id from the `PotId`.
pub fn account_id() -> T::AccountId {
T::PotId::get().into_account_truncating()
pub(crate) fn prepare_delayed_rewards(
collators: &Vec<T::AccountId>,
new_index: SessionIndex,
) {
// take snapshot of these collators' staking info
for collator in collators.iter() {
let collator_state = CandidatePool::<T>::get(collator).unwrap();
<AtStake<T>>::insert(new_index, collator, collator_state);
}

let old_index = new_index - 1;
// [TODO] what to do with this returned weight?
let (_, total_stake) = Self::get_total_collator_staking_num(old_index);
let total_issuance = Self::pot_issuance();

// take snapshot of previous session's staking totals for payout calculation
DelayedPayoutInfo::<T>::put(DelayedPayoutInfoT {
round: old_index,
total_stake,
total_issuance,
});
}
}

Expand All @@ -2836,8 +2874,11 @@ pub mod pallet {
/// - Writes: 1
/// # </weight>
fn note_author(author: T::AccountId) {
let block_num = <CollatorBlock<T>>::get(author.clone());
CollatorBlock::<T>::insert(author.clone(), block_num + 1);
// Querying will get us the current round, as PalletParachainStaking and PalletSession
// have not yet been initialized
let round = <Round<T>>::get().current;
let block_num = <CollatorBlocks<T>>::get(round, author.clone());
CollatorBlocks::<T>::insert(round, author.clone(), block_num + 1);
}
}

Expand All @@ -2859,13 +2900,22 @@ pub mod pallet {
DispatchClass::Mandatory,
);

let collators = Pallet::<T>::selected_candidates().to_vec();
if collators.is_empty() {
let selected_candidates = Pallet::<T>::selected_candidates().to_vec();
sfffaaa marked this conversation as resolved.
Show resolved Hide resolved
Self::prepare_delayed_rewards(&selected_candidates, new_index);

if selected_candidates.is_empty() {
// we never want to pass an empty set of collators. This would brick the chain.
log::error!("💥 keeping old session because of empty collator set!");

// get collators of previous session for snapshot
let old_collators = AtStake::<T>::iter_prefix(new_index - 1)
sfffaaa marked this conversation as resolved.
Show resolved Hide resolved
.map(|(id, _)| id)
.collect::<Vec<T::AccountId>>();
Self::prepare_delayed_rewards(&old_collators, new_index);
None
} else {
Some(collators)
Self::prepare_delayed_rewards(&selected_candidates, new_index);
Some(selected_candidates)
}
}

Expand All @@ -2883,7 +2933,7 @@ pub mod pallet {
/// 2. we need to clean up the state of the pallet.
fn end_session(end_index: SessionIndex) {
log::debug!("new_session: {:?}", end_index);
Self::peaq_reward_mechanism_impl();
Self::payout_collator();
}

fn start_session(_start_index: SessionIndex) {
Expand Down
67 changes: 53 additions & 14 deletions pallets/parachain-staking/src/migrations.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
//! Storage migrations for the parachain-staking pallet.

use crate::{
pallet::{Config, Pallet, OLD_STAKING_ID, STAKING_ID},
types::{AccountIdOf, Candidate, OldCandidate},
AtStake, CandidatePool, CollatorBlocks, ForceNewRound,
};
use frame_support::{
dispatch::GetStorageVersion, pallet_prelude::StorageVersion, traits::Get, weights::Weight,
dispatch::GetStorageVersion,
pallet_prelude::{StorageVersion, ValueQuery},
storage_alias,
traits::{Get, LockableCurrency, WithdrawReasons},
weights::Weight,
Twox64Concat,
};

use crate::pallet::{Config, Pallet};
use pallet_balances::Locks;
use sp_runtime::Permill;

// History of storage versions
#[derive(Default)]
enum Versions {
pub enum Versions {
_V7 = 7,
_V8 = 8,
V9 = 9,
#[default]
V10 = 10,
#[default]
V11 = 11,
}

pub(crate) fn on_runtime_upgrade<T: Config>() -> Weight {
upgrade::Migrate::<T>::on_runtime_upgrade()
}

mod upgrade {
use frame_support::traits::{LockableCurrency, WithdrawReasons};
use pallet_balances::Locks;
use sp_runtime::Permill;

use crate::{
pallet::{CandidatePool, OLD_STAKING_ID, STAKING_ID},
types::{Candidate, OldCandidate},
};

use super::*;

#[storage_alias]
type CollatorBlock<T: Config> =
StorageMap<Pallet<T>, Twox64Concat, AccountIdOf<T>, u32, ValueQuery>;
/// Migration implementation that deletes the old reward rate config and changes the staking ID.
pub struct Migrate<T>(sp_std::marker::PhantomData<T>);

Expand All @@ -40,6 +46,7 @@ mod upgrade {
let mut weight_writes = 0;
let mut weight_reads = 0;
let onchain_storage_version = Pallet::<T>::on_chain_storage_version();

if onchain_storage_version < StorageVersion::new(Versions::V9 as u16) {
// Change the STAKING_ID value
log::info!("Updating lock id from old staking ID to new staking ID.");
Expand All @@ -62,7 +69,8 @@ mod upgrade {
}
log::info!("V9 Migrating Done.");
}
if onchain_storage_version < StorageVersion::new(Versions::default() as u16) {

if onchain_storage_version < StorageVersion::new(Versions::V10 as u16) {
CandidatePool::<T>::translate(
|_key, old_candidate: OldCandidate<T::AccountId, T::CurrencyBalance, _>| {
let new_candidate = Candidate {
Expand All @@ -79,8 +87,39 @@ mod upgrade {
weight_reads += 1;
log::info!("V10 Migrating Done.");
}

if onchain_storage_version < StorageVersion::new(Versions::V11 as u16) {
log::info!(
"Running storage migration from version {:?} to {:?}",
onchain_storage_version,
Versions::default() as u16
);

let round = <Pallet<T>>::round();

// drain CollatorBlock StorageMap item
for (collator, blocknum) in <CollatorBlock<T>>::iter().drain() {
sfffaaa marked this conversation as resolved.
Show resolved Hide resolved
// migrate to CollatorBlocks StorageDoubleMap
<CollatorBlocks<T>>::insert(round.current, collator.clone(), blocknum);

// Backup collator staking stake for delayed rewards payment
let state = <CandidatePool<T>>::get(&collator).unwrap();
<AtStake<T>>::insert(round.current, collator.clone(), state);

weight_reads += 2;
weight_writes += 2;
}

// force start new session
<ForceNewRound<T>>::put(true);
weight_writes += 1;

log::info!("V11 Migrating Done.");
}
// update onchain storage version
StorageVersion::new(Versions::default() as u16).put::<Pallet<T>>();
weight_writes += 1;

T::DbWeight::get().reads_writes(weight_reads, weight_writes)
}
}
Expand Down
11 changes: 11 additions & 0 deletions pallets/parachain-staking/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,14 @@ pub type BalanceOf<T> = <<T as Config>::Currency as Currency<AccountIdOf<T>>>::B
pub type CandidateOf<T, S> = Candidate<AccountIdOf<T>, BalanceOf<T>, S>;
pub type MaxDelegatorsPerCollator<T> = <T as Config>::MaxDelegatorsPerCollator;
pub type StakeOf<T> = Stake<AccountIdOf<T>, BalanceOf<T>>;

#[derive(Default, Clone, Encode, Decode, RuntimeDebug, PartialEq, Eq, TypeInfo, MaxEncodedLen)]
/// Info needed to make delayed payments to stakers after round end
pub struct DelayedPayoutInfoT<SessionIndex, Balance: Default> {
/// The round index for which payouts should be made
pub round: SessionIndex,
/// total stake in the round
pub total_stake: Balance,
/// total issuance for round
pub total_issuance: Balance,
}