order | title |
---|---|
4 | Tendermint's expected behavior |
This section describes what the Application can expect from Tendermint.
The Tendermint consensus algorithm 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 and there will be no byzantine process. In these frequent, benign conditions:
PrepareProposal
will be called exactly once at the proposer process of round 0, height h;ProcessProposal
will be called exactly once at all processes except the proposer of round 0, and
will return accept in its Response*
;ExtendVote
will be called exactly once at all processesVerifyVoteExtension
will be called n-1 times at each validator process, where n is the number of validators; andFinalizeBlock
will be finally called at all processes at the end of height h, conveying the same prepared
block that all calls to PrepareProposal
and ProcessProposal
had previously reported for height h.However, the Application logic must be ready to cope with any possible run of Tendermint for a given height, including bad periods (byzantine proposers, network being asynchronous). In these cases, the sequence of calls to ABCI++ methods may not be so straighforward, but the Application should still be able to handle them, e.g., without crashing. The purpose of this section is to define what these sequences look like an a precise way.
As mentioned in the Basic Concepts section, Tendermint acts as a client of ABCI++ and the Application acts as a server. Thus, it is up to Tendermint to determine when and in which order the different ABCI++ methods will be called. A well-written Application design should consider any of these possible sequences.
The following grammar, written in case-sensitive Augmented Backus–Naur form (ABNF, specified in IETF rfc7405), specifies all possible sequences of calls to ABCI++, taken by a correct process, across all heights from the genesis block, including recovery runs, from the point of view of the Application.
start = clean-start / recovery
clean-start = init-chain [state-sync] consensus-exec
state-sync = *state-sync-attempt success-sync info
state-sync-attempt = offer-snapshot *apply-chunk
success-sync = offer-snapshot 1*apply-chunk
recovery = info *consensus-replay consensus-exec
consensus-replay = decide
consensus-exec = (inf)consensus-height
consensus-height = *consensus-round decide
consensus-round = proposer / non-proposer
proposer = prepare-proposal extend-proposer
extend-proposer = *got-vote [extend-vote] *got-vote
non-proposer = *got-vote [extend-non-proposer] *got-vote
extend-non-proposer = process-proposal *got-vote [extend-vote]
init-chain = %s"<InitChain>"
offer-snapshot = %s"<OfferSnapshot>"
apply-chunk = %s"<ApplySnapshotChunk>"
info = %s"<Info>"
prepare-proposal = %s"<PrepareProposal>"
process-proposal = %s"<ProcessProposal>"
extend-vote = %s"<ExtendVote>"
got-vote = %s"<VerifyVoteExtension>"
decide = %s"<FinalizeBlock>"
TODO Still hesitating... introduce n as total number of validators, so that we can bound the occurrences of
got-vote
in a round.
We have kept some of the ABCI++ methods out of the grammar, in order to keep it as clear and concise as possible. A common reason for keeping all these methods out is that they all can be called at any point in a sequence defined by the grammar above. Other reasons depend on the method in question:
Echo
and Flush
are only used for debugging purposes. Further, their handling by the Application should be trivial.CheckTx
is detached from the main method call sequence that drives block execution.Query
provides read-only access to the current Application state, so handling it should also be independent from
block execution.ListSnapshots
and LoadSnapshotChunk
provide read-only access to the Application's previously created
snapshots (if any), and help populate the parameters of OfferSnapshot
and ApplySnapshotChunk
at a process performing
state-sync while bootstrapping. Unlike ListSnapshots
and LoadSnapshotChunk
, both OfferSnapshot
and ApplySnapshotChunk
are included in the grammar.Finally, method Info
is a special case. The method's purpose is three-fold, it can be used
We have left 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.
start = clean-start / recovery
InitChain
, then it may optionally
start a state-sync mechanism to catch up with other processes. Finally, it enters normal
consensus execution.clean-start = init-chain [state-sync] consensus-exec
ApplySnapshotChunk
method 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 via ApplySnapshotChunk
.
At the end of a successful attempt, Tendermint calls Info
to make sure the recontructed state's
AppHash matches the one in the block header at the corresponding height.state-sync = *state-sync-attempt success-sync info state-sync-attempt = offer-snapshot *apply-chunk success-sync = offer-snapshot 1*apply-chunk
Info
to know from which height it needs to replay decisions
to the Application. To replay a decision, Tendermint simply calls FinalizeBlock
with the decided
block at that height. After this, Tendermint enters nomal consensus execution.recovery = info *consensus-replay consensus-exec consensus-replay = decide
consensus-exec
is 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).consensus-exec = (inf)consensus-height
FinalizeBlock
.
In each round, the sequence of method calls depends on whether the local process is the proposer or not.consensus-height = *consensus-round decide consensus-round = proposer / non-proposer
PrepareProposal
.
No calls to methods related to vote extensions (ExtendVote
, VerifyVoteExtension
) can be called
in the present round before PrepareProposal
. Once PrepareProposal
is called, calls to
ExtendVote
and VerifyVoteExtension
can come in any order, although the former will be called
at most once in this round.proposer = prepare-proposal extend-proposer extend-proposer = *got-vote [extend-vote] *got-vote
ProcessProposal
at most once. At most one call to ExtendVote
can occur only after ProcessProposal
is called.
A number of calls to VerifyVoteExtension
can occur in any order with respect to ProcessProposal
and ExtendVote
throughout the round.non-proposer = *got-vote [extend-non-proposer] *got-vote extend-non-proposer = process-proposal *got-vote [extend-vote]
init-chain = %s"<InitChain>" offer-snapshot = %s"<OfferSnapshot>" apply-chunk = %s"<ApplySnapshotChunk>" info = %s"<Info>" prepare-proposal = %s"<PrepareProposal>" process-proposal = %s"<ProcessProposal>" extend-vote = %s"<ExtendVote>" got-vote = %s"<VerifyVoteExtension>" decide = %s"<FinalizeBlock>"
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 advange 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 to ABCI++, namely Echo
, 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:
PrepareProposal
should set ResponsePrepareProposal.modified_tx
to false and return.ProcessProposal
should set ResponseProcessProposal.accept
to true and return.ExtendVote
should set ResponseExtendVote.extension
to an empty byte array and return.VerifyVoteExtension
should set ResponseVerifyVoteExtension.accept
to true if the extension is an empty byte array
and false otherwise, then return.FinalizeBlock
should coalesce the implementation of methods BeginBlock
, DeliverTx
, EndBlock
, and Commit
.
The logic extracted from DeliverTx
should be wrappped by a loop that will execute as many times as
transactions exist in RequestFinalizeBlock.tx
.