decred.org/dcrdex@v1.0.5/client/asset/bch/spv.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package bch 5 6 import ( 7 "bytes" 8 "context" 9 "errors" 10 "fmt" 11 "os" 12 "path/filepath" 13 "sync/atomic" 14 "time" 15 16 "decred.org/dcrdex/client/asset" 17 "decred.org/dcrdex/client/asset/btc" 18 "decred.org/dcrdex/dex" 19 dexbch "decred.org/dcrdex/dex/networks/bch" 20 "github.com/btcsuite/btcd/btcec/v2" 21 "github.com/btcsuite/btcd/btcjson" 22 "github.com/btcsuite/btcd/btcutil" 23 "github.com/btcsuite/btcd/btcutil/gcs" 24 "github.com/btcsuite/btcd/btcutil/psbt" 25 "github.com/btcsuite/btcd/chaincfg" 26 "github.com/btcsuite/btcd/chaincfg/chainhash" 27 "github.com/btcsuite/btcd/wire" 28 "github.com/btcsuite/btcwallet/waddrmgr" 29 btcwallet "github.com/btcsuite/btcwallet/wallet" 30 "github.com/btcsuite/btcwallet/wtxmgr" 31 "github.com/dcrlabs/bchwallet/chain" 32 bchwaddrmgr "github.com/dcrlabs/bchwallet/waddrmgr" 33 "github.com/dcrlabs/bchwallet/wallet" 34 "github.com/dcrlabs/bchwallet/wallet/txauthor" 35 "github.com/dcrlabs/bchwallet/walletdb" 36 _ "github.com/dcrlabs/bchwallet/walletdb/bdb" 37 bchwtxmgr "github.com/dcrlabs/bchwallet/wtxmgr" 38 neutrino "github.com/dcrlabs/neutrino-bch" 39 labschain "github.com/dcrlabs/neutrino-bch/chain" 40 "github.com/decred/slog" 41 "github.com/gcash/bchd/bchec" 42 bchchaincfg "github.com/gcash/bchd/chaincfg" 43 bchchainhash "github.com/gcash/bchd/chaincfg/chainhash" 44 bchtxscript "github.com/gcash/bchd/txscript" 45 bchwire "github.com/gcash/bchd/wire" 46 "github.com/gcash/bchlog" 47 "github.com/gcash/bchutil" 48 "github.com/jrick/logrotate/rotator" 49 btcneutrino "github.com/lightninglabs/neutrino" 50 "github.com/lightninglabs/neutrino/headerfs" 51 ) 52 53 const ( 54 DefaultM uint64 = 784931 // From bchutil. Used for gcs filters. 55 logDirName = "logs" 56 neutrinoDBName = "neutrino.db" 57 defaultAcctNum = 0 58 ) 59 60 var ( 61 waddrmgrNamespace = []byte("waddrmgr") 62 wtxmgrNamespace = []byte("wtxmgr") 63 ) 64 65 // bchSPVWallet is an implementation of btc.BTCWallet that runs a native Bitcoin 66 // Cash SPV Wallet. bchSPVWallet mostly just translates types from the btcsuite 67 // types to gcash and vice-versa. Startup and shutdown are notable exceptions, 68 // and have some critical code that needed to be duplicated (in order to avoid 69 // interface hell). 70 type bchSPVWallet struct { 71 // This section is populated in openSPVWallet. 72 dir string 73 chainParams *bchchaincfg.Params 74 btcParams *chaincfg.Params 75 log dex.Logger 76 77 // This section is populated in Start. 78 *wallet.Wallet 79 chainClient *labschain.NeutrinoClient 80 cl *neutrino.ChainService 81 loader *wallet.Loader 82 neutrinoDB walletdb.DB 83 84 peerManager *btc.SPVPeerManager 85 } 86 87 var _ btc.BTCWallet = (*bchSPVWallet)(nil) 88 89 // openSPVWallet creates a bchSPVWallet, but does not Start. 90 // Satisfies btc.BTCWalletConstructor. 91 func openSPVWallet(dir string, cfg *btc.WalletConfig, btcParams *chaincfg.Params, log dex.Logger) btc.BTCWallet { 92 var bchParams *bchchaincfg.Params 93 switch btcParams.Name { 94 case dexbch.MainNetParams.Name: 95 bchParams = &bchchaincfg.MainNetParams 96 case dexbch.TestNet4Params.Name: 97 bchParams = &bchchaincfg.TestNet4Params 98 case dexbch.RegressionNetParams.Name: 99 bchParams = &bchchaincfg.RegressionNetParams 100 } 101 102 w := &bchSPVWallet{ 103 dir: dir, 104 chainParams: bchParams, 105 btcParams: btcParams, 106 log: log, 107 } 108 return w 109 } 110 111 // createSPVWallet creates a new SPV wallet. 112 func createSPVWallet(privPass []byte, seed []byte, bday time.Time, walletDir string, log dex.Logger, extIdx, intIdx uint32, net *bchchaincfg.Params) error { 113 if err := logNeutrino(walletDir, log); err != nil { 114 return fmt.Errorf("error initializing bchwallet+neutrino logging: %w", err) 115 } 116 117 loader := wallet.NewLoader(net, walletDir, true, 250) 118 119 pubPass := []byte(wallet.InsecurePubPassphrase) 120 121 btcw, err := loader.CreateNewWallet(pubPass, privPass, seed, bday) 122 if err != nil { 123 return fmt.Errorf("CreateNewWallet error: %w", err) 124 } 125 126 errCloser := dex.NewErrorCloser() 127 defer errCloser.Done(log) 128 errCloser.Add(loader.UnloadWallet) 129 130 if extIdx > 0 || intIdx > 0 { 131 err = extendAddresses(extIdx, intIdx, btcw) 132 if err != nil { 133 return fmt.Errorf("failed to set starting address indexes: %w", err) 134 } 135 } 136 137 // The chain service DB 138 neutrinoDBPath := filepath.Join(walletDir, neutrinoDBName) 139 db, err := walletdb.Create("bdb", neutrinoDBPath, true) 140 if err != nil { 141 return fmt.Errorf("unable to create neutrino db at %q: %w", neutrinoDBPath, err) 142 } 143 if err = db.Close(); err != nil { 144 return fmt.Errorf("error closing newly created wallet database: %w", err) 145 } 146 147 if err := loader.UnloadWallet(); err != nil { 148 return fmt.Errorf("error unloading wallet: %w", err) 149 } 150 151 errCloser.Success() 152 return nil 153 } 154 155 // Start initializes the *bchwallet.Wallet and its supporting players and starts 156 // syncing. 157 func (w *bchSPVWallet) Start() (btc.SPVService, error) { 158 if err := logNeutrino(w.dir, w.log); err != nil { 159 return nil, fmt.Errorf("error initializing bchwallet+neutrino logging: %v", err) 160 } 161 // recoverWindow arguments borrowed from bchwallet directly. 162 w.loader = wallet.NewLoader(w.chainParams, w.dir, true, 250) 163 164 exists, err := w.loader.WalletExists() 165 if err != nil { 166 return nil, fmt.Errorf("error verifying wallet existence: %v", err) 167 } 168 if !exists { 169 return nil, errors.New("wallet not found") 170 } 171 172 w.log.Debug("Starting native BCH wallet...") 173 w.Wallet, err = w.loader.OpenExistingWallet([]byte(wallet.InsecurePubPassphrase), false) 174 if err != nil { 175 return nil, fmt.Errorf("couldn't load wallet: %w", err) 176 } 177 178 errCloser := dex.NewErrorCloser() 179 defer errCloser.Done(w.log) 180 errCloser.Add(w.loader.UnloadWallet) 181 182 neutrinoDBPath := filepath.Join(w.dir, neutrinoDBName) 183 w.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true) 184 if err != nil { 185 return nil, fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) 186 } 187 errCloser.Add(w.neutrinoDB.Close) 188 189 w.log.Debug("Starting neutrino chain service...") 190 w.cl, err = neutrino.NewChainService(neutrino.Config{ 191 DataDir: w.dir, 192 Database: w.neutrinoDB, 193 ChainParams: *w.chainParams, 194 // https://github.com/gcash/neutrino/pull/36 195 PersistToDisk: true, // keep cfilter headers on disk for efficient rescanning 196 // WARNING: PublishTransaction currently uses the entire duration 197 // because if an external bug, but even if the bug is resolved, a 198 // typical inv/getdata round trip is ~4 seconds, so we set this so 199 // neutrino does not cancel queries too readily. 200 BroadcastTimeout: 6 * time.Second, 201 }) 202 if err != nil { 203 return nil, fmt.Errorf("couldn't create Neutrino ChainService: %v", err) 204 } 205 errCloser.Add(w.cl.Stop) 206 207 w.chainClient = labschain.NewNeutrinoClient(w.chainParams, w.cl, &logAdapter{w.log}) 208 209 var defaultPeers []string 210 switch w.chainParams.Net { 211 // case bchwire.MainNet: 212 // addPeers = []string{"cfilters.ssgen.io"} 213 case bchwire.TestNet4: 214 // Add the address for a local bchd testnet4 node. 215 defaultPeers = []string{} 216 case bchwire.TestNet, bchwire.SimNet: // plain "wire.TestNet" is regnet! 217 defaultPeers = []string{"127.0.0.1:21577"} 218 } 219 peerManager := btc.NewSPVPeerManager(&spvService{w.cl}, defaultPeers, w.dir, w.log, w.chainParams.DefaultPort) 220 w.peerManager = peerManager 221 222 if err = w.chainClient.Start(); err != nil { // lazily starts connmgr 223 return nil, fmt.Errorf("couldn't start Neutrino client: %v", err) 224 } 225 226 w.log.Info("Synchronizing wallet with network...") 227 w.SynchronizeRPC(w.chainClient) 228 229 errCloser.Success() 230 231 w.peerManager.ConnectToInitialWalletPeers() 232 233 return &spvService{w.cl}, nil 234 } 235 236 func (w *bchSPVWallet) txDetails(txHash *bchchainhash.Hash) (*bchwtxmgr.TxDetails, error) { 237 details, err := wallet.UnstableAPI(w.Wallet).TxDetails(txHash) 238 if err != nil { 239 return nil, err 240 } 241 if details == nil { 242 return nil, btc.WalletTransactionNotFound 243 } 244 245 return details, nil 246 } 247 248 var _ btc.BTCWallet = (*bchSPVWallet)(nil) 249 250 func (w *bchSPVWallet) PublishTransaction(btcTx *wire.MsgTx, label string) error { 251 bchTx, err := convertMsgTxToBCH(btcTx) 252 if err != nil { 253 return err 254 } 255 return w.Wallet.PublishTransaction(bchTx) 256 } 257 258 func (w *bchSPVWallet) CalculateAccountBalances(account uint32, confirms int32) (btcwallet.Balances, error) { 259 bals, err := w.Wallet.CalculateAccountBalances(account, confirms) 260 if err != nil { 261 return btcwallet.Balances{}, err 262 } 263 return btcwallet.Balances{ 264 Total: btcutil.Amount(bals.Total), 265 Spendable: btcutil.Amount(bals.Spendable), 266 ImmatureReward: btcutil.Amount(bals.ImmatureReward), 267 }, nil 268 } 269 270 func (w *bchSPVWallet) ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTransactionsResult, error) { 271 res, err := w.Wallet.ListSinceBlock(start, end, syncHeight) 272 if err != nil { 273 return nil, err 274 } 275 276 btcRes := make([]btcjson.ListTransactionsResult, len(res)) 277 for i, r := range res { 278 btcRes[i] = btcjson.ListTransactionsResult{ 279 Abandoned: r.Abandoned, 280 Account: r.Account, 281 Address: r.Address, 282 Amount: r.Amount, 283 BIP125Replaceable: r.BIP125Replaceable, 284 BlockHash: r.BlockHash, 285 BlockIndex: r.BlockIndex, 286 BlockTime: r.BlockTime, 287 Category: r.Category, 288 Confirmations: r.Confirmations, 289 Fee: r.Fee, 290 Generated: r.Generated, 291 InvolvesWatchOnly: r.InvolvesWatchOnly, 292 Time: r.Time, 293 TimeReceived: r.TimeReceived, 294 Trusted: r.Trusted, 295 TxID: r.TxID, 296 Vout: r.Vout, 297 WalletConflicts: r.WalletConflicts, 298 Comment: r.Comment, 299 OtherAccount: r.OtherAccount, 300 } 301 } 302 303 return btcRes, nil 304 } 305 306 func (w *bchSPVWallet) GetTransactions(startBlock, endBlock int32, _ string, cancel <-chan struct{}) (*btcwallet.GetTransactionsResult, error) { 307 startID := wallet.NewBlockIdentifierFromHeight(startBlock) 308 endID := wallet.NewBlockIdentifierFromHeight(endBlock) 309 bchGTR, err := w.Wallet.GetTransactions(startID, endID, cancel) 310 if err != nil { 311 return nil, err 312 } 313 314 convertTxs := func(txs []wallet.TransactionSummary) []btcwallet.TransactionSummary { 315 transactions := make([]btcwallet.TransactionSummary, len(txs)) 316 for i, tx := range txs { 317 txHash := chainhash.Hash(*tx.Hash) 318 inputs := make([]btcwallet.TransactionSummaryInput, len(tx.MyInputs)) 319 for k, in := range tx.MyInputs { 320 inputs[k] = btcwallet.TransactionSummaryInput{ 321 Index: in.Index, 322 PreviousAccount: in.PreviousAccount, 323 PreviousAmount: btcutil.Amount(in.PreviousAmount), 324 } 325 } 326 outputs := make([]btcwallet.TransactionSummaryOutput, len(tx.MyOutputs)) 327 for k, out := range tx.MyOutputs { 328 outputs[k] = btcwallet.TransactionSummaryOutput{ 329 Index: out.Index, 330 Account: out.Account, 331 Internal: out.Internal, 332 } 333 } 334 transactions[i] = btcwallet.TransactionSummary{ 335 Hash: &txHash, 336 Transaction: tx.Transaction, 337 MyInputs: inputs, 338 MyOutputs: outputs, 339 Fee: btcutil.Amount(tx.Fee), 340 Timestamp: tx.Timestamp, 341 } 342 } 343 return transactions 344 } 345 346 btcGTR := &btcwallet.GetTransactionsResult{ 347 MinedTransactions: make([]btcwallet.Block, len(bchGTR.MinedTransactions)), 348 UnminedTransactions: convertTxs(bchGTR.UnminedTransactions), 349 } 350 351 for i, block := range bchGTR.MinedTransactions { 352 blockHash := chainhash.Hash(*block.Hash) 353 btcGTR.MinedTransactions[i] = btcwallet.Block{ 354 Hash: &blockHash, 355 Height: block.Height, 356 Timestamp: block.Timestamp, 357 Transactions: convertTxs(block.Transactions), 358 } 359 } 360 361 return btcGTR, nil 362 } 363 364 func (w *bchSPVWallet) ListUnspent(minconf, maxconf int32, acctName string) ([]*btcjson.ListUnspentResult, error) { 365 // bchwallet's ListUnspent takes either a list of addresses, or else returns 366 // all non-locked unspent outputs for all accounts. We need to iterate the 367 // results anyway to convert type. 368 uns, err := w.Wallet.ListUnspent(minconf, maxconf, nil) 369 if err != nil { 370 return nil, err 371 } 372 373 outs := make([]*btcjson.ListUnspentResult, len(uns)) 374 for i, u := range uns { 375 if u.Account != acctName { 376 continue 377 } 378 outs[i] = &btcjson.ListUnspentResult{ 379 TxID: u.TxID, 380 Vout: u.Vout, 381 Address: u.Address, 382 Account: u.Account, 383 ScriptPubKey: u.ScriptPubKey, 384 RedeemScript: u.RedeemScript, 385 Amount: u.Amount, 386 Confirmations: u.Confirmations, 387 Spendable: u.Spendable, 388 } 389 } 390 391 return outs, nil 392 } 393 394 // FetchInputInfo is not actually implemented in bchwallet. This is based on the 395 // btcwallet implementation. As this is used by btc.spvWallet, we really only 396 // need the TxOut, and to show ownership. 397 func (w *bchSPVWallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error) { 398 399 td, err := w.txDetails((*bchchainhash.Hash)(&prevOut.Hash)) 400 if err != nil { 401 return nil, nil, nil, 0, err 402 } 403 404 if prevOut.Index >= uint32(len(td.TxRecord.MsgTx.TxOut)) { 405 return nil, nil, nil, 0, fmt.Errorf("not enough outputs") 406 } 407 408 bchTxOut := td.TxRecord.MsgTx.TxOut[prevOut.Index] 409 410 // Verify we own at least one parsed address. 411 _, addrs, _, err := bchtxscript.ExtractPkScriptAddrs(bchTxOut.PkScript, w.chainParams) 412 if err != nil { 413 return nil, nil, nil, 0, err 414 } 415 notOurs := true 416 for i := 0; notOurs && i < len(addrs); i++ { 417 _, err := w.Wallet.AddressInfo(addrs[i]) 418 notOurs = err != nil 419 } 420 if notOurs { 421 return nil, nil, nil, 0, btcwallet.ErrNotMine 422 } 423 424 btcTxOut := &wire.TxOut{ 425 Value: bchTxOut.Value, 426 PkScript: bchTxOut.PkScript, 427 } 428 429 return nil, btcTxOut, nil, 0, nil 430 } 431 432 func (w *bchSPVWallet) LockOutpoint(op wire.OutPoint) { 433 w.Wallet.LockOutpoint(bchwire.OutPoint{ 434 Hash: bchchainhash.Hash(op.Hash), 435 Index: op.Index, 436 }) 437 } 438 439 func (w *bchSPVWallet) UnlockOutpoint(op wire.OutPoint) { 440 w.Wallet.UnlockOutpoint(bchwire.OutPoint{ 441 Hash: bchchainhash.Hash(op.Hash), 442 Index: op.Index, 443 }) 444 } 445 446 func (w *bchSPVWallet) LockedOutpoints() []btcjson.TransactionInput { 447 locks := w.Wallet.LockedOutpoints() 448 locked := make([]btcjson.TransactionInput, len(locks)) 449 for i, lock := range locks { 450 locked[i] = btcjson.TransactionInput{ 451 Txid: lock.Txid, 452 Vout: lock.Vout, 453 } 454 } 455 return locked 456 } 457 458 func (w *bchSPVWallet) NewChangeAddress(account uint32, _ waddrmgr.KeyScope) (btcutil.Address, error) { 459 bchAddr, err := w.Wallet.NewChangeAddress(account, bchwaddrmgr.KeyScopeBIP0044) 460 if err != nil { 461 return nil, err 462 } 463 return dexbch.DecodeCashAddress(bchAddr.String(), w.btcParams) 464 } 465 466 func (w *bchSPVWallet) NewAddress(account uint32, _ waddrmgr.KeyScope) (btcutil.Address, error) { 467 bchAddr, err := w.Wallet.NewAddress(account, bchwaddrmgr.KeyScopeBIP0044) 468 if err != nil { 469 return nil, err 470 } 471 return dexbch.DecodeCashAddress(bchAddr.String(), w.btcParams) 472 } 473 474 func (w *bchSPVWallet) PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) { 475 bchAddr, err := dexbch.BTCAddrToBCHAddr(a, w.btcParams) 476 if err != nil { 477 return nil, err 478 } 479 bchKey, err := w.Wallet.PrivKeyForAddress(bchAddr) 480 if err != nil { 481 return nil, err 482 } 483 484 priv, _ /* pub */ := btcec.PrivKeyFromBytes(bchKey.Serialize()) 485 return priv, nil 486 } 487 488 func (w *bchSPVWallet) SendOutputs(outputs []*wire.TxOut, _ *waddrmgr.KeyScope, account uint32, minconf int32, 489 satPerKb btcutil.Amount, _ btcwallet.CoinSelectionStrategy, label string) (*wire.MsgTx, error) { 490 491 bchOuts := make([]*bchwire.TxOut, len(outputs)) 492 for i, op := range outputs { 493 bchOuts[i] = &bchwire.TxOut{ 494 Value: op.Value, 495 PkScript: op.PkScript, 496 } 497 } 498 499 bchTx, err := w.Wallet.SendOutputs(bchOuts, account, minconf, bchutil.Amount(satPerKb)) 500 if err != nil { 501 return nil, err 502 } 503 504 btcTx, err := convertMsgTxToBTC(bchTx) 505 if err != nil { 506 return nil, err 507 } 508 509 return btcTx, nil 510 } 511 512 func (w *bchSPVWallet) HaveAddress(a btcutil.Address) (bool, error) { 513 bchAddr, err := dexbch.BTCAddrToBCHAddr(a, w.btcParams) 514 if err != nil { 515 return false, err 516 } 517 518 return w.Wallet.HaveAddress(bchAddr) 519 } 520 521 func (w *bchSPVWallet) Stop() { 522 w.log.Info("Unloading wallet") 523 if err := w.loader.UnloadWallet(); err != nil { 524 w.log.Errorf("UnloadWallet error: %v", err) 525 } 526 if w.chainClient != nil { 527 w.log.Trace("Stopping neutrino client chain interface") 528 w.chainClient.Stop() 529 w.chainClient.WaitForShutdown() 530 } 531 w.log.Trace("Stopping neutrino chain sync service") 532 if err := w.cl.Stop(); err != nil { 533 w.log.Errorf("error stopping neutrino chain service: %v", err) 534 } 535 w.log.Trace("Stopping neutrino DB.") 536 if err := w.neutrinoDB.Close(); err != nil { 537 w.log.Errorf("wallet db close error: %v", err) 538 } 539 540 w.log.Info("SPV wallet closed") 541 } 542 543 func (w *bchSPVWallet) AccountProperties(_ waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error) { 544 props, err := w.Wallet.AccountProperties(bchwaddrmgr.KeyScopeBIP0044, acct) 545 if err != nil { 546 return nil, err 547 } 548 return &waddrmgr.AccountProperties{ 549 // bch only has a subset of the btc AccountProperties. We'll give 550 // everything we have. The only two that are actually used are these 551 // first to fields. 552 ExternalKeyCount: props.ExternalKeyCount, 553 InternalKeyCount: props.InternalKeyCount, 554 AccountNumber: props.AccountNumber, 555 AccountName: props.AccountName, 556 ImportedKeyCount: props.ImportedKeyCount, 557 }, nil 558 } 559 560 func (w *bchSPVWallet) RescanAsync() error { 561 w.log.Info("Stopping wallet and chain client...") 562 w.Wallet.Stop() // stops Wallet and chainClient (not chainService) 563 w.Wallet.WaitForShutdown() 564 w.chainClient.WaitForShutdown() 565 566 w.ForceRescan() 567 568 w.log.Info("Starting wallet...") 569 w.Wallet.Start() 570 571 if err := w.chainClient.Start(); err != nil { 572 return fmt.Errorf("couldn't start Neutrino client: %v", err) 573 } 574 575 w.log.Info("Synchronizing wallet with network...") 576 w.Wallet.SynchronizeRPC(w.chainClient) 577 return nil 578 } 579 580 // ForceRescan forces a full rescan with active address discovery on wallet 581 // restart by dropping the complete transaction history and setting the 582 // "synced to" field to nil. See the btcwallet/cmd/dropwtxmgr app for more 583 // information. 584 func (w *bchSPVWallet) ForceRescan() { 585 w.log.Info("Dropping transaction history to perform full rescan...") 586 err := w.dropTransactionHistory() 587 if err != nil { 588 w.log.Errorf("Failed to drop wallet transaction history: %v", err) 589 // Continue to attempt restarting the wallet anyway. 590 } 591 592 err = walletdb.Update(w.Database(), func(dbtx walletdb.ReadWriteTx) error { 593 ns := dbtx.ReadWriteBucket(waddrmgrNamespace) // it'll be fine 594 return w.Manager.SetSyncedTo(ns, nil) // never synced, forcing recover from birthday 595 }) 596 if err != nil { 597 w.log.Errorf("Failed to reset wallet manager sync height: %v", err) 598 } 599 } 600 601 // dropTransactionHistory drops the transaction history. It is based off of the 602 // dropwtxmgr utility in the bchwallet repo. 603 func (w *bchSPVWallet) dropTransactionHistory() error { 604 w.log.Info("Dropping wallet transaction history") 605 606 return walletdb.Update(w.Database(), func(tx walletdb.ReadWriteTx) error { 607 err := tx.DeleteTopLevelBucket(wtxmgrNamespace) 608 if err != nil && err != walletdb.ErrBucketNotFound { 609 return err 610 } 611 ns, err := tx.CreateTopLevelBucket(wtxmgrNamespace) 612 if err != nil { 613 return err 614 } 615 err = bchwtxmgr.Create(ns) 616 if err != nil { 617 return err 618 } 619 620 ns = tx.ReadWriteBucket(waddrmgrNamespace) 621 birthdayBlock, err := bchwaddrmgr.FetchBirthdayBlock(ns) 622 if err != nil { 623 fmt.Println("Wallet does not have a birthday block " + 624 "set, falling back to rescan from genesis") 625 626 startBlock, err := bchwaddrmgr.FetchStartBlock(ns) 627 if err != nil { 628 return err 629 } 630 return bchwaddrmgr.PutSyncedTo(ns, startBlock) 631 } 632 633 // We'll need to remove our birthday block first because it 634 // serves as a barrier when updating our state to detect reorgs 635 // due to the wallet not storing all block hashes of the chain. 636 if err := bchwaddrmgr.DeleteBirthdayBlock(ns); err != nil { 637 return err 638 } 639 640 if err := bchwaddrmgr.PutSyncedTo(ns, &birthdayBlock); err != nil { 641 return err 642 } 643 return bchwaddrmgr.PutBirthdayBlock(ns, birthdayBlock) 644 }) 645 } 646 647 func (w *bchSPVWallet) WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) { 648 txDetails, err := w.txDetails((*bchchainhash.Hash)(txHash)) 649 if err != nil { 650 return nil, err 651 } 652 653 btcTx, err := convertMsgTxToBTC(&txDetails.MsgTx) 654 if err != nil { 655 return nil, err 656 } 657 658 credits := make([]wtxmgr.CreditRecord, len(txDetails.Credits)) 659 for i, c := range txDetails.Credits { 660 credits[i] = wtxmgr.CreditRecord{ 661 Amount: btcutil.Amount(c.Amount), 662 Index: c.Index, 663 Spent: c.Spent, 664 Change: c.Change, 665 } 666 } 667 668 debits := make([]wtxmgr.DebitRecord, len(txDetails.Debits)) 669 for i, d := range txDetails.Debits { 670 debits[i] = wtxmgr.DebitRecord{ 671 Amount: btcutil.Amount(d.Amount), 672 Index: d.Index, 673 } 674 } 675 676 return &wtxmgr.TxDetails{ 677 TxRecord: wtxmgr.TxRecord{ 678 MsgTx: *btcTx, 679 Hash: chainhash.Hash(txDetails.TxRecord.Hash), 680 Received: txDetails.TxRecord.Received, 681 SerializedTx: txDetails.TxRecord.SerializedTx, 682 }, 683 Block: wtxmgr.BlockMeta{ 684 Block: wtxmgr.Block{ 685 Hash: chainhash.Hash(txDetails.Block.Hash), 686 Height: txDetails.Block.Height, 687 }, 688 Time: txDetails.Block.Time, 689 }, 690 Credits: credits, 691 Debits: debits, 692 }, nil 693 } 694 695 func (w *bchSPVWallet) SyncedTo() waddrmgr.BlockStamp { 696 bs := w.Manager.SyncedTo() 697 return waddrmgr.BlockStamp{ 698 Height: bs.Height, 699 Hash: chainhash.Hash(bs.Hash), 700 Timestamp: bs.Timestamp, 701 } 702 703 } 704 705 func (w *bchSPVWallet) SignTx(btcTx *wire.MsgTx) error { 706 bchTx, err := convertMsgTxToBCH(btcTx) 707 if err != nil { 708 return err 709 } 710 711 var prevPkScripts [][]byte 712 var inputValues []bchutil.Amount 713 for _, txIn := range btcTx.TxIn { 714 _, txOut, _, _, err := w.FetchInputInfo(&txIn.PreviousOutPoint) 715 if err != nil { 716 return err 717 } 718 inputValues = append(inputValues, bchutil.Amount(txOut.Value)) 719 prevPkScripts = append(prevPkScripts, txOut.PkScript) 720 // Zero the previous witness and signature script or else 721 // AddAllInputScripts does some weird stuff. 722 txIn.SignatureScript = nil 723 txIn.Witness = nil 724 } 725 726 err = txauthor.AddAllInputScripts(bchTx, prevPkScripts, inputValues, &secretSource{w.Wallet, w.chainParams}) 727 if err != nil { 728 return err 729 } 730 731 if len(bchTx.TxIn) != len(btcTx.TxIn) { 732 return fmt.Errorf("txin count mismatch") 733 } 734 for i, txIn := range btcTx.TxIn { 735 txIn.SignatureScript = bchTx.TxIn[i].SignatureScript 736 } 737 return nil 738 } 739 740 // BlockNotifications returns a channel on which to receive notifications of 741 // newly processed blocks. The caller should only call BlockNotificaitons once. 742 func (w *bchSPVWallet) BlockNotifications(ctx context.Context) <-chan *btc.BlockNotification { 743 cl := w.NtfnServer.TransactionNotifications() 744 ch := make(chan *btc.BlockNotification, 1) 745 go func() { 746 defer cl.Done() 747 for { 748 select { 749 case note := <-cl.C: 750 if len(note.AttachedBlocks) > 0 { 751 lastBlock := note.AttachedBlocks[len(note.AttachedBlocks)-1] 752 select { 753 case ch <- &btc.BlockNotification{ 754 Hash: chainhash.Hash(*lastBlock.Hash), 755 Height: lastBlock.Height, 756 }: 757 default: 758 } 759 } 760 case <-ctx.Done(): 761 return 762 } 763 } 764 }() 765 return ch 766 } 767 768 func (w *bchSPVWallet) Birthday() time.Time { 769 return w.Manager.Birthday() 770 } 771 772 func (w *bchSPVWallet) updateDBBirthday(bday time.Time) error { 773 btcw, isLoaded := w.loader.LoadedWallet() 774 if !isLoaded { 775 return fmt.Errorf("wallet not loaded") 776 } 777 return walletdb.Update(btcw.Database(), func(dbtx walletdb.ReadWriteTx) error { 778 ns := dbtx.ReadWriteBucket(waddrmgrNamespace) 779 return btcw.Manager.SetBirthday(ns, bday) 780 }) 781 } 782 783 func (w *bchSPVWallet) Peers() ([]*asset.WalletPeer, error) { 784 return w.peerManager.Peers() 785 } 786 787 func (w *bchSPVWallet) AddPeer(addr string) error { 788 return w.peerManager.AddPeer(addr) 789 } 790 791 func (w *bchSPVWallet) RemovePeer(addr string) error { 792 return w.peerManager.RemovePeer(addr) 793 } 794 795 func (w *bchSPVWallet) TotalReceivedForAddr(btcAddr btcutil.Address, minConf int32) (btcutil.Amount, error) { 796 bchAddr, err := dexbch.BTCAddrToBCHAddr(btcAddr, w.btcParams) 797 if err != nil { 798 return 0, err 799 } 800 amt, err := w.Wallet.TotalReceivedForAddr(bchAddr, 0) 801 if err != nil { 802 return 0, err 803 } 804 return btcutil.Amount(amt), nil 805 } 806 807 // secretSource is used to locate keys and redemption scripts while signing a 808 // transaction. secretSource satisfies the txauthor.SecretsSource interface. 809 type secretSource struct { 810 w *wallet.Wallet 811 chainParams *bchchaincfg.Params 812 } 813 814 // ChainParams returns the chain parameters. 815 func (s *secretSource) ChainParams() *bchchaincfg.Params { 816 return s.chainParams 817 } 818 819 // GetKey fetches a private key for the specified address. 820 func (s *secretSource) GetKey(addr bchutil.Address) (*bchec.PrivateKey, bool, error) { 821 ma, err := s.w.AddressInfo(addr) 822 if err != nil { 823 return nil, false, err 824 } 825 826 mpka, ok := ma.(bchwaddrmgr.ManagedPubKeyAddress) 827 if !ok { 828 e := fmt.Errorf("managed address type for %v is `%T` but "+ 829 "want waddrmgr.ManagedPubKeyAddress", addr, ma) 830 return nil, false, e 831 } 832 833 privKey, err := mpka.PrivKey() 834 if err != nil { 835 return nil, false, err 836 } 837 838 k, _ /* pub */ := bchec.PrivKeyFromBytes(bchec.S256(), privKey.Serialize()) 839 840 return k, ma.Compressed(), nil 841 } 842 843 // GetScript fetches the redemption script for the specified p2sh/p2wsh address. 844 func (s *secretSource) GetScript(addr bchutil.Address) ([]byte, error) { 845 ma, err := s.w.AddressInfo(addr) 846 if err != nil { 847 return nil, err 848 } 849 850 msa, ok := ma.(bchwaddrmgr.ManagedScriptAddress) 851 if !ok { 852 e := fmt.Errorf("managed address type for %v is `%T` but "+ 853 "want waddrmgr.ManagedScriptAddress", addr, ma) 854 return nil, e 855 } 856 return msa.Script() 857 } 858 859 // spvService embeds gcash neutrino.ChainService and translates types. 860 type spvService struct { 861 *neutrino.ChainService 862 } 863 864 var _ btc.SPVService = (*spvService)(nil) 865 866 func (s *spvService) GetBlockHash(height int64) (*chainhash.Hash, error) { 867 bchHash, err := s.ChainService.GetBlockHash(height) 868 if err != nil { 869 return nil, err 870 } 871 return (*chainhash.Hash)(bchHash), nil 872 } 873 874 func (s *spvService) BestBlock() (*headerfs.BlockStamp, error) { 875 bs, err := s.ChainService.BestBlock() 876 if err != nil { 877 return nil, err 878 } 879 return &headerfs.BlockStamp{ 880 Height: bs.Height, 881 Hash: chainhash.Hash(bs.Hash), 882 Timestamp: bs.Timestamp, 883 }, nil 884 } 885 886 func (s *spvService) Peers() []btc.SPVPeer { 887 rawPeers := s.ChainService.Peers() 888 peers := make([]btc.SPVPeer, len(rawPeers)) 889 for i, p := range rawPeers { 890 peers[i] = p 891 } 892 return peers 893 } 894 895 func (s *spvService) AddPeer(addr string) error { 896 return s.ChainService.ConnectNode(addr, true) 897 } 898 899 func (s *spvService) RemovePeer(addr string) error { 900 return s.ChainService.RemoveNodeByAddr(addr) 901 } 902 903 func (s *spvService) GetBlockHeight(h *chainhash.Hash) (int32, error) { 904 return s.ChainService.GetBlockHeight((*bchchainhash.Hash)(h)) 905 } 906 907 func (s *spvService) GetBlockHeader(h *chainhash.Hash) (*wire.BlockHeader, error) { 908 hdr, err := s.ChainService.GetBlockHeader((*bchchainhash.Hash)(h)) 909 if err != nil { 910 return nil, err 911 } 912 return &wire.BlockHeader{ 913 Version: hdr.Version, 914 PrevBlock: chainhash.Hash(hdr.PrevBlock), 915 MerkleRoot: chainhash.Hash(hdr.MerkleRoot), 916 Timestamp: hdr.Timestamp, 917 Bits: hdr.Bits, 918 Nonce: hdr.Nonce, 919 }, nil 920 } 921 922 func (s *spvService) GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, _ ...btcneutrino.QueryOption) (*gcs.Filter, error) { 923 f, err := s.ChainService.GetCFilter(bchchainhash.Hash(blockHash), bchwire.GCSFilterRegular) 924 if err != nil { 925 return nil, err 926 } 927 928 b, err := f.Bytes() 929 if err != nil { 930 return nil, err 931 } 932 933 return gcs.FromBytes(f.N(), f.P(), DefaultM, b) 934 } 935 936 func (s *spvService) GetBlock(blockHash chainhash.Hash, _ ...btcneutrino.QueryOption) (*btcutil.Block, error) { 937 blk, err := s.ChainService.GetBlock(bchchainhash.Hash(blockHash)) 938 if err != nil { 939 return nil, err 940 } 941 942 b, err := blk.Bytes() 943 if err != nil { 944 return nil, err 945 } 946 947 return btcutil.NewBlockFromBytes(b) 948 } 949 950 func convertMsgTxToBTC(tx *bchwire.MsgTx) (*wire.MsgTx, error) { 951 buf := new(bytes.Buffer) 952 if err := tx.Serialize(buf); err != nil { 953 return nil, err 954 } 955 956 btcTx := new(wire.MsgTx) 957 if err := btcTx.Deserialize(buf); err != nil { 958 return nil, err 959 } 960 return btcTx, nil 961 } 962 963 func convertMsgTxToBCH(tx *wire.MsgTx) (*bchwire.MsgTx, error) { 964 buf := new(bytes.Buffer) 965 if err := tx.Serialize(buf); err != nil { 966 return nil, err 967 } 968 bchTx := new(bchwire.MsgTx) 969 if err := bchTx.Deserialize(buf); err != nil { 970 return nil, err 971 } 972 return bchTx, nil 973 } 974 975 func extendAddresses(extIdx, intIdx uint32, bchw *wallet.Wallet) error { 976 scopedKeyManager, err := bchw.Manager.FetchScopedKeyManager(bchwaddrmgr.KeyScopeBIP0044) 977 if err != nil { 978 return err 979 } 980 981 return walletdb.Update(bchw.Database(), func(dbtx walletdb.ReadWriteTx) error { 982 ns := dbtx.ReadWriteBucket(waddrmgrNamespace) 983 if extIdx > 0 { 984 scopedKeyManager.ExtendExternalAddresses(ns, defaultAcctNum, extIdx) 985 } 986 if intIdx > 0 { 987 scopedKeyManager.ExtendInternalAddresses(ns, defaultAcctNum, intIdx) 988 } 989 return nil 990 }) 991 } 992 993 var ( 994 loggingInited uint32 995 logFileName = "neutrino.log" 996 ) 997 998 // logRotator initializes a rotating file logger. 999 func logRotator(walletDir string) (*rotator.Rotator, error) { 1000 const maxLogRolls = 8 1001 logDir := filepath.Join(walletDir, logDirName) 1002 if err := os.MkdirAll(logDir, 0744); err != nil { 1003 return nil, fmt.Errorf("error creating log directory: %w", err) 1004 } 1005 1006 logFilename := filepath.Join(logDir, logFileName) 1007 return rotator.New(logFilename, 32*1024, false, maxLogRolls) 1008 } 1009 1010 // logNeutrino initializes logging in the neutrino + wallet packages. Logging 1011 // only has to be initialized once, so an atomic flag is used internally to 1012 // return early on subsequent invocations. 1013 // 1014 // In theory, the rotating file logger must be Closed at some point, but 1015 // there are concurrency issues with that since btcd and btcwallet have 1016 // unsupervised goroutines still running after shutdown. So we leave the rotator 1017 // running at the risk of losing some logs. 1018 func logNeutrino(walletDir string, errorLogger dex.Logger) error { 1019 if !atomic.CompareAndSwapUint32(&loggingInited, 0, 1) { 1020 return nil 1021 } 1022 1023 logSpinner, err := logRotator(walletDir) 1024 if err != nil { 1025 return fmt.Errorf("error initializing log rotator: %w", err) 1026 } 1027 1028 backendLog := bchlog.NewBackend(logSpinner) 1029 1030 logger := func(name string, lvl bchlog.Level) bchlog.Logger { 1031 l := backendLog.Logger(name) 1032 l.SetLevel(lvl) 1033 return &fileLoggerPlus{Logger: l, log: errorLogger.SubLogger(name)} 1034 } 1035 1036 neutrino.UseLogger(logger("NTRNO", bchlog.LevelDebug)) 1037 wallet.UseLogger(logger("BTCW", bchlog.LevelInfo)) 1038 bchwtxmgr.UseLogger(logger("TXMGR", bchlog.LevelInfo)) 1039 chain.UseLogger(logger("CHAIN", bchlog.LevelInfo)) 1040 1041 return nil 1042 } 1043 1044 // fileLoggerPlus logs everything to a file, and everything with level >= warn 1045 // to both file and a specified dex.Logger. 1046 type fileLoggerPlus struct { 1047 bchlog.Logger 1048 log dex.Logger 1049 } 1050 1051 func (f *fileLoggerPlus) Warnf(format string, params ...any) { 1052 f.log.Warnf(format, params...) 1053 f.Logger.Warnf(format, params...) 1054 } 1055 1056 func (f *fileLoggerPlus) Errorf(format string, params ...any) { 1057 f.log.Errorf(format, params...) 1058 f.Logger.Errorf(format, params...) 1059 } 1060 1061 func (f *fileLoggerPlus) Criticalf(format string, params ...any) { 1062 f.log.Criticalf(format, params...) 1063 f.Logger.Criticalf(format, params...) 1064 } 1065 1066 func (f *fileLoggerPlus) Warn(v ...any) { 1067 f.log.Warn(v...) 1068 f.Logger.Warn(v...) 1069 } 1070 1071 func (f *fileLoggerPlus) Error(v ...any) { 1072 f.log.Error(v...) 1073 f.Logger.Error(v...) 1074 } 1075 1076 func (f *fileLoggerPlus) Critical(v ...any) { 1077 f.log.Critical(v...) 1078 f.Logger.Critical(v...) 1079 } 1080 1081 type logAdapter struct { 1082 dex.Logger 1083 } 1084 1085 var _ bchlog.Logger = (*logAdapter)(nil) 1086 1087 func (a *logAdapter) Level() bchlog.Level { 1088 return bchlog.Level(a.Logger.Level()) 1089 } 1090 1091 func (a *logAdapter) SetLevel(lvl bchlog.Level) { 1092 a.Logger.SetLevel(slog.Level(lvl)) 1093 }