剖析Cosmos SDK(七):核心概念(Store、Encoding)

煤油灯科技2022-07-15 09:01

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翻译整理---

你可能也喜欢这些文章

科技互联网,科技,资讯,动态,洞察,量子,计算,AI,人工智能,机器人,区块链,Web3,分布式,操作系统,OS,芯片,视频,深度,论文