前面几讲里,“确认"永远是概率性的——等六个块,回滚的可能性小到可以忽略,但从不为零

如果你要做的是清算、是结算、是"这笔钱到底算不算数”,“极小概率"这个答案是不够用的。你想要的是一句"确定了,永不改变”。

这一讲讲的就是怎么拿到这句话。

前两讲的中本聪共识和 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 提供最终性,而抽样让消息复杂度与总验证者数解耦

思考题

  1. 完整推导法定人数交集:为什么 |A∩B| ≥ |A|+|B|−n?为什么"至少一个诚实"就足以保证安全?
  2. 如果 PBFT 去掉 COMMIT 阶段,构造一个具体的场景说明安全性会怎样被违反。
  3. 为什么 PREPARE 阶段只能保证"同一视图内"的唯一性,而不能跨视图?
  4. 详细说明 PBFT 视图切换的 O(n³) 是怎么来的。n=100 和 n=1000 时的差别有多大?
  5. Tendermint 的锁定规则中,为什么解锁必须要求"更高轮次"的 polka?如果允许同轮次解锁会怎样?
  6. 用锁定规则证明:若 v 在第 r 轮被提交,则第 r+1 轮不可能提交别的值。
  7. Tendermint 在 60/40 分区时停机,以太坊会稀释少数派。分别说出这两种选择在什么场景下更合适。
  8. HotStuff 多加了一个阶段,延迟增加了。为什么这个交换是划算的?在什么情况下不划算?
  9. 解释 Chained HotStuff 的流水线为什么不增加延迟。它和 CPU 流水线有什么共同的前提?
  10. 一条链有 180 个验证者,另一条有 100 万个。用第 3 讲的框架比较它们的去中心化程度,并说明这个差别在什么情况下会真实兑现。