在上一节中,你已经了解到模块定义业务逻辑,而 keeper 负责读取和写入模块状态。本页将解释这些状态究竟是如何被存储、提交,并在整个网络中变得可验证的。

什么是状态?

状态是区块链的持久化数据:账户余额、委托、治理提案、模块参数,以及其他任何能够在区块之间保留下来的数据。当一笔交易执行时,模块会更新状态。当一个区块被提交时,更新后的状态就会成为下一个区块的起点:
状态0
  ↓ 应用区块 1
状态1
  ↓ 应用区块 2
状态2

KVStore 模型

在最底层,Cosmos SDK 将状态存储为键值对。键和值都是字节数组。模块使用 Protocol Buffers 将结构化数据编码为这些字节,并在读取时再将其解码回来。 关于模块如何将数据序列化为字节的细节,请参见 编码与 Protobuf。 下面的示例展示了 bank 模块如何存储余额的一个概念性示例:
key:   0x2 | len(address) | address_bytes | denom_bytes
value: ProtocolBuffer(amount)


# example
key:   0x2 | 20 | cosmos1abc...xyz | uatom
value: ProtocolBuffer(1000000)
键编码了存储前缀、地址长度、地址以及币种。值则是经过 Protocol Buffer 编码的金额。实际实现请参见 x/bank/types/keys.go。 每个模块在键值存储中都拥有自己的命名空间。键由模块定义,通常以一个字节前缀开头,用于将它们与其他模块的键区分开来。

多重存储(Multistore)

单个模块存储只是整体图景的一部分。在应用层,所有模块存储会一起提交。 每个模块都有自己的 KVStore,所有模块存储都会挂载到一个 multistore 中,并作为单一状态根一起提交。 模块只能通过自己的 keeper 读取和写入自身的存储。访问由 StoreKey 控制,它是在应用启动时注册的一种带类型能力对象。未持有该 key 的模块无法打开这个存储。 这种隔离遵循对象能力模型:
  1. 模块不能直接修改另一个模块的状态
  2. 跨模块交互必须通过暴露出来的 keeper 方法进行
当一个区块执行结束时,multistore 会计算出一个新的根哈希(app hash),它代表整个应用状态。这个哈希会返回给 CometBFT,被写入区块头中,并使链的状态具备可验证性。交易生命周期 解释了这个 app hash 在何处生成和提交。 完整的存储栈从上到下如下:
模块 keeper
    ↓
KVStore(带命名空间,并包装 gas/trace)
    ↓
CommitMultiStore(multistore,计算 app hash)
    ↓
IAVL 树(带版本的 Merkle 树)
    ↓
数据库后端(默认为 goleveldb)

状态如何存储(IAVL 与 commit store)

每个模块的 KVStore 都由一个 CommitKVStore 支撑。更多细节请参见存储规范。 在这里描述的当前 SDK 存储实现中,Cosmos SDK 使用的是 IAVL,这是一种带版本的 AVL Merkle 树。 IAVL 为树中的每次读写都提供 O(log n) 复杂度,这意味着读取或写入一个键所花费的时间取决于树的高度,而不是键的总数量。它还会在每次区块提交时为状态建立版本,并生成确定性的根哈希,以便为轻客户端生成 Merkle 证明。 每次区块提交都会生成一个新的树版本和新的根哈希:
区块 1                    区块 2                    区块 3

       [根 h1]                    [根 h2]                    [根 h3]
        /      \                     /      \                     /      \
  [branch1] [branch2]         [branch1] [branch2']         [branch1] [branch2'']
    /  \      /  \              /  \      /   \              /  \      /    \
  [a]  [b]  [c]  [d]         [a]  [b]  [c]   [d']         [a]  [b]   [c']  [d']
                                               ↑                       ↑
                                            (已更新)               (已更新)

// branch1 和 branch2 是内部节点(它们存储的是哈希,不是数据)。
// 叶子节点(a、b、c、d)才是真实的键值条目。
// 当某个叶子发生变化时,只有通往根节点路径上的节点会被重写(以 ' 标记)。
// 未修改的子树(branch1、a、b)会在这三个版本之间共享。
只有被修改的节点会被重写,未变化的节点会在不同版本之间共享。只要任意叶子发生变化,根哈希就会变化。所有验证者都必须计算出相同的根哈希;如果结果不一致,共识就会停止。 正因如此,状态转换必须是确定性的,编码必须是确定性的,交易排序也必须保持一致。

应用哈希(app hash)

app hash 是应用已提交状态的密码学根哈希。它通过 CommitMultiStore 汇总所有模块存储。由于每个验证者都会以确定性的方式执行相同的状态转换,因此对于同一个区块,他们都应计算出相同的 app hash。

数据库后端

IAVL 树不会把数据存储在内存中。它会把带版本的节点写入数据库后端,而数据库后端本身是一个磁盘上的键值存储。 Cosmos SDK 使用 CometBFT 的 db 包 对数据库实现做抽象。默认后端是 goleveldb。其他受支持的后端还包括 PebbleDB、RocksDB 和 memDB(内存型,用于测试)。 数据库后端在节点启动时选择,并在 app.toml 中配置。应用代码永远不会直接与它交互;这层边界由 store 层负责管理。

SDK 中的存储类型

除了基础的 KVStore 之外,SDK 还提供了若干专门的存储包装器。

CommitKVStore (persistent store)

CommitKVStore 是由 IAVL 支撑的主要持久化存储。它会跨区块持久保留、生成带版本的提交,并参与 app hash 的计算。

CacheMultiStore (transaction isolation)

在执行每笔交易之前,Cosmos SDK 的 BaseApp 都会创建一个 CacheMultiStore;它是 multistore 的一个带缓存、写时复制视图。 该交易期间的所有写操作都会发生在这个缓存层中:
Multistore
   ↓ CacheWrap(每笔交易)
   ↓ 执行 tx
   → 成功 → 提交变更
   → 失败 → 丢弃
  • 如果交易成功,变更会写入底层存储。
  • 如果交易失败,缓存会被丢弃,不会提交任何状态变更。
这就是存储层实现交易原子性的方式。

Ephemeral store types

Transient store 会在每个区块结束时清空。它们用于存放每区块临时数据,例如计数器或中间计算结果,并且不会影响 app hash。 Memory store 会跨越区块提交而保留,但在节点重启时会重置。它们的 Commit() 是空操作,数据永远不会写入磁盘。它们用于在进程内缓存那些每个区块重新计算代价较高、但又不需要在重启后保留的数据。模块通过 MemoryStoreKey 访问它们,并在 app.go 中通过 MountMemoryStores 挂载。
存储类型区块提交后保留重启后保留
Transient否(每个区块结束时清空)否
Memory是否
IAVL(CommitKVStore)是是

Gas and trace store wrappers

所有存储访问都会通过 GasKVStore 和 TraceKVStore 这两个包装器附加额外行为。
  • GasKVStore 会为每次读写收取 gas
  • TraceKVStore 会记录每次存储操作以便调试
由于状态访问是交易执行中的主要成本,SDK 会在存储层收取 gas,使高成本的读写能够反映到交易手续费中。KVStore 的每次读写都会消耗 gas,而成本更高的操作自然会消耗更多。执行上下文、Gas 与事件 解释了运行时 gas 计量的工作方式。

Prefix store

prefix store 会包装一个 KVStore,并自动为每个键添加一个固定的字节前缀。这样 keeper 就可以将读写范围限定在某个子命名空间中,而不必在每次调用时手动构造带前缀的键。
prefixStore := prefix.NewStore(kvStore, types.KeyPrefix("balances"))
prefixStore.Set(key, value) // stored as "balances" + key
这就是模块如何避免在自身存储内部发生键冲突的。

Collections API(类型化状态访问)

在 Cosmos SDK 中,模块通常会使用 collections API 来定义类型化的状态访问。 模块不会手动构造字节键,而是定义如下这类带类型的 collection:
  • collections.Item[T]
  • collections.Map[K, V]
  • collections.Sequence
示例:
// 在 keeper 结构体中声明
Counter collections.Item[uint64]

// 在消息处理器中读写
count, _ := k.Counter.Get(ctx)
k.Counter.Set(ctx, count+1)
Collections API 定义存储 schema,处理编码与解码,确保键构造一致,并让状态访问具备类型安全性。 在底层,collection 仍然把数据存储在 KVStore 中。使用 collection,是为了在原始字节键之上提供一层更安全的抽象。基础接口定义请参见 collections/collections.go。完整的包指南请参见 Collections。

模块如何访问状态

模块不会直接与 multistore 交互。相反,每个模块都会定义一个 keeper,并通过每次调用时收到的执行 Context 来打开自己的 KVStore。关于 Context 如何在运行时携带 store 引用的细节,请参见 执行上下文。 一个 keeper 通常会持有:
  • 模块的store key(一种对象能力,用于从 Context 中打开模块的 KVStore),
  • 一个 Protobuf codec,用于对以字节形式存储的值进行编码和解码,
  • 模块所依赖的其他 keeper 的引用(接口)。
状态访问通常会经过 keeper:
MsgServer / QueryServer
        ↓
     Keeper
        ↓
     KVStore
keeper 会暴露高层方法,用来构造键、编码值并执行业务逻辑:
func (k Keeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
func (k Keeper) SetParams(ctx sdk.Context, params types.Params)
关于 keeper 在模块中的角色,请参见 Keeper。

创世与链初始化

在第一个区块执行之前,链必须以一个称为 genesis 的初始状态启动,该状态定义在 genesis.json 中。创世是对 KVStore 的第一次写入,它决定了在任何交易运行之前,每个模块的状态如何存在。 在 InitChain 期间,BaseApp 会调用每个模块的 InitGenesis 来填充其存储:
genesis.json
    ↓
BaseApp.InitChain
    ↓
Module.InitGenesis
    ↓
KVStores populated
关于模块如何定义其创世方法(DefaultGenesis、ValidateGenesis、InitGenesis、ExportGenesis)以及初始化顺序的信息,请参见 模块简介 和 交易生命周期。 如需查看模块中创世实现的实践演示,请参见“构建模块”教程中的 步骤 2:Proto 文件 和 步骤 8:module.go。

后续步骤

如需了解有关存储、裁剪策略和存储配置的更多信息,请参见 store 规范。如需查看完整的存储接口定义,请参见 SDK 源码中的 store/types/store.go。 由于 KV 存储只保存原始字节,模块在写入结构化数据之前必须先对其进行序列化。下一节 编码与 Protobuf 将说明 Cosmos SDK 如何使用 Protocol Buffers 以确定性方式对这些数据进行编码,以及为什么每个验证者都必须生成完全相同的字节。
In the previous section, you learned that modules define business logic and that keepers are responsible for reading and writing module state. This page explains how that state is actually stored, committed, and made verifiable across the network.

What is state?

State is the persistent data of the blockchain: account balances, delegations, governance proposals, module parameters, and any other data that survives between blocks. When a transaction executes, modules update state. When a block is committed, that updated state becomes the starting point for the next block:
State0
  ↓ apply Block 1
State1
  ↓ apply Block 2
State2

The KVStore model

At its lowest level, the Cosmos SDK stores state as key-value pairs. Both keys and values are byte arrays. Modules encode structured data into those bytes using Protocol Buffers, and decode them back when reading. See Encoding and Protobuf for details on how modules serialize data into bytes. The following example shows a conceptual example of how the bank module stores balances:
key:   0x2 | len(address) | address_bytes | denom_bytes
value: ProtocolBuffer(amount)

# example
key:   0x2 | 20 | cosmos1abc...xyz | uatom
value: ProtocolBuffer(1000000)
The key encodes the store prefix, address length, address, and denomination. The value is a Protocol Buffer-encoded amount. See x/bank/types/keys.go for the actual implementation. Each module owns its own namespace in the key-value store. Keys are defined by the module and typically begin with a byte prefix that distinguishes them from other module keys.

Multistore

A single module store is only part of the picture. At the application level, all module stores are committed together. Every module has its own KVStore, and all module stores are mounted inside a multistore that is committed as a single state root. A module can only read and write to its own store through its keeper. Access is gated by a StoreKey, which is a typed capability object registered at app startup. Modules that don’t hold the key cannot open the store. This isolation follows an object-capabilities model:
  1. Modules cannot directly mutate another module’s state
  2. Cross-module interaction must go through exposed keeper methods
When a block finishes executing, the multistore computes a new root hash (the app hash) that represents the entire application state. That hash is returned to CometBFT, included in the block header, and is what makes the chain’s state verifiable. Transaction Lifecycle explains where that app hash is produced and committed. The full storage stack from top to bottom is:
Module keeper
    ↓
KVStore (namespaced, wrapped with gas/trace)
    ↓
CommitMultiStore (multistore, computes app hash)
    ↓
IAVL tree (versioned Merkle tree)
    ↓
Database backend (goleveldb by default)

How state is stored (IAVL and commit stores)

Each module’s KVStore is backed by a CommitKVStore. See the store spec for more details. In the current SDK store implementation described here, the Cosmos SDK uses IAVL, a versioned AVL Merkle tree. IAVL gives every read and write of the tree O(log n) complexity, meaning the time to read or write a key scales with the height of the tree, not the total number of keys. It also versions state on each block commit, and produces deterministic root hashes that can be used to generate Merkle proofs for light clients. Each block commit produces a new tree version with a new root hash:
Block 1                    Block 2                    Block 3

       [root h1]                    [root h2]                    [root h3]
        /      \                     /      \                     /      \
  [branch1] [branch2]         [branch1] [branch2']         [branch1] [branch2'']
    /  \      /  \              /  \      /   \              /  \      /    \
  [a]  [b]  [c]  [d]         [a]  [b]  [c]   [d']         [a]  [b]   [c']  [d']
                                               ↑                       ↑
                                          (updated)               (updated)

// branch1 and branch2 are internal nodes (they store hashes, not data).
// Leaf nodes (a, b, c, d) are actual key-value entries.
// When a leaf changes, only nodes on the path to the root are rewritten (marked ').
// Unmodified subtrees (branch1, a, b) are shared across all three versions.
Only modified nodes are rewritten, and unchanged nodes are shared across versions. The root hash changes any time any leaf changes. All validators must compute the same root hash. If they disagree, consensus halts. Because of this, state transitions must be deterministic, encoding must be deterministic, and transaction ordering must be consistent.

App hash

The app hash is the cryptographic root hash of the application’s committed state. It summarizes all module stores together through the CommitMultiStore. Because every validator executes the same state transitions deterministically, they should all compute the same app hash for a given block.

Database backend

The IAVL tree does not store data in memory. It writes versioned nodes to a database backend, which is a key-value store on disk. The Cosmos SDK uses CometBFT’s db package to abstract over the database implementation. The default backend is goleveldb. Other supported backends include PebbleDB, RocksDB, and memDB (in-memory, for testing). The database backend is selected at node startup and configured in app.toml. Application code never interacts with it directly; the store layer owns that boundary.

Store types in the SDK

Beyond the base KVStore, the SDK provides several specialized store wrappers.

CommitKVStore (persistent store)

The CommitKVStore is the main persistent store backed by IAVL. It persists across blocks, produces versioned commits, and contributes to the app hash.

CacheMultiStore (transaction isolation)

Before executing each transaction, the Cosmos SDK’s BaseApp creates a CacheMultiStore — a cached, copy-on-write view of the multistore. All writes during that transaction occur in this cached layer:
Multistore
   ↓ CacheWrap (per transaction)
   ↓ Execute tx
   → Success → commit changes
   → Failure → discard
  • If the transaction succeeds, changes are written to the underlying store.
  • If the transaction fails, the cache is discarded and no state changes are committed.
This is how transaction atomicity is implemented in the store layer.

Ephemeral store types

Transient stores are cleared at the end of each block. They are used for temporary per-block data such as counters or intermediate calculations, and do not affect the app hash. Memory stores survive block commits but reset when the node restarts — their Commit() is a no-op and data is never written to disk. They are used for in-process caching of data that is expensive to recompute each block but does not need to survive a restart. Modules access them via MemoryStoreKey, mounted with MountMemoryStores in app.go.
Store typeSurvives block commitSurvives restart
TransientNo (cleared each block)No
MemoryYesNo
IAVL (CommitKVStore)YesYes

Gas and trace store wrappers

All store accesses are wrapped with additional behavior by the GasKVStore and TraceKVStore wrappers.
  • GasKVStore charges gas for each read and write
  • TraceKVStore logs each store operation for debugging
Because state access is the dominant cost of transaction execution, the SDK charges gas at the store layer so that expensive reads and writes are reflected in transaction fees. Every read and write of a KVStore costs gas, and expensive operations naturally cost more. Execution Context, Gas, and Events explains how gas metering works at runtime.

Prefix store

A prefix store wraps a KVStore and automatically prepends a fixed byte prefix to every key. This lets keepers scope their reads and writes to a sub-namespace without manually constructing prefixed keys on every call.
prefixStore := prefix.NewStore(kvStore, types.KeyPrefix("balances"))
prefixStore.Set(key, value) // stored as "balances" + key
This is how modules avoid key collisions within their own store.

Collections API (typed state access)

In the Cosmos SDK, modules commonly use the collections API to define typed state access. Instead of manually constructing byte keys, modules define typed collections such as:
  • collections.Item[T]
  • collections.Map[K, V]
  • collections.Sequence
Example:
// declared in the keeper struct
Counter collections.Item[uint64]

// read and write in message handlers
count, _ := k.Counter.Get(ctx)
k.Counter.Set(ctx, count+1)
The Collections API defines the storage schema, handles encoding and decoding, ensures consistent key construction, and makes state access type-safe. Under the hood, collections still store data in a KVStore. Collections are used to provide a safer abstraction over raw byte keys. See collections/collections.go for the base interface definitions. For the full package guide, see Collections.

How modules access state

Modules do not interact with the multistore directly. Instead, each module defines a keeper that opens its KVStore through the execution Context it receives on each call. For details on how Context carries the store reference at runtime, see Execution Context. A keeper typically holds:
  • the module’s store key (an object-capability used to open the module’s KVStore from Context),
  • a Protobuf codec used to encode and decode values stored as bytes,
  • references (interfaces) to other keepers the module depends on.
State access typically flows through the keeper:
MsgServer / QueryServer
        ↓
     Keeper
        ↓
     KVStore
The keeper exposes high-level methods that construct keys, encode values, and enforce business logic:
func (k Keeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
func (k Keeper) SetParams(ctx sdk.Context, params types.Params)
For the keeper’s role within a module, see Keeper.

Genesis and chain initialization

Before the first block executes, the chain must start with an initial state called genesis, defined in genesis.json. Genesis is the first write to the KVStores — it is how every module’s state exists before any transaction runs. During InitChain, BaseApp calls each module’s InitGenesis to populate its store:
genesis.json
    ↓
BaseApp.InitChain
    ↓
Module.InitGenesis
    ↓
KVStores populated
For information on how modules define their genesis methods (DefaultGenesis, ValidateGenesis, InitGenesis, ExportGenesis) and initialization ordering, see Intro to Modules and Transaction Lifecycle. For a walkthrough of genesis implementation in a module, see Step 2: Proto files and Step 8: module.go in the Build a Module tutorial.

Next steps

For more information on stores, pruning strategies, and store configuration, see the store spec. For the full store interface definitions, see store/types/store.go in the SDK source. Because KV stores only hold raw bytes, modules must serialize structured data before writing it. The next section, Encoding and Protobuf, explains how the Cosmos SDK uses Protocol Buffers to encode that data deterministically, and why every validator must produce exactly the same bytes.