
Cosmos SDK存储介绍
Cosmos SDK提供了大量的存储空间来保存应用程序的状态。默认情况下,Cosmos SDK应用程序的主要存储是一个multistore,即一个存储的存储。开发者可以根据他们的应用需求,在multistore中添加任意数量的键值存储。multistore的存在是为了支持Cosmos SDK的模块化,因为它允许每个模块声明和管理他们自己的状态子集。multistore中的键值存储只能用一个特定的能力键来访问,这个能力键通常由声明该存储的模块的管理员持有。
+-----------------------------------------------------+
| |
| +--------------------------------------------+ |
| | | |
| | KVStore 1 - Manage by keeper of Module 1 |
| | | |
| +--------------------------------------------+ |
| |
| +--------------------------------------------+ |
| | | |
| | KVStore 2 - Manage by keeper of Module 2 | |
| | | |
| +--------------------------------------------+ |
| |
| +--------------------------------------------+ |
| | | |
| | KVStore 3 - Manage by keeper of Module 2 | |
| | | |
| +--------------------------------------------+ |
| |
| +--------------------------------------------+ |
| | | |
| | KVStore 4 - Manage by keeper of Module 3 | |
| | | |
| +--------------------------------------------+ |
| |
| +--------------------------------------------+ |
| | | |
| | KVStore 5 - Manage by keeper of Module 4 | |
| | | |
| +--------------------------------------------+ |
| |
| Main Multistore |
| |
+-----------------------------------------------------+
Application's State
存储接口
Cosmos SDK存储的核心是一个持有CacheWrapper并具有GetStoreType()方法的对象:
type Store interface {
GetStoreType() StoreType
CacheWrapper
}
GetStoreType是返回存储类型的简单方法,而CacheWrapper是通过Write方法实现存储读取缓存和写入分支的简单接口:
//----------------------------------------
// CacheWrap
// CacheWrap is the most appropriate interface for store ephemeral branching and cache.
// For example, IAVLStore.CacheWrap() returns a CacheKVStore. CacheWrap should not return
// a Committer, since Commit ephemeral store make no sense. It can return KVStore,
// HeapStore, SpaceStore, etc.
type CacheWrap interface {
// Write syncs with the underlying store.
Write()
// CacheWrap recursively wraps again.
CacheWrap() CacheWrap
// CacheWrapWithTrace recursively wraps again with tracing enabled.
CacheWrapWithTrace(w io.Writer, tc TraceContext) CacheWrap
// CacheWrapWithListeners recursively wraps again with listening enabled
CacheWrapWithListeners(storeKey StoreKey, listeners []WriteListener) CacheWrap
}
type CacheWrapper interface {
// CacheWrap branches a store.
CacheWrap() CacheWrap
// CacheWrapWithTrace branches a store with tracing enabled.
CacheWrapWithTrace(w io.Writer, tc TraceContext) CacheWrap
// CacheWrapWithListeners recursively wraps again with listening enabled
CacheWrapWithListeners(storeKey StoreKey, listeners []WriteListener) CacheWrap
}
分支和缓存在Cosmos SDK中无处不在,并且需要在每个存储类型上实现。存储分支创建了一个独立的、短暂的存储分支,它可以被传递和更新而不影响主要的底层存储。这被用来触发临时的状态转换,如果发生错误,可能会在以后被恢复。
Commit Store
提交存储是一个有能力提交对底层树或数据库所做的改变的存储。Cosmos SDK通过用Committer扩展基本的存储接口来区分简单存储和提交存储:
// Stores of MultiStore must implement CommitStore.
type CommitStore interface {
Committer
Store
}
Committer是一个接口,它定义了将更改持久化到磁盘的方法:
// something that can persist to disk
type Committer interface {
Commit() CommitID
LastCommitID() CommitID
SetPruning(pruningtypes.PruningOptions)
GetPruning() pruningtypes.PruningOptions
}
CommitID是状态树的确定性的提交。它的哈希值会返回给底层的共识引擎并存储在区块头中。注意,提交存储接口的存在有多种目的,其中之一是确保不是每个对象都能提交存储。作为Cosmos SDK的对象能力模型的一部分,只有baseapp应该有提交存储的能力。例如,这就是模块通常通过该方法访问存储的ctx.KVStore()方法的原因,返回一个KVStore,而不是一个CommitKVStore。
Cosmos SDK提供了许多类型的存储,最常用的是CommitMultiStore、KVStore和GasKv存储。其他类型的存储包括Transient和TraceKV存储。
Multistore
Multistore接口
每个Cosmos SDK应用程序在其根目录持有一个multistore,以保持其状态。multistore是一个遵循multistore接口的KVStores的存储。
type MultiStore interface {
Store
// Branches MultiStore into a cached storage object.
// NOTE: Caller should probably not call .Write() on each, but
// call CacheMultiStore.Write().
CacheMultiStore() CacheMultiStore
// CacheMultiStoreWithVersion branches the underlying MultiStore where
// each stored is loaded at a specific version (height).
CacheMultiStoreWithVersion(version int64) (CacheMultiStore, error)
// Convenience for fetching substores.
// If the store does not exist, panics.
GetStore(StoreKey) Store
GetKVStore(StoreKey) KVStore
// TracingEnabled returns if tracing is enabled for the MultiStore.
TracingEnabled() bool
// SetTracer sets the tracer for the MultiStore that the underlying
// stores will utilize to trace operations. The modified MultiStore is
// returned.
SetTracer(w io.Writer) MultiStore
// SetTracingContext sets the tracing context for a MultiStore. It is
// implied that the caller should update the context when necessary between
// tracing operations. The modified MultiStore is returned.
SetTracingContext(TraceContext) MultiStore
// ListeningEnabled returns if listening is enabled for the KVStore belonging the provided StoreKey
ListeningEnabled(key StoreKey) bool
// AddListeners adds WriteListeners for the KVStore belonging to the provided StoreKey
// It appends the listeners to a current set, if one already exists
AddListeners(key StoreKey, listeners []WriteListener)
}
如果追踪功能被启用,那么分支多存储将首先在TraceKv.Store中包装所有的底层KVStore。
CommitMultiStore
Cosmos SDK中使用的Multistore的主要类型是CommitMultiStore,它是Multistore接口的一个扩展:
// CommitMultiStore is an interface for a MultiStore without cache capabilities.
type CommitMultiStore interface {
Committer
MultiStore
snapshottypes.Snapshotter
// Mount a store of type using the given db.
// If db == nil, the new store will use the CommitMultiStore db.
MountStoreWithDB(key StoreKey, typ StoreType, db dbm.DB)
// Panics on a nil key.
GetCommitStore(key StoreKey) CommitStore
// Panics on a nil key.
GetCommitKVStore(key StoreKey) CommitKVStore
// Load the latest persisted version. Called once after all calls to
// Mount*Store() are complete.
LoadLatestVersion() error
// LoadLatestVersionAndUpgrade will load the latest version, but also
// rename/delete/create sub-store keys, before registering all the keys
// in order to handle breaking formats in migrations
LoadLatestVersionAndUpgrade(upgrades *StoreUpgrades) error
// LoadVersionAndUpgrade will load the named version, but also
// rename/delete/create sub-store keys, before registering all the keys
// in order to handle breaking formats in migrations
LoadVersionAndUpgrade(ver int64, upgrades *StoreUpgrades) error
// Load a specific persisted version. When you load an old version, or when
// the last commit attempt didn't complete, the next commit after loading
// must be idempotent (return the same commit id). Otherwise the behavior is
// undefined.
LoadVersion(ver int64) error
// Set an inter-block (persistent) cache that maintains a mapping from
// StoreKeys to CommitKVStores.
SetInterBlockCache(MultiStorePersistentCache)
// SetInitialVersion sets the initial version of the IAVL tree. It is used when
// starting a new chain at an arbitrary height.
SetInitialVersion(version int64) error
// SetIAVLCacheSize sets the cache size of the IAVL tree.
SetIAVLCacheSize(size int)
}
至于具体实现,[rootMulti.Store] 是CommitMultiStore接口的首选实现。
// Store is composed of many CommitStores. Name contrasts with
// cacheMultiStore which is used for branching other MultiStores. It implements
// the CommitMultiStore interface.
type Store struct {
db dbm.DB
logger log.Logger
lastCommitInfo *types.CommitInfo
pruningManager *pruning.Manager
iavlCacheSize int
storesParams map[types.StoreKey]storeParams
stores map[types.StoreKey]types.CommitKVStore
keysByName map[string]types.StoreKey
lazyLoading bool
initialVersion int64
removalMap map[types.StoreKey]bool
traceWriter io.Writer
traceContext types.TraceContext
traceContextMutex sync.Mutex
interBlockCache types.MultiStorePersistentCache
listeners map[types.StoreKey][]types.WriteListener
}
rootMulti.Store是一个围绕db建立的基础层multistore,在这个基础层上可以安装多个KVStores,并且是baseapp中使用的默认multistore。
CacheMultiStore
每当rootMulti.Store需要被分支时,就会使用cachemulti.Store。
//----------------------------------------
// Store
// Store holds many branched stores.
// Implements MultiStore.
// NOTE: a Store (and MultiStores in general) should never expose the
// keys for the substores.
type Store struct {
db types.CacheKVStore
stores map[types.StoreKey]types.CacheWrap
keys map[string]types.StoreKey
traceWriter io.Writer
traceContext types.TraceContext
listeners map[types.StoreKey][]types.WriteListener
}
cachemulti.Store在其构造函数中分支所有子存储(为每个子存储创建一个虚拟存储),并将它们保存在Store.store中。此外还缓存了所有的读取查询。Store.GetKVStore()从Store. stores中返回存储,Store.Write()递归调用所有子存储的CacheWrap.Write()。
基础层的KVStores
KVStore和CommitKVStore接口
KVStore是一个简单的键值存储,用于存储和检索数据。CommitKVStore是KVStore,它也实现了一个Committer。默认情况下,安装在baseapp的主CommitMultiStore中的存储是CommitKVStores。KVStore接口主要是用来限制模块访问提交者的。
模块使用单独的KVStores来管理全局状态的子集。KVStores可以被持有特定密钥的对象访问。这个密钥应该只暴露给定义该存储的模块的守护者。
CommitKVStores通过其各自密钥的代理来声明,并在主应用程序文件中挂载在应用程序的multistore 上。在同一个文件中,密钥也被传递给负责管理存储的模块的守护者。
// BasicKVStore is a simple interface to get/set data
type BasicKVStore interface {
// Get returns nil if key doesn't exist. Panics on nil key.
Get(key []byte) []byte
// Has checks if a key exists. Panics on nil key.
Has(key []byte) bool
// Set sets the key. Panics on nil key or value.
Set(key, value []byte)
// Delete deletes the key. Panics on nil key.
Delete(key []byte)
}
// KVStore additionally provides iteration and deletion
type KVStore interface {
Store
BasicKVStore
// Iterator over a domain of keys in ascending order. End is exclusive.
// Start must be less than end, or the Iterator is invalid.
// Iterator must be closed by caller.
// To iterate over entire domain, use store.Iterator(nil, nil)
// CONTRACT: No writes may happen within a domain while an iterator exists over it.
// Exceptionally allowed for cachekv.Store, safe to write in the modules.
Iterator(start, end []byte) Iterator
// Iterator over a domain of keys in descending order. End is exclusive.
// Start must be less than end, or the Iterator is invalid.
// Iterator must be closed by caller.
// CONTRACT: No writes may happen within a domain while an iterator exists over it.
// Exceptionally allowed for cachekv.Store, safe to write in the modules.
ReverseIterator(start, end []byte) Iterator
}
除了传统的Get和Set方法,KVStore必须通过BasicKVStore接口来实现;KVStore必须提供一个Iterator(start, end)方法,返回一个Iterator对象。它被用来迭代一系列的键,通常是共享一个共同前缀的键。下面是一个来自银行模块keeper的例子,用于遍历所有账户余额:
// IterateAccountBalances iterates over the balances of a single account and
// provides the token balance to a callback. If true is returned from the
// callback, iteration is halted.
func (k BaseViewKeeper) IterateAccountBalances(ctx sdk.Context, addr sdk.AccAddress, cb func(sdk.Coin) bool) {
accountStore := k.getAccountStore(ctx, addr)
iterator := accountStore.Iterator(nil, nil)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var amount sdk.Int
if err := amount.Unmarshal(iterator.Value()); err != nil {
panic(err)
}
if cb(sdk.NewCoin(string(iterator.Key()), amount)) {
break
}
}
IAVL Store
baseapp中使用的KVStore和CommitKVStore的默认实现是iavl.Store。
// Store Implements types.KVStore and CommitKVStore.
type Store struct {
tree Tree
}
iavl存储是基于IAVL树,一个自平衡的二进制树,它保证:
- Get和Set操作是O(log n),其中n是树中元素的数量。
- 迭代有效地返回范围内的排序元素。
- 每个树的版本都是不可改变的,即使在提交后也可以检索到(取决于修剪设置)。
DbAdapter Store
dbadapter.Store是dbm.DB的适配器,使其实现KVStore 接口。
// Wrapper type for dbm.Db with implementation of KVStore
type Store struct {
dbm.DB
}
dbadapter.Store嵌入了dbm.DB,这意味着大部分KVStore的接口函数都被实现了。其他功能(主要是杂项)是手动实现的。这个商店主要用于暂存器中。
Transient Store
Transient.Store是一个底层的KVStore,在区块结束时被自动丢弃。
// Store is a wrapper for a MemDB with Commiter implementation
type Store struct {
dbadapter.Store
}
Transient.Store是一个带有dbm.NewMemDB()的dbadapter.Store。所有的KVStore方法都被重复使用。当Store.Commit()被调用时,一个新的dbadapter.Store被分配,丢弃以前的引用并使其被垃圾回收。
这种类型的存储对于保留那些只与每个块相关的信息是很有用的。一个例子是存储参数变化(即如果一个区块中的参数发生了变化,一个bool被设置为真)。
// Individual parameter store for each keeper
// Transient store persists for a block, so we use it for
// recording whether the parameter has been changed or not
type Subspace struct {
cdc codec.BinaryCodec
legacyAmino *codec.LegacyAmino
key storetypes.StoreKey // []byte -> []byte, stores parameter
tkey storetypes.StoreKey // []byte -> bool, stores parameter change
name []byte
table KeyTable
}
瞬态存储通常通过 TransientStore() 方法通过context访问:
// TransientStore fetches a TransientStore from the MultiStore.
func (c Context) TransientStore(key storetypes.StoreKey) KVStore {
return gaskv.NewStore(c.MultiStore().GetKVStore(key), c.GasMeter(), storetypes.TransientGasConfig())
}
KVStore包装器
CacheKVStore
cachekv.Store是一个包装器KVStore,它在底层KVStore上提供缓冲写入/缓存读取功能。
// it means the parent doesn't have the key. (No need to delete upon Write())
type Store struct {
mtx sync.Mutex
cache map[string]*cValue
deleted map[string]struct{}
unsortedCache map[string]struct{}
sortedCache *dbm.MemDB // always ascending sorted
parent types.KVStore
}
当一个IAWL存储需要被分支以创建隔离存储时,就会使用这种类型(通常当我们需要突变一个以后可能被恢复的状态时)。
Get
Store.Get()首先检查Store.cache是否有一个与键相关的值。如果该值存在,该函数将其返回。如果没有,该函数会调用Store.parent.Get(),将结果缓存在Store.cache中,并返回它。
Set
Store.Set()将键值对设置到Store.cache中。cValue有一个字段dirty bool,表示缓存的值是否与基础值不同。当Store.Set()缓存了一个新的键值对时,cValue.dirty被设置为true,所以当Store.Write()被调用时,它可以被写入底层存储。
迭代器
Store.Iterator()必须在缓存的项目和原始项目上进行遍历。在Store.iterator()中,为每个项目生成两个迭代器,并进行合并。 memIterator本质上是KVPairs的一个切片,用于缓存项目。mergeIterator是两个迭代器的组合,其中遍历发生在两个迭代器上。
GasKv Store
Cosmos SDK应用程序使用gas来跟踪资源的使用并防止垃圾邮件。GasKv.Store是一个KVStore包装器,它可以在每次对存储进行读或写时自动消耗气体。它是跟踪Cosmos SDK应用程序中存储使用情况的首选解决方案。
// Store applies gas tracking to an underlying KVStore. It implements the
// KVStore interface.
type Store struct {
gasMeter types.GasMeter
gasConfig types.GasConfig
parent types.KVStore
}
当调用父KVStore的方法时,GasKv.Store会根据Store.gasConfig自动消耗适当的gas:
// GasConfig defines gas cost for each operation on KVStores
type GasConfig struct {
HasCost Gas
DeleteCost Gas
ReadCostFlat Gas
ReadCostPerByte Gas
WriteCostFlat Gas
WriteCostPerByte Gas
IterNextCostFlat Gas
}
默认情况下,所有KVStore在检索时都包装在GasKv.Stores中。 这是在context的KVStore()方法中完成的:
// KVStore fetches a KVStore from the MultiStore.
func (c Context) KVStore(key storetypes.StoreKey) KVStore {
return gaskv.NewStore(c.MultiStore().GetKVStore(key), c.GasMeter(), storetypes.KVGasConfig())
}
在这种情况下,使用默认气体配置:
// KVGasConfig returns a default gas config for KVStores.
func KVGasConfig() GasConfig {
return GasConfig{
HasCost: 1000,
DeleteCost: 1000,
ReadCostFlat: 1000,
ReadCostPerByte: 3,
WriteCostFlat: 2000,
WriteCostPerByte: 30,
IterNextCostFlat: 30,
}
}
TraceKv Store
tracekv.Store是一个包装器KVStore,它提供了对底层KVStore的操作跟踪功能。 如果在父MultiStore 上启用了跟踪,则Cosmos SDK会在所有KVStore上自动应用它。
type (
// Store implements the KVStore interface with tracing enabled.
// Operations are traced on each core KVStore call and written to the
// underlying io.writer.
//
// TODO: Should we use a buffered writer and implement Commit on
// Store?
Store struct {
parent types.KVStore
writer io.Writer
context types.TraceContext
}
// operation represents an IO operation
operation string
// traceOperation implements a traced KVStore operation
traceOperation struct {
Operation operation `json:"operation"`
Key string `json:"key"`
Value string `json:"value"`
Metadata map[string]interface{} `json:"metadata"`
}
)
当调用每个KVStore方法时,tracekv.Store会自动将traceOperation记录到Store.writer。 traceOperation.Metadata不为nil时填充Store.context。TraceContext是一个map[string]interface{}。
Prefix Store
prefix.Store是一个包装器KVStore,它在底层KVStore 上提供自动键前缀功能。
// Store is similar with tendermint/tendermint/libs/db/prefix_db
// both gives access only to the limited subset of the store
// for convinience or safety
type Store struct {
parent types.KVStore
prefix []byte
}
当Store.{Get, Set}()被调用时,存储会将调用转发给它的父级,并在键上加上Store.prefix的前缀。
当Store.Iterator()被调用时,它不会简单地以Store.prefix为前缀,因为它并不像预期那样工作。在这种情况下,一些元素被遍历,即使它们不是以前缀开始的。
ListenKv.Store
listenkv.Store是一个封装的KVStore,它在底层KVStore上提供状态监听功能。它被Cosmos SDK自动应用于任何KVStore,其StoreKey在状态流配置期间被指定。关于状态流配置的其他信息可以在store/streaming/README.md找到。
// Store implements the KVStore interface with listening enabled.
// Operations are traced on each core KVStore call and written to any of the
// underlying listeners with the proper key and operation permissions
type Store struct {
parent types.KVStore
listeners []types.WriteListener
parentStoreKey types.StoreKey
}
当KVStore.Set或KVStore.Delete方法被调用时,listenkv.Store会自动将操作写入Store.listeners的集合中。
新的Store包(store/v2alpha1)
SDK正在过渡到使用这里列出的类型作为状态存储的默认接口。在撰写本文时,这些类型不能在应用程序中使用,并且与CommitMultiStore和相关类型不直接兼容。
这些类型使用Cosmos-SDK的新db子模块(github.com/cosmos/cosmos-sdk/db),而不是tmdb(github.com/tendermint/tm-db)。
BasicKVStore接口
只提供基本CRUD功能(Get、Set、Has和Delete方法)的接口,没有迭代或缓存。这被用来部分公开一个更大的存储空间的组件,例如一个root.Store。
MultiStore
这是主客户端存储的新接口(或者说接口集),取代了store/types.MultiStore(v1)的角色。与v1版相比,在行为上有一些明显的不同:
- 提交是原子性的,在整个存储状态上执行;单个子存储不能单独提交,也不能有不同的版本号。
- 存储器的当前版本和版本历史跟踪支持的db.Connection的版本。过去的版本只能以只读方式访问。
- 在初始化时定义了有效的子系统集合,并且不能在现有的存储实例中动态地更新。
CommitMultiStore
这是持久化应用状态的主要接口,类似于原来的CommitMultiStore。
- 过去的版本视图可以用GetVersion来访问,它返回一个BasicMultiStore。
- 用GetKVStore来访问子库。试图获取一个在初始化时没有定义的子存储将导致恐慌。
- 必须调用Close来释放被存储空间使用的DB资源。
BasicMultiStore
一个最小的接口,只允许访问子库。注意:由BasicMultiStore.GetKVStore返回的子存储是只读的,在调用Set或Delete时将会出现恐慌。
Implementation (root.Store)
MultiStore的典型实现是在store/v2alpha1/root。它在内部对状态存储和状态承诺进行了解耦:值被存储在支持的键值数据库中,并直接从该数据库中读取(状态存储,或SS),但也被映射到一个逻辑上独立的数据库中,该数据库生成加密证明(用于状态承诺或SC)。
每个子库的状态承诺部分被实现为一个独立的smt.Store。在内部,每个子库都被分配在同一个支持数据库的一个逻辑上独立的分区中,这样提交就适用于所有子库的状态。因此,过去版本的视图也包括所有子库的状态(包括SS和SC数据)。
可以选择将此存储配置为SC使用不同的后台数据库实例(例如,状态存储DB为badgerdb,状态提交DB为memdb。
SMT Store
store/v2alpha1/smt.Store将数值映射到稀疏默克尔树(SMT)中,并支持BasicKVStore接口以及用于生成加密证明的方法。
Encoding
虽然Cosmos SDK中的编码过去主要由go-amino编解码器处理,但Cosmos SDK正朝着使用gogoprotobuf进行状态和客户端编码的方向发展。
Cosmos SDK使用两种二进制线编码协议,Amino是一个对象编码规范和Protocol Buffers是Proto3的一个子集,有一个接口支持的扩展,Amino与Proto3基本兼容(但与Proto2不兼容)。
由于Amino有明显的性能缺陷,基于反射并且没有任何有意义的跨语言/客户端的支持,因此正在使用协议缓冲区,特别是gogoprotobuf,正在被用来代替Amino。请注意,这个使用Protocol Buffers而不是Amino的过程仍然是一个持续的过程。
Cosmos SDK中类型的二进制线编码可以分为两个主要类别,客户端编码和存储编码。客户端编码主要围绕交易处理和签名,而存储编码则围绕状态机转换中使用的类型以及最终存储在Merkle树中的内容。
对于存储编码,protobuf的定义可以存在于任何类型,通常会有一个基于Amino的“中间”类型。具体来说,基于protobuf的类型定义用于序列化和持久化,而基于Amino的类型则用于状态机中的业务逻辑,它们可以进行来回转换。注意,基于Amino的类型在未来可能会慢慢被淘汰,所以开发者应该注意尽可能使用protobuf的消息定义。
在编解码包中,存在两个核心接口:BinaryCodec和JSONCodec,其中前者封装了当前的Amino接口,只是它对实现后者的类型而不是通用的interface{}类型进行操作。
此外,Codec有两种实现方式。第一个是AminoCodec,其中二进制和JSON序列化都是通过Amino处理的。第二个是ProtoCodec,二进制和JSON的序列化都是通过Protobuf处理的。
这意味着模块可以使用Amino或Protobuf编码,但类型必须实现ProtoMarshaler。如果模块希望避免为他们的类型实现这个接口,他们可以直接使用Amino编解码器。
Amino
每个模块都使用一个Amino编解码器来序列化类型和接口。这个编解码器通常只在该模块的领域内注册类型和接口(如消息),但也有例外,如x/gov。每个模块都公开了一个RegisterLegacyAminoCodec函数,允许用户提供一个编解码器并注册所有的类型。应用程序将为每个必要的模块调用这个方法。
如果一个模块没有基于protobuf的类型定义(见下文),Amino就会被用来对原始的线型字节进行编码和解码,以达到具体的类型或接口:
bz := keeper.cdc.MustMarshal(typeOrInterface) keeper.cdc.MustUnmarshal(bz, &typeOrInterface)
注意,上述功能有长度前缀的变体,这通常用于数据需要流化或分组的时候(例如ResponseDeliverTx.Data)。
Authz授权
由于MsgExec消息类型可以包含不同的消息实例,所以开发者必须在其模块的codec.go文件的init方法内添加以下代码:
import authzcodec "github.com/cosmos/cosmos-sdk/x/authz/codec"
init() {
// Register all Amino interfaces and concrete types on the authz Amino codec so that this can later be
// used to properly serialize MsgGrant and MsgExec instances
RegisterLegacyAminoCodec(authzcodec.Amino)
}
这将允许x/authz模块使用Amino正确地序列化和去序列化MsgExec实例,这在使用Ledger签署这种消息时是必需的。
Gogoproto
我们鼓励模块为其各自的类型利用Protobuf编码。在Cosmos SDK中,我们使用了Gogoproto(打开新窗口)对Protobuf规范的具体实现,与谷歌官方的protobuf实现相比,它提供了速度和DX的改进。
关于protobuf消息定义的准则
除了遵循官方的Protocol Buffer指南外,我们建议在处理接口时在.proto文件中使用这些注释:
- 使用cosmos_proto.accepts_interface来注解接受接口的字段
- 将与protoName相同的完全限定名称传递给InterfaceRegistry.RegisterInterface
- 用cosmos_proto. implements_interface来注解接口实现。
- 将与protoName相同的全称传递给InterfaceRegistry.RegisterInterface。
交易编码
Protobuf的另一个重要用途是对事务进行编码和解码。交易是由应用程序或Cosmos SDK定义的,但随后被传递给底层的共识引擎以转发给其他对等方。由于底层共识引擎与应用无关,共识引擎只接受原始字节形式的交易。
- TxEncoder对象执行编码。
- TxDecoder对象执行解码。
// TxDecoder unmarshals transaction bytes type TxDecoder func(txBytes []byte) (Tx, error) // TxEncoder marshals transaction to bytes type TxEncoder func(tx Tx) ([]byte, error)
这两个对象的标准实现都可以在auth模块中找到:
package tx
import (
"fmt"
"google.golang.org/protobuf/encoding/protowire"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/codec/unknownproto"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/tx"
)
// DefaultTxDecoder returns a default protobuf TxDecoder using the provided Marshaler.
func DefaultTxDecoder(cdc codec.ProtoCodecMarshaler) sdk.TxDecoder {
return func(txBytes []byte) (sdk.Tx, error) {
// Make sure txBytes follow ADR-027.
err := rejectNonADR027TxRaw(txBytes)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrTxDecode, err.Error())
}
var raw tx.TxRaw
// reject all unknown proto fields in the root TxRaw
err = unknownproto.RejectUnknownFieldsStrict(txBytes, &raw, cdc.InterfaceRegistry())
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrTxDecode, err.Error())
}
err = cdc.Unmarshal(txBytes, &raw)
if err != nil {
return nil, err
}
var body tx.TxBody
// allow non-critical unknown fields in TxBody
txBodyHasUnknownNonCriticals, err := unknownproto.RejectUnknownFields(raw.BodyBytes, &body, true, cdc.InterfaceRegistry())
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrTxDecode, err.Error())
}
err = cdc.Unmarshal(raw.BodyBytes, &body)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrTxDecode, err.Error())
}
var authInfo tx.AuthInfo
// reject all unknown proto fields in AuthInfo
err = unknownproto.RejectUnknownFieldsStrict(raw.AuthInfoBytes, &authInfo, cdc.InterfaceRegistry())
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrTxDecode, err.Error())
}
err = cdc.Unmarshal(raw.AuthInfoBytes, &authInfo)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrTxDecode, err.Error())
}
theTx := &tx.Tx{
Body: &body,
AuthInfo: &authInfo,
Signatures: raw.Signatures,
}
return &wrapper{
tx: theTx,
bodyBz: raw.BodyBytes,
authInfoBz: raw.AuthInfoBytes,
txBodyHasUnknownNonCriticals: txBodyHasUnknownNonCriticals,
}, nil
}
}
// DefaultJSONTxDecoder returns a default protobuf JSON TxDecoder using the provided Marshaler.
func DefaultJSONTxDecoder(cdc codec.ProtoCodecMarshaler) sdk.TxDecoder {
return func(txBytes []byte) (sdk.Tx, error) {
var theTx tx.Tx
err := cdc.UnmarshalJSON(txBytes, &theTx)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrTxDecode, err.Error())
}
return &wrapper{
tx: &theTx,
}, nil
}
}
// rejectNonADR027TxRaw rejects txBytes that do not follow ADR-027. This is NOT
// a generic ADR-027 checker, it only applies decoding TxRaw. Specifically, it
// only checks that:
// - field numbers are in ascending order (1, 2, and potentially multiple 3s),
// - and varints are as short as possible.
// All other ADR-027 edge cases (e.g. default values) are not applicable with
// TxRaw.
func rejectNonADR027TxRaw(txBytes []byte) error {
// Make sure all fields are ordered in ascending order with this variable.
prevTagNum := protowire.Number(0)
for len(txBytes) > 0 {
tagNum, wireType, m := protowire.ConsumeTag(txBytes)
if m < 0 {
return fmt.Errorf("invalid length; %w", protowire.ParseError(m))
}
// TxRaw only has bytes fields.
if wireType != protowire.BytesType {
return fmt.Errorf("expected %d wire type, got %d", protowire.BytesType, wireType)
}
// Make sure fields are ordered in ascending order.
if tagNum < prevTagNum {
return fmt.Errorf("txRaw must follow ADR-027, got tagNum %d after tagNum %d", tagNum, prevTagNum)
}
prevTagNum = tagNum
// All 3 fields of TxRaw have wireType == 2, so their next component
// is a varint, so we can safely call ConsumeVarint here.
// Byte structure: <varint of bytes length><bytes sequence>
// Inner fields are verified in `DefaultTxDecoder`
lengthPrefix, m := protowire.ConsumeVarint(txBytes[m:])
if m < 0 {
return fmt.Errorf("invalid length; %w", protowire.ParseError(m))
}
// We make sure that this varint is as short as possible.
n := varintMinLength(lengthPrefix)
if n != m {
return fmt.Errorf("length prefix varint for tagNum %d is not as short as possible, read %d, only need %d", tagNum, m, n)
}
// Skip over the bytes that store fieldNumber and wireType bytes.
_, _, m = protowire.ConsumeField(txBytes)
if m < 0 {
return fmt.Errorf("invalid length; %w", protowire.ParseError(m))
}
txBytes = txBytes[m:]
}
return nil
}
// varintMinLength returns the minimum number of bytes necessary to encode an
// uint using varint encoding.
func varintMinLength(n uint64) int {
switch {
// Note: 1<<N == 2**N.
case n < 1<<(7):
return 1
case n < 1<<(7*2):
return 2
case n < 1<<(7*3):
return 3
case n < 1<<(7*4):
return 4
case n < 1<<(7*5):
return 5
case n < 1<<(7*6):
return 6
case n < 1<<(7*7):
return 7
case n < 1<<(7*8):
return 8
case n < 1<<(7*9):
return 9
default:
return 10
}
}
package tx
import (
"fmt"
"github.com/gogo/protobuf/proto"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
txtypes "github.com/cosmos/cosmos-sdk/types/tx"
)
// DefaultTxEncoder returns a default protobuf TxEncoder using the provided Marshaler
func DefaultTxEncoder() sdk.TxEncoder {
return func(tx sdk.Tx) ([]byte, error) {
txWrapper, ok := tx.(*wrapper)
if !ok {
return nil, fmt.Errorf("expected %T, got %T", &wrapper{}, tx)
}
raw := &txtypes.TxRaw{
BodyBytes: txWrapper.getBodyBytes(),
AuthInfoBytes: txWrapper.getAuthInfoBytes(),
Signatures: txWrapper.tx.Signatures,
}
return proto.Marshal(raw)
}
}
// DefaultJSONTxEncoder returns a default protobuf JSON TxEncoder using the provided Marshaler.
func DefaultJSONTxEncoder(cdc codec.ProtoCodecMarshaler) sdk.TxEncoder {
return func(tx sdk.Tx) ([]byte, error) {
txWrapper, ok := tx.(*wrapper)
if ok {
return cdc.MarshalJSON(txWrapper.tx)
}
protoTx, ok := tx.(*txtypes.Tx)
if ok {
return cdc.MarshalJSON(protoTx)
}
return nil, fmt.Errorf("expected %T, got %T", &wrapper{}, tx)
}
}
接口编码和Any的使用
Protobuf DSL是强类型的,这可能使插入变量类型的字段变得困难。想象一下,我们想创建一个Profile protobuf消息,作为账户的包装器:
message Profile {
// account is the account associated to a profile.
cosmos.auth.v1beta1.BaseAccount account = 1;
// bio is a short description of the account.
string bio = 4;
}
在这个简介的例子中,我们把账户硬编码为BaseAccount。然而,还有几种与归属有关的用户账户类型,如BaseVestingAccount或ContinuousVestingAccount。所有这些账户都是不同的,但它们都实现了AccountI接口。你将如何创建一个允许所有这些类型的账户的Profile,并有一个接受AccountI接口的账户字段?
// AccountI is an interface used to store coins at a given address within state.
// It presumes a notion of sequence numbers for replay protection,
// a notion of account numbers for replay protection for previously pruned accounts,
// and a pubkey for authentication purposes.
//
// Many complex conditions can be used in the concrete struct which implements AccountI.
type AccountI interface {
proto.Message
GetAddress() sdk.AccAddress
SetAddress(sdk.AccAddress) error // errors if already set.
GetPubKey() cryptotypes.PubKey // can return nil.
SetPubKey(cryptotypes.PubKey) error
GetAccountNumber() uint64
SetAccountNumber(uint64) error
GetSequence() uint64
SetSequence(uint64) error
// Ensure that account implements stringer
String() string
}
在ADR-019中,已决定使用Any来编码protobuf中的接口。Any包含了一个任意的序列化消息的字节,以及一个作为全局唯一标识符的URL,并可解析为该消息的类型。这个策略允许我们在protobuf消息中打包任意的Go类型。我们的新配置文件如下所示:
message Profile {
// account is the account associated to a profile.
google.protobuf.Any account = 1 [
(cosmos_proto.accepts_interface) = "AccountI"; // Asserts that this field only accepts Go types implementing `AccountI`. It is purely informational for now.
];
// bio is a short description of the account.
string bio = 4;
}
要在配置文件中添加帐户,我们需要首先使用codectypes.NewAnyWithValue将其“打包”到Any中:
var myAccount AccountI
myAccount = ... // Can be a BaseAccount, a ContinuousVestingAccount or any struct implementing `AccountI`
// Pack the account into an Any
accAny, err := codectypes.NewAnyWithValue(myAccount)
if err != nil {
return nil, err
}
// Create a new Profile with the any.
profile := Profile {
Account: accAny,
Bio: "some bio",
}
// We can then marshal the profile as usual.
bz, err := cdc.Marshal(profile)
jsonBz, err := cdc.MarshalJSON(profile)
总而言之,要对接口进行编码,必须1/将接口打包成Any和2/编组Any。为方便起见,Cosmos SDK提供了一个MarshalInterface方法来捆绑这两个步骤。
从Any内部检索具体Go类型的反向操作称为“解包”,由Any上的GetCachedValue()完成。
profileBz := ... // The proto-encoded bytes of a Profile, e.g. retrieved through gRPC.
var myProfile Profile
// Unmarshal the bytes into the myProfile struct.
err := cdc.Unmarshal(profilebz, &myProfile)
// Let's see the types of the Account field.
fmt.Printf("%T\n", myProfile.Account) // Prints "Any"
fmt.Printf("%T\n", myProfile.Account.GetCachedValue()) // Prints "BaseAccount", "ContinuousVestingAccount" or whatever was initially packed in the Any.
// Get the address of the accountt.
accAddr := myProfile.Account.GetCachedValue().(AccountI).GetAddress()
请务必注意,要使GetCachedValue()起作用,Profile(以及嵌入Profile的任何其他结构)必须实现UnpackInterfaces 方法:
func (p *Profile) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error {
if p.Account != nil {
var account AccountI
return unpacker.UnpackAny(p.Account, &account)
}
return nil
}
UnpackInterfaces在所有实现此方法的结构上被递归调用,以允许所有Anys正确地填充它们的GetCachedValue()。
Any编码在Cosmos SDK中的应用
上面的Profile例子是用于教育目的虚构的例子。在Cosmos SDK中,在一些地方使用Any编码(非详尽列表)。
- cryptotypes.PubKey接口用于编码不同类型的公钥。
- sdk.Msg接口用于编码一个交易中的不同Msgs。
- AccountI接口,用于编码x/auth查询响应中的不同类型的账户(类似于上面的例子)。
- Evidencei接口,用于编码x/evidence模块中的不同类型的证据。
- AuthorizationI接口,用于编码不同类型的x/authz授权。
- Validator结构,包含验证器的信息。
在x/staking中,将pubkey编码为Validator结构内的Any的真实例子显示在以下例子中:
// NewValidator constructs a new Validator
//nolint:interfacer
func NewValidator(operator sdk.ValAddress, pubKey cryptotypes.PubKey, description Description) (Validator, error) {
pkAny, err := codectypes.NewAnyWithValue(pubKey)
if err != nil {
return Validator{}, err
}
return Validator{
OperatorAddress: operator.String(),
ConsensusPubkey: pkAny,
Jailed: false,
Status: Unbonded,
Tokens: sdk.ZeroInt(),
DelegatorShares: sdk.ZeroDec(),
Description: description,
UnbondingHeight: int64(0),
UnbondingTime: time.Unix(0, 0).UTC(),
Commission: NewCommission(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()),
MinSelfDelegation: sdk.OneInt(),
}, nil
}
如何将模块更新为protobuf编码
如果模块不包含任何接口(例如账户或内容),那么它们可以简单地将任何通过其具体的Amino编解码器进行编码和持久化的现有类型迁移到Protobuf,并接受一个Marshaler作为编解码器,该编解码器通过ProtoCodec实现,无需进一步定制。
然而,如果一个模块类型组成了一个接口,它必须将其包裹在sdk.Any(来自/types包)类型中。要做到这一点,一个模块级的.proto文件必须使用google.protobuf.Any,用于各自的消息类型接口类型。
例如,在x/evidence模块中定义了一个Evidence接口,它被MsgSubmitEvidence使用。该结构定义必须使用sdk.Any来包装证据文件。在proto文件中,我们对其定义如下:
// proto/cosmos/evidence/v1beta1/tx.proto
message MsgSubmitEvidence {
string submitter = 1;
google.protobuf.Any evidence = 2 [(cosmos_proto.accepts_interface) = "Evidence"];
}
Cosmos SDK codec.Codec接口提供了支持方法MarshalInterface和UnmarshalInterface以方便将状态编码为Any。
模块应该使用InterfaceRegistry来注册接口,它提供了一种注册接口的机制。RegisterInterface(protoName string, iface interface{}, impls ...proto.Message)和实现。RegisterImplementations(iface interface{}, impls ...proto.Message),可以安全地从Any解压,类似于用Amino注册类型。
// InterfaceRegistry provides a mechanism for registering interfaces and
// implementations that can be safely unpacked from Any
type InterfaceRegistry interface {
AnyUnpacker
jsonpb.AnyResolver
// RegisterInterface associates protoName as the public name for the
// interface passed in as iface. This is to be used primarily to create
// a public facing registry of interface implementations for clients.
// protoName should be a well-chosen public facing name that remains stable.
// RegisterInterface takes an optional list of impls to be registered
// as implementations of iface.
//
// Ex:
// registry.RegisterInterface("cosmos.base.v1beta1.Msg", (*sdk.Msg)(nil))
RegisterInterface(protoName string, iface interface{}, impls ...proto.Message)
// RegisterImplementations registers impls as concrete implementations of
// the interface iface.
//
// Ex:
// registry.RegisterImplementations((*sdk.Msg)(nil), &MsgSend{}, &MsgMultiSend{})
RegisterImplementations(iface interface{}, impls ...proto.Message)
// ListAllInterfaces list the type URLs of all registered interfaces.
ListAllInterfaces() []string
// ListImplementations lists the valid type URLs for the given interface name that can be used
// for the provided interface type URL.
ListImplementations(ifaceTypeURL string) []string
}
此外,应该在反序列化中引入一个UnpackInterfaces阶段,以便在需要接口之前对其进行解包。直接或通过其成员之一包含 protobuf Any 的 Protobuf 类型应该实现 UnpackInterfacesMessage 接口:
type UnpackInterfacesMessage interface {
UnpackInterfaces(InterfaceUnpacker) error
}
原文链接:https://docs.cosmos.network/main/core/store.html
https://docs.cosmos.network/main/core/encoding.html
---煤油灯科技victorlamp.com翻译整理---