mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 01:43:16 +01:00
lnrpc+autopilot: add graph diameter calculation
* adds a brute force computation of the diameter * adds a more efficient calculation of the diameter
This commit is contained in:
parent
4d9a05c2f4
commit
52d56a8990
@ -1,5 +1,12 @@
|
||||
package autopilot
|
||||
|
||||
// diameterCutoff is used to discard nodes in the diameter calculation.
|
||||
// It is the multiplier for the eccentricity of the highest-degree node,
|
||||
// serving as a cutoff to discard all nodes with a smaller hop distance. This
|
||||
// number should not be set close to 1 and is a tradeoff for computation cost,
|
||||
// where 0 is maximally costly.
|
||||
const diameterCutoff = 0.75
|
||||
|
||||
// SimpleGraph stores a simplifed adj graph of a channel graph to speed
|
||||
// up graph processing by eliminating all unnecessary hashing and map access.
|
||||
type SimpleGraph struct {
|
||||
@ -62,5 +69,162 @@ func NewSimpleGraph(g ChannelGraph) (*SimpleGraph, error) {
|
||||
graph.Nodes[nodeIndex] = nodeID
|
||||
}
|
||||
|
||||
// We prepare to give some debug output about the size of the graph.
|
||||
totalChannels := 0
|
||||
for _, channels := range graph.Adj {
|
||||
totalChannels += len(channels)
|
||||
}
|
||||
|
||||
// The number of channels is double counted, so divide by two.
|
||||
log.Debugf("Initialized simple graph with %d nodes and %d "+
|
||||
"channels", len(graph.Adj), totalChannels/2)
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
// maxVal is a helper function to get the maximal value of all values of a map.
|
||||
func maxVal(mapping map[int]uint32) uint32 {
|
||||
maxValue := uint32(0)
|
||||
for _, value := range mapping {
|
||||
if maxValue < value {
|
||||
maxValue = value
|
||||
}
|
||||
}
|
||||
return maxValue
|
||||
}
|
||||
|
||||
// degree determines the number of edges for a node in the graph.
|
||||
func (graph *SimpleGraph) degree(node int) int {
|
||||
return len(graph.Adj[node])
|
||||
}
|
||||
|
||||
// nodeMaxDegree determines the node with the max degree and its degree.
|
||||
func (graph *SimpleGraph) nodeMaxDegree() (int, int) {
|
||||
var maxNode, maxDegree int
|
||||
for node := range graph.Adj {
|
||||
degree := graph.degree(node)
|
||||
if degree > maxDegree {
|
||||
maxNode = node
|
||||
maxDegree = degree
|
||||
}
|
||||
}
|
||||
return maxNode, maxDegree
|
||||
}
|
||||
|
||||
// shortestPathLengths performs a breadth-first-search from a node to all other
|
||||
// nodes, returning the lengths of the paths.
|
||||
func (graph *SimpleGraph) shortestPathLengths(node int) map[int]uint32 {
|
||||
// level indicates the shell of the search around the root node.
|
||||
var level uint32
|
||||
graphOrder := len(graph.Adj)
|
||||
|
||||
// nextLevel tracks which nodes should be visited in the next round.
|
||||
nextLevel := make([]int, 0, graphOrder)
|
||||
|
||||
// The root node is put as a starting point for the exploration.
|
||||
nextLevel = append(nextLevel, node)
|
||||
|
||||
// Seen tracks already visited nodes and tracks how far away they are.
|
||||
seen := make(map[int]uint32, graphOrder)
|
||||
|
||||
// Mark the root node as seen.
|
||||
seen[node] = level
|
||||
|
||||
// thisLevel contains the nodes that are explored in the round.
|
||||
thisLevel := make([]int, 0, graphOrder)
|
||||
|
||||
// We discover other nodes in a ring-like structure as long as we don't
|
||||
// have more nodes to explore.
|
||||
for len(nextLevel) > 0 {
|
||||
level++
|
||||
|
||||
// We swap the queues for efficient memory management.
|
||||
thisLevel, nextLevel = nextLevel, thisLevel
|
||||
|
||||
// Visit all neighboring nodes of the level and mark them as
|
||||
// seen if they were not discovered before.
|
||||
for _, thisNode := range thisLevel {
|
||||
for _, neighbor := range graph.Adj[thisNode] {
|
||||
_, ok := seen[neighbor]
|
||||
if !ok {
|
||||
nextLevel = append(nextLevel, neighbor)
|
||||
seen[neighbor] = level
|
||||
}
|
||||
|
||||
// If we have seen all nodes, we return early.
|
||||
if len(seen) == graphOrder {
|
||||
return seen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty the queue to be used in the next level.
|
||||
thisLevel = thisLevel[:0:cap(thisLevel)]
|
||||
}
|
||||
|
||||
return seen
|
||||
}
|
||||
|
||||
// nodeEccentricity calculates the eccentricity (longest shortest path to all
|
||||
// other nodes) of a node.
|
||||
func (graph *SimpleGraph) nodeEccentricity(node int) uint32 {
|
||||
pathLengths := graph.shortestPathLengths(node)
|
||||
return maxVal(pathLengths)
|
||||
}
|
||||
|
||||
// nodeEccentricities calculates the eccentricities for the given nodes.
|
||||
func (graph *SimpleGraph) nodeEccentricities(nodes []int) map[int]uint32 {
|
||||
eccentricities := make(map[int]uint32, len(graph.Adj))
|
||||
for _, node := range nodes {
|
||||
eccentricities[node] = graph.nodeEccentricity(node)
|
||||
}
|
||||
return eccentricities
|
||||
}
|
||||
|
||||
// Diameter returns the maximal eccentricity (longest shortest path
|
||||
// between any node pair) in the graph.
|
||||
//
|
||||
// Note: This method is exact but expensive, use DiameterRadialCutoff instead.
|
||||
func (graph *SimpleGraph) Diameter() uint32 {
|
||||
nodes := make([]int, len(graph.Adj))
|
||||
for a := range nodes {
|
||||
nodes[a] = a
|
||||
}
|
||||
eccentricities := graph.nodeEccentricities(nodes)
|
||||
return maxVal(eccentricities)
|
||||
}
|
||||
|
||||
// DiameterRadialCutoff is a method to efficiently evaluate the diameter of a
|
||||
// graph. The highest-degree node is usually central in the graph. We can
|
||||
// determine its eccentricity (shortest-longest path length to any other node)
|
||||
// and use it as an approximation for the radius of the network. We then
|
||||
// use this radius to compute a cutoff. All the nodes within a distance of the
|
||||
// cutoff are discarded, as they represent the inside of the graph. We then
|
||||
// loop over all outer nodes and determine their eccentricities, from which we
|
||||
// get the diameter.
|
||||
func (graph *SimpleGraph) DiameterRadialCutoff() uint32 {
|
||||
// Determine the reference node as the node with the highest degree.
|
||||
nodeMaxDegree, _ := graph.nodeMaxDegree()
|
||||
|
||||
distances := graph.shortestPathLengths(nodeMaxDegree)
|
||||
eccentricityMaxDegreeNode := maxVal(distances)
|
||||
|
||||
// We use the eccentricity to define a cutoff for the interior of the
|
||||
// graph from the reference node.
|
||||
cutoff := uint32(float32(eccentricityMaxDegreeNode) * diameterCutoff)
|
||||
log.Debugf("Cutoff radius is %d hops (max-degree node's "+
|
||||
"eccentricity is %d)", cutoff, eccentricityMaxDegreeNode)
|
||||
|
||||
// Remove the nodes that are close to the reference node.
|
||||
var nodes []int
|
||||
for node, distance := range distances {
|
||||
if distance > cutoff {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
}
|
||||
log.Debugf("Evaluated nodes: %d, discarded nodes %d",
|
||||
len(nodes), len(graph.Adj)-len(nodes))
|
||||
|
||||
// Compute the diameter of the remaining nodes.
|
||||
eccentricities := graph.nodeEccentricities(nodes)
|
||||
return maxVal(eccentricities)
|
||||
}
|
||||
|
94
autopilot/simple_graph_test.go
Normal file
94
autopilot/simple_graph_test.go
Normal file
@ -0,0 +1,94 @@
|
||||
package autopilot
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
testShortestPathLengths = map[int]uint32{
|
||||
0: 0,
|
||||
1: 1,
|
||||
2: 1,
|
||||
3: 1,
|
||||
4: 2,
|
||||
5: 2,
|
||||
6: 3,
|
||||
7: 3,
|
||||
8: 4,
|
||||
}
|
||||
testNodeEccentricities = map[int]uint32{
|
||||
0: 4,
|
||||
1: 5,
|
||||
2: 4,
|
||||
3: 3,
|
||||
4: 3,
|
||||
5: 3,
|
||||
6: 4,
|
||||
7: 4,
|
||||
8: 5,
|
||||
}
|
||||
)
|
||||
|
||||
// NewTestSimpleGraph is a helper that generates a SimpleGraph from a test
|
||||
// graph description.
|
||||
// Assumes that the graph description is internally consistent, i.e. edges are
|
||||
// not repeatedly defined.
|
||||
func NewTestSimpleGraph(graph testGraphDesc) SimpleGraph {
|
||||
// We convert the test graph description into an adjacency list.
|
||||
adjList := make([][]int, graph.nodes)
|
||||
for node, neighbors := range graph.edges {
|
||||
for _, neighbor := range neighbors {
|
||||
adjList[node] = append(adjList[node], neighbor)
|
||||
adjList[neighbor] = append(adjList[neighbor], node)
|
||||
}
|
||||
}
|
||||
|
||||
return SimpleGraph{Adj: adjList}
|
||||
}
|
||||
|
||||
func TestShortestPathLengths(t *testing.T) {
|
||||
simpleGraph := NewTestSimpleGraph(centralityTestGraph)
|
||||
|
||||
// Test the shortest path lengths from node 0 to all other nodes.
|
||||
shortestPathLengths := simpleGraph.shortestPathLengths(0)
|
||||
require.Equal(t, shortestPathLengths, testShortestPathLengths)
|
||||
}
|
||||
|
||||
func TestEccentricities(t *testing.T) {
|
||||
simpleGraph := NewTestSimpleGraph(centralityTestGraph)
|
||||
|
||||
// Test the node eccentricities for all nodes.
|
||||
nodes := make([]int, len(simpleGraph.Adj))
|
||||
for a := range nodes {
|
||||
nodes[a] = a
|
||||
}
|
||||
nodeEccentricities := simpleGraph.nodeEccentricities(nodes)
|
||||
require.Equal(t, nodeEccentricities, testNodeEccentricities)
|
||||
}
|
||||
|
||||
func TestDiameterExact(t *testing.T) {
|
||||
simpleGraph := NewTestSimpleGraph(centralityTestGraph)
|
||||
|
||||
// Test the diameter in a brute-force manner.
|
||||
diameter := simpleGraph.Diameter()
|
||||
require.Equal(t, uint32(5), diameter)
|
||||
}
|
||||
|
||||
func TestDiameterCutoff(t *testing.T) {
|
||||
simpleGraph := NewTestSimpleGraph(centralityTestGraph)
|
||||
|
||||
// Test the diameter by cutting out the inside of the graph.
|
||||
diameter := simpleGraph.DiameterRadialCutoff()
|
||||
require.Equal(t, uint32(5), diameter)
|
||||
}
|
||||
|
||||
func BenchmarkShortestPathOpt(b *testing.B) {
|
||||
// TODO: a method that generates a huge graph is needed
|
||||
simpleGraph := NewTestSimpleGraph(centralityTestGraph)
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
_ = simpleGraph.shortestPathLengths(0)
|
||||
}
|
||||
}
|
@ -75,6 +75,8 @@
|
||||
`remote_balance`](https://github.com/lightningnetwork/lnd/pull/5931) in
|
||||
`pending_force_closing_channels` under `pendingchannels` whereas before was
|
||||
empty(zero).
|
||||
* The graph's [diameter is calculated](https://github.com/lightningnetwork/lnd/pull/6066)
|
||||
and added to the `getnetworkinfo` output.
|
||||
|
||||
* [Add dev only RPC subserver and the devrpc.ImportGraph
|
||||
call](https://github.com/lightningnetwork/lnd/pull/6149)
|
||||
@ -131,6 +133,7 @@ gRPC performance metrics (latency to process `GetInfo`, etc)](https://github.com
|
||||
* 3nprob
|
||||
* Andreas Schjønhaug
|
||||
* asvdf
|
||||
* bitromortac
|
||||
* BTCparadigm
|
||||
* Carla Kirk-Cohen
|
||||
* Carsten Otto
|
||||
@ -151,4 +154,4 @@ gRPC performance metrics (latency to process `GetInfo`, etc)](https://github.com
|
||||
* Thebora Kompanioni
|
||||
* Torkel Rogstad
|
||||
* Vsevolod Kaganovych
|
||||
* Yong Yu
|
||||
* Yong Yu
|
12
rpcserver.go
12
rpcserver.go
@ -5904,10 +5904,20 @@ func (r *rpcServer) GetNetworkInfo(ctx context.Context,
|
||||
minChannelSize = 0
|
||||
}
|
||||
|
||||
// TODO(roasbeef): graph diameter
|
||||
// Graph diameter.
|
||||
channelGraph := autopilot.ChannelGraphFromDatabase(graph)
|
||||
simpleGraph, err := autopilot.NewSimpleGraph(channelGraph)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start := time.Now()
|
||||
diameter := simpleGraph.DiameterRadialCutoff()
|
||||
rpcsLog.Infof("elapsed time for diameter (%d) calculation: %v", diameter,
|
||||
time.Since(start))
|
||||
|
||||
// TODO(roasbeef): also add oldest channel?
|
||||
netInfo := &lnrpc.NetworkInfo{
|
||||
GraphDiameter: diameter,
|
||||
MaxOutDegree: maxChanOut,
|
||||
AvgOutDegree: float64(2*numChannels) / float64(numNodes),
|
||||
NumNodes: numNodes,
|
||||
|
Loading…
Reference in New Issue
Block a user