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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350