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