Skip to main content
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.