diff --git a/config_builder.go b/config_builder.go index 7fe2a4dc9..3fbb9b268 100644 --- a/config_builder.go +++ b/config_builder.go @@ -3,17 +3,30 @@ package lnd import ( "context" "fmt" + "net" + "os" "path/filepath" + "strconv" + "strings" "time" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btclog" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/walletdb" proxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/lightninglabs/neutrino" + "github.com/lightninglabs/neutrino/headerfs" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/lightningnetwork/lnd/macaroons" + "github.com/lightningnetwork/lnd/tor" + "github.com/lightningnetwork/lnd/walletunlocker" "github.com/lightningnetwork/lnd/watchtower" "github.com/lightningnetwork/lnd/watchtower/wtclient" "github.com/lightningnetwork/lnd/watchtower/wtdb" @@ -343,3 +356,313 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( return dbs, cleanUp, nil } + +// waitForWalletPassword blocks until a password is provided by the user to +// this RPC server. +func waitForWalletPassword(cfg *Config, + pwService *walletunlocker.UnlockerService, + loaderOpts []btcwallet.LoaderOption, shutdownChan <-chan struct{}) ( + *walletunlocker.WalletUnlockParams, error) { + + // Wait for user to provide the password. + ltndLog.Infof("Waiting for wallet encryption password. Use `lncli " + + "create` to create a wallet, `lncli unlock` to unlock an " + + "existing wallet, or `lncli changepassword` to change the " + + "password of an existing wallet and unlock it.") + + // We currently don't distinguish between getting a password to be used + // for creation or unlocking, as a new wallet db will be created if + // none exists when creating the chain control. + select { + + // The wallet is being created for the first time, we'll check to see + // if the user provided any entropy for seed creation. If so, then + // we'll create the wallet early to load the seed. + 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. + 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, + keychain.KeyDerivationVersion) + } + + loader, err := btcwallet.NewWalletLoader( + cfg.ActiveNetParams.Params, recoveryWindow, + loaderOpts..., + ) + if err != nil { + return nil, err + } + + // With the seed, we can now use the wallet loader to create + // the wallet, then pass it back to avoid unlocking it again. + 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. + if err := loader.UnloadWallet(); err != nil { + ltndLog.Errorf("Could not unload new "+ + "wallet: %v", err) + } + return nil, err + } + + // For new wallets, the ResetWalletTransactions flag is a no-op. + if cfg.ResetWalletTransactions { + ltndLog.Warnf("Ignoring reset-wallet-transactions " + + "flag for new wallet as it has no effect") + } + + return &walletunlocker.WalletUnlockParams{ + Password: password, + Birthday: birthday, + RecoveryWindow: recoveryWindow, + Wallet: newWallet, + ChansToRestore: initMsg.ChanBackups, + UnloadWallet: loader.UnloadWallet, + StatelessInit: initMsg.StatelessInit, + MacResponseChan: pwService.MacResponseChan, + }, nil + + // The wallet has already been created in the past, and is simply being + // unlocked. So we'll just return these passphrases. + case unlockMsg := <-pwService.UnlockMsgs: + // Resetting the transactions is something the user likely only + // wants to do once so we add a prominent warning to the log to + // remind the user to turn off the setting again after + // successful completion. + if cfg.ResetWalletTransactions { + ltndLog.Warnf("Dropped all transaction history from " + + "on-chain wallet. Remember to disable " + + "reset-wallet-transactions flag for next " + + "start of lnd") + } + + return &walletunlocker.WalletUnlockParams{ + Password: unlockMsg.Passphrase, + RecoveryWindow: unlockMsg.RecoveryWindow, + Wallet: unlockMsg.Wallet, + ChansToRestore: unlockMsg.ChanBackups, + UnloadWallet: unlockMsg.UnloadWallet, + StatelessInit: unlockMsg.StatelessInit, + MacResponseChan: pwService.MacResponseChan, + }, nil + + // If we got a shutdown signal we just return with an error immediately + case <-shutdownChan: + return nil, fmt.Errorf("shutting down") + } +} + +// initNeutrinoBackend inits a new instance of the neutrino light client +// backend given a target chain directory to store the chain state. +func initNeutrinoBackend(cfg *Config, chainDir string, + blockCache *blockcache.BlockCache) (*neutrino.ChainService, + func(), error) { + + // Both channel validation flags are false by default but their meaning + // is the inverse of each other. Therefore both cannot be true. For + // every other case, the neutrino.validatechannels overwrites the + // routing.assumechanvalid value. + if cfg.NeutrinoMode.ValidateChannels && cfg.Routing.AssumeChannelValid { + return nil, nil, fmt.Errorf("can't set both " + + "neutrino.validatechannels and routing." + + "assumechanvalid to true at the same time") + } + cfg.Routing.AssumeChannelValid = !cfg.NeutrinoMode.ValidateChannels + + // First we'll open the database file for neutrino, creating the + // database if needed. We append the normalized network name here to + // match the behavior of btcwallet. + dbPath := filepath.Join( + chainDir, lncfg.NormalizeNetwork(cfg.ActiveNetParams.Name), + ) + + // Ensure that the neutrino db path exists. + if err := os.MkdirAll(dbPath, 0700); err != nil { + return nil, nil, err + } + + dbName := filepath.Join(dbPath, "neutrino.db") + db, err := walletdb.Create( + "bdb", dbName, !cfg.SyncFreelist, cfg.DB.Bolt.DBTimeout, + ) + if err != nil { + return nil, nil, fmt.Errorf("unable to create neutrino "+ + "database: %v", err) + } + + headerStateAssertion, err := parseHeaderStateAssertion( + cfg.NeutrinoMode.AssertFilterHeader, + ) + if err != nil { + db.Close() + return nil, nil, err + } + + // With the database open, we can now create an instance of the + // neutrino light client. We pass in relevant configuration parameters + // required. + config := neutrino.Config{ + DataDir: dbPath, + Database: db, + ChainParams: *cfg.ActiveNetParams.Params, + AddPeers: cfg.NeutrinoMode.AddPeers, + ConnectPeers: cfg.NeutrinoMode.ConnectPeers, + Dialer: func(addr net.Addr) (net.Conn, error) { + dialAddr := addr + if tor.IsOnionFakeIP(addr) { + // Because the Neutrino address manager only + // knows IP addresses, we need to turn any fake + // tcp6 address that actually encodes an Onion + // v2 address back into the hostname + // representation before we can pass it to the + // dialer. + var err error + dialAddr, err = tor.FakeIPToOnionHost(addr) + if err != nil { + return nil, err + } + } + + return cfg.net.Dial( + dialAddr.Network(), dialAddr.String(), + cfg.ConnectionTimeout, + ) + }, + NameResolver: func(host string) ([]net.IP, error) { + if tor.IsOnionHost(host) { + // Neutrino internally uses btcd's address + // manager which only operates on an IP level + // and does not understand onion hosts. We need + // to turn an onion host into a fake + // representation of an IP address to make it + // possible to connect to a block filter backend + // that serves on an Onion v2 hidden service. + fakeIP, err := tor.OnionHostToFakeIP(host) + if err != nil { + return nil, err + } + + return []net.IP{fakeIP}, nil + } + + addrs, err := cfg.net.LookupHost(host) + if err != nil { + return nil, err + } + + ips := make([]net.IP, 0, len(addrs)) + for _, strIP := range addrs { + ip := net.ParseIP(strIP) + if ip == nil { + continue + } + + ips = append(ips, ip) + } + + return ips, nil + }, + AssertFilterHeader: headerStateAssertion, + BlockCache: blockCache.Cache, + BroadcastTimeout: cfg.NeutrinoMode.BroadcastTimeout, + PersistToDisk: cfg.NeutrinoMode.PersistFilters, + } + + neutrino.MaxPeers = 8 + neutrino.BanDuration = time.Hour * 48 + neutrino.UserAgentName = cfg.NeutrinoMode.UserAgentName + neutrino.UserAgentVersion = cfg.NeutrinoMode.UserAgentVersion + + neutrinoCS, err := neutrino.NewChainService(config) + if err != nil { + db.Close() + return nil, nil, fmt.Errorf("unable to create neutrino light "+ + "client: %v", err) + } + + if err := neutrinoCS.Start(); err != nil { + db.Close() + return nil, nil, err + } + + cleanUp := func() { + if err := neutrinoCS.Stop(); err != nil { + ltndLog.Infof("Unable to stop neutrino light client: %v", err) + } + db.Close() + } + + return neutrinoCS, cleanUp, nil +} + +// parseHeaderStateAssertion parses the user-specified neutrino header state +// into a headerfs.FilterHeader. +func parseHeaderStateAssertion(state string) (*headerfs.FilterHeader, error) { + if len(state) == 0 { + return nil, nil + } + + split := strings.Split(state, ":") + if len(split) != 2 { + return nil, fmt.Errorf("header state assertion %v in "+ + "unexpected format, expected format height:hash", state) + } + + height, err := strconv.ParseUint(split[0], 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid filter header height: %v", err) + } + + hash, err := chainhash.NewHashFromStr(split[1]) + if err != nil { + return nil, fmt.Errorf("invalid filter header hash: %v", err) + } + + return &headerfs.FilterHeader{ + Height: uint32(height), + FilterHash: *hash, + }, nil +} diff --git a/lnd.go b/lnd.go index 8cb964c4b..0a9fc346c 100644 --- a/lnd.go +++ b/lnd.go @@ -15,20 +15,15 @@ import ( "net/http" _ "net/http/pprof" // Blank import to set up profiling HTTP handlers. "os" - "path/filepath" "runtime/pprof" - "strconv" "strings" "sync" "time" - "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/wallet" - "github.com/btcsuite/btcwallet/walletdb" proxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/lightninglabs/neutrino" - "github.com/lightninglabs/neutrino/headerfs" "golang.org/x/crypto/acme/autocert" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -1417,313 +1412,3 @@ func startRestProxy(cfg *Config, rpcServer *rpcServer, restDialOpts []grpc.DialO return shutdown, nil } - -// waitForWalletPassword blocks until a password is provided by the user to -// this RPC server. -func waitForWalletPassword(cfg *Config, - pwService *walletunlocker.UnlockerService, - loaderOpts []btcwallet.LoaderOption, shutdownChan <-chan struct{}) ( - *walletunlocker.WalletUnlockParams, error) { - - // Wait for user to provide the password. - ltndLog.Infof("Waiting for wallet encryption password. Use `lncli " + - "create` to create a wallet, `lncli unlock` to unlock an " + - "existing wallet, or `lncli changepassword` to change the " + - "password of an existing wallet and unlock it.") - - // We currently don't distinguish between getting a password to be used - // for creation or unlocking, as a new wallet db will be created if - // none exists when creating the chain control. - select { - - // The wallet is being created for the first time, we'll check to see - // if the user provided any entropy for seed creation. If so, then - // we'll create the wallet early to load the seed. - 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. - 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, - keychain.KeyDerivationVersion) - } - - loader, err := btcwallet.NewWalletLoader( - cfg.ActiveNetParams.Params, recoveryWindow, - loaderOpts..., - ) - if err != nil { - return nil, err - } - - // With the seed, we can now use the wallet loader to create - // the wallet, then pass it back to avoid unlocking it again. - 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. - if err := loader.UnloadWallet(); err != nil { - ltndLog.Errorf("Could not unload new "+ - "wallet: %v", err) - } - return nil, err - } - - // For new wallets, the ResetWalletTransactions flag is a no-op. - if cfg.ResetWalletTransactions { - ltndLog.Warnf("Ignoring reset-wallet-transactions " + - "flag for new wallet as it has no effect") - } - - return &walletunlocker.WalletUnlockParams{ - Password: password, - Birthday: birthday, - RecoveryWindow: recoveryWindow, - Wallet: newWallet, - ChansToRestore: initMsg.ChanBackups, - UnloadWallet: loader.UnloadWallet, - StatelessInit: initMsg.StatelessInit, - MacResponseChan: pwService.MacResponseChan, - }, nil - - // The wallet has already been created in the past, and is simply being - // unlocked. So we'll just return these passphrases. - case unlockMsg := <-pwService.UnlockMsgs: - // Resetting the transactions is something the user likely only - // wants to do once so we add a prominent warning to the log to - // remind the user to turn off the setting again after - // successful completion. - if cfg.ResetWalletTransactions { - ltndLog.Warnf("Dropped all transaction history from " + - "on-chain wallet. Remember to disable " + - "reset-wallet-transactions flag for next " + - "start of lnd") - } - - return &walletunlocker.WalletUnlockParams{ - Password: unlockMsg.Passphrase, - RecoveryWindow: unlockMsg.RecoveryWindow, - Wallet: unlockMsg.Wallet, - ChansToRestore: unlockMsg.ChanBackups, - UnloadWallet: unlockMsg.UnloadWallet, - StatelessInit: unlockMsg.StatelessInit, - MacResponseChan: pwService.MacResponseChan, - }, nil - - // If we got a shutdown signal we just return with an error immediately - case <-shutdownChan: - return nil, fmt.Errorf("shutting down") - } -} - -// initNeutrinoBackend inits a new instance of the neutrino light client -// backend given a target chain directory to store the chain state. -func initNeutrinoBackend(cfg *Config, chainDir string, - blockCache *blockcache.BlockCache) (*neutrino.ChainService, - func(), error) { - - // Both channel validation flags are false by default but their meaning - // is the inverse of each other. Therefore both cannot be true. For - // every other case, the neutrino.validatechannels overwrites the - // routing.assumechanvalid value. - if cfg.NeutrinoMode.ValidateChannels && cfg.Routing.AssumeChannelValid { - return nil, nil, fmt.Errorf("can't set both " + - "neutrino.validatechannels and routing." + - "assumechanvalid to true at the same time") - } - cfg.Routing.AssumeChannelValid = !cfg.NeutrinoMode.ValidateChannels - - // First we'll open the database file for neutrino, creating the - // database if needed. We append the normalized network name here to - // match the behavior of btcwallet. - dbPath := filepath.Join( - chainDir, lncfg.NormalizeNetwork(cfg.ActiveNetParams.Name), - ) - - // Ensure that the neutrino db path exists. - if err := os.MkdirAll(dbPath, 0700); err != nil { - return nil, nil, err - } - - dbName := filepath.Join(dbPath, "neutrino.db") - db, err := walletdb.Create( - "bdb", dbName, !cfg.SyncFreelist, cfg.DB.Bolt.DBTimeout, - ) - if err != nil { - return nil, nil, fmt.Errorf("unable to create neutrino "+ - "database: %v", err) - } - - headerStateAssertion, err := parseHeaderStateAssertion( - cfg.NeutrinoMode.AssertFilterHeader, - ) - if err != nil { - db.Close() - return nil, nil, err - } - - // With the database open, we can now create an instance of the - // neutrino light client. We pass in relevant configuration parameters - // required. - config := neutrino.Config{ - DataDir: dbPath, - Database: db, - ChainParams: *cfg.ActiveNetParams.Params, - AddPeers: cfg.NeutrinoMode.AddPeers, - ConnectPeers: cfg.NeutrinoMode.ConnectPeers, - Dialer: func(addr net.Addr) (net.Conn, error) { - dialAddr := addr - if tor.IsOnionFakeIP(addr) { - // Because the Neutrino address manager only - // knows IP addresses, we need to turn any fake - // tcp6 address that actually encodes an Onion - // v2 address back into the hostname - // representation before we can pass it to the - // dialer. - var err error - dialAddr, err = tor.FakeIPToOnionHost(addr) - if err != nil { - return nil, err - } - } - - return cfg.net.Dial( - dialAddr.Network(), dialAddr.String(), - cfg.ConnectionTimeout, - ) - }, - NameResolver: func(host string) ([]net.IP, error) { - if tor.IsOnionHost(host) { - // Neutrino internally uses btcd's address - // manager which only operates on an IP level - // and does not understand onion hosts. We need - // to turn an onion host into a fake - // representation of an IP address to make it - // possible to connect to a block filter backend - // that serves on an Onion v2 hidden service. - fakeIP, err := tor.OnionHostToFakeIP(host) - if err != nil { - return nil, err - } - - return []net.IP{fakeIP}, nil - } - - addrs, err := cfg.net.LookupHost(host) - if err != nil { - return nil, err - } - - ips := make([]net.IP, 0, len(addrs)) - for _, strIP := range addrs { - ip := net.ParseIP(strIP) - if ip == nil { - continue - } - - ips = append(ips, ip) - } - - return ips, nil - }, - AssertFilterHeader: headerStateAssertion, - BlockCache: blockCache.Cache, - BroadcastTimeout: cfg.NeutrinoMode.BroadcastTimeout, - PersistToDisk: cfg.NeutrinoMode.PersistFilters, - } - - neutrino.MaxPeers = 8 - neutrino.BanDuration = time.Hour * 48 - neutrino.UserAgentName = cfg.NeutrinoMode.UserAgentName - neutrino.UserAgentVersion = cfg.NeutrinoMode.UserAgentVersion - - neutrinoCS, err := neutrino.NewChainService(config) - if err != nil { - db.Close() - return nil, nil, fmt.Errorf("unable to create neutrino light "+ - "client: %v", err) - } - - if err := neutrinoCS.Start(); err != nil { - db.Close() - return nil, nil, err - } - - cleanUp := func() { - if err := neutrinoCS.Stop(); err != nil { - ltndLog.Infof("Unable to stop neutrino light client: %v", err) - } - db.Close() - } - - return neutrinoCS, cleanUp, nil -} - -// parseHeaderStateAssertion parses the user-specified neutrino header state -// into a headerfs.FilterHeader. -func parseHeaderStateAssertion(state string) (*headerfs.FilterHeader, error) { - if len(state) == 0 { - return nil, nil - } - - split := strings.Split(state, ":") - if len(split) != 2 { - return nil, fmt.Errorf("header state assertion %v in "+ - "unexpected format, expected format height:hash", state) - } - - height, err := strconv.ParseUint(split[0], 10, 32) - if err != nil { - return nil, fmt.Errorf("invalid filter header height: %v", err) - } - - hash, err := chainhash.NewHashFromStr(split[1]) - if err != nil { - return nil, fmt.Errorf("invalid filter header hash: %v", err) - } - - return &headerfs.FilterHeader{ - Height: uint32(height), - FilterHash: *hash, - }, nil -}