Skip to content

Commit 04dde98

Browse files
authored
Merge pull request #8764 from ellemouton/rb-send-via-multi-path
[3/4] Route Blinding: send MPP over multiple blinded paths
2 parents b21521f + be4c3dd commit 04dde98

File tree

13 files changed

+647
-210
lines changed

13 files changed

+647
-210
lines changed

docs/release-notes/release-notes-0.18.3.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ commitment when the channel was force closed.
136136
the `lncli addinvoice` command to instruct LND to include blinded paths in the
137137
invoice.
138138

139+
* Add the ability to [send to use multiple blinded payment
140+
paths](https://github.com/lightningnetwork/lnd/pull/8764) in an MP payment.
141+
139142
## Testing
140143
## Database
141144

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,10 @@ var allTestCases = []*lntest.TestCase{
594594
Name: "mpp to single blinded path",
595595
TestFunc: testMPPToSingleBlindedPath,
596596
},
597+
{
598+
Name: "mpp to multiple blinded paths",
599+
TestFunc: testMPPToMultipleBlindedPaths,
600+
},
597601
{
598602
Name: "route blinding dummy hops",
599603
TestFunc: testBlindedRouteDummyHops,

itest/lnd_route_blinding_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,3 +1229,166 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) {
12291229
ht.AssertNumWaitingClose(hn, 0)
12301230
}
12311231
}
1232+
1233+
// testMPPToMultipleBlindedPaths tests that a two-shard MPP payment can be sent
1234+
// over a multiple blinded paths. The following network is created where Dave
1235+
// is the recipient and Alice the sender. Dave will create an invoice containing
1236+
// two blinded paths: one with Bob at the intro node and one with Carol as the
1237+
// intro node. Channel liquidity will be set up in such a way that Alice will be
1238+
// forced to send one shared via the Bob->Dave route and one over the
1239+
// Carol->Dave route.
1240+
//
1241+
// --- Bob ---
1242+
// / \
1243+
// Alice Dave
1244+
// \ /
1245+
// --- Carol ---
1246+
func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) {
1247+
alice, bob := ht.Alice, ht.Bob
1248+
1249+
// Create a four-node context consisting of Alice, Bob and three new
1250+
// nodes.
1251+
dave := ht.NewNode("dave", []string{
1252+
"--routing.blinding.min-num-real-hops=1",
1253+
"--routing.blinding.num-hops=1",
1254+
})
1255+
carol := ht.NewNode("carol", nil)
1256+
1257+
// Connect nodes to ensure propagation of channels.
1258+
ht.EnsureConnected(alice, carol)
1259+
ht.EnsureConnected(alice, bob)
1260+
ht.EnsureConnected(carol, dave)
1261+
ht.EnsureConnected(bob, dave)
1262+
1263+
// Fund the new nodes.
1264+
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, carol)
1265+
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, dave)
1266+
ht.MineBlocksAndAssertNumTxes(1, 2)
1267+
1268+
const paymentAmt = btcutil.Amount(300000)
1269+
1270+
nodes := []*node.HarnessNode{alice, bob, carol, dave}
1271+
1272+
reqs := []*lntest.OpenChannelRequest{
1273+
{
1274+
Local: alice,
1275+
Remote: bob,
1276+
Param: lntest.OpenChannelParams{
1277+
Amt: paymentAmt * 2 / 3,
1278+
},
1279+
},
1280+
{
1281+
Local: alice,
1282+
Remote: carol,
1283+
Param: lntest.OpenChannelParams{
1284+
Amt: paymentAmt * 2 / 3,
1285+
},
1286+
},
1287+
{
1288+
Local: bob,
1289+
Remote: dave,
1290+
Param: lntest.OpenChannelParams{Amt: paymentAmt * 2},
1291+
},
1292+
{
1293+
Local: carol,
1294+
Remote: dave,
1295+
Param: lntest.OpenChannelParams{Amt: paymentAmt * 2},
1296+
},
1297+
}
1298+
1299+
channelPoints := ht.OpenMultiChannelsAsync(reqs)
1300+
1301+
// Make sure every node has heard every channel.
1302+
for _, hn := range nodes {
1303+
for _, cp := range channelPoints {
1304+
ht.AssertTopologyChannelOpen(hn, cp)
1305+
}
1306+
1307+
// Each node should have exactly 5 edges.
1308+
ht.AssertNumEdges(hn, len(channelPoints), false)
1309+
}
1310+
1311+
// Ok now make a payment that must be split to succeed.
1312+
1313+
// Make Dave create an invoice for Alice to pay
1314+
invoice := &lnrpc.Invoice{
1315+
Memo: "test",
1316+
Value: int64(paymentAmt),
1317+
Blind: true,
1318+
}
1319+
invoiceResp := dave.RPC.AddInvoice(invoice)
1320+
1321+
// Assert that two blinded paths are included in the invoice.
1322+
payReq := dave.RPC.DecodePayReq(invoiceResp.PaymentRequest)
1323+
require.Len(ht, payReq.BlindedPaths, 2)
1324+
1325+
sendReq := &routerrpc.SendPaymentRequest{
1326+
PaymentRequest: invoiceResp.PaymentRequest,
1327+
MaxParts: 10,
1328+
TimeoutSeconds: 60,
1329+
FeeLimitMsat: noFeeLimitMsat,
1330+
}
1331+
payment := ht.SendPaymentAssertSettled(alice, sendReq)
1332+
1333+
preimageBytes, err := hex.DecodeString(payment.PaymentPreimage)
1334+
require.NoError(ht, err)
1335+
1336+
preimage, err := lntypes.MakePreimage(preimageBytes)
1337+
require.NoError(ht, err)
1338+
1339+
hash, err := lntypes.MakeHash(invoiceResp.RHash)
1340+
require.NoError(ht, err)
1341+
1342+
// Make sure we got the preimage.
1343+
require.True(ht, preimage.Matches(hash), "preimage doesn't match")
1344+
1345+
// Check that Alice split the payment in at least two shards. Because
1346+
// the hand-off of the htlc to the link is asynchronous (via a mailbox),
1347+
// there is some non-determinism in the process. Depending on whether
1348+
// the new pathfinding round is started before or after the htlc is
1349+
// locked into the channel, different sharding may occur. Therefore we
1350+
// can only check if the number of shards isn't below the theoretical
1351+
// minimum.
1352+
succeeded := 0
1353+
for _, htlc := range payment.Htlcs {
1354+
if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
1355+
succeeded++
1356+
}
1357+
}
1358+
1359+
const minExpectedShards = 2
1360+
require.GreaterOrEqual(ht, succeeded, minExpectedShards,
1361+
"expected shards not reached")
1362+
1363+
// Make sure Dave show the invoice as settled for the full amount.
1364+
inv := dave.RPC.LookupInvoice(invoiceResp.RHash)
1365+
1366+
require.EqualValues(ht, paymentAmt, inv.AmtPaidSat,
1367+
"incorrect payment amt")
1368+
1369+
require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State,
1370+
"Invoice not settled")
1371+
1372+
settled := 0
1373+
for _, htlc := range inv.Htlcs {
1374+
if htlc.State == lnrpc.InvoiceHTLCState_SETTLED {
1375+
settled++
1376+
}
1377+
}
1378+
require.Equal(ht, succeeded, settled, "num of HTLCs wrong")
1379+
1380+
// Close all channels without mining the closing transactions.
1381+
ht.CloseChannelAssertPending(alice, channelPoints[0], false)
1382+
ht.CloseChannelAssertPending(alice, channelPoints[1], false)
1383+
ht.CloseChannelAssertPending(bob, channelPoints[2], false)
1384+
ht.CloseChannelAssertPending(carol, channelPoints[3], false)
1385+
1386+
// Now mine a block to include all the closing transactions. (first
1387+
// iteration: no blinded paths)
1388+
ht.MineBlocksAndAssertNumTxes(1, 4)
1389+
1390+
// Assert that the channels are closed.
1391+
for _, hn := range nodes {
1392+
ht.AssertNumWaitingClose(hn, 0)
1393+
}
1394+
}

lnrpc/routerrpc/router_backend.go

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
280280
var (
281281
targetPubKey *route.Vertex
282282
routeHintEdges map[route.Vertex][]routing.AdditionalEdge
283-
blindedPmt *routing.BlindedPayment
283+
blindedPathSet *routing.BlindedPaymentPathSet
284284

285285
// finalCLTVDelta varies depending on whether we're sending to
286286
// a blinded route or an unblinded node. For blinded paths,
@@ -297,13 +297,14 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
297297
// Validate that the fields provided in the request are sane depending
298298
// on whether it is using a blinded path or not.
299299
if len(in.BlindedPaymentPaths) > 0 {
300-
blindedPmt, err = parseBlindedPayment(in)
300+
blindedPathSet, err = parseBlindedPaymentPaths(in)
301301
if err != nil {
302302
return nil, err
303303
}
304304

305-
if blindedPmt.Features != nil {
306-
destinationFeatures = blindedPmt.Features.Clone()
305+
pathFeatures := blindedPathSet.Features()
306+
if pathFeatures != nil {
307+
destinationFeatures = pathFeatures.Clone()
307308
}
308309
} else {
309310
// If we do not have a blinded path, a target pubkey must be
@@ -387,10 +388,10 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
387388
fromNode, toNode, amt, capacity,
388389
)
389390
},
390-
DestCustomRecords: record.CustomSet(in.DestCustomRecords),
391-
CltvLimit: cltvLimit,
392-
DestFeatures: destinationFeatures,
393-
BlindedPayment: blindedPmt,
391+
DestCustomRecords: record.CustomSet(in.DestCustomRecords),
392+
CltvLimit: cltvLimit,
393+
DestFeatures: destinationFeatures,
394+
BlindedPaymentPathSet: blindedPathSet,
394395
}
395396

396397
// Pass along an outgoing channel restriction if specified.
@@ -419,39 +420,24 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
419420

420421
return routing.NewRouteRequest(
421422
sourcePubKey, targetPubKey, amt, in.TimePref, restrictions,
422-
customRecords, routeHintEdges, blindedPmt, finalCLTVDelta,
423+
customRecords, routeHintEdges, blindedPathSet,
424+
finalCLTVDelta,
423425
)
424426
}
425427

426-
func parseBlindedPayment(in *lnrpc.QueryRoutesRequest) (
427-
*routing.BlindedPayment, error) {
428+
func parseBlindedPaymentPaths(in *lnrpc.QueryRoutesRequest) (
429+
*routing.BlindedPaymentPathSet, error) {
428430

429431
if len(in.PubKey) != 0 {
430432
return nil, fmt.Errorf("target pubkey: %x should not be set "+
431433
"when blinded path is provided", in.PubKey)
432434
}
433435

434-
if len(in.BlindedPaymentPaths) != 1 {
435-
return nil, errors.New("query routes only supports a single " +
436-
"blinded path")
437-
}
438-
439-
blindedPath := in.BlindedPaymentPaths[0]
440-
441436
if len(in.RouteHints) > 0 {
442437
return nil, errors.New("route hints and blinded path can't " +
443438
"both be set")
444439
}
445440

446-
blindedPmt, err := unmarshalBlindedPayment(blindedPath)
447-
if err != nil {
448-
return nil, fmt.Errorf("parse blinded payment: %w", err)
449-
}
450-
451-
if err := blindedPmt.Validate(); err != nil {
452-
return nil, fmt.Errorf("invalid blinded path: %w", err)
453-
}
454-
455441
if in.FinalCltvDelta != 0 {
456442
return nil, errors.New("final cltv delta should be " +
457443
"zero for blinded paths")
@@ -466,7 +452,21 @@ func parseBlindedPayment(in *lnrpc.QueryRoutesRequest) (
466452
"be populated in blinded path")
467453
}
468454

469-
return blindedPmt, nil
455+
paths := make([]*routing.BlindedPayment, len(in.BlindedPaymentPaths))
456+
for i, paymentPath := range in.BlindedPaymentPaths {
457+
blindedPmt, err := unmarshalBlindedPayment(paymentPath)
458+
if err != nil {
459+
return nil, fmt.Errorf("parse blinded payment: %w", err)
460+
}
461+
462+
if err := blindedPmt.Validate(); err != nil {
463+
return nil, fmt.Errorf("invalid blinded path: %w", err)
464+
}
465+
466+
paths[i] = blindedPmt
467+
}
468+
469+
return routing.NewBlindedPaymentPathSet(paths)
470470
}
471471

472472
func unmarshalBlindedPayment(rpcPayment *lnrpc.BlindedPaymentPath) (
@@ -1001,28 +1001,24 @@ func (r *RouterBackend) extractIntentFromSendRequest(
10011001
payIntent.Metadata = payReq.Metadata
10021002

10031003
if len(payReq.BlindedPaymentPaths) > 0 {
1004-
// NOTE: Currently we only choose a single payment path.
1005-
// This will be updated in a future PR to handle
1006-
// multiple blinded payment paths.
1007-
path := payReq.BlindedPaymentPaths[0]
1008-
if len(path.Hops) == 0 {
1009-
return nil, fmt.Errorf("a blinded payment " +
1010-
"must have at least 1 hop")
1004+
pathSet, err := BuildBlindedPathSet(
1005+
payReq.BlindedPaymentPaths,
1006+
)
1007+
if err != nil {
1008+
return nil, err
10111009
}
1010+
payIntent.BlindedPathSet = pathSet
10121011

1013-
finalHop := path.Hops[len(path.Hops)-1]
1014-
1015-
payIntent.BlindedPayment = MarshalBlindedPayment(path)
1016-
1017-
// Replace the target node with the blinded public key
1018-
// of the blinded path's final node.
1012+
// Replace the target node with the target public key
1013+
// of the blinded path set.
10191014
copy(
10201015
payIntent.Target[:],
1021-
finalHop.BlindedNodePub.SerializeCompressed(),
1016+
pathSet.TargetPubKey().SerializeCompressed(),
10221017
)
10231018

1024-
if !path.Features.IsEmpty() {
1025-
payIntent.DestFeatures = path.Features.Clone()
1019+
pathFeatures := pathSet.Features()
1020+
if !pathFeatures.IsEmpty() {
1021+
payIntent.DestFeatures = pathFeatures.Clone()
10261022
}
10271023
}
10281024
} else {
@@ -1163,9 +1159,29 @@ func (r *RouterBackend) extractIntentFromSendRequest(
11631159
return payIntent, nil
11641160
}
11651161

1166-
// MarshalBlindedPayment marshals a zpay32.BLindedPaymentPath into a
1162+
// BuildBlindedPathSet marshals a set of zpay32.BlindedPaymentPath and uses
1163+
// the result to build a new routing.BlindedPaymentPathSet.
1164+
func BuildBlindedPathSet(paths []*zpay32.BlindedPaymentPath) (
1165+
*routing.BlindedPaymentPathSet, error) {
1166+
1167+
marshalledPaths := make([]*routing.BlindedPayment, len(paths))
1168+
for i, path := range paths {
1169+
paymentPath := marshalBlindedPayment(path)
1170+
1171+
err := paymentPath.Validate()
1172+
if err != nil {
1173+
return nil, err
1174+
}
1175+
1176+
marshalledPaths[i] = paymentPath
1177+
}
1178+
1179+
return routing.NewBlindedPaymentPathSet(marshalledPaths)
1180+
}
1181+
1182+
// marshalBlindedPayment marshals a zpay32.BLindedPaymentPath into a
11671183
// routing.BlindedPayment.
1168-
func MarshalBlindedPayment(
1184+
func marshalBlindedPayment(
11691185
path *zpay32.BlindedPaymentPath) *routing.BlindedPayment {
11701186

11711187
return &routing.BlindedPayment{

0 commit comments

Comments
 (0)