insights: add channel insights package

This commit is contained in:
carla 2020-01-31 13:30:43 +02:00
parent 98848af064
commit b70cd0aa3f
No known key found for this signature in database
GPG key ID: 4CA7FE54A6213C91
2 changed files with 272 additions and 0 deletions

123
insights/insights.go Normal file
View file

@ -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
}

149
insights/insights_test.go Normal file
View file

@ -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])
}
}
})
}
}