From f64f98b150929616b28117abee61b0f96534fba3 Mon Sep 17 00:00:00 2001 From: Yaacov Akiba Slama Date: Wed, 23 Sep 2020 16:34:43 +0300 Subject: [PATCH] Try to send the largest amount from a single channel --- routing/pathfind.go | 23 ++++++++++++--- routing/payment_session.go | 59 +++++++++++++++++++++++++++---------- routing/unified_policies.go | 2 +- 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/routing/pathfind.go b/routing/pathfind.go index cfc7afaed9..d0c8dfca8c 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -480,6 +480,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // balance available. self := g.graph.sourceNode() + var splitNeeded bool + if source == self { max, total, err := getOutgoingBalance( self, outgoingChanMap, g.bandwidthHints, g.graph, @@ -497,7 +499,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // If there is only not enough capacity on a single route, it // may still be possible to complete the payment by splitting. if max < amt { - return nil, errNoPathFound + splitNeeded = true + amt = max } } @@ -832,8 +835,12 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, continue } + var amtForPolicy lnwire.MilliSatoshi + if !splitNeeded || fromNode != source { + amtForPolicy = amtToSend + } policy := unifiedPolicy.getPolicy( - amtToSend, g.bandwidthHints, + amtForPolicy, g.bandwidthHints, ) if policy == nil { @@ -876,6 +883,9 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // target. var pathEdges []*channeldb.ChannelEdgePolicy currentNode := source + if n, ok := distance[source]; ok && splitNeeded { + n.amountToReceive = amt + } for { // Determine the next hop forward using the next map. currentNodeWithDist, ok := distance[currentNode] @@ -895,6 +905,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // to source. if currentNode == target { break + } else if splitNeeded { + amt -= (currentNodeWithDist.nextHop).ComputeFeeFromIncoming(amt) } } @@ -914,8 +926,11 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, log.Debugf("Found route: probability=%v, hops=%v, fee=%v", distance[source].probability, len(pathEdges), distance[source].amountToReceive-amt) - - return pathEdges, nil + var e error + if splitNeeded { + e = extendedNoRouteError{err: errNoPathFound, amt: amt} + } + return pathEdges, e } // getProbabilityBasedDist converts a weight into a distance that takes into diff --git a/routing/payment_session.go b/routing/payment_session.go index 2d7ef8bf89..722efec2d4 100644 --- a/routing/payment_session.go +++ b/routing/payment_session.go @@ -1,6 +1,7 @@ package routing import ( + "errors" "fmt" "github.com/btcsuite/btclog" @@ -47,6 +48,11 @@ const ( errMissingDependentFeature ) +type extendedNoRouteError struct { + err noRouteError + amt lnwire.MilliSatoshi +} + var ( // DefaultShardMinAmt is the default amount beyond which we won't try to // further split the payment if no route is found. It is the minimum @@ -83,6 +89,20 @@ func (e noRouteError) Error() string { } } +// Error returns the string representation of the extendedNoRouteError +func (e extendedNoRouteError) Error() string { + switch e.err { + case errNoPathFound: + return fmt.Sprintf("unable to find a path to destination. Try to lower the amount to: %v", e.amt) + default: + return e.err.Error() + } +} + +func (e extendedNoRouteError) Unwrap() error { + return e.err +} + // FailureReason converts a path finding error into a payment-level failure. func (e noRouteError) FailureReason() channeldb.FailureReason { switch e { @@ -104,6 +124,11 @@ func (e noRouteError) FailureReason() channeldb.FailureReason { } } +// FailureReason converts a path finding error into a payment-level failure. +func (e extendedNoRouteError) FailureReason() channeldb.FailureReason { + return e.err.FailureReason() +} + // PaymentSession is used during SendPayment attempts to provide routes to // attempt. It also defines methods to give the PaymentSession additional // information learned during the previous attempts. @@ -268,7 +293,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi, cleanup() switch { - case err == errNoPathFound: + case errors.Is(err, errNoPathFound): // Don't split if this is a legacy payment without mpp // record. if p.payment.PaymentAddr == nil { @@ -288,22 +313,26 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi, return nil, errNoPathFound } - // This is where the magic happens. If we can't find a - // route, try it for half the amount. - maxAmt /= 2 - - // Put a lower bound on the minimum shard size. - if maxAmt < p.minShardAmt { - p.log.Debugf("not splitting because minimum "+ - "shard amount %v has been reached", - p.minShardAmt) - - return nil, errNoPathFound + var e extendedNoRouteError + if errors.As(err, &e) { + maxAmt = e.amt + } else { + // This is where the magic happens. If we can't find a + // route, try it for half the amount. + maxAmt /= 2 + + // Put a lower bound on the minimum shard size. + if maxAmt < p.minShardAmt { + p.log.Debugf("not splitting because minimum "+ + "shard amount %v has been reached", + p.minShardAmt) + + return nil, errNoPathFound + } + // Go pathfinding. + continue } - // Go pathfinding. - continue - // If there isn't enough local bandwidth, there is no point in // splitting. It won't be possible to create a complete set in // any case, but the sent out partial payments would be held by diff --git a/routing/unified_policies.go b/routing/unified_policies.go index 0ff509382e..a1f95a379b 100644 --- a/routing/unified_policies.go +++ b/routing/unified_policies.go @@ -160,7 +160,7 @@ func (u *unifiedPolicy) getPolicyLocal(amt lnwire.MilliSatoshi, for _, edge := range u.edges { // Check valid amount range for the channel. - if !edge.amtInRange(amt) { + if amt > 0 && !edge.amtInRange(amt) { continue }