Skip to content

Commit 3de6c54

Browse files
committed
multi: let blinded path invoice options be set per addinvoice call
Extend the configurability of blinded paths in invoices by adding the ability to change the global config options on a per-RPC basis.
1 parent ca91e17 commit 3de6c54

File tree

7 files changed

+2099
-1819
lines changed

7 files changed

+2099
-1819
lines changed

cmd/lncli/cmd_invoice.go

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,26 @@ var addInvoiceCommand = cli.Command{
9090
"ephemeral key so as not to reveal the real " +
9191
"node ID of this node.",
9292
},
93+
cli.UintFlag{
94+
Name: "min_real_blinded_hops",
95+
Usage: "The minimum number of real hops to use in a " +
96+
"blinded path. This option will only be used " +
97+
"if `--blind` has also been set.",
98+
},
99+
cli.UintFlag{
100+
Name: "num_blinded_hops",
101+
Usage: "The number of hops to use for each " +
102+
"blinded path included in the invoice. This " +
103+
"option will only be used if `--blind` has " +
104+
"also been set. Dummy hops will be used to " +
105+
"pad paths shorter than this.",
106+
},
107+
cli.UintFlag{
108+
Name: "max_blinded_paths",
109+
Usage: "The maximum number of blinded paths to add " +
110+
"to an invoice. This option will only be " +
111+
"used if `--blind` has also been set.",
112+
},
93113
},
94114
Action: actionDecorator(addInvoice),
95115
}
@@ -140,18 +160,24 @@ func addInvoice(ctx *cli.Context) error {
140160
"blinded paths in the same invoice")
141161
}
142162

163+
blindedPathCfg, err := parseBlindedPathCfg(ctx)
164+
if err != nil {
165+
return fmt.Errorf("could not parse blinded path config: %w",
166+
err)
167+
}
168+
143169
invoice := &lnrpc.Invoice{
144-
Memo: ctx.String("memo"),
145-
RPreimage: preimage,
146-
Value: amt,
147-
ValueMsat: amtMsat,
148-
DescriptionHash: descHash,
149-
FallbackAddr: ctx.String("fallback_addr"),
150-
Expiry: ctx.Int64("expiry"),
151-
CltvExpiry: ctx.Uint64("cltv_expiry_delta"),
152-
Private: ctx.Bool("private"),
153-
IsAmp: ctx.Bool("amp"),
154-
Blind: ctx.Bool("blind"),
170+
Memo: ctx.String("memo"),
171+
RPreimage: preimage,
172+
Value: amt,
173+
ValueMsat: amtMsat,
174+
DescriptionHash: descHash,
175+
FallbackAddr: ctx.String("fallback_addr"),
176+
Expiry: ctx.Int64("expiry"),
177+
CltvExpiry: ctx.Uint64("cltv_expiry_delta"),
178+
Private: ctx.Bool("private"),
179+
IsAmp: ctx.Bool("amp"),
180+
BlindedPathConfig: blindedPathCfg,
155181
}
156182

157183
resp, err := client.AddInvoice(ctxc, invoice)
@@ -164,6 +190,40 @@ func addInvoice(ctx *cli.Context) error {
164190
return nil
165191
}
166192

193+
func parseBlindedPathCfg(ctx *cli.Context) (*lnrpc.BlindedPathConfig, error) {
194+
if !ctx.Bool("blind") {
195+
if ctx.IsSet("min_real_blinded_hops") ||
196+
ctx.IsSet("num_blinded_hops") ||
197+
ctx.IsSet("max_blinded_paths") ||
198+
ctx.IsSet("blinded_path_omit_node") {
199+
200+
return nil, fmt.Errorf("blinded path options are " +
201+
"only used if the `--blind` options is set")
202+
}
203+
204+
return nil, nil
205+
}
206+
207+
var blindCfg lnrpc.BlindedPathConfig
208+
209+
if ctx.IsSet("min_real_blinded_hops") {
210+
minNumRealHops := uint32(ctx.Uint("min_real_blinded_hops"))
211+
blindCfg.MinNumRealHops = &minNumRealHops
212+
}
213+
214+
if ctx.IsSet("num_blinded_hops") {
215+
numHops := uint32(ctx.Uint("num_blinded_hops"))
216+
blindCfg.NumHops = &numHops
217+
}
218+
219+
if ctx.IsSet("max_blinded_paths") {
220+
maxPaths := uint32(ctx.Uint("max_blinded_paths"))
221+
blindCfg.MaxNumPaths = &maxPaths
222+
}
223+
224+
return &blindCfg, nil
225+
}
226+
167227
var lookupInvoiceCommand = cli.Command{
168228
Name: "lookupinvoice",
169229
Category: "Invoices",

itest/lnd_route_blinding_test.go

Lines changed: 73 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -363,13 +363,7 @@ func (b *blindedForwardTest) setupNetwork(ctx context.Context,
363363
require.NoError(b.ht, err, "interceptor")
364364
}
365365

366-
// Restrict Dave so that he only ever creates a single blinded path from
367-
// Bob to himself.
368-
b.dave = b.ht.NewNode("Dave", []string{
369-
"--bitcoin.timelockdelta=18",
370-
"--routing.blinding.min-num-real-hops=2",
371-
"--routing.blinding.num-hops=2",
372-
})
366+
b.dave = b.ht.NewNode("Dave", []string{"--bitcoin.timelockdelta=18"})
373367

374368
b.channels = setupFourHopNetwork(b.ht, b.carol, b.dave)
375369
}
@@ -378,11 +372,20 @@ func (b *blindedForwardTest) setupNetwork(ctx context.Context,
378372
// acting as the introduction point.
379373
func (b *blindedForwardTest) buildBlindedPath() *lnrpc.BlindedPaymentPath {
380374
// Let Dave add a blinded invoice.
375+
// Add restrictions so that he only ever creates a single blinded path
376+
// from Bob to himself.
377+
var (
378+
minNumRealHops uint32 = 2
379+
numHops uint32 = 2
380+
)
381381
invoice := b.dave.RPC.AddInvoice(&lnrpc.Invoice{
382382
RPreimage: b.preimage[:],
383383
Memo: "test",
384384
ValueMsat: 10_000_000,
385-
Blind: true,
385+
BlindedPathConfig: &lnrpc.BlindedPathConfig{
386+
MinNumRealHops: &minNumRealHops,
387+
NumHops: &numHops,
388+
},
386389
})
387390

388391
// Assert that only one blinded path is selected and that it contains
@@ -613,29 +616,37 @@ func testBlindedRouteInvoices(ht *lntest.HarnessTest) {
613616
testCase.setupNetwork(ctx, false)
614617

615618
// Let Dave add a blinded invoice.
619+
// Add restrictions so that he only ever creates a single blinded path
620+
// from Bob to himself.
621+
var (
622+
minNumRealHops uint32 = 2
623+
numHops uint32 = 2
624+
)
616625
invoice := testCase.dave.RPC.AddInvoice(&lnrpc.Invoice{
617626
Memo: "test",
618627
ValueMsat: 10_000_000,
619-
Blind: true,
628+
BlindedPathConfig: &lnrpc.BlindedPathConfig{
629+
MinNumRealHops: &minNumRealHops,
630+
NumHops: &numHops,
631+
},
620632
})
621633

622634
// Now let Alice pay the invoice.
623635
ht.CompletePaymentRequests(ht.Alice, []string{invoice.PaymentRequest})
624636

625-
// Restart Dave with blinded path restrictions that will result in him
626-
// creating a blinded path that uses himself as the introduction node.
627-
ht.RestartNodeWithExtraArgs(testCase.dave, []string{
628-
"--routing.blinding.min-num-real-hops=0",
629-
"--routing.blinding.num-hops=0",
630-
})
631-
ht.EnsureConnected(testCase.dave, testCase.carol)
632-
633637
// Let Dave add a blinded invoice.
634638
// Once again let Dave create a blinded invoice.
639+
// This time, add path restrictions that will result in him
640+
// creating a blinded path that uses himself as the introduction node.
641+
minNumRealHops = 0
642+
numHops = 0
635643
invoice = testCase.dave.RPC.AddInvoice(&lnrpc.Invoice{
636644
Memo: "test",
637645
ValueMsat: 10_000_000,
638-
Blind: true,
646+
BlindedPathConfig: &lnrpc.BlindedPathConfig{
647+
MinNumRealHops: &minNumRealHops,
648+
NumHops: &numHops,
649+
},
639650
})
640651

641652
// Assert that it contains a single blinded path with only an
@@ -898,12 +909,7 @@ func testMPPToSingleBlindedPath(ht *lntest.HarnessTest) {
898909
// nodes.
899910
alice, bob := ht.Alice, ht.Bob
900911

901-
// Restrict Dave so that he only ever chooses the Carol->Dave path for
902-
// a blinded route.
903-
dave := ht.NewNode("dave", []string{
904-
"--routing.blinding.min-num-real-hops=1",
905-
"--routing.blinding.num-hops=1",
906-
})
912+
dave := ht.NewNode("dave", nil)
907913
carol := ht.NewNode("carol", nil)
908914
eve := ht.NewNode("eve", nil)
909915

@@ -984,10 +990,19 @@ func testMPPToSingleBlindedPath(ht *lntest.HarnessTest) {
984990
}
985991

986992
// Make Dave create an invoice with a blinded path for Alice to pay.
993+
// Restrict the blinded path config such that Dave only ever chooses
994+
// the Carol->Dave path for a blinded route.
995+
var (
996+
numHops uint32 = 1
997+
minNumRealHops uint32 = 1
998+
)
987999
invoice := &lnrpc.Invoice{
9881000
Memo: "test",
9891001
Value: int64(paymentAmt),
990-
Blind: true,
1002+
BlindedPathConfig: &lnrpc.BlindedPathConfig{
1003+
NumHops: &numHops,
1004+
MinNumRealHops: &minNumRealHops,
1005+
},
9911006
}
9921007
invoiceResp := dave.RPC.AddInvoice(invoice)
9931008

@@ -1095,12 +1110,7 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) {
10951110
"--protocol.no-route-blinding",
10961111
})
10971112

1098-
// Configure Dave so that all blinded paths always contain 2 hops and
1099-
// so that there is no minimum number of real hops.
1100-
dave := ht.NewNode("dave", []string{
1101-
"--routing.blinding.min-num-real-hops=0",
1102-
"--routing.blinding.num-hops=2",
1103-
})
1113+
dave := ht.NewNode("dave", nil)
11041114

11051115
ht.EnsureConnected(alice, bob)
11061116
ht.EnsureConnected(bob, carol)
@@ -1150,10 +1160,19 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) {
11501160
}
11511161

11521162
// Make Dave create an invoice with a blinded path for Alice to pay.
1163+
// Configure the invoice so that all blinded paths always contain 2 hops
1164+
// and so that there is no minimum number of real hops.
1165+
var (
1166+
minNumRealHops uint32 = 0
1167+
numHops uint32 = 2
1168+
)
11531169
invoice := &lnrpc.Invoice{
11541170
Memo: "test",
11551171
Value: int64(paymentAmt),
1156-
Blind: true,
1172+
BlindedPathConfig: &lnrpc.BlindedPathConfig{
1173+
MinNumRealHops: &minNumRealHops,
1174+
NumHops: &numHops,
1175+
},
11571176
}
11581177
invoiceResp := dave.RPC.AddInvoice(invoice)
11591178

@@ -1178,18 +1197,24 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) {
11781197
require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State)
11791198

11801199
// Let's also test the case where Dave is not the introduction node.
1181-
// We restart Carol so that she supports route blinding. We also restart
1182-
// Dave and force a minimum of 1 real blinded hop. We keep the number
1183-
// of hops to 2 meaning that one dummy hop should be added.
1200+
// We restart Carol so that she supports route blinding.
11841201
ht.RestartNodeWithExtraArgs(carol, nil)
1185-
ht.RestartNodeWithExtraArgs(dave, []string{
1186-
"--routing.blinding.min-num-real-hops=1",
1187-
"--routing.blinding.num-hops=2",
1188-
})
11891202
ht.EnsureConnected(bob, carol)
11901203
ht.EnsureConnected(carol, dave)
11911204

11921205
// Make Dave create an invoice with a blinded path for Alice to pay.
1206+
// This time, configure the invoice so that there is always a minimum
1207+
// of 1 real blinded hop. We keep the number of total hops to 2 meaning
1208+
// that one dummy hop should be added.
1209+
minNumRealHops = 1
1210+
invoice = &lnrpc.Invoice{
1211+
Memo: "test",
1212+
Value: int64(paymentAmt),
1213+
BlindedPathConfig: &lnrpc.BlindedPathConfig{
1214+
MinNumRealHops: &minNumRealHops,
1215+
NumHops: &numHops,
1216+
},
1217+
}
11931218
invoiceResp = dave.RPC.AddInvoice(invoice)
11941219

11951220
// Assert that it contains a single blinded path and that the
@@ -1248,10 +1273,7 @@ func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) {
12481273

12491274
// Create a four-node context consisting of Alice, Bob and three new
12501275
// nodes.
1251-
dave := ht.NewNode("dave", []string{
1252-
"--routing.blinding.min-num-real-hops=1",
1253-
"--routing.blinding.num-hops=1",
1254-
})
1276+
dave := ht.NewNode("dave", nil)
12551277
carol := ht.NewNode("carol", nil)
12561278

12571279
// Connect nodes to ensure propagation of channels.
@@ -1311,10 +1333,17 @@ func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) {
13111333
// Ok now make a payment that must be split to succeed.
13121334

13131335
// Make Dave create an invoice for Alice to pay
1336+
var (
1337+
minNumRealHops uint32 = 1
1338+
numHops uint32 = 1
1339+
)
13141340
invoice := &lnrpc.Invoice{
13151341
Memo: "test",
13161342
Value: int64(paymentAmt),
1317-
Blind: true,
1343+
BlindedPathConfig: &lnrpc.BlindedPathConfig{
1344+
MinNumRealHops: &minNumRealHops,
1345+
NumHops: &numHops,
1346+
},
13181347
}
13191348
invoiceResp := dave.RPC.AddInvoice(invoice)
13201349

lnrpc/invoicesrpc/invoices.swagger.json

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,26 @@
395395
}
396396
}
397397
},
398+
"lnrpcBlindedPathConfig": {
399+
"type": "object",
400+
"properties": {
401+
"min_num_real_hops": {
402+
"type": "integer",
403+
"format": "int64",
404+
"description": "The minimum number of real hops to include in a blinded path. This doesn't\ninclude our node, so if the minimum is 1, then the path will contain at\nminimum our node along with an introduction node hop. If it is zero then\nthe shortest path will use our node as an introduction node."
405+
},
406+
"num_hops": {
407+
"type": "integer",
408+
"format": "int64",
409+
"description": "The number of hops to include in a blinded path. This doesn't include our\nnode, so if it is 1, then the path will contain our node along with an\nintroduction node or dummy node hop. If paths shorter than NumHops is\nfound, then they will be padded using dummy hops."
410+
},
411+
"max_num_paths": {
412+
"type": "integer",
413+
"format": "int64",
414+
"description": "The maximum number of blinded paths to select and add to an invoice."
415+
}
416+
}
417+
},
398418
"lnrpcFeature": {
399419
"type": "object",
400420
"properties": {
@@ -579,8 +599,8 @@
579599
"description": "Maps a 32-byte hex-encoded set ID to the sub-invoice AMP state for the\ngiven set ID. This field is always populated for AMP invoices, and can be\nused along side LookupInvoice to obtain the HTLC information related to a\ngiven sub-invoice.\nNote: Output only, don't specify for creating an invoice.",
580600
"title": "[EXPERIMENTAL]:"
581601
},
582-
"blind": {
583-
"type": "boolean",
602+
"blinded_path_config": {
603+
"$ref": "#/definitions/lnrpcBlindedPathConfig",
584604
"description": "Signals that the invoice should include blinded paths to hide the true\nidentity of the recipient."
585605
}
586606
}

0 commit comments

Comments
 (0)