Repository: lightningnetwork/lnd
Pull request #8752 (2024-05-14 13:45:17)

[1/4] Route Blinding Receives: Groundwork
To make review a bit more manageable, I've split out some of the groundwork needed for 
the main route blinding receives PR. This PR thus has no functional changes. 

Tracking Issue: https://github.com/lightningnetwork/lnd/issues/6690

## PR Overview: 

1) a couple of refactors & interface expansions so that we have easy access to the `BlindedPayment` associated with an edge in path finding later on. 
2) Remove the need to communicate the "TLV Option" in a blinded path feature bit vector. This bit can be assumed for any nodes in a blinded path. 
3) Add fields to `BlindedRouteData` that will be required for receives. Namely, PathID and Padding. 
4) Bolt11 blinded payment path encoding


EDIT:  bLIP proposal here: https://github.com/lightning/blips/pull/39

coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Auto Review Skipped
> 
> Auto reviews are disabled on this repository.
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai --><!-- tips_start -->

---

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

<details>
<summary>Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
	- `I pushed a fix in commit <commit_id>.`
	- `Generate unit testing code for this file.`
	- `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
	- `@coderabbitai generate unit testing code for this file.`
	-	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
	- `@coderabbitai generate interesting stats about this repository and render them as a table.`
	- `@coderabbitai show all the console.log statements in this repository.`
	- `@coderabbitai read src/utils.ts and generate unit testing code.`
	- `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (invoked as PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger a review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai help` to get help.


Additionally, you can add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.

### CodeRabbit Configration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://coderabbit.ai/docs) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](https://discord.com/invite/GsXnASn26c) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
lightninglabs-deploy: @bitromortac: review reminder
lightninglabs-deploy: @bitromortac: review reminder
lightninglabs-deploy: @bitromortac: review reminder
@ellemouton, remember to re-request review from reviewers when ready
ellemouton: # CLTVs, CLTV-deltas and invoice expiry in a payment

## Normal LN payment:

- Recipient (`D`) communicates via invoice the `min_final_expiry_delta` that they would accept. Meaning that if the current block height is `x` then the want the minimum difference between the CLTV value of the incoming HTLC to be `min_final_expiry_delta`. In otherwords, it is the buffer they want to be able to claim the HTLC if their peer goes on-chain. If the incoming payment has a CLTV value that would result in a safety buffer, then the recipient will reject the payment. The _sender_ will want the payment to resolve as quickly as possible and wont want their funds on their outgoing channel to be tied up for too long if something goes wrong, so they are insentivised to use the smallest possible CLTV for the path. So they will try to use a value as close to (but not less than) `x` (current block height) + `min_final_expiry_delta`. However, the sender also wants the payment to succeed and so will want to avoid the scenario where they initiate the payment at block `x` but then a block or two are mined before the recipient gets it and so the recipient will fail the payment. So to avoid this, the sender will add a few padding blocks (in LND, a padding of [3 is used](https://github.com/lightningnetwork/lnd/blob/6b64703db428a20d075a8eb5e0965b7e076c4267/routing/payment_session.go#L18).

- The Sender (`A`) finds a route to the recipient: A -> B -> C -> D. The sender constructs the parameters of the route starting at the final hop, C-> D. The sender sets the final `outgoing_cltv_value` to:
	
	C->D: `outgoing_cltv_value` = `x` (current block height`) + `b` (block buffer) + `min_final_cltv_expiry_delta` = `cltv_1`

For the other hops:
	
	B->C `outgoing_cltv_value` = `cltv_1` + C's advertised `cltv_expiry_delta` = `cltv_2`

	
	A-> `outgoing_cltv_value` = `cltv_2` + B's advertised `cltv_expiry_delta`


- The invoice (Bolt11) also contains an `expiry` field in seconds and an invoice creation field (unix timestamp). These are _completely_ separate from the CLTV expiry. There is no way for a routing node of the payment to know that it is routing a payment that will fail due to the recipient marking it as expired. So it could very well be that the actual payment parameters are valid long past the expiration of the invoice. 


## Route Blinding: 

- For route blinding, the recipient is given a way to be able to enforce an expiry on the blinded part of the route. This is nice because the payment can fail earlier (since each hop can check if the expiry has passed) and it gives some protection to the blinded hops so that after the path has expired, they can change their routing parameters. This also allows us to tie invoice expiry a bit closer to the actual path expiry. We have 2 options here afaict:
	1) re-use the existing `expiry` field, assume a 10-min block time, also look at the invoice creation time and use these to determine the approximate block at which the path expires. 
	2) add a new `max_cltv` field to the invoice which very explicitly communicates this. 

 * Re having `payment_constraints` for recipient: 
	- If we go with 1) above, then I think it makes sense to add the payment constraint with the absolute `max_cltv_expiry` in the recipient's encrypted data since then they dont need to store this separately in order to do the check when the payment comes in. I also like this cause then, just like all other hops on the blinded path, we repeat the same check against the "absolute source of truth". Otherwise we either need to persist this absolute truth somewhere else _or_ we need to use the invoice "expiry" to work out a "rough" expiry. I think it is ok for the sender to work out a rough expiry but not so much the reciever. 
	- If we go with 2) above, then maybe there is then not a reason to include the payment constraints for the recipient

	- the OG reason I incluced the `payment_constraints` for the recipient was cause of [this example](https://github.com/lightning/bolts/blob/c562d91ace0e95bec3c6f8758969eaf3627f23c8/proposals/route-blinding.md?plain=1#L274).  

- The flow is then as follows: 

	1) Recipient, `D` has: 
		- current block height, `x`
		- the `min_final_cltv_expiry_delta` which Dave requires for the last CLTV value.
		- the absolute block height at which the path will expire: `max_cltv_expiry` 

	2) `D` chooses a blinded path to himself: B -> C -> D. Let B's CLTV delta be `b_delta` and C's be `c_delta`. 
	3) `D` calculates the accumulated blinded path CLTV delta as follows: `total_cltv_delta` = `min_final_cltv_expiry_delta` + `b_delta` + `c_delta` (according to the example in the proposals doc but not explicitly stated in BOLT 4. See [this issue for discussion on this](https://github.com/lightning/bolts/issues/1174)
	4) `D` also constructs encrypted packets for each node on the blinded path. He uses this blob to communicate a `max_cltv` of `min_final_cltv_expiry_delta` + `c_delta` + `max_cltv_expiry` to `C` (according to the example) and a `max_cltv` of `min_final_cltv_expiry_delta` + `c_delta` + `b_delta` + `max_cltv_expiry` to `B`. In `D`'s packet to himself, he may include `max_cltv_expiry` so that he doesnt need to explicitly remember it.
	5) `D` sends the sender, `A`: the blinded path (`B` -> `B(C)` -> `B(D)`), the encrypted data to deliver to each hop and `total_cltv_delta`. Note that `D` does _not_ send `min_final_cltv_delta` to `A` (as is done in the normal payment) and `A` is instead expected to know that this value is now included in the `total_cltv_delta`. `D` does, however, also send `A` the `max_expiry_delta` so that `A` has an idea of when they need to use the route by. 
bitromortac: Thanks a lot for the writeup and thorough analysis @ellemouton!

some small corrections for iv):
> D also constructs encrypted packets for each node on the blinded path. He uses this blob to communicate a `max_cltv_expiry` of `x + min_final_cltv_expiry_delta + c_delta + max_cltv_expiry` to C (according to the example) and a `max_cltv_expiry` of `x + min_final_cltv_expiry_delta + c_delta + b_delta + max_cltv_expiry` to B. In D's packet to himself, he may include `max_cltv_expiry` so that he doesnt need to explicitly remember it.

Concerning the points:
1. The sender adding a buffer seems to be incompatible with the max cltv expiries, therefore I think leaving that to the recipient should be the best approach, it would just add it to it's `min_final_cltv_expiry_delta`,
2. which may need to be added to the last `max_cltv_expiry`, see https://github.com/lightningnetwork/lnd/pull/8735#discussion_r1654380105?
3. I would prefer if we could reuse the invoices expiry field, to not add more complexity. Perhaps one could compute it such that we statistically know that if the expiry of the invoice is reached, the path is still usable for a bit. We could use something like `expiry` = now + `total_expiry_delta` * 9 min, to make the invoice expire before the path. As the path expiry is just a probing protection, I think it makes sense to not add an extra field. I would also argue against per-blinded-path expiries, since the receiver can make them uniform for all paths and in the end it is a single invoice that's about to be paid?
ziggie1984: > CLTVs, CLTV-deltas and invoice expiry in a payment

Thank you for the explanation, I think I now understood the whole ctlv expiry for blinded paths.

1. I would also go with solution 1) seems less hacky to add an extra field to BOLT11 which kind of were not planned to be used for Blinded Paths in the first place ?
2. I would also include the buffer not really as optional, I find it quite useful to account for found blocks during the payment process.


But all those changes will only affect the follow up PRs regarding the CLTV Delta (I think PR 2)

Patch:
diff --git a/cmd/lncli/cmd_payments.go b/cmd/lncli/cmd_payments.go
index ec6a04b37..228dd3415 100644
--- a/cmd/lncli/cmd_payments.go
+++ b/cmd/lncli/cmd_payments.go
@@ -1099,8 +1099,13 @@ var queryRoutesCommand = cli.Command{
 		},
 		cli.Int64Flag{
 			Name: "final_cltv_delta",
-			Usage: "(optional) number of blocks the last hop has to reveal " +
-				"the preimage",
+			Usage: "(optional) number of blocks the last hop has " +
+				"to reveal the preimage. Note that this " +
+				"should not be set in the case where the " +
+				"path includes a blinded path since in " +
+				"that case, the receiver will already have " +
+				"accounted for this value in the " +
+				"blinded_cltv value",
 		},
 		cli.BoolFlag{
 			Name:  "use_mc",
@@ -1238,6 +1243,13 @@ func parseBlindedPaymentParameters(ctx *cli.Context) (
 		return nil, nil
 	}
 
+	// If a blinded path has been provided, then the final_cltv_delta flag
+	// should not be provided since this value will be ignored.
+	if ctx.IsSet("final_cltv_delta") {
+		return nil, fmt.Errorf("`final_cltv_delta` should not be " +
+			"provided if a blinded path is provided")
+	}
+
 	// If any one of our blinding related flags is set, we expect the
 	// full set to be set and we'll error out accordingly.
 	introNode, err := route.NewVertexFromStr(
diff --git a/docs/release-notes/release-notes-0.18.3.md b/docs/release-notes/release-notes-0.18.3.md
index 3017b1ffd..924590395 100644
--- a/docs/release-notes/release-notes-0.18.3.md
+++ b/docs/release-notes/release-notes-0.18.3.md
@@ -89,6 +89,9 @@
   channel. We will still wait for the channel to have at least one confirmation
   and so the main change here is that we don't error out for such a case.
 
+* [Groundwork](https://github.com/lightningnetwork/lnd/pull/8752) in preparation
+  for implementing route blinding receives.
+
 ## Testing
 ## Database
 
diff --git a/htlcswitch/hop/iterator.go b/htlcswitch/hop/iterator.go
index a89f1b734..ddfbe5934 100644
--- a/htlcswitch/hop/iterator.go
+++ b/htlcswitch/hop/iterator.go
@@ -360,6 +360,7 @@ func (b *BlindingKit) DecryptAndValidateFwdInfo(payload *Payload,
 	if err != nil {
 		return nil, err
 	}
+
 	// Validate the data in the blinded route against our incoming htlc's
 	// information.
 	if err := ValidateBlindedRouteData(
@@ -368,9 +369,31 @@ func (b *BlindingKit) DecryptAndValidateFwdInfo(payload *Payload,
 		return nil, err
 	}
 
+	// Exit early if this onion is for the exit hop of the route since
+	// route blinding receives are not yet supported.
+	if isFinalHop {
+		return nil, fmt.Errorf("being the final hop in a blinded " +
+			"path is not yet supported")
+	}
+
+	// At this point, we know we are a forwarding node for this onion
+	// and so we expect the relay info and next SCID fields to be set.
+	relayInfo, err := routeData.RelayInfo.UnwrapOrErr(
+		fmt.Errorf("relay info not set for non-final blinded hop"),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	nextSCID, err := routeData.ShortChannelID.UnwrapOrErr(
+		fmt.Errorf("next SCID not set for non-final blinded hop"),
+	)
+	if err != nil {
+		return nil, err
+	}
+
 	fwdAmt, err := calculateForwardingAmount(
-		b.IncomingAmount, routeData.RelayInfo.Val.BaseFee,
-		routeData.RelayInfo.Val.FeeRate,
+		b.IncomingAmount, relayInfo.Val.BaseFee, relayInfo.Val.FeeRate,
 	)
 	if err != nil {
 		return nil, err
@@ -400,10 +423,10 @@ func (b *BlindingKit) DecryptAndValidateFwdInfo(payload *Payload,
 	}
 
 	return &ForwardingInfo{
-		NextHop:         routeData.ShortChannelID.Val,
+		NextHop:         nextSCID.Val,
 		AmountToForward: fwdAmt,
 		OutgoingCTLV: b.IncomingCltv - uint32(
-			routeData.RelayInfo.Val.CltvExpiryDelta,
+			relayInfo.Val.CltvExpiryDelta,
 		),
 		// Remap from blinding override type to blinding point type.
 		NextBlinding: tlv.SomeRecordT(
diff --git a/lnrpc/invoicesrpc/utils.go b/lnrpc/invoicesrpc/utils.go
index 70d774082..2d1507267 100644
--- a/lnrpc/invoicesrpc/utils.go
+++ b/lnrpc/invoicesrpc/utils.go
@@ -266,6 +266,60 @@ func CreateRPCRouteHints(routeHints [][]zpay32.HopHint) []*lnrpc.RouteHint {
 	return res
 }
 
+// CreateRPCBlindedPayments takes a set of zpay32.BlindedPaymentPath and
+// converts them into a set of lnrpc.BlindedPaymentPaths.
+func CreateRPCBlindedPayments(blindedPaths []*zpay32.BlindedPaymentPath) (
+	[]*lnrpc.BlindedPaymentPath, error) {
+
+	var res []*lnrpc.BlindedPaymentPath
+	for _, path := range blindedPaths {
+		features := path.Features.Features()
+		var featuresSlice []lnrpc.FeatureBit
+		for feature := range features {
+			featuresSlice = append(
+				featuresSlice, lnrpc.FeatureBit(feature),
+			)
+		}
+
+		if len(path.Hops) == 0 {
+			return nil, fmt.Errorf("each blinded path must " +
+				"contain at least one hop")
+		}
+
+		var hops []*lnrpc.BlindedHop
+		for _, hop := range path.Hops {
+			blindedNodeID := hop.BlindedNodePub.
+				SerializeCompressed()
+			hops = append(hops, &lnrpc.BlindedHop{
+				BlindedNode:   blindedNodeID,
+				EncryptedData: hop.CipherText,
+			})
+		}
+
+		introNode := path.Hops[0].BlindedNodePub
+		firstBlindingPoint := path.FirstEphemeralBlindingPoint
+
+		blindedPath := &lnrpc.BlindedPath{
+			IntroductionNode: introNode.SerializeCompressed(),
+			BlindingPoint: firstBlindingPoint.
+				SerializeCompressed(),
+			BlindedHops: hops,
+		}
+
+		res = append(res, &lnrpc.BlindedPaymentPath{
+			BlindedPath:         blindedPath,
+			BaseFeeMsat:         uint64(path.FeeBaseMsat),
+			ProportionalFeeRate: path.FeeRate,
+			TotalCltvDelta:      uint32(path.CltvExpiryDelta),
+			HtlcMinMsat:         path.HTLCMinMsat,
+			HtlcMaxMsat:         path.HTLCMaxMsat,
+			Features:            featuresSlice,
+		})
+	}
+
+	return res, nil
+}
+
 // CreateZpay32HopHints takes in the lnrpc form of route hints and converts them
 // into an invoice decoded form.
 func CreateZpay32HopHints(routeHints []*lnrpc.RouteHint) ([][]zpay32.HopHint, error) {
diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go
index 305e62412..3cd5d9767 100644
--- a/lnrpc/lightning.pb.go
+++ b/lnrpc/lightning.pb.go
@@ -14304,19 +14304,20 @@ type PayReq struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Destination     string              `protobuf:"bytes,1,opt,name=destination,proto3" json:"destination,omitempty"`
-	PaymentHash     string              `protobuf:"bytes,2,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"`
-	NumSatoshis     int64               `protobuf:"varint,3,opt,name=num_satoshis,json=numSatoshis,proto3" json:"num_satoshis,omitempty"`
-	Timestamp       int64               `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
-	Expiry          int64               `protobuf:"varint,5,opt,name=expiry,proto3" json:"expiry,omitempty"`
-	Description     string              `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"`
-	DescriptionHash string              `protobuf:"bytes,7,opt,name=description_hash,json=descriptionHash,proto3" json:"description_hash,omitempty"`
-	FallbackAddr    string              `protobuf:"bytes,8,opt,name=fallback_addr,json=fallbackAddr,proto3" json:"fallback_addr,omitempty"`
-	CltvExpiry      int64               `protobuf:"varint,9,opt,name=cltv_expiry,json=cltvExpiry,proto3" json:"cltv_expiry,omitempty"`
-	RouteHints      []*RouteHint        `protobuf:"bytes,10,rep,name=route_hints,json=routeHints,proto3" json:"route_hints,omitempty"`
-	PaymentAddr     []byte              `protobuf:"bytes,11,opt,name=payment_addr,json=paymentAddr,proto3" json:"payment_addr,omitempty"`
-	NumMsat         int64               `protobuf:"varint,12,opt,name=num_msat,json=numMsat,proto3" json:"num_msat,omitempty"`
-	Features        map[uint32]*Feature `protobuf:"bytes,13,rep,name=features,proto3" json:"features,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	Destination     string                `protobuf:"bytes,1,opt,name=destination,proto3" json:"destination,omitempty"`
+	PaymentHash     string                `protobuf:"bytes,2,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"`
+	NumSatoshis     int64                 `protobuf:"varint,3,opt,name=num_satoshis,json=numSatoshis,proto3" json:"num_satoshis,omitempty"`
+	Timestamp       int64                 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+	Expiry          int64                 `protobuf:"varint,5,opt,name=expiry,proto3" json:"expiry,omitempty"`
+	Description     string                `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"`
+	DescriptionHash string                `protobuf:"bytes,7,opt,name=description_hash,json=descriptionHash,proto3" json:"description_hash,omitempty"`
+	FallbackAddr    string                `protobuf:"bytes,8,opt,name=fallback_addr,json=fallbackAddr,proto3" json:"fallback_addr,omitempty"`
+	CltvExpiry      int64                 `protobuf:"varint,9,opt,name=cltv_expiry,json=cltvExpiry,proto3" json:"cltv_expiry,omitempty"`
+	RouteHints      []*RouteHint          `protobuf:"bytes,10,rep,name=route_hints,json=routeHints,proto3" json:"route_hints,omitempty"`
+	PaymentAddr     []byte                `protobuf:"bytes,11,opt,name=payment_addr,json=paymentAddr,proto3" json:"payment_addr,omitempty"`
+	NumMsat         int64                 `protobuf:"varint,12,opt,name=num_msat,json=numMsat,proto3" json:"num_msat,omitempty"`
+	Features        map[uint32]*Feature   `protobuf:"bytes,13,rep,name=features,proto3" json:"features,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	BlindedPaths    []*BlindedPaymentPath `protobuf:"bytes,14,rep,name=blinded_paths,json=blindedPaths,proto3" json:"blinded_paths,omitempty"`
 }
 
 func (x *PayReq) Reset() {
@@ -14442,6 +14443,13 @@ func (x *PayReq) GetFeatures() map[uint32]*Feature {
 	return nil
 }
 
+func (x *PayReq) GetBlindedPaths() []*BlindedPaymentPath {
+	if x != nil {
+		return x.BlindedPaths
+	}
+	return nil
+}
+
 type Feature struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -20202,7 +20210,7 @@ var file_lightning_proto_rawDesc = []byte{
 	0x75, 0x62, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x27, 0x0a, 0x0c, 0x50, 0x61, 0x79,
 	0x52, 0x65, 0x71, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x61, 0x79,
 	0x5f, 0x72, 0x65, 0x71, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x79, 0x52,
-	0x65, 0x71, 0x22, 0xb0, 0x04, 0x0a, 0x06, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x20, 0x0a,
+	0x65, 0x71, 0x22, 0xf0, 0x04, 0x0a, 0x06, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x20, 0x0a,
 	0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01,
 	0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12,
 	0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18,
@@ -20232,7 +20240,11 @@ var file_lightning_proto_rawDesc = []byte{
 	0x37, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28,
 	0x0b, 0x32, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71,
 	0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08,
-	0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x1a, 0x4b, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74,
+	0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x62, 0x6c, 0x69, 0x6e,
+	0x64, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32,
+	0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x6c, 0x69, 0x6e, 0x64, 0x65, 0x64, 0x50,
+	0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0c, 0x62, 0x6c, 0x69, 0x6e,
+	0x64, 0x65, 0x64, 0x50, 0x61, 0x74, 0x68, 0x73, 0x1a, 0x4b, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74,
 	0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
 	0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76,
 	0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, 0x6e, 0x72,
@@ -21520,192 +21532,193 @@ var file_lightning_proto_depIdxs = []int32{
 	38,  // 138: lnrpc.AbandonChannelRequest.channel_point:type_name -> lnrpc.ChannelPoint
 	150, // 139: lnrpc.PayReq.route_hints:type_name -> lnrpc.RouteHint
 	244, // 140: lnrpc.PayReq.features:type_name -> lnrpc.PayReq.FeaturesEntry
-	179, // 141: lnrpc.FeeReportResponse.channel_fees:type_name -> lnrpc.ChannelFeeReport
-	38,  // 142: lnrpc.PolicyUpdateRequest.chan_point:type_name -> lnrpc.ChannelPoint
-	181, // 143: lnrpc.PolicyUpdateRequest.inbound_fee:type_name -> lnrpc.InboundFee
-	39,  // 144: lnrpc.FailedUpdate.outpoint:type_name -> lnrpc.OutPoint
-	11,  // 145: lnrpc.FailedUpdate.reason:type_name -> lnrpc.UpdateFailure
-	183, // 146: lnrpc.PolicyUpdateResponse.failed_updates:type_name -> lnrpc.FailedUpdate
-	186, // 147: lnrpc.ForwardingHistoryResponse.forwarding_events:type_name -> lnrpc.ForwardingEvent
-	38,  // 148: lnrpc.ExportChannelBackupRequest.chan_point:type_name -> lnrpc.ChannelPoint
-	38,  // 149: lnrpc.ChannelBackup.chan_point:type_name -> lnrpc.ChannelPoint
-	38,  // 150: lnrpc.MultiChanBackup.chan_points:type_name -> lnrpc.ChannelPoint
-	193, // 151: lnrpc.ChanBackupSnapshot.single_chan_backups:type_name -> lnrpc.ChannelBackups
-	190, // 152: lnrpc.ChanBackupSnapshot.multi_chan_backup:type_name -> lnrpc.MultiChanBackup
-	189, // 153: lnrpc.ChannelBackups.chan_backups:type_name -> lnrpc.ChannelBackup
-	193, // 154: lnrpc.RestoreChanBackupRequest.chan_backups:type_name -> lnrpc.ChannelBackups
-	198, // 155: lnrpc.BakeMacaroonRequest.permissions:type_name -> lnrpc.MacaroonPermission
-	198, // 156: lnrpc.MacaroonPermissionList.permissions:type_name -> lnrpc.MacaroonPermission
-	245, // 157: lnrpc.ListPermissionsResponse.method_permissions:type_name -> lnrpc.ListPermissionsResponse.MethodPermissionsEntry
-	20,  // 158: lnrpc.Failure.code:type_name -> lnrpc.Failure.FailureCode
-	209, // 159: lnrpc.Failure.channel_update:type_name -> lnrpc.ChannelUpdate
-	211, // 160: lnrpc.MacaroonId.ops:type_name -> lnrpc.Op
-	198, // 161: lnrpc.CheckMacPermRequest.permissions:type_name -> lnrpc.MacaroonPermission
-	215, // 162: lnrpc.RPCMiddlewareRequest.stream_auth:type_name -> lnrpc.StreamAuth
-	216, // 163: lnrpc.RPCMiddlewareRequest.request:type_name -> lnrpc.RPCMessage
-	216, // 164: lnrpc.RPCMiddlewareRequest.response:type_name -> lnrpc.RPCMessage
-	218, // 165: lnrpc.RPCMiddlewareResponse.register:type_name -> lnrpc.MiddlewareRegistration
-	219, // 166: lnrpc.RPCMiddlewareResponse.feedback:type_name -> lnrpc.InterceptFeedback
-	177, // 167: lnrpc.Peer.FeaturesEntry.value:type_name -> lnrpc.Feature
-	177, // 168: lnrpc.GetInfoResponse.FeaturesEntry.value:type_name -> lnrpc.Feature
-	4,   // 169: lnrpc.PendingChannelsResponse.PendingChannel.initiator:type_name -> lnrpc.Initiator
-	3,   // 170: lnrpc.PendingChannelsResponse.PendingChannel.commitment_type:type_name -> lnrpc.CommitmentType
-	226, // 171: lnrpc.PendingChannelsResponse.PendingOpenChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel
-	226, // 172: lnrpc.PendingChannelsResponse.WaitingCloseChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel
-	229, // 173: lnrpc.PendingChannelsResponse.WaitingCloseChannel.commitments:type_name -> lnrpc.PendingChannelsResponse.Commitments
-	226, // 174: lnrpc.PendingChannelsResponse.ClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel
-	226, // 175: lnrpc.PendingChannelsResponse.ForceClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel
-	108, // 176: lnrpc.PendingChannelsResponse.ForceClosedChannel.pending_htlcs:type_name -> lnrpc.PendingHTLC
-	15,  // 177: lnrpc.PendingChannelsResponse.ForceClosedChannel.anchor:type_name -> lnrpc.PendingChannelsResponse.ForceClosedChannel.AnchorState
-	113, // 178: lnrpc.WalletBalanceResponse.AccountBalanceEntry.value:type_name -> lnrpc.WalletAccountBalance
-	177, // 179: lnrpc.LightningNode.FeaturesEntry.value:type_name -> lnrpc.Feature
-	137, // 180: lnrpc.NodeMetricsResponse.BetweennessCentralityEntry.value:type_name -> lnrpc.FloatMetric
-	177, // 181: lnrpc.NodeUpdate.FeaturesEntry.value:type_name -> lnrpc.Feature
-	177, // 182: lnrpc.Invoice.FeaturesEntry.value:type_name -> lnrpc.Feature
-	154, // 183: lnrpc.Invoice.AmpInvoiceStateEntry.value:type_name -> lnrpc.AMPInvoiceState
-	177, // 184: lnrpc.PayReq.FeaturesEntry.value:type_name -> lnrpc.Feature
-	205, // 185: lnrpc.ListPermissionsResponse.MethodPermissionsEntry.value:type_name -> lnrpc.MacaroonPermissionList
-	114, // 186: lnrpc.Lightning.WalletBalance:input_type -> lnrpc.WalletBalanceRequest
-	117, // 187: lnrpc.Lightning.ChannelBalance:input_type -> lnrpc.ChannelBalanceRequest
-	30,  // 188: lnrpc.Lightning.GetTransactions:input_type -> lnrpc.GetTransactionsRequest
-	42,  // 189: lnrpc.Lightning.EstimateFee:input_type -> lnrpc.EstimateFeeRequest
-	46,  // 190: lnrpc.Lightning.SendCoins:input_type -> lnrpc.SendCoinsRequest
-	48,  // 191: lnrpc.Lightning.ListUnspent:input_type -> lnrpc.ListUnspentRequest
-	30,  // 192: lnrpc.Lightning.SubscribeTransactions:input_type -> lnrpc.GetTransactionsRequest
-	44,  // 193: lnrpc.Lightning.SendMany:input_type -> lnrpc.SendManyRequest
-	50,  // 194: lnrpc.Lightning.NewAddress:input_type -> lnrpc.NewAddressRequest
-	52,  // 195: lnrpc.Lightning.SignMessage:input_type -> lnrpc.SignMessageRequest
-	54,  // 196: lnrpc.Lightning.VerifyMessage:input_type -> lnrpc.VerifyMessageRequest
-	56,  // 197: lnrpc.Lightning.ConnectPeer:input_type -> lnrpc.ConnectPeerRequest
-	58,  // 198: lnrpc.Lightning.DisconnectPeer:input_type -> lnrpc.DisconnectPeerRequest
-	74,  // 199: lnrpc.Lightning.ListPeers:input_type -> lnrpc.ListPeersRequest
-	76,  // 200: lnrpc.Lightning.SubscribePeerEvents:input_type -> lnrpc.PeerEventSubscription
-	78,  // 201: lnrpc.Lightning.GetInfo:input_type -> lnrpc.GetInfoRequest
-	80,  // 202: lnrpc.Lightning.GetDebugInfo:input_type -> lnrpc.GetDebugInfoRequest
-	82,  // 203: lnrpc.Lightning.GetRecoveryInfo:input_type -> lnrpc.GetRecoveryInfoRequest
-	109, // 204: lnrpc.Lightning.PendingChannels:input_type -> lnrpc.PendingChannelsRequest
-	63,  // 205: lnrpc.Lightning.ListChannels:input_type -> lnrpc.ListChannelsRequest
-	111, // 206: lnrpc.Lightning.SubscribeChannelEvents:input_type -> lnrpc.ChannelEventSubscription
-	70,  // 207: lnrpc.Lightning.ClosedChannels:input_type -> lnrpc.ClosedChannelsRequest
-	96,  // 208: lnrpc.Lightning.OpenChannelSync:input_type -> lnrpc.OpenChannelRequest
-	96,  // 209: lnrpc.Lightning.OpenChannel:input_type -> lnrpc.OpenChannelRequest
-	93,  // 210: lnrpc.Lightning.BatchOpenChannel:input_type -> lnrpc.BatchOpenChannelRequest
-	106, // 211: lnrpc.Lightning.FundingStateStep:input_type -> lnrpc.FundingTransitionMsg
-	37,  // 212: lnrpc.Lightning.ChannelAcceptor:input_type -> lnrpc.ChannelAcceptResponse
-	88,  // 213: lnrpc.Lightning.CloseChannel:input_type -> lnrpc.CloseChannelRequest
-	171, // 214: lnrpc.Lightning.AbandonChannel:input_type -> lnrpc.AbandonChannelRequest
-	33,  // 215: lnrpc.Lightning.SendPayment:input_type -> lnrpc.SendRequest
-	33,  // 216: lnrpc.Lightning.SendPaymentSync:input_type -> lnrpc.SendRequest
-	35,  // 217: lnrpc.Lightning.SendToRoute:input_type -> lnrpc.SendToRouteRequest
-	35,  // 218: lnrpc.Lightning.SendToRouteSync:input_type -> lnrpc.SendToRouteRequest
-	155, // 219: lnrpc.Lightning.AddInvoice:input_type -> lnrpc.Invoice
-	160, // 220: lnrpc.Lightning.ListInvoices:input_type -> lnrpc.ListInvoiceRequest
-	159, // 221: lnrpc.Lightning.LookupInvoice:input_type -> lnrpc.PaymentHash
-	162, // 222: lnrpc.Lightning.SubscribeInvoices:input_type -> lnrpc.InvoiceSubscription
-	175, // 223: lnrpc.Lightning.DecodePayReq:input_type -> lnrpc.PayReqString
-	165, // 224: lnrpc.Lightning.ListPayments:input_type -> lnrpc.ListPaymentsRequest
-	167, // 225: lnrpc.Lightning.DeletePayment:input_type -> lnrpc.DeletePaymentRequest
-	168, // 226: lnrpc.Lightning.DeleteAllPayments:input_type -> lnrpc.DeleteAllPaymentsRequest
-	133, // 227: lnrpc.Lightning.DescribeGraph:input_type -> lnrpc.ChannelGraphRequest
-	135, // 228: lnrpc.Lightning.GetNodeMetrics:input_type -> lnrpc.NodeMetricsRequest
-	138, // 229: lnrpc.Lightning.GetChanInfo:input_type -> lnrpc.ChanInfoRequest
-	127, // 230: lnrpc.Lightning.GetNodeInfo:input_type -> lnrpc.NodeInfoRequest
-	119, // 231: lnrpc.Lightning.QueryRoutes:input_type -> lnrpc.QueryRoutesRequest
-	139, // 232: lnrpc.Lightning.GetNetworkInfo:input_type -> lnrpc.NetworkInfoRequest
-	141, // 233: lnrpc.Lightning.StopDaemon:input_type -> lnrpc.StopRequest
-	143, // 234: lnrpc.Lightning.SubscribeChannelGraph:input_type -> lnrpc.GraphTopologySubscription
-	173, // 235: lnrpc.Lightning.DebugLevel:input_type -> lnrpc.DebugLevelRequest
-	178, // 236: lnrpc.Lightning.FeeReport:input_type -> lnrpc.FeeReportRequest
-	182, // 237: lnrpc.Lightning.UpdateChannelPolicy:input_type -> lnrpc.PolicyUpdateRequest
-	185, // 238: lnrpc.Lightning.ForwardingHistory:input_type -> lnrpc.ForwardingHistoryRequest
-	188, // 239: lnrpc.Lightning.ExportChannelBackup:input_type -> lnrpc.ExportChannelBackupRequest
-	191, // 240: lnrpc.Lightning.ExportAllChannelBackups:input_type -> lnrpc.ChanBackupExportRequest
-	192, // 241: lnrpc.Lightning.VerifyChanBackup:input_type -> lnrpc.ChanBackupSnapshot
-	194, // 242: lnrpc.Lightning.RestoreChannelBackups:input_type -> lnrpc.RestoreChanBackupRequest
-	196, // 243: lnrpc.Lightning.SubscribeChannelBackups:input_type -> lnrpc.ChannelBackupSubscription
-	199, // 244: lnrpc.Lightning.BakeMacaroon:input_type -> lnrpc.BakeMacaroonRequest
-	201, // 245: lnrpc.Lightning.ListMacaroonIDs:input_type -> lnrpc.ListMacaroonIDsRequest
-	203, // 246: lnrpc.Lightning.DeleteMacaroonID:input_type -> lnrpc.DeleteMacaroonIDRequest
-	206, // 247: lnrpc.Lightning.ListPermissions:input_type -> lnrpc.ListPermissionsRequest
-	212, // 248: lnrpc.Lightning.CheckMacaroonPermissions:input_type -> lnrpc.CheckMacPermRequest
-	217, // 249: lnrpc.Lightning.RegisterRPCMiddleware:input_type -> lnrpc.RPCMiddlewareResponse
-	25,  // 250: lnrpc.Lightning.SendCustomMessage:input_type -> lnrpc.SendCustomMessageRequest
-	23,  // 251: lnrpc.Lightning.SubscribeCustomMessages:input_type -> lnrpc.SubscribeCustomMessagesRequest
-	66,  // 252: lnrpc.Lightning.ListAliases:input_type -> lnrpc.ListAliasesRequest
-	21,  // 253: lnrpc.Lightning.LookupHtlcResolution:input_type -> lnrpc.LookupHtlcResolutionRequest
-	115, // 254: lnrpc.Lightning.WalletBalance:output_type -> lnrpc.WalletBalanceResponse
-	118, // 255: lnrpc.Lightning.ChannelBalance:output_type -> lnrpc.ChannelBalanceResponse
-	31,  // 256: lnrpc.Lightning.GetTransactions:output_type -> lnrpc.TransactionDetails
-	43,  // 257: lnrpc.Lightning.EstimateFee:output_type -> lnrpc.EstimateFeeResponse
-	47,  // 258: lnrpc.Lightning.SendCoins:output_type -> lnrpc.SendCoinsResponse
-	49,  // 259: lnrpc.Lightning.ListUnspent:output_type -> lnrpc.ListUnspentResponse
-	29,  // 260: lnrpc.Lightning.SubscribeTransactions:output_type -> lnrpc.Transaction
-	45,  // 261: lnrpc.Lightning.SendMany:output_type -> lnrpc.SendManyResponse
-	51,  // 262: lnrpc.Lightning.NewAddress:output_type -> lnrpc.NewAddressResponse
-	53,  // 263: lnrpc.Lightning.SignMessage:output_type -> lnrpc.SignMessageResponse
-	55,  // 264: lnrpc.Lightning.VerifyMessage:output_type -> lnrpc.VerifyMessageResponse
-	57,  // 265: lnrpc.Lightning.ConnectPeer:output_type -> lnrpc.ConnectPeerResponse
-	59,  // 266: lnrpc.Lightning.DisconnectPeer:output_type -> lnrpc.DisconnectPeerResponse
-	75,  // 267: lnrpc.Lightning.ListPeers:output_type -> lnrpc.ListPeersResponse
-	77,  // 268: lnrpc.Lightning.SubscribePeerEvents:output_type -> lnrpc.PeerEvent
-	79,  // 269: lnrpc.Lightning.GetInfo:output_type -> lnrpc.GetInfoResponse
-	81,  // 270: lnrpc.Lightning.GetDebugInfo:output_type -> lnrpc.GetDebugInfoResponse
-	83,  // 271: lnrpc.Lightning.GetRecoveryInfo:output_type -> lnrpc.GetRecoveryInfoResponse
-	110, // 272: lnrpc.Lightning.PendingChannels:output_type -> lnrpc.PendingChannelsResponse
-	64,  // 273: lnrpc.Lightning.ListChannels:output_type -> lnrpc.ListChannelsResponse
-	112, // 274: lnrpc.Lightning.SubscribeChannelEvents:output_type -> lnrpc.ChannelEventUpdate
-	71,  // 275: lnrpc.Lightning.ClosedChannels:output_type -> lnrpc.ClosedChannelsResponse
-	38,  // 276: lnrpc.Lightning.OpenChannelSync:output_type -> lnrpc.ChannelPoint
-	97,  // 277: lnrpc.Lightning.OpenChannel:output_type -> lnrpc.OpenStatusUpdate
-	95,  // 278: lnrpc.Lightning.BatchOpenChannel:output_type -> lnrpc.BatchOpenChannelResponse
-	107, // 279: lnrpc.Lightning.FundingStateStep:output_type -> lnrpc.FundingStateStepResp
-	36,  // 280: lnrpc.Lightning.ChannelAcceptor:output_type -> lnrpc.ChannelAcceptRequest
-	89,  // 281: lnrpc.Lightning.CloseChannel:output_type -> lnrpc.CloseStatusUpdate
-	172, // 282: lnrpc.Lightning.AbandonChannel:output_type -> lnrpc.AbandonChannelResponse
-	34,  // 283: lnrpc.Lightning.SendPayment:output_type -> lnrpc.SendResponse
-	34,  // 284: lnrpc.Lightning.SendPaymentSync:output_type -> lnrpc.SendResponse
-	34,  // 285: lnrpc.Lightning.SendToRoute:output_type -> lnrpc.SendResponse
-	34,  // 286: lnrpc.Lightning.SendToRouteSync:output_type -> lnrpc.SendResponse
-	158, // 287: lnrpc.Lightning.AddInvoice:output_type -> lnrpc.AddInvoiceResponse
-	161, // 288: lnrpc.Lightning.ListInvoices:output_type -> lnrpc.ListInvoiceResponse
-	155, // 289: lnrpc.Lightning.LookupInvoice:output_type -> lnrpc.Invoice
-	155, // 290: lnrpc.Lightning.SubscribeInvoices:output_type -> lnrpc.Invoice
-	176, // 291: lnrpc.Lightning.DecodePayReq:output_type -> lnrpc.PayReq
-	166, // 292: lnrpc.Lightning.ListPayments:output_type -> lnrpc.ListPaymentsResponse
-	169, // 293: lnrpc.Lightning.DeletePayment:output_type -> lnrpc.DeletePaymentResponse
-	170, // 294: lnrpc.Lightning.DeleteAllPayments:output_type -> lnrpc.DeleteAllPaymentsResponse
-	134, // 295: lnrpc.Lightning.DescribeGraph:output_type -> lnrpc.ChannelGraph
-	136, // 296: lnrpc.Lightning.GetNodeMetrics:output_type -> lnrpc.NodeMetricsResponse
-	132, // 297: lnrpc.Lightning.GetChanInfo:output_type -> lnrpc.ChannelEdge
-	128, // 298: lnrpc.Lightning.GetNodeInfo:output_type -> lnrpc.NodeInfo
-	122, // 299: lnrpc.Lightning.QueryRoutes:output_type -> lnrpc.QueryRoutesResponse
-	140, // 300: lnrpc.Lightning.GetNetworkInfo:output_type -> lnrpc.NetworkInfo
-	142, // 301: lnrpc.Lightning.StopDaemon:output_type -> lnrpc.StopResponse
-	144, // 302: lnrpc.Lightning.SubscribeChannelGraph:output_type -> lnrpc.GraphTopologyUpdate
-	174, // 303: lnrpc.Lightning.DebugLevel:output_type -> lnrpc.DebugLevelResponse
-	180, // 304: lnrpc.Lightning.FeeReport:output_type -> lnrpc.FeeReportResponse
-	184, // 305: lnrpc.Lightning.UpdateChannelPolicy:output_type -> lnrpc.PolicyUpdateResponse
-	187, // 306: lnrpc.Lightning.ForwardingHistory:output_type -> lnrpc.ForwardingHistoryResponse
-	189, // 307: lnrpc.Lightning.ExportChannelBackup:output_type -> lnrpc.ChannelBackup
-	192, // 308: lnrpc.Lightning.ExportAllChannelBackups:output_type -> lnrpc.ChanBackupSnapshot
-	197, // 309: lnrpc.Lightning.VerifyChanBackup:output_type -> lnrpc.VerifyChanBackupResponse
-	195, // 310: lnrpc.Lightning.RestoreChannelBackups:output_type -> lnrpc.RestoreBackupResponse
-	192, // 311: lnrpc.Lightning.SubscribeChannelBackups:output_type -> lnrpc.ChanBackupSnapshot
-	200, // 312: lnrpc.Lightning.BakeMacaroon:output_type -> lnrpc.BakeMacaroonResponse
-	202, // 313: lnrpc.Lightning.ListMacaroonIDs:output_type -> lnrpc.ListMacaroonIDsResponse
-	204, // 314: lnrpc.Lightning.DeleteMacaroonID:output_type -> lnrpc.DeleteMacaroonIDResponse
-	207, // 315: lnrpc.Lightning.ListPermissions:output_type -> lnrpc.ListPermissionsResponse
-	213, // 316: lnrpc.Lightning.CheckMacaroonPermissions:output_type -> lnrpc.CheckMacPermResponse
-	214, // 317: lnrpc.Lightning.RegisterRPCMiddleware:output_type -> lnrpc.RPCMiddlewareRequest
-	26,  // 318: lnrpc.Lightning.SendCustomMessage:output_type -> lnrpc.SendCustomMessageResponse
-	24,  // 319: lnrpc.Lightning.SubscribeCustomMessages:output_type -> lnrpc.CustomMessage
-	67,  // 320: lnrpc.Lightning.ListAliases:output_type -> lnrpc.ListAliasesResponse
-	22,  // 321: lnrpc.Lightning.LookupHtlcResolution:output_type -> lnrpc.LookupHtlcResolutionResponse
-	254, // [254:322] is the sub-list for method output_type
-	186, // [186:254] is the sub-list for method input_type
-	186, // [186:186] is the sub-list for extension type_name
-	186, // [186:186] is the sub-list for extension extendee
-	0,   // [0:186] is the sub-list for field type_name
+	151, // 141: lnrpc.PayReq.blinded_paths:type_name -> lnrpc.BlindedPaymentPath
+	179, // 142: lnrpc.FeeReportResponse.channel_fees:type_name -> lnrpc.ChannelFeeReport
+	38,  // 143: lnrpc.PolicyUpdateRequest.chan_point:type_name -> lnrpc.ChannelPoint
+	181, // 144: lnrpc.PolicyUpdateRequest.inbound_fee:type_name -> lnrpc.InboundFee
+	39,  // 145: lnrpc.FailedUpdate.outpoint:type_name -> lnrpc.OutPoint
+	11,  // 146: lnrpc.FailedUpdate.reason:type_name -> lnrpc.UpdateFailure
+	183, // 147: lnrpc.PolicyUpdateResponse.failed_updates:type_name -> lnrpc.FailedUpdate
+	186, // 148: lnrpc.ForwardingHistoryResponse.forwarding_events:type_name -> lnrpc.ForwardingEvent
+	38,  // 149: lnrpc.ExportChannelBackupRequest.chan_point:type_name -> lnrpc.ChannelPoint
+	38,  // 150: lnrpc.ChannelBackup.chan_point:type_name -> lnrpc.ChannelPoint
+	38,  // 151: lnrpc.MultiChanBackup.chan_points:type_name -> lnrpc.ChannelPoint
+	193, // 152: lnrpc.ChanBackupSnapshot.single_chan_backups:type_name -> lnrpc.ChannelBackups
+	190, // 153: lnrpc.ChanBackupSnapshot.multi_chan_backup:type_name -> lnrpc.MultiChanBackup
+	189, // 154: lnrpc.ChannelBackups.chan_backups:type_name -> lnrpc.ChannelBackup
+	193, // 155: lnrpc.RestoreChanBackupRequest.chan_backups:type_name -> lnrpc.ChannelBackups
+	198, // 156: lnrpc.BakeMacaroonRequest.permissions:type_name -> lnrpc.MacaroonPermission
+	198, // 157: lnrpc.MacaroonPermissionList.permissions:type_name -> lnrpc.MacaroonPermission
+	245, // 158: lnrpc.ListPermissionsResponse.method_permissions:type_name -> lnrpc.ListPermissionsResponse.MethodPermissionsEntry
+	20,  // 159: lnrpc.Failure.code:type_name -> lnrpc.Failure.FailureCode
+	209, // 160: lnrpc.Failure.channel_update:type_name -> lnrpc.ChannelUpdate
+	211, // 161: lnrpc.MacaroonId.ops:type_name -> lnrpc.Op
+	198, // 162: lnrpc.CheckMacPermRequest.permissions:type_name -> lnrpc.MacaroonPermission
+	215, // 163: lnrpc.RPCMiddlewareRequest.stream_auth:type_name -> lnrpc.StreamAuth
+	216, // 164: lnrpc.RPCMiddlewareRequest.request:type_name -> lnrpc.RPCMessage
+	216, // 165: lnrpc.RPCMiddlewareRequest.response:type_name -> lnrpc.RPCMessage
+	218, // 166: lnrpc.RPCMiddlewareResponse.register:type_name -> lnrpc.MiddlewareRegistration
+	219, // 167: lnrpc.RPCMiddlewareResponse.feedback:type_name -> lnrpc.InterceptFeedback
+	177, // 168: lnrpc.Peer.FeaturesEntry.value:type_name -> lnrpc.Feature
+	177, // 169: lnrpc.GetInfoResponse.FeaturesEntry.value:type_name -> lnrpc.Feature
+	4,   // 170: lnrpc.PendingChannelsResponse.PendingChannel.initiator:type_name -> lnrpc.Initiator
+	3,   // 171: lnrpc.PendingChannelsResponse.PendingChannel.commitment_type:type_name -> lnrpc.CommitmentType
+	226, // 172: lnrpc.PendingChannelsResponse.PendingOpenChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel
+	226, // 173: lnrpc.PendingChannelsResponse.WaitingCloseChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel
+	229, // 174: lnrpc.PendingChannelsResponse.WaitingCloseChannel.commitments:type_name -> lnrpc.PendingChannelsResponse.Commitments
+	226, // 175: lnrpc.PendingChannelsResponse.ClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel
+	226, // 176: lnrpc.PendingChannelsResponse.ForceClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel
+	108, // 177: lnrpc.PendingChannelsResponse.ForceClosedChannel.pending_htlcs:type_name -> lnrpc.PendingHTLC
+	15,  // 178: lnrpc.PendingChannelsResponse.ForceClosedChannel.anchor:type_name -> lnrpc.PendingChannelsResponse.ForceClosedChannel.AnchorState
+	113, // 179: lnrpc.WalletBalanceResponse.AccountBalanceEntry.value:type_name -> lnrpc.WalletAccountBalance
+	177, // 180: lnrpc.LightningNode.FeaturesEntry.value:type_name -> lnrpc.Feature
+	137, // 181: lnrpc.NodeMetricsResponse.BetweennessCentralityEntry.value:type_name -> lnrpc.FloatMetric
+	177, // 182: lnrpc.NodeUpdate.FeaturesEntry.value:type_name -> lnrpc.Feature
+	177, // 183: lnrpc.Invoice.FeaturesEntry.value:type_name -> lnrpc.Feature
+	154, // 184: lnrpc.Invoice.AmpInvoiceStateEntry.value:type_name -> lnrpc.AMPInvoiceState
+	177, // 185: lnrpc.PayReq.FeaturesEntry.value:type_name -> lnrpc.Feature
+	205, // 186: lnrpc.ListPermissionsResponse.MethodPermissionsEntry.value:type_name -> lnrpc.MacaroonPermissionList
+	114, // 187: lnrpc.Lightning.WalletBalance:input_type -> lnrpc.WalletBalanceRequest
+	117, // 188: lnrpc.Lightning.ChannelBalance:input_type -> lnrpc.ChannelBalanceRequest
+	30,  // 189: lnrpc.Lightning.GetTransactions:input_type -> lnrpc.GetTransactionsRequest
+	42,  // 190: lnrpc.Lightning.EstimateFee:input_type -> lnrpc.EstimateFeeRequest
+	46,  // 191: lnrpc.Lightning.SendCoins:input_type -> lnrpc.SendCoinsRequest
+	48,  // 192: lnrpc.Lightning.ListUnspent:input_type -> lnrpc.ListUnspentRequest
+	30,  // 193: lnrpc.Lightning.SubscribeTransactions:input_type -> lnrpc.GetTransactionsRequest
+	44,  // 194: lnrpc.Lightning.SendMany:input_type -> lnrpc.SendManyRequest
+	50,  // 195: lnrpc.Lightning.NewAddress:input_type -> lnrpc.NewAddressRequest
+	52,  // 196: lnrpc.Lightning.SignMessage:input_type -> lnrpc.SignMessageRequest
+	54,  // 197: lnrpc.Lightning.VerifyMessage:input_type -> lnrpc.VerifyMessageRequest
+	56,  // 198: lnrpc.Lightning.ConnectPeer:input_type -> lnrpc.ConnectPeerRequest
+	58,  // 199: lnrpc.Lightning.DisconnectPeer:input_type -> lnrpc.DisconnectPeerRequest
+	74,  // 200: lnrpc.Lightning.ListPeers:input_type -> lnrpc.ListPeersRequest
+	76,  // 201: lnrpc.Lightning.SubscribePeerEvents:input_type -> lnrpc.PeerEventSubscription
+	78,  // 202: lnrpc.Lightning.GetInfo:input_type -> lnrpc.GetInfoRequest
+	80,  // 203: lnrpc.Lightning.GetDebugInfo:input_type -> lnrpc.GetDebugInfoRequest
+	82,  // 204: lnrpc.Lightning.GetRecoveryInfo:input_type -> lnrpc.GetRecoveryInfoRequest
+	109, // 205: lnrpc.Lightning.PendingChannels:input_type -> lnrpc.PendingChannelsRequest
+	63,  // 206: lnrpc.Lightning.ListChannels:input_type -> lnrpc.ListChannelsRequest
+	111, // 207: lnrpc.Lightning.SubscribeChannelEvents:input_type -> lnrpc.ChannelEventSubscription
+	70,  // 208: lnrpc.Lightning.ClosedChannels:input_type -> lnrpc.ClosedChannelsRequest
+	96,  // 209: lnrpc.Lightning.OpenChannelSync:input_type -> lnrpc.OpenChannelRequest
+	96,  // 210: lnrpc.Lightning.OpenChannel:input_type -> lnrpc.OpenChannelRequest
+	93,  // 211: lnrpc.Lightning.BatchOpenChannel:input_type -> lnrpc.BatchOpenChannelRequest
+	106, // 212: lnrpc.Lightning.FundingStateStep:input_type -> lnrpc.FundingTransitionMsg
+	37,  // 213: lnrpc.Lightning.ChannelAcceptor:input_type -> lnrpc.ChannelAcceptResponse
+	88,  // 214: lnrpc.Lightning.CloseChannel:input_type -> lnrpc.CloseChannelRequest
+	171, // 215: lnrpc.Lightning.AbandonChannel:input_type -> lnrpc.AbandonChannelRequest
+	33,  // 216: lnrpc.Lightning.SendPayment:input_type -> lnrpc.SendRequest
+	33,  // 217: lnrpc.Lightning.SendPaymentSync:input_type -> lnrpc.SendRequest
+	35,  // 218: lnrpc.Lightning.SendToRoute:input_type -> lnrpc.SendToRouteRequest
+	35,  // 219: lnrpc.Lightning.SendToRouteSync:input_type -> lnrpc.SendToRouteRequest
+	155, // 220: lnrpc.Lightning.AddInvoice:input_type -> lnrpc.Invoice
+	160, // 221: lnrpc.Lightning.ListInvoices:input_type -> lnrpc.ListInvoiceRequest
+	159, // 222: lnrpc.Lightning.LookupInvoice:input_type -> lnrpc.PaymentHash
+	162, // 223: lnrpc.Lightning.SubscribeInvoices:input_type -> lnrpc.InvoiceSubscription
+	175, // 224: lnrpc.Lightning.DecodePayReq:input_type -> lnrpc.PayReqString
+	165, // 225: lnrpc.Lightning.ListPayments:input_type -> lnrpc.ListPaymentsRequest
+	167, // 226: lnrpc.Lightning.DeletePayment:input_type -> lnrpc.DeletePaymentRequest
+	168, // 227: lnrpc.Lightning.DeleteAllPayments:input_type -> lnrpc.DeleteAllPaymentsRequest
+	133, // 228: lnrpc.Lightning.DescribeGraph:input_type -> lnrpc.ChannelGraphRequest
+	135, // 229: lnrpc.Lightning.GetNodeMetrics:input_type -> lnrpc.NodeMetricsRequest
+	138, // 230: lnrpc.Lightning.GetChanInfo:input_type -> lnrpc.ChanInfoRequest
+	127, // 231: lnrpc.Lightning.GetNodeInfo:input_type -> lnrpc.NodeInfoRequest
+	119, // 232: lnrpc.Lightning.QueryRoutes:input_type -> lnrpc.QueryRoutesRequest
+	139, // 233: lnrpc.Lightning.GetNetworkInfo:input_type -> lnrpc.NetworkInfoRequest
+	141, // 234: lnrpc.Lightning.StopDaemon:input_type -> lnrpc.StopRequest
+	143, // 235: lnrpc.Lightning.SubscribeChannelGraph:input_type -> lnrpc.GraphTopologySubscription
+	173, // 236: lnrpc.Lightning.DebugLevel:input_type -> lnrpc.DebugLevelRequest
+	178, // 237: lnrpc.Lightning.FeeReport:input_type -> lnrpc.FeeReportRequest
+	182, // 238: lnrpc.Lightning.UpdateChannelPolicy:input_type -> lnrpc.PolicyUpdateRequest
+	185, // 239: lnrpc.Lightning.ForwardingHistory:input_type -> lnrpc.ForwardingHistoryRequest
+	188, // 240: lnrpc.Lightning.ExportChannelBackup:input_type -> lnrpc.ExportChannelBackupRequest
+	191, // 241: lnrpc.Lightning.ExportAllChannelBackups:input_type -> lnrpc.ChanBackupExportRequest
+	192, // 242: lnrpc.Lightning.VerifyChanBackup:input_type -> lnrpc.ChanBackupSnapshot
+	194, // 243: lnrpc.Lightning.RestoreChannelBackups:input_type -> lnrpc.RestoreChanBackupRequest
+	196, // 244: lnrpc.Lightning.SubscribeChannelBackups:input_type -> lnrpc.ChannelBackupSubscription
+	199, // 245: lnrpc.Lightning.BakeMacaroon:input_type -> lnrpc.BakeMacaroonRequest
+	201, // 246: lnrpc.Lightning.ListMacaroonIDs:input_type -> lnrpc.ListMacaroonIDsRequest
+	203, // 247: lnrpc.Lightning.DeleteMacaroonID:input_type -> lnrpc.DeleteMacaroonIDRequest
+	206, // 248: lnrpc.Lightning.ListPermissions:input_type -> lnrpc.ListPermissionsRequest
+	212, // 249: lnrpc.Lightning.CheckMacaroonPermissions:input_type -> lnrpc.CheckMacPermRequest
+	217, // 250: lnrpc.Lightning.RegisterRPCMiddleware:input_type -> lnrpc.RPCMiddlewareResponse
+	25,  // 251: lnrpc.Lightning.SendCustomMessage:input_type -> lnrpc.SendCustomMessageRequest
+	23,  // 252: lnrpc.Lightning.SubscribeCustomMessages:input_type -> lnrpc.SubscribeCustomMessagesRequest
+	66,  // 253: lnrpc.Lightning.ListAliases:input_type -> lnrpc.ListAliasesRequest
+	21,  // 254: lnrpc.Lightning.LookupHtlcResolution:input_type -> lnrpc.LookupHtlcResolutionRequest
+	115, // 255: lnrpc.Lightning.WalletBalance:output_type -> lnrpc.WalletBalanceResponse
+	118, // 256: lnrpc.Lightning.ChannelBalance:output_type -> lnrpc.ChannelBalanceResponse
+	31,  // 257: lnrpc.Lightning.GetTransactions:output_type -> lnrpc.TransactionDetails
+	43,  // 258: lnrpc.Lightning.EstimateFee:output_type -> lnrpc.EstimateFeeResponse
+	47,  // 259: lnrpc.Lightning.SendCoins:output_type -> lnrpc.SendCoinsResponse
+	49,  // 260: lnrpc.Lightning.ListUnspent:output_type -> lnrpc.ListUnspentResponse
+	29,  // 261: lnrpc.Lightning.SubscribeTransactions:output_type -> lnrpc.Transaction
+	45,  // 262: lnrpc.Lightning.SendMany:output_type -> lnrpc.SendManyResponse
+	51,  // 263: lnrpc.Lightning.NewAddress:output_type -> lnrpc.NewAddressResponse
+	53,  // 264: lnrpc.Lightning.SignMessage:output_type -> lnrpc.SignMessageResponse
+	55,  // 265: lnrpc.Lightning.VerifyMessage:output_type -> lnrpc.VerifyMessageResponse
+	57,  // 266: lnrpc.Lightning.ConnectPeer:output_type -> lnrpc.ConnectPeerResponse
+	59,  // 267: lnrpc.Lightning.DisconnectPeer:output_type -> lnrpc.DisconnectPeerResponse
+	75,  // 268: lnrpc.Lightning.ListPeers:output_type -> lnrpc.ListPeersResponse
+	77,  // 269: lnrpc.Lightning.SubscribePeerEvents:output_type -> lnrpc.PeerEvent
+	79,  // 270: lnrpc.Lightning.GetInfo:output_type -> lnrpc.GetInfoResponse
+	81,  // 271: lnrpc.Lightning.GetDebugInfo:output_type -> lnrpc.GetDebugInfoResponse
+	83,  // 272: lnrpc.Lightning.GetRecoveryInfo:output_type -> lnrpc.GetRecoveryInfoResponse
+	110, // 273: lnrpc.Lightning.PendingChannels:output_type -> lnrpc.PendingChannelsResponse
+	64,  // 274: lnrpc.Lightning.ListChannels:output_type -> lnrpc.ListChannelsResponse
+	112, // 275: lnrpc.Lightning.SubscribeChannelEvents:output_type -> lnrpc.ChannelEventUpdate
+	71,  // 276: lnrpc.Lightning.ClosedChannels:output_type -> lnrpc.ClosedChannelsResponse
+	38,  // 277: lnrpc.Lightning.OpenChannelSync:output_type -> lnrpc.ChannelPoint
+	97,  // 278: lnrpc.Lightning.OpenChannel:output_type -> lnrpc.OpenStatusUpdate
+	95,  // 279: lnrpc.Lightning.BatchOpenChannel:output_type -> lnrpc.BatchOpenChannelResponse
+	107, // 280: lnrpc.Lightning.FundingStateStep:output_type -> lnrpc.FundingStateStepResp
+	36,  // 281: lnrpc.Lightning.ChannelAcceptor:output_type -> lnrpc.ChannelAcceptRequest
+	89,  // 282: lnrpc.Lightning.CloseChannel:output_type -> lnrpc.CloseStatusUpdate
+	172, // 283: lnrpc.Lightning.AbandonChannel:output_type -> lnrpc.AbandonChannelResponse
+	34,  // 284: lnrpc.Lightning.SendPayment:output_type -> lnrpc.SendResponse
+	34,  // 285: lnrpc.Lightning.SendPaymentSync:output_type -> lnrpc.SendResponse
+	34,  // 286: lnrpc.Lightning.SendToRoute:output_type -> lnrpc.SendResponse
+	34,  // 287: lnrpc.Lightning.SendToRouteSync:output_type -> lnrpc.SendResponse
+	158, // 288: lnrpc.Lightning.AddInvoice:output_type -> lnrpc.AddInvoiceResponse
+	161, // 289: lnrpc.Lightning.ListInvoices:output_type -> lnrpc.ListInvoiceResponse
+	155, // 290: lnrpc.Lightning.LookupInvoice:output_type -> lnrpc.Invoice
+	155, // 291: lnrpc.Lightning.SubscribeInvoices:output_type -> lnrpc.Invoice
+	176, // 292: lnrpc.Lightning.DecodePayReq:output_type -> lnrpc.PayReq
+	166, // 293: lnrpc.Lightning.ListPayments:output_type -> lnrpc.ListPaymentsResponse
+	169, // 294: lnrpc.Lightning.DeletePayment:output_type -> lnrpc.DeletePaymentResponse
+	170, // 295: lnrpc.Lightning.DeleteAllPayments:output_type -> lnrpc.DeleteAllPaymentsResponse
+	134, // 296: lnrpc.Lightning.DescribeGraph:output_type -> lnrpc.ChannelGraph
+	136, // 297: lnrpc.Lightning.GetNodeMetrics:output_type -> lnrpc.NodeMetricsResponse
+	132, // 298: lnrpc.Lightning.GetChanInfo:output_type -> lnrpc.ChannelEdge
+	128, // 299: lnrpc.Lightning.GetNodeInfo:output_type -> lnrpc.NodeInfo
+	122, // 300: lnrpc.Lightning.QueryRoutes:output_type -> lnrpc.QueryRoutesResponse
+	140, // 301: lnrpc.Lightning.GetNetworkInfo:output_type -> lnrpc.NetworkInfo
+	142, // 302: lnrpc.Lightning.StopDaemon:output_type -> lnrpc.StopResponse
+	144, // 303: lnrpc.Lightning.SubscribeChannelGraph:output_type -> lnrpc.GraphTopologyUpdate
+	174, // 304: lnrpc.Lightning.DebugLevel:output_type -> lnrpc.DebugLevelResponse
+	180, // 305: lnrpc.Lightning.FeeReport:output_type -> lnrpc.FeeReportResponse
+	184, // 306: lnrpc.Lightning.UpdateChannelPolicy:output_type -> lnrpc.PolicyUpdateResponse
+	187, // 307: lnrpc.Lightning.ForwardingHistory:output_type -> lnrpc.ForwardingHistoryResponse
+	189, // 308: lnrpc.Lightning.ExportChannelBackup:output_type -> lnrpc.ChannelBackup
+	192, // 309: lnrpc.Lightning.ExportAllChannelBackups:output_type -> lnrpc.ChanBackupSnapshot
+	197, // 310: lnrpc.Lightning.VerifyChanBackup:output_type -> lnrpc.VerifyChanBackupResponse
+	195, // 311: lnrpc.Lightning.RestoreChannelBackups:output_type -> lnrpc.RestoreBackupResponse
+	192, // 312: lnrpc.Lightning.SubscribeChannelBackups:output_type -> lnrpc.ChanBackupSnapshot
+	200, // 313: lnrpc.Lightning.BakeMacaroon:output_type -> lnrpc.BakeMacaroonResponse
+	202, // 314: lnrpc.Lightning.ListMacaroonIDs:output_type -> lnrpc.ListMacaroonIDsResponse
+	204, // 315: lnrpc.Lightning.DeleteMacaroonID:output_type -> lnrpc.DeleteMacaroonIDResponse
+	207, // 316: lnrpc.Lightning.ListPermissions:output_type -> lnrpc.ListPermissionsResponse
+	213, // 317: lnrpc.Lightning.CheckMacaroonPermissions:output_type -> lnrpc.CheckMacPermResponse
+	214, // 318: lnrpc.Lightning.RegisterRPCMiddleware:output_type -> lnrpc.RPCMiddlewareRequest
+	26,  // 319: lnrpc.Lightning.SendCustomMessage:output_type -> lnrpc.SendCustomMessageResponse
+	24,  // 320: lnrpc.Lightning.SubscribeCustomMessages:output_type -> lnrpc.CustomMessage
+	67,  // 321: lnrpc.Lightning.ListAliases:output_type -> lnrpc.ListAliasesResponse
+	22,  // 322: lnrpc.Lightning.LookupHtlcResolution:output_type -> lnrpc.LookupHtlcResolutionResponse
+	255, // [255:323] is the sub-list for method output_type
+	187, // [187:255] is the sub-list for method input_type
+	187, // [187:187] is the sub-list for extension type_name
+	187, // [187:187] is the sub-list for extension extendee
+	0,   // [0:187] is the sub-list for field type_name
 }
 
 func init() { file_lightning_proto_init() }
diff --git a/lnrpc/lightning.proto b/lnrpc/lightning.proto
index a13ca95c5..5b912e9c0 100644
--- a/lnrpc/lightning.proto
+++ b/lnrpc/lightning.proto
@@ -4289,6 +4289,7 @@ message PayReq {
     bytes payment_addr = 11;
     int64 num_msat = 12;
     map<uint32, Feature> features = 13;
+    repeated BlindedPaymentPath blinded_paths = 14;
 }
 
 enum FeatureBit {
diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json
index ae59f4a19..a0c6761ff 100644
--- a/lnrpc/lightning.swagger.json
+++ b/lnrpc/lightning.swagger.json
@@ -6299,6 +6299,12 @@
           "additionalProperties": {
             "$ref": "#/definitions/lnrpcFeature"
           }
+        },
+        "blinded_paths": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/lnrpcBlindedPaymentPath"
+          }
         }
       }
     },
diff --git a/record/blinded_data.go b/record/blinded_data.go
index 7990fa738..7b7081b3b 100644
--- a/record/blinded_data.go
+++ b/record/blinded_data.go
@@ -14,8 +14,18 @@ import (
 // route encrypted data blob that is created by the recipient to provide
 // forwarding information.
 type BlindedRouteData struct {
+	// Padding is an optional set of bytes that a recipient can use to pad
+	// the data so that the encrypted recipient data blobs are all the same
+	// length.
+	Padding tlv.OptionalRecordT[tlv.TlvType1, []byte]
+
 	// ShortChannelID is the channel ID of the next hop.
-	ShortChannelID tlv.RecordT[tlv.TlvType2, lnwire.ShortChannelID]
+	ShortChannelID tlv.OptionalRecordT[tlv.TlvType2, lnwire.ShortChannelID]
+
+	// PathID is a secret set of bytes that the blinded path creator will
+	// set so that they can check the value on decryption to ensure that the
+	// path they created was used for the intended purpose.
+	PathID tlv.OptionalRecordT[tlv.TlvType6, []byte]
 
 	// NextBlindingOverride is a blinding point that should be switched
 	// in for the next hop. This is used to combine two blinded paths into
@@ -24,7 +34,7 @@ type BlindedRouteData struct {
 	NextBlindingOverride tlv.OptionalRecordT[tlv.TlvType8, *btcec.PublicKey]
 
 	// RelayInfo provides the relay parameters for the hop.
-	RelayInfo tlv.RecordT[tlv.TlvType10, PaymentRelayInfo]
+	RelayInfo tlv.OptionalRecordT[tlv.TlvType10, PaymentRelayInfo]
 
 	// Constraints provides the payment relay constraints for the hop.
 	Constraints tlv.OptionalRecordT[tlv.TlvType12, PaymentConstraints]
@@ -33,16 +43,20 @@ type BlindedRouteData struct {
 	Features tlv.OptionalRecordT[tlv.TlvType14, lnwire.FeatureVector]
 }
 
-// NewBlindedRouteData creates the data that's provided for hops within a
-// blinded route.
-func NewBlindedRouteData(chanID lnwire.ShortChannelID,
+// NewNonFinalBlindedRouteData creates the data that's provided for hops within
+// a blinded route.
+func NewNonFinalBlindedRouteData(chanID lnwire.ShortChannelID,
 	blindingOverride *btcec.PublicKey, relayInfo PaymentRelayInfo,
 	constraints *PaymentConstraints,
 	features *lnwire.FeatureVector) *BlindedRouteData {
 
 	info := &BlindedRouteData{
-		ShortChannelID: tlv.NewRecordT[tlv.TlvType2](chanID),
-		RelayInfo:      tlv.NewRecordT[tlv.TlvType10](relayInfo),
+		ShortChannelID: tlv.SomeRecordT(
+			tlv.NewRecordT[tlv.TlvType2](chanID),
+		),
+		RelayInfo: tlv.SomeRecordT(
+			tlv.NewRecordT[tlv.TlvType10](relayInfo),
+		),
 	}
 
 	if blindingOverride != nil {
@@ -64,12 +78,36 @@ func NewBlindedRouteData(chanID lnwire.ShortChannelID,
 	return info
 }
 
+// NewFinalHopBlindedRouteData creates the data that's provided for the final
+// hop in a blinded route.
+func NewFinalHopBlindedRouteData(constraints *PaymentConstraints,
+	pathID []byte) *BlindedRouteData {
+
+	var data BlindedRouteData
+	if pathID != nil {
+		data.PathID = tlv.SomeRecordT(
+			tlv.NewPrimitiveRecord[tlv.TlvType6](pathID),
+		)
+	}
+
+	if constraints != nil {
+		data.Constraints = tlv.SomeRecordT(
+			tlv.NewRecordT[tlv.TlvType12](*constraints))
+	}
+
+	return &data
+}
+
 // DecodeBlindedRouteData decodes the data provided within a blinded route.
 func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) {
 	var (
 		d BlindedRouteData
 
+		padding          = d.Padding.Zero()
+		scid             = d.ShortChannelID.Zero()
+		pathID           = d.PathID.Zero()
 		blindingOverride = d.NextBlindingOverride.Zero()
+		relayInfo        = d.RelayInfo.Zero()
 		constraints      = d.Constraints.Zero()
 		features         = d.Features.Zero()
 	)
@@ -80,19 +118,35 @@ func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) {
 	}
 
 	typeMap, err := tlvRecords.ExtractRecords(
-		&d.ShortChannelID,
-		&blindingOverride, &d.RelayInfo.Val, &constraints,
-		&features,
+		&padding, &scid, &pathID, &blindingOverride, &relayInfo,
+		&constraints, &features,
 	)
 	if err != nil {
 		return nil, err
 	}
 
-	val, ok := typeMap[d.NextBlindingOverride.TlvType()]
+	val, ok := typeMap[d.Padding.TlvType()]
+	if ok && val == nil {
+		d.Padding = tlv.SomeRecordT(padding)
+	}
+
+	if val, ok := typeMap[d.ShortChannelID.TlvType()]; ok && val == nil {
+		d.ShortChannelID = tlv.SomeRecordT(scid)
+	}
+
+	if val, ok := typeMap[d.PathID.TlvType()]; ok && val == nil {
+		d.PathID = tlv.SomeRecordT(pathID)
+	}
+
+	val, ok = typeMap[d.NextBlindingOverride.TlvType()]
 	if ok && val == nil {
 		d.NextBlindingOverride = tlv.SomeRecordT(blindingOverride)
 	}
 
+	if val, ok := typeMap[d.RelayInfo.TlvType()]; ok && val == nil {
+		d.RelayInfo = tlv.SomeRecordT(relayInfo)
+	}
+
 	if val, ok := typeMap[d.Constraints.TlvType()]; ok && val == nil {
 		d.Constraints = tlv.SomeRecordT(constraints)
 	}
@@ -111,7 +165,19 @@ func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) {
 		recordProducers = make([]tlv.RecordProducer, 0, 5)
 	)
 
-	recordProducers = append(recordProducers, &data.ShortChannelID)
+	data.Padding.WhenSome(func(p tlv.RecordT[tlv.TlvType1, []byte]) {
+		recordProducers = append(recordProducers, &p)
+	})
+
+	data.ShortChannelID.WhenSome(func(scid tlv.RecordT[tlv.TlvType2,
+		lnwire.ShortChannelID]) {
+
+		recordProducers = append(recordProducers, &scid)
+	})
+
+	data.PathID.WhenSome(func(pathID tlv.RecordT[tlv.TlvType6, []byte]) {
+		recordProducers = append(recordProducers, &pathID)
+	})
 
 	data.NextBlindingOverride.WhenSome(func(pk tlv.RecordT[tlv.TlvType8,
 		*btcec.PublicKey]) {
@@ -119,7 +185,11 @@ func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) {
 		recordProducers = append(recordProducers, &pk)
 	})
 
-	recordProducers = append(recordProducers, &data.RelayInfo.Val)
+	data.RelayInfo.WhenSome(func(r tlv.RecordT[tlv.TlvType10,
+		PaymentRelayInfo]) {
+
+		recordProducers = append(recordProducers, &r)
+	})
 
 	data.Constraints.WhenSome(func(cs tlv.RecordT[tlv.TlvType12,
 		PaymentConstraints]) {
@@ -140,6 +210,19 @@ func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) {
 	return e[:], nil
 }
 
+// PadBy adds "n" padding bytes to the BlindedRouteData using the Padding field.
+// Callers should be aware that the total payload size will change by more than
+// "n" since the "n" bytes will be prefixed by BigSize type and length fields.
+// Callers may need to call PadBy iteratively until each encrypted data packet
+// is the same size and so each call will overwrite the Padding record.
+// Note that calling PadBy with an n value of 0 will still result in a zero
+// length TLV entry being added.
+func (b *BlindedRouteData) PadBy(n int) {
+	b.Padding = tlv.SomeRecordT(
+		tlv.NewPrimitiveRecord[tlv.TlvType1](make([]byte, n)),
+	)
+}
+
 // PaymentRelayInfo describes the relay policy for a blinded path.
 type PaymentRelayInfo struct {
 	// CltvExpiryDelta is the expiry delta for the payment.
@@ -153,8 +236,8 @@ type PaymentRelayInfo struct {
 	BaseFee uint32
 }
 
-// newPaymentRelayRecord creates a tlv.Record that encodes the payment relay
-// (type 10) type for an encrypted blob payload.
+// Record creates a tlv.Record that encodes the payment relay (type 10) type for
+// an encrypted blob payload.
 func (i *PaymentRelayInfo) Record() tlv.Record {
 	return tlv.MakeDynamicRecord(
 		10, &i, func() uint64 {
diff --git a/routing/additional_edge.go b/routing/additional_edge.go
index eee17cce1..5f2d42eeb 100644
--- a/routing/additional_edge.go
+++ b/routing/additional_edge.go
@@ -2,8 +2,8 @@ package routing
 
 import (
 	"errors"
+	"fmt"
 
-	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/lightningnetwork/lnd/channeldb/models"
 	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/lightningnetwork/lnd/routing/route"
@@ -29,6 +29,11 @@ type AdditionalEdge interface {
 
 	// EdgePolicy returns the policy of the additional edge.
 	EdgePolicy() *models.CachedEdgePolicy
+
+	// BlindedPayment returns the BlindedPayment that this additional edge
+	// info was derived from. It will return nil if this edge was not
+	// derived from a blinded route.
+	BlindedPayment() *BlindedPayment
 }
 
 // PayloadSizeFunc defines the interface for the payload size function.
@@ -60,12 +65,49 @@ func (p *PrivateEdge) IntermediatePayloadSize(amount lnwire.MilliSatoshi,
 	return hop.PayloadSize(channelID)
 }
 
+// BlindedPayment is a no-op for a PrivateEdge since it is not associated with
+// a blinded payment. This will thus return nil.
+func (p *PrivateEdge) BlindedPayment() *BlindedPayment {
+	return nil
+}
+
 // BlindedEdge implements the AdditionalEdge interface. Blinded hops are viewed
-// as additional edges because they are appened at the end of a normal route.
+// as additional edges because they are appended at the end of a normal route.
 type BlindedEdge struct {
-	policy        *models.CachedEdgePolicy
-	cipherText    []byte
-	blindingPoint *btcec.PublicKey
+	policy *models.CachedEdgePolicy
+
+	// blindedPayment is the BlindedPayment that this blinded edge was
+	// derived from.
+	blindedPayment *BlindedPayment
+
+	// hopIndex is the index of the hop in the blinded payment path that
+	// this edge is associated with.
+	hopIndex int
+}
+
+// NewBlindedEdge constructs a new BlindedEdge which packages the policy info
+// for a specific hop within the given blinded payment path. The hop index
+// should correspond to the hop within the blinded payment that this edge is
+// associated with.
+func NewBlindedEdge(policy *models.CachedEdgePolicy, payment *BlindedPayment,
+	hopIndex int) (*BlindedEdge, error) {
+
+	if payment == nil {
+		return nil, fmt.Errorf("blinded payment cannot be nil for " +
+			"blinded edge")
+	}
+
+	if hopIndex < 0 || hopIndex >= len(payment.BlindedPath.BlindedHops) {
+		return nil, fmt.Errorf("the hop index %d is outside the "+
+			"valid range between 0 and %d", hopIndex,
+			len(payment.BlindedPath.BlindedHops)-1)
+	}
+
+	return &BlindedEdge{
+		policy:         policy,
+		hopIndex:       hopIndex,
+		blindedPayment: payment,
+	}, nil
 }
 
 // EdgePolicy return the policy of the BlindedEdge.
@@ -78,15 +120,23 @@ func (b *BlindedEdge) EdgePolicy() *models.CachedEdgePolicy {
 func (b *BlindedEdge) IntermediatePayloadSize(_ lnwire.MilliSatoshi, _ uint32,
 	_ uint64) uint64 {
 
+	blindedPath := b.blindedPayment.BlindedPath
+
 	hop := route.Hop{
-		BlindingPoint: b.blindingPoint,
-		EncryptedData: b.cipherText,
+		BlindingPoint: blindedPath.BlindingPoint,
+		EncryptedData: blindedPath.BlindedHops[b.hopIndex].CipherText,
 	}
 
 	// For blinded paths the next chanID is in the encrypted data tlv.
 	return hop.PayloadSize(0)
 }
 
+// BlindedPayment returns the blinded payment that this edge is associated
+// with.
+func (b *BlindedEdge) BlindedPayment() *BlindedPayment {
+	return b.blindedPayment
+}
+
 // Compile-time constraints to ensure the PrivateEdge and the BlindedEdge
 // implement the AdditionalEdge interface.
 var _ AdditionalEdge = (*PrivateEdge)(nil)
diff --git a/routing/blinding.go b/routing/blinding.go
index d2d64aa5d..788fb7b77 100644
--- a/routing/blinding.go
+++ b/routing/blinding.go
@@ -88,13 +88,13 @@ func (b *BlindedPayment) Validate() error {
 // the case of multiple blinded hops, CLTV delta is fully accounted for in the
 // hints (both for intermediate hops and the final_cltv_delta for the receiving
 // node).
-func (b *BlindedPayment) toRouteHints() RouteHints {
+func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
 	// If we just have a single hop in our blinded route, it just contains
 	// an introduction node (this is a valid path according to the spec).
 	// Since we have the un-blinded node ID for the introduction node, we
 	// don't need to add any route hints.
 	if len(b.BlindedPath.BlindedHops) == 1 {
-		return nil
+		return nil, nil
 	}
 
 	hintCount := len(b.BlindedPath.BlindedHops) - 1
@@ -136,14 +136,13 @@ func (b *BlindedPayment) toRouteHints() RouteHints {
 		ToNodeFeatures: features,
 	}
 
-	hints[fromNode] = []AdditionalEdge{
-		&BlindedEdge{
-			policy:        edgePolicy,
-			cipherText:    b.BlindedPath.BlindedHops[0].CipherText,
-			blindingPoint: b.BlindedPath.BlindingPoint,
-		},
+	edge, err := NewBlindedEdge(edgePolicy, b, 0)
+	if err != nil {
+		return nil, err
 	}
 
+	hints[fromNode] = []AdditionalEdge{edge}
+
 	// Start at an offset of 1 because the first node in our blinded hops
 	// is the introduction node and terminate at the second-last node
 	// because we're dealing with hops as pairs.
@@ -169,14 +168,13 @@ func (b *BlindedPayment) toRouteHints() RouteHints {
 			ToNodeFeatures: features,
 		}
 
-		hints[fromNode] = []AdditionalEdge{
-			&BlindedEdge{
-				policy: edgePolicy,
-				cipherText: b.BlindedPath.BlindedHops[i].
-					CipherText,
-			},
+		edge, err := NewBlindedEdge(edgePolicy, b, i)
+		if err != nil {
+			return nil, err
 		}
+
+		hints[fromNode] = []AdditionalEdge{edge}
 	}
 
-	return hints
+	return hints, nil
 }
diff --git a/routing/pathfind.go b/routing/pathfind.go
index fbe4b5830..208a55085 100644
--- a/routing/pathfind.go
+++ b/routing/pathfind.go
@@ -96,9 +96,17 @@ type edgePolicyWithSource struct {
 // such as amounts and cltvs, as well as more complex features like destination
 // custom records and payment address.
 type finalHopParams struct {
-	amt         lnwire.MilliSatoshi
-	totalAmt    lnwire.MilliSatoshi
-	cltvDelta   uint16
+	amt      lnwire.MilliSatoshi
+	totalAmt lnwire.MilliSatoshi
+
+	// cltvDelta is the final hop's minimum CLTV expiry delta.
+	//
+	// NOTE that in the case of paying to a blinded path, this value will
+	// be set to a duplicate of the blinded path's accumulated CLTV value.
+	// We would then only need to use this value in the case where the
+	// introduction node of the path is also the destination node.
+	cltvDelta uint16
+
 	records     record.CustomSet
 	paymentAddr *[32]byte
 
@@ -190,10 +198,21 @@ func newRoute(sourceVertex route.Vertex,
 			// reporting through RPC. Set to zero for the final hop.
 			fee = 0
 
-			// As this is the last hop, we'll use the specified
-			// final CLTV delta value instead of the value from the
-			// last link in the route.
-			totalTimeLock += uint32(finalHop.cltvDelta)
+			// Only include the final hop CLTV delta in the total
+			// time lock value if this is not a route to a blinded
+			// path. For blinded paths, the total time-lock from the
+			// whole path will be deduced from the introduction
+			// node's CLTV delta. The exception is for the case
+			// where the final hop is the blinded path introduction
+			// node.
+			if blindedPath == nil ||
+				len(blindedPath.BlindedHops) == 1 {
+
+				// As this is the last hop, we'll use the
+				// specified final CLTV delta value instead of
+				// the value from the last link in the route.
+				totalTimeLock += uint32(finalHop.cltvDelta)
+			}
 			outgoingTimeLock = totalTimeLock
 
 			// Attach any custom records to the final hop.
@@ -968,6 +987,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
 				inboundFee,
 				fakeHopHintCapacity,
 				reverseEdge.edge.IntermediatePayloadSize,
+				reverseEdge.edge.BlindedPayment(),
 			)
 		}
 
diff --git a/routing/router.go b/routing/router.go
index 851db4af0..149cd3415 100644
--- a/routing/router.go
+++ b/routing/router.go
@@ -2001,6 +2001,7 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex,
 		// Assume that we're starting off with a regular payment.
 		requestHints  = routeHints
 		requestExpiry = finalExpiry
+		err           error
 	)
 
 	if blindedPayment != nil {
@@ -2038,7 +2039,10 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex,
 			requestExpiry = blindedPayment.CltvExpiryDelta
 		}
 
-		requestHints = blindedPayment.toRouteHints()
+		requestHints, err = blindedPayment.toRouteHints()
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	requestTarget, err := getTargetNode(target, blindedPayment)
diff --git a/routing/unified_edges.go b/routing/unified_edges.go
index 232c92e1c..d39eda1ef 100644
--- a/routing/unified_edges.go
+++ b/routing/unified_edges.go
@@ -51,7 +51,8 @@ func newNodeEdgeUnifier(sourceNode, toNode route.Vertex, useInboundFees bool,
 // incorrectly specified.
 func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex,
 	edge *models.CachedEdgePolicy, inboundFee models.InboundFee,
-	capacity btcutil.Amount, hopPayloadSizeFn PayloadSizeFunc) {
+	capacity btcutil.Amount, hopPayloadSizeFn PayloadSizeFunc,
+	blindedPayment *BlindedPayment) {
 
 	localChan := fromNode == u.sourceNode
 
@@ -86,12 +87,9 @@ func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex,
 		inboundFee = models.InboundFee{}
 	}
 
-	unifier.edges = append(unifier.edges, &unifiedEdge{
-		policy:           edge,
-		capacity:         capacity,
-		hopPayloadSizeFn: hopPayloadSizeFn,
-		inboundFees:      inboundFee,
-	})
+	unifier.edges = append(unifier.edges, newUnifiedEdge(
+		edge, capacity, inboundFee, hopPayloadSizeFn, blindedPayment,
+	))
 }
 
 // addGraphPolicies adds all policies that are known for the toNode in the
@@ -115,7 +113,7 @@ func (u *nodeEdgeUnifier) addGraphPolicies(g routingGraph) error {
 
 		u.addPolicy(
 			channel.OtherNode, channel.InPolicy, inboundFee,
-			channel.Capacity, defaultHopPayloadSize,
+			channel.Capacity, defaultHopPayloadSize, nil,
 		)
 
 		return nil
@@ -137,6 +135,24 @@ type unifiedEdge struct {
 	// is needed because hops of a blinded path differ in their payload
 	// structure compared to cleartext hops.
 	hopPayloadSizeFn PayloadSizeFunc
+
+	// blindedPayment if set, is the BlindedPayment that this edge was
+	// derived from originally.
+	blindedPayment *BlindedPayment
+}
+
+// newUnifiedEdge constructs a new unifiedEdge.
+func newUnifiedEdge(policy *models.CachedEdgePolicy, capacity btcutil.Amount,
+	inboundFees models.InboundFee, hopPayloadSizeFn PayloadSizeFunc,
+	blindedPayment *BlindedPayment) *unifiedEdge {
+
+	return &unifiedEdge{
+		policy:           policy,
+		capacity:         capacity,
+		inboundFees:      inboundFees,
+		hopPayloadSizeFn: hopPayloadSizeFn,
+		blindedPayment:   blindedPayment,
+	}
 }
 
 // amtInRange checks whether an amount falls within the valid range for a
@@ -292,12 +308,10 @@ func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi,
 		maxBandwidth = bandwidth
 
 		// Update best edge.
-		bestEdge = &unifiedEdge{
-			policy:           edge.policy,
-			capacity:         edge.capacity,
-			hopPayloadSizeFn: edge.hopPayloadSizeFn,
-			inboundFees:      edge.inboundFees,
-		}
+		bestEdge = newUnifiedEdge(
+			edge.policy, edge.capacity, edge.inboundFees,
+			edge.hopPayloadSizeFn, edge.blindedPayment,
+		)
 	}
 
 	return bestEdge
@@ -376,10 +390,10 @@ func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi,
 		}
 		maxFee = fee
 
-		bestPolicy = &unifiedEdge{
-			policy:      edge.policy,
-			inboundFees: edge.inboundFees,
-		}
+		bestPolicy = newUnifiedEdge(
+			edge.policy, 0, edge.inboundFees, nil,
+			edge.blindedPayment,
+		)
 
 		// The payload size function for edges to a connected peer is
 		// always the same hence there is not need to find the maximum.
@@ -404,15 +418,13 @@ func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi,
 	// chance for this node pair. But this is all only needed for nodes that
 	// have distinct policies for channels to the same peer.
 	policyCopy := *bestPolicy.policy
-	modifiedEdge := unifiedEdge{
-		policy:      &policyCopy,
-		inboundFees: bestPolicy.inboundFees,
-	}
-	modifiedEdge.policy.TimeLockDelta = maxTimelock
-	modifiedEdge.capacity = maxCapMsat.ToSatoshis()
-	modifiedEdge.hopPayloadSizeFn = hopPayloadSizeFn
+	policyCopy.TimeLockDelta = maxTimelock
+	modifiedEdge := newUnifiedEdge(
+		&policyCopy, maxCapMsat.ToSatoshis(), bestPolicy.inboundFees,
+		hopPayloadSizeFn, bestPolicy.blindedPayment,
+	)
 
-	return &modifiedEdge
+	return modifiedEdge
 }
 
 // minAmt returns the minimum amount that can be forwarded on this connection.
diff --git a/rpcserver.go b/rpcserver.go
index 4cd0fc458..a306d4fd2 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -6971,6 +6971,13 @@ func (r *rpcServer) DecodePayReq(ctx context.Context,
 	// Convert between the `lnrpc` and `routing` types.
 	routeHints := invoicesrpc.CreateRPCRouteHints(payReq.RouteHints)
 
+	blindedPaymentPaths, err := invoicesrpc.CreateRPCBlindedPayments(
+		payReq.BlindedPaymentPaths,
+	)
+	if err != nil {
+		return nil, err
+	}
+
 	var amtSat, amtMsat int64
 	if payReq.MilliSat != nil {
 		amtSat = int64(payReq.MilliSat.ToSatoshis())
@@ -6996,6 +7003,7 @@ func (r *rpcServer) DecodePayReq(ctx context.Context,
 		Expiry:          expiry,
 		CltvExpiry:      int64(payReq.MinFinalCLTVExpiry()),
 		RouteHints:      routeHints,
+		BlindedPaths:    blindedPaymentPaths,
 		PaymentAddr:     paymentAddr,
 		Features:        invoicesrpc.CreateRPCFeatures(payReq.Features),
 	}, nil
diff --git a/zpay32/blinded_path.go b/zpay32/blinded_path.go
new file mode 100644
index 000000000..128a05e4b
--- /dev/null
+++ b/zpay32/blinded_path.go
@@ -0,0 +1,246 @@
+package zpay32
+
+import (
+	"encoding/binary"
+	"fmt"
+	"io"
+	"math"
+
+	"github.com/btcsuite/btcd/btcec/v2"
+	sphinx "github.com/lightningnetwork/lightning-onion"
+	"github.com/lightningnetwork/lnd/lnwire"
+	"github.com/lightningnetwork/lnd/tlv"
+)
+
+const (
+	// relayInfoSize is the number of bytes that the relay info of a blinded
+	// payment will occupy.
+	// 	base fee: 4 bytes
+	//	prop fee: 4 bytes
+	//	cltv delta: 2 bytes
+	//	min htlc: 8 bytes
+	//	max htlc: 8 bytes
+	relayInfoSize = 26
+
+	// maxNumHopsPerPath is the maximum number of blinded path hops that can
+	// be included in a single encoded blinded path. This is calculated
+	// based on the `data_length` limit of 638 bytes for any tagged field in
+	// a BOLT 11 invoice along with the estimated number of bytes required
+	// for encoding the most minimal blinded path hop. See the [bLIP
+	// proposal](https://github.com/lightning/blips/pull/39) for a detailed
+	// calculation.
+	maxNumHopsPerPath = 7
+)
+
+// BlindedPaymentPath holds all the information a payer needs to know about a
+// blinded path to a receiver of a payment.
+type BlindedPaymentPath struct {
+	// FeeBaseMsat is the total base fee for the path in milli-satoshis.
+	FeeBaseMsat uint32
+
+	// FeeRate is the total fee rate for the path in parts per million.
+	FeeRate uint32
+
+	// CltvExpiryDelta is the total CLTV delta to apply to the path.
+	CltvExpiryDelta uint16
+
+	// HTLCMinMsat is the minimum number of milli-satoshis that any hop in
+	// the path will route.
+	HTLCMinMsat uint64
+
+	// HTLCMaxMsat is the maximum number of milli-satoshis that a hop in the
+	// path will route.
+	HTLCMaxMsat uint64
+
+	// Features is the feature bit vector for the path.
+	Features *lnwire.FeatureVector
+
+	// FirstEphemeralBlindingPoint is the blinding point to send to the
+	// introduction node. It will be used by the introduction node to derive
+	// a shared secret with the receiver which can then be used to decode
+	// the encrypted payload from the receiver.
+	FirstEphemeralBlindingPoint *btcec.PublicKey
+
+	// Hops is the blinded path. The first hop is the introduction node and
+	// so the BlindedNodeID of this hop will be the real node ID.
+	Hops []*sphinx.BlindedHopInfo
+}
+
+// DecodeBlindedPayment attempts to parse a BlindedPaymentPath from the passed
+// reader.
+func DecodeBlindedPayment(r io.Reader) (*BlindedPaymentPath, error) {
+	var relayInfo [relayInfoSize]byte
+	n, err := r.Read(relayInfo[:])
+	if err != nil {
+		return nil, err
+	}
+	if n != relayInfoSize {
+		return nil, fmt.Errorf("unable to read %d relay info bytes "+
+			"off of the given stream: %w", relayInfoSize, err)
+	}
+
+	var payment BlindedPaymentPath
+
+	// Parse the relay info fields.
+	payment.FeeBaseMsat = binary.BigEndian.Uint32(relayInfo[:4])
+	payment.FeeRate = binary.BigEndian.Uint32(relayInfo[4:8])
+	payment.CltvExpiryDelta = binary.BigEndian.Uint16(relayInfo[8:10])
+	payment.HTLCMinMsat = binary.BigEndian.Uint64(relayInfo[10:18])
+	payment.HTLCMaxMsat = binary.BigEndian.Uint64(relayInfo[18:])
+
+	// Parse the feature bit vector.
+	f := lnwire.EmptyFeatureVector()
+	err = f.Decode(r)
+	if err != nil {
+		return nil, err
+	}
+	payment.Features = f
+
+	// Parse the first ephemeral blinding point.
+	var blindingPointBytes [btcec.PubKeyBytesLenCompressed]byte
+	_, err = r.Read(blindingPointBytes[:])
+	if err != nil {
+		return nil, err
+	}
+
+	blinding, err := btcec.ParsePubKey(blindingPointBytes[:])
+	if err != nil {
+		return nil, err
+	}
+	payment.FirstEphemeralBlindingPoint = blinding
+
+	// Read the one byte hop number.
+	var numHops [1]byte
+	_, err = r.Read(numHops[:])
+	if err != nil {
+		return nil, err
+	}
+
+	payment.Hops = make([]*sphinx.BlindedHopInfo, int(numHops[0]))
+
+	// Parse each hop.
+	for i := 0; i < len(payment.Hops); i++ {
+		hop, err := DecodeBlindedHop(r)
+		if err != nil {
+			return nil, err
+		}
+
+		payment.Hops[i] = hop
+	}
+
+	return &payment, nil
+}
+
+// Encode serialises the BlindedPaymentPath and writes the bytes to the passed
+// writer.
+// 1) The first 26 bytes contain the relay info:
+//   - Base Fee in msat: uint32 (4 bytes).
+//   - Proportional Fee in PPM: uint32 (4 bytes).
+//   - CLTV expiry delta: uint16 (2 bytes).
+//   - HTLC min msat: uint64 (8 bytes).
+//   - HTLC max msat: uint64 (8 bytes).
+//
+// 2) Feature bit vector length (2 bytes).
+// 3) Feature bit vector (can be zero length).
+// 4) First blinding point: 33 bytes.
+// 5) Number of hops: 1 byte.
+// 6) Encoded BlindedHops.
+func (p *BlindedPaymentPath) Encode(w io.Writer) error {
+	var relayInfo [26]byte
+	binary.BigEndian.PutUint32(relayInfo[:4], p.FeeBaseMsat)
+	binary.BigEndian.PutUint32(relayInfo[4:8], p.FeeRate)
+	binary.BigEndian.PutUint16(relayInfo[8:10], p.CltvExpiryDelta)
+	binary.BigEndian.PutUint64(relayInfo[10:18], p.HTLCMinMsat)
+	binary.BigEndian.PutUint64(relayInfo[18:], p.HTLCMaxMsat)
+
+	_, err := w.Write(relayInfo[:])
+	if err != nil {
+		return err
+	}
+
+	err = p.Features.Encode(w)
+	if err != nil {
+		return err
+	}
+
+	_, err = w.Write(p.FirstEphemeralBlindingPoint.SerializeCompressed())
+	if err != nil {
+		return err
+	}
+
+	numHops := len(p.Hops)
+	if numHops > maxNumHopsPerPath {
+		return fmt.Errorf("the number of hops, %d, exceeds the "+
+			"maximum of %d", numHops, maxNumHopsPerPath)
+	}
+
+	_, err = w.Write([]byte{byte(numHops)})
+	if err != nil {
+		return err
+	}
+
+	for _, hop := range p.Hops {
+		err = EncodeBlindedHop(w, hop)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// DecodeBlindedHop reads a sphinx.BlindedHopInfo from the passed reader.
+func DecodeBlindedHop(r io.Reader) (*sphinx.BlindedHopInfo, error) {
+	var nodeIDBytes [btcec.PubKeyBytesLenCompressed]byte
+	_, err := r.Read(nodeIDBytes[:])
+	if err != nil {
+		return nil, err
+	}
+
+	nodeID, err := btcec.ParsePubKey(nodeIDBytes[:])
+	if err != nil {
+		return nil, err
+	}
+
+	dataLen, err := tlv.ReadVarInt(r, &[8]byte{})
+	if err != nil {
+		return nil, err
+	}
+
+	encryptedData := make([]byte, dataLen)
+	_, err = r.Read(encryptedData)
+	if err != nil {
+		return nil, err
+	}
+
+	return &sphinx.BlindedHopInfo{
+		BlindedNodePub: nodeID,
+		CipherText:     encryptedData,
+	}, nil
+}
+
+// EncodeBlindedHop writes the passed BlindedHopInfo to the given writer.
+//
+// 1) Blinded node pub key: 33 bytes
+// 2) Cipher text length: BigSize
+// 3) Cipher text.
+func EncodeBlindedHop(w io.Writer, hop *sphinx.BlindedHopInfo) error {
+	_, err := w.Write(hop.BlindedNodePub.SerializeCompressed())
+	if err != nil {
+		return err
+	}
+
+	if len(hop.CipherText) > math.MaxUint16 {
+		return fmt.Errorf("encrypted recipient data can not exceed a "+
+			"length of %d bytes", math.MaxUint16)
+	}
+
+	err = tlv.WriteVarInt(w, uint64(len(hop.CipherText)), &[8]byte{})
+	if err != nil {
+		return err
+	}
+
+	_, err = w.Write(hop.CipherText)
+
+	return err
+}
diff --git a/zpay32/decode.go b/zpay32/decode.go
index d37a34cf9..59bfccf00 100644
--- a/zpay32/decode.go
+++ b/zpay32/decode.go
@@ -215,6 +215,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
 			}
 
 			invoice.PaymentHash, err = parse32Bytes(base32Data)
+
 		case fieldTypeS:
 			if invoice.PaymentAddr != nil {
 				// We skip the field if we have already seen a
@@ -223,6 +224,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
 			}
 
 			invoice.PaymentAddr, err = parse32Bytes(base32Data)
+
 		case fieldTypeD:
 			if invoice.Description != nil {
 				// We skip the field if we have already seen a
@@ -231,6 +233,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
 			}
 
 			invoice.Description, err = parseDescription(base32Data)
+
 		case fieldTypeM:
 			if invoice.Metadata != nil {
 				// We skip the field if we have already seen a
@@ -248,6 +251,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
 			}
 
 			invoice.Destination, err = parseDestination(base32Data)
+
 		case fieldTypeH:
 			if invoice.DescriptionHash != nil {
 				// We skip the field if we have already seen a
@@ -256,6 +260,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
 			}
 
 			invoice.DescriptionHash, err = parse32Bytes(base32Data)
+
 		case fieldTypeX:
 			if invoice.expiry != nil {
 				// We skip the field if we have already seen a
@@ -264,6 +269,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
 			}
 
 			invoice.expiry, err = parseExpiry(base32Data)
+
 		case fieldTypeC:
 			if invoice.minFinalCLTVExpiry != nil {
 				// We skip the field if we have already seen a
@@ -271,7 +277,9 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
 				continue
 			}
 
-			invoice.minFinalCLTVExpiry, err = parseMinFinalCLTVExpiry(base32Data)
+			invoice.minFinalCLTVExpiry, err =
+				parseMinFinalCLTVExpiry(base32Data)
+
 		case fieldTypeF:
 			if invoice.FallbackAddr != nil {
 				// We skip the field if we have already seen a
@@ -279,7 +287,10 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
 				continue
 			}
 
-			invoice.FallbackAddr, err = parseFallbackAddr(base32Data, net)
+			invoice.FallbackAddr, err = parseFallbackAddr(
+				base32Data, net,
+			)
+
 		case fieldTypeR:
 			// An `r` field can be included in an invoice multiple
 			// times, so we won't skip it if we have already seen
@@ -289,7 +300,10 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
 				return err
 			}
 
-			invoice.RouteHints = append(invoice.RouteHints, routeHint)
+			invoice.RouteHints = append(
+				invoice.RouteHints, routeHint,
+			)
+
 		case fieldType9:
 			if invoice.Features != nil {
 				// We skip the field if we have already seen a
@@ -298,6 +312,19 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
 			}
 
 			invoice.Features, err = parseFeatures(base32Data)
+
+		case fieldTypeB:
+			blindedPaymentPath, err := parseBlindedPaymentPath(
+				base32Data,
+			)
+			if err != nil {
+				return err
+			}
+
+			invoice.BlindedPaymentPaths = append(
+				invoice.BlindedPaymentPaths, blindedPaymentPath,
+			)
+
 		default:
 			// Ignore unknown type.
 		}
@@ -495,6 +522,17 @@ func parseRouteHint(data []byte) ([]HopHint, error) {
 	return routeHint, nil
 }
 
+// parseBlindedPaymentPath attempts to parse a BlindedPaymentPath from the given
+// byte slice.
+func parseBlindedPaymentPath(data []byte) (*BlindedPaymentPath, error) {
+	base256Data, err := bech32.ConvertBits(data, 5, 8, false)
+	if err != nil {
+		return nil, err
+	}
+
+	return DecodeBlindedPayment(bytes.NewReader(base256Data))
+}
+
 // parseFeatures decodes any feature bits directly from the base32
 // representation.
 func parseFeatures(data []byte) (*lnwire.FeatureVector, error) {
diff --git a/zpay32/encode.go b/zpay32/encode.go
index bf544d062..130abdcfd 100644
--- a/zpay32/encode.go
+++ b/zpay32/encode.go
@@ -260,6 +260,29 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
 		}
 	}
 
+	for _, path := range invoice.BlindedPaymentPaths {
+		var buf bytes.Buffer
+
+		err := path.Encode(&buf)
+		if err != nil {
+			return err
+		}
+
+		blindedPathBase32, err := bech32.ConvertBits(
+			buf.Bytes(), 8, 5, true,
+		)
+		if err != nil {
+			return err
+		}
+
+		err = writeTaggedField(
+			bufferBase32, fieldTypeB, blindedPathBase32,
+		)
+		if err != nil {
+			return err
+		}
+	}
+
 	if invoice.Destination != nil {
 		// Convert 33 byte pubkey to 53 5-bit groups.
 		pubKeyBase32, err := bech32.ConvertBits(
diff --git a/zpay32/invoice.go b/zpay32/invoice.go
index f23992ec9..8b1457ab4 100644
--- a/zpay32/invoice.go
+++ b/zpay32/invoice.go
@@ -76,6 +76,10 @@ const (
 	// probing the recipient.
 	fieldTypeS = 16
 
+	// fieldTypeB contains blinded payment path information. This field may
+	// be repeated to include multiple blinded payment paths in the invoice.
+	fieldTypeB = 20
+
 	// maxInvoiceLength is the maximum total length an invoice can have.
 	// This is chosen to be the maximum number of bytes that can fit into a
 	// single QR code: https://en.wikipedia.org/wiki/QR_code#Storage
@@ -152,6 +156,10 @@ type Invoice struct {
 	// This field is un-exported and can only be read by the
 	// MinFinalCLTVExpiry() method. By forcing callers to read via this
 	// method, we can easily enforce the default if not specified.
+	//
+	// NOTE: this field is ignored in the case that the invoice contains
+	// blinded paths since then the final minimum cltv expiry delta is
+	// expected to be included in the route's accumulated CLTV delta value.
 	minFinalCLTVExpiry *uint64
 
 	// Description is a short description of the purpose of this invoice.
@@ -180,9 +188,17 @@ type Invoice struct {
 	// hint can be individually used to reach the destination. These usually
 	// represent private routes.
 	//
-	// NOTE: This is optional.
+	// NOTE: This is optional and should not be set at the same time as
+	// BlindedPaymentPaths.
 	RouteHints [][]HopHint
 
+	// BlindedPaymentPaths is a set of blinded payment paths that can be
+	// used to find the payment receiver.
+	//
+	// NOTE: This is optional and should not be set at the same time as
+	// RouteHints.
+	BlindedPaymentPaths []*BlindedPaymentPath
+
 	// Features represents an optional field used to signal optional or
 	// required support for features by the receiver.
 	Features *lnwire.FeatureVector
@@ -263,6 +279,15 @@ func RouteHint(routeHint []HopHint) func(*Invoice) {
 	}
 }
 
+// WithBlindedPaymentPath is a functional option that allows a caller of
+// NewInvoice to attach a blinded payment path to the invoice. The option can
+// be used multiple times to attach multiple paths.
+func WithBlindedPaymentPath(p *BlindedPaymentPath) func(*Invoice) {
+	return func(i *Invoice) {
+		i.BlindedPaymentPaths = append(i.BlindedPaymentPaths, p)
+	}
+}
+
 // Features is a functional option that allows callers of NewInvoice to set the
 // desired feature bits that are advertised on the invoice. If this option is
 // not used, an empty feature vector will automatically be populated.
@@ -355,6 +380,13 @@ func validateInvoice(invoice *Invoice) error {
 		return fmt.Errorf("no payment hash found")
 	}
 
+	if len(invoice.RouteHints) != 0 &&
+		len(invoice.BlindedPaymentPaths) != 0 {
+
+		return fmt.Errorf("cannot have both route hints and blinded " +
+			"payment paths")
+	}
+
 	// Either Description or DescriptionHash must be set, not both.
 	if invoice.Description != nil && invoice.DescriptionHash != nil {
 		return fmt.Errorf("both description and description hash set")


Test patch:
diff --git a/htlcswitch/hop/iterator_test.go b/htlcswitch/hop/iterator_test.go
index e69361b0a..9995c71ba 100644
--- a/htlcswitch/hop/iterator_test.go
+++ b/htlcswitch/hop/iterator_test.go
@@ -186,7 +186,7 @@ func TestDecryptAndValidateFwdInfo(t *testing.T) {
 
 	// Encode valid blinding data that we'll fake decrypting for our test.
 	maxCltv := 1000
-	blindedData := record.NewBlindedRouteData(
+	blindedData := record.NewNonFinalBlindedRouteData(
 		lnwire.NewShortChanIDFromInt(1500), nil,
 		record.PaymentRelayInfo{
 			CltvExpiryDelta: 10,
diff --git a/htlcswitch/hop/payload_test.go b/htlcswitch/hop/payload_test.go
index c22144d7b..7398813a3 100644
--- a/htlcswitch/hop/payload_test.go
+++ b/htlcswitch/hop/payload_test.go
@@ -646,7 +646,7 @@ func TestValidateBlindedRouteData(t *testing.T) {
 	}{
 		{
 			name: "max cltv expired",
-			data: record.NewBlindedRouteData(
+			data: record.NewNonFinalBlindedRouteData(
 				scid,
 				nil,
 				record.PaymentRelayInfo{},
@@ -663,7 +663,7 @@ func TestValidateBlindedRouteData(t *testing.T) {
 		},
 		{
 			name: "zero max cltv",
-			data: record.NewBlindedRouteData(
+			data: record.NewNonFinalBlindedRouteData(
 				scid,
 				nil,
 				record.PaymentRelayInfo{},
@@ -682,7 +682,7 @@ func TestValidateBlindedRouteData(t *testing.T) {
 		},
 		{
 			name: "amount below minimum",
-			data: record.NewBlindedRouteData(
+			data: record.NewNonFinalBlindedRouteData(
 				scid,
 				nil,
 				record.PaymentRelayInfo{},
@@ -699,7 +699,7 @@ func TestValidateBlindedRouteData(t *testing.T) {
 		},
 		{
 			name: "valid, no features",
-			data: record.NewBlindedRouteData(
+			data: record.NewNonFinalBlindedRouteData(
 				scid,
 				nil,
 				record.PaymentRelayInfo{},
@@ -714,7 +714,7 @@ func TestValidateBlindedRouteData(t *testing.T) {
 		},
 		{
 			name: "unknown features",
-			data: record.NewBlindedRouteData(
+			data: record.NewNonFinalBlindedRouteData(
 				scid,
 				nil,
 				record.PaymentRelayInfo{},
@@ -738,7 +738,7 @@ func TestValidateBlindedRouteData(t *testing.T) {
 		},
 		{
 			name: "valid data",
-			data: record.NewBlindedRouteData(
+			data: record.NewNonFinalBlindedRouteData(
 				scid,
 				nil,
 				record.PaymentRelayInfo{
diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go
index 5c32a85a3..514e856d8 100644
--- a/itest/lnd_route_blinding_test.go
+++ b/itest/lnd_route_blinding_test.go
@@ -676,7 +676,7 @@ func (b *blindedForwardTest) createBlindedRoute(hops []*forwardingEdge,
 
 		// Encode the route's blinded data and include it in the
 		// blinded hop.
-		payload := record.NewBlindedRouteData(
+		payload := record.NewNonFinalBlindedRouteData(
 			scid, nil, *relayInfo, constraints, nil,
 		)
 		payloadBytes, err := record.EncodeBlindedRouteData(payload)
@@ -739,7 +739,7 @@ func (b *blindedForwardTest) createBlindedRoute(hops []*forwardingEdge,
 		// node ID here so that it _looks like_ a valid
 		// forwarding hop (though in reality it's the last
 		// hop).
-		record.NewBlindedRouteData(
+		record.NewNonFinalBlindedRouteData(
 			lnwire.NewShortChanIDFromInt(100), nil,
 			record.PaymentRelayInfo{}, nil, nil,
 		),
diff --git a/record/blinded_data_test.go b/record/blinded_data_test.go
index f8e95cdcc..88ab9cddd 100644
--- a/record/blinded_data_test.go
+++ b/record/blinded_data_test.go
@@ -101,7 +101,7 @@ func TestBlindedDataEncoding(t *testing.T) {
 				}
 			}
 
-			encodedData := NewBlindedRouteData(
+			encodedData := NewNonFinalBlindedRouteData(
 				channelID, pubkey(t), info, constraints,
 				testCase.features,
 			)
@@ -118,6 +118,127 @@ func TestBlindedDataEncoding(t *testing.T) {
 	}
 }
 
+// TestBlindedDataFinalHopEncoding tests the encoding and decoding of a blinded
+// data blob intended for the final hop of a blinded path where only the pathID
+// will potentially be set.
+func TestBlindedDataFinalHopEncoding(t *testing.T) {
+	tests := []struct {
+		name        string
+		pathID      []byte
+		constraints bool
+	}{
+		{
+			name:   "with path ID",
+			pathID: []byte{1, 2, 3, 4, 5, 6},
+		},
+		{
+			name:   "with no path ID",
+			pathID: nil,
+		},
+		{
+			name:        "with path ID and constraints",
+			pathID:      []byte{1, 2, 3, 4, 5, 6},
+			constraints: true,
+		},
+	}
+
+	for _, test := range tests {
+		test := test
+		t.Run(test.name, func(t *testing.T) {
+			t.Parallel()
+
+			var constraints *PaymentConstraints
+			if test.constraints {
+				constraints = &PaymentConstraints{
+					MaxCltvExpiry:   4,
+					HtlcMinimumMsat: 5,
+				}
+			}
+
+			encodedData := NewFinalHopBlindedRouteData(
+				constraints, test.pathID,
+			)
+
+			encoded, err := EncodeBlindedRouteData(encodedData)
+			require.NoError(t, err)
+
+			b := bytes.NewBuffer(encoded)
+			decodedData, err := DecodeBlindedRouteData(b)
+			require.NoError(t, err)
+
+			require.Equal(t, encodedData, decodedData)
+		})
+	}
+}
+
+// TestBlindedRouteDataPadding tests the PadBy method of BlindedRouteData.
+func TestBlindedRouteDataPadding(t *testing.T) {
+	newBlindedRouteData := func() *BlindedRouteData {
+		channelID := lnwire.NewShortChanIDFromInt(1)
+		info := PaymentRelayInfo{
+			FeeRate:         2,
+			CltvExpiryDelta: 3,
+			BaseFee:         30,
+		}
+
+		constraints := &PaymentConstraints{
+			MaxCltvExpiry:   4,
+			HtlcMinimumMsat: 100,
+		}
+
+		return NewNonFinalBlindedRouteData(
+			channelID, pubkey(t), info, constraints, nil,
+		)
+	}
+
+	tests := []struct {
+		name                 string
+		paddingSize          int
+		expectedSizeIncrease uint64
+	}{
+		{
+			// Calling PadBy with an n value of 0 in the case where
+			// there is not yet a padding field will result in a
+			// zero length TLV entry being added. This will add 2
+			// bytes for the type and length fields.
+			name:                 "no extra padding",
+			expectedSizeIncrease: 2,
+		},
+		{
+			name: "small padding (length " +
+				"field of 1 byte)",
+			paddingSize:          200,
+			expectedSizeIncrease: 202,
+		},
+		{
+			name: "medium padding (length field " +
+				"of 3 bytes)",
+			paddingSize:          256,
+			expectedSizeIncrease: 260,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			data := newBlindedRouteData()
+
+			prePaddingEncoding, err := EncodeBlindedRouteData(data)
+			require.NoError(t, err)
+
+			data.PadBy(test.paddingSize)
+
+			postPaddingEncoding, err := EncodeBlindedRouteData(data)
+			require.NoError(t, err)
+
+			require.EqualValues(
+				t, test.expectedSizeIncrease,
+				len(postPaddingEncoding)-
+					len(prePaddingEncoding),
+			)
+		})
+	}
+}
+
 // TestBlindedRouteVectors tests encoding/decoding of the test vectors for
 // blinded route data provided in the specification.
 //
@@ -131,10 +252,11 @@ func TestBlindingSpecTestVectors(t *testing.T) {
 	tests := []struct {
 		encoded             string
 		expectedPaymentData *BlindedRouteData
+		expectedPadding     int
 	}{
 		{
 			encoded: "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456",
-			expectedPaymentData: NewBlindedRouteData(
+			expectedPaymentData: NewNonFinalBlindedRouteData(
 				lnwire.ShortChannelID{
 					BlockHeight: 0,
 					TxIndex:     0,
@@ -155,10 +277,11 @@ func TestBlindingSpecTestVectors(t *testing.T) {
 					lnwire.Features,
 				),
 			),
+			expectedPadding: 26,
 		},
 		{
 			encoded: "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00",
-			expectedPaymentData: NewBlindedRouteData(
+			expectedPaymentData: NewNonFinalBlindedRouteData(
 				lnwire.ShortChannelID{
 					TxPosition: 1105,
 				},
@@ -175,7 +298,8 @@ func TestBlindingSpecTestVectors(t *testing.T) {
 				lnwire.NewFeatureVector(
 					lnwire.NewRawFeatureVector(),
 					lnwire.Features,
-				)),
+				),
+			),
 		},
 	}
 
@@ -189,6 +313,12 @@ func TestBlindingSpecTestVectors(t *testing.T) {
 			decodedRoute, err := DecodeBlindedRouteData(buff)
 			require.NoError(t, err)
 
+			if test.expectedPadding != 0 {
+				test.expectedPaymentData.PadBy(
+					test.expectedPadding,
+				)
+			}
+
 			require.Equal(
 				t, test.expectedPaymentData, decodedRoute,
 			)
diff --git a/routing/additional_edge_test.go b/routing/additional_edge_test.go
index b5628c6b7..0324e2e10 100644
--- a/routing/additional_edge_test.go
+++ b/routing/additional_edge_test.go
@@ -42,9 +42,13 @@ func TestIntermediatePayloadSize(t *testing.T) {
 			hop: route.Hop{
 				EncryptedData: []byte{12, 13},
 			},
-			edge: &BlindedEdge{
-				cipherText: []byte{12, 13},
-			},
+			edge: &BlindedEdge{blindedPayment: &BlindedPayment{
+				BlindedPath: &sphinx.BlindedPath{
+					BlindedHops: []*sphinx.BlindedHopInfo{
+						{CipherText: []byte{12, 13}},
+					},
+				},
+			}},
 		},
 		{
 			name: "Blinded edge - introduction point",
@@ -52,10 +56,14 @@ func TestIntermediatePayloadSize(t *testing.T) {
 				EncryptedData: []byte{12, 13},
 				BlindingPoint: blindedPoint,
 			},
-			edge: &BlindedEdge{
-				cipherText:    []byte{12, 13},
-				blindingPoint: blindedPoint,
-			},
+			edge: &BlindedEdge{blindedPayment: &BlindedPayment{
+				BlindedPath: &sphinx.BlindedPath{
+					BlindingPoint: blindedPoint,
+					BlindedHops: []*sphinx.BlindedHopInfo{
+						{CipherText: []byte{12, 13}},
+					},
+				},
+			}},
 		},
 	}
 
diff --git a/routing/blinding_test.go b/routing/blinding_test.go
index f6327ecd5..58ad56594 100644
--- a/routing/blinding_test.go
+++ b/routing/blinding_test.go
@@ -128,7 +128,9 @@ func TestBlindedPaymentToHints(t *testing.T) {
 		HtlcMaximum:         htlcMax,
 		Features:            features,
 	}
-	require.Nil(t, blindedPayment.toRouteHints())
+	hints, err := blindedPayment.toRouteHints()
+	require.NoError(t, err)
+	require.Nil(t, hints)
 
 	// Populate the blinded payment with hops.
 	blindedPayment.BlindedPath.BlindedHops = []*sphinx.BlindedHopInfo{
@@ -146,41 +148,43 @@ func TestBlindedPaymentToHints(t *testing.T) {
 		},
 	}
 
+	policy1 := &models.CachedEdgePolicy{
+		TimeLockDelta: cltvDelta,
+		MinHTLC:       lnwire.MilliSatoshi(htlcMin),
+		MaxHTLC:       lnwire.MilliSatoshi(htlcMax),
+		FeeBaseMSat:   lnwire.MilliSatoshi(baseFee),
+		FeeProportionalMillionths: lnwire.MilliSatoshi(
+			ppmFee,
+		),
+		ToNodePubKey: func() route.Vertex {
+			return vb2
+		},
+		ToNodeFeatures: features,
+	}
+	policy2 := &models.CachedEdgePolicy{
+		ToNodePubKey: func() route.Vertex {
+			return vb3
+		},
+		ToNodeFeatures: features,
+	}
+
+	blindedEdge1, err := NewBlindedEdge(policy1, blindedPayment, 0)
+	require.NoError(t, err)
+
+	blindedEdge2, err := NewBlindedEdge(policy2, blindedPayment, 1)
+	require.NoError(t, err)
+
 	expected := RouteHints{
 		v1: {
-			//nolint:lll
-			&BlindedEdge{
-				policy: &models.CachedEdgePolicy{
-					TimeLockDelta: cltvDelta,
-					MinHTLC:       lnwire.MilliSatoshi(htlcMin),
-					MaxHTLC:       lnwire.MilliSatoshi(htlcMax),
-					FeeBaseMSat:   lnwire.MilliSatoshi(baseFee),
-					FeeProportionalMillionths: lnwire.MilliSatoshi(
-						ppmFee,
-					),
-					ToNodePubKey: func() route.Vertex {
-						return vb2
-					},
-					ToNodeFeatures: features,
-				},
-				blindingPoint: blindedPoint,
-				cipherText:    cipherText,
-			},
+			blindedEdge1,
 		},
 		vb2: {
-			&BlindedEdge{
-				policy: &models.CachedEdgePolicy{
-					ToNodePubKey: func() route.Vertex {
-						return vb3
-					},
-					ToNodeFeatures: features,
-				},
-				cipherText: cipherText,
-			},
+			blindedEdge2,
 		},
 	}
 
-	actual := blindedPayment.toRouteHints()
+	actual, err := blindedPayment.toRouteHints()
+	require.NoError(t, err)
 
 	require.Equal(t, len(expected), len(actual))
 	for vertex, expectedHint := range expected {
diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go
index 06005716f..0f2a2659b 100644
--- a/routing/pathfind_test.go
+++ b/routing/pathfind_test.go
@@ -3322,16 +3322,19 @@ func TestBlindedRouteConstruction(t *testing.T) {
 			ToNodeFeatures: tlvFeatures,
 		}
 
-		// Create final hop parameters for payment amount = 110. Note
-		// that final cltv delta is not set because blinded paths
-		// include this final delta in their aggregate delta. A
-		// sender-set delta may be added to account for block arrival
-		// during payment, but we do not set it in this test.
+		// Create final hop parameters for payment amount = 110.
 		totalAmt       lnwire.MilliSatoshi = 110
 		finalHopParams                     = finalHopParams{
 			amt:      totalAmt,
 			totalAmt: totalAmt,
 			metadata: metadata,
+
+			// We set a CLTV delta here just to test that this will
+			// be ignored by newRoute since this is a blinded path
+			// where the accumulated CLTV delta for the route
+			// communicated in the blinded path should be assumed to
+			// include the CLTV delta of the final hop.
+			cltvDelta: MaxCLTVDelta,
 		}
 	)
 
@@ -3341,7 +3344,9 @@ func TestBlindedRouteConstruction(t *testing.T) {
 	// that make up the graph we'll give to route construction. The hints
 	// map is keyed by source node, so we can retrieve our blinded edges
 	// accordingly.
-	blindedEdges := blindedPayment.toRouteHints()
+	blindedEdges, err := blindedPayment.toRouteHints()
+	require.NoError(t, err)
+
 	carolDaveEdge := blindedEdges[carolVertex][0]
 	daveEveEdge := blindedEdges[daveBlindedVertex][0]
 
diff --git a/routing/unified_edges_test.go b/routing/unified_edges_test.go
index 7b1650c02..82605e9b3 100644
--- a/routing/unified_edges_test.go
+++ b/routing/unified_edges_test.go
@@ -59,37 +59,37 @@ func TestNodeEdgeUnifier(t *testing.T) {
 	unifierFilled := newNodeEdgeUnifier(source, toNode, false, nil)
 
 	unifierFilled.addPolicy(
-		fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize,
+		fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, nil,
 	)
 	unifierFilled.addPolicy(
-		fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize,
+		fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize, nil,
 	)
 
 	unifierNoCapacity := newNodeEdgeUnifier(source, toNode, false, nil)
 	unifierNoCapacity.addPolicy(
-		fromNode, &p1, inboundFee1, 0, defaultHopPayloadSize,
+		fromNode, &p1, inboundFee1, 0, defaultHopPayloadSize, nil,
 	)
 	unifierNoCapacity.addPolicy(
-		fromNode, &p2, inboundFee2, 0, defaultHopPayloadSize,
+		fromNode, &p2, inboundFee2, 0, defaultHopPayloadSize, nil,
 	)
 
 	unifierNoInfo := newNodeEdgeUnifier(source, toNode, false, nil)
 	unifierNoInfo.addPolicy(
 		fromNode, &models.CachedEdgePolicy{}, models.InboundFee{},
-		0, defaultHopPayloadSize,
+		0, defaultHopPayloadSize, nil,
 	)
 
 	unifierInboundFee := newNodeEdgeUnifier(source, toNode, true, nil)
 	unifierInboundFee.addPolicy(
-		fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize,
+		fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, nil,
 	)
 	unifierInboundFee.addPolicy(
-		fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize,
+		fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize, nil,
 	)
 
 	unifierLocal := newNodeEdgeUnifier(fromNode, toNode, true, nil)
 	unifierLocal.addPolicy(
-		fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize,
+		fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, nil,
 	)
 
 	inboundFeeZero := models.InboundFee{}
@@ -98,10 +98,11 @@ func TestNodeEdgeUnifier(t *testing.T) {
 	}
 	unifierNegInboundFee := newNodeEdgeUnifier(source, toNode, true, nil)
 	unifierNegInboundFee.addPolicy(
-		fromNode, &p1, inboundFeeZero, c1, defaultHopPayloadSize,
+		fromNode, &p1, inboundFeeZero, c1, defaultHopPayloadSize, nil,
 	)
 	unifierNegInboundFee.addPolicy(
 		fromNode, &p2, inboundFeeNegative, c2, defaultHopPayloadSize,
+		nil,
 	)
 
 	tests := []struct {
diff --git a/zpay32/invoice_test.go b/zpay32/invoice_test.go
index a360ed2a0..006b4fb6d 100644
--- a/zpay32/invoice_test.go
+++ b/zpay32/invoice_test.go
@@ -7,7 +7,6 @@ import (
 	"bytes"
 	"encoding/hex"
 	"fmt"
-	"reflect"
 	"strings"
 	"testing"
 	"time"
@@ -17,7 +16,9 @@ import (
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/chaincfg"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
+	sphinx "github.com/lightningnetwork/lightning-onion"
 	"github.com/lightningnetwork/lnd/lnwire"
+	"github.com/stretchr/testify/require"
 )
 
 var (
@@ -116,6 +117,62 @@ var (
 
 	// Must be initialized in init().
 	testDescriptionHash [32]byte
+
+	testBlindedPK1Bytes, _ = hex.DecodeString("03f3311e948feb5115242c4e39" +
+		"6c81c448ab7ee5fd24c4e24e66c73533cc4f98b8")
+	testBlindedHopPK1, _   = btcec.ParsePubKey(testBlindedPK1Bytes)
+	testBlindedPK2Bytes, _ = hex.DecodeString("03a8c97ed5cd40d474e4ef18c8" +
+		"99854b25e5070106504cb225e6d2c112d61a805e")
+	testBlindedHopPK2, _   = btcec.ParsePubKey(testBlindedPK2Bytes)
+	testBlindedPK3Bytes, _ = hex.DecodeString("0220293926219d8efe733336e2" +
+		"b674570dd96aa763acb3564e6e367b384d861a0a")
+	testBlindedHopPK3, _   = btcec.ParsePubKey(testBlindedPK3Bytes)
+	testBlindedPK4Bytes, _ = hex.DecodeString("02c75eb336a038294eaaf76015" +
+		"8b2e851c3c0937262e35401ae64a1bee71a2e40c")
+	testBlindedHopPK4, _ = btcec.ParsePubKey(testBlindedPK4Bytes)
+
+	blindedPath1 = &BlindedPaymentPath{
+		FeeBaseMsat:                 40,
+		FeeRate:                     20,
+		CltvExpiryDelta:             130,
+		HTLCMinMsat:                 2,
+		HTLCMaxMsat:                 100,
+		Features:                    lnwire.EmptyFeatureVector(),
+		FirstEphemeralBlindingPoint: testBlindedHopPK1,
+		Hops: []*sphinx.BlindedHopInfo{
+			{
+				BlindedNodePub: testBlindedHopPK2,
+				CipherText:     []byte{1, 2, 3, 4, 5},
+			},
+			{
+				BlindedNodePub: testBlindedHopPK3,
+				CipherText:     []byte{5, 4, 3, 2, 1},
+			},
+			{
+				BlindedNodePub: testBlindedHopPK4,
+				CipherText: []byte{
+					1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+					11, 12, 13, 14,
+				},
+			},
+		},
+	}
+
+	blindedPath2 = &BlindedPaymentPath{
+		FeeBaseMsat:                 4,
+		FeeRate:                     2,
+		CltvExpiryDelta:             10,
+		HTLCMinMsat:                 0,
+		HTLCMaxMsat:                 10,
+		Features:                    lnwire.EmptyFeatureVector(),
+		FirstEphemeralBlindingPoint: testBlindedHopPK4,
+		Hops: []*sphinx.BlindedHopInfo{
+			{
+				BlindedNodePub: testBlindedHopPK3,
+				CipherText:     []byte{1, 2, 3, 4, 5},
+			},
+		},
+	}
 )
 
 func init() {
@@ -125,6 +182,8 @@ func init() {
 // TestDecodeEncode tests that an encoded invoice gets decoded into the expected
 // Invoice object, and that reencoding the decoded invoice gets us back to the
 // original encoded string.
+//
+//nolint:lll
 func TestDecodeEncode(t *testing.T) {
 	t.Parallel()
 
@@ -673,52 +732,77 @@ func TestDecodeEncode(t *testing.T) {
 				i.Destination = nil
 			},
 		},
+		{
+			// Invoice with blinded payment paths.
+			encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4js5fdqqqqq2qqqqqpgqyzqqqqqqqqqqqqyqqqqqqqqqqqvsqqqqlnxy0ffrlt2y2jgtzw89kgr3zg4dlwtlfycn3yuek8x5eucnuchqps82xf0m2u6sx5wnjw7xxgnxz5kf09quqsv5zvkgj7d5kpzttp4qz7q5qsyqcyq5pzq2feycsemrh7wvendc4kw3tsmkt25a36ev6kfehrv7ecfkrp5zs9q5zqxqspqtr4avek5quzjn427asptzews5wrczfhychr2sq6ue9phmn35tjqcrspqgpsgpgxquyqjzstpsxsu59zqqqqqpqqqqqqyqq2qqqqqqqqqqqqqqqqqqqqqqqqpgqqqqk8t6endgpc99824amqzk9japgu8synwf3wx4qp4ej2r0h8rghypsqsygpf8ynzr8vwleenxdhzke69wrwed2nk8t9n2e8xudnm8pxcvxs2q5qsyqcyq5y4rdlhtf84f8rgdj34275juwls2ftxtcfh035863q3p9k6s94hpxhdmzfn5gxpsazdznxs56j4vt3fdhe00g9v2l3szher50hp4xlggqkxf77f",
+			valid:          true,
+			decodedInvoice: func() *Invoice {
+				return &Invoice{
+					Net:         &chaincfg.MainNetParams,
+					MilliSat:    &testMillisat20mBTC,
+					Timestamp:   time.Unix(1496314658, 0),
+					PaymentHash: &testPaymentHash,
+					Description: &testCupOfCoffee,
+					Destination: testPubKey,
+					Features:    emptyFeatures,
+					BlindedPaymentPaths: []*BlindedPaymentPath{
+						blindedPath1,
+						blindedPath2,
+					},
+				}
+			},
+			beforeEncoding: func(i *Invoice) {
+				// Since this destination pubkey was recovered
+				// from the signature, we must set it nil before
+				// encoding to get back the same invoice string.
+				i.Destination = nil
+			},
+		},
 	}
 
 	for i, test := range tests {
-		var decodedInvoice *Invoice
-		net := &chaincfg.MainNetParams
-		if test.decodedInvoice != nil {
-			decodedInvoice = test.decodedInvoice()
-			net = decodedInvoice.Net
-		}
+		test := test
 
-		invoice, err := Decode(test.encodedInvoice, net)
-		if (err == nil) != test.valid {
-			t.Errorf("Decoding test %d failed: %v", i, err)
-			return
-		}
+		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+			t.Parallel()
 
-		if test.valid {
-			if err := compareInvoices(decodedInvoice, invoice); err != nil {
-				t.Errorf("Invoice decoding result %d not as expected: %v", i, err)
+			var decodedInvoice *Invoice
+			net := &chaincfg.MainNetParams
+			if test.decodedInvoice != nil {
+				decodedInvoice = test.decodedInvoice()
+				net = decodedInvoice.Net
+			}
+
+			invoice, err := Decode(test.encodedInvoice, net)
+			if !test.valid {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+				require.Equal(t, decodedInvoice, invoice)
+			}
+
+			if test.skipEncoding {
 				return
 			}
-		}
 
-		if test.skipEncoding {
-			continue
-		}
+			if test.beforeEncoding != nil {
+				test.beforeEncoding(decodedInvoice)
+			}
 
-		if test.beforeEncoding != nil {
-			test.beforeEncoding(decodedInvoice)
-		}
+			if decodedInvoice == nil {
+				return
+			}
 
-		if decodedInvoice != nil {
 			reencoded, err := decodedInvoice.Encode(
 				testMessageSigner,
 			)
-			if (err == nil) != test.valid {
-				t.Errorf("Encoding test %d failed: %v", i, err)
+			if !test.valid {
+				require.Error(t, err)
 				return
 			}
-
-			if test.valid && test.encodedInvoice != reencoded {
-				t.Errorf("Encoding %d failed, expected %v, got %v",
-					i, test.encodedInvoice, reencoded)
-				return
-			}
-		}
+			require.NoError(t, err)
+			require.Equal(t, test.encodedInvoice, reencoded)
+		})
 	}
 }
 
@@ -805,25 +889,42 @@ func TestNewInvoice(t *testing.T) {
 			valid:          true,
 			encodedInvoice: "lnbcrt241pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66df5c8pqjjt4z4ymmuaxfx8eh5v7hmzs3wrfas8m2sz5qz56rw2lxy8mmgm4xln0ha26qkw6u3vhu22pss2udugr9g74c3x20slpcqjgq0el4h6",
 		},
+		{
+			// Mainnet invoice with two blinded paths.
+			newInvoice: func() (*Invoice, error) {
+				return NewInvoice(&chaincfg.MainNetParams,
+					testPaymentHash,
+					time.Unix(1496314658, 0),
+					Amount(testMillisat20mBTC),
+					Description(testCupOfCoffee),
+					WithBlindedPaymentPath(blindedPath1),
+					WithBlindedPaymentPath(blindedPath2),
+				)
+			},
+			valid: true,
+			//nolint:lll
+			encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4js5fdqqqqq2qqqqqpgqyzqqqqqqqqqqqqyqqqqqqqqqqqvsqqqqlnxy0ffrlt2y2jgtzw89kgr3zg4dlwtlfycn3yuek8x5eucnuchqps82xf0m2u6sx5wnjw7xxgnxz5kf09quqsv5zvkgj7d5kpzttp4qz7q5qsyqcyq5pzq2feycsemrh7wvendc4kw3tsmkt25a36ev6kfehrv7ecfkrp5zs9q5zqxqspqtr4avek5quzjn427asptzews5wrczfhychr2sq6ue9phmn35tjqcrspqgpsgpgxquyqjzstpsxsu59zqqqqqpqqqqqqyqq2qqqqqqqqqqqqqqqqqqqqqqqqpgqqqqk8t6endgpc99824amqzk9japgu8synwf3wx4qp4ej2r0h8rghypsqsygpf8ynzr8vwleenxdhzke69wrwed2nk8t9n2e8xudnm8pxcvxs2q5qsyqcyq5y4rdlhtf84f8rgdj34275juwls2ftxtcfh035863q3p9k6s94hpxhdmzfn5gxpsazdznxs56j4vt3fdhe00g9v2l3szher50hp4xlggqkxf77f",
+		},
 	}
 
 	for i, test := range tests {
+		test := test
 
-		invoice, err := test.newInvoice()
-		if err != nil && !test.valid {
-			continue
-		}
-		encoded, err := invoice.Encode(testMessageSigner)
-		if (err == nil) != test.valid {
-			t.Errorf("NewInvoice test %d failed: %v", i, err)
-			return
-		}
+		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+			t.Parallel()
 
-		if test.valid && test.encodedInvoice != encoded {
-			t.Errorf("Encoding %d failed, expected %v, got %v",
-				i, test.encodedInvoice, encoded)
-			return
-		}
+			invoice, err := test.newInvoice()
+			if !test.valid {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+
+			encoded, err := invoice.Encode(testMessageSigner)
+			require.NoError(t, err)
+
+			require.Equal(t, test.encodedInvoice, encoded)
+		})
 	}
 }
 
@@ -909,73 +1010,6 @@ func TestInvoiceChecksumMalleability(t *testing.T) {
 	}
 }
 
-func compareInvoices(expected, actual *Invoice) error {
-	if !reflect.DeepEqual(expected.Net, actual.Net) {
-		return fmt.Errorf("expected net %v, got %v",
-			expected.Net, actual.Net)
-	}
-
-	if !reflect.DeepEqual(expected.MilliSat, actual.MilliSat) {
-		return fmt.Errorf("expected milli sat %d, got %d",
-			*expected.MilliSat, *actual.MilliSat)
-	}
-
-	if expected.Timestamp != actual.Timestamp {
-		return fmt.Errorf("expected timestamp %v, got %v",
-			expected.Timestamp, actual.Timestamp)
-	}
-
-	if !compareHashes(expected.PaymentHash, actual.PaymentHash) {
-		return fmt.Errorf("expected payment hash %x, got %x",
-			*expected.PaymentHash, *actual.PaymentHash)
-	}
-
-	if !reflect.DeepEqual(expected.Description, actual.Description) {
-		return fmt.Errorf("expected description \"%s\", got \"%s\"",
-			*expected.Description, *actual.Description)
-	}
-
-	if !comparePubkeys(expected.Destination, actual.Destination) {
-		return fmt.Errorf("expected destination pubkey %x, got %x",
-			expected.Destination.SerializeCompressed(),
-			actual.Destination.SerializeCompressed())
-	}
-
-	if !compareHashes(expected.DescriptionHash, actual.DescriptionHash) {
-		return fmt.Errorf("expected description hash %x, got %x",
-			*expected.DescriptionHash, *actual.DescriptionHash)
-	}
-
-	if expected.Expiry() != actual.Expiry() {
-		return fmt.Errorf("expected expiry %d, got %d",
-			expected.Expiry(), actual.Expiry())
-	}
-
-	if !reflect.DeepEqual(expected.FallbackAddr, actual.FallbackAddr) {
-		return fmt.Errorf("expected FallbackAddr %v, got %v",
-			expected.FallbackAddr, actual.FallbackAddr)
-	}
-
-	if len(expected.RouteHints) != len(actual.RouteHints) {
-		return fmt.Errorf("expected %d RouteHints, got %d",
-			len(expected.RouteHints), len(actual.RouteHints))
-	}
-
-	for i, routeHint := range expected.RouteHints {
-		err := compareRouteHints(routeHint, actual.RouteHints[i])
-		if err != nil {
-			return err
-		}
-	}
-
-	if !reflect.DeepEqual(expected.Features, actual.Features) {
-		return fmt.Errorf("expected features %v, got %v",
-			expected.Features, actual.Features)
-	}
-
-	return nil
-}
-
 func comparePubkeys(a, b *btcec.PublicKey) bool {
 	if a == b {
 		return true