github.com/lbryio/lbcd@v0.22.119/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/lbryio/lbcd/blockchain"
    14  	"github.com/lbryio/lbcd/btcec"
    15  	"github.com/lbryio/lbcd/chaincfg"
    16  	"github.com/lbryio/lbcd/chaincfg/chainhash"
    17  	"github.com/lbryio/lbcd/rpcclient"
    18  	"github.com/lbryio/lbcd/txscript"
    19  	"github.com/lbryio/lbcd/wire"
    20  	btcutil "github.com/lbryio/lbcutil"
    21  	"github.com/lbryio/lbcutil/hdkeychain"
    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  	coinbaseAddr, err := keyToAddr(coinbaseKey, net)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	// Track the coinbase generation address to ensure we properly track
   142  	// newly generated bitcoin we can spend.
   143  	addrs := make(map[uint32]btcutil.Address)
   144  	addrs[0] = coinbaseAddr
   145  
   146  	return &memWallet{
   147  		net:               net,
   148  		coinbaseKey:       coinbaseKey,
   149  		coinbaseAddr:      coinbaseAddr,
   150  		hdIndex:           1,
   151  		hdRoot:            hdRoot,
   152  		addrs:             addrs,
   153  		utxos:             make(map[wire.OutPoint]*utxo),
   154  		chainUpdateSignal: make(chan struct{}),
   155  		reorgJournal:      make(map[int32]*undoEntry),
   156  	}, nil
   157  }
   158  
   159  // Start launches all goroutines required for the wallet to function properly.
   160  func (m *memWallet) Start() {
   161  	go m.chainSyncer()
   162  }
   163  
   164  // SyncedHeight returns the height the wallet is known to be synced to.
   165  //
   166  // This function is safe for concurrent access.
   167  func (m *memWallet) SyncedHeight() int32 {
   168  	m.RLock()
   169  	defer m.RUnlock()
   170  	return m.currentHeight
   171  }
   172  
   173  // SetRPCClient saves the passed rpc connection to btcd as the wallet's
   174  // personal rpc connection.
   175  func (m *memWallet) SetRPCClient(rpcClient *rpcclient.Client) {
   176  	m.rpc = rpcClient
   177  }
   178  
   179  // IngestBlock is a call-back which is to be triggered each time a new block is
   180  // connected to the main chain. It queues the update for the chain syncer,
   181  // calling the private version in sequential order.
   182  func (m *memWallet) IngestBlock(height int32, header *wire.BlockHeader, filteredTxns []*btcutil.Tx) {
   183  	// Append this new chain update to the end of the queue of new chain
   184  	// updates.
   185  	m.chainMtx.Lock()
   186  	m.chainUpdates = append(m.chainUpdates, &chainUpdate{height,
   187  		filteredTxns, true})
   188  	m.chainMtx.Unlock()
   189  
   190  	// Launch a goroutine to signal the chainSyncer that a new update is
   191  	// available. We do this in a new goroutine in order to avoid blocking
   192  	// the main loop of the rpc client.
   193  	go func() {
   194  		m.chainUpdateSignal <- struct{}{}
   195  	}()
   196  }
   197  
   198  // ingestBlock updates the wallet's internal utxo state based on the outputs
   199  // created and destroyed within each block.
   200  func (m *memWallet) ingestBlock(update *chainUpdate) {
   201  	// Update the latest synced height, then process each filtered
   202  	// transaction in the block creating and destroying utxos within
   203  	// the wallet as a result.
   204  	m.currentHeight = update.blockHeight
   205  	undo := &undoEntry{
   206  		utxosDestroyed: make(map[wire.OutPoint]*utxo),
   207  	}
   208  	for _, tx := range update.filteredTxns {
   209  		mtx := tx.MsgTx()
   210  		isCoinbase := blockchain.IsCoinBaseTx(mtx)
   211  		txHash := mtx.TxHash()
   212  		m.evalOutputs(mtx.TxOut, &txHash, isCoinbase, undo)
   213  		m.evalInputs(mtx.TxIn, undo)
   214  	}
   215  
   216  	// Finally, record the undo entry for this block so we can
   217  	// properly update our internal state in response to the block
   218  	// being re-org'd from the main chain.
   219  	m.reorgJournal[update.blockHeight] = undo
   220  }
   221  
   222  // chainSyncer is a goroutine dedicated to processing new blocks in order to
   223  // keep the wallet's utxo state up to date.
   224  //
   225  // NOTE: This MUST be run as a goroutine.
   226  func (m *memWallet) chainSyncer() {
   227  	var update *chainUpdate
   228  
   229  	for range m.chainUpdateSignal {
   230  		// A new update is available, so pop the new chain update from
   231  		// the front of the update queue.
   232  		m.chainMtx.Lock()
   233  		update = m.chainUpdates[0]
   234  		m.chainUpdates[0] = nil // Set to nil to prevent GC leak.
   235  		m.chainUpdates = m.chainUpdates[1:]
   236  		m.chainMtx.Unlock()
   237  
   238  		m.Lock()
   239  		if update.isConnect {
   240  			m.ingestBlock(update)
   241  		} else {
   242  			m.unwindBlock(update)
   243  		}
   244  		m.Unlock()
   245  	}
   246  }
   247  
   248  // evalOutputs evaluates each of the passed outputs, creating a new matching
   249  // utxo within the wallet if we're able to spend the output.
   250  func (m *memWallet) evalOutputs(outputs []*wire.TxOut, txHash *chainhash.Hash,
   251  	isCoinbase bool, undo *undoEntry) {
   252  
   253  	for i, output := range outputs {
   254  		pkScript := output.PkScript
   255  
   256  		// Scan all the addresses we currently control to see if the
   257  		// output is paying to us.
   258  		for keyIndex, addr := range m.addrs {
   259  			pkHash := addr.ScriptAddress()
   260  			if !bytes.Contains(pkScript, pkHash) {
   261  				continue
   262  			}
   263  
   264  			// If this is a coinbase output, then we mark the
   265  			// maturity height at the proper block height in the
   266  			// future.
   267  			var maturityHeight int32
   268  			if isCoinbase {
   269  				maturityHeight = m.currentHeight + int32(m.net.CoinbaseMaturity)
   270  			}
   271  
   272  			op := wire.OutPoint{Hash: *txHash, Index: uint32(i)}
   273  			m.utxos[op] = &utxo{
   274  				value:          btcutil.Amount(output.Value),
   275  				keyIndex:       keyIndex,
   276  				maturityHeight: maturityHeight,
   277  				pkScript:       pkScript,
   278  			}
   279  			undo.utxosCreated = append(undo.utxosCreated, op)
   280  		}
   281  	}
   282  }
   283  
   284  // evalInputs scans all the passed inputs, destroying any utxos within the
   285  // wallet which are spent by an input.
   286  func (m *memWallet) evalInputs(inputs []*wire.TxIn, undo *undoEntry) {
   287  	for _, txIn := range inputs {
   288  		op := txIn.PreviousOutPoint
   289  		oldUtxo, ok := m.utxos[op]
   290  		if !ok {
   291  			continue
   292  		}
   293  
   294  		undo.utxosDestroyed[op] = oldUtxo
   295  		delete(m.utxos, op)
   296  	}
   297  }
   298  
   299  // UnwindBlock is a call-back which is to be executed each time a block is
   300  // disconnected from the main chain. It queues the update for the chain syncer,
   301  // calling the private version in sequential order.
   302  func (m *memWallet) UnwindBlock(height int32, header *wire.BlockHeader) {
   303  	// Append this new chain update to the end of the queue of new chain
   304  	// updates.
   305  	m.chainMtx.Lock()
   306  	m.chainUpdates = append(m.chainUpdates, &chainUpdate{height,
   307  		nil, false})
   308  	m.chainMtx.Unlock()
   309  
   310  	// Launch a goroutine to signal the chainSyncer that a new update is
   311  	// available. We do this in a new goroutine in order to avoid blocking
   312  	// the main loop of the rpc client.
   313  	go func() {
   314  		m.chainUpdateSignal <- struct{}{}
   315  	}()
   316  }
   317  
   318  // unwindBlock undoes the effect that a particular block had on the wallet's
   319  // internal utxo state.
   320  func (m *memWallet) unwindBlock(update *chainUpdate) {
   321  	undo := m.reorgJournal[update.blockHeight]
   322  
   323  	for _, utxo := range undo.utxosCreated {
   324  		delete(m.utxos, utxo)
   325  	}
   326  
   327  	for outPoint, utxo := range undo.utxosDestroyed {
   328  		m.utxos[outPoint] = utxo
   329  	}
   330  
   331  	delete(m.reorgJournal, update.blockHeight)
   332  }
   333  
   334  // newAddress returns a new address from the wallet's hd key chain.  It also
   335  // loads the address into the RPC client's transaction filter to ensure any
   336  // transactions that involve it are delivered via the notifications.
   337  func (m *memWallet) newAddress() (btcutil.Address, error) {
   338  	index := m.hdIndex
   339  
   340  	childKey, err := m.hdRoot.Derive(index)
   341  	if err != nil {
   342  		return nil, err
   343  	}
   344  	privKey, err := childKey.ECPrivKey()
   345  	if err != nil {
   346  		return nil, err
   347  	}
   348  
   349  	addr, err := keyToAddr(privKey, m.net)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  
   354  	err = m.rpc.LoadTxFilter(false, []btcutil.Address{addr}, nil)
   355  	if err != nil {
   356  		return nil, err
   357  	}
   358  
   359  	m.addrs[index] = addr
   360  
   361  	m.hdIndex++
   362  
   363  	return addr, nil
   364  }
   365  
   366  // NewAddress returns a fresh address spendable by the wallet.
   367  //
   368  // This function is safe for concurrent access.
   369  func (m *memWallet) NewAddress() (btcutil.Address, error) {
   370  	m.Lock()
   371  	defer m.Unlock()
   372  
   373  	return m.newAddress()
   374  }
   375  
   376  // fundTx attempts to fund a transaction sending amt bitcoin. The coins are
   377  // selected such that the final amount spent pays enough fees as dictated by the
   378  // passed fee rate. The passed fee rate should be expressed in
   379  // satoshis-per-byte. The transaction being funded can optionally include a
   380  // change output indicated by the change boolean.
   381  //
   382  // NOTE: The memWallet's mutex must be held when this function is called.
   383  func (m *memWallet) fundTx(tx *wire.MsgTx, amt btcutil.Amount,
   384  	feeRate btcutil.Amount, change bool) error {
   385  
   386  	const (
   387  		// spendSize is the largest number of bytes of a sigScript
   388  		// which spends a p2pkh output: OP_DATA_73 <sig> OP_DATA_33 <pubkey>
   389  		spendSize = 1 + 73 + 1 + 33
   390  	)
   391  
   392  	var (
   393  		amtSelected btcutil.Amount
   394  		txSize      int
   395  	)
   396  
   397  	for outPoint, utxo := range m.utxos {
   398  		// Skip any outputs that are still currently immature or are
   399  		// currently locked.
   400  		if !utxo.isMature(m.currentHeight) || utxo.isLocked {
   401  			continue
   402  		}
   403  
   404  		amtSelected += utxo.value
   405  
   406  		// Add the selected output to the transaction, updating the
   407  		// current tx size while accounting for the size of the future
   408  		// sigScript.
   409  		tx.AddTxIn(wire.NewTxIn(&outPoint, nil, nil))
   410  		txSize = tx.SerializeSize() + spendSize*len(tx.TxIn)
   411  
   412  		// Calculate the fee required for the txn at this point
   413  		// observing the specified fee rate. If we don't have enough
   414  		// coins from he current amount selected to pay the fee, then
   415  		// continue to grab more coins.
   416  		reqFee := btcutil.Amount(txSize * int(feeRate))
   417  		if amtSelected-reqFee < amt {
   418  			continue
   419  		}
   420  
   421  		// If we have any change left over and we should create a change
   422  		// output, then add an additional output to the transaction
   423  		// reserved for it.
   424  		changeVal := amtSelected - amt - reqFee
   425  		if changeVal > 0 && change {
   426  			addr, err := m.newAddress()
   427  			if err != nil {
   428  				return err
   429  			}
   430  			pkScript, err := txscript.PayToAddrScript(addr)
   431  			if err != nil {
   432  				return err
   433  			}
   434  			changeOutput := &wire.TxOut{
   435  				Value:    int64(changeVal),
   436  				PkScript: pkScript,
   437  			}
   438  			tx.AddTxOut(changeOutput)
   439  		}
   440  
   441  		return nil
   442  	}
   443  
   444  	// If we've reached this point, then coin selection failed due to an
   445  	// insufficient amount of coins.
   446  	return fmt.Errorf("not enough funds for coin selection")
   447  }
   448  
   449  // SendOutputs creates, then sends a transaction paying to the specified output
   450  // while observing the passed fee rate. The passed fee rate should be expressed
   451  // in satoshis-per-byte.
   452  func (m *memWallet) SendOutputs(outputs []*wire.TxOut,
   453  	feeRate btcutil.Amount) (*chainhash.Hash, error) {
   454  
   455  	tx, err := m.CreateTransaction(outputs, feeRate, true)
   456  	if err != nil {
   457  		return nil, err
   458  	}
   459  
   460  	return m.rpc.SendRawTransaction(tx, true)
   461  }
   462  
   463  // SendOutputsWithoutChange creates and sends a transaction that pays to the
   464  // specified outputs while observing the passed fee rate and ignoring a change
   465  // output. The passed fee rate should be expressed in sat/b.
   466  func (m *memWallet) SendOutputsWithoutChange(outputs []*wire.TxOut,
   467  	feeRate btcutil.Amount) (*chainhash.Hash, error) {
   468  
   469  	tx, err := m.CreateTransaction(outputs, feeRate, false)
   470  	if err != nil {
   471  		return nil, err
   472  	}
   473  
   474  	return m.rpc.SendRawTransaction(tx, true)
   475  }
   476  
   477  // CreateTransaction returns a fully signed transaction paying to the specified
   478  // outputs while observing the desired fee rate. The passed fee rate should be
   479  // expressed in satoshis-per-byte. The transaction being created can optionally
   480  // include a change output indicated by the change boolean.
   481  //
   482  // This function is safe for concurrent access.
   483  func (m *memWallet) CreateTransaction(outputs []*wire.TxOut,
   484  	feeRate btcutil.Amount, change bool) (*wire.MsgTx, error) {
   485  
   486  	m.Lock()
   487  	defer m.Unlock()
   488  
   489  	tx := wire.NewMsgTx(wire.TxVersion)
   490  
   491  	// Tally up the total amount to be sent in order to perform coin
   492  	// selection shortly below.
   493  	var outputAmt btcutil.Amount
   494  	for _, output := range outputs {
   495  		outputAmt += btcutil.Amount(output.Value)
   496  		tx.AddTxOut(output)
   497  	}
   498  
   499  	// Attempt to fund the transaction with spendable utxos.
   500  	if err := m.fundTx(tx, outputAmt, feeRate, change); err != nil {
   501  		return nil, err
   502  	}
   503  
   504  	// Populate all the selected inputs with valid sigScript for spending.
   505  	// Along the way record all outputs being spent in order to avoid a
   506  	// potential double spend.
   507  	spentOutputs := make([]*utxo, 0, len(tx.TxIn))
   508  	for i, txIn := range tx.TxIn {
   509  		outPoint := txIn.PreviousOutPoint
   510  		utxo := m.utxos[outPoint]
   511  
   512  		extendedKey, err := m.hdRoot.Derive(utxo.keyIndex)
   513  		if err != nil {
   514  			return nil, err
   515  		}
   516  
   517  		privKey, err := extendedKey.ECPrivKey()
   518  		if err != nil {
   519  			return nil, err
   520  		}
   521  
   522  		sigScript, err := txscript.SignatureScript(tx, i, utxo.pkScript,
   523  			txscript.SigHashAll, privKey, true)
   524  		if err != nil {
   525  			return nil, err
   526  		}
   527  
   528  		txIn.SignatureScript = sigScript
   529  
   530  		spentOutputs = append(spentOutputs, utxo)
   531  	}
   532  
   533  	// As these outputs are now being spent by this newly created
   534  	// transaction, mark the outputs are "locked". This action ensures
   535  	// these outputs won't be double spent by any subsequent transactions.
   536  	// These locked outputs can be freed via a call to UnlockOutputs.
   537  	for _, utxo := range spentOutputs {
   538  		utxo.isLocked = true
   539  	}
   540  
   541  	return tx, nil
   542  }
   543  
   544  // UnlockOutputs unlocks any outputs which were previously locked due to
   545  // being selected to fund a transaction via the CreateTransaction method.
   546  //
   547  // This function is safe for concurrent access.
   548  func (m *memWallet) UnlockOutputs(inputs []*wire.TxIn) {
   549  	m.Lock()
   550  	defer m.Unlock()
   551  
   552  	for _, input := range inputs {
   553  		utxo, ok := m.utxos[input.PreviousOutPoint]
   554  		if !ok {
   555  			continue
   556  		}
   557  
   558  		utxo.isLocked = false
   559  	}
   560  }
   561  
   562  // ConfirmedBalance returns the confirmed balance of the wallet.
   563  //
   564  // This function is safe for concurrent access.
   565  func (m *memWallet) ConfirmedBalance() btcutil.Amount {
   566  	m.RLock()
   567  	defer m.RUnlock()
   568  
   569  	var balance btcutil.Amount
   570  	for _, utxo := range m.utxos {
   571  		// Prevent any immature or locked outputs from contributing to
   572  		// the wallet's total confirmed balance.
   573  		if !utxo.isMature(m.currentHeight) || utxo.isLocked {
   574  			continue
   575  		}
   576  
   577  		balance += utxo.value
   578  	}
   579  
   580  	return balance
   581  }
   582  
   583  // keyToAddr maps the passed private to corresponding p2pkh address.
   584  func keyToAddr(key *btcec.PrivateKey, net *chaincfg.Params) (btcutil.Address, error) {
   585  	serializedKey := key.PubKey().SerializeCompressed()
   586  	pubKeyAddr, err := btcutil.NewAddressPubKey(serializedKey, net)
   587  	if err != nil {
   588  		return nil, err
   589  	}
   590  	return pubKeyAddr.AddressPubKeyHash(), nil
   591  }