本篇博客主要讨论关于PBFT
的一个问题,即:为什么PBFT
协议中需要Commit
阶段?
I. 写在前面:一个问题
我们首先来思考一个问题:对于BFT
类型的共识,每个正确节点在执行一条交易(客户端发出的执行请求)之前中需要满足什么样的条件?或者说它应该在何时执行一条交易?
笔者的理解是:它需要利用一个确切的机制/算法,去检查一种时机/条件。该算法保证满足了该时机/条件后,其他正确节点也会做出相应的执行操作,此时其便可执行该交易。这便暗含着一个要求,即:只要某个正确节点执行了一个交易,其他正确节点必须也对该交易进行执行。
我们以PBFT
中的场景为例,当编号为n
的消息m
在正确节点i
中得到了执行,那么该消息在正确节点j
中也必须得到执行,且执行编号也为n
。更重要的是,即使节点i
在执行了消息m
之后发生了主节点的切换,节点j还是必须以n
为编号执行消息m
。
有人可能会问:在后期,节点j
执行了编号为n
的消息p
,并且在节点i
也执行编号为n
的消息m'
(即对编号为n
的消息进行覆盖执行),不也能保证所有节点的状态一致性嘛。但这里存在两个问题:
- 如果真采用这种方式,那节点状态就永远得不到确认了,因为在后期永远存在被覆盖的可能性,这实际上就没有意义了;
PBFT
中就没必要设计那么复杂的View Change
机制了,每次换完主节点之后,丢弃之前的所有数据状态,重新开始不就行了吗?PBFT
中复杂的View Change
设计就是为了保证节点i
中执行了的交易,即使在切换主节点之后也一定会被其他正确节点执行。
引用StackOverflow中的一个回答如下。简言之,该回答中表示:整个PBFT
集群应该呈现出一种黑盒状态,其只要对client
返回了reply
,该reply
就不应该再被否认/改写。该回答中还提到一个名词durable access
,比较好地表达了该意思。
The system appears to a client as a black box. The whole idea of this box is to provide reliable access to some service, thus, it should mask the failures of a particular replica. Otherwise, if you discard everything at each view change, clients will constantly lose their data. So basically, your solution simply contradicts the specification.
II. Prepared数据作为可执行证明
PBFT
论文中提到“Prepared
原语保证了在同一个View
中,不同节点中编号n
对应的消息是一致的。”这一点的具体证明可以参考PBFT
论文以及本博客最后列出的参考文献。 也就是说,如果不考虑主节点的切换,Preprepare
阶段+Prepare
阶段就能达到共识的目的。
因此,在不存在主节点切换时,我们可以将Prepared
数据(包括对应的Preprepare
消息和2f个Prepare
消息)作为一条消息m
的“可执行”证明。
III. 假设不存在Commit阶段
我们假设集群中共有10个节点,节点A
到节点J
,其中节点A
到节点G
是诚实节点,节点H
、I
、J
是恶意节点。 假设不存在Commit
阶段,节点A
在达到Prepared
状态后便执行了编号n
对应的消息m
,但此时节点B
到节点G
都没有达到Prepared
状态。同样在此时,网络开始执行主节点切换。若不采用任何机制,新的主节点可能不知道编号n
已经对应了消息m
(或者新的主节点是恶意节点,假装不知道编号n
已经对应了消息m
)。于是,新的主节点发起对应于编号n
的消息m'
。节点B
到节点G
将针对新消息m'
进行共识,从而得到与节点A
中不一样的执行结果。
有同学可能会有以下几点疑问:
- 节点
A
不是有了Prepared
数据了嘛。它可以直接把这份Prepared
数据发送给其他节点,证明编号n
对应着消息m
。不就可以避免不一致了吗? 答:由于PBFT
是弱同步网络,节点A
的消息不保证能及时发送到其他节点。可能等其他节点收到节点A
的Prepared
数据之前,已经完成了编号n
对应消息m'
的共识了。 - 接着上一条,那就再定义一个机制嘛。即使其他节点达成了编号
n
对应消息m'
的共识,如果它们又收到节点A
的Prepared
消息,那就对之前的共识进行回滚,重新采用节点A
的Prepared
消息作为共识。 答:这不就和第1小节中的理解矛盾了嘛。其他节点已经达成了共识,却又在后期进行改写。并且在未改写之前,整个集群中的节点出现了两种状态,即分别执行了(n
,m
)和(n
,m'
)。 - 既然在主节点切换之前,节点
A
已经达到了Prepared
状态了,说明其他至少6个节点已经发出了编号n
对应消息m
的Prepare
消息或PrePrepare
消息。那么其他节点在主节点切换后,对编号n
对应两种消息(m
和m'
)的情形进行特殊处理不就行了吗?比如说采用View
编号小的消息m
. 答:这里有个问题:如果上一个View
中的主节点是恶意节点,那么他发送的消息将一直影响着整个集群(如果采用View
编号小的消息m
,恶意主节点的消息将一直有效并废弃新主节点中的对应消息),使得在新View
中也迟迟无法达成共识。这便失去了主节点切换的意义了:主节点切换就是为了解决当前主节点是恶意节点而阻塞共识的情况。
IV. Commit阶段如何解决上述问题
通过以上三小节的分析,我们可以发现,BFT
共识算法中最难的问题之一就是:如何在主节点切换后,还能保证消息执行的一致性。换句话说:若在View v
中节点i
以编号n
执行了消息m
,那么在View v+1
中节点j
也必须以编号n
执行消息m
。 下面,我们来看看Commit
阶段是如何解决该问题的(准确来说是配合View Change
来解决该问题)。
一方面,PBFT
中要求节点i
只有接收到了2f+1个Commit
消息后,才算是达到了Committed
状态,也才能执行对应的消息m
。这保证了: 在此时,PBFT
集群中至少有2f+1个节点到达了消息m
对应的Prepared
状态,拥有了Prepared
数据。
另一方面,PBFT
中要求节点在发出的VIEW-CHANGE
消息中携带Prepared
数据,且在新主节点发出的NEW-VIEW
消息中至少携带2f+1个VIEW-CHANGE
消息。结合上一段中提到的2f+1个节点拥有了Prepared
数据,可以得出:NEW-VIEW
消息中至少包含f+1个Prepared
数据。且又由于恶意节点最多f个,因此这f+1个Prepared
数据保证了至少一个是从诚实节点发出的。
也就是说:PBFT
通过Commit
和View Change
的机制,强制要求新主节点在NEW-VIEW
消息中包含所有在上一轮中已经被Committed
了的消息对应的Prepared
数据。
这里,读者可能会有几个疑惑:
- 为什么一定要是包含
Prepared
数据呢?包含Prepare
数据不行吗?只要表明了编号n
对应于消息m
就行了啊。 答:包含Prepare
数据不行,因为Prepare
消息可能是错误的(准确来说,由于恶意主节点的误导,诚实节点发出了错误信息)。其实这与另外一个问题是等价的,即:节点i在到达Prepared
状态后立即执行消息m
,并在发出的VIEW-CHANGE
消息中携带Prepare
消息(注意是Prepare
消息,不是Prepared
数据)。这保证了NEW-VIEW
消息中至少包含f+1个Prepare
消息,且至少一条是从诚实节点发出的。 但诚实节点发出的Prepare
消息不一定是正确的。具体而言,假设一共有4个节点(A
、B
、C
和D
,其中D
是恶意节点),在View v
中主节点是D
,其向A
和B
发送了a=1
的消息,而向C
发送了a=2
的消息。节点A
在收到B
的Prepare
消息后到达了Prepared
状态,此时发生了主节点切换。切换过程中的NEW-VIEW
消息中包含了2条Prepare
数据,一条是C
发出的,一条是D
发出的。也就是说在新View
中,集群将对a=2
进行共识。这就是我们在上一段所说的:NEW-VIEW
消息中确实至少包含了一条诚实节点发出的Prepare
消息,但由于恶意主节点的误导,这个Prepare
消息本身就是错误的。 那么如果NEW-VIEW
消息中包含的至少一条诚实节点发出的消息是Prepared
数据,会有什么不同呢?由第2节的分析可知,Prepared
数据是经历过“简单共识”过的,也就不会受到恶意主节点的误导,从而保证了在View v
中执行了的消息也一定还是会在View v+1
中执行。 - 既然
Prepared
数据是经过共识,其便是无法伪造的,为啥需要NEW-VIEW
消息中至少携带2f+1个VIEW-CHANGE
消息(从而至少包含f+1个Prepared
数据)?反正Prepared
无法伪造,有一个不就行了吗? 答:Prepared
数据虽然无法伪造,但恶意节点可能在发送VIEW-CHANGE
消息时故意遗漏某个消息m
对应的Prepared
数据。为处理这种情况,PBFT
要求NEW-VIEW
中至少包含2f+1条VIEW-CHANGE
消息。
综上,Commit
阶段主要是用来与View Change
机制进行配合,从而保证在上一个View
中以编号n
执行了的消息/交易m
,在新的View
中也会以相同的编号n
执行。引用论文中的一句话来说便是:
The prepare and commit phases are used to ensure that requests that commit are totally ordered across views.