diff --git a/routing/pathfind.go b/routing/pathfind.go index f2941e2ca..1e442d43c 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -375,4 +375,141 @@ func findPath(graph *channeldb.ChannelGraph, sourceNode *channeldb.LightningNode return pathEdges, nil } + +// findPaths implements a k-shortest paths algorithm to find all the reachable +// paths between the passed source and target. The algorithm will continue to +// traverse the graph until all possible candidate paths have been depleted. +// This function implements a modified version of Yen's. To find each path +// itself, we utilize our modified version of Dijkstra's found above. When +// examining possible spur and root paths, rather than removing edges or +// vertexes from the graph, we instead utilize a vertex+edge black-list that +// will be ignored by our modified Dijkstra's algorithm. With this approach, we +// make our inner path finding algorithm aware of our k-shortest paths +// algorithm, rather than attempting to use an unmodified path finding +// algorithm in a block box manner. +func findPaths(graph *channeldb.ChannelGraph, source *channeldb.LightningNode, + target *btcec.PublicKey, amt btcutil.Amount) ([][]*ChannelHop, error) { + + ignoredEdges := make(map[uint64]struct{}) + ignoredVertexes := make(map[vertex]struct{}) + + // TODO(roasbeef): modifying ordering within heap to eliminate final + // sorting step? + var ( + shortestPaths [][]*ChannelHop + candidatePaths pathHeap + ) + + // First we'll find a single shortest path from the source (our + // selfNode) to the target destination that's capable of carrying amt + // satoshis along the path before fees are calculated. + startingPath, err := findPath(graph, source, target, + ignoredVertexes, ignoredEdges, amt) + if err != nil { + log.Errorf("Unable to find path: %v", err) + return nil, err + } + + // Manually insert a "self" edge emanating from ourselves. This + // self-edge is required in order for the path finding algorithm to + // function properly. + firstPath := make([]*ChannelHop, 0, len(startingPath)+1) + firstPath = append(firstPath, &ChannelHop{ + ChannelEdgePolicy: &channeldb.ChannelEdgePolicy{ + Node: source, + }, + }) + firstPath = append(firstPath, startingPath...) + + shortestPaths = append(shortestPaths, firstPath) + + source.PubKey.Curve = nil + + // While we still have candidate paths to explore we'll keep exploring + // the sub-graphs created to find the next k-th shortest path. + for k := 1; k == 1 || candidatePaths.Len() != 0; k++ { + prevShortest := shortestPaths[k-1] + + // We'll examine each edge in the previous iteration's shortest + // path in order to find path deviations from each node in the + // path. + for i := 0; i < len(prevShortest)-1; i++ { + // These two maps will mark the edges and vertexes + // we'll exclude from the next path finding attempt. + // These are required to ensure the paths are unique + // and loopless. + ignoredEdges = make(map[uint64]struct{}) + ignoredVertexes = make(map[vertex]struct{}) + + // Our spur node is the i-th node in the prior shortest + // path, and our root path will be all nodes in the + // path leading up to our spurNode. + spurNode := prevShortest[i].Node + rootPath := prevShortest[:i+1] + + // Before we kickoff our next path finding iteration, + // we'll find all the edges we need to ignore in this + // next round. + for _, path := range shortestPaths { + // If our current rootPath is a prefix of this + // shortest path, then we'll remove the ege + // directly _after_ our spur node from the + // graph so we don't repeat paths. + if isSamePath(rootPath, path[:i+1]) { + ignoredEdges[path[i+1].ChannelID] = struct{}{} + } + } + + // Next we'll remove all entries in the root path that + // aren't the current spur node from the graph. + for _, hop := range rootPath { + node := hop.Node.PubKey + if node.IsEqual(spurNode.PubKey) { + continue + } + + ignoredVertexes[newVertex(node)] = struct{}{} + } + + // With the edges that are part of our root path, and + // the vertexes (other than the spur path) within the + // root path removed, we'll attempt to find another + // shortest path from the spur node to the destination. + spurPath, err := findPath(graph, spurNode, target, + ignoredVertexes, ignoredEdges, amt) + + // If we weren't able to find a path, we'll continue to + // the next round. + if err == ErrNoPathFound { + continue + } else if err != nil { + return nil, err + } + + // Create the new combined path by concatenating the + // rootPath to the spurPath. + newPath := append(rootPath, spurPath...) + + // We'll now add this newPath to the heap of candidate + // shortest paths. + heap.Push(&candidatePaths, path{ + dist: len(newPath), + hops: newPath, + }) + } + + // If our min-heap of candidate paths is empty, then we can + // exit early. + if candidatePaths.Len() == 0 { + break + } + + // To conclude this latest iteration, we'll take the shortest + // path in our set of candidate paths and add it to our + // shortestPaths list as the *next* shortest path. + nextShortestPath := heap.Pop(&candidatePaths).(path).hops + shortestPaths = append(shortestPaths, nextShortestPath) + } + + return shortestPaths, nil } diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 91aca3d0d..5f535e4ec 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -375,6 +375,65 @@ func TestBasicGraphPathFinding(t *testing.T) { } } +func TestKShortestPathFinding(t *testing.T) { + graph, cleanUp, aliases, err := parseTestGraph(basicGraphFilePath) + defer cleanUp() + if err != nil { + t.Fatalf("unable to create graph: %v", err) + } + + sourceNode, err := graph.SourceNode() + if err != nil { + t.Fatalf("unable to fetch source node: %v", err) + } + + // In this test we'd like to ensure that our algoirthm to find the + // k-shortest paths from a given source node to any destination node + // works as exepcted. + + // In our basic_graph.json, there exist two paths from roasbeef to luo + // ji. Our algorithm should properly find both paths, and also rank + // them in order of their total "distance". + + const paymentAmt = btcutil.Amount(100) + target := aliases["luoji"] + paths, err := findPaths(graph, sourceNode, target, paymentAmt) + if err != nil { + t.Fatalf("unable to find paths between roasbeef and "+ + "luo ji: %v", err) + } + + // The algorithm should've found two paths from roasbeef to luo ji. + if len(paths) != 2 { + t.Fatalf("two path shouldn't been found, instead %v were", + len(paths)) + } + + // Additinoally, the total hop length of the first path returned should + // be _less_ than that of the second path returned. + if len(paths[0]) > len(paths[1]) { + t.Fatalf("paths found not ordered properly") + } + + // Finally, we'll assert the exact expected ordering of both paths + // found. + assertExpectedPath := func(path []*ChannelHop, nodeAliases ...string) { + for i, hop := range path { + if hop.Node.Alias != nodeAliases[i] { + t.Fatalf("expected %v to be pos #%v in hop, "+ + "instead %v was", nodeAliases[i], i, + hop.Node.Alias) + } + } + } + + // The first route should be a direct route to luo ji. + assertExpectedPath(paths[0], "roasbeef", "luoji") + + // The second route should be a route to luo ji via satoshi. + assertExpectedPath(paths[1], "roasbeef", "satoshi", "luoji") +} + func TestNewRoutePathTooLong(t *testing.T) { // Ensure that potential paths which are over the maximum hop-limit are // rejected.