程序员视角看Neo共识机制 | Neo专栏

  • Post author:
  • Post category:未分类

获取议员名单

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共识算法的容错能力,公式是 = ⌊ ( −1) / 3 ⌋,理解的话就是只要有超过网络2/3的共识节点是一致的,那么这个结果就是可信的。即只要获取到的签名数量合法了,当前节点就可以根据已有的信息生成新的区块并向网络中进行广播。

视图更新

Neo网络的共识间隔是以定时任务来做的,而不是根据全网算力在数学意义上保证每个区块生成的大概时间。每轮的共识都是由当前选定的议长来发起,但如果当前选定的议长作恶,如果这个议长一直不发起共识或者故意发起错误的共识信息导致本轮共识无法最终完成怎么办?

为了解决这个问题,视图概念诞生了。在一个视图生存周期完成的时候,如果共识还没有被达成,则议员会发送广播请求进入下一个视图周期并重新选择议长,当请求更新视图的请求大于议员数量的2/3的时候,全网达成共识进入下一个视图周期重新开始共识过程。议长的选定算法和视图的编号有关系,这保证了每轮视图选定的议长不会是同一个。视图的生存时间是t*2^(view_number+1),其中t是默认的区块生成时间间隔,view_number是当前视图编号。议员在每次共识开始的时候进入编号为0的视图周期,如果当前周期完成的时候共识没有达成,则视图编号+1,并进入下一个视图周期。定义视图生存时间的代码在ConsensusServer类的InitializeConsensus方法中:

源码位置:

neo/Consensus/ConsensusService.cs/InitializeConsensus

当一轮视图周期完成的时候,如果共识没有达成则发出更新视图请求:

源码位置:

neo/Consensus/ConsensusService.cs

更新视图会把当前期望视图+1,并且广播更新视图的请求给所有的议员。这里需要注意的是,在当前节点发送了更新视图的请求之后,节点的当前视图编号并没有改变,而只是改变了期望视图编号。其他议员在收到更新视图的广播后会触发OnChangeViewReceived方法来更新自己的议员期望视图列表。

源码位置:

neo/Consensus/ConsensusService.cs

在每次收到更新视图请求之后都需要检查一下当前收到的请求数量是不是大于2/3的全体议员数,如果满足条件,则在新视图周期里重新开始共识过程。

发表评论