mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 01:43:16 +01:00
multi: extract wallet initialization
With this commit we extract the wallet creation/unlocking and initialization completely out of the main function. This will allow us to use custom implementations in the future.
This commit is contained in:
parent
047d8ea3bc
commit
e1da1f8941
@ -34,6 +34,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing/chainview"
|
||||
"github.com/lightningnetwork/lnd/walletunlocker"
|
||||
)
|
||||
|
||||
// Config houses necessary fields that a chainControl instance needs to
|
||||
@ -79,6 +80,10 @@ type Config struct {
|
||||
// BlockCache is the main cache for storing block information.
|
||||
BlockCache *blockcache.BlockCache
|
||||
|
||||
// WalletUnlockParams are the parameters that were used for unlocking
|
||||
// the main wallet.
|
||||
WalletUnlockParams *walletunlocker.WalletUnlockParams
|
||||
|
||||
// NeutrinoCS is a pointer to a neutrino ChainService. Must be non-nil if
|
||||
// using neutrino.
|
||||
NeutrinoCS *neutrino.ChainService
|
||||
|
@ -31,7 +31,7 @@ func main() {
|
||||
// Help was requested, exit normally.
|
||||
os.Exit(0)
|
||||
}
|
||||
implCfg := loadedConfig.ImplementationConfig()
|
||||
implCfg := loadedConfig.ImplementationConfig(shutdownInterceptor)
|
||||
|
||||
// Call the "real" main in a nested manner so the defers will properly
|
||||
// be executed in the case of a graceful shutdown.
|
||||
|
15
config.go
15
config.go
@ -1538,13 +1538,16 @@ func (c *Config) graphDatabaseDir() string {
|
||||
|
||||
// ImplementationConfig returns the configuration of what actual implementations
|
||||
// should be used when creating the main lnd instance.
|
||||
func (c *Config) ImplementationConfig() *ImplementationCfg {
|
||||
defaultImpl := &DefaultWalletImpl{}
|
||||
func (c *Config) ImplementationConfig(
|
||||
interceptor signal.Interceptor) *ImplementationCfg {
|
||||
|
||||
defaultImpl := NewDefaultWalletImpl(c, ltndLog, interceptor)
|
||||
return &ImplementationCfg{
|
||||
GrpcRegistrar: defaultImpl,
|
||||
RestRegistrar: defaultImpl,
|
||||
ExternalValidator: defaultImpl,
|
||||
DatabaseBuilder: NewDefaultDatabaseBuilder(c, ltndLog),
|
||||
GrpcRegistrar: defaultImpl,
|
||||
RestRegistrar: defaultImpl,
|
||||
ExternalValidator: defaultImpl,
|
||||
DatabaseBuilder: NewDefaultDatabaseBuilder(c, ltndLog),
|
||||
ChainControlBuilder: defaultImpl,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
package lnd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -19,12 +21,17 @@ import (
|
||||
"github.com/lightninglabs/neutrino"
|
||||
"github.com/lightninglabs/neutrino/headerfs"
|
||||
"github.com/lightningnetwork/lnd/blockcache"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/rpcperms"
|
||||
"github.com/lightningnetwork/lnd/signal"
|
||||
"github.com/lightningnetwork/lnd/tor"
|
||||
"github.com/lightningnetwork/lnd/walletunlocker"
|
||||
"github.com/lightningnetwork/lnd/watchtower"
|
||||
@ -79,6 +86,17 @@ type DatabaseBuilder interface {
|
||||
BuildDatabase(ctx context.Context) (*DatabaseInstances, func(), error)
|
||||
}
|
||||
|
||||
// ChainControlBuilder is an interface that must be satisfied by a custom wallet
|
||||
// implementation.
|
||||
type ChainControlBuilder interface {
|
||||
// BuildChainControl is responsible for creating or unlocking and then
|
||||
// fully initializing a wallet and returning it as part of a fully
|
||||
// populated chain control instance.
|
||||
BuildChainControl(context.Context, *DatabaseInstances,
|
||||
*rpcperms.InterceptorChain,
|
||||
[]*ListenerWithSignal) (*chainreg.ChainControl, func(), error)
|
||||
}
|
||||
|
||||
// ImplementationCfg is a struct that holds all configuration items for
|
||||
// components that can be implemented outside lnd itself.
|
||||
type ImplementationCfg struct {
|
||||
@ -97,11 +115,32 @@ type ImplementationCfg struct {
|
||||
// DatabaseBuilder is a type that can provide lnd's main database
|
||||
// backend instances.
|
||||
DatabaseBuilder
|
||||
|
||||
// ChainControlBuilder is a type that can provide a custom wallet
|
||||
// implementation.
|
||||
ChainControlBuilder
|
||||
}
|
||||
|
||||
// DefaultWalletImpl is the default implementation of our normal, btcwallet
|
||||
// backed configuration.
|
||||
type DefaultWalletImpl struct {
|
||||
cfg *Config
|
||||
logger btclog.Logger
|
||||
interceptor signal.Interceptor
|
||||
|
||||
pwService *walletunlocker.UnlockerService
|
||||
}
|
||||
|
||||
// NewDefaultWalletImpl creates a new default wallet implementation.
|
||||
func NewDefaultWalletImpl(cfg *Config, logger btclog.Logger,
|
||||
interceptor signal.Interceptor) *DefaultWalletImpl {
|
||||
|
||||
return &DefaultWalletImpl{
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
interceptor: interceptor,
|
||||
pwService: createWalletUnlockerService(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRestSubserver is called after lnd creates the main proxy.ServeMux
|
||||
@ -109,10 +148,13 @@ type DefaultWalletImpl struct {
|
||||
// their own REST proxy stubs to the main server instance.
|
||||
//
|
||||
// NOTE: This is part of the GrpcRegistrar interface.
|
||||
func (d *DefaultWalletImpl) RegisterRestSubserver(context.Context,
|
||||
*proxy.ServeMux, string, []grpc.DialOption) error {
|
||||
func (d *DefaultWalletImpl) RegisterRestSubserver(ctx context.Context,
|
||||
mux *proxy.ServeMux, restProxyDest string,
|
||||
restDialOpts []grpc.DialOption) error {
|
||||
|
||||
return nil
|
||||
return lnrpc.RegisterWalletUnlockerHandlerFromEndpoint(
|
||||
ctx, mux, restProxyDest, restDialOpts,
|
||||
)
|
||||
}
|
||||
|
||||
// RegisterGrpcSubserver is called for each net.Listener on which lnd creates a
|
||||
@ -120,7 +162,9 @@ func (d *DefaultWalletImpl) RegisterRestSubserver(context.Context,
|
||||
// register their own gRPC server structs to the main server instance.
|
||||
//
|
||||
// NOTE: This is part of the GrpcRegistrar interface.
|
||||
func (d *DefaultWalletImpl) RegisterGrpcSubserver(*grpc.Server) error {
|
||||
func (d *DefaultWalletImpl) RegisterGrpcSubserver(s *grpc.Server) error {
|
||||
lnrpc.RegisterWalletUnlockerServer(s, d.pwService)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -151,6 +195,393 @@ func (d *DefaultWalletImpl) Permissions() map[string][]bakery.Op {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildChainControl is responsible for creating or unlocking and then fully
|
||||
// initializing a wallet and returning it as part of a fully populated chain
|
||||
// control instance.
|
||||
//
|
||||
// NOTE: This is part of the ChainControlBuilder interface.
|
||||
func (d *DefaultWalletImpl) BuildChainControl(ctx context.Context,
|
||||
dbs *DatabaseInstances, interceptorChain *rpcperms.InterceptorChain,
|
||||
grpcListeners []*ListenerWithSignal) (*chainreg.ChainControl, func(),
|
||||
error) {
|
||||
|
||||
// Keep track of our various cleanup functions. We use a defer function
|
||||
// as well to not repeat ourselves with every return statement.
|
||||
var (
|
||||
cleanUpTasks []func()
|
||||
earlyExit = true
|
||||
cleanUp = func() {
|
||||
for _, fn := range cleanUpTasks {
|
||||
if fn == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fn()
|
||||
}
|
||||
}
|
||||
)
|
||||
defer func() {
|
||||
if earlyExit {
|
||||
cleanUp()
|
||||
}
|
||||
}()
|
||||
|
||||
// Initialize a new block cache.
|
||||
blockCache := blockcache.NewBlockCache(d.cfg.BlockCacheSize)
|
||||
|
||||
// Before starting the wallet, we'll create and start our Neutrino
|
||||
// light client instance, if enabled, in order to allow it to sync
|
||||
// while the rest of the daemon continues startup.
|
||||
mainChain := d.cfg.Bitcoin
|
||||
if d.cfg.registeredChains.PrimaryChain() == chainreg.LitecoinChain {
|
||||
mainChain = d.cfg.Litecoin
|
||||
}
|
||||
var neutrinoCS *neutrino.ChainService
|
||||
if mainChain.Node == "neutrino" {
|
||||
neutrinoBackend, neutrinoCleanUp, err := initNeutrinoBackend(
|
||||
d.cfg, mainChain.ChainDir, blockCache,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to initialize neutrino "+
|
||||
"backend: %v", err)
|
||||
d.logger.Error(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
cleanUpTasks = append(cleanUpTasks, neutrinoCleanUp)
|
||||
neutrinoCS = neutrinoBackend
|
||||
}
|
||||
|
||||
var (
|
||||
walletInitParams = walletunlocker.WalletUnlockParams{
|
||||
// In case we do auto-unlock, we need to be able to send
|
||||
// into the channel without blocking so we buffer it.
|
||||
MacResponseChan: make(chan []byte, 1),
|
||||
}
|
||||
privateWalletPw = lnwallet.DefaultPrivatePassphrase
|
||||
publicWalletPw = lnwallet.DefaultPublicPassphrase
|
||||
)
|
||||
|
||||
// If the user didn't request a seed, then we'll manually assume a
|
||||
// wallet birthday of now, as otherwise the seed would've specified
|
||||
// this information.
|
||||
walletInitParams.Birthday = time.Now()
|
||||
|
||||
d.pwService.SetLoaderOpts([]btcwallet.LoaderOption{dbs.WalletDB})
|
||||
d.pwService.SetMacaroonDB(dbs.MacaroonDB)
|
||||
walletExists, err := d.pwService.WalletExists()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !walletExists {
|
||||
interceptorChain.SetWalletNotCreated()
|
||||
} else {
|
||||
interceptorChain.SetWalletLocked()
|
||||
}
|
||||
|
||||
// If we've started in auto unlock mode, then a wallet should already
|
||||
// exist because we don't want to enable the RPC unlocker in that case
|
||||
// for security reasons (an attacker could inject their seed since the
|
||||
// RPC is unauthenticated). Only if the user explicitly wants to allow
|
||||
// wallet creation we don't error out here.
|
||||
if d.cfg.WalletUnlockPasswordFile != "" && !walletExists &&
|
||||
!d.cfg.WalletUnlockAllowCreate {
|
||||
|
||||
return nil, nil, fmt.Errorf("wallet unlock password file was " +
|
||||
"specified but wallet does not exist; initialize the " +
|
||||
"wallet before using auto unlocking")
|
||||
}
|
||||
|
||||
// What wallet mode are we running in? We've already made sure the no
|
||||
// seed backup and auto unlock aren't both set during config parsing.
|
||||
switch {
|
||||
// No seed backup means we're also using the default password.
|
||||
case d.cfg.NoSeedBackup:
|
||||
// We continue normally, the default password has already been
|
||||
// set above.
|
||||
|
||||
// A password for unlocking is provided in a file.
|
||||
case d.cfg.WalletUnlockPasswordFile != "" && walletExists:
|
||||
d.logger.Infof("Attempting automatic wallet unlock with " +
|
||||
"password provided in file")
|
||||
pwBytes, err := ioutil.ReadFile(d.cfg.WalletUnlockPasswordFile)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error reading password "+
|
||||
"from file %s: %v",
|
||||
d.cfg.WalletUnlockPasswordFile, err)
|
||||
}
|
||||
|
||||
// Remove any newlines at the end of the file. The lndinit tool
|
||||
// won't ever write a newline but maybe the file was provisioned
|
||||
// by another process or user.
|
||||
pwBytes = bytes.TrimRight(pwBytes, "\r\n")
|
||||
|
||||
// We have the password now, we can ask the unlocker service to
|
||||
// do the unlock for us.
|
||||
unlockedWallet, unloadWalletFn, err := d.pwService.LoadAndUnlock(
|
||||
pwBytes, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error unlocking wallet "+
|
||||
"with password from file: %v", err)
|
||||
}
|
||||
|
||||
cleanUpTasks = append(cleanUpTasks, func() {
|
||||
if err := unloadWalletFn(); err != nil {
|
||||
d.logger.Errorf("Could not unload wallet: %v",
|
||||
err)
|
||||
}
|
||||
})
|
||||
|
||||
privateWalletPw = pwBytes
|
||||
publicWalletPw = pwBytes
|
||||
walletInitParams.Wallet = unlockedWallet
|
||||
walletInitParams.UnloadWallet = unloadWalletFn
|
||||
|
||||
// If none of the automatic startup options are selected, we fall back
|
||||
// to the default behavior of waiting for the wallet creation/unlocking
|
||||
// over RPC.
|
||||
default:
|
||||
if err := d.interceptor.Notifier.NotifyReady(false); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
params, err := waitForWalletPassword(
|
||||
d.cfg, d.pwService, []btcwallet.LoaderOption{dbs.WalletDB},
|
||||
d.interceptor.ShutdownChannel(),
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to set up wallet password "+
|
||||
"listeners: %v", err)
|
||||
d.logger.Error(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
walletInitParams = *params
|
||||
privateWalletPw = walletInitParams.Password
|
||||
publicWalletPw = walletInitParams.Password
|
||||
cleanUpTasks = append(cleanUpTasks, func() {
|
||||
if err := walletInitParams.UnloadWallet(); err != nil {
|
||||
d.logger.Errorf("Could not unload wallet: %v",
|
||||
err)
|
||||
}
|
||||
})
|
||||
|
||||
if walletInitParams.RecoveryWindow > 0 {
|
||||
d.logger.Infof("Wallet recovery mode enabled with "+
|
||||
"address lookahead of %d addresses",
|
||||
walletInitParams.RecoveryWindow)
|
||||
}
|
||||
}
|
||||
|
||||
var macaroonService *macaroons.Service
|
||||
if !d.cfg.NoMacaroons {
|
||||
// Create the macaroon authentication/authorization service.
|
||||
macaroonService, err = macaroons.NewService(
|
||||
dbs.MacaroonDB, "lnd", walletInitParams.StatelessInit,
|
||||
macaroons.IPLockChecker,
|
||||
macaroons.CustomChecker(interceptorChain),
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to set up macaroon "+
|
||||
"authentication: %v", err)
|
||||
d.logger.Error(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
cleanUpTasks = append(cleanUpTasks, func() {
|
||||
if err := macaroonService.Close(); err != nil {
|
||||
d.logger.Errorf("Could not close macaroon "+
|
||||
"service: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Try to unlock the macaroon store with the private password.
|
||||
// Ignore ErrAlreadyUnlocked since it could be unlocked by the
|
||||
// wallet unlocker.
|
||||
err = macaroonService.CreateUnlock(&privateWalletPw)
|
||||
if err != nil && err != macaroons.ErrAlreadyUnlocked {
|
||||
err := fmt.Errorf("unable to unlock macaroons: %v", err)
|
||||
d.logger.Error(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// In case we actually needed to unlock the wallet, we now need
|
||||
// to create an instance of the admin macaroon and send it to
|
||||
// the unlocker so it can forward it to the user. In no seed
|
||||
// backup mode, there's nobody listening on the channel and we'd
|
||||
// block here forever.
|
||||
if !d.cfg.NoSeedBackup {
|
||||
adminMacBytes, err := bakeMacaroon(
|
||||
ctx, macaroonService, adminPermissions(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// The channel is buffered by one element so writing
|
||||
// should not block here.
|
||||
walletInitParams.MacResponseChan <- adminMacBytes
|
||||
|
||||
for _, lis := range grpcListeners {
|
||||
if lis.MacChan != nil {
|
||||
lis.MacChan <- adminMacBytes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the user requested a stateless initialization, no macaroon
|
||||
// files should be created.
|
||||
if !walletInitParams.StatelessInit &&
|
||||
!fileExists(d.cfg.AdminMacPath) &&
|
||||
!fileExists(d.cfg.ReadMacPath) &&
|
||||
!fileExists(d.cfg.InvoiceMacPath) {
|
||||
|
||||
// Create macaroon files for lncli to use if they don't
|
||||
// exist.
|
||||
err = genMacaroons(
|
||||
ctx, macaroonService, d.cfg.AdminMacPath,
|
||||
d.cfg.ReadMacPath, d.cfg.InvoiceMacPath,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to create macaroons "+
|
||||
"%v", err)
|
||||
d.logger.Error(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// As a security service to the user, if they requested
|
||||
// stateless initialization and there are macaroon files on disk
|
||||
// we log a warning.
|
||||
if walletInitParams.StatelessInit {
|
||||
msg := "Found %s macaroon on disk (%s) even though " +
|
||||
"--stateless_init was requested. Unencrypted " +
|
||||
"state is accessible by the host system. You " +
|
||||
"should change the password and use " +
|
||||
"--new_mac_root_key with --stateless_init to " +
|
||||
"clean up and invalidate old macaroons."
|
||||
|
||||
if fileExists(d.cfg.AdminMacPath) {
|
||||
d.logger.Warnf(msg, "admin", d.cfg.AdminMacPath)
|
||||
}
|
||||
if fileExists(d.cfg.ReadMacPath) {
|
||||
d.logger.Warnf(msg, "readonly", d.cfg.ReadMacPath)
|
||||
}
|
||||
if fileExists(d.cfg.InvoiceMacPath) {
|
||||
d.logger.Warnf(msg, "invoice", d.cfg.InvoiceMacPath)
|
||||
}
|
||||
}
|
||||
|
||||
// We add the macaroon service to our RPC interceptor. This
|
||||
// will start checking macaroons against permissions on every
|
||||
// RPC invocation.
|
||||
interceptorChain.AddMacaroonService(macaroonService)
|
||||
}
|
||||
|
||||
// Now that the wallet password has been provided, transition the RPC
|
||||
// state into Unlocked.
|
||||
interceptorChain.SetWalletUnlocked()
|
||||
|
||||
// Since calls to the WalletUnlocker service wait for a response on the
|
||||
// macaroon channel, we close it here to make sure they return in case
|
||||
// we did not return the admin macaroon above. This will be the case if
|
||||
// --no-macaroons is used.
|
||||
close(walletInitParams.MacResponseChan)
|
||||
|
||||
// We'll also close all the macaroon channels since lnd is done sending
|
||||
// macaroon data over it.
|
||||
for _, lis := range grpcListeners {
|
||||
if lis.MacChan != nil {
|
||||
close(lis.MacChan)
|
||||
}
|
||||
}
|
||||
|
||||
// With the information parsed from the configuration, create valid
|
||||
// instances of the pertinent interfaces required to operate the
|
||||
// Lightning Network Daemon.
|
||||
//
|
||||
// When we create the chain control, we need storage for the height
|
||||
// hints and also the wallet itself, for these two we want them to be
|
||||
// replicated, so we'll pass in the remote channel DB instance.
|
||||
chainControlCfg := &chainreg.Config{
|
||||
Bitcoin: d.cfg.Bitcoin,
|
||||
Litecoin: d.cfg.Litecoin,
|
||||
PrimaryChain: d.cfg.registeredChains.PrimaryChain,
|
||||
HeightHintCacheQueryDisable: d.cfg.HeightHintCacheQueryDisable,
|
||||
NeutrinoMode: d.cfg.NeutrinoMode,
|
||||
BitcoindMode: d.cfg.BitcoindMode,
|
||||
LitecoindMode: d.cfg.LitecoindMode,
|
||||
BtcdMode: d.cfg.BtcdMode,
|
||||
LtcdMode: d.cfg.LtcdMode,
|
||||
HeightHintDB: dbs.HeightHintDB,
|
||||
ChanStateDB: dbs.ChanStateDB.ChannelStateDB(),
|
||||
NeutrinoCS: neutrinoCS,
|
||||
ActiveNetParams: d.cfg.ActiveNetParams,
|
||||
FeeURL: d.cfg.FeeURL,
|
||||
Dialer: func(addr string) (net.Conn, error) {
|
||||
return d.cfg.net.Dial(
|
||||
"tcp", addr, d.cfg.ConnectionTimeout,
|
||||
)
|
||||
},
|
||||
BlockCache: blockCache,
|
||||
WalletUnlockParams: &walletInitParams,
|
||||
}
|
||||
|
||||
// Let's go ahead and create the partial chain control now that is only
|
||||
// dependent on our configuration and doesn't require any wallet
|
||||
// specific information.
|
||||
partialChainControl, pccCleanup, err := chainreg.NewPartialChainControl(
|
||||
chainControlCfg,
|
||||
)
|
||||
cleanUpTasks = append(cleanUpTasks, pccCleanup)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to create partial chain control: %v",
|
||||
err)
|
||||
d.logger.Error(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
walletConfig := &btcwallet.Config{
|
||||
PrivatePass: privateWalletPw,
|
||||
PublicPass: publicWalletPw,
|
||||
Birthday: walletInitParams.Birthday,
|
||||
RecoveryWindow: walletInitParams.RecoveryWindow,
|
||||
NetParams: d.cfg.ActiveNetParams.Params,
|
||||
CoinType: d.cfg.ActiveNetParams.CoinType,
|
||||
Wallet: walletInitParams.Wallet,
|
||||
LoaderOptions: []btcwallet.LoaderOption{dbs.WalletDB},
|
||||
ChainSource: partialChainControl.ChainSource,
|
||||
}
|
||||
|
||||
// Parse coin selection strategy.
|
||||
switch d.cfg.CoinSelectionStrategy {
|
||||
case "largest":
|
||||
walletConfig.CoinSelectionStrategy = wallet.CoinSelectionLargest
|
||||
|
||||
case "random":
|
||||
walletConfig.CoinSelectionStrategy = wallet.CoinSelectionRandom
|
||||
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unknown coin selection strategy "+
|
||||
"%v", d.cfg.CoinSelectionStrategy)
|
||||
}
|
||||
|
||||
// We've created the wallet configuration now, so we can finish
|
||||
// initializing the main chain control.
|
||||
activeChainControl, ccCleanup, err := chainreg.NewChainControl(
|
||||
walletConfig, partialChainControl,
|
||||
)
|
||||
cleanUpTasks = append(cleanUpTasks, ccCleanup)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to create chain control: %v", err)
|
||||
d.logger.Error(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
earlyExit = false
|
||||
return activeChainControl, cleanUp, nil
|
||||
}
|
||||
|
||||
// DatabaseInstances is a struct that holds all instances to the actual
|
||||
// databases that are used in lnd.
|
||||
type DatabaseInstances struct {
|
||||
@ -630,7 +1061,8 @@ func initNeutrinoBackend(cfg *Config, chainDir string,
|
||||
|
||||
cleanUp := func() {
|
||||
if err := neutrinoCS.Stop(); err != nil {
|
||||
ltndLog.Infof("Unable to stop neutrino light client: %v", err)
|
||||
ltndLog.Infof("Unable to stop neutrino light client: "+
|
||||
"%v", err)
|
||||
}
|
||||
db.Close()
|
||||
}
|
||||
|
366
lnd.go
366
lnd.go
@ -5,7 +5,6 @@
|
||||
package lnd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
@ -21,9 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcwallet/wallet"
|
||||
proxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/lightninglabs/neutrino"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
@ -32,17 +29,14 @@ import (
|
||||
"gopkg.in/macaroon.v2"
|
||||
|
||||
"github.com/lightningnetwork/lnd/autopilot"
|
||||
"github.com/lightningnetwork/lnd/blockcache"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
"github.com/lightningnetwork/lnd/cert"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/chanacceptor"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/monitoring"
|
||||
"github.com/lightningnetwork/lnd/rpcperms"
|
||||
@ -228,46 +222,6 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
|
||||
|
||||
defer cleanUp()
|
||||
|
||||
// Initialize a new block cache.
|
||||
blockCache := blockcache.NewBlockCache(cfg.BlockCacheSize)
|
||||
|
||||
// Before starting the wallet, we'll create and start our Neutrino
|
||||
// light client instance, if enabled, in order to allow it to sync
|
||||
// while the rest of the daemon continues startup.
|
||||
mainChain := cfg.Bitcoin
|
||||
if cfg.registeredChains.PrimaryChain() == chainreg.LitecoinChain {
|
||||
mainChain = cfg.Litecoin
|
||||
}
|
||||
var neutrinoCS *neutrino.ChainService
|
||||
if mainChain.Node == "neutrino" {
|
||||
neutrinoBackend, neutrinoCleanUp, err := initNeutrinoBackend(
|
||||
cfg, mainChain.ChainDir, blockCache,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to initialize neutrino "+
|
||||
"backend: %v", err)
|
||||
ltndLog.Error(err)
|
||||
return err
|
||||
}
|
||||
defer neutrinoCleanUp()
|
||||
neutrinoCS = neutrinoBackend
|
||||
}
|
||||
|
||||
var (
|
||||
walletInitParams = walletunlocker.WalletUnlockParams{
|
||||
// In case we do auto-unlock, we need to be able to send
|
||||
// into the channel without blocking so we buffer it.
|
||||
MacResponseChan: make(chan []byte, 1),
|
||||
}
|
||||
privateWalletPw = lnwallet.DefaultPrivatePassphrase
|
||||
publicWalletPw = lnwallet.DefaultPublicPassphrase
|
||||
)
|
||||
|
||||
// If the user didn't request a seed, then we'll manually assume a
|
||||
// wallet birthday of now, as otherwise the seed would've specified
|
||||
// this information.
|
||||
walletInitParams.Birthday = time.Now()
|
||||
|
||||
// If we have chosen to start with a dedicated listener for the
|
||||
// rpc server, we set it directly.
|
||||
var grpcListeners []*ListenerWithSignal
|
||||
@ -321,10 +275,6 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
|
||||
// it can be used to query for the current state of the wallet.
|
||||
lnrpc.RegisterStateServer(grpcServer, interceptorChain)
|
||||
|
||||
// Register the WalletUnlockerService with the GRPC server.
|
||||
pwService := createWalletUnlockerService(cfg)
|
||||
lnrpc.RegisterWalletUnlockerServer(grpcServer, pwService)
|
||||
|
||||
// Initialize, and register our implementation of the gRPC interface
|
||||
// exported by the rpcServer.
|
||||
rpcServer := newRPCServer(cfg, interceptorChain, implCfg, interceptor)
|
||||
@ -411,310 +361,14 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
|
||||
|
||||
defer cleanUp()
|
||||
|
||||
pwService.SetLoaderOpts([]btcwallet.LoaderOption{dbs.WalletDB})
|
||||
pwService.SetMacaroonDB(dbs.MacaroonDB)
|
||||
walletExists, err := pwService.WalletExists()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !walletExists {
|
||||
interceptorChain.SetWalletNotCreated()
|
||||
} else {
|
||||
interceptorChain.SetWalletLocked()
|
||||
}
|
||||
|
||||
// If we've started in auto unlock mode, then a wallet should already
|
||||
// exist because we don't want to enable the RPC unlocker in that case
|
||||
// for security reasons (an attacker could inject their seed since the
|
||||
// RPC is unauthenticated). Only if the user explicitly wants to allow
|
||||
// wallet creation we don't error out here.
|
||||
if cfg.WalletUnlockPasswordFile != "" && !walletExists &&
|
||||
!cfg.WalletUnlockAllowCreate {
|
||||
|
||||
return fmt.Errorf("wallet unlock password file was specified " +
|
||||
"but wallet does not exist; initialize the wallet " +
|
||||
"before using auto unlocking")
|
||||
}
|
||||
|
||||
// What wallet mode are we running in? We've already made sure the no
|
||||
// seed backup and auto unlock aren't both set during config parsing.
|
||||
switch {
|
||||
// No seed backup means we're also using the default password.
|
||||
case cfg.NoSeedBackup:
|
||||
// We continue normally, the default password has already been
|
||||
// set above.
|
||||
|
||||
// A password for unlocking is provided in a file.
|
||||
case cfg.WalletUnlockPasswordFile != "" && walletExists:
|
||||
ltndLog.Infof("Attempting automatic wallet unlock with " +
|
||||
"password provided in file")
|
||||
pwBytes, err := ioutil.ReadFile(cfg.WalletUnlockPasswordFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading password from file "+
|
||||
"%s: %v", cfg.WalletUnlockPasswordFile, err)
|
||||
}
|
||||
|
||||
// Remove any newlines at the end of the file. The lndinit tool
|
||||
// won't ever write a newline but maybe the file was provisioned
|
||||
// by another process or user.
|
||||
pwBytes = bytes.TrimRight(pwBytes, "\r\n")
|
||||
|
||||
// We have the password now, we can ask the unlocker service to
|
||||
// do the unlock for us.
|
||||
unlockedWallet, unloadWalletFn, err := pwService.LoadAndUnlock(
|
||||
pwBytes, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unlocking wallet with "+
|
||||
"password from file: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := unloadWalletFn(); err != nil {
|
||||
ltndLog.Errorf("Could not unload wallet: %v",
|
||||
err)
|
||||
}
|
||||
}()
|
||||
|
||||
privateWalletPw = pwBytes
|
||||
publicWalletPw = pwBytes
|
||||
walletInitParams.Wallet = unlockedWallet
|
||||
walletInitParams.UnloadWallet = unloadWalletFn
|
||||
|
||||
// If none of the automatic startup options are selected, we fall back
|
||||
// to the default behavior of waiting for the wallet creation/unlocking
|
||||
// over RPC.
|
||||
default:
|
||||
if err := interceptor.Notifier.NotifyReady(false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params, err := waitForWalletPassword(
|
||||
cfg, pwService, []btcwallet.LoaderOption{dbs.WalletDB},
|
||||
interceptor.ShutdownChannel(),
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to set up wallet password "+
|
||||
"listeners: %v", err)
|
||||
ltndLog.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
walletInitParams = *params
|
||||
privateWalletPw = walletInitParams.Password
|
||||
publicWalletPw = walletInitParams.Password
|
||||
defer func() {
|
||||
if err := walletInitParams.UnloadWallet(); err != nil {
|
||||
ltndLog.Errorf("Could not unload wallet: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if walletInitParams.RecoveryWindow > 0 {
|
||||
ltndLog.Infof("Wallet recovery mode enabled with "+
|
||||
"address lookahead of %d addresses",
|
||||
walletInitParams.RecoveryWindow)
|
||||
}
|
||||
}
|
||||
|
||||
var macaroonService *macaroons.Service
|
||||
if !cfg.NoMacaroons {
|
||||
// Create the macaroon authentication/authorization service.
|
||||
macaroonService, err = macaroons.NewService(
|
||||
dbs.MacaroonDB, "lnd", walletInitParams.StatelessInit,
|
||||
macaroons.IPLockChecker,
|
||||
macaroons.CustomChecker(interceptorChain),
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to set up macaroon "+
|
||||
"authentication: %v", err)
|
||||
ltndLog.Error(err)
|
||||
return err
|
||||
}
|
||||
defer macaroonService.Close()
|
||||
|
||||
// Try to unlock the macaroon store with the private password.
|
||||
// Ignore ErrAlreadyUnlocked since it could be unlocked by the
|
||||
// wallet unlocker.
|
||||
err = macaroonService.CreateUnlock(&privateWalletPw)
|
||||
if err != nil && err != macaroons.ErrAlreadyUnlocked {
|
||||
err := fmt.Errorf("unable to unlock macaroons: %v", err)
|
||||
ltndLog.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// In case we actually needed to unlock the wallet, we now need
|
||||
// to create an instance of the admin macaroon and send it to
|
||||
// the unlocker so it can forward it to the user. In no seed
|
||||
// backup mode, there's nobody listening on the channel and we'd
|
||||
// block here forever.
|
||||
if !cfg.NoSeedBackup {
|
||||
adminMacBytes, err := bakeMacaroon(
|
||||
ctx, macaroonService, adminPermissions(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The channel is buffered by one element so writing
|
||||
// should not block here.
|
||||
walletInitParams.MacResponseChan <- adminMacBytes
|
||||
|
||||
for _, lis := range grpcListeners {
|
||||
if lis.MacChan != nil {
|
||||
lis.MacChan <- adminMacBytes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the user requested a stateless initialization, no macaroon
|
||||
// files should be created.
|
||||
if !walletInitParams.StatelessInit &&
|
||||
!fileExists(cfg.AdminMacPath) &&
|
||||
!fileExists(cfg.ReadMacPath) &&
|
||||
!fileExists(cfg.InvoiceMacPath) {
|
||||
|
||||
// Create macaroon files for lncli to use if they don't
|
||||
// exist.
|
||||
err = genMacaroons(
|
||||
ctx, macaroonService, cfg.AdminMacPath,
|
||||
cfg.ReadMacPath, cfg.InvoiceMacPath,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to create macaroons "+
|
||||
"%v", err)
|
||||
ltndLog.Error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// As a security service to the user, if they requested
|
||||
// stateless initialization and there are macaroon files on disk
|
||||
// we log a warning.
|
||||
if walletInitParams.StatelessInit {
|
||||
msg := "Found %s macaroon on disk (%s) even though " +
|
||||
"--stateless_init was requested. Unencrypted " +
|
||||
"state is accessible by the host system. You " +
|
||||
"should change the password and use " +
|
||||
"--new_mac_root_key with --stateless_init to " +
|
||||
"clean up and invalidate old macaroons."
|
||||
|
||||
if fileExists(cfg.AdminMacPath) {
|
||||
ltndLog.Warnf(msg, "admin", cfg.AdminMacPath)
|
||||
}
|
||||
if fileExists(cfg.ReadMacPath) {
|
||||
ltndLog.Warnf(msg, "readonly", cfg.ReadMacPath)
|
||||
}
|
||||
if fileExists(cfg.InvoiceMacPath) {
|
||||
ltndLog.Warnf(msg, "invoice", cfg.InvoiceMacPath)
|
||||
}
|
||||
}
|
||||
|
||||
// We add the macaroon service to our RPC interceptor. This
|
||||
// will start checking macaroons against permissions on every
|
||||
// RPC invocation.
|
||||
interceptorChain.AddMacaroonService(macaroonService)
|
||||
}
|
||||
|
||||
// Now that the wallet password has been provided, transition the RPC
|
||||
// state into Unlocked.
|
||||
interceptorChain.SetWalletUnlocked()
|
||||
|
||||
// Since calls to the WalletUnlocker service wait for a response on the
|
||||
// macaroon channel, we close it here to make sure they return in case
|
||||
// we did not return the admin macaroon above. This will be the case if
|
||||
// --no-macaroons is used.
|
||||
close(walletInitParams.MacResponseChan)
|
||||
|
||||
// We'll also close all the macaroon channels since lnd is done sending
|
||||
// macaroon data over it.
|
||||
for _, lis := range grpcListeners {
|
||||
if lis.MacChan != nil {
|
||||
close(lis.MacChan)
|
||||
}
|
||||
}
|
||||
|
||||
// With the information parsed from the configuration, create valid
|
||||
// instances of the pertinent interfaces required to operate the
|
||||
// Lightning Network Daemon.
|
||||
//
|
||||
// When we create the chain control, we need storage for the height
|
||||
// hints and also the wallet itself, for these two we want them to be
|
||||
// replicated, so we'll pass in the remote channel DB instance.
|
||||
chainControlCfg := &chainreg.Config{
|
||||
Bitcoin: cfg.Bitcoin,
|
||||
Litecoin: cfg.Litecoin,
|
||||
PrimaryChain: cfg.registeredChains.PrimaryChain,
|
||||
HeightHintCacheQueryDisable: cfg.HeightHintCacheQueryDisable,
|
||||
NeutrinoMode: cfg.NeutrinoMode,
|
||||
BitcoindMode: cfg.BitcoindMode,
|
||||
LitecoindMode: cfg.LitecoindMode,
|
||||
BtcdMode: cfg.BtcdMode,
|
||||
LtcdMode: cfg.LtcdMode,
|
||||
HeightHintDB: dbs.HeightHintDB,
|
||||
ChanStateDB: dbs.ChanStateDB.ChannelStateDB(),
|
||||
NeutrinoCS: neutrinoCS,
|
||||
ActiveNetParams: cfg.ActiveNetParams,
|
||||
FeeURL: cfg.FeeURL,
|
||||
Dialer: func(addr string) (net.Conn, error) {
|
||||
return cfg.net.Dial("tcp", addr, cfg.ConnectionTimeout)
|
||||
},
|
||||
BlockCache: blockCache,
|
||||
}
|
||||
|
||||
// Let's go ahead and create the partial chain control now that is only
|
||||
// dependent on our configuration and doesn't require any wallet
|
||||
// specific information.
|
||||
partialChainControl, cleanup, err := chainreg.NewPartialChainControl(
|
||||
chainControlCfg,
|
||||
activeChainControl, cleanUp, err := implCfg.BuildChainControl(
|
||||
ctx, dbs, interceptorChain, grpcListeners,
|
||||
)
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to create partial chain control: %v",
|
||||
err)
|
||||
ltndLog.Error(err)
|
||||
return err
|
||||
return fmt.Errorf("error loading chain control: %v", err)
|
||||
}
|
||||
|
||||
walletConfig := &btcwallet.Config{
|
||||
PrivatePass: privateWalletPw,
|
||||
PublicPass: publicWalletPw,
|
||||
Birthday: walletInitParams.Birthday,
|
||||
RecoveryWindow: walletInitParams.RecoveryWindow,
|
||||
NetParams: cfg.ActiveNetParams.Params,
|
||||
CoinType: cfg.ActiveNetParams.CoinType,
|
||||
Wallet: walletInitParams.Wallet,
|
||||
LoaderOptions: []btcwallet.LoaderOption{dbs.WalletDB},
|
||||
}
|
||||
|
||||
// Parse coin selection strategy.
|
||||
switch cfg.CoinSelectionStrategy {
|
||||
case "largest":
|
||||
walletConfig.CoinSelectionStrategy = wallet.CoinSelectionLargest
|
||||
|
||||
case "random":
|
||||
walletConfig.CoinSelectionStrategy = wallet.CoinSelectionRandom
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown coin selection strategy %v",
|
||||
cfg.CoinSelectionStrategy)
|
||||
}
|
||||
|
||||
// We've created the wallet configuration now, so we can finish
|
||||
// initializing the main chain control.
|
||||
activeChainControl, cleanup, err := chainreg.NewChainControl(
|
||||
walletConfig, partialChainControl,
|
||||
)
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to create chain control: %v", err)
|
||||
ltndLog.Error(err)
|
||||
return err
|
||||
}
|
||||
defer cleanUp()
|
||||
|
||||
// Finally before we start the server, we'll register the "holy
|
||||
// trinity" of interface for our current "home chain" with the active
|
||||
@ -841,7 +495,8 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
|
||||
// connections.
|
||||
server, err := newServer(
|
||||
cfg, cfg.Listeners, dbs, activeChainControl, &idKeyDesc,
|
||||
walletInitParams.ChansToRestore, chainedAcceptor, torController,
|
||||
activeChainControl.Cfg.WalletUnlockParams.ChansToRestore,
|
||||
chainedAcceptor, torController,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to create server: %v", err)
|
||||
@ -1340,14 +995,7 @@ func startRestProxy(cfg *Config, rpcServer *rpcServer, restDialOpts []grpc.DialO
|
||||
mux := proxy.NewServeMux(customMarshalerOption)
|
||||
|
||||
// Register our services with the REST proxy.
|
||||
err := lnrpc.RegisterWalletUnlockerHandlerFromEndpoint(
|
||||
ctx, mux, restProxyDest, restDialOpts,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = lnrpc.RegisterStateHandlerFromEndpoint(
|
||||
err := lnrpc.RegisterStateHandlerFromEndpoint(
|
||||
ctx, mux, restProxyDest, restDialOpts,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -99,7 +99,7 @@ func Start(extraArgs string, rpcReady Callback) {
|
||||
Ready: rpcListening,
|
||||
}},
|
||||
}
|
||||
implCfg := loadedConfig.ImplementationConfig()
|
||||
implCfg := loadedConfig.ImplementationConfig(shutdownInterceptor)
|
||||
|
||||
// Call the "real" main in a nested manner so the defers will properly
|
||||
// be executed in the case of a graceful shutdown.
|
||||
|
Loading…
Reference in New Issue
Block a user