Merge pull request #7 from lightninglabs/rpc-addserver

trmrpc: Add rpcserver and cli tool
This commit is contained in:
Carla Kirk-Cohen 2020-01-31 13:30:05 +02:00 committed by GitHub
commit 76cb7cdade
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1322 additions and 155 deletions

View file

@ -58,6 +58,7 @@ $(GOACC_BIN):
build:
@$(call print, "Building terminator.")
$(GOBUILD) $(PKG)/cmd/terminator
$(GOBUILD) $(PKG)/cmd/trmcli
install:
@$(call print, "Installing terminator.")
@ -87,6 +88,10 @@ goveralls: $(GOVERALLS_BIN)
@$(call print, "Sending coverage report.")
$(GOVERALLS_BIN) -coverprofile=coverage.txt -service=travis-ci
rpc:
@$(call print, "Compiling protos.")
cd ./trmrpc; ./gen_protos.sh
travis-race: lint unit-race
travis-cover: lint unit-cover goveralls
@ -121,4 +126,5 @@ list:
clean:
@$(call print, "Cleaning source.$(NC)")
$(RM) ./terminator
$(RM) ./trmcli
$(RM) coverage.txt

View file

@ -0,0 +1,79 @@
package main
import (
"context"
"time"
"github.com/lightninglabs/terminator/trmrpc"
"github.com/urfave/cli"
)
var (
defaultMinMonitored = time.Hour * 24 * 7 * 4 // four weeks in hours
defaultOutlierMultiplier = 3
)
var closeRecommendationCommand = cli.Command{
Name: "closerecs",
Category: "channel",
Usage: "Get close recommendations for currently open channels.",
Flags: []cli.Flag{
cli.Int64Flag{
Name: "min_monitored",
Usage: "amount of time in seconds a channel should " +
"be monitored for to be eligible for close",
Value: int64(defaultMinMonitored.Seconds()),
},
cli.StringFlag{
Name: "outlier_mult",
Usage: "(optional with outlier strategy) Number of " +
"inter quartile ranges a channel should be " +
"from quartiles to be considered an outlier. " +
"Recommended values are 1.5 for aggressive " +
"recommendations and 3 for conservative ones.",
},
cli.StringFlag{
Name: "uptime_threshold",
Usage: "(optional) Uptime percentage threshold " +
"underneath which a channel will be recommended " +
"for close.",
},
},
Action: queryCloseRecommendations,
}
func queryCloseRecommendations(ctx *cli.Context) error {
client, cleanup := getClient(ctx)
defer cleanup()
// Set monitored value from cli and default outlier multiplier. The
// outlier multiplier will be overwritten if the user provided it.
req := &trmrpc.CloseRecommendationsRequest{
MinimumMonitored: ctx.Int64("min_monitored"),
OutlierMultiplier: float32(defaultOutlierMultiplier),
}
// If an a custom outlier multiple was set, use it.
if ctx.IsSet("outlier_mult") {
req.OutlierMultiplier = float32(ctx.Float64("outlier_mult"))
}
// If an uptime threshold was set, use it.
if ctx.IsSet("uptime_threshold") {
uptimeThreshold := float32(ctx.Float64("uptime_threshold"))
req.Threshold =
&trmrpc.CloseRecommendationsRequest_UptimeThreshold{
UptimeThreshold: uptimeThreshold,
}
}
rpcCtx := context.Background()
recs, err := client.CloseRecommendations(rpcCtx, req)
if err != nil {
return err
}
printRespJSON(recs)
return nil
}

32
cmd/trmcli/main.go Normal file
View file

@ -0,0 +1,32 @@
package main
import (
"os"
"github.com/urfave/cli"
)
var (
defaultRPCPort = "8419"
defaultRPCHostPort = "localhost:" + defaultRPCPort
)
func main() {
app := cli.NewApp()
app.Name = "trmcli"
app.Usage = "command line tool for terminator"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "rpcserver",
Value: defaultRPCHostPort,
Usage: "host:port of terminator",
},
}
app.Commands = []cli.Command{
closeRecommendationCommand,
}
if err := app.Run(os.Args); err != nil {
fatal(err)
}
}

97
cmd/trmcli/utils.go Normal file
View file

@ -0,0 +1,97 @@
package main
import (
"context"
"fmt"
"net"
"os"
"github.com/lightninglabs/protobuf-hex-display/jsonpb"
"github.com/lightninglabs/protobuf-hex-display/proto"
"github.com/lightninglabs/terminator/trmrpc"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/urfave/cli"
"google.golang.org/grpc"
)
var (
// maxMsgRecvSize is the largest message our client will receive. We
// set this to 200MiB atm.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
)
// fatal logs and error and exits.
func fatal(err error) {
_, _ = fmt.Fprintf(os.Stderr, "[trmcli] %v\n", err)
os.Exit(1)
}
// printRespJSON prints a proto message as json.
func printRespJSON(resp proto.Message) {
jsonMarshaler := &jsonpb.Marshaler{
EmitDefaults: true,
Indent: " ",
}
jsonStr, err := jsonMarshaler.MarshalToString(resp)
if err != nil {
fmt.Println("unable to decode response: ", err)
return
}
fmt.Println(jsonStr)
}
// getClient returns a terminator client.
func getClient(ctx *cli.Context) (trmrpc.TerminatorServerClient, func()) {
conn := getClientConn(ctx)
cleanUp := func() {
if err := conn.Close(); err != nil {
fatal(err)
}
}
return trmrpc.NewTerminatorServerClient(conn), cleanUp
}
// getClientConn gets a client connection to the address provided by the
// rpcserver flag.
func getClientConn(ctx *cli.Context) *grpc.ClientConn {
// We need to use a custom dialer so we can also connect to unix sockets
// and not just TCP addresses.
genericDialer := clientAddressDialer(defaultRPCPort)
opts := []grpc.DialOption{
grpc.WithContextDialer(genericDialer),
grpc.WithDefaultCallOptions(maxMsgRecvSize),
// TODO(carla): add tls and remove this option.
grpc.WithInsecure(),
}
conn, err := grpc.Dial(ctx.GlobalString("rpcserver"), opts...)
if err != nil {
fatal(fmt.Errorf("unable to connect to RPC server: %v", err))
}
return conn
}
// ClientAddressDialer parsed client address and returns a dialer.
func clientAddressDialer(defaultPort string) func(context.Context,
string) (net.Conn, error) {
return func(ctx context.Context, addr string) (net.Conn, error) {
parsedAddr, err := lncfg.ParseAddressString(
addr, defaultPort, net.ResolveTCPAddr,
)
if err != nil {
return nil, err
}
d := net.Dialer{}
return d.DialContext(
ctx, parsedAddr.Network(), parsedAddr.String(),
)
}
}

View file

@ -15,6 +15,7 @@ const (
defaultNetwork = "mainnet"
defaultMinimumMonitor = time.Hour * 24 * 7 * 4 // four weeks in hours
defaultDebugLevel = "info"
defaultRPCListen = "localhost:8419"
)
type config struct {
@ -48,6 +49,9 @@ type config struct {
// DebugLevel is a string defining the log level for the service either
// for all subsystems the same or individual level by subsystem.
DebugLevel string `long:"debuglevel" description:"Debug level for termaintor and its subsystems."`
// RPCListen is the listen address for the terminator rpc server.
RPCListen string `long:"rpclisten" description:"Address to listen on for gRPC clients"`
}
// loadConfig starts with a skeleton default config, and reads in user provided
@ -62,6 +66,7 @@ func loadConfig() (*config, error) {
MacaroonFile: defaultMacaroon,
MinimumMonitored: defaultMinimumMonitor,
DebugLevel: defaultDebugLevel,
RPCListen: defaultRPCListen,
}
// Parse command line options to obtain user specified values.

View file

@ -7,30 +7,15 @@ import (
"sort"
)
const (
// weakOutlierMultiplier is the multiplier that we apply to the
// inter-quartile range to calculate weak outliers. Using this value is
// less cautious than using the strong outlier multiplier because it
// will flag values that are closer to the lower/upper quartiles as
// outliers.
weakOutlierMultiplier = 1.5
// strongOutlierMultiplier is the multiplier that we apply to the
// inter-quartile range to calculate strong outliers. Using this value is
// more cautious than using the weak outlier multiplier because it will only
// flag extreme outliers.
strongOutlierMultiplier = 3
)
var (
// errNoValues is returned when an attempt is made to calculate the median of
// a zero length array.
errNoValues = errors.New("can't calculate median for zero length " +
"array")
// ErrTooFewValues is returned when there are too few values provided to
// errTooFewValues is returned when there are too few values provided to
// calculate quartiles.
ErrTooFewValues = errors.New("can't calculate quartiles for fewer than 3 " +
errTooFewValues = errors.New("can't calculate quartiles for fewer than 3 " +
"elements")
)
@ -80,7 +65,7 @@ func (d Dataset) rawValues() []float64 {
func (d Dataset) quartiles() (float64, float64, error) {
valueCount := len(d)
if valueCount < 3 {
return 0, 0, ErrTooFewValues
return 0, 0, errTooFewValues
}
// Get the cutoff points for calculating the lower and upper quartiles.
@ -113,6 +98,11 @@ func (d Dataset) quartiles() (float64, float64, error) {
return lowerQuartile, upperQuartile, nil
}
// Value returns the value that a label is associated with in a set.
func (d Dataset) Value(label string) float64 {
return d[label]
}
// OutlierResult returns the results of an outlier check.
type OutlierResult struct {
// UpperOutlier is true if the value is an upper outlier in the dataset.
@ -126,21 +116,16 @@ type OutlierResult struct {
// upper or lower outlier (or not an outlier) for the dataset.
// If a value is an upper or lower outlier, the result is recorded in the
// corresponding bool in an outlier result.
func (d Dataset) isIQROutlier(value float64, lowerQuartile,
upperQuartile float64, strong bool) *OutlierResult {
func (d Dataset) isIQROutlier(value, lowerQuartile, upperQuartile,
multiplier float64) *OutlierResult {
interquartileRange := upperQuartile - lowerQuartile
// quartileDistance is the distance from the upper/lower quartile a value
// must be to be considered an outlier. A larger quartile distance more
// strictly classifies outliers, because they are required to be further
// from the upper/lower quartile. Based on whether we want to find strong or
// weak outliers, the inter-quartile range (which is the base unit for this
// distance) is multiplier by a strong or weak multiplier.
quartileDistance := interquartileRange * weakOutlierMultiplier
if strong {
quartileDistance = interquartileRange * strongOutlierMultiplier
}
// from the upper/lower quartile.
quartileDistance := interquartileRange * multiplier
return &OutlierResult{
// A value is considered to be a upper outlier if it lies above the
@ -156,44 +141,93 @@ func (d Dataset) isIQROutlier(value float64, lowerQuartile,
// GetOutliers returns a map of the labels in the dataset to outlier results
// which indicate whether the associated value is an upper or lower inter-
// quartile outlier.
// quartile outlier. If there are too few values to calculate inter-quartile
// outliers, it will return false values for all data points.
//
// Strong is set to adjust whether we check for a strong or weak outlier. Strong
// outliers are 3 inter-quartile ranges below/above the lower/upper quartile and
// weak outliers are 1.5 inter-quartile ranges below/above the lower/upper
// quartile.
// An outlier multiplier is provided to determine how strictly we classify
// outliers; lower values will identify more outliers, thus being more strict,
// and higher values will identify fewer outliers, thus being less strict.
// Multipliers less than 1.5 are considered to provide "weak outliers", because
// the values are still relatively close the the rest of the dataset.
// Multipliers more than 3 are considered to provide "strong outliers" because
// they identify values that are far from the rest of the dataset.
//
// The effect of this value is illustrated in the example below:
// Given some random set of data, with lower quartile = 5 and upper quartile
// = 6, the inter-quartile range is 1.
//
// LQ UQ
// [ 1 2 5 5 5 6 6 6 8 11 ]
//
// For strong outliers, we multiply the inter-quartile range by 3 then check
// whether a value is below the lower quartile or above the upper quartile by
// that amount to determine whether it is a lower or upper outlier.
// Strong lower outlier bound: 5 - (1 * 3) = 2
// For larger values, eg multiplier=3, we will detect fewer outliers:
// Lower outlier bound: 5 - (1 * 3) = 2
// -> 1 is a strong lower outlier
// Strong upper outlier bound: 6 + (1 * 3) = 9
// Upper outlier bound: 6 + (1 * 3) = 9
// -> 11 is a strong upper outlier
//
// For weak outliers, we perform the same check, but we multiply the inter-
// quartile range by 1.5 rather than 3.
// For smaller values, eg multiplier=1.5, we detect more outliers:
// Weak lower outlier bound: 5 - (1 * 1.5) = 3.5
// -> 1 and 2 are weak lower outliers
// Weak upper outlier bound: 6 + (1 *1.5) = 7.5
// -> 8 and 11 are weak upper outliers
func (d Dataset) GetOutliers(strong bool) (map[string]*OutlierResult, error) {
func (d Dataset) GetOutliers(outlierMultiplier float64) (
map[string]*OutlierResult, error) {
outliers := make(map[string]*OutlierResult, len(d))
lower, upper, err := d.quartiles()
// If we could not calculate quartiles because there are too few values,
// we cannot calculate outliers so we return a map with all false
// outlier results.
if err == errTooFewValues {
log.Info(err)
// Return a map with no outliers.
for label := range d {
outliers[label] = &OutlierResult{
UpperOutlier: false,
LowerOutlier: false,
}
}
return outliers, nil
}
if err != nil {
return nil, err
}
outliers := make(map[string]*OutlierResult, len(d))
// If we could could calculate quartiles for the dataset, we get
// outliers and populate a result map.
for label, value := range d {
outliers[label] = d.isIQROutlier(value, lower, upper, strong)
outliers[label] = d.isIQROutlier(
value, lower, upper, outlierMultiplier,
)
}
return outliers, nil
}
// GetThreshold returns the set of values in a dataset <= or > a given
// threshold. The below bool is used to toggle whether we identify values
// above or below the threshold.
func (d Dataset) GetThreshold(thresholdValue float64, below bool) map[string]bool {
threshold := make(map[string]bool, len(d.rawValues()))
for label, value := range d {
// If we are looking for values below the threshold, check the
// current value then move on to the next one.
if below {
// If the value is below or equal to the threshold, we
// set the label's value in the map to true. Otherwise
// we set it to false.
threshold[label] = value <= thresholdValue
continue
}
// We are looking for values above the threshold. Set the
// label's value to true if the value is greater than the
// threshold.
threshold[label] = value > thresholdValue
}
return threshold
}

View file

@ -2,6 +2,7 @@ package dataset
import (
"fmt"
"reflect"
"testing"
)
@ -71,7 +72,7 @@ func TestQuartiles(t *testing.T) {
{
name: "no elements",
values: []float64{},
expectedErr: ErrTooFewValues,
expectedErr: errTooFewValues,
},
{
name: "three elements",
@ -146,15 +147,21 @@ func TestIsOutlier(t *testing.T) {
tests := []struct {
name string
values map[string]float64
expectedError error
expectedOutliers map[string]*OutlierResult
strong bool
multiplier float64
}{
{
name: "too few values",
expectedError: ErrTooFewValues,
values: make(map[string]float64),
strong: true,
name: "too few values - all false",
values: map[string]float64{
"a": 1,
},
multiplier: 3,
expectedOutliers: map[string]*OutlierResult{
"a": {
UpperOutlier: false,
LowerOutlier: false,
},
},
},
{
name: "lower outlier",
@ -166,7 +173,7 @@ func TestIsOutlier(t *testing.T) {
"e": 8,
"f": 10,
},
strong: true,
multiplier: 3,
expectedOutliers: map[string]*OutlierResult{
"a": {
UpperOutlier: false,
@ -189,7 +196,7 @@ func TestIsOutlier(t *testing.T) {
"e": 3,
"f": 10,
},
strong: true,
multiplier: 3,
expectedOutliers: map[string]*OutlierResult{
"a": noOutlier,
"b": noOutlier,
@ -212,14 +219,9 @@ func TestIsOutlier(t *testing.T) {
dataset := New(test.values)
outliers, err := dataset.GetOutliers(test.strong)
if err != test.expectedError {
t.Fatalf("expected: %v, got: %v", test.expectedError, err)
}
// If the error is non-nil, there is no need for further checks.
outliers, err := dataset.GetOutliers(test.multiplier)
if err != nil {
return
t.Fatalf("unexpected error: %v", err)
}
for label, outlier := range outliers {
@ -242,3 +244,69 @@ func TestIsOutlier(t *testing.T) {
})
}
}
// TestGetThreshold tests getting thresholds for a dataset.
func TestGetThreshold(t *testing.T) {
tests := []struct {
name string
dataset Dataset
threshold float64
below bool
expectedValues map[string]bool
}{
{
name: "no values",
dataset: make(map[string]float64),
threshold: 1,
below: false,
expectedValues: make(map[string]bool),
},
{
name: "below and equal",
dataset: map[string]float64{
"a": 1,
"b": 2,
"c": 3,
},
threshold: 2,
below: true,
expectedValues: map[string]bool{
"a": true,
"b": true,
"c": false,
},
},
{
name: "above and equal",
dataset: map[string]float64{
"a": 1,
"b": 2,
"c": 3,
},
threshold: 2,
below: false,
expectedValues: map[string]bool{
"a": false,
"b": false,
"c": true,
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
values := test.dataset.GetThreshold(
test.threshold, test.below,
)
if !reflect.DeepEqual(test.expectedValues, values) {
t.Fatalf("expected: %v, got: %v",
test.expectedValues, values)
}
})
}
}

26
dataset/log.go Normal file
View file

@ -0,0 +1,26 @@
package dataset
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// Subsystem defines the logging code for this subsystem.
const Subsystem = "DSET"
// 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(Subsystem, nil))
}
// 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
}

5
go.mod
View file

@ -3,9 +3,14 @@ module github.com/lightninglabs/terminator
require (
github.com/btcsuite/btcd v0.20.1-beta
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/golang/protobuf v1.3.3
github.com/jessevdk/go-flags v1.4.0
github.com/lightninglabs/loop v0.2.4-alpha
github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d
github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191025122959-1a0ab538d53c
github.com/urfave/cli v1.20.0
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55
google.golang.org/grpc v1.27.0
)
go 1.13

12
go.sum
View file

@ -51,6 +51,7 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
@ -59,6 +60,8 @@ github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k=
github.com/frankban/quicktest v1.2.2 h1:xfmOhhoH5fGPgbEAlhLpJH9p0z/0Qizio9osmvn9IUY=
@ -72,6 +75,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -81,6 +85,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
@ -132,6 +138,8 @@ github.com/lightninglabs/loop v0.2.4-alpha/go.mod h1:n/8uTYPcWrU12xAQmUvjvfxKTFW
github.com/lightninglabs/neutrino v0.0.0-20190906012717-f087198de655/go.mod h1:awTrhbCWjWNH4yVwZ4IE7nZbvpQ27e7OyD+jao7wRxA=
github.com/lightninglabs/neutrino v0.10.0 h1:yWVy2cOCCXbKFdpYCE9vD1fWRJDd9FtGXhUws4l9RkU=
github.com/lightninglabs/neutrino v0.10.0/go.mod h1:C3KhCMk1Mcx3j8v0qRVWM1Ow6rIJSvSPnUAq00ZNAfk=
github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d h1:QWD/5MPnaZfUVP7P8wLa4M8Td2DI7XXHXt2vhVtUgGI=
github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d/go.mod h1:KDb67YMzoh4eudnzClmvs2FbiLG9vxISmLApUkCa4uI=
github.com/lightningnetwork/lightning-onion v0.0.0-20190909101754-850081b08b6a h1:GoWPN4i4jTKRxhVNh9a2vvBBO1Y2seiJB+SopUYoKyo=
github.com/lightningnetwork/lightning-onion v0.0.0-20190909101754-850081b08b6a/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4=
github.com/lightningnetwork/lnd v0.7.1-beta-rc2.0.20190914085956-35027e52fc22/go.mod h1:VaY0b5o38keUN3Ga6GVb/Mgta4B/CcCXwNvPAvhbv/A=
@ -161,6 +169,7 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@ -175,6 +184,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY=
github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@ -240,6 +250,8 @@ google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=

4
log.go
View file

@ -2,7 +2,9 @@ package terminator
import (
"github.com/btcsuite/btclog"
"github.com/lightninglabs/terminator/dataset"
"github.com/lightninglabs/terminator/recommend"
"github.com/lightninglabs/terminator/trmrpc"
"github.com/lightningnetwork/lnd/build"
)
@ -22,6 +24,8 @@ var (
func init() {
setSubLogger(Subsystem, log, nil)
addSubLogger(recommend.Subsystem, recommend.UseLogger)
addSubLogger(dataset.Subsystem, dataset.UseLogger)
addSubLogger(trmrpc.Subsystem, trmrpc.UseLogger)
}
// UseLogger uses a specified Logger to output package logging info.

View file

@ -18,10 +18,17 @@ import (
"github.com/lightningnetwork/lnd/lnrpc"
)
// errZeroMinMonitored is returned when the minimum ages provided by the config
// is zero.
var errZeroMinMonitored = errors.New("must provide a non-zero minimum " +
"monitor time for channel exclusion")
var (
// errZeroMinMonitored is returned when the minimum age provided
// by the config is zero.
errZeroMinMonitored = errors.New("must provide a non-zero minimum " +
"monitor time for channel exclusion")
// DefaultOutlierMultiplier is the default value used in close
// recommendations based on outliers when there is no user provided
// value.
DefaultOutlierMultiplier float64 = 3
)
// CloseRecommendationConfig provides the functions and parameters required to
// provide close recommendations.
@ -30,20 +37,31 @@ type CloseRecommendationConfig struct {
// public channels.
OpenChannels func() ([]*lnrpc.Channel, error)
// StrongOutlier is set to true if only extreme outliers should be
// recommended for close. A strong outlier is one which is 3 inter-
// quartile ranges below the lower quartile (or above the upper quartile)
// amd a weak outlier is only 1.5 inter-quartile ranges away. Choosing
// to recommend strong outliers is a more cautious approach, because the
// recommendations will be more lenient, only recommending extreme outliers
// for closure.
StrongOutlier bool
// OutlierMultiplier is the number of inter quartile ranges a value
// should be away from the lower/upper quartile to be considered an
// outlier. Recommended values are 1.5 for more aggressive recommendations
// and 3 for more cautious recommendations.
OutlierMultiplier float64
// UptimeThreshold is the uptime percentage over the channel's observed
// lifetime beneath which channels will be recommended for close. This
// value is expressed as a percentage in [0,1], and will default to 0 if
// it is not set.
UptimeThreshold float64
// MinimumMonitored is the minimum amount of time that a channel must have
// been monitored for before it is considered for closing.
MinimumMonitored time.Duration
}
// Recommendation provides the value that a close recommendation was
// based on, and a boolean indicating whether we recommend closing the
// channel.
type Recommendation struct {
Value float64
RecommendClose bool
}
// Report contains a set of close recommendations and information about the
// number of channels considered for close.
type Report struct {
@ -54,9 +72,15 @@ type Report struct {
// for long enough to be considered for close.
ConsideredChannels int
// Recommendations is a map of chanel outpoints to a bool which indicates
// whether we should close the channel.
Recommendations map[string]bool
// OutlierRecommendations is a map of chanel outpoints to a bool which
// indicates whether we should close the channel based on whether it is
// an outlier.
OutlierRecommendations map[string]Recommendation
// ThresholdRecommendations is a map of chanel outpoints to a bool which
// indicates whether we should close the channel based on whether it is
// below a user provided threshold.
ThresholdRecommendations map[string]Recommendation
}
// CloseRecommendations returns a report which contains information about the
@ -82,34 +106,68 @@ func CloseRecommendations(cfg *CloseRecommendationConfig) (*Report, error) {
// been monitored for longer than the minimum time.
uptime := getUptimeDataset(filtered)
recs, err := getCloseRecs(uptime, cfg.StrongOutlier)
if err != nil {
return nil, err
}
return &Report{
report := &Report{
TotalChannels: len(channels),
ConsideredChannels: len(uptime),
Recommendations: recs,
}, nil
}
}
// getCloseRecs generates map of channel outpoint strings to booleans indicating
// whether we recommend closing a channel.
func getCloseRecs(uptime dataset.Dataset,
strongOutlier bool) (map[string]bool, error) {
outliers, err := uptime.GetOutliers(strongOutlier)
// Get close recommendations based on outliers.
report.OutlierRecommendations, err = getOutlierRecs(
uptime, cfg.OutlierMultiplier,
)
if err != nil {
return nil, err
}
recommendations := make(map[string]bool)
// Get close recommendations based on threshold.
report.ThresholdRecommendations = getThresholdRecs(
uptime, cfg.UptimeThreshold,
)
for chanpoint, outlier := range outliers {
// If the channel is a lower outlier, recommend it for closure.
if outlier.LowerOutlier {
recommendations[chanpoint] = true
return report, nil
}
// getThresholdRecs returns a map of channel points to values that are below a
// given threshold.
func getThresholdRecs(uptime dataset.Dataset,
threshold float64) map[string]Recommendation {
// Get a map of channel labels to a boolean indicating whether
// they are beneath the threshold.
thresholdValues := uptime.GetThreshold(threshold, true)
recommendations := make(
map[string]Recommendation, len(thresholdValues),
)
for chanPoint, belowThrehsold := range thresholdValues {
recommendations[chanPoint] = Recommendation{
Value: uptime.Value(chanPoint),
RecommendClose: belowThrehsold,
}
}
return recommendations
}
// getOutlierRecs generates map of channel outpoint strings to booleans indicating
// whether we recommend closing a channel.
func getOutlierRecs(uptime dataset.Dataset,
outlierMultiplier float64) (map[string]Recommendation, error) {
recommendations := make(map[string]Recommendation)
outliers, err := uptime.GetOutliers(outlierMultiplier)
if err != nil {
return nil, err
}
// Add a recommendation for each channel to our set of recommendations.
// If the channel is a lower outlier, we recommend it for close.
for chanPoint, outlier := range outliers {
recommendations[chanPoint] = Recommendation{
Value: uptime.Value(chanPoint),
RecommendClose: outlier.LowerOutlier,
}
}
@ -135,7 +193,7 @@ func filterChannels(openChannels []*lnrpc.Channel,
channels[channel.ChannelPoint] = channel
}
log.Debugf("considering: % channels for close out of %v",
log.Debugf("considering: %v channels for close out of %v",
len(channels), len(openChannels))
return channels

View file

@ -29,7 +29,7 @@ func TestCloseRecommendations(t *testing.T) {
return nil, nil
},
MinMonitored: time.Hour,
expectedErr: dataset.ErrTooFewValues,
expectedErr: nil,
},
{
name: "open channels fails",
@ -77,9 +77,9 @@ func TestCloseRecommendations(t *testing.T) {
t.Parallel()
_, err := CloseRecommendations(&CloseRecommendationConfig{
OpenChannels: test.OpenChannels,
StrongOutlier: true,
MinimumMonitored: test.MinMonitored,
OpenChannels: test.OpenChannels,
OutlierMultiplier: 3,
MinimumMonitored: test.MinMonitored,
})
if err != test.expectedErr {
t.Fatalf("expected: %v, got: %v", test.expectedErr, err)
@ -89,15 +89,29 @@ func TestCloseRecommendations(t *testing.T) {
}
// TestGetCloseRecs tests the generating of close recommendations for a set of
// channels.
// channels. It also contains a test case for when there are too few channels
// to calculate outliers, to test that the error is silenced and no
// recommendations are provided.
func TestGetCloseRecs(t *testing.T) {
tests := []struct {
name string
channelUptimes map[string]float64
expectedRecs map[string]bool
strongOutlier bool
name string
channelUptimes map[string]float64
expectedRecs map[string]Recommendation
outlierMultiplier float64
}{
{
name: "not enough values, all false",
channelUptimes: map[string]float64{
"a:0": 0.7,
},
expectedRecs: map[string]Recommendation{
"a:0": {
Value: 0.7,
RecommendClose: false,
},
},
outlierMultiplier: 2,
},
{
name: "similar values, weak outlier no recommendations",
channelUptimes: map[string]float64{
@ -105,8 +119,12 @@ func TestGetCloseRecs(t *testing.T) {
"a:1": 0.6,
"a:20": 0.5,
},
strongOutlier: false,
expectedRecs: map[string]bool{},
outlierMultiplier: 1.5,
expectedRecs: map[string]Recommendation{
"a:0": {Value: 0.7, RecommendClose: false},
"a:1": {Value: 0.6, RecommendClose: false},
"a:20": {Value: 0.5, RecommendClose: false},
},
},
{
name: "similar values, strong outlier no recommendations",
@ -115,8 +133,12 @@ func TestGetCloseRecs(t *testing.T) {
"a:1": 0.6,
"a:2": 0.5,
},
strongOutlier: true,
expectedRecs: map[string]bool{},
outlierMultiplier: 3,
expectedRecs: map[string]Recommendation{
"a:0": {Value: 0.7, RecommendClose: false},
"a:1": {Value: 0.6, RecommendClose: false},
"a:2": {Value: 0.5, RecommendClose: false},
},
},
{
name: "lower outlier recommended for close",
@ -128,9 +150,35 @@ func TestGetCloseRecs(t *testing.T) {
"a:4": 0.5,
"a:5": 0.1,
},
strongOutlier: true,
expectedRecs: map[string]bool{
"a:5": true,
outlierMultiplier: 3,
expectedRecs: map[string]Recommendation{
"a:0": {Value: 0.6, RecommendClose: false},
"a:1": {Value: 0.6, RecommendClose: false},
"a:2": {Value: 0.5, RecommendClose: false},
"a:3": {Value: 0.5, RecommendClose: false},
"a:4": {Value: 0.5, RecommendClose: false},
"a:5": {Value: 0.1, RecommendClose: true},
},
},
{
name: "zero multiplier replaced with default",
channelUptimes: map[string]float64{
"a:0": 0.6,
"a:1": 0.6,
"a:2": 0.5,
"a:3": 0.5,
"a:4": 0.5,
"a:5": 0.1,
},
outlierMultiplier: 0,
expectedRecs: map[string]Recommendation{
"a:0": {Value: 0.6, RecommendClose: false},
"a:1": {Value: 0.6, RecommendClose: false},
"a:2": {Value: 0.5, RecommendClose: false},
"a:3": {Value: 0.5, RecommendClose: false},
"a:4": {Value: 0.5, RecommendClose: false},
"a:5": {Value: 0.1, RecommendClose: true},
},
},
}
@ -143,17 +191,24 @@ func TestGetCloseRecs(t *testing.T) {
uptimeData := dataset.New(test.channelUptimes)
recs, err := getCloseRecs(uptimeData, test.strongOutlier)
recs, err := getOutlierRecs(uptimeData, test.outlierMultiplier)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Run through our expected set of recommendations and check that
// they match the set returned in the report.
if len(test.expectedRecs) != len(recs) {
t.Fatalf("expected: %v recommendations, got: %v",
len(test.expectedRecs), len(recs))
}
// Run through our expected set of true recommendations
// and check that they match the set returned in the report.
for channel, expectClose := range test.expectedRecs {
recClose := recs[channel]
if recClose != expectClose {
t.Fatalf("expected close rec: %v for channel: %v,"+
" got: %v", expectClose, channel, recClose)
t.Fatalf("expected close rec: %v"+
" for channel: %v, got: %v",
expectClose, channel, recClose)
}
}
})

View file

@ -2,12 +2,11 @@
package terminator
import (
"context"
"fmt"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/terminator/recommend"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightninglabs/terminator/trmrpc"
"github.com/lightningnetwork/lnd/signal"
)
// Main is the real entry point for terminator. It is required to ensure that
@ -18,9 +17,6 @@ func Main() error {
return fmt.Errorf("error loading config: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// NewBasicClient get a lightning rpc client with
client, err := lndclient.NewBasicClient(
config.RPCServer,
@ -30,44 +26,28 @@ func Main() error {
lndclient.MacFilename(config.MacaroonFile),
)
if err != nil {
return fmt.Errorf("cannot connect to lightning client: %v", err)
return fmt.Errorf("cannot connect to lightning client: %v",
err)
}
// Get channel close recommendations for the current set of open public
// channels.
report, err := recommend.CloseRecommendations(
&recommend.CloseRecommendationConfig{
// OpenChannels provides all of the open, public channels for the
// node.
OpenChannels: func() (channels []*lnrpc.Channel, e error) {
resp, err := client.ListChannels(ctx,
&lnrpc.ListChannelsRequest{
PublicOnly: true,
})
if err != nil {
return nil, err
}
// Instantiate the terminator gRPC server.
server := trmrpc.NewRPCServer(
&trmrpc.Config{
LightningClient: client,
RPCListen: config.RPCListen,
},
)
return resp.Channels, nil
},
// For the first iteration of the terminator, do not allow users
// to configure recommendations to penalize weak outliers.
StrongOutlier: true,
// Set the minimum monitor time to the value provided in our config.
MinimumMonitored: config.MinimumMonitored,
})
if err != nil {
return fmt.Errorf("could not get close recommendations: %v", err)
if err := server.Start(); err != nil {
return err
}
log.Infof("Considering: %v channels for closure from a "+
"total of: %v. Produced %v recommendations.", report.ConsideredChannels,
report.TotalChannels, len(report.Recommendations))
// Run until the user terminates.
<-signal.ShutdownChannel()
log.Infof("Received shutdown signal.")
for channel, rec := range report.Recommendations {
log.Infof("%v: %v", channel, rec)
if err := server.Stop(); err != nil {
return err
}
log.Info("That's all for now. I will be back.")

8
trmrpc/README.md Normal file
View file

@ -0,0 +1,8 @@
trmrpc
=====
This package implements terminator's RPC client and server.
## Generate protobuf definitions
1. Follow the installation steps provided in lnd's [installation instructions](https://github.com/lightningnetwork/lnd/blob/master/lnrpc/README.md#generate-protobuf-definitions).
2. Run [`gen_protos.sh`](https://github.com/lightninglabs/terminator/tree/master/trmrpc/gen_protos.sh) to generate new protobuf definitions.

View file

@ -0,0 +1,66 @@
package trmrpc
import (
"context"
"time"
"github.com/lightninglabs/terminator/recommend"
)
// parseRequest parses a rpc close recommendation request and returns the
// close recommendation config that the request requires.
func parseRequest(ctx context.Context, cfg *Config,
req *CloseRecommendationsRequest) *recommend.CloseRecommendationConfig {
// Create a close recommendations config with the minimum monitored
// value provided in the request and the default outlier multiplier.
recConfig := &recommend.CloseRecommendationConfig{
OpenChannels: cfg.wrapListChannels(ctx, true),
MinimumMonitored: time.Second *
time.Duration(req.MinimumMonitored),
OutlierMultiplier: recommend.DefaultOutlierMultiplier,
}
// If a non-zero outlier multiple was provided, set it on the config.
if req.OutlierMultiplier != 0 {
recConfig.OutlierMultiplier = float64(req.OutlierMultiplier)
}
threshold, ok := req.Threshold.(*CloseRecommendationsRequest_UptimeThreshold)
if ok {
recConfig.UptimeThreshold = float64(threshold.UptimeThreshold)
}
return recConfig
}
// parseResponse parses the response obtained getting a close recommendation
// and converts it to a close recommendation response.
func parseResponse(report *recommend.Report) *CloseRecommendationsResponse {
resp := &CloseRecommendationsResponse{
TotalChannels: int32(report.TotalChannels),
ConsideredChannels: int32(report.ConsideredChannels),
}
for chanPoint, rec := range report.OutlierRecommendations {
resp.OutlierRecommendations = append(
resp.OutlierRecommendations, &Recommendation{
ChanPoint: chanPoint,
Value: float32(rec.Value),
RecommendClose: rec.RecommendClose,
},
)
}
for chanPoint, rec := range report.ThresholdRecommendations {
resp.ThresholdRecommendations = append(
resp.ThresholdRecommendations, &Recommendation{
ChanPoint: chanPoint,
Value: float32(rec.Value),
RecommendClose: rec.RecommendClose,
},
)
}
return resp
}

9
trmrpc/gen_protos.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh
echo "Generating terminator gRPC server protos"
# Generate the protos.
protoc -I/usr/local/include -I. \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--go_out=plugins=grpc,paths=source_relative:. \
rpc.proto

26
trmrpc/log.go Normal file
View file

@ -0,0 +1,26 @@
package trmrpc
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// Subsystem defines the logging code for this subsystem.
const Subsystem = "TRPC"
// 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(Subsystem, nil))
}
// 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
}

368
trmrpc/rpc.pb.go Normal file
View file

@ -0,0 +1,368 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: rpc.proto
package trmrpc
import (
context "context"
fmt "fmt"
proto "github.com/golang/protobuf/proto"
_ "google.golang.org/genproto/googleapis/api/annotations"
grpc "google.golang.org/grpc"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type CloseRecommendationsRequest struct {
//
//The minimum amount of time in seconds that a channel should have been
//monitored by lnd to be eligible for close. This value is in place to
//protect against closing of newer channels.
MinimumMonitored int64 `protobuf:"varint,1,opt,name=minimum_monitored,json=minimumMonitored,proto3" json:"minimum_monitored,omitempty"`
//
//The number of inter-quartile ranges a value needs to be beneath the lower
//quartile/ above the upper quartile to be considered a lower/upper outlier.
//Lower values will be more aggressive in recommending channel closes, and
//upper values will be more conservative. Recommended values are 1.5 for
//aggressive recommendations and 3 for conservative recommendations.
OutlierMultiplier float32 `protobuf:"fixed32,2,opt,name=outlier_multiplier,json=outlierMultiplier,proto3" json:"outlier_multiplier,omitempty"`
//
//Threshold contains the threshold value that is used to recommend channels
//for closure.
//
// Types that are valid to be assigned to Threshold:
// *CloseRecommendationsRequest_UptimeThreshold
Threshold isCloseRecommendationsRequest_Threshold `protobuf_oneof:"threshold"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *CloseRecommendationsRequest) Reset() { *m = CloseRecommendationsRequest{} }
func (m *CloseRecommendationsRequest) String() string { return proto.CompactTextString(m) }
func (*CloseRecommendationsRequest) ProtoMessage() {}
func (*CloseRecommendationsRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_77a6da22d6a3feb1, []int{0}
}
func (m *CloseRecommendationsRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_CloseRecommendationsRequest.Unmarshal(m, b)
}
func (m *CloseRecommendationsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_CloseRecommendationsRequest.Marshal(b, m, deterministic)
}
func (m *CloseRecommendationsRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_CloseRecommendationsRequest.Merge(m, src)
}
func (m *CloseRecommendationsRequest) XXX_Size() int {
return xxx_messageInfo_CloseRecommendationsRequest.Size(m)
}
func (m *CloseRecommendationsRequest) XXX_DiscardUnknown() {
xxx_messageInfo_CloseRecommendationsRequest.DiscardUnknown(m)
}
var xxx_messageInfo_CloseRecommendationsRequest proto.InternalMessageInfo
func (m *CloseRecommendationsRequest) GetMinimumMonitored() int64 {
if m != nil {
return m.MinimumMonitored
}
return 0
}
func (m *CloseRecommendationsRequest) GetOutlierMultiplier() float32 {
if m != nil {
return m.OutlierMultiplier
}
return 0
}
type isCloseRecommendationsRequest_Threshold interface {
isCloseRecommendationsRequest_Threshold()
}
type CloseRecommendationsRequest_UptimeThreshold struct {
UptimeThreshold float32 `protobuf:"fixed32,3,opt,name=uptime_threshold,json=uptimeThreshold,proto3,oneof"`
}
func (*CloseRecommendationsRequest_UptimeThreshold) isCloseRecommendationsRequest_Threshold() {}
func (m *CloseRecommendationsRequest) GetThreshold() isCloseRecommendationsRequest_Threshold {
if m != nil {
return m.Threshold
}
return nil
}
func (m *CloseRecommendationsRequest) GetUptimeThreshold() float32 {
if x, ok := m.GetThreshold().(*CloseRecommendationsRequest_UptimeThreshold); ok {
return x.UptimeThreshold
}
return 0
}
// XXX_OneofWrappers is for the internal use of the proto package.
func (*CloseRecommendationsRequest) XXX_OneofWrappers() []interface{} {
return []interface{}{
(*CloseRecommendationsRequest_UptimeThreshold)(nil),
}
}
type CloseRecommendationsResponse struct {
//
//The total number of channels, before filtering out channels that are
//not eligible for close recommendations.
TotalChannels int32 `protobuf:"varint,1,opt,name=total_channels,json=totalChannels,proto3" json:"total_channels,omitempty"`
//
//The number of channels that were considered for close recommendations.
ConsideredChannels int32 `protobuf:"varint,2,opt,name=considered_channels,json=consideredChannels,proto3" json:"considered_channels,omitempty"`
//
//A map of channels to close recommendations, based out whether they are
//outliers in the uptime dataset. The absence of a channel in this set
//implies that it was not considered for close because it did not meet
//the criteria for close (it is private, or has not been monitored for
//long enough to make a decision).
OutlierRecommendations []*Recommendation `protobuf:"bytes,3,rep,name=outlier_recommendations,json=outlierRecommendations,proto3" json:"outlier_recommendations,omitempty"`
//
//A set of channel close recommendations, based out whether they are
//beneath the threshold provided in the request. The absence of a channel
//in this set implies that it was not considered for close because it
//did not meet the criteria for close (it is private, or has not been
//monitored for long enough to make a decision).
ThresholdRecommendations []*Recommendation `protobuf:"bytes,4,rep,name=threshold_recommendations,json=thresholdRecommendations,proto3" json:"threshold_recommendations,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *CloseRecommendationsResponse) Reset() { *m = CloseRecommendationsResponse{} }
func (m *CloseRecommendationsResponse) String() string { return proto.CompactTextString(m) }
func (*CloseRecommendationsResponse) ProtoMessage() {}
func (*CloseRecommendationsResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_77a6da22d6a3feb1, []int{1}
}
func (m *CloseRecommendationsResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_CloseRecommendationsResponse.Unmarshal(m, b)
}
func (m *CloseRecommendationsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_CloseRecommendationsResponse.Marshal(b, m, deterministic)
}
func (m *CloseRecommendationsResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_CloseRecommendationsResponse.Merge(m, src)
}
func (m *CloseRecommendationsResponse) XXX_Size() int {
return xxx_messageInfo_CloseRecommendationsResponse.Size(m)
}
func (m *CloseRecommendationsResponse) XXX_DiscardUnknown() {
xxx_messageInfo_CloseRecommendationsResponse.DiscardUnknown(m)
}
var xxx_messageInfo_CloseRecommendationsResponse proto.InternalMessageInfo
func (m *CloseRecommendationsResponse) GetTotalChannels() int32 {
if m != nil {
return m.TotalChannels
}
return 0
}
func (m *CloseRecommendationsResponse) GetConsideredChannels() int32 {
if m != nil {
return m.ConsideredChannels
}
return 0
}
func (m *CloseRecommendationsResponse) GetOutlierRecommendations() []*Recommendation {
if m != nil {
return m.OutlierRecommendations
}
return nil
}
func (m *CloseRecommendationsResponse) GetThresholdRecommendations() []*Recommendation {
if m != nil {
return m.ThresholdRecommendations
}
return nil
}
type Recommendation struct {
//
//The channel point [funding txid: outpoint] of the channel being considered
//for close.
ChanPoint string `protobuf:"bytes,1,opt,name=chan_point,json=chanPoint,proto3" json:"chan_point,omitempty"`
// The value of the metric that close recommendations were based on.
Value float32 `protobuf:"fixed32,2,opt,name=value,proto3" json:"value,omitempty"`
// A boolean indicating whether we recommend closing the channel.
RecommendClose bool `protobuf:"varint,3,opt,name=recommend_close,json=recommendClose,proto3" json:"recommend_close,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Recommendation) Reset() { *m = Recommendation{} }
func (m *Recommendation) String() string { return proto.CompactTextString(m) }
func (*Recommendation) ProtoMessage() {}
func (*Recommendation) Descriptor() ([]byte, []int) {
return fileDescriptor_77a6da22d6a3feb1, []int{2}
}
func (m *Recommendation) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Recommendation.Unmarshal(m, b)
}
func (m *Recommendation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Recommendation.Marshal(b, m, deterministic)
}
func (m *Recommendation) XXX_Merge(src proto.Message) {
xxx_messageInfo_Recommendation.Merge(m, src)
}
func (m *Recommendation) XXX_Size() int {
return xxx_messageInfo_Recommendation.Size(m)
}
func (m *Recommendation) XXX_DiscardUnknown() {
xxx_messageInfo_Recommendation.DiscardUnknown(m)
}
var xxx_messageInfo_Recommendation proto.InternalMessageInfo
func (m *Recommendation) GetChanPoint() string {
if m != nil {
return m.ChanPoint
}
return ""
}
func (m *Recommendation) GetValue() float32 {
if m != nil {
return m.Value
}
return 0
}
func (m *Recommendation) GetRecommendClose() bool {
if m != nil {
return m.RecommendClose
}
return false
}
func init() {
proto.RegisterType((*CloseRecommendationsRequest)(nil), "trmrpc.CloseRecommendationsRequest")
proto.RegisterType((*CloseRecommendationsResponse)(nil), "trmrpc.CloseRecommendationsResponse")
proto.RegisterType((*Recommendation)(nil), "trmrpc.Recommendation")
}
func init() { proto.RegisterFile("rpc.proto", fileDescriptor_77a6da22d6a3feb1) }
var fileDescriptor_77a6da22d6a3feb1 = []byte{
// 387 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x52, 0xed, 0x6e, 0xd3, 0x40,
0x10, 0xc4, 0x36, 0xad, 0xf0, 0x56, 0xa4, 0xe9, 0x51, 0x15, 0x53, 0x8a, 0x14, 0x05, 0x10, 0x91,
0x2a, 0x12, 0xa9, 0xbc, 0x01, 0xfd, 0xc3, 0x9f, 0x0a, 0x74, 0xc9, 0x7f, 0xeb, 0xb0, 0x57, 0xc9,
0x49, 0x77, 0xb7, 0xc7, 0xdd, 0x39, 0x4f, 0xc3, 0x5b, 0xf0, 0x82, 0xc8, 0x9f, 0x51, 0xd2, 0x28,
0xff, 0xec, 0x99, 0xd9, 0xbd, 0xd9, 0x9d, 0x85, 0xd4, 0xd9, 0x62, 0x6e, 0x1d, 0x05, 0x62, 0xe7,
0xc1, 0x69, 0x67, 0x8b, 0xdb, 0xbb, 0x35, 0xd1, 0x5a, 0xe1, 0x42, 0x58, 0xb9, 0x10, 0xc6, 0x50,
0x10, 0x41, 0x92, 0xf1, 0xad, 0x6a, 0xfa, 0x2f, 0x82, 0xf7, 0x8f, 0x8a, 0x3c, 0x72, 0x2c, 0x48,
0x6b, 0x34, 0x65, 0x4b, 0x73, 0xfc, 0x53, 0xa1, 0x0f, 0xec, 0x1e, 0xae, 0xb4, 0x34, 0x52, 0x57,
0x3a, 0xd7, 0x64, 0x64, 0x20, 0x87, 0x65, 0x16, 0x4d, 0xa2, 0x59, 0xc2, 0xc7, 0x1d, 0xf1, 0xd4,
0xe3, 0xec, 0x2b, 0x30, 0xaa, 0x82, 0x92, 0xe8, 0x72, 0x5d, 0xa9, 0x20, 0x6d, 0xfd, 0x99, 0xc5,
0x93, 0x68, 0x16, 0xf3, 0xab, 0x8e, 0x79, 0x1a, 0x08, 0x76, 0x0f, 0xe3, 0xca, 0x06, 0xa9, 0x31,
0x0f, 0x1b, 0x87, 0x7e, 0x43, 0xaa, 0xcc, 0x92, 0x5a, 0xfc, 0xe3, 0x05, 0xbf, 0x6c, 0x99, 0x55,
0x4f, 0x7c, 0xbf, 0x80, 0x74, 0x50, 0x4d, 0xff, 0xc6, 0x70, 0x77, 0xdc, 0xb5, 0xb7, 0x64, 0x3c,
0xb2, 0xcf, 0x30, 0x0a, 0x14, 0x84, 0xca, 0x8b, 0x8d, 0x30, 0x06, 0x95, 0x6f, 0x3c, 0x9f, 0xf1,
0xd7, 0x0d, 0xfa, 0xd8, 0x81, 0x6c, 0x01, 0x6f, 0x0a, 0x32, 0x5e, 0x96, 0xe8, 0xb0, 0xdc, 0x69,
0xe3, 0x46, 0xcb, 0x76, 0xd4, 0x50, 0xf0, 0x13, 0xde, 0xf6, 0x13, 0xba, 0xfd, 0xa7, 0xb3, 0x64,
0x92, 0xcc, 0x2e, 0x1e, 0x6e, 0xe6, 0xed, 0xda, 0xe7, 0xfb, 0xce, 0xf8, 0x4d, 0x57, 0x76, 0x60,
0x98, 0x2d, 0xe1, 0xdd, 0x30, 0xd6, 0xb3, 0x96, 0x2f, 0x4f, 0xb6, 0xcc, 0x86, 0xc2, 0x83, 0xa6,
0x53, 0x03, 0xa3, 0x7d, 0x88, 0x7d, 0x00, 0xa8, 0xa7, 0xcb, 0x2d, 0x49, 0x13, 0x9a, 0x5d, 0xa4,
0x3c, 0xad, 0x91, 0x5f, 0x35, 0xc0, 0xae, 0xe1, 0x6c, 0x2b, 0x54, 0x85, 0x5d, 0x56, 0xed, 0x0f,
0xfb, 0x02, 0x97, 0x83, 0xa3, 0xbc, 0xa8, 0xd7, 0xdd, 0xc4, 0xf3, 0x8a, 0x8f, 0x06, 0xb8, 0x09,
0xe1, 0xa1, 0x82, 0xf1, 0x0a, 0x9d, 0x96, 0x46, 0x04, 0x72, 0x4b, 0x74, 0x5b, 0x74, 0x4c, 0xc0,
0xf5, 0xb1, 0x84, 0xd8, 0xc7, 0x7e, 0x9a, 0x13, 0x57, 0x77, 0xfb, 0xe9, 0xb4, 0xa8, 0x0d, 0xf9,
0xf7, 0x79, 0x73, 0xc2, 0xdf, 0xfe, 0x07, 0x00, 0x00, 0xff, 0xff, 0x3f, 0x08, 0x22, 0xe1, 0xf5,
0x02, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// TerminatorServerClient is the client API for TerminatorServer service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type TerminatorServerClient interface {
CloseRecommendations(ctx context.Context, in *CloseRecommendationsRequest, opts ...grpc.CallOption) (*CloseRecommendationsResponse, error)
}
type terminatorServerClient struct {
cc *grpc.ClientConn
}
func NewTerminatorServerClient(cc *grpc.ClientConn) TerminatorServerClient {
return &terminatorServerClient{cc}
}
func (c *terminatorServerClient) CloseRecommendations(ctx context.Context, in *CloseRecommendationsRequest, opts ...grpc.CallOption) (*CloseRecommendationsResponse, error) {
out := new(CloseRecommendationsResponse)
err := c.cc.Invoke(ctx, "/trmrpc.TerminatorServer/CloseRecommendations", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// TerminatorServerServer is the server API for TerminatorServer service.
type TerminatorServerServer interface {
CloseRecommendations(context.Context, *CloseRecommendationsRequest) (*CloseRecommendationsResponse, error)
}
func RegisterTerminatorServerServer(s *grpc.Server, srv TerminatorServerServer) {
s.RegisterService(&_TerminatorServer_serviceDesc, srv)
}
func _TerminatorServer_CloseRecommendations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CloseRecommendationsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TerminatorServerServer).CloseRecommendations(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/trmrpc.TerminatorServer/CloseRecommendations",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TerminatorServerServer).CloseRecommendations(ctx, req.(*CloseRecommendationsRequest))
}
return interceptor(ctx, in, info, handler)
}
var _TerminatorServer_serviceDesc = grpc.ServiceDesc{
ServiceName: "trmrpc.TerminatorServer",
HandlerType: (*TerminatorServerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CloseRecommendations",
Handler: _TerminatorServer_CloseRecommendations_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "rpc.proto",
}

84
trmrpc/rpc.proto Normal file
View file

@ -0,0 +1,84 @@
syntax = "proto3";
import "google/api/annotations.proto";
package trmrpc;
service TerminatorServer {
rpc CloseRecommendations (CloseRecommendationsRequest) returns (CloseRecommendationsResponse);
}
message CloseRecommendationsRequest {
/*
The minimum amount of time in seconds that a channel should have been
monitored by lnd to be eligible for close. This value is in place to
protect against closing of newer channels.
*/
int64 minimum_monitored = 1;
/*
The number of inter-quartile ranges a value needs to be beneath the lower
quartile/ above the upper quartile to be considered a lower/upper outlier.
Lower values will be more aggressive in recommending channel closes, and
upper values will be more conservative. Recommended values are 1.5 for
aggressive recommendations and 3 for conservative recommendations.
*/
float outlier_multiplier = 2;
/*
Threshold contains the threshold value that is used to recommend channels
for closure.
*/
oneof threshold{
/*
The threshold percentage uptime over observed lifetime beneath which
channels will be recommended for closure.
*/
float uptime_threshold = 3;
}
}
message CloseRecommendationsResponse{
/*
The total number of channels, before filtering out channels that are
not eligible for close recommendations.
*/
int32 total_channels = 1;
/*
The number of channels that were considered for close recommendations.
*/
int32 considered_channels = 2;
/*
A map of channels to close recommendations, based out whether they are
outliers in the uptime dataset. The absence of a channel in this set
implies that it was not considered for close because it did not meet
the criteria for close (it is private, or has not been monitored for
long enough to make a decision).
*/
repeated Recommendation outlier_recommendations = 3;
/*
A set of channel close recommendations, based out whether they are
beneath the threshold provided in the request. The absence of a channel
in this set implies that it was not considered for close because it
did not meet the criteria for close (it is private, or has not been
monitored for long enough to make a decision).
*/
repeated Recommendation threshold_recommendations = 4;
}
message Recommendation{
/*
The channel point [funding txid: outpoint] of the channel being considered
for close.
*/
string chan_point = 1;
// The value of the metric that close recommendations were based on.
float value = 2;
// A boolean indicating whether we recommend closing the channel.
bool recommend_close = 3;
}

145
trmrpc/rpcserver.go Normal file
View file

@ -0,0 +1,145 @@
// Package trmrpc contains the proto files, generated code and server logic
// for the terminator's grpc server which serves requests for close
// recommendations.
//
// The Terminator 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 trmrpc
import (
"context"
"fmt"
"net"
"sync"
"sync/atomic"
"github.com/lightninglabs/terminator/recommend"
"github.com/lightningnetwork/lnd/lnrpc"
"google.golang.org/grpc"
)
// RPCServer implements the terminator service, serving requests over grpc.
type RPCServer struct {
// To be used atomically.
started int32
// To be used atomically.
stopped int32
// 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 to use when starting the grpc server.
rpcListener net.Listener
wg sync.WaitGroup
}
// Config provides closures and settings required to run the rpc server.
type Config struct {
// LightningClient is a client which can be used to query lnd.
LightningClient lnrpc.LightningClient
// RPCListen is the address:port that the rpc server should listen
// on.
RPCListen string
}
// wrapListChannels wraps the listchannels call to lnd, with a publicOnly bool
// that can be used to toggle whether private channels are included.
func (c *Config) wrapListChannels(ctx context.Context,
publicOnly bool) func() ([]*lnrpc.Channel, error) {
return func() (channels []*lnrpc.Channel, e error) {
resp, err := c.LightningClient.ListChannels(
ctx,
&lnrpc.ListChannelsRequest{
PublicOnly: publicOnly,
},
)
if err != nil {
return nil, err
}
return resp.Channels, nil
}
}
// 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 {
var opts []grpc.ServerOption
grpcServer := grpc.NewServer(opts...)
return &RPCServer{
cfg: cfg,
grpcServer: grpcServer,
}
}
// Start starts the listener and server.
func (s *RPCServer) Start() error {
if atomic.AddInt32(&s.started, 1) != 1 {
return nil
}
// Start the gRPC RPCServer listening for HTTP/2 connections.
log.Info("Starting gRPC listener")
grpcListener, err := net.Listen("tcp", s.cfg.RPCListen)
if err != nil {
return fmt.Errorf("RPC RPCServer unable to listen on %v",
s.cfg.RPCListen)
}
s.rpcListener = grpcListener
RegisterTerminatorServerServer(s.grpcServer, s)
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)
}
}()
return nil
}
// Stop stops the grpc listener and server.
func (s *RPCServer) Stop() error {
if atomic.AddInt32(&s.stopped, 1) != 1 {
return nil
}
// Stop the grpc server and wait for all go routines to terminate.
s.grpcServer.Stop()
s.wg.Wait()
return nil
}
// CloseRecommendations provides a set of close recommendations for the
// current set of open channels.
func (s *RPCServer) CloseRecommendations(ctx context.Context,
req *CloseRecommendationsRequest) (*CloseRecommendationsResponse,
error) {
cfg := parseRequest(ctx, s.cfg, req)
report, err := recommend.CloseRecommendations(cfg)
if err != nil {
return nil, err
}
return parseResponse(report), nil
}