mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-23 06:35:07 +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"
|
"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
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"
|
"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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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"];
|
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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
15
rpcserver.go
15
rpcserver.go
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue