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 // FailureReasonInsufficientBalance indicates that we didn't have enough
// balance to complete the payment. // balance to complete the payment.
//
// This reason isn't assigned anymore, but may still exist for older
// payments.
FailureReasonInsufficientBalance FailureReason = 4 FailureReasonInsufficientBalance FailureReason = 4
// TODO(halseth): cancel state. // TODO(halseth): cancel state.

@ -107,7 +107,7 @@ func onePathGraph(g *mockGraph) {
g.addChannel(chanIm1Target, targetNodeID, im1NodeID, 100000) g.addChannel(chanIm1Target, targetNodeID, im1NodeID, 100000)
} }
func twoPathGraph(g *mockGraph) { func twoPathGraph(g *mockGraph, capacityOut, capacityIn btcutil.Amount) {
// Create the following network of nodes: // Create the following network of nodes:
// source -> intermediate1 -> target // source -> intermediate1 -> target
// source -> intermediate2 -> target // source -> intermediate2 -> target
@ -120,10 +120,10 @@ func twoPathGraph(g *mockGraph) {
intermediate2 := newMockNode(im2NodeID) intermediate2 := newMockNode(im2NodeID)
g.addNode(intermediate2) g.addNode(intermediate2)
g.addChannel(chanSourceIm1, sourceNodeID, im1NodeID, 200000) g.addChannel(chanSourceIm1, sourceNodeID, im1NodeID, capacityOut)
g.addChannel(chanSourceIm2, sourceNodeID, im2NodeID, 200000) g.addChannel(chanSourceIm2, sourceNodeID, im2NodeID, capacityOut)
g.addChannel(chanIm1Target, targetNodeID, im1NodeID, 100000) g.addChannel(chanIm1Target, targetNodeID, im1NodeID, capacityIn)
g.addChannel(chanIm2Target, targetNodeID, im2NodeID, 100000) g.addChannel(chanIm2Target, targetNodeID, im2NodeID, capacityIn)
} }
var mppTestCases = []mppSendTestCase{ var mppTestCases = []mppSendTestCase{
@ -137,7 +137,9 @@ var mppTestCases = []mppSendTestCase{
{ {
name: "sufficient inbound", name: "sufficient inbound",
graph: twoPathGraph, graph: func(g *mockGraph) {
twoPathGraph(g, 200000, 100000)
},
amt: 70000, amt: 70000,
expectedAttempts: 5, expectedAttempts: 5,
expectedSuccesses: []expectedHtlcSuccess{ expectedSuccesses: []expectedHtlcSuccess{
@ -156,7 +158,9 @@ var mppTestCases = []mppSendTestCase{
// Test that a cap on the max htlcs makes it impossible to pay. // Test that a cap on the max htlcs makes it impossible to pay.
{ {
name: "no splitting", name: "no splitting",
graph: twoPathGraph, graph: func(g *mockGraph) {
twoPathGraph(g, 200000, 100000)
},
amt: 70000, amt: 70000,
expectedAttempts: 2, expectedAttempts: 2,
expectedSuccesses: []expectedHtlcSuccess{}, expectedSuccesses: []expectedHtlcSuccess{},
@ -188,6 +192,19 @@ var mppTestCases = []mppSendTestCase{
expectedFailure: true, expectedFailure: true,
maxShards: 1000, 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. // TestMppSend tests that a payment can be completed using multiple shards.

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

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

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