mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
965 lines
28 KiB
Go
965 lines
28 KiB
Go
// Copyright (c) 2013-2017 The btcsuite developers
|
|
// Copyright (c) 2015-2016 The Decred developers
|
|
// Copyright (C) 2015-2022 The Lightning Network Developers
|
|
|
|
package lnd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/http/pprof"
|
|
"os"
|
|
"runtime"
|
|
runtimePprof "runtime/pprof"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
proxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
|
"github.com/lightningnetwork/lnd/autopilot"
|
|
"github.com/lightningnetwork/lnd/build"
|
|
"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/macaroons"
|
|
"github.com/lightningnetwork/lnd/monitoring"
|
|
"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"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/keepalive"
|
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
|
"gopkg.in/macaroon.v2"
|
|
)
|
|
|
|
const (
|
|
// adminMacaroonFilePermissions is the file permission that is used for
|
|
// creating the admin macaroon file.
|
|
//
|
|
// Why 640 is safe:
|
|
// Assuming a reasonably secure Linux system, it will have a
|
|
// separate group for each user. E.g. a new user lnd gets assigned group
|
|
// lnd which nothing else belongs to. A system that does not do this is
|
|
// inherently broken already.
|
|
//
|
|
// Since there is no other user in the group, no other user can read
|
|
// admin macaroon unless the administrator explicitly allowed it. Thus
|
|
// there's no harm allowing group read.
|
|
adminMacaroonFilePermissions = 0640
|
|
)
|
|
|
|
// AdminAuthOptions returns a list of DialOptions that can be used to
|
|
// authenticate with the RPC server with admin capabilities.
|
|
// skipMacaroons=true should be set if we don't want to include macaroons with
|
|
// the auth options. This is needed for instance for the WalletUnlocker
|
|
// service, which must be usable also before macaroons are created.
|
|
//
|
|
// NOTE: This should only be called after the RPCListener has signaled it is
|
|
// ready.
|
|
func AdminAuthOptions(cfg *Config, skipMacaroons bool) ([]grpc.DialOption,
|
|
error) {
|
|
|
|
creds, err := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read TLS cert: %v", err)
|
|
}
|
|
|
|
// Create a dial options array.
|
|
opts := []grpc.DialOption{
|
|
grpc.WithTransportCredentials(creds),
|
|
}
|
|
|
|
// Get the admin macaroon if macaroons are active.
|
|
if !skipMacaroons && !cfg.NoMacaroons {
|
|
// Load the admin macaroon file.
|
|
macBytes, err := ioutil.ReadFile(cfg.AdminMacPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read macaroon "+
|
|
"path (check the network setting!): %v", err)
|
|
}
|
|
|
|
mac := &macaroon.Macaroon{}
|
|
if err = mac.UnmarshalBinary(macBytes); err != nil {
|
|
return nil, fmt.Errorf("unable to decode macaroon: %v",
|
|
err)
|
|
}
|
|
|
|
// Now we append the macaroon credentials to the dial options.
|
|
cred, err := macaroons.NewMacaroonCredential(mac)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error cloning mac: %v", err)
|
|
}
|
|
opts = append(opts, grpc.WithPerRPCCredentials(cred))
|
|
}
|
|
|
|
return opts, nil
|
|
}
|
|
|
|
// ListenerWithSignal is a net.Listener that has an additional Ready channel
|
|
// that will be closed when a server starts listening.
|
|
type ListenerWithSignal struct {
|
|
net.Listener
|
|
|
|
// Ready will be closed by the server listening on Listener.
|
|
Ready chan struct{}
|
|
|
|
// MacChan is an optional way to pass the admin macaroon to the program
|
|
// that started lnd. The channel should be buffered to avoid lnd being
|
|
// blocked on sending to the channel.
|
|
MacChan chan []byte
|
|
}
|
|
|
|
// ListenerCfg is a wrapper around custom listeners that can be passed to lnd
|
|
// when calling its main method.
|
|
type ListenerCfg struct {
|
|
// RPCListeners can be set to the listeners to use for the RPC server.
|
|
// If empty a regular network listener will be created.
|
|
RPCListeners []*ListenerWithSignal
|
|
}
|
|
|
|
var errStreamIsolationWithProxySkip = errors.New(
|
|
"while stream isolation is enabled, the TOR proxy may not be skipped",
|
|
)
|
|
|
|
// Main is the true entry point for lnd. It accepts a fully populated and
|
|
// validated main configuration struct and an optional listener config struct.
|
|
// This function starts all main system components then blocks until a signal
|
|
// is received on the shutdownChan at which point everything is shut down again.
|
|
func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
|
|
interceptor signal.Interceptor) error {
|
|
|
|
defer func() {
|
|
ltndLog.Info("Shutdown complete\n")
|
|
err := cfg.LogWriter.Close()
|
|
if err != nil {
|
|
ltndLog.Errorf("Could not close log rotator: %v", err)
|
|
}
|
|
}()
|
|
|
|
mkErr := func(format string, args ...interface{}) error {
|
|
ltndLog.Errorf("Shutting down because error in main "+
|
|
"method: "+format, args...)
|
|
return fmt.Errorf(format, args...)
|
|
}
|
|
|
|
// Show version at startup.
|
|
ltndLog.Infof("Version: %s commit=%s, build=%s, logging=%s, "+
|
|
"debuglevel=%s", build.Version(), build.Commit,
|
|
build.Deployment, build.LoggingType, cfg.DebugLevel)
|
|
|
|
var network string
|
|
switch {
|
|
case cfg.Bitcoin.TestNet3:
|
|
network = "testnet"
|
|
|
|
case cfg.Bitcoin.MainNet:
|
|
network = "mainnet"
|
|
|
|
case cfg.Bitcoin.SimNet:
|
|
network = "simnet"
|
|
|
|
case cfg.Bitcoin.RegTest:
|
|
network = "regtest"
|
|
|
|
case cfg.Bitcoin.SigNet:
|
|
network = "signet"
|
|
}
|
|
|
|
ltndLog.Infof("Active chain: %v (network=%v)",
|
|
strings.Title(BitcoinChainName), network,
|
|
)
|
|
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
// Enable http profiling server if requested.
|
|
if cfg.Profile != "" {
|
|
// Create the http handler.
|
|
pprofMux := http.NewServeMux()
|
|
pprofMux.HandleFunc("/debug/pprof/", pprof.Index)
|
|
pprofMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
|
pprofMux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
|
pprofMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
|
pprofMux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
|
|
|
if cfg.BlockingProfile != 0 {
|
|
runtime.SetBlockProfileRate(cfg.BlockingProfile)
|
|
}
|
|
if cfg.MutexProfile != 0 {
|
|
runtime.SetMutexProfileFraction(cfg.MutexProfile)
|
|
}
|
|
|
|
// Redirect all requests to the pprof handler, thus visiting
|
|
// `127.0.0.1:6060` will be redirected to
|
|
// `127.0.0.1:6060/debug/pprof`.
|
|
pprofMux.Handle("/", http.RedirectHandler(
|
|
"/debug/pprof/", http.StatusSeeOther,
|
|
))
|
|
|
|
ltndLog.Infof("Pprof listening on %v", cfg.Profile)
|
|
|
|
// Create the pprof server.
|
|
pprofServer := &http.Server{
|
|
Addr: cfg.Profile,
|
|
Handler: pprofMux,
|
|
ReadHeaderTimeout: cfg.HTTPHeaderTimeout,
|
|
}
|
|
|
|
// Shut the server down when lnd is shutting down.
|
|
defer func() {
|
|
ltndLog.Info("Stopping pprof server...")
|
|
err := pprofServer.Shutdown(ctx)
|
|
if err != nil {
|
|
ltndLog.Errorf("Stop pprof server got err: %v",
|
|
err)
|
|
}
|
|
}()
|
|
|
|
// Start the pprof server.
|
|
go func() {
|
|
err := pprofServer.ListenAndServe()
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
ltndLog.Errorf("Serving pprof got err: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Write cpu profile if requested.
|
|
if cfg.CPUProfile != "" {
|
|
f, err := os.Create(cfg.CPUProfile)
|
|
if err != nil {
|
|
return mkErr("unable to create CPU profile: %v", err)
|
|
}
|
|
_ = runtimePprof.StartCPUProfile(f)
|
|
defer func() {
|
|
_ = f.Close()
|
|
}()
|
|
defer runtimePprof.StopCPUProfile()
|
|
}
|
|
|
|
// Run configuration dependent DB pre-initialization. Note that this
|
|
// needs to be done early and once during the startup process, before
|
|
// any DB access.
|
|
if err := cfg.DB.Init(ctx, cfg.graphDatabaseDir()); err != nil {
|
|
return mkErr("error initializing DBs: %v", err)
|
|
}
|
|
|
|
tlsManagerCfg := &TLSManagerCfg{
|
|
TLSCertPath: cfg.TLSCertPath,
|
|
TLSKeyPath: cfg.TLSKeyPath,
|
|
TLSEncryptKey: cfg.TLSEncryptKey,
|
|
TLSExtraIPs: cfg.TLSExtraIPs,
|
|
TLSExtraDomains: cfg.TLSExtraDomains,
|
|
TLSAutoRefresh: cfg.TLSAutoRefresh,
|
|
TLSDisableAutofill: cfg.TLSDisableAutofill,
|
|
TLSCertDuration: cfg.TLSCertDuration,
|
|
|
|
LetsEncryptDir: cfg.LetsEncryptDir,
|
|
LetsEncryptDomain: cfg.LetsEncryptDomain,
|
|
LetsEncryptListen: cfg.LetsEncryptListen,
|
|
|
|
DisableRestTLS: cfg.DisableRestTLS,
|
|
|
|
HTTPHeaderTimeout: cfg.HTTPHeaderTimeout,
|
|
}
|
|
tlsManager := NewTLSManager(tlsManagerCfg)
|
|
serverOpts, restDialOpts, restListen, cleanUp,
|
|
err := tlsManager.SetCertificateBeforeUnlock()
|
|
if err != nil {
|
|
return mkErr("error setting cert before unlock: %v", err)
|
|
}
|
|
if cleanUp != nil {
|
|
defer cleanUp()
|
|
}
|
|
|
|
// If we have chosen to start with a dedicated listener for the
|
|
// rpc server, we set it directly.
|
|
grpcListeners := append([]*ListenerWithSignal{}, lisCfg.RPCListeners...)
|
|
if len(grpcListeners) == 0 {
|
|
// Otherwise we create listeners from the RPCListeners defined
|
|
// in the config.
|
|
for _, grpcEndpoint := range cfg.RPCListeners {
|
|
// Start a gRPC server listening for HTTP/2
|
|
// connections.
|
|
lis, err := lncfg.ListenOnAddress(grpcEndpoint)
|
|
if err != nil {
|
|
return mkErr("unable to listen on %s: %v",
|
|
grpcEndpoint, err)
|
|
}
|
|
defer lis.Close()
|
|
|
|
grpcListeners = append(
|
|
grpcListeners, &ListenerWithSignal{
|
|
Listener: lis,
|
|
Ready: make(chan struct{}),
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
// Create a new RPC interceptor that we'll add to the GRPC server. This
|
|
// will be used to log the API calls invoked on the GRPC server.
|
|
interceptorChain := rpcperms.NewInterceptorChain(
|
|
rpcsLog, cfg.NoMacaroons, cfg.RPCMiddleware.Mandatory,
|
|
)
|
|
if err := interceptorChain.Start(); err != nil {
|
|
return mkErr("error starting interceptor chain: %v", err)
|
|
}
|
|
defer func() {
|
|
err := interceptorChain.Stop()
|
|
if err != nil {
|
|
ltndLog.Warnf("error stopping RPC interceptor "+
|
|
"chain: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Allow the user to overwrite some defaults of the gRPC library related
|
|
// to connection keepalive (server side and client side pings).
|
|
serverKeepalive := keepalive.ServerParameters{
|
|
Time: cfg.GRPC.ServerPingTime,
|
|
Timeout: cfg.GRPC.ServerPingTimeout,
|
|
}
|
|
clientKeepalive := keepalive.EnforcementPolicy{
|
|
MinTime: cfg.GRPC.ClientPingMinWait,
|
|
PermitWithoutStream: cfg.GRPC.ClientAllowPingWithoutStream,
|
|
}
|
|
|
|
rpcServerOpts := interceptorChain.CreateServerOpts()
|
|
serverOpts = append(serverOpts, rpcServerOpts...)
|
|
serverOpts = append(
|
|
serverOpts, grpc.MaxRecvMsgSize(lnrpc.MaxGrpcMsgSize),
|
|
grpc.KeepaliveParams(serverKeepalive),
|
|
grpc.KeepaliveEnforcementPolicy(clientKeepalive),
|
|
)
|
|
|
|
grpcServer := grpc.NewServer(serverOpts...)
|
|
defer grpcServer.Stop()
|
|
|
|
// We'll also register the RPC interceptor chain as the StateServer, as
|
|
// it can be used to query for the current state of the wallet.
|
|
lnrpc.RegisterStateServer(grpcServer, interceptorChain)
|
|
|
|
// Initialize, and register our implementation of the gRPC interface
|
|
// exported by the rpcServer.
|
|
rpcServer := newRPCServer(cfg, interceptorChain, implCfg, interceptor)
|
|
err = rpcServer.RegisterWithGrpcServer(grpcServer)
|
|
if err != nil {
|
|
return mkErr("error registering gRPC server: %v", err)
|
|
}
|
|
|
|
// Now that both the WalletUnlocker and LightningService have been
|
|
// registered with the GRPC server, we can start listening.
|
|
err = startGrpcListen(cfg, grpcServer, grpcListeners)
|
|
if err != nil {
|
|
return mkErr("error starting gRPC listener: %v", err)
|
|
}
|
|
|
|
// Now start the REST proxy for our gRPC server above. We'll ensure
|
|
// we direct LND to connect to its loopback address rather than a
|
|
// wildcard to prevent certificate issues when accessing the proxy
|
|
// externally.
|
|
stopProxy, err := startRestProxy(
|
|
cfg, rpcServer, restDialOpts, restListen,
|
|
)
|
|
if err != nil {
|
|
return mkErr("error starting REST proxy: %v", err)
|
|
}
|
|
defer stopProxy()
|
|
|
|
// Start leader election if we're running on etcd. Continuation will be
|
|
// blocked until this instance is elected as the current leader or
|
|
// shutting down.
|
|
elected := false
|
|
if cfg.Cluster.EnableLeaderElection {
|
|
electionCtx, cancelElection := context.WithCancel(ctx)
|
|
|
|
go func() {
|
|
<-interceptor.ShutdownChannel()
|
|
cancelElection()
|
|
}()
|
|
|
|
ltndLog.Infof("Using %v leader elector",
|
|
cfg.Cluster.LeaderElector)
|
|
|
|
leaderElector, err := cfg.Cluster.MakeLeaderElector(
|
|
electionCtx, cfg.DB,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if !elected {
|
|
return
|
|
}
|
|
|
|
ltndLog.Infof("Attempting to resign from leader role "+
|
|
"(%v)", cfg.Cluster.ID)
|
|
|
|
if err := leaderElector.Resign(); err != nil {
|
|
ltndLog.Errorf("Leader elector failed to "+
|
|
"resign: %v", err)
|
|
}
|
|
}()
|
|
|
|
ltndLog.Infof("Starting leadership campaign (%v)",
|
|
cfg.Cluster.ID)
|
|
|
|
if err := leaderElector.Campaign(electionCtx); err != nil {
|
|
return mkErr("leadership campaign failed: %v", err)
|
|
}
|
|
|
|
elected = true
|
|
ltndLog.Infof("Elected as leader (%v)", cfg.Cluster.ID)
|
|
}
|
|
|
|
dbs, cleanUp, err := implCfg.DatabaseBuilder.BuildDatabase(ctx)
|
|
switch {
|
|
case err == channeldb.ErrDryRunMigrationOK:
|
|
ltndLog.Infof("%v, exiting", err)
|
|
return nil
|
|
case err != nil:
|
|
return mkErr("unable to open databases: %v", err)
|
|
}
|
|
|
|
defer cleanUp()
|
|
|
|
partialChainControl, walletConfig, cleanUp, err := implCfg.BuildWalletConfig(
|
|
ctx, dbs, interceptorChain, grpcListeners,
|
|
)
|
|
if err != nil {
|
|
return mkErr("error creating wallet config: %v", err)
|
|
}
|
|
|
|
defer cleanUp()
|
|
|
|
activeChainControl, cleanUp, err := implCfg.BuildChainControl(
|
|
partialChainControl, walletConfig,
|
|
)
|
|
if err != nil {
|
|
return mkErr("error loading chain control: %v", err)
|
|
}
|
|
|
|
defer cleanUp()
|
|
|
|
// TODO(roasbeef): add rotation
|
|
idKeyDesc, err := activeChainControl.KeyRing.DeriveKey(
|
|
keychain.KeyLocator{
|
|
Family: keychain.KeyFamilyNodeKey,
|
|
Index: 0,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return mkErr("error deriving node key: %v", err)
|
|
}
|
|
|
|
if cfg.Tor.StreamIsolation && cfg.Tor.SkipProxyForClearNetTargets {
|
|
return errStreamIsolationWithProxySkip
|
|
}
|
|
|
|
if cfg.Tor.Active {
|
|
if cfg.Tor.SkipProxyForClearNetTargets {
|
|
srvrLog.Info("Onion services are accessible via Tor! " +
|
|
"NOTE: Traffic to clearnet services is not " +
|
|
"routed via Tor.")
|
|
} else {
|
|
srvrLog.Infof("Proxying all network traffic via Tor "+
|
|
"(stream_isolation=%v)! NOTE: Ensure the "+
|
|
"backend node is proxying over Tor as well",
|
|
cfg.Tor.StreamIsolation)
|
|
}
|
|
}
|
|
|
|
// If tor is active and either v2 or v3 onion services have been
|
|
// specified, make a tor controller and pass it into both the watchtower
|
|
// server and the regular lnd server.
|
|
var torController *tor.Controller
|
|
if cfg.Tor.Active && (cfg.Tor.V2 || cfg.Tor.V3) {
|
|
torController = tor.NewController(
|
|
cfg.Tor.Control, cfg.Tor.TargetIPAddress,
|
|
cfg.Tor.Password,
|
|
)
|
|
|
|
// Start the tor controller before giving it to any other
|
|
// subsystems.
|
|
if err := torController.Start(); err != nil {
|
|
return mkErr("unable to initialize tor controller: %v",
|
|
err)
|
|
}
|
|
defer func() {
|
|
if err := torController.Stop(); err != nil {
|
|
ltndLog.Errorf("error stopping tor "+
|
|
"controller: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
var tower *watchtower.Standalone
|
|
if cfg.Watchtower.Active {
|
|
towerKeyDesc, err := activeChainControl.KeyRing.DeriveKey(
|
|
keychain.KeyLocator{
|
|
Family: keychain.KeyFamilyTowerID,
|
|
Index: 0,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return mkErr("error deriving tower key: %v", err)
|
|
}
|
|
|
|
wtCfg := &watchtower.Config{
|
|
BlockFetcher: activeChainControl.ChainIO,
|
|
DB: dbs.TowerServerDB,
|
|
EpochRegistrar: activeChainControl.ChainNotifier,
|
|
Net: cfg.net,
|
|
NewAddress: func() (btcutil.Address, error) {
|
|
return activeChainControl.Wallet.NewAddress(
|
|
lnwallet.TaprootPubkey, false,
|
|
lnwallet.DefaultAccountName,
|
|
)
|
|
},
|
|
NodeKeyECDH: keychain.NewPubKeyECDH(
|
|
towerKeyDesc, activeChainControl.KeyRing,
|
|
),
|
|
PublishTx: activeChainControl.Wallet.PublishTransaction,
|
|
ChainHash: *cfg.ActiveNetParams.GenesisHash,
|
|
}
|
|
|
|
// If there is a tor controller (user wants auto hidden
|
|
// services), then store a pointer in the watchtower config.
|
|
if torController != nil {
|
|
wtCfg.TorController = torController
|
|
wtCfg.WatchtowerKeyPath = cfg.Tor.WatchtowerKeyPath
|
|
wtCfg.EncryptKey = cfg.Tor.EncryptKey
|
|
wtCfg.KeyRing = activeChainControl.KeyRing
|
|
|
|
switch {
|
|
case cfg.Tor.V2:
|
|
wtCfg.Type = tor.V2
|
|
case cfg.Tor.V3:
|
|
wtCfg.Type = tor.V3
|
|
}
|
|
}
|
|
|
|
wtConfig, err := cfg.Watchtower.Apply(
|
|
wtCfg, lncfg.NormalizeAddresses,
|
|
)
|
|
if err != nil {
|
|
return mkErr("unable to configure watchtower: %v", err)
|
|
}
|
|
|
|
tower, err = watchtower.New(wtConfig)
|
|
if err != nil {
|
|
return mkErr("unable to create watchtower: %v", err)
|
|
}
|
|
}
|
|
|
|
// Initialize the MultiplexAcceptor. If lnd was started with the
|
|
// zero-conf feature bit, then this will be a ZeroConfAcceptor.
|
|
// Otherwise, this will be a ChainedAcceptor.
|
|
var multiAcceptor chanacceptor.MultiplexAcceptor
|
|
if cfg.ProtocolOptions.ZeroConf() {
|
|
multiAcceptor = chanacceptor.NewZeroConfAcceptor()
|
|
} else {
|
|
multiAcceptor = chanacceptor.NewChainedAcceptor()
|
|
}
|
|
|
|
// Set up the core server which will listen for incoming peer
|
|
// connections.
|
|
server, err := newServer(
|
|
cfg, cfg.Listeners, dbs, activeChainControl, &idKeyDesc,
|
|
activeChainControl.Cfg.WalletUnlockParams.ChansToRestore,
|
|
multiAcceptor, torController, tlsManager,
|
|
)
|
|
if err != nil {
|
|
return mkErr("unable to create server: %v", err)
|
|
}
|
|
|
|
// Set up an autopilot manager from the current config. This will be
|
|
// used to manage the underlying autopilot agent, starting and stopping
|
|
// it at will.
|
|
atplCfg, err := initAutoPilot(
|
|
server, cfg.Autopilot, activeChainControl.MinHtlcIn,
|
|
cfg.ActiveNetParams,
|
|
)
|
|
if err != nil {
|
|
return mkErr("unable to initialize autopilot: %v", err)
|
|
}
|
|
|
|
atplManager, err := autopilot.NewManager(atplCfg)
|
|
if err != nil {
|
|
return mkErr("unable to create autopilot manager: %v", err)
|
|
}
|
|
if err := atplManager.Start(); err != nil {
|
|
return mkErr("unable to start autopilot manager: %v", err)
|
|
}
|
|
defer atplManager.Stop()
|
|
|
|
err = tlsManager.LoadPermanentCertificate(activeChainControl.KeyRing)
|
|
if err != nil {
|
|
return mkErr("unable to load permanent TLS certificate: %v",
|
|
err)
|
|
}
|
|
|
|
// Now we have created all dependencies necessary to populate and
|
|
// start the RPC server.
|
|
err = rpcServer.addDeps(
|
|
server, interceptorChain.MacaroonService(), cfg.SubRPCServers,
|
|
atplManager, server.invoices, tower, multiAcceptor,
|
|
)
|
|
if err != nil {
|
|
return mkErr("unable to add deps to RPC server: %v", err)
|
|
}
|
|
if err := rpcServer.Start(); err != nil {
|
|
return mkErr("unable to start RPC server: %v", err)
|
|
}
|
|
defer rpcServer.Stop()
|
|
|
|
// We transition the RPC state to Active, as the RPC server is up.
|
|
interceptorChain.SetRPCActive()
|
|
|
|
if err := interceptor.Notifier.NotifyReady(true); err != nil {
|
|
return mkErr("error notifying ready: %v", err)
|
|
}
|
|
|
|
// We'll wait until we're fully synced to continue the start up of the
|
|
// remainder of the daemon. This ensures that we don't accept any
|
|
// possibly invalid state transitions, or accept channels with spent
|
|
// funds.
|
|
_, bestHeight, err := activeChainControl.ChainIO.GetBestBlock()
|
|
if err != nil {
|
|
return mkErr("unable to determine chain tip: %v", err)
|
|
}
|
|
|
|
ltndLog.Infof("Waiting for chain backend to finish sync, "+
|
|
"start_height=%v", bestHeight)
|
|
|
|
for {
|
|
if !interceptor.Alive() {
|
|
return nil
|
|
}
|
|
|
|
synced, ts, err := activeChainControl.Wallet.IsSynced()
|
|
if err != nil {
|
|
return mkErr("unable to determine if wallet is "+
|
|
"synced: %v", err)
|
|
}
|
|
|
|
ltndLog.Debugf("Syncing to block timestamp: %v, is synced=%v",
|
|
time.Unix(ts, 0), synced)
|
|
|
|
if synced {
|
|
break
|
|
}
|
|
|
|
time.Sleep(time.Second * 1)
|
|
}
|
|
|
|
_, bestHeight, err = activeChainControl.ChainIO.GetBestBlock()
|
|
if err != nil {
|
|
return mkErr("unable to determine chain tip: %v", err)
|
|
}
|
|
|
|
ltndLog.Infof("Chain backend is fully synced (end_height=%v)!",
|
|
bestHeight)
|
|
|
|
// With all the relevant chains initialized, we can finally start the
|
|
// server itself.
|
|
if err := server.Start(); err != nil {
|
|
return mkErr("unable to start server: %v", err)
|
|
}
|
|
defer server.Stop()
|
|
|
|
// We transition the server state to Active, as the server is up.
|
|
interceptorChain.SetServerActive()
|
|
|
|
// Now that the server has started, if the autopilot mode is currently
|
|
// active, then we'll start the autopilot agent immediately. It will be
|
|
// stopped together with the autopilot service.
|
|
if cfg.Autopilot.Active {
|
|
if err := atplManager.StartAgent(); err != nil {
|
|
return mkErr("unable to start autopilot agent: %v", err)
|
|
}
|
|
}
|
|
|
|
if cfg.Watchtower.Active {
|
|
if err := tower.Start(); err != nil {
|
|
return mkErr("unable to start watchtower: %v", err)
|
|
}
|
|
defer tower.Stop()
|
|
}
|
|
|
|
// Wait for shutdown signal from either a graceful server stop or from
|
|
// the interrupt handler.
|
|
<-interceptor.ShutdownChannel()
|
|
return nil
|
|
}
|
|
|
|
// bakeMacaroon creates a new macaroon with newest version and the given
|
|
// permissions then returns it binary serialized.
|
|
func bakeMacaroon(ctx context.Context, svc *macaroons.Service,
|
|
permissions []bakery.Op) ([]byte, error) {
|
|
|
|
mac, err := svc.NewMacaroon(
|
|
ctx, macaroons.DefaultRootKeyID, permissions...,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return mac.M().MarshalBinary()
|
|
}
|
|
|
|
// saveMacaroon bakes a macaroon with the specified macaroon permissions and
|
|
// writes it to a file with the given filename and file permissions.
|
|
func saveMacaroon(ctx context.Context, svc *macaroons.Service, filename string,
|
|
macaroonPermissions []bakery.Op, filePermissions os.FileMode) error {
|
|
|
|
macaroonBytes, err := bakeMacaroon(ctx, svc, macaroonPermissions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(filename, macaroonBytes, filePermissions)
|
|
if err != nil {
|
|
_ = os.Remove(filename)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// genDefaultMacaroons checks for three default macaroon files and generates
|
|
// them if they do not exist; one admin-level, one for invoice access and one
|
|
// read-only. Each macaroon is checked and created independently to ensure all
|
|
// three exist. The admin macaroon can also be used to generate more granular
|
|
// macaroons.
|
|
func genDefaultMacaroons(ctx context.Context, svc *macaroons.Service,
|
|
admFile, roFile, invoiceFile string) error {
|
|
|
|
// First, we'll generate a macaroon that only allows the caller to
|
|
// access invoice related calls. This is useful for merchants and other
|
|
// services to allow an isolated instance that can only query and
|
|
// modify invoices.
|
|
if !lnrpc.FileExists(invoiceFile) {
|
|
err := saveMacaroon(
|
|
ctx, svc, invoiceFile, invoicePermissions, 0644,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Generate the read-only macaroon and write it to a file.
|
|
if !lnrpc.FileExists(roFile) {
|
|
err := saveMacaroon(
|
|
ctx, svc, roFile, readPermissions, 0644,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Generate the admin macaroon and write it to a file.
|
|
if !lnrpc.FileExists(admFile) {
|
|
err := saveMacaroon(
|
|
ctx, svc, admFile, adminPermissions(),
|
|
adminMacaroonFilePermissions,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// adminPermissions returns a list of all permissions in a safe way that doesn't
|
|
// modify any of the source lists.
|
|
func adminPermissions() []bakery.Op {
|
|
admin := make([]bakery.Op, len(readPermissions)+len(writePermissions))
|
|
copy(admin[:len(readPermissions)], readPermissions)
|
|
copy(admin[len(readPermissions):], writePermissions)
|
|
return admin
|
|
}
|
|
|
|
// createWalletUnlockerService creates a WalletUnlockerService from the passed
|
|
// config.
|
|
func createWalletUnlockerService(cfg *Config) *walletunlocker.UnlockerService {
|
|
// The macaroonFiles are passed to the wallet unlocker so they can be
|
|
// deleted and recreated in case the root macaroon key is also changed
|
|
// during the change password operation.
|
|
macaroonFiles := []string{
|
|
cfg.AdminMacPath, cfg.ReadMacPath, cfg.InvoiceMacPath,
|
|
}
|
|
|
|
return walletunlocker.New(
|
|
cfg.ActiveNetParams.Params, macaroonFiles,
|
|
cfg.ResetWalletTransactions, nil,
|
|
)
|
|
}
|
|
|
|
// startGrpcListen starts the GRPC server on the passed listeners.
|
|
func startGrpcListen(cfg *Config, grpcServer *grpc.Server,
|
|
listeners []*ListenerWithSignal) error {
|
|
|
|
// Use a WaitGroup so we can be sure the instructions on how to input the
|
|
// password is the last thing to be printed to the console.
|
|
var wg sync.WaitGroup
|
|
|
|
for _, lis := range listeners {
|
|
wg.Add(1)
|
|
go func(lis *ListenerWithSignal) {
|
|
rpcsLog.Infof("RPC server listening on %s", lis.Addr())
|
|
|
|
// Close the ready chan to indicate we are listening.
|
|
close(lis.Ready)
|
|
|
|
wg.Done()
|
|
_ = grpcServer.Serve(lis)
|
|
}(lis)
|
|
}
|
|
|
|
// If Prometheus monitoring is enabled, start the Prometheus exporter.
|
|
if cfg.Prometheus.Enabled() {
|
|
err := monitoring.ExportPrometheusMetrics(
|
|
grpcServer, cfg.Prometheus,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Wait for gRPC servers to be up running.
|
|
wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
// startRestProxy starts the given REST proxy on the listeners found in the
|
|
// config.
|
|
func startRestProxy(cfg *Config, rpcServer *rpcServer, restDialOpts []grpc.DialOption,
|
|
restListen func(net.Addr) (net.Listener, error)) (func(), error) {
|
|
|
|
// We use the first RPC listener as the destination for our REST proxy.
|
|
// If the listener is set to listen on all interfaces, we replace it
|
|
// with localhost, as we cannot dial it directly.
|
|
restProxyDest := cfg.RPCListeners[0].String()
|
|
switch {
|
|
case strings.Contains(restProxyDest, "0.0.0.0"):
|
|
restProxyDest = strings.Replace(
|
|
restProxyDest, "0.0.0.0", "127.0.0.1", 1,
|
|
)
|
|
|
|
case strings.Contains(restProxyDest, "[::]"):
|
|
restProxyDest = strings.Replace(
|
|
restProxyDest, "[::]", "[::1]", 1,
|
|
)
|
|
}
|
|
|
|
var shutdownFuncs []func()
|
|
shutdown := func() {
|
|
for _, shutdownFn := range shutdownFuncs {
|
|
shutdownFn()
|
|
}
|
|
}
|
|
|
|
// Start a REST proxy for our gRPC server.
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
shutdownFuncs = append(shutdownFuncs, cancel)
|
|
|
|
// We'll set up a proxy that will forward REST calls to the GRPC
|
|
// server.
|
|
//
|
|
// The default JSON marshaler of the REST proxy only sets OrigName to
|
|
// true, which instructs it to use the same field names as specified in
|
|
// the proto file and not switch to camel case. What we also want is
|
|
// that the marshaler prints all values, even if they are falsey.
|
|
customMarshalerOption := proxy.WithMarshalerOption(
|
|
proxy.MIMEWildcard, &proxy.JSONPb{
|
|
MarshalOptions: *lnrpc.RESTJsonMarshalOpts,
|
|
UnmarshalOptions: *lnrpc.RESTJsonUnmarshalOpts,
|
|
},
|
|
)
|
|
mux := proxy.NewServeMux(
|
|
customMarshalerOption,
|
|
|
|
// Don't allow falling back to other HTTP methods, we want exact
|
|
// matches only. The actual method to be used can be overwritten
|
|
// by setting X-HTTP-Method-Override so there should be no
|
|
// reason for not specifying the correct method in the first
|
|
// place.
|
|
proxy.WithDisablePathLengthFallback(),
|
|
)
|
|
|
|
// Register our services with the REST proxy.
|
|
err := rpcServer.RegisterWithRestProxy(
|
|
ctx, mux, restDialOpts, restProxyDest,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Wrap the default grpc-gateway handler with the WebSocket handler.
|
|
restHandler := lnrpc.NewWebSocketProxy(
|
|
mux, rpcsLog, cfg.WSPingInterval, cfg.WSPongWait,
|
|
lnrpc.LndClientStreamingURIs,
|
|
)
|
|
|
|
// Use a WaitGroup so we can be sure the instructions on how to input the
|
|
// password is the last thing to be printed to the console.
|
|
var wg sync.WaitGroup
|
|
|
|
// Now spin up a network listener for each requested port and start a
|
|
// goroutine that serves REST with the created mux there.
|
|
for _, restEndpoint := range cfg.RESTListeners {
|
|
lis, err := restListen(restEndpoint)
|
|
if err != nil {
|
|
ltndLog.Errorf("gRPC proxy unable to listen on %s",
|
|
restEndpoint)
|
|
return nil, err
|
|
}
|
|
|
|
shutdownFuncs = append(shutdownFuncs, func() {
|
|
err := lis.Close()
|
|
if err != nil {
|
|
rpcsLog.Errorf("Error closing listener: %v",
|
|
err)
|
|
}
|
|
})
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
rpcsLog.Infof("gRPC proxy started at %s", lis.Addr())
|
|
|
|
// Create our proxy chain now. A request will pass
|
|
// through the following chain:
|
|
// req ---> CORS handler --> WS proxy --->
|
|
// REST proxy --> gRPC endpoint
|
|
corsHandler := allowCORS(restHandler, cfg.RestCORS)
|
|
|
|
wg.Done()
|
|
err := http.Serve(lis, corsHandler)
|
|
if err != nil && !lnrpc.IsClosedConnError(err) {
|
|
rpcsLog.Error(err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Wait for REST servers to be up running.
|
|
wg.Wait()
|
|
|
|
return shutdown, nil
|
|
}
|