dcrd/blockchain/utxoviewpoint.go
Dave Collins 2dac209198
blockchain: Use lastest major version deps.
This udpates the blockchain module to use the lastest module major
versions.

While here, it also corrects a few typos and updates some test function
names to more accurately reflect their purpose.

The updated direct dependencies are as follows:

- github.com/decred/dcrd/blockchain/stake/v2@v2.0.1
- github.com/decred/dcrd/chaincfg/v2@v2.2.0
- github.com/decred/dcrd/database/v2@v2.0.0
- github.com/decred/dcrd/dcrutil/v2@v2.0.0
- github.com/decred/dcrd/txscript/v2@v2.0.0
2019-08-08 13:10:20 -05:00

986 lines
35 KiB
Go

// Copyright (c) 2015-2016 The btcsuite developers
// Copyright (c) 2015-2019 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package blockchain
import (
"fmt"
"github.com/decred/dcrd/blockchain/stake/v2"
"github.com/decred/dcrd/blockchain/standalone"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/database/v2"
"github.com/decred/dcrd/dcrutil/v2"
"github.com/decred/dcrd/txscript/v2"
)
// utxoOutput houses details about an individual unspent transaction output such
// as whether or not it is spent, its public key script, and how much it pays.
//
// Standard public key scripts are stored in the database using a compressed
// format. Since the vast majority of scripts are of the standard form, a fairly
// significant savings is achieved by discarding the portions of the standard
// scripts that can be reconstructed.
//
// Also, since it is common for only a specific output in a given utxo entry to
// be referenced from a redeeming transaction, the script and amount for a given
// output is not uncompressed until the first time it is accessed. This
// provides a mechanism to avoid the overhead of needlessly uncompressing all
// outputs for a given utxo entry at the time of load.
//
// The struct is aligned for memory efficiency.
type utxoOutput struct {
pkScript []byte // The public key script for the output.
amount int64 // The amount of the output.
scriptVersion uint16 // The script version
compressed bool // The public key script is compressed.
spent bool // Output is spent.
}
// maybeDecompress decompresses the amount and public key script fields of the
// utxo and marks it decompressed if needed.
func (o *utxoOutput) maybeDecompress(compressionVersion uint32) {
// Nothing to do if it's not compressed.
if !o.compressed {
return
}
o.pkScript = decompressScript(o.pkScript, compressionVersion)
o.compressed = false
}
// UtxoEntry contains contextual information about an unspent transaction such
// as whether or not it is a coinbase transaction, which block it was found in,
// and the spent status of its outputs.
//
// The struct is aligned for memory efficiency.
type UtxoEntry struct {
sparseOutputs map[uint32]*utxoOutput // Sparse map of unspent outputs.
stakeExtra []byte // Extra data for the staking system.
txType stake.TxType // The stake type of the transaction.
height uint32 // Height of block containing tx.
index uint32 // Index of containing tx in block.
txVersion uint16 // The tx version of this tx.
isCoinBase bool // Whether entry is a coinbase tx.
hasExpiry bool // Whether entry has an expiry.
modified bool // Entry changed since load.
}
// TxVersion returns the transaction version of the transaction the
// utxo represents.
func (entry *UtxoEntry) TxVersion() uint16 {
return entry.txVersion
}
// HasExpiry returns the transaction expiry for the transaction that the utxo
// entry represents.
func (entry *UtxoEntry) HasExpiry() bool {
return entry.hasExpiry
}
// IsCoinBase returns whether or not the transaction the utxo entry represents
// is a coinbase.
func (entry *UtxoEntry) IsCoinBase() bool {
return entry.isCoinBase
}
// BlockHeight returns the height of the block containing the transaction the
// utxo entry represents.
func (entry *UtxoEntry) BlockHeight() int64 {
return int64(entry.height)
}
// BlockIndex returns the height of the block containing the transaction the
// utxo entry represents.
func (entry *UtxoEntry) BlockIndex() uint32 {
return entry.index
}
// TransactionType returns the transaction type of the transaction the utxo entry
// represents.
func (entry *UtxoEntry) TransactionType() stake.TxType {
return entry.txType
}
// IsOutputSpent returns whether or not the provided output index has been
// spent based upon the current state of the unspent transaction output view
// the entry was obtained from.
//
// Returns true if the output index references an output that does not exist
// either due to it being invalid or because the output is not part of the view
// due to previously being spent/pruned.
func (entry *UtxoEntry) IsOutputSpent(outputIndex uint32) bool {
output, ok := entry.sparseOutputs[outputIndex]
if !ok {
return true
}
return output.spent
}
// SpendOutput marks the output at the provided index as spent. Specifying an
// output index that does not exist will not have any effect.
func (entry *UtxoEntry) SpendOutput(outputIndex uint32) {
output, ok := entry.sparseOutputs[outputIndex]
if !ok {
return
}
// Nothing to do if the output is already spent.
if output.spent {
return
}
entry.modified = true
output.spent = true
}
// IsFullySpent returns whether or not the transaction the utxo entry represents
// is fully spent.
func (entry *UtxoEntry) IsFullySpent() bool {
// The entry is not fully spent if any of the outputs are unspent.
for _, output := range entry.sparseOutputs {
if !output.spent {
return false
}
}
return true
}
// AmountByIndex returns the amount of the provided output index.
//
// Returns 0 if the output index references an output that does not exist
// either due to it being invalid or because the output is not part of the view
// due to previously being spent/pruned.
func (entry *UtxoEntry) AmountByIndex(outputIndex uint32) int64 {
output, ok := entry.sparseOutputs[outputIndex]
if !ok {
return 0
}
return output.amount
}
// ScriptVersionByIndex returns the public key script for the provided output
// index.
//
// Returns 0 if the output index references an output that does not exist
// either due to it being invalid or because the output is not part of the view
// due to previously being spent/pruned.
func (entry *UtxoEntry) ScriptVersionByIndex(outputIndex uint32) uint16 {
output, ok := entry.sparseOutputs[outputIndex]
if !ok {
return 0
}
return output.scriptVersion
}
// PkScriptByIndex returns the public key script for the provided output index.
//
// Returns nil if the output index references an output that does not exist
// either due to it being invalid or because the output is not part of the view
// due to previously being spent/pruned.
func (entry *UtxoEntry) PkScriptByIndex(outputIndex uint32) []byte {
output, ok := entry.sparseOutputs[outputIndex]
if !ok {
return nil
}
// Ensure the output is decompressed before returning the script.
output.maybeDecompress(currentCompressionVersion)
return output.pkScript
}
// Clone returns a deep copy of the utxo entry.
func (entry *UtxoEntry) Clone() *UtxoEntry {
if entry == nil {
return nil
}
newEntry := &UtxoEntry{
stakeExtra: make([]byte, len(entry.stakeExtra)),
txVersion: entry.txVersion,
height: entry.height,
index: entry.index,
txType: entry.txType,
isCoinBase: entry.isCoinBase,
hasExpiry: entry.hasExpiry,
sparseOutputs: make(map[uint32]*utxoOutput),
}
copy(newEntry.stakeExtra, entry.stakeExtra)
for outputIndex, output := range entry.sparseOutputs {
newEntry.sparseOutputs[outputIndex] = &utxoOutput{
pkScript: output.pkScript,
amount: output.amount,
scriptVersion: output.scriptVersion,
compressed: output.compressed,
spent: output.spent,
}
}
return newEntry
}
// newUtxoEntry returns a new unspent transaction output entry with the provided
// coinbase flag and block height ready to have unspent outputs added.
func newUtxoEntry(txVersion uint16, height uint32, index uint32, isCoinBase bool, hasExpiry bool, txType stake.TxType) *UtxoEntry {
return &UtxoEntry{
sparseOutputs: make(map[uint32]*utxoOutput),
txVersion: txVersion,
height: height,
index: index,
isCoinBase: isCoinBase,
hasExpiry: hasExpiry,
txType: txType,
}
}
// UtxoViewpoint 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[chainhash.Hash]*UtxoEntry
bestHash chainhash.Hash
}
// BestHash returns the hash of the best block in the chain the view currently
// respresents.
func (view *UtxoViewpoint) BestHash() *chainhash.Hash {
return &view.bestHash
}
// SetBestHash sets the hash of the best block in the chain the view currently
// respresents.
func (view *UtxoViewpoint) SetBestHash(hash *chainhash.Hash) {
view.bestHash = *hash
}
// LookupEntry returns information about a given transaction according to the
// current state of the view. It will return nil if the passed transaction
// hash 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(txHash *chainhash.Hash) *UtxoEntry {
entry, ok := view.entries[*txHash]
if !ok {
return nil
}
return entry
}
// AddTxOuts adds all outputs in the passed transaction which are not provably
// 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 *dcrutil.Tx, blockHeight int64, blockIndex uint32) {
// When there are not already any utxos associated with the transaction,
// add a new entry for it to the view.
entry := view.LookupEntry(tx.Hash())
if entry == nil {
msgTx := tx.MsgTx()
txType := stake.DetermineTxType(msgTx)
entry = newUtxoEntry(msgTx.Version, uint32(blockHeight),
blockIndex, standalone.IsCoinBaseTx(msgTx), msgTx.Expiry != 0,
txType)
if txType == stake.TxTypeSStx {
stakeExtra := make([]byte, serializeSizeForMinimalOutputs(tx))
putTxToMinimalOutputs(stakeExtra, tx)
entry.stakeExtra = stakeExtra
}
view.entries[*tx.Hash()] = entry
} else {
entry.height = uint32(blockHeight)
entry.index = blockIndex
}
entry.modified = true
// Loop all of the transaction outputs and add those which are not
// provably unspendable.
for txOutIdx, txOut := range tx.MsgTx().TxOut {
// TODO allow pruning of stake utxs after all other outputs are spent
if txscript.IsUnspendable(txOut.Value, txOut.PkScript) {
continue
}
// Update existing entries. All fields are updated because it's
// possible (although extremely unlikely) that the existing
// entry is being replaced by a different transaction with the
// same hash. This is allowed so long as the previous
// transaction is fully spent.
if output, ok := entry.sparseOutputs[uint32(txOutIdx)]; ok {
output.spent = false
output.amount = txOut.Value
output.scriptVersion = txOut.Version
output.pkScript = txOut.PkScript
output.compressed = false
continue
}
// Add the unspent transaction output.
entry.sparseOutputs[uint32(txOutIdx)] = &utxoOutput{
spent: false,
amount: txOut.Value,
scriptVersion: txOut.Version,
pkScript: txOut.PkScript,
compressed: false,
}
}
}
// connectTransaction updates the view by adding all new utxos created by the
// passed transaction and marking all utxos that the transactions spend as
// 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 *dcrutil.Tx, blockHeight int64, blockIndex uint32, stxos *[]spentTxOut) error {
// Coinbase transactions don't have any inputs to spend.
msgTx := tx.MsgTx()
if standalone.IsCoinBaseTx(msgTx) {
// Add the transaction's outputs as available utxos.
view.AddTxOuts(tx, blockHeight, blockIndex)
return nil
}
// Spend the referenced utxos by marking them spent in the view and,
// if a slice was provided for the spent txout details, append an entry
// to it.
isVote := stake.IsSSGen(msgTx)
for txInIdx, txIn := range msgTx.TxIn {
// Ignore stakebase since it has no input.
if isVote && txInIdx == 0 {
continue
}
// Ensure the referenced utxo exists in the view. This should
// never happen unless there is a bug is introduced in the code.
originIndex := txIn.PreviousOutPoint.Index
entry := view.entries[txIn.PreviousOutPoint.Hash]
if entry == nil {
return AssertError(fmt.Sprintf("view missing input %v",
txIn.PreviousOutPoint))
}
entry.SpendOutput(originIndex)
// Don't create the stxo details if not requested.
if stxos == nil {
continue
}
// Populate the stxo details using the utxo entry. When the
// transaction is fully spent, set the additional stxo fields
// accordingly since those details will no longer be available
// in the utxo set.
var stxo = spentTxOut{
compressed: false,
amount: txIn.ValueIn,
scriptVersion: entry.ScriptVersionByIndex(originIndex),
pkScript: entry.PkScriptByIndex(originIndex),
}
if entry.IsFullySpent() {
stxo.txVersion = entry.TxVersion()
stxo.height = uint32(entry.BlockHeight())
stxo.index = entry.BlockIndex()
stxo.isCoinBase = entry.IsCoinBase()
stxo.hasExpiry = entry.HasExpiry()
stxo.txType = entry.TransactionType()
stxo.txFullySpent = true
if entry.txType == stake.TxTypeSStx {
stxo.stakeExtra = entry.stakeExtra
}
}
// Append the entry to the provided spent txouts slice.
*stxos = append(*stxos, stxo)
}
// Add the transaction's outputs as available utxos.
view.AddTxOuts(tx, blockHeight, blockIndex)
return nil
}
// disconnectTransactions updates the view by removing all utxos created by
// the transactions in either the regular or stake tree of the block, depending
// on the flag, and unspending all of the txos spent by those same transactions
// by using the provided spent txo information.
func (view *UtxoViewpoint) disconnectTransactions(block *dcrutil.Block, stxos []spentTxOut, stakeTree bool) error {
// Choose which transaction tree to use and the appropriate offset into the
// spent transaction outputs that corresponds to them depending on the flag.
// Transactions in the stake tree are spent before transactions in the
// regular tree, thus skip all of the outputs spent by the regular tree when
// disconnecting stake transactions.
stxoIdx := len(stxos) - 1
transactions := block.Transactions()
if stakeTree {
stxoIdx = len(stxos) - countSpentRegularOutputs(block) - 1
transactions = block.STransactions()
}
for txIdx := len(transactions) - 1; txIdx > -1; txIdx-- {
tx := transactions[txIdx]
msgTx := tx.MsgTx()
txType := stake.TxTypeRegular
if stakeTree {
txType = stake.DetermineTxType(msgTx)
}
isVote := txType == stake.TxTypeSSGen
// Clear this transaction from the view if it already exists or create a
// new empty entry for when it does not. This is done because the code
// relies on its existence in the view in order to signal modifications
// have happened.
isCoinbase := !stakeTree && txIdx == 0
entry := view.entries[*tx.Hash()]
if entry == nil {
entry = newUtxoEntry(msgTx.Version, uint32(block.Height()),
uint32(txIdx), isCoinbase, msgTx.Expiry != 0, txType)
view.entries[*tx.Hash()] = entry
}
entry.modified = true
entry.sparseOutputs = make(map[uint32]*utxoOutput)
// Loop backwards through all of the transaction inputs (except for the
// coinbase which has no inputs) and unspend the referenced txos. This
// is necessary to match the order of the spent txout entries.
if isCoinbase {
continue
}
for txInIdx := len(msgTx.TxIn) - 1; txInIdx > -1; txInIdx-- {
// Ignore stakebase since it has no input.
if isVote && txInIdx == 0 {
continue
}
// Ensure the spent txout index is decremented to stay in sync with
// the transaction input.
stxo := &stxos[stxoIdx]
stxoIdx--
// When there is not already an entry for the referenced transaction
// in the view, it means it was fully spent, so create a new utxo
// entry in order to resurrect it.
txIn := msgTx.TxIn[txInIdx]
originHash := &txIn.PreviousOutPoint.Hash
originIndex := txIn.PreviousOutPoint.Index
entry := view.entries[*originHash]
if entry == nil {
if !stxo.txFullySpent {
return AssertError(fmt.Sprintf("tried to revive unspent "+
"tx %v from non-fully spent stx entry", originHash))
}
entry = newUtxoEntry(msgTx.Version, stxo.height, stxo.index,
stxo.isCoinBase, stxo.hasExpiry, stxo.txType)
if stxo.txType == stake.TxTypeSStx {
entry.stakeExtra = stxo.stakeExtra
}
view.entries[*originHash] = entry
}
// Mark the entry as modified since it is either new or will be
// changed below.
entry.modified = true
// Restore the specific utxo using the stxo data from the spend
// journal if it doesn't already exist in the view.
output, ok := entry.sparseOutputs[originIndex]
if !ok {
// Add the unspent transaction output.
entry.sparseOutputs[originIndex] = &utxoOutput{
compressed: stxo.compressed,
spent: false,
amount: txIn.ValueIn,
scriptVersion: stxo.scriptVersion,
pkScript: stxo.pkScript,
}
continue
}
// Mark the existing referenced transaction output as unspent.
output.spent = false
}
}
return nil
}
// disconnectRegularTransactions updates the view by removing all utxos created
// by the transactions in regular tree of the provided block and unspending all
// of the txos spent by those same transactions by using the provided spent txo
// information.
func (view *UtxoViewpoint) disconnectRegularTransactions(block *dcrutil.Block, stxos []spentTxOut) error {
return view.disconnectTransactions(block, stxos, false)
}
// disconnectStakeTransactions updates the view by removing all utxos created
// by the transactions in stake tree of the provided block and unspending all
// of the txos spent by those same transactions by using the provided spent txo
// information.
func (view *UtxoViewpoint) disconnectStakeTransactions(block *dcrutil.Block, stxos []spentTxOut) error {
return view.disconnectTransactions(block, stxos, true)
}
// disconnectDisapprovedBlock updates the view by disconnecting all of the
// transactions in the regular tree of the passed block.
//
// Disconnecting a transaction entails removing the utxos created by it and
// restoring the outputs spent by it with the help of the provided spent txo
// information.
//func (view *UtxoViewpoint) disconnectDisapprovedBlock(db database.DB, block *dcrutil.Block, stxos []spentTxOut) error {
func (view *UtxoViewpoint) disconnectDisapprovedBlock(db database.DB, block *dcrutil.Block) error {
// Load all of the spent txos for the block from the database spend journal.
var stxos []spentTxOut
err := db.View(func(dbTx database.Tx) error {
var err error
stxos, err = dbFetchSpendJournalEntry(dbTx, block)
return err
})
if err != nil {
return err
}
// Load all of the utxos referenced by the inputs for all transactions in
// the block that don't already exist in the utxo view from the database.
err = view.fetchRegularInputUtxos(db, block)
if err != nil {
return err
}
// Sanity check the correct number of stxos are provided.
if len(stxos) != countSpentOutputs(block) {
panicf("provided %v stxos for block %v (height %v) which spends %v "+
"outputs", len(stxos), block.Hash(), block.MsgBlock().Header.Height,
countSpentOutputs(block))
}
return view.disconnectRegularTransactions(block, stxos)
}
// connectBlock updates the view by potentially disconnecting all of the
// transactions in the regular tree of the parent block of the passed block in
// the case the passed block disapproves the parent block, connecting all of
// transactions in both the regular and stake trees of the passed block, and
// setting the best hash for the view to the passed block.
//
// Connecting a transaction entails marking all utxos it spends as spent, and
// adding all of the new utxos created by it.
//
// Disconnecting a transaction entails removing the utxos created by it and
// restoring the outputs spent by it with the help of the provided spent txo
// information.
//
// In addition, when the 'stxos' argument is not nil, it will be updated to
// append an entry for each spent txout.
func (view *UtxoViewpoint) connectBlock(db database.DB, block, parent *dcrutil.Block, stxos *[]spentTxOut) error {
// Disconnect the transactions in the regular tree of the parent block if
// the passed block disapproves it.
if !headerApprovesParent(&block.MsgBlock().Header) {
err := view.disconnectDisapprovedBlock(db, parent)
if err != nil {
return err
}
}
// Load all of the utxos referenced by the inputs for all transactions in
// the block that don't already exist in the utxo view from the database.
err := view.fetchInputUtxos(db, block)
if err != nil {
return err
}
// Connect all of the transactions in both the regular and stake trees of
// the block. Notice that the stake tree is connected before the regular
// tree. This means that stake transactions are not able to redeem outputs
// of transactions created in the regular tree of the same block, which is
// important since the regular tree may be disapproved by the subsequent
// block while the stake tree must remain valid.
for i, stx := range block.STransactions() {
err := view.connectTransaction(stx, block.Height(), uint32(i), stxos)
if err != nil {
return err
}
}
for i, tx := range block.Transactions() {
err := view.connectTransaction(tx, block.Height(), uint32(i), stxos)
if err != nil {
return err
}
}
// Update the best hash for view to include this block since all of its
// transactions have been connected.
view.SetBestHash(block.Hash())
return nil
}
// disconnectBlock updates the view by disconnecting all transactions in both
// the regular and stake trees of the passed block, in the case the block
// disapproves the parent block, reconnecting all of the transactions in the
// regular tree of the previous block, and setting the best hash for the view to
// the parent block.
//
// Connecting a transaction entails marking all utxos it spends as spent, and
// adding all of the new utxos created by it.
//
// Disconnecting a transaction entails removing the utxos created by it and
// restoring the outputs spent by it with the help of the provided spent txo
// information.
//
// Note that, unlike block connection, the spent transaction output (stxo)
// information is required and failure to provide it will result in an assertion
// panic.
func (view *UtxoViewpoint) disconnectBlock(db database.DB, block, parent *dcrutil.Block, stxos []spentTxOut) error {
// Sanity check the correct number of stxos are provided.
if len(stxos) != countSpentOutputs(block) {
panicf("provided %v stxos for block %v (height %v) which spends %v "+
"outputs", len(stxos), block.Hash(), block.MsgBlock().Header.Height,
countSpentOutputs(block))
}
// Load all of the utxos referenced by the inputs for all transactions in
// the block don't already exist in the utxo view from the database.
err := view.fetchInputUtxos(db, block)
if err != nil {
return err
}
// Disconnect all of the transactions in both the regular and stake trees of
// the block. Notice that the regular tree is disconnected before the stake
// tree since that is the reverse of how they are connected.
err = view.disconnectRegularTransactions(block, stxos)
if err != nil {
return err
}
err = view.disconnectStakeTransactions(block, stxos)
if err != nil {
return err
}
// Reconnect the transactions in the regular tree of the parent block if the
// block that is being disconnected disapproves it.
if !headerApprovesParent(&block.MsgBlock().Header) {
// Load all of the utxos referenced by the inputs for all transactions
// in the regular tree of the parent block that don't already exist in
// the utxo view from the database.
err := view.fetchRegularInputUtxos(db, parent)
if err != nil {
return err
}
for i, tx := range parent.Transactions() {
err := view.connectTransaction(tx, parent.Height(), uint32(i), nil)
if err != nil {
return err
}
}
}
// Update the best hash for view to include this block since all of its
// transactions have been connected.
view.SetBestHash(&block.MsgBlock().Header.PrevBlock)
return nil
}
// Entries returns the underlying map that stores of all the utxo entries.
func (view *UtxoViewpoint) Entries() map[chainhash.Hash]*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 txHash, entry := range view.entries {
if entry == nil || (entry.modified && entry.IsFullySpent()) {
delete(view.entries, txHash)
continue
}
entry.modified = false
}
}
// viewFilteredSet represents a set of utxos to fetch from the database that are
// not already in a view.
type viewFilteredSet map[chainhash.Hash]struct{}
// add conditionally adds the provided utxo hash to the set if it does not
// already exist in the provided view.
func (set viewFilteredSet) add(view *UtxoViewpoint, hash *chainhash.Hash) {
if _, ok := view.entries[*hash]; !ok {
set[*hash] = struct{}{}
}
}
// fetchUtxosMain fetches unspent transaction output data about the provided
// set of transactions 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 transaction. Fully spent transactions, or those which otherwise
// don't exist, will result in a nil entry in the view.
func (view *UtxoViewpoint) fetchUtxosMain(db database.DB, filteredSet viewFilteredSet) error {
// Nothing to do if there are no requested hashes.
if len(filteredSet) == 0 {
return nil
}
// Load the unspent transaction output information for the requested set
// of transactions from the point of view of the end of the main chain.
//
// NOTE: Missing entries are not considered an error here and instead
// will result in nil entries in the view. This is intentionally done
// since other code uses the presence of an entry in the store as a way
// to optimize spend and unspend updates to apply only to the specific
// utxos that the caller needs access to.
return db.View(func(dbTx database.Tx) error {
for hash := range filteredSet {
hashCopy := hash
entry, err := dbFetchUtxoEntry(dbTx, &hashCopy)
if err != nil {
return err
}
view.entries[hash] = entry
}
return nil
})
}
// addRegularInputUtxos adds any outputs of transactions in the regular tree of
// the provided block that are referenced by inputs of transactions that are
// located later in the regular tree of the block and returns a set of the
// referenced outputs that are not already in the view and thus need to be
// fetched from the database.
func (view *UtxoViewpoint) addRegularInputUtxos(block *dcrutil.Block) viewFilteredSet {
// Build a map of in-flight transactions because some of the inputs in the
// regular transaction tree of this block could be referencing other
// transactions earlier in the block which are not yet in the chain.
txInFlight := map[chainhash.Hash]int{}
regularTxns := block.Transactions()
for i, tx := range regularTxns {
txInFlight[*tx.Hash()] = i
}
// Loop through all of the inputs of the transactions in the regular
// transaction tree (except for the coinbase which has no inputs) collecting
// them into sets of what is needed and what is already known (in-flight).
filteredSet := make(viewFilteredSet)
for i, tx := range regularTxns[1:] {
for _, txIn := range tx.MsgTx().TxIn {
// It is acceptable for a transaction input in the regular tree to
// reference the output of another transaction in the regular tree
// of this block only if the referenced transaction comes before the
// current one in this block. Add the outputs of the referenced
// transaction as available utxos when this is the case. Otherwise,
// the utxo details are still needed.
//
// NOTE: The >= is correct here because i is one less than the
// actual position of the transaction within the block due to
// skipping the coinbase.
originHash := &txIn.PreviousOutPoint.Hash
if inFlightIndex, ok := txInFlight[*originHash]; ok &&
i >= inFlightIndex {
originTx := regularTxns[inFlightIndex]
view.AddTxOuts(originTx, block.Height(), uint32(inFlightIndex))
continue
}
// Only request entries that are not already in the view from the
// database.
filteredSet.add(view, originHash)
}
}
return filteredSet
}
// fetchRegularInputUtxos loads utxo details about the input transactions
// referenced by the transactions in the regular tree of 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) fetchRegularInputUtxos(db database.DB, block *dcrutil.Block) error {
// Add any outputs of transactions in the regular tree of the block that are
// referenced by inputs of transactions that are located later in the tree
// and fetch any inputs that are not already in the view from the database.
filteredSet := view.addRegularInputUtxos(block)
return view.fetchUtxosMain(db, filteredSet)
}
// fetchInputUtxos loads utxo details about the input transactions referenced
// by the transactions in both the regular and stake trees of the given block
// into the view from the database as needed. In the case of regular tree,
// referenced entries that are earlier in the regular tree of the block are
// added to the view. In all cases, entries that are already in the view are
// not modified.
func (view *UtxoViewpoint) fetchInputUtxos(db database.DB, block *dcrutil.Block) error {
// Add any outputs of transactions in the regular tree of the block that are
// referenced by inputs of transactions that are located later in the tree
// and, while doing so, determine which inputs are not already in the view
// and thus need to be fetched from the database.
filteredSet := view.addRegularInputUtxos(block)
// Loop through all of the inputs of the transaction in the stake tree and
// add those that aren't already known to the set of what is needed.
//
// Note that, unlike in the regular transaction tree, transactions in the
// stake tree are not allowed to access outputs of transactions earlier in
// the block. This applies to both transactions earlier in the stake tree
// as well as those in the regular tree.
for _, stx := range block.STransactions() {
isVote := stake.IsSSGen(stx.MsgTx())
for txInIdx, txIn := range stx.MsgTx().TxIn {
// Ignore stakebase since it has no input.
if isVote && txInIdx == 0 {
continue
}
// Only request entries that are not already in the view
// from the database.
originHash := &txIn.PreviousOutPoint.Hash
filteredSet.add(view, originHash)
}
}
// Request the input utxos from the database.
return view.fetchUtxosMain(db, filteredSet)
}
// clone returns a deep copy of the view.
func (view *UtxoViewpoint) clone() *UtxoViewpoint {
clonedView := &UtxoViewpoint{
entries: make(map[chainhash.Hash]*UtxoEntry),
bestHash: view.bestHash,
}
for txHash, entry := range view.entries {
clonedView.entries[txHash] = entry.Clone()
}
return clonedView
}
// NewUtxoViewpoint returns a new empty unspent transaction output view.
func NewUtxoViewpoint() *UtxoViewpoint {
return &UtxoViewpoint{
entries: make(map[chainhash.Hash]*UtxoEntry),
}
}
// FetchUtxoView loads utxo details about the input transactions referenced by
// the passed transaction from the point of view of the end of the main chain
// while taking into account whether or not the transactions in the regular tree
// of the block just prior should be included or not depending on the provided
// flag. It also attempts to fetch the utxo details for the transaction itself
// so the returned view can be examined for duplicate unspent transaction
// outputs.
//
// This function is safe for concurrent access however the returned view is NOT.
func (b *BlockChain) FetchUtxoView(tx *dcrutil.Tx, includePrevRegularTxns bool) (*UtxoViewpoint, error) {
b.chainLock.RLock()
defer b.chainLock.RUnlock()
// The genesis block does not have any spendable transactions, so there
// can't possibly be any details about it. This is also necessary
// because the code below requires the parent block and the genesis
// block doesn't have one.
tip := b.bestChain.Tip()
view := NewUtxoViewpoint()
view.SetBestHash(&tip.hash)
if tip.height == 0 {
return view, nil
}
// Disconnect the transactions in the regular tree of the parent block if
// the caller requests it. In order to avoid the overhead of repeated
// lookups, only create a view with the changes once and cache it.
if !includePrevRegularTxns {
b.disapprovedViewLock.Lock()
if b.disapprovedView == nil || *b.disapprovedView.BestHash() !=
tip.hash {
// Grab the parent of the current block.
parent, err := b.fetchMainChainBlockByNode(tip.parent)
if err != nil {
b.disapprovedViewLock.Unlock()
return nil, err
}
// Disconnect the transactions in the regular tree of the parent
// block.
err = view.disconnectDisapprovedBlock(b.db, parent)
if err != nil {
b.disapprovedViewLock.Unlock()
return nil, err
}
// Clone the view so the caller can safely mutate it.
b.disapprovedView = view.clone()
} else {
// Clone the view so the caller can safely mutate it.
view = b.disapprovedView.clone()
}
b.disapprovedViewLock.Unlock()
}
// Create a set of needed transactions based on those referenced by the
// inputs of the passed transaction. Also, add the passed transaction
// itself as a way for the caller to detect duplicates that are not fully
// spent.
filteredSet := make(viewFilteredSet)
filteredSet.add(view, tx.Hash())
msgTx := tx.MsgTx()
if !standalone.IsCoinBaseTx(msgTx) {
isVote := stake.IsSSGen(msgTx)
for txInIdx, txIn := range msgTx.TxIn {
// Ignore stakebase since it has no input.
if isVote && txInIdx == 0 {
continue
}
filteredSet.add(view, &txIn.PreviousOutPoint.Hash)
}
}
err := view.fetchUtxosMain(b.db, filteredSet)
return view, err
}
// FetchUtxoEntry loads and returns the unspent transaction output entry for the
// passed hash from the point of view of the end of the main chain.
//
// NOTE: Requesting a hash 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 fully spent transactions. 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 (b *BlockChain) FetchUtxoEntry(txHash *chainhash.Hash) (*UtxoEntry, error) {
b.chainLock.RLock()
defer b.chainLock.RUnlock()
var entry *UtxoEntry
err := b.db.View(func(dbTx database.Tx) error {
var err error
entry, err = dbFetchUtxoEntry(dbTx, txHash)
return err
})
if err != nil {
return nil, err
}
return entry, nil
}