目录
在本文中,我们尝试从零开始设计一个拜占庭容错的共识算法。但由于本文是在阅读大量文献之后,思考整理得出的,难免会有“事后诸葛亮”的嫌疑,但这不要紧,我们的目的也不是真的为了设计一个全新的共识算法,而是站在设计者的角度,思考一个共识算法是如何一步步得出的。当然,在推理过程中,我们也会使用尽可能少的已知条件,让“从零开始”不那么“哗众取宠”。相信本文会让你对共识算法有一个较为全面的理解,以后如果遇到新的共识算法,也可以使用本文的思路分析,快速拿下。
在开始之前,我们先理清一些基本概念。
分布式一致性算法(共识算法):使集群中多个节点保持数据状态一致的通信算法。
拜占庭错误:节点除了宕机(不发送消息),还可能出现随机的恶意行为(发送假消息)。
拜占庭容错算法:能够容忍拜占庭错误的共识算法。设集群中拜占庭节点(错误节点)数为 f,当总结点数 N < 3f+1 时,不存在一个拜占庭容错算法。即总节点的最小数量为 3f+1,本文我们使用这个数量作为总节点的数量。
共识算法的正确性可以分为以下两个性质:
换句话说,如果没有安全性,不同节点就可能执行了不同请求,使系统失去了一致性,这是不可容忍的;如果没有活性,那么系统会永远“卡住”,无法处理请求。下面我们从这两个特性出发,尝试站在设计者的角度看一个拜占庭容错的共识算法是如何诞生的。
为了让算法在可证明正确性的同时可被实际应用,我们需要在特定的网络下设计算法。存在以下几个网络模型:
实际的网络可能存在丢包、分区等情况,所以不是一个同步的网络;而在异步网络下,无法保证活性;部分同步网络最符合实际,因此我们算法的设计将会基于这个假设。
我们需要设计一个这样的共识算法:即使是在异步网络下,也要保证安全性;在同步网络下,保证活性。这样,在部分同步网络下,就能保证算法的安全性和活性。
分布式一致性算法早在上个世纪就被广泛研究,但随着区块链的兴起,再次进入人们的视野。为了方便讨论,同时使共识算法更贴近于区块链的场景,我们把算法的一些参数对应到区块链中,把“请求”称为“区块”,请求的执行顺序称为“高度”,区块按照高度顺序串联起来就成了区块链。但请记住,共识算法并不关心请求是什么,不同系统可以有不同的请求结构。
同时,我们假设共识为单线程的,即只有完成上一个区块的共识,才能开始下一个区块的共识。并且我们假设存在某个机制,当节点发现自身区块落后于其他节点时,会自动从其他节点同步,本文不对这个机制做具体描述,并简单使用“快速恢复机制”来指代这个过程。
如何共识?
系统如何对一个高度的区块达成共识呢?这可以类比一个民主的组织如何达成一个决策,往往会有一个提案者(proposer,为了便于讨论,我们暂且称为 leader)提出一个方案,其他成员进行投票表决。对应到共识算法中,我们需要选出一个 leader,对于某个高度,(收集交易并)组装区块(这个过程我们简称为“出块”,组装好的含高度的区块我们称为“提案”)并广播,其它节点收到的提案后广播投票,收集其他节点的投票,当收集到足够多的相同投票时,确定该提案。
收集多少投票?
由于有 f 个拜占庭可能不投票,所以收集到 2f+1 个投票就该停止,否则系统将失去活性。由此我们给节点引入一个行为:
行为1:节点收集到 2f+1 个相同投票(含提案)时,提交该提案,执行区块并存储,最后向客户端响应。
加锁
另外,由于 leader 可能为错误节点,可能故意向不同节点发送不同提案(同一高度的不同区块),所以为了保证安全性,正确节点只会对某个高度投一票,即当高度 H 已经给区块 A 投过票时,不会再对该高度的其他区块投票,我们称这个行为为“加锁”,并且说节点对于高度 H “锁定”在了区块 A。
由此,我们引入第二个行为限制:
行为2:对于某个高度,正确节点发起或收到一个提案时,会锁定该区块,并坚持对已锁定的区块进行提案或投票,而会忽略该高度下的其他区块提案。
QC 性质
有了加锁机制之后,不同节点还有没有可能在同一高度提交不同区块呢?
稍加推理,我们会发现这是不可能的:假设在高度 H 上,节点 1 提交了区块 A,节点 2 提交了区块 B。那么说明有 2f+1 个节点对区块 A 投了票,有 2f+1 个节点对区块 B 投了票,由于总节点数为 3f+1,所以这两部分节点的交集为 (2f+1)*2 - (3f+1) = f+1,其中错误节点数为 f,所以交集一定有一个正确节点,这个节点对区块 A 和区块 B 都投了票,这与行为 2 产生了矛盾,所以假设不成立。
可见 2f+1 既是我们为了保证活性只能收集的最大数量,同时也是投票的最小数量(为了保证交集至少有一个正确节点),这个投票对应的节点集合称为 Quorum(委员会),投票集合称为 Quorum Certificate(委员会证书,简称 QC)。正如我们上面分析的,QC 具有一个非常重要的性质:任何两个 Quorum 至少有一个共同的正确节点。我们把这个性质称为 QC 性质。因为一个 Quorum 的节点数量为 2f+1,而错误节点数为 f,因此 Quorum 中至少有 f+1 个正确节点,所以 QC 性质也可以表述成 任何两个 f+1 个正确节点的集合至少有一个共同节点。关于 QC 性质的更多讨论,可以参考我的另一篇文章:深入理解PBFT算法——提交阶段的作用。
上文我们讨论了安全性,接下来我们重点关注下活性。
视图切换
共识算法活性丢失的最直接来源是 leader 出错。当 leader 为错误节点时(宕机或发送任意消息等),可能完全不发提案,也可能给其他节点发送任意不同的提案,使得每个节点都无法收集够 2f+1 个正确投票,共识无法正常进行。所以当检测到 leader 为错误节点时,需要更换 leader,我们称 leader 的一个任期为视图(view),更换 leader 的过程为 “视图切换”,每个视图有一个唯一的编号,称为视图号,并且每次视图切换时视图号都递增 1。至此,提案除了高度和区块之外,还多了一个视图号。
那么如何检测 leader 出错呢?最简单的办法是设定超时时间,如果超过某个时间还没有对某个高度的提案达成共识,就认为 leader 为错误节点,触发视图切换。
如何选举下一任的 leader 呢?最简单的办法是轮流法,即当前 leader 节点编号 = 视图号%节点数。其中“%”表示取余。
视图切换成功之后,假如上一个视图的共识没有遗留,即所有节点处于一致状态,那么新 leader 可以重新发起提案,继续推动共识的进行。
由于我们是用超时来触发视图切换的,所以不能断定上一个 leader 为错误节点,同时也无法保证旧视图中的共识已经全部完成,可能存在部分节点已经提交了某个区块,但其他节点还没收到该区块的情况,此时新 leader 必须保证这些区块能被所有正确节点提交。下面我们列举下进入新视图时各种可能的异常情况,同时一起思考新 leader 应该如何处理这些异常。
造成以上情况的原因是不难推测的,若有疑问可以参考附录的【情况分析】。在新视图中,我们来看看节点可以怎么做:
情况 1:假如新 leader 为正确节点,且在上一个视图中锁定该区块了,那么本次继续使用该区块发起提案,其他节点对该区块投票,可以正常达成共识。 假如新 leader 为错误节点(或者新 leader 为正确节点,但在上一个视图中没有锁定该区块),那么如果发起一个不同的提案,是不可能收集到足够投票的(行为 1 和行为 2 保证了安全性),并且可能使得其他节点锁定在这个区块,演变成了情况3;若没有使得其它节点锁定在不同区块,那么多次视图切换之后总能轮到锁定该区块的正确节点出块(活性)。
情况 2:假如新 leader 为正确节点,且在上一个视图中锁定该区块了,那么本次继续使用该区块发起提案,其他节点对该区块投票,可以正常达成共识。 假如新 leader 为错误节点(或者新 leader 为正确节点,但在上一个视图中没有锁定该区块),那么如果发起一个不同的提案,可能使得其他节点锁定在这个区块,演变成了情况3;若没有使得其它节点锁定在不同区块,那么多次视图切换之后总能轮到锁定该区块的正确节点出块。
情况 3:假如错误节点配合投票,且存在大于等于 f+1 的正确节点锁定在同一区块,轮到这里面的其中一个节点出块时,是可能达成共识的,但要想办法把其他锁解开,否则后续可能无法共识。如何说服节点解锁?道理很简单,就是给他们证据让他们相信该高度已有另一个区块提交了,可以放心解锁。最直接的证据就是 QC。由此,我们引入另一个行为限制:
行为3:leader 发起新提案时,携带本节点最近提交的提案的 QC,其他节点收到 QC 时会解锁在该高度下锁定的区块,并提交该 QC 对应的提案,继续新提案的共识。
假如此时发生了视图切换,新 leader 没有该高度的最新 QC,那么无法解锁其他节点,共识无法进行,但多轮视图切换之后总能轮到携带了最新 QC 的节点出块。
如果错误节点不配合投票,或者不存在大于等于 f+1 的正确节点锁定在同一区块,无论由谁发起提案,都是不可能收集到足够投票的,并且由于该高度下还没有形成一个 QC,后续也无法形成一个 QC,所以无法解锁,无法达成共识,我们称这种现象为“死锁”,死锁使得系统失去了活性。
死锁问题
为了避免死锁,我们要保证当一个新锁产生时,旧锁一定有机会能被解开。QC 解锁机制明显不具备这个特性,因为新旧锁的同时存在可能使得 QC 永远无法生成。
那么,如果新锁能解锁旧锁不就好了?如果要做到新锁能安全解锁旧锁,那么必须具备这样的特性:若新锁能够产生,那么旧锁对应的区块一定没有被提交,这样才能安全地解锁,换句话说,如果旧锁对应的区块被提交了,那么不可能产生新锁。
我们目前的机制明显无法做到这一点,因为加锁的动作是在收到区块时进行的,即使一个区块已经被提交了,该高度下仍然可能有另一个区块被提出来(可能是错误节点,也可能是正确节点没有锁定或提交那个区块)。简而言之,就是加锁的条件太宽松,所以我们可以尝试增加加锁的条件。一个比较直接的办法就是,在加锁前增加一轮投票(我们称为准备消息),收集到 2f+1 个准备消息时(我们称这个 QC 为“锁定 QC”,简称“QC 锁”),再进行加锁,加锁之后再进行第二轮投票(我们称为提交消息),收集第二轮的 QC(对应我们上面一直讨论的 QC,为了跟锁定 QC 区分,我们称这个 QC 为提交 QC)。至此,我们的行为 1 和行为 2 升级为:
行为1':节点收集到提交 QC 时,提交该提案,执行区块并存储,最后向客户端响应。
行为2':在一个视图中,对于某个高度,正确节点只会对一个区块发送准备消息,而会忽略该高度下的其他区块提案;对于某个高度(不关注视图号),正确节点收到一个锁定 QC 时,会锁定该区块,并坚持对已锁定的区块进行提案或投票,而会忽略该高度下的其他区块提案。
有了这样的机制,我们再重新审视是否能够满足刚刚提到的那个特性。先看同一个视图的情况,根据行为 2' 和 QC 性质,节点不可能收到冲突的锁定 QC,所以不会锁定在互斥的区块,两个互斥的 QC 锁只可能出现在不同视图中,视图号越大,表示锁越新;对于不同视图的情况,如果旧锁对应的区块被提交了,根据行为 1',已经有 2f+1 个节点有了旧锁;根据行为2',不可能有 2f+1 个节点对该高度的另一个区块发送准备消息,所以不可能形成新锁。可见这个解锁机制满足以上特性。
另外,根据快速恢复机制,假如节点收到了一个比本地 QC 锁的高度更大的 QC 锁时,会快速同步落后的区块,并从该高度继续共识,所以从这个意义上看,高度更大的 QC 锁也可以“解锁”高度小的 QC 锁。因此,高度越大,锁越新;高度相同时,视图号越大,锁越新。
因此,行为 3 升级为:
行为3':leader 基于最新锁定的区块发起新提案,并携带本节点最新的 QC 锁,其他节点收到 QC 锁时会解锁更旧的 QC 锁,并继续新提案的共识。
值得一提的是,假如最新锁定的区块已经提交,那么显然新提案是基于该区块出的一个新的区块,但是假如最新锁定的区块还未提交,那么 leader 可以选择继续使用该区块构造新提案,也可以选择基于该区块出一个新块,对于后者,需要增加一个设定:如果某个区块提交了,那么高度更低的区块也该提交。
使用以上的解锁机制,可以解决死锁问题。但是假如发生视图切换时,新 leader 没有最新 QC 锁,那么无法解锁其他节点,共识可能无法进行,并且,不像提交 QC,最新的 QC 锁是会动态变化的,也就是可能每次 leader 在视图切换之前都可能形成一个最新的 QC 锁,导致下一个 leader 的锁永远不是最新的,永远轮不到最新锁的节点出块,从而使系统失去活性。最新 QC 锁仿佛被“隐藏”了,所以这个问题被称为“隐藏锁”问题,具体例子可以阅读附录。
在讨论如何解决隐藏锁问题之前,我们先回看一下上面分析的几种情况,会发现存在一个问题,很多情况可能需要等待多轮视图切换才能达成共识,这大大影响了共识效率。
我们等待的目的都是为了轮到一个拥有最新 QC 锁的正确节点。那么有没有可能,切换新 leader 时,想办法把最新 QC 锁发送给新 leader,新 leader 再从中选出一个最新的 QC 锁,这样就可以直接在一轮共识中完成。
新 leader 应该收集多少个节点的消息呢?同样的,由于错误节点可能会故意不发送消息,所以为了保证活性,leader 最多在收集到 2f+1 个消息时就该停止。这也导致了最新的 QC 锁不一定能被 leader 收到,隐藏锁问题仍然存在。
如何解决隐藏锁问题呢?实际上,对于收集了 2f+1 个节点的最新 QC 锁的新 leader 来说,隐藏锁并不构成一个问题。回想一下,我们说如果新锁能够产生,那么旧锁对应的区块一定没有被提交,所以新锁可以安全地解锁旧锁。但是,其实新锁对应的区块也是可能没有被提交的,假如我们能找到新锁一定没被提交的证据,那么新锁也可以被安全地解锁。
如果某个节点本地的最新 QC 锁对应的提案已经被提交了,那么至少有 2f+1 个节点拥有该提案的相同 QC 锁,并且这 2f+1 个节点的最新 QC 锁要么等于该 QC 锁,要么为更大高度的 QC 锁,我们记这个集合为 C,根据 QC 性质,如果新 leader 收集 2f+1 个节点的最新 QC 锁,那么其中一定有一个正确节点在集合 C 中,所以一定能收集到已经提交的提案对应的 QC 锁或者更大高度的 QC 锁。也就是说,如果有一个节点拥有一个 QC 锁(不管视图号多大),但 leader 没有从任何节点收集到与之相同的 QC 锁或更大高度的 QC 锁,那么可以断定该 QC 锁对应的提案一定没有被提交,所以可以安全地解锁。于是,我们找到了另一个解锁的方法,leader 不一定非得获得最新的 QC 锁,只需要把这 2f+1 个 QC 锁作为证据广播给其他节点即可,其他节点根据上面提到的规则解锁,然后可以继续后面的共识,从而不存在隐藏锁问题。
值得一提的是,收集并广播 QC 锁的过程只需要在视图切换开始时执行一次,一旦完成了一次全体的解锁,在该视图中就不再需要关心此问题,因为在一个视图中不可能有节点锁定在不同区块。
广播所有 QC 锁无疑增大了视图切换的消息复杂度,有没有其他办法呢?我们前面提到最新锁可能无法被 leader 收集,所以会导致部分节点无法解锁,我们尝试直面问题,有没有办法保证最新的锁能被 leader 收集到呢?这样 leader 只需要广播最新的 QC 锁,就能让所有正确节点解锁,并且这个操作同样只需要在视图切换时进行一次。
我们在最开始有一个同步假设,节点的最大时延是有固定上限的,所以如果新 leader 在收集 QC 锁时,等待一个固定时间,如果这个时间大于时延上限的话,那么一定能够收集到最新的 QC 锁;如果小于时延上限,那么本次可能无法达成共识,下次视图切换时增加这个时延上限,这样轮了多次之后,总能超过时延上限。
以上方法可以减少视图切换的消息复杂度,但由于引入了一个固定时延,一旦出现视图切换(错误节点为 leader 时,总有机会能触发视图切换),不管网络有多好,共识都要等待一个固定的时间,从而失去了响应性。
响应性:系统达成共识的时间只依赖于网络的实际时延,而不依赖于已知的最大时延。通俗来说就是说当系统处于异步网络时,共识慢一些甚至无法达成都没关系,但只要系统切换到同步网络,且当前 leader 为正确节点,不管错误节点做出什么行为,都能在实际的网络延时下达成共识,网络好就快一些,网络不好就慢一些,而不是不管网络好坏,共识都要等待预设的网络最大时延。
难道就没有两全的办法吗?能否不要通过等待固定时延来确保收集到最新的 QC 锁呢?实际上,上面“广播所有锁”利用到的 QC 原理同样可以应用在我们遇到的这个问题上,也就是,我们在准备阶段之前再加一轮投票,收集到的 QC 称为预备 QC,收集到预备 QC 时才可以广播准备消息,那么当收集到 QC 锁时,一定有 2f+1 个节点拥有对应的预备 QC。假设某个节点的最新 QC 锁为 A,那么一定有 2f+1 个节点拥有对应的预备 QC,且这些节点的最新预备 QC 一定不旧于该预备 QC,我们记这些节点构成集合 C,当 leader 收集 2f+1 个节点的最新预备 QC 时,其中一定有一个正确节点位于集合 C 中,所以一定能收到不比 QC 锁 A 旧的预备 QC。因此,最终收集到的最新预备 QC,一定不旧于任意节点的最新锁定 QC。同时,预备 QC 也具备 QC 锁的基本性质,所以我们可以使用预备 QC 来解锁 QC 锁。从而解决了隐藏锁问题。
我们来总结一下我们最终的方案。
提案阶段:leader 使用已锁定但未提交的区块(若没有,则使用新区块)发起提案。
锁定阶段:节点收到 leader 的提案时,判断是否满足 a. 在该视图内没有对该高度的不同区块投过票,且 b. 没有锁定该高度下的另一个区块 或 提案携带的 QC 锁比本地的锁新(注意如果 QC 锁的高度远大于本地已知的高度时,节点会通过快速恢复机制补齐缺失的区块),若满足则广播对该提案的准备消息;节点收集到 2f+1 个准备消息(锁定 QC)之后,对该高度加锁,并广播提交消息。
提交阶段:节点收集到提交 QC 后,执行该区块并向客户端响应。
计时器:每次完成一个区块的提交、开始等待下一个区块时,设定一个超时时间,若在该时间内下一个区块没有完成提交,则触发视图的切换。
视图切换:上面提到的三种方式选择一种,若选用第三种,则需要在锁定阶段前加一个预备阶段,预备阶段逻辑与锁定阶段类似。
其实说到这里,我们基本上已经把我们即将介绍的几个共识算法——PBFT、Tendermint、Hotstuff——的核心思想讲完了。网上关于这些共识算法的描述有很多,本文并不打算对算法过程进行罗列,而只强调它们共同解决的问题、不同的解决办法、各自的优缺点。如果对算法细节感兴趣,建议阅读论文原文,特别是论文的证明部分。
正如我们上文讨论的,其实不管什么共识算法,无非就是要保证两个性质:安全性 和 活性。对于活性,则包含 leader 出错、死锁和典型的隐藏锁问题。
在安全性方面,这几个算法如出一辙,都是利用了我们上文提到的 QC 性质;对于 leader 出错,则都是使用了 leader 切换机制;对于死锁,基本上也都是通过增加一轮投票阶段来解决;但对于隐藏锁问题的解决却各有千秋,分别使用了我们上文提到的三个基本思想,下文我们会一一分析。
对于 PBFT,我们经常会看到类似这样的一张图:
上面的三个阶段其实就对应我们上面提到的提案阶段、锁定阶段、提交阶段。
准备阶段收集的准备证书就对应我们的 QC 锁;提交阶段收集的提交证书就对应我们的提交 QC。
在隐藏锁问题的解决上,PBFT 选用的是 “广播所有锁” 的方法,所以视图切换过程如下:
所有节点广播 View-Change 消息,该消息包含本节点的准备证书,新 leader 收集到 2f+1 个 View-Change 消息之后,将所有 View-Change 消息打包进 New-View 消息并广播给其他节点。不同的是,节点发送的准备证书并不只是最新的,还包含了其他的较近的准备证书,这可以看成是“快速恢复机制”的一部分;同时, New-View 消息中也不仅包含准备证书,还包含基于这些准备证书发起的新提案,这是为了在解锁的同时,进行新提案的发起,推进未完成共识的区块更快完成。
PBFT 最常被诟病的就是视图切换复杂度高,N 个 New-View,每个 New-View 含 2f+1个 View-Change,每个 View-Change 含若干个准备证书,每个准备证书有 2f+1 个签名,所以消息复杂度为 O(n^3)。这也是后来其他共识算法想方设法要解决的问题。
Tendermint 最大的改进就是降低了视图切换的消息复杂度。
Tendermint 与 PBFT 具有非常相似的两阶段投票协议,文末总结部分也有各个共识算法的阶段名称对应,这里不再赘述。值得一提的是,如上图所示,进入新一轮的 Propose 之前,可能是 New Round,也可能是 New Height,后者仅在共识成功时出现。也就是说,当某个高度的区块共识成功时(图中蓝色箭头组成的循环),会进入新的高度,否则,会进入新的 Round,直到该高度共识成功。这里的 Round 其实就对应 PBFT 中的视图(View),共识超时触发切换 Round,更换 leader。不同之处在于 Tendermint 即使共识成功进入 New Height 也会更换 leader,但这对于保证活性不是必要的,更多是为了保证各个节点能够按照正确的权重轮流出块,以达到激励分配的公平性,同时有研究表明在区块链的环境下频繁更换 leader 有助于提高链的质量。后文将提到的 Hotstuff 也是每个区块都会更换 leader。
不过以上的区别无关痛痒,隐藏锁的解决机制才是我们要重点讨论的话题。实际上,Tendermint 使用了我们上文提到的“引入固定时延”的机制。
首先,它使用了一个 Gossip 协议,节点间会互相同步收集到的消息(提案、投票等),网络模型遵循部分同步假设。也就是说,如果某个正确节点发送/接收到了消息 A,那么在某个时间间隔 Δ 之后,所有正确节点都会收到消息 A。
在这个基础上,Precommit 阶段引入超时时间 timeoutPrecommit,在这个超时时间内,节点可以(通过 Gossip 协议)向其他节点同步最新的锁。可证明当 timeoutPrecommit 大于某个值时,可以保证最新的 QC 锁一定能在进入下一个 round 前发送给所有正确节点(严谨证明参见论文 Lemma 6),即下一个 round 的 leader 一定含有最大的 QC 锁。因为使用了 Gossip 协议,所以它进入 New Round 时表面上并没有额外的消息传输。另外,Propose 和 Prevote 阶段也含有超时时间,当这些超时满足一定要求时,可证明活性(参见论文 Lemma 5)。
Hotstuff 的算法思想与 PBFT 和 Tendermint 是类似的,但使用了“增加一个阶段”的方式来解决隐藏锁问题。非流水线版本的 Hotstuff 共识流程如下图:
看起来感觉跟 PBFT 差别很大?实际上,如果我们把他画成下面全广播的网状形式,就好理解了:
这个图和 PBFT 很像,区别是图中红色框的部分。第二个框实际上就是为了解决隐藏锁问题而增加的阶段,而第一个框则是所有节点在视图切换时向主节点发送本地的最新 QC 锁(因为 Hotstuff 没有像 Tendermint 一样的 Gossip 机制)。其他阶段可以与 PBFT 一一对应进行理解。
Hotstuff 的一个改进点是使用了聚合签名,所有节点不直接广播已签名的投票消息,而是先将投票发送给 leader,再由 leader 将所有签名进行聚合之后,再广播给其他节点,这样可以减少验签时间,并且将网状通信变成了复杂度更低的星型通信。
将全广播的共识流程中的网状通信替换为星型通信,就有了一开始的那个共识流程图。
既然有非流水线版本,那就有流水线版本,这是 Hotstuff 的另一个重要改进。
由于每个投票阶段的消息非常类似,所以可以将他们的消息类型进行统一,并且不同提案的共识可以流水线化。具体来说,所有节点在 PREPARE 阶段收集足够的投票组成 genericQC,然后 genericQC 转发到下一个视图的 leader,新 leader 负责下一个 PRE-COMMIT 阶段。但新 leader 并不是真正地执行 PRE-COMMIT 阶段,而是重新进入一个新的 PREPARE 阶段,并提出新的提案。视图 v+1 的 PREPARE 阶段同时作为视图 v 的 PRE-COMMIT 阶段,视图 v+2 的 PREPARE 阶段同时作为视图 v+1 的 PRE-COMMIT 阶段和视图 v 的 COMMIT 阶段,以此类推。视图 v 的提案会在视图 v+4 的 PREPARE 阶段结束时提交。
因此,在 Hotstuff 的流水线的版本中,只有 NEW-VIEW 和 GENERIC 两种消息类型,前者包含上一个视图的 genericQC 和当前视图的提案,后者包含对当前视图提案的投票,足够的 GENERIC 消息进行聚合签名后组成 genericQC。
流水线化之后由于只有一种 QC,所以锁定和提交逻辑依赖于视图间的数量关系,有兴趣的话可以阅读原论文了解更多。
共识算法 | PBFT | Tendermint | Hotstuff |
---|---|---|---|
提案阶段 | Pre-prepare | Propose | Prepare |
锁定阶段 | Prepare | Prevote | Pre-Commit+Commit |
提交阶段 | Commit | Precommit+Commit | Decide |
视图切换 | View-Change | New Round | Next View |
QC 锁 | 1 pre-prepare + 2f prepare | 1 propose + 2f+1 prevote | lockedQC |
隐藏锁解决机制 | 广播所有锁 | 引入固定时延+Gossip | 增加一个阶段 |
视图切换复杂度 | O(n^3) | O(n^2) | O(n) |
响应性 | 有 | 无 | 有 |
1.5 节的情况造成原因分别如下:
假设有 4 个节点,编号 0~3,节点 3 为错误节点。开始时,所有节点都没有锁。