mirror of
https://github.com/btcsuite/btcd.git
synced 2024-11-19 01:40:07 +01:00
Merge pull request #2196 from Crypt-iQ/2181_eugene
blockchain: Add ReconsiderBlock()
This commit is contained in:
commit
cd5e5bab63
@ -870,192 +870,24 @@ func countSpentOutputs(block *btcutil.Block) int {
|
||||
//
|
||||
// This function MUST be called with the chain state lock held (for writes).
|
||||
func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error {
|
||||
// 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.
|
||||
tip := b.bestChain.Tip()
|
||||
if detachNodes.Len() != 0 {
|
||||
firstDetachNode := detachNodes.Front().Value.(*blockNode)
|
||||
if firstDetachNode.hash != tip.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, &tip.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.parent.hash != lastDetachNode.parent.hash {
|
||||
return AssertError(fmt.Sprintf("reorganize nodes do not have the "+
|
||||
"same fork point -- first attach parent %v, last detach "+
|
||||
"parent %v", &firstAttachNode.parent.hash,
|
||||
&lastDetachNode.parent.hash))
|
||||
}
|
||||
}
|
||||
|
||||
// Track the old and new best chains heads.
|
||||
oldBest := tip
|
||||
newBest := tip
|
||||
|
||||
// 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())
|
||||
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(&oldBest.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 = dbFetchBlockByNode(dbTx, n)
|
||||
return err
|
||||
})
|
||||
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()))
|
||||
}
|
||||
|
||||
// Load all of the utxos referenced by the block that aren't
|
||||
// already in the view.
|
||||
err = view.fetchInputUtxos(b.utxoCache, block)
|
||||
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)
|
||||
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(b.db, block, stxos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newBest = n.parent
|
||||
}
|
||||
|
||||
// Set the fork point only if there are nodes to attach since otherwise
|
||||
// blocks are only being disconnected and thus there is no fork point.
|
||||
var forkNode *blockNode
|
||||
if attachNodes.Len() > 0 {
|
||||
forkNode = newBest
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var err error
|
||||
block, err = dbFetchBlockByNode(dbTx, n)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store the loaded block for later.
|
||||
attachBlocks = append(attachBlocks, block)
|
||||
|
||||
// Skip checks if node has already been fully validated. Although
|
||||
// checkConnectBlock gets skipped, we still need to update the UTXO
|
||||
// view.
|
||||
if b.index.NodeStatus(n).KnownValid() {
|
||||
err = view.fetchInputUtxos(b.utxoCache, block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = view.connectTransactions(block, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newBest = n
|
||||
continue
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// In the case the block is determined to be invalid due to a
|
||||
// rule violation, mark it as invalid and mark all of its
|
||||
// descendants as having an invalid ancestor.
|
||||
err = b.checkConnectBlock(n, block, view, nil)
|
||||
if err != nil {
|
||||
if _, ok := err.(RuleError); ok {
|
||||
b.index.SetStatusFlags(n, statusValidateFailed)
|
||||
for de := e.Next(); de != nil; de = de.Next() {
|
||||
dn := de.Value.(*blockNode)
|
||||
b.index.SetStatusFlags(dn, statusInvalidAncestor)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
b.index.SetStatusFlags(n, statusValid)
|
||||
|
||||
newBest = n
|
||||
}
|
||||
|
||||
// Flush the utxo cache for the block disconnect below. The disconnect
|
||||
// code assumes that it's directly modifying the database so the cache
|
||||
// will be left in an inconsistent state. It needs to be flushed beforehand
|
||||
// in order for that to not happen.
|
||||
err := b.db.Update(func(dbTx database.Tx) error {
|
||||
return b.utxoCache.flush(dbTx, FlushRequired, b.BestSnapshot())
|
||||
})
|
||||
// Check first that the detach and the attach nodes are valid and they
|
||||
// pass verification.
|
||||
detachBlocks, attachBlocks, detachSpentTxOuts,
|
||||
err := b.verifyReorganizationValidity(detachNodes, attachNodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Track the old and new best chains heads.
|
||||
tip := b.bestChain.Tip()
|
||||
oldBest := tip
|
||||
newBest := tip
|
||||
|
||||
// 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 disconnected.
|
||||
view = NewUtxoViewpoint()
|
||||
view := NewUtxoViewpoint()
|
||||
view.SetBestHash(&b.bestChain.Tip().hash)
|
||||
|
||||
// Disconnect blocks from the main chain.
|
||||
@ -1072,8 +904,9 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
|
||||
|
||||
// Update the view to unspend all of the spent txos and remove
|
||||
// the utxos created by the block.
|
||||
err = view.disconnectTransactions(b.db, block,
|
||||
detachSpentTxOuts[i])
|
||||
err = view.disconnectTransactions(
|
||||
b.db, block, detachSpentTxOuts[i],
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -1084,6 +917,15 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newBest = n.parent
|
||||
}
|
||||
|
||||
// Set the fork point only if there are nodes to attach since otherwise
|
||||
// blocks are only being disconnected and thus there is no fork point.
|
||||
var forkNode *blockNode
|
||||
if attachNodes.Len() > 0 {
|
||||
forkNode = newBest
|
||||
}
|
||||
|
||||
// Connect the new best chain blocks using the utxocache directly. It's more
|
||||
@ -1109,6 +951,8 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newBest = n
|
||||
}
|
||||
|
||||
// Log the point where the chain forked and old and new best chain
|
||||
@ -1125,6 +969,176 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyReorganizationValidity will verify that the disconnects and the connects
|
||||
// that are in the list are able to be processed without mutating the chain.
|
||||
//
|
||||
// For the attach nodes, it'll check that each of the blocks are valid and will
|
||||
// change the status of the block node in the list to invalid if the block fails
|
||||
// to pass verification. For the detach nodes, it'll check that the blocks being
|
||||
// detached and their spend journals are present on the database.
|
||||
func (b *BlockChain) verifyReorganizationValidity(detachNodes, attachNodes *list.List) (
|
||||
[]*btcutil.Block, []*btcutil.Block, [][]SpentTxOut, error) {
|
||||
|
||||
// Nothing to do if no reorganize nodes were provided.
|
||||
if detachNodes.Len() == 0 && attachNodes.Len() == 0 {
|
||||
return nil, nil, nil, nil
|
||||
}
|
||||
|
||||
// Ensure the provided nodes match the current best chain.
|
||||
tip := b.bestChain.Tip()
|
||||
if detachNodes.Len() != 0 {
|
||||
firstDetachNode := detachNodes.Front().Value.(*blockNode)
|
||||
if firstDetachNode.hash != tip.hash {
|
||||
return nil, nil, nil,
|
||||
AssertError(fmt.Sprintf("reorganize nodes to detach are "+
|
||||
"not for the current best chain -- first detach node %v, "+
|
||||
"current chain %v", &firstDetachNode.hash, &tip.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.parent.hash != lastDetachNode.parent.hash {
|
||||
return nil, nil, nil,
|
||||
AssertError(fmt.Sprintf("reorganize nodes do not have the "+
|
||||
"same fork point -- first attach parent %v, last detach "+
|
||||
"parent %v", &firstAttachNode.parent.hash,
|
||||
&lastDetachNode.parent.hash))
|
||||
}
|
||||
}
|
||||
|
||||
// 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())
|
||||
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(&tip.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 = dbFetchBlockByNode(dbTx, n)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if n.hash != *block.Hash() {
|
||||
return nil, nil, nil, AssertError(
|
||||
fmt.Sprintf("detach block node hash %v (height "+
|
||||
"%v) does not match previous parent block hash %v",
|
||||
&n.hash, n.height, block.Hash()))
|
||||
}
|
||||
|
||||
// Load all of the utxos referenced by the block that aren't
|
||||
// already in the view.
|
||||
err = view.fetchInputUtxos(b.utxoCache, block)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 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)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Store the loaded block and spend journal entry for later.
|
||||
detachBlocks = append(detachBlocks, block)
|
||||
detachSpentTxOuts = append(detachSpentTxOuts, stxos)
|
||||
|
||||
err = view.disconnectTransactions(b.db, block, stxos)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 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 {
|
||||
var err error
|
||||
block, err = dbFetchBlockByNode(dbTx, n)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Store the loaded block for later.
|
||||
attachBlocks = append(attachBlocks, block)
|
||||
|
||||
// Skip checks if node has already been fully validated. Although
|
||||
// checkConnectBlock gets skipped, we still need to update the UTXO
|
||||
// view.
|
||||
if b.index.NodeStatus(n).KnownValid() {
|
||||
err = view.fetchInputUtxos(b.utxoCache, block)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
err = view.connectTransactions(block, nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// In the case the block is determined to be invalid due to a
|
||||
// rule violation, mark it as invalid and mark all of its
|
||||
// descendants as having an invalid ancestor.
|
||||
err = b.checkConnectBlock(n, block, view, nil)
|
||||
if err != nil {
|
||||
if _, ok := err.(RuleError); ok {
|
||||
b.index.SetStatusFlags(n, statusValidateFailed)
|
||||
for de := e.Next(); de != nil; de = de.Next() {
|
||||
dn := de.Value.(*blockNode)
|
||||
b.index.SetStatusFlags(dn, statusInvalidAncestor)
|
||||
}
|
||||
}
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
b.index.SetStatusFlags(n, statusValid)
|
||||
}
|
||||
|
||||
return detachBlocks, attachBlocks, detachSpentTxOuts, nil
|
||||
}
|
||||
|
||||
// connectBestChain handles connecting the passed block to the chain while
|
||||
// respecting proper chain selection according to the chain with the most
|
||||
// proof of work. In the typical case, the new block simply extends the main
|
||||
@ -1936,6 +1950,96 @@ func (b *BlockChain) InvalidateBlock(hash *chainhash.Hash) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ReconsiderBlock reconsiders the validity of the block with the given hash.
|
||||
//
|
||||
// This function is safe for concurrent access.
|
||||
func (b *BlockChain) ReconsiderBlock(hash *chainhash.Hash) error {
|
||||
b.chainLock.Lock()
|
||||
defer b.chainLock.Unlock()
|
||||
|
||||
log.Infof("Reconsidering block_hash=%v", hash[:])
|
||||
|
||||
reconsiderNode := b.index.LookupNode(hash)
|
||||
if reconsiderNode == nil {
|
||||
// Return an error if the block doesn't exist.
|
||||
return fmt.Errorf("requested block hash of %s is not found "+
|
||||
"and thus cannot be reconsidered", hash)
|
||||
}
|
||||
|
||||
// Nothing to do if the given block is already valid.
|
||||
if reconsiderNode.status.KnownValid() {
|
||||
log.Infof("block_hash=%x is valid, nothing to reconsider", hash[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear the status of the block being reconsidered.
|
||||
b.index.UnsetStatusFlags(reconsiderNode, statusInvalidAncestor)
|
||||
b.index.UnsetStatusFlags(reconsiderNode, statusValidateFailed)
|
||||
|
||||
// Grab all the tips.
|
||||
tips := b.index.InactiveTips(b.bestChain)
|
||||
tips = append(tips, b.bestChain.Tip())
|
||||
|
||||
log.Debugf("Examining %v inactive chain tips for reconsideration")
|
||||
|
||||
// Go through all the tips and unset the status for all the descendents of the
|
||||
// block being reconsidered.
|
||||
var reconsiderTip *blockNode
|
||||
for _, tip := range tips {
|
||||
// Continue if the given inactive tip is not a descendant of the block
|
||||
// being invalidated.
|
||||
if !tip.IsAncestor(reconsiderNode) {
|
||||
// Set as the reconsider tip if the block node being reconsidered
|
||||
// is a tip.
|
||||
if tip == reconsiderNode {
|
||||
reconsiderTip = reconsiderNode
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Mark the current tip as the tip being reconsidered.
|
||||
reconsiderTip = tip
|
||||
|
||||
// Unset the status of all the parents up until it reaches the block
|
||||
// being reconsidered.
|
||||
for n := tip; n != nil && n != reconsiderNode; n = n.parent {
|
||||
b.index.UnsetStatusFlags(n, statusInvalidAncestor)
|
||||
}
|
||||
}
|
||||
|
||||
// Compare the cumulative work for the branch being reconsidered.
|
||||
bestTipWork := b.bestChain.Tip().workSum
|
||||
if reconsiderTip.workSum.Cmp(bestTipWork) <= 0 {
|
||||
log.Debugf("Tip to reconsider has less cumulative work than current "+
|
||||
"chain tip: %v vs %v", reconsiderTip.workSum, bestTipWork)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the reconsider tip has a higher cumulative work, then reorganize
|
||||
// to it after checking the validity of the nodes.
|
||||
detachNodes, attachNodes := b.getReorganizeNodes(reconsiderTip)
|
||||
|
||||
// We're checking if the reorganization that'll happen is actually valid.
|
||||
// While this is called in reorganizeChain, we call it beforehand as the error
|
||||
// returned from reorganizeChain doesn't differentiate between actual disconnect/
|
||||
// connect errors or whether the branch we're trying to fork to is invalid.
|
||||
//
|
||||
// The block status changes here without being flushed so we immediately flush
|
||||
// the blockindex after we call this function.
|
||||
_, _, _, err := b.verifyReorganizationValidity(detachNodes, attachNodes)
|
||||
if writeErr := b.index.flushToDB(); writeErr != nil {
|
||||
log.Warnf("Error flushing block index changes to disk: %v", writeErr)
|
||||
}
|
||||
if err != nil {
|
||||
// If we errored out during the verification of the reorg branch,
|
||||
// it's ok to return nil as we reconsidered the block and determined
|
||||
// that it's invalid.
|
||||
return nil
|
||||
}
|
||||
|
||||
return b.reorganizeChain(detachNodes, attachNodes)
|
||||
}
|
||||
|
||||
// IndexManager provides a generic interface that the is called when blocks are
|
||||
// connected and disconnected to and from the tip of the main chain for the
|
||||
// purpose of supporting optional indexes.
|
||||
|
@ -1622,3 +1622,229 @@ func TestInvalidateBlock(t *testing.T) {
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconsiderBlock(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
chainGen func() (*BlockChain, []*chainhash.Hash, func())
|
||||
}{
|
||||
{
|
||||
name: "one branch, invalidate once and revalidate",
|
||||
chainGen: func() (*BlockChain, []*chainhash.Hash, func()) {
|
||||
chain, params, tearDown := utxoCacheTestChain("TestInvalidateBlock-one-branch-invalidate-once")
|
||||
|
||||
// Create a chain with 101 blocks.
|
||||
tip := btcutil.NewBlock(params.GenesisBlock)
|
||||
_, _, err := addBlocks(101, chain, tip, []*testhelper.SpendableOut{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Invalidate block 5.
|
||||
block, err := chain.BlockByHeight(5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
invalidateHash := block.Hash()
|
||||
|
||||
return chain, []*chainhash.Hash{invalidateHash}, tearDown
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalidate the active branch with a side branch present and revalidate",
|
||||
chainGen: func() (*BlockChain, []*chainhash.Hash, func()) {
|
||||
chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-invalidate-with-side-branch")
|
||||
|
||||
// Create a chain with 101 blocks.
|
||||
tip := btcutil.NewBlock(params.GenesisBlock)
|
||||
_, spendableOuts, err := addBlocks(101, chain, tip, []*testhelper.SpendableOut{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Invalidate block 5.
|
||||
block, err := chain.BlockByHeight(5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
invalidateHash := block.Hash()
|
||||
|
||||
// Create a side chain with 7 blocks that builds on block 1.
|
||||
b1, err := chain.BlockByHeight(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, err = addBlocks(6, chain, b1, spendableOuts[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return chain, []*chainhash.Hash{invalidateHash}, tearDown
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalidate a side branch and revalidate it",
|
||||
chainGen: func() (*BlockChain, []*chainhash.Hash, func()) {
|
||||
chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-invalidate-a-side-branch")
|
||||
|
||||
// Create a chain with 101 blocks.
|
||||
tip := btcutil.NewBlock(params.GenesisBlock)
|
||||
_, spendableOuts, err := addBlocks(101, chain, tip, []*testhelper.SpendableOut{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a side chain with 7 blocks that builds on block 1.
|
||||
b1, err := chain.BlockByHeight(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
altBlockHashes, _, err := addBlocks(6, chain, b1, spendableOuts[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Grab block at height 4:
|
||||
//
|
||||
// b2, b3, b4, b5
|
||||
// 0, 1, 2, 3
|
||||
invalidateHash := altBlockHashes[2]
|
||||
|
||||
return chain, []*chainhash.Hash{invalidateHash}, tearDown
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reconsider an invalid side branch with a higher work",
|
||||
chainGen: func() (*BlockChain, []*chainhash.Hash, func()) {
|
||||
chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-reconsider-an-invalid-side-branch-higher")
|
||||
|
||||
tip := btcutil.NewBlock(params.GenesisBlock)
|
||||
_, spendableOuts, err := addBlocks(6, chain, tip, []*testhelper.SpendableOut{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Select utxos to be spent from the best block and
|
||||
// modify the amount so that the block will be invalid.
|
||||
nextSpends, _ := randomSelect(spendableOuts[len(spendableOuts)-1])
|
||||
nextSpends[0].Amount += testhelper.LowFee
|
||||
|
||||
// Make an invalid block that best on top of the current tip.
|
||||
bestBlock, err := chain.BlockByHash(&chain.BestSnapshot().Hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
invalidBlock, _, _ := newBlock(chain, bestBlock, nextSpends)
|
||||
invalidateHash := invalidBlock.Hash()
|
||||
|
||||
// The block validation will fail here and we'll mark the
|
||||
// block as invalid in the block index.
|
||||
chain.ProcessBlock(invalidBlock, BFNone)
|
||||
|
||||
// Modify the amount again so it's valid.
|
||||
nextSpends[0].Amount -= testhelper.LowFee
|
||||
|
||||
return chain, []*chainhash.Hash{invalidateHash}, tearDown
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reconsider an invalid side branch with a lower work",
|
||||
chainGen: func() (*BlockChain, []*chainhash.Hash, func()) {
|
||||
chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-reconsider-an-invalid-side-branch-lower")
|
||||
|
||||
tip := btcutil.NewBlock(params.GenesisBlock)
|
||||
_, spendableOuts, err := addBlocks(6, chain, tip, []*testhelper.SpendableOut{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Select utxos to be spent from the best block and
|
||||
// modify the amount so that the block will be invalid.
|
||||
nextSpends, _ := randomSelect(spendableOuts[len(spendableOuts)-1])
|
||||
nextSpends[0].Amount += testhelper.LowFee
|
||||
|
||||
// Make an invalid block that best on top of the current tip.
|
||||
bestBlock, err := chain.BlockByHash(&chain.BestSnapshot().Hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
invalidBlock, _, _ := newBlock(chain, bestBlock, nextSpends)
|
||||
invalidateHash := invalidBlock.Hash()
|
||||
|
||||
// The block validation will fail here and we'll mark the
|
||||
// block as invalid in the block index.
|
||||
chain.ProcessBlock(invalidBlock, BFNone)
|
||||
|
||||
// Modify the amount again so it's valid.
|
||||
nextSpends[0].Amount -= testhelper.LowFee
|
||||
|
||||
// Add more blocks to make the invalid block a
|
||||
// side chain and not the most pow.
|
||||
_, _, err = addBlocks(3, chain, bestBlock, []*testhelper.SpendableOut{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return chain, []*chainhash.Hash{invalidateHash}, tearDown
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
chain, invalidateHashes, tearDown := test.chainGen()
|
||||
func() {
|
||||
defer tearDown()
|
||||
for _, invalidateHash := range invalidateHashes {
|
||||
// Cache the chain tips before the invalidate. Since we'll reconsider
|
||||
// the invalidated block, we should come back to these tips in the end.
|
||||
tips := chain.ChainTips()
|
||||
expectedChainTips := make(map[chainhash.Hash]ChainTip, len(tips))
|
||||
for _, tip := range tips {
|
||||
expectedChainTips[tip.BlockHash] = tip
|
||||
}
|
||||
|
||||
// Invalidation.
|
||||
err := chain.InvalidateBlock(invalidateHash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Reconsideration.
|
||||
err = chain.ReconsiderBlock(invalidateHash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Compare the tips aginst the tips we've cached.
|
||||
gotChainTips := chain.ChainTips()
|
||||
for _, gotChainTip := range gotChainTips {
|
||||
testChainTip, found := expectedChainTips[gotChainTip.BlockHash]
|
||||
if !found {
|
||||
t.Errorf("TestReconsiderBlock Failed test \"%s\". Couldn't find an expected "+
|
||||
"chain tip with height %d, hash %s, branchlen %d, status \"%s\"",
|
||||
test.name, testChainTip.Height, testChainTip.BlockHash.String(),
|
||||
testChainTip.BranchLen, testChainTip.Status.String())
|
||||
}
|
||||
|
||||
// If the invalid side branch is a lower work, we'll never
|
||||
// actually process the block again until the branch becomes
|
||||
// a greater work chain so it'll show up as valid-fork.
|
||||
if test.name == "reconsider an invalid side branch with a lower work" &&
|
||||
testChainTip.BlockHash == *invalidateHash {
|
||||
|
||||
testChainTip.Status = StatusValidFork
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(testChainTip, gotChainTip) {
|
||||
t.Errorf("TestReconsiderBlock Failed test \"%s\". Expected chain tip with "+
|
||||
"height %d, hash %s, branchlen %d, status \"%s\" but got "+
|
||||
"height %d, hash %s, branchlen %d, status \"%s\"", test.name,
|
||||
testChainTip.Height, testChainTip.BlockHash.String(),
|
||||
testChainTip.BranchLen, testChainTip.Status.String(),
|
||||
gotChainTip.Height, gotChainTip.BlockHash.String(),
|
||||
gotChainTip.BranchLen, gotChainTip.Status.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user