github.com/btcsuite/btcd@v0.24.0/integration/rpctest/memwallet.go (about)

     1  // Copyright (c) 2016-2017 The btcsuite developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package rpctest
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/binary"
    10  	"fmt"
    11  	"sync"
    12  
    13  	"github.com/btcsuite/btcd/blockchain"
    14  	"github.com/btcsuite/btcd/btcec/v2"
    15  	"github.com/btcsuite/btcd/btcutil"
    16  	"github.com/btcsuite/btcd/btcutil/hdkeychain"
    17  	"github.com/btcsuite/btcd/chaincfg"
    18  	"github.com/btcsuite/btcd/chaincfg/chainhash"
    19  	"github.com/btcsuite/btcd/rpcclient"
    20  	"github.com/btcsuite/btcd/txscript"
    21  	"github.com/btcsuite/btcd/wire"
    22  )
    23  
    24  var (
    25  	// hdSeed is the BIP 32 seed used by the memWallet to initialize it's
    26  	// HD root key. This value is hard coded in order to ensure
    27  	// deterministic behavior across test runs.
    28  	hdSeed = [chainhash.HashSize]byte{
    29  		0x79, 0xa6, 0x1a, 0xdb, 0xc6, 0xe5, 0xa2, 0xe1,
    30  		0x39, 0xd2, 0x71, 0x3a, 0x54, 0x6e, 0xc7, 0xc8,
    31  		0x75, 0x63, 0x2e, 0x75, 0xf1, 0xdf, 0x9c, 0x3f,
    32  		0xa6, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    33  	}
    34  )
    35  
    36  // utxo represents an unspent output spendable by the memWallet. The maturity
    37  // height of the transaction is recorded in order to properly observe the
    38  // maturity period of direct coinbase outputs.
    39  type utxo struct {
    40  	pkScript       []byte
    41  	value          btcutil.Amount
    42  	keyIndex       uint32
    43  	maturityHeight int32
    44  	isLocked       bool
    45  }
    46  
    47  // isMature returns true if the target utxo is considered "mature" at the
    48  // passed block height. Otherwise, false is returned.
    49  func (u *utxo) isMature(height int32) bool {
    50  	return height >= u.maturityHeight
    51  }
    52  
    53  // chainUpdate encapsulates an update to the current main chain. This struct is
    54  // used to sync up the memWallet each time a new block is connected to the main
    55  // chain.
    56  type chainUpdate struct {
    57  	blockHeight  int32
    58  	filteredTxns []*btcutil.Tx
    59  	isConnect    bool // True if connect, false if disconnect
    60  }
    61  
    62  // undoEntry is functionally the opposite of a chainUpdate. An undoEntry is
    63  // created for each new block received, then stored in a log in order to
    64  // properly handle block re-orgs.
    65  type undoEntry struct {
    66  	utxosDestroyed map[wire.OutPoint]*utxo
    67  	utxosCreated   []wire.OutPoint
    68  }
    69  
    70  // memWallet is a simple in-memory wallet whose purpose is to provide basic
    71  // wallet functionality to the harness. The wallet uses a hard-coded HD key
    72  // hierarchy which promotes reproducibility between harness test runs.
    73  type memWallet struct {
    74  	coinbaseKey  *btcec.PrivateKey
    75  	coinbaseAddr btcutil.Address
    76  
    77  	// hdRoot is the root master private key for the wallet.
    78  	hdRoot *hdkeychain.ExtendedKey
    79  
    80  	// hdIndex is the next available key index offset from the hdRoot.
    81  	hdIndex uint32
    82  
    83  	// currentHeight is the latest height the wallet is known to be synced
    84  	// to.
    85  	currentHeight int32
    86  
    87  	// addrs tracks all addresses belonging to the wallet. The addresses
    88  	// are indexed by their keypath from the hdRoot.
    89  	addrs map[uint32]btcutil.Address
    90  
    91  	// utxos is the set of utxos spendable by the wallet.
    92  	utxos map[wire.OutPoint]*utxo
    93  
    94  	// reorgJournal is a map storing an undo entry for each new block
    95  	// received. Once a block is disconnected, the undo entry for the
    96  	// particular height is evaluated, thereby rewinding the effect of the
    97  	// disconnected block on the wallet's set of spendable utxos.
    98  	reorgJournal map[int32]*undoEntry
    99  
   100  	chainUpdates      []*chainUpdate
   101  	chainUpdateSignal chan struct{}
   102  	chainMtx          sync.Mutex
   103  
   104  	net *chaincfg.Params
   105  
   106  	rpc *rpcclient.Client
   107  
   108  	sync.RWMutex
   109  }
   110  
   111  // newMemWallet creates and returns a fully initialized instance of the
   112  // memWallet given a particular blockchain's parameters.
   113  func newMemWallet(net *chaincfg.Params, harnessID uint32) (*memWallet, error) {
   114  	// The wallet's final HD seed is: hdSeed || harnessID. This method
   115  	// ensures that each harness instance uses a deterministic root seed
   116  	// based on its harness ID.
   117  	var harnessHDSeed [chainhash.HashSize + 4]byte
   118  	copy(harnessHDSeed[:], hdSeed[:])
   119  	binary.BigEndian.PutUint32(harnessHDSeed[:chainhash.HashSize], harnessID)
   120  
   121  	hdRoot, err := hdkeychain.NewMaster(harnessHDSeed[:], net)
   122  	if err != nil {
   123  		return nil, nil
   124  	}
   125  
   126  	// The first child key from the hd root is reserved as the coinbase
   127  	// generation address.
   128  	coinbaseChild, err := hdRoot.Derive(0)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	coinbaseKey, err := coinbaseChild.ECPrivKey()
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  
   137  	coinbaseAddr, err := keyToAddr(coinbaseKey, net)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	// Track the coinbase generation address to ensure we properly track
   143  	// newly generated bitcoin we can spend.
   144  	addrs := make(map[uint32]btcutil.Address)
   145  	addrs[0] = coinbaseAddr
   146  
   147  	return &memWallet{
   148  		net:               net,
   149  		coinbaseKey:       coinbaseKey,
   150  		coinbaseAddr:      coinbaseAddr,
   151  		hdIndex:           1,
   152  		hdRoot:            hdRoot,
   153  		addrs:             addrs,
   154  		utxos:             make(map[wire.OutPoint]*utxo),
   155  		chainUpdateSignal: make(chan struct{}),
   156  		reorgJournal:      make(map[int32]*undoEntry),
   157  	}, nil
   158  }
   159  
   160  // Start launches all goroutines required for the wallet to function properly.
   161  func (m *memWallet) Start() {
   162  	go m.chainSyncer()
   163  }
   164  
   165  // SyncedHeight returns the height the wallet is known to be synced to.
   166  //
   167  // This function is safe for concurrent access.
   168  func (m *memWallet) SyncedHeight() int32 {
   169  	m.RLock()
   170  	defer m.RUnlock()
   171  	return m.currentHeight
   172  }
   173  
   174  // SetRPCClient saves the passed rpc connection to btcd as the wallet's
   175  // personal rpc connection.
   176  func (m *memWallet) SetRPCClient(rpcClient *rpcclient.Client) {
   177  	m.rpc = rpcClient
   178  }
   179  
   180  // IngestBlock is a call-back which is to be triggered each time a new block is
   181  // connected to the main chain. It queues the update for the chain syncer,
   182  // calling the private version in sequential order.
   183  func (m *memWallet) IngestBlock(height int32, header *wire.BlockHeader, filteredTxns []*btcutil.Tx) {
   184  	// Append this new chain update to the end of the queue of new chain
   185  	// updates.
   186  	m.chainMtx.Lock()
   187  	m.chainUpdates = append(m.chainUpdates, &chainUpdate{height,
   188  		filteredTxns, true})
   189  	m.chainMtx.Unlock()
   190  
   191  	// Launch a goroutine to signal the chainSyncer that a new update is
   192  	// available. We do this in a new goroutine in order to avoid blocking
   193  	// the main loop of the rpc client.
   194  	go func() {
   195  		m.chainUpdateSignal <- struct{}{}
   196  	}()
   197  }
   198  
   199  // ingestBlock updates the wallet's internal utxo state based on the outputs
   200  // created and destroyed within each block.
   201  func (m *memWallet) ingestBlock(update *chainUpdate) {
   202  	// Update the latest synced height, then process each filtered
   203  	// transaction in the block creating and destroying utxos within
   204  	// the wallet as a result.
   205  	m.currentHeight = update.blockHeight
   206  	undo := &undoEntry{
   207  		utxosDestroyed: make(map[wire.OutPoint]*utxo),
   208  	}
   209  	for _, tx := range update.filteredTxns {
   210  		mtx := tx.MsgTx()
   211  		isCoinbase := blockchain.IsCoinBaseTx(mtx)
   212  		txHash := mtx.TxHash()
   213  		m.evalOutputs(mtx.TxOut, &txHash, isCoinbase, undo)
   214  		m.evalInputs(mtx.TxIn, undo)
   215  	}
   216  
   217  	// Finally, record the undo entry for this block so we can
   218  	// properly update our internal state in response to the block
   219  	// being re-org'd from the main chain.
   220  	m.reorgJournal[update.blockHeight] = undo
   221  }
   222  
   223  // chainSyncer is a goroutine dedicated to processing new blocks in order to
   224  // keep the wallet's utxo state up to date.
   225  //
   226  // NOTE: This MUST be run as a goroutine.
   227  func (m *memWallet) chainSyncer() {
   228  	var update *chainUpdate
   229  
   230  	for range m.chainUpdateSignal {
   231  		// A new update is available, so pop the new chain update from
   232  		// the front of the update queue.
   233  		m.chainMtx.Lock()
   234  		update = m.chainUpdates[0]
   235  		m.chainUpdates[0] = nil // Set to nil to prevent GC leak.
   236  		m.chainUpdates = m.chainUpdates[1:]
   237  		m.chainMtx.Unlock()
   238  
   239  		m.Lock()
   240  		if update.isConnect {
   241  			m.ingestBlock(update)
   242  		} else {
   243  			m.unwindBlock(update)
   244  		}
   245  		m.Unlock()
   246  	}
   247  }
   248  
   249  // evalOutputs evaluates each of the passed outputs, creating a new matching
   250  // utxo within the wallet if we're able to spend the output.
   251  func (m *memWallet) evalOutputs(outputs []*wire.TxOut, txHash *chainhash.Hash,
   252  	isCoinbase bool, undo *undoEntry) {
   253  
   254  	for i, output := range outputs {
   255  		pkScript := output.PkScript
   256  
   257  		// Scan all the addresses we currently control to see if the
   258  		// output is paying to us.
   259  		for keyIndex, addr := range m.addrs {
   260  			pkHash := addr.ScriptAddress()
   261  			if !bytes.Contains(pkScript, pkHash) {
   262  				continue
   263  			}
   264  
   265  			// If this is a coinbase output, then we mark the
   266  			// maturity height at the proper block height in the
   267  			// future.
   268  			var maturityHeight int32
   269  			if isCoinbase {
   270  				maturityHeight = m.currentHeight + int32(m.net.CoinbaseMaturity)
   271  			}
   272  
   273  			op := wire.OutPoint{Hash: *txHash, Index: uint32(i)}
   274  			m.utxos[op] = &utxo{
   275  				value:          btcutil.Amount(output.Value),
   276  				keyIndex:       keyIndex,
   277  				maturityHeight: maturityHeight,
   278  				pkScript:       pkScript,
   279  			}
   280  			undo.utxosCreated = append(undo.utxosCreated, op)
   281  		}
   282  	}
   283  }
   284  
   285  // evalInputs scans all the passed inputs, destroying any utxos within the
   286  // wallet which are spent by an input.
   287  func (m *memWallet) evalInputs(inputs []*wire.TxIn, undo *undoEntry) {
   288  	for _, txIn := range inputs {
   289  		op := txIn.PreviousOutPoint
   290  		oldUtxo, ok := m.utxos[op]
   291  		if !ok {
   292  			continue
   293  		}
   294  
   295  		undo.utxosDestroyed[op] = oldUtxo
   296  		delete(m.utxos, op)
   297  	}
   298  }
   299  
   300  // UnwindBlock is a call-back which is to be executed each time a block is
   301  // disconnected from the main chain. It queues the update for the chain syncer,
   302  // calling the private version in sequential order.
   303  func (m *memWallet) UnwindBlock(height int32, header *wire.BlockHeader) {
   304  	// Append this new chain update to the end of the queue of new chain
   305  	// updates.
   306  	m.chainMtx.Lock()
   307  	m.chainUpdates = append(m.chainUpdates, &chainUpdate{height,
   308  		nil, false})
   309  	m.chainMtx.Unlock()
   310  
   311  	// Launch a goroutine to signal the chainSyncer that a new update is
   312  	// available. We do this in a new goroutine in order to avoid blocking
   313  	// the main loop of the rpc client.
   314  	go func() {
   315  		m.chainUpdateSignal <- struct{}{}
   316  	}()
   317  }
   318  
   319  // unwindBlock undoes the effect that a particular block had on the wallet's
   320  // internal utxo state.
   321  func (m *memWallet) unwindBlock(update *chainUpdate) {
   322  	undo := m.reorgJournal[update.blockHeight]
   323  
   324  	for _, utxo := range undo.utxosCreated {
   325  		delete(m.utxos, utxo)
   326  	}
   327  
   328  	for outPoint, utxo := range undo.utxosDestroyed {
   329  		m.utxos[outPoint] = utxo
   330  	}
   331  
   332  	delete(m.reorgJournal, update.blockHeight)
   333  }
   334  
   335  // newAddress returns a new address from the wallet's hd key chain.  It also
   336  // loads the address into the RPC client's transaction filter to ensure any
   337  // transactions that involve it are delivered via the notifications.
   338  func (m *memWallet) newAddress() (btcutil.Address, error) {
   339  	index := m.hdIndex
   340  
   341  	childKey, err := m.hdRoot.Derive(index)
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  	privKey, err := childKey.ECPrivKey()
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  
   350  	addr, err := keyToAddr(privKey, m.net)
   351  	if err != nil {
   352  		return nil, err
   353  	}
   354  
   355  	err = m.rpc.LoadTxFilter(false, []btcutil.Address{addr}, nil)
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  
   360  	m.addrs[index] = addr
   361  
   362  	m.hdIndex++
   363  
   364  	return addr, nil
   365  }
   366  
   367  // NewAddress returns a fresh address spendable by the wallet.
   368  //
   369  // This function is safe for concurrent access.
   370  func (m *memWallet) NewAddress() (btcutil.Address, error) {
   371  	m.Lock()
   372  	defer m.Unlock()
   373  
   374  	return m.newAddress()
   375  }
   376  
   377  // fundTx attempts to fund a transaction sending amt bitcoin. The coins are
   378  // selected such that the final amount spent pays enough fees as dictated by the
   379  // passed fee rate. The passed fee rate should be expressed in
   380  // satoshis-per-byte. The transaction being funded can optionally include a
   381  // change output indicated by the change boolean.
   382  //
   383  // NOTE: The memWallet's mutex must be held when this function is called.
   384  func (m *memWallet) fundTx(tx *wire.MsgTx, amt btcutil.Amount,
   385  	feeRate btcutil.Amount, change bool) error {
   386  
   387  	const (
   388  		// spendSize is the largest number of bytes of a sigScript
   389  		// which spends a p2pkh output: OP_DATA_73 <sig> OP_DATA_33 <pubkey>
   390  		spendSize = 1 + 73 + 1 + 33
   391  	)
   392  
   393  	var (
   394  		amtSelected btcutil.Amount
   395  		txSize      int
   396  	)
   397  
   398  	for outPoint, utxo := range m.utxos {
   399  		// Skip any outputs that are still currently immature or are
   400  		// currently locked.
   401  		if !utxo.isMature(m.currentHeight) || utxo.isLocked {
   402  			continue
   403  		}
   404  
   405  		amtSelected += utxo.value
   406  
   407  		// Add the selected output to the transaction, updating the
   408  		// current tx size while accounting for the size of the future
   409  		// sigScript.
   410  		tx.AddTxIn(wire.NewTxIn(&outPoint, nil, nil))
   411  		txSize = tx.SerializeSize() + spendSize*len(tx.TxIn)
   412  
   413  		// Calculate the fee required for the txn at this point
   414  		// observing the specified fee rate. If we don't have enough
   415  		// coins from he current amount selected to pay the fee, then
   416  		// continue to grab more coins.
   417  		reqFee := btcutil.Amount(txSize * int(feeRate))
   418  		if amtSelected-reqFee < amt {
   419  			continue
   420  		}
   421  
   422  		// If we have any change left over and we should create a change
   423  		// output, then add an additional output to the transaction
   424  		// reserved for it.
   425  		changeVal := amtSelected - amt - reqFee
   426  		if changeVal > 0 && change {
   427  			addr, err := m.newAddress()
   428  			if err != nil {
   429  				return err
   430  			}
   431  			pkScript, err := txscript.PayToAddrScript(addr)
   432  			if err != nil {
   433  				return err
   434  			}
   435  			changeOutput := &wire.TxOut{
   436  				Value:    int64(changeVal),
   437  				PkScript: pkScript,
   438  			}
   439  			tx.AddTxOut(changeOutput)
   440  		}
   441  
   442  		return nil
   443  	}
   444  
   445  	// If we've reached this point, then coin selection failed due to an
   446  	// insufficient amount of coins.
   447  	return fmt.Errorf("not enough funds for coin selection")
   448  }
   449  
   450  // SendOutputs creates, then sends a transaction paying to the specified output
   451  // while observing the passed fee rate. The passed fee rate should be expressed
   452  // in satoshis-per-byte.
   453  func (m *memWallet) SendOutputs(outputs []*wire.TxOut,
   454  	feeRate btcutil.Amount) (*chainhash.Hash, error) {
   455  
   456  	tx, err := m.CreateTransaction(outputs, feeRate, true)
   457  	if err != nil {
   458  		return nil, err
   459  	}
   460  
   461  	return m.rpc.SendRawTransaction(tx, true)
   462  }
   463  
   464  // SendOutputsWithoutChange creates and sends a transaction that pays to the
   465  // specified outputs while observing the passed fee rate and ignoring a change
   466  // output. The passed fee rate should be expressed in sat/b.
   467  func (m *memWallet) SendOutputsWithoutChange(outputs []*wire.TxOut,
   468  	feeRate btcutil.Amount) (*chainhash.Hash, error) {
   469  
   470  	tx, err := m.CreateTransaction(outputs, feeRate, false)
   471  	if err != nil {
   472  		return nil, err
   473  	}
   474  
   475  	return m.rpc.SendRawTransaction(tx, true)
   476  }
   477  
   478  // CreateTransaction returns a fully signed transaction paying to the specified
   479  // outputs while observing the desired fee rate. The passed fee rate should be
   480  // expressed in satoshis-per-byte. The transaction being created can optionally
   481  // include a change output indicated by the change boolean.
   482  //
   483  // This function is safe for concurrent access.
   484  func (m *memWallet) CreateTransaction(outputs []*wire.TxOut,
   485  	feeRate btcutil.Amount, change bool) (*wire.MsgTx, error) {
   486  
   487  	m.Lock()
   488  	defer m.Unlock()
   489  
   490  	tx := wire.NewMsgTx(wire.TxVersion)
   491  
   492  	// Tally up the total amount to be sent in order to perform coin
   493  	// selection shortly below.
   494  	var outputAmt btcutil.Amount
   495  	for _, output := range outputs {
   496  		outputAmt += btcutil.Amount(output.Value)
   497  		tx.AddTxOut(output)
   498  	}
   499  
   500  	// Attempt to fund the transaction with spendable utxos.
   501  	if err := m.fundTx(tx, outputAmt, feeRate, change); err != nil {
   502  		return nil, err
   503  	}
   504  
   505  	// Populate all the selected inputs with valid sigScript for spending.
   506  	// Along the way record all outputs being spent in order to avoid a
   507  	// potential double spend.
   508  	spentOutputs := make([]*utxo, 0, len(tx.TxIn))
   509  	for i, txIn := range tx.TxIn {
   510  		outPoint := txIn.PreviousOutPoint
   511  		utxo := m.utxos[outPoint]
   512  
   513  		extendedKey, err := m.hdRoot.Derive(utxo.keyIndex)
   514  		if err != nil {
   515  			return nil, err
   516  		}
   517  
   518  		privKeyOld, err := extendedKey.ECPrivKey()
   519  		if err != nil {
   520  			return nil, err
   521  		}
   522  
   523  		privKey, _ := btcec.PrivKeyFromBytes(privKeyOld.Serialize())
   524  
   525  		sigScript, err := txscript.SignatureScript(tx, i, utxo.pkScript,
   526  			txscript.SigHashAll, privKey, true)
   527  		if err != nil {
   528  			return nil, err
   529  		}
   530  
   531  		txIn.SignatureScript = sigScript
   532  
   533  		spentOutputs = append(spentOutputs, utxo)
   534  	}
   535  
   536  	// As these outputs are now being spent by this newly created
   537  	// transaction, mark the outputs are "locked". This action ensures
   538  	// these outputs won't be double spent by any subsequent transactions.
   539  	// These locked outputs can be freed via a call to UnlockOutputs.
   540  	for _, utxo := range spentOutputs {
   541  		utxo.isLocked = true
   542  	}
   543  
   544  	return tx, nil
   545  }
   546  
   547  // UnlockOutputs unlocks any outputs which were previously locked due to
   548  // being selected to fund a transaction via the CreateTransaction method.
   549  //
   550  // This function is safe for concurrent access.
   551  func (m *memWallet) UnlockOutputs(inputs []*wire.TxIn) {
   552  	m.Lock()
   553  	defer m.Unlock()
   554  
   555  	for _, input := range inputs {
   556  		utxo, ok := m.utxos[input.PreviousOutPoint]
   557  		if !ok {
   558  			continue
   559  		}
   560  
   561  		utxo.isLocked = false
   562  	}
   563  }
   564  
   565  // ConfirmedBalance returns the confirmed balance of the wallet.
   566  //
   567  // This function is safe for concurrent access.
   568  func (m *memWallet) ConfirmedBalance() btcutil.Amount {
   569  	m.RLock()
   570  	defer m.RUnlock()
   571  
   572  	var balance btcutil.Amount
   573  	for _, utxo := range m.utxos {
   574  		// Prevent any immature or locked outputs from contributing to
   575  		// the wallet's total confirmed balance.
   576  		if !utxo.isMature(m.currentHeight) || utxo.isLocked {
   577  			continue
   578  		}
   579  
   580  		balance += utxo.value
   581  	}
   582  
   583  	return balance
   584  }
   585  
   586  // keyToAddr maps the passed private to corresponding p2pkh address.
   587  func keyToAddr(key *btcec.PrivateKey, net *chaincfg.Params) (btcutil.Address, error) {
   588  	serializedKey := key.PubKey().SerializeCompressed()
   589  	pubKeyAddr, err := btcutil.NewAddressPubKey(serializedKey, net)
   590  	if err != nil {
   591  		return nil, err
   592  	}
   593  	return pubKeyAddr.AddressPubKeyHash(), nil
   594  }