这个问题(1975首次发行,1978年被命名)描述了两个将军在攻击同一个敌人的场景。将军1号被认为是领导,而另一个被认为是跟随者。每个将军的军队都无法仅靠自己的力量成功打败敌军,所以他们需要合作并同一时间发起攻击。这看起来是一个简单的情况,但有一点要注意:
为了两军的沟通和决定作战时间,将军1号必须要派遣一个信使穿过敌人的营地去把攻击时间告诉将军2号。但是,信使可能会被敌人抓住因而信息无法传到友军。那会导致将军1号发起攻击时,将军2号和他的军队还呆在原地。
即使第一条信息传到了,将军2号也需要确认(ACK,注意和TCP三次握手的相似之处)他收到了信息,所以他要派遣一个信使回去,因此重复上一个信使可能被抓的情况。这会延伸到无限的ACK,两位将军将无法达成一致。
没有任何办法可以保证第二个要求,那就是每个将军都要确保对方同意了攻击计划。两个将军都总会怀疑他们最后的信使是否能到达。
(因为信使无法到达的可能性总是大于0,所以将军们永远无法以100%的自信达成共识。)
两个将军问题已被证实无解。
于1982年由Lamport、Shostak和Pease着名描述,是一个带反转的广义版本的两个将军问题。它描绘了同一个场景,但两个以上的将军需要对攻打他们共同敌人的时间作出同意。增加的一层复杂性就是,其中一个或几个将军有可能是叛徒,意味着他们可以对他们的选择撒谎(比如他们同意在0900发起攻击但实际上他们不)。
两个将军问题中领导者-跟随者的关系变成了指挥官-中尉的组合。为了在这里达成共识,指挥官和每个中尉必须就同一个决定达成一致(为了简单,只有攻击或撤退)。
除了IC2.之外,有趣的事,如果指挥官是叛徒,还是必须达成共识。结果,所有的中尉成为了多数票。
在这种情况下达成共识的算法是基于一个中尉所观察到的大多数决策的价值。
定理:对于任意m,如果有多于3m的将军和至多m个叛徒,算法OM(m)达到共识。
这说明只要2/3的成员是诚实的,算法就能达到共识。如果叛徒多于1/3,无法达到共识,这些军队无法协调他们的攻击,敌军胜利。
(m = 0 -> 没有叛徒,每个中尉都服从|m > 0 -> 每个中尉的最终选择来自于大多数中尉的选择)
一个从中尉2号角度来看的示意图应该看得更清楚 — — C是指挥官,L{i}是中尉i号:
(OM(1):中尉3号是叛徒-从L2角度来看)
步骤:
指挥官派v去找所有中尉
L1派v去找L2|L3派x去找L2
L2 <- 大多数(v,v,x) == v
最后的决定是来自L1、L2、L3的大多数票,因此达成了共识。
要记得,重要的是大多数中尉要选同一个决策,哪一个并不重要。
让我们来检验指挥官是叛徒的情况:
(OM(1): 指挥官是叛徒)
步骤:
他们的数值都一样所以达成了共识。这里花点时间来想一下,即使x,y,z各不相同,所有三个中尉的大多数(x,y,z)的值是相同的。在x,y,z全部不一样的情况时,我们可以假设他们采取默认选项撤退。
拜占庭将军问题是一个协议问题,拜占庭帝国军队的将军们必须全体一致的决定是否攻击某一支敌军。问题是这些将军在地理上是分隔开来的,并且将军中存在叛徒。叛徒可以任意行动以达到以下目标:欺骗某些将军采取进攻行动;促成一个不是所有将军都同意的决定,如当将军们不希望进攻时促成进攻行动;或者迷惑某些将军,使他们无法做出决定。如果叛徒达到了这些目的之一,则任何攻击行动的结果都是注定要失败的,只有完全达成一致的努力才能获得胜利。
这一问题是一种对现实世界的模型化,尤指网络当中由于软硬件错误、网络阻塞及恶意攻击导致的各种未知行为。显然,在此处默认了将军们在达成一致的过程中正确的传递出了自己的决定,也就是说叛徒只存在于将军当中,不存在于传令兵当中。故要让拜占庭将军问题有解,必须要具备一个重要前提,即信道必须是安全可靠的。关于信道可靠问题,会引出两军问题。两军问题的结论是,在一个不可靠的通信链路上试图通过通信以达成一致是基本不可能或者十分困难的。
拜占庭将军问题提出后,有很多的算法被提出用于解决这个问题。这类算法统称拜占庭容错算法(BFT: Byzantine Fault Tolerance)。简略来说,拜占庭容错(BFT)不是某一个具体算法,而是能够抵抗拜占庭将军问题导致的一系列失利的系统特点。 这意味着即使某些节点出现缺点或恶意行为,拜占庭容错系统也能够继续运转。本质上来说,拜占庭容错方案就是少数服从多数。
拜占庭将军问题的原始论文给出了一些解决思路,但其更注重理论上的可行性。算法效率不高,算法复杂度为指数级,且文中明确指出时间成本及消息传递数量很大。因此不具备太大的实用价值。
拜占庭容错系统需要达成如下两个指标:
拜占庭系统目前普遍采用的假设条件包括:
拜占庭容错是一个定义容许属于拜占庭将军问题失败类别的系统的特性。拜占庭故障(Byzantine Failure)是失效模式中最困难级别的。这意味着没有任何限制,也不会假设节点可以具有的行为类型(例如,一个节点可以生成任何类型的任意数据时假装成一个诚实的成员)。
拜占庭故障是最严重最难处理的。在飞机发动机系统、核电站和几乎所有行为取决于大量传感器结果的系统都需要拜占庭容错。就连SpaceX都曾考虑过它为系统的潜在需求。
前面提到的算法,只要叛徒的数量不超过将军的三分之一,就是拜占庭容错。其他变形的存在使得解决问题更容易,包括使用数字签名或通过在网络中的对等体之间施加通信限制。
拜占庭帝国进攻敌国,敌国能抵御5支拜占庭军队,拜占庭帝国派出10支军队,这10支军队任何一支单独去进攻都毫无胜算,除非有至少6支军队(一半以上)同时进攻,才能攻下敌国。
10支军队分散在敌国的四周,依靠通信兵骑马相互通信来协商进攻意向及进攻时间。问题来了!他们不确定他们中是否有叛徒。叛徒可能擅自变更进攻意向或者进攻时间。在这种状态下,拜占庭将军们怎么才能保证有6支以上军队在同一时间发起进攻,从而攻下敌国?假定军队之间的通信毫无问题。
没有叛徒情况下: 假如一个将军A提出一个进攻提议(比如:明天上午9点进攻,你愿意加入吗?)由通信兵分别告诉其他的将军。
- 如果幸运,A将军收到了其他5位将军以上的同意,发起进攻。
- 如果不幸,其他将军在此时发出不同的进攻提议(比如:明天上午10点、11点进攻,你愿意加入吗?)。
由于时间上的差异,不同的将军收到并认可的进攻提议可能是不一样的,这是可能出现A提议有3个支持者,B提议有4个支持者,C提议有2个支持者等等。有叛徒情况下:
- 一个叛徒通信兵会向不同的将军发出不同的进攻提议(比如,通知A明天上午9点进攻, 通知B明天下午1点进攻)。
- 而一个叛徒将军也可能同意多个进攻提议(即同意明天上午9点进攻又同意明天下午1点进攻)。
这种发送前后不一致的进攻提议,被称为“拜占庭错误”。而能够处理拜占庭错误的这种容错性,就称为拜占庭容错,Byzantine fault tolerance,简称为BFT。
这些协议假设系统里所有的参与者是已知的,并且产生安全边界,什么安全边界呢,即“如果N方参与,那么系统可以容忍N/4的恶意参与者。”
也就是说,比特币出现之前的那些电子现金协议,都设置了一个多方参与多方共识的容错机制,即假设N个人参与进来,他们不是匿名的,而是已知的,那么系统最多可以容忍四分之一的人搞破坏。只要搞破坏的人不超过四分之一,那么这个系统还会运行良好。
但问题是,在一个匿名的环境里,这样的安全边界对女巫攻击是脆弱的,也就是容易受到女巫攻击,因为一个单一的攻击者可以在服务器或僵尸网络上创造上千个冒充节点,然后用这些节点来单方面控制大多数份额。
在P2P网络中,因为节点可以随时加入或退出,那么为了维持网络稳定,同一份数据通常需要备份到多个分布式节点上,这叫做数据冗余机制。
如果网络中存在一个恶意节点,它可以通过控制多个虚假身份,然后利用这些身份控制或影响网络的其他正常节点。比如,原来需要备份到多个节点的数据被欺骗地备份到了同一个恶意节点(该恶意节点伪装成多重身份),这就是女巫攻击。
女巫攻击出自Flora Rhea Schreiberie在1973年的小说《女巫》(Sybil)改编的同名电影,讲的是一个化名Sybil
Dorsett的女人被诊断为分离性身份认同障碍,兼具16种人格,分裂出16种身份。
互联网上受到黑客集中控制的一群计算机,被黑客用来发起大规模的网络攻击,比如分布式拒绝服务攻击(DDoS)、发送海量垃圾邮件等,同时黑客控制的这些计算机所保存的信息也都可被黑客随意“取用”。
前面那句话就是说,在匿名环境下,比特币出现之前的那些电子现金协议,都是不可靠的,太脆弱。
区块链是不受中央权威管制的去中心化帐簿们。由于存储在这些帐簿中的价值,不良成员有巨大的经济动机去尝试造成故障。所以,拜占庭容错,也就是拜占庭将军问题的解决方案是区块链非常需要的。
在没有BFT的情况下,同行可以传输和发布虚假交易,从而有效地消除了区块链的可靠性。更糟糕的是,没有中央权威来接管和修复损害。
发明比特币时的一个重大突破就是利用“工作量证明”(Proof-of-Work)作为拜占庭将军问题的概率解决方案。
在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共识算法要点有:
PBFT(Practical Byzantine Fault Tolerance)
算法由麻省理工学院的Miguel Castro 和Barbara Liskov于1999年提出,解决了原始拜占庭容错算法效率不高的问题,将算法复杂度由指数级降低到多项式级,使得拜占庭容错算法在实际系统应用中变得可行。
PBFT是联盟币的共识算法的基础。实现了在有限个节点的情况下的拜占庭问题,有3f + 1的容错性(拜占庭将军问题也只在节点数N > 3f时可解),并同时保证一定的性能。其采用了密码学相关技术(RSA 签名算法、消息验证编码和摘要)确保消息传递过程无法被篡改和破坏。
拜占庭将军问题在节点数N > 3f时有解的正确性证明比较复杂,此处仅举一个简单例子说明。
在恶意节点数f = 1,节点数N = 3f = 3时,可以看到,发令者和接令者任意角色出现叛徒都会导致其他节点无法作出决定。
而当节点数N>3f(如4个)时,无论哪个角色出现叛徒,最终其他节点总能根据少数服从多数的原则达成共识。
为什么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原始论文中,存在副本节点(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发送
o:请求的具体操作
t:请求时客户端追加的时间戳
c:客户端标识。
REQUEST: 包含消息内容m,以及消息摘要d(m)。
客户端对请求进行签名。
(2)Pre-Prepare
主节点收到客户端的请求,需要对客户端请求消息签名是否正确进行校验。
非法请求则丢弃。正确请求则分配一个编号n,编号n主要用于对客户端的请求进行排序。然后广播一条<
消息给其它普通节点。
v:视图编号
d:客户端消息摘要
m:消息内容
主节点对
进行签名。
(3)Prepare
普通节点i收到主节点的Pre-Prepare消息,需要满足以下条件方可接受消息:
A、请求和预准备消息的签名正确,并且d与m的摘要一致。
B、当前视图编号是v。
C、该普通节点从未在视图v中接受过序号为n但是摘要d不同的消息m。
D、预准备消息的序号n在区间[h, H]内。
非法请求则丢弃。正确请求则普通节点i进入准备状态并向所有其它节点(包括主节点)发送一条
消息, v, n, d, m与上述Pre-Prepare消息内容相同,i是当前副本节点编号。
普通节点i对
签名。记录Pre-Prepare和Prepare消息到日志中,用于视图轮换过程中恢复未完成的请求操作。
Prepare阶段如果发生视图轮换会导致丢弃Prepare阶段的请求。
(4)Commit
主节点和普通节点收到PREPARE消息,需要满足以下条件方可接受消息:
A、普通节点对Prepare消息的签名正确。
B、消息的视图编号v与节点的当前视图编号一致。
C、n是否在区间[h, H]内。
非法请求则丢弃。如果节点i收到了2f+1个(包括自身在内)验证通过的Prepare消息,表明网络中的大多数节点已经收到同意信息,则向其它节点包括主节点发送一条
消息,v, n, d, i与上述PREPARE消息内容相同。
节点i对
签名。记录Commit消息到日志中,用于视图轮换过程中恢复未完成的请求操作。记录其它副本节点发送的Prepare消息到日志中。Commit阶段用来确保网络中大多数节点都已经收到足够多的信息来达成共识,如果Commit阶段发生视图轮换,会保存原来Commit阶段的请求,不会达不成共识,也不会丢失请求编号。
(5)Reply
主节点和普通节点收到Commit消息,需要满足以下条件方可接受消息:
A、节点对Commit消息的签名正确。
B、消息的视图编号v与节点的当前视图编号一致。
C、n是否在区间[h, H]内。
非法请求则丢弃。如果副本节点i收到了2f+1个(包括自身在内)验证通过的Commit消息,说明当前网络中的大部分节点已经达成共识,运行客户端的请求操作o,并返回
给客户端,
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发送
给其它节点,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
消息
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消息
V是有效的VIEW-CHANGE消息集合
O是主节点重新发起的未经完成的PRE-PREPARE消息集合
未完成的PRE-PREPARE消息集合的生成逻辑:
选取V中最小的stable checkpoint编号min-s,选取V中prepare消息的最大编号max-s。
在min-s和max-s之间,如果存在P消息集合,则创建<
消息。否则创建一个空的PRE-PREPARE消息,即:<
m(null)空消息,d(null)空消息摘要。
普通节点收到主节点的NEW-VIEW消息,验证有效性(各个节点都统计view-change的个数),有效的话,进入v+1状态,并且开始O中的PRE-PREPARE消息处理流程。
优点:
缺点:
PBFT在很多场景都有应用,在区块链场景中,一般适合于对强一致性有要求的私有链和联盟链场景,但如果能够结合DPOS节点代表选举规则,也可以应用于公有链。