blockchain: add flushNeededAfterPrune

flushNeededAfterPrune returns true if the utxocache needs to be flushed
after the pruning of the given slice of block hashes.  For the utxo
cache to be recoverable while pruning is enabled, we need to make sure
that there exists blocks since the last utxo cache flush.  If there are
blocks that are deleted after the last utxo cache flush, the utxo set is
irrecoverable.  The added method provides a way to tell if a flush is
needed.
This commit is contained in:
Calvin Kim 2023-08-25 17:34:05 +09:00
parent d387d162f3
commit dd37dfa80b
2 changed files with 148 additions and 0 deletions

View File

@ -711,3 +711,34 @@ func (b *BlockChain) InitConsistentState(tip *blockNode, interrupt <-chan struct
return nil
}
// flushNeededAfterPrune returns true if the utxo cache needs to be flushed after a prune
// of the block storage. In the case of an unexpected shutdown, the utxo cache needs
// to be reconstructed from where the utxo cache was last flushed. In order for the
// utxo cache to be reconstructed, we always need to have the blocks since the utxo cache
// flush last happened.
//
// Example: if the last flush hash was at height 100 and one of the deleted blocks was at
// height 98, this function will return true.
func (b *BlockChain) flushNeededAfterPrune(deletedBlockHashes []chainhash.Hash) (bool, error) {
lastFlushHeight, err := b.BlockHeightByHash(&b.utxoCache.lastFlushHash)
if err != nil {
return false, err
}
// Loop through all the block hashes and find out what the highest block height
// among the deleted hashes is.
highestDeletedHeight := int32(-1)
for _, deletedBlockHash := range deletedBlockHashes {
height, err := b.BlockHeightByHash(&deletedBlockHash)
if err != nil {
return false, err
}
if height > highestDeletedHeight {
highestDeletedHeight = height
}
}
return highestDeletedHeight >= lastFlushHeight, nil
}

View File

@ -587,3 +587,120 @@ func TestUtxoCacheFlush(t *testing.T) {
t.Fatal(err)
}
}
func TestFlushNeededAfterPrune(t *testing.T) {
// Construct a synthetic block chain with a block index consisting of
// the following structure.
// genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18
tip := tstTip
chain := newFakeChain(&chaincfg.MainNetParams)
chain.utxoCache = newUtxoCache(nil, 0)
branchNodes := chainedNodes(chain.bestChain.Genesis(), 18)
for _, node := range branchNodes {
chain.index.SetStatusFlags(node, statusValid)
chain.index.AddNode(node)
}
chain.bestChain.SetTip(tip(branchNodes))
tests := []struct {
name string
lastFlushHash chainhash.Hash
delHashes []chainhash.Hash
expected bool
}{
{
name: "deleted block up to height 9, last flush hash at block 10",
delHashes: func() []chainhash.Hash {
delBlockHashes := make([]chainhash.Hash, 0, 9)
for i := range branchNodes {
if branchNodes[i].height < 10 {
delBlockHashes = append(delBlockHashes, branchNodes[i].hash)
}
}
return delBlockHashes
}(),
lastFlushHash: func() chainhash.Hash {
// Just some sanity checking to make sure the height is 10.
if branchNodes[9].height != 10 {
panic("was looking for height 10")
}
return branchNodes[9].hash
}(),
expected: false,
},
{
name: "deleted blocks up to height 10, last flush hash at block 10",
delHashes: func() []chainhash.Hash {
delBlockHashes := make([]chainhash.Hash, 0, 10)
for i := range branchNodes {
if branchNodes[i].height < 11 {
delBlockHashes = append(delBlockHashes, branchNodes[i].hash)
}
}
return delBlockHashes
}(),
lastFlushHash: func() chainhash.Hash {
// Just some sanity checking to make sure the height is 10.
if branchNodes[9].height != 10 {
panic("was looking for height 10")
}
return branchNodes[9].hash
}(),
expected: true,
},
{
name: "deleted block height 17, last flush hash at block 5",
delHashes: func() []chainhash.Hash {
delBlockHashes := make([]chainhash.Hash, 1)
delBlockHashes[0] = branchNodes[16].hash
// Just some sanity checking to make sure the height is 10.
if branchNodes[16].height != 17 {
panic("was looking for height 17")
}
return delBlockHashes
}(),
lastFlushHash: func() chainhash.Hash {
// Just some sanity checking to make sure the height is 10.
if branchNodes[4].height != 5 {
panic("was looking for height 5")
}
return branchNodes[4].hash
}(),
expected: true,
},
{
name: "deleted block height 3, last flush hash at block 4",
delHashes: func() []chainhash.Hash {
delBlockHashes := make([]chainhash.Hash, 1)
delBlockHashes[0] = branchNodes[2].hash
// Just some sanity checking to make sure the height is 10.
if branchNodes[2].height != 3 {
panic("was looking for height 3")
}
return delBlockHashes
}(),
lastFlushHash: func() chainhash.Hash {
// Just some sanity checking to make sure the height is 10.
if branchNodes[3].height != 4 {
panic("was looking for height 4")
}
return branchNodes[3].hash
}(),
expected: false,
},
}
for _, test := range tests {
chain.utxoCache.lastFlushHash = test.lastFlushHash
got, err := chain.flushNeededAfterPrune(test.delHashes)
if err != nil {
t.Fatal(err)
}
if got != test.expected {
t.Fatalf("for test %s, expected need flush to return %v but got %v",
test.name, test.expected, got)
}
}
}