本篇博客主要讨论关于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.