From 14a603568628398644b4869e3c1cd3bf38177b5d Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Fri, 27 May 2022 00:54:24 -0500 Subject: [PATCH 1/5] implement native ltc spv wallet --- client/asset/ltc/ltc.go | 85 ++- client/asset/ltc/spv.go | 1049 +++++++++++++++++++++++++++++ client/cmd/simnet-trade-tests/run | 6 + dex/testing/loadbot/go.mod | 16 +- dex/testing/loadbot/go.sum | 60 +- dex/testing/ltc/harness.sh | 2 +- go.mod | 14 + go.sum | 34 + 8 files changed, 1254 insertions(+), 12 deletions(-) create mode 100644 client/asset/ltc/spv.go diff --git a/client/asset/ltc/ltc.go b/client/asset/ltc/ltc.go index feb58cd6e3..0718063abc 100644 --- a/client/asset/ltc/ltc.go +++ b/client/asset/ltc/ltc.go @@ -4,14 +4,20 @@ package ltc import ( + "errors" "fmt" + "path/filepath" + "time" "decred.org/dcrdex/client/asset" "decred.org/dcrdex/client/asset/btc" "decred.org/dcrdex/dex" + "decred.org/dcrdex/dex/config" dexbtc "decred.org/dcrdex/dex/networks/btc" dexltc "decred.org/dcrdex/dex/networks/ltc" "github.com/btcsuite/btcd/chaincfg" + ltcchaincfg "github.com/ltcsuite/ltcd/chaincfg" + "github.com/ltcsuite/ltcwallet/wallet" ) const ( @@ -24,6 +30,7 @@ const ( defaultFeeRateLimit = 100 minNetworkVersion = 210201 walletTypeRPC = "litecoindRPC" + walletTypeSPV = "SPV" walletTypeLegacy = "" walletTypeElectrum = "electrumRPC" ) @@ -101,14 +108,21 @@ var ( Tab: "Litecoin Core (external)", Description: "Connect to litecoind", DefaultConfigPath: dexbtc.SystemConfigPath("litecoin"), - ConfigOpts: append(walletNameOpt, commonOpts...), + ConfigOpts: append(btc.RPCConfigOpts("Litecoin", "9332"), btc.CommonConfigOpts("LTC", false)...), } electrumWalletDefinition = &asset.WalletDefinition{ Type: walletTypeElectrum, Tab: "Electrum-LTC (external)", Description: "Use an external Electrum-LTC Wallet", // json: DefaultConfigPath: filepath.Join(btcutil.AppDataDir("electrum-ltc", false), "config"), // e.g. ~/.electrum-ltc/config ConfigOpts: append(rpcOpts, commonOpts...), - ConfigOpts: commonOpts, + ConfigOpts: btc.CommonConfigOpts("LTC", false), + } + spvWalletDefinition = &asset.WalletDefinition{ + Type: walletTypeSPV, + Tab: "Native", + Description: "Use the built-in SPV wallet", + ConfigOpts: append(btc.SPVConfigOpts("LTC"), btc.CommonConfigOpts("LTC", false)...), + Seeded: true, } // WalletInfo defines some general information about a Litecoin wallet. WalletInfo = &asset.WalletInfo{ @@ -116,6 +130,7 @@ var ( Version: version, UnitInfo: dexltc.UnitInfo, AvailableWallets: []*asset.WalletDefinition{ + spvWalletDefinition, rpcWalletDefinition, electrumWalletDefinition, }, @@ -149,17 +164,69 @@ func (d *Driver) Info() *asset.WalletInfo { return WalletInfo } +// Exists checks the existence of the wallet. Part of the Creator interface, so +// only used for wallets with WalletDefinition.Seeded = true. +func (d *Driver) Exists(walletType, dataDir string, settings map[string]string, net dex.Network) (bool, error) { + if walletType != walletTypeSPV { + return false, fmt.Errorf("no Bitcoin wallet of type %q available", walletType) + } + + chainParams, err := parseChainParams(net) + if err != nil { + return false, err + } + netDir := filepath.Join(dataDir, chainParams.Name, "spv") + loader := wallet.NewLoader(chainParams, netDir, true, 60*time.Second, 250) + return loader.WalletExists() +} + +// Create creates a new SPV wallet. +func (d *Driver) Create(params *asset.CreateWalletParams) error { + if params.Type != walletTypeSPV { + return fmt.Errorf("SPV is the only seeded wallet type. required = %q, requested = %q", walletTypeSPV, params.Type) + } + if len(params.Seed) == 0 { + return errors.New("wallet seed cannot be empty") + } + if len(params.DataDir) == 0 { + return errors.New("must specify wallet data directory") + } + chainParams, err := parseChainParams(params.Net) + if err != nil { + return fmt.Errorf("error parsing chain: %w", err) + } + + walletCfg := new(btc.WalletConfig) + err = config.Unmapify(params.Settings, walletCfg) + if err != nil { + return err + } + + recoveryCfg := new(btc.RecoveryCfg) + err = config.Unmapify(params.Settings, recoveryCfg) + if err != nil { + return err + } + + return createSPVWallet(params.Pass, params.Seed, walletCfg.AdjustedBirthday(), params.DataDir, + params.Logger, recoveryCfg.NumExternalAddresses, recoveryCfg.NumInternalAddresses, chainParams) +} + // NewWallet is the exported constructor by which the DEX will import the // exchange wallet. func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) { - var params *chaincfg.Params + var cloneParams *chaincfg.Params + var ltcParams *ltcchaincfg.Params switch network { case dex.Mainnet: - params = dexltc.MainNetParams + cloneParams = dexltc.MainNetParams + ltcParams = <cchaincfg.MainNetParams case dex.Testnet: - params = dexltc.TestNet4Params + cloneParams = dexltc.TestNet4Params + ltcParams = <cchaincfg.TestNet4Params case dex.Regtest: - params = dexltc.RegressionNetParams + cloneParams = dexltc.RegressionNetParams + ltcParams = <cchaincfg.RegressionNetParams default: return nil, fmt.Errorf("unknown network ID %v", network) } @@ -173,7 +240,7 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) Symbol: "ltc", Logger: logger, Network: network, - ChainParams: params, + ChainParams: cloneParams, Ports: NetPorts, DefaultFallbackFee: defaultFee, DefaultFeeRateLimit: defaultFeeRateLimit, @@ -186,6 +253,10 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) switch cfg.Type { case walletTypeRPC, walletTypeLegacy: return btc.BTCCloneWallet(cloneCFG) + case walletTypeSPV: + return btc.OpenSPVWallet(cloneCFG, func(dir string, cfg *btc.WalletConfig, btcParams *chaincfg.Params, log dex.Logger) btc.BTCWallet { + return openSPVWallet(dir, cfg, btcParams, ltcParams, log) + }) case walletTypeElectrum: cloneCFG.Ports = dexbtc.NetPorts{} // no default ports return btc.ElectrumWallet(cloneCFG) diff --git a/client/asset/ltc/spv.go b/client/asset/ltc/spv.go new file mode 100644 index 0000000000..89c0b486dc --- /dev/null +++ b/client/asset/ltc/spv.go @@ -0,0 +1,1049 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package ltc + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "path/filepath" + "sync/atomic" + "time" + + "decred.org/dcrdex/client/asset" + "decred.org/dcrdex/client/asset/btc" + "decred.org/dcrdex/dex" + "decred.org/dcrdex/dex/config" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/gcs" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btclog" + "github.com/btcsuite/btcwallet/waddrmgr" + btcwallet "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/jrick/logrotate/rotator" + btcneutrino "github.com/lightninglabs/neutrino" + "github.com/lightninglabs/neutrino/headerfs" + ltcchaincfg "github.com/ltcsuite/ltcd/chaincfg" + ltcchainhash "github.com/ltcsuite/ltcd/chaincfg/chainhash" + "github.com/ltcsuite/ltcd/ltcutil" + ltctxscript "github.com/ltcsuite/ltcd/txscript" + ltcwire "github.com/ltcsuite/ltcd/wire" + "github.com/ltcsuite/ltcwallet/chain" + ltcwaddrmgr "github.com/ltcsuite/ltcwallet/waddrmgr" + "github.com/ltcsuite/ltcwallet/wallet" + "github.com/ltcsuite/ltcwallet/wallet/txauthor" + "github.com/ltcsuite/ltcwallet/walletdb" + _ "github.com/ltcsuite/ltcwallet/walletdb/bdb" + ltcwtxmgr "github.com/ltcsuite/ltcwallet/wtxmgr" + "github.com/ltcsuite/neutrino" +) + +const ( + DefaultM uint64 = 784931 // From ltcutil. Used for gcs filters. + logDirName = "logs" + neutrinoDBName = "neutrino.db" + defaultAcctNum = 0 +) + +var ( + waddrmgrNamespace = []byte("waddrmgr") + wtxmgrNamespace = []byte("wtxmgr") +) + +// ltcSPVWallet is an implementation of btc.BTCWallet that runs a native +// Litecoin SPV Wallet. ltcSPVWallet mostly just translates types from the +// btcsuite types to ltcsuite and vice-versa. Startup and shutdown are notable +// exceptions, and have some critical code that needed to be duplicated (in +// order to avoid interface hell). +type ltcSPVWallet struct { + // This section is populated in newSPVWallet. + dir string + chainParams *ltcchaincfg.Params + btcParams *chaincfg.Params + log dex.Logger + birthdayV atomic.Value // time.Time + allowAutomaticRescan bool + + // This section is populated in Start. + *wallet.Wallet + chainClient *chain.NeutrinoClient + cl *neutrino.ChainService + loader *wallet.Loader + neutrinoDB walletdb.DB +} + +var _ btc.BTCWallet = (*ltcSPVWallet)(nil) + +// openSPVWallet creates a ltcSPVWallet, but does not Start. +func openSPVWallet(dir string, cfg *btc.WalletConfig, btcParams *chaincfg.Params, + ltcParams *ltcchaincfg.Params, log dex.Logger) btc.BTCWallet { + + w := <cSPVWallet{ + dir: dir, + chainParams: ltcParams, + btcParams: btcParams, + log: log, + allowAutomaticRescan: !cfg.ActivelyUsed, + } + w.birthdayV.Store(cfg.AdjustedBirthday()) + return w +} + +// createSPVWallet creates a new SPV wallet. +func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dbDir string, log dex.Logger, extIdx, intIdx uint32, net *ltcchaincfg.Params) error { + netDir := filepath.Join(dbDir, net.Name, "spv") + + if err := logNeutrino(netDir, log); err != nil { + return fmt.Errorf("error initializing btcwallet+neutrino logging: %w", err) + } + + logDir := filepath.Join(netDir, logDirName) + err := os.MkdirAll(logDir, 0744) + if err != nil { + return fmt.Errorf("error creating wallet directories: %w", err) + } + + // timeout and recoverWindow arguments borrowed from btcwallet directly. + loader := wallet.NewLoader(net, netDir, true, 60*time.Second, 250) + + pubPass := []byte(wallet.InsecurePubPassphrase) + + btcw, err := loader.CreateNewWallet(pubPass, privPass, seed, bday) + if err != nil { + return fmt.Errorf("CreateNewWallet error: %w", err) + } + + errCloser := dex.NewErrorCloser(log) + defer errCloser.Done() + errCloser.Add(loader.UnloadWallet) + + if extIdx > 0 || intIdx > 0 { + err = extendAddresses(extIdx, intIdx, btcw) + if err != nil { + return fmt.Errorf("failed to set starting address indexes: %w", err) + } + } + + // The chain service DB + neutrinoDBPath := filepath.Join(netDir, neutrinoDBName) + db, err := walletdb.Create("bdb", neutrinoDBPath, true, 5*time.Second) + if err != nil { + return fmt.Errorf("unable to create neutrino db at %q: %w", neutrinoDBPath, err) + } + if err = db.Close(); err != nil { + 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) + } + + errCloser.Success() + return nil +} + +// Start initializes the *ltcwallet.Wallet and its supporting players and starts +// syncing. +func (w *ltcSPVWallet) Start() (btc.SPVService, error) { + if err := logNeutrino(w.dir, w.log); err != nil { + return nil, fmt.Errorf("error initializing btcwallet+neutrino logging: %v", err) + } + // recoverWindow arguments borrowed from ltcwallet directly. + w.loader = wallet.NewLoader(w.chainParams, w.dir, true, 60*time.Second, 250) + + exists, err := w.loader.WalletExists() + if err != nil { + return nil, fmt.Errorf("error verifying wallet existence: %v", err) + } + if !exists { + return nil, errors.New("wallet not found") + } + + w.log.Debug("Starting native LTC wallet...") + w.Wallet, err = w.loader.OpenExistingWallet([]byte(wallet.InsecurePubPassphrase), false) + if err != nil { + return nil, fmt.Errorf("couldn't load wallet: %w", err) + } + + errCloser := dex.NewErrorCloser(w.log) + defer errCloser.Done() + errCloser.Add(w.loader.UnloadWallet) + + neutrinoDBPath := filepath.Join(w.dir, neutrinoDBName) + w.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true, wallet.DefaultDBTimeout) + if err != nil { + return nil, fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) + } + errCloser.Add(w.neutrinoDB.Close) + + // Depending on the network, we add some addpeers or a connect peer. On + // regtest, if 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. On + // mainet and testnet3, add a known reliable persistent peer to be used in + // addition to normal DNS seed-based peer discovery. + // var addPeers []string + var connectPeers []string + switch w.chainParams.Net { + case ltcwire.TestNet, ltcwire.SimNet: // plain "wire.TestNet" is regnet! + connectPeers = []string{"localhost:20585"} + } + + w.log.Debug("Starting neutrino chain service...") + w.cl, err = neutrino.NewChainService(neutrino.Config{ + DataDir: w.dir, + Database: w.neutrinoDB, + ChainParams: *w.chainParams, + PersistToDisk: true, // keep cfilter headers on disk for efficient rescanning + // AddPeers: addPeers, + ConnectPeers: connectPeers, + // // WARNING: PublishTransaction currently uses the entire duration + // // because if an external bug, but even if the resolved, a typical + // // inv/getdata round trip is ~4 seconds, so we set this so neutrino does + // // not cancel queries too readily. + BroadcastTimeout: 6 * time.Second, + }) + if err != nil { + return nil, fmt.Errorf("couldn't create Neutrino ChainService: %v", err) + } + errCloser.Add(w.cl.Stop) + + w.chainClient = chain.NewNeutrinoClient(w.chainParams, w.cl) + + oldBday := w.Manager.Birthday() + wdb := w.Database() + + performRescan := w.Birthday().Before(oldBday) + if performRescan && !w.allowAutomaticRescan { + return nil, errors.New("cannot set earlier birthday while there are active deals") + } + + if !oldBday.Equal(w.Birthday()) { + err = walletdb.Update(wdb, func(dbtx walletdb.ReadWriteTx) error { + ns := dbtx.ReadWriteBucket(waddrmgrNamespace) + return w.Manager.SetBirthday(ns, w.Birthday()) + }) + if err != nil { + w.log.Errorf("Failed to reset wallet manager birthday: %v", err) + performRescan = false + } + } + + if performRescan { + w.ForceRescan() + } + + if err = w.chainClient.Start(); err != nil { // lazily starts connmgr + return nil, fmt.Errorf("couldn't start Neutrino client: %v", err) + } + + w.log.Info("Synchronizing wallet with network...") + w.SynchronizeRPC(w.chainClient) + + errCloser.Success() + + return &spvService{w.cl}, nil +} + +func (w *ltcSPVWallet) Birthday() time.Time { + return w.birthdayV.Load().(time.Time) +} + +func (w *ltcSPVWallet) Reconfigure(cfg *asset.WalletConfig, _ /* oldAddress */ string) (restart bool, err error) { + parsedCfg := new(btc.WalletConfig) + if err = config.Unmapify(cfg.Settings, parsedCfg); err != nil { + return + } + + newBday := parsedCfg.AdjustedBirthday() + if newBday.Equal(w.Birthday()) { + // It's the only setting we care about. + return + } + rescanRequired := newBday.Before(w.Birthday()) + if rescanRequired && parsedCfg.ActivelyUsed { + return false, errors.New("cannot decrease the birthday with active orders") + } + if err := w.updateDBBirthday(newBday); err != nil { + return false, fmt.Errorf("error storing new birthday: %w", err) + } + w.birthdayV.Store(newBday) + if rescanRequired { + if err = w.RescanAsync(); err != nil { + return false, fmt.Errorf("error initiating rescan after birthday adjustment: %w", err) + } + } + return +} + +func (w *ltcSPVWallet) updateDBBirthday(bday time.Time) error { + btcw, isLoaded := w.loader.LoadedWallet() + if !isLoaded { + return fmt.Errorf("wallet not loaded") + } + return walletdb.Update(btcw.Database(), func(dbtx walletdb.ReadWriteTx) error { + ns := dbtx.ReadWriteBucket(waddrmgrNamespace) + return btcw.Manager.SetBirthday(ns, bday) + }) +} + +func (w *ltcSPVWallet) txDetails(txHash *ltcchainhash.Hash) (*ltcwtxmgr.TxDetails, error) { + details, err := wallet.UnstableAPI(w.Wallet).TxDetails(txHash) + if err != nil { + return nil, err + } + if details == nil { + return nil, btc.WalletTransactionNotFound + } + + return details, nil +} + +func (w *ltcSPVWallet) PublishTransaction(btcTx *wire.MsgTx, label string) error { + ltcTx, err := convertMsgTxToLTC(btcTx) + if err != nil { + return err + } + + return w.Wallet.PublishTransaction(ltcTx, label) +} + +func (w *ltcSPVWallet) CalculateAccountBalances(account uint32, confirms int32) (btcwallet.Balances, error) { + bals, err := w.Wallet.CalculateAccountBalances(account, confirms) + if err != nil { + return btcwallet.Balances{}, err + } + return btcwallet.Balances{ + Total: btcutil.Amount(bals.Total), + Spendable: btcutil.Amount(bals.Spendable), + ImmatureReward: btcutil.Amount(bals.ImmatureReward), + }, nil +} + +func (w *ltcSPVWallet) ListUnspent(minconf, maxconf int32, acctName string) ([]*btcjson.ListUnspentResult, error) { + // ltcwallet's ListUnspent takes either a list of addresses, or else returns + // all non-locked unspent outputs for all accounts. We need to iterate the + // results anyway to convert type. + uns, err := w.Wallet.ListUnspent(minconf, maxconf, acctName) + if err != nil { + return nil, err + } + + outs := make([]*btcjson.ListUnspentResult, len(uns)) + for i, u := range uns { + if u.Account != acctName { + continue + } + outs[i] = &btcjson.ListUnspentResult{ + TxID: u.TxID, + Vout: u.Vout, + Address: u.Address, + Account: u.Account, + ScriptPubKey: u.ScriptPubKey, + RedeemScript: u.RedeemScript, + Amount: u.Amount, + Confirmations: u.Confirmations, + Spendable: u.Spendable, + } + } + + return outs, nil +} + +// FetchInputInfo is not actually implemented in ltcwallet. This is based on the +// btcwallet implementation. As this is used by btc.spvWallet, we really only +// need the TxOut, and to show ownership. +func (w *ltcSPVWallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error) { + + td, err := w.txDetails(convertHashToLTC(prevOut.Hash)) + if err != nil { + return nil, nil, nil, 0, err + } + + if prevOut.Index >= uint32(len(td.TxRecord.MsgTx.TxOut)) { + return nil, nil, nil, 0, fmt.Errorf("not enough outputs") + } + + ltcTxOut := td.TxRecord.MsgTx.TxOut[prevOut.Index] + + // Verify we own at least one parsed address. + _, addrs, _, err := ltctxscript.ExtractPkScriptAddrs(ltcTxOut.PkScript, w.chainParams) + if err != nil { + return nil, nil, nil, 0, err + } + notOurs := true + for i := 0; notOurs && i < len(addrs); i++ { + _, err := w.Wallet.AddressInfo(addrs[i]) + notOurs = err != nil + } + if notOurs { + return nil, nil, nil, 0, btcwallet.ErrNotMine + } + + btcTxOut := &wire.TxOut{ + Value: ltcTxOut.Value, + PkScript: ltcTxOut.PkScript, + } + + return nil, btcTxOut, nil, 0, nil +} + +func (w *ltcSPVWallet) LockOutpoint(op wire.OutPoint) { + w.Wallet.LockOutpoint(ltcwire.OutPoint{ + Hash: *convertHashToLTC(op.Hash), + Index: op.Index, + }) +} + +func (w *ltcSPVWallet) UnlockOutpoint(op wire.OutPoint) { + w.Wallet.UnlockOutpoint(ltcwire.OutPoint{ + Hash: *convertHashToLTC(op.Hash), + Index: op.Index, + }) +} + +func (w *ltcSPVWallet) LockedOutpoints() []btcjson.TransactionInput { + locks := w.Wallet.LockedOutpoints() + locked := make([]btcjson.TransactionInput, len(locks)) + for i, lock := range locks { + locked[i] = btcjson.TransactionInput{ + Txid: lock.Txid, + Vout: lock.Vout, + } + } + return locked +} + +func (w *ltcSPVWallet) NewChangeAddress(account uint32, _ waddrmgr.KeyScope) (btcutil.Address, error) { + ltcAddr, err := w.Wallet.NewChangeAddress(account, ltcwaddrmgr.KeyScopeBIP0084) + if err != nil { + return nil, err + } + return btcutil.DecodeAddress(ltcAddr.String(), w.btcParams) +} + +func (w *ltcSPVWallet) NewAddress(account uint32, _ waddrmgr.KeyScope) (btcutil.Address, error) { + ltcAddr, err := w.Wallet.NewAddress(account, ltcwaddrmgr.KeyScopeBIP0084) + if err != nil { + return nil, err + } + return btcutil.DecodeAddress(ltcAddr.String(), w.btcParams) +} + +func (w *ltcSPVWallet) PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) { + ltcAddr, err := ltcutil.DecodeAddress(a.String(), w.chainParams) + if err != nil { + return nil, err + } + + ltcKey, err := w.Wallet.PrivKeyForAddress(ltcAddr) + if err != nil { + return nil, err + } + + priv, _ /* pub */ := btcec.PrivKeyFromBytes(ltcKey.Serialize()) + return priv, nil +} + +func (w *ltcSPVWallet) SendOutputs(outputs []*wire.TxOut, _ *waddrmgr.KeyScope, account uint32, minconf int32, + satPerKb btcutil.Amount, css btcwallet.CoinSelectionStrategy, label string) (*wire.MsgTx, error) { + + ltcOuts := make([]*ltcwire.TxOut, len(outputs)) + for i, op := range outputs { + ltcOuts[i] = <cwire.TxOut{ + Value: op.Value, + PkScript: op.PkScript, + } + } + + ltcTx, err := w.Wallet.SendOutputs(ltcOuts, <cwaddrmgr.KeyScopeBIP0084, account, + minconf, ltcutil.Amount(satPerKb), wallet.CoinSelectionStrategy(css), label) + if err != nil { + return nil, err + } + + btcTx, err := convertMsgTxToBTC(ltcTx) + if err != nil { + return nil, err + } + + return btcTx, nil +} + +func (w *ltcSPVWallet) HaveAddress(a btcutil.Address) (bool, error) { + ltcAddr, err := ltcutil.DecodeAddress(a.String(), w.chainParams) + if err != nil { + return false, err + } + + return w.Wallet.HaveAddress(ltcAddr) +} + +func (w *ltcSPVWallet) Stop() { + w.log.Info("Unloading wallet") + if err := w.loader.UnloadWallet(); err != nil { + w.log.Errorf("UnloadWallet error: %v", err) + } + if w.chainClient != nil { + w.log.Trace("Stopping neutrino client chain interface") + w.chainClient.Stop() + w.chainClient.WaitForShutdown() + } + w.log.Trace("Stopping neutrino chain sync service") + if err := w.cl.Stop(); err != nil { + w.log.Errorf("error stopping neutrino chain service: %v", err) + } + w.log.Trace("Stopping neutrino DB.") + if err := w.neutrinoDB.Close(); err != nil { + w.log.Errorf("wallet db close error: %v", err) + } + + w.log.Info("SPV wallet closed") +} + +func (w *ltcSPVWallet) AccountProperties(_ waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error) { + props, err := w.Wallet.AccountProperties(ltcwaddrmgr.KeyScopeBIP0084, acct) + if err != nil { + return nil, err + } + return &waddrmgr.AccountProperties{ + AccountNumber: props.AccountNumber, + AccountName: props.AccountName, + ExternalKeyCount: props.ExternalKeyCount, + InternalKeyCount: props.InternalKeyCount, + ImportedKeyCount: props.ImportedKeyCount, + MasterKeyFingerprint: props.MasterKeyFingerprint, + KeyScope: waddrmgr.KeyScopeBIP0044, + IsWatchOnly: props.IsWatchOnly, + // The last two would need conversion but aren't currently used. + // AccountPubKey: props.AccountPubKey, + // AddrSchema: props.AddrSchema, + }, nil +} + +func (w *ltcSPVWallet) RescanAsync() error { + w.log.Info("Stopping wallet and chain client...") + w.Wallet.Stop() // stops Wallet and chainClient (not chainService) + w.Wallet.WaitForShutdown() + w.chainClient.WaitForShutdown() + + w.ForceRescan() + + w.log.Info("Starting wallet...") + w.Wallet.Start() + + if err := w.chainClient.Start(); err != nil { + return fmt.Errorf("couldn't start Neutrino client: %v", err) + } + + w.log.Info("Synchronizing wallet with network...") + w.Wallet.SynchronizeRPC(w.chainClient) + return nil +} + +// ForceRescan forces a full rescan with active address discovery on wallet +// restart by dropping the complete transaction history and setting the +// "synced to" field to nil. See the btcwallet/cmd/dropwtxmgr app for more +// information. +func (w *ltcSPVWallet) ForceRescan() { + w.log.Info("Dropping transaction history to perform full rescan...") + err := w.dropTransactionHistory() + if err != nil { + w.log.Errorf("Failed to drop wallet transaction history: %v", err) + // Continue to attempt restarting the wallet anyway. + } + + err = walletdb.Update(w.Database(), func(dbtx walletdb.ReadWriteTx) error { + ns := dbtx.ReadWriteBucket(waddrmgrNamespace) // it'll be fine + return w.Manager.SetSyncedTo(ns, nil) // never synced, forcing recover from birthday + }) + if err != nil { + w.log.Errorf("Failed to reset wallet manager sync height: %v", err) + } +} + +// dropTransactionHistory drops the transaction history. It is based off of the +// dropwtxmgr utility in the ltcwallet repo. +func (w *ltcSPVWallet) dropTransactionHistory() error { + w.log.Info("Dropping wallet transaction history") + + return walletdb.Update(w.Database(), func(tx walletdb.ReadWriteTx) error { + err := tx.DeleteTopLevelBucket(wtxmgrNamespace) + if err != nil && err != walletdb.ErrBucketNotFound { + return err + } + ns, err := tx.CreateTopLevelBucket(wtxmgrNamespace) + if err != nil { + return err + } + err = ltcwtxmgr.Create(ns) + if err != nil { + return err + } + + ns = tx.ReadWriteBucket(waddrmgrNamespace) + birthdayBlock, err := ltcwaddrmgr.FetchBirthdayBlock(ns) + if err != nil { + fmt.Println("Wallet does not have a birthday block " + + "set, falling back to rescan from genesis") + + startBlock, err := ltcwaddrmgr.FetchStartBlock(ns) + if err != nil { + return err + } + return ltcwaddrmgr.PutSyncedTo(ns, startBlock) + } + + // We'll need to remove our birthday block first because it + // serves as a barrier when updating our state to detect reorgs + // due to the wallet not storing all block hashes of the chain. + if err := ltcwaddrmgr.DeleteBirthdayBlock(ns); err != nil { + return err + } + + if err := ltcwaddrmgr.PutSyncedTo(ns, &birthdayBlock); err != nil { + return err + } + return ltcwaddrmgr.PutBirthdayBlock(ns, birthdayBlock) + }) +} + +func (w *ltcSPVWallet) WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) { + txDetails, err := w.txDetails(convertHashToLTC(*txHash)) + if err != nil { + return nil, err + } + + btcTx, err := convertMsgTxToBTC(&txDetails.MsgTx) + if err != nil { + return nil, err + } + + credits := make([]wtxmgr.CreditRecord, len(txDetails.Credits)) + for i, c := range txDetails.Credits { + credits[i] = wtxmgr.CreditRecord{ + Amount: btcutil.Amount(c.Amount), + Index: c.Index, + Spent: c.Spent, + Change: c.Change, + } + } + + debits := make([]wtxmgr.DebitRecord, len(txDetails.Debits)) + for i, d := range txDetails.Debits { + debits[i] = wtxmgr.DebitRecord{ + Amount: btcutil.Amount(d.Amount), + Index: d.Index, + } + } + + return &wtxmgr.TxDetails{ + TxRecord: wtxmgr.TxRecord{ + MsgTx: *btcTx, + Hash: *convertHashToBTC(txDetails.TxRecord.Hash), + Received: txDetails.TxRecord.Received, + SerializedTx: txDetails.TxRecord.SerializedTx, + }, + Block: wtxmgr.BlockMeta{ + Block: wtxmgr.Block{ + Hash: *convertHashToBTC(txDetails.Block.Hash), + Height: txDetails.Block.Height, + }, + Time: txDetails.Block.Time, + }, + Credits: credits, + Debits: debits, + }, nil +} + +func (w *ltcSPVWallet) SyncedTo() waddrmgr.BlockStamp { + bs := w.Manager.SyncedTo() + return waddrmgr.BlockStamp{ + Height: bs.Height, + Hash: *convertHashToBTC(bs.Hash), + Timestamp: bs.Timestamp, + } + +} + +func (w *ltcSPVWallet) SignTx(btcTx *wire.MsgTx) error { + ltcTx, err := convertMsgTxToLTC(btcTx) + if err != nil { + return err + } + + var prevPkScripts [][]byte + var inputValues []ltcutil.Amount + for _, txIn := range btcTx.TxIn { + _, txOut, _, _, err := w.FetchInputInfo(&txIn.PreviousOutPoint) + if err != nil { + return err + } + inputValues = append(inputValues, ltcutil.Amount(txOut.Value)) + prevPkScripts = append(prevPkScripts, txOut.PkScript) + // Zero the previous witness and signature script or else + // AddAllInputScripts does some weird stuff. + txIn.SignatureScript = nil + txIn.Witness = nil + } + + err = txauthor.AddAllInputScripts(ltcTx, prevPkScripts, inputValues, &secretSource{w.Wallet, w.chainParams}) + if err != nil { + return err + } + if len(ltcTx.TxIn) != len(btcTx.TxIn) { + return fmt.Errorf("txin count mismatch") + } + for i, txIn := range btcTx.TxIn { + ltcIn := ltcTx.TxIn[i] + txIn.SignatureScript = ltcIn.SignatureScript + txIn.Witness = make(wire.TxWitness, len(ltcIn.Witness)) + copy(txIn.Witness, ltcIn.Witness) + } + return nil +} + +// BlockNotifications returns a channel on which to receive notifications of +// newly processed blocks. The caller should only call BlockNotificaitons once. +func (w *ltcSPVWallet) BlockNotifications(ctx context.Context) <-chan *btc.BlockNotification { + cl := w.NtfnServer.TransactionNotifications() + ch := make(chan *btc.BlockNotification, 1) + go func() { + defer cl.Done() + for { + select { + case note := <-cl.C: + if len(note.AttachedBlocks) > 0 { + lastBlock := note.AttachedBlocks[len(note.AttachedBlocks)-1] + select { + case ch <- &btc.BlockNotification{ + Hash: *convertHashToBTC(*lastBlock.Hash), + Height: lastBlock.Height, + }: + default: + } + } + case <-ctx.Done(): + return + } + } + }() + return ch +} + +// secretSource is used to locate keys and redemption scripts while signing a +// transaction. secretSource satisfies the txauthor.SecretsSource interface. +type secretSource struct { + w *wallet.Wallet + chainParams *ltcchaincfg.Params +} + +// ChainParams returns the chain parameters. +func (s *secretSource) ChainParams() *ltcchaincfg.Params { + return s.chainParams +} + +// GetKey fetches a private key for the specified address. +func (s *secretSource) GetKey(addr ltcutil.Address) (*btcec.PrivateKey, bool, error) { + ma, err := s.w.AddressInfo(addr) + if err != nil { + return nil, false, err + } + + mpka, ok := ma.(ltcwaddrmgr.ManagedPubKeyAddress) + if !ok { + e := fmt.Errorf("managed address type for %v is `%T` but "+ + "want waddrmgr.ManagedPubKeyAddress", addr, ma) + return nil, false, e + } + + privKey, err := mpka.PrivKey() + if err != nil { + return nil, false, err + } + + k, _ /* pub */ := btcec.PrivKeyFromBytes(privKey.Serialize()) + + return k, ma.Compressed(), nil +} + +// GetScript fetches the redemption script for the specified p2sh/p2wsh address. +func (s *secretSource) GetScript(addr ltcutil.Address) ([]byte, error) { + ma, err := s.w.AddressInfo(addr) + if err != nil { + return nil, err + } + + msa, ok := ma.(ltcwaddrmgr.ManagedScriptAddress) + if !ok { + e := fmt.Errorf("managed address type for %v is `%T` but "+ + "want waddrmgr.ManagedScriptAddress", addr, ma) + return nil, e + } + return msa.Script() +} + +// spvService embeds ltcsuite neutrino.ChainService and translates types. +type spvService struct { + *neutrino.ChainService +} + +var _ btc.SPVService = (*spvService)(nil) + +func (s *spvService) GetBlockHash(height int64) (*chainhash.Hash, error) { + ltcHash, err := s.ChainService.GetBlockHash(height) + if err != nil { + return nil, err + } + return convertHashToBTC(*ltcHash), nil +} + +func (s *spvService) BestBlock() (*headerfs.BlockStamp, error) { + bs, err := s.ChainService.BestBlock() + if err != nil { + return nil, err + } + return &headerfs.BlockStamp{ + Height: bs.Height, + Hash: *convertHashToBTC(bs.Hash), + Timestamp: bs.Timestamp, + }, nil +} + +func (s *spvService) Peers() []btc.SPVPeer { + rawPeers := s.ChainService.Peers() + peers := make([]btc.SPVPeer, len(rawPeers)) + for i, p := range rawPeers { + peers[i] = p + } + return peers +} + +func (s *spvService) GetBlockHeight(h *chainhash.Hash) (int32, error) { + return s.ChainService.GetBlockHeight(convertHashToLTC(*h)) +} + +func (s *spvService) GetBlockHeader(h *chainhash.Hash) (*wire.BlockHeader, error) { + hdr, err := s.ChainService.GetBlockHeader(convertHashToLTC(*h)) + if err != nil { + return nil, err + } + return &wire.BlockHeader{ + Version: hdr.Version, + PrevBlock: *convertHashToBTC(hdr.PrevBlock), + MerkleRoot: *convertHashToBTC(hdr.MerkleRoot), + Timestamp: hdr.Timestamp, + Bits: hdr.Bits, + Nonce: hdr.Nonce, + }, nil +} + +func (s *spvService) GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, _ ...btcneutrino.QueryOption) (*gcs.Filter, error) { + f, err := s.ChainService.GetCFilter(*convertHashToLTC(blockHash), ltcwire.GCSFilterRegular) + if err != nil { + return nil, err + } + + b, err := f.Bytes() + if err != nil { + return nil, err + } + + return gcs.FromBytes(f.N(), f.P(), DefaultM, b) +} + +func (s *spvService) GetBlock(blockHash chainhash.Hash, _ ...btcneutrino.QueryOption) (*btcutil.Block, error) { + blk, err := s.ChainService.GetBlock(*convertHashToLTC(blockHash)) + if err != nil { + return nil, err + } + + b, err := blk.Bytes() + if err != nil { + return nil, err + } + + return btcutil.NewBlockFromBytes(b) +} + +func convertHashToBTC(src ltcchainhash.Hash) *chainhash.Hash { + var tgt chainhash.Hash + copy(tgt[:], src[:]) + return &tgt +} + +func convertHashToLTC(src chainhash.Hash) *ltcchainhash.Hash { + var tgt ltcchainhash.Hash + copy(tgt[:], src[:]) + return &tgt +} + +func convertMsgTxToBTC(tx *ltcwire.MsgTx) (*wire.MsgTx, error) { + buf := new(bytes.Buffer) + if err := tx.Serialize(buf); err != nil { + return nil, err + } + + btcTx := new(wire.MsgTx) + if err := btcTx.Deserialize(buf); err != nil { + return nil, err + } + return btcTx, nil +} + +func convertMsgTxToLTC(tx *wire.MsgTx) (*ltcwire.MsgTx, error) { + buf := new(bytes.Buffer) + if err := tx.Serialize(buf); err != nil { + return nil, err + } + ltcTx := new(ltcwire.MsgTx) + if err := ltcTx.Deserialize(buf); err != nil { + return nil, err + } + + // 153280a7351c702ce4e8d87a878cc3d1eab41650ce0b895a3598b62c3778ad5f from peer=2 was not accepted: non-mandatory-script-verify-flag (Witness program hash mismatch) + + return ltcTx, nil +} + +func parseChainParams(net dex.Network) (*ltcchaincfg.Params, error) { + switch net { + case dex.Mainnet: + return <cchaincfg.MainNetParams, nil + case dex.Testnet: + return <cchaincfg.TestNet4Params, nil + case dex.Regtest: + return <cchaincfg.RegressionNetParams, nil + } + return nil, fmt.Errorf("unknown network ID %v", net) +} + +func extendAddresses(extIdx, intIdx uint32, ltcw *wallet.Wallet) error { + scopedKeyManager, err := ltcw.Manager.FetchScopedKeyManager(ltcwaddrmgr.KeyScopeBIP0084) + if err != nil { + return err + } + + return walletdb.Update(ltcw.Database(), func(dbtx walletdb.ReadWriteTx) error { + ns := dbtx.ReadWriteBucket(waddrmgrNamespace) + if extIdx > 0 { + scopedKeyManager.ExtendExternalAddresses(ns, defaultAcctNum, extIdx) + } + if intIdx > 0 { + scopedKeyManager.ExtendInternalAddresses(ns, defaultAcctNum, intIdx) + } + return nil + }) +} + +var ( + loggingInited uint32 + logFileName = "neutrino.log" +) + +// 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) +} + +// logRotator initializes a rotating file logger. +func logRotator(netDir string) (*rotator.Rotator, error) { + const maxLogRolls = 8 + logDir := filepath.Join(netDir, logDirName) + if err := os.MkdirAll(logDir, 0744); err != nil { + return nil, fmt.Errorf("error creating log directory: %w", err) + } + + logFilename := filepath.Join(logDir, logFileName) + return rotator.New(logFilename, 32*1024, false, maxLogRolls) +} + +// logNeutrino initializes logging in the neutrino + wallet packages. Logging +// only has to be initialized once, so an atomic flag is used internally to +// return early on subsequent invocations. +// +// In theory, the the rotating file logger must be Close'd at some point, but +// there are concurrency issues with that since btcd and btcwallet have +// unsupervised goroutines still running after shutdown. So we leave the rotator +// running at the risk of losing some logs. +func logNeutrino(netDir string, errorLogger dex.Logger) error { + if !atomic.CompareAndSwapUint32(&loggingInited, 0, 1) { + return nil + } + + logSpinner, err := logRotator(netDir) + if err != nil { + return fmt.Errorf("error initializing log rotator: %w", err) + } + + backendLog := btclog.NewBackend(logWriter{logSpinner}) + + logger := func(name string, lvl btclog.Level) btclog.Logger { + l := backendLog.Logger(name) + l.SetLevel(lvl) + return &fileLoggerPlus{Logger: l, log: errorLogger.SubLogger(name)} + } + + neutrino.UseLogger(logger("NTRNO", btclog.LevelDebug)) + wallet.UseLogger(logger("LTCW", btclog.LevelInfo)) + ltcwtxmgr.UseLogger(logger("TXMGR", btclog.LevelInfo)) + chain.UseLogger(logger("CHAIN", btclog.LevelInfo)) + + return nil +} + +// fileLoggerPlus logs everything to a file, and everything with level >= warn +// to both file and a specified dex.Logger. +type fileLoggerPlus struct { + btclog.Logger + log dex.Logger +} + +func (f *fileLoggerPlus) Warnf(format string, params ...interface{}) { + f.log.Warnf(format, params...) + f.Logger.Warnf(format, params...) + +} + +func (f *fileLoggerPlus) Errorf(format string, params ...interface{}) { + f.log.Errorf(format, params...) + f.Logger.Errorf(format, params...) + +} + +func (f *fileLoggerPlus) Criticalf(format string, params ...interface{}) { + f.log.Criticalf(format, params...) + f.Logger.Criticalf(format, params...) + +} + +func (f *fileLoggerPlus) Warn(v ...interface{}) { + f.log.Warn(v...) + f.Logger.Warn(v...) + +} + +func (f *fileLoggerPlus) Error(v ...interface{}) { + f.log.Error(v...) + f.Logger.Error(v...) + +} + +func (f *fileLoggerPlus) Critical(v ...interface{}) { + f.log.Critical(v...) + f.Logger.Critical(v...) + +} diff --git a/client/cmd/simnet-trade-tests/run b/client/cmd/simnet-trade-tests/run index df3ca579b6..c37301ae26 100755 --- a/client/cmd/simnet-trade-tests/run +++ b/client/cmd/simnet-trade-tests/run @@ -36,6 +36,11 @@ case $1 in --regasset dcr ${@:2} ;; + ltcspvdcr) + ./simnet-trade-tests --base ltc --quote dcr --quote1node trading1 --quote2node trading2 \ + --regasset dcr --base1type spv ${@:2} + ;; + ltcelectrumdcr) ./simnet-trade-tests --base ltc --quote dcr --quote1node trading1 --quote2node trading2 \ --base1type electrum --regasset dcr ${@:2} @@ -72,6 +77,7 @@ dcrbtcelectrum - Decred RPC wallet and Bitcoin Electrum wallet on DCR-BTC market bchdcr - RPC wallets on BCH-DCR market bchspvdcr - Bitcoin Cash SPV wallet and Decred RPC Wallet on BCH-DCR market ltcdcr - RPC wallets on LTC-DCR market +ltcspvdcr - Litecoin SPV wallet and Decred RPC Wallet on LTC-DCR market ltcelectrumdcr - Litecoin Electrum wallet and Decred RPC wallet on LTC-DCR market dcrdoge - RPC wallets on DCR-DOGE market dcreth - Decred RPC wallet and Ethereum native wallet on DCR-ETH market diff --git a/dex/testing/loadbot/go.mod b/dex/testing/loadbot/go.mod index bed191e4aa..7c7d14d042 100644 --- a/dex/testing/loadbot/go.mod +++ b/dex/testing/loadbot/go.mod @@ -4,8 +4,6 @@ go 1.18 replace decred.org/dcrdex => ../../../ -replace github.com/gcash/neutrino => github.com/buck54321/neutrino-bch v0.0.0-20220616193517-b8fcce55a27c - require ( decred.org/dcrdex v0.0.0-20220620230547-1283356d184b github.com/Shopify/toxiproxy/v2 v2.4.0 @@ -108,6 +106,20 @@ require ( github.com/lightningnetwork/lnd/queue v1.0.1 // indirect github.com/lightningnetwork/lnd/ticker v1.0.0 // indirect github.com/lightningnetwork/lnd/tlv v1.0.2 // indirect + github.com/ltcsuite/lnd/clock v0.0.0-20200822020009-1a001cbb895a // indirect + github.com/ltcsuite/lnd/queue v1.0.3 // indirect + github.com/ltcsuite/lnd/ticker v1.0.1 // indirect + github.com/ltcsuite/ltcd v0.22.1-beta // indirect + github.com/ltcsuite/ltcd/btcec/v2 v2.1.0 // indirect + github.com/ltcsuite/ltcd/ltcutil v1.1.0 // indirect + github.com/ltcsuite/ltcd/ltcutil/psbt v1.1.0-1 // indirect + github.com/ltcsuite/ltcwallet v0.13.1 // indirect + github.com/ltcsuite/ltcwallet/wallet/txauthor v1.1.0 // indirect + github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0 // indirect + github.com/ltcsuite/ltcwallet/wallet/txsizes v1.1.0 // indirect + github.com/ltcsuite/ltcwallet/walletdb v1.3.5 // indirect + github.com/ltcsuite/ltcwallet/wtxmgr v1.5.0 // indirect + github.com/ltcsuite/neutrino v0.13.2 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.12 // indirect diff --git a/dex/testing/loadbot/go.sum b/dex/testing/loadbot/go.sum index 2ccaec37ac..0739d553f4 100644 --- a/dex/testing/loadbot/go.sum +++ b/dex/testing/loadbot/go.sum @@ -109,6 +109,7 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/bombsimon/wsl/v3 v3.3.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= @@ -164,8 +165,6 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/buck54321/neutrino-bch v0.0.0-20220616193517-b8fcce55a27c h1:4rjc12CvMWS2yIZ/cCDYkjulw1pXUnKkKk60QGgeDBA= -github.com/buck54321/neutrino-bch v0.0.0-20220616193517-b8fcce55a27c/go.mod h1:MshBO/Xf8SCndZFetZ8yg79db/JghnOiMmPiY1Eatlw= github.com/bufbuild/buf v0.37.0/go.mod h1:lQ1m2HkIaGOFba6w/aC3KYBHhKEOESP3gaAEpS3dAFM= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= @@ -188,6 +187,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -313,6 +313,7 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= @@ -334,6 +335,7 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= @@ -344,6 +346,8 @@ github.com/gcash/bchd v0.14.7/go.mod h1:Gk/O1ktRVW5Kao0RsnVXp3bWxeYQadqawZ1Im9HE github.com/gcash/bchd v0.15.2/go.mod h1:k9wIjgwnhbrAw+ruIPZ2tHZMzfFNdyUnORZZX7lqXGY= github.com/gcash/bchd v0.17.1/go.mod h1:qwEZ/wr6LyUo5IBgAPcAbYHzXrjnr5gc4tj03n1TwKc= github.com/gcash/bchd v0.17.2-0.20201218180520-5708823e0e99/go.mod h1:qwEZ/wr6LyUo5IBgAPcAbYHzXrjnr5gc4tj03n1TwKc= +github.com/gcash/bchd v0.18.1-0.20210522092846-10614544cf00/go.mod h1:M8ps+ZMPvdUoqCMoI/aKWKXg8GOE4zTKQpvGK9D940Y= +github.com/gcash/bchd v0.18.1-0.20210524104807-34d7fe5c34b2/go.mod h1:Ev71ERU8MTkTRNFPeNox+9Vz/ZWX0ejpkwgZW/ophgw= github.com/gcash/bchd v0.18.1/go.mod h1:Ev71ERU8MTkTRNFPeNox+9Vz/ZWX0ejpkwgZW/ophgw= github.com/gcash/bchd v0.19.0 h1:qM1wHgb9FlO95f1tj3q2deHVW7c7gB7GRnz2xVpn8EY= github.com/gcash/bchd v0.19.0/go.mod h1:Ev71ERU8MTkTRNFPeNox+9Vz/ZWX0ejpkwgZW/ophgw= @@ -354,12 +358,21 @@ github.com/gcash/bchutil v0.0.0-20191012211144-98e73ec336ba/go.mod h1:nUIrcbbtEQ github.com/gcash/bchutil v0.0.0-20200506001747-c2894cd54b33/go.mod h1:wB++2ZcHUvGLN1OgO9swBmJK1vmyshJLW9SNS+apXwc= github.com/gcash/bchutil v0.0.0-20210113190856-6ea28dff4000 h1:vVi7Ym3I9T4ZKhQy0/XLKzS3xAqX4K+/cSAmnvMR+HM= github.com/gcash/bchutil v0.0.0-20210113190856-6ea28dff4000/go.mod h1:H2USFGwtiu6CNMxiVQPqZkDzsoVSt9BLNqTfBBqGXRo= +github.com/gcash/bchwallet v0.8.3-0.20210524032547-09a7e9ef6126/go.mod h1:w+CpcX7VBjAiLMq+pkubcbEU6IqJWg/VNu3skgm6RGc= +github.com/gcash/bchwallet v0.8.3-0.20210524084514-53ddbcfd3712/go.mod h1:C7YNuYMdesznEyUVU305/9jtWDkzVbcNU9/ZgwD1QAU= github.com/gcash/bchwallet v0.8.3-0.20210524112536-14ca25bc6549/go.mod h1:4hGEkghrAx5yIOdvakkRP5ZT8K/ZMtJhrXXr1Mc6lz4= github.com/gcash/bchwallet v0.10.0 h1:kAd5q585B/QpjZxv6JAvDYVn1KJpCK8IePzjfUvlYl0= github.com/gcash/bchwallet v0.10.0/go.mod h1:7Uftb3ZU9+iM5nEpLT2502aYi2x+OuUOo7A7rl13kvw= +github.com/gcash/bchwallet/walletdb v0.0.0-20210524032111-4a6e424b2c66/go.mod h1:KNPI56t8uMlItrGyu3tS4NCd3jGfA6KjdUg4bsk+QJg= +github.com/gcash/bchwallet/walletdb v0.0.0-20210524032946-5175b789def6/go.mod h1:KNPI56t8uMlItrGyu3tS4NCd3jGfA6KjdUg4bsk+QJg= github.com/gcash/bchwallet/walletdb v0.0.0-20210524044131-61bcca2ae6f9/go.mod h1:KNPI56t8uMlItrGyu3tS4NCd3jGfA6KjdUg4bsk+QJg= github.com/gcash/bchwallet/walletdb v0.0.0-20210524114850-4837f9798568 h1:lUckiJp1DwpCHFi8IuvtssDS3i3VHqkyluksH0IhL64= github.com/gcash/bchwallet/walletdb v0.0.0-20210524114850-4837f9798568/go.mod h1:KNPI56t8uMlItrGyu3tS4NCd3jGfA6KjdUg4bsk+QJg= +github.com/gcash/neutrino v0.0.0-20210524024247-f7bf0435d155/go.mod h1:91zIrel7tOt35+0hAN8Rrr7O07LZrR9b98Nk4AcC/Lg= +github.com/gcash/neutrino v0.0.0-20210524083541-2900eef48821/go.mod h1:ud/wsgR459Qy+HmEAoQZF9XPTYaOYep5df8ynowCnLs= +github.com/gcash/neutrino v0.0.0-20210524105223-4cec86bbd8a4/go.mod h1:YBR6T+ZT02eR1S7JGqJ2gVPxZlfjWswTCXB4HZafp/U= +github.com/gcash/neutrino v0.0.0-20210524114821-3b1878290cf9 h1:V5UNzi/5pZxE5s6kfCe59VjJRmfkyI+npZizMcAvEdI= +github.com/gcash/neutrino v0.0.0-20210524114821-3b1878290cf9/go.mod h1:MshBO/Xf8SCndZFetZ8yg79db/JghnOiMmPiY1Eatlw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= @@ -553,6 +566,7 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.1.0/go.mod h1:ly5QWKtiqC7tGfzgXYtpoZYmEWx5Z82/b18ASEL+yGc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.4.0/go.mod h1:IOyTYjcIO0rkmnGBfJTL0NJ11exy/Tc2QEuv7hCXp24= github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0/go.mod h1:XnLCLFp3tjoZJszVKjfpyAK6J8sYIcQXWQxmqLWF21I= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= @@ -707,6 +721,37 @@ github.com/lightningnetwork/lnd/tlv v1.0.2/go.mod h1:fICAfsqk1IOsC1J7G9IdsWX1EqW github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/ltcsuite/lnd/clock v0.0.0-20200822020009-1a001cbb895a h1:HsowuWZjuKHWnTS1l+TdWbl1I1iAZszoW1fvoc8M3To= +github.com/ltcsuite/lnd/clock v0.0.0-20200822020009-1a001cbb895a/go.mod h1:d474AXivZyx25TMDB2tjjiQNuPrybFcgz+yl7vQgFTs= +github.com/ltcsuite/lnd/queue v1.0.3 h1:c3lV2VVknh6tUNBgSHrIUiyaaFfWWdt+s2q0Oz4fDaQ= +github.com/ltcsuite/lnd/queue v1.0.3/go.mod h1:L0MMGRrsJFPHhTInek8YgW2v7NyB6pXrAh6Bbg2D7u8= +github.com/ltcsuite/lnd/ticker v1.0.1 h1:+0KvqE4HYO+fFPgNo+42hhyEjK0DwBp//IPmkLhCMcI= +github.com/ltcsuite/lnd/ticker v1.0.1/go.mod h1:WZKpekfDVAVv7Gsrr0GAWC/U1XURfGesFg9sQYJbeL4= +github.com/ltcsuite/ltcd v0.20.1-beta/go.mod h1:ZFQaYdYULIuTQiWqs7AUiHD2XhDFeeHW1IH+UYMdABU= +github.com/ltcsuite/ltcd v0.22.0-beta/go.mod h1:/BXtm50r591uMfXf8XgSpL5er32HCvheJtBSPYK5bFM= +github.com/ltcsuite/ltcd v0.22.1-beta h1:aXeIMuzwPss4VABDyc7Zbx+NMLYFaG3YkNTPNkKL9XA= +github.com/ltcsuite/ltcd v0.22.1-beta/go.mod h1:O9R9U/mbZwRgr3So8TlNmW7CPc2ZQVhWyVlhXrqu/vo= +github.com/ltcsuite/ltcd/btcec/v2 v2.1.0 h1:0DMWBjQDb0V1+4kCLOJlNdHs7ewwYturuUfLHq8mosY= +github.com/ltcsuite/ltcd/btcec/v2 v2.1.0/go.mod h1:Vc9ZYXMcl5D6bA0VwMvGRDJYggO3YZ7/BuIri02Lq0E= +github.com/ltcsuite/ltcd/ltcutil v1.1.0 h1:btwbdHO9cEr22zW/vgCLiF6ghh+IDngJdJsyhJ6mntU= +github.com/ltcsuite/ltcd/ltcutil v1.1.0/go.mod h1:VbZlcopVgQteiCC5KRjIuxXH5wi1CtzhsvoYZ3K7FaE= +github.com/ltcsuite/ltcd/ltcutil/psbt v1.1.0-1 h1:jMJ3CA8n0qIwsVQgDPLqN2Kmz01qiB9k5eONzVlCY5g= +github.com/ltcsuite/ltcd/ltcutil/psbt v1.1.0-1/go.mod h1:jpDQOdehihA+lu9OW26YgHSJ+6lReEb8HNcQL5grC4k= +github.com/ltcsuite/ltcutil v0.0.0-20191227053721-6bec450ea6ad/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA= +github.com/ltcsuite/ltcwallet v0.13.1 h1:XMyrDHn0BmgUgkNbR/Lzg36vjRsup3xdiPLSD471UMg= +github.com/ltcsuite/ltcwallet v0.13.1/go.mod h1:e6pIWRM9gsd5JnMsI9SgCJM0wi7awWdr20F1C1KUPiw= +github.com/ltcsuite/ltcwallet/wallet/txauthor v1.1.0 h1:MaSgMq7LCB+6dVm9oLzNCVp0lmY1WKmevmMK/t2c6To= +github.com/ltcsuite/ltcwallet/wallet/txauthor v1.1.0/go.mod h1:I53YELeELfA+dVporL+t44O8ArpuF8AjdJbbWIQaE2Q= +github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0 h1:P6H9zsMpBBuGOsp9lnil7XfPaPujDqrbcmkqvDdiSiI= +github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0/go.mod h1:lmA2Ozxvbr2M8Mqb6ugOv5/FQT6x2Qnwg3yT/NiWEks= +github.com/ltcsuite/ltcwallet/wallet/txsizes v1.1.0 h1:W884jMwG3K3Hu8FEMnV7KX1bd4HQd/4yvaevisFo9s8= +github.com/ltcsuite/ltcwallet/wallet/txsizes v1.1.0/go.mod h1:G9+XTWnE0xaXzHRzTuP+SOIXFPXFMfYF+w/wxPHb0K8= +github.com/ltcsuite/ltcwallet/walletdb v1.3.5 h1:WymVw0FBQ8KJgH7B88ujRqBOJ9R0en9K9urpJW4atAE= +github.com/ltcsuite/ltcwallet/walletdb v1.3.5/go.mod h1:29SBzxA55wNxY3ctFw6t5PgsULwf3NMwg2MiGQgtrJE= +github.com/ltcsuite/ltcwallet/wtxmgr v1.5.0 h1:5pM7L26/OzJMcwQqGwGKIZvPIBt1Er4Ve9Ymc9KK6gc= +github.com/ltcsuite/ltcwallet/wtxmgr v1.5.0/go.mod h1:jAnztxV6d2JykUlGLCO5yNvtosmFaGMshBx0kps+K+M= +github.com/ltcsuite/neutrino v0.13.2 h1:SDbRn4zt4e6z/4uVkVzVeKYpgXyoAh8ksA3m/uCIBaU= +github.com/ltcsuite/neutrino v0.13.2/go.mod h1:eTkaETZBeu3es/FisfjY8Cp3M2fC4s+2V2VUeS8O1Ic= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -867,6 +912,7 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU= github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= @@ -894,6 +940,7 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= @@ -1105,14 +1152,17 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1223,6 +1273,7 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= @@ -1300,6 +1351,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1329,6 +1381,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbuf golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1520,6 +1573,7 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201022181438-0ff5f38871d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210106152847-07624b53cd92/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210207032614-bba0dbe2a9ea/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210426193834-eac7f76ac494/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= @@ -1551,6 +1605,7 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0-dev.0.20201218190559-666aea1fb34c/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= @@ -1613,6 +1668,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/dex/testing/ltc/harness.sh b/dex/testing/ltc/harness.sh index 6942e99414..427a59ba14 100755 --- a/dex/testing/ltc/harness.sh +++ b/dex/testing/ltc/harness.sh @@ -20,7 +20,7 @@ export DELTA_WALLET_SEED="cNueSN7jzE9DEQsP8SgyonVvMSWyqk2xjTK3RPh2HAdWrR6zb8Y9" export DELTA_ADDRESS="QYUukoqupSC86DmWZLj3miArZFcy3eGC4i" # Signal that the node needs to restart after encrypting wallet export RESTART_AFTER_ENCRYPT="1" -export EXTRA_ARGS="-blockfilterindex=1 -peerblockfilters=1 -rpcserialversion=2" +export EXTRA_ARGS="-blockfilterindex=1 -peerblockfilters=1 -rpcserialversion=2 --rpcbind=0.0.0.0 --rpcallowip=0.0.0.0/0" export CREATE_DEFAULT_WALLET="1" export NEEDS_MWEB_PEGIN_ACTIVATION="1" # $1 is the node to create with. $2 is the wallet name diff --git a/go.mod b/go.mod index f4745e403e..406a801308 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,13 @@ require ( github.com/jrick/logrotate v1.0.0 github.com/lib/pq v1.10.4 github.com/lightninglabs/neutrino v0.14.2 + github.com/ltcsuite/ltcd v0.22.0-beta + github.com/ltcsuite/ltcd/ltcutil v1.1.0 + github.com/ltcsuite/ltcwallet v0.13.1 + github.com/ltcsuite/ltcwallet/wallet/txauthor v1.1.0 + github.com/ltcsuite/ltcwallet/walletdb v1.3.5 + github.com/ltcsuite/ltcwallet/wtxmgr v1.5.0 + github.com/ltcsuite/neutrino v0.13.2 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/tyler-smith/go-bip39 v1.1.0 @@ -116,6 +123,13 @@ require ( github.com/lightningnetwork/lnd/queue v1.0.1 // indirect github.com/lightningnetwork/lnd/ticker v1.0.0 // indirect github.com/lightningnetwork/lnd/tlv v1.0.2 // indirect + github.com/ltcsuite/lnd/clock v0.0.0-20200822020009-1a001cbb895a // indirect + github.com/ltcsuite/lnd/queue v1.0.3 // indirect + github.com/ltcsuite/lnd/ticker v1.0.1 // indirect + github.com/ltcsuite/ltcd/btcec/v2 v2.1.0 // indirect + github.com/ltcsuite/ltcd/ltcutil/psbt v1.1.0-1 // indirect + github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0 // indirect + github.com/ltcsuite/ltcwallet/wallet/txsizes v1.1.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.12 // indirect diff --git a/go.sum b/go.sum index 581a96eb61..91ef54356d 100644 --- a/go.sum +++ b/go.sum @@ -335,6 +335,7 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= @@ -723,6 +724,36 @@ github.com/lightningnetwork/lnd/tlv v1.0.2/go.mod h1:fICAfsqk1IOsC1J7G9IdsWX1EqW github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/ltcsuite/lnd/clock v0.0.0-20200822020009-1a001cbb895a h1:HsowuWZjuKHWnTS1l+TdWbl1I1iAZszoW1fvoc8M3To= +github.com/ltcsuite/lnd/clock v0.0.0-20200822020009-1a001cbb895a/go.mod h1:d474AXivZyx25TMDB2tjjiQNuPrybFcgz+yl7vQgFTs= +github.com/ltcsuite/lnd/queue v1.0.3 h1:c3lV2VVknh6tUNBgSHrIUiyaaFfWWdt+s2q0Oz4fDaQ= +github.com/ltcsuite/lnd/queue v1.0.3/go.mod h1:L0MMGRrsJFPHhTInek8YgW2v7NyB6pXrAh6Bbg2D7u8= +github.com/ltcsuite/lnd/ticker v1.0.1 h1:+0KvqE4HYO+fFPgNo+42hhyEjK0DwBp//IPmkLhCMcI= +github.com/ltcsuite/lnd/ticker v1.0.1/go.mod h1:WZKpekfDVAVv7Gsrr0GAWC/U1XURfGesFg9sQYJbeL4= +github.com/ltcsuite/ltcd v0.20.1-beta/go.mod h1:ZFQaYdYULIuTQiWqs7AUiHD2XhDFeeHW1IH+UYMdABU= +github.com/ltcsuite/ltcd v0.22.0-beta h1:jYVHOeg2oBDvsduV26LtCZnzwifMDWoVbkdHTjGvHTk= +github.com/ltcsuite/ltcd v0.22.0-beta/go.mod h1:/BXtm50r591uMfXf8XgSpL5er32HCvheJtBSPYK5bFM= +github.com/ltcsuite/ltcd/btcec/v2 v2.1.0 h1:0DMWBjQDb0V1+4kCLOJlNdHs7ewwYturuUfLHq8mosY= +github.com/ltcsuite/ltcd/btcec/v2 v2.1.0/go.mod h1:Vc9ZYXMcl5D6bA0VwMvGRDJYggO3YZ7/BuIri02Lq0E= +github.com/ltcsuite/ltcd/ltcutil v1.1.0 h1:btwbdHO9cEr22zW/vgCLiF6ghh+IDngJdJsyhJ6mntU= +github.com/ltcsuite/ltcd/ltcutil v1.1.0/go.mod h1:VbZlcopVgQteiCC5KRjIuxXH5wi1CtzhsvoYZ3K7FaE= +github.com/ltcsuite/ltcd/ltcutil/psbt v1.1.0-1 h1:jMJ3CA8n0qIwsVQgDPLqN2Kmz01qiB9k5eONzVlCY5g= +github.com/ltcsuite/ltcd/ltcutil/psbt v1.1.0-1/go.mod h1:jpDQOdehihA+lu9OW26YgHSJ+6lReEb8HNcQL5grC4k= +github.com/ltcsuite/ltcutil v0.0.0-20191227053721-6bec450ea6ad/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA= +github.com/ltcsuite/ltcwallet v0.13.1 h1:XMyrDHn0BmgUgkNbR/Lzg36vjRsup3xdiPLSD471UMg= +github.com/ltcsuite/ltcwallet v0.13.1/go.mod h1:e6pIWRM9gsd5JnMsI9SgCJM0wi7awWdr20F1C1KUPiw= +github.com/ltcsuite/ltcwallet/wallet/txauthor v1.1.0 h1:MaSgMq7LCB+6dVm9oLzNCVp0lmY1WKmevmMK/t2c6To= +github.com/ltcsuite/ltcwallet/wallet/txauthor v1.1.0/go.mod h1:I53YELeELfA+dVporL+t44O8ArpuF8AjdJbbWIQaE2Q= +github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0 h1:P6H9zsMpBBuGOsp9lnil7XfPaPujDqrbcmkqvDdiSiI= +github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0/go.mod h1:lmA2Ozxvbr2M8Mqb6ugOv5/FQT6x2Qnwg3yT/NiWEks= +github.com/ltcsuite/ltcwallet/wallet/txsizes v1.1.0 h1:W884jMwG3K3Hu8FEMnV7KX1bd4HQd/4yvaevisFo9s8= +github.com/ltcsuite/ltcwallet/wallet/txsizes v1.1.0/go.mod h1:G9+XTWnE0xaXzHRzTuP+SOIXFPXFMfYF+w/wxPHb0K8= +github.com/ltcsuite/ltcwallet/walletdb v1.3.5 h1:WymVw0FBQ8KJgH7B88ujRqBOJ9R0en9K9urpJW4atAE= +github.com/ltcsuite/ltcwallet/walletdb v1.3.5/go.mod h1:29SBzxA55wNxY3ctFw6t5PgsULwf3NMwg2MiGQgtrJE= +github.com/ltcsuite/ltcwallet/wtxmgr v1.5.0 h1:5pM7L26/OzJMcwQqGwGKIZvPIBt1Er4Ve9Ymc9KK6gc= +github.com/ltcsuite/ltcwallet/wtxmgr v1.5.0/go.mod h1:jAnztxV6d2JykUlGLCO5yNvtosmFaGMshBx0kps+K+M= +github.com/ltcsuite/neutrino v0.13.2 h1:SDbRn4zt4e6z/4uVkVzVeKYpgXyoAh8ksA3m/uCIBaU= +github.com/ltcsuite/neutrino v0.13.2/go.mod h1:eTkaETZBeu3es/FisfjY8Cp3M2fC4s+2V2VUeS8O1Ic= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -1129,14 +1160,17 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= From a16b36ff7347580c205c1b5c11ec7f8e0214957e Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Wed, 24 Aug 2022 08:39:19 -0500 Subject: [PATCH 2/5] use dcrlabs. attempt testnet addpeer --- client/asset/ltc/spv.go | 53 ++++++++++++++++++++++++++++++-------- dex/testing/loadbot/go.mod | 1 + dex/testing/loadbot/go.sum | 2 ++ go.mod | 3 ++- go.sum | 2 ++ 5 files changed, 49 insertions(+), 12 deletions(-) diff --git a/client/asset/ltc/spv.go b/client/asset/ltc/spv.go index 89c0b486dc..6ac2660ca0 100644 --- a/client/asset/ltc/spv.go +++ b/client/asset/ltc/spv.go @@ -29,6 +29,9 @@ import ( "github.com/btcsuite/btcwallet/waddrmgr" btcwallet "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wtxmgr" + neutrino "github.com/dcrlabs/neutrino-ltc" + labschain "github.com/dcrlabs/neutrino-ltc/chain" + "github.com/decred/slog" "github.com/jrick/logrotate/rotator" btcneutrino "github.com/lightninglabs/neutrino" "github.com/lightninglabs/neutrino/headerfs" @@ -44,7 +47,6 @@ import ( "github.com/ltcsuite/ltcwallet/walletdb" _ "github.com/ltcsuite/ltcwallet/walletdb/bdb" ltcwtxmgr "github.com/ltcsuite/ltcwallet/wtxmgr" - "github.com/ltcsuite/neutrino" ) const ( @@ -75,7 +77,7 @@ type ltcSPVWallet struct { // This section is populated in Start. *wallet.Wallet - chainClient *chain.NeutrinoClient + chainClient *labschain.NeutrinoClient cl *neutrino.ChainService loader *wallet.Loader neutrinoDB walletdb.DB @@ -151,6 +153,19 @@ func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dbDir string, return nil } +// walletParams works around a bug in dcrwallet that doesn't recognize +// wire.TestNet4 in (*ScopedKeyManager).cloneKeyWithVersion which is called from +// AccountProperties. Only do this for the *wallet.Wallet, not the +// *neutrino.ChainService. +func (w *ltcSPVWallet) walletParams() *ltcchaincfg.Params { + if w.chainParams.Name != ltcchaincfg.TestNet4Params.Name { + return w.chainParams + } + spoofParams := *w.chainParams + spoofParams.Net = ltcwire.TestNet3 + return &spoofParams +} + // Start initializes the *ltcwallet.Wallet and its supporting players and starts // syncing. func (w *ltcSPVWallet) Start() (btc.SPVService, error) { @@ -158,7 +173,7 @@ func (w *ltcSPVWallet) Start() (btc.SPVService, error) { return nil, fmt.Errorf("error initializing btcwallet+neutrino logging: %v", err) } // recoverWindow arguments borrowed from ltcwallet directly. - w.loader = wallet.NewLoader(w.chainParams, w.dir, true, 60*time.Second, 250) + w.loader = wallet.NewLoader(w.walletParams(), w.dir, true, 60*time.Second, 250) exists, err := w.loader.WalletExists() if err != nil { @@ -190,9 +205,11 @@ func (w *ltcSPVWallet) Start() (btc.SPVService, error) { // alpha node as an additional peer so we don't have to type it in. On // mainet and testnet3, add a known reliable persistent peer to be used in // addition to normal DNS seed-based peer discovery. - // var addPeers []string + var addPeers []string var connectPeers []string switch w.chainParams.Net { + case ltcwire.TestNet4: + addPeers = []string{"127.0.0.1:19335"} case ltcwire.TestNet, ltcwire.SimNet: // plain "wire.TestNet" is regnet! connectPeers = []string{"localhost:20585"} } @@ -203,12 +220,12 @@ func (w *ltcSPVWallet) Start() (btc.SPVService, error) { Database: w.neutrinoDB, ChainParams: *w.chainParams, PersistToDisk: true, // keep cfilter headers on disk for efficient rescanning - // AddPeers: addPeers, - ConnectPeers: connectPeers, - // // WARNING: PublishTransaction currently uses the entire duration - // // because if an external bug, but even if the resolved, a typical - // // inv/getdata round trip is ~4 seconds, so we set this so neutrino does - // // not cancel queries too readily. + AddPeers: addPeers, + ConnectPeers: connectPeers, + // WARNING: PublishTransaction currently uses the entire duration + // because if an external bug, but even if the resolved, a typical + // inv/getdata round trip is ~4 seconds, so we set this so neutrino does + // not cancel queries too readily. BroadcastTimeout: 6 * time.Second, }) if err != nil { @@ -216,7 +233,7 @@ func (w *ltcSPVWallet) Start() (btc.SPVService, error) { } errCloser.Add(w.cl.Stop) - w.chainClient = chain.NewNeutrinoClient(w.chainParams, w.cl) + w.chainClient = labschain.NewNeutrinoClient(w.chainParams, w.cl, &logAdapter{w.log}) oldBday := w.Manager.Birthday() wdb := w.Database() @@ -1047,3 +1064,17 @@ func (f *fileLoggerPlus) Critical(v ...interface{}) { f.Logger.Critical(v...) } + +type logAdapter struct { + dex.Logger +} + +var _ btclog.Logger = (*logAdapter)(nil) + +func (a *logAdapter) Level() btclog.Level { + return btclog.Level(a.Logger.Level()) +} + +func (a *logAdapter) SetLevel(lvl btclog.Level) { + a.Logger.SetLevel(slog.Level(lvl)) +} diff --git a/dex/testing/loadbot/go.mod b/dex/testing/loadbot/go.mod index 7c7d14d042..9c35393e4a 100644 --- a/dex/testing/loadbot/go.mod +++ b/dex/testing/loadbot/go.mod @@ -40,6 +40,7 @@ require ( github.com/dchest/blake2b v1.0.0 // indirect github.com/dchest/siphash v1.2.3 // indirect github.com/dcrlabs/neutrino-bch v0.0.0-20220809174944-921b271ba678 // indirect + github.com/dcrlabs/neutrino-ltc v0.0.0-20220819181220-04c154bb8ed8 // indirect github.com/deckarep/golang-set v1.8.0 // indirect github.com/decred/base58 v1.0.4 // indirect github.com/decred/dcrd/addrmgr/v2 v2.0.0 // indirect diff --git a/dex/testing/loadbot/go.sum b/dex/testing/loadbot/go.sum index 0739d553f4..69b50e7e5b 100644 --- a/dex/testing/loadbot/go.sum +++ b/dex/testing/loadbot/go.sum @@ -230,6 +230,8 @@ github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/dcrlabs/neutrino-bch v0.0.0-20220809174944-921b271ba678 h1:1/HDT7cXD/3PvaPxantDm0De/asWwFd3W9/EXZP7Hc8= github.com/dcrlabs/neutrino-bch v0.0.0-20220809174944-921b271ba678/go.mod h1:Iq+pOckoLj0Hj7C0W3/bEDpVAj7moSxP5W1v3R0Mhl4= +github.com/dcrlabs/neutrino-ltc v0.0.0-20220819181220-04c154bb8ed8 h1:ks3t+7KVijKS9oJrBpj26SY9RQl+wmJ1CosHb79F+sI= +github.com/dcrlabs/neutrino-ltc v0.0.0-20220819181220-04c154bb8ed8/go.mod h1:dWndR5wa89ZDMGbiizpTvVDsck+GWOVhDgGo0GZmMxI= github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E= diff --git a/go.mod b/go.mod index 406a801308..baa5a90bb5 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/dchest/blake2b v1.0.0 github.com/dcrlabs/neutrino-bch v0.0.0-20220809174944-921b271ba678 + github.com/dcrlabs/neutrino-ltc v0.0.0-20220819181220-04c154bb8ed8 github.com/decred/base58 v1.0.4 github.com/decred/dcrd/addrmgr/v2 v2.0.0 github.com/decred/dcrd/blockchain/stake/v4 v4.0.0 @@ -59,7 +60,6 @@ require ( github.com/ltcsuite/ltcwallet/wallet/txauthor v1.1.0 github.com/ltcsuite/ltcwallet/walletdb v1.3.5 github.com/ltcsuite/ltcwallet/wtxmgr v1.5.0 - github.com/ltcsuite/neutrino v0.13.2 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/tyler-smith/go-bip39 v1.1.0 @@ -130,6 +130,7 @@ require ( github.com/ltcsuite/ltcd/ltcutil/psbt v1.1.0-1 // indirect github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0 // indirect github.com/ltcsuite/ltcwallet/wallet/txsizes v1.1.0 // indirect + github.com/ltcsuite/neutrino v0.13.2 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.12 // indirect diff --git a/go.sum b/go.sum index 91ef54356d..a7c9ac1b84 100644 --- a/go.sum +++ b/go.sum @@ -230,6 +230,8 @@ github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/dcrlabs/neutrino-bch v0.0.0-20220809174944-921b271ba678 h1:1/HDT7cXD/3PvaPxantDm0De/asWwFd3W9/EXZP7Hc8= github.com/dcrlabs/neutrino-bch v0.0.0-20220809174944-921b271ba678/go.mod h1:Iq+pOckoLj0Hj7C0W3/bEDpVAj7moSxP5W1v3R0Mhl4= +github.com/dcrlabs/neutrino-ltc v0.0.0-20220819181220-04c154bb8ed8 h1:ks3t+7KVijKS9oJrBpj26SY9RQl+wmJ1CosHb79F+sI= +github.com/dcrlabs/neutrino-ltc v0.0.0-20220819181220-04c154bb8ed8/go.mod h1:dWndR5wa89ZDMGbiizpTvVDsck+GWOVhDgGo0GZmMxI= github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E= From 0e036d36804ea5d576ce91a53e33dfb48af9f04b Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Wed, 31 Aug 2022 12:14:30 -0500 Subject: [PATCH 3/5] reorganize btc spv. drop pointless conversion funcs --- client/asset/bch/spv.go | 8 +- client/asset/btc/btc.go | 40 +- client/asset/btc/spv.go | 2101 ++++--------------------------- client/asset/btc/spv_wrapper.go | 1677 ++++++++++++++++++++++++ client/asset/ltc/ltc.go | 77 +- client/asset/ltc/spv.go | 104 +- dex/errors.go | 15 +- 7 files changed, 1984 insertions(+), 2038 deletions(-) create mode 100644 client/asset/btc/spv_wrapper.go diff --git a/client/asset/bch/spv.go b/client/asset/bch/spv.go index 667c77885a..bb966fba86 100644 --- a/client/asset/bch/spv.go +++ b/client/asset/bch/spv.go @@ -134,8 +134,8 @@ func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dbDir string, return fmt.Errorf("CreateNewWallet error: %w", err) } - errCloser := dex.NewErrorCloser(log) - defer errCloser.Done() + errCloser := dex.NewErrorCloser() + defer errCloser.Done(log) errCloser.Add(loader.UnloadWallet) if extIdx > 0 || intIdx > 0 { @@ -186,8 +186,8 @@ func (w *bchSPVWallet) Start() (btc.SPVService, error) { return nil, fmt.Errorf("couldn't load wallet: %w", err) } - errCloser := dex.NewErrorCloser(w.log) - defer errCloser.Done() + errCloser := dex.NewErrorCloser() + defer errCloser.Done(w.log) errCloser.Add(w.loader.UnloadWallet) neutrinoDBPath := filepath.Join(w.dir, neutrinoDBName) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 79f65c2fb7..4717314f64 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -99,7 +99,7 @@ var ( walletBlockAllowance = time.Second * 10 conventionalConversionFactor = float64(dexbtc.UnitInfo.Conventional.ConversionFactor) - electrumOpts = []*asset.ConfigOption{ + ElectrumConfigOpts = []*asset.ConfigOption{ { Key: "rpcuser", DisplayName: "JSON-RPC Username", @@ -112,16 +112,14 @@ var ( NoEcho: true, }, { - Key: "rpcbind", // match RPCConfig struct field tags - DisplayName: "JSON-RPC Address", - Description: "Electrum's 'rpchost' or :", - DefaultValue: "127.0.0.1", + Key: "rpcbind", // match RPCConfig struct field tags + DisplayName: "JSON-RPC Address", + Description: "Electrum's 'rpchost' or :", }, { - Key: "rpcport", - DisplayName: "JSON-RPC Port", - Description: "Electrum's 'rpcport' (if not set with address)", - DefaultValue: "6789", + Key: "rpcport", + DisplayName: "JSON-RPC Port", + Description: "Electrum's 'rpcport' (if not set with rpcbind)", }, } @@ -149,7 +147,7 @@ var ( Tab: "Electrum (external)", Description: "Use an external Electrum Wallet", // json: DefaultConfigPath: filepath.Join(btcutil.AppDataDir("electrum", false), "config"), // e.g. ~/.electrum/config - ConfigOpts: append(electrumOpts, CommonConfigOpts("BTC", true)...), + ConfigOpts: append(append(ElectrumConfigOpts, apiFallbackOpt(false)), CommonConfigOpts("BTC", false)...), } // WalletInfo defines some general information about a Bitcoin wallet. @@ -166,6 +164,17 @@ var ( } ) +func apiFallbackOpt(defaultV bool) *asset.ConfigOption { + return &asset.ConfigOption{ + Key: "apifeefallback", + DisplayName: "External fee rate estimates", + Description: "Allow fee rate estimation from a block explorer API. " + + "This is useful as a fallback for SPV wallets and RPC wallets " + + "that have recently been started.", + IsBoolean: defaultV, + } +} + // CommonConfigOpts are the common options that the Wallets recognize. func CommonConfigOpts(symbol string /* upper-case */, withApiFallback bool) []*asset.ConfigOption { opts := []*asset.ConfigOption{ @@ -209,14 +218,7 @@ func CommonConfigOpts(symbol string /* upper-case */, withApiFallback bool) []*a } if withApiFallback { - opts = append(opts, &asset.ConfigOption{ - Key: "apifeefallback", - DisplayName: "External fee rate estimates", - Description: "Allow fee rate estimation from a block explorer API. " + - "This is useful as a fallback for SPV wallets and RPC wallets " + - "that have recently been started.", - IsBoolean: true, - }) + opts = append(opts, apiFallbackOpt(true)) } return opts } @@ -974,7 +976,7 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, net dex.Network) (ass switch cfg.Type { case walletTypeSPV: - return OpenSPVWallet(cloneCFG, newExtendedWallet) + return OpenSPVWallet(cloneCFG, openSPVWallet) case walletTypeRPC, walletTypeLegacy: rpcWallet, err := BTCCloneWallet(cloneCFG) if err != nil { diff --git a/client/asset/btc/spv.go b/client/asset/btc/spv.go index fbf99a5371..371823a090 100644 --- a/client/asset/btc/spv.go +++ b/client/asset/btc/spv.go @@ -1,49 +1,21 @@ -// This code is available on the terms of the project LICENSE.md file, -// also available online at https://blueoakcouncil.org/license/1.0.0. - -// 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 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 -// transaction could be found on-chain. -// 3. We don't see a mempool. We're blind to new transactions until they are -// mined. This requires special handling by the caller. We've been -// anticipating this, so Core and Swapper are permissive of missing acks for -// audit requests. - package btc import ( - "bytes" "context" - "encoding/hex" - "encoding/json" "errors" "fmt" - "math" "os" "path/filepath" - "sort" - "sync" "sync/atomic" "time" "decred.org/dcrdex/client/asset" "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/config" - dexbtc "decred.org/dcrdex/dex/networks/btc" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/btcutil/gcs" - "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btclog" "github.com/btcsuite/btcwallet/chain" @@ -51,143 +23,38 @@ import ( "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/btcsuite/btcwallet/walletdb" - _ "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" -) - -const ( - WalletTransactionNotFound = dex.ErrorKind("wallet transaction not found") - SpentStatusUnknown = dex.ErrorKind("spend status not known") - // NOTE: possibly unexport the two above error kinds. - - // defaultBroadcastWait is long enough for btcwallet's PublishTransaction - // method to record the outgoing transaction and queue it for broadcasting. - // This rough duration is necessary since with neutrino as the wallet's - // chain service, its chainClient.SendRawTransaction call is blocking for up - // to neutrino.Config.BroadcastTimeout while peers either respond to the inv - // request with a getdata or time out. However, in virtually all cases, we - // just need to know that btcwallet was able to create and store the - // transaction record, and pass it to the chain service. - defaultBroadcastWait = 2 * time.Second - - maxFutureBlockTime = 2 * time.Hour // see MaxTimeOffsetSeconds in btcd/blockchain/validate.go - neutrinoDBName = "neutrino.db" - logDirName = "logs" - logFileName = "neutrino.log" - defaultAcctNum = 0 - defaultAcctName = "default" ) -var wAddrMgrBkt = []byte("waddrmgr") - -// BTCWallet is satisfied by *btcwallet.Wallet -> *walletExtender. -type BTCWallet interface { - PublishTransaction(tx *wire.MsgTx, label string) error - CalculateAccountBalances(account uint32, confirms int32) (wallet.Balances, error) - ListUnspent(minconf, maxconf int32, acctName string) ([]*btcjson.ListUnspentResult, error) - FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error) - ResetLockedOutpoints() - LockOutpoint(op wire.OutPoint) - UnlockOutpoint(op wire.OutPoint) - LockedOutpoints() []btcjson.TransactionInput - NewChangeAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) - NewAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) - PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) - Unlock(passphrase []byte, lock <-chan time.Time) error - Lock() - Locked() bool - SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope, account uint32, minconf int32, - satPerKb btcutil.Amount, coinSelectionStrategy wallet.CoinSelectionStrategy, label string) (*wire.MsgTx, error) - HaveAddress(a btcutil.Address) (bool, error) - WaitForShutdown() - ChainSynced() bool // currently unused - AccountProperties(scope waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error) - // The below methods are not implemented by *wallet.Wallet, so must be - // implemented by the BTCWallet implementation. - WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) - SyncedTo() waddrmgr.BlockStamp - SignTx(*wire.MsgTx) error - BlockNotifications(context.Context) <-chan *BlockNotification - RescanAsync() error - ForceRescan() - Start() (SPVService, error) - Stop() - Reconfigure(*asset.WalletConfig, string) (bool, error) - Birthday() time.Time -} - -// BlockNotification is block hash and height delivered by a BTCWallet when it -// is finished processing a block. -type BlockNotification struct { - Hash chainhash.Hash - Height int32 -} - -// SPVService is satisfied by *neutrino.ChainService, with the exception of the -// Peers method, which has a generic interface in place of neutrino.ServerPeer. -type SPVService interface { - GetBlockHash(int64) (*chainhash.Hash, error) - BestBlock() (*headerfs.BlockStamp, error) - Peers() []SPVPeer - GetBlockHeight(hash *chainhash.Hash) (int32, error) - GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, error) - GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error) - GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error) - Stop() error -} - -// SPVPeer is satisfied by *neutrino.ServerPeer, but is generalized to -// accommodate underlying implementations other than lightninglabs/neutrino. -type SPVPeer interface { - StartingHeight() int32 - LastBlock() int32 -} +// btcSPVWallet implements BTCWallet for Bitcoin. +type btcSPVWallet struct { + *wallet.Wallet + chainParams *chaincfg.Params + log dex.Logger + dir string + birthdayV atomic.Value // time.Time + // if allowAutomaticRescan is true, if when connect is called, + // spvWallet.birthday is earlier than the birthday stored in the btcwallet + // database, the transaction history will be wiped and a rescan will start. + allowAutomaticRescan bool -// btcChainService wraps *neutrino.ChainService in order to translate the -// neutrino.ServerPeer to the SPVPeer interface type. -type btcChainService struct { - *neutrino.ChainService -} + // Below fields are populated in Start. + loader *wallet.Loader + chainClient *chain.NeutrinoClient + cl *neutrino.ChainService + neutrinoDB walletdb.DB -func (s *btcChainService) Peers() []SPVPeer { - rawPeers := s.ChainService.Peers() - peers := make([]SPVPeer, 0, len(rawPeers)) - for _, p := range rawPeers { - peers = append(peers, p) - } - return peers + // rescanStarting is set while reloading the wallet and dropping + // transactions from the wallet db. + rescanStarting uint32 // atomic } -var _ SPVService = (*btcChainService)(nil) - -// BTCWalletConstructor is a function to construct a BTCWallet. -type BTCWalletConstructor func(dir string, cfg *WalletConfig, chainParams *chaincfg.Params, log dex.Logger) BTCWallet - -func extendAddresses(extIdx, intIdx uint32, btcw *wallet.Wallet) error { - scopedKeyManager, err := btcw.Manager.FetchScopedKeyManager(waddrmgr.KeyScopeBIP0084) - if err != nil { - return err - } - - return walletdb.Update(btcw.Database(), func(dbtx walletdb.ReadWriteTx) error { - ns := dbtx.ReadWriteBucket(wAddrMgrBkt) - if extIdx > 0 { - if err := scopedKeyManager.ExtendExternalAddresses(ns, defaultAcctNum, extIdx); err != nil { - return err - } - } - return scopedKeyManager.ExtendInternalAddresses(ns, defaultAcctNum, intIdx) - }) -} +var _ BTCWallet = (*btcSPVWallet)(nil) // createSPVWallet creates a new SPV wallet. func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dataDir string, log dex.Logger, extIdx, intIdx uint32, net *chaincfg.Params) error { - dir := filepath.Join(dataDir, net.Name, "spv") - if err := logNeutrino(dir); err != nil { return fmt.Errorf("error initializing btcwallet+neutrino logging: %w", err) } @@ -240,1771 +107,247 @@ func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dataDir strin return nil } -var ( - // loggingInited will be set when the log rotator has been initialized. - loggingInited uint32 -) - -// logRotator initializes a rotating file logger. -func logRotator(dir string) (*rotator.Rotator, error) { - const maxLogRolls = 8 - logDir := filepath.Join(dir, logDirName) - if err := os.MkdirAll(logDir, 0744); err != nil { - return nil, fmt.Errorf("error creating log directory: %w", err) - } - - logFilename := filepath.Join(logDir, logFileName) - return rotator.New(logFilename, 32*1024, false, maxLogRolls) -} - -// logNeutrino initializes logging in the neutrino + wallet packages. Logging -// only has to be initialized once, so an atomic flag is used internally to -// return early on subsequent invocations. -// -// In theory, the the rotating file logger must be Close'd at some point, but -// there are concurrency issues with that since btcd and btcwallet have -// unsupervised goroutines still running after shutdown. So we leave the rotator -// running at the risk of losing some logs. -func logNeutrino(dir string) error { - if !atomic.CompareAndSwapUint32(&loggingInited, 0, 1) { - return nil - } - - logSpinner, err := logRotator(dir) - if err != nil { - return fmt.Errorf("error initializing log rotator: %w", err) - } - - backendLog := btclog.NewBackend(logSpinner) - - logger := func(name string, lvl btclog.Level) btclog.Logger { - l := backendLog.Logger(name) - l.SetLevel(lvl) - return l - } - - neutrino.UseLogger(logger("NTRNO", btclog.LevelDebug)) - wallet.UseLogger(logger("BTCW", btclog.LevelInfo)) - wtxmgr.UseLogger(logger("TXMGR", btclog.LevelInfo)) - chain.UseLogger(logger("CHAIN", btclog.LevelInfo)) - - 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 { - hash chainhash.Hash - lastAccess time.Time -} - -// scanCheckpoint is a cached, incomplete filterScanResult. When another scan -// is requested for an outpoint with a cached *scanCheckpoint, the scan can -// pick up where it left off. -type scanCheckpoint struct { - res *filterScanResult - lastAccess time.Time -} - -// 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 { - chainParams *chaincfg.Params - cfg *WalletConfig - wallet BTCWallet - cl SPVService - acctNum uint32 - acctName string - dir string - newBTCWallet BTCWalletConstructor - decodeAddr dexbtc.AddressDecoder - - txBlocksMtx sync.Mutex - txBlocks map[chainhash.Hash]*hashEntry - - checkpointMtx sync.Mutex - checkpoints map[outPoint]*scanCheckpoint - - log dex.Logger - - tipChan chan *block - syncTarget int32 - lastPrenatalHeight int32 -} - -var _ Wallet = (*spvWallet)(nil) -var _ tipNotifier = (*spvWallet)(nil) - -// reconfigure attempts to reconfigure the rpcClient for the new settings. Live -// reconfiguration is only attempted if the new wallet type is walletTypeSPV. An -// error is generated if the birthday is reduced and the special_activelyUsed -// flag is set. -func (w *spvWallet) reconfigure(cfg *asset.WalletConfig, currentAddress string) (restartRequired bool, err error) { - if cfg.Type != walletTypeSPV { - restartRequired = true - return - } - return w.wallet.Reconfigure(cfg, currentAddress) -} - -func (w *walletExtender) Reconfigure(cfg *asset.WalletConfig, _ /* currentAddress */ string) (restartRequired bool, err error) { - - parsedCfg := new(WalletConfig) - if err = config.Unmapify(cfg.Settings, parsedCfg); err != nil { - return - } - - newBday := parsedCfg.AdjustedBirthday() - if newBday.Equal(w.Birthday()) { - // It's the only setting we care about. - return - } - rescanRequired := newBday.Before(w.Birthday()) - if rescanRequired && parsedCfg.ActivelyUsed { - return false, errors.New("cannot decrease the birthday with active orders") - } - if err := w.updateDBBirthday(newBday); err != nil { - return false, fmt.Errorf("error storing new birthday: %w", err) - } - w.birthdayV.Store(newBday) - if rescanRequired { - if err = w.RescanAsync(); err != nil { - return false, fmt.Errorf("error initiating rescan after birthday adjustment: %w", err) - } - } - return -} - -// tipFeed satisfies the tipNotifier interface, signaling that *spvWallet -// will take precedence in sending block notifications. -func (w *spvWallet) tipFeed() <-chan *block { - return w.tipChan -} - -// storeTxBlock stores the block hash for the tx in the cache. -func (w *spvWallet) storeTxBlock(txHash, blockHash chainhash.Hash) { - w.txBlocksMtx.Lock() - defer w.txBlocksMtx.Unlock() - w.txBlocks[txHash] = &hashEntry{ - hash: blockHash, - lastAccess: time.Now(), - } -} - -// txBlock attempts to retrieve the block hash for the tx from the cache. -func (w *spvWallet) txBlock(txHash chainhash.Hash) (chainhash.Hash, bool) { - w.txBlocksMtx.Lock() - defer w.txBlocksMtx.Unlock() - entry, found := w.txBlocks[txHash] - if !found { - return chainhash.Hash{}, false - } - entry.lastAccess = time.Now() - return entry.hash, true -} - -// cacheCheckpoint caches a *filterScanResult so that future scans can be -// skipped or shortened. -func (w *spvWallet) cacheCheckpoint(txHash *chainhash.Hash, vout uint32, res *filterScanResult) { - if res.spend != nil && res.blockHash == nil { - // Probably set the start time too late. Don't cache anything - return - } - w.checkpointMtx.Lock() - defer w.checkpointMtx.Unlock() - w.checkpoints[newOutPoint(txHash, vout)] = &scanCheckpoint{ - res: res, - lastAccess: time.Now(), - } -} - -// 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)] - if !found { - return nil - } - check.lastAccess = time.Now() - res := *check.res - return &res -} - -// 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) checkpoint(txHash *chainhash.Hash, vout uint32) *filterScanResult { - res := w.unvalidatedCheckpoint(txHash, vout) - if res == nil { - return nil - } - if !w.blockIsMainchain(&res.checkpoint, -1) { - // reorg detected, abandon the checkpoint. - w.log.Debugf("abandoning checkpoint %s because checkpoint block %q is orphaned", - newOutPoint(txHash, vout), res.checkpoint) - w.checkpointMtx.Lock() - delete(w.checkpoints, newOutPoint(txHash, vout)) - w.checkpointMtx.Unlock() - return nil - } - return res -} - -func (w *spvWallet) RawRequest(method string, params []json.RawMessage) (json.RawMessage, error) { - // Not needed for spv wallet. - return nil, errors.New("RawRequest not available on spv") -} - -func (w *spvWallet) estimateSmartFee(confTarget int64, mode *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) { - return nil, errors.New("EstimateSmartFee not available on spv") -} - -func (w *spvWallet) ownsAddress(addr btcutil.Address) (bool, error) { - return w.wallet.HaveAddress(addr) -} - -func (w *spvWallet) sendRawTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { - // Publish the transaction in a goroutine so the caller may wait for a given - // period before it goes asynchronous and it is assumed that btcwallet at - // least succeeded with its DB updates and queueing of the transaction for - // rebroadcasting. In the future, a new btcwallet method should be added - // that returns after performing its internal actions, but broadcasting - // asynchronously and sending the outcome in a channel or promise. - res := make(chan error, 1) - go func() { - tStart := time.Now() - defer close(res) - if err := w.wallet.PublishTransaction(tx, ""); err != nil { - w.log.Errorf("PublishTransaction(%v) failure: %v", tx.TxHash(), err) - res <- err - return - } - defer w.log.Tracef("PublishTransaction(%v) completed in %v", tx.TxHash(), - time.Since(tStart)) // after outpoint unlocking and signalling - res <- nil - }() - - select { - case err := <-res: - if err != nil { - return nil, err - } - case <-time.After(defaultBroadcastWait): - w.log.Debugf("No error from PublishTransaction after %v for txn %v. "+ - "Assuming wallet accepted it.", defaultBroadcastWait, tx.TxHash()) - } - - // bitcoind would unlock these, btcwallet does not. Although it seems like - // they are no longer returned from ListUnspent after publishing, it must - // not be returned by LockedOutpoints (listlockunspent) for the lockedSats - // computations to be correct. - for _, txIn := range tx.TxIn { - w.wallet.UnlockOutpoint(txIn.PreviousOutPoint) - } - - txHash := tx.TxHash() // down here in case... the msgTx was mutated? - return &txHash, nil -} - -func (w *spvWallet) getBlock(blockHash chainhash.Hash) (*wire.MsgBlock, error) { - block, err := w.cl.GetBlock(blockHash) - if err != nil { - return nil, fmt.Errorf("neutrino GetBlock error: %v", err) - } - - return block.MsgBlock(), nil -} - -func (w *spvWallet) getBlockHash(blockHeight int64) (*chainhash.Hash, error) { - return w.cl.GetBlockHash(blockHeight) -} - -func (w *spvWallet) getBlockHeight(h *chainhash.Hash) (int32, error) { - return w.cl.GetBlockHeight(h) -} - -func (w *spvWallet) getBestBlockHash() (*chainhash.Hash, error) { - blk := w.wallet.SyncedTo() - return &blk.Hash, nil -} - -// getBestBlockHeight returns the height of the best block processed by the -// wallet, which indicates the height at which the compact filters have been -// retrieved and scanned for wallet addresses. This is may be less than -// getChainHeight, which indicates the height that the chain service has reached -// in its retrieval of block headers and compact filter headers. -func (w *spvWallet) getBestBlockHeight() (int32, error) { - return w.wallet.SyncedTo().Height, nil -} - -// getChainStamp satisfies chainStamper for manual median time calculations. -func (w *spvWallet) getChainStamp(blockHash *chainhash.Hash) (stamp time.Time, prevHash *chainhash.Hash, err error) { - hdr, err := w.cl.GetBlockHeader(blockHash) - if err != nil { - return - } - return hdr.Timestamp, &hdr.PrevBlock, nil -} - -// medianTime is the median time for the current best block. -func (w *spvWallet) medianTime() (time.Time, error) { - blk := w.wallet.SyncedTo() - return calcMedianTime(w, &blk.Hash) -} - -// getChainHeight is only for confirmations since it does not reflect the wallet -// manager's sync height, just the chain service. -func (w *spvWallet) getChainHeight() (int32, error) { - blk, err := w.cl.BestBlock() - if err != nil { - return -1, err - } - return blk.Height, err -} - -func (w *spvWallet) peerCount() (uint32, error) { - return uint32(len(w.cl.Peers())), nil -} - -// syncHeight is the best known sync height among peers. -func (w *spvWallet) syncHeight() int32 { - var maxHeight int32 - for _, p := range w.cl.Peers() { - tipHeight := p.StartingHeight() - lastBlockHeight := p.LastBlock() - if lastBlockHeight > tipHeight { - tipHeight = lastBlockHeight - } - if tipHeight > maxHeight { - maxHeight = tipHeight - } - } - return maxHeight -} - -// syncStatus is information about the wallet's sync status. -// -// The neutrino wallet has a two stage sync: -// 1. chain service fetching block headers and filter headers -// 2. wallet address manager retrieving and scanning filters -// -// We only report a single sync height, so we are going to show some progress in -// the chain service sync stage that comes before the wallet has performed any -// address recovery/rescan, and switch to the wallet's sync height when it -// reports non-zero height. -func (w *spvWallet) syncStatus() (*syncStatus, error) { - // Chain service headers (block and filter) height. - chainBlk, err := w.cl.BestBlock() - if err != nil { - return nil, err - } - target := w.syncHeight() - currentHeight := chainBlk.Height - - var synced bool - var blk *block - // Wallet address manager sync height. - if chainBlk.Timestamp.After(w.wallet.Birthday()) { - // After the wallet's birthday, the wallet address manager should begin - // syncing. Although block time stamps are not necessarily monotonically - // increasing, this is a reasonable condition at which the wallet's sync - // height should be consulted instead of the chain service's height. - walletBlock := w.wallet.SyncedTo() - if walletBlock.Height == 0 { - // The wallet is about to start its sync, so just return the last - // chain service height prior to wallet birthday until it begins. - return &syncStatus{ - Target: target, - Height: atomic.LoadInt32(&w.lastPrenatalHeight), - Syncing: true, - }, nil - } - blk = &block{ - height: int64(walletBlock.Height), - hash: walletBlock.Hash, - } - currentHeight = walletBlock.Height - synced = currentHeight >= target // maybe && w.wallet.ChainSynced() - } else { - // Chain service still syncing. - blk = &block{ - height: int64(currentHeight), - hash: chainBlk.Hash, - } - atomic.StoreInt32(&w.lastPrenatalHeight, currentHeight) - } +// openSPVWallet is the BTCWalletConstructor for Bitcoin. +func openSPVWallet(dir string, cfg *WalletConfig, + chainParams *chaincfg.Params, log dex.Logger) BTCWallet { - if target > 0 && atomic.SwapInt32(&w.syncTarget, target) == 0 { - w.tipChan <- blk + w := &btcSPVWallet{ + dir: dir, + chainParams: chainParams, + log: log, + allowAutomaticRescan: !cfg.ActivelyUsed, } - - return &syncStatus{ - Target: target, - Height: int32(blk.height), - Syncing: !synced, - }, nil + w.birthdayV.Store(cfg.AdjustedBirthday()) + return w } -// ownsInputs determines if we own the inputs of the tx. -func (w *spvWallet) ownsInputs(txid string) bool { - txHash, err := chainhash.NewHashFromStr(txid) - if err != nil { - w.log.Warnf("Error decoding txid %q: %v", txid, err) - return false - } - txDetails, err := w.wallet.WalletTransaction(txHash) - if err != nil { - w.log.Warnf("walletTransaction(%v) error: %v", txid, err) - return false - } - - for _, txIn := range txDetails.MsgTx.TxIn { - _, _, _, _, err = w.wallet.FetchInputInfo(&txIn.PreviousOutPoint) - if err != nil { - if !errors.Is(err, wallet.ErrNotMine) { - w.log.Warnf("FetchInputInfo error: %v", err) - } - return false - } - } - return true +func (w *btcSPVWallet) Birthday() time.Time { + return w.birthdayV.Load().(time.Time) } -// balances retrieves a wallet's balance details. -func (w *spvWallet) balances() (*GetBalancesResult, error) { - // Determine trusted vs untrusted coins with listunspent. - unspents, err := w.wallet.ListUnspent(0, math.MaxInt32, w.acctName) - if err != nil { - return nil, fmt.Errorf("error listing unspent outputs: %w", err) - } - var trusted, untrusted btcutil.Amount - for _, txout := range unspents { - if txout.Confirmations > 0 || w.ownsInputs(txout.TxID) { - trusted += btcutil.Amount(toSatoshi(txout.Amount)) - continue - } - untrusted += btcutil.Amount(toSatoshi(txout.Amount)) - } - - // listunspent does not include immature coinbase outputs or locked outputs. - bals, err := w.wallet.CalculateAccountBalances(w.acctNum, 0 /* confs */) - if err != nil { - return nil, err - } - w.log.Tracef("Bals: spendable = %v (%v trusted, %v untrusted, %v assumed locked), immature = %v", - bals.Spendable, trusted, untrusted, bals.Spendable-trusted-untrusted, bals.ImmatureReward) - // Locked outputs would be in wallet.Balances.Spendable. Assume they would - // be considered trusted and add them back in. - if all := trusted + untrusted; bals.Spendable > all { - trusted += bals.Spendable - all +func (w *btcSPVWallet) updateDBBirthday(bday time.Time) error { + btcw, isLoaded := w.loader.LoadedWallet() + if !isLoaded { + return fmt.Errorf("wallet not loaded") } - - return &GetBalancesResult{ - Mine: Balances{ - Trusted: trusted.ToBTC(), - Untrusted: untrusted.ToBTC(), - Immature: bals.ImmatureReward.ToBTC(), - }, - }, nil + return walletdb.Update(btcw.Database(), func(dbtx walletdb.ReadWriteTx) error { + ns := dbtx.ReadWriteBucket(wAddrMgrBkt) + return btcw.Manager.SetBirthday(ns, bday) + }) } -// listUnspent retrieves list of the wallet's UTXOs. -func (w *spvWallet) listUnspent() ([]*ListUnspentResult, error) { - unspents, err := w.wallet.ListUnspent(0, math.MaxInt32, w.acctName) - if err != nil { - return nil, err - } - res := make([]*ListUnspentResult, 0, len(unspents)) - for _, utxo := range unspents { - // If the utxo is unconfirmed, we should determine whether it's "safe" - // by seeing if we control the inputs of its transaction. - safe := utxo.Confirmations > 0 || w.ownsInputs(utxo.TxID) - - // These hex decodings are unlikely to fail because they come directly - // from the listunspent result. Regardless, they should not result in an - // error for the caller as we can return the valid utxos. - pkScript, err := hex.DecodeString(utxo.ScriptPubKey) - if err != nil { - w.log.Warnf("ScriptPubKey decode failure: %v", err) - continue - } - - redeemScript, err := hex.DecodeString(utxo.RedeemScript) - if err != nil { - w.log.Warnf("ScriptPubKey decode failure: %v", err) - continue - } - - res = append(res, &ListUnspentResult{ - TxID: utxo.TxID, - Vout: utxo.Vout, - Address: utxo.Address, - // Label: , - ScriptPubKey: pkScript, - Amount: utxo.Amount, - Confirmations: uint32(utxo.Confirmations), - RedeemScript: redeemScript, - Spendable: utxo.Spendable, - // Solvable: , - SafePtr: &safe, - }) - } - return res, nil -} - -// lockUnspent locks and unlocks outputs for spending. An output that is part of -// an order, but not yet spent, should be locked until spent or until the order -// is canceled or fails. -func (w *spvWallet) lockUnspent(unlock bool, ops []*output) error { - switch { - case unlock && len(ops) == 0: - w.wallet.ResetLockedOutpoints() - default: - for _, op := range ops { - op := wire.OutPoint{Hash: op.pt.txHash, Index: op.pt.vout} - if unlock { - w.wallet.UnlockOutpoint(op) - } else { - w.wallet.LockOutpoint(op) - } - } - } - return nil -} - -// listLockUnspent returns a slice of outpoints for all unspent outputs marked -// as locked by a wallet. -func (w *spvWallet) listLockUnspent() ([]*RPCOutpoint, error) { - outpoints := w.wallet.LockedOutpoints() - pts := make([]*RPCOutpoint, 0, len(outpoints)) - for _, pt := range outpoints { - pts = append(pts, &RPCOutpoint{ - TxID: pt.Txid, - Vout: pt.Vout, - }) - } - return pts, nil -} - -// changeAddress gets a new internal address from the wallet. The address will -// be bech32-encoded (P2WPKH). -func (w *spvWallet) changeAddress() (btcutil.Address, error) { - return w.wallet.NewChangeAddress(w.acctNum, waddrmgr.KeyScopeBIP0084) -} - -// externalAddress gets a new bech32-encoded (P2WPKH) external address from the -// wallet. -func (w *spvWallet) externalAddress() (btcutil.Address, error) { - return w.wallet.NewAddress(w.acctNum, waddrmgr.KeyScopeBIP0084) -} - -func (w *spvWallet) refundAddress() (btcutil.Address, error) { - return w.externalAddress() -} - -// signTx attempts to have the wallet sign the transaction inputs. -func (w *spvWallet) signTx(tx *wire.MsgTx) (*wire.MsgTx, error) { - // Can't use btcwallet.Wallet.SignTransaction, because it doesn't work for - // segwit transactions (for real?). - return tx, w.wallet.SignTx(tx) -} - -// privKeyForAddress retrieves the private key associated with the specified -// address. -func (w *spvWallet) privKeyForAddress(addr string) (*btcec.PrivateKey, error) { - a, err := w.decodeAddr(addr, w.chainParams) - if err != nil { - return nil, err - } - return w.wallet.PrivKeyForAddress(a) -} - -// Unlock unlocks the wallet. -func (w *spvWallet) Unlock(pw []byte) error { - return w.wallet.Unlock(pw, nil) -} - -// Lock locks the wallet. -func (w *spvWallet) Lock() error { - w.wallet.Lock() - return nil -} - -// sendToAddress sends the amount to the address. feeRate is in units of -// sats/byte. -func (w *spvWallet) sendToAddress(address string, value, feeRate uint64, subtract bool) (*chainhash.Hash, error) { - addr, err := w.decodeAddr(address, w.chainParams) - if err != nil { - return nil, err - } - - pkScript, err := txscript.PayToAddrScript(addr) - if err != nil { - return nil, err - } - - if subtract { - return w.sendWithSubtract(pkScript, value, feeRate) - } - - wireOP := wire.NewTxOut(int64(value), pkScript) - if dexbtc.IsDust(wireOP, feeRate) { - return nil, errors.New("output value is dust") - } - - // converting sats/vB -> sats/kvB - feeRateAmt := btcutil.Amount(feeRate * 1e3) - tx, err := w.wallet.SendOutputs([]*wire.TxOut{wireOP}, nil, w.acctNum, 0, - feeRateAmt, wallet.CoinSelectionLargest, "") - if err != nil { - return nil, err - } - - txHash := tx.TxHash() - - return &txHash, nil -} - -func (w *spvWallet) sendWithSubtract(pkScript []byte, value, feeRate uint64) (*chainhash.Hash, error) { - txOutSize := dexbtc.TxOutOverhead + uint64(len(pkScript)) // send-to address - var unfundedTxSize uint64 = dexbtc.MinimumTxOverhead + dexbtc.P2WPKHOutputSize /* change */ + txOutSize - - unspents, err := w.listUnspent() - if err != nil { - return nil, fmt.Errorf("error listing unspent outputs: %w", err) - } - - utxos, _, _, err := convertUnspent(0, unspents, w.chainParams) - if err != nil { - return nil, fmt.Errorf("error converting unspent outputs: %w", err) - } - - // With sendWithSubtract, fees are subtracted from the sent amount, so we - // target an input sum, not an output value. Makes the math easy. - enough := func(_, inputsVal uint64) bool { - return inputsVal >= value - } - - sum, inputsSize, _, fundingCoins, _, _, err := fund(utxos, enough) - if err != nil { - return nil, fmt.Errorf("error funding sendWithSubtract value of %s: %w", amount(value), err) - } - - fees := (unfundedTxSize + uint64(inputsSize)) * feeRate - send := value - fees - extra := sum - send - - switch { - case fees > sum: - return nil, fmt.Errorf("fees > sum") - case fees > value: - return nil, fmt.Errorf("fees > value") - case send > sum: - return nil, fmt.Errorf("send > sum") - } - - tx := wire.NewMsgTx(wire.TxVersion) - for op := range fundingCoins { - wireOP := wire.NewOutPoint(&op.txHash, op.vout) - txIn := wire.NewTxIn(wireOP, []byte{}, nil) - tx.AddTxIn(txIn) - } - - change := extra - fees - changeAddr, err := w.changeAddress() - if err != nil { - return nil, fmt.Errorf("error retrieving change address: %w", err) - } - - changeScript, err := txscript.PayToAddrScript(changeAddr) - if err != nil { - return nil, fmt.Errorf("error generating pubkey script: %w", err) - } - - changeOut := wire.NewTxOut(int64(change), changeScript) - - // One last check for dust. - if dexbtc.IsDust(changeOut, feeRate) { - // Re-calculate fees and change - fees = (unfundedTxSize - dexbtc.P2WPKHOutputSize + uint64(inputsSize)) * feeRate - send = sum - fees - } else { - tx.AddTxOut(changeOut) - } - - wireOP := wire.NewTxOut(int64(send), pkScript) - if dexbtc.IsDust(wireOP, feeRate) { - return nil, errors.New("output value is dust") - } - tx.AddTxOut(wireOP) - - if err := w.wallet.SignTx(tx); err != nil { - return nil, fmt.Errorf("signing error: %w", err) - } - - return w.sendRawTransaction(tx) -} - -// estimateSendTxFee callers should provide at least one output value. -func (w *spvWallet) estimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract bool) (fee uint64, err error) { - minTxSize := uint64(tx.SerializeSize()) - var sendAmount uint64 - for _, txOut := range tx.TxOut { - sendAmount += uint64(txOut.Value) - } - - // If subtract is true, select enough inputs for sendAmount. Fees will be taken - // from the sendAmount. If not, select enough inputs to cover minimum fees. - enough := func(inputsSize, sum uint64) bool { - if subtract { - return sum >= sendAmount - } - minFee := (minTxSize + inputsSize) * feeRate - return sum >= sendAmount+minFee - } - - unspents, err := w.listUnspent() - if err != nil { - return 0, fmt.Errorf("error listing unspent outputs: %w", err) - } - - utxos, _, _, err := convertUnspent(0, unspents, w.chainParams) - if err != nil { - return 0, fmt.Errorf("error converting unspent outputs: %w", err) - } - - sum, inputsSize, _, _, _, _, err := fund(utxos, enough) - if err != nil { - return 0, err - } - - txSize := minTxSize + uint64(inputsSize) - estFee := feeRate * txSize - remaining := sum - sendAmount - - // Check if there will be a change output if there is enough remaining. - estFeeWithChange := (txSize + dexbtc.P2WPKHOutputSize) * feeRate - var changeValue uint64 - if remaining > estFeeWithChange { - changeValue = remaining - estFeeWithChange - } - - if subtract { - // fees are already included in sendAmount, anything else is change. - changeValue = remaining - } - - var finalFee uint64 - if dexbtc.IsDustVal(dexbtc.P2WPKHOutputSize, changeValue, feeRate, true) { - // remaining cannot cover a non-dust change and the fee for the change. - finalFee = estFee + remaining - } else { - // additional fee will be paid for non-dust change - finalFee = estFeeWithChange - } - - if subtract { - sendAmount -= finalFee - } - if dexbtc.IsDustVal(minTxSize, sendAmount, feeRate, true) { - return 0, errors.New("output value is dust") - } - - return finalFee, nil -} - -// 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. -func (w *spvWallet) swapConfirmations(txHash *chainhash.Hash, vout uint32, pkScript []byte, - startTime time.Time) (confs uint32, spent bool, err error) { - - // First, check if it's a wallet transaction. We probably won't be able - // to see the spend status, since the wallet doesn't track the swap contract - // output, but we can get the block if it's been mined. - blockHash, confs, spent, err := w.confirmations(txHash, vout) - if err == nil { - return confs, spent, nil - } - var assumedMempool bool - switch err { - case WalletTransactionNotFound: - w.log.Tracef("swapConfirmations - WalletTransactionNotFound: %v:%d", txHash, vout) - case SpentStatusUnknown: - w.log.Tracef("swapConfirmations - SpentStatusUnknown: %v:%d (block %v, confs %d)", - txHash, vout, blockHash, confs) - if blockHash == nil { - // We generated this swap, but it probably hasn't been mined yet. - // It's SpentStatusUnknown because the wallet doesn't track the - // spend status of the swap contract output itself, since it's not - // recognized as a wallet output. We'll still try to find the - // confirmations with other means, but if we can't find it, we'll - // report it as a zero-conf unspent output. This ignores the remote - // possibility that the output could be both in mempool and spent. - assumedMempool = true - } - default: - return 0, false, err - } - - // If we still don't have the block hash, we may have it stored. Check the - // dex database first. This won't give us the confirmations and spent - // status, but it will allow us to short circuit a longer scan if we already - // know the output is spent. - if blockHash == nil { - blockHash, _ = w.mainchainBlockForStoredTx(txHash) - } - - // Our last option is neutrino. - w.log.Tracef("swapConfirmations - scanFilters: %v:%d (block %v, start time %v)", - txHash, vout, blockHash, startTime) - utxo, err := w.scanFilters(txHash, vout, pkScript, startTime, blockHash) - if err != nil { - return 0, false, err - } - - if utxo.spend == nil && utxo.blockHash == nil { - if assumedMempool { - w.log.Tracef("swapConfirmations - scanFilters did not find %v:%d, assuming in mempool.", - txHash, vout) - // NOT asset.CoinNotFoundError since this is normal for mempool - // transactions with an SPV wallet. - return 0, false, nil - } - return 0, false, fmt.Errorf("output %s:%v not found with search parameters startTime = %s, pkScript = %x", - txHash, vout, startTime, pkScript) - } - - if utxo.blockHash != nil { - bestHeight, err := w.getChainHeight() - if err != nil { - return 0, false, fmt.Errorf("getBestBlockHeight error: %v", err) - } - confs = uint32(bestHeight) - utxo.blockHeight + 1 - } - - if utxo.spend != nil { - // In the off-chance that a spend was found but not the output itself, - // confs will be incorrect here. - // In situations where we're looking for the counter-party's swap, we - // revoke if it's found to be spent, without inspecting the confs, so - // accuracy of confs is not significant. When it's our output, we'll - // know the block and won't end up here. (even if we did, we just end up - // sending out some inaccurate Data-severity notifications to the UI - // until the match progresses) - return confs, true, nil - } - - // unspent - return confs, false, nil -} - -func (w *spvWallet) locked() bool { - return w.wallet.Locked() -} - -func (w *spvWallet) walletLock() error { - w.wallet.Lock() - return nil -} - -func (w *spvWallet) walletUnlock(pw []byte) error { - return w.Unlock(pw) -} - -func (w *spvWallet) getBlockHeader(blockHash *chainhash.Hash) (*blockHeader, error) { - hdr, err := w.cl.GetBlockHeader(blockHash) - if err != nil { - return nil, err - } - - tip, err := w.cl.BestBlock() - if err != nil { - return nil, fmt.Errorf("BestBlock error: %v", err) - } - - blockHeight, err := w.cl.GetBlockHeight(blockHash) - if err != nil { - return nil, err - } - - return &blockHeader{ - Hash: hdr.BlockHash().String(), - Confirmations: int64(confirms(blockHeight, tip.Height)), - Height: int64(blockHeight), - Time: hdr.Timestamp.Unix(), - }, nil -} - -func (w *spvWallet) getBestBlockHeader() (*blockHeader, error) { - hash, err := w.getBestBlockHash() - if err != nil { - return nil, err - } - return w.getBlockHeader(hash) -} - -func (w *spvWallet) logFilePath() string { - return filepath.Join(w.dir, logDirName, logFileName) -} - -// connect will start the wallet and begin syncing. -func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) (err error) { - w.wallet = w.newBTCWallet(w.dir, w.cfg, w.chainParams, w.log) - w.cl, err = w.wallet.Start() - if err != nil { - return err - } - - blockNotes := w.wallet.BlockNotifications(ctx) - - // Nanny for the caches checkpoints and txBlocks caches. - wg.Add(1) - go func() { - defer wg.Done() - defer w.wallet.Stop() - - ticker := time.NewTicker(time.Minute * 20) - defer ticker.Stop() - expiration := time.Hour * 2 - for { - select { - case <-ticker.C: - w.txBlocksMtx.Lock() - for txHash, entry := range w.txBlocks { - if time.Since(entry.lastAccess) > expiration { - delete(w.txBlocks, txHash) - } - } - w.txBlocksMtx.Unlock() - - w.checkpointMtx.Lock() - for outPt, check := range w.checkpoints { - if time.Since(check.lastAccess) > expiration { - delete(w.checkpoints, outPt) - } - } - w.checkpointMtx.Unlock() - - case blk := <-blockNotes: - syncTarget := atomic.LoadInt32(&w.syncTarget) - if syncTarget == 0 || (blk.Height < syncTarget && blk.Height%10_000 != 0) { - continue - } - - select { - case w.tipChan <- &block{ - hash: blk.Hash, - height: int64(blk.Height), - }: - default: - w.log.Warnf("tip report channel was blocking") - } - - case <-ctx.Done(): - return - } - } - }() - - return nil -} - -// Start initializes the *btcwallet.Wallet and its supporting players and starts -// syncing. -func (w *walletExtender) Start() (SPVService, error) { - if err := logNeutrino(w.dir); err != nil { - return nil, fmt.Errorf("error initializing btcwallet+neutrino logging: %v", err) +// startWallet initializes the *btcwallet.Wallet and its supporting players and +// starts syncing. +func (w *btcSPVWallet) Start() (SPVService, error) { + if err := logNeutrino(w.dir); err != nil { + return nil, fmt.Errorf("error initializing btcwallet+neutrino logging: %v", err) } // timeout and recoverWindow arguments borrowed from btcwallet directly. w.loader = wallet.NewLoader(w.chainParams, w.dir, true, 60*time.Second, 250) - exists, err := w.loader.WalletExists() - if err != nil { - return nil, fmt.Errorf("error verifying wallet existence: %v", err) - } - if !exists { - return nil, errors.New("wallet not found") - } - - w.log.Debug("Starting native BTC wallet...") - btcw, err := w.loader.OpenExistingWallet([]byte(wallet.InsecurePubPassphrase), false) - if err != nil { - return nil, fmt.Errorf("couldn't load wallet: %w", err) - } - - errCloser := dex.NewErrorCloser(w.log) - defer errCloser.Done() - errCloser.Add(w.loader.UnloadWallet) - - neutrinoDBPath := filepath.Join(w.dir, neutrinoDBName) - w.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true, wallet.DefaultDBTimeout) - if err != nil { - return nil, fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) - } - errCloser.Add(w.neutrinoDB.Close) - - // Depending on the network, we add some addpeers or a connect peer. On - // regtest, if 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. On - // mainet and testnet3, add a known reliable persistent peer to be used in - // addition to normal DNS seed-based peer discovery. - var addPeers []string - var connectPeers []string - switch w.chainParams.Net { - case wire.MainNet: - addPeers = []string{"cfilters.ssgen.io"} - case wire.TestNet3: - addPeers = []string{"dex-test.ssgen.io"} - case wire.TestNet, wire.SimNet: // plain "wire.TestNet" is regnet! - connectPeers = []string{"localhost:20575"} - } - w.log.Debug("Starting neutrino chain service...") - w.cl, err = neutrino.NewChainService(neutrino.Config{ - DataDir: w.dir, - Database: w.neutrinoDB, - ChainParams: *w.chainParams, - PersistToDisk: true, // keep cfilter headers on disk for efficient rescanning - AddPeers: addPeers, - ConnectPeers: connectPeers, - // WARNING: PublishTransaction currently uses the entire duration - // because if an external bug, but even if the bug is resolved, a - // typical inv/getdata round trip is ~4 seconds, so we set this so - // neutrino does not cancel queries too readily. - BroadcastTimeout: 6 * time.Second, - }) - if err != nil { - return nil, fmt.Errorf("couldn't create Neutrino ChainService: %v", err) - } - errCloser.Add(w.cl.Stop) - - w.chainClient = chain.NewNeutrinoClient(w.chainParams, w.cl) - w.Wallet = btcw - - oldBday := btcw.Manager.Birthday() - - performRescan := w.Birthday().Before(oldBday) - if performRescan && !w.allowAutomaticRescan { - return nil, errors.New("cannot set earlier birthday while there are active deals") - } - - if !oldBday.Equal(w.Birthday()) { - if err := w.updateDBBirthday(w.Birthday()); err != nil { - w.log.Errorf("Failed to reset wallet manager birthday: %v", err) - performRescan = false - } - } - - if performRescan { - w.ForceRescan() - } - - if err = w.chainClient.Start(); err != nil { // lazily starts connmgr - return nil, fmt.Errorf("couldn't start Neutrino client: %v", err) - } - - w.log.Info("Synchronizing wallet with network...") - btcw.SynchronizeRPC(w.chainClient) - - errCloser.Success() - - return &btcChainService{w.cl}, nil -} - -func (w *walletExtender) updateDBBirthday(bday time.Time) error { - btcw, isLoaded := w.loader.LoadedWallet() - if !isLoaded { - return fmt.Errorf("wallet not loaded") - } - return walletdb.Update(btcw.Database(), func(dbtx walletdb.ReadWriteTx) error { - ns := dbtx.ReadWriteBucket(wAddrMgrBkt) - return btcw.Manager.SetBirthday(ns, bday) - }) -} - -// moveWalletData will move all wallet files to a backup directory. -func (w *spvWallet) moveWalletData(backupDir string) error { - timeString := time.Now().Format("2006-01-02T15:04:05") - err := os.MkdirAll(backupDir, 0744) - if err != nil { - return err - } - backupFolder := filepath.Join(backupDir, timeString) - return os.Rename(w.dir, backupFolder) -} - -// numDerivedAddresses returns the number of internal and external addresses -// that the wallet has derived. -func (w *spvWallet) numDerivedAddresses() (internal, external uint32, err error) { - props, err := w.wallet.AccountProperties(waddrmgr.KeyScopeBIP0084, w.acctNum) - if err != nil { - return 0, 0, err - } - - return props.InternalKeyCount, props.ExternalKeyCount, nil -} - -// RescanAsync initiates a full wallet recovery (used address discovery -// and transaction scanning) by stopping the btcwallet, dropping the transaction -// history from the wallet db, resetting the synced-to height of the wallet -// manager, restarting the wallet and its chain client, and finally commanding -// the wallet to resynchronize, which starts asynchronous wallet recovery. -// Progress of the rescan should be monitored with syncStatus. During the rescan -// wallet balances and known transactions may not be reported accurately or -// located. The SPVService is not stopped, so most spvWallet methods will -// continue to work without error, but methods using the btcWallet will likely -// return incorrect results or errors. -func (w *walletExtender) RescanAsync() error { - if !atomic.CompareAndSwapUint32(&w.rescanStarting, 0, 1) { - w.log.Error("rescan already in progress") - } - defer atomic.StoreUint32(&w.rescanStarting, 0) - w.log.Info("Stopping wallet and chain client...") - w.Wallet.Stop() // stops Wallet and chainClient (not chainService) - w.Wallet.WaitForShutdown() - w.chainClient.WaitForShutdown() - - w.ForceRescan() - - w.log.Info("Starting wallet...") - w.Wallet.Start() - - if err := w.chainClient.Start(); err != nil { - return fmt.Errorf("couldn't start Neutrino client: %v", err) - } - - w.log.Info("Synchronizing wallet with network...") - w.Wallet.SynchronizeRPC(w.chainClient) - return nil -} - -// ForceRescan forces a full rescan with active address discovery on wallet -// restart by dropping the complete transaction history and setting the -// "synced to" field to nil. See the btcwallet/cmd/dropwtxmgr app for more -// information. -func (w *walletExtender) ForceRescan() { - wdb := w.Wallet.Database() - - w.log.Info("Dropping transaction history to perform full rescan...") - err := wallet.DropTransactionHistory(wdb, false) - if err != nil { - w.log.Errorf("Failed to drop wallet transaction history: %v", err) - // Continue to attempt restarting the wallet anyway. - } - - err = walletdb.Update(wdb, func(dbtx walletdb.ReadWriteTx) error { - ns := dbtx.ReadWriteBucket(wAddrMgrBkt) // it'll be fine - return w.Wallet.Manager.SetSyncedTo(ns, nil) // never synced, forcing recover from birthday - }) - if err != nil { - w.log.Errorf("Failed to reset wallet manager sync height: %v", err) - } -} - -// stop stops the wallet and database threads. -func (w *walletExtender) Stop() { - w.log.Info("Unloading wallet") - if err := w.loader.UnloadWallet(); err != nil { - w.log.Errorf("UnloadWallet error: %v", err) - } - if w.chainClient != nil { - w.log.Trace("Stopping neutrino client chain interface") - w.chainClient.Stop() - w.chainClient.WaitForShutdown() - } - w.log.Trace("Stopping neutrino chain sync service") - if err := w.cl.Stop(); err != nil { - w.log.Errorf("error stopping neutrino chain service: %v", err) - } - w.log.Trace("Stopping neutrino DB.") - if err := w.neutrinoDB.Close(); err != nil { - w.log.Errorf("wallet db close error: %v", err) - } - - // NOTE: Do we need w.Wallet.Stop() - - w.log.Info("SPV wallet closed") -} - -// blockForStoredTx looks for a block hash in the txBlocks index. -func (w *spvWallet) blockForStoredTx(txHash *chainhash.Hash) (*chainhash.Hash, int32, error) { - // Check if we know the block hash for the tx. - blockHash, found := w.txBlock(*txHash) - if !found { - return nil, 0, nil - } - // Check that the block is still mainchain. - blockHeight, err := w.cl.GetBlockHeight(&blockHash) - if err != nil { - w.log.Errorf("Error retrieving block height for hash %s: %v", blockHash, err) - return nil, 0, err - } - return &blockHash, blockHeight, nil -} - -// blockIsMainchain will be true if the blockHash is that of a mainchain block. -func (w *spvWallet) blockIsMainchain(blockHash *chainhash.Hash, blockHeight int32) bool { - if blockHeight < 0 { - var err error - blockHeight, err = w.cl.GetBlockHeight(blockHash) - if err != nil { - w.log.Errorf("Error getting block height for hash %s", blockHash) - return false - } - } - checkHash, err := w.cl.GetBlockHash(int64(blockHeight)) - if err != nil { - w.log.Errorf("Error retrieving block hash for height %d", blockHeight) - return false - } - - return *checkHash == *blockHash -} - -// mainchainBlockForStoredTx gets the block hash and height for the transaction -// 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) - if err != nil { - w.log.Errorf("Error retrieving mainchain block height for hash %s", blockHash) - return nil, 0 - } - if blockHash == nil { - return nil, 0 - } - if !w.blockIsMainchain(blockHash, blockHeight) { - return nil, 0 - } - return blockHash, blockHeight -} - -// findBlockForTime locates a good start block so that a search beginning at the -// 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 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(-maxFutureBlockTime) - - bestHeight, err := w.getChainHeight() - if err != nil { - return nil, 0, fmt.Errorf("getChainHeight error: %v", err) - } - - getBlockTimeForHeight := func(height int32) (*chainhash.Hash, time.Time, error) { - hash, err := w.cl.GetBlockHash(int64(height)) - if err != nil { - return nil, time.Time{}, err - } - header, err := w.cl.GetBlockHeader(hash) - if err != nil { - return nil, time.Time{}, err - } - return hash, header.Timestamp, nil - } - - iHeight := sort.Search(int(bestHeight), func(h int) bool { - var iTime time.Time - _, iTime, err = getBlockTimeForHeight(int32(h)) - if err != nil { - return true - } - return iTime.After(offsetTime) - }) - if err != nil { - return nil, 0, fmt.Errorf("binary search error finding best block for time %q: %w", matchTime, err) - } - - // 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 > medianTimeBlocks blocks with inverted time order. - var count int - var iHash *chainhash.Hash - var iTime time.Time - for iHeight > 0 { - iHash, iTime, err = getBlockTimeForHeight(int32(iHeight)) - if err != nil { - return nil, 0, fmt.Errorf("getBlockTimeForHeight error: %w", err) - } - if iTime.Before(offsetTime) { - count++ - if count == medianTimeBlocks { - return iHash, int32(iHeight), nil - } - } else { - count = 0 - } - iHeight-- - } - return w.chainParams.GenesisHash, 0, nil - -} - -// scanFilters enables searching for an output and its spending input by -// scanning BIP158 compact filters. Caller should supply either blockHash or -// startTime. blockHash takes precedence. If blockHash is supplied, the scan -// will start at that block and continue to the current blockchain tip, or until -// both the output and a spending transaction is found. if startTime is -// 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 := 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(&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 - _, limitHeight, err = w.findBlockForTime(startTime) - if err != nil { - return nil, err - } - } - } else { - // No checkpoint, but user supplied a block hash. - var err error - limitHeight, err = w.getBlockHeight(blockHash) - if err != nil { - return nil, fmt.Errorf("error getting height for supplied block hash %s", blockHash) - } - } - - w.log.Debugf("Performing cfilters scan for %v:%d from height %d", txHash, vout, limitHeight) - - // Do a filter scan. - utxo, err := w.filterScanFromHeight(*txHash, vout, pkScript, limitHeight, checkPt) - if err != nil { - return nil, fmt.Errorf("filterScanFromHeight error: %w", err) - } - if utxo == nil { - return nil, asset.CoinNotFoundError - } - - // If we found a block, let's store a reference in our local database so we - // can maybe bypass a long search next time. - if utxo.blockHash != nil { - w.log.Debugf("cfilters scan SUCCEEDED for %v:%d. block hash: %v, spent: %v", - txHash, vout, utxo.blockHash, utxo.spend != nil) - w.storeTxBlock(*txHash, *utxo.blockHash) - } - - w.cacheCheckpoint(txHash, vout, utxo) - - return utxo, nil -} - -// getTxOut finds an unspent transaction output and its number of confirmations. -// To match the behavior of the RPC method, even if an output is found, if it's -// known to be spent, no *wire.TxOut and no error will be returned. -func (w *spvWallet) getTxOut(txHash *chainhash.Hash, vout uint32, pkScript []byte, startTime time.Time) (*wire.TxOut, uint32, error) { - // Check for a wallet transaction first - txDetails, err := w.wallet.WalletTransaction(txHash) - var blockHash *chainhash.Hash - if err != nil && !errors.Is(err, WalletTransactionNotFound) { - return nil, 0, fmt.Errorf("walletTransaction error: %w", err) - } - - if txDetails != nil { - spent, found := outputSpendStatus(txDetails, vout) - if found { - if spent { - return nil, 0, nil - } - if len(txDetails.MsgTx.TxOut) <= int(vout) { - return nil, 0, fmt.Errorf("wallet transaction %s doesn't have enough outputs for vout %d", txHash, vout) - } - - var confs uint32 - if txDetails.Block.Height > 0 { - tip, err := w.cl.BestBlock() - if err != nil { - return nil, 0, fmt.Errorf("BestBlock error: %v", err) - } - confs = uint32(confirms(txDetails.Block.Height, tip.Height)) - } - - msgTx := &txDetails.MsgTx - if len(msgTx.TxOut) <= int(vout) { - return nil, 0, fmt.Errorf("wallet transaction %s found, but not enough outputs for vout %d", txHash, vout) - } - return msgTx.TxOut[vout], confs, nil - - } - if txDetails.Block.Hash != (chainhash.Hash{}) { - blockHash = &txDetails.Block.Hash - } - } - - // We don't really know if it's spent, so we'll need to scan. - utxo, err := w.scanFilters(txHash, vout, pkScript, startTime, blockHash) + exists, err := w.loader.WalletExists() if err != nil { - return nil, 0, err + return nil, fmt.Errorf("error verifying wallet existence: %v", err) } - - if utxo == nil || utxo.spend != nil || utxo.blockHash == nil { - return nil, 0, nil + if !exists { + return nil, errors.New("wallet not found") } - tip, err := w.cl.BestBlock() + w.log.Debug("Starting native BTC wallet...") + btcw, err := w.loader.OpenExistingWallet([]byte(wallet.InsecurePubPassphrase), false) if err != nil { - return nil, 0, fmt.Errorf("BestBlock error: %v", err) + return nil, fmt.Errorf("couldn't load wallet: %w", err) } - confs := uint32(confirms(int32(utxo.blockHeight), tip.Height)) - - return utxo.txOut, confs, nil -} - -// filterScanFromHeight scans BIP158 filters beginning at the specified block -// height until the tip, or until a spending transaction is found. -func (w *spvWallet) filterScanFromHeight(txHash chainhash.Hash, vout uint32, pkScript []byte, startBlockHeight int32, checkPt *filterScanResult) (*filterScanResult, error) { - walletBlock := w.wallet.SyncedTo() // where cfilters are received and processed - tip := walletBlock.Height + errCloser := dex.NewErrorCloser() + defer errCloser.Done(w.log) + errCloser.Add(w.loader.UnloadWallet) - res := checkPt - if res == nil { - res = new(filterScanResult) + neutrinoDBPath := filepath.Join(w.dir, neutrinoDBName) + w.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true, wallet.DefaultDBTimeout) + if err != nil { + return nil, fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) } + errCloser.Add(w.neutrinoDB.Close) -search: - for height := startBlockHeight; height <= tip; height++ { - if res.spend != nil && res.blockHash == nil { - w.log.Warnf("A spending input (%s) was found during the scan but the output (%s) "+ - "itself wasn't found. Was the startBlockHeight early enough?", - newOutPoint(&res.spend.txHash, res.spend.vin), - newOutPoint(&txHash, vout), - ) - return res, nil - } - blockHash, err := w.getBlockHash(int64(height)) - if err != nil { - return nil, fmt.Errorf("error getting block hash for height %d: %w", height, err) - } - matched, err := w.matchPkScript(blockHash, [][]byte{pkScript}) - if err != nil { - return nil, fmt.Errorf("matchPkScript error: %w", err) - } - - res.checkpoint = *blockHash - if !matched { - continue search - } - // Pull the block. - w.log.Tracef("Block %v matched pkScript for output %v:%d. Pulling the block...", - blockHash, txHash, vout) - block, err := w.cl.GetBlock(*blockHash) - if err != nil { - return nil, fmt.Errorf("GetBlock error: %v", err) - } - msgBlock := block.MsgBlock() - - // Scan every transaction. - nextTx: - for _, tx := range msgBlock.Transactions { - // Look for a spending input. - if res.spend == nil { - for vin, txIn := range tx.TxIn { - prevOut := &txIn.PreviousOutPoint - if prevOut.Hash == txHash && prevOut.Index == vout { - res.spend = &spendingInput{ - txHash: tx.TxHash(), - vin: uint32(vin), - blockHash: *blockHash, - blockHeight: uint32(height), - } - w.log.Tracef("Found txn %v spending %v in block %v (%d)", res.spend.txHash, - txHash, res.spend.blockHash, res.spend.blockHeight) - if res.blockHash != nil { - break search - } - // The output could still be in this block, just not - // in this transaction. - continue nextTx - } - } - } - // Only check for the output if this is the right transaction. - if res.blockHash != nil || tx.TxHash() != txHash { - continue nextTx - } - for _, txOut := range tx.TxOut { - if bytes.Equal(txOut.PkScript, pkScript) { - res.blockHash = blockHash - res.blockHeight = uint32(height) - res.txOut = txOut - w.log.Tracef("Found txn %v in block %v (%d)", txHash, res.blockHash, height) - if res.spend != nil { - break search - } - // Keep looking for the spending transaction. - continue nextTx - } - } - } + // Depending on the network, we add some addpeers or a connect peer. On + // regtest, if 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. On + // mainet and testnet3, add a known reliable persistent peer to be used in + // addition to normal DNS seed-based peer discovery. + var addPeers []string + var connectPeers []string + switch w.chainParams.Net { + case wire.MainNet: + addPeers = []string{"cfilters.ssgen.io"} + case wire.TestNet3: + addPeers = []string{"dex-test.ssgen.io"} + case wire.TestNet, wire.SimNet: // plain "wire.TestNet" is regnet! + connectPeers = []string{"localhost:20575"} } - return res, nil -} - -// matchPkScript pulls the filter for the block and attempts to match the -// supplied scripts. -func (w *spvWallet) matchPkScript(blockHash *chainhash.Hash, scripts [][]byte) (bool, error) { - filter, err := w.cl.GetCFilter(*blockHash, wire.GCSFilterRegular) + w.log.Debug("Starting neutrino chain service...") + w.cl, err = neutrino.NewChainService(neutrino.Config{ + DataDir: w.dir, + Database: w.neutrinoDB, + ChainParams: *w.chainParams, + PersistToDisk: true, // keep cfilter headers on disk for efficient rescanning + AddPeers: addPeers, + ConnectPeers: connectPeers, + // WARNING: PublishTransaction currently uses the entire duration + // because if an external bug, but even if the resolved, a typical + // inv/getdata round trip is ~4 seconds, so we set this so neutrino does + // not cancel queries too readily. + BroadcastTimeout: 6 * time.Second, + }) if err != nil { - return false, fmt.Errorf("GetCFilter error: %w", err) + return nil, fmt.Errorf("couldn't create Neutrino ChainService: %v", err) } + errCloser.Add(w.cl.Stop) - if filter.N() == 0 { - return false, fmt.Errorf("unexpected empty filter for %s", blockHash) - } + w.chainClient = chain.NewNeutrinoClient(w.chainParams, w.cl) + w.Wallet = btcw - var filterKey [gcs.KeySize]byte - copy(filterKey[:], blockHash[:gcs.KeySize]) + oldBday := btcw.Manager.Birthday() - matchFound, err := filter.MatchAny(filterKey, scripts) - if err != nil { - return false, fmt.Errorf("MatchAny error: %w", err) + performRescan := w.Birthday().Before(oldBday) + if performRescan && !w.allowAutomaticRescan { + return nil, errors.New("cannot set earlier birthday while there are active deals") } - return matchFound, nil -} -// searchBlockForRedemptions attempts to find spending info for the specified -// contracts by searching every input of all txs in the provided block range. -func (w *spvWallet) searchBlockForRedemptions(ctx context.Context, reqs map[outPoint]*findRedemptionReq, - blockHash chainhash.Hash) (discovered map[outPoint]*findRedemptionResult) { - - // Just match all the scripts together. - scripts := make([][]byte, 0, len(reqs)) - for _, req := range reqs { - scripts = append(scripts, req.pkScript) + if !oldBday.Equal(w.Birthday()) { + if err := w.updateDBBirthday(w.Birthday()); err != nil { + w.log.Errorf("Failed to reset wallet manager birthday: %v", err) + performRescan = false + } } - discovered = make(map[outPoint]*findRedemptionResult, len(reqs)) - - matchFound, err := w.matchPkScript(&blockHash, scripts) - if err != nil { - w.log.Errorf("matchPkScript error: %v", err) - return + if performRescan { + w.ForceRescan() } - if !matchFound { - return + if err = w.chainClient.Start(); err != nil { // lazily starts connmgr + return nil, fmt.Errorf("couldn't start Neutrino client: %v", err) } - // There is at least one match. Pull the block. - block, err := w.cl.GetBlock(blockHash) - if err != nil { - w.log.Errorf("neutrino GetBlock error: %v", err) - return - } + w.log.Info("Synchronizing wallet with network...") + btcw.SynchronizeRPC(w.chainClient) - for _, msgTx := range block.MsgBlock().Transactions { - newlyDiscovered := findRedemptionsInTx(ctx, true, reqs, msgTx, w.chainParams) - for outPt, res := range newlyDiscovered { - discovered[outPt] = res - } - } - return -} + errCloser.Success() -// findRedemptionsInMempool is unsupported for SPV. -func (w *spvWallet) findRedemptionsInMempool(ctx context.Context, reqs map[outPoint]*findRedemptionReq) (discovered map[outPoint]*findRedemptionResult) { - return + return &btcChainService{w.cl}, nil } -// confirmations looks for the confirmation count and spend status on a -// transaction output that pays to this wallet. -func (w *spvWallet) confirmations(txHash *chainhash.Hash, vout uint32) (blockHash *chainhash.Hash, confs uint32, spent bool, err error) { - details, err := w.wallet.WalletTransaction(txHash) - if err != nil { - return nil, 0, false, err +// stop stops the wallet and database threads. +func (w *btcSPVWallet) Stop() { + w.log.Info("Unloading wallet") + if err := w.loader.UnloadWallet(); err != nil { + w.log.Errorf("UnloadWallet error: %v", err) } - - if details.Block.Hash != (chainhash.Hash{}) { - blockHash = &details.Block.Hash - height, err := w.getChainHeight() - if err != nil { - return nil, 0, false, err - } - confs = uint32(confirms(details.Block.Height, height)) + if w.chainClient != nil { + w.log.Trace("Stopping neutrino client chain interface") + w.chainClient.Stop() + w.chainClient.WaitForShutdown() } - - spent, found := outputSpendStatus(details, vout) - if found { - return blockHash, confs, spent, nil + w.log.Trace("Stopping neutrino chain sync service") + if err := w.cl.Stop(); err != nil { + w.log.Errorf("error stopping neutrino chain service: %v", err) + } + w.log.Trace("Stopping neutrino DB.") + if err := w.neutrinoDB.Close(); err != nil { + w.log.Errorf("wallet db close error: %v", err) } - return blockHash, confs, false, SpentStatusUnknown + // NOTE: Do we need w.Wallet.Stop() + + w.log.Info("SPV wallet closed") } -// getWalletTransaction checks the wallet database for the specified -// transaction. Only transactions with output scripts that pay to the wallet or -// transactions that spend wallet outputs are stored in the wallet database. -// This is pretty much copy-paste from btcwallet 'gettransaction' JSON-RPC -// handler. -func (w *spvWallet) getWalletTransaction(txHash *chainhash.Hash) (*GetTransactionResult, error) { - // Option # 1 just copies from UnstableAPI.TxDetails. Duplicating the - // unexported bucket key feels dirty. - // - // var details *wtxmgr.TxDetails - // err := walletdb.View(w.Database(), func(dbtx walletdb.ReadTx) error { - // txKey := []byte("wtxmgr") - // txmgrNs := dbtx.ReadBucket(txKey) - // var err error - // details, err = w.TxStore.TxDetails(txmgrNs, txHash) - // return err - // }) - - // Option #2 - // This is what the JSON-RPC does (and has since at least May 2018). - details, err := w.wallet.WalletTransaction(txHash) - if err != nil { - if errors.Is(err, WalletTransactionNotFound) { - return nil, asset.CoinNotFoundError // for the asset.Wallet interface - } - return nil, err +func (w *btcSPVWallet) Reconfigure(cfg *asset.WalletConfig, _ /* currentAddress */ string) (restartRequired bool, err error) { + parsedCfg := new(WalletConfig) + if err = config.Unmapify(cfg.Settings, parsedCfg); err != nil { + return } - syncBlock := w.wallet.SyncedTo() - - // TODO: The serialized transaction is already in the DB, so reserializing - // might be avoided here. According to btcwallet, details.SerializedTx is - // "optional" (?), but we might check for it. - txRaw, err := serializeMsgTx(&details.MsgTx) - if err != nil { - return nil, err + newBday := parsedCfg.AdjustedBirthday() + if newBday.Equal(w.Birthday()) { + // It's the only setting we care about. + return } - - ret := &GetTransactionResult{ - TxID: txHash.String(), - Hex: txRaw, // 'Hex' field name is a lie, kinda - Time: uint64(details.Received.Unix()), - TimeReceived: uint64(details.Received.Unix()), + rescanRequired := newBday.Before(w.Birthday()) + if rescanRequired && parsedCfg.ActivelyUsed { + return false, errors.New("cannot decrease the birthday with active orders") } - - if details.Block.Height != -1 { - ret.BlockHash = details.Block.Hash.String() - ret.BlockTime = uint64(details.Block.Time.Unix()) - // ret.BlockHeight = uint64(details.Block.Height) - ret.Confirmations = uint64(confirms(details.Block.Height, syncBlock.Height)) + if err := w.updateDBBirthday(newBday); err != nil { + return false, fmt.Errorf("error storing new birthday: %w", err) } - - return ret, nil - - /* - var debitTotal, creditTotal btcutil.Amount // credits excludes change - for _, deb := range details.Debits { - debitTotal += deb.Amount - } - for _, cred := range details.Credits { - if !cred.Change { - creditTotal += cred.Amount - } + w.birthdayV.Store(newBday) + if rescanRequired { + if err = w.RescanAsync(); err != nil { + return false, fmt.Errorf("error initiating rescan after birthday adjustment: %w", err) } + } + return +} - // Fee can only be determined if every input is a debit. - if len(details.Debits) == len(details.MsgTx.TxIn) { - var outputTotal btcutil.Amount - for _, output := range details.MsgTx.TxOut { - outputTotal += btcutil.Amount(output.Value) - } - ret.Fee = (debitTotal - outputTotal).ToBTC() - } +// RescanAsync initiates a full wallet recovery (used address discovery +// and transaction scanning) by stopping the btcwallet, dropping the transaction +// history from the wallet db, resetting the synced-to height of the wallet +// manager, restarting the wallet and its chain client, and finally commanding +// the wallet to resynchronize, which starts asynchronous wallet recovery. +// Progress of the rescan should be monitored with syncStatus. During the rescan +// wallet balances and known transactions may not be reported accurately or +// located. The SPVService is not stopped, so most spvWallet methods will +// continue to work without error, but methods using the btcWallet will likely +// return incorrect results or errors. +func (w *btcSPVWallet) RescanAsync() error { + if !atomic.CompareAndSwapUint32(&w.rescanStarting, 0, 1) { + w.log.Error("rescan already in progress") + } + defer atomic.StoreUint32(&w.rescanStarting, 0) + w.log.Info("Stopping wallet and chain client...") + w.Wallet.Stop() // stops Wallet and chainClient (not chainService) + w.Wallet.WaitForShutdown() + w.chainClient.WaitForShutdown() - ret.Amount = creditTotal.ToBTC() - return ret, nil - */ -} + w.ForceRescan() -// walletExtender gives us access to a handful of fields or methods on -// *wallet.Wallet that don't make sense to stub out for testing. -type walletExtender struct { - *wallet.Wallet - chainParams *chaincfg.Params - log dex.Logger - dir string - birthdayV atomic.Value // time.Time - // if allowAutomaticRescan is true, if when connect is called, - // spvWallet.birthday is earlier than the birthday stored in the btcwallet - // database, the transaction history will be wiped and a rescan will start. - allowAutomaticRescan bool + w.log.Info("Starting wallet...") + w.Wallet.Start() - // Below fields are populated in Start. - loader *wallet.Loader - chainClient *chain.NeutrinoClient - cl *neutrino.ChainService - neutrinoDB walletdb.DB + if err := w.chainClient.Start(); err != nil { + return fmt.Errorf("couldn't start Neutrino client: %v", err) + } - // rescanStarting is set while reloading the wallet and dropping - // transactions from the wallet db. - rescanStarting uint32 // atomic + w.log.Info("Synchronizing wallet with network...") + w.Wallet.SynchronizeRPC(w.chainClient) + return nil } -var _ BTCWallet = (*walletExtender)(nil) - -// newExtendedWallet is the BTCWalletConstructor for Bitcoin. -func newExtendedWallet(dir string, cfg *WalletConfig, - chainParams *chaincfg.Params, log dex.Logger) BTCWallet { +// ForceRescan forces a full rescan with active address discovery on wallet +// restart by dropping the complete transaction history and setting the +// "synced to" field to nil. See the btcwallet/cmd/dropwtxmgr app for more +// information. +func (w *btcSPVWallet) ForceRescan() { + wdb := w.Wallet.Database() - w := &walletExtender{ - dir: dir, - chainParams: chainParams, - log: log, - allowAutomaticRescan: !cfg.ActivelyUsed, + w.log.Info("Dropping transaction history to perform full rescan...") + err := wallet.DropTransactionHistory(wdb, false) + if err != nil { + w.log.Errorf("Failed to drop wallet transaction history: %v", err) + // Continue to attempt restarting the wallet anyway. } - w.birthdayV.Store(cfg.AdjustedBirthday()) - return w -} -func (w *walletExtender) Birthday() time.Time { - return w.birthdayV.Load().(time.Time) + err = walletdb.Update(wdb, func(dbtx walletdb.ReadWriteTx) error { + ns := dbtx.ReadWriteBucket(wAddrMgrBkt) // it'll be fine + return w.Wallet.Manager.SetSyncedTo(ns, nil) // never synced, forcing recover from birthday + }) + if err != nil { + w.log.Errorf("Failed to reset wallet manager sync height: %v", err) + } } // walletTransaction pulls the transaction from the database. -func (w *walletExtender) WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) { +func (w *btcSPVWallet) WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) { details, err := wallet.UnstableAPI(w.Wallet).TxDetails(txHash) if err != nil { return nil, err @@ -2016,7 +359,7 @@ func (w *walletExtender) WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDe return details, nil } -func (w *walletExtender) SyncedTo() waddrmgr.BlockStamp { +func (w *btcSPVWallet) SyncedTo() waddrmgr.BlockStamp { return w.Wallet.Manager.SyncedTo() } @@ -2026,7 +369,7 @@ func (w *walletExtender) SyncedTo() waddrmgr.BlockStamp { // passes the birthday block and the wallet looks it up based on the birthday // Time and the downloaded block headers. // This is presently unused, but I have plans for it with a wallet rescan. -// func (w *walletExtender) getWalletBirthdayBlock() (*waddrmgr.BlockStamp, error) { +// func (w *btcSPVWallet) getWalletBirthdayBlock() (*waddrmgr.BlockStamp, error) { // var birthdayBlock waddrmgr.BlockStamp // err := walletdb.View(w.Database(), func(dbtx walletdb.ReadTx) error { // ns := dbtx.ReadBucket([]byte("waddrmgr")) // it'll be fine @@ -2041,7 +384,7 @@ func (w *walletExtender) SyncedTo() waddrmgr.BlockStamp { // } // signTransaction signs the transaction inputs. -func (w *walletExtender) SignTx(tx *wire.MsgTx) error { +func (w *btcSPVWallet) SignTx(tx *wire.MsgTx) error { var prevPkScripts [][]byte var inputValues []btcutil.Amount for _, txIn := range tx.TxIn { @@ -2061,7 +404,7 @@ func (w *walletExtender) SignTx(tx *wire.MsgTx) error { return txauthor.AddAllInputScripts(tx, prevPkScripts, inputValues, &secretSource{w, w.chainParams}) } -func (w *walletExtender) BlockNotifications(ctx context.Context) <-chan *BlockNotification { +func (w *btcSPVWallet) BlockNotifications(ctx context.Context) <-chan *BlockNotification { cl := w.Wallet.NtfnServer.TransactionNotifications() ch := make(chan *BlockNotification, 1) go func() { @@ -2090,7 +433,7 @@ func (w *walletExtender) BlockNotifications(ctx context.Context) <-chan *BlockNo // secretSource is used to locate keys and redemption scripts while signing a // transaction. secretSource satisfies the txauthor.SecretsSource interface. type secretSource struct { - w *walletExtender + w *btcSPVWallet chainParams *chaincfg.Params } @@ -2136,22 +479,36 @@ func (s *secretSource) GetScript(addr btcutil.Address) ([]byte, error) { return msa.Script() } -func confirms(txHeight, curHeight int32) int32 { - switch { - case txHeight == -1, txHeight > curHeight: - return 0 - default: - return curHeight - txHeight + 1 +// logNeutrino initializes logging in the neutrino + wallet packages. Logging +// only has to be initialized once, so an atomic flag is used internally to +// return early on subsequent invocations. +// +// In theory, the the rotating file logger must be Close'd at some point, but +// there are concurrency issues with that since btcd and btcwallet have +// unsupervised goroutines still running after shutdown. So we leave the rotator +// running at the risk of losing some logs. +func logNeutrino(dir string) error { + if !atomic.CompareAndSwapUint32(&loggingInited, 0, 1) { + return nil } -} -// outputSpendStatus will return the spend status of the output if it's found -// in the TxDetails.Credits. -func outputSpendStatus(details *wtxmgr.TxDetails, vout uint32) (spend, found bool) { - for _, credit := range details.Credits { - if credit.Index == vout { - return credit.Spent, true - } + logSpinner, err := logRotator(dir) + if err != nil { + return fmt.Errorf("error initializing log rotator: %w", err) + } + + backendLog := btclog.NewBackend(logWriter{logSpinner}) + + logger := func(name string, lvl btclog.Level) btclog.Logger { + l := backendLog.Logger(name) + l.SetLevel(lvl) + return l } - return false, false + + neutrino.UseLogger(logger("NTRNO", btclog.LevelDebug)) + wallet.UseLogger(logger("BTCW", btclog.LevelInfo)) + wtxmgr.UseLogger(logger("TXMGR", btclog.LevelInfo)) + chain.UseLogger(logger("CHAIN", btclog.LevelInfo)) + + return nil } diff --git a/client/asset/btc/spv_wrapper.go b/client/asset/btc/spv_wrapper.go new file mode 100644 index 0000000000..f13658ade0 --- /dev/null +++ b/client/asset/btc/spv_wrapper.go @@ -0,0 +1,1677 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +// 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 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 +// transaction could be found on-chain. +// 3. We don't see a mempool. We're blind to new transactions until they are +// mined. This requires special handling by the caller. We've been +// anticipating this, so Core and Swapper are permissive of missing acks for +// audit requests. + +package btc + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "math" + "os" + "path/filepath" + "sort" + "sync" + "sync/atomic" + "time" + + "decred.org/dcrdex/client/asset" + "decred.org/dcrdex/dex" + dexbtc "decred.org/dcrdex/dex/networks/btc" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/gcs" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/walletdb" + _ "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" +) + +const ( + WalletTransactionNotFound = dex.ErrorKind("wallet transaction not found") + SpentStatusUnknown = dex.ErrorKind("spend status not known") + // NOTE: possibly unexport the two above error kinds. + + // defaultBroadcastWait is long enough for btcwallet's PublishTransaction + // method to record the outgoing transaction and queue it for broadcasting. + // This rough duration is necessary since with neutrino as the wallet's + // chain service, its chainClient.SendRawTransaction call is blocking for up + // to neutrino.Config.BroadcastTimeout while peers either respond to the inv + // request with a getdata or time out. However, in virtually all cases, we + // just need to know that btcwallet was able to create and store the + // transaction record, and pass it to the chain service. + defaultBroadcastWait = 2 * time.Second + + maxFutureBlockTime = 2 * time.Hour // see MaxTimeOffsetSeconds in btcd/blockchain/validate.go + neutrinoDBName = "neutrino.db" + logDirName = "logs" + logFileName = "neutrino.log" + defaultAcctNum = 0 + defaultAcctName = "default" +) + +var wAddrMgrBkt = []byte("waddrmgr") + +// BTCWallet is roughly the (btcwallet/wallet.*Wallet) interface, with some +// additional required methods added. +type BTCWallet interface { + PublishTransaction(tx *wire.MsgTx, label string) error + CalculateAccountBalances(account uint32, confirms int32) (wallet.Balances, error) + ListUnspent(minconf, maxconf int32, acctName string) ([]*btcjson.ListUnspentResult, error) + FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error) + ResetLockedOutpoints() + LockOutpoint(op wire.OutPoint) + UnlockOutpoint(op wire.OutPoint) + LockedOutpoints() []btcjson.TransactionInput + NewChangeAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) + NewAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) + PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) + Unlock(passphrase []byte, lock <-chan time.Time) error + Lock() + Locked() bool + SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope, account uint32, minconf int32, + satPerKb btcutil.Amount, coinSelectionStrategy wallet.CoinSelectionStrategy, label string) (*wire.MsgTx, error) + HaveAddress(a btcutil.Address) (bool, error) + WaitForShutdown() + ChainSynced() bool // currently unused + AccountProperties(scope waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error) + // The below methods are not implemented by *wallet.Wallet, so must be + // implemented by the BTCWallet implementation. + WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) + SyncedTo() waddrmgr.BlockStamp + SignTx(*wire.MsgTx) error + BlockNotifications(context.Context) <-chan *BlockNotification + RescanAsync() error + ForceRescan() + Start() (SPVService, error) + Stop() + Reconfigure(*asset.WalletConfig, string) (bool, error) + Birthday() time.Time +} + +// BlockNotification is block hash and height delivered by a BTCWallet when it +// is finished processing a block. +type BlockNotification struct { + Hash chainhash.Hash + Height int32 +} + +// SPVService is satisfied by *neutrino.ChainService, with the exception of the +// Peers method, which has a generic interface in place of neutrino.ServerPeer. +type SPVService interface { + GetBlockHash(int64) (*chainhash.Hash, error) + BestBlock() (*headerfs.BlockStamp, error) + Peers() []SPVPeer + GetBlockHeight(hash *chainhash.Hash) (int32, error) + GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, error) + GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error) + GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error) + Stop() error +} + +// SPVPeer is satisfied by *neutrino.ServerPeer, but is generalized to +// accommodate underlying implementations other than lightninglabs/neutrino. +type SPVPeer interface { + StartingHeight() int32 + LastBlock() int32 +} + +// btcChainService wraps *neutrino.ChainService in order to translate the +// neutrino.ServerPeer to the SPVPeer interface type. +type btcChainService struct { + *neutrino.ChainService +} + +func (s *btcChainService) Peers() []SPVPeer { + rawPeers := s.ChainService.Peers() + peers := make([]SPVPeer, 0, len(rawPeers)) + for _, p := range rawPeers { + peers = append(peers, p) + } + return peers +} + +var _ SPVService = (*btcChainService)(nil) + +// BTCWalletConstructor is a function to construct a BTCWallet. +type BTCWalletConstructor func(dir string, cfg *WalletConfig, chainParams *chaincfg.Params, log dex.Logger) BTCWallet + +func extendAddresses(extIdx, intIdx uint32, btcw *wallet.Wallet) error { + scopedKeyManager, err := btcw.Manager.FetchScopedKeyManager(waddrmgr.KeyScopeBIP0084) + if err != nil { + return err + } + + return walletdb.Update(btcw.Database(), func(dbtx walletdb.ReadWriteTx) error { + ns := dbtx.ReadWriteBucket(wAddrMgrBkt) + if extIdx > 0 { + if err := scopedKeyManager.ExtendExternalAddresses(ns, defaultAcctNum, extIdx); err != nil { + return err + } + } + if intIdx > 0 { + return scopedKeyManager.ExtendInternalAddresses(ns, defaultAcctNum, intIdx) + } + return nil + }) +} + +var ( + // loggingInited will be set when the log rotator has been initialized. + loggingInited uint32 +) + +// logRotator initializes a rotating file logger. +func logRotator(dir string) (*rotator.Rotator, error) { + const maxLogRolls = 8 + logDir := filepath.Join(dir, logDirName) + if err := os.MkdirAll(logDir, 0744); err != nil { + return nil, fmt.Errorf("error creating log directory: %w", err) + } + + logFilename := filepath.Join(logDir, logFileName) + return rotator.New(logFilename, 32*1024, false, maxLogRolls) +} + +// 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 { + hash chainhash.Hash + lastAccess time.Time +} + +// scanCheckpoint is a cached, incomplete filterScanResult. When another scan +// is requested for an outpoint with a cached *scanCheckpoint, the scan can +// pick up where it left off. +type scanCheckpoint struct { + res *filterScanResult + 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 { + chainParams *chaincfg.Params + cfg *WalletConfig + wallet BTCWallet + cl SPVService + acctNum uint32 + acctName string + dir string + newBTCWallet BTCWalletConstructor + decodeAddr dexbtc.AddressDecoder + + txBlocksMtx sync.Mutex + txBlocks map[chainhash.Hash]*hashEntry + + checkpointMtx sync.Mutex + checkpoints map[outPoint]*scanCheckpoint + + log dex.Logger + + tipChan chan *block + syncTarget int32 + lastPrenatalHeight int32 +} + +var _ Wallet = (*spvWallet)(nil) +var _ tipNotifier = (*spvWallet)(nil) + +// reconfigure attempts to reconfigure the rpcClient for the new settings. Live +// reconfiguration is only attempted if the new wallet type is walletTypeSPV. An +// error is generated if the birthday is reduced and the special_activelyUsed +// flag is set. +func (w *spvWallet) reconfigure(cfg *asset.WalletConfig, currentAddress string) (restartRequired bool, err error) { + if cfg.Type != walletTypeSPV { + restartRequired = true + return + } + return w.wallet.Reconfigure(cfg, currentAddress) +} + +// tipFeed satisfies the tipNotifier interface, signaling that *spvWallet +// will take precedence in sending block notifications. +func (w *spvWallet) tipFeed() <-chan *block { + return w.tipChan +} + +// storeTxBlock stores the block hash for the tx in the cache. +func (w *spvWallet) storeTxBlock(txHash, blockHash chainhash.Hash) { + w.txBlocksMtx.Lock() + defer w.txBlocksMtx.Unlock() + w.txBlocks[txHash] = &hashEntry{ + hash: blockHash, + lastAccess: time.Now(), + } +} + +// txBlock attempts to retrieve the block hash for the tx from the cache. +func (w *spvWallet) txBlock(txHash chainhash.Hash) (chainhash.Hash, bool) { + w.txBlocksMtx.Lock() + defer w.txBlocksMtx.Unlock() + entry, found := w.txBlocks[txHash] + if !found { + return chainhash.Hash{}, false + } + entry.lastAccess = time.Now() + return entry.hash, true +} + +// cacheCheckpoint caches a *filterScanResult so that future scans can be +// skipped or shortened. +func (w *spvWallet) cacheCheckpoint(txHash *chainhash.Hash, vout uint32, res *filterScanResult) { + if res.spend != nil && res.blockHash == nil { + // Probably set the start time too late. Don't cache anything + return + } + w.checkpointMtx.Lock() + defer w.checkpointMtx.Unlock() + w.checkpoints[newOutPoint(txHash, vout)] = &scanCheckpoint{ + res: res, + lastAccess: time.Now(), + } +} + +// 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)] + if !found { + return nil + } + check.lastAccess = time.Now() + res := *check.res + return &res +} + +// 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) checkpoint(txHash *chainhash.Hash, vout uint32) *filterScanResult { + res := w.unvalidatedCheckpoint(txHash, vout) + if res == nil { + return nil + } + if !w.blockIsMainchain(&res.checkpoint, -1) { + // reorg detected, abandon the checkpoint. + w.log.Debugf("abandoning checkpoint %s because checkpoint block %q is orphaned", + newOutPoint(txHash, vout), res.checkpoint) + w.checkpointMtx.Lock() + delete(w.checkpoints, newOutPoint(txHash, vout)) + w.checkpointMtx.Unlock() + return nil + } + return res +} + +func (w *spvWallet) RawRequest(method string, params []json.RawMessage) (json.RawMessage, error) { + // Not needed for spv wallet. + return nil, errors.New("RawRequest not available on spv") +} + +func (w *spvWallet) estimateSmartFee(confTarget int64, mode *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) { + return nil, errors.New("EstimateSmartFee not available on spv") +} + +func (w *spvWallet) ownsAddress(addr btcutil.Address) (bool, error) { + return w.wallet.HaveAddress(addr) +} + +func (w *spvWallet) sendRawTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { + // Publish the transaction in a goroutine so the caller may wait for a given + // period before it goes asynchronous and it is assumed that btcwallet at + // least succeeded with its DB updates and queueing of the transaction for + // rebroadcasting. In the future, a new btcwallet method should be added + // that returns after performing its internal actions, but broadcasting + // asynchronously and sending the outcome in a channel or promise. + res := make(chan error, 1) + go func() { + tStart := time.Now() + defer close(res) + if err := w.wallet.PublishTransaction(tx, ""); err != nil { + w.log.Errorf("PublishTransaction(%v) failure: %v", tx.TxHash(), err) + res <- err + return + } + defer w.log.Tracef("PublishTransaction(%v) completed in %v", tx.TxHash(), + time.Since(tStart)) // after outpoint unlocking and signalling + res <- nil + }() + + select { + case err := <-res: + if err != nil { + return nil, err + } + case <-time.After(defaultBroadcastWait): + w.log.Debugf("No error from PublishTransaction after %v for txn %v. "+ + "Assuming wallet accepted it.", defaultBroadcastWait, tx.TxHash()) + } + + // bitcoind would unlock these, btcwallet does not. Although it seems like + // they are no longer returned from ListUnspent after publishing, it must + // not be returned by LockedOutpoints (listlockunspent) for the lockedSats + // computations to be correct. + for _, txIn := range tx.TxIn { + w.wallet.UnlockOutpoint(txIn.PreviousOutPoint) + } + + txHash := tx.TxHash() // down here in case... the msgTx was mutated? + return &txHash, nil +} + +func (w *spvWallet) getBlock(blockHash chainhash.Hash) (*wire.MsgBlock, error) { + block, err := w.cl.GetBlock(blockHash) + if err != nil { + return nil, fmt.Errorf("neutrino GetBlock error: %v", err) + } + + return block.MsgBlock(), nil +} + +func (w *spvWallet) getBlockHash(blockHeight int64) (*chainhash.Hash, error) { + return w.cl.GetBlockHash(blockHeight) +} + +func (w *spvWallet) getBlockHeight(h *chainhash.Hash) (int32, error) { + return w.cl.GetBlockHeight(h) +} + +func (w *spvWallet) getBestBlockHash() (*chainhash.Hash, error) { + blk := w.wallet.SyncedTo() + return &blk.Hash, nil +} + +// getBestBlockHeight returns the height of the best block processed by the +// wallet, which indicates the height at which the compact filters have been +// retrieved and scanned for wallet addresses. This is may be less than +// getChainHeight, which indicates the height that the chain service has reached +// in its retrieval of block headers and compact filter headers. +func (w *spvWallet) getBestBlockHeight() (int32, error) { + return w.wallet.SyncedTo().Height, nil +} + +// getChainStamp satisfies chainStamper for manual median time calculations. +func (w *spvWallet) getChainStamp(blockHash *chainhash.Hash) (stamp time.Time, prevHash *chainhash.Hash, err error) { + hdr, err := w.cl.GetBlockHeader(blockHash) + if err != nil { + return + } + return hdr.Timestamp, &hdr.PrevBlock, nil +} + +// medianTime is the median time for the current best block. +func (w *spvWallet) medianTime() (time.Time, error) { + blk := w.wallet.SyncedTo() + return calcMedianTime(w, &blk.Hash) +} + +// getChainHeight is only for confirmations since it does not reflect the wallet +// manager's sync height, just the chain service. +func (w *spvWallet) getChainHeight() (int32, error) { + blk, err := w.cl.BestBlock() + if err != nil { + return -1, err + } + return blk.Height, err +} + +func (w *spvWallet) peerCount() (uint32, error) { + return uint32(len(w.cl.Peers())), nil +} + +// syncHeight is the best known sync height among peers. +func (w *spvWallet) syncHeight() int32 { + var maxHeight int32 + for _, p := range w.cl.Peers() { + tipHeight := p.StartingHeight() + lastBlockHeight := p.LastBlock() + if lastBlockHeight > tipHeight { + tipHeight = lastBlockHeight + } + if tipHeight > maxHeight { + maxHeight = tipHeight + } + } + return maxHeight +} + +// syncStatus is information about the wallet's sync status. +// +// The neutrino wallet has a two stage sync: +// 1. chain service fetching block headers and filter headers +// 2. wallet address manager retrieving and scanning filters +// +// We only report a single sync height, so we are going to show some progress in +// the chain service sync stage that comes before the wallet has performed any +// address recovery/rescan, and switch to the wallet's sync height when it +// reports non-zero height. +func (w *spvWallet) syncStatus() (*syncStatus, error) { + // Chain service headers (block and filter) height. + chainBlk, err := w.cl.BestBlock() + if err != nil { + return nil, err + } + target := w.syncHeight() + currentHeight := chainBlk.Height + + var synced bool + var blk *block + // Wallet address manager sync height. + if chainBlk.Timestamp.After(w.wallet.Birthday()) { + // After the wallet's birthday, the wallet address manager should begin + // syncing. Although block time stamps are not necessarily monotonically + // increasing, this is a reasonable condition at which the wallet's sync + // height should be consulted instead of the chain service's height. + walletBlock := w.wallet.SyncedTo() + if walletBlock.Height == 0 { + // The wallet is about to start its sync, so just return the last + // chain service height prior to wallet birthday until it begins. + return &syncStatus{ + Target: target, + Height: atomic.LoadInt32(&w.lastPrenatalHeight), + Syncing: true, + }, nil + } + blk = &block{ + height: int64(walletBlock.Height), + hash: walletBlock.Hash, + } + currentHeight = walletBlock.Height + synced = currentHeight >= target // maybe && w.wallet.ChainSynced() + } else { + // Chain service still syncing. + blk = &block{ + height: int64(currentHeight), + hash: chainBlk.Hash, + } + atomic.StoreInt32(&w.lastPrenatalHeight, currentHeight) + } + + if target > 0 && atomic.SwapInt32(&w.syncTarget, target) == 0 { + w.tipChan <- blk + } + + return &syncStatus{ + Target: target, + Height: int32(blk.height), + Syncing: !synced, + }, nil +} + +// ownsInputs determines if we own the inputs of the tx. +func (w *spvWallet) ownsInputs(txid string) bool { + txHash, err := chainhash.NewHashFromStr(txid) + if err != nil { + w.log.Warnf("Error decoding txid %q: %v", txid, err) + return false + } + txDetails, err := w.wallet.WalletTransaction(txHash) + if err != nil { + w.log.Warnf("walletTransaction(%v) error: %v", txid, err) + return false + } + + for _, txIn := range txDetails.MsgTx.TxIn { + _, _, _, _, err = w.wallet.FetchInputInfo(&txIn.PreviousOutPoint) + if err != nil { + if !errors.Is(err, wallet.ErrNotMine) { + w.log.Warnf("FetchInputInfo error: %v", err) + } + return false + } + } + return true +} + +// balances retrieves a wallet's balance details. +func (w *spvWallet) balances() (*GetBalancesResult, error) { + // Determine trusted vs untrusted coins with listunspent. + unspents, err := w.wallet.ListUnspent(0, math.MaxInt32, w.acctName) + if err != nil { + return nil, fmt.Errorf("error listing unspent outputs: %w", err) + } + var trusted, untrusted btcutil.Amount + for _, txout := range unspents { + if txout.Confirmations > 0 || w.ownsInputs(txout.TxID) { + trusted += btcutil.Amount(toSatoshi(txout.Amount)) + continue + } + untrusted += btcutil.Amount(toSatoshi(txout.Amount)) + } + + // listunspent does not include immature coinbase outputs or locked outputs. + bals, err := w.wallet.CalculateAccountBalances(w.acctNum, 0 /* confs */) + if err != nil { + return nil, err + } + w.log.Tracef("Bals: spendable = %v (%v trusted, %v untrusted, %v assumed locked), immature = %v", + bals.Spendable, trusted, untrusted, bals.Spendable-trusted-untrusted, bals.ImmatureReward) + // Locked outputs would be in wallet.Balances.Spendable. Assume they would + // be considered trusted and add them back in. + if all := trusted + untrusted; bals.Spendable > all { + trusted += bals.Spendable - all + } + + return &GetBalancesResult{ + Mine: Balances{ + Trusted: trusted.ToBTC(), + Untrusted: untrusted.ToBTC(), + Immature: bals.ImmatureReward.ToBTC(), + }, + }, nil +} + +// listUnspent retrieves list of the wallet's UTXOs. +func (w *spvWallet) listUnspent() ([]*ListUnspentResult, error) { + unspents, err := w.wallet.ListUnspent(0, math.MaxInt32, w.acctName) + if err != nil { + return nil, err + } + res := make([]*ListUnspentResult, 0, len(unspents)) + for _, utxo := range unspents { + // If the utxo is unconfirmed, we should determine whether it's "safe" + // by seeing if we control the inputs of its transaction. + safe := utxo.Confirmations > 0 || w.ownsInputs(utxo.TxID) + + // These hex decodings are unlikely to fail because they come directly + // from the listunspent result. Regardless, they should not result in an + // error for the caller as we can return the valid utxos. + pkScript, err := hex.DecodeString(utxo.ScriptPubKey) + if err != nil { + w.log.Warnf("ScriptPubKey decode failure: %v", err) + continue + } + + redeemScript, err := hex.DecodeString(utxo.RedeemScript) + if err != nil { + w.log.Warnf("ScriptPubKey decode failure: %v", err) + continue + } + + res = append(res, &ListUnspentResult{ + TxID: utxo.TxID, + Vout: utxo.Vout, + Address: utxo.Address, + // Label: , + ScriptPubKey: pkScript, + Amount: utxo.Amount, + Confirmations: uint32(utxo.Confirmations), + RedeemScript: redeemScript, + Spendable: utxo.Spendable, + // Solvable: , + SafePtr: &safe, + }) + } + return res, nil +} + +// lockUnspent locks and unlocks outputs for spending. An output that is part of +// an order, but not yet spent, should be locked until spent or until the order +// is canceled or fails. +func (w *spvWallet) lockUnspent(unlock bool, ops []*output) error { + switch { + case unlock && len(ops) == 0: + w.wallet.ResetLockedOutpoints() + default: + for _, op := range ops { + op := wire.OutPoint{Hash: op.pt.txHash, Index: op.pt.vout} + if unlock { + w.wallet.UnlockOutpoint(op) + } else { + w.wallet.LockOutpoint(op) + } + } + } + return nil +} + +// listLockUnspent returns a slice of outpoints for all unspent outputs marked +// as locked by a wallet. +func (w *spvWallet) listLockUnspent() ([]*RPCOutpoint, error) { + outpoints := w.wallet.LockedOutpoints() + pts := make([]*RPCOutpoint, 0, len(outpoints)) + for _, pt := range outpoints { + pts = append(pts, &RPCOutpoint{ + TxID: pt.Txid, + Vout: pt.Vout, + }) + } + return pts, nil +} + +// changeAddress gets a new internal address from the wallet. The address will +// be bech32-encoded (P2WPKH). +func (w *spvWallet) changeAddress() (btcutil.Address, error) { + return w.wallet.NewChangeAddress(w.acctNum, waddrmgr.KeyScopeBIP0084) +} + +// externalAddress gets a new bech32-encoded (P2WPKH) external address from the +// wallet. +func (w *spvWallet) externalAddress() (btcutil.Address, error) { + return w.wallet.NewAddress(w.acctNum, waddrmgr.KeyScopeBIP0084) +} + +func (w *spvWallet) refundAddress() (btcutil.Address, error) { + return w.externalAddress() +} + +// signTx attempts to have the wallet sign the transaction inputs. +func (w *spvWallet) signTx(tx *wire.MsgTx) (*wire.MsgTx, error) { + // Can't use btcwallet.Wallet.SignTransaction, because it doesn't work for + // segwit transactions (for real?). + return tx, w.wallet.SignTx(tx) +} + +// privKeyForAddress retrieves the private key associated with the specified +// address. +func (w *spvWallet) privKeyForAddress(addr string) (*btcec.PrivateKey, error) { + a, err := w.decodeAddr(addr, w.chainParams) + if err != nil { + return nil, err + } + return w.wallet.PrivKeyForAddress(a) +} + +// Unlock unlocks the wallet. +func (w *spvWallet) Unlock(pw []byte) error { + return w.wallet.Unlock(pw, nil) +} + +// Lock locks the wallet. +func (w *spvWallet) Lock() error { + w.wallet.Lock() + return nil +} + +// sendToAddress sends the amount to the address. feeRate is in units of +// sats/byte. +func (w *spvWallet) sendToAddress(address string, value, feeRate uint64, subtract bool) (*chainhash.Hash, error) { + addr, err := w.decodeAddr(address, w.chainParams) + if err != nil { + return nil, err + } + + pkScript, err := txscript.PayToAddrScript(addr) + if err != nil { + return nil, err + } + + if subtract { + return w.sendWithSubtract(pkScript, value, feeRate) + } + + wireOP := wire.NewTxOut(int64(value), pkScript) + if dexbtc.IsDust(wireOP, feeRate) { + return nil, errors.New("output value is dust") + } + + // converting sats/vB -> sats/kvB + feeRateAmt := btcutil.Amount(feeRate * 1e3) + tx, err := w.wallet.SendOutputs([]*wire.TxOut{wireOP}, nil, w.acctNum, 0, + feeRateAmt, wallet.CoinSelectionLargest, "") + if err != nil { + return nil, err + } + + txHash := tx.TxHash() + + return &txHash, nil +} + +func (w *spvWallet) sendWithSubtract(pkScript []byte, value, feeRate uint64) (*chainhash.Hash, error) { + txOutSize := dexbtc.TxOutOverhead + uint64(len(pkScript)) // send-to address + var unfundedTxSize uint64 = dexbtc.MinimumTxOverhead + dexbtc.P2WPKHOutputSize /* change */ + txOutSize + + unspents, err := w.listUnspent() + if err != nil { + return nil, fmt.Errorf("error listing unspent outputs: %w", err) + } + + utxos, _, _, err := convertUnspent(0, unspents, w.chainParams) + if err != nil { + return nil, fmt.Errorf("error converting unspent outputs: %w", err) + } + + // With sendWithSubtract, fees are subtracted from the sent amount, so we + // target an input sum, not an output value. Makes the math easy. + enough := func(_, inputsVal uint64) bool { + return inputsVal >= value + } + + sum, inputsSize, _, fundingCoins, _, _, err := fund(utxos, enough) + if err != nil { + return nil, fmt.Errorf("error funding sendWithSubtract value of %s: %w", amount(value), err) + } + + fees := (unfundedTxSize + uint64(inputsSize)) * feeRate + send := value - fees + extra := sum - send + + switch { + case fees > sum: + return nil, fmt.Errorf("fees > sum") + case fees > value: + return nil, fmt.Errorf("fees > value") + case send > sum: + return nil, fmt.Errorf("send > sum") + } + + tx := wire.NewMsgTx(wire.TxVersion) + for op := range fundingCoins { + wireOP := wire.NewOutPoint(&op.txHash, op.vout) + txIn := wire.NewTxIn(wireOP, []byte{}, nil) + tx.AddTxIn(txIn) + } + + change := extra - fees + changeAddr, err := w.changeAddress() + if err != nil { + return nil, fmt.Errorf("error retrieving change address: %w", err) + } + + changeScript, err := txscript.PayToAddrScript(changeAddr) + if err != nil { + return nil, fmt.Errorf("error generating pubkey script: %w", err) + } + + changeOut := wire.NewTxOut(int64(change), changeScript) + + // One last check for dust. + if dexbtc.IsDust(changeOut, feeRate) { + // Re-calculate fees and change + fees = (unfundedTxSize - dexbtc.P2WPKHOutputSize + uint64(inputsSize)) * feeRate + send = sum - fees + } else { + tx.AddTxOut(changeOut) + } + + wireOP := wire.NewTxOut(int64(send), pkScript) + if dexbtc.IsDust(wireOP, feeRate) { + return nil, errors.New("output value is dust") + } + tx.AddTxOut(wireOP) + + if err := w.wallet.SignTx(tx); err != nil { + return nil, fmt.Errorf("signing error: %w", err) + } + + return w.sendRawTransaction(tx) +} + +// 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. +func (w *spvWallet) swapConfirmations(txHash *chainhash.Hash, vout uint32, pkScript []byte, + startTime time.Time) (confs uint32, spent bool, err error) { + + // First, check if it's a wallet transaction. We probably won't be able + // to see the spend status, since the wallet doesn't track the swap contract + // output, but we can get the block if it's been mined. + blockHash, confs, spent, err := w.confirmations(txHash, vout) + if err == nil { + return confs, spent, nil + } + var assumedMempool bool + switch err { + case WalletTransactionNotFound: + w.log.Tracef("swapConfirmations - WalletTransactionNotFound: %v:%d", txHash, vout) + case SpentStatusUnknown: + w.log.Tracef("swapConfirmations - SpentStatusUnknown: %v:%d (block %v, confs %d)", + txHash, vout, blockHash, confs) + if blockHash == nil { + // We generated this swap, but it probably hasn't been mined yet. + // It's SpentStatusUnknown because the wallet doesn't track the + // spend status of the swap contract output itself, since it's not + // recognized as a wallet output. We'll still try to find the + // confirmations with other means, but if we can't find it, we'll + // report it as a zero-conf unspent output. This ignores the remote + // possibility that the output could be both in mempool and spent. + assumedMempool = true + } + default: + return 0, false, err + } + + // If we still don't have the block hash, we may have it stored. Check the + // dex database first. This won't give us the confirmations and spent + // status, but it will allow us to short circuit a longer scan if we already + // know the output is spent. + if blockHash == nil { + blockHash, _ = w.mainchainBlockForStoredTx(txHash) + } + + // Our last option is neutrino. + w.log.Tracef("swapConfirmations - scanFilters: %v:%d (block %v, start time %v)", + txHash, vout, blockHash, startTime) + utxo, err := w.scanFilters(txHash, vout, pkScript, startTime, blockHash) + if err != nil { + return 0, false, err + } + + if utxo.spend == nil && utxo.blockHash == nil { + if assumedMempool { + w.log.Tracef("swapConfirmations - scanFilters did not find %v:%d, assuming in mempool.", + txHash, vout) + // NOT asset.CoinNotFoundError since this is normal for mempool + // transactions with an SPV wallet. + return 0, false, nil + } + return 0, false, fmt.Errorf("output %s:%v not found with search parameters startTime = %s, pkScript = %x", + txHash, vout, startTime, pkScript) + } + + if utxo.blockHash != nil { + bestHeight, err := w.getChainHeight() + if err != nil { + return 0, false, fmt.Errorf("getBestBlockHeight error: %v", err) + } + confs = uint32(bestHeight) - utxo.blockHeight + 1 + } + + if utxo.spend != nil { + // In the off-chance that a spend was found but not the output itself, + // confs will be incorrect here. + // In situations where we're looking for the counter-party's swap, we + // revoke if it's found to be spent, without inspecting the confs, so + // accuracy of confs is not significant. When it's our output, we'll + // know the block and won't end up here. (even if we did, we just end up + // sending out some inaccurate Data-severity notifications to the UI + // until the match progresses) + return confs, true, nil + } + + // unspent + return confs, false, nil +} + +func (w *spvWallet) locked() bool { + return w.wallet.Locked() +} + +func (w *spvWallet) walletLock() error { + w.wallet.Lock() + return nil +} + +func (w *spvWallet) walletUnlock(pw []byte) error { + return w.Unlock(pw) +} + +func (w *spvWallet) getBlockHeader(blockHash *chainhash.Hash) (*blockHeader, error) { + hdr, err := w.cl.GetBlockHeader(blockHash) + if err != nil { + return nil, err + } + + tip, err := w.cl.BestBlock() + if err != nil { + return nil, fmt.Errorf("BestBlock error: %v", err) + } + + blockHeight, err := w.cl.GetBlockHeight(blockHash) + if err != nil { + return nil, err + } + + return &blockHeader{ + Hash: hdr.BlockHash().String(), + Confirmations: int64(confirms(blockHeight, tip.Height)), + Height: int64(blockHeight), + Time: hdr.Timestamp.Unix(), + }, nil +} + +func (w *spvWallet) getBestBlockHeader() (*blockHeader, error) { + hash, err := w.getBestBlockHash() + if err != nil { + return nil, err + } + return w.getBlockHeader(hash) +} + +func (w *spvWallet) logFilePath() string { + return filepath.Join(w.dir, logDirName, logFileName) +} + +// connect will start the wallet and begin syncing. +func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) (err error) { + w.wallet = w.newBTCWallet(w.dir, w.cfg, w.chainParams, w.log) + w.cl, err = w.wallet.Start() + if err != nil { + return err + } + + blockNotes := w.wallet.BlockNotifications(ctx) + + // Nanny for the caches checkpoints and txBlocks caches. + wg.Add(1) + go func() { + defer wg.Done() + defer w.wallet.Stop() + + ticker := time.NewTicker(time.Minute * 20) + defer ticker.Stop() + expiration := time.Hour * 2 + for { + select { + case <-ticker.C: + w.txBlocksMtx.Lock() + for txHash, entry := range w.txBlocks { + if time.Since(entry.lastAccess) > expiration { + delete(w.txBlocks, txHash) + } + } + w.txBlocksMtx.Unlock() + + w.checkpointMtx.Lock() + for outPt, check := range w.checkpoints { + if time.Since(check.lastAccess) > expiration { + delete(w.checkpoints, outPt) + } + } + w.checkpointMtx.Unlock() + + case blk := <-blockNotes: + syncTarget := atomic.LoadInt32(&w.syncTarget) + if syncTarget == 0 || (blk.Height < syncTarget && blk.Height%10_000 != 0) { + continue + } + + select { + case w.tipChan <- &block{ + hash: blk.Hash, + height: int64(blk.Height), + }: + default: + w.log.Warnf("tip report channel was blocking") + } + + case <-ctx.Done(): + return + } + } + }() + + return nil +} + +// estimateSendTxFee callers should provide at least one output value. +func (w *spvWallet) estimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract bool) (fee uint64, err error) { + minTxSize := uint64(tx.SerializeSize()) + var sendAmount uint64 + for _, txOut := range tx.TxOut { + sendAmount += uint64(txOut.Value) + } + + // If subtract is true, select enough inputs for sendAmount. Fees will be taken + // from the sendAmount. If not, select enough inputs to cover minimum fees. + enough := func(inputsSize, sum uint64) bool { + if subtract { + return sum >= sendAmount + } + minFee := (minTxSize + inputsSize) * feeRate + return sum >= sendAmount+minFee + } + + unspents, err := w.listUnspent() + if err != nil { + return 0, fmt.Errorf("error listing unspent outputs: %w", err) + } + + utxos, _, _, err := convertUnspent(0, unspents, w.chainParams) + if err != nil { + return 0, fmt.Errorf("error converting unspent outputs: %w", err) + } + + sum, inputsSize, _, _, _, _, err := fund(utxos, enough) + if err != nil { + return 0, err + } + + txSize := minTxSize + uint64(inputsSize) + estFee := feeRate * txSize + remaining := sum - sendAmount + + // Check if there will be a change output if there is enough remaining. + estFeeWithChange := (txSize + dexbtc.P2WPKHOutputSize) * feeRate + var changeValue uint64 + if remaining > estFeeWithChange { + changeValue = remaining - estFeeWithChange + } + + if subtract { + // fees are already included in sendAmount, anything else is change. + changeValue = remaining + } + + var finalFee uint64 + if dexbtc.IsDustVal(dexbtc.P2WPKHOutputSize, changeValue, feeRate, true) { + // remaining cannot cover a non-dust change and the fee for the change. + finalFee = estFee + remaining + } else { + // additional fee will be paid for non-dust change + finalFee = estFeeWithChange + } + + if subtract { + sendAmount -= finalFee + } + if dexbtc.IsDustVal(minTxSize, sendAmount, feeRate, true) { + return 0, errors.New("output value is dust") + } + + return finalFee, nil +} + +// moveWalletData will move all wallet files to a backup directory. +func (w *spvWallet) moveWalletData(backupDir string) error { + timeString := time.Now().Format("2006-01-02T15:04:05") + err := os.MkdirAll(backupDir, 0744) + if err != nil { + return err + } + backupFolder := filepath.Join(backupDir, timeString) + return os.Rename(w.dir, backupFolder) +} + +// numDerivedAddresses returns the number of internal and external addresses +// that the wallet has derived. +func (w *spvWallet) numDerivedAddresses() (internal, external uint32, err error) { + props, err := w.wallet.AccountProperties(waddrmgr.KeyScopeBIP0084, w.acctNum) + if err != nil { + return 0, 0, err + } + + return props.InternalKeyCount, props.ExternalKeyCount, nil +} + +// blockForStoredTx looks for a block hash in the txBlocks index. +func (w *spvWallet) blockForStoredTx(txHash *chainhash.Hash) (*chainhash.Hash, int32, error) { + // Check if we know the block hash for the tx. + blockHash, found := w.txBlock(*txHash) + if !found { + return nil, 0, nil + } + // Check that the block is still mainchain. + blockHeight, err := w.cl.GetBlockHeight(&blockHash) + if err != nil { + w.log.Errorf("Error retrieving block height for hash %s: %v", blockHash, err) + return nil, 0, err + } + return &blockHash, blockHeight, nil +} + +// blockIsMainchain will be true if the blockHash is that of a mainchain block. +func (w *spvWallet) blockIsMainchain(blockHash *chainhash.Hash, blockHeight int32) bool { + if blockHeight < 0 { + var err error + blockHeight, err = w.cl.GetBlockHeight(blockHash) + if err != nil { + w.log.Errorf("Error getting block height for hash %s", blockHash) + return false + } + } + checkHash, err := w.cl.GetBlockHash(int64(blockHeight)) + if err != nil { + w.log.Errorf("Error retrieving block hash for height %d", blockHeight) + return false + } + + return *checkHash == *blockHash +} + +// mainchainBlockForStoredTx gets the block hash and height for the transaction +// 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) + if err != nil { + w.log.Errorf("Error retrieving mainchain block height for hash %s", blockHash) + return nil, 0 + } + if blockHash == nil { + return nil, 0 + } + if !w.blockIsMainchain(blockHash, blockHeight) { + return nil, 0 + } + return blockHash, blockHeight +} + +// findBlockForTime locates a good start block so that a search beginning at the +// 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 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(-maxFutureBlockTime) + + bestHeight, err := w.getChainHeight() + if err != nil { + return nil, 0, fmt.Errorf("getChainHeight error: %v", err) + } + + getBlockTimeForHeight := func(height int32) (*chainhash.Hash, time.Time, error) { + hash, err := w.cl.GetBlockHash(int64(height)) + if err != nil { + return nil, time.Time{}, err + } + header, err := w.cl.GetBlockHeader(hash) + if err != nil { + return nil, time.Time{}, err + } + return hash, header.Timestamp, nil + } + + iHeight := sort.Search(int(bestHeight), func(h int) bool { + var iTime time.Time + _, iTime, err = getBlockTimeForHeight(int32(h)) + if err != nil { + return true + } + return iTime.After(offsetTime) + }) + if err != nil { + return nil, 0, fmt.Errorf("binary search error finding best block for time %q: %w", matchTime, err) + } + + // 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 > medianTimeBlocks blocks with inverted time order. + var count int + var iHash *chainhash.Hash + var iTime time.Time + for iHeight > 0 { + iHash, iTime, err = getBlockTimeForHeight(int32(iHeight)) + if err != nil { + return nil, 0, fmt.Errorf("getBlockTimeForHeight error: %w", err) + } + if iTime.Before(offsetTime) { + count++ + if count == medianTimeBlocks { + return iHash, int32(iHeight), nil + } + } else { + count = 0 + } + iHeight-- + } + return w.chainParams.GenesisHash, 0, nil + +} + +// scanFilters enables searching for an output and its spending input by +// scanning BIP158 compact filters. Caller should supply either blockHash or +// startTime. blockHash takes precedence. If blockHash is supplied, the scan +// will start at that block and continue to the current blockchain tip, or until +// both the output and a spending transaction is found. if startTime is +// 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 := 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(&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 + _, limitHeight, err = w.findBlockForTime(startTime) + if err != nil { + return nil, err + } + } + } else { + // No checkpoint, but user supplied a block hash. + var err error + limitHeight, err = w.getBlockHeight(blockHash) + if err != nil { + return nil, fmt.Errorf("error getting height for supplied block hash %s", blockHash) + } + } + + w.log.Debugf("Performing cfilters scan for %v:%d from height %d", txHash, vout, limitHeight) + + // Do a filter scan. + utxo, err := w.filterScanFromHeight(*txHash, vout, pkScript, limitHeight, checkPt) + if err != nil { + return nil, fmt.Errorf("filterScanFromHeight error: %w", err) + } + if utxo == nil { + return nil, asset.CoinNotFoundError + } + + // If we found a block, let's store a reference in our local database so we + // can maybe bypass a long search next time. + if utxo.blockHash != nil { + w.log.Debugf("cfilters scan SUCCEEDED for %v:%d. block hash: %v, spent: %v", + txHash, vout, utxo.blockHash, utxo.spend != nil) + w.storeTxBlock(*txHash, *utxo.blockHash) + } + + w.cacheCheckpoint(txHash, vout, utxo) + + return utxo, nil +} + +// getTxOut finds an unspent transaction output and its number of confirmations. +// To match the behavior of the RPC method, even if an output is found, if it's +// known to be spent, no *wire.TxOut and no error will be returned. +func (w *spvWallet) getTxOut(txHash *chainhash.Hash, vout uint32, pkScript []byte, startTime time.Time) (*wire.TxOut, uint32, error) { + // Check for a wallet transaction first + txDetails, err := w.wallet.WalletTransaction(txHash) + var blockHash *chainhash.Hash + if err != nil && !errors.Is(err, WalletTransactionNotFound) { + return nil, 0, fmt.Errorf("walletTransaction error: %w", err) + } + + if txDetails != nil { + spent, found := outputSpendStatus(txDetails, vout) + if found { + if spent { + return nil, 0, nil + } + if len(txDetails.MsgTx.TxOut) <= int(vout) { + return nil, 0, fmt.Errorf("wallet transaction %s doesn't have enough outputs for vout %d", txHash, vout) + } + + var confs uint32 + if txDetails.Block.Height > 0 { + tip, err := w.cl.BestBlock() + if err != nil { + return nil, 0, fmt.Errorf("BestBlock error: %v", err) + } + confs = uint32(confirms(txDetails.Block.Height, tip.Height)) + } + + msgTx := &txDetails.MsgTx + if len(msgTx.TxOut) <= int(vout) { + return nil, 0, fmt.Errorf("wallet transaction %s found, but not enough outputs for vout %d", txHash, vout) + } + return msgTx.TxOut[vout], confs, nil + + } + if txDetails.Block.Hash != (chainhash.Hash{}) { + blockHash = &txDetails.Block.Hash + } + } + + // We don't really know if it's spent, so we'll need to scan. + utxo, err := w.scanFilters(txHash, vout, pkScript, startTime, blockHash) + if err != nil { + return nil, 0, err + } + + if utxo == nil || utxo.spend != nil || utxo.blockHash == nil { + return nil, 0, nil + } + + tip, err := w.cl.BestBlock() + if err != nil { + return nil, 0, fmt.Errorf("BestBlock error: %v", err) + } + + confs := uint32(confirms(int32(utxo.blockHeight), tip.Height)) + + return utxo.txOut, confs, nil +} + +// filterScanFromHeight scans BIP158 filters beginning at the specified block +// height until the tip, or until a spending transaction is found. +func (w *spvWallet) filterScanFromHeight(txHash chainhash.Hash, vout uint32, pkScript []byte, startBlockHeight int32, checkPt *filterScanResult) (*filterScanResult, error) { + walletBlock := w.wallet.SyncedTo() // where cfilters are received and processed + tip := walletBlock.Height + + res := checkPt + if res == nil { + res = new(filterScanResult) + } + +search: + for height := startBlockHeight; height <= tip; height++ { + if res.spend != nil && res.blockHash == nil { + w.log.Warnf("A spending input (%s) was found during the scan but the output (%s) "+ + "itself wasn't found. Was the startBlockHeight early enough?", + newOutPoint(&res.spend.txHash, res.spend.vin), + newOutPoint(&txHash, vout), + ) + return res, nil + } + blockHash, err := w.getBlockHash(int64(height)) + if err != nil { + return nil, fmt.Errorf("error getting block hash for height %d: %w", height, err) + } + matched, err := w.matchPkScript(blockHash, [][]byte{pkScript}) + if err != nil { + return nil, fmt.Errorf("matchPkScript error: %w", err) + } + + res.checkpoint = *blockHash + if !matched { + continue search + } + // Pull the block. + w.log.Tracef("Block %v matched pkScript for output %v:%d. Pulling the block...", + blockHash, txHash, vout) + block, err := w.cl.GetBlock(*blockHash) + if err != nil { + return nil, fmt.Errorf("GetBlock error: %v", err) + } + msgBlock := block.MsgBlock() + + // Scan every transaction. + nextTx: + for _, tx := range msgBlock.Transactions { + // Look for a spending input. + if res.spend == nil { + for vin, txIn := range tx.TxIn { + prevOut := &txIn.PreviousOutPoint + if prevOut.Hash == txHash && prevOut.Index == vout { + res.spend = &spendingInput{ + txHash: tx.TxHash(), + vin: uint32(vin), + blockHash: *blockHash, + blockHeight: uint32(height), + } + w.log.Tracef("Found txn %v spending %v in block %v (%d)", res.spend.txHash, + txHash, res.spend.blockHash, res.spend.blockHeight) + if res.blockHash != nil { + break search + } + // The output could still be in this block, just not + // in this transaction. + continue nextTx + } + } + } + // Only check for the output if this is the right transaction. + if res.blockHash != nil || tx.TxHash() != txHash { + continue nextTx + } + for _, txOut := range tx.TxOut { + if bytes.Equal(txOut.PkScript, pkScript) { + res.blockHash = blockHash + res.blockHeight = uint32(height) + res.txOut = txOut + w.log.Tracef("Found txn %v in block %v (%d)", txHash, res.blockHash, height) + if res.spend != nil { + break search + } + // Keep looking for the spending transaction. + continue nextTx + } + } + } + } + return res, nil +} + +// matchPkScript pulls the filter for the block and attempts to match the +// supplied scripts. +func (w *spvWallet) matchPkScript(blockHash *chainhash.Hash, scripts [][]byte) (bool, error) { + filter, err := w.cl.GetCFilter(*blockHash, wire.GCSFilterRegular) + if err != nil { + return false, fmt.Errorf("GetCFilter error: %w", err) + } + + if filter.N() == 0 { + return false, fmt.Errorf("unexpected empty filter for %s", blockHash) + } + + var filterKey [gcs.KeySize]byte + copy(filterKey[:], blockHash[:gcs.KeySize]) + + matchFound, err := filter.MatchAny(filterKey, scripts) + if err != nil { + return false, fmt.Errorf("MatchAny error: %w", err) + } + return matchFound, nil +} + +// searchBlockForRedemptions attempts to find spending info for the specified +// contracts by searching every input of all txs in the provided block range. +func (w *spvWallet) searchBlockForRedemptions(ctx context.Context, reqs map[outPoint]*findRedemptionReq, + blockHash chainhash.Hash) (discovered map[outPoint]*findRedemptionResult) { + + // Just match all the scripts together. + scripts := make([][]byte, 0, len(reqs)) + for _, req := range reqs { + scripts = append(scripts, req.pkScript) + } + + discovered = make(map[outPoint]*findRedemptionResult, len(reqs)) + + matchFound, err := w.matchPkScript(&blockHash, scripts) + if err != nil { + w.log.Errorf("matchPkScript error: %v", err) + return + } + + if !matchFound { + return + } + + // There is at least one match. Pull the block. + block, err := w.cl.GetBlock(blockHash) + if err != nil { + w.log.Errorf("neutrino GetBlock error: %v", err) + return + } + + for _, msgTx := range block.MsgBlock().Transactions { + newlyDiscovered := findRedemptionsInTx(ctx, true, reqs, msgTx, w.chainParams) + for outPt, res := range newlyDiscovered { + discovered[outPt] = res + } + } + return +} + +// findRedemptionsInMempool is unsupported for SPV. +func (w *spvWallet) findRedemptionsInMempool(ctx context.Context, reqs map[outPoint]*findRedemptionReq) (discovered map[outPoint]*findRedemptionResult) { + return +} + +// confirmations looks for the confirmation count and spend status on a +// transaction output that pays to this wallet. +func (w *spvWallet) confirmations(txHash *chainhash.Hash, vout uint32) (blockHash *chainhash.Hash, confs uint32, spent bool, err error) { + details, err := w.wallet.WalletTransaction(txHash) + if err != nil { + return nil, 0, false, err + } + + if details.Block.Hash != (chainhash.Hash{}) { + blockHash = &details.Block.Hash + height, err := w.getChainHeight() + if err != nil { + return nil, 0, false, err + } + confs = uint32(confirms(details.Block.Height, height)) + } + + spent, found := outputSpendStatus(details, vout) + if found { + return blockHash, confs, spent, nil + } + + return blockHash, confs, false, SpentStatusUnknown +} + +// getWalletTransaction checks the wallet database for the specified +// transaction. Only transactions with output scripts that pay to the wallet or +// transactions that spend wallet outputs are stored in the wallet database. +// This is pretty much copy-paste from btcwallet 'gettransaction' JSON-RPC +// handler. +func (w *spvWallet) getWalletTransaction(txHash *chainhash.Hash) (*GetTransactionResult, error) { + // Option # 1 just copies from UnstableAPI.TxDetails. Duplicating the + // unexported bucket key feels dirty. + // + // var details *wtxmgr.TxDetails + // err := walletdb.View(w.Database(), func(dbtx walletdb.ReadTx) error { + // txKey := []byte("wtxmgr") + // txmgrNs := dbtx.ReadBucket(txKey) + // var err error + // details, err = w.TxStore.TxDetails(txmgrNs, txHash) + // return err + // }) + + // Option #2 + // This is what the JSON-RPC does (and has since at least May 2018). + details, err := w.wallet.WalletTransaction(txHash) + if err != nil { + if errors.Is(err, WalletTransactionNotFound) { + return nil, asset.CoinNotFoundError // for the asset.Wallet interface + } + return nil, err + } + + syncBlock := w.wallet.SyncedTo() + + // TODO: The serialized transaction is already in the DB, so reserializing + // might be avoided here. According to btcwallet, details.SerializedTx is + // "optional" (?), but we might check for it. + txRaw, err := serializeMsgTx(&details.MsgTx) + if err != nil { + return nil, err + } + + ret := &GetTransactionResult{ + TxID: txHash.String(), + Hex: txRaw, // 'Hex' field name is a lie, kinda + Time: uint64(details.Received.Unix()), + TimeReceived: uint64(details.Received.Unix()), + } + + if details.Block.Height != -1 { + ret.BlockHash = details.Block.Hash.String() + ret.BlockTime = uint64(details.Block.Time.Unix()) + // ret.BlockHeight = uint64(details.Block.Height) + ret.Confirmations = uint64(confirms(details.Block.Height, syncBlock.Height)) + } + + return ret, nil + + /* + var debitTotal, creditTotal btcutil.Amount // credits excludes change + for _, deb := range details.Debits { + debitTotal += deb.Amount + } + for _, cred := range details.Credits { + if !cred.Change { + creditTotal += cred.Amount + } + } + + // Fee can only be determined if every input is a debit. + if len(details.Debits) == len(details.MsgTx.TxIn) { + var outputTotal btcutil.Amount + for _, output := range details.MsgTx.TxOut { + outputTotal += btcutil.Amount(output.Value) + } + ret.Fee = (debitTotal - outputTotal).ToBTC() + } + + ret.Amount = creditTotal.ToBTC() + return ret, nil + */ +} + +func confirms(txHeight, curHeight int32) int32 { + switch { + case txHeight == -1, txHeight > curHeight: + return 0 + default: + return curHeight - txHeight + 1 + } +} + +// outputSpendStatus will return the spend status of the output if it's found +// in the TxDetails.Credits. +func outputSpendStatus(details *wtxmgr.TxDetails, vout uint32) (spend, found bool) { + for _, credit := range details.Credits { + if credit.Index == vout { + return credit.Spent, true + } + } + return false, false +} diff --git a/client/asset/ltc/ltc.go b/client/asset/ltc/ltc.go index 0718063abc..c06b7f5c20 100644 --- a/client/asset/ltc/ltc.go +++ b/client/asset/ltc/ltc.go @@ -48,61 +48,6 @@ var ( Description: "The wallet name", }, } - commonOpts = []*asset.ConfigOption{ - { - Key: "rpcuser", - DisplayName: "JSON-RPC Username", - Description: "Litecoin's 'rpcuser' setting", - }, - { - Key: "rpcpassword", - DisplayName: "JSON-RPC Password", - Description: "Litecoin's 'rpcpassword' setting", - NoEcho: true, - }, - { - Key: "rpcbind", - DisplayName: "JSON-RPC Address", - Description: " or : (default 'localhost')", - DefaultValue: "127.0.0.1", - }, - { - Key: "rpcport", - DisplayName: "JSON-RPC Port", - Description: "Port for RPC connections (if not set in rpcbind)", - DefaultValue: "9332", - }, - { - Key: "fallbackfee", - DisplayName: "Fallback fee rate", - Description: "Litecoin's 'fallbackfee' rate. Units: LTC/kB", - DefaultValue: defaultFee * 1000 / 1e8, - }, - { - Key: "feeratelimit", - DisplayName: "Highest acceptable fee rate", - Description: "This is the highest network fee rate you are willing to " + - "pay on swap transactions. If feeratelimit is lower than a market's " + - "maxfeerate, you will not be able to trade on that market with this " + - "wallet. Units: LTC/kB", - DefaultValue: defaultFeeRateLimit * 1000 / 1e8, - }, - { - Key: "redeemconftarget", - DisplayName: "Redeem transaction confirmation target", - Description: "The target number of blocks for the redeem transaction to get a confirmation. Used to set the transaction's fee rate. (default: 2 blocks)", - DefaultValue: 2, - }, - { - Key: "txsplit", - DisplayName: "Pre-size funding inputs", - Description: "When placing an order, create a \"split\" transaction to fund the order without locking more of the wallet balance than " + - "necessary. Otherwise, excess funds may be reserved to fund the order until the first swap contract is broadcast " + - "during match settlement, or the order is canceled. This an extra transaction for which network mining fees are paid. " + - "Used only for standing-type orders, e.g. limit orders without immediate time-in-force.", - IsBoolean: true, - }, - } rpcWalletDefinition = &asset.WalletDefinition{ Type: walletTypeRPC, Tab: "Litecoin Core (external)", @@ -115,7 +60,7 @@ var ( Tab: "Electrum-LTC (external)", Description: "Use an external Electrum-LTC Wallet", // json: DefaultConfigPath: filepath.Join(btcutil.AppDataDir("electrum-ltc", false), "config"), // e.g. ~/.electrum-ltc/config ConfigOpts: append(rpcOpts, commonOpts...), - ConfigOpts: btc.CommonConfigOpts("LTC", false), + ConfigOpts: append(btc.ElectrumConfigOpts, btc.CommonConfigOpts("LTC", false)...), } spvWalletDefinition = &asset.WalletDefinition{ Type: walletTypeSPV, @@ -216,17 +161,13 @@ func (d *Driver) Create(params *asset.CreateWalletParams) error { // exchange wallet. func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) { var cloneParams *chaincfg.Params - var ltcParams *ltcchaincfg.Params switch network { case dex.Mainnet: cloneParams = dexltc.MainNetParams - ltcParams = <cchaincfg.MainNetParams case dex.Testnet: cloneParams = dexltc.TestNet4Params - ltcParams = <cchaincfg.TestNet4Params case dex.Regtest: cloneParams = dexltc.RegressionNetParams - ltcParams = <cchaincfg.RegressionNetParams default: return nil, fmt.Errorf("unknown network ID %v", network) } @@ -254,9 +195,7 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) case walletTypeRPC, walletTypeLegacy: return btc.BTCCloneWallet(cloneCFG) case walletTypeSPV: - return btc.OpenSPVWallet(cloneCFG, func(dir string, cfg *btc.WalletConfig, btcParams *chaincfg.Params, log dex.Logger) btc.BTCWallet { - return openSPVWallet(dir, cfg, btcParams, ltcParams, log) - }) + return btc.OpenSPVWallet(cloneCFG, openSPVWallet) case walletTypeElectrum: cloneCFG.Ports = dexbtc.NetPorts{} // no default ports return btc.ElectrumWallet(cloneCFG) @@ -264,3 +203,15 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) return nil, fmt.Errorf("unknown wallet type %q", cfg.Type) } } + +func parseChainParams(net dex.Network) (*ltcchaincfg.Params, error) { + switch net { + case dex.Mainnet: + return <cchaincfg.MainNetParams, nil + case dex.Testnet: + return <cchaincfg.TestNet4Params, nil + case dex.Regtest: + return <cchaincfg.RegressionNetParams, nil + } + return nil, fmt.Errorf("unknown network ID %v", net) +} diff --git a/client/asset/ltc/spv.go b/client/asset/ltc/spv.go index 6ac2660ca0..999df556b9 100644 --- a/client/asset/ltc/spv.go +++ b/client/asset/ltc/spv.go @@ -17,6 +17,7 @@ import ( "decred.org/dcrdex/client/asset/btc" "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/config" + dexltc "decred.org/dcrdex/dex/networks/ltc" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" @@ -67,7 +68,7 @@ var ( // exceptions, and have some critical code that needed to be duplicated (in // order to avoid interface hell). type ltcSPVWallet struct { - // This section is populated in newSPVWallet. + // This section is populated in openSPVWallet. dir string chainParams *ltcchaincfg.Params btcParams *chaincfg.Params @@ -86,9 +87,16 @@ type ltcSPVWallet struct { var _ btc.BTCWallet = (*ltcSPVWallet)(nil) // openSPVWallet creates a ltcSPVWallet, but does not Start. -func openSPVWallet(dir string, cfg *btc.WalletConfig, btcParams *chaincfg.Params, - ltcParams *ltcchaincfg.Params, log dex.Logger) btc.BTCWallet { - +func openSPVWallet(dir string, cfg *btc.WalletConfig, btcParams *chaincfg.Params, log dex.Logger) btc.BTCWallet { + var ltcParams *ltcchaincfg.Params + switch btcParams.Name { + case dexltc.MainNetParams.Name: + ltcParams = <cchaincfg.MainNetParams + case dexltc.TestNet4Params.Name: + ltcParams = <cchaincfg.TestNet4Params + case dexltc.RegressionNetParams.Name: + ltcParams = <cchaincfg.RegressionNetParams + } w := <cSPVWallet{ dir: dir, chainParams: ltcParams, @@ -105,7 +113,7 @@ func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dbDir string, netDir := filepath.Join(dbDir, net.Name, "spv") if err := logNeutrino(netDir, log); err != nil { - return fmt.Errorf("error initializing btcwallet+neutrino logging: %w", err) + return fmt.Errorf("error initializing dcrwallet+neutrino logging: %w", err) } logDir := filepath.Join(netDir, logDirName) @@ -124,8 +132,8 @@ func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dbDir string, return fmt.Errorf("CreateNewWallet error: %w", err) } - errCloser := dex.NewErrorCloser(log) - defer errCloser.Done() + errCloser := dex.NewErrorCloser() + defer errCloser.Done(log) errCloser.Add(loader.UnloadWallet) if extIdx > 0 || intIdx > 0 { @@ -170,7 +178,7 @@ func (w *ltcSPVWallet) walletParams() *ltcchaincfg.Params { // syncing. func (w *ltcSPVWallet) Start() (btc.SPVService, error) { if err := logNeutrino(w.dir, w.log); err != nil { - return nil, fmt.Errorf("error initializing btcwallet+neutrino logging: %v", err) + return nil, fmt.Errorf("error initializing dcrwallet+neutrino logging: %v", err) } // recoverWindow arguments borrowed from ltcwallet directly. w.loader = wallet.NewLoader(w.walletParams(), w.dir, true, 60*time.Second, 250) @@ -189,8 +197,8 @@ func (w *ltcSPVWallet) Start() (btc.SPVService, error) { return nil, fmt.Errorf("couldn't load wallet: %w", err) } - errCloser := dex.NewErrorCloser(w.log) - defer errCloser.Done() + errCloser := dex.NewErrorCloser() + defer errCloser.Done(w.log) errCloser.Add(w.loader.UnloadWallet) neutrinoDBPath := filepath.Join(w.dir, neutrinoDBName) @@ -380,7 +388,7 @@ func (w *ltcSPVWallet) ListUnspent(minconf, maxconf int32, acctName string) ([]* // need the TxOut, and to show ownership. func (w *ltcSPVWallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error) { - td, err := w.txDetails(convertHashToLTC(prevOut.Hash)) + td, err := w.txDetails((*ltcchainhash.Hash)(&prevOut.Hash)) if err != nil { return nil, nil, nil, 0, err } @@ -415,14 +423,14 @@ func (w *ltcSPVWallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wir func (w *ltcSPVWallet) LockOutpoint(op wire.OutPoint) { w.Wallet.LockOutpoint(ltcwire.OutPoint{ - Hash: *convertHashToLTC(op.Hash), + Hash: ltcchainhash.Hash(op.Hash), Index: op.Index, }) } func (w *ltcSPVWallet) UnlockOutpoint(op wire.OutPoint) { w.Wallet.UnlockOutpoint(ltcwire.OutPoint{ - Hash: *convertHashToLTC(op.Hash), + Hash: ltcchainhash.Hash(op.Hash), Index: op.Index, }) } @@ -634,7 +642,7 @@ func (w *ltcSPVWallet) dropTransactionHistory() error { } func (w *ltcSPVWallet) WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) { - txDetails, err := w.txDetails(convertHashToLTC(*txHash)) + txDetails, err := w.txDetails((*ltcchainhash.Hash)(txHash)) if err != nil { return nil, err } @@ -665,13 +673,13 @@ func (w *ltcSPVWallet) WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDeta return &wtxmgr.TxDetails{ TxRecord: wtxmgr.TxRecord{ MsgTx: *btcTx, - Hash: *convertHashToBTC(txDetails.TxRecord.Hash), + Hash: chainhash.Hash(txDetails.TxRecord.Hash), Received: txDetails.TxRecord.Received, SerializedTx: txDetails.TxRecord.SerializedTx, }, Block: wtxmgr.BlockMeta{ Block: wtxmgr.Block{ - Hash: *convertHashToBTC(txDetails.Block.Hash), + Hash: chainhash.Hash(txDetails.Block.Hash), Height: txDetails.Block.Height, }, Time: txDetails.Block.Time, @@ -685,7 +693,7 @@ func (w *ltcSPVWallet) SyncedTo() waddrmgr.BlockStamp { bs := w.Manager.SyncedTo() return waddrmgr.BlockStamp{ Height: bs.Height, - Hash: *convertHashToBTC(bs.Hash), + Hash: chainhash.Hash(bs.Hash), Timestamp: bs.Timestamp, } @@ -742,7 +750,7 @@ func (w *ltcSPVWallet) BlockNotifications(ctx context.Context) <-chan *btc.Block lastBlock := note.AttachedBlocks[len(note.AttachedBlocks)-1] select { case ch <- &btc.BlockNotification{ - Hash: *convertHashToBTC(*lastBlock.Hash), + Hash: chainhash.Hash(*lastBlock.Hash), Height: lastBlock.Height, }: default: @@ -820,7 +828,7 @@ func (s *spvService) GetBlockHash(height int64) (*chainhash.Hash, error) { if err != nil { return nil, err } - return convertHashToBTC(*ltcHash), nil + return (*chainhash.Hash)(ltcHash), nil } func (s *spvService) BestBlock() (*headerfs.BlockStamp, error) { @@ -830,7 +838,7 @@ func (s *spvService) BestBlock() (*headerfs.BlockStamp, error) { } return &headerfs.BlockStamp{ Height: bs.Height, - Hash: *convertHashToBTC(bs.Hash), + Hash: chainhash.Hash(bs.Hash), Timestamp: bs.Timestamp, }, nil } @@ -845,18 +853,18 @@ func (s *spvService) Peers() []btc.SPVPeer { } func (s *spvService) GetBlockHeight(h *chainhash.Hash) (int32, error) { - return s.ChainService.GetBlockHeight(convertHashToLTC(*h)) + return s.ChainService.GetBlockHeight((*ltcchainhash.Hash)(h)) } func (s *spvService) GetBlockHeader(h *chainhash.Hash) (*wire.BlockHeader, error) { - hdr, err := s.ChainService.GetBlockHeader(convertHashToLTC(*h)) + hdr, err := s.ChainService.GetBlockHeader((*ltcchainhash.Hash)(h)) if err != nil { return nil, err } return &wire.BlockHeader{ Version: hdr.Version, - PrevBlock: *convertHashToBTC(hdr.PrevBlock), - MerkleRoot: *convertHashToBTC(hdr.MerkleRoot), + PrevBlock: chainhash.Hash(hdr.PrevBlock), + MerkleRoot: chainhash.Hash(hdr.MerkleRoot), Timestamp: hdr.Timestamp, Bits: hdr.Bits, Nonce: hdr.Nonce, @@ -864,7 +872,7 @@ func (s *spvService) GetBlockHeader(h *chainhash.Hash) (*wire.BlockHeader, error } func (s *spvService) GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, _ ...btcneutrino.QueryOption) (*gcs.Filter, error) { - f, err := s.ChainService.GetCFilter(*convertHashToLTC(blockHash), ltcwire.GCSFilterRegular) + f, err := s.ChainService.GetCFilter(ltcchainhash.Hash(blockHash), ltcwire.GCSFilterRegular) if err != nil { return nil, err } @@ -878,7 +886,7 @@ func (s *spvService) GetCFilter(blockHash chainhash.Hash, filterType wire.Filter } func (s *spvService) GetBlock(blockHash chainhash.Hash, _ ...btcneutrino.QueryOption) (*btcutil.Block, error) { - blk, err := s.ChainService.GetBlock(*convertHashToLTC(blockHash)) + blk, err := s.ChainService.GetBlock(ltcchainhash.Hash(blockHash)) if err != nil { return nil, err } @@ -891,18 +899,6 @@ func (s *spvService) GetBlock(blockHash chainhash.Hash, _ ...btcneutrino.QueryOp return btcutil.NewBlockFromBytes(b) } -func convertHashToBTC(src ltcchainhash.Hash) *chainhash.Hash { - var tgt chainhash.Hash - copy(tgt[:], src[:]) - return &tgt -} - -func convertHashToLTC(src chainhash.Hash) *ltcchainhash.Hash { - var tgt ltcchainhash.Hash - copy(tgt[:], src[:]) - return &tgt -} - func convertMsgTxToBTC(tx *ltcwire.MsgTx) (*wire.MsgTx, error) { buf := new(bytes.Buffer) if err := tx.Serialize(buf); err != nil { @@ -926,23 +922,9 @@ func convertMsgTxToLTC(tx *wire.MsgTx) (*ltcwire.MsgTx, error) { return nil, err } - // 153280a7351c702ce4e8d87a878cc3d1eab41650ce0b895a3598b62c3778ad5f from peer=2 was not accepted: non-mandatory-script-verify-flag (Witness program hash mismatch) - return ltcTx, nil } -func parseChainParams(net dex.Network) (*ltcchaincfg.Params, error) { - switch net { - case dex.Mainnet: - return <cchaincfg.MainNetParams, nil - case dex.Testnet: - return <cchaincfg.TestNet4Params, nil - case dex.Regtest: - return <cchaincfg.RegressionNetParams, nil - } - return nil, fmt.Errorf("unknown network ID %v", net) -} - func extendAddresses(extIdx, intIdx uint32, ltcw *wallet.Wallet) error { scopedKeyManager, err := ltcw.Manager.FetchScopedKeyManager(ltcwaddrmgr.KeyScopeBIP0084) if err != nil { @@ -966,16 +948,6 @@ var ( logFileName = "neutrino.log" ) -// 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) -} - // logRotator initializes a rotating file logger. func logRotator(netDir string) (*rotator.Rotator, error) { const maxLogRolls = 8 @@ -1006,7 +978,7 @@ func logNeutrino(netDir string, errorLogger dex.Logger) error { return fmt.Errorf("error initializing log rotator: %w", err) } - backendLog := btclog.NewBackend(logWriter{logSpinner}) + backendLog := btclog.NewBackend(logSpinner) logger := func(name string, lvl btclog.Level) btclog.Logger { l := backendLog.Logger(name) @@ -1032,37 +1004,31 @@ type fileLoggerPlus struct { func (f *fileLoggerPlus) Warnf(format string, params ...interface{}) { f.log.Warnf(format, params...) f.Logger.Warnf(format, params...) - } func (f *fileLoggerPlus) Errorf(format string, params ...interface{}) { f.log.Errorf(format, params...) f.Logger.Errorf(format, params...) - } func (f *fileLoggerPlus) Criticalf(format string, params ...interface{}) { f.log.Criticalf(format, params...) f.Logger.Criticalf(format, params...) - } func (f *fileLoggerPlus) Warn(v ...interface{}) { f.log.Warn(v...) f.Logger.Warn(v...) - } func (f *fileLoggerPlus) Error(v ...interface{}) { f.log.Error(v...) f.Logger.Error(v...) - } func (f *fileLoggerPlus) Critical(v ...interface{}) { f.log.Critical(v...) f.Logger.Critical(v...) - } type logAdapter struct { diff --git a/dex/errors.go b/dex/errors.go index 947e647fc4..c8cf598411 100644 --- a/dex/errors.go +++ b/dex/errors.go @@ -43,15 +43,12 @@ func NewError(err error, detail string) Error { // scheduled with Add. If Success is not signaled before Done, the shutdown // routines will be run in the reverse order that they are added. type ErrorCloser struct { - log Logger closers []func() error - success bool } // NewErrorCloser creates a new ErrorCloser. -func NewErrorCloser(log Logger) *ErrorCloser { +func NewErrorCloser() *ErrorCloser { return &ErrorCloser{ - log: log, closers: make([]func() error, 0, 3), } } @@ -64,19 +61,15 @@ func (e *ErrorCloser) Add(closer func() error) { // Success cancels the running of any Add'ed functions. func (e *ErrorCloser) Success() { - e.success = true + e.closers = nil } // Done signals that the ErrorClose can run its registered functions if success // has not yet been flagged. -func (e *ErrorCloser) Done() { - if e.success { - return - } - +func (e *ErrorCloser) Done(log Logger) { for i := len(e.closers) - 1; i >= 0; i-- { if err := e.closers[i](); err != nil { - e.log.Errorf("error running shutdown function %d: %v", i, err) + log.Errorf("error running shutdown function %d: %v", i, err) } } } From d0f62d9db57c7486ba57f019d4aa79f8cdb7c7ca Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Wed, 28 Sep 2022 10:51:31 -0500 Subject: [PATCH 4/5] review followup --- client/asset/btc/btc.go | 37 ++++---- client/asset/btc/spv.go | 12 ++- client/asset/btc/spv_wrapper.go | 154 ++++++++++++++++---------------- client/asset/ltc/ltc.go | 3 +- client/asset/ltc/spv.go | 34 ++++--- client/asset/ltc/spv_test.go | 60 +++++++++++++ 6 files changed, 187 insertions(+), 113 deletions(-) create mode 100644 client/asset/ltc/spv_test.go diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 4717314f64..687263b89c 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -112,9 +112,10 @@ var ( NoEcho: true, }, { - Key: "rpcbind", // match RPCConfig struct field tags - DisplayName: "JSON-RPC Address", - Description: "Electrum's 'rpchost' or :", + Key: "rpcbind", // match RPCConfig struct field tags + DisplayName: "JSON-RPC Address", + Description: "Electrum's 'rpchost' or :", + DefaultValue: "127.0.0.1", }, { Key: "rpcport", @@ -171,7 +172,8 @@ func apiFallbackOpt(defaultV bool) *asset.ConfigOption { Description: "Allow fee rate estimation from a block explorer API. " + "This is useful as a fallback for SPV wallets and RPC wallets " + "that have recently been started.", - IsBoolean: defaultV, + IsBoolean: true, + DefaultValue: defaultV, } } @@ -643,7 +645,7 @@ func (d *Driver) Exists(walletType, dataDir string, settings map[string]string, } dir := filepath.Join(dataDir, chainParams.Name, "spv") // timeout and recoverWindow arguments borrowed from btcwallet directly. - loader := wallet.NewLoader(chainParams, dir, true, 60*time.Second, 250) + loader := wallet.NewLoader(chainParams, dir, true, dbTimeout, 250) return loader.WalletExists() } @@ -1153,18 +1155,19 @@ func OpenSPVWallet(cfg *BTCCloneCFG, walletConstructor BTCWalletConstructor) (*E } spvw := &spvWallet{ - chainParams: cfg.ChainParams, - cfg: walletCfg, - acctNum: defaultAcctNum, - acctName: defaultAcctName, - dir: filepath.Join(cfg.WalletCFG.DataDir, cfg.ChainParams.Name, "spv"), - txBlocks: make(map[chainhash.Hash]*hashEntry), - checkpoints: make(map[outPoint]*scanCheckpoint), - log: cfg.Logger.SubLogger("SPV"), - tipChan: make(chan *block, 8), - newBTCWallet: walletConstructor, - decodeAddr: btc.decodeAddr, - } + chainParams: cfg.ChainParams, + cfg: walletCfg, + acctNum: defaultAcctNum, + acctName: defaultAcctName, + dir: filepath.Join(cfg.WalletCFG.DataDir, cfg.ChainParams.Name, "spv"), + txBlocks: make(map[chainhash.Hash]*hashEntry), + checkpoints: make(map[outPoint]*scanCheckpoint), + log: cfg.Logger.SubLogger("SPV"), + tipChan: make(chan *block, 8), + decodeAddr: btc.decodeAddr, + } + + spvw.wallet = walletConstructor(spvw.dir, spvw.cfg, spvw.chainParams, spvw.log) btc.node = spvw diff --git a/client/asset/btc/spv.go b/client/asset/btc/spv.go index 371823a090..6d460c5b24 100644 --- a/client/asset/btc/spv.go +++ b/client/asset/btc/spv.go @@ -27,6 +27,10 @@ import ( "github.com/lightninglabs/neutrino" ) +const ( + dbTimeout = 20 * time.Second +) + // btcSPVWallet implements BTCWallet for Bitcoin. type btcSPVWallet struct { *wallet.Wallet @@ -65,7 +69,7 @@ func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dataDir strin return fmt.Errorf("error creating wallet directories: %w", err) } - loader := wallet.NewLoader(net, dir, true, 60*time.Second, 250) + loader := wallet.NewLoader(net, dir, true, dbTimeout, 250) pubPass := []byte(wallet.InsecurePubPassphrase) @@ -90,7 +94,7 @@ func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dataDir strin // The chain service DB neutrinoDBPath := filepath.Join(dir, neutrinoDBName) - db, err := walletdb.Create("bdb", neutrinoDBPath, true, 5*time.Second) + db, err := walletdb.Create("bdb", neutrinoDBPath, true, dbTimeout) if err != nil { bailOnWallet() return fmt.Errorf("unable to create neutrino db at %q: %w", neutrinoDBPath, err) @@ -143,7 +147,7 @@ func (w *btcSPVWallet) Start() (SPVService, error) { return nil, fmt.Errorf("error initializing btcwallet+neutrino logging: %v", err) } // timeout and recoverWindow arguments borrowed from btcwallet directly. - w.loader = wallet.NewLoader(w.chainParams, w.dir, true, 60*time.Second, 250) + w.loader = wallet.NewLoader(w.chainParams, w.dir, true, dbTimeout, 250) exists, err := w.loader.WalletExists() if err != nil { @@ -164,7 +168,7 @@ func (w *btcSPVWallet) Start() (SPVService, error) { errCloser.Add(w.loader.UnloadWallet) neutrinoDBPath := filepath.Join(w.dir, neutrinoDBName) - w.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true, wallet.DefaultDBTimeout) + w.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true, dbTimeout) if err != nil { return nil, fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) } diff --git a/client/asset/btc/spv_wrapper.go b/client/asset/btc/spv_wrapper.go index f13658ade0..3657f33eb8 100644 --- a/client/asset/btc/spv_wrapper.go +++ b/client/asset/btc/spv_wrapper.go @@ -252,15 +252,14 @@ func (w logWriter) Write(p []byte) (n int, err error) { // Bitcoin wallet. spvWallet controls an instance of btcwallet.Wallet directly // and does not run or connect to the RPC server. type spvWallet struct { - chainParams *chaincfg.Params - cfg *WalletConfig - wallet BTCWallet - cl SPVService - acctNum uint32 - acctName string - dir string - newBTCWallet BTCWalletConstructor - decodeAddr dexbtc.AddressDecoder + chainParams *chaincfg.Params + cfg *WalletConfig + wallet BTCWallet + cl SPVService + acctNum uint32 + acctName string + dir string + decodeAddr dexbtc.AddressDecoder txBlocksMtx sync.Mutex txBlocks map[chainhash.Hash]*hashEntry @@ -861,6 +860,74 @@ func (w *spvWallet) sendWithSubtract(pkScript []byte, value, feeRate uint64) (*c return w.sendRawTransaction(tx) } +// estimateSendTxFee callers should provide at least one output value. +func (w *spvWallet) estimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract bool) (fee uint64, err error) { + minTxSize := uint64(tx.SerializeSize()) + var sendAmount uint64 + for _, txOut := range tx.TxOut { + sendAmount += uint64(txOut.Value) + } + + // If subtract is true, select enough inputs for sendAmount. Fees will be taken + // from the sendAmount. If not, select enough inputs to cover minimum fees. + enough := func(inputsSize, sum uint64) bool { + if subtract { + return sum >= sendAmount + } + minFee := (minTxSize + inputsSize) * feeRate + return sum >= sendAmount+minFee + } + + unspents, err := w.listUnspent() + if err != nil { + return 0, fmt.Errorf("error listing unspent outputs: %w", err) + } + + utxos, _, _, err := convertUnspent(0, unspents, w.chainParams) + if err != nil { + return 0, fmt.Errorf("error converting unspent outputs: %w", err) + } + + sum, inputsSize, _, _, _, _, err := fund(utxos, enough) + if err != nil { + return 0, err + } + + txSize := minTxSize + uint64(inputsSize) + estFee := feeRate * txSize + remaining := sum - sendAmount + + // Check if there will be a change output if there is enough remaining. + estFeeWithChange := (txSize + dexbtc.P2WPKHOutputSize) * feeRate + var changeValue uint64 + if remaining > estFeeWithChange { + changeValue = remaining - estFeeWithChange + } + + if subtract { + // fees are already included in sendAmount, anything else is change. + changeValue = remaining + } + + var finalFee uint64 + if dexbtc.IsDustVal(dexbtc.P2WPKHOutputSize, changeValue, feeRate, true) { + // remaining cannot cover a non-dust change and the fee for the change. + finalFee = estFee + remaining + } else { + // additional fee will be paid for non-dust change + finalFee = estFeeWithChange + } + + if subtract { + sendAmount -= finalFee + } + if dexbtc.IsDustVal(minTxSize, sendAmount, feeRate, true) { + return 0, errors.New("output value is dust") + } + + return finalFee, nil +} + // 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 @@ -999,7 +1066,6 @@ func (w *spvWallet) logFilePath() string { // connect will start the wallet and begin syncing. func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) (err error) { - w.wallet = w.newBTCWallet(w.dir, w.cfg, w.chainParams, w.log) w.cl, err = w.wallet.Start() if err != nil { return err @@ -1059,74 +1125,6 @@ func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) (err error) return nil } -// estimateSendTxFee callers should provide at least one output value. -func (w *spvWallet) estimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract bool) (fee uint64, err error) { - minTxSize := uint64(tx.SerializeSize()) - var sendAmount uint64 - for _, txOut := range tx.TxOut { - sendAmount += uint64(txOut.Value) - } - - // If subtract is true, select enough inputs for sendAmount. Fees will be taken - // from the sendAmount. If not, select enough inputs to cover minimum fees. - enough := func(inputsSize, sum uint64) bool { - if subtract { - return sum >= sendAmount - } - minFee := (minTxSize + inputsSize) * feeRate - return sum >= sendAmount+minFee - } - - unspents, err := w.listUnspent() - if err != nil { - return 0, fmt.Errorf("error listing unspent outputs: %w", err) - } - - utxos, _, _, err := convertUnspent(0, unspents, w.chainParams) - if err != nil { - return 0, fmt.Errorf("error converting unspent outputs: %w", err) - } - - sum, inputsSize, _, _, _, _, err := fund(utxos, enough) - if err != nil { - return 0, err - } - - txSize := minTxSize + uint64(inputsSize) - estFee := feeRate * txSize - remaining := sum - sendAmount - - // Check if there will be a change output if there is enough remaining. - estFeeWithChange := (txSize + dexbtc.P2WPKHOutputSize) * feeRate - var changeValue uint64 - if remaining > estFeeWithChange { - changeValue = remaining - estFeeWithChange - } - - if subtract { - // fees are already included in sendAmount, anything else is change. - changeValue = remaining - } - - var finalFee uint64 - if dexbtc.IsDustVal(dexbtc.P2WPKHOutputSize, changeValue, feeRate, true) { - // remaining cannot cover a non-dust change and the fee for the change. - finalFee = estFee + remaining - } else { - // additional fee will be paid for non-dust change - finalFee = estFeeWithChange - } - - if subtract { - sendAmount -= finalFee - } - if dexbtc.IsDustVal(minTxSize, sendAmount, feeRate, true) { - return 0, errors.New("output value is dust") - } - - return finalFee, nil -} - // moveWalletData will move all wallet files to a backup directory. func (w *spvWallet) moveWalletData(backupDir string) error { timeString := time.Now().Format("2006-01-02T15:04:05") diff --git a/client/asset/ltc/ltc.go b/client/asset/ltc/ltc.go index c06b7f5c20..d0e97dff1f 100644 --- a/client/asset/ltc/ltc.go +++ b/client/asset/ltc/ltc.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "path/filepath" - "time" "decred.org/dcrdex/client/asset" "decred.org/dcrdex/client/asset/btc" @@ -121,7 +120,7 @@ func (d *Driver) Exists(walletType, dataDir string, settings map[string]string, return false, err } netDir := filepath.Join(dataDir, chainParams.Name, "spv") - loader := wallet.NewLoader(chainParams, netDir, true, 60*time.Second, 250) + loader := wallet.NewLoader(chainParams, netDir, true, dbTimeout, 250) return loader.WalletExists() } diff --git a/client/asset/ltc/spv.go b/client/asset/ltc/spv.go index 999df556b9..9f9446edca 100644 --- a/client/asset/ltc/spv.go +++ b/client/asset/ltc/spv.go @@ -55,6 +55,7 @@ const ( logDirName = "logs" neutrinoDBName = "neutrino.db" defaultAcctNum = 0 + dbTimeout = 20 * time.Second ) var ( @@ -123,7 +124,7 @@ func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dbDir string, } // timeout and recoverWindow arguments borrowed from btcwallet directly. - loader := wallet.NewLoader(net, netDir, true, 60*time.Second, 250) + loader := wallet.NewLoader(net, netDir, true, dbTimeout, 250) pubPass := []byte(wallet.InsecurePubPassphrase) @@ -145,7 +146,7 @@ func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dbDir string, // The chain service DB neutrinoDBPath := filepath.Join(netDir, neutrinoDBName) - db, err := walletdb.Create("bdb", neutrinoDBPath, true, 5*time.Second) + db, err := walletdb.Create("bdb", neutrinoDBPath, true, dbTimeout) if err != nil { return fmt.Errorf("unable to create neutrino db at %q: %w", neutrinoDBPath, err) } @@ -161,7 +162,7 @@ func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dbDir string, return nil } -// walletParams works around a bug in dcrwallet that doesn't recognize +// walletParams works around a bug in ltcwallet that doesn't recognize // wire.TestNet4 in (*ScopedKeyManager).cloneKeyWithVersion which is called from // AccountProperties. Only do this for the *wallet.Wallet, not the // *neutrino.ChainService. @@ -181,7 +182,8 @@ func (w *ltcSPVWallet) Start() (btc.SPVService, error) { return nil, fmt.Errorf("error initializing dcrwallet+neutrino logging: %v", err) } // recoverWindow arguments borrowed from ltcwallet directly. - w.loader = wallet.NewLoader(w.walletParams(), w.dir, true, 60*time.Second, 250) + + w.loader = wallet.NewLoader(w.walletParams(), w.dir, true, dbTimeout, 250) exists, err := w.loader.WalletExists() if err != nil { @@ -202,7 +204,7 @@ func (w *ltcSPVWallet) Start() (btc.SPVService, error) { errCloser.Add(w.loader.UnloadWallet) neutrinoDBPath := filepath.Join(w.dir, neutrinoDBName) - w.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true, wallet.DefaultDBTimeout) + w.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true, dbTimeout) if err != nil { return nil, fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) } @@ -211,8 +213,8 @@ func (w *ltcSPVWallet) Start() (btc.SPVService, error) { // Depending on the network, we add some addpeers or a connect peer. On // regtest, if 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. On - // mainet and testnet3, add a known reliable persistent peer to be used in - // addition to normal DNS seed-based peer discovery. + // testnet4, add a known reliable persistent peer to be used in addition to + // normal DNS seed-based peer discovery. var addPeers []string var connectPeers []string switch w.chainParams.Net { @@ -332,6 +334,14 @@ func (w *ltcSPVWallet) txDetails(txHash *ltcchainhash.Hash) (*ltcwtxmgr.TxDetail return details, nil } +func (w *ltcSPVWallet) addrLTC2BTC(addr ltcutil.Address) (btcutil.Address, error) { + return btcutil.DecodeAddress(addr.String(), w.btcParams) +} + +func (w *ltcSPVWallet) addrBTC2LTC(addr btcutil.Address) (ltcutil.Address, error) { + return ltcutil.DecodeAddress(addr.String(), w.chainParams) +} + func (w *ltcSPVWallet) PublishTransaction(btcTx *wire.MsgTx, label string) error { ltcTx, err := convertMsgTxToLTC(btcTx) if err != nil { @@ -452,7 +462,7 @@ func (w *ltcSPVWallet) NewChangeAddress(account uint32, _ waddrmgr.KeyScope) (bt if err != nil { return nil, err } - return btcutil.DecodeAddress(ltcAddr.String(), w.btcParams) + return w.addrLTC2BTC(ltcAddr) } func (w *ltcSPVWallet) NewAddress(account uint32, _ waddrmgr.KeyScope) (btcutil.Address, error) { @@ -460,11 +470,11 @@ func (w *ltcSPVWallet) NewAddress(account uint32, _ waddrmgr.KeyScope) (btcutil. if err != nil { return nil, err } - return btcutil.DecodeAddress(ltcAddr.String(), w.btcParams) + return w.addrLTC2BTC(ltcAddr) } func (w *ltcSPVWallet) PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) { - ltcAddr, err := ltcutil.DecodeAddress(a.String(), w.chainParams) + ltcAddr, err := w.addrBTC2LTC(a) if err != nil { return nil, err } @@ -504,7 +514,7 @@ func (w *ltcSPVWallet) SendOutputs(outputs []*wire.TxOut, _ *waddrmgr.KeyScope, } func (w *ltcSPVWallet) HaveAddress(a btcutil.Address) (bool, error) { - ltcAddr, err := ltcutil.DecodeAddress(a.String(), w.chainParams) + ltcAddr, err := w.addrBTC2LTC(a) if err != nil { return false, err } @@ -546,7 +556,7 @@ func (w *ltcSPVWallet) AccountProperties(_ waddrmgr.KeyScope, acct uint32) (*wad InternalKeyCount: props.InternalKeyCount, ImportedKeyCount: props.ImportedKeyCount, MasterKeyFingerprint: props.MasterKeyFingerprint, - KeyScope: waddrmgr.KeyScopeBIP0044, + KeyScope: waddrmgr.KeyScopeBIP0084, IsWatchOnly: props.IsWatchOnly, // The last two would need conversion but aren't currently used. // AccountPubKey: props.AccountPubKey, diff --git a/client/asset/ltc/spv_test.go b/client/asset/ltc/spv_test.go new file mode 100644 index 0000000000..6fe7fb09f1 --- /dev/null +++ b/client/asset/ltc/spv_test.go @@ -0,0 +1,60 @@ +package ltc + +import ( + "testing" + + dexltc "decred.org/dcrdex/dex/networks/ltc" + ltcchaincfg "github.com/ltcsuite/ltcd/chaincfg" + "github.com/ltcsuite/ltcd/ltcutil" +) + +func TestAddressTranslationRoundTrip(t *testing.T) { + w := ltcSPVWallet{ + chainParams: <cchaincfg.MainNetParams, + btcParams: dexltc.MainNetParams, + } + tests := []struct { + name string + addr string // ltcutil.Address + }{ + { + "bech32", + "ltc1qx9ry0xnsz9spzw0vy7p9szyycmtk4a4xkessy5", + }, + { + "p2pkh", + "LXyPtJexNLCdk99vYhgqrB2hXAdq6PQx8r", + }, + { + "p2sh", + "MWidTW5JsYaxPeDyQKF3525D8PJZLVb5Ho", + }, + { + "32-byte segwit program (taproot)", + "ltc1p7v5t2ltshynyrjj9ft5x5nulf76r8ml23789pwr29vu3rksnue3q2y7w9a", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ltcAddr, err := ltcutil.DecodeAddress(tt.addr, w.chainParams) + if err != nil { + t.Fatal(err) + } + btcAddr, err := w.addrLTC2BTC(ltcAddr) + if err != nil { + t.Errorf("ltcSPVWallet.addrLTC2BTC() error = %v", err) + return + } + if btcAddr.String() != tt.addr { + t.Errorf("ltcSPVWallet.addrLTC2BTC() = %v, want %v", btcAddr, tt.addr) + } + reLtcAddr, err := w.addrBTC2LTC(btcAddr) + if err != nil { + t.Errorf("ltcSPVWallet.addrBTC2LTC() error = %v", err) + } + if reLtcAddr.String() != tt.addr { + t.Errorf("ltcSPVWallet.addrBTC2LTC() = %v, want %v", reLtcAddr, tt.addr) + } + }) + } +} From 8225f8fb45ee75e056684e4d7ff21cf7b376a22f Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Wed, 28 Sep 2022 12:08:15 -0500 Subject: [PATCH 5/5] add testnet4 seeds --- client/asset/ltc/spv.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/client/asset/ltc/spv.go b/client/asset/ltc/spv.go index 9f9446edca..bf6afeeb16 100644 --- a/client/asset/ltc/spv.go +++ b/client/asset/ltc/spv.go @@ -8,6 +8,7 @@ import ( "context" "errors" "fmt" + "net/netip" "os" "path/filepath" "sync/atomic" @@ -61,6 +62,13 @@ const ( var ( waddrmgrNamespace = []byte("waddrmgr") wtxmgrNamespace = []byte("wtxmgr") + + testnet4Seeds = [][]byte{ + {0x12, 0xc0, 0x38, 0x95, 0x87, 0x4b}, + {0x3, 0x47, 0x1e, 0x2e, 0x87, 0x4b}, + {0x22, 0x59, 0x4e, 0x2d, 0x87, 0x4b}, + {0x22, 0x8c, 0xc5, 0x98, 0x87, 0x4b}, + } ) // ltcSPVWallet is an implementation of btc.BTCWallet that runs a native @@ -220,6 +228,11 @@ func (w *ltcSPVWallet) Start() (btc.SPVService, error) { switch w.chainParams.Net { case ltcwire.TestNet4: addPeers = []string{"127.0.0.1:19335"} + for _, host := range testnet4Seeds { + var addr netip.AddrPort + addr.UnmarshalBinary(host) + addPeers = append(addPeers, addr.String()) + } case ltcwire.TestNet, ltcwire.SimNet: // plain "wire.TestNet" is regnet! connectPeers = []string{"localhost:20585"} }