前面几讲里,“确认"永远是概率性的——等六个块,回滚的可能性小到可以忽略,但从不为零。
如果你要做的是清算、是结算、是"这笔钱到底算不算数”,“极小概率"这个答案是不够用的。你想要的是一句"确定了,永不改变”。
这一讲讲的就是怎么拿到这句话。
前两讲的中本聪共识和 PoS 解决的是"谁有资格参与"。这一讲回到第 2 讲的经典问题:参与者已知且固定时,如何达成确定性共识。
⚠️ 而"参与者已知且固定"这个前提,就是它的价钱——下面这张表把两条路的取舍并排放着,它们不是谁更好,是各自付了不同的钱:
这两类协议的目标不同:
中本聪共识:无准入 + 概率最终性 + ⚠️ 分区时会分叉
BFT 共识: 需准入 + 确定性最终性 + 分区时停机
一、共同的骨架
所有 BFT 协议都建立在第 2 讲的两个前提上:
① 部分同步模型:GST 之后消息在 Δ 内送达
⭐ 安全性【永不依赖】时间假设,活性只在 GST 之后保证
② n ≥ 3f + 1
以及一个共同的核心工具:
法定人数交集(Quorum Intersection)
这是理解全部 BFT 协议的钥匙。
打个比方
想象一个十人委员会,任何决议要七票才通过。
现在有人担心:“会不会通过了两个互相冲突的决议?”
不会,而且理由很简单:两个七人集合,在十个人里必然至少重叠四个人——因为 7 + 7 = 14,比 10 多出 4。那四个人如果对两个冲突的决议都投了赞成,就是当场自相矛盾,而这件事是留了记录、可以被指出来的。
⭐ 下面的推导就是把这个"必然重叠"算准确。它是全部 BFT 协议安全性的唯一来源——不是靠加密,不是靠时间,就是靠这个抽屉原理。
设 n = 3f+1,法定人数 = 2f+1。
任取两个法定人数集合 A、B:
|A ∩ B| ≥ |A| + |B| − n
= (2f+1) + (2f+1) − (3f+1)
= f + 1
⭐ 交集至少 f+1 个节点,其中最多 f 个是拜占庭的
⟹ 【至少有一个诚实节点同时属于 A 和 B】
⭐ 这一个诚实节点,就是安全性的全部来源。 它不会对两件矛盾的事都投赞成票,所以两个法定人数不可能确定两个互相冲突的值。
后面每一个协议的安全性论证,最终都归结到这一句话。
二、PBFT:三个阶段
Castro 和 Liskov 在 1999 年给出的方案,是所有现代 BFT 协议的祖先。
客户端 ──▶ 主节点(Primary)
│
① PRE-PREPARE:主节点广播 <序号 n, 值 v>
│
② PREPARE: ⭐ 每个副本广播"我看到主节点提议了 v"
│ 收集到 2f+1 个 ⟹ 进入 "prepared" 状态
│
③ COMMIT: 每个副本广播"我已 prepared"
│ 收集到 2f+1 个 ⟹ 执行并回复客户端
为什么需要三个阶段?两个不够吗?
这是 PBFT 最常被问、也最值得想清楚的问题。
PREPARE 阶段保证的是"同一视图内的唯一性":
若节点 X 达到 prepared(v),说明它收集到了 2f+1 个 PREPARE。
若节点 Y 达到 prepared(v'),同样收集了 2f+1 个。
⭐ 由法定人数交集,存在一个诚实节点同时投了 v 和 v'
⟹ 但诚实节点在同一视图内只会为一个值投票
⟹ 因此 v = v'
那 COMMIT 阶段做什么?——它保证"跨视图安全":
⚠️ 问题场景:
节点 X 已经 prepared(v) 并执行了,
但因为网络问题,其他节点没收到足够消息,触发了【视图切换】。
新视图的主节点如果不知道 v 已被 prepared,
⟹ 它可能提议另一个值 v'
⟹ X 已经执行了 v,别人执行了 v' —— 安全性被违反!
COMMIT 阶段的作用:
在执行【之前】,先确认"有 2f+1 个节点都已经 prepared 了"。
⟹ 那么任何未来的视图切换,
新主节点收集 2f+1 个节点的状态时,
必然会碰到至少一个知道 v 的诚实节点(又是法定人数交集)
⟹ 它无法提议别的值
⭐ 一句话总结:PREPARE 保证"没有两个值被同时准备",COMMIT 保证"已准备的值不会被视图切换抹掉"。
视图切换
主节点失效或作恶时,副本超时后发起视图切换:
① 每个副本广播 VIEW-CHANGE,附上自己所有已 prepared 的证明
② 新主节点收集 2f+1 个 VIEW-CHANGE
③ ⚠️ 它必须证明自己的新提议与所有已 prepared 的值兼容
⟹ 这个证明要包含它收到的全部 VIEW-CHANGE 消息
⚠️ 这就是 PBFT 的复杂度问题:
正常路径: O(n²) 消息(每个节点广播两轮)
视图切换: ⚠️ O(n³) —— 因为每条 NEW-VIEW 消息要携带 O(n) 条 VIEW-CHANGE,
而每条 VIEW-CHANGE 又携带 O(n) 条证明
⭐ n = 100 时,视图切换要传输的数据量已经难以承受。这是 PBFT 无法扩展到大规模验证者集的直接原因。
三、Tendermint:即时最终性
Tendermint(现在的 CometBFT,Cosmos 生态的共识引擎)做了一个关键简化:
⭐ 不设单独的视图切换协议——每一轮都换提议者。
每一轮(round):
① 提议:本轮的提议者广播一个块
② PREVOTE: 每个验证者投票
③ PRECOMMIT:⭐ 收到 2f+1 个 PREVOTE(称为 polka)后,广播 PRECOMMIT
④ 收到 2f+1 个 PRECOMMIT ⟹ 提交,【不可回滚】
任何一步超时 ⟹ 进入下一轮,换一个提议者,重来
⭐ 由于视图切换和正常流程是同一套机制,PBFT 那个 O(n³) 的特殊路径消失了。
锁定机制:安全性的关键
Tendermint 的安全性来自一条锁定规则:
⭐ 一旦某个验证者 PRECOMMIT 了值 v,它就【锁定】在 v 上:
后续轮次中,它只能对 v 投 PREVOTE。
唯一的解锁条件:看到一个【更高轮次】的 polka(2f+1 个 PREVOTE)支持另一个值。
为什么这样是安全的:
若 v 在第 r 轮被提交,说明有 2f+1 个节点 PRECOMMIT 了 v,它们全部锁定在 v。
在第 r+1 轮,任何其他值 v' 要得到 polka,需要 2f+1 个 PREVOTE。
⭐ 由法定人数交集,这 2f+1 里至少有一个诚实节点锁定在 v
⟹ 它不会给 v' 投 PREVOTE
⟹ v' 永远凑不齐 polka ⟹ 不可能被提交
代价:分区时停机
网络分成 60% / 40% 两半:
⚠️ 两边都凑不齐 2f+1(需要 > 2/3)
⟹ 两边都无法提交
⟹ 整条链停止出块,直到分区恢复
⭐ 这是第 2 讲那个选择的直接体现:Tendermint 牺牲活性保安全性。 对比以太坊的非活跃泄漏(第 18 讲)——它会主动稀释少数派,强行恢复活性,代价是可能永久分裂。两条路各有取舍,没有对错。
四、HotStuff:用一个额外阶段换线性复杂度
2018 年的 HotStuff 解决了 PBFT 视图切换昂贵的问题。它做了两件事:
① 用门限签名把广播变成收集
PBFT:每个节点向所有节点广播 ⟹ O(n²)
HotStuff:⭐ 所有节点把投票发给【领导者】,
领导者聚合成一个门限签名(第 8 讲)
再广播这一个签名
⟹ O(n) 消息
② 增加一个阶段,让视图切换变简单
HotStuff 三阶段:PREPARE → PRE-COMMIT → COMMIT
(加上决定阶段共四轮通信)
⭐ 多出的这一个阶段,让"新领导者需要知道的状态"被压缩成一个简单的量:最高的那个 QC(法定人数证书)。
PBFT 的视图切换:新主节点要收集 O(n) 条消息,每条含 O(n) 证明 ⟹ O(n³)
HotStuff 的视图切换:⭐ 新领导者只需知道【最高的 QC】——一个常数大小的对象
⟹ O(n)
⭐ 这是一个典型的工程权衡:多付一轮通信延迟,换来视图切换从 O(n³) 降到 O(n)。
③ 流水线化(Chained HotStuff)
既然每个阶段的消息格式相同,就可以让它们重叠:
区块 1: PREPARE ──▶ PRE-COMMIT ──▶ COMMIT
区块 2: PREPARE ────▶ PRE-COMMIT ──▶ COMMIT
区块 3: PREPARE ────▶ PRE-COMMIT ──▶ COMMIT
⭐ 每一轮通信同时推进三个区块的不同阶段
⟹ 吞吐量提升三倍,延迟不变
⭐ 这个思路和 CPU 的指令流水线完全一致。 Diem(原 Libra)、Aptos、以及多个新链的共识都基于它。
五、三者对比
| PBFT | Tendermint | HotStuff | |
|---|---|---|---|
| 年份 | 1999 | 2014 | 2018 |
| 正常路径消息 | O(n²) | O(n²) | O(n) |
| 视图切换 | O(n³) | O(n²) | O(n) |
| 阶段数 | 3 | 2 | 3(多一轮延迟) |
| 提议者轮换 | 仅失效时 | 每轮 | 每轮 |
| 流水线 | 否 | 否 | 是 |
| 代表系统 | 学术/联盟链 | Cosmos | Diem、Aptos |
六、为什么公链不能直接用 BFT
⭐ 十人委员会好用,是因为**“十个人是谁"这件事是确定的**。而公链上没有名册——谁都可以来,也可以造一万个身份。下面这一节说明这个前提一旦拿掉会发生什么。
看起来 BFT 又快又有确定性最终性,为什么以太坊不直接用?
① ⚠️ 消息复杂度限制了验证者数量
即使 HotStuff 的 O(n),n = 100 万时每轮也要处理百万条消息
⟹ 实际部署的 BFT 链验证者数通常在 100–200 量级
(Cosmos Hub 约 180 个)
② 验证者集必须【已知且固定】
而公链要求任何人可以随时加入退出
③ 分区时停机
对一个全球性的、无准入的网络,这是很强的假设
⭐ 验证者数量少不只是性能问题,它直接影响第 3 讲的"去中心化"维度:180 个验证者的链和 100 万个验证者的链,不该用同一个词描述。
以太坊的解法:两层结构
下一讲会详细讲,这里先给结论:
⭐ 以太坊把 BFT 的"确定性最终性"和中本聪共识的"大规模无准入"缝在了一起:
底层:LMD-GHOST —— 中本聪式的分叉选择,支持百万验证者,随时出块
上层:Casper FFG —— BFT 式的最终性工具,每 2 个 epoch 确定一次
关键技巧:【委员会抽样】
每个 slot 只让一个随机委员会投票,而不是全部验证者
⟹ 消息复杂度与总验证者数解耦
七、Go:法定人数与锁定规则
package bft
// QuorumSize 返回 n 个节点中的法定人数:⌊2n/3⌋ + 1。
// ⭐ 当 n = 3f+1 时,这恰好是 2f+1。
func QuorumSize(n int) int { return 2*n/3 + 1 }
// MaxFaulty 返回 n 个节点能容忍的最大拜占庭节点数。
func MaxFaulty(n int) int { return (n - 1) / 3 }
// QuorumsIntersectHonestly 验证第一节的核心论证:
// 任意两个法定人数的交集里,至少有一个诚实节点。
func QuorumsIntersectHonestly(n int) bool {
q := QuorumSize(n)
f := MaxFaulty(n)
minIntersection := 2*q - n // |A∩B| ≥ |A|+|B|−n
return minIntersection > f // 交集大小超过拜占庭节点上限 ⟹ 必含诚实节点
}
// Vote 是一次投票。
type Vote struct {
Round int
BlockHash [32]byte
Validator int
}
// Validator 实现 Tendermint 的锁定规则。
type Validator struct {
ID int
lockedRound int // −1 表示未锁定
lockedBlock [32]byte
}
func NewValidator(id int) *Validator {
return &Validator{ID: id, lockedRound: -1}
}
// CanPrevote 判断本验证者能否为某个区块投 PREVOTE。
// 锁定后只能投锁定的那个块,除非看到更高轮次的 polka 支持别的块。
func (v *Validator) CanPrevote(round int, block [32]byte, polkaRound int, polkaBlock [32]byte) bool {
if v.lockedRound < 0 {
return true // 未锁定,任何提议都可以投
}
if block == v.lockedBlock {
return true // 就是锁定的那个块
}
// 唯一的解锁路径:存在一个【更高轮次】的 polka 支持这个块
return polkaRound > v.lockedRound && polkaBlock == block
}
// OnPrecommit 记录锁定。
func (v *Validator) OnPrecommit(round int, block [32]byte) {
v.lockedRound = round
v.lockedBlock = block
}
八、本讲小结
- ⭐ 两类共识的目标不同:中本聪共识是无准入 + 概率最终性 + 分区时分叉;BFT 是需准入 + 确定性最终性 + 分区时停机。
- ⭐ 法定人数交集是全部 BFT 安全性的来源:
n=3f+1时两个2f+1集合至少交于f+1个节点,其中至少一个诚实——而诚实节点不会对矛盾的事都投赞成票。 - PBFT 三阶段的分工:PREPARE 保证"同一视图内没有两个值被准备”,COMMIT 保证"已准备的值不会被视图切换抹掉"。
- PBFT 的视图切换是 O(n³)——每条 NEW-VIEW 要带 O(n) 条 VIEW-CHANGE,每条又带 O(n) 证明。这是它无法扩展的直接原因。
- Tendermint 让每轮都换提议者,视图切换和正常流程合二为一,O(n³) 的特殊路径消失。
- Tendermint 的锁定规则:PRECOMMIT 过就锁定,只有看到更高轮次的 polka 才解锁。安全性证明同样归结到法定人数交集。
- Tendermint 在分区时完全停机——两边都凑不齐 2/3。这与以太坊的非活跃泄漏(稀释少数派、强行恢复活性、可能永久分裂)形成明确对比。
- HotStuff 做了两件事:门限签名把广播变成"发给领导者再聚合"(O(n²)→O(n));多加一个阶段,让新领导者只需知道"最高的 QC"这一个常数大小的对象(视图切换 O(n³)→O(n))。
- Chained HotStuff 的流水线:每轮通信同时推进三个区块的不同阶段,吞吐三倍而延迟不变——和 CPU 指令流水线完全同构。
- 公链不能直接用 BFT 的三个原因:消息复杂度限制验证者数(实际部署常在 100–200)、验证者集必须已知固定、分区时停机。
- 以太坊的解法是两层结构 + 委员会抽样:底层 LMD-GHOST 支持百万验证者,上层 Casper FFG 提供最终性,而抽样让消息复杂度与总验证者数解耦。
思考题
- 完整推导法定人数交集:为什么
|A∩B| ≥ |A|+|B|−n?为什么"至少一个诚实"就足以保证安全? - 如果 PBFT 去掉 COMMIT 阶段,构造一个具体的场景说明安全性会怎样被违反。
- 为什么 PREPARE 阶段只能保证"同一视图内"的唯一性,而不能跨视图?
- 详细说明 PBFT 视图切换的 O(n³) 是怎么来的。n=100 和 n=1000 时的差别有多大?
- Tendermint 的锁定规则中,为什么解锁必须要求"更高轮次"的 polka?如果允许同轮次解锁会怎样?
- 用锁定规则证明:若 v 在第 r 轮被提交,则第 r+1 轮不可能提交别的值。
- Tendermint 在 60/40 分区时停机,以太坊会稀释少数派。分别说出这两种选择在什么场景下更合适。
- HotStuff 多加了一个阶段,延迟增加了。为什么这个交换是划算的?在什么情况下不划算?
- 解释 Chained HotStuff 的流水线为什么不增加延迟。它和 CPU 流水线有什么共同的前提?
- 一条链有 180 个验证者,另一条有 100 万个。用第 3 讲的框架比较它们的去中心化程度,并说明这个差别在什么情况下会真实兑现。