Skip to content

Commit

Permalink
multi: add migrate-wallet-to-watch-only flag
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
guggero committed Jan 6, 2022
1 parent afc53d1 commit bab807a
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 25 deletions.
5 changes: 4 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
40 changes: 22 additions & 18 deletions config_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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),
},
}
}
Expand Down
17 changes: 12 additions & 5 deletions lncfg/remotesigner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
42 changes: 41 additions & 1 deletion lnwallet/btcwallet/btcwallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions lnwallet/btcwallet/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions sample-lnd.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit bab807a

Please sign in to comment.