☰
Substrate Collective Pallet 深度解析:理事会治理中的多成员投票、提案动议与 Prime 默认投票机制
2026/9/27 8:45:36 网站建设 项目流程
  • 区块链
  • 开发框架
  • 后端

【免费下载链接】substrate

Substrate: The platform for blockchain innovators

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

本文围绕 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 明确指出,集体成员有两种提供方式:

  1. 直接方式:调用 Root 可调用的set_members函数;
  2. 间接方式:由其他 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, ) -> DispatchResultWithPostInfo
  • new_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)分三种情况:

  1. 提前通过:yes_votes >= threshold时立即关闭,校验proposal_weight_bound与length_bound后执行提案,Pays::Yes;
  2. 提前否决:seats - no_votes < threshold(即使剩余所有成员都投赞成也无法达标)时立即关闭并移除动议,Pays::No(豁免费用);
  3. 投票期结束后的结算: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事件。

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 五块核心存储

存储项类型说明
ProposalsStorageValue<BoundedVec<Hash, MaxProposals>>活动动议哈希列表,受MaxProposals约束
ProposalOfStorageMap<Hash, Proposal>哈希 → 提案调用
VotingStorageMap<Hash, Votes>哈希 → 投票状态(threshold / ayes / nays / end)
ProposalCountStorageValue<u32>动议计数器(只增不减)
MembersStorageValue<Vec<AccountId>>当前成员(按值排序存储)
PrimeStorageValue<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

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

相关推荐

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

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

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

立即咨询