Skip to content

Commit db5af6f

Browse files
authored
Merge pull request #5017 from Roasbeef/default-max-parts
routing: if MaxShardAmt is set, then use that as a ceiling for our splits, use default of 16 for MaxParts
2 parents 8c06eb5 + db69331 commit db5af6f

10 files changed

+328
-184
lines changed

cmd/lncli/cmd_pay.go

+35-1
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ import (
1414
"strings"
1515
"time"
1616

17+
"github.com/btcsuite/btcutil"
1718
"github.com/jedib0t/go-pretty/table"
1819
"github.com/jedib0t/go-pretty/text"
1920
"github.com/lightninglabs/protobuf-hex-display/jsonpb"
2021
"github.com/lightningnetwork/lnd/lnrpc"
2122
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
2223
"github.com/lightningnetwork/lnd/lntypes"
24+
"github.com/lightningnetwork/lnd/lnwire"
2325
"github.com/lightningnetwork/lnd/record"
2426
"github.com/lightningnetwork/lnd/routing/route"
2527
"github.com/urfave/cli"
@@ -62,7 +64,7 @@ var (
6264
Name: "max_parts",
6365
Usage: "the maximum number of partial payments that may be " +
6466
"used",
65-
Value: 1,
67+
Value: routerrpc.DefaultMaxParts,
6668
}
6769

6870
jsonFlag = cli.BoolFlag{
@@ -71,6 +73,20 @@ var (
7173
"messages. Set by default on Windows because table " +
7274
"formatting is unsupported.",
7375
}
76+
77+
maxShardSizeSatFlag = cli.UintFlag{
78+
Name: "max_shard_size_sat",
79+
Usage: "the largest payment split that should be attempted if " +
80+
"payment splitting is required to attempt a payment, " +
81+
"specified in satoshis",
82+
}
83+
84+
maxShardSizeMsatFlag = cli.UintFlag{
85+
Name: "max_shard_size_msat",
86+
Usage: "the largest payment split that should be attempted if " +
87+
"payment splitting is required to attempt a payment, " +
88+
"specified in milli-satoshis",
89+
}
7490
)
7591

7692
// paymentFlags returns common flags for sendpayment and payinvoice.
@@ -115,6 +131,7 @@ func paymentFlags() []cli.Flag {
115131
Usage: "allow sending a circular payment to self",
116132
},
117133
dataFlag, inflightUpdatesFlag, maxPartsFlag, jsonFlag,
134+
maxShardSizeSatFlag, maxShardSizeMsatFlag,
118135
}
119136
}
120137

@@ -354,6 +371,23 @@ func sendPaymentRequest(ctx *cli.Context,
354371

355372
req.MaxParts = uint32(ctx.Uint(maxPartsFlag.Name))
356373

374+
switch {
375+
// If the max shard size is specified, then it should either be in sat
376+
// or msat, but not both.
377+
case ctx.Uint64(maxShardSizeMsatFlag.Name) != 0 &&
378+
ctx.Uint64(maxShardSizeSatFlag.Name) != 0:
379+
return fmt.Errorf("only --max_split_size_msat or " +
380+
"--max_split_size_sat should be set, but not both")
381+
382+
case ctx.Uint64(maxShardSizeMsatFlag.Name) != 0:
383+
req.MaxShardSizeMsat = ctx.Uint64(maxShardSizeMsatFlag.Name)
384+
385+
case ctx.Uint64(maxShardSizeSatFlag.Name) != 0:
386+
req.MaxShardSizeMsat = uint64(lnwire.NewMSatFromSatoshis(
387+
btcutil.Amount(ctx.Uint64(maxShardSizeSatFlag.Name)),
388+
))
389+
}
390+
357391
// Parse custom data records.
358392
data := ctx.String(dataFlag.Name)
359393
if data != "" {

lnrpc/routerrpc/router.pb.go

+194-179
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lnrpc/routerrpc/router.proto

+8
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,14 @@ message SendPaymentRequest {
253253
that show which htlcs are still in flight are suppressed.
254254
*/
255255
bool no_inflight_updates = 18;
256+
257+
/*
258+
The largest payment split that should be attempted when making a payment if
259+
splitting is necessary. Setting this value will effectively cause lnd to
260+
split more aggressively, vs only when it thinks it needs to. Note that this
261+
value is in milli-satoshis.
262+
*/
263+
uint64 max_shard_size_msat = 21;
256264
}
257265

258266
message TrackPaymentRequest {

lnrpc/routerrpc/router.swagger.json

+5
Original file line numberDiff line numberDiff line change
@@ -1405,6 +1405,11 @@
14051405
"type": "boolean",
14061406
"format": "boolean",
14071407
"description": "If set, only the final payment update is streamed back. Intermediate updates\nthat show which htlcs are still in flight are suppressed."
1408+
},
1409+
"max_shard_size_msat": {
1410+
"type": "string",
1411+
"format": "uint64",
1412+
"description": "The largest payment split that should be attempted when making a payment if\nsplitting is necessary. Setting this value will effectively cause lnd to\nsplit more aggressively, vs only when it thinks it needs to. Note that this\nvalue is in milli-satoshis."
14081413
}
14091414
}
14101415
},

lnrpc/routerrpc/router_backend.go

+21-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ import (
2424
"github.com/lightningnetwork/lnd/zpay32"
2525
)
2626

27+
const (
28+
// DefaultMaxParts is the default number of splits we'll possibly use
29+
// for MPP when the user is attempting to send a payment.
30+
//
31+
// TODO(roasbeef): make this value dynamic based on expected number of
32+
// attempts for given amount
33+
DefaultMaxParts = 16
34+
)
35+
2736
// RouterBackend contains the backend implementation of the router rpc sub
2837
// server calls.
2938
type RouterBackend struct {
@@ -554,14 +563,23 @@ func (r *RouterBackend) extractIntentFromSendRequest(
554563
}
555564
payIntent.CltvLimit = cltvLimit
556565

557-
// Take max htlcs from the request. Map zero to one for backwards
558-
// compatibility.
566+
// Attempt to parse the max parts value set by the user, if this value
567+
// isn't set, then we'll use the current default value for this
568+
// setting.
559569
maxParts := rpcPayReq.MaxParts
560570
if maxParts == 0 {
561-
maxParts = 1
571+
maxParts = DefaultMaxParts
562572
}
563573
payIntent.MaxParts = maxParts
564574

575+
// If this payment had a max shard amount specified, then we'll apply
576+
// that now, which'll force us to always make payment splits smaller
577+
// than this.
578+
if rpcPayReq.MaxShardSizeMsat > 0 {
579+
shardAmtMsat := lnwire.MilliSatoshi(rpcPayReq.MaxShardSizeMsat)
580+
payIntent.MaxShardAmt = &shardAmtMsat
581+
}
582+
565583
// Take fee limit from request.
566584
payIntent.FeeLimit, err = lnrpc.UnmarshallAmt(
567585
rpcPayReq.FeeLimitSat, rpcPayReq.FeeLimitMsat,

lntest/itest/lnd_multi-hop-error-propagation_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ out:
204204
FinalCltvDelta: int32(carolPayReq.CltvExpiry),
205205
TimeoutSeconds: 60,
206206
FeeLimitMsat: noFeeLimitMsat,
207+
MaxParts: 1,
207208
}
208209
sendAndAssertFailure(
209210
t, net.Alice,
@@ -240,6 +241,7 @@ out:
240241
FinalCltvDelta: int32(carolPayReq.CltvExpiry),
241242
TimeoutSeconds: 60,
242243
FeeLimitMsat: noFeeLimitMsat,
244+
MaxParts: 1,
243245
}
244246
sendAndAssertFailure(
245247
t, net.Alice,
@@ -300,6 +302,7 @@ out:
300302
PaymentRequest: carolInvoice2.PaymentRequest,
301303
TimeoutSeconds: 60,
302304
FeeLimitMsat: noFeeLimitMsat,
305+
MaxParts: 1,
303306
},
304307
)
305308

@@ -332,6 +335,7 @@ out:
332335
PaymentRequest: carolInvoice3.PaymentRequest,
333336
TimeoutSeconds: 60,
334337
FeeLimitMsat: noFeeLimitMsat,
338+
MaxParts: 1,
335339
}
336340
sendAndAssertFailure(
337341
t, net.Alice,
@@ -381,6 +385,7 @@ out:
381385
PaymentRequest: carolInvoice.PaymentRequest,
382386
TimeoutSeconds: 60,
383387
FeeLimitMsat: noFeeLimitMsat,
388+
MaxParts: 1,
384389
},
385390
lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE,
386391
)

routing/integrated_routing_context_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type integratedRoutingContext struct {
2828
target *mockNode
2929

3030
amt lnwire.MilliSatoshi
31+
maxShardAmt *lnwire.MilliSatoshi
3132
finalExpiry int32
3233

3334
mcCfg MissionControlConfig
@@ -151,6 +152,10 @@ func (c *integratedRoutingContext) testPayment(maxParts uint32,
151152
MaxParts: maxParts,
152153
}
153154

155+
if c.maxShardAmt != nil {
156+
payment.MaxShardAmt = c.maxShardAmt
157+
}
158+
154159
session, err := newPaymentSession(
155160
&payment, getBandwidthHints,
156161
func() (routingGraph, func(), error) {

routing/integrated_routing_test.go

+33
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ type mppSendTestCase struct {
8989
graph func(g *mockGraph)
9090
expectedFailure bool
9191
maxParts uint32
92+
maxShardSize btcutil.Amount
9293
}
9394

9495
const (
@@ -208,6 +209,33 @@ var mppTestCases = []mppSendTestCase{
208209
expectedFailure: true,
209210
maxParts: 10,
210211
},
212+
213+
// Test that if maxShardSize is set, then all attempts are below the
214+
// max shard size, yet still sum up to the total payment amount. A
215+
// payment of 30k satoshis with a max shard size of 10k satoshis should
216+
// produce 3 payments of 10k sats each.
217+
{
218+
name: "max shard size clamping",
219+
graph: onePathGraph,
220+
amt: 30_000,
221+
expectedAttempts: 3,
222+
expectedSuccesses: []expectedHtlcSuccess{
223+
{
224+
amt: 10_000,
225+
chans: []uint64{chanSourceIm1, chanIm1Target},
226+
},
227+
{
228+
amt: 10_000,
229+
chans: []uint64{chanSourceIm1, chanIm1Target},
230+
},
231+
{
232+
amt: 10_000,
233+
chans: []uint64{chanSourceIm1, chanIm1Target},
234+
},
235+
},
236+
maxParts: 1000,
237+
maxShardSize: 10_000,
238+
},
211239
}
212240

213241
// TestMppSend tests that a payment can be completed using multiple shards.
@@ -229,6 +257,11 @@ func testMppSend(t *testing.T, testCase *mppSendTestCase) {
229257

230258
ctx.amt = lnwire.NewMSatFromSatoshis(testCase.amt)
231259

260+
if testCase.maxShardSize != 0 {
261+
shardAmt := lnwire.NewMSatFromSatoshis(testCase.maxShardSize)
262+
ctx.maxShardAmt = &shardAmt
263+
}
264+
232265
attempts, err := ctx.testPayment(testCase.maxParts)
233266
switch {
234267
case err == nil && testCase.expectedFailure:

routing/payment_session.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,18 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
230230

231231
finalHtlcExpiry := int32(height) + int32(finalCltvDelta)
232232

233+
// Before we enter the loop below, we'll make sure to respect the max
234+
// payment shard size (if it's set), which is effectively our
235+
// client-side MTU that we'll attempt to respect at all times.
236+
maxShardActive := p.payment.MaxShardAmt != nil
237+
if maxShardActive && maxAmt > *p.payment.MaxShardAmt {
238+
p.log.Debug("Clamping payment attempt from %v to %v due to "+
239+
"max shard size of %v", maxAmt,
240+
*p.payment.MaxShardAmt, maxAmt)
241+
242+
maxAmt = *p.payment.MaxShardAmt
243+
}
244+
233245
for {
234246
// We'll also obtain a set of bandwidthHints from the lower
235247
// layer for each of our outbound channels. This will allow the
@@ -279,7 +291,8 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
279291
}
280292

281293
if !p.payment.DestFeatures.HasFeature(lnwire.MPPOptional) {
282-
p.log.Debug("not splitting because destination doesn't declare MPP")
294+
p.log.Debug("not splitting because " +
295+
"destination doesn't declare MPP")
283296

284297
return nil, errNoPathFound
285298
}

routing/router.go

+8
Original file line numberDiff line numberDiff line change
@@ -1684,6 +1684,14 @@ type LightningPayment struct {
16841684
// MaxParts is the maximum number of partial payments that may be used
16851685
// to complete the full amount.
16861686
MaxParts uint32
1687+
1688+
// MaxShardAmt is the largest shard that we'll attempt to split using.
1689+
// If this field is set, and we need to split, rather than attempting
1690+
// half of the original payment amount, we'll use this value if half
1691+
// the payment amount is greater than it.
1692+
//
1693+
// NOTE: This field is _optional_.
1694+
MaxShardAmt *lnwire.MilliSatoshi
16871695
}
16881696

16891697
// SendPayment attempts to send a payment as described within the passed

0 commit comments

Comments
 (0)