Tendermint Byzantine Consensus Algorithm

Zarko Milosevic

I. Definitions

A. Model

We consider a system composed of n server processes Π = {1, …, n} that communicate by exchanging messages. All messages sent between processes are digitally signed. Processes can be correct or faulty, where correct processes follow the algorithm and the faulty one may behave in an arbitrary way, i.e., we consider Byzantine faults. We assume than the number of faulty servers is limited to f.

We consider a partially synchronous system model: in all executions of the system, there is a bound Δ and an instant GST (Global Stabilization Time) such that all communication among correct processes after GST is reliable and Δ-timely, i.e., if a correct process p sends message m at time t ≥ GST to correct process q, then q will receive m before t + Δ. We do not make any assumption before GST. For example, messages among correct processes can be delayed, dropped or duplicated before GST. Spoofing/impersonation attacks are assumed to be impossible also before GST.

We assume that process steps (which might include sending and receiving messages) take zero time. Processes are equipped with clocks able to measure local timeouts.

B. Consensus

In the consensus problem, every process has an initial value from a set V and has eventually to irrevocably decide on a value from V. The problem is defined by an agreement, a termination, and a validity property.

• Agreement: No two correct processes decide differently.

• Termination: All correct processes eventually decide.

• Validity: If all correct processes have the same initial value, and no process is faulty, then this is the only possible decision value.

II. Tendermint

NOTE: The algorithm description and the corresponding proofs refers to the previous version of the pseudocode and needs to be updated!

In this section we present a new Byzantine consensus algorithm called Tendermint^{1} (see Algorithm 1). The algorithm requires n > 3f processes to tolerate at most f Byzantine faults, but for simplicity we present the algorithm for the case n = 3f + 1. The upon rules of Algorithm 1 are executed atomically.

1https://github.com/tendermint/tendermint

Algorithm 1 Tendermint consensus algorithm

1: Initialization: 2: v_{p} := v ∈ V /* value considered for consensus */ 3: round_{p} := 0 /* current round number */ 4: vote_{p} := nill /* locked value */ 5: ts_{p} := −1 /* if ts_{p} ≥ 0, it represents round in which vote_{p} has been locked */ 6: proof_{p} := ∅ /* set of signed messages as a proof that vote_{p} has been locked in round ts_{p} */ 7: state_{p} ∈ {init = 0, pre-prepared = 1, pre-committed = 2}, initially init 8: decision_{p} = nill

9: upon start do 10: StartRound(0)

11: Function StartRound(round) : 12: round_{p} ← round 13: state_{p} ← init 14: if proposer(round_{p}) = p then 15: if ts_{p} ≥ 0 then 16: proposal ← vote_{p} 17: else 18: proposal ← v_{p} 19: send ⟨PROPOSE, round_{p}, proposal, ts_{p}, proof_{p}⟩ to all 20: after timeoutP ropose execute OnT imeoutP ropose(round_{p})

21: upon receiving ⟨PROPOSE, round_{p}, vote, ts, proof⟩ from proposer(round_{p}) while state_{p} = init do 22: if isV alidP roposal(vote, ts, proof) then 23: send ⟨PREPARE, round_{p}, vote⟩ to all 24: state_{p} ← pre-prepared 25: if vote ≠ vote_{p} ∧ ts > ts_{p} then 26: vote_{p} ← nill 27: ts_{p} ← −1

28: Function OnT imeoutP ropose(round) : 29: if round = round_{p} ∧ state_{p} = init then 30: send ⟨PREPARE, round_{p}, nill⟩ to all 31: state_{p} ← pre-prepared

32: upon receiving ⟨PREPARE, round, vote⟩ from 2f + 1 processes while (round = round_{p} ∧ state_{p} ≤ pre-prepared) or

round > round_{p} do 33: vote_{p} ← vote 34: if vote = nill then 35: ts_{p} ← −1 36: else 37: ts_{p} ← round 38: if round > round_{p} then 39: round_{p} ← round 40: send ⟨PRECOMMIT, round_{p}, vote_{p}⟩ to all 41: state_{p} ← pre-committed 42: proof_{p} ← {set of received signed messages}

43: upon receiving ⟨PREPARE, round_{p}, ∗⟩ from 2f + 1 processes while state_{p} = pre-prepared /* a sign * denotes any value */ do 44: after timeoutP repare execute OnT imeoutP repare(round_{p})

45: Function OnT imeoutP repare(round) : 46: if round = round_{p} ∧ state_{p} = pre-prepared then 47: send ⟨PRECOMMIT, round_{p}, nill⟩ to all 48: state_{p} ← pre-committed

49: upon receiving (⟨PRECOMMIT, round, vote⟩ from 2f + 1 processes while round ≥ round_{p} do 50: if vote ≠ nill ∧ decision_{p} = nill then 51: decision_{p} ← vote 52: StartRound(round + 1)

53: upon receiving ⟨PRECOMMIT, round_{p}, ∗⟩ from 2f + 1 processes while state_{p} = pre-committed /* a sign * denotes any value */ do 54: after timeoutCommit execute OnT imeoutP recommit(round_{p})

55: Function OnT imeoutCommit(round) : 56: if round = round_{p} ∧ state_{p} = pre-committed then 57: StartRound(round + 1)

58: upon receiving message with round > round_{p} from f + 1 processes do 59: StartRound(round)

60: Function isV alidP roposal(vote, ts, proof) : 61: if vote = vote_{p} ∨ ts_{p} = −1 then 62: return true 63: if ts > ts_{p} and proof confirms that vote was locked at round ts then 64: return true 65: return false

It shares the basic mechanisms called locking and unlocking of a value with DSL algorithm for authenticated faults (Algorithm 2 from [?]), but algorithm structure and implementation of these mechanisms is different in Tendermint.

The algorithm proceeds in a sequence of rounds, where each round has a dedicated coordinator (called also proposer), that is defined with a deterministic function proposer. A correct process moves to a higher round either if a round timeout has expired (line 37) or if process receives a message from a correct process that is in a higher round (lines 26-27). We assume that messages from higher rounds are internally stored by the messaging layer (in case the rule from line 26 is satisfied, round transition is triggered), and then delivered to a process when it enters the corresponding round. Messages from lower rounds are dropped.

The locking mechanism has a role in ensuring agreement property of consensus, while the unlocking mechanism is related to termination property. Locking guarantees that two correct processes can lock only single value in some round r (they can however potentially lock different values in distinct rounds). A correct process can have only a single value locked at a particular point of algorithm execution. In Algorithm 1, a correct process p locks a value v in round r by setting a variable vote_{p} to v and a variable ts_{p} to r. The unlocking mechanism ensures that eventually (after GST) all correct processes can have only single value v locked. This helps to bring system into univalent configuration [?]. In Algorithm 1, a process p unlocks a value by setting vote_{p} to null and ts_{p} to -1. Initially, all correct process do not lock any value.

Each round is composed of three phases (steps) called propose, prepare and commit. A round starts with a propose phase (lines 28-37) in which a dedicated proposer sends its proposal to all processes. If a proposer p has locked some value v, then it proposes v; otherwise it proposes it’s initial value v_{p} (lines 32-35). In addition to proposed value, PROPOSE message also contains ts and a proof of locking a value (in case proposal is equal to locked value), that consist of a set of signed messages received by the process p in the round in which the value was locked.

When a correct process receives a proposal message from a designated proposer (lines 11-17), it checks if the message is a valid proposal (lines 41-46). A correct process p considers proposal as valid if (i) proposal value is equal to process locked value (vote_{p}) or if p hasn’t locked any value (ts_{p} = −1), or (ii) if proposal is different value but from a higher round and with a valid proof of locking. If a process accepts proposal as valid, it enters prepare phase by sending proposed value in PREPARE message to all processes. In case (ii), as there is a valid locked value in a higher round, the process unlocks it’s value.

When a correct process p receives 2f + 1 PREPARE messages for the same value v, then the process locks a value v (and saves a proof of locking into variable proof_{p}) and enters commit step by sending v in PRECOMMIT message (lines 18-23). When process received 2f + 1 PRECOMMIT messages for some value v, it decides value v.

Note that algorithm relies on state variable to prevent correct processes from sending more than one PREPARE and PRECOMMIT messages in a single round r. For simplicity, the current version of the algorithm does not provide termination condition, i.e., processes continue round execution even after deciding. We also assume that round timeout is being incremented in each round so that eventually all correct processes are able to receive all messages from correct processes in a round, before the round timeout expires. Furthermore, the algorithm solves only single instance consensus problem. The multi-instance version of Tendermint will be addressed in the next version of the document. However, it should be relatively straightforward to modify Algorithm 1 to get multi-instance version with the following modifications: (i) adding consensus instance number in each message, (ii) having per instance algorithm state variables and (iii) adding logic for starting next consensus instance.

A. Proof of correctness (sketch)

Agreement Let assume that a first correct process that decides is p and that p decides value v in round r. As p decided v this means that p receives at least 2f + 1 PRECOMMIT messages with value v in the round r (line 24). This means that at least f + 1 correct processes have locked value v in round r and have it’s variable vote set to v and ts variable set to r. As correct processes can send only single PRECOMMIT message per round, and there are at most f faulty processes, then no correct process can receive 2f + 1 PRECOMMIT messages in the round r for some other value v′ ≠ v. Therefore, no correct process can decide value different than v in the round r.

Now let consider the smallest round r′ > r in which correct process q locks some value v′. As q locks value v′ this means that q has received at least 2f + 1 PREPARE messages equal to v′ in the round r′. Therefore, at least f + 1 correct processes has sent v′ in PREPARE message in the round r′. As f + 1 correct process q′ locked value v in the round r, this means that one of those processes sent P ROP OSE message with value v′ in the round r′. According to function isV alidP roposal, q′ received valid proof of locking for value v′. This is in contradiction with the assumption that r′ is the smallest round in which some correct process locks some value (TODO: improve this part, it is not very precise as it could be reasoned that a Byzantine process could create correct proof of locking for some value v′ and round greater than r′ which can be proved impossible with the similar reasoning). As no correct process can lock any value different from v in rounds r′ > r, then no correct process can decide value different from v.

Termination

The Algorithm 1 does not terminate as the following scenario is possible: we consider rounds after GST, starting from round r and at round r a correct process p is the only process that locks some value v among the correct processes (0 ≤ ts_{p} ≤ r). Assume that no correct process has decided. Let assume that p would coordinate the round r′ > r. In rounds smaller than r′ p will reject any proposal from correct processes if it’s not equal to v (which is possible if all correct processes have different initial values), so it will not send P REP ARE message. As faulty processes could decide not to send any message, no correct processes will be able to lock a value and to decide in those rounds. Now let assume that in round r′ − 1 some correct process q locks value v′ ≠ v because a faulty process (together with all correct processes except p) sends PREPARE message with v′ to q. As q locks value v′ in round higher than r, q will reject proposal from p in round r′ so if faulty processes stay silent, again no correct processes can decide in that round. This scenario can be repeated also with the rounds k − 1 and k, in which the process q is proposer.

The property that needs to be ensured is the following: after GST, there exist a round such that a proposal from a correct process is accepted by all correct processes. The solution could be adding one more step (lockrelease step) at the end of each round where correct processes would send it’s locked value (together with ts and proof) to all processes, and then a correct process will release lock upon reception of proof of locking for a different value from higher round. We will also need to modify lines 35 and 36 to propose the value with the valid proof of locking from the highest round (if process is aware of such value) or initial value.