拜占庭容错

拜占庭容错

  拜占庭将军问题提出后,有很多的算法被提出用于解决这个问题。这类算法统称拜占庭容错算法(BFT: Byzantine Fault Tolerance)。简略来说,拜占庭容错(BFT)不是某一个具体算法,而是能够抵抗拜占庭将军问题导致的一系列失利的系统特点。 这意味着即使某些节点出现缺点或恶意行为,拜占庭容错系统也能够继续运转。本质上来说,拜占庭容错方案就是少数服从多数。

拜占庭容错系统需要达成如下两个指标:

  • 安全性:任何已经完成的请求都不会被更改,它可以在以后请求看到。在区块链系统中,可以理解为,已经生成的账本不可篡改,并且可以被节点随时查看。
  • 活性:可以接受并且执行非拜占庭客户端的请求,不会被任何因素影响而导致非拜占庭客户端的请求不能执行。在区块链系统中,可以理解为,系统需要持续生成区块,为用户记账,这主要靠挖矿的激励机制来保证。

  拜占庭系统目前普遍采用的假设条件包括:

  • 拜占庭节点的行为可以是任意的,拜占庭节点之间可以共谋;
  • 节点之间的错误是不相关的;
  • 节点之间通过异步网络连接,网络中的消息可能丢失、乱序、延时到达;
  • 服务器之间传递的信息,第三方可以知晓 ,但是不能窜改、伪造信息的内容和验证信息的完整性;

在BFT共识机制中,网络中节点的数量和身份必须是提前确定好的。且每一次节点的进出都需要对网络进行初始化,故其无法像PoW共识机制那样任何人都可以随时加入/退出挖矿。另外,由于节点间基于消息传递达成共识,因此采用BFT算法的网络无法承载大量的节点,业内普遍认为100个节点是BFT算法的上限。所以BFT算法无法直接用于公有链,而更多的应用于私有链和联盟链。业内大名鼎鼎的联盟链Hyperledger fabric v0.6采用的是PBFT,v1.0又推出PBFT的改进版本SBFT。后续又有相当多的人对其进行了改进,力求提高其扩展性。但往往都是基于对网络环境的理想假设,以省去部分共识阶段,实现更高的节点承载量。

  在可信环境下共识算法一般使用传统的分布式一致算法PAXOS或者RAFT。

BFT算法和公有链合适的结合点在于基于BFT的PoS共识算法(BFT based PoS)。基于BFT的PoS共识算法要点有:

    1. 网络节点通过锁定虚拟资产申请成为区块链系统的验证者(或矿工)。系统验证者的数量是动态变化的。
    2. 系统从当前验证者中随机选择一个人作为区块提案人。
    3. 系统验证者对区块提案进行投票表决,投票可能要进行多轮才能达成共识。每个人的投票比重与锁定的虚拟资产成比例

 

实用拜占庭容错

  PBFT(Practical Byzantine Fault Tolerance)算法由麻省理工学院的Miguel Castro 和Barbara Liskov于1999年提出,解决了原始拜占庭容错算法效率不高的问题,将算法复杂度由指数级降低到多项式级,使得拜占庭容错算法在实际系统应用中变得可行。

  PBFT是联盟币的共识算法的基础。实现了在有限个节点的情况下的拜占庭问题,有3f + 1的容错性(拜占庭将军问题也只在节点数N > 3f时可解),并同时保证一定的性能。其采用了密码学相关技术(RSA 签名算法、消息验证编码和摘要)确保消息传递过程无法被篡改和破坏。

  拜占庭将军问题在节点数N > 3f时有解的正确性证明比较复杂,此处仅举一个简单例子说明。

  在恶意节点数f = 1,节点数N = 3f = 3时,可以看到,发令者和接令者任意角色出现叛徒都会导致其他节点无法作出决定。

为什么PBFT算法最大容错节点数量f是(N-1)/3?

  假定节点总数是N,作恶节点数为f,那么剩下的正确节点数为N - f,意味着只要收到N - f个消息且N - f > f就能做出决定,但是这N - f个消息有可能有f个是由作恶节点冒充的(或因网络延迟导致f个恶意节点的消息先被收到),那么正确的消息就是N - f - f个,为了多数一致,正确消息必须占多数,也就是N - f - f > f ,所以N最少是3f + 1个。

为什么PBFT算法最大容错节点数量f是(N-1)/3?

  假定节点总数是N,作恶节点数为f,那么剩下的正确节点数为N - f,意味着只要收到N - f个消息且N - f > f就能做出决定,但是这N - f个消息有可能有f个是由作恶节点冒充的(或因网络延迟导致f个恶意节点的消息先被收到),那么正确的消息就是N - f - f个,为了多数一致,正确消息必须占多数,也就是N - f - f > f ,所以N最少是3f + 1个。

PBFT算法流程

  涉及角色:主节点、普通节点

  在PBFT原始论文中,存在副本节点(replica)和备份节点(backup)两种称谓。其中,副本节点一般包括主节点在内,备份节点则不包括,而两种称谓又相当容易混淆,因此本文将备份节点称为普通节点。

  每个主节点的工作过程称为一个视图(view),用v表示视图编号

  主节点由普通节点轮流当选,具体计算过程为主节点p = v mod |R|(|R|为节点个数)

主节点的作用:

  正常工作时,接收客户端的事务请求,验证request身份后,为该请求设置编号,广播pre-prepare消息

  新主节点当选时,根据自己收集的View-Change消息,发送View-New信息,让其它节点同步数据

  主节点与所有的其它节点维系心跳

  如果主节点宕机,会因为心跳超时,而触发重新选举,保证系统运行稳定

  如果主节点恶意发送错误编号的消息,那么会在后续的操作中,被副本节点察觉,因为 prepare和commit阶段都是会进行广播的,一旦不一致,触发view-change

  如果主节点不发送接收到的request,客户端在超时未回复时,会重发request到所有的副本节点,并触发view-change

  如果主节点节点篡改消息,因为有Request里面有数据和客户端的签名,所以primary无法篡改消息,其它副本会先验证消息的合法性,否则丢弃,并触发view-change

  综上所述,限制了权限的主节点,如果宕机、或者不发生消息、或者发送错误编号的消息、或者篡改消息,都会被其它节点感知,并触发view-change。

(1)Request

  客户端C向主节点p发送<REQUEST, o, t, c>

o:请求的具体操作

t:请求时客户端追加的时间戳

c:客户端标识。

REQUEST: 包含消息内容m,以及消息摘要d(m)。

客户端对请求进行签名。

(2)Pre-Prepare

  主节点收到客户端的请求,需要对客户端请求消息签名是否正确进行校验。

  非法请求则丢弃。正确请求则分配一个编号n,编号n主要用于对客户端的请求进行排序。然后广播一条<<PRE-PREPARE, v, n, d>, m>消息给其它普通节点。

v:视图编号

d:客户端消息摘要

m:消息内容

主节点对<PRE-PREPARE, v, n, d>进行签名。

(3)Prepare

  普通节点i收到主节点的Pre-Prepare消息,需要满足以下条件方可接受消息:

A、请求和预准备消息的签名正确,并且d与m的摘要一致。

B、当前视图编号是v。

C、该普通节点从未在视图v中接受过序号为n但是摘要d不同的消息m。

D、预准备消息的序号n在区间[h, H]内。

  非法请求则丢弃。正确请求则普通节点i进入准备状态并向所有其它节点(包括主节点)发送一条<PREPARE, v, n, d, i>消息, v, n, d, m与上述Pre-Prepare消息内容相同,i是当前副本节点编号。

  普通节点i对<PREPARE, v, n, d, i>签名。记录Pre-Prepare和Prepare消息到日志中,用于视图轮换过程中恢复未完成的请求操作。

  Prepare阶段如果发生视图轮换会导致丢弃Prepare阶段的请求。

(4)Commit

  主节点和普通节点收到PREPARE消息,需要满足以下条件方可接受消息:

A、普通节点对Prepare消息的签名正确。

B、消息的视图编号v与节点的当前视图编号一致。

C、n是否在区间[h, H]内。

  非法请求则丢弃。如果节点i收到了2f+1个(包括自身在内)验证通过的Prepare消息,表明网络中的大多数节点已经收到同意信息,则向其它节点包括主节点发送一条<COMMIT, v, n, d, i>消息,v, n, d, i与上述PREPARE消息内容相同。

  节点i对<COMMIT, v, n, d, i>签名。记录Commit消息到日志中,用于视图轮换过程中恢复未完成的请求操作。记录其它副本节点发送的Prepare消息到日志中。Commit阶段用来确保网络中大多数节点都已经收到足够多的信息来达成共识,如果Commit阶段发生视图轮换,会保存原来Commit阶段的请求,不会达不成共识,也不会丢失请求编号。

(5)Reply

  主节点和普通节点收到Commit消息,需要满足以下条件方可接受消息:

A、节点对Commit消息的签名正确。

B、消息的视图编号v与节点的当前视图编号一致。

C、n是否在区间[h, H]内。

  非法请求则丢弃。如果副本节点i收到了2f+1个(包括自身在内)验证通过的Commit消息,说明当前网络中的大部分节点已经达成共识,运行客户端的请求操作o,并返回<REPLY, v, t, c, i, r>给客户端,

  r:是请求操作结果,客户端如果收到f+1个相同的REPLY消息,说明客户端发起的请求已经达成全网共识,否则客户端需要判断是否重新发送请求给主节点。记录其它副本节点发送的Commit消息到日志中。

 

 

图为节点数为4,失效节点数为1情况下的共识过程,其中,C为客户端,0为主节点,3为失效节点。

注意:可能有人会注意到图中普通节点并未收到2f + 1个(包括自身在内)Prepare消息依然进入了Commit阶段,这是由于主节点不广播Prepare消息。查询多方资料后未得到充分的解释,姑且认为,默认已收到主节点的Prepare消息或将主节点的Pre-Prepare消息也算在内。

为什么节点需要收到2f+1个Prepare和Commit消息才确认?

具体的正确性证明比较麻烦,此处为便于理解只作简单分析

2f+1作为达成共识的一个条件,需要考虑各种极端情况,例如:

若规定收到大于2f+1个消息才确认,则在总节点数为3f+1的系统中,当f个恶意节点都不发送消息时,系统将永远无法达成共识。

若规定收到小于2f+1个消息才确认,则也无法保证达成共识或诚实节点的消息占大多数。

为什么客户端收到f+1个确认时,交易就成功了?

  因为默认问题节点为f,那么f+1个确认节点中,肯定有1个是诚实的节点,只要有1个诚实的确认消息,则交易成功,因为1个诚实的消息必须是2f+1个节点都commit操作成功了,才可能有这个1个最终确认消息的。所以为了提升交易处理的速度,只要有f+1个确认反馈,就可以表示交易成功。

垃圾回收

  为了确保在视图轮换的过程中,能够恢复先前的请求,每一个副本节点都记录一些消息到本地的日志中,当执行请求后副本节点需要把之前该请求的记录消息清除掉。最简单的做法是在Reply消息后,再执行一次当前状态的共识同步,但成本比较高,因此可以在执行完多条请求K(例如:100条)后执行一次状态同步。状态同步消息就是CheckPoint消息。

  节点i发送<CheckPoint, n, d, i>给其它节点,n是当前节点所保留的最后一个视图请求编号,d是对当前状态的一个摘要,该CheckPoint消息记录到日志中。如果副本节点i收到了2f+1个验证过的CheckPoint消息,则清除先前日志中的消息,并以n作为当前一个stable checkpoint(稳定检查点)。

  实际中,当节点i向其它节点发出CheckPoint消息后,其它节点还没有完成K条请求,所以不会立即对i的请求作出响应,还会按照自己的节奏,向前行进,但此时发出的CheckPoint并未形成stable,为了防止i的处理请求过快,设置一个上文提到的高低水位区间[h, H]来解决问题。低水位h等于上一个stable checkpoint的编号,高水位H = h + L,其中L是指定的数值,等于checkpoint周期处理请求数K的整数倍,可以设置为L = 2K。当节点i处理请求超过高水位H时,此时就会停止脚步,等待stable checkpoint发生变化,再继续前进。

视图轮换

  当普通节点感知到primary异常的时候,触发view-change,重新选举必须要有2f+1个节点都confirm(VIEW-CHANGE)了,发起重选才生效,一旦超过2f节点都发起VIEW-CHANGE消息,则选举结束,p =v+1 mod |R|节点当选为new Primary。并且new primary会根据自己统计的VIEW-CHANGE的内容,生成并广播NEW-VIEW消息,其它节点验证之后,开始新的view

<VIEW-CHANGE, v+1, n, C, P, i>消息

v+1 :新的view编号

n是最新的stable checkpoint的编号

C是2f+1验证过的CheckPoint消息集合

P是当前副本节点未完成的请求的PRE-PREPARE和PREPARE消息集合

新的主节点就是 newPrimary = v + 1 mod |R|。当newPrimary收到2f个有效的VIEW-CHANGE消息后,向其他节点广播NEW-VIEW消息

<NEW-VIEW, v+1, V, O>

V是有效的VIEW-CHANGE消息集合

O是主节点重新发起的未经完成的PRE-PREPARE消息集合

未完成的PRE-PREPARE消息集合的生成逻辑:

选取V中最小的stable checkpoint编号min-s,选取V中prepare消息的最大编号max-s。

在min-s和max-s之间,如果存在P消息集合,则创建<<PRE-PREPARE, v+1, n, d>, m>消息。否则创建一个空的PRE-PREPARE消息,即:<<PRE-PREPARE, v+1, n, d(null)>, m(null)>, m(null)空消息,d(null)空消息摘要。

普通节点收到主节点的NEW-VIEW消息,验证有效性(各个节点都统计view-change的个数),有效的话,进入v+1状态,并且开始O中的PRE-PREPARE消息处理流程。

总结

优点:

  • 节能。
  • 吞吐量高。
  • 分叉几率很低。
  • 节点数适当时交易延时极低。
  • PBFT中的主节点并不具备很大权限,与普通节点地位相对平等,如果主节点出现问题,普通节点可以拒绝其请求并可以很容易地将其替换。

缺点:

  • 节点需要选举或许可,不像PoW可以随意加入,去中心化程度相对较弱。
  • 不能很好的存贮记录交易信息,黑客能够截取一些失效的副本,这可能会让信息外漏。
  • 系统节点是固定的,无法应对公有链的开放环境。因此只适用于节点数量少且网络环境更可信的联盟链或私有链。
  • 安全边界较Pow等算法相对较低。Pow对网络内恶意算力容忍度为不超过1/2,PBFT对恶意节点数的容忍度则为1/3。
  • 受节点数量限制,可扩展性差,由于每个副本节点都需要和其它节点进行P2P的共识同步,因此随着节点的增多,性能会下降的很快。

PBFT在很多场景都有应用,在区块链场景中,一般适合于对强一致性有要求的私有链和联盟链场景,但如果能够结合DPOS节点代表选举规则,也可以应用于公有链。

posted @ 2021-11-12 16:12  Mr.peter  阅读(1513)  评论(0编辑  收藏  举报