decred.org/dcrdex@v1.0.3/client/asset/btc/spv.go (about)

     1  package btc
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"sync/atomic"
    10  	"time"
    11  
    12  	"decred.org/dcrdex/client/asset"
    13  	"decred.org/dcrdex/dex"
    14  	"github.com/btcsuite/btcd/btcec/v2"
    15  	"github.com/btcsuite/btcd/btcutil"
    16  	"github.com/btcsuite/btcd/chaincfg"
    17  	"github.com/btcsuite/btcd/chaincfg/chainhash"
    18  	"github.com/btcsuite/btcd/wire"
    19  	"github.com/btcsuite/btclog"
    20  	"github.com/btcsuite/btcwallet/chain"
    21  	"github.com/btcsuite/btcwallet/waddrmgr"
    22  	"github.com/btcsuite/btcwallet/wallet"
    23  	"github.com/btcsuite/btcwallet/wallet/txauthor"
    24  	"github.com/btcsuite/btcwallet/walletdb"
    25  	"github.com/btcsuite/btcwallet/wtxmgr"
    26  	"github.com/jrick/logrotate/rotator"
    27  	"github.com/lightninglabs/neutrino"
    28  )
    29  
    30  const (
    31  	dbTimeout = 20 * time.Second
    32  )
    33  
    34  // btcSPVWallet implements BTCWallet for Bitcoin.
    35  type btcSPVWallet struct {
    36  	*wallet.Wallet
    37  	chainParams *chaincfg.Params
    38  	log         dex.Logger
    39  	dir         string
    40  
    41  	// Below fields are populated in Start.
    42  	loader      *wallet.Loader
    43  	chainClient *chain.NeutrinoClient
    44  	cl          *neutrino.ChainService
    45  	neutrinoDB  walletdb.DB
    46  
    47  	// rescanStarting is set while reloading the wallet and dropping
    48  	// transactions from the wallet db.
    49  	rescanStarting uint32 // atomic
    50  
    51  	peerManager *SPVPeerManager
    52  }
    53  
    54  var _ BTCWallet = (*btcSPVWallet)(nil)
    55  
    56  // createSPVWallet creates a new SPV wallet.
    57  func createSPVWallet(privPass []byte, seed []byte, bday time.Time, walletDir string, log dex.Logger, extIdx, intIdx uint32, net *chaincfg.Params) error {
    58  	if err := logNeutrino(walletDir); err != nil {
    59  		return fmt.Errorf("error initializing btcwallet+neutrino logging: %w", err)
    60  	}
    61  
    62  	loader := wallet.NewLoader(net, walletDir, true, dbTimeout, 250)
    63  
    64  	pubPass := []byte(wallet.InsecurePubPassphrase)
    65  
    66  	// CreateWallet adds a -48 hrs buffer on the bday during creation.
    67  	btcw, err := loader.CreateNewWallet(pubPass, privPass, seed, bday)
    68  	if err != nil {
    69  		return fmt.Errorf("CreateNewWallet error: %w", err)
    70  	}
    71  
    72  	bailOnWallet := func() {
    73  		if err := loader.UnloadWallet(); err != nil {
    74  			log.Errorf("Error unloading wallet after createSPVWallet error: %v", err)
    75  		}
    76  	}
    77  
    78  	if extIdx > 0 || intIdx > 0 {
    79  		err = extendAddresses(extIdx, intIdx, btcw)
    80  		if err != nil {
    81  			bailOnWallet()
    82  			return fmt.Errorf("failed to set starting address indexes: %w", err)
    83  		}
    84  	}
    85  
    86  	// The chain service DB
    87  	neutrinoDBPath := filepath.Join(walletDir, neutrinoDBName)
    88  	db, err := walletdb.Create("bdb", neutrinoDBPath, true, dbTimeout)
    89  	if err != nil {
    90  		bailOnWallet()
    91  		return fmt.Errorf("unable to create neutrino db at %q: %w", neutrinoDBPath, err)
    92  	}
    93  	if err = db.Close(); err != nil {
    94  		bailOnWallet()
    95  		return fmt.Errorf("error closing newly created wallet database: %w", err)
    96  	}
    97  
    98  	if err := loader.UnloadWallet(); err != nil {
    99  		return fmt.Errorf("error unloading wallet: %w", err)
   100  	}
   101  
   102  	return nil
   103  }
   104  
   105  // openSPVWallet is the BTCWalletConstructor for Bitcoin.
   106  func openSPVWallet(dir string, cfg *WalletConfig,
   107  	chainParams *chaincfg.Params, log dex.Logger) BTCWallet {
   108  
   109  	w := &btcSPVWallet{
   110  		dir:         dir,
   111  		chainParams: chainParams,
   112  		log:         log,
   113  	}
   114  	return w
   115  }
   116  
   117  // AccountInfo returns the account information of the wallet for use by the
   118  // exchange wallet.
   119  func (w *btcSPVWallet) AccountInfo() XCWalletAccount {
   120  	return XCWalletAccount{
   121  		AccountName:   defaultAcctName,
   122  		AccountNumber: defaultAcctNum,
   123  	}
   124  }
   125  
   126  func (w *btcSPVWallet) Birthday() time.Time {
   127  	return w.Manager.Birthday()
   128  }
   129  
   130  func (w *btcSPVWallet) updateDBBirthday(bday time.Time) error {
   131  	btcw, isLoaded := w.loader.LoadedWallet()
   132  	if !isLoaded {
   133  		return fmt.Errorf("wallet not loaded")
   134  	}
   135  	return walletdb.Update(btcw.Database(), func(dbtx walletdb.ReadWriteTx) error {
   136  		ns := dbtx.ReadWriteBucket(wAddrMgrBkt)
   137  		return btcw.Manager.SetBirthday(ns, bday)
   138  	})
   139  }
   140  
   141  // Start initializes the *btcwallet.Wallet and its supporting players and
   142  // starts syncing.
   143  func (w *btcSPVWallet) Start() (SPVService, error) {
   144  	if err := logNeutrino(w.dir); err != nil {
   145  		return nil, fmt.Errorf("error initializing btcwallet+neutrino logging: %v", err)
   146  	}
   147  	// timeout and recoverWindow arguments borrowed from btcwallet directly.
   148  	w.loader = wallet.NewLoader(w.chainParams, w.dir, true, dbTimeout, 250)
   149  
   150  	exists, err := w.loader.WalletExists()
   151  	if err != nil {
   152  		return nil, fmt.Errorf("error verifying wallet existence: %v", err)
   153  	}
   154  	if !exists {
   155  		return nil, errors.New("wallet not found")
   156  	}
   157  
   158  	w.log.Debug("Starting native BTC wallet...")
   159  	btcw, err := w.loader.OpenExistingWallet([]byte(wallet.InsecurePubPassphrase), false)
   160  	if err != nil {
   161  		return nil, fmt.Errorf("couldn't load wallet: %w", err)
   162  	}
   163  
   164  	errCloser := dex.NewErrorCloser()
   165  	defer errCloser.Done(w.log)
   166  	errCloser.Add(w.loader.UnloadWallet)
   167  
   168  	neutrinoDBPath := filepath.Join(w.dir, neutrinoDBName)
   169  	w.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true, dbTimeout)
   170  	if err != nil {
   171  		return nil, fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err)
   172  	}
   173  	errCloser.Add(w.neutrinoDB.Close)
   174  
   175  	w.log.Debug("Starting neutrino chain service...")
   176  	w.cl, err = neutrino.NewChainService(neutrino.Config{
   177  		DataDir:       w.dir,
   178  		Database:      w.neutrinoDB,
   179  		ChainParams:   *w.chainParams,
   180  		PersistToDisk: true, // keep cfilter headers on disk for efficient rescanning
   181  		// AddPeers:      addPeers,
   182  		// ConnectPeers:  connectPeers,
   183  		// WARNING: PublishTransaction currently uses the entire duration
   184  		// because if an external bug, but even if the resolved, a typical
   185  		// inv/getdata round trip is ~4 seconds, so we set this so neutrino does
   186  		// not cancel queries too readily.
   187  		BroadcastTimeout: 6 * time.Second,
   188  	})
   189  	if err != nil {
   190  		return nil, fmt.Errorf("couldn't create Neutrino ChainService: %w", err)
   191  	}
   192  	errCloser.Add(w.cl.Stop)
   193  
   194  	var defaultPeers []string
   195  	switch w.chainParams.Net {
   196  	case wire.TestNet3:
   197  		defaultPeers = []string{"dex-test.ssgen.io:18333"}
   198  	case wire.TestNet, wire.SimNet: // plain "wire.TestNet" is regnet!
   199  		defaultPeers = []string{"127.0.0.1:20575"}
   200  	}
   201  	peerManager := NewSPVPeerManager(&btcChainService{w.cl}, defaultPeers, w.dir, w.log, w.chainParams.DefaultPort)
   202  	w.peerManager = peerManager
   203  
   204  	w.chainClient = chain.NewNeutrinoClient(w.chainParams, w.cl)
   205  	w.Wallet = btcw
   206  
   207  	if err = w.chainClient.Start(); err != nil { // lazily starts connmgr
   208  		return nil, fmt.Errorf("couldn't start Neutrino client: %v", err)
   209  	}
   210  
   211  	w.log.Info("Synchronizing wallet with network...")
   212  	btcw.SynchronizeRPC(w.chainClient)
   213  
   214  	errCloser.Success()
   215  
   216  	w.peerManager.ConnectToInitialWalletPeers()
   217  
   218  	return &btcChainService{w.cl}, nil
   219  }
   220  
   221  // Stop stops the wallet and database threads.
   222  func (w *btcSPVWallet) Stop() {
   223  	w.log.Info("Unloading wallet")
   224  	if err := w.loader.UnloadWallet(); err != nil {
   225  		w.log.Errorf("UnloadWallet error: %v", err)
   226  	}
   227  	if w.chainClient != nil {
   228  		w.log.Trace("Stopping neutrino client chain interface")
   229  		w.chainClient.Stop()
   230  		w.chainClient.WaitForShutdown()
   231  	}
   232  	w.log.Trace("Stopping neutrino chain sync service")
   233  	if err := w.cl.Stop(); err != nil {
   234  		w.log.Errorf("error stopping neutrino chain service: %v", err)
   235  	}
   236  	w.log.Trace("Stopping neutrino DB.")
   237  	if err := w.neutrinoDB.Close(); err != nil {
   238  		w.log.Errorf("wallet db close error: %v", err)
   239  	}
   240  
   241  	// NOTE: Do we need w.Wallet.Stop()
   242  
   243  	w.log.Info("SPV wallet closed")
   244  }
   245  
   246  // RescanAsync initiates a full wallet recovery (used address discovery
   247  // and transaction scanning) by stopping the btcwallet, dropping the transaction
   248  // history from the wallet db, resetting the synced-to height of the wallet
   249  // manager, restarting the wallet and its chain client, and finally commanding
   250  // the wallet to resynchronize, which starts asynchronous wallet recovery.
   251  // Progress of the rescan should be monitored with syncStatus. During the rescan
   252  // wallet balances and known transactions may not be reported accurately or
   253  // located. The SPVService is not stopped, so most spvWallet methods will
   254  // continue to work without error, but methods using the btcWallet will likely
   255  // return incorrect results or errors.
   256  func (w *btcSPVWallet) RescanAsync() error {
   257  	if !atomic.CompareAndSwapUint32(&w.rescanStarting, 0, 1) {
   258  		w.log.Error("rescan already in progress")
   259  	}
   260  	defer atomic.StoreUint32(&w.rescanStarting, 0)
   261  	w.log.Info("Stopping wallet and chain client...")
   262  	w.Wallet.Stop() // stops Wallet and chainClient (not chainService)
   263  	w.Wallet.WaitForShutdown()
   264  	w.chainClient.WaitForShutdown()
   265  
   266  	w.ForceRescan()
   267  
   268  	w.log.Info("Starting wallet...")
   269  	w.Wallet.Start()
   270  
   271  	if err := w.chainClient.Start(); err != nil {
   272  		return fmt.Errorf("couldn't start Neutrino client: %v", err)
   273  	}
   274  
   275  	w.log.Info("Synchronizing wallet with network...")
   276  	w.Wallet.SynchronizeRPC(w.chainClient)
   277  	return nil
   278  }
   279  
   280  // ForceRescan forces a full rescan with active address discovery on wallet
   281  // restart by dropping the complete transaction history and setting the
   282  // "synced to" field to nil. See the btcwallet/cmd/dropwtxmgr app for more
   283  // information.
   284  func (w *btcSPVWallet) ForceRescan() {
   285  	wdb := w.Wallet.Database()
   286  
   287  	w.log.Info("Dropping transaction history to perform full rescan...")
   288  	err := wallet.DropTransactionHistory(wdb, false)
   289  	if err != nil {
   290  		w.log.Errorf("Failed to drop wallet transaction history: %v", err)
   291  		// Continue to attempt restarting the wallet anyway.
   292  	}
   293  
   294  	err = walletdb.Update(wdb, func(dbtx walletdb.ReadWriteTx) error {
   295  		ns := dbtx.ReadWriteBucket(wAddrMgrBkt)      // it'll be fine
   296  		return w.Wallet.Manager.SetSyncedTo(ns, nil) // never synced, forcing recover from birthday
   297  	})
   298  	if err != nil {
   299  		w.log.Errorf("Failed to reset wallet manager sync height: %v", err)
   300  	}
   301  }
   302  
   303  // WalletTransaction pulls the transaction from the database.
   304  func (w *btcSPVWallet) WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) {
   305  	details, err := wallet.UnstableAPI(w.Wallet).TxDetails(txHash)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	if details == nil {
   310  		return nil, WalletTransactionNotFound
   311  	}
   312  
   313  	return details, nil
   314  }
   315  
   316  func (w *btcSPVWallet) SyncedTo() waddrmgr.BlockStamp {
   317  	return w.Wallet.Manager.SyncedTo()
   318  }
   319  
   320  // getWalletBirthdayBlock retrieves the wallet's birthday block.
   321  //
   322  // NOTE: The wallet birthday block hash is NOT SET until the chain service
   323  // passes the birthday block and the wallet looks it up based on the birthday
   324  // Time and the downloaded block headers.
   325  // This is presently unused, but I have plans for it with a wallet rescan.
   326  // func (w *btcSPVWallet) getWalletBirthdayBlock() (*waddrmgr.BlockStamp, error) {
   327  // 	var birthdayBlock waddrmgr.BlockStamp
   328  // 	err := walletdb.View(w.Database(), func(dbtx walletdb.ReadTx) error {
   329  // 		ns := dbtx.ReadBucket([]byte("waddrmgr")) // it'll be fine
   330  // 		var err error
   331  // 		birthdayBlock, _, err = w.Manager.BirthdayBlock(ns)
   332  // 		return err
   333  // 	})
   334  // 	if err != nil {
   335  // 		return nil, err // sadly, waddrmgr.ErrBirthdayBlockNotSet is expected during most of the chain sync
   336  // 	}
   337  // 	return &birthdayBlock, nil
   338  // }
   339  
   340  // SignTx signs the transaction inputs.
   341  func (w *btcSPVWallet) SignTx(tx *wire.MsgTx) error {
   342  	var prevPkScripts [][]byte
   343  	var inputValues []btcutil.Amount
   344  	for _, txIn := range tx.TxIn {
   345  		// NOTE: The BitcoinCash implementation of BTCWallet ONLY produces the
   346  		// *wire.TxOut.
   347  		_, txOut, _, _, err := w.Wallet.FetchInputInfo(&txIn.PreviousOutPoint)
   348  		if err != nil {
   349  			return err
   350  		}
   351  		inputValues = append(inputValues, btcutil.Amount(txOut.Value))
   352  		prevPkScripts = append(prevPkScripts, txOut.PkScript)
   353  		// Zero the previous witness and signature script or else
   354  		// AddAllInputScripts does some weird stuff.
   355  		txIn.SignatureScript = nil
   356  		txIn.Witness = nil
   357  	}
   358  	return txauthor.AddAllInputScripts(tx, prevPkScripts, inputValues, &secretSource{w, w.chainParams})
   359  }
   360  
   361  func (w *btcSPVWallet) BlockNotifications(ctx context.Context) <-chan *BlockNotification {
   362  	cl := w.Wallet.NtfnServer.TransactionNotifications()
   363  	ch := make(chan *BlockNotification, 1)
   364  	go func() {
   365  		defer cl.Done()
   366  		for {
   367  			select {
   368  			case note := <-cl.C:
   369  				if len(note.AttachedBlocks) > 0 {
   370  					lastBlock := note.AttachedBlocks[len(note.AttachedBlocks)-1]
   371  					select {
   372  					case ch <- &BlockNotification{
   373  						Hash:   *lastBlock.Hash,
   374  						Height: lastBlock.Height,
   375  					}:
   376  					default:
   377  					}
   378  				}
   379  			case <-ctx.Done():
   380  				return
   381  			}
   382  		}
   383  	}()
   384  	return ch
   385  }
   386  
   387  func (w *btcSPVWallet) AddPeer(addr string) error {
   388  	return w.peerManager.AddPeer(addr)
   389  }
   390  
   391  func (w *btcSPVWallet) RemovePeer(addr string) error {
   392  	return w.peerManager.RemovePeer(addr)
   393  }
   394  
   395  func (w *btcSPVWallet) Peers() ([]*asset.WalletPeer, error) {
   396  	return w.peerManager.Peers()
   397  }
   398  
   399  func (w *btcSPVWallet) GetTransactions(startHeight, endHeight int32, accountName string, cancel <-chan struct{}) (*wallet.GetTransactionsResult, error) {
   400  	startID := wallet.NewBlockIdentifierFromHeight(startHeight)
   401  	endID := wallet.NewBlockIdentifierFromHeight(endHeight)
   402  	return w.Wallet.GetTransactions(startID, endID, accountName, cancel)
   403  }
   404  
   405  // secretSource is used to locate keys and redemption scripts while signing a
   406  // transaction. secretSource satisfies the txauthor.SecretsSource interface.
   407  type secretSource struct {
   408  	w           *btcSPVWallet
   409  	chainParams *chaincfg.Params
   410  }
   411  
   412  // ChainParams returns the chain parameters.
   413  func (s *secretSource) ChainParams() *chaincfg.Params {
   414  	return s.chainParams
   415  }
   416  
   417  // GetKey fetches a private key for the specified address.
   418  func (s *secretSource) GetKey(addr btcutil.Address) (*btcec.PrivateKey, bool, error) {
   419  	ma, err := s.w.Wallet.AddressInfo(addr)
   420  	if err != nil {
   421  		return nil, false, err
   422  	}
   423  
   424  	mpka, ok := ma.(waddrmgr.ManagedPubKeyAddress)
   425  	if !ok {
   426  		e := fmt.Errorf("managed address type for %v is `%T` but "+
   427  			"want waddrmgr.ManagedPubKeyAddress", addr, ma)
   428  		return nil, false, e
   429  	}
   430  
   431  	privKey, err := mpka.PrivKey()
   432  	if err != nil {
   433  		return nil, false, err
   434  	}
   435  	return privKey, ma.Compressed(), nil
   436  }
   437  
   438  // GetScript fetches the redemption script for the specified p2sh/p2wsh address.
   439  func (s *secretSource) GetScript(addr btcutil.Address) ([]byte, error) {
   440  	ma, err := s.w.Wallet.AddressInfo(addr)
   441  	if err != nil {
   442  		return nil, err
   443  	}
   444  
   445  	msa, ok := ma.(waddrmgr.ManagedScriptAddress)
   446  	if !ok {
   447  		e := fmt.Errorf("managed address type for %v is `%T` but "+
   448  			"want waddrmgr.ManagedScriptAddress", addr, ma)
   449  		return nil, e
   450  	}
   451  	return msa.Script()
   452  }
   453  
   454  var (
   455  	// loggingInited will be set when the log rotator has been initialized.
   456  	loggingInited uint32
   457  )
   458  
   459  // logRotator initializes a rotating file logger.
   460  func logRotator(dir string) (*rotator.Rotator, error) {
   461  	const maxLogRolls = 8
   462  	logDir := filepath.Join(dir, logDirName)
   463  	if err := os.MkdirAll(logDir, 0744); err != nil {
   464  		return nil, fmt.Errorf("error creating log directory: %w", err)
   465  	}
   466  
   467  	logFilename := filepath.Join(logDir, logFileName)
   468  	return rotator.New(logFilename, 32*1024, false, maxLogRolls)
   469  }
   470  
   471  // logNeutrino initializes logging in the neutrino + wallet packages. Logging
   472  // only has to be initialized once, so an atomic flag is used internally to
   473  // return early on subsequent invocations.
   474  //
   475  // In theory, the rotating file logger must be Closed at some point, but
   476  // there are concurrency issues with that since btcd and btcwallet have
   477  // unsupervised goroutines still running after shutdown. So we leave the rotator
   478  // running at the risk of losing some logs.
   479  func logNeutrino(walletDir string) error {
   480  	if !atomic.CompareAndSwapUint32(&loggingInited, 0, 1) {
   481  		return nil
   482  	}
   483  
   484  	logSpinner, err := logRotator(walletDir)
   485  	if err != nil {
   486  		return fmt.Errorf("error initializing log rotator: %w", err)
   487  	}
   488  
   489  	backendLog := btclog.NewBackend(logSpinner)
   490  
   491  	logger := func(name string, lvl btclog.Level) btclog.Logger {
   492  		l := backendLog.Logger(name)
   493  		l.SetLevel(lvl)
   494  		return l
   495  	}
   496  
   497  	neutrino.UseLogger(logger("NTRNO", btclog.LevelDebug))
   498  	wallet.UseLogger(logger("BTCW", btclog.LevelInfo))
   499  	wtxmgr.UseLogger(logger("TXMGR", btclog.LevelInfo))
   500  	chain.UseLogger(logger("CHAIN", btclog.LevelInfo))
   501  
   502  	return nil
   503  }