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 }