Merge pull request #4198 from joostjager/mpp-precheck
routing: payment splitting pre-check
This commit is contained in:
commit
3b8ddece41
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user