本文作者为 Patract Labs 开发者张泰林。

目录:

生成账户(Accounts)质押资金(fn bond)设置验证人(fn validate)提名验证人(fn nominate)冻结验证人或提名人(fn chill)Phragmén选举算法奖励结算通货膨胀模型奖励分配(fn do_payout_stakers)奖励认领

Staking 是什么

Staking 是管理网络维护者的抵押资金的模块。网络维护者也称为Authorities (出块人)或者Validators (验证人),它们是基于抵押资金被选中,它们在正常履行职责的情况下会获得奖励,如果行为不当则会受到惩罚(没收一定的资金)。

验证人、提名人

Staking Module 有两个重要的角色:验证人和提名人

验证人(Validator):运行节点以主动维护网络,并履行生成块或保证链的最终性的职责。提名人(Nominator):将抵押资金分配给一个或多个验证人的过程,以便他们分享任何报酬和惩罚。

如何成为验证人/提名人

生成账户(Accounts)

用户可以通过Polkadot钱包或 https://polkadot.js.org/apps/#/accounts 生成自己的账户,注意保管好私钥和助记词。

为了保障用户资金安全,Staking Module设计了两层结构的独立密钥类型,采用两个不同的账户来管理资金, 我们称为:存储账户(Stash Account)和控制账户(Controller Account)(就好比房东和中介的关系)

Stash :存储账户主要用来存放用于质押的资金,存储账户可以指定一个控制账户,将申请提名人、验证人等功能委托给控制账户,存储账户的密钥可以长期保存在冷钱包中,以此保证用户资金的安全性。Controller:控制帐户是存储账户的代理,有申请提名人和验证人、设置收款账户和佣金的权利。如果是验证人,它还可以设置 session keys  会话密钥。只需要保证控制账户有足够的资金来支付交易手续费。

质押资金(fn bond)

用户需要质押一定的资金(质押金额不得小于限定的最小金额)来获取成为验证人或提名人的资格,质押行为由存储账户发起,质押过程可以设置控制账户、质押金额、收款账户。

控制账户推荐是一个与存储账户不同的账户,以此保证存储账户的安全,当然也可以设置成存储账户。一个存储账户只能由一个控制账户代理,一个控制账户只能代理一个存储账户。质押完成后质押的资金将被锁定。

收款账户是用来接收staking奖励的,有四个可选设置:

Staked:奖励支付给存储账户并用来质押Stash:奖励支付给存储账户,但奖励不用来质押Contriller:奖励支付给控制账户Account:奖励支付给一个指定账户

Staking Module 提供了以下几种质押相关的功能:

bond:质押,由存储账户调用,质押资金bond_extra:额外质押,已经调用过bond的存储账户,再次质押存储账户的free_balance资金unbond:解除质押,由控制账户调用(EraElectionStatusis Closed),资金解除质押后处于解冻中状态withdraw_unbonded:取回解冻资金,由控制账户调用(EraElectionStatusis Closed),由于资金在质押后有锁定时长限定,在波卡上锁定 28 天,Kusama 上是锁定 7 天,unbonded的资金也需要过了锁定期(从unbond开始计算)后才能被取出, 如果验证人被发现行为不端,将受到惩罚rebond:再次质押,由控制账户调用(EraElectionStatusis Closed),将处于解冻中的资金再次质押

设置验证人(fn validate)

成为验证人过程比较繁琐,这里就简述下步骤,详见:How to run a Validator on Polkadot

运行Polkadot验证人节点,并同步到链的最新状态质押DOT设置Session keys(Submitting the setKeys Transaction)设置验证人,包括设置验证人的佣金,佣金是按比例收取的,当分配staking奖励时,会优先支付验证人的佣金,剩余奖励才会分配给提名人。

注意:同一个stash account 只能成为验证人或提名人,验证人可以通过自抵押的方式提名自己,但不可以通过提名的方式。已经是提名人的stash account 不可以作为验证人。

提名验证人(fn nominate)

控制账户可以提交一份支持的信誉良好的候选验证人名单(提名最多只能有16个,即MAX_NOMINATIONS )成为提名人。在下一个Era,具有最多 DOT 支持的一定数量的验证人被选中,如果提名人支持的验证人被选中,就可以分享验证人出块奖励或惩罚。提名过程只能发生在非候选验证人选举阶段。

一旦提名阶段结束,NPoS选举机制以提名人及其投票为输入,输出一组数量有限的验证人,使任何验证人的支持度最大化,并尽可能均匀分布。这种选举机制的目标是最大限度地提高网络的安全性,实现提名人的公平代表。

冻结验证人或提名人(fn chill)

冻结是从活跃验证人节点池中移除验证人的行为,同时在下一个 Era 中取消它们在可选择候选人 list 中的资格。

冻结可以是自愿性的,并且是验证人可以决定的,例如,如果验证人的环境或服务器提供商有计划的中断,并且验证人希望退出以保护自己不被 slash。当发生自愿行为时,那么冻结将使验证人在当前会话中保持活跃状态,但会在下一个会话中将它们移动到非活跃集。验证人不会失去他们的提名人。

当被用作惩罚的一部分时,被冻结意味着未被提名。它还会在当前剩余的 era 中丧失验证人的能力,并在下一次选举中移除违规的验证人。

Polkadot 允许禁用一些验证人,但是如果禁用的验证人的数量太大,Polkadot 将触发一个新的验证人选举以获取完整的验证人节点池。禁用的验证人需要重新提交他们的验证意图和重新获得提名人的支持。

NPoS机制

NPoS(Nominated Proof of Stake,提名权益证明)是Polkadot基于PoS算法设计的共识算法,验证人( Validator)运行节点参与生产和确认区块,提名人(Nominator)可以抵押自己的dot获得提名权,并提名自己信任的验证人,获得奖励。

Phragmén选举算法

验证人选举算法是NPoS机制的核心,选举过程要具有公平代表性和安全性,Polkadot为此设计了Phragmén算法,确保每次选举都具有这种性质。

公平代表性:任何持有总股份至少 1/n 的提名人都保证至少有一个他们信任的验证人当选安全性:我们希望尽可能让对候选验证人很难获得一个验证人,他们只有得到足够高的支持才能做到这一点。因此,我们将选举结果的安全级别等同于被选验证人的最小支持数量。

下图一不具备公平代表性,图二具备公平代表性,并且图二右的安全级别更高。

Polkadot 每进入new_session,都有可能触发进入new_era,进入新的Era后,会通过Phragmen算法重新选出一组验证人。Phragmen算法的计算可以是off-chain的,也可以是on-chain,会优先从QueuedElected中读取选举结果,QueuedElected的结果是由offchain-workers计算提交的, 如果没有,就会执行on-chain计算选举。

小知识:Kusama 的运行速度是波卡的 4 倍,除了区块时间都是 6 秒。

周期PolkadotKusamaSlot6秒6秒Epoch4小时1小时Session4小时1小时Era24小时6小时

验证人数量的上限尚未确定,但仅受限于由频繁和大量的点对点消息传递而造成的网络带宽紧张。Polkadot 在网络成熟的时候将拥有约为 1000 个验证人。而波卡的金丝雀网络 Kusama 目前有700个验证人插槽,Polkadot主网目前有242个验证人插槽(截止2020/11/27)。

Staking 奖励

奖励结算

每个区块生成后,authorship->on_initialize会记录(note_author->reward_by_ids)区块生产者的ErasRewardPoints, 并在每个end_era 进行结算。Kusama上每天计算四次奖励,在波卡上每天计算一次奖励。

Reward Points增加规则:

主链区块生产者增加20点reward叔区块生产者增加2点reward引用叔区块的生产者增加1点reard

奖励结算规则(fn compute_total_payout):

根据年膨胀率计算当前era获得的实际奖励

// era奖励 = 年膨胀率 * 通证发行总量 / 每年era个数staker_payout = yearly_inflation(npos_token_staked / total_tokens) * total_tokens / era_per_year

根据最大年膨胀率计算当前era获得的最大奖励

max_payout = max_yearly_inflation * total_tokens / era_per_year

如果最大奖励减去实际奖励还有剩余奖励,将剩余奖励收归国库,用于支持生态发展支出

type RewardRemainder = Treasury;let rest = max_payout.saturating_sub(staker_payout);T::RewardRemainder::on_unbalanced(T::Currency::issue(rest));

通货膨胀模型

Staking 的奖励主要来源于DOT代币增发,这也是DOT主要的通胀来源。Polkadot希望有50%的代币被抵押到NPoS共识系统,30%的代币用于平行链插槽拍卖,20%代币在交易市场上流通。而在通胀率上,Polkadot希望是每年10%,在50%的抵押率中,抵押代币的平均年化收益为20%。

上图中,横坐标为抵押率,蓝色线纵坐标为年通胀率,绿色线纵坐标为年化收益率。

从图中可以看出:

当抵押率小于50%,年化收益率大于20%,这会鼓励用户抵押DOT;当抵押率等于50%,年化收益率等于20%,符合设计预期;当抵押率大于50%,年化收益率小于20%,这会鼓励用户赎回DOT。

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, );}

奖励分配(fn do_payout_stakers)

两个验证人在相同的工作中获得相同数量的 DOT,即它们的报酬与每个验证人的 stake 质押数量不成比例奖励的一部分(commission 按奖励百分比设置)优先用于支付验证人的佣金,其余部分按比例(即与 stake 成比例)支付给提名人和验证人验证人将获得两次奖励:一次作为验证的佣金,一次作为用自抵押提名自己的佣金

fn do_payout_stakers(    validator_stash: T::AccountId,    era: EraIndex,) -> DispatchResult {    // Validate input data    ......    /* Input data seems good, no errors allowed after this point */    // 1.获取当前era所有的ErasRewardPoints    let era_reward_points = >::get(&era);    let total_reward_points = era_reward_points.total;        // 2.获取该验证人获得的所有Reward Points    let validator_reward_points = era_reward_points.individual.get(&ledger.stash)        .map(|points| *points)        .unwrap_or_else(|| Zero::zero());    // Nothing to do if they have no reward points.    if validator_reward_points.is_zero() { return Ok(())}    // 3.计算该验证人奖励点所占比例,根据比例得到该验证人所得奖励    // This is the fraction of the total reward that the validator and the    // nominators will get.    let validator_total_reward_part = Perbill::from_rational_approximation(        validator_reward_points,        total_reward_points,    );    // This is how much validator + nominators are entitled to.    let validator_total_payout = validator_total_reward_part * era_payout;    // 4.优先支付验证人的佣金    let validator_prefs = Self::eras_validator_prefs(&era, &validator_stash);    // Validator first gets a cut off the top.    let validator_commission = validator_prefs.commission;    let validator_commission_payout = validator_commission * validator_total_payout;    let validator_leftover_payout = validator_total_payout - validator_commission_payout;        // 5.支付验证人质押获得的奖励    // Now let"s calculate how this is split to the validator.    let validator_exposure_part = Perbill::from_rational_approximation(        exposure.own,        exposure.total,    );    let validator_staking_payout = validator_exposure_part * validator_leftover_payout;    // We can now make total validator payout:    if let Some(imbalance) = Self::make_payout(        &ledger.stash,        validator_staking_payout + validator_commission_payout    ) {        Self::deposit_event(RawEvent::Reward(ledger.stash, imbalance.peek()));    }        // 6.支付提名人质押获得的奖励    // Lets now calculate how this is split to the nominators.    // Reward only the clipped exposures. Note this is not necessarily sorted.    for nominator in exposure.others.iter() {        let nominator_exposure_part = Perbill::from_rational_approximation(            nominator.value,            exposure.total,        );        let nominator_reward: BalanceOf = nominator_exposure_part * validator_leftover_payout;        // We can now make nominator payout:        if let Some(imbalance) = Self::make_payout(&nominator.who, nominator_reward) {            Self::deposit_event(RawEvent::Reward(nominator.who.clone(), imbalance.peek()));        }    }    Ok(())}

奖励认领

每个人都可以选择触发无人认领的 指定 era 中某个验证人的 staking 奖励(payout_stakers),认领后奖励将支付给每个在那个 era 提名的提名人及验证人。由于history_depth默认设置为84,即staking 奖励信息只在最新的84个Era 内有效,在波卡上约为 84 天,Kusama 上约 21 天。为了获得你的 staking 奖励,必须有人为你提名的每个验证人进行领取。

“问题:为什么需要认领,以及为什么不直接认领一个era的奖励?答案参考:https://wiki.polkadot.network/docs/en/learn-simple-payouts#docsNav总结得到以下几点:通过提交交易领取奖励,将staker的账户更新分散到多个区块中,这比所有奖励集中在一个区块中,更能保证网络的稳定性。如果有攻击者使用成百上千个提名一个验证人,账户存储更新的费用,不应该由Polkdot来承担奖励只支付给质押最高的256位提名人,降低staking集合的复杂性(文档中说,但代码没看到)

惩罚(Slash)

如果验证人在网络中行为不当(例如脱机、攻击网络或运行修改过的软件),则会发生 slash 惩罚。他们和他们的提名人会因为 slash 惩罚而失去一部分 DOT。总质押数较大的验证池将受到更严厉的 slash 惩罚。在start_era时会对slash进行清算处理。

在pallet-offences模块中定义了三种违规行为:

UnresponsivenessOffence (无响应)GrandpaEquivocationOffence (重复签名,voting)BabeEquivocationOffence (重复出块)

如果验证人因任何一项违规行为被举报,他们将被从验证人节点池中移除(也就是冻结),并且在他们移除的时候不会得到奖励。他们将立即被视为不活跃的验证人,并将失去提名人。他们需要重新发布验证意图和收集提名人的支持。

跨Era的Slash惩罚在 NPoS 中计算有三大难点:

一个提名人可以提名多个验证人,并通过其中任何一个被 slash 。在被 slash 之前,这些 stake 质押会被一个 era 一个 era 的重复使用。连续为 E 个 era 提名 N 个代币并不意味着你有 N*E 个代币用于 slash,你还是只有 N 个代币。可惩罚的违规行为还可以在事情发生后才被发现,且不是按照顺序的。

为了平衡以上的几点,我们只对参与者在某个时间段内可以收到的最大惩罚进行惩罚,而不是所有惩罚的加总。这样可以防止过度 slash。同样地,计算最大 slash 的时间跨度是有限的,验证人在 slash 事件后被冻结并撤回提名,如前一节所述。这可以防止怒退(rage-quit)攻击,在这种攻击中,一旦被发现行为不端,参与者故意行为不端的程度会更高,因为反正他们的 slash 数额已经达到最大值。

References

https://wiki.polkadot.network/docs/en/learn-stakinghttps://research.web3.foundation/en/latest/polkadot/Token%20Economics.htmlhttps://wiki.polkadot.network/docs/en/learn-phragmenhttps://wiki.polkadot.network/docs/en/maintain-guides-how-to-validate-polkadot波卡的 NPoS 机制是如何运作的?