变更记录

  • 2022-08-18 初稿
  • 2022-12-08 初稿
  • 2023-01-24 更新

状态

已接受,部分实现

摘要

这里提出了一套新的核心 API,作为开发 cosmos-sdk 应用的一种方式,最终将以一组核心服务和扩展接口取代现有的 AppModule 和 sdk.Context 框架。该核心 API 的目标是:

背景

历史上,模块通过 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 框架可以用安全且直接的方式扩展
核心 API 的设计原则如下:
  • 模块希望与状态机交互的所有对象都是服务
  • 所有服务都通过 context.Context 协调状态,而不是试图重建 sdk.Context 的“变量袋”式做法
  • 所有独立服务都隔离在独立包中,只暴露最小 API 并保持最小依赖
  • 核心 API 应当保持极简,并面向长期支持(LTS)设计
  • 一个“运行时”模块将实现核心 API 定义的所有“核心服务”,并能处理由核心扩展接口暴露的所有模块功能
  • 其他非核心和/或非 LTS 服务也可以由特定版本的运行时模块或其他模块提供,只要遵循相同的设计原则;这包括与特定非稳定版本第三方依赖(如 Comet)交互的功能
  • 核心 API 本身不实现任何功能,它只定义类型
  • 遵循 Go 稳定 API 兼容性指南:链接
“运行时”模块是指任何实现了组合 ABCI 应用这一核心功能的模块,这项工作当前由 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:
type KVStoreService interface {
    OpenKVStore(context.Context)

KVStore
}

type MemoryStoreService interface {
    OpenMemoryStore(context.Context)

KVStore
}

type TransientStoreService interface {
    OpenTransientStore(context.Context)

KVStore
}
模块可以像下面这样使用这些服务:
func (k msgServer)

Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error) {
    store := k.kvStoreSvc.OpenKVStore(ctx)
}
与当前运行时模块实现一样,模块无需显式命名这些 store key;运行时模块会为它们选择合适的名称,而模块只需要在依赖注入(或手动)构造函数中请求所需的 store 类型。

事件服务

事件 Service 将定义在 cosmossdk.io/core/event 包中。 事件 Service 允许模块发出类型化事件以及传统的非类型化事件:
package event

type Service interface {
  // EmitProtoEvent emits events represented as a protobuf message (as described in ADR 032).
  //
  // Callers SHOULD assume that these events may be included in consensus. These events
  // MUST be emitted deterministically and adding, removing or changing these events SHOULD
  // be considered state-machine breaking.
  EmitProtoEvent(ctx context.Context, event protoiface.MessageV1)

error

  // EmitKVEvent emits an event based on an event and kv-pair attributes.
  //
  // These events will not be part of consensus and adding, removing or changing these events is
  // not a state-machine breaking change.
  EmitKVEvent(ctx context.Context, eventType string, attrs ...KVEventAttribute)

error

  // EmitProtoEventNonConsensus emits events represented as a protobuf message (as described in ADR 032), without
  // including it in blockchain consensus.
  //
  // These events will not be part of consensus and adding, removing or changing events is
  // not a state-machine breaking change.
  EmitProtoEventNonConsensus(ctx context.Context, event protoiface.MessageV1)

error
}
通过 EmitProto 发出的类型化事件应被视为区块链共识的一部分(它们究竟属于区块还是 app hash,则由运行时决定和说明)。 由 EmitKVEvent 和 EmitProtoEventNonConsensus 发出的事件不被视为共识的一部分,其他模块也无法观察到它们。如果客户端侧有需要在补丁版本中增加事件,可以使用这些方法。

日志记录器

必须使用 depinject 提供一个日志记录器(cosmossdk.io/log),并通过 depinject.In 向模块开放使用。 使用它的模块应遵循 SDK 当前模式,在使用前先附加模块名。
type ModuleInputs struct {
    depinject.In

  Logger log.Logger
}

func ProvideModule(in ModuleInputs)

ModuleOutputs {
    keeper := keeper.NewKeeper(
    in.logger,
  )
}

func NewKeeper(logger log.Logger)

Keeper {
    return Keeper{
    logger: logger.With(log.ModuleKey, "x/"+types.ModuleName),
}
}

核心 AppModule 扩展接口

模块将通过构建在 cosmossdk.io/core/appmodule.AppModule 标记接口之上的扩展接口,向运行时模块提供其核心服务。这个标记接口只要求两个空方法,用于让 depinject 将实现者识别为 depinject.OnePerModule 类型以及应用模块实现:
type AppModule interface {
    depinject.OnePerModuleType

  // IsAppModule is a dummy method to tag a struct as implementing an AppModule.
  IsAppModule()
}
其他核心扩展接口将定义在 cosmossdk.io/core 中,并且应由有效的运行时实现支持。

MsgServer 和 QueryServer 注册

MsgServer 和 QueryServer 的注册通过实现 HasServices 扩展接口来完成:
type HasServices interface {
    AppModule

	RegisterServices(grpc.ServiceRegistrar)
}
由于 Msg 服务所必需的 cosmos.msg.v1.service protobuf 选项的存在,同一个 ServiceRegitrar 可以同时用于注册 Msg 服务和查询服务。

创世

创世 Handler 函数,即 DefaultGenesis、ValidateGenesis、InitGenesis 和 ExportGenesis, 是基于 GenesisSource 和 GenesisTarget 接口定义的。这两个接口会对创世数据源进行抽象; 创世数据源既可以是单个 JSON 对象,也可以是能够被高效流式处理的一组 JSON 对象集合。
// GenesisSource is a source for genesis data in JSON format. It may abstract over a
// single JSON object or separate files for each field in a JSON object that can
// be streamed over. Modules should open a separate io.ReadCloser for each field that
// is required. When fields represent arrays they can efficiently be streamed
// over. If there is no data for a field, this function should return nil, nil. It is
// important that the caller closes the reader when done with it.
type GenesisSource = func(field string) (io.ReadCloser, error)

// GenesisTarget is a target for writing genesis data in JSON format. It may
// abstract over a single JSON object or JSON in separate files that can be
// streamed over. Modules should open a separate io.WriteCloser for each field
// and should prefer writing fields as arrays when possible to support efficient
// iteration. It is important the caller closers the writer AND checks the error
// when done with it. It is expected that a stream of JSON data is written
// to the writer.
type GenesisTarget = func(field string) (io.WriteCloser, error)
给定模块的所有创世对象都应当符合 JSON 对象的语义。 JSON 对象中的每个字段都应当被分别读取和写入,以支持创世数据的流式处理。 ORM 和 collections 都支持 流式创世,因此使用这些框架的模块通常不需要编写任何手动的 创世代码。 为支持创世,模块应实现 HasGenesis 扩展接口:
type HasGenesis interface {
    AppModule

	// DefaultGenesis writes the default genesis for this module to the target.
	DefaultGenesis(GenesisTarget)

error

	// ValidateGenesis validates the genesis data read from the source.
	ValidateGenesis(GenesisSource)

error

	// InitGenesis initializes module state from the genesis source.
	InitGenesis(context.Context, GenesisSource)

error

	// ExportGenesis exports module state to the genesis target.
	ExportGenesis(context.Context, GenesisTarget)

error
}

前置 Blocker

对于在 BeginBlock 之前运行功能的模块,应实现 HasPreBlocker 接口:
type HasPreBlocker interface {
    AppModule
  PreBlock(context.Context)

error
}

Begin 与 End Blocker

对于在交易之前运行功能(begin blocker)或在交易之后运行功能 (end blocker)的模块,应实现 HasBeginBlocker 和/或 HasEndBlocker 接口:
type HasBeginBlocker interface {
    AppModule
  BeginBlock(context.Context)

error
}

type HasEndBlocker interface {
    AppModule
  EndBlock(context.Context)

error
}
BeginBlock 和 EndBlock 方法将接收一个 context.Context,原因如下:
  • 大多数模块除了 BlockInfo 之外并不需要其他 Comet 信息,因此我们可以消除对特定 Comet 版本的依赖
  • 对于少数需要 Comet 区块头和/或返回验证者更新的模块,特定版本的 runtime 模块将提供特定功能,以便与所支持的特定 Comet 版本进行交互
为了让 BeginBlock、EndBlock 和 InitGenesis 能够回传验证者更新并获取完整的 Comet 区块头,某个特定 Comet 版本的 runtime 模块可以提供如下服务:
type ValidatorUpdateService interface {
    SetValidatorUpdates(context.Context, []abci.ValidatorUpdate)
}
Header Service 定义了获取区块头信息的一种方式。这些信息针对所有实现进行了泛化:
type Service interface {
    GetHeaderInfo(context.Context)

Info
}

type Info struct {
    Height int64      // Height returns the height of the block
	Hash []byte       // Hash returns the hash of the block header
	Time time.Time    // Time returns the time of the block
	ChainID string    // ChainId returns the chain ID of the block
}
Comet Service 提供了一种获取 Comet 特定信息的方式:
type Service interface {
    GetCometInfo(context.Context)

Info
}

type CometInfo struct {
    Evidence []abci.Misbehavior // Misbehavior returns the misbehavior of the block
	// ValidatorsHash returns the hash of the validators
	// For Comet, it is the hash of the next validators
	ValidatorsHash []byte
	ProposerAddress []byte            // ProposerAddress returns the address of the block proposer
	DecidedLastCommit abci.CommitInfo // DecidedLastCommit returns the last commit info
}
如果用户希望向模块提供其他信息,则需要实现另一个类似这样的服务:
type RollKit Interface {
  ...
}
我们知道这些类型会在 Comet 层发生变化,而且实际上只有极少数模块需要这类 功能,因此有意将它们排除在 core 之外,以便让 core 仅保留必要、最小且稳定的 API 集合。

AppModule 的其余部分

当前的 AppModule 框架还处理了一些这个 core API 尚未覆盖的额外关注点。 这些包括:
  • gas
  • 区块头
  • 升级
  • gogo proto 和 amino 接口类型的注册
  • cobra 查询与 tx 命令
  • gRPC gateway
  • crisis 模块不变式
  • 模拟
无论是在 core 内部还是外部,都还需要定义额外的 AppModule 扩展接口来处理 这些关注点。 对于 gogo proto 和 amino 接口,一般应尽可能早地在初始化阶段完成注册; 在 ADR 057: App Wiring 中,protobuf 类型注册
发生在依赖注入之前(不过也可以改为由专门的 DI provider 来完成)。
gRPC gateway 注册大概率应由 runtime 模块处理,但 core API 不应依赖 gRPC gateway 类型,因为 1)我们已经在使用一个较旧的版本;2)未来框架有可能自动完成这类注册。 因此目前 runtime 模块可能应该提供某种专用类型来执行该注册,例如:
type GrpcGatewayInfo struct {
    Handlers []GrpcGatewayHandler
}

type GrpcGatewayHandler func(ctx context.Context, mux *runtime.ServeMux, client QueryClient)

error
模块则可以在 provider 中返回它:
func ProvideGrpcGateway()

GrpcGatewayInfo {
    return GrpcGatewayinfo {
    Handlers: []Handler {
    types.RegisterQueryHandlerClient
}
 
}
}
crisis 模块不变式和模拟未来都可能会被重新设计,因此应分别通过在 crisis 和 simulation 模块中定义的类型来管理。 CLI 命令的扩展接口将通过 cosmossdk.io/client/v2 模块及其 autocli 框架提供。

使用示例

下面是一个假设的 foo v2 模块的设置示例。该模块使用 ORM 进行状态 管理和创世处理。
type Keeper struct {
    db orm.ModuleDB
	evtSrv event.Service
}

func (k Keeper)

RegisterServices(r grpc.ServiceRegistrar) {
    foov1.RegisterMsgServer(r, k)

foov1.RegisterQueryServer(r, k)
}

func (k Keeper)

BeginBlock(context.Context)

error {
    return nil
}

func ProvideApp(config *foomodulev2.Module, evtSvc event.EventService, db orm.ModuleDB) (Keeper, appmodule.AppModule) {
    k := &Keeper{
    db: db, evtSvc: evtSvc
}

return k, k
}

运行时兼容版本

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/comet
  • cosmossdk.io/runtime/comet/v2
  • cosmossdk.io/runtime/rollkit
  • 等。
这些运行时模块应尽量采用语义化版本控制,即使其底层共识引擎本身没有采用。另外,由于运行时模块本身也是一等的 Cosmos SDK 模块,因此它应当拥有一个 protobuf 模块配置类型。对于每个这样的运行时模块,都应创建一个新的语义化版本模块配置类型,使 go 模块与模块配置类型之间保持 1:1 对应关系。这也是每个采用语义化版本控制的 Cosmos SDK 模块都应遵循的实践,如 ADR 057: App Wiring 所述。 当前,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 Implemented

Abstract

A new core API is proposed as a way to develop cosmos-sdk applications that will eventually replace the existing AppModule and sdk.Context frameworks a set of core services and extension interfaces. This core API aims to:

Context

Historically modules have exposed their functionality to the framework via the AppModule and AppModuleBasic interfaces which have the following shortcomings:
  • both AppModule and AppModuleBasic need 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
In order to interact with the state machine, modules have needed to do a combination of these things:
  • get store keys from the app
  • call methods on sdk.Context which contains more or less the full set of capability available to modules.
By isolating all the state machine functionality into 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

The core 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
The design principles of the core API are as follows:
  • everything that a module wants to interact with in the state machine is a service
  • all services coordinate state via context.Context and don’t try to recreate the “bag of variables” approach of sdk.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
A “runtime” module is any module which implements the core functionality of composing an ABCI app, which is currently handled by 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 convenient appmodule.Service “bundle service” so that for simplicity modules can declare a dependency on a single service.

Store Services

Store services will be defined in the cosmossdk.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:
type KVStoreService interface {
    OpenKVStore(context.Context)

KVStore
}

type MemoryStoreService interface {
    OpenMemoryStore(context.Context)

KVStore
}

type TransientStoreService interface {
    OpenTransientStore(context.Context)

KVStore
}
Modules can use these services like this:
func (k msgServer)

Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error) {
    store := k.kvStoreSvc.OpenKVStore(ctx)
}
Just as with the current runtime module implementation, modules will not need to explicitly name these store keys, but rather the runtime module will choose an appropriate name for them and modules just need to request the type of store they need in their dependency injection (or manual) constructors.

Event Service

The event Service will be defined in the cosmossdk.io/core/event package. The event Service allows modules to emit typed and legacy untyped events:
package event

type Service interface {
  // EmitProtoEvent emits events represented as a protobuf message (as described in ADR 032).
  //
  // Callers SHOULD assume that these events may be included in consensus. These events
  // MUST be emitted deterministically and adding, removing or changing these events SHOULD
  // be considered state-machine breaking.
  EmitProtoEvent(ctx context.Context, event protoiface.MessageV1)

error

  // EmitKVEvent emits an event based on an event and kv-pair attributes.
  //
  // These events will not be part of consensus and adding, removing or changing these events is
  // not a state-machine breaking change.
  EmitKVEvent(ctx context.Context, eventType string, attrs ...KVEventAttribute)

error

  // EmitProtoEventNonConsensus emits events represented as a protobuf message (as described in ADR 032), without
  // including it in blockchain consensus.
  //
  // These events will not be part of consensus and adding, removing or changing events is
  // not a state-machine breaking change.
  EmitProtoEventNonConsensus(ctx context.Context, event protoiface.MessageV1)

error
}
Typed events emitted with 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.
type ModuleInputs struct {
    depinject.In

  Logger log.Logger
}

func ProvideModule(in ModuleInputs)

ModuleOutputs {
    keeper := keeper.NewKeeper(
    in.logger,
  )
}

func NewKeeper(logger log.Logger)

Keeper {
    return Keeper{
    logger: logger.With(log.ModuleKey, "x/"+types.ModuleName),
}
}

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:
type AppModule interface {
    depinject.OnePerModuleType

  // IsAppModule is a dummy method to tag a struct as implementing an AppModule.
  IsAppModule()
}
Other core extension interfaces will be defined in 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:
type HasServices interface {
    AppModule

	RegisterServices(grpc.ServiceRegistrar)
}
Because of the 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 genesis Handler 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.
// GenesisSource is a source for genesis data in JSON format. It may abstract over a
// single JSON object or separate files for each field in a JSON object that can
// be streamed over. Modules should open a separate io.ReadCloser for each field that
// is required. When fields represent arrays they can efficiently be streamed
// over. If there is no data for a field, this function should return nil, nil. It is
// important that the caller closes the reader when done with it.
type GenesisSource = func(field string) (io.ReadCloser, error)

// GenesisTarget is a target for writing genesis data in JSON format. It may
// abstract over a single JSON object or JSON in separate files that can be
// streamed over. Modules should open a separate io.WriteCloser for each field
// and should prefer writing fields as arrays when possible to support efficient
// iteration. It is important the caller closers the writer AND checks the error
// when done with it. It is expected that a stream of JSON data is written
// to the writer.
type GenesisTarget = func(field string) (io.WriteCloser, error)
All genesis objects for a given module are expected to conform to the semantics of a JSON object. Each field in the JSON object should be read and written separately to support streaming genesis. The ORM and collections both support streaming genesis and modules using these frameworks generally do not need to write any manual genesis code. To support genesis, modules should implement the HasGenesis extension interface:
type HasGenesis interface {
    AppModule

	// DefaultGenesis writes the default genesis for this module to the target.
	DefaultGenesis(GenesisTarget)

error

	// ValidateGenesis validates the genesis data read from the source.
	ValidateGenesis(GenesisSource)

error

	// InitGenesis initializes module state from the genesis source.
	InitGenesis(context.Context, GenesisSource)

error

	// ExportGenesis exports module state to the genesis target.
	ExportGenesis(context.Context, GenesisTarget)

error
}

Pre Blockers

Modules that have functionality that runs before BeginBlock and should implement the has HasPreBlocker interfaces:
type HasPreBlocker interface {
    AppModule
  PreBlock(context.Context)

error
}

Begin and End Blockers

Modules that have functionality that runs before transactions (begin blockers) or after transactions (end blockers) should implement the has HasBeginBlocker and/or HasEndBlocker interfaces:
type HasBeginBlocker interface {
    AppModule
  BeginBlock(context.Context)

error
}

type HasEndBlocker interface {
    AppModule
  EndBlock(context.Context)

error
}
The BeginBlock and EndBlock methods will take a context.Context, because:
  • most modules don’t need Comet information other than BlockInfo so 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
In order for 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:
type ValidatorUpdateService interface {
    SetValidatorUpdates(context.Context, []abci.ValidatorUpdate)
}
Header Service defines a way to get header information about a block. This information is generalized for all implementations:
type Service interface {
    GetHeaderInfo(context.Context)

Info
}

type Info struct {
    Height int64      // Height returns the height of the block
	Hash []byte       // Hash returns the hash of the block header
	Time time.Time    // Time returns the time of the block
	ChainID string    // ChainId returns the chain ID of the block
}
Comet Service provides a way to get comet specific information:
type Service interface {
    GetCometInfo(context.Context)

Info
}

type CometInfo struct {
    Evidence []abci.Misbehavior // Misbehavior returns the misbehavior of the block
	// ValidatorsHash returns the hash of the validators
	// For Comet, it is the hash of the next validators
	ValidatorsHash []byte
	ProposerAddress []byte            // ProposerAddress returns the address of the block proposer
	DecidedLastCommit abci.CommitInfo // DecidedLastCommit returns the last commit info
}
If a user would like to provide a module other information they would need to implement another service like:
type RollKit Interface {
  ...
}
We know these types will change at the Comet level and that also a very limited set of modules actually need this functionality, so they are intentionally kept out of core to keep core limited to the necessary, minimal set of stable APIs.

Remaining Parts of AppModule

The current AppModule 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
Additional 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 registration
happens 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:
type GrpcGatewayInfo struct {
    Handlers []GrpcGatewayHandler
}

type GrpcGatewayHandler func(ctx context.Context, mux *runtime.ServeMux, client QueryClient)

error
which modules can return in a provider:
func ProvideGrpcGateway()

GrpcGatewayInfo {
    return GrpcGatewayinfo {
    Handlers: []Handler {
    types.RegisterQueryHandlerClient
}
 
}
}
Crisis module invariants and simulations are subject to potential redesign and should be managed with types defined in the crisis and simulation modules respectively. Extension interface for CLI commands will be provided via the cosmossdk.io/client/v2 module and its autocli framework.

Example Usage

Here is an example of setting up a hypothetical foo v2 module which uses the ORM for its state management and genesis.
type Keeper struct {
    db orm.ModuleDB
	evtSrv event.Service
}

func (k Keeper)

RegisterServices(r grpc.ServiceRegistrar) {
    foov1.RegisterMsgServer(r, k)

foov1.RegisterQueryServer(r, k)
}

func (k Keeper)

BeginBlock(context.Context)

error {
    return nil
}

func ProvideApp(config *foomodulev2.Module, evtSvc event.EventService, db orm.ModuleDB) (Keeper, appmodule.AppModule) {
    k := &Keeper{
    db: db, evtSvc: evtSvc
}

return k, k
}

Runtime Compatibility Version

The core 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 initial runtime 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/comet
  • cosmossdk.io/runtime/comet/v2
  • cosmossdk.io/runtime/rollkit
  • etc.
These runtime modules should attempt to be semantically versioned even if the underlying consensus engine is not. Also, because a runtime module is also a first class Cosmos SDK module, it should have a protobuf module config type. A new semantically versioned module config type should be created for each of these runtime module such that there is a 1:1 correspondence between the go module and module config type. This is the same practice should be followed for every semantically versioned Cosmos SDK module as described in ADR 057: App Wiring. Currently, 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 existing AppModule/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 AppModule functionality 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

References