- 区块链
- 开发框架
- 后端
【免费下载链接】substrate
Substrate: The platform for blockchain innovators
本文围绕 frame/collective/README.md 展开,系统讲解 Substrate 中
pallet-collective的核心设计:两种成员来源、两类默认投票策略、提案动议(motion)从提出到执行的完整生命周期,并结合仓库源码与真实运行时配置给出可落地的治理实践方案。读完本文,你将掌握如何在 Substrate 运行时中接入并配置理事会 / 技术委员会,理解提案门槛(threshold)、Prime 成员与弃权票的交互规则,并能读懂相关测试用例与权重估算逻辑。
一、Collective 是什么:把一组账户的“集体意愿”变成可执行调用
在 Substrate 生态中,pallet-collective是一个经典的多签治理原语:它把一组AccountId(成员)组织成一个集体,成员可以通过两个专用 Origin发起 dispatch 调用,从而让“集体的意志”可被链上执行。它的典型应用场景是理事会(Council)与技术委员会(Technical Committee)——例如节点示例运行时中,Council、TechnicalCommittee、AllianceMotion都是通过实例化pallet_collective得到的(见 bin/node/runtime/src/lib.rs)。
Council: pallet_collective::<Instance1>, TechnicalCommittee: pallet_collective::<Instance2>, AllianceMotion: pallet_collective::<Instance3>,每个实例拥有独立的成员集合、独立的投票状态、独立的存储,因此可以在同一个链上同时存在多个互不干扰的集体。
两个专用 Origin定义在 frame/collective/src/lib.rs 的RawOrigin枚举中:
Members(MemberCount, MemberCount):该调用已被集体的一定数量成员认可,参数为(认可人数, 总席位数);Member(AccountId):该调用已被单个成员认可。
这两个 Origin 是其他 pallet(如 Democracy、Treasury、Elections)授权给集体代行治理权限的桥梁。例如在节点运行时中,EnsureProportionAtLeast<AccountId, CouncilCollective, 3, 4>被用作外部多数提案的授权来源(bin/node/runtime/src/lib.rs),表示“需要 ≥3/4 的理事会成员认可”。
二、成员从哪里来:set_members直接设置与ChangeMembers间接管理
README 明确指出,集体成员有两种提供方式:
- 直接方式:调用 Root 可调用的
set_members函数; - 间接方式:由其他 pallet 实现
ChangeMemberstrait 来维护成员集合。
2.1 直接方式:set_members
set_members是pallet-collective的call_index(0)调用(frame/collective/src/lib.rs),签名如下:
pub fn set_members( origin: OriginFor<T>, new_members: Vec<T::AccountId>, prime: Option<T::AccountId>, old_count: MemberCount, ) -> DispatchResultWithPostInfonew_members:新成员列表,建议调用方提供排序后的列表(pallet 内部也会再次sort());prime:可选的 Prime 成员;old_count:存储中旧成员数量的上界,用于权重估算;- 调用来源必须满足
SetMembersOrigin(在运行时中通常配置为EnsureRoot,即只有 Root 或经由治理授权的来源可以设置成员)。
实现细节值得注意:
- 若
prime提供但不是成员,返回Error::PrimeAccountNotMember; - 若
new_members超过MaxMembers,只打 error 日志、不拒绝执行——这正是 README 强调的“pallet 假设成员数不超过MaxMembers用于权重计算,但不在set_members中强制”; - 内部走
ChangeMembers::set_members_sorted更新成员并清空 Prime(见change_members_sorted中Prime::<T, I>::kill())。
2.2 间接方式:ChangeMemberstrait
ChangeMembers由frame_support提供,pallet-collective为它实现了三个方法(frame/collective/src/lib.rs):
change_members_sorted(incoming, outgoing, new):更新成员列表,从所有进行中的动议投票里剔除已离任成员,并重置 Prime;set_prime(prime):设置 Prime;get_prime():读取 Prime。
典型用法是选举 pallet:节点运行时中pallet_elections_phragmen::Config的type ChangeMembers = Council、type InitializeMembers = Council(bin/node/runtime/src/lib.rs),即理事会成员由 Phragmén 选举产生,当选/离任时自动回调 collective 更新成员。README 中的 WARNING 在此得到印证:set_members与外部成员管理逻辑必须保持同步,否则会“成员集合与治理逻辑脱节”。
此外,InitializeMemberstrait 的initialize_members会在genesis 阶段初始化成员(要求存储中尚无成员,否则断言AlreadyInitialized),并自动排序(frame/collective/src/lib.rs)。genesis 配置GenesisConfig.members还校验了“不能包含重复账户”和“数量不能超过MaxMembers”(frame/collective/src/lib.rs)。
三、Prime 成员与两种默认投票策略
当投票期结束后仍存在**弃权票(abstentions)**时,Prime 成员与DefaultVote策略共同决定这些弃权票如何被“补齐”。该机制由DefaultVotetrait 抽象(frame/collective/src/lib.rs):
fn default_vote( prime_vote: Option<bool>, yes_votes: MemberCount, no_votes: MemberCount, len: MemberCount, ) -> bool;仓库内置两种实现:
| 策略 | 行为 | 源码位置 |
|---|---|---|
PrimeDefaultVote | 直接以 Prime 的投票作为弃权票的默认票;若未设置 Prime,则默认按“反对”(unwrap_or(false))处理 | frame/collective/src/lib.rs |
MoreThanMajorityThenPrimeDefaultVote | 先判断“赞成票是否超过全体过半(yes_votes * 2 > len)”,若过半则弃权默认投赞成;否则退回 Prime 投票 | frame/collective/src/lib.rs |
节点运行时的理事会与技术委员会均采用PrimeDefaultVote(bin/node/runtime/src/lib.rs)。README 中“如果未达阈值且无 Prime,则动议被直接丢弃而不执行”的语义,正是PrimeDefaultVote在无 Prime 时返回false(弃权计为反对)的体现。
四、动议(Motion)的完整生命周期:提案 → 投票 → 关闭 → 执行
README 描述了动议的核心流程,下面结合源码逐步拆解(核心逻辑集中在 frame/collective/src/lib.rs)。
4.1 提出:propose
propose(call_index(2))要求发起人是成员,参数为:
pub fn propose( origin: OriginFor<T>, #[pallet::compact] threshold: MemberCount, proposal: Box<<T as Config<I>>::Proposal>, #[pallet::compact] length_bound: u32, )threshold < 2时:不进入投票,直接执行(do_propose_execute),动议以RawOrigin::Members(1, seats)的 Origin 被 dispatch;threshold >= 2时:进入投票(do_propose_proposed),写入四类存储:Proposals:当前活动动议哈希的BoundedVec;ProposalOf:哈希 → 具体调用;ProposalCount:全局计数器(单调递增);Voting:哈希 →Votes { index, threshold, ayes, nays, end }(frame/collective/src/lib.rs),其中end = 当前区块号 + MotionDuration。
同时校验:
- 动议编码长度 ≤
length_bound(否则WrongProposalLength); - 动议权重 ≤
MaxProposalWeight(否则WrongProposalWeight); - 哈希不能重复(否则
DuplicateProposal); - 活动动议数不能超过
MaxProposals(否则TooManyProposals)。
测试用例limit_active_proposals验证了连续提出MaxProposals个动议后,第MaxProposals + 1个会被TooManyProposals拒绝(frame/collective/src/tests.rs)。
4.2 投票:vote
成员通过vote(call_index(3))投出赞成/反对:
pub fn vote( origin: OriginFor<T>, proposal: T::Hash, #[pallet::compact] index: ProposalIndex, approve: bool, )do_vote内部(frame/collective/src/lib.rs):
- 校验动议存在(
ProposalMissing)且index匹配(WrongIndex); - 同一次动议中,每位成员只保留一个有效票:重复投同样的票报
DuplicateVote;改投则从对侧列表swap_remove移出再插入新侧; - 返回“是否该成员在该动议上的首投”——首投免交易费(
Pays::No),改投/重复投票收费(Pays::Yes),此行为被测试motions_all_first_vote_free_works覆盖(frame/collective/src/tests.rs)。
4.3 关闭:close与三种结局
动议的关闭由close(call_index(6))完成,任何签名账户都可以调用。其核心逻辑do_close(frame/collective/src/lib.rs)分三种情况:
- 提前通过:
yes_votes >= threshold时立即关闭,校验proposal_weight_bound与length_bound后执行提案,Pays::Yes; - 提前否决:
seats - no_votes < threshold(即使剩余所有成员都投赞成也无法达标)时立即关闭并移除动议,Pays::No(豁免费用); - 投票期结束后的结算:
block_number >= voting.end前调用会报TooEarly(测试close_works验证了这一点,见 frame/collective/src/tests.rs)。此时:- 取 Prime 的投票(若 Prime 在 ayes 中则
Some(true)); - 调用
T::DefaultVote::default_vote(prime_vote, yes_votes, no_votes, seats)计算弃权票的默认方向; - 把弃权票数加到对应一侧:
abstentions = seats - (yes_votes + no_votes); - 重新计算
approved = yes_votes >= threshold; - 通过则执行提案(
do_approve_proposal以RawOrigin::Members(yes_votes, seats)分发,发Approved、Executed事件);不通过则do_disapprove_proposal移除动议并发Disapproved事件。
- 取 Prime 的投票(若 Prime 在 ayes 中则
close还要求调用者提供proposal_weight_bound(执行提案的权重上界)与length_bound(存储长度上界),validate_and_get_proposal通过storage::read直接读取存储长度进行校验(frame/collective/src/lib.rs)。
4.4 其他调用
execute(call_index(1)):成员以MemberOrigin直接执行一个提案(跳过投票);disapprove_proposal(call_index(5)):Root 专用,无论动议当前处于什么状态都直接否决并移除(ensure_root校验)。
五、存储结构与错误处理
5.1 五块核心存储
| 存储项 | 类型 | 说明 |
|---|---|---|
Proposals | StorageValue<BoundedVec<Hash, MaxProposals>> | 活动动议哈希列表,受MaxProposals约束 |
ProposalOf | StorageMap<Hash, Proposal> | 哈希 → 提案调用 |
Voting | StorageMap<Hash, Votes> | 哈希 → 投票状态(threshold / ayes / nays / end) |
ProposalCount | StorageValue<u32> | 动议计数器(只增不减) |
Members | StorageValue<Vec<AccountId>> | 当前成员(按值排序存储) |
Prime | StorageValue<AccountId> | 当前 Prime 成员 |
(定义见 frame/collective/src/lib.rs。)
5.2 错误枚举
Error共 11 个变体(frame/collective/src/lib.rs):NotMember、DuplicateProposal、ProposalMissing、WrongIndex、DuplicateVote、AlreadyInitialized、TooEarly、TooManyProposals、WrongProposalWeight、WrongProposalLength、PrimeAccountNotMember。各错误的触发条件均已在前文对应流程中说明。
5.3 事件(Event)——动议的可观察轨迹
每次动议操作都会产生链上事件:Proposed、Voted、Approved、Disapproved、Executed、MemberExecuted、Closed(frame/collective/src/lib.rs)。测试中对事件序列的断言(如close_with_prime_works期望Proposed → Voted → Closed → Approved → Executed,frame/collective/src/tests.rs)展示了完整的状态机流转,可用于索引器与治理 UI 的开发。
六、Origin 组合器:把集体意志授权给其他模块
README 强调“通过两个专用 Origin 派发调用”,而pallet-collective还导出了四个EnsureOrigin实现,供其他 pallet 作为Config中的授权来源(frame/collective/src/lib.rs):
| 组合器 | 语义 |
|---|---|
EnsureMember<AccountId, I> | Origin 必须是单个成员,返回该成员账户 |
EnsureMembers<AccountId, I, N> | Origin 必须获得至少 N 位成员认可 |
EnsureProportionMoreThan<AccountId, I, N, D> | 认可比例> N/D |
EnsureProportionAtLeast<AccountId, I, N, D> | 认可比例≥ N/D |
节点运行时大量使用这些组合器,例如:理事会3/4多数用于ExternalMajorityOrigin(bin/node/runtime/src/lib.rs),1/2用于FastTrackOrigin,2/3用于VetoOrigin等(bin/node/runtime/src/lib.rs)。这说明了 collective 在整条链治理栈中的“枢纽”地位:选举产生成员,成员通过动议形成 Origin,Origin 再驱动民主、国库等模块的执行。
七、在运行时中接入 Collective:参数配置实战
参考节点运行时(bin/node/runtime/src/lib.rs),配置一个理事会实例需要完成三步。
第一步:声明实例与参数
parameter_types! { pub const CouncilMotionDuration: BlockNumber = 5 * DAYS; pub const CouncilMaxProposals: u32 = 100; pub const CouncilMaxMembers: u32 = 100; } type CouncilCollective = pallet_collective::Instance1;第二步:实现pallet_collective::Config
impl pallet_collective::Config<CouncilCollective> for Runtime { type RuntimeOrigin = RuntimeOrigin; type Proposal = RuntimeCall; // 提案类型:运行时全部调用 type RuntimeEvent = RuntimeEvent; type MotionDuration = CouncilMotionDuration; // 投票期:5 天 type MaxProposals = CouncilMaxProposals; // 并行活动动议上限:100 type MaxMembers = CouncilMaxMembers; // 成员上限:100(用于权重估算) type DefaultVote = pallet_collective::PrimeDefaultVote; type WeightInfo = pallet_collective::weights::SubstrateWeight<Runtime>; type SetMembersOrigin = EnsureRoot<Self::AccountId>; // 只有 Root 可设成员 type MaxProposalWeight = MaxCollectivesProposalWeight; }第三步:注册进construct_runtime!
Council: pallet_collective::<Instance1>,各配置项的要点:
MotionDuration:动议最短投票窗口,任何成员都不得在窗口结束前强制结算(除非已提前达标);MaxProposals:同时存在的活动动议数量上限,超出报TooManyProposals;MaxMembers:仅用于权重估算,pallet 本身不在set_members/change_members_sorted中强制执行(README 明确说明这一点);测试与运行时也通过const_assert!(DesiredMembers::get() <= CouncilMaxMembers::get())保证选举人数不超过该上限(bin/node/runtime/src/lib.rs);SetMembersOrigin:控制谁能直接改成员,示例中为EnsureRoot;MaxProposalWeight:可提出/执行提案的重量上限,防止动议携带超重调用;WeightInfo:每个 extrinsic 的基准权重,由 frame/collective/src/benchmarking.rs 生成,注意 README 与代码注释均提示:修改MaxMembers后需重新跑基准并更新权重(set_members的权重复杂度为O(MP + N),close的权重在四种关闭路径中取max再累加提案执行权重)。
若采用“选举产生成员”的间接模式,还需把pallet_collective实例作为选举 pallet 的ChangeMembers/InitializeMembers,例如type ChangeMembers = Council(bin/node/runtime/src/lib.rs)。
八、运行测试与验证
pallet-collective自带 1500+ 行单元测试(frame/collective/src/tests.rs),覆盖以下关键行为,可作为理解与验证实现的入口:
initialize_members_sorts_members:成员初始化后自动排序;set_members_with_prime_works:设置 Prime、校验PrimeAccountNotMember;close_works:投票期内提前close报TooEarly,期后结算产生Closed + Disapproved事件;close_with_voting_prime_works:Prime 投了赞成时,弃权票按 Prime 方向补齐并执行提案;close_with_no_prime_but_majority_works:MoreThanMajorityThenPrimeDefaultVote下,即使无 Prime 也可由过半赞成票推动通过;removal_of_old_voters_votes_works:成员变动后,离任成员在投票列表中被剔除;proposal_weight_limit_works(_on_approve):提案权重上界校验与“否决路径忽略权重校验”的行为;limit_active_proposals:活动动议数量上限校验。
每个测试经由build_and_execute运行后还会调用do_try_state()做一致性校验(try-runtime特性,frame/collective/src/lib.rs):包括Proposals/ProposalOf哈希一一对应、票数总和不超过MaxMembers、动议索引唯一、成员有序且不超过上限、Prime 必须是成员等不变量。迁移逻辑则维护在 frame/collective/src/migrations(当前存储版本为 v4,见STORAGE_VERSION = StorageVersion::new(4))。
九、设计要点小结
- 成员管理双轨制:Root 直设(
set_members)与外部模块回调(ChangeMembers)并存,注意同步一致性; MaxMembers是软约束:它服务于权重估算而非运行时强制,治理上需由上层(如选举 pallet 的上限断言)兜底;- Prime 是弃权票的“方向盘”:
PrimeDefaultVote下 Prime 缺席则默认反对,MoreThanMajorityThenPrimeDefaultVote下可先看整体多数再回落 Prime; - 动议可提前结算:只要“赞成已达标”或“反对已无法挽回”,任何账户都可提前
close,无需等待MotionDuration结束; - 首投免费用:鼓励成员尽早表态,改投则收取费用,抑制反复横跳;
- 实例化复用:同一 pallet 通过
Instance1/2/3可在一条链上承载理事会、技术委员会、联盟治理等多个集体,互不干扰。
通过 frame/collective/README.md、frame/collective/src/lib.rs 与 bin/node/runtime/src/lib.rs 三者的对照阅读,开发者可以完整掌握从“成员集合 → 动议投票 → Origin 授权 → 跨模块治理执行”的整条链路,并将其复用到自定义 Substrate 链的治理设计中。
- 区块链
- 开发框架
- 后端
【免费下载链接】substrate
Substrate: The platform for blockchain innovators
相关推荐
Substrate Democracy Pallet 深度解析:从提案入队到公投投票的链上治理机制
Substrate Democracy Pallet 深度解析:从提案入队到公投投票的链上治理机制 导读 Democracy Pallet 是 Substrat
区块链开发框架后端Substrate 成员管理 Pallet 深度解析:用 pallet-membership 管理 Collective 成员与 Prime Member
Substrate 成员管理 Pallet 深度解析:用 pallet membership 管理 Collective 成员与 Prime Member 导读
区块链开发框架后端Apereo CAS 项目管理委员会(PMC)治理机制全解析:成员结构、投票规则与提名流程
Apereo CAS 项目管理委员会(PMC)治理机制全解析:成员结构、投票规则与提名流程 Apereo CAS 作为一个由志愿者驱动的大型开源单点登录(SSO
后端认证鉴权单点登录
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考