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) }