变更记录

  • 2019 年 9 月 4 日:初始草案

背景

Cosmos SDK 模块目前使用 KVStore 接口和 Codec 来访问各自的状态。虽然这为模块开发者提供了相当大的自由度,但很难实现模块化,用户体验也较为一般。 首先,每次模块尝试访问状态时,都必须先对值进行 marshal,然后设置或获取该值,最后再 unmarshal。通常这是通过声明 Keeper.GetXXX 和 Keeper.SetXXX 函数来完成的,但这类函数重复较多且难以维护。 其次,这使其更难与对象能力定理保持一致:访问状态的权限由 StoreKey 定义,而它会授予对整棵 Merkle 树的完全访问权限,因此一个模块无法安全地将某个特定键值对(或一组键值对)的访问权限传递给另一个模块。 最后,由于 getter/setter 函数被定义为模块 Keeper 的方法,审阅者在审查任何访问状态部分内容的函数时,都必须考虑整棵 Merkle 树的空间。没有静态方式可以知道该函数正在访问状态的哪一部分(以及未访问哪一部分)。

决策

我们将定义一个名为 Value 的类型:
type Value struct {
    m   Mapping
  key []byte
}
Value 作为状态中某个键值对的引用,其中 Value.m 定义它将访问的键值空间,而 Value.key 定义该引用对应的精确键。 我们将定义一个名为 Mapping 的类型:
type Mapping struct {
    storeKey sdk.StoreKey
  cdc      *codec.LegacyAmino
  prefix   []byte
}
Mapping 作为状态中某个键值空间的引用,其中 Mapping.storeKey 定义 IAVL(子)树,而 Mapping.prefix 定义可选的子空间前缀。 我们将为 Value 类型定义以下核心方法:
// Get and unmarshal stored data, noop if not exists, panic if cannot unmarshal
func (Value)

Get(ctx Context, ptr interface{
}) {
}

// Get and unmarshal stored data, return error if not exists or cannot unmarshal
func (Value)

GetSafe(ctx Context, ptr interface{
}) {
}

// Get stored data as raw byte slice
func (Value)

GetRaw(ctx Context) []byte {
}

// Marshal and set a raw value
func (Value)

Set(ctx Context, o interface{
}) {
}

// Check if a raw value exists
func (Value)

Exists(ctx Context)

bool {
}

// Delete a raw value value
func (Value)

Delete(ctx Context) {
}
我们将为 Mapping 类型定义以下核心方法:
// Constructs key-value pair reference corresponding to the key argument in the Mapping space
func (Mapping)

Value(key []byte)

Value {
}

// Get and unmarshal stored data, noop if not exists, panic if cannot unmarshal
func (Mapping)

Get(ctx Context, key []byte, ptr interface{
}) {
}

// Get and unmarshal stored data, return error if not exists or cannot unmarshal
func (Mapping)

GetSafe(ctx Context, key []byte, ptr interface{
})

// Get stored data as raw byte slice
func (Mapping)

GetRaw(ctx Context, key []byte) []byte {
}

// Marshal and set a raw value
func (Mapping)

Set(ctx Context, key []byte, o interface{
}) {
}

// Check if a raw value exists
func (Mapping)

Has(ctx Context, key []byte)

bool {
}

// Delete a raw value value
func (Mapping)

Delete(ctx Context, key []byte) {
}
Mapping 类型中每个接收 ctx、key 和 args... 参数的方法,都会将调用代理到 Mapping.Value(key),并传入参数 ctx 和 args...。 此外,我们还将定义并提供一组从 Value 类型派生出的通用类型:
type Boolean struct {
    Value
}

type Enum struct {
    Value
}

type Integer struct {
    Value; enc IntEncoding
}

type String struct {
    Value
}
// ...
其中,编码方案可以不同,核心方法中的 o 参数是强类型的,核心方法中的 ptr 参数则会被显式返回类型替代。 最后,我们将定义一组从 Mapping 类型派生出的类型:
type Indexer struct {
    m   Mapping
  enc IntEncoding
}
其中,核心方法中的 key 参数是强类型的。 访问器类型的一些属性包括:
  • 只有在调用接收 Context 作为参数的函数时,才会发生状态访问
  • 访问器类型结构体只授予对该结构体所引用状态的访问权限,不授予其他权限
  • Marshalling/Unmarshalling 会在核心方法内部隐式完成

状态

提议中

后果

正面

  • 序列化将自动完成
  • 代码更短、样板代码更少、用户体验更好
  • 对状态的引用可以安全传递
  • 访问范围更加明确

负面

  • 序列化格式将被隐藏
  • 与当前架构不同,但访问器类型的使用可以选择性启用
  • 特定类型的类型(例如 Boolean 和 Integer)必须手动定义

中性

参考


Changelog

  • 2019 Sep 04: Initial draft

Context

Cosmos SDK modules currently use the KVStore interface and Codec to access their respective state. While this provides a large degree of freedom to module developers, it is hard to modularize and the UX is mediocre. First, each time a module tries to access the state, it has to marshal the value and set or get the value and finally unmarshal. Usually this is done by declaring Keeper.GetXXX and Keeper.SetXXX functions, which are repetitive and hard to maintain. Second, this makes it harder to align with the object capability theorem: the right to access the state is defined as a StoreKey, which gives full access on the entire Merkle tree, so a module cannot send the access right to a specific key-value pair (or a set of key-value pairs) to another module safely. Finally, because the getter/setter functions are defined as methods of a module’s Keeper, the reviewers have to consider the whole Merkle tree space when they reviewing a function accessing any part of the state. There is no static way to know which part of the state that the function is accessing (and which is not).

Decision

We will define a type named Value:
type Value struct {
    m   Mapping
  key []byte
}
The Value works as a reference for a key-value pair in the state, where Value.m defines the key-value space it will access and Value.key defines the exact key for the reference. We will define a type named Mapping:
type Mapping struct {
    storeKey sdk.StoreKey
  cdc      *codec.LegacyAmino
  prefix   []byte
}
The Mapping works as a reference for a key-value space in the state, where Mapping.storeKey defines the IAVL (sub-)tree and Mapping.prefix defines the optional subspace prefix. We will define the following core methods for the Value type:
// Get and unmarshal stored data, noop if not exists, panic if cannot unmarshal
func (Value)

Get(ctx Context, ptr interface{
}) {
}

// Get and unmarshal stored data, return error if not exists or cannot unmarshal
func (Value)

GetSafe(ctx Context, ptr interface{
}) {
}

// Get stored data as raw byte slice
func (Value)

GetRaw(ctx Context) []byte {
}

// Marshal and set a raw value
func (Value)

Set(ctx Context, o interface{
}) {
}

// Check if a raw value exists
func (Value)

Exists(ctx Context)

bool {
}

// Delete a raw value value
func (Value)

Delete(ctx Context) {
}
We will define the following core methods for the Mapping type:
// Constructs key-value pair reference corresponding to the key argument in the Mapping space
func (Mapping)

Value(key []byte)

Value {
}

// Get and unmarshal stored data, noop if not exists, panic if cannot unmarshal
func (Mapping)

Get(ctx Context, key []byte, ptr interface{
}) {
}

// Get and unmarshal stored data, return error if not exists or cannot unmarshal
func (Mapping)

GetSafe(ctx Context, key []byte, ptr interface{
})

// Get stored data as raw byte slice
func (Mapping)

GetRaw(ctx Context, key []byte) []byte {
}

// Marshal and set a raw value
func (Mapping)

Set(ctx Context, key []byte, o interface{
}) {
}

// Check if a raw value exists
func (Mapping)

Has(ctx Context, key []byte)

bool {
}

// Delete a raw value value
func (Mapping)

Delete(ctx Context, key []byte) {
}
Each method of the Mapping type that is passed the arguments ctx, key, and args... will proxy the call to Mapping.Value(key) with arguments ctx and args.... In addition, we will define and provide a common set of types derived from the Value type:
type Boolean struct {
    Value
}

type Enum struct {
    Value
}

type Integer struct {
    Value; enc IntEncoding
}

type String struct {
    Value
}
// ...
Where the encoding schemes can be different, o arguments in core methods are typed, and ptr arguments in core methods are replaced by explicit return types. Finally, we will define a family of types derived from the Mapping type:
type Indexer struct {
    m   Mapping
  enc IntEncoding
}
Where the key argument in core method is typed. Some of the properties of the accessor types are:
  • State access happens only when a function which takes a Context as an argument is invoked
  • Accessor type structs give rights to access the state only that the struct is referring, no other
  • Marshalling/Unmarshalling happens implicitly within the core methods

Status

Proposed

Consequences

Positive

  • Serialization will be done automatically
  • Shorter code size, less boilerplate, better UX
  • References to the state can be transferred safely
  • Explicit scope of accessing

Negative

  • Serialization format will be hidden
  • Different architecture from the current, but the use of accessor types can be opt-in
  • Type-specific types (e.g. Boolean and Integer) have to be defined manually

Neutral

References