news 2026/9/26 2:10:05

Substrate Scored Pool Pallet 深度解析:基于评分排名的成员池与治理信号集成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Substrate Scored Pool Pallet 深度解析:基于评分排名的成员池与治理信号集成
  • 区块链
  • 开发框架
  • 后端

【免费下载链接】substrate

Substrate: The platform for blockchain innovators

项目地址:https://gitcode.com/gh_mirrors/su/substrate
点击查看免费下载

导读

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 中的示例值
CurrencyCurrency<AccountId> + ReservableCurrency<AccountId>用于押金(deposit)的货币,必须是可保留(reserve)余额的货币类型,通常直接用pallet-balancesBalances
MaximumMembersGet<u32>Pool 与 Members 两个有界向量的容量上限(#[pallet::constant])ConstU32<10>
ScoreAtLeast32Bit + Clone + Copy + Default + FullCodec + ... + MaxEncodedLen赋予成员的量化得分类型,要求全序可比较u64
RuntimeEventFrom<Event<Self,I>> + IsType<...>统一事件类型RuntimeEvent
CandidateDepositGet<BalanceOf<Self,I>>提交候选资格时被 reserve 的押金,退出或被踢时返还(#[pallet::constant])25
PeriodGet<BlockNumberFor<Self>>成员集合刷新的区块周期(#[pallet::constant])ConstU64<4>
MembershipInitializedInitializeMembers<AccountId>创世(pre-genesis)时的成员初始化信号接收者,通常与MembershipChanged相同TestChangeMembers
MembershipChangedChangeMembers<AccountId>每次成员变更时收到set_members_sorted信号的接收者TestChangeMembers
ScoreOriginEnsureOrigin<RuntimeOrigin>允许给候选人打分的来源(Origin)EnsureSignedBy<ScoreOrigin, u64>(账户 3)
KickOriginEnsureOrigin<RuntimeOrigin>允许移除实体的来源,可配置为 RootEnsureSignedBy<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)是模块的核心内部函数:

  1. 读取当前MemberCount与旧Members;
  2. 从 Pool 中过滤掉Score == None的实体,取前count个(Pool 本身按得分降序,因此天然是最高分);
  3. 将结果转为BoundedVec并排序(sort(),按 AccountId 字典序);
  4. 写入Members存储;
  5. 依据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):

  1. 为池中每个候选人reserve押金(余额不足会 panic,因为仅发生在创世时一次性执行);
  2. 写入CandidateExists;
  3. 按得分降序排序 Pool(None排末尾);
  4. 校验并写入member_count;
  5. 写入 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说明
PoolStorageValue<BoundedVec<(AccountId, Option<Score>)>>pool()按得分降序排列的候选池(None在末尾)
CandidateExistsStorageMap<Twox64Concat, AccountId, bool>candidate_exists()冗余索引,O(1) 判断是否已在池中(Pool 按分而非按账户排序,无法直接查询)
MembersStorageValue<BoundedVec<AccountId>>members()当前成员集合(已排序)
MemberCountStorageValue<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

项目地址:https://gitcode.com/gh_mirrors/su/substrate
点击查看免费下载
上一篇:NoteGen 开源项目使用与启动教程
下一篇:PuTTY 开源项目使用指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 2:09:05

使用函数二值化进行数据特征离散化

在数据分析与机器学习中,数据预处理是一个极为关键的环节。其中,特征的离散化可以有效地提升模型的表现。离散化特征是指将连续变量转换为离散变量,这对于分类任务以及某些模型结构特别有帮助。Python提供了丰富的函数和方法来实现数据的离散化,本文将重点介绍通过阈值二值…

作者头像 李华
网站建设 2026/9/26 2:07:12

Terraform 管理云主机实战:从零创建腾讯云 CVM 与状态管理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 2:06:42

clingo 的 austere 逻辑程序:用 reify 元编码计算稳定模型

人工智能AI Agent多模态语音AI 应用 【免费下载链接】ten-framework Open-source framework for conversational voice AI agents 项目地址&#xff1a; https://gitcode.com/TEN-framework/ten-framework 点击查看 免费下载 导读 本文围绕 clingo 仓库中 austere 示例 展开&…

作者头像 李华