diff --git a/cmd/lncli/cmd_walletunlocker.go b/cmd/lncli/cmd_walletunlocker.go index e21a94fe9..7712cc095 100644 --- a/cmd/lncli/cmd_walletunlocker.go +++ b/cmd/lncli/cmd_walletunlocker.go @@ -214,16 +214,21 @@ func create(ctx *cli.Context) error { } // Next, we'll see if the user has 24-word mnemonic they want to use to - // derive a seed within the wallet. + // derive a seed within the wallet or if they want to specify an + // extended master root key (xprv) directly. var ( hasMnemonic bool + hasXprv bool ) mnemonicCheck: for { fmt.Println() fmt.Printf("Do you have an existing cipher seed " + - "mnemonic you want to use? (Enter y/n): ") + "mnemonic or extended master root key you want to " + + "use?\nEnter 'y' to use an existing cipher seed " + + "mnemonic, 'x' to use an extended master root key " + + "\nor 'n' to create a new seed (Enter y/x/n): ") reader := bufio.NewReader(os.Stdin) answer, err := reader.ReadString('\n') @@ -240,20 +245,28 @@ mnemonicCheck: case "y": hasMnemonic = true break mnemonicCheck + + case "x": + hasXprv = true + break mnemonicCheck + case "n": - hasMnemonic = false break mnemonicCheck } } - // If the user *does* have an existing seed they want to use, then - // we'll read that in directly from the terminal. + // If the user *does* have an existing seed or root key they want to + // use, then we'll read that in directly from the terminal. var ( - cipherSeedMnemonic []string - aezeedPass []byte - recoveryWindow int32 + cipherSeedMnemonic []string + aezeedPass []byte + extendedRootKey string + extendedRootKeyBirthday uint64 + recoveryWindow int32 ) - if hasMnemonic { + switch { + // Use an existing cipher seed mnemonic in the aezeed format. + case hasMnemonic: // We'll now prompt the user to enter in their 24-word // mnemonic. fmt.Printf("Input your 24-word mnemonic separated by spaces: ") @@ -288,38 +301,37 @@ mnemonicCheck: return err } - for { - fmt.Println() - fmt.Printf("Input an optional address look-ahead "+ - "used to scan for used keys (default %d): ", - defaultRecoveryWindow) - - reader := bufio.NewReader(os.Stdin) - answer, err := reader.ReadString('\n') - if err != nil { - return err - } - - fmt.Println() - - answer = strings.TrimSpace(answer) - - if len(answer) == 0 { - recoveryWindow = defaultRecoveryWindow - break - } - - lookAhead, err := strconv.Atoi(answer) - if err != nil { - fmt.Printf("Unable to parse recovery "+ - "window: %v\n", err) - continue - } - - recoveryWindow = int32(lookAhead) - break + recoveryWindow, err = askRecoveryWindow() + if err != nil { + return err } - } else { + + // Use an existing extended master root key to create the wallet. + case hasXprv: + // We'll now prompt the user to enter in their extended master + // root key. + fmt.Printf("Input your extended master root key (usually " + + "starting with xprv... on mainnet): ") + reader := bufio.NewReader(os.Stdin) + extendedRootKey, err = reader.ReadString('\n') + if err != nil { + return err + } + extendedRootKey = strings.TrimSpace(extendedRootKey) + + extendedRootKeyBirthday, err = askBirthdayTimestamp() + if err != nil { + return err + } + + recoveryWindow, err = askRecoveryWindow() + if err != nil { + return err + } + + // Neither a seed nor a master root key was specified, the user wants + // to create a new seed. + default: // Otherwise, if the user doesn't have a mnemonic that they // want to use, we'll generate a fresh one with the GenSeed // command. @@ -352,36 +364,21 @@ mnemonicCheck: // Before we initialize the wallet, we'll display the cipher seed to // the user so they can write it down. - mnemonicWords := cipherSeedMnemonic - - fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " + - "RESTORE THE WALLET!!!") - fmt.Println() - - fmt.Println("---------------BEGIN LND CIPHER SEED---------------") - - numCols := 4 - colWords := monowidthColumns(mnemonicWords, numCols) - for i := 0; i < len(colWords); i += numCols { - fmt.Printf("%2d. %3s %2d. %3s %2d. %3s %2d. %3s\n", - i+1, colWords[i], i+2, colWords[i+1], i+3, - colWords[i+2], i+4, colWords[i+3]) + if len(cipherSeedMnemonic) > 0 { + printCipherSeedWords(cipherSeedMnemonic) } - fmt.Println("---------------END LND CIPHER SEED-----------------") - - fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " + - "RESTORE THE WALLET!!!") - // With either the user's prior cipher seed, or a newly generated one, // we'll go ahead and initialize the wallet. req := &lnrpc.InitWalletRequest{ - WalletPassword: walletPassword, - CipherSeedMnemonic: cipherSeedMnemonic, - AezeedPassphrase: aezeedPass, - RecoveryWindow: recoveryWindow, - ChannelBackups: chanBackups, - StatelessInit: statelessInit, + WalletPassword: walletPassword, + CipherSeedMnemonic: cipherSeedMnemonic, + AezeedPassphrase: aezeedPass, + ExtendedMasterKey: extendedRootKey, + ExtendedMasterKeyBirthdayTimestamp: extendedRootKeyBirthday, + RecoveryWindow: recoveryWindow, + ChannelBackups: chanBackups, + StatelessInit: statelessInit, } response, err := client.InitWallet(ctxc, req) if err != nil { @@ -663,3 +660,86 @@ func storeOrPrintAdminMac(ctx *cli.Context, adminMac []byte) error { fmt.Printf("Admin macaroon: %s\n", hex.EncodeToString(adminMac)) return nil } + +func askRecoveryWindow() (int32, error) { + for { + fmt.Println() + fmt.Printf("Input an optional address look-ahead used to scan "+ + "for used keys (default %d): ", defaultRecoveryWindow) + + reader := bufio.NewReader(os.Stdin) + answer, err := reader.ReadString('\n') + if err != nil { + return 0, err + } + + fmt.Println() + + answer = strings.TrimSpace(answer) + + if len(answer) == 0 { + return defaultRecoveryWindow, nil + } + + lookAhead, err := strconv.Atoi(answer) + if err != nil { + fmt.Printf("Unable to parse recovery window: %v\n", err) + continue + } + + return int32(lookAhead), nil + } +} + +func askBirthdayTimestamp() (uint64, error) { + for { + fmt.Println() + fmt.Printf("Input an optional wallet birthday unix timestamp " + + "of first block to start scanning from (default 0): ") + + reader := bufio.NewReader(os.Stdin) + answer, err := reader.ReadString('\n') + if err != nil { + return 0, err + } + + fmt.Println() + + answer = strings.TrimSpace(answer) + + if len(answer) == 0 { + return 0, nil + } + + birthdayTimestamp, err := strconv.ParseUint(answer, 10, 64) + if err != nil { + fmt.Printf("Unable to parse birthday timestamp: %v\n", + err) + + continue + } + + return birthdayTimestamp, nil + } +} + +func printCipherSeedWords(mnemonicWords []string) { + fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " + + "RESTORE THE WALLET!!!") + fmt.Println() + + fmt.Println("---------------BEGIN LND CIPHER SEED---------------") + + numCols := 4 + colWords := monowidthColumns(mnemonicWords, numCols) + for i := 0; i < len(colWords); i += numCols { + fmt.Printf("%2d. %3s %2d. %3s %2d. %3s %2d. %3s\n", + i+1, colWords[i], i+2, colWords[i+1], i+3, + colWords[i+2], i+4, colWords[i+3]) + } + + fmt.Println("---------------END LND CIPHER SEED-----------------") + + fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " + + "RESTORE THE WALLET!!!") +} diff --git a/docs/release-notes/release-notes-0.14.0.md b/docs/release-notes/release-notes-0.14.0.md index 907bd4088..b68efef4e 100644 --- a/docs/release-notes/release-notes-0.14.0.md +++ b/docs/release-notes/release-notes-0.14.0.md @@ -43,6 +43,12 @@ for more information. * It is now possible to fund a psbt [without specifying any outputs](https://github.com/lightningnetwork/lnd/pull/5442). This option is useful for CPFP bumping of unconfirmed outputs or general utxo consolidation. +* The internal wallet can now also be created or restored by using an [extended + master root key (`xprv`) instead of an + `aezeed`](https://github.com/lightningnetwork/lnd/pull/4717) only. This allows + wallet integrators to use existing seed mechanism that might already be in + place. **It is still not supported to use the same seed/root key on multiple + `lnd` instances simultaneously** though. ## Security diff --git a/go.mod b/go.mod index bdf24e3ed..691107c67 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,15 @@ require ( github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e // indirect github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82 github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2 - github.com/btcsuite/btcd v0.21.0-beta.0.20210513141527-ee5896bad5be + github.com/btcsuite/btcd v0.22.0-beta.0.20210803133449-f5a1fb9965e4 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 github.com/btcsuite/btcutil/psbt v1.0.3-0.20210527170813-e2ba6805a890 - github.com/btcsuite/btcwallet v0.12.1-0.20210803004036-eebed51155ec + github.com/btcsuite/btcwallet v0.12.1-0.20210822222949-9b5a201c344c github.com/btcsuite/btcwallet/wallet/txauthor v1.0.2-0.20210803004036-eebed51155ec github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec - github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210803004036-eebed51155ec + github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210822222949-9b5a201c344c github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e github.com/davecgh/go-spew v1.1.1 github.com/fsnotify/fsnotify v1.4.9 // indirect diff --git a/go.sum b/go.sum index f9f35e64a..86d525ec2 100644 --- a/go.sum +++ b/go.sum @@ -72,9 +72,8 @@ github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8/go.mod h1:3J08xEfcug github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.21.0-beta.0.20201208033208-6bd4c64a54fa/go.mod h1:Sv4JPQ3/M+teHz9Bo5jBpkNcP0x6r7rdihlNL/7tTAs= -github.com/btcsuite/btcd v0.21.0-beta.0.20210426180113-7eba688b65e5/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= -github.com/btcsuite/btcd v0.21.0-beta.0.20210513141527-ee5896bad5be h1:vDD/JWWS2v4GJUG/RZE/50wT6Saerbujijd7mFqgsKI= -github.com/btcsuite/btcd v0.21.0-beta.0.20210513141527-ee5896bad5be/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= +github.com/btcsuite/btcd v0.22.0-beta.0.20210803133449-f5a1fb9965e4 h1:EmyLrldY44jDVa3dQ2iscj1S6ExuVJhRzCZBOXo93r0= +github.com/btcsuite/btcd v0.22.0-beta.0.20210803133449-f5a1fb9965e4/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= @@ -85,8 +84,8 @@ github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890/go.mod h1:0DVlH github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= github.com/btcsuite/btcutil/psbt v1.0.3-0.20210527170813-e2ba6805a890 h1:0xUNvvwJ7RjzBs4nCF+YrK28S5P/b4uHkpPxY1ovGY4= github.com/btcsuite/btcutil/psbt v1.0.3-0.20210527170813-e2ba6805a890/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= -github.com/btcsuite/btcwallet v0.12.1-0.20210803004036-eebed51155ec h1:MAAR//aKu+I7bnxmWJZqGTX7fU7abWFBRoSzX6ty8zw= -github.com/btcsuite/btcwallet v0.12.1-0.20210803004036-eebed51155ec/go.mod h1:LNhKxGlbwEGVQFjS4Qa7BgR6NipPhTd1/93Ay049pBw= +github.com/btcsuite/btcwallet v0.12.1-0.20210822222949-9b5a201c344c h1:lOUYaSw0aqCHgLk+hA2QSYXhquRXdAvT6rB3sJMXI8w= +github.com/btcsuite/btcwallet v0.12.1-0.20210822222949-9b5a201c344c/go.mod h1:SdqXKJoEEi5LJq6zU67PcKiyqF97AcUOfBfyQHC7rqQ= github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU= github.com/btcsuite/btcwallet/wallet/txauthor v1.0.1-0.20210329233242-e0607006dce6/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU= github.com/btcsuite/btcwallet/wallet/txauthor v1.0.2-0.20210803004036-eebed51155ec h1:nuO8goa4gbgDM4iegCztF7mTq8io9NT1DAMoPrEI6S4= @@ -101,8 +100,8 @@ github.com/btcsuite/btcwallet/walletdb v1.3.5/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPT github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec h1:zcAU3Ij8SmqaE+ITtS76fua2Niq7DRNp46sJRhi8PiI= github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= github.com/btcsuite/btcwallet/wtxmgr v1.3.0/go.mod h1:awQsh1n/0ZrEQ+JZgWvHeo153ubzEisf/FyNtwI0dDk= -github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210803004036-eebed51155ec h1:q2OVY/GUKpdpfaVYztVrWoTRVzyzdDQftRcgHs/6cXI= -github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210803004036-eebed51155ec/go.mod h1:UM38ixX8VwJ9qey4umf//0H3ndn5kSImFZ46V54Nd5Q= +github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210822222949-9b5a201c344c h1:owWPexGfK4eSK4/Zy+XK2lET5qsnW7FRAc8OCOdD0Fg= +github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210822222949-9b5a201c344c/go.mod h1:UM38ixX8VwJ9qey4umf//0H3ndn5kSImFZ46V54Nd5Q= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc= 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/lnrpc/walletunlocker.pb.go b/lnrpc/walletunlocker.pb.go index c908e3ef2..2f02d2add 100644 --- a/lnrpc/walletunlocker.pb.go +++ b/lnrpc/walletunlocker.pb.go @@ -189,6 +189,29 @@ type InitWalletRequest struct { //admin macaroon returned in the response MUST be stored by the caller of the //RPC as otherwise all access to the daemon will be lost! StatelessInit bool `protobuf:"varint,6,opt,name=stateless_init,json=statelessInit,proto3" json:"stateless_init,omitempty"` + // + //extended_master_key is an alternative to specifying cipher_seed_mnemonic and + //aezeed_passphrase. Instead of deriving the master root key from the entropy + //of an aezeed cipher seed, the given extended master root key is used + //directly as the wallet's master key. This allows users to import/use a + //master key from another wallet. When doing so, lnd still uses its default + //SegWit only (BIP49/84) derivation paths and funds from custom/non-default + //derivation paths will not automatically appear in the on-chain wallet. Using + //an 'xprv' instead of an aezeed also has the disadvantage that the wallet's + //birthday is not known as that is an information that's only encoded in the + //aezeed, not the xprv. Therefore a birthday needs to be specified in + //extended_master_key_birthday_timestamp or a "safe" default value will be + //used. + ExtendedMasterKey string `protobuf:"bytes,7,opt,name=extended_master_key,json=extendedMasterKey,proto3" json:"extended_master_key,omitempty"` + // + //extended_master_key_birthday_timestamp is the optional unix timestamp in + //seconds to use as the wallet's birthday when using an extended master key + //to restore the wallet. lnd will only start scanning for funds in blocks that + //are after the birthday which can speed up the process significantly. If the + //birthday is not known, this should be left at its default value of 0 in + //which case lnd will start scanning from the first SegWit block (481824 on + //mainnet). + ExtendedMasterKeyBirthdayTimestamp uint64 `protobuf:"varint,8,opt,name=extended_master_key_birthday_timestamp,json=extendedMasterKeyBirthdayTimestamp,proto3" json:"extended_master_key_birthday_timestamp,omitempty"` } func (x *InitWalletRequest) Reset() { @@ -265,6 +288,20 @@ func (x *InitWalletRequest) GetStatelessInit() bool { return false } +func (x *InitWalletRequest) GetExtendedMasterKey() string { + if x != nil { + return x.ExtendedMasterKey + } + return "" +} + +func (x *InitWalletRequest) GetExtendedMasterKeyBirthdayTimestamp() uint64 { + if x != nil { + return x.ExtendedMasterKeyBirthdayTimestamp + } + return 0 +} + type InitWalletResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -605,8 +642,8 @@ var file_walletunlocker_proto_rawDesc = []byte{ 0x09, 0x52, 0x12, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x65, 0x65, 0x64, 0x4d, 0x6e, 0x65, 0x6d, 0x6f, 0x6e, 0x69, 0x63, 0x12, 0x27, 0x0a, 0x0f, 0x65, 0x6e, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, - 0x65, 0x6e, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x65, 0x64, 0x53, 0x65, 0x65, 0x64, 0x22, 0xaf, - 0x02, 0x0a, 0x11, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, + 0x65, 0x6e, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x65, 0x64, 0x53, 0x65, 0x65, 0x64, 0x22, 0xb3, + 0x03, 0x0a, 0x11, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x30, 0x0a, @@ -625,63 +662,71 @@ var file_walletunlocker_proto_rawDesc = []byte{ 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x74, 0x61, 0x74, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x49, 0x6e, 0x69, 0x74, - 0x22, 0x3b, 0x0a, 0x12, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, - 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, - 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x22, 0xd2, 0x01, - 0x0a, 0x13, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, - 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x27, - 0x0a, 0x0f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x77, 0x69, 0x6e, 0x64, 0x6f, - 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, - 0x79, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0f, 0x63, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, - 0x6b, 0x75, 0x70, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x0e, 0x63, 0x68, 0x61, - 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x74, 0x61, 0x74, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x49, 0x6e, - 0x69, 0x74, 0x22, 0x16, 0x0a, 0x14, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x57, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xbf, 0x01, 0x0a, 0x15, 0x43, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, - 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, - 0x21, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6e, 0x65, 0x77, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x5f, - 0x69, 0x6e, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x6c, 0x65, 0x73, 0x73, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x31, 0x0a, 0x15, 0x6e, 0x65, 0x77, - 0x5f, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x6b, - 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x6e, 0x65, 0x77, 0x4d, 0x61, 0x63, - 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x52, 0x6f, 0x6f, 0x74, 0x4b, 0x65, 0x79, 0x22, 0x3f, 0x0a, 0x16, - 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, - 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, - 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x32, 0xa5, 0x02, - 0x0a, 0x0e, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x72, - 0x12, 0x38, 0x0a, 0x07, 0x47, 0x65, 0x6e, 0x53, 0x65, 0x65, 0x64, 0x12, 0x15, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x6e, 0x53, 0x65, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x6e, 0x53, 0x65, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0a, 0x49, 0x6e, - 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x12, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x57, - 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, - 0x0c, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x12, 0x1a, 0x2e, + 0x12, 0x2e, 0x0a, 0x13, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x6d, 0x61, 0x73, + 0x74, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, + 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x4b, 0x65, 0x79, + 0x12, 0x52, 0x0a, 0x26, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x6d, 0x61, 0x73, + 0x74, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x62, 0x69, 0x72, 0x74, 0x68, 0x64, 0x61, 0x79, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x22, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4d, 0x61, 0x73, 0x74, 0x65, 0x72, + 0x4b, 0x65, 0x79, 0x42, 0x69, 0x72, 0x74, 0x68, 0x64, 0x61, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x22, 0x3b, 0x0a, 0x12, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x64, + 0x6d, 0x69, 0x6e, 0x5f, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0d, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, + 0x6e, 0x22, 0xd2, 0x01, 0x0a, 0x13, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x57, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x77, + 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x72, 0x65, 0x63, + 0x6f, 0x76, 0x65, 0x72, 0x79, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0f, 0x63, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, + 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, + 0x0e, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, + 0x25, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x5f, 0x69, 0x6e, 0x69, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x74, 0x61, 0x74, 0x65, 0x6c, 0x65, + 0x73, 0x73, 0x49, 0x6e, 0x69, 0x74, 0x22, 0x16, 0x0a, 0x14, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, + 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xbf, + 0x01, 0x0a, 0x15, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x75, 0x72, 0x72, + 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6e, 0x65, 0x77, 0x50, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x6c, + 0x65, 0x73, 0x73, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x31, 0x0a, + 0x15, 0x6e, 0x65, 0x77, 0x5f, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x5f, 0x72, 0x6f, + 0x6f, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x6e, 0x65, + 0x77, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x52, 0x6f, 0x6f, 0x74, 0x4b, 0x65, 0x79, + 0x22, 0x3f, 0x0a, 0x16, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x64, + 0x6d, 0x69, 0x6e, 0x5f, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0d, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, + 0x6e, 0x32, 0xa5, 0x02, 0x0a, 0x0e, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x55, 0x6e, 0x6c, 0x6f, + 0x63, 0x6b, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x07, 0x47, 0x65, 0x6e, 0x53, 0x65, 0x65, 0x64, 0x12, + 0x15, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x6e, 0x53, 0x65, 0x65, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, + 0x65, 0x6e, 0x53, 0x65, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, + 0x0a, 0x0a, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x12, 0x18, 0x2e, 0x6c, + 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, + 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x47, 0x0a, 0x0c, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x57, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, + 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x57, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, - 0x63, 0x2e, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x0e, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1c, 0x2e, 0x6c, + 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, + 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, + 0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, + 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/lnrpc/walletunlocker.proto b/lnrpc/walletunlocker.proto index f8fe914d7..730b323c5 100644 --- a/lnrpc/walletunlocker.proto +++ b/lnrpc/walletunlocker.proto @@ -149,6 +149,33 @@ message InitWalletRequest { RPC as otherwise all access to the daemon will be lost! */ bool stateless_init = 6; + + /* + extended_master_key is an alternative to specifying cipher_seed_mnemonic and + aezeed_passphrase. Instead of deriving the master root key from the entropy + of an aezeed cipher seed, the given extended master root key is used + directly as the wallet's master key. This allows users to import/use a + master key from another wallet. When doing so, lnd still uses its default + SegWit only (BIP49/84) derivation paths and funds from custom/non-default + derivation paths will not automatically appear in the on-chain wallet. Using + an 'xprv' instead of an aezeed also has the disadvantage that the wallet's + birthday is not known as that is an information that's only encoded in the + aezeed, not the xprv. Therefore a birthday needs to be specified in + extended_master_key_birthday_timestamp or a "safe" default value will be + used. + */ + string extended_master_key = 7; + + /* + extended_master_key_birthday_timestamp is the optional unix timestamp in + seconds to use as the wallet's birthday when using an extended master key + to restore the wallet. lnd will only start scanning for funds in blocks that + are after the birthday which can speed up the process significantly. If the + birthday is not known, this should be left at its default value of 0 in + which case lnd will start scanning from the first SegWit block (481824 on + mainnet). + */ + uint64 extended_master_key_birthday_timestamp = 8; } message InitWalletResponse { /* diff --git a/lnrpc/walletunlocker.swagger.json b/lnrpc/walletunlocker.swagger.json index b93505c74..eeb3710e6 100644 --- a/lnrpc/walletunlocker.swagger.json +++ b/lnrpc/walletunlocker.swagger.json @@ -300,6 +300,15 @@ "stateless_init": { "type": "boolean", "title": "stateless_init is an optional argument instructing the daemon NOT to create\nany *.macaroon files in its filesystem. If this parameter is set, then the\nadmin macaroon returned in the response MUST be stored by the caller of the\nRPC as otherwise all access to the daemon will be lost!" + }, + "extended_master_key": { + "type": "string", + "description": "extended_master_key is an alternative to specifying cipher_seed_mnemonic and\naezeed_passphrase. Instead of deriving the master root key from the entropy\nof an aezeed cipher seed, the given extended master root key is used\ndirectly as the wallet's master key. This allows users to import/use a\nmaster key from another wallet. When doing so, lnd still uses its default\nSegWit only (BIP49/84) derivation paths and funds from custom/non-default\nderivation paths will not automatically appear in the on-chain wallet. Using\nan 'xprv' instead of an aezeed also has the disadvantage that the wallet's\nbirthday is not known as that is an information that's only encoded in the\naezeed, not the xprv. Therefore a birthday needs to be specified in\nextended_master_key_birthday_timestamp or a \"safe\" default value will be\nused." + }, + "extended_master_key_birthday_timestamp": { + "type": "string", + "format": "uint64", + "description": "extended_master_key_birthday_timestamp is the optional unix timestamp in\nseconds to use as the wallet's birthday when using an extended master key\nto restore the wallet. lnd will only start scanning for funds in blocks that\nare after the birthday which can speed up the process significantly. If the\nbirthday is not known, this should be left at its default value of 0 in\nwhich case lnd will start scanning from the first SegWit block (481824 on\nmainnet)." } } }, diff --git a/lntest/harness.go b/lntest/harness.go index 061f77beb..004af9dd1 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -426,7 +426,7 @@ func (n *NetworkHarness) newNodeWithSeed(name string, extraArgs []string, // will finish initializing the LightningClient such that the HarnessNode can // be used for regular rpc operations. func (n *NetworkHarness) RestoreNodeWithSeed(name string, extraArgs []string, - password []byte, mnemonic []string, recoveryWindow int32, + password []byte, mnemonic []string, rootKey string, recoveryWindow int32, chanBackups *lnrpc.ChanBackupSnapshot, opts ...NodeOption) (*HarnessNode, error) { @@ -441,6 +441,7 @@ func (n *NetworkHarness) RestoreNodeWithSeed(name string, extraArgs []string, WalletPassword: password, CipherSeedMnemonic: mnemonic, AezeedPassphrase: password, + ExtendedMasterKey: rootKey, RecoveryWindow: recoveryWindow, ChannelBackups: chanBackups, } diff --git a/lntest/itest/lnd_channel_backup_test.go b/lntest/itest/lnd_channel_backup_test.go index d7a46755a..d1d42c2e1 100644 --- a/lntest/itest/lnd_channel_backup_test.go +++ b/lntest/itest/lnd_channel_backup_test.go @@ -122,8 +122,8 @@ func testChannelBackupRestore(net *lntest.NetworkHarness, t *harnessTest) { // obtained above. return func() (*lntest.HarnessNode, error) { return net.RestoreNodeWithSeed( - "dave", nil, password, - mnemonic, 1000, backupSnapshot, + "dave", nil, password, mnemonic, + "", 1000, backupSnapshot, copyPorts(oldNode), ) }, nil @@ -159,8 +159,8 @@ func testChannelBackupRestore(net *lntest.NetworkHarness, t *harnessTest) { // restart it again using Unlock. return func() (*lntest.HarnessNode, error) { newNode, err := net.RestoreNodeWithSeed( - "dave", nil, password, - mnemonic, 1000, nil, + "dave", nil, password, mnemonic, + "", 1000, nil, copyPorts(oldNode), ) if err != nil { @@ -208,7 +208,8 @@ func testChannelBackupRestore(net *lntest.NetworkHarness, t *harnessTest) { return func() (*lntest.HarnessNode, error) { newNode, err := net.RestoreNodeWithSeed( "dave", nil, password, mnemonic, - 1000, nil, copyPorts(oldNode), + "", 1000, nil, + copyPorts(oldNode), ) if err != nil { return nil, fmt.Errorf("unable to "+ @@ -1309,7 +1310,7 @@ func chanRestoreViaRPC(net *lntest.NetworkHarness, password []byte, return func() (*lntest.HarnessNode, error) { newNode, err := net.RestoreNodeWithSeed( - "dave", nil, password, mnemonic, 1000, nil, + "dave", nil, password, mnemonic, "", 1000, nil, copyPorts(oldNode), ) if err != nil { diff --git a/lntest/itest/lnd_recovery_test.go b/lntest/itest/lnd_recovery_test.go index fecab5acb..af56c7a6f 100644 --- a/lntest/itest/lnd_recovery_test.go +++ b/lntest/itest/lnd_recovery_test.go @@ -5,9 +5,12 @@ import ( "math" "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/lightningnetwork/lnd/aezeed" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/stretchr/testify/require" ) // testGetRecoveryInfo checks whether lnd gives the right information about @@ -34,7 +37,8 @@ func testGetRecoveryInfo(net *lntest.NetworkHarness, t *harnessTest) { // Restore Carol, passing in the password, mnemonic, and // desired recovery window. node, err := net.RestoreNodeWithSeed( - "Carol", nil, password, mnemonic, recoveryWindow, nil, + "Carol", nil, password, mnemonic, "", recoveryWindow, + nil, ) if err != nil { t.Fatalf("unable to restore node: %v", err) @@ -124,11 +128,14 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) { carol, mnemonic, _, err := net.NewNodeWithSeed( "Carol", nil, password, false, ) - if err != nil { - t.Fatalf("unable to create node with seed; %v", err) - } + require.NoError(t.t, err) shutdownAndAssert(net, t, carol) + // As long as the mnemonic is non-nil and the extended key is empty, the + // closure below will always restore the node from the seed. The tests + // need to manually overwrite this value to change that behavior. + rootKey := "" + // Create a closure for testing the recovery of Carol's wallet. This // method takes the expected value of Carol's balance when using the // given recovery window. Additionally, the caller can specify an action @@ -136,14 +143,15 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) { restoreCheckBalance := func(expAmount int64, expectedNumUTXOs uint32, recoveryWindow int32, fn func(*lntest.HarnessNode)) { + t.t.Helper() + // Restore Carol, passing in the password, mnemonic, and // desired recovery window. node, err := net.RestoreNodeWithSeed( - "Carol", nil, password, mnemonic, recoveryWindow, nil, + "Carol", nil, password, mnemonic, rootKey, + recoveryWindow, nil, ) - if err != nil { - t.Fatalf("unable to restore node: %v", err) - } + require.NoError(t.t, err) // Query carol for her current wallet balance, and also that we // gain the expected number of UTXOs. @@ -155,10 +163,7 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) { req := &lnrpc.WalletBalanceRequest{} ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) resp, err := node.WalletBalance(ctxt, req) - if err != nil { - t.Fatalf("unable to query wallet balance: %v", - err) - } + require.NoError(t.t, err) currBalance = resp.ConfirmedBalance utxoReq := &lnrpc.ListUnspentRequest{ @@ -166,9 +171,7 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) { } ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) utxoResp, err := node.ListUnspent(ctxt, utxoReq) - if err != nil { - t.Fatalf("unable to query utxos: %v", err) - } + require.NoError(t.t, err) currNumUTXOs = uint32(len(utxoResp.Utxos)) // Verify that Carol's balance and number of UTXOs @@ -206,6 +209,8 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) { // behavior to both default P2WKH and NP2WKH scopes. skipAndSend := func(nskip int) func(*lntest.HarnessNode) { return func(node *lntest.HarnessNode) { + t.t.Helper() + newP2WKHAddrReq := &lnrpc.NewAddressRequest{ Type: AddrTypeWitnessPubkeyHash, } @@ -218,17 +223,11 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) { for i := 0; i < nskip; i++ { ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) _, err = node.NewAddress(ctxt, newP2WKHAddrReq) - if err != nil { - t.Fatalf("unable to generate new "+ - "p2wkh address: %v", err) - } + require.NoError(t.t, err) ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) _, err = node.NewAddress(ctxt, newNP2WKHAddrReq) - if err != nil { - t.Fatalf("unable to generate new "+ - "np2wkh address: %v", err) - } + require.NoError(t.t, err) } // Send one BTC to the next P2WKH address. @@ -291,28 +290,22 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) { const minerAmt = 5 * btcutil.SatoshiPerBitcoin const finalBalance = 6 * btcutil.SatoshiPerBitcoin promptChangeAddr := func(node *lntest.HarnessNode) { + t.t.Helper() + minerAddr, err := net.Miner.NewAddress() - if err != nil { - t.Fatalf("unable to create new miner address: %v", err) - } + require.NoError(t.t, err) ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) resp, err := node.SendCoins(ctxt, &lnrpc.SendCoinsRequest{ Addr: minerAddr.String(), Amount: minerAmt, }) - if err != nil { - t.Fatalf("unable to send coins to miner: %v", err) - } + require.NoError(t.t, err) txid, err := waitForTxInMempool( net.Miner.Client, minerMempoolTimeout, ) - if err != nil { - t.Fatalf("transaction not found in mempool: %v", err) - } - if resp.Txid != txid.String() { - t.Fatalf("txid mismatch: %v vs %v", resp.Txid, - txid.String()) - } + require.NoError(t.t, err) + require.Equal(t.t, txid.String(), resp.Txid) + block := mineBlocks(t, net, 1, 1)[0] assertTxInBlock(t, block, txid) } @@ -323,4 +316,19 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) { // only have one UTXO present (the change output) of 6 - 5 - fee BTC. const fee = 27750 restoreCheckBalance(finalBalance-minerAmt-fee, 1, 21, nil) + + // Last of all, make sure we can also restore a node from the extended + // master root key directly instead of the seed. + var seedMnemonic aezeed.Mnemonic + copy(seedMnemonic[:], mnemonic) + cipherSeed, err := seedMnemonic.ToCipherSeed(password) + require.NoError(t.t, err) + extendedRootKey, err := hdkeychain.NewMaster( + cipherSeed.Entropy[:], harnessNetParams, + ) + require.NoError(t.t, err) + rootKey = extendedRootKey.String() + mnemonic = nil + + restoreCheckBalance(finalBalance-minerAmt-fee, 1, 21, nil) } 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)