Merge pull request #8976 from ellemouton/rb-follow-ups

route blinding: follow ups
This commit is contained in:
Olaoluwa Osuntokun 2024-08-07 17:54:31 -07:00 committed by GitHub
commit 459ee9b245
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 2533 additions and 1901 deletions

View file

@ -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",

View file

@ -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

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,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)

View file

@ -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,
)

View file

@ -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

View file

@ -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 {

View file

@ -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."
}
}

View file

@ -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
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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",
})
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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 {