Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lnrpc/routerrpc/router_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions lnrpc/routerrpc/router_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 3 additions & 0 deletions routing/heap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 15 additions & 13 deletions routing/pathfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 2 additions & 3 deletions routing/pathfind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion routing/payment_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
167 changes: 31 additions & 136 deletions routing/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
goErrors "errors"
"fmt"
"math"
"runtime"
"strings"
"sync"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The required fix is to let pathfinding increase the running amount to match min_htlc. Useful independently too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#6927 explores the idea

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),
)
}
42 changes: 5 additions & 37 deletions routing/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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
Expand Down