Merge pull request #4198 from joostjager/mpp-precheck

routing: payment splitting pre-check
This commit is contained in:
Olaoluwa Osuntokun 2020-04-20 14:10:17 -07:00 committed by GitHub
commit 3b8ddece41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 76 additions and 27 deletions

@ -116,9 +116,6 @@ const (
// FailureReasonInsufficientBalance indicates that we didn't have enough
// balance to complete the payment.
//
// This reason isn't assigned anymore, but may still exist for older
// payments.
FailureReasonInsufficientBalance FailureReason = 4
// TODO(halseth): cancel state.

@ -107,7 +107,7 @@ func onePathGraph(g *mockGraph) {
g.addChannel(chanIm1Target, targetNodeID, im1NodeID, 100000)
}
func twoPathGraph(g *mockGraph) {
func twoPathGraph(g *mockGraph, capacityOut, capacityIn btcutil.Amount) {
// Create the following network of nodes:
// source -> intermediate1 -> target
// source -> intermediate2 -> target
@ -120,10 +120,10 @@ func twoPathGraph(g *mockGraph) {
intermediate2 := newMockNode(im2NodeID)
g.addNode(intermediate2)
g.addChannel(chanSourceIm1, sourceNodeID, im1NodeID, 200000)
g.addChannel(chanSourceIm2, sourceNodeID, im2NodeID, 200000)
g.addChannel(chanIm1Target, targetNodeID, im1NodeID, 100000)
g.addChannel(chanIm2Target, targetNodeID, im2NodeID, 100000)
g.addChannel(chanSourceIm1, sourceNodeID, im1NodeID, capacityOut)
g.addChannel(chanSourceIm2, sourceNodeID, im2NodeID, capacityOut)
g.addChannel(chanIm1Target, targetNodeID, im1NodeID, capacityIn)
g.addChannel(chanIm2Target, targetNodeID, im2NodeID, capacityIn)
}
var mppTestCases = []mppSendTestCase{
@ -136,8 +136,10 @@ var mppTestCases = []mppSendTestCase{
// too. Mpp payment complete.
{
name: "sufficient inbound",
graph: twoPathGraph,
name: "sufficient inbound",
graph: func(g *mockGraph) {
twoPathGraph(g, 200000, 100000)
},
amt: 70000,
expectedAttempts: 5,
expectedSuccesses: []expectedHtlcSuccess{
@ -155,8 +157,10 @@ var mppTestCases = []mppSendTestCase{
// Test that a cap on the max htlcs makes it impossible to pay.
{
name: "no splitting",
graph: twoPathGraph,
name: "no splitting",
graph: func(g *mockGraph) {
twoPathGraph(g, 200000, 100000)
},
amt: 70000,
expectedAttempts: 2,
expectedSuccesses: []expectedHtlcSuccess{},
@ -188,6 +192,19 @@ var mppTestCases = []mppSendTestCase{
expectedFailure: true,
maxShards: 1000,
},
// Test that no attempts are made if the total local balance is
// insufficient.
{
name: "insufficient total balance",
graph: func(g *mockGraph) {
twoPathGraph(g, 100000, 500000)
},
amt: 300000,
expectedAttempts: 0,
expectedFailure: true,
maxShards: 10,
},
}
// TestMppSend tests that a payment can be completed using multiple shards.

@ -326,13 +326,14 @@ type PathFindingConfig struct {
MinProbability float64
}
// getMaxOutgoingAmt returns the maximum available balance in any of the
// channels of the given node.
func getMaxOutgoingAmt(node route.Vertex, outgoingChan *uint64,
// getOutgoingBalance returns the maximum available balance in any of the
// channels of the given node. The second return parameters is the total
// available balance.
func getOutgoingBalance(node route.Vertex, outgoingChan *uint64,
bandwidthHints map[uint64]lnwire.MilliSatoshi,
g routingGraph) (lnwire.MilliSatoshi, error) {
g routingGraph) (lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) {
var max lnwire.MilliSatoshi
var max, total lnwire.MilliSatoshi
cb := func(edgeInfo *channeldb.ChannelEdgeInfo, outEdge,
_ *channeldb.ChannelEdgePolicy) error {
@ -349,26 +350,30 @@ func getMaxOutgoingAmt(node route.Vertex, outgoingChan *uint64,
bandwidth, ok := bandwidthHints[chanID]
// If the bandwidth is not available for whatever reason, don't
// fail the pathfinding early.
// If the bandwidth is not available, use the channel capacity.
// This can happen when a channel is added to the graph after
// we've already queried the bandwidth hints.
if !ok {
max = lnwire.MaxMilliSatoshi
return nil
bandwidth = lnwire.NewMSatFromSatoshis(
edgeInfo.Capacity,
)
}
if bandwidth > max {
max = bandwidth
}
total += bandwidth
return nil
}
// Iterate over all channels of the to node.
err := g.forEachNodeChannel(node, cb)
if err != nil {
return 0, err
return 0, 0, err
}
return max, err
return max, total, err
}
// findPath attempts to find a path from the source node within the ChannelGraph
@ -447,12 +452,21 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
self := g.graph.sourceNode()
if source == self {
max, err := getMaxOutgoingAmt(
max, total, err := getOutgoingBalance(
self, r.OutgoingChannelID, g.bandwidthHints, g.graph,
)
if err != nil {
return nil, err
}
// If the total outgoing balance isn't sufficient, it will be
// impossible to complete the payment.
if total < amt {
return nil, errInsufficientBalance
}
// 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
}

@ -1768,7 +1768,7 @@ func TestPathInsufficientCapacity(t *testing.T) {
noRestrictions, testPathFindingConfig,
sourceNode.PubKeyBytes, target, payAmt, 0,
)
if err != errNoPathFound {
if err != errInsufficientBalance {
t.Fatalf("graph shouldn't be able to support payment: %v", err)
}
}
@ -2790,6 +2790,7 @@ type pathFindingTestContext struct {
t *testing.T
graph *channeldb.ChannelGraph
restrictParams RestrictParams
bandwidthHints map[uint64]lnwire.MilliSatoshi
pathFindingConfig PathFindingConfig
testGraphInstance *testGraphInstance
source route.Vertex
@ -2844,8 +2845,8 @@ func (c *pathFindingTestContext) findPath(target route.Vertex,
error) {
return dbFindPath(
c.graph, nil, nil, &c.restrictParams, &c.pathFindingConfig,
c.source, target, amt, 0,
c.graph, nil, c.bandwidthHints, &c.restrictParams,
&c.pathFindingConfig, c.source, target, amt, 0,
)
}

@ -30,6 +30,10 @@ const (
// not exist in the graph.
errNoPathFound
// errInsufficientLocalBalance is returned when none of the local
// channels have enough balance for the payment.
errInsufficientBalance
// errEmptyPaySession is returned when the empty payment session is
// queried for a route.
errEmptyPaySession
@ -57,6 +61,9 @@ func (e noRouteError) Error() string {
case errEmptyPaySession:
return "empty payment session"
case errInsufficientBalance:
return "insufficient local balance"
default:
return "unknown no-route error"
}
@ -73,6 +80,9 @@ func (e noRouteError) FailureReason() channeldb.FailureReason {
return channeldb.FailureReasonNoRoute
case errInsufficientBalance:
return channeldb.FailureReasonInsufficientBalance
default:
return channeldb.FailureReasonError
}
@ -278,6 +288,16 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
// 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
// the receiver until the mpp timeout.
case err == errInsufficientBalance:
p.log.Debug("not splitting because local balance " +
"is insufficient")
return nil, err
case err != nil:
return nil, err
}