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" "bytes"
"math/big" "math/big"
"net" "net"
"sort"
"sync/atomic" "sync/atomic"
"time" "time"
@ -501,3 +502,22 @@ func (m memNode) ForEachChannel(cb func(ChannelEdge) error) error {
return nil 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" "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 // PrefAttachment is an implementation of the AttachmentHeuristic interface
// that implement a non-linear preferential attachment heuristic. This means // that implement a non-linear preferential attachment heuristic. This means
// that given a threshold to allocate to automatic channel establishment, the // 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 // 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. // 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 // 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. // 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{}) ( chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*NodeScore, error) { 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 var maxChans int
nodeChanNum := make(map[NodeID]int) nodeChanNum := make(map[NodeID]int)
if err := g.ForEachNode(func(n Node) error { if err := g.ForEachNode(func(n Node) error {
var nodeChans int 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++ nodeChans++
return nil return nil
}) })
@ -132,9 +180,9 @@ func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []Channel,
case ok: case ok:
continue continue
// If the node had no channels, we skip it, since it would have // If the node had no large channels, we skip it, since it
// gotten a zero score anyway. // would have gotten a zero score anyway.
case nodeChans == 0: case nodeChans <= 0:
continue 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"]; double avg_channel_size = 7 [json_name = "avg_channel_size"];
int64 min_channel_size = 8 [json_name = "min_channel_size"]; int64 min_channel_size = 8 [json_name = "min_channel_size"];
int64 max_channel_size = 9 [json_name = "max_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 // TODO(roasbeef): fee rate info, expiry
// * also additional RPC for tracking fee info once in // * also additional RPC for tracking fee info once in

View file

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