routing: if MaxShardAmt is set, then use that as a ceiling for our splits

In this commit, we thread through the necessary state to allow users to
set a max shard amount. If this value is set, then this'll effectively
serve as a ceiling for all our split attempts. If we need to split,
we'll first try to use `paymentAmt/2`, if that's bigger than
`MaxShardAmt, then we'll use the latter instead.

Ideally in the future we have a dynamic way to automatically set both
the `MaxShardAmt` as well as `MaxParts` for users. Until then exposing
these two new fields will allow us to experiment with setting them
automatically using the RPC interface, and also give users a bit more
control over how we attempt to route payments, akin to coin control for
on-chain payments.

Fixes #4730
This commit is contained in:
Olaoluwa Osuntokun 2021-02-11 18:05:13 -08:00
parent 7398e59927
commit b73a6e2c61
No known key found for this signature in database
GPG Key ID: 3BBD59E99B280306
4 changed files with 57 additions and 1 deletions

@ -204,6 +204,7 @@ out:
FinalCltvDelta: int32(carolPayReq.CltvExpiry), FinalCltvDelta: int32(carolPayReq.CltvExpiry),
TimeoutSeconds: 60, TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat, FeeLimitMsat: noFeeLimitMsat,
MaxParts: 1,
} }
sendAndAssertFailure( sendAndAssertFailure(
t, net.Alice, t, net.Alice,
@ -240,6 +241,7 @@ out:
FinalCltvDelta: int32(carolPayReq.CltvExpiry), FinalCltvDelta: int32(carolPayReq.CltvExpiry),
TimeoutSeconds: 60, TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat, FeeLimitMsat: noFeeLimitMsat,
MaxParts: 1,
} }
sendAndAssertFailure( sendAndAssertFailure(
t, net.Alice, t, net.Alice,
@ -300,6 +302,7 @@ out:
PaymentRequest: carolInvoice2.PaymentRequest, PaymentRequest: carolInvoice2.PaymentRequest,
TimeoutSeconds: 60, TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat, FeeLimitMsat: noFeeLimitMsat,
MaxParts: 1,
}, },
) )
@ -332,6 +335,7 @@ out:
PaymentRequest: carolInvoice3.PaymentRequest, PaymentRequest: carolInvoice3.PaymentRequest,
TimeoutSeconds: 60, TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat, FeeLimitMsat: noFeeLimitMsat,
MaxParts: 1,
} }
sendAndAssertFailure( sendAndAssertFailure(
t, net.Alice, t, net.Alice,
@ -381,6 +385,7 @@ out:
PaymentRequest: carolInvoice.PaymentRequest, PaymentRequest: carolInvoice.PaymentRequest,
TimeoutSeconds: 60, TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat, FeeLimitMsat: noFeeLimitMsat,
MaxParts: 1,
}, },
lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE, lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE,
) )

@ -28,6 +28,7 @@ type integratedRoutingContext struct {
target *mockNode target *mockNode
amt lnwire.MilliSatoshi amt lnwire.MilliSatoshi
maxShardAmt *lnwire.MilliSatoshi
finalExpiry int32 finalExpiry int32
mcCfg MissionControlConfig mcCfg MissionControlConfig
@ -151,6 +152,10 @@ func (c *integratedRoutingContext) testPayment(maxParts uint32,
MaxParts: maxParts, MaxParts: maxParts,
} }
if c.maxShardAmt != nil {
payment.MaxShardAmt = c.maxShardAmt
}
session, err := newPaymentSession( session, err := newPaymentSession(
&payment, getBandwidthHints, &payment, getBandwidthHints,
func() (routingGraph, func(), error) { func() (routingGraph, func(), error) {

@ -89,6 +89,7 @@ type mppSendTestCase struct {
graph func(g *mockGraph) graph func(g *mockGraph)
expectedFailure bool expectedFailure bool
maxParts uint32 maxParts uint32
maxShardSize btcutil.Amount
} }
const ( const (
@ -208,6 +209,33 @@ var mppTestCases = []mppSendTestCase{
expectedFailure: true, expectedFailure: true,
maxParts: 10, maxParts: 10,
}, },
// Test that if maxShardSize is set, then all attempts are below the
// max shard size, yet still sum up to the total payment amount. A
// payment of 30k satoshis with a max shard size of 10k satoshis should
// produce 3 payments of 10k sats each.
{
name: "max shard size clamping",
graph: onePathGraph,
amt: 30_000,
expectedAttempts: 3,
expectedSuccesses: []expectedHtlcSuccess{
{
amt: 10_000,
chans: []uint64{chanSourceIm1, chanIm1Target},
},
{
amt: 10_000,
chans: []uint64{chanSourceIm1, chanIm1Target},
},
{
amt: 10_000,
chans: []uint64{chanSourceIm1, chanIm1Target},
},
},
maxParts: 1000,
maxShardSize: 10_000,
},
} }
// TestMppSend tests that a payment can be completed using multiple shards. // TestMppSend tests that a payment can be completed using multiple shards.
@ -229,6 +257,11 @@ func testMppSend(t *testing.T, testCase *mppSendTestCase) {
ctx.amt = lnwire.NewMSatFromSatoshis(testCase.amt) ctx.amt = lnwire.NewMSatFromSatoshis(testCase.amt)
if testCase.maxShardSize != 0 {
shardAmt := lnwire.NewMSatFromSatoshis(testCase.maxShardSize)
ctx.maxShardAmt = &shardAmt
}
attempts, err := ctx.testPayment(testCase.maxParts) attempts, err := ctx.testPayment(testCase.maxParts)
switch { switch {
case err == nil && testCase.expectedFailure: case err == nil && testCase.expectedFailure:

@ -230,6 +230,18 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
finalHtlcExpiry := int32(height) + int32(finalCltvDelta) finalHtlcExpiry := int32(height) + int32(finalCltvDelta)
// Before we enter the loop below, we'll make sure to respect the max
// payment shard size (if it's set), which is effectively our
// client-side MTU that we'll attempt to respect at all times.
maxShardActive := p.payment.MaxShardAmt != nil
if maxShardActive && maxAmt > *p.payment.MaxShardAmt {
p.log.Debug("Clamping payment attempt from %v to %v due to "+
"max shard size of %v", maxAmt,
*p.payment.MaxShardAmt, maxAmt)
maxAmt = *p.payment.MaxShardAmt
}
for { for {
// We'll also obtain a set of bandwidthHints from the lower // We'll also obtain a set of bandwidthHints from the lower
// layer for each of our outbound channels. This will allow the // layer for each of our outbound channels. This will allow the
@ -279,7 +291,8 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
} }
if !p.payment.DestFeatures.HasFeature(lnwire.MPPOptional) { if !p.payment.DestFeatures.HasFeature(lnwire.MPPOptional) {
p.log.Debug("not splitting because destination doesn't declare MPP") p.log.Debug("not splitting because " +
"destination doesn't declare MPP")
return nil, errNoPathFound return nil, errNoPathFound
} }