乐趣区

关于区块链: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 机制的细节进行源码级解析。

退出移动版