mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 18:10:34 +01:00
ed40eddafd
This fixes an issue where the diameter calculation would crash if the graph is empty.
236 lines
7.2 KiB
Go
236 lines
7.2 KiB
Go
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 {
|
|
// Nodes is a map from node index to NodeID.
|
|
Nodes []NodeID
|
|
|
|
// Adj stores nodes and neighbors in an adjacency list.
|
|
Adj [][]int
|
|
}
|
|
|
|
// NewSimpleGraph creates a simplified graph from the current channel graph.
|
|
// Returns an error if the channel graph iteration fails due to underlying
|
|
// failure.
|
|
func NewSimpleGraph(g ChannelGraph) (*SimpleGraph, error) {
|
|
nodes := make(map[NodeID]int)
|
|
adj := make(map[int][]int)
|
|
nextIndex := 0
|
|
|
|
// getNodeIndex returns the integer index of the passed node.
|
|
// The returned index is then used to create a simplifed adjacency list
|
|
// where each node is identified by its index instead of its pubkey, and
|
|
// also to create a mapping from node index to node pubkey.
|
|
getNodeIndex := func(node Node) int {
|
|
key := NodeID(node.PubKey())
|
|
nodeIndex, ok := nodes[key]
|
|
|
|
if !ok {
|
|
nodes[key] = nextIndex
|
|
nodeIndex = nextIndex
|
|
nextIndex++
|
|
}
|
|
|
|
return nodeIndex
|
|
}
|
|
|
|
// Iterate over each node and each channel and update the adj and the node
|
|
// index.
|
|
err := g.ForEachNode(func(node Node) error {
|
|
u := getNodeIndex(node)
|
|
|
|
return node.ForEachChannel(func(edge ChannelEdge) error {
|
|
v := getNodeIndex(edge.Peer)
|
|
|
|
adj[u] = append(adj[u], v)
|
|
return nil
|
|
})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
graph := &SimpleGraph{
|
|
Nodes: make([]NodeID, len(nodes)),
|
|
Adj: make([][]int, len(nodes)),
|
|
}
|
|
|
|
// Fill the adj and the node index to node pubkey mapping.
|
|
for nodeID, nodeIndex := range nodes {
|
|
graph.Adj[nodeIndex] = adj[nodeIndex]
|
|
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)
|
|
|
|
// Abort if we have an empty graph.
|
|
if len(graph.Adj) == 0 {
|
|
return seen
|
|
}
|
|
|
|
// 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)
|
|
}
|