首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

从NEO源码分析看DBFT共识协议

0x00 概论

不同于比特币使用的工作量证明(PoW)来实现共识,NEO提出了DBFT共识算法。DBFT改良自股权证明算法(PoS),我没有具体分析过PoS的源码,所以暂时还不是很懂具体哪里做了改动,有兴趣的同学可以看下NEO的官方文档。本文主要内容集中在对共识协议源码的分析,此外还会有对于一些理论的讲解。关于NEO网络通信部分源码分析我还另外写了一篇博客,所以本文中所有涉及到通信的内容我就不再赘述,有兴趣的同学可以去看我的另一篇博客。

0x01 获取议员名单

NEO的共识协议类似于西方国家的议会,每次区块的生成都在议长主持下由议会成员共同协商生成新的区块。NEO网络节点分为两种,一种为共识节点,另一种为普通节点。普通节点是不参与NEO新区快生成的,对应于普通人,共识节点参与共识的过程并且都有机会成为议长主持新区块的生成,对应于议员。 看官方文档似乎所有的共识节点都可以到NEO的服务器注册为议员,但是貌似成为议员还是有条件的,据社区大佬说,你账户里至少也要由个把亿才能成为议员,所以像我这样的穷逼是没希望了。但是在分析源码的时候我发现似乎并不是这样。源码中在每轮共识开始的时候调用ConsensusContext.cs中的Reset方法,在 重置共识的时候会调用Blockchain.Default.GetValidators()来获取议员列表,跟进去这个GetValidators()源码:

源码位置:neo/Core/BlockChain.cs

发现这里是调用了内部的GetValidators(IEnumerable others)方法,但是这里有点意思,这里传过去的参数,居然是个空的。再看这个内部的GetValidators方法:

源码位置:neo/Core/BlockChain.cs

我把第一个foreach循环中的代码都删掉了,因为明显传进来的others参数为0,所以循环体里的代码根本不会有执行的机会。这个方法的返回值是result,它值的数据有两个来源。第一个是pubkeys,pubkeys来自于本地缓存中的议员信息,这个信息是在区块链同步的时候保存的,也就是说只要共识节点开始接入区块链网络进行区块同步,就会获取到议员信息。而如果没有缓存议员信息或者缓存的议员信息丢失,就会使用内置的默认议员列表进行共识,之后再在共识的过程中缓存议员信息。 上面说到获取议员信息有两种途径,第二种的使用内置默认议员列表是直接将配置文件protocol.json中的数据读取到StandbyValidators字段中。接下来主要介绍第一种途径。 GetValidators方法的第二行调用了GetStates,并且传入类的类型是ValidatorState,这个方法位于LevelDBBlockChain.cs文件中,完整代码如下:

源码位置:neo/Implementations/BlockChains/LevelDB/LevelDBBlockChain.cs

可以看到这里是直接从leveldb的数据库中读取的议员数据。也就是说在读取数据之前,应该要创建/打开数据库才行,这部分的操作可以参考neo-cli项目,这个项目就在MainService类的OnStart方法中传入了数据库地址。 当然这只是从数据库中获取议员信息,向数据库中存入议员信息的工作主要由LevelDBBlockChain.cs文件中的Persist(Block block) 方法负责,这个方法接收一个区块类型作为参数,主要工作是将同步到的区块信息解析保存。涉及到议员信息的关键代码如下:

源码位置:neo/Implementations/BlockChains/LevelDB/LevelDBBlockChain.cs/Persist

通过调用GetAndChange方法将获取到的议员账户添加到数据库缓存中。

0x02 确定议长

共识节点通过调用ConsensusService类中的Start方法来开始参与共识。在Start方法中首先是注册了消息接收、数据保存等的事件通知,之后调用InitializeConsensus开启共识,InitializeConsensus方法接收一个整形参数,这个参数被称为为视图编号,具体视图的定义可以去查看官方文档,这里不做解释。当传入的视图编号为0时,就意味是着一轮新的共识,需要重置共识状态。重置共识状态的代码如下:

源码位置:neo/Consenus/ConsensusContext.cs

在代码中我添加了详尽的注释,确定议长的算法是当前区块高度+1 再减去当前的视图编号,结果mod上当前的议员人数,结果就是议长的下标。议员自己的编号则是自己在议员列表中的位置,因为这个位置的排序是根据每个议员的权重,所以理论上只要节点的议员成员是一致的,那么最终获得的序列也是一致,也就是说每个议员的编号在所有的共识节点都是一致的。 在共识节点中,除了在共识重置的时候会确定议长之外,在每次更新本地视图的时候也会重新确定议长:

源码位置:neo/Consensus/ConsensusContex.cs

0x03 议长发起共识

议长在更新完视图编号后,如果当前时间距离上次写入新区块的时间超过了预定的每轮共识的间隔时间(15s)则立即开始新一轮的共识,否则等到间隔时间后再发起共识,时间控制代码如下: 源码位置:neo/Consensus/ConsencusService.cs/InitializeConsensus

议长进行共识的函数是OnTimeout,由定时器定时执行。下面是议长发起共识的核心代码:

源码位置:neo/Consencus/ConsensusService.cs/OnTimeOut

议长将本地的交易生成新的Header并签名,然后将这个Header发送PrepareRequest广播给网络中的议员。

0x04 议员参与共识

议员在收到PrepareRequest广播之后会触发OnPrepareReceived方法:

源码位置:neo/Consensus/ConsensusService.cs

议员在收到议长共识请求之后,首先使用议长的公钥对收到的共识信息进行验证,在验证通过后将议长的签名添加到签名列表中。然后将内存中缓存并在议长Header的交易哈希列表中的交易添加到context里。 这里需要讲一下这个从内存中添加交易信息到context中的方法 AddTransaction。这个方法在每次添加交易之后都会比较当前context中的交易笔数是否和从议长那里获取的交易哈希数相同,如果相同而且记账人合约地址验证通过,则广播自己的签名到网络中,这部分核心代码如下:

源码位置:neo/Consensus/ConsensusService.cs/AddTransaction

因为所有的议员都需要同步各个共识节点的签名,所以议员节点也需要监听网络中别的节点对议长共识信息的响应并记录签名信息。在每次监听到共识响应并记录了收到的签名信息之后,节点需要调用CheckSignatures方法对当前收到的签名信息是否合法进行判断,CheckSignatures代码如下:

源码位置:neo/Consensus/ConsensusService.cs

CheckSignatures方法里首先是对当前签名数的合法性判断。也就是以获取的合法签名数量需要不小于M。M这个值的获取在ConsensusContext类中:

这个值的获取涉及到NEO共识算法的容错能力,公式是

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180214G0G99Y00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券