变更日志

  • 2023-01-17: 初始草案 (@alexanderbez)
  • 2023-04-06: 添加升级章节 (@alexanderbez)
  • 2023-04-10: 简化投票扩展状态持久化 (@alexanderbez)
  • 2023-07-07: 修订投票扩展状态持久化 (@alexanderbez)
  • 2023-08-24: 修订投票扩展权重计算和质押接口 (@davidterpay)

状态

已接受

摘要

本 ADR 概述了延续在 ADR 060: ABCI 1.0 (Phase I) 中提出的、在 Cosmos SDK 中实现 ABCI++ 的工作。 具体而言,本 ADR 概述了 ABCI 2.0 的设计与实现,其中 包括 ExtendVote、VerifyVoteExtension 和 FinalizeBlock。

背景

ABCI 2.0 延续了 ABCI++ 所承诺的更新,具体是另外三个 可由应用实现的 ABCI 方法,从而获得对共识流程更进一步的控制、 洞察与定制能力,解锁许多此前不可能实现的新型用例。 下面我们对这三个新方法进行说明:

ExtendVote

该方法允许每个验证者进程扩展 CometBFT 共识流程中的 pre-commit 阶段。 具体来说,它允许应用执行自定义业务逻辑来扩展 pre-commit 投票,并在投票中附带额外数据, 尽管这些数据会由同一个密钥单独签名。 这些数据称为投票扩展,它会与其所扩展的投票一起被广播和接收, 并会在下一个高度提供给应用。具体来说,下一个区块的提议者将通过 RequestPrepareProposal.local_last_commit.votes 接收到这些投票扩展。 如果应用没有可提供的投票扩展信息, 则返回一个长度为 0 的字节数组作为其投票扩展。 注意:
  • 尽管每个验证者进程都会提交自己的投票扩展,但只有下一个区块的提议者会收到前一个区块 pre-commit 阶段中包含的全部投票扩展。这意味着只有提议者会 通过 RequestPrepareProposal 隐式获得所有投票扩展的访问权限, 而且由于验证者不需要等待全部 pre-commit,只需要 2/3, 因此并非所有投票扩展都一定会被包含。
  • pre-commit 投票与投票扩展是分别签名的。

VerifyVoteExtension

该方法允许验证者校验其收到的每条 pre-commit 消息所附带的投票扩展数据。 如果校验失败,整个 pre-commit 消息都会被视为无效并被 CometBFT 忽略。 CometBFT 在校验 pre-commit 投票时会使用 VerifyVoteExtension。具体来说, 对于一条 pre-commit,CometBFT 会:
  • 如果消息不包含已签名的投票和已签名的投票扩展,则拒绝该消息
  • 如果投票签名或投票扩展签名校验失败,则拒绝该消息
  • 如果应用拒绝了 VerifyVoteExtension,则拒绝该消息
否则,CometBFT 将接受该 pre-commit 消息。 注意,这对活性有重要影响。也就是说,如果投票扩展反复无法被正确的验证者校验通过, 即使有足够多(+2/3)的验证者为该区块发送了 pre-commit 投票, CometBFT 也可能无法最终确认该区块。因此,VerifyVoteExtension 的使用必须格外谨慎。 CometBFT 建议:当应用检测到无效投票扩展时, 应当在 ResponseVerifyVoteExtension 中接受它,并在自身逻辑中忽略它。

FinalizeBlock

该方法将一个已决定的区块交付给应用。应用必须 以确定性的方式执行区块中的交易,并据此更新其状态。 通过 ResponseFinalizeBlock 中相应参数返回的、对区块和交易结果的密码学承诺, 会被包含到下一个区块的头部中。CometBFT 会在新区块 被决定时调用它。 换句话说,FinalizeBlock 将当前 ABCI 的执行流程,即 BeginBlock、一个或多个 DeliverTx 以及 EndBlock,封装为一个单一的 ABCI 方法。 CometBFT 将不再执行这些旧方法的请求, 而是只会直接调用 FinalizeBlock。

决策

我们将分两个不同阶段讨论为实现 ABCI 2.0 而对 Cosmos SDK 所做的变更, 分别是 VoteExtensions 和 FinalizeBlock。

VoteExtensions

与 PrepareProposal 和 ProcessProposal 类似,我们提议引入 两个新处理器,应用可以实现它们以提供和校验 投票扩展。 我们提议应用实现以下新处理器:
type ExtendVoteHandler func(sdk.Context, abci.RequestExtendVote)

abci.ResponseExtendVote
type VerifyVoteExtensionHandler func(sdk.Context, abci.RequestVerifyVoteExtension)

abci.ResponseVerifyVoteExtension
这两个处理器都会获得一个临时的上下文和状态。 上下文将包含相关元数据,例如区块高度和区块哈希。 状态将是应用已提交状态的一个缓存版本,并且在处理器执行结束后会被丢弃, 这意味着两个处理器都会获得一个全新的状态视图, 对其所做的任何修改都不会被写入。 如果应用决定实现 ExtendVoteHandler,则它必须返回一个 非 nil 的 ResponseExtendVote.VoteExtension。 请注意,ExtendVoteHandler 的实现不需要是确定性的, 但是,对于一组给定的投票扩展,VerifyVoteExtensionHandler 必须是 确定性的,否则链可能会遭受活性故障。此外, 请注意 CometBFT 会在每个高度按轮次推进,因此如果在某个高度下 无法对区块提案作出决定,CometBFT 将进入下一轮, 并因此会为每个验证者在新一轮中再次执行 ExtendVote 和 VerifyVoteExtension, 直到获得 2/3 的有效 pre-commit。 鉴于投票扩展的潜在实现方式和用例范围很广,以及校验方式也很多, 大多数应用应选择通过单一的处理器类型来实现这些处理器, 该类型可以注入任意数量的依赖,例如 keeper。此外, 该处理器类型还可以包含某种易失性的投票扩展状态管理能力, 用于协助投票扩展校验。这种状态管理可以是临时的, 也可以是某种磁盘持久化形式。 示例:
// VoteExtensionHandler implements an Oracle vote extension handler.
type VoteExtensionHandler struct {
    cdc   Codec
	mk    MyKeeper
	state VoteExtState // This could be a map or a DB connection object
}

// ExtendVoteHandler can do something with h.mk and possibly h.state to create
// a vote extension, such as fetching a series of prices for supported assets.
func (h VoteExtensionHandler)

ExtendVoteHandler(ctx sdk.Context, req abci.RequestExtendVote)

abci.ResponseExtendVote {
    prices := GetPrices(ctx, h.mk.Assets())

bz, err := EncodePrices(h.cdc, prices)
    if err != nil {
    panic(fmt.Errorf("failed to encode prices for vote extension: %w", err))
}

	// store our vote extension at the given height
	//
	// NOTE: Vote extensions can be overridden since we can timeout in a round.
	SetPrices(h.state, req, bz)

return abci.ResponseExtendVote{
    VoteExtension: bz
}
}

// VerifyVoteExtensionHandler can do something with h.state and req to verify
// the req.VoteExtension field, such as ensuring the provided oracle prices are
// within some valid range of our prices.
func (h VoteExtensionHandler)

VerifyVoteExtensionHandler(ctx sdk.Context, req abci.RequestVerifyVoteExtension)

abci.ResponseVerifyVoteExtension {
    prices, err := DecodePrices(h.cdc, req.VoteExtension)
    if err != nil {
    log("failed to decode vote extension", "err", err)

return abci.ResponseVerifyVoteExtension{
    Status: REJECT
}
	
}
    if err := ValidatePrices(h.state, req, prices); err != nil {
    log("failed to validate vote extension", "prices", prices, "err", err)

return abci.ResponseVerifyVoteExtension{
    Status: REJECT
}
	
}

	// store updated vote extensions at the given height
	//
	// NOTE: Vote extensions can be overridden since we can timeout in a round.
	SetPrices(h.state, req, req.VoteExtension)

return abci.ResponseVerifyVoteExtension{
    Status: ACCEPT
}
}

Vote Extension 的传播与验证

如前所述,高度 H 的 vote extension 只会在高度 H+1 的 PrepareProposal 阶段提供给 proposer。然而,为了让 vote extension 真正发挥作用,所有 validator 都应当能够在 H+1 期间访问到高度 H 上已经达成共识的 vote extension。 由于 CometBFT 会在 RequestPrepareProposal 中包含所有 vote extension 签名,我们提议由提议该区块的 validator 在 PrepareProposal 期间,通过一个特殊交易 VoteExtsTx,将 vote extension 及其对应签名手动“注入”到区块提案中。VoteExtsTx 将携带一个 ExtendedCommitInfo 对象,该对象直接来自 RequestPrepareProposal。 按惯例,VoteExtsTx 交易应当作为区块提案中的第一笔交易,不过链也可以实现自己的偏好设置。出于安全考虑,我们还提议 proposer 自身对其在 RequestPrepareProposal 中收到的所有 vote extension 签名进行验证。 validator 在收到 RequestProcessProposal 时,会接收到已注入的 VoteExtsTx,其中包含 vote extension 及其签名。如果不存在这样的交易,validator MUST REJECT 该提案。 当 validator 检查 VoteExtsTx 时,它会对每个 SignedVoteExtension 进行评估。对于每个已签名的 vote extension,validator 会生成签名字节并验证签名。只有当基于投票权重计算后,至少收到 2/3 的有效签名时,该区块提案才有效,否则 validator MUST REJECT 该提案。 为了具备验证签名的能力,BaseApp 必须能够访问 x/staking 模块,因为该模块维护了从共识地址到公钥的索引。不过,我们会避免直接依赖 x/staking,转而依赖一个接口。此外,Cosmos SDK 将公开一个默认的签名验证方法,供应用使用:
type ValidatorStore interface {
    GetPubKeyByConsAddr(context.Context, sdk.ConsAddress) (cmtprotocrypto.PublicKey, error)
}

// ValidateVoteExtensions is a function that an application can execute in
// ProcessProposal to verify vote extension signatures.
func (app *BaseApp)

ValidateVoteExtensions(ctx sdk.Context, currentHeight int64, extCommit abci.ExtendedCommitInfo)

error {
    votingPower := 0
    totalVotingPower := 0
    for _, vote := range extCommit.Votes {
    totalVotingPower += vote.Validator.Power
    if !vote.SignedLastBlock || len(vote.VoteExtension) == 0 {
    continue
}
    valConsAddr := sdk.ConsAddress(vote.Validator.Address)

pubKeyProto, err := valStore.GetPubKeyByConsAddr(ctx, valConsAddr)
    if err != nil {
    return fmt.Errorf("failed to get public key for validator %s: %w", valConsAddr, err)
}
    if len(vote.ExtensionSignature) == 0 {
    return fmt.Errorf("received a non-empty vote extension with empty signature for validator %s", valConsAddr)
}

cmtPubKey, err := cryptoenc.PubKeyFromProto(pubKeyProto)
    if err != nil {
    return fmt.Errorf("failed to convert validator %X public key: %w", valConsAddr, err)
}
    cve := cmtproto.CanonicalVoteExtension{
    Extension: vote.VoteExtension,
    Height:    currentHeight - 1, // the vote extension was signed in the previous height
			Round:     int64(extCommit.Round),
    ChainId:   app.GetChainID(),
}

extSignBytes, err := cosmosio.MarshalDelimited(&cve)
    if err != nil {
    return fmt.Errorf("failed to encode CanonicalVoteExtension: %w", err)
}
    if !cmtPubKey.VerifySignature(extSignBytes, vote.ExtensionSignature) {
    return errors.New("received vote with invalid signature")
}

votingPower += vote.Validator.Power
}
    if (votingPower / totalVotingPower) < threshold {
    return errors.New("not enough voting power for the vote extensions")
}

return nil
}
一旦按投票权重计算后,至少收到并验证了 2/3 的签名,validator 就可以使用这些 vote extension 来推导额外数据,或者基于这些 vote extension 作出某些决策。
注意:需要非常明确的一点是,上述的投票传播技术和 vote extension 验证机制都不是应用必须实现的。换句话说,proposer 不必一定验证和传播 vote extension 及其签名,proposer 也不必一定验证这些签名。应用可以实现自己的 PKI 机制,并用它来对 vote extension 进行签名和验证。

Vote Extension 持久化

在某些场景下,应用持久化从 vote extension 派生出的数据可能是有用的,甚至是必要的。为了支持这一用例,我们提议允许应用开发者定义一个 pre-Blocker hook,它会在 FinalizeBlock 的最开始被调用,也就是在 BeginBlock 之前调用(见下文)。 需要注意的是,我们不能允许应用在 ProcessProposal 期间直接写入应用状态,因为在 replay 过程中,CometBFT 不会调用 ProcessProposal,这会导致状态视图不完整。
func (a MyApp)

PreBlocker(ctx sdk.Context, req *abci.RequestFinalizeBlock)

error {
    voteExts := GetVoteExtensions(ctx, req.Txs)
	
	// Process and perform some compute on vote extensions, storing any resulting
	// state.
    if err a.processVoteExtensions(ctx, voteExts); if err != nil {
    return err
}
}

FinalizeBlock

现有的 ABCI 方法 BeginBlock、DeliverTx 和 EndBlock 自 ABCI 型应用诞生以来就一直存在。因此,应用、工具链以及开发者都已经习惯了这些方法及其使用场景。特别是,BeginBlock 和 EndBlock 在 ABCI 型应用中已经变得相当关键且强大。例如,应用可能希望在执行交易之前运行与分发和通胀相关的操作,然后在所有交易执行完成后再处理与 staking 相关的变更。 我们提议仅在 SDK 的核心模块接口中保留 BeginBlock 和 EndBlock,以便应用开发者可以继续基于现有执行流程进行构建。不过,我们会从 SDK 的 BaseApp 实现中移除 BeginBlock、DeliverTx 和 EndBlock,从而缩减 ABCI 暴露面。 之后将只存在一个统一的 FinalizeBlock 执行流程。具体来说,在 FinalizeBlock 中,我们会先执行应用的 BeginBlock,然后执行所有交易,最后再执行应用的 EndBlock。 需要注意的是,我们仍会在 BaseApp 中保留现有的交易执行机制,但会移除所有 DeliverTx 相关概念,也就是说,deliverState 将被 finalizeState 替代,并在 Commit 时提交。 不过,当前现有的 BeginBlock 和 EndBlock 的 ABCI 类型中包含一些参数和字段,例如分发逻辑使用的投票信息,以及证据处理使用的拜占庭 validator 信息。这些参数已经存在于 FinalizeBlock 的请求类型中,因此需要传递给应用对 BeginBlock 和 EndBlock 的实现。 这意味着 Cosmos SDK 的核心模块接口需要更新,以反映这些参数。实现这一点最简单直接的方式,就是把 RequestFinalizeBlock 直接传给 BeginBlock 和 EndBlock。或者,我们也可以在 SDK 中创建专用的代理类型来映射这些旧版 ABCI 类型,例如 LegacyBeginBlockRequest 和 LegacyEndBlockRequest。再或者,我们也可以完全设计一套新的类型和命名。
func (app *BaseApp)

FinalizeBlock(req abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) {
    ctx := ...
    if app.preBlocker != nil {
    ctx := app.finalizeBlockState.ctx
		rsp, err := app.preBlocker(ctx, req)
    if err != nil {
    return nil, err
}
    if rsp.ConsensusParamsChanged {
    app.finalizeBlockState.ctx = ctx.WithConsensusParams(app.GetConsensusParams(ctx))
}
	
}

beginBlockResp, err := app.beginBlock(req)

appendBlockEventAttr(beginBlockResp.Events, "begin_block")
    txExecResults := make([]abci.ExecTxResult, 0, len(req.Txs))
    for _, tx := range req.Txs {
    result := app.runTx(runTxModeFinalize, tx)

txExecResults = append(txExecResults, result)
}

endBlockResp, err := app.endBlock(app.finalizeBlockState.ctx)

appendBlockEventAttr(beginBlockResp.Events, "end_block")

return abci.ResponseFinalizeBlock{
    TxResults:             txExecResults,
    Events:                joinEvents(beginBlockResp.Events, endBlockResp.Events),
    ValidatorUpdates:      endBlockResp.ValidatorUpdates,
    ConsensusParamUpdates: endBlockResp.ConsensusParamUpdates,
    AppHash:               nil,
}
}

事件

许多工具、索引器以及生态库都依赖 BeginBlock 和 EndBlock 事件的存在。由于 CometBFT 现在只暴露 FinalizeBlockEvents,我们认为这些客户端和工具依然需要能够查询并继续依赖现有事件,尤其是在应用仍然会定义 BeginBlock 和 EndBlock 实现的情况下。 为了支持现有事件功能,我们提议为所有 BeginBlock 和 EndBlock 事件附加一个专用的 EventAttribute,其中 key=block,value=begin_block|end_block。这个 EventAttribute 会被追加到 BeginBlock 和 EndBlock 事件中的每一条事件上。`

升级

CometBFT 定义了一个共识参数 VoteExtensionsEnableHeight,用于指定启用并且强制要求使用 vote extension 的区块高度。 如果该值为零(默认值),则 vote extension 被禁用,应用也不需要实现和使用 vote extension。 但是,如果该值 H 为正,那么在配置高度 H 之后的所有高度上,vote extension 都必须存在(即使为空)。当到达配置的高度 H 时,PrepareProposal 还不会包含 vote extension,但会调用 ExtendVote 和 VerifyVoteExtension。随后,当到达高度 H+1 时,PrepareProposal 将包含来自高度 H 的 vote extension。 非常重要的一点是,对于 H 之后的所有高度:
  • vote extension CANNOT be disabled
  • 它们是强制性的,也就是说,所有发送出的 pre-commit 消息 MUST 附带一个 extension(即使为空)
当应用升级到支持 CometBFT v0.38 的 Cosmos SDK 版本时,必须在 upgrade handler 中确保将共识参数 VoteExtensionsEnableHeight 设为正确的值。例如,如果应用计划在高度 H 执行升级,那么 VoteExtensionsEnableHeight 的值应当设置为任意 >=H+1 的值。这意味着在升级高度 H 时,vote extension 仍不会启用,但在高度 H+1 时它们将被启用。

影响

向后兼容性

ABCI 2.0 与之前版本的 Cosmos SDK 和 CometBFT 天然不向后兼容。例如,一个向同一应用发送 RequestFinalizeBlock 的应用,如果对方不支持 ABCI 2.0,那么自然会失败。 此外,BeginBlock、DeliverTx 和 EndBlock 将从应用的 ABCI 接口中移除,同时模块接口中的输入和输出也会被修改。

正面影响

  • BeginBlock 和 EndBlock 的语义得以保留,因此对应用开发者带来的负担应当有限。
  • 多个 ABCI 请求被合并为单个请求,通信开销更低。
  • 为乐观执行打下基础。
  • vote extension 使得开发全新的一类应用原语成为可能,例如进程内价格预言机和加密 mempool。

负面

  • 现有的一些 Cosmos SDK 核心 API 可能需要修改,因此会产生破坏性变更。
  • 在 ProcessProposal 中对 100 多个投票扩展签名进行签名验证, 会给 ProcessProposal 带来显著的性能开销。当然, 签名验证过程可以借助错误组并使用 GOMAXPROCS 个 goroutine 并发执行。

中性

  • 在 PrepareProposal 期间必须手动将投票扩展“注入”到区块提案中, 这种方式比较别扭,而且还会不必要地占用区块空间。
  • ResetProcessProposalState 这一要求如果应用开发者不够谨慎, 可能会埋下隐患,但为了让应用能够提交来自投票扩展计算的状态, 这是必要的。

后续讨论

未来的讨论包括 ABCI 3.0 的设计与实现,它是 ABCI++ 的延续, 以及对乐观执行的一般性讨论。

参考资料


Changelog

  • 2023-01-17: Initial Draft (@alexanderbez)
  • 2023-04-06: Add upgrading section (@alexanderbez)
  • 2023-04-10: Simplify vote extension state persistence (@alexanderbez)
  • 2023-07-07: Revise vote extension state persistence (@alexanderbez)
  • 2023-08-24: Revise vote extension power calculations and staking interface (@davidterpay)

Status

ACCEPTED

Abstract

This ADR outlines the continuation of the efforts to implement ABCI++ in the Cosmos SDK outlined in ADR 060: ABCI 1.0 (Phase I). Specifically, this ADR outlines the design and implementation of ABCI 2.0, which includes ExtendVote, VerifyVoteExtension and FinalizeBlock.

Context

ABCI 2.0 continues the promised updates from ABCI++, specifically three additional ABCI methods that the application can implement in order to gain further control, insight and customization of the consensus process, unlocking many novel use-cases that previously not possible. We describe these three new methods below:

ExtendVote

This method allows each validator process to extend the pre-commit phase of the CometBFT consensus process. Specifically, it allows the application to perform custom business logic that extends the pre-commit vote and supply additional data as part of the vote, although they are signed separately by the same key. The data, called vote extension, will be broadcast and received together with the vote it is extending, and will be made available to the application in the next height. Specifically, the proposer of the next block will receive the vote extensions in RequestPrepareProposal.local_last_commit.votes. If the application does not have vote extension information to provide, it returns a 0-length byte array as its vote extension. NOTE:
  • Although each validator process submits its own vote extension, ONLY the proposer of the next block will receive all the vote extensions included as part of the pre-commit phase of the previous block. This means only the proposer will implicitly have access to all the vote extensions, via RequestPrepareProposal, and that not all vote extensions may be included, since a validator does not have to wait for all pre-commits, only 2/3.
  • The pre-commit vote is signed independently from the vote extension.

VerifyVoteExtension

This method allows validators to validate the vote extension data attached to each pre-commit message it receives. If the validation fails, the whole pre-commit message will be deemed invalid and ignored by CometBFT. CometBFT uses VerifyVoteExtension when validating a pre-commit vote. Specifically, for a pre-commit, CometBFT will:
  • Reject the message if it doesn’t contain a signed vote AND a signed vote extension
  • Reject the message if the vote’s signature OR the vote extension’s signature fails to verify
  • Reject the message if VerifyVoteExtension was rejected by the app
Otherwise, CometBFT will accept the pre-commit message. Note, this has important consequences on liveness, i.e., if vote extensions repeatedly cannot be verified by correct validators, CometBFT may not be able to finalize a block even if sufficiently many (+2/3) validators send pre-commit votes for that block. Thus, VerifyVoteExtension should be used with special care. CometBFT recommends that an application that detects an invalid vote extension SHOULD accept it in ResponseVerifyVoteExtension and ignore it in its own logic.

FinalizeBlock

This method delivers a decided block to the application. The application must execute the transactions in the block deterministically and update its state accordingly. Cryptographic commitments to the block and transaction results, returned via the corresponding parameters in ResponseFinalizeBlock, are included in the header of the next block. CometBFT calls it when a new block is decided. In other words, FinalizeBlock encapsulates the current ABCI execution flow of BeginBlock, one or more DeliverTx, and EndBlock into a single ABCI method. CometBFT will no longer execute requests for these legacy methods and instead will just simply call FinalizeBlock.

Decision

We will discuss changes to the Cosmos SDK to implement ABCI 2.0 in two distinct phases, VoteExtensions and FinalizeBlock.

VoteExtensions

Similarly for PrepareProposal and ProcessProposal, we propose to introduce two new handlers that an application can implement in order to provide and verify vote extensions. We propose the following new handlers for applications to implement:
type ExtendVoteHandler func(sdk.Context, abci.RequestExtendVote)

abci.ResponseExtendVote
type VerifyVoteExtensionHandler func(sdk.Context, abci.RequestVerifyVoteExtension)

abci.ResponseVerifyVoteExtension
An ephemeral context and state will be supplied to both handlers. The context will contain relevant metadata such as the block height and block hash. The state will be a cached version of the committed state of the application and will be discarded after the execution of the handler, this means that both handlers get a fresh state view and no changes made to it will be written. If an application decides to implement ExtendVoteHandler, it must return a non-nil ResponseExtendVote.VoteExtension. Recall, an implementation of ExtendVoteHandler does NOT need to be deterministic, however, given a set of vote extensions, VerifyVoteExtensionHandler must be deterministic, otherwise the chain may suffer from liveness faults. In addition, recall CometBFT proceeds in rounds for each height, so if a decision cannot be made about about a block proposal at a given height, CometBFT will proceed to the next round and thus will execute ExtendVote and VerifyVoteExtension again for the new round for each validator until 2/3 valid pre-commits can be obtained. Given the broad scope of potential implementations and use-cases of vote extensions, and how to verify them, most applications should choose to implement the handlers through a single handler type, which can have any number of dependencies injected such as keepers. In addition, this handler type could contain some notion of volatile vote extension state management which would assist in vote extension verification. This state management could be ephemeral or could be some form of on-disk persistence. Example:
// VoteExtensionHandler implements an Oracle vote extension handler.
type VoteExtensionHandler struct {
    cdc   Codec
	mk    MyKeeper
	state VoteExtState // This could be a map or a DB connection object
}

// ExtendVoteHandler can do something with h.mk and possibly h.state to create
// a vote extension, such as fetching a series of prices for supported assets.
func (h VoteExtensionHandler)

ExtendVoteHandler(ctx sdk.Context, req abci.RequestExtendVote)

abci.ResponseExtendVote {
    prices := GetPrices(ctx, h.mk.Assets())

bz, err := EncodePrices(h.cdc, prices)
    if err != nil {
    panic(fmt.Errorf("failed to encode prices for vote extension: %w", err))
}

	// store our vote extension at the given height
	//
	// NOTE: Vote extensions can be overridden since we can timeout in a round.
	SetPrices(h.state, req, bz)

return abci.ResponseExtendVote{
    VoteExtension: bz
}
}

// VerifyVoteExtensionHandler can do something with h.state and req to verify
// the req.VoteExtension field, such as ensuring the provided oracle prices are
// within some valid range of our prices.
func (h VoteExtensionHandler)

VerifyVoteExtensionHandler(ctx sdk.Context, req abci.RequestVerifyVoteExtension)

abci.ResponseVerifyVoteExtension {
    prices, err := DecodePrices(h.cdc, req.VoteExtension)
    if err != nil {
    log("failed to decode vote extension", "err", err)

return abci.ResponseVerifyVoteExtension{
    Status: REJECT
}
	
}
    if err := ValidatePrices(h.state, req, prices); err != nil {
    log("failed to validate vote extension", "prices", prices, "err", err)

return abci.ResponseVerifyVoteExtension{
    Status: REJECT
}
	
}

	// store updated vote extensions at the given height
	//
	// NOTE: Vote extensions can be overridden since we can timeout in a round.
	SetPrices(h.state, req, req.VoteExtension)

return abci.ResponseVerifyVoteExtension{
    Status: ACCEPT
}
}

Vote Extension Propagation & Verification

As mentioned previously, vote extensions for height H are only made available to the proposer at height H+1 during PrepareProposal. However, in order to make vote extensions useful, all validators should have access to the agreed upon vote extensions at height H during H+1. Since CometBFT includes all the vote extension signatures in RequestPrepareProposal, we propose that the proposing validator manually “inject” the vote extensions along with their respective signatures via a special transaction, VoteExtsTx, into the block proposal during PrepareProposal. The VoteExtsTx will be populated with a single ExtendedCommitInfo object which is received directly from RequestPrepareProposal. For convention, the VoteExtsTx transaction should be the first transaction in the block proposal, although chains can implement their own preferences. For safety purposes, we also propose that the proposer itself verify all the vote extension signatures it receives in RequestPrepareProposal. A validator, upon a RequestProcessProposal, will receive the injected VoteExtsTx which includes the vote extensions along with their signatures. If no such transaction exists, the validator MUST REJECT the proposal. When a validator inspects a VoteExtsTx, it will evaluate each SignedVoteExtension. For each signed vote extension, the validator will generate the signed bytes and verify the signature. At least 2/3 valid signatures, based on voting power, must be received in order for the block proposal to be valid, otherwise the validator MUST REJECT the proposal. In order to have the ability to validate signatures, BaseApp must have access to the x/staking module, since this module stores an index from consensus address to public key. However, we will avoid a direct dependency on x/staking and instead rely on an interface instead. In addition, the Cosmos SDK will expose a default signature verification method which applications can use:
type ValidatorStore interface {
    GetPubKeyByConsAddr(context.Context, sdk.ConsAddress) (cmtprotocrypto.PublicKey, error)
}

// ValidateVoteExtensions is a function that an application can execute in
// ProcessProposal to verify vote extension signatures.
func (app *BaseApp)

ValidateVoteExtensions(ctx sdk.Context, currentHeight int64, extCommit abci.ExtendedCommitInfo)

error {
    votingPower := 0
    totalVotingPower := 0
    for _, vote := range extCommit.Votes {
    totalVotingPower += vote.Validator.Power
    if !vote.SignedLastBlock || len(vote.VoteExtension) == 0 {
    continue
}
    valConsAddr := sdk.ConsAddress(vote.Validator.Address)

pubKeyProto, err := valStore.GetPubKeyByConsAddr(ctx, valConsAddr)
    if err != nil {
    return fmt.Errorf("failed to get public key for validator %s: %w", valConsAddr, err)
}
    if len(vote.ExtensionSignature) == 0 {
    return fmt.Errorf("received a non-empty vote extension with empty signature for validator %s", valConsAddr)
}

cmtPubKey, err := cryptoenc.PubKeyFromProto(pubKeyProto)
    if err != nil {
    return fmt.Errorf("failed to convert validator %X public key: %w", valConsAddr, err)
}
    cve := cmtproto.CanonicalVoteExtension{
    Extension: vote.VoteExtension,
    Height:    currentHeight - 1, // the vote extension was signed in the previous height
			Round:     int64(extCommit.Round),
    ChainId:   app.GetChainID(),
}

extSignBytes, err := cosmosio.MarshalDelimited(&cve)
    if err != nil {
    return fmt.Errorf("failed to encode CanonicalVoteExtension: %w", err)
}
    if !cmtPubKey.VerifySignature(extSignBytes, vote.ExtensionSignature) {
    return errors.New("received vote with invalid signature")
}

votingPower += vote.Validator.Power
}
    if (votingPower / totalVotingPower) < threshold {
    return errors.New("not enough voting power for the vote extensions")
}

return nil
}
Once at least 2/3 signatures, by voting power, are received and verified, the validator can use the vote extensions to derive additional data or come to some decision based on the vote extensions.
NOTE: It is very important to state, that neither the vote propagation technique nor the vote extension verification mechanism described above is required for applications to implement. In other words, a proposer is not required to verify and propagate vote extensions along with their signatures nor are proposers required to verify those signatures. An application can implement its own PKI mechanism and use that to sign and verify vote extensions.

Vote Extension Persistence

In certain contexts, it may be useful or necessary for applications to persist data derived from vote extensions. In order to facilitate this use case, we propose to allow app developers to define a pre-Blocker hook which will be called at the very beginning of FinalizeBlock, i.e. before BeginBlock (see below). Note, we cannot allow applications to directly write to the application state during ProcessProposal because during replay, CometBFT will NOT call ProcessProposal, which would result in an incomplete state view.
func (a MyApp)

PreBlocker(ctx sdk.Context, req *abci.RequestFinalizeBlock)

error {
    voteExts := GetVoteExtensions(ctx, req.Txs)
	
	// Process and perform some compute on vote extensions, storing any resulting
	// state.
    if err a.processVoteExtensions(ctx, voteExts); if err != nil {
    return err
}
}

FinalizeBlock

The existing ABCI methods BeginBlock, DeliverTx, and EndBlock have existed since the dawn of ABCI-based applications. Thus, applications, tooling, and developers have grown used to these methods and their use-cases. Specifically, BeginBlock and EndBlock have grown to be pretty integral and powerful within ABCI-based applications. E.g. an application might want to run distribution and inflation related operations prior to executing transactions and then have staking related changes to happen after executing all transactions. We propose to keep BeginBlock and EndBlock within the SDK’s core module interfaces only so application developers can continue to build against existing execution flows. However, we will remove BeginBlock, DeliverTx and EndBlock from the SDK’s BaseApp implementation and thus the ABCI surface area. What will then exist is a single FinalizeBlock execution flow. Specifically, in FinalizeBlock we will execute the application’s BeginBlock, followed by execution of all the transactions, finally followed by execution of the application’s EndBlock. Note, we will still keep the existing transaction execution mechanics within BaseApp, but all notions of DeliverTx will be removed, i.e. deliverState will be replace with finalizeState, which will be committed on Commit. However, there are current parameters and fields that exist in the existing BeginBlock and EndBlock ABCI types, such as votes that are used in distribution and byzantine validators used in evidence handling. These parameters exist in the FinalizeBlock request type, and will need to be passed to the application’s implementations of BeginBlock and EndBlock. This means the Cosmos SDK’s core module interfaces will need to be updated to reflect these parameters. The easiest and most straightforward way to achieve this is to just pass RequestFinalizeBlock to BeginBlock and EndBlock. Alternatively, we can create dedicated proxy types in the SDK that reflect these legacy ABCI types, e.g. LegacyBeginBlockRequest and LegacyEndBlockRequest. Or, we can come up with new types and names altogether.
func (app *BaseApp)

FinalizeBlock(req abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) {
    ctx := ...
    if app.preBlocker != nil {
    ctx := app.finalizeBlockState.ctx
		rsp, err := app.preBlocker(ctx, req)
    if err != nil {
    return nil, err
}
    if rsp.ConsensusParamsChanged {
    app.finalizeBlockState.ctx = ctx.WithConsensusParams(app.GetConsensusParams(ctx))
}
	
}

beginBlockResp, err := app.beginBlock(req)

appendBlockEventAttr(beginBlockResp.Events, "begin_block")
    txExecResults := make([]abci.ExecTxResult, 0, len(req.Txs))
    for _, tx := range req.Txs {
    result := app.runTx(runTxModeFinalize, tx)

txExecResults = append(txExecResults, result)
}

endBlockResp, err := app.endBlock(app.finalizeBlockState.ctx)

appendBlockEventAttr(beginBlockResp.Events, "end_block")

return abci.ResponseFinalizeBlock{
    TxResults:             txExecResults,
    Events:                joinEvents(beginBlockResp.Events, endBlockResp.Events),
    ValidatorUpdates:      endBlockResp.ValidatorUpdates,
    ConsensusParamUpdates: endBlockResp.ConsensusParamUpdates,
    AppHash:               nil,
}
}

Events

Many tools, indexers and ecosystem libraries rely on the existence BeginBlock and EndBlock events. Since CometBFT now only exposes FinalizeBlockEvents, we find that it will still be useful for these clients and tools to still query for and rely on existing events, especially since applications will still define BeginBlock and EndBlock implementations. In order to facilitate existing event functionality, we propose that all BeginBlock and EndBlock events have a dedicated EventAttribute with key=block and value=begin_block|end_block. The EventAttribute will be appended to each event in both BeginBlock and EndBlock events`.

Upgrading

CometBFT defines a consensus parameter, VoteExtensionsEnableHeight, which specifies the height at which vote extensions are enabled and required. If the value is set to zero, which is the default, then vote extensions are disabled and an application is not required to implement and use vote extensions. However, if the value H is positive, at all heights greater than the configured height H vote extensions must be present (even if empty). When the configured height H is reached, PrepareProposal will not include vote extensions yet, but ExtendVote and VerifyVoteExtension will be called. Then, when reaching height H+1, PrepareProposal will include the vote extensions from height H. It is very important to note, for all heights after H:
  • Vote extensions CANNOT be disabled
  • They are mandatory, i.e. all pre-commit messages sent MUST have an extension attached (even if empty)
When an application updates to the Cosmos SDK version with CometBFT v0.38 support, in the upgrade handler it must ensure to set the consensus parameter VoteExtensionsEnableHeight to the correct value. E.g. if an application is set to perform an upgrade at height H, then the value of VoteExtensionsEnableHeight should be set to any value >=H+1. This means that at the upgrade height, H, vote extensions will not be enabled yet, but at height H+1 they will be enabled.

Consequences

Backwards Compatibility

ABCI 2.0 is naturally not backwards compatible with prior versions of the Cosmos SDK and CometBFT. For example, an application that requests RequestFinalizeBlock to the same application that does not speak ABCI 2.0 will naturally fail. In addition, BeginBlock, DeliverTx and EndBlock will be removed from the application ABCI interfaces and along with the inputs and outputs being modified in the module interfaces.

Positive

  • BeginBlock and EndBlock semantics remain, so burden on application developers should be limited.
  • Less communication overhead as multiple ABCI requests are condensed into a single request.
  • Sets the groundwork for optimistic execution.
  • Vote extensions allow for an entirely new set of application primitives to be developed, such as in-process price oracles and encrypted mempools.

Negative

  • Some existing Cosmos SDK core APIs may need to be modified and thus broken.
  • Signature verification in ProcessProposal of 100+ vote extension signatures will add significant performance overhead to ProcessProposal. Granted, the signature verification process can happen concurrently using an error group with GOMAXPROCS goroutines.

Neutral

  • Having to manually “inject” vote extensions into the block proposal during PrepareProposal is an awkward approach and takes up block space unnecessarily.
  • The requirement of ResetProcessProposalState can create a footgun for application developers if they’re not careful, but this is necessary in order for applications to be able to commit state from vote extension computation.

Further Discussions

Future discussions include design and implementation of ABCI 3.0, which is a continuation of ABCI++ and the general discussion of optimistic execution.

References