Cosmos SDK Protocol Buffers 工具链实战指南:Buf 工作区、代码生成与 CI 集成

原创2026-10-11 14:34:24602 阅读
文章标签:区块链

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 / Msg service 定义 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

脚本内部按以下步骤执行:

  1. 格式化:find ./ -name "*.proto" -exec clang-format -i {} \; 对所有 .proto 文件统一格式;
  2. 逐目录生成 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 工具链是开箱即用的模板,迁移到自有应用只需三步:

  1. 复制配置骨架:将 buf.work.yaml(或直接指向你的 proto 目录)、buf.yaml、buf.gen.gogo.yaml、buf.gen.pulsar.yaml、buf.gen.swagger.yaml 按需引入;若需要 gRPC 网关与 Swagger,再引入 grpc-gateway 与 openapiv2 插件段;
  2. 声明依赖:在 buf.yaml 的 deps 中加入 buf.build/cosmos/cosmos-sdk,运行 buf mod update 生成自己的 buf.lock;
  3. 复用 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)守护兼容性,以及一整套可复用的故障排查与迁移方案。

登录后查看全文
cosmos-sdk