有效的方法调用序列
本节描述应用程序可以从 CometBFT 预期得到的行为。 当前被 CometBFT 采用的 Tendermint 共识算法,被设计为只要拜占庭验证者的投票权不超过总量的 1/3,就能在任何网络条件下保护安全性。不过,大多数时候网络会以同步方式运行,不会有进程落后,也不会存在拜占庭进程。下面描述的是在这些常见且良性的条件下,一个区块高度 h 期间会发生的情况:- 共识会在高度 h 的第 0 轮做出决定;
PrepareProposal会在高度 h、第 0 轮的提议者进程上被精确调用一次;ProcessProposal会在所有进程上被精确调用一次,并且会在其Response*中返回 accept;ExtendVote会在所有进程上被精确调用一次;VerifyVoteExtension会在每个验证者进程上被精确调用 n-1 次,其中 n 是验证者数量,并且会始终在其Response*中返回 accept;FinalizeBlock会在所有进程上被精确调用一次,并传递同一个已准备好的区块;此前所有针对高度 h 的PrepareProposal与ProcessProposal调用都已报告过该区块;以及Commit最终会在高度 h 结束时于所有进程上被精确调用一次。
Echo和Flush仅用于调试。此外,应用程序对它们的处理应当是很简单的。CheckTx独立于驱动区块执行的主方法调用序列。Query提供对当前应用程序状态的只读访问,因此对它的处理也应独立于区块执行。- 同样,
ListSnapshots和LoadSnapshotChunk提供对应用程序先前创建的快照(如果存在)的只读访问,并帮助在执行状态同步、完成引导的进程中填充OfferSnapshot与ApplySnapshotChunk的参数。不同于ListSnapshots和LoadSnapshotChunk,OfferSnapshot与ApplySnapshotChunk会 被包含在该语法中。
Info 方法是一个特殊情况。该方法有三重用途,可用于:
- 作为处理外部客户端 RPC 调用的一部分;
- 在恢复时作为 CometBFT 与应用程序之间的握手,用于检查是否有区块需要重放;以及
- 在 state-sync 结束时验证是否已经到达正确状态。
Info 的第一种用途排除在语法之外,原因与前述其他方法相同:它可以在任何时间发生,并且与区块执行序列无关。另一方面,第二种和第三种用途则出现在该语法中。
下面我们逐行检查这段语法,并提供更多细节。
- 当一个进程启动时,它可能是首次启动,也可能是在崩溃后启动(即恢复中)。
- 如果进程是从零开始启动,CometBFT 会先调用
InitChain,然后它可以选择性地启动 state-sync 机制以追赶其他进程。最后,它进入正常的共识执行。
- 在 state-sync 模式下,CometBFT 会进行一次或多次尝试来同步应用程序状态。每次尝试开始时,它会向应用程序提供一个在其他进程上发现的快照。如果应用程序接受该快照,随后会发生一系列对
ApplySnapshotChunk方法的调用,以便按顺序向应用程序提供重建本地状态所需的所有快照块。一次成功的尝试必须至少通过ApplySnapshotChunk提供一个块。一次成功尝试结束时,CometBFT 会调用Info,以确保重建出的状态其 AppHash 与对应高度区块头中的值一致。请注意,应用程序状态本身并不包含投票扩展。应用程序可以依赖 CometBFT 的保证,认为节点已经具备了继续执行所需的全部相关数据。
- 在恢复模式下,CometBFT 会先调用
Info,以确定需要从哪个高度开始向应用程序重放决定。之后,CometBFT 会进入共识执行,先是重放模式,然后是正常模式。
- 非终结符
consensus-exec是该语法中的一个关键点。它表示一个无限的共识高度序列。因此,这个语法是一个 omega 文法,因为它生成的是终结符的无限序列(即 API 调用)。
- 一个共识高度由零个或多个轮次组成,随后通过一次
FinalizeBlock调用做出决定并执行,再接着调用Commit。在每一轮中,方法调用序列取决于本地进程是否为提议者。请注意,如果某个高度包含零轮,这意味着该进程正在重放一个已经决定的值(追赶模式)。当使用某个区块调用FinalizeBlock时,CometBFT 运行的共识算法保证至少有一个非拜占庭验证者已经对该区块执行过ProcessProposal。
-
对于每一轮,如果本地进程是当前轮的提议者,CometBFT 会调用
PrepareProposal。PrepareProposal成功执行后,会得到一个提议区块,并且该区块会被(i)签名,以及(ii)存储(例如存入稳定存储)。 如果在这一步发生崩溃,那么节点在重启后针对同一轮的下一次执行流程将取决于崩溃发生的位置。 如果在(i)之前崩溃,那么恢复期间PrepareProposal会像第一次执行一样运行。 如果在(i)与(ii)之间崩溃,并且在很可能的情况下PrepareProposal生成了不同的区块,那么该区块的签名会失败,这意味着新区块不会被存储也不会被广播。 如果崩溃发生在(ii)之后,那么签名会失败,但对已存储的区块没有任何影响。 如果某个区块已被存储,它会被发送给所有验证者,包括提议者自身。 收到提议区块会触发针对该区块的ProcessProposal。 然后,应用程序可能会被要求为该轮扩展自己的投票。对VerifyVoteExtension的调用可以在任何时刻到来:本地进程可能在当前轮稍有滞后,或者收到来自该高度未来轮次的投票。
- 同样地,对于每一轮,如果本地进程_不是_当前轮的提议者,CometBFT 最多会调用一次
ProcessProposal。 在某些条件下,CometBFT 在某一轮中可能不会调用ProcessProposal; 示例可参见这一节。只有在调用ProcessProposal之后,才可能最多发生一次ExtendVote调用。整轮期间,若干次VerifyVoteExtension调用可以相对于ProcessProposal和ExtendVote以任意顺序发生。原因与上文相同,即当前轮中进程略微落后,或收到了该高度未来轮次的投票。
- 最后,该语法描述了它的所有终结符,它们表示可能出现在序列中的不同 ABCI++ 方法调用。
适配现有使用 ABCI 的应用程序
在某些情况下,现有使用旧版 ABCI 的应用程序可能需要在尽可能少改动的前提下适配 ABCI++。当然,在这种情况下,ABCI++ 相对于现有实现不会带来额外优势,但会保持 ABCI 已经提供的相同保证。 下面说明 ABCI++ 方法应如何实现。 首先,那些从 ABCI 0.17.0 到 ABCI 2.0 没有变化的方法,即Echo、Flush、Info、InitChain、Query、CheckTx、ListSnapshots、LoadSnapshotChunk、OfferSnapshot 和 ApplySnapshotChunk,其实现不需要做任何修改。
对于新增的方法:
-
PrepareProposal必须通过复制RequestPrepareProposal.txs中传入的交易列表,并保持相同顺序,来创建一个交易列表。 应用程序必须检查所有交易的总大小是否超过字节上限(RequestPrepareProposal.max_tx_bytes)。如果超过,应用程序必须从列表末尾开始移除交易,直到总字节大小小于或等于该上限。 -
ProcessProposal必须将ResponseProcessProposal.status设为 accept 并返回。 -
ExtendVote应将ResponseExtendVote.extension设为空字节数组并返回。 -
VerifyVoteExtension必须在扩展为空字节数组时将ResponseVerifyVoteExtension.accept设为 true,否则设为 false,然后返回。 -
FinalizeBlock需要合并方法BeginBlock、DeliverTx和EndBlock的实现。希望复用旧版DeliverTx实现代码的遗留应用程序,应将旧的DeliverTx逻辑包装进一个循环中,使其针对RequestFinalizeBlock.tx中的每笔交易执行一次迭代。
Commit 不再返回 AppHash。现在由 FinalizeBlock 来负责返回它。因此,需要对旧的 Commit 实现进行轻微重构,将 AppHash 的返回迁移到 FinalizeBlock。
适配投票扩展
CometBFT 会以应用程序无感知的方式,确保节点获得参与共识所需的全部数据。 在从崩溃中恢复,或通过状态同步加入网络时,CometBFT 会确保节点在切换到共识之前获得所需的投票扩展。 如果某个节点已经处于共识中但落后了,那么在追赶过程中,CometBFT 会通过检索其之前存储的旧高度ExtendedCommit 中的扩展,为该节点提供来自过去高度的投票扩展。
我们意识到,由于需要额外存储这些扩展,这种方式并不理想;我们正在优化该实现,以缓解这一问题。
不过,应用程序可以使用现有的 retain_height 参数来决定自己希望保留多少历史,就像处理区块历史时一样。retain_height 的使用在全网范围内带来的影响保持不变。
关于存储历史提交以及潜在优化的决策,可参见 RFC-100 中的详细讨论。
处理升级到 ABCI 2.0
如果应用程序升级到 ABCI 2.0,CometBFT 会在内部确保应用程序设置反映到其运行行为中。 CometBFT 会从应用程序配置中获取VoteExtensionsEnableHeight 的值( he),即共识继续推进所要求启用投票扩展的高度,并用它来决定要存储哪些数据,以及向正在追赶的对等节点发送哪些数据。
具体来说,在决策时将某个高度 h 的区块保存到区块存储中时:
- 如果 h ≥ he,则同时保存本地用于做出决定的对应扩展提交;
- 如果 h < he,则已保存的数据不发生变化。
- 如果 hp ≥ he,f 会使用扩展提交来重建带有相应扩展的 precommit 投票;
- 如果 hp < he,f 会像 ABCI 1.0 及更早版本那样,使用规范提交来重建 precommit 投票。
Valid method call sequences
This section describes what the Application can expect from CometBFT. The Tendermint consensus algorithm, currently adopted in CometBFT, is designed to protect safety under any network conditions, as long as less than 1/3 of validators’ voting power is byzantine. Most of the time, though, the network will behave synchronously, no process will fall behind, and there will be no byzantine process. The following describes what will happen during a block height h in these frequent, benign conditions:- Consensus will decide in round 0, for height h;
PrepareProposalwill be called exactly once at the proposer process of round 0, height h;ProcessProposalwill be called exactly once at all processes, and will return accept in itsResponse*;ExtendVotewill be called exactly once at all processes;VerifyVoteExtensionwill be called exactly n-1 times at each validator process, where n is the number of validators, and will always return accept in itsResponse*;FinalizeBlockwill be called exactly once at all processes, conveying the same prepared block that all calls toPrepareProposalandProcessProposalhad previously reported for height h; andCommitwill finally be called exactly once at all processes at the end of height h.
EchoandFlushare only used for debugging purposes. Further, their handling by the Application should be trivial.CheckTxis detached from the main method call sequence that drives block execution.Queryprovides read-only access to the current Application state, so handling it should also be independent from block execution.- Similarly,
ListSnapshotsandLoadSnapshotChunkprovide read-only access to the Application’s previously created snapshots (if any), and help populate the parameters ofOfferSnapshotandApplySnapshotChunkat a process performing state-sync while bootstrapping. UnlikeListSnapshotsandLoadSnapshotChunk, bothOfferSnapshotandApplySnapshotChunkare included in the grammar.
Info is a special case. The method’s purpose is three-fold, it can be used
- as part of handling an RPC call from an external client,
- as a handshake between CometBFT and the Application upon recovery to check whether any blocks need to be replayed, and
- at the end of state-sync to verify that the correct state has been reached.
Info’s first purpose out of the grammar for the same reasons as all the others: it can happen
at any time, and has nothing to do with the block execution sequence. The second and third purposes, on the other
hand, are present in the grammar.
Let us now examine the grammar line by line, providing further details.
- When a process starts, it may do so for the first time or after a crash (it is recovering).
- If the process is starting from scratch, CometBFT first calls
InitChain, then it may optionally start a state-sync mechanism to catch up with other processes. Finally, it enters normal consensus execution.
- In state-sync mode, CometBFT makes one or more attempts at synchronizing the Application’s state.
At the beginning of each attempt, it offers the Application a snapshot found at another process.
If the Application accepts the snapshot, a sequence of calls to
ApplySnapshotChunkmethod follow to provide the Application with all the snapshots needed, in order to reconstruct the state locally. A successful attempt must provide at least one chunk viaApplySnapshotChunk. At the end of a successful attempt, CometBFT callsInfoto make sure the reconstructed state’s AppHash matches the one in the block header at the corresponding height. Note that the state of the application does not contain vote extensions itself. The application can rely on CometBFT to ensure the node has all the relevant data to proceed with the execution beyond this point.
- In recovery mode, CometBFT first calls
Infoto know from which height it needs to replay decisions to the Application. After this, CometBFT enters consensus execution, first in replay mode and then in normal mode.
- The non-terminal
consensus-execis a key point in this grammar. It is an infinite sequence of consensus heights. The grammar is thus an omega-grammar, since it produces infinite sequences of terminals (i.e., the API calls).
- A consensus height consists of zero or more rounds before deciding and executing via a call to
FinalizeBlock, followed by a call toCommit. In each round, the sequence of method calls depends on whether the local process is the proposer or not. Note that, if a height contains zero rounds, this means the process is replaying an already decided value (catch-up mode). When callingFinalizeBlockwith a block, the consensus algorithm run by CometBFT guarantees that at least one non-byzantine validator has runProcessProposalon that block.
-
For every round, if the local process is the proposer of the current round, CometBFT calls
PrepareProposal. A successful execution ofPrepareProposalresults in a proposal block being (i) signed and (ii) stored (e.g., in stable storage). A crash during this step will direct how the node proceeds the next time it is executed, for the same round, after restarted. If it crashed before (i), then, during the recovery,PrepareProposalwill execute as if for the first time. Following a crash between (i) and (ii) and in (the likely) casePrepareProposalproduces a different block, the signing of this block will fail, which means that the new block will not be stored or broadcast. If the crash happened after (ii), then signing fails but nothing happens to the stored block. If a block was stored, it is sent to all validators, including the proposer. Receiving a proposal block triggersProcessProposalwith such a block. Then, optionally, the Application is asked to extend its vote for that round. Calls toVerifyVoteExtensioncan come at any time: the local process may be slightly late in the current round, or votes may come from a future round of this height.
- Also for every round, if the local process is not the proposer of the current round, CometBFT
will call
ProcessProposalat most once. Under certain conditions, CometBFT may not callProcessProposalin a round; see this section for an example. At most one call toExtendVotemay occur only afterProcessProposalis called. A number of calls toVerifyVoteExtensioncan occur in any order with respect toProcessProposalandExtendVotethroughout the round. The reasons are the same as above, namely, the process running slightly late in the current round, or votes from future rounds of this height received.
- Finally, the grammar describes all its terminal symbols, which denote the different ABCI++ method calls that may appear in a sequence.
Adapting existing Applications that use ABCI
In some cases, an existing Application using the legacy ABCI may need to be adapted to work with ABCI++ with as minimal changes as possible. In this case, of course, ABCI++ will not provide any advantage with respect to the existing implementation, but will keep the same guarantees already provided by ABCI. Here is how ABCI++ methods should be implemented. First of all, all the methods that did not change from ABCI 0.17.0 to ABCI 2.0, namelyEcho, Flush, Info, InitChain,
Query, CheckTx, ListSnapshots, LoadSnapshotChunk, OfferSnapshot, and ApplySnapshotChunk, do not need
to undergo any changes in their implementation.
As for the new methods:
-
PrepareProposalmust create a list of transactions by copying over the transaction list passed inRequestPrepareProposal.txs, in the same order. The Application must check whether the size of all transactions exceeds the byte limit (RequestPrepareProposal.max_tx_bytes). If so, the Application must remove transactions at the end of the list until the total byte size is at or below the limit. -
ProcessProposalmust setResponseProcessProposal.statusto accept and return. -
ExtendVoteis to setResponseExtendVote.extensionto an empty byte array and return. -
VerifyVoteExtensionmust setResponseVerifyVoteExtension.acceptto true if the extension is an empty byte array and false otherwise, then return. -
FinalizeBlockis to coalesce the implementation of methodsBeginBlock,DeliverTx, andEndBlock. Legacy applications looking to reuse old code that implementedDeliverTxshould wrap the legacyDeliverTxlogic in a loop that executes one transaction iteration per transaction inRequestFinalizeBlock.tx.
Commit, which is kept in ABCI++, no longer returns the AppHash. It is now up to
FinalizeBlock to do so. Thus, a slight refactoring of the old Commit implementation will be
needed to move the return of AppHash to FinalizeBlock.
Accommodating for vote extensions
In a manner transparent to the application, CometBFT ensures the node is provided with all the data it needs to participate in consensus. In the case of recovering from a crash, or joining the network via state sync, CometBFT will make sure the node acquires the necessary vote extensions before switching to consensus. If a node is already in consensus but falls behind, during catch-up, CometBFT will provide the node with vote extensions from past heights by retrieving the extensions withinExtendedCommit for old heights that it had previously stored.
We realize this is sub-optimal due to the increase in storage needed to store the extensions, we are
working on an optimization of this implementation which should alleviate this concern.
However, the application can use the existing retain_height parameter to decide how much
history it wants to keep, just as is done with the block history. The network-wide implications
of the usage of retain_height stay the same.
The decision to store
historical commits and potential optimizations, are discussed in detail in RFC-100
Handling upgrades to ABCI 2.0
If applications upgrade to ABCI 2.0, CometBFT internally ensures that the application setup is reflected in its operation. CometBFT retrieves from the application configuration the value ofVoteExtensionsEnableHeight( he,),
the height at which vote extensions are required for consensus to proceed, and uses it to determine the data it stores and data it sends to a peer that is catching up.
Namely, upon saving the block for a given height h in the block store at decision time
- if h ≥ he, the corresponding extended commit that was used to decide locally is saved as well
- if h < he, there are no changes to the data saved
- if hp ≥ he, f uses the extended commit to reconstruct the precommit votes with their corresponding extensions
- if hp < he, f uses the canonical commit to reconstruct the precommit votes, as done for ABCI 1.0 and earlier.