decred.org/dcrwallet/v3@v3.1.0/internal/loader/loader.go (about)

     1  // Copyright (c) 2015-2018 The btcsuite developers
     2  // Copyright (c) 2017-2020 The Decred developers
     3  // Use of this source code is governed by an ISC
     4  // license that can be found in the LICENSE file.
     5  
     6  package loader
     7  
     8  import (
     9  	"context"
    10  	"net"
    11  	"os"
    12  	"path/filepath"
    13  	"sync"
    14  
    15  	"decred.org/dcrwallet/v3/errors"
    16  	"decred.org/dcrwallet/v3/wallet"
    17  	_ "decred.org/dcrwallet/v3/wallet/drivers/bdb" // driver loaded during init
    18  	"github.com/decred/dcrd/chaincfg/v3"
    19  	"github.com/decred/dcrd/dcrutil/v4"
    20  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    21  )
    22  
    23  const (
    24  	walletDbName = "wallet.db"
    25  	driver       = "bdb"
    26  )
    27  
    28  // Loader implements the creating of new and opening of existing wallets, while
    29  // providing a callback system for other subsystems to handle the loading of a
    30  // wallet.  This is primarely intended for use by the RPC servers, to enable
    31  // methods and services which require the wallet when the wallet is loaded by
    32  // another subsystem.
    33  //
    34  // Loader is safe for concurrent access.
    35  type Loader struct {
    36  	callbacks   []func(*wallet.Wallet)
    37  	chainParams *chaincfg.Params
    38  	dbDirPath   string
    39  	wallet      *wallet.Wallet
    40  	db          wallet.DB
    41  
    42  	stakeOptions            *StakeOptions
    43  	gapLimit                uint32
    44  	watchLast               uint32
    45  	accountGapLimit         int
    46  	disableCoinTypeUpgrades bool
    47  	allowHighFees           bool
    48  	manualTickets           bool
    49  	relayFee                dcrutil.Amount
    50  	mixSplitLimit           int
    51  
    52  	mu sync.Mutex
    53  
    54  	DialCSPPServer DialFunc
    55  }
    56  
    57  // StakeOptions contains the various options necessary for stake mining.
    58  type StakeOptions struct {
    59  	VotingEnabled       bool
    60  	AddressReuse        bool
    61  	VotingAddress       stdaddr.StakeAddress
    62  	PoolAddress         stdaddr.StakeAddress
    63  	PoolFees            float64
    64  	StakePoolColdExtKey string
    65  }
    66  
    67  // DialFunc provides a method to dial a network connection.
    68  // If the dialed network connection is secured by TLS, TLS
    69  // configuration is provided by the method, not the caller.
    70  type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
    71  
    72  // NewLoader constructs a Loader.
    73  func NewLoader(chainParams *chaincfg.Params, dbDirPath string, stakeOptions *StakeOptions, gapLimit uint32,
    74  	watchLast uint32, allowHighFees bool, relayFee dcrutil.Amount, accountGapLimit int,
    75  	disableCoinTypeUpgrades bool, manualTickets bool, mixSplitLimit int) *Loader {
    76  
    77  	return &Loader{
    78  		chainParams:             chainParams,
    79  		dbDirPath:               dbDirPath,
    80  		stakeOptions:            stakeOptions,
    81  		gapLimit:                gapLimit,
    82  		watchLast:               watchLast,
    83  		accountGapLimit:         accountGapLimit,
    84  		disableCoinTypeUpgrades: disableCoinTypeUpgrades,
    85  		allowHighFees:           allowHighFees,
    86  		manualTickets:           manualTickets,
    87  		relayFee:                relayFee,
    88  		mixSplitLimit:           mixSplitLimit,
    89  	}
    90  }
    91  
    92  // onLoaded executes each added callback and prevents loader from loading any
    93  // additional wallets.  Requires mutex to be locked.
    94  func (l *Loader) onLoaded(w *wallet.Wallet, db wallet.DB) {
    95  	for _, fn := range l.callbacks {
    96  		fn(w)
    97  	}
    98  
    99  	l.wallet = w
   100  	l.db = db
   101  	l.callbacks = nil // not needed anymore
   102  }
   103  
   104  // RunAfterLoad adds a function to be executed when the loader creates or opens
   105  // a wallet.  Functions are executed in a single goroutine in the order they are
   106  // added.
   107  func (l *Loader) RunAfterLoad(fn func(*wallet.Wallet)) {
   108  	l.mu.Lock()
   109  	if l.wallet != nil {
   110  		w := l.wallet
   111  		l.mu.Unlock()
   112  		fn(w)
   113  	} else {
   114  		l.callbacks = append(l.callbacks, fn)
   115  		l.mu.Unlock()
   116  	}
   117  }
   118  
   119  // CreateWatchingOnlyWallet creates a new watch-only wallet using the provided
   120  // extended public key and public passphrase.
   121  func (l *Loader) CreateWatchingOnlyWallet(ctx context.Context, extendedPubKey string, pubPass []byte) (w *wallet.Wallet, err error) {
   122  	const op errors.Op = "loader.CreateWatchingOnlyWallet"
   123  
   124  	defer l.mu.Unlock()
   125  	l.mu.Lock()
   126  
   127  	if l.wallet != nil {
   128  		return nil, errors.E(op, errors.Exist, "wallet already loaded")
   129  	}
   130  
   131  	// Ensure that the network directory exists.
   132  	if fi, err := os.Stat(l.dbDirPath); err != nil {
   133  		if os.IsNotExist(err) {
   134  			// Attempt data directory creation
   135  			if err = os.MkdirAll(l.dbDirPath, 0700); err != nil {
   136  				return nil, errors.E(op, err)
   137  			}
   138  		} else {
   139  			return nil, errors.E(op, err)
   140  		}
   141  	} else {
   142  		if !fi.IsDir() {
   143  			return nil, errors.E(op, errors.Invalid, errors.Errorf("%q is not a directory", l.dbDirPath))
   144  		}
   145  	}
   146  
   147  	dbPath := filepath.Join(l.dbDirPath, walletDbName)
   148  	exists, err := fileExists(dbPath)
   149  	if err != nil {
   150  		return nil, errors.E(op, err)
   151  	}
   152  	if exists {
   153  		return nil, errors.E(op, errors.Exist, "wallet already exists")
   154  	}
   155  
   156  	// At this point it is asserted that there is no existing database file, and
   157  	// deleting anything won't destroy a wallet in use.  Defer a function that
   158  	// attempts to remove any written database file if this function errors.
   159  	defer func() {
   160  		if err != nil {
   161  			_ = os.Remove(dbPath)
   162  		}
   163  	}()
   164  
   165  	// Create the wallet database backed by bolt db.
   166  	err = os.MkdirAll(l.dbDirPath, 0700)
   167  	if err != nil {
   168  		return nil, errors.E(op, err)
   169  	}
   170  	db, err := wallet.CreateDB(driver, dbPath)
   171  	if err != nil {
   172  		return nil, errors.E(op, err)
   173  	}
   174  
   175  	// Initialize the watch-only database for the wallet before opening.
   176  	err = wallet.CreateWatchOnly(ctx, db, extendedPubKey, pubPass, l.chainParams)
   177  	if err != nil {
   178  		return nil, errors.E(op, err)
   179  	}
   180  
   181  	// Open the watch-only wallet.
   182  	so := l.stakeOptions
   183  	cfg := &wallet.Config{
   184  		DB:                      db,
   185  		PubPassphrase:           pubPass,
   186  		VotingEnabled:           so.VotingEnabled,
   187  		AddressReuse:            so.AddressReuse,
   188  		VotingAddress:           so.VotingAddress,
   189  		PoolAddress:             so.PoolAddress,
   190  		PoolFees:                so.PoolFees,
   191  		GapLimit:                l.gapLimit,
   192  		WatchLast:               l.watchLast,
   193  		AccountGapLimit:         l.accountGapLimit,
   194  		DisableCoinTypeUpgrades: l.disableCoinTypeUpgrades,
   195  		StakePoolColdExtKey:     so.StakePoolColdExtKey,
   196  		ManualTickets:           l.manualTickets,
   197  		AllowHighFees:           l.allowHighFees,
   198  		RelayFee:                l.relayFee,
   199  		MixSplitLimit:           l.mixSplitLimit,
   200  		Params:                  l.chainParams,
   201  	}
   202  	w, err = wallet.Open(ctx, cfg)
   203  	if err != nil {
   204  		return nil, errors.E(op, err)
   205  	}
   206  
   207  	l.onLoaded(w, db)
   208  	return w, nil
   209  }
   210  
   211  // CreateNewWallet creates a new wallet using the provided public and private
   212  // passphrases.  The seed is optional.  If non-nil, addresses are derived from
   213  // this seed.  If nil, a secure random seed is generated.
   214  func (l *Loader) CreateNewWallet(ctx context.Context, pubPassphrase, privPassphrase, seed []byte) (w *wallet.Wallet, err error) {
   215  	const op errors.Op = "loader.CreateNewWallet"
   216  
   217  	defer l.mu.Unlock()
   218  	l.mu.Lock()
   219  
   220  	if l.wallet != nil {
   221  		return nil, errors.E(op, errors.Exist, "wallet already opened")
   222  	}
   223  
   224  	// Ensure that the network directory exists.
   225  	if fi, err := os.Stat(l.dbDirPath); err != nil {
   226  		if os.IsNotExist(err) {
   227  			// Attempt data directory creation
   228  			if err = os.MkdirAll(l.dbDirPath, 0700); err != nil {
   229  				return nil, errors.E(op, err)
   230  			}
   231  		} else {
   232  			return nil, errors.E(op, err)
   233  		}
   234  	} else {
   235  		if !fi.IsDir() {
   236  			return nil, errors.E(op, errors.Errorf("%q is not a directory", l.dbDirPath))
   237  		}
   238  	}
   239  
   240  	dbPath := filepath.Join(l.dbDirPath, walletDbName)
   241  	exists, err := fileExists(dbPath)
   242  	if err != nil {
   243  		return nil, errors.E(op, err)
   244  	}
   245  	if exists {
   246  		return nil, errors.E(op, errors.Exist, "wallet DB exists")
   247  	}
   248  
   249  	// At this point it is asserted that there is no existing database file, and
   250  	// deleting anything won't destroy a wallet in use.  Defer a function that
   251  	// attempts to remove any written database file if this function errors.
   252  	defer func() {
   253  		if err != nil {
   254  			_ = os.Remove(dbPath)
   255  		}
   256  	}()
   257  
   258  	// Create the wallet database backed by bolt db.
   259  	err = os.MkdirAll(l.dbDirPath, 0700)
   260  	if err != nil {
   261  		return nil, errors.E(op, err)
   262  	}
   263  	db, err := wallet.CreateDB(driver, dbPath)
   264  	if err != nil {
   265  		return nil, errors.E(op, err)
   266  	}
   267  
   268  	// Initialize the newly created database for the wallet before opening.
   269  	err = wallet.Create(ctx, db, pubPassphrase, privPassphrase, seed, l.chainParams)
   270  	if err != nil {
   271  		return nil, errors.E(op, err)
   272  	}
   273  
   274  	// Open the newly-created wallet.
   275  	so := l.stakeOptions
   276  	cfg := &wallet.Config{
   277  		DB:                      db,
   278  		PubPassphrase:           pubPassphrase,
   279  		VotingEnabled:           so.VotingEnabled,
   280  		AddressReuse:            so.AddressReuse,
   281  		VotingAddress:           so.VotingAddress,
   282  		PoolAddress:             so.PoolAddress,
   283  		PoolFees:                so.PoolFees,
   284  		GapLimit:                l.gapLimit,
   285  		WatchLast:               l.watchLast,
   286  		AccountGapLimit:         l.accountGapLimit,
   287  		DisableCoinTypeUpgrades: l.disableCoinTypeUpgrades,
   288  		StakePoolColdExtKey:     so.StakePoolColdExtKey,
   289  		ManualTickets:           l.manualTickets,
   290  		AllowHighFees:           l.allowHighFees,
   291  		RelayFee:                l.relayFee,
   292  		Params:                  l.chainParams,
   293  	}
   294  	w, err = wallet.Open(ctx, cfg)
   295  	if err != nil {
   296  		return nil, errors.E(op, err)
   297  	}
   298  
   299  	l.onLoaded(w, db)
   300  	return w, nil
   301  }
   302  
   303  // OpenExistingWallet opens the wallet from the loader's wallet database path
   304  // and the public passphrase.  If the loader is being called by a context where
   305  // standard input prompts may be used during wallet upgrades, setting
   306  // canConsolePrompt will enable these prompts.
   307  func (l *Loader) OpenExistingWallet(ctx context.Context, pubPassphrase []byte) (w *wallet.Wallet, rerr error) {
   308  	const op errors.Op = "loader.OpenExistingWallet"
   309  
   310  	defer l.mu.Unlock()
   311  	l.mu.Lock()
   312  
   313  	if l.wallet != nil {
   314  		return nil, errors.E(op, errors.Exist, "wallet already opened")
   315  	}
   316  
   317  	// Open the database using the boltdb backend.
   318  	dbPath := filepath.Join(l.dbDirPath, walletDbName)
   319  	l.mu.Unlock()
   320  	db, err := wallet.OpenDB(driver, dbPath)
   321  	l.mu.Lock()
   322  
   323  	if err != nil {
   324  		log.Errorf("Failed to open database: %v", err)
   325  		return nil, errors.E(op, err)
   326  	}
   327  	// If this function does not return to completion the database must be
   328  	// closed.  Otherwise, because the database is locked on opens, any
   329  	// other attempts to open the wallet will hang, and there is no way to
   330  	// recover since this db handle would be leaked.
   331  	defer func() {
   332  		if rerr != nil {
   333  			db.Close()
   334  		}
   335  	}()
   336  
   337  	so := l.stakeOptions
   338  	cfg := &wallet.Config{
   339  		DB:                      db,
   340  		PubPassphrase:           pubPassphrase,
   341  		VotingEnabled:           so.VotingEnabled,
   342  		AddressReuse:            so.AddressReuse,
   343  		VotingAddress:           so.VotingAddress,
   344  		PoolAddress:             so.PoolAddress,
   345  		PoolFees:                so.PoolFees,
   346  		GapLimit:                l.gapLimit,
   347  		WatchLast:               l.watchLast,
   348  		AccountGapLimit:         l.accountGapLimit,
   349  		DisableCoinTypeUpgrades: l.disableCoinTypeUpgrades,
   350  		StakePoolColdExtKey:     so.StakePoolColdExtKey,
   351  		ManualTickets:           l.manualTickets,
   352  		AllowHighFees:           l.allowHighFees,
   353  		RelayFee:                l.relayFee,
   354  		MixSplitLimit:           l.mixSplitLimit,
   355  		Params:                  l.chainParams,
   356  	}
   357  	w, err = wallet.Open(ctx, cfg)
   358  	if err != nil {
   359  		return nil, errors.E(op, err)
   360  	}
   361  
   362  	l.onLoaded(w, db)
   363  	return w, nil
   364  }
   365  
   366  // DbDirPath returns the Loader's database directory path
   367  func (l *Loader) DbDirPath() string {
   368  	return l.dbDirPath
   369  }
   370  
   371  // WalletExists returns whether a file exists at the loader's database path.
   372  // This may return an error for unexpected I/O failures.
   373  func (l *Loader) WalletExists() (bool, error) {
   374  	const op errors.Op = "loader.WalletExists"
   375  	dbPath := filepath.Join(l.dbDirPath, walletDbName)
   376  	exists, err := fileExists(dbPath)
   377  	if err != nil {
   378  		return false, errors.E(op, err)
   379  	}
   380  	return exists, nil
   381  }
   382  
   383  // LoadedWallet returns the loaded wallet, if any, and a bool for whether the
   384  // wallet has been loaded or not.  If true, the wallet pointer should be safe to
   385  // dereference.
   386  func (l *Loader) LoadedWallet() (*wallet.Wallet, bool) {
   387  	l.mu.Lock()
   388  	w := l.wallet
   389  	l.mu.Unlock()
   390  	return w, w != nil
   391  }
   392  
   393  // UnloadWallet stops the loaded wallet, if any, and closes the wallet database.
   394  // Returns with errors.Invalid if the wallet has not been loaded with
   395  // CreateNewWallet or LoadExistingWallet.  The Loader may be reused if this
   396  // function returns without error.
   397  func (l *Loader) UnloadWallet() error {
   398  	const op errors.Op = "loader.UnloadWallet"
   399  
   400  	defer l.mu.Unlock()
   401  	l.mu.Lock()
   402  
   403  	if l.wallet == nil {
   404  		return errors.E(op, errors.Invalid, "wallet is unopened")
   405  	}
   406  
   407  	err := l.db.Close()
   408  	if err != nil {
   409  		return errors.E(op, err)
   410  	}
   411  
   412  	l.wallet = nil
   413  	l.db = nil
   414  	return nil
   415  }
   416  
   417  // NetworkBackend returns the associated wallet network backend, if any, and a
   418  // bool describing whether a non-nil network backend was set.
   419  func (l *Loader) NetworkBackend() (n wallet.NetworkBackend, ok bool) {
   420  	l.mu.Lock()
   421  	if l.wallet != nil {
   422  		n, _ = l.wallet.NetworkBackend()
   423  	}
   424  	l.mu.Unlock()
   425  	return n, n != nil
   426  }
   427  
   428  func fileExists(filePath string) (bool, error) {
   429  	_, err := os.Stat(filePath)
   430  	if err != nil {
   431  		if os.IsNotExist(err) {
   432  			return false, nil
   433  		}
   434  		return false, err
   435  	}
   436  	return true, nil
   437  }