order |
---|
5 |
A store is a data structure that holds the state of the application.
The Cosmos SDK comes with a large set of stores to persist the state of applications. By default, the main store of SDK applications is a multistore
, i.e. a store of stores. Developers can add any number of key-value stores to the multistore, depending on their application needs. The multistore exists to support the modularity of the Cosmos SDK, as it lets each module declare and manage their own subset of the state. Key-value stores in the multistore can only be accessed with a specific capability key
, which is typically held in the keeper
of the module that declared the store.
+-----------------------------------------------------+
| |
| +--------------------------------------------+ |
| | | |
| | 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
At its very core, a Cosmos SDK store
is an object that holds a CacheWrapper
and implements a GetStoreType()
method:
type Store interface {
GetStoreType() StoreType
CacheWrapper
}
The GetStoreType
is a simple method that returns the type of store, whereas a CacheWrapper
is a simple interface that specifies cache-wrapping and Write
methods:
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
}
type CacheWrapper interface {
// CacheWrap cache wraps.
CacheWrap() CacheWrap
// CacheWrapWithTrace cache wraps with tracing enabled.
CacheWrapWithTrace(w io.Writer, tc TraceContext) CacheWrap
}
Cache-wrapping is used ubiquitously in the Cosmos SDK and required to be implemented on every store type. A cache-wrapper creates a light snapshot of a store that can be passed around and updated without affecting the main underlying store. This is used to trigger temporary state-transitions that may be reverted later should an error occur. If a state-transition sequence is performed without issue, the cached store can be comitted to the underlying store at the end of the sequence.
A commit store is a store that has the ability to commit changes made to the underlying tree or db. The Cosmos SDK differentiates simple stores from commit stores by extending the basic store interfaces with a Committer
:
// Stores of MultiStore must implement CommitStore.
type CommitStore interface {
Committer
Store
}
The Committer
is an interface that defines methods to persist changes to disk:
// something that can persist to disk
type Committer interface {
Commit() CommitID
LastCommitID() CommitID
SetPruning(PruningOptions)
}
The CommitID
is a deterministic commit of the state tree. Its hash is returned to the underlying consensus engine and stored in the block header. Note that commit store interfaces exist for various purposes, one of which is to make sure not every object can commit the store. As part of the object-capabilities model of the Cosmos SDK, only baseapp
should have the ability to commit stores. For example, this is the reason why the ctx.KVStore()
method by which modules typically access stores returns a KVStore
and not a CommitKVStore
.
The Cosmos SDK comes with many types of stores, the most used being CommitMultiStore
, KVStore
and GasKv
store. Other types of stores include Transient
and TraceKV
stores.
Each Cosmos SDK application holds a multistore at its root to persist its state. The multistore is a store of KVStores
that follows the Multistore
interface:
type MultiStore interface {
Store
// Cache wrap MultiStore.
// NOTE: Caller should probably not call .Write() on each, but
// call CacheMultiStore.Write().
CacheMultiStore() CacheMultiStore
// CacheMultiStoreWithVersion cache-wraps 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
}
If tracing is enabled, then cache-wrapping the multistore will wrap all the underlying KVStore
in TraceKv.Store
before caching them.
The main type of Multistore
used in the Cosmos SDK is CommitMultiStore
, which is an extension of the Multistore
interface:
// A non-cache MultiStore.
type CommitMultiStore interface {
Committer
MultiStore
// 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)
}
As for concrete implementation, the rootMulti.Store
is the go-to implementation of the CommitMultiStore
interface. The rootMulti.Store
is a base-layer multistore built around a db
on top of which multiple KVStores
can be mounted, and is the default multistore store used in baseapp
.
Whenever the rootMulti.Store
needs to be cached-wrapped, a cachemulti.Store
is used.
type Store struct {
db types.CacheKVStore
stores map[types.StoreKey] types.CacheWrap
}
cachemulti.Store
cache wraps all substores in its constructor and hold them in Store.stores
. Store.GetKVStore()
returns the store from Store.stores
, and Store.Write()
recursively calls CacheWrap.Write()
on all the substores.
A KVStore
is a simple key-value store used to store and retrieve data. A CommitKVStore
is a KVStore
that also implements a Committer
. By default, stores mounted in baseapp
's main CommitMultiStore
are CommitKVStore
s. The KVStore
interface is primarily used to restrict modules from accessing the committer.
Individual KVStore
s are used by modules to manage a subset of the global state. KVStores
can be accessed by objects that hold a specific key. This key
should only be exposed to the keeper
of the module that defines the store.
CommitKVStore
s are declared by proxy of their respective key
and mounted on the application's multistore in the main application file. In the same file, the key
is also passed to the module's keeper
that is responsible for managing the store.
type KVStore interface {
Store
// Get returns nil iff 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)
// 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
}
// Stores of MultiStore must implement CommitStore.
type CommitKVStore interface {
Committer
KVStore
}
Apart from the traditional Get
and Set
methods, a KVStore
is expected to implement an Iterator()
method which returns an Iterator
object. The Iterator()
method is used to iterate over a domain of keys, typically keys that share a common prefix. Here is a common pattern of using an Iterator
that might be found in a module's keeper
:
store := ctx.KVStore(keeper.storeKey)
iterator := sdk.KVStorePrefixIterator(store, prefix) // proxy for store.Iterator
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var object types.Object
keeper.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &object)
if cb(object) {
break
}
}
The default implementation of KVStore
and CommitKVStore
used in baseapp
is the iavl.Store
. iavl
stores are based around an IAVL Tree, a self-balancing binary tree which guarantees that:
Get
andSet
operations are O(log n), where n is the number of elements in the tree.- Iteration efficiently returns the sorted elements within the range.
- Each tree version is immutable and can be retrieved even after a commit (depending on the pruning settings).
dbadapter.Store
is a adapter for dbm.DB
making it fulfilling the KVStore
interface.
type Store struct {
dbm.DB
}
dbadapter.Store
embeds dbm.DB
, meaning most of the KVStore
interface functions are implemented. The other functions (mostly miscellaneous) are manually implemented. This store is primarily used within Transient Stores
Transient.Store
is a base-layer KVStore
which is automatically discarded at the end of the block.
type Store struct {
dbadapter.Store
}
Transient.Store
is a dbadapter.Store
with a dbm.NewMemDB()
. All KVStore
methods are reused. When Store.Commit()
is called, a new dbadapter.Store
is assigned, discarding previous reference and making it garbage collected.
This type of store is useful to persist information that is only relevant per-block. One example would be to store parameter changes (i.e. a bool set to true
if a parameter changed in a block).
Transient stores are typically accessed via the context
via the TransientStore()
method:
// TransientStore fetches a TransientStore from the MultiStore.
func (c Context) TransientStore(key StoreKey) KVStore {
return gaskv.NewStore(c.MultiStore().GetKVStore(key), c.GasMeter(), stypes.TransientGasConfig())
}
cachekv.Store
is a wrapper KVStore
which provides buffered writing / cached reading functionalities over the underlying KVStore
.
type Store struct {
cache map[string]cValue
parent types.KVStore
}
This is the type used whenever an IAVL Store needs to be cache-wrapped (typically when setting value that might be reverted later).
Store.Get()
checks Store.cache
first in order to find if there is any cached value associated with the key. If the value exists, the function returns it. If not, the function calls Store.parent.Get()
, sets the key-value pair to the Store.cache
, and returns it.
Store.Set()
sets the key-value pair to the Store.cache
. cValue
has the field dirty bool which indicates whether the cached value is different from the underlying value. When Store.Set()
cache new pair, the cValue.dirty
is set true
so when Store.Write()
is called it can be written to the underlying store.
Store.Iterator()
have to traverse on both caches items and the original items. In Store.iterator()
, two iterators are generated for each of them, and merged. memIterator
is essentially a slice of the KVPairs
, used for cached items. mergeIterator
is a combination of two iterators, where traverse happens ordered on both iterators.
Cosmos SDK applications use gas
to track resources usage and prevent spam. GasKv.Store
is a KVStore
wrapper that enables automatic gas consumption each time a read or write to the store is made. It is the solution of choice to track storage usage in Cosmos SDK applications.
type Store struct {
gasMeter types.GasMeter
gasConfig types.GasConfig
parent types.KVStore
}
When methods of the parent KVStore
are called, GasKv.Store
automatically consumes appropriate amount of gas depending on the Store.gasConfig
:
type GasConfig struct {
HasCost Gas
DeleteCost Gas
ReadCostFlat Gas
ReadCostPerByte Gas
WriteCostFlat Gas
WriteCostPerByte Gas
IterNextCostFlat Gas
}
By default, all KVStores
are wrapped in GasKv.Stores
when retrieved. This is done in the KVStore()
method of the context
:
// KVStore fetches a KVStore from the MultiStore.
func (c Context) KVStore(key StoreKey) KVStore {
return gaskv.NewStore(c.MultiStore().GetKVStore(key), c.GasMeter(), stypes.KVGasConfig())
}
In this case, the default gas configuration is used:
func KVGasConfig() GasConfig {
return GasConfig{
HasCost: 1000,
DeleteCost: 1000,
ReadCostFlat: 1000,
ReadCostPerByte: 3,
WriteCostFlat: 2000,
WriteCostPerByte: 30,
IterNextCostFlat: 30,
}
}
tracekv.Store
is a wrapper KVStore
which provides operation tracing functionalities over the underlying KVStore
. It is applied automatically by the Cosmos SDK on all KVStore
if tracing is enabled on the parent MultiStore
.
type Store struct {
parent types.KVStore
writer io.Writer
context types.TraceContext
}
When each KVStore
methods are called, tracekv.Store
automatically logs traceOperation
to the Store.writer
.
type traceOperation struct {
Operation operation
Key string
Value string
Metadata map[string]interface{}
}
traceOperation.Metadata
is filled with Store.context
when it is not nil. TraceContext
is a map[string]interface{}
.
prefix.Store
is a wrapper KVStore
which provides automatic key-prefixing functionalities over the underlying KVStore
.
type Store struct {
parent types.KVStore
prefix []byte
}
When Store.{Get, Set}()
is called, the store forwards the call to its parent, with the key prefixed with the Store.prefix
.
When Store.Iterator()
is called, it does not simply prefix the Store.prefix
, since it does not work as intended. In that case, some of the elements are traversed even they are not starting with the prefix.
Learn about encoding.