【一文搞懂】从原文看Tendermint共识机制

本文是论文:The latest gossip on BFT consensus的阅读笔记,主要对论文的关键内容进行翻译和对一些概念进行解释,并且写一些本人在阅读过程中的个人理解。本文需要一定的区块链与共识算法的基础知识,如果阅读过程中有困难或名词不懂,请移步综述论文阅读笔记+概念解释文章区块链共识算法综述论文阅读笔记

本文所有的参考文章链接都在文末,强烈建议认真阅读原文并支持原作者,侵删。

带着问题看论文:

  • Tendermint共识算法和PBFT的区别是什么?
  • 为什么只能抵御33%的拜占庭节点?
  • 当系统中有33%的拜占庭节点时共识过程是怎样的?如何确保结果的一致性和正确性?
  • Tendermint共识算法的不足是什么?有哪些可以改进的地方?

The latest gossip on BFT consensus

摘要:本文提出了一种新的分布式网络事件排序协议Tendermint。该问题通常被称为拜占庭容错共识或原子广播,近年来由于基于区块链的数字货币,如比特币和以太坊的广泛成功,在没有中央机构的情况下,成功地解决了公共环境下的问题,引起了极大的关注。Tendermint将这方面的经典学术工作进行了现代化改造,并通过节点间的对等Gossip协议简化了BFT算法的设计

1 介绍

共识机制是复制状态机(SMR,概念可参考文章)中的核心机制之一,它保证了所有副本以相同顺序接受相同的事务,由此保证了所有副本都进行了相同的复制,最终达到了相同的状态。传统的基于SMR的系统通常具有副本数少、地理上集中式(局域网)、属于单个管理域的特点,因此这些系统只需要处理良性故障(非拜占庭故障,例如故障、崩溃),而对于恶性故障(拜占庭故障,例如作恶、欺骗,概念可参考文章)则一般采取忽略的应对方法。

但随着区块链和加密货币的流行,基于SMR的系统都逐渐面临新的挑战,例如节点极多、地理上分布式等,而节点之间不可能全连接也就使得新的基于SMR的系统必须是基于Gossip的对等协议实现的,即节点只与部分其他节点通信。

Tendermint共识算法灵感来源于PBFT算法(概念可参考文章)和认证故障的DLS算法。该算法以轮为单位运行,每轮都有一个专门的提议者(领导者),每轮至多生成一个区块,一个区块中可以包括多个交易,正常情况下一轮需要经过三个通信步骤。与DLS算法不同的是,Tendermint中的一个round表示一系列通信步骤,而非DLS算法中的单一通信。

Tendermint共识算法的主要创新点是提出了一种新的终止机制,这是因为现有的大多数同步系统的共识算法大多采用下图的流程来终止一轮共识过程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dPx3VvRa-1692028771694)(D:\App\Typora\ImageSpace\image-20230809155222598.png)]

其中:vi表示每个进程在下一轮中可接受的提议值(这个值有时是value本身,有时是签名信息),p1被选为下一轮的提议者,它在接收到其他节点传来的vi后选择合适的提议值x,并将x连带着从其他进程中接收到的值签名后的结果广播出去——发送x是为了让其他进程知道提议者提议了什么值,发送后者是为了让其他进程通过公私钥配对的方式确认提议者的身份,以防其他节点冒充提议者。

这种终止过程的问题在于随着系统中的节点数增加,通信复杂度会显著增加。而Tendermint则设计了一种新的终止机制,这种机制无需额外的通信,运行过程也完全类似于PBFT中的正常通信过程。

2 定义

2.1 模型的一些定义

  1. 模型定义于以下场景:

    • 一个通过交换消息进行通信的进程模型中可以出现拜占庭错误,即节点可以故意发错误消息或不发消息;

    • 每个节点在每轮共识阶段都有一定的投票权,投票权可以为零,节点之间的投票权可以不同;

    • 模型中的节点不属于同一个行政区(局域网),因此节点之间不可能采用全连接的方式通信;

    • 每个节点隶属于所有节点的某个子集,并称之为peers,因此所有正确节点之间都有一个间接通信通道;

    • 节点之间的通信使用Gossip协议;

  2. 我们以半同步系统模型来进行建模,即假设存在一个已知的界限时间△和一个全局稳定时间(Global Stabilization Time,GST),系统在GST之前异步运行,在GST之后同步运行,这就是半同步模型的含义。这里的同步包括两方面的含义:

    • 可靠的;
    • △及时性的(△-timely):
      • 例如当诚实节点p在t≥GST即同步之后向诚实节点q发送了消息m,那么q应该在t+△之前收到消息m;
      • 对△的进一步解释:由于系统中节点之间不一定(很小概率)拥有直接通信通道,因此q在接收到消息m之前,m应该经历了p与q之间的其他若干节点的Gossip转发,这个转发时间应该控制在△内。

    问题1 :Tendermint系统什么时候是GST之前?什么时候是GST之后?也就是节点之间什么阶段异步通信什么阶段同步通信?

  3. Gossip通信的一些时长保证:

    • 如果p在t发送了消息m,那么所有节点都将在max{t, GST } + △之前接收到m;
    • 如果q在t收到了消息m,那么所有节点都将在max{t, GST } + △之前接收到m;
  4. 上述定义中GST和△均为系统参数,为了确保算法的安全性,这些参数是不需要提前知道的,也就是说算法可以保证在GST之后的有界时间内终止。

  5. 系统在好状态(GST之后的时间,消息的Gossip通信具有可靠性和△-及时性)和坏状态(GST之前的时间,系统处于异步状态,且消息可能丢失)之间反复交替,但是该模型仍然能够进行适应性改变并应用于实践。

  6. 我们假设:

    • 节点的阶段(包括收发消息)是不消耗时间的;
    • 节点内置了时钟,可以检测内部超时;
    • 消息是密码学安全的:由于所有协议消息都包含了数字签名,因此在q接收到m时能够知道m的发送者是p且能通过签名验证p的身份;
    • 在伪代码中为了方便阅读我们省略了签名及验证的步骤;
    • 无效签名的消息会被直接丢弃。

2.2 复制状态机SMR

复制状态机的概念在第一章中介绍过,这里不重复说明了。简单来说,在存在拜占庭故障节点的情况下保证所有诚实节点(副本)都能够实现状态复制的关键是所有的副本接收并处理相同的请求序列。这个关键可以被拆分为两部分:

  • 协议:保证了所有副本正确接受了所有请求;
  • 顺序:保证了所有副本接受请求的顺序一致;

在拜占庭容错的复制状态机中还需要满足一个额外的条件:

  • 只有客户端提出的事务才会被副本执行。

在Tendermint中,副本具有验证事务的责任,只有有效的请求才会被处理,这就防止了恶意节点发送恶意请求从而破坏副本状态。

2.3 共识

Tendermint通过顺序执行共识过程来对每个区块达成共识,然后再由复制状态机来执行这些区块,从而使得所有复制状态机达成一致状态。本文考虑拜占庭共识问题的一个变种问题——有效的基于谓词的拜占庭共识。该共识由区块链系统驱动,由以下三个属性定义:

  • 一致性 :诚实节点选择的区块相同;
  • 最终性:诚实节点最终选择的区块相同;
  • 有效性:被选择的区块是合法的(通过valid()函数验证的)。

这个变体问题含有一个valid()函数来判断区块的合法性。

3 Tendermint共识算法

Tendermint共识算法通过一系列原子执行的规则来定义的,一旦消息日志包含的消息使相应的条件计算结果为true就触发这些规则。这段话存在两个问题:

  1. 什么时候条件计算结果为true?计算什么条件?
    • 条件计算公式建模为:2f + 1⟨PRECOMMIT, hp, r, id(v)⟩,这意味着当节点接收到高度为hp、轮次为r、消息v的hash值的PRECOMMIT消息且消息的总票权达到2f + 1时,条件计算结果为true;
  2. 这些规则是什么?
    • 其实就是满足了上述条件时需要进行的操作,将这些操作封装成函数并设置触发条件就形成了规则。

每个节点都使用Gossip协议来进行消息传递,收发的消息都会存在本地消息日志里。有些规则以“for the first time”约束结尾,表示只有第一次对应的条件计算为true时才会触发。这是因为这些规则并不总是改变算法变量的状态,所以如果没有这个约束,算法可能永远重复执行这些规则。

Tendermint共识算法由下面的伪代码定义:

Initialization:
	hp := 0		/* current height, or consensus instance we are currently executing */
	roundp := 0	/* current round number */
	stepp ∈ {propose, prevote, precommit}
	decisionp[] := nil
	lockedValuep := nil
	lockedRoundp := −1
	validValuep := nil
	validRoundp := −1
upon start do StartRound(0)
Function StartRound(round) :
	roundp ← round
	stepp ← propose
	if proposer(hp, roundp) = p then
		if validValuep ̸= nil then
			proposal ← validValuep
		else
			proposal ← getValue()
		broadcast ⟨PROPOSAL, hp, roundp, proposal, validRoundp⟩
	else
		schedule OnTimeoutPropose(hp, roundp) to be executed after timeoutPropose(roundp)

upon ⟨PROPOSAL, hp, roundp, v, −1⟩ from proposer(hp, roundp) while stepp = propose do
	if valid(v) ∧ (lockedRoundp = −1 ∨ lockedValuep = v) then
		broadcast ⟨PREVOTE, hp, roundp, id(v)⟩
	else
		broadcast ⟨PREVOTE, hp, roundp, nil⟩
	stepp ← prevote

upon ⟨PROPOSAL, hp, roundp, v, vr⟩ from proposer(hp, roundp) AND 2f + 1 ⟨PREVOTE, hp, vr, id(v)⟩ while stepp = propose ∧ (vr ≥ 0 ∧ vr < roundp) do
	if valid(v) ∧ (lockedRoundp ≤ vr ∨ lockedV aluep = v) then
		broadcast ⟨PREVOTE, hp, roundp, id(v)⟩
	else
		broadcast ⟨PREVOTE, hp, roundp, nil⟩
	stepp ← prevote

upon 2f + 1 ⟨PREVOTE, hp, roundp, ∗⟩ while stepp = prevote for the first time do
	schedule OnTimeoutPrevote(hp, roundp) to be executed after timeoutPrevote(roundp)

upon ⟨PROPOSAL, hp, roundp, v, ∗⟩ from proposer(hp, roundp) AND 2f + 1 ⟨PREVOTE, hp, roundp, id(v)⟩ while valid(v) ∧ stepp ≥ prevote for the first time do
	if stepp = prevote then
		lockedV aluep ← v
		lockedRoundp ← roundp
		broadcast ⟨PRECOMMIT, hp, roundp, id(v))⟩
		stepp ← precommit
	validValue ← v
	validRoundp ← roundp
upon 2f + 1 ⟨PREVOTE, hp, roundp, nil⟩ while stepp = prevote do
	broadcast ⟨PRECOMMIT, hp, roundp, nil⟩
	stepp ← precommit

upon 2f + 1 ⟨PRECOMMIT, hp, roundp, ∗⟩ for the first time do
	schedule OnTimeoutPrecommit(hp, roundp) to be executed after timeoutPrecommit(roundp)

upon ⟨PROPOSAL, hp, r, v, ∗⟩ from proposer(hp, r) AND 2f + 1 ⟨PRECOMMIT, hp, r, id(v)⟩ while decisionp[hp] = nil do
	if valid(v) then
		decisionp[hp] = v
		hp ← hp + 1
		reset lockedRoundp, lockedValuep, validRoundp and validValuep to initial values and empty message log
	StartRound(0)

upon f + 1 ⟨∗, hp, round, ∗, ∗⟩ with round > roundp do
	StartRound(round)

Function OnTimeoutPropose(height, round) :
	if height = hp ∧ round = roundp ∧ stepp = propose then
		broadcast ⟨PREVOTE, hp, roundp, nil⟩
		stepp ← prevote

Function OnTimeoutPrevote(height, round) :
	if height = hp ∧ round = roundp ∧ stepp = prevote then
		broadcast ⟨PRECOMMIT, hp, roundp, nil⟩
		stepp ← precommit

Function OnTimeoutPrecommit(height, round) :
	if height = hp ∧ round = roundp then
		StartRound(roundp + 1)

代码块里没法标下标,请自行区分角标p,或看原文。

其中:

  • 有下标p的变量是节点本地状态变量,无下标p的变量是值占位符(应该是指接收的消息里的值),符号*表示任意值;

  • 系统假定非诚实节点的票权小于总票权的1/3,即n > 3f,为了简化描述采用n = 3f + 1;

  • 算法以轮为单位运行,每轮系统中都有一个提议者,(轮次,提议者)的映射表是公开的,函数proposer(h, round)返回共识实例h的轮次为round的提议者;

  • 提议者的选取是加权轮询选取的,每个节点根据自己的票权进行身份轮询,具有更多投票权的验证者被更频繁地选为提议者,这与其权力成正比;

    提议者的选取过程可以看这篇文章

  • 内部协议状态的转换由消息接收超时过期两种事件触发。该部分算法中有三种超时: timeoutProposal、timeoutPrevote和timeoutPrecommit:

    • 超时可以防止算法阻塞和永远等待某些条件为真,确保进程在轮间持续过渡,并保证(在GST之后)正确进程之间的通信最终是及时和可靠的,以便它们可以做出决定,如果一直等待事件则无法确保GST之后的同步。
  • 每轮都会给超时增加一个时间,timeoutX® = initTimeoutX + r ∗ timeoutDelta,表示每轮给节点一定的合理处理时间,只要该轮处理时长在该时间内则始终不触发超时;

  • 该算法中包含三种消息:

    • Proposal:被当前轮的提议者用于提议一个可能的决定值;
    • Prevote和Precommit:被当前轮的验证者用于对提议值进行投票;
  • 该算法中需要经过两轮投票、三轮消息交换来决定一个最终值。在区块链场景下Tendermint决定的值是一个区块,一个区块可能包含了大量交易,因此为了减少通信量,只有在Proposal消息中含有该区块的全部数据,而在后续的Prevote和Precommit消息中只包含该区块的hash值,只需能够验证该区块即可。

  • 什么时候一个诚实节点能够发出对一个区块的Prevote消息?

    • 当该节点同意提议者所提议的区块时:

      • 发送包含对该区块hash后的值id(v)的Prevote消息;
    • 提议者所提议的区块并不是被盲目接受的,节点可以自行判断,如果认为提议者是恶意节点:

      • 发送包含nil值的Prevote消息;
  • 什么时候一个诚实节点能够发出对一个区块的Precommit消息?

    • 当该节点接收到该区块的Proposal消息和票权为2f + 1的Prevote消息时:
      • 该区块的Proposal消息确保了区块的身份,即“是这个区块”;
      • 票权为2f + 1的Prevote消息确保了正常节点达成共识,即“大家都认同这个区块”;
    • 当该节点没有收到Proposal提议区块对应的票权为2f + 1的Prevote消息时:
      • 发送value为nil的Precommit消息,这就保证了每一轮只能决定一个最终值(哪怕是nil);
  • 什么时候一个诚实节点接受一个区块作为该轮的最终决定值?

    • 当该节点接收到该区块的Proposal消息和票权为2f + 1的Precommit消息时:
      • 该区块的Proposal消息确保了区块的身份,即“是这个区块”;
      • 票权为2f + 1的Precommit消息确保了正常节点达成共识,即“我知道大家都认同这个区块”;
  • 每个节点在该算法中维护以下若干变量:

    • stepp:标志了节点在当前轮次中的的状态,共有三种状态:propose、prevote、precommit;

    • lockedValuep:存储着节点上次发出Precommit消息时附带的id(v),注意不为nil;

    • lockedRoundp:存储着节点上次发出非nil的Precommit消息时的轮次,与lockedValue对应;

      • 诚实节点正是通过lockedValuep和lockedRoundp这两个变量来对当前轮次的正确消息来进行加锁的,具体操作是在发送Precommit消息前设置lockedValue = v, lockedRound = r;

      问题2:看不懂一段话:As a correct process can decide a value v only if 2f + 1 PRECOMMIT messages for id(v) are received, this implies that a possible decision value is a value that is locked by at least f + 1 voting power equivalent of correct processes. Therefore, any value v for which PROPOSAL and 2f + 1 of the corresponding PREVOTE messages are received in some round r is a possible decision value.因为正确的进程只有在收到id(v)的2f + 1个预提交消息时才能决定一个值v,这意味着一个可能的决策值被正确进程至少f + 1个投票权所锁定。因此,在某轮r中收到的建议集及其对应的2f + 1个预投票消息v的任何值都是一个可能的决策值。

      • 这段话中的f + 1是怎么来的?为什么收到了2f + 1票权的Prevote消息还能共识不成功?看不懂的原因大概率是因为对这两个变量的作用没有完全理解,所以继续往下看。
    • validValuep:存储着最近可能的决定的值;

    • validRoundp:存储着最新的validValuep发生更新的轮次;

    • hp:在区块链应用下就是当前块的高度;

    • roundp:当前轮数;

    • decisionp:一个hp和id(v)的映射表,本质是个数组,hp位置对应的数据为id(v),hp隐式存储;

下面是该算法即共识过程的详细说明:

3.1 Proposal

每一轮都由一个选出的提议者通过Proposal消息提议一个可能的最终决定值开始,对应StartRound函数中的broadcast ⟨PROPOSAL, hp, roundp, proposal, validRoundp⟩这句代码(19行)。Proposal消息的结构如下图所示:

[Alt]https://zhuanlan.zhihu.com/p/84962224

伪代码中省略了签名过程,除此之外的数据均与伪代码中的四个变量一一对应。

问题3:Proof of Lock对应什么?按照伪代码来看应该对应validRoundp,但是网上文章提到节点需要维持一个PoLC(Proof of Lock Change)的投票集结构:PoLC表示在一个高度h和轮次r构成的元祖(h,r)下, 对于一个区块b(可能为空nil)prevote投票数超过2/3的集合。需要看代码才能知道这里到底封装了什么数据。

按照伪代码中的流程,在每个高度的初始轮次中,提议者可以自由选择提议块的内容,使用getValue()函数来从交易池中获取交易信息并打包成块,作为proposal变量内容广播出去;在非初始轮次中需要分类讨论(15~18行):

  • 如果validValue ≠ nil,即提议者节点还锁在上一个提议区块中,那么就需要重新对上一区块进行提议;

    问题4:什么时候提议者节点会锁在上一个提议区块中?为什么需要重新提议而不是提议新的块?

  • 如果validValue = nil,即提议者节点没有被锁在上一区块中,这时才可以重新提议新的区块;

需要注意的是,除了打包validValue,提议者还需要打包该变量对应的validRound,以让其他节点知道提议者是在先前的哪一轮中获取了该提议值。

问题5:看不懂一段话:Note that if a correct proposer p sends validValue with the validRound in the PROPOSAL, this implies that the process p received PROPOSAL and the corresponding 2f + 1 PREVOTE messages for validValue in the round validRound. 如果正确的提议者p在提议区块的validRound轮次中发送validValue,这意味着节点p在validRound轮次中收到了提议和相应的2f + 1对validValue的Prevote消息

  • 为什么?

问题6:看不懂一段话:If a correct process sends PROPOSAL message with validValue (validRound > −1) at time t > GST , by the Gossip communication property, the corresponding PROPOSAL and the PREVOTE messages will be received by all correct processes before time t+∆. Therefore, all correct processes will be able to verify the correctness of the suggested value as it is supported by the PROPOSAL and the corresponding 2f + 1 voting power equivalent PREVOTE messages.如果一个正确的进程发送带有validValue的提案消息(validRound >−1)时间t > GST,根据Gossip通信属性,相应的Proposal消息和Prevote消息将在时间t+∆之前被所有正确的流程接收。因此,所有正确的进程都能够验证建议值的正确性,因为建议和相应的相当于2f + 1投票权的预投票消息都支持建议值。

  • 为什么限定了validRound > −1

3.2 Prevote

一个诚实节点在下面的条件均成立的情况下接受提议者发出的Proposal消息(22~23行):

  • 通过调用外部函数valid()来判断该提议块是否合理,函数返回true;
  • 没有锁定值(即lockedRoundp = -1)或上锁值等于提议者提议的区块值( lockedValuep = v);

还有一种情况诚实节点也会接受该Proposal消息(28~29行):

  • 收到了该区块2f+1票权的Prevote消息;
  • 通过调用外部函数valid()来判断该提议块是否合理,函数返回true;
  • 0 ≤ vr < roundp
  • lockedRoundp ≤ vr 或 lockedValuep = v;

问题7 :设置这个条件的目的是什么?validRound和lockedRound的关系是什么?

如果上述条件都不满足,那么节点发送值为nil的Prevote消息;除此之外如果Prevote计时器timeoutProposal超时(该触发器在新一轮开始时触发)时节点还没有发出本轮的Prevote消息,那么节点同样也会发出值为nil的Prevote消息(57行)。

3.3 Precommit

一个诚实节点接收到提议者发来的Proposal消息和与之对应的票权为2f + 1的Prevote消息后,就广播包含该区块hash值的Precommit函数;否则它就广播值为nil的Precommit消息。除此之外当Precommit计时器timeoutPrevote超时(节点发送了Prevote消息并收到了任意2f + 1权重的Prevote消息时,该计时器启动)(65行),并且节点在该轮中还没有发过任何Precommit消息时,节点也会广播值为nil的Precommit消息。

3.4 Commit

如果节点在某轮中收到了提议者发来的Proposal消息和与之对应的票权为2f + 1的Precommit消息后,该节点就可以将该提议值设置为最终决定值(51行)。为了防止算法长久等待这个条件而阻塞,该算法依赖于timeoutPrecommit计时器。当节点收到任意2f + 1权重的Precommit消息时,该计时器启动。如果该计时器到期时该节点还没有作出决定,那么就启动下一轮(65行)。为什么在收到了2f+1权重的Precommit消息后还不决定?是因为这里的2f + 1是任意值的权重和,并不是针对某一值达成了共识,因此还需要等到某一值的权重达到2f + 1后才能进行决定。

当一个节点确定了最终值,它就会开启下一高度的共识过程。Gossip通信确保了Proposal消息和对应的票权2f + 1的Prevote消息最终能被所有诚实节点接受,所以所有节点会共同决定。

3.5 终止机制

Tendermint采取了一种新的终止机制,这种终止机制需要两个额外的变量:validValue和validRound,它们由提议者在提议阶段使用。在第r轮中,当节点收到值v的有效Proposal消息并且收到了该值对应的2f + 1票权的Prevote消息时,该节点就会更新这两个变量为v和roundp(42~43行)。

首先需要注意的一点是,由于Gossip通信的特性,在good period内,如果一个诚实节点p在r轮对v进行上锁,那么所有诚实节点都会将validValue更新为v、validRound更新为r(这一点在第4章中给出详细证明),这样它在后续轮次中提出的值能够被所有诚实节点接受。但是有可能出现以下情况:在good period内没有诚实节点锁定了某个值v,但是某些诚实节点在其他轮次中同步了validValue和validRound值。在这种情况下,算法仍然能够保证每个诚实节点能够接受validValue和validRound,这是通过添加了条件**validRoundq > lockedRoundc**来完成的,并且Gossip通信的属性又确保了q在validRound中接收到的Prevote消息会在△内被所有诚实节点接受,这就回到了一开始的正常情况。这就对问题7进行了回答,这个条件的设置是为了在这一轮中同步诚实节点的validValue和validRound。

由于诚实节点上锁的validValue总能够被其他所有诚实节点接受并同步,而总有诚实节点作为提议者的一轮,因此即使很多轮内都没有达成共识,也总会有诚实节点被选为提议者的轮次,之后所有诚实节点都能正常同步validValue和validRound,从而正常终止该高度的共识过程。不同于PBFT中的额外通信终止机制,Tendermint的终止机制均由正常消息完成,没有额外的网络开销。这就回答了一开始的问题,Tendermint和PBFT的区别。

3.6 个人理解

以上论文的共识算法说明就结束了,下面的第四章是对算法安全性等方面的证明工作,对理解算法的帮助不大。但是只是通过上述的简单说明并没有——至少我并没有——完全搞清楚共识算法的原理,例如:设置的变量作用是什么?正常运行流程下各个变量是如何工作的?在节点宕机或拜占庭节点的条件下各个变量又如何工作?算法如何保证区块链不会产生分叉?伪代码中各个函数与触发条件在什么情况下会调用、又干了些什么、为什么要设置这样的条件?……除了这些问题,上文中还有在阅读原文过程中的若干问题没有进行回答,下面就根据原文内容、伪代码定义和网上文章,梳理一下我个人对Tendermint共识算法的理解。

3.6.1 伪代码解读

  1. Initialization:
    	hp := 0		/* current height, or consensus instance we are currently executing */
    	roundp := 0	/* current round number */
    	stepp ∈ {propose, prevote, precommit}
    	decisionp[] := nil
    	lockedValuep := nil
    	lockedRoundp := −1
    	validValuep := nil
    	validRoundp := −1
    

    初始化过程比较好理解:

    • 初识高度和轮次均为0
    • 共分为三个阶段:propose, prevote, precommit;
    • 初识化阶段没有前序高度的Value值,因此decisionp[]为nil;
    • 后续四个变量会在后面的函数与进一步解读中进行解释。
  2. upon start do StartRound(0)
    Function StartRound(round) :
    	roundp ← round
    	stepp ← propose
    	if proposer(hp, roundp) = p then
    		if validValuep ̸= nil then
    			proposal ← validValuep
    		else
    			proposal ← getValue()
    		broadcast ⟨PROPOSAL, hp, roundp, proposal, validRoundp⟩
    	else
    		schedule OnTimeoutPropose(hp, roundp) to be executed after timeoutPropose(roundp)
    
    • 每个高度下第一轮轮数传入0,后续如果共识失败可能会进行多轮共识,此时传入对应的轮数;

    • 注意该函数是所有节点都会执行的,因此每轮开始时所有节点都进入propose阶段;

    • 函数中的分支结构通过proposer来进行静态判断:

      • 如果该节点是该高度本轮的提议者,那么:

        • 如果该节点的validValuep锁在了先前轮次的值,就继续提议该值;
        • 如果该节点的validValue没有锁在先前轮次的值,那么就使用getValue函数重新获取事件;

        并广播Proposal消息,其中validRoundp是该节点上次validValuep改变时的轮次;

      • 如果该节点不是该高度本轮的提议者,那么只需要启动一个计时器,该计时器超时会调用:

        Function OnTimeoutPropose(height, round) :
        	if height = hp ∧ round = roundp ∧ stepp = propose then
        		broadcast ⟨PREVOTE, hp, roundp, nil⟩
        		stepp ← prevote
        

        该函数的作用是如果该节点超时后仍处于propose阶段且高度轮次没变,那么就广播携带nil的Prevote消息,并进入下一阶段即prevote;

    • 即使是通过上面的梳理,代码仍然有不懂的问题:

      • 什么时候validValuep不为nil?

        想要回答这个问题,就需要在代码其他地方查看什么时候对validValuep进行了改变,毕竟在每个高度的初始轮次中validValuep是初始化为nil的。查找发现除了初始化以外,只在第42行validValuep ← v和第53行将validValuep设置为了初始值nil,那么能够使其不为nil的就只有第42行的代码了。下面需要理顺第42行代码在什么情况下被调用,这就需要回到这行代码所在函数的判断语句中来:

        upon ⟨PROPOSAL, hp, roundp, v, ∗⟩ from proposer(hp, roundp) AND 2f + 1 ⟨PREVOTE, hp, roundp, id(v)⟩ while valid(v) ∧ stepp ≥ prevote for the first time
        

        这个判断语句很长,大致触发条件如下:

        • 收到当前高度当前轮的任何vr的、值为v的Proposal消息且消息发送者无误;
        • 收到票权为2f + 1的对v的Prevote消息;
        • v合法且当前步骤为prevote或precommit;
        • 只在首次满足时触发;

        这个判断语句不管Proposal的值是在哪一轮中提出的,只要在当前高度当前轮对该值的Prevote满足条件就触发,这其实就代表着一个结点收到了对该值v超过2/3票权的Prevote消息,可以进入下一轮。需要注意的是,由于Proposal不会提出nil,因此收到超过2/3的nil的Prevote消息并不能满足该条件,该条件必须要对某个值满足该投票数才行。这也就回答了这个问题,即当之前某些轮中节点接收到了对值v超过2/3票权的Prevote消息时,validValuep就更新为v,同时validRoundp也更新为该轮的轮数roundp

      • 为什么在新的Proposal消息中要继续包含validValuep而不是重新获取新值?

        这个问题的回答就稍微有些难度了,这也变相地在问该变量设置的意义是什么。我们回到刚才的函数判断条件中,满足了2f + 1票权的Prevote消息其实就说明该消息通过了Prevote阶段的共识,进入了Precommit阶段,如果后续不出错的话该值就会被Commit并上链,高度增加并开始新的轮次,这样validValuep就会被重置初始化。那么满足该函数的判断条件只有一种情况:之前轮次中,Precommit阶段出错,没有收到票权超过2f + 1票权的该值的Precommit消息导致该值v没有完成共识,进而进入了该高度的该轮共识。

  3. upon ⟨PROPOSAL, hp, roundp, v, −1⟩ from proposer(hp, roundp) while stepp = propose do
    	if valid(v) ∧ (lockedRoundp = −1 ∨ lockedValuep = v) then
    		broadcast ⟨PREVOTE, hp, roundp, id(v)⟩
    	else
    		broadcast ⟨PREVOTE, hp, roundp, nil⟩
    	stepp ← prevote
    
    • 先看函数的触发条件:

      • 本高度本轮的Proposal消息提议者正确,且其中的vr=-1即先前validValuep没有变化过,始终是nil,即没有收到值v的超过2/3票权的Prevote消息;
      • 当前阶段为proposal;

      这样的触发条件意味着曾经没有达成过任何共识,无论是Prevote阶段还是Precommit阶段;

    • 再看分支条件:

      • 如果值v合法且满足lockedRoundp = -1和lockedValuep = v中的任意一个:广播对于该值的Prevote消息,表示本节点同意该提议;
      • 如果不满足,则广播对nil的Prevote消息,表示本节点共识失败;

      其中值v合法很好理解,关键是后两种条件满足其一即可,这样的条件有什么意义呢?这个问题不是很好回答,还是相当于在问这两个变量的作用是什么。让我们按照刚才的思路对这两个变量进行分析,首先在代码其他地方查看什么时候对这两个变量进行了改变,发现除了初始化以外只有第38~39行lockedValuep ← vlockedRoundp ← roundp以及第53行的重初始化对这两个变量进行了改变。除去第53行的重初始化操作,那么能造成这两个变量改变的就只有第38~39行的代码了,这段代码同样回到了刚才分析的函数里,只不过除了刚才分析的繁杂的条件,这里又添加了一个条件:stepp = prevote。综合起来梳理,就是一个结点收到了对该值v超过2/3票权的Prevote消息,并且此时节点仍处于Prevote阶段。结合刚才对于validValuep和validRoundp的改变并不需要满足这个条件,也就是说当节点处于precommit阶段时,只更改validValuep和validRoundp;当节点处于prevote阶段时,还需要额外锁定lockedRoundp和lockedValuep,并广播对该值v的Precommit消息,表示本节点已经收到了超过2/3的Prevote消息,已经知道大多数节点都认可该提议,现在要让其他节点知道本节点已经进入precommit阶段了。正常情况下节点只能通过这种方式进入precommit阶段,但是这里却出现了阶段判断,那么除了这里之外还有什么情况下节点会进入precommit阶段呢?答案就是第46行和第64行,其中前者表示当节点接收到超过2/3的对nil的Prevote消息时也广播对nil的Precommit消息并进入precommit阶段;后者表示当节点在precommit计时器超时的情况下与前者处理方式相同。这两种情况都是不正常的情况,在这种不正常的情况下节点可能出现先进入precommit阶段后才收到对该值v超过2/3票权的Prevote消息的情况,只有在这种情况下函数才只更改validValuep和validRoundp,表示虽然本节点没有对该消息进行Precommit消息确认,但是我认为它是一个可能的最终值。

  4. upon ⟨PROPOSAL, hp, roundp, v, vr⟩ from proposer(hp, roundp) AND 2f + 1 ⟨PREVOTE, hp, vr, id(v)⟩ while stepp = propose ∧ (vr ≥ 0 ∧ vr < roundp) do
    	if valid(v) ∧ (lockedRoundp ≤ vr ∨ lockedValuep = v) then
    		broadcast ⟨PREVOTE, hp, roundp, id(v)⟩
    	else
    		broadcast ⟨PREVOTE, hp, roundp, nil⟩
    	stepp ← prevote
    
    • 该函数的触发条件:

      • 上一个函数接受的是vr=-1即先前没有更改validValuep的值而是新提出的块的Proposal消息,而该函数接收的是更改了validValuep的值的情况,而vr则记录了改变该值时的轮数即validRoundp
      • 0 ≤ vr < roundp:确保了vr是此前的某一轮;
      • 在这种情况下即使收到了在vr轮对该值v超过2/3票权的投票的情况下,也不能盲目接受该值,还需要满足以下条件,否则发送:
        • 本节点处于propose阶段,可以发送Prevote消息;
        • 本节点锁住lockedValuep的轮次 lockedRoundp < vr 或 lockedValuep = v;

      其中第一点很好理解,第二点则要么本节点锁住lockedValuep的轮次 lockedRoundp在vr之前即vr轮所共识的值比我锁住的值新,要么vr轮共识的值与本节点锁住的值相等。第二点实际上确保了本节点不会理会那些在我记录可能值validValuep之前共识的旧值。

    • 这个函数说明即使本节点被锁在lockedValuep,但只要收到了2f + 1票权的Prevote消息,就仍然可以对比本节点锁的轮数lockedRoundp更新的值发出Prevote消息。

  5. upon ⟨PROPOSAL, hp, roundp, v, ∗⟩ from proposer(hp, roundp) AND 2f + 1 ⟨PREVOTE, hp, roundp, id(v)⟩ while valid(v) ∧ stepp ≥ prevote for the first time do
    	if stepp = prevote then
    		lockedValuep ← v
    		lockedRoundp ← roundp
    		broadcast ⟨PRECOMMIT, hp, roundp, id(v))⟩
    		stepp ← precommit
    	validValue ← v
    	validRoundp ← roundp
    
    • 该函数前面已经进行了很多分析,包括这四个变量是如何工作的,下面想针对validValue和lockedValue进行进一步的说明:
      • validValue的设计目的 是让节点保存一个更有可能被提议通过的区块值,因为只有在接收到2f + 1票权的Prevote消息时才会更新该值,而这些票权中至少有f + 1票权的票是由诚实节点投出的,那么这个区块按理来说就是能够被共识成功的区块。至于为什么最后可能没有共识成功,可能是拜占庭节点发出了不同的消息,导致诚实节点有的达到了票权,有的没有达到票权,以达成分叉的目的。可以看这个文章中所举的例子。上述的validValue能够很好的起到作用是建立在下一轮由该节点提议区块时,此时只需要提议validValue即可在正常节点间达成共识。但是如果下一轮是由其他节点提议区块,那么还是需要重新提议区块,因此该变量的“强度”没有那么高。
      • 相较之下lockedValue则是为了防止区块分叉而设计的变量:只要有一个节点提交了区块,那么由于提交区块的条件是收到票权为2f + 1的Precommit消息,那么其中必定至少有票权为 f (除去自身的1和拜占庭节点的f)的正常节点,既然这票权为f的节点发出了Precommit消息,那么就意味着这些节点被锁在了lockedValue中,这样后续的轮次中这些节点必不可能为其他值发送Prevote消息。这点怎么证明呢,需要回到先前的函数3和4中,一个节点发送Prevote消息要么是对初始轮次的值发送,要么是对在自己轮之前轮次2f +1票权且lockedRoundp ≤ vr 的值发送,前者由于已经有了lockedValue而不可能实现,后者则因为vr在自己轮之前lockedRoundp ≤ vr的条件互斥而不可能实现。上述内容其实是对[引理2](##4.2 引理2)的通俗化解释。综上,只要有一个节点提交了区块,那么其余区块总会同步到该区块上。
  6. upon ⟨PROPOSAL, hp, r, v, ∗⟩ from proposer(hp, r) AND 2f + 1 ⟨PRECOMMIT, hp, r, id(v)⟩ while decisionp[hp] = nil do
    	if valid(v) then
    		decisionp[hp] = v
    		hp ← hp + 1
    		reset lockedRoundp, lockedValuep, validRoundp and validValuep to initial values and empty message log
    	StartRound(0)
    
    • 这是某一高度完成共识的唯一出口,条件就是收到了对值v的2f + 1票权的Precommit消息;
    • 要做的就是记录v到decisionp里,高度+1,重置变量和消息日志,并重新执行StartRound(0)。

3.6.2 问题回答

无论是阅读论文之前,还是在论文阅读过程中,本文都提出了很多问题,这些问题在没有对Tendermint共识机制有一定了解的情况下或许很困惑,但是在阐明了上述共识机制原理的情况下,我想可以尝试对这些问题进行一个回答:

  1. 问题1
    • 根据计时器的设置不同,这个问题需要分类讨论:
      • 对于proposal阶段的节点:由于计时器是在提议区块时开始计时的,因此在这个阶段没有GST,一开始就是GST,在△内节点必进入下一阶段;
      • 对于prevote和precommit阶段的节点:由于计时器是在接收到2f + 1票权的Prevote或Precommit消息后才设置的,因此在接收到这些消息前系统是异步的,接收到这些消息后进入GST后的半同步阶段,在△内节点必进入下一阶段。
  2. 问题2
  3. 问题3
    • Tendermint中有个概念:PoLC,全称为Proof of Lock Change,表示在某个特定的高度和轮数(height, round),对某个块或nil(空块)超过总结点2/3的Prevote投票集合,简单来说PoLC就是Prevote的投票集[1]。其实和原文伪代码中的说法不冲突,统计了投票集中的结点数也就知道该轮是否具有超过2/3的Prevote消息了。
  4. 问题4
  5. 问题5
    • 这个问题也在解释函数的过程中说明了。
  6. 问题6
    • 因为只有非初始轮才能对validValue发出提议。

下面回答在阅读文章之前提出的问题,这些问题比较大,更涉及到对Tendermint整体的认知:

  1. Tendermint共识算法和PBFT的区别是什么?

    • 这篇文章给出了很好的对比。补充一点,Tendermint的所有信息都存储在blockchain里,而PBFT则是分散存放在节点中。
  2. 为什么只能抵御33%的拜占庭节点?

    • 是由共识机制决定的,其中两个步骤的投票都需要达到2f + 1票权的消息才能验证通过;至于为什么是这个数字而不是50%或其他数字,则需要研究BFT问题的具体证明,这里不深究。
  3. 当系统中有33%的拜占庭节点时共识过程是怎样的?如何确保结果的一致性和正确性?

    • https://blog.csdn.net/shangsongwww/article/details/116929784

      这张图很好地展示了Tendermint共识机制运行过程中内(出错)外(正确)圈的运行过程,一旦网络宕机或区块不合法等问题出现,节点之间将会通过对空区块进行共识来继续共识流程。而到了下一轮,提议者将会改变,区块可能恢复正常合法,宕机的节点也可能恢复正常,这样算法仍然可以在新的一轮中正常运行。至于其中的变量是如何变化的,在伪代码解析部分已经详细分析,这里不进行赘述。

  4. Tendermint共识算法的不足是什么?有哪些可以改进的地方?

    • 如果网络突然一分为二,也就是说如果突然出现了大面积网络问题,那么算法将卡死无法继续进行,因为算法在Prevote阶段和Precommit阶段都需要接收到2f + 1票权的消息才启动计时器,在大面积宕机的情况下始终无法启动计时器,也就无法进行超时处理,此时需要管理者干预;
    • Tendermint只能容忍33%的拜占庭节点,防御能力较弱。

4 Tendermint共识算法的证明

这一部分的引理和证明内容会给出原文,避免翻译引起歧义。但是证明过程只给出翻译,详情见原文。

4.1 引理1

  • For all f ≥ 0, any two sets of processes with voting power at least equal to 2f + 1 have at least one correct process in common.

  • 对于所有f > 0,任何两个至少拥有2f + 1票权的节点集合至少共同拥有一个诚实节点。

证明:总票权为n = 3f + 1,两个集合的总票权为(2f + 1) × 2 = 4f + 2 = n + f + 1,也就是说两个集合中至少有f + 1的票权是重合的,而非诚实节点票权最大为f,那么重合的票权中至少有1个票权是诚实节点,即至少有一个诚实节点。

4.2 引理2

  • If f + 1 correct processes lock value v in round r0 (lockedValue = v and lockedRound = r0), then in all rounds r > r0, they send PREVOTE for id(v) or nil.
  • 如果票权为f + 1诚实节点在第r0轮将值锁为v(即lockedValue = v且lockedRound = r0),那么在第r轮(其中r > r0)中这些诚实节点要么发送v的Prevote,要么发送nil。

证明:这一点通过对r进行归纳法证明。基本条件:r = r0 + 1。用C来表示一个票权和等于f + 1的诚实节点集合,在伪代码的22行和28行的条件中,节点在第r轮只会接受对v的Proposal消息,那么节点也无法在第r轮发送对其他值v’的Prevote消息。因此在基本条件下该引理成立。下面是从r1到r1+1的递归过程:假设r1+1轮之前集合C中没有进程发送过其他值v’的Prevote消息。在第r1+1轮中,根据引理1,C集合成员不可能收到其他值v‘的2f+1票权的Prevote消息,即lockedValue = v且lockeRound ≥ r0

4.3 引理3~7

未完待续……

参考文章

  1. https://www.researchgate.net/publication/326412260_The_latest_gossip_on_BFT_consensus
  2. https://zhuanlan.zhihu.com/p/339156677
  3. https://www.cnblogs.com/zmk-c/p/14535734.html
  4. https://blog.csdn.net/weixin_43988498/article/details/119031945
  5. https://zhuanlan.zhihu.com/p/84962224
  6. https://www.jianshu.com/p/68fb29cd00de
  7. https://www.sohu.com/a/224194350_661926
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值