作者:廖京辉

原文链接:https://mp.weixin.qq.com/s?__biz=MzUzNDQwNDQ0Mw==&mid=2247483919&idx=1&sn=596887560d33a3e4a6af9631e0c48738&chksm=fa940e3bcde3872dad416b3e6842e004199819ce76a953104bfe0730b424e2e46df8bc6f4325&scene=21#wechat_redirect

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

         /// <summary>/// 获取下一个区块的记账人列表/// </summary>/// <returns>返回一组公钥,表示下一个区块的记账人列表</returns>public ECPoint[] GetValidators(){            lock (_validators){                if (_validators.Count == 0){_validators.AddRange(GetValidators(Enumerable.Empty<Transaction>()));}return _validators.ToArray();}}

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

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

       public virtual IEnumerable<ECPoint> GetValidators(IEnumerable<Transaction> others){DataCache<UInt160, AccountState> accounts = GetStates<UInt160, AccountState>();DataCache<ECPoint, ValidatorState> validators = GetStates<ECPoint, ValidatorState>();MetaDataCache<ValidatorsCountState> validators_count = GetMetaData<ValidatorsCountState>();foreach (Transaction tx in others){                }            int count = (int)validators_count.Get().Votes.Select((p, i) => new{                Count = i,Votes = p}).Where(p => p.Votes > Fixed8.Zero).ToArray().WeightedFilter(0.25, 0.75, p => p.Votes.GetData(), (p, w) => new{p.Count,Weight = w}).WeightedAverage(p => p.Count, p => p.Weight);            count = Math.Max(count, StandbyValidators.Length);HashSet<ECPoint> sv = new HashSet<ECPoint>(StandbyValidators);ECPoint[] pubkeys = validators.Find().Select(p => p.Value).Where(p => (p.Registered && p.Votes > Fixed8.Zero) || sv.Contains(p.PublicKey)).OrderByDescending(p => p.Votes).ThenBy(p => p.PublicKey).Select(p => p.PublicKey).Take(count).ToArray();IEnumerable<ECPoint> result;            if (pubkeys.Length == count){result = pubkeys;}            else{HashSet<ECPoint> hashSet = new HashSet<ECPoint>(pubkeys);                for (int i = 0; i < StandbyValidators.Length && hashSet.Count < count; i++)hashSet.Add(StandbyValidators[i]);result = hashSet;}            return result.OrderBy(p => p);}

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

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

    public override DataCache<TKey, TValue> GetStates<TKey, TValue>(){Type t = typeof(TValue);            if (t == typeof(AccountState)) return new DbCache<TKey, TValue>(db, DataEntryPrefix.ST_Account);            if (t == typeof(UnspentCoinState)) return new DbCache<TKey, TValue>(db, DataEntryPrefix.ST_Coin);            if (t == typeof(SpentCoinState)) return new DbCache<TKey, TValue>(db, DataEntryPrefix.ST_SpentCoin);            if (t == typeof(ValidatorState)) return new DbCache<TKey, TValue>(db, DataEntryPrefix.ST_Validator);            if (t == typeof(AssetState)) return new DbCache<TKey, TValue>(db, DataEntryPrefix.ST_Asset);            if (t == typeof(ContractState)) return new DbCache<TKey, TValue>(db, DataEntryPrefix.ST_Contract);            if (t == typeof(StorageItem)) return new DbCache<TKey, TValue>(db, DataEntryPrefix.ST_Storage);            throw new NotSupportedException();}

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

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

            foreach (ECPoint pubkey in account.Votes){ValidatorState validator = validators.GetAndChange(pubkey);validator.Votes -= out_prev.Value;                       if (!validator.Registered && validator.Votes.Equals(Fixed8.Zero))validators.Delete(pubkey);}

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

0x02 确定议长

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

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

        /// <summary>/// 共识状态重置,准备发起新一轮共识/// </summary>/// <param name="wallet">钱包</param>public void Reset(Wallet wallet)        {State = ConsensusState.Initial;  //设置共识状态为 InitialPrevHash = Blockchain.Default.CurrentBlockHash;   //获取上一个区块的哈希BlockIndex = Blockchain.Default.Height + 1;  //新区块下标ViewNumber = 0;     //初始状态 视图编号为0Validators = Blockchain.Default.GetValidators();   //获取议员信息MyIndex = -1;   //当前议员下标初始化PrimaryIndex = BlockIndex % (uint)Validators.Length; //确定议长 p = (h-v)mod n 此处v = 0 TransactionHashes = null;Signatures = new byte[Validators.Length][];ExpectedView = new byte[Validators.Length];   //用于保存众议员当前视图编号KeyPair = null;            for (int i = 0; i < Validators.Length; i++){                //获取自己的议员编号以及密钥WalletAccount account = wallet.GetAccount(Validators[i]);                if (account?.HasKey == true){MyIndex = i;KeyPair = account.GetKey();                    break;}}_header = null;}}

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

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

        /// <summary>/// 更新共识视图/// </summary>/// <param name="view_number">新的视图编号</param>public void ChangeView(byte view_number)        {            int p = ((int)BlockIndex - view_number) % Validators.Length;            //设置共识状态为已发送签名State &= ConsensusState.SignatureSent;ViewNumber = view_number;            //议长编号PrimaryIndex = p >= 0 ? (uint)p : (uint)(p + Validators.Length);            if (State == ConsensusState.Initial){TransactionHashes = null;Signatures = new byte[Validators.Length][];}_header = null;}

0x03 议长发起共识

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

         //议长发起共识时间控制TimeSpan span = DateTime.Now - block_received_time;          if (span >= Blockchain.TimePerBlock)timer.Change(0, Timeout.Infinite); //间隔时间大于预定时间则立即发起共识elsetimer.Change(Blockchain.TimePerBlock - span, Timeout.InfiniteTimeSpan); //定时执行

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

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

         context.Timestamp = Math.Max(DateTime.Now.ToTimestamp(),  Blockchain.Default.GetHeader(context.PrevHash).Timestamp + 1);context.Nonce = GetNonce();//生成区块随机数//获取本地内存中的交易列表List<Transaction> transactions = LocalNode.GetMemoryPool().Where(p => CheckPolicy(p)).ToList();           //如果内存中缓存的交易信息数量大于区块最大交易数,则对内存中的交易信息进行排序 每字节手续费 越高越先确认交易if (transactions.Count >= Settings.Default.MaxTransactionsPerBlock)transactions = transactions.OrderByDescending(p => p.NetworkFee / p.Size).Take(Settings.Default.MaxTransactionsPerBlock - 1).ToList();                        //添加手续费交易transactions.Insert(0, CreateMinerTransaction(transactions, context.BlockIndex, context.Nonce));context.TransactionHashes = transactions.Select(p => p.Hash).ToArray();context.Transactions = transactions.ToDictionary(p => p.Hash);            //获取新区块记账人合约地址context.NextConsensus = Blockchain.GetConsensusAddress(Blockchain.Default.GetValidators(transactions).ToArray());            //生成新区块并签名context.Signatures[context.MyIndex] = context.MakeHeader().Sign(context.KeyPair);

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

0x04 议员参与共识

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

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

        /// <summary>/// 收到议长共识请求/// </summary>/// <param name="payload">议长的共识参数</param>/// <param name="message"></param>private void OnPrepareRequestReceived(ConsensusPayload payload, PrepareRequest message)        {Log($"{nameof(OnPrepareRequestReceived)}: height={payload.BlockIndex} view={message.ViewNumber} index={payload.ValidatorIndex} tx={message.TransactionHashes.Length}");            if (!context.State.HasFlag(ConsensusState.Backup) || context.State.HasFlag(ConsensusState.RequestReceived))//当前不处于回退状态或者已经收到了重置请求return;            if (payload.ValidatorIndex != context.PrimaryIndex) return;//只接受议长发起的共识请求if (payload.Timestamp <= Blockchain.Default.GetHeader(context.PrevHash).Timestamp || payload.Timestamp > DateTime.Now.AddMinutes(10).ToTimestamp()){Log($"Timestamp incorrect: {payload.Timestamp}");                return;}context.State |= ConsensusState.RequestReceived;//设置状态为收到议长共识请求context.Timestamp = payload.Timestamp;          //时间戳同步context.Nonce = message.Nonce;                  //区块随机数同步context.NextConsensus = message.NextConsensus;  context.TransactionHashes = message.TransactionHashes;  //交易哈希context.Transactions = new Dictionary<UInt256, Transaction>();            //议长公钥验证if (!Crypto.Default.VerifySignature(context.MakeHeader().GetHashData(), message.Signature, context.Validators[payload.ValidatorIndex].EncodePoint(false))) return;            //添加议长签名到议员签名列表context.Signatures = new byte[context.Validators.Length][];context.Signatures[payload.ValidatorIndex] = message.Signature;            //将内存中缓存的交易添加到共识的context中Dictionary<UInt256, Transaction> mempool = LocalNode.GetMemoryPool().ToDictionary(p => p.Hash);            foreach (UInt256 hash in context.TransactionHashes.Skip(1)){                if (mempool.TryGetValue(hash, out Transaction tx))                    if (!AddTransaction(tx, false))//从缓存队列中读取添加到contex中return;}            if (!AddTransaction(message.MinerTransaction, true)) return; //添加分配字节费的交易 矿工手续费交易LocalNode.AllowHashes(context.TransactionHashes.Except(context.Transactions.Keys));            if (context.Transactions.Count < context.TransactionHashes.Length)localNode.SynchronizeMemoryPool();}

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

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

                    //设置共识状态为已发送签名context.State |= ConsensusState.SignatureSent;                    //添加本地签名到签名列表context.Signatures[context.MyIndex] = context.MakeHeader().Sign(context.KeyPair);                    //广播共识响应SignAndRelay(context.MakePrepareResponse(context.Signatures[context.MyIndex]));                    //检查签名状态是否符合共识要求CheckSignatures();

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

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

        /// <summary>/// 验证共识协商结果/// </summary>private void CheckSignatures()        {            //验证当前已进行的协商的共识节点数是否合法if (context.Signatures.Count(p => p != null) >= context.M && context.TransactionHashes.All(p => context.Transactions.ContainsKey(p))){                //建立合约Contract contract = Contract.CreateMultiSigContract(context.M, context.Validators);                //创建新区块Block block = context.MakeHeader();                //设置区块参数ContractParametersContext sc = new ContractParametersContext(block);                for (int i = 0, j = 0; i < context.Validators.Length && j < context.M; i++)                    if (context.Signatures[i] != null){sc.AddSignature(contract, context.Validators[i], context.Signatures[i]);j++;}                //获取用于验证区块的脚本sc.Verifiable.Scripts = sc.GetScripts();block.Transactions = context.TransactionHashes.Select(p => context.Transactions[p]).ToArray();Log($"relay block: {block.Hash}");                //广播新区块if (!localNode.Relay(block))Log($"reject block: {block.Hash}");                //设置当前共识状态为新区块已广播context.State |= ConsensusState.BlockSent;}}

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

public int M => Validators.Length - (Validators.Length - 1) / 3;

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

从NEO源码分析看DBFT共识协议相关推荐

  1. NEO源码分析之UTXO全局资产

    作者:廖京辉 原文链接:https://mp.weixin.qq.com/s?__biz=MzUzNDQwNDQ0Mw==&mid=2247483941&idx=1&sn=4a ...

  2. NEO从源码分析看网络通信

    2019独角兽企业重金招聘Python工程师标准>>> 0x00 前言 NEO被称为中国版的Ethereum,支持C#和java开发,并且在社区的努力下已经把SDK拓展到了js,py ...

  3. NEO从源码分析看NEOVM

    2019独角兽企业重金招聘Python工程师标准>>> 0x00 前言 这篇文章是为下一篇<NEO从源码分析看UTXO转账交易>打前站,为交易的构造及执行的一些技术基础做 ...

  4. NEO从源码分析看数字资产

    0x00 引言 比特币是泡沫么?也许是的.毕竟这东西除了用来炒,干什么实事都感觉肉疼.但是有人将比特币泡沫和郁金香泡沫相提并论就很气人了,郁金香什么鬼,长那么一年,开那么几天,泡沫还没破呢,郁金香已经 ...

  5. NEO从源码分析看共识协议

    2019独角兽企业重金招聘Python工程师标准>>> 0x00 概论 不同于比特币使用的工作量证明(PoW)来实现共识,NEO提出了DBFT共识算法.DBFT改良自股权证明算法(P ...

  6. NEO从源码分析看nep2与nep6

    0x00 前言 混社区的时候(QQ群)总是听到大佬们聊到nep,好奇心驱使下就去neo官网找资料,然鹅,什么都没找到.后来就请教大佬,才知道nep是neo一系列提案,文档并不在neo官网,在这里.但是 ...

  7. Dubbo源码分析系列-深入RPC协议扩展

    导语   在之前的博客里面提到了关于扩展机制以及SPI的原理,这篇博客主要来讨论一下关于协议的扩展问题,在系统与系统之间通信就需要两个系统之间遵循相同的协议.而现在被熟知的常用的协议有TCP/IP协议 ...

  8. 英雄远征Erlang源码分析(5)-协议解析与玩家登录处理

    现在,客户端与服务器的连接算是正式建立了,此时用户需要做的第一件事就是登陆.不过在登录之前,我们要先研究下前后端通信的协议. 客户端与服务端建立连接后,通过提前制定好的协议进行交互.具体的协议文档在d ...

  9. 从vuex源码分析module与namespaced

    使用vue已经有半年有余, 在各种正式非正式项目中用过, 开始专注于业务比较多, 用到现在也遇见不少因为理解不深导致的问题. 有问题就有找原因的勇气, 所以带着问题搞一波. 带着问题看源码 所以来整理 ...

最新文章

  1. sshd系统自带启动脚本详解
  2. Java异常之try,catch,finally,throw,throws
  3. 如何在 C# 中使用 委托
  4. C#编译器优化那点事
  5. aix pax_通过Pax考试对JBoss Fuse 6.x进行集成测试,第一部分
  6. HH SaaS电商系统的商品关联版式功能模块设计
  7. HDU 5392 BC #51
  8. docker registry push 覆盖_原创 | 全网最实在的docker入门教程四
  9. 通过管道传输快速将MySQL的数据导入Redis
  10. 环信 之 注册及创建应用
  11. php发送sql,php学习笔记(二)php与mysql连接与用php发送SQL查询
  12. 【BZOJ】1176: [Balkan2007]Mokia(cdq分治)
  13. matlab实现Sobel边缘检测
  14. H5游戏开发-Egret引擎
  15. 字节跳动+京东+美团+腾讯面试总结,附赠课程+题库
  16. 什么是埃博拉免疫T-细胞?
  17. android 如何保存网页图片格式,求助,如何在安卓app内嵌的H5页面里长按保存图片?...
  18. 筛选鉴定与已经基因启动子相互作用的DNA结合蛋白-DNA Pull Down实验原理,技术流程
  19. 权限管理实现的两种方式(详解)
  20. 绿联外围设备_什么是外围设备(外围设备的类型和列表)?

热门文章

  1. 中国铁路轮轴行业市场供需与战略研究报告
  2. gen9 ws460c 惠普_HP刀片工作站WS460c Gen9无惧4K挑战
  3. wkhtmlpdf-python
  4. BSD常用小技巧[转]
  5. 计算机文件夹不在桌面显示,为什么我的电脑桌面上的有个文件夹里的文件突然不见了呢...
  6. 实时热力图_热浪滚滚的夏天,来聊聊热力图吧(上)
  7. 各种现代方法和技术在储集层研究中的运用
  8. 用matlab用mesh画正方体,用Matlab三维网线图函数mesh绘制正方体
  9. 计算机正常充电环境温度范围,笔记本电脑的充电突然变得很慢怎么了?给1%的电池充电只需7到8分钟...
  10. 【AI研究院】头条与抖音背后的AILab怎么样