From eef4ef0c9698540af76909da6956e1aff24a8318 Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Wed, 13 Oct 2021 01:25:58 -0500 Subject: [PATCH] chappjc review followup 1 Refactor initialization and connection. Move nearly all instantiation to connect. There is an assumption here that the caller will never try to use an unconnected wallet to do anything. Upon inspection, I think this is how we've designed things, but it's worth mentioning since calling some methods before connecting will definitely panic now. Log btcwallet and neutrino output to a rotating log file. Combine loadChainClient and startWallet functions into a single (*spvWallet).startWallet method. --- client/asset/btc/btc.go | 3 +- client/asset/btc/btc_test.go | 2 +- client/asset/btc/livetest/livetest.go | 23 +- client/asset/btc/livetest/regnet_test.go | 8 +- client/asset/btc/rpcclient.go | 4 +- client/asset/btc/spv.go | 400 ++++++++++++----------- client/asset/btc/wallet.go | 1 - client/asset/driver.go | 1 + client/core/core.go | 1 + go.mod | 2 +- go.sum | 3 +- 11 files changed, 242 insertions(+), 206 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 700fd2b941..60e6b8be5c 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -447,7 +447,7 @@ func (d *Driver) Create(params *asset.CreateWalletParams) error { return fmt.Errorf("error parsing chain: %w", err) } - return createSPVWallet(params.Pass, params.Seed, params.DataDir, chainParams) + return createSPVWallet(params.Pass, params.Seed, params.DataDir, params.Logger, chainParams) } // Open opens or connects to the BTC exchange wallet. Start the wallet with its @@ -786,7 +786,6 @@ func (btc *ExchangeWallet) shutdown() { delete(btc.findRedemptionQueue, contractOutpoint) } btc.findRedemptionMtx.Unlock() - btc.node.stop() } // getBlockchainInfoResult models the data returned from the getblockchaininfo diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index 5846377d7b..4b07a726c1 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -428,7 +428,7 @@ func (c *tRawRequester) RawRequest(_ context.Context, method string, params []js const testBlocksPerBlockTimeOffset = 4 func generateTestBlockTime(blockHeight int64) time.Time { - return time.Unix(1e6, 0).Add(time.Duration(blockHeight) * maxBlockTimeOffset / testBlocksPerBlockTimeOffset) + return time.Unix(1e6, 0).Add(time.Duration(blockHeight) * maxFutureBlockTime / testBlocksPerBlockTimeOffset) } func (c *testData) addRawTx(blockHeight int64, tx *wire.MsgTx) (*chainhash.Hash, *wire.MsgBlock) { diff --git a/client/asset/btc/livetest/livetest.go b/client/asset/btc/livetest/livetest.go index bcc6194108..216eff9d0b 100644 --- a/client/asset/btc/livetest/livetest.go +++ b/client/asset/btc/livetest/livetest.go @@ -160,6 +160,23 @@ func Run(t *testing.T, cfg *Config) { rig.backends["beta"], rig.connectionMasters["beta"] = tBackend(tCtx, t, cfg, "beta", "", tLogger.SubLogger("beta"), blkFunc) rig.backends["gamma"], rig.connectionMasters["gamma"] = tBackend(tCtx, t, cfg, "alpha", "gamma", tLogger.SubLogger("gamma"), blkFunc) defer rig.close() + + // Unlock the wallet for use. + err := rig.alpha().Unlock(walletPassword) + if err != nil { + t.Fatalf("error unlocking gamma wallet: %v", err) + } + + if cfg.SPV { + // The test expects beta and gamma to be unlocked. + if err := rig.beta().Unlock(walletPassword); err != nil { + t.Fatalf("beta Unlock error: %v", err) + } + if err := rig.gamma().Unlock(walletPassword); err != nil { + t.Fatalf("gamma Unlock error: %v", err) + } + } + var lots uint64 = 2 contractValue := lots * cfg.LotSize @@ -184,12 +201,6 @@ func Run(t *testing.T, cfg *Config) { name, float64(bal.Available)/1e8, float64(bal.Immature)/1e8, float64(bal.Locked)/1e8) } - // Unlock the wallet for use. - err := rig.alpha().Unlock(walletPassword) - if err != nil { - t.Fatalf("error unlocking gamma wallet: %v", err) - } - ord := &asset.Order{ Value: contractValue * 3, MaxSwapCount: lots * 3, diff --git a/client/asset/btc/livetest/regnet_test.go b/client/asset/btc/livetest/regnet_test.go index 0137beb196..da2b695338 100644 --- a/client/asset/btc/livetest/regnet_test.go +++ b/client/asset/btc/livetest/regnet_test.go @@ -99,6 +99,7 @@ func TestWallet(t *testing.T) { Pass: tPW, // match walletPassword in livetest.go -> Run DataDir: cfg.DataDir, Net: dex.Simnet, + Logger: logger, }) if err != nil { return fmt.Errorf("error creating SPV wallet: %w", err) @@ -173,13 +174,6 @@ func TestWallet(t *testing.T) { return nil, err } - // The test expects beta and gamma to be unlocked. - if name == "beta" || name == "gamma" { - if err := w.Unlock(tPW); err != nil { - return nil, err - } - } - return w, nil } diff --git a/client/asset/btc/rpcclient.go b/client/asset/btc/rpcclient.go index 5ccdb13d3d..4b96d8dbf3 100644 --- a/client/asset/btc/rpcclient.go +++ b/client/asset/btc/rpcclient.go @@ -111,8 +111,6 @@ func (wc *rpcClient) connect(ctx context.Context) error { return nil } -func (wc *rpcClient) stop() {} - // RawRequest passes the reqeuest to the wallet's RawRequester. func (wc *rpcClient) RawRequest(method string, params []json.RawMessage) (json.RawMessage, error) { return wc.requester.RawRequest(wc.ctx, method, params) @@ -406,7 +404,7 @@ func (wc *rpcClient) walletLock() error { } // sendToAddress sends the amount to the address. feeRate is in units of -// atoms/byte. +// sats/byte. func (wc *rpcClient) sendToAddress(address string, value, feeRate uint64, subtract bool) (*chainhash.Hash, error) { var success bool // 1e-5 = 1e-8 for satoshis * 1000 for kB. diff --git a/client/asset/btc/spv.go b/client/asset/btc/spv.go index 6ddc017d08..f5090d248c 100644 --- a/client/asset/btc/spv.go +++ b/client/asset/btc/spv.go @@ -4,8 +4,8 @@ // spvWallet implements a Wallet backed by a built-in btcwallet + Neutrino. // // There are a few challenges presented in using an SPV wallet for DEX. -// 1. Finding non-wallet related blockchain data requires posession of the -// pubkey script, not just transction hash and output index +// 1. Finding non-wallet related blockchain data requires possession of the +// pubkey script, not just transaction hash and output index // 2. Finding non-wallet related blockchain data can often entail extensive // scanning of compact filters. We can limit these scans with more // information, such as the match time, which would be the earliest a @@ -39,6 +39,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btclog" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/gcs" "github.com/btcsuite/btcutil/psbt" @@ -47,8 +48,9 @@ import ( "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/btcsuite/btcwallet/walletdb" - _ "github.com/btcsuite/btcwallet/walletdb/bdb" + _ "github.com/btcsuite/btcwallet/walletdb/bdb" // bdb init() registers a driver "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/jrick/logrotate/rotator" "github.com/lightninglabs/neutrino" "github.com/lightninglabs/neutrino/headerfs" ) @@ -58,13 +60,11 @@ const ( SpentStatusUnknown = dex.ErrorKind("spend status not known") // see btcd/blockchain/validate.go - maxBlockTimeOffset = 2 * time.Hour + maxFutureBlockTime = 2 * time.Hour neutrinoDBName = "neutrino.db" -) - -var ( - SimnetSeed = []byte("simnet-seed-of-considerable-length") - SimnetAddress = "bcrt1qq8t6vmptznycut4f3vxtguxgknmcnmpqlvd8wf" + logDirName = "logs" + defaultAcctNum = 0 + defaultAcctName = "default" ) // btcWallet is satisfied by *btcwallet.Wallet. @@ -109,116 +109,70 @@ type neutrinoService interface { Stop() error } -// hashMap manages a mapping of chainhash.Hash -> chainhash.Hash. hashMap is -// satisfied by hashmap.Map, or a stub for testing purposes. -type hashMap interface { - Get(chainhash.Hash) *chainhash.Hash - Set(k, v chainhash.Hash) - Run(context.Context) - Close() error -} - -// spvConfig is configuration for the built-in SPV wallet. -type spvConfig struct { - dbDir string - chainParams *chaincfg.Params - connectPeers []string -} - -// loadChainClient initializes the *btcwallet.Wallet and its supporting players. -// The returned wallet is not syncing. -func loadChainClient(cfg *spvConfig) (*wallet.Wallet, *wallet.Loader, *neutrino.ChainService, walletdb.DB, error) { - netDir := filepath.Join(cfg.dbDir, cfg.chainParams.Name) - - // timeout and recoverWindow arguments borrowed from btcwallet directly. - loader := wallet.NewLoader(cfg.chainParams, netDir, true, 60*time.Second, 250) - - exists, err := loader.WalletExists() - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("error verifying wallet existence: %v", err) - } - if !exists { - return nil, nil, nil, nil, fmt.Errorf("wallet not found") - } - - w, err := loader.OpenExistingWallet([]byte(wallet.InsecurePubPassphrase), false) - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("couldn't load wallet") - } - - neutrinoDBPath := filepath.Join(netDir, neutrinoDBName) - neutrinoDB, err := walletdb.Create("bdb", neutrinoDBPath, true, 5*time.Second) - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) - } - - // If we're on regtest and the peers haven't been explicitly set, add the - // simnet harness alpha node as an additional peer so we don't have to type - // it in. - var addPeers []string - if cfg.chainParams.Name == "regtest" && len(cfg.connectPeers) == 0 { - addPeers = append(addPeers, "localhost:20575") - } - - chainService, err := neutrino.NewChainService(neutrino.Config{ - DataDir: netDir, - Database: neutrinoDB, - ChainParams: *cfg.chainParams, - ConnectPeers: cfg.connectPeers, - AddPeers: addPeers, - }) - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("couldn't create Neutrino ChainService: %v", err) - } - - return w, loader, chainService, neutrinoDB, nil -} - -// startWallet starts neutrino and begins the wallet sync. -func startWallet(loader *wallet.Loader, chainClient *chain.NeutrinoClient) error { - w, loaded := loader.LoadedWallet() - if !loaded { - return fmt.Errorf("wallet not loaded") - } - err := chainClient.Start() - if err != nil { - return fmt.Errorf("couldn't start Neutrino client: %v", err) - } - - w.SynchronizeRPC(chainClient) - return nil -} - // createSPVWallet creates a new SPV wallet. -func createSPVWallet(privPass []byte, seed []byte, dbDir string, net *chaincfg.Params) error { +func createSPVWallet(privPass []byte, seed []byte, dbDir string, log dex.Logger, net *chaincfg.Params) error { netDir := filepath.Join(dbDir, net.Name) - err := os.MkdirAll(netDir, 0777) + logDir := filepath.Join(netDir, logDirName) + err := os.MkdirAll(logDir, 0777) if err != nil { return fmt.Errorf("error creating wallet directories: %v", err) } loader := wallet.NewLoader(net, netDir, true, 60*time.Second, 250) - defer loader.UnloadWallet() pubPass := []byte(wallet.InsecurePubPassphrase) - _, err = loader.CreateNewWallet(pubPass, privPass, seed, time.Now()) if err != nil { return fmt.Errorf("CreateNewWallet error: %w", err) } + bailOnWallet := func() { + if err := loader.UnloadWallet(); err != nil { + log.Errorf("Error unloading wallet after createSPVWallet error: %v", err) + } + } + neutrinoDBPath := filepath.Join(netDir, neutrinoDBName) db, err := walletdb.Create("bdb", neutrinoDBPath, true, 5*time.Second) if err != nil { + go bailOnWallet() return fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) } - if err := db.Close(); err != nil { + if err = db.Close(); err != nil { + go bailOnWallet() return fmt.Errorf("error closing newly created wallet database: %w", err) } + if err := loader.UnloadWallet(); err != nil { + return fmt.Errorf("error unloading wallet: %w", err) + } + return nil } +// spendingInput is added to a filterScanResult if a spending input is found. +type spendingInput struct { + txHash chainhash.Hash + vin uint32 + blockHash chainhash.Hash + blockHeight uint32 +} + +// filterScanResult is the result from a filter scan. +type filterScanResult struct { + // blockHash is the block that the output was found in. + blockHash *chainhash.Hash + // blockHeight is the height of the block that the output was found in. + blockHeight uint32 + // txOut is the output itself. + txOut *wire.TxOut + // spend will be set if a spending input is found. + spend *spendingInput + // checkpoint is used to track the last block scanned so that future scans + // can skip scanned blocks. + checkpoint chainhash.Hash +} + // hashEntry stores a chainhash.Hash with a last-access time that can be used // for cache maintenance. type hashEntry struct { @@ -234,17 +188,29 @@ type scanCheckpoint struct { lastAccess time.Time } +// logWriter implements an io.Writer that outputs to a rotating log file. +type logWriter struct { + *rotator.Rotator +} + +// Write writes the data in p to the log file. +func (w logWriter) Write(p []byte) (n int, err error) { + return w.Rotator.Write(p) +} + // spvWallet is an in-process btcwallet.Wallet + neutrino light-filter-based // Bitcoin wallet. spvWallet controls an instance of btcwallet.Wallet directly // and does not run or connect to the RPC server. type spvWallet struct { - ctx context.Context - chainParams *chaincfg.Params - wallet btcWallet - cl neutrinoService - chainClient *chain.NeutrinoClient - acctNum uint32 - neutrinoDB walletdb.DB + chainParams *chaincfg.Params + wallet btcWallet + cl neutrinoService + chainClient *chain.NeutrinoClient + acctNum uint32 + acctName string + netDir string + neutrinoDB walletdb.DB + connectPeers []string txBlocksMtx sync.Mutex txBlocks map[chainhash.Hash]*hashEntry @@ -260,26 +226,17 @@ var _ Wallet = (*spvWallet)(nil) // loadSPVWallet loads an existing wallet. func loadSPVWallet(dbDir string, logger dex.Logger, connectPeers []string, chainParams *chaincfg.Params) (*spvWallet, error) { - w, loader, chainService, neutrinoDB, err := loadChainClient(&spvConfig{ - dbDir: dbDir, - chainParams: chainParams, - connectPeers: connectPeers, - }) - if err != nil { - return nil, err - } + netDir := filepath.Join(dbDir, chainParams.Name) return &spvWallet{ - chainParams: chainParams, - cl: chainService, - chainClient: chain.NewNeutrinoClient(chainParams, chainService), - acctNum: 0, - wallet: &walletExtender{w, chainParams}, - neutrinoDB: neutrinoDB, - txBlocks: make(map[chainhash.Hash]*hashEntry), - checkpoints: make(map[outPoint]*scanCheckpoint), - log: logger, - loader: loader, + chainParams: chainParams, + acctNum: defaultAcctNum, + acctName: defaultAcctName, + netDir: netDir, + txBlocks: make(map[chainhash.Hash]*hashEntry), + checkpoints: make(map[outPoint]*scanCheckpoint), + log: logger, + connectPeers: connectPeers, }, nil } @@ -320,8 +277,8 @@ func (w *spvWallet) cacheCheckpoint(txHash *chainhash.Hash, vout uint32, res *fi } } -// checkpoint returns any cached *filterScanResult for the outpoint. -func (w *spvWallet) checkpoint(txHash *chainhash.Hash, vout uint32) *filterScanResult { +// unvalidatedCheckpoint returns any cached *filterScanResult for the outpoint. +func (w *spvWallet) unvalidatedCheckpoint(txHash *chainhash.Hash, vout uint32) *filterScanResult { w.checkpointMtx.Lock() defer w.checkpointMtx.Unlock() check, found := w.checkpoints[newOutPoint(txHash, vout)] @@ -329,26 +286,26 @@ func (w *spvWallet) checkpoint(txHash *chainhash.Hash, vout uint32) *filterScanR return nil } check.lastAccess = time.Now() - return check.res + res := *check.res + return &res } -// checkpointBlock returns a filterScanResult and the checkpoint block hash. If -// a result is found with an orphaned checkpoint block hash, it is cleared from +// checkpoint returns a filterScanResult and the checkpoint block hash. If a +// result is found with an orphaned checkpoint block hash, it is cleared from // the cache and not returned. -func (w *spvWallet) checkpointBlock(txHash *chainhash.Hash, vout uint32) (*filterScanResult, *chainhash.Hash) { - res := w.checkpoint(txHash, vout) +func (w *spvWallet) checkpoint(txHash *chainhash.Hash, vout uint32) *filterScanResult { + res := w.unvalidatedCheckpoint(txHash, vout) if res == nil { - return nil, nil + return nil } - blockHash := &res.checkpoint - if !w.blockIsMainchain(blockHash, -1) { + if !w.blockIsMainchain(&res.checkpoint, -1) { // reorg detected, abandon the checkpoint. w.checkpointMtx.Lock() delete(w.checkpoints, newOutPoint(txHash, vout)) w.checkpointMtx.Unlock() - return nil, nil + return nil } - return res, blockHash + return res } func (w *spvWallet) RawRequest(method string, params []json.RawMessage) (json.RawMessage, error) { @@ -463,7 +420,7 @@ func (w *spvWallet) balances() (*GetBalancesResult, error) { // ListUnspent retrieves list of the wallet's UTXOs. func (w *spvWallet) listUnspent() ([]*ListUnspentResult, error) { - unspents, err := w.wallet.ListUnspent(0, math.MaxInt32, "default") + unspents, err := w.wallet.ListUnspent(0, math.MaxInt32, w.acctName) if err != nil { return nil, err } @@ -479,16 +436,22 @@ func (w *spvWallet) listUnspent() ([]*ListUnspentResult, error) { if err != nil { return nil, fmt.Errorf("error decoding txid %q: %v", utxo.TxID, err) } - tx, _, _, _, err := w.wallet.FetchInputInfo(wire.NewOutPoint(txHash, utxo.Vout)) + txDetails, err := w.wallet.walletTransaction(txHash) if err != nil { - return nil, fmt.Errorf("FetchInputInfo error: %v", err) + return nil, fmt.Errorf("walletTransaction error: %v", err) } // To be "safe", we need to show that we own the inputs for the // utxo's transaction. We'll just try to find one. - for _, txIn := range tx.TxIn { + safe = true + // TODO: Keep a cache of our redemption outputs and allow those as + // safe inputs. + for _, txIn := range txDetails.MsgTx.TxIn { _, _, _, _, err := w.wallet.FetchInputInfo(&txIn.PreviousOutPoint) - if err == nil { - safe = true + if err != nil { + if !errors.Is(err, wallet.ErrNotMine) { + w.log.Warnf("FetchInputInfo error: %v", err) + } + safe = false break } } @@ -592,7 +555,7 @@ func (w *spvWallet) privKeyForAddress(addr string) (*btcec.PrivateKey, error) { // Unlock unlocks the wallet. func (w *spvWallet) Unlock(pw []byte) error { - return w.wallet.Unlock(pw, time.After(time.Duration(math.MaxInt64))) + return w.wallet.Unlock(pw, nil) } // Lock locks the wallet. @@ -602,7 +565,7 @@ func (w *spvWallet) Lock() error { } // sendToAddress sends the amount to the address. feeRate is in units of -// atoms/byte. +// sats/byte. func (w *spvWallet) sendToAddress(address string, value, feeRate uint64, subtract bool) (*chainhash.Hash, error) { addr, err := btcutil.DecodeAddress(address, w.chainParams) if err != nil { @@ -616,10 +579,8 @@ func (w *spvWallet) sendToAddress(address string, value, feeRate uint64, subtrac wireOP := wire.NewTxOut(int64(value), pkScript) - feeRateAmt, err := btcutil.NewAmount(float64(feeRate) / 1e5) - if err != nil { - return nil, err - } + // converting sats/vB -> sats/kvB + feeRateAmt := btcutil.Amount(feeRate * 1e3) // Could try with minconf 1 first. tx, err := w.wallet.SendOutputs([]*wire.TxOut{wireOP}, nil, w.acctNum, 0, feeRateAmt, "") @@ -632,7 +593,7 @@ func (w *spvWallet) sendToAddress(address string, value, feeRate uint64, subtrac return &txHash, nil } -// swapConfirmations attempts to get the numbe of confirmations and the spend +// swapConfirmations attempts to get the number of confirmations and the spend // status for the specified tx output. For swap outputs that were not generated // by this wallet, startTime must be supplied to limit the search. Use the match // time assigned by the server. @@ -791,9 +752,19 @@ func (w *spvWallet) calcMedianTime(blockHash *chainhash.Hash) (time.Time, error) // connect will start the wallet and begin syncing. func (w *spvWallet) connect(ctx context.Context) error { - w.ctx = ctx + const maxLogRolls = 8 + logFilename := filepath.Join(w.netDir, logDirName, "neutrino.log") + rotator, err := rotator.New(logFilename, 32*1024, false, maxLogRolls) + if err != nil { + return fmt.Errorf("error initializing neutrino log files: %w", err) + } + backendLog := btclog.NewBackend(logWriter{rotator}) + neutrino.UseLogger(backendLog.Logger("NTRNO")) + wallet.UseLogger(backendLog.Logger("BTCW")) + wtxmgr.UseLogger(backendLog.Logger("TXMGR")) + chain.UseLogger(backendLog.Logger("CHAIN")) - err := startWallet(w.loader, w.chainClient) + err = w.startWallet() if err != nil { return err } @@ -807,6 +778,7 @@ func (w *spvWallet) connect(ctx context.Context) error { go func() { ticker := time.NewTicker(time.Minute * 20) expiration := time.Hour * 2 + out: for { select { case <-ticker.C: @@ -826,14 +798,98 @@ func (w *spvWallet) connect(ctx context.Context) error { } w.checkpointMtx.Unlock() case <-ctx.Done(): - return + break out } } + w.stop() }() return nil } +// startWallet initializes the *btcwallet.Wallet and its supporting players and +// starts syncing. +func (w *spvWallet) startWallet() error { + // timeout and recoverWindow arguments borrowed from btcwallet directly. + w.loader = wallet.NewLoader(w.chainParams, w.netDir, true, 60*time.Second, 250) + + exists, err := w.loader.WalletExists() + if err != nil { + return fmt.Errorf("error verifying wallet existence: %v", err) + } + if !exists { + return fmt.Errorf("wallet not found") + } + + btcw, err := w.loader.OpenExistingWallet([]byte(wallet.InsecurePubPassphrase), false) + if err != nil { + return fmt.Errorf("couldn't load wallet") + } + + bailOnWallet := func() { + if err := w.loader.UnloadWallet(); err != nil { + w.log.Errorf("Error unloading wallet after loadChainClient error: %v", err) + } + } + + neutrinoDBPath := filepath.Join(w.netDir, neutrinoDBName) + w.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true, wallet.DefaultDBTimeout) + if err != nil { + go bailOnWallet() + return fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) + } + + bailOnWalletAndDB := func() { + if err := w.neutrinoDB.Close(); err != nil { + w.log.Errorf("Error closing neutrino database after loadChainClient error: %v", err) + } + bailOnWallet() + } + + // If we're on regtest and the peers haven't been explicitly set, add the + // simnet harness alpha node as an additional peer so we don't have to type + // it in. + var addPeers []string + if w.chainParams.Name == "regtest" && len(w.connectPeers) == 0 { + addPeers = append(addPeers, "localhost:20575") + } + + chainService, err := neutrino.NewChainService(neutrino.Config{ + DataDir: w.netDir, + Database: w.neutrinoDB, + ChainParams: *w.chainParams, + ConnectPeers: w.connectPeers, + AddPeers: addPeers, + }) + if err != nil { + go bailOnWalletAndDB() + return fmt.Errorf("couldn't create Neutrino ChainService: %v", err) + } + + bailOnEverything := func() { + if err := chainService.Stop(); err != nil { + w.log.Errorf("Error closing neutrino chain service after loadChainClient error: %v", err) + } + bailOnWalletAndDB() + } + + w.cl = chainService + w.chainClient = chain.NewNeutrinoClient(w.chainParams, chainService) + w.wallet = &walletExtender{btcw, w.chainParams} + + if err = w.chainClient.Start(); err != nil { + go bailOnEverything() + if err != nil { + w.log.Errorf("error unloading wallet after chain client start error: %v", err) + } + return fmt.Errorf("couldn't start Neutrino client: %v", err) + } + + btcw.SynchronizeRPC(w.chainClient) + + return nil +} + // stop stops the wallet and database threads. func (w *spvWallet) stop() { if err := w.loader.UnloadWallet(); err != nil { @@ -881,7 +937,7 @@ func (w *spvWallet) blockIsMainchain(blockHash *chainhash.Hash, blockHeight int3 } checkHash, err := w.cl.GetBlockHash(int64(blockHeight)) if err != nil { - w.log.Errorf("Error retriving block hash for height %d", blockHeight) + w.log.Errorf("Error retrieving block hash for height %d", blockHeight) return false } @@ -889,7 +945,7 @@ func (w *spvWallet) blockIsMainchain(blockHash *chainhash.Hash, blockHeight int3 } // mainchainBlockForStoredTx gets the block hash and height for the transaction -// IFF an entry has been stored in the blockTxs index. +// IFF an entry has been stored in the txBlocks index. func (w *spvWallet) mainchainBlockForStoredTx(txHash *chainhash.Hash) (*chainhash.Hash, int32) { // Check that the block is still mainchain. blockHash, blockHeight, err := w.blockForStoredTx(txHash) @@ -907,14 +963,14 @@ func (w *spvWallet) mainchainBlockForStoredTx(txHash *chainhash.Hash) (*chainhas } // findBlockForTime locates a good start block so that a search beginning at the -// returned block has a very low likelyhood of missing any blocks that have time +// returned block has a very low likelihood of missing any blocks that have time // > matchTime. This is done by performing a binary search (sort.Search) to find -// a block with a block time MAX_FUTURE_BLOCK_TIME before matchTime. To ensure +// a block with a block time maxFutureBlockTime before matchTime. To ensure // we also accommodate the median-block time rule and aren't missing anything // due to out of sequence block times we use an unsophisticated algorithm of // choosing the first block in an 11 block window with no times >= matchTime. func (w *spvWallet) findBlockForTime(matchTime time.Time) (*chainhash.Hash, int32, error) { - offsetTime := matchTime.Add(-maxBlockTimeOffset) + offsetTime := matchTime.Add(-maxFutureBlockTime) bestHeight, err := w.getBestBlockHeight() if err != nil { @@ -946,9 +1002,8 @@ func (w *spvWallet) findBlockForTime(matchTime time.Time) (*chainhash.Hash, int3 } // We're actually breaking an assumption of sort.Search here because block - // times aren't always monotonically increasing. This won't matter though - // as long as there are not > blockTimeSearchBuffer blocks with inverted - // time order. + // times aren't always monotonically increasing. This won't matter though as + // long as there are not > medianTimeBlocks blocks with inverted time order. var count int var iHash *chainhash.Hash var iTime time.Time @@ -979,22 +1034,25 @@ func (w *spvWallet) findBlockForTime(matchTime time.Time) (*chainhash.Hash, int3 // supplied, and the blockHash for the output is not known to the wallet, a // candidate block will be selected with findBlockTime. func (w *spvWallet) scanFilters(txHash *chainhash.Hash, vout uint32, pkScript []byte, startTime time.Time, blockHash *chainhash.Hash) (*filterScanResult, error) { + // TODO: Check that any blockHash supplied is not orphaned? + // Check if we know the block hash for the tx. var limitHeight int32 // See if we have a checkpoint to use. - checkPt, checkBlock := w.checkpointBlock(txHash, vout) + checkPt := w.checkpoint(txHash, vout) if checkPt != nil { if checkPt.blockHash != nil && checkPt.spend != nil { // We already have the output and the spending input, and // checkpointBlock already verified it's still mainchain. return checkPt, nil } - height, err := w.getBlockHeight(checkBlock) + height, err := w.getBlockHeight(&checkPt.checkpoint) if err != nil { return nil, fmt.Errorf("getBlockHeight error: %w", err) } limitHeight = height + 1 } else if blockHash == nil { + // No checkpoint and no block hash. Gotta guess based on time. blockHash, limitHeight = w.mainchainBlockForStoredTx(txHash) if blockHash == nil { var err error @@ -1004,6 +1062,7 @@ func (w *spvWallet) scanFilters(txHash *chainhash.Hash, vout uint32, pkScript [] } } } else { + // No checkpoint, but user supplied a block hash. var err error limitHeight, err = w.getBlockHeight(blockHash) if err != nil { @@ -1011,10 +1070,6 @@ func (w *spvWallet) scanFilters(txHash *chainhash.Hash, vout uint32, pkScript [] } } - // We only care about the limitHeight now, but we can tell if it's been set - // by whether blockHash is nil, since that means it wasn't passed in, and - // it wasn't found in the database. - // Do a filter scan. utxo, err := w.filterScanFromHeight(*txHash, vout, pkScript, limitHeight, checkPt) if err != nil { @@ -1427,29 +1482,6 @@ func (w *walletExtender) signTransaction(tx *wire.MsgTx) error { }) } -// spendingInput is added to a filterScanResult if a spending input is found. -type spendingInput struct { - txHash chainhash.Hash - vin uint32 - blockHash chainhash.Hash - blockHeight uint32 -} - -// filterScanResult is the result from a filter scan. -type filterScanResult struct { - // blockHash is the block that the output was found in. - blockHash *chainhash.Hash - // blockHeight is the height of the block that the output was found in. - blockHeight uint32 - // txOut is the output itself. - txOut *wire.TxOut - // spend will be set if a spending input is found. - spend *spendingInput - // checkpoint is used to track the last block scanned so that future scans - // can skip scanned blocks. - checkpoint chainhash.Hash -} - // secretSource is used to locate keys and redemption scripts while signing a // transaction. secretSource satisfies the txauthor.SecretsSource interface. type secretSource struct { diff --git a/client/asset/btc/wallet.go b/client/asset/btc/wallet.go index 17d91350bb..6590ac0e00 100644 --- a/client/asset/btc/wallet.go +++ b/client/asset/btc/wallet.go @@ -42,5 +42,4 @@ type Wallet interface { searchBlockForRedemptions(ctx context.Context, reqs map[outPoint]*findRedemptionReq, blockHash chainhash.Hash) (discovered map[outPoint]*findRedemptionResult) findRedemptionsInMempool(ctx context.Context, reqs map[outPoint]*findRedemptionReq) (discovered map[outPoint]*findRedemptionResult) getBlock(h chainhash.Hash) (*wire.MsgBlock, error) - stop() } diff --git a/client/asset/driver.go b/client/asset/driver.go index 67458e6b13..6d3a1a0f66 100644 --- a/client/asset/driver.go +++ b/client/asset/driver.go @@ -25,6 +25,7 @@ type CreateWalletParams struct { Settings map[string]string DataDir string Net dex.Network + Logger dex.Logger } // Driver is the interface required of all exchange wallets. diff --git a/client/core/core.go b/client/core/core.go index ae8ef189f2..4ea683eb7a 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -1811,6 +1811,7 @@ func (c *Core) createSeededWallet(assetID uint32, crypter encrypt.Crypter, form Settings: form.Config, DataDir: c.assetDataDirectory(assetID), Net: c.net, + Logger: c.log.SubLogger("CREATE"), }); err != nil { return nil, fmt.Errorf("Error creating wallet: %w", err) } diff --git a/go.mod b/go.mod index d64f3168c3..9f81361c7e 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( decred.org/dcrwallet/v2 v2.0.0-20210913145543-714c2f555f04 github.com/btcsuite/btcd v0.22.0-beta.0.20210803133449-f5a1fb9965e4 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f - github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce + github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 // note: hoists btcd's own require of btcutil github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce github.com/btcsuite/btcwallet v0.12.0 github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0 diff --git a/go.sum b/go.sum index a640e2e100..9a78758663 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,9 @@ github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= -github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 h1:9aGy5p7oXRUB4MCTmWm0+jzuh79GpjPIfv1leA5POD4= +github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce h1:3PRwz+js0AMMV1fHRrCdQ55akoomx4Q3ulozHC3BDDY= github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= github.com/btcsuite/btcwallet v0.12.0 h1:0kT0rDN8vNcAvuHp2qUS/hLsfa0VUn2dNQ2GvM9ozBA=