lnd+walletunlocker: allow creating wallet from extended key

In addition to creating a new wallet from an aezeed, we allow specifying
an exteded master root key as the main wallet key directly.
Because an exteded key (xprv) doesn't contain any information about the
creation time of the wallet, we must assume a birthday to start scanning
the chain from (if the user doesn't provide an explicit value). Since
lnd only uses SegWit addresses, it makes sense to
choose the date that corresponds to the first mainnet block that
contained SegWit transactions.
Restoring a wallet from an extended master root key will result in a
significantly longer initial wallet rescan time if the default value is
used.
This commit is contained in:
Oliver Gugger 2020-10-24 23:18:50 +02:00
parent 6d70468ad5
commit aa9435be84
No known key found for this signature in database
GPG Key ID: 8E4256593F177720
2 changed files with 138 additions and 22 deletions

38
lnd.go
View File

@ -1492,13 +1492,17 @@ func waitForWalletPassword(cfg *Config,
case initMsg := <-pwService.InitMsgs: case initMsg := <-pwService.InitMsgs:
password := initMsg.Passphrase password := initMsg.Passphrase
cipherSeed := initMsg.WalletSeed cipherSeed := initMsg.WalletSeed
extendedKey := initMsg.WalletExtendedKey
recoveryWindow := initMsg.RecoveryWindow recoveryWindow := initMsg.RecoveryWindow
// Before we proceed, we'll check the internal version of the // Before we proceed, we'll check the internal version of the
// seed. If it's greater than the current key derivation // seed. If it's greater than the current key derivation
// version, then we'll return an error as we don't understand // version, then we'll return an error as we don't understand
// this. // this.
if cipherSeed.InternalVersion != keychain.KeyDerivationVersion { const latestVersion = keychain.KeyDerivationVersion
if cipherSeed != nil &&
cipherSeed.InternalVersion != latestVersion {
return nil, fmt.Errorf("invalid internal "+ return nil, fmt.Errorf("invalid internal "+
"seed version %v, current version is %v", "seed version %v, current version is %v",
cipherSeed.InternalVersion, cipherSeed.InternalVersion,
@ -1515,10 +1519,36 @@ func waitForWalletPassword(cfg *Config,
// With the seed, we can now use the wallet loader to create // With the seed, we can now use the wallet loader to create
// the wallet, then pass it back to avoid unlocking it again. // the wallet, then pass it back to avoid unlocking it again.
birthday := cipherSeed.BirthdayTime() var (
newWallet, err := loader.CreateNewWallet( birthday time.Time
password, password, cipherSeed.Entropy[:], birthday, newWallet *wallet.Wallet
) )
switch {
// A normal cipher seed was given, use the birthday encoded in
// it and create the wallet from that.
case cipherSeed != nil:
birthday = cipherSeed.BirthdayTime()
newWallet, err = loader.CreateNewWallet(
password, password, cipherSeed.Entropy[:],
birthday,
)
// No seed was given, we're importing a wallet from its extended
// private key.
case extendedKey != nil:
birthday = initMsg.ExtendedKeyBirthday
newWallet, err = loader.CreateNewWalletExtendedKey(
password, password, extendedKey, birthday,
)
default:
// The unlocker service made sure either the cipher seed
// or the extended key is set so, we shouldn't get here.
// The default case is just here for readability and
// completeness.
err = fmt.Errorf("cannot create wallet, neither seed " +
"nor extended key was given")
}
if err != nil { if err != nil {
// Don't leave the file open in case the new wallet // Don't leave the file open in case the new wallet
// could not be created for whatever reason. // could not be created for whatever reason.

View File

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wallet"
"github.com/lightningnetwork/lnd/aezeed" "github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/chanbackup"
@ -50,9 +51,19 @@ type WalletInitMsg struct {
Passphrase []byte Passphrase []byte
// WalletSeed is the deciphered cipher seed that the wallet should use // WalletSeed is the deciphered cipher seed that the wallet should use
// to initialize itself. // to initialize itself. The seed might be nil if the wallet should be
// created from an extended master root key instead.
WalletSeed *aezeed.CipherSeed WalletSeed *aezeed.CipherSeed
// WalletExtendedKey is the wallet's extended master root key that
// should be used instead of the seed, if non-nil. The extended key is
// mutually exclusive to the wallet seed, but one of both is always set.
WalletExtendedKey *hdkeychain.ExtendedKey
// ExtendedKeyBirthday is the birthday of a wallet that's being restored
// through an extended key instead of an aezeed.
ExtendedKeyBirthday time.Time
// RecoveryWindow is the address look-ahead used when restoring a seed // RecoveryWindow is the address look-ahead used when restoring a seed
// with existing funds. A recovery window zero indicates that no // with existing funds. A recovery window zero indicates that no
// recovery should be attempted, such as after the wallet's initial // recovery should be attempted, such as after the wallet's initial
@ -353,29 +364,104 @@ func (u *UnlockerService) InitWallet(ctx context.Context,
return nil, fmt.Errorf("wallet already exists") return nil, fmt.Errorf("wallet already exists")
} }
// At this point, we know that the wallet doesn't already exist. So // At this point, we know the wallet doesn't already exist so we can
// we'll map the user provided aezeed and passphrase into a decoded // prepare the message that we'll send over the channel later.
// cipher seed instance.
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], in.CipherSeedMnemonic[:])
// If we're unable to map it back into the ciphertext, then either the
// mnemonic is wrong, or the passphrase is wrong.
cipherSeed, err := mnemonic.ToCipherSeed(in.AezeedPassphrase)
if err != nil {
return nil, err
}
// With the cipher seed deciphered, and the auth service created, we'll
// now send over the wallet password and the seed. This will allow the
// daemon to initialize itself and startup.
initMsg := &WalletInitMsg{ initMsg := &WalletInitMsg{
Passphrase: password, Passphrase: password,
WalletSeed: cipherSeed,
RecoveryWindow: uint32(recoveryWindow), RecoveryWindow: uint32(recoveryWindow),
StatelessInit: in.StatelessInit, StatelessInit: in.StatelessInit,
} }
// There are two supported ways to initialize the wallet. Either from
// the aezeed or the final extended master key directly.
switch {
// Don't allow the user to specify both as that would be ambiguous.
case len(in.CipherSeedMnemonic) > 0 && len(in.ExtendedMasterKey) > 0:
return nil, fmt.Errorf("cannot specify both the cipher " +
"seed mnemonic and the extended master key")
// The aezeed is the preferred and default way of initializing a wallet.
case len(in.CipherSeedMnemonic) > 0:
// We'll map the user provided aezeed and passphrase into a
// decoded cipher seed instance.
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], in.CipherSeedMnemonic)
// If we're unable to map it back into the ciphertext, then
// either the mnemonic is wrong, or the passphrase is wrong.
cipherSeed, err := mnemonic.ToCipherSeed(in.AezeedPassphrase)
if err != nil {
return nil, err
}
initMsg.WalletSeed = cipherSeed
// To support restoring a wallet where the seed isn't known or a wallet
// created externally to lnd, we also allow the extended master key
// (xprv) to be imported directly. This is what'll be stored in the
// btcwallet database anyway.
case len(in.ExtendedMasterKey) > 0:
extendedKey, err := hdkeychain.NewKeyFromString(
in.ExtendedMasterKey,
)
if err != nil {
return nil, err
}
// The on-chain wallet of lnd is going to derive keys based on
// the BIP49/84 key derivation paths from this root key. To make
// sure we use default derivation paths, we want to avoid
// deriving keys from something other than the master key (at
// depth 0, denoted with "m/" in BIP32 notation).
if extendedKey.Depth() != 0 {
return nil, fmt.Errorf("extended master key must " +
"be at depth 0 not a child key")
}
// Because we need the master key (at depth 0), it must be an
// extended private key as the first levels of BIP49/84
// derivation paths are hardened, which isn't possible with
// extended public keys.
if !extendedKey.IsPrivate() {
return nil, fmt.Errorf("extended master key must " +
"contain private keys")
}
// To avoid using the wrong master key, we check that it was
// issued for the correct network. This will cause problems if
// someone tries to import a "new" BIP84 zprv key because with
// this we only support the "legacy" zprv prefix. But it is
// trivial to convert between those formats, as long as the user
// knows what they're doing.
if !extendedKey.IsForNet(u.netParams) {
return nil, fmt.Errorf("extended master key must be "+
"for network %s", u.netParams.Name)
}
// When importing a wallet from its extended private key we
// don't know the birthday as that information is not encoded in
// that format. We therefore must set an arbitrary date to start
// rescanning at if the user doesn't provide an explicit value
// for it. Since lnd only uses SegWit addresses, we pick the
// date of the first block that contained SegWit transactions
// (481824).
initMsg.ExtendedKeyBirthday = time.Date(
2017, time.August, 24, 1, 57, 37, 0, time.UTC,
)
if in.ExtendedMasterKeyBirthdayTimestamp != 0 {
initMsg.ExtendedKeyBirthday = time.Unix(
int64(in.ExtendedMasterKeyBirthdayTimestamp), 0,
)
}
initMsg.WalletExtendedKey = extendedKey
// No key material was set, no wallet can be created.
default:
return nil, fmt.Errorf("must either specify cipher seed " +
"mnemonic or the extended master key")
}
// Before we return the unlock payload, we'll check if we can extract // Before we return the unlock payload, we'll check if we can extract
// any channel backups to pass up to the higher level sub-system. // any channel backups to pass up to the higher level sub-system.
chansToRestore := extractChanBackups(in.ChannelBackups) chansToRestore := extractChanBackups(in.ChannelBackups)