Skip to content

Commit

Permalink
blockchain: Add ReconsiderBlock to BlockChain
Browse files Browse the repository at this point in the history
ReconsiderBlock reconsiders the validity of the block for the passed
in blockhash. The behavior of the function mimics that of Bitcoin Core.

The invalid status of the block nodes are reset and if the chaintip that
is being reconsidered has more cumulative work, then we'll validate the
blocks and reorganize to it. If the cumulative work is lesser than the
current active chain tip, then nothing else will be done.
  • Loading branch information
kcalvinalvin authored and Crypt-iQ committed Jun 7, 2024
1 parent eabc9bf commit 52a8a2a
Show file tree
Hide file tree
Showing 2 changed files with 316 additions and 0 deletions.
90 changes: 90 additions & 0 deletions blockchain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -1950,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.
Expand Down
226 changes: 226 additions & 0 deletions blockchain/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
}
}()
}
}

0 comments on commit 52a8a2a

Please sign in to comment.