Bytom侧链Vapor源码浅析

在这篇文章中,作者将从Vapor节点的创建开始,进而拓展讲解Vapor节点出块过程中所涉及的源码。

做为Vapor源码解析系列的第一篇,本文首先对Vapor稍加介绍。Vapor是目前国内主流公链Bytom的高性能侧链,是从Bytom主链中发展出来的一条独立的高性能侧链。Vapor是平台最重要的区块链基础设施之一,目前采用DPoS的共识算法,具有高性能、高安全、可扩展等特点,用于搭建规模化的商业应用。

Vapor节点创建及出块模块的启动

Vapor入口函数:

vapor/cmd/vapord/main.go

1func main() { 2 cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir())) 3 cmd.Execute() 4}

传入参数node后会调用runNode函数并新建一个节点。

vapor/cmd/vapord/commands/run_node.go

1func runNode(cmd *cobra.Command, args []string) error { 2 startTime := time.Now() 3 setLogLevel(config.LogLevel) 4 5 // Create & start node 6 n := node.NewNode(config) 7 …… 8}

vapor节点的结构:

vapor/node/node.go

1type Node struct { 2 cmn.BaseService 3 4 config *cfg.Config 5 eventDispatcher *event.Dispatcher 6 syncManager *netsync.SyncManager 7 8 wallet *w.Wallet 9 accessTokens *accesstoken.CredentialStore 10 notificationMgr *websocket.WSNotificationManager 11 api *api.API 12 chain *protocol.Chain 13 blockProposer *blockproposer.BlockProposer 14 miningEnable bool 15}

其中与出块和共识相关的是blockProposer字段

新建节点的部分源码

vapor/node/node.go

1func NewNode(config *cfg.Config) *Node { 2 //…… 3 node := &Node{ 4 eventDispatcher: dispatcher, 5 config: config, 6 syncManager: syncManager, 7 accessTokens: accessTokens, 8 wallet: wallet, 9 chain: chain, 10 miningEnable: config.Mining, 11 12 notificationMgr: notificationMgr, 13 } 14 15 node.blockProposer = blockproposer.NewBlockProposer(chain, accounts, txPool, dispatcher) 16 node.BaseService = *cmn.NewBaseService(nil, "Node", node) 17 return node 18}

从这可以看到node.blockProposer本质上是一个vapor的block生成器,实际控制node启动出块的模块是vapor/proposal/blockproposer/blockproposer.go中的:

1func (b *BlockProposer) Start() { 2 b.Lock() 3 defer b.Unlock() 4 5 // Nothing to do if the miner is already running 6 if b.started { 7 return 8 } 9 10 b.quit = make(chan struct{}) 11 go b.generateBlocks() //出块功能的关键模块 12 13 b.started = true 14 log.Infof("block proposer started") 15}

出块模块可以通过api启动

vapor/api/miner.go

1func (a *API) startMining() Response { 2 a.blockProposer.Start() 3 if !a.IsMining() { 4 return NewErrorResponse(errors.New("Failed to start mining")) 5 } 6 return NewSuccessResponse("") 7}

以上讲解的是节点创建和出块模块启动所涉及的源码。

generateBlocks()函数开始,将要讲解是Vapor出块过程的具体源码。

Vapor的出块机制

Vapor采用的是DPoS的共识机制进行出块。DPoS是由被社区选举的可信帐户(受托人,得票数排行前10位)来创建区块。为了成为正式受托人,用户要去社区拉票,获得足够多用户的信任。用户根据自己持有的加密货币数量占总量的百分比来投票。DPoS机制类似于股份制公司,普通股民进不了董事会,要投票选举代表(受托人)代他们做决策。在讲解Vapor的出块流程之前,要先了解Vapor在DPoS的参数设定。

DPoS的参数信息位于 vapor/consensus/general.go

1type DPOSConfig struct { 2 NumOfConsensusNode int64 3 BlockNumEachNode uint64 4 RoundVoteBlockNums uint64 5 MinConsensusNodeVoteNum uint64 6 MinVoteOutputAmount uint64 7 BlockTimeInterval uint64 8 MaxTimeOffsetMs uint64 9}

接下来对参数进行具体解释

  • NumOfConsensusNode是DPOS中共识节点的数量,Vapor中设置为10,通过投票选出十个负责出块的共识节点。
  • BlockNumEachNode是每个共识节点连续出块的数量,Vapor中设置为12。
  • RoundVoteBlockNums为每轮投票的出块数,Vapor中设置为1200,也就是说每轮投票产生的共识节点会负责出块1200个。
  • MinConsensusNodeVoteNum是成为共识节点要求的最小BTM数量(单位为neu,一亿分之一BTM),Vapor中设置为100000000000000,也就是说一个节点想成为共识节点,账户中至少需要存有100万BTM。
  • MinVoteOutputAmoun为节点进行投票所要求的最小BTM 数量(单位为neu),Vapor中设置为100000000,节点想要参与投票,账户中需要1BTM
  • BlockTimeInterval为最短出块时间间隔,Vapor每间隔0.5秒出一个块。
  • MaxTimeOffsetMs为块时间允许比当前时间提前的最大秒数,在Vapor中设置为2秒。

讲完DPoS的参数设置后,就可以看看Vapor上出块的核心代码 generateBlocks

vapor/proposal/blockproposer/blockproposer.go

1func (b *BlockProposer) generateBlocks() { 2 xpub := config.CommonConfig.PrivateKey().XPub() 3 xpubStr := hex.EncodeToString(xpub[:]) 4 ticker := time.NewTicker(time.Duration(consensus.ActiveNetParams.BlockTimeInterval) * time.Millisecond) 5 defer ticker.Stop() 6 7 for { 8 select { 9 case <-b.quit: 10 return 11 case <-ticker.C: 12 } 13 //1 14 bestBlockHeader := b.chain.BestBlockHeader() 15 bestBlockHash := bestBlockHeader.Hash() 16 now := uint64(time.Now().UnixNano() / 1e6) 17 base := now 18 if now < bestBlockHeader.Timestamp { 19 base = bestBlockHeader.Timestamp 20 } 21 minTimeToNextBlock := consensus.ActiveNetParams.BlockTimeInterval - base%consensus.ActiveNetParams.BlockTimeInterval 22 nextBlockTime := base + minTimeToNextBlock 23 if (nextBlockTime - now) < consensus.ActiveNetParams.BlockTimeInterval/10 { 24 nextBlockTime += consensus.ActiveNetParams.BlockTimeInterval 25 } 26 27 //2 28 blocker, err := b.chain.GetBlocker(&bestBlockHash, nextBlockTime) 29 …… 30 if xpubStr != blocker { 31 continue 32 } 33 34 35 //3 36 warnDuration := time.Duration(consensus.ActiveNetParams.BlockTimeInterval*warnTimeNum/warnTimeDenom) * time.Millisecond 37 criticalDuration := time.Duration(consensus.ActiveNetParams.BlockTimeInterval*criticalTimeNum/criticalTimeDenom) * time.Millisecond 38 block, err := proposal.NewBlockTemplate(b.chain, b.accountManager, nextBlockTime, warnDuration, criticalDuration) 39 …… 40 //4 41 isOrphan, err := b.chain.ProcessBlock(block) 42 …… 43 //5 44 log.WithFields(log.Fields{"module": logModule, "height": block.BlockHeader.Height, "isOrphan": isOrphan, "tx": len(block.Transactions)}).Info("proposer processed block") 45 46 if err = b.eventDispatcher.Post(event.NewProposedBlockEvent{Block: *block}); err != nil { 47 log.WithFields(log.Fields{"module": logModule, "height": block.BlockHeader.Height, "error": err}).Error("proposer fail on post block") 48 } 49 } 50}

代码经过精简,省略了一些无关紧要的部分,并将重要的部分,分为5个模块。

  1. 计算并调整出块的时间
  2. 通过GetBlocker 获取顺序下一个block的公钥,并与当前块比对,判断当前块的出块顺序是否合法。
  3. 通过b.chain.ProcessBlock根据模板生成了一个block。
  4. 通过chain.ProcessBlock(block)尝试把block加工处理后加到本机持有的区块链上。
  5. 使用logrus框架记录新的块,并像网络中广播。

b.chain.GetBlocker

针对generateBlocks()中几个重要的模块进行拆分讲解。

vapor/protocol/consensus_node_manager.go

GetBlocker()传入当前高度块的哈希和下一个块的出块时间。

1// 返回一个特定时间戳的Blocker 2func (c *Chain) GetBlocker(prevBlockHash *bc.Hash, timeStamp uint64) (string, error) { 3 consensusNodeMap, err := c.getConsensusNodes(prevBlockHash) 4 //…… 5 6 prevVoteRoundLastBlock, err := c.getPrevRoundLastBlock(prevBlockHash) 7 //…… 8 9 startTimestamp := prevVoteRoundLastBlock.Timestamp + consensus.ActiveNetParams.BlockTimeInterval 10 //获取order,xpub为公钥 11 order := getBlockerOrder(startTimestamp, timeStamp, uint64(len(consensusNodeMap))) 12 for xPub, consensusNode := range consensusNodeMap { 13 if consensusNode.Order == order { 14 return xPub, nil 15 } 16 } 17 //…… 18}
  • 通过调用c.getConsensusNodes()获得一个存储共识节点的Map。
  • 获取上一轮投票的最后一个块,在加上最短出块时间间隔,计算得到这一轮的开始时间戳。
  • 调用getBlockerOrder,通过开始时间戳和当前要出块的时间戳计算出这个时间点出块的order。
  • 最后比对consensusNodeMapconsensusNode.Order,并返回公钥。

这个模块是为了找出当前时间戳对应出块的共识节点,并返回节点的公钥。因为DPoS中出块的节点和顺序必须是固定的,而使用generateBlocks()模块尝试出块的共识节点不一定是当前时间的合法出块节点,因此需要本模块通过对比公钥进行节点资格的验证。

proposal.NewBlockTemplate

vapor/proposal/proposal.go

1func NewBlockTemplate(chain *protocol.Chain, accountManager *account.Manager, timestamp uint64, warnDuration, criticalDuration time.Duration) (*types.Block, error) { 2 builder := newBlockBuilder(chain, accountManager, timestamp, warnDuration, criticalDuration) 3 return builder.build() 4} 5 6 7func newBlockBuilder(chain *protocol.Chain, accountManager *account.Manager, timestamp uint64, warnDuration, criticalDuration time.Duration) *blockBuilder { 8 preBlockHeader := chain.BestBlockHeader() 9 block := &types.Block{ 10 BlockHeader: types.BlockHeader{ 11 Version: 1, 12 Height: preBlockHeader.Height + 1, 13 PreviousBlockHash: preBlockHeader.Hash(), 14 Timestamp: timestamp, 15 BlockCommitment: types.BlockCommitment{}, 16 BlockWitness: types.BlockWitness{Witness: make([][]byte, consensus.ActiveNetParams.NumOfConsensusNode)}, 17 }, 18 } 19 20 builder := &blockBuilder{ 21 chain: chain, 22 accountManager: accountManager, 23 block: block, 24 txStatus: bc.NewTransactionStatus(), 25 utxoView: state.NewUtxoViewpoint(), 26 warnTimeoutCh: time.After(warnDuration), 27 criticalTimeoutCh: time.After(criticalDuration), 28 gasLeft: int64(consensus.ActiveNetParams.MaxBlockGas), 29 timeoutStatus: timeoutOk, 30 } 31 return builder 32}

在Vapor上每个区块有区块头和区块的主体,区块头中包含版本号、高度、上一区块的hash、时间戳等等,主体包括区块链的引用模块、账户管理器、区块头、Transaction状态(版本号和验证状态)、utxo视图等。这一部分的目的是将,区块的各种信息通过模板包装成一个block交给后面的ProcessBlock(block)加工处理。

b.chain.ProcessBlock

vapor/protocol/block.go

1func (c *Chain) ProcessBlock(block *types.Block) (bool, error) { 2 reply := make(chan processBlockResponse, 1) 3 c.processBlockCh <- &processBlockMsg{block: block, reply: reply} 4 response := <-reply 5 return response.isOrphan, response.err 6} 7 8 9func (c *Chain) blockProcesser() { 10 for msg := range c.processBlockCh { 11 isOrphan, err := c.processBlock(msg.block) 12 msg.reply <- processBlockResponse{isOrphan: isOrphan, err: err} 13 } 14}

很显然,这只是链更新的入口,block数据通过processBlockMsg结构传入了c.processBlockCh这个管道。随后数据通过blockProcesser()处理后存入了msg.reply管道,而最后处理这个block的是processBlock()函数:

1func (c *Chain) processBlock(block *types.Block) (bool, error) { 2 //1 3 blockHash := block.Hash() 4 if c.BlockExist(&blockHash) { 5 log.WithFields(log.Fields{"module": logModule, "hash": blockHash.String(), "height": block.Height}).Debug("block has been processed") 6 return c.orphanManage.BlockExist(&blockHash), nil 7 } 8 //2 9 c.markTransactions(block.Transactions...) 10 //3 11 if _, err := c.store.GetBlockHeader(&block.PreviousBlockHash); err != nil { 12 c.orphanManage.Add(block) 13 return true, nil 14 } 15 //4 16 if err := c.saveBlock(block); err != nil { 17 return false, err 18 } 19 20 bestBlock := c.saveSubBlock(block) 21 bestBlockHeader := &bestBlock.BlockHeader 22 23 c.cond.L.Lock() 24 defer c.cond.L.Unlock() 25 //5 26 if bestBlockHeader.PreviousBlockHash == c.bestBlockHeader.Hash() { 27 log.WithFields(log.Fields{"module": logModule}).Debug("append block to the end of mainchain") 28 return false, c.connectBlock(bestBlock) 29 } 30 //6 31 if bestBlockHeader.Height > c.bestBlockHeader.Height { 32 log.WithFields(log.Fields{"module": logModule}).Debug("start to reorganize chain") 33 return false, c.reorganizeChain(bestBlockHeader) 34 } 35 return false, nil 36}

processBlock()函数返回的bool表示的是block是否为孤块。

  1. 通过block的hash判断这个block是否已经在链上。若已存在,则报错并返回false(表示该block不是孤块)
  2. 将block中的Transactions标记,后续会调用c.knownTxs.Add()将Transactions加入到Transaction集合中。
  3. 判断是否为孤块,如果是,则调用孤块管理部分的模块处理并返回true。
  4. 保存block,在saveBlock()中会对签名和区块进行验证。
  5. bestBlockHeader.PreviousBlockHash == c.bestBlockHeader.Hash()的情况说明一切正常,新block被添加到链的末端。
  6. bestBlockHeader.Height > c.bestBlockHeader.Height 表示出现了分叉,需要回滚。

总结

本篇文章从Vapor设置出块开始,到出块流程结束,细节层层解析节点设置出块和出块部分所涉及的源码。虽然本文至此篇幅已经比较长,但仍有重要的问题没有讲解清楚。例如,generateBlocks()中的第2点,程序会对出块的顺序进行查验,但这个出块的顺序是怎么获得还未做细致的解析。

那么,下一篇文章将针对Vapor中DPoS机制的细节进行源码级解析。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

BFT等5种主流区块链共识的开源实现

共识算法是实现自主产权区块链的必不可少的关键环节,本文列出社区中相对成熟的区块链共识算法开源实现,包括BFT共识、Raft共识、Paxos共识、PoW共识等,可供希望开发自主产权区块链的团队参考学习。相关推荐:区块链开发系列教程(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fww

NEO从源码分析看UTXO交易

_0x00前言_社区大佬:“交易是操作区块链的唯一方式。”_0x01交易类型_在NEO中,几乎除了共识之外的所有的对区块链的操作都是一种“交易”,甚至在“交易”面前,合约都只是一个小弟。交易类型的定义在Core中的TransactionType中:源码位置:neo/Core/TransactionType

VRF在区块链中的应用

最近区块链领域流行了一种“怪病”,许多区块链项目或者设计方案都加入了一个叫做VRFs的算法。那么,(1)什么是VRFs?(2)VRFs在区块链中解决了什么问题?本文旨在介绍VRFs的原理以及在区块链中的用途,不涉及复杂的VRFs的详细设计实现以及安全性证明。VRFs介绍VRFs全称VerifiableRand