Merge pull request #2797 from halseth/autopilot-prefattach-small-chan-penalize

[autopilot] penalize small channels in preferantial attachment heuristic
This commit is contained in:
Olaoluwa Osuntokun 2019-03-27 18:09:31 -07:00 committed by GitHub
commit a069e78b74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 732 additions and 591 deletions

View file

@ -4,6 +4,7 @@ import (
"bytes"
"math/big"
"net"
"sort"
"sync/atomic"
"time"
@ -501,3 +502,22 @@ func (m memNode) ForEachChannel(cb func(ChannelEdge) error) error {
return nil
}
// Median returns the median value in the slice of Amounts.
func Median(vals []btcutil.Amount) btcutil.Amount {
sort.Slice(vals, func(i, j int) bool {
return vals[i] < vals[j]
})
num := len(vals)
switch {
case num == 0:
return 0
case num%2 == 0:
return (vals[num/2-1] + vals[num/2]) / 2
default:
return vals[num/2]
}
}

50
autopilot/graph_test.go Normal file
View file

@ -0,0 +1,50 @@
package autopilot_test
import (
"testing"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/autopilot"
)
// TestMedian tests the Median method.
func TestMedian(t *testing.T) {
t.Parallel()
testCases := []struct {
values []btcutil.Amount
median btcutil.Amount
}{
{
values: []btcutil.Amount{},
median: 0,
},
{
values: []btcutil.Amount{10},
median: 10,
},
{
values: []btcutil.Amount{10, 20},
median: 15,
},
{
values: []btcutil.Amount{10, 20, 30},
median: 20,
},
{
values: []btcutil.Amount{30, 10, 20},
median: 20,
},
{
values: []btcutil.Amount{10, 10, 10, 10, 5000000},
median: 10,
},
}
for _, test := range testCases {
res := autopilot.Median(test.values)
if res != test.median {
t.Fatalf("expected median %v, got %v", test.median, res)
}
}
}

View file

@ -8,6 +8,12 @@ import (
"github.com/btcsuite/btcutil"
)
// minMedianChanSizeFraction determines the minimum size a channel must have to
// count positively when calculating the scores using preferential attachment.
// The minimum channel size is calculated as median/minMedianChanSizeFraction,
// where median is the median channel size of the entire graph.
const minMedianChanSizeFraction = 4
// PrefAttachment is an implementation of the AttachmentHeuristic interface
// that implement a non-linear preferential attachment heuristic. This means
// that given a threshold to allocate to automatic channel establishment, the
@ -64,6 +70,10 @@ func (p *PrefAttachment) Name() string {
// implemented globally for each new participant, this results in a channel
// graph that is scale-free and follows a power law distribution with k=-3.
//
// To avoid assigning a high score to nodes with a large number of small
// channels, we only count channels at least as large as a given fraction of
// the graph's median channel size.
//
// The returned scores will be in the range [0.0, 1.0], where higher scores are
// given to nodes already having high connectivity in the graph.
//
@ -72,12 +82,50 @@ func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*NodeScore, error) {
// Count the number of channels for each particular node in the graph.
// We first run though the graph once in order to find the median
// channel size.
var (
allChans []btcutil.Amount
seenChans = make(map[uint64]struct{})
)
if err := g.ForEachNode(func(n Node) error {
err := n.ForEachChannel(func(e ChannelEdge) error {
if _, ok := seenChans[e.ChanID.ToUint64()]; ok {
return nil
}
seenChans[e.ChanID.ToUint64()] = struct{}{}
allChans = append(allChans, e.Capacity)
return nil
})
if err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
medianChanSize := Median(allChans)
// Count the number of large-ish channels for each particular node in
// the graph.
var maxChans int
nodeChanNum := make(map[NodeID]int)
if err := g.ForEachNode(func(n Node) error {
var nodeChans int
err := n.ForEachChannel(func(_ ChannelEdge) error {
err := n.ForEachChannel(func(e ChannelEdge) error {
// Since connecting to nodes with a lot of small
// channels actually worsens our connectivity in the
// graph (we will potentially waste time trying to use
// these useless channels in path finding), we decrease
// the counter for such channels.
if e.Capacity < medianChanSize/minMedianChanSizeFraction {
nodeChans--
return nil
}
// Larger channels we count.
nodeChans++
return nil
})
@ -132,9 +180,9 @@ func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
case ok:
continue
// If the node had no channels, we skip it, since it would have
// gotten a zero score anyway.
case nodeChans == 0:
// If the node had no large channels, we skip it, since it
// would have gotten a zero score anyway.
case nodeChans <= 0:
continue
}

File diff suppressed because it is too large Load diff

View file

@ -1738,6 +1738,7 @@ message NetworkInfo {
double avg_channel_size = 7 [json_name = "avg_channel_size"];
int64 min_channel_size = 8 [json_name = "min_channel_size"];
int64 max_channel_size = 9 [json_name = "max_channel_size"];
int64 median_channel_size_sat = 10 [json_name = "median_channel_size_sat"];
// TODO(roasbeef): fee rate info, expiry
// * also additional RPC for tracking fee info once in

View file

@ -2400,6 +2400,10 @@
"max_channel_size": {
"type": "string",
"format": "int64"
},
"median_channel_size_sat": {
"type": "string",
"format": "int64"
}
}
},

View file

@ -3954,6 +3954,7 @@ func (r *rpcServer) GetNetworkInfo(ctx context.Context,
totalNetworkCapacity btcutil.Amount
minChannelSize btcutil.Amount = math.MaxInt64
maxChannelSize btcutil.Amount
medianChanSize btcutil.Amount
)
// We'll use this map to de-duplicate channels during our traversal.
@ -3961,6 +3962,10 @@ func (r *rpcServer) GetNetworkInfo(ctx context.Context,
// edges for each channel within the graph.
seenChans := make(map[uint64]struct{})
// We also keep a list of all encountered capacities, in order to
// calculate the median channel size.
var allChans []btcutil.Amount
// We'll run through all the known nodes in the within our view of the
// network, tallying up the total number of nodes, and also gathering
// each node so we can measure the graph diameter and degree stats
@ -4007,6 +4012,7 @@ func (r *rpcServer) GetNetworkInfo(ctx context.Context,
numChannels++
seenChans[edge.ChannelID] = struct{}{}
allChans = append(allChans, edge.Capacity)
return nil
}); err != nil {
return err
@ -4023,6 +4029,9 @@ func (r *rpcServer) GetNetworkInfo(ctx context.Context,
return nil, err
}
// Find the median.
medianChanSize = autopilot.Median(allChans)
// If we don't have any channels, then reset the minChannelSize to zero
// to avoid outputting NaN in encoded JSON.
if numChannels == 0 {
@ -4032,7 +4041,6 @@ func (r *rpcServer) GetNetworkInfo(ctx context.Context,
// TODO(roasbeef): graph diameter
// TODO(roasbeef): also add oldest channel?
// * also add median channel size
netInfo := &lnrpc.NetworkInfo{
MaxOutDegree: maxChanOut,
AvgOutDegree: float64(numChannels) / float64(numNodes),
@ -4041,8 +4049,9 @@ func (r *rpcServer) GetNetworkInfo(ctx context.Context,
TotalNetworkCapacity: int64(totalNetworkCapacity),
AvgChannelSize: float64(totalNetworkCapacity) / float64(numChannels),
MinChannelSize: int64(minChannelSize),
MaxChannelSize: int64(maxChannelSize),
MinChannelSize: int64(minChannelSize),
MaxChannelSize: int64(maxChannelSize),
MedianChannelSizeSat: int64(medianChanSize),
}
// Similarly, if we don't have any channels, then we'll also set the