package walletunlocker

import (
	"context"
	"crypto/rand"
	"errors"
	"fmt"
	"os"
	"time"

	"github.com/btcsuite/btcd/btcutil/hdkeychain"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcwallet/waddrmgr"
	"github.com/btcsuite/btcwallet/wallet"
	"github.com/lightningnetwork/lnd/aezeed"
	"github.com/lightningnetwork/lnd/chanbackup"
	"github.com/lightningnetwork/lnd/keychain"
	"github.com/lightningnetwork/lnd/kvdb"
	"github.com/lightningnetwork/lnd/lnrpc"
	"github.com/lightningnetwork/lnd/lnwallet"
	"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
	"github.com/lightningnetwork/lnd/macaroons"
)

var (
	// ErrUnlockTimeout signals that we did not get the expected unlock
	// message before the timeout occurred.
	ErrUnlockTimeout = errors.New("got no unlock message before timeout")
)

// WalletUnlockParams holds the variables used to parameterize the unlocking of
// lnd's wallet after it has already been created.
type WalletUnlockParams struct {
	// Password is the public and private wallet passphrase.
	Password []byte

	// Birthday specifies the approximate time that this wallet was created.
	// This is used to bound any rescans on startup.
	Birthday time.Time

	// RecoveryWindow specifies the address lookahead when entering recovery
	// mode. A recovery will be attempted if this value is non-zero.
	RecoveryWindow uint32

	// Wallet is the loaded and unlocked Wallet. This is returned
	// from the unlocker service to avoid it being unlocked twice (once in
	// the unlocker service to check if the password is correct and again
	// later when lnd actually uses it). Because unlocking involves scrypt
	// which is resource intensive, we want to avoid doing it twice.
	Wallet *wallet.Wallet

	// ChansToRestore a set of static channel backups that should be
	// restored before the main server instance starts up.
	ChansToRestore ChannelsToRecover

	// UnloadWallet is a function for unloading the wallet, which should
	// be called on shutdown.
	UnloadWallet func() error

	// StatelessInit signals that the user requested the daemon to be
	// initialized stateless, which means no unencrypted macaroons should be
	// written to disk.
	StatelessInit bool

	// MacResponseChan is the channel for sending back the admin macaroon to
	// the WalletUnlocker service.
	MacResponseChan chan []byte

	// MacRootKey is the 32 byte macaroon root key specified by the user
	// during wallet initialization.
	MacRootKey []byte
}

// ChannelsToRecover wraps any set of packed (serialized+encrypted) channel
// back ups together. These can be passed in when unlocking the wallet, or
// creating a new wallet for the first time with an existing seed.
type ChannelsToRecover struct {
	// PackedMultiChanBackup is an encrypted and serialized multi-channel
	// backup.
	PackedMultiChanBackup chanbackup.PackedMulti

	// PackedSingleChanBackups is a series of encrypted and serialized
	// single-channel backup for one or more channels.
	PackedSingleChanBackups chanbackup.PackedSingles
}

// WalletInitMsg is a message sent by the UnlockerService when a user wishes to
// set up the internal wallet for the first time. The user MUST provide a
// passphrase, but is also able to provide their own source of entropy. If
// provided, then this source of entropy will be used to generate the wallet's
// HD seed. Otherwise, the wallet will generate one itself.
type WalletInitMsg struct {
	// Passphrase is the passphrase that will be used to encrypt the wallet
	// itself. This MUST be at least 8 characters.
	Passphrase []byte

	// WalletSeed is the deciphered cipher seed that the wallet should use
	// 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

	// WatchOnlyAccounts is a map of scoped account extended public keys
	// that should be imported to create a watch-only wallet.
	WatchOnlyAccounts map[waddrmgr.ScopedIndex]*hdkeychain.ExtendedKey

	// WatchOnlyBirthday is the birthday of the master root key the above
	// watch-only account xpubs were derived from.
	WatchOnlyBirthday time.Time

	// WatchOnlyMasterFingerprint is the fingerprint of the master root key
	// the above watch-only account xpubs were derived from.
	WatchOnlyMasterFingerprint uint32

	// 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
	// creation.
	RecoveryWindow uint32

	// ChanBackups a set of static channel backups that should be received
	// after the wallet has been initialized.
	ChanBackups ChannelsToRecover

	// StatelessInit signals that the user requested the daemon to be
	// initialized stateless, which means no unencrypted macaroons should be
	// written to disk.
	StatelessInit bool

	// MacRootKey is the 32 byte macaroon root key specified by the user
	// during wallet initialization.
	MacRootKey []byte
}

// WalletUnlockMsg is a message sent by the UnlockerService when a user wishes
// to unlock the internal wallet after initial setup. The user can optionally
// specify a recovery window, which will resume an interrupted rescan for used
// addresses.
type WalletUnlockMsg struct {
	// Passphrase is the passphrase that will be used to encrypt the wallet
	// itself. This MUST be at least 8 characters.
	Passphrase []byte

	// 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
	// creation, but before any addresses have been created.
	RecoveryWindow uint32

	// Wallet is the loaded and unlocked Wallet. This is returned through
	// the channel to avoid it being unlocked twice (once to check if the
	// password is correct, here in the WalletUnlocker and again later when
	// lnd actually uses it). Because unlocking involves scrypt which is
	// resource intensive, we want to avoid doing it twice.
	Wallet *wallet.Wallet

	// ChanBackups a set of static channel backups that should be received
	// after the wallet has been unlocked.
	ChanBackups ChannelsToRecover

	// UnloadWallet is a function for unloading the wallet, which should
	// be called on shutdown.
	UnloadWallet func() error

	// StatelessInit signals that the user requested the daemon to be
	// initialized stateless, which means no unencrypted macaroons should be
	// written to disk.
	StatelessInit bool
}

// UnlockerService implements the WalletUnlocker service used to provide lnd
// with a password for wallet encryption at startup. Additionally, during
// initial setup, users can provide their own source of entropy which will be
// used to generate the seed that's ultimately used within the wallet.
type UnlockerService struct {
	// Required by the grpc-gateway/v2 library for forward compatibility.
	lnrpc.UnimplementedWalletUnlockerServer

	// InitMsgs is a channel that carries all wallet init messages.
	InitMsgs chan *WalletInitMsg

	// UnlockMsgs is a channel where unlock parameters provided by the rpc
	// client to be used to unlock and decrypt an existing wallet will be
	// sent.
	UnlockMsgs chan *WalletUnlockMsg

	// MacResponseChan is the channel for sending back the admin macaroon to
	// the WalletUnlocker service.
	MacResponseChan chan []byte

	netParams *chaincfg.Params

	// macaroonFiles is the path to the three generated macaroons with
	// different access permissions. These might not exist in a stateless
	// initialization of lnd.
	macaroonFiles []string

	// resetWalletTransactions indicates that the wallet state should be
	// reset on unlock to force a full chain rescan.
	resetWalletTransactions bool

	// LoaderOpts holds the functional options for the wallet loader.
	loaderOpts []btcwallet.LoaderOption

	// macaroonDB is an instance of a database backend that stores all
	// macaroon root keys. This will be nil on initialization and must be
	// set using the SetMacaroonDB method as soon as it's available.
	macaroonDB kvdb.Backend
}

// New creates and returns a new UnlockerService.
func New(params *chaincfg.Params, macaroonFiles []string,
	resetWalletTransactions bool,
	loaderOpts []btcwallet.LoaderOption) *UnlockerService {

	return &UnlockerService{
		InitMsgs:   make(chan *WalletInitMsg, 1),
		UnlockMsgs: make(chan *WalletUnlockMsg, 1),

		// Make sure we buffer the channel is buffered so the main lnd
		// goroutine isn't blocking on writing to it.
		MacResponseChan:         make(chan []byte, 1),
		netParams:               params,
		macaroonFiles:           macaroonFiles,
		resetWalletTransactions: resetWalletTransactions,
		loaderOpts:              loaderOpts,
	}
}

// SetLoaderOpts can be used to inject wallet loader options after the unlocker
// service has been hooked to the main RPC server.
func (u *UnlockerService) SetLoaderOpts(loaderOpts []btcwallet.LoaderOption) {
	u.loaderOpts = loaderOpts
}

// SetMacaroonDB can be used to inject the macaroon database after the unlocker
// service has been hooked to the main RPC server.
func (u *UnlockerService) SetMacaroonDB(macaroonDB kvdb.Backend) {
	u.macaroonDB = macaroonDB
}

func (u *UnlockerService) newLoader(recoveryWindow uint32) (*wallet.Loader,
	error) {

	return btcwallet.NewWalletLoader(
		u.netParams, recoveryWindow, u.loaderOpts...,
	)
}

// WalletExists returns whether a wallet exists on the file path the
// UnlockerService is using.
func (u *UnlockerService) WalletExists() (bool, error) {
	loader, err := u.newLoader(0)
	if err != nil {
		return false, err
	}
	return loader.WalletExists()
}

// GenSeed is the first method that should be used to instantiate a new lnd
// instance. This method allows a caller to generate a new aezeed cipher seed
// given an optional passphrase. If provided, the passphrase will be necessary
// to decrypt the cipherseed to expose the internal wallet seed.
//
// Once the cipherseed is obtained and verified by the user, the InitWallet
// method should be used to commit the newly generated seed, and create the
// wallet.
func (u *UnlockerService) GenSeed(_ context.Context,
	in *lnrpc.GenSeedRequest) (*lnrpc.GenSeedResponse, error) {

	// Before we start, we'll ensure that the wallet hasn't already created
	// so we don't show a *new* seed to the user if one already exists.
	loader, err := u.newLoader(0)
	if err != nil {
		return nil, err
	}

	walletExists, err := loader.WalletExists()
	if err != nil {
		return nil, err
	}
	if walletExists {
		return nil, fmt.Errorf("wallet already exists")
	}

	var entropy [aezeed.EntropySize]byte

	switch {
	// If the user provided any entropy, then we'll make sure it's sized
	// properly.
	case len(in.SeedEntropy) != 0 && len(in.SeedEntropy) != aezeed.EntropySize:
		return nil, fmt.Errorf("incorrect entropy length: expected "+
			"16 bytes, instead got %v bytes", len(in.SeedEntropy))

	// If the user provided the correct number of bytes, then we'll copy it
	// over into our buffer for usage.
	case len(in.SeedEntropy) == aezeed.EntropySize:
		copy(entropy[:], in.SeedEntropy[:])

	// Otherwise, we'll generate a fresh new set of bytes to use as entropy
	// to generate the seed.
	default:
		if _, err := rand.Read(entropy[:]); err != nil {
			return nil, err
		}
	}

	// Now that we have our set of entropy, we'll create a new cipher seed
	// instance.
	//
	cipherSeed, err := aezeed.New(
		keychain.CurrentKeyDerivationVersion, &entropy, time.Now(),
	)
	if err != nil {
		return nil, err
	}

	// With our raw cipher seed obtained, we'll convert it into an encoded
	// mnemonic using the user specified pass phrase.
	mnemonic, err := cipherSeed.ToMnemonic(in.AezeedPassphrase)
	if err != nil {
		return nil, err
	}

	// Additionally, we'll also obtain the raw enciphered cipher seed as
	// well to return to the user.
	encipheredSeed, err := cipherSeed.Encipher(in.AezeedPassphrase)
	if err != nil {
		return nil, err
	}

	return &lnrpc.GenSeedResponse{
		CipherSeedMnemonic: mnemonic[:],
		EncipheredSeed:     encipheredSeed[:],
	}, nil
}

// extractChanBackups is a helper function that extracts the set of channel
// backups from the proto into a format that we'll pass to higher level
// sub-systems.
func extractChanBackups(chanBackups *lnrpc.ChanBackupSnapshot) *ChannelsToRecover {
	// If there aren't any populated channel backups, then we can exit
	// early as there's nothing to extract.
	if chanBackups == nil || (chanBackups.SingleChanBackups == nil &&
		chanBackups.MultiChanBackup == nil) {
		return nil
	}

	// Now that we know there's at least a single back up populated, we'll
	// extract the multi-chan backup (if it's there).
	var backups ChannelsToRecover
	if chanBackups.MultiChanBackup != nil {
		multiBackup := chanBackups.MultiChanBackup
		backups.PackedMultiChanBackup = multiBackup.MultiChanBackup
	}

	if chanBackups.SingleChanBackups == nil {
		return &backups
	}

	// Finally, we can extract all the single chan backups as well.
	for _, backup := range chanBackups.SingleChanBackups.ChanBackups {
		singleChanBackup := backup.ChanBackup

		backups.PackedSingleChanBackups = append(
			backups.PackedSingleChanBackups, singleChanBackup,
		)
	}

	return &backups
}

// InitWallet is used when lnd is starting up for the first time to fully
// initialize the daemon and its internal wallet. At the very least a wallet
// password must be provided. This will be used to encrypt sensitive material
// on disk.
//
// In the case of a recovery scenario, the user can also specify their aezeed
// mnemonic and passphrase. If set, then the daemon will use this prior state
// to initialize its internal wallet.
//
// Alternatively, this can be used along with the GenSeed RPC to obtain a
// seed, then present it to the user. Once it has been verified by the user,
// the seed can be fed into this RPC in order to commit the new wallet.
func (u *UnlockerService) InitWallet(ctx context.Context,
	in *lnrpc.InitWalletRequest) (*lnrpc.InitWalletResponse, error) {

	// Make sure the password meets our constraints.
	password := in.WalletPassword
	if err := ValidatePassword(password); err != nil {
		return nil, err
	}

	// Require that the recovery window be non-negative.
	recoveryWindow := in.RecoveryWindow
	if recoveryWindow < 0 {
		return nil, fmt.Errorf("recovery window %d must be "+
			"non-negative", recoveryWindow)
	}

	// Ensure that the macaroon root key is *exactly* 32-bytes.
	macaroonRootKey := in.MacaroonRootKey
	if len(macaroonRootKey) > 0 &&
		len(macaroonRootKey) != macaroons.RootKeyLen {

		return nil, fmt.Errorf("macaroon root key must be exactly "+
			"%v bytes, is instead %v",
			macaroons.RootKeyLen, len(macaroonRootKey),
		)
	}

	// We'll then open up the directory that will be used to store the
	// wallet's files so we can check if the wallet already exists.
	loader, err := u.newLoader(uint32(recoveryWindow))
	if err != nil {
		return nil, err
	}

	walletExists, err := loader.WalletExists()
	if err != nil {
		return nil, err
	}

	// If the wallet already exists, then we'll exit early as we can't
	// create the wallet if it already exists!
	if walletExists {
		return nil, fmt.Errorf("wallet already exists")
	}

	// 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,
		RecoveryWindow: uint32(recoveryWindow),
		StatelessInit:  in.StatelessInit,
		MacRootKey:     macaroonRootKey,
	}

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

	// The third option for creating a wallet is the watch-only mode:
	// Instead of providing the master root key directly, each individual
	// account is passed as an extended public key only. Because of the
	// hardened derivation path up to the account (depth 3), it is not
	// possible to create a master root extended _public_ key. Therefore, an
	// xpub must be derived and passed into the unlocker for _every_ account
	// lnd expects.
	case in.WatchOnly != nil && len(in.WatchOnly.Accounts) > 0:
		initMsg.WatchOnlyAccounts = make(
			map[waddrmgr.ScopedIndex]*hdkeychain.ExtendedKey,
			len(in.WatchOnly.Accounts),
		)

		for _, acct := range in.WatchOnly.Accounts {
			scopedIndex := waddrmgr.ScopedIndex{
				Scope: waddrmgr.KeyScope{
					Purpose: acct.Purpose,
					Coin:    acct.CoinType,
				},
				Index: acct.Account,
			}
			acctKey, err := hdkeychain.NewKeyFromString(acct.Xpub)
			if err != nil {
				return nil, fmt.Errorf("error parsing xpub "+
					"%v: %v", acct.Xpub, err)
			}

			// Just to make sure the user is doing the right thing,
			// we expect the public key to be at derivation depth
			// three (which is the account level) and the key not to
			// contain any private key material.
			if acctKey.Depth() != 3 {
				return nil, fmt.Errorf("xpub must be at " +
					"depth 3")
			}
			if acctKey.IsPrivate() {
				return nil, fmt.Errorf("xpub is not really " +
					"an xpub, contains private key")
			}

			initMsg.WatchOnlyAccounts[scopedIndex] = acctKey
		}

		// When importing a wallet from its extended public keys 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.WatchOnlyBirthday = time.Date(
			2017, time.August, 24, 1, 57, 37, 0, time.UTC,
		)
		if in.WatchOnly.MasterKeyBirthdayTimestamp != 0 {
			initMsg.WatchOnlyBirthday = time.Unix(
				int64(in.WatchOnly.MasterKeyBirthdayTimestamp),
				0,
			)
		}

	// 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)
	if chansToRestore != nil {
		initMsg.ChanBackups = *chansToRestore
	}

	// Deliver the initialization message back to the main daemon.
	select {
	case u.InitMsgs <- initMsg:
		// We need to read from the channel to let the daemon continue
		// its work and to get the admin macaroon. Once the response
		// arrives, we directly forward it to the client.
		select {
		case adminMac := <-u.MacResponseChan:
			return &lnrpc.InitWalletResponse{
				AdminMacaroon: adminMac,
			}, nil

		case <-ctx.Done():
			return nil, ErrUnlockTimeout
		}

	case <-ctx.Done():
		return nil, ErrUnlockTimeout
	}
}

// LoadAndUnlock creates a loader for the wallet and tries to unlock the wallet
// with the given password and recovery window. If the drop wallet transactions
// flag is set, the history state drop is performed before unlocking the wallet
// yet again.
func (u *UnlockerService) LoadAndUnlock(password []byte,
	recoveryWindow uint32) (*wallet.Wallet, func() error, error) {

	loader, err := u.newLoader(recoveryWindow)
	if err != nil {
		return nil, nil, err
	}

	// Check if wallet already exists.
	walletExists, err := loader.WalletExists()
	if err != nil {
		return nil, nil, err
	}

	if !walletExists {
		// Cannot unlock a wallet that does not exist!
		return nil, nil, fmt.Errorf("wallet not found")
	}

	// Try opening the existing wallet with the provided password.
	unlockedWallet, err := loader.OpenExistingWallet(password, false)
	if err != nil {
		// Could not open wallet, most likely this means that provided
		// password was incorrect.
		return nil, nil, err
	}

	// The user requested to drop their whole wallet transaction state to
	// force a full chain rescan for wallet addresses. Dropping the state
	// only properly takes effect after opening the wallet. That's why we
	// start, drop, stop and start again.
	if u.resetWalletTransactions {
		dropErr := wallet.DropTransactionHistory(
			unlockedWallet.Database(), true,
		)

		// Even if dropping the history fails, we'll want to unload the
		// wallet. If unloading fails, that error is probably more
		// important to be returned to the user anyway.
		if err := loader.UnloadWallet(); err != nil {
			return nil, nil, fmt.Errorf("could not unload "+
				"wallet (tx history drop err: %v): %v", dropErr,
				err)
		}

		// If dropping failed but unloading didn't, we'll still abort
		// and inform the user.
		if dropErr != nil {
			return nil, nil, dropErr
		}

		// All looks good, let's now open the wallet again. The loader
		// was unloaded and might have removed its remote DB connection,
		// so let's re-create it as well.
		loader, err = u.newLoader(recoveryWindow)
		if err != nil {
			return nil, nil, err
		}
		unlockedWallet, err = loader.OpenExistingWallet(password, false)
		if err != nil {
			return nil, nil, err
		}
	}

	return unlockedWallet, loader.UnloadWallet, nil
}

// UnlockWallet sends the password provided by the incoming UnlockWalletRequest
// over the UnlockMsgs channel in case it successfully decrypts an existing
// wallet found in the chain's wallet database directory.
func (u *UnlockerService) UnlockWallet(ctx context.Context,
	in *lnrpc.UnlockWalletRequest) (*lnrpc.UnlockWalletResponse, error) {

	password := in.WalletPassword
	recoveryWindow := uint32(in.RecoveryWindow)

	unlockedWallet, unloadFn, err := u.LoadAndUnlock(
		password, recoveryWindow,
	)
	if err != nil {
		return nil, err
	}

	// We successfully opened the wallet and pass the instance back to
	// avoid it needing to be unlocked again.
	walletUnlockMsg := &WalletUnlockMsg{
		Passphrase:     password,
		RecoveryWindow: recoveryWindow,
		Wallet:         unlockedWallet,
		UnloadWallet:   unloadFn,
		StatelessInit:  in.StatelessInit,
	}

	// 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)
	if chansToRestore != nil {
		walletUnlockMsg.ChanBackups = *chansToRestore
	}

	// At this point we were able to open the existing wallet with the
	// provided password. We send the password over the UnlockMsgs
	// channel, such that it can be used by lnd to open the wallet.
	select {
	case u.UnlockMsgs <- walletUnlockMsg:
		// We need to read from the channel to let the daemon continue
		// its work. But we don't need the returned macaroon for this
		// operation, so we read it but then discard it.
		select {
		case <-u.MacResponseChan:
			return &lnrpc.UnlockWalletResponse{}, nil

		case <-ctx.Done():
			return nil, ErrUnlockTimeout
		}

	case <-ctx.Done():
		return nil, ErrUnlockTimeout
	}
}

// ChangePassword changes the password of the wallet and sends the new password
// across the UnlockPasswords channel to automatically unlock the wallet if
// successful.
func (u *UnlockerService) ChangePassword(ctx context.Context,
	in *lnrpc.ChangePasswordRequest) (*lnrpc.ChangePasswordResponse, error) {

	loader, err := u.newLoader(0)
	if err != nil {
		return nil, err
	}

	// First, we'll make sure the wallet exists for the specific chain and
	// network.
	walletExists, err := loader.WalletExists()
	if err != nil {
		return nil, err
	}

	if !walletExists {
		return nil, errors.New("wallet not found")
	}

	publicPw := in.CurrentPassword
	privatePw := in.CurrentPassword

	// If the current password is blank, we'll assume the user is coming
	// from a --noseedbackup state, so we'll use the default passwords.
	if len(in.CurrentPassword) == 0 {
		publicPw = lnwallet.DefaultPublicPassphrase
		privatePw = lnwallet.DefaultPrivatePassphrase
	}

	// Make sure the new password meets our constraints.
	if err := ValidatePassword(in.NewPassword); err != nil {
		return nil, err
	}

	// Load the existing wallet in order to proceed with the password change.
	w, err := loader.OpenExistingWallet(publicPw, false)
	if err != nil {
		return nil, err
	}

	// Now that we've opened the wallet, we need to close it in case of an
	// error. But not if we succeed, then the caller must close it.
	orderlyReturn := false
	defer func() {
		if !orderlyReturn {
			_ = loader.UnloadWallet()
		}
	}()

	// Before we actually change the password, we need to check if all flags
	// were set correctly. The content of the previously generated macaroon
	// files will become invalid after we generate a new root key. So we try
	// to delete them here and they will be recreated during normal startup
	// later. If they are missing, this is only an error if the
	// stateless_init flag was not set.
	if in.NewMacaroonRootKey || in.StatelessInit {
		for _, file := range u.macaroonFiles {
			err := os.Remove(file)
			if err != nil && !in.StatelessInit {
				return nil, fmt.Errorf("could not remove "+
					"macaroon file: %v. if the wallet "+
					"was initialized stateless please "+
					"add the --stateless_init "+
					"flag", err)
			}
		}
	}

	// Attempt to change both the public and private passphrases for the
	// wallet. This will be done atomically in order to prevent one
	// passphrase change from being successful and not the other.
	err = w.ChangePassphrases(
		publicPw, in.NewPassword, privatePw, in.NewPassword,
	)
	if err != nil {
		return nil, fmt.Errorf("unable to change wallet passphrase: "+
			"%v", err)
	}

	// The next step is to load the macaroon database, change the password
	// then close it again.
	// Attempt to open the macaroon DB, unlock it and then change
	// the passphrase.
	rootKeyStore, err := macaroons.NewRootKeyStorage(u.macaroonDB)
	if err != nil {
		return nil, err
	}
	macaroonService, err := macaroons.NewService(
		rootKeyStore, "lnd", in.StatelessInit,
	)
	if err != nil {
		return nil, err
	}

	err = macaroonService.CreateUnlock(&privatePw)
	if err != nil {
		closeErr := macaroonService.Close()
		if closeErr != nil {
			return nil, fmt.Errorf("could not create unlock: %v "+
				"--> follow-up error when closing: %v", err,
				closeErr)
		}
		return nil, err
	}
	err = macaroonService.ChangePassword(privatePw, in.NewPassword)
	if err != nil {
		closeErr := macaroonService.Close()
		if closeErr != nil {
			return nil, fmt.Errorf("could not change password: %v "+
				"--> follow-up error when closing: %v", err,
				closeErr)
		}
		return nil, err
	}

	// If requested by the user, attempt to replace the existing
	// macaroon root key with a new one.
	if in.NewMacaroonRootKey {
		err = macaroonService.GenerateNewRootKey()
		if err != nil {
			closeErr := macaroonService.Close()
			if closeErr != nil {
				return nil, fmt.Errorf("could not generate "+
					"new root key: %v --> follow-up error "+
					"when closing: %v", err, closeErr)
			}
			return nil, err
		}
	}

	err = macaroonService.Close()
	if err != nil {
		return nil, fmt.Errorf("could not close macaroon service: %w",
			err)
	}

	// Finally, send the new password across the UnlockPasswords channel to
	// automatically unlock the wallet.
	walletUnlockMsg := &WalletUnlockMsg{
		Passphrase:    in.NewPassword,
		Wallet:        w,
		StatelessInit: in.StatelessInit,
		UnloadWallet:  loader.UnloadWallet,
	}
	select {
	case u.UnlockMsgs <- walletUnlockMsg:
		// We need to read from the channel to let the daemon continue
		// its work and to get the admin macaroon. Once the response
		// arrives, we directly forward it to the client.
		orderlyReturn = true
		select {
		case adminMac := <-u.MacResponseChan:
			return &lnrpc.ChangePasswordResponse{
				AdminMacaroon: adminMac,
			}, nil

		case <-ctx.Done():
			return nil, ErrUnlockTimeout
		}

	case <-ctx.Done():
		return nil, ErrUnlockTimeout
	}
}

// ValidatePassword assures the password meets all of our constraints.
func ValidatePassword(password []byte) error {
	// Passwords should have a length of at least 8 characters.
	if len(password) < 8 {
		return errors.New("password must have at least 8 characters")
	}

	return nil
}
