From 3e7473f4f0446d67562a57363e7573b530888809 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 4 Jun 2018 22:10:05 +0200 Subject: [PATCH] routing: backward searching --- routing/heap.go | 13 +- routing/missioncontrol.go | 2 +- routing/pathfind.go | 325 +++++++++++++++++++++++++------------- routing/pathfind_test.go | 96 ++++++----- routing/router.go | 2 +- routing/router_test.go | 2 +- rpcserver.go | 7 +- 7 files changed, 295 insertions(+), 152 deletions(-) diff --git a/routing/heap.go b/routing/heap.go index 25745cb97..6d4058991 100644 --- a/routing/heap.go +++ b/routing/heap.go @@ -1,6 +1,9 @@ package routing -import "github.com/lightningnetwork/lnd/channeldb" +import ( + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnwire" +) // nodeWithDist is a helper struct that couples the distance from the current // source to a node with a pointer to the node itself. @@ -12,6 +15,14 @@ type nodeWithDist struct { // node is the vertex itself. This pointer can be used to explore all // the outgoing edges (channels) emanating from a node. node *channeldb.LightningNode + + // amountToReceive is the amount that should be received by this node. + // Either as final payment to the final node or as an intermediate + // amount that includes also the fees for subsequent hops. + amountToReceive lnwire.MilliSatoshi + + // fee is the fee that this node is charging for forwarding. + fee lnwire.MilliSatoshi } // distanceHeap is a min-distance heap that's used within our path finding diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index bebac9ba5..b722b942f 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -375,7 +375,7 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment, path, err := findPath( nil, p.mc.graph, p.additionalEdges, p.mc.selfNode, payment.Target, pruneView.vertexes, pruneView.edges, - payment.Amount, p.bandwidthHints, + payment.Amount, payment.FeeLimit, p.bandwidthHints, ) if err != nil { return nil, err diff --git a/routing/pathfind.go b/routing/pathfind.go index a6025865a..d990a395d 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -10,7 +10,6 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcutil" "github.com/coreos/bbolt" "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/channeldb" @@ -69,9 +68,13 @@ type HopHint struct { // edge), as well as the total capacity. It also includes the origin chain of // the channel itself. type ChannelHop struct { - // Capacity is the total capacity of the channel being traversed. This - // value is expressed for stability in satoshis. - Capacity btcutil.Amount + // Bandwidth is an estimate of the maximum amount that can be sent + // through the channel in the direction indicated by ChannelEdgePolicy. + // It is based on the on-chain capacity of the channel, bandwidth + // hints passed in via SendRoute RPC and/or running amounts that + // represent pending payments. These running amounts have msat as + // unit. Therefore this property is expressed in msat too. + Bandwidth lnwire.MilliSatoshi // Chain is a 32-byte has that denotes the base blockchain network of // the channel. The 32-byte hash is the "genesis" block of the @@ -109,6 +112,14 @@ type Hop struct { Fee lnwire.MilliSatoshi } +// edgePolicyWithSource is a helper struct to keep track of the source node +// of a channel edge. ChannelEdgePolicy only contains to destination node +// of the edge. +type edgePolicyWithSource struct { + sourceNode *channeldb.LightningNode + edge *channeldb.ChannelEdgePolicy +} + // computeFee computes the fee to forward an HTLC of `amt` milli-satoshis over // the passed active payment channel. This value is currently computed as // specified in BOLT07, but will likely change in the near future. @@ -358,13 +369,12 @@ func newRoute(amtToSend, feeLimit lnwire.MilliSatoshi, sourceVertex Vertex, // enough capacity to carry the required amount which // includes the fee dictated at each hop. Make the comparison // in msat to prevent rounding errors. - if currentHop.AmtToForward+fee > lnwire.NewMSatFromSatoshis( - currentHop.Channel.Capacity) { + if currentHop.AmtToForward+fee > currentHop.Channel.Bandwidth { err := fmt.Sprintf("channel graph has insufficient "+ "capacity for the payment: need %v, have %v", - currentHop.AmtToForward.ToSatoshis(), - currentHop.Channel.Capacity) + currentHop.AmtToForward+fee, + currentHop.Channel.Bandwidth) return nil, newErrf(ErrInsufficientCapacity, err) } @@ -430,32 +440,22 @@ func (v Vertex) String() string { return fmt.Sprintf("%x", v[:]) } -// edgeWithPrev is a helper struct used in path finding that couples an -// directional edge with the node's ID in the opposite direction. -type edgeWithPrev struct { - edge *ChannelHop - prevNode [33]byte -} - // edgeWeight computes the weight of an edge. This value is used when searching // for the shortest path within the channel graph between two nodes. Weight is // is the fee itself plus a time lock penalty added to it. This benefits // channels with shorter time lock deltas and shorter (hops) routes in general. // RiskFactor controls the influence of time lock on route selection. This is // currently a fixed value, but might be configurable in the future. -func edgeWeight(amt lnwire.MilliSatoshi, e *channeldb.ChannelEdgePolicy) int64 { - // First, we'll compute the "pure" fee through this hop. We say pure, - // as this may not be what's ultimately paid as fees are properly - // calculated backwards, while we're going in the reverse direction. - pureFee := int64(computeFee(amt, e)) - +func edgeWeight(lockedAmt lnwire.MilliSatoshi, fee lnwire.MilliSatoshi, + timeLockDelta uint16) int64 { // timeLockPenalty is the penalty for the time lock delta of this channel. // It is controlled by RiskFactorBillionths and scales proportional // to the amount that will pass through channel. Rationale is that it if // a twice as large amount gets locked up, it is twice as bad. - timeLockPenalty := int64(amt) * int64(e.TimeLockDelta) * RiskFactorBillionths / 1000000000 + timeLockPenalty := int64(lockedAmt) * int64(timeLockDelta) * + RiskFactorBillionths / 1000000000 - return pureFee + timeLockPenalty + return int64(fee) + timeLockPenalty } // findPath attempts to find a path from the source node within the @@ -465,12 +465,15 @@ func edgeWeight(amt lnwire.MilliSatoshi, e *channeldb.ChannelEdgePolicy) int64 { // and the destination. The distance metric used for edges is related to the // time-lock+fee costs along a particular edge. If a path is found, this // function returns a slice of ChannelHop structs which encoded the chosen path -// from the target to the source. +// from the target to the source. The search is performed backwards from +// destination node back to source. This is to properly accumulate fees +// that need to be paid along the path and accurately check the amount +// to forward at every node against the available bandwidth. 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, + amt lnwire.MilliSatoshi, feeLimit lnwire.MilliSatoshi, bandwidthHints map[uint64]lnwire.MilliSatoshi) ([]*ChannelHop, error) { var err error @@ -488,7 +491,9 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, var nodeHeap distanceHeap // For each node in the graph, we create an entry in the distance - // map for the node set with a distance of "infinity". + // map for the node set with a distance of "infinity". graph.ForEachNode + // also returns the source node, so there is no need to add the source + // node explictly. 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 @@ -502,36 +507,60 @@ 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 { + additionalEdgesWithSrc := make(map[Vertex][]*edgePolicyWithSource) + for vertex, outgoingEdgePolicies := range additionalEdges { + // We'll also include all the nodes found within the additional edges + // that are not known to us yet in the distance map. node := &channeldb.LightningNode{PubKeyBytes: vertex} distance[vertex] = nodeWithDist{ dist: infinity, node: node, } + + // Build reverse lookup to find incoming edges. Needed + // because search is taken place from target to source. + for _, outgoingEdgePolicy := range outgoingEdgePolicies { + toVertex := outgoingEdgePolicy.Node.PubKeyBytes + incomingEdgePolicy := &edgePolicyWithSource{ + sourceNode: node, + edge: outgoingEdgePolicy, + } + + additionalEdgesWithSrc[toVertex] = + append(additionalEdgesWithSrc[toVertex], + incomingEdgePolicy) + } } + sourceVertex := Vertex(sourceNode.PubKeyBytes) + // 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. + // above, so we'll manually include the target node. The target + // node charges no fee. Distance is set to 0, because this is the + // starting point of the graph traversal. We are searching backwards to + // get the fees first time right and correctly match channel bandwidth. targetVertex := NewVertex(target) targetNode := &channeldb.LightningNode{PubKeyBytes: targetVertex} distance[targetVertex] = nodeWithDist{ - dist: infinity, - node: targetNode, + dist: 0, + node: targetNode, + amountToReceive: amt, + fee: 0, } - // 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) + // We'll use this map as a series of "next" hop pointers. So to get + // from `Vertex` to the target node, we'll take the edge that it's + // mapped to within `next`. + next := make(map[Vertex]*ChannelHop) // processEdge is a helper closure that will be used to make sure edges // satisfy our specific requirements. - processEdge := func(edge *channeldb.ChannelEdgePolicy, - bandwidth lnwire.MilliSatoshi, pivot Vertex) { + processEdge := func(fromNode *channeldb.LightningNode, + edge *channeldb.ChannelEdgePolicy, + bandwidth lnwire.MilliSatoshi, toNode Vertex) { - v := Vertex(edge.Node.PubKeyBytes) + fromVertex := Vertex(fromNode.PubKeyBytes) // If the edge is currently disabled, then we'll stop here, as // we shouldn't attempt to route through it. @@ -542,61 +571,114 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, // If this vertex or edge has been black listed, then we'll skip // exploring this edge. - if _, ok := ignoredNodes[v]; ok { + if _, ok := ignoredNodes[fromVertex]; 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) + toNodeDist := distance[toNode] - // 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 && bandwidth >= amt && - amt >= edge.MinHTLC && edge.TimeLockDelta != 0 { + amountToSend := toNodeDist.amountToReceive - distance[v] = nodeWithDist{ - dist: tempDist, - node: edge.Node, - } - - prev[v] = edgeWithPrev{ - edge: &ChannelHop{ - ChannelEdgePolicy: edge, - Capacity: bandwidth.ToSatoshis(), - }, - prevNode: pivot, - } - - // Add this new node to our heap as we'd like to further - // explore down this edge. - heap.Push(&nodeHeap, distance[v]) + // If the estimated band width of the channel edge is not able + // to carry the amount that needs to be send, return. + if bandwidth < amountToSend { + return } + + // If the amountToSend is less than the minimum required amount, + // return. + if amountToSend < edge.MinHTLC { + return + } + + // Compute fee that fromNode is charging. It is based on the + // amount that needs to be sent to the next node in the route. + // + // Source node has no precedessor to pay a fee. Therefore set + // fee to zero, because it should not be included in the + // fee limit check and edge weight. + // + // Also determine the time lock delta that will be added to + // the route if fromNode is selected. If fromNode is the + // source node, no additional timelock is required. + var fee lnwire.MilliSatoshi + var timeLockDelta uint16 + if fromVertex != sourceVertex { + fee = computeFee(amountToSend, edge) + timeLockDelta = edge.TimeLockDelta + } + + // amountToReceive is the amount that the node that + // is added to the distance map needs to receive from + // a (to be found) previous node in the route. That + // previous node will need to pay the amount that this + // node forwards plus the fee it charges. + amountToReceive := amountToSend + fee + + // Check if accumulated fees would exceed fee limit when + // this node would be added to the path. + totalFee := amountToReceive - amt + if totalFee > feeLimit { + return + } + + // By adding fromNode in the route, there will be an extra + // weight composed of the fee that this node will charge and + // the amount that will be locked for timeLockDelta blocks + // in the HTLC that is handed out to fromNode. + weight := edgeWeight(amountToReceive, fee, timeLockDelta) + + // Compute the tentative distance to this new channel/edge which + // is the distance from our toNode to the target node plus the + // weight of this edge. + tempDist := toNodeDist.dist + weight + + // If this new tentative distance is not better than the current + // best known distance to this node, return. + if tempDist >= distance[fromVertex].dist { + return + } + + // If the edge has no time lock delta, the payment will always + // fail, so return. + // + // TODO(joostjager): Is this really true? Can't it be that + // nodes take this risk in exchange for a extraordinary high + // fee? + if edge.TimeLockDelta == 0 { + return + } + + // All conditions are met and this new tentative distance is + // better than the current best known distance to this node. + // The new better distance is recorded, and also our + // "next hop" map is populated with this edge. + distance[fromVertex] = nodeWithDist{ + dist: tempDist, + node: fromNode, + amountToReceive: amountToReceive, + fee: fee, + } + + next[fromVertex] = &ChannelHop{ + ChannelEdgePolicy: edge, + Bandwidth: bandwidth, + } + + // Add this new node to our heap as we'd like to further + // explore backwards through this edge. + heap.Push(&nodeHeap, distance[fromVertex]) } // TODO(roasbeef): also add path caching // * similar to route caching, but doesn't factor in the amount - // To start, we add the source of our path finding attempt to the - // distance map with a distance of 0. This indicates our starting - // point in the graph traversal. - sourceVertex := Vertex(sourceNode.PubKeyBytes) - distance[sourceVertex] = nodeWithDist{ - dist: 0, - node: sourceNode, - } - - // To start, our source node will the sole item within our distance + // To start, our target node will the sole item within our distance // heap. - heap.Push(&nodeHeap, distance[sourceVertex]) + heap.Push(&nodeHeap, distance[targetVertex]) for nodeHeap.Len() != 0 { // Fetch the node within the smallest distance from our source @@ -604,20 +686,28 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, partialPath := heap.Pop(&nodeHeap).(nodeWithDist) bestNode := partialPath.node - // If we've reached our target (or we don't have any outgoing + // If we've reached our source (or we don't have any incoming // edges), then we're done here and can exit the graph // traversal early. - if bytes.Equal(bestNode.PubKeyBytes[:], targetVertex[:]) { + if bytes.Equal(bestNode.PubKeyBytes[:], sourceVertex[:]) { break } // Now that we've found the next potential step to take we'll - // examine all the outgoing edge (channels) from this node to + // examine all the incoming edges (channels) from this node to // further our graph traversal. pivot := Vertex(bestNode.PubKeyBytes) err := bestNode.ForEachChannel(tx, func(tx *bolt.Tx, edgeInfo *channeldb.ChannelEdgeInfo, - outEdge, _ *channeldb.ChannelEdgePolicy) error { + _, inEdge *channeldb.ChannelEdgePolicy) error { + + // If there is no edge policy for this candidate + // node, skip. Note that we are searching backwards + // so this node would have come prior to the pivot + // node in the route. + if inEdge == nil { + return nil + } // We'll query the lower layer to see if we can obtain // any more up to date information concerning the @@ -632,7 +722,31 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, ) } - processEdge(outEdge, edgeBandwidth, pivot) + // Lookup the source node at the other side of the + // channel via edgeInfo. This is necessary because this + // information is not present in inEdge. + channelSourcePubKeyBytes, err := edgeInfo.OtherNodeKeyBytes(pivot[:]) + if err != nil { + return err + } + + channelSourcePubKey, err := btcec.ParsePubKey( + channelSourcePubKeyBytes[:], btcec.S256()) + if err != nil { + return err + } + + // Lookup the full node details in order to be able to + // later iterate over all incoming edges of the source + // node. + channelSource, err := graph.FetchLightningNode(channelSourcePubKey) + if err != nil { + return err + } + + // Check if this candidate node is better than what + // we already have. + processEdge(channelSource, inEdge, edgeBandwidth, pivot) // TODO(roasbeef): return min HTLC as error in end? @@ -647,31 +761,31 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, // 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, pivot) + bandWidth := partialPath.amountToReceive + for _, reverseEdge := range additionalEdgesWithSrc[bestNode.PubKeyBytes] { + processEdge(reverseEdge.sourceNode, reverseEdge.edge, bandWidth, pivot) } } - // If the target node isn't found in the prev hop map, then a path + // If the source node isn't found in the next hop map, then a path // doesn't exist, so we terminate in an error. - if _, ok := prev[NewVertex(target)]; !ok { + if _, ok := next[sourceVertex]; !ok { return nil, newErrf(ErrNoPathFound, "unable to find a path to "+ "destination") } - // If the potential route if below the max hop limit, then we'll use - // the prevHop map to unravel the path. We end up with a list of edges - // in the reverse direction which we'll use to properly calculate the - // timelock and fee values. - pathEdges := make([]*ChannelHop, 0, len(prev)) - prevNode := NewVertex(target) - for prevNode != sourceVertex { // TODO(roasbeef): assumes no cycles - // Add the current hop to the limit of path edges then walk - // backwards from this hop via the prev pointer for this hop - // within the prevHop map. - pathEdges = append(pathEdges, prev[prevNode].edge) + // Use the nextHop map to unravel the forward path from source to target. + pathEdges := make([]*ChannelHop, 0, len(next)) + currentNode := sourceVertex + for currentNode != targetVertex { // TODO(roasbeef): assumes no cycles + // Determine the next hop forward using the next map. + nextNode := next[currentNode] - prevNode = Vertex(prev[prevNode].prevNode) + // Add the next hop to the list of path edges. + pathEdges = append(pathEdges, nextNode) + + // Advance current node. + currentNode = Vertex(nextNode.Node.PubKeyBytes) } // The route is invalid if it spans more than 20 hops. The current @@ -684,13 +798,6 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, "too many hops") } - // As our traversal of the prev map above walked backwards from the - // target to the source in the route, we need to reverse it before - // returning the final route. - for i := 0; i < numEdges/2; i++ { - pathEdges[i], pathEdges[numEdges-i-1] = pathEdges[numEdges-i-1], pathEdges[i] - } - return pathEdges, nil } @@ -707,7 +814,7 @@ func findPath(tx *bolt.Tx, graph *channeldb.ChannelGraph, // algorithm in a block box manner. func findPaths(tx *bolt.Tx, graph *channeldb.ChannelGraph, source *channeldb.LightningNode, target *btcec.PublicKey, - amt lnwire.MilliSatoshi, numPaths uint32, + amt lnwire.MilliSatoshi, feeLimit lnwire.MilliSatoshi, numPaths uint32, bandwidthHints map[uint64]lnwire.MilliSatoshi) ([][]*ChannelHop, error) { ignoredEdges := make(map[uint64]struct{}) @@ -725,7 +832,7 @@ func findPaths(tx *bolt.Tx, graph *channeldb.ChannelGraph, // satoshis along the path before fees are calculated. startingPath, err := findPath( tx, graph, nil, source, target, ignoredVertexes, ignoredEdges, - amt, bandwidthHints, + amt, feeLimit, bandwidthHints, ) if err != nil { log.Errorf("Unable to find path: %v", err) @@ -799,7 +906,7 @@ func findPaths(tx *bolt.Tx, graph *channeldb.ChannelGraph, // shortest path from the spur node to the destination. spurPath, err := findPath( tx, graph, nil, spurNode, target, - ignoredVertexes, ignoredEdges, amt, + ignoredVertexes, ignoredEdges, amt, feeLimit, bandwidthHints, ) diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 15fba7242..dfc6caaad 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -491,7 +491,12 @@ func TestFindLowestFeePath(t *testing.T) { // paths have equal total time locks, but the path through b has lower // fees (700 compared to 800 for the path through a). testChannels := []*testChannel{ - symmetricTestChannel("roasbeef", "a", 100000, &testChannelPolicy{ + symmetricTestChannel("roasbeef", "first", 100000, &testChannelPolicy{ + Expiry: 144, + FeeRate: 400, + MinHTLC: 1, + }), + symmetricTestChannel("first", "a", 100000, &testChannelPolicy{ Expiry: 144, FeeRate: 400, MinHTLC: 1, @@ -501,7 +506,7 @@ func TestFindLowestFeePath(t *testing.T) { FeeRate: 400, MinHTLC: 1, }), - symmetricTestChannel("roasbeef", "b", 100000, &testChannelPolicy{ + symmetricTestChannel("first", "b", 100000, &testChannelPolicy{ Expiry: 144, FeeRate: 100, MinHTLC: 1, @@ -537,7 +542,7 @@ func TestFindLowestFeePath(t *testing.T) { target := aliases["target"] path, err := findPath( nil, graph, nil, sourceNode, target, ignoredVertexes, - ignoredEdges, paymentAmt, nil, + ignoredEdges, paymentAmt, noFeeLimit, nil, ) if err != nil { t.Fatalf("unable to find path: %v", err) @@ -550,11 +555,11 @@ func TestFindLowestFeePath(t *testing.T) { } // Assert that the lowest fee route is returned. - if !bytes.Equal(route.Hops[0].Channel.Node.PubKeyBytes[:], + if !bytes.Equal(route.Hops[1].Channel.Node.PubKeyBytes[:], aliases["b"].SerializeCompressed()) { t.Fatalf("expected route to pass through b, "+ "but got a route through %v", - route.Hops[0].Channel.Node.Alias) + route.Hops[1].Channel.Node.Alias) } } @@ -566,28 +571,35 @@ type expectedHop struct { } type basicGraphPathFindingTestCase struct { - target string - paymentAmt btcutil.Amount - totalAmt lnwire.MilliSatoshi - totalTimeLock uint32 - expectedHops []expectedHop + target string + paymentAmt btcutil.Amount + feeLimit lnwire.MilliSatoshi + expectedTotalAmt lnwire.MilliSatoshi + expectedTotalTimeLock uint32 + expectedHops []expectedHop + expectFailureNoPath bool } var basicGraphPathFindingTests = []basicGraphPathFindingTestCase{ - // Basic route with one intermediate hop - {target: "sophon", paymentAmt: 100, totalTimeLock: 102, totalAmt: 100110, + // Basic route with one intermediate hop. + {target: "sophon", paymentAmt: 100, feeLimit: noFeeLimit, + expectedTotalTimeLock: 102, expectedTotalAmt: 100110, expectedHops: []expectedHop{ {alias: "songoku", fwdAmount: 100000, fee: 110, timeLock: 101}, {alias: "sophon", fwdAmount: 100000, fee: 0, timeLock: 101}, }}, - // Basic direct (one hop) route - {target: "luoji", paymentAmt: 100, totalTimeLock: 101, totalAmt: 100000, + + // Basic direct (one hop) route. + {target: "luoji", paymentAmt: 100, feeLimit: noFeeLimit, + expectedTotalTimeLock: 101, expectedTotalAmt: 100000, expectedHops: []expectedHop{ {alias: "luoji", fwdAmount: 100000, fee: 0, timeLock: 101}, }}, + // Three hop route where fees need to be added in to the forwarding amount. - // The high fee hop phamnewun should be avoided - {target: "elst", paymentAmt: 50000, totalTimeLock: 103, totalAmt: 50050210, + // The high fee hop phamnewun should be avoided. + {target: "elst", paymentAmt: 50000, feeLimit: noFeeLimit, + expectedTotalTimeLock: 103, expectedTotalAmt: 50050210, expectedHops: []expectedHop{ {alias: "songoku", fwdAmount: 50000200, fee: 50010, timeLock: 102}, {alias: "sophon", fwdAmount: 50000000, fee: 200, timeLock: 101}, @@ -598,12 +610,18 @@ var basicGraphPathFindingTests = []basicGraphPathFindingTestCase{ // songoku channel. Then there is no other option than to choose the // expensive phamnuwen channel. This test case was failing before // the route search was executed backwards. - {target: "elst", paymentAmt: 100000, totalTimeLock: 103, totalAmt: 110010220, + {target: "elst", paymentAmt: 100000, feeLimit: noFeeLimit, + expectedTotalTimeLock: 103, expectedTotalAmt: 110010220, expectedHops: []expectedHop{ {alias: "phamnuwen", fwdAmount: 100000200, fee: 10010020, timeLock: 102}, {alias: "sophon", fwdAmount: 100000000, fee: 200, timeLock: 101}, {alias: "elst", fwdAmount: 100000000, fee: 0, timeLock: 101}, - }}} + }}, + + // Basic route with fee limit. + {target: "sophon", paymentAmt: 100, feeLimit: 50, + expectFailureNoPath: true, + }} func TestBasicGraphPathFinding(t *testing.T) { t.Parallel() @@ -650,14 +668,20 @@ func testBasicGraphPathFindingCase(t *testing.T, graph *channeldb.ChannelGraph, target := aliases[test.target] path, err := findPath( nil, graph, nil, sourceNode, target, ignoredVertexes, - ignoredEdges, paymentAmt, nil, + ignoredEdges, paymentAmt, test.feeLimit, nil, ) + if test.expectFailureNoPath { + if err == nil { + t.Fatal("expected no path to be found") + } + return + } if err != nil { t.Fatalf("unable to find path: %v", err) } route, err := newRoute( - paymentAmt, noFeeLimit, sourceVertex, path, startingHeight, + paymentAmt, test.feeLimit, sourceVertex, path, startingHeight, finalHopCLTV, ) if err != nil { @@ -707,7 +731,7 @@ func testBasicGraphPathFindingCase(t *testing.T, graph *channeldb.ChannelGraph, exitHop[:], hopPayloads[lastHopIndex].NextAddress) } - var expectedTotalFee lnwire.MilliSatoshi = 0 + var expectedTotalFee lnwire.MilliSatoshi for i := 0; i < expectedHopCount; i++ { // We'll ensure that the amount to forward, and fees // computed for each hop are correct. @@ -736,13 +760,13 @@ func testBasicGraphPathFindingCase(t *testing.T, graph *channeldb.ChannelGraph, expectedTotalFee += expectedHops[i].fee } - if route.TotalAmount != test.totalAmt { + if route.TotalAmount != test.expectedTotalAmt { t.Fatalf("total amount incorrect: "+ "expected %v, got %v", - test.totalAmt, route.TotalAmount) + test.expectedTotalAmt, route.TotalAmount) } - if route.TotalTimeLock != test.totalTimeLock { + if route.TotalTimeLock != test.expectedTotalTimeLock { t.Fatalf("expected time lock of %v, instead have %v", 2, route.TotalTimeLock) } @@ -829,7 +853,7 @@ func TestPathFindingWithAdditionalEdges(t *testing.T) { // We should now be able to find a path from roasbeef to doge. path, err := findPath( nil, graph, additionalEdges, sourceNode, dogePubKey, nil, nil, - paymentAmt, nil, + paymentAmt, noFeeLimit, nil, ) if err != nil { t.Fatalf("unable to find private path to doge: %v", err) @@ -865,7 +889,7 @@ func TestKShortestPathFinding(t *testing.T) { paymentAmt := lnwire.NewMSatFromSatoshis(100) target := aliases["luoji"] paths, err := findPaths( - nil, graph, sourceNode, target, paymentAmt, 100, + nil, graph, sourceNode, target, paymentAmt, noFeeLimit, 100, nil, ) if err != nil { @@ -916,7 +940,7 @@ func TestNewRoute(t *testing.T) { FeeBaseMSat: baseFee, TimeLockDelta: timeLockDelta, }, - Capacity: capacity, + Bandwidth: lnwire.NewMSatFromSatoshis(capacity), } } @@ -1148,7 +1172,7 @@ func TestNewRoutePathTooLong(t *testing.T) { target := aliases["ursula"] _, err = findPath( nil, graph, nil, sourceNode, target, ignoredVertexes, - ignoredEdges, paymentAmt, nil, + ignoredEdges, paymentAmt, noFeeLimit, nil, ) if err != nil { t.Fatalf("path should have been found") @@ -1159,7 +1183,7 @@ func TestNewRoutePathTooLong(t *testing.T) { target = aliases["vincent"] path, err := findPath( nil, graph, nil, sourceNode, target, ignoredVertexes, - ignoredEdges, paymentAmt, nil, + ignoredEdges, paymentAmt, noFeeLimit, nil, ) if err == nil { t.Fatalf("should not have been able to find path, supposed to be "+ @@ -1201,7 +1225,7 @@ func TestPathNotAvailable(t *testing.T) { _, err = findPath( nil, graph, nil, sourceNode, unknownNode, ignoredVertexes, - ignoredEdges, 100, nil, + ignoredEdges, 100, noFeeLimit, nil, ) if !IsError(err, ErrNoPathFound) { t.Fatalf("path shouldn't have been found: %v", err) @@ -1237,7 +1261,7 @@ func TestPathInsufficientCapacity(t *testing.T) { payAmt := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin) _, err = findPath( nil, graph, nil, sourceNode, target, ignoredVertexes, - ignoredEdges, payAmt, nil, + ignoredEdges, payAmt, noFeeLimit, nil, ) if !IsError(err, ErrNoPathFound) { t.Fatalf("graph shouldn't be able to support payment: %v", err) @@ -1269,7 +1293,7 @@ func TestRouteFailMinHTLC(t *testing.T) { payAmt := lnwire.MilliSatoshi(10) _, err = findPath( nil, graph, nil, sourceNode, target, ignoredVertexes, - ignoredEdges, payAmt, nil, + ignoredEdges, payAmt, noFeeLimit, nil, ) if !IsError(err, ErrNoPathFound) { t.Fatalf("graph shouldn't be able to support payment: %v", err) @@ -1298,10 +1322,10 @@ func TestRouteFailDisabledEdge(t *testing.T) { // First, we'll try to route from roasbeef -> sophon. This should // succeed without issue, and return a single path via phamnuwen target := aliases["sophon"] - payAmt := lnwire.NewMSatFromSatoshis(120000) + payAmt := lnwire.NewMSatFromSatoshis(105000) _, err = findPath( nil, graph, nil, sourceNode, target, ignoredVertexes, - ignoredEdges, payAmt, nil, + ignoredEdges, payAmt, noFeeLimit, nil, ) if err != nil { t.Fatalf("unable to find path: %v", err) @@ -1322,7 +1346,7 @@ func TestRouteFailDisabledEdge(t *testing.T) { // failure as it is no longer eligible. _, err = findPath( nil, graph, nil, sourceNode, target, ignoredVertexes, - ignoredEdges, payAmt, nil, + ignoredEdges, payAmt, noFeeLimit, nil, ) if !IsError(err, ErrNoPathFound) { t.Fatalf("graph shouldn't be able to support payment: %v", err) @@ -1353,7 +1377,7 @@ func TestRouteExceededFeeLimit(t *testing.T) { amt := lnwire.NewMSatFromSatoshis(100) path, err := findPath( nil, graph, nil, sourceNode, target, ignoredVertices, - ignoredEdges, amt, nil, + ignoredEdges, amt, noFeeLimit, nil, ) if err != nil { t.Fatalf("unable to find path from roasbeef to phamnuwen for "+ diff --git a/routing/router.go b/routing/router.go index dd4d4d821..0e76301e2 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1384,7 +1384,7 @@ func (r *ChannelRouter) FindRoutes(target *btcec.PublicKey, // 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, numPaths, + tx, r.cfg.Graph, r.selfNode, target, amt, feeLimit, numPaths, bandwidthHints, ) if err != nil { diff --git a/routing/router_test.go b/routing/router_test.go index 7d7312fb8..0ce7a97e2 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -1687,7 +1687,7 @@ func TestFindPathFeeWeighting(t *testing.T) { // path even though the direct path has a higher potential time lock. path, err := findPath( nil, ctx.graph, nil, sourceNode, target, ignoreVertex, - ignoreEdge, amt, nil, + ignoreEdge, amt, noFeeLimit, nil, ) if err != nil { t.Fatalf("unable to find path: %v", err) diff --git a/rpcserver.go b/rpcserver.go index faee7607e..bccf05e41 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -3264,7 +3264,7 @@ func marshallRoute(route *routing.Route) *lnrpc.Route { for i, hop := range route.Hops { resp.Hops[i] = &lnrpc.Hop{ ChanId: hop.Channel.ChannelID, - ChanCapacity: int64(hop.Channel.Capacity), + ChanCapacity: int64(hop.Channel.Bandwidth.ToSatoshis()), AmtToForward: int64(hop.AmtToForward.ToSatoshis()), AmtToForwardMsat: int64(hop.AmtToForward), Fee: int64(hop.Fee.ToSatoshis()), @@ -3314,8 +3314,9 @@ func unmarshallRoute(rpcroute *lnrpc.Route, routingHop := &routing.ChannelHop{ ChannelEdgePolicy: channelEdgePolicy, - Capacity: btcutil.Amount(hop.ChanCapacity), - Chain: edgeInfo.ChainHash, + Bandwidth: lnwire.NewMSatFromSatoshis( + btcutil.Amount(hop.ChanCapacity)), + Chain: edgeInfo.ChainHash, } route.Hops[i] = &routing.Hop{