routerrpc+routing: adapt payment session for multi shard send

Modifies the payment session to launch additional pathfinding attempts
for lower amounts. If a single shot payment isn't possible, the goal is
to try to complete the payment using multiple htlcs. In previous
commits, the payment lifecycle has been prepared to deal with
partial-amount routes returned from the payment session. It will query
for additional shards if needed.

Additionally a new rpc payment parameter is added that controls the
maximum number of shards that will be used for the payment.
This commit is contained in:
Joost Jager committed 2020-04-09 08:20:49 +02:00
1 parent 46f5fc7400
commit e9bd691e6a
10 files changed
+520 -197

No files matched your search

+93 -51
View File
@@ -31,6 +31,13 @@ const (
errEmptyPaySession
)
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
// amount that we use as the shard size when splitting.
DefaultShardMinAmt = lnwire.NewMSatFromSatoshis(10000)
)
// Error returns the string representation of the noRouteError
func (e noRouteError) Error() string {
switch e {
@@ -111,6 +118,12 @@ type paymentSession struct {
pathFindingConfig PathFindingConfig
missionControl MissionController
// minShardAmt is the amount beyond which we won't try to further split
// the payment if no route is found. If the maximum number of htlcs
// specified in the payment is one, under no circumstances splitting
// will happen and this value remains unused.
minShardAmt lnwire.MilliSatoshi
}
// RequestRoute returns a route which is likely to be capable for successfully
@@ -155,58 +168,87 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
PaymentAddr: p.payment.PaymentAddr,
}
// We'll also obtain a set of bandwidthHints from the lower layer for
// each of our outbound channels. This will allow the path finding to
// skip any links that aren't active or just don't have enough bandwidth
// to carry the payment. New bandwidth hints are queried for every new
// path finding attempt, because concurrent payments may change
// balances.
bandwidthHints, err := p.getBandwidthHints()
if err != nil {
return nil, err
}
finalHtlcExpiry := int32(height) + int32(finalCltvDelta)
routingGraph, cleanup, err := p.getRoutingGraph()
if err != nil {
return nil, err
for {
// We'll also obtain a set of bandwidthHints from the lower
// layer for each of our outbound channels. This will allow the
// path finding to skip any links that aren't active or just
// don't have enough bandwidth to carry the payment. New
// bandwidth hints are queried for every new path finding
// attempt, because concurrent payments may change balances.
bandwidthHints, err := p.getBandwidthHints()
if err != nil {
return nil, err
}
log.Debugf("PaymentSession for %x: trying pathfinding with %v",
p.payment.PaymentHash, maxAmt)
// Get a routing graph.
routingGraph, cleanup, err := p.getRoutingGraph()
if err != nil {
return nil, err
}
sourceVertex := routingGraph.sourceNode()
// Find a route for the current amount.
path, err := p.pathFinder(
&graphParams{
additionalEdges: p.additionalEdges,
bandwidthHints: bandwidthHints,
graph: routingGraph,
},
restrictions, &p.pathFindingConfig,
sourceVertex, p.payment.Target,
maxAmt, finalHtlcExpiry,
)
// Close routing graph.
cleanup()
switch {
case err == errNoPathFound:
// No splitting if this is the last shard.
isLastShard := activeShards+1 >= p.payment.MaxHtlcs
if isLastShard {
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 {
return nil, errNoPathFound
}
// Go pathfinding.
continue
case err != nil:
return nil, err
}
// With the next candidate path found, we'll attempt to turn
// this into a route by applying the time-lock and fee
// requirements.
route, err := newRoute(
sourceVertex, path, height,
finalHopParams{
amt: maxAmt,
totalAmt: p.payment.Amount,
cltvDelta: finalCltvDelta,
records: p.payment.DestCustomRecords,
paymentAddr: p.payment.PaymentAddr,
},
)
if err != nil {
return nil, err
}
return route, err
}
defer cleanup()
sourceVertex := routingGraph.sourceNode()
path, err := p.pathFinder(
&graphParams{
additionalEdges: p.additionalEdges,
bandwidthHints: bandwidthHints,
graph: routingGraph,
},
restrictions, &p.pathFindingConfig,
sourceVertex, p.payment.Target,
maxAmt, finalHtlcExpiry,
)
if err != nil {
return nil, err
}
// With the next candidate path found, we'll attempt to turn this into
// a route by applying the time-lock and fee requirements.
route, err := newRoute(
sourceVertex, path, height,
finalHopParams{
amt: maxAmt,
totalAmt: maxAmt,
cltvDelta: finalCltvDelta,
records: p.payment.DestCustomRecords,
paymentAddr: p.payment.PaymentAddr,
},
)
if err != nil {
// TODO(roasbeef): return which edge/vertex didn't work
// out
return nil, err
}
return route, err
}