Detailed Analysis of Key Staking Pallet Functions¶
1. Staking Ledger Management¶
1.1 Getting and Updating Ledger¶
pub fn ledger(account: StakingAccount<T::AccountId>) -> Result<StakingLedger<T>, Error<T>> {
StakingLedger::<T>::get(account)
}
pub fn bonded(stash: &T::AccountId) -> Option<T::AccountId> {
StakingLedger::<T>::paired_account(Stash(stash.clone()))
}
Purpose: Managing relationships between stash and controller accounts, retrieving staking information.
1.2 Slashable Balance Calculation¶
pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf<T> {
Self::ledger(Stash(stash.clone())).map(|l| l.active).unwrap_or_default()
}
pub fn slashable_balance_of_vote_weight(
stash: &T::AccountId,
issuance: BalanceOf<T>,
) -> VoteWeight {
T::CurrencyToVote::to_vote(Self::slashable_balance_of(stash), issuance)
}
Purpose: Determining the amount that can be slashed and converting to votes for elections.
2. Interest and Reward System¶
2.1 Interest Calculation¶
fn calculate_interest(era: EraIndex, opt_vivid_staking: &Option<VividStakingState>) -> Perbill {
if let Some(vivid_staking) = opt_vivid_staking {
if vivid_staking.starting_era <= era && vivid_staking.ending_era >= era {
let eras_count = vivid_staking
.ending_era
.saturating_sub(vivid_staking.starting_era.saturating_sub(1));
let months_count = eras_count / 28;
return T::StandartStakingInterest::get()
+ Perbill::from_parts(
T::VividStakingInterestPerMonth::get().deconstruct() * months_count,
);
}
}
T::StandartStakingInterest::get()
}
Calculation Logic: 1. Check vivid staking activity in current era 2. Count number of locked months 3. Calculation: base_rate + additional_rate * months
2.2 Reward Payout¶
pub(super) fn do_payout_stakers(
validator_stash: T::AccountId,
era: EraIndex,
) -> DispatchResultWithPostInfo {
// Input data validation
let current_era = CurrentEra::<T>::get().ok_or_else(|| {
Error::<T>::InvalidEraToReward
.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
})?;
// Era history check
let history_depth = T::HistoryDepth::get();
ensure!(
era <= current_era && era >= current_era.saturating_sub(history_depth),
Error::<T>::InvalidEraToReward
);
// Get ledger and check for already claimed rewards
let mut ledger = Self::ledger(account.clone())?;
match ledger.claimed_rewards.binary_search(&era) {
Ok(_) => return Err(Error::<T>::AlreadyClaimed),
Err(pos) => ledger.claimed_rewards.try_insert(pos, era)?,
}
// Get era exposure
let exposure = <ErasStakersClipped<T>>::get(&era, &stash);
// Calculate rewards for nominators
for nominator in exposure.others.iter() {
let interest = Self::calculate_interest(era, &nominator.vivid_staking);
let mut nominator_reward: BalanceOf<T> = portion * interest * nominator.value;
// Deduct validator commission
validator_payment_commissions += validator_commission * nominator_reward;
nominator_reward -= validator_commission * nominator_reward;
// Create payout
if let Some((imbalance, dest)) = Self::make_payout(&nominator.who, nominator_reward) {
nominator_payout_count += 1;
Self::deposit_event(Event::<T>::Rewarded {
stash: nominator.who.clone(),
dest,
amount: imbalance.peek(),
});
total_imbalance.subsume(imbalance);
}
}
// Calculate rewards for validator
let validator_own_interest = Self::calculate_interest(era, &exposure.vivid_staking);
let validator_own_reward = portion * validator_own_interest * exposure.own;
let validator_reward = validator_own_reward + validator_payment_commissions;
// Payout to validator
if let Some((imbalance, dest)) = Self::make_payout(&stash, validator_reward) {
Self::deposit_event(Event::<T>::Rewarded {
stash: stash.clone(),
dest,
amount: imbalance.peek(),
});
total_imbalance.subsume(imbalance);
}
T::Reward::on_unbalanced(total_imbalance);
Ok(Some(T::WeightInfo::payout_stakers_alive_staked(nominator_payout_count)).into())
}
Key Steps: 1. Era validation and payout permission check 2. Get exposure (nominators and their stakes) 3. Calculate interest considering vivid staking 4. Deduct validator commission 5. Create payouts via make_payout()
2.3 Creating Payouts¶
fn make_payout(
stash: &T::AccountId,
amount: BalanceOf<T>,
) -> Option<(PositiveImbalanceOf<T>, RewardDestination<T::AccountId>)> {
let dest = Self::payee(StakingAccount::Stash(stash.clone()));
let maybe_imbalance = match dest {
RewardDestination::Controller => Self::bonded(stash)
.map(|controller| T::Currency::deposit_creating(&controller, amount)),
RewardDestination::Stash => T::Currency::deposit_into_existing(stash, amount).ok(),
RewardDestination::Staked => Self::ledger(Stash(stash.clone()))
.and_then(|mut ledger| {
ledger.active += amount;
ledger.total += amount;
let r = T::Currency::deposit_into_existing(stash, amount).ok();
let _ = ledger.update().defensive_proof("ledger fetched from storage, so it exists; qed.");
Ok(r)
})
.unwrap_or_default(),
RewardDestination::Account(dest_account) => {
Some(T::Currency::deposit_creating(&dest_account, amount))
},
RewardDestination::None => None,
};
maybe_imbalance.map(|imbalance| (imbalance, Self::payee(StakingAccount::Stash(stash.clone()))))
}
Reward Recipient Types: - Controller: To controller account - Stash: To stash account - Staked: Automatic addition to stake - Account: To specified account - None: No payout
3. Era Management¶
3.1 Planning New Session¶
fn new_session(
session_index: SessionIndex,
is_genesis: bool,
) -> Option<BoundedVec<T::AccountId, MaxWinnersOf<T>>> {
if let Some(current_era) = Self::current_era() {
let current_era_start_session_index = Self::eras_start_session_index(current_era)
.unwrap_or_else(|| {
frame_support::print("Error: start_session_index must be set for current_era");
0
});
let era_length = session_index.saturating_sub(current_era_start_session_index);
match ForceEra::<T>::get() {
Forcing::ForceNew => (),
Forcing::ForceAlways => (),
Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => (),
_ => return None,
}
let maybe_new_era_validators = Self::try_trigger_new_era(session_index, is_genesis);
if maybe_new_era_validators.is_some() && matches!(ForceEra::<T>::get(), Forcing::ForceNew) {
Self::set_force_era(Forcing::NotForcing);
}
maybe_new_era_validators
} else {
Self::try_trigger_new_era(session_index, is_genesis)
}
}
Planning Logic: 1. Check current era 2. Analyze forcing modes (ForceNew, ForceAlways) 3. Check if session limit in era is reached 4. Trigger new era if necessary
3.2 Starting New Era¶
fn start_era(start_session: SessionIndex) {
let active_era = ActiveEra::<T>::mutate(|active_era| {
let new_index = active_era.as_ref().map(|info| info.index + 1).unwrap_or(0);
*active_era = Some(ActiveEraInfo {
index: new_index,
start: None,
});
new_index
});
let bonding_duration = T::BondingDuration::get();
BondedEras::<T>::mutate(|bonded| {
bonded.push((active_era, start_session));
if active_era > bonding_duration {
let first_kept = active_era - bonding_duration;
let n_to_prune = bonded.iter().take_while(|&&(era_idx, _)| era_idx < first_kept).count();
for (pruned_era, _) in bonded.drain(..n_to_prune) {
slashing::clear_era_metadata::<T>(pruned_era);
}
if let Some(&(_, first_session)) = bonded.first() {
T::SessionInterface::prune_historical_up_to(first_session);
}
}
});
Self::apply_unapplied_slashes(active_era);
}
Actions on Era Start: 1. Increment active era index 2. Update BondedEras 3. Clean old data (slashing metadata) 4. Apply deferred slashes
3.3 Ending Era¶
fn end_era(active_era: ActiveEraInfo, _session_index: SessionIndex) {
if let Some(active_era_start) = active_era.start {
let now_as_millis_u64 = T::UnixTime::now().as_millis().saturated_into::<u64>();
let era_duration = (now_as_millis_u64 - active_era_start).saturated_into::<u64>();
let staked = Self::eras_total_stake(&active_era.index);
let issuance = T::Currency::total_issuance();
let portion = Perbill::from_rational(era_duration as u64, MILLISECONDS_PER_YEAR);
let estimated_interest = T::StandartStakingInterest::get()
+ Perbill::from_parts(T::VividStakingInterestPerMonth::get().deconstruct() * 12);
let estimated_payout = estimated_interest * portion * staked;
if (issuance + estimated_payout) <= T::IssuanceLimit::get() && <StakingActive<T>>::get() {
Self::deposit_event(Event::<T>::EraPaid { era_index: active_era.index });
<ErasDuration<T>>::insert(&active_era.index, era_duration);
} else {
<StakingActive<T>>::set(false);
}
<OffendingValidators<T>>::kill();
}
}
Ending Logic: 1. Calculate era duration 2. Estimate payouts considering issuance limit 3. Stop staking when limit is exceeded 4. Clean offender data
4. Election Provider Implementation¶
4.1 Getting Voters¶
pub fn get_npos_voters(bounds: DataProviderBounds) -> Vec<VoterOf<Self>> {
let mut voters_size_tracker: StaticTracker<Self> = StaticTracker::default();
let final_predicted_len = {
let all_voter_count = T::VoterList::count();
bounds.count.unwrap_or(all_voter_count.into()).min(all_voter_count.into()).0
};
let mut all_voters = Vec::<_>::with_capacity(final_predicted_len as usize);
let weight_of = Self::weight_of_fn();
let mut voters_seen = 0u32;
let mut validators_taken = 0u32;
let mut nominators_taken = 0u32;
let mut min_active_stake = u64::MAX;
let mut sorted_voters = T::VoterList::iter();
while all_voters.len() < final_predicted_len as usize
&& voters_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32)
{
let voter = match sorted_voters.next() {
Some(voter) => {
voters_seen.saturating_inc();
voter
},
None => break,
};
let voter_weight = weight_of(&voter);
if voter_weight.is_zero() {
continue;
}
if let Some(Nominations { targets, .. }) = <Nominators<T>>::get(&voter) {
if !targets.is_empty() {
let voter = (voter, voter_weight, targets);
if voters_size_tracker.try_register_voter(&voter, &bounds).is_err() {
Self::deposit_event(Event::<T>::SnapshotVotersSizeExceeded {
size: voters_size_tracker.size as u32,
});
break;
}
all_voters.push(voter);
nominators_taken.saturating_inc();
}
} else if Validators::<T>::contains_key(&voter) {
let self_vote = (
voter.clone(),
voter_weight,
vec![voter.clone()].try_into().expect("MaxVotesPerVoter must be >= 1"),
);
if voters_size_tracker.try_register_voter(&self_vote, &bounds).is_err() {
Self::deposit_event(Event::<T>::SnapshotVotersSizeExceeded {
size: voters_size_tracker.size as u32,
});
break;
}
all_voters.push(self_vote);
validators_taken.saturating_inc();
}
min_active_stake = if voter_weight < min_active_stake { voter_weight } else { min_active_stake };
}
MinimumActiveStake::<T>::put(min_active_stake);
all_voters
}
Voter Retrieval Algorithm: 1. Iterate through VoterList 2. Filter by weight (exclude zero weights) 3. Separate nominators and validators 4. Apply bounds constraints 5. Track minimum active stake
4.2 Getting Targets¶
pub fn get_npos_targets(bounds: DataProviderBounds) -> Vec<T::AccountId> {
let mut targets_size_tracker: StaticTracker<Self> = StaticTracker::default();
let final_predicted_len = {
let all_target_count = T::TargetList::count();
bounds.count.unwrap_or(all_target_count.into()).min(all_target_count.into()).0
};
let mut all_targets = Vec::<T::AccountId>::with_capacity(final_predicted_len as usize);
let mut targets_seen = 0;
let mut targets_iter = T::TargetList::iter();
while all_targets.len() < final_predicted_len as usize
&& targets_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32)
{
let target = match targets_iter.next() {
Some(target) => {
targets_seen.saturating_inc();
target
},
None => break,
};
if targets_size_tracker.try_register_target(target.clone(), &bounds).is_err() {
Self::deposit_event(Event::<T>::SnapshotTargetsSizeExceeded {
size: targets_size_tracker.size as u32,
});
break;
}
if Validators::<T>::contains_key(&target) {
all_targets.push(target);
}
}
all_targets
}
5. Runtime API¶
5.1 Nominations Quota¶
pub fn api_nominations_quota(balance: BalanceOf<T>) -> u32 {
T::NominationsQuota::get_quota(balance)
}
5.2 Payout Estimation¶
pub fn api_estimate_staking_payout(account: T::AccountId) -> BalanceOf<T> {
let Some(active_era) = Self::active_era() else { return Zero::zero() };
let era_duration: u64 = T::SessionsPerEra::get()
.saturating_mul(T::NextNewSession::average_session_length().saturated_into())
.saturating_mul(T::ExpectedBlockTime::get().saturated_into())
.try_into().unwrap_or(0);
let portion = Perbill::from_rational(era_duration, MILLISECONDS_PER_YEAR);
let Some(Nominations { targets, .. }) = Self::nominators(&account) else {
return Zero::zero();
};
targets.iter().fold(Zero::zero(), |acc, validator| {
let exposure = Self::eras_stakers_clipped(active_era.index, validator);
let Some(nominator_exposure) = exposure.others.iter().find(|&x| x.who == account) else {
return acc;
};
let validator_prefs = Self::eras_validator_prefs(&active_era.index, &validator);
let interest = Self::calculate_interest(active_era.index, &nominator_exposure.vivid_staking);
let nominator_reward: BalanceOf<T> = portion
* interest * Perbill::one()
.saturating_sub(validator_prefs.commission)
* nominator_exposure.value;
acc + nominator_reward
})
}
Estimation Algorithm: 1. Get active era 2. Calculate era duration 3. Iterate through nominated validators 4. Calculate rewards considering vivid staking and commissions 5. Sum all potential payouts
Conclusion¶
The Staking pallet implements a complex economic model with support for: - Flexible interest system (base + vivid) - Issuance limits - Efficient election algorithms - Runtime API for external integrations - Protection against abuse
The architecture provides high performance and security for the Proof-of-Stake system.