decred.org/dcrdex@v1.0.5/client/asset/dcr/externaltx.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package dcr
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"sync"
    10  	"time"
    11  
    12  	"decred.org/dcrdex/client/asset"
    13  	"github.com/decred/dcrd/chaincfg/chainhash"
    14  	"github.com/decred/dcrd/wire"
    15  )
    16  
    17  type externalTx struct {
    18  	hash *chainhash.Hash
    19  
    20  	// blockMtx protects access to the fields below it, which
    21  	// are set when the tx's block is found and cleared when
    22  	// the previously found tx block is orphaned.
    23  	blockMtx         sync.RWMutex
    24  	lastScannedBlock *chainhash.Hash
    25  	block            *block
    26  	tree             int8
    27  	outputSpenders   []*outputSpenderFinder
    28  }
    29  
    30  type outputSpenderFinder struct {
    31  	*wire.TxOut
    32  	op   outPoint
    33  	tree int8
    34  
    35  	spenderMtx       sync.RWMutex
    36  	lastScannedBlock *chainhash.Hash
    37  	spenderBlock     *block
    38  	spenderTx        *wire.MsgTx
    39  }
    40  
    41  // lookupTxOutWithBlockFilters returns confirmations and spend status of the
    42  // requested output. If the block containing the output is not yet known, a
    43  // a block filters scan is conducted to determine if the output is mined in a
    44  // block between the current best block and the block just before the provided
    45  // earliestTxTime. Returns asset.CoinNotFoundError if the block containing the
    46  // output is not found.
    47  func (dcr *ExchangeWallet) lookupTxOutWithBlockFilters(ctx context.Context, op outPoint, pkScript []byte, earliestTxTime time.Time) (uint32, bool, error) {
    48  	if len(pkScript) == 0 {
    49  		return 0, false, fmt.Errorf("cannot perform block filters lookup without a script")
    50  	}
    51  
    52  	output, outputBlock, err := dcr.externalTxOutput(ctx, op, pkScript, earliestTxTime)
    53  	if err != nil {
    54  		return 0, false, err // may be asset.CoinNotFoundError
    55  	}
    56  
    57  	spent, err := dcr.isOutputSpent(ctx, output)
    58  	if err != nil {
    59  		return 0, false, fmt.Errorf("error checking if output %s is spent: %w", op, err)
    60  	}
    61  
    62  	// Get the current tip height to calculate confirmations.
    63  	tip, err := dcr.getBestBlock(ctx)
    64  	if err != nil {
    65  		dcr.log.Errorf("getbestblock error %v", err)
    66  		tip = dcr.cachedBestBlock()
    67  	}
    68  	var confs uint32
    69  	if tip.height >= outputBlock.height { // slight possibility that the cached tip height is behind the output's block height
    70  		confs = uint32(tip.height + 1 - outputBlock.height)
    71  	}
    72  	return confs, spent, nil
    73  }
    74  
    75  // externalTxOutput attempts to locate the requested tx output in a mainchain
    76  // block and if found, returns the output details along with the block details.
    77  func (dcr *ExchangeWallet) externalTxOutput(ctx context.Context, op outPoint, pkScript []byte, earliestTxTime time.Time) (*outputSpenderFinder, *block, error) {
    78  	dcr.externalTxMtx.Lock()
    79  	tx := dcr.externalTxCache[op.txHash]
    80  	if tx == nil {
    81  		tx = &externalTx{hash: &op.txHash}
    82  		dcr.externalTxCache[op.txHash] = tx // never deleted (TODO)
    83  	}
    84  	dcr.externalTxMtx.Unlock()
    85  
    86  	// Hold the tx.blockMtx lock for 2 reasons:
    87  	// 1) To read/write the tx.block, tx.tree and tx.outputSpenders fields.
    88  	// 2) To prevent duplicate tx block scans if this tx block is not already
    89  	//    known. Holding this lock now ensures that any ongoing scan completes
    90  	//    before we try to access the tx.block field which may prevent
    91  	//    unnecessary rescan.
    92  	tx.blockMtx.Lock()
    93  	defer tx.blockMtx.Unlock()
    94  
    95  	// First check if the tx block is cached.
    96  	txBlock, err := dcr.txBlockFromCache(ctx, tx)
    97  	if err != nil {
    98  		return nil, nil, fmt.Errorf("error checking if tx %s is known to be mined: %w", tx.hash, err)
    99  	}
   100  
   101  	// Scan block filters to find the tx block if it is yet unknown.
   102  	if txBlock == nil {
   103  		dcr.log.Tracef("Output %s:%d NOT yet found; now searching with block filters.", op.txHash, op.vout)
   104  		txBlock, err = dcr.scanFiltersForTxBlock(ctx, tx, [][]byte{pkScript}, earliestTxTime)
   105  		if err != nil {
   106  			return nil, nil, fmt.Errorf("error checking if tx %s is mined: %w", tx.hash, err)
   107  		}
   108  		if txBlock == nil {
   109  			return nil, nil, asset.CoinNotFoundError
   110  		}
   111  	}
   112  
   113  	if len(tx.outputSpenders) <= int(op.vout) {
   114  		return nil, nil, fmt.Errorf("tx %s does not have an output at index %d", tx.hash, op.vout)
   115  	}
   116  	return tx.outputSpenders[op.vout], txBlock, nil
   117  }
   118  
   119  // txBlockFromCache returns the block containing this tx if it's known and
   120  // still part of the mainchain. It is not an error if the block is unknown
   121  // or invalidated (must check for a nil *block).
   122  // The tx.blockMtx MUST be locked for writing.
   123  func (dcr *ExchangeWallet) txBlockFromCache(ctx context.Context, tx *externalTx) (*block, error) {
   124  	if tx.block == nil {
   125  		return nil, nil
   126  	}
   127  
   128  	_, _, txBlockStillValid, err := dcr.blockHeader(ctx, tx.block.hash)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  
   133  	if txBlockStillValid { // both mainchain and not disapproved
   134  		// dcr.log.Tracef("Cached tx %s is mined in block %d (%s).", tx.hash, tx.block.height, tx.block.hash)
   135  		return tx.block, nil
   136  	}
   137  
   138  	// Tx block was previously set but seems to have been invalidated.
   139  	// Clear the tx tree, outputs and block info fields that must have
   140  	// been previously set.
   141  	dcr.log.Warnf("Block %s previously found to contain tx %s "+
   142  		"has been orphaned or disapproved by stakeholders.", tx.block.hash, tx.hash)
   143  	tx.block = nil
   144  	tx.tree = -1
   145  	tx.outputSpenders = nil
   146  	return nil, nil
   147  }
   148  
   149  // scanFiltersForTxBlock attempts to find the block containing the provided tx
   150  // by scanning block filters from the current best block down to the block just
   151  // before earliestTxTime or the block that was last scanned, if there was a
   152  // previous scan. If the tx block is found, the block hash, height and the tx
   153  // outputs details are cached; and the block is returned.
   154  // The tx.blockMtx MUST be locked for writing.
   155  func (dcr *ExchangeWallet) scanFiltersForTxBlock(ctx context.Context, tx *externalTx, txScripts [][]byte, earliestTxTime time.Time) (*block, error) {
   156  	// Scan block filters in reverse from the current best block to the last
   157  	// scanned block. If the last scanned block has been re-orged out of the
   158  	// mainchain, scan back to the mainchain ancestor of the lastScannedBlock.
   159  	var lastScannedBlock *block
   160  	if tx.lastScannedBlock != nil {
   161  		stopBlockHash, stopBlockHeight, err := dcr.mainchainAncestor(ctx, tx.lastScannedBlock)
   162  		if err != nil {
   163  			return nil, fmt.Errorf("error looking up mainchain ancestor for block %s", err)
   164  		}
   165  		tx.lastScannedBlock = stopBlockHash
   166  		lastScannedBlock = &block{hash: stopBlockHash, height: stopBlockHeight}
   167  	}
   168  
   169  	// Run cfilters scan in reverse from best block to lastScannedBlock or
   170  	// to block just before earliestTxTime.
   171  	currentTip := dcr.cachedBestBlock()
   172  	if lastScannedBlock == nil {
   173  		dcr.log.Debugf("Searching for tx %s in blocks between best block %d (%s) and the block just before %s.",
   174  			tx.hash, currentTip.height, currentTip.hash, earliestTxTime)
   175  	} else if lastScannedBlock.height < currentTip.height {
   176  		dcr.log.Debugf("Searching for tx %s in blocks %d (%s) to %d (%s).", tx.hash,
   177  			lastScannedBlock.height, lastScannedBlock.hash, currentTip.height, currentTip.hash)
   178  	} else {
   179  		if lastScannedBlock.height > currentTip.height {
   180  			dcr.log.Warnf("Previous cfilters look up for tx %s stopped at block %d but current tip is %d?",
   181  				tx.hash, lastScannedBlock.height, currentTip.height)
   182  		}
   183  		return nil, nil // no new blocks to scan
   184  	}
   185  
   186  	iHash := currentTip.hash
   187  	iHeight := currentTip.height
   188  
   189  	// Set the current tip as the last scanned block so subsequent
   190  	// scans cover the latest tip back to this current tip.
   191  	scanCompletedWithoutResults := func() (*block, error) {
   192  		tx.lastScannedBlock = currentTip.hash
   193  		dcr.log.Debugf("Tx %s NOT found in blocks %d (%s) to %d (%s).", tx.hash,
   194  			iHeight, iHash, currentTip.height, currentTip.hash)
   195  		return nil, nil
   196  	}
   197  
   198  	earliestTxStamp := earliestTxTime.Unix()
   199  	for {
   200  		msgTx, outputSpenders, err := dcr.findTxInBlock(ctx, *tx.hash, txScripts, iHash)
   201  		if err != nil {
   202  			return nil, err
   203  		}
   204  
   205  		if msgTx != nil {
   206  			tx.block = &block{hash: iHash, height: iHeight}
   207  			tx.tree = determineTxTree(msgTx)
   208  			tx.outputSpenders = outputSpenders
   209  			return tx.block, nil
   210  		}
   211  
   212  		// Block does not include the tx, check the previous block.
   213  		// Abort the search if we've scanned blocks from the tip back to the
   214  		// block we scanned last or the block just before earliestTxTime.
   215  		if iHeight == 0 {
   216  			return scanCompletedWithoutResults()
   217  		}
   218  		if lastScannedBlock != nil && iHeight <= lastScannedBlock.height {
   219  			return scanCompletedWithoutResults()
   220  		}
   221  		iBlock, err := dcr.wallet.GetBlockHeader(dcr.ctx, iHash)
   222  		if err != nil {
   223  			return nil, fmt.Errorf("getblockheader error for block %s: %w", iHash, translateRPCCancelErr(err))
   224  		}
   225  		if iBlock.Timestamp.Unix() <= earliestTxStamp {
   226  			return scanCompletedWithoutResults()
   227  		}
   228  
   229  		iHeight--
   230  		iHash = &iBlock.PrevBlock
   231  		continue
   232  	}
   233  }
   234  
   235  func (dcr *ExchangeWallet) findTxInBlock(ctx context.Context, txHash chainhash.Hash, txScripts [][]byte, blockHash *chainhash.Hash) (*wire.MsgTx, []*outputSpenderFinder, error) {
   236  	bingo, err := dcr.wallet.MatchAnyScript(ctx, blockHash, txScripts)
   237  	if err != nil {
   238  		return nil, nil, err
   239  	}
   240  	if !bingo {
   241  		return nil, nil, nil
   242  	}
   243  
   244  	blk, err := dcr.wallet.GetBlock(ctx, blockHash)
   245  	if err != nil {
   246  		return nil, nil, fmt.Errorf("error retrieving block %s: %w", blockHash, err)
   247  	}
   248  
   249  	var msgTx *wire.MsgTx
   250  	for _, tx := range append(blk.Transactions, blk.STransactions...) {
   251  		if tx.TxHash() == txHash {
   252  			dcr.log.Debugf("Found mined tx %s in block %s.", txHash, blk.BlockHash())
   253  			msgTx = tx
   254  			break
   255  		}
   256  	}
   257  
   258  	if msgTx == nil {
   259  		dcr.log.Debugf("Block %s filters matched scripts for tx %s but does NOT contain the tx.", blk.BlockHash(), txHash)
   260  		return nil, nil, nil
   261  	}
   262  
   263  	// We have the txs in this block, check if any them spends an output
   264  	// from the original tx.
   265  	outputSpenders := make([]*outputSpenderFinder, len(msgTx.TxOut))
   266  	for i, txOut := range msgTx.TxOut {
   267  		outputSpenders[i] = &outputSpenderFinder{
   268  			TxOut:            txOut,
   269  			op:               newOutPoint(&txHash, uint32(i)),
   270  			tree:             determineTxTree(msgTx),
   271  			lastScannedBlock: blockHash,
   272  		}
   273  	}
   274  	for _, tx := range append(blk.Transactions, blk.STransactions...) {
   275  		if tx.TxHash() == txHash {
   276  			continue // original tx, ignore
   277  		}
   278  		for _, txIn := range tx.TxIn {
   279  			if txIn.PreviousOutPoint.Hash == txHash { // found a spender
   280  				outputSpenders[txIn.PreviousOutPoint.Index].spenderBlock = &block{int64(blk.Header.Height), blockHash}
   281  				outputSpenders[txIn.PreviousOutPoint.Index].spenderTx = tx
   282  			}
   283  		}
   284  	}
   285  
   286  	return msgTx, outputSpenders, nil
   287  }
   288  
   289  func (dcr *ExchangeWallet) isOutputSpent(ctx context.Context, output *outputSpenderFinder) (bool, error) {
   290  	// Hold the output.spenderMtx lock for 2 reasons:
   291  	// 1) To read (and set) the spenderBlock field.
   292  	// 2) To prevent duplicate spender block scans if the spenderBlock is not
   293  	//    already known. Holding this lock now ensures that any ongoing scan
   294  	//    completes before we try to access the output.spenderBlock field
   295  	//    which may prevent unnecessary rescan.
   296  	output.spenderMtx.Lock()
   297  	defer output.spenderMtx.Unlock()
   298  
   299  	// Check if this output is known to be spent in a mainchain block.
   300  	if output.spenderBlock != nil {
   301  		_, _, spenderBlockStillValid, err := dcr.blockHeader(ctx, output.spenderBlock.hash)
   302  		if err != nil {
   303  			return false, err
   304  		}
   305  		if spenderBlockStillValid { // both mainchain and not disapproved
   306  			// dcr.log.Debugf("Found cached information for the spender of %s.", output.op)
   307  			return true, nil
   308  		}
   309  		// Output was previously found to have been spent but the block
   310  		// containing the spending tx seems to have been invalidated.
   311  		dcr.log.Warnf("Block %s previously found to contain spender of output %s "+
   312  			"has been orphaned or disapproved by stakeholders.",
   313  			output.spenderBlock.hash, output.op)
   314  		output.spenderBlock = nil
   315  		output.spenderTx = nil
   316  	}
   317  
   318  	// This tx output is not known to be spent as of last search (if any).
   319  	// Scan block filters starting from the block after the tx block or the
   320  	// lastScannedBlock (if there was a previous scan). Use mainchainAncestor
   321  	// to ensure that scanning starts from a mainchain block in the event that
   322  	// the lastScannedBlock have been re-orged out of the mainchain. We already
   323  	// checked that the txBlock is not invalidated above.
   324  	_, lastScannedHeight, err := dcr.mainchainAncestor(ctx, output.lastScannedBlock)
   325  	if err != nil {
   326  		return false, err
   327  	}
   328  	nextScanHeight := lastScannedHeight + 1
   329  
   330  	bestBlock := dcr.cachedBestBlock()
   331  	if lastScannedHeight == bestBlock.height {
   332  		// This is fine. No more blocks to scan
   333  		return false, nil
   334  	}
   335  	if lastScannedHeight > bestBlock.height {
   336  		// This is not fine, how did it scan a height above the best
   337  		// height? Log a warning, should never happen anyways.
   338  		dcr.log.Warnf("Attempted to look for output spender in block %d but current tip is %d!",
   339  			nextScanHeight, bestBlock.height)
   340  		// Return too, since there're obviously no more blocks to scan.
   341  		return false, nil
   342  	}
   343  
   344  	// Search for this output's spender in the blocks between startBlock and
   345  	// the current best block.
   346  	nextScanHash, err := dcr.wallet.GetBlockHash(ctx, nextScanHeight)
   347  	if err != nil {
   348  		return false, err
   349  	}
   350  	spenderTx, stopBlockHash, stopBlockHeight, err := dcr.findTxOutSpender(ctx, output.op, output.PkScript, &block{nextScanHeight, nextScanHash})
   351  	if stopBlockHash != nil { // might be nil if the search never scanned a block
   352  		output.lastScannedBlock = stopBlockHash
   353  	}
   354  	if err != nil {
   355  		return false, err
   356  	}
   357  
   358  	// Cache relevant spender info if the spender is found.
   359  	if spenderTx == nil {
   360  		return false, nil
   361  	}
   362  
   363  	output.spenderBlock = &block{hash: stopBlockHash, height: stopBlockHeight}
   364  	output.spenderTx = spenderTx
   365  	return true, nil
   366  }
   367  
   368  // findTxOutSpender attempts to find and return the tx that spends the provided
   369  // output by matching the provided outputPkScript against the block filters of
   370  // the mainchain blocks between the provided startBlock and the current best
   371  // block.
   372  // If no tx is found to spend the provided output, the hash of the block that
   373  // was last checked is returned along with any error that may have occurred
   374  // during the search.
   375  func (dcr *ExchangeWallet) findTxOutSpender(ctx context.Context, op outPoint, outputPkScript []byte, startBlock *block) (*wire.MsgTx, *chainhash.Hash, int64, error) {
   376  	var lastScannedHash *chainhash.Hash
   377  	var lastScannedHeight int64
   378  
   379  	iHeight := startBlock.height
   380  	iHash := startBlock.hash
   381  	bestBlock := dcr.cachedBestBlock()
   382  	for {
   383  		bingo, err := dcr.wallet.MatchAnyScript(ctx, iHash, [][]byte{outputPkScript})
   384  		if err != nil {
   385  			return nil, lastScannedHash, lastScannedHeight, err
   386  		}
   387  
   388  		if bingo {
   389  			dcr.log.Debugf("Output %s is likely spent in block %d (%s). Confirming.",
   390  				op, iHeight, iHash)
   391  			blk, err := dcr.wallet.GetBlock(ctx, iHash)
   392  			if err != nil {
   393  				return nil, lastScannedHash, lastScannedHeight, fmt.Errorf("error retrieving block %s: %w", iHash, err)
   394  			}
   395  			for _, tx := range append(blk.Transactions, blk.STransactions...) {
   396  				if txSpendsOutput(tx, op) {
   397  					dcr.log.Debugf("Found spender for output %s in block %d (%s), spender tx hash %s.",
   398  						op, iHeight, iHash, tx.TxHash())
   399  					return tx, iHash, iHeight, nil
   400  				}
   401  			}
   402  			dcr.log.Debugf("Output %s is NOT spent in block %d (%s).", op, iHeight, iHash)
   403  		}
   404  
   405  		lastScannedHeight = iHeight
   406  		lastScannedHash = iHash
   407  
   408  		if iHeight >= bestBlock.height { // reached the tip, stop searching
   409  			break
   410  		}
   411  
   412  		// Block does not include the output spender, check the next block.
   413  		iHeight++
   414  		nextHash, err := dcr.wallet.GetBlockHash(ctx, iHeight)
   415  		if err != nil {
   416  			return nil, lastScannedHash, lastScannedHeight, translateRPCCancelErr(err)
   417  		}
   418  		iHash = nextHash
   419  	}
   420  
   421  	dcr.log.Debugf("Output %s is NOT spent in blocks %d (%s) to %d (%s).",
   422  		op, startBlock.height, startBlock.hash, bestBlock.height, bestBlock.hash)
   423  	return nil, lastScannedHash, lastScannedHeight, nil // scanned up to best block, no spender found
   424  }
   425  
   426  // txSpendsOutput returns true if the passed tx has an input that spends the
   427  // specified output.
   428  func txSpendsOutput(tx *wire.MsgTx, op outPoint) bool {
   429  	if tx.TxHash() == op.txHash {
   430  		return false // no need to check inputs if this tx is the same tx that pays to the specified op
   431  	}
   432  	for _, txIn := range tx.TxIn {
   433  		prevOut := &txIn.PreviousOutPoint
   434  		if prevOut.Index == op.vout && prevOut.Hash == op.txHash {
   435  			return true // found spender
   436  		}
   437  	}
   438  	return false
   439  }