Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multi: route blinding for hodl invoices #9034

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
100 changes: 53 additions & 47 deletions cmd/commands/cmd_invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,41 @@ import (
"github.com/urfave/cli"
)

var (
blindFlag = cli.BoolFlag{
Name: "blind",
Usage: "creates an invoice that contains blinded paths. Note " +
"that invoices with blinded paths will be signed " +
"using a random ephemeral key so as not to reveal " +
"the real node ID of this node.",
}
minRealBlindedHops = 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.",
}
numBlindedHops = 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.",
}
maxBlindedPaths = 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.",
}
blindedPathNodeOmissions = 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.",
}
)

var AddInvoiceCommand = cli.Command{
Name: "addinvoice",
Category: "Invoices",
Expand Down Expand Up @@ -82,40 +117,11 @@ var AddInvoiceCommand = cli.Command{
Usage: "creates an AMP invoice. If true, preimage " +
"should not be set.",
},
cli.BoolFlag{
Name: "blind",
Usage: "creates an invoice that contains blinded " +
"paths. Note that invoices with blinded " +
"paths will be signed using a random " +
"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.",
},
blindFlag,
minRealBlindedHops,
numBlindedHops,
maxBlindedPaths,
blindedPathNodeOmissions,
},
Action: actionDecorator(addInvoice),
}
Expand Down Expand Up @@ -183,7 +189,7 @@ func addInvoice(ctx *cli.Context) error {
CltvExpiry: ctx.Uint64("cltv_expiry_delta"),
Private: ctx.Bool("private"),
IsAmp: ctx.Bool("amp"),
IsBlinded: ctx.Bool("blind"),
IsBlinded: ctx.Bool(blindFlag.Name),
BlindedPathConfig: blindedPathCfg,
}

Expand All @@ -198,11 +204,11 @@ func addInvoice(ctx *cli.Context) error {
}

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") {
if !ctx.Bool(blindFlag.Name) {
if ctx.IsSet(minRealBlindedHops.Name) ||
ctx.IsSet(numBlindedHops.Name) ||
ctx.IsSet(maxBlindedPaths.Name) ||
ctx.IsSet(blindedPathNodeOmissions.Name) {

return nil, fmt.Errorf("blinded path options are " +
"only used if the `--blind` options is set")
Expand All @@ -213,22 +219,22 @@ func parseBlindedPathCfg(ctx *cli.Context) (*lnrpc.BlindedPathConfig, error) {

var blindCfg lnrpc.BlindedPathConfig

if ctx.IsSet("min_real_blinded_hops") {
minNumRealHops := uint32(ctx.Uint("min_real_blinded_hops"))
if ctx.IsSet(minRealBlindedHops.Name) {
minNumRealHops := uint32(ctx.Uint(minRealBlindedHops.Name))
blindCfg.MinNumRealHops = &minNumRealHops
}

if ctx.IsSet("num_blinded_hops") {
numHops := uint32(ctx.Uint("num_blinded_hops"))
if ctx.IsSet(numBlindedHops.Name) {
numHops := uint32(ctx.Uint(numBlindedHops.Name))
blindCfg.NumHops = &numHops
}

if ctx.IsSet("max_blinded_paths") {
maxPaths := uint32(ctx.Uint("max_blinded_paths"))
if ctx.IsSet(maxBlindedPaths.Name) {
maxPaths := uint32(ctx.Uint(maxBlindedPaths.Name))
blindCfg.MaxNumPaths = &maxPaths
}

for _, pubKey := range ctx.StringSlice("blinded_path_omit_node") {
for _, pubKey := range ctx.StringSlice(blindedPathNodeOmissions.Name) {
pubKeyBytes, err := hex.DecodeString(pubKey)
if err != nil {
return nil, err
Expand Down
31 changes: 22 additions & 9 deletions cmd/commands/invoicesrpc_active.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ var addHoldInvoiceCommand = cli.Command{
"private channels in order to assist the " +
"payer in reaching you",
},
blindFlag,
minRealBlindedHops,
numBlindedHops,
maxBlindedPaths,
blindedPathNodeOmissions,
},
Action: actionDecorator(addHoldInvoice),
}
Expand Down Expand Up @@ -241,16 +246,24 @@ func addHoldInvoice(ctx *cli.Context) error {
return fmt.Errorf("unable to parse description_hash: %w", err)
}

blindedPathCfg, err := parseBlindedPathCfg(ctx)
if err != nil {
return fmt.Errorf("could not parse blinded path config: %w",
err)
}

invoice := &invoicesrpc.AddHoldInvoiceRequest{
Memo: ctx.String("memo"),
Hash: hash,
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"),
Memo: ctx.String("memo"),
Hash: hash,
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"),
IsBlinded: ctx.Bool(blindFlag.Name),
BlindedPathConfig: blindedPathCfg,
}

resp, err := client.AddHoldInvoice(ctxc, invoice)
Expand Down
4 changes: 4 additions & 0 deletions docs/release-notes/release-notes-0.19.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@

# New Features
## Functional Enhancements

* Add route blinding receive functionality to for [hodl
invoices](https://github.com/lightningnetwork/lnd/pull/9034).

Comment on lines +28 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve grammar and clarity in the new feature description.

The sentence structure in this new feature description can be improved for better clarity and grammatical correctness.

Consider applying the following changes:

-* Add route blinding receive functionality to for [hodl 
-  invoices](https://github.com/lightningnetwork/lnd/pull/9034).
+* Add route blinding receive functionality for [hodl 
+  invoices](https://github.com/lightningnetwork/lnd/pull/9034).

This change removes the redundant "to" and improves the overall readability of the sentence.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* Add route blinding receive functionality to for [hodl
invoices](https://github.com/lightningnetwork/lnd/pull/9034).
* Add route blinding receive functionality for [hodl
invoices](https://github.com/lightningnetwork/lnd/pull/9034).
🧰 Tools
🪛 LanguageTool

[grammar] ~29-~29: There seems to be a noun/verb agreement error. Did you mean “receives” or “received”?
Context: ...onal Enhancements * Add route blinding receive functionality to for [hodl invoices]...

(SINGULAR_NOUN_VERB_AGREEMENT)

🪛 Markdownlint

29-29: Expected: dash; Actual: asterisk
Unordered list style

(MD004, ul-style)

## RPC Additions

* [Add a new rpc endpoint](https://github.com/lightningnetwork/lnd/pull/8843)
Expand Down
53 changes: 52 additions & 1 deletion itest/lnd_route_blinding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/chainreg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node"
Expand Down Expand Up @@ -607,7 +608,7 @@ func setupFourHopNetwork(ht *lntest.HarnessTest,
// testBlindedRouteInvoices tests lnd's ability to create a blinded payment path
// which it then inserts into an invoice, sending to an invoice with a blinded
// path and forward payments in a blinded route and finally, receiving the
// payment.
// payment. It also tests that blinded paths work for hodl invoices.
func testBlindedRouteInvoices(ht *lntest.HarnessTest) {
ctx, testCase := newBlindedForwardTest(ht)
defer testCase.cleanup()
Expand Down Expand Up @@ -662,6 +663,56 @@ func testBlindedRouteInvoices(ht *lntest.HarnessTest) {

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

// We'll also test the invoice flow for HODL invoices.
preimage, err := lntypes.MakePreimage(testCase.preimage[:])
require.NoError(ht, err)
hash := preimage.Hash()

// Register the HODL invoice on Dave's node.
minNumRealHops = 2
numHops = 2
hodlResp := testCase.dave.RPC.AddHoldInvoice(
&invoicesrpc.AddHoldInvoiceRequest{
Memo: "test",
Hash: hash[:],
ValueMsat: 10_000_000,
IsBlinded: true,
BlindedPathConfig: &lnrpc.BlindedPathConfig{
MinNumRealHops: &minNumRealHops,
NumHops: &numHops,
},
},
)

Comment on lines +673 to +687
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Extract repeated code into a helper function to avoid duplication

The code block from lines 673-687 duplicates the logic for creating a HODL invoice with blinded path configuration. Extracting this into a helper function enhances maintainability.

Consider adding a helper function:

func addBlindedHoldInvoice(dave *node.HarnessNode, preimage lntypes.Preimage) *invoicesrpc.AddHoldInvoiceResp {
	var (
		minNumRealHops uint32 = 2
		numHops        uint32 = 2
	)
	hash := preimage.Hash()
	return dave.RPC.AddHoldInvoice(
		&invoicesrpc.AddHoldInvoiceRequest{
			Memo:      "test",
			Hash:      hash[:],
			ValueMsat: 10_000_000,
			IsBlinded: true,
			BlindedPathConfig: &lnrpc.BlindedPathConfig{
				MinNumRealHops: &minNumRealHops,
				NumHops:        &numHops,
			},
		},
	)
}

Then replace lines 673-687 with:

hodlResp := addBlindedHoldInvoice(testCase.dave, preimage)

// Subscribe to HOLD invoice stream on Dave's side.
invStream := testCase.dave.RPC.SubscribeSingleInvoice(hash[:])

// Assert that the invoice is open.
ht.AssertInvoiceState(invStream, lnrpc.Invoice_OPEN)

// From Alice, we will now attempt to settle the invoice.
sendReq := &routerrpc.SendPaymentRequest{
PaymentRequest: hodlResp.PaymentRequest,
TimeoutSeconds: 60,
FeeLimitSat: 1000000,
}
payStream := ht.Alice.RPC.SendPayment(sendReq)

// From Alice's side, the payment should now be in-flight.
ht.AssertPaymentStatusFromStream(
payStream, lnrpc.Payment_IN_FLIGHT,
)

// From Dave's side, the invoice should be in the accepted state.
ht.AssertInvoiceState(invStream, lnrpc.Invoice_ACCEPTED)

// Now, let Dave settle the invoice.
testCase.dave.RPC.SettleInvoice(preimage[:])
ht.AssertInvoiceState(invStream, lnrpc.Invoice_SETTLED)

// And that Alice has received the preimage.
ht.AssertPaymentStatus(ht.Alice, preimage, lnrpc.Payment_SUCCEEDED)
}

// testReceiverBlindedError tests handling of errors from the receiving node in
Expand Down
28 changes: 18 additions & 10 deletions lnrpc/invoicesrpc/addinvoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ type AddInvoiceConfig struct {
Graph *channeldb.ChannelGraph

// GenInvoiceFeatures returns a feature containing feature bits that
// should be advertised on freshly generated invoices.
GenInvoiceFeatures func() *lnwire.FeatureVector
// should be advertised on freshly generated invoices. The blinded
// boolean is true if the features are for an invoice containing a
// blinded path.
GenInvoiceFeatures func(blinded bool) *lnwire.FeatureVector

// GenAmpInvoiceFeatures returns a feature containing feature bits that
// should be advertised on freshly generated AMP invoices.
Expand All @@ -95,7 +97,8 @@ 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)
QueryBlindedRoutes func(*routing.BlindedPathRestrictions,
lnwire.MilliSatoshi) ([]*route.Route, error)
}

// AddInvoiceData contains the required data to create a new invoice.
Expand Down Expand Up @@ -173,10 +176,9 @@ type BlindedPathConfig struct {
// 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
// Restrictions are the various rules that should be followed when
// constructing the blinded path.
Restrictions *routing.BlindedPathRestrictions

// DefaultDummyHopPolicy holds the default policy values to use for
// dummy hops in a blinded path in the case where they cant be derived
Expand Down Expand Up @@ -484,7 +486,7 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
if invoice.Amp {
invoiceFeatures = cfg.GenAmpInvoiceFeatures()
} else {
invoiceFeatures = cfg.GenInvoiceFeatures()
invoiceFeatures = cfg.GenInvoiceFeatures(blind)
}
options = append(options, zpay32.Features(invoiceFeatures))

Expand Down Expand Up @@ -521,7 +523,13 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
//nolint:lll
paths, err := blindedpath.BuildBlindedPaymentPaths(
&blindedpath.BuildBlindedPathCfg{
FindRoutes: cfg.QueryBlindedRoutes,
FindRoutes: func(value lnwire.MilliSatoshi) (
[]*route.Route, error) {

return cfg.QueryBlindedRoutes(
blindCfg.Restrictions, value,
)
},
FetchChannelEdgesByID: cfg.Graph.FetchChannelEdgesByID,
FetchOurOpenChannels: cfg.ChanDB.FetchAllOpenChannels,
PathID: paymentAddr[:],
Expand All @@ -539,7 +547,7 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
blindCfg.RoutePolicyDecrMultiplier,
)
},
MinNumHops: blindCfg.MinNumPathHops,
MinNumHops: blindCfg.Restrictions.NumHops,
DefaultDummyHopPolicy: blindCfg.DefaultDummyHopPolicy,
},
)
Expand Down
20 changes: 19 additions & 1 deletion lnrpc/invoicesrpc/config_active.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/netann"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/route"
"google.golang.org/protobuf/proto"
)

Expand Down Expand Up @@ -61,7 +64,7 @@ type Config struct {

// GenInvoiceFeatures returns a feature containing feature bits that
// should be advertised on freshly generated invoices.
GenInvoiceFeatures func() *lnwire.FeatureVector
GenInvoiceFeatures func(blinded bool) *lnwire.FeatureVector

// GenAmpInvoiceFeatures returns a feature containing feature bits that
// should be advertised on freshly generated AMP invoices.
Expand All @@ -74,4 +77,19 @@ type Config struct {
// ParseAuxData is a function that can be used to parse the auxiliary
// data from the invoice.
ParseAuxData func(message proto.Message) error

// BlindedPathCfg takes the global routing blinded path policies and the
// given per-payment blinded path config values and uses these to
// construct the config values passed to the invoice server.
BlindedPathCfg func(bool, *lnrpc.BlindedPathConfig) (
*BlindedPathConfig, error)

// BestHeight can be used to get the current best block height known to
// LND.
BestHeight func() (uint32, error)

// 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(*routing.BlindedPathRestrictions,
lnwire.MilliSatoshi) ([]*route.Route, error)
}
Loading
Loading