mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-26 01:33:02 +01:00
lnrpc+routing: remove k shortest path finding
This commit is contained in:
parent
c4415f0400
commit
7a5bd29a69
@ -2977,11 +2977,6 @@ var queryRoutesCommand = cli.Command{
|
||||
Usage: "percentage of the payment's amount used as the " +
|
||||
"maximum fee allowed when sending the payment",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "num_max_routes",
|
||||
Usage: "the max number of routes to be returned",
|
||||
Value: 10,
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "final_cltv_delta",
|
||||
Usage: "(optional) number of blocks the last hop has to reveal " +
|
||||
@ -3035,7 +3030,6 @@ func queryRoutes(ctx *cli.Context) error {
|
||||
PubKey: dest,
|
||||
Amt: amt,
|
||||
FeeLimit: feeLimit,
|
||||
NumRoutes: int32(ctx.Int("num_max_routes")),
|
||||
FinalCltvDelta: int32(ctx.Int("final_cltv_delta")),
|
||||
}
|
||||
|
||||
|
25
lnd_test.go
25
lnd_test.go
@ -1460,7 +1460,6 @@ func testUpdateChannelPolicy(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
routesReq := &lnrpc.QueryRoutesRequest{
|
||||
PubKey: carol.PubKeyStr,
|
||||
Amt: int64(payAmt),
|
||||
NumRoutes: 1,
|
||||
FinalCltvDelta: defaultTimeLockDelta,
|
||||
}
|
||||
|
||||
@ -4270,7 +4269,6 @@ func testSingleHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
routesReq := &lnrpc.QueryRoutesRequest{
|
||||
PubKey: net.Bob.PubKeyStr,
|
||||
Amt: paymentAmt,
|
||||
NumRoutes: 1,
|
||||
FinalCltvDelta: defaultBitcoinTimeLockDelta,
|
||||
}
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
@ -4442,7 +4440,6 @@ func testMultiHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
routesReq := &lnrpc.QueryRoutesRequest{
|
||||
PubKey: carol.PubKeyStr,
|
||||
Amt: paymentAmt,
|
||||
NumRoutes: 1,
|
||||
FinalCltvDelta: defaultBitcoinTimeLockDelta,
|
||||
}
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
@ -4603,9 +4600,8 @@ func testSendToRouteErrorPropagation(net *lntest.NetworkHarness, t *harnessTest)
|
||||
// Query routes from Carol to Charlie which will be an invalid route
|
||||
// for Alice -> Bob.
|
||||
fakeReq := &lnrpc.QueryRoutesRequest{
|
||||
PubKey: charlie.PubKeyStr,
|
||||
Amt: int64(1),
|
||||
NumRoutes: 1,
|
||||
PubKey: charlie.PubKeyStr,
|
||||
Amt: int64(1),
|
||||
}
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
fakeRoute, err := carol.QueryRoutes(ctxt, fakeReq)
|
||||
@ -12342,9 +12338,8 @@ func testQueryRoutes(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
// Query for routes to pay from Alice to Dave.
|
||||
const paymentAmt = 1000
|
||||
routesReq := &lnrpc.QueryRoutesRequest{
|
||||
PubKey: dave.PubKeyStr,
|
||||
Amt: paymentAmt,
|
||||
NumRoutes: 1,
|
||||
PubKey: dave.PubKeyStr,
|
||||
Amt: paymentAmt,
|
||||
}
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
routesRes, err := net.Alice.QueryRoutes(ctxt, routesReq)
|
||||
@ -12646,10 +12641,9 @@ func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
// payments.
|
||||
testFeeCutoff := func(feeLimit *lnrpc.FeeLimit) {
|
||||
queryRoutesReq := &lnrpc.QueryRoutesRequest{
|
||||
PubKey: dave.PubKeyStr,
|
||||
Amt: paymentAmt,
|
||||
FeeLimit: feeLimit,
|
||||
NumRoutes: 2,
|
||||
PubKey: dave.PubKeyStr,
|
||||
Amt: paymentAmt,
|
||||
FeeLimit: feeLimit,
|
||||
}
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
routesResp, err := net.Alice.QueryRoutes(ctxt, queryRoutesReq)
|
||||
@ -12657,11 +12651,6 @@ func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
t.Fatalf("unable to get routes: %v", err)
|
||||
}
|
||||
|
||||
if len(routesResp.Routes) != 1 {
|
||||
t.Fatalf("expected one route, got %d",
|
||||
len(routesResp.Routes))
|
||||
}
|
||||
|
||||
checkRoute(routesResp.Routes[0])
|
||||
|
||||
invoice := &lnrpc.Invoice{Value: paymentAmt}
|
||||
|
@ -28,10 +28,9 @@ type RouterBackend struct {
|
||||
|
||||
// FindRoutes is a closure that abstracts away how we locate/query for
|
||||
// routes.
|
||||
FindRoutes func(source, target route.Vertex,
|
||||
FindRoute func(source, target route.Vertex,
|
||||
amt lnwire.MilliSatoshi, restrictions *routing.RestrictParams,
|
||||
numPaths uint32, finalExpiry ...uint16) (
|
||||
[]*route.Route, error)
|
||||
finalExpiry ...uint16) (*route.Route, error)
|
||||
}
|
||||
|
||||
// QueryRoutes attempts to query the daemons' Channel Router for a possible
|
||||
@ -122,53 +121,35 @@ func (r *RouterBackend) QueryRoutes(ctx context.Context,
|
||||
IgnoredEdges: ignoredEdges,
|
||||
}
|
||||
|
||||
// numRoutes will default to 10 if not specified explicitly.
|
||||
numRoutesIn := uint32(in.NumRoutes)
|
||||
if numRoutesIn == 0 {
|
||||
numRoutesIn = 10
|
||||
}
|
||||
|
||||
// Query the channel router for a possible path to the destination that
|
||||
// can carry `in.Amt` satoshis _including_ the total fee required on
|
||||
// the route.
|
||||
var (
|
||||
routes []*route.Route
|
||||
route *route.Route
|
||||
findErr error
|
||||
)
|
||||
|
||||
if in.FinalCltvDelta == 0 {
|
||||
routes, findErr = r.FindRoutes(
|
||||
route, findErr = r.FindRoute(
|
||||
sourcePubKey, targetPubKey, amtMSat, restrictions,
|
||||
numRoutesIn,
|
||||
)
|
||||
} else {
|
||||
routes, findErr = r.FindRoutes(
|
||||
route, findErr = r.FindRoute(
|
||||
sourcePubKey, targetPubKey, amtMSat, restrictions,
|
||||
numRoutesIn, uint16(in.FinalCltvDelta),
|
||||
uint16(in.FinalCltvDelta),
|
||||
)
|
||||
}
|
||||
if findErr != nil {
|
||||
return nil, findErr
|
||||
}
|
||||
|
||||
// As the number of returned routes can be less than the number of
|
||||
// requested routes, we'll clamp down the length of the response to the
|
||||
// minimum of the two.
|
||||
numRoutes := uint32(len(routes))
|
||||
if numRoutesIn < numRoutes {
|
||||
numRoutes = numRoutesIn
|
||||
}
|
||||
|
||||
// For each valid route, we'll convert the result into the format
|
||||
// required by the RPC system.
|
||||
|
||||
rpcRoute := r.MarshallRoute(route)
|
||||
|
||||
routeResp := &lnrpc.QueryRoutesResponse{
|
||||
Routes: make([]*lnrpc.Route, 0, in.NumRoutes),
|
||||
}
|
||||
for i := uint32(0); i < numRoutes; i++ {
|
||||
routeResp.Routes = append(
|
||||
routeResp.Routes,
|
||||
r.MarshallRoute(routes[i]),
|
||||
)
|
||||
Routes: []*lnrpc.Route{rpcRoute},
|
||||
}
|
||||
|
||||
return routeResp, nil
|
||||
|
@ -42,7 +42,6 @@ func TestQueryRoutes(t *testing.T) {
|
||||
request := &lnrpc.QueryRoutesRequest{
|
||||
PubKey: destKey,
|
||||
Amt: 100000,
|
||||
NumRoutes: 1,
|
||||
FinalCltvDelta: 100,
|
||||
FeeLimit: &lnrpc.FeeLimit{
|
||||
Limit: &lnrpc.FeeLimit_Fixed{
|
||||
@ -58,19 +57,14 @@ func TestQueryRoutes(t *testing.T) {
|
||||
|
||||
rt := &route.Route{}
|
||||
|
||||
findRoutes := func(source, target route.Vertex,
|
||||
findRoute := func(source, target route.Vertex,
|
||||
amt lnwire.MilliSatoshi, restrictions *routing.RestrictParams,
|
||||
numPaths uint32, finalExpiry ...uint16) (
|
||||
[]*route.Route, error) {
|
||||
finalExpiry ...uint16) (*route.Route, error) {
|
||||
|
||||
if int64(amt) != request.Amt*1000 {
|
||||
t.Fatal("unexpected amount")
|
||||
}
|
||||
|
||||
if numPaths != 1 {
|
||||
t.Fatal("unexpected number of routes")
|
||||
}
|
||||
|
||||
if source != sourceKey {
|
||||
t.Fatal("unexpected source key")
|
||||
}
|
||||
@ -101,14 +95,12 @@ func TestQueryRoutes(t *testing.T) {
|
||||
t.Fatal("unexpected ignored node")
|
||||
}
|
||||
|
||||
return []*route.Route{
|
||||
rt,
|
||||
}, nil
|
||||
return rt, nil
|
||||
}
|
||||
|
||||
backend := &RouterBackend{
|
||||
MaxPaymentMSat: lnwire.NewMSatFromSatoshis(1000000),
|
||||
FindRoutes: findRoutes,
|
||||
FindRoute: findRoute,
|
||||
SelfNode: route.Vertex{1, 2, 3},
|
||||
FetchChannelCapacity: func(chanID uint64) (
|
||||
btcutil.Amount, error) {
|
||||
|
@ -247,22 +247,18 @@ func (s *Server) EstimateRouteFee(ctx context.Context,
|
||||
|
||||
// Finally, we'll query for a route to the destination that can carry
|
||||
// that target amount, we'll only request a single route.
|
||||
routes, err := s.cfg.Router.FindRoutes(
|
||||
route, err := s.cfg.Router.FindRoute(
|
||||
s.cfg.RouterBackend.SelfNode, destNode, amtMsat,
|
||||
&routing.RestrictParams{
|
||||
FeeLimit: feeLimit,
|
||||
}, 1,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(routes) == 0 {
|
||||
return nil, fmt.Errorf("unable to find route to dest: %v", err)
|
||||
}
|
||||
|
||||
return &RouteFeeResponse{
|
||||
RoutingFeeMsat: int64(routes[0].TotalFees),
|
||||
TimeLockDelay: int64(routes[0].TotalTimeLock),
|
||||
RoutingFeeMsat: int64(route.TotalFees),
|
||||
TimeLockDelay: int64(route.TotalTimeLock),
|
||||
}, nil
|
||||
}
|
||||
|
1259
lnrpc/rpc.pb.go
1259
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load Diff
@ -1606,11 +1606,7 @@ message QueryRoutesRequest {
|
||||
/// The amount to send expressed in satoshis
|
||||
int64 amt = 2;
|
||||
|
||||
/**
|
||||
Deprecated. The max number of routes to return. In the future, QueryRoutes
|
||||
will only return a single route.
|
||||
*/
|
||||
int32 num_routes = 3 [deprecated = true];
|
||||
reserved 3;
|
||||
|
||||
/// An optional CLTV delta from the current height that should be used for the timelock of the final hop
|
||||
int32 final_cltv_delta = 4;
|
||||
|
@ -673,14 +673,6 @@
|
||||
"type": "string",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"name": "num_routes",
|
||||
"description": "*\nDeprecated. The max number of routes to return. In the future, QueryRoutes\nwill only return a single route.",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
{
|
||||
"name": "final_cltv_delta",
|
||||
"description": "/ An optional CLTV delta from the current height that should be used for the timelock of the final hop.",
|
||||
|
@ -616,180 +616,3 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex,
|
||||
|
||||
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(tx *bbolt.Tx, graph *channeldb.ChannelGraph,
|
||||
source, target route.Vertex, amt lnwire.MilliSatoshi,
|
||||
restrictions *RestrictParams, numPaths uint32,
|
||||
bandwidthHints map[uint64]lnwire.MilliSatoshi) (
|
||||
[][]*channeldb.ChannelEdgePolicy, error) {
|
||||
|
||||
// TODO(roasbeef): modifying ordering within heap to eliminate final
|
||||
// sorting step?
|
||||
var (
|
||||
shortestPaths [][]*channeldb.ChannelEdgePolicy
|
||||
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(
|
||||
&graphParams{
|
||||
tx: tx,
|
||||
graph: graph,
|
||||
bandwidthHints: bandwidthHints,
|
||||
},
|
||||
restrictions, source, target, 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([]*channeldb.ChannelEdgePolicy, 0, len(startingPath)+1)
|
||||
firstPath = append(firstPath, &channeldb.ChannelEdgePolicy{
|
||||
Node: &channeldb.LightningNode{PubKeyBytes: source},
|
||||
})
|
||||
firstPath = append(firstPath, startingPath...)
|
||||
|
||||
shortestPaths = append(shortestPaths, firstPath)
|
||||
|
||||
// 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 := uint32(1); k < numPaths; 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[EdgeLocator]struct{})
|
||||
ignoredVertexes := make(map[route.Vertex]struct{})
|
||||
|
||||
for e := range restrictions.IgnoredEdges {
|
||||
ignoredEdges[e] = struct{}{}
|
||||
}
|
||||
for n := range restrictions.IgnoredNodes {
|
||||
ignoredVertexes[n] = 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. This ensures that we create a new unique
|
||||
// path.
|
||||
for _, path := range shortestPaths {
|
||||
// If our current rootPath is a prefix of this
|
||||
// shortest path, then we'll remove the edge
|
||||
// directly _after_ our spur node from the
|
||||
// graph so we don't repeat paths.
|
||||
if len(path) > i+1 &&
|
||||
isSamePath(rootPath, path[:i+1]) {
|
||||
|
||||
locator := newEdgeLocator(path[i+1])
|
||||
ignoredEdges[*locator] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Next we'll remove all entries in the root path that
|
||||
// aren't the current spur node from the graph. This
|
||||
// ensures we don't create a path with loops.
|
||||
for _, hop := range rootPath {
|
||||
node := hop.Node.PubKeyBytes
|
||||
if node == spurNode.PubKeyBytes {
|
||||
continue
|
||||
}
|
||||
|
||||
ignoredVertexes[route.Vertex(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.
|
||||
//
|
||||
// TODO: Fee limit passed to spur path finding isn't
|
||||
// correct, because it doesn't take into account the
|
||||
// fees already paid on the root path.
|
||||
//
|
||||
// TODO: Outgoing channel restriction isn't obeyed for
|
||||
// spur paths.
|
||||
spurRestrictions := &RestrictParams{
|
||||
IgnoredEdges: ignoredEdges,
|
||||
IgnoredNodes: ignoredVertexes,
|
||||
FeeLimit: restrictions.FeeLimit,
|
||||
}
|
||||
|
||||
spurPath, err := findPath(
|
||||
&graphParams{
|
||||
tx: tx,
|
||||
graph: graph,
|
||||
bandwidthHints: bandwidthHints,
|
||||
},
|
||||
spurRestrictions, spurNode.PubKeyBytes,
|
||||
target, amt,
|
||||
)
|
||||
|
||||
// If we weren't able to find a path, we'll continue to
|
||||
// the next round.
|
||||
if IsError(err, ErrNoPathFound) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the new combined path by concatenating the
|
||||
// rootPath to the spurPath.
|
||||
newPathLen := len(rootPath) + len(spurPath)
|
||||
newPath := path{
|
||||
hops: make([]*channeldb.ChannelEdgePolicy, 0, newPathLen),
|
||||
dist: newPathLen,
|
||||
}
|
||||
newPath.hops = append(newPath.hops, rootPath...)
|
||||
newPath.hops = append(newPath.hops, spurPath...)
|
||||
|
||||
// TODO(roasbeef): add and consult path finger print
|
||||
|
||||
// We'll now add this newPath to the heap of candidate
|
||||
// shortest paths.
|
||||
heap.Push(&candidatePaths, 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
|
||||
}
|
||||
|
@ -948,63 +948,6 @@ func TestPathFindingWithAdditionalEdges(t *testing.T) {
|
||||
assertExpectedPath(t, graph.aliasMap, path, "songoku", "doge")
|
||||
}
|
||||
|
||||
func TestKShortestPathFinding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
graph, err := parseTestGraph(basicGraphFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create graph: %v", err)
|
||||
}
|
||||
defer graph.cleanUp()
|
||||
|
||||
sourceNode, err := graph.graph.SourceNode()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to fetch source node: %v", err)
|
||||
}
|
||||
|
||||
// In this test we'd like to ensure that our algorithm to find the
|
||||
// k-shortest paths from a given source node to any destination node
|
||||
// works as expected.
|
||||
|
||||
// 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".
|
||||
|
||||
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
||||
target := graph.aliasMap["luoji"]
|
||||
restrictions := &RestrictParams{
|
||||
FeeLimit: noFeeLimit,
|
||||
}
|
||||
paths, err := findPaths(
|
||||
nil, graph.graph, sourceNode.PubKeyBytes, target, paymentAmt,
|
||||
restrictions, 100, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find paths between roasbeef and "+
|
||||
"luo ji: %v", err)
|
||||
}
|
||||
|
||||
// The algorithm should have 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))
|
||||
}
|
||||
|
||||
// Additionally, 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")
|
||||
}
|
||||
|
||||
// The first route should be a direct route to luo ji.
|
||||
assertExpectedPath(t, graph.aliasMap, paths[0], "roasbeef", "luoji")
|
||||
|
||||
// The second route should be a route to luo ji via satoshi.
|
||||
assertExpectedPath(
|
||||
t, graph.aliasMap, paths[1], "roasbeef", "satoshi", "luoji",
|
||||
)
|
||||
}
|
||||
|
||||
// TestNewRoute tests whether the construction of hop payloads by newRoute
|
||||
// is executed correctly.
|
||||
func TestNewRoute(t *testing.T) {
|
||||
@ -1732,45 +1675,38 @@ 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(
|
||||
bobNode.PubKeyBytes, carol, amt, noRestrictions, 100,
|
||||
route, err := ctx.router.FindRoute(
|
||||
bobNode.PubKeyBytes, carol, amt, noRestrictions,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find route: %v", err)
|
||||
}
|
||||
|
||||
// We should come back with _exactly_ two routes.
|
||||
if len(routes) != 2 {
|
||||
t.Fatalf("expected %v routes, instead have: %v", 2,
|
||||
len(routes))
|
||||
}
|
||||
|
||||
// Now we'll examine the first route returned for correctness.
|
||||
// Now we'll examine the route returned for correctness.
|
||||
//
|
||||
// It should be sending the exact payment amount as there are no
|
||||
// additional hops.
|
||||
firstRoute := routes[0]
|
||||
if firstRoute.TotalAmount != amt {
|
||||
if route.TotalAmount != amt {
|
||||
t.Fatalf("wrong total amount: got %v, expected %v",
|
||||
firstRoute.TotalAmount, amt)
|
||||
route.TotalAmount, amt)
|
||||
}
|
||||
if firstRoute.Hops[0].AmtToForward != amt {
|
||||
if route.Hops[0].AmtToForward != amt {
|
||||
t.Fatalf("wrong forward amount: got %v, expected %v",
|
||||
firstRoute.Hops[0].AmtToForward, amt)
|
||||
route.Hops[0].AmtToForward, amt)
|
||||
}
|
||||
|
||||
fee := firstRoute.HopFee(0)
|
||||
fee := route.HopFee(0)
|
||||
if fee != 0 {
|
||||
t.Fatalf("wrong hop fee: got %v, expected %v", fee, 0)
|
||||
}
|
||||
|
||||
// The CLTV expiry should be the current height plus 9 (the expiry for
|
||||
// the B -> C channel.
|
||||
if firstRoute.TotalTimeLock !=
|
||||
if route.TotalTimeLock !=
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta {
|
||||
|
||||
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
||||
firstRoute.TotalTimeLock,
|
||||
route.TotalTimeLock,
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta)
|
||||
}
|
||||
|
||||
@ -1798,48 +1734,38 @@ func TestPathFindSpecExample(t *testing.T) {
|
||||
}
|
||||
|
||||
// We'll now request a route from A -> B -> C.
|
||||
routes, err = ctx.router.FindRoutes(
|
||||
source.PubKeyBytes, carol, amt, noRestrictions, 100,
|
||||
route, err = ctx.router.FindRoute(
|
||||
source.PubKeyBytes, carol, amt, noRestrictions,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find routes: %v", err)
|
||||
}
|
||||
|
||||
// We should come back with _exactly_ two routes.
|
||||
if len(routes) != 2 {
|
||||
t.Fatalf("expected %v routes, instead have: %v", 2,
|
||||
len(routes))
|
||||
}
|
||||
|
||||
// Both routes should be two hops.
|
||||
if len(routes[0].Hops) != 2 {
|
||||
// The route should be two hops.
|
||||
if len(route.Hops) != 2 {
|
||||
t.Fatalf("route should be %v hops, is instead %v", 2,
|
||||
len(routes[0].Hops))
|
||||
}
|
||||
if len(routes[1].Hops) != 2 {
|
||||
t.Fatalf("route should be %v hops, is instead %v", 2,
|
||||
len(routes[1].Hops))
|
||||
len(route.Hops))
|
||||
}
|
||||
|
||||
// The total amount should factor in a fee of 10199 and also use a CLTV
|
||||
// delta total of 29 (20 + 9),
|
||||
expectedAmt := lnwire.MilliSatoshi(5010198)
|
||||
if routes[0].TotalAmount != expectedAmt {
|
||||
if route.TotalAmount != expectedAmt {
|
||||
t.Fatalf("wrong amount: got %v, expected %v",
|
||||
routes[0].TotalAmount, expectedAmt)
|
||||
route.TotalAmount, expectedAmt)
|
||||
}
|
||||
if routes[0].TotalTimeLock != startingHeight+29 {
|
||||
if route.TotalTimeLock != startingHeight+29 {
|
||||
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
||||
routes[0].TotalTimeLock, startingHeight+29)
|
||||
route.TotalTimeLock, startingHeight+29)
|
||||
}
|
||||
|
||||
// Ensure that the hops of the first route are properly crafted.
|
||||
// Ensure that the hops of the route are properly crafted.
|
||||
//
|
||||
// After taking the fee, Bob should be forwarding the remainder which
|
||||
// is the exact payment to Bob.
|
||||
if routes[0].Hops[0].AmtToForward != amt {
|
||||
if route.Hops[0].AmtToForward != amt {
|
||||
t.Fatalf("wrong forward amount: got %v, expected %v",
|
||||
routes[0].Hops[0].AmtToForward, amt)
|
||||
route.Hops[0].AmtToForward, amt)
|
||||
}
|
||||
|
||||
// We shouldn't pay any fee for the first, hop, but the fee for the
|
||||
@ -1850,70 +1776,31 @@ func TestPathFindSpecExample(t *testing.T) {
|
||||
//
|
||||
// * 200 + 4999999 * 2000 / 1000000 = 10199
|
||||
|
||||
fee = routes[0].HopFee(0)
|
||||
fee = route.HopFee(0)
|
||||
if fee != 10199 {
|
||||
t.Fatalf("wrong hop fee: got %v, expected %v", fee, 10199)
|
||||
}
|
||||
|
||||
// While for the final hop, as there's no additional hop afterwards, we
|
||||
// pay no fee.
|
||||
fee = routes[0].HopFee(1)
|
||||
fee = route.HopFee(1)
|
||||
if fee != 0 {
|
||||
t.Fatalf("wrong hop fee: got %v, expected %v", fee, 0)
|
||||
}
|
||||
|
||||
// The outgoing CLTV value itself should be the current height plus 30
|
||||
// to meet Carol's requirements.
|
||||
if routes[0].Hops[0].OutgoingTimeLock !=
|
||||
if route.Hops[0].OutgoingTimeLock !=
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta {
|
||||
|
||||
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
||||
routes[0].Hops[0].OutgoingTimeLock,
|
||||
route.Hops[0].OutgoingTimeLock,
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta)
|
||||
}
|
||||
|
||||
// For B -> C, we assert that the final hop also has the proper
|
||||
// parameters.
|
||||
lastHop := routes[0].Hops[1]
|
||||
if lastHop.AmtToForward != amt {
|
||||
t.Fatalf("wrong forward amount: got %v, expected %v",
|
||||
lastHop.AmtToForward, amt)
|
||||
}
|
||||
if lastHop.OutgoingTimeLock !=
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta {
|
||||
|
||||
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
||||
lastHop.OutgoingTimeLock,
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta)
|
||||
}
|
||||
|
||||
// We'll also make similar assertions for the second route from A to C
|
||||
// via D.
|
||||
secondRoute := routes[1]
|
||||
expectedAmt = 5020398
|
||||
if secondRoute.TotalAmount != expectedAmt {
|
||||
t.Fatalf("wrong amount: got %v, expected %v",
|
||||
secondRoute.TotalAmount, expectedAmt)
|
||||
}
|
||||
expectedTimeLock := startingHeight + daveFinalCLTV + zpay32.DefaultFinalCLTVDelta
|
||||
if secondRoute.TotalTimeLock != uint32(expectedTimeLock) {
|
||||
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
||||
secondRoute.TotalTimeLock, expectedTimeLock)
|
||||
}
|
||||
onionPayload := secondRoute.Hops[0]
|
||||
if onionPayload.AmtToForward != amt {
|
||||
t.Fatalf("wrong forward amount: got %v, expected %v",
|
||||
onionPayload.AmtToForward, amt)
|
||||
}
|
||||
expectedTimeLock = startingHeight + zpay32.DefaultFinalCLTVDelta
|
||||
if onionPayload.OutgoingTimeLock != uint32(expectedTimeLock) {
|
||||
t.Fatalf("wrong outgoing time lock: got %v, expecting %v",
|
||||
onionPayload.OutgoingTimeLock,
|
||||
expectedTimeLock)
|
||||
}
|
||||
|
||||
// The B -> C hop should also be identical as the prior cases.
|
||||
lastHop = secondRoute.Hops[1]
|
||||
lastHop := route.Hops[1]
|
||||
if lastHop.AmtToForward != amt {
|
||||
t.Fatalf("wrong forward amount: got %v, expected %v",
|
||||
lastHop.AmtToForward, amt)
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -1312,72 +1311,12 @@ type routingMsg struct {
|
||||
err chan error
|
||||
}
|
||||
|
||||
// pathsToFeeSortedRoutes takes a set of paths, and returns a corresponding set
|
||||
// of routes. A route differs from a path in that it has full time-lock and
|
||||
// fee information attached. The set of routes returned 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 route.Vertex, paths [][]*channeldb.ChannelEdgePolicy,
|
||||
finalCLTVDelta uint16, amt lnwire.MilliSatoshi,
|
||||
currentHeight uint32) ([]*route.Route, error) {
|
||||
|
||||
validRoutes := make([]*route.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. 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(source, target route.Vertex,
|
||||
amt lnwire.MilliSatoshi, restrictions *RestrictParams, numPaths uint32,
|
||||
finalExpiry ...uint16) ([]*route.Route, error) {
|
||||
// FindRoute attempts to query the ChannelRouter for the optimum path to a
|
||||
// particular target destination to which it is able to send `amt` after
|
||||
// factoring in channel capacities and cumulative fees along the route.
|
||||
func (r *ChannelRouter) FindRoute(source, target route.Vertex,
|
||||
amt lnwire.MilliSatoshi, restrictions *RestrictParams,
|
||||
finalExpiry ...uint16) (*route.Route, error) {
|
||||
|
||||
var finalCLTVDelta uint16
|
||||
if len(finalExpiry) == 0 {
|
||||
@ -1388,11 +1327,6 @@ func (r *ChannelRouter) FindRoutes(source, target route.Vertex,
|
||||
|
||||
log.Debugf("Searching for path to %x, sending %v", target, amt)
|
||||
|
||||
// If we don't have a set of routes cached, we'll query the graph for a
|
||||
// set of potential routes to the destination node that can support our
|
||||
// payment amount. If no such routes can be found then an error will be
|
||||
// returned.
|
||||
|
||||
// We can short circuit the routing by opportunistically checking to
|
||||
// see if the target vertex event exists in the current graph.
|
||||
if _, exists, err := r.cfg.Graph.HasLightningNode(target); err != nil {
|
||||
@ -1402,16 +1336,8 @@ func (r *ChannelRouter) FindRoutes(source, target route.Vertex,
|
||||
return nil, newErrf(ErrTargetNotInNetwork, "target not found")
|
||||
}
|
||||
|
||||
// We'll also fetch the current block height so we can properly
|
||||
// calculate the required HTLC time locks within the route.
|
||||
_, currentHeight, err := r.cfg.Chain.GetBestBlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Before we open the db transaction below, we'll attempt to obtain a
|
||||
// set of bandwidth hints that can help us eliminate certain routes
|
||||
// early on in the path finding process.
|
||||
// We'll attempt to obtain a set of bandwidth hints that can help us
|
||||
// eliminate certain routes early on in the path finding process.
|
||||
bandwidthHints, err := generateBandwidthHints(
|
||||
r.selfNode, r.cfg.QueryBandwidth,
|
||||
)
|
||||
@ -1419,47 +1345,38 @@ func (r *ChannelRouter) FindRoutes(source, target route.Vertex,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx, err := r.cfg.Graph.Database().Begin(false)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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, source, target, amt, restrictions,
|
||||
numPaths, bandwidthHints,
|
||||
// Now that we know the destination is reachable within the graph, we'll
|
||||
// execute our path finding algorithm.
|
||||
path, err := findPath(
|
||||
&graphParams{
|
||||
graph: r.cfg.Graph,
|
||||
bandwidthHints: bandwidthHints,
|
||||
},
|
||||
restrictions, source, target, amt,
|
||||
)
|
||||
|
||||
// We'll fetch the current block height so we can properly calculate the
|
||||
// required HTLC time locks within the route.
|
||||
_, currentHeight, err := r.cfg.Chain.GetBestBlock()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx.Rollback()
|
||||
|
||||
// Now that we have a set of paths, we'll need to turn them into
|
||||
// *routes* by computing the required time-lock and fee information for
|
||||
// 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.
|
||||
sourceVertex := route.Vertex(r.selfNode.PubKeyBytes)
|
||||
validRoutes, err := pathsToFeeSortedRoutes(
|
||||
sourceVertex, shortestPaths, finalCLTVDelta, amt,
|
||||
uint32(currentHeight),
|
||||
// Create the route with absolute time lock values.
|
||||
route, err := newRoute(
|
||||
amt, source, path, uint32(currentHeight), finalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go log.Tracef("Obtained %v paths sending %v to %x: %v", len(validRoutes),
|
||||
go log.Tracef("Obtained path to send %v to %x: %v",
|
||||
amt, target, newLogClosure(func() string {
|
||||
return spew.Sdump(validRoutes)
|
||||
return spew.Sdump(route)
|
||||
}),
|
||||
)
|
||||
|
||||
return validRoutes, nil
|
||||
return route, nil
|
||||
}
|
||||
|
||||
// generateSphinxPacket generates then encodes a sphinx packet which encodes
|
||||
|
@ -23,10 +23,6 @@ import (
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
)
|
||||
|
||||
// defaultNumRoutes is the default value for the maximum number of routes to
|
||||
// be returned by FindRoutes
|
||||
const defaultNumRoutes = 10
|
||||
|
||||
type testCtx struct {
|
||||
router *ChannelRouter
|
||||
|
||||
@ -165,60 +161,6 @@ func createTestCtxFromFile(startingHeight uint32, testGraph string) (*testCtx, f
|
||||
return createTestCtxFromGraphInstance(startingHeight, graphInstance)
|
||||
}
|
||||
|
||||
// TestFindRoutesFeeSorting asserts that routes found by the FindRoutes method
|
||||
// within the channel router are properly returned in a sorted order, with the
|
||||
// lowest fee route coming first.
|
||||
func TestFindRoutesFeeSorting(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const startingBlockHeight = 101
|
||||
ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight, basicGraphFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create router: %v", err)
|
||||
}
|
||||
defer cleanUp()
|
||||
|
||||
// In this test we'd like to ensure proper integration of the various
|
||||
// functions that are involved in path finding, and also route
|
||||
// selection.
|
||||
|
||||
// Execute a query for all possible routes between roasbeef and luo ji.
|
||||
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
||||
target := ctx.aliases["luoji"]
|
||||
routes, err := ctx.router.FindRoutes(
|
||||
ctx.router.selfNode.PubKeyBytes,
|
||||
target, paymentAmt, noRestrictions, defaultNumRoutes,
|
||||
zpay32.DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find any routes: %v", err)
|
||||
}
|
||||
|
||||
// Exactly, two such paths should be found.
|
||||
if len(routes) != 2 {
|
||||
t.Fatalf("2 routes should've been selected, instead %v were: %v",
|
||||
len(routes), spew.Sdump(routes))
|
||||
}
|
||||
|
||||
// We shouldn't pay a fee for the fist route, but the second route
|
||||
// should have a fee intact.
|
||||
if routes[0].TotalFees != 0 {
|
||||
t.Fatalf("incorrect fees for first route, expected 0 got: %v",
|
||||
routes[0].TotalFees)
|
||||
}
|
||||
if routes[1].TotalFees == 0 {
|
||||
t.Fatalf("total fees not set in second route: %v",
|
||||
spew.Sdump(routes[0]))
|
||||
}
|
||||
|
||||
// The paths should properly be ranked according to their total fee
|
||||
// rate.
|
||||
if routes[0].TotalFees > routes[1].TotalFees {
|
||||
t.Fatalf("routes not ranked by total fee: %v",
|
||||
spew.Sdump(routes))
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindRoutesWithFeeLimit asserts that routes found by the FindRoutes method
|
||||
// within the channel router contain a total fee less than or equal to the fee
|
||||
// limit.
|
||||
@ -247,24 +189,20 @@ func TestFindRoutesWithFeeLimit(t *testing.T) {
|
||||
FeeLimit: lnwire.NewMSatFromSatoshis(10),
|
||||
}
|
||||
|
||||
routes, err := ctx.router.FindRoutes(
|
||||
route, err := ctx.router.FindRoute(
|
||||
ctx.router.selfNode.PubKeyBytes,
|
||||
target, paymentAmt, restrictions, defaultNumRoutes,
|
||||
target, paymentAmt, restrictions,
|
||||
zpay32.DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find any routes: %v", err)
|
||||
}
|
||||
|
||||
if len(routes) != 1 {
|
||||
t.Fatalf("expected 1 route, got %d", len(routes))
|
||||
if route.TotalFees > restrictions.FeeLimit {
|
||||
t.Fatalf("route exceeded fee limit: %v", spew.Sdump(route))
|
||||
}
|
||||
|
||||
if routes[0].TotalFees > restrictions.FeeLimit {
|
||||
t.Fatalf("route exceeded fee limit: %v", spew.Sdump(routes[0]))
|
||||
}
|
||||
|
||||
hops := routes[0].Hops
|
||||
hops := route.Hops
|
||||
if len(hops) != 2 {
|
||||
t.Fatalf("expected 2 hops, got %d", len(hops))
|
||||
}
|
||||
@ -1339,22 +1277,19 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
||||
t.Fatalf("unable to update edge policy: %v", err)
|
||||
}
|
||||
|
||||
// We should now be able to find two routes to node 2.
|
||||
// We should now be able to find a route to node 2.
|
||||
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
||||
targetNode := priv2.PubKey()
|
||||
var targetPubKeyBytes route.Vertex
|
||||
copy(targetPubKeyBytes[:], targetNode.SerializeCompressed())
|
||||
routes, err := ctx.router.FindRoutes(
|
||||
_, err = ctx.router.FindRoute(
|
||||
ctx.router.selfNode.PubKeyBytes,
|
||||
targetPubKeyBytes, paymentAmt, noRestrictions, defaultNumRoutes,
|
||||
targetPubKeyBytes, paymentAmt, noRestrictions,
|
||||
zpay32.DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find any routes: %v", err)
|
||||
}
|
||||
if len(routes) != 2 {
|
||||
t.Fatalf("expected to find 2 route, found: %v", len(routes))
|
||||
}
|
||||
|
||||
// Now check that we can update the node info for the partial node
|
||||
// without messing up the channel graph.
|
||||
@ -1388,19 +1323,16 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
||||
t.Fatalf("could not add node: %v", err)
|
||||
}
|
||||
|
||||
// Should still be able to find the routes, and the info should be
|
||||
// Should still be able to find the route, and the info should be
|
||||
// updated.
|
||||
routes, err = ctx.router.FindRoutes(
|
||||
_, err = ctx.router.FindRoute(
|
||||
ctx.router.selfNode.PubKeyBytes,
|
||||
targetPubKeyBytes, paymentAmt, noRestrictions, defaultNumRoutes,
|
||||
targetPubKeyBytes, paymentAmt, noRestrictions,
|
||||
zpay32.DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find any routes: %v", err)
|
||||
}
|
||||
if len(routes) != 2 {
|
||||
t.Fatalf("expected to find 2 route, found: %v", len(routes))
|
||||
}
|
||||
|
||||
copy1, err := ctx.graph.FetchLightningNode(priv1.PubKey())
|
||||
if err != nil {
|
||||
|
@ -462,7 +462,7 @@ func newRPCServer(s *server, macService *macaroons.Service,
|
||||
}
|
||||
return info.Capacity, nil
|
||||
},
|
||||
FindRoutes: s.chanRouter.FindRoutes,
|
||||
FindRoute: s.chanRouter.FindRoute,
|
||||
}
|
||||
|
||||
var (
|
||||
|
Loading…
x
Reference in New Issue
Block a user