diff --git a/lnd.go b/lnd.go index de30f5477..b2bf7aa98 100644 --- a/lnd.go +++ b/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. diff --git a/walletunlocker/service.go b/walletunlocker/service.go index b5d768686..5e6c5466a 100644 --- a/walletunlocker/service.go +++ b/walletunlocker/service.go @@ -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)