投票扩展是验证者可以附加到区块高度 H 的 pre-commit 投票上的任意字节。它们属于 ABCI 2.0 的一部分,并从 CometBFT v0.38 和 Cosmos SDK v0.50 开始可用。

启用投票扩展

投票扩展由 VoteExtensionsEnableHeight 共识参数控制。在配置的高度,CometBFT 会开始对每个验证者调用 ExtendVote 和 VerifyVoteExtension。在高度 H 生成的扩展,会在高度 H+1 通过 PrepareProposal 提供给区块提议者。 要在某个处理器中检查投票扩展是否已启用:
cp := ctx.ConsensusParams()
if cp.Abci != nil && req.Height > cp.Abci.VoteExtensionsEnableHeight {
    // vote extensions are available
}
ConsensusParams().Abci 是一个指针,使用前必须先进行 nil 检查。

ExtendVote

Cosmos SDK 定义了 ExtendVoteHandler:
type ExtendVoteHandler func(Context, *abci.RequestExtendVote) (*abci.ResponseExtendVote, error)
在 app.go 中通过 baseapp.SetExtendVoteHandler 注册处理器(定义见 baseapp/options.go):
app.SetExtendVoteHandler(myExtendVoteHandler)
如果设置了 ExtendVoteHandler,它必须返回一个非 nil 的 VoteExtension。空字节切片也是有效的。 ExtendVote 仅在本地验证者上调用,因此不需要是确定性的。常见用途包括:
  • 为预言机提交价格
  • 为加密 mempool 共享加密份额
保持扩展足够小,大型扩展会增加共识延迟。基准测试可参见 CometBFT QA results。

VerifyVoteExtension

SDK 定义了 VerifyVoteExtensionHandler:
type VerifyVoteExtensionHandler func(Context, *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error)
在 app.go 中注册:
app.SetVerifyVoteExtensionHandler(myVerifyVoteExtensionHandler)
VerifyVoteExtension 会在每个验证者上针对每个对等节点的 pre-commit 调用。它必须是确定性的,即同一个扩展在每个验证者上都必须产生相同结果。如果应用定义了 ExtendVoteHandler,也应该同时定义 VerifyVoteExtensionHandler。 始终在此处理器中校验传入扩展的大小。

校验投票扩展签名

在 PrepareProposal 或 ProcessProposal 中处理投票扩展之前,请先校验它们是否已被正确签名。SDK 为此提供了 baseapp.ValidateVoteExtensions:
err := baseapp.ValidateVoteExtensions(ctx, valStore, req.Height, ctx.ChainID(), req.LocalLastCommit)
if err != nil {
    return nil, err
}
ValidateVoteExtensions 会验证提交中的每个投票扩展是否都由对应验证者正确签名。valStore 是一个 baseapp.ValidatorStore,这是一个只包含单个方法的接口:
type ValidatorStore interface {
    GetPubKeyByConsAddr(context.Context, sdk.ConsAddress) (cmtprotocrypto.PublicKey, error)
}
在信任任何扩展数据之前,应当同时在 PrepareProposal(针对 req.LocalLastCommit)和 ProcessProposal(针对从注入交易中恢复出的 ExtendedCommitInfo)中调用 ValidateVoteExtensions。

投票扩展传播

高度 H 的投票扩展只会在高度 H+1 的 PrepareProposal 中,通过 req.LocalLastCommit 提供给区块提议者。它们不会在 ProcessProposal 期间提供给其他验证者。 如果所有验证者都需要在 H+1 使用扩展数据,提议者必须将其注入区块提案。由于 PrepareProposal 中的 Txs 字段类型为 [][]byte,因此任何字节切片都可以被前置到提案中,包括序列化后的扩展摘要:
injectedVoteExtTx := StakeWeightedPrices{
    StakeWeightedPrices: stakeWeightedPrices,
    ExtendedCommitInfo:  req.LocalLastCommit,
}
bz, err := json.Marshal(injectedVoteExtTx)
if err != nil {
    return nil, err
}
proposalTxs = append([][]byte{bz}, proposalTxs...)
FinalizeBlock 会忽略任何未实现 sdk.Tx 的字节切片,因此注入的扩展会在消息执行期间被安全跳过。 关于传播设计的更多细节,参见 ABCI 2.0 ADR。

通过 PreBlocker 恢复

SDK 的 PreBlocker 会在 FinalizeBlock 中任何消息执行之前运行。可以用它恢复注入的投票扩展,并在当前区块期间将结果提供给各模块使用:
func (h *ProposalHandler) PreBlocker(ctx sdk.Context, req *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) {
    res := &sdk.ResponsePreBlock{}
    if len(req.Txs) == 0 {
        return res, nil
    }
    cp := ctx.ConsensusParams()
    if cp.Abci != nil && req.Height > cp.Abci.VoteExtensionsEnableHeight {
        var injectedVoteExtTx StakeWeightedPrices
        if err := json.Unmarshal(req.Txs[0], &injectedVoteExtTx); err != nil {
            return nil, err
        }
        if err := h.keeper.SetOraclePrices(ctx, injectedVoteExtTx.StakeWeightedPrices); err != nil {
            return nil, err
        }
    }
    return res, nil
}
在 app.go 中注册 PreBlocker(见 baseapp/options.go):
app.SetPreBlocker(proposalHandler.PreBlocker)
sdk.PreBlocker 类型定义在 types/abci.go 中:
type PreBlocker func(Context, *abci.RequestFinalizeBlock) (*ResponsePreBlock, error)
在 PreBlocker 内部写入上下文的状态,在同一区块中的所有 BeginBlock 和消息处理器中都可用。
Vote extensions are arbitrary bytes that validators can attach to their pre-commit vote at block height H. They are part of ABCI 2.0 and are available starting from CometBFT v0.38 and Cosmos SDK v0.50.

Enabling vote extensions

Vote extensions are controlled by the VoteExtensionsEnableHeight consensus parameter. At the configured height, CometBFT begins calling ExtendVote and VerifyVoteExtension on every validator. Extensions produced at height H are available to the block proposer at height H+1 via PrepareProposal. To check whether vote extensions are active in a handler:
cp := ctx.ConsensusParams()
if cp.Abci != nil && req.Height > cp.Abci.VoteExtensionsEnableHeight {
    // vote extensions are available
}
ConsensusParams().Abci is a pointer and must be nil-checked before use.

ExtendVote

The Cosmos SDK defines ExtendVoteHandler:
type ExtendVoteHandler func(Context, *abci.RequestExtendVote) (*abci.ResponseExtendVote, error)
Register a handler in app.go via baseapp.SetExtendVoteHandler (defined in baseapp/options.go):
app.SetExtendVoteHandler(myExtendVoteHandler)
If ExtendVoteHandler is set, it must return a non-nil VoteExtension. An empty byte slice is valid. ExtendVote is called only on the local validator and does not need to be deterministic. Common uses include:
  • Submitting prices for an oracle
  • Sharing encryption shares for an encrypted mempool
Keep extensions small — large extensions increase consensus latency. See CometBFT QA results for benchmarks.

VerifyVoteExtension

The SDK defines VerifyVoteExtensionHandler:
type VerifyVoteExtensionHandler func(Context, *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error)
Register it in app.go:
app.SetVerifyVoteExtensionHandler(myVerifyVoteExtensionHandler)
VerifyVoteExtension is called on every validator for every peer’s pre-commit. It must be deterministic — the same extension must produce the same result on every validator. If an application defines ExtendVoteHandler, it should also define a VerifyVoteExtensionHandler. Always validate the size of incoming extensions in this handler.

Validating vote extension signatures

Before processing vote extensions in PrepareProposal or ProcessProposal, validate that they are properly signed. The SDK provides baseapp.ValidateVoteExtensions for this:
err := baseapp.ValidateVoteExtensions(ctx, valStore, req.Height, ctx.ChainID(), req.LocalLastCommit)
if err != nil {
    return nil, err
}
ValidateVoteExtensions verifies that each vote extension in the commit is correctly signed by its validator. valStore is a baseapp.ValidatorStore, an interface with a single method:
type ValidatorStore interface {
    GetPubKeyByConsAddr(context.Context, sdk.ConsAddress) (cmtprotocrypto.PublicKey, error)
}
Call ValidateVoteExtensions in both PrepareProposal (on req.LocalLastCommit) and ProcessProposal (on the ExtendedCommitInfo recovered from the injected transaction) before trusting any extension data.

Vote extension propagation

Vote extensions from height H are provided only to the block proposer at height H+1 via req.LocalLastCommit in PrepareProposal. They are not provided to other validators during ProcessProposal. If all validators need to use extension data at H+1, the proposer must inject it into the block proposal. Since the Txs field in PrepareProposal is a [][]byte, any byte slice — including a serialized extensions summary — can be prepended to the proposal:
injectedVoteExtTx := StakeWeightedPrices{
    StakeWeightedPrices: stakeWeightedPrices,
    ExtendedCommitInfo:  req.LocalLastCommit,
}
bz, err := json.Marshal(injectedVoteExtTx)
if err != nil {
    return nil, err
}
proposalTxs = append([][]byte{bz}, proposalTxs...)
FinalizeBlock ignores any byte slice that does not implement sdk.Tx, so injected extensions are safely skipped during message execution. For more details on propagation design, see the ABCI 2.0 ADR.

Recovery via PreBlocker

The SDK’s PreBlocker runs before any message execution in FinalizeBlock. Use it to recover injected vote extensions and make the results available to modules during the block:
func (h *ProposalHandler) PreBlocker(ctx sdk.Context, req *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) {
    res := &sdk.ResponsePreBlock{}
    if len(req.Txs) == 0 {
        return res, nil
    }
    cp := ctx.ConsensusParams()
    if cp.Abci != nil && req.Height > cp.Abci.VoteExtensionsEnableHeight {
        var injectedVoteExtTx StakeWeightedPrices
        if err := json.Unmarshal(req.Txs[0], &injectedVoteExtTx); err != nil {
            return nil, err
        }
        if err := h.keeper.SetOraclePrices(ctx, injectedVoteExtTx.StakeWeightedPrices); err != nil {
            return nil, err
        }
    }
    return res, nil
}
Register the PreBlocker in app.go (see baseapp/options.go):
app.SetPreBlocker(proposalHandler.PreBlocker)
The sdk.PreBlocker type is defined in types/abci.go:
type PreBlocker func(Context, *abci.RequestFinalizeBlock) (*ResponsePreBlock, error)
State written to the context inside PreBlocker is available to all BeginBlock and message handlers in the same block.