Skip to content

Commit

Permalink
blockchain, netsync, main, cmd/addblock: Use utxocache
Browse files Browse the repository at this point in the history
This change is part of the effort to add utxocache support to btcd.

utxo cache is now used by the BlockChain struct.  By default it's used
and the minimum cache is set to 250MiB.  The change made helps speed up
block/tx validation as the cache allows for much faster lookup of utxos.
The initial block download in particular is improved as the db i/o
bottleneck is remedied by the cache.
  • Loading branch information
kcalvinalvin committed Jun 2, 2023
1 parent 94ec601 commit 5032670
Show file tree
Hide file tree
Showing 10 changed files with 529 additions and 90 deletions.
124 changes: 97 additions & 27 deletions blockchain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ type BlockChain struct {
index *blockIndex
bestChain *chainView

// The UTXO state holds a cached view of the UTXO state of the chain.
// It is protected by the chain lock.
utxoCache *utxoCache

// These fields are related to handling of orphan blocks. They are
// protected by a combination of the chain lock and the orphan lock.
orphanLock sync.RWMutex
Expand Down Expand Up @@ -546,9 +550,14 @@ func (b *BlockChain) getReorganizeNodes(node *blockNode) (*list.List, *list.List
// connectBlock handles connecting the passed node/block to the end of the main
// (best) chain.
//
// This passed utxo view must have all referenced txos the block spends marked
// as spent and all of the new txos the block creates added to it. In addition,
// the passed stxos slice must be populated with all of the information for the
// Passing in a utxo view is optional. If the passed in utxo view is nil,
// connectBlock will assume that the utxo cache has already connected all the
// txs in the block being connected.
// If a utxo view is passed in, this passed utxo view must have all referenced
// txos the block spends marked as spent and all of the new txos the block creates
// added to it.
//
// The passed stxos slice must be populated with all of the information for the
// spent txos. This approach is used because the connection validation that
// must happen prior to calling this function requires the same details, so
// it would be inefficient to repeat it.
Expand Down Expand Up @@ -596,6 +605,16 @@ func (b *BlockChain) connectBlock(node *blockNode, block *btcutil.Block,
state := newBestState(node, blockSize, blockWeight, numTxns,
curTotalTxns+numTxns, node.CalcPastMedianTime())

// If a utxoviewpoint was passed in, we'll be writing that viewpoint
// directly to the database on disk. In order for the database to be
// consistent, we must flush the cache before writing the viewpoint.
if view != nil {
err = b.utxoCache.Flush(FlushRequired, state)
if err != nil {
return err
}
}

// Atomically insert info into the database.
err = b.db.Update(func(dbTx database.Tx) error {
// Update best block state.
Expand All @@ -614,6 +633,8 @@ func (b *BlockChain) connectBlock(node *blockNode, block *btcutil.Block,
// Update the utxo set using the state of the utxo view. This
// entails removing all of the utxos spent and adding the new
// ones created by the block.
//
// A nil viewpoint is a no-op.
err = dbPutUtxoView(dbTx, view)
if err != nil {
return err
Expand Down Expand Up @@ -644,7 +665,9 @@ func (b *BlockChain) connectBlock(node *blockNode, block *btcutil.Block,

// Prune fully spent entries and mark all entries in the view unmodified
// now that the modifications have been committed to the database.
view.commit()
if view != nil {
view.commit()
}

// This node is now the end of the best chain.
b.bestChain.SetTip(node)
Expand All @@ -665,7 +688,11 @@ func (b *BlockChain) connectBlock(node *blockNode, block *btcutil.Block,
b.sendNotification(NTBlockConnected, block)
b.chainLock.Lock()

return nil
// Since we may have changed the UTXO cache, we make sure it didn't exceed its
// maximum size.
b.stateLock.Lock()
defer b.stateLock.Unlock()
return b.utxoCache.Flush(FlushIfNeeded, state)
}

// disconnectBlock handles disconnecting the passed node/block from the end of
Expand Down Expand Up @@ -814,6 +841,13 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
return nil
}

// The rest of the reorg depends on all STXOs already being in the database
// so we flush before reorg.
err := b.utxoCache.Flush(FlushRequired, b.BestSnapshot())
if err != nil {
return err
}

// Ensure the provided nodes match the current best chain.
tip := b.bestChain.Tip()
if detachNodes.Len() != 0 {
Expand Down Expand Up @@ -875,7 +909,7 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error

// Load all of the utxos referenced by the block that aren't
// already in the view.
err = view.fetchInputUtxos(b.db, block)
err = view.fetchInputUtxos(b.db, nil, block)
if err != nil {
return err
}
Expand Down Expand Up @@ -942,7 +976,7 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
// checkConnectBlock gets skipped, we still need to update the UTXO
// view.
if b.index.NodeStatus(n).KnownValid() {
err = view.fetchInputUtxos(b.db, block)
err = view.fetchInputUtxos(b.db, nil, block)
if err != nil {
return err
}
Expand Down Expand Up @@ -994,7 +1028,7 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error

// Load all of the utxos referenced by the block that aren't
// already in the view.
err := view.fetchInputUtxos(b.db, block)
err := view.fetchInputUtxos(b.db, nil, block)
if err != nil {
return err
}
Expand All @@ -1021,7 +1055,7 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error

// Load all of the utxos referenced by the block that aren't
// already in the view.
err := view.fetchInputUtxos(b.db, block)
err := view.fetchInputUtxos(b.db, nil, block)
if err != nil {
return err
}
Expand All @@ -1043,6 +1077,13 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
}
}

// We call the flush at the end to update the last flush hash to the new
// best tip.
err = b.utxoCache.Flush(FlushRequired, b.BestSnapshot())
if err != nil {
return err
}

// Log the point where the chain forked and old and new best chain
// heads.
if forkNode != nil {
Expand Down Expand Up @@ -1095,11 +1136,18 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fla
// Perform several checks to verify the block can be connected
// to the main chain without violating any rules and without
// actually connecting the block.
view := NewUtxoViewpoint()
view.SetBestHash(parentHash)
stxos := make([]SpentTxOut, 0, countSpentOutputs(block))
if !fastAdd {
err := b.checkConnectBlock(node, block, view, &stxos)
// We create a viewpoint here to avoid mutating the utxo cache.
// The block is not considered valid until checkconnectblock
// returns and the mutation would force us to undo the cache.
//
// TODO (kcalvinalvin): Doing all of the validation before connecting
// the tx inside check connect block would allow us to pass the utxo
// cache directly to the check connect block. This would save on the
// expensive memory allocation done by fetch input utxos.
view := NewUtxoViewpoint()
view.SetBestHash(parentHash)
err := b.checkConnectBlock(node, block, view, nil)
if err == nil {
b.index.SetStatusFlags(node, statusValid)
} else if _, ok := err.(RuleError); ok {
Expand All @@ -1115,23 +1163,16 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fla
}
}

// In the fast add case the code to check the block connection
// was skipped, so the utxo view needs to load the referenced
// utxos, spend them, and add the new utxos being created by
// this block.
if fastAdd {
err := view.fetchInputUtxos(b.db, block)
if err != nil {
return false, err
}
err = view.connectTransactions(block, &stxos)
if err != nil {
return false, err
}
// Connect the transactions to the cache. All the txs are considered valid
// at this point as they have passed validation or was considered valid already.
stxos := make([]SpentTxOut, 0, countSpentOutputs(block))
err := b.utxoCache.connectTransactions(block, &stxos)
if err != nil {
return false, err
}

// Connect the block to the main chain.
err := b.connectBlock(node, block, view, stxos)
err = b.connectBlock(node, block, nil, stxos)
if err != nil {
// If we got hit with a rule error, then we'll mark
// that status of the block as invalid and flush the
Expand Down Expand Up @@ -1646,6 +1687,11 @@ type Config struct {
// This field is required.
DB database.DB

// The maximum size in bytes of the UTXO cache.
//
// This field is required.
UtxoCacheMaxSize uint64

// Interrupt specifies a channel the caller can close to signal that
// long running operations, such as catching up indexes or performing
// database migrations, should be interrupted.
Expand Down Expand Up @@ -1749,6 +1795,7 @@ func New(config *Config) (*BlockChain, error) {
maxRetargetTimespan: targetTimespan * adjustmentFactor,
blocksPerRetarget: int32(targetTimespan / targetTimePerBlock),
index: newBlockIndex(config.DB, params),
utxoCache: newUtxoCache(config.DB, config.UtxoCacheMaxSize),
hashCache: config.HashCache,
bestChain: newChainView(nil),
orphans: make(map[chainhash.Hash]*orphanBlock),
Expand Down Expand Up @@ -1797,10 +1844,33 @@ func New(config *Config) (*BlockChain, error) {
return nil, err
}

// Make sure the utxo state is catched up if it was left in an inconsistent
// state.
bestNode := b.bestChain.Tip()
if err := b.InitConsistentState(bestNode, config.Interrupt); err != nil {
return nil, err
}
log.Infof("Chain state (height %d, hash %v, totaltx %d, work %v)",
bestNode.height, bestNode.hash, b.stateSnapshot.TotalTxns,
bestNode.workSum)

return &b, nil
}

// CachedStateSize returns the total size of the cached state of the blockchain
// in bytes.
func (b *BlockChain) CachedStateSize() uint64 {
b.chainLock.Lock()
defer b.chainLock.Unlock()
return b.utxoCache.totalMemoryUsage()
}

// FlushCachedState flushes all the cached state of the blockchain to the
// database.
//
// This method is safe for concurrent access.
func (b *BlockChain) FlushCachedState(mode FlushMode) error {
b.chainLock.Lock()
defer b.chainLock.Unlock()
return b.utxoCache.Flush(mode, b.stateSnapshot)
}

0 comments on commit 5032670

Please sign in to comment.