变更记录
- 2022-08-18 初稿
- 2022-12-08 初稿
- 2023-01-24 更新
状态
已接受,部分实现摘要
这里提出了一套新的核心 API,作为开发 cosmos-sdk 应用的一种方式,最终将以一组核心服务和扩展接口取代现有的AppModule 和 sdk.Context 框架。该核心 API 的目标是:
- 更简单
- 可扩展性更强
- 比当前框架更稳定
- 支持确定性事件与查询
- 支持事件监听器
- 支持 ADR 033:基于 Protobuf 的模块间通信 客户端。
背景
历史上,模块通过AppModule 和 AppModuleBasic
接口向框架暴露其功能,但这些接口存在以下不足:
AppModule和AppModuleBasic都需要定义并注册,这不符合直觉- 应用需要实现完整接口,即使其中某些部分并不需要(尽管对此有一些变通方式)
- 接口方法严重依赖不稳定的第三方依赖,尤其是 Comet
- 这些接口中长期混杂了许多遗留的必需方法
- 从应用中获取 store key
- 调用
sdk.Context上的方法,而它或多或少包含了模块可用的全部能力集合。
sdk.Context 中,模块可用的功能集合就与该类型紧密耦合。如果上游依赖(例如 Comet)发生变化,
或者需要引入新功能(例如替代的 store 类型),这些变更就必须影响 sdk.Context 及其所有使用方(基本上就是所有模块)。此外,所有模块现在都接收 context.Context,并且需要通过一种不够易用的解包函数将其转换为 sdk.Context。
这些接口上的任何破坏性变更,例如由 Comet 这类第三方依赖施加的变更,都会带来一个副作用:迫使生态中的所有模块同步更新。这意味着几乎不可能存在一个模块版本既能在 2 或 3 个不同版本的 SDK 上运行,也能兼容另一个模块的 2 或 3 个不同版本。这种同步耦合拖慢了整个生态系统的开发速度,也使组件更新相比更稳定、松耦合的情况下被延迟得更久。
决策
core API 提出了一组核心 API,供模块依赖以与状态机交互并向其暴露自身功能。这些 API 经过有原则的设计,以实现:
- 尽量减少或消除依赖与无关功能之间的紧耦合
- API 能够提供长期稳定性保证
- SDK 框架可以用安全且直接的方式扩展
- 模块希望与状态机交互的所有对象都是服务
- 所有服务都通过
context.Context协调状态,而不是试图重建sdk.Context的“变量袋”式做法 - 所有独立服务都隔离在独立包中,只暴露最小 API 并保持最小依赖
- 核心 API 应当保持极简,并面向长期支持(LTS)设计
- 一个“运行时”模块将实现核心 API 定义的所有“核心服务”,并能处理由核心扩展接口暴露的所有模块功能
- 其他非核心和/或非 LTS 服务也可以由特定版本的运行时模块或其他模块提供,只要遵循相同的设计原则;这包括与特定非稳定版本第三方依赖(如 Comet)交互的功能
- 核心 API 本身不实现任何功能,它只定义类型
- 遵循 Go 稳定 API 兼容性指南:链接
BaseApp 和 ModuleManager 负责。实现核心 API 的运行时模块被有意与核心 API 分离,以便支持比 SDK 当前紧耦合的 BaseApp 设计更多的运行时模块并行版本和分叉,同时仍然保持高度的可组合性和兼容性。
仅基于核心 API 构建的模块,无需了解运行时、BaseApp 或 Comet 的具体版本即可实现兼容。按照这种模式,核心主线 SDK 中的模块可以很容易地与运行时的分叉版本进行组合。
这一设计旨在支持兼容依赖版本的矩阵。理想情况下,任意模块的某个版本都应兼容多个版本的运行时模块以及其他兼容模块。这将允许基于实战验证结果有选择地更新依赖。更保守的项目可能希望比节奏更快的项目更慢地更新某些依赖。
核心服务
核心 API 定义了以下“核心服务”。所有有效的运行时模块实现都应通过依赖注入和手动装配两种方式,向模块提供这些服务的实现。 下面描述的各个独立服务,也都会被统一打包进一个便捷的appmodule.Service
“捆绑服务”中,这样模块只需声明对单个服务的依赖即可简化使用。
Store 服务
Store 服务将定义在cosmossdk.io/core/store 包中。
通用的 store.KVStore 接口与当前 SDK 的 KVStore 接口相同。store key 已被重构进 store 服务中:不再要求 context 知道有哪些 store,而是反转这种模式,允许从通用 context 中获取 store。针对当前支持的三类 store,分别提供三种 store 服务:常规 kv-store、内存 store 和 transient store:
事件服务
事件Service 将定义在 cosmossdk.io/core/event 包中。
事件 Service 允许模块发出类型化事件以及传统的非类型化事件:
EmitProto 发出的类型化事件应被视为区块链共识的一部分(它们究竟属于区块还是 app hash,则由运行时决定和说明)。
由 EmitKVEvent 和 EmitProtoEventNonConsensus 发出的事件不被视为共识的一部分,其他模块也无法观察到它们。如果客户端侧有需要在补丁版本中增加事件,可以使用这些方法。
日志记录器
必须使用depinject 提供一个日志记录器(cosmossdk.io/log),并通过 depinject.In
向模块开放使用。
使用它的模块应遵循 SDK 当前模式,在使用前先附加模块名。
核心 AppModule 扩展接口
模块将通过构建在 cosmossdk.io/core/appmodule.AppModule 标记接口之上的扩展接口,向运行时模块提供其核心服务。这个标记接口只要求两个空方法,用于让 depinject 将实现者识别为 depinject.OnePerModule 类型以及应用模块实现:
cosmossdk.io/core 中,并且应由有效的运行时实现支持。
MsgServer 和 QueryServer 注册
MsgServer 和 QueryServer 的注册通过实现 HasServices 扩展接口来完成:
Msg 服务所必需的 cosmos.msg.v1.service protobuf 选项的存在,同一个 ServiceRegitrar 可以同时用于注册 Msg 服务和查询服务。
创世
创世Handler 函数,即 DefaultGenesis、ValidateGenesis、InitGenesis 和 ExportGenesis,
是基于 GenesisSource 和 GenesisTarget 接口定义的。这两个接口会对创世数据源进行抽象;
创世数据源既可以是单个 JSON 对象,也可以是能够被高效流式处理的一组 JSON 对象集合。
HasGenesis 扩展接口:
前置 Blocker
对于在 BeginBlock 之前运行功能的模块,应实现HasPreBlocker 接口:
Begin 与 End Blocker
对于在交易之前运行功能(begin blocker)或在交易之后运行功能 (end blocker)的模块,应实现HasBeginBlocker 和/或 HasEndBlocker 接口:
BeginBlock 和 EndBlock 方法将接收一个 context.Context,原因如下:
- 大多数模块除了
BlockInfo之外并不需要其他 Comet 信息,因此我们可以消除对特定 Comet 版本的依赖 - 对于少数需要 Comet 区块头和/或返回验证者更新的模块,特定版本的 runtime 模块将提供特定功能,以便与所支持的特定 Comet 版本进行交互
BeginBlock、EndBlock 和 InitGenesis 能够回传验证者更新并获取完整的 Comet
区块头,某个特定 Comet 版本的 runtime 模块可以提供如下服务:
AppModule 的其余部分
当前的AppModule 框架还处理了一些这个 core API 尚未覆盖的额外关注点。
这些包括:
- gas
- 区块头
- 升级
- gogo proto 和 amino 接口类型的注册
- cobra 查询与 tx 命令
- gRPC gateway
- crisis 模块不变式
- 模拟
AppModule 扩展接口来处理
这些关注点。
对于 gogo proto 和 amino 接口,一般应尽可能早地在初始化阶段完成注册;
在 ADR 057: App Wiring 中,protobuf 类型注册发生在依赖注入之前(不过也可以改为由专门的 DI provider 来完成)。 gRPC gateway 注册大概率应由 runtime 模块处理,但 core API 不应依赖 gRPC gateway 类型,因为 1)我们已经在使用一个较旧的版本;2)未来框架有可能自动完成这类注册。 因此目前 runtime 模块可能应该提供某种专用类型来执行该注册,例如:
cosmossdk.io/client/v2 模块及其
autocli 框架提供。
使用示例
下面是一个假设的foo v2 模块的设置示例。该模块使用 ORM 进行状态
管理和创世处理。
运行时兼容版本
core 模块将定义一个静态整型变量 cosmossdk.io/core.RuntimeCompatibilityVersion,
它是一个可在运行时访问的 core 模块次版本指示器。正确的 runtime 模块实现
应检查这个兼容版本;如果当前 RuntimeCompatibilityVersion 高于该 runtime 版本所能支持的
core API 版本,则应返回错误。当向 core 模块 API 添加 runtime 模块必须支持的新特性时,
这个版本号应当递增。
运行时模块
初始的runtime 模块将直接在现有的 github.com/cosmos/cosmos-sdk go 模块中创建,放在 runtime 包下。该模块将作为现有 BaseApp、sdk.Context 和模块管理器的轻量封装,并遵循 Cosmos SDK 现有的基于 0 的版本控制。为了迁移到语义化版本控制以及运行时模块化,将在 cosmossdk.io/runtime 前缀下创建新的官方支持运行时模块。对于每一种受支持的共识引擎,都应创建一个采用语义化版本控制的 go 模块,并为该共识引擎提供运行时实现。例如:
cosmossdk.io/runtime/cometcosmossdk.io/runtime/comet/v2cosmossdk.io/runtime/rollkit- 等。
github.com/cosmos/cosmos-sdk/runtime 使用 protobuf 配置类型 cosmos.app.runtime.v1alpha1.Module。当我们拥有独立的 v1 comet 运行时时,应使用专用的 protobuf 模块配置类型,例如 cosmos.runtime.comet.v1.Module1。当我们发布 comet 运行时的 v2 版本(cosmossdk.io/runtime/comet/v2)时,也应有对应的 cosmos.runtime.comet.v2.Module protobuf 类型。
为了更容易支持不同的共识引擎,并让它们支持本 ADR 中描述的同一套核心模块功能,应创建一个包含共享运行时组件的公共 go 模块。初期最容易共享的运行时组件可能是消息/查询路由器、模块间客户端、服务注册器以及事件路由器。这个公共运行时模块最初应创建为 cosmossdk.io/runtime/common go 模块。
当这一新架构实现后,Cosmos SDK 模块的主要依赖将是 cosmossdk.io/core,并且该模块应能够与任何受支持的共识引擎一起使用(前提是它没有显式依赖某些特定于共识引擎的功能,例如 Comet 的区块头)。这样,应用开发者就可以通过导入相应的运行时模块来选择所需的共识引擎。当前的 BaseApp 将被重构到 cosmossdk.io/runtime/comet 模块中,baseapp/ 中的路由基础设施将被重构到 cosmossdk.io/runtime/common 中并支持 ADR 033,最终将不再需要依赖 github.com/cosmos/cosmos-sdk。
简而言之,模块将主要依赖 cosmossdk.io/core,而每个 cosmossdk.io/runtime/{consensus-engine} 都会为对应的共识引擎实现 cosmossdk.io/core 功能。
作为这一架构的一部分,还需要解决的另一个问题是运行时与服务器之间的关系。比较合理的做法可能是将当前的服务器架构模块化,以便它能够与任何运行时一起使用,即便该运行时基于 Comet 之外的其他共识引擎。这意味着最终 Comet 运行时需要封装启动 Comet 和 ABCI 应用的逻辑。
测试
应在 core 中提供所有服务的 mock 实现,以便在不依赖任何特定运行时版本的情况下对模块进行单元测试。Mock 服务应允许测试观察服务行为,或提供非生产实现,例如可以使用内存存储来模拟存储。 对于集成测试,应提供一个 mock 运行时实现,使多个应用模块能够组合在一起进行测试,而无需依赖运行时或 Comet。后果
向后兼容性
运行时模块的早期版本应尽可能支持基于现有AppModule/sdk.Context 框架构建的模块。随着 core API 被更广泛采用,后续运行时版本可以选择放弃这类支持,仅支持 core API 以及任何运行时模块特定 API(例如 Comet 的特定版本)。
核心模块本身应尽可能长期保持在 go 语义化版本 v1,并遵循能够支持强长期支持(LTS)的设计原则。
旧版本 SDK 可以通过适配器支持基于 core 构建的模块:这些适配器将 core AppModule 实现包装为符合该 SDK 版本语义的 AppModule 实现,同时也可以通过包装 sdk.Context 来提供服务实现。
正面影响
- 更好的 API 封装和关注点分离
- 更稳定的 API
- 更强的框架可扩展性
- 确定性的事件和查询
- 事件监听器
- 支持模块间 msg 和 query 执行
- 更明确地支持模块版本的分叉与合并(包括运行时)
负面影响
中性影响
- 模块需要重构以使用该 API
AppModule功能的一些替代方案仍需在后续工作中定义 (类型注册、命令、不变量、模拟),这还需要额外的设计工作
进一步讨论
- gas
- 区块头
- 升级
- gogo proto 和 amino 接口类型的注册
- cobra 查询和 tx 命令
- gRPC gateway
- crisis 模块不变量
- 模拟
参考资料
Changelog
- 2022-08-18 First Draft
- 2022-12-08 First Draft
- 2023-01-24 Updates
Status
ACCEPTED Partially ImplementedAbstract
A new core API is proposed as a way to develop cosmos-sdk applications that will eventually replace the existingAppModule and sdk.Context frameworks a set of core services and extension interfaces. This core API aims to:
- be simpler
- more extensible
- more stable than the current framework
- enable deterministic events and queries,
- support event listeners
- ADR 033: Protobuf-based Inter-Module Communication clients.
Context
Historically modules have exposed their functionality to the framework via theAppModule and AppModuleBasic
interfaces which have the following shortcomings:
- both
AppModuleandAppModuleBasicneed to be defined and registered which is counter-intuitive - apps need to implement the full interfaces, even parts they don’t need (although there are workarounds for this),
- interface methods depend heavily on unstable third party dependencies, in particular Comet,
- legacy required methods have littered these interfaces for far too long
- get store keys from the app
- call methods on
sdk.Contextwhich contains more or less the full set of capability available to modules.
sdk.Context, the set of functionalities available to
modules are tightly coupled to this type. If there are changes to upstream dependencies (such as Comet)
or new functionalities are desired (such as alternate store types), the changes need impact sdk.Context and all
consumers of it (basically all modules). Also, all modules now receive context.Context and need to convert these
to sdk.Context’s with a non-ergonomic unwrapping function.
Any breaking changes to these interfaces, such as ones imposed by third-party dependencies like Comet, have the
side effect of forcing all modules in the ecosystem to update in lock-step. This means it is almost impossible to have
a version of the module which can be run with 2 or 3 different versions of the SDK or 2 or 3 different versions of
another module. This lock-step coupling slows down overall development within the ecosystem and causes updates to
components to be delayed longer than they would if things were more stable and loosely coupled.
Decision
Thecore API proposes a set of core APIs that modules can rely on to interact with the state machine and expose their
functionalities to it that are designed in a principled way such that:
- tight coupling of dependencies and unrelated functionalities is minimized or eliminated
- APIs can have long-term stability guarantees
- the SDK framework is extensible in a safe and straightforward way
- everything that a module wants to interact with in the state machine is a service
- all services coordinate state via
context.Contextand don’t try to recreate the “bag of variables” approach ofsdk.Context - all independent services are isolated in independent packages with minimal APIs and minimal dependencies
- the core API should be minimalistic and designed for long-term support (LTS)
- a “runtime” module will implement all the “core services” defined by the core API and can handle all module functionalities exposed by core extension interfaces
- other non-core and/or non-LTS services can be exposed by specific versions of runtime modules or other modules following the same design principles, this includes functionality that interacts with specific non-stable versions of third party dependencies such as Comet
- the core API doesn’t implement any functionality, it just defines types
- go stable API compatibility guidelines are followed: Link
BaseApp and the ModuleManager. Runtime modules which implement the core API are intentionally separate
from the core API in order to enable more parallel versions and forks of the runtime module than is possible with the
SDK’s current tightly coupled BaseApp design while still allowing for a high degree of composability and
compatibility.
Modules which are built only against the core API don’t need to know anything about which version of runtime,
BaseApp or Comet in order to be compatible. Modules from the core mainline SDK could be easily composed
with a forked version of runtime with this pattern.
This design is intended to enable matrices of compatible dependency versions. Ideally a given version of any module
is compatible with multiple versions of the runtime module and other compatible modules. This will allow dependencies
to be selectively updated based on battle-testing. More conservative projects may want to update some dependencies
slower than more fast moving projects.
Core Services
The following “core services” are defined by the core API. All valid runtime module implementations should provide implementations of these services to modules via both dependency injection and manual wiring. The individual services described below are all bundled in a convenientappmodule.Service
“bundle service” so that for simplicity modules can declare a dependency on a single service.
Store Services
Store services will be defined in thecosmossdk.io/core/store package.
The generic store.KVStore interface is the same as current SDK KVStore interface. Store keys have been refactored
into store services which, instead of expecting the context to know about stores, invert the pattern and allow
retrieving a store from a generic context. There are three store services for the three types of currently supported
stores - regular kv-store, memory, and transient:
Event Service
The eventService will be defined in the cosmossdk.io/core/event package.
The event Service allows modules to emit typed and legacy untyped events:
EmitProto should be assumed to be part of blockchain consensus (whether they are part of
the block or app hash is left to the runtime to specify).
Events emitted by EmitKVEvent and EmitProtoEventNonConsensus are not considered to be part of consensus and cannot be observed
by other modules. If there is a client-side need to add events in patch releases, these methods can be used.
Logger
A logger (cosmossdk.io/log) must be supplied using depinject, and will
be made available for modules to use via depinject.In.
Modules using it should follow the current pattern in the SDK by adding the module name before using it.
Core AppModule extension interfaces
Modules will provide their core services to the runtime module via extension interfaces built on top of the
cosmossdk.io/core/appmodule.AppModule tag interface. This tag interface requires only two empty methods which
allow depinject to identify implementors as depinject.OnePerModule types and as app module implementations:
cosmossdk.io/core should be supported by valid runtime
implementations.
MsgServer and QueryServer registration
MsgServer and QueryServer registration is done by implementing the HasServices extension interface:
cosmos.msg.v1.service protobuf option, required for Msg services, the same ServiceRegitrar can be
used to register both Msg and query services.
Genesis
The genesisHandler functions - DefaultGenesis, ValidateGenesis, InitGenesis and ExportGenesis - are specified
against the GenesisSource and GenesisTarget interfaces which will abstract over genesis sources which may be a single
JSON object or collections of JSON objects that can be efficiently streamed.
HasGenesis extension interface:
Pre Blockers
Modules that have functionality that runs before BeginBlock and should implement the hasHasPreBlocker interfaces:
Begin and End Blockers
Modules that have functionality that runs before transactions (begin blockers) or after transactions (end blockers) should implement the hasHasBeginBlocker and/or HasEndBlocker interfaces:
BeginBlock and EndBlock methods will take a context.Context, because:
- most modules don’t need Comet information other than
BlockInfoso we can eliminate dependencies on specific Comet versions - for the few modules that need Comet block headers and/or return validator updates, specific versions of the runtime module will provide specific functionality for interacting with the specific version(s) of Comet supported
BeginBlock, EndBlock and InitGenesis to send back validator updates and retrieve full Comet
block headers, the runtime module for a specific version of Comet could provide services like this:
Remaining Parts of AppModule
The currentAppModule framework handles a number of additional concerns which aren’t addressed by this core API.
These include:
- gas
- block headers
- upgrades
- registration of gogo proto and amino interface types
- cobra query and tx commands
- gRPC gateway
- crisis module invariants
- simulations
AppModule extension interfaces either inside or outside of core will need to be specified to handle
these concerns.
In the case of gogo proto and amino interfaces, the registration of these generally should happen as early
as possible during initialization and in ADR 057: App Wiring, protobuf type registrationhappens before dependency injection (although this could alternatively be done dedicated DI providers). gRPC gateway registration should probably be handled by the runtime module, but the core API shouldn’t depend on gRPC gateway types as 1) we are already using an older version and 2) it’s possible the framework can do this registration automatically in the future. So for now, the runtime module should probably provide some sort of specific type for doing this registration ex:
cosmossdk.io/client/v2 module and its
autocli framework.
Example Usage
Here is an example of setting up a hypotheticalfoo v2 module which uses the ORM for its state
management and genesis.
Runtime Compatibility Version
Thecore module will define a static integer var, cosmossdk.io/core.RuntimeCompatibilityVersion, which is
a minor version indicator of the core module that is accessible at runtime. Correct runtime module implementations
should check this compatibility version and return an error if the current RuntimeCompatibilityVersion is higher
than the version of the core API that this runtime version can support. When new features are adding to the core
module API that runtime modules are required to support, this version should be incremented.
Runtime Modules
The initialruntime module will simply be created within the existing github.com/cosmos/cosmos-sdk go module
under the runtime package. This module will be a small wrapper around the existing BaseApp, sdk.Context and
module manager and follow the Cosmos SDK’s existing 0-based versioning. To move to semantic
versioning as well as runtime modularity, new officially supported runtime modules will be created under the
cosmossdk.io/runtime prefix. For each supported consensus engine a semantically-versioned go module should be created
with a runtime implementation for that consensus engine. For example:
cosmossdk.io/runtime/cometcosmossdk.io/runtime/comet/v2cosmossdk.io/runtime/rollkit- etc.
github.com/cosmos/cosmos-sdk/runtime uses the protobuf config type cosmos.app.runtime.v1alpha1.Module.
When we have a standalone v1 comet runtime, we should use a dedicated protobuf module config type such as
cosmos.runtime.comet.v1.Module1. When we release v2 of the comet runtime (cosmossdk.io/runtime/comet/v2) we should
have a corresponding cosmos.runtime.comet.v2.Module protobuf type.
In order to make it easier to support different consensus engines that support the same core module functionality as
described in this ADR, a common go module should be created with shared runtime components. The easiest runtime components
to share initially are probably the message/query router, inter-module client, service register, and event router.
This common runtime module should be created initially as the cosmossdk.io/runtime/common go module.
When this new architecture has been implemented, the main dependency for a Cosmos SDK module would be
cosmossdk.io/core and that module should be able to be used with any supported consensus engine (to the extent
that it does not explicitly depend on consensus engine specific functionality such as Comet’s block headers). An
app developer would then be able to choose which consensus engine they want to use by importing the corresponding
runtime module. The current BaseApp would be refactored into the cosmossdk.io/runtime/comet module, the router
infrastructure in baseapp/ would be refactored into cosmossdk.io/runtime/common and support ADR 033, and eventually
a dependency on github.com/cosmos/cosmos-sdk would no longer be required.
In short, modules would depend primarily on cosmossdk.io/core, and each cosmossdk.io/runtime/{consensus-engine}
would implement the cosmossdk.io/core functionality for that consensus engine.
On additional piece that would need to be resolved as part of this architecture is how runtimes relate to the server.
Likely it would make sense to modularize the current server architecture so that it can be used with any runtime even
if that is based on a consensus engine besides Comet. This means that eventually the Comet runtime would need to
encapsulate the logic for starting Comet and the ABCI app.
Testing
A mock implementation of all services should be provided in core to allow for unit testing of modules without needing to depend on any particular version of runtime. Mock services should allow tests to observe service behavior or provide a non-production implementation - for instance memory stores can be used to mock stores. For integration testing, a mock runtime implementation should be provided that allows composing different app modules together for testing without a dependency on runtime or Comet.Consequences
Backwards Compatibility
Early versions of runtime modules should aim to support as much as possible modules built with the existingAppModule/sdk.Context framework. As the core API is more widely adopted, later runtime versions may choose to
drop support and only support the core API plus any runtime module specific APIs (like specific versions of Comet).
The core module itself should strive to remain at the go semantic version v1 as long as possible and follow design
principles that allow for strong long-term support (LTS).
Older versions of the SDK can support modules built against core with adaptors that convert wrap core AppModule
implementations in implementations of AppModule that conform to that version of the SDK’s semantics as well
as by providing service implementations by wrapping sdk.Context.
Positive
- better API encapsulation and separation of concerns
- more stable APIs
- more framework extensibility
- deterministic events and queries
- event listeners
- inter-module msg and query execution support
- more explicit support for forking and merging of module versions (including runtime)
Negative
Neutral
- modules will need to be refactored to use this API
- some replacements for
AppModulefunctionality still need to be defined in follow-ups (type registration, commands, invariants, simulations) and this will take additional design work
Further Discussions
- gas
- block headers
- upgrades
- registration of gogo proto and amino interface types
- cobra query and tx commands
- gRPC gateway
- crisis module invariants
- simulations