- 区块链
- 开发框架
- 后端
【免费下载链接】substrate
Substrate: The platform for blockchain innovators
导读
pallet-scored-pool(Scored Pool)是 Substrate FRAME 生态中一个极具特色的成员管理模块:它维护一个"评分会员池(scored membership pool)",池中每个实体(AccountId)都可以被赋予一个量化Score,系统每隔固定Period个区块,自动从池中挑选得分最高的前MemberCount名实体组成正式成员集合Members,并在每次刷新时通过MembershipChanged/MembershipInitialized两个 trait 将成员变更信号广播给其他模块(如集体投票、联盟治理等)。读完本文,你将掌握该 pallet 的核心设计、五个公开函数的调用链与参数语义、Config配置项逐项含义,以及如何在 runtime 中集成它并借助仓库测试用例验证行为。
本文基于仓库 frame/scored-pool/README.md 展开,并结合源码 frame/scored-pool/src/lib.rs、测试 frame/scored-pool/src/tests.rs 与 mock 配置 frame/scored-pool/src/mock.rs 进行深度印证。
一、模块定位与核心机制
1.1 什么是"评分成员池"
从 lib.rs 的 pallet 文档注释 可以提炼出该模块的核心数据模型:
- Pool(候选池):一个按得分降序排列的有界向量(
BoundedVec),元素为(AccountId, Option<Score>)。实体提交候选资格后进入 Pool,得分尚未被赋予时Score为None。 - Members(成员集合):从 Pool 中取
MemberCount个得分最高的实体构成,是一个有序的BoundedVec<AccountId>。 - 无分不入:
Score为None的实体永远不会进入Members。这是一个强约束,测试unscored_entities_must_not_be_used_for_filling_members(tests.rs)专门验证了即使把所有已评分成员全部踢出,None候选人也不会被填充进成员集合。
1.2 成员集合的刷新周期
模块实现了 on_initialize hook:
fn on_initialize(n: BlockNumberFor<T>) -> Weight { if n % T::Period::get() == Zero::zero() { let pool = <Pool<T, I>>::get(); <Pallet<T, I>>::refresh_members(pool, ChangeReceiver::MembershipChanged); } Weight::zero() }即每隔Period个区块,在区块初始化阶段从当前 Pool 重新挑选得分最高的MemberCount名成员写入Members,并调用T::MembershipChanged::set_members_sorted(new, old)。测试refreshing_happens_every_period(tests.rs)演示了在区块 1 提交候选人并打分后,Members仍为[20, 40],直到区块 4(Period = 4)触发on_initialize后才变为[15, 40]。
二、Config 配置项逐项解析
Configtrait 在 lib.rs 中定义,完整继承frame_system::Config。以下是全部关联类型的语义与建议取值:
| 关联类型 | 约束 | 说明 | mock 中的示例值 |
|---|---|---|---|
Currency | Currency<AccountId> + ReservableCurrency<AccountId> | 用于押金(deposit)的货币,必须是可保留(reserve)余额的货币类型,通常直接用pallet-balances | Balances |
MaximumMembers | Get<u32> | Pool 与 Members 两个有界向量的容量上限(#[pallet::constant]) | ConstU32<10> |
Score | AtLeast32Bit + Clone + Copy + Default + FullCodec + ... + MaxEncodedLen | 赋予成员的量化得分类型,要求全序可比较 | u64 |
RuntimeEvent | From<Event<Self,I>> + IsType<...> | 统一事件类型 | RuntimeEvent |
CandidateDeposit | Get<BalanceOf<Self,I>> | 提交候选资格时被 reserve 的押金,退出或被踢时返还(#[pallet::constant]) | 25 |
Period | Get<BlockNumberFor<Self>> | 成员集合刷新的区块周期(#[pallet::constant]) | ConstU64<4> |
MembershipInitialized | InitializeMembers<AccountId> | 创世(pre-genesis)时的成员初始化信号接收者,通常与MembershipChanged相同 | TestChangeMembers |
MembershipChanged | ChangeMembers<AccountId> | 每次成员变更时收到set_members_sorted信号的接收者 | TestChangeMembers |
ScoreOrigin | EnsureOrigin<RuntimeOrigin> | 允许给候选人打分的来源(Origin) | EnsureSignedBy<ScoreOrigin, u64>(账户 3) |
KickOrigin | EnsureOrigin<RuntimeOrigin> | 允许移除实体的来源,可配置为 Root | EnsureSignedBy<KickOrigin, u64>(账户 2) |
值得注意:Period、MaximumMembers、CandidateDeposit都通过#[pallet::constant]暴露为常量,可供 runtime 在其他地方引用;Score的AtLeast32Bit约束保证了二进制搜索排序所需的全序关系。
三、公开函数(Call)详解
模块共 5 个可调用函数,定义在 pallet::call。它们共同遵守一条索引约定:调用者必须传入自己在Pool中的index(位置索引),pallet 通过ensure_index(lib.rs)校验index越界(报InvalidIndex)与index处实体是否就是操作对象(报WrongAccountIndex)。
3.1submit_candidacy(origin)—— 提交候选资格
- 权限:签名(
ensure_signed)。 - 行为:先检查
CandidateExists防止重复提交(AlreadyInPool);从origin账户reserve掉CandidateDeposit;将(who, None)追加到 Pool 末尾(因为None得分永远排在最后);写入CandidateExists;发出CandidateAdded事件。 - 失败场景:账户余额不足以支付押金(如测试中账户 99 余额为 1,押金 25,报
InsufficientBalance);池已满(TooManyMembers)。 - 源码位置:lib.rs;测试 submit_candidacy_works 与 submit_candidacy_must_not_work。
3.2withdraw_candidacy(origin, index)—— 主动退出
- 权限:签名,且
index必须指向自己(ensure_index)。 - 行为:调用
remove_member从 Pool 移除自己、解除CandidateExists标记、unreserve返还押金;若自己当前在Members中,则立即触发一次成员刷新,由池中下一个最高分候选人替补(refresh_members,见 lib.rs);发出CandidateWithdrew事件。 - 测试验证:
withdraw_scored_candidacy_must_work(tests.rs)证明成员 40 退出后Members变为[20, 31],押金归零;withdraw_unscored_candidacy_must_work(tests.rs)证明无分候选人也可退出。
3.3kick(origin, dest, index)—— 强制移除
- 权限:
T::KickOrigin(可为 Root 或特定治理账户)。 - 行为:通过
T::Lookup::lookup(dest)解析目标账户,校验索引后执行与退出相同的remove_member流程;发出CandidateKicked事件。 - 测试验证:
kicking_works(tests.rs)证明被踢者押金返还且成员由[20, 31]接替;kicking_works_only_for_authorized(tests.rs)证明未授权来源被拒绝(BadOrigin)。
3.4score(origin, dest, index, score)—— 打分与重排
- 权限:
T::ScoreOrigin。 - 行为:从 Pool 中按
index移除旧条目,然后利用 Pool 按得分降序排列的性质做二分查找插入(binary_search_by_key,键为Reverse(score),None视为Default值),将(dest, Some(score))插入到保持排序的正确位置;写回 Pool;发出CandidateScored事件。 - 排序细节:由于得分相同的元素插在已有同分元素之前,
scoring_same_element_with_same_score_works(tests.rs)验证了同分(31 与 20 同为 2 分)时新条目排在原条目之前且顺序不破坏。 - 源码位置:lib.rs。
3.5change_member_count(origin, count)—— 调整成员数量
- 权限:
ensure_root(仅 Root)。 - 行为:调用
update_member_count,校验新值不超过MaximumMembers(否则TooManyMembers)后写入MemberCount存储。 - 生效时机:文档与源码均明确——该修改仅在下一次周期刷新时生效(lib.rs),不会立即改变
Members。
四、成员刷新与治理信号集成机制
4.1refresh_members内部逻辑
refresh_members(pool, notify)(lib.rs)是模块的核心内部函数:
- 读取当前
MemberCount与旧Members; - 从 Pool 中过滤掉
Score == None的实体,取前count个(Pool 本身按得分降序,因此天然是最高分); - 将结果转为
BoundedVec并排序(sort(),按 AccountId 字典序); - 写入
Members存储; - 依据
notify枚举(ChangeReceiver,见 lib.rs)决定调用哪个信号:- 创世首次加载:
T::MembershipInitialized::initialize_members(&new_members) - 周期刷新或成员变化:
T::MembershipChanged::set_members_sorted(&new_members, &old_members)
- 创世首次加载:
注意第 2 步也解释了"无分不入":过滤条件score.is_some()把None实体永远挡在Members之外。
4.2 两个信号 trait 的来源与典型实现
InitializeMembers与ChangeMembers定义在 frame/support/src/traits/members.rs:
InitializeMembers只有一个方法initialize_members(members: &[AccountId]),在创世时被调用,()(空实现)可直接作为占位。ChangeMembers的默认实现set_members_sorted会通过compute_members_diff_sorted计算 incoming/outgoing 差异,再调用必须实现的change_members_sorted。()同样有ChangeMembers空实现(members.rs)。
典型下游接收者:
- pallet-collective:
impl ChangeMembers<T::AccountId> for Pallet(frame/collective/src/lib.rs)在成员变更时清理旧成员在所有提案中的投票并重置 prime;impl InitializeMembers(frame/collective/src/lib.rs)在创世写入初始成员。这正是"得分池驱动集体治理"的经典组合。 - pallet-membership:其
Config同样要求MembershipInitialized/MembershipChanged两个关联类型(frame/membership/src/lib.rs),并在 add/remove/swap 时调用change_members_sorted(如 lib.rs、lib.rs)。 - 测试 mock 中的
TestChangeMembers(mock.rs)则通过断言old + incoming == new + outgoing校验变更的一致性,是理解该 trait 契约的最佳范例。
由此可以推断:Scored Pool 的典型落地场景是作为"自动按分排名的成员供给源"——它将成员资格的产生与淘汰完全交给打分机制与周期刷新,而非人工增删,再把结果以标准信号同步给上游治理模块。
五、创世配置(GenesisConfig)
创世配置定义在 lib.rs:
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> { pub pool: PoolT<T, I>, // 初始候选池,元素 (AccountId, Option<Score>) pub member_count: u32, // 初始成员数量 }genesis_build的构建流程(lib.rs):
- 为池中每个候选人
reserve押金(余额不足会 panic,因为仅发生在创世时一次性执行); - 写入
CandidateExists; - 按得分降序排序 Pool(
None排末尾); - 校验并写入
member_count; - 写入 Pool,并以
MembershipInitialized模式调用refresh_members,即首次加载即初始化Members并触发initialize_members信号。
mock 中的实例(mock.rs):
pallet_scored_pool::GenesisConfig::<Test> { pool: bounded_vec![(10, Some(1)), (20, Some(2)), (31, Some(2)), (40, Some(3)), (5, None)], member_count: 2, }配合Period = 4,测试query_membership_works(tests.rs)验证了创世结果:得分最高的两个实体 40(3 分)与 20(2 分,与 31 同分但索引更靠前)成为Members = [20, 40],且 31 与 40 的押金已被 reserve。
六、在 Runtime 中集成
6.1 Cargo 依赖声明
参考 Cargo.toml,在 runtime 中按以下方式引入:
[dependencies] pallet-scored-pool = { version = "4.0.0-dev", default-features = false, path = "frame/scored-pool" } [features] std = ["pallet-scored-pool/std"]该 pallet 依赖 frame-support、frame-system 与 sp-runtime 等核心库,并通过try-runtimefeature 支持链上迁移检查。
6.2 模块内封装调用
README 的 Usage 示例 展示了在一个自定义 pallet 中封装submit_candidacy的标准写法:在自己的Config上叠加scored_pool::Config,再通过<scored_pool::Pallet<T>>::submit_candidacy(...)转发调用(对应源码 lib.rs 中同构的 doctest):
use pallet_scored_pool::{self as scored_pool}; #[frame_support::pallet] 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 + scored_pool::Config {} #[pallet::call] impl<T: Config> Pallet<T> { #[pallet::weight(0)] pub fn candidate(origin: OriginFor<T>) -> DispatchResult { let who = ensure_signed(origin)?; let _ = <scored_pool::Pallet<T>>::submit_candidacy( T::RuntimeOrigin::from(Some(who.clone()).into()) ); Ok(()) } } }注意RuntimeOrigin::from(Some(who).into())把签名账户包装为Some形式,这是 FRAME pallet 之间调用时构造 signed origin 的惯用写法。
6.3 Config 完整接线示例
综合 mock.rs 与集体治理组合场景,一个可落地的Config接线如下:
impl pallet_scored_pool::Config for Runtime { type RuntimeEvent = RuntimeEvent; type KickOrigin = EnsureRoot<AccountId>; // 或治理账户 type MembershipInitialized = Council; // 复用 pallet-collective 作为接收者 type MembershipChanged = Council; type Currency = Balances; // pallet-balances type CandidateDeposit = CandidateDeposit; // 例如 ConstU128<1_000_000> type Period = ScoredPoolPeriod; // 例如 ConstU32<14_400>(每天刷新) type Score = u32; type ScoreOrigin = EnsureSignedBy<ScoreAuthority, AccountId>; type MaximumMembers = ConstU32<100>; }其中MembershipInitialized = Council依赖 pallet-collective 实现了InitializeMembers/ChangeMembers(见上文 4.2 的 collective 实现)。
七、存储布局与错误码速查
7.1 Storage 一览(lib.rs)
| 存储项 | 类型 | getter | 说明 |
|---|---|---|---|
Pool | StorageValue<BoundedVec<(AccountId, Option<Score>)>> | pool() | 按得分降序排列的候选池(None在末尾) |
CandidateExists | StorageMap<Twox64Concat, AccountId, bool> | candidate_exists() | 冗余索引,O(1) 判断是否已在池中(Pool 按分而非按账户排序,无法直接查询) |
Members | StorageValue<BoundedVec<AccountId>> | members() | 当前成员集合(已排序) |
MemberCount | StorageValue<u32> | member_count() | 成员集合大小 |
7.2 Error 一览(lib.rs)
AlreadyInPool:已提交候选资格,重复提交。InvalidIndex:传入的index越界。WrongAccountIndex:index位置上的实体与操作目标不符。TooManyMembers:Pool 容量达到MaximumMembers上限,或member_count超过上限。
八、测试验证与行为保障
模块测试覆盖 tests.rs,共 15 个用例,重点行为均有对应验证:
- 创世成员选取:
query_membership_works; - 候选资格:
submit_candidacy_works/submit_candidacy_must_not_work(余额不足与重复提交); - 打分与排序:
scoring_works(高分插入首位)/scoring_same_element_with_same_score_works(同分保持有序); - 踢出与权限:
kicking_works/kicking_works_only_for_authorized; - 周期刷新:
refreshing_works/refreshing_happens_every_period; - 退出与替补:
withdraw_scored_candidacy_must_work/withdraw_unscored_candidacy_must_work/withdraw_candidacy_must_only_work_for_members; - 索引校验:
oob_index_should_abort/index_mismatches_should_abort; - 边界与生命周期:
unscored_entities_must_not_be_used_for_filling_members/candidacy_resubmitting_works/pool_candidates_exceeded。
这些测试不仅保障了排序、押金、替补、周期刷新等核心不变量,也是二次开发时理解模块语义的最佳参考。
九、适用场景与注意事项
典型场景(基于模块机制推断,仓库未限定具体用例):
- 需要"按量化贡献/信誉动态决定治理成员"的网络,例如把验证人信誉、质押规模、链上贡献转化为
Score,由ScoreOrigin定期打分的理事会或技术委员会成员供给源; - 与 pallet-collective 组合,让理事会成员集合由得分池自动刷新,无需人工增删。
注意事项:
Score为None的实体永远不能成为成员,提交候选资格不等于获得成员身份,必须先被打分;change_member_count与score的效果均在下一个Period周期的刷新或触发刷新的移除操作时才体现到Members上;- 所有索引型调用(
withdraw_candidacy/kick/score)必须传入准确的Pool位置,否则报InvalidIndex或WrongAccountIndex,调用方应通过pool()getter 或find_in_pool辅助逻辑查询; - 押金在创世构建时即被 reserve,构建
GenesisConfig需保证池内账户余额充足; - 从 Cargo.toml 看该 pallet 版本为
4.0.0-dev,与 Substrate 主仓同源演进,集成时应保持与当前 runtime 的 FRAME 版本一致。
- 区块链
- 开发框架
- 后端
【免费下载链接】substrate
Substrate: The platform for blockchain innovators
相关推荐
Substrate 框架中的 Treasury Pallet:资金池治理与支出提案机制全解析
Substrate 框架中的 Treasury Pallet:资金池治理与支出提案机制全解析 本文围绕 Substrate 仓库中 frame/treasury
区块链开发框架后端Substrate Tips Pallet(pallet-tips)深入解析:基于 Treasury 的敏捷打赏机制与源码实现
Substrate Tips Pallet(pallet tips)深入解析:基于 Treasury 的敏捷打赏机制与源码实现 导读 本文系统讲解 Substr
区块链开发框架后端Substrate pallet-elections-phragmen 深度解析:基于顺序 Phragmén 的链上选举模块
Substrate pallet elections phragmen 深度解析:基于顺序 Phragmén 的链上选举模块 本文围绕 Substrate FR
区块链开发框架后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考