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