目录
1. 拜占庭容错算法
1.1 前言
在本文中,我们尝试从零开始设计一个拜占庭容错的共识算法。但由于本文是在阅读大量文献之后,思考整理得出的,难免会有“事后诸葛亮”的嫌疑,但这不要紧,我们的目的也不是真的为了设计一个全新的共识算法,而是站在设计者的角度,思考一个共识算法是如何一步步得出的。当然,在推理过程中,我们也会使用尽可能少的已知条件,让“从零开始”不那么“哗众取宠”。在文章的后半部分,我们会在前文的基础上,分析对比 PBFT、Tendermint、Hotstuff 这三个共识的共性和差异。相信本文会让你对共识算法有一个较为全面的理解,以后如果遇到新的共识算法,也可以使用本文的思路分析,快速拿下。
1.2 概念澄清
在开始之前,我们先理清一些基本概念。
分布式一致性算法(共识算法):使集群中多个节点保持数据状态一致的通信算法。
拜占庭错误:节点除了宕机(不发送消息),还可能出现随机的恶意行为(发送假消息)。
拜占庭容错算法:能够容忍拜占庭错误的共识算法。设集群中拜占庭节点(错误节点)数为 f,当总结点数 N < 3f+1 时,不存在一个拜占庭容错算法。即总节点的最小数量为 3f+1,本文我们使用这个数量作为总节点的数量。
共识算法的正确性可以分为以下两个性质:
- 安全性 (safety),也叫一致性 (agreement),表示 所有正确节点执行的请求(包括顺序)一致。
- 活性 (liveness),也叫可终止性 (termination),表示 共识总能在有限时间内达成。
换句话说,如果没有安全性,不同节点就可能执行了不同请求,使系统失去了一致性,这是不可容忍的;如果没有活性,那么系统会永远“卡住”,无法处理请求。下面我们从这两个特性出发,尝试站在设计者的角度看一个拜占庭容错的共识算法是如何诞生的。
1.3 假设
为了让算法在可证明正确性的同时可被实际应用,我们需要在特定的网络下设计算法。存在以下几个网络模型:
- 同步(asynchrony):网络延迟存在一个已知的固定上限 Δ。即消息总能在 Δ 时间内传输到目标节点。
- 异步(synchrony):网络延迟不存在固定上限 Δ。即消息不一定能在固定时间内传输到目标节点。
- 部分同步(partially synchrony):网络延迟固定上限 Δ 已知,但仅在一个未知的时间 GST(global stabilization time) 后成立。也就是在 GST 之后,网络为同步网络。当然,同步网络不会永远保持,只会保持一定时间,所以部分同步可以看成是异步网络和同步网络交替进行的网络模型。
实际的网络可能存在丢包、分区等情况,所以不是一个同步的网络;而在异步网络下,无法保证活性;部分同步网络最符合实际,因此我们算法的设计将会基于这个假设。
我们需要设计一个这样的共识算法:即使是在异步网络下,也要保证安全性;在同步网络下,保证活性。这样,在部分同步网络下,就能保证算法的安全性和活性。
分布式一致性算法早在上个世纪就被广泛研究,但随着区块链的兴起,再次进入人们的视野。为了方便讨论,同时使共识算法更贴近于区块链的场景,我们把算法的一些参数对应到区块链中,把“请求”称为“区块”,请求的执行顺序称为“高度”,区块按照高度顺序串联起来就成了区块链。但请记住,共识算法并不关心请求是什么,不同系统可以有不同的请求结构。
同时,我们假设共识为单线程的,即只有完成上一个区块的共识,才能开始下一个区块的共识。并且我们假设存在某个机制,当节点发现自身区块落后于其他节点时,会自动从其他节点同步,本文不对这个机制做具体描述,并简单使用“快速恢复机制”来指代这个过程。
1.4 安全性
如何共识?
系统如何对一个高度的区块达成共识呢?这可以类比一个民主的组织如何达成一个决策,往往会有一个提案者(proposer,为了便于讨论,我们暂且称为 leader)提出一个方案,其他成员进行投票表决。对应到共识算法中,我们需要选出一个 leader,对于某个高度,(收集交易并)组装区块(这个过程我们简称为“出块”,组装好的含高度的区块我们称为“提案”)并广播,其它节点收到的提案后广播投票,收集其他节点的投票,当收集到足够多的相同投票时,确定该提案。
收集多少投票?
由于有 f 个拜占庭可能不投票,所以收集到 2f+1 个投票就该停止,否则系统将失去活性。由此我们给节点引入一个行为:
行为1:节点收集到 2f+1 个相同投票(含提案)时,提交该提案,执行区块并存储,最后向客户端响应。
加锁
另外,由于 leader 可能为错误节点,可能故意向不同节点发送不同提案(同一高度的不同区块),所以为了保证安全性,正确节点只会对某个高度投一票,即当高度 H 已经给区块 A 投过票时,不会再对该高度的其他区块投票,我们称这个行为为“加锁”,并且说节点对于高度 H “锁定”在了区块 A。
由此,我们引入第二个行为限制:
行为2:对于某个高度,正确节点发起或收到一个提案时,会锁定该区块,并坚持对已锁定的区块进行提案或投票,而会忽略该高度下的其他区块提案。
QC 性质
有了加锁机制之后,不同节点还有没有可能在同一高度提交不同区块呢?
稍加推理,我们会发现这是不可能的:假设在高度 H 上,节点 1 提交了区块 A,节点 2 提交了区块 B。那么说明有 2f+1 个节点对区块 A 投了票,有 2f+1 个节点对区块 B 投了票,由于总节点数为 3f+1,所以这两部分节点的交集为 (2f+1)*2 - (3f+1) = f+1,其中错误节点数为 f,所以交集一定有一个正确节点,这个节点对区块 A 和区块 B 都投了票,这与行为 2 产生了矛盾,所以假设不成立。
可见 2f+1 既是我们为了保证活性只能收集的最大数量,同时也是投票的最小数量(为了保证交集至少有一个正确节点),这个投票对应的节点集合称为 Quorum(委员会),投票集合称为 Quorum Certificate(委员会证书,简称 QC)。正如我们上面分析的,QC 具有一个非常重要的性质:任何两个 Quorum 至少有一个共同的正确节点。我们把这个性质称为 QC 性质。因为一个 Quorum 的节点数量为 2f+1,而错误节点数为 f,因此 Quorum 中至少有 f+1 个正确节点,所以 QC 性质也可以表述成 任何两个 f+1 个正确节点的集合至少有一个共同节点。关于 QC 性质的更多讨论,可以参考我的另一篇文章:深入理解PBFT算法——提交阶段的作用。
1.5 活性
上文我们讨论了安全性,接下来我们重点关注下活性。
视图切换
共识算法活性丢失的最直接来源是 leader 出错。当 leader 为错误节点时(宕机或发送任意消息等),可能完全不发提案,也可能给其他节点发送任意不同的提案,使得每个节点都无法收集够 2f+1 个正确投票,共识无法正常进行。所以当检测到 leader 为错误节点时,需要更换 leader,我们称 leader 的一个任期为视图(view),更换 leader 的过程为 “视图切换”,每个视图有一个唯一的编号,称为视图号,并且每次视图切换时视图号都递增 1。至此,提案除了高度和区块之外,还多了一个视图号。
那么如何检测 leader 出错呢?最简单的办法是设定超时时间,如果超过某个时间还没有对某个高度的提