mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-13 11:09:23 +01:00
Merge pull request #8976 from ellemouton/rb-follow-ups
route blinding: follow ups
This commit is contained in:
commit
459ee9b245
19 changed files with 2533 additions and 1901 deletions
|
@ -90,6 +90,32 @@ 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.",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "blinded_path_omit_node",
|
||||
Usage: "The pub key (in hex) of a node not to " +
|
||||
"use on a blinded path. The flag may be " +
|
||||
"specified multiple times.",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(addInvoice),
|
||||
}
|
||||
|
@ -140,18 +166,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 +196,51 @@ 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
|
||||
}
|
||||
|
||||
for _, pubKey := range ctx.StringSlice("blinded_path_omit_node") {
|
||||
pubKeyBytes, err := hex.DecodeString(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blindCfg.NodeOmissionList = append(
|
||||
blindCfg.NodeOmissionList, pubKeyBytes,
|
||||
)
|
||||
}
|
||||
|
||||
return &blindCfg, nil
|
||||
}
|
||||
|
||||
var lookupInvoiceCommand = cli.Command{
|
||||
Name: "lookupinvoice",
|
||||
Category: "Invoices",
|
||||
|
|
|
@ -206,6 +206,10 @@ commitment when the channel was force closed.
|
|||
* Add the ability to [send to use multiple blinded payment
|
||||
paths](https://github.com/lightningnetwork/lnd/pull/8764) in an MP payment.
|
||||
|
||||
* [Improve route blinding invoice generation
|
||||
UX](https://github.com/lightningnetwork/lnd/pull/8976) by making various
|
||||
params configurable on a per-RPC basis.
|
||||
|
||||
## Testing
|
||||
## Database
|
||||
|
||||
|
|
|
@ -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,27 +1197,29 @@ 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
|
||||
// introduction node is Carol.
|
||||
payReq = dave.RPC.DecodePayReq(invoiceResp.PaymentRequest)
|
||||
for _, path := range payReq.BlindedPaths {
|
||||
ht.Logf("intro node: %x", path.BlindedPath.IntroductionNode)
|
||||
}
|
||||
|
||||
require.Len(ht, payReq.BlindedPaths, 1)
|
||||
|
||||
// The total number of hop payloads is 3: one for the introduction node
|
||||
|
@ -1248,10 +1269,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 +1329,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)
|
||||
|
||||
|
|
|
@ -96,28 +96,6 @@ type AddInvoiceConfig struct {
|
|||
// QueryBlindedRoutes can be used to generate a few routes to this node
|
||||
// that can then be used in the construction of a blinded payment path.
|
||||
QueryBlindedRoutes func(lnwire.MilliSatoshi) ([]*route.Route, error)
|
||||
|
||||
// BlindedRoutePolicyIncrMultiplier is the amount by which policy values
|
||||
// for hops in a blinded route will be bumped to avoid easy probing. For
|
||||
// example, a multiplier of 1.1 will bump all appropriate the values
|
||||
// (base fee, fee rate, CLTV delta and min HLTC) by 10%.
|
||||
BlindedRoutePolicyIncrMultiplier float64
|
||||
|
||||
// BlindedRoutePolicyDecrMultiplier is the amount by which appropriate
|
||||
// policy values for hops in a blinded route will be decreased to avoid
|
||||
// easy probing. For example, a multiplier of 0.9 will reduce
|
||||
// appropriate values (like maximum HTLC) by 10%.
|
||||
BlindedRoutePolicyDecrMultiplier float64
|
||||
|
||||
// MinNumBlindedPathHops is the minimum number of hops that a blinded
|
||||
// path should be. Dummy hops will be used to pad any route with a
|
||||
// length less than this.
|
||||
MinNumBlindedPathHops uint8
|
||||
|
||||
// DefaultDummyHopPolicy holds the default policy values to use for
|
||||
// dummy hops in a blinded path in the case where they cant be derived
|
||||
// through other means.
|
||||
DefaultDummyHopPolicy *blindedpath.BlindedHopPolicy
|
||||
}
|
||||
|
||||
// AddInvoiceData contains the required data to create a new invoice.
|
||||
|
@ -168,16 +146,44 @@ type AddInvoiceData struct {
|
|||
// NOTE: Preimage should always be set to nil when this value is true.
|
||||
Amp bool
|
||||
|
||||
// Blind signals that this invoice should disguise the location of the
|
||||
// recipient by adding blinded payment paths to the invoice instead of
|
||||
// revealing the destination node's real pub key.
|
||||
Blind bool
|
||||
// BlindedPathCfg holds the config values to use when constructing
|
||||
// blinded paths to add to the invoice. A non-nil BlindedPathCfg signals
|
||||
// that this invoice should disguise the location of the recipient by
|
||||
// adding blinded payment paths to the invoice instead of revealing the
|
||||
// destination node's real pub key.
|
||||
BlindedPathCfg *BlindedPathConfig
|
||||
|
||||
// RouteHints are optional route hints that can each be individually
|
||||
// used to assist in reaching the invoice's destination.
|
||||
RouteHints [][]zpay32.HopHint
|
||||
}
|
||||
|
||||
// BlindedPathConfig holds the configuration values required for blinded path
|
||||
// generation for invoices.
|
||||
type BlindedPathConfig struct {
|
||||
// RoutePolicyIncrMultiplier is the amount by which policy values for
|
||||
// hops in a blinded route will be bumped to avoid easy probing. For
|
||||
// example, a multiplier of 1.1 will bump all appropriate the values
|
||||
// (base fee, fee rate, CLTV delta and min HLTC) by 10%.
|
||||
RoutePolicyIncrMultiplier float64
|
||||
|
||||
// RoutePolicyDecrMultiplier is the amount by which appropriate policy
|
||||
// values for hops in a blinded route will be decreased to avoid easy
|
||||
// probing. For example, a multiplier of 0.9 will reduce appropriate
|
||||
// values (like maximum HTLC) by 10%.
|
||||
RoutePolicyDecrMultiplier float64
|
||||
|
||||
// MinNumPathHops is the minimum number of hops that a blinded path
|
||||
// should be. Dummy hops will be used to pad any route with a length
|
||||
// less than this.
|
||||
MinNumPathHops uint8
|
||||
|
||||
// DefaultDummyHopPolicy holds the default policy values to use for
|
||||
// dummy hops in a blinded path in the case where they cant be derived
|
||||
// through other means.
|
||||
DefaultDummyHopPolicy *blindedpath.BlindedHopPolicy
|
||||
}
|
||||
|
||||
// paymentHashAndPreimage returns the payment hash and preimage for this invoice
|
||||
// depending on the configuration.
|
||||
//
|
||||
|
@ -277,7 +283,9 @@ func (d *AddInvoiceData) mppPaymentHashAndPreimage() (*lntypes.Preimage,
|
|||
func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
||||
invoice *AddInvoiceData) (*lntypes.Hash, *invoices.Invoice, error) {
|
||||
|
||||
if invoice.Amp && invoice.Blind {
|
||||
blind := invoice.BlindedPathCfg != nil
|
||||
|
||||
if invoice.Amp && blind {
|
||||
return nil, nil, fmt.Errorf("AMP invoices with blinded paths " +
|
||||
"are not yet supported")
|
||||
}
|
||||
|
@ -420,7 +428,7 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
|||
// Only include a final CLTV expiry delta if this is not a blinded
|
||||
// invoice. In a blinded invoice, this value will be added to the total
|
||||
// blinded route CLTV delta value
|
||||
if !invoice.Blind {
|
||||
if !blind {
|
||||
options = append(options, zpay32.CLTVExpiry(cltvExpiryDelta))
|
||||
}
|
||||
|
||||
|
@ -433,7 +441,7 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
|||
|
||||
// Include route hints if needed.
|
||||
if len(invoice.RouteHints) > 0 || invoice.Private {
|
||||
if invoice.Blind {
|
||||
if blind {
|
||||
return nil, nil, fmt.Errorf("can't set both hop " +
|
||||
"hints and add blinded payment paths")
|
||||
}
|
||||
|
@ -492,7 +500,9 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
if invoice.Blind {
|
||||
if blind {
|
||||
blindCfg := invoice.BlindedPathCfg
|
||||
|
||||
// Use the 10-min-per-block assumption to get a rough estimate
|
||||
// of the number of blocks until the invoice expires. We want
|
||||
// to make sure that the blinded path definitely does not expire
|
||||
|
@ -525,12 +535,12 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
|||
|
||||
//nolint:lll
|
||||
return blindedpath.AddPolicyBuffer(
|
||||
p, cfg.BlindedRoutePolicyIncrMultiplier,
|
||||
cfg.BlindedRoutePolicyDecrMultiplier,
|
||||
p, blindCfg.RoutePolicyIncrMultiplier,
|
||||
blindCfg.RoutePolicyDecrMultiplier,
|
||||
)
|
||||
},
|
||||
MinNumHops: cfg.MinNumBlindedPathHops,
|
||||
DefaultDummyHopPolicy: cfg.DefaultDummyHopPolicy,
|
||||
MinNumHops: blindCfg.MinNumPathHops,
|
||||
DefaultDummyHopPolicy: blindCfg.DefaultDummyHopPolicy,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -560,7 +570,7 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
|||
// For an invoice without a blinded path, the main node
|
||||
// key is used to sign the invoice so that the sender
|
||||
// can derive the true pub key of the recipient.
|
||||
if !invoice.Blind {
|
||||
if !blind {
|
||||
return cfg.NodeSigner.SignMessageCompact(
|
||||
msg, false,
|
||||
)
|
||||
|
|
|
@ -395,6 +395,34 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"node_omission_list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "byte"
|
||||
},
|
||||
"description": "A list of node IDs of nodes that should not be used in any of our generated\nblinded paths."
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcFeature": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -579,8 +607,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
|
@ -3843,7 +3843,36 @@ 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;
|
||||
|
||||
/*
|
||||
A list of node IDs of nodes that should not be used in any of our generated
|
||||
blinded paths.
|
||||
*/
|
||||
repeated bytes node_omission_list = 4;
|
||||
}
|
||||
|
||||
enum InvoiceHTLCState {
|
||||
|
|
|
@ -3544,6 +3544,34 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"node_omission_list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "byte"
|
||||
},
|
||||
"description": "A list of node IDs of nodes that should not be used in any of our generated\nblinded paths."
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcBlindedPaymentPath": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -5491,8 +5519,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."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -266,12 +266,12 @@ const (
|
|||
// Bolt11BlindedPathsRequired is a required feature bit that indicates
|
||||
// that the node is able to understand the blinded path tagged field in
|
||||
// a BOLT 11 invoice.
|
||||
Bolt11BlindedPathsRequired = 260
|
||||
Bolt11BlindedPathsRequired = 262
|
||||
|
||||
// Bolt11BlindedPathsOptional is an optional feature bit that indicates
|
||||
// that the node is able to understand the blinded path tagged field in
|
||||
// a BOLT 11 invoice.
|
||||
Bolt11BlindedPathsOptional = 261
|
||||
Bolt11BlindedPathsOptional = 263
|
||||
|
||||
// MaxBolt11Feature is the maximum feature bit value allowed in bolt 11
|
||||
// invoices.
|
||||
|
|
4
log.go
4
log.go
|
@ -43,6 +43,7 @@ import (
|
|||
"github.com/lightningnetwork/lnd/peer"
|
||||
"github.com/lightningnetwork/lnd/peernotifier"
|
||||
"github.com/lightningnetwork/lnd/routing"
|
||||
"github.com/lightningnetwork/lnd/routing/blindedpath"
|
||||
"github.com/lightningnetwork/lnd/rpcperms"
|
||||
"github.com/lightningnetwork/lnd/signal"
|
||||
"github.com/lightningnetwork/lnd/sweep"
|
||||
|
@ -183,6 +184,9 @@ func SetupLoggers(root *build.RotatingLogWriter, interceptor signal.Interceptor)
|
|||
AddSubLogger(root, peersrpc.Subsystem, interceptor, peersrpc.UseLogger)
|
||||
AddSubLogger(root, graph.Subsystem, interceptor, graph.UseLogger)
|
||||
AddSubLogger(root, lncfg.Subsystem, interceptor, lncfg.UseLogger)
|
||||
AddSubLogger(
|
||||
root, blindedpath.Subsystem, interceptor, blindedpath.UseLogger,
|
||||
)
|
||||
}
|
||||
|
||||
// AddSubLogger is a helper method to conveniently create and register the
|
||||
|
|
|
@ -132,20 +132,28 @@ func BuildBlindedPaymentPaths(cfg *BuildBlindedPathCfg) (
|
|||
// For each route returned, we will construct the associated blinded
|
||||
// payment path.
|
||||
for _, route := range routes {
|
||||
path, err := buildBlindedPaymentPath(
|
||||
cfg, extractCandidatePath(route),
|
||||
)
|
||||
// Extract the information we need from the route.
|
||||
candidatePath := extractCandidatePath(route)
|
||||
|
||||
// Pad the given route with dummy hops until the minimum number
|
||||
// of hops is met.
|
||||
candidatePath.padWithDummyHops(cfg.MinNumHops)
|
||||
|
||||
path, err := buildBlindedPaymentPath(cfg, candidatePath)
|
||||
if errors.Is(err, errInvalidBlindedPath) {
|
||||
log.Debugf("Not using route (%s) as a blinded path "+
|
||||
"since it resulted in an invalid blinded path",
|
||||
route)
|
||||
|
||||
continue
|
||||
} else if err != nil {
|
||||
log.Errorf("Not using route (%s) as a blinded path: %v",
|
||||
err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("Route selected for blinded path: %s", candidatePath)
|
||||
|
||||
paths = append(paths, path)
|
||||
}
|
||||
|
@ -162,13 +170,6 @@ func BuildBlindedPaymentPaths(cfg *BuildBlindedPathCfg) (
|
|||
func buildBlindedPaymentPath(cfg *BuildBlindedPathCfg, path *candidatePath) (
|
||||
*zpay32.BlindedPaymentPath, error) {
|
||||
|
||||
// Pad the given route with dummy hops until the minimum number of hops
|
||||
// is met.
|
||||
err := path.padWithDummyHops(cfg.MinNumHops)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hops, minHTLC, maxHTLC, err := collectRelayInfo(cfg, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not collect blinded path relay "+
|
||||
|
@ -663,19 +664,34 @@ type candidatePath struct {
|
|||
hops []*blindedPathHop
|
||||
}
|
||||
|
||||
// String returns a string representation of the candidatePath which can be
|
||||
// useful for logging and debugging.
|
||||
func (c *candidatePath) String() string {
|
||||
str := fmt.Sprintf("[%s (intro node)]", c.introNode)
|
||||
|
||||
for _, hop := range c.hops {
|
||||
if hop.isDummy {
|
||||
str += "--->[dummy hop]"
|
||||
continue
|
||||
}
|
||||
|
||||
str += fmt.Sprintf("--<%d>-->[%s]", hop.channelID, hop.pubKey)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// padWithDummyHops will append n dummy hops to the candidatePath hop set. The
|
||||
// pub key for the dummy hop will be the same as the pub key for the final hop
|
||||
// of the path. That way, the final hop will be able to decrypt the data
|
||||
// encrypted for each dummy hop.
|
||||
func (c *candidatePath) padWithDummyHops(n uint8) error {
|
||||
func (c *candidatePath) padWithDummyHops(n uint8) {
|
||||
for len(c.hops) < int(n) {
|
||||
c.hops = append(c.hops, &blindedPathHop{
|
||||
pubKey: c.finalNodeID,
|
||||
isDummy: true,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// blindedPathHop holds the information we need to know about a hop in a route
|
||||
|
|
|
@ -922,6 +922,75 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) {
|
|||
HtlcMinimumMsat: 1000,
|
||||
}, data.Constraints.UnwrapOrFail(t).Val)
|
||||
require.Equal(t, []byte{1, 2, 3}, data.PathID.UnwrapOrFail(t).Val)
|
||||
|
||||
// Demonstrate that BuildBlindedPaymentPaths continues to use any
|
||||
// functioning paths even if some routes cant be used to build a blinded
|
||||
// path. We do this by forcing FetchChannelEdgesByID to error out for
|
||||
// the first 2 calls. FindRoutes returns 3 routes and so by the end, we
|
||||
// still get 1 valid path.
|
||||
var errCount int
|
||||
paths, err = BuildBlindedPaymentPaths(&BuildBlindedPathCfg{
|
||||
FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route,
|
||||
error) {
|
||||
|
||||
return []*route.Route{realRoute, realRoute, realRoute},
|
||||
nil
|
||||
},
|
||||
FetchChannelEdgesByID: func(chanID uint64) (
|
||||
*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
|
||||
*models.ChannelEdgePolicy, error) {
|
||||
|
||||
// Force the call to error for the first 2 channels.
|
||||
if errCount < 2 {
|
||||
errCount++
|
||||
|
||||
return nil, nil, nil,
|
||||
fmt.Errorf("edge not found")
|
||||
}
|
||||
|
||||
policy, ok := realPolicies[chanID]
|
||||
if !ok {
|
||||
return nil, nil, nil,
|
||||
fmt.Errorf("edge not found")
|
||||
}
|
||||
|
||||
return nil, policy, nil, nil
|
||||
},
|
||||
BestHeight: func() (uint32, error) {
|
||||
return 1000, nil
|
||||
},
|
||||
// In the spec example, all the policies get replaced with
|
||||
// the same static values.
|
||||
AddPolicyBuffer: func(_ *BlindedHopPolicy) (
|
||||
*BlindedHopPolicy, error) {
|
||||
|
||||
return &BlindedHopPolicy{
|
||||
FeeRate: 500,
|
||||
BaseFee: 100,
|
||||
CLTVExpiryDelta: 144,
|
||||
MinHTLCMsat: 1000,
|
||||
MaxHTLCMsat: lnwire.MaxMilliSatoshi,
|
||||
}, nil
|
||||
},
|
||||
PathID: []byte{1, 2, 3},
|
||||
ValueMsat: 1000,
|
||||
MinFinalCLTVExpiryDelta: 12,
|
||||
BlocksUntilExpiry: 200,
|
||||
|
||||
// By setting the minimum number of hops to 4, we force 2 dummy
|
||||
// hops to be added to the real route.
|
||||
MinNumHops: 4,
|
||||
|
||||
DefaultDummyHopPolicy: &BlindedHopPolicy{
|
||||
CLTVExpiryDelta: 50,
|
||||
FeeRate: 100,
|
||||
BaseFee: 100,
|
||||
MinHTLCMsat: 1000,
|
||||
MaxHTLCMsat: lnwire.MaxMilliSatoshi,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, paths, 1)
|
||||
}
|
||||
|
||||
// TestSingleHopBlindedPath tests that blinded path construction is done
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
"github.com/lightningnetwork/lnd/feature"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/lnutils"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/record"
|
||||
|
@ -1150,6 +1151,10 @@ type blindedPathRestrictions struct {
|
|||
// path. This doesn't include our node, so if the maximum is 1, then
|
||||
// the path will contain our node along with an introduction node hop.
|
||||
maxNumHops uint8
|
||||
|
||||
// nodeOmissionSet holds a set of node IDs of nodes that we should
|
||||
// ignore during blinded path selection.
|
||||
nodeOmissionSet fn.Set[route.Vertex]
|
||||
}
|
||||
|
||||
// blindedHop holds the information about a hop we have selected for a blinded
|
||||
|
@ -1253,6 +1258,12 @@ func processNodeForBlindedPath(g Graph, node route.Vertex,
|
|||
return nil, false, nil
|
||||
}
|
||||
|
||||
// If we have explicitly been told to ignore this node for blinded paths
|
||||
// then we skip it too.
|
||||
if restrictions.nodeOmissionSet.Contains(node) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
supports, err := supportsRouteBlinding(node)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
|
|
|
@ -3705,7 +3705,26 @@ func TestFindBlindedPaths(t *testing.T) {
|
|||
"charlie,eve,bob,dave",
|
||||
})
|
||||
|
||||
// 4) Finally, we will test the special case where the destination node
|
||||
// 4) Repeat the above test but instruct the function to never use
|
||||
// charlie.
|
||||
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||
minNumHops: 2,
|
||||
maxNumHops: 3,
|
||||
nodeOmissionSet: fn.NewSet[route.Vertex](
|
||||
ctx.keyFromAlias("charlie"),
|
||||
),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// We expect the following paths:
|
||||
// - F, B, D
|
||||
// - E, B, D
|
||||
assertPaths(paths, []string{
|
||||
"frank,bob,dave",
|
||||
"eve,bob,dave",
|
||||
})
|
||||
|
||||
// 5) Finally, we will test the special case where the destination node
|
||||
// is also the recipient.
|
||||
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
|
||||
minNumHops: 0,
|
||||
|
|
|
@ -599,6 +599,10 @@ type BlindedPathRestrictions struct {
|
|||
|
||||
// MaxNumPaths is the maximum number of blinded paths to select.
|
||||
MaxNumPaths uint8
|
||||
|
||||
// NodeOmissionSet is a set of nodes that should not be used within any
|
||||
// of the blinded paths that we generate.
|
||||
NodeOmissionSet fn.Set[route.Vertex]
|
||||
}
|
||||
|
||||
// FindBlindedPaths finds a selection of paths to the destination node that can
|
||||
|
@ -611,8 +615,9 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
|
|||
// path length restrictions.
|
||||
paths, err := findBlindedPaths(
|
||||
r.cfg.RoutingGraph, destination, &blindedPathRestrictions{
|
||||
minNumHops: restrictions.MinDistanceFromIntroNode,
|
||||
maxNumHops: restrictions.NumHops,
|
||||
minNumHops: restrictions.MinDistanceFromIntroNode,
|
||||
maxNumHops: restrictions.NumHops,
|
||||
nodeOmissionSet: restrictions.NodeOmissionSet,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -681,7 +686,7 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
|
|||
|
||||
// Sort the routes based on probability.
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
return routes[i].probability < routes[j].probability
|
||||
return routes[i].probability > routes[j].probability
|
||||
})
|
||||
|
||||
// Now just choose the best paths up until the maximum number of allowed
|
||||
|
|
|
@ -3158,7 +3158,7 @@ func TestFindBlindedPathsWithMC(t *testing.T) {
|
|||
}
|
||||
|
||||
for i, path := range expectedPaths {
|
||||
require.Equal(t, expectedPaths[i], path)
|
||||
require.Equal(t, path, actualPaths[i])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3244,4 +3244,19 @@ func TestFindBlindedPathsWithMC(t *testing.T) {
|
|||
"alice,bob,dave",
|
||||
"alice,frank,dave",
|
||||
})
|
||||
|
||||
// Test that if the user explicitly indicates that we should ignore
|
||||
// the Frank node during path selection, then this is done.
|
||||
routes, err = ctx.router.FindBlindedPaths(
|
||||
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
|
||||
MinDistanceFromIntroNode: 2,
|
||||
NumHops: 2,
|
||||
MaxNumPaths: 3,
|
||||
NodeOmissionSet: fn.NewSet(frank),
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assertPaths(routes, []string{
|
||||
"alice,bob,dave",
|
||||
})
|
||||
}
|
||||
|
|
95
rpcserver.go
95
rpcserver.go
|
@ -5210,6 +5210,7 @@ func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPayme
|
|||
if rpcPayReq.PaymentRequest != "" {
|
||||
payReq, err := zpay32.Decode(
|
||||
rpcPayReq.PaymentRequest, r.cfg.ActiveNetParams.Params,
|
||||
zpay32.WithErrorOnUnknownFeatureBit(),
|
||||
)
|
||||
if err != nil {
|
||||
return payIntent, err
|
||||
|
@ -5777,13 +5778,50 @@ 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,
|
||||
NodeOmissionSet: fn.NewSet[route.Vertex](),
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
for _, nodeIDBytes := range blindCfg.NodeOmissionList {
|
||||
vertex, err := route.NewVertexFromBytes(nodeIDBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blindingRestrictions.NodeOmissionSet.Add(vertex)
|
||||
}
|
||||
}
|
||||
|
||||
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 +5835,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
|
||||
|
@ -5820,10 +5858,6 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
|
|||
},
|
||||
GetAlias: r.server.aliasMgr.GetPeerAlias,
|
||||
BestHeight: r.server.cc.BestBlockTracker.BestHeight,
|
||||
BlindedRoutePolicyIncrMultiplier: r.server.cfg.Routing.
|
||||
BlindedPaths.PolicyIncreaseMultiplier,
|
||||
BlindedRoutePolicyDecrMultiplier: r.server.cfg.Routing.
|
||||
BlindedPaths.PolicyDecreaseMultiplier,
|
||||
QueryBlindedRoutes: func(amt lnwire.MilliSatoshi) (
|
||||
[]*route.Route, error) {
|
||||
|
||||
|
@ -5833,18 +5867,6 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
|
|||
blindingRestrictions,
|
||||
)
|
||||
},
|
||||
MinNumBlindedPathHops: r.server.cfg.Routing.BlindedPaths.
|
||||
NumHops,
|
||||
DefaultDummyHopPolicy: &blindedpath.BlindedHopPolicy{
|
||||
CLTVExpiryDelta: uint16(defaultDelta),
|
||||
FeeRate: uint32(r.server.cfg.Bitcoin.FeeRate),
|
||||
BaseFee: r.server.cfg.Bitcoin.BaseFee,
|
||||
MinHTLCMsat: r.server.cfg.Bitcoin.MinHTLCIn,
|
||||
|
||||
// MaxHTLCMsat will be calculated on the fly by using
|
||||
// the introduction node's channel's capacities.
|
||||
MaxHTLCMsat: 0,
|
||||
},
|
||||
}
|
||||
|
||||
value, err := lnrpc.UnmarshallAmt(invoice.Value, invoice.ValueMsat)
|
||||
|
@ -5857,6 +5879,33 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var blindedPathCfg *invoicesrpc.BlindedPathConfig
|
||||
if blind {
|
||||
bpConfig := r.server.cfg.Routing.BlindedPaths
|
||||
|
||||
blindedPathCfg = &invoicesrpc.BlindedPathConfig{
|
||||
RoutePolicyIncrMultiplier: bpConfig.
|
||||
PolicyIncreaseMultiplier,
|
||||
RoutePolicyDecrMultiplier: bpConfig.
|
||||
PolicyDecreaseMultiplier,
|
||||
DefaultDummyHopPolicy: &blindedpath.BlindedHopPolicy{
|
||||
CLTVExpiryDelta: uint16(defaultDelta),
|
||||
FeeRate: uint32(
|
||||
r.server.cfg.Bitcoin.FeeRate,
|
||||
),
|
||||
BaseFee: r.server.cfg.Bitcoin.BaseFee,
|
||||
MinHTLCMsat: r.server.cfg.Bitcoin.MinHTLCIn,
|
||||
|
||||
// MaxHTLCMsat will be calculated on the fly by
|
||||
// using the introduction node's channel's
|
||||
// capacities.
|
||||
MaxHTLCMsat: 0,
|
||||
},
|
||||
MinNumPathHops: blindingRestrictions.NumHops,
|
||||
}
|
||||
}
|
||||
|
||||
addInvoiceData := &invoicesrpc.AddInvoiceData{
|
||||
Memo: invoice.Memo,
|
||||
Value: value,
|
||||
|
@ -5867,7 +5916,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
|
|||
Private: invoice.Private,
|
||||
RouteHints: routeHints,
|
||||
Amp: invoice.IsAmp,
|
||||
Blind: invoice.Blind,
|
||||
BlindedPathCfg: blindedPathCfg,
|
||||
}
|
||||
|
||||
if invoice.RPreimage != nil {
|
||||
|
|
|
@ -17,10 +17,53 @@ import (
|
|||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
// DecodeOption is a type that can be used to supply functional options to the
|
||||
// Decode function.
|
||||
type DecodeOption func(*decodeOptions)
|
||||
|
||||
// WithKnownFeatureBits is a functional option that overwrites the set of
|
||||
// known feature bits. If not set, then LND's lnwire.Features variable will be
|
||||
// used by default.
|
||||
func WithKnownFeatureBits(features map[lnwire.FeatureBit]string) DecodeOption {
|
||||
return func(options *decodeOptions) {
|
||||
options.knownFeatureBits = features
|
||||
}
|
||||
}
|
||||
|
||||
// WithErrorOnUnknownFeatureBit is a functional option that will cause the
|
||||
// Decode function to return an error if the decoded invoice contains an unknown
|
||||
// feature bit.
|
||||
func WithErrorOnUnknownFeatureBit() DecodeOption {
|
||||
return func(options *decodeOptions) {
|
||||
options.errorOnUnknownFeature = true
|
||||
}
|
||||
}
|
||||
|
||||
// decodeOptions holds the set of Decode options.
|
||||
type decodeOptions struct {
|
||||
knownFeatureBits map[lnwire.FeatureBit]string
|
||||
errorOnUnknownFeature bool
|
||||
}
|
||||
|
||||
// newDecodeOptions constructs the default decodeOptions struct.
|
||||
func newDecodeOptions() *decodeOptions {
|
||||
return &decodeOptions{
|
||||
knownFeatureBits: lnwire.Features,
|
||||
errorOnUnknownFeature: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Decode parses the provided encoded invoice and returns a decoded Invoice if
|
||||
// it is valid by BOLT-0011 and matches the provided active network.
|
||||
func Decode(invoice string, net *chaincfg.Params) (*Invoice, error) {
|
||||
decodedInvoice := Invoice{}
|
||||
func Decode(invoice string, net *chaincfg.Params, opts ...DecodeOption) (
|
||||
*Invoice, error) {
|
||||
|
||||
options := newDecodeOptions()
|
||||
for _, o := range opts {
|
||||
o(options)
|
||||
}
|
||||
|
||||
var decodedInvoice Invoice
|
||||
|
||||
// Before bech32 decoding the invoice, make sure that it is not too large.
|
||||
// This is done as an anti-DoS measure since bech32 decoding is expensive.
|
||||
|
@ -134,7 +177,7 @@ func Decode(invoice string, net *chaincfg.Params) (*Invoice, error) {
|
|||
// If no feature vector was decoded, populate an empty one.
|
||||
if decodedInvoice.Features == nil {
|
||||
decodedInvoice.Features = lnwire.NewFeatureVector(
|
||||
nil, lnwire.Features,
|
||||
nil, options.knownFeatureBits,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -144,6 +187,24 @@ func Decode(invoice string, net *chaincfg.Params) (*Invoice, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if options.errorOnUnknownFeature {
|
||||
// Make sure that we understand all the required feature bits
|
||||
// in the invoice.
|
||||
unknownFeatureBits := decodedInvoice.Features.
|
||||
UnknownRequiredFeatures()
|
||||
|
||||
if len(unknownFeatureBits) > 0 {
|
||||
errStr := fmt.Sprintf("invoice contains " +
|
||||
"unknown feature bits:")
|
||||
|
||||
for _, bit := range unknownFeatureBits {
|
||||
errStr += fmt.Sprintf(" %d,", bit)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf(strings.TrimRight(errStr, ","))
|
||||
}
|
||||
}
|
||||
|
||||
return &decodedInvoice, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -191,6 +191,7 @@ func TestDecodeEncode(t *testing.T) {
|
|||
encodedInvoice string
|
||||
valid bool
|
||||
decodedInvoice func() *Invoice
|
||||
decodeOpts []DecodeOption
|
||||
skipEncoding bool
|
||||
beforeEncoding func(*Invoice)
|
||||
}{
|
||||
|
@ -758,6 +759,70 @@ func TestDecodeEncode(t *testing.T) {
|
|||
i.Destination = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
// Invoice with unknown feature bits but since the
|
||||
// WithErrorOnUnknownFeatureBit option is not provided,
|
||||
// it is not expected to error out.
|
||||
encodedInvoice: "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk",
|
||||
valid: true,
|
||||
skipEncoding: true,
|
||||
decodedInvoice: func() *Invoice {
|
||||
return &Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat25mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
PaymentAddr: &specPaymentAddr,
|
||||
Description: &testCoffeeBeans,
|
||||
Destination: testPubKey,
|
||||
Features: lnwire.NewFeatureVector(
|
||||
lnwire.NewRawFeatureVector(
|
||||
9, 15, 99, 100,
|
||||
),
|
||||
lnwire.Features,
|
||||
),
|
||||
}
|
||||
},
|
||||
decodeOpts: []DecodeOption{
|
||||
WithKnownFeatureBits(map[lnwire.FeatureBit]string{
|
||||
9: "9",
|
||||
15: "15",
|
||||
99: "99",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Invoice with unknown feature bits with option set to
|
||||
// error out on unknown feature bit.
|
||||
encodedInvoice: "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk",
|
||||
valid: false,
|
||||
skipEncoding: true,
|
||||
decodedInvoice: func() *Invoice {
|
||||
return &Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat25mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
PaymentAddr: &specPaymentAddr,
|
||||
Description: &testCoffeeBeans,
|
||||
Destination: testPubKey,
|
||||
Features: lnwire.NewFeatureVector(
|
||||
lnwire.NewRawFeatureVector(
|
||||
9, 15, 99, 100,
|
||||
),
|
||||
lnwire.Features,
|
||||
),
|
||||
}
|
||||
},
|
||||
decodeOpts: []DecodeOption{
|
||||
WithKnownFeatureBits(map[lnwire.FeatureBit]string{
|
||||
9: "9",
|
||||
15: "15",
|
||||
99: "99",
|
||||
}),
|
||||
WithErrorOnUnknownFeatureBit(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
@ -773,7 +838,9 @@ func TestDecodeEncode(t *testing.T) {
|
|||
net = decodedInvoice.Net
|
||||
}
|
||||
|
||||
invoice, err := Decode(test.encodedInvoice, net)
|
||||
invoice, err := Decode(
|
||||
test.encodedInvoice, net, test.decodeOpts...,
|
||||
)
|
||||
if !test.valid {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
|
|
Loading…
Add table
Reference in a new issue