☰
Substrate 成员管理 Pallet 深度解析:用 pallet-membership 管理 Collective 成员与 Prime Member
2026/9/26 3:18:21 网站建设 项目流程
  • 区块链
  • 开发框架
  • 后端

【免费下载链接】substrate

Substrate: The platform for blockchain innovators

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

导读

pallet-membership是 Substrate FRAME 体系中负责「一组AccountId成员集合」管理的通用构件,其核心职责是控制一组账户的成员资格,供 collective(集体决策体)等上层 Pallet 复用,并支持设置一位"首席成员(prime member)"。本文将基于 frame/membership/README.md 的定义,结合 frame/membership/src/lib.rs 的完整实现,逐层剖析其存储设计、Config 接口、七个核心交易、回调契约、创世配置、权重基准与 v4 存储迁移,并给出可直接落地的 Runtime 集成方法。

模块定位:Collective 成员管理的通用构件

原 README 对本模块的定义非常精炼:

Allows control of membership of a set ofAccountIds, useful for managing membership of a collective. A prime member may be set.

翻译过来即:允许对一组AccountId的成员资格进行控制,通常用于管理某个 collective 的成员;并且可以设置一名 prime member。

由此可提炼出本模块的三个设计要点,这也是整篇文章的主线:

  1. 成员集合管理:维护一个有序、无重复、有上限的AccountId列表,支持增删、替换、整体重置、成员改钥;
  2. 面向 collective 复用:Pallet 本身不定义任何治理逻辑,而是通过 FRAME 的 Trait 契约把"成员变化"广播给上层(如 Council、Alliance 这类集体决策体);
  3. Prime Member 机制:在成员中指定一名首席成员,并随成员变动自动"重组(rejig)"。

从源码结构看,该 Pallet 的仓库布局如下(frame/membership):

  • src/lib.rs:Pallet 主体(存储、事件、错误、七个交易、Trait 实现、Benchmark、测试);
  • src/migrations/v4.rs:存储版本 4 的迁移逻辑(migrate/pre_migrate/post_migrate),由 frame/membership/src/migrations/mod.rs 导出;
  • src/weights.rs:WeightInfo权重接口定义;
  • Cargo.toml:crate 名为pallet-membership,版本4.0.0-dev,License 为 Apache-2.0。

存储设计:有序成员列表与 Prime 成员

在 frame/membership/src/lib.rs 中定义了两块存储项:

/// The current membership, stored as an ordered Vec. #[pallet::storage] #[pallet::getter(fn members)] pub type Members<T: Config<I>, I: 'static = ()> = StorageValue<_, BoundedVec<T::AccountId, T::MaxMembers>, ValueQuery>; /// The current prime member, if one exists. #[pallet::storage] #[pallet::getter(fn prime)] pub type Prime<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>;

关键设计点:

  • Members是单值存储(StorageValue),类型为BoundedVec<T::AccountId, T::MaxMembers>,带ValueQuery(查询默认返回空向量)。始终维护为有序向量——所有增删操作都依赖binary_search定位,插入用try_insert保持有序性,这保证了Contains、SortedMembers等 Trait 实现的高效与正确。
  • Prime是可选单值存储(OptionQuery),不存在时返回None。
  • 当前存储版本为STORAGE_VERSION = StorageVersion::new(4)(见 lib.rs),并通过#[pallet::storage_version(STORAGE_VERSION)]声明。

Config 接口:权限 Origin 与回调 Trait 详解

在 lib.rs 中,ConfigTrait 是本 Pallet 的配置入口,集成时需全部指定:

#[pallet::config] pub trait Config<I: 'static = ()>: frame_system::Config { type RuntimeEvent: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; /// Required origin for adding a member (though can always be Root). type AddOrigin: EnsureOrigin<Self::RuntimeOrigin>; /// Required origin for removing a member (though can always be Root). type RemoveOrigin: EnsureOrigin<Self::RuntimeOrigin>; /// Required origin for adding and removing a member in a single action. type SwapOrigin: EnsureOrigin<Self::RuntimeOrigin>; /// Required origin for resetting membership. type ResetOrigin: EnsureOrigin<Self::RuntimeOrigin>; /// Required origin for setting or resetting the prime member. type PrimeOrigin: EnsureOrigin<Self::RuntimeOrigin>; /// The receiver of the signal for when the membership has been initialized. type MembershipInitialized: InitializeMembers<Self::AccountId>; /// The receiver of the signal for when the membership has changed. type MembershipChanged: ChangeMembers<Self::AccountId>; /// The maximum number of members that this membership can have. type MaxMembers: Get<u32>; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; }

逐项说明:

  • 五个权限 Origin:AddOrigin、RemoveOrigin、SwapOrigin、ResetOrigin、PrimeOrigin分别约束五种管理操作。注释特别强调"though can always be Root"——即任何这类 Origin 都可以直接配置为EnsureRoot,实现仅限治理体(如 Council)或 Root 调用。
  • 两个回调 Trait:MembershipInitialized: InitializeMembers<AccountId>负责创世初始化信号;MembershipChanged: ChangeMembers<AccountId>负责后续每次成员变更的信号。注释指出,如果初始化与变更不需要区分处理,两者可指向同一实现(源码测试就是这么做的)。
  • MaxMembers: Get<u32>:成员数量上限,注释明确"This is enforced in the code; the membership size can not exceed this limit"——它不只是 Benchmark 参数,而是由BoundedVec在代码层面强制执行的硬上限。
  • WeightInfo:由 frame/membership/src/weights.rs 提供的权重接口。

以 lib.rs 中的测试配置 为例,一个最小可用配置是这样的:

impl Config for Test { type RuntimeEvent = RuntimeEvent; type AddOrigin = EnsureSignedBy<One, u64>; type RemoveOrigin = EnsureSignedBy<Two, u64>; type SwapOrigin = EnsureSignedBy<Three, u64>; type ResetOrigin = EnsureSignedBy<Four, u64>; type PrimeOrigin = EnsureSignedBy<Five, u64>; type MembershipInitialized = TestChangeMembers; type MembershipChanged = TestChangeMembers; type MaxMembers = ConstU32<10>; type WeightInfo = (); }

这里用EnsureSignedBy<One, u64>表示"只有账户 1 可以添加成员",生产环境通常替换为EnsureRoot或EnsureOrigin<...>委托给 Council 等集体 Origin。

七个核心交易(Dispatchable Calls)逐个解析

lib.rs 中定义了 7 个#[pallet::call],均为固定权重50_000_000(源码中硬编码),并各自声明了call_index0~6。

add_member(call_index 0)——添加成员

#[pallet::call_index(0)] #[pallet::weight({50_000_000})] pub fn add_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult { T::AddOrigin::ensure_origin(origin)?; let who = T::Lookup::lookup(who)?; let mut members = <Members<T, I>>::get(); let location = members.binary_search(&who).err().ok_or(Error::<T, I>::AlreadyMember)?; members.try_insert(location, who.clone()) .map_err(|_| Error::<T, I>::TooManyMembers)?; <Members<T, I>>::put(&members); T::MembershipChanged::change_members_sorted(&[who], &[], &members[..]); Self::deposit_event(Event::MemberAdded); Ok(()) }

行为要点:仅允许T::AddOrigin;用binary_search检查是否已是成员(AlreadyMember);try_insert触发TooManyMembers上限保护;写入后通过change_members_sorted(&[who], &[], &members[..])通知上层"新增了谁"。

remove_member(call_index 1)——移除成员

仅允许T::RemoveOrigin;binary_search定位不到则报NotMember;移除后调用rejig_prime处理 prime 的去留(见下文),并发出MemberRemoved事件。值得注意:如果被移除的是 prime,prime 会被清空,不会自动转移到其他成员。

swap_member(call_index 2)——一步替换成员

#[pallet::call_index(2)] #[pallet::weight({50_000_000})] pub fn swap_member(origin, remove, add) -> DispatchResult { // ...origin 检查、lookup... if remove == add { return Ok(()) } let location = members.binary_search(&remove).ok().ok_or(Error::<T, I>::NotMember)?; let _ = members.binary_search(&add).err().ok_or(Error::<T, I>::AlreadyMember)?; members[location] = add.clone(); members.sort(); // ...写入、通知 change_members_sorted(&[add], &[remove], ...)、rejig_prime... Self::deposit_event(Event::MembersSwapped); Ok(()) }

"添加 + 移除"合二为一的单步操作,仅允许T::SwapOrigin。文档注释明确:prime 资格不会从被移除者转给新加入者("Prime membership isnotpassed fromremovetoadd, if extant."),替换后 prime 会经过rejig_prime重新判定。

reset_members(call_index 3)——整体重置成员

仅允许T::ResetOrigin;接收一个Vec<T::AccountId>并转换成BoundedVec(超上限则TooManyMembers),排序后调用:

T::MembershipChanged::set_members_sorted(&members[..], m); Self::rejig_prime(&members);

set_members_sorted会把"新旧成员差异"一并通知给上层,适用于成员大换血场景。

change_key(call_index 4)——成员自换钥匙

与前面不同,它不需要任何管理 Origin,仅要求Signed来源且调用者是当前成员:

let remove = ensure_signed(origin)?; // ...binary_search 校验、替换、排序、通知 change_members_sorted(&[new], &[remove], ...)... if Prime::<T, I>::get() == Some(remove) { Prime::<T, I>::put(&new); T::MembershipChanged::set_prime(Some(new)); }

注释明确:若调用者是 prime,则 prime 资格会转移给新 key("Prime membership is passed from the origin account tonew, if extant."),这是与swap_member最显著的区别。

set_prime / clear_prime(call_index 5 / 6)——设置与清除首席成员

#[pallet::call_index(5)] pub fn set_prime(origin, who) -> DispatchResult { T::PrimeOrigin::ensure_origin(origin)?; let who = T::Lookup::lookup(who)?; Self::members().binary_search(&who).ok().ok_or(Error::<T, I>::NotMember)?; Prime::<T, I>::put(&who); T::MembershipChanged::set_prime(Some(who)); Ok(()) } #[pallet::call_index(6)] pub fn clear_prime(origin) -> DispatchResult { T::PrimeOrigin::ensure_origin(origin)?; Prime::<T, I>::kill(); T::MembershipChanged::set_prime(None); Ok(()) }

set_prime要求目标必须是当前成员(否则NotMember);clear_prime直接kill掉存储项。两者都只允许T::PrimeOrigin。

内部辅助:rejig_prime

lib.rs 中的私有函数rejig_prime是 prime 一致性的保障:每次成员减少或替换后,若 prime 仍在新成员列表中则通过set_prime(Some(prime))通知上层,否则kill清除。这保证了Prime存储项永远不会指向非成员。

事件与错误

事件定义见 lib.rs:

事件含义
MemberAdded有成员被添加(详情见交易参数)
MemberRemoved有成员被移除
MembersSwapped两名成员被替换
MembersReset成员集合被整体重置
KeyChanged某成员更换了 key
Dummy { _phantom_data }仅用于满足元数据生成的占位事件,实际从不触发

错误定义见 lib.rs:

  • AlreadyMember:目标账户已在成员列表中(如重复添加、重复替换);
  • NotMember:目标账户不在成员列表中(如移除、替换、设置 prime 时);
  • TooManyMembers:成员数将超过MaxMembers上限。

与上层 Collective 的集成契约

这是 README 中"useful for managing membership of a collective"的技术落点。Pallet 实现了四个 FRAME Trait(lib.rs):

impl<T: Config<I>, I: 'static> Contains<T::AccountId> for Pallet<T, I> { fn contains(t: &T::AccountId) -> bool { Self::members().binary_search(t).is_ok() } } impl<T: Config<I>, I: 'static> SortedMembers<T::AccountId> for Pallet<T, I> { fn sorted_members() -> Vec<T::AccountId> { Self::members().to_vec() } fn count() -> usize { Members::<T, I>::decode_len().unwrap_or(0) } }
  • Contains<AccountId>:实现contains,让其他模块可以直接用T::Membership::contains(&who)判断成员资格;
  • SortedMembers<AccountId>:提供sorted_members()与高效的count()(直接读编码长度,避免解码整个向量),供需要遍历全体成员的模块(如议会选举权重分配)使用。

反向地,本 Pallet 通过Config中的MembershipChanged: ChangeMembers<AccountId>与MembershipInitialized: InitializeMembers<AccountId>向消费方广播变化。ChangeMembers契约包含change_members_sorted(incoming, outgoing, new)、set_prime、get_prime等方法(测试实现见 lib.rs)。测试中的TestChangeMembers实现还内建了一致性断言:旧成员 + incoming排序后必须等于新成员 + outgoing排序后,用于在单元测试中自动校验回调数据没有丢人漏人。

创世配置(GenesisConfig)

创世配置定义在 lib.rs:

#[pallet::genesis_config] pub struct GenesisConfig<T: Config<I>, I: 'static = ()> { pub members: BoundedVec<T::AccountId, T::MaxMembers>, #[serde(skip)] pub phantom: PhantomData<I>, }

genesis_build中的关键逻辑:

  1. 用BTreeSet检查重复成员,重复则直接断言失败(panic),测试用例genesis_build_panics_with_duplicate_members专门验证了这一点(lib.rs);
  2. 对成员排序;
  3. 调用T::MembershipInitialized::initialize_members(&members)——即创世时通过InitializeMembersTrait 通知上层初始化(注释指出这是 pre-genesis 阶段的初始化信号);
  4. 写入Members存储。

测试中的用法示例(lib.rs):

pallet_membership::GenesisConfig::<Test> { members: bounded_vec![10, 20, 30], ..Default::default() }.assimilate_storage(&mut t).unwrap();

权重与基准测试

每个交易在源码中固定标注#[pallet::weight({50_000_000})],即硬编码 50,000,000 权重单位。与此同时,lib.rs 使用benchmarks_instance_pallet!宏为全部 7 个交易定义了完整的基准用例:

  • add_member:成员数m从 1 到MaxMembers - 1扫描;
  • remove_member:覆盖"移除 prime"这一最重路径,verify阶段断言 prime 被正确 rejig;
  • swap_member:覆盖移除 non-prime 后 prime 需要重设的情况;
  • reset_member:整体换血且保留共同成员,验证 prime 状态;
  • change_key:注释指出最坏情况是"更换 prime 的 key",verify断言 prime 转移到新 key;
  • set_prime/clear_prime:分别断言Prime存储与MembershipChanged::get_prime()同步。

每个 benchmark 末尾的verify都会校验存储与回调的一致性,并用impl_benchmark_test_suite!把基准用例同时变成可运行的测试套件。若你修改了MaxMembers相关逻辑,按Config注释要求应重跑这些基准以重新生成权重。

v4 存储迁移

存储版本 4 的迁移实现在 frame/membership/src/migrations/v4.rs,核心函数migrate的作用是:把整个 Pallet 的存储迁移到新的前缀(pallet 名)下,用于处理运行时中 Pallet 改名导致的状态键前缀变化。

  • 触发条件:on_chain_storage_version < 4,且新旧前缀不同;
  • 操作:frame_support::storage::migration::move_pallet(old, new)移动全部存储,然后写入StorageVersion::new(4);
  • 配套提供了pre_migrate/post_migrate校验函数:迁移前断言新前缀下除 storage version key 外没有脏数据、链上版本 < 4;迁移后断言旧前缀完全清空、新前缀有数据、链上版本 == 4。

测试用例migration_v4(lib.rs)演示了标准的三段式调用序列:先move_pallet制造"旧状态",置版本为 0,再依次执行pre_migrate→migrate→post_migrate。这套函数可以直接挂接到OnRuntimeUpgrade的pre_upgrade/post_upgrade钩子中做运行时升级演练。

测试用例如何验证核心语义

lib.rs 内建了完整的#[cfg(test)]单元测试,是理解本模块行为语义的最佳入口,包括:

  • query_membership_works:创世后Membership::members()与InitializeMembers回调状态一致;
  • prime_member_works:非PrimeOrigin调用被拒(BadOrigin)、对非成员设置 prime 报NotMember、正确设置/清除及回调同步;
  • add_member_works/remove_member_works:分别验证AlreadyMember/NotMember错误,以及 prime 被移除后自动清除;
  • swap_member_works:验证NotMember/AlreadyMember分支,以及"swap 到自身"(remove == add)时直接成功返回、prime 保留;还验证了 swap 后成员列表重新排序;
  • change_key_works:验证非成员调用报NotMember、新 key 已是成员报AlreadyMember,以及 prime 转移给新 key;
  • reset_members_works:验证仅ResetOrigin可调用、成员重排、prime 在新集合中则保留、否则清除;
  • genesis_build_panics_with_duplicate_members:创世重复成员直接 panic;
  • migration_v4:完整迁移三件套。

这些用例共同勾勒出本模块的不可变语义:成员列表始终有序无重复且受MaxMembers约束;prime 永远是当前成员之一;每次变化都会同步触发上层回调。

在 Runtime 中集成:Cargo.toml 与 construct_runtime

要将该模块加入你的 Substrate Runtime,需要:

  1. 添加依赖(参考 frame/membership/Cargo.toml 的依赖与 feature 定义):
[dependencies] pallet-membership = { version = "4.0.0-dev", default-features = false, path = "frame/membership" } [features] std = [ "pallet-membership/std", ... ] runtime-benchmarks = [ "pallet-membership/runtime-benchmarks", ... ] try-runtime = [ "pallet-membership/try-runtime", ... ]
  1. 实现Config:按上文逐一指定 5 个 Origin(生产环境通常委托给EnsureRoot或集体治理 Origin)、MaxMembers常量、以及MembershipInitialized/MembershipChanged(指向 Council 等消费方,或一个内部实现的ChangeMembers适配器)。

  2. 注册进construct_runtime!(与 lib.rs 测试中的用法 一致):

construct_runtime!( pub enum Runtime { // ... Membership: pallet_membership::{Pallet, Call, Storage, Config<T>, Event<T>}, // ... } );
  1. 配置创世成员:在GenesisConfig的membership.members字段填入初始AccountId列表(注意去重、且数量不超过MaxMembers)。

值得注意的是,该 Pallet 的Pallet<T, I = ()>支持实例化(instantiable),可通过I泛型在同一 Runtime 中部署多个互不干扰的成员集合实例,适用于"多个委员会各管各的成员"的场景。

总结

pallet-membership是一个"小而专"的 FRAME 构件:它不关心治理规则本身,只负责把"一组有序、无重复、有上限的成员"管理好,并通过ChangeMembers/InitializeMembers/Contains/SortedMembers这些 Trait 契约,把成员变化精确地广播给 Council 等 collective 消费方。配合MaxMembers硬上限、自动维护的 prime member(含rejig_prime一致性保障)、完整的 Benchmark 与单元测试,以及 v4 存储迁移工具,它是构建链上任何"成员制集体"(议会、联盟、准入制社区)时最直接可复用的基础模块。

参考文件索引

  • 模块说明:frame/membership/README.md
  • 核心实现:frame/membership/src/lib.rs
  • 存储迁移:frame/membership/src/migrations/v4.rs、frame/membership/src/migrations/mod.rs
  • 权重接口:frame/membership/src/weights.rs
  • 依赖与特性声明:frame/membership/Cargo.toml
  • 区块链
  • 开发框架
  • 后端

【免费下载链接】substrate

Substrate: The platform for blockchain innovators

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

相关推荐

上一篇:Logto 微信原生连接器接入实战:从微信开放平台申请到 iOS/Android SDK 集成
下一篇:Express.js 完整速查指南:Hello World、路由、中间件与四大核心 API 全解析(Quick Reference 开源速查清单)

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询