PBFT是一种分布式节点间的状态复制算法,在总节点数为n的情况下,它能容错不超过 ⌊ n − 1 3 ⌋ \lfloor \frac {n-1}{3} \rfloor ⌊3n−1⌋的拜占庭节点,使得大多数的状态复制机(replica)保持一致的状态如果需要保持分布式节点之间的系统状态的一致性,只需要做到如下两点即可:
PBFT协议通过完成上述两个要求从而实现了系统的一致性.在介绍PBFT协议的具体内容前,应该首先明确该协议的系统模型.
PBFT算法以状态机复制的形式展示,将服务建立为在分布式环境下跨机器之间进行数据复制的状态机模型. 每一个状态复制机都会维护一个当前的服务状态,并且保证所有非拜占庭节点的系统状态全部相同, 而这些状态复制机执行客户的请求后转入下一个状态,并将执行结果反馈给client.
在PBFT协议中,主要有三个身份,client,primary和backup, 所有状态复制机中有一个主节点,称之为primary, 其他状态复制机称之为backup. client向primary发出操作请求,primary把client的所有请求发送给所有backup, primary和其他backup通过按照PBFT协议对操作进行响应,然后将执行结果返回给client. pbft论文中默认client在收到前一个request的执行结果之前不会发送下一个request.每一个primary在其任期内对应一个view, 下一任primary上任时,view自动加1.一个view中只有一个primary.
整个算法的整体流程如下:
在该协议中,假定拜占庭节点的数目最多为 f f f个, 而全体节点的数目为 3 f + 1 3f+1 3f+1.
client向primary发出一个request, request的具体形式如下:
< R E Q U E S T , o , t , c > σ i
o o o表示operation,即具体执行的操作,t表示timestamp, 之所以需要加上时间戳,是在一个client在发出多次相同operation的时候,primary用timestamp进行区分, c表示client编号, 最后client将这些信息进行签名,以证实自身合法的身份.
注意: 如果client发送给了非primary节点,那么该节点会将client的request转发给primary.
为了确保所有的非拜占庭节点保持一致的状态,primary需要对 request 分配一个序号(sequence number),如果全体状态复制机能够对每一个 r e q u e s t request request的序号达成一致,那么就能达成整个系统服务状态的一致性. 首先,primary会检查 r e q u e s t request request的合法性,客户端的检查客户端的签名是否正确, 如果不正确,则拒绝执行后续步骤; 如果正确,则给 r e q u e s t request request分配一个唯一的序列号n,然后向其他backup广播一条 p r e − p r e p a r e pre-prepare pre−prepare(可以翻译为预准备)消息,形式如下:
< < P R E − P R E P A R E , v , n , d > σ p , m > \left < \left < PRE-PREPARE, v, n, d \right > _{\sigma p}, m \right > ⟨⟨PRE−PREPARE,v,n,d⟩σp,m⟩
v v v是当前view序号, n n n是为request分配的序列号, d是request的摘要,m代表client的request信息.
backup收到pre-prepare消息时,会验证如下消息:
如果上述条件全部符合,则backup接受这个pre-prepare消息, 并且进入prepare阶段.
backup在prepare阶段,向本地的log中插入 pre-prepare消息以及自身的 prepare消息 ,并向其他backup(包括primary)广播一条prepare消息,prepare消息形式如下:
< P R E P A R E , v , n , d , i > σ i \left < PREPARE, v, n, d,i\right >_{\sigma i} ⟨PREPARE,v,n,d,i⟩σi
其中 i i i是backup的编号, σ i \sigma_i σi表示节点 i i i的签名, backup(包括primary)收到来自其他节点的prepare消息时,会验证如下信息:
如果上述验证通过,则将这条信息插入本地log.
如果replica i i i收到 2 f + 1 2f+1 2f+1(包括自身的)相同的 prepare消息,并且这些消息与replica收到的 pre-prepare消息的 m , v , n m, v, n m,v,n相同, 则认为 prepared(m, v, n, i) 为真,并将***prepared(m, v, n, i)*插入log, 然后进入commit 阶段.
p r e p a r e d ( m , v , n , i ) prepared(m, v, n, i) prepared(m,v,n,i)的主要作用就是,在一个view中一个request必然唯一的对应一个序列号n, 因为 p r e p a r e d ( m , v , n , i ) prepared(m, v, n, i) prepared(m,v,n,i)为真的条件是,有 2 f + 1 2f+1 2f+1个节点广播信息验证了 m , v , n m, v, n m,v,n的一一对应关系.
证明: 2 f + 1 2f+1 2f+1中最多 f f f个叛徒, 那么至少还有 f + 1 f+1 f+1个诚实的节点发送了与当前primary的pre-preapre消息匹配的prepare消息, 或者这 f + 1 f+1 f+1个节点中存在primary, primary发送 pre-prepare, 其他 f f f个诚实的节点发送了与primary一致的prepare 信息 ; 此时剩余 f f f个诚实节点不会对另外一个 request m’达成一致,即不会认为 p r e p a r e d ( m ′ , v , n , i ) prepared(m', v, n, i) prepared(m′,v,n,i)为真, f个拜占庭节点认同它们也发出f个 p r e p a r e d ( m ′ , v , n , i ) prepared(m', v, n, i) prepared(m′,v,n,i), 总共只能得到 2 f 2f 2f个 p r e p a r e d ( m ′ , v , n , i ) prepared(m', v, n, i) prepared(m′,v,n,i)信息, 由于数量不足,这无法使得 p r e p a r e d ( m ′ , v , n , i ) prepared(m', v, n, i) prepared(m′,v,n,i)为真. 这也解释了为什么prepared阶段需要 2 f + 1 2f+1 2f+1个节点的认同.因此, pre-prepare和prepare两个阶段保证了在同一个view下,n和request的一一对应关系.
如果 backup i i i判定 p r e p a r e d ( m , v , n , i ) prepared(m, v, n, i) prepared(m,v,n,i)为真,则进入commit阶段.
backup进入commit阶段时, 向其他backup(包括primary)广播一条commit消息,具体形式如下:
< C O M M I T , v , n , D ( m ) , i > σ i \left < COMMIT, v, n, D(m), i \right >_{\sigma i} ⟨COMMIT,v,n,D(m),i⟩σi
其他replica收到commit消息后,验证如下信息:
如果上述验证全部通过,节点将该commit信息插入本地log. 如果节点 i i i满足两个条件:
则作出定义** c o m m i t t e d − l o c a l ( m , n , v , i ) committed-local(m, n, v, i) committed−local(m,n,v,i)**为真.然后执行当前 request,并将执行结果返回给client. client如果收到了 f + 1 f+1 f+1个相同的执行结果,则认为执行结果可信.
节点 i i i每一个节点 i i i的状态都代表系统从执行了最低的序列号到当前序列n的所有request后的状态. c o m m i t t e d − l o c a l ( m , n , v , i ) committed-local(m, n, v, i) committed−local(m,n,v,i)为真确保所有节点能够按照相同顺序执行request.执行完毕后所有节点会返回给client执行结果. 执行结果的形式如下:
< R E P L Y , v , t , c , i , r > σ i
其中t是client对应的request的时间戳.
Checkpoint消息有两个作用:
当replica i i i 执行request-m后,如果request-m的序列号 n % T = = 0 n\%T == 0 n%T==0, 则广播一条checkpoint消息:
< C H E C K P O I N T , n , d , i > σ i
其中d是当前n对应的request执行后的系统状态的摘要, i i i是节点编号.
如果收到 2 f + 1 2f+1 2f+1 相互一致并且合法的checkpoint信息,这些checkpoint代表的系统状态是合法的,该checkpoint被称之为stable checkpoint,节点可以删除序列号n之前所有的日志.
当其他节点请求本地传输某个系统状态时, 2 f + 1 2f+1 2f+1个checkpoint消息可以作为传输的系统状态合法的证明.
checkpoint协议用来对下一次序列号的范围进行设定,当某个checkpoint被证明合法后,对序列号范围可以调整为H = h+k, 其中h是上一次稳定检查点的序列号, 假如每100个请求后进行一次检查点验证, k可以设置为200, 即H = 200.
view-change消息是为了应用系统中primary发生故障后用于切换新的primary.
backup每次收到一个request时,就会启动一个计时器,如果计时器超时之后,仍然没有执行该request,该backup认为primary出现了故障,遂发出一个view-change消息,格式如下:
< V I E W − C H A N G E , v + 1 , n , C , P , i > σ i
v + 1 v+1 v+1表示下一个视图的编号, n表示backup i i i已知的最近的一个stable checkpoint的编号, C是一个集合,其中包括 2 f + 1 2f+1 2f+1个有效的检查点信息以证明该checkpoint是stable checkpoint. P是一个集合,集合中的元素是 P m P_m Pm, P m P_m Pm也是一个集合, 每一个 P m P_m Pm中包含序列号大于n的pre-prepare消息以及与之匹配的 2 f 2f 2f个prepared消息.
view序号为v+1时,对应的primary计算方式为 ( v + 1 ) % n (v+1)\% n (v+1)%n. 当v+1视图的primary收到 2 f 2f 2f个有效的view-change消息时,向其他所有节点广播new-view消息,具体格式如下:
< N E W − V I E W , v + 1 , V , O > σ p
其中 V V V是一个集合, 包括 2 f + 1 2f+1 2f+1(包含新的primary自身)个view-change消息, O是pre-prepare消息的集合.
O的计算方式如下:
primary首先确定两个变量 m i n − s min-s min−s和 m a x − s max-s max−s, m i n − s min-s min−s指的是primary收到的所有view-change消息中最新的stable checkpoint的对应的序号n, m a x − s max-s max−s指的是view-change消息中,集合 P P P中所有 P m P_m Pm中pre-prepare消息编号的最大值.
primary为编号为 ( m i n − s , m a x − s ] ( min-s, max-s] (min−s,max−s]范围内的request, 重新创建view序号为v+1的pre-prepare信息. 这里包含两种情况:
第一种情况下, primary为P中所有request创建一个新的pre-prepare消息,格式为:
< < P R E − P R E P A R E , v + 1 , n , d > σ p , m > \left < \left < PRE-PREPARE, v+1, n, d \right > _{\sigma p}, m \right > ⟨⟨PRE−PREPARE,v+1,n,d⟩σp,m⟩
其中d是该request的摘要, n的范围为 ( m i n − s , m a x − s ] (min-s, max-s] (min−s,max−s]
第二种情况下, primary创建一个null request的pre-prepare消息,格式如下:
$ \left < PRE-PREPARE, v+1, n, d^{null} \right > _{\sigma p} $
d n u l l d^{null} dnull是特定的的null request摘要, null request不执行任何操作.
primary将 O O O中的信息插入log之中,如果primary计算得到的 m i n − s min-s min−s大于primary本地最新的stable checkpoint的编号n,则primary也会将相应的checkpoint的proof插入到log之中, 然后丢弃stable checkpoint之前的所有信息,执行后续的pre-prepare信息.
当backup收到primary发送的new-view信息时, 检查如下信息:
如果上述条件全部满足, backup选择最新的stable checkpoint, 删除stable checkpoint之前的所有log, 并根据执行集合O中的pre-prepare信息.执行步骤按照pbft的协议进行,如果该节点已经执行过该request,则会跳过.
**注意:**backup如果没有最新的stable checkpoint的状态,应该首先向其他节点请求最新的stable checkpoint的系统服务状态,然后再执行 ( m i n − s , m a x − s ] (min-s, max-s] (min−s,max−s]范围内的request,否则会导致系统状态不一致.
设系统总节点数为 N N N, 拜占庭节点数目为 f f f,于是剩余的诚实节点的数量为 N − f N-f N−f.
假设节点 i i i想从prepare阶段成功地进入commit阶段的前提是 i i i收到总共 Q Q Q(包括自己的)个与它收到的 < < P R E − P R E P A R E , v , n , d > σ p , m > \left < \left < PRE-PREPARE, v, n, d \right > _{\sigma p}, m \right > ⟨⟨PRE−PREPARE,v,n,d⟩σp,m⟩相互匹配的 < P R E P A R E , v , n , d , k > σ k \left < PREPARE, v, n, d,k\right >_{\sigma k} ⟨PREPARE,v,n,d,k⟩σk 消息, (匹配定义参看前文), Q Q Q的数量应该足够多,使得 i i i可以非常确定 p r e p a r e d ( m , n , v , j ) prepared(m, n, v, j) prepared(m,n,v,j)为真而 p r e p a r e d ( m ′ , n , v , j ) prepared(m', n, v, j) prepared(m′,n,v,j)不可能为真,其中 m ′ ≠ m m'\ne m m′=m, j j j可以等于 i i i.
系统中存在拜占庭节点, 所以 Q Q Q个投票中很可能包括 f f f个拜占庭节点的投票, 如果 f f f个拜占庭节点和一部分诚实的节点的数量(假设数量为A)大于等于Q, 它们可以达成共识, 唯一的确定 ( m , n , v ) (m, n, v) (m,n,v)的对应关系;但是这 f f f个拜占庭节点又想和剩余部分的诚实节点(假设数量为B并且A>B)达成另外一个共识, 唯一的确定 ( m ′ , n , v ) (m', n, v) (m′,n,v)的对应关系, 为了阻止这种情况的发生, 只需要保证 f f f个拜占庭节点和剩余的诚实节点的数量小于Q,它们就无法确定另外一个共识: ( m ′ , n , v ) (m', n, v) (m′,n,v).在此情况下, 如果一部分诚实的节点就 ( m , n , v ) (m, n, v) (m,n,v) 达成共识后不会担心会有其他不同的共识产生, 于是得到如下不等式
f + A < = Q → 2 f + 2 A < = 2 Q f+A<=Q\rightarrow 2f+2A<=2Q f+A<=Q→2f+2A<=2Q, 由于 A + B = N − f < 2 A A+B =N-f < 2A A+B=N−f<2A, 于是可以得到:
2 f + N − f < = 2 Q → N + f < 2 Q { 1 } 2f+N-f <= 2Q\rightarrow N+f<2Q \quad\quad\quad\quad \bold \{1\} 2f+N−f<=2Q→N+f<2Q{1}
另外一方面, f f f个拜占庭节点可能根本不会参与投票,这时必须要保证:
Q ≤ N − f → 2 Q ≤ 2 N − 2 f { 2 } Q\leq N-f \rightarrow 2Q \leq 2N-2f \quad\quad\quad\quad \bold \{2\} Q≤N−f→2Q≤2N−2f{2}
综合上述{1}和{2}两个不等式,可以得到如下结果:
N + f < 2 N − 2 f → N > 3 f N+f<2N-2f \rightarrow N>3f N+f<2N−2f→N>3f
当 N = 3 f + 1 N = 3f+1 N=3f+1 时, 2 Q > 4 f + 1 2Q > 4f +1 2Q>4f+1, 此时可以取 Q = 2 f + 1 Q = 2f+1 Q=2f+1就能满足要求.
另外,PBFT的论文中对于假设恶意节点数量为f时,总结点数n>3f的推导作了另外一番解释:
client发起一个请求后,系统中可能由于f个拜占庭节点不对请求进行响应,client需要对 n − f n-f n−f个响应进行处理,但是如果没有响应的 f f f个节点并非拜占庭节点, client还必须从 n − f n-f n−f中排除 f f f个响应,即 n − f − f = n − 2 f n-f-f = n-2f n−f−f=n−2f, 这些响应数量至少应该超过拜占庭节点的数量, 即 n − 2 f > f , n > 3 f n-2f>f, n > 3f n−2f>f,n>3f.
现在撤销 c o m m i t commit commit步骤, 只有 p r e − p r e p a r e pre-prepare pre−prepare阶段和 p r e p a r e prepare prepare,如果 p r e p a r e ( m , n , v , i ) prepare(m,n,v,i) prepare(m,n,v,i)为真,则直接执行request并且返回给client.
假设有部分诚实的节点数量为 f f f,成功的收到了 2 f + 1 2f+1 2f+1与本地pre-prepare匹配的 p r e p a r e prepare prepare消息, 它们执行了对应的request-m, 假设m对应的序号是 n n n, view序号是 v v v. 但是其他 2 f + 1 2f+1 2f+1个节点在接收 p r e p a r e prepare prepare消息过程中网络不好, 没能收到足够的 2 f + 1 2f+1 2f+1个匹配的 p r e p a r e prepare prepare消息, 所以它们不会执行这个m.(pbft协议中假设网络中信息传输会出现丢失的情况). 然后这 2 f + 1 2f+1 2f+1个节点进行了 v i e w − c h a n g e view-change view−change, 并且primary在这 2 f + 1 2f+1 2f+1个节点中产生, 这 2 f + 1 2f+1 2f+1个 v i e w − c h a n g e view-change view−change消息中没有request-m的对应的 2 f 2f 2f个prepared消息,并且如果出现 2 f + 1 2f+1 2f+1个节点的 view-change消息集合中的 P为空,新的primary重新发布 n e w − v i e w new-view new−view消息时,消息中的O只包含了一个null-request的pre-prepare消息:
< P R E − P R E P A R E , v + 1 , n , d n u l l > σ p \left < PRE-PREPARE, v+1, n, d^{null} \right > _{\sigma p} ⟨PRE−PREPARE,v+1,n,dnull⟩σp
此时执行了request-m的 f f f个节点验证 new-view后通过,它们本地的序号n对应request-m,但是new-view中的序号 n n n对应的是一个null request, 而不是request-m. 这就导致了这诚实的 f f f个节点和另外 2 f + 1 2f+1 2f+1个节点的系统状态已经出现了不一致.这不满足PBFT协议保证系统一致性的另外一个条件,所有系统中的非故障节点都必须对request的执行达成一致.
实际上,引入 c o m m i t commit commit步骤中, c o m m i t t e d − l o c a l l y committed-locally committed−locally为真才执行request这一条件, 就是保证至少有 2 f + 1 2f+1 2f+1个节点(其中至少 f + 1 f+1 f+1个诚实的节点)的 p r e p a r e d ( m , v , n ) prepared(m,v,n) prepared(m,v,n)为真, 这些节点一起执行request.
view-change时,这 2 f + 1 2f+1 2f+1个执行了reqeust-m的节点中,至少有 f + 1 f+1 f+1个节点参与其中,这个节点的参与,保证了request-m在view序号为 v+1的时候分配的编号仍然是 view序号为v时的序号,这就是为什么论文中说 c o m m i t commit commit步骤保证了不同 v i e w view view中request的有序执行.
client会将request广播给所有节点, 如果收到的节点已经处理了这条request,则会直接向client返回执行结果.如果该节点没有处理过这条request,则会将这条request转发给primary. 如果primary在一段时间内不广播该request,其他节点会怀疑primary作恶并且进行 v i e w − c h a n g e view-change view−change操作以更换primary.
在pbft协议中最多有 f f f个恶意的节点,当client收到 f + 1 f+1 f+1个相同的执行结果时,说明其中至少包含一个诚实的节点的执行结果. 诚实的节点返回执行结果的前提就是, 分别在prepared和committed-local达成了一致,并且这个一致性是唯一的,这也能说明系统中诚实的节点达成了唯一的一致, 所以client可以相信执行结果.
为了防止恶意的primary选择一个超级大的序列号n耗尽序列号的空间.不过这个解释不太令人信服,我暂时也不太清楚.
log可以记录节点是否执行了某个request.
另外如果发生 v i e w − c h a n g e view-change view−change时可能会从最新的stable checkpoint重新执行request,log记录了request, 方便重新执行.
其他节点请求stable point时可以直接传输 c o m m i t t e d − l o c a l committed-local committed−local消息方便该节点恢复.
[1] M. Fischer, N. Lynch, and M. Paterson. Impossibility of Distributed Consensus With One Faulty Process. Journal of the ACM, 32(2), 1985
[2] G. Bracha and S. Toueg. Asynchronous Consensus and Broadcast Protocols. Journal of the ACM, 32(4), 1995
[3] Castro M, Liskov B. Practical Byzantine fault tolerance[J]. operating systems design and implementation, 1999: 173-186.