Substrate的Staking模块分析

Substrate的Staking模块分析

Staking模块被网络管理维护人员用来管理资产用来抵押。

抵押模块是一组网络维护人员(可以称为authorities,也可以叫validators)根据那些自愿把资产存入抵押池中来选择的方法模块。这些在抵押池中的资金,正常情况下会获得奖励,如果发现维护人员没有正确履行职责,则会被没收。

抵押模块术语

  • 抵押Staking:将资产锁定一段时间,使其面临大幅惩罚损失掉的风险,从而使其成为一个有回报的网络维护者的过程。
  • 验证Validating:运行一个节点来主动维护网络的过程,通过出块或保证链的最终一致性。
  • 提名Nominating:将抵押的资金置于一个或者多个验证者背后,以分享他们所接受的奖励和惩罚的过程。
  • 隐藏账号Stash account:持有一个所有者用于抵押的资金的账号。
  • 控制账号Controller account:控制所有者资金的账号。
  • 周期Era:验证者集合(和每个验证者的活跃提名集合)是在一个周期后需要重新计算的,并在那里支付奖励。
  • 惩罚Slash:通过减少抵押者的资产来惩罚抵押者。

抵押模块的目标

抵押系统在Substrate的NPoS共识机制下,被设计用来使得下面几个目标成为可能:

  • 抵押资产被一个冷钱包所控制。
  • 在不影响实体作用角色的情况下,提取或存入部分资产。
  • 以最小的开销可以在角色(提名者、验证者和空闲)之间切换。

Staking 抵押

几乎任何与Staking模块的交互都需要bonding进程,也就是账号要成为一个staker,账号要绑定,一个持有资金用来抵押的账号stash account,持有这部分资金被冻结,作为被抵押的资金,与此成对存在的是controller account,这个控制账号能发送操作控制指令。

Validating 验证

一个验证者它的角色是验证区块和保证区块最终一致性,维护网络的诚实。验证者应该避免任何恶意的错误行为和离线。声明有兴趣成为验证者的绑定账号不会立即被选择为验证者,相反,他们被宣布为候选人,他们可能在下届选举中被选为验证者。选举结果由提名者及其投票决定。

Nominator 提名者

一个提名者在维护网络时不承担任何直接的角色,而是对要选举的一组验证人进行投票。账号一旦声明了提名的利益,将在下一轮选举中生效。提名人stash账号中的资产表明其投票的重要性,验证者获得的奖励和惩罚都由验证者和提名者共享。这条规则鼓励提名者尽可能不要投票给行为不端/离线的验证者,因为如果提名者投错了票,他们也会失去资产。

奖励和惩罚

奖励和惩罚是抵押模块的核心,试图包含有效的行为,同时惩罚任何不当行为或缺乏可用性的行为。一旦有不当行为被报告,惩罚可以发生在任何时间点。一旦确定了惩罚,一个惩罚的值将从验证者的余额中扣除,同时也将从所有投票给该验证者的提名者的余额中扣除。与惩罚类似,奖励也在验证者和提名者之间共享。然而,奖励资金并不总是转移到stash账号。

Chilling 冷却

最后,上面的任何角色都可以选择暂时退一步,冷静一下。这意味着,如果他们是提名者,他们将不再被视为选民,如果他们是验证者,他们将不再是下次选举的候选人。

实现细节

Slot Stake

这个周期SlotStake会在整个section都会被用到。在每个era结尾都会计算一个value,包含所有validators的最小抵押value,当然这个抵押value包含自己抵押的和接收到别人的抵押。

Reward Calculation

在每个era结尾都会给Validators和Nominators奖励,总的奖励根据era duration和抵押比例(总的抵押币/总的供应币)来计算。

[authorship::EventHandler]在区块生产时增加奖励点。

validator能宣称一个总额,被命名为[validator_payment],那个在每个奖励偿付过程中不会跟nominators分享。这个validator_payment的币会从总奖励总减出来,总的奖励是分给validator和nominator的,减去这个validator_payment之后,就会按照比例分割后奖励给validator和nominator。

Additional Fund Management Operations

任何放到stash账号下的资产都可以有如下操作的目标:

  • controller账号能够将部分比例(不是所有)资产自由化,使用[unbond]方法调用。当然这个资产不是立马就可以用,一个解冻周期[BondingDuration]需要经过后,才可以移动这部分资产。当这个解冻周期过去后,可以调用[withdraw_unbonded]方法来实际提取这笔资产。

Election Algorithm

现在的选举算法是基于Phragmen来实现的。

选举算法除了选出具有最多利害关系值和票数的验证者外,还试图以平等的方式将提名者的选票分配给候选人。为了进一步确保这一点,可以应用一个可选的后处理,它迭代地规范化提名者所确定的值,直到某个提名者的投票总数差异小于阈值。

GenesisConfig

抵押模块是要依靠[GenesisConfig]。

相关的模块:

struct

/// 一个era中所有奖励的点,用来将总点奖励支出在所有validators中切分
#[derive(Encode, Decode, Default)]
pub struct EraPoints {
    /// 奖励点点总数,等于给每个validator的奖励点总和 
    total: Points,
    /// 一个指定validator的奖励点,vec中的索引对应到现在validator集合中的索引
    individual: Vec,
}

/// 在一个惩罚事件上的优先级
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)]
pub struct ValidatorPrefs {
    /// 预先奖励验证者;只有其余的被他们自己和提名者瓜分。
    #[codec(compact)]
    pub validator_payment: Balance,
}

/// 只是用一个 Balance/BlockNumber 元组去编码时,一大块资金将解锁。
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)]
pub struct UnlockChunk {
    /// 将要被解锁的资金总额
    #[codec(compact)]
    value: Balance,
    /// era轮次,在这个轮次上,奖励点会解锁
    #[codec(compact)]
    era: EraIndex,
}

/// 一个绑定的stash账号账本
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)]
pub struct StakingLedger {
    /// stash账号,其实际被锁定用来抵押的余额
    pub stash: AccountId,
    /// stash账号目前被统计的总币额,包括`active`加上`unlocking`的余额
    #[codec(compact)]
    pub total: Balance,
    /// stash账号中,将用来在任何将要来临轮次中抵押的总余额
    #[codec(compact)]
    pub active: Balance,
    /// stash中解锁的余额,将是变得自由,最终也会被转移出stash账号,当然其首先不要被惩罚掉
    pub unlocking: Vec>,
}

/// 一个个人提名者拥有可以被惩罚掉的暴露(这个暴露包括账号和余额)å
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, RuntimeDebug)]
pub struct IndividualExposure {
    /// 提名者的stash账号
    who: AccountId,
    /// 暴露的资金总额
    #[codec(compact)]
    value: Balance,
}

/// 系统中支持单个validator的抵押的快照。
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, Default, RuntimeDebug)]
pub struct Exposure {
    /// 支持这个validator的总币额
    #[codec(compact)]
    pub total: Balance,
    /// validator自己暴露的stash账号余额
    #[codec(compact)]
    pub own: Balance,
    /// 提名者暴露的stash账号和余额
    pub others: Vec>,
}

/// 一个惩罚事件被触发,惩罚validator一定数量的余额
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, Default, RuntimeDebug)]
pub struct SlashJournalEntry {
    who: AccountId,
    amount: Balance,
    own_slash: Balance, // the amount of `who`'s own exposure that was slashed
}

Storage

/// 抵押参与的Validator数量
pub ValidatorCount get(fn validator_count) config(): u32;
/// 在紧急条件被强加的时候,抵押参与的Validator的最小数量,默认是4
pub MinimumValidatorCount get(fn minimum_validator_count) config():
    u32 = DEFAULT_MINIMUM_VALIDATOR_COUNT;

/// 任何Validator,可能永远不会被惩罚或强行踢掉。它是一个Vec,因为它们易于初始化,并且性能影响很小(我们期望不超过4个Invulnerables),并且仅限于testnets。
pub Invulnerables get(fn invulnerables) config(): Vec;

/// controller账号控制的stash账号的Map
pub Bonded get(fn bonded): map T::AccountId => Option;
/// 所有controller账号对应到的抵押余额的Map
pub Ledger get(fn ledger):map T::AccountId => Option>>;

/// 当奖励发放的时候,key存放stash账号,value存放RewardDestination
pub Payee get(fn payee): map T::AccountId => RewardDestination;

/// validator的stash账号为key,value是validator自己的预先奖励
pub Validators get(fn validators): linked_map T::AccountId => ValidatorPrefs>;

/// 提名者自己stash账号为key,value为提名的所有validators
pub Nominators get(fn nominators): linked_map T::AccountId => Vec;

/// 某个Validator下面所有的抵押支持者,key为stash账号,value是Exposure
pub Stakers get(fn stakers): map T::AccountId => Exposure>;

/// 现在当选的validators集合,stash账号ID被用来作为key
pub CurrentElected get(fn current_elected): Vec;

/// 现在era的索引 
pub CurrentEra get(fn current_era) config(): EraIndex;

/// 现在era的开始
pub CurrentEraStart get(fn current_era_start): MomentOf;

/// 现在era开始的session索引
pub CurrentEraStartSessionIndex get(fn current_era_start_session_index): SessionIndex;

/// 现在era的奖励,使用现在当选集合的indices 
CurrentEraPointsEarned get(fn current_era_reward): EraPoints;

/// 对于没个validator slot中总的活跃余额
pub SlotStake get(fn slot_stake) build(|config: &GenesisConfig| {
    config.stakers.iter().map(|&(_, _, value, _)| value).min().unwrap_or_default()
}): BalanceOf;

/// 是否强制开启新的era
pub ForceEra get(fn force_era) config(): Forcing;

/// 分给举报者的惩罚金额的比例,剩下的惩罚余额由`Slash`来处理
pub SlashRewardFraction get(fn slash_reward_fraction) config(): Perbill;

/// A mapping from still-bonded eras to the first session index of that era.
BondedEras: Vec<(EraIndex, SessionIndex)>;

/// 在一个指定era中,所有发生的惩罚
EraSlashJournal get(fn era_slash_journal):
    map EraIndex => Vec>>;

初始化设定

add_extra_genesis {
    config(stakers):
        Vec<(T::AccountId, T::AccountId, BalanceOf, StakerStatus)>;
    build(|config: &GenesisConfig| {
        for &(ref stash, ref controller, balance, ref status) in &config.stakers {
            assert!(
                T::Currency::free_balance(&stash) >= balance,
                "Stash does not have enough balance to bond."
            );
            let _ = >::bond(
                T::Origin::from(Some(stash.clone()).into()),
                T::Lookup::unlookup(controller.clone()),
                balance,
                RewardDestination::Staked,
            );
            let _ = match status {
                StakerStatus::Validator => {
                    >::validate(
                        T::Origin::from(Some(controller.clone()).into()),
                        Default::default(),
                    )
                },
                StakerStatus::Nominator(votes) => {
                    >::nominate(
                        T::Origin::from(Some(controller.clone()).into()),
                        votes.iter().map(|l| T::Lookup::unlookup(l.clone())).collect(),
                    )
                }, _ => Ok(())
            };
        }
    });
}

Module

  • fn bond(origin,controller: ::Source,#[compact] value: BalanceOf, payee: RewardDestination)
    • origin作为stash账号,controller账号控制这个stash账号,value用来锁定,当然要大于minimum_balance,payee是Stash,Staked和Controller三个选择
  • fn bond_extra(origin,#[compact] max_additional: BalanceOf)
    • 增加stash账号下额外自由余额进入抵押
  • fn unbond(origin,#[compact] value: BalanceOf)
    • 把抵押中部分解锁解绑出来,如果剩下的小于T::Currency::minimum_balance(),则直接全部解绑
  • fn withdraw_unboned(origin)
    • 提取解绑的余额出来,这个需要controller账号来签名发起请求
  • fn validate(origin,prefs: ValidatorPrefs>)
    • 一个提名者声明作为一个验证者
  • fn nominate(origin,targets: Vec<::Source>)
    • 一个验证者声明做一个提名者
  • fn chill(origin)
    • 一个stash账号声明不做提名者和验证者
  • fn set_payee(origin,payee: RewardDestination)
    • 为一个controler设置payment
  • fn set_controller(origin, controller: ::Source)
    • 为一个stash账号设置一个controller账号
  • fn set_validator_count(origin,#[compact] new: u32)
    • 设置validators的理想数量
  • fn force_no_eras(origin)
    • Root调用,强制无限期的没有新的eras
  • fn force_new_eras(origin)
    • Root调用,在下一个session强制开启一个新的era
  • fn set_invulnerables(origin,validators: Vec)
    • Root调用,设置几个无敌账号,不会被惩罚到
  • fn force_unstake(origin,stash: T::AccountId)
    • Root调用,把某个抵押者直接剔除
  • fn force_new_era_always(origin)
    • Root调用,在sessions结束后无限制的开启一个新的era
  • pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf
    • 对于一个validator的controller账号总余额能用来惩罚
  • fn update_ledger(controller: &T::AccountId,ledger: &StakingLedger>)
    • 更新一个controller的ledger,这会更新stash的锁,会锁死所有资产,除了在接下来交易中的支付资产
  • fn slash_validator(stash: &T::AccountId,slash: BalanceOf,exposure: &Exposure>,journal: &mut Vec>>) -> NegativeImbalanceOf
    • 惩罚给定的validator,通过给定的exposure计算出具体的金额
  • fn make_payout(stash: &T::AccountId, amout: BalanceOf) -> Option>
    • 给一个抵押者实际的奖励支付
  • fn reward_validator(stash: &T::AccountId, reward: BalanceOf) -> PositiveImbalanceOf
    • 奖励一个validator指定的金额,添加奖励给到validator和其nominators的余额,当然在这之前,需要提前支付掉validator的提前支付,按照两者的exposure的比例来发放奖励
  • fn new_session(session_index: SessionIndex) -> Option<(Vec,Vec<(T::AccountId,Exposure>)>)>
    • session刚结束,提供一个validators集合给下一个session,如果在一个era的结束,同时也有之前validator集合的exposure
  • fn new_era(start_session_index: SessionIndex) -> Option>
    • era已经改变了,实施新的抵押集合
  • fn select_validators() -> (BalanceOf, Option>)
    • 从组装的stakers及其角色首选项中选择一个新的validator集合
  • fn kill_stash(stash: &T::AccountId)
    • 从一个抵押系统中移除一个stash账号所有相关数据
  • pub fn reward_by_ids(validators_points: impl IntoIterator)
    • 使用validators的stash账号ID添加为奖励点
  • pub fn reward_by_indices(validators_points: impl IntoIterator)
    • 使用validator的索引作为奖励点
  • fn ensure_new_era()
    • 确保在现在session的结束后有一个新的era

结合Runtime

impl balances::Trait for Runtime {
    ...
    type OnFreeBalanceZero = ((staking,Contracts),Session); // 1. OnFreeBalanceZero跟staking什么关系?
    ...
}

impl authorship::Trait for Runtime {
    ...
    type EventHandler = (staking,ImOnline); // 2. 出块节点如何跟staking结合?
}

impl session::Trait for Runtime {
    type OnSessionEnding = Staking; // 3. session ending跟Staking如何关联呢?
    ...
    type ValidatorIdOf = staking::StashOf; // 4. ValidatorIdOf集合跟staking如何关联呢?
    type SelectInitialValidators = Staking; // 5. 选择初始Validator集合跟Staking如何关联呢?
}

impl session::historical::Trait for Runtime {
    type FullIdentification = staking::Exposure; // 6. session模块中用到的所有身份标记跟staking是如何关联?
    type FullIdentificationOf = staking::ExposureOf;
}

impl staking::Trait for Runtime {
    type Currency = Balances;
    type Time = Timestamp;
    type CurrencyToVote = CurrencyToVoteHandler;
    type RewardRemainder = Treasury; // 7. 国库如何跟RewardRemainder结合?
    type Event = Event;
    type Slash = Treasury; // 惩罚资金进入国库
    type Reward = (); // 奖励来自铸币
    type SessionsPerEra = SessionPerEra;
    type SessionInterface = Self;
    type RewardCurve = RewardCurve; // 8. NPoS的RewardCurve如何跟staking关联?
}

impl offences::Trait for Runtime {
    ...
    type OnOffenceHandler = Staking; // 9. Staking中的offences处理是怎么样的?
}

1. OnFreeBalanceZero跟staking什么关系?

在balances模块中,on_free_too_low(who: &T::AccountId)会调用OnFreeBalanceZero::on_free_balance_zero(who),背后的意思也非常简单,当一个账号自由余额太低,被系统收割的时候,其validator和nominator中抵押的币都会被清理掉。注意,validator和nominator抵押的币都是free balance,只是加上了资产锁。

2. 出块节点如何跟staking结合?

在staking模块中,增加奖励点给到区块生产者:

如果是非叔块的生产,就拿出20个奖励点去给到区块生产者进行奖励。

如果是之前未被引用过的叔块,对叔块的每个引用给2个奖励点给区块生产者进行奖励。

如果是被引用过叔块,对叔块的每个引用给一个奖励点给区块生产者进行奖励。

impl authorship::EventHandler for Module {
    fn note_author(author: T::AccountId) {
        Self::reward_by_ids(vec![(author, 20)]);
    }
    fn note_uncle(author: T::AccountId, _age: T::BlockNumber) {
        Self::reward_by_ids(vec![
            (>::author(), 2),
            (author, 1)
        ])
    }
}

在authorship模块中,note_author()note_uncle()方法:

pub trait EventHandler {
    /// Note that the given account ID is the author of the current block.
    fn note_author(author: Author);

    /// Note that the given account ID authored the given uncle, and how many
    /// blocks older than the current block it is (age >= 0, so siblings are allowed)
    fn note_uncle(author: Author, age: BlockNumber);
}

Module中:

fn on_initialize(now: T::BlockNumber){
    ...
    T::EventHandler::note_author(Self::author());
}

3. session ending跟Staking如何关联呢?

在staking模块中:

impl session::OnSessionEnding for Module {
    fn on_session_ending(_ending: SessionIndex, start_session: SessionIndex) -> Option> {
        Self::new_session(start_session - 1).map(|(new, _old)| new)
    }
}

impl OnSessionEnding>> for Module {
    fn on_session_ending(_ending: SessionIndex, start_session: SessionIndex)
        -> Option<(Vec, Vec<(T::AccountId, Exposure>)>)>
    {
        Self::new_session(start_session - 1)
    }
}

在session的Module里:

pub fn rotate_session(){
    ...
    // Get next validator set.
    let maybe_next_validators = T::OnSessionEnding::on_session_ending(session_index, applied_at);
}

根据staking中抵押来获取下一个session的validator集合,然后session模块中调用。

4. Validator集合跟staking如何关联呢?

impl SessionInterface<::AccountId> for T where
    ...
    T::ValidatorIdOf: Convert<::AccountId, Option<::AccountId>>
    
/// 一个`Convert`实现能够找到stash账号从这个给定的controller账号中 
pub struct StashOf(rstd::marker::PhantomData);

impl Convert> for StashOf {
    fn convert(controller: T::AccountId) -> Option {
        >::ledger(&controller).map(|l| l.stash)
    }
}

/// 一个从stash账号ID到现在那个账号的提名者的exposure的转换
pub struct ExposureOf(rstd::marker::PhantomData);

impl Convert>>>
    for ExposureOf
{
    fn convert(validator: T::AccountId) -> Option>> {
        Some(>::stakers(&validator))
    }
}

在session模块Module中

fn set_keys(origin,keys: T::Keys, proof: Vec) -> Result {
    ...
    let who = match T:: ValidatorIdOf::convert(who) {
        Some(val_id) => val_id,
            None => return Err("no associated validator ID for account."),
        };
    }
}

允许一个账号设置其session key去成为一个验证者。

5. 选择初始Validator集合跟Staking如何关联呢?

在staking模块中:

impl SelectInitialValidators for Module {
    fn select_initial_validators() -> Option> {
        >::select_validators().1
    }
}

在session模块的decl_storage中:

add_extra_genesis {
    config(keys): Vec<(T::ValidatorId, T::Keys)>;
    build(|config: &GenesisConfig| {
    ...
    let initial_validators = T::SelectInitialValidators::select_initial_validators()
            .unwrap_or_else(|| config.keys.iter().map(|(ref v, _)| v.clone()).collect());
    ...

在session模块中,初始化配置选择validator。

6. session模块中用到的所有身份标记跟staking是如何关联?

FullIdentification是出块认证者的身份标记,

T: session::historical::Trait<
    FullIdentification = Exposure<::AccountId, BalanceOf>,
    FullIdentificationOf = ExposureOf,
>,

pub Stakers get(fn stakers): map T::AccountId => Exposure>;

7. 国库如何跟RewardRemainder结合?

在staking模块中

/// Tokens have been minted and are unused for validator-reward.
type RewardRemainder: OnUnbalanced>;

在staking模块的Module中:

fn new_era(start_session_index: SessionIndex) -> Option> {
    ...
    T::RewardRemainder::on_unbalanced(T::Currency::issue(rest));
}

on_unbalanced()方法是support模块中的:

pub trait OnUnbalanced {
    /// Handler for some imbalance. Infallible.
    fn on_unbalanced(amount: Imbalance) {
        amount.try_drop().unwrap_or_else(Self::on_nonzero_unbalanced)
    }

    /// Actually handle a non-zero imbalance. You probably want to implement this rather than
    /// `on_unbalanced`.
    fn on_nonzero_unbalanced(amount: Imbalance);
}

当账号下余额减少了,需要处理这种不平衡,validator奖励是余额增加的原因,余额减少的原因是惩罚之类的。

OnUnbalanced for SplitTwoWays
{
    fn on_nonzero_unbalanced(amount: I) {
        let total: u32 = Part1::VALUE + Part2::VALUE;
        let amount1 = amount.peek().saturating_mul(Part1::VALUE.into()) / total.into();
        let (imb1, imb2) = amount.split(amount1);
        Target1::on_unbalanced(imb1);
        Target2::on_unbalanced(imb2);
    }
}

在runtime中可以找到:

pub type DealWithFees = SplitTwoWays<
    Balance,
    NegativeImbalance,
    _4, Treasury,   // 4 parts (80%) goes to the treasury.
    _1, Author,     // 1 part (20%) goes to the block author.
>;

手续费和惩罚80%进入国库,20%给到出块认证者。

8. NPoS的RewardCurve如何跟staking关联?

奖励曲线:

pallet_staking_reward_curve::build! {
    const REWARD_CURVE: PiecewiseLinear<'static> = curve!(
        min_inflation: 0_025_000,
        max_inflation: 0_100_000,
        ideal_stake: 0_500_000,
        falloff: 0_050_000,
        max_piece_count: 40,
        test_precision: 0_005_000,
    );
}

在staking模块中:

/// The NPoS reward curve to use.
type RewardCurve: Get<&'static PiecewiseLinear<'static>>;

new_era()方法中,用到了RewardCurve:

fn new_era(start_session_index: SessionIndex) -> Option> {
    ...
    let (total_payout, max_payout) = inflation::compute_total_payout(
        &T::RewardCurve::get(),
        total_rewarded_stake.clone(),
        T::Currency::total_issuance(),
        // Duration of era; more than u64::MAX is rewarded as u64::MAX.
        era_duration.saturated_into::(),
    );

简单理解下这个compute_total_payout()方法,在inflation.rs中是根据era持续周期和抵押率(提名者和验证者抵押除以总发行)来计算总的奖励。

其计算公式如下:

payout = yearly_inflation(npos_token_staked / total_tokens) * total_tokens / era_per_year

era_duration是毫秒单位。

9. Staking中的offences处理是怎么样的?

在staking模块中:

fn on_offence(
    offenders: &[OffenceDetails>],
    slash_fraction: &[Perbill],
) {}

在offences模块Module中:

fn report_offence(reporters: Vec, offence: O) {
    ...
    T::OnOffenceHandler::on_offence(&concurrent_offenders, &slash_perbill);
}

你可能感兴趣的:(Substrate的Staking模块分析)