Complete Staking Pallet Analysis: mod.rs and impls.rs¶
Introduction¶
The Staking pallet is the central component of the Proof-of-Stake system in Taler blockchain. It implements a complex economic model with support for standard and vivid staking, validator management, nomination and reward distribution.
File Structure¶
1. mod.rs - Main Pallet Structure¶
1.1 Imports and Dependencies¶
// Main dependencies
use frame_election_provider_support::{ElectionProvider, ElectionProviderBase, SortedListProvider, VoteWeight};
use frame_support::{pallet_prelude::*, traits::{Currency, Defensive, EnsureOrigin, ...}};
use sp_staking::{EraIndex, SessionIndex, StakingAccount};
1.2 Configuration (Config trait)¶
Currency Types: - Currency: Main currency for staking (LockableCurrency) - CurrencyBalance: Balance type with constraints - CurrencyToVote: Balance to vote conversion
Time Parameters: - UnixTime: Time for calculating era duration - SessionsPerEra: Number of sessions per era - BondingDuration: Bonding duration - SlashDeferDuration: Deferred slash application
Economic Parameters: - IssuanceLimit: Token issuance limit - StandartStakingInterest: Standard interest rate - VividStakingInterestPerMonth: Additional rate per months
System Components: - ElectionProvider: Validator election provider - VoterList: List of voters (nominators) - TargetList: List of targets (validators) - SessionInterface: Interface for session management
1.3 Storage (Data Storage)¶
Account Management:
pub type Bonded<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, T::AccountId>;
pub type Ledger<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, StakingLedger<T>>;
pub type Payee<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, RewardDestination<T::AccountId>>;
Validators and Nominators:
pub type Validators<T: Config> = CountedStorageMap<_, Twox64Concat, T::AccountId, ValidatorPrefs>;
pub type Nominators<T: Config> = CountedStorageMap<_, Twox64Concat, T::AccountId, Nominations<T>>;
Era Management:
pub type CurrentEra<T> = StorageValue<_, EraIndex>;
pub type ActiveEra<T> = StorageValue<_, ActiveEraInfo>;
pub type ErasStakers<T: Config> = StorageDoubleMap<_, Twox64Concat, EraIndex, Twox64Concat, T::AccountId, Exposure<T::AccountId, BalanceOf<T>>>;
1.4 Events¶
EraPaid: Era payoutRewarded: Staker rewardSlashed: Staker slashBonded: Bonding fundsUnbonded: Unbonding fundsVividStakingScheduled: Vivid staking schedulingVividStakingCancelled: Vivid staking cancellation
1.5 Errors¶
NotController/NotStash: Wrong account typeAlreadyBonded/AlreadyPaired: Already linked accountsInsufficientBond: Insufficient bondVividStakingInProgress: Vivid staking in progress
1.6 Dispatchable Functions¶
Main Operations: - bond(): Bonding funds - bond_extra(): Additional bonding - unbond(): Unbonding funds - withdraw_unbonded(): Withdrawing funds
Vivid Staking: - vivid(): Activate vivid staking - unvivid(): Cancel vivid staking
Validation: - validate(): Become validator - nominate(): Nominate validators - chill(): Stop participation
2. impls.rs - Logic Implementation¶
2.1 Main Data Structures¶
StakingLedger:
pub struct StakingLedger<T: Config> {
pub stash: T::AccountId, // Stash account
pub total: BalanceOf<T>, // Total balance
pub active: BalanceOf<T>, // Active balance
pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
pub claimed_rewards: BoundedVec<EraIndex, T::HistoryDepth>,
pub vivid_staking: Option<VividStakingState>,
pub controller: Option<T::AccountId>,
}
Exposure:
pub struct Exposure<AccountId, Balance: HasCompact> {
pub total: Balance, // Total exposure
pub own: Balance, // Own exposure
pub others: Vec<IndividualExposure<AccountId, Balance>>,
pub vivid_staking: Option<VividStakingState>,
}
2.2 Key Functions¶
Ledger Management:
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()))
}
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()
}
Reward Payout:
pub(super) fn do_payout_stakers(
validator_stash: T::AccountId,
era: EraIndex,
) -> DispatchResultWithPostInfo {
// Era validation
let current_era = CurrentEra::<T>::get().ok_or_else(|| {
Error::<T>::InvalidEraToReward.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
})?;
// 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())
}
2.3 Election Provider Implementation¶
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
}
2.4 Session Management¶
SessionManager Implementation:
impl<T: Config> pallet_session::SessionManager<T::AccountId> for Pallet<T> {
fn new_session(new_index: SessionIndex) -> Option<Vec<T::AccountId>> {
log!(trace, "planning new session {}", new_index);
CurrentPlannedSession::<T>::put(new_index);
Self::new_session(new_index, false).map(|v| v.into_inner())
}
fn start_session(start_index: SessionIndex) {
log!(trace, "starting session {}", start_index);
Self::start_session(start_index)
}
fn end_session(end_index: SessionIndex) {
log!(trace, "ending session {}", end_index);
Self::end_session(end_index)
}
}
2.5 Runtime API¶
API for External Calls:
pub fn api_nominations_quota(balance: BalanceOf<T>) -> u32 {
T::NominationsQuota::get_quota(balance)
}
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
})
}
Taler Staking Features¶
1. Vivid Staking¶
- Additional interest for long-term locking
- Parameters:
starting_era,ending_era - Calculation:
StandartStakingInterest + VividStakingInterestPerMonth * months
2. Issuance Limits¶
IssuanceLimit: Maximum token issuance- Control through
StakingActiveflag - Stop staking when limit is reached
3. Custom Data Types¶
VividStakingState: Vivid staking state- Extended
Exposurewith vivid support - Special events for vivid operations
4. Economic Model¶
- Base rate:
StandartStakingInterest - Additional rate:
VividStakingInterestPerMonth * months - Issuance limit:
IssuanceLimit - Control through
StakingActive
Architectural Decisions¶
1. Separation of Concerns¶
mod.rs: Structure, configuration, events, errorsimpls.rs: Business logic, algorithms, interfaces
2. Modularity¶
- Separate modules for slashing, weights, benchmarking
- Clear separation of storage, events, errors
3. Performance¶
- Optimized storage operations
- BoundedVec for size limitations
- Efficient election algorithms
4. Security¶
- Checks at all levels
- Defensive programming
- Input data validation
Conclusion¶
The Staking pallet is a complex Proof-of-Stake management system with support for: - Standard staking - Vivid staking with additional interest - Flexible reward system - Administrative management - Runtime API for external integrations
The architecture provides high performance, security and scalability of the staking system.