From 8a29c01354e8867fc8548eaffd25c0b6d250f3f6 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 12 Feb 2018 16:19:46 -0800 Subject: [PATCH] routing: modify FindRoutes to take a max number of routes to return --- routing/pathfind_test.go | 4 +- routing/router.go | 125 ++++++++++++++++++++++----------------- 2 files changed, 74 insertions(+), 55 deletions(-) diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index b77c08ad..21db48d5 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -778,7 +778,7 @@ func TestPathFindSpecExample(t *testing.T) { // Query for a route of 4,999,999 mSAT to carol. carol := ctx.aliases["C"] const amt lnwire.MilliSatoshi = 4999999 - routes, err := ctx.router.FindRoutes(carol, amt) + routes, err := ctx.router.FindRoutes(carol, amt, 100) if err != nil { t.Fatalf("unable to find route: %v", err) } @@ -838,7 +838,7 @@ func TestPathFindSpecExample(t *testing.T) { // We'll now request a route from A -> B -> C. ctx.router.routeCache = make(map[routeTuple][]*Route) - routes, err = ctx.router.FindRoutes(carol, amt) + routes, err = ctx.router.FindRoutes(carol, amt, 100) if err != nil { t.Fatalf("unable to find routes: %v", err) } diff --git a/routing/router.go b/routing/router.go index 2663755a..ff7fde75 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1157,7 +1157,6 @@ func pruneNodeFromRoutes(routes []*Route, skipNode Vertex) []*Route { // pruneChannelFromRoutes accepts a set of routes, and returns a new set of // routes with the target channel filtered out. func pruneChannelFromRoutes(routes []*Route, skipChan uint64) []*Route { - prunedRoutes := make([]*Route, 0, len(routes)) for _, route := range routes { if route.containsChannel(skipChan) { @@ -1173,17 +1172,70 @@ func pruneChannelFromRoutes(routes []*Route, skipChan uint64) []*Route { return prunedRoutes } -// FindRoutes attempts to query the ChannelRouter for the all available paths -// to a particular target destination which is able to send `amt` after -// factoring in channel capacities and cumulative fees along each route route. -// To find all eligible paths, we use a modified version of Yen's algorithm -// which itself uses a modified version of Dijkstra's algorithm within its -// inner loop. Once we have a set of candidate routes, we calculate the -// required fee and time lock values running backwards along the route. The +// pathsToFeeSortedRoutes takes a set of paths, and returns a corresponding set +// of of routes. A route differs from a path in that it has full time-lock and +// fee information attached. The set of routes return ed may be less than the +// initial set of paths as it's possible we drop a route if it can't handle the +// total payment flow after fees are calculated. +func pathsToFeeSortedRoutes(source Vertex, paths [][]*ChannelHop, finalCLTVDelta uint16, + amt lnwire.MilliSatoshi, currentHeight uint32) ([]*Route, error) { + + validRoutes := make([]*Route, 0, len(paths)) + for _, path := range paths { + // Attempt to make the path into a route. We snip off the first + // hop in the path as it contains a "self-hop" that is inserted + // by our KSP algorithm. + route, err := newRoute( + amt, source, path[1:], currentHeight, finalCLTVDelta, + ) + if err != nil { + // TODO(roasbeef): report straw breaking edge? + continue + } + + // If the path as enough total flow to support the computed + // route, then we'll add it to our set of valid routes. + validRoutes = append(validRoutes, route) + } + + // If all our perspective routes were eliminating during the transition + // from path to route, then we'll return an error to the caller + if len(validRoutes) == 0 { + return nil, newErr(ErrNoPathFound, "unable to find a path to "+ + "destination") + } + + // Finally, we'll sort the set of validate routes to optimize for + // lowest total fees, using the required time-lock within the route as + // a tie-breaker. + sort.Slice(validRoutes, func(i, j int) bool { + // To make this decision we first check if the total fees + // required for both routes are equal. If so, then we'll let + // the total time lock be the tie breaker. Otherwise, we'll put + // the route with the lowest total fees first. + if validRoutes[i].TotalFees == validRoutes[j].TotalFees { + timeLockI := validRoutes[i].TotalTimeLock + timeLockJ := validRoutes[j].TotalTimeLock + return timeLockI < timeLockJ + } + + return validRoutes[i].TotalFees < validRoutes[j].TotalFees + }) + + return validRoutes, nil +} + +// FindRoutes attempts to query the ChannelRouter for a bounded number +// available paths to a particular target destination which is able to send +// `amt` after factoring in channel capacities and cumulative fees along each +// route route. To `numPaths eligible paths, we use a modified version of +// Yen's algorithm which itself uses a modified version of Dijkstra's algorithm +// within its inner loop. Once we have a set of candidate routes, we calculate +// the required fee and time lock values running backwards along the route. The // route that will be ranked the highest is the one with the lowest cumulative // fee along the route. func (r *ChannelRouter) FindRoutes(target *btcec.PublicKey, - amt lnwire.MilliSatoshi, finalExpiry ...uint16) ([]*Route, error) { + amt lnwire.MilliSatoshi, numPaths uint32, finalExpiry ...uint16) ([]*Route, error) { var finalCLTVDelta uint16 if len(finalExpiry) == 0 { @@ -1192,8 +1244,6 @@ func (r *ChannelRouter) FindRoutes(target *btcec.PublicKey, finalCLTVDelta = finalExpiry[0] } - // TODO(roasbeef): make num routes a param - dest := target.SerializeCompressed() log.Debugf("Searching for path to %x, sending %v", dest, amt) @@ -1242,8 +1292,9 @@ func (r *ChannelRouter) FindRoutes(target *btcec.PublicKey, // Now that we know the destination is reachable within the graph, // we'll execute our KSP algorithm to find the k-shortest paths from // our source to the destination. - shortestPaths, err := findPaths(tx, r.cfg.Graph, r.selfNode, target, - amt) + shortestPaths, err := findPaths( + tx, r.cfg.Graph, r.selfNode, target, amt, numPaths, + ) if err != nil { tx.Rollback() return nil, err @@ -1256,55 +1307,23 @@ func (r *ChannelRouter) FindRoutes(target *btcec.PublicKey, // each path. During this process, some paths may be discarded if they // aren't able to support the total satoshis flow once fees have been // factored in. - validRoutes := make([]*Route, 0, len(shortestPaths)) sourceVertex := Vertex(r.selfNode.PubKeyBytes) - for _, path := range shortestPaths { - // Attempt to make the path into a route. We snip off the first - // hop in the path as it contains a "self-hop" that is inserted - // by our KSP algorithm. - route, err := newRoute(amt, sourceVertex, path[1:], - uint32(currentHeight), finalCLTVDelta) - if err != nil { - continue - } - - // If the path as enough total flow to support the computed - // route, then we'll add it to our set of valid routes. - validRoutes = append(validRoutes, route) + validRoutes, err := pathsToFeeSortedRoutes( + sourceVertex, shortestPaths, finalCLTVDelta, amt, + uint32(currentHeight), + ) + if err != nil { + return nil, err } - // If all our perspective routes were eliminating during the transition - // from path to route, then we'll return an error to the caller - if len(validRoutes) == 0 { - return nil, newErr(ErrNoPathFound, "unable to find a path to "+ - "destination") - } - - // Finally, we'll sort the set of validate routes to optimize for - // lowest total fees, using the required time-lock within the - // route as a tie-breaker. - sort.Slice(validRoutes, func(i, j int) bool { - // To make this decision we first check if the total fees - // required for both routes are equal. If so, then we'll let - // the total time lock be the tie breaker. Otherwise, we'll - // put the route with the lowest total fees first. - if validRoutes[i].TotalFees == validRoutes[j].TotalFees { - timeLockI := validRoutes[i].TotalTimeLock - timeLockJ := validRoutes[j].TotalTimeLock - return timeLockI < timeLockJ - } - - return validRoutes[i].TotalFees < validRoutes[j].TotalFees - }) - go log.Tracef("Obtained %v paths sending %v to %x: %v", len(validRoutes), amt, dest, newLogClosure(func() string { return spew.Sdump(validRoutes) }), ) - // Populate the cache with this set of fresh routes so we can - // reuse them in the future. + // Populate the cache with this set of fresh routes so we can reuse + // them in the future. r.routeCacheMtx.Lock() r.routeCache[rt] = validRoutes r.routeCacheMtx.Unlock()