本文为万向区块链技术中心研究组撰写,介绍了PBFT算法的正常流程。
1. 系统模型 本部分介绍PBFT算法运行的系统模型。
1.1 网络 PBFT工作在异步的分布式系统中,系统中各个节点彼此通过网络连接。 系统运行时,消息的传递允许出现下列情形:
不能正确发送
延迟
重复
乱序
1.2 Byzantine failure model 系统允许错误节点也就是拜占庭节点表现出任意行为,但是需要附加一个限定条件: 节点失效彼此应相互独立,从而大部分或全部节点不会同时失效。
在有恶意攻击存在的情况下,可以采取类似于下列措施来保证这个限制的成立:
各节点运行的服务程序和操作系统的版本尽可能多样化
各节点的管理员帐号和密码不同
1.3 消息加密属性
1.3.1 使用加密技术的目的 防止身份欺骗、重播攻击
监测错误消息
1.3.2 具体使用的加密技术
公钥签名:用于验证消息发送者身份,PBFT中,实际上只用于view-change和new-view消息,以及出现错误的情况。其他消息都采用下面将会提到的MAC(消息认证码)进行认证。这是算法设计中提出的一种优化措施,用于提升算法性能。
MAC:即消息认证码,用于算法正常操作流程中的消息认证
消息摘要:用于检测错误消息
1.4 敌手特性 算法限定敌手(adversary)可以:
串通拜占庭节点
延迟通信或正常节点
同时,敌手不可以:
无限延迟正常节点的通信
伪造正常节点签名
从消息摘要反推原始消息
让不同消息产生相同摘要
2. 服务属性 本部分介绍运行PBFT算法的系统的服务属性。
2.1 关于副本复制服务 PBFT算法可用于实现确定性的副本复制服务(Replicated service)。 副本复制服务拥有状态(state)和操作(operation)。
客户端(client)向服务发起请求,以执行操作,并等待响应。
服务由 n个节点组成。操作可以执行任何计算,只要这些计算始终产生确定性的结果。
节点和客户端如果遵循算法的预定步骤执行操作,则被称为正常节点或客户端。
2.2 关于Safety和Liveness 只要系统中失效节点的个数不超过容错数 ,系统就能提供safety和liveness。
2.2.1 Safety
Safety的提供,是系统能保证客户端请求的线性一致性(linearizability),即请求按顺序一次一条地被执行。
PBFT相对于之前的算法如Rampart等的一个显著的不同在于,其Safety不依赖于同步假设。
算法不需限定客户端一定是正常的,允许其发送不合法的请求,原因是各正常节点可以一致性地监测客户端请求的各种操作。并且算法可以通过权限控制的方式对客户端进行限制。
2.2.2 Liveness
由于算法不依赖于同步提供 Safety,因此必须通过同步假设来提供 Liveness。
这里的同步假设是,客户端的请求最终总能在有限的时间内被系统接收到。
客户端可能会通过多次重传的方式,发送请求到服务,确保其被服务接收到。
PBFT所依赖的同步假设其实是比较弱的假设,原因是在真实的系统中,网络错误最终总可以修复。
2.3 关于算法弹性 PBFT的算法弹性(resiliency)是最优的:假定系统中失效节点最大个数为f,则系统最少只需要 3f+1 个节点就可以保证Safety和Liveness。
简单证明:
考虑到最不理想的情况,系统有最大数量的失效节点,即f个。(总节点数为n) 客户端此时可以接收到的回复个数最坏情况是 n-f,因为失效节点可能都不会回复。 但是,由于网络等原因,客户端接收到的 n-f 个请求中,实际上有可能包含有失效节点的回复(有可能是错误的),而另外一些正常节点的回复还未及时收到。 这其中,最坏的情况是,n-f个结果中,有f个是失效节点发送的。按照PBFT算法的定义,客户端需要收到 f+1 个相同的回复,才被当作是正确的结果。因此 n-f 个结果中,出去f个失效节点的结果,即 n-f-f = n-2f 至少要是f+1, 即 n-2f = f+1,也就是说 n=3f+1是最少需要的节点数量。
2.4 关于信息保密性 一般情况下,为确保服务的高效性,不能提供容错的信息保密性。
可能可以使用secret sharing scheme来获得操作参数和部分对操作来说透明的状态的保密性。
3. 算法主流程 本部分介绍运行PBFT算法的主流程,即正常操作流程。
3.1 主流程简介 3.1.1 相关定义
算法是状态机副本复制技术的一种形式:服务被建模为状态机,其状态在分布式系统中的不同副本节点上被复制。每个状态机副本节点保存维护着服务状态,并实现服务的各种操作。
假设所有副本节点个数为n,算法中,每个节点依次编号为 0, 1, ..., n-1
方便起见,假设系统中的副本节点总数为 3f+1。可以有更多数量的节点,但是这不会使算法的弹性更优,只会使系统的性能降低。
系统在称为视图(view)的配置下工作。视图以整数编号,从0开始。在一个具体的视图 v 中,通过 p =v mod n,决定出主节点(primary),而其余节点成为副本节点(backup)。当主节点失效时,进行视图变更(view change)。视图的编号是连续递增的。
3.1.2 算法主流程简要描述
算法主流程可简要描述如下:
1. 客户端通过向主节点发送请求,以调用服务的操作;
2. 主节点向其他所有副本节点广播该请求;
3. 各节点执行客户请求,同时将回复发送到客户端;
4. 客户端收到 f+1 个来自不同节点的相同的回复后,此回复即为本次请求的结果。
因为算法基于状态机副本复制技术,所以节点需满足两个条件:
必须是确定性的,即对于给定的状态,以及给定的参数集合,调用相同的操作后,将始终得到相同的结
果。
各节点拥有相同的初始状态。
在满足上述两个条件的情况下,算法可以保证系统的Safety属性:即使存在失效的节点,各正常副本节点仍可以就不同的请求的执行顺序达成总体的一致。
3.2 算法主流程 接下来详细描述算法主流程。为方便起见,这里省略讨论以下细节:
节点因空间不足导致错误,以及如何恢复;
类似网络的原因等导致的客户端消息的重传。
另外,假设消息使用公钥签名进行认证,而不是更高效的 MAC 的方式。算法流程的启动从客户端发送请求开始。
3.2.1 客户端操作
客户端操作流程示意图如下:
文章图片
客户端向其认为的Primary节点发送请求:
相关的几点说明:
请求中的时间戳用于保证请求只被执行一次的语义:所有的时间戳都严格排序,即后发送的请求比先发送的请求拥有更高的时间戳。
每个副本节点向客户端发送回复时,都会包含当前的视图编号。客户端可以通过该视图编号来确定当前的主节点,并且只向其认为的主节点发送请求。
每个副本节点执行完请求后,各自单独地向客户端发送回复,其格式为:
客户端等待来自 f+1 个不同副本节点的相同回复,即t和r要相同。如果客户端等到了 f+1 个相同回复,r即为请求的结果。 之所以该结果是有效的,是因为错误节点的个数最多为f个,因此必然至少有一个正常节点回复了正确结果,此结果就是 r。
如果客户端没有及时收到回复,则会将请求广播给所有副本节点。副本节点收到请求后,如果发现已经执行过该请求,就只是将结果重新发送至客户端;否则,它会把请求转发到主节点。如果主节点没有把请求广播给其他节点,则最终会被足够多的副本节点认定为错误节点,从而触发视图变更。
在接下的流程讨论中,假定客户端等待一个请求完成后,才发送下一个请求。但是,算法允许客户端异步地发送请求,并且可以保证不同请求按顺序执行。
3.2.2 三阶段协议
主节点收到客户端请求后,将启动三阶段协议,也就是算法接下来的流程。
这三阶段是pre-prepare,prepare和commit。前两阶段,即 pre-prepare 和 prepare 用于保证当前视图中请求
被排好序,而后两阶段 prepare 和 commit 保证请求在视图变更后,仍旧是排好序的。
3.2.2.1 pre-prepare阶段
pre-prepare 阶段流程示意图如下:
文章图片
pre-prepare阶段中,主节点组装预准备消息,同时把客户端请求附加在其后:<
, m>,其中,v 指示当前消息当前在哪个视图中被编号和发送,m 是客户端的请求消息,n是主节点给 m 分配的一个序号(sequence number), d 是 m 的摘要。
这里需要注意的是,请求 m 并没有放在预准备消息中,这样做可以使预准备消息变得更简短。这样做有两个好处:
1、降低通信的负载:在视图变更时,由于各节点收到的预准备消息会被用来证明一个特定的请求确实在特定的视图中被赋予了一个序号,较简短的预准备消息将使数据传输量更少。
2、有助于传输优化:算法运行中,一方面需要向各节点发送客户端请求,另一方面需要传输协议消息以实现对客户请求的排序。通过对这两者解耦,可以实现对较小的协议消息的传输以及对应于较大的请求的大消息的传输分别进行优化。
每个副本节点接收到预准备消息后,会进行如下校验,如果条件都满足的话,就接受该消息;否则就什么也不做:
客户端请求和预准备消息的签名都正确;d 和 m的消息摘要一致;
当前节点的当前视图是v;
当前节点未曾接受另外一条预准备消息,其包含的视图编号和消息序号都和本条消息相同,但对应的是不同的客户端请求;
预准备消息中的序号 n 位于低水线 h 和高水线 H 之间。这是为了防止有可能出错的主节点随意地选择序号来耗尽序号空间,例如故意选择一个非常大的序号。
如果副本节点接受预准备消息,接下来就进入prepare 阶段,如下节所示。
3.2.2.2 prepare阶段
prepare 阶段流程示意图如下:
文章图片
在 prepare 阶段,节点会组装并广播准备消息给其他所有副本节点,同时把预准备和准备消息写入到本地消息日志中。
准备消息格式如下:。其中,i 为节点编号,其余参数和预准备消息中的含义相同。
对于副本节点(包括主节点)来说,当其收到其他节点发送过来的准备消息时,会对这些消息进行校验,如果这些消息满足下列条件:
签名正确
其视图编号和节点的当前视图编号相同
消息中的序号在 h 和 H 之间
则节点会接受准备消息,并写入消息日志中。
对于一个副本节点 i 来说,如果其消息日志中包含如下消息:
客户端请求 m
在视图 v 中将 m 分配序号 n 的预准备消息
2f个由不同的副本节点发送的、和预准备消息相匹配的准备消息;这里匹配的含义是,有相同的视图编号、请求序号,以及消息摘要。
我们就称prepared(m, v, n, i)为true。
算法的预准备和准备阶段用于保证所有的正常副本节点就同一视图中的所有请求的顺序达成一致。具体来说,这两阶段能确保以下不变式:
如果prepared(m, v, n, i)为true,则对任意一个正常的副本节点j(包含i)来说,prepared(m', v, n, j)肯定为false,这里m'是不同于m的一个请求。
简单证明如下:
因为prepared(m, v, n, i)为true,而错误节点最多为f个,所以至少有f个正常节点发送了准备消息,再加上主节点,这样至少有f+1个节点已经就m在视图v中被编号为n达成了一致。因此,如果prepared(m', v, n, j)为true,意味着上述f+1个节点中至少有一个节点发送了两个相互矛盾的预准备或准备消息,也就是说,这些消息拥有相同的视图编号和序号,但是对应着不同的请求消息。但这是不可能的,因为该节点是正常节点,因此prepared(m', v, n, j)一定为false。
对于副本节点i来说,prepared(m, v, n, i)变为true,则其将进入commit阶段,如下节所示。
3.2.2.3 commit阶段
commit 阶段流程示意图如下:
文章图片
节点进入 commit 阶段时,副本节点i将向其他所有副本节点广播确认消息:
,
对于副本节点来说,当其收到其他节点发来的确认消息的时候,会判断其是否满足下列条件:
签名正确;
消息中的视图编号等于当前节点的视图编号;
消息中的请求序号在h和H之间。
如果以上条件均满足,则节点则会接受确认消息并写入本地的日志消息中。
对于副本节点i来说,如果:
prepared(m, v, n, i)为true
并且已经接受了 2f+1 个来自不同节点的、和 m 对应的预准备消息相匹配的确认消息(可能包含它自己的)
我们称 committed-local(m, v, n, i) 为 true。这里确认消息和预准备消息匹配的含义是,它们有相同的视图编号、消息序号,以及消息摘要。
另外,如果至少存在 f+1 个节点,对于其中每一个节点i来说,如果 prepared(m, v, n, i) 为true,我们则称committed(m, v, n) 为 true。
commit 阶段能保证以下不变式:
如果对某个副本节点来说,committed-local(m, v, n, i) 为 true,则 committed(m, v, n) s也为true。
上述不变式和视图变更协议一起能够保证:
所有正常节点能够就所有本地确认的请求的序号达成一致,即使这些请求是在不同的视图中确认的。对应的证明将在另外一篇文档中给出。
另外,该不变式也能保证:任何一个请求如果在一个副本节点被确认,那么它最终也会被至少 f+1 个副本节点确认。
对于任何一个副本节点i来说,如果:
committed-local(m, v, n, i) 为 true
i 的状态反映了所有序号小于 n 的请求顺序执行的结果
此时,它就可以执行 m 所请求的操作。这就保证了所有的正常节点,按相同的顺序执行请求,从而保证算法的安全性。
在执行了请求的操作后,每个节点单独地给客户端发送回复。对于同样内容的请求,节点会忽略那些时间戳更早的,以保证请求只被执行一次。
此外,算法并不要求消息按顺序投递,因此,节点可以乱序确认请求。这样做没有问题,因为算法只在一个请求对应的预准备、准备和确认消息都收集完全时才会执行该请求。
以下是 f=1,即失效节点数为1个,总共节点数为 4个时,PBFT 算法的运行示意图:
文章图片
【区块链|PBFT算法流程】关注我,后续将更新PBFT的补充算法流程。
推荐阅读