
本文介绍常见的POS及DPOS共识算法，包括本次新实现的chain33的DPOS共识插件。  
本文循序渐进，通过对常见的Pos、DPos算法的介绍，使读者了解Pos、Dpos共识机制，并逐步演进到Eos的Dpos共识、Tendmint的Pos共识，最后介绍了Chain33的Dpos共识算法实现。  

# 一、POS共识机制 

## 1 POS共识机制介绍  

权益证明（ Proof of Stake，PoS） ，最早在 2013 年被提出，最早在 Peercoin 系统中被实现，类似现实生活中的股东机制，拥有股份越多的人越容易获取记账权（ 同时越倾向于维护网络的正常工作） 。  

典型的过程是通过保证金（ 代币、资产、名声等具备价值属性的物品即可） 来对赌一个合法的块成为新的区块，收益为抵押资本的利息和交易服务费。提供证明的保证金（ 例如通过转账货币记录） 越多，则获得记账权的概率就越大。合法记账者可以获得收益。  

PoS 试图解决在 PoW 中大量资源被浪费的缺点，受到了广泛关注。恶意参与者将存在保证金被罚没的风险，即损失经济利益。一般的，对于 PoS 来说，需要掌握超过全网 1/3 的资源，才有可能左右最终的结果。这个也很容易理解，三个人投票，前两人分别支持一方，这时候，第三方的投票将决定最终结果。  

在股权证明模式下， 有一个名词叫币龄， 每个币每天产生1币龄， 例如，你持有100个币， 总共持有了30天， 那么， 此时你的币龄就为3000， 这个时候， 如果你发现了一个PoS区块， 你的币龄就会被清空为0。 你每被清空365币龄， 你将会从区块中获得0.05个币的利息（ 可以理解为年利率5%） ， 那么在这个案例中， 利息=3000×5%/365=0.41个币。  

## 2 POS共识机制的优点  

1.省资源：  
不需要挖矿，不需要大量耗费电力和能源。  
2.更加去中心化：  
相对于比特币等PoW类型的加密货币，更加去中心化，相比PoW算法的51%算力攻击，PoS需要购买51%的货币，成本更高，没有攻击意义。  
3.避免通货紧缩：  
PoS机制的加密货币按一定的年利率新增货币，可以有效避免紧缩出现，保持基本稳定。  

## 3 POS共识机制的缺点  

1.开发者作恶：  
纯PoS机制的加密货币，只能通过IPO的方式发行，这就导致“少数人”（通常是开发者）获得大量成本极低的加密货币，在利益面前，很难保证他们不会大量抛售。  

2.信用度低：   
PoS机制的加密货币，信用基础不够牢固。 为解决这个问题，很多采用PoW+PoS的双重机制，通过PoW挖矿发行加密货币，使用PoS维护网络稳定。或者采用DPoS机制，通过社区选举的方式，增强信任。  

# 二、传统DPOS共识机制 

## 1 DPOS共识机制介绍  

EOS 项目刚刚发布的时候的共识机制是 DPoS(Deligated Proof of Stake，委托股权证明)，类似于 Bitshares 和 Steem，这种共识机制采用随机的见证人出块顺序，出块速度为 3 秒，交易不可逆需要45秒。  

为什么需要 45 秒呢？因为 DPoS 下，见证人生产一个新区块，才表示他对之前的整条区块链进行了确认，表明这个见证人认可目前的整条链。而一个交易要达到不可逆状态，需要 2/3 以上的见证人确认，在 EOS 里就是 14 个见证人。我们假设一个交易被包含在 1000 号区块中，需要其他13个见证人轮流出块至 1013 号区块，这样才能“收集”到14个见证人对此交易的确认（包括生产1000区块的见证人）。2/3 以上的见证人确认的交易，就是不可逆的交易了，这就是 45 秒确认时间的由来。  

DPoS机制类似于董事会。董事会成员数量有限, 由大家选举产生。被选中的董事会成员可以行使权利。  

以比特股(BitShare）为例:  

DPoS是由被社区选举的可信帐户（受托人，得票数排行前101位）来创建区块。为了成为正式受托人，用户要去社区拉票，获得足够多用户的信任。用户根据自己持有的加密货币数量占总量的百分比来投票，选举代表（受托人）代他们做决策。  

这101个受托人可以理解为101个矿池，而这101个矿池彼此的权利是完全相等的。那些握着加密货币的用户可以随时通过投票更换这些代表（矿池），如果他们提供的算力不稳定，计算机宕机、或者试图利用手中的权力作恶，他们将会立刻被选民们通过投票踢出系统，而后备代表可以随时顶上去。  


## 2 DPOS算法描述  

```go
for round i //分成很多个round，round无限持续
   dlist_i = get N delegates sort by votes //根据投票结果选出得票率最高的N个受托人
   dlist_i = shuffle(dlist_i) //随机改变顺序
   loop //round完了，退出循环
       slot = global_time_offset / block_interval
       pos = slot % N
       if dlist_i[pos] exists in this node //delegate在这个节点
           generateBlock(keypair of dlist_i[pos]) //产生block
       else
           skip
```

在每一轮循环里，系统会重新统计得票排名。在选出最高的N个受托人里，系统采用先打乱顺序，然后受托人依此生产区块。一轮区块生产完毕后进入下一个周期。  

## 3 DPOS算法优缺点  

### 优点：  

1.能耗更低。  
2.更加去中心化。  
3.更快的确认速度。  

### 缺点：  

1.持股人投票的积极性并不高。  
2.对于坏节点的处理存在诸多困难。社区选举不能及时有效的阻止一些破坏节点的出现，给网络造成安全隐患。  


# 三、初级BFT-DPOS共识机制 

每个见证人出块时依然全网广播，其他见证人收到新区块后，立即对此区块进行验证，并将验证签名完成的区块立即返回出块见证人，不需等待其他见证人自己出块时再确认。从当前的出块见证人看来，他生产了一个区块，并全网广播，然后陆续收到了其他见证人对此区块的确认，在收到 2/3 见证人确认的瞬间，区块（包括其中的交易）就不可逆了。  

交易确认时间大大缩短，从 45 秒缩短至 3 秒左右（主要为等待生产区块的时间）。  

![初级BFT-DPOS共识示意图](./bft-dpos.png)  

# 四、EOS的BFT-DPOS共识机制 

![EOS的BFT-DPOS共识示意图](./eos-bft-dpos.png)  
EOS 最终采用了BFT-DPoS共识机制从而达到了500毫秒的出块间隔。  
该机制的具体过程是：  
EOS的持有者通过投票系统对各个超级节点竞选者进行投票，选出21个节点为超级节点。  
这21个超级节点以自身的网络资源状况商议出一个出块权拥有顺序。  
在每个超级节点拥有出块权时，以间隔为500毫秒(500毫秒是EOS团队通过大量实验测试得出的当前网络状态下可达到的最小的稳定状态下的出块间隔)连续产生12个新区块，然后切换到下一个超级节点连续产生之后的12个区块。  
该方式可以保证一个超级节点可以连续以500毫秒的间隔产生区块，因为在同一超级节点产生新区块时不受当前网络状况的影响.  

由于网络的延迟很难使得其他节点对已经产生的区块进行确认，使其成为不可逆区块。因此EOS引入了 BFT协议，当超级节点A产生第一个新区块后，A将该区块进行签名并广播给其他超级节点，其他超级节点对该区块进行验证后对其进行签名并返回给A节点，当A节点收到来自14个不同节点签名的区块后，该区块就成为不可逆区块串联到之前的区块链中(以500毫秒产生新区块的过程和对区块进行BFT协议共识的过程在超级节点中是同时进行的，即确认过程不影响超级节点产生新的区块)。  

![EOS生产区块示意图](./eos-pb.png)  

区块生成和确认以pipeline的方式进行，是同时进行的，加快了出块速度。  
EOS团队通过大量实验测试，在当前的网络状况下，一个超级节点广播一个新区块并确认的过程可在1秒的时间内完成。因此，每个新区块的产生到成为不可逆区块最多需要1.5秒的时间，这就使得跨链通信的时延大大缩小。  

![EOS固定出块顺序示意图](./eos-pb-order.png)  

上述过程虽然可以保证同一超级节点产生新区块时可以达到500毫秒的间隔，但当切换超级节点产生区块时，由于网络延迟使得上一节点产生的最后几个新区块有可能被该超级节点忽略。为解决此问题，EOS选用了确定顺序的超级节点轮流出块，比如以纽约(美国东海岸)、芝加哥(美国中部)、洛杉矶(美国西海岸)、日本东京、中国上海这样的顺序，该顺序使得上一节点产生的最后区块传播到下一节点时有最小的延迟，从而避免下一个超级节点忽略上一节点产生的区块。如果是随机定义出块权的超级节点，那么在现有的网络条件下，出块间隔只有控制在3 秒时才可保证下一节点较大概率上不会忽略上一节点产生的区块。  

使用上述BFT-DPoS协议就可以使得EOS的出块间隔从原来的3秒降低到500毫秒，这也使得跨链通信的时延大大缩短，单位时间内可确认的交易数量大大提升。  

# 五、星云链的DPOS共识机制 

Nebulas 的 DPOS 共识机制中使用 21 个代理节点,这些节点预先定义在创世纪块配置中(conf/default/genesis.conf)。  
这些代理节点在代码逻辑中又被称为“朝代”(Dynasty)。  
每隔 15 秒出一个区块,21 个节点按时间槽顺序依次出块。  
当前出生产区块的节点称为“Proposer”,也称为“miner”。  
区块生产节点会获取区块奖励以及区块中的所有交易的油费（Gas）。  

blockLoop 函数的逻辑非常简单,每一秒钟,查看是否可以生成区块(mintBlock函数)。  
mintBlock 函数用来生产区块,其调用关系如下图所示:  
![星云链出块时序示意图](./nb-seq.png)  

TailBlock 函数获取最近的区块(区块链尾部的区块)。  
checkDeadline 函数查看最近区块的时间和当前的时间比较,如果时间差距太大,停止生产区块。Nebulas 定义了最小的生产区块时间为 2.25 秒,最大的生产区块时间为 5.25秒。checkDeadline 函数查看当前的时间和标准出块的时间进行比较,如果不在生产区块合理的时间内,停止生成区块。  
checkProposer 函数查看当前区块的正确的 Proposer,如果不是自己,停止生成区块。  
generateRandomSeed 函数使用 VRF(可验证随机函数)生成随机数,并更新区块头中的随机数字段。  
SignBlock 函数对区块头进行签名,并更新区块头的签名字段。  
一个区块中收集的交易的多少,取决于时间。因为每 15 秒生成一个区块,生成区块的“标准”时间是确定的。一个节点生产区块的时间离“标准”时间不能偏差太大,并
且收集交易的时间不能超过 5.25 秒。  
当一个区块经过2/3节点见证后，才变为不可逆区块。耗时:2/3 * 21 * 15 = 210s。  

# 六、Tendermint的共识机制 

Tendermint的共识是一种类Pos共识，指定固定的几个validator，每个validator有不同的权重值，根据节点的权重值选择节点进行出块。每一个块的生成都经过投票共识。  
其共识状态机运行如下：  

![Tendermint共识状态机示意图](./tendermint-state.png)  


## 1 正常流程  
图中外圈的蓝色线为正常流程：  
(1) Tendermint算法先随机选出一些节点作为Validators，然后选择其中一个validator作为proposer节点。  
(2) Proposer节点开始监听并收集全网的所有交易，几分钟后，组装一个新块，并向全网广播，这个就是 proposal block。  
(3) 全网所有validator节点收到这个 proposal block后，开始读取这个block里的所有交易，一一进行验证  
    a. 如果没有问题，就发出一条 pre-vote 投票消息，表示同意这个block，投一个肯定票，  
    b. 如果发现block里有非法交易，则投一个反对票，这些投票消息会被广播到所有validator节点，所以每个validator节点既会发出一个投票消息，又会收集别人的投票消息.  
(4) 当发现收集到的同意投票数量超过 2/3时，就发出一个pre-commit 投票信息，这是第二阶段的投票了，这时每个节点也在监听并收集pre-commit的投票消息。  
(5) 当一个validator节点收集到的 pre-commit 同意票数超过2/3时，说明这个block 是得到了大多数人同意的，可以放心把这个block写入本地的区块链，追加到末尾，即完成commit。同时区块高度增一，proposer节点索引也增1，进入下一轮(round), 开始提议新区块。  

## 2 超时处理  

大部分情况下，一个块只需要一轮就可以完成确认，达成共识，但是有时候网络状态不好，经常发生超时，这时候就会进入图中内圈红色流程了。  
由于每一轮(round)只有一个proposer有权力出块，如果这个proposer挂了或者网络不好超时了，怎么办？  
(1) 在proposer节点propose阶段，所有validator节点会启动一个定时器，设置超时时间为T（这个T的值是根据网络情况动态计算出来的）。  
(2) 如果在这个时间内还没有收到proposer节点发来的新块，就认为这个proposer节点挂了，所有validator节点不会继续等下去，会立刻在本机生成一个特殊结构的空块，假装这个空块是从Proposer节点那里收到的，这样，无论如何，在时间T内，都会收到一个proposal区块，要么是一个正常块要么是一个空块。  
(3) 然后接着对这个块进行pre-vote投票和pre-commit投票。  
     如果proposer挂了，绝大部分validator看到的都是一个空块，因此空块会获得多数投票，进入commit阶段。  
(4) commit空块的时候，不会真的往区块链写入一个空块，而是什么都不写，区块高度不自增，保持不变，这样相当于什么也没有干，这一轮(round)是在空转。  
这一轮转完了，下一轮开始的时候会换下一个validator当proposer，这样当前那个挂掉的proposer，就不会卡主整个网络。   

## 3 锁机制  

在上面的图中，其实还隐含有一个锁机制，没有在图中表现出来。举个例子，有四个validator 节点，A,B,C,D, 在某个R轮，  
（1）在propose阶段，proposer节点广播出了新块blockX  
（2）A的超时时间内没有收到这个新块，向外广播pre-vote nil，B,C,D都收到了，向外广播pre-vote投给blockX  
（3）现在四个节点进入了pre-commit阶段，A处于红色内圈，B,C,D处于蓝色外圈  
（4）假设A由于自身网络不好，又没有在规定时间内收到超过2/3个对blockX的投票，于是只能发出 pre-commit nil投票消息投给空块  
（5）D收到了B和C的pre-vote消息，加上自己的，就超过了2/3了，于是D在本机区块链里commit了blockX  
（6）B和C网络出现问题，收不到D在pre-commit消息，这是B和C只能看到2票投给了blockX，一票投给了空块，全部不足2/3，于是B和C都只能 commit空块，高度不变，进人R+1轮，A也只能看到2票投给了blockX，一票投给了空块，也只能commit空块，高度不变，进人R+1轮  
（7）在R+1轮，由于新换了一个proposer, 提议了新的区块blockY，A,B,C 三个可能会在达成共识，提交blockY，于是在同样的高度，就有blockX和blockY两个块，产生了分叉。  

Tendermint加上了锁的机制，具体就是，在第7步，即使proposer出了新块blockY，A,B,C只能被锁定在第6步他们的pre-commit块上，即A在第6步投给了空块，那么在第R+1轮，只能继续投给空块，B在第6步投给了blockX，那么在新一轮，永远只能投给blockX，C也是类似。这样在R+1轮，就会有1票投给空块，两票投给blockX，最终达成共识blockX，A,B,C三人都会commit blockX，与D一致，没有产生冲突。  

 
# 七、Chain33 DPOS共识机制 

## 1 概念定义 

![Chain33 DPOS共识概念示意图](./chain33-dpos.png)  

一个Cycle周期中，所有委托节点按时间槽（Period）顺序轮一遍。  
一个Cycle由多个Period构成，每个Period对应一个委托节点负责持续出块的时间段(时间槽)。  
一个Period分为多个出块周期，每个出块周期由负责出块的节点生成一个区块，并通过P2P广播到其他节点。  
Period切换时，要对已生成区块确认，并进行节点重新选举，推举出2/3多数公认的下一个Period继续负责下一个Period的出块的节点。  
选举投票及切换通知确认，可以保存下来，作为证据，因为每张投票都有具体节点的签名。//该逻辑暂未实现。  
对于各节点时间不同步，投票共识未达成等异常情况，可能会有部分节点、甚至全部节点停止生成区块的情况，直到错误被修正；通常情况下，各受托节点或者多数受托节点保持系统时间和时间服务器同步，可保证2/3多数的共识可以达成，系统可以正常工作。  



## 2 状态机  

![Chain33 DPOS共识状态机示意图](./chain33-dpos-state.png)  

### （1）. Init状态处理逻辑：  
1.3s周期检查链接状态，超过2/3节点链接正常，则发起Vote，包含本节点认为接下来的时间段应该哪一个节点出块、时间范围、出块起始高度等信息。  
2.如果收到超时消息，则重新检查链接状态。  
3.如果收到投票消息，则保存，到Voting状态统计选票。  
4.如果收到其他消息，则做丢弃处理。  
5.如果区块未同步，则进行区块同步。  

### （2）. Voting状态处理逻辑：  
1.3s周期检查投票状态，未超过2/3，则切换状态到Init状态。  
2.如果收到超时消息，则认为是超时未达成共识，切换到Init状态，重新开始新一轮投票。  
3.如果收到投票消息，则判断是否有节点票数超过2/3，如果超过，则达成共识，切换状态到Voted状态，对于出块节点设定出块定时器；对于非出块节点设定周期超时定时器，并同步出块节点生成的区块。如果没有节点票数超过2/3，且未收到所有投票，则继续等待投票直到超时。  

### （3）. Voted状态处理逻辑：  
1.对于出块节点：  
1s定时器超时，如果还处在Period周期内，一个出块循环中尚未出块则出块，已经出块则空转等待下一个出块周期的到来；如果Period周期结束，则notify周边节点本周期结束，包括结束height，time等信息。
状态切换到Init状态，开始新一个节点的选举。  
2.对于非出块节点：  
周期定时器超时，则切换状态到WaitNotify状态，等待出块节点发来的notify消息，如果高度不同步，需要进行区块同步。  
3.如果收到出块节点发来的notify消息，直接切换状态到WaitNotify状态，检查区块高度，如果高度不同步，需要进行区块同步。  
4.如果收到投票消息，则返回本周期的本节点投票，以帮助新启动节点完成投票及状态切换。  

### （4）. WaitNotify状态处理逻辑：
1.2s周期检查是否收到Notify。  
如果超时未收到，则进入Init状态，参与投票。  
如果超时前收到，则记录相关结果，如果区块高度未同步则进行区块同步，并进入Init状态，参与新一轮投票。  

## 3 DPos共识相关配置  
当前实现，对于受托节点采用了配置指定的方式，各受托节点的公钥配置在genesis.json文件中，各个受托节点使用相同的genesis.json文件；  
各受托节点自身的私钥、地址信息保存在各自的priv_validator.json文件中。  

这种配置方式借鉴了tendermint的配置方式，后续可以进一步修改为投票选举受托节点的方式，通过智能合约来对受托节点进行动态管理。  

1.genesis.json:  

```json
{
    "genesis_time":"0001-01-01T00:00:00Z",
    "chain_id":"test-chain-Ep9EcD",
    "validators":[
        {
            "pub_key":{
                "type":"ed25519",
                "data":"220ACBE680DF2473A0CB48987A00FCC1812F106A7390BE6B8E2D31122C992A19"
            },
            "name":""
        }
    ],
    "app_hash":""
}
```

2.priv_validator.json  

```json
{
    "address":"02A13174B92727C4902DB099E51A3339F48BD45E",
    "pub_key":{
        "type":"ed25519",
        "data":"220ACBE680DF2473A0CB48987A00FCC1812F106A7390BE6B8E2D31122C992A19"
        },
     "priv_key":{
        "type":"ed25519",
        "data":"B3DC4C0725884EBB7264B92F1D8D37584A64ADE1799D997EC64B4FE3973E08DE220ACBE680DF2473A0CB48987A00FCC1812F106A7390BE6B8E2D31122C992A19"
      }
}
```
3.chain33.test.toml  

```json
[consensus.sub.dpos]
genesis="14KEKbYtKKQm4wMthSK9J4La4nAiidGozt"
genesisBlockTime=1514533394
timeoutCheckConnections=1000
timeoutVoting=3000
timeoutWaitNotify=2000
createEmptyBlocks=false
createEmptyBlocksInterval=0
validatorNodes=["127.0.0.1:46656"]
blockInterval=3
continueBlockNum=12
isValidator=true
```

## 4 代码配合时序  

### 4.1 Chain33 DPOS 代码时序:  

![Chain33 DPOS共识时序示意图](./chain33-dpos-seq.png)  

Client会启动EventLoop和StartConsensus两个go routine。  
StartConsensus中做如下处理：  
1.做区块同步。  
2.判断本节点是否为受托节点：  
　如果不是，则不参与出块共识，只是被动同步区块。  
　如果是，则创建共识模块，启动Node模块，进行共识通信和共识状态机运行。  
3.Node模块做如下处理：  
　启动listenRoutine，进行共识通信的监听，接受外部连接，并通过收发go routine进行通信。  
　主动连接其他受托节点。  
　启动BroadcastRoutine，支持消息广播到其他参与共识的受托节点。  
　启动共识模块go routine。  
4.共识模块做如下处理：  
　启动receiveRoutine，对接收到的共识消息进行处理，主要通过消息驱动dposState状态机的运行，在不同的状态中，做不同的事情。  
　timeoutTicker，作为定时器模块，会启动timeoutRoutine，在状态机中会设定不同的超时定时器到timeoutTicker模块，timeoutRoutine会把超时事件通过channel通知到receiveRoutine模块，最终驱动状态机的处理和状态变化。  
  dosState作为状态机核心逻辑，受各种消息（投票、投票响应、切换通知等）、事件（各种超时）的驱动，进行状态的维护和跃迁，保证各节点处理的一致。  


参考附录:  

### 4.2 Solo共识代码时序:  

![Solo共识时序示意图](./solo-seq.png)  

### 4.3 Tendermint共识代码时序:  
![Tendermint共识时序示意图](./tendermint-seq.png)  



