Skip to content

Commit f0558ba

Browse files
committed
multi: send MPP payment to blinded path
Make various sender side adjustments so that a sender is able to send an MP payment to a single blinded path without actually including an MPP record in the payment.
1 parent 64a99d4 commit f0558ba

File tree

4 files changed

+238
-8
lines changed

4 files changed

+238
-8
lines changed

channeldb/payment_control.go

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,12 @@ var (
6767

6868
// ErrValueMismatch is returned if we try to register a non-MPP attempt
6969
// with an amount that doesn't match the payment amount.
70-
ErrValueMismatch = errors.New("attempted value doesn't match payment" +
70+
ErrValueMismatch = errors.New("attempted value doesn't match payment " +
7171
"amount")
7272

7373
// ErrValueExceedsAmt is returned if we try to register an attempt that
7474
// would take the total sent amount above the payment amount.
75-
ErrValueExceedsAmt = errors.New("attempted value exceeds payment" +
75+
ErrValueExceedsAmt = errors.New("attempted value exceeds payment " +
7676
"amount")
7777

7878
// ErrNonMPPayment is returned if we try to register an MPP attempt for
@@ -83,6 +83,17 @@ var (
8383
// a payment that already has an MPP attempt registered.
8484
ErrMPPayment = errors.New("payment has MPP attempts")
8585

86+
// ErrMPPRecordInBlindedPayment is returned if we try to register an
87+
// attempt with an MPP record for a payment to a blinded path.
88+
ErrMPPRecordInBlindedPayment = errors.New("blinded payment cannot " +
89+
"contain MPP records")
90+
91+
// ErrBlindedPaymentTotalAmountMismatch is returned if we try to
92+
// register an HTLC shard to a blinded route where the total amount
93+
// doesn't match existing shards.
94+
ErrBlindedPaymentTotalAmountMismatch = errors.New("blinded path " +
95+
"total amount mismatch")
96+
8697
// ErrMPPPaymentAddrMismatch is returned if we try to register an MPP
8798
// shard where the payment address doesn't match existing shards.
8899
ErrMPPPaymentAddrMismatch = errors.New("payment address mismatch")
@@ -96,7 +107,7 @@ var (
96107
// attempt to a payment that has at least one of its HTLCs settled.
97108
ErrPaymentPendingSettled = errors.New("payment has settled htlcs")
98109

99-
// ErrPaymentAlreadyFailed is returned when we try to add a new attempt
110+
// ErrPaymentPendingFailed is returned when we try to add a new attempt
100111
// to a payment that already has a failure reason.
101112
ErrPaymentPendingFailed = errors.New("payment has failure reason")
102113

@@ -334,12 +345,48 @@ func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,
334345
return err
335346
}
336347

348+
// If the final hop has encrypted data, then we know this is a
349+
// blinded payment. In blinded payments, MPP records are not set
350+
// for split payments and the recipient is responsible for using
351+
// a consistent PathID across the various encrypted data
352+
// payloads that we received from them for this payment. All we
353+
// need to check is that the total amount field for each HTLC
354+
// in the split payment is correct.
355+
isBlinded := len(attempt.Route.FinalHop().EncryptedData) != 0
356+
337357
// Make sure any existing shards match the new one with regards
338358
// to MPP options.
339359
mpp := attempt.Route.FinalHop().MPP
360+
361+
// MPP records should not be set for attempts to blinded paths.
362+
if isBlinded && mpp != nil {
363+
return ErrMPPRecordInBlindedPayment
364+
}
365+
340366
for _, h := range payment.InFlightHTLCs() {
341367
hMpp := h.Route.FinalHop().MPP
342368

369+
// If this is a blinded payment, then no existing HTLCs
370+
// should have MPP records.
371+
if isBlinded && hMpp != nil {
372+
return ErrMPPRecordInBlindedPayment
373+
}
374+
375+
// If this is a blinded payment, then we just need to
376+
// check that the TotalAmtMsat field for this shard
377+
// is equal to that of any other shard in the same
378+
// payment.
379+
if isBlinded {
380+
if attempt.Route.FinalHop().TotalAmtMsat !=
381+
h.Route.FinalHop().TotalAmtMsat {
382+
383+
//nolint:lll
384+
return ErrBlindedPaymentTotalAmountMismatch
385+
}
386+
387+
continue
388+
}
389+
343390
switch {
344391
// We tried to register a non-MPP attempt for a MPP
345392
// payment.
@@ -367,9 +414,10 @@ func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,
367414
}
368415

369416
// If this is a non-MPP attempt, it must match the total amount
370-
// exactly.
417+
// exactly. Note that a blinded payment is considered an MPP
418+
// attempt.
371419
amt := attempt.Route.ReceiverAmt()
372-
if mpp == nil && amt != payment.Info.Value {
420+
if !isBlinded && mpp == nil && amt != payment.Info.Value {
373421
return ErrValueMismatch
374422
}
375423

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,10 @@ var allTestCases = []*lntest.TestCase{
586586
Name: "on chain to blinded",
587587
TestFunc: testErrorHandlingOnChainFailure,
588588
},
589+
{
590+
Name: "mpp to single blinded path",
591+
TestFunc: testMPPToSingleBlindedPath,
592+
},
589593
{
590594
Name: "removetx",
591595
TestFunc: testRemoveTx,

itest/lnd_route_blinding_test.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,3 +878,176 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) {
878878
ht.CloseChannel(testCase.carol, testCase.channels[2])
879879
testCase.cancel()
880880
}
881+
882+
// testMPPToSingleBlindedPath tests that a two-shard MPP payment can be sent
883+
// over a single blinded path.
884+
// The following graph is created where Dave is the destination node, and he
885+
// will choose Carol as the introduction node. The channel capacities are set in
886+
// such a way that Alice will have to split the payment to dave over both the
887+
// A->B->C-D and A->E->C->D routes.
888+
//
889+
// ---- Bob ---
890+
// / \
891+
// Alice Carol --- Dave
892+
// \ /
893+
// ---- Eve ---
894+
func testMPPToSingleBlindedPath(ht *lntest.HarnessTest) {
895+
// Create a five-node context consisting of Alice, Bob and three new
896+
// nodes.
897+
alice, bob := ht.Alice, ht.Bob
898+
899+
// Restrict Dave so that he only ever chooses the Carol->Dave path for
900+
// a blinded route.
901+
dave := ht.NewNode("dave", []string{
902+
"--invoices.blinding.min-num-real-hops=1",
903+
"--invoices.blinding.num-hops=1",
904+
})
905+
carol := ht.NewNode("carol", nil)
906+
eve := ht.NewNode("eve", nil)
907+
908+
// Connect nodes to ensure propagation of channels.
909+
ht.EnsureConnected(alice, bob)
910+
ht.EnsureConnected(alice, eve)
911+
ht.EnsureConnected(carol, bob)
912+
ht.EnsureConnected(carol, eve)
913+
ht.EnsureConnected(carol, dave)
914+
915+
// Send coins to the nodes and mine 1 blocks to confirm them.
916+
for i := 0; i < 2; i++ {
917+
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, carol)
918+
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, dave)
919+
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, eve)
920+
ht.MineBlocksAndAssertNumTxes(1, 3)
921+
}
922+
923+
const paymentAmt = btcutil.Amount(300000)
924+
925+
nodes := []*node.HarnessNode{alice, bob, carol, dave, eve}
926+
reqs := []*lntest.OpenChannelRequest{
927+
{
928+
Local: alice,
929+
Remote: bob,
930+
Param: lntest.OpenChannelParams{
931+
Amt: paymentAmt * 2 / 3,
932+
},
933+
},
934+
{
935+
Local: alice,
936+
Remote: eve,
937+
Param: lntest.OpenChannelParams{
938+
Amt: paymentAmt * 2 / 3,
939+
},
940+
},
941+
{
942+
Local: bob,
943+
Remote: carol,
944+
Param: lntest.OpenChannelParams{
945+
Amt: paymentAmt * 2,
946+
},
947+
},
948+
{
949+
Local: eve,
950+
Remote: carol,
951+
Param: lntest.OpenChannelParams{
952+
Amt: paymentAmt * 2,
953+
},
954+
},
955+
{
956+
Local: carol,
957+
Remote: dave,
958+
Param: lntest.OpenChannelParams{
959+
Amt: paymentAmt * 2,
960+
},
961+
},
962+
}
963+
964+
channelPoints := ht.OpenMultiChannelsAsync(reqs)
965+
966+
// Make sure every node has heard about every channel.
967+
for _, hn := range nodes {
968+
for _, cp := range channelPoints {
969+
ht.AssertTopologyChannelOpen(hn, cp)
970+
}
971+
972+
// Each node should have exactly 5 edges.
973+
ht.AssertNumEdges(hn, len(channelPoints), false)
974+
}
975+
976+
// Make Dave create an invoice with a blinded path for Alice to pay.
977+
invoice := &lnrpc.Invoice{
978+
Memo: "test",
979+
Value: int64(paymentAmt),
980+
Blind: true,
981+
}
982+
invoiceResp := dave.RPC.AddInvoice(invoice)
983+
984+
sendReq := &routerrpc.SendPaymentRequest{
985+
PaymentRequest: invoiceResp.PaymentRequest,
986+
MaxParts: 10,
987+
TimeoutSeconds: 60,
988+
FeeLimitMsat: noFeeLimitMsat,
989+
}
990+
payment := ht.SendPaymentAssertSettled(alice, sendReq)
991+
992+
preimageBytes, err := hex.DecodeString(payment.PaymentPreimage)
993+
require.NoError(ht, err)
994+
995+
preimage, err := lntypes.MakePreimage(preimageBytes)
996+
require.NoError(ht, err)
997+
998+
hash, err := lntypes.MakeHash(invoiceResp.RHash)
999+
require.NoError(ht, err)
1000+
1001+
// Make sure we got the preimage.
1002+
require.True(ht, preimage.Matches(hash), "preimage doesn't match")
1003+
1004+
// Check that Alice split the payment in at least two shards. Because
1005+
// the hand-off of the htlc to the link is asynchronous (via a mailbox),
1006+
// there is some non-determinism in the process. Depending on whether
1007+
// the new pathfinding round is started before or after the htlc is
1008+
// locked into the channel, different sharding may occur. Therefore, we
1009+
// can only check if the number of shards isn't below the theoretical
1010+
// minimum.
1011+
succeeded := 0
1012+
for _, htlc := range payment.Htlcs {
1013+
if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
1014+
succeeded++
1015+
}
1016+
}
1017+
1018+
const minExpectedShards = 2
1019+
require.GreaterOrEqual(ht, succeeded, minExpectedShards,
1020+
"expected shards not reached")
1021+
1022+
// Make sure Dave show the invoice as settled for the full amount.
1023+
inv := dave.RPC.LookupInvoice(invoiceResp.RHash)
1024+
1025+
require.EqualValues(ht, paymentAmt, inv.AmtPaidSat,
1026+
"incorrect payment amt")
1027+
1028+
require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State,
1029+
"Invoice not settled")
1030+
1031+
settled := 0
1032+
for _, htlc := range inv.Htlcs {
1033+
if htlc.State == lnrpc.InvoiceHTLCState_SETTLED {
1034+
settled++
1035+
}
1036+
}
1037+
require.Equal(ht, succeeded, settled, "num of HTLCs wrong")
1038+
1039+
// Close all channels without mining the closing transactions.
1040+
ht.CloseChannelAssertPending(alice, channelPoints[0], false)
1041+
ht.CloseChannelAssertPending(alice, channelPoints[1], false)
1042+
ht.CloseChannelAssertPending(bob, channelPoints[2], false)
1043+
ht.CloseChannelAssertPending(eve, channelPoints[3], false)
1044+
ht.CloseChannelAssertPending(carol, channelPoints[4], false)
1045+
1046+
// Now mine a block to include all the closing transactions.
1047+
ht.MineBlocksAndAssertNumTxes(1, 5)
1048+
1049+
// Assert that the channels are closed.
1050+
for _, hn := range nodes {
1051+
ht.AssertNumWaitingClose(hn, 0)
1052+
}
1053+
}

routing/payment_session.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,12 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
338338
switch {
339339
case err == errNoPathFound:
340340
// Don't split if this is a legacy payment without mpp
341-
// record.
342-
if p.payment.PaymentAddr == nil {
341+
// record. If it has a blinded path though, then we
342+
// can split. Split payments to blinded paths won't have
343+
// MPP records.
344+
if p.payment.PaymentAddr == nil &&
345+
p.payment.BlindedPayment == nil {
346+
343347
p.log.Debugf("not splitting because payment " +
344348
"address is unspecified")
345349

@@ -357,7 +361,8 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
357361
!destFeatures.HasFeature(lnwire.AMPOptional) {
358362

359363
p.log.Debug("not splitting because " +
360-
"destination doesn't declare MPP or AMP")
364+
"destination doesn't declare MPP or " +
365+
"AMP")
361366

362367
return nil, errNoPathFound
363368
}

0 commit comments

Comments
 (0)