diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index 1c14dd4d0..02caf26c3 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -282,8 +282,11 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment, // Taking into account this prune view, we'll attempt to locate a path // to our destination, respecting the recommendations from // missionControl. - path, err := findPath(nil, p.mc.graph, p.mc.selfNode, payment.Target, - pruneView.vertexes, pruneView.edges, payment.Amount) + path, err := findPath( + nil, p.mc.graph, p.additionalEdges, p.mc.selfNode, + payment.Target, pruneView.vertexes, pruneView.edges, + payment.Amount, + ) if err != nil { return nil, err } diff --git a/routing/pathfind.go b/routing/pathfind.go index 7867a41ba..74743d658 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -455,6 +455,7 @@ func edgeWeight(amt lnwire.MilliSatoshi, e *channeldb.ChannelEdgePolicy) int64 { // function returns a slice of ChannelHop structs which encoded the chosen path // from the target to the source. func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, + additionalEdges map[Vertex][]*channeldb.ChannelEdgePolicy, sourceNode *channeldb.LightningNode, target *btcec.PublicKey, ignoredNodes map[Vertex]struct{}, ignoredEdges map[uint64]struct{}, amt lnwire.MilliSatoshi) ([]*ChannelHop, error) { @@ -473,9 +474,8 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, // traversal. var nodeHeap distanceHeap - // For each node/Vertex the graph we create an entry in the distance - // map for the node set with a distance of "infinity". We also mark - // add the node to our set of unvisited nodes. + // For each node in the graph, we create an entry in the distance + // map for the node set with a distance of "infinity". distance := make(map[Vertex]nodeWithDist) if err := graph.ForEachNode(tx, func(_ *bolt.Tx, node *channeldb.LightningNode) error { // TODO(roasbeef): with larger graph can just use disk seeks @@ -489,6 +489,86 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, return nil, err } + // We'll also include all the nodes found within the additional edges + // that are not known to us yet in the distance map. + for vertex := range additionalEdges { + node := &channeldb.LightningNode{PubKeyBytes: vertex} + distance[vertex] = nodeWithDist{ + dist: infinity, + node: node, + } + } + + // We can't always assume that the end destination is publicly + // advertised to the network and included in the graph.ForEachNode call + // above, so we'll manually include the target node. + targetVertex := NewVertex(target) + targetNode := &channeldb.LightningNode{PubKeyBytes: targetVertex} + distance[targetVertex] = nodeWithDist{ + dist: infinity, + node: targetNode, + } + + // We'll use this map as a series of "previous" hop pointers. So to get + // to `Vertex` we'll take the edge that it's mapped to within `prev`. + prev := make(map[Vertex]edgeWithPrev) + + // processEdge is a helper closure that will be used to make sure edges + // satisfy our specific requirements. + processEdge := func(edge *channeldb.ChannelEdgePolicy, + capacity btcutil.Amount, pivot Vertex) { + + v := Vertex(edge.Node.PubKeyBytes) + + // If the edge is currently disabled, then we'll stop here, as + // we shouldn't attempt to route through it. + edgeFlags := lnwire.ChanUpdateFlag(edge.Flags) + if edgeFlags&lnwire.ChanUpdateDisabled != 0 { + return + } + + // If this vertex or edge has been black listed, then we'll skip + // exploring this edge. + if _, ok := ignoredNodes[v]; ok { + return + } + if _, ok := ignoredEdges[edge.ChannelID]; ok { + return + } + + // Compute the tentative distance to this new channel/edge which + // is the distance to our pivot node plus the weight of this + // edge. + tempDist := distance[pivot].dist + edgeWeight(amt, edge) + + // If this new tentative distance is better than the current + // best known distance to this node, then we record the new + // better distance, and also populate our "next hop" map with + // this edge. We'll also shave off irrelevant edges by adding + // the sufficient capacity of an edge and clearing their + // min-htlc amount to our relaxation condition. + if tempDist < distance[v].dist && capacity >= amt.ToSatoshis() && + amt >= edge.MinHTLC && edge.TimeLockDelta != 0 { + + distance[v] = nodeWithDist{ + dist: tempDist, + node: edge.Node, + } + + prev[v] = edgeWithPrev{ + edge: &ChannelHop{ + ChannelEdgePolicy: edge, + Capacity: capacity, + }, + prevNode: pivot, + } + + // Add this new node to our heap as we'd like to further + // explore down this edge. + heap.Push(&nodeHeap, distance[v]) + } + } + // TODO(roasbeef): also add path caching // * similar to route caching, but doesn't factor in the amount @@ -505,11 +585,6 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, // heap. heap.Push(&nodeHeap, distance[sourceVertex]) - targetBytes := target.SerializeCompressed() - - // We'll use this map as a series of "previous" hop pointers. So to get - // to `Vertex` we'll take the edge that it's mapped to within `prev`. - prev := make(map[Vertex]edgeWithPrev) for nodeHeap.Len() != 0 { // Fetch the node within the smallest distance from our source // from the heap. @@ -519,7 +594,7 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, // If we've reached our target (or we don't have any outgoing // edges), then we're done here and can exit the graph // traversal early. - if bytes.Equal(bestNode.PubKeyBytes[:], targetBytes) { + if bytes.Equal(bestNode.PubKeyBytes[:], targetVertex[:]) { break } @@ -529,65 +604,9 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, pivot := Vertex(bestNode.PubKeyBytes) err := bestNode.ForEachChannel(tx, func(tx *bolt.Tx, edgeInfo *channeldb.ChannelEdgeInfo, - outEdge, inEdge *channeldb.ChannelEdgePolicy) error { + outEdge, _ *channeldb.ChannelEdgePolicy) error { - v := Vertex(outEdge.Node.PubKeyBytes) - - // If the outgoing edge is currently disabled, then - // we'll stop here, as we shouldn't attempt to route - // through it. - edgeFlags := lnwire.ChanUpdateFlag(outEdge.Flags) - if edgeFlags&lnwire.ChanUpdateDisabled == lnwire.ChanUpdateDisabled { - return nil - } - - // If this Vertex or edge has been black listed, then - // we'll skip exploring this edge during this - // iteration. - if _, ok := ignoredNodes[v]; ok { - return nil - } - if _, ok := ignoredEdges[outEdge.ChannelID]; ok { - return nil - } - - // Compute the tentative distance to this new - // channel/edge which is the distance to our current - // pivot node plus the weight of this edge. - tempDist := distance[pivot].dist + edgeWeight(amt, outEdge) - - // If this new tentative distance is better than the - // current best known distance to this node, then we - // record the new better distance, and also populate - // our "next hop" map with this edge. We'll also shave - // off irrelevant edges by adding the sufficient - // capacity of an edge and clearing their min-htlc - // amount to our relaxation condition. - if tempDist < distance[v].dist && - edgeInfo.Capacity >= amt.ToSatoshis() && - amt >= outEdge.MinHTLC && - outEdge.TimeLockDelta != 0 { - - distance[v] = nodeWithDist{ - dist: tempDist, - node: outEdge.Node, - } - prev[v] = edgeWithPrev{ - // We'll use the *incoming* edge here - // as we need to use the routing policy - // specified by the node this channel - // connects to. - edge: &ChannelHop{ - ChannelEdgePolicy: outEdge, - Capacity: edgeInfo.Capacity, - }, - prevNode: bestNode.PubKeyBytes, - } - - // Add this new node to our heap as we'd like - // to further explore down this edge. - heap.Push(&nodeHeap, distance[v]) - } + processEdge(outEdge, edgeInfo.Capacity, pivot) // TODO(roasbeef): return min HTLC as error in end? @@ -596,6 +615,15 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, if err != nil { return nil, err } + + // Then, we'll examine all the additional edges from the node + // we're currently visiting. Since we don't know the capacity + // of the private channel, we'll assume it was selected as a + // routing hint due to having enough capacity for the payment + // and use the payment amount as its capacity. + for _, edge := range additionalEdges[bestNode.PubKeyBytes] { + processEdge(edge, amt.ToSatoshis(), pivot) + } } // If the target node isn't found in the prev hop map, then a path @@ -669,7 +697,8 @@ func findPaths(tx *bolt.Tx, graph *channeldb.ChannelGraph, // selfNode) to the target destination that's capable of carrying amt // satoshis along the path before fees are calculated. startingPath, err := findPath( - tx, graph, source, target, ignoredVertexes, ignoredEdges, amt, + tx, graph, nil, source, target, ignoredVertexes, ignoredEdges, + amt, ) if err != nil { log.Errorf("Unable to find path: %v", err) @@ -742,8 +771,8 @@ func findPaths(tx *bolt.Tx, graph *channeldb.ChannelGraph, // root path removed, we'll attempt to find another // shortest path from the spur node to the destination. spurPath, err := findPath( - tx, graph, spurNode, target, ignoredVertexes, - ignoredEdges, amt, + tx, graph, nil, spurNode, target, + ignoredVertexes, ignoredEdges, amt, ) // If we weren't able to find a path, we'll continue to diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 0a4d4eeb4..b65e50e7a 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -311,8 +311,10 @@ func TestBasicGraphPathFinding(t *testing.T) { paymentAmt := lnwire.NewMSatFromSatoshis(100) target := aliases["sophon"] - path, err := findPath(nil, graph, sourceNode, target, ignoredVertexes, - ignoredEdges, paymentAmt) + path, err := findPath( + nil, graph, nil, sourceNode, target, ignoredVertexes, + ignoredEdges, paymentAmt, + ) if err != nil { t.Fatalf("unable to find path: %v", err) } @@ -451,8 +453,10 @@ func TestBasicGraphPathFinding(t *testing.T) { // exist two possible paths in the graph, but the shorter (1 hop) path // should be selected. target = aliases["luoji"] - path, err = findPath(nil, graph, sourceNode, target, ignoredVertexes, - ignoredEdges, paymentAmt) + path, err = findPath( + nil, graph, nil, sourceNode, target, ignoredVertexes, + ignoredEdges, paymentAmt, + ) if err != nil { t.Fatalf("unable to find route: %v", err) } @@ -572,8 +576,10 @@ func TestNewRoutePathTooLong(t *testing.T) { // We start by confirming that routing a payment 20 hops away is possible. // Alice should be able to find a valid route to ursula. target := aliases["ursula"] - _, err = findPath(nil, graph, sourceNode, target, ignoredVertexes, - ignoredEdges, paymentAmt) + _, err = findPath( + nil, graph, nil, sourceNode, target, ignoredVertexes, + ignoredEdges, paymentAmt, + ) if err != nil { t.Fatalf("path should have been found") } @@ -581,8 +587,10 @@ func TestNewRoutePathTooLong(t *testing.T) { // Vincent is 21 hops away from Alice, and thus no valid route should be // presented to Alice. target = aliases["vincent"] - path, err := findPath(nil, graph, sourceNode, target, ignoredVertexes, - ignoredEdges, paymentAmt) + path, err := findPath( + nil, graph, nil, sourceNode, target, ignoredVertexes, + ignoredEdges, paymentAmt, + ) if err == nil { t.Fatalf("should not have been able to find path, supposed to be "+ "greater than 20 hops, found route with %v hops", @@ -621,8 +629,10 @@ func TestPathNotAvailable(t *testing.T) { t.Fatalf("unable to parse pubkey: %v", err) } - _, err = findPath(nil, graph, sourceNode, unknownNode, ignoredVertexes, - ignoredEdges, 100) + _, err = findPath( + nil, graph, nil, sourceNode, unknownNode, ignoredVertexes, + ignoredEdges, 100, + ) if !IsError(err, ErrNoPathFound) { t.Fatalf("path shouldn't have been found: %v", err) } @@ -655,8 +665,10 @@ func TestPathInsufficientCapacity(t *testing.T) { target := aliases["sophon"] payAmt := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin) - _, err = findPath(nil, graph, sourceNode, target, ignoredVertexes, - ignoredEdges, payAmt) + _, err = findPath( + nil, graph, nil, sourceNode, target, ignoredVertexes, + ignoredEdges, payAmt, + ) if !IsError(err, ErrNoPathFound) { t.Fatalf("graph shouldn't be able to support payment: %v", err) } @@ -683,8 +695,10 @@ func TestRouteFailMinHTLC(t *testing.T) { // attempt should fail. target := aliases["songoku"] payAmt := lnwire.MilliSatoshi(10) - _, err = findPath(nil, graph, sourceNode, target, ignoredVertexes, - ignoredEdges, payAmt) + _, err = findPath( + nil, graph, nil, sourceNode, target, ignoredVertexes, + ignoredEdges, payAmt, + ) if !IsError(err, ErrNoPathFound) { t.Fatalf("graph shouldn't be able to support payment: %v", err) } @@ -711,8 +725,10 @@ func TestRouteFailDisabledEdge(t *testing.T) { // succeed without issue, and return a single path. target := aliases["songoku"] payAmt := lnwire.NewMSatFromSatoshis(10000) - _, err = findPath(nil, graph, sourceNode, target, ignoredVertexes, - ignoredEdges, payAmt) + _, err = findPath( + nil, graph, nil, sourceNode, target, ignoredVertexes, + ignoredEdges, payAmt, + ) if err != nil { t.Fatalf("unable to find path: %v", err) } @@ -730,8 +746,10 @@ func TestRouteFailDisabledEdge(t *testing.T) { // Now, if we attempt to route through that edge, we should get a // failure as it is no longer eligible. - _, err = findPath(nil, graph, sourceNode, target, ignoredVertexes, - ignoredEdges, payAmt) + _, err = findPath( + nil, graph, nil, sourceNode, target, ignoredVertexes, + ignoredEdges, payAmt, + ) if !IsError(err, ErrNoPathFound) { t.Fatalf("graph shouldn't be able to support payment: %v", err) } diff --git a/routing/router_test.go b/routing/router_test.go index 8c73826ed..b7029e678 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -1602,8 +1602,8 @@ func TestFindPathFeeWeighting(t *testing.T) { // the edge weighting, we should select the direct path over the 2 hop // path even though the direct path has a higher potential time lock. path, err := findPath( - nil, ctx.graph, sourceNode, target, ignoreVertex, ignoreEdge, - amt, + nil, ctx.graph, nil, sourceNode, target, ignoreVertex, + ignoreEdge, amt, ) if err != nil { t.Fatalf("unable to find path: %v", err)