|
|
|
\section{Tendermint}
|
|
\label{sec:tendermint}
|
|
|
|
\newcommand\Disseminate{\textbf{Disseminate}}
|
|
|
|
\newcommand\Proposal{\mathsf{PROPOSAL}}
|
|
\newcommand\ProposalPart{\mathsf{PROPOSAL\mbox{-}PART}}
|
|
\newcommand\PrePrepare{\mathsf{INIT}}
|
|
\newcommand\Prevote{\mathsf{PREVOTE}}
|
|
\newcommand\Precommit{\mathsf{PRECOMMIT}}
|
|
\newcommand\Decision{\mathsf{DECISION}}
|
|
|
|
\newcommand\ViewChange{\mathsf{VC}}
|
|
\newcommand\ViewChangeAck{\mathsf{VC\mbox{-}ACK}}
|
|
\newcommand\NewPrePrepare{\mathsf{VC\mbox{-}INIT}}
|
|
\newcommand\coord{\mathsf{proposer}}
|
|
|
|
\newcommand\newHeight{newHeight}
|
|
\newcommand\newRound{newRound}
|
|
\newcommand\nil{nil}
|
|
\newcommand\prevote{prevote}
|
|
\newcommand\precommit{precommit}
|
|
\newcommand\commit{commit}
|
|
|
|
\newcommand\timeoutPropose{timeoutPropose}
|
|
\newcommand\timeoutPrevote{timeoutPrevote}
|
|
\newcommand\timeoutPrecommit{timeoutPrecommit}
|
|
\newcommand\proofOfLocking{proof\mbox{-}of\mbox{-}locking}
|
|
|
|
\begin{algorithm}[htb!]
|
|
\def\baselinestretch{1}
|
|
\scriptsize\raggedright
|
|
\begin{algorithmic}[1]
|
|
\SHORTSPACE
|
|
\INIT{}
|
|
\STATE $height_p := 0$ \COMMENT{current height, or consensus instance we are currently executing}
|
|
\STATE $round_p := 0$ \COMMENT{current round number}
|
|
\STATE $step_p \in \set{\propose=0, \prevote=1, \precommit=2, \commit=3}$, initially $\propose$ \COMMENT{current round step}
|
|
\STATE $lockedValue := nil$
|
|
\STATE $lockedRound := -1$
|
|
\STATE $validValue := nil$
|
|
\STATE $validRound := -1$
|
|
\ENDINIT
|
|
|
|
\SPACE
|
|
\UPON{start}
|
|
\STATE $StartRound(0)$
|
|
\ENDUPON
|
|
|
|
\SPACE
|
|
\FUNCTION{$StartRound(round)$} \label{line:tab:startRound}
|
|
\STATE $round_p \assign round$
|
|
\STATE $state_p \assign \propose$
|
|
\IF{$\coord(height_p, round_p) = p$}
|
|
\IF{$lockedValue_p \neq \nil$}
|
|
\STATE $proposal \assign lockedValue$
|
|
\ELSIF{$validValue_p \neq \nil$}
|
|
\STATE $proposal \assign validValue$
|
|
\ELSE
|
|
\STATE $proposal \assign getValue()$
|
|
\ENDIF
|
|
\STATE \PBroadcast\ $\li{\Proposal,height_p, round_p, proposal}$ to all \label{line:tab:send-proposal}
|
|
\ENDIF
|
|
\STATE \textbf{after} $\timeoutPropose$ execute $OnTimeoutPropose(height_p, round_p)$
|
|
\ENDFUNCTION
|
|
|
|
\SPACE
|
|
\UPON{receiving $\li{\Proposal,height_p,round_p, proposal}$ \From\ $\coord(height_p,round_p)$ \With\ $state_p = \propose$} \label{line:tab:recvProposal}
|
|
\IF{$lockedValue_p \neq \nil$} \label{line:tab:hasLockedValue}
|
|
\STATE \PBroadcast \ $\li{\Prevote,height_p,round_p,lockedValue_p}$ to all \label{line:tab:send-locked-value}
|
|
\ELSIF{$valid(proposal))$} \label{line:tab:locked-value-nil}
|
|
\STATE \PBroadcast \ $\li{\Prevote,height_p,round_p,proposal}$ to all \label{line:tab:send-prevote}
|
|
\ELSE
|
|
\STATE \PBroadcast \ $\li{\Prevote,height_p,round_p,\nil}$ to all \label{line:tab:send-prevote-nil}
|
|
\ENDIF
|
|
\STATE $state_p \assign \prevote$ \label{line:tab:setStateToPrevote}
|
|
\ENDUPON
|
|
|
|
\SPACE
|
|
\FUNCTION{$OnTimeoutPropose(height,round)$} \label{line:tab:onTimeoutPropose}
|
|
\IF{$height = height_p \wedge round = round_p \wedge state_p = \propose$}
|
|
\STATE \PBroadcast \ $\li{\Prevote,height_p,round_p,\nil}$ to all
|
|
\STATE $state_p \assign \prevote$
|
|
\ENDIF
|
|
\ENDFUNCTION
|
|
|
|
\SPACE
|
|
\UPON{receiving $\li{\Prevote,height_p, round_p,*}$ \From\ at least $2f+1$ processes \With\
|
|
$state_p \le \prevote$} \label{line:tab:recvAny2/3Prevote}
|
|
\STATE \textbf{after} $\timeoutPrevote$ execute $OnTimeoutPrevote(height_p, round_p)$ \label{line:tab:timeoutPrevote}
|
|
\ENDUPON
|
|
|
|
\SPACE
|
|
\UPON{receiving $\li{\Prevote,height_p, round_p,v}$ \From\ at least $2f+1$ processes \With\
|
|
$state_p \le \prevote$} \label{line:tab:recvPrevote}
|
|
\IF{$v \neq \nil$}
|
|
\STATE $lockedValue_p \assign v$ \label{line:tab:setLockedValue}
|
|
\STATE $lockedRound_p \assign round_p$ \label{line:tab:setLockedRound}
|
|
\STATE \PBroadcast \ $\li{\Precommit,height_p,round_p,lockedValue_p}$ to all \label{line:tab:send-precommit}
|
|
\ELSE
|
|
\STATE \PBroadcast \ $\li{\Precommit,height_p,round_p,\nil}$ to all \label{line:tab:send-precommit-nil}
|
|
\ENDIF
|
|
\STATE $state_p \assign \precommit$ \label{line:tab:setStateToCommit}
|
|
\ENDUPON
|
|
|
|
\SPACE
|
|
\UPON{receiving $\li{\Prevote,height_p,round,v}$ \From\ at least $2f+1$ processes \With\
|
|
$round > lockedRound_p$} \label{line:tab:unlockRule}
|
|
\IF{$v \neq lockedValue_p$}
|
|
\STATE $lockedValue_p \assign \nil$
|
|
\STATE $lockedRound_p \assign -1$
|
|
\ENDIF
|
|
\ENDUPON
|
|
|
|
\SPACE
|
|
\UPON{receiving $\li{\Prevote,height_p,round,v}$ \From\ at least $2f+1$ processes \With\
|
|
$v \neq \nil \wedge round > validRound_p$} \label{line:tab:validValueRule}
|
|
\STATE $validValue_p \assign v$ \label{line:tab:setValidValue}
|
|
\STATE $validRound_p \assign round$ \label{line:tab:setValidRound}
|
|
\ENDUPON
|
|
|
|
\SPACE
|
|
\FUNCTION{$OnTimeoutPrevote(height,round)$} \label{line:tab:onTimeoutPrevote}
|
|
\IF{$height = height_p \wedge round = round_p \wedge state_p = \prevote$}
|
|
\STATE \PBroadcast \ $\li{\Precommit,height_p,round_p,\nil}$ to all \label{line:tab:precommit-nil-onTimeout}
|
|
\STATE $state_p \assign \precommit$
|
|
\ENDIF
|
|
\ENDFUNCTION
|
|
|
|
\SPACE
|
|
\UPON{receiving $\li{\Precommit,height_p,round,v}$ \From\ at least $2f+1$ processes \With\ $state_p < \commit$} \label{line:tab:onPrecommitRule}
|
|
\IF{$vote \neq \nil$}
|
|
\STATE $decide(height_p, v)$ \label{line:tab:decide}
|
|
\STATE$height_p \assign height_p + 1$
|
|
\STATE reset $round_p$, $step_p$, $lockedRound_p$, $lockedValue_p$, $validValue_p$ and $validRound$ to init values
|
|
\STATE $StartRound(0)$
|
|
\ENDIF
|
|
\IF{$round_p = round$}
|
|
\STATE $StartRound(round_p+1)$
|
|
\ENDIF
|
|
\ENDUPON
|
|
|
|
\SPACE
|
|
\UPON{receiving $\li{\Precommit,height_p,round_p,*}$ \From\ at least $2f+1$ processes \With\ $state_p = \precommit$} \label{line:tab:startTimeoutPrecommit}
|
|
\STATE \textbf{after} $\timeoutPrecommit$ execute $OnTimeoutPrecommit(height_p, round_p)$
|
|
\ENDUPON
|
|
|
|
\SPACE
|
|
\FUNCTION{$OnTimeoutPrecommit(height,round)$} \label{line:tab:onTimeoutPrecommit}
|
|
\IF{$height = height_p \wedge round = round_p \wedge state_p = \precommit$}
|
|
\STATE $StartRound(round_p+1)$
|
|
\ENDIF
|
|
\ENDFUNCTION
|
|
|
|
\SPACE
|
|
\UPON{receiving message \textbf{with} $height = height_p$ and $round > round_p$ \From\ at least $f+1$ processes} \label{line:tab:skipRounds}
|
|
\STATE $StartRound(round)$
|
|
\ENDUPON
|
|
|
|
\end{algorithmic}
|
|
\caption{Tendermint consensus algorithm}
|
|
\label{alg:tendermint}
|
|
\end{algorithm}
|
|
|
|
|
|
In this section we present a new Byzantine consensus algorithm called Tendermint\footnote{https://github.com/tendermint/tendermint}
|
|
(see Algorithm~\ref{alg:tendermint}).
|
|
The algorithm requires $N > 3f$, i.e., faulty processes have together the voting power that is smaller than one third of the total voting power. For simplicity we present the algorithm for the case $N = 3f + 1$. We use the notation "upon receiving message $\li{TAG, h, r, v}$ from at least $X$ processes", to denote that a message with the given field values (* denotes any value) has been received from number of processes with the aggregate voting power equals at least $X$.
|
|
The upon rules of Algorithm~\ref{alg:tendermint} are executed atomically.
|
|
Algorithm shares the basic mechanisms called the locking and the unlocking of a value with the DLS algorithm for authenticated faults (the Algorithm 2 from \cite{DLS88:jacm}), but the algorithm structure and the implementation of these mechanisms is different in Tendermint.
|
|
|
|
Tendermint consensus algorithm is optimized for the gossip based communication so the consensus layer creates a minimum number of information that needs to be gossiped. Processes exchange only three message types: $\Proposal$, $\Prevote$ and $\Precommit$, and there is only a single mode of execution, i.e., there is no separation between the normal and the recovery mode, which is the case in PBFT-like protocols (e.g., \cite{CL02:tcs}, \cite{Ver09:spinning} or \cite{Cle09:aardvark}). We believe that this makes the protocol simpler to understand and implement correctly, as one of the Tendermint goals is proposing \emph{understandable} Byzantine consensus protocol following Raft path~\cite{Ongaro14:raft}.
|
|
|
|
The algorithm proceeds in a sequence of rounds, where each round has a dedicated coordinator (called proposer). The assignment scheme of rounds to coordinators is known to all processes and is given as a function $\coord(height, round)$ returning the coordinator for round $r$ in the consensus instance $height$. In the algorithm~\ref{alg:tendermint} processes agree (decides) on a sequence of values (solves multiple instances problem) by sequentially executing one consensus instances after the other.
|
|
|
|
Tendermint decomposes the consensus problem into four relatively independent mechanisms (or subproblems) which are discussed in the following subsections:
|
|
\begin{itemize}
|
|
\item locking mechanism
|
|
\item unlocking mechanism
|
|
\item finding the right value to propose
|
|
\end{itemize}
|
|
|
|
\subsection{Locking mechanism}
|
|
\label{sec:locking}
|
|
|
|
A process in Tendermint initially does not have any preference about what the decided value should be ($lockedValue := nil$). The process learns about possible decision value from the coordinator of the current round. As a proposer might be a faulty process, different processes
|
|
might receive different suggestions. Using the locking mechanism, correct processes try to converge to a single value within the single round boundary\footnote{We will explain in the part related to safety how we will build on this mechanism to ensure safety across round boundaries.}.
|
|
If during the locking phase, a sufficient number of correct processes succeed in agreeing on the common value, then they will later decide on that value.
|
|
|
|
More formally, the locking mechanism ensures that two correct processes can lock only a single value in some round $r$ (they can however potentially lock different values in distinct rounds). In Algorithm~\ref{alg:tendermint}, a correct process $p$ locks a value $v$ in round $r$ by setting a variable $lockedValue_p$ to $v$ and a variable $lockedRound_p$ to $r$ (see line~\ref{line:tab:setLockedValue} and \ref{line:tab:setLockedRound}). A correct process can have only a single value locked at a particular point of the algorithm execution.
|
|
However, before a value is decided, a process might change his mind and unlock its current value using the unlocking mechanism.
|
|
|
|
Locking phase starts by a process suggesting what value should be locked in the current round. This is achieved by sending a $\Prevote$ message to all processes. If a correct process has locked some value $v$ ($lockedValue = v \neq \nil$), he votes for $v$ and ignores the suggested value by the proposer (see lines~\ref{line:tab:hasLockedValue}-\ref{line:tab:send-locked-value}). Otherwise, a process might accept the value suggested by the proposer if the proposal is valid\footnote{Validity of a value is an application specific and defined by the function \emph{valid()}.}, and vote for that value (see lines~\ref{line:tab:locked-value-nil}-\ref{line:tab:setStateToPrevote}). In case the suggested value is invalid or a process has not received any proposal from the current coordinator within $\timeoutPropose$ from the start of the current round, it votes with $\nil$ value (see line~\ref{line:tab:send-prevote-nil} and line \ref{line:tab:onTimeoutPropose}). Note that $\nil$ is special value that can not be locked, and it is used to inform others that the algorithm step has not completed successfully; in case of the locking phase, a process has not received a valid value from the proposer on time.
|
|
|
|
A process locks value $v$ if it receives at least $2f+1$ $\Prevote$ messages for the value $v$ in the current round (see lines~\ref{line:tab:recvPrevote}-\ref{line:tab:setLockedRound}). A correct process sends only a single $\Prevote$ message in every round. Algorithm relies on the $state$ variable to prevent correct processes from sending more than one $\Prevote$ message in a single round. As a correct process sends a single $\Prevote$ message in a round, and $f$ is the total voting power of faulty processes, two correct processes cannot receive $2f+1$ $\Prevote$ messages for different values in a single round. So if two correct processes lock some value in a given round $r$, then it must be the same value. We will prove this formally in the section~\ref{sec:proof}.
|
|
|
|
The locking phase ends by processes informing others if they locked some value in the current round (or not) by sending $\Precommit$ message. If a process has locked some value $v$ in the current round, then this value is sent in the $\Precommit$ message (see line~\ref{line:tab:send-precommit}); otherwise $\nil$ is sent (line~\ref{line:tab:send-precommit-nil} and \ref{line:tab:precommit-nil-onTimeout}). A correct process sends only single $\Precommit$ message in every round. Similarly as with the $\Prevote$ message, the algorithm relies on the $state$ variable to prevent correct processes from sending more than one $\Precommit$ message in a single round.
|
|
|
|
\subsection{Unlocking mechanism}
|
|
\label{sec:unlocking}
|
|
|
|
Initially, a correct process does not lock any value. During the algorithm execution, processes might end up locking different values in different rounds. As we discussed in the locking section (section~\ref{sec:locking}), a correct process that has locked some value will vote
|
|
only for that value at the beginning of the locking phase. With correct processes voting for different values during the locking phase, it is not possible to reach an agreement what value should be locked, and later decided. Without unlocking mechanism in place, the algorithm could loop forever in that state, and no decision will be reached. The unlocking mechanism is therefore essential for the algorithm termination as it allows processes to release locks and try to converge to a common value in the following rounds. In Algorithm~\ref{alg:tendermint}, a correct process $p$ unlocks a value by setting $lockedValue_p$ to $\nil$ and $lockedRound_p$ to $-1$.
|
|
|
|
A correct process $p$ releases its locked value if he observes that some correct process might\footnote{We say that other process \emph{might} have locked different value as we do not get a direct message from that process claiming that he locked a value with the corresponding proof of locking. We infer this information by observing $\Prevote$ messages.} have locked a different value in the round $r$ higher than the current $lockedRound_p$ ($r > lockedRound_p$). This is the case if a process $p$ receives at least $2f+1$ $\Prevote$ messages with value $v' \neq lockedValue_p$ and $round > lockedRound_p$ (see the rule starting at line~\ref{line:tab:unlockRule}). The process also implicitly releases a lock on its current value by locking a different value at lines~\ref{line:tab:setLockedValue} and \ref{line:tab:setLockedRound}.
|
|
|
|
The unlocking mechanism, together with the underlying gossip layer, ensures the following properties:
|
|
|
|
\begin{itemize}
|
|
\item (i) if a correct process $p$ locks a value $v$ in a round $r$ during a good period (period of synchrony), then at the end of the round $r$ all correct processes will lock either $v$ or they will not lock any value
|
|
\item (ii) if a correct process $p$ locks a value $v$ in a round $r$, then every correct process $q$ that has locked a different value in some of the previous rounds, i.e., $lockedValue_q \neq v$ and $lockedRound_q < r$, will eventually unlock it's value.
|
|
\end{itemize}
|
|
|
|
From the property (ii) follows that after a sufficiently big good period in which no correct process manages to lock a value, all correct processes will lock either some common value $v$ or they will not lock any value (where $v$ is the value locked in the highest round $r$ by some correct process). This follows from the fact that processes keep resending messages at the gossip layer, so during good periods, $\Prevote$ messages that led to the correct process locking the value $v$ in the round $r$ are received by all correct processes. Therefore, they either stay with $v$ as its locked value, or unlock its current value.
|
|
|
|
Both properties ensure that during a sufficiently long good period, there will eventually be a round, such that at the beginning of this round, all correct processes will lock the same value or they will not lock any value. As we will explain in the following section (\ref{sec:findRighValue}), this is one of the essential mechanisms for ensuring termination of the Tendermint consensus algorithm.
|
|
|
|
\subsection{Finding the right value to propose}
|
|
\label{sec:findRighValue}
|
|
|
|
As we discussed in section~\ref{sec:locking}, a correct process in Tendermint votes always for its locked value (if it exists) or for the \emph{valid} value suggested by the coordinator of the current round. In order to reach a decision in some round $r$ it is necessary that all correct processes vote for the same value in that round (we do not depend on faulty processes for termination). Then correct processes can lock that value, and later decide. Tendermint relies on the coordinator to ensure that processes that have not locked any value, vote for the same value. As a correct process always votes for its locked value, then several conditions need to be fulfilled just before the round $r$ starts so we can achieve such preferable scenario:
|
|
|
|
\begin{enumerate}
|
|
\item if a correct process $p$ has locked some value $v$ at the beginning of the round $r$, then all other correct processes have either locked $v$ or they have not locked any value. \label{enum:termination:v-is-locked}
|
|
\item the proposer is a correct process, and a value suggested by the proposer is $v$. \label{enum:termination:v-is-proposed}
|
|
\end{enumerate}
|
|
|
|
In case those conditions hold, all correct processes will lock $v$ in the round $r$ and later decide.
|
|
|
|
The existence of a round where the condition \ref{enum:termination:v-is-locked}) holds, follows from the unlocking mechanism. The first part of the condition \ref{enum:termination:v-is-proposed}) follows from the fact that $\coord$ function eventually provides a correct proposer. The second part of the condition \ref{enum:termination:v-is-proposed}) is more tricky as the proposer needs to suggest exactly the value that is locked by correct processes. If the proposer has locked $v$ at the beginning of the round $r$, then it trivially follows from the condition \ref{enum:termination:v-is-locked}). Otherwise, we need an additional mechanism that ensures that the correct proposer suggests $v$.
|
|
|
|
In order to ensure this condition, Tendermint has an additional mechanism for determining what is the good value to suggest such that this condition holds. It is implemented using variables $validValue$ and $validRound$. Using these two variables every process tracks the highest round ($validRound$) in which correct process might have locked some value ($validValue$). So whenever process observes at least $2f+1$ $\Prevote$ messages with $round > validRound_p$ (see the rule at line~\ref{line:tab:validValueRule}) for some value $v \neq \nil$,
|
|
it updates $validValue$ and $validRound$ variables.
|
|
|
|
This mechanism, together with the underlying gossip layer, ensures the following properties:
|
|
\begin{itemize}
|
|
\item (i) if a correct process $p$ locks a value $v$ in a round $r$ during a good period (period of synchrony), then at the end of the round $r$ all correct processes will have $validValue$ equal to $v$ and $validRound$ equal to $r$.
|
|
\item (ii) if no correct process $p$ locks some value during a sufficiently long good period, then all correct processes will eventually have $validValue$ equal to the same value $v$.
|
|
\end{itemize}
|
|
|
|
If during a good period, no correct process locks a value during a sufficiently long period (so the property i) does not hold), then
|
|
all correct processes will have $validValue$ equal to $v$ due to the following reasoning: During this time period, all correct processes will receive delayed $\li{Prevote}$ messages from previous rounds (because of resending messages at the gossip layer). Now let denote with $r$ and $v$, a round and a value, such that there are at least $2f+1$ $\li{Prevote}$ messages with $r$ and $v$, and $r$ is the highest among such $(*, r)$ pairs. Then all correct processes will receive those messages and set $validValue$ to $v$.
|
|
|
|
In both cases, during sufficiently long good period, there will eventually be a round, such that at the beginning of this round, all correct processes will have $validValue$ equal to the same value; and in case some correct process has locked some value $v$, then the $validValue$ will be equal to $v$.
|
|
|
|
The unlocking mechanism and the mechanism for determining the right proposal value ensures that during a period of synchrony, there will be a round $r$ such that (i) all correct processes either lock the same value $v$ or they do not lock any value, and (ii) the $validValue$ is equal to $v$ at all correct processes. If a proposer of such round $r$ is correct, then he proposes $v$, all correct processes vote for $v$ during the locking phase; therefore they lock $v$, send a $\Precommit$ message with $v$ and later decide. The formal proof of termination is provided in section~\ref{sec:proof}.
|
|
|
|
|