Btcd區(qū)塊鏈的構(gòu)建(五)

《Btcd區(qū)塊鏈的構(gòu)建(三)》《Btcd區(qū)塊鏈的構(gòu)建(四)》中我們分析了區(qū)塊擴(kuò)展主鏈的完整過程恼蓬,本文將分析區(qū)塊擴(kuò)展側(cè)鏈且側(cè)鏈變主鏈的情況和“孤兒”區(qū)塊加入?yún)^(qū)塊鏈的過程处硬。當(dāng)側(cè)鏈變成主鏈時(shí)荷辕,側(cè)鏈上的區(qū)塊將被加入到主鏈上疮方,同時(shí)分叉點(diǎn)后主鏈上的區(qū)塊將被從主鏈移除骡显。側(cè)鏈上的區(qū)塊加到主鏈上的過程與我們前面分析的區(qū)塊擴(kuò)展主鏈的過程一樣惫谤;區(qū)塊從主鏈上移除時(shí)溜歪,與區(qū)塊加入到主鏈上相反痹愚,區(qū)塊中交易花費(fèi)的utxos(通過spendjournal記錄)將重新回到utxoset中拯腮,交易輸出的utxo則被從utxoset中刪除动壤。同時(shí)琼懊,當(dāng)區(qū)塊加入主鏈或者側(cè)鏈后哼丈,如果有“孤兒”區(qū)塊的父區(qū)塊是該區(qū)塊醉旦,則還要將“孤兒”區(qū)塊添加到區(qū)塊鏈上车胡,并從“孤兒”池中移出匈棘。

我們?cè)诜治鯿onnectBestChain()時(shí)看到主卫,如果區(qū)塊的父區(qū)塊不是主鏈上的“尾節(jié)點(diǎn)”队秩,則它將被加入到側(cè)鏈上馍资。如果側(cè)鏈擴(kuò)展后的工作量大于主鏈的工作量鸟蟹,則側(cè)鏈要通過reorganizeChain()變成主鏈建钥。在調(diào)用reorganizeChain()之前 熊经,要通過getReorganizeNodes()方法找到主鏈上分叉點(diǎn)之后的所有區(qū)塊和側(cè)鏈上所有區(qū)塊,我們先來看看它的實(shí)現(xiàn)槐壳。

//btcd/blockchain/chain.go

// getReorganizeNodes finds the fork point between the main chain and the passed
// node and returns a list of block nodes that would need to be detached from
// the main chain and a list of block nodes that would need to be attached to
// the fork point (which will be the end of the main chain after detaching the
// returned list of block nodes) in order to reorganize the chain such that the
// passed node is the new end of the main chain.  The lists will be empty if the
// passed node is not on a side chain.
//
// This function MUST be called with the chain state lock held (for reads).
func (b *BlockChain) getReorganizeNodes(node *blockNode) (*list.List, *list.List) {
    // Nothing to detach or attach if there is no node.
    attachNodes := list.New()
    detachNodes := list.New()
    if node == nil {
        return detachNodes, attachNodes
    }

    // Find the fork point (if any) adding each block to the list of nodes
    // to attach to the main tree.  Push them onto the list in reverse order
    // so they are attached in the appropriate order when iterating the list
    // later.
    ancestor := node
    for ; ancestor.parent != nil; ancestor = ancestor.parent {                     (1)
        if ancestor.inMainChain {
            break
        }
        attachNodes.PushFront(ancestor)
    }

    // TODO(davec): Use prevNodeFromNode function in case the requested
    // node is further back than the what is in memory.  This shouldn't
    // happen in the normal course of operation, but the ability to fetch
    // input transactions of arbitrary blocks will likely to be exposed at
    // some point and that could lead to an issue here.

    // Start from the end of the main chain and work backwards until the
    // common ancestor adding each block to the list of nodes to detach from
    // the main chain.
    for n := b.bestNode; n != nil && n.parent != nil; n = n.parent {               (2)
        if n.hash.IsEqual(&ancestor.hash) {
            break
        }
        detachNodes.PushBack(n)
    }

    return detachNodes, attachNodes
}

getReorganizeNodes()的實(shí)現(xiàn)比較簡單,首先從側(cè)鏈的鏈尾(剛剛加入的區(qū)塊)向前遍歷枫笛,直到主鏈與側(cè)鏈的分叉點(diǎn)刑巧,并將側(cè)鏈上的所有區(qū)塊節(jié)點(diǎn)按原來的順序添加到attachNodes中海诲,這樣可以保證這些區(qū)塊按其先后順序依次加入主鏈特幔;接著從主鏈的鏈尾向前遍歷直到分叉點(diǎn)蚯斯,并將訪問到的區(qū)塊節(jié)點(diǎn)按相反的順序添加到detachNodes中拍嵌,即鏈尾節(jié)點(diǎn)是detachNodes的第一個(gè)元素横辆,這樣方便從鏈尾到分叉點(diǎn)依次從主鏈上移除狈蚤。

細(xì)心的讀者可能發(fā)現(xiàn)锌畸,在向前遍歷側(cè)鏈或者主鏈時(shí)潭枣,父節(jié)點(diǎn)均直接訪問內(nèi)存中的blockNode對(duì)象而沒有從數(shù)據(jù)庫中加載盆犁,那么在向前遍歷區(qū)塊鏈至分叉點(diǎn)時(shí)會(huì)出現(xiàn)區(qū)塊未實(shí)例化為blockNode的情況嗎蚣抗?我們?cè)谇懊娣治鰉aybeAcceptBlock()時(shí)看到翰铡,它會(huì)調(diào)用blockIndex的PrevNodeFromBlock()方法為新區(qū)塊和其父區(qū)塊構(gòu)造blockNode對(duì)象锭魔,并在新區(qū)塊加入側(cè)鏈或者主鏈時(shí)將其添加到blockIndex中以供后續(xù)查找迷捧。所以漠秋,Btcd節(jié)點(diǎn)啟動(dòng)后收到的新的區(qū)塊捅位,如果不是孤兒區(qū)塊艇搀,只要加入了區(qū)塊鏈焰雕,均會(huì)在內(nèi)存中有實(shí)例化的blockNode對(duì)象矩屁。也就是說档插,節(jié)點(diǎn)啟動(dòng)后,內(nèi)存中為所有加入主鏈或者側(cè)鏈的區(qū)塊保留blockNode對(duì)象氛悬。我們?cè)凇禕tcd區(qū)塊鏈的構(gòu)建(三)》中介紹過blockNode的定義,它的size不到200個(gè)字節(jié)镜遣,假如節(jié)點(diǎn)新同步的區(qū)塊有100萬個(gè)悲关,則需要占用約190M的內(nèi)存寓辱。然而秫筏,如果節(jié)點(diǎn)停止后再重啟这敬,已經(jīng)存入數(shù)據(jù)庫的區(qū)塊不會(huì)再全部實(shí)例化阳掐,而是按需加載到內(nèi)存中锚烦。我們?cè)赾onnectBlock()中分析過涮俄,只有當(dāng)區(qū)塊加入主鏈時(shí)彻亲,區(qū)塊相關(guān)的MetaData才被寫入數(shù)據(jù)庫苞尝,即數(shù)據(jù)庫中只記錄主鏈相關(guān)的Meta,那么節(jié)點(diǎn)重啟后利赋,側(cè)鏈上的區(qū)塊和“孤兒”區(qū)塊的Meta信息將會(huì)丟失。假如節(jié)點(diǎn)停止后重啟,同步到一個(gè)區(qū)塊導(dǎo)致主鏈分叉啤月,且分叉點(diǎn)離主鏈鏈尾較遠(yuǎn),若一段時(shí)間后側(cè)鏈要變成主鏈强重,那么主鏈鏈尾節(jié)點(diǎn)到分叉點(diǎn)的區(qū)塊不一定都有實(shí)例化的blockNode對(duì)象间景,這將導(dǎo)致getReorganizeNodes()中代碼(2)處沒有記錄完整的待移除區(qū)塊的集合圾亏,會(huì)使得節(jié)點(diǎn)上區(qū)塊鏈的狀態(tài)與網(wǎng)絡(luò)不一致志鹃。盡管出現(xiàn)這種情形的概率較小曹铃,而且節(jié)點(diǎn)出錯(cuò)后會(huì)被網(wǎng)絡(luò)逐步糾正,這里仍然建議使用prevNodeFromNode()來獲取父區(qū)塊的blockNode對(duì)象评甜。

getReorganizeNodes()收集完待移除和待添加的區(qū)塊節(jié)點(diǎn)后忍坷,由reorganizeChain()完成側(cè)鏈變成主鏈的過程。

//btcd/blockchain/chain.go

// reorganizeChain reorganizes the block chain by disconnecting the nodes in the
// detachNodes list and connecting the nodes in the attach list.  It expects
// that the lists are already in the correct order and are in sync with the
// end of the current best chain.  Specifically, nodes that are being
// disconnected must be in reverse order (think of popping them off the end of
// the chain) and nodes the are being attached must be in forwards order
// (think pushing them onto the end of the chain).
//
// The flags modify the behavior of this function as follows:
//  - BFDryRun: Only the checks which ensure the reorganize can be completed
//    successfully are performed.  The chain is not reorganized.
//
// This function MUST be called with the chain state lock held (for writes).
func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List, flags BehaviorFlags) error {
    // All of the blocks to detach and related spend journal entries needed
    // to unspend transaction outputs in the blocks being disconnected must
    // be loaded from the database during the reorg check phase below and
    // then they are needed again when doing the actual database updates.
    // Rather than doing two loads, cache the loaded data into these slices.
    detachBlocks := make([]*btcutil.Block, 0, detachNodes.Len())                      (1)
    detachSpentTxOuts := make([][]spentTxOut, 0, detachNodes.Len())
    attachBlocks := make([]*btcutil.Block, 0, attachNodes.Len())

    // Disconnect all of the blocks back to the point of the fork.  This
    // entails loading the blocks and their associated spent txos from the
    // database and using that information to unspend all of the spent txos
    // and remove the utxos created by the blocks.
    view := NewUtxoViewpoint()
    view.SetBestHash(&b.bestNode.hash)
    for e := detachNodes.Front(); e != nil; e = e.Next() {
        n := e.Value.(*blockNode)
        var block *btcutil.Block
        err := b.db.View(func(dbTx database.Tx) error {
            var err error
            block, err = dbFetchBlockByHash(dbTx, &n.hash)                            (2)
            return err
        })
        if err != nil {
            return err
        }

        // Load all of the utxos referenced by the block that aren't
        // already in the view.
        err = view.fetchInputUtxos(b.db, block)                                       (3)
        if err != nil {
            return err
        }

        // Load all of the spent txos for the block from the spend
        // journal.
        var stxos []spentTxOut
        err = b.db.View(func(dbTx database.Tx) error {
            stxos, err = dbFetchSpendJournalEntry(dbTx, block, view)                  (4)
            return err
        })
        if err != nil {
            return err
        }

        // Store the loaded block and spend journal entry for later.
        detachBlocks = append(detachBlocks, block)
        detachSpentTxOuts = append(detachSpentTxOuts, stxos)

        err = view.disconnectTransactions(block, stxos)                               (5)
        if err != nil {
            return err
        }
    }

    // Perform several checks to verify each block that needs to be attached
    // to the main chain can be connected without violating any rules and
    // without actually connecting the block.
    //
    // NOTE: These checks could be done directly when connecting a block,
    // however the downside to that approach is that if any of these checks
    // fail after disconnecting some blocks or attaching others, all of the
    // operations have to be rolled back to get the chain back into the
    // state it was before the rule violation (or other failure).  There are
    // at least a couple of ways accomplish that rollback, but both involve
    // tweaking the chain and/or database.  This approach catches these
    // issues before ever modifying the chain.
    for e := attachNodes.Front(); e != nil; e = e.Next() {
        n := e.Value.(*blockNode)
        var block *btcutil.Block
        err := b.db.View(func(dbTx database.Tx) error {
            // NOTE: This block is not in the main chain, so the
            // block has to be loaded directly from the database
            // instead of using the dbFetchBlockByHash function.
            blockBytes, err := dbTx.FetchBlock(&n.hash)                               (6)
            if err != nil {
                return err
            }

            block, err = btcutil.NewBlockFromBytes(blockBytes)                        (7)
            if err != nil {
                return err
            }
            block.SetHeight(n.height)
            return nil
        })
        if err != nil {
            return err
        }

        // Store the loaded block for later.
        attachBlocks = append(attachBlocks, block)

        // Notice the spent txout details are not requested here and
        // thus will not be generated.  This is done because the state
        // is not being immediately written to the database, so it is
        // not needed.
        err = b.checkConnectBlock(n, block, view, nil)                                (8)
        if err != nil {
            return err
        }
    }

    ......

    // Reset the view for the actual connection code below.  This is
    // required because the view was previously modified when checking if
    // the reorg would be successful and the connection code requires the
    // view to be valid from the viewpoint of each block being connected or
    // disconnected.
    view = NewUtxoViewpoint()                                                         (9)
    view.SetBestHash(&b.bestNode.hash)

    // Disconnect blocks from the main chain.
    for i, e := 0, detachNodes.Front(); e != nil; i, e = i+1, e.Next() {
        n := e.Value.(*blockNode)
        block := detachBlocks[i]

        // Load all of the utxos referenced by the block that aren't
        // already in the view.
        err := view.fetchInputUtxos(b.db, block)                                      (10)
        if err != nil {
            return err
        }

        // Update the view to unspend all of the spent txos and remove
        // the utxos created by the block.
        err = view.disconnectTransactions(block, detachSpentTxOuts[i])                (11)
        if err != nil {
            return err
        }

        // Update the database and chain state.
        err = b.disconnectBlock(n, block, view)                                       (12)
        if err != nil {
            return err
        }
    }

    // Connect the new best chain blocks.
    for i, e := 0, attachNodes.Front(); e != nil; i, e = i+1, e.Next() {
        n := e.Value.(*blockNode)
        block := attachBlocks[i]

        // Load all of the utxos referenced by the block that aren't
        // already in the view.
        err := view.fetchInputUtxos(b.db, block)                                      (13) 
        if err != nil {
            return err
        }

        // Update the view to mark all utxos referenced by the block
        // as spent and add all transactions being created by this block
        // to it.  Also, provide an stxo slice so the spent txout
        // details are generated.
        stxos := make([]spentTxOut, 0, countSpentOutputs(block))
        err = view.connectTransactions(block, &stxos)                                 (14)
        if err != nil {
            return err
        }

        // Update the database and chain state.
        err = b.connectBlock(n, block, view, stxos)                                   (15)
        if err != nil {
            return err
        }
    }

    ......

    return nil
}

reorganizeChain()在進(jìn)行主鏈切換時(shí)分為兩個(gè)階段: 一是對(duì)切換過程中涉及到的block隶症、transaction及utxoset操作進(jìn)行驗(yàn)證;二是進(jìn)行真正的移除和添加操作胁住。沒有選擇邊檢查邊移除或者邊添加區(qū)塊的操作彪见,是為了防止中間步驟驗(yàn)證失敗導(dǎo)致整個(gè)切換過程需要回滾余指。這兩個(gè)階段分別使用了不同的UtxoViewpoint碉碉,所以其中涉及到對(duì)utxoset的操作互不影響垢粮。在驗(yàn)證或者操作階段足丢,均需要訪問待移除或者待添加的區(qū)塊信息和待移除區(qū)塊的spendJournal,它們可能需要從數(shù)據(jù)庫中讀取耀鸦,所以getReorganizeNodes()對(duì)這些信息進(jìn)行了緩存袖订。同時(shí),在切換過程中楞艾,總是先從主鏈移除區(qū)塊硫眯,再添加原側(cè)鏈上的區(qū)塊两入。

reorganizeChain()中的主要步驟是:

  1. 先定義并初始化緩存择葡,包括主鏈上待移除的區(qū)塊detachBlocks刁岸、欲添加的側(cè)鏈上的區(qū)塊attachBlocks和待移除的區(qū)塊的spentTxOut集體detachSpentTxOuts,如代碼(1)處所示;
  2. 隨后酝碳,從數(shù)據(jù)庫中加載待移除區(qū)塊及它花費(fèi)的utxoentry疏哗、spentTxouts,并緩存下來芽偏,如代碼(2)污尉、(3)、(4)處所示;
  3. 代碼(5)處調(diào)用UtxoViewpoint的disconnectTransactions()對(duì)utxoset操作仿村,包括將交易產(chǎn)生的utxo清除和交易花費(fèi)的txout重新放回utxoset等锐朴,我們隨后進(jìn)一步分析;
  4. 接著,從數(shù)據(jù)庫中加載并實(shí)例化等添加的側(cè)鏈區(qū)塊蔼囊,并緩存下來包颁,如代碼(6)、(7)處所示;
  5. 代碼(8)處調(diào)用checkConnectBlock()對(duì)區(qū)塊連入主鏈時(shí)的各種條件作最后檢查压真,我們?cè)谠?a href="http://www.reibang.com/p/06fe9de09172" target="_blank">《Btcd區(qū)塊鏈的構(gòu)建(三)》和《Btcd區(qū)塊鏈的構(gòu)建(四)》中對(duì)這一過程有過詳細(xì)分析蘑险,不再贅述;
  6. 上述檢查(主要是disconnectTransactions()和checkConnectBlock())均通過后滴肿,接下來開始第二階段的操作。代碼(9)處重新定義了一個(gè)UtxoViewpoint對(duì)象;
  7. 首先將待移除的區(qū)塊從鏈尾向前逐個(gè)從主鏈上移除佃迄,代碼(10)泼差、(11)處與(3)贵少、(5)處相同,只是在新的UtxoViewpoint中操作;
  8. 代碼(12)處調(diào)用disconnectBlock()將區(qū)塊從主鏈移除堆缘,值得注意的是滔灶,區(qū)塊并沒有從區(qū)塊文件中刪除,只是將其Meta從數(shù)據(jù)庫從刪除吼肥,我們隨后進(jìn)一步分析它;
  9. 區(qū)塊移除完成后录平,接著開始將原側(cè)鏈上的區(qū)塊逐個(gè)添加到主鏈。類似地缀皱,代碼(13)處從數(shù)據(jù)庫中將區(qū)塊花費(fèi)的utxo加載進(jìn)UtxoViewpoint中;
  10. 由于在驗(yàn)證階段已經(jīng)調(diào)用過checkConnectBlock()(代碼(8)處)對(duì)區(qū)塊上下文做過檢查斗这,這里不用再作完整性檢查,只是調(diào)用UtxoViewpoint的connectTransactions()將區(qū)塊花費(fèi)的utxo設(shè)為已花費(fèi)并記錄區(qū)塊花費(fèi)的所有spentTxOut;
  11. 最后啤斗,調(diào)用connectBlock()將區(qū)塊寫入主鏈表箭,更新相關(guān)Meta到數(shù)據(jù)庫,與我們前面分析區(qū)塊加入主鏈的過程一樣;

側(cè)鏈切換成主鏈前后區(qū)塊鏈的狀態(tài)如下圖示意:

其中钮莲,黑色框示意主鏈中的區(qū)塊免钻,淺綠色框示意側(cè)鏈上的區(qū)塊。值得注意的是崔拥,側(cè)鏈只存于內(nèi)存极舔,而不會(huì)寫入數(shù)據(jù)庫。

接下來握童,我們看看disconnectTransactions()的實(shí)現(xiàn)姆怪,它與我們前面分析過的connectTransaction()相反。

//btcd/blockchain/utxoviewpoint.go

// disconnectTransactions updates the view by removing all of the transactions
// created by the passed block, restoring all utxos the transactions spent by
// using the provided spent txo information, and setting the best hash for the
// view to the block before the passed block.
func (view *UtxoViewpoint) disconnectTransactions(block *btcutil.Block, stxos []spentTxOut) error {
    // Sanity check the correct number of stxos are provided.
    if len(stxos) != countSpentOutputs(block) {                                    (1)
        return AssertError("disconnectTransactions called with bad " +
            "spent transaction out information")
    }

    // Loop backwards through all transactions so everything is unspent in
    // reverse order.  This is necessary since transactions later in a block
    // can spend from previous ones.
    stxoIdx := len(stxos) - 1
    transactions := block.Transactions()
    for txIdx := len(transactions) - 1; txIdx > -1; txIdx-- {                      (2)     
        tx := transactions[txIdx]

        // Clear this transaction from the view if it already exists or
        // create a new empty entry for when it does not.  This is done
        // because the code relies on its existence in the view in order
        // to signal modifications have happened.
        isCoinbase := txIdx == 0
        entry := view.entries[*tx.Hash()]                                          (3)
        if entry == nil {
            entry = newUtxoEntry(tx.MsgTx().Version, isCoinbase, 
                block.Height())
            view.entries[*tx.Hash()] = entry
        }
        entry.modified = true                                                      (4)
        entry.sparseOutputs = make(map[uint32]*utxoOutput)

        // Loop backwards through all of the transaction inputs (except
        // for the coinbase which has no inputs) and unspend the
        // referenced txos.  This is necessary to match the order of the
        // spent txout entries.
        if isCoinbase {
            continue
        }
        for txInIdx := len(tx.MsgTx().TxIn) - 1; txInIdx > -1; txInIdx-- {         (5)
            // Ensure the spent txout index is decremented to stay
            // in sync with the transaction input.
            stxo := &stxos[stxoIdx]
            stxoIdx--

            // When there is not already an entry for the referenced
            // transaction in the view, it means it was fully spent,
            // so create a new utxo entry in order to resurrect it.
            txIn := tx.MsgTx().TxIn[txInIdx]
            originHash := &txIn.PreviousOutPoint.Hash
            originIndex := txIn.PreviousOutPoint.Index
            entry := view.entries[*originHash]                                     (6)
            if entry == nil {                                                      
                entry = newUtxoEntry(stxo.version,
                    stxo.isCoinBase, stxo.height)
                view.entries[*originHash] = entry
            }

            // Mark the entry as modified since it is either new
            // or will be changed below.
            entry.modified = true

            // Restore the specific utxo using the stxo data from
            // the spend journal if it doesn't already exist in the
            // view.
            output, ok := entry.sparseOutputs[originIndex]                         (7)            
            if !ok {
                // Add the unspent transaction output.
                entry.sparseOutputs[originIndex] = &utxoOutput{
                    spent:      false,
                    compressed: stxo.compressed,
                    amount:     stxo.amount,
                    pkScript:   stxo.pkScript,
                }
                continue
            }

            // Mark the existing referenced transaction output as
            // unspent.
            output.spent = false                                                   (8)
        }
    }

    // Update the best hash for view to the previous block since all of the
    // transactions for the current block have been disconnected.
    view.SetBestHash(&block.MsgBlock().Header.PrevBlock)                           (9)
    return nil
}

disconnectTransactions()主要是將區(qū)塊中的交易從utxoset中移除澡绩,并將交易已經(jīng)花費(fèi)的所有spentTxOut重設(shè)為unspent稽揭,其主要步驟是:

  1. 首先對(duì)區(qū)塊花費(fèi)的交易數(shù)量作檢查,確保從數(shù)據(jù)庫中讀到的spentJournal與待處理的區(qū)塊是對(duì)應(yīng)的肥卡,如代碼(1)處所示;
  2. 隨后從最后一個(gè)交易開始往前遍歷處理區(qū)塊中的交易溪掀,之所以從最后一個(gè)開始處理,是因?yàn)楹竺娴慕灰卓赡芑ㄙM(fèi)前面的交易步鉴,在處理的過程中揪胃,要把交易本身從utxoset中移除,同時(shí)將已經(jīng)花費(fèi)的交易恢復(fù)到utxoset中,如果排在前面的交易被后面的交易花費(fèi)氛琢,從前向后處理時(shí)喊递,前面的交易可能先被從utxoset中移除,處理后面的交易時(shí)又被恢復(fù)到utxoset中阳似,導(dǎo)致utxoset不正確骚勘。由于交易按“倒序”訪問,處理交易中的輸入時(shí)也是按“倒序”處理的,如代碼(5)處所示;
  3. 代碼(3)處查詢當(dāng)前交易是否在utxoset中俏讹,如果不在当宴,則為其創(chuàng)建一個(gè)空的utxoentry,并將其modified狀態(tài)設(shè)為true泽疆,如代碼(4)處所示户矢。請(qǐng)注意,在調(diào)用disconnectTransactions()之前殉疼,調(diào)用fetchInputUtxos()將區(qū)塊中交易輸入引用的utxoentry加載到UtxoViewpoint中梯浪,除非交易花費(fèi)了同一區(qū)塊中排在前面的交易,當(dāng)前處理的交易并不會(huì)存在于view.entries中株依,所以代碼(4)處為當(dāng)前交易創(chuàng)建一個(gè)utxoentry并將其添加到view.entries中驱证,同時(shí)將其輸出清空,這樣在connectBlock()或者disconnectBlock()中向數(shù)據(jù)庫更新utxoset時(shí)會(huì)將其從數(shù)據(jù)庫中刪除恋腕,達(dá)到將交易從utxoset中移除的目的;
  4. 代碼(5)處開始“倒序”處理交易中的輸入;
  5. 代碼(6)處查詢交易輸入引用的utxoentry是否在view.entries中抹锄,fetchInputUtxos()已經(jīng)將區(qū)塊中交易輸入引用的utxoentry加載到UtxoViewpoint中了,還會(huì)出現(xiàn)查詢結(jié)果為空的情況嗎荠藤?如果交易的輸出全部被花費(fèi)伙单,則其對(duì)應(yīng)的utxoentry將被從utxoset中移除,加載到UtxoViewpoint中后哈肖,view.entries中該記錄為nil(請(qǐng)注意吻育,view.entries中該記錄,只是其值為nil)淤井。所以布疼,當(dāng)查詢?yōu)榭諘r(shí),說明引用的交易被完全花費(fèi)了币狠,這時(shí)游两,為其創(chuàng)建新的utxoentry并添加到utxoset中,即將其恢復(fù)到utxoset中;
  6. 隨后漩绵,根據(jù)spentTxout恢復(fù)utxoentry中的每一項(xiàng)輸出贱案,并將輸出的狀態(tài)設(shè)為unspent,如代碼(7)止吐、(8)處所示;
  7. 最后宝踪,處理完區(qū)塊中所有交易后,將UtxoViewpoint的觀察點(diǎn)移向父區(qū)塊;

上一篇文章中我們分析過connectBlock()碍扔,它實(shí)現(xiàn)將區(qū)塊最終寫入主鏈瘩燥;與之對(duì)應(yīng)地,區(qū)塊最終從主鏈移除是在disconnectBlock()中實(shí)現(xiàn)的不同。

//btcd/blockchain/chain.go

// disconnectBlock handles disconnecting the passed node/block from the end of
// the main (best) chain.
//
// This function MUST be called with the chain state lock held (for writes).
func (b *BlockChain) disconnectBlock(node *blockNode, block *btcutil.Block, view *UtxoViewpoint) error {
    // Make sure the node being disconnected is the end of the best chain.
    if !node.hash.IsEqual(&b.bestNode.hash) {                                       (1)
        return AssertError("disconnectBlock must be called with the " +
            "block at the end of the main chain")
    }

    // Get the previous block node.  This function is used over simply
    // accessing node.parent directly as it will dynamically create previous
    // block nodes as needed.  This helps allow only the pieces of the chain
    // that are needed to remain in memory.
    prevNode, err := b.index.PrevNodeFromNode(node)                                 (2)
    if err != nil {
        return err
    }

    // Calculate the median time for the previous block.
    medianTime, err := b.index.CalcPastMedianTime(prevNode)                         (3)
    if err != nil {
        return err
    }

    // Load the previous block since some details for it are needed below.
    var prevBlock *btcutil.Block
    err = b.db.View(func(dbTx database.Tx) error {
        var err error
        prevBlock, err = dbFetchBlockByHash(dbTx, &prevNode.hash)                   (4)
        return err
    })
    if err != nil {
        return err
    }

    // Generate a new best state snapshot that will be used to update the
    // database and later memory if all database updates are successful.
    b.stateLock.RLock()
    curTotalTxns := b.stateSnapshot.TotalTxns
    b.stateLock.RUnlock()
    numTxns := uint64(len(prevBlock.MsgBlock().Transactions))
    blockSize := uint64(prevBlock.MsgBlock().SerializeSize())
    newTotalTxns := curTotalTxns - uint64(len(block.MsgBlock().Transactions))
    state := newBestState(prevNode, blockSize, numTxns, newTotalTxns,               (5)
        medianTime)

    err = b.db.Update(func(dbTx database.Tx) error {
        // Update best block state.
        err := dbPutBestState(dbTx, state, node.workSum)                            (6)
        if err != nil {
            return err
        }

        // Remove the block hash and height from the block index which
        // tracks the main chain.
        err = dbRemoveBlockIndex(dbTx, block.Hash(), node.height)                   (7)
        if err != nil {
            return err
        }

        // Update the utxo set using the state of the utxo view.  This
        // entails restoring all of the utxos spent and removing the new
        // ones created by the block.
        err = dbPutUtxoView(dbTx, view)                                             (8)
        if err != nil {
            return err
        }

        // Update the transaction spend journal by removing the record
        // that contains all txos spent by the block .
        err = dbRemoveSpendJournalEntry(dbTx, block.Hash())                         (9)
        if err != nil {
            return err
        }

        // Allow the index manager to call each of the currently active
        // optional indexes with the block being disconnected so they
        // can update themselves accordingly.
        if b.indexManager != nil {
            err := b.indexManager.DisconnectBlock(dbTx, block, view)                (10)
            if err != nil {
                return err
            }
        }

        return nil
    })
    if err != nil {
        return err
    }

    // Prune fully spent entries and mark all entries in the view unmodified
    // now that the modifications have been committed to the database.
    view.commit()                                                                   (11)

    // Mark block as being in a side chain.
    node.inMainChain = false

    // This node's parent is now the end of the best chain.
    b.bestNode = node.parent                                                        (12)

    // Update the state for the best block.  Notice how this replaces the
    // entire struct instead of updating the existing one.  This effectively
    // allows the old version to act as a snapshot which callers can use
    // freely without needing to hold a lock for the duration.  See the
    // comments on the state variable for more details.
    b.stateLock.Lock()
    b.stateSnapshot = state                                                         (13)
    b.stateLock.Unlock()

    // Notify the caller that the block was disconnected from the main
    // chain.  The caller would typically want to react with actions such as
    // updating wallets.
    b.chainLock.Unlock()
    b.sendNotification(NTBlockDisconnected, block)                                  (14)
    b.chainLock.Lock()

    return nil
}

其主要步驟是:

  1. 代碼(1)處比較等移除區(qū)塊的Hash值是否與鏈尾節(jié)點(diǎn)的Hash值相等厉膀,保證移除的是主鏈鏈尾節(jié)點(diǎn);
  2. 隨后,查找父區(qū)塊,并計(jì)算父區(qū)塊的MTP站蝠,blockSize及新的總交易數(shù)量等等,然后定義新的主鏈狀態(tài)卓鹿,如代碼(3)菱魔、(4)、(5)吟孙、(13)處所示;
  3. 接著澜倦,更新數(shù)據(jù)庫中主鏈狀態(tài)BestState,將區(qū)塊記錄從Bucket “hashidx”和“heightidx”中移除杰妓,更新utxoset藻治,移除區(qū)塊的spendJournal,從indexer中將區(qū)塊記錄移除巷挥;
  4. 接著桩卵,更新內(nèi)存中主鏈狀態(tài),如更新utxoentry倍宾、尾節(jié)點(diǎn)和主鏈狀態(tài)快照等等;
  5. 最后雏节,向blockManager通知NTBlockDisconnected事件,將區(qū)塊中的交易重新放回mempool;

到此高职,我們就分析了側(cè)鏈切換成主鏈的完整過程钩乍,其中比較關(guān)鍵的是伴隨區(qū)塊移除或者添加到主鏈上時(shí)數(shù)據(jù)庫和內(nèi)存中utxoset的變化,其中涉及到的驗(yàn)證過程與我們前面分析的一樣怔锌。區(qū)塊擴(kuò)展主鏈(包括擴(kuò)展側(cè)鏈且側(cè)鏈切換成主鏈)或者擴(kuò)展側(cè)鏈后寥粹,節(jié)點(diǎn)要檢查“孤兒”區(qū)塊池中是否有“孤兒”的父區(qū)塊就是剛剛加入?yún)^(qū)塊鏈的區(qū)塊,如果有則應(yīng)將“孤兒”移出“孤兒池”并添加到區(qū)塊鏈上埃元。對(duì)“孤兒”區(qū)塊的處理是在processOrphans()中實(shí)現(xiàn)的涝涤。同時(shí),我們?cè)?a href="http://www.reibang.com/p/4fcf5d77ef2f" target="_blank">《Btcd區(qū)塊鏈的構(gòu)建(一)》中分析ProcessBlock()時(shí)看到亚情,如果新區(qū)塊的父區(qū)塊不在區(qū)塊鏈中妄痪,則調(diào)用addOrphanBlock()將其加入到“孤兒”區(qū)塊池中。

//btcd/blockchain/chain.go

// addOrphanBlock adds the passed block (which is already determined to be
// an orphan prior calling this function) to the orphan pool.  It lazily cleans
// up any expired blocks so a separate cleanup poller doesn't need to be run.
// It also imposes a maximum limit on the number of outstanding orphan
// blocks and will remove the oldest received orphan block if the limit is
// exceeded.
func (b *BlockChain) addOrphanBlock(block *btcutil.Block) {
    // Remove expired orphan blocks.
    for _, oBlock := range b.orphans {
        if time.Now().After(oBlock.expiration) {                                             (1)
            b.removeOrphanBlock(oBlock)
            continue
        }

        // Update the oldest orphan block pointer so it can be discarded
        // in case the orphan pool fills up.
        if b.oldestOrphan == nil || oBlock.expiration.Before(b.oldestOrphan.expiration) {    (2)
            b.oldestOrphan = oBlock
        }
    }

    // Limit orphan blocks to prevent memory exhaustion.
    if len(b.orphans)+1 > maxOrphanBlocks {                                                  (3)
        // Remove the oldest orphan to make room for the new one.
        b.removeOrphanBlock(b.oldestOrphan)
        b.oldestOrphan = nil
    }

    // Protect concurrent access.  This is intentionally done here instead
    // of near the top since removeOrphanBlock does its own locking and
    // the range iterator is not invalidated by removing map entries.
    b.orphanLock.Lock()
    defer b.orphanLock.Unlock()

    // Insert the block into the orphan map with an expiration time
    // 1 hour from now.
    expiration := time.Now().Add(time.Hour)                                                  (4)
    oBlock := &orphanBlock{
        block:      block,
        expiration: expiration,
    }
    b.orphans[*block.Hash()] = oBlock                                                        (5)

    // Add to previous hash lookup index for faster dependency lookups.
    prevHash := &block.MsgBlock().Header.PrevBlock
    b.prevOrphans[*prevHash] = append(b.prevOrphans[*prevHash], oBlock)                      (6)

    return
}

其主要過程是:

  1. 先將超過“生存期”的區(qū)塊從“孤兒池”b.orphans和b.prevOrphans中移除楞件,“孤兒”的生存期為1個(gè)小時(shí)衫生,即區(qū)塊加入“孤兒池”滿一個(gè)小時(shí)后就應(yīng)該被移除;同時(shí)土浸,更新“年齡”最大的“孤兒”區(qū)塊罪针,如代碼(1)、(2)處所示;
  2. 將“年齡”滿1個(gè)小時(shí)的“孤兒”移除后黄伊,如果“孤兒”區(qū)塊數(shù)量還大于99泪酱,則最年長的“孤兒”移除,保證“孤兒池”大小不超過100,如代碼(3)處所示;
  3. 在清理完“孤兒池”后墓阀,代碼(4)處為區(qū)塊封裝orphanBlock對(duì)象毡惜,可以看到“孤兒”的生存期被設(shè)為1個(gè)小時(shí);
  4. 代碼(5)處將“孤兒”區(qū)塊添加到“孤兒池”中;
  5. 代碼(6)處將“孤兒”區(qū)塊添加到b.prevOrphans中,b.prevOrphans以“孤兒”區(qū)塊父區(qū)塊Hash為Key記錄了同一父區(qū)塊的所有“孤兒”;

我們接下來分析processOrphans()的實(shí)現(xiàn)斯撮。

//btcd/blockchain/process.go

// processOrphans determines if there are any orphans which depend on the passed
// block hash (they are no longer orphans if true) and potentially accepts them.
// It repeats the process for the newly accepted blocks (to detect further
// orphans which may no longer be orphans) until there are no more.
//
// The flags do not modify the behavior of this function directly, however they
// are needed to pass along to maybeAcceptBlock.
//
// This function MUST be called with the chain state lock held (for writes).
func (b *BlockChain) processOrphans(hash *chainhash.Hash, flags BehaviorFlags) error {
    // Start with processing at least the passed hash.  Leave a little room
    // for additional orphan blocks that need to be processed without
    // needing to grow the array in the common case.
    processHashes := make([]*chainhash.Hash, 0, 10)
    processHashes = append(processHashes, hash)                                             (1)
    for len(processHashes) > 0 {
        // Pop the first hash to process from the slice.
        processHash := processHashes[0]                                                     (2)
        processHashes[0] = nil // Prevent GC leak.
        processHashes = processHashes[1:]                                                   (3)

        // Look up all orphans that are parented by the block we just
        // accepted.  This will typically only be one, but it could
        // be multiple if multiple blocks are mined and broadcast
        // around the same time.  The one with the most proof of work
        // will eventually win out.  An indexing for loop is
        // intentionally used over a range here as range does not
        // reevaluate the slice on each iteration nor does it adjust the
        // index for the modified slice.
        for i := 0; i < len(b.prevOrphans[*processHash]); i++ {                             (4)
            orphan := b.prevOrphans[*processHash][i]
            ......

            // Remove the orphan from the orphan pool.
            orphanHash := orphan.block.Hash()
            b.removeOrphanBlock(orphan)                                                     (5)
            i--

            // Potentially accept the block into the block chain.
            _, err := b.maybeAcceptBlock(orphan.block, flags)                               (6)
            if err != nil {
                return err
            }

            // Add this block to the list of blocks to process so
            // any orphan blocks that depend on this block are
            // handled too.
            processHashes = append(processHashes, orphanHash)                               (7)
        }
    }
    return nil
}

請(qǐng)注意经伙,processOrphans()的輸入?yún)?shù)hash是剛剛添加到區(qū)塊鏈上的區(qū)塊的Hash,而不是“孤兒”區(qū)塊的Hash勿锅,其主要過程是:

  1. 定義一個(gè)臨時(shí)的slice processHashes帕膜,用于緩存剛剛添加到區(qū)塊鏈上的區(qū)塊Hash,如代碼(1)處所示;
  2. 隨后溢十,循環(huán)處理所有以剛剛添加到區(qū)塊鏈上的區(qū)塊為父區(qū)塊的“孤兒”垮刹。首先,從processHashes中取出首元素张弛,并將首元素從processHashes中移除荒典,類似棧的pop操作,如代碼(2)乌庶、(3)處所示种蝶。這里采用for循環(huán)處理processHashes直到其為空,而沒有采用range操作瞒大,是由于processHashes中元素個(gè)數(shù)會(huì)在循環(huán)體中修改螃征,如代碼(7)處所示;
  3. 如果父區(qū)塊有“孤兒”,則循環(huán)處理所有“孤兒”透敌,如代碼(4)處所示;
  4. 在處理“孤兒”區(qū)塊時(shí)盯滚,首先將其從“孤兒池”中移除,接著調(diào)用maybeAcceptBlock()將其加入到區(qū)塊鏈上酗电,如代碼(5)魄藕、(6)處所示;
  5. 如果“孤兒”區(qū)塊正確加入?yún)^(qū)塊鏈,則將其添加到processHashes中撵术,繼續(xù)檢查是否還有以它為父區(qū)塊的“孤兒”背率,若有,則重復(fù)上述過程嫩与,直到新添加到區(qū)塊鏈中的區(qū)塊沒有“孤兒”子區(qū)塊為止;

節(jié)點(diǎn)處理“孤兒區(qū)塊”的過程如下圖示意:

圖中寝姿,黑色方框表示區(qū)塊鏈上的節(jié)點(diǎn),綠色方框表示剛剛添加到區(qū)塊鏈上的新節(jié)點(diǎn)划滋,紅色橢圓表示“孤兒池”饵筑,其中的粉色方框表示“孤兒”區(qū)塊。在步驟a)中处坪,區(qū)塊A是區(qū)塊B和C的父節(jié)點(diǎn)根资,但它們都是“孤兒”區(qū)塊架专,因?yàn)樗鼈兊母竻^(qū)塊都不在區(qū)塊鏈上。當(dāng)區(qū)塊A的父區(qū)塊(圖中綠色區(qū)塊)加入?yún)^(qū)鏈后玄帕,區(qū)塊A從“孤兒池”中移除部脚,并被添加到區(qū)塊鏈路上,如圖中步驟(b)所示裤纹;區(qū)塊A添加到區(qū)塊鏈上后睛低,進(jìn)一步處理它在“孤兒池”中的子區(qū)塊B和C,并分別將它們添加到區(qū)塊鏈上服傍,如步驟c)所示。區(qū)塊B和C中入?yún)^(qū)塊鏈后骂铁,由于“孤兒池”中沒有以他們?yōu)楦腹?jié)點(diǎn)的區(qū)塊了吹零,則結(jié)束對(duì)“孤兒”區(qū)塊的處理過程。

至此拉庵,我們完整地分析了ProcessBlock()中的各個(gè)步驟灿椅,它主要包括三個(gè)階段: 一是調(diào)用checkBlockSanity()對(duì)區(qū)塊進(jìn)行完整性檢查;二是調(diào)用maybeAcceptBlock()將區(qū)塊添加到區(qū)塊鏈上钞支;三是調(diào)用processOrphans()對(duì)“孤兒”區(qū)塊進(jìn)行處理茫蛹。其中,第二階段最為復(fù)雜烁挟,我們將在下一篇文章《Btcd區(qū)塊鏈的構(gòu)建(總結(jié)篇)》中對(duì)這一過程進(jìn)行回顧與總結(jié)婴洼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市撼嗓,隨后出現(xiàn)的幾起案子柬采,更是在濱河造成了極大的恐慌,老刑警劉巖且警,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粉捻,死亡現(xiàn)場離奇詭異,居然都是意外死亡斑芜,警方通過查閱死者的電腦和手機(jī)肩刃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杏头,“玉大人盈包,你說我怎么就攤上這事〈笾荩” “怎么了续语?”我有些...
    開封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長厦画。 經(jīng)常有香客問我疮茄,道長滥朱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任力试,我火速辦了婚禮徙邻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘畸裳。我一直安慰自己缰犁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開白布怖糊。 她就那樣靜靜地躺著帅容,像睡著了一般。 火紅的嫁衣襯著肌膚如雪伍伤。 梳的紋絲不亂的頭發(fā)上并徘,一...
    開封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音扰魂,去河邊找鬼麦乞。 笑死,一個(gè)胖子當(dāng)著我的面吹牛劝评,可吹牛的內(nèi)容都是我干的姐直。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼蒋畜,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼声畏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起姻成,我...
    開封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤砰识,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后佣渴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辫狼,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年辛润,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了膨处。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡砂竖,死狀恐怖真椿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情乎澄,我是刑警寧澤突硝,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站置济,受9級(jí)特大地震影響解恰,放射性物質(zhì)發(fā)生泄漏锋八。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一护盈、第九天 我趴在偏房一處隱蔽的房頂上張望挟纱。 院中可真熱鬧,春花似錦腐宋、人聲如沸紊服。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽欺嗤。三九已至,卻和暖如春卫枝,著一層夾襖步出監(jiān)牢的瞬間剂府,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人域帐。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓稚铣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親铡羡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子积蔚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容