Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

blockchain: Add UtxoCache Initialize tests. #2599

Merged
merged 4 commits into from
Mar 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions blockchain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ type BlockChain struct {
sigCache *txscript.SigCache
indexManager indexers.IndexManager
interrupt <-chan struct{}
utxoCache *UtxoCache
utxoCache UtxoCacher

// subsidyCache is the cache that provides quick lookup of subsidy
// values.
Expand Down Expand Up @@ -2152,7 +2152,7 @@ type Config struct {
// the database directly.
//
// This field is required.
UtxoCache *UtxoCache
UtxoCache UtxoCacher
}

// New returns a BlockChain instance using the provided configuration details.
Expand Down
2 changes: 1 addition & 1 deletion blockchain/chainio.go
Original file line number Diff line number Diff line change
Expand Up @@ -1931,7 +1931,7 @@ func (b *BlockChain) initChainState(ctx context.Context) error {

// Initialize the utxo cache to ensure that the state of the utxo set is
// caught up to the tip of the best chain.
return b.InitUtxoCache(tip)
return b.utxoCache.Initialize(b, tip)
}

// dbFetchBlockByNode uses an existing database transaction to retrieve the raw
Expand Down
29 changes: 29 additions & 0 deletions blockchain/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"math"
mrand "math/rand"
"os"
"reflect"
"testing"
"time"

Expand Down Expand Up @@ -754,6 +755,34 @@ func (g *chaingenHarness) ExpectTip(tipName string) {
}
}

// ExpectUtxoSetState expects the provided block to be the last flushed block in
// the utxo set state in the database.
func (g *chaingenHarness) ExpectUtxoSetState(blockName string) {
g.t.Helper()

// Fetch the utxo set state from the database.
var gotState *utxoSetState
err := g.chain.db.View(func(dbTx database.Tx) error {
var err error
gotState, err = dbFetchUtxoSetState(dbTx)
return err
})
if err != nil {
g.t.Fatalf("unexpected error fetching utxo set state: %v", err)
}

// Validate that the state matches the expected state.
block := g.BlockByName(blockName)
wantState := &utxoSetState{
lastFlushHeight: block.Header.Height,
lastFlushHash: block.BlockHash(),
}
if !reflect.DeepEqual(gotState, wantState) {
davecgh marked this conversation as resolved.
Show resolved Hide resolved
g.t.Fatalf("mismatched utxo set state:\nwant: %+v\n got: %+v\n", wantState,
gotState)
}
}

// AcceptedToSideChainWithExpectedTip expects the tip block associated with the
// generator to be accepted to a side chain, but the current best chain tip to
// be the provided value.
Expand Down
76 changes: 61 additions & 15 deletions blockchain/utxocache.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,49 @@ const (
periodicFlushInterval = time.Minute * 2
)

// UtxoCacher represents a utxo cache that sits on top of the utxo set database.
//
// The interface contract requires that all of these methods are safe for
// concurrent access.
type UtxoCacher interface {
davecgh marked this conversation as resolved.
Show resolved Hide resolved
// Commit updates the cache based on the state of each entry in the provided
// view.
//
// All entries in the provided view that are marked as modified and spent are
// removed from the view. Additionally, all entries that are added to the
// cache are removed from the provided view.
Commit(view *UtxoViewpoint) error

// FetchEntries adds the requested transaction outputs to the provided view.
// It first checks the cache for each output, and if an output does not exist
// in the cache, it will fetch it from the database.
//
// Upon completion of this function, the view will contain an entry for each
// requested outpoint. Spent outputs, or those which otherwise don't exist,
// will result in a nil entry in the view.
FetchEntries(filteredSet viewFilteredSet, view *UtxoViewpoint) error

// FetchEntry returns the specified transaction output from the utxo set. If
// the output exists in the cache, it is returned immediately. Otherwise, it
// uses an existing database transaction to fetch the output from the
// database, cache it, and return it to the caller. The entry that is
// returned can safely be mutated by the caller without invalidating the
// cache.
//
// When there is no entry for the provided output, nil will be returned for
// both the entry and the error.
FetchEntry(dbTx database.Tx, outpoint wire.OutPoint) (*UtxoEntry, error)

// Initialize initializes the utxo cache by ensuring that the utxo set is
// caught up to the tip of the best chain.
Initialize(b *BlockChain, tip *blockNode) error

// MaybeFlush conditionally flushes the cache to the database. A flush can be
// forced by setting the force flush parameter.
MaybeFlush(bestHash *chainhash.Hash, bestHeight uint32, forceFlush bool,
logFlush bool) error
}

// UtxoCache is an unspent transaction output cache that sits on top of the
// utxo set database and provides significant runtime performance benefits at
// the cost of some additional memory usage. It drastically reduces the amount
Expand Down Expand Up @@ -128,6 +171,9 @@ type UtxoCache struct {
timeNow func() time.Time
}

// Ensure UtxoCache implements the UtxoCacher interface.
var _ UtxoCacher = (*UtxoCache)(nil)

// UtxoCacheConfig is a descriptor which specifies the utxo cache instance
// configuration.
type UtxoCacheConfig struct {
Expand Down Expand Up @@ -367,8 +413,8 @@ func (c *UtxoCache) FetchEntries(filteredSet viewFilteredSet, view *UtxoViewpoin
return err
}

// Commit updates all entries in the cache based on the state of each entry in
// the provided view.
// Commit updates the cache based on the state of each entry in the provided
// view.
//
// All entries in the provided view that are marked as modified and spent are
// removed from the view. Additionally, all entries that are added to the cache
Expand Down Expand Up @@ -607,22 +653,22 @@ func (c *UtxoCache) MaybeFlush(bestHash *chainhash.Hash, bestHeight uint32,
return nil
}

// InitUtxoCache initializes the utxo cache by ensuring that the utxo set is
// caught up to the tip of the best chain.
// Initialize initializes the utxo cache by ensuring that the utxo set is caught
// up to the tip of the best chain.
//
// Since the cache is only flushed to the database periodically, the utxo set
// may not be caught up to the tip of the best chain. This function catches the
// utxo set up by replaying all blocks from the block after the block that was
// last flushed to the tip block through the cache.
//
// This function should only be called during initialization.
func (b *BlockChain) InitUtxoCache(tip *blockNode) error {
func (c *UtxoCache) Initialize(b *BlockChain, tip *blockNode) error {
log.Infof("UTXO cache initializing (max size: %d MiB)...",
b.utxoCache.maxSize/1024/1024)
c.maxSize/1024/1024)

// Fetch the utxo set state from the database.
var state *utxoSetState
err := b.db.View(func(dbTx database.Tx) error {
err := c.db.View(func(dbTx database.Tx) error {
var err error
state, err = dbFetchUtxoSetState(dbTx)
return err
Expand All @@ -639,7 +685,7 @@ func (b *BlockChain) InitUtxoCache(tip *blockNode) error {
lastFlushHeight: uint32(tip.height),
lastFlushHash: tip.hash,
}
err := b.db.Update(func(dbTx database.Tx) error {
err := c.db.Update(func(dbTx database.Tx) error {
return dbPutUtxoSetState(dbTx, state)
})
if err != nil {
Expand All @@ -649,8 +695,8 @@ func (b *BlockChain) InitUtxoCache(tip *blockNode) error {

// Set the last flush hash and the last eviction height from the saved state
// since that is where we are starting from.
b.utxoCache.lastFlushHash = state.lastFlushHash
b.utxoCache.lastEvictionHeight = state.lastFlushHeight
c.lastFlushHash = state.lastFlushHash
c.lastEvictionHeight = state.lastFlushHeight

// If state is already caught up to the tip, return as there is nothing to do.
if state.lastFlushHash == tip.hash {
Expand Down Expand Up @@ -680,7 +726,7 @@ func (b *BlockChain) InitUtxoCache(tip *blockNode) error {
// disconnecting a block, this will occur very infrequently. In the typical
// catchup case, the fork node will be the last flushed node itself and this
// loop will be skipped.
view := NewUtxoViewpoint(b.utxoCache)
view := NewUtxoViewpoint(c)
view.SetBestHash(&tip.hash)
var nextBlockToDetach *dcrutil.Block
n := lastFlushedNode
Expand Down Expand Up @@ -745,15 +791,15 @@ func (b *BlockChain) InitUtxoCache(tip *blockNode) error {
// view that are marked as modified and spent are removed from the view.
// Additionally, all entries that are added to the cache are removed from
// the view.
err = b.utxoCache.Commit(view)
err = c.Commit(view)
if err != nil {
return err
}

// Conditionally flush the utxo cache to the database. Don't force flush
// since many blocks may be disconnected and connected in quick succession
// when initializing.
err = b.utxoCache.MaybeFlush(&n.hash, uint32(n.height), false, true)
err = c.MaybeFlush(&n.hash, uint32(n.height), false, true)
if err != nil {
return err
}
Expand Down Expand Up @@ -820,14 +866,14 @@ func (b *BlockChain) InitUtxoCache(tip *blockNode) error {
// view that are marked as modified and spent are removed from the view.
// Additionally, all entries that are added to the cache are removed from
// the view.
err = b.utxoCache.Commit(view)
err = c.Commit(view)
if err != nil {
return err
}

// Conditionally flush the utxo cache to the database. Don't force flush
// since many blocks may be connected in quick succession when initializing.
err = b.utxoCache.MaybeFlush(&n.hash, uint32(n.height), false, true)
err = c.MaybeFlush(&n.hash, uint32(n.height), false, true)
if err != nil {
return err
}
Expand Down
Loading