- 区块链
- 开发框架
- 后端
【免费下载链接】substrate
Substrate: The platform for blockchain innovators
导读
本文以 Substrate 仓库中的 frame/staking/README.md 为骨架,结合 pallet-staking 源码 深入剖析 FRAME Staking 模块的设计与实现。Staking 模块是 Substrate 网络维护者(Validator/Authority)的产生机制:网络依据自愿锁定资金的多少选出维护者,正常履行职责者获得奖励,失职者则面临资金被削减(Slash)的风险。读完本文,你将掌握 Stash/Controller 账户体系、Validator/Nominator/Idle 三种角色流转、Era 与 Session 的关系、收益计算公式、Slash 惩罚流程以及 Phragmén 选举算法,并能在自己的 Runtime 中正确配置与调用该模块。
模块定位:如何选出网络维护者
在 Substrate 的 NPoS(Nominated Proof-of-Stake)体系中,Staking 模块是"由谁来维护网络"这一核心问题的答案。它基于**自愿锁定资金(under deposit)**的原则,从一群自我举荐的账户中选出一组网络维护者:正常运行期间这些资金会获得奖励,但一旦被认定未妥善履行职责,资金将面临Slash(罚没)。这一机制在代码中的直接体现是 frame/staking/Cargo.toml 所声明的pallet-stakingcrate,其仓库文档即 frame/staking/README.md。
关键术语
- Staking(质押):将资金锁定一段时间并承担 Slash 风险,以成为受奖励的网络维护者。
- Validating(验证):运行节点主动维护网络——生产区块或保证链的最终确定性。
- Nominating(提名):将质押资金投到一个或多个验证者身后,共享其获得的奖励与惩罚。
- Stash account(金库账户):持有所有者用于质押的资金。
- Controller account(控制器账户):控制所有者质押资金的账户(当前版本已弃用独立控制器,见下文)。
- Era(时代):由整数个 Session 组成,是重新计算验证者集合(及各验证者的活跃提名者集合)并支付奖励的周期。
- Slash(罚没):通过减少质押者资金实现的惩罚。
设计目标
- 支持由冷钱包(cold wallet)控制的资金参与质押。
- 在不中断实体角色的前提下,随时追加或提取部分资金。
- 在提名者、验证者、闲置三种角色之间以最小开销切换。
质押参与者模型:Stash 与 Controller
几乎所有与 Staking 模块的交互都始于bonding(绑定)。要成为 bonded 状态,需要将一个持有资金的Stash 账户(资金在质押期间被冻结)与一个活跃的Controller 账户(负责发出资金使用指令)配对。在 pallet/mod.rs 的bond调用中可以看到,ensure_signed取得 stash 后,模块在Bonded与Payee两个存储项中写入绑定关系,并创建StakingLedger:
// frame/staking/src/pallet/mod.rs(节选) pub fn bond( origin: OriginFor<T>, #[pallet::compact] value: BalanceOf<T>, payee: RewardDestination<T::AccountId>, ) -> DispatchResult { let stash = ensure_signed(origin)?; // Controller 账户已弃用,默认与 stash 相同 let controller_to_be_deprecated = stash.clone(); ... <Bonded<T>>::insert(&stash, &stash); <Payee<T>>::insert(&stash, payee); ... }注意源码中let controller_to_be_deprecated = stash.clone();这一行——它印证了 README 中的说明:Controller 账户正在被 Proxy 账户取代,已无法为 stash 单独设置一个不同的控制器地址。set_controller调用目前的作用只是把控制器指回 stash 自身。
StakingLedger(定义于 lib.rs)记录了每次质押的核心状态:
stash:真正锁定资金、暴露于风险的账户;total:当前核算的总额,等于active加上所有unlocking余额;active:将在后续轮次中继续处于风险中的金额;unlocking:正在解锁、最终可转出 stash 的金额队列(FIFO,新 era 的块追加在队尾),以UnlockChunk { value, era }表示;claimed_rewards:该 staker 已领取奖励的 era 列表(仅验证者维护)。
三种角色与状态流转
任何已质押的账户对都处于三种角色之一:Validator、Nominator或Idle,对应StakerStatus枚举。模块提供三个对应的指令来切换角色:
| 调用 | 目标角色 | 说明 |
|---|---|---|
validate | Validator | 声明希望成为验证者候选 |
nominate | Nominator | 声明提名一组验证者 |
chill | Idle | 暂时退出,不再参与选举与投票 |
Validating(验证)
验证者负责验证区块或保障最终性,因此必须避免恶意行为和离线。声明了验证意愿的 bonded 账户并不会立即当选:它们只是成为candidate(候选者),可能在下一次era 选举中被选为验证者,选举结果由提名者及其投票决定。从validate的源码(pallet/mod.rs)可以看到严格的资格检查:
ledger.active >= MinValidatorBond(否则返回InsufficientBond);prefs.commission >= MinCommission(否则返回CommissionTooLow);- 若
MaxValidatorsCount已设置且当前验证者数已达上限,则返回TooManyValidators。
Nomination(提名)
提名者不直接参与网络维护,而是投票给一组验证者参与选举。提名意愿在下一轮选举生效;提名者 stash 中的资金数额决定了其投票权重。验证者获得的奖励与惩罚都会按比例分摊给其提名者——这条规则从经济上激励提名者不要给作恶或离线的验证者投票,否则自己也会损失资金。nominate的源码约束包括:ledger.active >= MinNominatorBond、targets 非空且不超过NominationsQuota::get_quota(ledger.active)(即由质押余额决定的提名配额),并逐一检查目标必须是未设置blocked的验证者。
Chilling(退出)
任何角色都可以暂时"休息"(chill):提名者不再被视为投票者,验证者也不再是下一轮选举的候选。chill由 controller 签名调用,模块内部通过chill_stash清理对应存储。此外还有面向治理场景的chill_other:当系统中验证者/提名者数量接近ChillThreshold阈值、或某提名者因MaxNominations配置调低而变得不可解码时,任何人都可以调用它强制剔除不合格者。
Era 与 Session 管理
模块实现了SessionManagertrait,这是查询新验证者集合以及在该 era 结束后对验证者集合发放奖励的唯一 API。相关机制包括:
SessionsPerEra配置决定一个 era 包含多少个 session;- 每个 era 结束时,验证者集合与每个验证者的活跃提名者集合被重新计算并支付奖励;
- 新的验证者列表会写入 Session 模块的
Validators存储。
在 pallet/mod.rs 中可以看到CurrentEra(最新计划 era)、ActiveEra(当前正在发放奖励的 era,其验证者集合必须等于SessionInterface::validators())等存储项,以及Forcing枚举(NotForcing/ForceNew/ForceNone/ForceAlways)用于治理强制切换 era 的模式。SessionInterfacetrait 则封装了对pallet-session的交互:禁用验证者、读取验证者列表、裁剪历史 session 数据。
收益与惩罚:Rewards and Slash
收益与惩罚是 Staking 模块的核心,目标是"奖励良好行为,惩罚不当行为或不可用性"。
奖励领取:payout_stakers
每个 era 的奖励必须在超过HISTORY_DEPTH(由Config::HistoryDepth配置)之前领取。payout_stakers可由任何账户调用(源码中ensure_signed(origin)?后直接调用do_payout_stakers),领取后同时支付给验证者及其提名者。为限制逐个修改提名者账户存储带来的 I/O 开销,每个验证者只有MaxNominatorRewardedPerValidator个最大的质押者能够领取奖励——这正是ErasStakersClipped存储(clipped Exposure)存在的意义:total与own字段保持不变,但others只保留前 N 个最大的提名者。
惩罚机制:Slash
Slash 可以在任何时候发生,一旦不当行为被上报并经裁决,将从验证者及其所有提名者的stash 账户中扣除相应金额。完整的 Slash 逻辑位于 slashing.rs,其关键实现是StakingLedger::slash(lib.rs):
- 采用**比例削减(proportional slashing)**策略:若存在计划在
slash_era + BondingDuration及之后解锁的解锁块,则在活跃余额与这些解锁块之间按比例分摊削减;否则只削减活跃余额; - 削减以"偏好"形式实施:如果活跃余额(加上部分更应被削减的解锁块)不足以覆盖,则继续消耗活跃余额与解锁块,直至满足削减金额;
- 永远不会削减超过给定金额;当某块变成粉尘(dust)时,最后一块会被少削一点以补偿;
- 削减通过
Config::OnStakingUpdate::on_slash通知监听者。
模块还通过FilterHistoricalOffences过滤历史不当行为:只处理发生在当前 bonding period 内的上报,更早的上报会触发OldSlashingReportDiscarded事件并被丢弃(lib.rs)。治理层面,cancel_deferred_slash可在SlashDeferDuration窗口内取消待执行的削减,SlashRewardFraction配置上报者获得的举报赏金比例。
Era 收益计算与分配
Era 总奖励:通胀曲线
每个 era 的总收益由 inflation.rs 中的compute_total_payout计算。其依据是Config::RewardCurve(即EraPayouttrait,兼容旧的PiecewiseLinear曲线ConvertCurve)定义的年度通胀模型:
staker_payout = yearly_inflation(npos_token_staked / total_tokens) * total_tokens / era_per_year remaining_payout = max_yearly_inflation * total_tokens / era_per_year - staker_payout源码中的实际实现以毫秒计算:portion = era_duration / MILLISECONDS_PER_YEAR(儒略年 365.25 天),payout = portion * yearly_inflation.calculate_for_fraction_times_denominator(...),maximum = portion * (yearly_inflation.maximum * total_tokens)。剩余部分remaining_payout会发送到可配置端点Config::RewardRemainder(通常流向国库)。inflation.rs 的单元测试展示了一条典型曲线的参数:min_inflation: 2.5%、max_inflation: 10%、ideal_stake: 50%、falloff: 5%,即质押率达到 50% 时通胀最高、偏离理想质押率时通胀递减——该模型旨在激励网络趋向目标质押率。
奖励点数:reward_by_ids 与 EventHandler
era 总奖励在验证者及其提名者之间按奖励点数(reward points)分配。点数通过reward_by_ids/reward_by_indices累加(其中reward_by_ids的定义见 lib.rs 的Pallet::reward_by_ids)。同时模块实现了pallet_authorship::EventHandler:每当区块生产者或被引用的叔块生产者出现时自动为其增加点数。每个 era 的点数记录在ErasRewardPoints存储中,分配公式以individual / total为比例。
验证者佣金与按质押比例分配
验证者可以在ValidatorPrefs中声明一个commission(佣金)比例(Perbill类型,另含blocked字段表示是否接受新提名)。每次支付时,佣金先从验证者及其提名者的总奖励中扣除,剩余部分按各自质押比例(Exposure中的own/others除以total)在验证者与其提名者之间分摊。注意:比例分摊基于验证者背后的全部exposure,而不仅是验证者与 top N 提名者的 exposure。
奖励去向:Payee
所有获奖实体都可以通过set_payee选择奖励目的地(RewardDestination枚举,定义于 lib.rs):
Staked:存入 stash 账户,同时增加质押金额(复利);Stash:存入 stash 账户,不增加质押金额;Controller:存入 controller 账户(显然不增加质押金额);Account(AccountId):存入任意指定账户;None:不接收奖励。
默认值为Staked。
资金管理操作:增加、解绑与再绑定
已进入 stash 的资金支持以下操作:
| 调用 | 签名要求 | 作用 |
|---|---|---|
bond_extra | stash | 将 stash 自由余额中超出ledger.total的部分追加质押(无金额上限) |
unbond | controller | 将部分(或全部)活跃资金调度为解锁;并非立即可用,需等待BondingDuration(以 era 数计) |
withdraw_unbonded | controller | 解锁期结束后真正提取资金 |
rebond | controller | 将已调度解锁的资金重新绑定回活跃质押 |
reap_stash | 任何人 | 当 stash 总余额或 ledger 总额低于 Existential Deposit 时清理其全部质押数据结构 |
unbond的源码实现值得注意:当解锁块数量已达到MaxUnlockingChunks上限时,模块会先自动调用do_withdraw_unbonded清理已到期的块,再尝试调度新的解锁;若仍无空位则返回NoMoreChunks。为避免质押系统残留粉尘,unbond在活跃余额低于minimum_balance时会把它一并加入解锁金额并清零。此外,若调用后活跃余额低于MinNominatorBond/MinValidatorBond(且账户仍处于相应角色),会返回InsufficientBond提示先chill。
选举算法:Phragmén
当前选举算法基于Phragmén 方法实现(参考实现见 w3f 的 NPoS 项目,NPoS 即 Nominated Proof-of-Stake 的标准选举方案)。算法不仅选出拥有最多质押价值与投票的验证者,还试图将提名者的票在候选人之间均衡分配。为进一步保证均衡性,可以应用可选的后处理步骤:迭代归一化提名者的质押值,直到某个提名者各票之间的总差异小于阈值。在代码层面,选举由Config::ElectionProvider/GenesisElectionProvider抽象,staking 自身实现ElectionDataProvider提供候选人与投票数据;VoterList(如pallet-bags-list)维护按权重排序的投票者列表,TargetList维护可被提名的目标及其被认可质押。选举结果的胜者数量由选举提供者的MaxWinners决定,并与ValidatorCount保持一致。
Config trait 与关键关联类型
在 Runtime 中接入该模块需要实现 pallet/mod.rs 中的Configtrait,核心关联类型包括:
| 关联类型/常量 | 作用 |
|---|---|
Currency/CurrencyBalance | 质押所依托的货币及余额类型(须实现LockableCurrency) |
UnixTime | 计算 era 时长(毫秒) |
ElectionProvider/GenesisElectionProvider | 常规与创世时的选举提供者 |
NominationsQuota | 依据质押余额决定每个提名者可提名的人数上限 |
HistoryDepth | 历史保留的 era 数(claimed_rewards等存储的上界) |
RewardRemainder | 每 era 剩余奖励的去向端点 |
Slash/Reward | 削减与奖励时的OnUnbalanced处理器(通常调整总发行量) |
SessionsPerEra | 每个 era 包含的 session 数 |
BondingDuration | 解绑后资金需锁定的 era 数 |
SlashDeferDuration | 削减延迟生效的 era 数(须小于BondingDuration) |
AdminOrigin | 可取消延迟削减、设置最低佣金的管理来源 |
MaxNominatorRewardedPerValidator | 每个验证者可领取奖励的最大提名者数量 |
MaxUnlockingChunks | StakingLedger.unlocking的最大块数 |
VoterList/TargetList | NPoS 选举的投票者/目标排序列表提供者 |
EventListeners | 接收 staking 更新事件(当前仅上报 slashing) |
integrity_test(pallet/mod.rs)会在运行时构建时校验这些配置的一致性,例如SlashDeferDuration < BondingDuration、两个 ElectionProvider 的MaxWinners一致、MaxNominations与MaxVotesPerVoter相等且大于 1。
GenesisConfig:创世质押者
GenesisConfig是可选的,允许在链创世时预设一批初始质押者。其字段(源码见 pallet/mod.rs)为:
validator_count:理想的活跃验证者数量;minimum_validator_count:触发紧急条件前的最小参与人数;invulnerables:不可被 Slash 或强制移除的验证者(仅用于测试网,预期不超过 4 个);force_era:初始 era 强制模式;slash_reward_fraction:举报者获得的削减比例;canceled_payout:因特殊原因取消削减后给予举报者的补偿金额;stakers:(stash, controller, balance, StakerStatus)列表,创世时依次调用bond、validate/nominate建立初始质押者;min_nominator_bond/min_validator_bond:两类角色的最低活跃质押;max_validator_count/max_nominator_count:两类角色的人数上限(None表示不限制)。
genesis_build会断言每个 stash 的可用余额足够质押,且创世结束后VoterList::count() == Nominators::count() + Validators::count(),确保所有创世 staker 都被正确纳入排序列表。
代码实战:在自定义 Pallet 中奖励验证者
以下示例展示了如何在自己的 pallet 中调用 staking 模块的reward_by_ids(来源于 README.md 的 Usage 章节,与 lib.rs 中的实现一一对应):
use pallet_staking::{self as staking}; #[frame_support::pallet(dev_mode)] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; #[pallet::pallet] pub struct Pallet<T>(_); #[pallet::config] pub trait Config: frame_system::Config + staking::Config {} #[pallet::call] impl<T: Config> Pallet<T> { /// Reward a validator. #[pallet::weight(0)] pub fn reward_myself(origin: OriginFor<T>) -> DispatchResult { let reported = ensure_signed(origin)?; <staking::Pallet<T>>::reward_by_ids(vec![(reported, 10)]); Ok(()) } } }要点:
- 自定义 pallet 的
Config必须同时继承frame_system::Config与staking::Config,才能通过<staking::Pallet<T>>访问其公开函数; reward_by_ids接收Vec<(AccountId, u32)>形式的(验证者, 点数)列表,直接把点数累加到该 era 的ErasRewardPoints;- 该函数由模块内部(如
pallet-authorship的事件回调)与外部 pallet 共同使用,是奖励点数注入的唯一入口。
相关模块与生态
- Balances(
pallet-balances):管理质押所依托的余额,提供LockableCurrency能力(见 frame/balances)。 - Session(
pallet-session):管理 session,并在每个 era 结束时保存新的验证者列表(见 frame/session)。 - Authorship(
pallet-authorship):向 Staking 模块回调奖励点数。 - Bags-list(
pallet-bags-list):作为VoterList提供者,按质押权重维护排序的投票者列表。 - Election-provider-support:选举实现的支撑框架(见 frame/election-provider-support)。
- Staking Reward Curve(frame/staking/reward-curve):以过程宏
build!定义年度通胀的PiecewiseLinear曲线。
测试与验证
模块的单元测试位于 frame/staking/src/tests.rs,覆盖了上述全部核心路径,例如:
bond_extra_works:验证追加质押对StakingLedger的正确更新;payout_stakers_handles_basic_errors:验证 payout 调用在各种错误输入下返回一致消耗的权重;payout_stakers_handles_weight_refund:验证按被奖励提名者数量精确计算并退还权重;- inflation.rs 中的
npos_curve_is_sensible测试则对通胀曲线的年化/日化/小时化输出做了数值断言。
这些测试用例是理解模块行为边界的绝佳起点,也是自定义 Runtime 集成时最可靠的参考依据。
总结
Substrate 的 Staking 模块通过"质押换角色、era 驱动选举、点数分配收益、比例削减惩罚"这一套闭环设计,将经济激励与网络安全深度绑定。无论是接入 NPoS 共识的 Runtime 开发者,还是研究链上治理与经济模型的读者,理解 frame/staking/src/pallet/mod.rs 中的Config约束、lib.rs 中的账本与削减实现、inflation.rs 中的收益曲线,都是掌握 Substrate 质押体系的关键一步。本文所述参数均以当前仓库源码为准,实际接入时请以你自己的 Runtime 配置(如SessionsPerEra、BondingDuration、MaxNominatorRewardedPerValidator)作为最终依据。
- 区块链
- 开发框架
- 后端
【免费下载链接】substrate
Substrate: The platform for blockchain innovators
相关推荐
Substrate Root Offences Pallet 源码解析:用根权限直接触发质押惩罚(Slash)的测试利器
Substrate Root Offences Pallet 源码解析:用根权限直接触发质押惩罚(Slash)的测试利器 导读 frame/root offen
区块链开发框架后端Substrate 中 GRANDPA 终结论 Runtime 模块(pallet-grandpa)深度解析
Substrate 中 GRANDPA 终结论 Runtime 模块(pallet grandpa)深度解析 本文基于仓库 frame/grandpa/READ
区块链开发框架后端Initia权益质押计划:代币经济与激励机制深度分析
Initia权益质押计划:代币经济与激励机制深度分析 引言:构建跨链生态的质押新范式 在区块链技术快速演进的今天,权益证明(Proof of Stake)机制已
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考