blockchain: flush the utxo cache on prune if needed

If the prune will delete block past the last flush hash of the
utxocache, the cache will need to be flushed first to avoid a case
where the utxocache is irrecoverable.  The newly added code adds this
flush logic to connectBlock.
This commit is contained in:
Calvin Kim 2023-08-25 17:57:04 +09:00
parent dd37dfa80b
commit ebc93a34ce
3 changed files with 113 additions and 1 deletions

View File

@ -642,6 +642,24 @@ func (b *BlockChain) connectBlock(node *blockNode, block *btcutil.Block,
if err != nil { if err != nil {
return err return err
} }
// We may need to flush if the prune will delete blocks that
// are past our last flush block.
//
// NOTE: the database will never be inconsistent here as the
// actual blocks are not deleted until the db.Update returns.
needsFlush, err := b.flushNeededAfterPrune(deletedHashes)
if err != nil {
return err
}
if needsFlush {
// Since the deleted hashes are past our last
// flush block, flush the utxo cache now.
err = b.utxoCache.flush(dbTx, FlushRequired, state)
if err != nil {
return err
}
}
} }
} }
@ -719,7 +737,7 @@ func (b *BlockChain) connectBlock(node *blockNode, block *btcutil.Block,
// Since we may have changed the UTXO cache, we make sure it didn't exceed its // Since we may have changed the UTXO cache, we make sure it didn't exceed its
// maximum size. If we're pruned and have flushed already, this will be a no-op. // maximum size. If we're pruned and have flushed already, this will be a no-op.
return b.db.Update(func(dbTx database.Tx) error { return b.db.Update(func(dbTx database.Tx) error {
return b.utxoCache.flush(dbTx, FlushIfNeeded, b.BestSnapshot()) return b.utxoCache.flush(dbTx, FlushIfNeeded, state)
}) })
} }

BIN
blockchain/testdata/blk_0_to_14131.dat vendored Normal file

Binary file not shown.

View File

@ -16,6 +16,7 @@ import (
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/database" "github.com/btcsuite/btcd/database"
"github.com/btcsuite/btcd/database/ffldb"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
) )
@ -704,3 +705,96 @@ func TestFlushNeededAfterPrune(t *testing.T) {
} }
} }
} }
func TestFlushOnPrune(t *testing.T) {
chain, tearDown, err := chainSetup("TestFlushOnPrune", &chaincfg.MainNetParams)
if err != nil {
panic(fmt.Sprintf("error loading blockchain with database: %v", err))
}
defer tearDown()
chain.utxoCache.maxTotalMemoryUsage = 10 * 1024 * 1024
chain.utxoCache.cachedEntries.maxTotalMemoryUsage = chain.utxoCache.maxTotalMemoryUsage
// Set the maxBlockFileSize and the prune target small so that we can trigger a
// prune to happen.
maxBlockFileSize := uint32(8192)
chain.pruneTarget = uint64(maxBlockFileSize) * 2
// Read blocks from the file.
blocks, err := loadBlocks("blk_0_to_14131.dat")
if err != nil {
t.Fatalf("failed to read block from file. %v", err)
}
syncBlocks := func() {
for i, block := range blocks {
if i == 0 {
// Skip the genesis block.
continue
}
isMainChain, _, err := chain.ProcessBlock(block, BFNone)
if err != nil {
t.Fatal(err)
}
if !isMainChain {
t.Fatalf("expected block %s to be on the main chain", block.Hash())
}
}
}
// Sync the chain.
ffldb.TstRunWithMaxBlockFileSize(chain.db, maxBlockFileSize, syncBlocks)
// Function that errors out if the block that should exist doesn't exist.
shouldExist := func(dbTx database.Tx, blockHash *chainhash.Hash) {
bytes, err := dbTx.FetchBlock(blockHash)
if err != nil {
t.Fatal(err)
}
block, err := btcutil.NewBlockFromBytes(bytes)
if err != nil {
t.Fatalf("didn't find block %v. %v", blockHash, err)
}
if !block.Hash().IsEqual(blockHash) {
t.Fatalf("expected to find block %v but got %v",
blockHash, block.Hash())
}
}
// Function that errors out if the block that shouldn't exist exists.
shouldNotExist := func(dbTx database.Tx, blockHash *chainhash.Hash) {
bytes, err := dbTx.FetchBlock(chaincfg.MainNetParams.GenesisHash)
if err == nil {
t.Fatalf("expected block %s to be pruned", blockHash)
}
if len(bytes) != 0 {
t.Fatalf("expected block %s to be pruned but got %v",
blockHash, bytes)
}
}
// The below code checks that the correct blocks were pruned.
chain.db.View(func(dbTx database.Tx) error {
exist := false
for _, block := range blocks {
// Blocks up to the last flush hash should not exist.
// The utxocache is big enough so that it shouldn't flush
// on it being full. It should only flush on prunes.
if block.Hash().IsEqual(&chain.utxoCache.lastFlushHash) {
exist = true
}
if exist {
shouldExist(dbTx, block.Hash())
} else {
shouldNotExist(dbTx, block.Hash())
}
}
return nil
})
}