Bytom侧链Vapor源码浅析-节点出块过程
Bytom侧链Vapor源码浅析-节点出块过程
在这篇文章中,作者将从Vapor节点的创建开始,进而拓展讲解Vapor节点出块过程中所涉及的源码。
做为Vapor源码解析系列的第一篇,本文首先对Vapor稍加介绍。Vapor是目前国内主流公链Bytom的高性能侧链,是从Bytom主链中发展出来的一条独立的高性能侧链。Vapor是平台最重要的区块链基础设施之一,目前采用DPoS的共识算法,具有高性能、高安全、可扩展等特点,用于搭建规模化的商业应用。
Vapor节点创建及出块模块的启动
Vapor入口函数:
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节点的结构:
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字段
新建节点的部分源码
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启动
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个模块。
- 计算并调整出块的时间
- 通过
GetBlocker获取顺序下一个block的公钥,并与当前块比对,判断当前块的出块顺序是否合法。 - 通过
b.chain.ProcessBlock根据模板生成了一个block。 - 通过
chain.ProcessBlock(block)尝试把block加工处理后加到本机持有的区块链上。 - 使用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。 - 最后比对
consensusNodeMap中consensusNode.Order,并返回公钥。
这个模块是为了找出当前时间戳对应出块的共识节点,并返回节点的公钥。因为DPoS中出块的节点和顺序必须是固定的,而使用generateBlocks()模块尝试出块的共识节点不一定是当前时间的合法出块节点,因此需要本模块通过对比公钥进行节点资格的验证。
proposal.NewBlockTemplate
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
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是否为孤块。
- 通过block的hash判断这个block是否已经在链上。若已存在,则报错并返回false(表示该block不是孤块)
- 将block中的Transactions标记,后续会调用
c.knownTxs.Add()将Transactions加入到Transaction集合中。 - 判断是否为孤块,如果是,则调用孤块管理部分的模块处理并返回true。
- 保存block,在
saveBlock()中会对签名和区块进行验证。 bestBlockHeader.PreviousBlockHash == c.bestBlockHeader.Hash()的情况说明一切正常,新block被添加到链的末端。bestBlockHeader.Height > c.bestBlockHeader.Height表示出现了分叉,需要回滚。
总结
本篇文章从Vapor设置出块开始,到出块流程结束,细节层层解析节点设置出块和出块部分所涉及的源码。虽然本文至此篇幅已经比较长,但仍有重要的问题没有讲解清楚。例如,generateBlocks()中的第2点,程序会对出块的顺序进行查验,但这个出块的顺序是怎么获得还未做细致的解析。
那么,下一篇文章将针对Vapor中DPoS机制的细节进行源码级解析。
Bytom侧链Vapor源码浅析-节点出块过程的更多相关文章
- Android开发之Theme、Style探索及源码浅析
1 背景 前段时间群里有伙伴问到了关于Android开发中Theme与Style的问题,当然,这类东西在网上随便一搜一大把模板,所以关于怎么用的问题我想这里也就不做太多的说明了,我们这里把重点放在理解 ...
- spring源码浅析——IOC
=========================================== 原文链接: spring源码浅析--IOC 转载请注明出处! ======================= ...
- CountDownLatch源码浅析
Cmd Markdown链接 CountDownLatch源码浅析 参考好文: JDK1.8源码分析之CountDownLatch(五) Java并发之CountDownLatch源码分析 Count ...
- 【深入浅出jQuery】源码浅析--整体架构
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- 【深入浅出jQuery】源码浅析2--奇技淫巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- Struts2源码浅析-ConfigurationProvider
ConfigurationProvider接口 主要完成struts配置文件 加载 注册过程 ConfigurationProvider接口定义 public interface Configurat ...
- HashSet其实就那么一回事儿之源码浅析
上篇文章<HashMap其实就那么一回事儿之源码浅析>介绍了hashMap, 本次将带大家看看HashSet, HashSet其实就是基于HashMap实现, 因此,熟悉了HashMap ...
- Android 手势识别类 ( 三 ) GestureDetector 源码浅析
前言:上 篇介绍了提供手势绘制的视图平台GestureOverlayView,但是在视图平台上绘制出的手势,是需要存储以及在必要的利用时加载取出手势.所 以,用户绘制出的一个完整的手势是需要一定的代码 ...
- 【深入浅出jQuery】源码浅析2--使用技巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
随机推荐
- 01-flask虚拟环境
一.虚拟环境 每一个环境都相当于一个新的操作系统.你可以在这个新的环境里安装软件,保存资料.每一个虚拟环境可以安装不同版本的软件,在不同虚拟环境中运行不同版本的软件互不影响. 二.virtuale ...
- SQL注入环境的搭建
使用Phpstudy搭建SQL注入环境: 1.下载phpstudy安装 2.下载sql实验环境 所用环境的代码是一个印度人的开源项目平台.里面包含了基本的各种注入类型,同时又有get和post类型,以 ...
- Python 3基础教程8-if else语句
终于更新到了,教程8, 如果对python软件测试.接口测试.自动化测试.面试经验交流.感兴趣可以加君羊软件测试交流: 1079636098,还会有同行一起技术交流. 本文介绍if else语句,不多 ...
- JavaScript动画实例:粒子文本
1.粒子文本的实现原理 粒子文本的实现原理是:使用两张 canvas,一张是用户看不到的canvas1,用来绘制文本:另一张是用户看到的canvas2,用来根据canvas1中绘制的文本数据来生成粒子 ...
- threadLocal源码土话解说
前言 废话不多说,先了解什么是threadLocal,下面是threadLocal类的说明注释, 这段话大致(猜的)意思是,改类为线程提供了一个局部变量,但是呢,这个变量和普通的变量又有所不同,怎么不 ...
- vue : rem自适应的应用
我们知道,rem是一个css单位,指的是HTML根节点的字体大小. MDN:css单位 而我们在用rem布局的时候必然会遇到一个问题:我们需要根据用户的屏幕大小去计算rem. 以下是代码. (在VUE ...
- 设计模式:decade模式
目的:为系统中的一组联动接口提供一个高层次的接口,从而降低系统的复杂性 优点:使用窗口模式可以使得接口变少 继承关系图: 例子: class Subsystem1 { public: void Ope ...
- 设计模式:Iterator模式
目的:将数据的存储和数据的查询分开,降低数据的耦合性 继承关系图: 例子: //定义迭代器接口 template<typename T> class Iterator { public: ...
- [jvm] -- 常用内存参数配置篇
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ) Eden : from : to = 8 : 1 : 1 ( 可 ...
- [redis] -- 集群篇
三种集群方式 主从同步:主从复制模式中包含一个主数据库实例(master)与一个或多个从数据库实例(slave) 优点: master能自动将数据同步到slave,可以进行读写分离,分担master的 ...