From 31caca1da40522774e6f8ec33b0a2086355a603c Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 31 Jan 2018 03:43:41 -0600 Subject: [PATCH] blockchain: Generalize and optimize chain reorg. This generalizes and optimizes the reorganize chain function to take advantage of the linear structure of the nodes being attached and detached in order to avoid reloading the parent blocks as well as allow it to rewind the chain in the case no attach nodes are provided or extend the chain in the case no detach nodes are provided. It also adds several assertions to ensure the assumptions about the state hold and generally cleans up the function for code consistency. --- blockchain/chain.go | 143 +++++++++++++++++++++++++++++++------------- 1 file changed, 101 insertions(+), 42 deletions(-) diff --git a/blockchain/chain.go b/blockchain/chain.go index 1f01f936..793d5794 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -1197,8 +1197,32 @@ func countNumberOfTransactions(block, parent *dcrutil.Block) uint64 { // // This function MUST be called with the chain state lock held (for writes). func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List, flags BehaviorFlags) error { - formerBestHash := b.bestNode.hash - formerBestHeight := b.bestNode.height + // Nothing to do if no reorganize nodes were provided. + if detachNodes.Len() == 0 && attachNodes.Len() == 0 { + return nil + } + + // Ensure the provided nodes match the current best chain. + if detachNodes.Len() != 0 { + firstDetachNode := detachNodes.Front().Value.(*blockNode) + if firstDetachNode.hash != b.bestNode.hash { + return AssertError(fmt.Sprintf("reorganize nodes to detach are "+ + "not for the current best chain -- first detach node %v, "+ + "current chain %v", &firstDetachNode.hash, &b.bestNode.hash)) + } + } + + // Ensure the provided nodes are for the same fork point. + if attachNodes.Len() != 0 && detachNodes.Len() != 0 { + firstAttachNode := attachNodes.Front().Value.(*blockNode) + lastDetachNode := detachNodes.Back().Value.(*blockNode) + if firstAttachNode.parentHash != lastDetachNode.parentHash { + return AssertError(fmt.Sprintf("reorganize nodes do not have the "+ + "same fork point -- first attach parent %v, last detach "+ + "parent %v", &firstAttachNode.parentHash, + &lastDetachNode.parentHash)) + } + } // Ensure all of the needed side chain blocks are in the cache. for e := attachNodes.Front(); e != nil; e = e.Next() { @@ -1213,6 +1237,10 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List, flags } } + // Track the old and new best chains heads. + oldBest := b.bestNode + newBest := b.bestNode + // 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 @@ -1226,22 +1254,36 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List, flags // database and using that information to unspend all of the spent txos // and remove the utxos created by the blocks. view := NewUtxoViewpoint() - view.SetBestHash(&formerBestHash) + view.SetBestHash(&oldBest.hash) view.SetStakeViewpoint(ViewpointPrevValidInitial) - i := 0 + var nextBlockToDetach *dcrutil.Block for e := detachNodes.Front(); e != nil; e = e.Next() { + // Grab the block to detach based on the node. Use the fact that the + // blocks are being detached in reverse order, so the parent of the + // current block being detached is the next one being detached. n := e.Value.(*blockNode) - var block *dcrutil.Block - var parent *dcrutil.Block - var err error - block, err = b.fetchBlockFromHash(&n.hash) - if err != nil { - return err - } - parent, err = b.fetchBlockFromHash(&n.parentHash) + block := nextBlockToDetach + if block == nil { + var err error + block, err = b.fetchBlockFromHash(&n.hash) + if err != nil { + return err + } + } + if n.hash != *block.Hash() { + return AssertError(fmt.Sprintf("detach block node hash %v (height "+ + "%v) does not match previous parent block hash %v", &n.hash, + n.height, block.Hash())) + } + + // Grab the parent of the current block and also save a reference to it + // as the next block to detach so it doesn't need to be loaded again on + // the next iteration. + parent, err := b.fetchBlockFromHash(&n.parentHash) if err != nil { return err } + nextBlockToDetach = parent // Load all of the spent txos for the block from the spend // journal. @@ -1271,7 +1313,25 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List, flags return err } - i++ + newBest = n + } + + // Set the fork point and grab the fork block when there are nodes to be + // attached. The fork block is used as the parent to the first node to be + // attached below. + var forkNode *blockNode + var forkBlock *dcrutil.Block + if attachNodes.Len() > 0 { + var err error + forkNode, err = b.index.PrevNodeFromNode(newBest) + if err != nil { + return err + } + + forkBlock, err = b.fetchBlockFromHash(&forkNode.hash) + if err != nil { + return err + } } // Perform several checks to verify each block that needs to be attached @@ -1286,7 +1346,6 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List, flags // 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. - var topBlock *blockNode for e := attachNodes.Front(); e != nil; e = e.Next() { n := e.Value.(*blockNode) b.blockCacheLock.RLock() @@ -1301,10 +1360,9 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List, flags if err != nil { return err } - topBlock = n + + newBest = n } - newHash := topBlock.hash - newHeight := topBlock.height log.Debugf("New best chain validation completed successfully, " + "commencing with the reorganization.") @@ -1316,10 +1374,10 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List, flags // Send a notification that a blockchain reorganization is in progress. reorgData := &ReorganizationNtfnsData{ - formerBestHash, - formerBestHeight, - newHash, - newHeight, + oldBest.hash, + oldBest.height, + newBest.hash, + newBest.height, } b.chainLock.Unlock() b.sendNotification(NTReorganization, reorgData) @@ -1331,21 +1389,29 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List, flags // view to be valid from the viewpoint of each block being connected or // disconnected. view = NewUtxoViewpoint() - view.SetBestHash(&formerBestHash) + view.SetBestHash(&oldBest.hash) view.SetStakeViewpoint(ViewpointPrevValidInitial) // Disconnect blocks from the main chain. for i, e := 0, detachNodes.Front(); e != nil; i, e = i+1, e.Next() { + // Since the blocks are being detached in reverse order, the parent of + // current block being detached is the next one being detached up to + // the point of the fork at which point it's the fork block. n := e.Value.(*blockNode) block := detachBlocks[i] - parent, err := b.fetchBlockFromHash(&n.parentHash) - if err != nil { - return err + parent := forkBlock + if i < len(detachBlocks)-1 { + parent = detachBlocks[i+1] + } + if n.parentHash != *parent.Hash() { + return AssertError(fmt.Sprintf("detach block node hash %v (height "+ + "%v) parent hash %v does not match previous parent block "+ + "hash %v", &n.hash, n.height, &n.parentHash, parent.Hash())) } // Load all of the utxos referenced by the block that aren't // already in the view. - err = view.fetchInputUtxos(b.db, block, parent) + err := view.fetchInputUtxos(b.db, block, parent) if err != nil { return err } @@ -1395,23 +1461,16 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List, flags delete(b.blockCache, n.hash) } - // Log the point where the chain forked. - firstAttachNode := attachNodes.Front().Value.(*blockNode) - forkNode, err := b.index.PrevNodeFromNode(firstAttachNode) - if err == nil { - log.Infof("REORGANIZE: Chain forks at %v, height %v", - forkNode.hash, - forkNode.height) + // Log the point where the chain forked and old and new best chain + // heads. + if forkNode != nil { + log.Infof("REORGANIZE: Chain forks at %v (height %v)", + forkNode.hash, forkNode.height) } - - // Log the old and new best chain heads. - lastAttachNode := attachNodes.Back().Value.(*blockNode) - log.Infof("REORGANIZE: Old best chain head was %v, height %v", - formerBestHash, - formerBestHeight) - log.Infof("REORGANIZE: New best chain head is %v, height %v", - lastAttachNode.hash, - lastAttachNode.height) + log.Infof("REORGANIZE: Old best chain head was %v (height %v)", + &oldBest.hash, oldBest.height) + log.Infof("REORGANIZE: New best chain head is %v (height %v)", + newBest.hash, newBest.height) return nil }