8.9 KiB
| order | title |
|---|---|
| 6 | ABCI++ extra |
Introduction
In the section CometBFT's expected behaviour, we presented the most common behaviour, usually referred to as the good case. However, the grammar specified in the same section is more general and covers more scenarios that an Application designer needs to account for.
In this section, we give more information about these possible scenarios. We focus on methods
introduced by ABCI++: PrepareProposal and ProcessProposal. Specifically, we concentrate
on the part of the grammar presented below.
consensus-height = *consensus-round decide commit
consensus-round = proposer / non-proposer
proposer = [prepare-proposal process-proposal]
non-proposer = [process-proposal]
We can see from the grammar that we can have several rounds before deciding a block. The reasons why one round may not be enough are:
- network asynchrony, and
- a Byzantine process being the proposer.
If we assume that the consensus algorithm decides on block X in round r, in the rounds
r' <= r, CometBFT can exhibit any of the following behaviours:
- Call
PrepareProposaland/orProcessProposalfor blockX. - Call
PrepareProposaland/orProcessProposalfor blockY \neq X. - Does not call
PrepareProposaland/orProcessProposal.
In the rounds in which the process is the proposer, CometBFT's PrepareProposal call is always followed by the
ProcessProposal call. The reason is that the process also broadcasts the proposal to itself, which is locally delivered and triggers the ProcessProposal call.
The proposal processed by ProcessProposal is the same as what was returned by any of the preceding PrepareProposal invoked for the same height and round.
While in the absence of restarts there is only one such preceding invocations, if the proposer restarts there could have been one extra invocation to PrepareProposal for each restart.
As the number of rounds the consensus algorithm needs to decide in a given run is a priori unknown, the application needs to account for any number of rounds, where each round can exhibit any of these three behaviours. Recall that the application is unaware of the internals of consensus and thus of the rounds.
Possible scenarios
The unknown number of rounds we can have when following the consensus algorithm yields a vast number of
scenarios we can expect. Listing them all is unfeasible. However, here we give several of them and draw the
main conclusions. Specifically, we will show that before block X is decided:
- On a correct node,
PrepareProposalmay be called multiple times and for different blocks (Scenario 1). - On a correct node,
ProcessProposalmay be called multiple times and for different blocks (Scenario 2). - On a correct node,
PrepareProposalandProcessProposalfor blockXmay not be called (Scenario 3). - On a correct node,
PrepareProposalandProcessProposalmay not be called at all (Scenario 4).
Basic information
Each scenario is presented from the perspective of a process p. More precisely, we show what happens in
each round's step of the Tendermint consensus algorithm. While in
practice the consensus algorithm works with respect to voting power of the validators, in this document
we refer to number of processes (e.g., n, f+1, 2f+1) for simplicity. The legend is below:
Round X
- Propose: Describes what happens while
step_p = propose. - Prevote: Describes what happens while
step_p = prevote. - Precommit: Describes what happens while
step_p = precommit.
Scenario 1
p calls ProcessProposal many times with different values.
Round 0
- Propose: The proposer of this round is a Byzantine process, and it chooses not to send the proposal
message. Therefore, $p$'s
timeoutProposeexpires, it sendsPrevotefornil, and it does not callProcessProposal. All correct processes do the same. - Prevote:
peventually receives2f+1Prevotemessages forniland startstimeoutPrevote. WhentimeoutPrevoteexpires it sendsPrecommitfornil. - Precommit:
peventually receives2f+1Precommitmessages forniland startstimeoutPrecommit. When it expires, it moves to the next round.
Round 1
- Propose: A correct process is the proposer in this round. Its
validValueisnil, and it is free to generate and propose a new blockY. Processpreceives this proposal in time, callsProcessProposalfor blockY, and broadcasts aPrevotemessage for it. - Prevote: Due to network asynchrony less than
2f+1processes sendPrevotefor this block. Therefore,pdoes not updatevalidValuein this round. - Precommit: Since less than
2f+1processes sendPrevote, no correct process will lock on this block and sendPrecommitmessage. As a consequence,pdoes not decide onY.
Round 2
- Propose: Same as in Round 1, just another correct process is the proposer, and it
proposes another value
Z. Processpreceives the proposal on time, callsProcessProposalfor new blockZ, and broadcasts aPrevotemessage for it. - Prevote: Same as in Round 1.
- Precommit: Same as in Round 1.
Rounds like these can continue until we have a round in which process p updates its validValue or until
we reach round r where process p decides on a block. After that, it will not call ProcessProposal
anymore for this height.
Scenario 2
p calls PrepareProposal many times with different values.
Round 0
- Propose: Process
pis the proposer in this round. ItsvalidValueisnil, and it is free to generate and propose new blockY. Before proposing, it callsPrepareProposalforY. After that, it broadcasts the proposal, delivers it to itself, callsProcessProposaland broadcastsPrevotefor it. - Prevote: Due to network asynchrony less than
2f+1processes receive the proposal on time and sendPrevotefor it. Therefore,pdoes not updatevalidValuein this round. - Precommit: Since less than
2f+1processes sendPrevote, no correct process will lock on this block and send non-nilPrecommitmessage. As a consequence,pdoes not decide onY.
After this round, we can have multiple rounds like those in Scenario 1. The important thing
is that process p should not update its validValue. Consequently, when process p reaches the round
when it is again the proposer, it will ask the mempool for the new block again, and the mempool may return a
different block Z, and we can have the same round as Round 0 just for a different block. As
a result, process p calls PrepareProposal again but for a different value. When it reaches round r
some process will propose block X and if p receives 2f+1 Precommit messages, it will decide on this
value.
Scenario 3
p calls PrepareProposal and ProcessProposal for many values, but decides on a value for which it did
not call PrepareProposal or ProcessProposal.
In this scenario, in all rounds before r we can have any round presented in Scenario 1 or
Scenario 2. What is important is that:
-
no proposer proposed block
Xor if it did, processp, due to asynchrony, did not receive it in time, so it did not callProcessProposal, and -
if
pwas the proposer it proposed some other value\neq X.
Round r
- Propose: A correct process is the proposer in this round, and it proposes block
X. Due to asynchrony, the proposal message arrives to processpafter itstimeoutProposeexpires and it sendsPrevotefornil. Consequently, processpdoes not callProcessProposalfor blockX. However, the same proposal arrives at other processes before theirtimeoutProposeexpires, and they sendPrevotefor this proposal. - Prevote: Process
preceives2f+1Prevotemessages for proposalX, updates correspondingly itsvalidValueandlockedValueand sendsPrecommitmessage. All correct processes do the same. - Precommit: Finally, process
preceives2f+1Precommitmessages, and decides on blockX.
Scenario 4
Scenario 3 can be translated into a scenario where p does not call PrepareProposal and
ProcessProposal at all. For this, it is necessary that process p is not the proposer in any of the
rounds 0 <= r' <= r and that due to network asynchrony or Byzantine proposer, it does not receive the
proposal before timeoutPropose expires. As a result, it will enter round r without calling
PrepareProposal and ProcessProposal before it, and as shown in Round r of Scenario 3 it
will decide in this round. Again without calling any of these two calls.