diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index a372af521cd..3fc9f60345a 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -298,7 +298,7 @@ func (r *RouterBackend) QueryRoutes(ctx context.Context, if err != nil { return nil, err } - restrictions.LastHop = &lastHop + restrictions.LastHops = []route.Vertex{lastHop} } // If we have any TLV records destined for the final hop, then we'll diff --git a/lnrpc/routerrpc/router_backend_test.go b/lnrpc/routerrpc/router_backend_test.go index a5b629fe208..699bfa80f54 100644 --- a/lnrpc/routerrpc/router_backend_test.go +++ b/lnrpc/routerrpc/router_backend_test.go @@ -160,9 +160,7 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool, t.Fatal("expecting 0% probability for ignored pair") } - if *restrictions.LastHop != lastHop { - t.Fatal("unexpected last hop") - } + require.Equal(t, []route.Vertex{lastHop}, restrictions.LastHops) if restrictions.OutgoingChannelIDs[0] != outgoingChan { t.Fatal("unexpected outgoing channel id") diff --git a/routing/heap.go b/routing/heap.go index 36563bb661f..42452e22342 100644 --- a/routing/heap.go +++ b/routing/heap.go @@ -44,6 +44,9 @@ type nodeWithDist struct { // routingInfoSize is the total size requirement for the payloads field // in the onion packet from this hop towards the final destination. routingInfoSize uint64 + + // destinationDistance is the number of hops to the destination. + destinationDistance int } // distanceHeap is a min-distance heap that's used within our path finding diff --git a/routing/pathfind.go b/routing/pathfind.go index a92d0a7e52c..fd0a365c78b 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -316,9 +316,10 @@ type RestrictParams struct { // first hop. If nil, any channel may be used. OutgoingChannelIDs []uint64 - // LastHop is the pubkey of the last node before the final destination - // is reached. If nil, any node may be used. - LastHop *route.Vertex + // LastHops are the pubkeys of the last nodes before the final + // destination is reached. If empty, any node may be used. The ordering + // is from the final node backwards. + LastHops []route.Vertex // CltvLimit is the maximum time lock of the route excluding the final // ctlv. After path finding is complete, the caller needs to increase @@ -774,14 +775,15 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // The new better distance is recorded, and also our "next hop" // map is populated with this edge. withDist := &nodeWithDist{ - dist: tempDist, - weight: tempWeight, - node: fromVertex, - amountToReceive: amountToReceive, - incomingCltv: incomingCltv, - probability: probability, - nextHop: edge, - routingInfoSize: routingInfoSize, + dist: tempDist, + weight: tempWeight, + node: fromVertex, + amountToReceive: amountToReceive, + incomingCltv: incomingCltv, + probability: probability, + nextHop: edge, + routingInfoSize: routingInfoSize, + destinationDistance: toNodeDist.destinationDistance + 1, } distance[fromVertex] = withDist @@ -868,8 +870,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, } // Apply last hop restriction if set. - if r.LastHop != nil && - pivot == target && fromNode != *r.LastHop { + if partialPath.destinationDistance < len(r.LastHops) && + fromNode != r.LastHops[partialPath.destinationDistance] { continue } diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 7f576196a4e..dcc06f9af10 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -2500,7 +2500,7 @@ func runRestrictLastHop(t *testing.T, useCache bool) { // Find the best path given the restriction to use b as the last hop. // This should force pathfinding to not take the lowest cost option. - ctx.restrictParams.LastHop = &lastHop + ctx.restrictParams.LastHops = []route.Vertex{lastHop} path, err := ctx.findPath(target, paymentAmt) require.NoError(t, err, "unable to find path") if path[0].ChannelID != 3 { @@ -2953,8 +2953,7 @@ func runRouteToSelf(t *testing.T, useCache bool) { outgoingChanID := uint64(1) lastHop := ctx.keyFromAlias("b") ctx.restrictParams.OutgoingChannelIDs = []uint64{outgoingChanID} - ctx.restrictParams.LastHop = &lastHop - + ctx.restrictParams.LastHops = []route.Vertex{lastHop} // Find the best path to self given that we want to go out via channel 1 // and return through node b. path, err = ctx.findPath(target, paymentAmt) diff --git a/routing/payment_session.go b/routing/payment_session.go index f3092ee7b4f..e54de3fc53c 100644 --- a/routing/payment_session.go +++ b/routing/payment_session.go @@ -248,11 +248,16 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi, // Taking into account this prune view, we'll attempt to locate a path // to our destination, respecting the recommendations from // MissionControl. + var lastHops []route.Vertex + if p.payment.LastHop != nil { + lastHops = []route.Vertex{*p.payment.LastHop} + } + restrictions := &RestrictParams{ ProbabilitySource: p.missionControl.GetProbability, FeeLimit: feeLimit, OutgoingChannelIDs: p.payment.OutgoingChannelIDs, - LastHop: p.payment.LastHop, + LastHops: lastHops, CltvLimit: cltvLimit, DestCustomRecords: p.payment.DestCustomRecords, DestFeatures: p.payment.DestFeatures, diff --git a/routing/router.go b/routing/router.go index 348914db733..6bb25288cae 100644 --- a/routing/router.go +++ b/routing/router.go @@ -4,6 +4,7 @@ import ( "bytes" goErrors "errors" "fmt" + "math" "runtime" "strings" "sync" @@ -1793,10 +1794,11 @@ func (r *ChannelRouter) FindRoute(source, target route.Vertex, route, err := newRoute( source, path, uint32(currentHeight), finalHopParams{ - amt: amt, - totalAmt: amt, - cltvDelta: finalExpiry, - records: destCustomRecords, + amt: amt, + totalAmt: amt, + cltvDelta: finalExpiry, + records: destCustomRecords, + paymentAddr: restrictions.PaymentAddr, }, ) if err != nil { @@ -2699,147 +2701,40 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi, hops []route.Vertex, outgoingChan *uint64, finalCltvDelta int32, payAddr *[32]byte) (*route.Route, error) { - log.Tracef("BuildRoute called: hopsCount=%v, amt=%v", - len(hops), amt) - - var outgoingChans map[uint64]struct{} - if outgoingChan != nil { - outgoingChans = map[uint64]struct{}{ - *outgoingChan: {}, - } - } - - // If no amount is specified, we need to build a route for the minimum - // amount that this route can carry. - useMinAmt := amt == nil - - // We'll attempt to obtain a set of bandwidth hints that helps us select - // the best outgoing channel to use in case no outgoing channel is set. - bandwidthHints, err := newBandwidthManager( - r.cachedGraph, r.selfNode.PubKeyBytes, r.cfg.GetLink, - ) - if err != nil { - return nil, err + // TODO: Allow min amount route building via path finding. + if amt == nil { + return nil, errors.New("Not implemented") } - // Fetch the current block height outside the routing transaction, to - // prevent the rpc call blocking the database. - _, height, err := r.cfg.Chain.GetBestBlock() - if err != nil { - return nil, err + // Build last hops restriction all the way back to the source. + var lastHops []route.Vertex + target := hops[len(hops)-1] + for i := len(hops) - 2; i >= 0; i-- { + lastHops = append(lastHops, hops[i]) } + lastHops = append(lastHops, r.selfNode.PubKeyBytes) - // Allocate a list that will contain the unified policies for this - // route. - edges := make([]*unifiedPolicy, len(hops)) + restrictions := &RestrictParams{ + ProbabilitySource: func(fromNode, toNode route.Vertex, + amt lnwire.MilliSatoshi) float64 { - var runningAmt lnwire.MilliSatoshi - if useMinAmt { - // For minimum amount routes, aim to deliver at least 1 msat to - // the destination. There are nodes in the wild that have a - // min_htlc channel policy of zero, which could lead to a zero - // amount payment being made. - runningAmt = 1 - } else { - // If an amount is specified, we need to build a route that - // delivers exactly this amount to the final destination. - runningAmt = *amt + return 1 + }, + FeeLimit: lnwire.MaxMilliSatoshi, + CltvLimit: math.MaxUint32, + PaymentAddr: payAddr, + LastHops: lastHops, } - // Traverse hops backwards to accumulate fees in the running amounts. - source := r.selfNode.PubKeyBytes - for i := len(hops) - 1; i >= 0; i-- { - toNode := hops[i] - - var fromNode route.Vertex - if i == 0 { - fromNode = source - } else { - fromNode = hops[i-1] - } - - localChan := i == 0 - - // Build unified policies for this hop based on the channels - // known in the graph. - u := newUnifiedPolicies(source, toNode, outgoingChans) - - err := u.addGraphPolicies(r.cachedGraph) - if err != nil { - return nil, err - } - - // Exit if there are no channels. - unifiedPolicy, ok := u.policies[fromNode] - if !ok { - return nil, ErrNoChannel{ - fromNode: fromNode, - position: i, - } - } - - // If using min amt, increase amt if needed. - if useMinAmt { - min := unifiedPolicy.minAmt() - if min > runningAmt { - runningAmt = min - } - } - - // Get a forwarding policy for the specific amount that we want - // to forward. - policy := unifiedPolicy.getPolicy(runningAmt, bandwidthHints) - if policy == nil { - return nil, ErrNoChannel{ - fromNode: fromNode, - position: i, - } - } - - // Add fee for this hop. - if !localChan { - runningAmt += policy.ComputeFee(runningAmt) - } - - log.Tracef("Select channel %v at position %v", policy.ChannelID, i) - - edges[i] = unifiedPolicy + if outgoingChan != nil { + restrictions.OutgoingChannelIDs = []uint64{*outgoingChan} } - // Now that we arrived at the start of the route and found out the route - // total amount, we make a forward pass. Because the amount may have - // been increased in the backward pass, fees need to be recalculated and - // amount ranges re-checked. - var pathEdges []*channeldb.CachedEdgePolicy - receiverAmt := runningAmt - for i, edge := range edges { - policy := edge.getPolicy(receiverAmt, bandwidthHints) - if policy == nil { - return nil, ErrNoChannel{ - fromNode: hops[i-1], - position: i, - } - } + log.Tracef("BuildRoute called: hopsCount=%v, amt=%v, target=%v", + len(hops), amt, target) - if i > 0 { - // Decrease the amount to send while going forward. - receiverAmt -= policy.ComputeFeeFromIncoming( - receiverAmt, - ) - } - - pathEdges = append(pathEdges, policy) - } - - // Build and return the final route. - return newRoute( - source, pathEdges, uint32(height), - finalHopParams{ - amt: receiverAmt, - totalAmt: receiverAmt, - cltvDelta: uint16(finalCltvDelta), - records: nil, - paymentAddr: payAddr, - }, + return r.FindRoute( + r.selfNode.PubKeyBytes, target, *amt, 0, + restrictions, nil, nil, uint16(finalCltvDelta), ) } diff --git a/routing/router_test.go b/routing/router_test.go index 42683fc9c37..5cf606c1c8f 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -3001,7 +3001,10 @@ func TestBuildRoute(t *testing.T) { // Setup a three node network. chanCapSat := btcutil.Amount(100000) paymentAddrFeatures := lnwire.NewFeatureVector( - lnwire.NewRawFeatureVector(lnwire.PaymentAddrOptional), + lnwire.NewRawFeatureVector( + lnwire.PaymentAddrOptional, + lnwire.TLVOnionPayloadOptional, + ), lnwire.Features, ) testChannels := []*testChannel{ @@ -3115,42 +3118,7 @@ func TestBuildRoute(t *testing.T) { t.Fatalf("unexpected total amount %v", rt.TotalAmount) } - // Build the route for the minimum amount. - rt, err = ctx.router.BuildRoute( - nil, hops, nil, 40, &payAddr, - ) - if err != nil { - t.Fatal(err) - } - - // Check that we get the expected route back. The minimum that we can - // send from b to c is 20 sats. Hop b charges 1200 msat for the - // forwarding. The channel between hop a and b can carry amounts in the - // range [5, 100], so 21200 msats is the minimum amount for this route. - checkHops(rt, []uint64{1, 7}, payAddr) - if rt.TotalAmount != 21200 { - t.Fatalf("unexpected total amount %v", rt.TotalAmount) - } - - // Test a route that contains incompatible channel htlc constraints. - // There is no amount that can pass through both channel 5 and 4. - hops = []route.Vertex{ - ctx.aliases["e"], ctx.aliases["c"], - } - _, err = ctx.router.BuildRoute( - nil, hops, nil, 40, nil, - ) - errNoChannel, ok := err.(ErrNoChannel) - if !ok { - t.Fatalf("expected incompatible policies error, but got %v", - err) - } - if errNoChannel.position != 0 { - t.Fatalf("unexpected no channel error position") - } - if errNoChannel.fromNode != ctx.aliases["a"] { - t.Fatalf("unexpected no channel error node") - } + // TODO: Min amount test. } // edgeCreationModifier is an enum-like type used to modify steps that are