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.
This commit is contained in:
Elle Mouton 2024-08-05 13:51:12 +02:00
parent ca91e17115
commit 3de6c5415a
No known key found for this signature in database
GPG key ID: D7D916376026F177
7 changed files with 2101 additions and 1821 deletions

View file

@ -90,6 +90,26 @@ var addInvoiceCommand = cli.Command{
"ephemeral key so as not to reveal the real " +
"node ID of this node.",
},
cli.UintFlag{
Name: "min_real_blinded_hops",
Usage: "The minimum number of real hops to use in a " +
"blinded path. This option will only be used " +
"if `--blind` has also been set.",
},
cli.UintFlag{
Name: "num_blinded_hops",
Usage: "The number of hops to use for each " +
"blinded path included in the invoice. This " +
"option will only be used if `--blind` has " +
"also been set. Dummy hops will be used to " +
"pad paths shorter than this.",
},
cli.UintFlag{
Name: "max_blinded_paths",
Usage: "The maximum number of blinded paths to add " +
"to an invoice. This option will only be " +
"used if `--blind` has also been set.",
},
},
Action: actionDecorator(addInvoice),
}
@ -140,18 +160,24 @@ func addInvoice(ctx *cli.Context) error {
"blinded paths in the same invoice")
}
blindedPathCfg, err := parseBlindedPathCfg(ctx)
if err != nil {
return fmt.Errorf("could not parse blinded path config: %w",
err)
}
invoice := &lnrpc.Invoice{
Memo: ctx.String("memo"),
RPreimage: preimage,
Value: amt,
ValueMsat: amtMsat,
DescriptionHash: descHash,
FallbackAddr: ctx.String("fallback_addr"),
Expiry: ctx.Int64("expiry"),
CltvExpiry: ctx.Uint64("cltv_expiry_delta"),
Private: ctx.Bool("private"),
IsAmp: ctx.Bool("amp"),
Blind: ctx.Bool("blind"),
Memo: ctx.String("memo"),
RPreimage: preimage,
Value: amt,
ValueMsat: amtMsat,
DescriptionHash: descHash,
FallbackAddr: ctx.String("fallback_addr"),
Expiry: ctx.Int64("expiry"),
CltvExpiry: ctx.Uint64("cltv_expiry_delta"),
Private: ctx.Bool("private"),
IsAmp: ctx.Bool("amp"),
BlindedPathConfig: blindedPathCfg,
}
resp, err := client.AddInvoice(ctxc, invoice)
@ -164,6 +190,40 @@ func addInvoice(ctx *cli.Context) error {
return nil
}
func parseBlindedPathCfg(ctx *cli.Context) (*lnrpc.BlindedPathConfig, error) {
if !ctx.Bool("blind") {
if ctx.IsSet("min_real_blinded_hops") ||
ctx.IsSet("num_blinded_hops") ||
ctx.IsSet("max_blinded_paths") ||
ctx.IsSet("blinded_path_omit_node") {
return nil, fmt.Errorf("blinded path options are " +
"only used if the `--blind` options is set")
}
return nil, nil
}
var blindCfg lnrpc.BlindedPathConfig
if ctx.IsSet("min_real_blinded_hops") {
minNumRealHops := uint32(ctx.Uint("min_real_blinded_hops"))
blindCfg.MinNumRealHops = &minNumRealHops
}
if ctx.IsSet("num_blinded_hops") {
numHops := uint32(ctx.Uint("num_blinded_hops"))
blindCfg.NumHops = &numHops
}
if ctx.IsSet("max_blinded_paths") {
maxPaths := uint32(ctx.Uint("max_blinded_paths"))
blindCfg.MaxNumPaths = &maxPaths
}
return &blindCfg, nil
}
var lookupInvoiceCommand = cli.Command{
Name: "lookupinvoice",
Category: "Invoices",

View file

@ -363,13 +363,7 @@ func (b *blindedForwardTest) setupNetwork(ctx context.Context,
require.NoError(b.ht, err, "interceptor")
}
// Restrict Dave so that he only ever creates a single blinded path from
// Bob to himself.
b.dave = b.ht.NewNode("Dave", []string{
"--bitcoin.timelockdelta=18",
"--routing.blinding.min-num-real-hops=2",
"--routing.blinding.num-hops=2",
})
b.dave = b.ht.NewNode("Dave", []string{"--bitcoin.timelockdelta=18"})
b.channels = setupFourHopNetwork(b.ht, b.carol, b.dave)
}
@ -378,11 +372,20 @@ func (b *blindedForwardTest) setupNetwork(ctx context.Context,
// acting as the introduction point.
func (b *blindedForwardTest) buildBlindedPath() *lnrpc.BlindedPaymentPath {
// Let Dave add a blinded invoice.
// Add restrictions so that he only ever creates a single blinded path
// from Bob to himself.
var (
minNumRealHops uint32 = 2
numHops uint32 = 2
)
invoice := b.dave.RPC.AddInvoice(&lnrpc.Invoice{
RPreimage: b.preimage[:],
Memo: "test",
ValueMsat: 10_000_000,
Blind: true,
BlindedPathConfig: &lnrpc.BlindedPathConfig{
MinNumRealHops: &minNumRealHops,
NumHops: &numHops,
},
})
// Assert that only one blinded path is selected and that it contains
@ -613,29 +616,37 @@ func testBlindedRouteInvoices(ht *lntest.HarnessTest) {
testCase.setupNetwork(ctx, false)
// Let Dave add a blinded invoice.
// Add restrictions so that he only ever creates a single blinded path
// from Bob to himself.
var (
minNumRealHops uint32 = 2
numHops uint32 = 2
)
invoice := testCase.dave.RPC.AddInvoice(&lnrpc.Invoice{
Memo: "test",
ValueMsat: 10_000_000,
Blind: true,
BlindedPathConfig: &lnrpc.BlindedPathConfig{
MinNumRealHops: &minNumRealHops,
NumHops: &numHops,
},
})
// Now let Alice pay the invoice.
ht.CompletePaymentRequests(ht.Alice, []string{invoice.PaymentRequest})
// Restart Dave with blinded path restrictions that will result in him
// creating a blinded path that uses himself as the introduction node.
ht.RestartNodeWithExtraArgs(testCase.dave, []string{
"--routing.blinding.min-num-real-hops=0",
"--routing.blinding.num-hops=0",
})
ht.EnsureConnected(testCase.dave, testCase.carol)
// Let Dave add a blinded invoice.
// Once again let Dave create a blinded invoice.
// This time, add path restrictions that will result in him
// creating a blinded path that uses himself as the introduction node.
minNumRealHops = 0
numHops = 0
invoice = testCase.dave.RPC.AddInvoice(&lnrpc.Invoice{
Memo: "test",
ValueMsat: 10_000_000,
Blind: true,
BlindedPathConfig: &lnrpc.BlindedPathConfig{
MinNumRealHops: &minNumRealHops,
NumHops: &numHops,
},
})
// Assert that it contains a single blinded path with only an
@ -898,12 +909,7 @@ func testMPPToSingleBlindedPath(ht *lntest.HarnessTest) {
// nodes.
alice, bob := ht.Alice, ht.Bob
// Restrict Dave so that he only ever chooses the Carol->Dave path for
// a blinded route.
dave := ht.NewNode("dave", []string{
"--routing.blinding.min-num-real-hops=1",
"--routing.blinding.num-hops=1",
})
dave := ht.NewNode("dave", nil)
carol := ht.NewNode("carol", nil)
eve := ht.NewNode("eve", nil)
@ -984,10 +990,19 @@ func testMPPToSingleBlindedPath(ht *lntest.HarnessTest) {
}
// Make Dave create an invoice with a blinded path for Alice to pay.
// Restrict the blinded path config such that Dave only ever chooses
// the Carol->Dave path for a blinded route.
var (
numHops uint32 = 1
minNumRealHops uint32 = 1
)
invoice := &lnrpc.Invoice{
Memo: "test",
Value: int64(paymentAmt),
Blind: true,
BlindedPathConfig: &lnrpc.BlindedPathConfig{
NumHops: &numHops,
MinNumRealHops: &minNumRealHops,
},
}
invoiceResp := dave.RPC.AddInvoice(invoice)
@ -1095,12 +1110,7 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) {
"--protocol.no-route-blinding",
})
// Configure Dave so that all blinded paths always contain 2 hops and
// so that there is no minimum number of real hops.
dave := ht.NewNode("dave", []string{
"--routing.blinding.min-num-real-hops=0",
"--routing.blinding.num-hops=2",
})
dave := ht.NewNode("dave", nil)
ht.EnsureConnected(alice, bob)
ht.EnsureConnected(bob, carol)
@ -1150,10 +1160,19 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) {
}
// Make Dave create an invoice with a blinded path for Alice to pay.
// Configure the invoice so that all blinded paths always contain 2 hops
// and so that there is no minimum number of real hops.
var (
minNumRealHops uint32 = 0
numHops uint32 = 2
)
invoice := &lnrpc.Invoice{
Memo: "test",
Value: int64(paymentAmt),
Blind: true,
BlindedPathConfig: &lnrpc.BlindedPathConfig{
MinNumRealHops: &minNumRealHops,
NumHops: &numHops,
},
}
invoiceResp := dave.RPC.AddInvoice(invoice)
@ -1178,18 +1197,24 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) {
require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State)
// Let's also test the case where Dave is not the introduction node.
// We restart Carol so that she supports route blinding. We also restart
// Dave and force a minimum of 1 real blinded hop. We keep the number
// of hops to 2 meaning that one dummy hop should be added.
// We restart Carol so that she supports route blinding.
ht.RestartNodeWithExtraArgs(carol, nil)
ht.RestartNodeWithExtraArgs(dave, []string{
"--routing.blinding.min-num-real-hops=1",
"--routing.blinding.num-hops=2",
})
ht.EnsureConnected(bob, carol)
ht.EnsureConnected(carol, dave)
// Make Dave create an invoice with a blinded path for Alice to pay.
// This time, configure the invoice so that there is always a minimum
// of 1 real blinded hop. We keep the number of total hops to 2 meaning
// that one dummy hop should be added.
minNumRealHops = 1
invoice = &lnrpc.Invoice{
Memo: "test",
Value: int64(paymentAmt),
BlindedPathConfig: &lnrpc.BlindedPathConfig{
MinNumRealHops: &minNumRealHops,
NumHops: &numHops,
},
}
invoiceResp = dave.RPC.AddInvoice(invoice)
// Assert that it contains a single blinded path and that the
@ -1248,10 +1273,7 @@ func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) {
// Create a four-node context consisting of Alice, Bob and three new
// nodes.
dave := ht.NewNode("dave", []string{
"--routing.blinding.min-num-real-hops=1",
"--routing.blinding.num-hops=1",
})
dave := ht.NewNode("dave", nil)
carol := ht.NewNode("carol", nil)
// Connect nodes to ensure propagation of channels.
@ -1311,10 +1333,17 @@ func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) {
// Ok now make a payment that must be split to succeed.
// Make Dave create an invoice for Alice to pay
var (
minNumRealHops uint32 = 1
numHops uint32 = 1
)
invoice := &lnrpc.Invoice{
Memo: "test",
Value: int64(paymentAmt),
Blind: true,
BlindedPathConfig: &lnrpc.BlindedPathConfig{
MinNumRealHops: &minNumRealHops,
NumHops: &numHops,
},
}
invoiceResp := dave.RPC.AddInvoice(invoice)

View file

@ -395,6 +395,26 @@
}
}
},
"lnrpcBlindedPathConfig": {
"type": "object",
"properties": {
"min_num_real_hops": {
"type": "integer",
"format": "int64",
"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."
},
"num_hops": {
"type": "integer",
"format": "int64",
"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."
},
"max_num_paths": {
"type": "integer",
"format": "int64",
"description": "The maximum number of blinded paths to select and add to an invoice."
}
}
},
"lnrpcFeature": {
"type": "object",
"properties": {
@ -579,8 +599,8 @@
"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.",
"title": "[EXPERIMENTAL]:"
},
"blind": {
"type": "boolean",
"blinded_path_config": {
"$ref": "#/definitions/lnrpcBlindedPathConfig",
"description": "Signals that the invoice should include blinded paths to hide the true\nidentity of the recipient."
}
}

File diff suppressed because it is too large Load diff

View file

@ -3843,7 +3843,30 @@ message Invoice {
Signals that the invoice should include blinded paths to hide the true
identity of the recipient.
*/
bool blind = 29;
BlindedPathConfig blinded_path_config = 29;
}
message BlindedPathConfig {
/*
The minimum number of real hops to include in a blinded path. This doesn't
include our node, so if the minimum is 1, then the path will contain at
minimum our node along with an introduction node hop. If it is zero then
the shortest path will use our node as an introduction node.
*/
optional uint32 min_num_real_hops = 1;
/*
The number of hops to include in a blinded path. This doesn't include our
node, so if it is 1, then the path will contain our node along with an
introduction node or dummy node hop. If paths shorter than NumHops is
found, then they will be padded using dummy hops.
*/
optional uint32 num_hops = 2;
/*
The maximum number of blinded paths to select and add to an invoice.
*/
optional uint32 max_num_paths = 3;
}
enum InvoiceHTLCState {

View file

@ -3544,6 +3544,26 @@
}
}
},
"lnrpcBlindedPathConfig": {
"type": "object",
"properties": {
"min_num_real_hops": {
"type": "integer",
"format": "int64",
"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."
},
"num_hops": {
"type": "integer",
"format": "int64",
"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."
},
"max_num_paths": {
"type": "integer",
"format": "int64",
"description": "The maximum number of blinded paths to select and add to an invoice."
}
}
},
"lnrpcBlindedPaymentPath": {
"type": "object",
"properties": {
@ -5491,8 +5511,8 @@
"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.",
"title": "[EXPERIMENTAL]:"
},
"blind": {
"type": "boolean",
"blinded_path_config": {
"$ref": "#/definitions/lnrpcBlindedPathConfig",
"description": "Signals that the invoice should include blinded paths to hide the true\nidentity of the recipient."
}
}

View file

@ -5777,13 +5777,40 @@ func (r *rpcServer) sendPaymentSync(
func (r *rpcServer) AddInvoice(ctx context.Context,
invoice *lnrpc.Invoice) (*lnrpc.AddInvoiceResponse, error) {
defaultDelta := r.cfg.Bitcoin.TimeLockDelta
var (
defaultDelta = r.cfg.Bitcoin.TimeLockDelta
blindCfg = invoice.BlindedPathConfig
blind = blindCfg != nil
)
globalBlindCfg := r.server.cfg.Routing.BlindedPaths
blindingRestrictions := &routing.BlindedPathRestrictions{
MinDistanceFromIntroNode: r.server.cfg.Routing.BlindedPaths.
MinNumRealHops,
NumHops: r.server.cfg.Routing.BlindedPaths.NumHops,
MaxNumPaths: r.server.cfg.Routing.BlindedPaths.MaxNumPaths,
MinDistanceFromIntroNode: globalBlindCfg.MinNumRealHops,
NumHops: globalBlindCfg.NumHops,
MaxNumPaths: globalBlindCfg.MaxNumPaths,
}
if blind {
if blindCfg.MinNumRealHops != nil {
blindingRestrictions.MinDistanceFromIntroNode =
uint8(*blindCfg.MinNumRealHops)
}
if blindCfg.NumHops != nil {
blindingRestrictions.NumHops = uint8(*blindCfg.NumHops)
}
if blindCfg.MaxNumPaths != nil {
blindingRestrictions.MaxNumPaths =
uint8(*blindCfg.MaxNumPaths)
}
}
if blindingRestrictions.MinDistanceFromIntroNode >
blindingRestrictions.NumHops {
return nil, fmt.Errorf("the minimum number of real " +
"hops in a blinded path must be smaller than " +
"or equal to the number of hops expected to " +
"be included in each path")
}
addInvoiceCfg := &invoicesrpc.AddInvoiceConfig{
@ -5797,7 +5824,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
GenInvoiceFeatures: func() *lnwire.FeatureVector {
v := r.server.featureMgr.Get(feature.SetInvoice)
if invoice.Blind {
if blind {
// If an invoice includes blinded paths, then a
// payment address is not required since we use
// the PathID in the final hop's encrypted data
@ -5843,7 +5870,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
}
var blindedPathCfg *invoicesrpc.BlindedPathConfig
if invoice.Blind {
if blind {
bpConfig := r.server.cfg.Routing.BlindedPaths
blindedPathCfg = &invoicesrpc.BlindedPathConfig{
@ -5864,8 +5891,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
// capacities.
MaxHTLCMsat: 0,
},
MinNumPathHops: r.server.cfg.Routing.BlindedPaths.
NumHops,
MinNumPathHops: blindingRestrictions.NumHops,
}
}