From bab807a57d51f055119f7161bc1b7a30e1376a10 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:34 +0100 Subject: [PATCH] multi: add migrate-wallet-to-watch-only flag To enable converting an existing wallet with private key material into a watch-only wallet on first startup with remote signing enabled, we add a new flag. Since the conversion is a destructive process, this shouldn't happen automatically just because remote signing is enabled. --- config.go | 5 +++- config_builder.go | 40 +++++++++++++++++-------------- lncfg/remotesigner.go | 17 +++++++++---- lnwallet/btcwallet/btcwallet.go | 42 ++++++++++++++++++++++++++++++++- lnwallet/btcwallet/config.go | 7 ++++++ sample-lnd.conf | 7 ++++++ 6 files changed, 93 insertions(+), 25 deletions(-) diff --git a/config.go b/config.go index 218b42b24..1a59cd2e5 100644 --- a/config.go +++ b/config.go @@ -1612,7 +1612,10 @@ func (c *Config) ImplementationConfig( // watch-only source of chain and address data. But we don't need any // private key material in that btcwallet base wallet. if c.RemoteSigner.Enable { - rpcImpl := NewRPCSignerWalletImpl(c, ltndLog, interceptor) + rpcImpl := NewRPCSignerWalletImpl( + c, ltndLog, interceptor, + c.RemoteSigner.MigrateWatchOnly, + ) return &ImplementationCfg{ GrpcRegistrar: rpcImpl, RestRegistrar: rpcImpl, diff --git a/config_builder.go b/config_builder.go index 1fd9fb116..ed1b57711 100644 --- a/config_builder.go +++ b/config_builder.go @@ -144,8 +144,9 @@ type DefaultWalletImpl struct { logger btclog.Logger interceptor signal.Interceptor - watchOnly bool - pwService *walletunlocker.UnlockerService + watchOnly bool + migrateWatchOnly bool + pwService *walletunlocker.UnlockerService } // NewDefaultWalletImpl creates a new default wallet implementation. @@ -560,16 +561,17 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context, } walletConfig := &btcwallet.Config{ - PrivatePass: privateWalletPw, - PublicPass: publicWalletPw, - Birthday: walletInitParams.Birthday, - RecoveryWindow: walletInitParams.RecoveryWindow, - NetParams: d.cfg.ActiveNetParams.Params, - CoinType: d.cfg.ActiveNetParams.CoinType, - Wallet: walletInitParams.Wallet, - LoaderOptions: []btcwallet.LoaderOption{dbs.WalletDB}, - ChainSource: partialChainControl.ChainSource, - WatchOnly: d.watchOnly, + PrivatePass: privateWalletPw, + PublicPass: publicWalletPw, + Birthday: walletInitParams.Birthday, + RecoveryWindow: walletInitParams.RecoveryWindow, + NetParams: d.cfg.ActiveNetParams.Params, + CoinType: d.cfg.ActiveNetParams.CoinType, + Wallet: walletInitParams.Wallet, + LoaderOptions: []btcwallet.LoaderOption{dbs.WalletDB}, + ChainSource: partialChainControl.ChainSource, + WatchOnly: d.watchOnly, + MigrateWatchOnly: d.migrateWatchOnly, } // Parse coin selection strategy. @@ -650,15 +652,17 @@ type RPCSignerWalletImpl struct { // NewRPCSignerWalletImpl creates a new instance of the remote signing wallet // implementation. func NewRPCSignerWalletImpl(cfg *Config, logger btclog.Logger, - interceptor signal.Interceptor) *RPCSignerWalletImpl { + interceptor signal.Interceptor, + migrateWatchOnly bool) *RPCSignerWalletImpl { return &RPCSignerWalletImpl{ DefaultWalletImpl: &DefaultWalletImpl{ - cfg: cfg, - logger: logger, - interceptor: interceptor, - watchOnly: true, - pwService: createWalletUnlockerService(cfg), + cfg: cfg, + logger: logger, + interceptor: interceptor, + watchOnly: true, + migrateWatchOnly: migrateWatchOnly, + pwService: createWalletUnlockerService(cfg), }, } } diff --git a/lncfg/remotesigner.go b/lncfg/remotesigner.go index c4a0821d6..66995122c 100644 --- a/lncfg/remotesigner.go +++ b/lncfg/remotesigner.go @@ -13,11 +13,12 @@ const ( // RemoteSigner holds the configuration options for a remote RPC signer. type RemoteSigner struct { - Enable bool `long:"enable" description:"Use a remote signer for signing any on-chain related transactions or messages. Only recommended if local wallet is initialized as watch-only. Remote signer must use the same seed/root key as the local watch-only wallet but must have private keys."` - RPCHost string `long:"rpchost" description:"The remote signer's RPC host:port"` - MacaroonPath string `long:"macaroonpath" description:"The macaroon to use for authenticating with the remote signer"` - TLSCertPath string `long:"tlscertpath" description:"The TLS certificate to use for establishing the remote signer's identity"` - Timeout time.Duration `long:"timeout" description:"The timeout for connecting to and signing requests with the remote signer. Valid time units are {s, m, h}."` + Enable bool `long:"enable" description:"Use a remote signer for signing any on-chain related transactions or messages. Only recommended if local wallet is initialized as watch-only. Remote signer must use the same seed/root key as the local watch-only wallet but must have private keys."` + RPCHost string `long:"rpchost" description:"The remote signer's RPC host:port"` + MacaroonPath string `long:"macaroonpath" description:"The macaroon to use for authenticating with the remote signer"` + TLSCertPath string `long:"tlscertpath" description:"The TLS certificate to use for establishing the remote signer's identity"` + Timeout time.Duration `long:"timeout" description:"The timeout for connecting to and signing requests with the remote signer. Valid time units are {s, m, h}."` + MigrateWatchOnly bool `long:"migrate-wallet-to-watch-only" description:"If a wallet with private key material already exists, migrate it into a watch-only wallet on first startup. WARNING: This cannot be undone! Make sure you have backed up your seed before you use this flag! All private keys will be purged from the wallet after first unlock with this flag!"` } // Validate checks the values configured for our remote RPC signer. @@ -32,5 +33,11 @@ func (r *RemoteSigner) Validate() error { time.Millisecond) } + if r.MigrateWatchOnly && !r.Enable { + return fmt.Errorf("remote signer: cannot turn on wallet " + + "migration to watch-only if remote signing is not " + + "enabled") + } + return nil } diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index 579ca130d..ddaf1f06c 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -287,15 +287,38 @@ func (b *BtcWallet) InternalWallet() *base.Wallet { // // This is a part of the WalletController interface. func (b *BtcWallet) Start() error { + // Is the wallet (according to its database) currently watch-only + // already? If it is, we won't need to convert it later. + walletIsWatchOnly := b.wallet.Manager.WatchOnly() + + // If the wallet is watch-only, but we don't expect it to be, then we + // are in an unexpected state and cannot continue. + if walletIsWatchOnly && !b.cfg.WatchOnly { + return fmt.Errorf("wallet is watch-only but we expect it " + + "not to be; check if remote signing was disabled by " + + "accident") + } + // We'll start by unlocking the wallet and ensuring that the KeyScope: // (1017, 1) exists within the internal waddrmgr. We'll need this in // order to properly generate the keys required for signing various // contracts. If this is a watch-only wallet, we don't have any private // keys and therefore unlocking is not necessary. - if !b.cfg.WatchOnly { + if !walletIsWatchOnly { if err := b.wallet.Unlock(b.cfg.PrivatePass, nil); err != nil { return err } + + // If the wallet isn't about to be converted, we need to inform + // the user that this wallet still contains all private key + // material and that they need to migrate the existing wallet. + if b.cfg.WatchOnly && !b.cfg.MigrateWatchOnly { + log.Warnf("Wallet is expected to be in watch-only " + + "mode but hasn't been migrated to watch-only " + + "yet, it still contains private keys; " + + "consider turning on the watch-only wallet " + + "migration in remote signing mode") + } } scope, err := b.wallet.Manager.FetchScopedKeyManager(b.chainKeyScope) @@ -343,6 +366,23 @@ func (b *BtcWallet) Start() error { } } + // If this is the first startup with remote signing and wallet + // migration turned on and the wallet wasn't previously + // migrated, we can do that now that we made sure all accounts + // that we need were derived correctly. + if !walletIsWatchOnly && b.cfg.WatchOnly && + b.cfg.MigrateWatchOnly { + + log.Infof("Migrating wallet to watch-only mode, " + + "purging all private key material") + + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + err = b.wallet.Manager.ConvertToWatchingOnly(ns) + if err != nil { + return err + } + } + return nil }) if err != nil { diff --git a/lnwallet/btcwallet/config.go b/lnwallet/btcwallet/config.go index f97f2943f..b9c642c5d 100644 --- a/lnwallet/btcwallet/config.go +++ b/lnwallet/btcwallet/config.go @@ -76,6 +76,13 @@ type Config struct { // WatchOnly indicates that the wallet was initialized with public key // material only and does not contain any private keys. WatchOnly bool + + // MigrateWatchOnly indicates that if a wallet with private key material + // already exists, it should be attempted to be converted into a + // watch-only wallet on first startup. This flag has no effect if no + // wallet exists and a watch-only one is created directly, or, if the + // wallet was previously converted to a watch-only already. + MigrateWatchOnly bool } // NetworkDir returns the directory name of a network directory to hold wallet diff --git a/sample-lnd.conf b/sample-lnd.conf index f9a81167e..295d64c60 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -1274,6 +1274,13 @@ litecoin.node=ltcd ; Valid time units are {s, m, h}. ; remotesigner.timeout=5s +; If a wallet with private key material already exists, migrate it into a +; watch-only wallet on first startup. +; WARNING: This cannot be undone! Make sure you have backed up your seed before +; you use this flag! All private keys will be purged from the wallet after first +; unlock with this flag! +; remotesigner.migrate-wallet-to-watch-only=true + [gossip] ; Specify a set of pinned gossip syncers, which will always be actively syncing