在上一节中,编码与 Protobuf 解释了数据如何被序列化,以及为什么每个验证者都必须以完全相同的方式对状态进行编码。本页介绍模块运行时所处的运行环境:承载区块元数据和状态访问能力的上下文对象、限制计算量的 gas 系统,以及允许模块发出可观测信号的事件系统。

什么是 sdk.Context

Cosmos SDK 中的每个消息处理器、keeper 方法和区块钩子都会接收一个 sdk.Context。它是单个工作单元(一次交易、一次查询或一次区块钩子执行)的执行环境,承载了代码读取状态、发出事件和消耗 gas 所需的一切内容。与其将 store、gas meter 和区块头作为独立参数传给每个函数,不如由 Context 将它们打包为一个值。 Context 结构体定义在 types/context.go 中:
type Context struct {
    ms            storetypes.MultiStore
    chainID       string
    gasMeter      storetypes.GasMeter
    blockGasMeter storetypes.GasMeter
    eventManager  EventManagerI
    // ... additional fields
}
Context 是值类型。它按值传递,并通过返回新副本的 With* 方法进行修改。这意味着模块可以安全地派生一个子上下文(例如使用不同的 gas meter),而不会影响调用方的上下文。

区块元数据

Context 提供对当前区块元数据的只读访问(见 types/context.go):
  • ctx.BlockHeight() 返回当前区块高度。
  • ctx.BlockTime() 返回区块时间戳。
  • ctx.ChainID() 返回链标识字符串。
  • ctx.Logger() 返回一个绑定到当前执行上下文的结构化日志器。模块使用它进行运维日志记录(例如记录升级激活或意外状态),而不会影响共识。
这些值由 BaseApp 在任何区块逻辑运行之前,根据 CometBFT 提供的区块头填充。模块读取它们以实现依赖时间的逻辑(例如检查锁仓期是否已经结束),或为事件打上区块高度标签。 当上下文用于内存池校验而非区块执行时,ctx.IsCheckTx() 返回 true。若需要更细粒度的分支判断,ctx.ExecMode() 会返回精确的执行模式:ExecModeCheck、ExecModeReCheck、ExecModeSimulate、ExecModePrepareProposal、ExecModeProcessProposal、ExecModeFinalize 等(更多细节见 types/context.go)。需要在模拟或提案处理期间表现不同行为的模块,应使用 ExecMode() 而不是 IsCheckTx()。

Context 与状态访问

状态通过 context 访问。context 持有对 multistore 的引用,而每个 keeper 都通过 context 打开自己的 store:
func (k Keeper) GetCount(ctx context.Context) (uint64, error) {
    return k.counter.Get(ctx)
}
keeper 并不直接持有对实时 multistore 的引用;它会在每次调用时从 context 中打开本模块的 store。这就是为什么每个 keeper 方法都必须传入 context:它是访问当前区块状态、gas meter 和该执行单元对应事件管理器的入口。

使用 CacheContext 实现原子子执行

需要尝试某个子操作并在失败时回滚的模块,可以调用 ctx.CacheContext()。它会返回一个分支上下文副本和一个 writeCache 函数。子操作中的所有状态变更都会写入该分支。调用 writeCache() 会将这些变更刷回父上下文;不调用则会以原子方式丢弃这些变更。
cacheCtx, writeCache := ctx.CacheContext()
if err := doRiskyOperation(cacheCtx); err != nil {
    return err // branch is discarded, no state changes applied
}
writeCache() // flush branch to parent context

Gas 计量

Gas 衡量什么

Gas 是一种计算单位。在 Cosmos SDK 中,gas 同时覆盖计算和状态访问。每次 store 读取、store 写入和迭代器步进都会消耗 gas。像 AnteHandler 中的签名校验这类复杂计算同样会消耗 gas。 Gas 系统的存在是为了防止滥用。如果没有 gas 限制,单笔交易就可能通过无限制计算或未建立索引的状态扫描耗尽节点资源。

Gas 上限与交易 gas meter

每笔交易都会在其 auth_info.fee.gas_limit 字段中指定一个 gas 上限。当 BaseApp 开始执行一笔交易时,它会创建一个以该上限初始化的 GasMeter,并将其附加到 context 上。 GasMeter 接口提供两个关键方法:
type GasMeter interface {
    GasConsumed() Gas
    ConsumeGas(amount Gas, descriptor string)
    // ...
}
GasConsumed 返回当前执行单元到目前为止已使用的 gas 总量。ConsumeGas 会把指定数量累加到当前总量中;如果消耗量超过上限,则会以 ErrorOutOfGas panic。 提交交易时,用户会在 fees、gas 和 gas-prices 三个值中指定其中两个,第三个通过公式 fees = gas * gas-prices 推导得出。gas 的值会成为 GasWanted:交易允许消耗的最大 gas。执行过程中实际消耗的 gas 是 GasUsed。当 FinalizeBlock 完成时,GasWanted 和 GasUsed 都会返回给 CometBFT。

Gas 如何被消耗

Gas 会在 store 层自动消耗。通过 GasKVStore 包装器进行的每次读取和写入,都会先扣除 gas,再委托给底层 store:
  • Get(store 读取)会收取固定读取成本,以及按 key 和 value 字节数计算的成本。
  • Set(store 写入)会收取固定写入成本,以及按 key 和 value 字节数计算的成本。
对于普通状态访问,模块不需要手动跟踪 gas,store 层会自动处理。只有当存在 store 操作无法捕获的计算成本时(例如模块在 store 之外执行加密操作),模块才需要直接调用 ctx.GasMeter().ConsumeGas(...)。

Gas 耗尽时会发生什么

如果执行期间 gas 被耗尽,ConsumeGas 会以 ErrorOutOfGas panic。BaseApp 会从这个 panic 中恢复,丢弃当前消息执行分支,并向用户返回错误。即便如此,直到失败点为止已经消耗的 gas 仍可能被收费,而且在消息执行开始之前,AnteHandler 的副作用也可能已经生效。

区块 gas 上限

除了每笔交易的 gas meter 之外,还存在一个区块级 gas meter,用于跟踪一个区块内所有交易消耗的 gas 总量。区块 gas 上限可防止单个区块消耗无限制的计算资源。如果某笔交易会导致区块 gas 总量超过上限,它就不会被打包进该区块。区块 gas 上限和最小 gas 价格在 app.toml 中配置。

事件

什么是事件

事件是在交易和区块执行期间发出的可观测信号。模块通过发出事件来描述发生了什么:代币被转移、某个验证者被惩罚、某项治理提案已通过。事件携带类型字符串以及结构化的键值数据。 事件不是共识状态的一部分。它们不会存储在 KVStore 中,不会影响 app hash,也不是确定性执行所必需的。相反,它们由 BaseApp 收集并包含在区块结果中,供索引器、区块浏览器和 relayer 消费。

EventManager

模块通过附加在 context 上的 EventManager 发出事件。 EventManager 会为每笔交易重新创建,并收集该次执行期间发出的所有事件。

标准事件类型

SDK 会为每笔交易自动发出一个 message 事件,并由 BaseApp 设置以下属性:
  • message.action — 消息的完整类型 URL(例如 /cosmos.bank.v1beta1.Msg/Send)
  • message.module — 模块名,从类型 URL 推导得出
  • message.sender — 签名者地址(如果存在)
这些常量定义在 types/events.go 中。模块在发出自己的事件时也遵循相同约定。

发出事件

模块使用 EmitEvent 或 EmitTypedEvent 发出事件:
// emit an untyped event
ctx.EventManager().EmitEvent(sdk.NewEvent(
    "increment",
    sdk.NewAttribute("new_count", strconv.FormatUint(newCount, 10)),
))
EmitEvent 会将原始键值事件追加到管理器累计的事件列表中。 对于由 protobuf 消息类型支撑的事件,EmitTypedEvent 会自动把消息字段序列化为事件属性:
ctx.EventManager().EmitTypedEvent(&types.EventCounterIncremented{
    NewCount: newCount,
})
使用 EmitTypedEvent 是现代做法。它提供类型安全,并通过 proto 定义显式声明事件模式,使客户端能够将事件反序列化回带类型的结构体。

区块事件与交易事件

在 BeginBlock 或 EndBlock 钩子期间发出的事件是 区块事件:它们描述区块级别发生的事情(如铸造了通胀、应用了验证者更新)。在消息处理器内部发出的事件是 交易事件:它们描述某一笔具体交易做了什么。 这两类事件都会包含在 CometBFT 返回给网络的 FinalizeBlock 响应中,但会分开报告,以便客户端区分区块级活动和逐笔交易活动。

谁会消费事件

事件由节点之外的组件消费:
  • 区块浏览器 会索引事件,向用户展示一笔交易中发生了什么(哪些代币发生了转移、哪个验证者被惩罚、哪项提案通过了)。
  • Relayer(IBC)会订阅特定事件类型,以检测数据包发送和确认。
  • 索引器和链下服务 会基于事件流构建可查询的链上活动数据库。事件也可以通过节点的 REST API 和 WebSocket 端点查询。
  • 钱包和 UI 会向用户展示事件数据,作为交易回执。
事件会包含在 CometBFT 每个区块结束后返回的区块结果中。它们不会被重放或重新处理;一旦区块最终确定,其事件也随之固定。

查询事件

事件会按 {type}.{key}={value} 格式建立索引,并且可以在查询交易时进行过滤。字符串值必须用单引号包裹。
过滤条件说明
tx.height=23区块高度 23 上的所有交易
message.action='/cosmos.bank.v1beta1.Msg/Send'包含 bank Send 消息的交易
message.module='bank'来自 x/bank 模块的交易

综合来看

在交易执行期间,context、gas 和 events 共同构成运行时层:
BaseApp 为该交易创建 Context
    ↓
AnteHandler 运行
    → 签名验证、扣除手续费、初始化 gas meter
    ↓
消息处理器运行
    → 每次对 store 的读写都会通过 GasKVStore 消耗 gas
    → 模块逻辑通过 EventManager 发出事件
    ↓
如果 gas 耗尽 → panic → 状态回滚,并对已消耗的 gas 收费
如果执行成功 → 提交状态变更,并在区块结果中返回事件
context 会将 gas meter 和 event manager 传递到每一次 keeper 调用中。gas 会在 store 层透明地被消耗。事件会持续累积,并在执行完成后作为区块结果的一部分返回。 下一节 SDK Structure 简介 将说明 SDK 应用在代码库中的组织方式:模块位于哪里、app/ 中放什么,以及各个部分如何组装在一起。
In the previous section, Encoding and Protobuf explained how data is serialized and why every validator must encode state identically. This page covers the runtime environment that modules execute within: the context object that carries block metadata and state access, the gas system that limits computation, and the event system that allows modules to emit observable signals.

What is sdk.Context

Every message handler, keeper method, and block hook in the Cosmos SDK receives an sdk.Context. It is the execution environment for a single unit of work (a transaction, a query, or a block hook) and carries everything that code needs to read state, emit events, and consume gas. Rather than passing the store, gas meter, and block header as separate arguments to every function, Context bundles them into a single value. The Context struct is defined in types/context.go:
type Context struct {
    ms            storetypes.MultiStore
    chainID       string
    gasMeter      storetypes.GasMeter
    blockGasMeter storetypes.GasMeter
    eventManager  EventManagerI
    // ... additional fields
}
Context is a value type. It is passed by value and mutated through With* methods that return a new copy. This means a module can safely derive a sub-context (for example, with a different gas meter) without affecting the caller’s context.

Block metadata

Context exposes read-only access to the current block’s metadata (see types/context.go):
  • ctx.BlockHeight() returns the current block number.
  • ctx.BlockTime() returns the block’s timestamp.
  • ctx.ChainID() returns the chain identifier string.
  • ctx.Logger() returns a structured logger scoped to the current execution context. Modules use this for operational logging (e.g., logging an upgrade activation or an unexpected state) without affecting consensus.
These values are populated by BaseApp from the block header provided by CometBFT before any block logic runs. Modules read them to implement time-dependent logic (for example, checking whether a vesting period has elapsed) or to tag events with the block height. ctx.IsCheckTx() returns true when the context is being used for mempool validation rather than block execution. For finer-grained branching, ctx.ExecMode() returns the precise execution mode: ExecModeCheck, ExecModeReCheck, ExecModeSimulate, ExecModePrepareProposal, ExecModeProcessProposal, ExecModeFinalize, and others (see types/context.go for more details). Modules that need to behave differently during simulation or proposal handling use ExecMode() instead of IsCheckTx().

Context and state access

State is accessed through context. The context holds a reference to the multistore, and each keeper opens its own store through the context:
func (k Keeper) GetCount(ctx context.Context) (uint64, error) {
    return k.counter.Get(ctx)
}
The keeper does not hold a direct reference to the live multistore; it opens its module’s store from the context on each call. This is why context must be passed to every keeper method: it is the gateway to the current block’s state, the gas meter, and the event manager for that execution unit.

Atomic sub-execution with CacheContext

Modules that need to attempt a sub-operation and revert it on failure can call ctx.CacheContext(), which returns a branched copy of the context and a writeCache function. All state changes in the sub-operation go into the branch. Calling writeCache() flushes them to the parent context; not calling it discards them atomically.
cacheCtx, writeCache := ctx.CacheContext()
if err := doRiskyOperation(cacheCtx); err != nil {
    return err // branch is discarded, no state changes applied
}
writeCache() // flush branch to parent context

Gas metering

What gas measures

Gas is a unit of computation. In the Cosmos SDK, gas accounts for both computation and state access. Every store read, store write, and iterator step costs gas. Complex computations such as signature verification in the AnteHandler also cost gas. The gas system exists to prevent abuse. Without a gas limit, a single transaction could exhaust a node’s resources with an unbounded computation or an unindexed state scan.

Gas limit and the transaction gas meter

Every transaction specifies a gas limit in its auth_info.fee.gas_limit field. When BaseApp begins executing a transaction, it creates a GasMeter initialized with that limit and attaches it to the context. The GasMeter interface provides two key methods:
type GasMeter interface {
    GasConsumed() Gas
    ConsumeGas(amount Gas, descriptor string)
    // ...
}
GasConsumed returns the total gas used so far in the current execution unit. ConsumeGas adds to the running total and panics with ErrorOutOfGas if consumption exceeds the limit. When submitting a transaction, users specify two of the three values fees, gas, and gas-prices — the third is derived from the equation fees = gas * gas-prices. The gas value becomes GasWanted: the maximum gas the transaction is allowed to consume. The actual gas consumed during execution is GasUsed. Both GasWanted and GasUsed are returned to CometBFT when FinalizeBlock completes.

How gas is consumed

Gas is consumed automatically at the store layer. Every read and write through the GasKVStore wrapper charges gas before delegating to the underlying store:
  • A Get (store read) charges a flat read cost plus a per-byte cost for the key and value.
  • A Set (store write) charges a flat write cost plus a per-byte cost for the key and value.
Modules do not need to manually track gas for ordinary state access — the store layer handles it automatically. Modules call ctx.GasMeter().ConsumeGas(...) directly only for computation costs that are not captured by store operations (for example, a module that performs a cryptographic operation outside the store).

When gas runs out

If gas is exhausted during execution, ConsumeGas panics with ErrorOutOfGas. BaseApp recovers from this panic, discards the current message execution branch, and returns an error to the user. Fees may still be charged for the gas consumed up to the point of failure, and AnteHandler side effects may already have been applied before message execution started.

Block gas limit

In addition to the per-transaction gas meter, there is a block-level gas meter that tracks total gas consumed by all transactions in a block. The block gas limit prevents a single block from consuming unbounded computation. If a transaction would cause the block’s gas total to exceed the limit, it is excluded from the block. The block gas limit and minimum gas prices are configured in app.toml.

Events

What events are

Events are observable signals emitted during transaction and block execution. A module emits events to describe what happened: tokens were transferred, a validator was slashed, a governance proposal passed. Events carry structured key-value data alongside a type string. Events are not part of consensus state. They are not stored in the KVStore, do not affect the app hash, and are not required for deterministic execution. Instead, they are collected by BaseApp and included in the block result, where indexers, explorers, and relayers consume them.

EventManager

Modules emit events through the EventManager, which is attached to the context. The EventManager is created fresh for each transaction and collects all events emitted during that execution.

Standard event types

The SDK automatically emits a message event for every transaction, with these attributes set by BaseApp:
  • message.action — the full type URL of the message (e.g., /cosmos.bank.v1beta1.Msg/Send)
  • message.module — the module name, derived from the type URL
  • message.sender — the signer address, if present
These are defined as constants in types/events.go. Modules follow the same convention when emitting their own events.

Emitting events

Modules emit events using EmitEvent or EmitTypedEvent:
// emit an untyped event
ctx.EventManager().EmitEvent(sdk.NewEvent(
    "increment",
    sdk.NewAttribute("new_count", strconv.FormatUint(newCount, 10)),
))
EmitEvent appends a raw key-value event to the manager’s accumulated list. For events backed by protobuf message types, EmitTypedEvent serializes the message’s fields into event attributes automatically:
ctx.EventManager().EmitTypedEvent(&types.EventCounterIncremented{
    NewCount: newCount,
})
Using EmitTypedEvent is the modern approach. It provides type safety and makes the event schema explicit through proto definitions, allowing clients to deserialize events back into typed structs.

Block events and transaction events

Events emitted during BeginBlock or EndBlock hooks are block events: they describe things that happened at the block level (inflation minted, validator updates applied). Events emitted inside a message handler are transaction events: they describe what a specific transaction did. Both types are included in the FinalizeBlock response that CometBFT returns to the network, but they are reported separately so clients can distinguish block-level activity from per-transaction activity.

Who consumes events

Events are consumed outside the node:
  • Block explorers index events to show users what happened in a transaction (which tokens moved, which validator was slashed, which proposal passed).
  • Relayers (IBC) subscribe to specific event types to detect packet sends and acknowledgments.
  • Indexers and off-chain services build queryable databases of chain activity from event streams. Events can also be queried via the node’s REST API and WebSocket endpoint.
  • Wallets and UIs display event data to users as transaction receipts.
Events are included in the block result that CometBFT returns after each block. They are not replayed or reprocessed; once a block is finalized, its events are fixed.

Querying events

Events are indexed using the format {type}.{key}={value} and can be filtered when querying transactions. String values must be wrapped in single quotes.
FilterDescription
tx.height=23All transactions at block height 23
message.action='/cosmos.bank.v1beta1.Msg/Send'Transactions containing a bank Send message
message.module='bank'Transactions from the x/bank module

Putting it together

During transaction execution, context, gas, and events work together as the runtime layer:
BaseApp creates Context for the transaction
    ↓
AnteHandler runs
    → signature verification, fee deduction, gas meter initialized
    ↓
Message handler runs
    → each store read/write consumes gas via GasKVStore
    → module logic emits events via EventManager
    ↓
If gas exhausted → panic → state reverted, fees charged for gas consumed
If execution succeeds → state changes committed, events returned in block result
The context carries the gas meter and event manager into every keeper call. Gas is consumed transparently at the store layer. Events accumulate and are returned as part of the block result once execution completes. The next section, Intro to SDK Structure, explains how an SDK application is structured as a codebase: where modules live, what goes in app/, and how all the pieces are assembled.