blockchain: Convert to direct single-step reorgs.

This modifies the chain reorganization logic to directly perform the
reorg one block at a time with rollback in the case of failure, as
opposed to the existing memory-based two-step approach, so that it is
more optimized for the typical case, better handles large reorgs, gives
the ability to implement better caching strategies, and helps provide a
path to decouple the chain processing and connection code from the
download logic.  It also removes the cached stxos from the view since
the aforementioned changes make them no longer necessary.

A side effect of these changes is that it is no longer possible to know
if a reorg will succeed before actually performing it, so the
NTReorganization notification is now sent after a successful reorg.  The
notification really should have been sent after the reorg before anyway.

Prior to these changes, chain reorganization used a two-step approach
such that the first step involved checking all of the blocks along the
reorg path in memory and then actually performing the reorg in a second
step if those checks succeeded.  While that approach does have some
benefits in terms of avoiding any intermediate mutation to the current
best chain for failed reorgs, and thus not requiring a rollback in that
case, it also has some disadvantages such as not scaling well with large
reorgs, being more difficult to make use of different caching
strategies, and hindering the ability to decouple the connection code
from the download logic.

In a certain sense, the approach this replaces assumed that a reorg
would fail and took measures to detect that condition prior to
performing the reorg, while the new approach assumes the reorg will
succeed and rolls back the changes in the very rare case it doesn't.
This is an acceptable and safe assumption because the proof-of-work
requirements make it exceedingly expensive to create blocks that are
valid enough to trigger a reorg yet ultimately end up failing to
connect, thus miners are heavily disincentivized from creating such
invalid blocks and attackers are also unable to easily create such
blocks either.  Even in the case of attack, the only result would be
nodes performing slightly more database updates than the existing
approach.

The following results show the difference between performing the large
reorg full block tests before and after these changes:

before: 4.3GB memory usage, 2m18.629s to complete
after:  2.8GB memory usage, 2m04.056s to complete

As can be seen, the new approach takes much less memory and is also a
bit faster as well.
This commit is contained in:
Dave Collins 2018-10-16 00:07:25 -05:00
parent df1898cead
commit 9c4569ae12
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
3 changed files with 182 additions and 291 deletions

View File

@ -683,70 +683,6 @@ func (b *BlockChain) isMajorityVersion(minVer int32, startNode *blockNode, numRe
return numFound >= numRequired
}
// getReorganizeNodes finds the fork point between the main chain and the passed
// node and returns a list of block nodes that would need to be detached from
// the main chain and a list of block nodes that would need to be attached to
// the fork point (which will be the end of the main chain after detaching the
// returned list of block nodes) in order to reorganize the chain such that the
// passed node is the new end of the main chain. The lists will be empty if the
// passed node is not on a side chain or if the reorganize would involve
// reorganizing to a known invalid chain.
//
// This function may modify the validation state of nodes in the block index
// without flushing.
//
// This function MUST be called with the chain state lock held (for reads).
func (b *BlockChain) getReorganizeNodes(node *blockNode) (*list.List, *list.List) {
// Nothing to detach or attach if there is no node.
attachNodes := list.New()
detachNodes := list.New()
if node == nil {
return detachNodes, attachNodes
}
// Do not allow a reorganize to a known invalid chain. Note that all
// intermediate ancestors other than the direct parent are also checked
// below, however, this check allows extra to work to be avoided in the
// majority of cases since reorgs across multiple unvalidated blocks are
// not very common.
if b.index.NodeStatus(node.parent).KnownInvalid() {
b.index.SetStatusFlags(node, statusInvalidAncestor)
return detachNodes, attachNodes
}
// Find the fork point (if any) adding each block to the list of nodes
// to attach to the main tree. Push them onto the list in reverse order
// so they are attached in the appropriate order when iterating the list
// later.
//
// In the case a known invalid block is detected while constructing this
// list, mark all of its descendants as having an invalid ancestor and
// prevent the reorganize by not returning any nodes.
forkNode := b.bestChain.FindFork(node)
for n := node; n != nil && n != forkNode; n = n.parent {
if b.index.NodeStatus(n).KnownInvalid() {
for e := attachNodes.Front(); e != nil; e = e.Next() {
dn := e.Value.(*blockNode)
b.index.SetStatusFlags(dn, statusInvalidAncestor)
}
attachNodes.Init()
return detachNodes, attachNodes
}
attachNodes.PushFront(n)
}
// Start from the end of the main chain and work backwards until the
// common ancestor adding each block to the list of nodes to detach from
// the main chain.
for n := b.bestChain.Tip(); n != nil && n != forkNode; n = n.parent {
detachNodes.PushBack(n)
}
return detachNodes, attachNodes
}
// pushMainChainBlockCache pushes a block onto the main chain block cache,
// and removes any old blocks from the cache that might be present.
func (b *BlockChain) pushMainChainBlockCache(block *dcrutil.Block) {
@ -1104,73 +1040,70 @@ func countSpentOutputs(block *dcrutil.Block) int {
return countSpentRegularOutputs(block) + countSpentStakeOutputs(block)
}
// reorganizeChain reorganizes the block chain by disconnecting the nodes in the
// detachNodes list and connecting the nodes in the attach list. It expects
// that the lists are already in the correct order and are in sync with the
// end of the current best chain. Specifically, nodes that are being
// disconnected must be in reverse order (think of popping them off the end of
// the chain) and nodes the are being attached must be in forwards order
// (think pushing them onto the end of the chain).
// reorganizeChainInternal attempts to reorganize the block chain to the
// provided tip without attempting to undo failed reorgs.
//
// Since reorganizing to a new chain tip might involve validating blocks that
// have not previously been validated, or attempting to reorganize to a branch
// that is already known to be invalid, it possible for the reorganize to fail.
// When that is the case, this function will return the error without attempting
// to undo what has already been reorganized to that point. That means the best
// chain tip will be set to some intermediate block along the reorg path and
// will not actually be the best chain. This is acceptable because this
// function is only intended to be called from the reorganizeChain function
// which handles reorg failures by reorganizing back to the known good best
// chain tip.
//
// A reorg entails disconnecting all blocks from the current best chain tip back
// to the fork point between it and the provided target tip in reverse order
// (think popping them off the end of the chain) and then connecting the blocks
// on the new branch in forwards order (think pushing them onto the end of the
// chain).
//
// This function may modify the validation state of nodes in the block index
// without flushing in the case the chain is not able to reorganize due to a
// block failing to connect.
//
// This function MUST be called with the chain state lock held (for writes).
func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error {
// Nothing to do if no reorganize nodes were provided.
if detachNodes.Len() == 0 && attachNodes.Len() == 0 {
return nil
func (b *BlockChain) reorganizeChainInternal(targetTip *blockNode) error {
// Find the fork point adding each block to a slice of blocks to attach
// below once the current best chain has been disconnected. They are added
// to the slice from back to front so that so they are attached in the
// appropriate order when iterating the slice later.
//
// In the case a known invalid block is detected while constructing this
// list, mark all of its descendants as having an invalid ancestor and
// prevent the reorganize.
fork := b.bestChain.FindFork(targetTip)
attachNodes := make([]*blockNode, targetTip.height-fork.height)
for n := targetTip; n != nil && n != fork; n = n.parent {
if b.index.NodeStatus(n).KnownInvalid() {
for _, dn := range attachNodes[n.height-fork.height:] {
b.index.SetStatusFlags(dn, statusInvalidAncestor)
}
str := fmt.Sprintf("block %s is known to be invalid or a "+
"descendant of an invalid block", n.hash)
return ruleError(ErrKnownInvalidBlock, str)
}
attachNodes[n.height-fork.height-1] = n
}
// Ensure the provided nodes match the current best chain.
// Disconnect all of the blocks back to the point of the fork. This entails
// loading the blocks and their associated spent txos from the database and
// using that information to unspend all of the spent txos and remove the
// utxos created by the blocks. In addition, if a block votes against its
// parent, the regular transactions are reconnected.
tip := b.bestChain.Tip()
if detachNodes.Len() != 0 {
firstDetachNode := detachNodes.Front().Value.(*blockNode)
if firstDetachNode.hash != tip.hash {
panicf("reorganize nodes to detach are not for the current best "+
"chain -- first detach node %v, current chain %v",
&firstDetachNode.hash, &tip.hash)
}
}
// Ensure the provided nodes are for the same fork point.
if attachNodes.Len() != 0 && detachNodes.Len() != 0 {
firstAttachNode := attachNodes.Front().Value.(*blockNode)
lastDetachNode := detachNodes.Back().Value.(*blockNode)
if firstAttachNode.parent.hash != lastDetachNode.parent.hash {
panicf("reorganize nodes do not have the same fork point -- first "+
"attach parent %v, last detach parent %v",
&firstAttachNode.parent.hash, &lastDetachNode.parent.hash)
}
}
// Track the old and new best chains heads.
oldBest := tip
newBest := tip
// All of the blocks to detach and related spend journal entries needed
// to unspend transaction outputs in the blocks being disconnected must
// be loaded from the database during the reorg check phase below and
// then they are needed again when doing the actual database updates.
// Rather than doing two loads, cache the loaded data into these slices.
detachBlocks := make([]*dcrutil.Block, 0, detachNodes.Len())
detachSpentTxOuts := make([][]spentTxOut, 0, detachNodes.Len())
attachBlocks := make([]*dcrutil.Block, 0, attachNodes.Len())
// Disconnect all of the blocks back to the point of the fork. This
// entails loading the blocks and their associated spent txos from the
// database and using that information to unspend all of the spent txos
// and remove the utxos created by the blocks. In addition, if a block
// votes against its parent, the regular transactions are reconnected.
view := NewUtxoViewpoint()
view.SetBestHash(&oldBest.hash)
view.SetBestHash(&tip.hash)
var nextBlockToDetach *dcrutil.Block
for e := detachNodes.Front(); e != nil; e = e.Next() {
for tip != nil && tip != fork {
// Grab the block to detach based on the node. Use the fact that the
// blocks are being detached in reverse order, so the parent of the
// current block being detached is the next one being detached.
n := e.Value.(*blockNode)
n := tip
block := nextBlockToDetach
if block == nil {
var err error
@ -1194,8 +1127,7 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
}
nextBlockToDetach = parent
// Load all of the spent txos for the block from the spend
// journal.
// Load all of the spent txos for the block from the spend journal.
var stxos []spentTxOut
err = b.db.View(func(dbTx database.Tx) error {
stxos, err = dbFetchSpendJournalEntry(dbTx, block)
@ -1205,10 +1137,6 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
return err
}
// Store the loaded block and spend journal entry for later.
detachBlocks = append(detachBlocks, block)
detachSpentTxOuts = append(detachSpentTxOuts, stxos)
// Update the view to unspend all of the spent txos and remove the utxos
// created by the block. Also, if the block votes against its parent,
// reconnect all of the regular transactions.
@ -1217,49 +1145,44 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
return err
}
newBest = n.parent
// Update the database and chain state.
err = b.disconnectBlock(n, block, parent, view)
if err != nil {
return err
}
tip = n.parent
}
// Set the fork point and grab the fork block when there are nodes to be
// attached. The fork block is used as the parent to the first node to be
// attached below.
var forkNode *blockNode
var forkBlock *dcrutil.Block
if attachNodes.Len() > 0 {
forkNode = newBest
// Load the fork block if there are blocks to attach and its not already
// loaded which will be the case if no nodes were detached. The fork block
// is used as the parent to the first node to be attached below.
forkBlock := nextBlockToDetach
if len(attachNodes) > 0 && forkBlock == nil {
var err error
forkBlock, err = b.fetchMainChainBlockByNode(forkNode)
forkBlock, err = b.fetchMainChainBlockByNode(tip)
if err != nil {
return err
}
}
// Perform several checks to verify each block that needs to be attached
// to the main chain can be connected without violating any rules and
// without actually connecting the block.
//
// NOTE: These checks could be done directly when connecting a block,
// however the downside to that approach is that if any of these checks
// fail after disconnecting some blocks or attaching others, all of the
// operations have to be rolled back to get the chain back into the
// state it was before the rule violation (or other failure). There are
// at least a couple of ways accomplish that rollback, but both involve
// tweaking the chain and/or database. This approach catches these
// issues before ever modifying the chain.
for i, e := 0, attachNodes.Front(); e != nil; i, e = i+1, e.Next() {
// Attempt to connect each block that needs to be attached to the main
// chain. This entails performing several checks to verify each block can
// be connected without violating any consensus rules and updating the
// relevant information related to the current chain state.
var prevBlockAttached *dcrutil.Block
for i, n := range attachNodes {
// Grab the block to attach based on the node. Use the fact that the
// parent of the block is either the fork point for the first node being
// attached or the previous one that was attached for subsequent blocks
// to optimize.
n := e.Value.(*blockNode)
block, err := b.fetchBlockByNode(n)
if err != nil {
return err
}
parent := forkBlock
if i > 0 {
parent = attachBlocks[i-1]
parent = prevBlockAttached
}
if n.parent.hash != *parent.Hash() {
panicf("attach block node hash %v (height %v) parent hash %v does "+
@ -1267,56 +1190,79 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
&n.parent.hash, parent.Hash())
}
// Store the loaded block for later.
attachBlocks = append(attachBlocks, block)
// Store the loaded block as parent of next iteration.
prevBlockAttached = block
// Skip validation if the block is already known to be valid.
// However, the UTXO view still needs to be updated.
// Skip validation if the block is already known to be valid. However,
// the utxo view still needs to be updated and the stxos are still
// needed.
stxos := make([]spentTxOut, 0, countSpentOutputs(block))
if b.index.NodeStatus(n).KnownValid() {
stxos := make([]spentTxOut, 0, countSpentOutputs(block))
// Update the view to mark all utxos referenced by the block as
// spent and add all transactions being created by this block to it.
// In the case the block votes against the parent, also disconnect
// all of the regular transactions in the parent block. Finally,
// provide an stxo slice so the spent txout details are generated.
err := view.connectBlock(b.db, block, parent, &stxos)
if err != nil {
return err
}
view.cachedStxos[n.hash] = stxos
newBest = n
continue
} else {
// In the case the block is determined to be invalid due to a rule
// violation, mark it as invalid and mark all of its descendants as
// having an invalid ancestor.
err = b.checkConnectBlock(n, block, parent, view, &stxos)
if err != nil {
if _, ok := err.(RuleError); ok {
b.index.SetStatusFlags(n, statusValidateFailed)
for _, dn := range attachNodes[i+1:] {
b.index.SetStatusFlags(dn, statusInvalidAncestor)
}
}
return err
}
b.index.SetStatusFlags(n, statusValid)
}
// In the case the block is determined to be invalid due to a rule
// violation, mark it as invalid and mark all of its descendants as
// having an invalid ancestor.
stxos := make([]spentTxOut, 0, countSpentOutputs(block))
err = b.checkConnectBlock(n, block, parent, view, &stxos)
// Update the database and chain state.
err = b.connectBlock(n, block, parent, view, stxos)
if err != nil {
if _, ok := err.(RuleError); ok {
b.index.SetStatusFlags(n, statusValidateFailed)
for de := e.Next(); de != nil; de = de.Next() {
dn := de.Value.(*blockNode)
b.index.SetStatusFlags(dn, statusInvalidAncestor)
}
}
return err
}
b.index.SetStatusFlags(n, statusValid)
view.cachedStxos[n.hash] = stxos
newBest = n
tip = n
}
log.Debugf("New best chain validation completed successfully, " +
"commencing with the reorganization.")
// Send a notification that a blockchain reorganization is in progress.
reorgData := &ReorganizationNtfnsData{
oldBest.hash,
oldBest.height,
newBest.hash,
newBest.height,
return nil
}
// reorganizeChain attempts to reorganize the block chain to the provided tip.
// The tip must have already been determined to be on another branch by the
// caller. Upon return, the chain will be fully reorganized to the provided tip
// or an appropriate error will be returned and the chain will remain at the
// same tip it was prior to calling this function.
//
// Reorganizing the chain entails disconnecting all blocks from the current best
// chain tip back to the fork point between it and the provided target tip in
// reverse order (think popping them off the end of the chain) and then
// connecting the blocks on the new branch in forwards order (think pushing them
// onto the end of the chain).
//
// This function may modify the validation state of nodes in the block index
// without flushing in the case the chain is not able to reorganize due to a
// block failing to connect.
//
// This function MUST be called with the chain state lock held (for writes).
func (b *BlockChain) reorganizeChain(targetTip *blockNode) error {
// Nothing to do if there is no target tip or the target tip is already the
// current tip.
if targetTip == nil {
return nil
}
origTip := b.bestChain.Tip()
if origTip == targetTip {
return nil
}
b.chainLock.Unlock()
b.sendNotification(NTReorganization, reorgData)
b.chainLock.Lock()
// Send a notification announcing the start of the chain reorganization.
b.chainLock.Unlock()
@ -1330,93 +1276,47 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
b.chainLock.Lock()
}()
// Reset the view for the actual connection code below. This is
// required because the view was previously modified when checking if
// the reorg would be successful and the connection code requires the
// view to be valid from the viewpoint of each block being connected or
// disconnected.
view = NewUtxoViewpoint()
view.SetBestHash(&oldBest.hash)
// Disconnect blocks from the main chain.
for i, e := 0, detachNodes.Front(); e != nil; i, e = i+1, e.Next() {
// Since the blocks are being detached in reverse order, the parent of
// current block being detached is the next one being detached up to
// the final one at which point it's the block that is already saved
// from the next block to detach above.
n := e.Value.(*blockNode)
block := detachBlocks[i]
parent := nextBlockToDetach
if i < len(detachBlocks)-1 {
parent = detachBlocks[i+1]
}
if n.parent.hash != *parent.Hash() {
panicf("detach block node hash %v (height %v) parent hash %v does "+
"not match previous parent block hash %v", &n.hash, n.height,
&n.parent.hash, parent.Hash())
// Attempt to reorganize to the chain to the new tip. In the case it fails,
// reorganize back to the original tip. There is no way to recover if the
// chain fails to reorganize back to the original tip since something is
// very wrong if a chain tip that was already known to be valid fails to
// reconnect.
//
// NOTE: The failure handling makes an assumption that a block in the path
// between the fork point and original tip are not somehow invalidated in
// between the point a reorged chain fails to connect and the reorg back to
// the original tip. That is a safe assumption with the current code due to
// all modifications which mark blocks invalid being performed under the
// chain lock, however, this will need to be reworked if that assumption is
// violated.
fork := b.bestChain.FindFork(targetTip)
reorgErr := b.reorganizeChainInternal(targetTip)
if reorgErr != nil {
if err := b.reorganizeChainInternal(origTip); err != nil {
panicf("failed to reorganize back to known good chain tip %s "+
"(height %d): %v -- probable database corruption", origTip.hash,
origTip.height, err)
}
// Update the view to unspend all of the spent txos and remove the utxos
// created by the block. Also, if the block votes against its parent,
// reconnect all of the regular transactions.
err := view.disconnectBlock(b.db, block, parent, detachSpentTxOuts[i])
if err != nil {
return err
}
// Update the database and chain state.
err = b.disconnectBlock(n, block, parent, view)
if err != nil {
return err
}
return reorgErr
}
// Connect the new best chain blocks.
for i, e := 0, attachNodes.Front(); e != nil; i, e = i+1, e.Next() {
// Grab the block to attach based on the node. Use the fact that the
// parent of the block is either the fork point for the first node being
// attached or the previous one that was attached for subsequent blocks
// to optimize.
n := e.Value.(*blockNode)
block := attachBlocks[i]
parent := forkBlock
if i > 0 {
parent = attachBlocks[i-1]
}
if n.parent.hash != *parent.Hash() {
panicf("attach block node hash %v (height %v) parent hash %v does "+
"not match previous parent block hash %v", &n.hash, n.height,
&n.parent.hash, parent.Hash())
}
// Send a notification that a blockchain reorganization took place.
reorgData := &ReorganizationNtfnsData{origTip.hash, origTip.height,
targetTip.hash, targetTip.height}
b.chainLock.Unlock()
b.sendNotification(NTReorganization, reorgData)
b.chainLock.Lock()
// Update the view to mark all utxos referenced by the block as spent
// and add all transactions being created by this block to it. In the
// case the block votes against the parent, also disconnect all of the
// regular transactions in the parent block. Finally, provide an stxo
// slice so the spent txout details are generated.
stxos := make([]spentTxOut, 0, countSpentOutputs(block))
err := view.connectBlock(b.db, block, parent, &stxos)
if err != nil {
return err
}
// Update the database and chain state.
err = b.connectBlock(n, block, parent, view, stxos)
if err != nil {
return err
}
// Log the point where the chain forked and old and new best chain tips.
if fork != nil {
log.Infof("REORGANIZE: Chain forks at %v (height %v)", fork.hash,
fork.height)
}
// Log the point where the chain forked and old and new best chain
// heads.
if forkNode != nil {
log.Infof("REORGANIZE: Chain forks at %v (height %v)",
forkNode.hash, forkNode.height)
}
log.Infof("REORGANIZE: Old best chain head was %v (height %v)",
&oldBest.hash, oldBest.height)
log.Infof("REORGANIZE: New best chain head is %v (height %v)",
newBest.hash, newBest.height)
log.Infof("REORGANIZE: Old best chain tip was %v (height %v)",
&origTip.hash, origTip.height)
log.Infof("REORGANIZE: New best chain tip is %v (height %v)",
targetTip.hash, targetTip.height)
return nil
}
@ -1524,8 +1424,7 @@ func (b *BlockChain) forceHeadReorganization(formerBest chainhash.Hash, newBest
// block index to the database. It is safe to ignore any flushing
// errors here as the only time the index will be modified is if the
// block failed to connect.
attach, detach := b.getReorganizeNodes(newBestNode)
err := b.reorganizeChain(attach, detach)
err := b.reorganizeChain(newBestNode)
b.flushBlockIndexWarnOnly()
return err
}
@ -1694,15 +1593,14 @@ func (b *BlockChain) connectBestChain(node *blockNode, block, parent *dcrutil.Bl
// find the common ancestor of both sides of the fork, disconnect the
// blocks that form the (now) old fork from the main chain, and attach
// the blocks that form the new chain to the main chain starting at the
// common ancenstor (the point where the chain forked).
detachNodes, attachNodes := b.getReorganizeNodes(node)
// common ancestor (the point where the chain forked).
//
// Reorganize the chain and flush any potential unsaved changes to the
// block index to the database. It is safe to ignore any flushing
// errors here as the only time the index will be modified is if the
// block failed to connect.
log.Infof("REORGANIZE: Block %v is causing a reorganize.", node.hash)
err := b.reorganizeChain(detachNodes, attachNodes)
err := b.reorganizeChain(node)
b.flushBlockIndexWarnOnly()
if err != nil {
return 0, err

View File

@ -61,8 +61,8 @@ const (
// NTChainReorgDone indicates that a chain reorganization has concluded.
NTChainReorgDone
// NTReorganization indicates that a blockchain reorganization is in
// progress.
// NTReorganization indicates that a blockchain reorganization has taken
// place.
NTReorganization
// NTSpentAndMissedTickets indicates spent or missed tickets from a newly

View File

@ -247,9 +247,8 @@ func newUtxoEntry(txVersion uint16, height uint32, index uint32, isCoinBase bool
// The unspent outputs are needed by other transactions for things such as
// script validation and double spend prevention.
type UtxoViewpoint struct {
entries map[chainhash.Hash]*UtxoEntry
cachedStxos map[chainhash.Hash][]spentTxOut
bestHash chainhash.Hash
entries map[chainhash.Hash]*UtxoEntry
bestHash chainhash.Hash
}
// BestHash returns the hash of the best block in the chain the view currently
@ -536,25 +535,20 @@ func (view *UtxoViewpoint) disconnectStakeTransactions(block *dcrutil.Block, stx
// information.
//func (view *UtxoViewpoint) disconnectDisapprovedBlock(db database.DB, block *dcrutil.Block, stxos []spentTxOut) error {
func (view *UtxoViewpoint) disconnectDisapprovedBlock(db database.DB, block *dcrutil.Block) error {
// Load all of the spent txos for the block either from the cached values
// or from the database spend journal.
// Load all of the spent txos for the block from the database spend journal.
var stxos []spentTxOut
if blockStxos, ok := view.cachedStxos[*block.Hash()]; ok {
stxos = blockStxos
} else {
err := db.View(func(dbTx database.Tx) error {
var err error
stxos, err = dbFetchSpendJournalEntry(dbTx, block)
return err
})
if err != nil {
return err
}
err := db.View(func(dbTx database.Tx) error {
var err error
stxos, err = dbFetchSpendJournalEntry(dbTx, block)
return err
})
if err != nil {
return err
}
// Load all of the utxos referenced by the inputs for all transactions in
// the block that don't already exist in the utxo view from the database.
err := view.fetchRegularInputUtxos(db, block)
err = view.fetchRegularInputUtxos(db, block)
if err != nil {
return err
}
@ -878,8 +872,7 @@ func (view *UtxoViewpoint) clone() *UtxoViewpoint {
// NewUtxoViewpoint returns a new empty unspent transaction output view.
func NewUtxoViewpoint() *UtxoViewpoint {
return &UtxoViewpoint{
entries: make(map[chainhash.Hash]*UtxoEntry),
cachedStxos: make(map[chainhash.Hash][]spentTxOut),
entries: make(map[chainhash.Hash]*UtxoEntry),
}
}