变更记录

  • 2020 年 4 月 27 日:初始草案
  • 2020 年 8 月 5 日:更新指南

状态

已接受

背景

Protocol Buffers 提供了一份基础风格指南, 而 Buf 在此之上进行了扩展。我们希望在尽可能大的范围内遵循业界已被接受的指南和经验, 仅在我们的使用场景有明确理由时才偏离这些做法。

采用 Any

将 google.protobuf.Any 作为接口类型编码的推荐方式(相对于 oneof)之后, 包命名就成为编码中的核心部分,因为完全限定消息名现在会出现在编码后的 消息中。

当前目录组织

到目前为止,我们大体遵循了 Buf 的 DEFAULT 建议,只有一个小的偏离:禁用了 PACKAGE_DIRECTORY_MATCH。 虽然这对代码开发来说比较方便,但 Buf 也对此给出了警告:
如果你不这样做,那么在多种语言的许多 Protobuf 插件中,你会遇到非常糟糕的体验

采用 gRPC 查询

在 ADR 021 中,gRPC 被采纳为 Protobuf 原生查询方案。因此,完整的 gRPC 服务路径成为 ABCI 查询路径的关键组成部分。 未来,诸如 CosmWasm 之类的技术可能允许在持久化脚本内部发起 gRPC 查询,而这些查询路由会存储在 脚本二进制文件中。

决策

本 ADR 的目标是提供一套经过审慎考虑的命名约定,以便:
  • 鼓励在用户直接与 .proto 文件和 protobuf 完全限定名交互时获得良好的使用体验
  • 在简洁性与两种风险之间取得平衡:一种是过度优化(让名称过短且晦涩),另一种是优化不足(只是接受臃肿且包含大量冗余信息的名称)
这些指南旨在作为 Cosmos SDK 和第三方模块的风格指南。 作为起点,我们应当采用 Buf 的 DEFAULT 检查器集合中的全部检查器,包括 PACKAGE_DIRECTORY_MATCH, 但以下两项除外: 此外,还应遵循下文描述的进一步指南。

原则

简洁且有描述性的名称

名称应当足够有描述性,以表达其含义并与其他名称区分开来。 鉴于我们会在 google.protobuf.Any 以及 gRPC 查询路由中使用完全限定名, 我们应当尽量保持名称简洁,但也不要走得太远。一个通用经验法则是: 如果更短的名称能表达更多或同样的信息,就选择更短的名称。 例如,cosmos.bank.MsgSend(19 字节)传达的信息与 cosmos_sdk.x.bank.v1.MsgSend(28 字节)大致相同,但前者更简洁。 这种简洁性既让名称更易于使用,也能减少交易和网络传输中的空间占用。 我们也应当抵制过度优化的诱惑,不要通过缩写把名称压缩得过于晦涩。 例如,我们不应仅仅为了节省几个字节,就把 cosmos.bank.MsgSend 缩减成 csm.bk.MSnd。 目标是让名称简洁但不晦涩。

名称首先是给客户端使用的

包名和类型名应当为了用户的利益而选择, 而不一定是出于与 Go 代码库相关的历史包袱考虑。

为长期使用做规划

从长期支持的角度出发,我们应当预期自己所选的名称会被长期使用, 因此现在正是为未来做出最佳选择的机会。

版本管理

关于稳定包版本的指南

总体而言,schema 演进是更新 protobuf schema 的方式。这意味着新的字段、 消息和 RPC 方法会被添加到现有 schema 中,而旧字段、旧消息和旧 RPC 方法 则会尽可能长期保留。 在区块链场景中,破坏兼容性往往是不可接受的。例如,不可变智能合约 可能依赖宿主链上的某些数据 schema。如果宿主链破坏了这些 schema, 智能合约可能会遭到无法修复的破坏。即使问题可以修复(例如在客户端软件中), 通常也会付出很高的代价。 与其破坏兼容性,我们应当尽一切努力去演进 schema,而不是简单地破坏它们。 应在所有稳定包(非 alpha 或 beta)上使用 Buf 破坏性变更检测,以防止此类破坏发生。 考虑到这一点,一个包的不同稳定版本(例如 v1 或 v2)在很大程度上应被视为 不同的包,因此这应当是升级 protobuf schema 的最后手段。在以下场景中,创建 v2 可能是合理的:
  • 我们想创建一个与现有模块功能相似的新模块,而使用 v2 是最自然的方式。 在这种情况下,本质上只是存在两个不同但相似、API 不同的模块。
  • 我们想为现有模块增加一个全新改造过的 API,而将其加入现有包过于繁琐, 因此放入 v2 对用户来说更清晰。在这种情况下,如果 v1 仍被不可变智能合约积极使用, 就应谨慎避免弃用对 v1 的支持。

关于不稳定(alpha 和 beta)包版本的指南

建议使用以下准则来标记包为 alpha 或 beta:
  • 将某项内容标记为 alpha 或 beta 应当是最后手段,优先应当是直接放入稳定包中(即 v1 或 v2)
  • 当且仅当存在积极讨论,计划在近期移除该包或对其做重大修改时,包应当标记为 alpha
  • 当且仅当存在积极讨论,计划在近期对其功能进行重大重构或重做、但不是移除时,包应当标记为 beta
  • 模块可以且应当同时在稳定包(即 v1 或 v2)和不稳定包(alpha 或 beta)中拥有类型定义。
不应使用 alpha 和 beta 来逃避维护兼容性的责任。 代码一旦发布到实际环境中,尤其是在区块链上,修改它的代价就会很高。在某些情况下, 例如不可变智能合约,破坏性变更甚至可能根本无法修复。 当将某项内容标记为 alpha 或 beta 时,维护者应当问自己以下问题:
  • 要求其他人修改他们的代码,其代价是什么?而我们保留未来修改它的选择权,又能带来什么收益?
  • 将其推进到 v1 的计划是什么,这会如何影响用户?
alpha 或 beta 真正应该用来传达的是“已计划进行变更”。 作为一个案例,gRPC reflection 位于 grpc.reflection.v1alpha 包中。它自 2017 年以来就没有变更,如今已被 gRPCurl 等其他广泛使用的软件采用。 有些人可能已经在生产服务中使用它,因此如果他们真的把包改成 grpc.reflection.v1, 某些软件就会因此中断,而他们大概也不想这么做。因此,v1alpha 包现在在很大程度上已经成了 事实上的 v1。我们不要重蹈覆辙。 以下是处理非稳定包的指南:
  • 对非稳定包,应当使用 Buf 推荐的版本后缀 (例如 v1alpha1)
  • 非稳定包通常应从破坏性变更检测中排除
  • 不可变智能合约模块(即 CosmWasm)应当阻止智能合约或持久化脚本与 alpha/beta 包交互

省略 v1 后缀

与其使用 Buf 推荐的版本后缀, 对于实际上并不存在第二个版本的包,我们可以省略 v1。这使得像 cosmos.bank.Send 这样的常见用例可以拥有更简洁的名称。确实存在第二版或第三版的包,则可以通过 .v2 或 .v3 来表示。

包命名

采用简短且唯一的顶级包名

顶级包应采用一个简短的名称,并且已知不会与 Cosmos 生态系统中常见使用的其他名称发生冲突。 在不久的将来,应建立一个注册表,用于保留并索引 Cosmos 生态系统中使用的顶级包名。 由于 Cosmos SDK 的目标是为 Cosmos 项目提供顶级类型,因此建议在 Cosmos SDK 中使用 顶级包名 cosmos,而不是更长的 cosmos_sdk。 ICS 规范也可以考虑基于标准编号采用像 ics23 这样简短的顶级包名。

限制子包深度

增加子包层级深度时应当保持谨慎。通常,一个模块或一个库只需要一个子包。 虽然在源码中会使用 x 或 modules 来表示模块,但对于 .proto 文件来说, 这通常没有必要,因为子包最主要的用途本来就是表示模块。只有那些确定不会被频繁使用的内容, 才应当使用较深的子包层级。 对于 Cosmos SDK,建议我们直接写 cosmos.bank、 cosmos.gov 等,而不是 cosmos.x.bank。在实践中,大多数非模块类型 可以直接放进 cosmos 包中,或者在需要时引入 cosmos.base 包。请注意, 这种命名不会改变 Go 包名,也就是说,cosmos.bank protobuf 包仍然会位于 x/bank 中。

消息命名

消息类型名应当在不失去清晰度的前提下尽可能简洁。用于交易的 sdk.Msg 类型将保留 Msg 前缀,因为这能提供有用的上下文。

服务和 RPC 命名

ADR 021 规定模块应当 实现一个 gRPC 查询服务。我们应当将简洁性原则应用到查询服务和 RPC 名称上, 因为它们可能会被 CosmWasm 之类的持久化脚本模块调用。与此同时,用户也可能通过 gRPCurl 之类的工具使用这些查询路径。 例如,我们可以将 /cosmos_sdk.x.bank.v1.QueryService/QueryBalance 缩短为 /cosmos.bank.Query/Balance,而不会损失太多有用信息。 RPC 请求和响应类型应当遵循 ServiceNameMethodNameRequest/ ServiceNameMethodNameResponse 命名约定。也就是说,对于 Query 服务上的 一个名为 Balance 的 RPC 方法,请求和响应类型应分别为 QueryBalanceRequest 和 QueryBalanceResponse。这会比 BalanceRequest 和 BalanceResponse 更具自解释性。

查询服务仅使用 Query

与其采用 Buf 默认的服务后缀建议, 我们应当直接为查询服务使用更短的 Query。 对于其他类型的 gRPC 服务,我们应当考虑继续遵循 Buf 的默认建议。

从查询服务 RPC 名称中省略 Get 和 Query

由于 Get 和 Query 在完全限定名中是冗余的,因此应当从 Query 服务名称中省略。 例如,/cosmos.bank.Query/QueryBalance 只是把 Query 说了两遍, 却没有提供任何新信息。

未来改进

应当建立一个顶级包名注册表,用于协调整个生态系统中的命名、避免冲突, 并帮助开发者发现有用的 schema。一个简单的起点可以是一个采用社区治理的 git 仓库。

后果

正面

  • 名称会更简洁,更易于阅读和输入
  • 所有使用 Any 的交易都会更短(将移除 _sdk.x 和 .v1)
  • .proto 文件导入会更加标准(路径中不再包含 "third_party/proto")
  • 对客户端来说,代码生成会更容易,因为 .proto 文件将集中到单个 proto/ 目录中,而不是分散在整个 Cosmos SDK 中

负面

中性

  • 需要对 .proto 文件进行重新组织和重构
  • 某些模块可能需要标记为 alpha 或 beta

参考


Changelog

  • 2020 April 27: Initial Draft
  • 2020 August 5: Update guidelines

Status

Accepted

Context

Protocol Buffers provide a basic style guide and Buf builds upon that. To the extent possible, we want to follow industry accepted guidelines and wisdom for the effective usage of protobuf, deviating from those only when there is clear rationale for our use case.

Adoption of Any

The adoption of google.protobuf.Any as the recommended approach for encoding interface types (as opposed to oneof) makes package naming a central part of the encoding as fully-qualified message names now appear in encoded messages.

Current Directory Organization

Thus far we have mostly followed Buf’s DEFAULT recommendations, with the minor deviation of disabling PACKAGE_DIRECTORY_MATCH which although being convenient for developing code comes with the warning from Buf that:
you will have a very bad time with many Protobuf plugins across various languages if you do not do this

Adoption of gRPC Queries

In ADR 021, gRPC was adopted for Protobuf native queries. The full gRPC service path thus becomes a key part of ABCI query path. In the future, gRPC queries may be allowed from within persistent scripts by technologies such as CosmWasm and these query routes would be stored within script binaries.

Decision

The goal of this ADR is to provide thoughtful naming conventions that:
  • encourage a good user experience for when users interact directly with .proto files and fully-qualified protobuf names
  • balance conciseness against the possibility of either over-optimizing (making names too short and cryptic) or under-optimizing (just accepting bloated names with lots of redundant information)
These guidelines are meant to act as a style guide for both the Cosmos SDK and third-party modules. As a starting point, we should adopt all of the DEFAULT checkers in Buf’s including PACKAGE_DIRECTORY_MATCH, except: Further guidelines to be described below.

Principles

Concise and Descriptive Names

Names should be descriptive enough to convey their meaning and distinguish them from other names. Given that we are using fully-qualifed names within google.protobuf.Any as well as within gRPC query routes, we should aim to keep names concise, without going overboard. The general rule of thumb should be if a shorter name would convey more or else the same thing, pick the shorter name. For instance, cosmos.bank.MsgSend (19 bytes) conveys roughly the same information as cosmos_sdk.x.bank.v1.MsgSend (28 bytes) but is more concise. Such conciseness makes names both more pleasant to work with and take up less space within transactions and on the wire. We should also resist the temptation to over-optimize, by making names cryptically short with abbreviations. For instance, we shouldn’t try to reduce cosmos.bank.MsgSend to csm.bk.MSnd just to save a few bytes. The goal is to make names concise but not cryptic.

Names are for Clients First

Package and type names should be chosen for the benefit of users, not necessarily because of legacy concerns related to the go code-base.

Plan for Longevity

In the interests of long-term support, we should plan on the names we do choose to be in usage for a long time, so now is the opportunity to make the best choices for the future.

Versioning

Guidelines on Stable Package Versions

In general, schema evolution is the way to update protobuf schemas. That means that new fields, messages, and RPC methods are added to existing schemas and old fields, messages and RPC methods are maintained as long as possible. Breaking things is often unacceptable in a blockchain scenario. For instance, immutable smart contracts may depend on certain data schemas on the host chain. If the host chain breaks those schemas, the smart contract may be irreparably broken. Even when things can be fixed (for instance in client software), this often comes at a high cost. Instead of breaking things, we should make every effort to evolve schemas rather than just breaking them. Buf breaking change detection should be used on all stable (non-alpha or beta) packages to prevent such breakage. With that in mind, different stable versions (i.e. v1 or v2) of a package should more or less be considered different packages and this should be last resort approach for upgrading protobuf schemas. Scenarios where creating a v2 may make sense are:
  • we want to create a new module with similar functionality to an existing module and adding v2 is the most natural way to do this. In that case, there are really just two different, but similar modules with different APIs.
  • we want to add a new revamped API for an existing module and it’s just too cumbersome to add it to the existing package, so putting it in v2 is cleaner for users. In this case, care should be made to not deprecate support for v1 if it is actively used in immutable smart contracts.

Guidelines on unstable (alpha and beta) package versions

The following guidelines are recommended for marking packages as alpha or beta:
  • marking something as alpha or beta should be a last resort and just putting something in the stable package (i.e. v1 or v2) should be preferred
  • a package should be marked as alpha if and only if there are active discussions to remove or significantly alter the package in the near future
  • a package should be marked as beta if and only if there is an active discussion to significantly refactor/rework the functionality in the near future but not remove it
  • modules can and should have types in both stable (i.e. v1 or v2) and unstable (alpha or beta) packages.
alpha and beta should not be used to avoid responsibility for maintaining compatibility. Whenever code is released into the wild, especially on a blockchain, there is a high cost to changing things. In some cases, for instance with immutable smart contracts, a breaking change may be impossible to fix. When marking something as alpha or beta, maintainers should ask the questions:
  • what is the cost of asking others to change their code vs the benefit of us maintaining the optionality to change it?
  • what is the plan for moving this to v1 and how will that affect users?
alpha or beta should really be used to communicate “changes are planned”. As a case study, gRPC reflection is in the package grpc.reflection.v1alpha. It hasn’t been changed since 2017 and it is now used in other widely used software like gRPCurl. Some folks probably use it in production services and so if they actually went and changed the package to grpc.reflection.v1, some software would break and they probably don’t want to do that… So now the v1alpha package is more or less the de-facto v1. Let’s not do that. The following are guidelines for working with non-stable packages:
  • Buf’s recommended version suffix (ex. v1alpha1) should be used for non-stable packages
  • non-stable packages should generally be excluded from breaking change detection
  • immutable smart contract modules (i.e. CosmWasm) should block smart contracts/persistent scripts from interacting with alpha/beta packages

Omit v1 suffix

Instead of using Buf’s recommended version suffix, we can omit v1 for packages that don’t actually have a second version. This allows for more concise names for common use cases like cosmos.bank.Send. Packages that do have a second or third version can indicate that with .v2 or .v3.

Package Naming

Adopt a short, unique top-level package name

Top-level packages should adopt a short name that is known to not collide with other names in common usage within the Cosmos ecosystem. In the near future, a registry should be created to reserve and index top-level package names used within the Cosmos ecosystem. Because the Cosmos SDK is intended to provide the top-level types for the Cosmos project, the top-level package name cosmos is recommended for usage within the Cosmos SDK instead of the longer cosmos_sdk. ICS specifications could consider a short top-level package like ics23 based upon the standard number.

Limit sub-package depth

Sub-package depth should be increased with caution. Generally a single sub-package is needed for a module or a library. Even though x or modules is used in source code to denote modules, this is often unnecessary for .proto files as modules are the primary thing sub-packages are used for. Only items which are known to be used infrequently should have deep sub-package depths. For the Cosmos SDK, it is recommended that we simply write cosmos.bank, cosmos.gov, etc. rather than cosmos.x.bank. In practice, most non-module types can go straight in the cosmos package or we can introduce a cosmos.base package if needed. Note that this naming will not change go package names, i.e. the cosmos.bank protobuf package will still live in x/bank.

Message Naming

Message type names should be as concise possible without losing clarity. sdk.Msg types which are used in transactions will retain the Msg prefix as that provides helpful context.

Service and RPC Naming

ADR 021 specifies that modules should implement a gRPC query service. We should consider the principle of conciseness for query service and RPC names as these may be called from persistent script modules such as CosmWasm. Also, users may use these query paths from tools like gRPCurl. As an example, we can shorten /cosmos_sdk.x.bank.v1.QueryService/QueryBalance to /cosmos.bank.Query/Balance without losing much useful information. RPC request and response types should follow the ServiceNameMethodNameRequest/ ServiceNameMethodNameResponse naming convention. i.e. for an RPC method named Balance on the Query service, the request and response types would be QueryBalanceRequest and QueryBalanceResponse. This will be more self-explanatory than BalanceRequest and BalanceResponse.

Use just Query for the query service

Instead of Buf’s default service suffix recommendation, we should simply use the shorter Query for query services. For other types of gRPC services, we should consider sticking with Buf’s default recommendation.

Omit Get and Query from query service RPC names

Get and Query should be omitted from Query service names because they are redundant in the fully-qualified name. For instance, /cosmos.bank.Query/QueryBalance just says Query twice without any new information.

Future Improvements

A registry of top-level package names should be created to coordinate naming across the ecosystem, prevent collisions, and also help developers discover useful schemas. A simple starting point would be a git repository with community-based governance.

Consequences

Positive

  • names will be more concise and easier to read and type
  • all transactions using Any will be at shorter (_sdk.x and .v1 will be removed)
  • .proto file imports will be more standard (without "third_party/proto" in the path)
  • code generation will be easier for clients because .proto files will be in a single proto/ directory which can be copied rather than scattered throughout the Cosmos SDK

Negative

Neutral

  • .proto files will need to be reorganized and refactored
  • some modules may need to be marked as alpha or beta

References