routing: add findPaths function whcih implements Yen's Algorithm

With this commit we make our routing more robust by looking for the
k-shortest paths rather than a single shortest path and using that
unconditionally. In a nut shell Yen’s algorithm does the following:
   * Find the shortest path from the source to the destination
   * From k=1…K, walk the kth shortest path and find possible
divergence from each node in the path

Our version of Yen’s implemented is slightly modified, rather than
actually increasing the edge weights or remove vertexes from the graph,
we instead use two black-lists: one for edges and the other for
vertexes. Our modified version of Djikstra’s algorithm is then made
aware of these black lists in order to more efficiently implement the
path iteration via spur and root node.
This commit is contained in:
Olaoluwa Osuntokun 2017-03-20 18:22:04 -07:00
parent 56d27f2d58
commit aaa04bb2e5
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2
2 changed files with 196 additions and 0 deletions

View File

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

View File

@ -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.