共识算法可以被定义为一个通过区块链网络达成共识的机制。公共的(去中心化的)区块链作为一个分布式系统,并不依赖于一个中央机构,而是由分布式节点多数认可来实现交易。与此同时,共识算法开始发挥作用,它保证了协议规则的正常执行以及交易可以在免信任情况下发生,因此所有的交易都只能被执行一次。
区块链作为一个典型的分布式系统,当需要对其共识算法进行分析的时候就需要先介绍一些基础的分布式知识,同时在共识算法设计的时候也必须要注意这些理论约束。
FLP定定理的论文是由Fischer, Lynch and Patterson三位作者于1985年发表,之后该论文毫无疑问得获得了Dijkstra奖。FLP定理的具体表述为:在含有多个确定性进程的异步系统中,只要有一个进程可能发生故障,那么就不存在协议可以保证在有限时间内所有进程达成一致。异步系统的假设是FLP定理的关键,该定理假设进程是完全异步的,无法获得任何进程处理速度或者消息传输延迟的信息,进程也不能使用同步时钟,因此无法使用基于超时(Timeout)的算法;此外,定理还假设进程无法判断其他进程是处于完全停止状态还是缓慢运行状态。
举个例子,在一个分布式的异步共识系统中,每一个领导者都会从提议者处收集提案;当收集到全部提议者发出的提案后就可以达成一致。显然,当没有故障节点出现的时候,这种共识协议是可行的,只是达成共识的速度会受到最慢节点网络连接的影响。但是当这些提案者中发生了最简单的崩溃或者其他故障,领导者也不会知道,更不会知道需要等待多长时间才能收集到全部的提案,即使领导者重试这个过程依然无法知道故障节点是否可以恢复,因此除了等待别无他法,最终导致整个系统无法达成共识。
CAP定理起源于加州大学柏克莱分校的计算机科学家埃里克·布鲁尔在2000年的分布式计算原理研讨会(PODC)上提出的一个猜想。在2002年,麻省理工学院的赛斯·吉尔伯特和南希·林奇发表了布鲁尔猜想的证明,使之成为一个定理。CAP定理指出对于一个分布式计算系统来说,不可能同时满足如下三个特点,即一致性(Consistency),可用性(Availability)和分区容错性(Partition tolerance)。
- 一致性是指强一致性,也称原子一致性,即分布式系统中的状态在某一时刻必须保持一致。如果一个写操作执行成功,那么之后的所有读请求都必须读到这个新的数据,如果写操作执行失败,那么所有的读操作都不能读到这个数据,不能存在中间状态。
- 可用性是指当集群中部分节点出现故障的时候,集群整体任然可以作为一个处理客户端的请求,所有的读写请求都会在一个有限的时间内得到响应。
- 分区容错性是指当网络出现分区,不同分区的集群节点之间无法相互通信的时候,被分隔的节点仍能对外提供服务。
CAP定理表明,上述三个性质中,理论上最多只能同时满足其中两个;如果同时满足一致性和可用性,则要求网络不能出现分区;如果同时满足可用性和分区容错性,那么不同分区的网络同时对外提供服务就可能导致状态不一致;如果同时满足一致性和网络分区,那么不同分区的网络为了实现状态的一致就必须等待从而导致不能满足一致性。
理论虽然如此,但是在工程实践中这三点并不是非此即彼的关系,往往会放宽一定的限制来满足实际需要。CAP定理给出的一致性要求是强一致性,意味着无论更新操作是在哪一个副本执行之后,之后所有的副本的读操作都要立刻能获得最新的数据。但是这种强一致性在工程中几乎是不可能实现的,不论是因为网络传输中的延迟还是系统本身的延迟。这就导致实践中分布式系统会将这条限制放宽为弱一致性,即用户读到某一操作对系统特定数据的更新需要一段时间。
对于区块链系统来说,它的最终一致性即是弱一致性的表现。以比特币和以太坊为代表的绝大多数公链通常以牺牲强一致性来同时满足最终一致性、可用性和分区容错性。在某一时间节点,区块链可能出现分叉,每一个分叉各自独立的维护一个交易集合,但是随着时间的推移,总有一个分叉获得越来越多的认可,最后会达到最终一致性。同时一些联盟链例如超级账本则会以牺牲可用性来满足强一致性和分区容错性。
比特币采用的共识算法被称为工作量证明(Proof Of Work,简称POW),简单理解就是一份用来确认你做过一定量的工作的证明。监测工作的整个过程通常是极为低效的,而通过对工作的结果进行认证来证明完成了相应的工作量,则是一种非常高效的方式。比如现实生活中的毕业证、驾驶证等等,也是通过检验结果的方式(通过相关的考试)所取得的证明。在 PoW 的工作方式中,区块链参与者(称为「矿工」)要在区块链中添加一块交易,必须解决某种复杂但是无用的计算问题。本质上,这种做法可确保矿工花费了一些金钱或资源(矿机)完成工作,这表示了它们将不会损害区块链系统,因为对系统的损害也将会导致投资的损失,进而损害他们自身。这种共识算法的问题也显而易见,对于复杂而无用问题的解决唯一存在的意义仅在于完成证明。
在联盟链或者私有链的场景下,为了达到共识可以采取一种更为高效的方法,比如超级账本采用过的实用拜占庭容错算法(Practical Byzantine Fault Tolerance,简称PBFT)。实用拜占庭容错算法来自于分布式计算中的经典问题——拜占庭将军问题。
拜占庭将军问题是Leslie Lamport在10世纪80年代提出的一个假想问题。拜占庭是东罗马帝国的首都,由于当时拜占庭罗马帝国国土辽阔,每支军队的驻地分隔很远,将军们只能靠信使传递消息。发生战争时将军们必须制订统一的行动计划。
然而,这些将军中有叛徒,叛徒希望通过影响统一行动计划的制定与传播,破坏忠诚的将军们一致的行动计划。因此,将军们必须有一个预定的方法协议,使所有忠诚的将军够达成一致。而且少数几个叛徒不能使忠诚的将军做出错误的计划。也就是说,拜占庭将军问题的实质就是要寻找一个方法,使得将军们在一个有版徒的非信任环境中建立对战斗计划的共识。
在分布式系统中,特别是在区块链网络环境中,也和拜占庭将军的环境类似,有运行正常的服务器(类似忠诚的拜占庭将军),还有故障的服务器,有破坏者的服务器(类似叛变的拜占庭将军)。共识算法的核心是在正常的节点间形成对网络状态的共识。
PBFT使用了较少的预选定将军数,因此运行非常高效。它的优点是高交易通量和吞吐量,但是不足之处在于是对于完全的中心化进行了一定的取舍。当有太多的将军,比如超过100位的将军参与其中,会导致在彼此交流信息的时候产生大量的消耗,算法的性能会大幅的下降。
在PBFT算法中,混入了一些心存鬼胎的将军,而诚实的将军为了一致的行动计划付出了很多的代价。如果有一种的神奇的水晶球可以窥探人心,只有诚实可靠的将军才能参与行动计划,那么将军间的协作一定是最为高效的。
Raft算法就是这样的一种算法,对于参与者的数量十分的宽容,但是缺无法容忍心怀鬼胎的将军。为了保证Raft算法能够正常运行,一般而言需要一种带许可的网络,对每一个参与其中的将军发放一本证书,诚实的将军才能获得证书并且参与其中。
不同的算法保证了在不同场景下区块链节点可以达成一致,共识只是一种手段,最终的目的还是要让分布在不同地理位置的节点接收到一致的交易,执行交易后得到一致状态,这就好比一个银行账户在相同银行的不同网点所看到的余额都是一致的。
比特币网络源源不断的接收到来自不同用户的交易,矿工为了获得收益,需要将这些交易打包成区块后添加到区块链网络中最长的链上,然而网络中的所有全节点都是对等平权的,那要如何判断谁可以打包这些交易获得收益呢?
这个时候就需要用到工作量证明(PoW,Proof-of-Work)
的方式决定记账权。工作量证明的思想由来已久,最早是为了防止资源或服务的滥用,或者拒绝服务攻击等场景提出的一种经济对策。一般要求使用服务或者资源之前首先完成一些具有一定难度或者适当工作量的复杂运算,并且这种工作量很容易被证明。
当矿工想要打包这些交易获得稀缺“记账权”的时候必须要消耗一定的资源,以此来提高门槛确保“记账权”确实被想要打包区块的矿工所获得。获得记账权需要消耗大量的资源,而其它节点验证这个过程缺很容易,利用了Pow资源消耗的不对称性。
网络中的任何全节点,都可以试图创建区块,但只有在满足下列条件时创建的区块才会被其他节点认可和接受:
- 区块中包含的交易都是合法的;
- 区块哈希要小于等于一个目标值(争夺记账权);
要满足第一个条件很简单,节点只要将每笔交易都验证一遍,丢弃掉不合法的交易即可。但要满足第二个条件就需要挖矿。
需要特别注意的是比特币“挖矿�”虽然在客观上会创建新的比特币,但是”挖矿“的最终目的并不是为了创建比特币,它只是作为一种激励手段来支撑去中心化的清算机制。只不过通过”挖矿“这样一个�方式将去中心化的安全机制与参与者的利益相统一,也就是后文会提到的激励相容。
比特币挖矿简单来说就是找到一个随机数(Nonce)参与哈希运算Hash(Block Header),使得最后得到的哈希值符合难度要求,用公式表示就是Hash(Block Header)<= target
比特币采用的哈希算法是 SHA-256 ,也就是说最后会产生256位的输出,一共2^256种可能的取值,如果要满足条件无异于用计算机进行一次又一次大海捞针式地搜索,而Block Header也不是单一字段,而是区块头字段拼接而来。
最后得到的哈希值小于target的意思是:把哈希后得到的bytes转换成数字后小于target转换成的数字。
举个例子,直观地感受一下挖矿的难度;
SHA-256计算123的值
a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3
下面这段字符是比特币第1000个区块的哈希(2009年1月产生);
00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09
可以看到前面有8个0,虽然哈希值的生成是随机的,但是生成前面有8个0的值对计算机穷举来说也并不算太难。
再看一下这段字符,是比特币第560000个区块的哈希(2019年1月产生);
0000000000000000002c7b276daf6efb2b6aa68e2ce3be67ef925b3264ae7122
可以看到前面有18个0,要生成满足这个条件的哈希对于普通电脑来说几乎是不可能完成的任务了。
简单来说挖矿难度的高低就是生成区块头的哈希值有多少0。
比特币的供应是通过“挖矿”创造的,类似于黄金开采过程,同时为了�模拟这个过程将“挖矿”产生的比特币设计为逐步递减。大约每四年(或正好每210,000块),产生一个新区块获得的�比特币数量将减少一半。2009年1月开始每个区块50比特币,2012年11月每个区块减半到25比特币,2016年7月再次减少到12.5比特币。基于这个公式,比特币挖矿奖励指数级下降,到2140年左右,所有的比特币(两千一百万)将发行完毕。2140年以后将不再会产生新的比特币。
在比特币系统中出块时间被设置为一个10分钟的常数,但是在实际运行过程中矿工挖出区块的速度并不每次都是10分钟这么精确,矿工挖出区块的世界会随着挖矿难度的变化在10分钟上下浮动,挖矿难度越大,出块时间就越长,为了得到相对平均的出块时间,比特币设计了一种动态调节挖矿难度的算法。
对于比特币来说,其每产生2016个区块调整一次挖矿难度,一个块需要10分钟,2016个块大概是两周的时间,调整挖矿难度的逻辑已经都包含在比特币代码中了,当大多数诚实节点采用这个策略的时候整个网络就会自动遵循这个策略。
挖矿难度的计算公式如下:
diffculty = difficulty_1_target / target
此处的 difficulty_1_target 为一个常数,这是非常大的一个数字( 2^(256-32)−1 )。表示挖矿的初始难度,目标值越小,区块生成难度越大。
2^(256-32)−1 是比特币的初始难度,是前2016个块的难度。
这个难度被存储在比特币的区块头nBits字段中,当有恶意节点为了更快的挖出区块获得收益而篡改这个值时,挖矿产生的区块是无法通过诚实节点校验的,诚实节点不会接收这个由恶意节点挖出的区块,这样恶意节点白白浪费了算力做了无用功。
比特币系统中区块的生产速度是根据之前产生区块速度调整的,出块速度大于10分钟,比特币系统会认为需要降低难度,于是便提高第一个公式中target的值,而target则通过如下公式计算;
target = current_target * ( actual_time / excepted_time )
target是经过公式调整后的难度值,current_target是系统当前难度值,actual_time是实际产生区块的时间,excepted_time是期望出块时间(2016块*10分钟),actual_time有上下限,actual_time最多8周,最小二分之一周。
比特币中nBits
标识了挖矿的难度,也就是说这个区块头进行SHA-256哈希算法后得到的bytes转换成数字后要小于这个难度,SHA-256计算后的结果有256位,如果直接存储需要32个字节,比较占用空间,所以比特币采用了一种压缩算法。
nBits
有4个字节32位,将SHA-256计算得到的值经过如下算法压缩到32位;
- 将数字转换为 256 进制。
- 如果第一位数字大于 127(0x7f),则前面添加 0。
- 压缩结果中的第一位存放该256进制数的位数。
- 后面三个数存放该256进制数的前三位,如果不足三位,从后补零。
举个例子,将十进制1000压缩;
1. 1000转换256进制数,1000 = 3 * 256 + 232 = 3*256^(2-1) + 232*256^(1-1)
2. 3小于127,不需要补0,跳过
3. 从第一部看到1000转换成256位数有2位,压缩结果第一位应该存放2
4. 因为只有两位,所以最后一位补0,得到存放的值为 [2, 3, 232, 0]十进制,转换十六进制 [0x02, 0x03, 0xe8, 0x00] 合并存储到nbits为 0x0203e800
在第一个公式中difficulty_1_target
的值为 2^(256-32)-1,转换成256进制为;
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
第一位大于0x7f,前面补0,变为
00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
其长度等于 28+1=29 (0x1d),且长度超过三位,无需补零,则压缩结果为:0x1d00FFFF,因为压缩存储容量只有才4个字节,前两字节已经被长度和添加的 00 所占用,只剩下2个字节来存储数字,这样后面的26个 FF 值被丢弃。
T=0x00FFFF * 256^(0x1b-3) = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
比特币中的difficulty就是0x1d00FFFF,如果区块中的nBits为0x1d00FFFF则说明这个区块挖矿难度为最小挖矿难度1.
实际上专业的矿池程序会保留被截断的FF:
00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
我们算一下比特币101799号区块的挖矿难度,通过区块链浏览器可以看到101799号区块的nBits为0x1b0404cb
D = 0x00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF / 0x00000000000404CB000000000000000000000000000000000000000000000000 = 16307.669773817162 (pdiFF)
pdiFF
也被称为矿池难度。
为了找到符合条件的值在挖矿的时候需要不断地调整区块头中Nonce的值,但是又会产生一个问题,在比特币中Nonce的值是32位的,如果挖矿难度太大,就算穷尽Nonce的所有可能还是不能算出符合条件的值。
为了解决这个问题,比特币在寻找符合挖矿难度值的时候又增加了“铸币交易”这个变量。
在一个区块产生的时候,会有一个铸币交易(coinbase),也就是矿工为自己铸币,产生新的比特币。
铸币交易没有UTXO输入,只有输出指向自己的比特币地址,当挖矿成功,这个区块被网络接收的时候,新产生的币就转移到这个矿工地址了。
看一下铸币交易包含的字段;
- transaction hash:“交易哈希”字段32个字节全部填充0(因为其没有UTXO输入);
- ouput index:“交易输出索引”字段全部填充0xFF(十进制的255);
- coinbase data:coinbase数据长度最小2字节,最大100字节。除了开始的几个字节外,矿工可以任意使用coinbase的其他部分,随意填充任何数据。以创世块为例,中本聪在coinbase中填入了这样的数据“The Times 03/Jan/ 2009 -Chancellor on brink of second bailout for banks“;
- coinbase data size:coinbase数据大小;
- sequence number:现在未使用,设置为0xffffffff
可以看到铸币交易的coinbase data字段是我们可以控制的,当Nonce不能满足挖矿难度的时候,我们可以通过调整coinbase data字段,通过改变铸币交易的内容产生不同的交易哈希从而影响区块头的默克尔树根的值,提供更多的可能来满足挖矿难度的要求。
通过上面的流程,进行一次可能的挖矿尝试被称为H
。
1 H/s = 每秒可执行一次哈希运算。
1 KH/s = 每秒1,000哈希(一千次)。
1 MH/s = 每秒1,000,000次哈希(百万次)。
1 GH/s = 每秒1,000,000,000次哈希(十亿次)。
1 TH/s = 每秒1,000,000,000,000次哈希(万亿次)。
1 PH/s = 每秒1,000,000,000,000,000次哈希。
1 EH/s = 每秒1,000,000,000,000,000,000次哈希。
矿工在挖矿的时候就会出现很长的时间找不到符合条件的哈希值,如果找不到哈希值不能打包区块就没有收益,显然对矿工十分不友好,一旦挖到就像中彩票一样获得非常丰厚的回报。
为了避免单个矿工挖矿收益的不稳定性,就出现了矿池。矿池集合了大量的矿工,平均挖矿的收益,避免了挖矿收益的不稳定性。
但是现在还有一个问题没有解决,单个矿工挖到目标值以后如果私吞收益,不告诉矿主私自向网络中广播区块怎么办?
矿池有集中托管式的,也有分布式的。
- 集中托管式矿池,矿工可以把挖矿的机器托管给矿池,由矿池统一操作维护,只需要支付一些电费管理费即可,这样就避免了私自广播。
- 分布式矿池,矿工将机器自行管理,通过矿池协议从网络连接矿池即可,这样就会出现私自广播的可能。
回顾一下铸币交易coinbase
,可以看到有output
字段,UTXO模型中币的来源都是上一个交易的output,所以可以把铸币交易的output字段设置为矿池的地址,然后随机生成一些coinbase data
的填充后生成区块头的默克尔树,最后发由矿工去尝试目标值。
通过这样的方式,即使矿工找到满足条件的哈希值,铸币交易的地址也是矿池的地址,私自广播区块没有任何收益。如果调整了铸币交易的地址,就又回到了独立挖矿的场景。
如果要获知全网算力,可以通过出块时间、挖矿难度大致反推出全网算力。
当一个区块产生之后,因为可能会有分叉产生,所以它不是立即被其它区块信任的。区块链网络上的节点总是相信最长链上的区块,当一条交易记录被打包进一个区块之后,就有了一个确认,而这个区块所在的链后面被再加入一个区块,就是第二个确认……如此下去,一个交易有了6个确认,就认为这个交易已经确定了,只有极小的的可能性会被修改。
为什么是6个确认呢?因为每多产生一个确认,区块链产生分叉的可能性就小一点。一般而言当有6个确认的时候,链分叉的可能性就可以忽略不计了,也就说明打包这个交易的区块在最长的链上。或者可以这样理解,每一个确认就是一个挖矿过程,需要做大量的工作,当恶意节点想篡改这笔交易需要在第7个确认产生之前,做完这6个确认的工作,还要再尽快地广播出去。这对于现实情况来说几乎是一件不可能的事情,因此比特币系统在等待时间和安全性之间做了权衡,认为6个确认足以满足需要。
由于比特币的区块平均产生时间是10分钟,所以一个交易要1小时左右才能保证成功(最快)。不过也不是所有的系统都这样认为,有些网站在接受比特币支付时,认为4个确认就可以给客户发货了,区块确认越多则越难被逆转。