decred.org/dcrdex@v1.0.5/client/asset/btc/spv_test.go (about)

     1  //go:build !spvlive && !harness
     2  
     3  // This code is available on the terms of the project LICENSE.md file,
     4  // also available online at https://blueoakcouncil.org/license/1.0.0.
     5  
     6  package btc
     7  
     8  import (
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"testing"
    13  	"time"
    14  
    15  	"decred.org/dcrdex/client/asset"
    16  	"decred.org/dcrdex/dex"
    17  	"decred.org/dcrdex/dex/encode"
    18  	"github.com/btcsuite/btcd/btcec/v2"
    19  	"github.com/btcsuite/btcd/btcjson"
    20  	"github.com/btcsuite/btcd/btcutil"
    21  	"github.com/btcsuite/btcd/btcutil/gcs"
    22  	"github.com/btcsuite/btcd/btcutil/gcs/builder"
    23  	"github.com/btcsuite/btcd/btcutil/psbt"
    24  	"github.com/btcsuite/btcd/chaincfg"
    25  	"github.com/btcsuite/btcd/chaincfg/chainhash"
    26  	"github.com/btcsuite/btcd/peer"
    27  	"github.com/btcsuite/btcd/txscript"
    28  	"github.com/btcsuite/btcd/wire"
    29  	"github.com/btcsuite/btcwallet/chain"
    30  	"github.com/btcsuite/btcwallet/waddrmgr"
    31  	"github.com/btcsuite/btcwallet/wallet"
    32  	"github.com/btcsuite/btcwallet/walletdb"
    33  	"github.com/btcsuite/btcwallet/wtxmgr"
    34  	"github.com/lightninglabs/neutrino"
    35  	"github.com/lightninglabs/neutrino/headerfs"
    36  )
    37  
    38  type tBtcWallet struct {
    39  	*testData
    40  }
    41  
    42  func (c *tBtcWallet) ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTransactionsResult, error) {
    43  	return nil, nil
    44  }
    45  
    46  func (c *tBtcWallet) PublishTransaction(tx *wire.MsgTx, label string) error {
    47  	c.sentRawTx = tx
    48  	if c.sendErr != nil {
    49  		return c.sendErr
    50  	}
    51  	if c.sendToAddressErr != nil {
    52  		return c.sendToAddressErr
    53  	}
    54  	if c.badSendHash != nil {
    55  		return errors.New("bad hash")
    56  	}
    57  	return c.sendErr
    58  }
    59  
    60  func (c *tBtcWallet) CalculateAccountBalances(account uint32, confirms int32) (wallet.Balances, error) {
    61  	if c.getBalancesErr != nil {
    62  		return wallet.Balances{}, c.getBalancesErr
    63  	}
    64  	bal := &c.getBalances.Mine
    65  	mustAmount := func(v float64) btcutil.Amount {
    66  		amt, _ := btcutil.NewAmount(v)
    67  		return amt
    68  	}
    69  	return wallet.Balances{
    70  		Total:          mustAmount(bal.Trusted + bal.Untrusted),
    71  		Spendable:      mustAmount(bal.Trusted + bal.Untrusted),
    72  		ImmatureReward: mustAmount(bal.Immature),
    73  	}, nil
    74  }
    75  
    76  func (c *tBtcWallet) ListUnspent(minconf, maxconf int32, acctName string) ([]*btcjson.ListUnspentResult, error) {
    77  	if c.listUnspentErr != nil {
    78  		return nil, c.listUnspentErr
    79  	}
    80  	unspents := make([]*btcjson.ListUnspentResult, 0, len(c.listUnspent))
    81  	for _, u := range c.listUnspent {
    82  		unspents = append(unspents, &btcjson.ListUnspentResult{
    83  			TxID:    u.TxID,
    84  			Vout:    u.Vout,
    85  			Address: u.Address,
    86  			// Account: ,
    87  			ScriptPubKey:  u.ScriptPubKey.String(),
    88  			RedeemScript:  u.RedeemScript.String(),
    89  			Amount:        u.Amount,
    90  			Confirmations: int64(u.Confirmations),
    91  			Spendable:     u.Spendable,
    92  		})
    93  	}
    94  
    95  	return unspents, nil
    96  }
    97  
    98  func (c *tBtcWallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error) {
    99  	return c.fetchInputInfoTx, nil, nil, 0, nil
   100  }
   101  
   102  func (c *tBtcWallet) ResetLockedOutpoints() {}
   103  
   104  func (c *tBtcWallet) LockOutpoint(op wire.OutPoint) {
   105  	if c.lockedCoins != nil {
   106  		// check if already locked
   107  		for _, l := range c.lockedCoins {
   108  			if l.TxID == op.Hash.String() && l.Vout == op.Index {
   109  				return
   110  			}
   111  		}
   112  
   113  		c.lockedCoins = append(c.lockedCoins, &RPCOutpoint{
   114  			TxID: op.Hash.String(),
   115  			Vout: op.Index,
   116  		})
   117  		return
   118  	}
   119  
   120  	c.lockedCoins = []*RPCOutpoint{{
   121  		TxID: op.Hash.String(),
   122  		Vout: op.Index,
   123  	}}
   124  }
   125  
   126  func (c *tBtcWallet) UnlockOutpoint(op wire.OutPoint) {}
   127  
   128  func (c *tBtcWallet) LockedOutpoints() []btcjson.TransactionInput {
   129  	unspents := make([]btcjson.TransactionInput, 0)
   130  	for _, u := range c.listLockUnspent {
   131  		unspents = append(unspents, btcjson.TransactionInput{
   132  			Txid: u.TxID,
   133  			Vout: u.Vout,
   134  		})
   135  	}
   136  
   137  	return unspents
   138  }
   139  
   140  func (c *tBtcWallet) NewChangeAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) {
   141  	if c.changeAddrErr != nil {
   142  		return nil, c.changeAddrErr
   143  	}
   144  	return btcutil.DecodeAddress(c.changeAddr, &chaincfg.MainNetParams)
   145  }
   146  
   147  func (c *tBtcWallet) NewAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) {
   148  	if c.newAddressErr != nil {
   149  		return nil, c.newAddressErr
   150  	}
   151  	return btcutil.DecodeAddress(c.newAddress, &chaincfg.MainNetParams)
   152  }
   153  
   154  func (c *tBtcWallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, additionalPrevScriptsadditionalPrevScripts map[wire.OutPoint][]byte,
   155  	additionalKeysByAddress map[string]*btcutil.WIF, p2shRedeemScriptsByAddress map[string][]byte) ([]wallet.SignatureError, error) {
   156  
   157  	if c.signTxErr != nil {
   158  		return nil, c.signTxErr
   159  	}
   160  	c.signFunc(tx)
   161  	if c.sigIncomplete {
   162  		return []wallet.SignatureError{{Error: errors.New("tBtcWallet SignTransaction error")}}, nil
   163  	}
   164  	return nil, nil
   165  }
   166  
   167  func (c *tBtcWallet) PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) {
   168  	if c.privKeyForAddrErr != nil {
   169  		return nil, c.privKeyForAddrErr
   170  	}
   171  	return c.privKeyForAddr.PrivKey, nil
   172  }
   173  
   174  func (c *tBtcWallet) Database() walletdb.DB {
   175  	return nil
   176  }
   177  
   178  func (c *tBtcWallet) Unlock(passphrase []byte, lock <-chan time.Time) error {
   179  	return c.unlockErr
   180  }
   181  
   182  func (c *tBtcWallet) Lock() {}
   183  
   184  func (c *tBtcWallet) Locked() bool {
   185  	return c.locked
   186  }
   187  
   188  func (c *tBtcWallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
   189  	account uint32, minconf int32, satPerKb btcutil.Amount,
   190  	coinSelectionStrategy wallet.CoinSelectionStrategy, label string) (*wire.MsgTx, error) {
   191  	if c.sendToAddressErr != nil {
   192  		return nil, c.sendToAddressErr
   193  	}
   194  	tx := wire.NewMsgTx(wire.TxVersion)
   195  	for _, txOut := range outputs {
   196  		tx.AddTxOut(txOut)
   197  	}
   198  	tx.AddTxIn(dummyInput())
   199  
   200  	return tx, nil
   201  }
   202  
   203  func (c *tBtcWallet) HaveAddress(a btcutil.Address) (bool, error) {
   204  	if c.ownedAddresses != nil && c.ownedAddresses[a.String()] {
   205  		return true, nil
   206  	}
   207  	return c.ownsAddress, nil
   208  }
   209  
   210  func (c *tBtcWallet) Stop() {}
   211  
   212  func (c *tBtcWallet) Start() (SPVService, error) {
   213  	return nil, nil
   214  }
   215  
   216  func (c *tBtcWallet) WaitForShutdown() {}
   217  
   218  func (c *tBtcWallet) ChainSynced() bool {
   219  	c.blockchainMtx.RLock()
   220  	defer c.blockchainMtx.RUnlock()
   221  	if c.getBlockchainInfo == nil {
   222  		return false
   223  	}
   224  	return c.getBlockchainInfo.Blocks >= c.getBlockchainInfo.Headers // -1 ok for chain sync ?
   225  }
   226  
   227  func (c *tBtcWallet) SynchronizeRPC(chainClient chain.Interface) {}
   228  
   229  func (c *tBtcWallet) WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) {
   230  	if c.getTransactionErr != nil {
   231  		return nil, c.getTransactionErr
   232  	}
   233  	var txData *GetTransactionResult
   234  	if c.getTransactionMap != nil {
   235  		if txData = c.getTransactionMap["any"]; txData == nil {
   236  			txData = c.getTransactionMap[txHash.String()]
   237  		}
   238  	}
   239  	if txData == nil {
   240  		return nil, WalletTransactionNotFound
   241  	}
   242  
   243  	tx, _ := msgTxFromBytes(txData.Bytes)
   244  	blockHash, _ := chainhash.NewHashFromStr(txData.BlockHash)
   245  
   246  	blk := c.getBlock(*blockHash)
   247  	var blockHeight int32
   248  	if blk != nil {
   249  		blockHeight = int32(blk.height)
   250  	} else {
   251  		blockHeight = -1
   252  	}
   253  
   254  	credits := make([]wtxmgr.CreditRecord, 0, len(tx.TxIn))
   255  	debits := make([]wtxmgr.DebitRecord, 0, len(tx.TxIn))
   256  	for i, in := range tx.TxIn {
   257  		credits = append(credits, wtxmgr.CreditRecord{
   258  			// Amount:,
   259  			Index: uint32(i),
   260  			Spent: c.walletTxSpent,
   261  			// Change: ,
   262  		})
   263  
   264  		var debitAmount int64
   265  		// The sources of transaction inputs all need to be added to getTransactionMap
   266  		// in order to get accurate Fees and Amounts when calling GetWalletTransaction
   267  		// when using the SPV wallet.
   268  		if gtr := c.getTransactionMap[in.PreviousOutPoint.Hash.String()]; gtr != nil {
   269  			tx, _ := msgTxFromBytes(gtr.Bytes)
   270  			debitAmount = tx.TxOut[in.PreviousOutPoint.Index].Value
   271  		}
   272  
   273  		debits = append(debits, wtxmgr.DebitRecord{
   274  			Amount: btcutil.Amount(debitAmount),
   275  		})
   276  
   277  	}
   278  	return &wtxmgr.TxDetails{
   279  		TxRecord: wtxmgr.TxRecord{
   280  			MsgTx:    *tx,
   281  			Received: time.Unix(int64(txData.Time), 0),
   282  		},
   283  		Block: wtxmgr.BlockMeta{
   284  			Block: wtxmgr.Block{
   285  				Hash:   *blockHash,
   286  				Height: blockHeight,
   287  			},
   288  		},
   289  		Credits: credits,
   290  		Debits:  debits,
   291  	}, nil
   292  }
   293  
   294  func (c *tBtcWallet) SyncedTo() waddrmgr.BlockStamp {
   295  	bestHash, bestHeight := c.bestBlock() // NOTE: in reality this may be lower than the chain service's best block
   296  	blk := c.getBlock(*bestHash)
   297  	return waddrmgr.BlockStamp{
   298  		Height:    int32(bestHeight),
   299  		Hash:      *bestHash,
   300  		Timestamp: blk.msgBlock.Header.Timestamp,
   301  	}
   302  }
   303  
   304  func (c *tBtcWallet) SignTx(tx *wire.MsgTx) error {
   305  	if c.signTxErr != nil {
   306  		return c.signTxErr
   307  	}
   308  	c.signFunc(tx)
   309  	if c.sigIncomplete {
   310  		return errors.New("tBtcWallet SignTransaction error")
   311  	}
   312  	return nil
   313  }
   314  
   315  func (c *tBtcWallet) AccountProperties(scope waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error) {
   316  	return nil, nil
   317  }
   318  
   319  func (c *tBtcWallet) BlockNotifications(ctx context.Context) <-chan *BlockNotification {
   320  	return nil
   321  }
   322  
   323  func (c *tBtcWallet) ForceRescan() {}
   324  
   325  func (c *tBtcWallet) RescanAsync() error { return nil }
   326  
   327  func (c *tBtcWallet) Birthday() time.Time {
   328  	return time.Time{}
   329  }
   330  
   331  func (c *tBtcWallet) Reconfigure(*asset.WalletConfig, string) (bool, error) {
   332  	return false, nil
   333  }
   334  
   335  func (c *tBtcWallet) Peers() ([]*asset.WalletPeer, error) {
   336  	return nil, nil
   337  }
   338  func (c *tBtcWallet) AddPeer(string) error {
   339  	return nil
   340  }
   341  func (c *tBtcWallet) RemovePeer(string) error {
   342  	return nil
   343  }
   344  
   345  func (c *tBtcWallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcutil.Amount, error) {
   346  	return 0, nil
   347  }
   348  
   349  type tNeutrinoClient struct {
   350  	*testData
   351  }
   352  
   353  func (c *tNeutrinoClient) Stop() error { return nil }
   354  
   355  func (c *tNeutrinoClient) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) {
   356  	c.blockchainMtx.RLock()
   357  	defer c.blockchainMtx.RUnlock()
   358  	for height, blockHash := range c.mainchain {
   359  		if height == blockHeight {
   360  			return blockHash, nil
   361  		}
   362  	}
   363  	return nil, fmt.Errorf("no (test) block at height %d", blockHeight)
   364  }
   365  
   366  func (c *tNeutrinoClient) BestBlock() (*headerfs.BlockStamp, error) {
   367  	c.blockchainMtx.RLock()
   368  	if c.getBestBlockHashErr != nil {
   369  		defer c.blockchainMtx.RUnlock() // reading c.getBestBlockHashErr below
   370  		return nil, c.getBestBlockHashErr
   371  	}
   372  	c.blockchainMtx.RUnlock()
   373  	bestHash, bestHeight := c.bestBlock()
   374  	blk := c.getBlock(*bestHash)
   375  	return &headerfs.BlockStamp{
   376  		Height:    int32(bestHeight),
   377  		Hash:      *bestHash,
   378  		Timestamp: blk.msgBlock.Header.Timestamp, // neutrino sets this as of 88c025e
   379  	}, nil
   380  }
   381  
   382  func (c *tNeutrinoClient) Peers() []SPVPeer {
   383  	c.blockchainMtx.RLock()
   384  	defer c.blockchainMtx.RUnlock()
   385  	peer := &neutrino.ServerPeer{Peer: &peer.Peer{}}
   386  	if c.getBlockchainInfo != nil {
   387  		peer.UpdateLastBlockHeight(int32(c.getBlockchainInfo.Headers))
   388  	}
   389  	return []SPVPeer{peer}
   390  }
   391  
   392  func (c *tNeutrinoClient) AddPeer(string) error {
   393  	return nil
   394  }
   395  
   396  func (c *tNeutrinoClient) GetBlockHeight(hash *chainhash.Hash) (int32, error) {
   397  	block := c.getBlock(*hash)
   398  	if block == nil {
   399  		return 0, fmt.Errorf("(test) block not found for block hash %s", hash)
   400  	}
   401  	return int32(block.height), nil
   402  }
   403  
   404  func (c *tNeutrinoClient) GetBlockHeader(blkHash *chainhash.Hash) (*wire.BlockHeader, error) {
   405  	block := c.getBlock(*blkHash)
   406  	if block == nil {
   407  		return nil, errors.New("no block verbose found")
   408  	}
   409  	return &block.msgBlock.Header, nil
   410  }
   411  
   412  func (c *tNeutrinoClient) GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error) {
   413  	var key [gcs.KeySize]byte
   414  	copy(key[:], blockHash.CloneBytes()[:])
   415  	scripts := c.getCFilterScripts[blockHash]
   416  	scripts = append(scripts, encode.RandomBytes(10))
   417  	return gcs.BuildGCSFilter(builder.DefaultP, builder.DefaultM, key, scripts)
   418  }
   419  
   420  func (c *tNeutrinoClient) GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error) {
   421  	blk := c.getBlock(blockHash)
   422  	if blk == nil {
   423  		return nil, fmt.Errorf("no (test) block %s", blockHash)
   424  	}
   425  	return btcutil.NewBlock(blk.msgBlock), nil
   426  }
   427  
   428  type tSPVPeer struct {
   429  	startHeight, lastHeight int32
   430  }
   431  
   432  func (p *tSPVPeer) StartingHeight() int32 {
   433  	return p.startHeight
   434  }
   435  
   436  func (p *tSPVPeer) LastBlock() int32 {
   437  	return p.lastHeight
   438  }
   439  
   440  func TestSwapConfirmations(t *testing.T) {
   441  	wallet, node, shutdown := tNewWallet(true, walletTypeSPV)
   442  	defer shutdown()
   443  
   444  	spv := wallet.node.(*spvWallet)
   445  	node.txOutRes = nil
   446  
   447  	const tipHeight = 10
   448  	const swapHeight = 1
   449  	const spendHeight = 3
   450  	const swapConfs = tipHeight - swapHeight + 1
   451  
   452  	for i := int64(0); i <= tipHeight; i++ {
   453  		node.addRawTx(i, dummyTx())
   454  	}
   455  
   456  	_, _, pkScript, _, _, _, _ := makeSwapContract(true, time.Hour*12)
   457  
   458  	swapTx := makeRawTx([]dex.Bytes{pkScript}, []*wire.TxIn{dummyInput()})
   459  	swapTxHash := swapTx.TxHash()
   460  	const vout = 0
   461  	swapOutPt := NewOutPoint(&swapTxHash, vout)
   462  	swapBlockHash, _ := node.addRawTx(swapHeight, swapTx)
   463  
   464  	spendTx := dummyTx()
   465  	spendTx.TxIn[0].PreviousOutPoint.Hash = swapTxHash
   466  	spendBlockHash, _ := node.addRawTx(spendHeight, spendTx)
   467  	spendTxHash := spendTx.TxHash()
   468  
   469  	matchTime := time.Unix(1, 0)
   470  
   471  	checkSuccess := func(tag string, expConfs uint32, expSpent bool) {
   472  		t.Helper()
   473  		confs, spent, err := spv.SwapConfirmations(&swapTxHash, vout, pkScript, matchTime)
   474  		if err != nil {
   475  			t.Fatalf("%s error: %v", tag, err)
   476  		}
   477  		if confs != expConfs {
   478  			t.Fatalf("wrong number of %s confs. wanted %d, got %d", tag, expConfs, confs)
   479  		}
   480  		if spent != expSpent {
   481  			t.Fatalf("%s path not expected spent status. wanted %t, got %t", tag, expSpent, spent)
   482  		}
   483  	}
   484  
   485  	checkFailure := func(tag string) {
   486  		t.Helper()
   487  		_, _, err := spv.SwapConfirmations(&swapTxHash, vout, pkScript, matchTime)
   488  		if err == nil {
   489  			t.Fatalf("no error for %q test", tag)
   490  		}
   491  	}
   492  
   493  	// confirmations() path
   494  	node.confsErr = tErr
   495  	checkFailure("confirmations")
   496  
   497  	node.confsErr = nil
   498  	node.confs = 10
   499  	node.confsSpent = true
   500  	txB, _ := serializeMsgTx(swapTx)
   501  	node.getTransactionMap = map[string]*GetTransactionResult{
   502  		"any": {
   503  			BlockHash:  swapBlockHash.String(),
   504  			BlockIndex: swapHeight,
   505  			Bytes:      txB,
   506  		}}
   507  	node.walletTxSpent = true
   508  	checkSuccess("confirmations", swapConfs, true)
   509  	node.getTransactionMap = nil
   510  	node.walletTxSpent = false
   511  	node.confsErr = WalletTransactionNotFound
   512  
   513  	// DB path.
   514  	node.dbBlockForTx[swapTxHash] = &hashEntry{hash: *swapBlockHash}
   515  	node.dbBlockForTx[spendTxHash] = &hashEntry{hash: *spendBlockHash}
   516  	node.checkpoints[swapOutPt] = &ScanCheckpoint{
   517  		Res: &FilterScanResult{
   518  			BlockHash:   swapBlockHash,
   519  			BlockHeight: swapHeight,
   520  			Spend:       &SpendingInput{},
   521  			Checkpoint:  *spendBlockHash,
   522  		},
   523  	}
   524  	checkSuccess("GetSpend", swapConfs, true)
   525  	delete(node.checkpoints, swapOutPt)
   526  	delete(node.dbBlockForTx, swapTxHash)
   527  
   528  	// Neutrino scan
   529  
   530  	// Fail cuz no match time provided and block not known.
   531  	matchTime = time.Time{}
   532  	checkFailure("no match time")
   533  	matchTime = time.Unix(1, 0)
   534  
   535  	// spent
   536  	node.getCFilterScripts[*spendBlockHash] = [][]byte{pkScript}
   537  	delete(node.checkpoints, swapOutPt)
   538  	checkSuccess("spend", 0, true)
   539  	node.getCFilterScripts[*spendBlockHash] = nil
   540  
   541  	// Not found
   542  	delete(node.checkpoints, swapOutPt)
   543  	checkFailure("no utxo")
   544  
   545  	// Found.
   546  	node.getCFilterScripts[*swapBlockHash] = [][]byte{pkScript}
   547  	delete(node.checkpoints, swapOutPt)
   548  	checkSuccess("scan find", swapConfs, false)
   549  }
   550  
   551  func TestFindBlockForTime(t *testing.T) {
   552  	wallet, node, shutdown := tNewWallet(true, walletTypeSPV)
   553  	defer shutdown()
   554  	spv := wallet.node.(*spvWallet)
   555  
   556  	const tipHeight = 40
   557  
   558  	for i := 0; i <= tipHeight; i++ {
   559  		node.addRawTx(int64(i), dummyTx())
   560  	}
   561  
   562  	const searchBlock = 35
   563  	matchTime := generateTestBlockTime(searchBlock)
   564  	const offsetBlock = searchBlock - testBlocksPerBlockTimeOffset
   565  	const startBlock = offsetBlock - medianTimeBlocks
   566  	height, err := spv.FindBlockForTime(matchTime)
   567  	if err != nil {
   568  		t.Fatalf("FindBlockForTime error: %v", err)
   569  	}
   570  	if height != startBlock {
   571  		t.Fatalf("wrong height. wanted %d, got %d", startBlock, height)
   572  	}
   573  
   574  	// But if we shift the startBlock time to > offsetBlock time, the window
   575  	// will continue down 11 more.
   576  	_, blk := node.getBlockAtHeight(startBlock)
   577  	blk.msgBlock.Header.Timestamp = generateTestBlockTime(offsetBlock)
   578  	height, err = spv.FindBlockForTime(matchTime)
   579  	if err != nil {
   580  		t.Fatalf("FindBlockForTime error for shifted start block: %v", err)
   581  	}
   582  	if height != startBlock-medianTimeBlocks {
   583  		t.Fatalf("wrong height. wanted %d, got %d", startBlock-11, height)
   584  	}
   585  
   586  	// And doing an early enough block just returns genesis
   587  	height, err = spv.FindBlockForTime(generateTestBlockTime(10))
   588  	if err != nil {
   589  		t.Fatalf("FindBlockForTime error for genesis test: %v", err)
   590  	}
   591  	if height != 0 {
   592  		t.Fatalf("not genesis: height = %d", height)
   593  	}
   594  
   595  	// A time way in the future still returns at least the last 11 blocks.
   596  	height, err = spv.FindBlockForTime(generateTestBlockTime(100))
   597  	if err != nil {
   598  		t.Fatalf("FindBlockForTime error for future test: %v", err)
   599  	}
   600  	// +1 because tip block is included here, as opposed to the shifted start
   601  	// block, where the shifted block wasn't included.
   602  	if height != tipHeight-medianTimeBlocks+1 {
   603  		t.Fatalf("didn't get tip - 11. wanted %d, got %d", tipHeight-medianTimeBlocks, height)
   604  	}
   605  }
   606  
   607  func TestGetTxOut(t *testing.T) {
   608  	wallet, node, shutdown := tNewWallet(true, walletTypeSPV)
   609  	defer shutdown()
   610  	spv := wallet.node.(*spvWallet)
   611  
   612  	_, _, pkScript, _, _, _, _ := makeSwapContract(true, time.Hour*12)
   613  	const vout = 0
   614  	const blockHeight = 10
   615  	const tipHeight = 20
   616  	tx := makeRawTx([]dex.Bytes{pkScript}, []*wire.TxIn{dummyInput()})
   617  	txHash := tx.TxHash()
   618  	outPt := NewOutPoint(&txHash, vout)
   619  	blockHash, _ := node.addRawTx(blockHeight, tx)
   620  	txB, _ := serializeMsgTx(tx)
   621  	node.addRawTx(tipHeight, dummyTx())
   622  	spendingTx := dummyTx()
   623  	spendingTx.TxIn[0].PreviousOutPoint.Hash = txHash
   624  	spendBlockHash, _ := node.addRawTx(tipHeight-1, spendingTx)
   625  
   626  	// Prime the db
   627  	for h := int64(1); h <= tipHeight; h++ {
   628  		node.addRawTx(h, dummyTx())
   629  	}
   630  
   631  	// Abnormal error
   632  	node.getTransactionErr = tErr
   633  	_, _, err := spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight))
   634  	if err == nil {
   635  		t.Fatalf("no error for getWalletTransaction error")
   636  	}
   637  
   638  	// Wallet transaction found
   639  	node.getTransactionErr = nil
   640  	node.getTransactionMap = map[string]*GetTransactionResult{"any": {
   641  		BlockHash: blockHash.String(),
   642  		Bytes:     txB,
   643  	}}
   644  
   645  	_, confs, err := spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight))
   646  	if err != nil {
   647  		t.Fatalf("error for wallet transaction found: %v", err)
   648  	}
   649  	if confs != tipHeight-blockHeight+1 {
   650  		t.Fatalf("wrong confs for wallet transaction. wanted %d, got %d", tipHeight-blockHeight+1, confs)
   651  	}
   652  
   653  	// No wallet transaction, but we have a spend recorded.
   654  	node.getTransactionErr = WalletTransactionNotFound
   655  	node.getTransactionMap = nil
   656  	node.checkpoints[outPt] = &ScanCheckpoint{
   657  		Res: &FilterScanResult{
   658  			BlockHash:  blockHash,
   659  			Spend:      &SpendingInput{},
   660  			Checkpoint: *spendBlockHash,
   661  		}}
   662  	op, confs, err := spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight))
   663  	if op != nil || confs != 0 || err != nil {
   664  		t.Fatal("wrong result for spent txout", op != nil, confs, err)
   665  	}
   666  	delete(node.checkpoints, outPt)
   667  
   668  	// no spend record. gotta scan
   669  
   670  	// case 1: we have a block hash in the database
   671  	node.dbBlockForTx[txHash] = &hashEntry{hash: *blockHash}
   672  	node.getCFilterScripts[*blockHash] = [][]byte{pkScript}
   673  	_, _, err = spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight))
   674  	if err != nil {
   675  		t.Fatalf("error for GetUtxo with cached hash: %v", err)
   676  	}
   677  
   678  	// case 2: no block hash in db. Will do scan and store them.
   679  	delete(node.dbBlockForTx, txHash)
   680  	delete(node.checkpoints, outPt)
   681  	_, _, err = spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight))
   682  	if err != nil {
   683  		t.Fatalf("error for GetUtxo with no cached hash: %v", err)
   684  	}
   685  	if _, inserted := node.dbBlockForTx[txHash]; !inserted {
   686  		t.Fatalf("db not updated after GetUtxo scan success")
   687  	}
   688  
   689  	// case 3: spending tx found first
   690  	delete(node.checkpoints, outPt)
   691  	node.getCFilterScripts[*spendBlockHash] = [][]byte{pkScript}
   692  	txOut, _, err := spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight))
   693  	if err != nil {
   694  		t.Fatalf("error for spent tx: %v", err)
   695  	}
   696  	if txOut != nil {
   697  		t.Fatalf("spend output returned from getTxOut")
   698  	}
   699  
   700  	// Make sure we can find it with the checkpoint.
   701  	node.checkpoints[outPt].Res.Spend = nil
   702  	node.getCFilterScripts[*spendBlockHash] = nil
   703  	// We won't actually scan for the output itself, so nil'ing these should
   704  	// have no effect.
   705  	node.getCFilterScripts[*blockHash] = nil
   706  	_, _, err = spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight))
   707  	if err != nil {
   708  		t.Fatalf("error for checkpointed output: %v", err)
   709  	}
   710  }
   711  
   712  func TestTryBlocksWithNotifier(t *testing.T) {
   713  	defaultWalletBlockAllowance := walletBlockAllowance
   714  	defaultBlockTicker := blockTicker
   715  
   716  	walletBlockAllowance = 50 * time.Millisecond
   717  	blockTicker = 20 * time.Millisecond
   718  
   719  	defer func() {
   720  		walletBlockAllowance = defaultWalletBlockAllowance
   721  		blockTicker = defaultBlockTicker
   722  	}()
   723  
   724  	wallet, node, shutdown := tNewWallet(true, walletTypeSPV)
   725  	defer shutdown()
   726  
   727  	spv := wallet.node.(*spvWallet)
   728  
   729  	getNote := func(timeout time.Duration) bool {
   730  		select {
   731  		case <-node.tipChanged:
   732  			return true
   733  		case <-time.After(timeout):
   734  			return false
   735  		}
   736  	}
   737  
   738  	if getNote(walletBlockAllowance * 2) {
   739  		t.Fatalf("got a first block")
   740  	}
   741  
   742  	// Feed (*baseWallet).watchBlocks:
   743  	// (*spvWallet).getBestBlockHeader -> (*spvWallet).getBestBlockHash -> (*tBtcWallet).syncedTo -> (*testData).bestBlock [mainchain]
   744  	// (*spvWallet).getBestBlockHeader -> (*spvWallet).getBlockHeader -> (*testData).getBlock [verboseBlocks]
   745  
   746  	var tipHeight int64
   747  	addBlock := func() *BlockVector { // update the mainchain and verboseBlocks testData fields
   748  		tipHeight++
   749  		h, _ := node.addRawTx(tipHeight, dummyTx())
   750  		return &BlockVector{tipHeight, *h}
   751  	}
   752  
   753  	// Start with no blocks so that we're not synced.
   754  	node.blockchainMtx.Lock()
   755  	node.getBlockchainInfo = &GetBlockchainInfoResult{
   756  		Headers: 3,
   757  		Blocks:  0,
   758  	}
   759  	node.blockchainMtx.Unlock()
   760  
   761  	addBlock()
   762  
   763  	if !getNote(blockTicker * 2) {
   764  		t.Fatalf("0th block didn't send tip change update")
   765  	}
   766  
   767  	addBlock()
   768  
   769  	// It should not come through on the block tick, since it will be queued.
   770  	if getNote(blockTicker * 2) {
   771  		t.Fatalf("got non-0th block that should've been queued")
   772  	}
   773  
   774  	// And it won't come through after a single block allowance, because we're
   775  	// not synced.
   776  	if getNote(walletBlockAllowance * 2) {
   777  		t.Fatal("block didn't wait for the syncing mode allowance")
   778  	}
   779  
   780  	// But it will come through after the sync timeout = 10 * normal timeout.
   781  	if !getNote(walletBlockAllowance * 9) {
   782  		t.Fatal("block didn't time out in syncing mode")
   783  	}
   784  
   785  	// But if we're synced, it should come through after the normal block
   786  	// allowance.
   787  	addBlock()
   788  	node.blockchainMtx.Lock()
   789  	node.getBlockchainInfo = &GetBlockchainInfoResult{
   790  		Headers: tipHeight,
   791  		Blocks:  tipHeight,
   792  	}
   793  	node.blockchainMtx.Unlock()
   794  	if !getNote(walletBlockAllowance * 2) {
   795  		t.Fatal("block didn't time out in normal mode")
   796  	}
   797  
   798  	// On the other hand, a wallet block should come through immediately. Not
   799  	// even waiting on the block tick.
   800  	spv.tipChan <- addBlock()
   801  	if !getNote(blockTicker / 2) {
   802  		t.Fatal("wallet block wasn't sent through")
   803  	}
   804  
   805  	// If we do the same thing but make sure that a polled block is queued
   806  	// first, we should still see the block right away, and the queued block
   807  	// should be canceled.
   808  	blk := addBlock()
   809  	time.Sleep(blockTicker * 2)
   810  	spv.tipChan <- blk
   811  	if !getNote(blockTicker / 2) {
   812  		t.Fatal("wallet block wasn't sent through with polled block queued")
   813  	}
   814  
   815  	if getNote(walletBlockAllowance * 2) {
   816  		t.Fatal("queued polled block that should have been canceled came through")
   817  	}
   818  }
   819  
   820  func TestGetBlockHeader(t *testing.T) {
   821  	wallet, node, shutdown := tNewWallet(true, walletTypeSPV)
   822  	defer shutdown()
   823  
   824  	const tipHeight = 12
   825  	const blockHeight = 11 // 2 confirmations
   826  	var blockHash, prevHash, h chainhash.Hash
   827  	var blockHdr msgBlockWithHeight
   828  	for height := 0; height <= tipHeight; height++ {
   829  		hdr := &msgBlockWithHeight{
   830  			msgBlock: &wire.MsgBlock{
   831  				Header: wire.BlockHeader{
   832  					PrevBlock: h,
   833  					Timestamp: time.Unix(int64(height), 0),
   834  				},
   835  				Transactions: nil,
   836  			},
   837  			height: int64(height),
   838  		}
   839  
   840  		h = hdr.msgBlock.BlockHash()
   841  
   842  		switch height {
   843  		case blockHeight - 1:
   844  			prevHash = h
   845  			blockHdr = *hdr
   846  		case blockHeight:
   847  			blockHash = h
   848  		}
   849  
   850  		node.verboseBlocks[h] = hdr
   851  		hh := h // just because we are storing pointers in mainchain
   852  		node.mainchain[int64(height)] = &hh
   853  	}
   854  
   855  	hdr, mainchain, err := wallet.tipRedeemer.GetBlockHeader(&blockHash)
   856  	if err != nil {
   857  		t.Fatalf("initial success error: %v", err)
   858  	}
   859  	if !mainchain {
   860  		t.Fatalf("expected block %s to be in mainchain", blockHash)
   861  	}
   862  	if hdr.Hash != blockHash.String() {
   863  		t.Fatal("wrong header?")
   864  	}
   865  	if hdr.Height != blockHeight {
   866  		t.Fatal("wrong height?")
   867  	}
   868  	if hdr.Time != blockHeight {
   869  		t.Fatalf("wrong block time stamp, wanted %d, got %d", blockHeight, hdr.Time)
   870  	}
   871  	if hdr.Confirmations != 2 {
   872  		t.Fatalf("expected 2 confs, got %d", hdr.Confirmations)
   873  	}
   874  	if hdr.PreviousBlockHash != prevHash.String() {
   875  		t.Fatalf("wrong previous hash, want: %s got: %s", prevHash.String(), hdr.PreviousBlockHash)
   876  	}
   877  
   878  	node.mainchain[int64(blockHeight)] = &chainhash.Hash{0x01} // mainchain ended up with different block
   879  	hdr, mainchain, err = wallet.tipRedeemer.GetBlockHeader(&blockHash)
   880  	if err != nil {
   881  		t.Fatalf("initial success error: %v", err)
   882  	}
   883  	if mainchain {
   884  		t.Fatalf("expected block %s to be classified as oprhan", blockHash)
   885  	}
   886  	if hdr.Confirmations != -1 {
   887  		t.Fatalf("expected -1 confs for side chain block, got %d", hdr.Confirmations)
   888  	}
   889  	node.mainchain[int64(blockHeight)] = &blockHash // clean up
   890  
   891  	// Can't fetch header error.
   892  	delete(node.verboseBlocks, blockHash) // can't find block by hash
   893  	if _, _, err := wallet.tipRedeemer.GetBlockHeader(&blockHash); err == nil {
   894  		t.Fatalf("Can't fetch header error not propagated")
   895  	}
   896  	node.verboseBlocks[blockHash] = &blockHdr // clean up
   897  
   898  	// Main chain is shorter than requested block.
   899  	prevMainchain := node.mainchain
   900  	node.mainchain = map[int64]*chainhash.Hash{
   901  		0: node.mainchain[0],
   902  		1: node.mainchain[1],
   903  	}
   904  	hdr, mainchain, err = wallet.tipRedeemer.GetBlockHeader(&blockHash)
   905  	if err != nil {
   906  		t.Fatalf("invalid tip height not noticed")
   907  	}
   908  	if mainchain {
   909  		t.Fatalf("expected block %s to be classified as orphan", blockHash)
   910  	}
   911  	if hdr.Confirmations != -1 {
   912  		t.Fatalf("confirmations not zero for lower mainchain tip height, got: %d", hdr.Confirmations)
   913  	}
   914  	node.mainchain = prevMainchain // clean up
   915  }