CometBFT’s expected behavior
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.