mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-22 22:25:24 +01:00
Merge pull request #2797 from halseth/autopilot-prefattach-small-chan-penalize
[autopilot] penalize small channels in preferantial attachment heuristic
This commit is contained in:
commit
a069e78b74
7 changed files with 732 additions and 591 deletions
|
@ -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
50
autopilot/graph_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
1175
lnrpc/rpc.pb.go
1175
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
|
@ -2400,6 +2400,10 @@
|
|||
"max_channel_size": {
|
||||
"type": "string",
|
||||
"format": "int64"
|
||||
},
|
||||
"median_channel_size_sat": {
|
||||
"type": "string",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
15
rpcserver.go
15
rpcserver.go
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue