Skip to content

Commit

Permalink
Merge pull request gcash#445 from gcash/utxotest
Browse files Browse the repository at this point in the history
Fix utxo consistency bugs on hard shutdown
  • Loading branch information
cpacia authored Mar 9, 2021
2 parents 877fb62 + 77d5ee1 commit 90684b9
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 8 deletions.
17 changes: 9 additions & 8 deletions blockchain/utxocache.go
Original file line number Diff line number Diff line change
Expand Up @@ -791,12 +791,9 @@ func (s *utxoCache) InitConsistentState(tip *blockNode, fastSync bool, interrupt
}
}

// Now we can update the status already to avoid redoing this work when
// Now we can flush and update the status to avoid redoing this work when
// interrupted.
err = s.db.Update(func(dbTx database.Tx) error {
return dbPutUtxoStateConsistency(dbTx, ucsConsistent, statusHash)
})
if err != nil {
if err := s.flush(&BestState{Hash: *statusHash}); err != nil {
return err
}

Expand Down Expand Up @@ -828,9 +825,7 @@ func (s *utxoCache) InitConsistentState(tip *blockNode, fastSync bool, interrupt
}
}

// We can update this after each batch to avoid having to redo the work
// when interrupted.
return node, dbPutUtxoStateConsistency(dbTx, ucsConsistent, &node.hash)
return node, nil
}

for node := statusNodeNext; node.height <= tip.height; {
Expand All @@ -845,6 +840,12 @@ func (s *utxoCache) InitConsistentState(tip *blockNode, fastSync bool, interrupt
return err
}

// We can flush after each batch to avoid having to redo the work
// when interrupted.
if err := s.flush(&BestState{Hash: node.hash}); err != nil {
return err
}

if interruptRequested(interrupt) {
log.Warn("UTXO state reconstruction interrupted")

Expand Down
155 changes: 155 additions & 0 deletions blockchain/utxocache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,158 @@ func TestUtxoCache_Reorg(t *testing.T) {
assertConsistencyState(t, chain, ucsConsistent, b5b.Hash())
assertNbEntriesOnDisk(t, chain, len(totalSpendableOuts))
}

func TestUtxoCache_InitConsistentState(t *testing.T) {
// First we are going to simulate a hard shutdown where the utxo cache has
// entries (spends plus new utxos) that are not persisted to the database.
//
// On next startup the chain height should be ahead of the utxo set height
// and this should trigger the utxo set on disk to be rolled forward using
// the blocks from the chain.
t.Run("Hard shutdown before flush", func(t *testing.T) {
chain, params, tearDown := utxoCacheTestChain("TestUtxoCache_InitConsistentState")
defer tearDown()
tip := bchutil.NewBlock(params.GenesisBlock)

// Create base blocks 1 and 2 and flush to disk.
var emptySpendableOuts []*spendableOut
b1, spendableOuts1 := addBlock(chain, tip, emptySpendableOuts)
b2, spendableOuts2 := addBlock(chain, b1, spendableOuts1)
t.Log(spew.Sdump(spendableOuts2))
// db cache
// block 1: stxo
// block 2: utxo

if err := chain.FlushCachedState(FlushRequired); err != nil {
t.Fatalf("unexpected error while flushing cache: %v", err)
}

// db cache
// block 1: stxo
// block 2: utxo
assertConsistencyState(t, chain, ucsConsistent, b2.Hash())
assertNbEntriesOnDisk(t, chain, len(spendableOuts2))

// Add blocks 3 and 4.
// Spend the outputs of block 2 and 3.
b3, spendableOuts3 := addBlock(chain, b2, spendableOuts2)
b4, spendableOuts4 := addBlock(chain, b3, spendableOuts3)
// db cache
// block 1: stxo
// block 2: utxo stxo << these are left spent without flush
// ---
// block 3: stxo
// block 4: utxo

// At this point the chain should have blocks 1-4 saved on disk, but the utxo set
// should only be persisted through block 2. Calling InitConsistentState at this point
// should roll the utxo set forward to block 4.

// Reset the utxo cache
chain.utxoCache = newUtxoCache(chain.db, 10*1024*1024)

err := chain.utxoCache.InitConsistentState(chain.bestChain.Tip(), false, nil)
if err != nil {
t.Fatalf("failed to init utxo cache: %v", err)
}

// db cache
// block 1: stxo
// block 2: stxo
// block 3: stxo
// block 4: utxo

assertConsistencyState(t, chain, ucsConsistent, b4.Hash())
assertNbEntriesOnDisk(t, chain, len(spendableOuts4))
})

// Next we are going to simulate a crash mid-flush where the state is left as ucsFlushOngoing.
t.Run("Hard shutdown mid flush", func(t *testing.T) {
chain, params, tearDown := utxoCacheTestChain("TestUtxoCache_InitConsistentState")
defer tearDown()
tip := bchutil.NewBlock(params.GenesisBlock)

// Create base blocks 1 and 2 and flush to disk.
var emptySpendableOuts []*spendableOut
b1, spendableOuts1 := addBlock(chain, tip, emptySpendableOuts)
b2, spendableOuts2 := addBlock(chain, b1, spendableOuts1)
t.Log(spew.Sdump(spendableOuts2))
// db cache
// block 1: stxo
// block 2: utxo

if err := chain.FlushCachedState(FlushRequired); err != nil {
t.Fatalf("unexpected error while flushing cache: %v", err)
}

// db cache
// block 1: stxo
// block 2: utxo
assertConsistencyState(t, chain, ucsConsistent, b2.Hash())
assertNbEntriesOnDisk(t, chain, len(spendableOuts2))

// Add blocks 3 and 4.
// Spend the outputs of block 2 and 3.
b3, spendableOuts3 := addBlock(chain, b2, spendableOuts2)
b4, spendableOuts4 := addBlock(chain, b3, spendableOuts3)
// db cache
// block 1: stxo
// block 2: utxo stxo << these are left spent without flush
// ---
// block 3: stxo
// block 4: utxo

// At this point the chain should have blocks 1-4 saved on disk, but the utxo set
// should only be persisted through block 2.

// Now we're going to manually save one utxo and delete one spend and then stop,
// simulating a hard shutdown.

addedUtxo, deletedSpend := false, false
err := chain.db.Update(func(dbTx database.Tx) error {
for outpoint, entry := range chain.utxoCache.cachedEntries {
if entry == nil {
continue
}
if entry.IsSpent() && !deletedSpend {
if err := dbDeleteUtxoEntries(dbTx, []wire.OutPoint{outpoint}); err != nil {
return err
}
deletedSpend = true
}
if !entry.IsSpent() && !addedUtxo {
if err := dbPutUtxoEntries(dbTx, map[wire.OutPoint]*UtxoEntry{outpoint: entry}); err != nil {
return err
}
addedUtxo = true
}
if deletedSpend && addedUtxo {
break
}
}

return dbPutUtxoStateConsistency(dbTx, ucsFlushOngoing, &chain.utxoCache.lastFlushHash)
})
if err != nil {
t.Fatalf("unexpected error while puting utxo state to db: %v", err)
}

// Reset the utxo cache
chain.utxoCache = newUtxoCache(chain.db, 10*1024*1024)

err = chain.utxoCache.InitConsistentState(chain.bestChain.Tip(), false, nil)
if err != nil {
t.Fatalf("failed to init utxo cache: %v", err)
}

// db cache
// block 1: stxo
// block 2: stxo
// block 3: stxo
// block 4: utxo

assertConsistencyState(t, chain, ucsConsistent, b4.Hash())
assertNbEntriesOnDisk(t, chain, len(spendableOuts4))
})

}
7 changes: 7 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,13 @@ func (sp *serverPeer) OnVersion(_ *peer.Peer, msg *wire.MsgVersion) *wire.MsgRej
return wire.NewMsgReject(msg.Command(), wire.RejectNonstandard, reason)
}

// Do not allow connections to Bitcoin ABC peers
if strings.Contains(msg.UserAgent, "Bitcoin ABC") {
srvrLog.Debugf("Rejecting peer %s for running Bitcoin ABC", sp.Peer)
reason := "Not Bitcoin Cash node"
return wire.NewMsgReject(msg.Command(), wire.RejectNonstandard, reason)
}

// Reject outbound peers that are not full nodes.
wantServices := wire.SFNodeNetwork
if !isInbound && !hasServices(msg.Services, wantServices) {
Expand Down

0 comments on commit 90684b9

Please sign in to comment.