HotStuff共识算法详解
一、简介在2020-04-03,笔者曾经介绍过著名的PBFT算法,这次,笔者将尝试将HotStuff算法的面纱毫无保留给大伙揭开。其实,真正好的论文都是惜墨如金,用词精准到位而又恰到好处,似乎实在不需要他人再写其他蹩脚的注脚和介绍。所以,所有笔者的博客都会尽可能遵守一个原则:尽可能为读者提供一个容易理解的框架和方式。这种容易理解不是要将“经典”取而代之,而只是一种入门,一种补充,一种理解方式。经典
1.简介
在2020-04-03,笔者曾经介绍过著名的PBFT算法,这次,笔者将尝试将HotStuff算法的面纱毫无保留给大伙揭开。其实,真正好的论文都是惜墨如金,用词精准到位而又恰到好处,似乎实在不需要他人再写其他蹩脚的注脚和介绍。所以,所有笔者的博客都会尽可能遵守一个原则:尽可能为读者提供一个容易理解的框架和方式。这种容易理解不是要将“经典”取而代之,而只是一种入门,一种补充,一种理解方式。经典之所以成为经典,笔者的理解就是“你可以从各个角度解读,但是如果你要达到和经典一样的深刻和精准,那么你最终的效果也只会是成为经典现在的样子”。言归正传,HotStuff算法作为BFT共识算法,再2019年被提出不久,就被Facebook在数字货币系统Libra中采用,必定有其过人之处。
HotStuff优越的地方在于以下几个点:
1. 算法复杂度为O(n);
2. 算法实现复杂度相对更低;
3. 易于理解;
首先算法复杂度变为O(n),实在是一个听起来非常具有吸引力的算法,特别是在其他BFT类共识算法普遍在O(n2)甚至更坏的前提下。但是O(n)的复杂度实现是需要付出代价的,这个也是HotStuff算法所不能避免的拥有一些小缺陷的地方;第二个是算法实现的复杂度相对更低,本质上是因为HotStuff算法中整个系统的状态变换以及算法流程变得简单了。众所周知,有限状态机模型中,状态值越多,状态转换的路径越多,算法实现的难度以及调试的难度越高,而所谓的易于实现,指的就是整个实现的过程,包括开发调试测试都相对的简单;最后是易于理解,这是个“悖论”,即当你越来越理解基于投票的共识算法时,你会发现HotStuff算法的流程处理是更加简单和方便的,而当你对这类共识算法不甚了解时,HotStuff算法会让你摸不着头脑,因此,笔者不建议学习BFT类共识算法的同学拿HotStuff算法来当作入门的材料,而是应该去了解PBFT,这里也可以去看看笔者写的“科普”文章https://blog.csdn.net/ganzr/article/details/105276253
2.算法思路
基于投票的共识算法其实基本就是一个套路:确定一个leader负责提出提案(proposal),其他节点负责投票,根据投票结果来确定改提案是否通过。这里简单回顾以下PBFT到底是怎么切合上述的这个套路的:首先PBFT算法中通过视图机制来确定leader节点,所谓视图机制就是用一个单调递增的序号来表示每一轮共识,一轮共识最多对一个提案达成共识,每一个节点事先都有一个公开的标识号,按照每一个视图号可以计算到底是哪个节点当选leader。当选leader的节点提出提案后,其他节点的投票和收集投票的由每一个节点单独完成,原因很简单,因为有拜占庭节点的存在,节点只相信自己所获取到的关于某个节点的投票信息,每一个节点基于自己收集的消息来确定是否该提案形成了共识,即被大部分人承认。
那么PBFT中的关键点在哪呢?就在于每一个节点都做了一次收集投票信息的工作,这就导致了整个算法的工作量是重复的(起码对于所有非拜占庭节点而言是重复的)。而PBFT之所以这么做的原因是节点只相信自己所获取到的关于某个节点的投票信息,如果能解决这个信任问题,那么就能解决这个限制带来的重复功。HotStuff所进行的优化也就是基于此,即通过使用门限签名,确保投票结果不能被伪造。更具体而言,HotStuff中,leader除了负责提出提案以外,还需要收集其他节点的投票,并将投票结果整合成一个非常容易检查合法性但是又无法伪造的证据。门限签名的特定就是,当且仅当对同一个数据具备了足够多的子签名,才能最终合成一个签名,而其他人只需要确定最终的签名就能确保整个签名构建过程是合法的。门限签名的使用,使得所有节点都可以将收集投票信息的工作委派给leader,并可以确保leader无法作假。因此,HotStuff最终的算法复杂度直接降低了一个数量级。
如果仅仅是使用门限签名,HotStuff算不上一个非常优秀的PBFT共识算法,HotStuff共识算法更加优越的地方在于其将整个算法的每一轮的共识的同步阶段,都在标准化成一个简单的动作——投票以及收集投票——的前提下,使用流水线与”搭便车“的设计方法,从而将整个算法进行优化。打个比喻就是,本来一个产品的流水线分为三个连续的子任务,生产一个产品需要单独完成三个不同的子任务,现在变成了三个子任务都是相同的工作内容,且每一个子任务的完成,都代表着3个产品的不同阶段任务的完成。可以通过下面的流程图来理解。
2.1 PBFT算法思路
上图是PBFT算法的流程图,可以分为以下步骤:
- request:客户端C在request阶段发送请求到peer0
- preprepare:peer0在接收到客户端C的请求后,在pre-prepare阶段将该请求广播
- prepare:其他节点peer1,peer2,peer3,在在接收到广播的请求后,进入prepare阶段,并在该阶段将“准备提交该请求”这一结果广播给其他所有节点
- commit:所有的节点,在自身接收到足够多(超过2/3,包含自己在内)的其他节点广播的“准备提交该请求”结果后,进入commit阶段,广播一个“提交该请求”的结果给其他节点
- reply:当节点在commit阶段接收到足够多(超2/3,包含自己在内)的其他节点广播的“提交该请求”的结果后,就可以确认该请求被共识,进入reply阶段,并在reply阶段返回“请求共识成功”的结果给客户端。
首先我们来理一理上述流程到底做了什么,主要是看pre-prepare,prepare和commit阶段都做了些什么
1. 总的来说,三个阶段都可以看作是一种投票行为,但只有prepare和commit阶段里才需要统计投票结果。
2. pre- prepare阶段实质上是peer0广播“准备提交该请求”,prepare则是其他节点广播“准备提交该请求”,这里的请求指的是同一个请求。此处为何分成两个阶段?我们可以先想想如果将这两个阶段合并成一个阶段,需要怎么做?请看下图
可以看到,peer0与其他peer在prepare阶段部分先后地广播“准备提交该请求”这一信息。为什么在pbft中要额外加一个prepare阶段?实质上,我们可以看到整个共识的过程有一个非常重要的前提:所有节点都是针对同一个“请求”进行投票公式。在实际的应用中,不可能要求客户端排好顺序然后逐个发送请求,也没办法保证每一个节点接收到的请求的顺序是一致的。因为如果能保证节点接收到的请求的顺序是一致的,就没必要再用共识算法了,共识算法在根本上来讲其实就是对请求的顺序进行共识。
那么问题的重点在于如何保证“所有节点都是针对同一个请求”这个前提成立呢?
答案是:1.先选出一个公认的节点——leader,由leader来广播client的请求;2.将共识用严格递增的序号区分开,每一个序号有且仅切对应一个请求。
1比较好理解,问题在于如何选出一个公认的节点;
2其实有两个含义,一个是leader节点不应该广播两个具有一样序号的不同的请求;非leader节点则不应该接收两个具有同一个序号的不同的请求。如此一来,只要超过2/3的节点是遵守这个规则(无论是leader或者是非leader),都不会出现两个具有相同序号的不同的请求被成功共识,因为上述流程的每一个阶段的投票共识都需要超过2/3节点的投票。
我们可以想想,如果1和2都满足了,是否就能够使得“所有的节点都针对同一个请求”的前期成立。
答案是否定的。因为这个前提的描述中,有一个隐藏的条件,即在同一个时间段内所有的节点都针对同一个请求。如果选出了leader,但是每一个节点选出leader的时间段是错开的,那么其实等同于没有选出leader。
因此,pbft算法还有一个最基本的条件,就是时间的同步。换句话来说,就是每一个节点的时间跟其他时间的误差要在一定范围内。假设最大的时间差为,则节点之间的广播消息的延时最起码是要小于的。这样才能保证节点在接收到其他节点的时间时,来得及调整本地的时间(从这里也可以看出来,网络的延迟对于共识的效率而言是非常影响的,因为一旦网络延迟很大,就会导致节点在时间上的同步出现问题,从而破坏了整个算法的基础,算法无法往下执行。)
时间的同步需要考虑哪些问题才能保证整个共识算法能够一致持续下去呢?
1.首先每一轮共识必须要有一个超时时间控制,否则如果其中一轮共识中的某个阶段从出现了问题——例如leader节点宕机——就会导致共识无法进行下去;
2.超时时间的设置需要考虑整体的网络时延以及节点共识过程中所进行的操作的耗时。首先超时时间必须大于上述所说的节点最大时间差,其次再考虑节点操作耗时(跟代码实现,机器配置有关)
时间的同步如何实现才是最简单又最有效的?
首先,此处的时间的同步不是严格意义上的物理时钟的同步,而是时间段的同步。因为每一轮共识不是发生在某一个时刻,而是发生在一个时间段,时间段的长度就是每一轮共识的超时时间。当然可以用两个时刻来描述这个时间段,例如用2021年7月5日10:49:10~2021.7.5.10:49:20这10秒钟作为一个时间段。可以发现,这样的表示非常的麻烦,本质上如果知道了每一个时间段的大小,其实只需要一个序号就可以用来区分不同的时间段,这也就是所谓的“逻辑时钟”,即用自然数来表示一个时刻,时刻之间变化所对应的物理时间是一段时间,即逻辑时钟每个固定秒之后才会跳转一次。可以看到,这样用序号来表示时间段的做法于前面用严格递增的序号来区分不同的请求的做法是更加匹配的。因此,当用逻辑时钟来做时间的同步之后,整个共识的流程如下:
时间的同步流程就很简单,即只需要通过广播自己所在的用逻辑时钟表示的时刻——view N,只要超过2/3的节点的view是一致的,就可以保证时间是同步的。
对于如何选出一个leader有很多的方法。
一个最简单的方法是,由始至终都用同一个节点作为leader节点。但是万一该leader节点出错,则整个共识都会停滞,因此,需要在leader节点出错的情况下,更换leader节点。
如何判断leader节点出错?
只要本地节点判断共识停滞,即在当前的view中,共识超时,就会判断leader节点出错。但是这种判断如果仅仅是节点本地的判断,就可能会造成一部分节点判断leader出错而另一部分节点判断没有出错。如果保证大部分节点的判断是一致的,则需要对“leader节点出错”本身进行共识。而共识的方法就是将节点本地对leader出错的判断当作是当前leader节点在pre-prepare发出的“请求”,所有其他节点都会在prepare广播一个“准备提交节点出错请求”的消息,并在收集到足够的view-change消息后在commit阶段广播一个“提交leader节点出错的请求"的消息,至此,所有其他正常的节点都会对”leader节点出错”形成共识。
2.2HotStuff的思路
在2.1章节,详细了描述了PBFT算法中的关键细节,这些细节是理解HotStuff共识的基础。HotStuff的优化体现在以下几个点:
1. 将节点广播投票改成leader节点收集投票然后再广播,从而将的复杂度降低为
2.每一个轮的投票除了是对本轮的请求的共识,也是对上一轮请求的共识。
3.每一轮投票更换一次leader节点,尽可能减少leader出错时更换leader所需要的时间。
这几个优化点到底应该如何理解?
1. 首先pbft中,算法的复杂度很大程度上在于每一个节点都做了大量重复的工作,即每一个节点都会广播自己的投票并收集其他人的投票。在这里收集投票才是每一个节点广播的目的,而投票本身因为数据签名是无法被捏造和篡改的,因此,其实不需要每一个节点都收集投票,而只需要一个节点负责收集后将收集到的结果广播给每一个节点,就可以将的通信复杂度降低为。
2. 我们可以发现,当看一轮共识,节点会经历三个阶段,preprepare,prepare,commit,而这三个阶段的算法流程内容都是收集投票:
- preprepare:需要收集leader的proposal(投票的一种特殊形式);
- prepare:需要收集所有节点对于proposal的投票信息;
- commit:需要收集所有节点对于prepare阶段中“自身节点收集到2/3的其他节点投票”的信息;
既然如此,能否在一轮投票中同时完成针对多个proposal的投票?如果proposal之间没有顺序,那么可以每一次投票同时针对多个proposal,比如leader节点同时将n个proposal提出,其他节点则在prepare阶段投一次票,而这个投票是同时对这n个proposal生效的。然后这些proposal是有顺序的,那么能够依旧并行处理这些proposal的投票呢?其实也是可以的,回想下cpu在执行指令的方式,每一个指令会有多个周期,将不同执行的不同周期重叠,从而实现几条指令的并行执行,即所谓的cpu流水线技术。同理,此处的proposal也分几个阶段,将不同的proposal的不同阶段重合,就可以得到以下的效果图:
3. 在pbft中,有一个专门的view-change阶段来更换出问题的leader,但是这个view-change实质上就是一个特殊的proposal,这个proposal不需要leader来发出,而是节点本身在判断当前leader出问题时,就会直接广播一个针对这个特殊的proposal的投票。那么这里的leader更换就需要走至少一轮完整的共识。而在HotStuff中,为了减少更换leader所需要的走的流程,主动在每次发送完proposal之后就更换leader。这种主动的有规律地更换leader有利有弊,下文会在适当的时候做阐述。
2.3 HotStuff算法流程
非流水线HotStuff
非流水线的HotStuff主要是对将“leader负责收集每一轮的投票信息”思想融合到pbft中。从pbft的流程图可以看到,pbft中出现了2轮所有节点广播以及收集投票+1轮client收集投票,那如果换成leader负责收集投票,需要leader的几轮广播和收集投票?答案是3轮。
- PBFT中prepare阶段的算法效果
在prepare阶段,在算法正常进行时,每一个合法的节点都将接收到至少2/3的投票数,我们将“一个节点接收到至少2/3的投票数”称为事件A,显然至少2/3节点都发生了事件A,但是节点之间不知道彼此之间是否发生了事件A;
- PBFT中commit阶段的算法效果
在commit阶段,每一个节点都会将自己发生了事件A的消息广播给其他节点,并收集其他节点关于事件A的广播,我们把“收集到至少2/3节点广播事件A”称作事件B,此时,每一个节点都知道至少有2/3节点都发生了事件A,但是不确定其他节点是否知道事件B。
- PBFT中reply阶段的算法效果
在reply阶段,每一个节点都将事件B返回给client,此时client只要收集到至少2/3节点的事件B的广播时,就可以判断,系统已经对该proposal形成共识,因为事件B表示“至少2/3节点都知道有至少2/3节点对proposal进行了投票”,将“收集到至少2/3节点的事件B的广播”称作事件C。
我们再看看,如果换成了leader来做收集投票的事情,应该怎么做:
1.收集至少2/3节点的投票,即leader节点发生了事件A,此时leader节点把这个时间的证据保留下来,广播给其他节点,那么其他节点也就相当于发生了事件A;但是还是不知道其他节点是否有接收到leader广播的事件A;
2.此时收到事件A的节点会广播一个投票给leader;leader会收集这些投票,即leader发生了事件B,同理,讲这个事件B广播给其他节点,其他的节点在受到时也相当于发生了事件B,但是还是不知道其他接待你是否有收到leader广播的事件B;
3.跟第2步一样,收到事件B的节点也会广播一个投票给leader,leader收集,此时leader节点上发生了事件C,同理,讲这个事件广播给其他节点,其他节点受到后,就确认了共识已经达成。
综上所述,HotStuff的流程图如下:
流水线HotStuff
从非流水线的hotstuff中,节点的步骤有4个,分别是pre-prepare,prepare,pre-commit,commit,在每一阶段中都会有一次广播,因此,可以讲不同的proposal的不同的阶段重合,然后服用每一轮的广播来完成这些重叠的proposal的阶段的变换。如下图所示
此处,QC即上述的事件发生的“证据”即至少2/3节点的投票的
可以看到:
1. 每一个cmd(即proposal)在prepare被提出,经历后续三个阶段后达成共识;
2. 每一个QC,既是所指向的cmd达到下一个阶段的证明,也是上一个cmd的的证明,同时还是上上个cmd的证明,且分别表示不同的cmd到达了不通阶段的证明。
3.总结
可以看到,HotStuff的整体思路是使用leader来完成本来每一个节点都需要重复做的事情,更重要的是将整个共识的阶段抽象成了流水线的模型,从而将每一轮广播的证明的功能最大化,从而将工作量再减少2/3,因为每一个QC同时证明了三个cmd的状态。但是我们也可以看到,HotStuff严重依赖leader,一旦出现了leader出错,这个算法就会停止,所以在HotStuff中,每一个leader在广播完一个(QC,cmd)之后就会更换另一个leader,有兴趣的同学,非常推荐你去读下原文。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)