mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-05-12 21:00:03 +02:00
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:
parent
6d70468ad5
commit
aa9435be84
38
lnd.go
38
lnd.go
@ -1492,13 +1492,17 @@ func waitForWalletPassword(cfg *Config,
|
||||
case initMsg := <-pwService.InitMsgs:
|
||||
password := initMsg.Passphrase
|
||||
cipherSeed := initMsg.WalletSeed
|
||||
extendedKey := initMsg.WalletExtendedKey
|
||||
recoveryWindow := initMsg.RecoveryWindow
|
||||
|
||||
// Before we proceed, we'll check the internal version of the
|
||||
// seed. If it's greater than the current key derivation
|
||||
// version, then we'll return an error as we don't understand
|
||||
// this.
|
||||
if cipherSeed.InternalVersion != keychain.KeyDerivationVersion {
|
||||
const latestVersion = keychain.KeyDerivationVersion
|
||||
if cipherSeed != nil &&
|
||||
cipherSeed.InternalVersion != latestVersion {
|
||||
|
||||
return nil, fmt.Errorf("invalid internal "+
|
||||
"seed version %v, current version is %v",
|
||||
cipherSeed.InternalVersion,
|
||||
@ -1515,10 +1519,36 @@ func waitForWalletPassword(cfg *Config,
|
||||
|
||||
// With the seed, we can now use the wallet loader to create
|
||||
// the wallet, then pass it back to avoid unlocking it again.
|
||||
birthday := cipherSeed.BirthdayTime()
|
||||
newWallet, err := loader.CreateNewWallet(
|
||||
password, password, cipherSeed.Entropy[:], birthday,
|
||||
var (
|
||||
birthday time.Time
|
||||
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 {
|
||||
// Don't leave the file open in case the new wallet
|
||||
// could not be created for whatever reason.
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcwallet/wallet"
|
||||
"github.com/lightningnetwork/lnd/aezeed"
|
||||
"github.com/lightningnetwork/lnd/chanbackup"
|
||||
@ -50,9 +51,19 @@ type WalletInitMsg struct {
|
||||
Passphrase []byte
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
// with existing funds. A recovery window zero indicates that no
|
||||
// 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")
|
||||
}
|
||||
|
||||
// At this point, we know that the wallet doesn't already exist. So
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
// At this point, we know the wallet doesn't already exist so we can
|
||||
// prepare the message that we'll send over the channel later.
|
||||
initMsg := &WalletInitMsg{
|
||||
Passphrase: password,
|
||||
WalletSeed: cipherSeed,
|
||||
RecoveryWindow: uint32(recoveryWindow),
|
||||
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
|
||||
// any channel backups to pass up to the higher level sub-system.
|
||||
chansToRestore := extractChanBackups(in.ChannelBackups)
|
||||
|
Loading…
x
Reference in New Issue
Block a user