From 37cd482db3ea5e7f1a88689381788b43e7ca8e19 Mon Sep 17 00:00:00 2001 From: stasatdaglabs <39559713+stasatdaglabs@users.noreply.github.com> Date: Thu, 30 Aug 2018 16:00:03 +0300 Subject: [PATCH] [DEV-75] Update UTXO model to work with Diffs (#53) * [DEV-75] Removed fetchEntryByHash, which was no longer used. * [DEV-75] Removed a few functions in manager.go that weren't used by anything. * [DEV-75] checkConnectBlock will soon not accept a utxoViewpoint, so removed the call to view.Tip() so that it could be deleted. * [DEV-75] Got rid of tips in UtxoViewpoint. * [DEV-75] Added the full UTXO set to the virtual block. * [DEV-75] Implemented UTXO-wrangling when adding a new block. * [DEV-75] Added isCoinbase to utxoEntry creation. * [DEV-75] Added blockHeight to utxoEntry creation. * Implemented fetching the fullUTXOSet from the database. * [DEV-75] Simplified DAGState because almost all of the state in it was unnecessary. Also got rid of dbDAGState. * [DEV-75] Made the process around adding a new block, UTXO-wise, much safer. * [DEV-75] Implemented melding the virtual UTXO diff to the database. * [DEV-75] Fixed utxoSet loading from the wrong bucket. * [DEV-75] Began pruning utxoviewpoint.go. Replaced FetchUtxoEntry with a fullUTXOSet-based GetUTXOEntry. * [DEV-75] Removed fetchUtxos. * [DEV-75] Moved GetUTXOEntry into the virtual block. * [DEV-75] Updated IndexManager to not use utxoViewpoints. * [DEV-75] Fixed some bad login in restoreUTXO involving nodeDiffs. * [DEV-75] Got rid of the UTXO spend journal, which wasn't used anywhere. * [DEV-75] Moved some STXO-related validation logic out of connectToDAG and into checkConnectBlock. * [DEV-75] Renamed UtxoXxx to UTXOXxx. Removed a bunch of methods that were no longer used by anything. * [DEV-75] Another Utxo -> UTXO rename. * [DEV-75] Removed IsModified from UTXOView, which was not used anywhere. * [DEV-75] Renamed nodeDiff to provisionalNode. Added a bunch of comments. * [DEV-75] Removed the test for genesis in pastUTXO, since it can never happen. * [DEV-75] Wrote tests for errors in pastUTXO. * [DEV-75] Wrote tests for errors in restoreUTXO. * [DEV-75] Improved testErrorThroughPatching. * [DEV-75] Wrote tests for errors in verifyAndBuildUTXO. * [DEV-75] Used TipHashes instead of tips().hashes(), fixed comments in a few places, and brought back an error return that I erroneously removed. * [DEV-75] Removed UTXO set logs. * [DEV-75] Recreated add/remove/contains functions for utxoCollection. * [DEV-75] Changed newUTXOEntry to use an object initializer. * [DEV-75] Changed the UTXO bucket version to 1. * Added a comment that clarifies that the index is not initialized before initDAGState is called. * [DEV-75] Renamed GetVirtualBlock to VirtualBlock. * [DEV-75] Removed superfluous variables. * [DEV-75] Combined connectBlockToParents with updateParentsDiffs. * [DEV-75] Removed another superfluous variable. * [DEV-75] In pastUTXO, first fetch transactions from the database and only then add them. * [DEV-75] Reworded the comment for commit(). * [DEV-75] Made all the connectUTXO subfunctions not BlockDAG methods. * [DEV-75] Updated the comment for connectUTXO. * [DEV-75] Updated the comment explaining why we're creating provisionalNodes. * [DEV-75] Removed a couple of unnecessary calls to toProvisionalNode. * [DEV-75] Renamed connectUTXO to applyUTXOChanges. * [DEV-75] Replaced allProvisionalNodes with provisionalNodeSet, an object that holds provisionalNodes for this particular call to applyUTXOChanges. * [DEV-75] Changed createProvisionalNode to accept a boolean "withParents" instead of relying on the caller to supply the node's parents or an empty set. * [DEV-75] Made most applyUTXOChanges subfunctions be methods on provisionalNode. * [DEV-75] Fixed a couple of bad comments. * [DEV-75] Added descriptive error messages to callers of applyUTXOChanges. * [DEV-75] Fixed weird English. * [DEV-75] Replaced DAGState with a slice of DAG tip hashes. * [DEV-75] In createProvisionalNode, changed withParents to withRelatives to avoid certain kinds of attacks. * [DEV-75] Renamed createdProvisionalNode to newProvisionalNode. * [DEV-75] Added precalculating the amount of transactions inside a new block's blue set. * [DEV-75] Pruned unnecessary variable. --- Gopkg.lock | 8 +- Gopkg.toml | 4 + blockdag/blockindex.go | 3 +- blockdag/common_test.go | 12 +- blockdag/dag.go | 645 +++++++++++++-------- blockdag/dag_test.go | 103 +++- blockdag/dagio.go | 393 +++++-------- blockdag/dagio_test.go | 104 ++-- blockdag/fullblocks_test.go | 22 +- blockdag/indexers/addrindex.go | 18 +- blockdag/indexers/cfindex.go | 4 +- blockdag/indexers/common.go | 4 +- blockdag/indexers/manager.go | 22 +- blockdag/indexers/txindex.go | 4 +- blockdag/notifications_test.go | 2 +- blockdag/scriptval.go | 8 +- blockdag/scriptval_test.go | 2 +- blockdag/utxoset.go | 150 +++-- blockdag/utxoset_test.go | 70 ++- blockdag/{utxoviewpoint.go => utxoview.go} | 212 ++----- blockdag/validate.go | 82 +-- blockdag/validate_test.go | 2 +- blockdag/virtualblock.go | 63 +- blockdag/virtualblock_test.go | 8 +- cmd/findcheckpoint/findcheckpoint.go | 7 +- mempool/mempool.go | 10 +- mempool/mempool_test.go | 12 +- mempool/policy.go | 2 +- mining/cpuminer/cpuminer.go | 10 +- mining/mining.go | 38 +- mining/policy.go | 4 +- mining/policy_test.go | 14 +- netsync/manager.go | 41 +- server/p2p/p2p.go | 19 +- server/rpc/rpcserver.go | 123 ++-- 35 files changed, 1168 insertions(+), 1057 deletions(-) rename blockdag/{utxoviewpoint.go => utxoview.go} (64%) diff --git a/Gopkg.lock b/Gopkg.lock index aa2519975..618d4758c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,12 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + branch = "master" + name = "bou.ke/monkey" + packages = ["."] + revision = "21d0bd9ad94e052d4a8117b0910ba026bec94c99" + [[projects]] name = "github.com/aead/siphash" packages = ["."] @@ -95,6 +101,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "9910d3cbbf65bc6204c141bb24b416f79962d69115cba9a0e97a28246fb2a0ea" + inputs-digest = "5976f1edbea819ab26c66eb5e5604c255a72f174b7a47bb218f3b8a6733d0c3a" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 777b8bdc8..54cc860aa 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -59,6 +59,10 @@ branch = "master" name = "golang.org/x/crypto" +[[constraint]] + branch = "master" + name = "bou.ke/monkey" + [prune] go-tests = true unused-packages = true diff --git a/blockdag/blockindex.go b/blockdag/blockindex.go index df4134f2f..ca8593f0f 100644 --- a/blockdag/blockindex.go +++ b/blockdag/blockindex.go @@ -90,7 +90,7 @@ type blockNode struct { // diff is the UTXO representation of the block // A block's UTXO is reconstituted by applying diffWith on every block in the chain of diffChildren // from the virtual block down to the block. See diffChild - diff utxoDiff + diff *utxoDiff // diffChild is the child that diff will be built from. See diff diffChild *blockNode @@ -155,7 +155,6 @@ func initBlockNode(node *blockNode, blockHeader *wire.BlockHeader, parents block func addNodeAsChildToParents(node *blockNode) { for _, parent := range node.parents { parent.children.add(node) - parent.diffChild = node } } diff --git a/blockdag/common_test.go b/blockdag/common_test.go index 348326094..4dd89ee3e 100644 --- a/blockdag/common_test.go +++ b/blockdag/common_test.go @@ -115,10 +115,10 @@ func loadBlocks(filename string) (blocks []*util.Block, err error) { return } -// chainSetup is used to create a new db and chain instance with the genesis +// dagSetup is used to create a new db and chain instance with the genesis // block already inserted. In addition to the new chain instance, it returns // a teardown function the caller should invoke when done testing to clean up. -func chainSetup(dbName string, params *dagconfig.Params) (*BlockDAG, func(), error) { +func dagSetup(dbName string, params *dagconfig.Params) (*BlockDAG, func(), error) { if !isSupportedDbType(testDbType) { return nil, nil, fmt.Errorf("unsupported db type %v", testDbType) } @@ -187,8 +187,8 @@ func chainSetup(dbName string, params *dagconfig.Params) (*BlockDAG, func(), err return chain, teardown, nil } -// loadUtxoView returns a utxo view loaded from a file. -func loadUtxoView(filename string) (*UtxoViewpoint, error) { +// loadUTXOView returns a utxo view loaded from a file. +func loadUTXOView(filename string) (*UTXOView, error) { // The utxostore file format is: // // @@ -210,7 +210,7 @@ func loadUtxoView(filename string) (*UtxoViewpoint, error) { } defer fi.Close() - view := NewUtxoViewpoint() + view := NewUTXOView() for { // Hash of the utxo entry. var hash daghash.Hash @@ -245,7 +245,7 @@ func loadUtxoView(filename string) (*UtxoViewpoint, error) { } // Deserialize it and add it to the view. - entry, err := deserializeUtxoEntry(serialized) + entry, err := deserializeUTXOEntry(serialized) if err != nil { return nil, err } diff --git a/blockdag/dag.go b/blockdag/dag.go index 3efc34a5a..992a96d92 100644 --- a/blockdag/dag.go +++ b/blockdag/dag.go @@ -47,47 +47,6 @@ type orphanBlock struct { expiration time.Time } -// DAGState houses information about the current tips and other info -// related to the state of the DAG. -// -// The GetDAGState method can be used to obtain access to this information -// in a concurrent safe manner and the data will not be changed out from under -// the caller when chain state changes occur as the function name implies. -// However, the returned snapshot must be treated as immutable since it is -// shared by all callers. -type DAGState struct { - SelectedTip SelectedTipState // State of the selected tip - TipHashes []daghash.Hash // The hashes of the tips - TotalTxs uint64 // The total number of transactions in the DAG. -} - -type SelectedTipState struct { - Hash daghash.Hash // The hash of the tip. - Height int32 // The height of the tip. - Bits uint32 // The difficulty bits of the tip. - BlockSize uint64 // The size of the tip. - NumTxs uint64 // The number of transactions in the tip. - MedianTime time.Time // Median time as per CalcPastMedianTime. -} - -// newDAGState returns a new state instance for the given parameters. -func newDAGState(tipHashes []daghash.Hash, node *blockNode, blockSize, numTxs, - totalTxs uint64, medianTime time.Time) *DAGState { - - return &DAGState{ - SelectedTip: SelectedTipState{ - Hash: node.hash, - Height: node.height, - Bits: node.bits, - BlockSize: blockSize, - NumTxs: numTxs, - MedianTime: medianTime, - }, - TipHashes: tipHashes, - TotalTxs: totalTxs, - } -} - // BlockDAG provides functions for working with the bitcoin block chain. // It includes functionality such as rejecting duplicate blocks, ensuring blocks // follow all rules, orphan handling, checkpoint handling, and best chain @@ -126,7 +85,7 @@ type BlockDAG struct { index *blockIndex // virtual tracks the current tips. - virtual *virtualBlock + virtual *VirtualBlock // These fields are related to handling of orphan blocks. They are // protected by a combination of the chain lock and the orphan lock. @@ -140,20 +99,6 @@ type BlockDAG struct { nextCheckpoint *dagconfig.Checkpoint checkpointNode *blockNode - // The state is used as a fairly efficient way to cache information - // about the current DAG state that is returned to callers when - // requested. It operates on the principle of MVCC such that any time a - // new block becomes the best block, the state pointer is replaced with - // a new struct and the old state is left untouched. In this way, - // multiple callers can be pointing to different DAG states. - // This is acceptable for most callers because the state is only being - // queried at a specific point in time. - // - // In addition, some of the fields are stored in the database so the - // DAG state can be quickly reconstructed on load. - stateLock sync.RWMutex - dagState *DAGState - // The following caches are used to efficiently keep track of the // current deployment threshold state of each rule change deployment. // @@ -361,7 +306,7 @@ type SequenceLock struct { } // CalcSequenceLock computes a relative lock-time SequenceLock for the passed -// transaction using the passed UtxoViewpoint to obtain the past median time +// transaction using the passed UTXOView to obtain the past median time // for blocks in which the referenced inputs of the transactions were included // within. The generated SequenceLock lock can be used in conjunction with a // block height, and adjusted median block time to determine if all the inputs @@ -369,7 +314,7 @@ type SequenceLock struct { // the candidate transaction to be included in a block. // // This function is safe for concurrent access. -func (dag *BlockDAG) CalcSequenceLock(tx *util.Tx, utxoView *UtxoViewpoint, mempool bool) (*SequenceLock, error) { +func (dag *BlockDAG) CalcSequenceLock(tx *util.Tx, utxoView *UTXOView, mempool bool) (*SequenceLock, error) { dag.dagLock.Lock() defer dag.dagLock.Unlock() @@ -380,7 +325,7 @@ func (dag *BlockDAG) CalcSequenceLock(tx *util.Tx, utxoView *UtxoViewpoint, memp // transaction. See the exported version, CalcSequenceLock for further details. // // This function MUST be called with the chain state lock held (for writes). -func (dag *BlockDAG) calcSequenceLock(node *blockNode, tx *util.Tx, utxoView *UtxoViewpoint, mempool bool) (*SequenceLock, error) { +func (dag *BlockDAG) calcSequenceLock(node *blockNode, tx *util.Tx, utxoView *UTXOView, mempool bool) (*SequenceLock, error) { // A value of -1 for each relative lock type represents a relative time // lock value that will allow a transaction to be included in a block // at any given height or time. @@ -487,135 +432,6 @@ func LockTimeToSequence(isSeconds bool, locktime uint64) uint64 { locktime>>wire.SequenceLockTimeGranularity } -// connectBlock handles connecting the passed node/block to the DAG. -// -// 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 -// 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. -// -// This function MUST be called with the chain state lock held (for writes). -func (dag *BlockDAG) connectBlock(node *blockNode, block *util.Block, view *UtxoViewpoint, stxos []spentTxOut) error { - // Sanity check the correct number of stxos are provided. - if len(stxos) != countSpentOutputs(block) { - return AssertError("connectBlock called with inconsistent " + - "spent transaction out information") - } - - // No warnings about unknown rules or versions until the chain is - // current. - if dag.isCurrent() { - // Warn if any unknown new rules are either about to activate or - // have already been activated. - if err := dag.warnUnknownRuleActivations(node); err != nil { - return err - } - - // Warn if a high enough percentage of the last blocks have - // unexpected versions. - if err := dag.warnUnknownVersions(node); err != nil { - return err - } - } - - // Write any block status changes to DB before updating best state. - err := dag.index.flushToDB() - if err != nil { - return err - } - - // Generate a new state snapshot that will be used to update the - // database and later memory if all database updates are successful. - dag.stateLock.RLock() - currentTotalTxs := dag.dagState.TotalTxs - dag.stateLock.RUnlock() - numTxs := uint64(len(block.MsgBlock().Transactions)) - blockSize := uint64(block.MsgBlock().SerializeSize()) - state := newDAGState(view.tips.hashes(), node, blockSize, numTxs, - currentTotalTxs+numTxs, node.CalcPastMedianTime()) - - // Atomically insert info into the database. - err = dag.db.Update(func(dbTx database.Tx) error { - // Update best block state. - err := dbPutDAGState(dbTx, state) - if err != nil { - return err - } - - // Add the block hash and height to the block index which tracks - // the main chain. - err = dbPutBlockIndex(dbTx, block.Hash(), node.height) - if err != nil { - return err - } - - // 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. - err = dbPutUtxoView(dbTx, view) - if err != nil { - return err - } - - // Update the transaction spend journal by adding a record for - // the block that contains all txos spent by it. - err = dbPutSpendJournalEntry(dbTx, block.Hash(), stxos) - if err != nil { - return err - } - - // Allow the index manager to call each of the currently active - // optional indexes with the block being connected so they can - // update themselves accordingly. - if dag.indexManager != nil { - err := dag.indexManager.ConnectBlock(dbTx, block, view) - if err != nil { - return err - } - } - - return nil - }) - if err != nil { - return err - } - - // Prune fully spent entries and mark all entries in the view unmodified - // now that the modifications have been committed to the database. - view.commit() - - // This node is now at the end of the DAG. - dag.virtual.AddTip(node) - - // Update the state for the best block. Notice how this replaces the - // entire struct instead of updating the existing one. This effectively - // allows the old version to act as a snapshot which callers can use - // freely without needing to hold a lock for the duration. See the - // comments on the state variable for more details. - dag.setDAGState(state) - - // Notify the caller that the block was connected to the main chain. - // The caller would typically want to react with actions such as - // updating wallets. - dag.dagLock.Unlock() - dag.sendNotification(NTBlockConnected, dag.virtual.Tips().hashes()) - dag.dagLock.Lock() - - return nil -} - -// countSpentOutputs returns the number of utxos the passed block spends. -func countSpentOutputs(block *util.Block) int { - // Exclude the coinbase transaction since it can't spend anything. - var numSpent int - for _, tx := range block.Transactions()[1:] { - numSpent += len(tx.MsgTx().TxIn) - } - return numSpent -} - // connectToDAG handles connecting the passed block to the DAG. // // The flags modify the behavior of this function as follows: @@ -630,11 +446,8 @@ func (dag *BlockDAG) connectToDAG(node *blockNode, parentNodes blockSet, block * // Perform several checks to verify the block can be connected // to the DAG without violating any rules and without actually // connecting the block. - view := NewUtxoViewpoint() - view.SetTips(parentNodes) - stxos := make([]spentTxOut, 0, countSpentOutputs(block)) if !fastAdd { - err := dag.checkConnectBlock(node, block, view, &stxos) + err := dag.checkConnectBlock(node, block) if err == nil { dag.index.SetStatusFlags(node, statusValid) } else if _, ok := err.(RuleError); ok { @@ -657,23 +470,8 @@ func (dag *BlockDAG) connectToDAG(node *blockNode, parentNodes blockSet, block * } } - // 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(dag.db, block) - if err != nil { - return err - } - err = view.connectTransactions(node, block.Transactions(), &stxos) - if err != nil { - return err - } - } - // Connect the block to the DAG. - err := dag.connectBlock(node, block, view, stxos) + err := dag.connectBlock(node, block) if err != nil { return err } @@ -681,6 +479,382 @@ func (dag *BlockDAG) connectToDAG(node *blockNode, parentNodes blockSet, block * return nil } +// connectBlock handles connecting the passed node/block to the DAG. +// +// This function MUST be called with the chain state lock held (for writes). +func (dag *BlockDAG) connectBlock(node *blockNode, block *util.Block) error { + // No warnings about unknown rules or versions until the DAG is + // current. + if dag.isCurrent() { + // Warn if any unknown new rules are either about to activate or + // have already been activated. + if err := dag.warnUnknownRuleActivations(node); err != nil { + return err + } + + // Warn if a high enough percentage of the last blocks have + // unexpected versions. + if err := dag.warnUnknownVersions(node); err != nil { + return err + } + } + + // Add the node to the virtual and update the UTXO set of the DAG. + utxoDiff, err := dag.applyUTXOChanges(node, block) + if err != nil { + return err + } + + // Write any block status changes to DB before updating the DAG state. + err = dag.index.flushToDB() + if err != nil { + return err + } + + // Atomically insert info into the database. + err = dag.db.Update(func(dbTx database.Tx) error { + // Update best block state. + err := dbPutDAGTipHashes(dbTx, dag.virtual.TipHashes()) + if err != nil { + return err + } + + // Add the block hash and height to the block index which tracks + // the main chain. + err = dbPutBlockIndex(dbTx, block.Hash(), node.height) + if err != nil { + return err + } + + // Update the UTXO set using the diffSet that was melded into the + // full UTXO set. + err = dbPutUTXODiff(dbTx, utxoDiff) + if err != nil { + return err + } + + // Allow the index manager to call each of the currently active + // optional indexes with the block being connected so they can + // update themselves accordingly. + if dag.indexManager != nil { + err := dag.indexManager.ConnectBlock(dbTx, block, dag.virtual) + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + return err + } + + // Notify the caller that the block was connected to the main chain. + // The caller would typically want to react with actions such as + // updating wallets. + dag.dagLock.Unlock() + dag.sendNotification(NTBlockConnected, dag.virtual.TipHashes()) + dag.dagLock.Lock() + + return nil +} + +// applyUTXOChanges does the following: +// 1. Verifies that each transaction within the new block could spend an existing UTXO. +// 2. Connects each of the new block's parents to the block. +// 3. Adds the new block to the DAG's tips. +// 4. Updates the DAG's full UTXO set. +// 5. Updates each of the tips' utxoDiff. +// +// This function MUST be called with the chain state lock held (for writes). +func (dag *BlockDAG) applyUTXOChanges(node *blockNode, block *util.Block) (*utxoDiff, error) { + // Prepare provisionalNodes for all the relevant nodes to avoid modifying the original nodes. + // We avoid modifying the original nodes in this function because it could potentially + // fail if the block is not valid, thus bringing all the affected nodes (and the virtual) + // into an undefined state. + provisionalSet := newProvisionalNodeSet() + newNodeProvisional := provisionalSet.newProvisionalNode(node, true, block.Transactions()) + + // Clone the virtual block so that we don't modify the existing one. + virtualClone := dag.virtual.clone() + + newBlockUTXO, err := newNodeProvisional.verifyAndBuildUTXO(virtualClone, dag.db) + if err != nil { + return nil, fmt.Errorf("error verifying UTXO for %v: %s", node, err) + } + + err = newNodeProvisional.updateParents(virtualClone, newBlockUTXO) + if err != nil { + return nil, fmt.Errorf("failed updating parents of %v: %s", node, err) + } + + // Update the virtual block's children (the DAG tips) to include the new block. + virtualClone.AddTip(node) + + // Build a UTXO set for the new virtual block and update the DAG tips' diffs. + virtualNodeProvisional := provisionalSet.newProvisionalNode(&virtualClone.blockNode, true, nil) + newVirtualUTXO, err := virtualNodeProvisional.pastUTXO(virtualClone, dag.db) + if err != nil { + return nil, fmt.Errorf("could not restore past UTXO for virtual %v: %s", virtualClone, err) + } + + // Apply new utxoDiffs to all the tips + err = updateTipsUTXO(virtualNodeProvisional.parents, virtualClone, newVirtualUTXO) + if err != nil { + return nil, fmt.Errorf("failed updating the tips' UTXO: %s", err) + } + + // It is now safe to meld the UTXO set to base. + diffSet := newVirtualUTXO.(*diffUTXOSet) + utxoDiff := diffSet.utxoDiff + diffSet.meldToBase() + + // It is now safe to commit all the provisionalNodes + for _, provisional := range provisionalSet { + provisional.commit() + + // Set the status to valid for all index nodes to make sure the changes get + // written to the database. + if provisional != virtualNodeProvisional { + dag.index.SetStatusFlags(provisional.original, statusValid) + } + } + + // It is now safe to apply the new virtual block + dag.virtual = virtualClone + + return utxoDiff, nil +} + +// provisionalNodeSet is a temporary collection of provisionalNodes. It is used exclusively +// inside applyUTXOChanges. The purpose of this set is twofold: +// 1. Provide easy access to all provisionalNodes created inside this particular call to applyUTXOChanges +// 2. Avoid needless recreation of provisionalNodes +type provisionalNodeSet map[daghash.Hash]*provisionalNode + +// newProvisionalNodeSet creates an empty provisionalNodeSet +func newProvisionalNodeSet() provisionalNodeSet { + return provisionalNodeSet{} +} + +// provisionalNode is a temporary and partial copy of a blockNode. It is used exclusively +// inside applyUTXOChanges. We use this struct instead of the original blockNode because +// applyUTXOChanges has a few points of failure which, were we to modify it, would leave the +// blockNode in an undefined state. +// +// Important: provisionalNode.original must be treated as immutable. +type provisionalNode struct { + original *blockNode + selectedParent *provisionalNode + parents []*provisionalNode + children []*provisionalNode + diff *utxoDiff + diffChild *provisionalNode + transactions []*util.Tx +} + +// newProvisionalNode takes a node and builds a provisionalNode from it. +// To avoid building the entire DAG in provisionalNode format we pass withRelatives = true +// only when the node's relatives (parents and children) are required. +func (pns provisionalNodeSet) newProvisionalNode(node *blockNode, withRelatives bool, + transactions []*util.Tx) *provisionalNode { + if existingProvisional, ok := pns[node.hash]; ok { + return existingProvisional + } + + provisional := &provisionalNode{ + original: node, + parents: []*provisionalNode{}, + children: []*provisionalNode{}, + transactions: transactions, + } + if node.hash != zeroHash { + pns[node.hash] = provisional + } + + if withRelatives { + for _, parent := range node.parents { + provisional.parents = append(provisional.parents, pns.newProvisionalNode(parent, false, nil)) + } + if node.selectedParent != nil { + provisional.selectedParent = pns[node.selectedParent.hash] + } + + for _, child := range node.children { + provisional.children = append(provisional.children, pns.newProvisionalNode(child, false, nil)) + } + if node.diffChild != nil { + provisional.diffChild = pns[node.diffChild.hash] + } + } + if node.diff != nil { + provisional.diff = node.diff.clone() + } + + return provisional +} + +// verifyAndBuildUTXO verifies all transactions in the given block (in provisionalNode format) and builds its UTXO +func (p *provisionalNode) verifyAndBuildUTXO(virtual *VirtualBlock, db database.DB) (utxoSet, error) { + utxo, err := p.pastUTXO(virtual, db) + if err != nil { + return nil, err + } + + for _, tx := range p.transactions { + ok := utxo.addTx(tx.MsgTx(), p.original.height) + if !ok { + return nil, fmt.Errorf("transaction %v is not compatible with UTXO", tx) + } + } + + return utxo, nil +} + +// pastUTXO returns the UTXO of a given block's (in provisionalNode format) past +func (p *provisionalNode) pastUTXO(virtual *VirtualBlock, db database.DB) (utxoSet, error) { + pastUTXO, err := p.selectedParent.restoreUTXO(virtual) + if err != nil { + return nil, err + } + + // Fetch from the database all the transactions for this block's blue set (besides the selected parent) + var blueBlockTransactions []*util.Tx + err = db.View(func(tx database.Tx) error { + // Precalculate the amount of transactions in this block's blue set, besides the selected parent. + // This is to avoid an attack in which an attacker fabricates a block that will deliberately cause + // a lot of copying, causing a high cost to the whole network. + transactionCount := 0 + blueBlocks := make([]*util.Block, 0, len(p.original.blues)-1) + for i := len(p.original.blues) - 1; i >= 0; i-- { + blueBlockNode := p.original.blues[i] + if blueBlockNode == p.original.selectedParent { + continue + } + + blueBlock, err := dbFetchBlockByNode(tx, blueBlockNode) + if err != nil { + return err + } + + transactionCount += len(blueBlock.Transactions()) + blueBlocks = append(blueBlocks, blueBlock) + } + + blueBlockTransactions = make([]*util.Tx, 0, transactionCount) + for _, blueBlock := range blueBlocks { + blueBlockTransactions = append(blueBlockTransactions, blueBlock.Transactions()...) + } + + return nil + }) + if err != nil { + return nil, err + } + + // Add all transactions to the pastUTXO + // Purposefully ignore failures - these are just unaccepted transactions + for _, tx := range blueBlockTransactions { + _ = pastUTXO.addTx(tx.MsgTx(), p.original.height) + } + + return pastUTXO, nil +} + +// restoreUTXO restores the UTXO of a given block (in provisionalNode format) from its diff +func (p *provisionalNode) restoreUTXO(virtual *VirtualBlock) (utxoSet, error) { + stack := []*provisionalNode{p} + current := p + + for current.diffChild != nil { + current = current.diffChild + stack = append(stack, current) + } + + utxo := utxoSet(virtual.utxoSet) + + var err error + for i := len(stack) - 1; i >= 0; i-- { + utxo, err = utxo.withDiff(stack[i].diff) + if err != nil { + return nil, err + } + } + + return utxo, nil +} + +// updateParents adds this block (in provisionalNode format) to the children sets of its parents +// and updates the diff of any parent whose DiffChild is this block +func (p *provisionalNode) updateParents(virtual *VirtualBlock, newBlockUTXO utxoSet) error { + virtualDiffFromNewBlock, err := virtual.utxoSet.diffFrom(newBlockUTXO) + if err != nil { + return err + } + + p.diff = virtualDiffFromNewBlock + + for _, parent := range p.parents { + if parent.diffChild == nil { + parentUTXO, err := parent.restoreUTXO(virtual) + if err != nil { + return err + } + parent.diffChild = p + parent.diff, err = newBlockUTXO.diffFrom(parentUTXO) + if err != nil { + return err + } + } + + parent.children = append(parent.children, p) + } + + return nil +} + +// updateTipsUTXO builds and applies new diff UTXOs for all the DAG's tips (in provisionalNode format) +func updateTipsUTXO(tipProvisionals []*provisionalNode, virtual *VirtualBlock, virtualUTXO utxoSet) error { + for _, tipProvisional := range tipProvisionals { + tipUTXO, err := tipProvisional.restoreUTXO(virtual) + if err != nil { + return err + } + tipProvisional.diff, err = virtualUTXO.diffFrom(tipUTXO) + if err != nil { + return err + } + } + + return nil +} + +// commit updates the original blockNode this provisionalNode was created from with all the changes made to it +func (p *provisionalNode) commit() { + if p.selectedParent != nil { + p.original.selectedParent = p.selectedParent.original + } + + parents := newSet() + for _, parent := range p.parents { + parents.add(parent.original) + } + p.original.parents = parents + + children := newSet() + for _, child := range p.children { + children.add(child.original) + } + p.original.children = children + + if p.diff != nil { + p.original.diff = p.diff + } + if p.diffChild != nil { + p.original.diffChild = p.diffChild.original + } +} + // isCurrent returns whether or not the chain believes it is current. Several // factors are used to guess, but the key factors that allow the chain to // believe it is current are: @@ -719,31 +893,13 @@ func (dag *BlockDAG) IsCurrent() bool { return dag.isCurrent() } -// GetDAGState returns information about the DAG and related state as of the -// current point in time. The returned instance must be treated as immutable -// since it is shared by all callers. +// VirtualBlock returns the DAG's virtual block in the current point in time. +// The returned instance must be treated as immutable since it is shared by all +// callers. // // This function is safe for concurrent access. -func (dag *BlockDAG) GetDAGState() *DAGState { - dag.stateLock.RLock() - defer func() { - dag.stateLock.RUnlock() - }() - - return dag.dagState -} - -// setDAGState sets information about the DAG and related state as of the -// current point in time. -// -// This function is safe for concurrent access. -func (dag *BlockDAG) setDAGState(dagState *DAGState) { - dag.stateLock.Lock() - defer func() { - dag.stateLock.Unlock() - }() - - dag.dagState = dagState +func (dag *BlockDAG) VirtualBlock() *VirtualBlock { + return dag.virtual } // HeaderByHash returns the block header identified by the given hash or an @@ -1112,11 +1268,11 @@ type IndexManager interface { // ConnectBlock is invoked when a new block has been connected to the // main chain. - ConnectBlock(database.Tx, *util.Block, *UtxoViewpoint) error + ConnectBlock(database.Tx, *util.Block, *VirtualBlock) error // DisconnectBlock is invoked when a block has been disconnected from // the main chain. - DisconnectBlock(database.Tx, *util.Block, *UtxoViewpoint) error + DisconnectBlock(database.Tx, *util.Block, *VirtualBlock) error } // Config is a descriptor which specifies the blockchain instance configuration. @@ -1209,7 +1365,7 @@ func New(config *Config) (*BlockDAG, error) { targetTimePerBlock := int64(params.TargetTimePerBlock / time.Second) adjustmentFactor := params.RetargetAdjustmentFactor index := newBlockIndex(config.DB, params) - b := BlockDAG{ + dag := BlockDAG{ checkpoints: config.Checkpoints, checkpointsByHeight: checkpointsByHeight, db: config.DB, @@ -1222,7 +1378,6 @@ func New(config *Config) (*BlockDAG, error) { blocksPerRetarget: int32(targetTimespan / targetTimePerBlock), index: index, virtual: newVirtualBlock(nil, params.K), - genesis: index.LookupNode(params.GenesisHash), orphans: make(map[daghash.Hash]*orphanBlock), prevOrphans: make(map[daghash.Hash][]*orphanBlock), warningCaches: newThresholdCaches(vbNumBits), @@ -1232,28 +1387,32 @@ func New(config *Config) (*BlockDAG, error) { // Initialize the chain state from the passed database. When the db // does not yet contain any chain state, both it and the chain state // will be initialized to contain only the genesis block. - if err := b.initDAGState(); err != nil { + if err := dag.initDAGState(); err != nil { return nil, err } + // Save a reference to the genesis block. Note that we may only get + // an index reference to it here because the index is uninitialized + // before initDAGState. + dag.genesis = index.LookupNode(params.GenesisHash) + // Initialize and catch up all of the currently active optional indexes // as needed. if config.IndexManager != nil { - err := config.IndexManager.Init(&b, config.Interrupt) + err := config.IndexManager.Init(&dag, config.Interrupt) if err != nil { return nil, err } } // Initialize rule change threshold state caches. - if err := b.initThresholdCaches(); err != nil { + if err := dag.initThresholdCaches(); err != nil { return nil, err } - selectedTip := b.virtual.SelectedTip() - log.Infof("DAG state (height %d, hash %v, totaltx %d, work %v)", - selectedTip.height, selectedTip.hash, b.dagState.TotalTxs, - selectedTip.workSum) + selectedTip := dag.virtual.SelectedTip() + log.Infof("DAG state (height %d, hash %v, work %v)", + selectedTip.height, selectedTip.hash, selectedTip.workSum) - return &b, nil + return &dag, nil } diff --git a/blockdag/dag_test.go b/blockdag/dag_test.go index e81242c3a..cbfbc5636 100644 --- a/blockdag/dag_test.go +++ b/blockdag/dag_test.go @@ -5,7 +5,11 @@ package blockdag import ( + "bou.ke/monkey" + "errors" + "github.com/daglabs/btcd/database" "reflect" + "strings" "testing" "time" @@ -38,7 +42,7 @@ func TestHaveBlock(t *testing.T) { } // Create a new database and chain instance to run tests against. - chain, teardownFunc, err := chainSetup("haveblock", + chain, teardownFunc, err := dagSetup("haveblock", &dagconfig.MainNetParams) if err != nil { t.Errorf("Failed to setup chain instance: %v", err) @@ -168,9 +172,8 @@ func TestCalcSequenceLock(t *testing.T) { Value: 10, }}, }) - utxoView := NewUtxoViewpoint() + utxoView := NewUTXOView() utxoView.AddTxOuts(targetTx, int32(numBlocksToGenerate)-4) - utxoView.SetTips(setFromSlice(node)) // Create a utxo that spends the fake utxo created above for use in the // transactions created in the tests. It has an age of 4 blocks. Note @@ -214,7 +217,7 @@ func TestCalcSequenceLock(t *testing.T) { tests := []struct { tx *wire.MsgTx - view *UtxoViewpoint + view *UTXOView mempool bool want *SequenceLock }{ @@ -646,3 +649,95 @@ func TestIntervalBlockHashes(t *testing.T) { } } } + +// TestPastUTXOErrors tests all error-cases in restoreUTXO. +// The non-error-cases are tested in the more general tests. +func TestVerifyAndBuildUTXOErrors(t *testing.T) { + targetErrorMessage := "not compatible with UTXO" + testErrorThroughPatching( + t, + targetErrorMessage, + (*diffUTXOSet).addTx, + func(fus *diffUTXOSet, tx *wire.MsgTx, blockHeight int32) bool { + return false + }, + ) +} + +// TestPastUTXOErrors tests all error-cases in restoreUTXO. +// The non-error-cases are tested in the more general tests. +func TestPastUTXOErrors(t *testing.T) { + targetErrorMessage := "dbFetchBlockByNode error" + testErrorThroughPatching( + t, + targetErrorMessage, + dbFetchBlockByNode, + func(dbTx database.Tx, node *blockNode) (*util.Block, error) { + return nil, errors.New(targetErrorMessage) + }, + ) +} + +// TestRestoreUTXOErrors tests all error-cases in restoreUTXO. +// The non-error-cases are tested in the more general tests. +func TestRestoreUTXOErrors(t *testing.T) { + targetErrorMessage := "withDiff error" + testErrorThroughPatching( + t, + targetErrorMessage, + (*fullUTXOSet).withDiff, + func(fus *fullUTXOSet, other *utxoDiff) (utxoSet, error) { + return nil, errors.New(targetErrorMessage) + }, + ) +} + +func testErrorThroughPatching(t *testing.T, expectedErrorMessage string, targetFunction interface{}, replacementFunction interface{}) { + // Load up blocks such that there is a fork in the DAG. + // (genesis block) -> 1 -> 2 -> 3 -> 4 + // \-> 3b + testFiles := []string{ + "blk_0_to_4.dat", + "blk_3B.dat", + } + + var blocks []*util.Block + for _, file := range testFiles { + blockTmp, err := loadBlocks(file) + if err != nil { + t.Fatalf("Error loading file: %v\n", err) + } + blocks = append(blocks, blockTmp...) + } + + // Create a new database and dag instance to run tests against. + dag, teardownFunc, err := dagSetup("testErrorThroughPatching", &dagconfig.MainNetParams) + if err != nil { + t.Fatalf("Failed to setup dag instance: %v", err) + } + defer teardownFunc() + + // Since we're not dealing with the real block chain, set the coinbase + // maturity to 1. + dag.TstSetCoinbaseMaturity(1) + + monkey.Patch(targetFunction, replacementFunction) + + err = nil + for i := 1; i < len(blocks); i++ { + _, err = dag.ProcessBlock(blocks[i], BFNone) + if err != nil { + break + } + } + if err == nil { + t.Errorf("ProcessBlock unexpectedly succeeded. "+ + "Expected: %s", expectedErrorMessage) + } + if !strings.Contains(err.Error(), expectedErrorMessage) { + t.Errorf("ProcessBlock returned wrong error. "+ + "Want: %s, got: %s", expectedErrorMessage, err) + } + + monkey.Unpatch(targetFunction) +} diff --git a/blockdag/dagio.go b/blockdag/dagio.go index b241b58b8..1a9a5677b 100644 --- a/blockdag/dagio.go +++ b/blockdag/dagio.go @@ -7,16 +7,14 @@ package blockdag import ( "bytes" "encoding/binary" + "encoding/json" "fmt" "sync" - "time" - - "encoding/json" "github.com/daglabs/btcd/dagconfig/daghash" "github.com/daglabs/btcd/database" - "github.com/daglabs/btcd/wire" "github.com/daglabs/btcd/util" + "github.com/daglabs/btcd/wire" ) const ( @@ -25,14 +23,9 @@ const ( // wire.MaxBlockHeaderPayload is quite long. blockHdrSize = wire.MaxBlockHeaderPayload - // latestUtxoSetBucketVersion is the current version of the utxo set + // latestUTXOSetBucketVersion is the current version of the UTXO set // bucket that is used to track all unspent outputs. - latestUtxoSetBucketVersion = 2 - - // latestSpendJournalBucketVersion is the current version of the spend - // journal bucket that is used to track all spent transactions for use - // in reorgs. - latestSpendJournalBucketVersion = 1 + latestUTXOSetBucketVersion = 1 ) var ( @@ -48,17 +41,9 @@ var ( // the block height -> block hash index. heightIndexBucketName = []byte("heightidx") - // dagStateKeyName is the name of the db key used to store the DAG - // state. - dagStateKeyName = []byte("dagstate") - - // spendJournalVersionKeyName is the name of the db key used to store - // the version of the spend journal currently in the database. - spendJournalVersionKeyName = []byte("spendjournalversion") - - // spendJournalBucketName is the name of the db bucket used to house - // transactions outputs that are spent in each block. - spendJournalBucketName = []byte("spendjournal") + // dagTipHashesKeyName is the name of the db key used to store the DAG + // tip hashes. + dagTipHashesKeyName = []byte("dagtiphashes") // utxoSetVersionKeyName is the name of the db key used to store the // version of the utxo set currently in the database. @@ -66,7 +51,7 @@ var ( // utxoSetBucketName is the name of the db bucket used to house the // unspent transaction output set. - utxoSetBucketName = []byte("utxosetv2") + utxoSetBucketName = []byte("utxoset") // byteOrder is the preferred byte order used for serializing numeric // fields for storage in the database. @@ -105,18 +90,6 @@ func isDeserializeErr(err error) bool { return ok } -// dbFetchVersion fetches an individual version with the given key from the -// metadata bucket. It is primarily used to track versions on entities such as -// buckets. It returns zero if the provided key does not exist. -func dbFetchVersion(dbTx database.Tx, key []byte) uint32 { - serialized := dbTx.Metadata().Get(key) - if serialized == nil { - return 0 - } - - return byteOrder.Uint32(serialized[:]) -} - // dbPutVersion uses an existing database transaction to update the provided // key in the metadata bucket to the given version. It is primarily used to // track versions on entities such as buckets. @@ -126,24 +99,6 @@ func dbPutVersion(dbTx database.Tx, key []byte, version uint32) error { return dbTx.Metadata().Put(key, serialized[:]) } -// dbFetchOrCreateVersion uses an existing database transaction to attempt to -// fetch the provided key from the metadata bucket as a version and in the case -// it doesn't exist, it adds the entry with the provided default version and -// returns that. This is useful during upgrades to automatically handle loading -// and adding version keys as necessary. -func dbFetchOrCreateVersion(dbTx database.Tx, key []byte, defaultVersion uint32) (uint32, error) { - version := dbFetchVersion(dbTx, key) - if version == 0 { - version = defaultVersion - err := dbPutVersion(dbTx, key, version) - if err != nil { - return 0, err - } - } - - return version, nil -} - // ----------------------------------------------------------------------------- // The transaction spend journal consists of an entry for each block connected // to the main chain which contains the transaction outputs the block spends @@ -152,7 +107,7 @@ func dbFetchOrCreateVersion(dbTx database.Tx, key []byte, defaultVersion uint32) // This is required because reorganizing the chain necessarily entails // disconnecting blocks to get back to the point of the fork which implies // unspending all of the transaction outputs that each block previously spent. -// Since the utxo set, by definition, only contains unspent transaction outputs, +// Since the UTXO set, by definition, only contains unspent transaction outputs, // the spent transaction outputs must be resurrected from somewhere. There is // more than one way this could be done, however this is the most straight // forward method that does not require having a transaction index and unpruned @@ -160,7 +115,7 @@ func dbFetchOrCreateVersion(dbTx database.Tx, key []byte, defaultVersion uint32) // // NOTE: This format is NOT self describing. The additional details such as // the number of entries (transaction inputs) are expected to come from the -// block itself and the utxo set (for legacy entries). The rationale in doing +// block itself and the UTXO set (for legacy entries). The rationale in doing // this is to save space. This is also the reason the spent outputs are // serialized in the reverse order they are spent because later transactions are // allowed to spend outputs from earlier ones in the same block. @@ -410,25 +365,15 @@ func serializeSpendJournalEntry(stxos []spentTxOut) []byte { return serialized } -// dbPutSpendJournalEntry uses an existing database transaction to update the -// spend journal entry for the given block hash using the provided slice of -// spent txouts. The spent txouts slice must contain an entry for every txout -// the transactions in the block spend in the order they are spent. -func dbPutSpendJournalEntry(dbTx database.Tx, blockHash *daghash.Hash, stxos []spentTxOut) error { - spendBucket := dbTx.Metadata().Bucket(spendJournalBucketName) - serialized := serializeSpendJournalEntry(stxos) - return spendBucket.Put(blockHash[:], serialized) -} - // ----------------------------------------------------------------------------- -// The unspent transaction output (utxo) set consists of an entry for each +// The unspent transaction output (UTXO) set consists of an entry for each // unspent output using a format that is optimized to reduce space using domain // specific compression algorithms. This format is a slightly modified version // of the format used in Bitcoin Core. // // Each entry is keyed by an outpoint as specified below. It is important to // note that the key encoding uses a VLQ, which employs an MSB encoding so -// iteration of utxos when doing byte-wise comparisons will produce them in +// iteration of UTXOs when doing byte-wise comparisons will produce them in // order. // // The serialized key format is: @@ -511,7 +456,7 @@ var outpointKeyPool = sync.Pool{ }, } -// outpointKey returns a key suitable for use as a database key in the utxo set +// outpointKey returns a key suitable for use as a database key in the UTXO set // while making use of a free list. A new buffer is allocated if there are not // already any available on the free list. The returned byte slice should be // returned to the free list by using the recycleOutpointKey function when the @@ -519,7 +464,7 @@ var outpointKeyPool = sync.Pool{ // the caller can calculate such as when used to write to the database. func outpointKey(outpoint wire.OutPoint) *[]byte { // A VLQ employs an MSB encoding, so they are useful not only to reduce - // the amount of storage space, but also so iteration of utxos when + // the amount of storage space, but also so iteration of UTXOs when // doing byte-wise comparisons will produce them in order. key := outpointKeyPool.Get().(*[]byte) idx := uint64(outpoint.Index) @@ -537,9 +482,9 @@ func recycleOutpointKey(key *[]byte) { // utxoEntryHeaderCode returns the calculated header code to be used when // serializing the provided utxo entry. -func utxoEntryHeaderCode(entry *UtxoEntry) (uint64, error) { +func utxoEntryHeaderCode(entry *UTXOEntry) (uint64, error) { if entry.IsSpent() { - return 0, AssertError("attempt to serialize spent utxo header") + return 0, AssertError("attempt to serialize spent UTXO header") } // As described in the serialization format comments, the header code @@ -553,9 +498,9 @@ func utxoEntryHeaderCode(entry *UtxoEntry) (uint64, error) { return headerCode, nil } -// serializeUtxoEntry returns the entry serialized to a format that is suitable +// serializeUTXOEntry returns the entry serialized to a format that is suitable // for long-term storage. The format is described in detail above. -func serializeUtxoEntry(entry *UtxoEntry) ([]byte, error) { +func serializeUTXOEntry(entry *UTXOEntry) ([]byte, error) { // Spent outputs have no serialization. if entry.IsSpent() { return nil, nil @@ -581,10 +526,24 @@ func serializeUtxoEntry(entry *UtxoEntry) ([]byte, error) { return serialized, nil } -// deserializeUtxoEntry decodes a utxo entry from the passed serialized byte -// slice into a new UtxoEntry using a format that is suitable for long-term +// deserializeOutPoint decodes an outPoint from the passed serialized byte +// slice into a new wire.OutPoint using a format that is suitable for long- +// term storage. this format is described in detail above. +func deserializeOutPoint(serialized []byte) (*wire.OutPoint, error) { + if len(serialized) <= daghash.HashSize { + return nil, errDeserialize("unexpected end of data") + } + + hash := daghash.Hash{} + hash.SetBytes(serialized[:daghash.HashSize]) + index, _ := deserializeVLQ(serialized[daghash.HashSize:]) + return wire.NewOutPoint(&hash, uint32(index)), nil +} + +// deserializeUTXOEntry decodes a UTXO entry from the passed serialized byte +// slice into a new UTXOEntry using a format that is suitable for long-term // storage. The format is described in detail above. -func deserializeUtxoEntry(serialized []byte) (*UtxoEntry, error) { +func deserializeUTXOEntry(serialized []byte) (*UTXOEntry, error) { // Deserialize the header code. code, offset := deserializeVLQ(serialized) if offset >= len(serialized) { @@ -602,10 +561,10 @@ func deserializeUtxoEntry(serialized []byte) (*UtxoEntry, error) { amount, pkScript, _, err := decodeCompressedTxOut(serialized[offset:]) if err != nil { return nil, errDeserialize(fmt.Sprintf("unable to decode "+ - "utxo: %v", err)) + "UTXO: %v", err)) } - entry := &UtxoEntry{ + entry := &UTXOEntry{ amount: int64(amount), pkScript: pkScript, blockHeight: blockHeight, @@ -618,70 +577,38 @@ func deserializeUtxoEntry(serialized []byte) (*UtxoEntry, error) { return entry, nil } -// dbFetchUtxoEntryByHash attempts to find and fetch a utxo for the given hash. -// It uses a cursor and seek to try and do this as efficiently as possible. -// -// When there are no entries for the provided hash, nil will be returned for the -// both the entry and the error. -func dbFetchUtxoEntryByHash(dbTx database.Tx, hash *daghash.Hash) (*UtxoEntry, error) { - // Attempt to find an entry by seeking for the hash along with a zero - // index. Due to the fact the keys are serialized as , - // where the index uses an MSB encoding, if there are any entries for - // the hash at all, one will be found. - cursor := dbTx.Metadata().Bucket(utxoSetBucketName).Cursor() - key := outpointKey(wire.OutPoint{Hash: *hash, Index: 0}) - ok := cursor.Seek(*key) - recycleOutpointKey(key) - if !ok { - return nil, nil - } - - // An entry was found, but it could just be an entry with the next - // highest hash after the requested one, so make sure the hashes - // actually match. - cursorKey := cursor.Key() - if len(cursorKey) < daghash.HashSize { - return nil, nil - } - if !bytes.Equal(hash[:], cursorKey[:daghash.HashSize]) { - return nil, nil - } - - return deserializeUtxoEntry(cursor.Value()) -} - -// dbFetchUtxoEntry uses an existing database transaction to fetch the specified -// transaction output from the utxo set. +// dbFetchUTXOEntry uses an existing database transaction to fetch the specified +// transaction output from the UTXO set. // // When there is no entry for the provided output, nil will be returned for both // the entry and the error. -func dbFetchUtxoEntry(dbTx database.Tx, outpoint wire.OutPoint) (*UtxoEntry, error) { +func dbFetchUTXOEntry(dbTx database.Tx, outpoint wire.OutPoint) (*UTXOEntry, error) { // Fetch the unspent transaction output information for the passed // transaction output. Return now when there is no entry. key := outpointKey(outpoint) utxoBucket := dbTx.Metadata().Bucket(utxoSetBucketName) - serializedUtxo := utxoBucket.Get(*key) + serializedUTXO := utxoBucket.Get(*key) recycleOutpointKey(key) - if serializedUtxo == nil { + if serializedUTXO == nil { return nil, nil } // A non-nil zero-length entry means there is an entry in the database // for a spent transaction output which should never be the case. - if len(serializedUtxo) == 0 { + if len(serializedUTXO) == 0 { return nil, AssertError(fmt.Sprintf("database contains entry "+ "for spent tx output %v", outpoint)) } // Deserialize the utxo entry and return it. - entry, err := deserializeUtxoEntry(serializedUtxo) + entry, err := deserializeUTXOEntry(serializedUTXO) if err != nil { // Ensure any deserialization errors are returned as database // corruption errors. if isDeserializeErr(err) { return nil, database.Error{ ErrorCode: database.ErrCorruption, - Description: fmt.Sprintf("corrupt utxo entry "+ + Description: fmt.Sprintf("corrupt UTXO entry "+ "for %v: %v", outpoint, err), } } @@ -692,36 +619,29 @@ func dbFetchUtxoEntry(dbTx database.Tx, outpoint wire.OutPoint) (*UtxoEntry, err return entry, nil } -// dbPutUtxoView uses an existing database transaction to update the utxo set -// in the database based on the provided utxo view contents and state. In +// dbPutUTXODiff uses an existing database transaction to update the UTXO set +// in the database based on the provided UTXO view contents and state. In // particular, only the entries that have been marked as modified are written // to the database. -func dbPutUtxoView(dbTx database.Tx, view *UtxoViewpoint) error { +func dbPutUTXODiff(dbTx database.Tx, diff *utxoDiff) error { utxoBucket := dbTx.Metadata().Bucket(utxoSetBucketName) - for outpoint, entry := range view.entries { - // No need to update the database if the entry was not modified. - if entry == nil || !entry.isModified() { - continue - } - - // Remove the utxo entry if it is spent. - if entry.IsSpent() { - key := outpointKey(outpoint) - err := utxoBucket.Delete(*key) - recycleOutpointKey(key) - if err != nil { - return err - } - - continue - } - - // Serialize and store the utxo entry. - serialized, err := serializeUtxoEntry(entry) + for outPoint := range diff.toRemove { + key := outpointKey(outPoint) + err := utxoBucket.Delete(*key) + recycleOutpointKey(key) if err != nil { return err } - key := outpointKey(outpoint) + } + + for outPoint, entry := range diff.toAdd { + // Serialize and store the UTXO entry. + serialized, err := serializeUTXOEntry(entry) + if err != nil { + return err + } + + key := outpointKey(outPoint) err = utxoBucket.Put(*key, serialized) // NOTE: The key is intentionally not recycled here since the // database interface contract prohibits modifications. It will @@ -787,48 +707,38 @@ func dbFetchHeightByHash(dbTx database.Tx, hash *daghash.Hash) (int32, error) { return int32(byteOrder.Uint32(serializedHeight)), nil } -// dbDAGState represents the data to be stored in the database for the current -// DAG state. -type dbDAGState struct { - Tips []daghash.Hash - TotalTxs uint64 +// serializeDAGTipHashes returns the serialization of the DAG tip hashes. +// This is data to be stored in the DAG tip hashes bucket. +func serializeDAGTipHashes(tipHashes []daghash.Hash) ([]byte, error) { + return json.Marshal(tipHashes) } -// serializeDAGState returns the serialization of the DAG state. -// This is data to be stored in the DAG state bucket. -func serializeDAGState(state dbDAGState) ([]byte, error) { - return json.Marshal(state) -} - -// deserializeDAGState deserializes the passed serialized DAG -// state. This is data stored in the DAG state bucket and is updated after -// every block is connected or disconnected form the DAG. -func deserializeDAGState(serializedData []byte) (*dbDAGState, error) { - var dbState dbDAGState - err := json.Unmarshal(serializedData, &dbState) +// deserializeDAGTipHashes deserializes the passed serialized DAG tip hashes. +// This is data stored in the DAG tip hashes bucket and is updated after +// every block is connected to the DAG. +func deserializeDAGTipHashes(serializedData []byte) ([]daghash.Hash, error) { + var tipHashes []daghash.Hash + err := json.Unmarshal(serializedData, &tipHashes) if err != nil { return nil, database.Error{ ErrorCode: database.ErrCorruption, - Description: "corrupt DAG state", + Description: "corrupt DAG tip hashes", } } - return &dbState, nil + return tipHashes, nil } -// dbPutDAGState uses an existing database transaction to update the DAG -// state with the given parameters. -func dbPutDAGState(dbTx database.Tx, state *DAGState) error { - serializedData, err := serializeDAGState(dbDAGState{ - Tips: state.TipHashes, - TotalTxs: state.TotalTxs, - }) +// dbPutDAGTipHashes uses an existing database transaction to store the latest +// tip hashes of the DAG. +func dbPutDAGTipHashes(dbTx database.Tx, tipHashes []daghash.Hash) error { + serializedData, err := serializeDAGTipHashes(tipHashes) if err != nil { return err } - return dbTx.Metadata().Put(dagStateKeyName, serializedData) + return dbTx.Metadata().Put(dagTipHashesKeyName, serializedData) } // createDAGState initializes both the database and the DAG state to the @@ -841,19 +751,23 @@ func (dag *BlockDAG) createDAGState() error { header := &genesisBlock.MsgBlock().Header node := newBlockNode(header, newSet(), dag.dagParams.K) node.status = statusDataStored | statusValid + + genesisCoinbase := genesisBlock.Transactions()[0].MsgTx() + genesisCoinbaseTxIn := genesisCoinbase.TxIn[0] + genesisCoinbaseTxOut := genesisCoinbase.TxOut[0] + genesisCoinbaseOutpoint := *wire.NewOutPoint(&genesisCoinbaseTxIn.PreviousOutPoint.Hash, genesisCoinbaseTxIn.PreviousOutPoint.Index) + genesisCoinbaseUTXOEntry := newUTXOEntry(genesisCoinbaseTxOut, true, 0) + node.diff = &utxoDiff{ + toAdd: utxoCollection{genesisCoinbaseOutpoint: genesisCoinbaseUTXOEntry}, + toRemove: utxoCollection{}, + } + + dag.virtual.utxoSet.addTx(genesisCoinbase, 0) dag.virtual.SetTips(setFromSlice(node)) // Add the new node to the index which is used for faster lookups. dag.index.addNode(node) - // Initialize the DAG state. Since it is the genesis block, use - // its timestamp for the median time. - numTxs := uint64(len(genesisBlock.MsgBlock().Transactions)) - blockSize := uint64(genesisBlock.MsgBlock().SerializeSize()) - dagState := newDAGState(dag.virtual.Tips().hashes(), node, blockSize, numTxs, - numTxs, time.Unix(node.timestamp, 0)) - dag.setDAGState(dagState) - // Create the initial the database chain state including creating the // necessary index buckets and inserting the genesis block. err := dag.db.Update(func(dbTx database.Tx) error { @@ -879,18 +793,6 @@ func (dag *BlockDAG) createDAGState() error { return err } - // Create the bucket that houses the spend journal data and - // store its version. - _, err = meta.CreateBucket(spendJournalBucketName) - if err != nil { - return err - } - err = dbPutVersion(dbTx, utxoSetVersionKeyName, - latestUtxoSetBucketVersion) - if err != nil { - return err - } - // Create the bucket that houses the utxo set and store its // version. Note that the genesis block coinbase transaction is // intentionally not inserted here since it is not spendable by @@ -899,8 +801,8 @@ func (dag *BlockDAG) createDAGState() error { if err != nil { return err } - err = dbPutVersion(dbTx, spendJournalVersionKeyName, - latestSpendJournalBucketVersion) + err = dbPutVersion(dbTx, utxoSetVersionKeyName, + latestUTXOSetBucketVersion) if err != nil { return err } @@ -918,8 +820,8 @@ func (dag *BlockDAG) createDAGState() error { return err } - // Store the current DAG state into the database. - err = dbPutDAGState(dbTx, dag.dagState) + // Store the current DAG tip hashes into the database. + err = dbPutDAGTipHashes(dbTx, dag.virtual.TipHashes()) if err != nil { return err } @@ -938,7 +840,7 @@ func (dag *BlockDAG) initDAGState() error { // everything from scratch or upgrade certain buckets. var initialized bool err := dag.db.View(func(dbTx database.Tx) error { - initialized = dbTx.Metadata().Get(dagStateKeyName) != nil + initialized = dbTx.Metadata().Get(dagTipHashesKeyName) != nil return nil }) if err != nil { @@ -953,13 +855,13 @@ func (dag *BlockDAG) initDAGState() error { // Attempt to load the DAG state from the database. return dag.db.View(func(dbTx database.Tx) error { - // Fetch the stored DAG state from the database metadata. + // Fetch the stored DAG tipHashes from the database metadata. // When it doesn't exist, it means the database hasn't been // initialized for use with the DAG yet, so break out now to allow // that to happen under a writable database transaction. - serializedData := dbTx.Metadata().Get(dagStateKeyName) - log.Tracef("Serialized DAG state: %x", serializedData) - state, err := deserializeDAGState(serializedData) + serializedData := dbTx.Metadata().Get(dagTipHashesKeyName) + log.Tracef("Serialized DAG tip hashes: %x", serializedData) + tipHashes, err := deserializeDAGTipHashes(serializedData) if err != nil { return err } @@ -1025,36 +927,72 @@ func (dag *BlockDAG) initDAGState() error { i++ } - // Set the DAG view to the stored state. + // Load all of the known UTXO entries and construct the full + // UTXO set accordingly. Since the number of entries is already + // known, perform a single alloc for them versus a whole bunch + // of little ones to reduce pressure on the GC. + log.Infof("Loading UTXO set...") + + utxoEntryBucket := dbTx.Metadata().Bucket(utxoSetBucketName) + + // Determine how many UTXO entries will be loaded into the index so we can + // allocate the right amount. + var utxoEntryCount int32 + cursor = utxoEntryBucket.Cursor() + for ok := cursor.First(); ok; ok = cursor.Next() { + utxoEntryCount++ + } + + fullUTXOCollection := make(utxoCollection, utxoEntryCount) + for ok := cursor.First(); ok; ok = cursor.Next() { + // Deserialize the outPoint + outPoint, err := deserializeOutPoint(cursor.Key()) + if err != nil { + // Ensure any deserialization errors are returned as database + // corruption errors. + if isDeserializeErr(err) { + return database.Error{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf("corrupt outPoint: %v", err), + } + } + + return err + } + + // Deserialize the utxo entry + entry, err := deserializeUTXOEntry(cursor.Value()) + if err != nil { + // Ensure any deserialization errors are returned as database + // corruption errors. + if isDeserializeErr(err) { + return database.Error{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf("corrupt utxo entry: %v", err), + } + } + + return err + } + + fullUTXOCollection[*outPoint] = entry + } + + // Apply the loaded utxoCollection to the virtual block. + dag.virtual.utxoSet.utxoCollection = fullUTXOCollection + + // Apply the stored tips to the virtual block. tips := newSet() - for _, tipHash := range state.Tips { + for _, tipHash := range tipHashes { tip := dag.index.LookupNode(&tipHash) if tip == nil { return AssertError(fmt.Sprintf("initDAGState: cannot find "+ - "DAG tip %s in block index", state.Tips)) + "DAG tip %s in block index", tipHashes)) } tips.add(tip) } dag.virtual.SetTips(tips) - // Load the raw block bytes for the selected tip. - selectedTip := dag.virtual.selectedParent - blockBytes, err := dbTx.FetchBlock(&selectedTip.hash) - if err != nil { - return err - } - var block wire.MsgBlock - err = block.Deserialize(bytes.NewReader(blockBytes)) - if err != nil { - return err - } - - // Initialize the DAG state. - blockSize := uint64(len(blockBytes)) - numTxns := uint64(len(block.Transactions)) - dagState := newDAGState(dag.virtual.Tips().hashes(), selectedTip, blockSize, numTxns, state.TotalTxs, selectedTip.CalcPastMedianTime()) - dag.setDAGState(dagState) - return nil }) } @@ -1078,23 +1016,6 @@ func deserializeBlockRow(blockRow []byte) (*wire.BlockHeader, blockStatus, error return &header, blockStatus(statusByte), nil } -// dbFetchHeaderByHash uses an existing database transaction to retrieve the -// block header for the provided hash. -func dbFetchHeaderByHash(dbTx database.Tx, hash *daghash.Hash) (*wire.BlockHeader, error) { - headerBytes, err := dbTx.FetchBlockHeader(hash) - if err != nil { - return nil, err - } - - var header wire.BlockHeader - err = header.Deserialize(bytes.NewReader(headerBytes)) - if err != nil { - return nil, err - } - - return &header, nil -} - // dbFetchBlockByNode uses an existing database transaction to retrieve the // raw block for the provided node, deserialize it, and return a util.Block // with the height set. diff --git a/blockdag/dagio_test.go b/blockdag/dagio_test.go index 92ab494ee..01eb4edc0 100644 --- a/blockdag/dagio_test.go +++ b/blockdag/dagio_test.go @@ -410,14 +410,14 @@ func TestUtxoSerialization(t *testing.T) { tests := []struct { name string - entry *UtxoEntry + entry *UTXOEntry serialized []byte }{ // From tx in main blockchain: // b7c3332bc138e2c9429818f5fed500bcc1746544218772389054dc8047d7cd3f:0 { name: "height 1, coinbase", - entry: &UtxoEntry{ + entry: &UTXOEntry{ amount: 5000000000, pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac"), blockHeight: 1, @@ -429,7 +429,7 @@ func TestUtxoSerialization(t *testing.T) { // b7c3332bc138e2c9429818f5fed500bcc1746544218772389054dc8047d7cd3f:0 { name: "height 1, coinbase, spent", - entry: &UtxoEntry{ + entry: &UTXOEntry{ amount: 5000000000, pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac"), blockHeight: 1, @@ -441,7 +441,7 @@ func TestUtxoSerialization(t *testing.T) { // 8131ffb0a2c945ecaf9b9063e59558784f9c3a74741ce6ae2a18d0571dac15bb:1 { name: "height 100001, not coinbase", - entry: &UtxoEntry{ + entry: &UTXOEntry{ amount: 1000000, pkScript: hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc88ac"), blockHeight: 100001, @@ -453,7 +453,7 @@ func TestUtxoSerialization(t *testing.T) { // 8131ffb0a2c945ecaf9b9063e59558784f9c3a74741ce6ae2a18d0571dac15bb:1 { name: "height 100001, not coinbase, spent", - entry: &UtxoEntry{ + entry: &UTXOEntry{ amount: 1000000, pkScript: hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc88ac"), blockHeight: 100001, @@ -465,14 +465,14 @@ func TestUtxoSerialization(t *testing.T) { for i, test := range tests { // Ensure the utxo entry serializes to the expected value. - gotBytes, err := serializeUtxoEntry(test.entry) + gotBytes, err := serializeUTXOEntry(test.entry) if err != nil { - t.Errorf("serializeUtxoEntry #%d (%s) unexpected "+ + t.Errorf("serializeUTXOEntry #%d (%s) unexpected "+ "error: %v", i, test.name, err) continue } if !bytes.Equal(gotBytes, test.serialized) { - t.Errorf("serializeUtxoEntry #%d (%s): mismatched "+ + t.Errorf("serializeUTXOEntry #%d (%s): mismatched "+ "bytes - got %x, want %x", i, test.name, gotBytes, test.serialized) continue @@ -485,9 +485,9 @@ func TestUtxoSerialization(t *testing.T) { } // Deserialize to a utxo entry. - utxoEntry, err := deserializeUtxoEntry(test.serialized) + utxoEntry, err := deserializeUTXOEntry(test.serialized) if err != nil { - t.Errorf("deserializeUtxoEntry #%d (%s) unexpected "+ + t.Errorf("deserializeUTXOEntry #%d (%s) unexpected "+ "error: %v", i, test.name, err) continue } @@ -495,7 +495,7 @@ func TestUtxoSerialization(t *testing.T) { // The deserialized entry must not be marked spent since unspent // entries are not serialized. if utxoEntry.IsSpent() { - t.Errorf("deserializeUtxoEntry #%d (%s) output should "+ + t.Errorf("deserializeUTXOEntry #%d (%s) output should "+ "not be marked spent", i, test.name) continue } @@ -503,26 +503,26 @@ func TestUtxoSerialization(t *testing.T) { // Ensure the deserialized entry has the same properties as the // ones in the test entry. if utxoEntry.Amount() != test.entry.Amount() { - t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ + t.Errorf("deserializeUTXOEntry #%d (%s) mismatched "+ "amounts: got %d, want %d", i, test.name, utxoEntry.Amount(), test.entry.Amount()) continue } if !bytes.Equal(utxoEntry.PkScript(), test.entry.PkScript()) { - t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ + t.Errorf("deserializeUTXOEntry #%d (%s) mismatched "+ "scripts: got %x, want %x", i, test.name, utxoEntry.PkScript(), test.entry.PkScript()) continue } if utxoEntry.BlockHeight() != test.entry.BlockHeight() { - t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ + t.Errorf("deserializeUTXOEntry #%d (%s) mismatched "+ "block height: got %d, want %d", i, test.name, utxoEntry.BlockHeight(), test.entry.BlockHeight()) continue } if utxoEntry.IsCoinBase() != test.entry.IsCoinBase() { - t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ + t.Errorf("deserializeUTXOEntry #%d (%s) mismatched "+ "coinbase flag: got %v, want %v", i, test.name, utxoEntry.IsCoinBase(), test.entry.IsCoinBase()) continue @@ -537,13 +537,13 @@ func TestUtxoEntryHeaderCodeErrors(t *testing.T) { tests := []struct { name string - entry *UtxoEntry + entry *UTXOEntry code uint64 errType error }{ { name: "Force assertion due to spent output", - entry: &UtxoEntry{packedFlags: tfSpent}, + entry: &UTXOEntry{packedFlags: tfSpent}, errType: AssertError(""), }, } @@ -590,85 +590,79 @@ func TestUtxoEntryDeserializeErrors(t *testing.T) { for _, test := range tests { // Ensure the expected error type is returned and the returned // entry is nil. - entry, err := deserializeUtxoEntry(test.serialized) + entry, err := deserializeUTXOEntry(test.serialized) if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { - t.Errorf("deserializeUtxoEntry (%s): expected error "+ + t.Errorf("deserializeUTXOEntry (%s): expected error "+ "type does not match - got %T, want %T", test.name, err, test.errType) continue } if entry != nil { - t.Errorf("deserializeUtxoEntry (%s): returned entry "+ + t.Errorf("deserializeUTXOEntry (%s): returned entry "+ "is not nil", test.name) continue } } } -// TestDAGStateSerialization ensures serializing and deserializing the -// DAG state works as expected. -func TestDAGStateSerialization(t *testing.T) { +// TestDAGTipHashesSerialization ensures serializing and deserializing the +// DAG tip hashes works as expected. +func TestDAGTipHashesSerialization(t *testing.T) { t.Parallel() tests := []struct { name string - state dbDAGState + tipHashes []daghash.Hash serialized []byte }{ { - name: "genesis", - state: dbDAGState{ - Tips: []daghash.Hash{*newHashFromStr("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")}, - TotalTxs: 1, - }, - serialized: []byte("{\"Tips\":[[111,226,140,10,182,241,179,114,193,166,162,70,174,99,247,79,147,30,131,101,225,90,8,156,104,214,25,0,0,0,0,0]],\"TotalTxs\":1}"), + name: "genesis", + tipHashes: []daghash.Hash{*newHashFromStr("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")}, + serialized: []byte("[[111,226,140,10,182,241,179,114,193,166,162,70,174,99,247,79,147,30,131,101,225,90,8,156,104,214,25,0,0,0,0,0]]"), }, { - name: "block 1", - state: dbDAGState{ - Tips: []daghash.Hash{*newHashFromStr("00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048")}, - TotalTxs: 2, - }, - serialized: []byte("{\"Tips\":[[72,96,235,24,191,27,22,32,227,126,148,144,252,138,66,117,20,65,111,215,81,89,171,134,104,142,154,131,0,0,0,0]],\"TotalTxs\":2}"), + name: "block 1", + tipHashes: []daghash.Hash{*newHashFromStr("00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048")}, + serialized: []byte("[[72,96,235,24,191,27,22,32,227,126,148,144,252,138,66,117,20,65,111,215,81,89,171,134,104,142,154,131,0,0,0,0]]"), }, } for i, test := range tests { - gotBytes, err := serializeDAGState(test.state) + gotBytes, err := serializeDAGTipHashes(test.tipHashes) if err != nil { - t.Errorf("serializeDAGState #%d (%s) "+ + t.Errorf("serializeDAGTipHashes #%d (%s) "+ "unexpected error: %v", i, test.name, err) continue } - // Ensure the state serializes to the expected value. + // Ensure the tipHashes serializes to the expected value. if !bytes.Equal(gotBytes, test.serialized) { - t.Errorf("serializeDAGState #%d (%s): mismatched "+ + t.Errorf("serializeDAGTipHashes #%d (%s): mismatched "+ "bytes - got %s, want %s", i, test.name, string(gotBytes), string(test.serialized)) continue } // Ensure the serialized bytes are decoded back to the expected - // state. - state, err := deserializeDAGState(test.serialized) + // tipHashes. + tipHashes, err := deserializeDAGTipHashes(test.serialized) if err != nil { - t.Errorf("deserializeDAGState #%d (%s) "+ + t.Errorf("deserializeDAGTipHashes #%d (%s) "+ "unexpected error: %v", i, test.name, err) continue } - if !reflect.DeepEqual(*state, test.state) { - t.Errorf("deserializeDAGState #%d (%s) "+ - "mismatched state - got %v, want %v", i, - test.name, *state, test.state) + if !reflect.DeepEqual(tipHashes, test.tipHashes) { + t.Errorf("deserializeDAGTipHashes #%d (%s) "+ + "mismatched tipHashes - got %v, want %v", i, + test.name, tipHashes, test.tipHashes) continue } } } -// TestDAGStateDeserializeErrors performs negative tests against -// deserializing the DAG state to ensure error paths work as expected. -func TestDAGStateDeserializeErrors(t *testing.T) { +// TestDAGTipHashesDeserializeErrors performs negative tests against +// deserializing the DAG tip hashes to ensure error paths work as expected. +func TestDAGTipHashesDeserializeErrors(t *testing.T) { t.Parallel() tests := []struct { @@ -683,16 +677,16 @@ func TestDAGStateDeserializeErrors(t *testing.T) { }, { name: "corrupted data", - serialized: []byte("{\"Tips\":[111,226,140,10,182,241,179,114,193,166,162,70,174,99,247,7"), + serialized: []byte("[[111,226,140,10,182,241,179,114,193,166,162,70,174,99,247,7"), errType: database.Error{ErrorCode: database.ErrCorruption}, }, } for _, test := range tests { // Ensure the expected error type and code is returned. - _, err := deserializeDAGState(test.serialized) + _, err := deserializeDAGTipHashes(test.serialized) if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { - t.Errorf("deserializeDAGState (%s): expected "+ + t.Errorf("deserializeDAGTipHashes (%s): expected "+ "error type does not match - got %T, want %T", test.name, err, test.errType) continue @@ -700,8 +694,8 @@ func TestDAGStateDeserializeErrors(t *testing.T) { if derr, ok := err.(database.Error); ok { tderr := test.errType.(database.Error) if derr.ErrorCode != tderr.ErrorCode { - t.Errorf("deserializeDAGState (%s): "+ - "wrong error code got: %v, want: %v", + t.Errorf("deserializeDAGTipHashes (%s): "+ + "wrong error code got: %v, want: %v", test.name, derr.ErrorCode, tderr.ErrorCode) continue diff --git a/blockdag/fullblocks_test.go b/blockdag/fullblocks_test.go index 72fed2824..dc8c3dec2 100644 --- a/blockdag/fullblocks_test.go +++ b/blockdag/fullblocks_test.go @@ -57,10 +57,10 @@ func isSupportedDbType(dbType string) bool { return false } -// chainSetup is used to create a new db and chain instance with the genesis +// dagSetup is used to create a new db and chain instance with the genesis // block already inserted. In addition to the new chain instance, it returns // a teardown function the caller should invoke when done testing to clean up. -func chainSetup(dbName string, params *dagconfig.Params) (*blockdag.BlockDAG, func(), error) { +func dagSetup(dbName string, params *dagconfig.Params) (*blockdag.BlockDAG, func(), error) { if !isSupportedDbType(testDbType) { return nil, nil, fmt.Errorf("unsupported db type %v", testDbType) } @@ -142,7 +142,7 @@ func TestFullBlocks(t *testing.T) { } // Create a new database and chain instance to run tests against. - chain, teardownFunc, err := chainSetup("fullblocktest", + dag, teardownFunc, err := dagSetup("fullblocktest", &dagconfig.RegressionNetParams) if err != nil { t.Errorf("Failed to setup chain instance: %v", err) @@ -160,7 +160,7 @@ func TestFullBlocks(t *testing.T) { t.Logf("Testing block %s (hash %s, height %d)", item.Name, block.Hash(), blockHeight) - isOrphan, err := chain.ProcessBlock(block, + isOrphan, err := dag.ProcessBlock(block, blockdag.BFNone) if err != nil { t.Fatalf("block %q (hash %s, height %d) should "+ @@ -186,7 +186,7 @@ func TestFullBlocks(t *testing.T) { t.Logf("Testing block %s (hash %s, height %d)", item.Name, block.Hash(), blockHeight) - _, err := chain.ProcessBlock(block, blockdag.BFNone) + _, err := dag.ProcessBlock(block, blockdag.BFNone) if err == nil { t.Fatalf("block %q (hash %s, height %d) should not "+ "have been accepted", item.Name, block.Hash(), @@ -243,7 +243,7 @@ func TestFullBlocks(t *testing.T) { t.Logf("Testing block %s (hash %s, height %d)", item.Name, block.Hash(), blockHeight) - isOrphan, err := chain.ProcessBlock(block, blockdag.BFNone) + isOrphan, err := dag.ProcessBlock(block, blockdag.BFNone) if err != nil { // Ensure the error code is of the expected type. if _, ok := err.(blockdag.RuleError); !ok { @@ -272,14 +272,14 @@ func TestFullBlocks(t *testing.T) { item.Name, block.Hash(), blockHeight) // Ensure hash and height match. - dagState := chain.GetDAGState() - if dagState.SelectedTip.Hash != item.Block.BlockHash() || - dagState.SelectedTip.Height != blockHeight { + virtualBlock := dag.VirtualBlock() + if virtualBlock.SelectedTipHash() != item.Block.BlockHash() || + virtualBlock.SelectedTipHeight() != blockHeight { t.Fatalf("block %q (hash %s, height %d) should be "+ "the current tip -- got (hash %s, height %d)", - item.Name, block.Hash(), blockHeight, dagState.SelectedTip.Hash, - dagState.SelectedTip.Height) + item.Name, block.Hash(), blockHeight, virtualBlock.SelectedTipHash(), + virtualBlock.SelectedTipHeight()) } } diff --git a/blockdag/indexers/addrindex.go b/blockdag/indexers/addrindex.go index a5c33d907..81a5e1247 100644 --- a/blockdag/indexers/addrindex.go +++ b/blockdag/indexers/addrindex.go @@ -662,7 +662,7 @@ func (idx *AddrIndex) indexPkScript(data writeIndexData, pkScript []byte, txIdx // indexBlock extract all of the standard addresses from all of the transactions // in the passed block and maps each of them to the associated transaction using // the passed map. -func (idx *AddrIndex) indexBlock(data writeIndexData, block *util.Block, view *blockdag.UtxoViewpoint) { +func (idx *AddrIndex) indexBlock(data writeIndexData, block *util.Block, virtual *blockdag.VirtualBlock) { for txIdx, tx := range block.Transactions() { // Coinbases do not reference any inputs. Since the block is // required to have already gone through full validation, it has @@ -670,11 +670,11 @@ func (idx *AddrIndex) indexBlock(data writeIndexData, block *util.Block, view *b // a coinbase. if txIdx != 0 { for _, txIn := range tx.MsgTx().TxIn { - // The view should always have the input since + // The UTXO should always have the input since // the index contract requires it, however, be // safe and simply ignore any missing entries. - entry := view.LookupEntry(txIn.PreviousOutPoint) - if entry == nil { + entry, ok := virtual.GetUTXOEntry(txIn.PreviousOutPoint) + if !ok { continue } @@ -693,7 +693,7 @@ func (idx *AddrIndex) indexBlock(data writeIndexData, block *util.Block, view *b // the transactions in the block involve. // // This is part of the Indexer interface. -func (idx *AddrIndex) ConnectBlock(dbTx database.Tx, block *util.Block, view *blockdag.UtxoViewpoint) error { +func (idx *AddrIndex) ConnectBlock(dbTx database.Tx, block *util.Block, virtual *blockdag.VirtualBlock) error { // The offset and length of the transactions within the serialized // block. txLocs, err := block.TxLoc() @@ -709,7 +709,7 @@ func (idx *AddrIndex) ConnectBlock(dbTx database.Tx, block *util.Block, view *bl // Build all of the address to transaction mappings in a local map. addrsToTxns := make(writeIndexData) - idx.indexBlock(addrsToTxns, block, view) + idx.indexBlock(addrsToTxns, block, virtual) // Add all of the index entries for each address. addrIdxBucket := dbTx.Metadata().Bucket(addrIndexKey) @@ -731,10 +731,10 @@ func (idx *AddrIndex) ConnectBlock(dbTx database.Tx, block *util.Block, view *bl // each transaction in the block involve. // // This is part of the Indexer interface. -func (idx *AddrIndex) DisconnectBlock(dbTx database.Tx, block *util.Block, view *blockdag.UtxoViewpoint) error { +func (idx *AddrIndex) DisconnectBlock(dbTx database.Tx, block *util.Block, virtual *blockdag.VirtualBlock) error { // Build all of the address to transaction mappings in a local map. addrsToTxns := make(writeIndexData) - idx.indexBlock(addrsToTxns, block, view) + idx.indexBlock(addrsToTxns, block, virtual) // Remove all of the index entries for each address. bucket := dbTx.Metadata().Bucket(addrIndexKey) @@ -833,7 +833,7 @@ func (idx *AddrIndex) indexUnconfirmedAddresses(pkScript []byte, tx *util.Tx) { // addresses not being indexed. // // This function is safe for concurrent access. -func (idx *AddrIndex) AddUnconfirmedTx(tx *util.Tx, utxoView *blockdag.UtxoViewpoint) { +func (idx *AddrIndex) AddUnconfirmedTx(tx *util.Tx, utxoView *blockdag.UTXOView) { // Index addresses of all referenced previous transaction outputs. // // The existence checks are elided since this is only called after the diff --git a/blockdag/indexers/cfindex.go b/blockdag/indexers/cfindex.go index 09d549b9e..580a3c09d 100644 --- a/blockdag/indexers/cfindex.go +++ b/blockdag/indexers/cfindex.go @@ -203,7 +203,7 @@ func storeFilter(dbTx database.Tx, block *util.Block, f *gcs.Filter, // connected to the main chain. This indexer adds a hash-to-cf mapping for // every passed block. This is part of the Indexer interface. func (idx *CfIndex) ConnectBlock(dbTx database.Tx, block *util.Block, - view *blockdag.UtxoViewpoint) error { + virtual *blockdag.VirtualBlock) error { f, err := builder.BuildBasicFilter(block.MsgBlock()) if err != nil { @@ -227,7 +227,7 @@ func (idx *CfIndex) ConnectBlock(dbTx database.Tx, block *util.Block, // disconnected from the main chain. This indexer removes the hash-to-cf // mapping for every passed block. This is part of the Indexer interface. func (idx *CfIndex) DisconnectBlock(dbTx database.Tx, block *util.Block, - view *blockdag.UtxoViewpoint) error { + virtual *blockdag.VirtualBlock) error { for _, key := range cfIndexKeys { err := dbDeleteFilterIdxEntry(dbTx, key, block.Hash()) diff --git a/blockdag/indexers/common.go b/blockdag/indexers/common.go index b502a3838..682fd121c 100644 --- a/blockdag/indexers/common.go +++ b/blockdag/indexers/common.go @@ -52,11 +52,11 @@ type Indexer interface { // ConnectBlock is invoked when the index manager is notified that a new // block has been connected to the main chain. - ConnectBlock(dbTx database.Tx, block *util.Block, view *blockdag.UtxoViewpoint) error + ConnectBlock(dbTx database.Tx, block *util.Block, virtual *blockdag.VirtualBlock) error // DisconnectBlock is invoked when the index manager is notified that a // block has been disconnected from the main chain. - DisconnectBlock(dbTx database.Tx, block *util.Block, view *blockdag.UtxoViewpoint) error + DisconnectBlock(dbTx database.Tx, block *util.Block, virtual *blockdag.VirtualBlock) error } // AssertError identifies an error that indicates an internal code consistency diff --git a/blockdag/indexers/manager.go b/blockdag/indexers/manager.go index 2b393b97e..6063bb66a 100644 --- a/blockdag/indexers/manager.go +++ b/blockdag/indexers/manager.go @@ -5,7 +5,6 @@ package indexers import ( - "bytes" "fmt" "github.com/daglabs/btcd/blockdag" @@ -13,6 +12,7 @@ import ( "github.com/daglabs/btcd/database" "github.com/daglabs/btcd/wire" "github.com/daglabs/btcd/util" + "bytes" ) var ( @@ -68,7 +68,7 @@ func dbFetchIndexerTip(dbTx database.Tx, idxKey []byte) (*daghash.Hash, int32, e // given block using the provided indexer and updates the tip of the indexer // accordingly. An error will be returned if the current tip for the indexer is // not the previous block for the passed block. -func dbIndexConnectBlock(dbTx database.Tx, indexer Indexer, block *util.Block, view *blockdag.UtxoViewpoint) error { +func dbIndexConnectBlock(dbTx database.Tx, indexer Indexer, block *util.Block, virtual *blockdag.VirtualBlock) error { // Assert that the block being connected properly connects to the // current tip of the index. idxKey := indexer.Key() @@ -86,7 +86,7 @@ func dbIndexConnectBlock(dbTx database.Tx, indexer Indexer, block *util.Block, v } // Notify the indexer with the connected block so it can index it. - if err := indexer.ConnectBlock(dbTx, block, view); err != nil { + if err := indexer.ConnectBlock(dbTx, block, virtual); err != nil { return err } @@ -98,7 +98,7 @@ func dbIndexConnectBlock(dbTx database.Tx, indexer Indexer, block *util.Block, v // given block using the provided indexer and updates the tip of the indexer // accordingly. An error will be returned if the current tip for the indexer is // not the passed block. -func dbIndexDisconnectBlock(dbTx database.Tx, indexer Indexer, block *util.Block, view *blockdag.UtxoViewpoint) error { +func dbIndexDisconnectBlock(dbTx database.Tx, indexer Indexer, block *util.Block, virtual *blockdag.VirtualBlock) error { // Assert that the block being disconnected is the current tip of the // index. idxKey := indexer.Key() @@ -115,7 +115,7 @@ func dbIndexDisconnectBlock(dbTx database.Tx, indexer Indexer, block *util.Block // Notify the indexer with the disconnected block so it can remove all // of the appropriate entries. - if err := indexer.DisconnectBlock(dbTx, block, view); err != nil { + if err := indexer.DisconnectBlock(dbTx, block, virtual); err != nil { return err } @@ -314,8 +314,8 @@ func dbFetchTx(dbTx database.Tx, hash *daghash.Hash) (*wire.MsgTx, error) { // transactions in the block. This is sometimes needed when catching indexes up // because many of the txouts could actually already be spent however the // associated scripts are still required to index them. -func makeUtxoView(dbTx database.Tx, block *util.Block, interrupt <-chan struct{}) (*blockdag.UtxoViewpoint, error) { - view := blockdag.NewUtxoViewpoint() +func makeUtxoView(dbTx database.Tx, block *util.Block, interrupt <-chan struct{}) (*blockdag.UTXOView, error) { + view := blockdag.NewUTXOView() for txIdx, tx := range block.Transactions() { // Coinbases do not reference any inputs. Since the block is // required to have already gone through full validation, it has @@ -350,11 +350,11 @@ func makeUtxoView(dbTx database.Tx, block *util.Block, interrupt <-chan struct{} // checks, and invokes each indexer. // // This is part of the blockchain.IndexManager interface. -func (m *Manager) ConnectBlock(dbTx database.Tx, block *util.Block, view *blockdag.UtxoViewpoint) error { +func (m *Manager) ConnectBlock(dbTx database.Tx, block *util.Block, virtual *blockdag.VirtualBlock) error { // Call each of the currently active optional indexes with the block // being connected so they can update accordingly. for _, index := range m.enabledIndexes { - err := dbIndexConnectBlock(dbTx, index, block, view) + err := dbIndexConnectBlock(dbTx, index, block, virtual) if err != nil { return err } @@ -368,11 +368,11 @@ func (m *Manager) ConnectBlock(dbTx database.Tx, block *util.Block, view *blockd // the index entries associated with the block. // // This is part of the blockchain.IndexManager interface. -func (m *Manager) DisconnectBlock(dbTx database.Tx, block *util.Block, view *blockdag.UtxoViewpoint) error { +func (m *Manager) DisconnectBlock(dbTx database.Tx, block *util.Block, virtual *blockdag.VirtualBlock) error { // Call each of the currently active optional indexes with the block // being disconnected so they can update accordingly. for _, index := range m.enabledIndexes { - err := dbIndexDisconnectBlock(dbTx, index, block, view) + err := dbIndexDisconnectBlock(dbTx, index, block, virtual) if err != nil { return err } diff --git a/blockdag/indexers/txindex.go b/blockdag/indexers/txindex.go index 001e64eef..37da17aee 100644 --- a/blockdag/indexers/txindex.go +++ b/blockdag/indexers/txindex.go @@ -388,7 +388,7 @@ func (idx *TxIndex) Create(dbTx database.Tx) error { // for every transaction in the passed block. // // This is part of the Indexer interface. -func (idx *TxIndex) ConnectBlock(dbTx database.Tx, block *util.Block, view *blockdag.UtxoViewpoint) error { +func (idx *TxIndex) ConnectBlock(dbTx database.Tx, block *util.Block, virtual *blockdag.VirtualBlock) error { // Increment the internal block ID to use for the block being connected // and add all of the transactions in the block to the index. newBlockID := idx.curBlockID + 1 @@ -411,7 +411,7 @@ func (idx *TxIndex) ConnectBlock(dbTx database.Tx, block *util.Block, view *bloc // hash-to-transaction mapping for every transaction in the block. // // This is part of the Indexer interface. -func (idx *TxIndex) DisconnectBlock(dbTx database.Tx, block *util.Block, view *blockdag.UtxoViewpoint) error { +func (idx *TxIndex) DisconnectBlock(dbTx database.Tx, block *util.Block, virtual *blockdag.VirtualBlock) error { // Remove all of the transactions in the block from the index. if err := dbRemoveTxIndexEntries(dbTx, block); err != nil { return err diff --git a/blockdag/notifications_test.go b/blockdag/notifications_test.go index 1d82c367c..30580f91a 100644 --- a/blockdag/notifications_test.go +++ b/blockdag/notifications_test.go @@ -18,7 +18,7 @@ func TestNotifications(t *testing.T) { } // Create a new database and chain instance to run tests against. - chain, teardownFunc, err := chainSetup("notifications", + chain, teardownFunc, err := dagSetup("notifications", &dagconfig.MainNetParams) if err != nil { t.Fatalf("Failed to setup chain instance: %v", err) diff --git a/blockdag/scriptval.go b/blockdag/scriptval.go index 4b1042a6a..02eaca86d 100644 --- a/blockdag/scriptval.go +++ b/blockdag/scriptval.go @@ -29,7 +29,7 @@ type txValidator struct { validateChan chan *txValidateItem quitChan chan struct{} resultChan chan error - utxoView *UtxoViewpoint + utxoView *UTXOView flags txscript.ScriptFlags sigCache *txscript.SigCache } @@ -166,7 +166,7 @@ func (v *txValidator) Validate(items []*txValidateItem) error { // newTxValidator returns a new instance of txValidator to be used for // validating transaction scripts asynchronously. -func newTxValidator(utxoView *UtxoViewpoint, flags txscript.ScriptFlags, sigCache *txscript.SigCache) *txValidator { +func newTxValidator(utxoView *UTXOView, flags txscript.ScriptFlags, sigCache *txscript.SigCache) *txValidator { return &txValidator{ validateChan: make(chan *txValidateItem), quitChan: make(chan struct{}), @@ -179,7 +179,7 @@ func newTxValidator(utxoView *UtxoViewpoint, flags txscript.ScriptFlags, sigCach // ValidateTransactionScripts validates the scripts for the passed transaction // using multiple goroutines. -func ValidateTransactionScripts(tx *util.Tx, utxoView *UtxoViewpoint, flags txscript.ScriptFlags, sigCache *txscript.SigCache) error { +func ValidateTransactionScripts(tx *util.Tx, utxoView *UTXOView, flags txscript.ScriptFlags, sigCache *txscript.SigCache) error { // Collect all of the transaction inputs and required information for // validation. txIns := tx.MsgTx().TxIn @@ -205,7 +205,7 @@ func ValidateTransactionScripts(tx *util.Tx, utxoView *UtxoViewpoint, flags txsc // checkBlockScripts executes and validates the scripts for all transactions in // the passed block using multiple goroutines. -func checkBlockScripts(block *util.Block, utxoView *UtxoViewpoint, scriptFlags txscript.ScriptFlags, sigCache *txscript.SigCache) error { +func checkBlockScripts(block *util.Block, utxoView *UTXOView, scriptFlags txscript.ScriptFlags, sigCache *txscript.SigCache) error { // Collect all of the transaction inputs and required information for // validation for all transactions in the block into a single slice. numInputs := 0 diff --git a/blockdag/scriptval_test.go b/blockdag/scriptval_test.go index 33aecad18..89250d891 100644 --- a/blockdag/scriptval_test.go +++ b/blockdag/scriptval_test.go @@ -34,7 +34,7 @@ func TestCheckBlockScripts(t *testing.T) { } storeDataFile := fmt.Sprintf("%d.utxostore", testBlockNum) - view, err := loadUtxoView(storeDataFile) + view, err := loadUTXOView(storeDataFile) if err != nil { t.Errorf("Error loading txstore: %v\n", err) return diff --git a/blockdag/utxoset.go b/blockdag/utxoset.go index 997b8d278..4114fbcba 100644 --- a/blockdag/utxoset.go +++ b/blockdag/utxoset.go @@ -1,15 +1,15 @@ package blockdag import ( + "errors" "fmt" "github.com/daglabs/btcd/wire" - "errors" "sort" "strings" ) // utxoCollection represents a set of UTXOs indexed by their outPoints -type utxoCollection map[wire.OutPoint]*UtxoEntry +type utxoCollection map[wire.OutPoint]*UTXOEntry func (uc utxoCollection) String() string { utxoStrings := make([]string, len(uc)) @@ -26,11 +26,27 @@ func (uc utxoCollection) String() string { return fmt.Sprintf("[ %s ]", strings.Join(utxoStrings, ", ")) } +// add adds a new UTXO entry to this collection +func (uc utxoCollection) add(outPoint wire.OutPoint, entry *UTXOEntry) { + uc[outPoint] = entry +} + +// remove removes a UTXO entry from this collection if it exists +func (uc utxoCollection) remove(outPoint wire.OutPoint) { + delete(uc, outPoint) +} + +// contains returns a boolean value indicating whether a UTXO entry is in the set +func (uc utxoCollection) contains(outPoint wire.OutPoint) bool { + _, ok := uc[outPoint] + return ok +} + // clone returns a clone of this collection func (uc utxoCollection) clone() utxoCollection { clone := utxoCollection{} for outPoint, entry := range uc { - clone[outPoint] = entry + clone.add(outPoint, entry) } return clone @@ -90,10 +106,10 @@ func (d *utxoDiff) diffFrom(other *utxoDiff) (*utxoDiff, error) { // If they are not in other.toAdd - should be added in result.toRemove // If they are in other.toRemove - base utxoSet is not the same for outPoint, utxoEntry := range d.toAdd { - if _, ok := other.toAdd[outPoint]; !ok { - result.toRemove[outPoint] = utxoEntry + if !other.toAdd.contains(outPoint) { + result.toRemove.add(outPoint, utxoEntry) } - if _, ok := other.toRemove[outPoint]; ok { + if other.toRemove.contains(outPoint) { return nil, errors.New("diffFrom: transaction both in d.toAdd and in other.toRemove") } } @@ -102,10 +118,10 @@ func (d *utxoDiff) diffFrom(other *utxoDiff) (*utxoDiff, error) { // If they are not in other.toRemove - should be added in result.toAdd // If they are in other.toAdd - base utxoSet is not the same for outPoint, utxoEntry := range d.toRemove { - if _, ok := other.toRemove[outPoint]; !ok { - result.toAdd[outPoint] = utxoEntry + if !other.toRemove.contains(outPoint) { + result.toAdd.add(outPoint, utxoEntry) } - if _, ok := other.toAdd[outPoint]; ok { + if other.toAdd.contains(outPoint) { return nil, errors.New("diffFrom: transaction both in d.toRemove and in other.toAdd") } } @@ -113,16 +129,16 @@ func (d *utxoDiff) diffFrom(other *utxoDiff) (*utxoDiff, error) { // All transactions in other.toAdd: // If they are not in d.toAdd - should be added in result.toAdd for outPoint, utxoEntry := range other.toAdd { - if _, ok := d.toAdd[outPoint]; !ok { - result.toAdd[outPoint] = utxoEntry + if !d.toAdd.contains(outPoint) { + result.toAdd.add(outPoint, utxoEntry) } } // All transactions in other.toRemove: // If they are not in d.toRemove - should be added in result.toRemove for outPoint, utxoEntry := range other.toRemove { - if _, ok := d.toRemove[outPoint]; !ok { - result.toRemove[outPoint] = utxoEntry + if !d.toRemove.contains(outPoint) { + result.toRemove.add(outPoint, utxoEntry) } } @@ -163,10 +179,10 @@ func (d *utxoDiff) withDiff(diff *utxoDiff) (*utxoDiff, error) { // If they are in diff.toAdd - should throw an error // Otherwise - should be ignored for outPoint, utxoEntry := range d.toAdd { - if _, ok := diff.toRemove[outPoint]; !ok { - result.toAdd[outPoint] = utxoEntry + if !diff.toRemove.contains(outPoint) { + result.toAdd.add(outPoint, utxoEntry) } - if _, ok := diff.toAdd[outPoint]; ok { + if diff.toAdd.contains(outPoint) { return nil, errors.New("withDiff: transaction both in d.toAdd and in other.toAdd") } } @@ -176,10 +192,10 @@ func (d *utxoDiff) withDiff(diff *utxoDiff) (*utxoDiff, error) { // If they are in diff.toRemove - should throw an error // Otherwise - should be ignored for outPoint, utxoEntry := range d.toRemove { - if _, ok := diff.toAdd[outPoint]; !ok { - result.toRemove[outPoint] = utxoEntry + if !diff.toAdd.contains(outPoint) { + result.toRemove.add(outPoint, utxoEntry) } - if _, ok := diff.toRemove[outPoint]; ok { + if diff.toRemove.contains(outPoint) { return nil, errors.New("withDiff: transaction both in d.toRemove and in other.toRemove") } } @@ -187,16 +203,16 @@ func (d *utxoDiff) withDiff(diff *utxoDiff) (*utxoDiff, error) { // All transactions in diff.toAdd: // If they are not in d.toRemove - should be added in result.toAdd for outPoint, utxoEntry := range diff.toAdd { - if _, ok := d.toRemove[outPoint]; !ok { - result.toAdd[outPoint] = utxoEntry + if !d.toRemove.contains(outPoint) { + result.toAdd.add(outPoint, utxoEntry) } } // All transactions in diff.toRemove: // If they are not in d.toAdd - should be added in result.toRemove for outPoint, utxoEntry := range diff.toRemove { - if _, ok := d.toAdd[outPoint]; !ok { - result.toRemove[outPoint] = utxoEntry + if !d.toAdd.contains(outPoint) { + result.toRemove.add(outPoint, utxoEntry) } } @@ -216,10 +232,17 @@ func (d utxoDiff) String() string { } // newUTXOEntry creates a new utxoEntry representing the given txOut -func newUTXOEntry(txOut *wire.TxOut) *UtxoEntry { - entry := new(UtxoEntry) - entry.amount = txOut.Value - entry.pkScript = txOut.PkScript +func newUTXOEntry(txOut *wire.TxOut, isCoinbase bool, blockHeight int32) *UTXOEntry { + entry := &UTXOEntry{ + amount: txOut.Value, + pkScript: txOut.PkScript, + blockHeight: blockHeight, + packedFlags: tfModified, + } + + if isCoinbase { + entry.packedFlags |= tfCoinBase + } return entry } @@ -240,7 +263,7 @@ type utxoSet interface { fmt.Stringer diffFrom(other utxoSet) (*utxoDiff, error) withDiff(utxoDiff *utxoDiff) (utxoSet, error) - addTx(tx *wire.MsgTx) (ok bool) + addTx(tx *wire.MsgTx, blockHeight int32) (ok bool) clone() utxoSet } @@ -277,22 +300,25 @@ func (fus *fullUTXOSet) withDiff(other *utxoDiff) (utxoSet, error) { } // addTx adds a transaction to this utxoSet and returns true iff it's valid in this UTXO's context -func (fus *fullUTXOSet) addTx(tx *wire.MsgTx) bool { - if !fus.containsInputs(tx) { - return false - } +func (fus *fullUTXOSet) addTx(tx *wire.MsgTx, blockHeight int32) bool { + isCoinbase := IsCoinBaseTx(tx) + if !isCoinbase { + if !fus.containsInputs(tx) { + return false + } - for _, txIn := range tx.TxIn { - outPoint := *wire.NewOutPoint(&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index) - delete(fus.utxoCollection, outPoint) + for _, txIn := range tx.TxIn { + outPoint := *wire.NewOutPoint(&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index) + fus.remove(outPoint) + } } for i, txOut := range tx.TxOut { hash := tx.TxHash() outPoint := *wire.NewOutPoint(&hash, uint32(i)) - entry := newUTXOEntry(txOut) + entry := newUTXOEntry(txOut, isCoinbase, blockHeight) - fus.utxoCollection[outPoint] = entry + fus.add(outPoint, entry) } return true @@ -301,7 +327,7 @@ func (fus *fullUTXOSet) addTx(tx *wire.MsgTx) bool { func (fus *fullUTXOSet) containsInputs(tx *wire.MsgTx) bool { for _, txIn := range tx.TxIn { outPoint := *wire.NewOutPoint(&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index) - if _, ok := fus.utxoCollection[outPoint]; !ok { + if !fus.contains(outPoint) { return false } } @@ -319,6 +345,11 @@ func (fus *fullUTXOSet) clone() utxoSet { return &fullUTXOSet{utxoCollection: fus.utxoCollection.clone()} } +func (fus *fullUTXOSet) getUTXOEntry(outPoint wire.OutPoint) (*UTXOEntry, bool) { + utxoEntry, ok := fus.utxoCollection[outPoint] + return utxoEntry, ok +} + // diffUTXOSet represents a utxoSet with a base fullUTXOSet and a UTXODiff type diffUTXOSet struct { base *fullUTXOSet @@ -359,30 +390,33 @@ func (dus *diffUTXOSet) withDiff(other *utxoDiff) (utxoSet, error) { } // addTx adds a transaction to this utxoSet and returns true iff it's valid in this UTXO's context -func (dus *diffUTXOSet) addTx(tx *wire.MsgTx) bool { - if !dus.containsInputs(tx) { - return false - } +func (dus *diffUTXOSet) addTx(tx *wire.MsgTx, blockHeight int32) bool { + isCoinbase := IsCoinBaseTx(tx) + if !isCoinbase { + if !dus.containsInputs(tx) { + return false + } - for _, txIn := range tx.TxIn { - outPoint := *wire.NewOutPoint(&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index) - if _, ok := dus.utxoDiff.toAdd[outPoint]; ok { - delete(dus.utxoDiff.toAdd, outPoint) - } else { - prevUTXOEntry := dus.base.utxoCollection[outPoint] - dus.utxoDiff.toRemove[outPoint] = prevUTXOEntry + for _, txIn := range tx.TxIn { + outPoint := *wire.NewOutPoint(&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index) + if dus.utxoDiff.toAdd.contains(outPoint) { + dus.utxoDiff.toAdd.remove(outPoint) + } else { + prevUTXOEntry := dus.base.utxoCollection[outPoint] + dus.utxoDiff.toRemove.add(outPoint, prevUTXOEntry) + } } } for i, txOut := range tx.TxOut { hash := tx.TxHash() outPoint := *wire.NewOutPoint(&hash, uint32(i)) - entry := newUTXOEntry(txOut) + entry := newUTXOEntry(txOut, isCoinbase, blockHeight) - if _, ok := dus.utxoDiff.toRemove[outPoint]; ok { - delete(dus.utxoDiff.toRemove, outPoint) + if dus.utxoDiff.toRemove.contains(outPoint) { + dus.utxoDiff.toRemove.remove(outPoint) } else { - dus.utxoDiff.toAdd[outPoint] = entry + dus.utxoDiff.toAdd.add(outPoint, entry) } } @@ -392,9 +426,9 @@ func (dus *diffUTXOSet) addTx(tx *wire.MsgTx) bool { func (dus *diffUTXOSet) containsInputs(tx *wire.MsgTx) bool { for _, txIn := range tx.TxIn { outPoint := *wire.NewOutPoint(&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index) - _, isInBase := dus.base.utxoCollection[outPoint] - _, isInDiffToAdd := dus.utxoDiff.toAdd[outPoint] - _, isInDiffToRemove := dus.utxoDiff.toRemove[outPoint] + isInBase := dus.base.contains(outPoint) + isInDiffToAdd := dus.utxoDiff.toAdd.contains(outPoint) + isInDiffToRemove := dus.utxoDiff.toRemove.contains(outPoint) if (!isInBase && !isInDiffToAdd) || isInDiffToRemove { return false } @@ -406,11 +440,11 @@ func (dus *diffUTXOSet) containsInputs(tx *wire.MsgTx) bool { // meldToBase updates the base fullUTXOSet with all changes in diff func (dus *diffUTXOSet) meldToBase() { for outPoint := range dus.utxoDiff.toRemove { - delete(dus.base.utxoCollection, outPoint) + dus.base.remove(outPoint) } for outPoint, utxoEntry := range dus.utxoDiff.toAdd { - dus.base.utxoCollection[outPoint] = utxoEntry + dus.base.add(outPoint, utxoEntry) } dus.utxoDiff = newUTXODiff() diff --git a/blockdag/utxoset_test.go b/blockdag/utxoset_test.go index 5c271ce70..b0ba61216 100644 --- a/blockdag/utxoset_test.go +++ b/blockdag/utxoset_test.go @@ -5,6 +5,7 @@ import ( "github.com/daglabs/btcd/wire" "reflect" "testing" + "math" ) // TestUTXOCollection makes sure that utxoCollection cloning and string representations work as expected. @@ -13,8 +14,8 @@ func TestUTXOCollection(t *testing.T) { hash1, _ := daghash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111") outPoint0 := *wire.NewOutPoint(hash0, 0) outPoint1 := *wire.NewOutPoint(hash1, 0) - utxoEntry0 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 10}) - utxoEntry1 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 20}) + utxoEntry0 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 10}, true, 0) + utxoEntry1 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 20}, false, 1) // For each of the following test cases, we will: // .String() the given collection and compare it to expectedString @@ -72,8 +73,8 @@ func TestUTXODiff(t *testing.T) { hash1, _ := daghash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111") outPoint0 := *wire.NewOutPoint(hash0, 0) outPoint1 := *wire.NewOutPoint(hash1, 0) - utxoEntry0 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 10}) - utxoEntry1 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 20}) + utxoEntry0 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 10}, true, 0) + utxoEntry1 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 20}, false, 1) diff := utxoDiff{ toAdd: utxoCollection{outPoint0: utxoEntry0}, toRemove: utxoCollection{outPoint1: utxoEntry1}, @@ -109,7 +110,7 @@ func TestUTXODiff(t *testing.T) { func TestUTXODiffRules(t *testing.T) { hash0, _ := daghash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000") outPoint0 := *wire.NewOutPoint(hash0, 0) - utxoEntry0 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 10}) + utxoEntry0 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 10}, true, 0) // For each of the following test cases, we will: // this.diffFrom(other) and compare it to expectedDiffFromResult @@ -329,8 +330,8 @@ func TestFullUTXOSet(t *testing.T) { outPoint1 := *wire.NewOutPoint(hash1, 0) txOut0 := &wire.TxOut{PkScript: []byte{}, Value: 10} txOut1 := &wire.TxOut{PkScript: []byte{}, Value: 20} - utxoEntry0 := newUTXOEntry(txOut0) - utxoEntry1 := newUTXOEntry(txOut1) + utxoEntry0 := newUTXOEntry(txOut0, true, 0) + utxoEntry1 := newUTXOEntry(txOut1, false, 1) diff := &utxoDiff{ toAdd: utxoCollection{outPoint0: utxoEntry0}, toRemove: utxoCollection{outPoint1: utxoEntry1}, @@ -360,11 +361,11 @@ func TestFullUTXOSet(t *testing.T) { transaction0 := wire.NewMsgTx(1) transaction0.TxIn = []*wire.TxIn{txIn0} transaction0.TxOut = []*wire.TxOut{txOut0} - if ok = emptySet.addTx(transaction0); ok { + if ok = emptySet.addTx(transaction0, 0); ok { t.Errorf("addTx unexpectedly succeeded") } emptySet = &fullUTXOSet{utxoCollection: utxoCollection{outPoint0: utxoEntry0}} - if ok = emptySet.addTx(transaction0); !ok { + if ok = emptySet.addTx(transaction0, 0); !ok { t.Errorf("addTx unexpectedly failed") } @@ -391,8 +392,8 @@ func TestDiffUTXOSet(t *testing.T) { outPoint1 := *wire.NewOutPoint(hash1, 0) txOut0 := &wire.TxOut{PkScript: []byte{}, Value: 10} txOut1 := &wire.TxOut{PkScript: []byte{}, Value: 20} - utxoEntry0 := newUTXOEntry(txOut0) - utxoEntry1 := newUTXOEntry(txOut1) + utxoEntry0 := newUTXOEntry(txOut0, true, 0) + utxoEntry1 := newUTXOEntry(txOut1, false, 1) diff := &utxoDiff{ toAdd: utxoCollection{outPoint0: utxoEntry0}, toRemove: utxoCollection{outPoint1: utxoEntry1}, @@ -629,11 +630,13 @@ func TestUTXOSetDiffRules(t *testing.T) { // TestDiffUTXOSet_addTx makes sure that diffUTXOSet addTx works as expected func TestDiffUTXOSet_addTx(t *testing.T) { - // transaction0 is coinbase. As such, it does not have any inputs + // transaction0 is coinbase. As such, it has exactly one input with hash zero and MaxUInt32 index + hash0, _ := daghash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000") + txIn0 := &wire.TxIn{SignatureScript: []byte{}, PreviousOutPoint: wire.OutPoint{Hash: *hash0, Index: math.MaxUint32}, Sequence: 0} txOut0 := &wire.TxOut{PkScript: []byte{0}, Value: 10} - utxoEntry0 := newUTXOEntry(txOut0) + utxoEntry0 := newUTXOEntry(txOut0, true, 0) transaction0 := wire.NewMsgTx(1) - transaction0.TxIn = []*wire.TxIn{} + transaction0.TxIn = []*wire.TxIn{txIn0} transaction0.TxOut = []*wire.TxOut{txOut0} // transaction1 spends transaction0 @@ -641,7 +644,7 @@ func TestDiffUTXOSet_addTx(t *testing.T) { outPoint1 := *wire.NewOutPoint(&hash1, 0) txIn1 := &wire.TxIn{SignatureScript: []byte{}, PreviousOutPoint: wire.OutPoint{Hash: hash1, Index: 0}, Sequence: 0} txOut1 := &wire.TxOut{PkScript: []byte{1}, Value: 20} - utxoEntry1 := newUTXOEntry(txOut1) + utxoEntry1 := newUTXOEntry(txOut1, false, 1) transaction1 := wire.NewMsgTx(1) transaction1.TxIn = []*wire.TxIn{txIn1} transaction1.TxOut = []*wire.TxOut{txOut1} @@ -651,7 +654,7 @@ func TestDiffUTXOSet_addTx(t *testing.T) { outPoint2 := *wire.NewOutPoint(&hash2, 0) txIn2 := &wire.TxIn{SignatureScript: []byte{}, PreviousOutPoint: wire.OutPoint{Hash: hash2, Index: 0}, Sequence: 0} txOut2 := &wire.TxOut{PkScript: []byte{2}, Value: 30} - utxoEntry2 := newUTXOEntry(txOut2) + utxoEntry2 := newUTXOEntry(txOut2, false, 2) transaction2 := wire.NewMsgTx(1) transaction2.TxIn = []*wire.TxIn{txIn2} transaction2.TxOut = []*wire.TxOut{txOut2} @@ -661,18 +664,20 @@ func TestDiffUTXOSet_addTx(t *testing.T) { outPoint3 := *wire.NewOutPoint(&hash3, 0) // For each of the following test cases, we will: - // 1. startSet.addTx() all the transactions in toAdd, in order + // 1. startSet.addTx() all the transactions in toAdd, in order, with the initial block height startHeight // 2. Compare the result set with expectedSet tests := []struct { name string startSet *diffUTXOSet + startHeight int32 toAdd []*wire.MsgTx expectedSet *diffUTXOSet }{ { - name: "add coinbase transaction to empty set", - startSet: newDiffUTXOSet(newFullUTXOSet(), newUTXODiff()), - toAdd: []*wire.MsgTx{transaction0}, + name: "add coinbase transaction to empty set", + startSet: newDiffUTXOSet(newFullUTXOSet(), newUTXODiff()), + startHeight: 0, + toAdd: []*wire.MsgTx{transaction0}, expectedSet: &diffUTXOSet{ base: &fullUTXOSet{utxoCollection: utxoCollection{}}, utxoDiff: &utxoDiff{ @@ -682,9 +687,10 @@ func TestDiffUTXOSet_addTx(t *testing.T) { }, }, { - name: "add regular transaction to empty set", - startSet: newDiffUTXOSet(newFullUTXOSet(), newUTXODiff()), - toAdd: []*wire.MsgTx{transaction1}, + name: "add regular transaction to empty set", + startSet: newDiffUTXOSet(newFullUTXOSet(), newUTXODiff()), + startHeight: 0, + toAdd: []*wire.MsgTx{transaction1}, expectedSet: &diffUTXOSet{ base: &fullUTXOSet{utxoCollection: utxoCollection{}}, utxoDiff: &utxoDiff{ @@ -702,7 +708,8 @@ func TestDiffUTXOSet_addTx(t *testing.T) { toRemove: utxoCollection{}, }, }, - toAdd: []*wire.MsgTx{transaction1}, + startHeight: 1, + toAdd: []*wire.MsgTx{transaction1}, expectedSet: &diffUTXOSet{ base: &fullUTXOSet{utxoCollection: utxoCollection{outPoint1: utxoEntry0}}, utxoDiff: &utxoDiff{ @@ -720,7 +727,8 @@ func TestDiffUTXOSet_addTx(t *testing.T) { toRemove: utxoCollection{}, }, }, - toAdd: []*wire.MsgTx{transaction1}, + startHeight: 1, + toAdd: []*wire.MsgTx{transaction1}, expectedSet: &diffUTXOSet{ base: newFullUTXOSet(), utxoDiff: &utxoDiff{ @@ -738,7 +746,8 @@ func TestDiffUTXOSet_addTx(t *testing.T) { toRemove: utxoCollection{outPoint2: utxoEntry1}, }, }, - toAdd: []*wire.MsgTx{transaction1}, + startHeight: 1, + toAdd: []*wire.MsgTx{transaction1}, expectedSet: &diffUTXOSet{ base: newFullUTXOSet(), utxoDiff: &utxoDiff{ @@ -756,7 +765,8 @@ func TestDiffUTXOSet_addTx(t *testing.T) { toRemove: utxoCollection{}, }, }, - toAdd: []*wire.MsgTx{transaction1, transaction2}, + startHeight: 1, + toAdd: []*wire.MsgTx{transaction1, transaction2}, expectedSet: &diffUTXOSet{ base: &fullUTXOSet{utxoCollection: utxoCollection{outPoint1: utxoEntry0}}, utxoDiff: &utxoDiff{ @@ -770,9 +780,9 @@ func TestDiffUTXOSet_addTx(t *testing.T) { for _, test := range tests { diffSet := test.startSet.clone() - // Apply all transactions, in order, to diffSet - for _, transaction := range test.toAdd { - diffSet.addTx(transaction) + // Apply all transactions to diffSet, in order, with the initial block height startHeight + for i, transaction := range test.toAdd { + diffSet.addTx(transaction, test.startHeight + int32(i)) } // Make sure that the result diffSet equals to the expectedSet diff --git a/blockdag/utxoviewpoint.go b/blockdag/utxoview.go similarity index 64% rename from blockdag/utxoviewpoint.go rename to blockdag/utxoview.go index 371cc5302..df0ba941d 100644 --- a/blockdag/utxoviewpoint.go +++ b/blockdag/utxoview.go @@ -10,8 +10,8 @@ import ( "github.com/daglabs/btcd/dagconfig/daghash" "github.com/daglabs/btcd/database" "github.com/daglabs/btcd/txscript" - "github.com/daglabs/btcd/wire" "github.com/daglabs/btcd/util" + "github.com/daglabs/btcd/wire" ) // txoFlags is a bitmask defining additional information and state for a @@ -30,11 +30,11 @@ const ( tfModified ) -// UtxoEntry houses details about an individual transaction output in a utxo +// UTXOEntry houses details about an individual transaction output in a utxo // view such as whether or not it was contained in a coinbase tx, the height of // the block that contains the tx, whether or not it is spent, its public key // script, and how much it pays. -type UtxoEntry struct { +type UTXOEntry struct { // NOTE: Additions, deletions, or modifications to the order of the // definitions in this struct should not be changed without considering // how it affects alignment on 64-bit platforms. The current order is @@ -52,32 +52,26 @@ type UtxoEntry struct { packedFlags txoFlags } -// isModified returns whether or not the output has been modified since it was -// loaded. -func (entry *UtxoEntry) isModified() bool { - return entry.packedFlags&tfModified == tfModified -} - // IsCoinBase returns whether or not the output was contained in a coinbase // transaction. -func (entry *UtxoEntry) IsCoinBase() bool { +func (entry *UTXOEntry) IsCoinBase() bool { return entry.packedFlags&tfCoinBase == tfCoinBase } // BlockHeight returns the height of the block containing the output. -func (entry *UtxoEntry) BlockHeight() int32 { +func (entry *UTXOEntry) BlockHeight() int32 { return entry.blockHeight } // IsSpent returns whether or not the output has been spent based upon the // current state of the unspent transaction output view it was obtained from. -func (entry *UtxoEntry) IsSpent() bool { +func (entry *UTXOEntry) IsSpent() bool { return entry.packedFlags&tfSpent == tfSpent } // Spend marks the output as spent. Spending an output that is already spent // has no effect. -func (entry *UtxoEntry) Spend() { +func (entry *UTXOEntry) Spend() { // Nothing to do if the output is already spent. if entry.IsSpent() { return @@ -88,22 +82,22 @@ func (entry *UtxoEntry) Spend() { } // Amount returns the amount of the output. -func (entry *UtxoEntry) Amount() int64 { +func (entry *UTXOEntry) Amount() int64 { return entry.amount } // PkScript returns the public key script for the output. -func (entry *UtxoEntry) PkScript() []byte { +func (entry *UTXOEntry) PkScript() []byte { return entry.pkScript } // Clone returns a shallow copy of the utxo entry. -func (entry *UtxoEntry) Clone() *UtxoEntry { +func (entry *UTXOEntry) Clone() *UTXOEntry { if entry == nil { return nil } - return &UtxoEntry{ + return &UTXOEntry{ amount: entry.amount, pkScript: entry.pkScript, blockHeight: entry.blockHeight, @@ -111,47 +105,29 @@ func (entry *UtxoEntry) Clone() *UtxoEntry { } } -// UtxoViewpoint represents a view into the set of unspent transaction outputs +// UTXOView represents a view into the set of unspent transaction outputs // from a specific point of view in the chain. For example, it could be for // the end of the main chain, some point in the history of the main chain, or // down a side chain. // // The unspent outputs are needed by other transactions for things such as // script validation and double spend prevention. -type UtxoViewpoint struct { - entries map[wire.OutPoint]*UtxoEntry - tips blockSet +type UTXOView struct { + entries map[wire.OutPoint]*UTXOEntry } -// Tips returns the hashes of the tips in the DAG the view currently -// represents. -func (view *UtxoViewpoint) Tips() blockSet { - return view.tips -} - -// SetTips sets the hashes of the tips in the DAG the view currently -// represents. -func (view *UtxoViewpoint) SetTips(tips blockSet) { - view.tips = tips -} - -// AddBlock removes all the parents of block from the tips and adds -// the given block to the tips. -func (view *UtxoViewpoint) AddBlock(block *blockNode) { - updatedTips := view.tips.clone() - for _, parent := range block.parents { - updatedTips.remove(parent) +// NewUTXOView returns a new empty unspent transaction output view. +func NewUTXOView() *UTXOView { + return &UTXOView{ + entries: make(map[wire.OutPoint]*UTXOEntry), } - - updatedTips.add(block) - view.tips = updatedTips } // LookupEntry returns information about a given transaction output according to // the current state of the view. It will return nil if the passed output does // not exist in the view or is otherwise not available such as when it has been // disconnected during a reorg. -func (view *UtxoViewpoint) LookupEntry(outpoint wire.OutPoint) *UtxoEntry { +func (view *UTXOView) LookupEntry(outpoint wire.OutPoint) *UTXOEntry { return view.entries[outpoint] } @@ -159,7 +135,7 @@ func (view *UtxoViewpoint) LookupEntry(outpoint wire.OutPoint) *UtxoEntry { // unspendable. When the view already has an entry for the output, it will be // marked unspent. All fields will be updated for existing entries since it's // possible it has changed during a reorg. -func (view *UtxoViewpoint) addTxOut(outpoint wire.OutPoint, txOut *wire.TxOut, isCoinBase bool, blockHeight int32) { +func (view *UTXOView) addTxOut(outpoint wire.OutPoint, txOut *wire.TxOut, isCoinBase bool, blockHeight int32) { // Don't add provably unspendable outputs. if txscript.IsUnspendable(txOut.PkScript) { return @@ -171,7 +147,7 @@ func (view *UtxoViewpoint) addTxOut(outpoint wire.OutPoint, txOut *wire.TxOut, i // is allowed so long as the previous transaction is fully spent. entry := view.LookupEntry(outpoint) if entry == nil { - entry = new(UtxoEntry) + entry = new(UTXOEntry) view.entries[outpoint] = entry } @@ -188,7 +164,7 @@ func (view *UtxoViewpoint) addTxOut(outpoint wire.OutPoint, txOut *wire.TxOut, i // it exists and is not provably unspendable. When the view already has an // entry for the output, it will be marked unspent. All fields will be updated // for existing entries since it's possible it has changed during a reorg. -func (view *UtxoViewpoint) AddTxOut(tx *util.Tx, txOutIdx uint32, blockHeight int32) { +func (view *UTXOView) AddTxOut(tx *util.Tx, txOutIdx uint32, blockHeight int32) { // Can't add an output for an out of bounds index. if txOutIdx >= uint32(len(tx.MsgTx().TxOut)) { return @@ -207,7 +183,7 @@ func (view *UtxoViewpoint) AddTxOut(tx *util.Tx, txOutIdx uint32, blockHeight in // unspendable to the view. When the view already has entries for any of the // outputs, they are simply marked unspent. All fields will be updated for // existing entries since it's possible it has changed during a reorg. -func (view *UtxoViewpoint) AddTxOuts(tx *util.Tx, blockHeight int32) { +func (view *UTXOView) AddTxOuts(tx *util.Tx, blockHeight int32) { // Loop all of the transaction outputs and add those which are not // provably unspendable. isCoinBase := IsCoinBase(tx) @@ -228,7 +204,7 @@ func (view *UtxoViewpoint) AddTxOuts(tx *util.Tx, blockHeight int32) { // spent. In addition, when the 'stxos' argument is not nil, it will be updated // to append an entry for each spent txout. An error will be returned if the // view does not contain the required utxos. -func (view *UtxoViewpoint) connectTransaction(tx *util.Tx, blockHeight int32, stxos *[]spentTxOut) error { +func (view *UTXOView) connectTransaction(tx *util.Tx, blockHeight int32, stxos *[]spentTxOut) error { // Coinbase transactions don't have any inputs to spend. if IsCoinBase(tx) { // Add the transaction's outputs as available utxos. @@ -271,84 +247,26 @@ func (view *UtxoViewpoint) connectTransaction(tx *util.Tx, blockHeight int32, st return nil } -// connectTransactions updates the view by adding all new utxos created by all -// of the transactions in the passed block, marking all utxos the transactions -// spend as spent, and setting the best hash for the view to the passed block. -// In addition, when the 'stxos' argument is not nil, it will be updated to -// append an entry for each spent txout. -func (view *UtxoViewpoint) connectTransactions(block *blockNode, transactions []*util.Tx, stxos *[]spentTxOut) error { - for _, tx := range transactions { - err := view.connectTransaction(tx, block.height, stxos) - if err != nil { - return err - } - } - - // Update the tips for view to include this block since all of its - // transactions have been connected. - view.AddBlock(block) - return nil -} - -// fetchEntryByHash attempts to find any available utxo for the given hash by -// searching the entire set of possible outputs for the given hash. It checks -// the view first and then falls back to the database if needed. -func (view *UtxoViewpoint) fetchEntryByHash(db database.DB, hash *daghash.Hash) (*UtxoEntry, error) { - // First attempt to find a utxo with the provided hash in the view. - prevOut := wire.OutPoint{Hash: *hash} - for idx := uint32(0); idx < MaxOutputsPerBlock; idx++ { - prevOut.Index = idx - entry := view.LookupEntry(prevOut) - if entry != nil { - return entry, nil - } - } - - // Check the database since it doesn't exist in the view. This will - // often by the case since only specifically referenced utxos are loaded - // into the view. - var entry *UtxoEntry - err := db.View(func(dbTx database.Tx) error { - var err error - entry, err = dbFetchUtxoEntryByHash(dbTx, hash) - return err - }) - return entry, err -} - // RemoveEntry removes the given transaction output from the current state of // the view. It will have no effect if the passed output does not exist in the // view. -func (view *UtxoViewpoint) RemoveEntry(outpoint wire.OutPoint) { +func (view *UTXOView) RemoveEntry(outpoint wire.OutPoint) { delete(view.entries, outpoint) } // Entries returns the underlying map that stores of all the utxo entries. -func (view *UtxoViewpoint) Entries() map[wire.OutPoint]*UtxoEntry { +func (view *UTXOView) Entries() map[wire.OutPoint]*UTXOEntry { return view.entries } -// commit prunes all entries marked modified that are now fully spent and marks -// all entries as unmodified. -func (view *UtxoViewpoint) commit() { - for outpoint, entry := range view.entries { - if entry == nil || (entry.isModified() && entry.IsSpent()) { - delete(view.entries, outpoint) - continue - } - - entry.packedFlags ^= tfModified - } -} - -// fetchUtxosMain fetches unspent transaction output data about the provided +// fetchUTXOs fetches unspent transaction output data about the provided // set of outpoints from the point of view of the end of the main chain at the // time of the call. // // 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. -func (view *UtxoViewpoint) fetchUtxosMain(db database.DB, outpoints map[wire.OutPoint]struct{}) error { +func (view *UTXOView) fetchUTXOs(db database.DB, outpoints map[wire.OutPoint]struct{}) error { // Nothing to do if there are no requested outputs. if len(outpoints) == 0 { return nil @@ -363,7 +281,7 @@ func (view *UtxoViewpoint) fetchUtxosMain(db database.DB, outpoints map[wire.Out // to unnecessarily avoid attempting to reload it from the database. return db.View(func(dbTx database.Tx) error { for outpoint := range outpoints { - entry, err := dbFetchUtxoEntry(dbTx, outpoint) + entry, err := dbFetchUTXOEntry(dbTx, outpoint) if err != nil { return err } @@ -375,36 +293,12 @@ func (view *UtxoViewpoint) fetchUtxosMain(db database.DB, outpoints map[wire.Out }) } -// fetchUtxos loads the unspent transaction outputs for the provided set of -// outputs into the view from the database as needed unless they already exist -// in the view in which case they are ignored. -func (view *UtxoViewpoint) fetchUtxos(db database.DB, outpoints map[wire.OutPoint]struct{}) error { - // Nothing to do if there are no requested outputs. - if len(outpoints) == 0 { - return nil - } - - // Filter entries that are already in the view. - neededSet := make(map[wire.OutPoint]struct{}) - for outpoint := range outpoints { - // Already loaded into the current view. - if _, ok := view.entries[outpoint]; ok { - continue - } - - neededSet[outpoint] = struct{}{} - } - - // Request the input utxos from the database. - return view.fetchUtxosMain(db, neededSet) -} - -// fetchInputUtxos loads the unspent transaction outputs for the inputs +// fetchInputUTXOs loads the unspent transaction outputs for the inputs // referenced by the transactions in the given block into the view from the // database as needed. In particular, referenced entries that are earlier in // the block are added to the view and entries that are already in the view are // not modified. -func (view *UtxoViewpoint) fetchInputUtxos(db database.DB, block *util.Block) error { +func (view *UTXOView) fetchInputUTXOs(db database.DB, block *util.Block) error { // Build a map of in-flight transactions because some of the inputs in // this block could be referencing other transactions earlier in this // block which are not yet in the chain. @@ -451,23 +345,16 @@ func (view *UtxoViewpoint) fetchInputUtxos(db database.DB, block *util.Block) er } // Request the input utxos from the database. - return view.fetchUtxosMain(db, neededSet) + return view.fetchUTXOs(db, neededSet) } -// NewUtxoViewpoint returns a new empty unspent transaction output view. -func NewUtxoViewpoint() *UtxoViewpoint { - return &UtxoViewpoint{ - entries: make(map[wire.OutPoint]*UtxoEntry), - } -} - -// FetchUtxoView loads unspent transaction outputs for the inputs referenced by +// FetchUTXOView loads unspent transaction outputs for the inputs referenced by // the passed transaction from the point of view of the end of the main chain. // It also attempts to fetch the utxos for the outputs of the transaction itself // so the returned view can be examined for duplicate transactions. // // This function is safe for concurrent access however the returned view is NOT. -func (dag *BlockDAG) FetchUtxoView(tx *util.Tx) (*UtxoViewpoint, error) { +func (dag *BlockDAG) FetchUTXOView(tx *util.Tx) (*UTXOView, error) { // Create a set of needed outputs based on those referenced by the // inputs of the passed transaction and the outputs of the transaction // itself. @@ -485,36 +372,9 @@ func (dag *BlockDAG) FetchUtxoView(tx *util.Tx) (*UtxoViewpoint, error) { // Request the utxos from the point of view of the end of the main // chain. - view := NewUtxoViewpoint() + view := NewUTXOView() dag.dagLock.RLock() - err := view.fetchUtxosMain(dag.db, neededSet) + err := view.fetchUTXOs(dag.db, neededSet) dag.dagLock.RUnlock() return view, err } - -// FetchUtxoEntry loads and returns the requested unspent transaction output -// from the point of view of the end of the main chain. -// -// NOTE: Requesting an output for which there is no data will NOT return an -// error. Instead both the entry and the error will be nil. This is done to -// allow pruning of spent transaction outputs. In practice this means the -// caller must check if the returned entry is nil before invoking methods on it. -// -// This function is safe for concurrent access however the returned entry (if -// any) is NOT. -func (dag *BlockDAG) FetchUtxoEntry(outpoint wire.OutPoint) (*UtxoEntry, error) { - dag.dagLock.RLock() - defer dag.dagLock.RUnlock() - - var entry *UtxoEntry - err := dag.db.View(func(dbTx database.Tx) error { - var err error - entry, err = dbFetchUtxoEntry(dbTx, outpoint) - return err - }) - if err != nil { - return nil, err - } - - return entry, nil -} diff --git a/blockdag/validate.go b/blockdag/validate.go index 077b0cbb8..d3165b32e 100644 --- a/blockdag/validate.go +++ b/blockdag/validate.go @@ -53,16 +53,6 @@ var ( // a package level variable to avoid the need to create a new instance // every time a check is needed. zeroHash daghash.Hash - - // block91842Hash is one of the two nodes which violate the rules - // set forth in BIP0030. It is defined as a package level variable to - // avoid the need to create a new instance every time a check is needed. - block91842Hash = newHashFromStr("00000000000a4d0a398161ffc163c503763b1f4360639393e0e4c8e300e0caec") - - // block91880Hash is one of the two nodes which violate the rules - // set forth in BIP0030. It is defined as a package level variable to - // avoid the need to create a new instance every time a check is needed. - block91880Hash = newHashFromStr("00000000000743f190a18c5577a3c2d2a1f610ae9601ac046a38084ccb7cd721") ) // isNullOutpoint determines whether or not a previous transaction output point @@ -317,13 +307,6 @@ func checkProofOfWork(header *wire.BlockHeader, powLimit *big.Int, flags Behavio return nil } -// CheckProofOfWork ensures the block header bits which indicate the target -// difficulty is in min/max range and that the block hash is less than the -// target difficulty as claimed. -func CheckProofOfWork(block *util.Block, powLimit *big.Int) error { - return checkProofOfWork(&block.MsgBlock().Header, powLimit, BFNone) -} - // CountSigOps returns the number of signature operations for all transaction // input and output scripts in the provided transaction. This uses the // quicker, but imprecise, signature operation counting mechanism from @@ -353,7 +336,7 @@ func CountSigOps(tx *util.Tx) int { // transactions which are of the pay-to-script-hash type. This uses the // precise, signature operation counting mechanism from the script engine which // requires access to the input transaction scripts. -func CountP2SHSigOps(tx *util.Tx, isCoinBaseTx bool, utxoView *UtxoViewpoint) (int, error) { +func CountP2SHSigOps(tx *util.Tx, isCoinBaseTx bool, utxoView *UTXOView) (int, error) { // Coinbase transactions have no interesting inputs. if isCoinBaseTx { return 0, nil @@ -801,7 +784,7 @@ func (dag *BlockDAG) checkBlockContext(block *util.Block, parents blockSet, sele // http://r6.ca/blog/20120206T005236Z.html. // // This function MUST be called with the chain state lock held (for reads). -func (dag *BlockDAG) ensureNoDuplicateTx(node *blockNode, block *util.Block, view *UtxoViewpoint) error { +func (dag *BlockDAG) ensureNoDuplicateTx(node *blockNode, block *util.Block) error { // Fetch utxos for all of the transaction ouputs in this block. // Typically, there will not be any utxos for any of the outputs. fetchSet := make(map[wire.OutPoint]struct{}) @@ -812,16 +795,12 @@ func (dag *BlockDAG) ensureNoDuplicateTx(node *blockNode, block *util.Block, vie fetchSet[prevOut] = struct{}{} } } - err := view.fetchUtxos(dag.db, fetchSet) - if err != nil { - return err - } // Duplicate transactions are only allowed if the previous transaction // is fully spent. for outpoint := range fetchSet { - utxo := view.LookupEntry(outpoint) - if utxo != nil && !utxo.IsSpent() { + utxo, ok := dag.virtual.GetUTXOEntry(outpoint) + if ok && !utxo.IsSpent() { str := fmt.Sprintf("tried to overwrite transaction %v "+ "at block height %d that is not fully spent", outpoint.Hash, utxo.BlockHeight()) @@ -843,7 +822,7 @@ func (dag *BlockDAG) ensureNoDuplicateTx(node *blockNode, block *util.Block, vie // // NOTE: The transaction MUST have already been sanity checked with the // CheckTransactionSanity function prior to calling this function. -func CheckTransactionInputs(tx *util.Tx, txHeight int32, utxoView *UtxoViewpoint, dagParams *dagconfig.Params) (int64, error) { +func CheckTransactionInputs(tx *util.Tx, txHeight int32, utxoView *UTXOView, dagParams *dagconfig.Params) (int64, error) { // Coinbase transactions have no inputs. if IsCoinBase(tx) { return 0, nil @@ -958,7 +937,7 @@ func CheckTransactionInputs(tx *util.Tx, txHeight int32, utxoView *UtxoViewpoint // with that node. // // This function MUST be called with the chain state lock held (for writes). -func (dag *BlockDAG) checkConnectBlock(node *blockNode, block *util.Block, view *UtxoViewpoint, stxos *[]spentTxOut) error { +func (dag *BlockDAG) checkConnectBlock(node *blockNode, block *util.Block) error { // If the side chain blocks end up in the database, a call to // CheckBlockSanity should be done here in case a previous version // allowed a block that is no longer valid. However, since the @@ -972,15 +951,7 @@ func (dag *BlockDAG) checkConnectBlock(node *blockNode, block *util.Block, view return ruleError(ErrMissingTxOut, str) } - // Ensure the view is for the node being checked. - parentHashes := block.MsgBlock().Header.PrevBlocks - if !view.Tips().hashesEqual(parentHashes) { - return AssertError(fmt.Sprintf("inconsistent view when "+ - "checking block connection: tips are %v instead "+ - "of expected %v", view.Tips(), parentHashes)) - } - - err := dag.ensureNoDuplicateTx(node, block, view) + err := dag.ensureNoDuplicateTx(node, block) if err != nil { return err } @@ -990,7 +961,8 @@ func (dag *BlockDAG) checkConnectBlock(node *blockNode, block *util.Block, view // // These utxo entries are needed for verification of things such as // transaction inputs, counting pay-to-script-hashes, and scripts. - err = view.fetchInputUtxos(dag.db, block) + view := NewUTXOView() + err = view.fetchInputUTXOs(dag.db, block) if err != nil { return err } @@ -1036,6 +1008,8 @@ func (dag *BlockDAG) checkConnectBlock(node *blockNode, block *util.Block, view // still relatively cheap as compared to running the scripts) checks // against all the inputs when the signature operations are out of // bounds. + targetSpentOutputCount := countSpentOutputs(block) + stxos := make([]spentTxOut, 0, targetSpentOutputCount) var totalFees int64 for _, tx := range transactions { txFee, err := CheckTransactionInputs(tx, node.height, view, @@ -1057,15 +1031,17 @@ func (dag *BlockDAG) checkConnectBlock(node *blockNode, block *util.Block, view // provably unspendable as available utxos. Also, the passed // spent txos slice is updated to contain an entry for each // spent txout in the order each transaction spends them. - err = view.connectTransaction(tx, node.height, stxos) + err = view.connectTransaction(tx, node.height, &stxos) if err != nil { return err } } - // Update the tips for view to include this block since all of its - // transactions have been connected. - view.AddBlock(node) + // Sanity check the correct number of stxos are provided. + if len(stxos) != targetSpentOutputCount { + return AssertError("connectBlock called with inconsistent " + + "spent transaction out information") + } // The total output values of the coinbase transaction must not exceed // the expected subsidy value plus total transaction fees gained from @@ -1135,13 +1111,19 @@ func (dag *BlockDAG) checkConnectBlock(node *blockNode, block *util.Block, view } } - // Update the view tips to include this block since all of its - // transactions have been connected. - view.AddBlock(node) - return nil } +// countSpentOutputs returns the number of utxos the passed block spends. +func countSpentOutputs(block *util.Block) int { + // Exclude the coinbase transaction since it can't spend anything. + var numSpent int + for _, tx := range block.Transactions()[1:] { + numSpent += len(tx.MsgTx().TxIn) + } + return numSpent +} + // CheckConnectBlockTemplate fully validates that connecting the passed block to // the main chain does not violate any consensus rules, aside from the proof of // work requirement. The block must connect to the current tip of the main chain. @@ -1156,7 +1138,7 @@ func (dag *BlockDAG) CheckConnectBlockTemplate(block *util.Block) error { // This only checks whether the block can be connected to the tip of the // current chain. - tips := dag.virtual.Tips() + tips := dag.virtual.tips() header := block.MsgBlock().Header prevHashes := header.PrevBlocks if !tips.hashesEqual(prevHashes) { @@ -1180,10 +1162,6 @@ func (dag *BlockDAG) CheckConnectBlockTemplate(block *util.Block) error { return err } - // Leave the spent txouts entry nil in the state since the information - // is not needed and thus extra work can be avoided. - view := NewUtxoViewpoint() - view.SetTips(tips) - newNode := newBlockNode(&header, dag.virtual.Tips(), dag.dagParams.K) - return dag.checkConnectBlock(newNode, block, view, nil) + newNode := newBlockNode(&header, dag.virtual.tips(), dag.dagParams.K) + return dag.checkConnectBlock(newNode, block) } diff --git a/blockdag/validate_test.go b/blockdag/validate_test.go index 19ab03a43..991fc9d8d 100644 --- a/blockdag/validate_test.go +++ b/blockdag/validate_test.go @@ -67,7 +67,7 @@ func TestSequenceLocksActive(t *testing.T) { // ensure it fails. func TestCheckConnectBlockTemplate(t *testing.T) { // Create a new database and chain instance to run tests against. - chain, teardownFunc, err := chainSetup("checkconnectblocktemplate", + chain, teardownFunc, err := dagSetup("checkconnectblocktemplate", &dagconfig.MainNetParams) if err != nil { t.Errorf("Failed to setup chain instance: %v", err) diff --git a/blockdag/virtualblock.go b/blockdag/virtualblock.go index 244c544c7..5c607cd6f 100644 --- a/blockdag/virtualblock.go +++ b/blockdag/virtualblock.go @@ -6,31 +6,44 @@ package blockdag import ( "sync" + "github.com/daglabs/btcd/dagconfig/daghash" + "github.com/daglabs/btcd/wire" ) -// virtualBlock is a virtual block whose parents are the tips of the DAG. -type virtualBlock struct { +// VirtualBlock is a virtual block whose parents are the tips of the DAG. +type VirtualBlock struct { mtx sync.Mutex phantomK uint32 + utxoSet *fullUTXOSet blockNode } -// newVirtualBlock creates and returns a new virtualBlock. -func newVirtualBlock(tips blockSet, phantomK uint32) *virtualBlock { +// newVirtualBlock creates and returns a new VirtualBlock. +func newVirtualBlock(tips blockSet, phantomK uint32) *VirtualBlock { // The mutex is intentionally not held since this is a constructor. - var virtual virtualBlock + var virtual VirtualBlock virtual.phantomK = phantomK + virtual.utxoSet = newFullUTXOSet() virtual.setTips(tips) return &virtual } +// clone creates and returns a clone of the virtual block. +func (v *VirtualBlock) clone() *VirtualBlock { + return &VirtualBlock{ + phantomK: v.phantomK, + utxoSet: v.utxoSet.clone().(*fullUTXOSet), + blockNode: v.blockNode, + } +} + // setTips replaces the tips of the virtual block with the blocks in the // given blockSet. This only differs from the exported version in that it // is up to the caller to ensure the lock is held. // // This function MUST be called with the view mutex locked (for writes). -func (v *virtualBlock) setTips(tips blockSet) { +func (v *VirtualBlock) setTips(tips blockSet) { v.blockNode = *newBlockNode(nil, tips, v.phantomK) } @@ -38,7 +51,7 @@ func (v *virtualBlock) setTips(tips blockSet) { // given blockSet. // // This function is safe for concurrent access. -func (v *virtualBlock) SetTips(tips blockSet) { +func (v *VirtualBlock) SetTips(tips blockSet) { v.mtx.Lock() v.setTips(tips) v.mtx.Unlock() @@ -50,8 +63,8 @@ func (v *virtualBlock) SetTips(tips blockSet) { // is up to the caller to ensure the lock is held. // // This function MUST be called with the view mutex locked (for writes). -func (v *virtualBlock) addTip(newTip *blockNode) { - updatedTips := v.Tips().clone() +func (v *VirtualBlock) addTip(newTip *blockNode) { + updatedTips := v.tips().clone() for _, parent := range newTip.parents { updatedTips.remove(parent) } @@ -65,17 +78,17 @@ func (v *virtualBlock) addTip(newTip *blockNode) { // from the set. // // This function is safe for concurrent access. -func (v *virtualBlock) AddTip(newTip *blockNode) { +func (v *VirtualBlock) AddTip(newTip *blockNode) { v.mtx.Lock() v.addTip(newTip) v.mtx.Unlock() } -// Tips returns the current tip block nodes for the DAG. It will return +// tips returns the current tip block nodes for the DAG. It will return // an empty blockSet if there is no tip. // // This function is safe for concurrent access. -func (v *virtualBlock) Tips() blockSet { +func (v *VirtualBlock) tips() blockSet { return v.parents } @@ -83,6 +96,30 @@ func (v *virtualBlock) Tips() blockSet { // It will return nil if there is no tip. // // This function is safe for concurrent access. -func (v *virtualBlock) SelectedTip() *blockNode { +func (v *VirtualBlock) SelectedTip() *blockNode { return v.selectedParent } + +// SelectedTipHeight returns the height of the selected tip of the virtual block. +func (v *VirtualBlock) SelectedTipHeight() int32 { + return v.SelectedTip().height +} + +// TipHashes returns the hashes of the tips of the virtual block. +func (v *VirtualBlock) TipHashes() []daghash.Hash { + return v.tips().hashes() +} + +// SelectedTipHash returns the hash of the selected tip of the virtual block. +func (v *VirtualBlock) SelectedTipHash() daghash.Hash { + return v.SelectedTip().hash; +} + +// GetUTXOEntry returns the requested unspent transaction output. The returned +// instance must be treated as immutable since it is shared by all callers. +// +// This function is safe for concurrent access. However, the returned entry (if +// any) is NOT. +func (v *VirtualBlock) GetUTXOEntry(outPoint wire.OutPoint) (*UTXOEntry, bool) { + return v.utxoSet.getUTXOEntry(outPoint) +} diff --git a/blockdag/virtualblock_test.go b/blockdag/virtualblock_test.go index 6e4ea61af..7e9dcca0b 100644 --- a/blockdag/virtualblock_test.go +++ b/blockdag/virtualblock_test.go @@ -10,7 +10,7 @@ import ( "github.com/daglabs/btcd/dagconfig/daghash" ) -// TestVirtualBlock ensures that virtualBlock works as expected. +// TestVirtualBlock ensures that VirtualBlock works as expected. func TestVirtualBlock(t *testing.T) { // For the purposes of this test, we'll create blockNodes whose hashes are a // series of numbers from 0 to n. @@ -38,7 +38,7 @@ func TestVirtualBlock(t *testing.T) { node5 := buildNode(setFromSlice(node3, node4)) node6 := buildNode(setFromSlice(node3, node4)) - // Given an empty virtualBlock, each of the following test cases will: + // Given an empty VirtualBlock, each of the following test cases will: // Set its tips to tipsToSet // Add to it all the tips in tipsToAdd, one after the other // Call .Tips() on it and compare the result to expectedTips @@ -81,7 +81,7 @@ func TestVirtualBlock(t *testing.T) { } for _, test := range tests { - // Create an empty virtualBlock + // Create an empty VirtualBlock virtual := newVirtualBlock(nil, phantomK) // Set the tips. This will be the initial state @@ -93,7 +93,7 @@ func TestVirtualBlock(t *testing.T) { } // Ensure that the virtual block's tips are now equal to expectedTips - resultTips := virtual.Tips() + resultTips := virtual.tips() if !reflect.DeepEqual(resultTips, test.expectedTips) { t.Errorf("unexpected tips in test \"%s\". "+ "Expected: %v, got: %v.", test.name, test.expectedTips, resultTips) diff --git a/cmd/findcheckpoint/findcheckpoint.go b/cmd/findcheckpoint/findcheckpoint.go index f988ed36b..87ef91113 100644 --- a/cmd/findcheckpoint/findcheckpoint.go +++ b/cmd/findcheckpoint/findcheckpoint.go @@ -162,11 +162,12 @@ func main() { // Get the latest block hash and height from the database and report // status. - dagState := dag.GetDAGState() - fmt.Printf("Block database loaded with block height %d\n", dagState.SelectedTip.Height) + virtualBlock := dag.VirtualBlock() + fmt.Printf("Block database loaded with block height %d\n", virtualBlock.SelectedTipHeight()) // Find checkpoint candidates. - candidates, err := findCandidates(dag, &dagState.SelectedTip.Hash) + selectedTipHash := virtualBlock.SelectedTipHash() + candidates, err := findCandidates(dag, &selectedTipHash) if err != nil { fmt.Fprintln(os.Stderr, "Unable to identify candidates:", err) return diff --git a/mempool/mempool.go b/mempool/mempool.go index 957364f23..ffe6e8d9a 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -55,9 +55,9 @@ type Config struct { // associated with. ChainParams *dagconfig.Params - // FetchUtxoView defines the function to use to fetch unspent + // FetchUTXOView defines the function to use to fetch unspent // transaction output information. - FetchUtxoView func(*util.Tx) (*blockdag.UtxoViewpoint, error) + FetchUtxoView func(*util.Tx) (*blockdag.UTXOView, error) // BestHeight defines the function to use to access the block height of // the current best chain. @@ -71,7 +71,7 @@ type Config struct { // CalcSequenceLock defines the function to use in order to generate // the current sequence lock for the given transaction using the passed // utxo view. - CalcSequenceLock func(*util.Tx, *blockdag.UtxoViewpoint) (*blockdag.SequenceLock, error) + CalcSequenceLock func(*util.Tx, *blockdag.UTXOView) (*blockdag.SequenceLock, error) // IsDeploymentActive returns true if the target deploymentID is // active, and false otherwise. The mempool uses this function to gauge @@ -515,7 +515,7 @@ func (mp *TxPool) RemoveDoubleSpends(tx *util.Tx) { // helper for maybeAcceptTransaction. // // This function MUST be called with the mempool lock held (for writes). -func (mp *TxPool) addTransaction(utxoView *blockdag.UtxoViewpoint, tx *util.Tx, height int32, fee int64) *TxDesc { +func (mp *TxPool) addTransaction(utxoView *blockdag.UTXOView, tx *util.Tx, height int32, fee int64) *TxDesc { // Add the transaction to the pool and mark the referenced outpoints // as spent by the pool. txD := &TxDesc{ @@ -585,7 +585,7 @@ func (mp *TxPool) CheckSpend(op wire.OutPoint) *util.Tx { // transaction pool. // // This function MUST be called with the mempool lock held (for reads). -func (mp *TxPool) fetchInputUtxos(tx *util.Tx) (*blockdag.UtxoViewpoint, error) { +func (mp *TxPool) fetchInputUtxos(tx *util.Tx) (*blockdag.UTXOView, error) { utxoView, err := mp.cfg.FetchUtxoView(tx) if err != nil { return nil, err diff --git a/mempool/mempool_test.go b/mempool/mempool_test.go index f602d361e..778d07c81 100644 --- a/mempool/mempool_test.go +++ b/mempool/mempool_test.go @@ -26,18 +26,18 @@ import ( // transactions to appear as though they are spending completely valid utxos. type fakeChain struct { sync.RWMutex - utxos *blockdag.UtxoViewpoint + utxos *blockdag.UTXOView currentHeight int32 medianTimePast time.Time } -// FetchUtxoView loads utxo details about the inputs referenced by the passed +// FetchUTXOView loads utxo details about the inputs referenced by the passed // transaction from the point of view of the fake chain. It also attempts to // fetch the utxos for the outputs of the transaction itself so the returned // view can be examined for duplicate transactions. // // This function is safe for concurrent access however the returned view is NOT. -func (s *fakeChain) FetchUtxoView(tx *util.Tx) (*blockdag.UtxoViewpoint, error) { +func (s *fakeChain) FetchUtxoView(tx *util.Tx) (*blockdag.UTXOView, error) { s.RLock() defer s.RUnlock() @@ -45,7 +45,7 @@ func (s *fakeChain) FetchUtxoView(tx *util.Tx) (*blockdag.UtxoViewpoint, error) // do not affect the fake chain's view. // Add an entry for the tx itself to the new view. - viewpoint := blockdag.NewUtxoViewpoint() + viewpoint := blockdag.NewUTXOView() prevOut := wire.OutPoint{Hash: *tx.Hash()} for txOutIdx := range tx.MsgTx().TxOut { prevOut.Index = uint32(txOutIdx) @@ -98,7 +98,7 @@ func (s *fakeChain) SetMedianTimePast(mtp time.Time) { // CalcSequenceLock returns the current sequence lock for the passed // transaction associated with the fake chain instance. func (s *fakeChain) CalcSequenceLock(tx *util.Tx, - view *blockdag.UtxoViewpoint) (*blockdag.SequenceLock, error) { + view *blockdag.UTXOView) (*blockdag.SequenceLock, error) { return &blockdag.SequenceLock{ Seconds: -1, @@ -299,7 +299,7 @@ func newPoolHarness(chainParams *dagconfig.Params) (*poolHarness, []spendableOut } // Create a new fake chain and harness bound to it. - chain := &fakeChain{utxos: blockdag.NewUtxoViewpoint()} + chain := &fakeChain{utxos: blockdag.NewUTXOView()} harness := poolHarness{ signKey: signKey, payAddr: payAddr, diff --git a/mempool/policy.go b/mempool/policy.go index a7605911c..99842e0a9 100644 --- a/mempool/policy.go +++ b/mempool/policy.go @@ -85,7 +85,7 @@ func calcMinRequiredTxRelayFee(serializedSize int64, minRelayTxFee util.Amount) // context of this function is one whose referenced public key script is of a // standard form and, for pay-to-script-hash, does not have more than // maxStandardP2SHSigOps signature operations. -func checkInputsStandard(tx *util.Tx, utxoView *blockdag.UtxoViewpoint) error { +func checkInputsStandard(tx *util.Tx, utxoView *blockdag.UTXOView) error { // NOTE: The reference implementation also does a coinbase check here, // but coinbases have already been rejected prior to calling this // function so no need to recheck. diff --git a/mining/cpuminer/cpuminer.go b/mining/cpuminer/cpuminer.go index bc2549abd..fdd603168 100644 --- a/mining/cpuminer/cpuminer.go +++ b/mining/cpuminer/cpuminer.go @@ -162,7 +162,7 @@ func (m *CPUMiner) submitBlock(block *util.Block) bool { // a new block, but the check only happens periodically, so it is // possible a block was found and submitted in between. msgBlock := block.MsgBlock() - if !daghash.AreEqual(msgBlock.Header.PrevBlocks, m.g.GetDAGState().TipHashes) { + if !daghash.AreEqual(msgBlock.Header.PrevBlocks, m.g.VirtualBlock().TipHashes()) { log.Debugf("Block submitted via CPU miner with previous "+ "blocks %s is stale", msgBlock.Header.PrevBlocks) return false @@ -247,8 +247,8 @@ func (m *CPUMiner) solveBlock(msgBlock *wire.MsgBlock, blockHeight int32, hashesCompleted = 0 // The current block is stale if the DAG has changed. - dagState := m.g.GetDAGState() - if !daghash.AreEqual(header.PrevBlocks, dagState.TipHashes) { + virtualBlock := m.g.VirtualBlock() + if !daghash.AreEqual(header.PrevBlocks, virtualBlock.TipHashes()) { return false } @@ -326,7 +326,7 @@ out: // this would otherwise end up building a new block template on // a block that is in the process of becoming stale. m.submitBlockLock.Lock() - curHeight := m.g.GetDAGState().SelectedTip.Height + curHeight := m.g.VirtualBlock().SelectedTipHeight() if curHeight != 0 && !m.cfg.IsCurrent() { m.submitBlockLock.Unlock() time.Sleep(time.Second) @@ -585,7 +585,7 @@ func (m *CPUMiner) GenerateNBlocks(n uint32) ([]*daghash.Hash, error) { // be changing and this would otherwise end up building a new block // template on a block that is in the process of becoming stale. m.submitBlockLock.Lock() - curHeight := m.g.GetDAGState().SelectedTip.Height + curHeight := m.g.VirtualBlock().SelectedTipHeight() // Choose a payment address at random. rand.Seed(time.Now().UnixNano()) diff --git a/mining/mining.go b/mining/mining.go index a38449fd0..84a8b5d21 100644 --- a/mining/mining.go +++ b/mining/mining.go @@ -216,7 +216,7 @@ type BlockTemplate struct { // viewA will contain all of its original entries plus all of the entries // in viewB. It will replace any entries in viewB which also exist in viewA // if the entry in viewA is spent. -func mergeUtxoView(viewA *blockdag.UtxoViewpoint, viewB *blockdag.UtxoViewpoint) { +func mergeUtxoView(viewA *blockdag.UTXOView, viewB *blockdag.UTXOView) { viewAEntries := viewA.Entries() for outpoint, entryB := range viewB.Entries() { if entryA, exists := viewAEntries[outpoint]; !exists || @@ -282,7 +282,7 @@ func createCoinbaseTx(params *dagconfig.Params, coinbaseScript []byte, nextBlock // spendTransaction updates the passed view by marking the inputs to the passed // transaction as spent. It also adds all outputs in the passed transaction // which are not provably unspendable as available unspent transaction outputs. -func spendTransaction(utxoView *blockdag.UtxoViewpoint, tx *util.Tx, height int32) error { +func spendTransaction(utxoView *blockdag.UTXOView, tx *util.Tx, height int32) error { for _, txIn := range tx.MsgTx().TxIn { entry := utxoView.LookupEntry(txIn.PreviousOutPoint) if entry != nil { @@ -311,14 +311,14 @@ func logSkippedDeps(tx *util.Tx, deps map[daghash.Hash]*txPrioItem) { // on the end of the provided best chain. In particular, it is one second after // the median timestamp of the last several blocks per the chain consensus // rules. -func MinimumMedianTime(dagState *blockdag.DAGState) time.Time { - return dagState.SelectedTip.MedianTime.Add(time.Second) +func MinimumMedianTime(dagMedianTime time.Time) time.Time { + return dagMedianTime.Add(time.Second) } // medianAdjustedTime returns the current time adjusted to ensure it is at least // one second after the median timestamp of the last several blocks per the // chain consensus rules. -func medianAdjustedTime(chainState *blockdag.DAGState, timeSource blockdag.MedianTimeSource) time.Time { +func medianAdjustedTime(dagMedianTime time.Time, timeSource blockdag.MedianTimeSource) time.Time { // The timestamp for the block must not be before the median timestamp // of the last several blocks. Thus, choose the maximum between the // current time and one second after the past median time. The current @@ -326,7 +326,7 @@ func medianAdjustedTime(chainState *blockdag.DAGState, timeSource blockdag.Media // block timestamp does not supported a precision greater than one // second. newTimestamp := timeSource.AdjustedTime() - minTimestamp := MinimumMedianTime(chainState) + minTimestamp := MinimumMedianTime(dagMedianTime) if newTimestamp.Before(minTimestamp) { newTimestamp = minTimestamp } @@ -432,8 +432,8 @@ func NewBlkTmplGenerator(policy *Policy, params *dagconfig.Params, // ----------------------------------- -- func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress util.Address) (*BlockTemplate, error) { // Extend the most recently known best block. - dagState := g.dag.GetDAGState() - nextBlockHeight := dagState.SelectedTip.Height + 1 + virtualBlock := g.dag.VirtualBlock() + nextBlockHeight := virtualBlock.SelectedTipHeight() + 1 // Create a standard coinbase transaction paying to the provided // address. NOTE: The coinbase value will be updated to include the @@ -471,7 +471,7 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress util.Address) (*BlockTe // avoided. blockTxns := make([]*util.Tx, 0, len(sourceTxns)) blockTxns = append(blockTxns, coinbaseTx) - blockUtxos := blockdag.NewUtxoViewpoint() + blockUtxos := blockdag.NewUTXOView() // dependers is used to track transactions which depend on another // transaction in the source pool. This, in conjunction with the @@ -515,7 +515,7 @@ mempoolLoop: // mempool since a transaction which depends on other // transactions in the mempool must come after those // dependencies in the final generated block. - utxos, err := g.dag.FetchUtxoView(tx) + utxos, err := g.dag.FetchUTXOView(tx) if err != nil { log.Warnf("Unable to fetch utxo view for tx %s: %v", tx.Hash(), err) @@ -746,7 +746,7 @@ mempoolLoop: // Calculate the required difficulty for the block. The timestamp // is potentially adjusted to ensure it comes after the median time of // the last several blocks per the chain consensus rules. - ts := medianAdjustedTime(dagState, g.timeSource) + ts := medianAdjustedTime(virtualBlock.SelectedTip().CalcPastMedianTime(), g.timeSource) reqDifficulty, err := g.dag.CalcNextRequiredDifficulty(ts) if err != nil { return nil, err @@ -764,7 +764,7 @@ mempoolLoop: var msgBlock wire.MsgBlock msgBlock.Header = wire.BlockHeader{ Version: nextBlockVersion, - PrevBlocks: dagState.TipHashes, + PrevBlocks: virtualBlock.TipHashes(), MerkleRoot: *merkles[len(merkles)-1], Timestamp: ts, Bits: reqDifficulty, @@ -808,7 +808,8 @@ func (g *BlkTmplGenerator) UpdateBlockTime(msgBlock *wire.MsgBlock) error { // The new timestamp is potentially adjusted to ensure it comes after // the median time of the last several blocks per the chain consensus // rules. - newTime := medianAdjustedTime(g.dag.GetDAGState(), g.timeSource) + dagMedianTime := g.dag.VirtualBlock().CalcPastMedianTime() + newTime := medianAdjustedTime(dagMedianTime, g.timeSource) msgBlock.Header.Timestamp = newTime // Recalculate the difficulty if running on a network that requires it. @@ -851,14 +852,13 @@ func (g *BlkTmplGenerator) UpdateExtraNonce(msgBlock *wire.MsgBlock, blockHeight return nil } -// GetDAGState returns information about the current state -// as of the current point in time using the DAG instance -// associated with the block template generator. The returned state must be -// treated as immutable since it is shared by all callers. +// VirtualBlock returns the DAG's virtual block in the current point in time. +// The returned instance must be treated as immutable since it is shared by all +// callers. // // This function is safe for concurrent access. -func (g *BlkTmplGenerator) GetDAGState() *blockdag.DAGState { - return g.dag.GetDAGState() +func (g *BlkTmplGenerator) VirtualBlock() *blockdag.VirtualBlock { + return g.dag.VirtualBlock() } // TxSource returns the associated transaction source. diff --git a/mining/policy.go b/mining/policy.go index 0be26ac84..f3d18a564 100644 --- a/mining/policy.go +++ b/mining/policy.go @@ -54,7 +54,7 @@ func minInt(a, b int) int { // age is the sum of this value for each txin. Any inputs to the transaction // which are currently in the mempool and hence not mined into a block yet, // contribute no additional input age to the transaction. -func calcInputValueAge(tx *wire.MsgTx, utxoView *blockdag.UtxoViewpoint, nextBlockHeight int32) float64 { +func calcInputValueAge(tx *wire.MsgTx, utxoView *blockdag.UTXOView, nextBlockHeight int32) float64 { var totalInputAge float64 for _, txIn := range tx.TxIn { // Don't attempt to accumulate the total input age if the @@ -86,7 +86,7 @@ func calcInputValueAge(tx *wire.MsgTx, utxoView *blockdag.UtxoViewpoint, nextBlo // of each of its input values multiplied by their age (# of confirmations). // Thus, the final formula for the priority is: // sum(inputValue * inputAge) / adjustedTxSize -func CalcPriority(tx *wire.MsgTx, utxoView *blockdag.UtxoViewpoint, nextBlockHeight int32) float64 { +func CalcPriority(tx *wire.MsgTx, utxoView *blockdag.UTXOView, nextBlockHeight int32) float64 { // In order to encourage spending multiple old unspent transaction // outputs thereby reducing the total set, don't count the constant // overhead for each input as well as enough bytes of the signature diff --git a/mining/policy_test.go b/mining/policy_test.go index 85854ac04..fedac5adf 100644 --- a/mining/policy_test.go +++ b/mining/policy_test.go @@ -46,12 +46,12 @@ func hexToBytes(s string) []byte { // provided source transactions as if there were available at the respective // block height specified in the heights slice. The length of the source txns // and source tx heights must match or it will panic. -func newUtxoViewpoint(sourceTxns []*wire.MsgTx, sourceTxHeights []int32) *blockdag.UtxoViewpoint { +func newUtxoViewpoint(sourceTxns []*wire.MsgTx, sourceTxHeights []int32) *blockdag.UTXOView { if len(sourceTxns) != len(sourceTxHeights) { panic("each transaction must have its block height specified") } - view := blockdag.NewUtxoViewpoint() + view := blockdag.NewUTXOView() for i, tx := range sourceTxns { view.AddTxOuts(util.NewTx(tx), sourceTxHeights[i]) } @@ -128,11 +128,11 @@ func TestCalcPriority(t *testing.T) { } tests := []struct { - name string // test description - tx *wire.MsgTx // tx to calc priority for - utxoView *blockdag.UtxoViewpoint // inputs to tx - nextHeight int32 // height for priority calc - want float64 // expected priority + name string // test description + tx *wire.MsgTx // tx to calc priority for + utxoView *blockdag.UTXOView // inputs to tx + nextHeight int32 // height for priority calc + want float64 // expected priority }{ { name: "one height 7 input, prio tx height 169", diff --git a/netsync/manager.go b/netsync/manager.go index b9097aacc..815513c5b 100644 --- a/netsync/manager.go +++ b/netsync/manager.go @@ -17,8 +17,8 @@ import ( "github.com/daglabs/btcd/database" "github.com/daglabs/btcd/mempool" peerpkg "github.com/daglabs/btcd/peer" - "github.com/daglabs/btcd/wire" "github.com/daglabs/btcd/util" + "github.com/daglabs/btcd/wire" ) const ( @@ -226,7 +226,7 @@ func (sm *SyncManager) startSync() { return } - dagState := sm.dag.GetDAGState() + virtualBlock := sm.dag.VirtualBlock() var bestPeer *peerpkg.Peer for peer, state := range sm.peerStates { if !state.syncCandidate { @@ -239,7 +239,7 @@ func (sm *SyncManager) startSync() { // doesn't have a later block when it's equal, it will likely // have one soon so it is a reasonable choice. It also allows // the case where both are at 0 such as during regression test. - if peer.LastBlock() < dagState.SelectedTip.Height { + if peer.LastBlock() < virtualBlock.SelectedTipHeight() { state.syncCandidate = false continue } @@ -284,13 +284,13 @@ func (sm *SyncManager) startSync() { // not support the headers-first approach so do normal block // downloads when in regression test mode. if sm.nextCheckpoint != nil && - dagState.SelectedTip.Height < sm.nextCheckpoint.Height && + virtualBlock.SelectedTipHeight() < sm.nextCheckpoint.Height && sm.chainParams != &dagconfig.RegressionNetParams { bestPeer.PushGetHeadersMsg(locator, sm.nextCheckpoint.Hash) sm.headersFirstMode = true log.Infof("Downloading headers for blocks %d to "+ - "%d from peer %s", dagState.SelectedTip.Height+1, + "%d from peer %s", virtualBlock.SelectedTipHeight()+1, sm.nextCheckpoint.Height, bestPeer.Addr()) } else { bestPeer.PushGetBlocksMsg(locator, &zeroHash) @@ -392,8 +392,9 @@ func (sm *SyncManager) handleDonePeerMsg(peer *peerpkg.Peer) { if sm.syncPeer == peer { sm.syncPeer = nil if sm.headersFirstMode { - dagState := sm.dag.GetDAGState() - sm.resetHeaderState(&dagState.SelectedTip.Hash, dagState.SelectedTip.Height) + virtualBlock := sm.dag.VirtualBlock() + selectedTipHash := virtualBlock.SelectedTipHash() + sm.resetHeaderState(&selectedTipHash, virtualBlock.SelectedTipHeight()) } sm.startSync() } @@ -482,7 +483,7 @@ func (sm *SyncManager) current() bool { // No matter what chain thinks, if we are below the block we are syncing // to we are not current. - if sm.dag.GetDAGState().SelectedTip.Height < sm.syncPeer.LastBlock() { + if sm.dag.VirtualBlock().SelectedTipHeight() < sm.syncPeer.LastBlock() { return false } return true @@ -615,9 +616,10 @@ func (sm *SyncManager) handleBlockMsg(bmsg *blockMsg) { // Update this peer's latest block height, for future // potential sync node candidacy. - dagState := sm.dag.GetDAGState() - heightUpdate = dagState.SelectedTip.Height - blkHashUpdate = &dagState.SelectedTip.Hash + virtualBlock := sm.dag.VirtualBlock() + selectedTipHash := virtualBlock.SelectedTipHash() + heightUpdate = virtualBlock.SelectedTipHeight() + blkHashUpdate = &selectedTipHash // Clear the rejected transactions. sm.rejectedTxns = make(map[daghash.Hash]struct{}) @@ -872,9 +874,9 @@ func (sm *SyncManager) haveInventory(invVect *wire.InvVect) (bool, error) { prevOut := wire.OutPoint{Hash: invVect.Hash} for i := uint32(0); i < 2; i++ { prevOut.Index = i - entry, err := sm.dag.FetchUtxoEntry(prevOut) - if err != nil { - return false, err + entry, ok := sm.dag.VirtualBlock().GetUTXOEntry(prevOut) + if !ok { + return false, nil } if entry != nil && !entry.IsSpent() { return true, nil @@ -1182,7 +1184,7 @@ func (sm *SyncManager) handleBlockchainNotification(notification *blockdag.Notif iv := wire.NewInvVect(wire.InvTypeBlock, block.Hash()) sm.peerNotifier.RelayInventory(iv, block.MsgBlock().Header) - // A block has been connected to the main block chain. + // A block has been connected to the block DAG. case blockdag.NTBlockConnected: block, ok := notification.Data.(*util.Block) if !ok { @@ -1220,7 +1222,7 @@ func (sm *SyncManager) handleBlockchainNotification(notification *blockdag.Notif } } - // A block has been disconnected from the main block chain. + // A block has been disconnected from the block DAG. case blockdag.NTBlockDisconnected: block, ok := notification.Data.(*util.Block) if !ok { @@ -1396,12 +1398,13 @@ func New(config *Config) (*SyncManager, error) { feeEstimator: config.FeeEstimator, } - dagState := sm.dag.GetDAGState() + virtualBlock := sm.dag.VirtualBlock() + selectedTipHash := virtualBlock.SelectedTipHash() if !config.DisableCheckpoints { // Initialize the next checkpoint based on the current height. - sm.nextCheckpoint = sm.findNextHeaderCheckpoint(dagState.SelectedTip.Height) + sm.nextCheckpoint = sm.findNextHeaderCheckpoint(virtualBlock.SelectedTipHeight()) if sm.nextCheckpoint != nil { - sm.resetHeaderState(&dagState.SelectedTip.Hash, dagState.SelectedTip.Height) + sm.resetHeaderState(&selectedTipHash, virtualBlock.SelectedTipHeight()) } } else { log.Info("Checkpoints are disabled") diff --git a/server/p2p/p2p.go b/server/p2p/p2p.go index 3b11f336e..db39651d9 100644 --- a/server/p2p/p2p.go +++ b/server/p2p/p2p.go @@ -290,8 +290,9 @@ func newServerPeer(s *Server, isPersistent bool) *Peer { // newestBlock returns the current best block hash and height using the format // required by the configuration for the peer package. func (sp *Peer) newestBlock() (*daghash.Hash, int32, error) { - dagState := sp.server.DAG.GetDAGState() - return &dagState.SelectedTip.Hash, dagState.SelectedTip.Height, nil + virtualBlock := sp.server.DAG.VirtualBlock() + selectedTipHash := virtualBlock.SelectedTipHash() + return &selectedTipHash, virtualBlock.SelectedTipHeight(), nil } // addKnownAddresses adds the given addresses to the set of known addresses to @@ -1307,9 +1308,9 @@ func (s *Server) pushBlockMsg(sp *Peer, hash *daghash.Hash, doneChan chan<- stru // to trigger it to issue another getblocks message for the next // batch of inventory. if sendInv { - dagState := sp.server.DAG.GetDAGState() + selectedTipHash := sp.server.DAG.VirtualBlock().SelectedTipHash() invMsg := wire.NewMsgInvSizeHint(1) - iv := wire.NewInvVect(wire.InvTypeBlock, &dagState.SelectedTip.Hash) + iv := wire.NewInvVect(wire.InvTypeBlock, &selectedTipHash) invMsg.AddInvVect(iv) sp.QueueMessage(invMsg, doneChan) sp.continueHash = nil @@ -2418,7 +2419,7 @@ func NewServer(listenAddrs []string, db database.DB, dagParams *dagconfig.Params // If no feeEstimator has been found, or if the one that has been found // is behind somehow, create a new one and start over. - if s.FeeEstimator == nil || s.FeeEstimator.LastKnownHeight() != s.DAG.GetDAGState().SelectedTip.Height { + if s.FeeEstimator == nil || s.FeeEstimator.LastKnownHeight() != s.DAG.VirtualBlock().SelectedTipHeight() { s.FeeEstimator = mempool.NewFeeEstimator( mempool.DefaultEstimateFeeMaxRollback, mempool.DefaultEstimateFeeMinRegisteredBlocks) @@ -2436,10 +2437,10 @@ func NewServer(listenAddrs []string, db database.DB, dagParams *dagconfig.Params MaxTxVersion: 1, }, ChainParams: dagParams, - FetchUtxoView: s.DAG.FetchUtxoView, - BestHeight: func() int32 { return s.DAG.GetDAGState().SelectedTip.Height }, - MedianTimePast: func() time.Time { return s.DAG.GetDAGState().SelectedTip.MedianTime }, - CalcSequenceLock: func(tx *util.Tx, view *blockdag.UtxoViewpoint) (*blockdag.SequenceLock, error) { + FetchUtxoView: s.DAG.FetchUTXOView, + BestHeight: func() int32 { return s.DAG.VirtualBlock().SelectedTipHeight() }, + MedianTimePast: func() time.Time { return s.DAG.VirtualBlock().SelectedTip().CalcPastMedianTime() }, + CalcSequenceLock: func(tx *util.Tx, view *blockdag.UTXOView) (*blockdag.SequenceLock, error) { return s.DAG.CalcSequenceLock(tx, view, true) }, IsDeploymentActive: s.DAG.IsDeploymentActive, diff --git a/server/rpc/rpcserver.go b/server/rpc/rpcserver.go index b6ff0e3b5..62fbbc08a 100644 --- a/server/rpc/rpcserver.go +++ b/server/rpc/rpcserver.go @@ -1009,18 +1009,18 @@ func handleGetBestBlock(s *Server, cmd interface{}, closeChan <-chan struct{}) ( // All other "get block" commands give either the height, the // hash, or both but require the block SHA. This gets both for // the best block. - dagState := s.cfg.DAG.GetDAGState() + virtualBlock := s.cfg.DAG.VirtualBlock() result := &btcjson.GetBestBlockResult{ - Hash: dagState.SelectedTip.Hash.String(), - Height: dagState.SelectedTip.Height, + Hash: virtualBlock.SelectedTipHash().String(), + Height: virtualBlock.SelectedTipHeight(), } return result, nil } // handleGetBestBlockHash implements the getbestblockhash command. func handleGetBestBlockHash(s *Server, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { - dagState := s.cfg.DAG.GetDAGState() - return dagState.SelectedTip.Hash.String(), nil + virtualBlock := s.cfg.DAG.VirtualBlock() + return virtualBlock.SelectedTipHash().String(), nil } // getDifficultyRatio returns the proof-of-work difficulty as a multiple of the @@ -1087,11 +1087,11 @@ func handleGetBlock(s *Server, cmd interface{}, closeChan <-chan struct{}) (inte return nil, internalRPCError(err.Error(), context) } blk.SetHeight(blockHeight) - dagState := s.cfg.DAG.GetDAGState() + virtualBlock := s.cfg.DAG.VirtualBlock() // Get the hashes for the next blocks unless there are none. var nextHashStrings []string - if blockHeight < dagState.SelectedTip.Height { + if blockHeight < virtualBlock.SelectedTipHeight() { childHashes, err := s.cfg.DAG.ChildHashesByHash(hash) if err != nil { context := "No next block" @@ -1110,7 +1110,7 @@ func handleGetBlock(s *Server, cmd interface{}, closeChan <-chan struct{}) (inte PreviousHashes: daghash.Strings(blockHeader.PrevBlocks), Nonce: blockHeader.Nonce, Time: blockHeader.Timestamp.Unix(), - Confirmations: uint64(1 + dagState.SelectedTip.Height - blockHeight), + Confirmations: uint64(1 + virtualBlock.SelectedTipHeight() - blockHeight), Height: int64(blockHeight), Size: int32(len(blkBytes)), Bits: strconv.FormatInt(int64(blockHeader.Bits), 16), @@ -1132,7 +1132,7 @@ func handleGetBlock(s *Server, cmd interface{}, closeChan <-chan struct{}) (inte for i, tx := range txns { rawTxn, err := createTxRawResult(params, tx.MsgTx(), tx.Hash().String(), blockHeader, hash.String(), - blockHeight, dagState.SelectedTip.Height) + blockHeight, virtualBlock.SelectedTipHeight()) if err != nil { return nil, err } @@ -1169,15 +1169,15 @@ func handleGetBlockDAGInfo(s *Server, cmd interface{}, closeChan <-chan struct{} // populate the response to this call primarily from this snapshot. params := s.cfg.ChainParams dag := s.cfg.DAG - dagState := dag.GetDAGState() + virtualBlock := dag.VirtualBlock() - chainInfo := &btcjson.GetBlockDAGInfoResult{ + dagInfo := &btcjson.GetBlockDAGInfoResult{ DAG: params.Name, - Blocks: dagState.SelectedTip.Height, - Headers: dagState.SelectedTip.Height, - TipHashes: daghash.Strings(dagState.TipHashes), - Difficulty: getDifficultyRatio(dagState.SelectedTip.Bits, params), - MedianTime: dagState.SelectedTip.MedianTime.Unix(), + Blocks: virtualBlock.SelectedTipHeight(), + Headers: virtualBlock.SelectedTipHeight(), + TipHashes: daghash.Strings(virtualBlock.TipHashes()), + Difficulty: getDifficultyRatio(virtualBlock.SelectedTip().Header().Bits, params), + MedianTime: virtualBlock.SelectedTip().CalcPastMedianTime().Unix(), Pruned: false, Bip9SoftForks: make(map[string]*btcjson.Bip9SoftForkDescription), } @@ -1222,7 +1222,7 @@ func handleGetBlockDAGInfo(s *Server, cmd interface{}, closeChan <-chan struct{} // Finally, populate the soft-fork description with all the // information gathered above. - chainInfo.Bip9SoftForks[forkName] = &btcjson.Bip9SoftForkDescription{ + dagInfo.Bip9SoftForks[forkName] = &btcjson.Bip9SoftForkDescription{ Status: strings.ToLower(statusString), Bit: deploymentDetails.BitNumber, StartTime: int64(deploymentDetails.StartTime), @@ -1230,13 +1230,13 @@ func handleGetBlockDAGInfo(s *Server, cmd interface{}, closeChan <-chan struct{} } } - return chainInfo, nil + return dagInfo, nil } // handleGetBlockCount implements the getblockcount command. func handleGetBlockCount(s *Server, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { - dagState := s.cfg.DAG.GetDAGState() - return int64(dagState.SelectedTip.Height), nil + virtualBlock := s.cfg.DAG.VirtualBlock() + return int64(virtualBlock.SelectedTipHeight()), nil } // handleGetBlockHash implements the getblockhash command. @@ -1283,11 +1283,11 @@ func handleGetBlockHeader(s *Server, cmd interface{}, closeChan <-chan struct{}) context := "Failed to obtain block height" return nil, internalRPCError(err.Error(), context) } - dagState := s.cfg.DAG.GetDAGState() + virtualBlock := s.cfg.DAG.VirtualBlock() // Get the hashes for the next blocks unless there are none. var nextHashStrings []string - if blockHeight < dagState.SelectedTip.Height { + if blockHeight < virtualBlock.SelectedTipHeight() { childHashes, err := s.cfg.DAG.ChildHashesByHash(hash) if err != nil { context := "No next block" @@ -1299,7 +1299,7 @@ func handleGetBlockHeader(s *Server, cmd interface{}, closeChan <-chan struct{}) params := s.cfg.ChainParams blockHeaderReply := btcjson.GetBlockHeaderVerboseResult{ Hash: c.Hash, - Confirmations: uint64(1 + dagState.SelectedTip.Height - blockHeight), + Confirmations: uint64(1 + virtualBlock.SelectedTipHeight() - blockHeight), Height: blockHeight, Version: blockHeader.Version, VersionHex: fmt.Sprintf("%08x", blockHeader.Version), @@ -1493,7 +1493,8 @@ func (state *gbtWorkState) updateBlockTemplate(s *Server, useCoinbaseValue bool) // generated. var msgBlock *wire.MsgBlock var targetDifficulty string - tipHashes := s.cfg.DAG.GetDAGState().TipHashes + virtualBlock := s.cfg.DAG.VirtualBlock() + tipHashes := virtualBlock.TipHashes() template := state.template if template == nil || state.tipHashes == nil || !daghash.AreEqual(state.tipHashes, tipHashes) || @@ -1532,8 +1533,7 @@ func (state *gbtWorkState) updateBlockTemplate(s *Server, useCoinbaseValue bool) // Get the minimum allowed timestamp for the block based on the // median timestamp of the last several blocks per the chain // consensus rules. - dagState := s.cfg.DAG.GetDAGState() - minTimestamp := mining.MinimumMedianTime(dagState) + minTimestamp := mining.MinimumMedianTime(virtualBlock.SelectedTip().CalcPastMedianTime()) // Update work state to ensure another block template isn't // generated until needed. @@ -1889,7 +1889,7 @@ func handleGetBlockTemplateRequest(s *Server, request *btcjson.TemplateRequest, } // No point in generating or accepting work before the chain is synced. - currentHeight := s.cfg.DAG.GetDAGState().SelectedTip.Height + currentHeight := s.cfg.DAG.VirtualBlock().SelectedTipHeight() if currentHeight != 0 && !s.cfg.SyncMgr.IsCurrent() { return nil, &btcjson.RPCError{ Code: btcjson.ErrRPCClientInInitialDownload, @@ -2054,7 +2054,7 @@ func handleGetBlockTemplateProposal(s *Server, request *btcjson.TemplateRequest) block := util.NewBlock(&msgBlock) // Ensure the block is building from the expected previous blocks. - expectedPrevHashes := s.cfg.DAG.GetDAGState().TipHashes + expectedPrevHashes := s.cfg.DAG.VirtualBlock().TipHashes() prevHashes := block.MsgBlock().Header.PrevBlocks if !daghash.AreEqual(expectedPrevHashes, prevHashes) { return "bad-prevblk", nil @@ -2176,8 +2176,8 @@ func handleGetCurrentNet(s *Server, cmd interface{}, closeChan <-chan struct{}) // handleGetDifficulty implements the getdifficulty command. func handleGetDifficulty(s *Server, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { - dagState := s.cfg.DAG.GetDAGState() - return getDifficultyRatio(dagState.SelectedTip.Bits, s.cfg.ChainParams), nil + virtualBlock := s.cfg.DAG.VirtualBlock() + return getDifficultyRatio(virtualBlock.SelectedTip().Header().Bits, s.cfg.ChainParams), nil } // handleGetGenerate implements the getgenerate command. @@ -2234,15 +2234,15 @@ func handleGetHeaders(s *Server, cmd interface{}, closeChan <-chan struct{}) (in // handleGetInfo implements the getinfo command. We only return the fields // that are not related to wallet functionality. func handleGetInfo(s *Server, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { - dagState := s.cfg.DAG.GetDAGState() + virtualBlock := s.cfg.DAG.VirtualBlock() ret := &btcjson.InfoDAGResult{ Version: int32(1000000*version.AppMajor + 10000*version.AppMinor + 100*version.AppPatch), ProtocolVersion: int32(maxProtocolVersion), - Blocks: dagState.SelectedTip.Height, + Blocks: virtualBlock.SelectedTipHeight(), TimeOffset: int64(s.cfg.TimeSource.Offset().Seconds()), Connections: s.cfg.ConnMgr.ConnectedCount(), Proxy: config.MainConfig().Proxy, - Difficulty: getDifficultyRatio(dagState.SelectedTip.Bits, s.cfg.ChainParams), + Difficulty: getDifficultyRatio(virtualBlock.SelectedTip().Header().Bits, s.cfg.ChainParams), TestNet: config.MainConfig().TestNet3, RelayFee: config.MainConfig().MinRelayTxFee.ToBTC(), } @@ -2286,12 +2286,21 @@ func handleGetMiningInfo(s *Server, cmd interface{}, closeChan <-chan struct{}) } } - dagState := s.cfg.DAG.GetDAGState() + virtualBlock := s.cfg.DAG.VirtualBlock() + selectedTipHash := virtualBlock.SelectedTipHash() + selectedBlock, err := s.cfg.DAG.BlockByHash(&selectedTipHash) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInternal.Code, + Message: "could not find block for selected tip", + } + } + result := btcjson.GetMiningInfoResult{ - Blocks: int64(dagState.SelectedTip.Height), - CurrentBlockSize: dagState.SelectedTip.BlockSize, - CurrentBlockTx: dagState.SelectedTip.NumTxs, - Difficulty: getDifficultyRatio(dagState.SelectedTip.Bits, s.cfg.ChainParams), + Blocks: int64(virtualBlock.SelectedTipHeight()), + CurrentBlockSize: uint64(selectedBlock.MsgBlock().SerializeSize()), + CurrentBlockTx: uint64(len(selectedBlock.MsgBlock().Transactions)), + Difficulty: getDifficultyRatio(virtualBlock.SelectedTip().Header().Bits, s.cfg.ChainParams), Generate: s.cfg.CPUMiner.IsMining(), GenProcLimit: s.cfg.CPUMiner.NumWorkers(), HashesPerSec: int64(s.cfg.CPUMiner.HashesPerSecond()), @@ -2476,7 +2485,7 @@ func handleGetRawTransaction(s *Server, cmd interface{}, closeChan <-chan struct // The verbose flag is set, so generate the JSON object and return it. var blkHeader *wire.BlockHeader var blkHashStr string - var chainHeight int32 + var dagHeight int32 if blkHash != nil { // Fetch the header from chain. header, err := s.cfg.DAG.HeaderByHash(blkHash) @@ -2487,11 +2496,11 @@ func handleGetRawTransaction(s *Server, cmd interface{}, closeChan <-chan struct blkHeader = &header blkHashStr = blkHash.String() - chainHeight = s.cfg.DAG.GetDAGState().SelectedTip.Height + dagHeight = s.cfg.DAG.VirtualBlock().SelectedTipHeight() } rawTxn, err := createTxRawResult(s.cfg.ChainParams, mtx, txHash.String(), - blkHeader, blkHashStr, blkHeight, chainHeight) + blkHeader, blkHashStr, blkHeight, dagHeight) if err != nil { return nil, err } @@ -2543,16 +2552,16 @@ func handleGetTxOut(s *Server, cmd interface{}, closeChan <-chan struct{}) (inte return nil, internalRPCError(errStr, "") } - dagState := s.cfg.DAG.GetDAGState() - bestBlockHash = dagState.SelectedTip.Hash.String() + virtualBlock := s.cfg.DAG.VirtualBlock() + bestBlockHash = virtualBlock.SelectedTipHash().String() confirmations = 0 value = txOut.Value pkScript = txOut.PkScript isCoinbase = blockdag.IsCoinBaseTx(mtx) } else { out := wire.OutPoint{Hash: *txHash, Index: c.Vout} - entry, err := s.cfg.DAG.FetchUtxoEntry(out) - if err != nil { + entry, ok := s.cfg.DAG.VirtualBlock().GetUTXOEntry(out) + if !ok { return nil, rpcNoTxInfoError(txHash) } @@ -2565,9 +2574,9 @@ func handleGetTxOut(s *Server, cmd interface{}, closeChan <-chan struct{}) (inte return nil, nil } - dagState := s.cfg.DAG.GetDAGState() - bestBlockHash = dagState.SelectedTip.Hash.String() - confirmations = 1 + dagState.SelectedTip.Height - entry.BlockHeight() + virtualBlock := s.cfg.DAG.VirtualBlock() + bestBlockHash = virtualBlock.SelectedTipHash().String() + confirmations = 1 + virtualBlock.SelectedTipHeight() - entry.BlockHeight() value = entry.Amount() pkScript = entry.PkScript() isCoinbase = entry.IsCoinBase() @@ -3060,7 +3069,7 @@ func handleSearchRawTransactions(s *Server, cmd interface{}, closeChan <-chan st } // The verbose flag is set, so generate the JSON object and return it. - dagState := s.cfg.DAG.GetDAGState() + virtualBlock := s.cfg.DAG.VirtualBlock() srtList := make([]btcjson.SearchRawTransactionsResult, len(addressTxns)) for i := range addressTxns { // The deserialized transaction is needed, so deserialize the @@ -3130,7 +3139,7 @@ func handleSearchRawTransactions(s *Server, cmd interface{}, closeChan <-chan st result.Time = uint64(blkHeader.Timestamp.Unix()) result.Blocktime = uint64(blkHeader.Timestamp.Unix()) result.BlockHash = blkHashStr - result.Confirmations = uint64(1 + dagState.SelectedTip.Height - blkHeight) + result.Confirmations = uint64(1 + virtualBlock.SelectedTipHeight() - blkHeight) } } @@ -3315,18 +3324,18 @@ func handleValidateAddress(s *Server, cmd interface{}, closeChan <-chan struct{} } func verifyDAG(s *Server, level, depth int32) error { - dagState := s.cfg.DAG.GetDAGState() - finishHeight := dagState.SelectedTip.Height - depth + virtualBlock := s.cfg.DAG.VirtualBlock() + finishHeight := virtualBlock.SelectedTipHeight() - depth if finishHeight < 0 { finishHeight = 0 } log.Infof("Verifying chain for %d blocks at level %d", - dagState.SelectedTip.Height-finishHeight, level) + virtualBlock.SelectedTipHeight()-finishHeight, level) - currentHash := &dagState.SelectedTip.Hash - for height := dagState.SelectedTip.Height; height > finishHeight; { + currentHash := virtualBlock.SelectedTipHash() + for height := virtualBlock.SelectedTipHeight(); height > finishHeight; { // Level 0 just looks up the block. - block, err := s.cfg.DAG.BlockByHash(currentHash) + block, err := s.cfg.DAG.BlockByHash(¤tHash) if err != nil { log.Errorf("Verify is unable to fetch block at "+ "height %d: %v", height, err) @@ -3345,7 +3354,7 @@ func verifyDAG(s *Server, level, depth int32) error { } } - currentHash = block.MsgBlock().Header.SelectedPrevBlock() + currentHash = *block.MsgBlock().Header.SelectedPrevBlock() } log.Infof("Chain verify completed successfully")