☰
Cosmos SDK 模块推荐目录结构:从 proto 定义到 x/{module_name} 的完整组织规范
2026/10/11 13:35:14 网站建设 项目流程
  • 区块链

【免费下载链接】cosmos-sdk

Framework for building performant, customizable blockchains with native interoperability

项目地址:https://gitcode.com/gh_mirrors/co/cosmos-sdk
点击查看免费下载

导读

在 Cosmos SDK 中,一个业务模块(Module)的代码组织方式直接决定了其可维护性、可测试性与可组合性。本文以官方文档《Recommended Folder Structure》为核心,系统讲解 Cosmos SDK 推荐的标准模块目录结构:包括proto/下的消息与服务定义组织,以及x/{module_name}下根目录、client/、exported/、keeper/、module/、simulation/各子目录的职责划分。读完本文,你将掌握如何规划一个新模块的文件布局、如何通过expected_keepers.go接口契约解耦模块间依赖,并了解每类文件的源码级职责与真实落地样例。

目录结构总览:官方推荐的标准布局

官方文档明确说明,以下结构是**建议性(suggestion)**而非强制规范,应用开发者被鼓励在此基础上改进并回馈社区。一个典型模块由两大块组成:协议缓冲区(Protocol Buffers)定义所在的proto目录,以及 Go 实现所在的x/{module_name}目录。

proto 目录:定义链上数据结构与服务接口

官方推荐将 proto 文件按如下路径组织:

proto └── {project_name} └── {module_name} └── {proto_version} ├── {module_name}.proto ├── event.proto ├── genesis.proto ├── query.proto └── tx.proto

各文件职责如下:

文件职责
{module_name}.proto模块的通用消息类型定义
event.proto与事件相关的消息类型定义
genesis.proto与创世状态(genesis state)相关的消息类型定义
query.proto模块的 Query 服务及相关的消息类型定义
tx.proto模块的 Msg 服务及相关的消息类型定义

以当前仓库为例,x/bank(银行模块)的 proto 定义位于 proto/cosmos/bank/v1beta1,包含bank.proto、event相关定义、genesis.proto、query.proto、tx.proto和authz.proto等文件;x/staking(质押模块)对应 proto/cosmos/staking/v1beta1,包含staking.proto、genesis.proto、query.proto、tx.proto、authz.proto。可见v1beta1即文档中的{proto_version}位置。此外,模块自身的配置(如 depinject 所需的 module config)还可放在 proto/cosmos/bank/module/v1/module.proto 这类module/{version}路径下。

这些*.proto文件经由 proto/buf.gen.gogo.yaml 与 proto/buf.gen.pulsar.yaml 中配置的代码生成工具(protocgen 脚本参见 scripts/protocgen.sh、scripts/protocgen-pulsar.sh),生成对应的*.pb.go文件,供 Go 代码使用。

x/{module_name} 目录:模块的 Go 实现

官方推荐的 Go 侧结构如下:

x/{module_name} ├── client │ ├── cli │ │ ├── query.go │ │ └── tx.go │ └── testutil │ ├── cli_test.go │ └── suite.go ├── exported │ └── exported.go ├── keeper │ ├── genesis.go │ ├── grpc_query.go │ ├── hooks.go │ ├── invariants.go │ ├── keeper.go │ ├── keys.go │ ├── msg_server.go │ └── querier.go ├── module │ └── module.go │ └── abci.go │ └── autocli.go ├── simulation │ ├── decoder.go │ ├── genesis.go │ ├── operations.go │ └── params.go ├── {module_name}.pb.go ├── codec.go ├── errors.go ├── events.go ├── events.pb.go ├── expected_keepers.go ├── genesis.go ├── genesis.pb.go ├── keys.go ├── msgs.go ├── params.go ├── query.pb.go ├── tx.pb.go └── README.md

需要说明的是:官方文档中的module/子目录在当前仓库的模块实现中一般直接并入根级module.go(如 x/bank/module.go、x/staking/module.go),BeginBlocker/EndBlocker逻辑也常直接放在 keeper 层(如 x/staking/keeper/abci.go)。这是社区在实际演进中对建议结构的合理适配,下文将按职责逐一讲解。

根目录文件:类型、编解码与模块契约

根目录承载模块最核心的类型定义与编解码逻辑,其中多数文件由 proto 生成,其余为手写契约代码。

由 Protocol Buffers 生成的文件(*.pb.go)

  • {module_name}.pb.go:模块通用消息类型的生成代码;
  • events.pb.go:事件消息类型;
  • genesis.pb.go:创世状态类型;
  • query.pb.go:Query 服务相关类型;
  • tx.pb.go:Msg 服务相关类型。

以x/bank为例,x/bank/types 下即包含bank.pb.go、genesis.pb.go、query.pb.go、tx.pb.go、authz.pb.go等生成文件。

手写的类型与工具文件

文件职责(依据官方文档并结合源码验证)
codec.go模块接口类型的注册方法。参见 x/bank/types/codec.go,其中将MsgSend、MsgMultiSend、SendAuthorization等注册进InterfaceRegistry,供 Amino/JSON 编解码使用
errors.go模块的哨兵错误(sentinel errors),如 x/bank/types/errors.go 中定义的ErrNoInputs、ErrSendDisabled等,统一通过errorsmod.Register注册到 ABCI 错误码空间
events.go模块的事件类型及构造函数,如 x/bank/types/events.go 中的NewCoinReceivedEvent、NewCoinSpentEvent等
expected_keepers.go模块的期望 keeper 接口契约(详见下文"解耦设计"小节)
genesis.go创世状态的默认值、校验与转换方法,如 x/bank/types/genesis.go 中的DefaultGenesisState()与Validate()
keys.go模块 store key 及相关辅助函数,如 x/bank/types/keys.go 定义了StoreKey = "bank"以及余额(BalancesPrefix)、总供给(SupplyKey)、denom 元数据、SendEnabled 等 store 前缀常量
msgs.go模块消息类型定义及其方法(ValidateBasic、GetSigners等),如 x/bank/types/msgs.go 中的MsgSend、MsgMultiSend
params.go模块参数类型定义及关联方法,如 x/bank/types/params.go 中的Params与SendEnabled

此外,x/staking还展示了两个典型补充:exported.go(见下节)与hooks.go(x/staking/types/hooks.go),后者定义了质押模块对外广播的 hook 接口,供其他模块订阅"委托/解委托/验证人变更"等事件。

expected_keepers.go 与 exported/:模块间解耦的两板斧

这是官方文档着重强调的设计模式,也是 Cosmos SDK 模块化思想的精髓所在。

为什么需要接口契约

如果模块 A 要使用模块 B 的 Keeper,直接 import 模块 B 会造成强耦合,甚至引发 import cycle。官方推荐的方案是:模块 A 在expected_keepers.go中声明一个最小的接口,只列出自己需要的方法,并在运行时接收模块 B 的 keeper 实例。

以x/bank为例,x/bank/types/expected_keepers.go 声明了AccountKeeper接口——它只暴露银行模块真正需要的账户方法(GetAccount、SetAccount、NewAccount、模块账户相关方法、AddressCodec()等),而不要求完整的 x/auth Keeper。x/bank/keeper/keeper.go中的BaseKeeper结构体正是持有ak types.AccountKeeper这一接口字段来访问账户信息。

exported/:提供规范化的跨模块类型

文档指出:接口契约中的方法可能会操作或返回"由实现该 keeper 的模块特有"的类型,此时就需要exported/出场。exported/中定义的类型使用规范化(canonical)类型,使模块可以通过expected_keepers.go的接口契约接收 keeper,同时保持代码 DRY(Don't Repeat Yourself)并避免 import cycle 混乱。

实际案例:

  • x/bank/exported/exported.go 定义了GenesisBalance接口(GetAddress()/GetCoins()),供其他模块以通用方式读取创世余额;
  • 当前仓库中x/staking的规范化类型位于 x/staking/types/exported.go,定义了DelegationI与ValidatorI接口——后者列出了验证人的规范化访问方法(GetMoniker()、IsBonded()、GetConsensusPower(math.Int) int64、TokensFromShares(...)等)。该文件在官方结构图中对应x/{module_name}/exported/exported.go,从源码结构看,将exported.go置于独立的exported/包内(而非模块types包内)正是为了减少依赖面、避免引入math之外的重型依赖。

keeper/ 子目录:状态读写与消息处理的核心

keeper/目录承载模块的Keeper与MsgServer实现,是模块状态机的核心。官方推荐的文件分工如下:

文件职责
keeper.goKeeper结构体与构造函数、核心状态读写方法
msg_server.goMsgServer实现,处理各Msg消息
grpc_query.gogRPC Query 服务实现
genesis.go创世状态的初始化(InitGenesis)与导出(ExportGenesis)
hooks.go模块 hook 的调用与分发逻辑
invariants.go不变量检查(invariants),用于链上一致性断言
keys.gostore key 与集合(collections)对象定义
querier.go早期风格的 querier(现代模块通常由 grpc_query.go 取代)

Keeper 接口与实现分离

以 x/bank/keeper/keeper.go 为例:文件顶部先声明Keeper接口(组合SendKeeper、创世方法、供给查询、denom 元数据、模块间转账SendCoinsFromModuleToAccount/MintCoins/BurnCoins/DelegateCoins/UndelegateCoins、虚拟账户相关方法以及types.QueryServer),再由BaseKeeper具体实现,并通过var _ Keeper = (*BaseKeeper)(nil)做编译期断言。这种"接口 + 实现"的写法让模块可插拔、可 mock。

MsgServer:消息的入口校验与执行

x/bank/keeper/msg_server.go 展示了MsgSend的处理链路:先通过AddressCodec解析 from/to 地址,校验Amount.IsValid()与IsAllPositive(),再检查IsSendEnabledCoins与BlockedAddr(to)(禁止向黑名单地址转账),最后调用SendCoins完成转账并埋点 telemetry。它通过NewMsgServerImpl(keeper)构造,并实现types.MsgServer接口。

gRPC Query 服务

x/staking/keeper/grpc_query.go 展示了查询服务的实现模式:以Querier结构体组合*Keeper,实现types.QueryServer,并通过runtime.KVStoreAdapter(k.storeService.OpenKVStore(ctx))访问状态。x/bank的对应实现位于 x/bank/keeper/grpc_query.go,其中大量使用collections包进行分页查询(如GetPaginatedTotalSupply)。

BeginBlocker 与 EndBlocker

官方文档指出module/abci.go用于定义BeginBlocker/EndBlocker(仅在确实需要时编写)。当前仓库中 x/staking/keeper/abci.go 即包含BeginBlocker(持久化历史 header 与验证人集合、按HistoricalEntries参数修剪旧条目)与EndBlocker(调用BlockValidatorUpdates更新验证人集合)的实现;x/bank/module.go中也通过实现appmodule.HasEndBlocker接口注册了EndBlock。

module/(module.go):AppModule 与 AppModuleBasic

模块的AppModule与AppModuleBasic是模块接入应用(App)的门面。以 x/bank/module.go 为例:

  • AppModuleBasic负责静态能力:模块名(Name())、Legacy Amino 编解码注册、默认创世状态(DefaultGenesis)、创世校验(ValidateGenesis)、gRPC Gateway 路由、CLI 命令(GetTxCmd)、接口注册(RegisterInterfaces);
  • AppModule组合AppModuleBasic并实现appmodule.AppModule、module.HasGenesis、module.HasServices等接口,在RegisterServices中注册MsgServer与QueryServer(调用types.RegisterMsgServer/types.RegisterQueryServer);
  • 文件头部还定义了ConsensusVersion = 4,用于模块升级时的共识版本迁移判定。

autocli.go:从 proto 自动生成 CLI

官方文档中的module/autocli.go承载模块的 autocli 选项。autocli 可以根据 proto 定义自动生成查询与交易命令,模块只需声明少量定制。以 x/bank/autocli.go 为例,其通过AutoCLIOptions()返回autocliv1.ModuleOptions,将Balance、AllBalances、SpendableBalances、TotalSupply、SupplyOf、Params等 RPC 映射为simd query bank balance [address] [denom]这类具体命令(Use、Short、PositionalArgs字段)。自定义 CLI 的完整实现则可参考 x/bank/client/cli/tx.go,其中NewSendTxCmd生成simd tx bank send [from] [to] [amount],NewMultiSendTxCmd生成simd tx bank multi-send ...,并支持--split标志将金额平均分发给多个地址。

client/ 子目录:CLI 命令与测试套件

  • client/cli/query.go:模块查询命令(如余额、供给、参数查询);
  • client/cli/tx.go:模块交易命令(如转账);
  • client/testutil/:CLI 测试套件,通常包含cli_test.go与suite.go。

当前仓库x/bank/client/cli/下为tx.go与tx_test.go,x/staking/client/cli/下则同时包含查询与交易命令。这些 CLI 命令由AppModuleBasic.GetTxCmd()汇总到simd二进制中。从 x/bank/client/cli/tx.go 的源码看,命令遵循统一模式:用client.GetClientTxContext(cmd)获取客户端上下文、解析参数、构造Msg,最后调用tx.GenerateOrBroadcastTxCLI完成签名与广播;cobra.MinimumNArgs/cobra.ExactArgs保证参数个数校验。

simulation/ 子目录:区块链模拟器支持

simulation/包为区块链模拟器(simapp)提供确定性仿真所需的函数。官方推荐结构包含:

  • decoder.go:解码模拟中的操作数据;
  • genesis.go:生成随机的创世状态;
  • operations.go:定义模拟操作(如随机转账、随机质押);
  • params.go:模拟参数。

当前仓库的x/bank/simulation/下有genesis.go、msg_factory.go、operations.go、proposals.go及对应测试文件;simapp通过AppModuleSimulation接口集成这些函数(参见 x/bank/module.go 中的_ module.AppModuleSimulation = AppModule{}断言)。模拟器的详细设计参见 14-simulator.md。

根目录 README.md:模块规格说明书

每个模块根目录应有README.md,作为模块规格文档,概述重要概念、状态存储结构以及消息与事件类型定义(如 x/bank/README.md、x/staking/README.md)。如何编写模块规格的详细指导见 docs/spec/SPEC_MODULE.md。

与 Keeper 设计文档的衔接

目录结构中大量文件(keeper.go、expected_keepers.go、keys.go、genesis.go等)都与"Keeper 设计"一脉相承:expected_keepers.go中的接口契约正是 06-keeper.md 的 Type Definition 小节所强调的依赖注入方式。建议在动手搭建模块目录前,先通读 00-intro.md 至 16-testing.md 的模块开发系列文档,形成完整的开发认知。

设计要点总结

  1. 主次分明:proto 目录只做类型与服务定义,Go 代码负责实现,两者通过*.pb.go生成代码衔接。
  2. 依赖反转:通过expected_keepers.go的最小接口 +exported/的规范化类型,实现模块间"面向接口编程",避免直接依赖与 import cycle。
  3. 职责单一:keeper/(状态与消息处理)、client/(CLI)、module/(接入 App)、simulation/(仿真)各司其职,测试文件与实现文件就近放置。
  4. 文件即文档:根目录README.md是模块规格的入口,errors.go、events.go、keys.go等命名本身即可传达模块的结构语义。
  5. 规范是建议而非教条:官方结构是社区最佳实践的沉淀,实际模块(如x/staking将 exported 类型放入types/exported.go、x/bank将 abci 逻辑放入 keeper)可以在保持职责清晰的前提下灵活调整。
  • 区块链

【免费下载链接】cosmos-sdk

Framework for building performant, customizable blockchains with native interoperability

项目地址:https://gitcode.com/gh_mirrors/co/cosmos-sdk
点击查看免费下载

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

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

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

立即咨询