diff --git a/chain/client.go b/chain/client.go new file mode 100644 index 0000000..ab645bd --- /dev/null +++ b/chain/client.go @@ -0,0 +1,135 @@ +package chain + +import ( + "io/ioutil" + "os" + "sync" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/rpcclient" +) + +// BitcoinClient is an interface which represents a connection to a bitcoin +// client. +type BitcoinClient interface { + // GetTxDetail looks up a transaction. + GetTxDetail(txHash *chainhash.Hash) (*btcjson.TxRawResult, error) +} + +// BitcoinConfig defines exported config options for the connection to the +// btcd/bitcoind backend. +type BitcoinConfig struct { + Host string `long:"host" description:"host:port of the bitcoind/btcd instance address"` + User string `long:"user" description:"bitcoind/btcd user name"` + Password string `long:"password" description:"bitcoind/btcd password"` + HTTPPostMode bool `long:"httppostmode" description:"Use HTTP POST mode? bitcoind only supports this mode"` + UseTLS bool `long:"usetls" description:"Use TLS to connect? bitcoind only supports non-TLS connections"` + TLSPath string `long:"tlspath" description:"Path to btcd tls certificate, bitcoind only supports non-TLS connections"` +} + +// DefaultConfig is the default config that we use to +var DefaultConfig = &BitcoinConfig{ + Host: "localhost:8332", + UseTLS: false, + HTTPPostMode: true, +} + +// bitcoinClient is a wrapper around the RPC connection to the chain backend +// and allows transactions to be queried. +type bitcoinClient struct { + sync.Mutex + + rpcClient *rpcclient.Client + + // txDetailCache holds a cache of transactions we have previously looked + // up. + txDetailCache map[string]*btcjson.TxRawResult +} + +// GetTxDetail fetches a single transaction from the chain and returns it +// in a format that contains more details, like the block hash it was included +// in for example. +func (c *bitcoinClient) GetTxDetail(txHash *chainhash.Hash) ( + *btcjson.TxRawResult, error) { + + c.Lock() + cachedTx, ok := c.txDetailCache[txHash.String()] + c.Unlock() + if ok { + return cachedTx, nil + } + + tx, err := c.rpcClient.GetRawTransactionVerbose(txHash) + if err != nil { + return nil, err + } + + // Do not cache the transaction if it has not confirmed yet. If we do, + // we won't ever lookup the confirmed transaction because it is already + // cached. + if tx.BlockHash == "" { + return tx, nil + } + + c.Lock() + c.txDetailCache[txHash.String()] = tx + c.Unlock() + + return tx, nil +} + +// NewBitcoinClient attempts to connect to a bitcoin rpcclient with the config +// provided and returns a BitcoinClient wrapper which can be used to access the +// chain connection. +func NewBitcoinClient(cfg *BitcoinConfig) (BitcoinClient, error) { + client, err := getBitcoinConn(cfg) + if err != nil { + return nil, err + } + + return &bitcoinClient{ + rpcClient: client, + txDetailCache: make(map[string]*btcjson.TxRawResult), + }, nil +} + +// getBitcoinConn gets a bitcoin rpc client from the config details provided. +func getBitcoinConn(cfg *BitcoinConfig) (*rpcclient.Client, error) { + // In case we use TLS and a certificate argument is provided, we need to + // read that file and provide it to the RPC connection as byte slice. + var rpcCert []byte + if cfg.UseTLS && cfg.TLSPath != "" { + certFile, err := os.Open(cfg.TLSPath) + if err != nil { + return nil, err + } + rpcCert, err = ioutil.ReadAll(certFile) + if err != nil { + return nil, err + } + if err := certFile.Close(); err != nil { + return nil, err + } + } + + // Connect to bitcoin core RPC server using HTTP POST mode. + connCfg := &rpcclient.ConnConfig{ + Host: cfg.Host, + User: cfg.User, + Pass: cfg.Password, + HTTPPostMode: cfg.HTTPPostMode, + DisableTLS: !cfg.UseTLS, + Certificates: rpcCert, + } + + // Notice the notification parameter is nil since notifications are + // not supported in HTTP POST mode. + return rpcclient.New(connCfg, nil) +} + +// Stop closes the connection to the chain backend and should always be +// called on cleanup. +func (c *bitcoinClient) Stop() { + c.rpcClient.Shutdown() +} diff --git a/config.go b/config.go index fa898f1..abe3b71 100644 --- a/config.go +++ b/config.go @@ -5,6 +5,7 @@ import ( "time" "github.com/jessevdk/go-flags" + "github.com/lightninglabs/faraday/chain" "github.com/lightningnetwork/lnd/build" ) @@ -15,6 +16,10 @@ const ( defaultMinimumMonitor = time.Hour * 24 * 7 * 4 // four weeks in hours defaultDebugLevel = "info" defaultRPCListen = "localhost:8465" + + // By default we do not require connecting to a bitcoin node so that + // we can serve basic functionality by default. + defaultChainConn = false ) type Config struct { @@ -36,6 +41,9 @@ type Config struct { // Simnet is set to true when using bitcoind's regtest. Regtest bool `long:"regtest" description:"Use regtest"` + // ChainConn specifies whether to attempt connecting to a bitcoin backend. + ChainConn bool `long:"connect_bitcoin" description:"Whether to attempt to connect to a backing bitcoin node. Some endpoints will not be available if this option is not enabled."` + // MinimumMonitored is the minimum amount of time that a channel must be monitored for before we consider it for termination. MinimumMonitored time.Duration `long:"min_monitored" description:"The minimum amount of time that a channel must be monitored for before recommending termination. Valid time units are {s, m, h}."` @@ -54,6 +62,9 @@ type Config struct { // CORSOrigin specifies the CORS header that should be set on REST responses. No header is added if the value is empty. CORSOrigin string `long:"corsorigin" description:"The value to send in the Access-Control-Allow-Origin header. Header will be omitted if empty."` + + // Bitcoin is the configuration required to connect to a bitcoin node. + Bitcoin *chain.BitcoinConfig `group:"bitcoin" namespace:"bitcoin"` } // DefaultConfig returns all default values for the Config struct. @@ -64,6 +75,8 @@ func DefaultConfig() Config { MinimumMonitored: defaultMinimumMonitor, DebugLevel: defaultDebugLevel, RPCListen: defaultRPCListen, + ChainConn: defaultChainConn, + Bitcoin: chain.DefaultConfig, } } @@ -98,6 +111,21 @@ func LoadConfig() (*Config, error) { return nil, fmt.Errorf("do not specify more than one network flag") } + // If the user has opted into connecting to a bitcoin backend, check + // that we have a rpc user and password, and that tls path is set if + // required. + if config.ChainConn { + if config.Bitcoin.User == "" || config.Bitcoin.Password == "" { + return nil, fmt.Errorf("rpc user and password " + + "required when chainconn is set") + } + + if config.Bitcoin.UseTLS && config.Bitcoin.TLSPath == "" { + return nil, fmt.Errorf("bitcoin.tlspath required " + + "when chainconn is set") + } + } + if err := build.ParseAndSetDebugLevels(config.DebugLevel, logWriter); err != nil { return nil, err } diff --git a/faraday.go b/faraday.go index f5db3d5..d4d68bb 100644 --- a/faraday.go +++ b/faraday.go @@ -4,6 +4,7 @@ package faraday import ( "fmt" + "github.com/lightninglabs/faraday/chain" "github.com/lightninglabs/faraday/frdrpc" "github.com/lightninglabs/lndclient" "github.com/lightningnetwork/lnd/signal" @@ -35,14 +36,22 @@ func Main() error { defer client.Close() // Instantiate the faraday gRPC server. - server := frdrpc.NewRPCServer( - &frdrpc.Config{ - Lnd: client.LndServices, - RPCListen: config.RPCListen, - RESTListen: config.RESTListen, - CORSOrigin: config.CORSOrigin, - }, - ) + cfg := &frdrpc.Config{ + Lnd: client.LndServices, + RPCListen: config.RPCListen, + RESTListen: config.RESTListen, + CORSOrigin: config.CORSOrigin, + } + + // If the client chose to connect to a bitcoin client, get one now. + if config.ChainConn { + cfg.BitcoinClient, err = chain.NewBitcoinClient(config.Bitcoin) + if err != nil { + return err + } + } + + server := frdrpc.NewRPCServer(cfg) // Catch intercept signals, then start the server. signal.Intercept() diff --git a/frdrpc/rpcserver.go b/frdrpc/rpcserver.go index a6f7485..fe58dc5 100644 --- a/frdrpc/rpcserver.go +++ b/frdrpc/rpcserver.go @@ -19,6 +19,7 @@ import ( proxy "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/lightninglabs/faraday/accounting" + "github.com/lightninglabs/faraday/chain" "github.com/lightninglabs/faraday/fiat" "github.com/lightninglabs/faraday/recommend" "github.com/lightninglabs/faraday/revenue" @@ -102,6 +103,10 @@ type Config struct { // CORSOrigin specifies the CORS header that should be set on REST // responses. No header is added if the value is empty. CORSOrigin string + + // BitcoinClient is set if the client opted to connect to a bitcoin + // backend, if not, it will be nil. + BitcoinClient chain.BitcoinClient } // NewRPCServer returns a server which will listen for rpc requests on the