拜占庭容错(BFT)算法让一组已知身份的节点通过多轮投票达成即时一致:经典协议由预准备、准备、提交三阶段构成,收到超过三分之二合格票即最终化,天然容忍不超过三分之一的拜占庭节点。它以通信开销换确定性最终性,是联盟链与新公链表决内核的通用件。本节手跑一轮三阶段表决、触发一次换主,并算清它的规模天花板。
把共识想象成一场圆桌会议:主座提出提案(预准备),与会者互相表态(准备),意见汇总到位后正式举手(提交),过线即决议。现在告诉你会议室里坐着内奸——他可以对不同的人低声说不同的话,可以中途装死,甚至可以在被点名时装傻。会议还能开出一致决议吗?拜占庭容错算法的答案分两半:能,前提是内奸不足三分之一;且决议一旦做出,任何翻案都要再烧掉一批信誉。这个"信誉"在联盟链里是准入资格,在公链里是质押资产——表决内核相同,押注物不同。
三阶段的每一阶段都在回答一个具体问题。预准备:主座把提案编号并广播,确立"我们正在讨论这件事"。准备:与会者收到提案后向所有人广播"我收到了主座的编号提案",当某人收到超过三分之二的准备票,他就确信"至少三分之二的人看过同一份提案"——这一步消灭内奸"对不同人说不同提案"的空间。提交:确认大家都过了准备线后,与会者广播"我准备执行",再收到超过三分之二的提交票,决议落地。为什么需要两轮而不是一轮?因为一轮投票只能证明"我看过了",不能证明"大多数人也确认过大家的确认"——分布式系统里,"我知道你知道"本身就是需要传递的信息。

把会议规则写成代码,让内奸随机捣乱,看决议是否依然正确:
import random class PBFT: def __init__(self, nodes: list, faulty: set): self.nodes, self.faulty = nodes, faulty self.n = len(nodes) self.q = 2 * self.n // 3 + 1 # 三分之二多数 + 1 def deliver(self, sender, receiver, msg) -> bool: """消息投递:内奸的消息随机伪造或丢弃""" if sender in self.faulty: if random.random() < 0.5: return False # 装死 msg = (msg[0], msg[1], "伪造载荷") # 乱说 return True def run_view(self, primary, proposal): votes = {"prepare": set(), "commit": set()} # 预准备:主节点广播(主节点若为内奸则提案被部分节点丢弃) for replica in self.nodes: if replica == primary: continue if not self.deliver(primary, replica, ("pre-prepare", 7, proposal)): continue # 准备阶段:诚实副本互播 for peer in self.nodes: if peer != replica: self.deliver(replica, peer, ("prepare", 7, proposal)) votes["prepare"].add(replica) if len(votes["prepare"]) + (0 if primary in self.faulty else 1) < self.q: return "未过准备线 -> 等待超时触发换主" # 提交阶段:诚实副本确认多数已准备 for replica in votes["prepare"]: votes["commit"].add(replica) if len(votes["commit"]) >= self.q: return f"决议落地:{proposal}(最终,不可回滚)" return "未过提交线 -> 换主重试" random.seed(3) net = PBFT(["主", "副一", "副二", "奸"], faulty={"奸"}) print(net.run_view("主", "区块高度91的提案"))
多跑几次会观察到两种结局:内奸捣乱成功拖延时返回换主,诚实多数齐整时决议落地——错误决议永远不会出现,最坏情况只是慢。再补一段换主的骨架,理解活性如何自救:
def view_change(timeout_monitor): """主节点失联或作恶时的自救流程(骨架)""" if timeout_monitor.primary_silent(rounds=2): new_view = timeout_monitor.view + 1 # 各副本互播自己已确认到的高度与锁定状态 status = timeout_monitor.collect_local_status() # 诚实多数对"新主是谁、从哪个高度续跑"达成一致 if status.honest_agreement(): timeout_monitor.switch_primary( new_primary=rotate(new_view)) return f"视图 {new_view}:换主成功,从锁定高度续跑" return "少数派等待 -> 随大流同步"
一轮投票的漏洞可以用具体骗局演示:内奸对甲说"提案是 A"、对乙说"提案是 B",甲乙各自看到的"多数"其实都掺了内奸的伪造票。准备阶段的全互播消灭了这个空间——每个人独立数出的诚实票必须超过三分之二,内奸的票即使全算上也凑不出第二个多数。提交阶段再走一轮,是为了处理"有人刚过准备线就执行、有人还蒙在鼓里"的窗口:两轮过后,"我确认你确认了"成为共同知识,执行才会同时发生。三分之二的门槛则直接来自 4.1 节推导的不等式:总节点数减去坏节点数再减去可能失联的诚实票,必须仍稳压坏票能伪造的上限——四节点容一奸、七节点容两奸,逐级排下去。
⚠️ 工程现场的两条红线:其一,动态成员的 BFT 集合(节点可加入退出)需要额外的成员变更协议,直接热插拔节点可能在三分之二线附近翻车,联盟链管理台的"一键扩容"背后必须有严谨的过渡轮次;其二,全网消息量随节点数平方级增长(人人互播),几十个节点已是经典实现的舒适区上限,上千节点的公链必须靠委员会抽样或聚合签名把消息量压下来——这是 BFT 内核多见于联盟链与"委员会制"公链的根本原因。
💡 记忆锚点:预准备定议程,准备对暗号,提交按快门。两轮投票拍到的是"共同知识"这张合影,快门按下即最终。
BFT 挡得住协议内的谎言与装死,挡不住协议外的三样东西:主节点审查(按时出块但故意剔除特定交易,协议层看不出"缺席的合法交易",需要交易池交叉比对等应用层对策);分域故障(节点看似分散,实际同云同机房,一次断电同时放倒"多数",物理拓扑审计因此是联盟链上线前的必修课);治理层合谋(超过三分之一的成员协商一致作恶,任何容错数学都无解——信任假设终究有边界)。即时最终性是它最贵的商品:一旦三分之二线越过,回滚等于系统自毁,这份"确定性"正是清算、结算类业务等待的东西。
表决内核讲完,炉型谱系还剩几个"非主流"分支——委托、权威与各种混合变体。下一节把它们一次盘完,然后进入全章的收束:不可能三角与选型决策。