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 }