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 }