区块链:以太坊DApp开发实战
上QQ阅读APP看书,第一时间看更新

1.3 常见的共识算法

目前在区块链中,使节点账本保持一致的共识算法常见的有如下几种:


•PoW,代表者是比特币(BTC),区块链1.0。

•PoS,代表者是以太坊(ETH),以太坊正在从PoW过渡到PoS,区块链2.0。

•DPoS,代表者是柚子(EOS),区块链3.0。

•PBFT拜占庭容错,联盟链中常用。


下面通俗地介绍前3种共识算法的概念及优缺点。

1.3.1 PoW算法

PoW(Proof of Work,工作量证明)的字面意思是谁干的活多,谁的话语权就大,在一定层面上类似于现实生活中“多劳多得”的概念。

以比特币为例,比特币挖矿就是通过计算符合某一个比特币区块头的哈希散列值争夺记账权。这个过程需要通过大量的计算实现,简单理解就是挖矿者进行的计算量越大(工作量大),它尝试解答问题的次数也就变得越多,解出正确答案的概率自然越高,从而就有大概率获得记账权,即该矿工所挖出的区块被串接入主链。

下面对上述一段话所涉及的几个术语做一下解释。

(1)区块头(Header):区块链中区块的头部。比如你有一个饭盒,饭盒的第一层类似动物头部,称之为头部;第一层放着米饭,米饭就是头部装载的东西。

(2)哈希(Hash):数学中的散列函数(数学公式)。

(3)哈希散列值:通过哈希函数得出的值。例如,有加法公式“1+2=3”,那么哈希公式hash(1,2)计算出来的结果即为哈希散列值。

(4)区块头的哈希散列值:饭盒第一层装的是米饭,那么这个值就是区块头装的东西。

(5)记账权,话语权:在大家都参与挖区块的情况下,谁挖出的区块是有效的,谁就有记账权或话语权。

在PoW共识算法下,当很多个节点都在挖矿时,每个节点都有可能挖出一个区块。比特币区块链定义了区块被挖出后,随之要被广播到其他节点中去,然后每个节点根据对应的验证方式对区块进行是否合法的验证操作,被确认合法的区块便会被并入主链中去。

对比现实生活,比如数学竞赛,参赛者相当于矿工,一道题目,谁先做出就公布计算过程和答案,不由裁判判断,由参赛者来一起验证;大家都认可后,宣布该题目结束,解题者及相关信息被记录到纸质册子或数据库,之后继续下一道题。

回到比特币挖矿中,其实就是计算出正确的哈希散列值,一旦计算出来,就生成新区块,并将生成的区块信息以广播的形式告诉其他节点。其他节点收到广播信息后,停下手上的计算工作,开始验证该区块的信息。若信息有效,则当前最新区块被节点承认,各个节点开始挖下一个区块;若信息无效,则各个节点继续自己的计算工作。这里的难题在于哈希散列值的计算随着比特币中难度系数(一个能增加计算难度的变量数字)的增大会越来越困难,导致计算需要耗费大量的电力资源,工作量巨大。

此外,成功挖出有效区块的矿工(节点)将会获得奖励。在不同的区块链体系中,奖励的东西不同,比特币区块链体系中的奖励是比特币。

图1-3是节点使用PoW共识算法共同承认胜出块,并将胜出块串接上链的模型图。

图1-3 用PoW共识算法选出胜出块

PoW共识算法具有下面的优缺点:

(1)优点


•机制设计独特。例如挖矿难度系数自动调整、区块奖励逐步减半等,这些因素都是基于经济学原理的,能吸引和鼓励更多的节点参与挖矿。

•早参与早获利。越早参与的人获得的越多。在初始阶段,会促使加密货币迅速发展,节点网络迅速扩大。

•通过“挖矿”的方式发行币,把代币分散给个人,实现了相对公平。


(2)缺点


•算力是计算机硬件(CPU、GPU等)提供的,要耗费电力,是对能源的直接消耗,与人类追求节能、清洁、环保的理念相悖。

•PoW机制发展到今天,算力的提供已经不再是单纯的CPU了,而是逐步发展到GPU、FPGA乃至ASIC矿机。用户也从个人挖矿发展到大的矿池、矿场,算力集中越来越明显。这与去中心化的思想背道而驰。

•按照目前的挖矿速度,随着难度越来越大,当挖矿的成本高于挖矿收益时,人们挖矿的积极性会降低,造成大量算力减少。

•基于PoW节点网络的安全性令人堪忧。

•大于51%算力的攻击。在“PoW共识机制的51%算力攻击”一节中会详细介绍。


问题解答:

(1)如果遇到同时解出问题的情况怎么办?

确实存在会同时解出的情况,即使我们把区块生成时间的时间戳定义到秒或者毫秒级别,依然会有同时间挖到矿的情况。对于这种情况,PoW共识算法无能为力。具体的解决方法会在“链的分叉”一节中谈到。

(2)为什么是51%算力,而不是50.1%?

这个问题将会在“PoW共识机制的51%算力攻击”一节中进行解答。

1.3.2 PoS算法

PoS(Proof of Stake,股权证明)是由点点币(PPCoin)首先应用的。该算法没有挖矿过程,而是在创世区块内写明股权分配比例,之后通过转让、交易的方式,也就是我们说的IPO(Initial Public Offerings)公开募股方式,逐渐分散到用户钱包地址中去,并通过“利息”的方式新增货币,实现对节点地址的奖励。

PoS的意思是股份制。也就是说,谁的股份多,谁的话语权就大,这和现实生活中股份制公司的股东差不多。但是,在区块链的应用中,我们不可能真实地给链中的节点分配股份,取而代之的是另外一些东西,例如代币,让这些东西来充当股份,再将这些东西分配给链中的各节点。下面我们通过示例来阐述这个概念。

例如,在虚拟货币的应用中,我们可以把持币量的多少看作拥有股权、股份的多少,假设某区块链公链使用了最基础的还没进行变种开发的PoS共识机制,以节点所拥有的XXX代币的数量来衡量这个节点拥有的股份是多少。假设共有3个节点,A、B和C,其中A节点拥有10000个XXX代币,而B、C节点分别有1000个、2000个,那么在这个区块链网络中A节点产生的区块是最有可能被选中的,它的话语权是比较大的。

再例如,假设某条非虚拟货币相关的与实体业结合的公有链,汽车链,我们可以把每一位车主所拥有的车辆数目和他的车价值多少钱来分配股份(比如规定一个公式:车数×车价值=股份的多少)。

可见,在PoS中,股份只是一个衡量话语权的概念。我们可以在自己的PoS应用中进行更加复杂的实现,比如使用多个变量参与到股份值的计算中。

PoS共识算法以拥有某样东西的数量来衡量话语权的多少,只要节点拥有这类东西,哪怕只拥有一个,也是有话语权的,即使这种话语权很小。

在PoS中,块是已经铸造好的。PoW有挖矿的概念,而PoS没有。在BTC比特币公链中,可以挖矿;而在没有使用PoS共识算法的公链节点中,就没有挖矿这一回事。当然,如果将挖矿的概念进行其他拓展,则另当别论。

PoS共识算法具有下面的优缺点:

(1)优点


•缩短了共识达成的时间,链中共识块的速度更快。

•不再需要大量消耗能源挖矿,节能。

•作弊得不偿失。如果一名持有多于50%以上股权的人(节点)作弊,相当于他坑了自己,因为他是拥有股权最多的人,作弊导致的结果往往是拥有股权越多的人损失越多。


(2)缺点


•攻击成本低,只要节点有物品数量,例如代币数量,就能发起脏数据的区块攻击。

•初始的代币分配是通过IPO方式发行的,这就导致“少数人”(通常是开发者)获得了大量成本极低的加密货币,在利益面前,很难保证这些人不会大量抛售。

•拥有代币数量大的节点获得记账权的概率会更大,使得网络共识受少数富裕账户支配,从而失去公正性。


1.3.3 DPoS算法

PoW和PoS虽然都能在一定程度上有效地解决记账行为的一致性共识问题,也各有各的优缺点,但是现有的比特币PoW机制纯粹依赖算力,导致专业从事挖矿的矿工群体似乎已和比特币社区完全分隔,某些矿池的巨大算力俨然成为另一个中心,这与比特币的去中心化思想相冲突。PoS机制虽然考虑到了PoW的不足,但依据IPO的方式发行代币数量,导致少部分账户代币量巨大,权力也很大,有可能支配记账权。DPoS(Delegated Proof of Stake,股份授权证明机制)共识算法的出现就是为了解决PoW和PoS的不足。

DPoS引入了“见证者节点”这个概念。见证者节点可以生成区块。注意,这里有权限生成区块的是见证者节点,而不是持股节点。

下面我们主要以EOS区块链为例介绍DPoS算法。

持有EOS代币的节点为持股节点,但不一定是见证者节点。见证者节点由持股节点投票选举产生。DPoS的选举方式如下:每一个持有股份的节点都可以投票选举见证者节点,得到总同意票数中的前N位候选者可以当选为见证者节点。这个N值需满足:至少一半的参与投票者相信N已经充分地去中心化(至少有一半参与投票的持股节点数认为,当达到了N位见证者的时候,这条区块链已经充分地去中心化了),且最好是奇数。请注意,最好是奇数的原因会在分叉一节中进行说明。

见证者节点的候选名单每个维护周期更新一次,见证者节点们被选出之后,会进行随机排列,每个见证者节点按顺序有一定的权限时间生成区块,若见证人在给定的时间片不能生成区块,区块生成权限将交给下一个时间片对应的见证人。DPoS的这种设计使得区块的生成更为快速,也更加节能。这里“一定的权限时间”不受算法硬性限制。此外,见证者节点的排序是根据一定算法随机进行的。

DPoS共识算法具有下面的优缺点:

(1)优点


•能耗更低。DPoS机制将节点数量进一步减少到N个,在保证网络安全的前提下,整个网络的能耗进一步降低,网络运行成本最低。

•更加去中心化,选举的N值必须充分体现中心化。

•避免了PoS的少部分账户代币量巨大导致权力太大的问题,话语权在被选举出的N个节点中。

•更快的确认速度,由见证者节点进行确认,而不是所有的持股节点。


(2)缺点


•投票的积极性并不高。绝大多数持股节点未参与投票。因为投票需要时间、精力等。

•选举固定数量的见证人作为记账候选人有可能不适合完全去中心化的场景,在网络节点很少的场景,选举的见证人的代表性也不强。

•对于坏节点的处理存在诸多困难。社区选举不能及时有效地阻止一些破坏节点的出现,给节点网络造成安全隐患。


图1-4所示是DPoS共识算法选举的大致模型图。

图1-4 用DPoS共识算法选出胜出块

目前,EOS的超级节点有21个,也就是说N=21,但是拥有EOS代币能投票的节点却有很多,那么为什么N这么小呢?原因是这样的:虽然N代表的是节点们认同的一个能够代表已经足够去中心化的值,但是N可以取23,也可以取25,或者更大,这些数看起来都足够去中心化了,事实上在EOS公链的发展过程中,N值渐渐地趋向于既要足够去中心化又要让性能跟得上,即处于中间值,以达到平衡性能的同时又满足去中心化。

提示

EOS的投票事实上还是一种抵押。投票的EOS会被抵押成资源,例如CPU和网络资源,但是抵押的EOS也可以被换回来。把抵押的资源换回EOS通常需要3天时间。

1.3.4 共识算法的编码尝试

本节尝试使用伪代码来实现3种共识算法,之所以使用伪代码是为了使读者更容易理解。

1. 实现PoW共识算法

首先,区块链中的各个节点会互相通信以广播新生成的区块。这里既可以用“生成”一词来描述,也可以使用“挖出”一词来描述,表达的意思是一样的,都是指区块的产生。

此时,我们需要使用一个候选区块数组来保存每一个节点广播过来的和自己当前节点生成的区块对象,以及一个全局的区块数组来表示当前公链的区块。区块数组的定义如下:

假设Block结构体内的数据类型如下所示:

然后我们需要一个难度系数的变量,例如difficulty,用来控制PoW算法的难度。这个数不一定越大就代表越难,只需要体现出PoW算法所描述的工作量难度即可。假设它是整型数,数值越大,计算难度就越大,那么此时difficulty系数会处于被随时调节的状态中。在区块链的设计中,例如比特币BTC的难度系数就有其动态调节的算法。

这里,我们假设难度系数difficulty=1。

有了难度系数后,还需要一个专门用来根据difficulty校验区块哈希值的函数。我们现在需要假设一种难度的验证算法,假设用哈希值前缀0(值0x后的0)的个数来和difficulty做比较,如果哈希值包含这些前缀0,那么校验通过。请注意,这是一种很简单的验证算法,且个数很有限,而在比特币公链中,则要复杂得多。

现在假设节点启动了一个子协程,一个用来生成区块的方法,并添加到候选区块数组中去,等待校验。下面的这个方法(函数)是用来生成新区块的:

假设节点启动了一个子协程且在不断地计算候选区块数组中区块的哈希值,所计算出的哈希值满足难度系数difficulty的检验。

在各个节点确认的过程中,如果达到了所规定的节点数量,那么我们就判断该区块胜出,最终被公链接纳。

最后解答一下伪代码中留下的疑问——为什么还要进行一次校验才广播块呢?因为难度系数difficulty是动态改变的,且候选块数组中的difficulty不一定就是我们当前的节点所生产的,即使是当前节点生产的,也有可能在生成的时候难度系数已经被出块了,所以在最后广播的时候还需要根据最新的difficulty难度系数再做一次校验。

2. 实现PoS共识算法

相对于PoW,由于PoS共识算法没有“挖矿”的概念,且它不是靠计算工作量来进行共识的,体现在代码上也会是另外一种情形。

首先我们依然需要定义一个候选区块数组来保存每一个节点广播过来的和自己当前节点生成的区块对象:

candidateBlocks []Blocks  //候选区块数组

每个区块结构体有一个变量,用来记录生成这个区块的节点地址。这个变量在PoW的伪代码实现中并没发挥作用,但是在PoS中却很重要。同样地,和上述PoW一样,我们定义如下的区块结构体:

其中,NodeAddress变量用来记录区块的节点地址。

其次,需要有一个子协程,专门负责遍历候选区块数组,并根据区块的节点地址nodeAddress获取节点所拥有的代币数量,然后分配股权。

接下来,从stakeRecord中选出一个竞选胜利者。这个概率和上面的coinNum有关,coinNum越大就越有机会。为什么呢?因为它的统计方式是用coinNum作为循环界限,然后对应添加coinNum次的nodeAddress,所以coinNum越大,这个nodeAddress就被添加得越多,后面节点能被选上出块的概率也就越大。

这里还要解答一个疑点,为什么已经包含了的就不再重复添加,因为当前的候选区块数组candidateBlocks中可能含有同一个节点中的多个区块,而每一个节点中的股权只需要统计一次,即coinNum只需要循环一次即可。如果是多次循环,就会造成不公平,因为会造成多次添加。

在股权被分配好后,接下来准备选出节点胜利者。选择的方式也是使用算法,在这个例子中我们依然采取最简单的随机数的形式进行选择。注意,切勿被这样的方式限制了思维,这个选择算法是可以自定义的,因而可以是更加复杂的算法。

index := randInt()  // 得出一个整型随机数
winner := stakeRecord[index]  // 取出胜利者节点的地址

在最后的步骤中,就能根据这个winner去所有候选区块中选出节点地址和它一样的区块,这个区块就是胜利区块,将会被广播出去。

以上是一个很简单的PoS算法机制的代码实现,仅单纯地根据持币数量来进行股权分配。事实上,事情往往是比较复杂的。设想股权的分配不仅只和代币数量有关,例如以太坊设想的PoS共识算法的实现中加入了币龄,情况又会如何呢?这时在候选成功后,以太坊会扣除币龄。作为开发者应当理解PoS的精髓——其算法的实现往往会衍生出各种各样的变种,只有了解了这一点,才能在开发自己的公有链时随心而行。

3. 实现DPoS共识算法

DPoS的伪代码实现可以理解为PoS的升级版,之前例子中相同的数据结构体的定义这里不再重复。

首先定义好见证者节点的结构体:

然后我们用一个由各个见证者节点组成的数组代表这一批见证者节点,往后的随机排序操作也将会在这个数组中进行。

var WitnessList []WitnessNode  // 见证者节点

现在我们需要准备一个专门用来对WitnessList进行随机排序的方法,这个方法须依赖某种算法对WitnessList进行排序,具体算法可以自定义,但要根据不同的业务需求而定。

下面我们依然以一个最简单的随机数排序为例。

上面NeedRestVotes()的作用是判断是否需要重新投票选出见证者节点,对应DPoS算法描述中的每过一个周期就开始重新排名,在这个阶段还需要进行节点的剔除,例如剔除一些坏节点。

这里以检查坏节点为例,因为坏节点的检查时刻在进行,所以我们可以用一个子协程(Go语言中,协程是一种轻量级线程,为了更加贴切,下面的Go代码中统称线程为协程)来专门检查坏节点。

同时,还要不断地检测是否有新的见证者节点被投票选出,是的话,就要添加这个节点的信息进入到见证者节点数组中。同时要对见证者节点总数的数量进行N值限制。

最后我们使用出块函数从WitnessList见证者列表中从上到下逐个找出出块节点,进行出块,并检测当前轮到的节点是否出块超时,超时就轮到下一个,以此类推,对应DPoS共识算法的伪代码如下:

在广播块出去后,其他见证者接收到了广播,会对这个区块进行签名见证,当达到了某个我们所设定的认为见证已经足够了的值时,那么这个区块就被确认了。

从上述伪代码发现,传统的DPoS算法直接应用的时候存在如下问题:每个见证者节点都是循环着使用别的节点信息去生成块,而不是使用自己节点的信息。假设见证者节点A、B、C在一轮的出块顺序中,节点C排在第一位,且在节点A和B中使用节点C所生成的区块都没有出差错,那么节点C就会在节点A和B中都生成一次,由于时间戳不一样,导致该块的哈希不确定,但最终只能有一个块被选上,这样就导致了算力浪费。当然,也有可能不止节点A和B,还可能有更多的节点都使用节点C生成了区块,那么结果就是更多的认证者节点生成了一个节点的块,去广播。

要解决上述问题,可以使用EOS的做法:EOS通过见证者节点信息注册,使得每个节点都知道所有见证者节点的信息,同时被注册的节点都必须是满足投票条件的。

当每个见证节点都有了所有见证者节点的信息后,在每一次的最终块出现后,都会使用特定的算法对节点列表进行排序。如此,当需要出块的时候,节点会根据区块链中最后一个区块的时间来参与到某些计算中,得出当前应该出块的见证者节点在列表中的下标,然后判断这个节点是不是自己。不是的话,就会让自己延迟(delay)一定的时间,然后重复上面的步骤,这个延迟的时间就是DPoS中的出块超时,然后会自动轮到下一个见证者节点。如果是自己就出块,出块后就广播出去,等到2/3的见证者节点都签名确认了,那么这个块就是最终有效的。所以,EOS中的DPoS并不是传统示范代码中的那样,一个节点循环着生成含有别的节点的信息的块。

EOS的要点是,每个见证者节点的自身代码对所有见证者节点的排序是不一样的,各节点存在同时出块的可能,但其提供了2/3见证者节点都签名确认这一环节,即谁最快被2/3的见证者节点确认,谁才是最终有效的,解决了多个节点同时生成一个节点块的问题。