Bytom侧链Vapor源码浅析-节点出块过程

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

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

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

Vapor入口函数:

vapor/cmd/vapord/main.go

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

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

vapor/cmd/vapord/commands/run_node.go

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

vapor节点的结构:

vapor/node/node.go

type Node struct {
cmn.BaseService config *cfg.Config
eventDispatcher *event.Dispatcher
syncManager *netsync.SyncManager wallet *w.Wallet
accessTokens *accesstoken.CredentialStore
notificationMgr *websocket.WSNotificationManager
api *api.API
chain *protocol.Chain
blockProposer *blockproposer.BlockProposer
miningEnable bool
}

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

新建节点的部分源码

vapor/node/node.go

func NewNode(config *cfg.Config) *Node {
//……
node := &Node{
eventDispatcher: dispatcher,
config: config,
syncManager: syncManager,
accessTokens: accessTokens,
wallet: wallet,
chain: chain,
miningEnable: config.Mining, notificationMgr: notificationMgr,
} node.blockProposer = blockproposer.NewBlockProposer(chain, accounts, txPool, dispatcher)
node.BaseService = *cmn.NewBaseService(nil, "Node", node)
return node
}

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

func (b *BlockProposer) Start() {
b.Lock()
defer b.Unlock() // Nothing to do if the miner is already running
if b.started {
return
} b.quit = make(chan struct{})
go b.generateBlocks() //出块功能的关键模块 b.started = true
log.Infof("block proposer started")
}

出块模块可以通过api启动

vapor/api/miner.go

func (a *API) startMining() Response {
a.blockProposer.Start()
if !a.IsMining() {
return NewErrorResponse(errors.New("Failed to start mining"))
}
return NewSuccessResponse("")
}

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

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

Vapor的出块机制

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

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

type DPOSConfig struct {
NumOfConsensusNode int64
BlockNumEachNode uint64
RoundVoteBlockNums uint64
MinConsensusNodeVoteNum uint64
MinVoteOutputAmount uint64
BlockTimeInterval uint64
MaxTimeOffsetMs uint64
}

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

  • 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

func (b *BlockProposer) generateBlocks() {
xpub := config.CommonConfig.PrivateKey().XPub()
xpubStr := hex.EncodeToString(xpub[:])
ticker := time.NewTicker(time.Duration(consensus.ActiveNetParams.BlockTimeInterval) * time.Millisecond)
defer ticker.Stop() for {
select {
case <-b.quit:
return
case <-ticker.C:
}
//1
bestBlockHeader := b.chain.BestBlockHeader()
bestBlockHash := bestBlockHeader.Hash()
now := uint64(time.Now().UnixNano() / 1e6)
base := now
if now < bestBlockHeader.Timestamp {
base = bestBlockHeader.Timestamp
}
minTimeToNextBlock := consensus.ActiveNetParams.BlockTimeInterval - base%consensus.ActiveNetParams.BlockTimeInterval
nextBlockTime := base + minTimeToNextBlock
if (nextBlockTime - now) < consensus.ActiveNetParams.BlockTimeInterval/10 {
nextBlockTime += consensus.ActiveNetParams.BlockTimeInterval
} //2
blocker, err := b.chain.GetBlocker(&bestBlockHash, nextBlockTime)
……
if xpubStr != blocker {
continue
} //3
warnDuration := time.Duration(consensus.ActiveNetParams.BlockTimeInterval*warnTimeNum/warnTimeDenom) * time.Millisecond
criticalDuration := time.Duration(consensus.ActiveNetParams.BlockTimeInterval*criticalTimeNum/criticalTimeDenom) * time.Millisecond
block, err := proposal.NewBlockTemplate(b.chain, b.accountManager, nextBlockTime, warnDuration, criticalDuration)
……
//4
isOrphan, err := b.chain.ProcessBlock(block)
……
//5
log.WithFields(log.Fields{"module": logModule, "height": block.BlockHeader.Height, "isOrphan": isOrphan, "tx": len(block.Transactions)}).Info("proposer processed block") if err = b.eventDispatcher.Post(event.NewProposedBlockEvent{Block: *block}); err != nil {
log.WithFields(log.Fields{"module": logModule, "height": block.BlockHeader.Height, "error": err}).Error("proposer fail on post block")
}
}
}

代码经过精简,省略了一些无关紧要的部分,并将重要的部分,分为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()传入当前高度块的哈希和下一个块的出块时间。

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

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

proposal.NewBlockTemplate

vapor/proposal/proposal.go

func NewBlockTemplate(chain *protocol.Chain, accountManager *account.Manager, timestamp uint64, warnDuration, criticalDuration time.Duration) (*types.Block, error) {
builder := newBlockBuilder(chain, accountManager, timestamp, warnDuration, criticalDuration)
return builder.build()
}
func newBlockBuilder(chain *protocol.Chain, accountManager *account.Manager, timestamp uint64, warnDuration, criticalDuration time.Duration) *blockBuilder {
preBlockHeader := chain.BestBlockHeader()
block := &types.Block{
BlockHeader: types.BlockHeader{
Version: 1,
Height: preBlockHeader.Height + 1,
PreviousBlockHash: preBlockHeader.Hash(),
Timestamp: timestamp,
BlockCommitment: types.BlockCommitment{},
BlockWitness: types.BlockWitness{Witness: make([][]byte, consensus.ActiveNetParams.NumOfConsensusNode)},
},
} builder := &blockBuilder{
chain: chain,
accountManager: accountManager,
block: block,
txStatus: bc.NewTransactionStatus(),
utxoView: state.NewUtxoViewpoint(),
warnTimeoutCh: time.After(warnDuration),
criticalTimeoutCh: time.After(criticalDuration),
gasLeft: int64(consensus.ActiveNetParams.MaxBlockGas),
timeoutStatus: timeoutOk,
}
return builder
}

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

b.chain.ProcessBlock

vapor/protocol/block.go

func (c *Chain) ProcessBlock(block *types.Block) (bool, error) {
reply := make(chan processBlockResponse, 1)
c.processBlockCh <- &processBlockMsg{block: block, reply: reply}
response := <-reply
return response.isOrphan, response.err
}
func (c *Chain) blockProcesser() {
for msg := range c.processBlockCh {
isOrphan, err := c.processBlock(msg.block)
msg.reply <- processBlockResponse{isOrphan: isOrphan, err: err}
}
}

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

func (c *Chain) processBlock(block *types.Block) (bool, error) {
//1
blockHash := block.Hash()
if c.BlockExist(&blockHash) {
log.WithFields(log.Fields{"module": logModule, "hash": blockHash.String(), "height": block.Height}).Debug("block has been processed")
return c.orphanManage.BlockExist(&blockHash), nil
}
//2
c.markTransactions(block.Transactions...)
//3
if _, err := c.store.GetBlockHeader(&block.PreviousBlockHash); err != nil {
c.orphanManage.Add(block)
return true, nil
}
//4
if err := c.saveBlock(block); err != nil {
return false, err
} bestBlock := c.saveSubBlock(block)
bestBlockHeader := &bestBlock.BlockHeader c.cond.L.Lock()
defer c.cond.L.Unlock()
//5
if bestBlockHeader.PreviousBlockHash == c.bestBlockHeader.Hash() {
log.WithFields(log.Fields{"module": logModule}).Debug("append block to the end of mainchain")
return false, c.connectBlock(bestBlock)
}
//6
if bestBlockHeader.Height > c.bestBlockHeader.Height {
log.WithFields(log.Fields{"module": logModule}).Debug("start to reorganize chain")
return false, c.reorganizeChain(bestBlockHeader)
}
return false, nil
}

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机制的细节进行源码级解析。

Bytom侧链Vapor源码浅析-节点出块过程的更多相关文章

  1. Android开发之Theme、Style探索及源码浅析

    1 背景 前段时间群里有伙伴问到了关于Android开发中Theme与Style的问题,当然,这类东西在网上随便一搜一大把模板,所以关于怎么用的问题我想这里也就不做太多的说明了,我们这里把重点放在理解 ...

  2. spring源码浅析——IOC

    =========================================== 原文链接: spring源码浅析--IOC   转载请注明出处! ======================= ...

  3. CountDownLatch源码浅析

    Cmd Markdown链接 CountDownLatch源码浅析 参考好文: JDK1.8源码分析之CountDownLatch(五) Java并发之CountDownLatch源码分析 Count ...

  4. 【深入浅出jQuery】源码浅析--整体架构

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  5. 【深入浅出jQuery】源码浅析2--奇技淫巧

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  6. Struts2源码浅析-ConfigurationProvider

    ConfigurationProvider接口 主要完成struts配置文件 加载 注册过程 ConfigurationProvider接口定义 public interface Configurat ...

  7. HashSet其实就那么一回事儿之源码浅析

    上篇文章<HashMap其实就那么一回事儿之源码浅析>介绍了hashMap,  本次将带大家看看HashSet, HashSet其实就是基于HashMap实现, 因此,熟悉了HashMap ...

  8. Android 手势识别类 ( 三 ) GestureDetector 源码浅析

    前言:上 篇介绍了提供手势绘制的视图平台GestureOverlayView,但是在视图平台上绘制出的手势,是需要存储以及在必要的利用时加载取出手势.所 以,用户绘制出的一个完整的手势是需要一定的代码 ...

  9. 【深入浅出jQuery】源码浅析2--使用技巧

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

随机推荐

  1. ShaderLab-坐标转换

    观察空间就是相机的空间 投影矩阵本质就是对x.y.z分量进行不同程度的缩放(z还做了平移),结果就是视锥体近切面远切面变成正方形.视锥体的中心在(0,0). (对于正交相机,这一步已经得到了立方体) ...

  2. 【五学x红小豆xRS】两边三地大联动-句型

    <第五共和国> Tohara LY Sara'm, Sabang Chua Setuk KS Kareh Moh Induree Junchi Chueh? 阁下!和这样的虫豸在一起,怎么 ...

  3. Reface.AppStarter 基本示例

    Reface.AppStarter 向应用层提供以下几项 核心 功能 以模块化组织你的应用程序 自动注册组件至 IOC 容器 自动映射配置文件至配置类 在模块定义类中额外追加组件至 IOC 容器 在模 ...

  4. OSCP Learning Notes - Netcat

    Introduction to Netcat Connecting va Listening Bind Shells Attacker connects to victim on listening ...

  5. OSCP Learning Notes - Exploit(8)

    Tools: 3. hydra Hydra v8.9.1 (c) 2019 by van Hauser/THC - Please do not use in military or secret se ...

  6. Ethical Hacking - GAINING ACCESS(7)

    Server Side Attacks - NEXPOSE NeXpose is a vulnerability management framework, it allows us to disco ...

  7. DJANGO-天天生鲜项目从0到1-001-环境框架搭建

    本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...

  8. CSS3多栏布局

    CSS3多栏布局 分栏数: column-count:auto|num: auto为默认值,表示元素只有一列.num取值为大于0的整数 每栏宽度: column-width:auto|<leng ...

  9. eclipse IDE usage of my own and tutorials link list

    设置 字符集 Eclipse 修改字符集 默认情况下 Eclipse 字符集为 GBK,但现在很多项目采用的是 UTF-8,这是我们就需要设置我们的 Eclipse 开发环境字符集为 UTF-8, 设 ...

  10. Java线程-- 线程池

    1.线程池概念 线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源. 为什么要使用线程池? 在java中,如果每个请求到达就创 ...