Cosmos SDK Protocol Buffers 工具链实战指南:Buf 工作区、代码生成与 CI 集成
Cosmos SDK Protocol Buffers 工具链实战指南:Buf 工作区、代码生成与 CI 集成
导读
Protocol Buffers(protobuf)是 Cosmos SDK 描述状态机、交易消息、gRPC 查询与 REST 接口的统一序列化协议,几乎所有模块的 types、query、tx 定义都从 .proto 文件生成。本篇指南以 docs/docs/build/tooling/00-protobuf.md 为主体,结合当前仓库中的 Makefile、生成脚本与 buf 配置文件,完整讲解 Cosmos SDK 的 protobuf 工具链:从 Docker 化的 proto-builder 镜像、三套 buf.gen*.yaml 生成模板,到 CI 中的 lint / 破坏性变更检查与常见故障排查。读完你将能够在自己的 Cosmos SDK 应用中复刻这套生成、格式化、lint 与 Swagger 文档管线。
为什么 Cosmos SDK 重度依赖 Protocol Buffers
Cosmos SDK 的核心数据模型——账户、余额、治理提案、质押记录、交易消息——都通过 .proto 文件定义,再由生成器产出 Go 代码。proto/ 目录是这一切的源头:
- 消息类型(如
x/bank的MsgSend、Input、Output)定义状态与交易结构; Query/Msgservice 定义 gRPC 接口,配合google.api.http注解同时暴露 REST 端点;- 模块级扩展(如
cosmos.msg.v1.signer、cosmos.query.v1.module_query_safe)为 SDK 提供跨模块的能力标注。
正因如此,生成工具链的稳定性、可复现性与产物一致性直接决定了整个框架的工程质量。Cosmos SDK 的方案是:用 Docker 镜像锁定工具链版本,用 Buf 抽象 protoc 复杂度,用三套生成模板分别产出 gogo、pulsar 与 Swagger 三类产物。
用 Docker 镜像生成 proto:Makefile 中的命令体系
Cosmos SDK 将完整的 protobuf 工具链(protoc、各类插件、clang-format、swagger-combine)打包进一个 Docker 镜像,仓库内外均可直接复用。文档提到的镜像为 ghcr.io/cosmos/proto-builder;以当前仓库实际内容为准,Makefile 中锁定的是:
protoVer=0.18.1
protoImageName=ghcr.io/cosmos/proto-builder:$(protoVer)
protoImage=$(DOCKER) run --rm -e GOTOOLCHAIN=auto -v $(CURDIR):/workspace --workdir /workspace $(protoImageName)
proto-all: proto-format proto-lint proto-gen
三个核心 target 分别对应格式化、lint 与生成:
proto-gen:
@echo "Generating Protobuf files"
@$(protoImage) sh ./scripts/protocgen.sh 2>&1 | tee protocgen.log | \
awk '{print $$0} /contains the reserved field name/ && /tendermint/ {next} 1'
proto-swagger-gen:
@echo "Generating Protobuf Swagger"
@$(protoImage) sh ./scripts/protoc-swagger-gen.sh
proto-format:
@$(protoImage) find ./ -name "*.proto" -exec clang-format -i {} \;
proto-lint:
@$(protoImage) buf lint --error-format=json
proto-check-breaking:
@$(protoImage) buf breaking --against $(HTTPS_GIT)#branch=main
proto-update-deps:
# 从 CometBFT 拉取 tendermint 相关 .proto 后执行 buf mod update
$(DOCKER) run --rm -v $(CURDIR)/proto:/workspace --workdir /workspace $(protoImageName) buf mod update
关键点说明:
protoImage每次执行都会启动一个一次性容器,把当前目录挂载到/workspace并以该目录为工作目录(--rm保证容器用完即删),从而彻底隔离本机 protoc / 插件版本漂移;-e GOTOOLCHAIN=auto让镜像内的 Go 工具按go.mod自动选择工具链版本;proto-all一键串起「格式化 → lint → 生成」全流程,这也是 CI 与本地开发统一执行的入口;proto-update-deps负责从 CometBFT 固定版本(如v0.38.0)拉取tendermint/abci、tendermint/types、tendermint/crypto等.proto文件,再执行buf mod update刷新依赖锁。
镜像本身由 contrib/devtools/Dockerfile 构建:基于 bufbuild/buf:1.64 与 golang:1.26.6-alpine,内置 nodejs、npm、git、make、clang-extra-tools、g++、jq,全局安装 swagger-combine,并通过 go install 预装 protoc-gen-go-pulsar、protoc-gen-go、protoc-gen-grpc-gateway、protoc-gen-openapiv2、protoc-gen-go-grpc,最后克隆 gogoproto 仓库执行 make install 安装全部 gogo 插件。这套镜像由 .github/workflows/proto-docker.yml 在推送 contrib/devtools/Dockerfile 时自动构建并发布 amd64 / arm64 双架构镜像到 ghcr.io。
scripts/protocgen.sh:gogo 代码生成全流程拆解
生成入口脚本是 scripts/protocgen.sh,它展示了 Cosmos SDK 生成流程的完整逻辑。手动运行方式在脚本头部有注释:
# docker build --pull --rm -f "contrib/devtools/Dockerfile" -t cosmossdk-proto:latest "contrib/devtools"
# docker run --rm -v $(pwd):/workspace --workdir /workspace cosmossdk-proto sh ./scripts/protocgen.sh
脚本内部按以下步骤执行:
- 格式化:
find ./ -name "*.proto" -exec clang-format -i {} \;对所有.proto文件统一格式; - 逐目录生成 gogo 代码:遍历
proto/cosmos与proto/amino下的每个 proto 目录,对每个文件执行一条关键判断:
# 若 proto 文件已声明 go_package 指向 cosmossdk.io/api/...,
# 说明它是为 google.golang.org/protobuf 原生构建的,gogo 生成应跳过
if grep -q "option go_package" "$file" && grep -H -o -c 'option go_package.*cosmossdk.io/api' "$file" | grep -q ':0$'; then
buf generate --template buf.gen.gogo.yaml $file
fi
即:gogo 生成只针对非 pulsar 原生的 proto 文件,避免同一份定义被两套生成器重复产出造成冲突;
3. 生成测试用 proto:依次对 testutil/testdata、baseapp/testutil 执行 buf generate,并在 tests/integration/tx/internal 执行 make codegen;
4. 整理产物目录:把生成的 github.com/cosmos/cosmos-sdk/* 与 cosmossdk.io/** 复制到仓库正确位置后删除临时目录;其中 store/v2 是特例——生成路径为 store/v2 但源码实际位于 store/,脚本会将其内容搬移过去以匹配仓库布局;
5. 依赖整理:执行 go mod tidy 收尾;
6. 接力 pulsar 生成:最后调用 scripts/protocgen-pulsar.sh。
pulsar 脚本负责新版 google.golang.org/protobuf API 的产物:
set -eo pipefail
echo "Cleaning API directory"
(cd api; find ./ -type f \( -iname \*.pulsar.go -o -iname \*.pb.go -o -iname \*.cosmos_orm.go -o -iname \*.pb.gw.go \) -delete; find . -empty -type d -delete; cd ..)
echo "Generating API module"
(cd proto; buf generate --template buf.gen.pulsar.yaml)
echo "Generate Pulsar Test Data"
(cd testutil/testdata; buf generate --template buf.gen.pulsar.yaml)
echo "Generate x/tx"
(cd x/tx; make codegen)
它先清空 api/ 目录中所有历史生成物(.pulsar.go、.pb.go、.cosmos_orm.go、.pb.gw.go),再按 buf.gen.pulsar.yaml 全量重新生成,保证 api/ 目录永远是干净、可复现的产物。
Buf 工具链:从 workspace 到生成模板
Buf 是 protobuf 生态中的统一工具,它把复杂的 protoc 工具链(插件发现、include 路径、依赖管理、lint、breaking 检查)抽象为简单的 buf 命令。仓库中所有以 buf 开头的文件共同构成这套体系。
Workspace:buf.work.yaml
当项目存在一个或多个包含 protobuf 的目录时,用 Buf workspace 统一管理。仓库根目录的 buf.work.yaml 全文如下:
version: v1
directories:
- proto
它将 proto/ 声明为唯一的工作区目录,buf 会在该目录内解析 buf.yaml 与依赖。如果你的应用同时维护多个 proto 包目录(例如 SDK 本体 + 自研模块),只需在 directories 中追加即可。
proto/ 目录全景
proto/ 是全部 protobuf 定义的存放处,目录内包含 7 个 buf 相关文件与若干子目录:
├── README.md
├── buf.gen.gogo.yaml
├── buf.gen.pulsar.yaml
├── buf.gen.swagger.yaml
├── buf.lock
├── buf.md
├── buf.yaml
├── cosmos # 24 个 SDK 模块的 proto(app/auth/authz/bank/base/.../upgrade/vesting 等)
├── tendermint # CometBFT 相关 proto(abci/types/crypto/libs/p2p/version)
└── amino # amino 自定义扩展(amino.proto)
proto/README.md 还说明:该目录应定期通过 buf push 同步到 buf.build/cosmos/cosmos-sdk,buf.md 与各 protobuf 包内注释承载面向使用者的文档。如果你依赖 Cosmos SDK 的 proto 定义,直接声明依赖 buf.build/cosmos/cosmos-sdk 即可。
buf.gen.gogo.yaml:gogoproto 生成模板
proto/buf.gen.gogo.yaml 用于生成模块内使用的 gogo 代码:
version: v1
plugins:
- name: gocosmos
out: ..
opt: plugins=grpc,Mgoogle/protobuf/any.proto=github.com/cosmos/gogoproto/types/any
- name: grpc-gateway
out: ..
opt: logtostderr=true,allow_colon_final_segments=true
gocosmos是 gogoproto 生态的生成插件,相比官方protoc-gen-go,它为各类对象提供更便捷的辅助方法(如Equal、String),且文档明确其 encode/decode 步骤性能更高;opt: plugins=grpc,Mgoogle/protobuf/any.proto=...同时生成 gRPC 服务代码,并把google.protobuf.Any映射到 gogoproto 的types/any包,这是 SDK 支持接口多态(interface registry)的基础;grpc-gateway插件为带google.api.http注解的 service 生成 REST ↔ gRPC 网关代码。
生成产物以 out: .. 输出到 proto/ 的上级目录,即仓库根下的各模块,例如 x/bank/types/ 中的 bank.pb.go、query.pb.go、tx.pb.go 都是这套模板的产物。
buf.gen.pulsar.yaml:protobuf apiv2 生成模板
proto/buf.gen.pulsar.yaml 使用 Cosmos 自研的 cosmos-proto 中的 go-pulsar 插件,生成新版 google.golang.org/protobuf API(protobuf-apiv2)的代码:
version: v1
managed:
enabled: true
go_package_prefix:
default: cosmossdk.io/api
except:
- buf.build/googleapis/googleapis
- buf.build/cosmos/gogo-proto
- buf.build/cosmos/cosmos-proto
override:
plugins:
- name: go-pulsar
out: ../api
opt: paths=source_relative
- name: go-grpc
out: ../api
opt: paths=source_relative
managed模式由 buf 统一注入go_package,默认前缀为cosmossdk.io/api,因此所有 pulsar 产物集中输出到 api/ 目录,例如 api/cosmos/bank/v1beta1/ 下的query.pulsar.go、query_grpc.pb.go、tx.pulsar.go;except列表排除第三方依赖包(googleapis、gogo-proto、cosmos-proto),避免改写它们的包名;- 文档指出:
go-pulsar之所以取代官方生成器,是因为它为 Cosmos SDK 应用提供了额外辅助,且 encode/decode 性能优于官方protoc-gen-go; - 产物路径
paths=source_relative保证目录结构与 proto 定义一一对应。
值得注意的是,同一份 proto 定义会同时产出两套代码:gogo 版本给模块内状态机与 Amino 兼容层使用(x/bank/types),pulsar 版本给 api/ 下的通用 API 客户端与新版编解码使用(api/cosmos/bank/v1beta1)。这就是 protocgen.sh 中 go_package 判断存在的意义——部分 proto(如 cosmos/msg/v1、cosmos/query/v1)天生只生成 pulsar 版本。
buf.gen.swagger.yaml:REST 文档生成模板
proto/buf.gen.swagger.yaml 负责生成链的 REST API Swagger 文档:
version: v1
plugins:
- name: openapiv2
out: ../tmp-swagger-gen
opt: logtostderr=true,fqn_for_openapi_name=true,simple_operation_ids=true
它只覆盖 Query / Msg service 中通过 google.api.http 显式声明了 REST 路由的端点。以 proto/cosmos/bank/v1beta1/query.proto 为例:
service Query {
rpc Balance(QueryBalanceRequest) returns (QueryBalanceResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/cosmos/bank/v1beta1/balances/{address}/by_denom";
}
rpc AllBalances(QueryAllBalancesRequest) returns (QueryAllBalancesResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/cosmos/bank/v1beta1/balances/{address}";
}
...
}
生成流程由 scripts/protoc-swagger-gen.sh 驱动:遍历 proto/cosmos 下各目录,仅对存在 query.proto 或 service.proto 的目录执行 buf generate --template buf.gen.swagger.yaml;随后用 Node 包 swagger-combine 依据 client/docs/config.json 将所有分片合并为 client/docs/swagger-ui/swagger.yaml,最后清理 tmp-swagger-gen 临时目录。
buf.lock:自动生成的依赖锁
proto/buf.lock 由 buf mod update 自动生成,锁定 buf.yaml 中声明的四个依赖及其精确 commit 与 digest:
version: v1
deps:
- remote: buf.build
owner: cosmos
repository: cosmos-proto
commit: 04467658e59e44bbb22fe568206e1f70
digest: shake256:73a640bd60e0c523b0f8237ff34eab67c45a38b64bbbde1d80224819d272dbf316...
- remote: buf.build
owner: cosmos
repository: gogo-proto
commit: 88ef6483f90f478fb938c37dde52ece3
...
- remote: buf.build
owner: googleapis
repository: googleapis
...
- remote: buf.build
owner: protocolbuffers
repository: wellknowntypes
...
文档特别提醒:不需要复制当前仓库的 lock 文件。若你的项目依赖 Cosmos SDK 的 proto 定义,应在自己的 buf.yaml 中声明依赖 buf.build/cosmos/cosmos-sdk,再自行运行 buf mod update 生成属于你的 lock。
buf.yaml:包名、lint 与破坏性检查配置
proto/buf.yaml 是 proto 模块的根配置,逐段含义如下:
# This module represents buf.build/cosmos/cosmos-sdk
version: v1
name: buf.build/cosmos/cosmos-sdk
deps:
- buf.build/cosmos/cosmos-proto
- buf.build/cosmos/gogo-proto
- buf.build/googleapis/googleapis
- buf.build/protocolbuffers/wellknowntypes
breaking:
use:
- FILE
ignore:
- testpb
lint:
use:
- STANDARD
- COMMENTS
- FILE_LOWER_SNAKE_CASE
except:
- UNARY_RPC
- COMMENT_FIELD
- SERVICE_SUFFIX
- PACKAGE_VERSION_SUFFIX
- RPC_REQUEST_STANDARD_NAME
ignore:
- tendermint
name声明该模块在 Buf Schema Registry 中的包名,是外部依赖buf.build/cosmos/cosmos-sdk解析的锚点;deps声明四个上游依赖:cosmos-proto(SDK 扩展)、gogo-proto(gogo 类型)、googleapis(google.api.http注解)、wellknowntypes(google.protobuf.*常用类型);breaking使用FILE级别检查器检测破坏性变更,ignore跳过testpb;lint启用STANDARD、COMMENTS、FILE_LOWER_SNAKE_CASE三组规则,并通过except豁免五项 SDK 特有的约定(如允许不带-service后缀的 service 命名、允许包版本后缀缺失等),ignore跳过 tendermint 目录(其定义源自 CometBFT 上游,不受 SDK lint 约束)。
CI 集成:lint 与破坏性检查自动门禁
仓库通过 .github/workflows/proto.yml 在 CI 中强制执行 protobuf 质量门禁,该工作流仅在 proto 文件变更时触发:
detect-proto-changes:用 diff action 检测proto/**(SDK 本体)与enterprise/poa/proto/**(POA 模块)是否有改动,并解析出目标分支;lint:使用bufbuild/buf-lint-action对proto输入执行buf lint;break-check:使用bufbuild/buf-breaking-action将proto与目标分支的HEAD~1进行比较,跑FILE级别的破坏性检查,防止任何字段删除/类型变更悄悄进入主干;- 针对 POA 模块的
lint-poa与break-check-poa同理。
此外还有两个相关工作流:
- .github/workflows/proto-docker.yml:在
contrib/devtools/Dockerfile变更或手动触发时,通过 buildx 构建并推送 amd64 / arm64 双架构的ghcr.io/cosmos/proto-builder镜像; - .github/workflows/proto-registry.yml:当前处于注释状态的
buf push工作流,设计上在打v*标签时把proto推送到buf.build/cosmos/cosmos-sdk,以便外部项目通过 BSR 消费 SDK 的 proto 定义。
SDK 自定义 proto 扩展注解
除了标准 proto3 语法,Cosmos SDK 的 .proto 文件广泛使用自研扩展,理解它们有助于阅读 proto 定义:
cosmos.query.v1.module_query_safe:定义于 proto/cosmos/query/v1/query.proto,标注该查询「可在状态机内安全调用」(如被其他模块 Keeper 或 CosmWasm 合约调用),要求查询确定性强、gas 计量正确;amino扩展:定义于 proto/amino/amino.proto,提供name(注册到 Amino 类型注册表的字符串,最多 39 字符以兼容 Ledger 硬件)与message_encoding/ 字段级encoding(描述 Amino 编码格式);cosmos.msg.v1.signer与cosmos_proto.scalar:在 proto/cosmos/bank/v1beta1/bank.proto 中可见,前者声明消息签名者字段,后者把字符串字段标注为地址标量(如cosmos.AddressString),供编解码与校验层识别。
这些扩展正是 buf.yaml 依赖 buf.build/cosmos/cosmos-proto 与 buf.build/cosmos/gogo-proto 的原因——生成器需要这些依赖中的 descriptor 定义才能编译你的 proto 文件。
故障排查:proto 生成常见问题
文档末尾给出了生成阶段的高频问题与官方建议,整理如下:
protoc: command not found 或插件未找到
优先使用文档推荐的 Docker 流程,避免本地工具链漂移:
make proto-image && make proto-gen
这会把兼容版本的 protoc 与插件锁定在镜像内。若确实使用本地工具链,请确保 $PATH 包含 $(go env GOPATH)/bin(go install 安装的插件默认落在该目录)。
Buf import 报错(如 google/api/annotations.proto not found)
检查项目是否包含必需的 buf.yaml 与 buf.gen*.yaml,确认 deps 声明齐全且 buf.lock 已生成。在 monorepo 中,务必把 third_party/proto(或等价目录)纳入 buf 配置并指向它们。
Shell / CI 差异
不同环境对 shell 选项(如 set -o pipefail)的处理不一致,导致生成脚本行为不同。统一通过文档化的 Docker 镜像执行生成,可规避这类差异。
版本锁定
始终把 proto-builder 镜像钉在推荐 tag 上(如 ghcr.io/cosmos/proto-builder:<version>,当前仓库锁定 0.18.1,见 Makefile),防止插件/版本不匹配。遇到不确定的情况,重建镜像并重新生成:
make clean proto-image proto-gen
在自己的应用中复用这套管线
Cosmos SDK 的 protobuf 工具链是开箱即用的模板,迁移到自有应用只需三步:
- 复制配置骨架:将
buf.work.yaml(或直接指向你的 proto 目录)、buf.yaml、buf.gen.gogo.yaml、buf.gen.pulsar.yaml、buf.gen.swagger.yaml按需引入;若需要 gRPC 网关与 Swagger,再引入grpc-gateway与openapiv2插件段; - 声明依赖:在
buf.yaml的deps中加入buf.build/cosmos/cosmos-sdk,运行buf mod update生成自己的buf.lock; - 复用 Makefile 模式:照搬 Makefile 的
protoImageName/protoImage定义与proto-all、proto-gen、proto-lint等 target,同时保留protoImage的挂载与GOTOOLCHAIN=auto设置,即可获得与 Cosmos SDK 完全一致的格式化、lint、生成体验。
至此,你已掌握 Cosmos SDK protobuf 工具链的完整脉络:Docker 镜像锁定环境、三套 buf 模板产出三路产物、CI 双门禁(lint + breaking)守护兼容性,以及一整套可复用的故障排查与迁移方案。