程序员视角看Neo共识机制

获取议员名单
Neo网络节点分为两种,一种为共识节点,另一种为普通节点。相对于普通节点,共识节点将参与共识过程并且有机会成为议长主持新区块的生成。
接下来,我将通过源码分析来介绍如何通过Neo的服务器注册议员。源码中,每轮共识开始时会调用ConsensusContext.cs中的Reset方法,在重置共识时会调用Blockchain.Default.GetValidators()来获取议员列表,跟进去这个GetValidators()源码:
源码位置:

neo/Implementations/BlockChains/LevelDB/LevelDBBlockChain.cs

发现这里调用了内部的GetValidators(IEnumerable<Transaction> 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方法将获取到的议员账户添加到数据库缓存中。
确定议长
共识节点通过调用ConsensusService类中的Start方法开始参与共识。在Start方法中,先注册消息接收、数据保存等事件通知,然后调用InitializeConsensus开启共识,接收一个名为「视图编号」的整形参数。当传入的视图编号为0时,即一轮新共识需要重置共识状态。重置共识状态的代码如下:
源码位置:
neo/Consenus/ConsensusContext.cs

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

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

议长进行共识的函数是OnTimeout,由定时器定时执行。下面是议长发起共识的核心代码:
源码位置:
neo/Consencus/ConsensusService.cs/OnTimeOut
议长将本地的交易生成新的Header并签名,然后将这个Header发送PrepareRequest广播给网络中的议员。
议员参与共识
议员在收到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共识算法的容错能力,公式是