Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client/ui/btc: Recover BTC SPV wallet #1507

Merged
merged 1 commit into from May 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 53 additions & 9 deletions client/asset/btc/btc.go
Expand Up @@ -419,7 +419,6 @@ type WalletConfig struct {
RedeemConfTarget uint64 `ini:"redeemconftarget"`
ActivelyUsed bool `ini:"special:activelyUsed"` //injected by core
WalletName string `ini:"walletname"` // RPC
Peer string `ini:"peer"` // SPV
Birthday uint64 `ini:"walletbirthday"` // SPV
}

Expand Down Expand Up @@ -523,7 +522,14 @@ func (d *Driver) Create(params *asset.CreateWalletParams) error {
return err
}

return createSPVWallet(params.Pass, params.Seed, walletCfg.adjustedBirthday(), params.DataDir, params.Logger, chainParams)
recoveryCfg := new(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)
}

// Open opens or connects to the BTC exchange wallet. Start the wallet with its
Expand Down Expand Up @@ -626,6 +632,49 @@ var _ asset.Accelerator = (*ExchangeWalletSPV)(nil)
var _ asset.Rescanner = (*ExchangeWalletSPV)(nil)
var _ asset.FeeRater = (*ExchangeWalletFullNode)(nil)
var _ asset.LogFiler = (*ExchangeWalletSPV)(nil)
var _ asset.Recoverer = (*ExchangeWalletSPV)(nil)

// RecoveryCfg is the information that is transferred from the old wallet
// to the new one when the wallet is recovered.
type RecoveryCfg struct {
NumExternalAddresses uint32 `ini:"numexternaladdr"`
NumInternalAddresses uint32 `ini:"numinternaladdr"`
}

// GetRecoveryCfg returns information that will help the wallet get
// back to its previous state after it is recreated. Part of the
// Recoverer interface.
func (btc *ExchangeWalletSPV) GetRecoveryCfg() (map[string]string, error) {
w := btc.node.(*spvWallet)

internal, external, err := w.numDerivedAddresses()
if err != nil {
return nil, err
}

reCfg := &RecoveryCfg{
NumInternalAddresses: internal,
NumExternalAddresses: external,
}
cfg, err := config.Mapify(reCfg)
if err != nil {
return nil, err
}

return cfg, nil
}

// Destroy will delete all the wallet files so the wallet can be recreated.
// Part of the Recoverer interface.
func (btc *ExchangeWalletSPV) Move(backupDir string) error {
w := btc.node.(*spvWallet)
err := w.moveWalletData(backupDir)
if err != nil {
return fmt.Errorf("unable to move wallet data: %w", err)
}

return nil
}

// Rescan satisfies the asset.Rescanner interface, and issues a rescan wallet
// command if the backend is an SPV wallet.
Expand Down Expand Up @@ -892,13 +941,8 @@ func openSPVWallet(cfg *BTCCloneCFG) (*ExchangeWalletSPV, error) {
return nil, err
}

var peers []string
martonp marked this conversation as resolved.
Show resolved Hide resolved
if walletCfg.Peer != "" {
peers = append(peers, walletCfg.Peer)
}

allowAutomaticRescan := !walletCfg.ActivelyUsed
btc.node = loadSPVWallet(cfg.WalletCFG.DataDir, cfg.Logger.SubLogger("SPV"), peers, cfg.ChainParams, walletCfg.adjustedBirthday(), allowAutomaticRescan)
btc.node = loadSPVWallet(cfg.WalletCFG.DataDir, cfg.Logger.SubLogger("SPV"), cfg.ChainParams, walletCfg.adjustedBirthday(), allowAutomaticRescan)

return &ExchangeWalletSPV{baseWallet: btc}, nil
}
Expand Down Expand Up @@ -3585,7 +3629,7 @@ func (btc *baseWallet) watchBlocks(ctx context.Context) {
blockAllowance := walletBlockAllowance
syncStatus, err := btc.node.syncStatus()
if err != nil {
btc.log.Errorf("Error retreiving sync status before queuing polled block: %v", err)
btc.log.Errorf("Error retrieving sync status before queuing polled block: %v", err)
} else if syncStatus.Syncing {
blockAllowance *= 10
}
Expand Down
83 changes: 68 additions & 15 deletions client/asset/btc/spv.go
Expand Up @@ -79,6 +79,8 @@ const (
defaultAcctName = "default"
)

var wAddrMgrBkt = []byte("waddrmgr")

// btcWallet is satisfied by *btcwallet.Wallet -> *walletExtender.
type btcWallet interface {
PublishTransaction(tx *wire.MsgTx, label string) error
Expand Down Expand Up @@ -127,25 +129,43 @@ type neutrinoService interface {

var _ neutrinoService = (*neutrino.ChainService)(nil)

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 {
scopedKeyManager.ExtendExternalAddresses(ns, defaultAcctNum, extIdx)
}
if intIdx > 0 {
scopedKeyManager.ExtendInternalAddresses(ns, defaultAcctNum, intIdx)
}
return nil
})
}

// createSPVWallet creates a new SPV wallet.
func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dbDir string, log dex.Logger, net *chaincfg.Params) error {
func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dbDir string, log dex.Logger, extIdx, intIdx uint32, net *chaincfg.Params) error {
netDir := filepath.Join(dbDir, net.Name)

if err := logNeutrino(netDir); err != nil {
return fmt.Errorf("error initializing btcwallet+neutrino logging: %v", err)
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: %v", err)
return fmt.Errorf("error creating wallet directories: %w", err)
}

loader := wallet.NewLoader(net, netDir, true, 60*time.Second, 250)

pubPass := []byte(wallet.InsecurePubPassphrase)

_, err = loader.CreateNewWallet(pubPass, privPass, seed, bday)
btcw, err := loader.CreateNewWallet(pubPass, privPass, seed, bday)
if err != nil {
return fmt.Errorf("CreateNewWallet error: %w", err)
}
Expand All @@ -156,11 +176,20 @@ func createSPVWallet(privPass []byte, seed []byte, bday time.Time, dbDir string,
}
}

if extIdx > 0 || intIdx > 0 {
err = extendAddresses(extIdx, intIdx, btcw)
if err != nil {
bailOnWallet()
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 {
bailOnWallet()
return fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err)
return fmt.Errorf("unable to create neutrino db at %q: %w", neutrinoDBPath, err)
}
if err = db.Close(); err != nil {
bailOnWallet()
Expand Down Expand Up @@ -290,7 +319,6 @@ type spvWallet struct {
acctName string
netDir string
neutrinoDB walletdb.DB
connectPeers []string

txBlocksMtx sync.Mutex
txBlocks map[chainhash.Hash]*hashEntry
Expand All @@ -314,7 +342,7 @@ var _ Wallet = (*spvWallet)(nil)
var _ tipNotifier = (*spvWallet)(nil)

// loadSPVWallet loads an existing wallet.
func loadSPVWallet(dbDir string, logger dex.Logger, connectPeers []string, chainParams *chaincfg.Params, birthday time.Time, allowAutomaticRescan bool) *spvWallet {
func loadSPVWallet(dbDir string, logger dex.Logger, chainParams *chaincfg.Params, birthday time.Time, allowAutomaticRescan bool) *spvWallet {
return &spvWallet{
chainParams: chainParams,
acctNum: defaultAcctNum,
Expand All @@ -323,7 +351,6 @@ func loadSPVWallet(dbDir string, logger dex.Logger, connectPeers []string, chain
txBlocks: make(map[chainhash.Hash]*hashEntry),
checkpoints: make(map[outPoint]*scanCheckpoint),
log: logger,
connectPeers: connectPeers,
tipChan: make(chan *block, 8),
allowAutomaticRescan: allowAutomaticRescan,
birthday: birthday,
Expand Down Expand Up @@ -1169,15 +1196,14 @@ func (w *spvWallet) startWallet() error {
// 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!
if len(w.connectPeers) == 0 {
w.connectPeers = []string{"localhost:20575"}
}
connectPeers = []string{"localhost:20575"}
}
w.log.Debug("Starting neutrino chain service...")
chainService, err := neutrino.NewChainService(neutrino.Config{
Expand All @@ -1186,7 +1212,7 @@ func (w *spvWallet) startWallet() error {
ChainParams: *w.chainParams,
PersistToDisk: true, // keep cfilter headers on disk for efficient rescanning
AddPeers: addPeers,
ConnectPeers: w.connectPeers,
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
Expand Down Expand Up @@ -1220,7 +1246,7 @@ func (w *spvWallet) startWallet() error {

if !oldBday.Equal(w.birthday) {
err = walletdb.Update(wdb, func(dbtx walletdb.ReadWriteTx) error {
ns := dbtx.ReadWriteBucket([]byte("waddrmgr"))
ns := dbtx.ReadWriteBucket(wAddrMgrBkt)
return btcw.Manager.SetBirthday(ns, w.birthday)
})
if err != nil {
Expand All @@ -1244,6 +1270,33 @@ func (w *spvWallet) startWallet() error {
return 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.netDir, backupFolder)
}

// numDerivedAddresses returns the number of internal and external addresses
// that the wallet has derived.
func (w *spvWallet) numDerivedAddresses() (internal, external uint32, err error) {
btcw, ok := w.loader.LoadedWallet()
if !ok {
return 0, 0, err
}

props, err := btcw.AccountProperties(waddrmgr.KeyScopeBIP0084, w.acctNum)
if err != nil {
return 0, 0, err
}

return props.InternalKeyCount, props.ExternalKeyCount, nil
}

// rescanWalletAsync 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
Expand Down Expand Up @@ -1306,8 +1359,8 @@ func (w *spvWallet) forceRescan() {
}

err = walletdb.Update(wdb, func(dbtx walletdb.ReadWriteTx) error {
ns := dbtx.ReadWriteBucket([]byte("waddrmgr")) // it'll be fine
return btcw.Manager.SetSyncedTo(ns, nil) // never synced, forcing recover from birthday
ns := dbtx.ReadWriteBucket(wAddrMgrBkt) // it'll be fine
return btcw.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)
Expand Down
20 changes: 20 additions & 0 deletions client/asset/interface.go
Expand Up @@ -20,6 +20,7 @@ const (
WalletTraitLogFiler // The Wallet allows for downloading of a log file.
WalletTraitFeeRater // Wallet can provide a fee rate for non-critical transactions
WalletTraitAccelerator // This wallet can accelerate transactions using the CPFP technique
WalletTraitRecoverer // The wallet is an asset.Recoverer.
)

// IsRescanner tests if the WalletTrait has the WalletTraitRescanner bit set.
Expand Down Expand Up @@ -52,6 +53,12 @@ func (wt WalletTrait) IsAccelerator() bool {
return wt&WalletTraitAccelerator != 0
}

// IsRecoverer tests if the WalletTrait has the WalletTraitRecoverer bit set,
// which indicates the wallet implements the Recoverer interface.
func (wt WalletTrait) IsRecoverer() bool {
return wt&WalletTraitRecoverer != 0
}

// DetermineWalletTraits returns the WalletTrait bitset for the provided Wallet.
func DetermineWalletTraits(w Wallet) (t WalletTrait) {
if _, is := w.(Rescanner); is {
Expand All @@ -69,6 +76,9 @@ func DetermineWalletTraits(w Wallet) (t WalletTrait) {
if _, is := w.(Accelerator); is {
t |= WalletTraitAccelerator
}
if _, is := w.(Recoverer); is {
t |= WalletTraitRecoverer
}
return t
}

Expand Down Expand Up @@ -336,6 +346,16 @@ type Rescanner interface {
Rescan(ctx context.Context) error
}

// Recoverer is a wallet implementation with recover functionality.
type Recoverer interface {
// GetRecoveryCfg returns information that will help the wallet get back to
// its previous state after it is recreated.
GetRecoveryCfg() (map[string]string, error)
// Move will move all wallet files to a backup directory so the wallet can
// be recreated.
Move(backupdir string) error
}

// Sender is a wallet that can send funds to an address, as opposed to
// withdrawing a certain amount from the source wallet/account.
type Sender interface {
Expand Down