2017-11-28 03:12:09 +03:00
|
|
|
// Copyright (c) 2013-2017 The btcsuite developers
|
|
|
|
// Copyright (c) 2015-2016 The Decred developers
|
|
|
|
// Copyright (C) 2015-2017 The Lightning Network Developers
|
|
|
|
|
2015-12-17 03:42:52 +03:00
|
|
|
package main
|
|
|
|
|
2015-12-30 03:23:27 +03:00
|
|
|
import (
|
2017-08-14 23:54:06 +03:00
|
|
|
"bytes"
|
2017-06-06 01:18:06 +03:00
|
|
|
"crypto/rand"
|
2017-08-14 23:54:06 +03:00
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
|
|
|
"crypto/x509/pkix"
|
|
|
|
"encoding/pem"
|
2015-12-30 03:23:27 +03:00
|
|
|
"fmt"
|
2017-07-26 02:22:06 +03:00
|
|
|
"io/ioutil"
|
2017-08-14 23:54:06 +03:00
|
|
|
"math/big"
|
2015-12-30 03:23:27 +03:00
|
|
|
"net"
|
2016-01-17 06:09:41 +03:00
|
|
|
"net/http"
|
|
|
|
_ "net/http/pprof"
|
2015-12-30 05:59:16 +03:00
|
|
|
"os"
|
2018-01-06 00:43:47 +03:00
|
|
|
"path/filepath"
|
2016-03-23 04:50:11 +03:00
|
|
|
"runtime"
|
2017-10-15 03:08:27 +03:00
|
|
|
"runtime/pprof"
|
2017-12-17 20:28:38 +03:00
|
|
|
"sync"
|
2017-06-06 01:18:06 +03:00
|
|
|
"time"
|
2015-12-30 03:23:27 +03:00
|
|
|
|
2018-01-16 19:18:41 +03:00
|
|
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
2017-08-18 04:50:57 +03:00
|
|
|
|
2016-10-16 01:35:26 +03:00
|
|
|
"golang.org/x/net/context"
|
|
|
|
|
2016-03-23 04:50:11 +03:00
|
|
|
"google.golang.org/grpc"
|
2017-07-26 02:22:06 +03:00
|
|
|
"google.golang.org/grpc/credentials"
|
2016-03-23 04:50:11 +03:00
|
|
|
|
2016-10-16 00:38:47 +03:00
|
|
|
proxy "github.com/grpc-ecosystem/grpc-gateway/runtime"
|
2017-12-22 08:23:24 +03:00
|
|
|
flags "github.com/jessevdk/go-flags"
|
2017-08-11 07:40:15 +03:00
|
|
|
"github.com/lightningnetwork/lnd/autopilot"
|
2016-03-23 04:50:11 +03:00
|
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
2016-01-16 21:38:48 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
2017-06-06 01:18:06 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
2017-08-18 04:50:57 +03:00
|
|
|
"github.com/lightningnetwork/lnd/macaroons"
|
2017-10-12 12:37:37 +03:00
|
|
|
"github.com/lightningnetwork/lnd/walletunlocker"
|
2017-06-06 05:30:55 +03:00
|
|
|
"github.com/roasbeef/btcd/btcec"
|
2018-02-04 05:14:09 +03:00
|
|
|
"github.com/roasbeef/btcd/wire"
|
2017-07-31 02:01:44 +03:00
|
|
|
"github.com/roasbeef/btcutil"
|
2015-12-30 03:23:27 +03:00
|
|
|
)
|
|
|
|
|
2017-07-26 02:22:06 +03:00
|
|
|
const (
|
2017-08-14 23:54:06 +03:00
|
|
|
// Make certificate valid for 14 months.
|
|
|
|
autogenCertValidity = 14 /*months*/ * 30 /*days*/ * 24 * time.Hour
|
2017-07-26 02:22:06 +03:00
|
|
|
)
|
|
|
|
|
2016-03-23 04:50:11 +03:00
|
|
|
var (
|
2017-05-03 05:49:14 +03:00
|
|
|
cfg *config
|
|
|
|
shutdownChannel = make(chan struct{})
|
|
|
|
registeredChains = newChainRegistry()
|
2017-08-14 23:54:06 +03:00
|
|
|
|
2017-08-22 10:03:03 +03:00
|
|
|
macaroonDatabaseDir string
|
|
|
|
|
2017-08-14 23:54:06 +03:00
|
|
|
// End of ASN.1 time.
|
|
|
|
endOfTime = time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
|
|
|
|
|
|
|
|
// Max serial number.
|
|
|
|
serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128)
|
2017-10-12 12:37:37 +03:00
|
|
|
|
|
|
|
/*
|
|
|
|
* These cipher suites fit the following criteria:
|
|
|
|
* - Don't use outdated algorithms like SHA-1 and 3DES
|
|
|
|
* - Don't use ECB mode or other insecure symmetric methods
|
|
|
|
* - Included in the TLS v1.2 suite
|
|
|
|
* - Are available in the Go 1.7.6 standard library (more are
|
|
|
|
* available in 1.8.3 and will be added after lnd no longer
|
|
|
|
* supports 1.7, including suites that support CBC mode)
|
|
|
|
*
|
|
|
|
* The cipher suites are ordered from strongest to weakest
|
|
|
|
* primitives, but the client's preference order has more
|
|
|
|
* effect during negotiation.
|
|
|
|
**/
|
|
|
|
tlsCipherSuites = []uint16{
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
|
|
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
|
|
|
tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
|
|
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
|
|
}
|
2016-03-23 04:50:11 +03:00
|
|
|
)
|
|
|
|
|
2016-07-13 03:03:29 +03:00
|
|
|
// lndMain is the true entry point for lnd. This function is required since
|
|
|
|
// defers created in the top-level scope of a main method aren't executed if
|
|
|
|
// os.Exit() is called.
|
|
|
|
func lndMain() error {
|
2016-03-23 04:50:11 +03:00
|
|
|
// Load the configuration, and parse any command line options. This
|
|
|
|
// function will also set up logging properly.
|
2016-02-23 05:24:56 +03:00
|
|
|
loadedConfig, err := loadConfig()
|
|
|
|
if err != nil {
|
2016-07-13 03:03:29 +03:00
|
|
|
return err
|
2016-02-23 05:24:56 +03:00
|
|
|
}
|
2016-03-23 04:50:11 +03:00
|
|
|
cfg = loadedConfig
|
2017-06-21 18:07:44 +03:00
|
|
|
defer func() {
|
|
|
|
if logRotator != nil {
|
|
|
|
logRotator.Close()
|
|
|
|
}
|
|
|
|
}()
|
2016-03-23 04:50:11 +03:00
|
|
|
|
|
|
|
// Show version at startup.
|
|
|
|
ltndLog.Infof("Version %s", version())
|
2016-02-23 05:24:56 +03:00
|
|
|
|
2016-06-21 07:42:07 +03:00
|
|
|
// Enable http profiling server if requested.
|
|
|
|
if cfg.Profile != "" {
|
|
|
|
go func() {
|
|
|
|
listenAddr := net.JoinHostPort("", cfg.Profile)
|
|
|
|
profileRedirect := http.RedirectHandler("/debug/pprof",
|
|
|
|
http.StatusSeeOther)
|
|
|
|
http.Handle("/", profileRedirect)
|
|
|
|
fmt.Println(http.ListenAndServe(listenAddr, nil))
|
|
|
|
}()
|
|
|
|
}
|
2016-01-17 06:01:06 +03:00
|
|
|
|
2017-10-15 03:08:27 +03:00
|
|
|
// Write cpu profile if requested.
|
|
|
|
if cfg.CPUProfile != "" {
|
|
|
|
f, err := os.Create(cfg.CPUProfile)
|
|
|
|
if err != nil {
|
|
|
|
ltndLog.Errorf("Unable to create cpu profile: %v", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
pprof.StartCPUProfile(f)
|
|
|
|
defer f.Close()
|
|
|
|
defer pprof.StopCPUProfile()
|
|
|
|
}
|
|
|
|
|
2018-01-06 00:43:47 +03:00
|
|
|
// Create the network-segmented directory for the channel database.
|
|
|
|
graphDir := filepath.Join(cfg.DataDir,
|
|
|
|
defaultGraphSubDirname,
|
|
|
|
normalizeNetwork(activeNetParams.Name))
|
|
|
|
|
2016-03-23 04:50:11 +03:00
|
|
|
// Open the channeldb, which is dedicated to storing channel, and
|
2017-01-13 08:01:50 +03:00
|
|
|
// network related metadata.
|
2018-01-06 00:43:47 +03:00
|
|
|
chanDB, err := channeldb.Open(graphDir)
|
2016-03-23 04:50:11 +03:00
|
|
|
if err != nil {
|
2017-05-03 06:03:13 +03:00
|
|
|
ltndLog.Errorf("unable to open channeldb: %v", err)
|
2016-07-13 03:03:29 +03:00
|
|
|
return err
|
2016-03-23 04:50:11 +03:00
|
|
|
}
|
|
|
|
defer chanDB.Close()
|
|
|
|
|
2017-08-18 04:50:57 +03:00
|
|
|
// Only process macaroons if --no-macaroons isn't set.
|
2018-01-16 19:18:41 +03:00
|
|
|
ctx := context.Background()
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
var macaroonService *bakery.Bakery
|
2017-08-18 04:50:57 +03:00
|
|
|
if !cfg.NoMacaroons {
|
|
|
|
// Create the macaroon authentication/authorization service.
|
2018-01-16 19:18:41 +03:00
|
|
|
macaroonService, err = macaroons.NewService(macaroonDatabaseDir,
|
|
|
|
macaroons.IPLockChecker)
|
2017-08-18 04:50:57 +03:00
|
|
|
if err != nil {
|
2017-08-22 10:03:03 +03:00
|
|
|
srvrLog.Errorf("unable to create macaroon service: %v", err)
|
2017-08-18 04:50:57 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create macaroon files for lncli to use if they don't exist.
|
2017-08-22 10:03:03 +03:00
|
|
|
if !fileExists(cfg.AdminMacPath) && !fileExists(cfg.ReadMacPath) {
|
2018-01-16 19:18:41 +03:00
|
|
|
err = genMacaroons(ctx, macaroonService,
|
|
|
|
cfg.AdminMacPath, cfg.ReadMacPath)
|
2017-08-18 04:50:57 +03:00
|
|
|
if err != nil {
|
|
|
|
ltndLog.Errorf("unable to create macaroon "+
|
|
|
|
"files: %v", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-12 12:37:37 +03:00
|
|
|
// Ensure we create TLS key and certificate if they don't exist
|
|
|
|
if !fileExists(cfg.TLSCertPath) && !fileExists(cfg.TLSKeyPath) {
|
|
|
|
if err := genCertPair(cfg.TLSCertPath, cfg.TLSKeyPath); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
cert, err := tls.LoadX509KeyPair(cfg.TLSCertPath, cfg.TLSKeyPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tlsConf := &tls.Config{
|
|
|
|
Certificates: []tls.Certificate{cert},
|
|
|
|
CipherSuites: tlsCipherSuites,
|
|
|
|
MinVersion: tls.VersionTLS12,
|
|
|
|
}
|
|
|
|
sCreds := credentials.NewTLS(tlsConf)
|
|
|
|
serverOpts := []grpc.ServerOption{grpc.Creds(sCreds)}
|
2017-12-17 20:28:38 +03:00
|
|
|
cCreds, err := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "")
|
2017-10-12 12:37:37 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
proxyOpts := []grpc.DialOption{grpc.WithTransportCredentials(cCreds)}
|
|
|
|
|
|
|
|
// We wait until the user provides a password over RPC. In case lnd is
|
|
|
|
// started with the --noencryptwallet flag, we use the default password
|
|
|
|
// "hello" for wallet encryption.
|
2017-10-20 05:53:19 +03:00
|
|
|
privateWalletPw := []byte("hello")
|
|
|
|
publicWalletPw := []byte("public")
|
2017-10-12 12:37:37 +03:00
|
|
|
if !cfg.NoEncryptWallet {
|
2017-10-20 05:53:19 +03:00
|
|
|
privateWalletPw, publicWalletPw, err = waitForWalletPassword(
|
2017-12-17 20:28:38 +03:00
|
|
|
cfg.RPCListeners, cfg.RESTListeners, serverOpts, proxyOpts,
|
2017-10-20 05:53:19 +03:00
|
|
|
tlsConf, macaroonService,
|
|
|
|
)
|
2017-10-12 12:37:37 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-18 21:49:32 +03:00
|
|
|
// With the information parsed from the configuration, create valid
|
2017-08-22 10:03:03 +03:00
|
|
|
// instances of the pertinent interfaces required to operate the
|
2017-05-18 21:49:32 +03:00
|
|
|
// Lightning Network Daemon.
|
2017-10-12 12:37:37 +03:00
|
|
|
activeChainControl, chainCleanUp, err := newChainControlFromConfig(cfg,
|
2017-10-20 05:53:19 +03:00
|
|
|
chanDB, privateWalletPw, publicWalletPw)
|
2015-12-30 03:23:27 +03:00
|
|
|
if err != nil {
|
2017-05-18 21:49:32 +03:00
|
|
|
fmt.Printf("unable to create chain control: %v\n", err)
|
2016-07-13 03:03:29 +03:00
|
|
|
return err
|
2015-12-30 03:23:27 +03:00
|
|
|
}
|
2017-05-25 03:46:37 +03:00
|
|
|
if chainCleanUp != nil {
|
|
|
|
defer chainCleanUp()
|
|
|
|
}
|
2017-03-27 20:25:44 +03:00
|
|
|
|
2017-05-03 05:49:14 +03:00
|
|
|
// Finally before we start the server, we'll register the "holy
|
|
|
|
// trinity" of interface for our current "home chain" with the active
|
|
|
|
// chainRegistry interface.
|
|
|
|
primaryChain := registeredChains.PrimaryChain()
|
2017-05-18 21:49:32 +03:00
|
|
|
registeredChains.RegisterChain(primaryChain, activeChainControl)
|
2017-05-03 05:49:14 +03:00
|
|
|
|
2017-06-06 01:18:06 +03:00
|
|
|
idPrivKey, err := activeChainControl.wallet.GetIdentitykey()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
idPrivKey.Curve = btcec.S256()
|
|
|
|
|
2017-05-18 21:49:32 +03:00
|
|
|
// Set up the core server which will listen for incoming peer
|
|
|
|
// connections.
|
2017-12-17 20:28:38 +03:00
|
|
|
server, err := newServer(cfg.Listeners, chanDB, activeChainControl,
|
2017-06-06 01:18:06 +03:00
|
|
|
idPrivKey)
|
2016-01-17 06:09:41 +03:00
|
|
|
if err != nil {
|
2016-03-25 00:32:55 +03:00
|
|
|
srvrLog.Errorf("unable to create server: %v\n", err)
|
2016-07-13 03:03:29 +03:00
|
|
|
return err
|
2016-01-17 06:09:41 +03:00
|
|
|
}
|
2017-06-06 01:18:06 +03:00
|
|
|
|
2017-06-06 01:44:18 +03:00
|
|
|
// Next, we'll initialize the funding manager itself so it can answer
|
|
|
|
// queries while the wallet+chain are still syncing.
|
2017-06-06 01:18:06 +03:00
|
|
|
nodeSigner := newNodeSigner(idPrivKey)
|
|
|
|
var chanIDSeed [32]byte
|
|
|
|
if _, err := rand.Read(chanIDSeed[:]); err != nil {
|
2016-08-04 22:37:50 +03:00
|
|
|
return err
|
|
|
|
}
|
2017-06-06 01:18:06 +03:00
|
|
|
fundingMgr, err := newFundingManager(fundingConfig{
|
|
|
|
IDKey: idPrivKey.PubKey(),
|
|
|
|
Wallet: activeChainControl.wallet,
|
|
|
|
Notifier: activeChainControl.chainNotifier,
|
|
|
|
FeeEstimator: activeChainControl.feeEstimator,
|
|
|
|
SignMessage: func(pubKey *btcec.PublicKey,
|
|
|
|
msg []byte) (*btcec.Signature, error) {
|
2016-01-17 06:09:41 +03:00
|
|
|
|
2017-06-06 01:18:06 +03:00
|
|
|
if pubKey.IsEqual(idPrivKey.PubKey()) {
|
|
|
|
return nodeSigner.SignMessage(pubKey, msg)
|
|
|
|
}
|
|
|
|
|
|
|
|
return activeChainControl.msgSigner.SignMessage(
|
|
|
|
pubKey, msg,
|
|
|
|
)
|
|
|
|
},
|
2017-08-08 15:48:12 +03:00
|
|
|
CurrentNodeAnnouncement: func() (lnwire.NodeAnnouncement, error) {
|
2017-08-05 04:32:25 +03:00
|
|
|
return server.genNodeAnnouncement(true)
|
2017-07-14 22:47:52 +03:00
|
|
|
},
|
2017-06-06 01:18:06 +03:00
|
|
|
SendAnnouncement: func(msg lnwire.Message) error {
|
2017-08-22 10:03:03 +03:00
|
|
|
errChan := server.authGossiper.ProcessLocalAnnouncement(msg,
|
2017-06-06 01:18:06 +03:00
|
|
|
idPrivKey.PubKey())
|
2017-06-08 20:48:07 +03:00
|
|
|
return <-errChan
|
2017-06-06 01:18:06 +03:00
|
|
|
},
|
2018-01-21 07:27:25 +03:00
|
|
|
ArbiterChan: server.breachArbiter.newContracts,
|
2017-09-25 01:01:05 +03:00
|
|
|
SendToPeer: server.SendToPeer,
|
|
|
|
NotifyWhenOnline: server.NotifyWhenOnline,
|
|
|
|
FindPeer: server.FindPeer,
|
|
|
|
TempChanIDSeed: chanIDSeed,
|
2017-06-06 01:18:06 +03:00
|
|
|
FindChannel: func(chanID lnwire.ChannelID) (*lnwallet.LightningChannel, error) {
|
|
|
|
dbChannels, err := chanDB.FetchAllChannels()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, channel := range dbChannels {
|
2017-07-31 02:01:44 +03:00
|
|
|
if chanID.IsChanPoint(&channel.FundingOutpoint) {
|
2018-01-17 07:56:51 +03:00
|
|
|
// TODO(rosbeef): populate baecon
|
2017-06-06 01:18:06 +03:00
|
|
|
return lnwallet.NewLightningChannel(
|
|
|
|
activeChainControl.signer,
|
2018-01-17 07:56:51 +03:00
|
|
|
server.witnessBeacon,
|
2017-06-06 01:18:06 +03:00
|
|
|
channel)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("unable to find channel")
|
|
|
|
},
|
2017-06-17 01:15:26 +03:00
|
|
|
DefaultRoutingPolicy: activeChainControl.routingPolicy,
|
2017-12-17 18:13:23 +03:00
|
|
|
NumRequiredConfs: func(chanAmt btcutil.Amount,
|
|
|
|
pushAmt lnwire.MilliSatoshi) uint16 {
|
|
|
|
// For large channels we increase the number
|
|
|
|
// of confirmations we require for the
|
|
|
|
// channel to be considered open. As it is
|
|
|
|
// always the responder that gets to choose
|
|
|
|
// value, the pushAmt is value being pushed
|
|
|
|
// to us. This means we have more to lose
|
|
|
|
// in the case this gets re-orged out, and
|
|
|
|
// we will require more confirmations before
|
|
|
|
// we consider it open.
|
|
|
|
// TODO(halseth): Use Litecoin params in case
|
|
|
|
// of LTC channels.
|
|
|
|
|
|
|
|
// In case the user has explicitly specified
|
|
|
|
// a default value for the number of
|
|
|
|
// confirmations, we use it.
|
|
|
|
defaultConf := uint16(cfg.Bitcoin.DefaultNumChanConfs)
|
|
|
|
if defaultConf != 0 {
|
|
|
|
return defaultConf
|
|
|
|
}
|
|
|
|
|
|
|
|
// If not we return a value scaled linearly
|
|
|
|
// between 3 and 6, depending on channel size.
|
|
|
|
// TODO(halseth): Use 1 as minimum?
|
|
|
|
minConf := uint64(3)
|
|
|
|
maxConf := uint64(6)
|
|
|
|
maxChannelSize := uint64(
|
|
|
|
lnwire.NewMSatFromSatoshis(maxFundingAmount))
|
|
|
|
stake := lnwire.NewMSatFromSatoshis(chanAmt) + pushAmt
|
|
|
|
conf := maxConf * uint64(stake) / maxChannelSize
|
|
|
|
if conf < minConf {
|
|
|
|
conf = minConf
|
|
|
|
}
|
|
|
|
if conf > maxConf {
|
|
|
|
conf = maxConf
|
|
|
|
}
|
|
|
|
return uint16(conf)
|
2017-07-31 02:01:59 +03:00
|
|
|
},
|
|
|
|
RequiredRemoteDelay: func(chanAmt btcutil.Amount) uint16 {
|
2017-12-17 18:13:23 +03:00
|
|
|
// We scale the remote CSV delay (the time the
|
|
|
|
// remote have to claim funds in case of a unilateral
|
|
|
|
// close) linearly from minRemoteDelay blocks
|
|
|
|
// for small channels, to maxRemoteDelay blocks
|
|
|
|
// for channels of size maxFundingAmount.
|
|
|
|
// TODO(halseth): Litecoin parameter for LTC.
|
|
|
|
|
|
|
|
// In case the user has explicitly specified
|
|
|
|
// a default value for the remote delay, we
|
|
|
|
// use it.
|
|
|
|
defaultDelay := uint16(cfg.Bitcoin.DefaultRemoteDelay)
|
|
|
|
if defaultDelay > 0 {
|
|
|
|
return defaultDelay
|
|
|
|
}
|
|
|
|
|
|
|
|
// If not we scale according to channel size.
|
|
|
|
delay := uint16(maxRemoteDelay *
|
|
|
|
chanAmt / maxFundingAmount)
|
|
|
|
if delay < minRemoteDelay {
|
|
|
|
delay = minRemoteDelay
|
|
|
|
}
|
|
|
|
if delay > maxRemoteDelay {
|
|
|
|
delay = maxRemoteDelay
|
|
|
|
}
|
|
|
|
return delay
|
2017-07-31 02:01:59 +03:00
|
|
|
},
|
2018-01-19 01:07:56 +03:00
|
|
|
WatchNewChannel: server.chainArb.WatchNewChannel,
|
2018-02-04 05:14:09 +03:00
|
|
|
ReportShortChanID: func(chanPoint wire.OutPoint,
|
|
|
|
sid lnwire.ShortChannelID) error {
|
|
|
|
|
|
|
|
cid := lnwire.NewChanIDFromOutPoint(&chanPoint)
|
|
|
|
return server.htlcSwitch.UpdateShortChanID(cid, sid)
|
|
|
|
},
|
2016-03-23 04:50:11 +03:00
|
|
|
})
|
2017-06-06 01:18:06 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := fundingMgr.Start(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
server.fundingMgr = fundingMgr
|
2016-03-23 04:50:11 +03:00
|
|
|
|
2018-01-16 19:18:41 +03:00
|
|
|
// Check macaroon authentication if macaroons aren't disabled.
|
|
|
|
if macaroonService != nil {
|
|
|
|
serverOpts = append(serverOpts,
|
|
|
|
grpc.UnaryInterceptor(macaroons.UnaryServerInterceptor(
|
|
|
|
macaroonService, permissions)),
|
|
|
|
grpc.StreamInterceptor(macaroons.StreamServerInterceptor(
|
|
|
|
macaroonService, permissions)),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2017-06-06 01:44:18 +03:00
|
|
|
// Initialize, and register our implementation of the gRPC interface
|
|
|
|
// exported by the rpcServer.
|
2018-01-16 19:18:41 +03:00
|
|
|
rpcServer := newRPCServer(server)
|
2017-06-06 01:18:06 +03:00
|
|
|
if err := rpcServer.Start(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-10-12 12:37:37 +03:00
|
|
|
|
|
|
|
grpcServer := grpc.NewServer(serverOpts...)
|
2017-06-06 01:18:06 +03:00
|
|
|
lnrpc.RegisterLightningServer(grpcServer, rpcServer)
|
2015-12-30 03:23:27 +03:00
|
|
|
|
2017-06-06 01:44:18 +03:00
|
|
|
// Next, Start the gRPC server listening for HTTP/2 connections.
|
2017-12-17 20:28:38 +03:00
|
|
|
for _, listener := range cfg.RPCListeners {
|
|
|
|
lis, err := net.Listen("tcp", listener)
|
|
|
|
if err != nil {
|
|
|
|
ltndLog.Errorf("RPC server unable to listen on %s", listener)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer lis.Close()
|
|
|
|
go func() {
|
|
|
|
rpcsLog.Infof("RPC server listening on %s", lis.Addr())
|
|
|
|
grpcServer.Serve(lis)
|
|
|
|
}()
|
2015-12-30 03:23:27 +03:00
|
|
|
}
|
2017-12-17 20:28:38 +03:00
|
|
|
|
2016-10-16 00:38:47 +03:00
|
|
|
// Finally, start the REST proxy for our gRPC server above.
|
|
|
|
mux := proxy.NewServeMux()
|
2017-12-17 20:28:38 +03:00
|
|
|
err = lnrpc.RegisterLightningHandlerFromEndpoint(ctx, mux,
|
|
|
|
cfg.RPCListeners[0], proxyOpts)
|
2016-10-16 00:38:47 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-12-17 20:28:38 +03:00
|
|
|
for _, restEndpoint := range cfg.RESTListeners {
|
2017-08-14 23:54:06 +03:00
|
|
|
listener, err := tls.Listen("tcp", restEndpoint, tlsConf)
|
|
|
|
if err != nil {
|
2017-12-17 20:28:38 +03:00
|
|
|
ltndLog.Errorf("gRPC proxy unable to listen on %s", restEndpoint)
|
|
|
|
return err
|
2017-08-14 23:54:06 +03:00
|
|
|
}
|
2017-12-17 20:28:38 +03:00
|
|
|
defer listener.Close()
|
|
|
|
go func() {
|
|
|
|
rpcsLog.Infof("gRPC proxy started at %s", listener.Addr())
|
|
|
|
http.Serve(listener, mux)
|
|
|
|
}()
|
|
|
|
}
|
2016-10-16 00:38:47 +03:00
|
|
|
|
2017-06-06 01:18:06 +03:00
|
|
|
// If we're not in simnet mode, 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.
|
|
|
|
if !(cfg.Bitcoin.SimNet || cfg.Litecoin.SimNet) {
|
|
|
|
_, bestHeight, err := activeChainControl.chainIO.GetBestBlock()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
ltndLog.Infof("Waiting for chain backend to finish sync, "+
|
|
|
|
"start_height=%v", bestHeight)
|
|
|
|
|
|
|
|
for {
|
|
|
|
synced, err := activeChainControl.wallet.IsSynced()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if synced {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
time.Sleep(time.Second * 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, bestHeight, err = activeChainControl.chainIO.GetBestBlock()
|
|
|
|
if err != nil {
|
|
|
|
return 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 {
|
2017-12-30 20:38:15 +03:00
|
|
|
srvrLog.Errorf("unable to start server: %v\n", err)
|
2017-06-06 01:18:06 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-08-11 07:40:15 +03:00
|
|
|
// Now that the server has started, if the autopilot mode is currently
|
|
|
|
// active, then we'll initialize a fresh instance of it and start it.
|
|
|
|
var pilot *autopilot.Agent
|
|
|
|
if cfg.Autopilot.Active {
|
|
|
|
pilot, err := initAutoPilot(server, cfg.Autopilot)
|
|
|
|
if err != nil {
|
|
|
|
ltndLog.Errorf("unable to create autopilot agent: %v",
|
|
|
|
err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := pilot.Start(); err != nil {
|
|
|
|
ltndLog.Errorf("unable to start autopilot agent: %v",
|
|
|
|
err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-06 01:18:06 +03:00
|
|
|
addInterruptHandler(func() {
|
|
|
|
ltndLog.Infof("Gracefully shutting down the server...")
|
|
|
|
rpcServer.Stop()
|
|
|
|
fundingMgr.Stop()
|
|
|
|
server.Stop()
|
2017-08-11 07:40:15 +03:00
|
|
|
|
|
|
|
if pilot != nil {
|
|
|
|
pilot.Stop()
|
|
|
|
}
|
|
|
|
|
2017-06-06 01:18:06 +03:00
|
|
|
server.WaitForShutdown()
|
|
|
|
})
|
|
|
|
|
2016-03-23 04:50:11 +03:00
|
|
|
// Wait for shutdown signal from either a graceful server stop or from
|
|
|
|
// the interrupt handler.
|
|
|
|
<-shutdownChannel
|
|
|
|
ltndLog.Info("Shutdown complete")
|
2016-07-13 03:03:29 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
// Use all processor cores.
|
|
|
|
// TODO(roasbeef): remove this if required version # is > 1.6?
|
|
|
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
|
|
|
|
|
|
|
// Call the "real" main in a nested manner so the defers will properly
|
|
|
|
// be executed in the case of a graceful shutdown.
|
|
|
|
if err := lndMain(); err != nil {
|
2017-04-01 15:33:49 +03:00
|
|
|
if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp {
|
|
|
|
} else {
|
|
|
|
fmt.Fprintln(os.Stderr, err)
|
|
|
|
}
|
2016-07-13 03:03:29 +03:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
2015-12-17 03:42:52 +03:00
|
|
|
}
|
2017-07-26 02:22:06 +03:00
|
|
|
|
|
|
|
// fileExists reports whether the named file or directory exists.
|
|
|
|
// This function is taken from https://github.com/btcsuite/btcd
|
|
|
|
func fileExists(name string) bool {
|
|
|
|
if _, err := os.Stat(name); err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2017-08-14 23:54:06 +03:00
|
|
|
// genCertPair generates a key/cert pair to the paths provided. The
|
|
|
|
// auto-generated certificates should *not* be used in production for public
|
|
|
|
// access as they're self-signed and don't necessarily contain all of the
|
|
|
|
// desired hostnames for the service. For production/public use, consider a
|
|
|
|
// real PKI.
|
|
|
|
//
|
|
|
|
// This function is adapted from https://github.com/btcsuite/btcd and
|
|
|
|
// https://github.com/btcsuite/btcutil
|
2017-07-26 02:22:06 +03:00
|
|
|
func genCertPair(certFile, keyFile string) error {
|
|
|
|
rpcsLog.Infof("Generating TLS certificates...")
|
|
|
|
|
|
|
|
org := "lnd autogenerated cert"
|
2017-08-14 23:54:06 +03:00
|
|
|
now := time.Now()
|
|
|
|
validUntil := now.Add(autogenCertValidity)
|
|
|
|
|
|
|
|
// Check that the certificate validity isn't past the ASN.1 end of time.
|
|
|
|
if validUntil.After(endOfTime) {
|
|
|
|
validUntil = endOfTime
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a serial number that's below the serialNumberLimit.
|
|
|
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to generate serial number: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Collect the host's IP addresses, including loopback, in a slice.
|
|
|
|
ipAddresses := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}
|
|
|
|
|
|
|
|
// addIP appends an IP address only if it isn't already in the slice.
|
|
|
|
addIP := func(ipAddr net.IP) {
|
|
|
|
for _, ip := range ipAddresses {
|
|
|
|
if bytes.Equal(ip, ipAddr) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ipAddresses = append(ipAddresses, ipAddr)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add all the interface IPs that aren't already in the slice.
|
|
|
|
addrs, err := net.InterfaceAddrs()
|
2017-07-26 02:22:06 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-08-14 23:54:06 +03:00
|
|
|
for _, a := range addrs {
|
|
|
|
ipAddr, _, err := net.ParseCIDR(a.String())
|
|
|
|
if err == nil {
|
|
|
|
addIP(ipAddr)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Collect the host's names into a slice.
|
|
|
|
host, err := os.Hostname()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
dnsNames := []string{host}
|
|
|
|
if host != "localhost" {
|
|
|
|
dnsNames = append(dnsNames, "localhost")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a private key for the certificate.
|
|
|
|
priv, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Construct the certificate template.
|
|
|
|
template := x509.Certificate{
|
|
|
|
SerialNumber: serialNumber,
|
|
|
|
Subject: pkix.Name{
|
|
|
|
Organization: []string{org},
|
|
|
|
CommonName: host,
|
|
|
|
},
|
|
|
|
NotBefore: now.Add(-time.Hour * 24),
|
|
|
|
NotAfter: validUntil,
|
|
|
|
|
|
|
|
KeyUsage: x509.KeyUsageKeyEncipherment |
|
|
|
|
x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
|
|
|
IsCA: true, // so can sign self.
|
|
|
|
BasicConstraintsValid: true,
|
|
|
|
|
|
|
|
DNSNames: dnsNames,
|
|
|
|
IPAddresses: ipAddresses,
|
|
|
|
|
|
|
|
// This signature algorithm is most likely to be compatible
|
|
|
|
// with clients using less-common TLS libraries like BoringSSL.
|
|
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
|
|
}
|
|
|
|
|
|
|
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template,
|
|
|
|
&template, &priv.PublicKey, priv)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to create certificate: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
certBuf := &bytes.Buffer{}
|
|
|
|
err = pem.Encode(certBuf, &pem.Block{Type: "CERTIFICATE",
|
|
|
|
Bytes: derBytes})
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to encode certificate: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
keybytes := x509.MarshalPKCS1PrivateKey(priv)
|
|
|
|
keyBuf := &bytes.Buffer{}
|
|
|
|
err = pem.Encode(keyBuf, &pem.Block{Type: "RSA PRIVATE KEY",
|
|
|
|
Bytes: keybytes})
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to encode private key: %v", err)
|
|
|
|
}
|
2017-07-26 02:22:06 +03:00
|
|
|
|
|
|
|
// Write cert and key files.
|
2017-08-14 23:54:06 +03:00
|
|
|
if err = ioutil.WriteFile(certFile, certBuf.Bytes(), 0644); err != nil {
|
2017-07-26 02:22:06 +03:00
|
|
|
return err
|
|
|
|
}
|
2017-08-14 23:54:06 +03:00
|
|
|
if err = ioutil.WriteFile(keyFile, keyBuf.Bytes(), 0600); err != nil {
|
2017-07-26 02:22:06 +03:00
|
|
|
os.Remove(certFile)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
rpcsLog.Infof("Done generating TLS certificates")
|
|
|
|
return nil
|
|
|
|
}
|
2017-08-18 04:50:57 +03:00
|
|
|
|
|
|
|
// genMacaroons generates a pair of macaroon files; one admin-level and one
|
|
|
|
// read-only. These can also be used to generate more granular macaroons.
|
2018-01-16 19:18:41 +03:00
|
|
|
func genMacaroons(ctx context.Context, svc *bakery.Bakery, admFile,
|
|
|
|
roFile string) error {
|
|
|
|
|
|
|
|
// Generate the read-only macaroon and write it to a file.
|
|
|
|
roMacaroon, err := svc.Oven.NewMacaroon(ctx, bakery.LatestVersion, nil,
|
|
|
|
readPermissions...)
|
2017-08-18 04:50:57 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-01-16 19:18:41 +03:00
|
|
|
roBytes, err := roMacaroon.M().MarshalBinary()
|
2017-08-18 04:50:57 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-01-16 19:18:41 +03:00
|
|
|
if err = ioutil.WriteFile(roFile, roBytes, 0644); err != nil {
|
|
|
|
os.Remove(admFile)
|
2017-08-18 04:50:57 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-01-16 19:18:41 +03:00
|
|
|
// Generate the admin macaroon and write it to a file.
|
|
|
|
admMacaroon, err := svc.Oven.NewMacaroon(ctx, bakery.LatestVersion,
|
|
|
|
nil, append(readPermissions, writePermissions...)...)
|
2017-09-13 23:44:05 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-01-16 19:18:41 +03:00
|
|
|
admBytes, err := admMacaroon.M().MarshalBinary()
|
2017-08-18 04:50:57 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-01-16 19:18:41 +03:00
|
|
|
if err = ioutil.WriteFile(admFile, admBytes, 0600); err != nil {
|
2017-08-18 04:50:57 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2017-10-12 12:37:37 +03:00
|
|
|
|
|
|
|
// waitForWalletPassword will spin up gRPC and REST endpoints for the
|
|
|
|
// WalletUnlocker server, and block until a password is provided by
|
|
|
|
// the user to this RPC server.
|
2017-12-17 20:28:38 +03:00
|
|
|
func waitForWalletPassword(grpcEndpoints, restEndpoints []string,
|
2017-10-12 12:37:37 +03:00
|
|
|
serverOpts []grpc.ServerOption, proxyOpts []grpc.DialOption,
|
2018-01-16 19:18:41 +03:00
|
|
|
tlsConf *tls.Config, macaroonService *bakery.Bakery) ([]byte, []byte, error) {
|
2017-10-20 05:53:19 +03:00
|
|
|
|
2017-10-12 12:37:37 +03:00
|
|
|
// Set up a new PasswordService, which will listen
|
|
|
|
// for passwords provided over RPC.
|
|
|
|
grpcServer := grpc.NewServer(serverOpts...)
|
|
|
|
|
|
|
|
chainConfig := cfg.Bitcoin
|
|
|
|
if registeredChains.PrimaryChain() == litecoinChain {
|
|
|
|
chainConfig = cfg.Litecoin
|
|
|
|
}
|
|
|
|
pwService := walletunlocker.New(macaroonService,
|
|
|
|
chainConfig.ChainDir, activeNetParams.Params)
|
|
|
|
lnrpc.RegisterWalletUnlockerServer(grpcServer, pwService)
|
|
|
|
|
2017-12-17 20:28:38 +03:00
|
|
|
// 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
|
2017-10-12 12:37:37 +03:00
|
|
|
|
2017-12-17 20:28:38 +03:00
|
|
|
for _, grpcEndpoint := range grpcEndpoints {
|
|
|
|
// Start a gRPC server listening for HTTP/2 connections, solely
|
|
|
|
// used for getting the encryption password from the client.
|
|
|
|
lis, err := net.Listen("tcp", grpcEndpoint)
|
|
|
|
if err != nil {
|
|
|
|
ltndLog.Errorf("password RPC server unable to listen on %s",
|
|
|
|
grpcEndpoint)
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
defer lis.Close()
|
2017-10-12 12:37:37 +03:00
|
|
|
|
2017-12-17 20:28:38 +03:00
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
rpcsLog.Infof("password RPC server listening on %s", lis.Addr())
|
|
|
|
wg.Done()
|
|
|
|
grpcServer.Serve(lis)
|
|
|
|
}()
|
|
|
|
}
|
2017-10-12 12:37:37 +03:00
|
|
|
|
|
|
|
// Start a REST proxy for our gRPC server above.
|
|
|
|
ctx := context.Background()
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
mux := proxy.NewServeMux()
|
2017-12-17 20:28:38 +03:00
|
|
|
|
|
|
|
err := lnrpc.RegisterWalletUnlockerHandlerFromEndpoint(ctx, mux,
|
|
|
|
grpcEndpoints[0], proxyOpts)
|
2017-10-12 12:37:37 +03:00
|
|
|
if err != nil {
|
2017-10-20 05:53:19 +03:00
|
|
|
return nil, nil, err
|
2017-10-12 12:37:37 +03:00
|
|
|
}
|
2017-12-17 20:28:38 +03:00
|
|
|
|
2017-10-12 12:37:37 +03:00
|
|
|
srv := &http.Server{Handler: mux}
|
|
|
|
|
2017-12-17 20:28:38 +03:00
|
|
|
for _, restEndpoint := range restEndpoints {
|
|
|
|
lis, err := tls.Listen("tcp", restEndpoint, tlsConf)
|
2017-10-12 12:37:37 +03:00
|
|
|
if err != nil {
|
2017-12-17 20:28:38 +03:00
|
|
|
ltndLog.Errorf("password gRPC proxy unable to listen on %s",
|
|
|
|
restEndpoint)
|
|
|
|
return nil, nil, err
|
2017-10-12 12:37:37 +03:00
|
|
|
}
|
2017-12-17 20:28:38 +03:00
|
|
|
defer lis.Close()
|
|
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
rpcsLog.Infof("password gRPC proxy started at %s", lis.Addr())
|
|
|
|
wg.Done()
|
|
|
|
srv.Serve(lis)
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Wait for gRPC and REST servers to be up running.
|
|
|
|
wg.Wait()
|
2017-10-12 12:37:37 +03:00
|
|
|
|
|
|
|
// Wait for user to provide the password.
|
|
|
|
ltndLog.Infof("Waiting for wallet encryption password. " +
|
|
|
|
"Use `lncli create` to create wallet, or " +
|
|
|
|
"`lncli unlock` to unlock already created wallet.")
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
case walletPw := <-pwService.CreatePasswords:
|
2017-10-20 05:53:19 +03:00
|
|
|
return walletPw, walletPw, nil
|
2017-10-12 12:37:37 +03:00
|
|
|
case walletPw := <-pwService.UnlockPasswords:
|
2017-10-20 05:53:19 +03:00
|
|
|
return walletPw, walletPw, nil
|
2017-10-12 12:37:37 +03:00
|
|
|
case <-shutdownChannel:
|
2017-10-20 05:53:19 +03:00
|
|
|
return nil, nil, fmt.Errorf("shutting down")
|
2017-10-12 12:37:37 +03:00
|
|
|
}
|
|
|
|
}
|