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

stakingv3: rank dapps in-between tiers #1240

Merged
merged 24 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion pallets/dapp-staking-v3/rpc/runtime-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

#![cfg_attr(not(feature = "std"), no_std)]

use astar_primitives::dapp_staking::{DAppId, EraNumber, PeriodNumber, TierId};
use astar_primitives::dapp_staking::{DAppId, EraNumber, PeriodNumber, RankedTier, TierId};
use astar_primitives::BlockNumber;
pub use sp_std::collections::btree_map::BTreeMap;

Expand All @@ -27,6 +27,7 @@ sp_api::decl_runtime_apis! {
/// dApp Staking Api.
///
/// Used to provide information otherwise not available via RPC.
#[api_version(2)]
pub trait DappStakingApi {

/// How many periods are there in one cycle.
Expand All @@ -42,6 +43,10 @@ sp_api::decl_runtime_apis! {
fn blocks_per_era() -> BlockNumber;

/// Get dApp tier assignment for the given dApp.
#[changed_in(2)]
fn get_dapp_tier_assignment() -> BTreeMap<DAppId, TierId>;

/// Get dApp ranked tier assignment for the given dApp.
fn get_dapp_tier_assignment() -> BTreeMap<DAppId, RankedTier>;
Dinonard marked this conversation as resolved.
Show resolved Hide resolved
}
}
9 changes: 6 additions & 3 deletions pallets/dapp-staking-v3/src/benchmarking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,7 @@ mod benchmarks {

#[block]
{
let (dapp_tiers, _) = Pallet::<T>::get_dapp_tier_assignment_and_rewards(
let (dapp_tiers, _count) = Pallet::<T>::get_dapp_tier_assignment_and_rewards(
reward_era,
reward_period,
reward_pool,
Expand Down Expand Up @@ -1041,14 +1041,17 @@ mod benchmarks {
&cleanup_marker.dapp_tiers_index,
DAppTierRewardsFor::<T> {
dapps: (0..T::MaxNumberOfContracts::get())
.map(|dapp_id| (dapp_id as DAppId, 0))
.collect::<BTreeMap<DAppId, TierId>>()
.map(|dapp_id| (dapp_id as DAppId, RankedTier::new_saturated(0, 0)))
.collect::<BTreeMap<DAppId, RankedTier>>()
.try_into()
.expect("Using `MaxNumberOfContracts` as length; QED."),
rewards: vec![1_000_000_000_000; T::NumberOfTiers::get() as usize]
.try_into()
.expect("Using `NumberOfTiers` as length; QED."),
period: 1,
rank_rewards: vec![0; T::NumberOfTiers::get() as usize]
.try_into()
.expect("Using `NumberOfTiers` as length; QED."),
},
);

Expand Down
124 changes: 83 additions & 41 deletions pallets/dapp-staking-v3/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ pub use sp_std::vec::Vec;
use astar_primitives::{
dapp_staking::{
AccountCheck, CycleConfiguration, DAppId, EraNumber, Observer as DAppStakingObserver,
PeriodNumber, SmartContractHandle, StakingRewardHandler, TierId, TierSlots as TierSlotFunc,
PeriodNumber, Rank, RankedTier, SmartContractHandle, StakingRewardHandler, TierId,
TierSlots as TierSlotFunc,
},
oracle::PriceProvider,
Balance, BlockNumber,
Expand All @@ -71,7 +72,9 @@ mod benchmarking;
mod types;
pub use types::*;

pub mod migration;
pub mod weights;

pub use weights::WeightInfo;

const LOG_TARGET: &str = "dapp-staking";
Expand All @@ -91,7 +94,7 @@ pub mod pallet {
use super::*;

/// The current storage version.
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(6);
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(7);

#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
Expand Down Expand Up @@ -198,6 +201,10 @@ pub mod pallet {
#[pallet::constant]
type NumberOfTiers: Get<u32>;

/// Tier ranking enabled.
#[pallet::constant]
type RankingEnabled: Get<bool>;
Dinonard marked this conversation as resolved.
Show resolved Hide resolved

/// Weight info for various calls & operations in the pallet.
type WeightInfo: WeightInfo;

Expand Down Expand Up @@ -289,6 +296,7 @@ pub mod pallet {
beneficiary: T::AccountId,
smart_contract: T::SmartContract,
tier_id: TierId,
rank: Rank,
Copy link

@GuiGou12358 GuiGou12358 Jun 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,
IMHO, it was better to add the new field at the end to be backwards compatible with existing clients listening to your events.
Changing the order of fields introduces a breaking change for events consumers.
Adding a new field at the end doesn't necessarily break the consumer: the new field is ignored but it still work.
Hoping this comment can be helpful for the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to query by key. Any reason why you are querying by index?

era: EraNumber,
amount: Balance,
},
Expand Down Expand Up @@ -1403,14 +1411,16 @@ pub mod pallet {
Error::<T>::RewardExpired
);

let (amount, tier_id) =
let (amount, ranked_tier) =
dapp_tiers
.try_claim(dapp_info.id)
.map_err(|error| match error {
DAppTierError::NoDAppInTiers => Error::<T>::NoClaimableRewards,
_ => Error::<T>::InternalClaimDAppError,
})?;

let (tier_id, rank) = ranked_tier.deconstruct();

// Get reward destination, and deposit the reward.
let beneficiary = dapp_info.reward_beneficiary();
T::StakingRewardHandler::payout_reward(&beneficiary, amount)
Expand All @@ -1423,6 +1433,7 @@ pub mod pallet {
beneficiary: beneficiary.clone(),
smart_contract,
tier_id,
rank,
era,
amount,
});
Expand Down Expand Up @@ -1670,7 +1681,7 @@ pub mod pallet {
}

/// Returns the dApp tier assignment for the current era, based on the current stake amounts.
pub fn get_dapp_tier_assignment() -> BTreeMap<DAppId, TierId> {
pub fn get_dapp_tier_assignment() -> BTreeMap<DAppId, RankedTier> {
let protocol_state = ActiveProtocolState::<T>::get();

let (dapp_tiers, _count) = Self::get_dapp_tier_assignment_and_rewards(
Expand All @@ -1691,7 +1702,11 @@ pub mod pallet {
///
/// 2. Sort the entries by the score, in descending order - the top score dApp comes first.
///
/// 3. Read in tier configuration. This contains information about how many slots per tier there are,
/// 3. Calculate rewards for each tier.
/// This is done by dividing the total reward pool into tier reward pools,
/// after which the tier reward pool is divided by the number of available slots in the tier.
///
/// 4. Read in tier configuration. This contains information about how many slots per tier there are,
/// as well as the threshold for each tier. Threshold is the minimum amount of stake required to be eligible for a tier.
/// Iterate over tier thresholds & capacities, starting from the top tier, and assign dApps to them.
///
Expand All @@ -1705,10 +1720,6 @@ pub mod pallet {
/// ```
/// (Sort the entries by dApp ID, in ascending order. This is so we can efficiently search for them using binary search.)
///
/// 4. Calculate rewards for each tier.
/// This is done by dividing the total reward pool into tier reward pools,
/// after which the tier reward pool is divided by the number of available slots in the tier.
///
/// The returned object contains information about each dApp that made it into a tier.
/// Alongside tier assignment info, number of read DB contract stake entries is returned.
pub(crate) fn get_dapp_tier_assignment_and_rewards(
Expand Down Expand Up @@ -1737,47 +1748,16 @@ pub mod pallet {
// Sort by amount staked, in reverse - top dApp will end in the first place, 0th index.
dapp_stakes.sort_unstable_by(|(_, amount_1), (_, amount_2)| amount_2.cmp(amount_1));

// 3.
// Iterate over configured tier and potential dApps.
// Each dApp will be assigned to the best possible tier if it satisfies the required condition,
// and tier capacity hasn't been filled yet.
let mut dapp_tiers = BTreeMap::new();
let tier_config = TierConfig::<T>::get();

let mut global_idx = 0;
let mut tier_id = 0;
for (tier_capacity, tier_threshold) in tier_config
.slots_per_tier
.iter()
.zip(tier_config.tier_thresholds.iter())
{
let max_idx = global_idx
.saturating_add(*tier_capacity as usize)
.min(dapp_stakes.len());

// Iterate over dApps until one of two conditions has been met:
// 1. Tier has no more capacity
// 2. dApp doesn't satisfy the tier threshold (since they're sorted, none of the following dApps will satisfy the condition either)
for (dapp_id, stake_amount) in dapp_stakes[global_idx..max_idx].iter() {
if tier_threshold.is_satisfied(*stake_amount) {
global_idx.saturating_inc();
dapp_tiers.insert(*dapp_id, tier_id);
} else {
break;
}
}

tier_id.saturating_inc();
}

// In case when tier has 1 more free slot, but two dApps with exactly same score satisfy the threshold,
// one of them will be assigned to the tier, and the other one will be assigned to the lower tier, if it exists.
//
// In the current implementation, the dApp with the lower dApp Id has the advantage.
// There is no guarantee this will persist in the future, so it's best for dApps to do their
// best to avoid getting themselves into such situations.

// 4. Calculate rewards.
// 3. Calculate rewards.
let tier_rewards = tier_config
.reward_portion
.iter()
Expand All @@ -1791,6 +1771,67 @@ pub mod pallet {
})
.collect::<Vec<_>>();

// 4.
// Iterate over configured tier and potential dApps.
// Each dApp will be assigned to the best possible tier if it satisfies the required condition,
// and tier capacity hasn't been filled yet.
let mut dapp_tiers = BTreeMap::new();
let mut tier_slots = BTreeMap::new();

let mut upper_bound = Balance::zero();
let mut rank_rewards = Vec::new();

for (tier_id, (tier_capacity, tier_threshold)) in tier_config
.slots_per_tier
.iter()
.zip(tier_config.tier_thresholds.iter())
.enumerate()
{
let lower_bound = tier_threshold.threshold();

// Iterate over dApps until one of two conditions has been met:
// 1. Tier has no more capacity
// 2. dApp doesn't satisfy the tier threshold (since they're sorted, none of the following dApps will satisfy the condition either)
for (dapp_id, staked_amount) in dapp_stakes
.iter()
.skip(dapp_tiers.len())
.take_while(|(_, amount)| tier_threshold.is_satisfied(*amount))
.take(*tier_capacity as usize)
ermalkaleci marked this conversation as resolved.
Show resolved Hide resolved
{
let rank = if T::RankingEnabled::get() {
RankedTier::find_rank(lower_bound, upper_bound, *staked_amount)
} else {
0
};
tier_slots.insert(*dapp_id, RankedTier::new_saturated(tier_id as u8, rank));
}

// sum of all ranks for this tier
let ranks_sum = tier_slots
.iter()
.fold(0u32, |accum, (_, x)| accum.saturating_add(x.rank().into()));
Dinonard marked this conversation as resolved.
Show resolved Hide resolved

let reward_per_rank = if ranks_sum.is_zero() {
Balance::zero()
} else {
// calculate reward per rank
let tier_reward = tier_rewards.get(tier_id).copied().unwrap_or_default();
let empty_slots = tier_capacity.saturating_sub(tier_slots.len() as u16);
let remaining_reward = tier_reward.saturating_mul(empty_slots.into());
// make sure required reward doesn't exceed remaining reward
let reward_per_rank = tier_reward.saturating_div(RankedTier::MAX_RANK.into());
let expected_reward_for_ranks =
reward_per_rank.saturating_mul(ranks_sum.into());
let reward_for_ranks = expected_reward_for_ranks.min(remaining_reward);
// re-calculate reward per rank based on available reward
reward_for_ranks.saturating_div(ranks_sum.into())
};

rank_rewards.push(reward_per_rank);
dapp_tiers.append(&mut tier_slots);
upper_bound = lower_bound; // current threshold becomes upper bound for next tier
}

// 5.
// Prepare and return tier & rewards info.
// In case rewards creation fails, we just write the default value. This should never happen though.
Expand All @@ -1799,6 +1840,7 @@ pub mod pallet {
dapp_tiers,
tier_rewards,
period,
rank_rewards,
)
.unwrap_or_default(),
counter,
Expand Down
Loading
Loading