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.
This commit is contained in:
Oliver Gugger 2022-01-05 11:04:34 +01:00
parent afc53d1c52
commit bab807a57d
No known key found for this signature in database
GPG key ID: 8E4256593F177720
6 changed files with 93 additions and 25 deletions

View file

@ -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,

View file

@ -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),
},
}
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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

View file

@ -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