From b70cd0aa3f4c2ab535f752690f643b780b5177cd Mon Sep 17 00:00:00 2001 From: carla Date: Fri, 31 Jan 2020 13:30:43 +0200 Subject: [PATCH] insights: add channel insights package --- insights/insights.go | 123 +++++++++++++++++++++++++++++++ insights/insights_test.go | 149 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 insights/insights.go create mode 100644 insights/insights_test.go diff --git a/insights/insights.go b/insights/insights.go new file mode 100644 index 0000000..cc53bbe --- /dev/null +++ b/insights/insights.go @@ -0,0 +1,123 @@ +package insights + +import ( + "time" + + "github.com/lightninglabs/terminator/revenue" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwire" +) + +// ChannelInfo provides a set of performance metrics for a lightning channel. +type ChannelInfo struct { + // ChannelPoint is the outpoint of the channel's funding transaction. + ChannelPoint string + + // MonitoredFor is the amount of time the channel's uptime has been + // monitored by lnd. + MonitoredFor time.Duration + + // Uptime is the total amount of time the channel's remote peer has + // been online for. + Uptime time.Duration + + // VolumeIncoming is the volume in millisatoshis that the channel has + // forwarded through the node as the incoming channel. + VolumeIncoming lnwire.MilliSatoshi + + // VolumeOutgoing is the volume in millisatoshis that the channel has + // forwarded through the node as the outgoing channel. + VolumeOutgoing lnwire.MilliSatoshi + + // FeesEarned is the total fees earned by the channel while routing. + // Note that fees are split evenly between incoming and outgoing + // channels. + FeesEarned lnwire.MilliSatoshi + + // Confirmations is the number of confirmations the funding transction + // has. + Confirmations uint32 + + // Private indicates whether the channel is private. + Private bool +} + +// Config provides insights with everything it needs to obtain channel +// insights. +type Config struct { + // OpenChannels is a function which returns all of our currently open, + // public and private channels. + OpenChannels func() ([]*lnrpc.Channel, error) + + // CurrentHeight is a function which returns the current block + // currentHeight. + CurrentHeight func() (uint32, error) + + // RevenueReport is a report our channels revenue. + RevenueReport *revenue.Report +} + +// GetChannels returns an array of channel insights. +func GetChannels(cfg *Config) ([]*ChannelInfo, error) { + // Get the current block height. + height, err := cfg.CurrentHeight() + if err != nil { + return nil, err + } + + channels, err := cfg.OpenChannels() + if err != nil { + return nil, err + } + + insights := make([]*ChannelInfo, 0, len(channels)) + for _, channel := range channels { + // Get the short channel ID so we can calculate the number of + // blocks the channel has been open for. + shortID := lnwire.NewShortChanIDFromInt(channel.ChanId) + + // Calculate the number of confirmations the channel has. We + // do not need to check whether channel height >= current + // height because we are working with already open channels. + // If the funding transaction is in the current block, it is + // considered to have one confirmation, so we add one to the + // current height to reflect this. + confirmations := (height + 1) - shortID.BlockHeight + + monitored := time.Second * time.Duration(channel.Lifetime) + uptime := time.Second * time.Duration(channel.Uptime) + + // Create a channel insight for the channel. + channelInsight := &ChannelInfo{ + ChannelPoint: channel.ChannelPoint, + MonitoredFor: monitored, + Uptime: uptime, + Confirmations: confirmations, + Private: channel.Private, + } + + // If the channel is not present in the revenue report, it has + // not generated any revenue over the period so we can add it + // to our set of insights and proceed to the next channel. + reports, ok := cfg.RevenueReport.ChannelPairs[channel.ChannelPoint] + if !ok { + insights = append(insights, channelInsight) + continue + } + + // Accumulate revenue totals for the channel. + for _, rev := range reports { + channelInsight.VolumeIncoming += rev.AmountIncoming + channelInsight.VolumeOutgoing += rev.AmountOutgoing + + // We spilt fees evenly between the channels, so that + // we do not double count fees. + channelInsight.FeesEarned += + (rev.FeesOutgoing + rev.FeesIncoming) / 2 + } + + insights = append(insights, channelInsight) + } + + return insights, nil +} diff --git a/insights/insights_test.go b/insights/insights_test.go new file mode 100644 index 0000000..9ca0356 --- /dev/null +++ b/insights/insights_test.go @@ -0,0 +1,149 @@ +package insights + +import ( + "reflect" + "testing" + "time" + + "github.com/lightninglabs/terminator/revenue" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwire" +) + +// TestGetChannels tests gathering of channel insights from a set of lnrpc +// channels and a revenue report. +func TestGetChannels(t *testing.T) { + // Make a short channel ID for a channel at height 1000. + channelHeight1000 := lnwire.ShortChannelID{ + BlockHeight: 1000, + TxIndex: 1, + TxPosition: 3, + } + + // Create an empty revenue report. + noRevenue := &revenue.Report{ + ChannelPairs: map[string]map[string]revenue.Revenue{}, + } + + // report is a revenue report with the channel opened in block 1000 in + // it. + report := &revenue.Report{ + ChannelPairs: map[string]map[string]revenue.Revenue{ + "a:1": { + "b:1": revenue.Revenue{ + AmountOutgoing: 25, + AmountIncoming: 10, + FeesIncoming: 10, + FeesOutgoing: 10, + }, + "b:2": revenue.Revenue{ + AmountOutgoing: 0, + AmountIncoming: 10, + FeesIncoming: 20, + FeesOutgoing: 0, + }, + }, + }, + } + + hourInSeconds := int64(time.Hour.Seconds()) + + tests := []struct { + name string + channels []*lnrpc.Channel + currentHeight uint32 + revenue *revenue.Report + expectedInsights []*ChannelInfo + }{ + { + name: "no channels", + channels: []*lnrpc.Channel{}, + currentHeight: 2000, + revenue: noRevenue, + expectedInsights: []*ChannelInfo{}, + }, { + name: "one confirmation", + channels: []*lnrpc.Channel{ + { + ChannelPoint: "a:1", + Lifetime: hourInSeconds, + Uptime: hourInSeconds / 2, + ChanId: channelHeight1000.ToUint64(), + }, + }, + currentHeight: 1000, + revenue: noRevenue, + expectedInsights: []*ChannelInfo{ + { + ChannelPoint: "a:1", + MonitoredFor: time.Hour, + Uptime: time.Minute * 30, + Confirmations: 1, + Private: false, + }, + }, + }, + { + name: "two confirmations", + channels: []*lnrpc.Channel{ + { + ChannelPoint: "a:1", + Lifetime: hourInSeconds, + Uptime: hourInSeconds / 2, + ChanId: channelHeight1000.ToUint64(), + }, + }, + currentHeight: 1001, + revenue: report, + expectedInsights: []*ChannelInfo{ + { + ChannelPoint: "a:1", + MonitoredFor: time.Hour, + Uptime: time.Minute * 30, + Confirmations: 2, + VolumeIncoming: 20, + VolumeOutgoing: 25, + FeesEarned: 20, + Private: false, + }, + }, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + insights, err := GetChannels(&Config{ + OpenChannels: func() ( + channels []*lnrpc.Channel, err error) { + + return test.channels, nil + }, + CurrentHeight: func() (u uint32, e error) { + return test.currentHeight, nil + }, + RevenueReport: test.revenue, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(test.expectedInsights) != len(insights) { + t.Fatalf("expected: %v insights, got: %v", + len(test.expectedInsights), + len(insights)) + } + + for i, insight := range test.expectedInsights { + if !reflect.DeepEqual(insights[i], insight) { + t.Fatalf("expected: %v, got: %v", + insight, insights[i]) + } + } + + }) + } +}