diff --git a/log.go b/log.go index d3f149df0..f604accef 100644 --- a/log.go +++ b/log.go @@ -28,6 +28,7 @@ import ( "github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/signal" "github.com/lightningnetwork/lnd/sweep" + "github.com/lightningnetwork/lnd/watchtower" ) // Loggers per subsystem. A single backend logger is created and all subsystem @@ -75,6 +76,7 @@ var ( arpcLog = build.NewSubLogger("ARPC", backendLog.Logger) invcLog = build.NewSubLogger("INVC", backendLog.Logger) nannLog = build.NewSubLogger("NANN", backendLog.Logger) + wtwrLog = build.NewSubLogger("WTWR", backendLog.Logger) ) // Initialize package-global logger variables. @@ -97,6 +99,7 @@ func init() { autopilotrpc.UseLogger(arpcLog) invoices.UseLogger(invcLog) netann.UseLogger(nannLog) + watchtower.UseLogger(wtwrLog) } // subsystemLoggers maps each subsystem identifier to its associated logger. @@ -125,6 +128,7 @@ var subsystemLoggers = map[string]btclog.Logger{ "ARPC": arpcLog, "INVC": invcLog, "NANN": nannLog, + "WTWR": wtwrLog, } // initLogRotator initializes the logging rotator to write logs to logFile and diff --git a/watchtower/conf.go b/watchtower/conf.go new file mode 100644 index 000000000..9383d8172 --- /dev/null +++ b/watchtower/conf.go @@ -0,0 +1,14 @@ +// +build !experimental + +package watchtower + +// Conf specifies the watchtower options that be configured from the command +// line or configuration file. In non-experimental builds, we disallow such +// configuration. +type Conf struct{} + +// Apply returns an error signaling that the Conf could not be applied in +// non-experimental builds. +func (c *Conf) Apply(cfg *Config) (*Config, error) { + return nil, ErrNonExperimentalConf +} diff --git a/watchtower/conf_experimental.go b/watchtower/conf_experimental.go new file mode 100644 index 000000000..4b4749989 --- /dev/null +++ b/watchtower/conf_experimental.go @@ -0,0 +1,65 @@ +// +build experimental + +package watchtower + +import ( + "time" + + "github.com/lightningnetwork/lnd/lncfg" +) + +// Conf specifies the watchtower options that can be configured from the command +// line or configuration file. +type Conf struct { + RawListeners []string `long:"listen" description:"Add interfaces/ports to listen for peer connections"` + + ReadTimeout time.Duration `long:"readtimeout" description:"Duration the watchtower server will wait for messages to be received before hanging up on clients"` + + WriteTimeout time.Duration `long:"writetimeout" description:"Duration the watchtower server will wait for messages to be written before hanging up on client connections"` +} + +// Apply completes the passed Config struct by applying any parsed Conf options. +// If the corresponding values parsed by Conf are already set in the Config, +// those fields will be not be modified. +func (c *Conf) Apply(cfg *Config) (*Config, error) { + // Set the Config's listening addresses if they are empty. + if cfg.ListenAddrs == nil { + // Without a network, we will be unable to resolve the listening + // addresses. + if cfg.Net == nil { + return nil, ErrNoNetwork + } + + // If no addresses are specified by the Config, we will resort + // to the default peer port. + if len(c.RawListeners) == 0 { + addr := DefaultPeerPortStr + c.RawListeners = append(c.RawListeners, addr) + } + + // Normalize the raw listening addresses so that they can be + // used by the brontide listener. + var err error + cfg.ListenAddrs, err = lncfg.NormalizeAddresses( + c.RawListeners, DefaultPeerPortStr, + cfg.Net.ResolveTCPAddr, + ) + if err != nil { + return nil, err + } + } + + // If the Config has no read timeout, we will use the parsed Conf + // value. + if cfg.ReadTimeout == 0 && c.ReadTimeout != 0 { + cfg.ReadTimeout = c.ReadTimeout + } + + // If the Config has no write timeout, we will use the parsed Conf + // value. + if cfg.WriteTimeout == 0 && c.WriteTimeout != 0 { + cfg.WriteTimeout = c.WriteTimeout + } + + return cfg, nil +} diff --git a/watchtower/config.go b/watchtower/config.go new file mode 100644 index 000000000..733ebb0bc --- /dev/null +++ b/watchtower/config.go @@ -0,0 +1,81 @@ +package watchtower + +import ( + "fmt" + "net" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/tor" + "github.com/lightningnetwork/lnd/watchtower/lookout" +) + +const ( + // DefaultPeerPort is the default server port to which clients can + // connect. + DefaultPeerPort = 9911 + + // DefaultReadTimeout is the default timeout after which the tower will + // hang up on a client if nothing is received. + DefaultReadTimeout = 15 * time.Second + + // DefaultWriteTimeout is the default timeout after which the tower will + // hang up on a client if it is unable to send a message. + DefaultWriteTimeout = 15 * time.Second +) + +var ( + // DefaultPeerPortStr is the default server port as a string. + DefaultPeerPortStr = fmt.Sprintf(":%d", DefaultPeerPort) +) + +// Config defines the resources and parameters used to configure a Watchtower. +// All nil-able elements with the Config must be set in order for the Watchtower +// to function properly. +type Config struct { + // BlockFetcher supports the ability to fetch blocks from the network by + // hash. + BlockFetcher lookout.BlockFetcher + + // DB provides access to persistent storage of sessions and state + // updates uploaded by watchtower clients, and the ability to query for + // breach hints when receiving new blocks. + DB DB + + // EpochRegistrar supports the ability to register for events + // corresponding to newly created blocks. + EpochRegistrar lookout.EpochRegistrar + + // Net specifies the network type that the watchtower will use to listen + // for client connections. Either a clear net or Tor are supported. + Net tor.Net + + // NewAddress is used to generate reward addresses, where a cut of + // successfully sent funds can be received. + NewAddress func() (btcutil.Address, error) + + // NodePrivKey is private key to be used in accepting new brontide + // connections. + NodePrivKey *btcec.PrivateKey + + // PublishTx provides the ability to send a signed transaction to the + // network. + // + // TODO(conner): replace with lnwallet.WalletController interface to + // have stronger guarantees wrt. returned error types. + PublishTx func(*wire.MsgTx) error + + // ListenAddrs specifies which address to which clients may connect. + ListenAddrs []net.Addr + + // ReadTimeout specifies how long a client may go without sending a + // message. + ReadTimeout time.Duration + + // WriteTimeout specifies how long a client may go without reading a + // message from the other end, if the connection has stopped buffering + // the server's replies. + WriteTimeout time.Duration +} diff --git a/watchtower/errors.go b/watchtower/errors.go new file mode 100644 index 000000000..e8682d4f0 --- /dev/null +++ b/watchtower/errors.go @@ -0,0 +1,18 @@ +package watchtower + +import "errors" + +var ( + // ErrNoListeners signals that no listening ports were provided, + // rendering the tower unable to receive client requests. + ErrNoListeners = errors.New("no listening ports were specified") + + // ErrNonExperimentalConf signals that an attempt to apply a + // non-experimental Conf to a Config was detected. + ErrNonExperimentalConf = errors.New("cannot use watchtower in non-" + + "experimental builds") + + // ErrNoNetwork signals that no tor.Net is provided in the Config, which + // prevents resolution of listening addresses. + ErrNoNetwork = errors.New("no network specified, must be tor or clearnet") +) diff --git a/watchtower/interface.go b/watchtower/interface.go new file mode 100644 index 000000000..59b1b8486 --- /dev/null +++ b/watchtower/interface.go @@ -0,0 +1,14 @@ +package watchtower + +import ( + "github.com/lightningnetwork/lnd/watchtower/lookout" + "github.com/lightningnetwork/lnd/watchtower/wtserver" +) + +// DB abstracts the persistent functionality required to run the watchtower +// daemon. It composes the database interfaces required by the lookout and +// wtserver subsystems. +type DB interface { + lookout.DB + wtserver.DB +} diff --git a/watchtower/log.go b/watchtower/log.go new file mode 100644 index 000000000..f2fb5fc15 --- /dev/null +++ b/watchtower/log.go @@ -0,0 +1,49 @@ +package watchtower + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/watchtower/lookout" + "github.com/lightningnetwork/lnd/watchtower/wtserver" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger("WTWR", nil)) +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + UseLogger(btclog.Disabled) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger + lookout.UseLogger(logger) + wtserver.UseLogger(logger) +} + +// logClosure is used to provide a closure over expensive logging operations so +// don't have to be performed when the logging level doesn't warrant it. +type logClosure func() string + +// String invokes the underlying function and returns the result. +func (c logClosure) String() string { + return c() +} + +// newLogClosure returns a new closure over a function that returns a string +// which itself provides a Stringer interface so that it can be used with the +// logging system. +func newLogClosure(c func() string) logClosure { + return logClosure(c) +} diff --git a/watchtower/standalone.go b/watchtower/standalone.go new file mode 100644 index 000000000..ebb3646d2 --- /dev/null +++ b/watchtower/standalone.go @@ -0,0 +1,136 @@ +package watchtower + +import ( + "net" + "sync/atomic" + + "github.com/lightningnetwork/lnd/brontide" + "github.com/lightningnetwork/lnd/watchtower/lookout" + "github.com/lightningnetwork/lnd/watchtower/wtserver" +) + +// Standalone encapsulates the server-side functionality required by watchtower +// clients. A Standalone couples the two primary subsystems such that, as a +// unit, this instance can negotiate sessions with clients, accept state updates +// for active sessions, monitor the chain for breaches matching known breach +// hints, publish reconstructed justice transactions on behalf of tower clients. +type Standalone struct { + started uint32 // to be used atomically + stopped uint32 // to be used atomically + + cfg *Config + + // server is the client endpoint, used for negotiating sessions and + // uploading state updates. + server wtserver.Interface + + // lookout is a service that monitors the chain and inspects the + // transactions found in new blocks against the state updates received + // by the server. + lookout lookout.Service +} + +// New validates the passed Config and returns a fresh Standalone instance if +// the tower's subsystems could be properly initialized. +func New(cfg *Config) (*Standalone, error) { + // The tower must have listening address in order to accept new updates + // from clients. + if len(cfg.ListenAddrs) == 0 { + return nil, ErrNoListeners + } + + // Assign the default read timeout if none is provided. + if cfg.ReadTimeout == 0 { + cfg.ReadTimeout = DefaultReadTimeout + } + + // Assign the default write timeout if none is provided. + if cfg.WriteTimeout == 0 { + cfg.WriteTimeout = DefaultWriteTimeout + } + + punisher := lookout.NewBreachPunisher(&lookout.PunisherConfig{ + PublishTx: cfg.PublishTx, + }) + + // Initialize the lookout service with its required resources. + lookout := lookout.New(&lookout.Config{ + BlockFetcher: cfg.BlockFetcher, + DB: cfg.DB, + EpochRegistrar: cfg.EpochRegistrar, + Punisher: punisher, + }) + + // Create a brontide listener on each of the provided listening + // addresses. Client should be able to connect to any of open ports to + // communicate with this Standalone instance. + listeners := make([]net.Listener, 0, len(cfg.ListenAddrs)) + for _, listenAddr := range cfg.ListenAddrs { + listener, err := brontide.NewListener( + cfg.NodePrivKey, listenAddr.String(), + ) + if err != nil { + return nil, err + } + + listeners = append(listeners, listener) + } + + // Initialize the server with its required resources. + server, err := wtserver.New(&wtserver.Config{ + DB: cfg.DB, + NodePrivKey: cfg.NodePrivKey, + Listeners: listeners, + ReadTimeout: cfg.ReadTimeout, + WriteTimeout: cfg.WriteTimeout, + NewAddress: cfg.NewAddress, + }) + if err != nil { + return nil, err + } + + return &Standalone{ + cfg: cfg, + server: server, + lookout: lookout, + }, nil +} + +// Start idempotently starts the Standalone, an error is returned if the +// subsystems could not be initialized. +func (w *Standalone) Start() error { + if !atomic.CompareAndSwapUint32(&w.started, 0, 1) { + return nil + } + + log.Infof("Starting watchtower") + + if err := w.lookout.Start(); err != nil { + return err + } + if err := w.server.Start(); err != nil { + w.lookout.Stop() + return err + } + + log.Infof("Watchtower started successfully") + + return nil +} + +// Stop idempotently stops the Standalone and blocks until the subsystems have +// completed their shutdown. +func (w *Standalone) Stop() error { + if !atomic.CompareAndSwapUint32(&w.stopped, 0, 1) { + return nil + } + + log.Infof("Stopping watchtower") + + w.server.Stop() + w.lookout.Stop() + + log.Infof("Watchtower stopped successfully") + + return nil +}