routing: modify FindRoutes to take a max number of routes to return

This commit is contained in:
Olaoluwa Osuntokun 2018-02-12 16:19:46 -08:00
parent 33ba60ce66
commit 8a29c01354
No known key found for this signature in database
GPG Key ID: 964EA263DD637C21
2 changed files with 74 additions and 55 deletions

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

@ -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()