在《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()中的主要步驟是:
- 先定義并初始化緩存择葡,包括主鏈上待移除的區(qū)塊detachBlocks刁岸、欲添加的側(cè)鏈上的區(qū)塊attachBlocks和待移除的區(qū)塊的spentTxOut集體detachSpentTxOuts,如代碼(1)處所示;
- 隨后酝碳,從數(shù)據(jù)庫中加載待移除區(qū)塊及它花費(fèi)的utxoentry疏哗、spentTxouts,并緩存下來芽偏,如代碼(2)污尉、(3)、(4)處所示;
- 代碼(5)處調(diào)用UtxoViewpoint的disconnectTransactions()對(duì)utxoset操作仿村,包括將交易產(chǎn)生的utxo清除和交易花費(fèi)的txout重新放回utxoset等锐朴,我們隨后進(jìn)一步分析;
- 接著,從數(shù)據(jù)庫中加載并實(shí)例化等添加的側(cè)鏈區(qū)塊蔼囊,并緩存下來包颁,如代碼(6)、(7)處所示;
- 代碼(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ì)分析蘑险,不再贅述;
- 上述檢查(主要是disconnectTransactions()和checkConnectBlock())均通過后滴肿,接下來開始第二階段的操作。代碼(9)處重新定義了一個(gè)UtxoViewpoint對(duì)象;
- 首先將待移除的區(qū)塊從鏈尾向前逐個(gè)從主鏈上移除佃迄,代碼(10)泼差、(11)處與(3)贵少、(5)處相同,只是在新的UtxoViewpoint中操作;
- 代碼(12)處調(diào)用disconnectBlock()將區(qū)塊從主鏈移除堆缘,值得注意的是滔灶,區(qū)塊并沒有從區(qū)塊文件中刪除,只是將其Meta從數(shù)據(jù)庫從刪除吼肥,我們隨后進(jìn)一步分析它;
- 區(qū)塊移除完成后录平,接著開始將原側(cè)鏈上的區(qū)塊逐個(gè)添加到主鏈。類似地缀皱,代碼(13)處從數(shù)據(jù)庫中將區(qū)塊花費(fèi)的utxo加載進(jìn)UtxoViewpoint中;
- 由于在驗(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;
- 最后啤斗,調(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稽揭,其主要步驟是:
- 首先對(duì)區(qū)塊花費(fèi)的交易數(shù)量作檢查,確保從數(shù)據(jù)庫中讀到的spentJournal與待處理的區(qū)塊是對(duì)應(yīng)的肥卡,如代碼(1)處所示;
- 隨后從最后一個(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)處查詢當(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中移除的目的;
- 代碼(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中;
- 隨后漩绵,根據(jù)spentTxout恢復(fù)utxoentry中的每一項(xiàng)輸出贱案,并將輸出的狀態(tài)設(shè)為unspent,如代碼(7)止吐、(8)處所示;
- 最后宝踪,處理完區(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)處比較等移除區(qū)塊的Hash值是否與鏈尾節(jié)點(diǎn)的Hash值相等厉膀,保證移除的是主鏈鏈尾節(jié)點(diǎn);
- 隨后,查找父區(qū)塊,并計(jì)算父區(qū)塊的MTP站蝠,blockSize及新的總交易數(shù)量等等,然后定義新的主鏈狀態(tài)卓鹿,如代碼(3)菱魔、(4)、(5)吟孙、(13)處所示;
- 接著澜倦,更新數(shù)據(jù)庫中主鏈狀態(tài)BestState,將區(qū)塊記錄從Bucket “hashidx”和“heightidx”中移除杰妓,更新utxoset藻治,移除區(qū)塊的spendJournal,從indexer中將區(qū)塊記錄移除巷挥;
- 接著桩卵,更新內(nèi)存中主鏈狀態(tài),如更新utxoentry倍宾、尾節(jié)點(diǎn)和主鏈狀態(tài)快照等等;
- 最后雏节,向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
}
其主要過程是:
- 先將超過“生存期”的區(qū)塊從“孤兒池”b.orphans和b.prevOrphans中移除楞件,“孤兒”的生存期為1個(gè)小時(shí)衫生,即區(qū)塊加入“孤兒池”滿一個(gè)小時(shí)后就應(yīng)該被移除;同時(shí)土浸,更新“年齡”最大的“孤兒”區(qū)塊罪针,如代碼(1)、(2)處所示;
- 將“年齡”滿1個(gè)小時(shí)的“孤兒”移除后黄伊,如果“孤兒”區(qū)塊數(shù)量還大于99泪酱,則最年長的“孤兒”移除,保證“孤兒池”大小不超過100,如代碼(3)處所示;
- 在清理完“孤兒池”后墓阀,代碼(4)處為區(qū)塊封裝orphanBlock對(duì)象毡惜,可以看到“孤兒”的生存期被設(shè)為1個(gè)小時(shí);
- 代碼(5)處將“孤兒”區(qū)塊添加到“孤兒池”中;
- 代碼(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勿锅,其主要過程是:
- 定義一個(gè)臨時(shí)的slice processHashes帕膜,用于緩存剛剛添加到區(qū)塊鏈上的區(qū)塊Hash,如代碼(1)處所示;
- 隨后溢十,循環(huán)處理所有以剛剛添加到區(qū)塊鏈上的區(qū)塊為父區(qū)塊的“孤兒”垮刹。首先,從processHashes中取出首元素张弛,并將首元素從processHashes中移除荒典,類似棧的pop操作,如代碼(2)乌庶、(3)處所示种蝶。這里采用for循環(huán)處理processHashes直到其為空,而沒有采用range操作瞒大,是由于processHashes中元素個(gè)數(shù)會(huì)在循環(huán)體中修改螃征,如代碼(7)處所示;
- 如果父區(qū)塊有“孤兒”,則循環(huán)處理所有“孤兒”透敌,如代碼(4)處所示;
- 在處理“孤兒”區(qū)塊時(shí)盯滚,首先將其從“孤兒池”中移除,接著調(diào)用maybeAcceptBlock()將其加入到區(qū)塊鏈上酗电,如代碼(5)魄藕、(6)處所示;
- 如果“孤兒”區(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é)婴洼。