变更记录
- 2019 年 9 月 4 日:初始草案
背景
Cosmos SDK 模块目前使用KVStore 接口和 Codec 来访问各自的状态。虽然这为模块开发者提供了相当大的自由度,但很难实现模块化,用户体验也较为一般。
首先,每次模块尝试访问状态时,都必须先对值进行 marshal,然后设置或获取该值,最后再 unmarshal。通常这是通过声明 Keeper.GetXXX 和 Keeper.SetXXX 函数来完成的,但这类函数重复较多且难以维护。
其次,这使其更难与对象能力定理保持一致:访问状态的权限由 StoreKey 定义,而它会授予对整棵 Merkle 树的完全访问权限,因此一个模块无法安全地将某个特定键值对(或一组键值对)的访问权限传递给另一个模块。
最后,由于 getter/setter 函数被定义为模块 Keeper 的方法,审阅者在审查任何访问状态部分内容的函数时,都必须考虑整棵 Merkle 树的空间。没有静态方式可以知道该函数正在访问状态的哪一部分(以及未访问哪一部分)。
决策
我们将定义一个名为Value 的类型:
Value 作为状态中某个键值对的引用,其中 Value.m 定义它将访问的键值空间,而 Value.key 定义该引用对应的精确键。
我们将定义一个名为 Mapping 的类型:
Mapping 作为状态中某个键值空间的引用,其中 Mapping.storeKey 定义 IAVL(子)树,而 Mapping.prefix 定义可选的子空间前缀。
我们将为 Value 类型定义以下核心方法:
Mapping 类型定义以下核心方法:
Mapping 类型中每个接收 ctx、key 和 args... 参数的方法,都会将调用代理到 Mapping.Value(key),并传入参数 ctx 和 args...。
此外,我们还将定义并提供一组从 Value 类型派生出的通用类型:
o 参数是强类型的,核心方法中的 ptr 参数则会被显式返回类型替代。
最后,我们将定义一组从 Mapping 类型派生出的类型:
key 参数是强类型的。
访问器类型的一些属性包括:
- 只有在调用接收
Context作为参数的函数时,才会发生状态访问 - 访问器类型结构体只授予对该结构体所引用状态的访问权限,不授予其他权限
- Marshalling/Unmarshalling 会在核心方法内部隐式完成
状态
提议中后果
正面
- 序列化将自动完成
- 代码更短、样板代码更少、用户体验更好
- 对状态的引用可以安全传递
- 访问范围更加明确
负面
- 序列化格式将被隐藏
- 与当前架构不同,但访问器类型的使用可以选择性启用
- 特定类型的类型(例如
Boolean和Integer)必须手动定义
中性
参考
Changelog
- 2019 Sep 04: Initial draft
Context
Cosmos SDK modules currently use theKVStore 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 namedValue:
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:
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:
Mapping type:
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:
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:
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
Contextas 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
ProposedConsequences
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.
BooleanandInteger) have to be defined manually