阅读视图

发现新文章,点击刷新页面。
🔲 ☆

所有 BFT 共识的区块链都是中心化的

首先给出一个共识机制在去中心化程度上的排名,这个排名几乎是毋庸置疑的:

PoW > PoS > DPoS > BFT

然后从处理分叉的角度,对比一下 PoS 和 BFT 的差异。

因为 BFT 算法本身决定了,所有使用 BFT 共识的链,都不会存在分叉,无论是软分叉还是硬分叉。没有分叉的链,意味着整个网络同一时刻只会有一个版本,而这个版本取决于项目发行方,哪怕项目发行方不是官方,这一版本也只能来自于某个中心化的组织。所以,使用 BFT 共识的区块链都是中心化的。

假如网络发行方对网络进行了让人无法接受的更改,会发生什么?

在 PoS 共识下,验证者可以选择旧的规则,也可以选择新的规则,这两种规则可以同时存在,直到大多数验证者达成一致,网络恢复一致。如果验证者始终无法达成一致,就会一直分叉下去。

在 BFT 共识下,验证者可以选择旧的规则,也可以选择新的规则,但是如果一方数量达到半数,网络将会停止。直到验证者线下达成一致,网络才会重新启动。

也就是说,当面临本应该分叉的情形时,BFT 会直接停机,这也是为什么 Solona 和 SUI 都出现过网络停止的原因。

到这里你就明白,这里说的中心化,是指在 BFT 网络中不会同时存在两个网络,当然使用其他共识的网络也几乎不会出现这种情况,但是容许这种情况发生。

更进一步的说明,这里说的中心化,是指 BFT 网络中如果一定比例的验证者想要让网络停止,网络就可以停止,只能通过新启动另外一个网络(其实也属于硬分叉的一种)来让网络恢复正常。

这种差异会产生什么影响?以太坊网络中,即使大多数节点已经挂掉,只要还有少数存在,网络就能够正常运行。而 BFT 网络对验证者的容错能力不到一半,如果半数验证者停掉,网络会直接瘫痪,你的所有链上资产无法继续转移。

从投资的角度,如果你打算长期持有某种代币,你觉得哪种网络更安全,更能让你的资产安全受到保障?

不过还要注意的是一点,网络的可靠性不一定来自于去中心化程度,Coinbase 的 Base 网络可靠性来自于美国政府的监管和半合规化,很多交易所和政府机构都会把钱放到 Coinbase Prime 的信托服务里,所以 Base 网络也是比较可靠的。

🔲 ☆

Ethereum Casper 为什么需要 EIP-7251

Casper the Friendly Finality Gadget 是以太坊现在使用的共识机制,属于 PoS 的一种实现。这种关系类似于同样是 PoW 挖矿,Bitcoin 使用 sha256 而 Dogecoin 使用 scrypt。其他的 PoS 实现还有比如 Cardano 的 Ouroboros。

EIP-7251 的主张是增加单个验证者的质押额度上限,原先是 32 ETH,希望改为 2048 ETH,这样可以有效减少验证者的数量,同时有效 P2P 网络的通信量。

这项改动有点迫在眉睫,因为以太坊在测试环境中模拟了大量质押者的情况,测试结果 显示,当质押者数量达到 2.1 M,网络的投票率会不到 50%,已经不能正常进入 Final 状态,意味着检查点机制失效,整个网络处于非常不安全的状态。而以太坊现在的验证者数量已经达到了 1.4M。如果不及时做出改变,以太坊网络将在不久的将来奔溃。

那为什么以太坊会面临这样的困境?PoS 不是公链专属的共识机制,能够适用于大规模网络的吗?

究其原因,Ehtereum Casper 其实是对 BFT 的改进,而不是对 PoS 的改进。

先来看看 Vitalik 是怎么描述 Ethereum Casper 的,他把 Ehtereum Casper 相对于 BFT 的改进视为重中之重:

再来看一下 Ehtereum Casper 的具体流程:节点质押资产成为验证者,然后通过 VRF 来随机选择一个节点出块,出块后所有验证者都对块的有效性进行一次投票。这些投票会先投递给委员会的成员,委员会成员聚合投票结果之后,再在委员会成员之间同步。委员会成员是每隔一段时间随机选举出来的。

对于了解 BFT 但是不了解 Ethereum Casper 的人,在接触到以太坊网络后,当知道只有收到 2/3 投票的块才有资格被标记为 Final 状态时,会不会对 2/3 这个数字有点敏感?因为 2/3 是 BFT 一直在强调的投票比例,以保证 3f+1 的容错能力。

BFT 的投票机制保障了网络绝对不存在分叉,以太坊引入了 BFT 的这个优点,使得 Ethereum Casper 处理分叉场景相对容易,只需要判断哪个区块的得票率最高,就可以认定主流块了。如果验证者同时对两个块投票,验证者会为此受到惩罚,这也是以太坊在众多 PoS 链中唯一一个有 Slash 机制的原因。同时结合 checkpoint 机制,以太坊就可以面对非常复杂的分叉情况,整个网络分叉成树都能从中找出主链。

问题在于,Ethereum Casper 在引入 BFT 优点的同时也引入了 BFT 的缺点,那就是通信量过大。BFT 的通信量是 O(n2) 级别的,一般只能承受 100 个以下的节点规模,例如 这篇报告 就给出了具体的数值。

可以大致计算对比一下 BFT 和 Ethereum Casper 的消息量。

BFT 在 100 个节点的时候大概是 50 tps 的能力,消息膨胀量 O(n2),那么消息数量是:

n = (100^2) * 50)  = 500000  = 0.5 M/s

Ethereum Casper 在 2M 验证者的时候大概 50% 的投票率,以太坊的块时间是 12 秒,一共 64 个委员会,消息膨胀量 O(n),那么消息数量为:

n = 2M * 0.5 / 12 * 64  = 1000000 / 12 * 64  = 5 M/s

这样计算比较草率和粗略,结果数字上差了一个数量级,但是考虑到两种共识机制具体实现上有很大差异,包括测试的硬件环境差异,有出入很正常,总体上差不太多。

所以由于以太坊集成了 BFT 的投票机制,导致以太坊网络需要大量的通信量。或者说,Ehtereum Casper 改进了 BFT 并且把 Stake 机制加入其中,使得 BFT 更进一步能够支撑起十万规模的节点数量。

同时,有没有注意到,Ethereum Casper 的消息膨胀量仅仅只是 O(n),为什么呢,因为 Ethereum Caspe 不需要进行第二次投票,一次就够了。

另外,委员会机制有点像联盟链的分层共识。有些国内公司需要在没有 token 概念的前提下,对区块链技术进行改进,但是 BFT 算法最多只能撑起几十个节点的规模,于是有了基于 BFT 的分层共识,基本思路是,从所有节点中选出一部分节点作为提案节点,然后提案节点来进行出块和投票,其他节点只接收数据,并且每隔一段时间换一次共识组(提案节点)。

对于联盟链,VRF + BFT + 分层共识已经是比较完善的技术组合了。

与之相比,以太坊多出来的是 Stake 机制,联盟链中每一个节点都是验证者,都有机会出块,而以太坊想成为验证者,需要事先质押一定量的 token 才行。后面的委员会机制相比分层共识,也有一些改进,委员会机制保留了每一个验证者的投票权,只是选出一些代表来归集投票结果。而分层共识直接剥夺了多数节点的出块权,只有少数节点负责出块。

所以以太坊的共识能简单理解为 Stake + VRF + BFT + 委员会机制。

🔲 ☆

区块链中的 PBFT 不需要第二次投票

PBFT 为什么需要进行两次投票,第二次投票的作用是什么?这个问题困扰我很久。

逆向推导

从这个角度想,第二次投票在什么情况下是发挥作用的?在第二次投票的结果和第一次不一致的情况下,才是发挥作用的。如果第二次投票的结果和第一次严格一致,那当然没有必要进行第二次投票。

那在什么情况下,第二次投票的结果会和第一次不一样?只有当恶意节点存在并且刻意在第二次投票阶段投出不同的票,两次投票的结果才会不一样。

这是传统 PBFT 的常规操作流程图,其中节点 3 是错误节点或者恶意节点,从始至终没有响应:

这是去掉 prepare 阶段,只保留一次投票过程的流程图,其中节点 3 仍然是错误节点,没有响应:

关键在于,在这个场景中,节点 0、1、2 都是诚实节点,绝不可能恶意投票或者不投票,那么 commit 阶段的结果一定是和 prepare 的结果一致的,所以即使去掉 prepare 阶段,系统最终也会达成一致。

节点 3 一直都是恶意节点,如果在 commit 阶段,0、1、2 中的某个节点投出了和 prepare 不一致的票,整个系统就存在超过 1 个恶意节点,超出了容错能力。

正向理解

要证明第二次投票是必要的,等同于说明如果没有第二次投票,系统将会无法正常运转。

逻辑上,即使说第二次投票有各种各样的好处,通过冗余来增加系统的容错能力、能够及时发现错误并且快速调整到一致的状态等,也不能说明第二次投票是非要不可的。比如这个 Why is the commit phase in PBFT necessary? 中的高赞回答,说了很多但只是正向解释了 commit 阶段的设计和作用。

我目前看到比较靠谱的一个解释在这里:PBFT: Why cant the replicas perform the request after 2/3 have prepared? why do we need commit phase?

其中提到如果没有 commit 阶段,当 view change 的时候,节点将无法保证请求执行的顺序。

我觉得 StackOverFlow 中的描述和高赞回答提到的论文含义还是有出入的。高赞回答的意思是,节点的 execute 因为缺少 commit 阶段而不一致,有的快有的慢。但即使有两轮投票,节点也可能在 commit 阶段之后 execute 之前发生故障,导致执行上的差异,所以这种故障还不是关键场景。

更加合理的场景是论文 Practical Byzantine Fault Tolerance and Proactive Recovery 中提到的,view change 发生的时候,不同的请求使用了相同的序列号,被打包进不同的 view 中。(这句话很凌乱)

Replicas may collect prepared certificates in different views with the same sequence number and different requests. The commit phase solves this problem as follows.

单次投票流程

这个场景基于只投票一次的流程,也就是没有 prepare 阶段的流程。

场景设置

视图 V1

  1. R1 提出提议 P,并广播给 R2, R3, R4。
  2. 提议 P 在 R2, R3, R4 被执行,但 R1 未执行
R1: --R2: P --> 执行 PR3: P --> 执行 PR4: P --> 执行 P

视图切换到 V2

  1. 假设 R1 发生故障,视图切换到 V2
  2. R2 提出新的提议 P’
  3. R2 提出新的提议 P’ 并广播给 R1, R3, R4
  4. 新的提议 P’ 被所有副本执行
R1: --      P' --> 执行 P'R2: P --> 执行 P      P' --> 执行 P'R3: P --> 执行 P      P' --> 执行 P'R4: P --> 执行 P      P' --> 执行 P'

具体示例

假设提议 P 和 P’ 是对相同账户余额的操作:

  • 提议 P:增加账户 A 的余额 10 单位。
  • 提议 P’:减少账户 A 的余额 5 单位。

在视图 V1 和 V2 中的操作顺序和结果如下:

视图 V1

R1: 账户 A 余额 = 100 (未执行 P)R2: 账户 A 余额 = 110 (执行 P)R3: 账户 A 余额 = 110 (执行 P)R4: 账户 A 余额 = 110 (执行 P)

视图 V2

R1: 账户 A 余额 = 100 (未执行 P) --> 执行 P' --> 账户 A 余额 = 95R2: 账户 A 余额 = 110 (执行 P) --> 执行 P' --> 账户 A 余额 = 105R3: 账户 A 余额 = 110 (执行 P) --> 执行 P' --> 账户 A 余额 = 105R4: 账户 A 余额 = 110 (执行 P) --> 执行 P' --> 账户 A 余额 = 105

场景分析

再来重复一下这句话,不同的请求(R2)使用了相同的序列号(R1 认为是 P),被打包进不同的 view (P’)中。相同的序列号应该是指执行的时序,就是当前时间点轮到哪个请求执行了。

在上面这个场景中,确实由于 A 节点故障导致最终状态出现了不一致。

两次投票

两次投票的流程又是如何解决上述场景中的问题?

  1. 如果 A 节点故障发生在收到 prepare 结果之后、开始 commit 之前,所有节点都不会进入 execute 阶段。
  2. 如果 A 节点故障发生在收到 commit 结果之后、开始 execute 之前,A 节点会根据 commit 结果再次尝试执行 P,然后再执行 P’

场景分析

是不是注意到,第 2 条存在一点不公平?

两次投票的场景下,A 节点可以根据 commit 结果再次尝试执行 P。

单词投票的场景下,A 节点并没有根据 commit 的结果再次尝试执行 P,而是直接执行了 P’。

那么其实两次投票并没有完全避免在 execute 之前节点故障导致的状态不一致,仅仅只是通过增加一次通讯的形式,来反复确认其他节点的状态和自己预期是一致的,减少状态不一致的风险。

两次投票把发现故障的时间提前了,如果节点 A 没有在 commit 阶段发出投票,其他节点就知道 A 节点故障了,而不是等到自己已经 execute 了,才发现 A 没有 execute。多一次确认多一份保障,减少系统 execute 后回滚的成本,尽可能在 execute 之前就商量好。两次投票最大的作用应该也就这样了。

总的来说,第二次投票始终都没有体现出必须存在的意义,而只是带来了一些好处,加强了系统的安全性。这个问题可能类似于,TCP 为什么需要 3 次握手才能建立连接?2 次不行吗?估计 1 次也行,只是会引起一些麻烦,3 次确认足够保险。

无状态与有状态

为什么 PBFT 需要反复确认,尽量避免 execute 之后的状态不一致呢?也许任何系统的回滚都是一件非常慎重的事情,所以不惜增加 execute 之前的沟通成本。

无状态

回到上面单次投票的场景,出故障的 A 节点在什么情况下就不会执行 P’ 了?

  1. A 节点知道自己执行 P 失败了
  2. 执行 P’ 之前一定要执行 P

满足这两个条件,即使是单次投票,也可以实现和两次投票一样的效果。

对于无状态的系统,如果节点只记录了一个最终的数字,那还挺难办的,节点知道自己没有执行 P,然后收到了一个 P‘,节点 A 将无法分辨 P’ 的位置,是在 P 后面还是和 P 同等位置。

正常顺序是:

O -> P -> P'

对于 A 节点来说,知道自己没有执行 P,但是收到了一个 P’:

O -> (P')?

要不要执行呢?A 节点就执行了,状态就错乱了。

基于这一点原因,无状态的系统的 execute 是非常慎重的。

有状态

区块链属于有状态的系统,天然记录了自己的执行记录(区块),以及会对请求进行强制的排序(区块哈希、父哈希)。

一个节点收到了区块,它一定能够判断出这个区块的位置,是否应该本轮执行,以及自己是否缺少区块,及时从其他节点把区块同步过来。

所以在区块链的使用场景下,如果只是为了达到多数节点最终状态一致的效果,完全没有必要进行第二次投票。

疑问

PBFT 为什么需要进行两次投票?这个问题在 GPT-4o 的知识边界,详细追问它,它就会开始胡说八道了,这符合 GPT-4o 不了解就开始编造的特点。

以我有限的互联网信息搜索能力,我一直没有找到一个足以让我信服的理由,证明 PBFT 中的第二次投票是必要的。

经过我自己反复的推演,我能得到的结论只有二次投票并不是必须的,仅有一次投票,也可以达到多数节点一致的结果。

可为什么长久以来,PBFT 包括各种变体 Tendermint、HotStuff,都保留了两次投票的流程?为什么从来没有人质疑过第二次投票其实不需要?

我到底错在哪里?也许是对 PBFT 了解不够深入,还没有触及到第二次投票真正发挥作用的场景吧?可如果真的存在这样的场景,为什么没有找到资料把这种场景直接了当地描述出来?

❌