Skip to content

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 payout
  • Rewarded: Staker reward
  • Slashed: Staker slash
  • Bonded: Bonding funds
  • Unbonded: Unbonding funds
  • VividStakingScheduled: Vivid staking scheduling
  • VividStakingCancelled: Vivid staking cancellation

1.5 Errors

  • NotController/NotStash: Wrong account type
  • AlreadyBonded/AlreadyPaired: Already linked accounts
  • InsufficientBond: Insufficient bond
  • VividStakingInProgress: 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 StakingActive flag
  • Stop staking when limit is reached

3. Custom Data Types

  • VividStakingState: Vivid staking state
  • Extended Exposure with 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, errors
  • impls.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.