mirror of
https://github.com/lightninglabs/faraday.git
synced 2026-08-16 13:01:10 +02:00
Rename createDefaultMacaroonFile to withMacaroonService since this is now a more appropriate name.
558 lines
17 KiB
Go
558 lines
17 KiB
Go
// Package frdrpcserver contains the server logic for faraday's grpc server
|
|
// which serves requests for close recommendations.
|
|
//
|
|
// The Faraday server interface is implemented by the RPCServer struct.
|
|
// To keep this file readable, each function implemented by the interface
|
|
// has a file named after the function call which contains rpc parsing
|
|
// code for the request and response. If the call requires extensive
|
|
// additional logic, and unexported function with the same name should
|
|
// be created in this file as well.
|
|
package frdrpcserver
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
proxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightningnetwork/lnd/macaroons"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
|
|
|
"github.com/lightninglabs/faraday/accounting"
|
|
"github.com/lightninglabs/faraday/chain"
|
|
"github.com/lightninglabs/faraday/fiat"
|
|
"github.com/lightninglabs/faraday/frdrpc"
|
|
"github.com/lightninglabs/faraday/frdrpcserver/perms"
|
|
"github.com/lightninglabs/faraday/recommend"
|
|
"github.com/lightninglabs/faraday/resolutions"
|
|
"github.com/lightninglabs/faraday/revenue"
|
|
)
|
|
|
|
var (
|
|
// customMarshalerOption is the configuratino we use for the JSON
|
|
// marshaler of the REST proxy. The default JSON marshaler 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: protojson.MarshalOptions{
|
|
UseProtoNames: true,
|
|
EmitUnpopulated: true,
|
|
},
|
|
},
|
|
)
|
|
|
|
// maxMsgRecvSize is the largest message our REST proxy will receive. We
|
|
// set this to 200MiB atm.
|
|
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
|
|
|
|
// maxInvoiceQueries is the maximum number of invoices we request from
|
|
// lnd at a time.
|
|
maxInvoiceQueries = 1000
|
|
|
|
// maxPaymentQueries is the maximum number of invoices we request from
|
|
// lnd at a time. It is less than the number of invoices we request
|
|
// because payments have a lot of htlc data.
|
|
maxPaymentQueries = 500
|
|
|
|
// maxForwardQueries is the maximum number of forwards we request from
|
|
// lnd at a time. It is more than the number of invoices we request
|
|
// because forwards have less data.
|
|
maxForwardQueries = 2000
|
|
|
|
// errServerAlreadyStarted is the error that is returned if the server
|
|
// is requested to start while it's already been started.
|
|
errServerAlreadyStarted = fmt.Errorf("server can only be started once")
|
|
|
|
// ErrBitcoinNodeRequired is required when an endpoint which requires
|
|
// a bitcoin node backend is hit and we are not connected to one.
|
|
ErrBitcoinNodeRequired = errors.New("bitcoin node required")
|
|
)
|
|
|
|
// RPCServer implements the faraday service, serving requests over grpc.
|
|
type RPCServer struct {
|
|
// To be used atomically.
|
|
started int32
|
|
|
|
// To be used atomically.
|
|
stopped int32
|
|
|
|
// Required by the grpc-gateway/v2 library for forward compatibility.
|
|
// Must be after the atomically used variables to not break struct
|
|
// alignment.
|
|
frdrpc.UnimplementedFaradayServerServer
|
|
|
|
// cfg contains closures and settings required for operation.
|
|
cfg *Config
|
|
|
|
// grpcServer is the main gRPC RPCServer that this RPC server will
|
|
// register itself with and accept client requests from.
|
|
grpcServer *grpc.Server
|
|
|
|
// rpcListener is the listener to use when starting the gRPC server.
|
|
rpcListener net.Listener
|
|
|
|
// restServer is the REST proxy server.
|
|
restServer *http.Server
|
|
|
|
macaroonService *lndclient.MacaroonService
|
|
|
|
restCancel func()
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// Config provides closures and settings required to run the rpc server.
|
|
type Config struct {
|
|
// Lnd is a client which can be used to query lnd.
|
|
Lnd lndclient.LndServices
|
|
|
|
// RPCListen is the address:port that the gRPC server should listen on.
|
|
RPCListen string
|
|
|
|
// RESTListen is the address:port that the REST server should listen on.
|
|
RESTListen string
|
|
|
|
// 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
|
|
|
|
// TLSServerConfig is the configuration to serve a secure connection
|
|
// over TLS.
|
|
TLSServerConfig *tls.Config
|
|
|
|
// RestClientConfig is the client configuration to connect to a TLS
|
|
// server started with the TLS config above. This is used for the REST
|
|
// proxy that connects internally to the gRPC server and therefore is a
|
|
// TLS client.
|
|
RestClientConfig *credentials.TransportCredentials
|
|
|
|
// FaradayDir is the main directory faraday uses. The macaroon database
|
|
// will be created there.
|
|
FaradayDir string
|
|
|
|
// MacaroonPath is the full path to the default faraday macaroon file
|
|
// that is created automatically. This path normally is within
|
|
// FaradayDir unless otherwise specified by the user.
|
|
MacaroonPath string
|
|
}
|
|
|
|
// NewRPCServer returns a server which will listen for rpc requests on the
|
|
// rpc listen address provided. Note that the server returned is not running,
|
|
// and should be started using Start().
|
|
func NewRPCServer(cfg *Config) *RPCServer {
|
|
return &RPCServer{
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
// Start starts the listener and server.
|
|
func (s *RPCServer) Start() error {
|
|
if atomic.AddInt32(&s.started, 1) != 1 {
|
|
return errServerAlreadyStarted
|
|
}
|
|
|
|
// Depending on how far we got in initializing the server, we might need
|
|
// to clean up certain services that were already started. Keep track of
|
|
// them with this map of service name to shutdown function.
|
|
shutdownFuncs := make(map[string]func() error)
|
|
defer func() {
|
|
for serviceName, shutdownFn := range shutdownFuncs {
|
|
if err := shutdownFn(); err != nil {
|
|
log.Errorf("Error shutting down %s service: %v",
|
|
serviceName, err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Set up the macaroon service.
|
|
var err error
|
|
s.macaroonService, err = lndclient.NewMacaroonService(
|
|
&lndclient.MacaroonServiceConfig{
|
|
DBPath: s.cfg.FaradayDir,
|
|
DBFileName: "macaroons.db",
|
|
DBTimeout: macDatabaseOpenTimeout,
|
|
MacaroonLocation: faradayMacaroonLocation,
|
|
MacaroonPath: s.cfg.MacaroonPath,
|
|
Checkers: []macaroons.Checker{
|
|
macaroons.IPLockChecker,
|
|
},
|
|
RequiredPerms: perms.RequiredPermissions,
|
|
DBPassword: macDbDefaultPw,
|
|
LndClient: &s.cfg.Lnd,
|
|
EphemeralKey: lndclient.SharedKeyNUMS,
|
|
KeyLocator: lndclient.SharedKeyLocator,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("error creating macroon service: %v", err)
|
|
}
|
|
|
|
// Start the macaroon service and let it create its default macaroon in
|
|
// case it doesn't exist yet.
|
|
if err := s.macaroonService.Start(); err != nil {
|
|
return fmt.Errorf("error starting macaroon service: %v", err)
|
|
}
|
|
shutdownFuncs["macaroon"] = s.macaroonService.Stop
|
|
|
|
// First we add the security interceptor to our gRPC server options that
|
|
// checks the macaroons for validity.
|
|
unaryInterceptor, streamInterceptor, err := s.macaroonService.Interceptors()
|
|
if err != nil {
|
|
return fmt.Errorf("error with macaroon interceptor: %v", err)
|
|
}
|
|
|
|
// Add our TLS configuration and then create our server instance. It's
|
|
// important that we let gRPC create the TLS listener and we don't just
|
|
// use tls.NewListener(). Otherwise we run into the ALPN error with non-
|
|
// golang clients.
|
|
tlsCredentials := credentials.NewTLS(s.cfg.TLSServerConfig)
|
|
s.grpcServer = grpc.NewServer(
|
|
grpc.UnaryInterceptor(unaryInterceptor),
|
|
grpc.StreamInterceptor(streamInterceptor),
|
|
grpc.Creds(tlsCredentials),
|
|
)
|
|
|
|
// Start the gRPC RPCServer listening for HTTP/2 connections.
|
|
log.Info("Starting gRPC listener")
|
|
s.rpcListener, err = net.Listen("tcp", s.cfg.RPCListen)
|
|
if err != nil {
|
|
return fmt.Errorf("RPC RPCServer unable to listen on %v",
|
|
s.cfg.RPCListen)
|
|
|
|
}
|
|
shutdownFuncs["gRPC listener"] = s.rpcListener.Close
|
|
log.Infof("gRPC server listening on %s", s.rpcListener.Addr())
|
|
|
|
frdrpc.RegisterFaradayServerServer(s.grpcServer, s)
|
|
|
|
// We'll also create and start an accompanying proxy to serve clients
|
|
// through REST. An empty address indicates REST is disabled.
|
|
if s.cfg.RESTListen != "" {
|
|
log.Infof("Starting REST proxy listener ")
|
|
restListener, err := net.Listen("tcp", s.cfg.RESTListen)
|
|
if err != nil {
|
|
return fmt.Errorf("REST server unable to listen on "+
|
|
"%v: %v", s.cfg.RESTListen, err)
|
|
|
|
}
|
|
restListener = tls.NewListener(
|
|
restListener, s.cfg.TLSServerConfig,
|
|
)
|
|
shutdownFuncs["REST listener"] = restListener.Close
|
|
log.Infof("REST server listening on %s", restListener.Addr())
|
|
|
|
// We'll dial into the local gRPC server so we need to set some
|
|
// gRPC dial options and CORS settings.
|
|
var restCtx context.Context
|
|
restCtx, s.restCancel = context.WithCancel(context.Background())
|
|
mux := proxy.NewServeMux(customMarshalerOption)
|
|
var restHandler http.Handler = mux
|
|
if s.cfg.CORSOrigin != "" {
|
|
restHandler = allowCORS(restHandler, s.cfg.CORSOrigin)
|
|
}
|
|
proxyOpts := []grpc.DialOption{
|
|
grpc.WithTransportCredentials(*s.cfg.RestClientConfig),
|
|
grpc.WithDefaultCallOptions(maxMsgRecvSize),
|
|
}
|
|
|
|
// With TLS enabled by default, we cannot call 0.0.0.0
|
|
// internally from the REST proxy as that IP address isn't in
|
|
// the cert. We need to rewrite it to the loopback address.
|
|
restProxyDest := s.cfg.RPCListen
|
|
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,
|
|
)
|
|
}
|
|
err = frdrpc.RegisterFaradayServerHandlerFromEndpoint(
|
|
restCtx, mux, restProxyDest, proxyOpts,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.restServer = &http.Server{Handler: restHandler}
|
|
|
|
s.wg.Add(1)
|
|
go func() {
|
|
defer s.wg.Done()
|
|
err := s.restServer.Serve(restListener)
|
|
// ErrServerClosed is always returned when the proxy is
|
|
// shut down, so don't log it.
|
|
if err != nil && err != http.ErrServerClosed {
|
|
log.Error(err)
|
|
}
|
|
}()
|
|
} else {
|
|
log.Infof("REST proxy disabled")
|
|
}
|
|
|
|
s.wg.Add(1)
|
|
go func() {
|
|
defer s.wg.Done()
|
|
if err := s.grpcServer.Serve(s.rpcListener); err != nil {
|
|
log.Errorf("could not serve grpc server: %v", err)
|
|
}
|
|
}()
|
|
|
|
// If we got here successfully, there's no need to shutdown anything
|
|
// anymore.
|
|
shutdownFuncs = nil
|
|
|
|
return nil
|
|
}
|
|
|
|
// StartAsSubserver is an alternative to Start where the RPC server does not
|
|
// create its own gRPC server but registers to an existing one. The same goes
|
|
// for REST (if enabled), instead of creating an own mux and HTTP server, we
|
|
// register to an existing one.
|
|
func (s *RPCServer) StartAsSubserver(lndClient lndclient.LndServices,
|
|
withMacaroonService bool) error {
|
|
|
|
if atomic.AddInt32(&s.started, 1) != 1 {
|
|
return errServerAlreadyStarted
|
|
}
|
|
|
|
if withMacaroonService {
|
|
// Start the macaroon service and let it create its default
|
|
// macaroon in case it doesn't exist yet.
|
|
if err := s.macaroonService.Start(); err != nil {
|
|
return fmt.Errorf("error starting macaroon service: %v", err)
|
|
}
|
|
}
|
|
|
|
s.cfg.Lnd = lndClient
|
|
return nil
|
|
}
|
|
|
|
// ValidateMacaroon extracts the macaroon from the context's gRPC metadata,
|
|
// checks its signature, makes sure all specified permissions for the called
|
|
// method are contained within and finally ensures all caveat conditions are
|
|
// met. A non-nil error is returned if any of the checks fail. This method is
|
|
// needed to enable faraday running as an external subserver in the same process
|
|
// as lnd but still validate its own macaroons.
|
|
func (s *RPCServer) ValidateMacaroon(ctx context.Context,
|
|
requiredPermissions []bakery.Op, fullMethod string) error {
|
|
|
|
if s.macaroonService == nil {
|
|
return fmt.Errorf("macaroon service not yet initialised")
|
|
}
|
|
|
|
// Delegate the call to faraday's own macaroon validator service.
|
|
return s.macaroonService.ValidateMacaroon(
|
|
ctx, requiredPermissions, fullMethod,
|
|
)
|
|
}
|
|
|
|
// Stop stops the grpc listener and server.
|
|
func (s *RPCServer) Stop() error {
|
|
if atomic.AddInt32(&s.stopped, 1) != 1 {
|
|
return nil
|
|
}
|
|
|
|
if s.restServer != nil {
|
|
s.restCancel()
|
|
err := s.restServer.Close()
|
|
if err != nil {
|
|
log.Errorf("unable to close REST listener: %v", err)
|
|
}
|
|
}
|
|
|
|
if s.macaroonService != nil {
|
|
if err := s.macaroonService.Stop(); err != nil {
|
|
log.Errorf("Error stopping macaroon service: %v", err)
|
|
}
|
|
}
|
|
|
|
// Stop the grpc server and wait for all go routines to terminate.
|
|
if s.grpcServer != nil {
|
|
s.grpcServer.Stop()
|
|
}
|
|
s.wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
// OutlierRecommendations provides a set of close recommendations for the
|
|
// current set of open channels based on whether they are outliers.
|
|
func (s *RPCServer) OutlierRecommendations(ctx context.Context,
|
|
req *frdrpc.OutlierRecommendationsRequest) (*frdrpc.CloseRecommendationsResponse,
|
|
error) {
|
|
|
|
if req.RecRequest == nil {
|
|
return nil, errors.New("recommendation request field required")
|
|
}
|
|
|
|
log.Debugf("[OutlierRecommendations]: metric: %v, multiplier: %v",
|
|
req.RecRequest.Metric, req.OutlierMultiplier)
|
|
|
|
cfg, multiplier := parseOutlierRequest(ctx, s.cfg, req)
|
|
|
|
report, err := recommend.OutlierRecommendations(cfg, multiplier)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return rpcResponse(report), nil
|
|
}
|
|
|
|
// ThresholdRecommendations provides a set of close recommendations for the
|
|
// current set of open channels based on whether they are above or below a
|
|
// given threshold.
|
|
func (s *RPCServer) ThresholdRecommendations(ctx context.Context,
|
|
req *frdrpc.ThresholdRecommendationsRequest) (*frdrpc.CloseRecommendationsResponse,
|
|
error) {
|
|
|
|
if req.RecRequest == nil {
|
|
return nil, errors.New("recommendation request field required")
|
|
}
|
|
|
|
log.Debugf("[ThresholdRecommendations]: metric: %v, threshold: %v",
|
|
req.RecRequest.Metric, req.ThresholdValue)
|
|
|
|
cfg, threshold := parseThresholdRequest(ctx, s.cfg, req)
|
|
|
|
report, err := recommend.ThresholdRecommendations(cfg, threshold)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return rpcResponse(report), nil
|
|
}
|
|
|
|
// RevenueReport returns a pairwise revenue report for a channel
|
|
// over the period requested.
|
|
func (s *RPCServer) RevenueReport(ctx context.Context,
|
|
req *frdrpc.RevenueReportRequest) (*frdrpc.RevenueReportResponse, error) {
|
|
|
|
log.Debugf("[RevenueReport]: range: %v-%v, channels: %v", req.StartTime,
|
|
req.EndTime, req.ChanPoints)
|
|
|
|
revenueConfig := parseRevenueRequest(ctx, s.cfg, req)
|
|
|
|
report, err := revenue.GetRevenueReport(revenueConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return rpcRevenueResponse(req.GetChanPoints(), report)
|
|
}
|
|
|
|
// ChannelInsights returns the channel insights for our currently open set
|
|
// of channels.
|
|
func (s *RPCServer) ChannelInsights(ctx context.Context,
|
|
_ *frdrpc.ChannelInsightsRequest) (*frdrpc.ChannelInsightsResponse, error) {
|
|
|
|
log.Debugf("[ChannelInsights]")
|
|
|
|
insights, err := channelInsights(ctx, s.cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return rpcChannelInsightsResponse(insights), nil
|
|
}
|
|
|
|
// ExchangeRate provides a fiat estimate for a set of timestamped bitcoin
|
|
// prices.
|
|
func (s *RPCServer) ExchangeRate(ctx context.Context,
|
|
req *frdrpc.ExchangeRateRequest) (*frdrpc.ExchangeRateResponse, error) {
|
|
|
|
log.Debugf("[FiatEstimate]: %v requests", len(req.Timestamps))
|
|
|
|
timestamps, priceCfg, err := parseExchangeRateRequest(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
prices, err := fiat.GetPrices(ctx, timestamps, priceCfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return exchangeRateResponse(prices), nil
|
|
}
|
|
|
|
// NodeAudit returns an on chain report for the period requested.
|
|
func (s *RPCServer) NodeAudit(ctx context.Context,
|
|
req *frdrpc.NodeAuditRequest) (*frdrpc.NodeAuditResponse, error) {
|
|
|
|
log.Debugf("[NodeAudit]: range: %v-%v, fiat: %v", req.StartTime,
|
|
req.EndTime, req.DisableFiat)
|
|
|
|
onChain, offChain, err := parseNodeAuditRequest(ctx, s.cfg, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
onChainReport, err := accounting.OnChainReport(ctx, onChain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
offChainReport, err := accounting.OffChainReport(ctx, offChain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return rpcReportResponse(append(onChainReport, offChainReport...))
|
|
}
|
|
|
|
// CloseReport returns a close report for the channel provided. Note that this
|
|
// endpoint requires connection to an external bitcoind node.
|
|
func (s *RPCServer) CloseReport(ctx context.Context,
|
|
req *frdrpc.CloseReportRequest) (*frdrpc.CloseReportResponse, error) {
|
|
|
|
log.Debugf("[CloseReport]: %v", req.ChannelPoint)
|
|
|
|
if err := s.requireNode(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg := parseCloseReportRequest(ctx, s.cfg)
|
|
|
|
report, err := resolutions.ChannelCloseReport(cfg, req.ChannelPoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return rpcCloseReportResponse(report), nil
|
|
}
|
|
|
|
// requireNode fails if we do not have a connection to a backing bitcoin node.
|
|
func (s *RPCServer) requireNode() error {
|
|
if s.cfg.BitcoinClient == nil {
|
|
return ErrBitcoinNodeRequired
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// allowCORS wraps the given http.Handler with a function that adds the
|
|
// Access-Control-Allow-Origin header to the response.
|
|
func allowCORS(handler http.Handler, origin string) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
handler.ServeHTTP(w, r)
|
|
})
|
|
}
|