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

     1  // Copyright (c) 2013-2015 The btcsuite developers
     2  // Copyright (c) 2015-2018 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 main
     7  
     8  import (
     9  	"bufio"
    10  	"context"
    11  	"fmt"
    12  	"net"
    13  	"net/http"
    14  	_ "net/http/pprof"
    15  	"os"
    16  	"path/filepath"
    17  	"runtime"
    18  	"runtime/pprof"
    19  	"time"
    20  
    21  	"decred.org/dcrwallet/v3/chain"
    22  	"decred.org/dcrwallet/v3/errors"
    23  	ldr "decred.org/dcrwallet/v3/internal/loader"
    24  	"decred.org/dcrwallet/v3/internal/loggers"
    25  	"decred.org/dcrwallet/v3/internal/prompt"
    26  	"decred.org/dcrwallet/v3/internal/rpc/rpcserver"
    27  	"decred.org/dcrwallet/v3/internal/vsp"
    28  	"decred.org/dcrwallet/v3/p2p"
    29  	"decred.org/dcrwallet/v3/spv"
    30  	"decred.org/dcrwallet/v3/ticketbuyer"
    31  	"decred.org/dcrwallet/v3/version"
    32  	"decred.org/dcrwallet/v3/wallet"
    33  	"github.com/decred/dcrd/addrmgr/v2"
    34  	"github.com/decred/dcrd/wire"
    35  )
    36  
    37  func init() {
    38  	// Format nested errors without newlines (better for logs).
    39  	errors.Separator = ":: "
    40  }
    41  
    42  var (
    43  	cfg *config
    44  )
    45  
    46  func main() {
    47  	// Create a context that is cancelled when a shutdown request is received
    48  	// through an interrupt signal or an RPC request.
    49  	ctx := withShutdownCancel(context.Background())
    50  	go shutdownListener()
    51  
    52  	// Run the wallet until permanent failure or shutdown is requested.
    53  	if err := run(ctx); err != nil && !errors.Is(err, context.Canceled) {
    54  		os.Exit(1)
    55  	}
    56  }
    57  
    58  // done returns whether the context's Done channel was closed due to
    59  // cancellation or exceeded deadline.
    60  func done(ctx context.Context) bool {
    61  	select {
    62  	case <-ctx.Done():
    63  		return true
    64  	default:
    65  		return false
    66  	}
    67  }
    68  
    69  func zero(b []byte) {
    70  	for i := range b {
    71  		b[i] = 0
    72  	}
    73  }
    74  
    75  // run is the main startup and teardown logic performed by the main package.  It
    76  // is responsible for parsing the config, starting RPC servers, loading and
    77  // syncing the wallet (if necessary), and stopping all started services when the
    78  // context is cancelled.
    79  func run(ctx context.Context) error {
    80  	// Load configuration and parse command line.  This function also
    81  	// initializes logging and configures it accordingly.
    82  	tcfg, _, err := loadConfig(ctx)
    83  	if err != nil {
    84  		return err
    85  	}
    86  	cfg = tcfg
    87  	defer loggers.CloseLogRotator()
    88  
    89  	// Show version at startup.
    90  	log.Infof("Version %s (Go version %s %s/%s)", version.String(), runtime.Version(),
    91  		runtime.GOOS, runtime.GOARCH)
    92  	if cfg.NoFileLogging {
    93  		log.Info("File logging disabled")
    94  	}
    95  
    96  	// Read IPC messages from the read end of a pipe created and passed by the
    97  	// parent process, if any.  When this pipe is closed, shutdown is
    98  	// initialized.
    99  	if cfg.PipeRx != nil {
   100  		go serviceControlPipeRx(uintptr(*cfg.PipeRx))
   101  	}
   102  	if cfg.PipeTx != nil {
   103  		go serviceControlPipeTx(uintptr(*cfg.PipeTx))
   104  	} else {
   105  		go drainOutgoingPipeMessages()
   106  	}
   107  
   108  	// Run the pprof profiler if enabled.
   109  	if len(cfg.Profile) > 0 {
   110  		if done(ctx) {
   111  			return ctx.Err()
   112  		}
   113  
   114  		profileRedirect := http.RedirectHandler("/debug/pprof", http.StatusSeeOther)
   115  		http.Handle("/", profileRedirect)
   116  		for _, listenAddr := range cfg.Profile {
   117  			listenAddr := listenAddr // copy for closure
   118  			go func() {
   119  				log.Infof("Starting profile server on %s", listenAddr)
   120  				err := http.ListenAndServe(listenAddr, nil)
   121  				if err != nil {
   122  					fatalf("Unable to run profiler: %v", err)
   123  				}
   124  			}()
   125  		}
   126  	}
   127  
   128  	// Write cpu profile if requested.
   129  	if cfg.CPUProfile != "" {
   130  		if done(ctx) {
   131  			return ctx.Err()
   132  		}
   133  
   134  		f, err := os.Create(cfg.CPUProfile)
   135  		if err != nil {
   136  			log.Errorf("Unable to create cpu profile: %v", err.Error())
   137  			return err
   138  		}
   139  		pprof.StartCPUProfile(f)
   140  		defer f.Close()
   141  		defer pprof.StopCPUProfile()
   142  	}
   143  
   144  	// Write mem profile if requested.
   145  	if cfg.MemProfile != "" {
   146  		if done(ctx) {
   147  			return ctx.Err()
   148  		}
   149  
   150  		f, err := os.Create(cfg.MemProfile)
   151  		if err != nil {
   152  			log.Errorf("Unable to create mem profile: %v", err)
   153  			return err
   154  		}
   155  		defer func() {
   156  			pprof.WriteHeapProfile(f)
   157  			f.Close()
   158  		}()
   159  	}
   160  
   161  	if done(ctx) {
   162  		return ctx.Err()
   163  	}
   164  
   165  	// Create the loader which is used to load and unload the wallet.  If
   166  	// --noinitialload is not set, this function is responsible for loading the
   167  	// wallet.  Otherwise, loading is deferred so it can be performed over RPC.
   168  	dbDir := networkDir(cfg.AppDataDir.Value, activeNet.Params)
   169  	stakeOptions := &ldr.StakeOptions{
   170  		VotingEnabled:       cfg.EnableVoting,
   171  		VotingAddress:       cfg.TBOpts.votingAddress,
   172  		PoolAddress:         cfg.poolAddress,
   173  		PoolFees:            cfg.PoolFees,
   174  		StakePoolColdExtKey: cfg.StakePoolColdExtKey,
   175  	}
   176  	loader := ldr.NewLoader(activeNet.Params, dbDir, stakeOptions,
   177  		cfg.GapLimit, cfg.WatchLast, cfg.AllowHighFees, cfg.RelayFee.Amount,
   178  		cfg.AccountGapLimit, cfg.DisableCoinTypeUpgrades, cfg.ManualTickets,
   179  		cfg.MixSplitLimit)
   180  	loader.DialCSPPServer = cfg.dialCSPPServer
   181  
   182  	// Stop any services started by the loader after the shutdown procedure is
   183  	// initialized and this function returns.
   184  	defer func() {
   185  		// When panicing, do not cleanly unload the wallet (by closing
   186  		// the db).  If a panic occurred inside a bolt transaction, the
   187  		// db mutex is still held and this causes a deadlock.
   188  		if r := recover(); r != nil {
   189  			panic(r)
   190  		}
   191  		err := loader.UnloadWallet()
   192  		if err != nil && !errors.Is(err, errors.Invalid) {
   193  			log.Errorf("Failed to close wallet: %v", err)
   194  		} else if err == nil {
   195  			log.Infof("Closed wallet")
   196  		}
   197  	}()
   198  
   199  	// Open the wallet when --noinitialload was not set.
   200  	var vspClient *vsp.Client
   201  	passphrase := []byte{}
   202  	if !cfg.NoInitialLoad {
   203  		walletPass := []byte(cfg.WalletPass)
   204  		if cfg.PromptPublicPass {
   205  			walletPass, _ = passPrompt(ctx, "Enter public wallet passphrase", false)
   206  		}
   207  
   208  		if done(ctx) {
   209  			return ctx.Err()
   210  		}
   211  
   212  		// Load the wallet.  It must have been created already or this will
   213  		// return an appropriate error.
   214  		var w *wallet.Wallet
   215  		errc := make(chan error, 1)
   216  		go func() {
   217  			defer zero(walletPass)
   218  			var err error
   219  			w, err = loader.OpenExistingWallet(ctx, walletPass)
   220  			if err != nil {
   221  				log.Errorf("Failed to open wallet: %v", err)
   222  				if errors.Is(err, errors.Passphrase) {
   223  					// walletpass not provided, advice using --walletpass or --promptpublicpass
   224  					if cfg.WalletPass == wallet.InsecurePubPassphrase {
   225  						log.Info("Configure public passphrase with walletpass or promptpublicpass options.")
   226  					}
   227  				}
   228  			}
   229  			errc <- err
   230  		}()
   231  		select {
   232  		case <-ctx.Done():
   233  			return ctx.Err()
   234  		case err := <-errc:
   235  			if err != nil {
   236  				return err
   237  			}
   238  		}
   239  
   240  		// TODO(jrick): I think that this prompt should be removed
   241  		// entirely instead of enabling it when --noinitialload is
   242  		// unset.  It can be replaced with an RPC request (either
   243  		// providing the private passphrase as a parameter, or require
   244  		// unlocking the wallet first) to trigger a full accounts
   245  		// rescan.
   246  		//
   247  		// Until then, since --noinitialload users are expecting to use
   248  		// the wallet only over RPC, disable this feature for them.
   249  		if cfg.Pass != "" {
   250  			passphrase = []byte(cfg.Pass)
   251  			err = w.Unlock(ctx, passphrase, nil)
   252  			if err != nil {
   253  				log.Errorf("Incorrect passphrase in pass config setting.")
   254  				return err
   255  			}
   256  		} else {
   257  			passphrase = startPromptPass(ctx, w)
   258  		}
   259  
   260  		if cfg.VSPOpts.URL != "" {
   261  			changeAccountName := cfg.ChangeAccount
   262  			if changeAccountName == "" && cfg.CSPPServer == "" {
   263  				log.Warnf("Change account not set, using "+
   264  					"purchase account %q", cfg.PurchaseAccount)
   265  				changeAccountName = cfg.PurchaseAccount
   266  			}
   267  			changeAcct, err := w.AccountNumber(ctx, changeAccountName)
   268  			if err != nil {
   269  				log.Warnf("failed to get account number for "+
   270  					"ticket change account %q: %v",
   271  					changeAccountName, err)
   272  				return err
   273  			}
   274  			purchaseAcct, err := w.AccountNumber(ctx, cfg.PurchaseAccount)
   275  			if err != nil {
   276  				log.Warnf("failed to get account number for "+
   277  					"ticket purchase account %q: %v",
   278  					cfg.PurchaseAccount, err)
   279  				return err
   280  			}
   281  			vspCfg := vsp.Config{
   282  				URL:    cfg.VSPOpts.URL,
   283  				PubKey: cfg.VSPOpts.PubKey,
   284  				Dialer: cfg.dial,
   285  				Wallet: w,
   286  				Policy: vsp.Policy{
   287  					MaxFee:     cfg.VSPOpts.MaxFee.Amount,
   288  					FeeAcct:    purchaseAcct,
   289  					ChangeAcct: changeAcct,
   290  				},
   291  			}
   292  			vspClient, err = ldr.VSP(vspCfg)
   293  			if err != nil {
   294  				log.Errorf("vsp: %v", err)
   295  				return err
   296  			}
   297  		}
   298  
   299  		var tb *ticketbuyer.TB
   300  		if cfg.MixChange || cfg.EnableTicketBuyer {
   301  			tb = ticketbuyer.New(w)
   302  		}
   303  
   304  		var lastFlag, lastLookup string
   305  		lookup := func(flag, name string) (account uint32) {
   306  			if tb != nil && err == nil {
   307  				lastFlag = flag
   308  				lastLookup = name
   309  				account, err = w.AccountNumber(ctx, name)
   310  			}
   311  			return
   312  		}
   313  		var (
   314  			purchaseAccount    uint32 // enableticketbuyer
   315  			votingAccount      uint32 // enableticketbuyer
   316  			mixedAccount       uint32 // (enableticketbuyer && csppserver) || mixchange
   317  			changeAccount      uint32 // (enableticketbuyer && csppserver) || mixchange
   318  			ticketSplitAccount uint32 // enableticketbuyer && csppserver
   319  
   320  			votingAddr  = cfg.TBOpts.votingAddress
   321  			poolFeeAddr = cfg.poolAddress
   322  		)
   323  		if cfg.EnableTicketBuyer {
   324  			purchaseAccount = lookup("purchaseaccount", cfg.PurchaseAccount)
   325  			if cfg.CSPPServer != "" {
   326  				poolFeeAddr = nil
   327  			}
   328  			if cfg.CSPPServer != "" && cfg.TBOpts.VotingAccount == "" {
   329  				err := errors.New("cannot run mixed ticketbuyer without --votingaccount")
   330  				log.Error(err)
   331  				return err
   332  			}
   333  			if cfg.TBOpts.VotingAccount != "" {
   334  				votingAccount = lookup("ticketbuyer.votingaccount", cfg.TBOpts.VotingAccount)
   335  				votingAddr = nil
   336  			}
   337  		}
   338  		if (cfg.EnableTicketBuyer && cfg.CSPPServer != "") || cfg.MixChange {
   339  			mixedAccount = lookup("mixedaccount", cfg.mixedAccount)
   340  			changeAccount = lookup("changeaccount", cfg.ChangeAccount)
   341  		}
   342  		if cfg.EnableTicketBuyer && cfg.CSPPServer != "" {
   343  			ticketSplitAccount = lookup("ticketsplitaccount", cfg.TicketSplitAccount)
   344  		}
   345  		if err != nil {
   346  			log.Errorf("%s: account %q does not exist", lastFlag, lastLookup)
   347  			return err
   348  		}
   349  
   350  		if tb != nil {
   351  			// Start a ticket buyer.
   352  			tb.AccessConfig(func(c *ticketbuyer.Config) {
   353  				c.BuyTickets = cfg.EnableTicketBuyer
   354  				c.Account = purchaseAccount
   355  				c.Maintain = cfg.TBOpts.BalanceToMaintainAbsolute.Amount
   356  				c.VotingAddr = votingAddr
   357  				c.PoolFeeAddr = poolFeeAddr
   358  				c.Limit = int(cfg.TBOpts.Limit)
   359  				c.VotingAccount = votingAccount
   360  				c.CSPPServer = cfg.CSPPServer
   361  				c.DialCSPPServer = cfg.dialCSPPServer
   362  				c.MixChange = cfg.MixChange
   363  				c.MixedAccount = mixedAccount
   364  				c.MixedAccountBranch = cfg.mixedBranch
   365  				c.TicketSplitAccount = ticketSplitAccount
   366  				c.ChangeAccount = changeAccount
   367  				c.VSP = vspClient
   368  			})
   369  			log.Infof("Starting auto transaction creator")
   370  			tbdone := make(chan struct{})
   371  			go func() {
   372  				err := tb.Run(ctx, passphrase)
   373  				if err != nil && !errors.Is(err, context.Canceled) {
   374  					log.Errorf("Transaction creator ended: %v", err)
   375  				}
   376  				tbdone <- struct{}{}
   377  			}()
   378  			defer func() { <-tbdone }()
   379  		}
   380  	}
   381  
   382  	if done(ctx) {
   383  		return ctx.Err()
   384  	}
   385  
   386  	// Create and start the RPC servers to serve wallet client connections.  If
   387  	// any of the servers can not be started, it will be nil.  If none of them
   388  	// can be started, this errors since at least one server must run for the
   389  	// wallet to be useful.
   390  	//
   391  	// Servers will be associated with a loaded wallet if it has already been
   392  	// loaded, or after it is loaded later on.
   393  	gRPCServer, jsonRPCServer, err := startRPCServers(loader)
   394  	if err != nil {
   395  		log.Errorf("Unable to create RPC servers: %v", err)
   396  		return err
   397  	}
   398  	if gRPCServer != nil {
   399  		// Start wallet, voting and network gRPC services after a
   400  		// wallet is loaded.
   401  		loader.RunAfterLoad(func(w *wallet.Wallet) {
   402  			rpcserver.StartWalletService(gRPCServer, w, cfg.dialCSPPServer)
   403  			rpcserver.StartNetworkService(gRPCServer, w)
   404  			rpcserver.StartVotingService(gRPCServer, w)
   405  		})
   406  		defer func() {
   407  			log.Warn("Stopping gRPC server...")
   408  			gRPCServer.Stop()
   409  			log.Info("gRPC server shutdown")
   410  		}()
   411  	}
   412  	if jsonRPCServer != nil {
   413  		go func() {
   414  			for range jsonRPCServer.RequestProcessShutdown() {
   415  				requestShutdown()
   416  			}
   417  		}()
   418  		defer func() {
   419  			log.Warn("Stopping JSON-RPC server...")
   420  			jsonRPCServer.Stop()
   421  			log.Info("JSON-RPC server shutdown")
   422  		}()
   423  	}
   424  
   425  	// When not running with --noinitialload, it is the main package's
   426  	// responsibility to synchronize the wallet with the network through SPV or
   427  	// the trusted dcrd server.  This blocks until cancelled.
   428  	if !cfg.NoInitialLoad {
   429  		if done(ctx) {
   430  			return ctx.Err()
   431  		}
   432  
   433  		loader.RunAfterLoad(func(w *wallet.Wallet) {
   434  			if vspClient != nil && cfg.VSPOpts.Sync {
   435  				vspClient.ProcessManagedTickets(ctx)
   436  			}
   437  
   438  			if cfg.SPV {
   439  				spvLoop(ctx, w)
   440  			} else {
   441  				rpcSyncLoop(ctx, w)
   442  			}
   443  		})
   444  	}
   445  
   446  	// Wait until shutdown is signaled before returning and running deferred
   447  	// shutdown tasks.
   448  	<-ctx.Done()
   449  	return ctx.Err()
   450  }
   451  
   452  func passPrompt(ctx context.Context, prefix string, confirm bool) (passphrase []byte, err error) {
   453  	os.Stdout.Sync()
   454  	c := make(chan struct{}, 1)
   455  	go func() {
   456  		passphrase, err = prompt.PassPrompt(bufio.NewReader(os.Stdin), prefix, confirm)
   457  		c <- struct{}{}
   458  	}()
   459  	select {
   460  	case <-ctx.Done():
   461  		return nil, ctx.Err()
   462  	case <-c:
   463  		return passphrase, err
   464  	}
   465  }
   466  
   467  // startPromptPass prompts the user for a password to unlock their wallet in
   468  // the event that it was restored from seed or --promptpass flag is set.
   469  func startPromptPass(ctx context.Context, w *wallet.Wallet) []byte {
   470  	promptPass := cfg.PromptPass
   471  
   472  	// Watching only wallets never require a password.
   473  	if w.WatchingOnly() {
   474  		return nil
   475  	}
   476  
   477  	// The wallet is totally desynced, so we need to resync accounts.
   478  	// Prompt for the password. Then, set the flag it wallet so it
   479  	// knows which address functions to call when resyncing.
   480  	needSync, err := w.NeedsAccountsSync(ctx)
   481  	if err != nil {
   482  		log.Errorf("Error determining whether an accounts sync is necessary: %v", err)
   483  	}
   484  	if err == nil && needSync {
   485  		fmt.Println("*** ATTENTION ***")
   486  		fmt.Println("Since this is your first time running we need to sync accounts. Please enter")
   487  		fmt.Println("the private wallet passphrase. This will complete syncing of the wallet")
   488  		fmt.Println("accounts and then leave your wallet unlocked. You may relock wallet after by")
   489  		fmt.Println("calling 'walletlock' through the RPC.")
   490  		fmt.Println("*****************")
   491  		promptPass = true
   492  	}
   493  	if cfg.EnableTicketBuyer {
   494  		promptPass = true
   495  	}
   496  
   497  	if !promptPass {
   498  		return nil
   499  	}
   500  
   501  	// We need to rescan accounts for the initial sync. Unlock the
   502  	// wallet after prompting for the passphrase. The special case
   503  	// of a --createtemp simnet wallet is handled by first
   504  	// attempting to automatically open it with the default
   505  	// passphrase. The wallet should also request to be unlocked
   506  	// if stake mining is currently on, so users with this flag
   507  	// are prompted here as well.
   508  	for {
   509  		if w.ChainParams().Net == wire.SimNet {
   510  			err := w.Unlock(ctx, wallet.SimulationPassphrase, nil)
   511  			if err == nil {
   512  				// Unlock success with the default password.
   513  				return wallet.SimulationPassphrase
   514  			}
   515  		}
   516  
   517  		passphrase, err := passPrompt(ctx, "Enter private passphrase", false)
   518  		if err != nil {
   519  			return nil
   520  		}
   521  
   522  		err = w.Unlock(ctx, passphrase, nil)
   523  		if err != nil {
   524  			fmt.Println("Incorrect password entered. Please " +
   525  				"try again.")
   526  			continue
   527  		}
   528  		return passphrase
   529  	}
   530  }
   531  
   532  func spvLoop(ctx context.Context, w *wallet.Wallet) {
   533  	addr := &net.TCPAddr{IP: net.ParseIP("::1"), Port: 0}
   534  	amgrDir := filepath.Join(cfg.AppDataDir.Value, w.ChainParams().Name)
   535  	amgr := addrmgr.New(amgrDir, cfg.lookup)
   536  	lp := p2p.NewLocalPeer(w.ChainParams(), addr, amgr)
   537  	lp.SetDialFunc(cfg.dial)
   538  	syncer := spv.NewSyncer(w, lp)
   539  	if len(cfg.SPVConnect) > 0 {
   540  		syncer.SetPersistentPeers(cfg.SPVConnect)
   541  	}
   542  	w.SetNetworkBackend(syncer)
   543  	for {
   544  		err := syncer.Run(ctx)
   545  		if done(ctx) {
   546  			return
   547  		}
   548  		log.Errorf("SPV synchronization ended: %v", err)
   549  	}
   550  }
   551  
   552  // rpcSyncLoop loops forever, attempting to create a connection to the
   553  // consensus RPC server.  If this connection succeeds, the RPC client is used as
   554  // the loaded wallet's network backend and used to keep the wallet synchronized
   555  // to the network.  If/when the RPC connection is lost, the wallet is
   556  // disassociated from the client and a new connection is attempmted.
   557  func rpcSyncLoop(ctx context.Context, w *wallet.Wallet) {
   558  	certs := readCAFile()
   559  	dial := cfg.dial
   560  	if cfg.NoDcrdProxy {
   561  		dial = new(net.Dialer).DialContext
   562  	}
   563  	for {
   564  		syncer := chain.NewSyncer(w, &chain.RPCOptions{
   565  			Address:     cfg.RPCConnect,
   566  			DefaultPort: activeNet.JSONRPCClientPort,
   567  			User:        cfg.DcrdUsername,
   568  			Pass:        cfg.DcrdPassword,
   569  			Dial:        dial,
   570  			CA:          certs,
   571  			Insecure:    cfg.DisableClientTLS,
   572  		})
   573  		err := syncer.Run(ctx)
   574  		if err != nil {
   575  			loggers.SyncLog.Errorf("Wallet synchronization stopped: %v", err)
   576  			select {
   577  			case <-ctx.Done():
   578  				return
   579  			case <-time.After(5 * time.Second):
   580  			}
   581  		}
   582  	}
   583  }
   584  
   585  func readCAFile() []byte {
   586  	// Read certificate file if TLS is not disabled.
   587  	var certs []byte
   588  	if !cfg.DisableClientTLS {
   589  		var err error
   590  		certs, err = os.ReadFile(cfg.CAFile.Value)
   591  		if err != nil {
   592  			log.Warnf("Cannot open CA file: %v", err)
   593  			// If there's an error reading the CA file, continue
   594  			// with nil certs and without the client connection.
   595  			certs = nil
   596  		}
   597  	} else {
   598  		log.Info("Chain server RPC TLS is disabled")
   599  	}
   600  
   601  	return certs
   602  }